@descix/cli 1.0.0

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.
Files changed (94) hide show
  1. package/README.md +95 -0
  2. package/SERVICE_README_sdk.md +399 -0
  3. package/agent-assets/guides/README.md +61 -0
  4. package/agent-assets/guides/dynamic-site-pattern.md +125 -0
  5. package/agent-assets/guides/knowledge-only-pattern.md +93 -0
  6. package/agent-assets/guides/microservice-pattern.md +152 -0
  7. package/agent-assets/guides/static-site-pattern.md +111 -0
  8. package/agent-assets/instructions/app-asset-validation.md +477 -0
  9. package/agent-assets/instructions/onboarding-flow.md +408 -0
  10. package/agent-assets/instructions/standard-folder-structure.md +125 -0
  11. package/agent-assets/reference/app-creation.md +381 -0
  12. package/agent-assets/reference/architecture.md +406 -0
  13. package/agent-assets/reference/local-dev.md +138 -0
  14. package/agent-assets/reference/mcp-integration.md +424 -0
  15. package/agent-assets/reference/workspace-config.md +415 -0
  16. package/agent-assets/rules/descix_mcp.template.mdc +243 -0
  17. package/bin/descix.js +3930 -0
  18. package/bin/mcp-server.js +382 -0
  19. package/descix-cli-1.0.0.tgz +0 -0
  20. package/lib/agent-files.js +162 -0
  21. package/lib/api-client.js +458 -0
  22. package/lib/auth-guard.js +67 -0
  23. package/lib/commands/app-wizard.js +627 -0
  24. package/lib/commands/auth.js +407 -0
  25. package/lib/commands/buy.js +207 -0
  26. package/lib/commands/clone.js +91 -0
  27. package/lib/commands/config.js +155 -0
  28. package/lib/commands/doctor.js +277 -0
  29. package/lib/commands/folder.js +291 -0
  30. package/lib/commands/init.js +257 -0
  31. package/lib/commands/kb.js +654 -0
  32. package/lib/commands/mcp.js +273 -0
  33. package/lib/commands/serve.js +12 -0
  34. package/lib/commands/status.js +239 -0
  35. package/lib/commands/update.js +653 -0
  36. package/lib/core/Chunker.js +610 -0
  37. package/lib/core/Hydrator.js +801 -0
  38. package/lib/core/PathContext.js +335 -0
  39. package/lib/core/Syncer.js +310 -0
  40. package/lib/core/index.js +32 -0
  41. package/lib/global-config.js +86 -0
  42. package/lib/google-storage-adc.js +511 -0
  43. package/lib/wallet-file.js +232 -0
  44. package/lib/wizard/setup.js +502 -0
  45. package/lib/workspace-config.js +712 -0
  46. package/lib/workspace-utils.js +126 -0
  47. package/package.json +41 -0
  48. package/templates/agent-claude.md +226 -0
  49. package/templates/agent-cline.md +133 -0
  50. package/templates/agent-copilot.md +133 -0
  51. package/templates/agent-cursor.md +133 -0
  52. package/templates/descix_mcp.mdc +122 -0
  53. package/templates/drive/README.md +42 -0
  54. package/templates/drive/app/assets/app_description.md +13 -0
  55. package/templates/drive/app/assets/icon.png +0 -0
  56. package/templates/drive/app/assets/system_instructions.md +17 -0
  57. package/templates/drive/app/kb/General/README.md +25 -0
  58. package/templates/drive/community/Apps/.gitkeep +4 -0
  59. package/templates/drive/community/community_assets/community_description.md +15 -0
  60. package/templates/drive/community/community_assets/icon.png +0 -0
  61. package/templates/drive_templates/README.md +104 -0
  62. package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
  63. package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
  64. package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
  65. package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
  66. package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
  67. package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
  68. package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
  69. package/templates/scaffolds/README.md +62 -0
  70. package/templates/scaffolds/microservice/Dockerfile +19 -0
  71. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
  72. package/templates/scaffolds/microservice/app.js +50 -0
  73. package/templates/scaffolds/microservice/app.yaml +8 -0
  74. package/templates/scaffolds/microservice/defaults-config.json +5 -0
  75. package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
  76. package/templates/scaffolds/microservice/manifest.json +49 -0
  77. package/templates/scaffolds/microservice/package-lock.json +2645 -0
  78. package/templates/scaffolds/microservice/package.json +29 -0
  79. package/templates/scaffolds/microservice/scripts/register.js +55 -0
  80. package/templates/scaffolds/microservice/services/apiFront.js +75 -0
  81. package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
  82. package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
  83. package/templates/scaffolds/microservice/services/utils.js +247 -0
  84. package/templates/scaffolds/microservice/setup-schema.json +18 -0
  85. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
  86. package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
  87. package/templates/scaffolds/site/README.md +51 -0
  88. package/templates/scaffolds/site/app.js +22 -0
  89. package/templates/scaffolds/site/index.html +42 -0
  90. package/templates/scaffolds/site/styles.css +123 -0
  91. package/test-hydration.js +150 -0
  92. package/vendor/mcp/transports/http-sse-transport.js +196 -0
  93. package/vendor/mcp/transports/index.js +15 -0
  94. package/vendor/mcp/transports/stdio-transport.js +118 -0
