@descix/cli 1.0.6 → 1.0.7
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/agent-assets/guides/dynamic-site-pattern.md +10 -14
- 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/onboarding-flow.md +4 -4
- package/agent-assets/reference/app-creation.md +36 -101
- package/agent-assets/reference/architecture.md +24 -27
- package/agent-assets/reference/mcp-integration.md +14 -22
- package/agent-assets/reference/workspace-config.md +97 -340
- package/agent-assets/rules/descix_mcp.template.mdc +1 -1
- package/bin/descix.js +137 -75
- package/lib/api-client.js +30 -22
- package/lib/commands/airdrop.js +10 -0
- package/lib/commands/auth.js +16 -2
- package/lib/commands/briefer/index.js +8 -5
- package/lib/commands/config.js +13 -31
- package/lib/commands/corpus.js +161 -54
- package/lib/commands/health.js +116 -21
- package/lib/commands/init.js +17 -45
- package/lib/commands/kb.js +7 -0
- 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/wallet-file.js +36 -37
- package/lib/wizard/setup.js +104 -22
- package/lib/workspace-config.js +265 -163
- package/lib/workspace-identity.js +0 -1
- package/package.json +2 -2
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,90 @@ 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
|
+
* This existed as three byte-identical copies (setSitePort, setMicroservicePort,
|
|
267
|
+
* setStaticSite) and `set-localpath` hand-rolled a FOURTH variant in bin/descix.js that
|
|
268
|
+
* walked env.products ONLY. That omission is the whole reason the platform app silently
|
|
269
|
+
* failed: the loop found nothing, wrote nothing, and the command still printed success.
|
|
270
|
+
* A fourth copy here would have reproduced the same class of bug, so there is now one.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} appId - App identifier
|
|
273
|
+
* @returns {Object} the live env.platform or env.products[] entry
|
|
274
|
+
* @throws if appId is not mapped
|
|
275
|
+
*/
|
|
276
|
+
_liveEnvEntry(appId) {
|
|
277
|
+
let entry = null;
|
|
278
|
+
if (this.env?.platform?.appId === appId) {
|
|
279
|
+
entry = this.env.platform;
|
|
280
|
+
} else if (Array.isArray(this.env?.products)) {
|
|
281
|
+
entry = this.env.products.find(p => p.appId === appId) || null;
|
|
282
|
+
}
|
|
283
|
+
if (!entry) {
|
|
284
|
+
throw new Error(unmappedAppMessage(appId));
|
|
285
|
+
}
|
|
286
|
+
return entry;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Update an app's localPath in env.platform or env.products[].
|
|
291
|
+
*
|
|
292
|
+
* The canonical write path for localPath, and the sibling that was missing while
|
|
293
|
+
* setSitePort/setMicroservicePort/setStaticSite all existed. Backs `descix app set-localpath`.
|
|
294
|
+
*
|
|
295
|
+
* localPath MUST be workspace-root-relative. It is validated here through
|
|
296
|
+
* resolveWorkspacePath — the LOADER'S OWN resolver, not a second copy of its rules — so a
|
|
297
|
+
* value the loader will refuse on the next read is refused now, before anything is written.
|
|
298
|
+
* Writing a value the loader rejects is what bricked workspaces: every subsequent command
|
|
299
|
+
* threw, including the repair verbs, while the rejection forbade hand-editing.
|
|
300
|
+
*
|
|
301
|
+
* Persists via save() — same auto-save pattern as its siblings.
|
|
302
|
+
* Hard-fails if appId is not mapped in env.platform or env.products.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} appId - App identifier (must exist in env.platform or env.products)
|
|
305
|
+
* @param {string} localPath - New path, relative to the workspace root
|
|
306
|
+
* @returns {Promise<string>} Path to saved config (from save())
|
|
307
|
+
*/
|
|
308
|
+
async setLocalPath(appId, localPath) {
|
|
309
|
+
if (!appId) throw new Error('appId is required');
|
|
310
|
+
if (!localPath) throw new Error('localPath is required');
|
|
311
|
+
|
|
312
|
+
const entry = this._liveEnvEntry(appId);
|
|
313
|
+
|
|
314
|
+
// Validate through the loader's own resolver. Throws on an absolute path.
|
|
315
|
+
resolveWorkspacePath(this.workspaceRoot, localPath, appId);
|
|
316
|
+
|
|
317
|
+
entry.localPath = localPath;
|
|
318
|
+
return this.save();
|
|
319
|
+
}
|
|
320
|
+
|
|
152
321
|
/**
|
|
153
322
|
* Get the absolute path to an app's site/ directory
|
|
154
323
|
* @param {string} appId - App identifier
|
|
@@ -216,54 +385,98 @@ export class WorkspaceConfig {
|
|
|
216
385
|
*
|
|
217
386
|
* Automatically searches upward from startDir to find workspace root.
|
|
218
387
|
*
|
|
388
|
+
* ABSENT vs UNREADABLE IS DECIDED HERE AND NOWHERE ELSE. Callers consume the distinction as a
|
|
389
|
+
* type (WorkspaceUnreadableError), never by re-deciding it or by matching on message text.
|
|
390
|
+
*
|
|
219
391
|
* @param {string} startDir - Directory to start searching from (default: cwd)
|
|
220
392
|
* @returns {Promise<WorkspaceConfig>} Configuration object
|
|
221
|
-
* @throws {Error}
|
|
393
|
+
* @throws {Error} NOT_CONFIGURED_MESSAGE when no workspace.json exists
|
|
394
|
+
* @throws {WorkspaceUnreadableError} when one exists and cannot be read or parsed
|
|
222
395
|
*/
|
|
223
396
|
static async load(startDir = process.cwd()) {
|
|
224
397
|
// First, find workspace root by searching upward
|
|
225
398
|
const workspaceRoot = await WorkspaceConfig.findWorkspaceRoot(startDir);
|
|
226
399
|
if (!workspaceRoot) {
|
|
227
|
-
throw new Error(
|
|
228
|
-
'Workspace not configured.\n' +
|
|
229
|
-
'Run "npx descix init" first to initialize your workspace.'
|
|
230
|
-
);
|
|
400
|
+
throw new Error(NOT_CONFIGURED_MESSAGE);
|
|
231
401
|
}
|
|
232
402
|
|
|
233
403
|
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
404
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
405
|
+
let data;
|
|
406
|
+
try {
|
|
407
|
+
data = await fs.readFile(configPath, 'utf-8');
|
|
408
|
+
} catch (error) {
|
|
409
|
+
// ENOENT is the only genuinely ABSENT case reachable here: findWorkspaceRoot stat'd this
|
|
410
|
+
// file a moment ago, so ENOENT means it disappeared in between. Everything else — EACCES
|
|
411
|
+
// on a file we can see but not open, EISDIR, EIO — is a file that EXISTS. Reporting any of
|
|
412
|
+
// those as "not configured" is what sent users to `descix init` over recoverable data.
|
|
413
|
+
if (error.code === 'ENOENT') throw new Error(NOT_CONFIGURED_MESSAGE);
|
|
414
|
+
throw new WorkspaceUnreadableError(configPath, error);
|
|
415
|
+
}
|
|
245
416
|
|
|
246
|
-
|
|
417
|
+
let parsed;
|
|
418
|
+
try {
|
|
419
|
+
parsed = JSON.parse(data);
|
|
247
420
|
} catch (error) {
|
|
248
|
-
|
|
421
|
+
// The file is present and holds bytes we could not parse. Name the parse error and the
|
|
422
|
+
// path; never absorb this into "not configured".
|
|
423
|
+
throw new WorkspaceUnreadableError(configPath, error);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// v1 format hard-error: has communities block but no env block.
|
|
427
|
+
// This throw now sits OUTSIDE any catch, so it propagates on its own. The previous
|
|
428
|
+
// `error.message.includes('v1 workspace format')` re-throw was narrow string matching
|
|
429
|
+
// (DeSciX anti-pattern #6) guarding a catch that should never have been this wide.
|
|
430
|
+
if (parsed.communities && !parsed.env) {
|
|
249
431
|
throw new Error(
|
|
250
|
-
'
|
|
251
|
-
|
|
432
|
+
'v1 workspace format is not supported. Migrate to v2.1.\n' +
|
|
433
|
+
` File: ${configPath}\n` +
|
|
434
|
+
'\n' +
|
|
435
|
+
'This file is still on disk and has NOT been modified. There is no automatic v1 → v2.1\n' +
|
|
436
|
+
'migration: no surviving code reads the v1 layout, so its contents cannot be converted\n' +
|
|
437
|
+
'for you — but they are still readable BY YOU, and this file is the only copy of them.\n' +
|
|
438
|
+
moveAsideRemedy(configPath, 'descix app init')
|
|
252
439
|
);
|
|
253
440
|
}
|
|
441
|
+
|
|
442
|
+
// A v2.1 file carrying a TOP-LEVEL `apiUrl` is refused by name. That key is retired: no
|
|
443
|
+
// reader consumes it (the origin owner resolves `env.apiUrl` only) and save() never writes
|
|
444
|
+
// it, so a developer who set it by hand would be silently ignored on every invocation —
|
|
445
|
+
// exactly what the retired setter verb used to do from the other side, printing a success
|
|
446
|
+
// banner over a value that landed nowhere. The file is otherwise fine; the remedy is to remove the
|
|
447
|
+
// one key and set the origin where it is read. Nothing here is destructive.
|
|
448
|
+
if (typeof parsed.apiUrl === 'string' && parsed.apiUrl.trim() !== '') {
|
|
449
|
+
throw new Error(
|
|
450
|
+
'workspace.json carries a top-level "apiUrl", which is a retired v1 key that nothing reads.\n' +
|
|
451
|
+
` File: ${configPath}\n` +
|
|
452
|
+
` Value: ${parsed.apiUrl.trim()}\n` +
|
|
453
|
+
'\n' +
|
|
454
|
+
'The API origin lives at env.apiUrl. Remove the top-level key, then set the origin with:\n' +
|
|
455
|
+
` ${TOP_LEVEL_API_URL_REMEDY}`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return new WorkspaceConfig(parsed, workspaceRoot);
|
|
254
460
|
}
|
|
255
|
-
|
|
461
|
+
|
|
256
462
|
/**
|
|
257
463
|
* Try to load workspace configuration, return null if not found
|
|
258
464
|
* Useful for commands that need to check if workspace exists
|
|
259
|
-
*
|
|
465
|
+
*
|
|
466
|
+
* RETURNS NULL FOR ABSENT, THROWS FOR UNREADABLE. "There is no workspace here" and "the
|
|
467
|
+
* workspace here is damaged" are different answers and only the first one is a soft no.
|
|
468
|
+
* Swallowing the second made a corrupt workspace look like a fresh directory, which is how a
|
|
469
|
+
* damaged workspace silently fell back to the DEFAULT (prod) origin instead of stopping.
|
|
470
|
+
*
|
|
260
471
|
* @param {string} startDir - Directory to start searching from
|
|
261
|
-
* @returns {Promise<WorkspaceConfig|null>}
|
|
472
|
+
* @returns {Promise<WorkspaceConfig|null>} config, or null when no workspace.json exists
|
|
473
|
+
* @throws {WorkspaceUnreadableError} when one exists and cannot be read or parsed
|
|
262
474
|
*/
|
|
263
475
|
static async tryLoad(startDir = process.cwd()) {
|
|
264
476
|
try {
|
|
265
477
|
return await WorkspaceConfig.load(startDir);
|
|
266
|
-
} catch {
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (error instanceof WorkspaceUnreadableError) throw error;
|
|
267
480
|
return null;
|
|
268
481
|
}
|
|
269
482
|
}
|
|
@@ -338,23 +551,6 @@ export class WorkspaceConfig {
|
|
|
338
551
|
return configPath;
|
|
339
552
|
}
|
|
340
553
|
|
|
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
554
|
// ============ App Registration Methods ============
|
|
359
555
|
|
|
360
556
|
/**
|
|
@@ -372,6 +568,14 @@ export class WorkspaceConfig {
|
|
|
372
568
|
throw new Error('appId and localPath are required');
|
|
373
569
|
}
|
|
374
570
|
|
|
571
|
+
// Validate through THE LOADER'S OWN RESOLVER — the same owner setLocalPath consumes, never a
|
|
572
|
+
// second copy of its rules. Three writers persisted a localPath and only ONE validated it, so
|
|
573
|
+
// `descix app init -p /abs` wrote an absolute value, created directories at that arbitrary
|
|
574
|
+
// absolute location, printed success, and bricked the workspace on the NEXT read — the loader
|
|
575
|
+
// refused the value for every command including the repair verbs, while the refusal forbade
|
|
576
|
+
// hand-editing. A path this CLI accepts must be honoured, or refused before anything is written.
|
|
577
|
+
resolveWorkspacePath(this.workspaceRoot, appConfig.localPath, appId);
|
|
578
|
+
|
|
375
579
|
if (!this.env) this.env = {};
|
|
376
580
|
if (!Array.isArray(this.env.products)) this.env.products = [];
|
|
377
581
|
|
|
@@ -401,58 +605,6 @@ export class WorkspaceConfig {
|
|
|
401
605
|
return true;
|
|
402
606
|
}
|
|
403
607
|
|
|
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
608
|
/**
|
|
457
609
|
* Resolve context based on file path
|
|
458
610
|
*
|
|
@@ -512,23 +664,6 @@ export class WorkspaceConfig {
|
|
|
512
664
|
}
|
|
513
665
|
}
|
|
514
666
|
|
|
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
667
|
return null;
|
|
533
668
|
}
|
|
534
669
|
|
|
@@ -583,19 +718,20 @@ export class WorkspaceConfig {
|
|
|
583
718
|
}
|
|
584
719
|
|
|
585
720
|
/**
|
|
586
|
-
* Get the API origin this workspace is configured for
|
|
721
|
+
* Get the API origin this workspace is configured for.
|
|
587
722
|
*
|
|
588
|
-
*
|
|
723
|
+
* The workspace names its origin at `env.apiUrl` and nowhere else. This used to end in
|
|
589
724
|
* `return DEFAULT_API_URL` — the shipped PROD origin — which meant an unconfigured workspace
|
|
590
725
|
* was indistinguishable from one that had deliberately chosen production, and every caller
|
|
591
726
|
* received a prod origin the developer had never picked. Resolution and the fail-loud live in
|
|
592
727
|
* `lib/origin.js`; this method only reports what THIS workspace file says.
|
|
593
728
|
*
|
|
594
|
-
* There is
|
|
595
|
-
* environment
|
|
596
|
-
* (`descix config set-env
|
|
729
|
+
* There is no environment-name-to-localhost derivation: an environment names a cloud
|
|
730
|
+
* environment. A local origin is either per-run (`descix --api-url https://localhost:4000
|
|
731
|
+
* serve ...`) or a custom env (`descix config set-env local --url ...`) — see
|
|
732
|
+
* TOP_LEVEL_API_URL_REMEDY, the one owner of that list.
|
|
597
733
|
*
|
|
598
|
-
* @returns {string
|
|
734
|
+
* @returns {string} the configured origin, or the declared default when this workspace names none
|
|
599
735
|
*/
|
|
600
736
|
getApiUrl() {
|
|
601
737
|
// Consumes the ONE origin owner rather than re-deriving the precedence or re-spelling the
|
|
@@ -604,7 +740,6 @@ export class WorkspaceConfig {
|
|
|
604
740
|
// resolveOrigin() and printed by the api-client, never by a null returned from here.
|
|
605
741
|
return resolveOrigin({
|
|
606
742
|
workspaceEnvApiUrl: this.env?.apiUrl,
|
|
607
|
-
legacyApiUrl: this.apiUrl,
|
|
608
743
|
}).origin;
|
|
609
744
|
}
|
|
610
745
|
|
|
@@ -686,19 +821,7 @@ export class WorkspaceConfig {
|
|
|
686
821
|
async setSitePort(appId, port) {
|
|
687
822
|
if (!appId) throw new Error('appId is required');
|
|
688
823
|
|
|
689
|
-
|
|
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
|
-
}
|
|
824
|
+
const entry = this._liveEnvEntry(appId);
|
|
702
825
|
|
|
703
826
|
if (port === null || port === undefined) {
|
|
704
827
|
// Remove site.port; clean up empty site.{}
|
|
@@ -735,19 +858,7 @@ export class WorkspaceConfig {
|
|
|
735
858
|
async setMicroservicePort(appId, port) {
|
|
736
859
|
if (!appId) throw new Error('appId is required');
|
|
737
860
|
|
|
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
|
-
}
|
|
861
|
+
const entry = this._liveEnvEntry(appId);
|
|
751
862
|
|
|
752
863
|
if (port === null || port === undefined) {
|
|
753
864
|
// Remove microservice.port; clean up empty microservice.{}
|
|
@@ -790,19 +901,7 @@ export class WorkspaceConfig {
|
|
|
790
901
|
if (!appId) throw new Error('appId is required');
|
|
791
902
|
if (!fields || typeof fields !== 'object') throw new Error('fields object is required');
|
|
792
903
|
|
|
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
|
-
}
|
|
904
|
+
const entry = this._liveEnvEntry(appId);
|
|
806
905
|
|
|
807
906
|
if (!entry.site) entry.site = {};
|
|
808
907
|
|
|
@@ -836,9 +935,12 @@ export class WorkspaceConfig {
|
|
|
836
935
|
* Persistently set the target environment in workspace.json.
|
|
837
936
|
*
|
|
838
937
|
* Updates env.environment and env.apiUrl, then saves. EVERY environment writes a
|
|
839
|
-
* URL — including dev, which writes the cloud DEV origin.
|
|
840
|
-
*
|
|
841
|
-
*
|
|
938
|
+
* URL — including dev, which writes the cloud DEV origin. For custom envs, uses --url or
|
|
939
|
+
* defaults to https://{name}.descix.net. The `config set-env` verb refuses `--url` on a
|
|
940
|
+
* KNOWN name (it sends you to `config init --env`), so a local backend is either per-run —
|
|
941
|
+
* `descix --api-url https://localhost:4000 <command>`, no workspace change — or a custom name
|
|
942
|
+
* (`set-env local --url ...`). Mind that cloud-core derives DEPLOY_ENV and port auto-detect
|
|
943
|
+
* only from `env.environment === 'DEV'`, so a custom name is not a DEV workspace.
|
|
842
944
|
*
|
|
843
945
|
* @param {string} envName - Environment name (dev, demo, prod, or custom)
|
|
844
946
|
* @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.7",
|
|
4
4
|
"description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"author": "DeSciX",
|
|
30
30
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@descix/app-sdk": "^0.1.
|
|
32
|
+
"@descix/app-sdk": "^0.1.5",
|
|
33
33
|
"@descix/sdk": "^2.0.0",
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
35
35
|
"axios": "^1.7.9",
|