@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
package/bin/descix.js
ADDED
|
@@ -0,0 +1,3930 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DeSciX Unified CLI
|
|
5
|
+
*
|
|
6
|
+
* Production command-line interface for DeSciX platform.
|
|
7
|
+
* Combines CLI and MCP functionality with unified authentication.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { DeSciXApiClient } from '../lib/api-client.js';
|
|
13
|
+
import { requireAuth } from '../lib/auth-guard.js';
|
|
14
|
+
import { WorkspaceConfig } from '../lib/workspace-config.js';
|
|
15
|
+
import { GlobalConfig } from '../lib/global-config.js';
|
|
16
|
+
import * as authCommands from '../lib/commands/auth.js';
|
|
17
|
+
import * as configCommands from '../lib/commands/config.js';
|
|
18
|
+
import { runAppWizard } from '../lib/commands/app-wizard.js';
|
|
19
|
+
import * as buyCommands from '../lib/commands/buy.js';
|
|
20
|
+
import { runInit } from '../lib/commands/init.js';
|
|
21
|
+
import * as folderCommands from '../lib/commands/folder.js';
|
|
22
|
+
import * as updateCommands from '../lib/commands/update.js';
|
|
23
|
+
import { runStatus } from '../lib/commands/status.js';
|
|
24
|
+
import { runDoctor } from '../lib/commands/doctor.js';
|
|
25
|
+
import * as kbCommands from '../lib/commands/kb.js';
|
|
26
|
+
import fs from 'fs/promises';
|
|
27
|
+
import path from 'path';
|
|
28
|
+
import os from 'os';
|
|
29
|
+
|
|
30
|
+
const program = new Command();
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.name('descix')
|
|
34
|
+
.description('DeSciX CLI - Unified command-line interface')
|
|
35
|
+
.version('1.0.0');
|
|
36
|
+
|
|
37
|
+
// ============ Authentication Commands (No Auth Required) ============
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command('login')
|
|
42
|
+
.description('Authenticate with DeSciX via device login (opens browser)')
|
|
43
|
+
.option('-u, --url <url>', 'API URL override')
|
|
44
|
+
.option('--dev', 'Use development server (https://localhost:4000)')
|
|
45
|
+
.option('--wallet', 'Use direct wallet connection (advanced, not yet implemented)')
|
|
46
|
+
.action(async (options) => {
|
|
47
|
+
try {
|
|
48
|
+
// Handle --dev flag
|
|
49
|
+
if (options.dev) {
|
|
50
|
+
const workspaceConfig = await WorkspaceConfig.tryLoad(process.cwd());
|
|
51
|
+
if (workspaceConfig) {
|
|
52
|
+
workspaceConfig.apiUrl = 'https://localhost:4000';
|
|
53
|
+
workspaceConfig.environment = 'development';
|
|
54
|
+
await workspaceConfig.save(process.cwd());
|
|
55
|
+
} else {
|
|
56
|
+
const gc = await GlobalConfig.load();
|
|
57
|
+
gc.api_url = 'https://localhost:4000';
|
|
58
|
+
gc.environment = 'development';
|
|
59
|
+
await gc.save();
|
|
60
|
+
}
|
|
61
|
+
console.log(chalk.cyan('ā Configured for development (https://localhost:4000)\n'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (options.wallet) {
|
|
65
|
+
await authCommands.loginWallet();
|
|
66
|
+
} else {
|
|
67
|
+
await authCommands.loginDevice(options);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
program
|
|
75
|
+
.command('logout')
|
|
76
|
+
.description('Clear saved credentials')
|
|
77
|
+
.action(async () => {
|
|
78
|
+
try {
|
|
79
|
+
await authCommands.logout();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ============ Init Command (Git-aware workspace setup) ============
|
|
86
|
+
|
|
87
|
+
program
|
|
88
|
+
.command('init')
|
|
89
|
+
.description('Initialize workspace for DeSciX app development (Git-aware)')
|
|
90
|
+
.option('-c, --community <id>', 'Community ID')
|
|
91
|
+
.option('-a, --app <name>', 'App name')
|
|
92
|
+
.option('-p, --path <path>', 'Project path (defaults to current directory)')
|
|
93
|
+
.option('-f, --force', 'Overwrite existing workspace.json')
|
|
94
|
+
.option('--from-invite <token>', 'Resolve an invite token to pre-fill app context')
|
|
95
|
+
.action(async (options) => {
|
|
96
|
+
try {
|
|
97
|
+
// API client is optional for init (used for listing communities)
|
|
98
|
+
let apiClient = null;
|
|
99
|
+
try {
|
|
100
|
+
apiClient = new DeSciXApiClient();
|
|
101
|
+
await apiClient.loadCredentials();
|
|
102
|
+
} catch {
|
|
103
|
+
// Continue without API client
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await runInit(apiClient, options);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ============ Auth-Required Commands ============
|
|
114
|
+
|
|
115
|
+
program
|
|
116
|
+
.command('whoami')
|
|
117
|
+
.description('Show current authentication status')
|
|
118
|
+
.action(async () => {
|
|
119
|
+
try {
|
|
120
|
+
const apiClient = new DeSciXApiClient();
|
|
121
|
+
await requireAuth(apiClient);
|
|
122
|
+
await authCommands.whoami();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
program
|
|
129
|
+
.command('reconnect')
|
|
130
|
+
.description('Reconnect using saved wallet credentials')
|
|
131
|
+
.action(async () => {
|
|
132
|
+
try {
|
|
133
|
+
await authCommands.reconnect();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
program
|
|
140
|
+
.command('status')
|
|
141
|
+
.description('Show comprehensive status of developer environment')
|
|
142
|
+
.action(async () => {
|
|
143
|
+
try {
|
|
144
|
+
await runStatus();
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(chalk.red(`\nā Error: ${error.message}\n`));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
program
|
|
152
|
+
.command('doctor')
|
|
153
|
+
.description('Diagnose environment issues (Node, Auth, gcloud, ADC)')
|
|
154
|
+
.action(async () => {
|
|
155
|
+
try {
|
|
156
|
+
await runDoctor();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// runDoctor handles its own errors mostly, but catch-all here
|
|
159
|
+
console.error(chalk.red(`\nā Error: ${error.message}\n`));
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ============ Buy Commands (Crypto Payments) ============
|
|
165
|
+
|
|
166
|
+
const buyCommand = program
|
|
167
|
+
.command('buy')
|
|
168
|
+
.description('Crypto payment operations');
|
|
169
|
+
|
|
170
|
+
buyCommand
|
|
171
|
+
.command('quote')
|
|
172
|
+
.description('Create a crypto payment quote')
|
|
173
|
+
.requiredOption('-a, --amount <usd>', 'USD amount to purchase')
|
|
174
|
+
.requiredOption('-c, --chain <chain>', 'Blockchain to use (polygon, ethereum, bnb)')
|
|
175
|
+
.option('-t, --type <type>', 'Purchase type: usdcx or community_token', 'usdcx')
|
|
176
|
+
.option('-e, --email <email>', 'Email for claim link (guest mode)')
|
|
177
|
+
.option('--community <id>', 'Community ID for community token purchase')
|
|
178
|
+
.action(async (options) => {
|
|
179
|
+
try {
|
|
180
|
+
await buyCommands.createQuote(options);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
buyCommand
|
|
187
|
+
.command('status <quoteId>')
|
|
188
|
+
.description('Check status of a payment quote')
|
|
189
|
+
.action(async (quoteId) => {
|
|
190
|
+
try {
|
|
191
|
+
await buyCommands.checkStatus(quoteId);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
buyCommand
|
|
198
|
+
.command('poll <quoteId>')
|
|
199
|
+
.description('Poll for quote status until completed (with exponential backoff)')
|
|
200
|
+
.option('--max <attempts>', 'Maximum poll attempts', '120')
|
|
201
|
+
.option('--interval <ms>', 'Initial poll interval in ms', '5000')
|
|
202
|
+
.action(async (quoteId, options) => {
|
|
203
|
+
try {
|
|
204
|
+
await buyCommands.pollStatus(quoteId, {
|
|
205
|
+
maxAttempts: parseInt(options.max),
|
|
206
|
+
initialInterval: parseInt(options.interval)
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
buyCommand
|
|
214
|
+
.command('chains')
|
|
215
|
+
.description('List supported blockchains for payments')
|
|
216
|
+
.action(() => {
|
|
217
|
+
buyCommands.listChains();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ============ Sync Commands ============
|
|
221
|
+
|
|
222
|
+
const syncCommand = program
|
|
223
|
+
.command('sync')
|
|
224
|
+
.description('Content sync operations (context-aware)');
|
|
225
|
+
|
|
226
|
+
// Context-aware sync: assets
|
|
227
|
+
syncCommand
|
|
228
|
+
.command('assets')
|
|
229
|
+
.description('Sync app assets (icon, description, system_instructions) - context-aware')
|
|
230
|
+
.option('--push', 'Push local assets to Drive (default)')
|
|
231
|
+
.option('--pull', 'Pull assets from Drive to local')
|
|
232
|
+
.option('--status', 'Show sync status without syncing')
|
|
233
|
+
.option('-c, --community <id>', 'Community ID (uses context if not provided)')
|
|
234
|
+
.option('-a, --app <id>', 'App ID (uses context if not provided)')
|
|
235
|
+
.action(async (options) => {
|
|
236
|
+
try {
|
|
237
|
+
const apiClient = new DeSciXApiClient();
|
|
238
|
+
await requireAuth(apiClient);
|
|
239
|
+
|
|
240
|
+
// Load workspace context
|
|
241
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
242
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
243
|
+
|
|
244
|
+
const communityId = ctx.communityId;
|
|
245
|
+
const appId = ctx.appId;
|
|
246
|
+
|
|
247
|
+
if (!communityId || !appId) {
|
|
248
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
249
|
+
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory\n'));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (options.status) {
|
|
254
|
+
console.log(chalk.cyan(`\nš Asset Sync Status: ${communityId}/${appId}\n`));
|
|
255
|
+
// TODO: Implement status check
|
|
256
|
+
console.log(chalk.gray(' Status check not yet implemented for assets.\n'));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (options.pull) {
|
|
261
|
+
console.log(chalk.cyan(`\nš„ Pulling assets from Drive: ${communityId}/${appId}\n`));
|
|
262
|
+
// TODO: Implement pull
|
|
263
|
+
console.log(chalk.gray(' Pull not yet implemented. Use descix update app for push.\n'));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Default: push
|
|
268
|
+
await updateCommands.updateApp(options);
|
|
269
|
+
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Context-aware sync: kb
|
|
277
|
+
syncCommand
|
|
278
|
+
.command('kb')
|
|
279
|
+
.description('Sync knowledge base files - context-aware')
|
|
280
|
+
.argument('[stage]', 'Specific stage: stage1, stage2, stage3, all (default: all)')
|
|
281
|
+
.option('--push', 'Push local to Drive (Stage 1)')
|
|
282
|
+
.option('--pull', 'Pull from Drive to local (Stage 1)')
|
|
283
|
+
.option('--status', 'Show sync status')
|
|
284
|
+
.option('-c, --community <id>', 'Community ID (uses context if not provided)')
|
|
285
|
+
.option('-a, --app <id>', 'App ID (uses context if not provided)')
|
|
286
|
+
.option('-k, --kb <name>', 'KB name (default: General)')
|
|
287
|
+
.action(async (stage, options) => {
|
|
288
|
+
try {
|
|
289
|
+
const apiClient = new DeSciXApiClient();
|
|
290
|
+
await requireAuth(apiClient);
|
|
291
|
+
|
|
292
|
+
// Load workspace context
|
|
293
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
294
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
295
|
+
|
|
296
|
+
const communityId = ctx.communityId;
|
|
297
|
+
const appId = ctx.appId;
|
|
298
|
+
const kbId = options.kb || ctx.kbId || 'General';
|
|
299
|
+
|
|
300
|
+
if (!communityId || !appId) {
|
|
301
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
302
|
+
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory\n'));
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (options.status) {
|
|
307
|
+
console.log(chalk.cyan(`\nš KB Sync Status: ${communityId}/${appId}/${kbId}\n`));
|
|
308
|
+
// Call existing status logic
|
|
309
|
+
const stages = stage === 'stage1' ? [1] : stage === 'stage2' ? [2] : stage === 'stage3' ? [3] : [1, 2, 3];
|
|
310
|
+
|
|
311
|
+
for (const s of stages) {
|
|
312
|
+
if (s === 2) {
|
|
313
|
+
const response = await apiClient.invoke('get_drive_gcs_sync_status', {
|
|
314
|
+
community_id: communityId, app_id: appId, kb_id: kbId
|
|
315
|
+
});
|
|
316
|
+
const result = response.message || response;
|
|
317
|
+
console.log(chalk.yellow('Stage 2: Drive ā GCS'));
|
|
318
|
+
console.log(chalk.gray(` To Extract: ${result.to_extract || 0}`));
|
|
319
|
+
console.log(chalk.gray(` In Sync: ${result.in_sync || 0}\n`));
|
|
320
|
+
}
|
|
321
|
+
if (s === 3) {
|
|
322
|
+
const response = await apiClient.invoke('get_gcs_pinecone_sync_status', {
|
|
323
|
+
community_id: communityId, app_id: appId, kb_id: kbId
|
|
324
|
+
});
|
|
325
|
+
const result = response.message || response;
|
|
326
|
+
console.log(chalk.yellow('Stage 3: GCS ā Pinecone'));
|
|
327
|
+
console.log(chalk.gray(` To Vectorize: ${result.to_vectorize || 0}`));
|
|
328
|
+
console.log(chalk.gray(` Total Vectors: ${result.total_vectors || 0}\n`));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Determine what to sync
|
|
335
|
+
const stage1Only = stage === 'stage1' || options.push;
|
|
336
|
+
const stageToRun = stage || 'all';
|
|
337
|
+
|
|
338
|
+
console.log(chalk.cyan(`\nš¤ KB Sync: ${communityId}/${appId}/${kbId} (${stageToRun})\n`));
|
|
339
|
+
|
|
340
|
+
await updateCommands.updateKB({
|
|
341
|
+
...options,
|
|
342
|
+
kb: kbId,
|
|
343
|
+
stage1Only: stage1Only,
|
|
344
|
+
skipStage1: stage === 'stage2' || stage === 'stage3'
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Context-aware sync: site
|
|
354
|
+
syncCommand
|
|
355
|
+
.command('site')
|
|
356
|
+
.description('Sync CodeSite to GCS - context-aware')
|
|
357
|
+
.option('--push', 'Deploy to GCS (default)')
|
|
358
|
+
.option('--preview', 'Deploy to preview URL')
|
|
359
|
+
.option('--status', 'Show deployment status')
|
|
360
|
+
.option('--port <number>', 'Register local dev server port')
|
|
361
|
+
.option('-c, --community <id>', 'Community ID (uses context if not provided)')
|
|
362
|
+
.option('-a, --app <id>', 'App ID (uses context if not provided)')
|
|
363
|
+
.action(async (options) => {
|
|
364
|
+
try {
|
|
365
|
+
const apiClient = new DeSciXApiClient();
|
|
366
|
+
await requireAuth(apiClient);
|
|
367
|
+
|
|
368
|
+
// Load workspace context
|
|
369
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
370
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
371
|
+
|
|
372
|
+
const communityId = ctx.communityId;
|
|
373
|
+
const appId = ctx.appId;
|
|
374
|
+
|
|
375
|
+
if (!communityId || !appId) {
|
|
376
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
377
|
+
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory\n'));
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (options.status) {
|
|
382
|
+
console.log(chalk.cyan(`\nš Site Status: ${communityId}/${appId}\n`));
|
|
383
|
+
const response = await apiClient.invoke('get_site_manifest', {
|
|
384
|
+
community_id: communityId, app_id: appId
|
|
385
|
+
});
|
|
386
|
+
const result = response.message || response;
|
|
387
|
+
if (result.manifest) {
|
|
388
|
+
console.log(chalk.gray(` Files: ${Object.keys(result.manifest.files || {}).length}`));
|
|
389
|
+
console.log(chalk.gray(` URL: ${result.site_url}\n`));
|
|
390
|
+
} else {
|
|
391
|
+
console.log(chalk.gray(' No site deployed.\n'));
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Default: push
|
|
397
|
+
await updateCommands.updateSite({
|
|
398
|
+
...options,
|
|
399
|
+
preview: options.preview,
|
|
400
|
+
port: options.port ? parseInt(options.port) : undefined
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ============ Community/App Commands ============
|
|
410
|
+
|
|
411
|
+
const communityCommand = program
|
|
412
|
+
.command('community')
|
|
413
|
+
.description('Community operations');
|
|
414
|
+
|
|
415
|
+
communityCommand
|
|
416
|
+
.command('list')
|
|
417
|
+
.description('List all communities')
|
|
418
|
+
.action(async () => {
|
|
419
|
+
try {
|
|
420
|
+
const apiClient = new DeSciXApiClient();
|
|
421
|
+
await requireAuth(apiClient);
|
|
422
|
+
|
|
423
|
+
const response = await apiClient.invoke('find_communities', {}, { allowGuest: false });
|
|
424
|
+
const result = response.message || response;
|
|
425
|
+
const communities = result.communities || [];
|
|
426
|
+
|
|
427
|
+
console.log(chalk.green(`\nā
Found ${communities.length} communities\n`));
|
|
428
|
+
console.log(chalk.cyan('Communities:'));
|
|
429
|
+
communities.forEach((c, idx) => {
|
|
430
|
+
console.log(chalk.yellow(`${idx + 1}. ${c.community_name} (${c.community_id})`));
|
|
431
|
+
console.log(chalk.gray(` Token: ${c.token_symbol}`));
|
|
432
|
+
console.log(chalk.gray(` Description: ${c.community_description || 'N/A'}`));
|
|
433
|
+
console.log();
|
|
434
|
+
});
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error(chalk.red(error.message));
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
communityCommand
|
|
442
|
+
.command('info')
|
|
443
|
+
.description('Get community information')
|
|
444
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
445
|
+
.action(async (options) => {
|
|
446
|
+
try {
|
|
447
|
+
const apiClient = new DeSciXApiClient();
|
|
448
|
+
await requireAuth(apiClient);
|
|
449
|
+
|
|
450
|
+
const response = await apiClient.invoke('get_community', { community_id: options.community }, { allowGuest: false });
|
|
451
|
+
const result = response.message || response;
|
|
452
|
+
const community = result.community;
|
|
453
|
+
|
|
454
|
+
console.log(chalk.green('\nā
Community Information:\n'));
|
|
455
|
+
console.log(chalk.cyan(` Name: ${community.community_name}`));
|
|
456
|
+
console.log(chalk.gray(` ID: ${community.community_id}`));
|
|
457
|
+
console.log(chalk.gray(` Token: ${community.token_symbol}`));
|
|
458
|
+
console.log(chalk.gray(` Description: ${community.community_description || 'N/A'}\n`));
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error(chalk.red(error.message));
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
communityCommand
|
|
466
|
+
.command('check-token')
|
|
467
|
+
.description('Check if a token symbol is available')
|
|
468
|
+
.argument('<symbol>', 'Token symbol to check (1-7 chars)')
|
|
469
|
+
.action(async (symbol) => {
|
|
470
|
+
try {
|
|
471
|
+
const apiClient = new DeSciXApiClient();
|
|
472
|
+
await requireAuth(apiClient);
|
|
473
|
+
|
|
474
|
+
const tokenSymbol = symbol.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
475
|
+
if (tokenSymbol.length < 1 || tokenSymbol.length > 7) {
|
|
476
|
+
console.error(chalk.red('\nā Token symbol must be 1-7 alphanumeric characters\n'));
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
console.log(chalk.cyan(`\nš Checking token symbol '${tokenSymbol}'...\n`));
|
|
481
|
+
|
|
482
|
+
const response = await apiClient.invoke('check_token_symbol_available', { token_symbol: tokenSymbol });
|
|
483
|
+
const result = response.message || response;
|
|
484
|
+
|
|
485
|
+
if (result.available) {
|
|
486
|
+
console.log(chalk.green(`ā
Token symbol '${result.symbol}' is available!\n`));
|
|
487
|
+
} else {
|
|
488
|
+
console.log(chalk.red(`ā Token symbol '${result.symbol}' is NOT available\n`));
|
|
489
|
+
if (result.existing_contract) {
|
|
490
|
+
console.log(chalk.yellow(` Already used by contract: ${result.existing_contract}`));
|
|
491
|
+
}
|
|
492
|
+
if (result.existing_community) {
|
|
493
|
+
console.log(chalk.yellow(` Already used by community: ${result.existing_community}`));
|
|
494
|
+
}
|
|
495
|
+
console.log();
|
|
496
|
+
}
|
|
497
|
+
} catch (error) {
|
|
498
|
+
console.error(chalk.red(error.message));
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
communityCommand
|
|
504
|
+
.command('create')
|
|
505
|
+
.description('Community creation is via PWA (Workspace Config). Admins: use descix-admin community create')
|
|
506
|
+
.action(async () => {
|
|
507
|
+
console.log(chalk.cyan('\nš¦ Community creation\n'));
|
|
508
|
+
console.log(chalk.white('Create communities in the PWA (Workspace Config / Device Setup).'));
|
|
509
|
+
console.log(chalk.white('Platform admins can use: descix-admin community create -n "Name" -t SYMBOL\n'));
|
|
510
|
+
console.log(chalk.gray(' descix-admin uses the same workspace root and .descix/wallet.json as this CLI.\n'));
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Deploy a token contract without creating community
|
|
514
|
+
communityCommand
|
|
515
|
+
.command('deploy-token')
|
|
516
|
+
.description('Deploy a new token contract (without creating a community)')
|
|
517
|
+
.requiredOption('-t, --token <symbol>', 'Token symbol (e.g., POWCH)')
|
|
518
|
+
.requiredOption('-n, --name <name>', 'Token name (e.g., "Powch Token")')
|
|
519
|
+
.option('--treasury <address>', 'Treasury wallet address (defaults to platform wallet)')
|
|
520
|
+
.option('-c, --community <id>', 'Community ID to link the token to (optional)')
|
|
521
|
+
.action(async (options) => {
|
|
522
|
+
try {
|
|
523
|
+
const apiClient = new DeSciXApiClient();
|
|
524
|
+
await requireAuth(apiClient);
|
|
525
|
+
|
|
526
|
+
const tokenSymbol = options.token.toUpperCase();
|
|
527
|
+
const tokenName = options.name;
|
|
528
|
+
|
|
529
|
+
console.log(chalk.cyan('\nš¦ Deploying token contract...\n'));
|
|
530
|
+
console.log(chalk.gray(` Token Symbol: ${tokenSymbol}`));
|
|
531
|
+
console.log(chalk.gray(` Token Name: ${tokenName}`));
|
|
532
|
+
if (options.treasury) {
|
|
533
|
+
console.log(chalk.gray(` Treasury: ${options.treasury}`));
|
|
534
|
+
}
|
|
535
|
+
if (options.community) {
|
|
536
|
+
console.log(chalk.gray(` Community: ${options.community}`));
|
|
537
|
+
}
|
|
538
|
+
console.log();
|
|
539
|
+
|
|
540
|
+
console.log(chalk.yellow('ā³ This may take a few minutes while transactions confirm on-chain...\n'));
|
|
541
|
+
|
|
542
|
+
const response = await apiClient.invoke('deploy_contract', {
|
|
543
|
+
template_type: 'DAITA_v2',
|
|
544
|
+
token_symbol: tokenSymbol,
|
|
545
|
+
token_name: tokenName,
|
|
546
|
+
treasury_wallet: options.treasury || null,
|
|
547
|
+
community_id: options.community || null
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Handle response wrapping
|
|
551
|
+
const result = response.message || response;
|
|
552
|
+
|
|
553
|
+
if (result.status !== 'OK') {
|
|
554
|
+
throw new Error(result.message || 'Deployment failed');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const deployment = result.deployment;
|
|
558
|
+
|
|
559
|
+
console.log(chalk.green('ā
Token contract deployed successfully!\n'));
|
|
560
|
+
console.log(chalk.white('Contract Details:'));
|
|
561
|
+
console.log(chalk.gray(` Proxy Address: ${deployment.proxy_address}`));
|
|
562
|
+
console.log(chalk.gray(` Implementation: ${deployment.implementation_address}`));
|
|
563
|
+
console.log(chalk.gray(` Token Symbol: ${deployment.token_symbol}`));
|
|
564
|
+
console.log(chalk.gray(` Token Name: ${deployment.token_name}`));
|
|
565
|
+
console.log(chalk.gray(` Network: ${deployment.network} (chainId: ${deployment.chain_id})`));
|
|
566
|
+
console.log(chalk.gray(` TX Hash: ${deployment.tx_hash}`));
|
|
567
|
+
console.log();
|
|
568
|
+
|
|
569
|
+
if (deployment.network === 'polygon') {
|
|
570
|
+
console.log(chalk.cyan(`View on Polygonscan: https://polygonscan.com/address/${deployment.proxy_address}`));
|
|
571
|
+
} else if (deployment.network === 'amoy') {
|
|
572
|
+
console.log(chalk.cyan(`View on Amoy: https://amoy.polygonscan.com/address/${deployment.proxy_address}`));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (!options.community) {
|
|
576
|
+
console.log(chalk.yellow('\nNext steps:'));
|
|
577
|
+
console.log(chalk.gray(` 1. Create community: descix community create -n "Name" -t ${tokenSymbol} -p ./path --no-token-contract`));
|
|
578
|
+
console.log(chalk.gray(` 2. Link token: descix community link-token -c <community-id> -a ${deployment.proxy_address}`));
|
|
579
|
+
}
|
|
580
|
+
console.log();
|
|
581
|
+
|
|
582
|
+
} catch (error) {
|
|
583
|
+
console.error(chalk.red(`\nā Error: ${error.message}`));
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Link existing token contract to community
|
|
589
|
+
communityCommand
|
|
590
|
+
.command('link-token')
|
|
591
|
+
.description('Link an existing token contract to a community')
|
|
592
|
+
.requiredOption('-c, --community <id>', 'Community ID to link to')
|
|
593
|
+
.requiredOption('-a, --address <address>', 'Contract address to link')
|
|
594
|
+
.option('-t, --token <symbol>', 'Token symbol (optional, uses contract symbol if not provided)')
|
|
595
|
+
.action(async (options) => {
|
|
596
|
+
try {
|
|
597
|
+
const apiClient = new DeSciXApiClient();
|
|
598
|
+
await requireAuth(apiClient);
|
|
599
|
+
|
|
600
|
+
console.log(chalk.cyan('\nš Linking token contract to community...\n'));
|
|
601
|
+
console.log(chalk.gray(` Community ID: ${options.community}`));
|
|
602
|
+
console.log(chalk.gray(` Contract Address: ${options.address}`));
|
|
603
|
+
if (options.token) {
|
|
604
|
+
console.log(chalk.gray(` Token Symbol: ${options.token}`));
|
|
605
|
+
}
|
|
606
|
+
console.log();
|
|
607
|
+
|
|
608
|
+
const response = await apiClient.invoke('link_contract_to_community', {
|
|
609
|
+
community_id: options.community,
|
|
610
|
+
contract_address: options.address,
|
|
611
|
+
token_symbol: options.token || null
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// Handle response wrapping
|
|
615
|
+
const result = response.message || response;
|
|
616
|
+
|
|
617
|
+
if (result.status !== 'OK') {
|
|
618
|
+
throw new Error(result.message || 'Linking failed');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const link = result.link;
|
|
622
|
+
|
|
623
|
+
console.log(chalk.green('ā
Token contract linked successfully!\n'));
|
|
624
|
+
console.log(chalk.white('Link Details:'));
|
|
625
|
+
console.log(chalk.gray(` Community: ${link.community_name} (${link.community_id})`));
|
|
626
|
+
console.log(chalk.gray(` Contract: ${link.contract_address}`));
|
|
627
|
+
console.log(chalk.gray(` Token Symbol: ${link.token_symbol}`));
|
|
628
|
+
console.log(chalk.gray(` Contract Type: ${link.contract_type}`));
|
|
629
|
+
console.log(chalk.gray(` Network: ${link.network}`));
|
|
630
|
+
console.log();
|
|
631
|
+
|
|
632
|
+
} catch (error) {
|
|
633
|
+
console.error(chalk.red(`\nā Error: ${error.message}`));
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Get contract details
|
|
639
|
+
communityCommand
|
|
640
|
+
.command('contract')
|
|
641
|
+
.description('Get details about a deployed contract')
|
|
642
|
+
.option('-a, --address <address>', 'Contract address')
|
|
643
|
+
.option('-t, --token <symbol>', 'Token symbol')
|
|
644
|
+
.option('-c, --community <id>', 'Community ID')
|
|
645
|
+
.action(async (options) => {
|
|
646
|
+
try {
|
|
647
|
+
const apiClient = new DeSciXApiClient();
|
|
648
|
+
await requireAuth(apiClient);
|
|
649
|
+
|
|
650
|
+
if (!options.address && !options.token && !options.community) {
|
|
651
|
+
console.error(chalk.red('\nā Must provide --address, --token, or --community\n'));
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const response = await apiClient.invoke('get_contract_details', {
|
|
656
|
+
contract_address: options.address || null,
|
|
657
|
+
token_symbol: options.token || null,
|
|
658
|
+
community_id: options.community || null
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Handle response wrapping
|
|
662
|
+
const result = response.message || response;
|
|
663
|
+
|
|
664
|
+
if (result.status === 'NOT_FOUND') {
|
|
665
|
+
console.log(chalk.yellow('\nā Contract not found\n'));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (result.status !== 'OK' || !result.contract) {
|
|
670
|
+
throw new Error(result.message || 'Failed to get contract details');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const contract = result.contract;
|
|
674
|
+
|
|
675
|
+
console.log(chalk.cyan('\nš Contract Details\n'));
|
|
676
|
+
console.log(chalk.gray(` Address: ${contract.address}`));
|
|
677
|
+
if (contract.implementation_address) {
|
|
678
|
+
console.log(chalk.gray(` Implementation: ${contract.implementation_address}`));
|
|
679
|
+
}
|
|
680
|
+
console.log(chalk.gray(` Type: ${contract.contract_type}`));
|
|
681
|
+
console.log(chalk.gray(` ERC Spec: ${contract.erc_spec}`));
|
|
682
|
+
console.log(chalk.gray(` Token Symbol: ${contract.token_symbol}`));
|
|
683
|
+
console.log(chalk.gray(` Token Name: ${contract.token_name}`));
|
|
684
|
+
console.log(chalk.gray(` Chain: ${contract.chain} (${contract.chain_id})`));
|
|
685
|
+
if (contract.product_path) {
|
|
686
|
+
console.log(chalk.gray(` Linked To: ${contract.product_path}`));
|
|
687
|
+
}
|
|
688
|
+
console.log(chalk.gray(` Deployed At: ${contract.deployed_at || 'N/A'}`));
|
|
689
|
+
console.log(chalk.gray(` Deployed By: ${contract.deployed_by || 'N/A'}`));
|
|
690
|
+
console.log(chalk.gray(` TX Hash: ${contract.deployment_tx_hash || 'N/A'}`));
|
|
691
|
+
console.log();
|
|
692
|
+
|
|
693
|
+
if (contract.chain === 'polygon') {
|
|
694
|
+
console.log(chalk.cyan(`View on Polygonscan: https://polygonscan.com/address/${contract.address}`));
|
|
695
|
+
}
|
|
696
|
+
console.log();
|
|
697
|
+
|
|
698
|
+
} catch (error) {
|
|
699
|
+
console.error(chalk.red(`\nā Error: ${error.message}`));
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// ============ App Commands ============
|
|
705
|
+
|
|
706
|
+
const appCommand = program
|
|
707
|
+
.command('app')
|
|
708
|
+
.description('App management operations');
|
|
709
|
+
|
|
710
|
+
appCommand
|
|
711
|
+
.command('list')
|
|
712
|
+
.description('List apps you have access to (purchases + owned)')
|
|
713
|
+
.action(async () => {
|
|
714
|
+
try {
|
|
715
|
+
const apiClient = new DeSciXApiClient();
|
|
716
|
+
await requireAuth(apiClient);
|
|
717
|
+
const response = await apiClient.invoke('fetch_my_purchases', { product_type: 'APP' });
|
|
718
|
+
const apps = (response.message || response).apps || [];
|
|
719
|
+
if (apps.length === 0) {
|
|
720
|
+
console.log(chalk.yellow('\nNo apps found. Run bootstrap or purchase an app.\n'));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const idW = Math.max(6, ...apps.map(a => (a.app_id || '').length));
|
|
724
|
+
const comW = Math.max(9, ...apps.map(a => (a.community_id || '').length));
|
|
725
|
+
const nameW = Math.max(8, ...apps.map(a => (a.app_name || '').length));
|
|
726
|
+
console.log('\n' + chalk.bold(
|
|
727
|
+
'APP ID'.padEnd(idW + 2) + 'COMMUNITY'.padEnd(comW + 2) + 'APP NAME'
|
|
728
|
+
));
|
|
729
|
+
console.log('ā'.repeat(idW + comW + nameW + 4));
|
|
730
|
+
for (const app of apps) {
|
|
731
|
+
console.log(
|
|
732
|
+
chalk.cyan((app.app_id || '').padEnd(idW + 2)) +
|
|
733
|
+
chalk.gray((app.community_id || '').padEnd(comW + 2)) +
|
|
734
|
+
(app.app_name || '')
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
console.log();
|
|
738
|
+
} catch (error) {
|
|
739
|
+
console.error(chalk.red(error.message));
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
appCommand
|
|
745
|
+
.command('init')
|
|
746
|
+
.description('Initialize local workspace and Firestore KB for an app (idempotent)')
|
|
747
|
+
.requiredOption('-a, --app <app_id>', 'App ID (e.g. daita)')
|
|
748
|
+
.option('--kb <name>', 'Knowledge base name', 'General')
|
|
749
|
+
.option('-p, --path <dir>', 'Local app directory (default: auto-detected or cwd)')
|
|
750
|
+
.option('-c, --community <community_id>', 'Community to create app in (if not yet in Products registry)')
|
|
751
|
+
.action(async (options) => {
|
|
752
|
+
try {
|
|
753
|
+
const apiClient = new DeSciXApiClient();
|
|
754
|
+
await requireAuth(apiClient);
|
|
755
|
+
const appId = options.app;
|
|
756
|
+
const kbId = options.kb || 'General';
|
|
757
|
+
|
|
758
|
+
// Resolve community_id from Products registry
|
|
759
|
+
let communityId;
|
|
760
|
+
try {
|
|
761
|
+
const productCtx = await apiClient.invoke('get_product_context', { app_id: appId });
|
|
762
|
+
communityId = (productCtx.message || productCtx).community_id;
|
|
763
|
+
} catch (e) {
|
|
764
|
+
// Product not found ā fall through to creation below
|
|
765
|
+
}
|
|
766
|
+
// Product missing or returned null community_id ā create if --community was provided
|
|
767
|
+
if (!communityId) {
|
|
768
|
+
if (options.community) {
|
|
769
|
+
console.log(chalk.gray(` App '${appId}' not in Products registry. Creating in community '${options.community}'...`));
|
|
770
|
+
await apiClient.invoke('create_app_for_community', {
|
|
771
|
+
community_id: options.community,
|
|
772
|
+
app_name: appId,
|
|
773
|
+
});
|
|
774
|
+
communityId = options.community;
|
|
775
|
+
console.log(chalk.green(` ā Created App + Products + Purchase for ${appId}`));
|
|
776
|
+
} else {
|
|
777
|
+
throw new Error(`App '${appId}' not found in Products registry. Use --community to create it.`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 1. Workspace.json ā register app if not already mapped
|
|
782
|
+
const workspaceConfig = await WorkspaceConfig.tryLoad();
|
|
783
|
+
const alreadyMapped = workspaceConfig?.getAppByAppId(appId);
|
|
784
|
+
let appPath = alreadyMapped?.absolutePath;
|
|
785
|
+
|
|
786
|
+
if (!alreadyMapped) {
|
|
787
|
+
const localPath = options.path || '.';
|
|
788
|
+
const wsRoot = workspaceConfig?.workspaceRoot || process.cwd();
|
|
789
|
+
const cfg = workspaceConfig || new WorkspaceConfig({}, wsRoot);
|
|
790
|
+
cfg.registerApp(communityId, appId, { localPath, kbId });
|
|
791
|
+
await cfg.save(wsRoot);
|
|
792
|
+
appPath = path.resolve(wsRoot, localPath);
|
|
793
|
+
console.log(chalk.gray(` workspace.json updated: ${appId} ā ${localPath}`));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// 2. Create rigid app folder structure (site, kb, microservice)
|
|
797
|
+
if (appPath) {
|
|
798
|
+
const siteDir = path.join(appPath, 'site');
|
|
799
|
+
const kbDir = path.join(appPath, 'kb', kbId);
|
|
800
|
+
const msDir = path.join(appPath, 'microservice');
|
|
801
|
+
await fs.mkdir(siteDir, { recursive: true });
|
|
802
|
+
await fs.mkdir(kbDir, { recursive: true });
|
|
803
|
+
await fs.mkdir(msDir, { recursive: true });
|
|
804
|
+
console.log(chalk.gray(` Created: site/, kb/${kbId}/, microservice/`));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// 3. Create KnowledgeBase Firestore doc (Git Mode ā no Drive required)
|
|
808
|
+
const kbResponse = await apiClient.invoke('init_git_mode_kb', { app_id: appId, kb_name: kbId });
|
|
809
|
+
const kbResult = kbResponse.message || kbResponse;
|
|
810
|
+
|
|
811
|
+
console.log(chalk.green(`\nā ${appId} initialized`));
|
|
812
|
+
console.log(chalk.gray(` Community: ${communityId}`));
|
|
813
|
+
console.log(chalk.gray(` KB: ${kbId} ā ${kbResult.created ? 'created' : 'already exists'}\n`));
|
|
814
|
+
console.log(chalk.cyan('Next steps:'));
|
|
815
|
+
console.log(chalk.gray(` Add markdown files to kb/${kbId}/ then run:`));
|
|
816
|
+
console.log(chalk.white(` descix update kb -a ${appId}\n`));
|
|
817
|
+
} catch (error) {
|
|
818
|
+
console.error(chalk.red(error.message));
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
appCommand
|
|
824
|
+
.command('register-folder')
|
|
825
|
+
.description('Register your base Drive folder (must be shared with dip@descix.net)')
|
|
826
|
+
.option('-u, --url <folderUrl>', 'Drive folder URL or ID')
|
|
827
|
+
.action(async (options) => {
|
|
828
|
+
try {
|
|
829
|
+
const apiClient = new DeSciXApiClient();
|
|
830
|
+
await requireAuth(apiClient);
|
|
831
|
+
|
|
832
|
+
if (!options.url) {
|
|
833
|
+
console.error(chalk.red('Error: Folder URL or ID required. Use -u or --url'));
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const response = await apiClient.invoke('register_base_folder', {
|
|
838
|
+
folder_url: options.url
|
|
839
|
+
});
|
|
840
|
+
const result = response.message || response;
|
|
841
|
+
|
|
842
|
+
console.log(chalk.green('\nā
Folder registered successfully!\n'));
|
|
843
|
+
console.log(chalk.cyan(` Folder ID: ${result.folder_id}`));
|
|
844
|
+
console.log(chalk.gray(` Folder Name: ${result.folder_name}\n`));
|
|
845
|
+
} catch (error) {
|
|
846
|
+
console.error(chalk.red(error.message));
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
appCommand
|
|
852
|
+
.command('create')
|
|
853
|
+
.description('Create an app in a community. --quick with -c/-a for CLI-driven creation.')
|
|
854
|
+
.option('-c, --community <id>', 'Community ID')
|
|
855
|
+
.option('-a, --app <name>', 'App name')
|
|
856
|
+
.option('--quick', 'Create app via CLI (registers in Products, creates Drive skeleton); requires -c and -a')
|
|
857
|
+
.option('--overwrite', 'Overwrite existing (with --quick)')
|
|
858
|
+
.action(async (options) => {
|
|
859
|
+
try {
|
|
860
|
+
if (options.quick && options.community && options.app) {
|
|
861
|
+
const apiClient = new DeSciXApiClient();
|
|
862
|
+
await requireAuth(apiClient);
|
|
863
|
+
|
|
864
|
+
// Ensure Drive base folder is registered in workspace.json
|
|
865
|
+
let wsConfig;
|
|
866
|
+
try {
|
|
867
|
+
wsConfig = await WorkspaceConfig.load(process.cwd());
|
|
868
|
+
} catch {
|
|
869
|
+
console.error(chalk.red('\nā No workspace found. Run `descix init` first.\n'));
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
const baseFolderId = wsConfig.driveConfig?.base_folder_id;
|
|
873
|
+
|
|
874
|
+
if (!baseFolderId) {
|
|
875
|
+
console.log(chalk.yellow('\nā No Drive base folder registered in workspace.json.'));
|
|
876
|
+
console.log(chalk.white(' App creation requires a shared Drive folder for KB and asset storage.'));
|
|
877
|
+
console.log(chalk.white(' Share a Google Drive folder with dip@descix.net, then provide its URL.\n'));
|
|
878
|
+
|
|
879
|
+
const readline = await import('readline');
|
|
880
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
881
|
+
const folderUrl = await new Promise((resolve) => {
|
|
882
|
+
rl.question(chalk.cyan(' Drive folder URL or ID: '), (answer) => {
|
|
883
|
+
rl.close();
|
|
884
|
+
resolve(answer.trim());
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
if (!folderUrl) {
|
|
889
|
+
console.error(chalk.red('\nā Drive folder is required for app creation.\n'));
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Register the folder via backend (validates sharing + extracts folder ID)
|
|
894
|
+
const regResponse = await apiClient.invoke('register_base_folder', {
|
|
895
|
+
folder_url: folderUrl
|
|
896
|
+
});
|
|
897
|
+
const regResult = regResponse.message || regResponse;
|
|
898
|
+
console.log(chalk.green(` ā Folder registered: ${regResult.folder_name} (${regResult.folder_id})`));
|
|
899
|
+
|
|
900
|
+
// Persist to workspace.json
|
|
901
|
+
wsConfig.driveConfig = { base_folder_id: regResult.folder_id };
|
|
902
|
+
await wsConfig.save();
|
|
903
|
+
console.log(chalk.green(' ā Saved to workspace.json\n'));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const response = await apiClient.invoke('create_app_for_community', {
|
|
907
|
+
community_id: options.community,
|
|
908
|
+
app_name: options.app,
|
|
909
|
+
create_skeleton: true,
|
|
910
|
+
overwrite: options.overwrite
|
|
911
|
+
});
|
|
912
|
+
const result = response.message || response;
|
|
913
|
+
console.log(chalk.green('\nā
App created successfully!\n'));
|
|
914
|
+
console.log(chalk.cyan(` App ID: ${result.app_id}`));
|
|
915
|
+
console.log(chalk.cyan(` Community: ${result.community_id}`));
|
|
916
|
+
console.log(chalk.gray(` Products: Products/${result.app_id}`));
|
|
917
|
+
console.log(chalk.gray(` Firestore: Community/${result.community_id}/Apps/${result.app_id}`));
|
|
918
|
+
if (result.skeleton_result) {
|
|
919
|
+
console.log(chalk.gray(` Drive: ${result.skeleton_result.folder_id || 'created'}`));
|
|
920
|
+
}
|
|
921
|
+
console.log(chalk.cyan('\n Next: descix app init -a ' + result.app_id + '\n'));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
console.log(chalk.cyan('\nš¦ App creation\n'));
|
|
925
|
+
console.log(chalk.white('Create apps in the PWA (Device Setup / App Manager) or via CLI:\n'));
|
|
926
|
+
console.log(chalk.white(' descix app create --quick -c <community> -a <app-name>\n'));
|
|
927
|
+
console.log(chalk.gray('This registers the app in the Products registry, creates Firestore'));
|
|
928
|
+
console.log(chalk.gray('documents, grants entitlement, and creates Drive skeleton.\n'));
|
|
929
|
+
} catch (error) {
|
|
930
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
appCommand
|
|
936
|
+
.command('upload')
|
|
937
|
+
.description('Upload local files to app knowledge base')
|
|
938
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
939
|
+
.requiredOption('-a, --app <id>', 'App ID')
|
|
940
|
+
.requiredOption('-p, --path <localPath>', 'Local folder path')
|
|
941
|
+
.option('-k, --kb <id>', 'Knowledge Base ID', 'General')
|
|
942
|
+
.action(async (options) => {
|
|
943
|
+
try {
|
|
944
|
+
const apiClient = new DeSciXApiClient();
|
|
945
|
+
await requireAuth(apiClient);
|
|
946
|
+
|
|
947
|
+
const fs = await import('fs/promises');
|
|
948
|
+
const path = await import('path');
|
|
949
|
+
|
|
950
|
+
// Read files from local path
|
|
951
|
+
const localPath = options.path;
|
|
952
|
+
const stats = await fs.stat(localPath);
|
|
953
|
+
if (!stats.isDirectory()) {
|
|
954
|
+
throw new Error(`Path is not a directory: ${localPath}`);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
console.log(chalk.gray(`Reading files from ${localPath}...`));
|
|
958
|
+
|
|
959
|
+
// Recursively read files
|
|
960
|
+
const files = [];
|
|
961
|
+
async function readDir(dirPath, basePath = '') {
|
|
962
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
963
|
+
for (const entry of entries) {
|
|
964
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
965
|
+
const relPath = path.join(basePath, entry.name);
|
|
966
|
+
|
|
967
|
+
if (entry.isDirectory()) {
|
|
968
|
+
await readDir(fullPath, relPath);
|
|
969
|
+
} else if (entry.isFile()) {
|
|
970
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
971
|
+
files.push({
|
|
972
|
+
name: entry.name,
|
|
973
|
+
path: relPath,
|
|
974
|
+
content: Buffer.from(content).toString('base64') // Base64 encode
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
await readDir(localPath);
|
|
981
|
+
|
|
982
|
+
if (files.length === 0) {
|
|
983
|
+
throw new Error('No files found in the specified directory');
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
console.log(chalk.gray(`Found ${files.length} files. Uploading...`));
|
|
987
|
+
|
|
988
|
+
const response = await apiClient.invoke('upload_files_to_kb', {
|
|
989
|
+
community_id: options.community,
|
|
990
|
+
app_id: options.app,
|
|
991
|
+
kb_id: options.kb,
|
|
992
|
+
files: files
|
|
993
|
+
});
|
|
994
|
+
const result = response.message || response;
|
|
995
|
+
|
|
996
|
+
console.log(chalk.green('\nā
Files uploaded successfully!\n'));
|
|
997
|
+
console.log(chalk.cyan(` Uploaded: ${result.uploaded || 0} files`));
|
|
998
|
+
console.log(chalk.gray(` Total Vectors: ${result.totalVectors || 0}\n`));
|
|
999
|
+
|
|
1000
|
+
if (result.errors && result.errors.length > 0) {
|
|
1001
|
+
console.log(chalk.yellow(' Errors:'));
|
|
1002
|
+
result.errors.forEach(err => {
|
|
1003
|
+
console.log(chalk.red(` - ${err.file}: ${err.error}`));
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
console.error(chalk.red(error.message));
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
appCommand
|
|
1013
|
+
.command('upload-tree')
|
|
1014
|
+
.description('Upload folder tree with subfolders as KBs')
|
|
1015
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
1016
|
+
.requiredOption('-a, --app <id>', 'App ID')
|
|
1017
|
+
.requiredOption('-p, --path <localPath>', 'Local folder path')
|
|
1018
|
+
.option('--confirm', 'Confirm each subfolder as KB')
|
|
1019
|
+
.option('--dry-run', 'Show what would be uploaded without uploading')
|
|
1020
|
+
.action(async (options) => {
|
|
1021
|
+
try {
|
|
1022
|
+
const apiClient = new DeSciXApiClient();
|
|
1023
|
+
await requireAuth(apiClient);
|
|
1024
|
+
|
|
1025
|
+
const fs = await import('fs/promises');
|
|
1026
|
+
const path = await import('path');
|
|
1027
|
+
const readline = await import('readline');
|
|
1028
|
+
|
|
1029
|
+
const localPath = options.path;
|
|
1030
|
+
const stats = await fs.stat(localPath);
|
|
1031
|
+
if (!stats.isDirectory()) {
|
|
1032
|
+
throw new Error(`Path is not a directory: ${localPath}`);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
console.log(chalk.gray(`Scanning ${localPath}...\n`));
|
|
1036
|
+
|
|
1037
|
+
// Find top-level subdirectories
|
|
1038
|
+
const entries = await fs.readdir(localPath, { withFileTypes: true });
|
|
1039
|
+
const subdirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.'));
|
|
1040
|
+
const rootFiles = entries.filter(e => e.isFile() && !e.name.startsWith('.'));
|
|
1041
|
+
|
|
1042
|
+
if (subdirs.length === 0) {
|
|
1043
|
+
throw new Error('No subdirectories found. Use "app upload" for single KB upload.');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
console.log(chalk.cyan(`Found ${subdirs.length} subdirectories:\n`));
|
|
1047
|
+
|
|
1048
|
+
// Count files in each subdirectory
|
|
1049
|
+
const subdirInfo = [];
|
|
1050
|
+
for (const dir of subdirs) {
|
|
1051
|
+
const dirPath = path.join(localPath, dir.name);
|
|
1052
|
+
const files = await readDirectoryFiles(dirPath);
|
|
1053
|
+
subdirInfo.push({
|
|
1054
|
+
name: dir.name,
|
|
1055
|
+
path: dirPath,
|
|
1056
|
+
fileCount: files.length
|
|
1057
|
+
});
|
|
1058
|
+
console.log(chalk.gray(` [${subdirInfo.length}] ${dir.name}/ (${files.length} files)`));
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (rootFiles.length > 0) {
|
|
1062
|
+
console.log(chalk.yellow(`\n Note: ${rootFiles.length} root-level files will go to "General" KB\n`));
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (options.dryRun) {
|
|
1066
|
+
console.log(chalk.cyan('\nā
Dry run complete. Use without --dry-run to upload.\n'));
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Confirm each subdirectory if --confirm flag
|
|
1071
|
+
const rl = readline.createInterface({
|
|
1072
|
+
input: process.stdin,
|
|
1073
|
+
output: process.stdout
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
const askYesNo = (question, defaultYes = true) => {
|
|
1077
|
+
return new Promise((resolve) => {
|
|
1078
|
+
const suffix = defaultYes ? '(Y/n)' : '(y/N)';
|
|
1079
|
+
rl.question(`${question} ${suffix}: `, (answer) => {
|
|
1080
|
+
const trimmed = answer.trim().toLowerCase();
|
|
1081
|
+
resolve(trimmed === '' ? defaultYes : (trimmed === 'y' || trimmed === 'yes'));
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
const kbsToUpload = [];
|
|
1087
|
+
for (const info of subdirInfo) {
|
|
1088
|
+
if (options.confirm) {
|
|
1089
|
+
const proceed = await askYesNo(`Create KB "${info.name}" and upload ${info.fileCount} files?`, true);
|
|
1090
|
+
if (!proceed) {
|
|
1091
|
+
console.log(chalk.gray(` Skipping ${info.name}\n`));
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
kbsToUpload.push(info);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
rl.close();
|
|
1099
|
+
|
|
1100
|
+
if (kbsToUpload.length === 0) {
|
|
1101
|
+
console.log(chalk.yellow('\nā ļø No KBs selected for upload.\n'));
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Upload each subdirectory as a KB
|
|
1106
|
+
for (const info of kbsToUpload) {
|
|
1107
|
+
console.log(chalk.cyan(`\nš¦ Uploading to KB "${info.name}"...`));
|
|
1108
|
+
|
|
1109
|
+
// Create KB if it doesn't exist
|
|
1110
|
+
try {
|
|
1111
|
+
await apiClient.invoke('create_skeleton_kb', {
|
|
1112
|
+
community_id: options.community,
|
|
1113
|
+
app_id: options.app,
|
|
1114
|
+
kb_name: info.name
|
|
1115
|
+
});
|
|
1116
|
+
console.log(chalk.gray(` ā KB "${info.name}" created`));
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
// KB might already exist, continue
|
|
1119
|
+
if (!err.message.includes('already exists')) {
|
|
1120
|
+
console.log(chalk.yellow(` ā ļø Could not create KB: ${err.message}`));
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Read files from subdirectory
|
|
1125
|
+
const files = await readDirectoryFiles(info.path);
|
|
1126
|
+
|
|
1127
|
+
if (files.length === 0) {
|
|
1128
|
+
console.log(chalk.yellow(` ā ļø No files found in ${info.name}/`));
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Upload in batches
|
|
1133
|
+
const BATCH_SIZE = 10;
|
|
1134
|
+
let uploaded = 0;
|
|
1135
|
+
let errors = [];
|
|
1136
|
+
|
|
1137
|
+
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
|
1138
|
+
const batch = files.slice(i, i + BATCH_SIZE);
|
|
1139
|
+
const batchNum = Math.floor(i / BATCH_SIZE) + 1;
|
|
1140
|
+
const totalBatches = Math.ceil(files.length / BATCH_SIZE);
|
|
1141
|
+
|
|
1142
|
+
try {
|
|
1143
|
+
process.stdout.write(chalk.gray(` Batch ${batchNum}/${totalBatches}...`));
|
|
1144
|
+
const response = await apiClient.invoke('upload_files_to_kb', {
|
|
1145
|
+
community_id: options.community,
|
|
1146
|
+
app_id: options.app,
|
|
1147
|
+
kb_id: info.name,
|
|
1148
|
+
files: batch
|
|
1149
|
+
});
|
|
1150
|
+
const result = response.message || response;
|
|
1151
|
+
uploaded += result.uploaded || batch.length;
|
|
1152
|
+
if (result.errors) {
|
|
1153
|
+
errors.push(...result.errors);
|
|
1154
|
+
}
|
|
1155
|
+
console.log(chalk.green(' ā'));
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
console.log(chalk.red(` ā ${err.message}`));
|
|
1158
|
+
errors.push({ batch: batchNum, error: err.message });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
console.log(chalk.green(` ā
Uploaded ${uploaded} files to KB "${info.name}"`));
|
|
1163
|
+
if (errors.length > 0) {
|
|
1164
|
+
console.log(chalk.yellow(` ā ļø ${errors.length} errors during upload`));
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Handle root files if any
|
|
1169
|
+
if (rootFiles.length > 0) {
|
|
1170
|
+
console.log(chalk.cyan(`\nš¦ Uploading ${rootFiles.length} root files to "General" KB...`));
|
|
1171
|
+
const rootFilesData = [];
|
|
1172
|
+
for (const file of rootFiles) {
|
|
1173
|
+
const filePath = path.join(localPath, file.name);
|
|
1174
|
+
const content = await fs.readFile(filePath);
|
|
1175
|
+
rootFilesData.push({
|
|
1176
|
+
name: file.name,
|
|
1177
|
+
path: file.name,
|
|
1178
|
+
content: content.toString('base64')
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
try {
|
|
1183
|
+
const response = await apiClient.invoke('upload_files_to_kb', {
|
|
1184
|
+
community_id: options.community,
|
|
1185
|
+
app_id: options.app,
|
|
1186
|
+
kb_id: 'General',
|
|
1187
|
+
files: rootFilesData
|
|
1188
|
+
});
|
|
1189
|
+
const result = response.message || response;
|
|
1190
|
+
console.log(chalk.green(` ā
Uploaded ${result.uploaded || 0} root files`));
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
console.log(chalk.yellow(` ā ļø Could not upload root files: ${err.message}`));
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
console.log(chalk.green('\nā
Upload tree complete!\n'));
|
|
1197
|
+
|
|
1198
|
+
// Helper function to read directory files
|
|
1199
|
+
async function readDirectoryFiles(dirPath, basePath = '') {
|
|
1200
|
+
const files = [];
|
|
1201
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
1202
|
+
|
|
1203
|
+
for (const entry of entries) {
|
|
1204
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
1205
|
+
const relPath = path.join(basePath, entry.name);
|
|
1206
|
+
|
|
1207
|
+
if (entry.isDirectory()) {
|
|
1208
|
+
const subFiles = await readDirectoryFiles(fullPath, relPath);
|
|
1209
|
+
files.push(...subFiles);
|
|
1210
|
+
} else if (entry.isFile()) {
|
|
1211
|
+
// Skip hidden files
|
|
1212
|
+
if (entry.name.startsWith('.')) {
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
const content = await fs.readFile(fullPath);
|
|
1216
|
+
files.push({
|
|
1217
|
+
name: entry.name,
|
|
1218
|
+
path: relPath,
|
|
1219
|
+
content: content.toString('base64')
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
return files;
|
|
1225
|
+
}
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
console.error(chalk.red(error.message));
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
appCommand
|
|
1233
|
+
.command('sync')
|
|
1234
|
+
.description('Sync knowledge base (vectorize files in Drive folder)')
|
|
1235
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
1236
|
+
.requiredOption('-a, --app <id>', 'App ID')
|
|
1237
|
+
.option('-k, --kb <id>', 'Knowledge Base ID', 'General')
|
|
1238
|
+
.action(async (options) => {
|
|
1239
|
+
try {
|
|
1240
|
+
const apiClient = new DeSciXApiClient();
|
|
1241
|
+
await requireAuth(apiClient);
|
|
1242
|
+
|
|
1243
|
+
const response = await apiClient.invoke('sync_knowledge_base', {
|
|
1244
|
+
community_id: options.community,
|
|
1245
|
+
app_id: options.app,
|
|
1246
|
+
kb_id: options.kb
|
|
1247
|
+
});
|
|
1248
|
+
const result = response.message || response;
|
|
1249
|
+
|
|
1250
|
+
console.log(chalk.green('\nā
Sync initiated!\n'));
|
|
1251
|
+
console.log(chalk.cyan(` KB: ${options.community}/${options.app}/${options.kb}`));
|
|
1252
|
+
if (result.note) {
|
|
1253
|
+
console.log(chalk.gray(` Note: ${result.note}\n`));
|
|
1254
|
+
}
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
console.error(chalk.red(error.message));
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
appCommand
|
|
1262
|
+
.command('set-codesite')
|
|
1263
|
+
.description('Set CodeSite URL/path for an app (use "descix site servelocal" for local dev port)')
|
|
1264
|
+
.option('-c, --community <id>', 'Community ID (uses context if not provided)')
|
|
1265
|
+
.option('-a, --app <id>', 'App ID (uses context if not provided)')
|
|
1266
|
+
.requiredOption('-u, --url <url>', 'CodeSite URL (GCS path or HTTPS URL)')
|
|
1267
|
+
.action(async (options) => {
|
|
1268
|
+
try {
|
|
1269
|
+
const apiClient = new DeSciXApiClient();
|
|
1270
|
+
await requireAuth(apiClient);
|
|
1271
|
+
|
|
1272
|
+
// Load workspace context
|
|
1273
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
1274
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
1275
|
+
|
|
1276
|
+
const communityId = ctx.communityId;
|
|
1277
|
+
const appId = ctx.appId;
|
|
1278
|
+
|
|
1279
|
+
if (!communityId || !appId) {
|
|
1280
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
1281
|
+
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory\n'));
|
|
1282
|
+
process.exit(1);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const response = await apiClient.invoke('update_app_metadata', {
|
|
1286
|
+
community_id: communityId,
|
|
1287
|
+
app_id: appId,
|
|
1288
|
+
ip_site_gcs_path_url: options.url
|
|
1289
|
+
});
|
|
1290
|
+
const result = response.message || response;
|
|
1291
|
+
|
|
1292
|
+
console.log(chalk.green('\nā
CodeSite URL updated!\n'));
|
|
1293
|
+
console.log(chalk.cyan(` App: ${communityId}/${appId}`));
|
|
1294
|
+
console.log(chalk.gray(` CodeSite URL: ${options.url}\n`));
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
console.error(chalk.red(error.message));
|
|
1297
|
+
process.exit(1);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
appCommand
|
|
1302
|
+
.command('set-price')
|
|
1303
|
+
.description('Set app price in USDCX')
|
|
1304
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
1305
|
+
.requiredOption('-a, --app <id>', 'App ID')
|
|
1306
|
+
.requiredOption('-p, --price <amount>', 'Price in USDCX (decimal number)')
|
|
1307
|
+
.action(async (options) => {
|
|
1308
|
+
try {
|
|
1309
|
+
const apiClient = new DeSciXApiClient();
|
|
1310
|
+
await requireAuth(apiClient);
|
|
1311
|
+
|
|
1312
|
+
const price = parseFloat(options.price);
|
|
1313
|
+
if (isNaN(price) || price < 0) {
|
|
1314
|
+
throw new Error('Price must be a non-negative number');
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const response = await apiClient.invoke('update_app_metadata', {
|
|
1318
|
+
community_id: options.community,
|
|
1319
|
+
app_id: options.app,
|
|
1320
|
+
price: price
|
|
1321
|
+
});
|
|
1322
|
+
const result = response.message || response;
|
|
1323
|
+
|
|
1324
|
+
console.log(chalk.green('\nā
App price updated!\n'));
|
|
1325
|
+
console.log(chalk.cyan(` App: ${options.community}/${options.app}`));
|
|
1326
|
+
console.log(chalk.gray(` Price: ${price} USDCX\n`));
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
console.error(chalk.red(error.message));
|
|
1329
|
+
process.exit(1);
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
appCommand
|
|
1334
|
+
.command('set-api-url')
|
|
1335
|
+
.description('Set API base URL for app\'s own backend')
|
|
1336
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
1337
|
+
.requiredOption('-a, --app <id>', 'App ID')
|
|
1338
|
+
.requiredOption('-u, --url <url>', 'API base URL (e.g., https://api.example.com)')
|
|
1339
|
+
.action(async (options) => {
|
|
1340
|
+
try {
|
|
1341
|
+
const apiClient = new DeSciXApiClient();
|
|
1342
|
+
await requireAuth(apiClient);
|
|
1343
|
+
|
|
1344
|
+
// Validate URL format
|
|
1345
|
+
try {
|
|
1346
|
+
new URL(options.url);
|
|
1347
|
+
} catch {
|
|
1348
|
+
throw new Error('Invalid URL format');
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const response = await apiClient.invoke('update_app_metadata', {
|
|
1352
|
+
community_id: options.community,
|
|
1353
|
+
app_id: options.app,
|
|
1354
|
+
api_base_url: options.url
|
|
1355
|
+
});
|
|
1356
|
+
const result = response.message || response;
|
|
1357
|
+
|
|
1358
|
+
console.log(chalk.green('\nā
API base URL updated!\n'));
|
|
1359
|
+
console.log(chalk.cyan(` App: ${options.community}/${options.app}`));
|
|
1360
|
+
console.log(chalk.gray(` API URL: ${options.url}\n`));
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
console.error(chalk.red(error.message));
|
|
1363
|
+
process.exit(1);
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
appCommand
|
|
1368
|
+
.command('deploy')
|
|
1369
|
+
.description('Deploy a microservice for an app you own (registers manifest.json)')
|
|
1370
|
+
.option('-m, --manifest <path>', 'Path to manifest.json (default: ./manifest.json)')
|
|
1371
|
+
.action(async (options) => {
|
|
1372
|
+
try {
|
|
1373
|
+
const apiClient = new DeSciXApiClient();
|
|
1374
|
+
await requireAuth(apiClient);
|
|
1375
|
+
|
|
1376
|
+
// Read manifest.json from current directory or specified path
|
|
1377
|
+
const manifestPath = path.resolve(options.manifest || './manifest.json');
|
|
1378
|
+
|
|
1379
|
+
console.log(chalk.cyan(`\nš¦ Deploying microservice from ${manifestPath}\n`));
|
|
1380
|
+
|
|
1381
|
+
let manifest;
|
|
1382
|
+
try {
|
|
1383
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
1384
|
+
manifest = JSON.parse(manifestContent);
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
if (error.code === 'ENOENT') {
|
|
1387
|
+
console.error(chalk.red(`ā manifest.json not found at ${manifestPath}`));
|
|
1388
|
+
console.error(chalk.gray('\nCreate a manifest.json with your service configuration.'));
|
|
1389
|
+
console.error(chalk.gray('See DeSciX_Core/descix-cli/templates/scaffolds/microservice/manifest.json for a template.\n'));
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
}
|
|
1392
|
+
throw new Error(`Failed to parse manifest.json: ${error.message}`);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Validate required fields
|
|
1396
|
+
if (!manifest.service?.name) {
|
|
1397
|
+
throw new Error('manifest.service.name is required');
|
|
1398
|
+
}
|
|
1399
|
+
if (!manifest.service?.app_id) {
|
|
1400
|
+
throw new Error('manifest.service.app_id is required (the app you own)');
|
|
1401
|
+
}
|
|
1402
|
+
if (!manifest.service?.community_id) {
|
|
1403
|
+
throw new Error('manifest.service.community_id is required');
|
|
1404
|
+
}
|
|
1405
|
+
if (!manifest.service?.domain) {
|
|
1406
|
+
throw new Error('manifest.service.domain is required');
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
console.log(chalk.gray(` Service: ${manifest.service.name}`));
|
|
1410
|
+
console.log(chalk.gray(` App: ${manifest.service.community_id}/${manifest.service.app_id}`));
|
|
1411
|
+
console.log(chalk.gray(` Domain: ${manifest.service.domain}`));
|
|
1412
|
+
console.log(chalk.gray(` Commands: ${Object.keys(manifest.commands || {}).length}\n`));
|
|
1413
|
+
|
|
1414
|
+
// Call register_service
|
|
1415
|
+
const response = await apiClient.invoke('register_service', { manifest });
|
|
1416
|
+
const result = response.message || response;
|
|
1417
|
+
|
|
1418
|
+
console.log(chalk.green('ā
Microservice deployed successfully!\n'));
|
|
1419
|
+
console.log(chalk.cyan(` Service: ${manifest.service.name}`));
|
|
1420
|
+
console.log(chalk.gray(` Commands registered: ${Object.keys(manifest.commands || {}).length}`));
|
|
1421
|
+
|
|
1422
|
+
if (response.app_updated) {
|
|
1423
|
+
console.log(chalk.gray(` App api_base_url: ${response.api_base_url}`));
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
console.log(chalk.gray('\n The service manifest cache will refresh within 5 minutes.'));
|
|
1427
|
+
console.log(chalk.gray(' For immediate effect, an admin can run: descix microservice reload\n'));
|
|
1428
|
+
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
console.error(chalk.red(`\nā Deployment failed: ${error.message}\n`));
|
|
1431
|
+
process.exit(1);
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
appCommand
|
|
1436
|
+
.command('set-repo')
|
|
1437
|
+
.description('Link a GitHub repository to your app')
|
|
1438
|
+
.requiredOption('-a, --app <id>', 'App ID')
|
|
1439
|
+
.option('--repo <url>', 'SSH git URL (e.g., git@github.com:user/repo.git)')
|
|
1440
|
+
.option('--unlink', 'Remove the linked repository')
|
|
1441
|
+
.option('--branch <name>', 'Default branch', 'main')
|
|
1442
|
+
.option('--subfolder <path>', 'Subfolder within repo')
|
|
1443
|
+
.action(async (options) => {
|
|
1444
|
+
if (!options.repo && !options.unlink) {
|
|
1445
|
+
console.error('Either --repo <url> or --unlink is required.');
|
|
1446
|
+
process.exit(1);
|
|
1447
|
+
}
|
|
1448
|
+
try {
|
|
1449
|
+
const apiClient = new DeSciXApiClient();
|
|
1450
|
+
await requireAuth(apiClient);
|
|
1451
|
+
const result = await apiClient.invoke('set_app_repo', {
|
|
1452
|
+
app_id: options.app,
|
|
1453
|
+
repo_url: options.unlink ? null : options.repo,
|
|
1454
|
+
repo_branch: options.branch,
|
|
1455
|
+
repo_subfolder: options.subfolder
|
|
1456
|
+
});
|
|
1457
|
+
if (result.cleared) {
|
|
1458
|
+
console.log(result.message);
|
|
1459
|
+
} else {
|
|
1460
|
+
console.log('\nRepository linked successfully.\n');
|
|
1461
|
+
console.log(result.instructions);
|
|
1462
|
+
console.log(`\nFingerprint: ${result.fingerprint}\n`);
|
|
1463
|
+
}
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
1466
|
+
process.exit(1);
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
appCommand
|
|
1471
|
+
.command('invite')
|
|
1472
|
+
.description('Generate invite links to share your app')
|
|
1473
|
+
.requiredOption('-a, --app <id>', 'App ID')
|
|
1474
|
+
.option('--type <type>', 'Invite type: try or install', 'install')
|
|
1475
|
+
.option('--hint <text>', 'Context for the AI agent (e.g., "Physics students, no coding background")')
|
|
1476
|
+
.option('--expires <duration>', 'Expiry duration (e.g., 30d, 7d, 24h)')
|
|
1477
|
+
.action(async (options) => {
|
|
1478
|
+
try {
|
|
1479
|
+
const apiClient = new DeSciXApiClient();
|
|
1480
|
+
await requireAuth(apiClient);
|
|
1481
|
+
const result = await apiClient.invoke('create_app_invite', {
|
|
1482
|
+
app_id: options.app,
|
|
1483
|
+
invite_type: options.type,
|
|
1484
|
+
agent_hint: options.hint,
|
|
1485
|
+
expires_in: options.expires
|
|
1486
|
+
});
|
|
1487
|
+
console.log(`\nInvite created for ${options.app}\n`);
|
|
1488
|
+
console.log(` Try (browser): ${result.try_url}`);
|
|
1489
|
+
console.log(` Install (VS Code): ${result.install_url}`);
|
|
1490
|
+
console.log(` Expires: ${result.expires_at}`);
|
|
1491
|
+
console.log(` Token: ${result.token}\n`);
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
1494
|
+
process.exit(1);
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
// ============ Knowledge Base Commands ============
|
|
1499
|
+
|
|
1500
|
+
const kbCommand = program
|
|
1501
|
+
.command('kb')
|
|
1502
|
+
.description('Knowledge base operations');
|
|
1503
|
+
|
|
1504
|
+
kbCommand
|
|
1505
|
+
.command('create')
|
|
1506
|
+
.description('Create a new knowledge base in an app')
|
|
1507
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
1508
|
+
.requiredOption('-a, --app <id>', 'App ID')
|
|
1509
|
+
.requiredOption('-k, --kb <id>', 'Knowledge Base ID')
|
|
1510
|
+
.option('--drive-folder <id>', 'Google Drive folder ID (optional, will create if not provided)')
|
|
1511
|
+
.action(async (options) => {
|
|
1512
|
+
try {
|
|
1513
|
+
const apiClient = new DeSciXApiClient();
|
|
1514
|
+
await requireAuth(apiClient);
|
|
1515
|
+
|
|
1516
|
+
const response = await apiClient.invoke('create_skeleton_kb', {
|
|
1517
|
+
community_id: options.community,
|
|
1518
|
+
app_id: options.app,
|
|
1519
|
+
kb_name: options.kb,
|
|
1520
|
+
drive_folder_id: options.driveFolder || null
|
|
1521
|
+
});
|
|
1522
|
+
const result = response.message || response;
|
|
1523
|
+
|
|
1524
|
+
console.log(chalk.green('\nā
Knowledge Base created successfully!\n'));
|
|
1525
|
+
console.log(chalk.cyan(` KB ID: ${options.kb}`));
|
|
1526
|
+
console.log(chalk.gray(` App: ${options.community}/${options.app}`));
|
|
1527
|
+
if (result.drive_folder_id) {
|
|
1528
|
+
console.log(chalk.gray(` Drive Folder: ${result.drive_folder_id}\n`));
|
|
1529
|
+
}
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
console.error(chalk.red(error.message));
|
|
1532
|
+
process.exit(1);
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
// Phase 0 CLI-Centric KB Processing Commands
|
|
1537
|
+
kbCommand
|
|
1538
|
+
.command('pull')
|
|
1539
|
+
.description('Pull KB content from Drive and convert to local markdown')
|
|
1540
|
+
.option('-c, --community <id>', 'Community ID')
|
|
1541
|
+
.option('-a, --app <id>', 'App ID')
|
|
1542
|
+
.option('-k, --kb <id>', 'Knowledge Base ID (default: General)')
|
|
1543
|
+
.option('-v, --verbose', 'Show verbose output')
|
|
1544
|
+
.option('--merge-mode <mode>', 'Merge mode: merge|overwrite|force-overwrite (default: merge)')
|
|
1545
|
+
.option('--dry-run', 'Show what would happen without making changes')
|
|
1546
|
+
.action(async (options) => {
|
|
1547
|
+
try {
|
|
1548
|
+
let apiClient = null;
|
|
1549
|
+
try {
|
|
1550
|
+
apiClient = new DeSciXApiClient();
|
|
1551
|
+
await apiClient.loadCredentials();
|
|
1552
|
+
} catch {
|
|
1553
|
+
// Continue without API client
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
await kbCommands.runKbPull(apiClient, options);
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
1559
|
+
process.exit(1);
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
kbCommand
|
|
1564
|
+
.command('chunk')
|
|
1565
|
+
.description('Generate chunks from local markdown files')
|
|
1566
|
+
.option('-c, --community <id>', 'Community ID')
|
|
1567
|
+
.option('-a, --app <id>', 'App ID')
|
|
1568
|
+
.option('-k, --kb <id>', 'Knowledge Base ID (default: General)')
|
|
1569
|
+
.option('-s, --chunk-size <size>', 'Chunk size in characters (default: 512)')
|
|
1570
|
+
.option('-o, --overlap <size>', 'Overlap between chunks (default: 64)')
|
|
1571
|
+
.option('-v, --verbose', 'Show verbose output')
|
|
1572
|
+
.action(async (options) => {
|
|
1573
|
+
try {
|
|
1574
|
+
await kbCommands.runKbChunk(options);
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
1577
|
+
process.exit(1);
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
kbCommand
|
|
1582
|
+
.command('sync')
|
|
1583
|
+
.description('Sync local chunks to Pinecone via service layer')
|
|
1584
|
+
.option('-c, --community <id>', 'Community ID')
|
|
1585
|
+
.option('-a, --app <id>', 'App ID')
|
|
1586
|
+
.option('-k, --kb <id>', 'Knowledge Base ID (default: General)')
|
|
1587
|
+
.option('-v, --verbose', 'Show verbose output')
|
|
1588
|
+
.action(async (options) => {
|
|
1589
|
+
try {
|
|
1590
|
+
const apiClient = new DeSciXApiClient();
|
|
1591
|
+
await requireAuth(apiClient);
|
|
1592
|
+
|
|
1593
|
+
await kbCommands.runKbSync(apiClient, options);
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
1596
|
+
process.exit(1);
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
kbCommand
|
|
1601
|
+
.command('build')
|
|
1602
|
+
.description('Full KB pipeline: [push staging] ā pull ā chunk ā sync')
|
|
1603
|
+
.option('-c, --community <id>', 'Community ID')
|
|
1604
|
+
.option('-a, --app <id>', 'App ID')
|
|
1605
|
+
.option('-k, --kb <id>', 'Knowledge Base ID (default: General)')
|
|
1606
|
+
.option('-s, --chunk-size <size>', 'Chunk size in characters (default: 2000)')
|
|
1607
|
+
.option('-o, --overlap <size>', 'Overlap between chunks (default: 500)')
|
|
1608
|
+
.option('-v, --verbose', 'Show verbose output')
|
|
1609
|
+
.option('-i, --interactive', 'Enable interactive prompts for conflicts')
|
|
1610
|
+
.option('--skip-staging', 'Skip staging push step')
|
|
1611
|
+
.option('--merge-mode <mode>', 'Pull merge mode: merge|overwrite|force-overwrite (default: merge)')
|
|
1612
|
+
.option('--dry-run', 'Show what would happen without making changes')
|
|
1613
|
+
.action(async (options) => {
|
|
1614
|
+
try {
|
|
1615
|
+
const apiClient = new DeSciXApiClient();
|
|
1616
|
+
await requireAuth(apiClient);
|
|
1617
|
+
|
|
1618
|
+
await kbCommands.runKbBuild(apiClient, options);
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
1621
|
+
process.exit(1);
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
kbCommand
|
|
1626
|
+
.command('push')
|
|
1627
|
+
.description('Push staging files to Drive')
|
|
1628
|
+
.option('-c, --community <id>', 'Community ID')
|
|
1629
|
+
.option('-a, --app <id>', 'App ID')
|
|
1630
|
+
.option('-k, --kb <id>', 'Knowledge Base ID (default: General)')
|
|
1631
|
+
.option('-v, --verbose', 'Show verbose output')
|
|
1632
|
+
.option('-i, --interactive', 'Enable interactive prompts for conflicts')
|
|
1633
|
+
.option('--on-conflict <action>', 'Conflict handling: overwrite|skip (default: overwrite)')
|
|
1634
|
+
.option('--no-move', 'Do not move files to .processed after upload')
|
|
1635
|
+
.option('--dry-run', 'Show what would happen without making changes')
|
|
1636
|
+
.action(async (options) => {
|
|
1637
|
+
try {
|
|
1638
|
+
let apiClient = null;
|
|
1639
|
+
try {
|
|
1640
|
+
apiClient = new DeSciXApiClient();
|
|
1641
|
+
await apiClient.loadCredentials();
|
|
1642
|
+
} catch {
|
|
1643
|
+
// Continue without API client
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
await kbCommands.runKbPush(apiClient, {
|
|
1647
|
+
...options,
|
|
1648
|
+
moveToProcessed: options.move !== false
|
|
1649
|
+
});
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
1652
|
+
process.exit(1);
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
kbCommand
|
|
1657
|
+
.command('status')
|
|
1658
|
+
.description('Show KB sync status (local vs Pinecone)')
|
|
1659
|
+
.option('-c, --community <id>', 'Community ID')
|
|
1660
|
+
.option('-a, --app <id>', 'App ID')
|
|
1661
|
+
.option('-k, --kb <id>', 'Knowledge Base ID (default: General)')
|
|
1662
|
+
.action(async (options) => {
|
|
1663
|
+
try {
|
|
1664
|
+
const apiClient = new DeSciXApiClient();
|
|
1665
|
+
await requireAuth(apiClient);
|
|
1666
|
+
|
|
1667
|
+
await kbCommands.runKbStatus(apiClient, options);
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
1670
|
+
process.exit(1);
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
kbCommand
|
|
1675
|
+
.command('compare')
|
|
1676
|
+
.description('Show file-level deltas between local and Drive')
|
|
1677
|
+
.option('-c, --community <id>', 'Community ID')
|
|
1678
|
+
.option('-a, --app <id>', 'App ID')
|
|
1679
|
+
.option('-k, --kb <id>', 'Knowledge Base ID (default: General)')
|
|
1680
|
+
.option('--staging', 'Only show staging ā Drive delta')
|
|
1681
|
+
.option('--local', 'Only show local KB ā Drive delta')
|
|
1682
|
+
.action(async (options) => {
|
|
1683
|
+
try {
|
|
1684
|
+
await kbCommands.runKbCompare(null, options);
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
1687
|
+
process.exit(1);
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
// ============ Site Commands ============
|
|
1692
|
+
|
|
1693
|
+
const siteCommand = program
|
|
1694
|
+
.command('site')
|
|
1695
|
+
.description('Site lifecycle management: init, upload, servelocal');
|
|
1696
|
+
|
|
1697
|
+
// site init - Copy scaffold to app
|
|
1698
|
+
siteCommand
|
|
1699
|
+
.command('init')
|
|
1700
|
+
.description('Initialize site scaffold in current app')
|
|
1701
|
+
.option('-f, --force', 'Overwrite existing site/ folder')
|
|
1702
|
+
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
1703
|
+
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
1704
|
+
.action(async (options) => {
|
|
1705
|
+
try {
|
|
1706
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
1707
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
1708
|
+
|
|
1709
|
+
if (!ctx.communityId || !ctx.appId) {
|
|
1710
|
+
console.error(chalk.red('\nā No app context found.'));
|
|
1711
|
+
console.log(chalk.gray(' cd into an app directory, or use -c and -a flags\n'));
|
|
1712
|
+
process.exit(1);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const appConfig = workspaceConfig.getApp(ctx.communityId, ctx.appId);
|
|
1716
|
+
if (!appConfig) {
|
|
1717
|
+
console.error(chalk.red('\nā App not found in workspace.json.'));
|
|
1718
|
+
console.log(chalk.gray(' Run "npx descix init" to set up your workspace.\n'));
|
|
1719
|
+
process.exit(1);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const appPath = appConfig.absolutePath ||
|
|
1723
|
+
path.join(workspaceConfig.getWorkspaceRoot(), appConfig.localPath);
|
|
1724
|
+
|
|
1725
|
+
console.log(chalk.cyan('\nš Adding site scaffold...\n'));
|
|
1726
|
+
|
|
1727
|
+
const { copyScaffold } = await import('../lib/core/Hydrator.js');
|
|
1728
|
+
const stats = await copyScaffold('site', appPath, {
|
|
1729
|
+
verbose: true,
|
|
1730
|
+
force: options.force
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
console.log(chalk.green(`\nā
Site scaffold added (${stats.copied} files)\n`));
|
|
1734
|
+
console.log(chalk.gray(' Next steps:'));
|
|
1735
|
+
console.log(chalk.gray(' - Edit site/index.html for your content'));
|
|
1736
|
+
console.log(chalk.gray(' - Run descix site upload to deploy\n'));
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
1739
|
+
process.exit(1);
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
// site upload - Deploy to GCS (context-aware, replaces deploy)
|
|
1744
|
+
siteCommand
|
|
1745
|
+
.command('upload')
|
|
1746
|
+
.description('Upload site to GCS and update app metadata')
|
|
1747
|
+
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
1748
|
+
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
1749
|
+
.option('-p, --path <localPath>', 'Local directory to deploy', './site')
|
|
1750
|
+
.option('--preview', 'Deploy to preview path')
|
|
1751
|
+
.option('--full', 'Force full upload (ignore delta)')
|
|
1752
|
+
.option('--dry-run', 'Show what would be deployed')
|
|
1753
|
+
.option('--no-cache', 'Set Cache-Control: no-cache')
|
|
1754
|
+
.action(async (options) => {
|
|
1755
|
+
try {
|
|
1756
|
+
const apiClient = new DeSciXApiClient();
|
|
1757
|
+
await requireAuth(apiClient);
|
|
1758
|
+
|
|
1759
|
+
// Load workspace context
|
|
1760
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
1761
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
1762
|
+
|
|
1763
|
+
const communityId = ctx.communityId;
|
|
1764
|
+
const appId = ctx.appId;
|
|
1765
|
+
|
|
1766
|
+
if (!communityId || !appId) {
|
|
1767
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
1768
|
+
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory\n'));
|
|
1769
|
+
process.exit(1);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// Import GitUtils for file hashing
|
|
1773
|
+
const { GitUtils } = await import('@descix/sdk/integrations/git');
|
|
1774
|
+
const mime = (await import('mime-types')).default;
|
|
1775
|
+
|
|
1776
|
+
const localPath = path.resolve(options.path);
|
|
1777
|
+
|
|
1778
|
+
// Check if directory exists
|
|
1779
|
+
try {
|
|
1780
|
+
const stat = await fs.stat(localPath);
|
|
1781
|
+
if (!stat.isDirectory()) {
|
|
1782
|
+
throw new Error(`Path is not a directory: ${localPath}`);
|
|
1783
|
+
}
|
|
1784
|
+
} catch (err) {
|
|
1785
|
+
if (err.code === 'ENOENT') {
|
|
1786
|
+
throw new Error(`Directory not found: ${localPath}`);
|
|
1787
|
+
}
|
|
1788
|
+
throw err;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// Get Git status
|
|
1792
|
+
const gitUtils = new GitUtils(process.cwd());
|
|
1793
|
+
const gitStatus = await gitUtils.getStatus();
|
|
1794
|
+
|
|
1795
|
+
console.log(chalk.cyan(`\nš Site Upload: ${localPath} ā GCS\n`));
|
|
1796
|
+
console.log(chalk.gray(` Target: ${communityId}/${appId}`));
|
|
1797
|
+
if (options.preview) console.log(chalk.yellow(` Mode: PREVIEW`));
|
|
1798
|
+
if (gitStatus.isGitRepo) {
|
|
1799
|
+
console.log(chalk.gray(` Git: ${gitStatus.branch} @ ${gitStatus.shortHash}`));
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// 1. Compute local file hashes
|
|
1803
|
+
const localFiles = await gitUtils.getFileHashes(localPath);
|
|
1804
|
+
const fileList = Object.entries(localFiles).map(([filePath, info]) => ({
|
|
1805
|
+
path: filePath,
|
|
1806
|
+
hash: info.hash,
|
|
1807
|
+
size: info.size,
|
|
1808
|
+
content_type: mime.lookup(filePath) || 'application/octet-stream'
|
|
1809
|
+
}));
|
|
1810
|
+
|
|
1811
|
+
console.log(chalk.gray(`\n Found ${fileList.length} files\n`));
|
|
1812
|
+
|
|
1813
|
+
if (fileList.length === 0) {
|
|
1814
|
+
console.log(chalk.yellow(' No files found in directory. Nothing to deploy.\n'));
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// 2. Request deploy token with file list
|
|
1819
|
+
const tokenResponse = await apiClient.invoke('get_site_deploy_token', {
|
|
1820
|
+
community_id: communityId,
|
|
1821
|
+
app_id: appId,
|
|
1822
|
+
files: fileList,
|
|
1823
|
+
preview: options.preview || false
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
const { signed_urls, existing_manifest, token_id, site_url } = tokenResponse.message;
|
|
1827
|
+
|
|
1828
|
+
// 3. Determine delta
|
|
1829
|
+
let filesToUpload = fileList;
|
|
1830
|
+
let filesToDelete = [];
|
|
1831
|
+
|
|
1832
|
+
if (!options.full && existing_manifest) {
|
|
1833
|
+
const delta = gitUtils.compareSyncState(localFiles, existing_manifest);
|
|
1834
|
+
filesToUpload = fileList.filter(f =>
|
|
1835
|
+
delta.added.includes(f.path) || delta.modified.includes(f.path)
|
|
1836
|
+
);
|
|
1837
|
+
filesToDelete = delta.deleted;
|
|
1838
|
+
|
|
1839
|
+
console.log(chalk.gray(` Delta: ${delta.added.length} added, ${delta.modified.length} modified, ${delta.unchanged.length} unchanged, ${delta.deleted.length} deleted`));
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
if (options.dryRun) {
|
|
1843
|
+
console.log(chalk.yellow('\nš Dry run - would upload:'));
|
|
1844
|
+
if (filesToUpload.length === 0) {
|
|
1845
|
+
console.log(chalk.gray(' (no files to upload)'));
|
|
1846
|
+
} else {
|
|
1847
|
+
filesToUpload.forEach(f => console.log(chalk.gray(` + ${f.path} (${(f.size / 1024).toFixed(1)}KB)`)));
|
|
1848
|
+
}
|
|
1849
|
+
if (filesToDelete.length > 0) {
|
|
1850
|
+
console.log(chalk.yellow('\nWould delete:'));
|
|
1851
|
+
filesToDelete.forEach(f => console.log(chalk.gray(` - ${f}`)));
|
|
1852
|
+
}
|
|
1853
|
+
console.log(chalk.gray(`\n Site URL: ${site_url}\n`));
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
if (filesToUpload.length === 0) {
|
|
1858
|
+
console.log(chalk.green('\nā
Site already up to date on GCS.'));
|
|
1859
|
+
console.log(chalk.gray(' Ensuring app metadata is synchronized...'));
|
|
1860
|
+
// Fall through to confirm_site_deploy to ensure DB is updated with relative path
|
|
1861
|
+
} else {
|
|
1862
|
+
// 4. Upload files directly to GCS using signed URLs
|
|
1863
|
+
console.log(chalk.gray(`\n Uploading ${filesToUpload.length} files...`));
|
|
1864
|
+
|
|
1865
|
+
let uploadedCount = 0;
|
|
1866
|
+
let uploadErrors = [];
|
|
1867
|
+
|
|
1868
|
+
for (const file of filesToUpload) {
|
|
1869
|
+
const signedUrl = signed_urls[file.path];
|
|
1870
|
+
if (!signedUrl) {
|
|
1871
|
+
uploadErrors.push(`No signed URL for: ${file.path}`);
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
try {
|
|
1876
|
+
const content = await fs.readFile(path.join(localPath, file.path));
|
|
1877
|
+
|
|
1878
|
+
const response = await fetch(signedUrl, {
|
|
1879
|
+
method: 'PUT',
|
|
1880
|
+
headers: {
|
|
1881
|
+
'Content-Type': file.content_type,
|
|
1882
|
+
...(options.noCache ? { 'Cache-Control': 'no-cache' } : {})
|
|
1883
|
+
},
|
|
1884
|
+
body: content
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
if (!response.ok) {
|
|
1888
|
+
uploadErrors.push(`Failed to upload ${file.path}: ${response.status} ${response.statusText}`);
|
|
1889
|
+
console.log(chalk.red(` ā ${file.path}`));
|
|
1890
|
+
} else {
|
|
1891
|
+
uploadedCount++;
|
|
1892
|
+
console.log(chalk.green(` ā ${file.path}`));
|
|
1893
|
+
}
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
uploadErrors.push(`Error uploading ${file.path}: ${err.message}`);
|
|
1896
|
+
console.log(chalk.red(` ā ${file.path}`));
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
if (uploadErrors.length > 0) {
|
|
1901
|
+
console.log(chalk.yellow(`\nā ļø ${uploadErrors.length} errors during upload:`));
|
|
1902
|
+
uploadErrors.forEach(e => console.log(chalk.red(` - ${e}`)));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// 5. Confirm deployment (ALWAYS call this to ensure DB is updated)
|
|
1907
|
+
const confirmResponse = await apiClient.invoke('confirm_site_deploy', {
|
|
1908
|
+
community_id: communityId,
|
|
1909
|
+
app_id: appId,
|
|
1910
|
+
token_id: token_id,
|
|
1911
|
+
manifest: {
|
|
1912
|
+
version: '1.0',
|
|
1913
|
+
app_id: appId,
|
|
1914
|
+
community_id: communityId,
|
|
1915
|
+
files: localFiles,
|
|
1916
|
+
source_path: options.path
|
|
1917
|
+
},
|
|
1918
|
+
git_info: gitStatus.isGitRepo ? {
|
|
1919
|
+
commit: gitStatus.shortHash,
|
|
1920
|
+
branch: gitStatus.branch
|
|
1921
|
+
} : null,
|
|
1922
|
+
preview: options.preview || false
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
const result = confirmResponse.message;
|
|
1926
|
+
|
|
1927
|
+
console.log(chalk.green(`\nā
Site uploaded successfully!\n`));
|
|
1928
|
+
console.log(chalk.cyan(` URL: ${result.site_url}`));
|
|
1929
|
+
console.log(chalk.gray(` Files: ${result.files_count}`));
|
|
1930
|
+
console.log(chalk.gray(` Deployed: ${result.deployed_at}`));
|
|
1931
|
+
if (result.preview) {
|
|
1932
|
+
console.log(chalk.yellow(` Mode: PREVIEW`));
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
if (!options.preview) {
|
|
1936
|
+
console.log(chalk.gray(`\n For local development, run:`));
|
|
1937
|
+
console.log(chalk.cyan(` descix site servelocal <port>\n`));
|
|
1938
|
+
}
|
|
1939
|
+
console.log('');
|
|
1940
|
+
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
console.error(chalk.red(`\nā Upload failed: ${error.message}\n`));
|
|
1943
|
+
process.exit(1);
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
// site servelocal - Register local dev server port
|
|
1948
|
+
siteCommand
|
|
1949
|
+
.command('servelocal [port]')
|
|
1950
|
+
.description('Register local dev server port (or "n" to disable)')
|
|
1951
|
+
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
1952
|
+
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
1953
|
+
.action(async (port, options) => {
|
|
1954
|
+
try {
|
|
1955
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
1956
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
1957
|
+
|
|
1958
|
+
const communityId = ctx.communityId;
|
|
1959
|
+
const appId = ctx.appId;
|
|
1960
|
+
|
|
1961
|
+
if (!communityId || !appId) {
|
|
1962
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
1963
|
+
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory\n'));
|
|
1964
|
+
process.exit(1);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
const appConfig = workspaceConfig.getApp(communityId, appId);
|
|
1968
|
+
if (!appConfig) {
|
|
1969
|
+
console.error(chalk.red('\nā App not found in workspace.json.'));
|
|
1970
|
+
console.log(chalk.gray(' Run "npx descix init" to set up your workspace.\n'));
|
|
1971
|
+
process.exit(1);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// Handle disable case
|
|
1975
|
+
if (port === 'n' || port === 'N') {
|
|
1976
|
+
if (appConfig.site) {
|
|
1977
|
+
delete appConfig.site.port;
|
|
1978
|
+
if (Object.keys(appConfig.site).length === 0) {
|
|
1979
|
+
delete appConfig.site;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
await workspaceConfig.save();
|
|
1983
|
+
console.log(chalk.green(`\nā
Local site server disabled for ${communityId}/${appId}\n`));
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// Handle port registration
|
|
1988
|
+
if (!port) {
|
|
1989
|
+
// Show current status
|
|
1990
|
+
const currentPort = appConfig.site?.port;
|
|
1991
|
+
if (currentPort) {
|
|
1992
|
+
console.log(chalk.cyan(`\nš Local site server: port ${currentPort}`));
|
|
1993
|
+
console.log(chalk.gray(` App: ${communityId}/${appId}\n`));
|
|
1994
|
+
} else {
|
|
1995
|
+
console.log(chalk.yellow(`\nā ļø No local site server configured.`));
|
|
1996
|
+
console.log(chalk.gray(` Usage: descix site servelocal <port>\n`));
|
|
1997
|
+
}
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
const portNum = parseInt(port);
|
|
2002
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
2003
|
+
console.error(chalk.red('\nā Invalid port number.\n'));
|
|
2004
|
+
process.exit(1);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
if (!appConfig.site) appConfig.site = {};
|
|
2008
|
+
appConfig.site.port = portNum;
|
|
2009
|
+
if (!appConfig.site.devCommand) {
|
|
2010
|
+
appConfig.site.devCommand = 'npm run dev';
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
await workspaceConfig.save();
|
|
2014
|
+
console.log(chalk.green(`\nā
Local site server registered!`));
|
|
2015
|
+
console.log(chalk.cyan(` Port: ${portNum}`));
|
|
2016
|
+
console.log(chalk.gray(` App: ${communityId}/${appId}`));
|
|
2017
|
+
console.log(chalk.gray(`\n The PWA will proxy /apps/${communityId}/${appId}/* to localhost:${portNum}\n`));
|
|
2018
|
+
|
|
2019
|
+
} catch (error) {
|
|
2020
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
2021
|
+
process.exit(1);
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
// site list - List deployed files (context-aware)
|
|
2026
|
+
siteCommand
|
|
2027
|
+
.command('list')
|
|
2028
|
+
.description('List deployed site files')
|
|
2029
|
+
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
2030
|
+
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
2031
|
+
.option('--preview', 'List preview site')
|
|
2032
|
+
.action(async (options) => {
|
|
2033
|
+
try {
|
|
2034
|
+
const apiClient = new DeSciXApiClient();
|
|
2035
|
+
await requireAuth(apiClient);
|
|
2036
|
+
|
|
2037
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
2038
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
2039
|
+
|
|
2040
|
+
const communityId = ctx.communityId;
|
|
2041
|
+
const appId = ctx.appId;
|
|
2042
|
+
|
|
2043
|
+
if (!communityId || !appId) {
|
|
2044
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
2045
|
+
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory\n'));
|
|
2046
|
+
process.exit(1);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
const response = await apiClient.invoke('list_site_files', {
|
|
2050
|
+
community_id: communityId,
|
|
2051
|
+
app_id: appId,
|
|
2052
|
+
preview: options.preview || false
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
const result = response.message;
|
|
2056
|
+
|
|
2057
|
+
console.log(chalk.cyan(`\nš Site Files: ${communityId}/${appId}${options.preview ? ' (preview)' : ''}\n`));
|
|
2058
|
+
console.log(chalk.gray(` URL: ${result.site_url}`));
|
|
2059
|
+
console.log(chalk.gray(` Files: ${result.files_count}\n`));
|
|
2060
|
+
|
|
2061
|
+
if (result.files.length === 0) {
|
|
2062
|
+
console.log(chalk.gray(' No files deployed.\n'));
|
|
2063
|
+
} else {
|
|
2064
|
+
result.files.forEach(f => {
|
|
2065
|
+
const sizeKB = (f.size / 1024).toFixed(1);
|
|
2066
|
+
console.log(chalk.gray(` ${f.path.padEnd(40)} ${sizeKB.padStart(8)}KB ${f.contentType}`));
|
|
2067
|
+
});
|
|
2068
|
+
console.log('');
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
console.error(chalk.red(`\nā Error: ${error.message}\n`));
|
|
2073
|
+
process.exit(1);
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
// site status - Get deployment status (context-aware)
|
|
2078
|
+
siteCommand
|
|
2079
|
+
.command('status')
|
|
2080
|
+
.description('Get deployment status and manifest')
|
|
2081
|
+
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
2082
|
+
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
2083
|
+
.option('--preview', 'Check preview site')
|
|
2084
|
+
.action(async (options) => {
|
|
2085
|
+
try {
|
|
2086
|
+
const apiClient = new DeSciXApiClient();
|
|
2087
|
+
await requireAuth(apiClient);
|
|
2088
|
+
|
|
2089
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
2090
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
2091
|
+
|
|
2092
|
+
const communityId = ctx.communityId;
|
|
2093
|
+
const appId = ctx.appId;
|
|
2094
|
+
|
|
2095
|
+
if (!communityId || !appId) {
|
|
2096
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
2097
|
+
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory\n'));
|
|
2098
|
+
process.exit(1);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
const response = await apiClient.invoke('get_site_manifest', {
|
|
2102
|
+
community_id: communityId,
|
|
2103
|
+
app_id: appId,
|
|
2104
|
+
preview: options.preview || false
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
const result = response.message;
|
|
2108
|
+
|
|
2109
|
+
console.log(chalk.cyan(`\nš Site Status: ${communityId}/${appId}${options.preview ? ' (preview)' : ''}\n`));
|
|
2110
|
+
|
|
2111
|
+
if (!result.exists) {
|
|
2112
|
+
console.log(chalk.yellow(' No site deployed.\n'));
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
const manifest = result.manifest;
|
|
2117
|
+
const deploy = manifest.deploy || {};
|
|
2118
|
+
|
|
2119
|
+
console.log(chalk.gray(` URL: ${result.site_url}`));
|
|
2120
|
+
console.log(chalk.gray(` Deployed: ${deploy.deployed_at || 'unknown'}`));
|
|
2121
|
+
console.log(chalk.gray(` By: ${deploy.deployed_by || 'unknown'}`));
|
|
2122
|
+
if (deploy.git_commit) {
|
|
2123
|
+
console.log(chalk.gray(` Git: ${deploy.git_branch || 'unknown'} @ ${deploy.git_commit}`));
|
|
2124
|
+
}
|
|
2125
|
+
console.log(chalk.gray(` Files: ${Object.keys(manifest.files || {}).length}`));
|
|
2126
|
+
console.log('');
|
|
2127
|
+
|
|
2128
|
+
} catch (error) {
|
|
2129
|
+
console.error(chalk.red(`\nā Error: ${error.message}\n`));
|
|
2130
|
+
process.exit(1);
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
// site delete - Delete deployed site (context-aware)
|
|
2135
|
+
siteCommand
|
|
2136
|
+
.command('delete')
|
|
2137
|
+
.description('Delete deployed site')
|
|
2138
|
+
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
2139
|
+
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
2140
|
+
.option('--preview', 'Delete preview site only')
|
|
2141
|
+
.option('--confirm', 'Skip confirmation prompt')
|
|
2142
|
+
.action(async (options) => {
|
|
2143
|
+
try {
|
|
2144
|
+
const apiClient = new DeSciXApiClient();
|
|
2145
|
+
await requireAuth(apiClient);
|
|
2146
|
+
|
|
2147
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
2148
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
2149
|
+
|
|
2150
|
+
const communityId = ctx.communityId;
|
|
2151
|
+
const appId = ctx.appId;
|
|
2152
|
+
|
|
2153
|
+
if (!communityId || !appId) {
|
|
2154
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
2155
|
+
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory\n'));
|
|
2156
|
+
process.exit(1);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
if (!options.confirm) {
|
|
2160
|
+
const readline = await import('readline');
|
|
2161
|
+
const rl = readline.createInterface({
|
|
2162
|
+
input: process.stdin,
|
|
2163
|
+
output: process.stdout
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
const answer = await new Promise(resolve => {
|
|
2167
|
+
rl.question(chalk.yellow(`\nā ļø Delete site for ${communityId}/${appId}${options.preview ? ' (preview)' : ''}? (y/N) `), resolve);
|
|
2168
|
+
});
|
|
2169
|
+
rl.close();
|
|
2170
|
+
|
|
2171
|
+
if (answer.toLowerCase() !== 'y') {
|
|
2172
|
+
console.log(chalk.gray('\n Cancelled.\n'));
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
const response = await apiClient.invoke('delete_site_files', {
|
|
2178
|
+
community_id: communityId,
|
|
2179
|
+
app_id: appId,
|
|
2180
|
+
preview: options.preview || false
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
const result = response.message;
|
|
2184
|
+
|
|
2185
|
+
if (result.success) {
|
|
2186
|
+
console.log(chalk.green(`\nā
Site deleted successfully!`));
|
|
2187
|
+
console.log(chalk.gray(` Files removed: ${result.deleted_count}\n`));
|
|
2188
|
+
} else {
|
|
2189
|
+
console.log(chalk.yellow(`\nā ļø Deletion completed with errors:`));
|
|
2190
|
+
result.errors.forEach(e => console.log(chalk.red(` - ${e}`)));
|
|
2191
|
+
console.log(chalk.gray(` Files removed: ${result.deleted_count}\n`));
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
console.error(chalk.red(`\nā Error: ${error.message}\n`));
|
|
2196
|
+
process.exit(1);
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
// ============ Microservice Commands ============
|
|
2201
|
+
|
|
2202
|
+
const microserviceCommand = program
|
|
2203
|
+
.command('microservice')
|
|
2204
|
+
.description('Microservice lifecycle management');
|
|
2205
|
+
|
|
2206
|
+
// microservice init - Copy scaffold to app
|
|
2207
|
+
microserviceCommand
|
|
2208
|
+
.command('init')
|
|
2209
|
+
.description('Initialize microservice scaffold in current app')
|
|
2210
|
+
.option('-f, --force', 'Overwrite existing microservice/ folder')
|
|
2211
|
+
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
2212
|
+
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
2213
|
+
.action(async (options) => {
|
|
2214
|
+
try {
|
|
2215
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
2216
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
2217
|
+
|
|
2218
|
+
if (!ctx.communityId || !ctx.appId) {
|
|
2219
|
+
console.error(chalk.red('\nā No app context found.'));
|
|
2220
|
+
console.log(chalk.gray(' cd into an app directory, or use -c and -a flags\n'));
|
|
2221
|
+
process.exit(1);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
const appConfig = workspaceConfig.getApp(ctx.communityId, ctx.appId);
|
|
2225
|
+
if (!appConfig) {
|
|
2226
|
+
console.error(chalk.red('\nā App not found in workspace.json.'));
|
|
2227
|
+
console.log(chalk.gray(' Run "npx descix init" to set up your workspace.\n'));
|
|
2228
|
+
process.exit(1);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
const appPath = appConfig.absolutePath ||
|
|
2232
|
+
path.join(workspaceConfig.getWorkspaceRoot(), appConfig.localPath);
|
|
2233
|
+
|
|
2234
|
+
console.log(chalk.cyan('\nš Adding microservice scaffold...\n'));
|
|
2235
|
+
|
|
2236
|
+
const { copyScaffold } = await import('../lib/core/Hydrator.js');
|
|
2237
|
+
const stats = await copyScaffold('microservice', appPath, {
|
|
2238
|
+
verbose: true,
|
|
2239
|
+
force: options.force
|
|
2240
|
+
});
|
|
2241
|
+
|
|
2242
|
+
// Configuration Injection
|
|
2243
|
+
const microserviceDir = path.join(appPath, 'microservice');
|
|
2244
|
+
const defaultsPath = path.join(microserviceDir, 'defaults-config.json');
|
|
2245
|
+
const overridesPath = path.join(microserviceDir, 'dev-overrides.json');
|
|
2246
|
+
|
|
2247
|
+
// 1. Inject Context into defaults-config.json
|
|
2248
|
+
try {
|
|
2249
|
+
const defaultsContent = await fs.readFile(defaultsPath, 'utf-8');
|
|
2250
|
+
const defaults = JSON.parse(defaultsContent);
|
|
2251
|
+
defaults.community_id = ctx.communityId;
|
|
2252
|
+
defaults.app_id = ctx.appId;
|
|
2253
|
+
await fs.writeFile(defaultsPath, JSON.stringify(defaults, null, 2));
|
|
2254
|
+
console.log(chalk.gray(` ā Injected context into defaults-config.json`));
|
|
2255
|
+
} catch (err) {
|
|
2256
|
+
console.warn(chalk.yellow(` ā Could not update defaults-config.json: ${err.message}`));
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// 2. Inject Credentials into dev-overrides.json
|
|
2260
|
+
try {
|
|
2261
|
+
const apiClient = new DeSciXApiClient();
|
|
2262
|
+
const credentials = await apiClient.loadCredentials();
|
|
2263
|
+
|
|
2264
|
+
if (credentials) {
|
|
2265
|
+
const overrides = {
|
|
2266
|
+
CORE_API_URL: apiClient.baseUrl ? `${apiClient.baseUrl}/apifront` : undefined,
|
|
2267
|
+
OWNER_WALLET_ADDRESS: credentials.walletAddress,
|
|
2268
|
+
OWNER_SIGNATURE: credentials.signature,
|
|
2269
|
+
OWNER_USER_ID: credentials.userId
|
|
2270
|
+
};
|
|
2271
|
+
|
|
2272
|
+
// Merge with existing overrides if any
|
|
2273
|
+
let existingOverrides = {};
|
|
2274
|
+
try {
|
|
2275
|
+
const content = await fs.readFile(overridesPath, 'utf-8');
|
|
2276
|
+
existingOverrides = JSON.parse(content);
|
|
2277
|
+
} catch {}
|
|
2278
|
+
|
|
2279
|
+
await fs.writeFile(overridesPath, JSON.stringify({ ...existingOverrides, ...overrides }, null, 2));
|
|
2280
|
+
console.log(chalk.gray(` ā Injected owner credentials into dev-overrides.json`));
|
|
2281
|
+
}
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
console.warn(chalk.yellow(` ā Could not update dev-overrides.json: ${err.message}`));
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
console.log(chalk.green(`\nā
Microservice scaffold added (${stats.copied} files)\n`));
|
|
2287
|
+
console.log(chalk.gray(' Next steps:'));
|
|
2288
|
+
console.log(chalk.gray(' - Edit manifest.json to define your commands'));
|
|
2289
|
+
console.log(chalk.gray(' - Implement handlers in services/commandHandlers/'));
|
|
2290
|
+
console.log(chalk.gray(' - Run descix microservice register to register with gateway\n'));
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
2293
|
+
process.exit(1);
|
|
2294
|
+
}
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
// microservice register - Register with gateway
|
|
2298
|
+
microserviceCommand
|
|
2299
|
+
.command('register')
|
|
2300
|
+
.description('Register microservice with gateway (requires SERVICE_README)')
|
|
2301
|
+
.option('-m, --manifest <path>', 'Path to manifest.json', './manifest.json')
|
|
2302
|
+
.option('-r, --readme <path>', 'Path to local SERVICE_README file')
|
|
2303
|
+
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
2304
|
+
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
2305
|
+
.option('--skip-readme-check', 'Skip SERVICE_README check (not recommended)')
|
|
2306
|
+
.action(async (options) => {
|
|
2307
|
+
try {
|
|
2308
|
+
const apiClient = new DeSciXApiClient();
|
|
2309
|
+
await requireAuth(apiClient);
|
|
2310
|
+
|
|
2311
|
+
// Auto-detect context
|
|
2312
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
2313
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
2314
|
+
|
|
2315
|
+
// Read manifest.json
|
|
2316
|
+
const manifestPath = path.resolve(options.manifest);
|
|
2317
|
+
|
|
2318
|
+
let manifest;
|
|
2319
|
+
try {
|
|
2320
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
2321
|
+
manifest = JSON.parse(manifestContent);
|
|
2322
|
+
} catch (error) {
|
|
2323
|
+
if (error.code === 'ENOENT') {
|
|
2324
|
+
console.error(chalk.red(`ā manifest.json not found at ${manifestPath}`));
|
|
2325
|
+
console.error(chalk.gray('\nCreate a manifest.json with your service configuration.'));
|
|
2326
|
+
console.error(chalk.gray('Use descix microservice init to scaffold a template.\n'));
|
|
2327
|
+
process.exit(1);
|
|
2328
|
+
}
|
|
2329
|
+
throw new Error(`Failed to parse manifest.json: ${error.message}`);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// Validate required fields
|
|
2333
|
+
if (!manifest.service?.name) {
|
|
2334
|
+
throw new Error('manifest.service.name is required');
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// Use detected context if manifest fields are missing
|
|
2338
|
+
if (!manifest.service?.app_id && ctx.appId) {
|
|
2339
|
+
if (!manifest.service) manifest.service = {};
|
|
2340
|
+
manifest.service.app_id = ctx.appId;
|
|
2341
|
+
}
|
|
2342
|
+
if (!manifest.service?.community_id && ctx.communityId) {
|
|
2343
|
+
if (!manifest.service) manifest.service = {};
|
|
2344
|
+
manifest.service.community_id = ctx.communityId;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
if (!manifest.service?.app_id) {
|
|
2348
|
+
throw new Error('manifest.service.app_id is required (or use -a flag)');
|
|
2349
|
+
}
|
|
2350
|
+
if (!manifest.service?.community_id) {
|
|
2351
|
+
throw new Error('manifest.service.community_id is required (or use -c flag)');
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
const serviceName = manifest.service?.name;
|
|
2355
|
+
|
|
2356
|
+
if (!serviceName) {
|
|
2357
|
+
throw new Error('manifest.service.name is required');
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
const appId = manifest.service?.app_id;
|
|
2361
|
+
const communityId = manifest.service?.community_id;
|
|
2362
|
+
const readmeFileName = `SERVICE_README_${serviceName}.md`;
|
|
2363
|
+
|
|
2364
|
+
console.log(chalk.cyan(`\nš¦ Registering microservice: ${serviceName}\n`));
|
|
2365
|
+
console.log(chalk.gray(` Community: ${communityId}`));
|
|
2366
|
+
console.log(chalk.gray(` App: ${appId}`));
|
|
2367
|
+
console.log(chalk.gray(` Commands: ${Object.keys(manifest.commands || {}).length}\n`));
|
|
2368
|
+
|
|
2369
|
+
// Option 1: Local README file provided via --readme
|
|
2370
|
+
if (options.readme) {
|
|
2371
|
+
const readmePath = path.resolve(options.readme);
|
|
2372
|
+
console.log(chalk.cyan(`š Reading local README: ${readmePath}\n`));
|
|
2373
|
+
|
|
2374
|
+
let readmeContent;
|
|
2375
|
+
try {
|
|
2376
|
+
readmeContent = await fs.readFile(readmePath, 'utf-8');
|
|
2377
|
+
} catch (error) {
|
|
2378
|
+
if (error.code === 'ENOENT') {
|
|
2379
|
+
console.error(chalk.red(`ā README file not found: ${readmePath}`));
|
|
2380
|
+
process.exit(1);
|
|
2381
|
+
}
|
|
2382
|
+
throw new Error(`Failed to read README: ${error.message}`);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// Register with local README content for vectorization
|
|
2386
|
+
const response = await apiClient.invoke('register_service', {
|
|
2387
|
+
manifest,
|
|
2388
|
+
readme_content: readmeContent
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
console.log(chalk.green('ā
Microservice registered successfully!\n'));
|
|
2392
|
+
console.log(chalk.cyan(` Service: ${serviceName}`));
|
|
2393
|
+
console.log(chalk.gray(` Commands: ${Object.keys(manifest.commands || {}).length}`));
|
|
2394
|
+
console.log(chalk.gray(` README vectorized for tell_me_how discovery\n`));
|
|
2395
|
+
|
|
2396
|
+
// Option 2: Check for SERVICE_README in Drive folder
|
|
2397
|
+
} else if (!options.skipReadmeCheck) {
|
|
2398
|
+
console.log(chalk.cyan(`š Checking for ${readmeFileName} in app Drive folder...\n`));
|
|
2399
|
+
|
|
2400
|
+
const checkResponse = await apiClient.invoke('check_service_readme_exists', {
|
|
2401
|
+
community_id: communityId,
|
|
2402
|
+
app_id: appId,
|
|
2403
|
+
service_name: serviceName
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
const readmeExists = checkResponse.message?.exists || checkResponse.exists || false;
|
|
2407
|
+
|
|
2408
|
+
if (!readmeExists) {
|
|
2409
|
+
// Output AI-friendly instructions for creating the README
|
|
2410
|
+
outputServiceReadmeInstructions(serviceName, appId, communityId, manifest);
|
|
2411
|
+
process.exit(1);
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
console.log(chalk.green(`ā
Found ${readmeFileName}\n`));
|
|
2415
|
+
|
|
2416
|
+
// Read README content from Drive
|
|
2417
|
+
const readmeResponse = await apiClient.invoke('get_service_readme_content', {
|
|
2418
|
+
community_id: communityId,
|
|
2419
|
+
app_id: appId,
|
|
2420
|
+
service_name: serviceName
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
const readmeContent = readmeResponse.message?.content || readmeResponse.content || '';
|
|
2424
|
+
|
|
2425
|
+
// Register with README content for vectorization
|
|
2426
|
+
const response = await apiClient.invoke('register_service', {
|
|
2427
|
+
manifest,
|
|
2428
|
+
readme_content: readmeContent
|
|
2429
|
+
});
|
|
2430
|
+
|
|
2431
|
+
console.log(chalk.green('ā
Microservice registered successfully!\n'));
|
|
2432
|
+
console.log(chalk.cyan(` Service: ${serviceName}`));
|
|
2433
|
+
console.log(chalk.gray(` Commands: ${Object.keys(manifest.commands || {}).length}`));
|
|
2434
|
+
console.log(chalk.gray(` README vectorized for tell_me_how discovery\n`));
|
|
2435
|
+
|
|
2436
|
+
// Option 3: Skip README check
|
|
2437
|
+
} else {
|
|
2438
|
+
// Skip README check - register without vectorization
|
|
2439
|
+
console.log(chalk.yellow('ā ļø Skipping README check (tool discovery will be limited)\n'));
|
|
2440
|
+
|
|
2441
|
+
const response = await apiClient.invoke('register_service', { manifest });
|
|
2442
|
+
|
|
2443
|
+
console.log(chalk.green('ā
Microservice registered successfully!\n'));
|
|
2444
|
+
console.log(chalk.cyan(` Service: ${serviceName}`));
|
|
2445
|
+
console.log(chalk.gray(` Commands: ${Object.keys(manifest.commands || {}).length}`));
|
|
2446
|
+
console.log(chalk.yellow(` ā ļø No README - service won't appear in tell_me_how results\n`));
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
} catch (error) {
|
|
2450
|
+
console.error(chalk.red(`\nā Registration failed: ${error.message}\n`));
|
|
2451
|
+
process.exit(1);
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
|
|
2455
|
+
// microservice vectorize - Vectorize SERVICE_README (context-aware)
|
|
2456
|
+
microserviceCommand
|
|
2457
|
+
.command('vectorize')
|
|
2458
|
+
.description('Vectorize SERVICE_README for tell_me_how discovery')
|
|
2459
|
+
.option('-n, --name <name>', 'Service name (reads from manifest.json if not provided)')
|
|
2460
|
+
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
2461
|
+
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
2462
|
+
.option('-r, --readme <path>', 'Path to SERVICE_README file')
|
|
2463
|
+
.action(async (options) => {
|
|
2464
|
+
try {
|
|
2465
|
+
const apiClient = new DeSciXApiClient();
|
|
2466
|
+
await requireAuth(apiClient);
|
|
2467
|
+
|
|
2468
|
+
// Auto-detect context
|
|
2469
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
2470
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
2471
|
+
|
|
2472
|
+
let communityId = ctx.communityId;
|
|
2473
|
+
let appId = ctx.appId;
|
|
2474
|
+
let serviceName = options.name;
|
|
2475
|
+
|
|
2476
|
+
// Try to read from manifest.json if not provided
|
|
2477
|
+
if (!serviceName || !communityId || !appId) {
|
|
2478
|
+
try {
|
|
2479
|
+
// Try to find manifest.json in current or parent directories
|
|
2480
|
+
let manifestPath = './manifest.json';
|
|
2481
|
+
|
|
2482
|
+
// Check current directory first
|
|
2483
|
+
try {
|
|
2484
|
+
await fs.access(manifestPath);
|
|
2485
|
+
} catch {
|
|
2486
|
+
// If not in current directory, try to find the microservice path for detected context
|
|
2487
|
+
if (ctx.communityId && ctx.appId && workspaceConfig.workspaceRoot) {
|
|
2488
|
+
const appConfig = workspaceConfig.getApp(ctx.communityId, ctx.appId);
|
|
2489
|
+
if (appConfig) {
|
|
2490
|
+
const microservicePath = path.join(
|
|
2491
|
+
workspaceConfig.workspaceRoot,
|
|
2492
|
+
appConfig.localPath,
|
|
2493
|
+
'microservice'
|
|
2494
|
+
);
|
|
2495
|
+
manifestPath = path.join(microservicePath, 'manifest.json');
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// console.log(chalk.gray(` Reading manifest from: ${manifestPath}`));
|
|
2501
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
2502
|
+
const manifest = JSON.parse(manifestContent);
|
|
2503
|
+
if (!serviceName) serviceName = manifest.service?.name;
|
|
2504
|
+
if (!communityId) communityId = manifest.service?.community_id || ctx.communityId;
|
|
2505
|
+
if (!appId) appId = manifest.service?.app_id || ctx.appId;
|
|
2506
|
+
} catch (e) {
|
|
2507
|
+
// console.log(chalk.gray(` Manifest not found or invalid: ${e.message}`));
|
|
2508
|
+
// manifest.json not found, use detected context
|
|
2509
|
+
if (!communityId) communityId = ctx.communityId;
|
|
2510
|
+
if (!appId) appId = ctx.appId;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
if (!serviceName) {
|
|
2515
|
+
console.error(chalk.red('\nā Service name required.'));
|
|
2516
|
+
console.log(chalk.gray(' Provide -n flag or ensure manifest.json exists in current directory.\n'));
|
|
2517
|
+
process.exit(1);
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
if (!communityId || !appId) {
|
|
2521
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
2522
|
+
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory.\n'));
|
|
2523
|
+
process.exit(1);
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// Find README file
|
|
2527
|
+
let readmePath = options.readme;
|
|
2528
|
+
if (!readmePath) {
|
|
2529
|
+
// Try common locations
|
|
2530
|
+
const possiblePaths = [
|
|
2531
|
+
`./SERVICE_README_${serviceName}.md`,
|
|
2532
|
+
`./microservice/SERVICE_README_${serviceName}.md`,
|
|
2533
|
+
`./SERVICE_README.md`
|
|
2534
|
+
];
|
|
2535
|
+
for (const p of possiblePaths) {
|
|
2536
|
+
try {
|
|
2537
|
+
await fs.access(p);
|
|
2538
|
+
readmePath = p;
|
|
2539
|
+
break;
|
|
2540
|
+
} catch {
|
|
2541
|
+
// Try next
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
if (!readmePath) {
|
|
2547
|
+
console.error(chalk.red(`\nā README file not found.`));
|
|
2548
|
+
console.log(chalk.gray(` Expected: SERVICE_README_${serviceName}.md`));
|
|
2549
|
+
console.log(chalk.gray(` Provide path with -r flag.\n`));
|
|
2550
|
+
process.exit(1);
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
readmePath = path.resolve(readmePath);
|
|
2554
|
+
console.log(chalk.cyan(`\nš Vectorizing README for service: ${serviceName}\n`));
|
|
2555
|
+
console.log(chalk.gray(` Community: ${communityId}`));
|
|
2556
|
+
console.log(chalk.gray(` App: ${appId}`));
|
|
2557
|
+
console.log(chalk.gray(` README: ${readmePath}\n`));
|
|
2558
|
+
|
|
2559
|
+
let readmeContent;
|
|
2560
|
+
try {
|
|
2561
|
+
readmeContent = await fs.readFile(readmePath, 'utf-8');
|
|
2562
|
+
} catch (error) {
|
|
2563
|
+
if (error.code === 'ENOENT') {
|
|
2564
|
+
console.error(chalk.red(`ā README file not found: ${readmePath}`));
|
|
2565
|
+
process.exit(1);
|
|
2566
|
+
}
|
|
2567
|
+
throw new Error(`Failed to read README: ${error.message}`);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
const response = await apiClient.invoke('vectorize_service_readme', {
|
|
2571
|
+
service_name: serviceName,
|
|
2572
|
+
community_id: communityId,
|
|
2573
|
+
app_id: appId,
|
|
2574
|
+
readme_content: readmeContent
|
|
2575
|
+
});
|
|
2576
|
+
|
|
2577
|
+
// Handle wrapped response
|
|
2578
|
+
const result = response.message || response;
|
|
2579
|
+
|
|
2580
|
+
const isSuccess = result.status === 'OK' ||
|
|
2581
|
+
(typeof result === 'string' && result.includes('vectorized')) ||
|
|
2582
|
+
(result.message && result.message.includes('vectorized')) ||
|
|
2583
|
+
result.chunks_created !== undefined;
|
|
2584
|
+
|
|
2585
|
+
if (isSuccess) {
|
|
2586
|
+
let chunksCreated = result.chunks_created;
|
|
2587
|
+
if (!chunksCreated) {
|
|
2588
|
+
const msgStr = typeof result === 'string' ? result : result.message;
|
|
2589
|
+
const match = msgStr?.match(/(\d+) chunks/);
|
|
2590
|
+
if (match) chunksCreated = match[1];
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
console.log(chalk.green('ā
README vectorized successfully!\n'));
|
|
2594
|
+
console.log(chalk.gray(` Chunks created: ${chunksCreated || 'unknown'}`));
|
|
2595
|
+
console.log(chalk.gray(` Service will now appear in tell_me_how results\n`));
|
|
2596
|
+
} else {
|
|
2597
|
+
const errorMsg = result.message || result.error || JSON.stringify(result);
|
|
2598
|
+
console.error(chalk.red(`ā Vectorization failed: ${errorMsg}\n`));
|
|
2599
|
+
process.exit(1);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
} catch (error) {
|
|
2603
|
+
console.error(chalk.red(`\nā Vectorization failed: ${error.message}\n`));
|
|
2604
|
+
process.exit(1);
|
|
2605
|
+
}
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
// microservice list - List registered services
|
|
2609
|
+
microserviceCommand
|
|
2610
|
+
.command('list')
|
|
2611
|
+
.description('List all registered microservices')
|
|
2612
|
+
.action(async () => {
|
|
2613
|
+
try {
|
|
2614
|
+
const apiClient = new DeSciXApiClient();
|
|
2615
|
+
// list_services is public, no auth required
|
|
2616
|
+
|
|
2617
|
+
const response = await apiClient.invoke('list_services', {}, { allowGuest: true });
|
|
2618
|
+
const services = response.message?.services || response.services || [];
|
|
2619
|
+
|
|
2620
|
+
console.log(chalk.green(`\nā
Found ${services.length} registered microservices\n`));
|
|
2621
|
+
|
|
2622
|
+
if (services.length === 0) {
|
|
2623
|
+
console.log(chalk.gray(' No microservices registered yet.\n'));
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
console.log(chalk.cyan('Microservices:'));
|
|
2628
|
+
services.forEach((s, idx) => {
|
|
2629
|
+
console.log(chalk.yellow(` ${idx + 1}. ${s.name} (v${s.version || '?'})`));
|
|
2630
|
+
console.log(chalk.gray(` Domain: ${s.domain}`));
|
|
2631
|
+
console.log(chalk.gray(` Commands: ${s.commandCount || 0}`));
|
|
2632
|
+
console.log(chalk.gray(` Status: ${s.status || 'unknown'}`));
|
|
2633
|
+
console.log();
|
|
2634
|
+
});
|
|
2635
|
+
} catch (error) {
|
|
2636
|
+
console.error(chalk.red(error.message));
|
|
2637
|
+
process.exit(1);
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
|
|
2641
|
+
// microservice health - Check service health
|
|
2642
|
+
microserviceCommand
|
|
2643
|
+
.command('health <name>')
|
|
2644
|
+
.description('Check health of a registered microservice')
|
|
2645
|
+
.action(async (serviceName) => {
|
|
2646
|
+
try {
|
|
2647
|
+
const apiClient = new DeSciXApiClient();
|
|
2648
|
+
|
|
2649
|
+
const response = await apiClient.invoke('service_health_check', { service_name: serviceName }, { allowGuest: true });
|
|
2650
|
+
const result = response.message || response;
|
|
2651
|
+
|
|
2652
|
+
if (result.healthy) {
|
|
2653
|
+
console.log(chalk.green(`\nā
Microservice '${serviceName}' is healthy\n`));
|
|
2654
|
+
console.log(chalk.gray(` Status: ${result.statusCode}`));
|
|
2655
|
+
if (result.healthData) {
|
|
2656
|
+
console.log(chalk.gray(` Data: ${JSON.stringify(result.healthData)}`));
|
|
2657
|
+
}
|
|
2658
|
+
} else {
|
|
2659
|
+
console.log(chalk.yellow(`\nā ļø Microservice '${serviceName}' is unhealthy\n`));
|
|
2660
|
+
console.log(chalk.gray(` Error: ${result.error || 'Unknown'}`));
|
|
2661
|
+
}
|
|
2662
|
+
console.log();
|
|
2663
|
+
} catch (error) {
|
|
2664
|
+
console.error(chalk.red(error.message));
|
|
2665
|
+
process.exit(1);
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
|
|
2669
|
+
// microservice reload - Reload manifests (admin)
|
|
2670
|
+
microserviceCommand
|
|
2671
|
+
.command('reload')
|
|
2672
|
+
.description('Reload microservice manifests (admin only)')
|
|
2673
|
+
.action(async () => {
|
|
2674
|
+
try {
|
|
2675
|
+
const apiClient = new DeSciXApiClient();
|
|
2676
|
+
await requireAuth(apiClient);
|
|
2677
|
+
|
|
2678
|
+
console.log(chalk.cyan('\nš Reloading microservice manifests...\n'));
|
|
2679
|
+
|
|
2680
|
+
const response = await apiClient.invoke('reload_service_manifests', {});
|
|
2681
|
+
const result = response.message || response;
|
|
2682
|
+
|
|
2683
|
+
console.log(chalk.green(`ā
${result}\n`));
|
|
2684
|
+
|
|
2685
|
+
if (response.services) {
|
|
2686
|
+
console.log(chalk.gray(` Services: ${response.services.join(', ')}\n`));
|
|
2687
|
+
}
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
console.error(chalk.red(error.message));
|
|
2690
|
+
process.exit(1);
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
|
|
2694
|
+
// microservice register-delegate - Register delegate key
|
|
2695
|
+
microserviceCommand
|
|
2696
|
+
.command('register-delegate')
|
|
2697
|
+
.description('Register service delegate key for Intelligent Mesh')
|
|
2698
|
+
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
2699
|
+
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
2700
|
+
.option('-s, --slot <id>', 'Service slot ID (uses first available if not provided)')
|
|
2701
|
+
.action(async (options) => {
|
|
2702
|
+
try {
|
|
2703
|
+
const apiClient = new DeSciXApiClient();
|
|
2704
|
+
await requireAuth(apiClient);
|
|
2705
|
+
|
|
2706
|
+
// Auto-detect context
|
|
2707
|
+
const workspaceConfig = await WorkspaceConfig.load();
|
|
2708
|
+
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
2709
|
+
|
|
2710
|
+
let communityId = ctx.communityId;
|
|
2711
|
+
let appId = ctx.appId;
|
|
2712
|
+
|
|
2713
|
+
// Try manifest.json if not detected
|
|
2714
|
+
if (!communityId || !appId) {
|
|
2715
|
+
try {
|
|
2716
|
+
const manifestContent = await fs.readFile('./manifest.json', 'utf-8');
|
|
2717
|
+
const manifest = JSON.parse(manifestContent);
|
|
2718
|
+
if (!communityId) communityId = manifest.service?.community_id;
|
|
2719
|
+
if (!appId) appId = manifest.service?.app_id;
|
|
2720
|
+
} catch {
|
|
2721
|
+
// Not found
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
if (!communityId || !appId) {
|
|
2726
|
+
console.error(chalk.red('\nā Community and App ID required.'));
|
|
2727
|
+
console.log(chalk.gray(' Either provide -c and -a flags, cd into an app directory, or have manifest.json present.\n'));
|
|
2728
|
+
process.exit(1);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
console.log(chalk.cyan('\nš Registering Service Delegate Key\n'));
|
|
2732
|
+
|
|
2733
|
+
// Fetch entitlements
|
|
2734
|
+
const entitlementsResponse = await apiClient.invoke('fetch_my_purchases', {});
|
|
2735
|
+
const entitlements = entitlementsResponse.message || {};
|
|
2736
|
+
const serviceSlots = entitlements.service_slots || [];
|
|
2737
|
+
|
|
2738
|
+
if (serviceSlots.length === 0) {
|
|
2739
|
+
console.error(chalk.red('ā No service slots available.'));
|
|
2740
|
+
console.log(chalk.white(' You need a "Runner NFT" or Subscription to create a service.'));
|
|
2741
|
+
process.exit(1);
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// Select slot
|
|
2745
|
+
let selectedSlot = serviceSlots[0];
|
|
2746
|
+
if (options.slot) {
|
|
2747
|
+
selectedSlot = serviceSlots.find(s => (s.nft_id || s.id) === options.slot);
|
|
2748
|
+
if (!selectedSlot) {
|
|
2749
|
+
console.error(chalk.red(`ā Slot ${options.slot} not found in your entitlements.`));
|
|
2750
|
+
process.exit(1);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
console.log(chalk.gray(` Selected slot: ${selectedSlot.name} (${selectedSlot.type})\n`));
|
|
2755
|
+
|
|
2756
|
+
// Generate key pair
|
|
2757
|
+
const crypto = await import('crypto');
|
|
2758
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
|
|
2759
|
+
namedCurve: 'secp256k1',
|
|
2760
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
2761
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
2762
|
+
});
|
|
2763
|
+
|
|
2764
|
+
console.log(chalk.gray(' Generating key pair...'));
|
|
2765
|
+
|
|
2766
|
+
// Register with Virtual Registry
|
|
2767
|
+
const registerResponse = await apiClient.invoke('register_delegate', {
|
|
2768
|
+
slot_id: selectedSlot.nft_id || selectedSlot.id,
|
|
2769
|
+
slot_type: selectedSlot.type,
|
|
2770
|
+
public_key: publicKey,
|
|
2771
|
+
app_id: appId,
|
|
2772
|
+
community_id: communityId
|
|
2773
|
+
});
|
|
2774
|
+
|
|
2775
|
+
console.log(chalk.green(' ā Delegate registered with Core\n'));
|
|
2776
|
+
|
|
2777
|
+
// Save to dev-overrides.json
|
|
2778
|
+
const overridesPath = path.resolve(process.cwd(), 'dev-overrides.json');
|
|
2779
|
+
let overrides = {};
|
|
2780
|
+
|
|
2781
|
+
try {
|
|
2782
|
+
const content = await fs.readFile(overridesPath, 'utf-8');
|
|
2783
|
+
overrides = JSON.parse(content);
|
|
2784
|
+
} catch (e) {
|
|
2785
|
+
// File doesn't exist, start fresh
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
overrides.SERVICE_KEY = {
|
|
2789
|
+
privateKey,
|
|
2790
|
+
publicKey,
|
|
2791
|
+
slotId: selectedSlot.nft_id || selectedSlot.id,
|
|
2792
|
+
slotType: selectedSlot.type,
|
|
2793
|
+
appId: appId,
|
|
2794
|
+
communityId: communityId,
|
|
2795
|
+
createdAt: new Date().toISOString()
|
|
2796
|
+
};
|
|
2797
|
+
|
|
2798
|
+
await fs.writeFile(overridesPath, JSON.stringify(overrides, null, 2));
|
|
2799
|
+
console.log(chalk.green(`ā
Key saved to dev-overrides.json\n`));
|
|
2800
|
+
console.log(chalk.gray(` Path: ${overridesPath}\n`));
|
|
2801
|
+
|
|
2802
|
+
} catch (error) {
|
|
2803
|
+
console.error(chalk.red(`\nā Error: ${error.message}\n`));
|
|
2804
|
+
process.exit(1);
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
|
|
2808
|
+
// ============ Role Commands ============
|
|
2809
|
+
|
|
2810
|
+
const roleCommand = program
|
|
2811
|
+
.command('role')
|
|
2812
|
+
.description('Role management operations');
|
|
2813
|
+
|
|
2814
|
+
roleCommand
|
|
2815
|
+
.command('create')
|
|
2816
|
+
.description('Create a new role')
|
|
2817
|
+
.requiredOption('--scope <scope>', 'Scope: PLATFORM, COMMUNITY, or APP')
|
|
2818
|
+
.requiredOption('--scope-id <id>', 'Scope ID (community_id or app_id)')
|
|
2819
|
+
.requiredOption('--role-id <id>', 'Role ID (e.g., "member", "admin")')
|
|
2820
|
+
.requiredOption('--name <name>', 'Role display name')
|
|
2821
|
+
.option('--description <text>', 'Role description')
|
|
2822
|
+
.option('--price-usdsci <amount>', 'Price in USDCX (default: 0 = free)')
|
|
2823
|
+
.action(async (options) => {
|
|
2824
|
+
try {
|
|
2825
|
+
const apiClient = new DeSciXApiClient();
|
|
2826
|
+
await requireAuth(apiClient);
|
|
2827
|
+
|
|
2828
|
+
const price = options.priceUsdsci ? parseFloat(options.priceUsdsci) : 0;
|
|
2829
|
+
if (isNaN(price) || price < 0) {
|
|
2830
|
+
throw new Error('Price must be a non-negative number');
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
const response = await apiClient.invoke('create_role', {
|
|
2834
|
+
scope: options.scope,
|
|
2835
|
+
scope_id: options.scopeId,
|
|
2836
|
+
role_id: options.roleId,
|
|
2837
|
+
role_name: options.name,
|
|
2838
|
+
role_description: options.description || '',
|
|
2839
|
+
price_usdsci: price
|
|
2840
|
+
});
|
|
2841
|
+
const result = response.message || response;
|
|
2842
|
+
|
|
2843
|
+
console.log(chalk.green('\nā
Role created successfully!\n'));
|
|
2844
|
+
console.log(chalk.cyan(` Role ID: ${options.roleId}`));
|
|
2845
|
+
console.log(chalk.gray(` Scope: ${options.scope}/${options.scopeId}`));
|
|
2846
|
+
console.log(chalk.gray(` Price: ${price} USDCX\n`));
|
|
2847
|
+
} catch (error) {
|
|
2848
|
+
console.error(chalk.red(error.message));
|
|
2849
|
+
process.exit(1);
|
|
2850
|
+
}
|
|
2851
|
+
});
|
|
2852
|
+
|
|
2853
|
+
// ============ Incentives Commands ============
|
|
2854
|
+
|
|
2855
|
+
const referralCommand = program
|
|
2856
|
+
.command('referral')
|
|
2857
|
+
.description('Referral and incentive operations');
|
|
2858
|
+
|
|
2859
|
+
referralCommand
|
|
2860
|
+
.command('create')
|
|
2861
|
+
.description('Generate a referral code for a community/app')
|
|
2862
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
2863
|
+
.option('-a, --app <id>', 'App ID (optional)')
|
|
2864
|
+
.action(async (options) => {
|
|
2865
|
+
try {
|
|
2866
|
+
const apiClient = new DeSciXApiClient();
|
|
2867
|
+
await requireAuth(apiClient);
|
|
2868
|
+
|
|
2869
|
+
const response = await apiClient.invoke('generate_referral_code', {
|
|
2870
|
+
community_id: options.community,
|
|
2871
|
+
app_id: options.app || null
|
|
2872
|
+
});
|
|
2873
|
+
const result = response.message || response;
|
|
2874
|
+
|
|
2875
|
+
console.log(chalk.green('\nā
Referral code generated!\n'));
|
|
2876
|
+
console.log(chalk.cyan(` Code: ${result.referral_code || result.code}`));
|
|
2877
|
+
console.log(chalk.gray(` Community: ${options.community}`));
|
|
2878
|
+
if (options.app) {
|
|
2879
|
+
console.log(chalk.gray(` App: ${options.app}`));
|
|
2880
|
+
}
|
|
2881
|
+
console.log(chalk.gray(`\n Share this code to earn REF points!\n`));
|
|
2882
|
+
} catch (error) {
|
|
2883
|
+
console.error(chalk.red(error.message));
|
|
2884
|
+
process.exit(1);
|
|
2885
|
+
}
|
|
2886
|
+
});
|
|
2887
|
+
|
|
2888
|
+
program
|
|
2889
|
+
.command('stats')
|
|
2890
|
+
.description('Get user stats (REP/REF/DIP) for a community')
|
|
2891
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
2892
|
+
.action(async (options) => {
|
|
2893
|
+
try {
|
|
2894
|
+
const apiClient = new DeSciXApiClient();
|
|
2895
|
+
await requireAuth(apiClient);
|
|
2896
|
+
|
|
2897
|
+
const response = await apiClient.invoke('get_user_community_stats', {
|
|
2898
|
+
community_id: options.community
|
|
2899
|
+
});
|
|
2900
|
+
const result = response.message || response;
|
|
2901
|
+
|
|
2902
|
+
console.log(chalk.green('\nā
User Stats\n'));
|
|
2903
|
+
console.log(chalk.cyan(` Community: ${options.community}`));
|
|
2904
|
+
console.log(chalk.gray(` REP: ${result.rep || 0}`));
|
|
2905
|
+
console.log(chalk.gray(` REF: ${result.ref || 0}`));
|
|
2906
|
+
console.log(chalk.gray(` DIP: ${result.dip || 0}\n`));
|
|
2907
|
+
} catch (error) {
|
|
2908
|
+
console.error(chalk.red(error.message));
|
|
2909
|
+
process.exit(1);
|
|
2910
|
+
}
|
|
2911
|
+
});
|
|
2912
|
+
|
|
2913
|
+
const repCommand = program
|
|
2914
|
+
.command('rep')
|
|
2915
|
+
.description('Reputation operations');
|
|
2916
|
+
|
|
2917
|
+
repCommand
|
|
2918
|
+
.command('grant')
|
|
2919
|
+
.description('Grant REP points to a user (admin/dev only)')
|
|
2920
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
2921
|
+
.requiredOption('-u, --user <id>', 'User ID')
|
|
2922
|
+
.requiredOption('-a, --amount <amount>', 'REP amount to grant')
|
|
2923
|
+
.option('--reason <text>', 'Reason for granting REP')
|
|
2924
|
+
.action(async (options) => {
|
|
2925
|
+
try {
|
|
2926
|
+
const apiClient = new DeSciXApiClient();
|
|
2927
|
+
await requireAuth(apiClient);
|
|
2928
|
+
|
|
2929
|
+
const amount = parseInt(options.amount);
|
|
2930
|
+
if (isNaN(amount) || amount <= 0) {
|
|
2931
|
+
throw new Error('Amount must be a positive integer');
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
const response = await apiClient.invoke('increment_rep', {
|
|
2935
|
+
community_id: options.community,
|
|
2936
|
+
user_id: options.user,
|
|
2937
|
+
amount: amount,
|
|
2938
|
+
reason: options.reason || 'CLI grant'
|
|
2939
|
+
});
|
|
2940
|
+
const result = response.message || response;
|
|
2941
|
+
|
|
2942
|
+
console.log(chalk.green('\nā
REP granted!\n'));
|
|
2943
|
+
console.log(chalk.cyan(` User: ${options.user}`));
|
|
2944
|
+
console.log(chalk.gray(` Community: ${options.community}`));
|
|
2945
|
+
console.log(chalk.gray(` Amount: +${amount} REP`));
|
|
2946
|
+
console.log(chalk.gray(` New Total: ${result.new_total || result.rep || 'N/A'}\n`));
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
console.error(chalk.red(error.message));
|
|
2949
|
+
process.exit(1);
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
|
|
2953
|
+
// ============ Chat Session Management ============
|
|
2954
|
+
|
|
2955
|
+
/**
|
|
2956
|
+
* Get interaction_id from session file
|
|
2957
|
+
* @param {string} communityId
|
|
2958
|
+
* @param {string} appId
|
|
2959
|
+
* @returns {Promise<string|null>}
|
|
2960
|
+
*/
|
|
2961
|
+
async function getSessionInteractionId(communityId, appId) {
|
|
2962
|
+
const sessionPath = path.join(os.homedir(), '.descix', 'sessions', `${communityId}_${appId}.json`);
|
|
2963
|
+
try {
|
|
2964
|
+
const data = JSON.parse(await fs.readFile(sessionPath, 'utf-8'));
|
|
2965
|
+
return data.interaction_id || null;
|
|
2966
|
+
} catch {
|
|
2967
|
+
return null;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
/**
|
|
2972
|
+
* Save interaction_id to session file
|
|
2973
|
+
* @param {string} communityId
|
|
2974
|
+
* @param {string} appId
|
|
2975
|
+
* @param {string} interactionId
|
|
2976
|
+
*/
|
|
2977
|
+
async function saveSessionInteractionId(communityId, appId, interactionId) {
|
|
2978
|
+
const sessionDir = path.join(os.homedir(), '.descix', 'sessions');
|
|
2979
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
2980
|
+
const sessionPath = path.join(sessionDir, `${communityId}_${appId}.json`);
|
|
2981
|
+
await fs.writeFile(sessionPath, JSON.stringify({
|
|
2982
|
+
interaction_id: interactionId,
|
|
2983
|
+
updated: Date.now()
|
|
2984
|
+
}, null, 2));
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
/**
|
|
2988
|
+
* Clear session file
|
|
2989
|
+
* @param {string} communityId
|
|
2990
|
+
* @param {string} appId
|
|
2991
|
+
*/
|
|
2992
|
+
async function clearSession(communityId, appId) {
|
|
2993
|
+
const sessionPath = path.join(os.homedir(), '.descix', 'sessions', `${communityId}_${appId}.json`);
|
|
2994
|
+
try {
|
|
2995
|
+
await fs.unlink(sessionPath);
|
|
2996
|
+
} catch {
|
|
2997
|
+
// File doesn't exist, that's fine
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// ============ Chat Commands ============
|
|
3002
|
+
|
|
3003
|
+
program
|
|
3004
|
+
.command('chat [question...]')
|
|
3005
|
+
.description('Chat with an app agent. Usage: descix chat "Your question" or descix chat -q "Your question"')
|
|
3006
|
+
.option('-c, --community <id>', 'Community ID (defaults to descix)')
|
|
3007
|
+
.option('-a, --app <id>', 'App ID (defaults to agent)')
|
|
3008
|
+
.option('-q, --question <text>', 'Question to ask (alternative to positional argument)')
|
|
3009
|
+
.option('-k, --kb <id>', 'Knowledge Base ID', 'General')
|
|
3010
|
+
.option('--new', 'Start a new conversation (clear session)')
|
|
3011
|
+
.action(async (questionArgs, options) => {
|
|
3012
|
+
try {
|
|
3013
|
+
const apiClient = new DeSciXApiClient();
|
|
3014
|
+
await requireAuth(apiClient);
|
|
3015
|
+
|
|
3016
|
+
// Get question from positional args or -q option
|
|
3017
|
+
const question = options.question || (questionArgs && questionArgs.join(' '));
|
|
3018
|
+
if (!question) {
|
|
3019
|
+
console.error(chalk.red('Error: Question required. Usage: descix chat "Your question" or descix chat -q "Your question"'));
|
|
3020
|
+
process.exit(1);
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// Auto-detect app from current directory if not specified
|
|
3024
|
+
let communityId = options.community;
|
|
3025
|
+
let appId = options.app;
|
|
3026
|
+
|
|
3027
|
+
if (!communityId || !appId) {
|
|
3028
|
+
try {
|
|
3029
|
+
const { PathContext } = await import('../lib/core/PathContext.js');
|
|
3030
|
+
const ctx = await PathContext.tryLoad();
|
|
3031
|
+
if (ctx) {
|
|
3032
|
+
const detected = ctx.detectContext();
|
|
3033
|
+
if (detected) {
|
|
3034
|
+
communityId = communityId || detected.communityId;
|
|
3035
|
+
appId = appId || detected.appId;
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
} catch {
|
|
3039
|
+
// Fall through to defaults
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
// Fall back to defaults if still not set
|
|
3044
|
+
communityId = communityId || 'descix';
|
|
3045
|
+
appId = appId || 'agent';
|
|
3046
|
+
|
|
3047
|
+
// Get session interaction_id unless --new flag
|
|
3048
|
+
let previousInteractionId = null;
|
|
3049
|
+
if (!options.new) {
|
|
3050
|
+
previousInteractionId = await getSessionInteractionId(communityId, appId);
|
|
3051
|
+
} else {
|
|
3052
|
+
// Clear session if --new flag
|
|
3053
|
+
await clearSession(communityId, appId);
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
console.log(chalk.gray(`Asking ${communityId}/${appId}...`));
|
|
3057
|
+
|
|
3058
|
+
const response = await apiClient.invoke('ask_question_to_app', {
|
|
3059
|
+
app_id: appId,
|
|
3060
|
+
knowledgebase_name: options.kb,
|
|
3061
|
+
user_input: question,
|
|
3062
|
+
previous_interaction_id: previousInteractionId,
|
|
3063
|
+
streaming: false
|
|
3064
|
+
});
|
|
3065
|
+
const result = response.message || response;
|
|
3066
|
+
|
|
3067
|
+
// Save new interaction_id for next message
|
|
3068
|
+
if (result.interaction_id) {
|
|
3069
|
+
await saveSessionInteractionId(communityId, appId, result.interaction_id);
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
console.log(chalk.green('\nā
Response:\n'));
|
|
3073
|
+
console.log(chalk.white(result.response || result.text || JSON.stringify(result, null, 2)));
|
|
3074
|
+
|
|
3075
|
+
const sources = result.sources || response.sources || response.message?.sources || [];
|
|
3076
|
+
if (sources && sources.length > 0) {
|
|
3077
|
+
console.log(chalk.cyan('\nš Sources:'));
|
|
3078
|
+
sources.forEach((src, i) => {
|
|
3079
|
+
const fileName = src.fileName || src.file_path || src.source || src;
|
|
3080
|
+
const fileId = src.fileId || src.id || '';
|
|
3081
|
+
const score = src.score || src.similarity || 0;
|
|
3082
|
+
const scoreStr = score ? ` (score: ${score.toFixed(3)})` : '';
|
|
3083
|
+
const idStr = fileId ? ` [ID: ${fileId}]` : '';
|
|
3084
|
+
console.log(chalk.gray(` ${i + 1}. ${fileName}${idStr}${scoreStr}`));
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
console.log('');
|
|
3088
|
+
} catch (error) {
|
|
3089
|
+
console.error(chalk.red(error.message));
|
|
3090
|
+
process.exit(1);
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
|
|
3094
|
+
program
|
|
3095
|
+
.command('new-chat')
|
|
3096
|
+
.description('Clear chat session for an app (start fresh conversation)')
|
|
3097
|
+
.option('-c, --community <id>', 'Community ID (defaults to descix)')
|
|
3098
|
+
.option('-a, --app <id>', 'App ID (defaults to agent)')
|
|
3099
|
+
.action(async (options) => {
|
|
3100
|
+
try {
|
|
3101
|
+
const communityId = options.community || 'descix';
|
|
3102
|
+
const appId = options.app || 'agent';
|
|
3103
|
+
await clearSession(communityId, appId);
|
|
3104
|
+
console.log(chalk.green(`Chat session cleared for ${communityId}/${appId}. Next chat will start fresh.`));
|
|
3105
|
+
} catch (error) {
|
|
3106
|
+
console.error(chalk.red(error.message));
|
|
3107
|
+
process.exit(1);
|
|
3108
|
+
}
|
|
3109
|
+
});
|
|
3110
|
+
|
|
3111
|
+
// ============ Purchases Command ============
|
|
3112
|
+
|
|
3113
|
+
program
|
|
3114
|
+
.command('purchases')
|
|
3115
|
+
.description('List your purchased communities and apps')
|
|
3116
|
+
.action(async () => {
|
|
3117
|
+
try {
|
|
3118
|
+
const apiClient = new DeSciXApiClient();
|
|
3119
|
+
await requireAuth(apiClient);
|
|
3120
|
+
|
|
3121
|
+
const response = await apiClient.invoke('fetch_my_purchases', {});
|
|
3122
|
+
const result = response.message || response;
|
|
3123
|
+
|
|
3124
|
+
const communities = result.communities || [];
|
|
3125
|
+
const apps = result.apps || [];
|
|
3126
|
+
|
|
3127
|
+
console.log(chalk.green('\nā
Your Purchases:\n'));
|
|
3128
|
+
console.log(chalk.cyan(`Communities (${communities.length}):`));
|
|
3129
|
+
communities.forEach((c, idx) => {
|
|
3130
|
+
console.log(chalk.yellow(` ${idx + 1}. ${c.community_name} (${c.community_id})`));
|
|
3131
|
+
});
|
|
3132
|
+
console.log();
|
|
3133
|
+
console.log(chalk.cyan(`Apps (${apps.length}):`));
|
|
3134
|
+
apps.forEach((a, idx) => {
|
|
3135
|
+
console.log(chalk.yellow(` ${idx + 1}. ${a.app_name || a.app_id} (${a.community_id}/${a.app_id})`));
|
|
3136
|
+
});
|
|
3137
|
+
console.log();
|
|
3138
|
+
} catch (error) {
|
|
3139
|
+
console.error(chalk.red(error.message));
|
|
3140
|
+
process.exit(1);
|
|
3141
|
+
}
|
|
3142
|
+
});
|
|
3143
|
+
|
|
3144
|
+
// ============ Configuration Commands ============
|
|
3145
|
+
|
|
3146
|
+
const configCommand = program
|
|
3147
|
+
.command('config')
|
|
3148
|
+
.description('Manage CLI configuration');
|
|
3149
|
+
|
|
3150
|
+
configCommand
|
|
3151
|
+
.command('show')
|
|
3152
|
+
.description('Show current configuration')
|
|
3153
|
+
.action(async () => {
|
|
3154
|
+
try {
|
|
3155
|
+
await configCommands.show();
|
|
3156
|
+
} catch (error) {
|
|
3157
|
+
process.exit(1);
|
|
3158
|
+
}
|
|
3159
|
+
});
|
|
3160
|
+
|
|
3161
|
+
configCommand
|
|
3162
|
+
.command('set-url')
|
|
3163
|
+
.description('Set API URL')
|
|
3164
|
+
.argument('<url>', 'API URL (e.g., https://localhost:4000)')
|
|
3165
|
+
.option('-g, --global', 'Save to global config (~/.descixrc)')
|
|
3166
|
+
.action(async (url, options) => {
|
|
3167
|
+
try {
|
|
3168
|
+
await configCommands.setUrl(url, options);
|
|
3169
|
+
} catch (error) {
|
|
3170
|
+
process.exit(1);
|
|
3171
|
+
}
|
|
3172
|
+
});
|
|
3173
|
+
|
|
3174
|
+
configCommand
|
|
3175
|
+
.command('init')
|
|
3176
|
+
.description('Initialize configuration')
|
|
3177
|
+
.option('--dev', 'Initialize for development (https://localhost:4000)')
|
|
3178
|
+
.option('-g, --global', 'Save to global config (~/.descixrc)')
|
|
3179
|
+
.action(async (options) => {
|
|
3180
|
+
try {
|
|
3181
|
+
const env = options.dev ? 'dev' : 'prod';
|
|
3182
|
+
await configCommands.init(env, options);
|
|
3183
|
+
} catch (error) {
|
|
3184
|
+
process.exit(1);
|
|
3185
|
+
}
|
|
3186
|
+
});
|
|
3187
|
+
|
|
3188
|
+
configCommand
|
|
3189
|
+
.command('set-sync-mode')
|
|
3190
|
+
.description('Set KB sync mode for an app (git or drive)')
|
|
3191
|
+
.argument('<mode>', 'Sync mode: "git" (local-first) or "drive" (PWA-first)')
|
|
3192
|
+
.option('-c, --community <id>', 'Community ID')
|
|
3193
|
+
.option('-a, --app <id>', 'App ID')
|
|
3194
|
+
.action(async (mode, options) => {
|
|
3195
|
+
try {
|
|
3196
|
+
await configCommands.setSyncMode(mode, options);
|
|
3197
|
+
} catch (error) {
|
|
3198
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
3199
|
+
process.exit(1);
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
3202
|
+
|
|
3203
|
+
// ============ Documentation Commands ============
|
|
3204
|
+
|
|
3205
|
+
const docsCommand = program
|
|
3206
|
+
.command('docs')
|
|
3207
|
+
.description('Documentation operations');
|
|
3208
|
+
|
|
3209
|
+
docsCommand
|
|
3210
|
+
.command('sync')
|
|
3211
|
+
.description('Sync README/ folder to platform-docs KB for RAG search')
|
|
3212
|
+
.option('--path <path>', 'Path to README folder (default: ./README)', './README')
|
|
3213
|
+
.action(async (options) => {
|
|
3214
|
+
try {
|
|
3215
|
+
const apiClient = new DeSciXApiClient();
|
|
3216
|
+
await requireAuth(apiClient);
|
|
3217
|
+
|
|
3218
|
+
const fs = await import('fs/promises');
|
|
3219
|
+
const path = await import('path');
|
|
3220
|
+
|
|
3221
|
+
const readmePath = path.resolve(options.path || './README');
|
|
3222
|
+
|
|
3223
|
+
console.log(chalk.cyan(`\nš Syncing documentation to platform-docs KB\n`));
|
|
3224
|
+
console.log(chalk.gray(`Reading from: ${readmePath}\n`));
|
|
3225
|
+
|
|
3226
|
+
// Check if README folder exists
|
|
3227
|
+
try {
|
|
3228
|
+
const stats = await fs.stat(readmePath);
|
|
3229
|
+
if (!stats.isDirectory()) {
|
|
3230
|
+
throw new Error(`Path is not a directory: ${readmePath}`);
|
|
3231
|
+
}
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
if (error.code === 'ENOENT') {
|
|
3234
|
+
throw new Error(`README folder not found: ${readmePath}`);
|
|
3235
|
+
}
|
|
3236
|
+
throw error;
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
// Recursively read all .md files
|
|
3240
|
+
const files = [];
|
|
3241
|
+
async function readDir(dirPath, basePath = '') {
|
|
3242
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
3243
|
+
for (const entry of entries) {
|
|
3244
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
3245
|
+
const relPath = path.join(basePath, entry.name);
|
|
3246
|
+
|
|
3247
|
+
if (entry.isDirectory()) {
|
|
3248
|
+
// Skip node_modules and .git
|
|
3249
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.')) {
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
await readDir(fullPath, relPath);
|
|
3253
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
3254
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
3255
|
+
files.push({
|
|
3256
|
+
name: entry.name,
|
|
3257
|
+
path: relPath,
|
|
3258
|
+
content: Buffer.from(content).toString('base64') // Base64 encode
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
await readDir(readmePath);
|
|
3265
|
+
|
|
3266
|
+
if (files.length === 0) {
|
|
3267
|
+
throw new Error('No .md files found in README folder');
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
console.log(chalk.gray(`Found ${files.length} markdown files. Uploading...\n`));
|
|
3271
|
+
|
|
3272
|
+
// Upload files to platform-docs KB
|
|
3273
|
+
const response = await apiClient.invoke('upload_files_to_kb', {
|
|
3274
|
+
community_id: 'descix',
|
|
3275
|
+
app_id: 'platform-docs',
|
|
3276
|
+
kb_id: 'General',
|
|
3277
|
+
files: files
|
|
3278
|
+
});
|
|
3279
|
+
const result = response.message || response;
|
|
3280
|
+
|
|
3281
|
+
console.log(chalk.green(`\nā
Uploaded ${result.uploaded || 0} files\n`));
|
|
3282
|
+
|
|
3283
|
+
// Sync to RAG
|
|
3284
|
+
console.log(chalk.gray('Syncing to RAG vectors...\n'));
|
|
3285
|
+
const syncResponse = await apiClient.invoke('sync_knowledge_base', {
|
|
3286
|
+
community_id: 'descix',
|
|
3287
|
+
app_id: 'platform-docs',
|
|
3288
|
+
kb_id: 'General'
|
|
3289
|
+
});
|
|
3290
|
+
const syncResult = syncResponse.message || syncResponse;
|
|
3291
|
+
|
|
3292
|
+
console.log(chalk.green('ā
Documentation synced to RAG!\n'));
|
|
3293
|
+
console.log(chalk.cyan(` Total Vectors: ${syncResult.vectorCount || result.totalVectors || 0}\n`));
|
|
3294
|
+
|
|
3295
|
+
if (result.errors && result.errors.length > 0) {
|
|
3296
|
+
console.log(chalk.yellow(' Errors:'));
|
|
3297
|
+
result.errors.forEach(err => {
|
|
3298
|
+
console.log(chalk.red(` - ${err.file}: ${err.error}`));
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
console.log(chalk.gray('\nQuery documentation with: descix rag query -c descix -a platform-docs -k General -q "your question"\n'));
|
|
3303
|
+
|
|
3304
|
+
} catch (error) {
|
|
3305
|
+
console.error(chalk.red(error.message));
|
|
3306
|
+
process.exit(1);
|
|
3307
|
+
}
|
|
3308
|
+
});
|
|
3309
|
+
|
|
3310
|
+
// ============ Store Search Commands ============
|
|
3311
|
+
|
|
3312
|
+
const storeCommand = program
|
|
3313
|
+
.command('store')
|
|
3314
|
+
.description('Store search operations');
|
|
3315
|
+
|
|
3316
|
+
storeCommand
|
|
3317
|
+
.command('search')
|
|
3318
|
+
.description('Search store catalog (communities, apps, products, documents)')
|
|
3319
|
+
.option('-q, --query <text>', 'Search query')
|
|
3320
|
+
.option('-t, --type <types>', 'Entity types (comma-separated: COMMUNITY,APP,KB,IPDOC,ROLE)')
|
|
3321
|
+
.option('-c, --community <id>', 'Filter by community ID')
|
|
3322
|
+
.option('-a, --app <id>', 'Filter by app ID')
|
|
3323
|
+
.option('-k, --kb <id>', 'Filter by knowledge base ID')
|
|
3324
|
+
.option('--price-min <number>', 'Minimum price filter')
|
|
3325
|
+
.option('--price-max <number>', 'Maximum price filter')
|
|
3326
|
+
.option('--tags <tags>', 'Filter by tags (comma-separated)')
|
|
3327
|
+
.option('-l, --limit <number>', 'Number of results', '20')
|
|
3328
|
+
.option('--offset <number>', 'Pagination offset', '0')
|
|
3329
|
+
.action(async (options) => {
|
|
3330
|
+
try {
|
|
3331
|
+
const apiClient = new DeSciXApiClient();
|
|
3332
|
+
await requireAuth(apiClient);
|
|
3333
|
+
|
|
3334
|
+
const params = {
|
|
3335
|
+
query: options.query || '',
|
|
3336
|
+
entity_types: options.type ? options.type.split(',').map(t => t.trim()) : [],
|
|
3337
|
+
community_id: options.community || null,
|
|
3338
|
+
app_id: options.app || null,
|
|
3339
|
+
kb_id: options.kb || null,
|
|
3340
|
+
price_min: options.priceMin ? parseFloat(options.priceMin) : null,
|
|
3341
|
+
price_max: options.priceMax ? parseFloat(options.priceMax) : null,
|
|
3342
|
+
tags: options.tags ? options.tags.split(',').map(t => t.trim()) : [],
|
|
3343
|
+
limit: parseInt(options.limit),
|
|
3344
|
+
offset: parseInt(options.offset)
|
|
3345
|
+
};
|
|
3346
|
+
|
|
3347
|
+
const response = await apiClient.invoke('search_store', params);
|
|
3348
|
+
const result = response.message || response;
|
|
3349
|
+
|
|
3350
|
+
console.log(chalk.green(`\nā
Found ${result.total || result.results?.length || 0} results\n`));
|
|
3351
|
+
|
|
3352
|
+
if (result.query) {
|
|
3353
|
+
console.log(chalk.cyan(`Query: "${result.query}"\n`));
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
(result.results || []).forEach((r, idx) => {
|
|
3357
|
+
const num = idx + 1 + parseInt(options.offset);
|
|
3358
|
+
console.log(chalk.yellow(`${num}. [${r.entity_type}] ${r.name}`));
|
|
3359
|
+
if (r.description) {
|
|
3360
|
+
console.log(chalk.gray(` ${r.description.substring(0, 150)}...`));
|
|
3361
|
+
}
|
|
3362
|
+
if (r.price !== undefined && r.price > 0) {
|
|
3363
|
+
console.log(chalk.cyan(` Price: ${r.price} USDCX`));
|
|
3364
|
+
}
|
|
3365
|
+
if (r.community_id) {
|
|
3366
|
+
console.log(chalk.gray(` Community: ${r.community_id}`));
|
|
3367
|
+
}
|
|
3368
|
+
if (r.app_id) {
|
|
3369
|
+
console.log(chalk.gray(` App: ${r.community_id}/${r.app_id}`));
|
|
3370
|
+
}
|
|
3371
|
+
if (r.score !== undefined) {
|
|
3372
|
+
console.log(chalk.gray(` Score: ${(r.score * 100).toFixed(1)}%`));
|
|
3373
|
+
}
|
|
3374
|
+
console.log();
|
|
3375
|
+
});
|
|
3376
|
+
} catch (error) {
|
|
3377
|
+
console.error(chalk.red(error.message));
|
|
3378
|
+
process.exit(1);
|
|
3379
|
+
}
|
|
3380
|
+
});
|
|
3381
|
+
|
|
3382
|
+
storeCommand
|
|
3383
|
+
.command('generate-summary')
|
|
3384
|
+
.description('Generate AI summary for an entity')
|
|
3385
|
+
.requiredOption('-t, --type <type>', 'Entity type (COMMUNITY, APP, KB, IPDOC)')
|
|
3386
|
+
.requiredOption('-i, --id <id>', 'Entity ID')
|
|
3387
|
+
.option('-c, --community <id>', 'Community ID (required for APP, KB, IPDOC)')
|
|
3388
|
+
.option('-a, --app <id>', 'App ID (required for KB, IPDOC)')
|
|
3389
|
+
.option('-k, --kb <id>', 'KB ID (required for IPDOC)')
|
|
3390
|
+
.action(async (options) => {
|
|
3391
|
+
try {
|
|
3392
|
+
const apiClient = new DeSciXApiClient();
|
|
3393
|
+
await requireAuth(apiClient);
|
|
3394
|
+
|
|
3395
|
+
const params = {
|
|
3396
|
+
entity_type: options.type,
|
|
3397
|
+
entity_id: options.id,
|
|
3398
|
+
community_id: options.community || null,
|
|
3399
|
+
app_id: options.app || null,
|
|
3400
|
+
kb_id: options.kb || null
|
|
3401
|
+
};
|
|
3402
|
+
|
|
3403
|
+
console.log(chalk.gray(`Generating summary for ${options.type} ${options.id}...`));
|
|
3404
|
+
|
|
3405
|
+
const response = await apiClient.invoke('generate_entity_summary', params);
|
|
3406
|
+
const result = response.message || response;
|
|
3407
|
+
|
|
3408
|
+
console.log(chalk.green('\nā
Summary generated!\n'));
|
|
3409
|
+
console.log(chalk.cyan(`Entity: ${result.entity_name} (${result.entity_type})\n`));
|
|
3410
|
+
console.log(chalk.white(result.summary));
|
|
3411
|
+
console.log();
|
|
3412
|
+
} catch (error) {
|
|
3413
|
+
console.error(chalk.red(error.message));
|
|
3414
|
+
process.exit(1);
|
|
3415
|
+
}
|
|
3416
|
+
});
|
|
3417
|
+
|
|
3418
|
+
/**
|
|
3419
|
+
* Output AI-friendly instructions for creating SERVICE_README
|
|
3420
|
+
* Designed for both human developers and AI agents (like Cursor) to use directly
|
|
3421
|
+
*/
|
|
3422
|
+
function outputServiceReadmeInstructions(serviceName, appId, communityId, manifest) {
|
|
3423
|
+
const readmeFileName = `SERVICE_README_${serviceName}.md`;
|
|
3424
|
+
const commands = Object.entries(manifest.commands || {});
|
|
3425
|
+
|
|
3426
|
+
// Generate command documentation from manifest
|
|
3427
|
+
let commandDocs = '';
|
|
3428
|
+
for (const [cmdName, cmdConfig] of commands) {
|
|
3429
|
+
const params = cmdConfig.inputSchema?.properties || {};
|
|
3430
|
+
const required = cmdConfig.inputSchema?.required || [];
|
|
3431
|
+
|
|
3432
|
+
commandDocs += `### ${cmdName}
|
|
3433
|
+
**Description:** ${cmdConfig.description || 'No description provided'}
|
|
3434
|
+
**Use when:** [Describe scenarios when this command should be used]
|
|
3435
|
+
**Prerequisites:** ${cmdConfig.guestAllowed ? 'None (guest allowed)' : 'User must be authenticated'}
|
|
3436
|
+
**Parameters:**
|
|
3437
|
+
`;
|
|
3438
|
+
for (const [paramName, paramConfig] of Object.entries(params)) {
|
|
3439
|
+
const isRequired = required.includes(paramName);
|
|
3440
|
+
commandDocs += `- \`${paramName}\` (${isRequired ? 'required' : 'optional'}, ${paramConfig.type || 'any'}): ${paramConfig.description || 'No description'}\n`;
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
commandDocs += `
|
|
3444
|
+
**Example:**
|
|
3445
|
+
\`\`\`json
|
|
3446
|
+
{
|
|
3447
|
+
"command": "${cmdName}",
|
|
3448
|
+
"params": { ${required.map(r => `"${r}": "value"`).join(', ')} }
|
|
3449
|
+
}
|
|
3450
|
+
\`\`\`
|
|
3451
|
+
|
|
3452
|
+
`;
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
console.log(chalk.red(`
|
|
3456
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
3457
|
+
ā SERVICE_README Required ā
|
|
3458
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
3459
|
+
`));
|
|
3460
|
+
|
|
3461
|
+
console.log(chalk.white(`Your microservice needs a ${readmeFileName} file for tool discovery.
|
|
3462
|
+
|
|
3463
|
+
š Required file: ${readmeFileName}
|
|
3464
|
+
š Location: Google Drive folder for app '${appId}' in community '${communityId}'
|
|
3465
|
+
|
|
3466
|
+
This README is vectorized and used by the \`tell_me_how\` command to help
|
|
3467
|
+
users and AI agents discover your service's capabilities.
|
|
3468
|
+
|
|
3469
|
+
To create this file, use the following template:
|
|
3470
|
+
`));
|
|
3471
|
+
|
|
3472
|
+
console.log(chalk.yellow(`---BEGIN TEMPLATE---`));
|
|
3473
|
+
console.log(chalk.white(`# ${serviceName} Service
|
|
3474
|
+
|
|
3475
|
+
## Overview
|
|
3476
|
+
[One paragraph description of what this service does and when to use it.
|
|
3477
|
+
Include the key use cases and benefits.]
|
|
3478
|
+
|
|
3479
|
+
## Available Commands
|
|
3480
|
+
|
|
3481
|
+
${commandDocs || '### [command_name]\n**Description:** [what this command does]\n**Use when:** [scenarios]\n**Prerequisites:** [requirements]\n**Parameters:**\n- \\`param1\\` (required, string): [description]\n\n**Example:**\n\\`\\`\\`json\n{\n "command": "[command_name]",\n "params": { "param1": "value" }\n}\n\\`\\`\\`\n'}
|
|
3482
|
+
## Common Workflows
|
|
3483
|
+
|
|
3484
|
+
[Describe how commands work together for common tasks]
|
|
3485
|
+
|
|
3486
|
+
## Troubleshooting
|
|
3487
|
+
|
|
3488
|
+
[Common errors and how to resolve them]
|
|
3489
|
+
`));
|
|
3490
|
+
console.log(chalk.yellow(`---END TEMPLATE---`));
|
|
3491
|
+
|
|
3492
|
+
console.log(chalk.white(`
|
|
3493
|
+
After creating the file:
|
|
3494
|
+
|
|
3495
|
+
1. Upload ${readmeFileName} to the app's Drive folder
|
|
3496
|
+
Location: Your Drive > [DeSciX folder] > ${communityId} > ${appId}/
|
|
3497
|
+
|
|
3498
|
+
2. Re-run this command:
|
|
3499
|
+
descix microservice register -m ${manifest ? './manifest.json' : '<path-to-manifest>'}
|
|
3500
|
+
|
|
3501
|
+
š” Tip: Your AI coding assistant (like Cursor) can create this file for you
|
|
3502
|
+
using the template above. Just ask it to "create the SERVICE_README file".
|
|
3503
|
+
`));
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
// ============ Clone Command ============
|
|
3507
|
+
|
|
3508
|
+
program
|
|
3509
|
+
.command('clone')
|
|
3510
|
+
.description('Clone a DeSciX app repository')
|
|
3511
|
+
.requiredOption('-a, --app <id>', 'App ID')
|
|
3512
|
+
.option('--path <dir>', 'Target directory')
|
|
3513
|
+
.action(async (options) => {
|
|
3514
|
+
try {
|
|
3515
|
+
const apiClient = new DeSciXApiClient();
|
|
3516
|
+
await requireAuth(apiClient);
|
|
3517
|
+
await runClone(apiClient, { app_id: options.app, targetPath: options.path });
|
|
3518
|
+
} catch (error) {
|
|
3519
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
3520
|
+
process.exit(1);
|
|
3521
|
+
}
|
|
3522
|
+
});
|
|
3523
|
+
|
|
3524
|
+
// ============ MCP Commands ============
|
|
3525
|
+
|
|
3526
|
+
import * as mcpCommands from '../lib/commands/mcp.js';
|
|
3527
|
+
import { runClone } from '../lib/commands/clone.js';
|
|
3528
|
+
|
|
3529
|
+
const mcpCommand = program
|
|
3530
|
+
.command('mcp')
|
|
3531
|
+
.description('MCP server operations');
|
|
3532
|
+
|
|
3533
|
+
mcpCommand
|
|
3534
|
+
.command('init')
|
|
3535
|
+
.description('Initialize MCP server for this workspace')
|
|
3536
|
+
.action(async () => {
|
|
3537
|
+
try {
|
|
3538
|
+
await mcpCommands.init();
|
|
3539
|
+
} catch (error) {
|
|
3540
|
+
process.exit(1);
|
|
3541
|
+
}
|
|
3542
|
+
});
|
|
3543
|
+
|
|
3544
|
+
mcpCommand
|
|
3545
|
+
.command('test')
|
|
3546
|
+
.description('Test MCP server with a sample query')
|
|
3547
|
+
.option('-q, --query <text>', 'Test query', 'What is this knowledge base about?')
|
|
3548
|
+
.action(async (options) => {
|
|
3549
|
+
try {
|
|
3550
|
+
await mcpCommands.test(options);
|
|
3551
|
+
} catch (error) {
|
|
3552
|
+
process.exit(1);
|
|
3553
|
+
}
|
|
3554
|
+
});
|
|
3555
|
+
|
|
3556
|
+
mcpCommand
|
|
3557
|
+
.command('config')
|
|
3558
|
+
.description('Show current workspace configuration')
|
|
3559
|
+
.action(async () => {
|
|
3560
|
+
try {
|
|
3561
|
+
await mcpCommands.config();
|
|
3562
|
+
} catch (error) {
|
|
3563
|
+
process.exit(1);
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
|
|
3567
|
+
mcpCommand
|
|
3568
|
+
.command('quickstart')
|
|
3569
|
+
.description('One-command setup: authenticate, configure, and test')
|
|
3570
|
+
.action(async () => {
|
|
3571
|
+
try {
|
|
3572
|
+
await mcpCommands.quickstart();
|
|
3573
|
+
} catch (error) {
|
|
3574
|
+
process.exit(1);
|
|
3575
|
+
}
|
|
3576
|
+
});
|
|
3577
|
+
|
|
3578
|
+
// ============ Intelligent Tool Discovery ============
|
|
3579
|
+
|
|
3580
|
+
program
|
|
3581
|
+
.command('tell-me-how')
|
|
3582
|
+
.description('Discover platform tools using natural language (Intelligent MCP Mesh)')
|
|
3583
|
+
.argument('<question>', 'What do you want to accomplish? Use natural language.')
|
|
3584
|
+
.option('-s, --scope <scope>', 'Search scope: project, entitlements (default), discovery', 'entitlements')
|
|
3585
|
+
.option('-j, --json', 'Output raw JSON response')
|
|
3586
|
+
.action(async (question, options) => {
|
|
3587
|
+
try {
|
|
3588
|
+
const apiClient = new DeSciXApiClient();
|
|
3589
|
+
await requireAuth(apiClient);
|
|
3590
|
+
|
|
3591
|
+
const { scope, json: jsonOutput } = options;
|
|
3592
|
+
|
|
3593
|
+
// Validate scope
|
|
3594
|
+
const validScopes = ['project', 'entitlements', 'discovery'];
|
|
3595
|
+
if (!validScopes.includes(scope)) {
|
|
3596
|
+
console.error(chalk.red(`Invalid scope '${scope}'. Must be one of: ${validScopes.join(', ')}`));
|
|
3597
|
+
process.exit(1);
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
// Get project context for 'project' scope
|
|
3601
|
+
let project_context = null;
|
|
3602
|
+
if (scope === 'project') {
|
|
3603
|
+
try {
|
|
3604
|
+
const { WorkspaceConfig } = await import('../lib/workspace-config.js');
|
|
3605
|
+
const wsConfig = await WorkspaceConfig.load(process.cwd());
|
|
3606
|
+
if (wsConfig) {
|
|
3607
|
+
// Extract community_ids from primaryCommunity and directoryMappings
|
|
3608
|
+
const communityIds = new Set();
|
|
3609
|
+
const appIds = new Set();
|
|
3610
|
+
|
|
3611
|
+
if (wsConfig.primaryCommunity) {
|
|
3612
|
+
communityIds.add(wsConfig.primaryCommunity);
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
// Extract from directoryMappings
|
|
3616
|
+
for (const mapping of Object.values(wsConfig.directoryMappings || {})) {
|
|
3617
|
+
if (mapping.communityId) communityIds.add(mapping.communityId);
|
|
3618
|
+
if (mapping.appId) appIds.add(mapping.appId);
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
// Extract from defaultContext
|
|
3622
|
+
if (wsConfig.defaultContext?.communityId) {
|
|
3623
|
+
communityIds.add(wsConfig.defaultContext.communityId);
|
|
3624
|
+
}
|
|
3625
|
+
if (wsConfig.defaultContext?.appId) {
|
|
3626
|
+
appIds.add(wsConfig.defaultContext.appId);
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
project_context = {
|
|
3630
|
+
community_ids: Array.from(communityIds),
|
|
3631
|
+
app_ids: Array.from(appIds)
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
} catch (e) {
|
|
3635
|
+
console.error(chalk.yellow(`ā ļø Could not read .descix/workspace.json for project scope`));
|
|
3636
|
+
console.error(chalk.gray(` Run 'descix init' first, or use --scope entitlements`));
|
|
3637
|
+
process.exit(1);
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
console.log(chalk.cyan(`\nš Searching for tools... (scope: ${scope})\n`));
|
|
3642
|
+
console.log(chalk.gray(` Question: "${question}"\n`));
|
|
3643
|
+
|
|
3644
|
+
const response = await apiClient.invoke('tell_me_how', {
|
|
3645
|
+
question,
|
|
3646
|
+
scope,
|
|
3647
|
+
project_context
|
|
3648
|
+
});
|
|
3649
|
+
|
|
3650
|
+
const result = response.message || response;
|
|
3651
|
+
|
|
3652
|
+
if (jsonOutput) {
|
|
3653
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
// Format output for human consumption
|
|
3658
|
+
if (result.status === 'ERROR') {
|
|
3659
|
+
console.error(chalk.red(`\nā ${result.message}\n`));
|
|
3660
|
+
process.exit(1);
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
console.log(chalk.green(`\nš” ${result.explanation || 'Results found'}\n`));
|
|
3664
|
+
|
|
3665
|
+
const tools = result.recommended_tools || [];
|
|
3666
|
+
if (tools.length > 0) {
|
|
3667
|
+
console.log(chalk.cyan('Recommended Tools:'));
|
|
3668
|
+
console.log(chalk.cyan('ā'.repeat(50)));
|
|
3669
|
+
|
|
3670
|
+
tools.forEach((tool, idx) => {
|
|
3671
|
+
console.log(chalk.yellow(`\n${idx + 1}. ${tool.command || tool.service}`));
|
|
3672
|
+
console.log(chalk.gray(` ${tool.description || 'No description'}`));
|
|
3673
|
+
if (tool.community_id && tool.app_id) {
|
|
3674
|
+
console.log(chalk.gray(` Source: ${tool.community_id}/${tool.app_id}`));
|
|
3675
|
+
}
|
|
3676
|
+
if (tool.relevance_score) {
|
|
3677
|
+
console.log(chalk.gray(` Relevance: ${(tool.relevance_score * 100).toFixed(1)}%`));
|
|
3678
|
+
}
|
|
3679
|
+
});
|
|
3680
|
+
|
|
3681
|
+
console.log(chalk.cyan('\nā'.repeat(50)));
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
// Show purchase requirements for discovery scope
|
|
3685
|
+
if (scope === 'discovery' && result.requires_purchase?.length > 0) {
|
|
3686
|
+
console.log(chalk.yellow('\nā ļø Some tools require purchase:\n'));
|
|
3687
|
+
result.requires_purchase.forEach(item => {
|
|
3688
|
+
console.log(chalk.gray(` ⢠${item.service} - ${item.reason}`));
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
// Show next steps
|
|
3693
|
+
if (tools.length > 0) {
|
|
3694
|
+
console.log(chalk.white('\nš Next Steps:\n'));
|
|
3695
|
+
console.log(chalk.gray(' 1. Use the recommended tool via MCP or CLI'));
|
|
3696
|
+
console.log(chalk.gray(' 2. Example MCP call:'));
|
|
3697
|
+
console.log(chalk.gray(` execute_remote_command({ command: "${tools[0].command || tools[0].service}", params: {...} })`));
|
|
3698
|
+
console.log(chalk.gray(' 3. Or use the CLI:'));
|
|
3699
|
+
console.log(chalk.gray(` descix <command> [options]`));
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
console.log();
|
|
3703
|
+
|
|
3704
|
+
} catch (error) {
|
|
3705
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
3706
|
+
process.exit(1);
|
|
3707
|
+
}
|
|
3708
|
+
});
|
|
3709
|
+
|
|
3710
|
+
// ============ Folder Commands ============
|
|
3711
|
+
|
|
3712
|
+
const folderCommand = program
|
|
3713
|
+
.command('folder')
|
|
3714
|
+
.description('Drive folder management');
|
|
3715
|
+
|
|
3716
|
+
folderCommand
|
|
3717
|
+
.command('set')
|
|
3718
|
+
.description('Set folder ID for an entity')
|
|
3719
|
+
.option('--user <folderId>', 'Set user base folder')
|
|
3720
|
+
.option('--community <communityId>', 'Set community folder (requires folderId positional)')
|
|
3721
|
+
.option('--app <communityApp>', 'Set app folder: "community app" (requires folderId positional)')
|
|
3722
|
+
.option('--kb <communityAppKb>', 'Set KB folder: "community app kb" (requires folderId positional)')
|
|
3723
|
+
.argument('[folderId]', 'Folder ID (for community/app/kb)')
|
|
3724
|
+
.action(async (folderId, options) => {
|
|
3725
|
+
try {
|
|
3726
|
+
await folderCommands.setFolder({ ...options, folderId });
|
|
3727
|
+
} catch (error) {
|
|
3728
|
+
process.exit(1);
|
|
3729
|
+
}
|
|
3730
|
+
});
|
|
3731
|
+
|
|
3732
|
+
folderCommand
|
|
3733
|
+
.command('get')
|
|
3734
|
+
.description('Get folder info for an entity')
|
|
3735
|
+
.option('--user', 'Get user base folder')
|
|
3736
|
+
.option('--community <communityId>', 'Get community folder')
|
|
3737
|
+
.option('--app <communityApp>', 'Get app folder: "community app"')
|
|
3738
|
+
.option('--kb <communityAppKb>', 'Get KB folder: "community app kb"')
|
|
3739
|
+
.action(async (options) => {
|
|
3740
|
+
try {
|
|
3741
|
+
await folderCommands.getFolder(options);
|
|
3742
|
+
} catch (error) {
|
|
3743
|
+
process.exit(1);
|
|
3744
|
+
}
|
|
3745
|
+
});
|
|
3746
|
+
|
|
3747
|
+
folderCommand
|
|
3748
|
+
.command('allocate')
|
|
3749
|
+
.description('Allocate a new folder for an entity')
|
|
3750
|
+
.option('--app <communityApp>', 'Allocate app folder: "community app"')
|
|
3751
|
+
.option('--kb <communityAppKb>', 'Allocate KB folder: "community app kb"')
|
|
3752
|
+
.option('-n, --name <name>', 'Custom folder name')
|
|
3753
|
+
.action(async (options) => {
|
|
3754
|
+
try {
|
|
3755
|
+
await folderCommands.allocateFolder(options);
|
|
3756
|
+
} catch (error) {
|
|
3757
|
+
process.exit(1);
|
|
3758
|
+
}
|
|
3759
|
+
});
|
|
3760
|
+
|
|
3761
|
+
folderCommand
|
|
3762
|
+
.command('validate <folderId>')
|
|
3763
|
+
.description('Validate folder access')
|
|
3764
|
+
.action(async (folderId, options) => {
|
|
3765
|
+
try {
|
|
3766
|
+
await folderCommands.validateFolder(folderId, options);
|
|
3767
|
+
} catch (error) {
|
|
3768
|
+
process.exit(1);
|
|
3769
|
+
}
|
|
3770
|
+
});
|
|
3771
|
+
|
|
3772
|
+
// ============ Update Commands (Context-Driven) ============
|
|
3773
|
+
|
|
3774
|
+
const updateCommand = program
|
|
3775
|
+
.command('update')
|
|
3776
|
+
.description('Context-driven resource sync (auto-detects from workspace)')
|
|
3777
|
+
.argument('[type]', 'Update type: app, kb, site, all (auto-detects if not specified)')
|
|
3778
|
+
.option('-c, --community <id>', 'Community ID (optional; app_id sufficient for Unified Registry)')
|
|
3779
|
+
.option('-a, --app <id>', 'App ID (globally unique; required if not in app directory)')
|
|
3780
|
+
.option('--kb <name>', 'Specific KB to update (for kb type)')
|
|
3781
|
+
.option('--stage1', 'KB: Local to Drive only (skip vectorization)')
|
|
3782
|
+
.option('--preview', 'Site: Deploy to preview URL')
|
|
3783
|
+
.option('--build <cmd>', 'Site: Run build command first')
|
|
3784
|
+
.option('--full', 'Force full sync (ignore delta)')
|
|
3785
|
+
.option('--port <number>', 'Site: Register local dev server port')
|
|
3786
|
+
.action(async (type, options) => {
|
|
3787
|
+
try {
|
|
3788
|
+
switch (type) {
|
|
3789
|
+
case 'app':
|
|
3790
|
+
await updateCommands.updateApp(options);
|
|
3791
|
+
break;
|
|
3792
|
+
case 'kb':
|
|
3793
|
+
await updateCommands.updateKB({ ...options, stage1Only: options.stage1 });
|
|
3794
|
+
break;
|
|
3795
|
+
case 'site':
|
|
3796
|
+
await updateCommands.updateSite({
|
|
3797
|
+
...options,
|
|
3798
|
+
port: options.port ? parseInt(options.port) : undefined
|
|
3799
|
+
});
|
|
3800
|
+
break;
|
|
3801
|
+
case 'all':
|
|
3802
|
+
await updateCommands.updateAll(options);
|
|
3803
|
+
break;
|
|
3804
|
+
default:
|
|
3805
|
+
// Auto-detect
|
|
3806
|
+
await updateCommands.updateAuto(options);
|
|
3807
|
+
}
|
|
3808
|
+
} catch (error) {
|
|
3809
|
+
process.exit(1);
|
|
3810
|
+
}
|
|
3811
|
+
});
|
|
3812
|
+
|
|
3813
|
+
// ============ Local Gateway Command ============
|
|
3814
|
+
|
|
3815
|
+
program
|
|
3816
|
+
.command('serve')
|
|
3817
|
+
.description('Start the unified local gateway (reverse-proxy for PWA, Core, Powch, microservices)')
|
|
3818
|
+
.option('-p, --port <port>', 'Gateway port', '5173')
|
|
3819
|
+
.option('-w, --workspace <path>', 'Workspace root override')
|
|
3820
|
+
.action(async (options) => {
|
|
3821
|
+
try {
|
|
3822
|
+
const { runServe } = await import('../lib/commands/serve.js');
|
|
3823
|
+
await runServe({
|
|
3824
|
+
port: parseInt(options.port, 10),
|
|
3825
|
+
workspaceRoot: options.workspace || process.cwd(),
|
|
3826
|
+
});
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
console.error(chalk.red('Gateway error:', error.message));
|
|
3829
|
+
process.exit(1);
|
|
3830
|
+
}
|
|
3831
|
+
});
|
|
3832
|
+
|
|
3833
|
+
// ============ Quickstart Command ============
|
|
3834
|
+
|
|
3835
|
+
program
|
|
3836
|
+
.command('quickstart')
|
|
3837
|
+
.description('One-command setup: auth ā workspace ā agent files ā MCP config')
|
|
3838
|
+
.option('-u, --url <url>', 'API URL override')
|
|
3839
|
+
.option('--dev', 'Use development server (https://localhost:4000)')
|
|
3840
|
+
.action(async (options) => {
|
|
3841
|
+
const { generateAgentFiles, generateMcpConfig } = await import('../lib/agent-files.js');
|
|
3842
|
+
const { WalletFileManager } = await import('../lib/wallet-file.js');
|
|
3843
|
+
|
|
3844
|
+
console.log(chalk.cyan('\nš DeSciX Quickstart\n'));
|
|
3845
|
+
|
|
3846
|
+
const workspaceRoot = process.cwd();
|
|
3847
|
+
|
|
3848
|
+
// Step 1: Auth ā login if no wallet.json
|
|
3849
|
+
const walletPath = WalletFileManager.getWalletPath(workspaceRoot);
|
|
3850
|
+
let needsLogin = true;
|
|
3851
|
+
try {
|
|
3852
|
+
const wallet = await WalletFileManager.loadWalletFile(walletPath);
|
|
3853
|
+
if (wallet && WalletFileManager.hasValidSession(wallet)) {
|
|
3854
|
+
console.log(chalk.green(`ā Already authenticated as ${wallet.userId}`));
|
|
3855
|
+
needsLogin = false;
|
|
3856
|
+
}
|
|
3857
|
+
} catch { /* no wallet */ }
|
|
3858
|
+
|
|
3859
|
+
if (needsLogin) {
|
|
3860
|
+
const loginOptions = {};
|
|
3861
|
+
if (options.url) loginOptions.url = options.url;
|
|
3862
|
+
if (options.dev) loginOptions.dev = true;
|
|
3863
|
+
await authCommands.loginDevice(loginOptions);
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
// Step 2: Workspace init ā create workspace.json if missing
|
|
3867
|
+
const wsConfigPath = path.join(workspaceRoot, '.descix', 'workspace.json');
|
|
3868
|
+
let hasWorkspace = false;
|
|
3869
|
+
try {
|
|
3870
|
+
await fs.access(wsConfigPath);
|
|
3871
|
+
hasWorkspace = true;
|
|
3872
|
+
console.log(chalk.green('ā Workspace already initialized'));
|
|
3873
|
+
} catch { /* missing */ }
|
|
3874
|
+
|
|
3875
|
+
if (!hasWorkspace) {
|
|
3876
|
+
console.log(chalk.cyan('\nš Initialize Workspace\n'));
|
|
3877
|
+
await runInit({ path: workspaceRoot });
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
// Step 3: Generate agent instruction files
|
|
3881
|
+
console.log(chalk.cyan('\nš Generating Agent Instructions\n'));
|
|
3882
|
+
const written = await generateAgentFiles(workspaceRoot);
|
|
3883
|
+
for (const f of written) {
|
|
3884
|
+
console.log(chalk.green(` ā ${f}`));
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
// Step 4: Generate .vscode/mcp.json (skipped if DeSciX extension handles MCP)
|
|
3888
|
+
const mcpWritten = await generateMcpConfig(workspaceRoot);
|
|
3889
|
+
if (mcpWritten) {
|
|
3890
|
+
console.log(chalk.green(' ā .vscode/mcp.json'));
|
|
3891
|
+
} else {
|
|
3892
|
+
console.log(chalk.green(' ā MCP handled by DeSciX extension (mcp.json skipped)'));
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
// Step 5: Copy SDK assets
|
|
3896
|
+
try {
|
|
3897
|
+
const { pullSdkAssets } = await import('../lib/wizard/setup.js');
|
|
3898
|
+
const pulled = await pullSdkAssets(workspaceRoot);
|
|
3899
|
+
if (pulled) console.log(chalk.green(' ā .descix/sdk-assets/'));
|
|
3900
|
+
} catch { /* setup.js pullSdkAssets may not be exported ā skip */ }
|
|
3901
|
+
|
|
3902
|
+
// Done
|
|
3903
|
+
console.log(chalk.green('\nā
Quickstart complete!\n'));
|
|
3904
|
+
console.log(chalk.white('Open your editor ā the AI knows about DeSciX.\n'));
|
|
3905
|
+
console.log(chalk.gray(' Copilot / Cline / Claude Code will have DeSciX MCP tools'));
|
|
3906
|
+
console.log(chalk.gray(' Ask: "What DeSciX tools do I have?"\n'));
|
|
3907
|
+
});
|
|
3908
|
+
|
|
3909
|
+
// ============ MCP Server Command (for npx usage) ============
|
|
3910
|
+
|
|
3911
|
+
program
|
|
3912
|
+
.command('mcp-serve')
|
|
3913
|
+
.description('Start MCP server for Cursor/VS Code integration (used by npx)')
|
|
3914
|
+
.action(async () => {
|
|
3915
|
+
try {
|
|
3916
|
+
await import('./mcp-server.js');
|
|
3917
|
+
} catch (error) {
|
|
3918
|
+
console.error(chalk.red('MCP server error:', error.message));
|
|
3919
|
+
process.exit(1);
|
|
3920
|
+
}
|
|
3921
|
+
});
|
|
3922
|
+
|
|
3923
|
+
// Parse arguments
|
|
3924
|
+
program.parse();
|
|
3925
|
+
|
|
3926
|
+
// Show help if no command provided
|
|
3927
|
+
if (!process.argv.slice(2).length) {
|
|
3928
|
+
program.outputHelp();
|
|
3929
|
+
}
|
|
3930
|
+
|