@@ -0,0 +1,712 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+
4
+ /**
5
+ * WorkspaceConfig - Manages workspace-specific configuration
6
+ *
7
+ * This is the SOLE configuration methodology for DeSciX CLI.
8
+ * All path resolution and context detection uses workspace.json.
9
+ *
10
+ * Loads and manages .descix/workspace.json for workspace-level operations.
11
+ */
12
+ export class WorkspaceConfig {
13
+ constructor(config, workspaceRoot = null) {
14
+ // Store workspace root for later use
15
+ this.workspaceRoot = workspaceRoot || config.workspaceRoot || null;
16
+
17
+ // Core config
18
+ this.version = config.version || '1.0';
19
+ this.type = config.type || 'workspace'; // 'workspace', 'community', or 'app'
20
+
21
+ // Legacy fields (v1 compatibility)
22
+ this.primaryCommunity = config.primaryCommunity;
23
+ this.directoryMappings = config.directoryMappings || {};
24
+ this.additionalContexts = config.additionalContexts || [];
25
+ this.defaultContext = config.defaultContext;
26
+ this.apiUrl = config.apiUrl;
27
+ this.environment = config.environment;
28
+
29
+ // V2 community tracking (root workspace)
30
+ this.communities = config.communities || {};
31
+
32
+ // V2.1 Product tracking (Unified Registry)
33
+ this.products = config.products || {};
34
+
35
+ // app_id → localPath map from env.platform + env.products (Unified Registry)
36
+ this._appIdToConfig = this._buildAppIdMap(config);
37
+
38
+ // Environment URLs for descix-serve gateway routing
39
+ this.env = config.env || {};
40
+
41
+ // Drive configuration (base_folder_id for template-based navigation)
42
+ this.driveConfig = config.driveConfig || null;
43
+ }
44
+
45
+ /**
46
+ * Build app_id → config map from env.platform and env.products
47
+ * Unified Registry: app_id is globally unique; localPath maps to directory
48
+ * @param {Object} config - Parsed workspace.json
49
+ * @returns {Object} Map of appId -> { localPath, communityId?, kbId }
50
+ */
51
+ _buildAppIdMap(config) {
52
+ const map = {};
53
+ const env = config.env || {};
54
+ const wsRoot = config.workspaceRoot || this.workspaceRoot;
55
+
56
+ // env.platform (e.g. daita -> DeSciX_Cloud)
57
+ if (env.platform?.appId && env.platform?.localPath) {
58
+ map[env.platform.appId] = {
59
+ localPath: env.platform.localPath,
60
+ communityId: env.platform.communityId || null,
61
+ kbId: env.platform.kbId || 'General'
62
+ };
63
+ }
64
+
65
+ // env.products array (e.g. powch -> DeSciX_Powch)
66
+ const products = Array.isArray(env.products) ? env.products : [];
67
+ for (const p of products) {
68
+ if (p?.appId && p?.localPath) {
69
+ map[p.appId] = {
70
+ localPath: p.localPath,
71
+ communityId: p.communityId || null,
72
+ kbId: p.kbId || 'General'
73
+ };
74
+ }
75
+ }
76
+
77
+ return map;
78
+ }
79
+
80
+ /**
81
+ * Get app config by app_id (Unified Registry - app_id is globally unique)
82
+ * Resolves from env.platform, env.products, communities, or products
83
+ * @param {string} appId - App/product identifier
84
+ * @returns {Object|null} { localPath, absolutePath?, communityId?, kbId } or null
85
+ */
86
+ getAppByAppId(appId) {
87
+ if (!appId || !this.workspaceRoot) return null;
88
+
89
+ // env.platform + env.products (primary for manually crafted workspace)
90
+ const fromEnv = this._appIdToConfig[appId];
91
+ if (fromEnv) {
92
+ const absPath = path.join(this.workspaceRoot, fromEnv.localPath);
93
+ return {
94
+ localPath: fromEnv.localPath,
95
+ absolutePath: absPath,
96
+ communityId: fromEnv.communityId,
97
+ kbId: fromEnv.kbId || 'General'
98
+ };
99
+ }
100
+
101
+ // Legacy: products object
102
+ const product = this.products[appId];
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
+ }
112
+
113
+ // Legacy: communities.apps
114
+ for (const [, comm] of Object.entries(this.communities || {})) {
115
+ const app = comm?.apps?.[appId];
116
+ if (app?.localPath) {
117
+ const absPath = app.absolutePath || path.join(this.workspaceRoot, app.localPath);
118
+ return {
119
+ localPath: app.localPath,
120
+ absolutePath: absPath,
121
+ communityId: null, // Will be derived from Products on backend
122
+ kbId: app.kbId || 'General'
123
+ };
124
+ }
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Find the workspace root by searching up from startDir
132
+ *
133
+ * Workspace root is identified by (in priority order):
134
+ * 1. .descix/ folder (DeSciX workspace marker)
135
+ * 2. .cursor/ folder (Cursor IDE workspace)
136
+ * 3. .vscode/ folder (VS Code workspace)
137
+ *
138
+ * This ensures we find the actual IDE workspace root, not just the current directory.
139
+ *
140
+ * @param {string} startDir - Directory to start searching from (default: cwd)
141
+ * @returns {Promise<string|null>} Workspace root path, or null if not found
142
+ */
143
+ static async findWorkspaceRoot(startDir = process.cwd()) {
144
+ let currentDir = path.resolve(startDir);
145
+ const root = path.parse(currentDir).root;
146
+
147
+ while (currentDir !== root) {
148
+ // Workspace root requires .descix/workspace.json — marker dirs alone are not enough
149
+ // (sub-projects like DeSciX_Cloud have .descix/ for wallet.json but are not workspace roots)
150
+ const wsConfigPath = path.join(currentDir, '.descix', 'workspace.json');
151
+ try {
152
+ const stat = await fs.stat(wsConfigPath);
153
+ if (stat.isFile()) {
154
+ return currentDir;
155
+ }
156
+ } catch {
157
+ // workspace.json not found at this level, continue up
158
+ }
159
+
160
+ const parentDir = path.dirname(currentDir);
161
+ if (parentDir === currentDir) {
162
+ break;
163
+ }
164
+ currentDir = parentDir;
165
+ }
166
+
167
+ return null; // No workspace found
168
+ }
169
+
170
+ /**
171
+ * Load workspace configuration from .descix/workspace.json
172
+ *
173
+ * Automatically searches upward from startDir to find workspace root.
174
+ *
175
+ * @param {string} startDir - Directory to start searching from (default: cwd)
176
+ * @returns {Promise<WorkspaceConfig>} Configuration object
177
+ * @throws {Error} If workspace not found
178
+ */
179
+ static async load(startDir = process.cwd()) {
180
+ // First, find workspace root by searching upward
181
+ // If startDir is provided, use it. If not, use process.cwd()
182
+ const workspaceRoot = await WorkspaceConfig.findWorkspaceRoot(startDir);
183
+ 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
+ throw new Error(
188
+ 'Workspace not configured.\n' +
189
+ 'Run "npx descix init" first to initialize your workspace.'
190
+ );
191
+ }
192
+
193
+ const configPath = path.join(workspaceRoot, '.descix', 'workspace.json');
194
+ try {
195
+ const data = await fs.readFile(configPath, 'utf-8');
196
+ const parsed = JSON.parse(data);
197
+ return new WorkspaceConfig(parsed, workspaceRoot);
198
+ } catch (error) {
199
+ throw new Error(
200
+ 'Workspace not configured.\n' +
201
+ 'Run "npx descix init" first to initialize your workspace.'
202
+ );
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Try to load workspace configuration, return null if not found
208
+ * Useful for commands that need to check if workspace exists
209
+ *
210
+ * @param {string} startDir - Directory to start searching from
211
+ * @returns {Promise<WorkspaceConfig|null>}
212
+ */
213
+ static async tryLoad(startDir = process.cwd()) {
214
+ try {
215
+ return await WorkspaceConfig.load(startDir);
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Save workspace configuration in v2.1 format.
223
+ * Always writes env.platform / env.products structure — never v1 communities.
224
+ * If loaded from a v1 communities format (e.g. PWA device_check_status response),
225
+ * auto-converts communities.apps → env.products on write.
226
+ * @param {string} [workspaceRoot] - Workspace root directory (uses stored root if not provided)
227
+ * @returns {Promise<string>} Path to saved config
228
+ */
229
+ async save(workspaceRoot = null) {
230
+ const root = workspaceRoot || this.workspaceRoot;
231
+ if (!root) {
232
+ throw new Error('Workspace root not set. Provide workspaceRoot parameter or load config first.');
233
+ }
234
+
235
+ const descixDir = path.join(root, '.descix');
236
+ await fs.mkdir(descixDir, { recursive: true });
237
+ const configPath = path.join(descixDir, 'workspace.json');
238
+
239
+ this.workspaceRoot = root;
240
+
241
+ // Build env block for v2.1 output.
242
+ // Native v2.1 path: this.env is already populated from env.platform/env.products.
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
+ }
260
+
261
+ const configData = {
262
+ version: '2.1',
263
+ workspaceRoot: path.resolve(root),
264
+ type: this.type,
265
+ };
266
+
267
+ if (envBlock) configData.env = envBlock;
268
+ if (this.driveConfig) configData.driveConfig = this.driveConfig;
269
+
270
+ await fs.writeFile(configPath, JSON.stringify(configData, null, 2), 'utf-8');
271
+ return configPath;
272
+ }
273
+
274
+ /**
275
+ * Compute and store absolute paths for all communities and apps
276
+ * Called automatically by save() to ensure paths are pre-computed
277
+ *
278
+ * @param {string} workspaceRoot - Workspace root directory
279
+ */
280
+ computeAbsolutePaths(workspaceRoot) {
281
+ 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
+
297
+ // Compute absolute paths for products
298
+ for (const [productId, product] of Object.entries(this.products || {})) {
299
+ if (product.localPath) {
300
+ product.absolutePath = path.join(absWorkspaceRoot, product.localPath);
301
+ }
302
+ }
303
+ }
304
+
305
+ // ============ Community Tracking Methods ============
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
+
322
+ /**
323
+ * Register an app in the v2.1 workspace (env.platform or env.products).
324
+ * community_id is server-authoritative — not stored in workspace.json.
325
+ * @param {string} communityId - Accepted for call-site compatibility but not stored
326
+ * @param {string} appId - App identifier (globally unique)
327
+ * @param {Object} appConfig - App configuration
328
+ * @param {string} appConfig.localPath - Local folder path relative to workspaceRoot
329
+ * @param {string} [appConfig.kbId] - Default knowledge base ID
330
+ * @returns {boolean} Success status
331
+ */
332
+ registerApp(communityId, appId, appConfig) {
333
+ if (!appId || !appConfig.localPath) {
334
+ throw new Error('appId and localPath are required');
335
+ }
336
+
337
+ if (!this.env) this.env = {};
338
+ if (!Array.isArray(this.env.products)) this.env.products = [];
339
+
340
+ const entry = { appId, localPath: appConfig.localPath, kbId: appConfig.kbId || 'General' };
341
+
342
+ if (this.env.platform?.appId === appId) {
343
+ this.env.platform.localPath = appConfig.localPath;
344
+ if (appConfig.kbId) this.env.platform.kbId = appConfig.kbId;
345
+ } else {
346
+ const idx = this.env.products.findIndex(p => p.appId === appId);
347
+ if (idx >= 0) {
348
+ this.env.products[idx] = { ...this.env.products[idx], ...entry };
349
+ } else {
350
+ this.env.products.push(entry);
351
+ }
352
+ }
353
+
354
+ this._appIdToConfig = this._buildAppIdMap({ env: this.env, workspaceRoot: this.workspaceRoot });
355
+ this.version = '2.1';
356
+ return true;
357
+ }
358
+
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
+ /**
439
+ * Register a product (Unified Registry)
440
+ * @param {string} productId - Global product identifier
441
+ * @param {Object} productConfig - Product configuration
442
+ * @returns {boolean} Success status
443
+ */
444
+ registerProduct(productId, productConfig) {
445
+ if (!productId || !productConfig.localPath) {
446
+ throw new Error('productId and localPath are required');
447
+ }
448
+
449
+ this.products[productId] = {
450
+ type: productConfig.type || 'APP',
451
+ localPath: productConfig.localPath,
452
+ context: productConfig.context || {},
453
+ registeredAt: new Date().toISOString()
454
+ };
455
+
456
+ this.version = '2.0';
457
+ return true;
458
+ }
459
+
460
+ /**
461
+ * Get product configuration
462
+ * @param {string} productId
463
+ * @returns {Object|null}
464
+ */
465
+ getProduct(productId) {
466
+ return this.products[productId] || null;
467
+ }
468
+
469
+ /**
470
+ * List all registered products
471
+ * @returns {Array} Array of product IDs
472
+ */
473
+ listProducts() {
474
+ return Object.keys(this.products);
475
+ }
476
+
477
+ /**
478
+ * Remove a product registration
479
+ * @param {string} productId
480
+ * @returns {boolean}
481
+ */
482
+ unregisterProduct(productId) {
483
+ if (this.products[productId]) {
484
+ delete this.products[productId];
485
+ return true;
486
+ }
487
+ return false;
488
+ }
489
+
490
+ /**
491
+ * Resolve context based on file path
492
+ *
493
+ * Maps file paths to appropriate community/app/kb based on directory mappings.
494
+ * Uses longest match (most specific directory) when multiple mappings match.
495
+ *
496
+ * @param {string} filePath - File path to resolve
497
+ * @returns {object} Context with communityId, appId, kbId
498
+ */
499
+ resolveContext(filePath) {
500
+ // Normalize path (handle both Unix and Windows paths)
501
+ const normalized = filePath.replace(/\\/g, '/');
502
+
503
+ // Try to match directory mappings (longest match first for specificity)
504
+ const matches = [];
505
+ for (const [dir, context] of Object.entries(this.directoryMappings)) {
506
+ const normalizedDir = dir.replace(/\\/g, '/');
507
+ if (normalized.includes(normalizedDir + '/') || normalized.startsWith(normalizedDir)) {
508
+ matches.push({ dir: normalizedDir, context, length: normalizedDir.length });
509
+ }
510
+ }
511
+
512
+ if (matches.length > 0) {
513
+ // Return longest match (most specific directory)
514
+ matches.sort((a, b) => b.length - a.length);
515
+ return matches[0].context;
516
+ }
517
+
518
+ // Fallback to default context
519
+ return this.defaultContext;
520
+ }
521
+
522
+ /**
523
+ * Detect community/app context from current working directory
524
+ * Matches cwd against known absolutePaths in communities/apps.
525
+ *
526
+ * This is the primary method for autodiscovery of app context.
527
+ *
528
+ * @param {string} startDir - Directory to detect context from (default: cwd)
529
+ * @returns {{ communityId: string, appId: string, kbId: string }|null}
530
+ */
531
+ detectContext(startDir = process.cwd()) {
532
+ const cwd = path.resolve(startDir);
533
+ const wsRoot = this.workspaceRoot ? path.resolve(this.workspaceRoot) : null;
534
+
535
+ // env.platform + env.products (Unified Registry - primary)
536
+ if (wsRoot) {
537
+ for (const [appId, cfg] of Object.entries(this._appIdToConfig || {})) {
538
+ const appPath = path.resolve(wsRoot, cfg.localPath);
539
+ if (cwd.startsWith(appPath)) {
540
+ return {
541
+ appId,
542
+ communityId: cfg.communityId || null,
543
+ kbId: cfg.kbId || 'General'
544
+ };
545
+ }
546
+ }
547
+ }
548
+
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
+ // products object (legacy)
567
+ for (const [productId, product] of Object.entries(this.products || {})) {
568
+ let productPath = product.absolutePath;
569
+ if (!productPath && product.localPath && wsRoot) {
570
+ productPath = path.join(wsRoot, product.localPath);
571
+ }
572
+
573
+ if (productPath && cwd.startsWith(productPath)) {
574
+ return {
575
+ productId,
576
+ communityId: product.context?.community || null,
577
+ appId: product.context?.app || productId,
578
+ kbId: product.kbId || 'General'
579
+ };
580
+ }
581
+ }
582
+
583
+ return null;
584
+ }
585
+
586
+ /**
587
+ * Resolve context by merging CLI options with detected context
588
+ * CLI options take priority over detected context
589
+ * Unified Registry: app_id only is sufficient; community_id derived on backend
590
+ *
591
+ * @param {Object} options - CLI options { community, app, kb }
592
+ * @returns {{ communityId: string|null, appId: string|null, kbId: string }}
593
+ */
594
+ resolveContextWithOptions(options = {}) {
595
+ const detected = this.detectContext();
596
+
597
+ return {
598
+ communityId: options.community || detected?.communityId || null,
599
+ appId: options.app || detected?.appId || null,
600
+ kbId: options.kb || detected?.kbId || 'General'
601
+ };
602
+ }
603
+
604
+ /**
605
+ * Resolve context and throw if app not determined
606
+ * Unified Registry: app_id only required (community_id derived on backend)
607
+ *
608
+ * @param {Object} options - CLI options { community, app, kb }
609
+ * @returns {{ communityId: string|null, appId: string, kbId: string }}
610
+ * @throws {Error} If appId cannot be determined
611
+ */
612
+ requireContext(options = {}) {
613
+ const ctx = this.resolveContextWithOptions(options);
614
+
615
+ if (!ctx.appId) {
616
+ throw new Error(
617
+ 'Could not determine app context.\n\n' +
618
+ 'Options:\n' +
619
+ ' 1. cd into an app directory\n' +
620
+ ' 2. Use flag: -a <app_id>\n\n' +
621
+ 'Example: npx descix update kb -a daita'
622
+ );
623
+ }
624
+
625
+ return ctx;
626
+ }
627
+
628
+ /**
629
+ * Get the workspace root path
630
+ * @returns {string|null}
631
+ */
632
+ getWorkspaceRoot() {
633
+ return this.workspaceRoot;
634
+ }
635
+
636
+ /**
637
+ * Get API URL for the current workspace.
638
+ * In DEV mode, derives from env.platform.microservice.port (v2.1 format).
639
+ * Eliminates the need for DESCIX_API_URL in local dev.
640
+ * @returns {string}
641
+ */
642
+ getApiUrl() {
643
+ if (this.apiUrl) return this.apiUrl;
644
+ const platformPort = this.env?.platform?.microservice?.port;
645
+ if (platformPort && this.env?.environment === 'DEV') return `https://localhost:${platformPort}`;
646
+ return 'https://descix.net';
647
+ }
648
+
649
+ /**
650
+ * Get all contexts (default + additional)
651
+ *
652
+ * Useful for multi-context queries that search across multiple
653
+ * communities or apps simultaneously.
654
+ *
655
+ * @returns {Array} Array of context objects
656
+ */
657
+ getAllContexts() {
658
+ return [this.defaultContext, ...this.additionalContexts];
659
+ }
660
+
661
+ /**
662
+ * Validate configuration
663
+ *
664
+ * @returns {object} Validation result with isValid and errors
665
+ */
666
+ validate() {
667
+ const errors = [];
668
+
669
+ // For v2 root workspace with communities
670
+ if (this.version === '2.0' && Object.keys(this.communities).length > 0) {
671
+ for (const [communityId, community] of Object.entries(this.communities)) {
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`);
704
+ }
705
+
706
+ return {
707
+ isValid: errors.length === 0,
708
+ errors
709
+ };
710
+ }
711
+ }
712
+