@descix/cli 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -303,18 +303,17 @@ User asks question
303
303
  When `--scope project` is used:
304
304
 
305
305
  1. CLI/MCP loads `workspace.json`
306
- 2. Extracts configured communities and apps
307
- 3. Filters `tell_me_how` results to only tools applicable to those apps
306
+ 2. Extracts the app this repo holds (`env.app`)
307
+ 3. Filters `tell_me_how` results to only tools applicable to that app
308
308
  4. Returns focused recommendations
309
309
 
310
310
  ```javascript
311
- // workspace.json (v2.1)
311
+ // workspace.json (v3 single-app)
312
312
  {
313
+ "version": "3.0",
313
314
  "env": {
314
- "apiUrl": "https://dev.descix.net",
315
- "products": [
316
- { "appId": "daita-agent", "communityId": "daita", "localPath": ".", "kbId": "General" }
317
- ]
315
+ "apiUrl": "https://descix.net",
316
+ "app": { "appId": "daita-agent", "communityId": "daita", "kbId": "General" }
318
317
  }
319
318
  }
320
319
 
@@ -338,11 +337,11 @@ descix tell-me-how "How do I sync my knowledge base?"
338
337
 
339
338
  ### 6.2 workspace.json from Hydration
340
339
 
341
- `descix config init --env …` creates `workspace.json`; `descix app init` adds the app's `env.products[]` entry; no verb writes `driveConfig.base_folder_id` — set it in `.descix/workspace.json` when you use `descix drive pull/push`:
340
+ `descix config init --env …` creates `workspace.json`; `descix app init` (from the repo root) writes the app's `env.app`; no verb writes `driveConfig.base_folder_id` — set it in `.descix/workspace.json` when you use `descix drive pull/push`:
342
341
 
343
342
  ```javascript
