Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/atxp/src/commands/paas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
workerListCommand,
workerLogsCommand,
workerDeleteCommand,
workerInfoCommand,
} from './worker.js';
import {
dbCreateCommand,
Expand Down Expand Up @@ -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 <name>')}`);
process.exit(1);
}
await workerInfoCommand(name);
break;

case 'logs':
if (!name) {
console.error(chalk.red('Error: Worker name is required'));
Expand All @@ -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);
}
}
Expand Down
93 changes: 93 additions & 0 deletions packages/atxp/src/commands/paas/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
workerListCommand,
workerLogsCommand,
workerDeleteCommand,
workerInfoCommand,
parseEnvArg,
parseEnvFile,
validateEnvVarName,
Expand Down Expand Up @@ -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');
});
});
});

90 changes: 90 additions & 0 deletions packages/atxp/src/commands/paas/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,93 @@ export async function workerDeleteCommand(name: string): Promise<void> {
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<void> {
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);
}
}
Loading