@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,291 @@
1
+ /**
2
+ * Folder Commands - Drive Folder Management
3
+ *
4
+ * Commands for managing Drive folder IDs for entities:
5
+ * descix folder set --user/--community/--app/--kb <folder-id>
6
+ * descix folder get --user/--community/--app/--kb
7
+ * descix folder allocate --app/--kb
8
+ * descix folder validate <folder-id>
9
+ */
10
+
11
+ import chalk from 'chalk';
12
+ import ora from 'ora';
13
+ import { DeSciXApiClient } from '../api-client.js';
14
+ import { requireAuth } from '../auth-guard.js';
15
+ import { GlobalConfig } from '../global-config.js';
16
+
17
+ /**
18
+ * Extract folder ID from Drive URL or return as-is
19
+ */
20
+ function extractFolderId(input) {
21
+ if (!input) return null;
22
+
23
+ const trimmed = input.trim();
24
+
25
+ // Handle full URLs
26
+ if (trimmed.includes('drive.google.com')) {
27
+ const foldersMatch = trimmed.match(/\/folders\/([a-zA-Z0-9_-]+)/);
28
+ if (foldersMatch && foldersMatch[1]) return foldersMatch[1];
29
+
30
+ const dMatch = trimmed.match(/\/d\/([a-zA-Z0-9_-]+)/);
31
+ if (dMatch && dMatch[1]) return dMatch[1];
32
+
33
+ const idMatch = trimmed.match(/[?&]id=([a-zA-Z0-9_-]+)/);
34
+ if (idMatch && idMatch[1]) return idMatch[1];
35
+ }
36
+
37
+ return trimmed;
38
+ }
39
+
40
+ /**
41
+ * Set folder ID for an entity
42
+ */
43
+ export async function setFolder(options) {
44
+ const apiClient = new DeSciXApiClient();
45
+ await requireAuth(apiClient);
46
+
47
+ const spinner = ora('Setting folder...').start();
48
+
49
+ try {
50
+ let entityType, entityIds, folderId;
51
+
52
+ if (options.user) {
53
+ entityType = 'user';
54
+ folderId = extractFolderId(options.user);
55
+ } else if (options.community) {
56
+ entityType = 'community';
57
+ entityIds = { community_id: options.community };
58
+ folderId = extractFolderId(options.folderId);
59
+ } else if (options.app) {
60
+ entityType = 'app';
61
+ // Parse "community app" format
62
+ const parts = options.app.split(/\s+/);
63
+ if (parts.length < 2) {
64
+ throw new Error('App requires both community and app ID: --app "community app"');
65
+ }
66
+ entityIds = { community_id: parts[0], app_id: parts[1] };
67
+ folderId = extractFolderId(options.folderId);
68
+ } else if (options.kb) {
69
+ entityType = 'kb';
70
+ // Parse "community app kb" format
71
+ const parts = options.kb.split(/\s+/);
72
+ if (parts.length < 3) {
73
+ throw new Error('KB requires community, app, and kb ID: --kb "community app kb"');
74
+ }
75
+ entityIds = { community_id: parts[0], app_id: parts[1], kb_id: parts[2] };
76
+ folderId = extractFolderId(options.folderId);
77
+ } else {
78
+ throw new Error('Specify entity type: --user, --community, --app, or --kb');
79
+ }
80
+
81
+ if (!folderId) {
82
+ throw new Error('Folder ID is required');
83
+ }
84
+
85
+ const response = await apiClient.invoke('set_folder_id', {
86
+ entity_type: entityType,
87
+ entity_ids: entityIds,
88
+ folder_id: folderId
89
+ });
90
+
91
+ const result = response.message || response;
92
+
93
+ spinner.succeed('Folder set successfully');
94
+
95
+ console.log(chalk.green(`\n✓ ${result.message}`));
96
+ console.log(chalk.gray(` Entity: ${entityType}`));
97
+ console.log(chalk.gray(` Folder: ${folderId}`));
98
+ console.log(chalk.cyan(` URL: https://drive.google.com/drive/folders/${folderId}\n`));
99
+
100
+ // Update global config if setting user folder
101
+ if (entityType === 'user') {
102
+ const globalConfig = await GlobalConfig.load();
103
+ globalConfig.user_base_folder_id = folderId;
104
+ globalConfig.registered_at = new Date().toISOString();
105
+ await globalConfig.save();
106
+ }
107
+
108
+ return result;
109
+
110
+ } catch (error) {
111
+ spinner.fail('Failed to set folder');
112
+ console.error(chalk.red(`\n❌ ${error.message}\n`));
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Get folder info for an entity
119
+ */
120
+ export async function getFolder(options) {
121
+ const apiClient = new DeSciXApiClient();
122
+ await requireAuth(apiClient);
123
+
124
+ const spinner = ora('Getting folder info...').start();
125
+
126
+ try {
127
+ let entityType, entityIds;
128
+
129
+ if (options.user) {
130
+ entityType = 'user';
131
+ } else if (options.community) {
132
+ entityType = 'community';
133
+ entityIds = { community_id: options.community };
134
+ } else if (options.app) {
135
+ entityType = 'app';
136
+ const parts = options.app.split(/\s+/);
137
+ if (parts.length < 2) {
138
+ throw new Error('App requires both community and app ID: --app "community app"');
139
+ }
140
+ entityIds = { community_id: parts[0], app_id: parts[1] };
141
+ } else if (options.kb) {
142
+ entityType = 'kb';
143
+ const parts = options.kb.split(/\s+/);
144
+ if (parts.length < 3) {
145
+ throw new Error('KB requires community, app, and kb ID: --kb "community app kb"');
146
+ }
147
+ entityIds = { community_id: parts[0], app_id: parts[1], kb_id: parts[2] };
148
+ } else {
149
+ throw new Error('Specify entity type: --user, --community, --app, or --kb');
150
+ }
151
+
152
+ const response = await apiClient.invoke('get_folder_info', {
153
+ entity_type: entityType,
154
+ entity_ids: entityIds
155
+ });
156
+
157
+ const result = response.message || response;
158
+
159
+ spinner.succeed('Folder info retrieved');
160
+
161
+ console.log(chalk.cyan(`\n📁 Folder Info: ${entityType}\n`));
162
+
163
+ if (result.has_folder) {
164
+ console.log(chalk.green(` ✓ Folder registered`));
165
+ console.log(chalk.white(` ID: ${result.folder_id}`));
166
+ if (result.folder_name) {
167
+ console.log(chalk.white(` Name: ${result.folder_name}`));
168
+ }
169
+ if (result.kb_folder_id) {
170
+ console.log(chalk.white(` KB Folder: ${result.kb_folder_id}`));
171
+ }
172
+ console.log(chalk.cyan(` URL: https://drive.google.com/drive/folders/${result.folder_id}`));
173
+ } else {
174
+ console.log(chalk.yellow(` ⚠ No folder registered`));
175
+ console.log(chalk.gray(` Use 'descix folder set' to register a folder`));
176
+ }
177
+
178
+ console.log('');
179
+ return result;
180
+
181
+ } catch (error) {
182
+ spinner.fail('Failed to get folder info');
183
+ console.error(chalk.red(`\n❌ ${error.message}\n`));
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Allocate a new folder for an entity
190
+ */
191
+ export async function allocateFolder(options) {
192
+ const apiClient = new DeSciXApiClient();
193
+ await requireAuth(apiClient);
194
+
195
+ const spinner = ora('Allocating folder...').start();
196
+
197
+ try {
198
+ let entityType, entityIds, folderName;
199
+
200
+ if (options.app) {
201
+ entityType = 'app';
202
+ const parts = options.app.split(/\s+/);
203
+ if (parts.length < 2) {
204
+ throw new Error('App requires both community and app ID: --app "community app"');
205
+ }
206
+ entityIds = { community_id: parts[0], app_id: parts[1] };
207
+ folderName = options.name || `${parts[0]}_${parts[1]}`;
208
+ } else if (options.kb) {
209
+ entityType = 'kb';
210
+ const parts = options.kb.split(/\s+/);
211
+ if (parts.length < 3) {
212
+ throw new Error('KB requires community, app, and kb ID: --kb "community app kb"');
213
+ }
214
+ entityIds = { community_id: parts[0], app_id: parts[1], kb_id: parts[2] };
215
+ folderName = options.name || parts[2];
216
+ } else {
217
+ throw new Error('Specify entity type: --app or --kb');
218
+ }
219
+
220
+ const response = await apiClient.invoke('allocate_folder', {
221
+ entity_type: entityType,
222
+ entity_ids: entityIds,
223
+ folder_name: folderName
224
+ });
225
+
226
+ const result = response.message || response;
227
+
228
+ spinner.succeed('Folder allocated successfully');
229
+
230
+ console.log(chalk.green(`\n✓ New folder created`));
231
+ console.log(chalk.white(` Name: ${result.folder_name}`));
232
+ console.log(chalk.white(` ID: ${result.folder_id}`));
233
+ console.log(chalk.cyan(` URL: ${result.drive_url}\n`));
234
+
235
+ return result;
236
+
237
+ } catch (error) {
238
+ spinner.fail('Failed to allocate folder');
239
+ console.error(chalk.red(`\n❌ ${error.message}\n`));
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Validate folder access
246
+ */
247
+ export async function validateFolder(folderId, options = {}) {
248
+ const apiClient = new DeSciXApiClient();
249
+ await requireAuth(apiClient);
250
+
251
+ const spinner = ora('Validating folder...').start();
252
+
253
+ try {
254
+ const parsedFolderId = extractFolderId(folderId);
255
+
256
+ if (!parsedFolderId) {
257
+ throw new Error('Folder ID is required');
258
+ }
259
+
260
+ const response = await apiClient.invoke('validate_folder_access', {
261
+ folder_id: parsedFolderId
262
+ });
263
+
264
+ const result = response.message || response;
265
+
266
+ if (result.accessible) {
267
+ spinner.succeed('Folder is accessible');
268
+ console.log(chalk.green(`\n✓ ${result.message}`));
269
+ console.log(chalk.white(` Name: ${result.folder_name}`));
270
+ console.log(chalk.white(` ID: ${parsedFolderId}`));
271
+ console.log(chalk.cyan(` URL: https://drive.google.com/drive/folders/${parsedFolderId}\n`));
272
+ } else {
273
+ spinner.fail('Folder not accessible');
274
+ console.log(chalk.red(`\n❌ ${result.message}\n`));
275
+ }
276
+
277
+ return result;
278
+
279
+ } catch (error) {
280
+ spinner.fail('Validation failed');
281
+ console.error(chalk.red(`\n❌ ${error.message}\n`));
282
+ throw error;
283
+ }
284
+ }
285
+
286
+ export default {
287
+ setFolder,
288
+ getFolder,
289
+ allocateFolder,
290
+ validateFolder
291
+ };
@@ -0,0 +1,257 @@
1
+ /**
2
+ * DeSciX CLI Init Command
3
+ *
4
+ * Creates .descix/workspace.json (template-based creation is via PWA/Admin CLI).
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import readline from 'readline';
9
+ import path from 'path';
10
+ import fs from 'fs/promises';
11
+ import { fileURLToPath } from 'url';
12
+ import { dirname, join } from 'path';
13
+ // clone.js is imported dynamically inside the invite flow to avoid circular deps
14
+ import { WorkspaceConfig } from '../workspace-config.js';
15
+ import { generateAgentFiles } from '../agent-files.js';
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+
20
+ /**
21
+ * Create readline interface for interactive prompts
22
+ */
23
+ function createPrompt() {
24
+ return readline.createInterface({
25
+ input: process.stdin,
26
+ output: process.stdout
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Ask for text input with optional default
32
+ */
33
+ function askInput(rl, question, defaultValue = '') {
34
+ return new Promise((resolve) => {
35
+ const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
36
+ rl.question(prompt, (answer) => {
37
+ resolve(answer.trim() || defaultValue);
38
+ });
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Ask a yes/no question
44
+ */
45
+ function askYesNo(rl, question, defaultYes = true) {
46
+ return new Promise((resolve) => {
47
+ const suffix = defaultYes ? '(Y/n)' : '(y/N)';
48
+ rl.question(`${question} ${suffix}: `, (answer) => {
49
+ const trimmed = answer.trim().toLowerCase();
50
+ if (trimmed === '') {
51
+ resolve(defaultYes);
52
+ } else {
53
+ resolve(trimmed === 'y' || trimmed === 'yes');
54
+ }
55
+ });
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Run the init command
61
+ * @param {Object} apiClient - API client (optional, for listing communities)
62
+ * @param {Object} options - Command options
63
+ */
64
+ export async function runInit(apiClient, options = {}) {
65
+ const rl = createPrompt();
66
+ const projectPath = options.path ? path.resolve(options.path) : process.cwd();
67
+
68
+ // If --from-invite provided, resolve the invite first to pre-fill context
69
+ if (options.fromInvite) {
70
+ if (!apiClient) {
71
+ console.error(chalk.red('\n❌ Authentication required for --from-invite. Run "descix login" first.\n'));
72
+ rl.close();
73
+ return {};
74
+ }
75
+ try {
76
+ console.log(chalk.cyan('Resolving invite...\n'));
77
+ const inviteData = await apiClient.invoke('resolve_invite', { invite_token: options.fromInvite });
78
+
79
+ // Write app.json seed file
80
+ const descixDir = join(projectPath, '.descix');
81
+ await fs.mkdir(descixDir, { recursive: true });
82
+ const appJson = {
83
+ version: '1.0',
84
+ invite_token: options.fromInvite,
85
+ invite_type: inviteData.invite_type,
86
+ app_id: inviteData.app_id,
87
+ community_id: inviteData.community_id,
88
+ app_name: inviteData.app_name,
89
+ agent_hint: inviteData.agent_hint,
90
+ kb_ready: inviteData.kb_ready,
91
+ has_repo: inviteData.has_repo
92
+ };
93
+ await fs.writeFile(join(descixDir, 'app.json'), JSON.stringify(appJson, null, 2));
94
+
95
+ console.log(chalk.green(`Invite resolved: ${inviteData.app_name} (${inviteData.community_id}/${inviteData.app_id})`));
96
+ if (inviteData.agent_hint) {
97
+ console.log(chalk.gray(`Agent hint: ${inviteData.agent_hint}`));
98
+ }
99
+
100
+ // Auto-fill community and app for the rest of init
101
+ options.community = options.community || inviteData.community_id;
102
+ options.app = options.app || inviteData.app_id;
103
+
104
+ // Offer to clone if repo exists
105
+ if (inviteData.has_repo) {
106
+ console.log(chalk.cyan('\nThis app has a linked repository.'));
107
+ const { runClone } = await import('./clone.js');
108
+ await runClone(apiClient, { app_id: inviteData.app_id });
109
+ }
110
+ } catch (error) {
111
+ console.error(chalk.red(`\n❌ Failed to resolve invite: ${error.message}\n`));
112
+ rl.close();
113
+ return {};
114
+ }
115
+ }
116
+
117
+ console.log(chalk.cyan('\n╔════════════════════════════════════════════╗'));
118
+ console.log(chalk.cyan('║ DeSciX Workspace Initialization ║'));
119
+ console.log(chalk.cyan('╚════════════════════════════════════════════╝\n'));
120
+
121
+ try {
122
+ const descixDir = join(projectPath, '.descix');
123
+ const configPath = join(descixDir, 'workspace.json');
124
+ let hasExisting = false;
125
+ try {
126
+ await fs.access(configPath);
127
+ hasExisting = true;
128
+ } catch {
129
+ // No existing config
130
+ }
131
+
132
+ if (hasExisting && !options.force) {
133
+ const overwrite = await askYesNo(rl, chalk.yellow('Workspace already initialized. Overwrite?'), false);
134
+ if (!overwrite) {
135
+ console.log(chalk.gray('\nInitialization cancelled.\n'));
136
+ rl.close();
137
+ return {};
138
+ }
139
+ }
140
+
141
+ let communityId = options.community;
142
+ if (!communityId) {
143
+ if (apiClient) {
144
+ try {
145
+ const response = await apiClient.invoke('find_communities', {});
146
+ const communities = response.message?.communities || [];
147
+ if (communities.length > 0) {
148
+ console.log(chalk.cyan('Available Communities:'));
149
+ communities.slice(0, 10).forEach((c, i) => {
150
+ console.log(chalk.gray(` ${i + 1}. ${c.community_id} (${c.community_name})`));
151
+ });
152
+ if (communities.length > 10) console.log(chalk.gray(` ... and ${communities.length - 10} more`));
153
+ console.log();
154
+ }
155
+ } catch {
156
+ // ignore
157
+ }
158
+ }
159
+ communityId = await askInput(rl, chalk.white('Community ID'));
160
+ if (!communityId) {
161
+ console.log(chalk.red('\n❌ Community ID is required.\n'));
162
+ rl.close();
163
+ return {};
164
+ }
165
+ }
166
+
167
+ const defaultAppName = path.basename(projectPath).toLowerCase().replace(/[^a-z0-9]/g, '_');
168
+ let appName = options.app;
169
+ if (!appName) {
170
+ appName = await askInput(rl, chalk.white('App name'), defaultAppName);
171
+ if (!appName) {
172
+ console.log(chalk.red('\n❌ App name is required.\n'));
173
+ rl.close();
174
+ return {};
175
+ }
176
+ }
177
+
178
+ const appId = appName.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
179
+ console.log(chalk.cyan('\n─── Summary ───\n'));
180
+ console.log(chalk.white(` Project: ${projectPath}`));
181
+ console.log(chalk.white(` Community: ${communityId}`));
182
+ console.log(chalk.white(` App: ${appName} (${appId})`));
183
+
184
+ const proceed = await askYesNo(rl, chalk.white('\nProceed?'), true);
185
+ if (!proceed) {
186
+ console.log(chalk.gray('\nCancelled.\n'));
187
+ rl.close();
188
+ return {};
189
+ }
190
+
191
+ console.log(chalk.gray('\nWriting .descix/workspace.json...\n'));
192
+ const config = new WorkspaceConfig({ version: '2.0', type: 'workspace', communities: {} }, projectPath);
193
+ config.registerApp(communityId, appId, { localPath: '.', kbId: 'General' });
194
+ await config.save(projectPath);
195
+
196
+ console.log(chalk.green('Created:'));
197
+ console.log(chalk.green(' ✓ .descix/workspace.json'));
198
+
199
+ // Generate agent instruction files with correct context
200
+ try {
201
+ const agentFilesWritten = await generateAgentFiles(projectPath);
202
+ for (const f of agentFilesWritten) {
203
+ console.log(chalk.green(` ✓ ${f}`));
204
+ }
205
+ } catch {
206
+ // Agent file generation is best-effort
207
+ }
208
+
209
+ console.log(chalk.cyan('\n─── Next Steps ───\n'));
210
+ console.log(chalk.white(` 1. descix app init -a ${appId} -c ${communityId} # register app on platform`));
211
+ console.log(chalk.white(` 2. descix update kb -c ${communityId} -a ${appId} # sync KB to Pinecone`));
212
+ console.log(chalk.white(` 3. descix chat -c ${communityId} -a ${appId} -q "test" # verify RAG\n`));
213
+ console.log(chalk.green('✅ Workspace initialized.\n'));
214
+ rl.close();
215
+ return { created: ['.descix/workspace.json'], skipped: [], warnings: [] };
216
+
217
+ } catch (error) {
218
+ rl.close();
219
+ throw error;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Non-interactive init: writes .descix/workspace.json only.
225
+ * @param {Object} options - { path?, communityId, appName? }
226
+ * @returns {Promise<Object>} { created, skipped, warnings }
227
+ */
228
+ export async function initWorkspace(options) {
229
+ const projectPath = options.path ? path.resolve(options.path) : process.cwd();
230
+ const communityId = options.communityId;
231
+ const appName = options.appName || path.basename(projectPath).toLowerCase().replace(/[^a-z0-9]/g, '_');
232
+ const appId = appName.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
233
+ if (!communityId) {
234
+ throw new Error('communityId is required');
235
+ }
236
+ const config = new WorkspaceConfig({ version: '2.0', type: 'workspace', communities: {} }, projectPath);
237
+ config.registerApp(communityId, appId, { localPath: '.', kbId: 'General' });
238
+ await config.save(projectPath);
239
+
240
+ // Generate agent instruction files with workspace context
241
+ const created = ['.descix/workspace.json'];
242
+ try {
243
+ const agentFilesWritten = await generateAgentFiles(projectPath);
244
+ created.push(...agentFilesWritten);
245
+ } catch {
246
+ // Best-effort
247
+ }
248
+
249
+ return {
250
+ created,
251
+ skipped: [],
252
+ warnings: [],
253
+ nextSteps: [`descix app init -a ${appId} -c ${communityId}`, `descix update kb -c ${communityId} -a ${appId}`]
254
+ };
255
+ }
256
+
257
+ export default { runInit, initWorkspace };