@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.
- package/README.md +1 -1
- package/agent-assets/guides/README.md +1 -1
- package/agent-assets/guides/dynamic-site-pattern.md +7 -16
- package/agent-assets/instructions/app-asset-validation.md +1 -1
- package/agent-assets/reference/architecture.md +20 -25
- package/agent-assets/reference/local-dev.md +1 -6
- package/agent-assets/reference/mcp-integration.md +15 -9
- package/agent-assets/reference/platform-api-consumer.md +11 -5
- package/agent-assets/reference/workspace-config.md +2 -2
- package/bin/descix.js +211 -688
- package/lib/command-visibility.js +213 -0
- package/lib/commands/corpus.js +2 -5
- package/lib/commands/kb.js +2 -2
- package/lib/commands/media-upload.js +176 -0
- package/lib/commands/retired-kb-sync.js +5 -27
- package/lib/commands/status.js +0 -9
- package/lib/output.js +31 -0
- package/lib/sdk-assets.js +28 -0
- package/lib/verb-invokes.generated.json +233 -0
- package/lib/workspace-config.js +9 -44
- package/package.json +6 -2
- package/templates/agent-claude.md +1 -3
- package/templates/agent-cline.md +1 -2
- package/templates/agent-copilot.md +1 -2
- package/templates/agent-cursor.md +1 -2
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +1 -1
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +2 -3
- package/agent-assets/rules/descix_mcp.template.mdc +0 -243
- package/lib/commands/briefer/errors.js +0 -88
- package/lib/commands/briefer/index.js +0 -322
- package/lib/commands/briefer/sources/canonical-sources.js +0 -129
- package/lib/commands/briefer/sources/entitlements.js +0 -220
- package/lib/commands/briefer/sources/environments.js +0 -205
- package/lib/commands/briefer/sources/identifiers.js +0 -214
- package/lib/commands/briefer/sources/microservice-deploy.js +0 -267
- package/lib/commands/briefer/sources/routing.js +0 -275
- package/lib/commands/briefer/sources/what-is-not.js +0 -223
- package/lib/commands/briefer/stitcher.js +0 -134
- package/lib/commands/briefer/util/source-reader.js +0 -502
- package/lib/commands/mcp.js +0 -273
- package/lib/commands/update.js +0 -581
- package/lib/wizard/setup.js +0 -585
- package/templates/descix_mcp.mdc +0 -122
package/lib/commands/update.js
DELETED
|
@@ -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
|
-
};
|