@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,627 @@
1
+ /**
2
+ * Interactive App Creation Wizard
3
+ * Guides developers through creating/updating an app with all assets
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import readline from 'readline';
8
+ import * as fs from 'fs/promises';
9
+ import * as path from 'path';
10
+
11
+ /**
12
+ * Create readline interface for interactive prompts
13
+ */
14
+ function createPrompt() {
15
+ return readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Ask a yes/no question
23
+ */
24
+ async function askYesNo(rl, question, defaultYes = true) {
25
+ return new Promise((resolve) => {
26
+ const suffix = defaultYes ? '(Y/n)' : '(y/N)';
27
+ rl.question(`${question} ${suffix}: `, (answer) => {
28
+ const trimmed = answer.trim().toLowerCase();
29
+ if (trimmed === '') {
30
+ resolve(defaultYes);
31
+ } else {
32
+ resolve(trimmed === 'y' || trimmed === 'yes');
33
+ }
34
+ });
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Ask for text input with optional default
40
+ */
41
+ async function askInput(rl, question, defaultValue = '') {
42
+ return new Promise((resolve) => {
43
+ const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
44
+ rl.question(prompt, (answer) => {
45
+ resolve(answer.trim() || defaultValue);
46
+ });
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Ask for file/folder path with validation
52
+ */
53
+ async function askPath(rl, question, mustExist = true, isDirectory = false) {
54
+ return new Promise((resolve) => {
55
+ const askPathRecursive = async () => {
56
+ const input = await askInput(rl, question);
57
+
58
+ if (!input) {
59
+ resolve(null); // Allow skip
60
+ return;
61
+ }
62
+
63
+ // Expand ~ to home directory
64
+ const expandedPath = input.startsWith('~')
65
+ ? path.join(process.env.HOME || '', input.slice(1))
66
+ : input;
67
+
68
+ const absolutePath = path.isAbsolute(expandedPath)
69
+ ? expandedPath
70
+ : path.resolve(process.cwd(), expandedPath);
71
+
72
+ if (mustExist) {
73
+ try {
74
+ const stats = await fs.stat(absolutePath);
75
+ if (isDirectory && !stats.isDirectory()) {
76
+ console.log(chalk.yellow(` ⚠️ Path exists but is not a directory.`));
77
+ askPathRecursive();
78
+ return;
79
+ }
80
+ if (!isDirectory && stats.isDirectory()) {
81
+ console.log(chalk.yellow(` ⚠️ Path is a directory, expected a file.`));
82
+ askPathRecursive();
83
+ return;
84
+ }
85
+ resolve(absolutePath);
86
+ } catch (err) {
87
+ console.log(chalk.yellow(` ⚠️ Path does not exist: ${absolutePath}`));
88
+ askPathRecursive();
89
+ }
90
+ } else {
91
+ resolve(absolutePath);
92
+ }
93
+ };
94
+
95
+ askPathRecursive();
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Read file content as base64
101
+ */
102
+ async function readFileAsBase64(filePath) {
103
+ const content = await fs.readFile(filePath);
104
+ return content.toString('base64');
105
+ }
106
+
107
+ /**
108
+ * Read all files from a directory recursively
109
+ */
110
+ async function readDirectoryFiles(dirPath, basePath = '') {
111
+ const files = [];
112
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
113
+
114
+ for (const entry of entries) {
115
+ const fullPath = path.join(dirPath, entry.name);
116
+ const relPath = path.join(basePath, entry.name);
117
+
118
+ if (entry.isDirectory()) {
119
+ const subFiles = await readDirectoryFiles(fullPath, relPath);
120
+ files.push(...subFiles);
121
+ } else if (entry.isFile()) {
122
+ // Skip hidden files and common non-content files
123
+ if (entry.name.startsWith('.') || entry.name === 'Thumbs.db') {
124
+ continue;
125
+ }
126
+ const content = await fs.readFile(fullPath);
127
+ files.push({
128
+ name: entry.name,
129
+ path: relPath,
130
+ content: content.toString('base64')
131
+ });
132
+ }
133
+ }
134
+
135
+ return files;
136
+ }
137
+
138
+ import crypto from 'crypto';
139
+
140
+ /**
141
+ * Generate a new service key pair
142
+ */
143
+ function generateServiceKey() {
144
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
145
+ namedCurve: 'secp256k1',
146
+ publicKeyEncoding: {
147
+ type: 'spki',
148
+ format: 'pem'
149
+ },
150
+ privateKeyEncoding: {
151
+ type: 'pkcs8',
152
+ format: 'pem'
153
+ }
154
+ });
155
+
156
+ return { publicKey, privateKey };
157
+ }
158
+
159
+ /**
160
+ * Run the interactive app creation wizard
161
+ */
162
+ export async function runAppWizard(apiClient, options) {
163
+ const rl = createPrompt();
164
+
165
+ console.log(chalk.cyan('\n╔════════════════════════════════════════════╗'));
166
+ console.log(chalk.cyan('║ DeSciX App Creation Wizard ║'));
167
+ console.log(chalk.cyan('╚════════════════════════════════════════════╝\n'));
168
+
169
+ try {
170
+ // Phase 1: Entitlements Discovery
171
+ console.log(chalk.gray(' Fetching entitlements...'));
172
+ const entitlementsResponse = await apiClient.invoke('fetch_my_purchases', {});
173
+ const entitlements = entitlementsResponse.message || {};
174
+ const serviceSlots = entitlements.service_slots || [];
175
+
176
+ if (serviceSlots.length === 0) {
177
+ console.log(chalk.yellow('\n⚠️ No service slots available.'));
178
+ console.log(chalk.white(' You need a "Runner NFT" or Subscription to create an app.'));
179
+ console.log(chalk.gray(' Visit https://descix.net/store to purchase one.\n'));
180
+ rl.close();
181
+ return;
182
+ }
183
+
184
+ console.log(chalk.cyan('\nSelect a Service Slot for this App:'));
185
+ serviceSlots.forEach((slot, index) => {
186
+ const statusColor = slot.status === 'active' ? chalk.green : chalk.gray;
187
+ console.log(`${index + 1}. ${chalk.bold(slot.name)} (${slot.type})`);
188
+ console.log(` Specs: ${slot.specs.ram}, ${slot.specs.cpu}`);
189
+ console.log(` Status: ${statusColor(slot.status)}`);
190
+ });
191
+
192
+ const slotIndex = await askInput(rl, chalk.white('\nSelect Slot #'), '1');
193
+ const selectedSlot = serviceSlots[parseInt(slotIndex) - 1];
194
+
195
+ if (!selectedSlot) {
196
+ console.log(chalk.red('\n❌ Invalid selection.\n'));
197
+ rl.close();
198
+ return;
199
+ }
200
+
201
+ console.log(chalk.green(`\n ✓ Selected: ${selectedSlot.name}\n`));
202
+
203
+ // Step 1: Get community and app name if not provided
204
+ let communityId = options.community;
205
+ let appName = options.name;
206
+
207
+ if (!communityId) {
208
+ // Show available communities
209
+ const communities = entitlements.communities || [];
210
+ if (communities.length > 0) {
211
+ console.log(chalk.cyan('Select Community:'));
212
+ communities.forEach((c, i) => {
213
+ console.log(`${i + 1}. ${c.community_name} (${c.community_id})`);
214
+ });
215
+ const commIndex = await askInput(rl, chalk.white('\nSelect Community # (or type ID)'), '1');
216
+ const selectedComm = communities[parseInt(commIndex) - 1];
217
+ communityId = selectedComm ? selectedComm.community_id : commIndex;
218
+ } else {
219
+ communityId = await askInput(rl, chalk.white('Community ID'));
220
+ }
221
+
222
+ if (!communityId) {
223
+ console.log(chalk.red('\n❌ Community ID is required.\n'));
224
+ rl.close();
225
+ return;
226
+ }
227
+ }
228
+
229
+ if (!appName) {
230
+ appName = await askInput(rl, chalk.white('App name'));
231
+ if (!appName) {
232
+ console.log(chalk.red('\n❌ App name is required.\n'));
233
+ rl.close();
234
+ return;
235
+ }
236
+ }
237
+
238
+ console.log(chalk.gray(`\n📋 Creating app '${appName}' in community '${communityId}'...\n`));
239
+
240
+ // Step 2: Check developer permission
241
+ console.log(chalk.gray(' Checking developer permissions...'));
242
+ const permCheck = await apiClient.invoke('check_developer_permission', {
243
+ community_id: communityId
244
+ });
245
+ const permResult = permCheck.message || permCheck;
246
+
247
+ if (!permResult.has_permission) {
248
+ console.log(chalk.red(`\n❌ ${permResult.message}`));
249
+ console.log(chalk.yellow(' Contact the community admin to request developer access.\n'));
250
+ rl.close();
251
+ return;
252
+ }
253
+ console.log(chalk.green(` ✓ Developer permission confirmed (${permResult.permission_type})\n`));
254
+
255
+ // Step 3: Check base folder registration
256
+ console.log(chalk.gray(' Checking Drive folder registration...'));
257
+ let folderCheck = await apiClient.invoke('get_user_base_folder', {});
258
+ let folderResult = folderCheck.message || folderCheck;
259
+
260
+ if (!folderResult.has_folder) {
261
+ console.log(chalk.yellow(`\n⚠️ No base folder registered yet.`));
262
+ console.log(chalk.gray('\n You need a Google Drive folder shared with dip@descix.net (Editor access).'));
263
+ console.log(chalk.gray(' This is where your app files will be stored.\n'));
264
+
265
+ const registerNow = await askYesNo(rl, chalk.white(' Do you have a folder ready to register?'), true);
266
+
267
+ if (!registerNow) {
268
+ console.log(chalk.gray('\n To set up a folder:'));
269
+ console.log(chalk.white(' 1. Create a folder in your Google Drive'));
270
+ console.log(chalk.white(' 2. Share it with dip@descix.net (Editor access)'));
271
+ console.log(chalk.white(' 3. Run this wizard again\n'));
272
+ rl.close();
273
+ return;
274
+ }
275
+
276
+ // Ask for folder URL/ID
277
+ const folderInput = await askInput(rl, chalk.white('\n Enter folder URL or ID'));
278
+ if (!folderInput) {
279
+ console.log(chalk.red('\n❌ Folder URL is required.\n'));
280
+ rl.close();
281
+ return;
282
+ }
283
+
284
+ // Extract folder ID from URL if needed
285
+ let folderId = folderInput.trim();
286
+ if (folderId.includes('drive.google.com')) {
287
+ const match = folderId.match(/\/folders\/([a-zA-Z0-9_-]+)/);
288
+ if (match && match[1]) {
289
+ folderId = match[1];
290
+ }
291
+ }
292
+ // Also handle /d/ URLs
293
+ if (folderId.includes('/d/')) {
294
+ const match = folderId.match(/\/d\/([a-zA-Z0-9_-]+)/);
295
+ if (match && match[1]) {
296
+ folderId = match[1];
297
+ }
298
+ }
299
+
300
+ console.log(chalk.gray(`\n Registering folder: ${folderId}...`));
301
+
302
+ try {
303
+ const registerResult = await apiClient.invoke('register_base_folder', {
304
+ folder_url: folderId
305
+ });
306
+ const registerData = registerResult.message || registerResult;
307
+ console.log(chalk.green(` ✓ Folder registered: ${registerData.folder_name || folderId}`));
308
+ folderResult = { has_folder: true, folder_id: folderId };
309
+ } catch (err) {
310
+ console.log(chalk.red(`\n❌ ${err.message}`));
311
+ console.log(chalk.yellow('\n Make sure you\'ve shared the folder with dip@descix.net (Editor access)\n'));
312
+ rl.close();
313
+ return;
314
+ }
315
+ } else {
316
+ console.log(chalk.green(` ✓ Base folder registered: ${folderResult.folder_id}`));
317
+ }
318
+ console.log();
319
+
320
+ // Step 4: Check if app already exists
321
+ const appId = appName.toLowerCase().replace(/[^a-z0-9_-]/g, '');
322
+ console.log(chalk.gray(' Checking for existing app...'));
323
+ const existsCheck = await apiClient.invoke('check_app_exists', {
324
+ community_id: communityId,
325
+ app_id: appId
326
+ });
327
+ const existsResult = existsCheck.message || existsCheck;
328
+
329
+ let overwrite = false;
330
+ if (existsResult.exists) {
331
+ console.log(chalk.yellow(`\n⚠️ App '${appId}' already exists in community '${communityId}'.`));
332
+ overwrite = await askYesNo(rl, chalk.white(' Overwrite existing app?'), false);
333
+ if (!overwrite) {
334
+ console.log(chalk.gray('\n Operation cancelled.\n'));
335
+ rl.close();
336
+ return;
337
+ }
338
+ console.log(chalk.gray(' Will update existing app.\n'));
339
+ } else {
340
+ console.log(chalk.green(` ✓ App '${appId}' is available\n`));
341
+ }
342
+
343
+ // Step 5: Collect optional assets
344
+ console.log(chalk.cyan('─── Optional Assets ───\n'));
345
+ console.log(chalk.gray(' Press Enter to skip any of these.\n'));
346
+
347
+ // Icon file
348
+ const iconPath = await askPath(rl, chalk.white(' Icon file path (png/jpg)'), true, false);
349
+ let iconBase64 = null;
350
+ if (iconPath) {
351
+ try {
352
+ iconBase64 = await readFileAsBase64(iconPath);
353
+ console.log(chalk.green(` ✓ Icon loaded: ${path.basename(iconPath)}`));
354
+ } catch (err) {
355
+ console.log(chalk.yellow(` ⚠️ Could not read icon: ${err.message}`));
356
+ }
357
+ }
358
+
359
+ // Description file
360
+ const descPath = await askPath(rl, chalk.white(' Description file path (txt/md)'), true, false);
361
+ let description = null;
362
+ if (descPath) {
363
+ try {
364
+ description = await fs.readFile(descPath, 'utf-8');
365
+ console.log(chalk.green(` ✓ Description loaded: ${path.basename(descPath)} (${description.length} chars)`));
366
+ } catch (err) {
367
+ console.log(chalk.yellow(` ⚠️ Could not read description: ${err.message}`));
368
+ }
369
+ }
370
+
371
+ // System instructions (text or file)
372
+ console.log(chalk.gray(' System instructions: Enter text directly or provide file path'));
373
+ const sysInstrInput = await askInput(rl, chalk.white(' System instructions (text or file path)'), true);
374
+ let systemInstructions = null;
375
+ let systemInstructionsFile = null;
376
+ if (sysInstrInput) {
377
+ // Check if it's a file path (contains path separators or ends with common extensions)
378
+ if (sysInstrInput.includes('/') || sysInstrInput.includes('\\') ||
379
+ sysInstrInput.endsWith('.txt') || sysInstrInput.endsWith('.md')) {
380
+ try {
381
+ systemInstructionsFile = await fs.readFile(sysInstrInput, 'utf-8');
382
+ systemInstructions = systemInstructionsFile;
383
+ console.log(chalk.green(` ✓ System instructions loaded from file: ${path.basename(sysInstrInput)} (${systemInstructions.length} chars)`));
384
+ } catch (err) {
385
+ console.log(chalk.yellow(` ⚠️ Could not read system instructions file: ${err.message}`));
386
+ // Treat as text if file read fails
387
+ systemInstructions = sysInstrInput;
388
+ console.log(chalk.gray(` Using as text input (${systemInstructions.length} chars)`));
389
+ }
390
+ } else {
391
+ // Treat as direct text input
392
+ systemInstructions = sysInstrInput;
393
+ console.log(chalk.green(` ✓ System instructions entered as text (${systemInstructions.length} chars)`));
394
+ }
395
+ }
396
+
397
+ // Knowledge base folder
398
+ const kbPath = await askPath(rl, chalk.white(' Knowledge base folder path'), true, true);
399
+ let kbFiles = null;
400
+ if (kbPath) {
401
+ try {
402
+ kbFiles = await readDirectoryFiles(kbPath);
403
+ console.log(chalk.green(` ✓ KB folder loaded: ${kbFiles.length} files`));
404
+ } catch (err) {
405
+ console.log(chalk.yellow(` ⚠️ Could not read KB folder: ${err.message}`));
406
+ }
407
+ }
408
+
409
+ // Step 6: Confirm and create
410
+ console.log(chalk.cyan('\n─── Summary ───\n'));
411
+ console.log(chalk.white(` Community: ${communityId}`));
412
+ console.log(chalk.white(` App Name: ${appName}`));
413
+ console.log(chalk.white(` App ID: ${appId}`));
414
+ console.log(chalk.white(` Slot: ${selectedSlot.name} (${selectedSlot.type})`));
415
+ console.log(chalk.white(` Icon: ${iconPath ? path.basename(iconPath) : 'Default'}`));
416
+ console.log(chalk.white(` Description: ${description ? `${description.length} chars` : 'Default'}`));
417
+ console.log(chalk.white(` Sys Instr: ${systemInstructions ? `${systemInstructions.length} chars` : 'Default'}`));
418
+ console.log(chalk.white(` KB Files: ${kbFiles ? `${kbFiles.length} files` : 'None'}`));
419
+ console.log(chalk.white(` Overwrite: ${overwrite ? 'Yes' : 'No'}`));
420
+
421
+ const proceed = await askYesNo(rl, chalk.white('\n Proceed with app creation?'), true);
422
+ if (!proceed) {
423
+ console.log(chalk.gray('\n Operation cancelled.\n'));
424
+ rl.close();
425
+ return;
426
+ }
427
+
428
+ // Step 7: Create skeleton app
429
+ console.log(chalk.gray('\n Creating app structure...'));
430
+ const createResult = await apiClient.invoke('create_skeleton_app', {
431
+ community_id: communityId,
432
+ app_name: appName,
433
+ overwrite: overwrite,
434
+ app_description: description,
435
+ // icon_url will be set after upload
436
+ });
437
+ const createData = createResult.message || createResult;
438
+ console.log(chalk.green(` ✓ App structure created`));
439
+ console.log(chalk.gray(` Drive folder: ${createData.folder_id}`));
440
+
441
+ // New Step: Generate and Register Service Key
442
+ console.log(chalk.gray(' Generating Service Delegate Key...'));
443
+ const { publicKey, privateKey } = generateServiceKey();
444
+
445
+ // Register delegate with Core (Virtual Registry)
446
+ try {
447
+ await apiClient.invoke('register_delegate', {
448
+ slot_id: selectedSlot.nft_id || selectedSlot.id,
449
+ slot_type: selectedSlot.type,
450
+ public_key: publicKey,
451
+ app_id: appId,
452
+ community_id: communityId
453
+ });
454
+ console.log(chalk.green(' ✓ Service Delegate Key registered'));
455
+
456
+ // Save key to dev-overrides.json (Standard Config Pattern)
457
+ const overridesPath = path.resolve(process.cwd(), 'dev-overrides.json');
458
+ let overrides = {};
459
+
460
+ try {
461
+ const content = await fs.readFile(overridesPath, 'utf-8');
462
+ overrides = JSON.parse(content);
463
+ } catch (e) {
464
+ // File doesn't exist or invalid, start fresh
465
+ }
466
+
467
+ // Update SERVICE_KEY in overrides
468
+ overrides.SERVICE_KEY = {
469
+ privateKey,
470
+ publicKey,
471
+ slotId: selectedSlot.nft_id || selectedSlot.id,
472
+ slotType: selectedSlot.type,
473
+ appId,
474
+ communityId,
475
+ createdAt: new Date().toISOString()
476
+ };
477
+
478
+ await fs.writeFile(overridesPath, JSON.stringify(overrides, null, 2));
479
+ console.log(chalk.green(` ✓ Key saved to dev-overrides.json`));
480
+
481
+ // Add to .gitignore if it exists
482
+ try {
483
+ const gitignorePath = path.resolve(process.cwd(), '.gitignore');
484
+ let gitignore = await fs.readFile(gitignorePath, 'utf-8').catch(() => '');
485
+ if (!gitignore.includes('dev-overrides.json')) {
486
+ await fs.appendFile(gitignorePath, '\ndev-overrides.json\n');
487
+ console.log(chalk.gray(' Added dev-overrides.json to .gitignore'));
488
+ }
489
+ } catch (err) {
490
+ // Ignore gitignore errors
491
+ }
492
+
493
+ } catch (err) {
494
+ console.log(chalk.yellow(` ⚠️ Failed to register delegate key: ${err.message}`));
495
+ console.log(chalk.gray(' You can register it later manually.'));
496
+ }
497
+
498
+ // Step 8: Upload assets (icon, description, system_instructions)
499
+ if (iconBase64) {
500
+ console.log(chalk.gray(' Uploading icon...'));
501
+ try {
502
+ await apiClient.invoke('upload_app_asset', {
503
+ community_id: communityId,
504
+ app_id: appId,
505
+ asset_type: 'icon',
506
+ file_name: `icon${path.extname(iconPath)}`,
507
+ file_content: iconBase64
508
+ });
509
+ console.log(chalk.green(` ✓ Icon uploaded`));
510
+ } catch (err) {
511
+ console.log(chalk.yellow(` ⚠️ Icon upload failed: ${err.message}`));
512
+ }
513
+ }
514
+
515
+ if (description) {
516
+ console.log(chalk.gray(' Uploading description...'));
517
+ try {
518
+ const descBase64 = Buffer.from(description, 'utf-8').toString('base64');
519
+ await apiClient.invoke('upload_app_asset', {
520
+ community_id: communityId,
521
+ app_id: appId,
522
+ asset_type: 'description',
523
+ file_name: 'app_description.md',
524
+ file_content: descBase64
525
+ });
526
+ console.log(chalk.green(` ✓ Description uploaded`));
527
+ } catch (err) {
528
+ console.log(chalk.yellow(` ⚠️ Description upload failed: ${err.message}`));
529
+ }
530
+ }
531
+
532
+ if (systemInstructions) {
533
+ console.log(chalk.gray(' Uploading system instructions...'));
534
+ try {
535
+ const sysInstrBase64 = Buffer.from(systemInstructions, 'utf-8').toString('base64');
536
+ await apiClient.invoke('upload_app_asset', {
537
+ community_id: communityId,
538
+ app_id: appId,
539
+ asset_type: 'system_instructions',
540
+ file_name: 'system_instructions.md',
541
+ file_content: sysInstrBase64
542
+ });
543
+ console.log(chalk.green(` ✓ System instructions uploaded`));
544
+ } catch (err) {
545
+ console.log(chalk.yellow(` ⚠️ System instructions upload failed: ${err.message}`));
546
+ }
547
+ }
548
+
549
+ // Step 9: Upload KB files if provided (in batches to avoid payload limits)
550
+ if (kbFiles && kbFiles.length > 0) {
551
+ console.log(chalk.gray(` Uploading ${kbFiles.length} KB files...`));
552
+
553
+ const BATCH_SIZE = 5; // Upload 5 files at a time (reduced for vectorization time)
554
+ let totalUploaded = 0;
555
+ let totalErrors = [];
556
+
557
+ try {
558
+ for (let i = 0; i < kbFiles.length; i += BATCH_SIZE) {
559
+ const batch = kbFiles.slice(i, i + BATCH_SIZE);
560
+ const batchNum = Math.floor(i / BATCH_SIZE) + 1;
561
+ const totalBatches = Math.ceil(kbFiles.length / BATCH_SIZE);
562
+
563
+ if (totalBatches > 1) {
564
+ process.stdout.write(chalk.gray(` Batch ${batchNum}/${totalBatches}...`));
565
+ }
566
+
567
+ try {
568
+ const uploadResult = await apiClient.invoke('upload_files_to_kb', {
569
+ community_id: communityId,
570
+ app_id: appId,
571
+ kb_id: 'General',
572
+ files: batch
573
+ });
574
+ const uploadData = uploadResult.message || uploadResult;
575
+ totalUploaded += uploadData.uploaded || batch.length;
576
+ if (uploadData.errors?.length) {
577
+ totalErrors.push(...uploadData.errors);
578
+ }
579
+ if (totalBatches > 1) {
580
+ console.log(chalk.green(` ✓`));
581
+ }
582
+ } catch (batchErr) {
583
+ if (totalBatches > 1) {
584
+ console.log(chalk.yellow(` failed: ${batchErr.message}`));
585
+ }
586
+ console.log(chalk.gray(` Files in batch: ${batch.map(f => f.name).join(', ')}`));
587
+ totalErrors.push({ batch: batchNum, error: batchErr.message });
588
+ }
589
+ }
590
+
591
+ console.log(chalk.green(` ✓ Uploaded ${totalUploaded} files`));
592
+ if (totalErrors.length > 0) {
593
+ console.log(chalk.yellow(` ⚠️ ${totalErrors.length} errors during upload`));
594
+ }
595
+ } catch (err) {
596
+ console.log(chalk.yellow(` ⚠️ KB upload failed: ${err.message}`));
597
+ }
598
+ }
599
+
600
+ // Done!
601
+ console.log(chalk.green('\n╔════════════════════════════════════════════╗'));
602
+ console.log(chalk.green('║ App Created Successfully! ║'));
603
+ console.log(chalk.green('╚════════════════════════════════════════════╝\n'));
604
+ console.log(chalk.white(` App ID: ${appId}`));
605
+ console.log(chalk.white(` Community: ${communityId}`));
606
+ console.log(chalk.white(` Folder: ${createData.folder_id}`));
607
+
608
+ console.log(chalk.gray(`\n To process your Knowledge Base:`));
609
+ console.log(chalk.cyan(` 1. descix kb pull -c ${communityId} -a ${appId}`));
610
+ console.log(chalk.cyan(` 2. descix kb build -c ${communityId} -a ${appId}`));
611
+
612
+ console.log(chalk.gray(`\n Query your app with:`));
613
+ console.log(chalk.cyan(` npx descix chat -c ${communityId} -a ${appId} "Your question here"`));
614
+
615
+ console.log(chalk.gray(`\n Add a CodeSite (static HTML/JS/CSS app):`));
616
+ console.log(chalk.cyan(` 1. Build your static site in a local directory`));
617
+ console.log(chalk.cyan(` 2. Deploy it: descix site deploy -c ${communityId} -a ${appId} -p <localPath>`));
618
+ console.log(chalk.cyan(` 3. The CodeSite will be available in the PWA's "My Apps" view\n`));
619
+
620
+ rl.close();
621
+
622
+ } catch (error) {
623
+ rl.close();
624
+ throw error;
625
+ }
626
+ }
627
+