@descix/cli 1.0.21 → 2.0.1
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/bin/descix.js +14 -122
- package/lib/commands/appInit.js +15 -29
- package/lib/commands/auth.js +10 -1
- package/lib/commands/config.js +4 -6
- package/lib/commands/corpus.js +1 -1
- package/lib/commands/health.js +18 -44
- package/lib/commands/init.js +2 -3
- package/lib/commands/kb.js +1 -1
- package/lib/commands/microservice-restart.js +28 -33
- package/lib/commands/status.js +4 -6
- package/lib/verb-invokes.generated.json +2 -3
- package/lib/workspace-config.js +167 -153
- package/lib/workspace-identity.js +6 -13
- package/package.json +2 -2
- package/lib/workspace-utils.js +0 -106
package/lib/workspace-config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { ENV_ORIGINS } from '@descix/app-sdk/dev';
|
|
3
|
+
import { ENV_ORIGINS, resolveWorkspaceApps } from '@descix/app-sdk/dev';
|
|
4
4
|
import { resolveOrigin } from './origin.js';
|
|
5
5
|
// The one canonical KB-sync surface, from its owner. The example in requireContext() is a remedy
|
|
6
6
|
// a stuck developer will copy verbatim, so it must resolve to the live verb rather than a literal
|
|
@@ -22,8 +22,9 @@ import { npxCommand } from './invocation.js';
|
|
|
22
22
|
*/
|
|
23
23
|
export function unmappedAppMessage(appId) {
|
|
24
24
|
return (
|
|
25
|
-
`App "${appId}" is not
|
|
26
|
-
'
|
|
25
|
+
`App "${appId}" is not this workspace's app (workspace.json env.app). ` +
|
|
26
|
+
'A v3 workspace is single-app: run commands for another app from ITS repo root, ' +
|
|
27
|
+
'or run `descix app init -a <id>` in a fresh repo to create its workspace.'
|
|
27
28
|
);
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -55,6 +56,55 @@ export function resolveWorkspacePath(workspaceRoot, localPath, label = 'app') {
|
|
|
55
56
|
return path.resolve(workspaceRoot, localPath);
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The local app set of a loaded workspace — THE one CLI-side entry to resolveWorkspaceApps.
|
|
61
|
+
* Every command that enumerates local apps (health, microservice restart) goes through here, so
|
|
62
|
+
* a config that carries no workspaceRoot FAILS LOUD in one place instead of each caller quietly
|
|
63
|
+
* substituting the cwd and enumerating a different workspace than the one it was handed.
|
|
64
|
+
*
|
|
65
|
+
* @param {WorkspaceConfig} wsConfig
|
|
66
|
+
* @returns {{ shellApp: Object|null, apps: Object[] }} see resolveWorkspaceApps
|
|
67
|
+
*/
|
|
68
|
+
export function resolveLocalApps(wsConfig) {
|
|
69
|
+
if (!wsConfig?.workspaceRoot) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'Cannot enumerate the workspace apps: the workspace config carries no workspaceRoot. ' +
|
|
72
|
+
'Load it with WorkspaceConfig.load() (which sets it) rather than constructing it bare.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return resolveWorkspaceApps(wsConfig.workspaceRoot);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `descix app init` runs where the APP REPO is — its root becomes the workspace root. A nested
|
|
80
|
+
* checkout used to WALK UP, find a parent workspace and silently register into it (the measured
|
|
81
|
+
* trap: an app repo cloned inside another workspace mapped itself into the parent's registry).
|
|
82
|
+
* Under v3 single-app there is nothing to register into: a workspace found at an ANCESTOR of the
|
|
83
|
+
* init directory is a refusal, loud, before anything is written anywhere.
|
|
84
|
+
*
|
|
85
|
+
* A workspace AT the directory itself is fine — that is the re-init case.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} dir - the directory `app init` is about to make a workspace root
|
|
88
|
+
* @throws when a workspace.json exists at a strict ancestor of dir
|
|
89
|
+
*/
|
|
90
|
+
export async function assertNoParentWorkspace(dir = process.cwd()) {
|
|
91
|
+
const resolved = path.resolve(dir);
|
|
92
|
+
const found = await WorkspaceConfig.findWorkspaceRoot(resolved);
|
|
93
|
+
if (found && path.resolve(found) !== resolved) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`This directory is nested inside another DeSciX workspace.\n` +
|
|
96
|
+
` Here: ${resolved}\n` +
|
|
97
|
+
` Parent workspace: ${found} (.descix/workspace.json)\n` +
|
|
98
|
+
`\n` +
|
|
99
|
+
`A v3 workspace is single-app: the app repo's own root is its workspace root, and\n` +
|
|
100
|
+
`"descix app init" refuses to run inside someone else's workspace rather than\n` +
|
|
101
|
+
`silently registering this checkout into it.\n` +
|
|
102
|
+
`Move this checkout OUT of ${found} (sibling checkouts, one gateway config, is the\n` +
|
|
103
|
+
`supported local layout), then run "descix app init" again from the repo root.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
58
108
|
/**
|
|
59
109
|
* The ONE text for "there is no workspace here". It is correct advice for exactly one state —
|
|
60
110
|
* the file does not exist — and it is a lie for every other state, which is why it now has a
|
|
@@ -144,6 +194,22 @@ export class WorkspaceUnreadableError extends Error {
|
|
|
144
194
|
}
|
|
145
195
|
}
|
|
146
196
|
|
|
197
|
+
/**
|
|
198
|
+
* A workspace.json in a RETIRED format (v1, v2.1 multi-app). It is a TYPE, like
|
|
199
|
+
* WorkspaceUnreadableError, because tryLoad() has to branch on it: "there is no workspace
|
|
200
|
+
* here" and "the workspace here is in a retired format" are different answers, and only the
|
|
201
|
+
* first is a soft null. Measured 2026-10-10: the refusal threw a PLAIN Error, tryLoad
|
|
202
|
+
* swallowed it, and five app repos' stale v2.1 files silently resolved to the DEFAULT origin —
|
|
203
|
+
* the exact wrong-env hazard the loader exists to prevent.
|
|
204
|
+
*/
|
|
205
|
+
export class WorkspaceRetiredFormatError extends Error {
|
|
206
|
+
constructor(message) {
|
|
207
|
+
super(message);
|
|
208
|
+
this.name = 'WorkspaceRetiredFormatError';
|
|
209
|
+
this.code = 'WORKSPACE_RETIRED_FORMAT';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
147
213
|
/**
|
|
148
214
|
* WorkspaceConfig - Manages workspace-specific configuration
|
|
149
215
|
*
|
|
@@ -166,7 +232,7 @@ export class WorkspaceConfig {
|
|
|
166
232
|
this.environment = config.environment;
|
|
167
233
|
this.directoryMappings = config.directoryMappings || {};
|
|
168
234
|
|
|
169
|
-
// app_id →
|
|
235
|
+
// app_id → config map from env.app (v3 single-app)
|
|
170
236
|
this._appIdToConfig = this._buildAppIdMap(config);
|
|
171
237
|
|
|
172
238
|
// Environment URLs for descix-serve gateway routing
|
|
@@ -177,50 +243,39 @@ export class WorkspaceConfig {
|
|
|
177
243
|
}
|
|
178
244
|
|
|
179
245
|
/**
|
|
180
|
-
* Build app_id → config map from env.
|
|
181
|
-
*
|
|
246
|
+
* Build the app_id → config map from env.app (v3 single-app).
|
|
247
|
+
*
|
|
248
|
+
* v3 holds exactly ONE app and its localPath is implicitly '.': the app repo's root IS the
|
|
249
|
+
* workspace root. The map shape survives (one entry) so every reader keyed on appId keeps
|
|
250
|
+
* its contract; the retired v2.1 multi-app walk (env.platform + env.products) is refused by
|
|
251
|
+
* load() and has no reader here any more.
|
|
252
|
+
*
|
|
182
253
|
* @param {Object} config - Parsed workspace.json
|
|
183
254
|
* @returns {Object} Map of appId -> { localPath, communityId?, kbId }
|
|
184
255
|
*/
|
|
185
256
|
_buildAppIdMap(config) {
|
|
186
257
|
const map = {};
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
localPath: env.platform.localPath,
|
|
194
|
-
communityId: env.platform.communityId || null,
|
|
195
|
-
kbId: env.platform.kbId || 'General'
|
|
258
|
+
const app = config.env?.app;
|
|
259
|
+
if (app?.appId) {
|
|
260
|
+
map[app.appId] = {
|
|
261
|
+
localPath: '.',
|
|
262
|
+
communityId: app.communityId || null,
|
|
263
|
+
kbId: app.kbId || 'General'
|
|
196
264
|
};
|
|
197
265
|
}
|
|
198
|
-
|
|
199
|
-
// env.products array (e.g. powch -> DeSciX_Powch)
|
|
200
|
-
const products = Array.isArray(env.products) ? env.products : [];
|
|
201
|
-
for (const p of products) {
|
|
202
|
-
if (p?.appId && p?.localPath) {
|
|
203
|
-
map[p.appId] = {
|
|
204
|
-
localPath: p.localPath,
|
|
205
|
-
communityId: p.communityId || null,
|
|
206
|
-
kbId: p.kbId || 'General'
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
266
|
return map;
|
|
212
267
|
}
|
|
213
268
|
|
|
214
269
|
/**
|
|
215
270
|
* Get app config by app_id (Unified Registry - app_id is globally unique)
|
|
216
|
-
* Resolves from env.
|
|
271
|
+
* Resolves from env.app (v3 single-app)
|
|
217
272
|
* @param {string} appId - App/product identifier
|
|
218
273
|
* @returns {Object|null} { localPath, absolutePath?, communityId?, kbId } or null
|
|
219
274
|
*/
|
|
220
275
|
getAppByAppId(appId) {
|
|
221
276
|
if (!appId || !this.workspaceRoot) return null;
|
|
222
277
|
|
|
223
|
-
// env.
|
|
278
|
+
// env.app (v3 single-app)
|
|
224
279
|
const fromEnv = this._appIdToConfig[appId];
|
|
225
280
|
if (fromEnv) {
|
|
226
281
|
const absPath = resolveWorkspacePath(this.workspaceRoot, fromEnv.localPath, appId);
|
|
@@ -235,87 +290,29 @@ export class WorkspaceConfig {
|
|
|
235
290
|
return null;
|
|
236
291
|
}
|
|
237
292
|
|
|
238
|
-
|
|
239
|
-
* The RAW registry entry for an app — `localPath` exactly as workspace.json carries it, with NO
|
|
240
|
-
* path resolution and therefore no possibility of throwing on a bad stored value.
|
|
241
|
-
*
|
|
242
|
-
* This exists for exactly one caller shape: a command that is about to REPLACE an app's
|
|
243
|
-
* localPath. Such a command must not resolve the OUTGOING value. getAppByAppId() resolves, and
|
|
244
|
-
* resolving the value you are on your way to overwrite is what made `descix app set-localpath`
|
|
245
|
-
* unusable precisely when it was needed — a workspace already carrying a rejected localPath
|
|
246
|
-
* could not be repaired by the one verb whose job is repairing it, while the rejection itself
|
|
247
|
-
* forbade hand-editing. The verb was the prescribed remedy and the verb refused.
|
|
248
|
-
*
|
|
249
|
-
* It reads the SAME map getAppByAppId reads (_buildAppIdMap is still the one owner of "what is
|
|
250
|
-
* mapped"); it simply stops short of resolution. Do NOT use it to obtain a usable path — take
|
|
251
|
-
* getAppByAppId().absolutePath for that, so the loader's rejection still runs.
|
|
252
|
-
*
|
|
253
|
-
* @param {string} appId - App/product identifier
|
|
254
|
-
* @returns {{ localPath: string, communityId: string|null, kbId: string }|null}
|
|
255
|
-
*/
|
|
256
|
-
getAppEntry(appId) {
|
|
257
|
-
if (!appId) return null;
|
|
258
|
-
return this._appIdToConfig?.[appId] || null;
|
|
259
|
-
}
|
|
293
|
+
|
|
260
294
|
|
|
261
295
|
/**
|
|
262
296
|
* THE ONE OWNER of "find the LIVE env entry for this app, or hard-fail".
|
|
263
297
|
*
|
|
264
298
|
* Live means the actual object inside this.env — mutate it and save() persists the mutation.
|
|
265
|
-
* getAppByAppId()
|
|
299
|
+
* getAppByAppId() returns copies and is useless for writing.
|
|
266
300
|
*
|
|
267
|
-
* The one lookup every entry setter (setMicroservicePort, setStaticSite
|
|
268
|
-
*
|
|
269
|
-
* still reports success.
|
|
301
|
+
* The one lookup every entry setter (setMicroservicePort, setStaticSite) uses. In v3 the only
|
|
302
|
+
* live entry is env.app; any other id hard-fails with the canonical unmapped message.
|
|
270
303
|
*
|
|
271
304
|
* @param {string} appId - App identifier
|
|
272
|
-
* @returns {Object} the live env.
|
|
273
|
-
* @throws if appId is not
|
|
305
|
+
* @returns {Object} the live env.app entry
|
|
306
|
+
* @throws if appId is not THE app
|
|
274
307
|
*/
|
|
275
308
|
_liveEnvEntry(appId) {
|
|
276
|
-
|
|
277
|
-
if (this.env?.platform?.appId === appId) {
|
|
278
|
-
entry = this.env.platform;
|
|
279
|
-
} else if (Array.isArray(this.env?.products)) {
|
|
280
|
-
entry = this.env.products.find(p => p.appId === appId) || null;
|
|
281
|
-
}
|
|
309
|
+
const entry = this.env?.app?.appId === appId ? this.env.app : null;
|
|
282
310
|
if (!entry) {
|
|
283
311
|
throw new Error(unmappedAppMessage(appId));
|
|
284
312
|
}
|
|
285
313
|
return entry;
|
|
286
314
|
}
|
|
287
315
|
|
|
288
|
-
/**
|
|
289
|
-
* Update an app's localPath in env.platform or env.products[].
|
|
290
|
-
*
|
|
291
|
-
* The canonical write path for localPath. Backs `descix app set-localpath`.
|
|
292
|
-
*
|
|
293
|
-
* localPath MUST be workspace-root-relative. It is validated here through
|
|
294
|
-
* resolveWorkspacePath — the LOADER'S OWN resolver, not a second copy of its rules — so a
|
|
295
|
-
* value the loader will refuse on the next read is refused now, before anything is written.
|
|
296
|
-
* Writing a value the loader rejects is what bricked workspaces: every subsequent command
|
|
297
|
-
* threw, including the repair verbs, while the rejection forbade hand-editing.
|
|
298
|
-
*
|
|
299
|
-
* Persists via save() — same auto-save pattern as its siblings.
|
|
300
|
-
* Hard-fails if appId is not mapped in env.platform or env.products.
|
|
301
|
-
*
|
|
302
|
-
* @param {string} appId - App identifier (must exist in env.platform or env.products)
|
|
303
|
-
* @param {string} localPath - New path, relative to the workspace root
|
|
304
|
-
* @returns {Promise<string>} Path to saved config (from save())
|
|
305
|
-
*/
|
|
306
|
-
async setLocalPath(appId, localPath) {
|
|
307
|
-
if (!appId) throw new Error('appId is required');
|
|
308
|
-
if (!localPath) throw new Error('localPath is required');
|
|
309
|
-
|
|
310
|
-
const entry = this._liveEnvEntry(appId);
|
|
311
|
-
|
|
312
|
-
// Validate through the loader's own resolver. Throws on an absolute path.
|
|
313
|
-
resolveWorkspacePath(this.workspaceRoot, localPath, appId);
|
|
314
|
-
|
|
315
|
-
entry.localPath = localPath;
|
|
316
|
-
return this.save();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
316
|
/**
|
|
320
317
|
* Get the absolute path to an app's site/ directory
|
|
321
318
|
* @param {string} appId - App identifier
|
|
@@ -426,17 +423,39 @@ export class WorkspaceConfig {
|
|
|
426
423
|
// `error.message.includes('v1 workspace format')` re-throw was narrow string matching
|
|
427
424
|
// (DeSciX anti-pattern #6) guarding a catch that should never have been this wide.
|
|
428
425
|
if (parsed.communities && !parsed.env) {
|
|
429
|
-
throw new
|
|
430
|
-
'v1 workspace format is not supported. Migrate to
|
|
426
|
+
throw new WorkspaceRetiredFormatError(
|
|
427
|
+
'v1 workspace format is not supported. Migrate to v3.\n' +
|
|
431
428
|
` File: ${configPath}\n` +
|
|
432
429
|
'\n' +
|
|
433
|
-
'This file is still on disk and has NOT been modified. There is no automatic v1 →
|
|
430
|
+
'This file is still on disk and has NOT been modified. There is no automatic v1 → v3\n' +
|
|
434
431
|
'migration: no surviving code reads the v1 layout, so its contents cannot be converted\n' +
|
|
435
432
|
'for you — but they are still readable BY YOU, and this file is the only copy of them.\n' +
|
|
436
433
|
moveAsideRemedy(configPath, 'descix app init')
|
|
437
434
|
);
|
|
438
435
|
}
|
|
439
436
|
|
|
437
|
+
// v2.1 MULTI-APP hard-error: env.platform / env.products are the retired mapping model —
|
|
438
|
+
// one workspace registering OTHER repos' apps by localPath. v3 is single-app (CEO
|
|
439
|
+
// 2026-10-09; fork (b) 2026-10-10): the app repo's root IS the workspace root, and local
|
|
440
|
+
// multi-app dev is sibling checkouts behind one gateway config. Same posture as the v1
|
|
441
|
+
// refusal above: loud, non-destructive, the remedy is one command per app repo.
|
|
442
|
+
if (parsed.env && (parsed.env.platform || parsed.env.products)) {
|
|
443
|
+
throw new WorkspaceRetiredFormatError(
|
|
444
|
+
'v2.1 multi-app workspace format is not supported. Migrate to v3 (single-app).\n' +
|
|
445
|
+
` File: ${configPath}\n` +
|
|
446
|
+
'\n' +
|
|
447
|
+
'This file is still on disk and has NOT been modified. v3 holds exactly ONE app\n' +
|
|
448
|
+
'(env.app) and the app repo\'s own root is the workspace root. To migrate:\n' +
|
|
449
|
+
' • In EACH app repo this file mapped, run from that repo\'s root:\n' +
|
|
450
|
+
' descix config init --env <dev|demo|prod> # pins env.apiUrl\n' +
|
|
451
|
+
' descix app init -a <app_id> # writes the v3 env.app block\n' +
|
|
452
|
+
' • A gateway hosting several local apps lists their repo roots in ITS OWN\n' +
|
|
453
|
+
' workspace.json under env.gateway.appRepos (fork (b): sibling checkouts, one\n' +
|
|
454
|
+
' gateway config).\n' +
|
|
455
|
+
moveAsideRemedy(configPath, 'descix app init')
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
440
459
|
// A v2.1 file carrying a TOP-LEVEL `apiUrl` is refused by name. That key is retired: no
|
|
441
460
|
// reader consumes it (the origin owner resolves `env.apiUrl` only) and save() never writes
|
|
442
461
|
// it, so a developer who set it by hand would be silently ignored on every invocation —
|
|
@@ -461,8 +480,9 @@ export class WorkspaceConfig {
|
|
|
461
480
|
* Try to load workspace configuration, return null if not found
|
|
462
481
|
* Useful for commands that need to check if workspace exists
|
|
463
482
|
*
|
|
464
|
-
* RETURNS NULL FOR ABSENT, THROWS FOR UNREADABLE. "There is no
|
|
465
|
-
* workspace here is damaged"
|
|
483
|
+
* RETURNS NULL FOR ABSENT, THROWS FOR UNREADABLE AND FOR RETIRED FORMATS. "There is no
|
|
484
|
+
* workspace here", "the workspace here is damaged" and "the workspace here is in a retired
|
|
485
|
+
* format" are different answers and only the first one is a soft no.
|
|
466
486
|
* Swallowing the second made a corrupt workspace look like a fresh directory, which is how a
|
|
467
487
|
* damaged workspace silently fell back to the DEFAULT (prod) origin instead of stopping.
|
|
468
488
|
*
|
|
@@ -475,13 +495,14 @@ export class WorkspaceConfig {
|
|
|
475
495
|
return await WorkspaceConfig.load(startDir);
|
|
476
496
|
} catch (error) {
|
|
477
497
|
if (error instanceof WorkspaceUnreadableError) throw error;
|
|
498
|
+
if (error instanceof WorkspaceRetiredFormatError) throw error;
|
|
478
499
|
return null;
|
|
479
500
|
}
|
|
480
501
|
}
|
|
481
502
|
|
|
482
503
|
/**
|
|
483
|
-
* Save workspace configuration in
|
|
484
|
-
*
|
|
504
|
+
* Save workspace configuration in v3 (single-app) format: env.app, never
|
|
505
|
+
* env.platform / env.products.
|
|
485
506
|
*
|
|
486
507
|
* PRESERVES KEYS IT DOES NOT UNDERSTAND. This method used to serialize a fixed field list from
|
|
487
508
|
* memory over the whole file, so ANY top-level key it did not itself enumerate was silently
|
|
@@ -508,7 +529,7 @@ export class WorkspaceConfig {
|
|
|
508
529
|
|
|
509
530
|
this.workspaceRoot = root;
|
|
510
531
|
|
|
511
|
-
// Build env block for
|
|
532
|
+
// Build env block for v3 output
|
|
512
533
|
const envBlock = (this.env && Object.keys(this.env).length > 0) ? this.env : null;
|
|
513
534
|
|
|
514
535
|
// The keys this writer OWNS. Everything else on disk is carried through untouched.
|
|
@@ -537,7 +558,7 @@ export class WorkspaceConfig {
|
|
|
537
558
|
|
|
538
559
|
const configData = {
|
|
539
560
|
...carried,
|
|
540
|
-
version: '
|
|
561
|
+
version: '3.0',
|
|
541
562
|
workspaceRoot: path.resolve(root),
|
|
542
563
|
type: this.type,
|
|
543
564
|
};
|
|
@@ -552,54 +573,47 @@ export class WorkspaceConfig {
|
|
|
552
573
|
// ============ App Registration Methods ============
|
|
553
574
|
|
|
554
575
|
/**
|
|
555
|
-
* Register
|
|
556
|
-
*
|
|
557
|
-
*
|
|
576
|
+
* Register THE app of this v3 workspace (env.app).
|
|
577
|
+
*
|
|
578
|
+
* Single-app is the FORMAT, not a convention: a workspace that already holds a different app
|
|
579
|
+
* refuses loudly — there is no second slot to put one in, and silently replacing env.app
|
|
580
|
+
* would re-point every path-derived surface (site/, microservice/, manifests) at a different
|
|
581
|
+
* platform record. Re-registering the SAME app is idempotent (the `app init` re-run case) and
|
|
582
|
+
* may update kbId / communityId.
|
|
583
|
+
*
|
|
584
|
+
* localPath is GONE as an input: the app repo's root is the workspace root, always ('.').
|
|
585
|
+
*
|
|
586
|
+
* communityId is STORED, not just validated. It was previously accepted as a parameter and
|
|
587
|
+
* silently discarded, so `descix init -c egpt` wrote no trace of `egpt` anywhere — and
|
|
588
|
+
* `workspace-identity.js` (this entry's reader) then had no community to report, which is
|
|
589
|
+
* how the generated agent files named nobody's community. Writer and reader are a pair.
|
|
590
|
+
*
|
|
591
|
+
* @param {string} communityId - The app's community (server-authoritative value, mirrored here)
|
|
558
592
|
* @param {string} appId - App identifier (globally unique)
|
|
559
|
-
* @param {Object} appConfig -
|
|
560
|
-
* @param {string} appConfig.localPath - Local folder path relative to workspaceRoot
|
|
561
|
-
* @param {string} [appConfig.kbId] - Default knowledge base ID
|
|
593
|
+
* @param {Object} [appConfig] - { kbId? }
|
|
562
594
|
* @returns {boolean} Success status
|
|
563
595
|
*/
|
|
564
|
-
registerApp(communityId, appId, appConfig) {
|
|
565
|
-
if (!appId
|
|
566
|
-
throw new Error('appId
|
|
596
|
+
registerApp(communityId, appId, appConfig = {}) {
|
|
597
|
+
if (!appId) {
|
|
598
|
+
throw new Error('appId is required');
|
|
567
599
|
}
|
|
568
600
|
|
|
569
|
-
// Validate through THE LOADER'S OWN RESOLVER — the same owner setLocalPath consumes, never a
|
|
570
|
-
// second copy of its rules. Three writers persisted a localPath and only ONE validated it, so
|
|
571
|
-
// `descix app init -p /abs` wrote an absolute value, created directories at that arbitrary
|
|
572
|
-
// absolute location, printed success, and bricked the workspace on the NEXT read — the loader
|
|
573
|
-
// refused the value for every command including the repair verbs, while the refusal forbade
|
|
574
|
-
// hand-editing. A path this CLI accepts must be honoured, or refused before anything is written.
|
|
575
|
-
resolveWorkspacePath(this.workspaceRoot, appConfig.localPath, appId);
|
|
576
|
-
|
|
577
601
|
if (!this.env) this.env = {};
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const entry = { appId, localPath: appConfig.localPath, kbId: appConfig.kbId || 'General' };
|
|
586
|
-
if (communityId) entry.communityId = communityId;
|
|
587
|
-
|
|
588
|
-
if (this.env.platform?.appId === appId) {
|
|
589
|
-
this.env.platform.localPath = appConfig.localPath;
|
|
590
|
-
if (appConfig.kbId) this.env.platform.kbId = appConfig.kbId;
|
|
591
|
-
if (communityId) this.env.platform.communityId = communityId;
|
|
592
|
-
} else {
|
|
593
|
-
const idx = this.env.products.findIndex(p => p.appId === appId);
|
|
594
|
-
if (idx >= 0) {
|
|
595
|
-
this.env.products[idx] = { ...this.env.products[idx], ...entry };
|
|
596
|
-
} else {
|
|
597
|
-
this.env.products.push(entry);
|
|
598
|
-
}
|
|
602
|
+
const existing = this.env.app;
|
|
603
|
+
if (existing?.appId && existing.appId !== appId) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`This workspace already holds app '${existing.appId}' — a v3 workspace is single-app.\n` +
|
|
606
|
+
`To work on '${appId}', clone or create its OWN repo and run "descix app init" from\n` +
|
|
607
|
+
`that repo's root (sibling checkouts, one gateway config, is the supported local layout).`
|
|
608
|
+
);
|
|
599
609
|
}
|
|
600
610
|
|
|
611
|
+
const entry = { ...(existing || {}), appId, kbId: appConfig.kbId || existing?.kbId || 'General' };
|
|
612
|
+
if (communityId) entry.communityId = communityId;
|
|
613
|
+
this.env.app = entry;
|
|
614
|
+
|
|
601
615
|
this._appIdToConfig = this._buildAppIdMap({ env: this.env, workspaceRoot: this.workspaceRoot });
|
|
602
|
-
this.version = '
|
|
616
|
+
this.version = '3.0';
|
|
603
617
|
return true;
|
|
604
618
|
}
|
|
605
619
|
|
|
@@ -648,7 +662,7 @@ export class WorkspaceConfig {
|
|
|
648
662
|
const cwd = path.resolve(startDir);
|
|
649
663
|
const wsRoot = this.workspaceRoot ? path.resolve(this.workspaceRoot) : null;
|
|
650
664
|
|
|
651
|
-
// env.
|
|
665
|
+
// env.app (v3 single-app): at most one entry
|
|
652
666
|
if (wsRoot) {
|
|
653
667
|
for (const [appId, cfg] of Object.entries(this._appIdToConfig || {})) {
|
|
654
668
|
const appPath = resolveWorkspacePath(wsRoot, cfg.localPath, appId);
|
|
@@ -776,7 +790,7 @@ export class WorkspaceConfig {
|
|
|
776
790
|
throw new Error(
|
|
777
791
|
`"${dottedKey}" is not a workspace-level env key.\n` +
|
|
778
792
|
` Settable here: ${ALLOWED.join(', ')}\n` +
|
|
779
|
-
' Per-app keys have their own verbs: descix app set-site / set-port
|
|
793
|
+
' Per-app keys have their own verbs: descix app set-site / set-port.'
|
|
780
794
|
);
|
|
781
795
|
}
|
|
782
796
|
|
|
@@ -806,7 +820,7 @@ export class WorkspaceConfig {
|
|
|
806
820
|
}
|
|
807
821
|
|
|
808
822
|
/**
|
|
809
|
-
* Update
|
|
823
|
+
* Update the app's microservice.port on env.app (v3 single-app).
|
|
810
824
|
*
|
|
811
825
|
* Operates on the entry's microservice.{} slot.
|
|
812
826
|
* This is the canonical write path for the microservice port that `descix microservice init`
|
|
@@ -815,9 +829,9 @@ export class WorkspaceConfig {
|
|
|
815
829
|
*
|
|
816
830
|
* Pass `null` to remove microservice.port; if microservice.{} becomes empty, it is also deleted.
|
|
817
831
|
* Persists via save() at the end — same auto-save pattern as setEnvironment().
|
|
818
|
-
* Hard-fails if appId is not
|
|
832
|
+
* Hard-fails if appId is not THE app (env.app).
|
|
819
833
|
*
|
|
820
|
-
* @param {string} appId - App identifier (must
|
|
834
|
+
* @param {string} appId - App identifier (must be env.app.appId)
|
|
821
835
|
* @param {number|string|null} port - Port number to set, or null to remove microservice.port
|
|
822
836
|
* @returns {Promise<string>} Path to saved config (from save())
|
|
823
837
|
*/
|
|
@@ -843,7 +857,7 @@ export class WorkspaceConfig {
|
|
|
843
857
|
}
|
|
844
858
|
|
|
845
859
|
/**
|
|
846
|
-
* Update
|
|
860
|
+
* Update the app's site config on env.app (v3 single-app).
|
|
847
861
|
*
|
|
848
862
|
* Parallel to setMicroservicePort(), but operates on the entry's site.{} slot. This is the
|
|
849
863
|
* canonical write path for site.static — the relative path the dev gateway's
|
|
@@ -867,9 +881,9 @@ export class WorkspaceConfig {
|
|
|
867
881
|
* Pass null for a field to remove it (what `--unset` does for all three). Removals clear
|
|
868
882
|
* only the named field. If site.{} becomes empty after removals it is deleted.
|
|
869
883
|
* Persists via save() at the end — same auto-save pattern as setMicroservicePort().
|
|
870
|
-
* Hard-fails if appId is not
|
|
884
|
+
* Hard-fails if appId is not THE app (env.app).
|
|
871
885
|
*
|
|
872
|
-
* @param {string} appId - App identifier (must
|
|
886
|
+
* @param {string} appId - App identifier (must be env.app.appId)
|
|
873
887
|
* @param {Object} fields - Fields to set on site.{}
|
|
874
888
|
* @param {string|null} [fields.static] - Relative static-site path to set, or null to remove site.static
|
|
875
889
|
* @param {number|string|null} [fields.port] - Site dev-server port to set, or null to remove site.port
|
|
@@ -1006,9 +1020,9 @@ export class WorkspaceConfig {
|
|
|
1006
1020
|
validate() {
|
|
1007
1021
|
const errors = [];
|
|
1008
1022
|
|
|
1009
|
-
//
|
|
1023
|
+
// v3 validation: must have env block
|
|
1010
1024
|
if (!this.env || Object.keys(this.env).length === 0) {
|
|
1011
|
-
errors.push('Missing env block — workspace.json must be
|
|
1025
|
+
errors.push('Missing env block — workspace.json must be v3 format with env.app');
|
|
1012
1026
|
}
|
|
1013
1027
|
|
|
1014
1028
|
return {
|
|
@@ -24,10 +24,9 @@ import { resolveOrigin } from './origin.js';
|
|
|
24
24
|
/**
|
|
25
25
|
* Read the workspace's identity from a parsed `.descix/workspace.json`.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* wrong again, so the two are commented as a pair.
|
|
27
|
+
* v3 single-app: the one key the writer (`WorkspaceConfig.registerApp()`) writes is `env.app`,
|
|
28
|
+
* so that is the one key read here — writer and reader are a pair. The legacy `defaultContext`
|
|
29
|
+
* survives as the last resort for files written before the env block existed.
|
|
31
30
|
*
|
|
32
31
|
* @param {object} config - the PARSED workspace.json (not a WorkspaceConfig instance, so that
|
|
33
32
|
* `agent-files.js` can stay usable from the VS Code extension where it
|
|
@@ -37,22 +36,16 @@ import { resolveOrigin } from './origin.js';
|
|
|
37
36
|
*/
|
|
38
37
|
export function readIdentity(config = {}) {
|
|
39
38
|
const env = config.env || {};
|
|
40
|
-
const
|
|
41
|
-
const products = Array.isArray(env.products) ? env.products : [];
|
|
42
|
-
// The first registered product is the workspace's app when there is no platform app. A
|
|
43
|
-
// workspace with several products has no single identity, and `init` only ever writes one.
|
|
44
|
-
const product = products[0] || {};
|
|
39
|
+
const app = env.app || {};
|
|
45
40
|
const legacy = config.defaultContext || {};
|
|
46
41
|
|
|
47
42
|
const appId =
|
|
48
|
-
|
|
49
|
-
product.appId ||
|
|
43
|
+
app.appId ||
|
|
50
44
|
legacy.appId ||
|
|
51
45
|
null;
|
|
52
46
|
|
|
53
47
|
const communityId =
|
|
54
|
-
|
|
55
|
-
product.communityId ||
|
|
48
|
+
app.communityId ||
|
|
56
49
|
legacy.communityId ||
|
|
57
50
|
config.primaryCommunity ||
|
|
58
51
|
null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descix/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "DeSciX",
|
|
31
31
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@descix/app-sdk": "^0.
|
|
33
|
+
"@descix/app-sdk": "^0.3.0",
|
|
34
34
|
"@descix/sdk": "^3.0.0",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
36
36
|
"axios": "^1.7.9",
|