@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,653 @@
1
+ /**
2
+ * Update Commands - Context-Driven Resource Syncing
3
+ *
4
+ * Commands for updating resources based on workspace.json context detection:
5
+ * descix update # Auto-detect what to update
6
+ * descix update app # Sync app assets to Drive
7
+ * descix update kb # Sync KB + vectorize
8
+ * descix update site # Deploy site to GCS
9
+ * descix update all # Update everything
10
+ *
11
+ * Uses WorkspaceConfig as sole configuration methodology.
12
+ * Folder structure follows convention:
13
+ * {appPath}/assets/ - App assets (icon, description, system_instructions)
14
+ * {appPath}/kb/{kbId}/ - Knowledge base files
15
+ * {appPath}/site/ - Static site files
16
+ * {appPath}/microservice/ - Microservice code
17
+ */
18
+
19
+ import chalk from 'chalk';
20
+ import ora from 'ora';
21
+ import * as fs from 'fs/promises';
22
+ import * as path from 'path';
23
+ import crypto from 'crypto';
24
+ import { DeSciXApiClient } from '../api-client.js';
25
+ import { requireAuth } from '../auth-guard.js';
26
+ import { WorkspaceConfig } from '../workspace-config.js';
27
+ import { runKbChunk, runKbSync } from './kb.js';
28
+
29
+ /**
30
+ * Helper to load workspace context with options override
31
+ * Unified Registry: app_id only required; community_id derived on backend
32
+ * @param {Object} options - CLI options with community, app, kb
33
+ * @returns {{ workspaceConfig, communityId, appId, kbId, appPath }}
34
+ */
35
+ async function loadWorkspaceContext(options = {}) {
36
+ const workspaceConfig = await WorkspaceConfig.load();
37
+ const ctx = workspaceConfig.requireContext(options);
38
+
39
+ // Prefer getAppByAppId (env.platform/products, Unified Registry)
40
+ let appConfig = workspaceConfig.getAppByAppId(ctx.appId);
41
+ if (!appConfig && ctx.communityId) {
42
+ appConfig = workspaceConfig.getApp(ctx.communityId, ctx.appId);
43
+ if (appConfig) {
44
+ appConfig = {
45
+ localPath: appConfig.localPath,
46
+ absolutePath: appConfig.absolutePath || (workspaceConfig.workspaceRoot && appConfig.localPath
47
+ ? path.join(workspaceConfig.workspaceRoot, appConfig.localPath)
48
+ : null),
49
+ communityId: ctx.communityId,
50
+ kbId: appConfig.kbId || 'General'
51
+ };
52
+ }
53
+ }
54
+
55
+ if (!appConfig) {
56
+ throw new Error(
57
+ `App "${ctx.appId}" not found in workspace.json.\n` +
58
+ 'Run "npx descix init" or ensure env.platform/env.products maps app_id to localPath.'
59
+ );
60
+ }
61
+
62
+ const appPath = appConfig.absolutePath ||
63
+ (appConfig.localPath && workspaceConfig.workspaceRoot
64
+ ? path.join(workspaceConfig.workspaceRoot, appConfig.localPath)
65
+ : null);
66
+
67
+ if (!appPath) {
68
+ throw new Error(`App path not configured for ${ctx.appId}`);
69
+ }
70
+
71
+ return {
72
+ workspaceConfig,
73
+ communityId: ctx.communityId || appConfig.communityId,
74
+ appId: ctx.appId,
75
+ kbId: ctx.kbId || appConfig.kbId || 'General',
76
+ appPath
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Compute SHA256 hash of file content
82
+ */
83
+ function computeHash(content) {
84
+ return crypto.createHash('sha256').update(content).digest('hex');
85
+ }
86
+
87
+ /**
88
+ * Read all files from a directory
89
+ */
90
+ async function readDirectoryFiles(dirPath) {
91
+ const files = [];
92
+
93
+ try {
94
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
95
+
96
+ for (const entry of entries) {
97
+ if (entry.name.startsWith('.')) continue; // Skip hidden files
98
+
99
+ const fullPath = path.join(dirPath, entry.name);
100
+
101
+ if (entry.isFile()) {
102
+ const content = await fs.readFile(fullPath);
103
+ files.push({
104
+ name: entry.name,
105
+ path: entry.name,
106
+ content: content.toString('base64'),
107
+ hash: computeHash(content),
108
+ size: content.length
109
+ });
110
+ } else if (entry.isDirectory()) {
111
+ // Recursive read for subdirectories
112
+ const subFiles = await readDirectoryFilesRecursive(fullPath, entry.name);
113
+ files.push(...subFiles);
114
+ }
115
+ }
116
+ } catch (error) {
117
+ if (error.code !== 'ENOENT') throw error;
118
+ }
119
+
120
+ return files;
121
+ }
122
+
123
+ /**
124
+ * Recursively read files from directory
125
+ */
126
+ async function readDirectoryFilesRecursive(dirPath, basePath = '') {
127
+ const files = [];
128
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
129
+
130
+ for (const entry of entries) {
131
+ if (entry.name.startsWith('.')) continue;
132
+
133
+ const fullPath = path.join(dirPath, entry.name);
134
+ const relativePath = path.join(basePath, entry.name);
135
+
136
+ if (entry.isFile()) {
137
+ const content = await fs.readFile(fullPath);
138
+ files.push({
139
+ name: entry.name,
140
+ path: relativePath,
141
+ content: content.toString('base64'),
142
+ hash: computeHash(content),
143
+ size: content.length
144
+ });
145
+ } else if (entry.isDirectory()) {
146
+ const subFiles = await readDirectoryFilesRecursive(fullPath, relativePath);
147
+ files.push(...subFiles);
148
+ }
149
+ }
150
+
151
+ return files;
152
+ }
153
+
154
+ /**
155
+ * Auto-detect and run appropriate update based on current directory
156
+ */
157
+ export async function updateAuto(options = {}) {
158
+ const spinner = ora('Loading context...').start();
159
+
160
+ try {
161
+ const ctx = await loadWorkspaceContext(options);
162
+ const cwd = process.cwd();
163
+
164
+ // Detect what to sync based on current directory relative to app path
165
+ const relativePath = path.relative(ctx.appPath, cwd);
166
+ let syncType = 'app'; // default
167
+
168
+ if (relativePath.startsWith('kb') || relativePath.startsWith('kb/')) {
169
+ syncType = 'kb';
170
+ } else if (relativePath.startsWith('site') || relativePath.startsWith('site/')) {
171
+ syncType = 'site';
172
+ } else if (relativePath.startsWith('assets') || relativePath.startsWith('assets/')) {
173
+ syncType = 'app';
174
+ } else if (relativePath.startsWith('microservice') || relativePath.startsWith('microservice/')) {
175
+ syncType = 'microservice';
176
+ }
177
+
178
+ spinner.succeed(`Context loaded: ${ctx.communityId || '?'}/${ctx.appId}`);
179
+ console.log(chalk.gray(` Auto-detected sync type: ${syncType}\n`));
180
+
181
+ // Route to appropriate update function
182
+ switch (syncType) {
183
+ case 'app':
184
+ return await updateApp(options);
185
+ case 'kb':
186
+ return await updateKB(options);
187
+ case 'site':
188
+ return await updateSite(options);
189
+ case 'microservice':
190
+ console.log(chalk.yellow(' Microservice deployment not supported via update. Use \'descix deploy\'.\n'));
191
+ return { success: false, reason: 'use_deploy' };
192
+ default:
193
+ console.log(chalk.yellow(` Unknown sync type: ${syncType}. Use 'descix update app/kb/site' explicitly.\n`));
194
+ return { success: false, reason: 'unknown_sync_type' };
195
+ }
196
+
197
+ } catch (error) {
198
+ spinner.fail('Update failed');
199
+ console.error(chalk.red(`\nāŒ ${error.message}\n`));
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Update app assets (icon, description, system_instructions)
206
+ * Stage 1: Local → Drive (DIRECT using ADC - no backend)
207
+ * Stage 2: Drive → GCS/Firestore (backend refresh_listing_assets)
208
+ *
209
+ * Convention: Assets are in {appPath}/assets/
210
+ */
211
+ export async function updateApp(options = {}) {
212
+ const apiClient = new DeSciXApiClient();
213
+ await requireAuth(apiClient);
214
+
215
+ const spinner = ora('Loading context...').start();
216
+
217
+ try {
218
+ const ctx = await loadWorkspaceContext(options);
219
+
220
+ spinner.text = 'Reading app assets...';
221
+
222
+ // Convention: assets folder
223
+ const assetsDir = path.join(ctx.appPath, 'assets');
224
+
225
+ // Get list of local asset files
226
+ let localFiles = [];
227
+ try {
228
+ const entries = await fs.readdir(assetsDir, { withFileTypes: true });
229
+ for (const entry of entries) {
230
+ if (entry.isFile() && !entry.name.startsWith('.')) {
231
+ const fullPath = path.join(assetsDir, entry.name);
232
+ const stats = await fs.stat(fullPath);
233
+ localFiles.push({
234
+ name: entry.name,
235
+ path: fullPath,
236
+ size: stats.size,
237
+ modifiedTime: stats.mtime.toISOString()
238
+ });
239
+ }
240
+ }
241
+ } catch (error) {
242
+ if (error.code === 'ENOENT') {
243
+ spinner.succeed('No app assets to sync');
244
+ console.log(chalk.gray(`\n Directory: ${assetsDir}`));
245
+ console.log(chalk.gray(' No files found.\n'));
246
+ return { success: true, synced: 0 };
247
+ }
248
+ throw error;
249
+ }
250
+
251
+ if (localFiles.length === 0) {
252
+ spinner.succeed('No app assets to sync');
253
+ console.log(chalk.gray(`\n Directory: ${assetsDir}`));
254
+ console.log(chalk.gray(' No files found.\n'));
255
+ return { success: true, synced: 0 };
256
+ }
257
+
258
+ spinner.succeed(`Found ${localFiles.length} files`);
259
+
260
+ console.log(chalk.cyan('\nšŸ“¤ Updating App Assets\n'));
261
+ console.log(chalk.gray(` Community: ${ctx.communityId}`));
262
+ console.log(chalk.gray(` App: ${ctx.appId}`));
263
+ console.log(chalk.gray(` Source: ${assetsDir}\n`));
264
+
265
+ // Show files
266
+ console.log(chalk.gray(' Files:'));
267
+ localFiles.forEach(f => {
268
+ console.log(chalk.gray(` • ${f.name} (${(f.size / 1024).toFixed(1)}KB)`));
269
+ });
270
+ console.log('');
271
+
272
+ // Stage 1: Upload directly to Drive using ADC
273
+ spinner.start('Stage 1: Uploading to Drive (direct)...');
274
+
275
+ const googleStorageADC = await import('../google-storage-adc.js');
276
+
277
+ // Get base folder ID from workspace config or user profile
278
+ let baseFolderId = ctx.workspaceConfig.driveConfig?.base_folder_id;
279
+
280
+ if (!baseFolderId) {
281
+ const userInfo = await apiClient.invoke('get_my_profile', {});
282
+ baseFolderId = userInfo.message?.base_folder_id;
283
+ }
284
+
285
+ if (!baseFolderId) {
286
+ throw new Error('No base Drive folder configured. Run descix login --setup first.');
287
+ }
288
+
289
+ // Navigate to assets folder: {base}/{community}/{app}/assets
290
+ const assetsPath = `${ctx.communityId}/${ctx.appId}/assets`;
291
+ let assetsFolderId = await googleStorageADC.findFolderByPath(baseFolderId, assetsPath);
292
+
293
+ if (!assetsFolderId) {
294
+ spinner.text = 'Stage 1: Creating folder structure...';
295
+ assetsFolderId = await googleStorageADC.ensureFolderPath(baseFolderId, assetsPath);
296
+ }
297
+
298
+ // List Drive files for delta comparison
299
+ const driveFiles = await googleStorageADC.listFolderWithMeta(assetsFolderId);
300
+
301
+ // Compute delta
302
+ const { toUpload, toUpdate, unchanged } = googleStorageADC.computeDelta(localFiles, driveFiles);
303
+
304
+ // Upload changed files directly to Drive
305
+ const uploaded = [];
306
+ for (const file of [...toUpload, ...toUpdate]) {
307
+ spinner.text = `Stage 1: Uploading ${file.name}...`;
308
+ await googleStorageADC.uploadFileToDrive(file.path, file.name, assetsFolderId);
309
+ uploaded.push(file.name);
310
+ }
311
+
312
+ spinner.succeed(`Stage 1: ${uploaded.length} files uploaded (${unchanged.length} unchanged)`);
313
+
314
+ // Stage 2: Trigger backend to sync Drive → GCS/Firestore
315
+ spinner.start('Stage 2: Syncing to platform...');
316
+
317
+ const response = await apiClient.invoke('refresh_listing_assets', {
318
+ community_id: ctx.communityId,
319
+ app_id: ctx.appId
320
+ });
321
+
322
+ const result = response.message || response;
323
+
324
+ spinner.succeed('Stage 2: Platform sync complete');
325
+
326
+ // Summary
327
+ if (uploaded.length > 0) {
328
+ console.log(chalk.green('\n Uploaded to Drive:'));
329
+ uploaded.forEach(f => console.log(chalk.green(` āœ“ ${f}`)));
330
+ }
331
+
332
+ if (unchanged.length > 0) {
333
+ console.log(chalk.gray('\n Unchanged:'));
334
+ unchanged.forEach(f => console.log(chalk.gray(` • ${f.name}`)));
335
+ }
336
+
337
+ if (result.icon_updated) {
338
+ console.log(chalk.green('\n āœ“ Icon synced to platform'));
339
+ }
340
+ if (result.description_updated) {
341
+ console.log(chalk.green(' āœ“ Description synced to platform'));
342
+ }
343
+ if (result.system_instructions_updated) {
344
+ console.log(chalk.green(' āœ“ System instructions synced to platform'));
345
+ }
346
+
347
+ if (result.errors?.length > 0) {
348
+ console.log(chalk.yellow('\n Errors:'));
349
+ result.errors.forEach(e => console.log(chalk.yellow(` ⚠ ${e}`)));
350
+ }
351
+
352
+ console.log('');
353
+ return { success: true, uploaded, unchanged: unchanged.map(f => f.name), ...result };
354
+
355
+ } catch (error) {
356
+ spinner.fail('Update failed');
357
+ console.error(chalk.red(`\nāŒ ${error.message}\n`));
358
+ throw error;
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Update knowledge base (Git Mode: local chunk → kb_sync_chunks → Pinecone)
364
+ *
365
+ * Drive Mode (Drive → GCS → Pinecone) is server-side only for PWA users.
366
+ * CLI mandates Git Mode: local files are chunked locally and upserted directly.
367
+ *
368
+ * Convention: KB files are in {appPath}/kb/{kbId}/
369
+ */
370
+ export async function updateKB(options = {}) {
371
+ const apiClient = new DeSciXApiClient();
372
+ await requireAuth(apiClient);
373
+
374
+ const spinner = ora('Loading context...').start();
375
+
376
+ try {
377
+ const ctx = await loadWorkspaceContext(options);
378
+ const kbId = ctx.kbId;
379
+
380
+ spinner.succeed(`KB: ${ctx.appId}/${kbId}`);
381
+
382
+ // Resolve community_id from Products when missing (Unified Registry - app_id only)
383
+ if (!ctx.communityId) {
384
+ try {
385
+ const productCtx = await apiClient.invoke('get_product_context', { app_id: ctx.appId });
386
+ ctx.communityId = productCtx?.community_id || productCtx?.message?.community_id;
387
+ if (!ctx.communityId) {
388
+ throw new Error(`Product "${ctx.appId}" not found in Products registry. Run bootstrap.`);
389
+ }
390
+ } catch (err) {
391
+ if (err.message?.includes('not found')) throw err;
392
+ throw new Error(`Could not resolve community for app "${ctx.appId}". Run bootstrap.`);
393
+ }
394
+ }
395
+
396
+ console.log(chalk.cyan('\nšŸ“¤ Updating Knowledge Base\n'));
397
+ console.log(chalk.gray(` Community: ${ctx.communityId}`));
398
+ console.log(chalk.gray(` App: ${ctx.appId}`));
399
+ console.log(chalk.gray(` KB: ${kbId}`));
400
+ console.log(chalk.gray(` Source: ${path.join(ctx.appPath, 'kb', kbId)}\n`));
401
+
402
+ // Git Mode: chunk local files → kb_sync_chunks → Pinecone
403
+ // (Drive → GCS → Pinecone is server-side only for PWA users)
404
+ const kbOptions = { ...options, app: ctx.appId, community: ctx.communityId, kb: kbId, quiet: true };
405
+ await runKbChunk(kbOptions);
406
+ await runKbSync(apiClient, kbOptions);
407
+
408
+ console.log(chalk.green('\nāœ“ Knowledge base updated successfully\n'));
409
+ return { success: true };
410
+
411
+ } catch (error) {
412
+ spinner.fail('Update failed');
413
+ console.error(chalk.red(`\nāŒ ${error.message}\n`));
414
+ throw error;
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Update site (deploy to GCS)
420
+ * Convention: Site files are in {appPath}/site/
421
+ */
422
+ export async function updateSite(options = {}) {
423
+ const apiClient = new DeSciXApiClient();
424
+ await requireAuth(apiClient);
425
+
426
+ const spinner = ora('Loading context...').start();
427
+
428
+ try {
429
+ const ctx = await loadWorkspaceContext(options);
430
+
431
+ // Convention: site folder path
432
+ const siteDir = path.join(ctx.appPath, 'site');
433
+
434
+ // Handle local port registration
435
+ if (options.port) {
436
+ spinner.text = `Registering local port: ${options.port}`;
437
+ try {
438
+ const appConfig = ctx.workspaceConfig.getApp(ctx.communityId, ctx.appId);
439
+
440
+ if (appConfig) {
441
+ if (!appConfig.site) appConfig.site = {};
442
+ appConfig.site.port = options.port;
443
+ // Also set devCommand if not present (default guess)
444
+ if (!appConfig.site.devCommand) {
445
+ appConfig.site.devCommand = 'npm run dev';
446
+ }
447
+
448
+ await ctx.workspaceConfig.save();
449
+ console.log(chalk.green(`\nāœ… Local port ${options.port} registered in workspace.json`));
450
+ } else {
451
+ console.log(chalk.yellow(`\nāš ļø App not found in workspace.json. Run 'descix init' first.`));
452
+ }
453
+ } catch (err) {
454
+ console.log(chalk.yellow(`\nāš ļø Could not register port: ${err.message}`));
455
+ }
456
+ }
457
+
458
+ // Run build command if specified
459
+ if (options.build) {
460
+ spinner.text = `Running build: ${options.build}`;
461
+ const { exec } = await import('child_process');
462
+ const { promisify } = await import('util');
463
+ const execAsync = promisify(exec);
464
+
465
+ try {
466
+ await execAsync(options.build, { cwd: siteDir });
467
+ spinner.succeed('Build completed');
468
+ } catch (error) {
469
+ spinner.fail('Build failed');
470
+ console.error(chalk.red(`\nāŒ ${error.message}\n`));
471
+ return { success: false, reason: 'build_failed' };
472
+ }
473
+ }
474
+
475
+ spinner.text = 'Reading site files...';
476
+
477
+ // Import GitUtils for file hashing (same as site deploy command)
478
+ const { GitUtils } = await import('@descix/sdk/integrations/git');
479
+ const mime = (await import('mime-types')).default;
480
+
481
+ const gitUtils = new GitUtils(ctx.appPath);
482
+ const localFiles = await gitUtils.getFileHashes(siteDir);
483
+
484
+ const fileList = Object.entries(localFiles).map(([filePath, info]) => ({
485
+ path: filePath,
486
+ hash: info.hash,
487
+ size: info.size,
488
+ content_type: mime.lookup(filePath) || 'application/octet-stream'
489
+ }));
490
+
491
+ spinner.succeed(`Found ${fileList.length} files`);
492
+
493
+ if (fileList.length === 0) {
494
+ console.log(chalk.yellow('\n No files found in site directory. Nothing to deploy.\n'));
495
+ return { success: true, synced: 0 };
496
+ }
497
+
498
+ console.log(chalk.cyan('\nšŸ“¤ Deploying CodeSite\n'));
499
+ console.log(chalk.gray(` Community: ${ctx.communityId}`));
500
+ console.log(chalk.gray(` App: ${ctx.appId}`));
501
+ console.log(chalk.gray(` Source: ${siteDir}`));
502
+ if (options.preview) console.log(chalk.yellow(` Mode: PREVIEW`));
503
+ console.log('');
504
+
505
+ // Request deploy token
506
+ spinner.start('Requesting deploy token...');
507
+
508
+ const tokenResponse = await apiClient.invoke('get_site_deploy_token', {
509
+ community_id: ctx.communityId,
510
+ app_id: ctx.appId,
511
+ files: fileList,
512
+ preview: options.preview || false
513
+ });
514
+
515
+ const { signed_urls, existing_manifest, token_id, site_url } = tokenResponse.message;
516
+
517
+ // Determine delta
518
+ let filesToUpload = fileList;
519
+
520
+ if (!options.full && existing_manifest) {
521
+ const delta = gitUtils.compareSyncState(localFiles, existing_manifest);
522
+ filesToUpload = fileList.filter(f =>
523
+ delta.added.includes(f.path) || delta.modified.includes(f.path)
524
+ );
525
+ spinner.succeed(`Delta: ${delta.added.length} added, ${delta.modified.length} modified, ${delta.unchanged.length} unchanged`);
526
+ } else {
527
+ spinner.succeed('Full upload');
528
+ }
529
+
530
+ if (filesToUpload.length === 0) {
531
+ console.log(chalk.green('\nāœ… Site already up to date!\n'));
532
+ console.log(chalk.cyan(` URL: ${site_url}\n`));
533
+ return { success: true, synced: 0, url: site_url };
534
+ }
535
+
536
+ // Upload files
537
+ spinner.start(`Uploading ${filesToUpload.length} files...`);
538
+
539
+ let uploadedCount = 0;
540
+
541
+ for (const file of filesToUpload) {
542
+ const signedUrl = signed_urls[file.path];
543
+ if (!signedUrl) continue;
544
+
545
+ const content = await fs.readFile(path.join(siteDir, file.path));
546
+
547
+ const response = await fetch(signedUrl, {
548
+ method: 'PUT',
549
+ headers: { 'Content-Type': file.content_type },
550
+ body: content
551
+ });
552
+
553
+ if (response.ok) uploadedCount++;
554
+ }
555
+
556
+ spinner.succeed(`Uploaded ${uploadedCount} files`);
557
+
558
+ // Confirm deployment
559
+ spinner.start('Confirming deployment...');
560
+
561
+ const confirmResponse = await apiClient.invoke('confirm_site_deploy', {
562
+ community_id: ctx.communityId,
563
+ app_id: ctx.appId,
564
+ token_id: token_id,
565
+ manifest: {
566
+ version: '1.0',
567
+ app_id: ctx.appId,
568
+ community_id: ctx.communityId,
569
+ files: localFiles
570
+ },
571
+ preview: options.preview || false
572
+ });
573
+
574
+ const result = confirmResponse.message;
575
+
576
+ spinner.succeed('Site deployed!');
577
+
578
+ console.log(chalk.green(`\nāœ… CodeSite deployed successfully!\n`));
579
+ console.log(chalk.cyan(` URL: ${result.site_url}`));
580
+ console.log(chalk.gray(` Files: ${result.files_count}\n`));
581
+
582
+ return { success: true, url: result.site_url, files: result.files_count };
583
+
584
+ } catch (error) {
585
+ spinner.fail('Deployment failed');
586
+ console.error(chalk.red(`\nāŒ ${error.message}\n`));
587
+ throw error;
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Update all resources (assets, KB, site)
593
+ * Convention-based: checks for existence of assets/, kb/, site/ folders
594
+ */
595
+ export async function updateAll(options = {}) {
596
+ const spinner = ora('Loading context...').start();
597
+
598
+ try {
599
+ const ctx = await loadWorkspaceContext(options);
600
+ spinner.succeed(`Context: ${ctx.communityId}/${ctx.appId}`);
601
+
602
+ console.log(chalk.cyan('\nšŸ“¤ Updating All Resources\n'));
603
+
604
+ const results = {};
605
+
606
+ // Check for assets folder and update if exists
607
+ const assetsDir = path.join(ctx.appPath, 'assets');
608
+ try {
609
+ await fs.access(assetsDir);
610
+ console.log(chalk.white('─── App Assets ───\n'));
611
+ results.app = await updateApp(options);
612
+ } catch {
613
+ // assets folder doesn't exist, skip
614
+ }
615
+
616
+ // Check for KB folder and update if exists
617
+ const kbDir = path.join(ctx.appPath, 'kb');
618
+ try {
619
+ await fs.access(kbDir);
620
+ console.log(chalk.white('\n─── Knowledge Base ───\n'));
621
+ results.kb = await updateKB(options);
622
+ } catch {
623
+ // kb folder doesn't exist, skip
624
+ }
625
+
626
+ // Check for site folder and update if exists
627
+ const siteDir = path.join(ctx.appPath, 'site');
628
+ try {
629
+ await fs.access(siteDir);
630
+ console.log(chalk.white('\n─── CodeSite ───\n'));
631
+ results.site = await updateSite(options);
632
+ } catch {
633
+ // site folder doesn't exist, skip
634
+ }
635
+
636
+ console.log(chalk.green('\nāœ… All resources updated!\n'));
637
+
638
+ return { success: true, results };
639
+
640
+ } catch (error) {
641
+ spinner.fail('Update failed');
642
+ console.error(chalk.red(`\nāŒ ${error.message}\n`));
643
+ throw error;
644
+ }
645
+ }
646
+
647
+ export default {
648
+ updateAuto,
649
+ updateApp,
650
+ updateKB,
651
+ updateSite,
652
+ updateAll
653
+ };