@descix/cli 1.0.6 → 1.0.8
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.
- package/README.md +1 -1
- package/agent-assets/guides/README.md +1 -1
- package/agent-assets/guides/dynamic-site-pattern.md +17 -30
- package/agent-assets/guides/knowledge-only-pattern.md +11 -15
- package/agent-assets/guides/microservice-pattern.md +12 -14
- package/agent-assets/guides/static-site-pattern.md +12 -15
- package/agent-assets/instructions/app-asset-validation.md +1 -1
- package/agent-assets/instructions/onboarding-flow.md +4 -4
- package/agent-assets/reference/app-creation.md +36 -101
- package/agent-assets/reference/architecture.md +43 -51
- package/agent-assets/reference/local-dev.md +1 -6
- package/agent-assets/reference/mcp-integration.md +21 -23
- package/agent-assets/reference/workspace-config.md +97 -340
- package/bin/descix.js +337 -618
- package/lib/api-client.js +30 -22
- package/lib/command-visibility.js +213 -0
- package/lib/commands/airdrop.js +10 -0
- package/lib/commands/auth.js +16 -2
- package/lib/commands/config.js +13 -31
- package/lib/commands/corpus.js +163 -59
- package/lib/commands/health.js +116 -21
- package/lib/commands/init.js +17 -45
- package/lib/commands/kb.js +7 -0
- package/lib/commands/retired-kb-sync.js +5 -27
- package/lib/commands/status.js +0 -9
- package/lib/core/Chunker.js +1 -1
- package/lib/core/CorpusWalker.js +238 -47
- package/lib/core/ManifestLoader.js +68 -0
- package/lib/core/Syncer.js +147 -46
- package/lib/interactive.js +158 -0
- package/lib/origin.js +3 -3
- package/lib/sdk-assets.js +28 -0
- package/lib/verb-invokes.generated.json +233 -0
- package/lib/wallet-file.js +36 -37
- package/lib/workspace-config.js +266 -199
- package/lib/workspace-identity.js +0 -1
- package/package.json +7 -3
- package/templates/agent-claude.md +1 -3
- package/templates/agent-cline.md +1 -2
- package/templates/agent-copilot.md +1 -2
- package/templates/agent-cursor.md +1 -2
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +1 -1
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +2 -3
- package/agent-assets/rules/descix_mcp.template.mdc +0 -243
- package/lib/commands/briefer/errors.js +0 -88
- package/lib/commands/briefer/index.js +0 -319
- package/lib/commands/briefer/sources/canonical-sources.js +0 -129
- package/lib/commands/briefer/sources/entitlements.js +0 -220
- package/lib/commands/briefer/sources/environments.js +0 -205
- package/lib/commands/briefer/sources/identifiers.js +0 -214
- package/lib/commands/briefer/sources/microservice-deploy.js +0 -267
- package/lib/commands/briefer/sources/routing.js +0 -275
- package/lib/commands/briefer/sources/what-is-not.js +0 -223
- package/lib/commands/briefer/stitcher.js +0 -134
- package/lib/commands/briefer/util/source-reader.js +0 -502
- package/lib/commands/mcp.js +0 -273
- package/lib/commands/update.js +0 -581
- package/lib/wizard/setup.js +0 -503
- package/templates/descix_mcp.mdc +0 -122
package/lib/workspace-config.js
CHANGED
|
@@ -54,9 +54,98 @@ export function resolveWorkspacePath(workspaceRoot, localPath, label = 'app') {
|
|
|
54
54
|
return path.resolve(workspaceRoot, localPath);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The ONE text for "there is no workspace here". It is correct advice for exactly one state —
|
|
59
|
+
* the file does not exist — and it is a lie for every other state, which is why it now has a
|
|
60
|
+
* single owner instead of being retyped at each throw site.
|
|
61
|
+
*/
|
|
62
|
+
const NOT_CONFIGURED_MESSAGE =
|
|
63
|
+
'Workspace not configured.\n' +
|
|
64
|
+
'Run "npx descix init" first to initialize your workspace.';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* THE ONE OWNER of "how do I set the API origin". Named by every refusal that turns a developer
|
|
68
|
+
* away from a retired way of setting it (the retired setter verb, a top-level `apiUrl` key), so
|
|
69
|
+
* the three live surfaces are spelled once. Order: persistent known env, persistent custom
|
|
70
|
+
* origin, per-run.
|
|
71
|
+
*/
|
|
72
|
+
export const TOP_LEVEL_API_URL_REMEDY =
|
|
73
|
+
'descix config init --env dev|demo|prod # pin a known environment (writes env.apiUrl)\n' +
|
|
74
|
+
' descix config set-env <name> --url <origin> # pin a custom origin under your own env name\n' +
|
|
75
|
+
' descix --api-url <origin> <command> # this run only; no workspace change (e.g. serve)';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* THE ONE OWNER of the "move it aside, do not delete it" remedy.
|
|
79
|
+
*
|
|
80
|
+
* TWO different diagnoses reach this SAME remedy: a workspace.json that cannot be READ
|
|
81
|
+
* (WorkspaceUnreadableError) and one that reads fine but carries the retired v1 schema
|
|
82
|
+
* (load()'s v1 branch). Each used to hand-write its own remedy and THEY DISAGREED - the v1
|
|
83
|
+
* branch prescribed DELETING the file outright, while its sibling forty lines above it in this
|
|
84
|
+
* same file told the user their contents are lost only if they delete it. Two derivations of
|
|
85
|
+
* one fact drift silently; this is the extracted owner both now consume.
|
|
86
|
+
*
|
|
87
|
+
* A REFUSAL MUST NEVER PRESCRIBE A REMEDY THAT DESTROYS THE THING IT IS DIAGNOSING. That rule
|
|
88
|
+
* was ALREADY WRITTEN IN THIS FILE, as prose, a few lines above the branch that violated it -
|
|
89
|
+
* and it still shipped. A rule stated beside code does not enforce itself, and proximity may
|
|
90
|
+
* even hurt: a reader who sees the rule assumes it is in force. So it is expressed here as a
|
|
91
|
+
* FUNCTION every refusal must CALL, rather than a comment every author must NOTICE.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} configPath - absolute path to the workspace.json being refused
|
|
94
|
+
* @param {string} initVerb - the verb that creates a fresh workspace once this file is aside
|
|
95
|
+
* @returns {string} the remedy block (no trailing newline)
|
|
96
|
+
*/
|
|
97
|
+
export function moveAsideRemedy(configPath, initVerb) {
|
|
98
|
+
return (
|
|
99
|
+
` • Recover it: restore ${path.basename(configPath)} from version control or a backup.\n` +
|
|
100
|
+
` • If you do not need its contents, move it aside first\n` +
|
|
101
|
+
` (mv "${configPath}" "${configPath}.broken") and then run "${initVerb}".\n` +
|
|
102
|
+
` Its contents are lost only if you delete it.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* THE ONE OWNER of "this workspace.json EXISTS and I could not read it".
|
|
108
|
+
*
|
|
109
|
+
* ABSENT and UNREADABLE are two different facts about the world and they need two different
|
|
110
|
+
* remedies. load() used to collapse them — every read error, JSON syntax error and permission
|
|
111
|
+
* error became NOT_CONFIGURED_MESSAGE — so a corrupt-but-present workspace.json reported as
|
|
112
|
+
* absent and the message prescribed `descix init` over a file that was still recoverable. A
|
|
113
|
+
* REFUSAL MUST NEVER PRESCRIBE A REMEDY THAT DESTROYS THE THING IT IS DIAGNOSING.
|
|
114
|
+
*
|
|
115
|
+
* It is a TYPE, not a message prefix, because callers have to branch on it (tryLoad returns
|
|
116
|
+
* null for absent and rethrows this) and matching on error text is the narrow-string-matching
|
|
117
|
+
* anti-pattern this file already suffered from — load()'s own v1 re-throw was an
|
|
118
|
+
* `error.message.includes(...)` test, and it is deleted in the same change.
|
|
119
|
+
*
|
|
120
|
+
* save() already refuses to overwrite a present-but-unparseable file and names it. This is the
|
|
121
|
+
* READ side of that same judgment, so the two halves of the file's lifecycle now agree.
|
|
122
|
+
*/
|
|
123
|
+
export class WorkspaceUnreadableError extends Error {
|
|
124
|
+
constructor(configPath, cause) {
|
|
125
|
+
// fs errors already lead with their code ("EACCES: permission denied, open ..."); JSON.parse
|
|
126
|
+
// errors carry no code at all. Prefix only when it is not already there, so the reason line
|
|
127
|
+
// never reads "EACCES: EACCES: ...".
|
|
128
|
+
const raw = cause?.message || String(cause);
|
|
129
|
+
const detail = cause?.code && !raw.startsWith(cause.code) ? `${cause.code}: ${raw}` : raw;
|
|
130
|
+
super(
|
|
131
|
+
`Workspace config exists but could not be read.\n` +
|
|
132
|
+
` File: ${configPath}\n` +
|
|
133
|
+
` Reason: ${detail}\n` +
|
|
134
|
+
`\n` +
|
|
135
|
+
`This file is still on disk and has NOT been modified. Do NOT run "descix init" here —\n` +
|
|
136
|
+
`that command is for a workspace that is ABSENT, and this one is DAMAGED.\n` +
|
|
137
|
+
moveAsideRemedy(configPath, 'descix init')
|
|
138
|
+
);
|
|
139
|
+
this.name = 'WorkspaceUnreadableError';
|
|
140
|
+
this.code = 'WORKSPACE_UNREADABLE';
|
|
141
|
+
this.configPath = configPath;
|
|
142
|
+
this.cause = cause;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
57
146
|
/**
|
|
58
147
|
* WorkspaceConfig - Manages workspace-specific configuration
|
|
59
|
-
*
|
|
148
|
+
*
|
|
60
149
|
* This is the SOLE configuration methodology for DeSciX CLI.
|
|
61
150
|
* All path resolution and context detection uses workspace.json.
|
|
62
151
|
*
|
|
@@ -73,13 +162,9 @@ export class WorkspaceConfig {
|
|
|
73
162
|
|
|
74
163
|
// Legacy fields (kept for save() PWA response conversion compatibility)
|
|
75
164
|
this.defaultContext = config.defaultContext;
|
|
76
|
-
this.apiUrl = config.apiUrl;
|
|
77
165
|
this.environment = config.environment;
|
|
78
166
|
this.directoryMappings = config.directoryMappings || {};
|
|
79
167
|
|
|
80
|
-
// V2.1 Product tracking (Unified Registry)
|
|
81
|
-
this.products = config.products || {};
|
|
82
|
-
|
|
83
168
|
// app_id → localPath map from env.platform + env.products (Unified Registry)
|
|
84
169
|
this._appIdToConfig = this._buildAppIdMap(config);
|
|
85
170
|
|
|
@@ -149,6 +234,87 @@ export class WorkspaceConfig {
|
|
|
149
234
|
return null;
|
|
150
235
|
}
|
|
151
236
|
|
|
237
|
+
/**
|
|
238
|
+
* The RAW registry entry for an app — `localPath` exactly as workspace.json carries it, with NO
|
|
239
|
+
* path resolution and therefore no possibility of throwing on a bad stored value.
|
|
240
|
+
*
|
|
241
|
+
* This exists for exactly one caller shape: a command that is about to REPLACE an app's
|
|
242
|
+
* localPath. Such a command must not resolve the OUTGOING value. getAppByAppId() resolves, and
|
|
243
|
+
* resolving the value you are on your way to overwrite is what made `descix app set-localpath`
|
|
244
|
+
* unusable precisely when it was needed — a workspace already carrying a rejected localPath
|
|
245
|
+
* could not be repaired by the one verb whose job is repairing it, while the rejection itself
|
|
246
|
+
* forbade hand-editing. The verb was the prescribed remedy and the verb refused.
|
|
247
|
+
*
|
|
248
|
+
* It reads the SAME map getAppByAppId reads (_buildAppIdMap is still the one owner of "what is
|
|
249
|
+
* mapped"); it simply stops short of resolution. Do NOT use it to obtain a usable path — take
|
|
250
|
+
* getAppByAppId().absolutePath for that, so the loader's rejection still runs.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} appId - App/product identifier
|
|
253
|
+
* @returns {{ localPath: string, communityId: string|null, kbId: string }|null}
|
|
254
|
+
*/
|
|
255
|
+
getAppEntry(appId) {
|
|
256
|
+
if (!appId) return null;
|
|
257
|
+
return this._appIdToConfig?.[appId] || null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* THE ONE OWNER of "find the LIVE env entry for this app, or hard-fail".
|
|
262
|
+
*
|
|
263
|
+
* Live means the actual object inside this.env — mutate it and save() persists the mutation.
|
|
264
|
+
* getAppByAppId() and getAppEntry() both return copies and are useless for writing.
|
|
265
|
+
*
|
|
266
|
+
* The one lookup every entry setter (setMicroservicePort, setStaticSite, setLocalPath) uses. A
|
|
267
|
+
* private walk of env.products ONLY misses the platform app, finds nothing, writes nothing and
|
|
268
|
+
* still reports success.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} appId - App identifier
|
|
271
|
+
* @returns {Object} the live env.platform or env.products[] entry
|
|
272
|
+
* @throws if appId is not mapped
|
|
273
|
+
*/
|
|
274
|
+
_liveEnvEntry(appId) {
|
|
275
|
+
let entry = null;
|
|
276
|
+
if (this.env?.platform?.appId === appId) {
|
|
277
|
+
entry = this.env.platform;
|
|
278
|
+
} else if (Array.isArray(this.env?.products)) {
|
|
279
|
+
entry = this.env.products.find(p => p.appId === appId) || null;
|
|
280
|
+
}
|
|
281
|
+
if (!entry) {
|
|
282
|
+
throw new Error(unmappedAppMessage(appId));
|
|
283
|
+
}
|
|
284
|
+
return entry;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Update an app's localPath in env.platform or env.products[].
|
|
289
|
+
*
|
|
290
|
+
* The canonical write path for localPath. Backs `descix app set-localpath`.
|
|
291
|
+
*
|
|
292
|
+
* localPath MUST be workspace-root-relative. It is validated here through
|
|
293
|
+
* resolveWorkspacePath — the LOADER'S OWN resolver, not a second copy of its rules — so a
|
|
294
|
+
* value the loader will refuse on the next read is refused now, before anything is written.
|
|
295
|
+
* Writing a value the loader rejects is what bricked workspaces: every subsequent command
|
|
296
|
+
* threw, including the repair verbs, while the rejection forbade hand-editing.
|
|
297
|
+
*
|
|
298
|
+
* Persists via save() — same auto-save pattern as its siblings.
|
|
299
|
+
* Hard-fails if appId is not mapped in env.platform or env.products.
|
|
300
|
+
*
|
|
301
|
+
* @param {string} appId - App identifier (must exist in env.platform or env.products)
|
|
302
|
+
* @param {string} localPath - New path, relative to the workspace root
|
|
303
|
+
* @returns {Promise<string>} Path to saved config (from save())
|
|
304
|
+
*/
|
|
305
|
+
async setLocalPath(appId, localPath) {
|
|
306
|
+
if (!appId) throw new Error('appId is required');
|
|
307
|
+
if (!localPath) throw new Error('localPath is required');
|
|
308
|
+
|
|
309
|
+
const entry = this._liveEnvEntry(appId);
|
|
310
|
+
|
|
311
|
+
// Validate through the loader's own resolver. Throws on an absolute path.
|
|
312
|
+
resolveWorkspacePath(this.workspaceRoot, localPath, appId);
|
|
313
|
+
|
|
314
|
+
entry.localPath = localPath;
|
|
315
|
+
return this.save();
|
|
316
|
+
}
|
|
317
|
+
|
|
152
318
|
/**
|
|
153
319
|
* Get the absolute path to an app's site/ directory
|
|
154
320
|
* @param {string} appId - App identifier
|
|
@@ -216,54 +382,98 @@ export class WorkspaceConfig {
|
|
|
216
382
|
*
|
|
217
383
|
* Automatically searches upward from startDir to find workspace root.
|
|
218
384
|
*
|
|
385
|
+
* ABSENT vs UNREADABLE IS DECIDED HERE AND NOWHERE ELSE. Callers consume the distinction as a
|
|
386
|
+
* type (WorkspaceUnreadableError), never by re-deciding it or by matching on message text.
|
|
387
|
+
*
|
|
219
388
|
* @param {string} startDir - Directory to start searching from (default: cwd)
|
|
220
389
|
* @returns {Promise<WorkspaceConfig>} Configuration object
|
|
221
|
-
* @throws {Error}
|
|
390
|
+
* @throws {Error} NOT_CONFIGURED_MESSAGE when no workspace.json exists
|
|
391
|
+
* @throws {WorkspaceUnreadableError} when one exists and cannot be read or parsed
|
|
222
392
|
*/
|
|
223
393
|
static async load(startDir = process.cwd()) {
|
|
224
394
|
// First, find workspace root by searching upward
|
|
225
395
|
const workspaceRoot = await WorkspaceConfig.findWorkspaceRoot(startDir);
|
|
226
396
|
if (!workspaceRoot) {
|
|
227
|
-
throw new Error(
|
|
228
|
-
'Workspace not configured.\n' +
|
|
229
|
-
'Run "npx descix init" first to initialize your workspace.'
|
|
230
|
-
);
|
|
397
|
+
throw new Error(NOT_CONFIGURED_MESSAGE);
|
|
231
398
|
}
|
|
232
399
|
|
|
233
400
|
const configPath = path.join(workspaceRoot, '.descix', 'workspace.json');
|
|
234
|
-
try {
|
|
235
|
-
const data = await fs.readFile(configPath, 'utf-8');
|
|
236
|
-
const parsed = JSON.parse(data);
|
|
237
401
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
402
|
+
let data;
|
|
403
|
+
try {
|
|
404
|
+
data = await fs.readFile(configPath, 'utf-8');
|
|
405
|
+
} catch (error) {
|
|
406
|
+
// ENOENT is the only genuinely ABSENT case reachable here: findWorkspaceRoot stat'd this
|
|
407
|
+
// file a moment ago, so ENOENT means it disappeared in between. Everything else — EACCES
|
|
408
|
+
// on a file we can see but not open, EISDIR, EIO — is a file that EXISTS. Reporting any of
|
|
409
|
+
// those as "not configured" is what sent users to `descix init` over recoverable data.
|
|
410
|
+
if (error.code === 'ENOENT') throw new Error(NOT_CONFIGURED_MESSAGE);
|
|
411
|
+
throw new WorkspaceUnreadableError(configPath, error);
|
|
412
|
+
}
|
|
245
413
|
|
|
246
|
-
|
|
414
|
+
let parsed;
|
|
415
|
+
try {
|
|
416
|
+
parsed = JSON.parse(data);
|
|
247
417
|
} catch (error) {
|
|
248
|
-
|
|
418
|
+
// The file is present and holds bytes we could not parse. Name the parse error and the
|
|
419
|
+
// path; never absorb this into "not configured".
|
|
420
|
+
throw new WorkspaceUnreadableError(configPath, error);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// v1 format hard-error: has communities block but no env block.
|
|
424
|
+
// This throw now sits OUTSIDE any catch, so it propagates on its own. The previous
|
|
425
|
+
// `error.message.includes('v1 workspace format')` re-throw was narrow string matching
|
|
426
|
+
// (DeSciX anti-pattern #6) guarding a catch that should never have been this wide.
|
|
427
|
+
if (parsed.communities && !parsed.env) {
|
|
249
428
|
throw new Error(
|
|
250
|
-
'
|
|
251
|
-
|
|
429
|
+
'v1 workspace format is not supported. Migrate to v2.1.\n' +
|
|
430
|
+
` File: ${configPath}\n` +
|
|
431
|
+
'\n' +
|
|
432
|
+
'This file is still on disk and has NOT been modified. There is no automatic v1 → v2.1\n' +
|
|
433
|
+
'migration: no surviving code reads the v1 layout, so its contents cannot be converted\n' +
|
|
434
|
+
'for you — but they are still readable BY YOU, and this file is the only copy of them.\n' +
|
|
435
|
+
moveAsideRemedy(configPath, 'descix app init')
|
|
252
436
|
);
|
|
253
437
|
}
|
|
438
|
+
|
|
439
|
+
// A v2.1 file carrying a TOP-LEVEL `apiUrl` is refused by name. That key is retired: no
|
|
440
|
+
// reader consumes it (the origin owner resolves `env.apiUrl` only) and save() never writes
|
|
441
|
+
// it, so a developer who set it by hand would be silently ignored on every invocation —
|
|
442
|
+
// exactly what the retired setter verb used to do from the other side, printing a success
|
|
443
|
+
// banner over a value that landed nowhere. The file is otherwise fine; the remedy is to remove the
|
|
444
|
+
// one key and set the origin where it is read. Nothing here is destructive.
|
|
445
|
+
if (typeof parsed.apiUrl === 'string' && parsed.apiUrl.trim() !== '') {
|
|
446
|
+
throw new Error(
|
|
447
|
+
'workspace.json carries a top-level "apiUrl", which is a retired v1 key that nothing reads.\n' +
|
|
448
|
+
` File: ${configPath}\n` +
|
|
449
|
+
` Value: ${parsed.apiUrl.trim()}\n` +
|
|
450
|
+
'\n' +
|
|
451
|
+
'The API origin lives at env.apiUrl. Remove the top-level key, then set the origin with:\n' +
|
|
452
|
+
` ${TOP_LEVEL_API_URL_REMEDY}`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return new WorkspaceConfig(parsed, workspaceRoot);
|
|
254
457
|
}
|
|
255
|
-
|
|
458
|
+
|
|
256
459
|
/**
|
|
257
460
|
* Try to load workspace configuration, return null if not found
|
|
258
461
|
* Useful for commands that need to check if workspace exists
|
|
259
|
-
*
|
|
462
|
+
*
|
|
463
|
+
* RETURNS NULL FOR ABSENT, THROWS FOR UNREADABLE. "There is no workspace here" and "the
|
|
464
|
+
* workspace here is damaged" are different answers and only the first one is a soft no.
|
|
465
|
+
* Swallowing the second made a corrupt workspace look like a fresh directory, which is how a
|
|
466
|
+
* damaged workspace silently fell back to the DEFAULT (prod) origin instead of stopping.
|
|
467
|
+
*
|
|
260
468
|
* @param {string} startDir - Directory to start searching from
|
|
261
|
-
* @returns {Promise<WorkspaceConfig|null>}
|
|
469
|
+
* @returns {Promise<WorkspaceConfig|null>} config, or null when no workspace.json exists
|
|
470
|
+
* @throws {WorkspaceUnreadableError} when one exists and cannot be read or parsed
|
|
262
471
|
*/
|
|
263
472
|
static async tryLoad(startDir = process.cwd()) {
|
|
264
473
|
try {
|
|
265
474
|
return await WorkspaceConfig.load(startDir);
|
|
266
|
-
} catch {
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error instanceof WorkspaceUnreadableError) throw error;
|
|
267
477
|
return null;
|
|
268
478
|
}
|
|
269
479
|
}
|
|
@@ -338,23 +548,6 @@ export class WorkspaceConfig {
|
|
|
338
548
|
return configPath;
|
|
339
549
|
}
|
|
340
550
|
|
|
341
|
-
/**
|
|
342
|
-
* Compute and store absolute paths for all communities and apps
|
|
343
|
-
* Called automatically by save() to ensure paths are pre-computed
|
|
344
|
-
*
|
|
345
|
-
* @param {string} workspaceRoot - Workspace root directory
|
|
346
|
-
*/
|
|
347
|
-
computeAbsolutePaths(workspaceRoot) {
|
|
348
|
-
const absWorkspaceRoot = path.resolve(workspaceRoot);
|
|
349
|
-
|
|
350
|
-
// Compute absolute paths for products
|
|
351
|
-
for (const [productId, product] of Object.entries(this.products || {})) {
|
|
352
|
-
if (product.localPath) {
|
|
353
|
-
product.absolutePath = resolveWorkspacePath(absWorkspaceRoot, product.localPath, productId);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
551
|
// ============ App Registration Methods ============
|
|
359
552
|
|
|
360
553
|
/**
|
|
@@ -372,6 +565,14 @@ export class WorkspaceConfig {
|
|
|
372
565
|
throw new Error('appId and localPath are required');
|
|
373
566
|
}
|
|
374
567
|
|
|
568
|
+
// Validate through THE LOADER'S OWN RESOLVER — the same owner setLocalPath consumes, never a
|
|
569
|
+
// second copy of its rules. Three writers persisted a localPath and only ONE validated it, so
|
|
570
|
+
// `descix app init -p /abs` wrote an absolute value, created directories at that arbitrary
|
|
571
|
+
// absolute location, printed success, and bricked the workspace on the NEXT read — the loader
|
|
572
|
+
// refused the value for every command including the repair verbs, while the refusal forbade
|
|
573
|
+
// hand-editing. A path this CLI accepts must be honoured, or refused before anything is written.
|
|
574
|
+
resolveWorkspacePath(this.workspaceRoot, appConfig.localPath, appId);
|
|
575
|
+
|
|
375
576
|
if (!this.env) this.env = {};
|
|
376
577
|
if (!Array.isArray(this.env.products)) this.env.products = [];
|
|
377
578
|
|
|
@@ -401,58 +602,6 @@ export class WorkspaceConfig {
|
|
|
401
602
|
return true;
|
|
402
603
|
}
|
|
403
604
|
|
|
404
|
-
/**
|
|
405
|
-
* Register a product (Unified Registry)
|
|
406
|
-
* @param {string} productId - Global product identifier
|
|
407
|
-
* @param {Object} productConfig - Product configuration
|
|
408
|
-
* @returns {boolean} Success status
|
|
409
|
-
*/
|
|
410
|
-
registerProduct(productId, productConfig) {
|
|
411
|
-
if (!productId || !productConfig.localPath) {
|
|
412
|
-
throw new Error('productId and localPath are required');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
this.products[productId] = {
|
|
416
|
-
type: productConfig.type || 'APP',
|
|
417
|
-
localPath: productConfig.localPath,
|
|
418
|
-
context: productConfig.context || {},
|
|
419
|
-
registeredAt: new Date().toISOString()
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
this.version = '2.0';
|
|
423
|
-
return true;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Get product configuration
|
|
428
|
-
* @param {string} productId
|
|
429
|
-
* @returns {Object|null}
|
|
430
|
-
*/
|
|
431
|
-
getProduct(productId) {
|
|
432
|
-
return this.products[productId] || null;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* List all registered products
|
|
437
|
-
* @returns {Array} Array of product IDs
|
|
438
|
-
*/
|
|
439
|
-
listProducts() {
|
|
440
|
-
return Object.keys(this.products);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Remove a product registration
|
|
445
|
-
* @param {string} productId
|
|
446
|
-
* @returns {boolean}
|
|
447
|
-
*/
|
|
448
|
-
unregisterProduct(productId) {
|
|
449
|
-
if (this.products[productId]) {
|
|
450
|
-
delete this.products[productId];
|
|
451
|
-
return true;
|
|
452
|
-
}
|
|
453
|
-
return false;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
605
|
/**
|
|
457
606
|
* Resolve context based on file path
|
|
458
607
|
*
|
|
@@ -512,23 +661,6 @@ export class WorkspaceConfig {
|
|
|
512
661
|
}
|
|
513
662
|
}
|
|
514
663
|
|
|
515
|
-
// products object (legacy)
|
|
516
|
-
for (const [productId, product] of Object.entries(this.products || {})) {
|
|
517
|
-
let productPath = product.absolutePath;
|
|
518
|
-
if (!productPath && product.localPath && wsRoot) {
|
|
519
|
-
productPath = resolveWorkspacePath(wsRoot, product.localPath, productId);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if (productPath && cwd.startsWith(productPath)) {
|
|
523
|
-
return {
|
|
524
|
-
productId,
|
|
525
|
-
communityId: product.context?.community || null,
|
|
526
|
-
appId: product.context?.app || productId,
|
|
527
|
-
kbId: product.kbId || 'General'
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
664
|
return null;
|
|
533
665
|
}
|
|
534
666
|
|
|
@@ -583,19 +715,20 @@ export class WorkspaceConfig {
|
|
|
583
715
|
}
|
|
584
716
|
|
|
585
717
|
/**
|
|
586
|
-
* Get the API origin this workspace is configured for
|
|
718
|
+
* Get the API origin this workspace is configured for.
|
|
587
719
|
*
|
|
588
|
-
*
|
|
720
|
+
* The workspace names its origin at `env.apiUrl` and nowhere else. This used to end in
|
|
589
721
|
* `return DEFAULT_API_URL` — the shipped PROD origin — which meant an unconfigured workspace
|
|
590
722
|
* was indistinguishable from one that had deliberately chosen production, and every caller
|
|
591
723
|
* received a prod origin the developer had never picked. Resolution and the fail-loud live in
|
|
592
724
|
* `lib/origin.js`; this method only reports what THIS workspace file says.
|
|
593
725
|
*
|
|
594
|
-
* There is
|
|
595
|
-
* environment
|
|
596
|
-
* (`descix config set-env
|
|
726
|
+
* There is no environment-name-to-localhost derivation: an environment names a cloud
|
|
727
|
+
* environment. A local origin is either per-run (`descix --api-url https://localhost:4000
|
|
728
|
+
* serve ...`) or a custom env (`descix config set-env local --url ...`) — see
|
|
729
|
+
* TOP_LEVEL_API_URL_REMEDY, the one owner of that list.
|
|
597
730
|
*
|
|
598
|
-
* @returns {string
|
|
731
|
+
* @returns {string} the configured origin, or the declared default when this workspace names none
|
|
599
732
|
*/
|
|
600
733
|
getApiUrl() {
|
|
601
734
|
// Consumes the ONE origin owner rather than re-deriving the precedence or re-spelling the
|
|
@@ -604,7 +737,6 @@ export class WorkspaceConfig {
|
|
|
604
737
|
// resolveOrigin() and printed by the api-client, never by a null returned from here.
|
|
605
738
|
return resolveOrigin({
|
|
606
739
|
workspaceEnvApiUrl: this.env?.apiUrl,
|
|
607
|
-
legacyApiUrl: this.apiUrl,
|
|
608
740
|
}).origin;
|
|
609
741
|
}
|
|
610
742
|
|
|
@@ -672,60 +804,16 @@ export class WorkspaceConfig {
|
|
|
672
804
|
return this.save();
|
|
673
805
|
}
|
|
674
806
|
|
|
675
|
-
/**
|
|
676
|
-
* Update an app's site.port in env.products[] (or env.platform if it is the platform app).
|
|
677
|
-
*
|
|
678
|
-
* Pass `null` to remove site.port; if site.{} becomes empty, site.{} is also deleted.
|
|
679
|
-
* Persists via save() at the end — same auto-save pattern as setEnvironment().
|
|
680
|
-
* Hard-fails if appId is not mapped in env.platform or env.products.
|
|
681
|
-
*
|
|
682
|
-
* @param {string} appId - App identifier (must exist in env.platform or env.products)
|
|
683
|
-
* @param {number|string|null} port - Port number to set, or null to remove site.port
|
|
684
|
-
* @returns {Promise<string>} Path to saved config (from save())
|
|
685
|
-
*/
|
|
686
|
-
async setSitePort(appId, port) {
|
|
687
|
-
if (!appId) throw new Error('appId is required');
|
|
688
|
-
|
|
689
|
-
// Find the live env entry (platform or products[]) — not a copy
|
|
690
|
-
let entry = null;
|
|
691
|
-
if (this.env?.platform?.appId === appId) {
|
|
692
|
-
entry = this.env.platform;
|
|
693
|
-
} else if (Array.isArray(this.env?.products)) {
|
|
694
|
-
entry = this.env.products.find(p => p.appId === appId) || null;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
if (!entry) {
|
|
698
|
-
throw new Error(
|
|
699
|
-
unmappedAppMessage(appId)
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (port === null || port === undefined) {
|
|
704
|
-
// Remove site.port; clean up empty site.{}
|
|
705
|
-
if (entry.site) {
|
|
706
|
-
delete entry.site.port;
|
|
707
|
-
if (Object.keys(entry.site).length === 0) {
|
|
708
|
-
delete entry.site;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
} else {
|
|
712
|
-
if (!entry.site) entry.site = {};
|
|
713
|
-
entry.site.port = port;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
return this.save();
|
|
717
|
-
}
|
|
718
|
-
|
|
719
807
|
/**
|
|
720
808
|
* Update an app's microservice.port in env.products[] (or env.platform if it is the platform app).
|
|
721
809
|
*
|
|
722
|
-
*
|
|
810
|
+
* Operates on the entry's microservice.{} slot.
|
|
723
811
|
* This is the canonical write path for the microservice port that `descix microservice init`
|
|
724
812
|
* reads (and hard-fails on if missing). Backs the `descix app set-port` command, closing
|
|
725
813
|
* WS-CLI-MESH-ROUTING-GAP without hand-editing workspace.json.
|
|
726
814
|
*
|
|
727
815
|
* Pass `null` to remove microservice.port; if microservice.{} becomes empty, it is also deleted.
|
|
728
|
-
* Persists via save() at the end — same auto-save pattern as
|
|
816
|
+
* Persists via save() at the end — same auto-save pattern as setEnvironment().
|
|
729
817
|
* Hard-fails if appId is not mapped in env.platform or env.products.
|
|
730
818
|
*
|
|
731
819
|
* @param {string} appId - App identifier (must exist in env.platform or env.products)
|
|
@@ -735,19 +823,7 @@ export class WorkspaceConfig {
|
|
|
735
823
|
async setMicroservicePort(appId, port) {
|
|
736
824
|
if (!appId) throw new Error('appId is required');
|
|
737
825
|
|
|
738
|
-
|
|
739
|
-
let entry = null;
|
|
740
|
-
if (this.env?.platform?.appId === appId) {
|
|
741
|
-
entry = this.env.platform;
|
|
742
|
-
} else if (Array.isArray(this.env?.products)) {
|
|
743
|
-
entry = this.env.products.find(p => p.appId === appId) || null;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if (!entry) {
|
|
747
|
-
throw new Error(
|
|
748
|
-
unmappedAppMessage(appId)
|
|
749
|
-
);
|
|
750
|
-
}
|
|
826
|
+
const entry = this._liveEnvEntry(appId);
|
|
751
827
|
|
|
752
828
|
if (port === null || port === undefined) {
|
|
753
829
|
// Remove microservice.port; clean up empty microservice.{}
|
|
@@ -768,7 +844,7 @@ export class WorkspaceConfig {
|
|
|
768
844
|
/**
|
|
769
845
|
* Update an app's site config in env.products[] (or env.platform if it is the platform app).
|
|
770
846
|
*
|
|
771
|
-
* Parallel to
|
|
847
|
+
* Parallel to setMicroservicePort(), but operates on the entry's site.{} slot's
|
|
772
848
|
* static-site fields. This is the canonical write path for site.static — the relative path the
|
|
773
849
|
* dev gateway's staticSitePlugin serves at /p/{appId}/ (see createViteProxyConfig:
|
|
774
850
|
* site.static is resolved against the app's localPath; "." means the localPath itself).
|
|
@@ -777,7 +853,7 @@ export class WorkspaceConfig {
|
|
|
777
853
|
*
|
|
778
854
|
* Mutates site.static and/or site.port. Pass static === null to remove site.static; pass
|
|
779
855
|
* port === null to remove site.port. If site.{} becomes empty after removals it is deleted.
|
|
780
|
-
* Persists via save() at the end — same auto-save pattern as
|
|
856
|
+
* Persists via save() at the end — same auto-save pattern as setMicroservicePort().
|
|
781
857
|
* Hard-fails if appId is not mapped in env.platform or env.products.
|
|
782
858
|
*
|
|
783
859
|
* @param {string} appId - App identifier (must exist in env.platform or env.products)
|
|
@@ -790,19 +866,7 @@ export class WorkspaceConfig {
|
|
|
790
866
|
if (!appId) throw new Error('appId is required');
|
|
791
867
|
if (!fields || typeof fields !== 'object') throw new Error('fields object is required');
|
|
792
868
|
|
|
793
|
-
|
|
794
|
-
let entry = null;
|
|
795
|
-
if (this.env?.platform?.appId === appId) {
|
|
796
|
-
entry = this.env.platform;
|
|
797
|
-
} else if (Array.isArray(this.env?.products)) {
|
|
798
|
-
entry = this.env.products.find(p => p.appId === appId) || null;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
if (!entry) {
|
|
802
|
-
throw new Error(
|
|
803
|
-
unmappedAppMessage(appId)
|
|
804
|
-
);
|
|
805
|
-
}
|
|
869
|
+
const entry = this._liveEnvEntry(appId);
|
|
806
870
|
|
|
807
871
|
if (!entry.site) entry.site = {};
|
|
808
872
|
|
|
@@ -815,7 +879,7 @@ export class WorkspaceConfig {
|
|
|
815
879
|
}
|
|
816
880
|
}
|
|
817
881
|
|
|
818
|
-
// site.port — set or remove (
|
|
882
|
+
// site.port — set or remove (for static+devCommand sites)
|
|
819
883
|
if ('port' in fields) {
|
|
820
884
|
if (fields.port === null || fields.port === undefined) {
|
|
821
885
|
delete entry.site.port;
|
|
@@ -836,9 +900,12 @@ export class WorkspaceConfig {
|
|
|
836
900
|
* Persistently set the target environment in workspace.json.
|
|
837
901
|
*
|
|
838
902
|
* Updates env.environment and env.apiUrl, then saves. EVERY environment writes a
|
|
839
|
-
* URL — including dev, which writes the cloud DEV origin.
|
|
840
|
-
*
|
|
841
|
-
*
|
|
903
|
+
* URL — including dev, which writes the cloud DEV origin. For custom envs, uses --url or
|
|
904
|
+
* defaults to https://{name}.descix.net. The `config set-env` verb refuses `--url` on a
|
|
905
|
+
* KNOWN name (it sends you to `config init --env`), so a local backend is either per-run —
|
|
906
|
+
* `descix --api-url https://localhost:4000 <command>`, no workspace change — or a custom name
|
|
907
|
+
* (`set-env local --url ...`). Mind that cloud-core derives DEPLOY_ENV and port auto-detect
|
|
908
|
+
* only from `env.environment === 'DEV'`, so a custom name is not a DEV workspace.
|
|
842
909
|
*
|
|
843
910
|
* @param {string} envName - Environment name (dev, demo, prod, or custom)
|
|
844
911
|
* @param {string|null} [apiUrl] - Explicit API URL override
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descix/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test \"tests/*.test.js\"",
|
|
18
18
|
"pack": "npm pack",
|
|
19
|
-
"check:doc-paths": "node scripts/check-doc-paths.js"
|
|
19
|
+
"check:doc-paths": "node scripts/check-doc-paths.js",
|
|
20
|
+
"briefer": "node scripts/briefer/run.mjs"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
|
22
23
|
"descix",
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
"author": "DeSciX",
|
|
30
31
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"@descix/app-sdk": "^0.1.
|
|
33
|
+
"@descix/app-sdk": "^0.1.5",
|
|
33
34
|
"@descix/sdk": "^2.0.0",
|
|
34
35
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
35
36
|
"axios": "^1.7.9",
|
|
@@ -41,6 +42,9 @@
|
|
|
41
42
|
"ora": "^9.0.0",
|
|
42
43
|
"@descix/platform-api": "^2.0.0"
|
|
43
44
|
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"acorn": "^8.18.0"
|
|
47
|
+
},
|
|
44
48
|
"files": [
|
|
45
49
|
"bin",
|
|
46
50
|
"lib",
|