diff --git a/packages/atxp/src/commands/paas/index.ts b/packages/atxp/src/commands/paas/index.ts index 7462a94..ac763e8 100644 --- a/packages/atxp/src/commands/paas/index.ts +++ b/packages/atxp/src/commands/paas/index.ts @@ -4,6 +4,7 @@ import { workerListCommand, workerLogsCommand, workerDeleteCommand, + workerInfoCommand, } from './worker.js'; import { dbCreateCommand, @@ -163,6 +164,15 @@ async function handleWorkerCommand( await workerListCommand(); break; + case 'info': + if (!name) { + console.error(chalk.red('Error: Worker name is required')); + console.log(`Usage: ${chalk.cyan('npx atxp paas worker info ')}`); + process.exit(1); + } + await workerInfoCommand(name); + break; + case 'logs': if (!name) { console.error(chalk.red('Error: Worker name is required')); @@ -189,7 +199,7 @@ async function handleWorkerCommand( default: console.error(chalk.red(`Unknown worker command: ${subCommand}`)); - console.log('Available commands: deploy, list, logs, delete'); + console.log('Available commands: deploy, list, info, logs, delete'); process.exit(1); } } diff --git a/packages/atxp/src/commands/paas/worker.test.ts b/packages/atxp/src/commands/paas/worker.test.ts index 8d984ec..8761eb6 100644 --- a/packages/atxp/src/commands/paas/worker.test.ts +++ b/packages/atxp/src/commands/paas/worker.test.ts @@ -20,6 +20,7 @@ import { workerListCommand, workerLogsCommand, workerDeleteCommand, + workerInfoCommand, parseEnvArg, parseEnvFile, validateEnvVarName, @@ -689,4 +690,96 @@ describe('Worker Commands', () => { }); }); }); + + describe('workerInfoCommand', () => { + it('should get worker info and display details', async () => { + const mockResponse = JSON.stringify({ + success: true, + worker: { + name: 'my-worker', + url: 'https://my-worker.example.com', + createdOn: '2024-01-01T00:00:00Z', + modifiedOn: '2024-01-02T00:00:00Z', + bindings: { + databases: [], + storage: [], + analytics: [], + envVars: [], + secrets: [], + }, + }, + }); + vi.mocked(callTool).mockResolvedValue(mockResponse); + + await workerInfoCommand('my-worker'); + + expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'get_worker_info', { + name: 'my-worker', + }); + expect(console.log).toHaveBeenCalled(); + }); + + it('should display all binding types when present', async () => { + const mockResponse = JSON.stringify({ + success: true, + worker: { + name: 'my-worker', + url: 'https://my-worker.example.com', + createdOn: '2024-01-01T00:00:00Z', + modifiedOn: '2024-01-02T00:00:00Z', + bindings: { + databases: [{ binding: 'DB', databaseId: 'db-123' }], + storage: [{ binding: 'BUCKET', bucketName: 'my-bucket' }], + analytics: [{ binding: 'ANALYTICS', dataset: 'my-dataset' }], + envVars: ['API_URL', 'DEBUG'], + secrets: ['API_KEY'], + }, + }, + }); + vi.mocked(callTool).mockResolvedValue(mockResponse); + + await workerInfoCommand('my-worker'); + + expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'get_worker_info', { + name: 'my-worker', + }); + expect(console.log).toHaveBeenCalled(); + }); + + it('should call process.exit when worker is not found', async () => { + const mockResponse = JSON.stringify({ + success: false, + error: 'Worker not found', + }); + vi.mocked(callTool).mockResolvedValue(mockResponse); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + + await workerInfoCommand('nonexistent-worker'); + + expect(console.error).toHaveBeenCalled(); + expect(exitSpy).toHaveBeenCalledWith(1); + }); + + it('should handle error response without error message', async () => { + const mockResponse = JSON.stringify({ + success: false, + }); + vi.mocked(callTool).mockResolvedValue(mockResponse); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + + await workerInfoCommand('my-worker'); + + expect(console.error).toHaveBeenCalled(); + expect(exitSpy).toHaveBeenCalledWith(1); + }); + + it('should fallback to raw output when JSON parsing fails', async () => { + vi.mocked(callTool).mockResolvedValue('not valid json'); + + await workerInfoCommand('my-worker'); + + expect(console.log).toHaveBeenCalledWith('not valid json'); + }); + }); }); + diff --git a/packages/atxp/src/commands/paas/worker.ts b/packages/atxp/src/commands/paas/worker.ts index 727d36f..e6e7694 100644 --- a/packages/atxp/src/commands/paas/worker.ts +++ b/packages/atxp/src/commands/paas/worker.ts @@ -391,3 +391,93 @@ export async function workerDeleteCommand(name: string): Promise { const result = await callTool(SERVER, 'delete_worker', { name }); console.log(result); } + +interface WorkerInfoResponse { + success: boolean; + error?: string; + worker?: { + name: string; + url: string; + createdOn: string; + modifiedOn: string; + bindings: { + databases: Array<{ binding: string; databaseId: string }>; + storage: Array<{ binding: string; bucketName: string }>; + analytics: Array<{ binding: string; dataset: string }>; + envVars: string[]; + secrets: string[]; + }; + }; +} + +export async function workerInfoCommand(name: string): Promise { + const result = await callTool(SERVER, 'get_worker_info', { name }); + + try { + const data = JSON.parse(result) as WorkerInfoResponse; + + if (!data.success || !data.worker) { + console.error(chalk.red(`Error: ${data.error || 'Worker not found'}`)); + process.exit(1); + } + + const worker = data.worker; + + console.log(chalk.bold(`Worker: ${worker.name}`)); + console.log(); + console.log(`${chalk.gray('URL:')} ${worker.url}`); + console.log(`${chalk.gray('Created:')} ${worker.createdOn}`); + console.log(`${chalk.gray('Modified:')} ${worker.modifiedOn}`); + + const { bindings } = worker; + const hasBindings = + bindings.databases.length > 0 || + bindings.storage.length > 0 || + bindings.analytics.length > 0 || + bindings.envVars.length > 0 || + bindings.secrets.length > 0; + + if (hasBindings) { + console.log(); + console.log(chalk.bold('Bindings:')); + + if (bindings.databases.length > 0) { + console.log(` ${chalk.cyan('Databases:')}`); + for (const db of bindings.databases) { + console.log(` ${db.binding} -> ${chalk.gray(db.databaseId)}`); + } + } + + if (bindings.storage.length > 0) { + console.log(` ${chalk.cyan('Storage:')}`); + for (const bucket of bindings.storage) { + console.log(` ${bucket.binding} -> ${chalk.gray(bucket.bucketName)}`); + } + } + + if (bindings.analytics.length > 0) { + console.log(` ${chalk.cyan('Analytics:')}`); + for (const analytics of bindings.analytics) { + console.log(` ${analytics.binding} -> ${chalk.gray(analytics.dataset)}`); + } + } + + if (bindings.envVars.length > 0) { + console.log(` ${chalk.cyan('Environment Variables:')}`); + for (const envVar of bindings.envVars) { + console.log(` ${envVar}`); + } + } + + if (bindings.secrets.length > 0) { + console.log(` ${chalk.cyan('Secrets:')}`); + for (const secret of bindings.secrets) { + console.log(` ${secret}`); + } + } + } + } catch { + // If parsing fails, just output the raw result + console.log(result); + } +}