diff --git a/package.json b/package.json index 43349b3dd4..5b5183b4dc 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,12 @@ "description": "ObjectStack Protocol & Specification - Monorepo for TypeScript Interfaces, JSON Schemas, and Convention Configurations", "scripts": { "build": "turbo run build --filter=!@objectstack/docs", - "dev": "pnpm --filter @objectstack/cli build && node packages/cli/bin/objectstack.js serve --dev", + "dev": "pnpm --filter @objectstack/cli build && node packages/cli/bin/run.js serve --dev", "dev:studio": "pnpm --filter @objectstack/studio dev", - "studio": "pnpm --filter @objectstack/cli build && node packages/cli/bin/objectstack.js studio", + "studio": "pnpm --filter @objectstack/cli build && node packages/cli/bin/run.js studio", "test": "turbo run test --filter=@objectstack/spec", "clean": "turbo run clean && rm -rf dist", - "doctor": "pnpm --filter @objectstack/cli build && node packages/cli/bin/objectstack.js doctor", + "doctor": "pnpm --filter @objectstack/cli build && node packages/cli/bin/run.js doctor", "setup": "pnpm install && pnpm --filter @objectstack/spec build", "version": "changeset version", "release": "pnpm run build && changeset publish", diff --git a/packages/cli/README.md b/packages/cli/README.md index 34dec8f445..9c8baf0efb 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -2,6 +2,8 @@ Command Line Interface for building metadata-driven applications with the ObjectStack Protocol. +Built on [oclif](https://oclif.io/) — commands are auto-discovered, and plugins can extend the CLI without modifying the main package. + ## Installation ```bash @@ -40,6 +42,7 @@ os compile | `os init [name]` | Initialize a new ObjectStack project in the current directory | | `os dev [package]` | Start development mode with hot reload | | `os serve [config]` | Start the ObjectStack server with plugin auto-detection | +| `os studio [config]` | Launch Studio UI with development server | ### Build & Validate @@ -73,6 +76,20 @@ Available generate types: `object`, `view`, `action`, `flow`, `agent`, `dashboar |---------|-------------| | `os test [files]` | Run Quality Protocol test scenarios against a running server | | `os doctor` | Check development environment health | +| `os lint [config]` | Check configuration for style and convention issues | +| `os diff [before] [after]` | Compare two configurations and detect breaking changes | + +### Reference + +| Command | Description | +|---------|-------------| +| `os explain [schema]` | Display human-readable explanation of an ObjectStack schema | + +### Code Transforms + +| Command | Description | +|---------|-------------| +| `os codemod v2-to-v3` | Migrate ObjectStack v2 config to v3 format | ## Configuration @@ -120,12 +137,12 @@ export default defineStack({ - `-p, --port ` — Server port (default: `3000`) - `--dev` — Run in development mode (load devPlugins, pretty logging) +- `--ui` — Enable Studio UI - `--no-server` — Skip starting HTTP server plugin ### `os generate` - `-d, --dir ` — Override target directory -- `--dry-run` — Preview without writing files ### `os plugin list` @@ -140,75 +157,78 @@ export default defineStack({ - `-c, --config ` — Configuration file path -## Plugin CLI Extensions +### `os info` + +- `--json` — Output as JSON -Plugins can extend the CLI with custom commands via the `contributes.commands` manifest field. The CLI automatically discovers and loads these commands at startup. +### `os doctor` -### How to Create a CLI Plugin +- `-v, --verbose` — Show fix suggestions for warnings +- `--scan-deprecations` — Scan for deprecated patterns -**1. Declare commands in the plugin manifest:** +## oclif Plugin System -```typescript -export default defineStack({ - manifest: { - id: 'com.acme.marketplace', - version: '1.0.0', - type: 'plugin', - name: 'Marketplace Plugin', - contributes: { - commands: [ - { - name: 'marketplace', - description: 'Manage marketplace applications', - module: './dist/cli.js', - }, - ], - }, - }, -}); +The CLI uses oclif's built-in plugin system for extensibility. Third-party plugins (e.g., cloud commands, marketplace tools) can extend the CLI without modifying the main package. + +### How Plugin Extension Works + +1. **Create an oclif plugin package** with its own `oclif` config in `package.json` +2. **Export oclif Command classes** from the plugin's `src/commands/` directory +3. **Install the plugin** via `os plugins install ` or declare it in the main CLI's `oclif.plugins` + +### Creating a CLI Plugin + +**1. Configure the plugin's `package.json`:** + +```json +{ + "name": "@acme/plugin-marketplace", + "oclif": { + "commands": { + "strategy": "pattern", + "target": "./dist/commands", + "glob": "**/*.js" + } + } +} ``` -**2. Export Commander.js commands from the module:** +**2. Create oclif Command classes:** ```typescript -// src/cli.ts -import { Command } from 'commander'; - -const marketplaceCommand = new Command('marketplace') - .description('Manage marketplace applications') - .addCommand( - new Command('search') - .argument('') - .action(async (query) => { /* ... */ }) - ) - .addCommand( - new Command('install') - .argument('') - .action(async (app) => { /* ... */ }) - ); - -// Named export (recommended) -export const commands = [marketplaceCommand]; -// Also supports: export default Command | Command[] +// src/commands/marketplace/search.ts +import { Args, Command, Flags } from '@oclif/core'; + +export default class MarketplaceSearch extends Command { + static override description = 'Search marketplace applications'; + + static override args = { + query: Args.string({ description: 'Search query', required: true }), + }; + + async run() { + const { args } = await this.parse(MarketplaceSearch); + // Implementation... + } +} ``` -**3. Register the plugin in the host project and use:** +**3. Install and use:** ```bash -os plugin add @acme/plugin-marketplace +os plugins install @acme/plugin-marketplace os marketplace search "crm" -os marketplace install com.acme.crm ``` -For a complete guide, see the [Plugin CLI Extensions](/docs/guides/plugins#cli-command-extensions) section in the Plugins guide. +### Key Differences from Previous Plugin Model -### `os info` - -- `--json` — Output as JSON - -### `os doctor` - -- `-v, --verbose` — Show fix suggestions for warnings +| Before (Commander.js) | After (oclif) | +|---|---| +| Plugins declared in `objectstack.config.ts` | Plugins installed via `os plugins install` or `oclif.plugins` | +| Custom `loadPluginCommands` mechanism | oclif's built-in plugin discovery | +| `contributes.commands` in manifest | `oclif.commands` in `package.json` | +| Commander.js `new Command(...)` exports | oclif `class extends Command` exports | +| Project config determines CLI commands | CLI commands available without project init | ## Typical Workflow @@ -223,6 +243,30 @@ os dev # 7. Start dev server os compile # 8. Build for production ``` +## Architecture + +``` +@objectstack/cli (oclif) +├── bin/run.js # Entry point (os / objectstack) +├── src/commands/ # Auto-discovered command classes +│ ├── init.ts # os init +│ ├── dev.ts # os dev +│ ├── serve.ts # os serve +│ ├── compile.ts # os compile +│ ├── validate.ts # os validate +│ ├── generate.ts # os generate (alias: g) +│ ├── plugin/ # os plugin +│ │ ├── list.ts +│ │ ├── info.ts +│ │ ├── add.ts +│ │ └── remove.ts +│ ├── codemod/ # os codemod +│ │ └── v2-to-v3.ts +│ └── ... +├── src/utils/ # Shared utilities +└── package.json # oclif config under "oclif" key +``` + ## License Apache-2.0 diff --git a/packages/cli/bin/objectstack.js b/packages/cli/bin/objectstack.js deleted file mode 100755 index c7ad3e202b..0000000000 --- a/packages/cli/bin/objectstack.js +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -import '../dist/bin.js'; diff --git a/packages/cli/bin/run-dev.js b/packages/cli/bin/run-dev.js new file mode 100644 index 0000000000..774534ce47 --- /dev/null +++ b/packages/cli/bin/run-dev.js @@ -0,0 +1,5 @@ +#!/usr/bin/env tsx + +import { execute } from '@oclif/core'; + +await execute({ type: 'esm', development: true, dir: import.meta.url }); diff --git a/packages/cli/bin/run.js b/packages/cli/bin/run.js new file mode 100644 index 0000000000..7a6c197a5f --- /dev/null +++ b/packages/cli/bin/run.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { execute } from '@oclif/core'; + +await execute({ type: 'esm', dir: import.meta.url }); diff --git a/packages/cli/package.json b/packages/cli/package.json index fde3b76fc0..a2ac9e4913 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,25 +3,41 @@ "version": "3.0.6", "description": "Command Line Interface for ObjectStack Protocol", "main": "dist/index.js", + "types": "dist/index.d.ts", "bin": { - "objectstack": "./bin/objectstack.js", - "os": "./bin/objectstack.js" + "objectstack": "./bin/run.js", + "os": "./bin/run.js" }, "scripts": { - "build": "tsup", - "dev": "tsup --watch", + "build": "tsc -p tsconfig.build.json", + "dev": "tsc -p tsconfig.build.json --watch", "test": "vitest run", "lint": "eslint src" }, "keywords": [ "objectstack", "cli", + "oclif", "compiler", "scaffold" ], "type": "module", "author": "Steedos", "license": "Apache-2.0", + "oclif": { + "bin": "os", + "dirname": "objectstack", + "commands": { + "strategy": "pattern", + "target": "./dist/commands", + "glob": "**/*.js" + }, + "plugins": [ + "@oclif/plugin-help", + "@oclif/plugin-plugins" + ], + "topicSeparator": " " + }, "dependencies": { "@objectstack/core": "workspace:*", "@objectstack/driver-memory": "workspace:^", @@ -30,9 +46,9 @@ "@objectstack/rest": "workspace:*", "@objectstack/runtime": "workspace:^", "@objectstack/spec": "workspace:*", + "@oclif/core": "^4.8.0", "bundle-require": "^5.1.0", "chalk": "^5.3.0", - "commander": "^14.0.3", "tsx": "^4.7.1", "zod": "^4.3.6" }, @@ -40,6 +56,8 @@ "@objectstack/core": "workspace:*" }, "devDependencies": { + "@oclif/plugin-help": "^6.2.37", + "@oclif/plugin-plugins": "^5.4.56", "@types/node": "^25.2.2", "tsup": "^8.0.2", "typescript": "^5.3.3", diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index f3faf2675d..7e7f9d6f63 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -1,106 +1,13 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { createRequire } from 'module'; -import { Command } from 'commander'; -import chalk from 'chalk'; - -// Commands -import { compileCommand } from './commands/compile.js'; -import { devCommand } from './commands/dev.js'; -import { doctorCommand } from './commands/doctor.js'; -import { createCommand } from './commands/create.js'; -import { serveCommand } from './commands/serve.js'; -import { studioCommand } from './commands/studio.js'; -import { testCommand } from './commands/test.js'; -import { validateCommand } from './commands/validate.js'; -import { initCommand } from './commands/init.js'; -import { infoCommand } from './commands/info.js'; -import { generateCommand } from './commands/generate.js'; -import { pluginCommand } from './commands/plugin.js'; -import { diffCommand } from './commands/diff.js'; -import { lintCommand } from './commands/lint.js'; -import { explainCommand } from './commands/explain.js'; -import { codemodCommand } from './commands/codemod.js'; -import { loadPluginCommands } from './utils/plugin-commands.js'; - -const require = createRequire(import.meta.url); -const pkg = require('../package.json'); - -// ─── Global Error Handling ────────────────────────────────────────── -process.on('unhandledRejection', (err: any) => { - console.error(chalk.red(`\n ✗ Unhandled error: ${err?.message || err}`)); - if (err?.stack && process.env.DEBUG) { - console.error(chalk.dim(err.stack)); - } - process.exit(1); -}); - -// ─── Program Definition ───────────────────────────────────────────── -const program = new Command(); - -program - .name('objectstack') - .description('ObjectStack CLI — Build metadata-driven apps with the ObjectStack Protocol') - .version(pkg.version, '-v, --version') - .configureHelp({ - sortSubcommands: false, - }) - .addHelpText('before', ` -${chalk.bold.cyan('◆ ObjectStack CLI')} ${chalk.dim(`v${pkg.version}`)} -`) - .addHelpText('after', ` -${chalk.bold('Workflow:')} - ${chalk.dim('$')} os init ${chalk.dim('# Create a new project')} - ${chalk.dim('$')} os generate object task ${chalk.dim('# Add metadata')} - ${chalk.dim('$')} os plugin add ${chalk.dim('# Add a plugin')} - ${chalk.dim('$')} os validate ${chalk.dim('# Check configuration')} - ${chalk.dim('$')} os dev ${chalk.dim('# Start dev server')} - ${chalk.dim('$')} os studio ${chalk.dim('# Dev server + Studio UI')} - ${chalk.dim('$')} os compile ${chalk.dim('# Build for production')} - -${chalk.dim('Aliases: objectstack | os')} -${chalk.dim('Docs: https://objectstack.dev')} -`); - -// ── Development ── -program.addCommand(initCommand); -program.addCommand(devCommand); -program.addCommand(serveCommand); -program.addCommand(studioCommand); - -// ── Build & Validate ── -program.addCommand(compileCommand); -program.addCommand(validateCommand); -program.addCommand(infoCommand); - -// ── Scaffolding ── -program.addCommand(generateCommand); -program.addCommand(createCommand); - -// ── Plugin Management ── -program.addCommand(pluginCommand); - -// ── Quality ── -program.addCommand(testCommand); -program.addCommand(doctorCommand); -program.addCommand(lintCommand); -program.addCommand(diffCommand); - -// ── Reference ── -program.addCommand(explainCommand); - -// ── Code Transforms ── -program.addCommand(codemodCommand); - -// ── Plugin-Contributed Commands ── -// Load commands from installed plugins that declare `contributes.commands` in their manifest. -// This must complete before `program.parse()` so that plugin commands are available. -loadPluginCommands(program).then(() => { - program.parse(process.argv); -}).catch((err) => { - // If plugin command loading fails, still parse with built-in commands - if (process.env.DEBUG) { - console.error(chalk.yellow(`\n ⚠ Plugin command loading failed: ${err?.message || err}`)); - } - program.parse(process.argv); -}); +/** + * ObjectStack CLI — oclif-based entry point. + * + * All commands are auto-discovered from `src/commands/` by oclif. + * Plugins extend the CLI via oclif's built-in plugin system + * (configured in package.json under "oclif.plugins"). + * + * Run `os --help` for available commands. + */ + +export { execute } from '@oclif/core'; diff --git a/packages/cli/src/commands/codemod.ts b/packages/cli/src/commands/codemod/v2-to-v3.ts similarity index 76% rename from packages/cli/src/commands/codemod.ts rename to packages/cli/src/commands/codemod/v2-to-v3.ts index 583bc92653..c94e1cfeff 100644 --- a/packages/cli/src/commands/codemod.ts +++ b/packages/cli/src/commands/codemod/v2-to-v3.ts @@ -1,10 +1,10 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Command } from 'commander'; +import { Command, Flags } from '@oclif/core'; import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; -import { printHeader, printSuccess, printError, printInfo, printStep, createTimer } from '../utils/format.js'; +import { printHeader, printSuccess, printError, printInfo, printStep, createTimer } from '../../utils/format.js'; // ─── Transform Definitions ────────────────────────────────────────── @@ -76,25 +76,31 @@ function walkDir(dir: string, ext: string): string[] { return results; } -// ─── v2-to-v3 Sub-Command ────────────────────────────────────────── +// ─── Command ──────────────────────────────────────────────────────── + +export default class V2ToV3 extends Command { + static override description = 'Migrate ObjectStack v2 config to v3 format'; + + static override flags = { + dir: Flags.string({ description: 'Directory to scan', default: 'src/' }), + 'dry-run': Flags.boolean({ description: 'Show changes without writing files' }), + }; + + async run(): Promise { + const { flags } = await this.parse(V2ToV3); -const v2ToV3Command = new Command('v2-to-v3') - .description('Migrate ObjectStack v2 code patterns to v3') - .option('--dir ', 'Directory to scan', 'src/') - .option('--dry-run', 'Show changes without writing files') - .action(async (options) => { printHeader('Codemod: v2 → v3'); const timer = createTimer(); - const dir = path.resolve(process.cwd(), options.dir); + const dir = path.resolve(process.cwd(), flags.dir); if (!fs.existsSync(dir)) { printError(`Directory not found: ${dir}`); process.exit(1); } - console.log(` ${chalk.dim('Directory:')} ${chalk.white(options.dir)}`); - console.log(` ${chalk.dim('Dry run:')} ${chalk.white(options.dryRun ? 'yes' : 'no')}`); + console.log(` ${chalk.dim('Directory:')} ${chalk.white(flags.dir)}`); + console.log(` ${chalk.dim('Dry run:')} ${chalk.white(flags['dry-run'] ? 'yes' : 'no')}`); console.log(''); printStep('Scanning TypeScript files...'); @@ -132,7 +138,7 @@ const v2ToV3Command = new Command('v2-to-v3') filesModified++; totalTransforms += fileTransforms; - if (options.dryRun) { + if (flags['dry-run']) { printInfo(`${relPath} — ${fileTransforms} change(s)`); } else { fs.writeFileSync(file, content); @@ -152,7 +158,7 @@ const v2ToV3Command = new Command('v2-to-v3') } console.log(''); - if (options.dryRun) { + if (flags['dry-run']) { printInfo(`Would modify ${filesModified} file(s) with ${totalTransforms} total change(s)`); console.log(chalk.dim(' Run without --dry-run to apply changes')); } else { @@ -161,18 +167,5 @@ const v2ToV3Command = new Command('v2-to-v3') } console.log(''); - }); - -// ─── Main Codemod Command ─────────────────────────────────────────── - -export const codemodCommand = new Command('codemod') - .description('Run automated code transformations') - .addCommand(v2ToV3Command) - .action(() => { - printHeader('Codemod'); - console.log(chalk.bold(' Available codemods:')); - console.log(` ${chalk.cyan('v2-to-v3'.padEnd(16))} Migrate ObjectStack v2 code patterns to v3`); - console.log(''); - console.log(chalk.dim(' Usage: objectstack codemod v2-to-v3 [--dir src/] [--dry-run]')); - console.log(''); - }); + } +} diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 2a5dffb2e2..8bec51d5be 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Command } from 'commander'; +import { Args, Command, Flags } from '@oclif/core'; import path from 'path'; import fs from 'fs'; import chalk from 'chalk'; @@ -19,47 +19,55 @@ import { printMetadataStats, } from '../utils/format.js'; -export const compileCommand = new Command('compile') - .description('Compile ObjectStack configuration to JSON artifact') - .argument('[config]', 'Source configuration file') - .option('-o, --output ', 'Output JSON file', 'dist/objectstack.json') - .option('--json', 'Output compile result as JSON (for CI)') - .action(async (configPath, options) => { +export default class Compile extends Command { + static override description = 'Compile ObjectStack configuration to JSON artifact'; + + static override args = { + config: Args.string({ description: 'Source configuration file', required: false }), + }; + + static override flags = { + output: Flags.string({ char: 'o', description: 'Output JSON file', default: 'dist/objectstack.json' }), + json: Flags.boolean({ description: 'Output compile result as JSON (for CI)' }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(Compile); const timer = createTimer(); - if (!options.json) { + if (!flags.json) { printHeader('Compile'); } try { // 1. Load Configuration - if (!options.json) printStep('Loading configuration...'); - const { config, absolutePath, duration } = await loadConfig(configPath); + if (!flags.json) printStep('Loading configuration...'); + const { config, absolutePath, duration } = await loadConfig(args.config); - if (!options.json) { + if (!flags.json) { printKV('Config', path.relative(process.cwd(), absolutePath)); printKV('Load time', `${duration}ms`); } // 2. Normalize map-formatted stack definition and validate against Protocol - if (!options.json) printStep('Validating protocol compliance...'); + if (!flags.json) printStep('Validating protocol compliance...'); const normalized = normalizeStackInput(config as Record); const result = ObjectStackDefinitionSchema.safeParse(normalized); if (!result.success) { - if (options.json) { + if (flags.json) { console.log(JSON.stringify({ success: false, errors: (result.error as unknown as ZodError).issues })); - process.exit(1); + this.exit(1); } console.log(''); printError('Validation failed'); formatZodErrors(result.error as unknown as ZodError); - process.exit(1); + this.exit(1); } // 3. Generate Artifact - if (!options.json) printStep('Writing artifact...'); - const output = options.output; + if (!flags.json) printStep('Writing artifact...'); + const output = flags.output!; const artifactPath = path.resolve(process.cwd(), output); const artifactDir = path.dirname(artifactPath); @@ -73,7 +81,7 @@ export const compileCommand = new Command('compile') const sizeKB = (jsonContent.length / 1024).toFixed(1); const stats = collectMetadataStats(config); - if (options.json) { + if (flags.json) { console.log(JSON.stringify({ success: true, output: artifactPath, @@ -94,12 +102,13 @@ export const compileCommand = new Command('compile') console.log(''); } catch (error: any) { - if (options.json) { + if (flags.json) { console.log(JSON.stringify({ success: false, error: error.message })); - process.exit(1); + this.exit(1); } console.log(''); printError(error.message || String(error)); - process.exit(1); + this.error(error.message || String(error)); } - }); + } +} diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts index cdd8523397..922ee08f87 100644 --- a/packages/cli/src/commands/create.ts +++ b/packages/cli/src/commands/create.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Command } from 'commander'; +import { Args, Command, Flags } from '@oclif/core'; import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; @@ -182,37 +182,46 @@ function toCamelCase(str: string): string { return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); } -export const createCommand = new Command('create') - .description('Create a new package, plugin, or example from template') - .argument('', 'Type of project to create (plugin, example)') - .argument('[name]', 'Name of the project') - .option('-d, --dir ', 'Target directory') - .action(async (type: string, name?: string, options?: { dir?: string }) => { +export default class Create extends Command { + static override description = 'Create a new package, plugin, or example from template'; + + static override args = { + type: Args.string({ description: 'Type of project to create (plugin, example)', required: true }), + name: Args.string({ description: 'Name of the project', required: false }), + }; + + static override flags = { + dir: Flags.string({ char: 'd', description: 'Target directory' }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(Create); + console.log(chalk.bold(`\n📦 ObjectStack Project Creator`)); console.log(chalk.dim(`-------------------------------`)); - if (!templates[type as keyof typeof templates]) { - console.error(chalk.red(`\n❌ Unknown type: ${type}`)); + if (!templates[args.type as keyof typeof templates]) { + console.error(chalk.red(`\n❌ Unknown type: ${args.type}`)); console.log(chalk.dim('Available types: plugin, example')); process.exit(1); } - if (!name) { + if (!args.name) { console.error(chalk.red('\n❌ Project name is required')); - console.log(chalk.dim(`Usage: objectstack create ${type} `)); + console.log(chalk.dim(`Usage: objectstack create ${args.type} `)); process.exit(1); } - const template = templates[type as keyof typeof templates]; + const template = templates[args.type as keyof typeof templates]; const cwd = process.cwd(); // Determine target directory let targetDir: string; - if (options?.dir) { - targetDir = path.resolve(cwd, options.dir); + if (flags.dir) { + targetDir = path.resolve(cwd, flags.dir); } else { - const baseDir = type === 'plugin' ? 'packages/plugins' : 'examples'; - const projectName = type === 'plugin' ? `plugin-${name}` : name; + const baseDir = args.type === 'plugin' ? 'packages/plugins' : 'examples'; + const projectName = args.type === 'plugin' ? `plugin-${args.name}` : args.name; targetDir = path.join(cwd, baseDir, projectName); } @@ -222,7 +231,7 @@ export const createCommand = new Command('create') process.exit(1); } - console.log(`📁 Creating ${type}: ${chalk.blue(name)}`); + console.log(`📁 Creating ${args.type}: ${chalk.blue(args.name)}`); console.log(`📂 Location: ${chalk.dim(targetDir)}`); console.log(''); @@ -239,7 +248,7 @@ export const createCommand = new Command('create') fs.mkdirSync(dir, { recursive: true }); } - const content = contentFn(name); + const content = contentFn(args.name); const fileContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2); @@ -268,4 +277,5 @@ export const createCommand = new Command('create') process.exit(1); } - }); + } +} diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 6295921972..f9bf10c773 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -1,19 +1,29 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Command } from 'commander'; +import { Args, Command, Flags } from '@oclif/core'; import chalk from 'chalk'; import { execSync, spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; import { printHeader, printKV, printStep, printError } from '../utils/format.js'; -export const devCommand = new Command('dev') - .description('Start development mode with hot-reload') - .argument('[package]', 'Package name or filter pattern', 'all') - .option('-w, --watch', 'Enable watch mode (default)', true) - .option('--ui', 'Enable Studio UI at /_studio/') - .option('-v, --verbose', 'Verbose output') - .action(async (packageName, options) => { +export default class Dev extends Command { + static override description = 'Start development mode with hot-reload'; + + static override args = { + package: Args.string({ description: 'Package name or filter pattern', default: 'all', required: false }), + }; + + static override flags = { + watch: Flags.boolean({ char: 'w', description: 'Enable watch mode (default)', default: true }), + ui: Flags.boolean({ description: 'Enable Studio UI at /_studio/' }), + verbose: Flags.boolean({ char: 'v', description: 'Verbose output' }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(Dev); + const packageName = args.package; + printHeader('Development Mode'); // Check if we are running inside a package (Single Package Mode) @@ -28,7 +38,7 @@ export const devCommand = new Command('dev') // usage: objectstack serve --dev const binPath = process.argv[1]; // path to objectstack bin - const child = spawn(process.execPath, [binPath, 'serve', '--dev', ...(options.ui ? ['--ui'] : []), ...(options.verbose ? ['--verbose'] : [])], { + const child = spawn(process.execPath, [binPath, 'serve', '--dev', ...(flags.ui ? ['--ui'] : []), ...(flags.verbose ? ['--verbose'] : [])], { stdio: 'inherit', env: { ...process.env, NODE_ENV: 'development' } }); @@ -69,4 +79,5 @@ export const devCommand = new Command('dev') printError(`Development mode failed: ${error.message || error}`); process.exit(1); } - }); + } +} diff --git a/packages/cli/src/commands/diff.ts b/packages/cli/src/commands/diff.ts index 50ebc29eda..492f243810 100644 --- a/packages/cli/src/commands/diff.ts +++ b/packages/cli/src/commands/diff.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Command } from 'commander'; +import { Args, Command, Flags } from '@oclif/core'; import chalk from 'chalk'; import { loadConfig } from '../utils/config.js'; import { @@ -153,19 +153,27 @@ function diffNamedArrays( // ─── Command ──────────────────────────────────────────────────────── -export const diffCommand = new Command('diff') - .description('Compare two ObjectStack configurations and detect breaking changes') - .argument('[before]', 'Path to the "before" config file') - .argument('[after]', 'Path to the "after" config file') - .option('--before ', 'Path to the "before" config (alternative)') - .option('--after ', 'Path to the "after" config (alternative)') - .option('--json', 'Output as JSON') - .option('--breaking-only', 'Show only breaking changes') - .action(async (beforeArg, afterArg, options) => { +export default class Diff extends Command { + static override description = 'Compare two ObjectStack configurations and detect breaking changes'; + + static override args = { + before: Args.string({ description: 'Path to the "before" config file', required: false }), + after: Args.string({ description: 'Path to the "after" config file', required: false }), + }; + + static override flags = { + before: Flags.string({ description: 'Path to the "before" config (alternative)' }), + after: Flags.string({ description: 'Path to the "after" config (alternative)' }), + json: Flags.boolean({ description: 'Output as JSON' }), + 'breaking-only': Flags.boolean({ description: 'Show only breaking changes' }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(Diff); const timer = createTimer(); - const beforePath: string | undefined = beforeArg || options.before; - const afterPath: string | undefined = afterArg || options.after; + const beforePath: string | undefined = args.before || flags.before; + const afterPath: string | undefined = args.after || flags.after; if (!beforePath || !afterPath) { printError('Two config file paths are required.'); @@ -175,7 +183,7 @@ export const diffCommand = new Command('diff') process.exit(1); } - if (!options.json) { + if (!flags.json) { printHeader('Diff'); printStep('Loading configurations...'); } @@ -184,7 +192,7 @@ export const diffCommand = new Command('diff') const { config: beforeConfig } = await loadConfig(beforePath); const { config: afterConfig } = await loadConfig(afterPath); - if (!options.json) { + if (!flags.json) { printInfo(`Before: ${chalk.white(beforePath)}`); printInfo(`After: ${chalk.white(afterPath)}`); } @@ -215,14 +223,14 @@ export const diffCommand = new Command('diff') } // ── Filter ── - const diffs = options.breakingOnly + const diffs = flags['breaking-only'] ? allDiffs.filter((d) => d.breaking) : allDiffs; const breakingCount = allDiffs.filter((d) => d.breaking).length; // ── Output ── - if (options.json) { + if (flags.json) { console.log(JSON.stringify({ before: beforePath, after: afterPath, @@ -237,7 +245,7 @@ export const diffCommand = new Command('diff') console.log(''); if (diffs.length === 0) { - printSuccess(options.breakingOnly + printSuccess(flags['breaking-only'] ? 'No breaking changes detected.' : 'No changes detected.'); console.log(''); @@ -274,7 +282,7 @@ export const diffCommand = new Command('diff') console.log(''); } catch (error: any) { - if (options.json) { + if (flags.json) { console.log(JSON.stringify({ error: error.message })); process.exit(1); } @@ -282,4 +290,5 @@ export const diffCommand = new Command('diff') printError(error.message || String(error)); process.exit(1); } - }); + } +} diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 2e473bedcb..bf6205fc35 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Command } from 'commander'; +import { Command, Flags } from '@oclif/core'; import chalk from 'chalk'; import { execSync } from 'child_process'; import fs from 'fs'; @@ -299,11 +299,17 @@ function scanDeprecatedPatterns(dir: string): Array<{ file: string; line: number // ─── Command ──────────────────────────────────────────────────────── -export const doctorCommand = new Command('doctor') - .description('Check development environment and configuration health') - .option('-v, --verbose', 'Show detailed information') - .option('--scan-deprecations', 'Scan for deprecated ObjectStack patterns') - .action(async (options) => { +export default class Doctor extends Command { + static override description = 'Check development environment and configuration health'; + + static override flags = { + verbose: Flags.boolean({ char: 'v', description: 'Show detailed information' }), + 'scan-deprecations': Flags.boolean({ description: 'Scan for deprecated ObjectStack patterns' }), + }; + + async run(): Promise { + const { flags } = await this.parse(Doctor); + printHeader('Environment Health Check'); const results: HealthCheckResult[] = []; @@ -439,7 +445,7 @@ export const doctorCommand = new Command('doctor') printError(`${padded} ${result.message}`); } - if (result.fix && (options.verbose || result.status === 'error')) { + if (result.fix && (flags.verbose || result.status === 'error')) { console.log(chalk.dim(` → ${result.fix}`)); } @@ -526,7 +532,7 @@ export const doctorCommand = new Command('doctor') } // ── Deprecation Pattern Scan ───────────────────────────────────── - if (options.scanDeprecations) { + if (flags['scan-deprecations']) { printStep('Scanning for deprecated ObjectStack patterns...'); const scanDir = path.join(cwd, 'src'); const deprecations = scanDeprecatedPatterns(scanDir); @@ -534,7 +540,7 @@ export const doctorCommand = new Command('doctor') hasWarnings = true; for (const dep of deprecations) { printWarning(`${dep.file}:${dep.line} — ${dep.description}`); - if (options.verbose) { + if (flags.verbose) { console.log(chalk.dim(` → ${dep.replacement}`)); } } @@ -562,4 +568,5 @@ export const doctorCommand = new Command('doctor') } console.log(''); - }); + } +} diff --git a/packages/cli/src/commands/explain.ts b/packages/cli/src/commands/explain.ts index ee3dc81c67..efababa105 100644 --- a/packages/cli/src/commands/explain.ts +++ b/packages/cli/src/commands/explain.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Command } from 'commander'; +import { Args, Command, Flags } from '@oclif/core'; import chalk from 'chalk'; import { printHeader, @@ -314,15 +314,24 @@ const SCHEMAS: Record = { // ─── Command ──────────────────────────────────────────────────────── -export const explainCommand = new Command('explain') - .description('Display human-readable explanation of an ObjectStack schema') - .argument('[schema]', 'Schema name (e.g., object, field, view, flow, agent, app)') - .option('--json', 'Output as JSON') - .action(async (schemaName, options) => { +export default class Explain extends Command { + static override description = 'Display human-readable explanation of an ObjectStack schema'; + + static override args = { + schema: Args.string({ description: 'Schema name (e.g., object, field, view, flow, agent, app)', required: false }), + }; + + static override flags = { + json: Flags.boolean({ description: 'Output as JSON' }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(Explain); + const schemaName = args.schema; // ── No argument: list all schemas ── if (!schemaName) { - if (options.json) { + if (flags.json) { console.log(JSON.stringify({ schemas: Object.entries(SCHEMAS).map(([key, s]) => ({ name: key, @@ -347,7 +356,7 @@ export const explainCommand = new Command('explain') // ── Lookup schema ── const schema = SCHEMAS[schemaName.toLowerCase()]; if (!schema) { - if (options.json) { + if (flags.json) { console.log(JSON.stringify({ error: `Unknown schema: ${schemaName}` })); process.exit(1); } @@ -359,7 +368,7 @@ export const explainCommand = new Command('explain') } // ── JSON output ── - if (options.json) { + if (flags.json) { console.log(JSON.stringify(schema, null, 2)); return; } @@ -399,4 +408,5 @@ export const explainCommand = new Command('explain') // Documentation link printKV(' Docs', `https://objectstack.dev/docs/${schema.docsPath}`); console.log(''); - }); + } +} diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts index f8c06b25c9..e7d33f309f 100644 --- a/packages/cli/src/commands/generate.ts +++ b/packages/cli/src/commands/generate.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Command } from 'commander'; +import { Args, Command, Flags } from '@oclif/core'; import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; @@ -305,14 +305,7 @@ function generateTypesFromConfig(config: Record): string { // ─── Command ──────────────────────────────────────────────────────── -const generateMetadataCommand = new Command('metadata') - .alias('m') - .description('Generate metadata scaffold (object, view, action, flow, agent, dashboard, app)') - .argument('', 'Metadata type to generate') - .argument('', 'Name for the metadata (use kebab-case)') - .option('-d, --dir ', 'Target directory (overrides default)') - .option('--dry-run', 'Show what would be created without writing files') - .action(async (type: string, name: string, options) => { +async function runMetadataGeneration(type: string, name: string, flags: { dir?: string; dryRun?: boolean }): Promise { printHeader('Generate'); const generator = GENERATORS[type]; @@ -330,7 +323,7 @@ const generateMetadataCommand = new Command('metadata') process.exit(1); } - const dir = options.dir || generator.defaultDir; + const dir = flags.dir || generator.defaultDir; const fileName = `${toSnakeCase(name)}.ts`; const filePath = path.join(process.cwd(), dir, fileName); @@ -339,7 +332,7 @@ const generateMetadataCommand = new Command('metadata') console.log(` ${chalk.dim('File:')} ${chalk.white(path.join(dir, fileName))}`); console.log(''); - if (options.dryRun) { + if (flags.dryRun) { printInfo('Dry run — no files written'); console.log(''); console.log(chalk.dim(' Content:')); @@ -395,14 +388,9 @@ const generateMetadataCommand = new Command('metadata') printError(error.message || String(error)); process.exit(1); } - }); - -const generateTypesCommand = new Command('types') - .description('Generate TypeScript type definitions from ObjectStack configuration') - .argument('[config]', 'Configuration file path') - .option('-o, --output ', 'Output file path', 'src/types/objectstack.d.ts') - .option('--dry-run', 'Show what would be generated without writing files') - .action(async (configPath, options) => { +} + +async function runTypesGeneration(configPath: string | undefined, flags: { output: string; dryRun?: boolean }): Promise { printHeader('Generate Types'); try { @@ -411,12 +399,12 @@ const generateTypesCommand = new Command('types') const { config, absolutePath } = await loadConfig(configPath); console.log(` ${chalk.dim('Config:')} ${chalk.white(absolutePath)}`); - console.log(` ${chalk.dim('Output:')} ${chalk.white(options.output)}`); + console.log(` ${chalk.dim('Output:')} ${chalk.white(flags.output)}`); console.log(''); const content = generateTypesFromConfig(config as Record); - if (options.dryRun) { + if (flags.dryRun) { printInfo('Dry run — no files written'); console.log(''); for (const line of content.split('\n')) { @@ -426,20 +414,20 @@ const generateTypesCommand = new Command('types') return; } - const outPath = path.resolve(process.cwd(), options.output); + const outPath = path.resolve(process.cwd(), flags.output); const outDir = path.dirname(outPath); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } fs.writeFileSync(outPath, content); - printSuccess(`Generated types at ${options.output}`); + printSuccess(`Generated types at ${flags.output}`); console.log(''); } catch (error: any) { printError(error.message || String(error)); process.exit(1); } - }); +} // ─── Client SDK Generator ─────────────────────────────────────────── @@ -541,12 +529,7 @@ function generateClientFromConfig(config: Record): string { return lines.join('\n') + '\n'; } -const generateClientCommand = new Command('client') - .description('Generate a type-safe client SDK from ObjectStack configuration') - .argument('[config]', 'Configuration file path') - .option('-o, --output ', 'Output file path', 'src/client/objectstack-client.ts') - .option('--dry-run', 'Show output without writing') - .action(async (configPath, options) => { +async function runClientGeneration(configPath: string | undefined, flags: { output: string; dryRun?: boolean }): Promise { printHeader('Generate Client SDK'); try { @@ -556,13 +539,13 @@ const generateClientCommand = new Command('client') const { config, absolutePath } = await loadConfig(configPath); console.log(` ${chalk.dim('Config:')} ${chalk.white(absolutePath)}`); - console.log(` ${chalk.dim('Output:')} ${chalk.white(options.output)}`); + console.log(` ${chalk.dim('Output:')} ${chalk.white(flags.output)}`); console.log(''); printStep('Generating client SDK...'); const content = generateClientFromConfig(config as Record); - if (options.dryRun) { + if (flags.dryRun) { printInfo('Dry run — no files written'); console.log(''); for (const line of content.split('\n')) { @@ -572,20 +555,20 @@ const generateClientCommand = new Command('client') return; } - const outPath = path.resolve(process.cwd(), options.output); + const outPath = path.resolve(process.cwd(), flags.output); const outDir = path.dirname(outPath); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } fs.writeFileSync(outPath, content); - printSuccess(`Generated client SDK at ${options.output} (${timer.display()})`); + printSuccess(`Generated client SDK at ${flags.output} (${timer.display()})`); console.log(''); } catch (error: any) { printError(error.message || String(error)); process.exit(1); } - }); +} // ─── Migration Generator ──────────────────────────────────────────── @@ -777,13 +760,7 @@ function generateMigrationTs(config: Record): string { return lines.join('\n') + '\n'; } -const generateMigrationCommand = new Command('migration') - .description('Generate database migration from ObjectStack schema') - .argument('[config]', 'Configuration file path') - .option('-o, --output ', 'Output file path') - .option('--format ', 'Output format: sql or typescript', 'typescript') - .option('--dry-run', 'Show output without writing') - .action(async (configPath, options) => { +async function runMigrationGeneration(configPath: string | undefined, flags: { output?: string; format: string; dryRun?: boolean }): Promise { printHeader('Generate Migration'); try { @@ -792,23 +769,23 @@ const generateMigrationCommand = new Command('migration') printInfo('Loading configuration...'); const { config, absolutePath } = await loadConfig(configPath); - const ext = options.format === 'sql' ? 'sql' : 'ts'; + const ext = flags.format === 'sql' ? 'sql' : 'ts'; // Format: YYYYMMDDHHmmss (e.g. 20250101120000) const timestamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14); const defaultOutput = `migrations/${timestamp}_migration.${ext}`; - const output = options.output || defaultOutput; + const output = flags.output || defaultOutput; console.log(` ${chalk.dim('Config:')} ${chalk.white(absolutePath)}`); - console.log(` ${chalk.dim('Format:')} ${chalk.white(options.format)}`); + console.log(` ${chalk.dim('Format:')} ${chalk.white(flags.format)}`); console.log(` ${chalk.dim('Output:')} ${chalk.white(output)}`); console.log(''); printStep('Generating migration...'); - const content = options.format === 'sql' + const content = flags.format === 'sql' ? generateMigrationSql(config as Record) : generateMigrationTs(config as Record); - if (options.dryRun) { + if (flags.dryRun) { printInfo('Dry run — no files written'); console.log(''); for (const line of content.split('\n')) { @@ -831,15 +808,11 @@ const generateMigrationCommand = new Command('migration') printError(error.message || String(error)); process.exit(1); } - }); +} // ─── JSON Schema Generator ────────────────────────────────────────── -const generateSchemaCommand = new Command('schema') - .description('Generate JSON Schema for objectstack.config.ts (for IDE autocomplete)') - .option('-o, --output ', 'Output file path', 'objectstack.schema.json') - .option('--dry-run', 'Show output without writing') - .action(async (options) => { +async function runSchemaGeneration(flags: { output: string; dryRun?: boolean }): Promise { printHeader('Generate Schema'); try { @@ -864,20 +837,20 @@ const generateSchemaCommand = new Command('schema') const content = JSON.stringify(schema, null, 2) + '\n'; - if (options.dryRun) { + if (flags.dryRun) { printInfo('Dry run — no files written'); console.log(''); console.log(content); return; } - const outPath = path.resolve(process.cwd(), options.output); + const outPath = path.resolve(process.cwd(), flags.output); const outDir = path.dirname(outPath); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } fs.writeFileSync(outPath, content); - printSuccess(`Generated JSON Schema at ${options.output} (${timer.display()})`); + printSuccess(`Generated JSON Schema at ${flags.output} (${timer.display()})`); console.log(''); console.log(chalk.dim(' Usage: Reference in your IDE or editor for autocomplete')); console.log(chalk.dim(` Path: ${outPath}`)); @@ -887,47 +860,65 @@ const generateSchemaCommand = new Command('schema') printError(error.message || String(error)); process.exit(1); } - }); +} // ─── Main Generate Command ────────────────────────────────────────── -export const generateCommand = new Command('generate') - .alias('g') - .description('Generate metadata files or TypeScript types') - .argument('[type]', 'Metadata type to generate (object, view, action, flow, agent, dashboard, app)') - .argument('[name]', 'Name for the metadata (use kebab-case)') - .option('-d, --dir ', 'Target directory (overrides default)') - .option('--dry-run', 'Show what would be created without writing files') - .addCommand(generateTypesCommand) - .addCommand(generateClientCommand) - .addCommand(generateMigrationCommand) - .addCommand(generateSchemaCommand) - .action(async (type: string | undefined, name: string | undefined, options) => { - if (!type) { - printHeader('Generate'); - console.log(chalk.bold(' Sub-commands:')); - console.log(` ${chalk.cyan('types'.padEnd(12))} Generate TypeScript type definitions from config`); - console.log(` ${chalk.cyan('client'.padEnd(12))} Generate a type-safe client SDK from config`); - console.log(` ${chalk.cyan('migration'.padEnd(12))} Generate database migration from schema`); - console.log(` ${chalk.cyan('schema'.padEnd(12))} Generate JSON Schema for objectstack.config.ts (IDE autocomplete)`); - console.log(''); - console.log(chalk.bold(' Metadata types:')); - for (const [key, gen] of Object.entries(GENERATORS)) { - console.log(` ${chalk.cyan(key.padEnd(12))} ${chalk.dim(gen.description)}`); - } - console.log(''); - console.log(chalk.dim(' Usage: objectstack generate ')); - console.log(chalk.dim(' Usage: objectstack generate types [config]')); - return; +export default class Generate extends Command { + static override description = 'Generate metadata files or TypeScript types'; + + static override aliases = ['g']; + + static override args = { + type: Args.string({ description: 'Metadata type to generate (object, view, action, flow, agent, dashboard, app)', required: true }), + name: Args.string({ description: 'Name for the metadata (use kebab-case)', required: false }), + }; + + static override flags = { + dir: Flags.string({ char: 'd', description: 'Target directory (overrides default)' }), + 'dry-run': Flags.boolean({ description: 'Show what would be created without writing files' }), + output: Flags.string({ char: 'o', description: 'Output file path' }), + format: Flags.string({ description: 'Output format: sql or typescript', default: 'typescript' }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(Generate); + + // Route to sub-commands by type name + switch (args.type) { + case 'types': + return runTypesGeneration(args.name, { + output: flags.output ?? 'src/types/objectstack.d.ts', + dryRun: flags['dry-run'], + }); + case 'client': + return runClientGeneration(args.name, { + output: flags.output ?? 'src/client/objectstack-client.ts', + dryRun: flags['dry-run'], + }); + case 'migration': + return runMigrationGeneration(args.name, { + output: flags.output, + format: flags.format ?? 'typescript', + dryRun: flags['dry-run'], + }); + case 'schema': + return runSchemaGeneration({ + output: flags.output ?? 'objectstack.schema.json', + dryRun: flags['dry-run'], + }); } - // Delegate to metadata command action - if (!name) { + // Metadata generation + if (!args.name) { printError('Missing required argument: '); console.log(chalk.dim(' Usage: objectstack generate ')); process.exit(1); } - // Execute metadata generation inline - await generateMetadataCommand.parseAsync([type, name, ...process.argv.slice(4)], { from: 'user' }); - }); + await runMetadataGeneration(args.type, args.name, { + dir: flags.dir, + dryRun: flags['dry-run'], + }); + } +} diff --git a/packages/cli/src/commands/info.ts b/packages/cli/src/commands/info.ts index 2dd37d7a61..18130b5296 100644 --- a/packages/cli/src/commands/info.ts +++ b/packages/cli/src/commands/info.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Command } from 'commander'; +import { Args, Command, Flags } from '@oclif/core'; import chalk from 'chalk'; import { normalizeStackInput } from '@objectstack/spec'; import { loadConfig } from '../utils/config.js'; @@ -15,23 +15,31 @@ import { printMetadataStats, } from '../utils/format.js'; -export const infoCommand = new Command('info') - .description('Display metadata summary of an ObjectStack configuration') - .argument('[config]', 'Configuration file path') - .option('--json', 'Output as JSON') - .action(async (configPath, options) => { +export default class Info extends Command { + static override description = 'Display metadata summary of an ObjectStack configuration'; + + static override args = { + config: Args.string({ description: 'Configuration file path', required: false }), + }; + + static override flags = { + json: Flags.boolean({ description: 'Output as JSON' }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(Info); const timer = createTimer(); - if (!options.json) { + if (!flags.json) { printHeader('Info'); } try { - const { config: rawConfig, absolutePath, duration } = await loadConfig(configPath); + const { config: rawConfig, absolutePath, duration } = await loadConfig(args.config); const config: any = normalizeStackInput(rawConfig as Record); const stats = collectMetadataStats(config); - if (options.json) { + if (flags.json) { console.log(JSON.stringify({ config: absolutePath, manifest: config.manifest || null, @@ -104,7 +112,7 @@ export const infoCommand = new Command('info') console.log(''); } catch (error: any) { - if (options.json) { + if (flags.json) { console.log(JSON.stringify({ error: error.message })); process.exit(1); } @@ -112,4 +120,5 @@ export const infoCommand = new Command('info') printError(error.message || String(error)); process.exit(1); } - }); + } +} diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index cf8e0c5be3..2230b0621e 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -1,12 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Command } from 'commander'; +import { Args, Command, Flags } from '@oclif/core'; import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; import { printHeader, printSuccess, printError, printStep, printKV, printInfo } from '../utils/format.js'; -const TEMPLATES: Record; devDependencies: Record; @@ -179,33 +179,48 @@ function toTitleCase(str: string): string { return str.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); } -export const initCommand = new Command('init') - .description('Initialize a new ObjectStack project in the current directory') - .argument('[name]', 'Project name (defaults to directory name)') - .option('-t, --template