@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,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KB (Knowledge Base) Commands - SDK Architecture V2
|
|
3
|
+
*
|
|
4
|
+
* Commands for local KB processing using canonical core modules:
|
|
5
|
+
* - pull: Download from Drive, convert to text (Hydrator)
|
|
6
|
+
* - push: Upload staging files to Drive (Hydrator)
|
|
7
|
+
* - chunk: Generate chunks from text files (Chunker)
|
|
8
|
+
* - sync: Push chunks to Pinecone via API (Syncer)
|
|
9
|
+
* - build: Full pipeline (push staging ā pull ā chunk ā sync)
|
|
10
|
+
* - status: Show sync status
|
|
11
|
+
* - compare: Show file-level deltas
|
|
12
|
+
*
|
|
13
|
+
* Architecture:
|
|
14
|
+
* - Uses WorkspaceConfig for all path resolution (supports v2.1 env.platform/products format)
|
|
15
|
+
* - All operations delegate to lib/core/ modules
|
|
16
|
+
* - Supports interactive and unattended (agent) modes
|
|
17
|
+
* - CLI handles UX (prompts, progress, errors)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import chalk from 'chalk';
|
|
21
|
+
import ora from 'ora';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import * as fs from 'fs/promises';
|
|
24
|
+
import inquirer from 'inquirer';
|
|
25
|
+
import { WorkspaceConfig } from '../workspace-config.js';
|
|
26
|
+
import { hydrateKb, pushStaging, checkStagingFiles } from '../core/Hydrator.js';
|
|
27
|
+
import { processKb } from '../core/Chunker.js';
|
|
28
|
+
import { syncKb, getSyncStatus } from '../core/Syncer.js';
|
|
29
|
+
import * as driveADC from '../google-storage-adc.js';
|
|
30
|
+
|
|
31
|
+
// ============ Pull Command ============
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pull KB content from Drive and convert to local text
|
|
35
|
+
* Delegates to Hydrator.hydrateKb()
|
|
36
|
+
*
|
|
37
|
+
* @param {DeSciXApiClient|null} apiClient - API client (unused, kept for interface)
|
|
38
|
+
* @param {Object} options - Command options
|
|
39
|
+
* @param {string} options.community - Community ID
|
|
40
|
+
* @param {string} options.app - App ID
|
|
41
|
+
* @param {string} options.kb - KB ID
|
|
42
|
+
* @param {boolean} options.verbose - Verbose output
|
|
43
|
+
* @param {string} options.mergeMode - 'merge' | 'overwrite' | 'force-overwrite'
|
|
44
|
+
* @param {boolean} options.interactive - Enable interactive prompts
|
|
45
|
+
* @param {boolean} options.dryRun - Show what would happen
|
|
46
|
+
*/
|
|
47
|
+
export async function runKbPull(apiClient, options) {
|
|
48
|
+
const spinner = ora('Loading workspace configuration...').start();
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// 1. Load WorkspaceConfig (from workspace.json - no searching)
|
|
52
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
53
|
+
|
|
54
|
+
// 2. Resolve context (auto-detect from cwd or use CLI flags)
|
|
55
|
+
const { communityId, appId, kbId } = workspaceConfig.requireContext(options);
|
|
56
|
+
|
|
57
|
+
spinner.text = `Pulling KB: ${communityId}/${appId}/${kbId}`;
|
|
58
|
+
|
|
59
|
+
// 3. Validate Drive configuration
|
|
60
|
+
const driveConfig = workspaceConfig.driveConfig;
|
|
61
|
+
if (!driveConfig?.base_folder_id) {
|
|
62
|
+
spinner.fail('Drive not configured');
|
|
63
|
+
console.log(chalk.yellow('\nš” Run "descix setup --dev" first to link Drive.\n'));
|
|
64
|
+
throw new Error('The base_folder_id is required for KB operations.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. Get paths
|
|
68
|
+
const workspaceRoot = workspaceConfig.getWorkspaceRoot();
|
|
69
|
+
const appPath = workspaceConfig.getAppByAppId(appId)?.absolutePath;
|
|
70
|
+
|
|
71
|
+
// 5. Delegate to Hydrator with merge mode options
|
|
72
|
+
spinner.text = 'Connecting to Google Drive...';
|
|
73
|
+
|
|
74
|
+
const result = await hydrateKb({
|
|
75
|
+
workspaceRoot,
|
|
76
|
+
communityId,
|
|
77
|
+
appId,
|
|
78
|
+
kbId,
|
|
79
|
+
baseFolderId: driveConfig.base_folder_id,
|
|
80
|
+
localPath: appPath ? (path.relative(workspaceRoot, appPath) || '.') : `${communityId}/${appId}`
|
|
81
|
+
}, {
|
|
82
|
+
verbose: options.verbose,
|
|
83
|
+
mergeMode: options.mergeMode || 'merge',
|
|
84
|
+
dryRun: options.dryRun,
|
|
85
|
+
onProgress: (msg) => { spinner.text = msg; }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Build status message
|
|
89
|
+
const parts = [];
|
|
90
|
+
if (result.pulled > 0) parts.push(`${result.pulled} downloaded`);
|
|
91
|
+
if (result.converted > 0) parts.push(`${result.converted} converted`);
|
|
92
|
+
if (result.skipped > 0) parts.push(`${result.skipped} skipped`);
|
|
93
|
+
if (result.unchanged > 0) parts.push(`${result.unchanged} unchanged`);
|
|
94
|
+
|
|
95
|
+
const statusMsg = parts.length > 0 ? parts.join(', ') : 'No changes';
|
|
96
|
+
spinner.succeed(`Pull complete: ${statusMsg}`);
|
|
97
|
+
|
|
98
|
+
// Show next steps
|
|
99
|
+
if (!options.quiet) {
|
|
100
|
+
console.log(chalk.cyan('\nš Next steps:'));
|
|
101
|
+
console.log(chalk.gray(' 1. Review files in kb/' + kbId + '/'));
|
|
102
|
+
console.log(chalk.gray(' 2. Run "descix kb chunk" to generate chunks'));
|
|
103
|
+
console.log(chalk.gray(' 3. Run "descix kb sync" to push to Pinecone\n'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
spinner.fail('Pull failed');
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============ Push Command ============
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Push staging files to Drive
|
|
118
|
+
* Delegates to Hydrator.pushStaging()
|
|
119
|
+
*
|
|
120
|
+
* @param {DeSciXApiClient|null} apiClient - API client (unused)
|
|
121
|
+
* @param {Object} options - Command options
|
|
122
|
+
* @param {boolean} options.interactive - Enable interactive prompts for conflicts
|
|
123
|
+
* @param {string} options.onConflict - 'overwrite' | 'skip' (default: 'overwrite')
|
|
124
|
+
* @param {boolean} options.moveToProcessed - Move files to .processed (default: true)
|
|
125
|
+
* @param {boolean} options.dryRun - Show what would happen
|
|
126
|
+
*/
|
|
127
|
+
export async function runKbPush(apiClient, options) {
|
|
128
|
+
const spinner = ora('Loading workspace configuration...').start();
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// 1. Load WorkspaceConfig
|
|
132
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
133
|
+
|
|
134
|
+
// 2. Resolve context
|
|
135
|
+
const { communityId, appId, kbId } = workspaceConfig.requireContext(options);
|
|
136
|
+
|
|
137
|
+
spinner.text = `Pushing staging: ${communityId}/${appId}/${kbId}`;
|
|
138
|
+
|
|
139
|
+
// 3. Validate Drive configuration
|
|
140
|
+
const driveConfig = workspaceConfig.driveConfig;
|
|
141
|
+
if (!driveConfig?.base_folder_id) {
|
|
142
|
+
spinner.fail('Drive not configured');
|
|
143
|
+
throw new Error('Run "descix setup --dev" first to link Drive.');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 4. Get paths
|
|
147
|
+
const workspaceRoot = workspaceConfig.getWorkspaceRoot();
|
|
148
|
+
const appPath = workspaceConfig.getAppByAppId(appId)?.absolutePath;
|
|
149
|
+
const localPath = appPath ? (path.relative(workspaceRoot, appPath) || '.') : `${communityId}/${appId}`;
|
|
150
|
+
const stagingDir = path.join(workspaceRoot, localPath, 'kb', 'staging');
|
|
151
|
+
|
|
152
|
+
// Check if staging has files
|
|
153
|
+
const stagingCheck = await checkStagingFiles(stagingDir);
|
|
154
|
+
if (!stagingCheck.hasFiles) {
|
|
155
|
+
spinner.info('No files in staging directory');
|
|
156
|
+
return { uploaded: 0, skipped: 0, errors: 0, processed: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 5. Delegate to Hydrator
|
|
160
|
+
spinner.text = 'Uploading to Google Drive...';
|
|
161
|
+
|
|
162
|
+
// Create conflict prompt callback for interactive mode
|
|
163
|
+
const onConflictPrompt = options.interactive ? async (fileName, fileInfo) => {
|
|
164
|
+
spinner.stop();
|
|
165
|
+
const { action } = await inquirer.prompt([{
|
|
166
|
+
type: 'list',
|
|
167
|
+
name: 'action',
|
|
168
|
+
message: `File "${fileName}" exists in Drive. What would you like to do?`,
|
|
169
|
+
choices: [
|
|
170
|
+
{ name: 'Overwrite this file', value: 'overwrite' },
|
|
171
|
+
{ name: 'Overwrite all conflicts', value: 'overwrite-all' },
|
|
172
|
+
{ name: 'Skip this file', value: 'skip' },
|
|
173
|
+
{ name: 'Skip all conflicts', value: 'skip-all' }
|
|
174
|
+
]
|
|
175
|
+
}]);
|
|
176
|
+
spinner.start('Continuing upload...');
|
|
177
|
+
return action;
|
|
178
|
+
} : null;
|
|
179
|
+
|
|
180
|
+
const result = await pushStaging({
|
|
181
|
+
workspaceRoot,
|
|
182
|
+
communityId,
|
|
183
|
+
appId,
|
|
184
|
+
kbId,
|
|
185
|
+
baseFolderId: driveConfig.base_folder_id,
|
|
186
|
+
localPath
|
|
187
|
+
}, {
|
|
188
|
+
verbose: options.verbose,
|
|
189
|
+
interactive: options.interactive,
|
|
190
|
+
onConflict: options.onConflict || 'overwrite',
|
|
191
|
+
moveToProcessed: options.moveToProcessed !== false,
|
|
192
|
+
dryRun: options.dryRun,
|
|
193
|
+
onConflictPrompt
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Build status message
|
|
197
|
+
const parts = [];
|
|
198
|
+
if (result.uploaded > 0) parts.push(`${result.uploaded} uploaded`);
|
|
199
|
+
if (result.skipped > 0) parts.push(`${result.skipped} skipped`);
|
|
200
|
+
if (result.errors > 0) parts.push(`${result.errors} errors`);
|
|
201
|
+
|
|
202
|
+
const statusMsg = parts.length > 0 ? parts.join(', ') : 'No files processed';
|
|
203
|
+
spinner.succeed(`Push complete: ${statusMsg}`);
|
|
204
|
+
|
|
205
|
+
if (result.processed?.length > 0 && options.moveToProcessed !== false) {
|
|
206
|
+
console.log(chalk.gray(` Files moved to kb/staging/.processed/`));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Show next steps
|
|
210
|
+
if (!options.quiet) {
|
|
211
|
+
console.log(chalk.cyan('\nš Next steps:'));
|
|
212
|
+
console.log(chalk.gray(' 1. Run "descix kb pull" to sync all Drive content'));
|
|
213
|
+
console.log(chalk.gray(' 2. Run "descix kb chunk" to generate chunks\n'));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result;
|
|
217
|
+
|
|
218
|
+
} catch (error) {
|
|
219
|
+
spinner.fail('Push failed');
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ============ Chunk Command ============
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generate chunks from text files
|
|
228
|
+
* Delegates to Chunker.processKb()
|
|
229
|
+
*
|
|
230
|
+
* @param {Object} options - { community, app, kb, chunkSize, overlap, verbose }
|
|
231
|
+
*/
|
|
232
|
+
export async function runKbChunk(options) {
|
|
233
|
+
const spinner = ora('Loading workspace configuration...').start();
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
// 1. Load WorkspaceConfig
|
|
237
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
238
|
+
|
|
239
|
+
// 2. Resolve context
|
|
240
|
+
const { communityId, appId, kbId } = workspaceConfig.requireContext(options);
|
|
241
|
+
|
|
242
|
+
spinner.text = `Chunking KB: ${communityId}/${appId}/${kbId}`;
|
|
243
|
+
|
|
244
|
+
// 3. Get paths
|
|
245
|
+
const workspaceRoot = workspaceConfig.getWorkspaceRoot();
|
|
246
|
+
const appPath = workspaceConfig.getAppByAppId(appId)?.absolutePath;
|
|
247
|
+
|
|
248
|
+
// 4. Delegate to Chunker
|
|
249
|
+
const chunkOptions = {
|
|
250
|
+
maxChunkSize: parseInt(options.chunkSize || 2000),
|
|
251
|
+
overlapSize: parseInt(options.overlap || 500),
|
|
252
|
+
verbose: options.verbose
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (options.verbose) {
|
|
256
|
+
console.log(chalk.gray(` Chunk size: ${chunkOptions.maxChunkSize}`));
|
|
257
|
+
console.log(chalk.gray(` Overlap: ${chunkOptions.overlapSize}`));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const result = await processKb({
|
|
261
|
+
workspaceRoot,
|
|
262
|
+
communityId,
|
|
263
|
+
appId,
|
|
264
|
+
kbId,
|
|
265
|
+
localPath: appPath ? (path.relative(workspaceRoot, appPath) || '.') : `${communityId}/${appId}`
|
|
266
|
+
}, chunkOptions);
|
|
267
|
+
|
|
268
|
+
spinner.succeed(`Generated ${result.totalChunks} chunks from ${result.files} files`);
|
|
269
|
+
|
|
270
|
+
// Show next steps
|
|
271
|
+
if (!options.quiet) {
|
|
272
|
+
console.log(chalk.cyan('\nš Next steps:'));
|
|
273
|
+
console.log(chalk.gray(' Run "descix kb sync" to push chunks to Pinecone\n'));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return result;
|
|
277
|
+
|
|
278
|
+
} catch (error) {
|
|
279
|
+
spinner.fail('Chunk failed');
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============ Sync Command ============
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Sync chunks to Pinecone via API
|
|
288
|
+
* Delegates to Syncer.syncKb()
|
|
289
|
+
*
|
|
290
|
+
* @param {DeSciXApiClient} apiClient - API client for backend
|
|
291
|
+
* @param {Object} options - { community, app, kb, verbose }
|
|
292
|
+
*/
|
|
293
|
+
export async function runKbSync(apiClient, options) {
|
|
294
|
+
const spinner = ora('Loading workspace configuration...').start();
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
if (!apiClient) {
|
|
298
|
+
spinner.fail('Authentication required');
|
|
299
|
+
throw new Error('Run "descix login" first.');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 1. Load WorkspaceConfig
|
|
303
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
304
|
+
|
|
305
|
+
// 2. Resolve context
|
|
306
|
+
const { communityId, appId, kbId } = workspaceConfig.requireContext(options);
|
|
307
|
+
|
|
308
|
+
spinner.text = `Syncing KB: ${communityId}/${appId}/${kbId}`;
|
|
309
|
+
|
|
310
|
+
// 3. Get paths
|
|
311
|
+
const workspaceRoot = workspaceConfig.getWorkspaceRoot();
|
|
312
|
+
const appPath = workspaceConfig.getAppByAppId(appId)?.absolutePath;
|
|
313
|
+
|
|
314
|
+
// 4. Delegate to Syncer
|
|
315
|
+
const result = await syncKb(apiClient, {
|
|
316
|
+
workspaceRoot,
|
|
317
|
+
communityId,
|
|
318
|
+
appId,
|
|
319
|
+
kbId,
|
|
320
|
+
localPath: appPath ? (path.relative(workspaceRoot, appPath) || '.') : `${communityId}/${appId}`
|
|
321
|
+
}, {
|
|
322
|
+
verbose: options.verbose,
|
|
323
|
+
onProgress: (msg) => { spinner.text = msg; }
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (result.synced > 0 || result.deleted > 0) {
|
|
327
|
+
spinner.succeed(`Synced ${result.synced} chunks (${result.deleted} deleted)`);
|
|
328
|
+
} else {
|
|
329
|
+
spinner.succeed('Already in sync');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return result;
|
|
333
|
+
|
|
334
|
+
} catch (error) {
|
|
335
|
+
spinner.fail('Sync failed');
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ============ Build Command (Combined) ============
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Full KB build pipeline: [push staging] ā pull ā chunk ā sync
|
|
344
|
+
*
|
|
345
|
+
* Enhanced pipeline:
|
|
346
|
+
* 1. Check staging - if files exist, push to Drive first
|
|
347
|
+
* 2. Pull from Drive with merge mode
|
|
348
|
+
* 3. Chunk local files
|
|
349
|
+
* 4. Sync to Pinecone
|
|
350
|
+
*
|
|
351
|
+
* @param {DeSciXApiClient} apiClient - API client
|
|
352
|
+
* @param {Object} options - Combined options for all steps
|
|
353
|
+
* @param {boolean} options.interactive - Enable interactive prompts
|
|
354
|
+
* @param {boolean} options.skipStaging - Skip staging push step
|
|
355
|
+
* @param {string} options.mergeMode - 'merge' | 'overwrite' | 'force-overwrite'
|
|
356
|
+
* @param {boolean} options.dryRun - Show what would happen
|
|
357
|
+
*/
|
|
358
|
+
export async function runKbBuild(apiClient, options) {
|
|
359
|
+
const { interactive = false, skipStaging = false, dryRun = false } = options;
|
|
360
|
+
|
|
361
|
+
console.log(chalk.cyan('\n=== KB Build Pipeline ===\n'));
|
|
362
|
+
|
|
363
|
+
if (dryRun) {
|
|
364
|
+
console.log(chalk.yellow('DRY RUN MODE - No changes will be made\n'));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
// Load context once for all steps
|
|
369
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
370
|
+
const { communityId, appId, kbId } = workspaceConfig.requireContext(options);
|
|
371
|
+
const workspaceRoot = workspaceConfig.getWorkspaceRoot();
|
|
372
|
+
const appPath = workspaceConfig.getAppByAppId(appId)?.absolutePath;
|
|
373
|
+
const localPath = appPath ? (path.relative(workspaceRoot, appPath) || '.') : `${communityId}/${appId}`;
|
|
374
|
+
const stagingDir = path.join(workspaceRoot, localPath, 'kb', 'staging');
|
|
375
|
+
|
|
376
|
+
// Show which app we're building
|
|
377
|
+
console.log(chalk.white(`Target: ${chalk.cyan(communityId)}/${chalk.cyan(appId)}/${chalk.gray(kbId)}\n`));
|
|
378
|
+
|
|
379
|
+
let stepNum = 1;
|
|
380
|
+
const totalSteps = skipStaging ? 3 : 4;
|
|
381
|
+
|
|
382
|
+
// Step 1: Check and push staging (unless skipped)
|
|
383
|
+
if (!skipStaging) {
|
|
384
|
+
const stagingCheck = await checkStagingFiles(stagingDir);
|
|
385
|
+
|
|
386
|
+
if (stagingCheck.hasFiles) {
|
|
387
|
+
console.log(chalk.white(`[${stepNum}/${totalSteps}] Push staging files to Drive`));
|
|
388
|
+
console.log(chalk.gray(` Found ${stagingCheck.count} file(s) in staging:`));
|
|
389
|
+
stagingCheck.files.forEach(f => console.log(chalk.gray(` - ${f}`)));
|
|
390
|
+
console.log('');
|
|
391
|
+
|
|
392
|
+
await runKbPush(apiClient, {
|
|
393
|
+
...options,
|
|
394
|
+
quiet: true,
|
|
395
|
+
onConflict: interactive ? undefined : 'overwrite'
|
|
396
|
+
});
|
|
397
|
+
console.log('');
|
|
398
|
+
} else {
|
|
399
|
+
console.log(chalk.gray(`[${stepNum}/${totalSteps}] No staging files to push\n`));
|
|
400
|
+
}
|
|
401
|
+
stepNum++;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Step 2: Pull from Drive
|
|
405
|
+
console.log(chalk.white(`[${stepNum}/${totalSteps}] Pull from Drive`));
|
|
406
|
+
|
|
407
|
+
// After initial setup, default to merge mode to preserve local files
|
|
408
|
+
const mergeMode = options.mergeMode || 'merge';
|
|
409
|
+
if (mergeMode !== 'force-overwrite') {
|
|
410
|
+
console.log(chalk.gray(` Mode: ${mergeMode} (use --force-overwrite to replace all)\n`));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
await runKbPull(apiClient, {
|
|
414
|
+
...options,
|
|
415
|
+
quiet: true,
|
|
416
|
+
mergeMode
|
|
417
|
+
});
|
|
418
|
+
console.log('');
|
|
419
|
+
stepNum++;
|
|
420
|
+
|
|
421
|
+
// Step 3: Chunk
|
|
422
|
+
console.log(chalk.white(`[${stepNum}/${totalSteps}] Generate chunks`));
|
|
423
|
+
await runKbChunk({ ...options, quiet: true });
|
|
424
|
+
console.log('');
|
|
425
|
+
stepNum++;
|
|
426
|
+
|
|
427
|
+
// Step 4: Sync
|
|
428
|
+
console.log(chalk.white(`[${stepNum}/${totalSteps}] Sync to Pinecone`));
|
|
429
|
+
await runKbSync(apiClient, { ...options, quiet: true });
|
|
430
|
+
|
|
431
|
+
console.log(chalk.green('\nā
KB build complete!\n'));
|
|
432
|
+
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ============ Status Command ============
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Show KB sync status
|
|
443
|
+
*
|
|
444
|
+
* @param {DeSciXApiClient} apiClient - API client
|
|
445
|
+
* @param {Object} options - { community, app, kb }
|
|
446
|
+
*/
|
|
447
|
+
export async function runKbStatus(apiClient, options) {
|
|
448
|
+
const spinner = ora('Checking sync status...').start();
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
// Load WorkspaceConfig
|
|
452
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
453
|
+
const { communityId, appId, kbId } = workspaceConfig.requireContext(options);
|
|
454
|
+
|
|
455
|
+
const workspaceRoot = workspaceConfig.getWorkspaceRoot();
|
|
456
|
+
const appPath = workspaceConfig.getAppByAppId(appId)?.absolutePath;
|
|
457
|
+
|
|
458
|
+
const status = await getSyncStatus(apiClient, {
|
|
459
|
+
workspaceRoot,
|
|
460
|
+
communityId,
|
|
461
|
+
appId,
|
|
462
|
+
kbId,
|
|
463
|
+
localPath: appPath ? (path.relative(workspaceRoot, appPath) || '.') : `${communityId}/${appId}`
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
spinner.stop();
|
|
467
|
+
|
|
468
|
+
console.log(chalk.cyan(`\nš KB Status: ${communityId}/${appId}/${kbId}\n`));
|
|
469
|
+
console.log(chalk.white(` Local chunks: ${status.local}`));
|
|
470
|
+
console.log(chalk.white(` Remote chunks: ${status.remote}`));
|
|
471
|
+
|
|
472
|
+
if (status.inSync) {
|
|
473
|
+
console.log(chalk.green('\n ā In sync\n'));
|
|
474
|
+
} else {
|
|
475
|
+
console.log(chalk.yellow('\n ā Out of sync:'));
|
|
476
|
+
if (status.toUpload > 0) {
|
|
477
|
+
console.log(chalk.gray(` - ${status.toUpload} chunks to upload`));
|
|
478
|
+
}
|
|
479
|
+
if (status.toDelete > 0) {
|
|
480
|
+
console.log(chalk.gray(` - ${status.toDelete} chunks to delete`));
|
|
481
|
+
}
|
|
482
|
+
console.log(chalk.gray('\n Run "descix kb sync" to synchronize\n'));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return status;
|
|
486
|
+
|
|
487
|
+
} catch (error) {
|
|
488
|
+
spinner.fail('Status check failed');
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ============ Compare Command ============
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Show file-level deltas between local and Drive
|
|
497
|
+
*
|
|
498
|
+
* @param {DeSciXApiClient|null} apiClient - API client (unused)
|
|
499
|
+
* @param {Object} options - { community, app, kb, staging, local }
|
|
500
|
+
*/
|
|
501
|
+
export async function runKbCompare(apiClient, options) {
|
|
502
|
+
const spinner = ora('Loading workspace configuration...').start();
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
// Load WorkspaceConfig
|
|
506
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
507
|
+
const { communityId, appId, kbId } = workspaceConfig.requireContext(options);
|
|
508
|
+
|
|
509
|
+
const driveConfig = workspaceConfig.driveConfig;
|
|
510
|
+
if (!driveConfig?.base_folder_id) {
|
|
511
|
+
spinner.fail('Drive not configured');
|
|
512
|
+
throw new Error('Run "descix setup --dev" first to link Drive.');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const workspaceRoot = workspaceConfig.getWorkspaceRoot();
|
|
516
|
+
const appPath = workspaceConfig.getAppByAppId(appId)?.absolutePath;
|
|
517
|
+
const localPath = appPath ? (path.relative(workspaceRoot, appPath) || '.') : `${communityId}/${appId}`;
|
|
518
|
+
|
|
519
|
+
spinner.text = 'Connecting to Google Drive...';
|
|
520
|
+
|
|
521
|
+
await driveADC.verifyDriveAuth();
|
|
522
|
+
|
|
523
|
+
// Get Drive KB folder
|
|
524
|
+
const kbPath = `${communityId}/${appId}/kb/${kbId}`;
|
|
525
|
+
const kbDriveFolderId = await driveADC.findFolderByPath(driveConfig.base_folder_id, kbPath);
|
|
526
|
+
|
|
527
|
+
spinner.stop();
|
|
528
|
+
|
|
529
|
+
console.log(chalk.cyan(`\nš KB Compare: ${communityId}/${appId}/${kbId}\n`));
|
|
530
|
+
|
|
531
|
+
// Compare staging ā Drive
|
|
532
|
+
if (options.staging !== false) {
|
|
533
|
+
const stagingDir = path.join(workspaceRoot, localPath, 'kb', 'staging');
|
|
534
|
+
const stagingCheck = await checkStagingFiles(stagingDir);
|
|
535
|
+
|
|
536
|
+
console.log(chalk.white('Staging ā Drive:'));
|
|
537
|
+
|
|
538
|
+
if (!stagingCheck.hasFiles) {
|
|
539
|
+
console.log(chalk.gray(' (no files in staging)\n'));
|
|
540
|
+
} else {
|
|
541
|
+
// Get Drive files for comparison
|
|
542
|
+
const driveFiles = kbDriveFolderId
|
|
543
|
+
? await driveADC.listDriveFiles(kbDriveFolderId)
|
|
544
|
+
: [];
|
|
545
|
+
const driveFileNames = new Set(driveFiles.map(f => f.name.toLowerCase()));
|
|
546
|
+
|
|
547
|
+
for (const fileName of stagingCheck.files) {
|
|
548
|
+
const existsInDrive = driveFileNames.has(fileName.toLowerCase());
|
|
549
|
+
if (existsInDrive) {
|
|
550
|
+
console.log(chalk.yellow(` ~ ${fileName}`) + chalk.gray(' (would overwrite)'));
|
|
551
|
+
} else {
|
|
552
|
+
console.log(chalk.green(` + ${fileName}`) + chalk.gray(' (to upload)'));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
console.log('');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Compare local KB ā Drive
|
|
560
|
+
if (options.local !== false) {
|
|
561
|
+
const kbDir = path.join(workspaceRoot, localPath, 'kb', kbId);
|
|
562
|
+
|
|
563
|
+
console.log(chalk.white('Local KB ā Drive:'));
|
|
564
|
+
|
|
565
|
+
// Get local files
|
|
566
|
+
let localFiles = [];
|
|
567
|
+
try {
|
|
568
|
+
const entries = await fs.readdir(kbDir, { withFileTypes: true });
|
|
569
|
+
localFiles = entries
|
|
570
|
+
.filter(e => !e.isDirectory() && !e.name.startsWith('.'))
|
|
571
|
+
.map(e => e.name);
|
|
572
|
+
} catch {
|
|
573
|
+
// KB directory doesn't exist
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Get Drive files
|
|
577
|
+
const driveFiles = kbDriveFolderId
|
|
578
|
+
? await driveADC.listDriveFiles(kbDriveFolderId)
|
|
579
|
+
: [];
|
|
580
|
+
|
|
581
|
+
// Load metadata for hash comparison
|
|
582
|
+
let metadata = { files: {} };
|
|
583
|
+
try {
|
|
584
|
+
const metaPath = path.join(kbDir, '.descix_metadata.json');
|
|
585
|
+
const metaContent = await fs.readFile(metaPath, 'utf-8');
|
|
586
|
+
metadata = JSON.parse(metaContent);
|
|
587
|
+
} catch {
|
|
588
|
+
// No metadata
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const localSet = new Set(localFiles.map(f => f.toLowerCase()));
|
|
592
|
+
const driveSet = new Set(driveFiles.map(f => f.name.toLowerCase()));
|
|
593
|
+
|
|
594
|
+
let hasDeltas = false;
|
|
595
|
+
|
|
596
|
+
// Files in Drive but not local
|
|
597
|
+
for (const driveFile of driveFiles) {
|
|
598
|
+
const localName = driveFile.name.toLowerCase();
|
|
599
|
+
if (!localSet.has(localName)) {
|
|
600
|
+
console.log(chalk.cyan(` ā ${driveFile.name}`) + chalk.gray(' (Drive only)'));
|
|
601
|
+
hasDeltas = true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Files in local
|
|
606
|
+
for (const localFile of localFiles) {
|
|
607
|
+
const meta = metadata.files?.[localFile];
|
|
608
|
+
|
|
609
|
+
if (!driveSet.has(localFile.toLowerCase())) {
|
|
610
|
+
// Local only
|
|
611
|
+
console.log(chalk.magenta(` * ${localFile}`) + chalk.gray(' (local only)'));
|
|
612
|
+
hasDeltas = true;
|
|
613
|
+
} else if (meta?.drive_modified) {
|
|
614
|
+
// In both - check if in sync
|
|
615
|
+
const driveFile = driveFiles.find(f => f.name.toLowerCase() === localFile.toLowerCase());
|
|
616
|
+
if (driveFile && driveFile.modifiedTime !== meta.drive_modified) {
|
|
617
|
+
console.log(chalk.yellow(` ā» ${localFile}`) + chalk.gray(' (Drive updated)'));
|
|
618
|
+
hasDeltas = true;
|
|
619
|
+
} else {
|
|
620
|
+
console.log(chalk.green(` = ${localFile}`) + chalk.gray(' (in sync)'));
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
console.log(chalk.gray(` ? ${localFile}`) + chalk.gray(' (unknown sync state)'));
|
|
624
|
+
hasDeltas = true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!hasDeltas && localFiles.length === 0 && driveFiles.length === 0) {
|
|
629
|
+
console.log(chalk.gray(' (no files)\n'));
|
|
630
|
+
} else {
|
|
631
|
+
console.log('');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Legend
|
|
636
|
+
console.log(chalk.gray('Legend: + new ~ overwrite ā Drive only * local only = in sync ā» updated\n'));
|
|
637
|
+
|
|
638
|
+
return { communityId, appId, kbId };
|
|
639
|
+
|
|
640
|
+
} catch (error) {
|
|
641
|
+
spinner.fail('Compare failed');
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export default {
|
|
647
|
+
runKbPull,
|
|
648
|
+
runKbPush,
|
|
649
|
+
runKbChunk,
|
|
650
|
+
runKbSync,
|
|
651
|
+
runKbBuild,
|
|
652
|
+
runKbStatus,
|
|
653
|
+
runKbCompare
|
|
654
|
+
};
|