@descix/cli 1.0.1 → 1.0.3
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/LICENSE.md +671 -0
- package/README.md +43 -4
- package/agent-assets/guides/README.md +2 -2
- package/agent-assets/guides/dynamic-site-pattern.md +29 -14
- package/agent-assets/guides/knowledge-only-pattern.md +5 -5
- package/agent-assets/guides/microservice-pattern.md +48 -4
- package/agent-assets/guides/static-site-pattern.md +8 -0
- package/agent-assets/instructions/app-asset-validation.md +5 -5
- package/agent-assets/instructions/onboarding-flow.md +4 -4
- package/agent-assets/instructions/standard-folder-structure.md +20 -11
- package/agent-assets/reference/app-creation.md +42 -20
- package/agent-assets/reference/architecture.md +24 -22
- package/agent-assets/reference/local-dev.md +230 -67
- package/agent-assets/reference/mcp-integration.md +9 -9
- package/agent-assets/reference/platform-api-consumer.md +244 -0
- package/agent-assets/reference/workspace-config.md +7 -7
- package/agent-assets/rules/descix_mcp.template.mdc +8 -8
- package/bin/descix.js +2559 -1435
- package/bin/kb-deny-lint.mjs +91 -0
- package/bin/mcp-server.js +175 -91
- package/lib/agent-files.js +37 -25
- package/lib/api-client.js +223 -56
- package/lib/auth-guard.js +13 -3
- package/lib/chat-session.js +187 -0
- package/lib/cli-version.js +59 -0
- package/lib/commands/airdrop.js +251 -0
- package/lib/commands/auth.js +254 -72
- package/lib/commands/briefer/errors.js +88 -0
- package/lib/commands/briefer/index.js +319 -0
- package/lib/commands/briefer/sources/canonical-sources.js +129 -0
- package/lib/commands/briefer/sources/entitlements.js +209 -0
- package/lib/commands/briefer/sources/environments.js +205 -0
- package/lib/commands/briefer/sources/identifiers.js +214 -0
- package/lib/commands/briefer/sources/microservice-deploy.js +267 -0
- package/lib/commands/briefer/sources/routing.js +275 -0
- package/lib/commands/briefer/sources/what-is-not.js +223 -0
- package/lib/commands/briefer/stitcher.js +134 -0
- package/lib/commands/briefer/util/source-reader.js +502 -0
- package/lib/commands/config.js +210 -90
- package/lib/commands/corpus.js +978 -0
- package/lib/commands/credits.js +122 -0
- package/lib/commands/health.js +627 -0
- package/lib/commands/kb-list-render.js +32 -0
- package/lib/commands/kb.js +190 -413
- package/lib/commands/mcp.js +5 -5
- package/lib/commands/microservice-restart.js +359 -0
- package/lib/commands/model-config.js +313 -0
- package/lib/commands/retired-kb-sync.js +134 -0
- package/lib/commands/serve.js +26 -1
- package/lib/commands/status.js +23 -51
- package/lib/commands/update.js +36 -107
- package/lib/core/Chunker.js +491 -17
- package/lib/core/CorpusDenyLint.js +326 -0
- package/lib/core/CorpusWalker.js +357 -0
- package/lib/core/Hydrator.js +75 -29
- package/lib/core/ManifestLoader.js +533 -0
- package/lib/core/SiteWalker.js +218 -0
- package/lib/core/Syncer.js +163 -11
- package/lib/diagnostic-log.js +41 -0
- package/lib/environment-report.js +100 -0
- package/lib/global-config.js +4 -1
- package/lib/oauth-client.js +164 -0
- package/lib/origin.js +143 -0
- package/lib/service-api-client.js +95 -0
- package/lib/wallet-environment.js +49 -0
- package/lib/wallet-file.js +16 -2
- package/lib/wizard/setup.js +10 -9
- package/lib/workspace-config.js +380 -235
- package/lib/workspace-identity.js +92 -0
- package/lib/workspace-utils.js +41 -61
- package/package.json +19 -6
- package/templates/agent-claude.md +157 -9
- package/templates/agent-cline.md +68 -8
- package/templates/agent-copilot.md +68 -8
- package/templates/agent-cursor.md +68 -8
- package/templates/alignment-schema.json +20 -0
- package/templates/cloud-brain-hook.sh +20 -0
- package/templates/drive/README.md +1 -1
- package/templates/drive_templates/README.md +2 -2
- package/templates/scaffolds/README.md +6 -10
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +30 -0
- package/templates/scaffolds/microservice/app.js +51 -10
- package/templates/scaffolds/microservice/app.yaml +2 -2
- package/templates/scaffolds/microservice/defaults-config-dev.json +4 -0
- package/templates/scaffolds/microservice/dev-overrides.example.json +5 -0
- package/templates/scaffolds/microservice/manifest.json +0 -1
- package/templates/scaffolds/microservice/package.json +3 -3
- package/templates/scaffolds/microservice/scripts/register.js +56 -41
- package/templates/scaffolds/microservice/services/mcpClient.js +55 -65
- package/templates/scaffolds/microservice/services/utils.js +20 -238
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +24 -0
- package/lib/commands/app-wizard.js +0 -627
- package/lib/commands/folder.js +0 -291
- package/lib/core/PathContext.js +0 -335
- package/templates/scaffolds/microservice/dev-overrides.json.example +0 -5
- package/templates/scaffolds/site/DeSciXAppSDK.js +0 -62
- package/templates/scaffolds/site/README.md +0 -51
- package/templates/scaffolds/site/app.js +0 -22
- package/templates/scaffolds/site/index.html +0 -42
- package/templates/scaffolds/site/styles.css +0 -123
package/lib/workspace-config.js
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { ENV_ORIGINS } from '@descix/app-sdk/dev';
|
|
4
|
+
import { resolveOrigin } from './origin.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ONE OWNER for turning a workspace-relative localPath into an absolute path.
|
|
8
|
+
*
|
|
9
|
+
* This was resolved four different ways across this file and its callers — three with
|
|
10
|
+
* path.join and one with path.resolve — and the two treatments DISAGREE on an absolute
|
|
11
|
+
* localPath. path.join('/ws', '/ws/app') yields '/ws/ws/app' (the "doubling"); path.resolve
|
|
12
|
+
* yields '/ws/app'. Concretely, getAppByAppId() and detectContext() both derive from the same
|
|
13
|
+
* _appIdToConfig entry, so an absolute localPath made detectContext match a directory that
|
|
14
|
+
* getAppByAppId reported as a doubled path that does not exist.
|
|
15
|
+
*
|
|
16
|
+
* FAIL LOUD rather than quietly absorbing it. An absolute localPath in workspace.json is a
|
|
17
|
+
* PORTABILITY defect, not a valid configuration: workspace.json is shared across checkouts and
|
|
18
|
+
* machines, and a machine-specific absolute path cannot survive either. Silently resolving it
|
|
19
|
+
* would make the doubling disappear while leaving the unshareable config in place. Name the
|
|
20
|
+
* offending entry and the config verb that fixes it.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveWorkspacePath(workspaceRoot, localPath, label = 'app') {
|
|
23
|
+
if (!localPath) return null;
|
|
24
|
+
if (path.isAbsolute(localPath)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`workspace.json: localPath for '${label}' is an absolute path (${localPath}). ` +
|
|
27
|
+
`localPath must be relative to the workspace root so the workspace is portable across ` +
|
|
28
|
+
`checkouts and machines. Fix it with the config verb — never by hand-editing workspace.json.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return path.resolve(workspaceRoot, localPath);
|
|
32
|
+
}
|
|
3
33
|
|
|
4
34
|
/**
|
|
5
35
|
* WorkspaceConfig - Manages workspace-specific configuration
|
|
@@ -18,16 +48,11 @@ export class WorkspaceConfig {
|
|
|
18
48
|
this.version = config.version || '1.0';
|
|
19
49
|
this.type = config.type || 'workspace'; // 'workspace', 'community', or 'app'
|
|
20
50
|
|
|
21
|
-
// Legacy fields (
|
|
22
|
-
this.primaryCommunity = config.primaryCommunity;
|
|
23
|
-
this.directoryMappings = config.directoryMappings || {};
|
|
24
|
-
this.additionalContexts = config.additionalContexts || [];
|
|
51
|
+
// Legacy fields (kept for save() PWA response conversion compatibility)
|
|
25
52
|
this.defaultContext = config.defaultContext;
|
|
26
53
|
this.apiUrl = config.apiUrl;
|
|
27
54
|
this.environment = config.environment;
|
|
28
|
-
|
|
29
|
-
// V2 community tracking (root workspace)
|
|
30
|
-
this.communities = config.communities || {};
|
|
55
|
+
this.directoryMappings = config.directoryMappings || {};
|
|
31
56
|
|
|
32
57
|
// V2.1 Product tracking (Unified Registry)
|
|
33
58
|
this.products = config.products || {};
|
|
@@ -89,7 +114,7 @@ export class WorkspaceConfig {
|
|
|
89
114
|
// env.platform + env.products (primary for manually crafted workspace)
|
|
90
115
|
const fromEnv = this._appIdToConfig[appId];
|
|
91
116
|
if (fromEnv) {
|
|
92
|
-
const absPath =
|
|
117
|
+
const absPath = resolveWorkspacePath(this.workspaceRoot, fromEnv.localPath, appId);
|
|
93
118
|
return {
|
|
94
119
|
localPath: fromEnv.localPath,
|
|
95
120
|
absolutePath: absPath,
|
|
@@ -98,33 +123,29 @@ export class WorkspaceConfig {
|
|
|
98
123
|
};
|
|
99
124
|
}
|
|
100
125
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (product?.localPath) {
|
|
104
|
-
const absPath = product.absolutePath || path.join(this.workspaceRoot, product.localPath);
|
|
105
|
-
return {
|
|
106
|
-
localPath: product.localPath,
|
|
107
|
-
absolutePath: absPath,
|
|
108
|
-
communityId: product.context?.community || null,
|
|
109
|
-
kbId: product.kbId || 'General'
|
|
110
|
-
};
|
|
111
|
-
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
112
128
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
129
|
+
/**
|
|
130
|
+
* Get the absolute path to an app's site/ directory
|
|
131
|
+
* @param {string} appId - App identifier
|
|
132
|
+
* @returns {string|null} Absolute path to site/ or null if app not mapped
|
|
133
|
+
*/
|
|
134
|
+
getSitePath(appId) {
|
|
135
|
+
const appConfig = this.getAppByAppId(appId);
|
|
136
|
+
if (!appConfig?.absolutePath) return null;
|
|
137
|
+
return path.join(appConfig.absolutePath, 'site');
|
|
138
|
+
}
|
|
126
139
|
|
|
127
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Get the absolute path to an app's microservice/ directory
|
|
142
|
+
* @param {string} appId - App identifier
|
|
143
|
+
* @returns {string|null} Absolute path to microservice/ or null if app not mapped
|
|
144
|
+
*/
|
|
145
|
+
getMicroservicePath(appId) {
|
|
146
|
+
const appConfig = this.getAppByAppId(appId);
|
|
147
|
+
if (!appConfig?.absolutePath) return null;
|
|
148
|
+
return path.join(appConfig.absolutePath, 'microservice');
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
/**
|
|
@@ -178,24 +199,30 @@ export class WorkspaceConfig {
|
|
|
178
199
|
*/
|
|
179
200
|
static async load(startDir = process.cwd()) {
|
|
180
201
|
// First, find workspace root by searching upward
|
|
181
|
-
// If startDir is provided, use it. If not, use process.cwd()
|
|
182
202
|
const workspaceRoot = await WorkspaceConfig.findWorkspaceRoot(startDir);
|
|
183
203
|
if (!workspaceRoot) {
|
|
184
|
-
// Fallback: Check if we are running from within a package in the repo
|
|
185
|
-
// and try to find the root by looking for .descix in parent directories
|
|
186
|
-
// This is redundant with findWorkspaceRoot but let's be explicit about the error
|
|
187
204
|
throw new Error(
|
|
188
205
|
'Workspace not configured.\n' +
|
|
189
206
|
'Run "npx descix init" first to initialize your workspace.'
|
|
190
207
|
);
|
|
191
208
|
}
|
|
192
|
-
|
|
209
|
+
|
|
193
210
|
const configPath = path.join(workspaceRoot, '.descix', 'workspace.json');
|
|
194
211
|
try {
|
|
195
212
|
const data = await fs.readFile(configPath, 'utf-8');
|
|
196
213
|
const parsed = JSON.parse(data);
|
|
214
|
+
|
|
215
|
+
// v1 format hard-error: has communities block but no env block
|
|
216
|
+
if (parsed.communities && !parsed.env) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
'v1 workspace format is not supported. Migrate to v2.1.\n' +
|
|
219
|
+
'Delete .descix/workspace.json and re-run "descix app init" to create a v2.1 workspace.'
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
197
223
|
return new WorkspaceConfig(parsed, workspaceRoot);
|
|
198
224
|
} catch (error) {
|
|
225
|
+
if (error.message.includes('v1 workspace format')) throw error;
|
|
199
226
|
throw new Error(
|
|
200
227
|
'Workspace not configured.\n' +
|
|
201
228
|
'Run "npx descix init" first to initialize your workspace.'
|
|
@@ -220,9 +247,7 @@ export class WorkspaceConfig {
|
|
|
220
247
|
|
|
221
248
|
/**
|
|
222
249
|
* Save workspace configuration in v2.1 format.
|
|
223
|
-
* Always writes env.platform / env.products structure
|
|
224
|
-
* If loaded from a v1 communities format (e.g. PWA device_check_status response),
|
|
225
|
-
* auto-converts communities.apps → env.products on write.
|
|
250
|
+
* Always writes env.platform / env.products structure.
|
|
226
251
|
* @param {string} [workspaceRoot] - Workspace root directory (uses stored root if not provided)
|
|
227
252
|
* @returns {Promise<string>} Path to saved config
|
|
228
253
|
*/
|
|
@@ -238,25 +263,8 @@ export class WorkspaceConfig {
|
|
|
238
263
|
|
|
239
264
|
this.workspaceRoot = root;
|
|
240
265
|
|
|
241
|
-
// Build env block for v2.1 output
|
|
242
|
-
|
|
243
|
-
// Legacy conversion path: this.env is absent but communities are populated (v1 PWA response).
|
|
244
|
-
let envBlock = (this.env && Object.keys(this.env).length > 0) ? this.env : null;
|
|
245
|
-
|
|
246
|
-
if (!envBlock && Object.keys(this.communities || {}).length > 0) {
|
|
247
|
-
const products = [];
|
|
248
|
-
for (const [, community] of Object.entries(this.communities)) {
|
|
249
|
-
for (const [appId, app] of Object.entries(community.apps || {})) {
|
|
250
|
-
if (app.localPath) {
|
|
251
|
-
products.push({ appId, localPath: app.localPath, kbId: app.kbId || 'General' });
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
envBlock = { products };
|
|
256
|
-
// Update in-memory state so callers see a consistent env after save
|
|
257
|
-
this.env = envBlock;
|
|
258
|
-
this._appIdToConfig = this._buildAppIdMap({ env: this.env, workspaceRoot: this.workspaceRoot });
|
|
259
|
-
}
|
|
266
|
+
// Build env block for v2.1 output
|
|
267
|
+
const envBlock = (this.env && Object.keys(this.env).length > 0) ? this.env : null;
|
|
260
268
|
|
|
261
269
|
const configData = {
|
|
262
270
|
version: '2.1',
|
|
@@ -279,46 +287,17 @@ export class WorkspaceConfig {
|
|
|
279
287
|
*/
|
|
280
288
|
computeAbsolutePaths(workspaceRoot) {
|
|
281
289
|
const absWorkspaceRoot = path.resolve(workspaceRoot);
|
|
282
|
-
|
|
283
|
-
for (const [communityId, community] of Object.entries(this.communities || {})) {
|
|
284
|
-
// Compute community absolute path
|
|
285
|
-
if (community.localPath) {
|
|
286
|
-
community.absolutePath = path.join(absWorkspaceRoot, community.localPath);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Compute app absolute paths
|
|
290
|
-
for (const [appId, app] of Object.entries(community.apps || {})) {
|
|
291
|
-
if (app.localPath) {
|
|
292
|
-
app.absolutePath = path.join(absWorkspaceRoot, app.localPath);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
290
|
|
|
297
291
|
// Compute absolute paths for products
|
|
298
292
|
for (const [productId, product] of Object.entries(this.products || {})) {
|
|
299
293
|
if (product.localPath) {
|
|
300
|
-
product.absolutePath =
|
|
294
|
+
product.absolutePath = resolveWorkspacePath(absWorkspaceRoot, product.localPath, productId);
|
|
301
295
|
}
|
|
302
296
|
}
|
|
303
297
|
}
|
|
304
298
|
|
|
305
|
-
// ============
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Register a community in the root workspace
|
|
309
|
-
* @param {string} communityId - Community identifier
|
|
310
|
-
* @param {Object} communityConfig - Community configuration
|
|
311
|
-
* @param {string} communityConfig.localPath - Local folder path
|
|
312
|
-
* @param {string} communityConfig.tokenSymbol - Token symbol
|
|
313
|
-
* @param {Object} [communityConfig.apps] - Apps in this community
|
|
314
|
-
* @returns {boolean} Success status
|
|
315
|
-
*/
|
|
316
|
-
registerCommunity(communityId, communityConfig) {
|
|
317
|
-
// V2: Community identity is server-authoritative (Products registry).
|
|
318
|
-
// workspace.json does not track communities. No-op.
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
|
|
299
|
+
// ============ App Registration Methods ============
|
|
300
|
+
|
|
322
301
|
/**
|
|
323
302
|
* Register an app in the v2.1 workspace (env.platform or env.products).
|
|
324
303
|
* community_id is server-authoritative — not stored in workspace.json.
|
|
@@ -337,11 +316,18 @@ export class WorkspaceConfig {
|
|
|
337
316
|
if (!this.env) this.env = {};
|
|
338
317
|
if (!Array.isArray(this.env.products)) this.env.products = [];
|
|
339
318
|
|
|
319
|
+
// communityId is STORED, not just validated. It was previously accepted as a parameter and
|
|
320
|
+
// silently discarded, so `descix init -c egpt` wrote no trace of `egpt` anywhere — and
|
|
321
|
+
// `workspace-identity.js` (this entry's reader) then had no community to report, which is
|
|
322
|
+
// how the generated agent files named nobody's community. Writer and reader are a pair:
|
|
323
|
+
// the key order in `readIdentity()` mirrors the shape written here.
|
|
340
324
|
const entry = { appId, localPath: appConfig.localPath, kbId: appConfig.kbId || 'General' };
|
|
325
|
+
if (communityId) entry.communityId = communityId;
|
|
341
326
|
|
|
342
327
|
if (this.env.platform?.appId === appId) {
|
|
343
328
|
this.env.platform.localPath = appConfig.localPath;
|
|
344
329
|
if (appConfig.kbId) this.env.platform.kbId = appConfig.kbId;
|
|
330
|
+
if (communityId) this.env.platform.communityId = communityId;
|
|
345
331
|
} else {
|
|
346
332
|
const idx = this.env.products.findIndex(p => p.appId === appId);
|
|
347
333
|
if (idx >= 0) {
|
|
@@ -356,85 +342,6 @@ export class WorkspaceConfig {
|
|
|
356
342
|
return true;
|
|
357
343
|
}
|
|
358
344
|
|
|
359
|
-
/**
|
|
360
|
-
* Get community configuration by ID
|
|
361
|
-
* @param {string} communityId - Community identifier
|
|
362
|
-
* @returns {Object|null} Community configuration or null
|
|
363
|
-
*/
|
|
364
|
-
getCommunity(communityId) {
|
|
365
|
-
return this.communities[communityId] || null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Get app configuration from a community
|
|
370
|
-
* @param {string} communityId - Community identifier
|
|
371
|
-
* @param {string} appId - App identifier
|
|
372
|
-
* @returns {Object|null} App configuration or null
|
|
373
|
-
*/
|
|
374
|
-
getApp(communityId, appId) {
|
|
375
|
-
const community = this.communities[communityId];
|
|
376
|
-
if (!community || !community.apps) return null;
|
|
377
|
-
return community.apps[appId] || null;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* List all registered communities
|
|
382
|
-
* @returns {Array} Array of community IDs
|
|
383
|
-
*/
|
|
384
|
-
listCommunities() {
|
|
385
|
-
return Object.keys(this.communities);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* List all apps in a community
|
|
390
|
-
* @param {string} communityId - Community identifier
|
|
391
|
-
* @returns {Array} Array of app IDs
|
|
392
|
-
*/
|
|
393
|
-
listApps(communityId) {
|
|
394
|
-
const community = this.communities[communityId];
|
|
395
|
-
if (!community || !community.apps) return [];
|
|
396
|
-
return Object.keys(community.apps);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Remove a community registration
|
|
401
|
-
* @param {string} communityId - Community identifier
|
|
402
|
-
* @returns {boolean} Success status
|
|
403
|
-
*/
|
|
404
|
-
unregisterCommunity(communityId) {
|
|
405
|
-
if (this.communities[communityId]) {
|
|
406
|
-
// Remove directory mappings for all apps
|
|
407
|
-
const apps = this.communities[communityId].apps || {};
|
|
408
|
-
for (const [appId, appConfig] of Object.entries(apps)) {
|
|
409
|
-
if (appConfig.localPath) {
|
|
410
|
-
delete this.directoryMappings[appConfig.localPath];
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
delete this.communities[communityId];
|
|
414
|
-
return true;
|
|
415
|
-
}
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Remove an app registration
|
|
421
|
-
* @param {string} communityId - Community identifier
|
|
422
|
-
* @param {string} appId - App identifier
|
|
423
|
-
* @returns {boolean} Success status
|
|
424
|
-
*/
|
|
425
|
-
unregisterApp(communityId, appId) {
|
|
426
|
-
const community = this.communities[communityId];
|
|
427
|
-
if (community && community.apps && community.apps[appId]) {
|
|
428
|
-
const appConfig = community.apps[appId];
|
|
429
|
-
if (appConfig.localPath) {
|
|
430
|
-
delete this.directoryMappings[appConfig.localPath];
|
|
431
|
-
}
|
|
432
|
-
delete community.apps[appId];
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
345
|
/**
|
|
439
346
|
* Register a product (Unified Registry)
|
|
440
347
|
* @param {string} productId - Global product identifier
|
|
@@ -535,7 +442,7 @@ export class WorkspaceConfig {
|
|
|
535
442
|
// env.platform + env.products (Unified Registry - primary)
|
|
536
443
|
if (wsRoot) {
|
|
537
444
|
for (const [appId, cfg] of Object.entries(this._appIdToConfig || {})) {
|
|
538
|
-
const appPath =
|
|
445
|
+
const appPath = resolveWorkspacePath(wsRoot, cfg.localPath, appId);
|
|
539
446
|
if (cwd.startsWith(appPath)) {
|
|
540
447
|
return {
|
|
541
448
|
appId,
|
|
@@ -546,28 +453,11 @@ export class WorkspaceConfig {
|
|
|
546
453
|
}
|
|
547
454
|
}
|
|
548
455
|
|
|
549
|
-
for (const [commId, comm] of Object.entries(this.communities || {})) {
|
|
550
|
-
for (const [appId, app] of Object.entries(comm.apps || {})) {
|
|
551
|
-
let appPath = app.absolutePath;
|
|
552
|
-
if (!appPath && app.localPath && wsRoot) {
|
|
553
|
-
appPath = path.join(wsRoot, app.localPath);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (appPath && cwd.startsWith(appPath)) {
|
|
557
|
-
return {
|
|
558
|
-
communityId: commId,
|
|
559
|
-
appId,
|
|
560
|
-
kbId: app.kbId || 'General'
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
456
|
// products object (legacy)
|
|
567
457
|
for (const [productId, product] of Object.entries(this.products || {})) {
|
|
568
458
|
let productPath = product.absolutePath;
|
|
569
459
|
if (!productPath && product.localPath && wsRoot) {
|
|
570
|
-
productPath =
|
|
460
|
+
productPath = resolveWorkspacePath(wsRoot, product.localPath, productId);
|
|
571
461
|
}
|
|
572
462
|
|
|
573
463
|
if (productPath && cwd.startsWith(productPath)) {
|
|
@@ -634,16 +524,303 @@ export class WorkspaceConfig {
|
|
|
634
524
|
}
|
|
635
525
|
|
|
636
526
|
/**
|
|
637
|
-
* Get API
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
527
|
+
* Get the API origin this workspace is configured for, or NULL when none is configured.
|
|
528
|
+
*
|
|
529
|
+
* Priority: env.apiUrl > legacy this.apiUrl. There is NO third branch. This used to end in
|
|
530
|
+
* `return DEFAULT_API_URL` — the shipped PROD origin — which meant an unconfigured workspace
|
|
531
|
+
* was indistinguishable from one that had deliberately chosen production, and every caller
|
|
532
|
+
* received a prod origin the developer had never picked. Resolution and the fail-loud live in
|
|
533
|
+
* `lib/origin.js`; this method only reports what THIS workspace file says.
|
|
534
|
+
*
|
|
535
|
+
* There is likewise no environment-name-to-localhost derivation: an environment names a cloud
|
|
536
|
+
* environment, and localhost is a URL you set explicitly
|
|
537
|
+
* (`descix config set-env dev --url https://localhost:4000`, or env.apiUrl).
|
|
538
|
+
*
|
|
539
|
+
* @returns {string|null} the configured origin, or null if this workspace names none
|
|
641
540
|
*/
|
|
642
541
|
getApiUrl() {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
542
|
+
// Consumes the ONE origin owner rather than re-deriving the precedence or re-spelling the
|
|
543
|
+
// declared default. Under (A') an unconfigured workspace resolves to the declared default
|
|
544
|
+
// PROD; the SOURCE that distinguishes "chose prod" from "chose nothing" is carried by
|
|
545
|
+
// resolveOrigin() and printed by the api-client, never by a null returned from here.
|
|
546
|
+
return resolveOrigin({
|
|
547
|
+
workspaceEnvApiUrl: this.env?.apiUrl,
|
|
548
|
+
legacyApiUrl: this.apiUrl,
|
|
549
|
+
}).origin;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Known environment URL mapping.
|
|
554
|
+
* Shared between `descix config set-env` and the `--env` global flag.
|
|
555
|
+
* Origins come from the ONE owner (@descix/app-sdk/dev envOrigins); this map
|
|
556
|
+
* adds only the CLI's own concern, the Secret Manager label.
|
|
557
|
+
* @type {Object.<string, {url: string, secretLabel: string}>}
|
|
558
|
+
*/
|
|
559
|
+
static ENV_MAP = {
|
|
560
|
+
dev: { url: ENV_ORIGINS.dev, secretLabel: 'DEBUG' },
|
|
561
|
+
demo: { url: ENV_ORIGINS.demo, secretLabel: 'DEMO' },
|
|
562
|
+
prod: { url: ENV_ORIGINS.prod, secretLabel: 'LIVE' },
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Set a WORKSPACE-LEVEL env key (env.gateway.port, env.devCerts, env.powchUrl,
|
|
567
|
+
* env.siteUrl) — the keys that describe the workspace itself rather than one app.
|
|
568
|
+
*
|
|
569
|
+
* These all previously had NO CLI verb, so the only way to set them was to hand-edit
|
|
570
|
+
* .descix/workspace.json (redteam G-6). Hand-editing a generated file is how the
|
|
571
|
+
* shape drifts and how a developer ends up debugging their own typo, which is the
|
|
572
|
+
* opposite of "it should all just work".
|
|
573
|
+
*
|
|
574
|
+
* Pass null to REMOVE a key; empty parent objects are cleaned up so the file never
|
|
575
|
+
* accumulates `"devCerts": {}` noise.
|
|
576
|
+
*
|
|
577
|
+
* @param {string} dottedKey - one of: gateway.port | devCerts.dir | devCerts.cert | devCerts.key | powchUrl | siteUrl
|
|
578
|
+
* @param {string|number|null} value
|
|
579
|
+
* @returns {Promise<string>} Path to saved config (from save())
|
|
580
|
+
*/
|
|
581
|
+
async setEnvKey(dottedKey, value) {
|
|
582
|
+
const ALLOWED = ['gateway.port', 'devCerts.dir', 'devCerts.cert', 'devCerts.key', 'powchUrl', 'siteUrl'];
|
|
583
|
+
if (!ALLOWED.includes(dottedKey)) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`"${dottedKey}" is not a workspace-level env key.\n` +
|
|
586
|
+
` Settable here: ${ALLOWED.join(', ')}\n` +
|
|
587
|
+
' Per-app keys have their own verbs: descix app set-site / set-port / set-localpath.'
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (!this.env) this.env = {};
|
|
592
|
+
const parts = dottedKey.split('.');
|
|
593
|
+
|
|
594
|
+
if (value === null || value === undefined) {
|
|
595
|
+
if (parts.length === 1) {
|
|
596
|
+
delete this.env[parts[0]];
|
|
597
|
+
} else {
|
|
598
|
+
const parent = this.env[parts[0]];
|
|
599
|
+
if (parent) {
|
|
600
|
+
delete parent[parts[1]];
|
|
601
|
+
if (Object.keys(parent).length === 0) delete this.env[parts[0]];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return this.save();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (parts.length === 1) {
|
|
608
|
+
this.env[parts[0]] = value;
|
|
609
|
+
} else {
|
|
610
|
+
if (!this.env[parts[0]]) this.env[parts[0]] = {};
|
|
611
|
+
this.env[parts[0]][parts[1]] = value;
|
|
612
|
+
}
|
|
613
|
+
return this.save();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Update an app's site.port in env.products[] (or env.platform if it is the platform app).
|
|
618
|
+
*
|
|
619
|
+
* Pass `null` to remove site.port; if site.{} becomes empty, site.{} is also deleted.
|
|
620
|
+
* Persists via save() at the end — same auto-save pattern as setEnvironment().
|
|
621
|
+
* Hard-fails if appId is not mapped in env.platform or env.products.
|
|
622
|
+
*
|
|
623
|
+
* @param {string} appId - App identifier (must exist in env.platform or env.products)
|
|
624
|
+
* @param {number|string|null} port - Port number to set, or null to remove site.port
|
|
625
|
+
* @returns {Promise<string>} Path to saved config (from save())
|
|
626
|
+
*/
|
|
627
|
+
async setSitePort(appId, port) {
|
|
628
|
+
if (!appId) throw new Error('appId is required');
|
|
629
|
+
|
|
630
|
+
// Find the live env entry (platform or products[]) — not a copy
|
|
631
|
+
let entry = null;
|
|
632
|
+
if (this.env?.platform?.appId === appId) {
|
|
633
|
+
entry = this.env.platform;
|
|
634
|
+
} else if (Array.isArray(this.env?.products)) {
|
|
635
|
+
entry = this.env.products.find(p => p.appId === appId) || null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (!entry) {
|
|
639
|
+
throw new Error(
|
|
640
|
+
`App "${appId}" is not mapped in workspace.json. ` +
|
|
641
|
+
'Use `descix app init` to register, or `descix app set-localpath -a <id> -p <path>` to repoint.'
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (port === null || port === undefined) {
|
|
646
|
+
// Remove site.port; clean up empty site.{}
|
|
647
|
+
if (entry.site) {
|
|
648
|
+
delete entry.site.port;
|
|
649
|
+
if (Object.keys(entry.site).length === 0) {
|
|
650
|
+
delete entry.site;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
if (!entry.site) entry.site = {};
|
|
655
|
+
entry.site.port = port;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return this.save();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Update an app's microservice.port in env.products[] (or env.platform if it is the platform app).
|
|
663
|
+
*
|
|
664
|
+
* Parallel to setSitePort(), but operates on the entry's microservice.{} slot.
|
|
665
|
+
* This is the canonical write path for the microservice port that `descix microservice init`
|
|
666
|
+
* reads (and hard-fails on if missing). Backs the `descix app set-port` command, closing
|
|
667
|
+
* WS-CLI-MESH-ROUTING-GAP without hand-editing workspace.json.
|
|
668
|
+
*
|
|
669
|
+
* Pass `null` to remove microservice.port; if microservice.{} becomes empty, it is also deleted.
|
|
670
|
+
* Persists via save() at the end — same auto-save pattern as setSitePort()/setEnvironment().
|
|
671
|
+
* Hard-fails if appId is not mapped in env.platform or env.products.
|
|
672
|
+
*
|
|
673
|
+
* @param {string} appId - App identifier (must exist in env.platform or env.products)
|
|
674
|
+
* @param {number|string|null} port - Port number to set, or null to remove microservice.port
|
|
675
|
+
* @returns {Promise<string>} Path to saved config (from save())
|
|
676
|
+
*/
|
|
677
|
+
async setMicroservicePort(appId, port) {
|
|
678
|
+
if (!appId) throw new Error('appId is required');
|
|
679
|
+
|
|
680
|
+
// Find the live env entry (platform or products[]) — not a copy
|
|
681
|
+
let entry = null;
|
|
682
|
+
if (this.env?.platform?.appId === appId) {
|
|
683
|
+
entry = this.env.platform;
|
|
684
|
+
} else if (Array.isArray(this.env?.products)) {
|
|
685
|
+
entry = this.env.products.find(p => p.appId === appId) || null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (!entry) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`App "${appId}" is not mapped in workspace.json. ` +
|
|
691
|
+
'Use `descix app init` to register, or `descix app set-localpath -a <id> -p <path>` to repoint.'
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (port === null || port === undefined) {
|
|
696
|
+
// Remove microservice.port; clean up empty microservice.{}
|
|
697
|
+
if (entry.microservice) {
|
|
698
|
+
delete entry.microservice.port;
|
|
699
|
+
if (Object.keys(entry.microservice).length === 0) {
|
|
700
|
+
delete entry.microservice;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
if (!entry.microservice) entry.microservice = {};
|
|
705
|
+
entry.microservice.port = port;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return this.save();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Update an app's site config in env.products[] (or env.platform if it is the platform app).
|
|
713
|
+
*
|
|
714
|
+
* Parallel to setSitePort()/setMicroservicePort(), but operates on the entry's site.{} slot's
|
|
715
|
+
* static-site fields. This is the canonical write path for site.static — the relative path the
|
|
716
|
+
* dev gateway's staticSitePlugin serves at /p/{appId}/ (see createViteProxyConfig:
|
|
717
|
+
* site.static is resolved against the app's localPath; "." means the localPath itself).
|
|
718
|
+
* Backs the `descix app set-site` command, closing the site.static workspace gap without
|
|
719
|
+
* hand-editing workspace.json (the org rule forbids hand edits — CEO-D-2026-06-02-SSGPOD-SITE-PREPROD).
|
|
720
|
+
*
|
|
721
|
+
* Mutates site.static and/or site.port. Pass static === null to remove site.static; pass
|
|
722
|
+
* port === null to remove site.port. If site.{} becomes empty after removals it is deleted.
|
|
723
|
+
* Persists via save() at the end — same auto-save pattern as setSitePort()/setMicroservicePort().
|
|
724
|
+
* Hard-fails if appId is not mapped in env.platform or env.products.
|
|
725
|
+
*
|
|
726
|
+
* @param {string} appId - App identifier (must exist in env.platform or env.products)
|
|
727
|
+
* @param {Object} fields - Fields to set on site.{}
|
|
728
|
+
* @param {string|null} [fields.static] - Relative static-site path to set, or null to remove site.static
|
|
729
|
+
* @param {number|string|null} [fields.port] - Site dev-server port to set, or null to remove site.port
|
|
730
|
+
* @returns {Promise<string>} Path to saved config (from save())
|
|
731
|
+
*/
|
|
732
|
+
async setStaticSite(appId, fields = {}) {
|
|
733
|
+
if (!appId) throw new Error('appId is required');
|
|
734
|
+
if (!fields || typeof fields !== 'object') throw new Error('fields object is required');
|
|
735
|
+
|
|
736
|
+
// Find the live env entry (platform or products[]) — not a copy
|
|
737
|
+
let entry = null;
|
|
738
|
+
if (this.env?.platform?.appId === appId) {
|
|
739
|
+
entry = this.env.platform;
|
|
740
|
+
} else if (Array.isArray(this.env?.products)) {
|
|
741
|
+
entry = this.env.products.find(p => p.appId === appId) || null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!entry) {
|
|
745
|
+
throw new Error(
|
|
746
|
+
`App "${appId}" is not mapped in workspace.json. ` +
|
|
747
|
+
'Use `descix app init` to register, or `descix app set-localpath -a <id> -p <path>` to repoint.'
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (!entry.site) entry.site = {};
|
|
752
|
+
|
|
753
|
+
// site.static — set or remove
|
|
754
|
+
if ('static' in fields) {
|
|
755
|
+
if (fields.static === null || fields.static === undefined) {
|
|
756
|
+
delete entry.site.static;
|
|
757
|
+
} else {
|
|
758
|
+
entry.site.static = fields.static;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// site.port — set or remove (parallel to setSitePort, for static+devCommand sites)
|
|
763
|
+
if ('port' in fields) {
|
|
764
|
+
if (fields.port === null || fields.port === undefined) {
|
|
765
|
+
delete entry.site.port;
|
|
766
|
+
} else {
|
|
767
|
+
entry.site.port = fields.port;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Clean up an empty site.{} so we never leave a bare {} behind
|
|
772
|
+
if (Object.keys(entry.site).length === 0) {
|
|
773
|
+
delete entry.site;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return this.save();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Persistently set the target environment in workspace.json.
|
|
781
|
+
*
|
|
782
|
+
* Updates env.environment and env.apiUrl, then saves. EVERY environment writes a
|
|
783
|
+
* URL — including dev, which writes the cloud DEV origin. Pointing at a local
|
|
784
|
+
* backend is `--url`: `descix config set-env dev --url https://localhost:4000`.
|
|
785
|
+
* For custom envs, uses --url or defaults to https://{name}.descix.net.
|
|
786
|
+
*
|
|
787
|
+
* @param {string} envName - Environment name (dev, demo, prod, or custom)
|
|
788
|
+
* @param {string|null} [apiUrl] - Explicit API URL override
|
|
789
|
+
* @returns {Promise<{configPath: string, environment: string, apiUrl: string, secretLabel: string}>}
|
|
790
|
+
*/
|
|
791
|
+
async setEnvironment(envName, apiUrl = null) {
|
|
792
|
+
const normalized = envName.toLowerCase();
|
|
793
|
+
const known = WorkspaceConfig.ENV_MAP[normalized];
|
|
794
|
+
|
|
795
|
+
let resolvedUrl;
|
|
796
|
+
let secretLabel;
|
|
797
|
+
let envLabel;
|
|
798
|
+
|
|
799
|
+
if (known) {
|
|
800
|
+
resolvedUrl = apiUrl || known.url; // explicit --url overrides even for known envs
|
|
801
|
+
secretLabel = known.secretLabel;
|
|
802
|
+
envLabel = normalized.toUpperCase();
|
|
803
|
+
} else {
|
|
804
|
+
resolvedUrl = apiUrl || `https://${normalized}.descix.net`;
|
|
805
|
+
secretLabel = normalized.toUpperCase();
|
|
806
|
+
envLabel = normalized.toUpperCase();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Update in-memory state
|
|
810
|
+
if (!this.env) this.env = {};
|
|
811
|
+
this.env.environment = envLabel;
|
|
812
|
+
|
|
813
|
+
this.env.apiUrl = resolvedUrl;
|
|
814
|
+
|
|
815
|
+
// Save to disk
|
|
816
|
+
const configPath = await this.save();
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
configPath,
|
|
820
|
+
environment: envLabel,
|
|
821
|
+
apiUrl: resolvedUrl,
|
|
822
|
+
secretLabel
|
|
823
|
+
};
|
|
647
824
|
}
|
|
648
825
|
|
|
649
826
|
/**
|
|
@@ -665,44 +842,12 @@ export class WorkspaceConfig {
|
|
|
665
842
|
*/
|
|
666
843
|
validate() {
|
|
667
844
|
const errors = [];
|
|
668
|
-
|
|
669
|
-
//
|
|
670
|
-
if (this.
|
|
671
|
-
|
|
672
|
-
if (community.apps) {
|
|
673
|
-
for (const [appId, app] of Object.entries(community.apps)) {
|
|
674
|
-
if (!app.localPath) {
|
|
675
|
-
errors.push(`Community "${communityId}" app "${appId}" missing localPath`);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
return {
|
|
681
|
-
isValid: errors.length === 0,
|
|
682
|
-
errors
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Legacy v1 validation
|
|
687
|
-
if (!this.primaryCommunity) {
|
|
688
|
-
errors.push('Missing primaryCommunity');
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (!this.defaultContext) {
|
|
692
|
-
errors.push('Missing defaultContext');
|
|
693
|
-
} else {
|
|
694
|
-
if (!this.defaultContext.communityId) errors.push('defaultContext missing communityId');
|
|
695
|
-
if (!this.defaultContext.appId) errors.push('defaultContext missing appId');
|
|
696
|
-
if (!this.defaultContext.kbId) errors.push('defaultContext missing kbId');
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// Validate directory mappings
|
|
700
|
-
for (const [dir, context] of Object.entries(this.directoryMappings)) {
|
|
701
|
-
if (!context.communityId) errors.push(`Directory "${dir}" mapping missing communityId`);
|
|
702
|
-
if (!context.appId) errors.push(`Directory "${dir}" mapping missing appId`);
|
|
703
|
-
if (!context.kbId) errors.push(`Directory "${dir}" mapping missing kbId`);
|
|
845
|
+
|
|
846
|
+
// v2.1 validation: must have env block
|
|
847
|
+
if (!this.env || Object.keys(this.env).length === 0) {
|
|
848
|
+
errors.push('Missing env block — workspace.json must be v2.1 format with env.platform or env.products');
|
|
704
849
|
}
|
|
705
|
-
|
|
850
|
+
|
|
706
851
|
return {
|
|
707
852
|
isValid: errors.length === 0,
|
|
708
853
|
errors
|