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
11 changes: 11 additions & 0 deletions .changeset/21285-drop-dead-oclif-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@objectstack/cli': patch
---

The published `package.json` no longer declares `oclif.plugins`, and the package no longer lists `@oclif/plugin-help` or `@oclif/plugin-plugins` as devDependencies. The array named both plugins, but they were only devDependencies, and oclif loads an `oclif.plugins` entry only when the same name is in `dependencies`. Neither plugin ever loaded.

Clause-②: no

**What changes for an operator.** Nothing. `os --help`, every command and topic, and the output of `os help` and `os plugins` read byte-identical before and after the change. `os help` and `os plugins …` were never commands, and each still exits 2 with `command … not found`. Use `os --help` or `os <command> --help` for help.

**What the README now says.** It said `os plugins install`, `uninstall` and `update` came from `@oclif/plugin-plugins` and installed CLI extensions. That was never true. This CLI ships no plugin manager. To add commands to it, build an `os` distribution: a package whose own `package.json` lists the extension in both `oclif.plugins` and `dependencies`.
16 changes: 9 additions & 7 deletions content/docs/plugins/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -399,13 +399,15 @@ export default class MarketplaceSearch extends Command {
#### Step 3: Load the Plugin into the CLI

<Callout type="warn">
**`os plugins install` is not available today.** `@objectstack/cli`'s `package.json`
lists `@oclif/plugin-plugins` under `oclif.plugins`, but the package sits in
`devDependencies` — and oclif's core-plugin loader only matches names that appear in
`dependencies`. The result is that neither `os plugins …` nor `os help` is a
registered command; `os --help` shows no `plugins` topic. Until that is fixed, load a
CLI extension by building an `os` distribution that lists the package in **both**
`oclif.plugins` and `dependencies`.
**`@objectstack/cli` ships no plugin manager.** Its `package.json` declares no
`oclif.plugins`, and it does not depend on `@oclif/plugin-plugins` or
`@oclif/plugin-help`. So `os plugins …` (`install`, `link`, `update`, …) and `os help`
are not commands: each exits 2 with `command … not found`. `os --help` is the help
entry, and it shows no `plugins` topic. To load a CLI extension, build an `os`
distribution: a package whose own `package.json` lists the extension in **both**
`oclif.plugins` and `dependencies`. oclif's core-plugin loader matches `oclif.plugins`
names only against `dependencies`, so a name listed under `devDependencies` alone never
loads.
</Callout>

#### Using the Extended CLI
Expand Down
7 changes: 4 additions & 3 deletions docs/qa/platform-checklist/areas/cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@
"title": "Wrong flags and unknown commands error with usage and a nonzero exit — never silently ignored, never executed anyway",
"since": "v16",
"status": "active",
"revision": 1,
"revision": 2,
"priority": "P2",
"surface": "cli",
"personas": ["operator (local shell)"],
Expand Down Expand Up @@ -655,11 +655,12 @@
],
"source": [
"packages/cli/src/commands/ (the 20 top-level commands + 10 topics enumerated as variants — oclif pattern discovery per package.json oclif.commands)",
"packages/cli/package.json (oclif.plugins = help + plugins only — no not-found plugin, so unknown commands hard-error)",
"packages/cli/package.json (no oclif.plugins at all — no plugin manager, no help command and no not-found plugin, so unknown commands hard-error)",
"@oclif/core parse contract (Nonexistent flag / enum FailedFlagValidation / command-not-found, exit 2)"
],
"history": [
{ "revision": 1, "date": "2026-08-07", "change": "new item: flag/command error UX with the full registered command surface enumerated from src/commands/ as variants, and never-executed-anyway as the load-bearing negative", "ref": "claude/platform-test-checklist-ocwugl" }
{ "revision": 1, "date": "2026-08-07", "change": "new item: flag/command error UX with the full registered command surface enumerated from src/commands/ as variants, and never-executed-anyway as the load-bearing negative", "ref": "claude/platform-test-checklist-ocwugl" },
{ "revision": 2, "date": "2026-10-02", "change": "source: package.json no longer declares oclif.plugins — the two listed plugins sat in devDependencies and never loaded, so the array was dropped; the registered surface and the unknown-command hard-error are unchanged (os --help and the full command table read byte-identical before and after)", "ref": "claude/issue-21285-drop-dead-oclif-plugins" }
]
},
{
Expand Down
15 changes: 8 additions & 7 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ Common variables: `OS_DATABASE_URL`, `OS_DATABASE_DRIVER`,

- `-d, --dir <directory>` — Override target directory

### `os plugins` (oclif)
### `os plugins` and `os help` (not commands)

`os plugins install`, `os plugins uninstall`, `os plugins update`, and friends come from `@oclif/plugin-plugins`. They install third-party CLI extensions (oclif plugins), not runtime plugins for an ObjectStack project. See [oclif's plugin docs](https://oclif.io/docs/plugins) for the full surface.
This package ships no oclif plugin manager: its `package.json` declares no `oclif.plugins` and does not depend on `@oclif/plugin-plugins` or `@oclif/plugin-help`. So `os plugins` (`install`, `uninstall`, `update`, `link`, and the rest) and `os help` are not registered commands; each exits 2 with `command … not found`. Use `os --help` or `os <command> --help` for help. To add third-party CLI commands, see [oclif Plugin System](#oclif-plugin-system).

### `os info`

Expand All @@ -218,13 +218,13 @@ Common variables: `OS_DATABASE_URL`, `OS_DATABASE_DRIVER`,

## oclif Plugin System

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.
The CLI is built on oclif, and oclif's plugin system is its only command-extension mechanism: a third-party package (e.g., cloud commands, marketplace tools) ships oclif Command classes and is loaded as an oclif plugin, without modifying this package. The `os` binary this package publishes declares no plugins and ships no plugin manager, so there is no `os plugins install`.

### 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 <package>` or declare it in the main CLI's `oclif.plugins`
3. **Load the plugin through an `os` distribution you build**: a package whose own `package.json` lists the plugin in **both** `oclif.plugins` and `dependencies`. oclif's core-plugin loader matches `oclif.plugins` names only against `dependencies`; a name listed under `devDependencies` alone never loads.

### Creating a CLI Plugin

Expand Down Expand Up @@ -263,18 +263,19 @@ export default class MarketplaceSearch extends Command {
}
```

**3. Install and use:**
**3. Load it through your `os` distribution, then use it:**

List `@acme/plugin-marketplace` in both `oclif.plugins` and `dependencies` of the distribution's `package.json` (see [How Plugin Extension Works](#how-plugin-extension-works)). Its commands then appear in that distribution's `os --help`:

```bash
os plugins install @acme/plugin-marketplace
os marketplace search "crm"
```

### Key Differences from Previous Plugin Model

| Before (Commander.js) | After (oclif) |
|---|---|
| Plugins declared in `objectstack.config.ts` | Plugins installed via `os plugins install` or `oclif.plugins` |
| Plugins declared in `objectstack.config.ts` | Plugins listed in an `os` distribution's `oclif.plugins` and `dependencies` |
| 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 |
Expand Down
19 changes: 10 additions & 9 deletions packages/cli/bin/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,16 @@ import { flush, handle, run, settings } from '@oclif/core';
* (`plugin?.type !== 'link'` guards the `isProduction` early return), and this
* setting is checked ahead of that — so a `plugins link`ed TypeScript plugin
* would no longer be auto-transpiled through this entry. ⭐ That path is not
* reachable today: `@oclif/plugin-plugins` sits in `devDependencies`, and
* oclif's core-plugin loader only matches names under `dependencies`, so
* `os plugins` is not a registered command at all (measured on this entry —
* `os --help` lists 34 topics and none of them is `plugins`; the count is the
* control, so the zero is a reading). `content/docs/plugins/index.mdx` says the
* same in its own words and tells an extension author to build an `os`
* distribution listing the package in both places. ⛔ If that is ever fixed,
* this line is what has to be revisited — the remedy is `bin/run-dev.js`, or
* building the plugin.
* reachable today: this package ships no plugin manager — `package.json`
* declares no `oclif.plugins` and does not depend on `@oclif/plugin-plugins` —
* so `os plugins` (and with it `os plugins link`) is not a registered command
* at all (measured on this entry — `os --help` lists 34 entries, 12 topics and
* 22 commands, and none of them is `plugins`; the count is the control, so the
* zero is a reading). `content/docs/plugins/index.mdx` says the same in its own
* words and tells an extension author to build an `os` distribution listing
* the package in both `oclif.plugins` and `dependencies`. ⛔ If a plugin
* manager is ever shipped, this line is what has to be revisited — the remedy
* is `bin/run-dev.js`, or building the plugin.
*
* The other change in behaviour is a convergence, not a loss: on an UNBUILT
* tree this file now answers oclif's "command not found" under
Expand Down
6 changes: 0 additions & 6 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@
"hooks": {
"preparse": "./dist/hooks/preparse/strip-arg-separator.js"
},
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-plugins"
],
"topicSeparator": " "
},
"dependencies": {
Expand Down Expand Up @@ -137,8 +133,6 @@
"@objectstack/connector-rest": "workspace:*",
"@objectstack/driver-turso": "workspace:*",
"@objectstack/plugin-dev": "workspace:*",
"@oclif/plugin-help": "^7.0.2",
"@oclif/plugin-plugins": "^7.0.3",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^26.6.3",
"typescript": "^6.0.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
*
* There is no `codemod` command. oclif resolves this CLI's commands by
* globbing `dist/commands/**` (`package.json` → `oclif.commands`, pattern
* strategy); nothing under `src/commands/` compiles to `codemod`, and neither
* bundled plugin (`@oclif/plugin-help`, `@oclif/plugin-plugins`) supplies one.
* strategy); nothing under `src/commands/` compiles to `codemod`, and no plugin
* supplies one (`package.json` declares no `oclif.plugins`).
* An operator who followed the prescription got oclif's exit 2,
* `command codemod:v2-to-v3 not found` — after spending their time on it.
* `content/docs/protocol/backward-compatibility.mdx` already recorded the
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2375,8 +2375,8 @@ export default class Doctor extends Command {
// #10680 — this line used to prescribe `objectstack codemod v2-to-v3`,
// a command `os` has never registered. oclif resolves commands by
// globbing `dist/commands/**/*.js` (package.json `oclif.commands`);
// there is no `src/commands/codemod*`, and neither bundled plugin
// (`@oclif/plugin-help`, `@oclif/plugin-plugins`) supplies one — so the
// there is no `src/commands/codemod*`, and no plugin supplies one (the
// package declares no `oclif.plugins`) — so the
// prescription exited 2, `command codemod:v2-to-v3 not found`, for every
// operator who followed it. `content/docs/protocol/backward-compatibility.mdx`
// already records the automated codemod as "not yet available".
Expand Down
78 changes: 55 additions & 23 deletions packages/cli/test/plugin-commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,75 @@
import { describe, it, expect } from 'vitest';
import { createRequire } from 'node:module';

/**
* The custom loadPluginCommands mechanism has been removed.
* Plugin command extension is now handled by oclif's built-in plugin system.
* The published `os` binary's oclif plugin surface.
*
* Plugins extend the CLI by:
* 1. Including `oclif` config in their package.json
* 2. Exporting oclif Command classes from `src/commands/`
* 3. Being installed via `os plugins install <package>`
* Command extension is handled by oclif's plugin system: a plugin package
* carries its own `oclif` config and exports oclif Command classes from
* `src/commands/`. The host project's `objectstack.config.ts` does not decide
* which CLI commands exist.
*
* The objectstack.config.ts no longer determines CLI command availability.
* This package ships NO plugin manager and loads no plugin of its own. It used
* to list `@oclif/plugin-help` and `@oclif/plugin-plugins` under
* `oclif.plugins` while both sat in `devDependencies`. `@oclif/core`'s
* core-plugin loader (`lib/config/plugin-loader.js`, `loadCorePlugins`)
* matches `oclif.plugins` names only against `dependencies`, so neither ever
* loaded: `os plugins …` and `os help` were never commands, and `os --help`
* read the same with or without the array. The array was dead configuration,
* and every text that read it as "`os plugins install` works" was false.
*
* What these pins hold is the state the published text now describes
* (`README.md` → "`os plugins` and `os help` (not commands)" and "oclif Plugin
* System"; `content/docs/plugins/index.mdx` → the Step 3 callout;
* `bin/run.js` → the `enableAutoTranspile` note on linked plugins). Shipping a
* plugin manager is a product change, not a manifest tweak: it lists the
* plugin in BOTH `oclif.plugins` and `dependencies`, and corrects those texts
* in the same change.
*/

describe('oclif Plugin System', () => {
it('should have oclif plugins configured in package.json', async () => {
const { createRequire } = await import('module');
const require = createRequire(import.meta.url);
const pkg = require('../package.json');
const require = createRequire(import.meta.url);
const pkg = require('../package.json');

const DEPENDENCY_FIELDS = [
'dependencies',
'optionalDependencies',
'peerDependencies',
'devDependencies',
] as const;

describe('oclif plugin surface — the published `os` ships no plugin manager', () => {
it('declares no oclif.plugins (oclif would load an entry only from `dependencies`)', () => {
expect(pkg.oclif).toBeDefined();
expect(pkg.oclif.plugins).toContain('@oclif/plugin-help');
expect(pkg.oclif.plugins).toContain('@oclif/plugin-plugins');
expect(
pkg.oclif.plugins,
'package.json `oclif.plugins` is back. An entry here loads only when the same name is in `dependencies` ' +
'(oclif loadCorePlugins); otherwise it is dead configuration. Either way README.md, ' +
'content/docs/plugins/index.mdx and bin/run.js state that this CLI ships no plugin manager — change them with it.',
).toBeUndefined();
});

it('should have oclif command discovery configured', async () => {
const { createRequire } = await import('module');
const require = createRequire(import.meta.url);
const pkg = require('../package.json');
it('depends on no @oclif/plugin-* package in any dependency field', () => {
const found = DEPENDENCY_FIELDS.flatMap((field) =>
Object.keys(pkg[field] ?? {})
.filter((name) => name.startsWith('@oclif/plugin-'))
.map((name) => `${field}: ${name}`),
);
expect(
found,
'an oclif plugin package is listed. Not named in `oclif.plugins` + `dependencies`, it never loads and is dead ' +
'weight; loaded, it makes `os plugins` / `os help` real and the published text false.',
).toEqual([]);
});
});

describe('oclif command discovery and entry points', () => {
it('discovers commands by pattern under dist/commands', () => {
expect(pkg.oclif.commands).toBeDefined();
expect(pkg.oclif.commands.strategy).toBe('pattern');
expect(pkg.oclif.commands.target).toBe('./dist/commands');
});

it('should have bin entries pointing to oclif runner', async () => {
const { createRequire } = await import('module');
const require = createRequire(import.meta.url);
const pkg = require('../package.json');

it('points both bin entries at the oclif runner', () => {
expect(pkg.bin.os).toBe('./bin/run.js');
expect(pkg.bin.objectstack).toBe('./bin/run.js');
});
Expand Down
Loading
Loading