diff --git a/packages/atxp/src/commands/paas/index.ts b/packages/atxp/src/commands/paas/index.ts index 50a0b31..29d77d8 100644 --- a/packages/atxp/src/commands/paas/index.ts +++ b/packages/atxp/src/commands/paas/index.ts @@ -33,6 +33,11 @@ import { analyticsEventsCommand, analyticsStatsCommand, } from './analytics.js'; +import { + secretsSetCommand, + secretsListCommand, + secretsDeleteCommand, +} from './secrets.js'; interface PaasOptions { code?: string; @@ -112,6 +117,12 @@ function showPaasHelp(): void { console.log(' ' + chalk.cyan('paas analytics stats') + ' Get analytics statistics'); console.log(); + console.log(chalk.bold('Secrets Commands:')); + console.log(' ' + chalk.cyan('paas secrets set') + ' ' + chalk.yellow(' KEY=VALUE') + ' Set a secret'); + console.log(' ' + chalk.cyan('paas secrets list') + ' ' + chalk.yellow('') + ' List secrets'); + console.log(' ' + chalk.cyan('paas secrets delete') + ' ' + chalk.yellow(' ') + ' Delete a secret'); + console.log(); + console.log(chalk.bold('Examples:')); console.log(' npx atxp paas worker deploy my-api --code ./worker.js'); console.log(' npx atxp paas db create my-database'); @@ -119,6 +130,7 @@ function showPaasHelp(): void { console.log(' npx atxp paas storage upload my-bucket images/logo.png --file ./logo.png'); console.log(' npx atxp paas dns add example.com'); console.log(' npx atxp paas dns connect example.com my-api'); + console.log(' npx atxp paas secrets set my-api API_KEY=sk-abc123'); } export async function paasCommand(args: string[], options: PaasOptions): Promise { @@ -152,9 +164,13 @@ export async function paasCommand(args: string[], options: PaasOptions): Promise await handleAnalyticsCommand(subCommand, restArgs, options); break; + case 'secrets': + await handleSecretsCommand(subCommand, restArgs); + break; + default: console.error(chalk.red(`Unknown PAAS category: ${category}`)); - console.log('Available categories: worker, db, storage, dns, analytics'); + console.log('Available categories: worker, db, storage, dns, analytics, secrets'); console.log(`Run ${chalk.cyan('npx atxp paas help')} for usage information.`); process.exit(1); } @@ -473,3 +489,48 @@ async function handleAnalyticsCommand( process.exit(1); } } + +async function handleSecretsCommand( + subCommand: string, + args: string[] +): Promise { + const workerName = args[0]; + + switch (subCommand) { + case 'set': { + const keyValuePair = args[1]; + if (!workerName || !keyValuePair) { + console.error(chalk.red('Error: Worker name and KEY=VALUE are required')); + console.log(`Usage: ${chalk.cyan('npx atxp paas secrets set KEY=VALUE')}`); + process.exit(1); + } + await secretsSetCommand(workerName, keyValuePair); + break; + } + + case 'list': + if (!workerName) { + console.error(chalk.red('Error: Worker name is required')); + console.log(`Usage: ${chalk.cyan('npx atxp paas secrets list ')}`); + process.exit(1); + } + await secretsListCommand(workerName); + break; + + case 'delete': { + const key = args[1]; + if (!workerName || !key) { + console.error(chalk.red('Error: Worker name and secret key are required')); + console.log(`Usage: ${chalk.cyan('npx atxp paas secrets delete ')}`); + process.exit(1); + } + await secretsDeleteCommand(workerName, key); + break; + } + + default: + console.error(chalk.red(`Unknown secrets command: ${subCommand}`)); + console.log('Available commands: set, list, delete'); + process.exit(1); + } +} diff --git a/packages/atxp/src/commands/paas/secrets.ts b/packages/atxp/src/commands/paas/secrets.ts new file mode 100644 index 0000000..27d96b2 --- /dev/null +++ b/packages/atxp/src/commands/paas/secrets.ts @@ -0,0 +1,79 @@ +import { callTool } from '../../call-tool.js'; +import chalk from 'chalk'; + +const SERVER = 'paas.mcp.atxp.ai'; + +/** + * Validate that a secret key follows UPPER_SNAKE_CASE convention + */ +function isValidSecretKey(key: string): boolean { + return /^[A-Z][A-Z0-9_]*$/.test(key); +} + +/** + * Parse KEY=VALUE format into key and value + */ +function parseKeyValue(input: string): { key: string; value: string } | null { + const eqIndex = input.indexOf('='); + if (eqIndex === -1) { + return null; + } + const key = input.substring(0, eqIndex); + const value = input.substring(eqIndex + 1); + return { key, value }; +} + +export async function secretsSetCommand( + workerName: string, + keyValuePair: string +): Promise { + const parsed = parseKeyValue(keyValuePair); + if (!parsed) { + console.error(chalk.red('Error: Invalid format. Use KEY=VALUE')); + console.log(`Usage: ${chalk.cyan('npx atxp paas secrets set KEY=VALUE')}`); + process.exit(1); + } + + const { key, value } = parsed; + + if (!isValidSecretKey(key)) { + console.error(chalk.red('Error: Secret key must be UPPER_SNAKE_CASE (e.g., API_KEY, DATABASE_URL)')); + console.log(`Example: ${chalk.cyan('npx atxp paas secrets set my-worker API_KEY=sk-123')}`); + process.exit(1); + } + + if (!value) { + console.error(chalk.red('Error: Secret value cannot be empty')); + process.exit(1); + } + + const result = await callTool(SERVER, 'set_secret', { + worker_name: workerName, + key, + value, + }); + console.log(result); +} + +export async function secretsListCommand(workerName: string): Promise { + const result = await callTool(SERVER, 'list_secrets', { + worker_name: workerName, + }); + console.log(result); +} + +export async function secretsDeleteCommand( + workerName: string, + key: string +): Promise { + if (!isValidSecretKey(key)) { + console.error(chalk.red('Error: Secret key must be UPPER_SNAKE_CASE (e.g., API_KEY, DATABASE_URL)')); + process.exit(1); + } + + const result = await callTool(SERVER, 'delete_secret', { + worker_name: workerName, + key, + }); + console.log(result); +}