feat: migrate CLI architecture from Commander.js to oclif#704
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Convert the init command to use oclif Command class pattern: - Replace Commander import with @oclif/core (Args, Command, Flags) - Convert initCommand to Init class extending Command - Map .argument() to static args, .option() to static flags - Replace .action() handler with async run() method - Replace process.exit(1) with this.error() for oclif convention - Export TEMPLATES for external consumers - Update bin.ts to remove Commander-based initCommand registration - Update test to check oclif static properties Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate the compile command to use oclif's class-based Command pattern, matching the existing init command structure. This includes: - Replace Commander.js fluent API with oclif Args/Flags static properties - Export default class instead of named command instance - Replace process.exit(1) with this.exit(1) and this.error() - Remove compile command registration from bin.ts (oclif auto-discovers) - Update test assertions to use oclif class properties Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate the dev command to use oclif's class-based Command pattern with static args/flags definitions instead of Commander.js's fluent API. - Import Args, Command, Flags from @oclif/core - Export default class Dev extending Command - Define static args (package) and flags (watch, ui, verbose) - Use this.parse(Dev) to extract args/flags - Preserve all existing business logic unchanged Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r.js to oclif Convert three CLI command files from Commander.js pattern (named exports with chained .description/.argument/.option/.action) to oclif class-based pattern (default class exports with static args/flags and async run()). - serve.ts: Serve class with port, dev, ui, server flags - studio.ts: Studio class with config arg and port flag - validate.ts: Validate class with config arg, strict and json flags - Replace process.exit(1) with this.exit(1) in command context - Replace options.X with flags.X, positional args with args.X Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
….js to oclif Convert three CLI command files from Commander.js pattern to oclif pattern: - info.ts: Convert to oclif Command class with Args/Flags - create.ts: Convert to oclif Command class, preserve exported templates - generate.ts: Convert main command and sub-commands to oclif pattern, extract internal Commander sub-commands into plain async functions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert test, doctor, lint, and diff commands from Commander.js pattern to oclif class-based pattern with Args/Flags static properties. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… to oclif - Convert explain command to oclif class pattern with Args/Flags - Split codemod command into codemod/v2-to-v3.ts subcommand - Delete old codemod.ts (oclif auto-discovers subcommands from directory) - Update bin.ts to remove Commander.js imports and addCommand calls Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Convert all 16 commands to oclif Command classes - Split plugin subcommands into topic structure (plugin/list, plugin/add, etc.) - Split codemod subcommands into topic structure (codemod/v2-to-v3) - Add oclif configuration to package.json - Create oclif bin entry points (bin/run.js, bin/run-dev.js) - Remove Commander.js dependency and loadPluginCommands mechanism - Update all tests for oclif command classes (35 tests passing) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
- Update CLIExtensionSpec to reference oclif plugin model - Add OclifPluginConfigSchema for plugin package.json validation - Update cli-extension.test.ts for new schemas (11 tests passing) - Update README.md with oclif architecture, plugin docs, migration table - Remove old bin/objectstack.js, add bin/run.js and bin/run-dev.js - Update root package.json scripts to use new bin entry Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Migrates the @objectstack/cli package from a Commander.js-based CLI (with custom plugin command loading) to an oclif-based CLI to enable native command auto-discovery and a decoupled plugin model, and updates the spec/kernel protocol docs + validation accordingly.
Changes:
- Replaced Commander.js command definitions with oclif
Commandclasses (including topic-basedplugin/*andcodemod/*subcommands). - Switched CLI build/entrypoints to oclif-friendly
tscoutput structure and added oclif runtime config/plugins inpackage.json. - Updated spec protocol docs/schema/tests for the new oclif plugin configuration model.
Reviewed changes
Copilot reviewed 33 out of 37 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds oclif dependencies and updates lockfile snapshot graph accordingly. |
| packages/spec/src/kernel/cli-extension.zod.ts | Updates CLI extension protocol docs and introduces OclifPluginConfigSchema. |
| packages/spec/src/kernel/cli-extension.test.ts | Replaces Commander export-shape tests with oclif config validation tests. |
| packages/cli/tsconfig.build.json | Adds a dedicated tsc build config emitting dist/ with preserved structure. |
| packages/cli/test/plugin.test.ts | Updates plugin command tests to assert oclif static properties. |
| packages/cli/test/plugin-commands.test.ts | Replaces tests for removed custom plugin loader with package.json oclif config assertions. |
| packages/cli/test/commands.test.ts | Updates command tests to import/assert oclif command classes. |
| packages/cli/src/utils/plugin-helpers.ts | Extracts shared plugin name/version/type helpers for oclif plugin commands. |
| packages/cli/src/utils/plugin-commands.ts | Removes Commander-specific loadPluginCommands implementation. |
| packages/cli/src/index.ts | Updates public exports to re-export oclif command classes for programmatic use/tests. |
| packages/cli/src/commands/compile.ts | Migrates compile to oclif Command. |
| packages/cli/src/commands/create.ts | Migrates create to oclif Command. |
| packages/cli/src/commands/dev.ts | Migrates dev to oclif Command. |
| packages/cli/src/commands/diff.ts | Migrates diff to oclif Command. |
| packages/cli/src/commands/doctor.ts | Migrates doctor to oclif Command. |
| packages/cli/src/commands/explain.ts | Migrates explain to oclif Command. |
| packages/cli/src/commands/generate.ts | Migrates generate to oclif and routes sub-modes internally. |
| packages/cli/src/commands/info.ts | Migrates info to oclif Command. |
| packages/cli/src/commands/init.ts | Migrates init to oclif Command. |
| packages/cli/src/commands/lint.ts | Migrates lint to oclif Command. |
| packages/cli/src/commands/serve.ts | Migrates serve to oclif Command. |
| packages/cli/src/commands/studio.ts | Migrates studio to oclif Command and delegates to serve. |
| packages/cli/src/commands/test.ts | Migrates test to oclif Command. |
| packages/cli/src/commands/validate.ts | Migrates validate to oclif Command. |
| packages/cli/src/commands/plugin/add.ts | Introduces os plugin add as an oclif subcommand. |
| packages/cli/src/commands/plugin/info.ts | Introduces os plugin info as an oclif subcommand. |
| packages/cli/src/commands/plugin/list.ts | Introduces os plugin list as an oclif subcommand. |
| packages/cli/src/commands/plugin/remove.ts | Introduces os plugin remove as an oclif subcommand. |
| packages/cli/src/commands/codemod/v2-to-v3.ts | Migrates codemod to an oclif topic subcommand (codemod v2-to-v3). |
| packages/cli/src/commands/plugin.ts | Removes old Commander aggregated plugin command implementation. |
| packages/cli/src/bin.ts | Replaces Commander bootstrap with an oclif execute export. |
| packages/cli/package.json | Adds oclif config/plugins, updates bin entry, and switches build scripts to tsc. |
| packages/cli/bin/run.js | New oclif runtime entrypoint for published CLI. |
| packages/cli/bin/run-dev.js | New tsx-based dev runner for oclif. |
| packages/cli/bin/objectstack.js | Removes old bin wrapper for Commander-based entrypoint. |
| packages/cli/README.md | Updates CLI docs for oclif architecture and plugin model. |
| package.json | Updates repo-level scripts to invoke the new oclif runner. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| static override flags = { | ||
| dev: Flags.boolean({ char: 'd', description: 'Add as a dev-only plugin' }), | ||
| config: Flags.string({ char: 'c', description: 'Configuration file path' }), | ||
| }; |
There was a problem hiding this comment.
--dev is defined for PluginAdd, but the flag is never used when editing objectstack.config.ts (the plugin is always added to plugins). Either implement --dev by inserting into devPlugins, or remove the flag to avoid a misleading option.
| console.log(''); | ||
| printError(error.message || String(error)); | ||
| process.exit(1); | ||
| this.error(error.message || String(error)); |
There was a problem hiding this comment.
In the non-JSON error path, this prints the error via printError(...) and then calls this.error(...), which will also print/format an error and throw. This can lead to duplicate error output. Consider using either this.error(...) alone, or printing via printError(...) and then this.exit(1).
| this.error(error.message || String(error)); | |
| this.exit(1); |
| 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 }), | ||
| }; |
There was a problem hiding this comment.
Generate now declares type as a required arg. This changes the UX from the previous implementation where running os generate with no args printed available subcommands/types; oclif will now error immediately for missing args. If you want to preserve the old behavior, make type optional and handle the empty case in run() (or split types/client/migration/schema into their own oclif commands).
| // In dev mode, Studio UI is enabled by default (use --no-ui to disable). | ||
| // Always serves the pre-built dist/ — no Vite dev server, no extra port. | ||
| const enableUI = options.ui ?? isDev; | ||
| const enableUI = flags.ui || isDev; |
There was a problem hiding this comment.
const enableUI = flags.ui || isDev; prevents users from disabling UI in dev mode (an explicit --no-ui would still result in enableUI === true when isDev is true). If the intent is “UI defaults to enabled in dev mode but can be explicitly turned off,” compute enableUI so an explicit user choice overrides the dev default.
| const enableUI = flags.ui || isDev; | |
| const enableUI = flags.ui ?? isDev; |
| if (!template) { | ||
| printError(`Unknown template: ${options.template}`); | ||
| printError(`Unknown template: ${flags.template}`); | ||
| console.log(chalk.dim(` Available: ${Object.keys(TEMPLATES).join(', ')}`)); | ||
| process.exit(1); | ||
| this.error(`Unknown template: ${flags.template}`); | ||
| } |
There was a problem hiding this comment.
This branch prints an error with printError(...) and then calls this.error(...), which also reports an error and throws. That can result in duplicated messaging and inconsistent formatting. Consider choosing one mechanism (either this.error(...) only, or printError(...) + this.exit(1)).
| * "commands": { | ||
| * "strategy": "pattern", | ||
| * "target": "./dist/commands", | ||
| * "glob": "**\/*.js" | ||
| * } |
There was a problem hiding this comment.
The JSON example shows "glob": "**\/*.js", which isn’t a valid glob pattern (the backslash is literal in JSON). This looks like an accidental escape; it should be "**/*.js" to match typical oclif configs.
| if (!args.name) { | ||
| printError('Missing required argument: <name>'); | ||
| console.log(chalk.dim(' Usage: objectstack generate <type> <name>')); | ||
| process.exit(1); |
There was a problem hiding this comment.
Inside an oclif Command, using process.exit(1) (here for missing <name>) bypasses oclif’s exit/error handling and can make testing/hook behavior inconsistent. Prefer this.exit(1) or this.error(...) for command failures.
| process.exit(1); | |
| this.exit(1); |
| "@oclif/plugin-help": "^6.2.37", | ||
| "@oclif/plugin-plugins": "^5.4.56", | ||
| "@types/node": "^25.2.2", | ||
| "tsup": "^8.0.2", |
There was a problem hiding this comment.
tsup is still listed in devDependencies, but the package scripts now build/watch via tsc -p tsconfig.build.json and there are no remaining script references to tsup. If it’s no longer needed, consider removing it (and any related config) to reduce dependency surface.
| "tsup": "^8.0.2", |
| static override flags = { | ||
| port: Flags.string({ char: 'p', description: 'Server port', default: '3000' }), | ||
| dev: Flags.boolean({ description: 'Run in development mode (load devPlugins)' }), | ||
| ui: Flags.boolean({ description: 'Enable Studio UI at /_studio/ (default: true in dev mode)' }), |
There was a problem hiding this comment.
The ui flag is described as supporting --no-ui, but it’s declared without allowNo: true, so oclif won’t accept --no-ui. If you want the documented behavior, define the flag with allowNo: true (and choose an appropriate default).
| ui: Flags.boolean({ description: 'Enable Studio UI at /_studio/ (default: true in dev mode)' }), | |
| ui: Flags.boolean({ description: 'Enable Studio UI at /_studio/ (default: true in dev mode)', allowNo: true }), |
Current CLI uses Commander.js with a custom
loadPluginCommandsmechanism that requiresobjectstack.config.tsto discover plugin commands — making cloud commands (os login,os push) unusable before project init (chicken-egg problem). Migrates to oclif for native plugin discovery, unifiedbin/osentry, and decoupled command extension.Command Migration
new Command('name').action(...)→class extends Command { async run() }pluginsubcommands split into topic dir:commands/plugin/{list,info,add,remove}.tscodemodsubcommands split into topic dir:commands/codemod/v2-to-v3.tsPlugin System
loadPluginCommands+plugin-commands.ts— replaced by oclif's native plugin system@oclif/plugin-helpfor auto-generated help,@oclif/plugin-pluginsfor runtime plugin installos plugins install <pkg>— no project config neededBuild & Entry Points
tsup→tsc -p tsconfig.build.json(oclif needs preserved file structure for command discovery)bin/run.js/bin/run-dev.jsusing oclifexecute(); removed oldbin/objectstack.jspackage.jsongainsoclifconfig section with pattern-based command discoverySpec Updates
cli-extension.zod.tsdocumentation updated for oclif plugin modelOclifPluginConfigSchemafor validating pluginpackage.jsonoclif sectionsCLIExtensionExportSchema(Commander.js-specific)Tests
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.