@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.
- package/README.md +95 -0
- package/SERVICE_README_sdk.md +399 -0
- package/agent-assets/guides/README.md +61 -0
- package/agent-assets/guides/dynamic-site-pattern.md +125 -0
- package/agent-assets/guides/knowledge-only-pattern.md +93 -0
- package/agent-assets/guides/microservice-pattern.md +152 -0
- package/agent-assets/guides/static-site-pattern.md +111 -0
- package/agent-assets/instructions/app-asset-validation.md +477 -0
- package/agent-assets/instructions/onboarding-flow.md +408 -0
- package/agent-assets/instructions/standard-folder-structure.md +125 -0
- package/agent-assets/reference/app-creation.md +381 -0
- package/agent-assets/reference/architecture.md +406 -0
- package/agent-assets/reference/local-dev.md +138 -0
- package/agent-assets/reference/mcp-integration.md +424 -0
- package/agent-assets/reference/workspace-config.md +415 -0
- package/agent-assets/rules/descix_mcp.template.mdc +243 -0
- package/bin/descix.js +3930 -0
- package/bin/mcp-server.js +382 -0
- package/descix-cli-1.0.0.tgz +0 -0
- package/lib/agent-files.js +162 -0
- package/lib/api-client.js +458 -0
- package/lib/auth-guard.js +67 -0
- package/lib/commands/app-wizard.js +627 -0
- package/lib/commands/auth.js +407 -0
- package/lib/commands/buy.js +207 -0
- package/lib/commands/clone.js +91 -0
- package/lib/commands/config.js +155 -0
- package/lib/commands/doctor.js +277 -0
- package/lib/commands/folder.js +291 -0
- package/lib/commands/init.js +257 -0
- package/lib/commands/kb.js +654 -0
- package/lib/commands/mcp.js +273 -0
- package/lib/commands/serve.js +12 -0
- package/lib/commands/status.js +239 -0
- package/lib/commands/update.js +653 -0
- package/lib/core/Chunker.js +610 -0
- package/lib/core/Hydrator.js +801 -0
- package/lib/core/PathContext.js +335 -0
- package/lib/core/Syncer.js +310 -0
- package/lib/core/index.js +32 -0
- package/lib/global-config.js +86 -0
- package/lib/google-storage-adc.js +511 -0
- package/lib/wallet-file.js +232 -0
- package/lib/wizard/setup.js +502 -0
- package/lib/workspace-config.js +712 -0
- package/lib/workspace-utils.js +126 -0
- package/package.json +41 -0
- package/templates/agent-claude.md +226 -0
- package/templates/agent-cline.md +133 -0
- package/templates/agent-copilot.md +133 -0
- package/templates/agent-cursor.md +133 -0
- package/templates/descix_mcp.mdc +122 -0
- package/templates/drive/README.md +42 -0
- package/templates/drive/app/assets/app_description.md +13 -0
- package/templates/drive/app/assets/icon.png +0 -0
- package/templates/drive/app/assets/system_instructions.md +17 -0
- package/templates/drive/app/kb/General/README.md +25 -0
- package/templates/drive/community/Apps/.gitkeep +4 -0
- package/templates/drive/community/community_assets/community_description.md +15 -0
- package/templates/drive/community/community_assets/icon.png +0 -0
- package/templates/drive_templates/README.md +104 -0
- package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
- package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
- package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
- package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
- package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
- package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
- package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
- package/templates/scaffolds/README.md +62 -0
- package/templates/scaffolds/microservice/Dockerfile +19 -0
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
- package/templates/scaffolds/microservice/app.js +50 -0
- package/templates/scaffolds/microservice/app.yaml +8 -0
- package/templates/scaffolds/microservice/defaults-config.json +5 -0
- package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
- package/templates/scaffolds/microservice/manifest.json +49 -0
- package/templates/scaffolds/microservice/package-lock.json +2645 -0
- package/templates/scaffolds/microservice/package.json +29 -0
- package/templates/scaffolds/microservice/scripts/register.js +55 -0
- package/templates/scaffolds/microservice/services/apiFront.js +75 -0
- package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
- package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
- package/templates/scaffolds/microservice/services/utils.js +247 -0
- package/templates/scaffolds/microservice/setup-schema.json +18 -0
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
- package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
- package/templates/scaffolds/site/README.md +51 -0
- package/templates/scaffolds/site/app.js +22 -0
- package/templates/scaffolds/site/index.html +42 -0
- package/templates/scaffolds/site/styles.css +123 -0
- package/test-hydration.js +150 -0
- package/vendor/mcp/transports/http-sse-transport.js +196 -0
- package/vendor/mcp/transports/index.js +15 -0
- 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
|
+
|