Skip to content

Commit b9ac384

Browse files
authored
Merge pull request #607 from objectstack-ai/copilot/add-cli-command-plugin
2 parents 3804ab7 + 2cb3d63 commit b9ac384

23 files changed

Lines changed: 1253 additions & 21 deletions

content/docs/guides/plugins.mdx

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,243 @@ const slowPlugin: PluginMetadata = {
465465
};
466466
```
467467

468+
### CLI Command Extensions
469+
470+
Plugins can extend the ObjectStack CLI (`os`) with custom commands. This enables third-party packages — such as marketplace tools, deployment utilities, or domain-specific workflows — to register new top-level subcommands.
471+
472+
#### How It Works
473+
474+
```
475+
┌──────────────────────────────────────────────────────────┐
476+
│ os (CLI) │
477+
│ ┌────────────┐ ┌────────────┐ ┌─────────────────────┐ │
478+
│ │ Built-in │ │ Built-in │ │ Plugin Commands │ │
479+
│ │ init, dev │ │ plugin, │ │ marketplace, │ │
480+
│ │ compile .. │ │ generate │ │ deploy, ... │ │
481+
│ └────────────┘ └────────────┘ └─────────────────────┘ │
482+
│ ▲ │
483+
│ │ │
484+
│ loadPluginCommands() │
485+
│ reads objectstack.config.ts │
486+
│ discovers contributes.commands │
487+
│ dynamically imports modules │
488+
└──────────────────────────────────────────────────────────┘
489+
```
490+
491+
1. The plugin declares `contributes.commands` in its manifest.
492+
2. The CLI scans installed plugins at startup via `loadPluginCommands()`.
493+
3. Each plugin module is dynamically imported and its exported Commander.js `Command` instances are registered.
494+
495+
#### Step 1: Declare Commands in the Manifest
496+
497+
Add a `contributes.commands` section to the plugin's manifest:
498+
499+
```typescript
500+
// objectstack.config.ts (plugin package)
501+
import { defineStack } from '@objectstack/spec';
502+
503+
export default defineStack({
504+
manifest: {
505+
id: 'com.acme.marketplace',
506+
namespace: 'marketplace',
507+
version: '1.0.0',
508+
type: 'plugin',
509+
name: 'Marketplace Plugin',
510+
contributes: {
511+
commands: [
512+
{
513+
name: 'marketplace',
514+
description: 'Manage marketplace applications',
515+
module: './dist/cli.js', // optional, defaults to package main
516+
},
517+
],
518+
},
519+
},
520+
});
521+
```
522+
523+
Each command entry has:
524+
525+
| Property | Type | Required | Description |
526+
|:---------|:-----|:---------|:------------|
527+
| `name` | `string` || CLI command name (lowercase, hyphens allowed) |
528+
| `description` | `string` | optional | Help text shown in `os --help` |
529+
| `module` | `string` | optional | Module path exporting Commander.js commands |
530+
531+
#### Step 2: Implement the CLI Module
532+
533+
Create a module that exports Commander.js `Command` instances. The CLI supports three export forms:
534+
535+
```typescript
536+
// src/cli.ts
537+
import { Command } from 'commander';
538+
539+
// ── Subcommands ──────────────────────────────────────────
540+
541+
const publishCommand = new Command('publish')
542+
.description('Publish an app to the marketplace')
543+
.argument('<package>', 'Package to publish')
544+
.option('--public', 'Make publicly visible')
545+
.action(async (pkg: string, options: { public?: boolean }) => {
546+
console.log(`Publishing ${pkg}...`);
547+
// ... publish logic
548+
});
549+
550+
const searchCommand = new Command('search')
551+
.description('Search marketplace apps')
552+
.argument('<query>', 'Search query')
553+
.option('-l, --limit <n>', 'Max results', '10')
554+
.action(async (query: string, options: { limit: string }) => {
555+
console.log(`Searching for "${query}"...`);
556+
// ... search logic
557+
});
558+
559+
const installCommand = new Command('install')
560+
.description('Install an app from the marketplace')
561+
.argument('<app>', 'App identifier')
562+
.action(async (app: string) => {
563+
console.log(`Installing ${app}...`);
564+
// ... install logic
565+
});
566+
567+
// ── Main Command ─────────────────────────────────────────
568+
569+
const marketplaceCommand = new Command('marketplace')
570+
.description('Manage marketplace applications')
571+
.addCommand(publishCommand)
572+
.addCommand(searchCommand)
573+
.addCommand(installCommand);
574+
575+
// Export as named array (recommended)
576+
export const commands = [marketplaceCommand];
577+
578+
// Alternative: export default (single command or array)
579+
// export default marketplaceCommand;
580+
// export default [marketplaceCommand, anotherCommand];
581+
```
582+
583+
#### Step 3: Register the Plugin
584+
585+
Add the plugin to the host project's `objectstack.config.ts`:
586+
587+
```typescript
588+
// Host project objectstack.config.ts
589+
import { defineStack } from '@objectstack/spec';
590+
import marketplacePlugin from '@acme/plugin-marketplace';
591+
592+
export default defineStack({
593+
manifest: {
594+
id: 'com.example.my-app',
595+
version: '1.0.0',
596+
type: 'app',
597+
name: 'My App',
598+
},
599+
plugins: [
600+
marketplacePlugin,
601+
],
602+
});
603+
```
604+
605+
Or use the CLI to add it:
606+
607+
```bash
608+
os plugin add @acme/plugin-marketplace
609+
pnpm add @acme/plugin-marketplace
610+
```
611+
612+
#### Using the Extended CLI
613+
614+
Once installed, the new commands appear in `os --help` and can be invoked directly:
615+
616+
```bash
617+
# List available commands (includes plugin commands)
618+
os --help
619+
620+
# Use marketplace commands
621+
os marketplace search "crm"
622+
os marketplace install com.acme.crm
623+
os marketplace publish ./dist --public
624+
```
625+
626+
#### Complete Example: Deployment Plugin
627+
628+
Here's a full example of a deployment CLI plugin:
629+
630+
```typescript
631+
// @acme/plugin-deploy/src/cli.ts
632+
import { Command } from 'commander';
633+
634+
const deployCommand = new Command('deploy')
635+
.description('Deploy ObjectStack app to cloud')
636+
.addCommand(
637+
new Command('staging')
638+
.description('Deploy to staging environment')
639+
.option('--skip-tests', 'Skip pre-deploy tests')
640+
.action(async (options) => {
641+
console.log('Deploying to staging...');
642+
if (!options.skipTests) {
643+
console.log('Running tests first...');
644+
}
645+
// deploy logic
646+
})
647+
)
648+
.addCommand(
649+
new Command('production')
650+
.description('Deploy to production')
651+
.option('--confirm', 'Skip confirmation prompt')
652+
.action(async (options) => {
653+
if (!options.confirm) {
654+
// prompt for confirmation
655+
}
656+
console.log('Deploying to production...');
657+
})
658+
)
659+
.addCommand(
660+
new Command('status')
661+
.description('Check deployment status')
662+
.action(async () => {
663+
console.log('Checking deployment status...');
664+
})
665+
);
666+
667+
export const commands = [deployCommand];
668+
```
669+
670+
```typescript
671+
// @acme/plugin-deploy/objectstack.config.ts
672+
import { defineStack } from '@objectstack/spec';
673+
674+
export default defineStack({
675+
manifest: {
676+
id: 'com.acme.deploy',
677+
version: '1.0.0',
678+
type: 'plugin',
679+
name: 'Deploy Plugin',
680+
contributes: {
681+
commands: [
682+
{
683+
name: 'deploy',
684+
description: 'Deploy ObjectStack app to cloud',
685+
module: './dist/cli.js',
686+
},
687+
],
688+
},
689+
},
690+
});
691+
```
692+
693+
Usage:
694+
695+
```bash
696+
os deploy staging --skip-tests
697+
os deploy production --confirm
698+
os deploy status
699+
```
700+
701+
<Callout type="info">
702+
**Graceful Degradation**: If a plugin command fails to load (e.g., missing dependency), the CLI logs a warning in `DEBUG` mode and continues with built-in commands. Plugin commands never block the CLI.
703+
</Callout>
704+
468705
---
469706

470707
## Directory Structure Convention
@@ -480,6 +717,7 @@ plugin-my-feature/
480717
├── CHANGELOG.md
481718
└── src/
482719
├── index.ts # Entry point (required)
720+
├── cli.ts # CLI commands (optional, for contributes.commands)
483721
├── my_feature/ # Domain folder (snake_case)
484722
│ ├── feature.object.ts # Business object
485723
│ ├── feature.view.ts # View definition

content/docs/references/api/index.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This section contains all protocol schemas for the api layer of ObjectStack.
2323
<Card href="/docs/references/api/plugin-rest-api" title="Plugin Rest Api" description="Source: packages/spec/src/api/plugin-rest-api.zod.ts" />
2424
<Card href="/docs/references/api/protocol" title="Protocol" description="Source: packages/spec/src/api/protocol.zod.ts" />
2525
<Card href="/docs/references/api/realtime" title="Realtime" description="Source: packages/spec/src/api/realtime.zod.ts" />
26+
<Card href="/docs/references/api/realtime-shared" title="Realtime Shared" description="Source: packages/spec/src/api/realtime-shared.zod.ts" />
2627
<Card href="/docs/references/api/registry" title="Registry" description="Source: packages/spec/src/api/registry.zod.ts" />
2728
<Card href="/docs/references/api/rest-server" title="Rest Server" description="Source: packages/spec/src/api/rest-server.zod.ts" />
2829
<Card href="/docs/references/api/router" title="Router" description="Source: packages/spec/src/api/router.zod.ts" />

content/docs/references/api/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"plugin-rest-api",
2323
"protocol",
2424
"realtime",
25+
"realtime-shared",
2526
"registry",
2627
"rest-server",
2728
"router",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
title: Realtime Shared
3+
description: Realtime Shared protocol schemas
4+
---
5+
6+
Realtime Shared Protocol
7+
8+
Shared schemas and types for real-time communication protocols.
9+
10+
This module consolidates overlapping definitions between the transport-level
11+
12+
realtime protocol (SSE/Polling/WebSocket) and the WebSocket collaboration protocol.
13+
14+
**Architecture:**
15+
16+
- `realtime-shared.zod.ts` — Shared base schemas (Presence, Event types)
17+
18+
- `realtime.zod.ts` — Transport-layer protocol (Channel, Subscription, Transport selection)
19+
20+
- `websocket.zod.ts` — Collaboration protocol (Cursor, OT editing, Advanced presence)
21+
22+
@see realtime.zod.ts for transport-layer configuration
23+
24+
@see websocket.zod.ts for collaborative editing protocol
25+
26+
<Callout type="info">
27+
**Source:** `packages/spec/src/api/realtime-shared.zod.ts`
28+
</Callout>
29+
30+
## TypeScript Usage
31+
32+
```typescript
33+
import { BasePresence, PresenceStatus, RealtimeRecordAction } from '@objectstack/spec/api';
34+
import type { BasePresence, PresenceStatus, RealtimeRecordAction } from '@objectstack/spec/api';
35+
36+
// Validate data
37+
const result = BasePresence.parse(data);
38+
```
39+
40+
---
41+
42+
## BasePresence
43+
44+
### Properties
45+
46+
| Property | Type | Required | Description |
47+
| :--- | :--- | :--- | :--- |
48+
| **userId** | `string` || User identifier |
49+
| **status** | `Enum<'online' \| 'away' \| 'busy' \| 'offline'>` || Current presence status |
50+
| **lastSeen** | `string` || ISO 8601 datetime of last activity |
51+
| **metadata** | `Record<string, any>` | optional | Custom presence data (e.g., current page, custom status) |
52+
53+
54+
---
55+
56+
## PresenceStatus
57+
58+
### Allowed Values
59+
60+
* `online`
61+
* `away`
62+
* `busy`
63+
* `offline`
64+
65+
66+
---
67+
68+
## RealtimeRecordAction
69+
70+
### Allowed Values
71+
72+
* `created`
73+
* `updated`
74+
* `deleted`
75+
76+
77+
---
78+

0 commit comments

Comments
 (0)