@descix/cli 1.0.1 → 1.0.3
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/LICENSE.md +671 -0
- package/README.md +43 -4
- package/agent-assets/guides/README.md +2 -2
- package/agent-assets/guides/dynamic-site-pattern.md +29 -14
- package/agent-assets/guides/knowledge-only-pattern.md +5 -5
- package/agent-assets/guides/microservice-pattern.md +48 -4
- package/agent-assets/guides/static-site-pattern.md +8 -0
- package/agent-assets/instructions/app-asset-validation.md +5 -5
- package/agent-assets/instructions/onboarding-flow.md +4 -4
- package/agent-assets/instructions/standard-folder-structure.md +20 -11
- package/agent-assets/reference/app-creation.md +42 -20
- package/agent-assets/reference/architecture.md +24 -22
- package/agent-assets/reference/local-dev.md +230 -67
- package/agent-assets/reference/mcp-integration.md +9 -9
- package/agent-assets/reference/platform-api-consumer.md +244 -0
- package/agent-assets/reference/workspace-config.md +7 -7
- package/agent-assets/rules/descix_mcp.template.mdc +8 -8
- package/bin/descix.js +2559 -1435
- package/bin/kb-deny-lint.mjs +91 -0
- package/bin/mcp-server.js +175 -91
- package/lib/agent-files.js +37 -25
- package/lib/api-client.js +223 -56
- package/lib/auth-guard.js +13 -3
- package/lib/chat-session.js +187 -0
- package/lib/cli-version.js +59 -0
- package/lib/commands/airdrop.js +251 -0
- package/lib/commands/auth.js +254 -72
- package/lib/commands/briefer/errors.js +88 -0
- package/lib/commands/briefer/index.js +319 -0
- package/lib/commands/briefer/sources/canonical-sources.js +129 -0
- package/lib/commands/briefer/sources/entitlements.js +209 -0
- package/lib/commands/briefer/sources/environments.js +205 -0
- package/lib/commands/briefer/sources/identifiers.js +214 -0
- package/lib/commands/briefer/sources/microservice-deploy.js +267 -0
- package/lib/commands/briefer/sources/routing.js +275 -0
- package/lib/commands/briefer/sources/what-is-not.js +223 -0
- package/lib/commands/briefer/stitcher.js +134 -0
- package/lib/commands/briefer/util/source-reader.js +502 -0
- package/lib/commands/config.js +210 -90
- package/lib/commands/corpus.js +978 -0
- package/lib/commands/credits.js +122 -0
- package/lib/commands/health.js +627 -0
- package/lib/commands/kb-list-render.js +32 -0
- package/lib/commands/kb.js +190 -413
- package/lib/commands/mcp.js +5 -5
- package/lib/commands/microservice-restart.js +359 -0
- package/lib/commands/model-config.js +313 -0
- package/lib/commands/retired-kb-sync.js +134 -0
- package/lib/commands/serve.js +26 -1
- package/lib/commands/status.js +23 -51
- package/lib/commands/update.js +36 -107
- package/lib/core/Chunker.js +491 -17
- package/lib/core/CorpusDenyLint.js +326 -0
- package/lib/core/CorpusWalker.js +357 -0
- package/lib/core/Hydrator.js +75 -29
- package/lib/core/ManifestLoader.js +533 -0
- package/lib/core/SiteWalker.js +218 -0
- package/lib/core/Syncer.js +163 -11
- package/lib/diagnostic-log.js +41 -0
- package/lib/environment-report.js +100 -0
- package/lib/global-config.js +4 -1
- package/lib/oauth-client.js +164 -0
- package/lib/origin.js +143 -0
- package/lib/service-api-client.js +95 -0
- package/lib/wallet-environment.js +49 -0
- package/lib/wallet-file.js +16 -2
- package/lib/wizard/setup.js +10 -9
- package/lib/workspace-config.js +380 -235
- package/lib/workspace-identity.js +92 -0
- package/lib/workspace-utils.js +41 -61
- package/package.json +19 -6
- package/templates/agent-claude.md +157 -9
- package/templates/agent-cline.md +68 -8
- package/templates/agent-copilot.md +68 -8
- package/templates/agent-cursor.md +68 -8
- package/templates/alignment-schema.json +20 -0
- package/templates/cloud-brain-hook.sh +20 -0
- package/templates/drive/README.md +1 -1
- package/templates/drive_templates/README.md +2 -2
- package/templates/scaffolds/README.md +6 -10
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +30 -0
- package/templates/scaffolds/microservice/app.js +51 -10
- package/templates/scaffolds/microservice/app.yaml +2 -2
- package/templates/scaffolds/microservice/defaults-config-dev.json +4 -0
- package/templates/scaffolds/microservice/dev-overrides.example.json +5 -0
- package/templates/scaffolds/microservice/manifest.json +0 -1
- package/templates/scaffolds/microservice/package.json +3 -3
- package/templates/scaffolds/microservice/scripts/register.js +56 -41
- package/templates/scaffolds/microservice/services/mcpClient.js +55 -65
- package/templates/scaffolds/microservice/services/utils.js +20 -238
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +24 -0
- package/lib/commands/app-wizard.js +0 -627
- package/lib/commands/folder.js +0 -291
- package/lib/core/PathContext.js +0 -335
- package/templates/scaffolds/microservice/dev-overrides.json.example +0 -5
- package/templates/scaffolds/site/DeSciXAppSDK.js +0 -62
- package/templates/scaffolds/site/README.md +0 -51
- package/templates/scaffolds/site/app.js +0 -22
- package/templates/scaffolds/site/index.html +0 -42
- package/templates/scaffolds/site/styles.css +0 -123
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Command — environment-aware platform health checker.
|
|
3
|
+
*
|
|
4
|
+
* WS-CLI-HEALTH-ENV-PIVOT (2026-05-26):
|
|
5
|
+
* `descix health --env <env>` pivots the probe surface based on env:
|
|
6
|
+
* - dev (default): existing local-port checks (UNCHANGED behavior)
|
|
7
|
+
* - demo: gcloud probes against demo cloud resources + synthetic HTTPS
|
|
8
|
+
* probes against *.demo.descix.net hosts
|
|
9
|
+
* - prod: gcloud probes against prod cloud resources + synthetic HTTPS
|
|
10
|
+
* probes against the three in-scope hosts (descix.net, egpt.descix.net,
|
|
11
|
+
* powch.descix.net)
|
|
12
|
+
*
|
|
13
|
+
* For any non-DEV env, missing gcloud auth is a HARD FAIL with a clear remediation
|
|
14
|
+
* message. There is NO silent fallback to local-port checks for --env=demo|prod.
|
|
15
|
+
*
|
|
16
|
+
* Per feedback_no-hardcoded-fallbacks: no defaults, no silent fallback. Probe
|
|
17
|
+
* surface mismatches surface as failures.
|
|
18
|
+
*
|
|
19
|
+
* See: DeSciX/V2_docs/architecture/platform-must-know-briefer.md §2 (health probes)
|
|
20
|
+
*
|
|
21
|
+
* Output format (all probes):
|
|
22
|
+
* <PASS|FAIL> <name> [<env>] (<probe>)
|
|
23
|
+
*
|
|
24
|
+
* Designed for use by platform-starter, EVPs, and pre-flight automation to verify
|
|
25
|
+
* service availability BEFORE the MCP layer is running.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import chalk from 'chalk';
|
|
29
|
+
import { exec } from 'child_process';
|
|
30
|
+
import { promisify } from 'util';
|
|
31
|
+
import https from 'https';
|
|
32
|
+
import http from 'http';
|
|
33
|
+
import { WorkspaceConfig } from '../workspace-config.js';
|
|
34
|
+
|
|
35
|
+
const execAsync = promisify(exec);
|
|
36
|
+
|
|
37
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const SUPPORTED_ENVS = new Set(['dev', 'demo', 'prod']);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* In-scope PROD hosts per platform-must-know-briefer.md §2 (cutover #1).
|
|
43
|
+
* Probing any other host in PROD is a misconfig and hard-fails.
|
|
44
|
+
*/
|
|
45
|
+
const PROD_INSCOPE_APPS = new Set(['daita', 'egpt', 'powch']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Canonical PROD host map (cutover #1 hosts).
|
|
49
|
+
*/
|
|
50
|
+
const PROD_HOST_BY_APP = {
|
|
51
|
+
daita: 'descix.net',
|
|
52
|
+
egpt: 'egpt.descix.net',
|
|
53
|
+
powch: 'powch.descix.net',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const URL_MAP_NAME = 'descix-discord-app-lb';
|
|
57
|
+
const CLOUD_RUN_REGION = 'us-central1';
|
|
58
|
+
|
|
59
|
+
// ─── Default IO adapters (injectable for tests) ──────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Default exec adapter. Returns { stdout, stderr } as strings.
|
|
63
|
+
* Throws on non-zero exit. Tests replace this with a mock.
|
|
64
|
+
*/
|
|
65
|
+
async function defaultExec(cmd) {
|
|
66
|
+
return execAsync(cmd, { maxBuffer: 16 * 1024 * 1024 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Default HTTPS probe. Resolves with { status, error? } — never rejects.
|
|
71
|
+
* Tests replace this with a mock.
|
|
72
|
+
*/
|
|
73
|
+
function defaultHttpsProbe(url, timeoutMs = 8000) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = new URL(url);
|
|
77
|
+
const client = parsed.protocol === 'http:' ? http : https;
|
|
78
|
+
const req = client.get(url, { timeout: timeoutMs }, (res) => {
|
|
79
|
+
res.resume();
|
|
80
|
+
resolve({ status: res.statusCode });
|
|
81
|
+
});
|
|
82
|
+
req.on('error', (err) => resolve({ status: 0, error: err.message }));
|
|
83
|
+
req.on('timeout', () => { req.destroy(); resolve({ status: 0, error: 'timeout' }); });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
resolve({ status: 0, error: err.message });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── DEV path (UNCHANGED behavior) ───────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build the service registry from workspace.json for DEV port checks.
|
|
94
|
+
* Returns an array of { name, appId, port, type, protocol? }.
|
|
95
|
+
*/
|
|
96
|
+
function buildServiceRegistry(wsConfig) {
|
|
97
|
+
const services = [];
|
|
98
|
+
const env = wsConfig.env || {};
|
|
99
|
+
|
|
100
|
+
if (env.platform) {
|
|
101
|
+
if (env.platform.microservice?.port) {
|
|
102
|
+
services.push({
|
|
103
|
+
name: env.platform.appId || 'platform',
|
|
104
|
+
appId: env.platform.appId,
|
|
105
|
+
port: env.platform.microservice.port,
|
|
106
|
+
type: 'microservice',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (env.platform.site?.port) {
|
|
110
|
+
services.push({
|
|
111
|
+
name: `${env.platform.appId || 'platform'}-site`,
|
|
112
|
+
appId: env.platform.appId,
|
|
113
|
+
port: env.platform.site.port,
|
|
114
|
+
type: 'site',
|
|
115
|
+
protocol: env.platform.site.protocol || 'https',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const product of (env.products || [])) {
|
|
121
|
+
if (product.microservice?.port) {
|
|
122
|
+
services.push({
|
|
123
|
+
name: product.appId,
|
|
124
|
+
appId: product.appId,
|
|
125
|
+
port: product.microservice.port,
|
|
126
|
+
type: 'microservice',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (product.site?.port) {
|
|
130
|
+
services.push({
|
|
131
|
+
name: `${product.appId}-site`,
|
|
132
|
+
appId: product.appId,
|
|
133
|
+
port: product.site.port,
|
|
134
|
+
type: 'site',
|
|
135
|
+
protocol: product.site.protocol || 'https',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return services;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* DEV mode: check if a local port is listening (lsof).
|
|
145
|
+
*/
|
|
146
|
+
async function checkPortDev(port, execFn) {
|
|
147
|
+
try {
|
|
148
|
+
const { stdout } = await execFn(`lsof -i :${port} -sTCP:LISTEN 2>/dev/null | head -3`);
|
|
149
|
+
const listening = stdout.trim().length > 0;
|
|
150
|
+
return { healthy: listening, method: 'port-check', port };
|
|
151
|
+
} catch {
|
|
152
|
+
return { healthy: false, method: 'port-check', port };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── CLOUD path (--env=demo|prod) ────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* HARD-FAIL probe: confirm gcloud has an active authenticated account.
|
|
160
|
+
* Throws a clean Error (caught by runHealth) on failure.
|
|
161
|
+
*/
|
|
162
|
+
async function probeGcloudAuth(execFn) {
|
|
163
|
+
let stdout;
|
|
164
|
+
try {
|
|
165
|
+
({ stdout } = await execFn('gcloud auth list --filter=status:ACTIVE --format=json'));
|
|
166
|
+
} catch (err) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`gcloud CLI is not installed or not on PATH.\n` +
|
|
169
|
+
` Remediation: install https://cloud.google.com/sdk/docs/install ` +
|
|
170
|
+
`and run \`gcloud auth login\`.\n` +
|
|
171
|
+
` Underlying error: ${err.message}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
let parsed;
|
|
175
|
+
try {
|
|
176
|
+
parsed = JSON.parse(stdout);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`gcloud auth list returned non-JSON output. Underlying error: ${err.message}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`No active gcloud account.\n` +
|
|
185
|
+
` Remediation: run \`gcloud auth login\` (and \`gcloud config set project descix\`).`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return { account: parsed[0].account };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the list of apps to probe for a given env.
|
|
193
|
+
* - demo: every product in workspace + the platform app
|
|
194
|
+
* - prod: only descix/egpt/powch (cutover #1 in-scope); raise on out-of-scope filter
|
|
195
|
+
*/
|
|
196
|
+
function resolveAppsForCloudProbe(env, wsConfig, filterAppId) {
|
|
197
|
+
const allApps = [];
|
|
198
|
+
if (wsConfig.env?.platform?.appId) {
|
|
199
|
+
allApps.push(wsConfig.env.platform.appId);
|
|
200
|
+
}
|
|
201
|
+
for (const product of (wsConfig.env?.products || [])) {
|
|
202
|
+
if (product.appId) allApps.push(product.appId);
|
|
203
|
+
}
|
|
204
|
+
// Dedupe
|
|
205
|
+
const unique = [...new Set(allApps)];
|
|
206
|
+
|
|
207
|
+
if (env === 'prod') {
|
|
208
|
+
// PROD scope guard: only in-scope hosts allowed.
|
|
209
|
+
if (filterAppId && !PROD_INSCOPE_APPS.has(filterAppId)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`'${filterAppId}' is not in PROD cutover #1 scope. ` +
|
|
212
|
+
`In-scope apps: ${[...PROD_INSCOPE_APPS].join(', ')}.`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const inScope = unique.filter(a => PROD_INSCOPE_APPS.has(a));
|
|
216
|
+
return filterAppId ? inScope.filter(a => a === filterAppId) : inScope;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// DEMO: all mapped apps (or filter if -m given)
|
|
220
|
+
return filterAppId ? unique.filter(a => a === filterAppId) : unique;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build the HTTPS host for an app in a given env.
|
|
225
|
+
*/
|
|
226
|
+
function hostForApp(env, appId) {
|
|
227
|
+
if (env === 'prod') {
|
|
228
|
+
return PROD_HOST_BY_APP[appId] || `${appId}.descix.net`;
|
|
229
|
+
}
|
|
230
|
+
// demo
|
|
231
|
+
return `${appId}.${env}.descix.net`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Probe the LB URL map. Returns { healthy, command, observed }.
|
|
236
|
+
*/
|
|
237
|
+
async function probeUrlMap(execFn) {
|
|
238
|
+
const cmd = `gcloud compute url-maps describe ${URL_MAP_NAME} --format=json`;
|
|
239
|
+
try {
|
|
240
|
+
const { stdout } = await execFn(cmd);
|
|
241
|
+
const parsed = JSON.parse(stdout);
|
|
242
|
+
const hostRuleCount = Array.isArray(parsed.hostRules) ? parsed.hostRules.length : 0;
|
|
243
|
+
const healthy = hostRuleCount > 0;
|
|
244
|
+
return {
|
|
245
|
+
healthy,
|
|
246
|
+
command: cmd,
|
|
247
|
+
observed: `hostRules=${hostRuleCount}`,
|
|
248
|
+
};
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return {
|
|
251
|
+
healthy: false,
|
|
252
|
+
command: cmd,
|
|
253
|
+
observed: `error: ${err.message.split('\n')[0]}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Probe Cloud Run services for env. Returns { healthy, command, observed, services[] }.
|
|
260
|
+
*/
|
|
261
|
+
async function probeCloudRunServices(env, execFn) {
|
|
262
|
+
// gcloud's --filter regex uses POSIX ERE; ".*-{env}$" matches per-app services named like "daita-demo".
|
|
263
|
+
const filterArg = `metadata.name~^.*-${env}$`;
|
|
264
|
+
const cmd = `gcloud run services list --region=${CLOUD_RUN_REGION} --filter='${filterArg}' --format=json`;
|
|
265
|
+
try {
|
|
266
|
+
const { stdout } = await execFn(cmd);
|
|
267
|
+
const parsed = JSON.parse(stdout);
|
|
268
|
+
const list = Array.isArray(parsed) ? parsed : [];
|
|
269
|
+
// Extract per-service ready state from .status.conditions[type='Ready']
|
|
270
|
+
const services = list.map(svc => {
|
|
271
|
+
const conditions = svc.status?.conditions || [];
|
|
272
|
+
const readyCond = conditions.find(c => c.type === 'Ready');
|
|
273
|
+
const ready = readyCond?.status === 'True';
|
|
274
|
+
return { name: svc.metadata?.name, ready };
|
|
275
|
+
});
|
|
276
|
+
const allReady = services.length > 0 && services.every(s => s.ready);
|
|
277
|
+
return {
|
|
278
|
+
healthy: allReady,
|
|
279
|
+
command: cmd,
|
|
280
|
+
observed: `count=${services.length}, ready=${services.filter(s => s.ready).length}`,
|
|
281
|
+
services,
|
|
282
|
+
};
|
|
283
|
+
} catch (err) {
|
|
284
|
+
return {
|
|
285
|
+
healthy: false,
|
|
286
|
+
command: cmd,
|
|
287
|
+
observed: `error: ${err.message.split('\n')[0]}`,
|
|
288
|
+
services: [],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Probe the central apiFront Cloud Function for env.
|
|
295
|
+
*/
|
|
296
|
+
async function probeApiFrontFunction(env, execFn) {
|
|
297
|
+
const fnName = `apiFront-http-${env}`;
|
|
298
|
+
const cmd = `gcloud functions describe ${fnName} --gen2 --region=${CLOUD_RUN_REGION} --format=json`;
|
|
299
|
+
try {
|
|
300
|
+
const { stdout } = await execFn(cmd);
|
|
301
|
+
const parsed = JSON.parse(stdout);
|
|
302
|
+
const state = parsed.state || parsed.status || 'UNKNOWN';
|
|
303
|
+
return {
|
|
304
|
+
healthy: state === 'ACTIVE',
|
|
305
|
+
command: cmd,
|
|
306
|
+
observed: `state=${state}`,
|
|
307
|
+
};
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return {
|
|
310
|
+
healthy: false,
|
|
311
|
+
command: cmd,
|
|
312
|
+
observed: `error: ${err.message.split('\n')[0]}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Synthetic HTTPS probe for a single app host.
|
|
319
|
+
*/
|
|
320
|
+
async function probeSyntheticHost(env, appId, httpsProbeFn) {
|
|
321
|
+
const host = hostForApp(env, appId);
|
|
322
|
+
const url = `https://${host}/`;
|
|
323
|
+
const result = await httpsProbeFn(url);
|
|
324
|
+
// Healthy = any 2xx/3xx (LB or app responded). 4xx/5xx/timeout = unhealthy.
|
|
325
|
+
const healthy = result.status >= 200 && result.status < 400;
|
|
326
|
+
return {
|
|
327
|
+
healthy,
|
|
328
|
+
command: `curl -sS -o /dev/null -w "%{http_code}" ${url}`,
|
|
329
|
+
observed: result.error ? `error: ${result.error}` : `HTTP ${result.status}`,
|
|
330
|
+
appId,
|
|
331
|
+
url,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─── Output formatting ───────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Render a single probe result line in the canonical format:
|
|
339
|
+
* <PASS|FAIL> <name> [<env>] (<probe>)
|
|
340
|
+
*/
|
|
341
|
+
function formatProbeLine(name, env, healthy, probeDetail) {
|
|
342
|
+
const status = healthy ? chalk.green('PASS') : chalk.red('FAIL');
|
|
343
|
+
return ` ${status} ${name} [${env}] (${probeDetail})`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Main entry ──────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Run health checks. Env-aware:
|
|
350
|
+
* - dev (default): local port checks via lsof
|
|
351
|
+
* - demo|prod: gcloud + synthetic HTTPS probes
|
|
352
|
+
*
|
|
353
|
+
* @param {Object} options
|
|
354
|
+
* @param {string} [options.env='dev'] - Target env (dev|demo|prod)
|
|
355
|
+
* @param {string} [options.microservice] - Filter to a specific appId
|
|
356
|
+
* @param {boolean} [options.json] - Emit JSON
|
|
357
|
+
* @param {Function} [options._exec] - Inject exec adapter (test only)
|
|
358
|
+
* @param {Function} [options._httpsProbe] - Inject HTTPS probe (test only)
|
|
359
|
+
* @param {WorkspaceConfig} [options._wsConfig] - Inject workspace config (test only)
|
|
360
|
+
* @returns {Promise<Object>} structured output
|
|
361
|
+
*/
|
|
362
|
+
export async function runHealth(options = {}) {
|
|
363
|
+
const {
|
|
364
|
+
env: rawEnv,
|
|
365
|
+
microservice: filterName,
|
|
366
|
+
json: jsonOutput,
|
|
367
|
+
_exec,
|
|
368
|
+
_httpsProbe,
|
|
369
|
+
_wsConfig,
|
|
370
|
+
} = options;
|
|
371
|
+
|
|
372
|
+
const env = (rawEnv || 'dev').toLowerCase();
|
|
373
|
+
if (!SUPPORTED_ENVS.has(env)) {
|
|
374
|
+
const msg = `Unknown env: '${rawEnv}'. Supported: dev, demo, prod.`;
|
|
375
|
+
if (jsonOutput) {
|
|
376
|
+
console.log(JSON.stringify({ env: rawEnv, error: msg }, null, 2));
|
|
377
|
+
} else {
|
|
378
|
+
console.error(chalk.red(`\n${msg}\n`));
|
|
379
|
+
}
|
|
380
|
+
// Hard-fail by throwing — caller exits non-zero.
|
|
381
|
+
throw new Error(msg);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const execFn = _exec || defaultExec;
|
|
385
|
+
const httpsProbeFn = _httpsProbe || defaultHttpsProbe;
|
|
386
|
+
|
|
387
|
+
// Load workspace config (skipped if injected).
|
|
388
|
+
let wsConfig = _wsConfig;
|
|
389
|
+
if (!wsConfig) {
|
|
390
|
+
try {
|
|
391
|
+
wsConfig = await WorkspaceConfig.load(process.cwd());
|
|
392
|
+
} catch (err) {
|
|
393
|
+
const msg = `Failed to load workspace config: ${err.message}`;
|
|
394
|
+
if (jsonOutput) {
|
|
395
|
+
console.log(JSON.stringify({ env, error: msg }, null, 2));
|
|
396
|
+
} else {
|
|
397
|
+
console.error(chalk.red(`\n${msg}`));
|
|
398
|
+
console.error(chalk.gray(' Ensure .descix/workspace.json exists and is valid\n'));
|
|
399
|
+
}
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (env === 'dev') {
|
|
405
|
+
return runHealthDev(wsConfig, filterName, jsonOutput, execFn);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return runHealthCloud(env, wsConfig, filterName, jsonOutput, execFn, httpsProbeFn);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* DEV path — unchanged from pre-pivot behavior.
|
|
413
|
+
*/
|
|
414
|
+
async function runHealthDev(wsConfig, filterName, jsonOutput, execFn) {
|
|
415
|
+
let services = buildServiceRegistry(wsConfig);
|
|
416
|
+
|
|
417
|
+
if (filterName) {
|
|
418
|
+
services = services.filter(s => s.appId === filterName || s.name === filterName);
|
|
419
|
+
if (services.length === 0) {
|
|
420
|
+
const msg = `No service found matching "${filterName}" in workspace.json`;
|
|
421
|
+
if (jsonOutput) {
|
|
422
|
+
console.log(JSON.stringify({ env: 'dev', error: msg }, null, 2));
|
|
423
|
+
} else {
|
|
424
|
+
console.error(chalk.red(`\n${msg}`));
|
|
425
|
+
console.error(chalk.gray(' Available services: ' +
|
|
426
|
+
buildServiceRegistry(wsConfig).map(s => s.appId).filter((v, i, a) => a.indexOf(v) === i).join(', ') + '\n'));
|
|
427
|
+
}
|
|
428
|
+
throw new Error(msg);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const results = [];
|
|
433
|
+
const startTime = Date.now();
|
|
434
|
+
for (const svc of services) {
|
|
435
|
+
const check = await checkPortDev(svc.port, execFn);
|
|
436
|
+
results.push({
|
|
437
|
+
service: svc.name,
|
|
438
|
+
appId: svc.appId,
|
|
439
|
+
type: svc.type,
|
|
440
|
+
port: svc.port,
|
|
441
|
+
...check,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
const totalTime = Date.now() - startTime;
|
|
445
|
+
|
|
446
|
+
const output = {
|
|
447
|
+
environment: 'dev',
|
|
448
|
+
timestamp: new Date().toISOString(),
|
|
449
|
+
check_duration_ms: totalTime,
|
|
450
|
+
probe_surface: 'local-port',
|
|
451
|
+
services: results,
|
|
452
|
+
all_healthy: results.every(r => r.healthy),
|
|
453
|
+
summary: {
|
|
454
|
+
total: results.length,
|
|
455
|
+
healthy: results.filter(r => r.healthy).length,
|
|
456
|
+
unhealthy: results.filter(r => !r.healthy).length,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
if (jsonOutput) {
|
|
461
|
+
console.log(JSON.stringify(output, null, 2));
|
|
462
|
+
return output;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
console.log(chalk.cyan(`\nPlatform Health Check (DEV)`));
|
|
466
|
+
console.log(chalk.cyan('='.repeat(50)));
|
|
467
|
+
console.log(chalk.gray(` Timestamp: ${output.timestamp}`));
|
|
468
|
+
console.log(chalk.gray(` Check duration: ${output.check_duration_ms}ms`));
|
|
469
|
+
console.log(chalk.gray(` Probe surface: local-port (lsof)\n`));
|
|
470
|
+
|
|
471
|
+
for (const r of results) {
|
|
472
|
+
const probeDetail = `${r.type}, port:${r.port}`;
|
|
473
|
+
console.log(formatProbeLine(r.service, 'dev', r.healthy, probeDetail));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
console.log();
|
|
477
|
+
if (output.all_healthy) {
|
|
478
|
+
console.log(chalk.green(`All ${output.summary.total} services healthy.\n`));
|
|
479
|
+
} else {
|
|
480
|
+
console.log(chalk.yellow(`${output.summary.healthy}/${output.summary.total} healthy, ${output.summary.unhealthy} unhealthy.\n`));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return output;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* CLOUD path — gcloud + synthetic HTTPS probes for --env=demo|prod.
|
|
488
|
+
*/
|
|
489
|
+
async function runHealthCloud(env, wsConfig, filterName, jsonOutput, execFn, httpsProbeFn) {
|
|
490
|
+
const startTime = Date.now();
|
|
491
|
+
const probes = [];
|
|
492
|
+
|
|
493
|
+
// ── HARD FAIL: gcloud auth must be present ──
|
|
494
|
+
let authInfo;
|
|
495
|
+
try {
|
|
496
|
+
authInfo = await probeGcloudAuth(execFn);
|
|
497
|
+
} catch (err) {
|
|
498
|
+
if (jsonOutput) {
|
|
499
|
+
console.log(JSON.stringify({
|
|
500
|
+
env,
|
|
501
|
+
error: 'gcloud-auth-missing',
|
|
502
|
+
message: err.message,
|
|
503
|
+
}, null, 2));
|
|
504
|
+
} else {
|
|
505
|
+
console.error(chalk.red(`\nHEALTH HARD-FAIL: gcloud auth check failed for --env=${env}.`));
|
|
506
|
+
console.error(chalk.gray(` ${err.message}\n`));
|
|
507
|
+
}
|
|
508
|
+
throw err;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── 1. URL map ──
|
|
512
|
+
const urlMap = await probeUrlMap(execFn);
|
|
513
|
+
probes.push({
|
|
514
|
+
name: URL_MAP_NAME,
|
|
515
|
+
type: 'url-map',
|
|
516
|
+
env,
|
|
517
|
+
healthy: urlMap.healthy,
|
|
518
|
+
command: urlMap.command,
|
|
519
|
+
observed: urlMap.observed,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// ── 2. Cloud Run services ──
|
|
523
|
+
const cloudRun = await probeCloudRunServices(env, execFn);
|
|
524
|
+
probes.push({
|
|
525
|
+
name: `cloud-run-${env}`,
|
|
526
|
+
type: 'cloud-run-list',
|
|
527
|
+
env,
|
|
528
|
+
healthy: cloudRun.healthy,
|
|
529
|
+
command: cloudRun.command,
|
|
530
|
+
observed: cloudRun.observed,
|
|
531
|
+
services: cloudRun.services,
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// ── 3. apiFront Cloud Function ──
|
|
535
|
+
const apiFront = await probeApiFrontFunction(env, execFn);
|
|
536
|
+
probes.push({
|
|
537
|
+
name: `apiFront-http-${env}`,
|
|
538
|
+
type: 'cloud-function',
|
|
539
|
+
env,
|
|
540
|
+
healthy: apiFront.healthy,
|
|
541
|
+
command: apiFront.command,
|
|
542
|
+
observed: apiFront.observed,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// ── 4. Synthetic HTTPS probes per app ──
|
|
546
|
+
let apps;
|
|
547
|
+
try {
|
|
548
|
+
apps = resolveAppsForCloudProbe(env, wsConfig, filterName || null);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
if (jsonOutput) {
|
|
551
|
+
console.log(JSON.stringify({ env, error: err.message }, null, 2));
|
|
552
|
+
} else {
|
|
553
|
+
console.error(chalk.red(`\nHEALTH HARD-FAIL: ${err.message}\n`));
|
|
554
|
+
}
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
for (const appId of apps) {
|
|
559
|
+
const synthetic = await probeSyntheticHost(env, appId, httpsProbeFn);
|
|
560
|
+
probes.push({
|
|
561
|
+
name: appId,
|
|
562
|
+
type: 'synthetic-https',
|
|
563
|
+
env,
|
|
564
|
+
healthy: synthetic.healthy,
|
|
565
|
+
command: synthetic.command,
|
|
566
|
+
observed: synthetic.observed,
|
|
567
|
+
appId,
|
|
568
|
+
url: synthetic.url,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const totalTime = Date.now() - startTime;
|
|
573
|
+
|
|
574
|
+
const output = {
|
|
575
|
+
environment: env,
|
|
576
|
+
timestamp: new Date().toISOString(),
|
|
577
|
+
check_duration_ms: totalTime,
|
|
578
|
+
probe_surface: 'cloud (gcloud + HTTPS)',
|
|
579
|
+
gcloud_account: authInfo.account,
|
|
580
|
+
probes,
|
|
581
|
+
all_healthy: probes.every(p => p.healthy),
|
|
582
|
+
summary: {
|
|
583
|
+
total: probes.length,
|
|
584
|
+
healthy: probes.filter(p => p.healthy).length,
|
|
585
|
+
unhealthy: probes.filter(p => !p.healthy).length,
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
if (jsonOutput) {
|
|
590
|
+
console.log(JSON.stringify(output, null, 2));
|
|
591
|
+
return output;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
console.log(chalk.cyan(`\nPlatform Health Check (${env.toUpperCase()})`));
|
|
595
|
+
console.log(chalk.cyan('='.repeat(50)));
|
|
596
|
+
console.log(chalk.gray(` Timestamp: ${output.timestamp}`));
|
|
597
|
+
console.log(chalk.gray(` Check duration: ${output.check_duration_ms}ms`));
|
|
598
|
+
console.log(chalk.gray(` Probe surface: cloud (gcloud + HTTPS)`));
|
|
599
|
+
console.log(chalk.gray(` gcloud account: ${authInfo.account}\n`));
|
|
600
|
+
|
|
601
|
+
for (const p of probes) {
|
|
602
|
+
const probeDetail = `${p.type}: ${p.observed}`;
|
|
603
|
+
console.log(formatProbeLine(p.name, env, p.healthy, probeDetail));
|
|
604
|
+
// Print the underlying command in gray for transparency.
|
|
605
|
+
console.log(chalk.gray(` $ ${p.command}`));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
console.log();
|
|
609
|
+
if (output.all_healthy) {
|
|
610
|
+
console.log(chalk.green(`All ${output.summary.total} probes healthy.\n`));
|
|
611
|
+
} else {
|
|
612
|
+
console.log(chalk.yellow(`${output.summary.healthy}/${output.summary.total} healthy, ${output.summary.unhealthy} unhealthy.\n`));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return output;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Export probe builders for testing (advanced cases).
|
|
619
|
+
export const __internals = {
|
|
620
|
+
buildServiceRegistry,
|
|
621
|
+
resolveAppsForCloudProbe,
|
|
622
|
+
hostForApp,
|
|
623
|
+
PROD_INSCOPE_APPS,
|
|
624
|
+
SUPPORTED_ENVS,
|
|
625
|
+
URL_MAP_NAME,
|
|
626
|
+
CLOUD_RUN_REGION,
|
|
627
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure render helpers for `descix kb list` — kept out of bin/descix.js so the
|
|
3
|
+
* count-truth display contract is unit-testable (WS-KB-OBSERVABILITY-LIES).
|
|
4
|
+
*
|
|
5
|
+
* The server (`list_knowledge_bases`) tags every row with `count_source`:
|
|
6
|
+
* 'live' — the TRUE Pinecone count (authoritative)
|
|
7
|
+
* 'cached' — the fast cached counter (may under-report un-synced KBs)
|
|
8
|
+
* 'unknown' — a live count was requested but Pinecone enumeration failed
|
|
9
|
+
*
|
|
10
|
+
* An 'unknown' count is rendered '?' — NEVER a fabricated 0 (anti-pattern #7).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The Vectors-column cell for a KB row.
|
|
15
|
+
* @param {Object} kb row from list_knowledge_bases
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function kbVectorCell(kb) {
|
|
19
|
+
if (!kb || kb.count_source === 'unknown' || kb.rag_vector_count == null) return '?';
|
|
20
|
+
return String(kb.rag_vector_count);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The Source-column cell for a KB row. Falls back to the requested mode when the
|
|
25
|
+
* server did not tag the row (older backend), so the column is always populated.
|
|
26
|
+
* @param {Object} kb row from list_knowledge_bases
|
|
27
|
+
* @param {boolean} live whether the CLI requested the live count
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function kbCountSource(kb, live) {
|
|
31
|
+
return (kb && kb.count_source) || (live ? 'live' : 'cached');
|
|
32
|
+
}
|