diff --git a/packages/atxp/src/commands/paas/help.test.ts b/packages/atxp/src/commands/paas/help.test.ts new file mode 100644 index 0000000..107b87c --- /dev/null +++ b/packages/atxp/src/commands/paas/help.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect } from 'vitest'; +import { COMMAND_HELP, PAAS_CATEGORIES, type CommandHelp } from './help.js'; + +describe('PAAS Help Coverage', () => { + // Define expected commands per category (source of truth from index.ts switch cases) + const EXPECTED_COMMANDS: Record = { + worker: ['deploy', 'list', 'logs', 'delete'], + db: ['create', 'list', 'query', 'delete'], + storage: ['create', 'list', 'upload', 'download', 'files', 'delete-bucket', 'delete-file'], + dns: ['add', 'list', 'record', 'connect'], + analytics: ['list', 'schema', 'query', 'events', 'stats'], + secrets: ['set', 'list', 'delete'], + }; + + // DNS record has nested subcommands + const EXPECTED_SUBCOMMANDS: Record = { + 'dns.record': ['create', 'list', 'delete'], + }; + + it('should have all categories defined', () => { + const categoryNames = PAAS_CATEGORIES.map((c) => c.name); + for (const category of Object.keys(EXPECTED_COMMANDS)) { + expect(categoryNames, `Missing category: ${category}`).toContain(category); + } + }); + + it('should have correct commands listed for each category', () => { + for (const [categoryName, expectedCommands] of Object.entries(EXPECTED_COMMANDS)) { + const category = PAAS_CATEGORIES.find((c) => c.name === categoryName); + expect(category, `Category not found: ${categoryName}`).toBeDefined(); + expect( + category!.commands.sort(), + `Category ${categoryName} has wrong commands` + ).toEqual(expectedCommands.sort()); + } + }); + + it('should have help for all commands', () => { + for (const [category, commands] of Object.entries(EXPECTED_COMMANDS)) { + for (const cmd of commands) { + const key = `${category}.${cmd}`; + expect(COMMAND_HELP[key], `Missing help for: ${key}`).toBeDefined(); + } + } + }); + + it('should have help for nested subcommands', () => { + for (const [parentKey, subCommands] of Object.entries(EXPECTED_SUBCOMMANDS)) { + for (const subCmd of subCommands) { + const key = `${parentKey}.${subCmd}`; + expect(COMMAND_HELP[key], `Missing help for nested command: ${key}`).toBeDefined(); + } + } + }); + + it('should have required fields in each command help', () => { + for (const [key, help] of Object.entries(COMMAND_HELP)) { + expect(help.usage, `${key} missing usage`).toBeDefined(); + expect(help.usage.length, `${key} has empty usage`).toBeGreaterThan(0); + expect(help.description, `${key} missing description`).toBeDefined(); + expect(help.description.length, `${key} has empty description`).toBeGreaterThan(0); + expect(help.examples, `${key} missing examples array`).toBeDefined(); + expect(help.examples.length, `${key} should have at least one example`).toBeGreaterThan(0); + } + }); + + it('should have valid examples in each command help', () => { + for (const [key, help] of Object.entries(COMMAND_HELP)) { + for (const example of help.examples) { + expect(example.command, `${key} has example with missing command`).toBeDefined(); + expect( + example.command.startsWith('npx atxp paas'), + `${key} example should start with "npx atxp paas": ${example.command}` + ).toBe(true); + } + } + }); + + it('should have options with flag and description when options are defined', () => { + for (const [key, help] of Object.entries(COMMAND_HELP)) { + if (help.options) { + for (const option of help.options) { + expect(option.flag, `${key} has option with missing flag`).toBeDefined(); + expect(option.flag.length, `${key} has option with empty flag`).toBeGreaterThan(0); + expect(option.description, `${key} has option with missing description`).toBeDefined(); + expect( + option.description.length, + `${key} has option with empty description` + ).toBeGreaterThan(0); + } + } + } + }); + + it('should reference existing commands in related field', () => { + const allKeys = new Set(Object.keys(COMMAND_HELP)); + + for (const [key, help] of Object.entries(COMMAND_HELP)) { + if (help.related) { + for (const related of help.related) { + // Convert "worker list" to "worker.list" and "dns record create" to "dns.record.create" + const relatedKey = related.replaceAll(' ', '.'); + expect( + allKeys.has(relatedKey), + `${key} references non-existent related command: ${related} (looked for ${relatedKey})` + ).toBe(true); + } + } + } + }); +}); + +describe('PAAS Categories', () => { + it('should have description for each category', () => { + for (const category of PAAS_CATEGORIES) { + expect(category.description, `Category ${category.name} missing description`).toBeDefined(); + expect( + category.description.length, + `Category ${category.name} has empty description` + ).toBeGreaterThan(0); + } + }); + + it('should have commands array for each category', () => { + for (const category of PAAS_CATEGORIES) { + expect(category.commands, `Category ${category.name} missing commands`).toBeDefined(); + expect( + category.commands.length, + `Category ${category.name} has no commands` + ).toBeGreaterThan(0); + } + }); +}); diff --git a/packages/atxp/src/commands/paas/help.ts b/packages/atxp/src/commands/paas/help.ts new file mode 100644 index 0000000..dbb0b6f --- /dev/null +++ b/packages/atxp/src/commands/paas/help.ts @@ -0,0 +1,578 @@ +import chalk from 'chalk'; + +// ============================================================================ +// Types +// ============================================================================ + +interface CommandOption { + flag: string; + description: string; + required?: boolean; +} + +interface CommandExample { + command: string; + description?: string; +} + +export interface CommandHelp { + usage: string; + description: string; + options?: CommandOption[]; + examples: CommandExample[]; + related?: string[]; +} + +export interface CategoryInfo { + name: string; + description: string; + commands: string[]; +} + +// ============================================================================ +// Category Definitions +// ============================================================================ + +export const PAAS_CATEGORIES: CategoryInfo[] = [ + { + name: 'worker', + description: 'Deploy and manage serverless workers', + commands: ['deploy', 'list', 'logs', 'delete'], + }, + { + name: 'db', + description: 'Create and manage D1 databases', + commands: ['create', 'list', 'query', 'delete'], + }, + { + name: 'storage', + description: 'Manage R2 storage buckets and files', + commands: ['create', 'list', 'upload', 'download', 'files', 'delete-bucket', 'delete-file'], + }, + { + name: 'dns', + description: 'Manage domains and DNS records', + commands: ['add', 'list', 'record', 'connect'], + }, + { + name: 'analytics', + description: 'Query and analyze worker analytics', + commands: ['list', 'schema', 'query', 'events', 'stats'], + }, + { + name: 'secrets', + description: 'Manage worker secrets', + commands: ['set', 'list', 'delete'], + }, +]; + +// ============================================================================ +// Command Help Registry +// ============================================================================ + +export const COMMAND_HELP: Record = { + // --------------------------------------------------------------------------- + // Worker Commands + // --------------------------------------------------------------------------- + 'worker.deploy': { + usage: 'atxp paas worker deploy [options]', + description: 'Deploy a serverless worker with optional database, storage, and analytics bindings.', + options: [ + { flag: '--code ', description: 'Path to worker code file', required: true }, + { flag: '--db ', description: 'Bind a database (repeatable)' }, + { flag: '--bucket ', description: 'Bind a storage bucket (repeatable)' }, + { flag: '--env KEY=VALUE', description: 'Set environment variable (repeatable)' }, + { flag: '--env-file ', description: 'Load env vars from file' }, + { flag: '--enable-analytics [NAME]', description: 'Enable Analytics Engine binding [default: ANALYTICS]' }, + ], + examples: [ + { command: 'npx atxp paas worker deploy my-api --code ./worker.js' }, + { command: 'npx atxp paas worker deploy my-api --code ./worker.js --db DB:mydb' }, + { command: 'npx atxp paas worker deploy my-api --code ./worker.js --bucket STORAGE:mybucket' }, + { command: 'npx atxp paas worker deploy my-api --code ./worker.js --enable-analytics' }, + { command: 'npx atxp paas worker deploy my-api --code ./worker.js --env API_KEY=abc123' }, + ], + related: ['worker list', 'worker logs', 'worker delete'], + }, + 'worker.list': { + usage: 'atxp paas worker list', + description: 'List all deployed workers in your account.', + examples: [ + { command: 'npx atxp paas worker list' }, + ], + related: ['worker deploy', 'worker logs', 'worker delete'], + }, + 'worker.logs': { + usage: 'atxp paas worker logs [options]', + description: 'View logs for a deployed worker.', + options: [ + { flag: '--limit ', description: 'Number of log entries to show' }, + { flag: '--level ', description: 'Filter by log level (error, warn, info, debug)' }, + { flag: '--since