@descix/cli 1.0.7 → 1.0.9

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 (43) hide show
  1. package/README.md +1 -1
  2. package/agent-assets/guides/README.md +1 -1
  3. package/agent-assets/guides/dynamic-site-pattern.md +7 -16
  4. package/agent-assets/instructions/app-asset-validation.md +1 -1
  5. package/agent-assets/reference/architecture.md +20 -25
  6. package/agent-assets/reference/local-dev.md +1 -6
  7. package/agent-assets/reference/mcp-integration.md +15 -9
  8. package/agent-assets/reference/platform-api-consumer.md +11 -5
  9. package/agent-assets/reference/workspace-config.md +2 -2
  10. package/bin/descix.js +211 -688
  11. package/lib/command-visibility.js +213 -0
  12. package/lib/commands/corpus.js +2 -5
  13. package/lib/commands/kb.js +2 -2
  14. package/lib/commands/media-upload.js +176 -0
  15. package/lib/commands/retired-kb-sync.js +5 -27
  16. package/lib/commands/status.js +0 -9
  17. package/lib/output.js +31 -0
  18. package/lib/sdk-assets.js +28 -0
  19. package/lib/verb-invokes.generated.json +233 -0
  20. package/lib/workspace-config.js +9 -44
  21. package/package.json +6 -2
  22. package/templates/agent-claude.md +1 -3
  23. package/templates/agent-cline.md +1 -2
  24. package/templates/agent-copilot.md +1 -2
  25. package/templates/agent-cursor.md +1 -2
  26. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +1 -1
  27. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +2 -3
  28. package/agent-assets/rules/descix_mcp.template.mdc +0 -243
  29. package/lib/commands/briefer/errors.js +0 -88
  30. package/lib/commands/briefer/index.js +0 -322
  31. package/lib/commands/briefer/sources/canonical-sources.js +0 -129
  32. package/lib/commands/briefer/sources/entitlements.js +0 -220
  33. package/lib/commands/briefer/sources/environments.js +0 -205
  34. package/lib/commands/briefer/sources/identifiers.js +0 -214
  35. package/lib/commands/briefer/sources/microservice-deploy.js +0 -267
  36. package/lib/commands/briefer/sources/routing.js +0 -275
  37. package/lib/commands/briefer/sources/what-is-not.js +0 -223
  38. package/lib/commands/briefer/stitcher.js +0 -134
  39. package/lib/commands/briefer/util/source-reader.js +0 -502
  40. package/lib/commands/mcp.js +0 -273
  41. package/lib/commands/update.js +0 -581
  42. package/lib/wizard/setup.js +0 -585
  43. package/templates/descix_mcp.mdc +0 -122
