@descix/cli 1.0.5 → 1.0.7
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 +130 -34
- package/agent-assets/guides/dynamic-site-pattern.md +10 -14
- package/agent-assets/guides/knowledge-only-pattern.md +11 -15
- package/agent-assets/guides/microservice-pattern.md +12 -14
- package/agent-assets/guides/static-site-pattern.md +12 -15
- package/agent-assets/instructions/onboarding-flow.md +4 -4
- package/agent-assets/reference/app-creation.md +36 -101
- package/agent-assets/reference/architecture.md +24 -27
- package/agent-assets/reference/local-dev.md +0 -4
- package/agent-assets/reference/mcp-integration.md +14 -22
- package/agent-assets/reference/workspace-config.md +97 -340
- package/agent-assets/rules/descix_mcp.template.mdc +1 -1
- package/bin/descix.js +159 -98
- package/lib/api-client.js +30 -22
- package/lib/commands/airdrop.js +10 -0
- package/lib/commands/auth.js +33 -3
- package/lib/commands/briefer/index.js +8 -5
- package/lib/commands/config.js +13 -31
- package/lib/commands/corpus.js +161 -54
- package/lib/commands/health.js +116 -21
- package/lib/commands/init.js +25 -48
- package/lib/commands/kb.js +14 -4
- package/lib/commands/update.js +0 -1
- package/lib/core/Chunker.js +1 -1
- package/lib/core/CorpusWalker.js +238 -47
- package/lib/core/Hydrator.js +88 -11
- package/lib/core/ManifestLoader.js +68 -0
- package/lib/core/Syncer.js +147 -46
- package/lib/interactive.js +158 -0
- package/lib/origin.js +3 -3
- package/lib/wallet-file.js +36 -37
- package/lib/wizard/setup.js +104 -22
- package/lib/workspace-config.js +325 -167
- package/lib/workspace-identity.js +0 -1
- package/package.json +3 -3
- package/templates/agent-claude.md +0 -3
- package/templates/agent-cline.md +0 -3
- package/templates/agent-copilot.md +0 -3
- package/templates/agent-cursor.md +0 -3
- package/templates/drive/app/kb/General/README.md +1 -1
- package/templates/drive_templates/agent_app_template/kb/General/README.md +1 -1
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +3 -3
- package/templates/scaffolds/microservice/defaults-config.json +3 -1
- package/templates/scaffolds/microservice/manifest.json +9 -5
- package/templates/scaffolds/microservice/package.json +1 -1
- package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +2 -2
package/lib/commands/health.js
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* WS-CLI-HEALTH-ENV-PIVOT (2026-05-26):
|
|
5
5
|
* `descix health --env <env>` pivots the probe surface based on env:
|
|
6
|
-
* - dev (default):
|
|
6
|
+
* - dev (default): BOUND (lsof) AND SERVES (a route probe) per service, plus a gateway
|
|
7
|
+
* row from env.gateway.port. A bound port is not a health check: measured
|
|
8
|
+
* 2026-09-16, a port answering 500 to every request reported PASS, and the
|
|
9
|
+
* gateway had no row. A microservice's health path is read from its OWN served
|
|
10
|
+
* manifest (`GET /manifest` → service.healthEndpoint — cloud declares
|
|
11
|
+
* /api/health, powch /health), never assumed.
|
|
7
12
|
* - demo: gcloud probes against demo cloud resources + synthetic HTTPS
|
|
8
13
|
* probes against *.demo.descix.net hosts
|
|
9
14
|
* - prod: gcloud probes against prod cloud resources + synthetic HTTPS
|
|
@@ -28,6 +33,7 @@
|
|
|
28
33
|
import chalk from 'chalk';
|
|
29
34
|
import { exec } from 'child_process';
|
|
30
35
|
import { promisify } from 'util';
|
|
36
|
+
import { localUpstreamOrigin, resolveGatewayPort } from '@descix/app-sdk/dev';
|
|
31
37
|
import https from 'https';
|
|
32
38
|
import http from 'http';
|
|
33
39
|
import { WorkspaceConfig } from '../workspace-config.js';
|
|
@@ -67,17 +73,22 @@ async function defaultExec(cmd) {
|
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
/**
|
|
70
|
-
* Default
|
|
71
|
-
*
|
|
76
|
+
* Default HTTP(S) probe. Resolves with { status, body, error? } — never rejects.
|
|
77
|
+
* `insecure` tolerates the workspace's self-signed dev certs and is set ONLY by the DEV
|
|
78
|
+
* path; cloud probes keep TLS validation. Tests replace this with a mock.
|
|
72
79
|
*/
|
|
73
|
-
function defaultHttpsProbe(url, timeoutMs = 8000) {
|
|
80
|
+
function defaultHttpsProbe(url, timeoutMs = 8000, { insecure = false } = {}) {
|
|
74
81
|
return new Promise((resolve) => {
|
|
75
82
|
try {
|
|
76
83
|
const parsed = new URL(url);
|
|
77
84
|
const client = parsed.protocol === 'http:' ? http : https;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
const opts = { timeout: timeoutMs };
|
|
86
|
+
if (insecure && parsed.protocol === 'https:') opts.rejectUnauthorized = false;
|
|
87
|
+
const req = client.get(url, opts, (res) => {
|
|
88
|
+
let body = '';
|
|
89
|
+
res.setEncoding('utf8');
|
|
90
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
91
|
+
res.on('end', () => resolve({ status: res.statusCode, body }));
|
|
81
92
|
});
|
|
82
93
|
req.on('error', (err) => resolve({ status: 0, error: err.message }));
|
|
83
94
|
req.on('timeout', () => { req.destroy(); resolve({ status: 0, error: 'timeout' }); });
|
|
@@ -90,29 +101,34 @@ function defaultHttpsProbe(url, timeoutMs = 8000) {
|
|
|
90
101
|
// ─── DEV path (UNCHANGED behavior) ───────────────────────────────────────────
|
|
91
102
|
|
|
92
103
|
/**
|
|
93
|
-
* Build the service registry from workspace.json for DEV
|
|
94
|
-
* Returns an array of { name, appId, port, type,
|
|
104
|
+
* Build the service registry from workspace.json for DEV checks.
|
|
105
|
+
* Returns an array of { name, appId, port, type, origin }. `origin` — scheme included — comes
|
|
106
|
+
* from the ONE owner of a local upstream's origin (@descix/app-sdk localOrigin), never a
|
|
107
|
+
* protocol default re-derived here. The gateway is a row too: it is the process a local platform
|
|
108
|
+
* developer most needs to see, and it used to be structurally invisible to this command.
|
|
95
109
|
*/
|
|
96
110
|
function buildServiceRegistry(wsConfig) {
|
|
97
111
|
const services = [];
|
|
98
112
|
const env = wsConfig.env || {};
|
|
99
113
|
|
|
100
114
|
if (env.platform) {
|
|
115
|
+
const name = env.platform.appId || 'platform';
|
|
101
116
|
if (env.platform.microservice?.port) {
|
|
102
117
|
services.push({
|
|
103
|
-
name
|
|
118
|
+
name,
|
|
104
119
|
appId: env.platform.appId,
|
|
105
120
|
port: env.platform.microservice.port,
|
|
106
121
|
type: 'microservice',
|
|
122
|
+
origin: localUpstreamOrigin(env.platform.microservice, 'env.platform.microservice'),
|
|
107
123
|
});
|
|
108
124
|
}
|
|
109
125
|
if (env.platform.site?.port) {
|
|
110
126
|
services.push({
|
|
111
|
-
name: `${
|
|
127
|
+
name: `${name}-site`,
|
|
112
128
|
appId: env.platform.appId,
|
|
113
129
|
port: env.platform.site.port,
|
|
114
130
|
type: 'site',
|
|
115
|
-
|
|
131
|
+
origin: localUpstreamOrigin(env.platform.site, 'env.platform.site'),
|
|
116
132
|
});
|
|
117
133
|
}
|
|
118
134
|
}
|
|
@@ -124,6 +140,7 @@ function buildServiceRegistry(wsConfig) {
|
|
|
124
140
|
appId: product.appId,
|
|
125
141
|
port: product.microservice.port,
|
|
126
142
|
type: 'microservice',
|
|
143
|
+
origin: localUpstreamOrigin(product.microservice, `env.products[${product.appId}].microservice`),
|
|
127
144
|
});
|
|
128
145
|
}
|
|
129
146
|
if (product.site?.port) {
|
|
@@ -132,14 +149,82 @@ function buildServiceRegistry(wsConfig) {
|
|
|
132
149
|
appId: product.appId,
|
|
133
150
|
port: product.site.port,
|
|
134
151
|
type: 'site',
|
|
135
|
-
|
|
152
|
+
origin: localUpstreamOrigin(product.site, `env.products[${product.appId}].site`),
|
|
136
153
|
});
|
|
137
154
|
}
|
|
138
155
|
}
|
|
139
156
|
|
|
157
|
+
// The gateway — port from its ONE owner (resolveGatewayPort: --port > env.gateway.port >
|
|
158
|
+
// the SDK default), origin from the same owner as every other row.
|
|
159
|
+
const { port: gatewayPort } = resolveGatewayPort(wsConfig);
|
|
160
|
+
services.push({
|
|
161
|
+
name: 'gateway',
|
|
162
|
+
appId: null,
|
|
163
|
+
port: gatewayPort,
|
|
164
|
+
type: 'gateway',
|
|
165
|
+
origin: localUpstreamOrigin({ port: gatewayPort }, 'env.gateway'),
|
|
166
|
+
});
|
|
167
|
+
|
|
140
168
|
return services;
|
|
141
169
|
}
|
|
142
170
|
|
|
171
|
+
/** What a DEV run reads, and what it does not — printed on green AND red, and carried in JSON. */
|
|
172
|
+
const DEV_PROBE_BOUNDARY =
|
|
173
|
+
'Probes: bound = lsof on the port; serves = microservices: GET /manifest then GET its ' +
|
|
174
|
+
'service.healthEndpoint (2xx); sites: GET / (2xx); gateway: GET /__descix/app-binding.json ' +
|
|
175
|
+
'(a JSON binding, not HTML). Not probed: authentication, any command, credits, the mesh ' +
|
|
176
|
+
'registration, or the deployed cloud — use --env demo|prod for that.';
|
|
177
|
+
|
|
178
|
+
/** A JSON body, or null when it is not one (a vite dev server answers 200 HTML for any path). */
|
|
179
|
+
function parseJsonBody(body) {
|
|
180
|
+
try {
|
|
181
|
+
const v = JSON.parse(body);
|
|
182
|
+
return v && typeof v === 'object' ? v : null;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const is2xx = (status) => typeof status === 'number' && status >= 200 && status < 300;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* DEV mode: does the bound process actually SERVE? Resolves { serves, probe } where `probe` says
|
|
192
|
+
* exactly what was asked and what came back, so a FAIL line names its own reason.
|
|
193
|
+
*/
|
|
194
|
+
async function checkServesDev(svc, httpsProbeFn) {
|
|
195
|
+
const opts = { insecure: true };
|
|
196
|
+
try {
|
|
197
|
+
if (svc.type === 'microservice') {
|
|
198
|
+
const m = await httpsProbeFn(`${svc.origin}/manifest`, 8000, opts);
|
|
199
|
+
if (!is2xx(m.status)) {
|
|
200
|
+
return { serves: false, probe: `GET /manifest -> ${m.status || m.error || 'unreachable'}` };
|
|
201
|
+
}
|
|
202
|
+
const manifest = parseJsonBody(m.body);
|
|
203
|
+
const healthEndpoint = manifest?.service?.healthEndpoint;
|
|
204
|
+
if (typeof healthEndpoint !== 'string' || healthEndpoint === '') {
|
|
205
|
+
return { serves: false, probe: 'GET /manifest -> 200 but it names no service.healthEndpoint' };
|
|
206
|
+
}
|
|
207
|
+
const h = await httpsProbeFn(`${svc.origin}${healthEndpoint}`, 8000, opts);
|
|
208
|
+
return { serves: is2xx(h.status), probe: `GET ${healthEndpoint} -> ${h.status || h.error || 'unreachable'}` };
|
|
209
|
+
}
|
|
210
|
+
if (svc.type === 'gateway') {
|
|
211
|
+
const b = await httpsProbeFn(`${svc.origin}/__descix/app-binding.json`, 8000, opts);
|
|
212
|
+
const binding = is2xx(b.status) ? parseJsonBody(b.body) : null;
|
|
213
|
+
const isBinding = !!binding && typeof binding.mode === 'string';
|
|
214
|
+
return {
|
|
215
|
+
serves: isBinding,
|
|
216
|
+
probe: `GET /__descix/app-binding.json -> ${b.status || b.error || 'unreachable'}` +
|
|
217
|
+
(is2xx(b.status) && !isBinding ? ' (not a binding: something else is on this port)' : ''),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// site
|
|
221
|
+
const s = await httpsProbeFn(`${svc.origin}/`, 8000, opts);
|
|
222
|
+
return { serves: is2xx(s.status) && !!s.body, probe: `GET / -> ${s.status || s.error || 'unreachable'}` };
|
|
223
|
+
} catch (err) {
|
|
224
|
+
return { serves: false, probe: `probe threw: ${err.message}` };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
143
228
|
/**
|
|
144
229
|
* DEV mode: check if a local port is listening (lsof).
|
|
145
230
|
*/
|
|
@@ -402,16 +487,16 @@ export async function runHealth(options = {}) {
|
|
|
402
487
|
}
|
|
403
488
|
|
|
404
489
|
if (env === 'dev') {
|
|
405
|
-
return runHealthDev(wsConfig, filterName, jsonOutput, execFn);
|
|
490
|
+
return runHealthDev(wsConfig, filterName, jsonOutput, execFn, httpsProbeFn);
|
|
406
491
|
}
|
|
407
492
|
|
|
408
493
|
return runHealthCloud(env, wsConfig, filterName, jsonOutput, execFn, httpsProbeFn);
|
|
409
494
|
}
|
|
410
495
|
|
|
411
496
|
/**
|
|
412
|
-
* DEV path —
|
|
497
|
+
* DEV path — bound AND serves, per service, plus the gateway.
|
|
413
498
|
*/
|
|
414
|
-
async function runHealthDev(wsConfig, filterName, jsonOutput, execFn) {
|
|
499
|
+
async function runHealthDev(wsConfig, filterName, jsonOutput, execFn, httpsProbeFn) {
|
|
415
500
|
let services = buildServiceRegistry(wsConfig);
|
|
416
501
|
|
|
417
502
|
if (filterName) {
|
|
@@ -432,13 +517,20 @@ async function runHealthDev(wsConfig, filterName, jsonOutput, execFn) {
|
|
|
432
517
|
const results = [];
|
|
433
518
|
const startTime = Date.now();
|
|
434
519
|
for (const svc of services) {
|
|
435
|
-
const
|
|
520
|
+
const bound = (await checkPortDev(svc.port, execFn)).healthy;
|
|
521
|
+
const { serves, probe } = bound
|
|
522
|
+
? await checkServesDev(svc, httpsProbeFn)
|
|
523
|
+
: { serves: false, probe: 'not bound' };
|
|
436
524
|
results.push({
|
|
437
525
|
service: svc.name,
|
|
438
526
|
appId: svc.appId,
|
|
439
527
|
type: svc.type,
|
|
440
528
|
port: svc.port,
|
|
441
|
-
|
|
529
|
+
bound,
|
|
530
|
+
serves,
|
|
531
|
+
serves_probe: probe,
|
|
532
|
+
healthy: bound && serves,
|
|
533
|
+
method: 'port+route',
|
|
442
534
|
});
|
|
443
535
|
}
|
|
444
536
|
const totalTime = Date.now() - startTime;
|
|
@@ -447,7 +539,8 @@ async function runHealthDev(wsConfig, filterName, jsonOutput, execFn) {
|
|
|
447
539
|
environment: 'dev',
|
|
448
540
|
timestamp: new Date().toISOString(),
|
|
449
541
|
check_duration_ms: totalTime,
|
|
450
|
-
probe_surface: 'local-port',
|
|
542
|
+
probe_surface: 'local-port+route',
|
|
543
|
+
probe_boundary: DEV_PROBE_BOUNDARY,
|
|
451
544
|
services: results,
|
|
452
545
|
all_healthy: results.every(r => r.healthy),
|
|
453
546
|
summary: {
|
|
@@ -466,10 +559,12 @@ async function runHealthDev(wsConfig, filterName, jsonOutput, execFn) {
|
|
|
466
559
|
console.log(chalk.cyan('='.repeat(50)));
|
|
467
560
|
console.log(chalk.gray(` Timestamp: ${output.timestamp}`));
|
|
468
561
|
console.log(chalk.gray(` Check duration: ${output.check_duration_ms}ms`));
|
|
469
|
-
console.log(chalk.gray(` Probe surface: local-port
|
|
562
|
+
console.log(chalk.gray(` Probe surface: local-port+route`));
|
|
563
|
+
console.log(chalk.gray(` ${DEV_PROBE_BOUNDARY}\n`));
|
|
470
564
|
|
|
471
565
|
for (const r of results) {
|
|
472
|
-
const
|
|
566
|
+
const yn = (v) => (v ? 'yes' : 'no');
|
|
567
|
+
const probeDetail = `${r.type}, port:${r.port}, bound:${yn(r.bound)}, serves:${yn(r.serves)}, ${r.serves_probe}`;
|
|
473
568
|
console.log(formatProbeLine(r.service, 'dev', r.healthy, probeDetail));
|
|
474
569
|
}
|
|
475
570
|
|
package/lib/commands/init.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
-
import readline from 'readline';
|
|
9
8
|
import path from 'path';
|
|
10
9
|
import fs from 'fs/promises';
|
|
11
10
|
import { fileURLToPath } from 'url';
|
|
@@ -13,56 +12,33 @@ import { dirname, join } from 'path';
|
|
|
13
12
|
// clone.js is imported dynamically inside the invite flow to avoid circular deps
|
|
14
13
|
import { WorkspaceConfig } from '../workspace-config.js';
|
|
15
14
|
import { generateAgentFiles } from '../agent-files.js';
|
|
15
|
+
// The canonical KB-sync surface is owned by retired-kb-sync.js. Consume the constant: a literal
|
|
16
|
+
// spelled here is a second derivation of the same fact, and the removed verb this replaced
|
|
17
|
+
// reached a developer who had done everything right.
|
|
18
|
+
import { CANONICAL_KB_SYNC } from './retired-kb-sync.js';
|
|
19
|
+
// "May I prompt?" has ONE OWNER. init does not derive it, and holds no TTY check of its own.
|
|
20
|
+
import { createPromptSession } from '../interactive.js';
|
|
16
21
|
|
|
17
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
23
|
const __dirname = dirname(__filename);
|
|
19
24
|
|
|
20
|
-
/**
|
|
21
|
-
* Create readline interface for interactive prompts
|
|
22
|
-
*/
|
|
23
|
-
function createPrompt() {
|
|
24
|
-
return readline.createInterface({
|
|
25
|
-
input: process.stdin,
|
|
26
|
-
output: process.stdout
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Ask for text input with optional default
|
|
32
|
-
*/
|
|
33
|
-
function askInput(rl, question, defaultValue = '') {
|
|
34
|
-
return new Promise((resolve) => {
|
|
35
|
-
const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
|
36
|
-
rl.question(prompt, (answer) => {
|
|
37
|
-
resolve(answer.trim() || defaultValue);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Ask a yes/no question
|
|
44
|
-
*/
|
|
45
|
-
function askYesNo(rl, question, defaultYes = true) {
|
|
46
|
-
return new Promise((resolve) => {
|
|
47
|
-
const suffix = defaultYes ? '(Y/n)' : '(y/N)';
|
|
48
|
-
rl.question(`${question} ${suffix}: `, (answer) => {
|
|
49
|
-
const trimmed = answer.trim().toLowerCase();
|
|
50
|
-
if (trimmed === '') {
|
|
51
|
-
resolve(defaultYes);
|
|
52
|
-
} else {
|
|
53
|
-
resolve(trimmed === 'y' || trimmed === 'yes');
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
25
|
/**
|
|
60
26
|
* Run the init command
|
|
61
27
|
* @param {Object} apiClient - API client (optional, for listing communities)
|
|
62
28
|
* @param {Object} options - Command options
|
|
63
29
|
*/
|
|
64
30
|
export async function runInit(apiClient, options = {}) {
|
|
65
|
-
|
|
31
|
+
// The session is GATED AT CONSTRUCTION: under a non-TTY this throws NonInteractiveError,
|
|
32
|
+
// which bin/descix.js's init action prints and exits 1 on. It can no longer hang on an open
|
|
33
|
+
// pipe/FIFO, nor exit 0 writing nothing on /dev/null.
|
|
34
|
+
const rl = createPromptSession({
|
|
35
|
+
what: 'descix init',
|
|
36
|
+
nonInteractiveForm: [
|
|
37
|
+
'descix init has no non-interactive form today. Run it in a terminal.',
|
|
38
|
+
'',
|
|
39
|
+
'Nothing has been changed. An existing .descix/workspace.json is left as it is.'
|
|
40
|
+
]
|
|
41
|
+
});
|
|
66
42
|
const projectPath = options.path ? path.resolve(options.path) : process.cwd();
|
|
67
43
|
|
|
68
44
|
// If --from-invite provided, resolve the invite first to pre-fill context
|
|
@@ -130,7 +106,7 @@ export async function runInit(apiClient, options = {}) {
|
|
|
130
106
|
}
|
|
131
107
|
|
|
132
108
|
if (hasExisting && !options.force) {
|
|
133
|
-
const overwrite = await askYesNo(
|
|
109
|
+
const overwrite = await rl.askYesNo(chalk.yellow('Workspace already initialized. Overwrite?'), false);
|
|
134
110
|
if (!overwrite) {
|
|
135
111
|
console.log(chalk.gray('\nInitialization cancelled.\n'));
|
|
136
112
|
rl.close();
|
|
@@ -156,7 +132,7 @@ export async function runInit(apiClient, options = {}) {
|
|
|
156
132
|
// ignore
|
|
157
133
|
}
|
|
158
134
|
}
|
|
159
|
-
communityId = await
|
|
135
|
+
communityId = await rl.ask(chalk.white('Community ID'));
|
|
160
136
|
if (!communityId) {
|
|
161
137
|
console.log(chalk.red('\n❌ Community ID is required.\n'));
|
|
162
138
|
rl.close();
|
|
@@ -167,7 +143,7 @@ export async function runInit(apiClient, options = {}) {
|
|
|
167
143
|
const defaultAppName = path.basename(projectPath).toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
168
144
|
let appName = options.app;
|
|
169
145
|
if (!appName) {
|
|
170
|
-
appName = await
|
|
146
|
+
appName = await rl.ask(chalk.white('App name'), defaultAppName);
|
|
171
147
|
if (!appName) {
|
|
172
148
|
console.log(chalk.red('\n❌ App name is required.\n'));
|
|
173
149
|
rl.close();
|
|
@@ -181,7 +157,7 @@ export async function runInit(apiClient, options = {}) {
|
|
|
181
157
|
console.log(chalk.white(` Community: ${communityId}`));
|
|
182
158
|
console.log(chalk.white(` App: ${appName} (${appId})`));
|
|
183
159
|
|
|
184
|
-
const proceed = await askYesNo(
|
|
160
|
+
const proceed = await rl.askYesNo(chalk.white('\nProceed?'), true);
|
|
185
161
|
if (!proceed) {
|
|
186
162
|
console.log(chalk.gray('\nCancelled.\n'));
|
|
187
163
|
rl.close();
|
|
@@ -208,8 +184,9 @@ export async function runInit(apiClient, options = {}) {
|
|
|
208
184
|
|
|
209
185
|
console.log(chalk.cyan('\n─── Next Steps ───\n'));
|
|
210
186
|
console.log(chalk.white(` 1. descix app init -a ${appId} -c ${communityId} # register app on platform`));
|
|
211
|
-
console.log(chalk.
|
|
212
|
-
console.log(chalk.white(` 3.
|
|
187
|
+
console.log(chalk.gray(` 2. Create a corpus manifest at .descix/manifests/General.json`));
|
|
188
|
+
console.log(chalk.white(` 3. ${CANONICAL_KB_SYNC} -a ${appId} # sync KB to Pinecone`));
|
|
189
|
+
console.log(chalk.white(` 4. descix chat -c ${communityId} -a ${appId} -q "test" # verify RAG\n`));
|
|
213
190
|
console.log(chalk.green('✅ Workspace initialized.\n'));
|
|
214
191
|
rl.close();
|
|
215
192
|
return { created: ['.descix/workspace.json'], skipped: [], warnings: [] };
|
|
@@ -250,7 +227,7 @@ export async function initWorkspace(options) {
|
|
|
250
227
|
created,
|
|
251
228
|
skipped: [],
|
|
252
229
|
warnings: [],
|
|
253
|
-
nextSteps: [`descix app init -a ${appId} -c ${communityId}`,
|
|
230
|
+
nextSteps: [`descix app init -a ${appId} -c ${communityId}`, `${CANONICAL_KB_SYNC} -a ${appId}`]
|
|
254
231
|
};
|
|
255
232
|
}
|
|
256
233
|
|
package/lib/commands/kb.js
CHANGED
|
@@ -22,11 +22,17 @@ import ora from 'ora';
|
|
|
22
22
|
import * as path from 'path';
|
|
23
23
|
import * as fs from 'fs/promises';
|
|
24
24
|
import inquirer from 'inquirer';
|
|
25
|
+
// "May I prompt?" has ONE OWNER. --interactive is an opt-IN, not a guarantee a human is there.
|
|
26
|
+
import { requireInteractive } from '../interactive.js';
|
|
25
27
|
import { WorkspaceConfig } from '../workspace-config.js';
|
|
26
28
|
import { hydrateKb, pushStaging, checkStagingFiles } from '../core/Hydrator.js';
|
|
27
29
|
import { processKb } from '../core/Chunker.js';
|
|
28
30
|
import { syncKb, getSyncStatus } from '../core/Syncer.js';
|
|
29
31
|
import * as driveADC from '../google-storage-adc.js';
|
|
32
|
+
// The one canonical KB-sync surface, from its owner. Never spell it as a literal here: these
|
|
33
|
+
// commands' next steps are read by a developer as instructions, so they must name the live verb
|
|
34
|
+
// and nothing else.
|
|
35
|
+
import { CANONICAL_KB_SYNC } from './retired-kb-sync.js';
|
|
30
36
|
|
|
31
37
|
// ============ Pull Command ============
|
|
32
38
|
|
|
@@ -110,8 +116,7 @@ export async function runKbPull(apiClient, options) {
|
|
|
110
116
|
if (!options.quiet) {
|
|
111
117
|
console.log(chalk.cyan('\n📋 Next steps:'));
|
|
112
118
|
console.log(chalk.gray(' 1. Review files in kb/' + kbId + '/'));
|
|
113
|
-
console.log(chalk.gray(
|
|
114
|
-
console.log(chalk.gray(' 3. Run "descix kb sync" to push to Pinecone\n'));
|
|
119
|
+
console.log(chalk.gray(` 2. Run "${CANONICAL_KB_SYNC} -a <app_id>" to push to Pinecone\n`));
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
return result;
|
|
@@ -173,6 +178,11 @@ export async function runKbPush(apiClient, options) {
|
|
|
173
178
|
// Create conflict prompt callback for interactive mode
|
|
174
179
|
const onConflictPrompt = options.interactive ? async (fileName, fileInfo) => {
|
|
175
180
|
spinner.stop();
|
|
181
|
+
requireInteractive({
|
|
182
|
+
what: 'descix kb push --interactive',
|
|
183
|
+
question: `File "${fileName}" exists in Drive. Overwrite or skip?`,
|
|
184
|
+
nonInteractiveForm: ['descix kb push # without --interactive, conflicts resolve without a prompt']
|
|
185
|
+
});
|
|
176
186
|
const { action } = await inquirer.prompt([{
|
|
177
187
|
type: 'list',
|
|
178
188
|
name: 'action',
|
|
@@ -220,8 +230,8 @@ export async function runKbPush(apiClient, options) {
|
|
|
220
230
|
// Show next steps
|
|
221
231
|
if (!options.quiet) {
|
|
222
232
|
console.log(chalk.cyan('\n📋 Next steps:'));
|
|
223
|
-
console.log(chalk.gray(' 1. Run "descix
|
|
224
|
-
console.log(chalk.gray(
|
|
233
|
+
console.log(chalk.gray(' 1. Run "descix drive pull" to sync all Drive content'));
|
|
234
|
+
console.log(chalk.gray(` 2. Run "${CANONICAL_KB_SYNC} -a <app_id>" to push to Pinecone\n`));
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
return result;
|
package/lib/commands/update.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Commands for updating resources based on workspace.json context detection:
|
|
5
5
|
* descix update # Auto-detect what to update
|
|
6
6
|
* descix update app # Sync app assets to Drive
|
|
7
|
-
* descix update kb # Sync KB + vectorize
|
|
8
7
|
* descix update site # Deploy site to GCS
|
|
9
8
|
* descix update all # Update everything
|
|
10
9
|
*
|
package/lib/core/Chunker.js
CHANGED
|
@@ -938,7 +938,7 @@ export async function processKb(config, options = {}) {
|
|
|
938
938
|
try {
|
|
939
939
|
await fs.access(srcDir);
|
|
940
940
|
} catch {
|
|
941
|
-
throw new Error(`Source directory not found: ${srcDir}\nRun "descix
|
|
941
|
+
throw new Error(`Source directory not found: ${srcDir}\nRun "descix drive pull" first.`);
|
|
942
942
|
}
|
|
943
943
|
|
|
944
944
|
await fs.mkdir(chunksDir, { recursive: true });
|