Skip to content

Commit 64a1513

Browse files
emilioaccclaude
andauthored
feat: Add worker info command to display worker details (#31)
* feat: Add worker info command to display worker details - Add workerInfoCommand() function that calls get_worker_info tool - Display worker name, URL, creation/modification timestamps - Format bindings output: databases, storage, analytics, env vars, secrets - Add 'info' case to handleWorkerCommand() switch - Update help text and error messages to include info command ATXP-1430 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: trigger CI * test: add tests for workerInfoCommand Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: correct process.exit mocking in workerInfoCommand tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: trigger CI * chore: trigger CI build --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 39b4403 commit 64a1513

3 files changed

Lines changed: 194 additions & 1 deletion

File tree

packages/atxp/src/commands/paas/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
workerListCommand,
55
workerLogsCommand,
66
workerDeleteCommand,
7+
workerInfoCommand,
78
} from './worker.js';
89
import {
910
dbCreateCommand,
@@ -163,6 +164,15 @@ async function handleWorkerCommand(
163164
await workerListCommand();
164165
break;
165166

167+
case 'info':
168+
if (!name) {
169+
console.error(chalk.red('Error: Worker name is required'));
170+
console.log(`Usage: ${chalk.cyan('npx atxp paas worker info <name>')}`);
171+
process.exit(1);
172+
}
173+
await workerInfoCommand(name);
174+
break;
175+
166176
case 'logs':
167177
if (!name) {
168178
console.error(chalk.red('Error: Worker name is required'));
@@ -189,7 +199,7 @@ async function handleWorkerCommand(
189199

190200
default:
191201
console.error(chalk.red(`Unknown worker command: ${subCommand}`));
192-
console.log('Available commands: deploy, list, logs, delete');
202+
console.log('Available commands: deploy, list, info, logs, delete');
193203
process.exit(1);
194204
}
195205
}

packages/atxp/src/commands/paas/worker.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
workerListCommand,
2121
workerLogsCommand,
2222
workerDeleteCommand,
23+
workerInfoCommand,
2324
parseEnvArg,
2425
parseEnvFile,
2526
validateEnvVarName,
@@ -689,4 +690,96 @@ describe('Worker Commands', () => {
689690
});
690691
});
691692
});
693+
694+
describe('workerInfoCommand', () => {
695+
it('should get worker info and display details', async () => {
696+
const mockResponse = JSON.stringify({
697+
success: true,
698+
worker: {
699+
name: 'my-worker',
700+
url: 'https://my-worker.example.com',
701+
createdOn: '2024-01-01T00:00:00Z',
702+
modifiedOn: '2024-01-02T00:00:00Z',
703+
bindings: {
704+
databases: [],
705+
storage: [],
706+
analytics: [],
707+
envVars: [],
708+
secrets: [],
709+
},
710+
},
711+
});
712+
vi.mocked(callTool).mockResolvedValue(mockResponse);
713+
714+
await workerInfoCommand('my-worker');
715+
716+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'get_worker_info', {
717+
name: 'my-worker',
718+
});
719+
expect(console.log).toHaveBeenCalled();
720+
});
721+
722+
it('should display all binding types when present', async () => {
723+
const mockResponse = JSON.stringify({
724+
success: true,
725+
worker: {
726+
name: 'my-worker',
727+
url: 'https://my-worker.example.com',
728+
createdOn: '2024-01-01T00:00:00Z',
729+
modifiedOn: '2024-01-02T00:00:00Z',
730+
bindings: {
731+
databases: [{ binding: 'DB', databaseId: 'db-123' }],
732+
storage: [{ binding: 'BUCKET', bucketName: 'my-bucket' }],
733+
analytics: [{ binding: 'ANALYTICS', dataset: 'my-dataset' }],
734+
envVars: ['API_URL', 'DEBUG'],
735+
secrets: ['API_KEY'],
736+
},
737+
},
738+
});
739+
vi.mocked(callTool).mockResolvedValue(mockResponse);
740+
741+
await workerInfoCommand('my-worker');
742+
743+
expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'get_worker_info', {
744+
name: 'my-worker',
745+
});
746+
expect(console.log).toHaveBeenCalled();
747+
});
748+
749+
it('should call process.exit when worker is not found', async () => {
750+
const mockResponse = JSON.stringify({
751+
success: false,
752+
error: 'Worker not found',
753+
});
754+
vi.mocked(callTool).mockResolvedValue(mockResponse);
755+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
756+
757+
await workerInfoCommand('nonexistent-worker');
758+
759+
expect(console.error).toHaveBeenCalled();
760+
expect(exitSpy).toHaveBeenCalledWith(1);
761+
});
762+
763+
it('should handle error response without error message', async () => {
764+
const mockResponse = JSON.stringify({
765+
success: false,
766+
});
767+
vi.mocked(callTool).mockResolvedValue(mockResponse);
768+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
769+
770+
await workerInfoCommand('my-worker');
771+
772+
expect(console.error).toHaveBeenCalled();
773+
expect(exitSpy).toHaveBeenCalledWith(1);
774+
});
775+
776+
it('should fallback to raw output when JSON parsing fails', async () => {
777+
vi.mocked(callTool).mockResolvedValue('not valid json');
778+
779+
await workerInfoCommand('my-worker');
780+
781+
expect(console.log).toHaveBeenCalledWith('not valid json');
782+
});
783+
});
692784
});
785+

