@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.
Files changed (100) hide show
  1. package/LICENSE.md +671 -0
  2. package/README.md +43 -4
  3. package/agent-assets/guides/README.md +2 -2
  4. package/agent-assets/guides/dynamic-site-pattern.md +29 -14
  5. package/agent-assets/guides/knowledge-only-pattern.md +5 -5
  6. package/agent-assets/guides/microservice-pattern.md +48 -4
  7. package/agent-assets/guides/static-site-pattern.md +8 -0
  8. package/agent-assets/instructions/app-asset-validation.md +5 -5
  9. package/agent-assets/instructions/onboarding-flow.md +4 -4
  10. package/agent-assets/instructions/standard-folder-structure.md +20 -11
  11. package/agent-assets/reference/app-creation.md +42 -20
  12. package/agent-assets/reference/architecture.md +24 -22
  13. package/agent-assets/reference/local-dev.md +230 -67
  14. package/agent-assets/reference/mcp-integration.md +9 -9
  15. package/agent-assets/reference/platform-api-consumer.md +244 -0
  16. package/agent-assets/reference/workspace-config.md +7 -7
  17. package/agent-assets/rules/descix_mcp.template.mdc +8 -8
  18. package/bin/descix.js +2559 -1435
  19. package/bin/kb-deny-lint.mjs +91 -0
  20. package/bin/mcp-server.js +175 -91
  21. package/lib/agent-files.js +37 -25
  22. package/lib/api-client.js +223 -56
  23. package/lib/auth-guard.js +13 -3
  24. package/lib/chat-session.js +187 -0
  25. package/lib/cli-version.js +59 -0
  26. package/lib/commands/airdrop.js +251 -0
  27. package/lib/commands/auth.js +254 -72
  28. package/lib/commands/briefer/errors.js +88 -0
  29. package/lib/commands/briefer/index.js +319 -0
  30. package/lib/commands/briefer/sources/canonical-sources.js +129 -0
  31. package/lib/commands/briefer/sources/entitlements.js +209 -0
  32. package/lib/commands/briefer/sources/environments.js +205 -0
  33. package/lib/commands/briefer/sources/identifiers.js +214 -0
  34. package/lib/commands/briefer/sources/microservice-deploy.js +267 -0
  35. package/lib/commands/briefer/sources/routing.js +275 -0
  36. package/lib/commands/briefer/sources/what-is-not.js +223 -0
  37. package/lib/commands/briefer/stitcher.js +134 -0
  38. package/lib/commands/briefer/util/source-reader.js +502 -0
  39. package/lib/commands/config.js +210 -90
  40. package/lib/commands/corpus.js +978 -0
  41. package/lib/commands/credits.js +122 -0
  42. package/lib/commands/health.js +627 -0
  43. package/lib/commands/kb-list-render.js +32 -0
  44. package/lib/commands/kb.js +190 -413
  45. package/lib/commands/mcp.js +5 -5
  46. package/lib/commands/microservice-restart.js +359 -0
  47. package/lib/commands/model-config.js +313 -0
  48. package/lib/commands/retired-kb-sync.js +134 -0
  49. package/lib/commands/serve.js +26 -1
  50. package/lib/commands/status.js +23 -51
  51. package/lib/commands/update.js +36 -107
  52. package/lib/core/Chunker.js +491 -17
  53. package/lib/core/CorpusDenyLint.js +326 -0
  54. package/lib/core/CorpusWalker.js +357 -0
  55. package/lib/core/Hydrator.js +75 -29
  56. package/lib/core/ManifestLoader.js +533 -0
  57. package/lib/core/SiteWalker.js +218 -0
  58. package/lib/core/Syncer.js +163 -11
  59. package/lib/diagnostic-log.js +41 -0
  60. package/lib/environment-report.js +100 -0
  61. package/lib/global-config.js +4 -1
  62. package/lib/oauth-client.js +164 -0
  63. package/lib/origin.js +143 -0
  64. package/lib/service-api-client.js +95 -0
  65. package/lib/wallet-environment.js +49 -0
  66. package/lib/wallet-file.js +16 -2
  67. package/lib/wizard/setup.js +10 -9
  68. package/lib/workspace-config.js +380 -235
  69. package/lib/workspace-identity.js +92 -0
  70. package/lib/workspace-utils.js +41 -61
  71. package/package.json +19 -6
  72. package/templates/agent-claude.md +157 -9
  73. package/templates/agent-cline.md +68 -8
  74. package/templates/agent-copilot.md +68 -8
  75. package/templates/agent-cursor.md +68 -8
  76. package/templates/alignment-schema.json +20 -0
  77. package/templates/cloud-brain-hook.sh +20 -0
  78. package/templates/drive/README.md +1 -1
  79. package/templates/drive_templates/README.md +2 -2
  80. package/templates/scaffolds/README.md +6 -10
  81. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +30 -0
  82. package/templates/scaffolds/microservice/app.js +51 -10
  83. package/templates/scaffolds/microservice/app.yaml +2 -2
  84. package/templates/scaffolds/microservice/defaults-config-dev.json +4 -0
  85. package/templates/scaffolds/microservice/dev-overrides.example.json +5 -0
  86. package/templates/scaffolds/microservice/manifest.json +0 -1
  87. package/templates/scaffolds/microservice/package.json +3 -3
  88. package/templates/scaffolds/microservice/scripts/register.js +56 -41
  89. package/templates/scaffolds/microservice/services/mcpClient.js +55 -65
  90. package/templates/scaffolds/microservice/services/utils.js +20 -238
  91. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +24 -0
  92. package/lib/commands/app-wizard.js +0 -627
  93. package/lib/commands/folder.js +0 -291
  94. package/lib/core/PathContext.js +0 -335
  95. package/templates/scaffolds/microservice/dev-overrides.json.example +0 -5
  96. package/templates/scaffolds/site/DeSciXAppSDK.js +0 -62
  97. package/templates/scaffolds/site/README.md +0 -51
  98. package/templates/scaffolds/site/app.js +0 -22
  99. package/templates/scaffolds/site/index.html +0 -42
  100. package/templates/scaffolds/site/styles.css +0 -123