344
343
  {
345
- "env": { "apiUrl": "https://dev.descix.net", "products": [ /* one entry per app */ ] },
344
+ "env": { "apiUrl": "https://descix.net", "app": { /* THE app this repo holds */ } },
346
345
  "driveConfig": {
347
346
  "base_folder_id": "..." // Only used by `descix drive pull/push` — unrelated to tell_me_how scope
348
347
  }
@@ -1,33 +1,44 @@
1
1
  # Workspace Configuration Guide
2
2
 
3
- `.descix/workspace.json` is the CLI's only local configuration file. It is **version 2.1**, it is
4
- written **only by CLI verbs**, and the loader refuses the shapes that hand edits produce. This page
5
- is the schema and the verb that owns each key.
3
+ `.descix/workspace.json` is the CLI's only local configuration file. It is **version 3.0
4
+ (single-app)**: one repo, one app, and the repo root IS the workspace root. It is written **only by
5
+ CLI verbs**, and the loader refuses the shapes that hand edits produce. This page is the schema and
6
+ the verb that owns each key.
6
7
 
7
8
  ---
8
9
 
9
- ## 1. Format (v2.1 — the only format)
10
+ ## 1. Format (v3 — the only format)
11
+
12
+ An app repo's workspace holds exactly one app, `env.app`:
10
13
 
11
14
  ```json
12
15
  {
13
- "version": "2.1",
16
+ "version": "3.0",
14
17
  "type": "workspace",
15
- "workspaceRoot": "/path/to/workspace",
18
+ "workspaceRoot": "/path/to/my-app-repo",
16
19
  "env": {
17
- "environment": "DEV",
18
- "apiUrl": "https://dev.descix.net",
20
+ "environment": "PROD",
21
+ "apiUrl": "https://descix.net",
19
22
  "gateway": { "port": 5599 },
20
23
  "devCerts": { "dir": "/Users/you/.descix/dev-certs-san" },
21
- "powchUrl": "https://powch.dev.descix.net",
22
- "products": [
23
- { "appId": "egpt-mydocs", "communityId": "egpt", "localPath": ".", "kbId": "General", "site": { "static": "site" } },
24
- { "appId": "egpt-tool", "communityId": "egpt", "localPath": "tool", "kbId": "General", "site": { "port": 5174 }, "microservice": { "port": 4001 } }
25
- ]
24
+ "powchUrl": "https://powch.descix.net",
25
+ "app": {
26
+ "appId": "egpt-mydocs",
27
+ "communityId": "egpt",
28
+ "kbId": "General",
29
+ "site": { "static": "site" },
30
+ "microservice": { "port": 4001 }
31
+ }
26
32
  },
27
33
  "driveConfig": { "base_folder_id": "1ABC..." }
28
34
  }
29
35
  ```
30
36
 
37
+ Run `descix app init` **from the repo root** — it writes `env.app` and makes that directory the
38
+ workspace root. Inside another DeSciX workspace it refuses (`This directory is nested inside another
39
+ DeSciX workspace`) rather than registering into the parent: keep app repos as sibling checkouts. A
40
+ workspace that already holds a different app refuses a second one — that app gets its own repo.
41
+
31
42
  | Key | Meaning | Owned by |
32
43
  |---|---|---|
33
44
  | `env.environment` | `DEV`, `DEMO` or `PROD` | `descix config init --env dev\|demo\|prod` |
@@ -36,29 +47,53 @@ is the schema and the verb that owns each key.
36
47
  | `env.devCerts` | `dir`, or `cert` + `key` — the TLS pair for the gateway and every app behind it; unset means the SDK's shipped pair. Passkey sign-in needs it trusted: `descix dev-certs check`, then the human runs `descix dev-certs trust` (see `reference/local-dev.md` §3.3) | `descix config set-dev-certs --dir\|--cert\|--key\|--clear` |
37
48
  | `env.powchUrl` | Powch's own origin (optional; Powch is cross-origin from the shell by design) | `descix config set-powch-url <url>` |
38
49
  | `env.siteUrl` | the App Shell origin the gateway proxies `/` to (optional; default is the API origin) | `descix config set-site-url <url>` |
39
- | `env.products[]` | one entry per app in this workspace, see below | `descix app init`, `app set-site`, `app set-port`, `app set-localpath`, `app unmap` |
40
- | `env.platform` | the platform shell's own entry — platform contributors only; an app developer has none | platform runbook |
50
+ | `env.app` | THE app this repo holds, see below | `descix app init`, `app set-site`, `app set-port` |
41
51
  | `driveConfig.base_folder_id` | the Drive base folder for `descix drive pull/push` | no verb; set it in `.descix/workspace.json` |
42
52
 
43
- A product entry:
53
+ `env.app`:
44
54
 
45
55
  | Field | Meaning | Written by |
46
56
  |---|---|---|
47
57
  | `appId` | the platform app id (`<community>-<short>`) | `descix app init` |
48
58
  | `communityId` | the app's community; stored so context detection can name it | `descix app init` |
49
- | `localPath` | the app directory, relative to `workspaceRoot` (`.` when the workspace root is the app) | `descix app init -p`, `descix app set-localpath` |
50
59
  | `kbId` | the default knowledge base (default `General`) | `descix app init --kb` |
51
- | `site.static` | a directory under `localPath` served from disk at `/p/<appId>/` | `descix app set-site --static <dir>` |
60
+ | `site.static` | a directory relative to the workspace root (the repo root) served from disk at `/p/<appId>/` — `site`, `site/dist`, `.` (the root itself) or `../scratch`; an absolute path is refused at the write and on read | `descix app set-site --static <dir>` |
52
61
  | `site.port` | a framework dev server the gateway proxies `/p/<appId>` to (no path rewrite) | `descix app set-site --port <n>` |
62
+ | `site.protocol` | `http` or `https` — the dev server's scheme (dev-server mode only; default `https`) | `descix app set-site --port <n> --protocol <scheme>` |
53
63
  | `microservice.port` | the local port the gateway proxies `/s/<appId>` to; `descix microservice init` requires it | `descix app set-port -p <n>` |
54
64
 
55
65
  Sites and services are started by you (or your framework); the CLI does not store or run a dev command.
56
66
 
67
+ ### Gateway workspaces
68
+
69
+ A workspace that serves several local app repos behind one `descix serve` (platform contributors)
70
+ lists them under `env.gateway`, with paths relative to its own root:
71
+
72
+ ```json
73
+ "gateway": { "port": 5599, "shell": "../platform-shell", "appRepos": ["../my-app", "../other-app"] }
74
+ ```
75
+
76
+ | Key | Meaning | Verb |
77
+ |---|---|---|
78
+ | `env.gateway.shell` | the repo whose app the gateway serves at `/` | `descix config gateway set-shell <repo\|none>` |
79
+ | `env.gateway.appRepos[]` | repos whose apps the gateway serves at `/p/<appId>` | `descix config gateway add-repo <repo>`, `descix config gateway remove-repo <repo>` |
80
+
81
+ Run the verbs from the gateway workspace's root; `<repo>` is relative to that root. Each listed repo
82
+ holds its OWN v3 workspace (`descix app init` in that repo), and that file is the sole authority on
83
+ its app's id, site and microservice — the gateway workspace never restates them. The verbs refuse,
84
+ writing nothing: a run in a single-app workspace (one that holds `env.app`), an absolute path, a
85
+ repo with no `.descix/workspace.json` or no `env.app`, a repo already listed (or already the
86
+ shell), one appId listed twice, and `remove-repo` of a repo that is not listed. `descix serve`
87
+ applies the same checks when it reads the file.
88
+
57
89
  ### Refused shapes
58
90
 
59
91
  - A `communities` block with no `env` block — the v1 format — fails on load: `v1 workspace format is
60
- not supported. Migrate to v2.1.` The file is left untouched; re-create it with `descix config init
92
+ not supported. Migrate to v3.` The file is left untouched; re-create it with `descix config init
61
93
  --env …` and `descix app init`.
94
+ - An `env.platform` or `env.products` block — the multi-app format — fails on load: `v2.1 multi-app
95
+ workspace format is not supported. Migrate to v3 (single-app).` The file is left untouched; run
96
+ `descix config init --env …` and `descix app init -a <app_id>` from each app repo's own root.
62
97
  - A top-level `apiUrl` key is refused by name; the origin lives at `env.apiUrl` and nowhere else.
63
98
 
64
99
  ---
@@ -143,14 +178,13 @@ The git path (`descix kb corpus sync`) needs none of this.
143
178
 
144
179
  ## 6. CLI Context Resolution
145
180
 
146
- `WorkspaceConfig.detectContext()` matches your current directory against every product's resolved
147
- `localPath` (longest match wins) and returns `{ communityId, appId, kbId }` from that entry —
148
- `communityId` is `null` for an entry registered by a CLI that did not yet store it.
181
+ `WorkspaceConfig.detectContext()` returns `{ communityId, appId, kbId }` from `env.app` when your
182
+ current directory is the workspace root or anywhere below it.
149
183
  `resolveContextWithOptions()` lets explicit `-c` / `-a` / `-k` flags win over detection.
150
184
 
151
185
  ```bash
152
- # From inside an app directory — detected
153
- cd my-app
186
+ # From inside the app repo — detected
187
+ cd my-app-repo
154
188
  descix kb corpus sync
155
189
 
156
190
  # From anywhere — flags
@@ -164,7 +198,7 @@ descix site status -c egpt -a egpt-mydocs # site status/list need both when
164
198
 
165
199
  | Concept | Owner |
166
200
  |---|---|
167
- | Workspace loading, the v2.1 writer, app registration, context detection | `WorkspaceConfig`, internal to `@descix/cli` |
201
+ | Workspace loading, the v3 writer, app registration, context detection | `WorkspaceConfig`, internal to `@descix/cli` |
168
202
  | Origin resolution precedence | flag → `DESCIX_API_URL` → `env.apiUrl` → default (`descix config` commands) |
169
203
  | User-level settings (`~/.descixrc`) | `GlobalConfig`, internal to `@descix/cli` |
170
204
  | `.descix/wallet.json` | written by `descix login` |
package/bin/descix.js CHANGED
@@ -7,7 +7,8 @@
7
7
  * Combines CLI and MCP functionality with unified authentication.
8
8
  */
9
9
 
10
- import { Command, Option } from 'commander';
10
+ import { Option } from 'commander';
11
+ import { DescixCommand } from '../lib/descix-command.js';
11
12
  import chalk from 'chalk';
12
13
  import { DeSciXApiClient } from '../lib/api-client.js';
13
14
  import { requireAuth } from '../lib/auth-guard.js';
@@ -73,7 +74,7 @@ function fail(error) {
73
74
  process.exit(1);
74
75
  }
75
76
 
76
- const program = new Command();
77
+ const program = new DescixCommand();
77
78
 
78
79
  program
79
80
  .name('descix')
@@ -155,6 +156,7 @@ program.hook('preAction', (thisCommand) => {
155
156
 
156
157
  program
157
158
  .command('login')
159
+ .runbookAnchor('#_3-pin-production-then-sign-in')
158
160
  .description('Authenticate with DeSciX via device login (opens browser)')
159
161
  .option('-u, --url <url>', 'API URL override')
160
162
  .option('--wallet', 'Use direct wallet connection (advanced, not yet implemented)')
@@ -174,6 +176,7 @@ program
174
176
 
175
177
  program
176
178
  .command('logout')
179
+ .runbookAnchor('#_3-pin-production-then-sign-in')
177
180
  .description('Clear saved credentials')
178
181
  .action(async () => {
179
182
  try {
@@ -229,6 +232,7 @@ program
229
232
 
230
233
  program
231
234
  .command('whoami')
235
+ .runbookAnchor('#_3-pin-production-then-sign-in')
232
236
  .description('Show current authentication status')
233
237
  .action(async () => {
234
238
  try {
@@ -265,6 +269,7 @@ program
265
269
 
266
270
  program
267
271
  .command('doctor')
272
+ .runbookAnchor('#_3-pin-production-then-sign-in')
268
273
  .description('Diagnose environment issues (Node, Auth, gcloud, ADC)')
269
274
  .action(async () => {
270
275
  try {
@@ -1041,6 +1046,7 @@ appCommand
1041
1046
 
1042
1047
  appCommand
1043
1048
  .command('init')
1049
+ .runbookAnchor('#_4-connect-the-repository-to-its-app')
1044
1050
  // ONE PATH TO AN APP (CEO-D-2026-08-14): `app init` is THE app entry point — it creates the
1045
1051
  // app when it does not exist yet and then registers/scaffolds it locally. The separate
1046
1052
  // `app create` command is DELETED (no compat fence): two commands for one outcome is how apps
@@ -1151,6 +1157,7 @@ appCommand
1151
1157
 
1152
1158
  appCommand
1153
1159
  .command('set-listed')
1160
+ .runbookAnchor('#_9-list-it-and-discover-the-rest')
1154
1161
  .description('List or unlist your app in the app store (on: shown in the public store; off: not shown to non-admin visitors)')
1155
1162
  .argument('<state>', 'on or off')
1156
1163
  .requiredOption('-c, --community <id>', 'Community ID')
@@ -1482,7 +1489,7 @@ appCommand
1482
1489
  appCommand
1483
1490
  .command('set-port')
1484
1491
  .description('Set the microservice port for this workspace\'s app (writes env.app.microservice.port). Pass "n" to remove.')
1485
- .requiredOption('-a, --app <app_id>', 'App ID (must be mapped in workspace.json)')
1492
+ .requiredOption('-a, --app <app_id>', 'App ID (must be this workspace\'s app, env.app)')
1486
1493
  .requiredOption('-p, --port <port>', 'Microservice port number (1-65535), or "n" to remove')
1487
1494
  .action(async (options) => {
1488
1495
  try {
@@ -1503,7 +1510,7 @@ appCommand
1503
1510
  }
1504
1511
 
1505
1512
  // setMicroservicePort mutates the live env entry and saves — and hard-fails (canonical
1506
- // "not mapped in workspace.json" error) for an unmapped app, surfaced in the catch below.
1513
+ // "is not this workspace's app" error) for any other app id, surfaced in the catch below.
1507
1514
  await workspaceConfig.setMicroservicePort(appId, portNum);
1508
1515
 
1509
1516
  console.log(chalk.green(`\n✓ Microservice port set for ${appId}`));
@@ -1520,9 +1527,9 @@ appCommand
1520
1527
  //
1521
1528
  // Canonical write path for an app's site.{} slot in workspace.json. TWO MODES, one active
1522
1529
  // per write:
1523
- // static mode (--static): site.static — the relative dir (under the app's localPath;
1524
- // "." = the localPath itself) the gateway's staticSitePlugin
1525
- // serves at /p/{appId}/.
1530
+ // static mode (--static): site.static — the dir relative to the workspace root (the
1531
+ // repo root; "." = the root itself) the gateway's
1532
+ // staticSitePlugin serves at /p/{appId}/.
1526
1533
  // dev-server mode (--port): site.port [+ site.protocol] — the local dev server the
1527
1534
  // gateway proxies /p/{appId} to (vite-style hotserving).
1528
1535
  // Setting one mode CLEARS the other's fields; passing BOTH flags in one invocation is the
@@ -1532,9 +1539,10 @@ appCommand
1532
1539
  // Backed by WorkspaceConfig.setStaticSite. It writes ONLY the local workspace.json site.{} slot.
1533
1540
  appCommand
1534
1541
  .command('set-site')
1542
+ .runbookAnchor('#_5-run-it-locally-descix-serve')
1535
1543
  .description("Set this workspace app's site config (env.app.site). One active mode per write: --static <dir> (built site served at /p/<app>/) or --port <n> [--protocol http|https] (dev server proxied at /p/<app>); setting one clears the other. Pass both flags together to keep both — the gateway then serves the dev server (site.port wins). --unset clears site.{} entirely.")
1536
- .requiredOption('-a, --app <app_id>', 'App ID (must be mapped in workspace.json)')
1537
- .option('--static <path>', 'Static mode: relative dir under the app localPath (e.g. "site"; "." = the localPath itself). Alone, it clears site.port and site.protocol (one active mode per write).')
1544
+ .requiredOption('-a, --app <app_id>', 'App ID (must be this workspace\'s app, env.app)')
1545
+ .option('--static <path>', 'Static mode: a dir relative to the workspace root (the repo root), e.g. "site" or "../scratch"; "." = the root itself. An absolute path is refused. Alone, it clears site.port and site.protocol (one active mode per write).')
1538
1546
  .option('--port <port>', 'Dev-server mode: proxy /p/<app> to your local dev server on this port (1-65535). Alone, it clears site.static. With --static in the same call, both are kept and the gateway serves the dev server.')
1539
1547
  .option('--protocol <scheme>', 'Dev-server upstream scheme: "http" or "https" (default https when unset). Dev-server mode only — requires site.port. A wrong scheme fails loud at the gateway (502); there is no auto-detection.')
1540
1548
  .option('--unset', 'Remove site.static, site.port and site.protocol for the app, clearing the site.{} slot')
@@ -1558,13 +1566,9 @@ appCommand
1558
1566
 
1559
1567
  const fields = {};
1560
1568
 
1561
- if (options.static !== undefined) {
1562
- if (typeof options.static !== 'string' || options.static.trim() === '') {
1563
- console.error(chalk.red('\n❌ --static requires a non-empty relative path (e.g. "site" or ".").\n'));
1564
- process.exit(1);
1565
- }
1566
- fields.static = options.static;
1567
- }
1569
+ // The --static path rule has ONE owner (assertSiteStatic, applied in setStaticSite);
1570
+ // a refusal surfaces through the catch below.
1571
+ if (options.static !== undefined) fields.static = options.static;
1568
1572
 
1569
1573
  if (options.port !== undefined) {
1570
1574
  const portNum = parseInt(options.port, 10);
@@ -1584,7 +1588,7 @@ appCommand
1584
1588
  }
1585
1589
 
1586
1590
  // setStaticSite applies ONE-ACTIVE-MODE-PER-WRITE and saves — it hard-fails (canonical
1587
- // "not mapped in workspace.json" error, or protocol-without-port) in the catch below.
1591
+ // "is not this workspace's app" error, or protocol-without-port) in the catch below.
1588
1592
  await workspaceConfig.setStaticSite(appId, fields);
1589
1593
 
1590
1594
  console.log(chalk.green(`\n✓ site config set for ${appId}`));
@@ -1622,7 +1626,7 @@ appCommand
1622
1626
  appCommand
1623
1627
  .command('open')
1624
1628
  .description('Resolve (and optionally open) an app\'s local gateway dev URL. Hard-fails if the app has no site config.')
1625
- .requiredOption('-a, --app <app_id>', 'App ID (must be mapped in workspace.json with a site)')
1629
+ .requiredOption('-a, --app <app_id>', 'App ID (an app this workspace serves, with a site)')
1626
1630
  .option('--open', 'Open the resolved URL in the default browser')
1627
1631
  .action(async (options) => {
1628
1632
  try {
@@ -1700,6 +1704,7 @@ const kbCommand = program
1700
1704
 
1701
1705
  kbCommand
1702
1706
  .command('list')
1707
+ .runbookAnchor('#_6-knowledge-bases')
1703
1708
  .description('List knowledge bases for an app')
1704
1709
  .requiredOption('-a, --app <app_id>', 'App ID')
1705
1710
  .option('--cached', 'Show the fast cached vector counter instead of the TRUE live Pinecone count. The cached counter is written ONLY by the CLI/PWA sync path, so it reports 0 for KBs ingested by any other route even when their vectors retrieve. Default is the live count.')
@@ -1996,6 +2001,7 @@ const corpusCommand = kbCommand
1996
2001
 
1997
2002
  corpusCommand
1998
2003
  .command('sync')
2004
+ .runbookAnchor('#_6-knowledge-bases')
1999
2005
  .description('Sync the files a corpus manifest names into the app\'s knowledge base')
2000
2006
  .requiredOption('-a, --app <id>', 'App ID')
2001
2007
  .option('-c, --community <community_id>', 'Community ID (optional; resolved from Products when omitted)')
@@ -2025,6 +2031,7 @@ corpusCommand
2025
2031
 
2026
2032
  corpusCommand
2027
2033
  .command('status')
2034
+ .runbookAnchor('#_6-knowledge-bases')
2028
2035
  .description('Show corpus sync state (files, chunks, last sync, resolved ref)')
2029
2036
  .requiredOption('-a, --app <id>', 'App ID')
2030
2037
  .option('-k, --kb <name>', 'KB name (default: all)')
@@ -2187,6 +2194,7 @@ siteCommand
2187
2194
  // site upload - Deploy to GCS (context-aware, manifest-aware)
2188
2195
  siteCommand
2189
2196
  .command('upload')
2197
+ .runbookAnchor('#_8-deploy-the-site')
2190
2198
  .description('Upload site to GCS and update app metadata (uses .descix/manifests/site.json when present)')
2191
2199
  .option('-c, --community <id>', 'Community ID (auto-detects from context)')
2192
2200
  .option('-a, --app <id>', 'App ID (auto-detects from context)')
@@ -2590,6 +2598,7 @@ siteCommand
2590
2598
  // site status - Get deployment status (context-aware)
2591
2599
  siteCommand
2592
2600
  .command('status')
2601
+ .runbookAnchor('#_8-deploy-the-site')
2593
2602
  .description('Get deployment status and manifest')
2594
2603
  .option('-c, --community <id>', 'Community ID (auto-detects from context)')
2595
2604
  .option('-a, --app <id>', 'App ID (auto-detects from context)')
@@ -3353,6 +3362,7 @@ program
3353
3362
 
3354
3363
  program
3355
3364
  .command('chat [question...]')
3365
+ .runbookAnchor('#_6-knowledge-bases')
3356
3366
  .description('Chat with an app agent. Usage: descix chat "Your question" or descix chat -q "Your question"')
3357
3367
  .option('-c, --community <id>', 'Community ID (optional; server resolves it from Products)')
3358
3368
  .option('-a, --app <id>', 'App ID (required unless run from inside a mapped app directory)')
@@ -3669,6 +3679,7 @@ const configCommand = program
3669
3679
 
3670
3680
  configCommand
3671
3681
  .command('show')
3682
+ .runbookAnchor('#_3-pin-production-then-sign-in')
3672
3683
  .description('Show current configuration')
3673
3684
  .action(async () => {
3674
3685
  try {
@@ -3680,6 +3691,7 @@ configCommand
3680
3691
 
3681
3692
  configCommand
3682
3693
  .command('init')
3694
+ .runbookAnchor('#_3-pin-production-then-sign-in')
3683
3695
  .description('Pin this workspace to an environment (requires the global --env dev|demo|prod)')
3684
3696
  // `--dev` DELETED, not deprecated: its absence meant PRODUCTION, so the most common
3685
3697
  // invocation (`descix config init`) wrote a prod origin nobody named. There is no default.
@@ -3728,6 +3740,51 @@ configCommand
3728
3740
  }
3729
3741
  });
3730
3742
 
3743
+ // The gateway workspace's repo list (env.gateway.shell, env.gateway.appRepos[]). One writer:
3744
+ // WorkspaceConfig.setGatewayRepo(), which validates with the gateway's own reader.
3745
+ const configGatewayCommand = configCommand
3746
+ .command('gateway')
3747
+ .description('Write a gateway workspace\'s repo list (env.gateway.shell, env.gateway.appRepos[]) — never hand-edit it')
3748
+ .addHelpText('after', '\nRepo paths are relative to the gateway workspace root (sibling checkouts: ../<repo>).\n' +
3749
+ 'Each listed repo must carry its own v3 .descix/workspace.json with env.app (descix app init there).\n' +
3750
+ 'Refused in a single-app workspace (one that holds env.app).\n');
3751
+
3752
+ configGatewayCommand
3753
+ .command('set-shell')
3754
+ .description('Set the repo whose app the gateway serves at / (env.gateway.shell)')
3755
+ .argument('<repo>', 'Repo root relative to the workspace root, or "none" to remove')
3756
+ .action(async (repo) => {
3757
+ try {
3758
+ await configCommands.setGatewayRepo('set-shell', repo === 'none' ? null : repo);
3759
+ } catch (error) {
3760
+ fail(error);
3761
+ }
3762
+ });
3763
+
3764
+ configGatewayCommand
3765
+ .command('add-repo')
3766
+ .description('List a repo whose app the gateway serves at /p/<appId> (env.gateway.appRepos[])')
3767
+ .argument('<repo>', 'Repo root relative to the workspace root')
3768
+ .action(async (repo) => {
3769
+ try {
3770
+ await configCommands.setGatewayRepo('add-repo', repo);
3771
+ } catch (error) {
3772
+ fail(error);
3773
+ }
3774
+ });
3775
+
3776
+ configGatewayCommand
3777
+ .command('remove-repo')
3778
+ .description('Remove a listed repo from env.gateway.appRepos[]')
3779
+ .argument('<repo>', 'Repo root relative to the workspace root, as listed')
3780
+ .action(async (repo) => {
3781
+ try {
3782
+ await configCommands.setGatewayRepo('remove-repo', repo);
3783
+ } catch (error) {
3784
+ fail(error);
3785
+ }
3786
+ });
3787
+
3731
3788
  configCommand
3732
3789
  .command('set-dev-certs')
3733
3790
  .description('Point local dev servers at your own TLS cert pair (env.devCerts) — the gateway AND every app behind it')
@@ -4055,6 +4112,7 @@ mcpCommand
4055
4112
 
4056
4113
  program
4057
4114
  .command('tell-me-how')
4115
+ .runbookAnchor('#_9-list-it-and-discover-the-rest')
4058
4116
  .description('Discover platform tools using natural language (Intelligent MCP Mesh)')
4059
4117
  .argument('<question>', 'What do you want to accomplish? Use natural language.')
4060
4118
  .option('-s, --scope <scope>', 'Search scope (validated against the published tell_me_how enum): bootstrap, artifact, project, entitlements (default), discovery', 'entitlements')
@@ -4195,6 +4253,7 @@ program
4195
4253
 
4196
4254
  program
4197
4255
  .command('serve')
4256
+ .runbookAnchor('#_5-run-it-locally-descix-serve')
4198
4257
  .description('Start the unified local gateway (one HTTPS origin: App Shell at /, your app at /p/<app>, /apifront to the API)')
4199
4258
  .option('-p, --port <port>', 'Gateway port for this run (default: env.gateway.port, else 5173)')
4200
4259
  .option('-w, --workspace <path>', 'Workspace root override')
@@ -4226,6 +4285,7 @@ const devCertsCommand = program
4226
4285
 
4227
4286
  devCertsCommand
4228
4287
  .command('check')
4288
+ .runbookAnchor('#_5-run-it-locally-descix-serve')
4229
4289
  .description('Check whether the dev-server cert is trusted for https://localhost (exit 0 only when trusted)')
4230
4290
  .option('--json', 'Output raw JSON')
4231
4291
  .action(async (options) => {
@@ -4239,6 +4299,7 @@ devCertsCommand
4239
4299
 
4240
4300
  devCertsCommand
4241
4301
  .command('trust')
4302
+ .runbookAnchor('#_5-run-it-locally-descix-serve')
4242
4303
  .description('Trust the dev-server cert in the macOS keychain (prompts for your password)')
4243
4304
  .action(async () => {
4244
4305
  try {
@@ -308,3 +308,21 @@ export async function setSiteUrl(url) {
308
308
  'App Shell URL set — `descix serve` will proxy / here.',
309
309
  );
310
310
  }
311
+
312
+ /**
313
+ * `descix config gateway set-shell | add-repo | remove-repo` — the CLI face of the one writer,
314
+ * WorkspaceConfig.setGatewayRepo(). A refusal propagates to the bin's fail(), which prints it once.
315
+ *
316
+ * @param {'set-shell'|'add-repo'|'remove-repo'} op
317
+ * @param {string|null} repo
318
+ */
319
+ export async function setGatewayRepo(op, repo) {
320
+ const workspaceConfig = await WorkspaceConfig.load();
321
+ const configPath = await workspaceConfig.setGatewayRepo(op, repo);
322
+ const gateway = workspaceConfig.env?.gateway || {};
323
+ console.log(chalk.green(`\n✅ Gateway ${op}: ${repo === null ? '(shell removed)' : repo}\n`));
324
+ console.log(chalk.white(` env.gateway.shell: ${gateway.shell ?? '(none)'}`));
325
+ console.log(chalk.white(` env.gateway.appRepos: ${gateway.appRepos?.length ? gateway.appRepos.join(', ') : '(none)'}`));
326
+ console.log(chalk.gray(` Saved to: ${configPath}\n`));
327
+ return configPath;
328
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * DescixCommand — the CLI's commander Command, carrying ONE extra fact per verb: the
3
+ * app-developer runbook section that documents it.
4
+ *
5
+ * The fact is declared BESIDE the verb's own definition in bin/descix.js
6
+ * (`.command('serve').runbookAnchor('#_5-run-it-locally-descix-serve')`), so the verb tree and
7
+ * its runbook pointers can never be two lists. `createCommand` makes every subcommand a
8
+ * DescixCommand (commander's documented subclassing hook), so `.runbookAnchor()` chains on any
9
+ * `.command(...)` in the tree.
10
+ *
11
+ * The value is the anchor VitePress renders for the runbook heading (leading '#'). Whether it
12
+ * names a REAL heading is gated by tests/cli-verbs-generated.test.js against the runbook
13
+ * source (scripts/lib/runbook-anchors.mjs owns the slug rule). A verb with no runbook section
14
+ * declares nothing — there is no placeholder anchor. Read only by
15
+ * scripts/lib/cli-verb-tree-hook.mjs, which publishes the tree into
16
+ * @descix/platform-api/mcp-tools (cli-verbs.generated.json).
17
+ */
18
+ import { Command } from 'commander';
19
+
20
+ const ANCHOR_SHAPE = /^#[^\sA-Z#]+$/;
21
+
22
+ export class DescixCommand extends Command {
23
+ createCommand(name) {
24
+ return new DescixCommand(name);
25
+ }
26
+
27
+ /** @param {string} anchor '#<slug>' as rendered by the docs site @returns {this} */
28
+ runbookAnchor(anchor) {
29
+ if (this._descixRunbookAnchor !== undefined) {
30
+ throw new Error(`descix ${this.name()}: runbookAnchor declared twice (${this._descixRunbookAnchor}, ${anchor}) — one verb, one section`);
31
+ }
32
+ if (typeof anchor !== 'string' || !ANCHOR_SHAPE.test(anchor)) {
33
+ throw new Error(`descix ${this.name()}: runbookAnchor must be a rendered anchor like "#_5-run-it-locally-descix-serve"; got ${JSON.stringify(anchor)}`);
34
+ }
35
+ this._descixRunbookAnchor = anchor;
36
+ return this;
37
+ }
38
+
39
+ /** @returns {string|null} the declared anchor, or null when the verb has no runbook section */
40
+ declaredRunbookAnchor() {
41
+ return this._descixRunbookAnchor ?? null;
42
+ }
43
+ }
@@ -93,6 +93,9 @@
93
93
  "community rename": [
94
94
  "rename_community"
95
95
  ],
96
+ "config gateway add-repo": [],
97
+ "config gateway remove-repo": [],
98
+ "config gateway set-shell": [],
96
99
  "config init": [],
97
100
  "config set-dev-certs": [],
98
101
  "config set-env": [],