packages/atxp/src/commands/paas/worker.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,93 @@ export async function workerDeleteCommand(name: string): Promise<void> {
391391
const result = await callTool(SERVER, 'delete_worker', { name });
392392
console.log(result);
393393
}
394+
395+
interface WorkerInfoResponse {
396+
success: boolean;
397+
error?: string;
398+
worker?: {
399+
name: string;
400+
url: string;
401+
createdOn: string;
402+
modifiedOn: string;
403+
bindings: {
404+
databases: Array<{ binding: string; databaseId: string }>;
405+
storage: Array<{ binding: string; bucketName: string }>;
406+
analytics: Array<{ binding: string; dataset: string }>;
407+
envVars: string[];
408+
secrets: string[];
409+
};
410+
};
411+
}
412+
413+
export async function workerInfoCommand(name: string): Promise<void> {
414+
const result = await callTool(SERVER, 'get_worker_info', { name });
415+
416+
try {
417+
const data = JSON.parse(result) as WorkerInfoResponse;
418+
419+
if (!data.success || !data.worker) {
420+
console.error(chalk.red(`Error: ${data.error || 'Worker not found'}`));
421+
process.exit(1);
422+
}
423+
424+
const worker = data.worker;
425+
426+
console.log(chalk.bold(`Worker: ${worker.name}`));
427+
console.log();
428+
console.log(`${chalk.gray('URL:')} ${worker.url}`);
429+
console.log(`${chalk.gray('Created:')} ${worker.createdOn}`);
430+
console.log(`${chalk.gray('Modified:')} ${worker.modifiedOn}`);
431+
432+
const { bindings } = worker;
433+
const hasBindings =
434+
bindings.databases.length > 0 ||
435+
bindings.storage.length > 0 ||
436+
bindings.analytics.length > 0 ||
437+
bindings.envVars.length > 0 ||
438+
bindings.secrets.length > 0;
439+
440+
if (hasBindings) {
441+
console.log();
442+
console.log(chalk.bold('Bindings:'));
443+
444+
if (bindings.databases.length > 0) {
445+
console.log(` ${chalk.cyan('Databases:')}`);
446+
for (const db of bindings.databases) {
447+
console.log(` ${db.binding} -> ${chalk.gray(db.databaseId)}`);
448+
}
449+
}
450+
451+
if (bindings.storage.length > 0) {
452+
console.log(` ${chalk.cyan('Storage:')}`);
453+
for (const bucket of bindings.storage) {
454+
console.log(` ${bucket.binding} -> ${chalk.gray(bucket.bucketName)}`);
455+
}
456+
}
457+
458+
if (bindings.analytics.length > 0) {
459+
console.log(` ${chalk.cyan('Analytics:')}`);
460+
for (const analytics of bindings.analytics) {
461+
console.log(` ${analytics.binding} -> ${chalk.gray(analytics.dataset)}`);
462+
}
463+
}
464+
465+
if (bindings.envVars.length > 0) {
466+
console.log(` ${chalk.cyan('Environment Variables:')}`);
467+
for (const envVar of bindings.envVars) {
468+
console.log(` ${envVar}`);
469+
}
470+
}
471+
472+
if (bindings.secrets.length > 0) {
473+
console.log(` ${chalk.cyan('Secrets:')}`);
474+
for (const secret of bindings.secrets) {
475+
console.log(` ${secret}`);
476+
}
477+
}
478+
}
479+
} catch {
480+
// If parsing fails, just output the raw result
481+
console.log(result);
482+
}
483+
}

0 commit comments

Comments
 (0)