@@ -1,581 +0,0 @@
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 site # Deploy site to GCS
8
- * descix update all # Update everything
9
- *
10
- * Uses WorkspaceConfig as sole configuration methodology.
11
- * Folder structure follows convention:
12
- * {appPath}/assets/ - App assets (icon, description, system_instructions)
13
- * {appPath}/kb/{kbId}/ - Knowledge base files
14
- * {appPath}/site/ - Static site files
15
- * {appPath}/microservice/ - Microservice code
16
- */
17
-
18
- import chalk from 'chalk';
19
- import ora from 'ora';
20
- import * as fs from 'fs/promises';
21
- import * as path from 'path';
22
- import crypto from 'crypto';
23
- import { DeSciXApiClient } from '../api-client.js';
24
- import { requireAuth } from '../auth-guard.js';
25
- import { WorkspaceConfig } from '../workspace-config.js';
26
- import { refuseRetiredKbSync } from './retired-kb-sync.js';
27
-
28
- /**
29
- * Helper to load workspace context with options override
30
- * Unified Registry: app_id only required; community_id derived on backend
31
- * @param {Object} options - CLI options with community, app, kb
32
- * @returns {{ workspaceConfig, communityId, appId, kbId, appPath }}
33
- */
34
- async function loadWorkspaceContext(options = {}) {
35
- const workspaceConfig = await WorkspaceConfig.load();
36
- const ctx = workspaceConfig.requireContext(options);
37
-
38
- // Unified Registry: getAppByAppId only — v1 community-keyed fallback removed
39
- const appConfig = workspaceConfig.getAppByAppId(ctx.appId);
40
-
41
- if (!appConfig) {
42
- throw new Error(
43
- `App "${ctx.appId}" not found in workspace.json.\n` +
44
- 'Use `descix app init` to register, or `descix app set-localpath -a <id> -p <path>` to repoint.'
45
- );
46
- }
47
-
48
- const appPath = appConfig.absolutePath ||
49
- (appConfig.localPath && workspaceConfig.workspaceRoot
50
- ? path.join(workspaceConfig.workspaceRoot, appConfig.localPath)
51
- : null);
52
-
53
- if (!appPath) {
54
- throw new Error(`App path not configured for ${ctx.appId}`);
55
- }
56
-
57
- return {
58
- workspaceConfig,
59
- communityId: ctx.communityId || appConfig.communityId,
60
- appId: ctx.appId,
61
- kbId: ctx.kbId || appConfig.kbId || 'General',
62
- appPath
63
- };
64
- }
65
-
66
- /**
67
- * Compute SHA256 hash of file content
68
- */
69
- function computeHash(content) {
70
- return crypto.createHash('sha256').update(content).digest('hex');
71
- }
72
-
73
- /**
74
- * Read all files from a directory
75
- */
76
- async function readDirectoryFiles(dirPath) {
77
- const files = [];
78
-
79
- try {
80
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
81
-
82
- for (const entry of entries) {
83
- if (entry.name.startsWith('.')) continue; // Skip hidden files
84
-
85
- const fullPath = path.join(dirPath, entry.name);
86
-
87
- if (entry.isFile()) {
88
- const content = await fs.readFile(fullPath);
89
- files.push({
90
- name: entry.name,
91
- path: entry.name,
92
- content: content.toString('base64'),
93
- hash: computeHash(content),
94
- size: content.length
95
- });
96
- } else if (entry.isDirectory()) {
97
- // Recursive read for subdirectories
98
- const subFiles = await readDirectoryFilesRecursive(fullPath, entry.name);
99
- files.push(...subFiles);
100
- }
101
- }
102
- } catch (error) {
103
- if (error.code !== 'ENOENT') throw error;
104
- }
105
-
106
- return files;
107
- }
108
-
109
- /**
110
- * Recursively read files from directory
111
- */
112
- async function readDirectoryFilesRecursive(dirPath, basePath = '') {
113
- const files = [];
114
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
115
-
116
- for (const entry of entries) {
117
- if (entry.name.startsWith('.')) continue;
118
-
119
- const fullPath = path.join(dirPath, entry.name);
120
- const relativePath = path.join(basePath, entry.name);
121
-
122
- if (entry.isFile()) {
123
- const content = await fs.readFile(fullPath);
124
- files.push({
125
- name: entry.name,
126
- path: relativePath,
127
- content: content.toString('base64'),
128
- hash: computeHash(content),
129
- size: content.length
130
- });
131
- } else if (entry.isDirectory()) {
132
- const subFiles = await readDirectoryFilesRecursive(fullPath, relativePath);
133
- files.push(...subFiles);
134
- }
135
- }
136
-
137
- return files;
138
- }
139
-
140
- /**
141
- * Auto-detect and run appropriate update based on current directory
142
- */
143
- export async function updateAuto(options = {}) {
144
- const spinner = ora('Loading context...').start();
145
-
146
- try {
147
- const ctx = await loadWorkspaceContext(options);
148
- const cwd = process.cwd();
149
-
150
- // Detect what to sync based on current directory relative to app path
151
- const relativePath = path.relative(ctx.appPath, cwd);
152
- let syncType = 'app'; // default
153
-
154
- if (relativePath.startsWith('kb') || relativePath.startsWith('kb/')) {
155
- syncType = 'kb';
156
- } else if (relativePath.startsWith('site') || relativePath.startsWith('site/')) {
157
- syncType = 'site';
158
- } else if (relativePath.startsWith('assets') || relativePath.startsWith('assets/')) {
159
- syncType = 'app';
160
- } else if (relativePath.startsWith('microservice') || relativePath.startsWith('microservice/')) {
161
- syncType = 'microservice';
162
- }
163
-
164
- spinner.succeed(`Context loaded: ${ctx.communityId || '?'}/${ctx.appId}`);
165
- console.log(chalk.gray(` Auto-detected sync type: ${syncType}\n`));
166
-
167
- // Route to appropriate update function
168
- switch (syncType) {
169
- case 'app':
170
- return await updateApp(options);
171
- case 'kb':
172
- // kb sync has exactly ONE surface. Auto-detect lands here whenever the user is
173
- // standing in the app's kb/ directory, so this path is REACHED in normal use —
174
- // it is the single most likely way someone still tries to sync a KB via `update`.
175
- // Refuses through the one owner so this branch and the explicit `update kb`
176
- // branch cannot drift apart.
177
- spinner.stop();
178
- return refuseRetiredKbSync('descix update kb', chalk.red);
179
- case 'site':
180
- return await updateSite(options);
181
- case 'microservice':
182
- console.log(chalk.yellow(' Microservice deployment not supported via update. Use \'descix microservice deploy\'.\n'));
183
- return { success: false, reason: 'use_deploy' };
184
- default:
185
- console.log(chalk.yellow(` Unknown sync type: ${syncType}. Use 'descix update app/kb/site' explicitly.\n`));
186
- return { success: false, reason: 'unknown_sync_type' };
187
- }
188
-
189
- } catch (error) {
190
- spinner.fail('Update failed');
191
- console.error(chalk.red(`\nāŒ ${error.message}\n`));
192
- throw error;
193
- }
194
- }
195
-
196
- /**
197
- * Update app assets (icon, description, system_instructions)
198
- * Stage 1: Local → Drive (DIRECT using ADC - no backend)
199
- * Stage 2: Drive → GCS/Firestore (backend refresh_listing_assets)
200
- *
201
- * Convention: Assets are in {appPath}/assets/
202
- */
203
- export async function updateApp(options = {}) {
204
- const apiClient = new DeSciXApiClient();
205
- await requireAuth(apiClient);
206
-
207
- const spinner = ora('Loading context...').start();
208
-
209
- try {
210
- const ctx = await loadWorkspaceContext(options);
211
-
212
- spinner.text = 'Reading app assets...';
213
-
214
- // Convention: assets folder
215
- const assetsDir = path.join(ctx.appPath, 'assets');
216
-
217
- // Get list of local asset files
218
- let localFiles = [];
219
- try {
220
- const entries = await fs.readdir(assetsDir, { withFileTypes: true });
221
- for (const entry of entries) {
222
- if (entry.isFile() && !entry.name.startsWith('.')) {
223
- const fullPath = path.join(assetsDir, entry.name);
224
- const stats = await fs.stat(fullPath);
225
- localFiles.push({
226
- name: entry.name,
227
- path: fullPath,
228
- size: stats.size,
229
- modifiedTime: stats.mtime.toISOString()
230
- });
231
- }
232
- }
233
- } catch (error) {
234
- if (error.code === 'ENOENT') {
235
- spinner.succeed('No app assets to sync');
236
- console.log(chalk.gray(`\n Directory: ${assetsDir}`));
237
- console.log(chalk.gray(' No files found.\n'));
238
- return { success: true, synced: 0 };
239
- }
240
- throw error;
241
- }
242
-
243
- if (localFiles.length === 0) {
244
- spinner.succeed('No app assets to sync');
245
- console.log(chalk.gray(`\n Directory: ${assetsDir}`));
246
- console.log(chalk.gray(' No files found.\n'));
247
- return { success: true, synced: 0 };
248
- }
249
-
250
- spinner.succeed(`Found ${localFiles.length} files`);
251
-
252
- console.log(chalk.cyan('\nšŸ“¤ Updating App Assets\n'));
253
- console.log(chalk.gray(` Community: ${ctx.communityId}`));
254
- console.log(chalk.gray(` App: ${ctx.appId}`));
255
- console.log(chalk.gray(` Source: ${assetsDir}\n`));
256
-
257
- // Show files
258
- console.log(chalk.gray(' Files:'));
259
- localFiles.forEach(f => {
260
- console.log(chalk.gray(` • ${f.name} (${(f.size / 1024).toFixed(1)}KB)`));
261
- });
262
- console.log('');
263
-
264
- // Stage 1: Upload directly to Drive using ADC
265
- spinner.start('Stage 1: Uploading to Drive (direct)...');
266
-
267
- const googleStorageADC = await import('../google-storage-adc.js');
268
-
269
- // Get base folder ID from workspace config or user profile
270
- let baseFolderId = ctx.workspaceConfig.driveConfig?.base_folder_id;
271
-
272
- if (!baseFolderId) {
273
- // `get_my_profile` was a call to a verb NOTHING SERVES: it is absent from Cloud's command
274
- // registry and had zero references anywhere in DeSciX_Cloud, DeSciX_Core or DeSciX_Powch
275
- // outside this one call site (measured 2026-08-29). `get_folder_info` is the served,
276
- // AUTHENTICATED verb that owns this fact — registry.js maps it to appCommands.js, and for
277
- // entity_type 'user' it returns the caller's own base_folder_id as `folder_id`.
278
- const folderInfo = await apiClient.invoke('get_folder_info', { entity_type: 'user' });
279
- baseFolderId = folderInfo.message?.folder_id;
280
- }
281
-
282
- if (!baseFolderId) {
283
- throw new Error('No base Drive folder configured: set driveConfig.base_folder_id in .descix/workspace.json.');
284
- }
285
-
286
- // Navigate to assets folder: {base}/{community}/{app}/assets
287
- const assetsPath = `${ctx.communityId}/${ctx.appId}/assets`;
288
- let assetsFolderId = await googleStorageADC.findFolderByPath(baseFolderId, assetsPath);
289
-
290
- if (!assetsFolderId) {
291
- spinner.text = 'Stage 1: Creating folder structure...';
292
- assetsFolderId = await googleStorageADC.ensureFolderPath(baseFolderId, assetsPath);
293
- }
294
-
295
- // List Drive files for delta comparison
296
- const driveFiles = await googleStorageADC.listFolderWithMeta(assetsFolderId);
297
-
298
- // Compute delta
299
- const { toUpload, toUpdate, unchanged } = googleStorageADC.computeDelta(localFiles, driveFiles);
300
-
301
- // Upload changed files directly to Drive
302
- const uploaded = [];
303
- for (const file of [...toUpload, ...toUpdate]) {
304
- spinner.text = `Stage 1: Uploading ${file.name}...`;
305
- await googleStorageADC.uploadFileToDrive(file.path, file.name, assetsFolderId);
306
- uploaded.push(file.name);
307
- }
308
-
309
- spinner.succeed(`Stage 1: ${uploaded.length} files uploaded (${unchanged.length} unchanged)`);
310
-
311
- // Stage 2: Trigger backend to sync Drive → GCS/Firestore
312
- spinner.start('Stage 2: Syncing to platform...');
313
-
314
- const response = await apiClient.invoke('refresh_listing_assets', {
315
- community_id: ctx.communityId,
316
- app_id: ctx.appId
317
- });
318
-
319
- const result = response.message || response;
320
-
321
- spinner.succeed('Stage 2: Platform sync complete');
322
-
323
- // Summary
324
- if (uploaded.length > 0) {
325
- console.log(chalk.green('\n Uploaded to Drive:'));
326
- uploaded.forEach(f => console.log(chalk.green(` āœ“ ${f}`)));
327
- }
328
-
329
- if (unchanged.length > 0) {
330
- console.log(chalk.gray('\n Unchanged:'));
331
- unchanged.forEach(f => console.log(chalk.gray(` • ${f.name}`)));
332
- }
333
-
334
- if (result.icon_updated) {
335
- console.log(chalk.green('\n āœ“ Icon synced to platform'));
336
- }
337
- if (result.description_updated) {
338
- console.log(chalk.green(' āœ“ Description synced to platform'));
339
- }
340
- if (result.system_instructions_updated) {
341
- console.log(chalk.green(' āœ“ System instructions synced to platform'));
342
- }
343
-
344
- if (result.errors?.length > 0) {
345
- console.log(chalk.yellow('\n Errors:'));
346
- result.errors.forEach(e => console.log(chalk.yellow(` ⚠ ${e}`)));
347
- }
348
-
349
- console.log('');
350
- return { success: true, uploaded, unchanged: unchanged.map(f => f.name), ...result };
351
-
352
- } catch (error) {
353
- spinner.fail('Update failed');
354
- console.error(chalk.red(`\nāŒ ${error.message}\n`));
355
- throw error;
356
- }
357
- }
358
-
359
- // updateKB is DELETED. `update kb` refuses and names `descix kb corpus sync`; `update all`
360
- // and `update auto` no longer touch the KB. No exported symbol survives without a caller.
361
-
362
- /**
363
- * Update site (deploy to GCS)
364
- * Convention: Site files are in {appPath}/site/
365
- */
366
- export async function updateSite(options = {}) {
367
- const apiClient = new DeSciXApiClient();
368
- await requireAuth(apiClient);
369
-
370
- const spinner = ora('Loading context...').start();
371
-
372
- try {
373
- const ctx = await loadWorkspaceContext(options);
374
-
375
- // Convention: site folder path
376
- const siteDir = path.join(ctx.appPath, 'site');
377
-
378
- // Handle local port registration — setSitePort mutates the live env entry and saves
379
- if (options.port) {
380
- spinner.text = `Registering local port: ${options.port}`;
381
- try {
382
- await ctx.workspaceConfig.setSitePort(ctx.appId, options.port);
383
- console.log(chalk.green(`\nāœ… Local port ${options.port} registered in workspace.json`));
384
- } catch (err) {
385
- console.log(chalk.yellow(`\nāš ļø Could not register port: ${err.message}`));
386
- }
387
- }
388
-
389
- // Run build command if specified
390
- if (options.build) {
391
- spinner.text = `Running build: ${options.build}`;
392
- const { exec } = await import('child_process');
393
- const { promisify } = await import('util');
394
- const execAsync = promisify(exec);
395
-
396
- try {
397
- await execAsync(options.build, { cwd: siteDir });
398
- spinner.succeed('Build completed');
399
- } catch (error) {
400
- spinner.fail('Build failed');
401
- console.error(chalk.red(`\nāŒ ${error.message}\n`));
402
- return { success: false, reason: 'build_failed' };
403
- }
404
- }
405
-
406
- spinner.text = 'Reading site files...';
407
-
408
- // Import GitUtils for file hashing (same as site deploy command)
409
- const { GitUtils } = await import('@descix/sdk/integrations/git');
410
- const mime = (await import('mime-types')).default;
411
-
412
- const gitUtils = new GitUtils(ctx.appPath);
413
- const localFiles = await gitUtils.getFileHashes(siteDir);
414
-
415
- const fileList = Object.entries(localFiles).map(([filePath, info]) => ({
416
- path: filePath,
417
- hash: info.hash,
418
- size: info.size,
419
- content_type: mime.lookup(filePath) || 'application/octet-stream'
420
- }));
421
-
422
- spinner.succeed(`Found ${fileList.length} files`);
423
-
424
- if (fileList.length === 0) {
425
- console.log(chalk.yellow('\n No files found in site directory. Nothing to deploy.\n'));
426
- return { success: true, synced: 0 };
427
- }
428
-
429
- console.log(chalk.cyan('\nšŸ“¤ Deploying CodeSite\n'));
430
- console.log(chalk.gray(` Community: ${ctx.communityId}`));
431
- console.log(chalk.gray(` App: ${ctx.appId}`));
432
- console.log(chalk.gray(` Source: ${siteDir}`));
433
- if (options.preview) console.log(chalk.yellow(` Mode: PREVIEW`));
434
- console.log('');
435
-
436
- // Request deploy token
437
- spinner.start('Requesting deploy token...');
438
-
439
- const tokenResponse = await apiClient.invoke('get_site_deploy_token', {
440
- community_id: ctx.communityId,
441
- app_id: ctx.appId,
442
- files: fileList,
443
- preview: options.preview || false
444
- });
445
-
446
- const { signed_urls, upload_headers, existing_manifest, token_id, site_url } = tokenResponse.message;
447
-
448
- // Determine delta
449
- let filesToUpload = fileList;
450
-
451
- if (!options.full && existing_manifest) {
452
- const delta = gitUtils.compareSyncState(localFiles, existing_manifest);
453
- filesToUpload = fileList.filter(f =>
454
- delta.added.includes(f.path) || delta.modified.includes(f.path)
455
- );
456
- spinner.succeed(`Delta: ${delta.added.length} added, ${delta.modified.length} modified, ${delta.unchanged.length} unchanged`);
457
- } else {
458
- spinner.succeed('Full upload');
459
- }
460
-
461
- if (filesToUpload.length === 0) {
462
- console.log(chalk.green('\nāœ… Site already up to date!\n'));
463
- console.log(chalk.cyan(` URL: ${site_url}\n`));
464
- return { success: true, synced: 0, url: site_url };
465
- }
466
-
467
- // Upload files
468
- spinner.start(`Uploading ${filesToUpload.length} files...`);
469
-
470
- let uploadedCount = 0;
471
-
472
- for (const file of filesToUpload) {
473
- const signedUrl = signed_urls[file.path];
474
- if (!signedUrl) continue;
475
-
476
- const content = await fs.readFile(path.join(siteDir, file.path));
477
-
478
- // Canonical contract (WS-DEPLOY-HARDENING item 4): the server binds Cache-Control into
479
- // the v4 signed URL signature (generateSiteUploadUrls/siteAssetCacheControl). Ferry the
480
- // exact header bag the server minted — never re-derive the policy client-side, and never
481
- // send a header the signature doesn't expect (GCS returns 403 signature-mismatch).
482
- const headers = (upload_headers && upload_headers[file.path]) || { 'Content-Type': file.content_type };
483
- const response = await fetch(signedUrl, {
484
- method: 'PUT',
485
- headers,
486
- body: content
487
- });
488
-
489
- if (response.ok) uploadedCount++;
490
- }
491
-
492
- spinner.succeed(`Uploaded ${uploadedCount} files`);
493
-
494
- // Confirm deployment
495
- spinner.start('Confirming deployment...');
496
-
497
- const confirmResponse = await apiClient.invoke('confirm_site_deploy', {
498
- community_id: ctx.communityId,
499
- app_id: ctx.appId,
500
- token_id: token_id,
501
- manifest: {
502
- version: '1.0',
503
- app_id: ctx.appId,
504
- community_id: ctx.communityId,
505
- files: localFiles
506
- },
507
- preview: options.preview || false
508
- });
509
-
510
- const result = confirmResponse.message;
511
-
512
- spinner.succeed('Site deployed!');
513
-
514
- console.log(chalk.green(`\nāœ… CodeSite deployed successfully!\n`));
515
- console.log(chalk.cyan(` URL: ${result.site_url}`));
516
- console.log(chalk.gray(` Files: ${result.files_count}\n`));
517
-
518
- return { success: true, url: result.site_url, files: result.files_count };
519
-
520
- } catch (error) {
521
- spinner.fail('Deployment failed');
522
- console.error(chalk.red(`\nāŒ ${error.message}\n`));
523
- throw error;
524
- }
525
- }
526
-
527
- /**
528
- * Update all resources (assets, KB, site)
529
- * Convention-based: checks for existence of assets/, kb/, site/ folders
530
- */
531
- export async function updateAll(options = {}) {
532
- const spinner = ora('Loading context...').start();
533
-
534
- try {
535
- const ctx = await loadWorkspaceContext(options);
536
- spinner.succeed(`Context: ${ctx.communityId}/${ctx.appId}`);
537
-
538
- console.log(chalk.cyan('\nšŸ“¤ Updating All Resources\n'));
539
-
540
- const results = {};
541
-
542
- // Check for assets folder and update if exists
543
- const assetsDir = path.join(ctx.appPath, 'assets');
544
- try {
545
- await fs.access(assetsDir);
546
- console.log(chalk.white('─── App Assets ───\n'));
547
- results.app = await updateApp(options);
548
- } catch {
549
- // assets folder doesn't exist, skip
550
- }
551
-
552
- // Knowledge bases are NOT part of `update all`. The one KB sync surface is
553
- // `descix kb corpus sync` (git-manifest); this composite keeps doing app and site.
554
-
555
- // Check for site folder and update if exists
556
- const siteDir = path.join(ctx.appPath, 'site');
557
- try {
558
- await fs.access(siteDir);
559
- console.log(chalk.white('\n─── CodeSite ───\n'));
560
- results.site = await updateSite(options);
561
- } catch {
562
- // site folder doesn't exist, skip
563
- }
564
-
565
- console.log(chalk.green('\nāœ… All resources updated!\n'));
566
-
567
- return { success: true, results };
568
-
569
- } catch (error) {
570
- spinner.fail('Update failed');
571
- console.error(chalk.red(`\nāŒ ${error.message}\n`));
572
- throw error;
573
- }
574
- }
575
-
576
- export default {
577
- updateAuto,
578
- updateApp,
579
- updateSite,
580
- updateAll
581
- };