@descix/cli 1.0.1 → 1.0.4

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.
Files changed (100) hide show
  1. package/LICENSE.md +671 -0
  2. package/README.md +43 -4
  3. package/agent-assets/guides/README.md +2 -2
  4. package/agent-assets/guides/dynamic-site-pattern.md +29 -14
  5. package/agent-assets/guides/knowledge-only-pattern.md +5 -5
  6. package/agent-assets/guides/microservice-pattern.md +48 -4
  7. package/agent-assets/guides/static-site-pattern.md +8 -0
  8. package/agent-assets/instructions/app-asset-validation.md +5 -5
  9. package/agent-assets/instructions/onboarding-flow.md +4 -4
  10. package/agent-assets/instructions/standard-folder-structure.md +20 -11
  11. package/agent-assets/reference/app-creation.md +42 -20
  12. package/agent-assets/reference/architecture.md +24 -22
  13. package/agent-assets/reference/local-dev.md +230 -67
  14. package/agent-assets/reference/mcp-integration.md +21 -15
  15. package/agent-assets/reference/platform-api-consumer.md +244 -0
  16. package/agent-assets/reference/workspace-config.md +7 -7
  17. package/agent-assets/rules/descix_mcp.template.mdc +8 -8
  18. package/bin/descix.js +2559 -1435
  19. package/bin/kb-deny-lint.mjs +91 -0
  20. package/bin/mcp-server.js +175 -91
  21. package/lib/agent-files.js +37 -25
  22. package/lib/api-client.js +223 -56
  23. package/lib/auth-guard.js +13 -3
  24. package/lib/chat-session.js +187 -0
  25. package/lib/cli-version.js +59 -0
  26. package/lib/commands/airdrop.js +251 -0
  27. package/lib/commands/auth.js +254 -72
  28. package/lib/commands/briefer/errors.js +88 -0
  29. package/lib/commands/briefer/index.js +319 -0
  30. package/lib/commands/briefer/sources/canonical-sources.js +129 -0
  31. package/lib/commands/briefer/sources/entitlements.js +220 -0
  32. package/lib/commands/briefer/sources/environments.js +205 -0
  33. package/lib/commands/briefer/sources/identifiers.js +214 -0
  34. package/lib/commands/briefer/sources/microservice-deploy.js +267 -0
  35. package/lib/commands/briefer/sources/routing.js +275 -0
  36. package/lib/commands/briefer/sources/what-is-not.js +223 -0
  37. package/lib/commands/briefer/stitcher.js +134 -0
  38. package/lib/commands/briefer/util/source-reader.js +502 -0
  39. package/lib/commands/config.js +210 -90
  40. package/lib/commands/corpus.js +978 -0
  41. package/lib/commands/credits.js +122 -0
  42. package/lib/commands/health.js +627 -0
  43. package/lib/commands/kb-list-render.js +32 -0
  44. package/lib/commands/kb.js +190 -413
  45. package/lib/commands/mcp.js +5 -5
  46. package/lib/commands/microservice-restart.js +359 -0
  47. package/lib/commands/model-config.js +313 -0
  48. package/lib/commands/retired-kb-sync.js +134 -0
  49. package/lib/commands/serve.js +26 -1
  50. package/lib/commands/status.js +23 -51
  51. package/lib/commands/update.js +36 -107
  52. package/lib/core/Chunker.js +491 -17
  53. package/lib/core/CorpusDenyLint.js +326 -0
  54. package/lib/core/CorpusWalker.js +357 -0
  55. package/lib/core/Hydrator.js +75 -29
  56. package/lib/core/ManifestLoader.js +533 -0
  57. package/lib/core/SiteWalker.js +218 -0
  58. package/lib/core/Syncer.js +163 -11
  59. package/lib/diagnostic-log.js +41 -0
  60. package/lib/environment-report.js +100 -0
  61. package/lib/global-config.js +4 -1
  62. package/lib/oauth-client.js +164 -0
  63. package/lib/origin.js +143 -0
  64. package/lib/service-api-client.js +95 -0
  65. package/lib/wallet-environment.js +49 -0
  66. package/lib/wallet-file.js +16 -2
  67. package/lib/wizard/setup.js +10 -9
  68. package/lib/workspace-config.js +380 -235
  69. package/lib/workspace-identity.js +92 -0
  70. package/lib/workspace-utils.js +41 -61
  71. package/package.json +19 -6
  72. package/templates/agent-claude.md +157 -9
  73. package/templates/agent-cline.md +68 -8
  74. package/templates/agent-copilot.md +68 -8
  75. package/templates/agent-cursor.md +68 -8
  76. package/templates/alignment-schema.json +20 -0
  77. package/templates/cloud-brain-hook.sh +20 -0
  78. package/templates/drive/README.md +1 -1
  79. package/templates/drive_templates/README.md +2 -2
  80. package/templates/scaffolds/README.md +6 -10
  81. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +104 -149
  82. package/templates/scaffolds/microservice/app.js +51 -10
  83. package/templates/scaffolds/microservice/app.yaml +2 -2
  84. package/templates/scaffolds/microservice/defaults-config-dev.json +4 -0
  85. package/templates/scaffolds/microservice/dev-overrides.example.json +5 -0
  86. package/templates/scaffolds/microservice/manifest.json +0 -1
  87. package/templates/scaffolds/microservice/package.json +3 -3
  88. package/templates/scaffolds/microservice/scripts/register.js +56 -41
  89. package/templates/scaffolds/microservice/services/mcpClient.js +55 -65
  90. package/templates/scaffolds/microservice/services/utils.js +20 -238
  91. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +24 -0
  92. package/lib/commands/app-wizard.js +0 -627
  93. package/lib/commands/folder.js +0 -291
  94. package/lib/core/PathContext.js +0 -335
  95. package/templates/scaffolds/microservice/dev-overrides.json.example +0 -5
  96. package/templates/scaffolds/site/DeSciXAppSDK.js +0 -62
  97. package/templates/scaffolds/site/README.md +0 -51
  98. package/templates/scaffolds/site/app.js +0 -22
  99. package/templates/scaffolds/site/index.html +0 -42
  100. package/templates/scaffolds/site/styles.css +0 -123
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Model Config Commands — WS-CONFIG-BOOTSTRAP-FIX item #10
3
+ *
4
+ * CLI surface for the four resolver-chain knobs:
5
+ * - descix app set-default-model -a <app_id> -m <model_name> [--env=dev|demo|prod]
6
+ * - descix app set-default-model -a <app_id> --clear [--env=dev|demo|prod]
7
+ * - descix kb set-override-model -a <app_id> -k <kb_name> -m <model_name> [--env=dev|demo|prod]
8
+ * - descix kb clear-override-model -a <app_id> -k <kb_name> [--env=dev|demo|prod]
9
+ *
10
+ * Per CEO 2026-05-26 inheritance chain:
11
+ * options.model || kb.kb_model_override || app.default_app_model || levelConfig.model || utils.DEFAULT_AI_MODEL
12
+ *
13
+ * --clear flows use FieldValue.delete() server-side (NOT null) per tripwire #2.
14
+ *
15
+ * Every action appends a JSONL audit-log line to {workspace-root}/docs/audit/model-config-changes.jsonl.
16
+ *
17
+ * This module performs NO Firestore I/O directly — it is HTTP-only, routing to backend handlers
18
+ * via DeSciXApiClient.invoke (set_app_default_model, clear_app_default_model, set_kb_model_override,
19
+ * clear_kb_model_override). This preserves the CLI invariant: zero direct service imports.
20
+ */
21
+
22
+ import chalk from 'chalk';
23
+ import fs from 'fs/promises';
24
+ import path from 'path';
25
+ import { DeSciXApiClient } from '../api-client.js';
26
+ import { requireAuth } from '../auth-guard.js';
27
+ import { WorkspaceConfig } from '../workspace-config.js';
28
+
29
+ const AUDIT_LOG_RELATIVE = 'docs/audit/model-config-changes.jsonl';
30
+
31
+ /**
32
+ * Resolve the audit-log absolute path. Anchored at workspace root so the JSONL lives in
33
+ * the worktree alongside related design docs. Creates the parent dir if missing.
34
+ */
35
+ async function resolveAuditLogPath() {
36
+ const ws = await WorkspaceConfig.tryLoad();
37
+ const root = (ws && ws.getWorkspaceRoot && ws.getWorkspaceRoot()) || process.cwd();
38
+ const abs = path.resolve(root, AUDIT_LOG_RELATIVE);
39
+ await fs.mkdir(path.dirname(abs), { recursive: true });
40
+ return abs;
41
+ }
42
+
43
+ /**
44
+ * Append a single JSONL line to the audit log.
45
+ *
46
+ * Schema (per dispatch §2.6):
47
+ * { date, action, env, app, kb?, before, after, performed_by }
48
+ *
49
+ * `performed_by` is sourced from the wallet credential email if available; if not,
50
+ * we record "unknown" rather than guessing.
51
+ */
52
+ async function appendAudit({ action, env, app_id, kb_name, before, after, performed_by }) {
53
+ const line = {
54
+ date: new Date().toISOString(),
55
+ action,
56
+ env: env || null,
57
+ app: app_id,
58
+ ...(kb_name ? { kb: kb_name } : {}),
59
+ before,
60
+ after,
61
+ performed_by: performed_by || 'unknown'
62
+ };
63
+ const auditPath = await resolveAuditLogPath();
64
+ await fs.appendFile(auditPath, JSON.stringify(line) + '\n');
65
+ return { path: auditPath, line };
66
+ }
67
+
68
+ /**
69
+ * Best-effort identity from the API client wallet credentials.
70
+ *
71
+ * Resolution order (mirrors backend's `params.user?.email || user_id || wallet`
72
+ * pattern so audit lines remain comparable whether identity is captured at the
73
+ * CLI or downstream):
74
+ * 1. c.email — primary, human-readable
75
+ * 2. c.user_email — alternate spelling some legacy wallet files used
76
+ * 3. c.userId — Powch's user_id (often the email, but kept distinct
77
+ * in the wallet schema; was missing from this chain)
78
+ * 4. c.user_id — snake_case variant for cross-service consistency
79
+ * 5. c.walletAddress — 0x-address; durable identity even when email absent
80
+ * 6. c.wallet_address — snake_case variant
81
+ * 7. c.address — generic last fallback
82
+ * Returns null on any failure; `appendAudit` then writes the literal
83
+ * "unknown" to make the audit line explicitly mark "identity could not be
84
+ * captured" rather than silently dropping the field.
85
+ */
86
+ function identityFromApiClient(apiClient) {
87
+ try {
88
+ const c = apiClient.credentials || {};
89
+ return c.email
90
+ || c.user_email
91
+ || c.userId
92
+ || c.user_id
93
+ || c.walletAddress
94
+ || c.wallet_address
95
+ || c.address
96
+ || null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Pretty-print the resolution chain that will apply going forward.
104
+ * The print is deterministic and includes:
105
+ * - the chain template
106
+ * - the current value of App.default_app_model
107
+ * - the per-level platform defaults (L1..L5)
108
+ * - per-KB overrides (with explicit "cleared" markers when null/absent)
109
+ */
110
+ function printResolution({ app_id, resolution }) {
111
+ if (!resolution) return;
112
+ const dam = resolution.default_app_model === null ? 'null' : `"${resolution.default_app_model}"`;
113
+ const dai = resolution.default_ai_model ? `"${resolution.default_ai_model}"` : 'null';
114
+
115
+ console.log(chalk.cyan('\nResolution chain that will apply going forward:'));
116
+ console.log(chalk.gray(` ${resolution.chain}`));
117
+ console.log(chalk.gray(` app.default_app_model = ${dam}`));
118
+ console.log(chalk.gray(` utils.DEFAULT_AI_MODEL = ${dai}`));
119
+
120
+ const levels = resolution.intelligence_levels || {};
121
+ const levelKeys = Object.keys(levels).sort();
122
+ if (levelKeys.length > 0) {
123
+ const parts = levelKeys.map(k => `L${k}: ${(levels[k] && levels[k].model) || '?'}`);
124
+ console.log(chalk.gray(` Per-level: ${parts.join(' | ')}`));
125
+ }
126
+
127
+ const overrides = resolution.kb_overrides || {};
128
+ const keys = Object.keys(overrides).sort();
129
+ if (keys.length > 0) {
130
+ const parts = keys.map(k => {
131
+ const v = overrides[k];
132
+ return v === null || v === undefined ? `${k}=cleared` : `${k}="${v}"`;
133
+ });
134
+ console.log(chalk.gray(` KB overrides: ${parts.join(', ')}`));
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Pretty-print the before/after pair for a field.
140
+ */
141
+ function printBeforeAfter({ field, before, after }) {
142
+ const fmt = (v) => (v === null || v === undefined) ? chalk.gray('<absent>') : chalk.white(`"${v}"`);
143
+ console.log(chalk.cyan(`\n${field}:`));
144
+ console.log(` before: ${fmt(before)}`);
145
+ console.log(` after: ${fmt(after)}`);
146
+ }
147
+
148
+ // ============ App: set / clear default_app_model ============
149
+
150
+ /**
151
+ * descix app set-default-model -a <app_id> -m <model_name> [--clear]
152
+ *
153
+ * HARD-FAIL on missing -a; HARD-FAIL on (no -m AND no --clear); HARD-FAIL on (-m AND --clear).
154
+ * Backend HARD-FAILs on unknown app.
155
+ */
156
+ export async function runAppSetDefaultModel(options) {
157
+ const { app: app_id, model: model_name, clear, env } = options || {};
158
+
159
+ if (!app_id) throw new Error('Missing required option: -a, --app <app_id>');
160
+ if (!clear && !model_name) {
161
+ throw new Error('Either -m, --model <model_name> or --clear is required.');
162
+ }
163
+ if (clear && model_name) {
164
+ throw new Error('-m and --clear are mutually exclusive.');
165
+ }
166
+
167
+ const apiClient = new DeSciXApiClient();
168
+ await requireAuth(apiClient);
169
+
170
+ if (clear) {
171
+ const response = await apiClient.invoke('clear_app_default_model', { app_id });
172
+ const result = response.message || response;
173
+
174
+ console.log(chalk.green(`\n✓ Cleared App.default_app_model for ${app_id}`));
175
+ printBeforeAfter({
176
+ field: 'App.default_app_model',
177
+ before: result.before?.default_app_model,
178
+ after: result.after?.default_app_model
179
+ });
180
+ printResolution({ app_id, resolution: result.resolution });
181
+
182
+ const audit = await appendAudit({
183
+ action: 'clear_app_default_model',
184
+ env,
185
+ app_id,
186
+ before: result.before,
187
+ after: result.after,
188
+ performed_by: identityFromApiClient(apiClient)
189
+ });
190
+ console.log(chalk.gray(`\nAudit log: ${audit.path}\n`));
191
+ return result;
192
+ }
193
+
194
+ const response = await apiClient.invoke('set_app_default_model', { app_id, model_name });
195
+ const result = response.message || response;
196
+
197
+ console.log(chalk.green(`\n✓ Set App.default_app_model = "${model_name}" for ${app_id}`));
198
+ printBeforeAfter({
199
+ field: 'App.default_app_model',
200
+ before: result.before?.default_app_model,
201
+ after: result.after?.default_app_model
202
+ });
203
+ printResolution({ app_id, resolution: result.resolution });
204
+
205
+ const audit = await appendAudit({
206
+ action: 'set_app_default_model',
207
+ env,
208
+ app_id,
209
+ before: result.before,
210
+ after: result.after,
211
+ performed_by: identityFromApiClient(apiClient)
212
+ });
213
+ console.log(chalk.gray(`\nAudit log: ${audit.path}\n`));
214
+ return result;
215
+ }
216
+
217
+ // ============ KB: set kb_model_override ============
218
+
219
+ /**
220
+ * descix kb set-override-model -a <app_id> -k <kb_name> -m <model_name>
221
+ *
222
+ * HARD-FAIL on missing -a/-k/-m. Backend HARD-FAILs on unknown app/KB.
223
+ */
224
+ export async function runKbSetOverrideModel(options) {
225
+ const { app: app_id, kb: kb_name, model: model_name, env } = options || {};
226
+
227
+ if (!app_id) throw new Error('Missing required option: -a, --app <app_id>');
228
+ if (!kb_name) throw new Error('Missing required option: -k, --kb <kb_name>');
229
+ if (!model_name) throw new Error('Missing required option: -m, --model <model_name>');
230
+
231
+ const apiClient = new DeSciXApiClient();
232
+ await requireAuth(apiClient);
233
+
234
+ const response = await apiClient.invoke('set_kb_model_override', { app_id, kb_name, model_name });
235
+ const result = response.message || response;
236
+
237
+ console.log(chalk.green(`\n✓ Set KB.kb_model_override = "${model_name}" for ${app_id}/${kb_name}`));
238
+ printBeforeAfter({
239
+ field: `KB.kb_model_override (${kb_name})`,
240
+ before: result.before?.kb_model_override,
241
+ after: result.after?.kb_model_override
242
+ });
243
+ printResolution({ app_id, resolution: result.resolution });
244
+
245
+ const audit = await appendAudit({
246
+ action: 'set_kb_model_override',
247
+ env,
248
+ app_id,
249
+ kb_name,
250
+ before: result.before,
251
+ after: result.after,
252
+ performed_by: identityFromApiClient(apiClient)
253
+ });
254
+ console.log(chalk.gray(`\nAudit log: ${audit.path}\n`));
255
+ return result;
256
+ }
257
+
258
+ // ============ KB: clear kb_model_override ============
259
+
260
+ /**
261
+ * descix kb clear-override-model -a <app_id> -k <kb_name>
262
+ *
263
+ * HARD-FAIL on missing -a/-k. Backend HARD-FAILs on unknown app/KB.
264
+ * Backend uses FieldValue.delete() — field GONE after this, not null.
265
+ */
266
+ export async function runKbClearOverrideModel(options) {
267
+ const { app: app_id, kb: kb_name, env } = options || {};
268
+
269
+ if (!app_id) throw new Error('Missing required option: -a, --app <app_id>');
270
+ if (!kb_name) throw new Error('Missing required option: -k, --kb <kb_name>');
271
+
272
+ const apiClient = new DeSciXApiClient();
273
+ await requireAuth(apiClient);
274
+
275
+ const response = await apiClient.invoke('clear_kb_model_override', { app_id, kb_name });
276
+ const result = response.message || response;
277
+
278
+ console.log(chalk.green(`\n✓ Cleared KB.kb_model_override for ${app_id}/${kb_name}`));
279
+ printBeforeAfter({
280
+ field: `KB.kb_model_override (${kb_name})`,
281
+ before: result.before?.kb_model_override,
282
+ after: result.after?.kb_model_override
283
+ });
284
+ printResolution({ app_id, resolution: result.resolution });
285
+
286
+ const audit = await appendAudit({
287
+ action: 'clear_kb_model_override',
288
+ env,
289
+ app_id,
290
+ kb_name,
291
+ before: result.before,
292
+ after: result.after,
293
+ performed_by: identityFromApiClient(apiClient)
294
+ });
295
+ console.log(chalk.gray(`\nAudit log: ${audit.path}\n`));
296
+ return result;
297
+ }
298
+
299
+ // Re-export helpers so tests can exercise them with mocked api clients / fs.
300
+ export const _internal = {
301
+ resolveAuditLogPath,
302
+ appendAudit,
303
+ identityFromApiClient,
304
+ printResolution,
305
+ printBeforeAfter,
306
+ AUDIT_LOG_RELATIVE
307
+ };
308
+
309
+ export default {
310
+ runAppSetDefaultModel,
311
+ runKbSetOverrideModel,
312
+ runKbClearOverrideModel
313
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * ONE OWNER for the retirement of every superseded KB-sync surface.
3
+ *
4
+ * CEO 2026-08-29: "there should never be a deprecated path and then everything else is a
5
+ * waterfall of problems." The CLI exposes exactly ONE kb sync surface — `descix kb corpus
6
+ * sync` (git-synced manifest). Every other sync path is DELETED: its implementation is gone
7
+ * and its old name exits NON-ZERO naming the replacement. That is not a compat fence — a
8
+ * compat fence is a path that still WORKS under an old name. These do not work at all.
9
+ *
10
+ * Why a module and not a literal at each site: two derivations of one fact is the general
11
+ * form of mirror drift. The retired set is enumerated ONCE here. bin/descix.js registers by
12
+ * ITERATING this list via registerAllRetiredKbSync() -- it does not name any retired verb
13
+ * itself -- and tests/kb-sync-single-surface.test.js asserts against the same list. So the
14
+ * drift is closed in BOTH directions: a surface added to the list is registered, and a
15
+ * surface cannot be registered without being in the list.
16
+ */
17
+
18
+ /** The single canonical KB sync surface. Named in every refusal. */
19
+ export const CANONICAL_KB_SYNC = 'descix kb corpus sync';
20
+
21
+ /** The verb that creates a KB — `kb corpus sync`'s dependency (I4). */
22
+ export const CANONICAL_KB_CREATE = 'descix kb create';
23
+
24
+ /**
25
+ * Every KB-sync surface retired by ws-devplane-cli-kb-surface-single-canonical-sync.
26
+ * `invocation` is what a user types; `parent` is the commander parent it hung off.
27
+ */
28
+ export const RETIRED_KB_SYNC_SURFACES = [
29
+ { id: 'sync', parent: 'program', name: 'sync', invocation: 'descix sync', registered: true },
30
+ { id: 'sync.kb', parent: 'sync', name: 'kb', invocation: 'descix sync kb', registered: true },
31
+ { id: 'kb.chunk', parent: 'kb', name: 'chunk', invocation: 'descix kb chunk', registered: true },
32
+ { id: 'kb.sync', parent: 'kb', name: 'sync', invocation: 'descix kb sync', registered: true },
33
+ // registered:false -- `update` survives for app/site, so this one is a dispatcher branch
34
+ // inside the live `update` command rather than a commander registration of its own.
35
+ { id: 'update.kb', parent: 'update', name: 'kb', invocation: 'descix update kb', registered: false },
36
+ ];
37
+
38
+ /** Implementations deleted with those surfaces. No exported symbol survives without a caller (I3). */
39
+ export const DELETED_KB_SYNC_SYMBOLS = [
40
+ 'runKbChunk',
41
+ 'runKbSync',
42
+ 'runKbBuild',
43
+ 'runKbStatus',
44
+ 'runKbCompare',
45
+ 'updateKB',
46
+ ];
47
+
48
+ /**
49
+ * The single refusal message. Fails loud and names the replacement — never warns, never falls back.
50
+ * @param {string} invocation - the retired invocation the user typed, e.g. 'descix kb chunk'
51
+ * @returns {string}
52
+ */
53
+ export function retiredKbSyncMessage(invocation) {
54
+ return (
55
+ `\`${invocation}\` has been REMOVED. There is no replacement flag and no fallback.\n\n` +
56
+ `Use the one canonical KB sync surface:\n` +
57
+ ` ${CANONICAL_KB_SYNC} -a <app_id> [-k <kb_name>]\n\n` +
58
+ `It syncs from a git-tracked corpus manifest (.descix/manifests/), which the removed ` +
59
+ `path could not do. If the KB does not exist yet, create it first with ` +
60
+ `\`${CANONICAL_KB_CREATE}\`.`
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Refuse a retired invocation LOUDLY and exit non-zero.
66
+ *
67
+ * MEASURED, not assumed: a `throw` from updateAuto is NOT swallowed — updateAuto's own catch
68
+ * prints the message before rethrowing, so both forms reach the user and exit 1. This exists
69
+ * for two smaller, real reasons: (1) ONE owner for the refusal, so the dispatcher branch and
70
+ * updateAuto's kb branch cannot drift into different wording or different exit codes; and
71
+ * (2) a deliberate retirement is not a crash, so it should not be framed as "✖ Update failed".
72
+ *
73
+ * @param {string} invocation - the retired invocation, e.g. 'descix update kb'
74
+ * @param {(s:string)=>string} red - chalk.red or equivalent
75
+ * @returns {never}
76
+ */
77
+ export function refuseRetiredKbSync(invocation, red) {
78
+ console.error(red(`\n❌ ${retiredKbSyncMessage(invocation)}\n`));
79
+ process.exit(1);
80
+ }
81
+
82
+ /**
83
+ * Register a retired invocation so it exits NON-ZERO naming the canonical surface.
84
+ * Accepts and ignores any args/options the old surface took, so an old script fails with
85
+ * this message rather than a confusing commander parse error.
86
+ *
87
+ * @param {import('commander').Command} parent - commander parent to attach to
88
+ * @param {string} name - the retired command name
89
+ * @param {string} invocation - full invocation used in the message
90
+ * @param {(s:string)=>string} red - chalk.red or equivalent
91
+ */
92
+ export function registerRetiredKbSync(parent, name, invocation, red) {
93
+ return parent
94
+ .command(name, { hidden: true })
95
+ .description(`REMOVED — use \`${CANONICAL_KB_SYNC}\``)
96
+ .allowUnknownOption(true)
97
+ // helpOption(false) is load-bearing: commander intercepts `--help` BEFORE the action and
98
+ // exits 0. A retired surface must exit NON-ZERO however it is invoked — `descix kb chunk`
99
+ // and `descix kb chunk --help` must both fail and name the replacement. Without this, the
100
+ // most natural way a user probes a dead verb is the one way it appears to still work.
101
+ .helpOption(false)
102
+ .argument('[args...]', 'ignored')
103
+ .action(() => {
104
+ console.error(red(`\n❌ ${retiredKbSyncMessage(invocation)}\n`));
105
+ process.exit(1);
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Register EVERY retired surface by iterating RETIRED_KB_SYNC_SURFACES.
111
+ *
112
+ * This is what makes the one-owner claim true rather than merely asserted: bin/descix.js
113
+ * calls this once and never names a retired verb itself, so a retired surface cannot be
114
+ * registered without appearing in the list above.
115
+ *
116
+ * @param {Record<string, import('commander').Command>} roots - seed parents, e.g. { program, kb }
117
+ * @param {(s:string)=>string} red - chalk.red or equivalent
118
+ * @returns {Record<string, import('commander').Command>} roots plus each registered surface by id
119
+ */
120
+ export function registerAllRetiredKbSync(roots, red) {
121
+ const byId = { ...roots };
122
+ for (const s of RETIRED_KB_SYNC_SURFACES) {
123
+ if (!s.registered) continue;
124
+ const parent = byId[s.parent];
125
+ if (!parent) {
126
+ throw new Error(
127
+ `retired-kb-sync: no parent '${s.parent}' available for '${s.invocation}'. ` +
128
+ 'Seed it in the roots map or order the list so the parent is registered first.'
129
+ );
130
+ }
131
+ byId[s.id] = registerRetiredKbSync(parent, s.name, s.invocation, red);
132
+ }
133
+ return byId;
134
+ }
@@ -1,12 +1,37 @@
1
1
  /**
2
2
  * descix serve — thin CLI wrapper around @descix/app-sdk gateway.
3
+ *
4
+ * The API target follows the SAME channel every other CLI command uses: the
5
+ * global `--api-url` / `--env` flags are normalized into DESCIX_API_URL by the
6
+ * program's preAction hook, and this command consumes that one value. There is
7
+ * no second `--api-url` flag on `serve`.
8
+ *
9
+ * The port follows the same shape: this wrapper passes ONLY what the developer
10
+ * explicitly asked for (`--port`). It does NOT substitute a default — the
11
+ * gateway's resolveGatewayPort owns the chain (--port > env.gateway.port >
12
+ * built-in default), so the server and the product map the shell bakes cannot
13
+ * name different ports.
14
+ *
15
+ * `cwd` is passed SEPARATELY from `workspaceRoot` on purpose. workspaceRoot is
16
+ * walked UP from to find .descix/workspace.json, so by the time the gateway has
17
+ * it, the directory the developer was actually standing in is gone — and that
18
+ * directory is exactly what picks the app to serve (AMB-2).
3
19
  */
4
20
 
5
21
  import { runGateway } from '@descix/app-sdk/dev';
6
22
 
7
23
  export async function runServe(options = {}) {
24
+ const apiUrl = process.env.DESCIX_API_URL || undefined;
25
+
8
26
  await runGateway({
9
- port: options.port || 5173,
27
+ port: options.port,
28
+ portSource: options.port !== undefined ? '--port' : undefined,
10
29
  workspaceRoot: options.workspaceRoot || process.cwd(),
30
+ apiUrl,
31
+ apiSource: apiUrl ? 'CLI --api-url/--env (DESCIX_API_URL)' : undefined,
32
+ siteUrl: options.siteUrl,
33
+ siteSource: options.siteUrl ? 'CLI --site-url' : undefined,
34
+ app: options.app,
35
+ cwd: process.cwd(),
11
36
  });
12
37
  }
@@ -7,7 +7,7 @@
7
7
  * - Workspace context and mode
8
8
  * - Configuration state
9
9
  *
10
- * Uses PathContext for workspace resolution - no scattered process.cwd() calls
10
+ * Uses WorkspaceConfig for workspace resolution - no scattered process.cwd() calls
11
11
  */
12
12
 
13
13
  import chalk from 'chalk';
@@ -18,8 +18,6 @@ import { exec } from 'child_process';
18
18
  import { promisify } from 'util';
19
19
  import { DeSciXApiClient } from '../api-client.js';
20
20
  import { GlobalConfig } from '../global-config.js';
21
- import { PathContext } from '../core/PathContext.js';
22
- import { detectWorkspaceMode } from '../workspace-utils.js';
23
21
  import { isAuthenticated } from '../auth-guard.js';
24
22
 
25
23
  const execAsync = promisify(exec);
@@ -94,19 +92,18 @@ export async function runStatus(options = {}) {
94
92
  console.log(`Environment: ${globalConfig.environment} (${globalConfig.api_url})`);
95
93
  console.log();
96
94
 
97
- // Try to load workspace context — WorkspaceConfig first (handles v2), fallback to PathContext
98
- let ctx = null;
95
+ // Load workspace context via WorkspaceConfig
99
96
  let workspaceRoot = null;
100
97
  let workspaceConfig = null;
101
98
  try {
102
99
  const { WorkspaceConfig } = await import('../workspace-config.js');
103
- const wsConfig = await WorkspaceConfig.load();
104
- workspaceRoot = wsConfig.getWorkspaceRoot();
105
- workspaceConfig = wsConfig;
100
+ const wsConfig = await WorkspaceConfig.tryLoad();
101
+ if (wsConfig) {
102
+ workspaceRoot = wsConfig.getWorkspaceRoot();
103
+ workspaceConfig = wsConfig;
104
+ }
106
105
  } catch {
107
- // Fallback to PathContext
108
- ctx = await PathContext.tryLoad();
109
- workspaceRoot = ctx?.getWorkspaceRoot();
106
+ // workspace not found — workspaceConfig stays null
110
107
  }
111
108
 
112
109
  // 2. Authentication
@@ -141,52 +138,27 @@ export async function runStatus(options = {}) {
141
138
  console.log(chalk.white('Workspace Context'));
142
139
  console.log(chalk.white('-----------------'));
143
140
 
144
- if (workspaceConfig || ctx) {
141
+ if (workspaceConfig) {
145
142
  console.log(`Config File: ${path.join(workspaceRoot, '.descix', 'workspace.json')}`);
146
143
 
147
- if (workspaceConfig) {
148
- // V2 path — use WorkspaceConfig which handles env.platform/products format
149
- try {
150
- const defaultCtx = workspaceConfig.defaultContext;
151
- if (defaultCtx) {
152
- console.log(`Community: ${defaultCtx.communityId || 'unknown'}`);
153
- console.log(`App: ${defaultCtx.appId || 'unknown'}`);
154
- }
155
-
156
- // List all configured apps
157
- const platform = workspaceConfig.env?.platform;
158
- const products = workspaceConfig.env?.products || [];
159
- const allApps = [platform?.appId, ...products.map(p => p.appId)].filter(Boolean);
160
- if (allApps.length > 0) {
161
- console.log(`Local Apps: ${allApps.join(', ')}`);
162
- }
163
-
164
- console.log(`API: ${workspaceConfig.apiUrl || 'unknown'}`);
165
- console.log(`Local Root: ${workspaceRoot}`);
166
- } catch (error) {
167
- console.log(`Mode: ${chalk.red('Unknown/Invalid')} (${error.message})`);
168
- }
169
- } else {
170
- // Legacy PathContext path
171
- const wsRawConfig = ctx.getWorkspaceConfig();
172
- try {
173
- const mode = detectWorkspaceMode(wsRawConfig);
174
- console.log(`Mode: ${chalk.green(mode)}`);
175
-
176
- if (wsRawConfig.communities) {
177
- for (const [commId, comm] of Object.entries(wsRawConfig.communities)) {
178
- const appCount = Object.keys(comm.apps || {}).length;
179
- console.log(chalk.gray(` └─ ${commId}: ${appCount} app(s)`));
180
- }
181
- }
182
- console.log(`Local Root: ${workspaceRoot}`);
183
- } catch (error) {
184
- console.log(`Mode: ${chalk.red('Unknown/Invalid')} (${error.message})`);
144
+ try {
145
+ // List all configured apps from env.platform + env.products
146
+ const platform = workspaceConfig.env?.platform;
147
+ const products = workspaceConfig.env?.products || [];
148
+ const allApps = [platform?.appId, ...products.map(p => p.appId)].filter(Boolean);
149
+ if (allApps.length > 0) {
150
+ console.log(`Local Apps: ${allApps.join(', ')}`);
185
151
  }
152
+
153
+ console.log(`API: ${workspaceConfig.getApiUrl() ||
154
+ '(not configured — run `descix config set-env dev|demo|prod`)'}`);
155
+ console.log(`Local Root: ${workspaceRoot}`);
156
+ } catch (error) {
157
+ console.log(`Mode: ${chalk.red('Unknown/Invalid')} (${error.message})`);
186
158
  }
187
159
  } else {
188
160
  console.log(`Status: ${chalk.yellow('No workspace found')}`);
189
- console.log(`Action: Run 'descix init -c <community> -a <app> -p .' to create a workspace`);
161
+ console.log(`Action: Run 'descix app init -a <app_id>' to initialize a workspace`);
190
162
  }
191
163
  console.log();
192
164