@descix/cli 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/agent-assets/guides/README.md +1 -1
- package/agent-assets/guides/dynamic-site-pattern.md +7 -16
- package/agent-assets/instructions/app-asset-validation.md +1 -1
- package/agent-assets/reference/architecture.md +20 -25
- package/agent-assets/reference/local-dev.md +1 -6
- package/agent-assets/reference/mcp-integration.md +15 -9
- package/agent-assets/reference/platform-api-consumer.md +11 -5
- package/agent-assets/reference/workspace-config.md +2 -2
- package/bin/descix.js +211 -688
- package/lib/command-visibility.js +213 -0
- package/lib/commands/corpus.js +2 -5
- package/lib/commands/kb.js +2 -2
- package/lib/commands/media-upload.js +176 -0
- package/lib/commands/retired-kb-sync.js +5 -27
- package/lib/commands/status.js +0 -9
- package/lib/output.js +31 -0
- package/lib/sdk-assets.js +28 -0
- package/lib/verb-invokes.generated.json +233 -0
- package/lib/workspace-config.js +9 -44
- package/package.json +6 -2
- package/templates/agent-claude.md +1 -3
- package/templates/agent-cline.md +1 -2
- package/templates/agent-copilot.md +1 -2
- package/templates/agent-cursor.md +1 -2
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +1 -1
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +2 -3
- package/agent-assets/rules/descix_mcp.template.mdc +0 -243
- package/lib/commands/briefer/errors.js +0 -88
- package/lib/commands/briefer/index.js +0 -322
- package/lib/commands/briefer/sources/canonical-sources.js +0 -129
- package/lib/commands/briefer/sources/entitlements.js +0 -220
- package/lib/commands/briefer/sources/environments.js +0 -205
- package/lib/commands/briefer/sources/identifiers.js +0 -214
- package/lib/commands/briefer/sources/microservice-deploy.js +0 -267
- package/lib/commands/briefer/sources/routing.js +0 -275
- package/lib/commands/briefer/sources/what-is-not.js +0 -223
- package/lib/commands/briefer/stitcher.js +0 -134
- package/lib/commands/briefer/util/source-reader.js +0 -502
- package/lib/commands/mcp.js +0 -273
- package/lib/commands/update.js +0 -581
- package/lib/wizard/setup.js +0 -585
- package/templates/descix_mcp.mdc +0 -122
package/bin/descix.js
CHANGED
|
@@ -31,8 +31,7 @@ import * as airdropCommands from '../lib/commands/airdrop.js';
|
|
|
31
31
|
import { runInit } from '../lib/commands/init.js';
|
|
32
32
|
// "May I prompt?" has ONE OWNER. No command in this file derives it.
|
|
33
33
|
import { createPromptSession } from '../lib/interactive.js';
|
|
34
|
-
import
|
|
35
|
-
import { registerAllRetiredKbSync, refuseRetiredKbSync, CANONICAL_KB_SYNC } from '../lib/commands/retired-kb-sync.js';
|
|
34
|
+
import { registerAllRetiredKbSync, CANONICAL_KB_SYNC } from '../lib/commands/retired-kb-sync.js';
|
|
36
35
|
import { runStatus } from '../lib/commands/status.js';
|
|
37
36
|
import { runDoctor } from '../lib/commands/doctor.js';
|
|
38
37
|
import { runHealth } from '../lib/commands/health.js';
|
|
@@ -40,36 +39,13 @@ import * as kbCommands from '../lib/commands/kb.js';
|
|
|
40
39
|
import { kbVectorCell, kbCountSource } from '../lib/commands/kb-list-render.js';
|
|
41
40
|
import * as corpusCommands from '../lib/commands/corpus.js';
|
|
42
41
|
import * as modelConfigCommands from '../lib/commands/model-config.js';
|
|
43
|
-
import
|
|
42
|
+
import { isHelpInvocation, getCommandSurface, applyVisibility } from '../lib/command-visibility.js';
|
|
43
|
+
import { progress } from '../lib/output.js';
|
|
44
|
+
import { runMediaUpload } from '../lib/commands/media-upload.js';
|
|
44
45
|
import fs from 'fs/promises';
|
|
45
46
|
import path from 'path';
|
|
46
47
|
import os from 'os';
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Emit human-facing PROGRESS for a command that can also emit JSON.
|
|
51
|
-
*
|
|
52
|
-
* When `--json` is set, stdout is a DATA CHANNEL: it must contain the JSON document and
|
|
53
|
-
* nothing else, or every scripted consumer breaks. Progress then goes to stderr, where a
|
|
54
|
-
* human piping stdout still sees it. Without `--json`, stdout is the human channel and
|
|
55
|
-
* progress belongs there.
|
|
56
|
-
*
|
|
57
|
-
* ONE OWNER of that decision. Before this existed, each --json command re-decided it by
|
|
58
|
-
* calling console.log directly, and both of them got it wrong: `mcp execute --json` prefixed
|
|
59
|
-
* the document with "Executing <tool>..." and `app media-upload --json` prefixed it with a
|
|
60
|
-
* header, a per-file listing and a per-file +/x line. Both exited 0, so a consumer saw a
|
|
61
|
-
* success code and an unparseable stream. Measured 2026-08-21/22 (seat BEAST): two seats had
|
|
62
|
-
* independently hand-rolled prefix-stripping workarounds (`tail -n +2`, `raw.find('[')`)
|
|
63
|
-
* rather than reporting it.
|
|
64
|
-
*
|
|
65
|
-
* @param {Object} options - the command's parsed options (read for .json)
|
|
66
|
-
* @param {...any} args - passed through to console.log / console.error
|
|
67
|
-
*/
|
|
68
|
-
function progress(options, ...args) {
|
|
69
|
-
if (options && options.json) console.error(...args);
|
|
70
|
-
else console.log(...args);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
49
|
/**
|
|
74
50
|
* THE ONE OWNER of "a command threw, and the process must stop saying why".
|
|
75
51
|
*
|
|
@@ -99,7 +75,8 @@ program
|
|
|
99
75
|
.description('DeSciX CLI - Unified command-line interface')
|
|
100
76
|
.version(CLI_VERSION)
|
|
101
77
|
.option('--env <name>', 'Target environment: dev, demo, prod (overrides API URL)')
|
|
102
|
-
.option('--api-url <url>', 'Direct API URL override (e.g., https://demo.descix.net)')
|
|
78
|
+
.option('--api-url <url>', 'Direct API URL override (e.g., https://demo.descix.net)')
|
|
79
|
+
.option('--admin', 'Show every command in --help, including admin-only ones (also: DESCIX_ADMIN=1). Hiding is a listing convenience only — a hidden command still runs; the server is the real gate.');
|
|
103
80
|
|
|
104
81
|
// ============ Global Environment Override ============
|
|
105
82
|
// Maps --env flag to DESCIX_API_URL before any command runs.
|
|
@@ -116,24 +93,27 @@ const ENV_URL_MAP = Object.fromEntries(
|
|
|
116
93
|
// stays; the provenance is now RECORDED alongside it, in the same breath, so the owner can name
|
|
117
94
|
// the actual source. Recording it here rather than deriving it in the owner keeps one owner of
|
|
118
95
|
// the fact: the only code that knows a flag was passed is the code that read the flag.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
96
|
+
//
|
|
97
|
+
// ONE OWNER of "apply --api-url / --env to DESCIX_API_URL", called from TWO sites: the
|
|
98
|
+
// `preAction` hook below (which only ever fires before an ACTION runs — never for `--help`,
|
|
99
|
+
// since Commander short-circuits help before reaching any action), and the help-time admin-
|
|
100
|
+
// surface bootstrap near the bottom of this file (which runs BEFORE `program.parseAsync()`, so
|
|
101
|
+
// `program.opts()` is not populated yet and the two flags are read off raw argv instead). Without
|
|
102
|
+
// this second call site, `descix --api-url https://X --help` silently queried the DEFAULT origin
|
|
103
|
+
// for the admin-listing surface while claiming to target X — the same class of misreport
|
|
104
|
+
// `resolveEffectiveOrigin` exists to end, just newly reachable through the one invocation shape
|
|
105
|
+
// (`--help`) that never runs an action.
|
|
106
|
+
function applyGlobalOriginOverride({ apiUrl, env }) {
|
|
127
107
|
// --api-url takes highest priority
|
|
128
|
-
if (
|
|
129
|
-
process.env.DESCIX_API_URL =
|
|
130
|
-
recordInvocationOrigin(
|
|
108
|
+
if (apiUrl) {
|
|
109
|
+
process.env.DESCIX_API_URL = apiUrl;
|
|
110
|
+
recordInvocationOrigin(apiUrl, 'flagApiUrl');
|
|
131
111
|
return;
|
|
132
112
|
}
|
|
133
113
|
|
|
134
114
|
// --env maps to known URLs
|
|
135
|
-
if (
|
|
136
|
-
const envName =
|
|
115
|
+
if (env) {
|
|
116
|
+
const envName = env.toLowerCase();
|
|
137
117
|
if (!(envName in ENV_URL_MAP)) {
|
|
138
118
|
// THE REFUSAL CARRIES THE CUSTOM-ENVIRONMENT PATH. This validator runs BEFORE any command
|
|
139
119
|
// action, so it is now the only thing a developer typing `--env staging` ever sees — the
|
|
@@ -142,7 +122,7 @@ program.hook('preAction', (thisCommand) => {
|
|
|
142
122
|
// names would strand every self-hosted or port-forwarded gateway with no next step, which
|
|
143
123
|
// is the same dead end as a remedy string for a command that does not run.
|
|
144
124
|
console.error(chalk.red(
|
|
145
|
-
`Unknown environment: ${
|
|
125
|
+
`Unknown environment: ${env}. Known environments: ${Object.keys(ENV_URL_MAP).join(', ')}.\n\n` +
|
|
146
126
|
'For a self-hosted or port-forwarded gateway, name it with its origin instead:\n' +
|
|
147
127
|
` descix config set-env ${envName} --url https://...`
|
|
148
128
|
));
|
|
@@ -153,6 +133,16 @@ program.hook('preAction', (thisCommand) => {
|
|
|
153
133
|
process.env.DESCIX_API_URL = ENV_URL_MAP[envName];
|
|
154
134
|
recordInvocationOrigin(ENV_URL_MAP[envName], 'flagEnv');
|
|
155
135
|
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
program.hook('preAction', (thisCommand) => {
|
|
139
|
+
// Read from the ROOT program, not from the action command. `--env` and `--api-url` are
|
|
140
|
+
// declared ONCE, globally, and Commander binds them to the root — an identically-named option
|
|
141
|
+
// redeclared on a subcommand is shadowed and silently never populated (measured on 1.0.4:
|
|
142
|
+
// `descix config init --env dev` reached its action with `{}` and rejected its own advertised
|
|
143
|
+
// flag). One declaration, read from where Commander actually puts it.
|
|
144
|
+
const opts = program.opts();
|
|
145
|
+
applyGlobalOriginOverride({ apiUrl: opts.apiUrl, env: opts.env });
|
|
156
146
|
});
|
|
157
147
|
|
|
158
148
|
// ============ Authentication Commands (No Auth Required) ============
|
|
@@ -302,32 +292,6 @@ program
|
|
|
302
292
|
}
|
|
303
293
|
});
|
|
304
294
|
|
|
305
|
-
// ============ Briefer Command (Code-Grounded Mental Model Regen) ============
|
|
306
|
-
// WS-DESCIX-BRIEFER-CLI M1: CLI scaffolding + extractor contract.
|
|
307
|
-
// See DeSciX/DeSciX_Core/descix-cli/lib/commands/briefer/ for implementation.
|
|
308
|
-
// Scope doc: docs/design/ws-descix-briefer-cli.md
|
|
309
|
-
// Briefer target: DeSciX/V2_docs/architecture/platform-must-know-briefer.md
|
|
310
|
-
|
|
311
|
-
program
|
|
312
|
-
.command('briefer')
|
|
313
|
-
.description('Regenerate platform-must-know-briefer.md from live code + gcloud + Firestore (HARD-FAIL on drift)')
|
|
314
|
-
.option('--env <name>', 'Target environment: dev|demo|prod (also accepted at the top level: descix --env=demo briefer ...)')
|
|
315
|
-
.option('--out <path>', 'Override output path (default: workspace-root/DeSciX/V2_docs/architecture/platform-must-know-briefer.md)')
|
|
316
|
-
.option('--check', 'Drift-detection mode: regen to memory, diff against canonical, non-zero exit on drift')
|
|
317
|
-
.option('-v, --verbose', 'Print per-source paths, citations, and timings')
|
|
318
|
-
.action(async (options) => {
|
|
319
|
-
try {
|
|
320
|
-
// Resolve --env in priority order: subcommand explicit > program parent > 'dev'.
|
|
321
|
-
// Commander would otherwise silently default to 'dev' even when the parent
|
|
322
|
-
// --env=demo is supplied (because the subcommand owns its own flag space).
|
|
323
|
-
const parentEnv = program.opts().env || null;
|
|
324
|
-
const env = options.env || parentEnv || 'dev';
|
|
325
|
-
await brieferCommand.runBriefer({ ...options, env });
|
|
326
|
-
} catch (error) {
|
|
327
|
-
console.error(chalk.red(`\n❌ Briefer command failed: ${error.message}\n`));
|
|
328
|
-
process.exit(1);
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
295
|
|
|
332
296
|
// ============ Buy Commands (Crypto Payments) ============
|
|
333
297
|
|
|
@@ -433,7 +397,7 @@ creditsCommand
|
|
|
433
397
|
|
|
434
398
|
creditsCommand
|
|
435
399
|
.command('grant')
|
|
436
|
-
.description('
|
|
400
|
+
.description('Grant AI credits to a user')
|
|
437
401
|
.requiredOption('--user <user_id>', 'Target user id')
|
|
438
402
|
.requiredOption('--usd <amount>', 'USD amount to grant')
|
|
439
403
|
.requiredOption('--reason <text>', 'Audit reason')
|
|
@@ -447,7 +411,7 @@ creditsCommand
|
|
|
447
411
|
|
|
448
412
|
creditsCommand
|
|
449
413
|
.command('refund')
|
|
450
|
-
.description('
|
|
414
|
+
.description('Remove AI credits from a user (e.g. after a Stripe refund)')
|
|
451
415
|
.requiredOption('--user <user_id>', 'Target user id')
|
|
452
416
|
.requiredOption('--usd <amount>', 'USD amount to remove')
|
|
453
417
|
.requiredOption('--reason <text>', 'Audit reason')
|
|
@@ -593,7 +557,7 @@ communityCommand
|
|
|
593
557
|
|
|
594
558
|
communityCommand
|
|
595
559
|
.command('create')
|
|
596
|
-
.description('
|
|
560
|
+
.description('Create a new community with token contract (requires platform admin, deploys to live Polygon)')
|
|
597
561
|
.requiredOption('-n, --name <name>', 'Community display name (e.g. "SMILE")')
|
|
598
562
|
.requiredOption('-t, --token <symbol>', 'Token symbol (e.g. SMILE)')
|
|
599
563
|
.option('--icon <url>', 'Icon URL for the community')
|
|
@@ -661,7 +625,7 @@ communityCommand
|
|
|
661
625
|
|
|
662
626
|
communityCommand
|
|
663
627
|
.command('delete')
|
|
664
|
-
.description('
|
|
628
|
+
.description('Delete a community with full cascade cleanup (all apps, KBs, vectors, GCS)')
|
|
665
629
|
.requiredOption('-n, --name <community_id>', 'Community ID to delete')
|
|
666
630
|
.option('--dry-run', 'Preview what would be deleted without executing')
|
|
667
631
|
.option('--soft', 'Soft delete only (mark as hidden, no cascade)')
|
|
@@ -770,7 +734,7 @@ communityCommand
|
|
|
770
734
|
|
|
771
735
|
communityCommand
|
|
772
736
|
.command('rename')
|
|
773
|
-
.description('
|
|
737
|
+
.description('Rename a community_id across all surfaces (Firestore, Products, Pinecone metadata, ServiceManifests, descix-chain registry). On-chain contract + token symbol are UNTOUCHED. Dry-run first.')
|
|
774
738
|
.argument('<old_community_id>', 'Current community_id (e.g. unkamon)')
|
|
775
739
|
.argument('<new_community_id>', 'New canonical community_id (e.g. unk)')
|
|
776
740
|
.option('--dry-run', 'Preview the full cascade plan without executing')
|
|
@@ -1207,37 +1171,14 @@ appCommand
|
|
|
1207
1171
|
}
|
|
1208
1172
|
});
|
|
1209
1173
|
|
|
1210
|
-
appCommand
|
|
1211
|
-
.command('create')
|
|
1212
|
-
// DELETED SURFACE (CEO-D-2026-08-14, SUPER-DRY: delete the old path, fail loud, no compat
|
|
1213
|
-
// fence). `app create` created the platform record and STOPPED, leaving the caller to
|
|
1214
|
-
// remember `app init` — two commands for one outcome. That is how apps ended up half-made
|
|
1215
|
-
// with no knowledge base at all. `app init` now creates AND initializes in one path.
|
|
1216
|
-
.description('[REMOVED] Use `descix app init -a <name> -c <community>` — it creates and initializes in one step.')
|
|
1217
|
-
// Swallow whatever the old invocation passed so the caller always gets the pointer below
|
|
1218
|
-
// rather than a generic commander arity error that names no replacement.
|
|
1219
|
-
.allowUnknownOption(true)
|
|
1220
|
-
.argument('[ignored...]')
|
|
1221
|
-
.action(() => {
|
|
1222
|
-
console.error(chalk.red('\n❌ `descix app create` has been removed.\n'));
|
|
1223
|
-
console.error(chalk.white(' Use the single canonical path, which creates AND initializes:\n'));
|
|
1224
|
-
console.error(chalk.cyan(' descix app init -a <app-name> -c <community> [-s <short>]\n'));
|
|
1225
|
-
console.error(chalk.gray(' It registers the app (Products + Firestore + entitlement), guarantees'));
|
|
1226
|
-
console.error(chalk.gray(' the default knowledge base, then registers and scaffolds it locally.'));
|
|
1227
|
-
console.error(chalk.gray(' Creating an app without a default KB is no longer possible.\n'));
|
|
1228
|
-
process.exit(1);
|
|
1229
|
-
});
|
|
1230
1174
|
|
|
1231
1175
|
// descix app media-upload — upload media/asset files to an app's GCS assets prefix via the
|
|
1232
|
-
// API surface (WS-V1-PURGE Phase 1, item 2; media-via-API-surface PLATFORM half).
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1235
|
-
// (`get_asset_upload_token` over /apifront, user-session authed), upload each file straight
|
|
1236
|
-
// to GCS, and print the ASSET REFERENCES. An app microservice then fetches an uploaded asset
|
|
1237
|
-
// by reference over the Core broker via `get_app_asset` (no shared local filesystem needed).
|
|
1176
|
+
// API surface (WS-V1-PURGE Phase 1, item 2; media-via-API-surface PLATFORM half). Command body
|
|
1177
|
+
// lives in lib/commands/media-upload.js (runMediaUpload) — see that module for the full
|
|
1178
|
+
// contract (server-owned quota/path/permission checks, verbatim upload_headers ferrying).
|
|
1238
1179
|
appCommand
|
|
1239
1180
|
.command('media-upload')
|
|
1240
|
-
.description('Upload media/asset files to an app\'s GCS assets prefix
|
|
1181
|
+
.description('Upload media/asset files to an app\'s GCS assets prefix (200 MB per app today; app owners, community admins and platform admins may upload — returns asset references)')
|
|
1241
1182
|
.requiredOption('-a, --app <id>', 'App ID (community is resolved server-side from Products)')
|
|
1242
1183
|
.requiredOption('-f, --file <path...>', 'One or more local file paths to upload')
|
|
1243
1184
|
.option('--prefix <relPath>', 'Optional sub-path under the app assets/ prefix (e.g. "shows/myshow")', '')
|
|
@@ -1246,160 +1187,13 @@ appCommand
|
|
|
1246
1187
|
try {
|
|
1247
1188
|
const apiClient = new DeSciXApiClient();
|
|
1248
1189
|
await requireAuth(apiClient);
|
|
1249
|
-
|
|
1250
|
-
const path = await import('path');
|
|
1251
|
-
const mime = (await import('mime-types')).default;
|
|
1252
|
-
|
|
1253
|
-
const appId = options.app;
|
|
1254
|
-
const localFiles = Array.isArray(options.file) ? options.file : [options.file];
|
|
1255
|
-
const subPrefix = (options.prefix || '').replace(/^\/+|\/+$/g, '');
|
|
1256
|
-
|
|
1257
|
-
// Build the upload descriptor list: object path under assets/ = [subPrefix/]basename.
|
|
1258
|
-
const fileDescriptors = [];
|
|
1259
|
-
for (const localPath of localFiles) {
|
|
1260
|
-
const abs = path.resolve(localPath);
|
|
1261
|
-
let stat;
|
|
1262
|
-
try {
|
|
1263
|
-
stat = await fs.stat(abs);
|
|
1264
|
-
} catch {
|
|
1265
|
-
console.error(chalk.red(`\n❌ File not found: ${abs}\n`));
|
|
1266
|
-
process.exit(1);
|
|
1267
|
-
}
|
|
1268
|
-
if (!stat.isFile()) {
|
|
1269
|
-
console.error(chalk.red(`\n❌ Not a file: ${abs}\n`));
|
|
1270
|
-
process.exit(1);
|
|
1271
|
-
}
|
|
1272
|
-
const base = path.basename(abs);
|
|
1273
|
-
const objectPath = subPrefix ? `${subPrefix}/${base}` : base;
|
|
1274
|
-
fileDescriptors.push({
|
|
1275
|
-
path: objectPath,
|
|
1276
|
-
content_type: mime.lookup(abs) || 'application/octet-stream',
|
|
1277
|
-
size: stat.size,
|
|
1278
|
-
_absolutePath: abs
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
console.log(chalk.cyan(`\n Media Upload: ${appId} → GCS assets/\n`));
|
|
1283
|
-
fileDescriptors.forEach(f => progress(options, chalk.gray(` • ${f.path} (${(f.size / 1024).toFixed(1)}KB, ${f.content_type})`)));
|
|
1284
|
-
|
|
1285
|
-
// 1. Request a signed-PUT upload token over the API surface.
|
|
1286
|
-
const tokenResponse = await apiClient.invoke('get_asset_upload_token', {
|
|
1287
|
-
app_id: appId,
|
|
1288
|
-
files: fileDescriptors.map(f => ({ path: f.path, content_type: f.content_type, size: f.size }))
|
|
1289
|
-
});
|
|
1290
|
-
const token = tokenResponse.message || tokenResponse;
|
|
1291
|
-
const { signed_urls, objects } = token;
|
|
1292
|
-
|
|
1293
|
-
// 2. PUT each file directly to GCS using its signed URL.
|
|
1294
|
-
progress(options, chalk.gray(`\n Uploading ${fileDescriptors.length} file(s)...`));
|
|
1295
|
-
const uploaded = [];
|
|
1296
|
-
const errors = [];
|
|
1297
|
-
for (const f of fileDescriptors) {
|
|
1298
|
-
const signedUrl = signed_urls?.[f.path];
|
|
1299
|
-
if (!signedUrl) {
|
|
1300
|
-
errors.push(`No signed URL for: ${f.path}`);
|
|
1301
|
-
progress(options, chalk.red(` x ${f.path}`));
|
|
1302
|
-
continue;
|
|
1303
|
-
}
|
|
1304
|
-
try {
|
|
1305
|
-
const content = await fs.readFile(f._absolutePath);
|
|
1306
|
-
const resp = await fetch(signedUrl, {
|
|
1307
|
-
method: 'PUT',
|
|
1308
|
-
headers: { 'Content-Type': f.content_type },
|
|
1309
|
-
body: content
|
|
1310
|
-
});
|
|
1311
|
-
if (!resp.ok) {
|
|
1312
|
-
errors.push(`Failed ${f.path}: ${resp.status} ${resp.statusText}`);
|
|
1313
|
-
progress(options, chalk.red(` x ${f.path}`));
|
|
1314
|
-
} else {
|
|
1315
|
-
const obj = (objects || []).find(o => o.path === f.path) || {};
|
|
1316
|
-
// The asset REFERENCE an app handler / get_app_asset consumes: the gs:// URI.
|
|
1317
|
-
const gcsRef = obj.gcs_path
|
|
1318
|
-
? `gs://${token.bucket}/${obj.gcs_path}`
|
|
1319
|
-
: null;
|
|
1320
|
-
uploaded.push({
|
|
1321
|
-
path: f.path,
|
|
1322
|
-
ref: gcsRef,
|
|
1323
|
-
gcs_path: obj.gcs_path || null,
|
|
1324
|
-
public_url: obj.public_url || null,
|
|
1325
|
-
content_type: f.content_type,
|
|
1326
|
-
size: f.size
|
|
1327
|
-
});
|
|
1328
|
-
progress(options, chalk.green(` + ${f.path}`));
|
|
1329
|
-
}
|
|
1330
|
-
} catch (err) {
|
|
1331
|
-
errors.push(`Error ${f.path}: ${err.message}`);
|
|
1332
|
-
progress(options, chalk.red(` x ${f.path}`));
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (errors.length > 0) {
|
|
1337
|
-
progress(options, chalk.yellow(`\n ${errors.length} error(s):`));
|
|
1338
|
-
errors.forEach(e => progress(options, chalk.red(` - ${e}`)));
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
if (uploaded.length === 0) {
|
|
1342
|
-
console.error(chalk.red('\n❌ No files uploaded.\n'));
|
|
1343
|
-
process.exit(1);
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
if (options.json) {
|
|
1347
|
-
console.log('\n' + JSON.stringify({ app_id: appId, assets: uploaded }, null, 2) + '\n');
|
|
1348
|
-
} else {
|
|
1349
|
-
console.log(chalk.green(`\n ✅ Uploaded ${uploaded.length} asset(s).\n`));
|
|
1350
|
-
console.log(chalk.cyan(' Asset references (pass these to your app handler; the service fetches via get_app_asset):'));
|
|
1351
|
-
uploaded.forEach(u => {
|
|
1352
|
-
console.log(chalk.white(` ${u.path}`));
|
|
1353
|
-
console.log(chalk.gray(` ref: ${u.ref}`));
|
|
1354
|
-
console.log(chalk.gray(` public_url: ${u.public_url}`));
|
|
1355
|
-
});
|
|
1356
|
-
console.log();
|
|
1357
|
-
}
|
|
1190
|
+
await runMediaUpload(apiClient, options);
|
|
1358
1191
|
} catch (error) {
|
|
1359
1192
|
console.error(chalk.red(`\n❌ ${error.message}\n`));
|
|
1360
1193
|
process.exit(1);
|
|
1361
1194
|
}
|
|
1362
1195
|
});
|
|
1363
1196
|
|
|
1364
|
-
appCommand
|
|
1365
|
-
.command('set-codesite')
|
|
1366
|
-
.description('Set CodeSite URL/path for an app (use "descix site servelocal" for local dev port)')
|
|
1367
|
-
.option('-c, --community <id>', 'Community ID (uses context if not provided)')
|
|
1368
|
-
.option('-a, --app <id>', 'App ID (uses context if not provided)')
|
|
1369
|
-
.requiredOption('-u, --url <url>', 'CodeSite URL (GCS path or HTTPS URL)')
|
|
1370
|
-
.action(async (options) => {
|
|
1371
|
-
try {
|
|
1372
|
-
const apiClient = new DeSciXApiClient();
|
|
1373
|
-
await requireAuth(apiClient);
|
|
1374
|
-
|
|
1375
|
-
// Load workspace context
|
|
1376
|
-
const workspaceConfig = await WorkspaceConfig.load();
|
|
1377
|
-
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
1378
|
-
|
|
1379
|
-
const communityId = ctx.communityId;
|
|
1380
|
-
const appId = ctx.appId;
|
|
1381
|
-
|
|
1382
|
-
if (!communityId || !appId) {
|
|
1383
|
-
console.error(chalk.red('\n❌ Community and App ID required.'));
|
|
1384
|
-
console.log(chalk.gray(' Either provide -c and -a flags, or cd into an app directory\n'));
|
|
1385
|
-
process.exit(1);
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
const response = await apiClient.invoke('update_app_metadata', {
|
|
1389
|
-
community_id: communityId,
|
|
1390
|
-
app_id: appId,
|
|
1391
|
-
ip_site_gcs_path_url: options.url
|
|
1392
|
-
});
|
|
1393
|
-
const result = response.message || response;
|
|
1394
|
-
|
|
1395
|
-
console.log(chalk.green('\n✅ CodeSite URL updated!\n'));
|
|
1396
|
-
console.log(chalk.cyan(` App: ${communityId}/${appId}`));
|
|
1397
|
-
console.log(chalk.gray(` CodeSite URL: ${options.url}\n`));
|
|
1398
|
-
} catch (error) {
|
|
1399
|
-
console.error(chalk.red(error.message));
|
|
1400
|
-
process.exit(1);
|
|
1401
|
-
}
|
|
1402
|
-
});
|
|
1403
1197
|
|
|
1404
1198
|
appCommand
|
|
1405
1199
|
.command('set-price')
|
|
@@ -1460,6 +1254,43 @@ appCommand
|
|
|
1460
1254
|
}
|
|
1461
1255
|
});
|
|
1462
1256
|
|
|
1257
|
+
appCommand
|
|
1258
|
+
.command('set-listed')
|
|
1259
|
+
.description('List or unlist your app in the app store (on: shown in the public store; off: not shown to non-admin visitors)')
|
|
1260
|
+
.argument('<state>', 'on or off')
|
|
1261
|
+
.requiredOption('-c, --community <id>', 'Community ID')
|
|
1262
|
+
.requiredOption('-a, --app <id>', 'App ID')
|
|
1263
|
+
.action(async (state, options) => {
|
|
1264
|
+
try {
|
|
1265
|
+
const wanted = { on: true, off: false }[String(state).toLowerCase()];
|
|
1266
|
+
if (wanted === undefined) {
|
|
1267
|
+
throw new Error(`set-listed takes 'on' or 'off', not '${state}'.`);
|
|
1268
|
+
}
|
|
1269
|
+
const apiClient = new DeSciXApiClient();
|
|
1270
|
+
await requireAuth(apiClient);
|
|
1271
|
+
|
|
1272
|
+
const response = await apiClient.invoke('update_app', {
|
|
1273
|
+
community_id: options.community,
|
|
1274
|
+
app_id: options.app,
|
|
1275
|
+
listed: wanted
|
|
1276
|
+
});
|
|
1277
|
+
const result = response.message || response;
|
|
1278
|
+
// The receipt is the value the SERVER holds after the write, never the value requested.
|
|
1279
|
+
const stored = result?.app?.listed;
|
|
1280
|
+
if (stored !== wanted) {
|
|
1281
|
+
throw new Error(`update_app returned listed=${JSON.stringify(stored)} for ${options.app}; expected ${wanted}. The store listing did not change.`);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
console.log(chalk.green(`\n✅ App ${stored ? 'listed' : 'unlisted'}.\n`));
|
|
1285
|
+
console.log(chalk.cyan(` App: ${options.community}/${options.app}`));
|
|
1286
|
+
console.log(chalk.gray(` listed: ${stored}`));
|
|
1287
|
+
console.log(chalk.gray(' The store view is cached for up to 5 minutes, so the change can take that long to show.\n'));
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
console.error(chalk.red(error.message));
|
|
1290
|
+
process.exit(1);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1463
1294
|
appCommand
|
|
1464
1295
|
.command('set-api-url')
|
|
1465
1296
|
.description('Set API base URL for app\'s own backend')
|
|
@@ -1848,7 +1679,7 @@ appCommand
|
|
|
1848
1679
|
// Canonical write path for an app's microservice.port in workspace.json.
|
|
1849
1680
|
// `descix microservice init` READS env.products[<app>].microservice.port and hard-fails
|
|
1850
1681
|
// if it is missing; this command is how that port is set, without hand-editing workspace.json.
|
|
1851
|
-
// Backed by WorkspaceConfig.setMicroservicePort
|
|
1682
|
+
// Backed by WorkspaceConfig.setMicroservicePort.
|
|
1852
1683
|
appCommand
|
|
1853
1684
|
.command('set-port')
|
|
1854
1685
|
.description('Set the microservice port for a mapped app (writes env.products[<app>].microservice.port). Pass "n" to remove.')
|
|
@@ -1893,10 +1724,8 @@ appCommand
|
|
|
1893
1724
|
// the app's localPath; "." means the localPath itself). Optionally sets site.port for
|
|
1894
1725
|
// dev-server sites. Parallel to `descix app set-port` (microservice.port); closes the
|
|
1895
1726
|
// site.static gap so workspace.json never needs hand-editing (CEO-D-2026-06-02-SSGPOD-SITE-PREPROD).
|
|
1896
|
-
// Backed by WorkspaceConfig.setStaticSite (parallel to
|
|
1897
|
-
//
|
|
1898
|
-
// NOTE: This is NOT `set-codesite` — that writes the Firestore ip_site_gcs_path_url (a prod
|
|
1899
|
-
// concern). set-site writes ONLY the local workspace.json site.{} slot.
|
|
1727
|
+
// Backed by WorkspaceConfig.setStaticSite (parallel to setMicroservicePort).
|
|
1728
|
+
// It writes ONLY the local workspace.json site.{} slot.
|
|
1900
1729
|
appCommand
|
|
1901
1730
|
.command('set-site')
|
|
1902
1731
|
.description("Set a mapped app static-site config (writes env.products[<app>].site.static, the relative dir served at /p/<app>/). Optionally --port; --unset clears site.{}.")
|
|
@@ -2119,32 +1948,6 @@ KB's vectors by id-prefix). The Source column shows how each count was obtained:
|
|
|
2119
1948
|
}
|
|
2120
1949
|
});
|
|
2121
1950
|
|
|
2122
|
-
kbCommand
|
|
2123
|
-
.command('create')
|
|
2124
|
-
.description('Create a new knowledge base in an app')
|
|
2125
|
-
.requiredOption('-c, --community <id>', 'Community ID')
|
|
2126
|
-
.requiredOption('-a, --app <id>', 'App ID')
|
|
2127
|
-
.requiredOption('-k, --kb <id>', 'Knowledge Base ID')
|
|
2128
|
-
.action(async (options) => {
|
|
2129
|
-
try {
|
|
2130
|
-
const apiClient = new DeSciXApiClient();
|
|
2131
|
-
await requireAuth(apiClient);
|
|
2132
|
-
|
|
2133
|
-
const response = await apiClient.invoke('create_skeleton_kb', {
|
|
2134
|
-
community_id: options.community,
|
|
2135
|
-
app_id: options.app,
|
|
2136
|
-
kb_name: options.kb
|
|
2137
|
-
});
|
|
2138
|
-
const result = response.message || response;
|
|
2139
|
-
|
|
2140
|
-
console.log(chalk.green('\n✅ Knowledge Base created successfully!\n'));
|
|
2141
|
-
console.log(chalk.cyan(` KB ID: ${options.kb}`));
|
|
2142
|
-
console.log(chalk.gray(` App: ${options.community}/${options.app}`));
|
|
2143
|
-
} catch (error) {
|
|
2144
|
-
console.error(chalk.red(error.message));
|
|
2145
|
-
process.exit(1);
|
|
2146
|
-
}
|
|
2147
|
-
});
|
|
2148
1951
|
|
|
2149
1952
|
kbCommand
|
|
2150
1953
|
.command('delete')
|
|
@@ -2514,7 +2317,7 @@ driveCommand
|
|
|
2514
2317
|
|
|
2515
2318
|
const siteCommand = program
|
|
2516
2319
|
.command('site')
|
|
2517
|
-
.description('Site lifecycle management: init, upload,
|
|
2320
|
+
.description('Site lifecycle management: init, upload, status, list, delete');
|
|
2518
2321
|
|
|
2519
2322
|
// site init - Copy scaffold to app
|
|
2520
2323
|
siteCommand
|
|
@@ -2570,8 +2373,7 @@ siteCommand
|
|
|
2570
2373
|
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
2571
2374
|
.option('-p, --path <localPath>', 'Local directory to deploy (ignored when site manifest exists)', './site')
|
|
2572
2375
|
.option('--preview', 'Deploy to preview path')
|
|
2573
|
-
.option('--
|
|
2574
|
-
.option('--dry-run', 'Show what would be deployed')
|
|
2376
|
+
.option('--dry-run', 'Show what would be uploaded and which server files would be removed')
|
|
2575
2377
|
.option('--no-cache', 'Set Cache-Control: no-cache')
|
|
2576
2378
|
.action(async (options) => {
|
|
2577
2379
|
try {
|
|
@@ -2744,92 +2546,76 @@ siteCommand
|
|
|
2744
2546
|
|
|
2745
2547
|
const { signed_urls, upload_headers, existing_manifest, token_id, site_url } = tokenResponse.message;
|
|
2746
2548
|
|
|
2747
|
-
// 3.
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
if (!options.full && existing_manifest) {
|
|
2752
|
-
const delta = gitUtils.compareSyncState(localFiles, existing_manifest);
|
|
2753
|
-
filesToUpload = fileList.filter(f =>
|
|
2754
|
-
delta.added.includes(f.path) || delta.modified.includes(f.path)
|
|
2755
|
-
);
|
|
2756
|
-
filesToDelete = delta.deleted;
|
|
2757
|
-
|
|
2758
|
-
console.log(chalk.gray(` Delta: ${delta.added.length} added, ${delta.modified.length} modified, ${delta.unchanged.length} unchanged, ${delta.deleted.length} deleted`));
|
|
2759
|
-
}
|
|
2549
|
+
// 3. A deploy REPLACES the site: every file of this build is uploaded, and on confirm the
|
|
2550
|
+
// server deletes every site file this build does not contain (confirm_site_deploy).
|
|
2551
|
+
const filesToUpload = fileList;
|
|
2760
2552
|
|
|
2761
2553
|
if (options.dryRun) {
|
|
2762
2554
|
console.log(chalk.yellow('\n Dry run - would upload:'));
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
if (
|
|
2769
|
-
console.log(chalk.yellow('\
|
|
2770
|
-
|
|
2555
|
+
filesToUpload.forEach(f => console.log(chalk.gray(` + ${f.path} (${(f.size / 1024).toFixed(1)}KB)`)));
|
|
2556
|
+
// The server's last deploy manifest names what it holds; the confirm step lists the
|
|
2557
|
+
// storage prefix itself, so this preview is the manifest's view of what would go.
|
|
2558
|
+
const lastDeployed = Object.keys(existing_manifest?.files || {});
|
|
2559
|
+
const wouldRemove = lastDeployed.filter(p => !(p in localFiles)).sort();
|
|
2560
|
+
if (wouldRemove.length > 0) {
|
|
2561
|
+
console.log(chalk.yellow('\n Would remove from the server (in the last deploy, not in this build):'));
|
|
2562
|
+
wouldRemove.forEach(f => console.log(chalk.gray(` - ${f}`)));
|
|
2771
2563
|
}
|
|
2772
2564
|
console.log(chalk.gray(`\n Site URL: ${site_url}\n`));
|
|
2773
2565
|
return;
|
|
2774
2566
|
}
|
|
2775
2567
|
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
console.log(chalk.gray(' Ensuring app metadata is synchronized...'));
|
|
2779
|
-
// Fall through to confirm_site_deploy to ensure DB is updated
|
|
2780
|
-
} else {
|
|
2781
|
-
// 4. Upload files directly to GCS using signed URLs
|
|
2782
|
-
console.log(chalk.gray(`\n Uploading ${filesToUpload.length} files...`));
|
|
2568
|
+
// 4. Upload files directly to GCS using signed URLs
|
|
2569
|
+
console.log(chalk.gray(`\n Uploading ${filesToUpload.length} files...`));
|
|
2783
2570
|
|
|
2784
|
-
|
|
2785
|
-
|
|
2571
|
+
let uploadedCount = 0;
|
|
2572
|
+
let uploadErrors = [];
|
|
2786
2573
|
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2574
|
+
for (const file of filesToUpload) {
|
|
2575
|
+
const signedUrl = signed_urls[file.path];
|
|
2576
|
+
if (!signedUrl) {
|
|
2577
|
+
uploadErrors.push(`No signed URL for: ${file.path}`);
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2793
2580
|
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
const response = await fetch(signedUrl, {
|
|
2811
|
-
method: 'PUT',
|
|
2812
|
-
headers,
|
|
2813
|
-
body: content
|
|
2814
|
-
});
|
|
2581
|
+
try {
|
|
2582
|
+
const content = await fs.readFile(file._absolutePath);
|
|
2583
|
+
|
|
2584
|
+
// Canonical contract (WS-DEPLOY-HARDENING item 4): the server binds Cache-Control
|
|
2585
|
+
// into the v4 signed URL signature (generateSiteUploadUrls/siteAssetCacheControl).
|
|
2586
|
+
// Ferry the exact header bag the server minted — never re-derive the policy
|
|
2587
|
+
// client-side. A mismatched header (including a client-side --no-cache override)
|
|
2588
|
+
// makes GCS return 403 signature-mismatch, so once the server ferries a bound
|
|
2589
|
+
// header for this path, --no-cache is a no-op for it (falls back to legacy
|
|
2590
|
+
// Content-Type-only behavior for paths the server didn't ferry a header for).
|
|
2591
|
+
const ferriedHeaders = upload_headers && upload_headers[file.path];
|
|
2592
|
+
const headers = ferriedHeaders || {
|
|
2593
|
+
'Content-Type': file.content_type,
|
|
2594
|
+
...(options.noCache ? { 'Cache-Control': 'no-cache' } : {})
|
|
2595
|
+
};
|
|
2815
2596
|
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
uploadErrors.push(`Error uploading ${file.path}: ${err.message}`);
|
|
2597
|
+
const response = await fetch(signedUrl, {
|
|
2598
|
+
method: 'PUT',
|
|
2599
|
+
headers,
|
|
2600
|
+
body: content
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
if (!response.ok) {
|
|
2604
|
+
uploadErrors.push(`Failed to upload ${file.path}: ${response.status} ${response.statusText}`);
|
|
2825
2605
|
console.log(chalk.red(` x ${file.path}`));
|
|
2606
|
+
} else {
|
|
2607
|
+
uploadedCount++;
|
|
2608
|
+
console.log(chalk.green(` + ${file.path}`));
|
|
2826
2609
|
}
|
|
2610
|
+
} catch (err) {
|
|
2611
|
+
uploadErrors.push(`Error uploading ${file.path}: ${err.message}`);
|
|
2612
|
+
console.log(chalk.red(` x ${file.path}`));
|
|
2827
2613
|
}
|
|
2614
|
+
}
|
|
2828
2615
|
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
}
|
|
2616
|
+
if (uploadErrors.length > 0) {
|
|
2617
|
+
uploadErrors.forEach(e => console.log(chalk.red(` - ${e}`)));
|
|
2618
|
+
throw new Error(`${uploadErrors.length} of ${filesToUpload.length} files failed to upload. The deploy was not confirmed and nothing was removed from the server.`);
|
|
2833
2619
|
}
|
|
2834
2620
|
|
|
2835
2621
|
// 5. Confirm deployment (ALWAYS call this to ensure DB is updated)
|
|
@@ -2856,6 +2642,12 @@ siteCommand
|
|
|
2856
2642
|
console.log(chalk.green(`\n Site uploaded successfully!\n`));
|
|
2857
2643
|
console.log(chalk.cyan(` URL: ${result.site_url}`));
|
|
2858
2644
|
console.log(chalk.gray(` Files: ${result.files_count}`));
|
|
2645
|
+
const pruned = Array.isArray(result.pruned_files) ? result.pruned_files : null;
|
|
2646
|
+
if (pruned === null) {
|
|
2647
|
+
throw new Error('confirm_site_deploy returned no pruned_files list: this server does not replace the site on deploy, so files from earlier builds may still be served.');
|
|
2648
|
+
}
|
|
2649
|
+
console.log(chalk.gray(` Removed from the server: ${pruned.length}`));
|
|
2650
|
+
pruned.forEach(f => console.log(chalk.gray(` - ${f}`)));
|
|
2859
2651
|
console.log(chalk.gray(` Deployed: ${result.deployed_at}`));
|
|
2860
2652
|
if (result.preview) {
|
|
2861
2653
|
console.log(chalk.yellow(` Mode: PREVIEW`));
|
|
@@ -2863,7 +2655,7 @@ siteCommand
|
|
|
2863
2655
|
|
|
2864
2656
|
if (!options.preview) {
|
|
2865
2657
|
console.log(chalk.gray(`\n For local development, run:`));
|
|
2866
|
-
console.log(chalk.cyan(` descix
|
|
2658
|
+
console.log(chalk.cyan(` descix serve --app ${appId}\n`));
|
|
2867
2659
|
}
|
|
2868
2660
|
console.log('');
|
|
2869
2661
|
|
|
@@ -2873,75 +2665,6 @@ siteCommand
|
|
|
2873
2665
|
}
|
|
2874
2666
|
});
|
|
2875
2667
|
|
|
2876
|
-
// site servelocal - Register local dev server port
|
|
2877
|
-
siteCommand
|
|
2878
|
-
.command('servelocal [port]')
|
|
2879
|
-
.description('Register local dev server port (or "n" to disable)')
|
|
2880
|
-
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
2881
|
-
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
2882
|
-
.action(async (port, options) => {
|
|
2883
|
-
try {
|
|
2884
|
-
const workspaceConfig = await WorkspaceConfig.load();
|
|
2885
|
-
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
2886
|
-
|
|
2887
|
-
const appId = ctx.appId;
|
|
2888
|
-
|
|
2889
|
-
if (!appId) {
|
|
2890
|
-
console.error(chalk.red('\n❌ App ID required.'));
|
|
2891
|
-
console.log(chalk.gray(' Either provide -a flag, or cd into an app directory\n'));
|
|
2892
|
-
process.exit(1);
|
|
2893
|
-
}
|
|
2894
|
-
|
|
2895
|
-
// Handle disable case — setSitePort(appId, null) removes site.port / cleans site.{}
|
|
2896
|
-
if (port === 'n' || port === 'N') {
|
|
2897
|
-
await workspaceConfig.setSitePort(appId, null);
|
|
2898
|
-
console.log(chalk.green(`\n✅ Local site server disabled for ${appId}\n`));
|
|
2899
|
-
return;
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
|
-
// Handle status query (no port argument)
|
|
2903
|
-
if (!port) {
|
|
2904
|
-
const appConfig = workspaceConfig.getAppByAppId(appId);
|
|
2905
|
-
if (!appConfig) {
|
|
2906
|
-
console.error(chalk.red(`\n❌ ${unmappedAppMessage(appId)}\n`));
|
|
2907
|
-
process.exit(1);
|
|
2908
|
-
}
|
|
2909
|
-
// Read site.port from the live env entry (not from the constructed copy)
|
|
2910
|
-
let liveEntry = null;
|
|
2911
|
-
if (workspaceConfig.env?.platform?.appId === appId) {
|
|
2912
|
-
liveEntry = workspaceConfig.env.platform;
|
|
2913
|
-
} else if (Array.isArray(workspaceConfig.env?.products)) {
|
|
2914
|
-
liveEntry = workspaceConfig.env.products.find(p => p.appId === appId) || null;
|
|
2915
|
-
}
|
|
2916
|
-
const currentPort = liveEntry?.site?.port;
|
|
2917
|
-
if (currentPort) {
|
|
2918
|
-
console.log(chalk.cyan(`\n📍 Local site server: port ${currentPort}`));
|
|
2919
|
-
console.log(chalk.gray(` App: ${appId}\n`));
|
|
2920
|
-
} else {
|
|
2921
|
-
console.log(chalk.yellow(`\n⚠️ No local site server configured.`));
|
|
2922
|
-
console.log(chalk.gray(` Usage: descix site servelocal <port>\n`));
|
|
2923
|
-
}
|
|
2924
|
-
return;
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
|
-
const portNum = parseInt(port);
|
|
2928
|
-
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
2929
|
-
console.error(chalk.red('\n❌ Invalid port number.\n'));
|
|
2930
|
-
process.exit(1);
|
|
2931
|
-
}
|
|
2932
|
-
|
|
2933
|
-
// setSitePort mutates the live env entry and saves — no stale-copy problem
|
|
2934
|
-
await workspaceConfig.setSitePort(appId, portNum);
|
|
2935
|
-
console.log(chalk.green(`\n✅ Local site server registered!`));
|
|
2936
|
-
console.log(chalk.cyan(` Port: ${portNum}`));
|
|
2937
|
-
console.log(chalk.gray(` App: ${appId}`));
|
|
2938
|
-
console.log(chalk.gray(`\n The gateway will proxy /p/${appId}/* to localhost:${portNum}\n`));
|
|
2939
|
-
|
|
2940
|
-
} catch (error) {
|
|
2941
|
-
console.error(chalk.red(`\n❌ ${error.message}\n`));
|
|
2942
|
-
process.exit(1);
|
|
2943
|
-
}
|
|
2944
|
-
});
|
|
2945
2668
|
|
|
2946
2669
|
// site list - List deployed files (context-aware)
|
|
2947
2670
|
siteCommand
|
|
@@ -3270,127 +2993,6 @@ microserviceCommand
|
|
|
3270
2993
|
}
|
|
3271
2994
|
});
|
|
3272
2995
|
|
|
3273
|
-
// microservice deploy - Cloud Run deploy (broker-first; no per-app LB for non-core apps)
|
|
3274
|
-
// WS-MCP-SURFACE-SPLIT §4b: this is the REAL Admin/local deploy (Admin SDK) — it stays fully
|
|
3275
|
-
// functional for operator/CI use (EVP/COS deploy runbooks invoke it). It is intentionally NOT
|
|
3276
|
-
// advertised over MCP (not mcp:true), so the PUBLIC surface sees no deploy tool. The public
|
|
3277
|
-
// "microservice deploy" is "coming soon" (a no-op) — satisfied here by absence from the MCP
|
|
3278
|
-
// listing, NOT by neutering this admin path. Do not stub this out.
|
|
3279
|
-
microserviceCommand
|
|
3280
|
-
.command('deploy')
|
|
3281
|
-
.description('[ADMIN/LOCAL] Deploy microservice to Cloud Run (uses deploy-service-env.sh; powch re-provisions platform NEG only). Public MCP deploy is coming soon; this real deploy is Admin/local-only.')
|
|
3282
|
-
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
3283
|
-
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
3284
|
-
.option('--env <env>', 'Target environment: dev|demo|prod')
|
|
3285
|
-
.option('--dry-run', 'Print deploy plan without executing gcloud')
|
|
3286
|
-
.option('--skip-register', 'Skip manifest registration after deploy (service may self-register on boot)')
|
|
3287
|
-
.action(async (options, command) => {
|
|
3288
|
-
try {
|
|
3289
|
-
const apiClient = new DeSciXApiClient();
|
|
3290
|
-
await requireAuth(apiClient);
|
|
3291
|
-
|
|
3292
|
-
const workspaceConfig = await WorkspaceConfig.load();
|
|
3293
|
-
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
3294
|
-
const appId = ctx.appId;
|
|
3295
|
-
|
|
3296
|
-
if (!appId) {
|
|
3297
|
-
console.error(chalk.red('\n❌ App ID required.'));
|
|
3298
|
-
console.log(chalk.gray(' cd into an app directory, or use -a flag\n'));
|
|
3299
|
-
process.exit(1);
|
|
3300
|
-
}
|
|
3301
|
-
|
|
3302
|
-
const parentEnv = command?.parent?.opts?.()?.env;
|
|
3303
|
-
const deployEnv = (options.env || parentEnv || workspaceConfig.env?.environment)?.toLowerCase();
|
|
3304
|
-
if (!deployEnv || !['dev', 'demo', 'prod'].includes(deployEnv)) {
|
|
3305
|
-
console.error(chalk.red('\n❌ --env is required (dev|demo|prod).'));
|
|
3306
|
-
console.log(chalk.gray(' Example: descix --env demo microservice deploy -a powch\n'));
|
|
3307
|
-
process.exit(1);
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
const workspaceRoot = workspaceConfig.getWorkspaceRoot();
|
|
3311
|
-
if (!workspaceRoot) {
|
|
3312
|
-
throw new Error('Could not resolve workspace root (.descix/workspace.json not found)');
|
|
3313
|
-
}
|
|
3314
|
-
|
|
3315
|
-
const deployScript = path.join(
|
|
3316
|
-
workspaceRoot,
|
|
3317
|
-
'DeSciX/DeSciX_Cloud/microservice/admin/scripts/deploy/deploy-service-env.sh'
|
|
3318
|
-
);
|
|
3319
|
-
|
|
3320
|
-
try {
|
|
3321
|
-
await fs.access(deployScript);
|
|
3322
|
-
} catch {
|
|
3323
|
-
throw new Error(`Deploy script not found: ${deployScript}`);
|
|
3324
|
-
}
|
|
3325
|
-
|
|
3326
|
-
console.log(chalk.cyan(`\n🚀 Deploying microservice: ${appId} (${deployEnv})\n`));
|
|
3327
|
-
if (options.dryRun) {
|
|
3328
|
-
console.log(chalk.yellow(' Dry run — gcloud deploy will not execute\n'));
|
|
3329
|
-
}
|
|
3330
|
-
|
|
3331
|
-
const { spawnSync } = await import('child_process');
|
|
3332
|
-
const result = spawnSync('bash', [deployScript, appId, deployEnv], {
|
|
3333
|
-
cwd: path.dirname(deployScript),
|
|
3334
|
-
env: {
|
|
3335
|
-
...process.env,
|
|
3336
|
-
ECHO_MODE: options.dryRun ? 'true' : 'false'
|
|
3337
|
-
},
|
|
3338
|
-
stdio: 'inherit'
|
|
3339
|
-
});
|
|
3340
|
-
|
|
3341
|
-
if (result.status !== 0) {
|
|
3342
|
-
process.exit(result.status || 1);
|
|
3343
|
-
}
|
|
3344
|
-
|
|
3345
|
-
if (options.skipRegister) {
|
|
3346
|
-
console.log(chalk.gray('\n Skipped manifest registration (--skip-register).\n'));
|
|
3347
|
-
return;
|
|
3348
|
-
}
|
|
3349
|
-
|
|
3350
|
-
const microserviceDir = workspaceConfig.getMicroservicePath(appId);
|
|
3351
|
-
if (!microserviceDir) {
|
|
3352
|
-
console.log(chalk.yellow('\n⚠️ Deploy complete. Run descix microservice register to register manifest.\n'));
|
|
3353
|
-
return;
|
|
3354
|
-
}
|
|
3355
|
-
|
|
3356
|
-
const manifestPath = path.join(microserviceDir, 'manifest.json');
|
|
3357
|
-
try {
|
|
3358
|
-
await fs.access(manifestPath);
|
|
3359
|
-
} catch {
|
|
3360
|
-
console.log(chalk.yellow('\n⚠️ Deploy complete. No manifest.json — run descix microservice register when ready.\n'));
|
|
3361
|
-
return;
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
console.log(chalk.cyan('\n📦 Registering microservice manifest...\n'));
|
|
3365
|
-
const registerArgs = [
|
|
3366
|
-
process.argv[1],
|
|
3367
|
-
'microservice',
|
|
3368
|
-
'register',
|
|
3369
|
-
'-m', manifestPath,
|
|
3370
|
-
'-a', appId
|
|
3371
|
-
];
|
|
3372
|
-
if (ctx.communityId) {
|
|
3373
|
-
registerArgs.push('-c', ctx.communityId);
|
|
3374
|
-
}
|
|
3375
|
-
|
|
3376
|
-
const registerResult = spawnSync(process.execPath, registerArgs, {
|
|
3377
|
-
cwd: process.cwd(),
|
|
3378
|
-
stdio: 'inherit',
|
|
3379
|
-
env: process.env
|
|
3380
|
-
});
|
|
3381
|
-
|
|
3382
|
-
if (registerResult.status !== 0) {
|
|
3383
|
-
console.log(chalk.yellow('\n⚠️ Cloud Run deploy succeeded but manifest registration failed.'));
|
|
3384
|
-
console.log(chalk.gray(` Retry: descix microservice register -m ${manifestPath} -a ${appId}\n`));
|
|
3385
|
-
process.exit(registerResult.status || 1);
|
|
3386
|
-
}
|
|
3387
|
-
|
|
3388
|
-
console.log(chalk.green('\n✅ Microservice deploy complete (Cloud Run + manifest registration).\n'));
|
|
3389
|
-
} catch (error) {
|
|
3390
|
-
console.error(chalk.red(`\n❌ Deploy failed: ${error.message}\n`));
|
|
3391
|
-
process.exit(1);
|
|
3392
|
-
}
|
|
3393
|
-
});
|
|
3394
2996
|
|
|
3395
2997
|
// microservice register - Register with gateway
|
|
3396
2998
|
microserviceCommand
|
|
@@ -3734,7 +3336,7 @@ microserviceCommand
|
|
|
3734
3336
|
// microservice reload - Reload manifests (admin)
|
|
3735
3337
|
microserviceCommand
|
|
3736
3338
|
.command('reload')
|
|
3737
|
-
.description('Reload microservice manifests
|
|
3339
|
+
.description('Reload microservice manifests')
|
|
3738
3340
|
.action(async () => {
|
|
3739
3341
|
try {
|
|
3740
3342
|
const apiClient = new DeSciXApiClient();
|
|
@@ -4004,45 +3606,6 @@ program
|
|
|
4004
3606
|
}
|
|
4005
3607
|
});
|
|
4006
3608
|
|
|
4007
|
-
const repCommand = program
|
|
4008
|
-
.command('rep')
|
|
4009
|
-
.description('Reputation operations');
|
|
4010
|
-
|
|
4011
|
-
repCommand
|
|
4012
|
-
.command('grant')
|
|
4013
|
-
.description('Grant REP points to a user (admin/dev only)')
|
|
4014
|
-
.requiredOption('-c, --community <id>', 'Community ID')
|
|
4015
|
-
.requiredOption('-u, --user <id>', 'User ID')
|
|
4016
|
-
.requiredOption('-a, --amount <amount>', 'REP amount to grant')
|
|
4017
|
-
.option('--reason <text>', 'Reason for granting REP')
|
|
4018
|
-
.action(async (options) => {
|
|
4019
|
-
try {
|
|
4020
|
-
const apiClient = new DeSciXApiClient();
|
|
4021
|
-
await requireAuth(apiClient);
|
|
4022
|
-
|
|
4023
|
-
const amount = parseInt(options.amount);
|
|
4024
|
-
if (isNaN(amount) || amount <= 0) {
|
|
4025
|
-
throw new Error('Amount must be a positive integer');
|
|
4026
|
-
}
|
|
4027
|
-
|
|
4028
|
-
const response = await apiClient.invoke('increment_rep', {
|
|
4029
|
-
community_id: options.community,
|
|
4030
|
-
user_id: options.user,
|
|
4031
|
-
amount: amount,
|
|
4032
|
-
reason: options.reason || 'CLI grant'
|
|
4033
|
-
});
|
|
4034
|
-
const result = response.message || response;
|
|
4035
|
-
|
|
4036
|
-
console.log(chalk.green('\n✅ REP granted!\n'));
|
|
4037
|
-
console.log(chalk.cyan(` User: ${options.user}`));
|
|
4038
|
-
console.log(chalk.gray(` Community: ${options.community}`));
|
|
4039
|
-
console.log(chalk.gray(` Amount: +${amount} REP`));
|
|
4040
|
-
console.log(chalk.gray(` New Total: ${result.new_total || result.rep || 'N/A'}\n`));
|
|
4041
|
-
} catch (error) {
|
|
4042
|
-
console.error(chalk.red(error.message));
|
|
4043
|
-
process.exit(1);
|
|
4044
|
-
}
|
|
4045
|
-
});
|
|
4046
3609
|
|
|
4047
3610
|
// ============ Chat Commands ============
|
|
4048
3611
|
|
|
@@ -4693,57 +4256,12 @@ program
|
|
|
4693
4256
|
|
|
4694
4257
|
// ============ MCP Commands ============
|
|
4695
4258
|
|
|
4696
|
-
import * as mcpCommands from '../lib/commands/mcp.js';
|
|
4697
4259
|
import { runClone } from '../lib/commands/clone.js';
|
|
4698
4260
|
|
|
4699
4261
|
const mcpCommand = program
|
|
4700
4262
|
.command('mcp')
|
|
4701
4263
|
.description('MCP server operations');
|
|
4702
4264
|
|
|
4703
|
-
mcpCommand
|
|
4704
|
-
.command('init')
|
|
4705
|
-
.description('Initialize MCP server for this workspace')
|
|
4706
|
-
.action(async () => {
|
|
4707
|
-
try {
|
|
4708
|
-
await mcpCommands.init();
|
|
4709
|
-
} catch (error) {
|
|
4710
|
-
fail(error);
|
|
4711
|
-
}
|
|
4712
|
-
});
|
|
4713
|
-
|
|
4714
|
-
mcpCommand
|
|
4715
|
-
.command('test')
|
|
4716
|
-
.description('Test MCP server with a sample query')
|
|
4717
|
-
.option('-q, --query <text>', 'Test query', 'What is this knowledge base about?')
|
|
4718
|
-
.action(async (options) => {
|
|
4719
|
-
try {
|
|
4720
|
-
await mcpCommands.test(options);
|
|
4721
|
-
} catch (error) {
|
|
4722
|
-
fail(error);
|
|
4723
|
-
}
|
|
4724
|
-
});
|
|
4725
|
-
|
|
4726
|
-
mcpCommand
|
|
4727
|
-
.command('config')
|
|
4728
|
-
.description('Show current workspace configuration')
|
|
4729
|
-
.action(async () => {
|
|
4730
|
-
try {
|
|
4731
|
-
await mcpCommands.config();
|
|
4732
|
-
} catch (error) {
|
|
4733
|
-
fail(error);
|
|
4734
|
-
}
|
|
4735
|
-
});
|
|
4736
|
-
|
|
4737
|
-
mcpCommand
|
|
4738
|
-
.command('quickstart')
|
|
4739
|
-
.description('One-command setup: authenticate, configure, and test')
|
|
4740
|
-
.action(async () => {
|
|
4741
|
-
try {
|
|
4742
|
-
await mcpCommands.quickstart();
|
|
4743
|
-
} catch (error) {
|
|
4744
|
-
fail(error);
|
|
4745
|
-
}
|
|
4746
|
-
});
|
|
4747
4265
|
|
|
4748
4266
|
mcpCommand
|
|
4749
4267
|
.command('execute')
|
|
@@ -4948,55 +4466,6 @@ program
|
|
|
4948
4466
|
}
|
|
4949
4467
|
});
|
|
4950
4468
|
|
|
4951
|
-
// ============ Update Commands (Context-Driven) ============
|
|
4952
|
-
|
|
4953
|
-
const updateCommand = program
|
|
4954
|
-
.command('update')
|
|
4955
|
-
// `update kb` is REMOVED — kb sync has exactly one surface, `descix kb corpus sync`.
|
|
4956
|
-
// `update app` and `update site` are NOT kb-sync surfaces and are untouched, so the
|
|
4957
|
-
// `update` verb itself stays registered. `update all`/`auto` keep doing app and site and
|
|
4958
|
-
// no longer touch the KB; they print where kb sync went rather than failing, because
|
|
4959
|
-
// failing them would delete non-kb functionality this change never targeted.
|
|
4960
|
-
.description('Context-driven resource sync for app and site (auto-detects from workspace). For knowledge bases use `descix kb corpus sync`.')
|
|
4961
|
-
.argument('[type]', 'Update type: app, site, all (auto-detects if not specified)')
|
|
4962
|
-
.option('-c, --community <id>', 'Community ID (optional; app_id sufficient for Unified Registry)')
|
|
4963
|
-
.option('-a, --app <id>', 'App ID (globally unique; required if not in app directory)')
|
|
4964
|
-
.option('--preview', 'Site: Deploy to preview URL')
|
|
4965
|
-
.option('--build <cmd>', 'Site: Run build command first')
|
|
4966
|
-
.option('--full', 'Force full sync (ignore delta)')
|
|
4967
|
-
.option('--port <number>', 'Site: Register local dev server port')
|
|
4968
|
-
.action(async (type, options) => {
|
|
4969
|
-
try {
|
|
4970
|
-
switch (type) {
|
|
4971
|
-
case 'app':
|
|
4972
|
-
await updateCommands.updateApp(options);
|
|
4973
|
-
break;
|
|
4974
|
-
case 'kb':
|
|
4975
|
-
refuseRetiredKbSync('descix update kb', chalk.red);
|
|
4976
|
-
break;
|
|
4977
|
-
case 'site':
|
|
4978
|
-
await updateCommands.updateSite({
|
|
4979
|
-
...options,
|
|
4980
|
-
port: options.port ? parseInt(options.port) : undefined
|
|
4981
|
-
});
|
|
4982
|
-
break;
|
|
4983
|
-
case 'all':
|
|
4984
|
-
console.log(chalk.gray(` (knowledge bases are not included: use \`${CANONICAL_KB_SYNC}\`)`));
|
|
4985
|
-
await updateCommands.updateAll(options);
|
|
4986
|
-
break;
|
|
4987
|
-
default:
|
|
4988
|
-
// Auto-detect. NO blanket "KBs are excluded" line here: auto resolves to exactly
|
|
4989
|
-
// one target, and if that target is the KB the user gets the refusal by name.
|
|
4990
|
-
// Printing the advisory first would announce an exclusion and then refuse for
|
|
4991
|
-
// that exact reason — an advisory that fires on correct behaviour trains people
|
|
4992
|
-
// to ignore advisories.
|
|
4993
|
-
await updateCommands.updateAuto(options);
|
|
4994
|
-
}
|
|
4995
|
-
} catch (error) {
|
|
4996
|
-
fail(error);
|
|
4997
|
-
}
|
|
4998
|
-
});
|
|
4999
|
-
|
|
5000
4469
|
// ============ Local Gateway Command ============
|
|
5001
4470
|
|
|
5002
4471
|
program
|
|
@@ -5065,9 +4534,7 @@ program
|
|
|
5065
4534
|
// resolution — silently, with "Quickstart complete!" and exit 0.
|
|
5066
4535
|
//
|
|
5067
4536
|
// quickstart is an ONBOARDING flow: a user who asked to be set up and already IS set up has
|
|
5068
|
-
// SUCCEEDED, so this SKIPS and REPORTS and continues.
|
|
5069
|
-
// on the same fact because it was asked specifically to CREATE a workspace and cannot honour
|
|
5070
|
-
// that. Different verbs, different contracts, ONE OWNER for the underlying fact.
|
|
4537
|
+
// SUCCEEDED, so this SKIPS and REPORTS and continues.
|
|
5071
4538
|
const existingRoot = await WorkspaceConfig.findWorkspaceRoot(workspaceRoot);
|
|
5072
4539
|
if (existingRoot) {
|
|
5073
4540
|
console.log(chalk.green('✓ Workspace already initialized'));
|
|
@@ -5111,11 +4578,9 @@ program
|
|
|
5111
4578
|
}
|
|
5112
4579
|
|
|
5113
4580
|
// Step 5: Copy SDK assets
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
if (pulled) console.log(chalk.green(' ✓ .descix/sdk-assets/'));
|
|
5118
|
-
} catch { /* setup.js pullSdkAssets may not be exported — skip */ }
|
|
4581
|
+
const { pullSdkAssets } = await import('../lib/sdk-assets.js');
|
|
4582
|
+
await pullSdkAssets(targetRoot);
|
|
4583
|
+
console.log(chalk.green(' ✓ .descix/sdk-assets/'));
|
|
5119
4584
|
|
|
5120
4585
|
// Done
|
|
5121
4586
|
console.log(chalk.green('\n✅ Quickstart complete!\n'));
|
|
@@ -5138,11 +4603,69 @@ program
|
|
|
5138
4603
|
}
|
|
5139
4604
|
});
|
|
5140
4605
|
|
|
4606
|
+
// ============ Public-vs-admin help listing (CEO ruling, see lib/command-visibility.js) ============
|
|
4607
|
+
//
|
|
4608
|
+
// The class comes from the server (`get_command_surface`, guest-allowed) at the moment help is
|
|
4609
|
+
// about to render — never a committed table. Only a help-rendering invocation pays the network
|
|
4610
|
+
// cost: isHelpInvocation() is a fast, offline, syntactic check (no parsing, no I/O) so every
|
|
4611
|
+
// ordinary command keeps running at today's speed. `--admin` / `DESCIX_ADMIN=1` skip this
|
|
4612
|
+
// entirely, which is what "show everything" means — nothing is ever hidden unless this block
|
|
4613
|
+
// explicitly hides it.
|
|
4614
|
+
const rawArgv = process.argv.slice(2);
|
|
4615
|
+
const adminMode = rawArgv.includes('--admin') || process.env.DESCIX_ADMIN === '1';
|
|
4616
|
+
|
|
4617
|
+
/** Read `--flag <value>` or `--flag=<value>` out of raw argv, pre-parse — Commander has not run yet at this point. */
|
|
4618
|
+
function rawFlagValue(argv, flag) {
|
|
4619
|
+
const i = argv.indexOf(flag);
|
|
4620
|
+
if (i !== -1) return argv[i + 1];
|
|
4621
|
+
const joined = argv.find((tok) => tok.startsWith(`${flag}=`));
|
|
4622
|
+
return joined === undefined ? undefined : joined.slice(flag.length + 1);
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
if (!adminMode && isHelpInvocation(program, rawArgv, { valueFlags: ['--env', '--api-url'], booleanFlags: ['--admin'] })) {
|
|
4626
|
+
// Apply --api-url / --env BEFORE constructing the client. The `preAction` hook above does the
|
|
4627
|
+
// same thing but only ever fires before an ACTION runs — Commander short-circuits straight to
|
|
4628
|
+
// help output without one, so for a help-rendering invocation this is the only place that
|
|
4629
|
+
// ever applies these flags. Without it, `descix --api-url https://X --help` silently asked
|
|
4630
|
+
// the DEFAULT origin (not X) whether each command is admin.
|
|
4631
|
+
applyGlobalOriginOverride({ apiUrl: rawFlagValue(rawArgv, '--api-url'), env: rawFlagValue(rawArgv, '--env') });
|
|
4632
|
+
|
|
4633
|
+
// BOUNDED, regardless of what the underlying HTTP client's own timeout is: "help must stay
|
|
4634
|
+
// fast" is a promise about THIS invocation, not about how patient axios feels like being
|
|
4635
|
+
// against an origin that accepts the TCP connection and then never answers.
|
|
4636
|
+
const SURFACE_FETCH_TIMEOUT_MS = 5000;
|
|
4637
|
+
let timeoutHandle;
|
|
4638
|
+
try {
|
|
4639
|
+
const apiClient = new DeSciXApiClient();
|
|
4640
|
+
await apiClient.ensureInitialized();
|
|
4641
|
+
const { commands } = await Promise.race([
|
|
4642
|
+
getCommandSurface(apiClient.baseUrl, apiClient),
|
|
4643
|
+
new Promise((_, reject) => {
|
|
4644
|
+
timeoutHandle = setTimeout(
|
|
4645
|
+
() => reject(new Error(`timed out after ${SURFACE_FETCH_TIMEOUT_MS}ms`)),
|
|
4646
|
+
SURFACE_FETCH_TIMEOUT_MS
|
|
4647
|
+
);
|
|
4648
|
+
timeoutHandle.unref?.(); // never itself the reason the process stays alive
|
|
4649
|
+
}),
|
|
4650
|
+
]);
|
|
4651
|
+
applyVisibility(program, commands);
|
|
4652
|
+
} catch (error) {
|
|
4653
|
+
// FAIL LOUD ABOUT THE DEGRADATION, NEVER SILENTLY: help must still render (everything
|
|
4654
|
+
// visible, the safe default) — offline, an old server without the command, or any other
|
|
4655
|
+
// fetch failure is not a reason to hang or crash `--help`.
|
|
4656
|
+
console.error(chalk.gray(
|
|
4657
|
+
`(admin-only commands could not be filtered from this listing: ${error.message}. Showing everything. Use --admin to always see everything.)`
|
|
4658
|
+
));
|
|
4659
|
+
} finally {
|
|
4660
|
+
clearTimeout(timeoutHandle);
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
|
|
5141
4664
|
// Parse arguments
|
|
5142
|
-
program.
|
|
4665
|
+
await program.parseAsync(process.argv);
|
|
5143
4666
|
|
|
5144
4667
|
// Show help if no command provided
|
|
5145
|
-
if (!
|
|
4668
|
+
if (!rawArgv.length) {
|
|
5146
4669
|
program.outputHelp();
|
|
5147
4670
|
}
|
|
5148
4671
|
|