@descix/cli 2.0.1 → 2.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/agent-assets/guides/microservice-pattern.md +3 -1
- package/bin/descix.js +17 -10
- package/lib/commands/airdrop.js +28 -145
- package/lib/commands/appInit.js +39 -5
- package/lib/commands/config.js +18 -4
- package/lib/verb-invokes.generated.json +2 -0
- package/package.json +1 -1
- package/templates/scaffolds/microservice/package.json +1 -1
- package/templates/scaffolds/microservice/scripts/register.js +10 -0
|
@@ -116,7 +116,9 @@ descix kb corpus sync -c <community> -a <app>
|
|
|
116
116
|
|
|
117
117
|
**What registration is, and is not.** It publishes your `manifest.json` — the command list —
|
|
118
118
|
and vectorizes the README you pass with `-r`, so `tell_me_how` can discover your tools.
|
|
119
|
-
`scripts/register.js` writes that manifest and logs the commands it published.
|
|
119
|
+
`scripts/register.js` writes that manifest and logs the commands it published. It targets a RUNNING
|
|
120
|
+
service: it first calls the service's own `/api` unsigned and registers only if the mesh guard
|
|
121
|
+
answers 401, so start the service (`npm run dev`), then run `npm run register`. There is no
|
|
120
122
|
separate developer-facing vectorize step — `descix microservice vectorize` exists but is an
|
|
121
123
|
admin-only utility (for re-vectorizing without re-registering), not part of the normal flow.
|
|
122
124
|
It is a DISCOVERY step.
|
package/bin/descix.js
CHANGED
|
@@ -442,11 +442,9 @@ const airdropCommand = program
|
|
|
442
442
|
|
|
443
443
|
airdropCommand
|
|
444
444
|
.command('execute-queue')
|
|
445
|
-
.description('
|
|
446
|
-
.option('--community <slug>', 'Community slug for per-community batch scoping
|
|
447
|
-
.option('--dry-run', 'Read-only preview: encode calldata, estimate gas, validate net-zero invariant. No PK, no tx, no state mutation.')
|
|
448
|
-
.option('--apply', 'Live execution: requires --signer-pk-file or interactive prompt-password. Mutually exclusive with --dry-run.')
|
|
449
|
-
.option('--signer-pk-file <path>', 'Path to file containing admin signer PK (0x + 64 hex). Required for --apply unless prompted interactively.')
|
|
445
|
+
.description('DRY-RUN ONLY preview of the airdrop migration queue (admin-only). Execution lives in daita-sam (act plan + the role holder\'s own wallet) — the apply leg is removed.')
|
|
446
|
+
.option('--community <slug>', 'Community slug for per-community batch scoping')
|
|
447
|
+
.option('--dry-run', 'Read-only preview: encode calldata, estimate gas, validate net-zero invariant. No PK, no tx, no state mutation. (The default; the flag is accepted for scripts.)')
|
|
450
448
|
.option('--batch-size <n>', 'Cap on users processed this run (server caps at AIRDROP_MAX_RUN_USERS)')
|
|
451
449
|
.action(async (options) => {
|
|
452
450
|
try {
|
|
@@ -2411,11 +2409,20 @@ siteCommand
|
|
|
2411
2409
|
filesToUpload.forEach(f => console.log(chalk.gray(` + ${f.path} (${(f.size / 1024).toFixed(1)}KB)`)));
|
|
2412
2410
|
// The server's last deploy manifest names what it holds; the confirm step lists the
|
|
2413
2411
|
// storage prefix itself, so this preview is the manifest's view of what would go.
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2412
|
+
// THE REMOVE SET IS ALWAYS STATED, including empty (regression from 1.0.20 measured
|
|
2413
|
+
// 2026-10-10: the silent branch read as "didn't check", and an absence claim the
|
|
2414
|
+
// instrument cannot make — no server manifest — must say THAT, not imply none).
|
|
2415
|
+
if (existing_manifest?.files) {
|
|
2416
|
+
const lastDeployed = Object.keys(existing_manifest.files);
|
|
2417
|
+
const wouldRemove = lastDeployed.filter(p => !(p in localFiles)).sort();
|
|
2418
|
+
if (wouldRemove.length > 0) {
|
|
2419
|
+
console.log(chalk.yellow('\n Would remove from the server (in the last deploy, not in this build):'));
|
|
2420
|
+
wouldRemove.forEach(f => console.log(chalk.gray(` - ${f}`)));
|
|
2421
|
+
} else {
|
|
2422
|
+
console.log(chalk.gray('\n Would remove: none (every file of the last deploy is in this build)'));
|
|
2423
|
+
}
|
|
2424
|
+
} else {
|
|
2425
|
+
console.log(chalk.gray('\n Would remove: unknown — the server returned no previous deploy manifest (first deploy, or manifest unreadable)'));
|
|
2419
2426
|
}
|
|
2420
2427
|
console.log(chalk.gray(`\n Site URL: ${site_url}\n`));
|
|
2421
2428
|
return;
|
package/lib/commands/airdrop.js
CHANGED
|
@@ -1,149 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Airdrop Commands
|
|
2
|
+
* Airdrop Commands — DRY-RUN ONLY.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Per CEO-D-MANUAL-TRIGGER-NO-CRON (2026-04-20), Cloud Scheduler cron was dropped
|
|
6
|
-
* from Round B in favor of operator-invoked manual triggers via this CLI.
|
|
4
|
+
* descix airdrop execute-queue [--community <slug>] [--dry-run] [--batch-size <n>]
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The apply leg (an admin PK forwarded through the server to Powch's broadcast) is REMOVED,
|
|
7
|
+
* CLI and server both (CEO 2026-10-10, daita-sam M2 envelope constraint 6): migration batches
|
|
8
|
+
* execute in daita-sam — act plan + typed confirm + the role holder's own wallet, client-signed.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* --dry-run Read-only preview path. No PK prompt. No tx. No state mutation.
|
|
14
|
-
* --apply Live execution path. Requires --signer-pk-file or interactive
|
|
15
|
-
* prompt-password. Mutually exclusive with --dry-run.
|
|
16
|
-
* --signer-pk-file <path> File containing the admin's per-invocation signer PK (0x + 64 hex,
|
|
17
|
-
* one line). Inline --signer-pk and env vars REJECTED on security
|
|
18
|
-
* grounds (process listing leak).
|
|
19
|
-
*
|
|
20
|
-
* Access: requires admin session (platform-admin group membership enforced server-side
|
|
21
|
-
* in `airdrop_execute_queue` via `isPlatformAdmin(user)`). CLI-side, we require an
|
|
22
|
-
* authenticated session (`requireAuth`). The server rejects non-admin users.
|
|
23
|
-
*
|
|
24
|
-
* Safety: the `BATCH_UPDATE_BALANCES_BROADCAST_ENABLED` flag gates the actual on-chain
|
|
25
|
-
* broadcast in Powch. While that flag is false, `airdrop_execute_queue --apply` will assemble
|
|
26
|
-
* batches but Powch returns `stopped_at_broadcast_boundary` — useful for verifying queue
|
|
27
|
-
* assembly without spending MATIC.
|
|
10
|
+
* Access: requires an authenticated session (`requireAuth`); the server additionally requires
|
|
11
|
+
* platform admin for the preview.
|
|
28
12
|
*/
|
|
29
13
|
|
|
30
|
-
import fs from 'fs';
|
|
31
|
-
// "May I prompt?" has ONE OWNER. This site holds no TTY check of its own.
|
|
32
|
-
import { requireInteractive } from '../interactive.js';
|
|
33
|
-
import readline from 'readline';
|
|
34
14
|
import chalk from 'chalk';
|
|
35
15
|
import { DeSciXApiClient } from '../api-client.js';
|
|
36
16
|
import { requireAuth } from '../auth-guard.js';
|
|
37
17
|
|
|
38
|
-
const PK_REGEX = /^0x[a-fA-F0-9]{64}$/;
|
|
39
|
-
|
|
40
18
|
/**
|
|
41
|
-
*
|
|
42
|
-
* and a trailing newline only — does NOT mutate any other formatting.
|
|
43
|
-
*
|
|
44
|
-
* @param {string} path - Filesystem path to the PK file.
|
|
45
|
-
* @returns {string} Validated PK in `0x` + 64 hex form.
|
|
46
|
-
* @throws {Error} If the file is missing or contents are malformed.
|
|
47
|
-
*/
|
|
48
|
-
export function readSignerPkFromFile(path) {
|
|
49
|
-
let raw;
|
|
50
|
-
try {
|
|
51
|
-
raw = fs.readFileSync(path, 'utf8');
|
|
52
|
-
} catch (e) {
|
|
53
|
-
throw new Error(`--signer-pk-file: cannot read ${path}: ${e.message}`);
|
|
54
|
-
}
|
|
55
|
-
const trimmed = raw.trim();
|
|
56
|
-
if (!PK_REGEX.test(trimmed)) {
|
|
57
|
-
// CRITICAL: do not echo the contents (even partial) into the error message.
|
|
58
|
-
throw new Error(`--signer-pk-file: contents do not match expected 0x + 64 hex format`);
|
|
59
|
-
}
|
|
60
|
-
return trimmed;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Interactive password prompt (no echo). Reads a line of stdin with terminal echo disabled
|
|
65
|
-
* so the typed PK never appears on screen. Validates 0x + 64-hex format.
|
|
66
|
-
*
|
|
67
|
-
* @returns {Promise<string>} Validated PK in `0x` + 64 hex form.
|
|
68
|
-
*/
|
|
69
|
-
export async function promptSignerPkInteractive({ stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
70
|
-
// Gate FIRST: a non-TTY here previously hung forever holding a secret prompt open. This site
|
|
71
|
-
// keeps its own readline because it must disable terminal echo so the PK is never displayed;
|
|
72
|
-
// the DECISION of whether prompting is allowed is still the owner's alone.
|
|
73
|
-
requireInteractive({
|
|
74
|
-
what: 'descix airdrop',
|
|
75
|
-
question: 'signer private key',
|
|
76
|
-
nonInteractiveForm: ['descix airdrop --signer-pk-file <path> # reads the key from a file, never echoed']
|
|
77
|
-
});
|
|
78
|
-
return await new Promise((resolve, reject) => {
|
|
79
|
-
const rl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
|
|
80
|
-
// Disable echo so the PK isn't visible while typing.
|
|
81
|
-
const originalWrite = stdout.write.bind(stdout);
|
|
82
|
-
stdout.write = (str, ...rest) => {
|
|
83
|
-
// Suppress only the echoed characters during the prompt; allow our prompt to render.
|
|
84
|
-
if (typeof str === 'string' && str.length === 1) return true;
|
|
85
|
-
return originalWrite(str, ...rest);
|
|
86
|
-
};
|
|
87
|
-
stdout.write('Enter signer PK (0x + 64 hex, will not be echoed): ');
|
|
88
|
-
rl.question('', (answer) => {
|
|
89
|
-
stdout.write = originalWrite;
|
|
90
|
-
stdout.write('\n');
|
|
91
|
-
rl.close();
|
|
92
|
-
const trimmed = (answer || '').trim();
|
|
93
|
-
if (!PK_REGEX.test(trimmed)) {
|
|
94
|
-
reject(new Error('prompt-password: input does not match expected 0x + 64 hex format'));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
resolve(trimmed);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Execute the airdrop migration batch queue on the target environment.
|
|
19
|
+
* Preview (dry-run) the airdrop migration batch queue on the target environment.
|
|
104
20
|
*
|
|
105
21
|
* @param {Object} options
|
|
106
|
-
* @param {string} [options.community] - Community slug
|
|
107
|
-
*
|
|
108
|
-
* @param {boolean} [options.dryRun] - If true, read-only preview path. Mutually exclusive with --apply.
|
|
109
|
-
* @param {boolean} [options.apply] - If true, live execution path. Requires --signer-pk-file or
|
|
110
|
-
* interactive prompt-password. Mutually exclusive with --dry-run.
|
|
111
|
-
* @param {string} [options.signerPkFile] - Path to file containing the admin's signer PK.
|
|
22
|
+
* @param {string} [options.community] - Community slug scoping the preview (omitted = aggregate).
|
|
23
|
+
* @param {boolean} [options.dryRun] - Accepted for scripts; dry-run is the only mode.
|
|
112
24
|
* @param {number} [options.batchSize] - Optional cap on number of users processed this run.
|
|
113
25
|
* @param {Object} [options.apiClient] - Optional injected API client (for tests).
|
|
114
|
-
* @param {Function} [options.promptFn] - Optional injected interactive PK prompt (for tests).
|
|
115
26
|
*/
|
|
116
27
|
export async function executeQueue(options) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const isDryRun = options.dryRun === true || (!options.apply && options.dryRun !== false);
|
|
126
|
-
const isApply = options.apply === true;
|
|
127
|
-
|
|
128
|
-
if (isApply && !options.community) {
|
|
129
|
-
throw new Error('--community <slug> is required for --apply mode (per-community batch scoping per CEO-D-B1-FIX-DESIGN-LOCK Decision 3).');
|
|
28
|
+
// THE APPLY LEG IS STRUCK — refused before auth, before any network touch (CEO 2026-10-10, daita-sam M2 envelope constraint 6; the server
|
|
29
|
+
// refuses it too): migration batches execute in daita-sam — act plan + the role holder's own
|
|
30
|
+
// wallet signs, client-side. No PK ever transits this CLI again.
|
|
31
|
+
if (options.apply || options.signerPkFile) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'airdrop execute-queue is DRY-RUN ONLY: the apply leg (admin PK forwarded through the ' +
|
|
34
|
+
'server) is removed. Execute migration batches in daita-sam (act plan + your own wallet).'
|
|
35
|
+
);
|
|
130
36
|
}
|
|
37
|
+
const isDryRun = true;
|
|
131
38
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (isApply) {
|
|
135
|
-
if (options.signerPkFile) {
|
|
136
|
-
signerPkHex = readSignerPkFromFile(options.signerPkFile);
|
|
137
|
-
} else {
|
|
138
|
-
const promptFn = options.promptFn || promptSignerPkInteractive;
|
|
139
|
-
signerPkHex = await promptFn();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
39
|
+
const apiClient = options.apiClient || new DeSciXApiClient();
|
|
40
|
+
await requireAuth(apiClient);
|
|
142
41
|
|
|
143
42
|
const params = {};
|
|
144
43
|
if (options.community) params.community = options.community;
|
|
145
|
-
params.mode =
|
|
146
|
-
if (signerPkHex) params.signer_pk_hex = signerPkHex;
|
|
44
|
+
params.mode = 'dry-run';
|
|
147
45
|
if (options.batchSize !== undefined && options.batchSize !== null) {
|
|
148
46
|
const bs = Number(options.batchSize);
|
|
149
47
|
if (!Number.isFinite(bs) || bs <= 0) {
|
|
@@ -157,18 +55,13 @@ export async function executeQueue(options) {
|
|
|
157
55
|
}
|
|
158
56
|
|
|
159
57
|
const envLabel = process.env.DESCIX_API_URL || '(workspace default)';
|
|
160
|
-
|
|
161
|
-
console.log(chalk.cyan(`\nairdrop execute-queue [${mode}] target=${envLabel}`));
|
|
58
|
+
console.log(chalk.cyan(`\nairdrop execute-queue [${chalk.yellow('DRY-RUN')}] target=${envLabel}`));
|
|
162
59
|
if (options.community) {
|
|
163
60
|
console.log(chalk.gray(` community: ${options.community}`));
|
|
164
61
|
}
|
|
165
62
|
if (params.batch_size) {
|
|
166
63
|
console.log(chalk.gray(` batch_size: ${params.batch_size}`));
|
|
167
64
|
}
|
|
168
|
-
if (signerPkHex) {
|
|
169
|
-
// Just confirm a PK is loaded; never echo any portion of it.
|
|
170
|
-
console.log(chalk.gray(` signer_pk: loaded (0x${signerPkHex.slice(2,6)}…${signerPkHex.slice(-4)})`));
|
|
171
|
-
}
|
|
172
65
|
console.log('');
|
|
173
66
|
|
|
174
67
|
let response;
|
|
@@ -184,12 +77,6 @@ export async function executeQueue(options) {
|
|
|
184
77
|
console.error(chalk.gray(' Verify your membership in the DESCIX_ADMIN_GROUP Google Group.\n'));
|
|
185
78
|
}
|
|
186
79
|
throw error;
|
|
187
|
-
} finally {
|
|
188
|
-
// Belt-and-suspenders: clear local PK var after the single use. We deliberately
|
|
189
|
-
// do NOT mutate `params.signer_pk_hex` here because the api-client/axios layer may
|
|
190
|
-
// still be serializing the body, and modern axios keeps no reference after the
|
|
191
|
-
// request resolves anyway. The local var nulling is the meaningful action.
|
|
192
|
-
signerPkHex = null;
|
|
193
80
|
}
|
|
194
81
|
|
|
195
82
|
const result = response?.message || response;
|
|
@@ -212,7 +99,7 @@ export async function executeQueue(options) {
|
|
|
212
99
|
const nzLabel = nz.passes ? chalk.green('PASS') : chalk.red('FAIL');
|
|
213
100
|
console.log(` net_zero_assertion: ${nzLabel} (Σ=${nz.sum})`);
|
|
214
101
|
const pbc = result.pre_batch_checks || {};
|
|
215
|
-
console.log(` pre_batch_checks: ${pbc.passes ? chalk.green('PASS') : chalk.red('FAIL —
|
|
102
|
+
console.log(` pre_batch_checks: ${pbc.passes ? chalk.green('PASS') : chalk.red('FAIL — an execute would refuse')}`);
|
|
216
103
|
for (const r of (pbc.refusals || [])) {
|
|
217
104
|
console.log(chalk.red(` ${r.kind} ${r.wallet} (${r.community_id}) holds ${r.held}, needs ${r.required} base units — rows ${(r.pending_ids || []).join(', ')}`));
|
|
218
105
|
}
|
|
@@ -226,8 +113,8 @@ export async function executeQueue(options) {
|
|
|
226
113
|
}
|
|
227
114
|
}
|
|
228
115
|
console.log('');
|
|
229
|
-
console.log(chalk.gray('No
|
|
230
|
-
console.log(chalk.gray('
|
|
116
|
+
console.log(chalk.gray('No tx submitted. No DB rows mutated.'));
|
|
117
|
+
console.log(chalk.gray('To execute migration batches: daita-sam (act plan + your own wallet).'));
|
|
231
118
|
console.log('');
|
|
232
119
|
return result;
|
|
233
120
|
}
|
|
@@ -254,13 +141,9 @@ export async function executeQueue(options) {
|
|
|
254
141
|
}
|
|
255
142
|
|
|
256
143
|
console.log('');
|
|
257
|
-
|
|
258
|
-
console.log(chalk.gray('Apply complete. See on_chain_log for the trigger audit row.'));
|
|
259
|
-
} else {
|
|
260
|
-
console.log(chalk.gray('Dry-run complete. No broadcast attempted. No on_chain_log broadcast row written.'));
|
|
261
|
-
}
|
|
144
|
+
console.log(chalk.gray('Dry-run complete. No broadcast attempted. No on_chain_log broadcast row written.'));
|
|
262
145
|
console.log('');
|
|
263
146
|
return result;
|
|
264
147
|
}
|
|
265
148
|
|
|
266
|
-
export default { executeQueue
|
|
149
|
+
export default { executeQueue };
|
package/lib/commands/appInit.js
CHANGED
|
@@ -23,7 +23,8 @@ import { generateAgentFiles } from '../agent-files.js';
|
|
|
23
23
|
*/
|
|
24
24
|
export async function runAppInit(apiClient, options) {
|
|
25
25
|
let appId = options.app;
|
|
26
|
-
|
|
26
|
+
let kbId = options.kb || 'General';
|
|
27
|
+
let createdHere = false;
|
|
27
28
|
|
|
28
29
|
// v3 single-app: THIS directory becomes (or already is) the workspace root. A workspace at an
|
|
29
30
|
// ANCESTOR is a refusal, loud, BEFORE anything is written anywhere — the walk-up used to
|
|
@@ -59,6 +60,7 @@ export async function runAppInit(apiClient, options) {
|
|
|
59
60
|
const created = response.message || response;
|
|
60
61
|
appId = created.app_id || appId;
|
|
61
62
|
communityId = created.community_id || options.community;
|
|
63
|
+
createdHere = true;
|
|
62
64
|
console.log(chalk.green(`\n✅ App created: ${appId}`));
|
|
63
65
|
if (appId !== options.app) {
|
|
64
66
|
console.log(chalk.gray(` The platform composed the id from the community and the name you gave.`));
|
|
@@ -71,6 +73,35 @@ export async function runAppInit(apiClient, options) {
|
|
|
71
73
|
);
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
// MAPPING AN EXISTING APP ADOPTS ITS KNOWLEDGE BASES AND NEVER CREATES ONE (measured
|
|
77
|
+
// 2026-10-10: mapping `egpt` without --kb created an empty `General` on PROD beside its
|
|
78
|
+
// real `Corpus`). One existing KB → adopted; several → --kb must name one of THEM; a named
|
|
79
|
+
// KB that does not exist is a refusal, never a create. Only the CREATE leg above may create,
|
|
80
|
+
// because only a brand-new app has nothing to adopt.
|
|
81
|
+
if (!createdHere) {
|
|
82
|
+
const kbResp = await apiClient.invoke('list_knowledge_bases', { community_id: communityId, app_id: appId });
|
|
83
|
+
const existingKbs = ((kbResp.message || kbResp).knowledge_bases || []).map((kb) => kb.name);
|
|
84
|
+
if (options.kb) {
|
|
85
|
+
if (!existingKbs.includes(options.kb)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`App '${appId}' has no KB named '${options.kb}' — it has: ${existingKbs.join(', ') || '(none)'}.\n` +
|
|
88
|
+
`Mapping an existing app never creates a KB; name one that exists with --kb, or create ` +
|
|
89
|
+
`one deliberately with its own verb.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
kbId = options.kb;
|
|
93
|
+
} else if (existingKbs.length === 1) {
|
|
94
|
+
kbId = existingKbs[0];
|
|
95
|
+
} else if (existingKbs.length > 1) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`App '${appId}' has ${existingKbs.length} knowledge bases: ${existingKbs.join(', ')}.\n` +
|
|
98
|
+
`Name the one this repo syncs with --kb <name>.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
// zero KBs: every app is guaranteed a default KB at creation, so an empty list is a
|
|
102
|
+
// platform-side anomaly — keep the declared kbId and let the sync surface report it.
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
// 1. Workspace.json — THIS repo root is the workspace root and holds exactly this app (v3).
|
|
75
106
|
// registerApp is idempotent for the same id and REFUSES a different one: a repo that already
|
|
76
107
|
// holds another app errors loudly instead of being silently re-pointed.
|
|
@@ -100,9 +131,12 @@ export async function runAppInit(apiClient, options) {
|
|
|
100
131
|
}
|
|
101
132
|
console.log(chalk.gray(` Created: site/, microservice/, assets/`));
|
|
102
133
|
|
|
103
|
-
// 3.
|
|
104
|
-
|
|
105
|
-
|
|
134
|
+
// 3. The default KB — CREATE leg only (see the adopt block above: map never creates).
|
|
135
|
+
let kbResult = null;
|
|
136
|
+
if (createdHere) {
|
|
137
|
+
const kbResponse = await apiClient.invoke('init_git_mode_kb', { app_id: appId, kb_name: kbId });
|
|
138
|
+
kbResult = kbResponse.message || kbResponse;
|
|
139
|
+
}
|
|
106
140
|
|
|
107
141
|
console.log(chalk.green(`\n✓ ${appId} initialized`));
|
|
108
142
|
console.log(chalk.gray(` Community: ${communityId}`));
|
|
@@ -119,7 +153,7 @@ export async function runAppInit(apiClient, options) {
|
|
|
119
153
|
console.log(chalk.gray(` Retry with: descix community refresh-identity -c ${communityId}`));
|
|
120
154
|
}
|
|
121
155
|
}
|
|
122
|
-
console.log(chalk.gray(` KB: ${kbId} — ${kbResult.created ? 'created' : 'already exists'}\n`));
|
|
156
|
+
console.log(chalk.gray(` KB: ${kbId} — ${kbResult ? (kbResult.created ? 'created' : 'already exists') : 'adopted (existing app; map never creates)'}\n`));
|
|
123
157
|
console.log(chalk.gray(` (every app is guaranteed a default KB at creation; an empty one`));
|
|
124
158
|
console.log(chalk.gray(` says so rather than answering from general knowledge)\n`));
|
|
125
159
|
|
package/lib/commands/config.js
CHANGED
|
@@ -34,10 +34,24 @@ export async function show() {
|
|
|
34
34
|
console.log(chalk.yellow(` ⚠ This workspace pins ${normalizeOrigin(pinned)} (${environmentNameFor(pinned)}), but ${effective.source} overrides it for this shell.`));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// v3 single-app: the one app this workspace holds
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// v3 single-app: the one app this workspace holds. Every fact a verb can set is shown here,
|
|
38
|
+
// so the verb's effect is verifiable without hand-reading workspace.json (field, 2026-10-10:
|
|
39
|
+
// neither the adopted kbId nor env.gateway.port was printed by anything).
|
|
40
|
+
const app = workspaceConfig.env?.app;
|
|
41
|
+
if (app?.appId) {
|
|
42
|
+
console.log(chalk.white(` App: ${app.appId}`));
|
|
43
|
+
if (app.kbId) {
|
|
44
|
+
console.log(chalk.white(` KB: ${app.kbId}`));
|
|
45
|
+
}
|
|
46
|
+
if (app.site?.static) {
|
|
47
|
+
console.log(chalk.white(` Site: static ${app.site.static}`));
|
|
48
|
+
} else if (app.site?.port) {
|
|
49
|
+
console.log(chalk.white(` Site: dev server on port ${app.site.port} (${app.site.protocol || 'https'})`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const gatewayPort = workspaceConfig.env?.gateway?.port;
|
|
53
|
+
if (gatewayPort) {
|
|
54
|
+
console.log(chalk.white(` Gateway port: ${gatewayPort}`));
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
console.log('');
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"create_app_for_community",
|
|
13
13
|
"get_product_context",
|
|
14
14
|
"init_git_mode_kb",
|
|
15
|
+
"list_knowledge_bases",
|
|
15
16
|
"update_community_metadata"
|
|
16
17
|
],
|
|
17
18
|
"app invite": [
|
|
@@ -189,6 +190,7 @@
|
|
|
189
190
|
"find_communities",
|
|
190
191
|
"get_product_context",
|
|
191
192
|
"init_git_mode_kb",
|
|
193
|
+
"list_knowledge_bases",
|
|
192
194
|
"resolve_invite",
|
|
193
195
|
"sync_auto_purchases",
|
|
194
196
|
"update_community_metadata"
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"gen-jwt-secret": "node -e 'console.log(require(\"crypto\").randomBytes(32).toString(\"hex\"))'"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@descix/cloud-core": "^
|
|
14
|
+
"@descix/cloud-core": "^2.0.0",
|
|
15
15
|
"@descix/platform-api": "^2.0.0",
|
|
16
16
|
"@google-cloud/secret-manager": "^5.0.1",
|
|
17
17
|
"@google-cloud/storage": "^7.7.0",
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* (audience = CORE_API_URL). On Cloud Run that works with no setup. Under local developer ADC,
|
|
24
24
|
* ID-token minting is unsupported without impersonation and this reports a named failure — it
|
|
25
25
|
* does not fall back to anything.
|
|
26
|
+
*
|
|
27
|
+
* REGISTRATION TARGETS A RUNNING SERVICE. Before it registers, the bootstrap calls this service's
|
|
28
|
+
* own /api UNSIGNED and registers only if the mesh guard answers 401 (the mesh startup self-test).
|
|
29
|
+
* The origin it calls is derived from utils.PORT — the same config value app.js binds — so start
|
|
30
|
+
* the service (npm run dev) first, in the same checkout, then run this script.
|
|
26
31
|
*/
|
|
27
32
|
|
|
28
33
|
import fs from 'fs';
|
|
@@ -51,12 +56,17 @@ async function register() {
|
|
|
51
56
|
// Used ONLY to name the derived domain in a message. The platform, not this script,
|
|
52
57
|
// owns the derivation.
|
|
53
58
|
siteDomain: utils.SITE_DOMAIN,
|
|
59
|
+
// The running service's own listener, from the config owner app.js binds (utils.PORT).
|
|
60
|
+
meshSelfUrl: `http://127.0.0.1:${utils.PORT}`,
|
|
54
61
|
});
|
|
55
62
|
|
|
56
63
|
const result = await bootstrap.register();
|
|
57
64
|
|
|
58
65
|
if (result.status !== 'ok') {
|
|
59
66
|
console.error(`[Register] ${manifest.service.name}: registration ${result.status} — ${result.error || result.reason}`);
|
|
67
|
+
if (String(result.error || result.reason).includes('mesh self-test FAILED')) {
|
|
68
|
+
console.error('[Register] REMEDY: start the service (npm run dev), then re-run npm run register');
|
|
69
|
+
}
|
|
60
70
|
process.exit(1);
|
|
61
71
|
}
|
|
62
72
|
console.log(`[Register] SUCCESS: ${manifest.service.name} registered via register_service`);
|