@@ -28,12 +28,12 @@ async function loadWorkspaceConfig(workspaceRoot = process.cwd()) {
28
28
  } catch (error) {
29
29
  // Return default config if file doesn't exist
30
30
  return {
31
- primaryCommunity: 'descix',
31
+ primaryCommunity: 'daita',
32
32
  directoryMappings: {},
33
33
  additionalContexts: [],
34
34
  defaultContext: {
35
- communityId: 'descix',
36
- appId: 'agent',
35
+ communityId: 'daita',
36
+ appId: 'daita',
37
37
  kbId: 'General'
38
38
  }
39
39
  };
@@ -105,7 +105,7 @@ export async function init() {
105
105
  });
106
106
 
107
107
  // Use default community for now (can be enhanced with prompts)
108
- const primaryCommunity = 'descix';
108
+ const primaryCommunity = 'daita';
109
109
 
110
110
  // Generate .descix/workspace.json
111
111
  const workspaceRoot = process.cwd();
@@ -193,7 +193,7 @@ export async function test(options = {}) {
193
193
 
194
194
  if (!status.enabled) {
195
195
  spinner.warn(chalk.yellow('RAG not enabled for this knowledge base'));
196
- console.log(chalk.gray('Enable RAG with: descix rag sync -c ' + config.defaultContext.communityId + ' -a ' + config.defaultContext.appId + ' -k ' + config.defaultContext.kbId));
196
+ console.log(chalk.gray('Enable RAG with: descix kb corpus sync -c ' + config.defaultContext.communityId + ' -a ' + config.defaultContext.appId + ' -k ' + config.defaultContext.kbId));
197
197
  throw new Error('RAG not enabled');
198
198
  }
199
199
 
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Microservice Restart Command (DEV only)
3
+ *
4
+ * Eliminates the "hand-kill + nohup-relaunch" tribal-knowledge workflow:
5
+ * 1. Resolve the service from workspace.json `env.platform` / `env.products`
6
+ * (HARD-FAIL on unknown / ambiguous / missing devCommand).
7
+ * 2. Find the running PID via `lsof -iTCP:<port> -sTCP:LISTEN` (or `ps`).
8
+ * 3. SIGTERM → wait up to 5s → SIGKILL.
9
+ * 4. Re-launch the canonical devCommand from `{workspaceRoot}/{localPath}/microservice/`
10
+ * detached via `setsid + nohup`, redirecting stdout/stderr to a canonical log.
11
+ * 5. Poll until the port is bound again; print new PID + log path.
12
+ * 6. Run `descix health -m <name>` and print the result.
13
+ *
14
+ * Scope:
15
+ * - v1: --env=dev only. --env=demo|prod HARD-FAIL with deploy-script pointer.
16
+ *
17
+ * Design rules:
18
+ * - NO hardcoded launch signatures (would mask workspace.json drift).
19
+ * - NO fallback log path defaults — falls back to /tmp ONLY if the canonical
20
+ * dir cannot be created (e.g. read-only FS), and logs the fallback reason.
21
+ * - Mockable: all OS interactions (lsof / ps / kill / spawn / fs / health)
22
+ * pass through injectable `deps`. Tests substitute an in-memory stub.
23
+ */
24
+
25
+ import chalk from 'chalk';
26
+ import { exec, spawn } from 'child_process';
27
+ import { promisify } from 'util';
28
+ import fs from 'fs/promises';
29
+ import path from 'path';
30
+ import { WorkspaceConfig } from '../workspace-config.js';
31
+
32
+ const execAsync = promisify(exec);
33
+
34
+ /** Sleep helper (real-time; tests inject a fake). */
35
+ const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
36
+
37
+ /* ------------------------------------------------------------------ */
38
+ /* Service registry resolution */
39
+ /* ------------------------------------------------------------------ */
40
+
41
+ /**
42
+ * Build the restart-eligible service registry from workspace.json.
43
+ * Only entries with BOTH `microservice.port` and `microservice.devCommand`
44
+ * are restart-eligible (devCommand is the canonical launch signature).
45
+ *
46
+ * @param {WorkspaceConfig} wsConfig
47
+ * @returns {Array<{name, localPath, port, devCommand, microserviceDir}>}
48
+ */
49
+ export function buildRestartRegistry(wsConfig) {
50
+ const env = wsConfig.env || {};
51
+ const root = wsConfig.workspaceRoot;
52
+ const out = [];
53
+
54
+ const collect = (entry) => {
55
+ if (!entry?.appId || !entry?.localPath) return;
56
+ const ms = entry.microservice;
57
+ if (!ms || !ms.port) return;
58
+ out.push({
59
+ name: entry.appId,
60
+ localPath: entry.localPath,
61
+ port: ms.port,
62
+ devCommand: ms.devCommand || null,
63
+ microserviceDir: path.join(root, entry.localPath, 'microservice')
64
+ });
65
+ };
66
+
67
+ collect(env.platform);
68
+ for (const p of (env.products || [])) collect(p);
69
+ return out;
70
+ }
71
+
72
+ /**
73
+ * Resolve a service entry by name. Hard-fail on unknown / ambiguous /
74
+ * missing devCommand. Caller is expected to propagate the thrown Error.
75
+ *
76
+ * @param {WorkspaceConfig} wsConfig
77
+ * @param {string} name
78
+ * @returns {{name, localPath, port, devCommand, microserviceDir}}
79
+ */
80
+ export function resolveService(wsConfig, name) {
81
+ const registry = buildRestartRegistry(wsConfig);
82
+ const matches = registry.filter((s) => s.name === name);
83
+
84
+ if (matches.length === 0) {
85
+ const candidates = registry.map((s) => s.name).join(', ') || '(none)';
86
+ throw new Error(
87
+ `Service '${name}' not found in workspace.json env.platform/env.products.\n` +
88
+ ` Restart-eligible services (with microservice.port + microservice.devCommand): ${candidates}\n` +
89
+ ` Add an entry to .descix/workspace.json or run 'descix microservice list'.`
90
+ );
91
+ }
92
+ if (matches.length > 1) {
93
+ throw new Error(
94
+ `Service name '${name}' is ambiguous — matches ${matches.length} entries: ` +
95
+ matches.map((m) => `${m.name}@${m.localPath}`).join(', ')
96
+ );
97
+ }
98
+
99
+ const svc = matches[0];
100
+ if (!svc.devCommand) {
101
+ throw new Error(
102
+ `Service '${name}' has no microservice.devCommand in workspace.json — ` +
103
+ `cannot determine canonical launch signature. ` +
104
+ `Add 'devCommand' under env.products[].microservice to enable restart.`
105
+ );
106
+ }
107
+ return svc;
108
+ }
109
+
110
+ /* ------------------------------------------------------------------ */
111
+ /* PID discovery */
112
+ /* ------------------------------------------------------------------ */
113
+
114
+ /**
115
+ * Find the PID listening on a TCP port via `lsof`.
116
+ * Returns null when nothing is listening (clean state — restart still
117
+ * proceeds to launch).
118
+ */
119
+ async function findPidByPort(port, deps) {
120
+ const run = deps?.execAsync || execAsync;
121
+ try {
122
+ const { stdout } = await run(`lsof -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null`);
123
+ const lines = stdout.split('\n').map((s) => s.trim()).filter(Boolean);
124
+ if (lines.length === 0) return null;
125
+ // lsof -t emits PIDs one per line. If multiple, take the first listener.
126
+ return parseInt(lines[0], 10) || null;
127
+ } catch {
128
+ // `lsof` exits non-zero when no match — that is the "nothing listening" case.
129
+ return null;
130
+ }
131
+ }
132
+
133
+ /* ------------------------------------------------------------------ */
134
+ /* Kill */
135
+ /* ------------------------------------------------------------------ */
136
+
137
+ /**
138
+ * SIGTERM the PID, wait up to `gracePeriodMs`, then SIGKILL if still alive.
139
+ * Considers PID "gone" when `process.kill(pid, 0)` throws ESRCH.
140
+ */
141
+ export async function killPid(pid, opts = {}) {
142
+ const deps = opts.deps || {};
143
+ const gracePeriodMs = opts.gracePeriodMs ?? 5000;
144
+ const pollMs = opts.pollMs ?? 250;
145
+ const sleep = deps.sleep || realSleep;
146
+ const procKill = deps.processKill || ((p, sig) => process.kill(p, sig));
147
+
148
+ procKill(pid, 'SIGTERM');
149
+
150
+ const deadline = Date.now() + gracePeriodMs;
151
+ while (Date.now() < deadline) {
152
+ try {
153
+ procKill(pid, 0);
154
+ } catch (err) {
155
+ if (err.code === 'ESRCH') return { method: 'SIGTERM', pid };
156
+ // EPERM = process exists but we cannot signal it — treat as alive.
157
+ }
158
+ await sleep(pollMs);
159
+ }
160
+
161
+ // Still alive — SIGKILL.
162
+ try {
163
+ procKill(pid, 'SIGKILL');
164
+ } catch {
165
+ /* already dead */
166
+ }
167
+ return { method: 'SIGKILL', pid };
168
+ }
169
+
170
+ /* ------------------------------------------------------------------ */
171
+ /* Launch */
172
+ /* ------------------------------------------------------------------ */
173
+
174
+ /**
175
+ * Resolve the canonical log path for a restart.
176
+ * apps/<community>/<app>/.descix/logs/<service>-<ts>.log
177
+ * Falls back to /tmp/<service>-restart-<ts>.log if the canonical dir
178
+ * cannot be created.
179
+ */
180
+ export async function resolveLogPath(wsConfig, serviceName, opts = {}) {
181
+ const deps = opts.deps || {};
182
+ const fsImpl = deps.fs || fs;
183
+ const ts = (opts.timestamp || new Date().toISOString()).replace(/[:.]/g, '-');
184
+ const root = wsConfig.workspaceRoot;
185
+ const community = wsConfig.env?.communityId || 'unkamon';
186
+ const canonicalDir = path.join(root, 'apps', community, serviceName, '.descix', 'logs');
187
+ try {
188
+ await fsImpl.mkdir(canonicalDir, { recursive: true });
189
+ return {
190
+ path: path.join(canonicalDir, `${serviceName}-${ts}.log`),
191
+ fallback: false
192
+ };
193
+ } catch (err) {
194
+ return {
195
+ path: path.join('/tmp', `${serviceName}-restart-${ts}.log`),
196
+ fallback: true,
197
+ fallbackReason: err.message
198
+ };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Launch the canonical devCommand detached, with stdout/stderr appended
204
+ * to `logPath`. Returns the spawned PID.
205
+ *
206
+ * NOTE: We deliberately invoke through `bash -lc` so devCommands like
207
+ * `npm run dev:service` or `nodemon app.js` resolve PATH the same way an
208
+ * interactive shell would.
209
+ */
210
+ export async function launchService(svc, logPath, opts = {}) {
211
+ const deps = opts.deps || {};
212
+ const fsImpl = deps.fs || fs;
213
+ const spawnImpl = deps.spawn || spawn;
214
+
215
+ // Open the log file in append mode for redirection.
216
+ const logFd = await fsImpl.open(logPath, 'a');
217
+ const stdout = logFd.fd ?? logFd; // node fs Promise FileHandle vs raw fd
218
+ // For real fs/promises, we get a FileHandle; need .fd. For tests, accept raw.
219
+ const stdoutFd = typeof stdout === 'number' ? stdout : logFd.fd;
220
+
221
+ const child = spawnImpl('bash', ['-lc', svc.devCommand], {
222
+ cwd: svc.microserviceDir,
223
+ detached: true,
224
+ stdio: ['ignore', stdoutFd, stdoutFd],
225
+ env: { ...process.env }
226
+ });
227
+
228
+ // Detach so the child survives the CLI exit.
229
+ if (typeof child.unref === 'function') child.unref();
230
+
231
+ // Close our FD in the parent — the child has its own dup'd handle.
232
+ if (typeof logFd.close === 'function') {
233
+ try { await logFd.close(); } catch { /* fd already adopted */ }
234
+ }
235
+
236
+ return child.pid;
237
+ }
238
+
239
+ /* ------------------------------------------------------------------ */
240
+ /* Port-bound poll */
241
+ /* ------------------------------------------------------------------ */
242
+
243
+ /**
244
+ * Wait until something is listening on `port`, polling every `pollMs`
245
+ * up to `timeoutMs`. Returns the new PID, or null on timeout.
246
+ */
247
+ export async function waitForPort(port, opts = {}) {
248
+ const deps = opts.deps || {};
249
+ const sleep = deps.sleep || realSleep;
250
+ const timeoutMs = opts.timeoutMs ?? 15000;
251
+ const pollMs = opts.pollMs ?? 500;
252
+ const deadline = Date.now() + timeoutMs;
253
+
254
+ while (Date.now() < deadline) {
255
+ const pid = await findPidByPort(port, deps);
256
+ if (pid) return pid;
257
+ await sleep(pollMs);
258
+ }
259
+ return null;
260
+ }
261
+
262
+ /* ------------------------------------------------------------------ */
263
+ /* Top-level orchestrator */
264
+ /* ------------------------------------------------------------------ */
265
+
266
+ /**
267
+ * Restart a microservice. Returns a structured result for tests + CLI.
268
+ *
269
+ * @param {Object} args
270
+ * @param {string} args.name Service name (e.g. 'beast').
271
+ * @param {string} args.env 'dev' | 'demo' | 'prod'.
272
+ * @param {Object} [args.deps] Dependency-injection overrides for tests.
273
+ * @param {Object} [args.workspaceConfig] Pre-loaded config (test override).
274
+ */
275
+ export async function restartMicroservice(args) {
276
+ const { name, env, deps = {} } = args;
277
+ const log = deps.log || ((msg) => console.log(msg));
278
+
279
+ // Env gate — demo/prod out of scope for v1.
280
+ if (env && env !== 'dev') {
281
+ throw new Error(
282
+ `DEMO and PROD restarts are out of scope for 'descix microservice restart'; ` +
283
+ `use 'gcloud run services update ${name}-${env}' or the deploy script.`
284
+ );
285
+ }
286
+
287
+ const wsConfig = args.workspaceConfig || await WorkspaceConfig.load(process.cwd());
288
+ const svc = resolveService(wsConfig, name);
289
+
290
+ log(chalk.cyan(`\nRestarting microservice '${svc.name}' (DEV)`));
291
+ log(chalk.gray(` cwd: ${svc.microserviceDir}`));
292
+ log(chalk.gray(` command: ${svc.devCommand}`));
293
+ log(chalk.gray(` port: ${svc.port}\n`));
294
+
295
+ // Step 1: locate running PID.
296
+ const oldPid = await findPidByPort(svc.port, deps);
297
+ if (oldPid) {
298
+ log(chalk.gray(` Found PID ${oldPid} listening on :${svc.port}`));
299
+ const killResult = await killPid(oldPid, { deps });
300
+ log(chalk.green(` Killed PID ${oldPid} via ${killResult.method}`));
301
+ } else {
302
+ log(chalk.yellow(` No PID listening on :${svc.port} — launching fresh`));
303
+ }
304
+
305
+ // Step 2: resolve log path.
306
+ const logRes = await resolveLogPath(wsConfig, svc.name, { deps });
307
+ if (logRes.fallback) {
308
+ log(chalk.yellow(` Canonical log dir unavailable (${logRes.fallbackReason}); using ${logRes.path}`));
309
+ } else {
310
+ log(chalk.gray(` Log: ${logRes.path}`));
311
+ }
312
+
313
+ // Step 3: launch.
314
+ const newPid = await launchService(svc, logRes.path, { deps });
315
+ log(chalk.gray(` Spawned wrapper PID ${newPid}`));
316
+
317
+ // Step 4: wait for port-bound confirmation.
318
+ const boundPid = await waitForPort(svc.port, { deps, ...(args.waitOptions || {}) });
319
+ if (!boundPid) {
320
+ log(chalk.red(`\nFAIL: '${svc.name}' did not bind to port ${svc.port} within timeout`));
321
+ log(chalk.gray(` Inspect log: ${logRes.path}`));
322
+ throw new Error(`microservice restart failed: ${svc.name} did not bind to :${svc.port}`);
323
+ }
324
+
325
+ log(chalk.green(`\nPASS: '${svc.name}' restarted`));
326
+ log(chalk.gray(` New PID: ${boundPid}`));
327
+ log(chalk.gray(` Log: ${logRes.path}`));
328
+
329
+ // Step 5: health check via the existing `descix health` path.
330
+ // Silence runHealth's own console output — we surface a single-line summary.
331
+ if (deps.runHealth) {
332
+ const origLog = console.log;
333
+ console.log = () => {};
334
+ try {
335
+ const h = await deps.runHealth({ microservice: svc.name, json: true });
336
+ console.log = origLog;
337
+ const svcResult = (h?.services || []).find((s) => s.appId === svc.name);
338
+ if (svcResult?.healthy) {
339
+ log(chalk.green(` Health: PASS`));
340
+ } else {
341
+ log(chalk.yellow(` Health: ${svcResult ? 'FAIL' : 'unknown'}`));
342
+ }
343
+ } catch (err) {
344
+ console.log = origLog;
345
+ log(chalk.yellow(` Health check skipped: ${err.message}`));
346
+ }
347
+ }
348
+
349
+ return {
350
+ name: svc.name,
351
+ port: svc.port,
352
+ oldPid,
353
+ newPid: boundPid,
354
+ logPath: logRes.path,
355
+ logFallback: logRes.fallback,
356
+ devCommand: svc.devCommand,
357
+ microserviceDir: svc.microserviceDir
358
+ };
359
+ }