@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.
- package/README.md +95 -0
- package/SERVICE_README_sdk.md +399 -0
- package/agent-assets/guides/README.md +61 -0
- package/agent-assets/guides/dynamic-site-pattern.md +125 -0
- package/agent-assets/guides/knowledge-only-pattern.md +93 -0
- package/agent-assets/guides/microservice-pattern.md +152 -0
- package/agent-assets/guides/static-site-pattern.md +111 -0
- package/agent-assets/instructions/app-asset-validation.md +477 -0
- package/agent-assets/instructions/onboarding-flow.md +408 -0
- package/agent-assets/instructions/standard-folder-structure.md +125 -0
- package/agent-assets/reference/app-creation.md +381 -0
- package/agent-assets/reference/architecture.md +406 -0
- package/agent-assets/reference/local-dev.md +138 -0
- package/agent-assets/reference/mcp-integration.md +424 -0
- package/agent-assets/reference/workspace-config.md +415 -0
- package/agent-assets/rules/descix_mcp.template.mdc +243 -0
- package/bin/descix.js +3930 -0
- package/bin/mcp-server.js +382 -0
- package/descix-cli-1.0.0.tgz +0 -0
- package/lib/agent-files.js +162 -0
- package/lib/api-client.js +458 -0
- package/lib/auth-guard.js +67 -0
- package/lib/commands/app-wizard.js +627 -0
- package/lib/commands/auth.js +407 -0
- package/lib/commands/buy.js +207 -0
- package/lib/commands/clone.js +91 -0
- package/lib/commands/config.js +155 -0
- package/lib/commands/doctor.js +277 -0
- package/lib/commands/folder.js +291 -0
- package/lib/commands/init.js +257 -0
- package/lib/commands/kb.js +654 -0
- package/lib/commands/mcp.js +273 -0
- package/lib/commands/serve.js +12 -0
- package/lib/commands/status.js +239 -0
- package/lib/commands/update.js +653 -0
- package/lib/core/Chunker.js +610 -0
- package/lib/core/Hydrator.js +801 -0
- package/lib/core/PathContext.js +335 -0
- package/lib/core/Syncer.js +310 -0
- package/lib/core/index.js +32 -0
- package/lib/global-config.js +86 -0
- package/lib/google-storage-adc.js +511 -0
- package/lib/wallet-file.js +232 -0
- package/lib/wizard/setup.js +502 -0
- package/lib/workspace-config.js +712 -0
- package/lib/workspace-utils.js +126 -0
- package/package.json +41 -0
- package/templates/agent-claude.md +226 -0
- package/templates/agent-cline.md +133 -0
- package/templates/agent-copilot.md +133 -0
- package/templates/agent-cursor.md +133 -0
- package/templates/descix_mcp.mdc +122 -0
- package/templates/drive/README.md +42 -0
- package/templates/drive/app/assets/app_description.md +13 -0
- package/templates/drive/app/assets/icon.png +0 -0
- package/templates/drive/app/assets/system_instructions.md +17 -0
- package/templates/drive/app/kb/General/README.md +25 -0
- package/templates/drive/community/Apps/.gitkeep +4 -0
- package/templates/drive/community/community_assets/community_description.md +15 -0
- package/templates/drive/community/community_assets/icon.png +0 -0
- package/templates/drive_templates/README.md +104 -0
- package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
- package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
- package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
- package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
- package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
- package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
- package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
- package/templates/scaffolds/README.md +62 -0
- package/templates/scaffolds/microservice/Dockerfile +19 -0
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
- package/templates/scaffolds/microservice/app.js +50 -0
- package/templates/scaffolds/microservice/app.yaml +8 -0
- package/templates/scaffolds/microservice/defaults-config.json +5 -0
- package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
- package/templates/scaffolds/microservice/manifest.json +49 -0
- package/templates/scaffolds/microservice/package-lock.json +2645 -0
- package/templates/scaffolds/microservice/package.json +29 -0
- package/templates/scaffolds/microservice/scripts/register.js +55 -0
- package/templates/scaffolds/microservice/services/apiFront.js +75 -0
- package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
- package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
- package/templates/scaffolds/microservice/services/utils.js +247 -0
- package/templates/scaffolds/microservice/setup-schema.json +18 -0
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
- package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
- package/templates/scaffolds/site/README.md +51 -0
- package/templates/scaffolds/site/app.js +22 -0
- package/templates/scaffolds/site/index.html +42 -0
- package/templates/scaffolds/site/styles.css +123 -0
- package/test-hydration.js +150 -0
- package/vendor/mcp/transports/http-sse-transport.js +196 -0
- package/vendor/mcp/transports/index.js +15 -0
- 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
|
+
};
|