Skip to content

Commit b3a8a81

Browse files
Copilothotlong
andcommitted
feat: update CLI extension spec, README, and bin entry points for oclif
- 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>
1 parent dd1e67b commit b3a8a81

5 files changed

Lines changed: 209 additions & 139 deletions

File tree

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
"description": "ObjectStack Protocol & Specification - Monorepo for TypeScript Interfaces, JSON Schemas, and Convention Configurations",
66
"scripts": {
77
"build": "turbo run build --filter=!@objectstack/docs",
8-
"dev": "pnpm --filter @objectstack/cli build && node packages/cli/bin/objectstack.js serve --dev",
8+
"dev": "pnpm --filter @objectstack/cli build && node packages/cli/bin/run.js serve --dev",
99
"dev:studio": "pnpm --filter @objectstack/studio dev",
10-
"studio": "pnpm --filter @objectstack/cli build && node packages/cli/bin/objectstack.js studio",
10+
"studio": "pnpm --filter @objectstack/cli build && node packages/cli/bin/run.js studio",
1111
"test": "turbo run test --filter=@objectstack/spec",
1212
"clean": "turbo run clean && rm -rf dist",
13-
"doctor": "pnpm --filter @objectstack/cli build && node packages/cli/bin/objectstack.js doctor",
13+
"doctor": "pnpm --filter @objectstack/cli build && node packages/cli/bin/run.js doctor",
1414
"setup": "pnpm install && pnpm --filter @objectstack/spec build",
1515
"version": "changeset version",
1616
"release": "pnpm run build && changeset publish",

packages/cli/README.md

Lines changed: 98 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Command Line Interface for building metadata-driven applications with the ObjectStack Protocol.
44

5+
Built on [oclif](https://oclif.io/) — commands are auto-discovered, and plugins can extend the CLI without modifying the main package.
6+
57
## Installation
68

79
```bash
@@ -40,6 +42,7 @@ os compile
4042
| `os init [name]` | Initialize a new ObjectStack project in the current directory |
4143
| `os dev [package]` | Start development mode with hot reload |
4244
| `os serve [config]` | Start the ObjectStack server with plugin auto-detection |
45+
| `os studio [config]` | Launch Studio UI with development server |
4346

4447
### Build & Validate
4548

@@ -73,6 +76,20 @@ Available generate types: `object`, `view`, `action`, `flow`, `agent`, `dashboar
7376
|---------|-------------|
7477
| `os test [files]` | Run Quality Protocol test scenarios against a running server |
7578
| `os doctor` | Check development environment health |
79+
| `os lint [config]` | Check configuration for style and convention issues |
80+
| `os diff [before] [after]` | Compare two configurations and detect breaking changes |
81+
82+
### Reference
83+
84+
| Command | Description |
85+
|---------|-------------|
86+
| `os explain [schema]` | Display human-readable explanation of an ObjectStack schema |
87+
88+
### Code Transforms
89+
90+
| Command | Description |
91+
|---------|-------------|
92+
| `os codemod v2-to-v3` | Migrate ObjectStack v2 config to v3 format |
7693

7794
## Configuration
7895

@@ -120,12 +137,12 @@ export default defineStack({
120137

121138
- `-p, --port <port>` — Server port (default: `3000`)
122139
- `--dev` — Run in development mode (load devPlugins, pretty logging)
140+
- `--ui` — Enable Studio UI
123141
- `--no-server` — Skip starting HTTP server plugin
124142

125143
### `os generate`
126144

127145
- `-d, --dir <directory>` — Override target directory
128-
- `--dry-run` — Preview without writing files
129146

130147
### `os plugin list`
131148

@@ -140,75 +157,78 @@ export default defineStack({
140157

141158
- `-c, --config <path>` — Configuration file path
142159

143-
## Plugin CLI Extensions
160+
### `os info`
161+
162+
- `--json` — Output as JSON
144163

145-
Plugins can extend the CLI with custom commands via the `contributes.commands` manifest field. The CLI automatically discovers and loads these commands at startup.
164+
### `os doctor`
146165

147-
### How to Create a CLI Plugin
166+
- `-v, --verbose` — Show fix suggestions for warnings
167+
- `--scan-deprecations` — Scan for deprecated patterns
148168

149-
**1. Declare commands in the plugin manifest:**
169+
## oclif Plugin System
150170

151-
```typescript
152-
export default defineStack({
153-
manifest: {
154-
id: 'com.acme.marketplace',
155-
version: '1.0.0',
156-
type: 'plugin',
157-
name: 'Marketplace Plugin',
158-
contributes: {
159-
commands: [
160-
{
161-
name: 'marketplace',
162-
description: 'Manage marketplace applications',
163-
module: './dist/cli.js',
164-
},
165-
],
166-
},
167-
},
168-
});
171+
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.
172+
173+
### How Plugin Extension Works
174+
175+
1. **Create an oclif plugin package** with its own `oclif` config in `package.json`
176+
2. **Export oclif Command classes** from the plugin's `src/commands/` directory
177+
3. **Install the plugin** via `os plugins install <package>` or declare it in the main CLI's `oclif.plugins`
178+
179+
### Creating a CLI Plugin
180+
181+
**1. Configure the plugin's `package.json`:**
182+
183+
```json
184+
{
185+
"name": "@acme/plugin-marketplace",
186+
"oclif": {
187+
"commands": {
188+
"strategy": "pattern",
189+
"target": "./dist/commands",
190+
"glob": "**/*.js"
191+
}
192+
}
193+
}
169194
```
170195

171-
**2. Export Commander.js commands from the module:**
196+
**2. Create oclif Command classes:**
172197

173198
```typescript
174-
// src/cli.ts
175-
import { Command } from 'commander';
176-
177-
const marketplaceCommand = new Command('marketplace')
178-
.description('Manage marketplace applications')
179-
.addCommand(
180-
new Command('search')
181-
.argument('<query>')
182-
.action(async (query) => { /* ... */ })
183-
)
184-
.addCommand(
185-
new Command('install')
186-
.argument('<app>')
187-
.action(async (app) => { /* ... */ })
188-
);
189-
190-
// Named export (recommended)
191-
export const commands = [marketplaceCommand];
192-
// Also supports: export default Command | Command[]
199+
// src/commands/marketplace/search.ts
200+
import { Args, Command, Flags } from '@oclif/core';
201+
202+
export default class MarketplaceSearch extends Command {
203+
static override description = 'Search marketplace applications';
204+
205+
static override args = {
206+
query: Args.string({ description: 'Search query', required: true }),
207+
};
208+
209+
async run() {
210+
const { args } = await this.parse(MarketplaceSearch);
211+
// Implementation...
212+
}
213+
}
193214
```
194215

195-
**3. Register the plugin in the host project and use:**
216+
**3. Install and use:**
196217

197218
```bash
198-
os plugin add @acme/plugin-marketplace
219+
os plugins install @acme/plugin-marketplace
199220
os marketplace search "crm"
200-
os marketplace install com.acme.crm
201221
```
202222

203-
For a complete guide, see the [Plugin CLI Extensions](/docs/guides/plugins#cli-command-extensions) section in the Plugins guide.
223+
### Key Differences from Previous Plugin Model
204224

205-
### `os info`
206-
207-
- `--json` — Output as JSON
208-
209-
### `os doctor`
210-
211-
- `-v, --verbose` — Show fix suggestions for warnings
225+
| Before (Commander.js) | After (oclif) |
226+
|---|---|
227+
| Plugins declared in `objectstack.config.ts` | Plugins installed via `os plugins install` or `oclif.plugins` |
228+
| Custom `loadPluginCommands` mechanism | oclif's built-in plugin discovery |
229+
| `contributes.commands` in manifest | `oclif.commands` in `package.json` |
230+
| Commander.js `new Command(...)` exports | oclif `class extends Command` exports |
231+
| Project config determines CLI commands | CLI commands available without project init |
212232

213233
## Typical Workflow
214234

@@ -223,6 +243,30 @@ os dev # 7. Start dev server
223243
os compile # 8. Build for production
224244
```
225245

246+
## Architecture
247+
248+
```
249+
@objectstack/cli (oclif)
250+
├── bin/run.js # Entry point (os / objectstack)
251+
├── src/commands/ # Auto-discovered command classes
252+
│ ├── init.ts # os init
253+
│ ├── dev.ts # os dev
254+
│ ├── serve.ts # os serve
255+
│ ├── compile.ts # os compile
256+
│ ├── validate.ts # os validate
257+
│ ├── generate.ts # os generate (alias: g)
258+
│ ├── plugin/ # os plugin <subcommand>
259+
│ │ ├── list.ts
260+
│ │ ├── info.ts
261+
│ │ ├── add.ts
262+
│ │ └── remove.ts
263+
│ ├── codemod/ # os codemod <subcommand>
264+
│ │ └── v2-to-v3.ts
265+
│ └── ...
266+
├── src/utils/ # Shared utilities
267+
└── package.json # oclif config under "oclif" key
268+
```
269+
226270
## License
227271

228272
Apache-2.0

packages/cli/bin/objectstack.js

Lines changed: 0 additions & 2 deletions
This file was deleted.

packages/spec/src/kernel/cli-extension.test.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect } from 'vitest';
22
import {
33
CLICommandContributionSchema,
4-
CLIExtensionExportSchema,
4+
OclifPluginConfigSchema,
55
} from './cli-extension.zod';
66

77
describe('CLICommandContributionSchema', () => {
@@ -18,9 +18,9 @@ describe('CLICommandContributionSchema', () => {
1818
const result = CLICommandContributionSchema.parse({
1919
name: 'deploy',
2020
description: 'Deploy to cloud',
21-
module: './dist/cli.js',
21+
module: './dist/commands/deploy.js',
2222
});
23-
expect(result.module).toBe('./dist/cli.js');
23+
expect(result.module).toBe('./dist/commands/deploy.js');
2424
});
2525

2626
it('should accept minimal command (name only)', () => {
@@ -64,30 +64,44 @@ describe('CLICommandContributionSchema', () => {
6464
});
6565
});
6666

67-
describe('CLIExtensionExportSchema', () => {
68-
it('should accept named commands export', () => {
69-
const result = CLIExtensionExportSchema.parse({
70-
commands: [{}, {}],
67+
describe('OclifPluginConfigSchema', () => {
68+
it('should accept valid oclif plugin config', () => {
69+
const result = OclifPluginConfigSchema.parse({
70+
commands: {
71+
strategy: 'pattern',
72+
target: './dist/commands',
73+
glob: '**/*.js',
74+
},
7175
});
72-
expect(result.commands).toHaveLength(2);
76+
expect(result.commands?.strategy).toBe('pattern');
77+
expect(result.commands?.target).toBe('./dist/commands');
7378
});
7479

75-
it('should accept default export', () => {
76-
const result = CLIExtensionExportSchema.parse({
77-
default: {},
80+
it('should accept config with topicSeparator', () => {
81+
const result = OclifPluginConfigSchema.parse({
82+
commands: { strategy: 'pattern' },
83+
topicSeparator: ' ',
7884
});
79-
expect(result.default).toBeDefined();
85+
expect(result.topicSeparator).toBe(' ');
8086
});
8187

82-
it('should accept default array export', () => {
83-
const result = CLIExtensionExportSchema.parse({
84-
default: [{}, {}],
88+
it('should accept empty config', () => {
89+
const result = OclifPluginConfigSchema.parse({});
90+
expect(result).toBeDefined();
91+
});
92+
93+
it('should accept config with only commands', () => {
94+
const result = OclifPluginConfigSchema.parse({
95+
commands: {
96+
strategy: 'explicit',
97+
},
8598
});
86-
expect(Array.isArray(result.default)).toBe(true);
99+
expect(result.commands?.strategy).toBe('explicit');
87100
});
88101

89-
it('should accept empty object', () => {
90-
const result = CLIExtensionExportSchema.parse({});
91-
expect(result).toBeDefined();
102+
it('should reject invalid strategy', () => {
103+
expect(() => OclifPluginConfigSchema.parse({
104+
commands: { strategy: 'invalid' },
105+
})).toThrow();
92106
});
93107
});

0 commit comments

Comments
 (0)