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
63 changes: 62 additions & 1 deletion packages/atxp/src/commands/paas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ import {
analyticsEventsCommand,
analyticsStatsCommand,
} from './analytics.js';
import {
secretsSetCommand,
secretsListCommand,
secretsDeleteCommand,
} from './secrets.js';

interface PaasOptions {
code?: string;
Expand Down Expand Up @@ -112,13 +117,20 @@ 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('<worker> KEY=VALUE') + ' Set a secret');
console.log(' ' + chalk.cyan('paas secrets list') + ' ' + chalk.yellow('<worker>') + ' List secrets');
console.log(' ' + chalk.cyan('paas secrets delete') + ' ' + chalk.yellow('<worker> <key>') + ' 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');
console.log(' npx atxp paas db query my-database --sql "SELECT * FROM users"');
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<void> {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -473,3 +489,48 @@ async function handleAnalyticsCommand(
process.exit(1);
}
}

async function handleSecretsCommand(
subCommand: string,
args: string[]
): Promise<void> {
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 <worker> 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 <worker>')}`);
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 <worker> <key>')}`);
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);
}
}
79 changes: 79 additions & 0 deletions packages/atxp/src/commands/paas/secrets.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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 <worker> 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<void> {
const result = await callTool(SERVER, 'list_secrets', {
worker_name: workerName,
});
console.log(result);
}

export async function secretsDeleteCommand(
workerName: string,
key: string
): Promise<void> {
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);
}
Loading