@descix/cli 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/descix.js +14 -5
- package/lib/commands/appInit.js +39 -5
- package/lib/verb-invokes.generated.json +2 -0
- package/lib/workspace-config.js +24 -6
- package/package.json +1 -1
package/bin/descix.js
CHANGED
|
@@ -2411,11 +2411,20 @@ siteCommand
|
|
|
2411
2411
|
filesToUpload.forEach(f => console.log(chalk.gray(` + ${f.path} (${(f.size / 1024).toFixed(1)}KB)`)));
|
|
2412
2412
|
// The server's last deploy manifest names what it holds; the confirm step lists the
|
|
2413
2413
|
// storage prefix itself, so this preview is the manifest's view of what would go.
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2414
|
+
// THE REMOVE SET IS ALWAYS STATED, including empty (regression from 1.0.20 measured
|
|
2415
|
+
// 2026-10-10: the silent branch read as "didn't check", and an absence claim the
|
|
2416
|
+
// instrument cannot make — no server manifest — must say THAT, not imply none).
|
|
2417
|
+
if (existing_manifest?.files) {
|
|
2418
|
+
const lastDeployed = Object.keys(existing_manifest.files);
|
|
2419
|
+
const wouldRemove = lastDeployed.filter(p => !(p in localFiles)).sort();
|
|
2420
|
+
if (wouldRemove.length > 0) {
|
|
2421
|
+
console.log(chalk.yellow('\n Would remove from the server (in the last deploy, not in this build):'));
|
|
2422
|
+
wouldRemove.forEach(f => console.log(chalk.gray(` - ${f}`)));
|
|
2423
|
+
} else {
|
|
2424
|
+
console.log(chalk.gray('\n Would remove: none (every file of the last deploy is in this build)'));
|
|
2425
|
+
}
|
|
2426
|
+
} else {
|
|
2427
|
+
console.log(chalk.gray('\n Would remove: unknown — the server returned no previous deploy manifest (first deploy, or manifest unreadable)'));
|
|
2419
2428
|
}
|
|
2420
2429
|
console.log(chalk.gray(`\n Site URL: ${site_url}\n`));
|
|
2421
2430
|
return;
|
package/lib/commands/appInit.js
CHANGED
|
@@ -23,7 +23,8 @@ import { generateAgentFiles } from '../agent-files.js';
|
|
|
23
23
|
*/
|
|
24
24
|
export async function runAppInit(apiClient, options) {
|
|
25
25
|
let appId = options.app;
|
|
26
|
-
|
|
26
|
+
let kbId = options.kb || 'General';
|
|
27
|
+
let createdHere = false;
|
|
27
28
|
|
|
28
29
|
// v3 single-app: THIS directory becomes (or already is) the workspace root. A workspace at an
|
|
29
30
|
// ANCESTOR is a refusal, loud, BEFORE anything is written anywhere — the walk-up used to
|
|
@@ -59,6 +60,7 @@ export async function runAppInit(apiClient, options) {
|
|
|
59
60
|
const created = response.message || response;
|
|
60
61
|
appId = created.app_id || appId;
|
|
61
62
|
communityId = created.community_id || options.community;
|
|
63
|
+
createdHere = true;
|
|
62
64
|
console.log(chalk.green(`\n✅ App created: ${appId}`));
|
|
63
65
|
if (appId !== options.app) {
|
|
64
66
|
console.log(chalk.gray(` The platform composed the id from the community and the name you gave.`));
|
|
@@ -71,6 +73,35 @@ export async function runAppInit(apiClient, options) {
|
|
|
71
73
|
);
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
// MAPPING AN EXISTING APP ADOPTS ITS KNOWLEDGE BASES AND NEVER CREATES ONE (measured
|
|
77
|
+
// 2026-10-10: mapping `egpt` without --kb created an empty `General` on PROD beside its
|
|
78
|
+
// real `Corpus`). One existing KB → adopted; several → --kb must name one of THEM; a named
|
|
79
|
+
// KB that does not exist is a refusal, never a create. Only the CREATE leg above may create,
|
|
80
|
+
// because only a brand-new app has nothing to adopt.
|
|
81
|
+
if (!createdHere) {
|
|
82
|
+
const kbResp = await apiClient.invoke('list_knowledge_bases', { community_id: communityId, app_id: appId });
|
|
83
|
+
const existingKbs = ((kbResp.message || kbResp).knowledge_bases || []).map((kb) => kb.name);
|
|
84
|
+
if (options.kb) {
|
|
85
|
+
if (!existingKbs.includes(options.kb)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`App '${appId}' has no KB named '${options.kb}' — it has: ${existingKbs.join(', ') || '(none)'}.\n` +
|
|
88
|
+
`Mapping an existing app never creates a KB; name one that exists with --kb, or create ` +
|
|
89
|
+
`one deliberately with its own verb.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
kbId = options.kb;
|
|
93
|
+
} else if (existingKbs.length === 1) {
|
|
94
|
+
kbId = existingKbs[0];
|
|
95
|
+
} else if (existingKbs.length > 1) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`App '${appId}' has ${existingKbs.length} knowledge bases: ${existingKbs.join(', ')}.\n` +
|
|
98
|
+
`Name the one this repo syncs with --kb <name>.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
// zero KBs: every app is guaranteed a default KB at creation, so an empty list is a
|
|
102
|
+
// platform-side anomaly — keep the declared kbId and let the sync surface report it.
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
// 1. Workspace.json — THIS repo root is the workspace root and holds exactly this app (v3).
|
|
75
106
|
// registerApp is idempotent for the same id and REFUSES a different one: a repo that already
|
|
76
107
|
// holds another app errors loudly instead of being silently re-pointed.
|
|
@@ -100,9 +131,12 @@ export async function runAppInit(apiClient, options) {
|
|
|
100
131
|
}
|
|
101
132
|
console.log(chalk.gray(` Created: site/, microservice/, assets/`));
|
|
102
133
|
|
|
103
|
-
// 3.
|
|
104
|
-
|
|
105
|
-
|
|
134
|
+
// 3. The default KB — CREATE leg only (see the adopt block above: map never creates).
|
|
135
|
+
let kbResult = null;
|
|
136
|
+
if (createdHere) {
|
|
137
|
+
const kbResponse = await apiClient.invoke('init_git_mode_kb', { app_id: appId, kb_name: kbId });
|
|
138
|
+
kbResult = kbResponse.message || kbResponse;
|
|
139
|
+
}
|
|
106
140
|
|
|
107
141
|
console.log(chalk.green(`\n✓ ${appId} initialized`));
|
|
108
142
|
console.log(chalk.gray(` Community: ${communityId}`));
|
|
@@ -119,7 +153,7 @@ export async function runAppInit(apiClient, options) {
|
|
|
119
153
|
console.log(chalk.gray(` Retry with: descix community refresh-identity -c ${communityId}`));
|
|
120
154
|
}
|
|
121
155
|
}
|
|
122
|
-
console.log(chalk.gray(` KB: ${kbId} — ${kbResult.created ? 'created' : 'already exists'}\n`));
|
|
156
|
+
console.log(chalk.gray(` KB: ${kbId} — ${kbResult ? (kbResult.created ? 'created' : 'already exists') : 'adopted (existing app; map never creates)'}\n`));
|
|
123
157
|
console.log(chalk.gray(` (every app is guaranteed a default KB at creation; an empty one`));
|
|
124
158
|
console.log(chalk.gray(` says so rather than answering from general knowledge)\n`));
|
|
125
159
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"create_app_for_community",
|
|
13
13
|
"get_product_context",
|
|
14
14
|
"init_git_mode_kb",
|
|
15
|
+
"list_knowledge_bases",
|
|
15
16
|
"update_community_metadata"
|
|
16
17
|
],
|
|
17
18
|
"app invite": [
|
|
@@ -189,6 +190,7 @@
|
|
|
189
190
|
"find_communities",
|
|
190
191
|
"get_product_context",
|
|
191
192
|
"init_git_mode_kb",
|
|
193
|
+
"list_knowledge_bases",
|
|
192
194
|
"resolve_invite",
|
|
193
195
|
"sync_auto_purchases",
|
|
194
196
|
"update_community_metadata"
|
package/lib/workspace-config.js
CHANGED
|
@@ -194,6 +194,22 @@ export class WorkspaceUnreadableError extends Error {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
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
|
+
|
|
197
213
|
/**
|
|
198
214
|
* WorkspaceConfig - Manages workspace-specific configuration
|
|
199
215
|
*
|
|
@@ -407,11 +423,11 @@ export class WorkspaceConfig {
|
|
|
407
423
|
// `error.message.includes('v1 workspace format')` re-throw was narrow string matching
|
|
408
424
|
// (DeSciX anti-pattern #6) guarding a catch that should never have been this wide.
|
|
409
425
|
if (parsed.communities && !parsed.env) {
|
|
410
|
-
throw new
|
|
411
|
-
'v1 workspace format is not supported. Migrate to
|
|
426
|
+
throw new WorkspaceRetiredFormatError(
|
|
427
|
+
'v1 workspace format is not supported. Migrate to v3.\n' +
|
|
412
428
|
` File: ${configPath}\n` +
|
|
413
429
|
'\n' +
|
|
414
|
-
'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' +
|
|
415
431
|
'migration: no surviving code reads the v1 layout, so its contents cannot be converted\n' +
|
|
416
432
|
'for you — but they are still readable BY YOU, and this file is the only copy of them.\n' +
|
|
417
433
|
moveAsideRemedy(configPath, 'descix app init')
|
|
@@ -424,7 +440,7 @@ export class WorkspaceConfig {
|
|
|
424
440
|
// multi-app dev is sibling checkouts behind one gateway config. Same posture as the v1
|
|
425
441
|
// refusal above: loud, non-destructive, the remedy is one command per app repo.
|
|
426
442
|
if (parsed.env && (parsed.env.platform || parsed.env.products)) {
|
|
427
|
-
throw new
|
|
443
|
+
throw new WorkspaceRetiredFormatError(
|
|
428
444
|
'v2.1 multi-app workspace format is not supported. Migrate to v3 (single-app).\n' +
|
|
429
445
|
` File: ${configPath}\n` +
|
|
430
446
|
'\n' +
|
|
@@ -464,8 +480,9 @@ export class WorkspaceConfig {
|
|
|
464
480
|
* Try to load workspace configuration, return null if not found
|
|
465
481
|
* Useful for commands that need to check if workspace exists
|
|
466
482
|
*
|
|
467
|
-
* RETURNS NULL FOR ABSENT, THROWS FOR UNREADABLE. "There is no
|
|
468
|
-
* 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.
|
|
469
486
|
* Swallowing the second made a corrupt workspace look like a fresh directory, which is how a
|
|
470
487
|
* damaged workspace silently fell back to the DEFAULT (prod) origin instead of stopping.
|
|
471
488
|
*
|
|
@@ -478,6 +495,7 @@ export class WorkspaceConfig {
|
|
|
478
495
|
return await WorkspaceConfig.load(startDir);
|
|
479
496
|
} catch (error) {
|
|
480
497
|
if (error instanceof WorkspaceUnreadableError) throw error;
|
|
498
|
+
if (error instanceof WorkspaceRetiredFormatError) throw error;
|
|
481
499
|
return null;
|
|
482
500
|
}
|
|
483
501
|
}
|