@aikdna/kdna-cli 0.9.0
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 +202 -0
- package/NOTICE +9 -0
- package/README.md +73 -0
- package/package.json +58 -0
- package/skills/kdna-loader/SKILL.md +257 -0
- package/src/agent.js +434 -0
- package/src/cli.js +260 -0
- package/src/cluster.js +235 -0
- package/src/cmds/_common.js +100 -0
- package/src/cmds/cluster.js +235 -0
- package/src/cmds/domain.js +638 -0
- package/src/cmds/identity.js +31 -0
- package/src/cmds/legacy.js +83 -0
- package/src/cmds/quality.js +87 -0
- package/src/cmds/registry.js +114 -0
- package/src/cmds/setup.js +8 -0
- package/src/compare.js +324 -0
- package/src/diff.js +288 -0
- package/src/identity.js +211 -0
- package/src/init.js +168 -0
- package/src/install.js +849 -0
- package/src/loader.js +70 -0
- package/src/publish.js +600 -0
- package/src/registry.js +258 -0
- package/src/search.js +73 -0
- package/src/setup.js +197 -0
- package/src/verify.js +423 -0
- package/src/version.js +112 -0
- package/templates/cluster/KDNA_Cluster.json +25 -0
- package/templates/cluster/README.md +32 -0
- package/templates/minimal-domain/KDNA_Core.json +54 -0
- package/templates/minimal-domain/KDNA_Patterns.json +37 -0
- package/templates/minimal-domain/kdna.json +31 -0
- package/templates/minimal-domain/tests/before-after.json +16 -0
- package/templates/standard-domain/KDNA_Core.json +76 -0
- package/templates/standard-domain/KDNA_Patterns.json +44 -0
- package/templates/standard-domain/README.md +74 -0
- package/templates/standard-domain/USAGE.md +59 -0
- package/templates/standard-domain/evals/1_excluded_case.json +16 -0
- package/templates/standard-domain/evals/3_boundary_cases.json +38 -0
- package/templates/standard-domain/evals/3_core_cases.json +35 -0
- package/templates/standard-domain/evals/3_failure_cases.json +35 -0
- package/templates/standard-domain/evals/scoring.json +60 -0
- package/templates/standard-domain/kdna.json +28 -0
- package/validators/kdna-lint.js +53 -0
- package/validators/kdna-validate.js +92 -0
package/src/install.js
ADDED
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KDNA Install — v0.7 .kdna-first installer.
|
|
3
|
+
*
|
|
4
|
+
* Sources (priority order):
|
|
5
|
+
* kdna install <bare> → @aikdna/<bare>, from registry
|
|
6
|
+
* kdna install @scope/name → from registry (any scope)
|
|
7
|
+
* kdna install @scope/name@1.2.3 → version pinned (TODO post-v0.7.0)
|
|
8
|
+
* kdna install ./folder → local directory (dev)
|
|
9
|
+
* kdna install ./file.kdna → local .kdna file
|
|
10
|
+
*
|
|
11
|
+
* Removed in v0.7 (breaking): github:user/repo, --from-git, cluster:github:...,
|
|
12
|
+
* tarball/SSH fallbacks. Install is now strictly .kdna-driven from the registry.
|
|
13
|
+
*
|
|
14
|
+
* Schema v2.0 — see kdna-registry/SCHEMA.md
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const { execSync, execFileSync } = require('child_process');
|
|
21
|
+
const { RegistryResolver, parseName } = require('./registry');
|
|
22
|
+
|
|
23
|
+
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
24
|
+
const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
|
|
25
|
+
|
|
26
|
+
// Agent skill directories (search order)
|
|
27
|
+
const AGENT_SKILL_DIRS = [
|
|
28
|
+
path.join(process.env.HOME || '', '.agents', 'skills'),
|
|
29
|
+
path.join(process.env.HOME || '', '.claude', 'skills'),
|
|
30
|
+
path.join(process.env.HOME || '', '.codex', 'skills'),
|
|
31
|
+
path.join(process.env.HOME || '', '.cursor', 'skills'),
|
|
32
|
+
path.join(process.env.HOME || '', '.gemini', 'antigravity', 'skills'),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ensure the kdna-loader skill is installed in ALL detected agent directories.
|
|
37
|
+
* Without this, installed KDNA domains are invisible to agents.
|
|
38
|
+
*/
|
|
39
|
+
function ensureLoaderSkill() {
|
|
40
|
+
const alreadyInstalled = [];
|
|
41
|
+
const toInstall = [];
|
|
42
|
+
const toUpdate = []; // present but outdated (pre-v2.1)
|
|
43
|
+
|
|
44
|
+
// v2.1 marker — present in current SKILL.md, absent in old one
|
|
45
|
+
const V2_1_MARKER = 'applies_when';
|
|
46
|
+
|
|
47
|
+
for (const dir of AGENT_SKILL_DIRS) {
|
|
48
|
+
const skillFile = path.join(dir, 'kdna-loader', 'SKILL.md');
|
|
49
|
+
if (fs.existsSync(skillFile)) {
|
|
50
|
+
let isCurrent = false;
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
53
|
+
isCurrent = content.includes(V2_1_MARKER);
|
|
54
|
+
} catch {
|
|
55
|
+
/* unreadable — treat as missing */
|
|
56
|
+
}
|
|
57
|
+
if (isCurrent) alreadyInstalled.push(dir);
|
|
58
|
+
else toUpdate.push(dir);
|
|
59
|
+
} else {
|
|
60
|
+
toInstall.push(dir);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If all up-to-date, nothing to do
|
|
65
|
+
if (toInstall.length === 0 && toUpdate.length === 0) return;
|
|
66
|
+
|
|
67
|
+
// Notify which are current
|
|
68
|
+
if (alreadyInstalled.length > 0) {
|
|
69
|
+
console.log(
|
|
70
|
+
` ✓ kdna-loader (v2.1) found in: ${alreadyInstalled.map((d) => path.basename(path.dirname(d))).join(', ')}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Install + update share the same target list
|
|
75
|
+
const targets = [...toInstall, ...toUpdate];
|
|
76
|
+
const verb =
|
|
77
|
+
toUpdate.length && !toInstall.length
|
|
78
|
+
? 'Updating'
|
|
79
|
+
: toInstall.length && !toUpdate.length
|
|
80
|
+
? 'Installing'
|
|
81
|
+
: 'Installing/updating';
|
|
82
|
+
console.log(` ${verb} kdna-loader skill (v2.1)...`);
|
|
83
|
+
|
|
84
|
+
let installed = 0;
|
|
85
|
+
const sources = [];
|
|
86
|
+
|
|
87
|
+
// Source 1: download from kdna-skills repo (single source of truth, v0.7.4+).
|
|
88
|
+
// This must come FIRST so we don't ship stale local copies to users.
|
|
89
|
+
sources.push({
|
|
90
|
+
type: 'remote',
|
|
91
|
+
url: 'https://raw.githubusercontent.com/knowledge-dna/kdna-skills/main/kdna-loader/SKILL.md',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Source 2: offline fallback — KDNA repo local checkout, only used if the
|
|
95
|
+
// CDN is unreachable. The npm-published tarball does NOT include SKILL.md
|
|
96
|
+
// files anymore (they live solely in kdna-skills).
|
|
97
|
+
const localTemplate = path.resolve(__dirname, '..', 'skills', 'kdna-loader', 'SKILL.md');
|
|
98
|
+
if (fs.existsSync(localTemplate)) {
|
|
99
|
+
sources.push({ type: 'local', path: localTemplate });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const dir of targets) {
|
|
103
|
+
const skillDir = path.join(dir, 'kdna-loader');
|
|
104
|
+
for (const src of sources) {
|
|
105
|
+
try {
|
|
106
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
107
|
+
if (src.type === 'local') {
|
|
108
|
+
fs.copyFileSync(src.path, path.join(skillDir, 'SKILL.md'));
|
|
109
|
+
} else {
|
|
110
|
+
execSync(`curl -fsSL -o "${path.join(skillDir, 'SKILL.md')}" "${src.url}"`, {
|
|
111
|
+
stdio: 'pipe',
|
|
112
|
+
timeout: 10000,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
installed++;
|
|
116
|
+
break; // Move to next agent dir
|
|
117
|
+
} catch {
|
|
118
|
+
// Try next source
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (installed > 0) {
|
|
124
|
+
console.log(
|
|
125
|
+
` ✓ kdna-loader installed/updated in ${installed} agent director${installed > 1 ? 'ies' : 'y'}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (installed < targets.length) {
|
|
130
|
+
console.log(
|
|
131
|
+
` ⚠ Could not install to ${targets.length - installed} agent director${targets.length - installed > 1 ? 'ies' : 'y'}.`,
|
|
132
|
+
);
|
|
133
|
+
console.log(' Run: kdna setup --force');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (installed === 0 && alreadyInstalled.length === 0) {
|
|
137
|
+
console.log(' ⚠ Could not install kdna-loader anywhere.');
|
|
138
|
+
console.log(' Run: kdna setup');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function error(msg) {
|
|
143
|
+
console.error(`Error: ${msg}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ensureDir(dir) {
|
|
148
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readJson(p) {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function sha256File(filePath) {
|
|
160
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function scopeDir(scope) {
|
|
164
|
+
return path.join(INSTALL_DIR, scope);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function domainDir(scope, ident) {
|
|
168
|
+
return path.join(INSTALL_DIR, scope, ident);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Legacy detection ───────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
function detectLegacyInstalls() {
|
|
174
|
+
if (!fs.existsSync(INSTALL_DIR)) return [];
|
|
175
|
+
const entries = fs.readdirSync(INSTALL_DIR);
|
|
176
|
+
// Legacy: any direct child of INSTALL_DIR that is a directory AND does NOT start with @
|
|
177
|
+
return entries.filter((e) => {
|
|
178
|
+
if (e.startsWith('@') || e.startsWith('.')) return false;
|
|
179
|
+
try {
|
|
180
|
+
return fs.statSync(path.join(INSTALL_DIR, e)).isDirectory();
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function warnLegacy() {
|
|
188
|
+
const legacy = detectLegacyInstalls();
|
|
189
|
+
if (!legacy.length) return;
|
|
190
|
+
console.error('');
|
|
191
|
+
console.error('═'.repeat(64));
|
|
192
|
+
console.error(' v0.7 breaking change: legacy (un-scoped) domains detected');
|
|
193
|
+
console.error('═'.repeat(64));
|
|
194
|
+
console.error('');
|
|
195
|
+
console.error(' These directories use the old un-scoped path layout:');
|
|
196
|
+
legacy.forEach((d) => console.error(` ~/.kdna/domains/${d}/`));
|
|
197
|
+
console.error('');
|
|
198
|
+
console.error(' Run: kdna remove <name> then kdna install <name>');
|
|
199
|
+
console.error(' (CLI will not read or update legacy directories.)');
|
|
200
|
+
console.error('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Source parsing ─────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
function parseSource(input) {
|
|
206
|
+
// Local file
|
|
207
|
+
if (
|
|
208
|
+
input.endsWith('.kdna') &&
|
|
209
|
+
(input.startsWith('./') || input.startsWith('/') || input.startsWith('~/'))
|
|
210
|
+
) {
|
|
211
|
+
const resolved = path.resolve(input.replace(/^~/, process.env.HOME || ''));
|
|
212
|
+
if (!fs.existsSync(resolved)) error(`Local file not found: ${resolved}`);
|
|
213
|
+
return { type: 'local-file', path: resolved };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Local directory
|
|
217
|
+
if (input.startsWith('./') || input.startsWith('/') || input.startsWith('~/')) {
|
|
218
|
+
const resolved = path.resolve(input.replace(/^~/, process.env.HOME || ''));
|
|
219
|
+
if (!fs.existsSync(resolved)) error(`Local path not found: ${resolved}`);
|
|
220
|
+
if (!fs.statSync(resolved).isDirectory()) error(`Not a directory: ${resolved}`);
|
|
221
|
+
return { type: 'local-dir', path: resolved };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Registry name (bare or @scope/name)
|
|
225
|
+
const parsed = parseName(input);
|
|
226
|
+
if (!parsed) {
|
|
227
|
+
error(
|
|
228
|
+
`Cannot parse "${input}". Use:\n` +
|
|
229
|
+
` kdna install <name> # @aikdna/<name>\n` +
|
|
230
|
+
` kdna install @scope/name # any scope\n` +
|
|
231
|
+
` kdna install ./folder # local directory\n` +
|
|
232
|
+
` kdna install ./file.kdna # local .kdna file`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return { type: 'registry', parsed };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Download helpers ──────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
function downloadFile(url, dest) {
|
|
241
|
+
ensureDir(path.dirname(dest));
|
|
242
|
+
let lastErr;
|
|
243
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
244
|
+
try {
|
|
245
|
+
execFileSync('curl', ['-fsSL', '--retry', '2', '--retry-delay', '1', '-o', dest, url], {
|
|
246
|
+
timeout: 90000,
|
|
247
|
+
stdio: 'pipe',
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
} catch (e) {
|
|
251
|
+
lastErr = e;
|
|
252
|
+
if (attempt < 3) {
|
|
253
|
+
// brief pause between attempts
|
|
254
|
+
try {
|
|
255
|
+
execFileSync('sleep', ['1'], { stdio: 'ignore' });
|
|
256
|
+
} catch {
|
|
257
|
+
/* ignore */
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const stderr = lastErr?.stderr?.toString().trim() || lastErr?.message || 'unknown';
|
|
263
|
+
throw new Error(`download failed after 3 attempts: ${stderr}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ─── Extraction ────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
function extractKdna(kdnaPath, destDir) {
|
|
269
|
+
ensureDir(destDir);
|
|
270
|
+
const script = `import zipfile
|
|
271
|
+
zf = zipfile.ZipFile(${JSON.stringify(kdnaPath)}, 'r')
|
|
272
|
+
zf.extractall(${JSON.stringify(destDir)})
|
|
273
|
+
zf.close()
|
|
274
|
+
print('ok')
|
|
275
|
+
`;
|
|
276
|
+
try {
|
|
277
|
+
execSync(`python3 -c ${JSON.stringify(script)}`, { stdio: 'pipe' });
|
|
278
|
+
return;
|
|
279
|
+
} catch {
|
|
280
|
+
/* try unzip */
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
execSync(`unzip -q -o "${kdnaPath}" -d "${destDir}"`, { stdio: 'pipe' });
|
|
284
|
+
return;
|
|
285
|
+
} catch {
|
|
286
|
+
error('Cannot extract .kdna file. Install python3 or unzip.');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Signature verification ────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
function verifySignature({ destDir, scope, entry, lenient = true }) {
|
|
293
|
+
const manifest = readJson(path.join(destDir, 'kdna.json'));
|
|
294
|
+
if (!manifest) {
|
|
295
|
+
if (lenient) {
|
|
296
|
+
console.warn(' ⚠ No kdna.json — cannot verify signature.');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
error('No kdna.json in package — cannot verify signature.');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const trustKey = scope.trust_pubkey;
|
|
303
|
+
const isPlaceholder = !trustKey || trustKey.includes('PLACEHOLDER');
|
|
304
|
+
|
|
305
|
+
// v0.7 bootstrap: signatures may be absent. Warn but allow.
|
|
306
|
+
if (!entry.signature || !manifest.signature) {
|
|
307
|
+
if (isPlaceholder) {
|
|
308
|
+
console.warn(
|
|
309
|
+
` ⚠ Bootstrap mode: scope ${entry.name.split('/')[0]} has placeholder trust key. Signature not verified.`,
|
|
310
|
+
);
|
|
311
|
+
} else {
|
|
312
|
+
console.warn(
|
|
313
|
+
` ⚠ ${entry.name}: no signature on package. (Will be required post-bootstrap.)`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Author pubkey fingerprint must match scope trust_pubkey
|
|
320
|
+
if (manifest.author?.pubkey !== trustKey) {
|
|
321
|
+
error(`${entry.name}: author.pubkey does not match scope trust key. Refusing to install.`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Full Ed25519 verify (requires public_key_pem embedded in the package)
|
|
325
|
+
const pem = manifest.author?.public_key_pem;
|
|
326
|
+
if (!pem) {
|
|
327
|
+
// Legacy package (signed but no embedded PEM). Trust the fingerprint match.
|
|
328
|
+
console.log(' ✓ Signature OK (legacy fingerprint-only mode — no PEM)');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 1. Confirm the embedded PEM hashes to the claimed pubkey fingerprint
|
|
333
|
+
const computedFingerprint = 'ed25519:' + crypto.createHash('sha256').update(pem).digest('hex');
|
|
334
|
+
if (computedFingerprint !== manifest.author.pubkey) {
|
|
335
|
+
error(
|
|
336
|
+
`${entry.name}: embedded public_key_pem does not match author.pubkey fingerprint. Refusing.`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 2. Verify the Ed25519 signature over the canonical payload
|
|
341
|
+
// Canonical payload reconstruction must match publish.js exactly:
|
|
342
|
+
// - sorted .json filenames
|
|
343
|
+
// - for kdna.json: strip "signature" field before hashing
|
|
344
|
+
// - others: raw bytes
|
|
345
|
+
// - hash each, format "name:hex", join with "\n"
|
|
346
|
+
const sigHex = manifest.signature.replace(/^ed25519:/, '');
|
|
347
|
+
try {
|
|
348
|
+
const files = fs
|
|
349
|
+
.readdirSync(destDir)
|
|
350
|
+
.filter((f) => f.endsWith('.json'))
|
|
351
|
+
.sort();
|
|
352
|
+
const parts = [];
|
|
353
|
+
for (const f of files) {
|
|
354
|
+
const full = path.join(destDir, f);
|
|
355
|
+
let buf;
|
|
356
|
+
if (f === 'kdna.json') {
|
|
357
|
+
const obj = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
358
|
+
delete obj.signature;
|
|
359
|
+
delete obj._source; // install-time metadata, not part of signed payload
|
|
360
|
+
buf = Buffer.from(JSON.stringify(obj));
|
|
361
|
+
} else {
|
|
362
|
+
buf = fs.readFileSync(full);
|
|
363
|
+
}
|
|
364
|
+
const hash = crypto.createHash('sha256').update(buf).digest('hex');
|
|
365
|
+
parts.push(`${f}:${hash}`);
|
|
366
|
+
}
|
|
367
|
+
const payload = parts.join('\n');
|
|
368
|
+
|
|
369
|
+
const publicKey = crypto.createPublicKey(pem);
|
|
370
|
+
const ok = crypto.verify(null, Buffer.from(payload), publicKey, Buffer.from(sigHex, 'hex'));
|
|
371
|
+
if (!ok) {
|
|
372
|
+
error(`${entry.name}: Ed25519 signature INVALID. Package may be tampered. Refusing.`);
|
|
373
|
+
}
|
|
374
|
+
console.log(' ✓ Signature OK (Ed25519 verified)');
|
|
375
|
+
} catch (e) {
|
|
376
|
+
if (e.message?.includes('INVALID')) throw e;
|
|
377
|
+
error(`${entry.name}: signature verification failed: ${e.message}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ─── Status confirmation (interactive) ─────────────────────────────────
|
|
382
|
+
|
|
383
|
+
function confirmStatus(entry, yes) {
|
|
384
|
+
const status = entry.status || 'experimental';
|
|
385
|
+
if (yes || (status !== 'experimental' && status !== 'draft')) return true;
|
|
386
|
+
|
|
387
|
+
console.log(` ${entry.name} is ${status} — judgment quality is not yet verified.`);
|
|
388
|
+
console.log(` Pass --yes to skip this prompt.`);
|
|
389
|
+
try {
|
|
390
|
+
const buf = Buffer.alloc(1);
|
|
391
|
+
process.stdout.write('Continue? [y/N] ');
|
|
392
|
+
fs.readSync(0, buf, 0, 1);
|
|
393
|
+
return buf.toString().trim().toLowerCase() === 'y';
|
|
394
|
+
} catch {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── Cleanup stale temps ───────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
function cleanStaleTemps() {
|
|
402
|
+
if (!fs.existsSync(INSTALL_DIR)) return;
|
|
403
|
+
try {
|
|
404
|
+
for (const scopeName of fs.readdirSync(INSTALL_DIR)) {
|
|
405
|
+
if (!scopeName.startsWith('@')) continue;
|
|
406
|
+
const sd = path.join(INSTALL_DIR, scopeName);
|
|
407
|
+
if (!fs.statSync(sd).isDirectory()) continue;
|
|
408
|
+
for (const child of fs.readdirSync(sd)) {
|
|
409
|
+
if (child.endsWith('.tmp') || child.endsWith('.kdna.tmp')) {
|
|
410
|
+
try {
|
|
411
|
+
fs.rmSync(path.join(sd, child), { recursive: true, force: true });
|
|
412
|
+
} catch {
|
|
413
|
+
/* ignore */
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
/* ignore */
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ─── Main install ──────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
function cmdInstallExtended(input, args = []) {
|
|
426
|
+
warnLegacy();
|
|
427
|
+
ensureDir(INSTALL_DIR);
|
|
428
|
+
cleanStaleTemps();
|
|
429
|
+
|
|
430
|
+
// Auto-install loader skill if missing (without it, agents can't see installed domains)
|
|
431
|
+
ensureLoaderSkill();
|
|
432
|
+
|
|
433
|
+
const yes = args.includes('--yes');
|
|
434
|
+
const source = parseSource(input);
|
|
435
|
+
|
|
436
|
+
switch (source.type) {
|
|
437
|
+
case 'registry':
|
|
438
|
+
return installFromRegistry(source.parsed, yes);
|
|
439
|
+
case 'local-file':
|
|
440
|
+
return installFromLocalFile(source.path, yes);
|
|
441
|
+
case 'local-dir':
|
|
442
|
+
return installFromLocalDir(source.path, yes);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function installFromRegistry(parsed, yes) {
|
|
447
|
+
const resolver = new RegistryResolver({ allowNetwork: true });
|
|
448
|
+
let scope, entry;
|
|
449
|
+
try {
|
|
450
|
+
({ scope, entry } = resolver.resolve(parsed.full));
|
|
451
|
+
} catch (e) {
|
|
452
|
+
error(e.message);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (parsed.wasShort) {
|
|
456
|
+
console.log(` Resolved "${parsed.ident}" → ${entry.name}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (entry.deprecated) {
|
|
460
|
+
console.warn(
|
|
461
|
+
` ⚠ ${entry.name} is deprecated.${entry.replaced_by ? ` Use ${entry.replaced_by} instead.` : ''}`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (entry.access && entry.access !== 'open') {
|
|
465
|
+
error(`${entry.name} requires "${entry.access}" access. Not installable via CLI yet.`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (entry.type === 'cluster') {
|
|
469
|
+
return installCluster(entry, resolver, yes);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!entry.kdna_url) {
|
|
473
|
+
error(
|
|
474
|
+
`${entry.name}@${entry.version} has no kdna_url in registry.\n` +
|
|
475
|
+
`release_status: ${entry.release_status || 'unknown'}\n` +
|
|
476
|
+
`(This domain has not been published as a .kdna file yet. It will be available after v0.7 republish.)`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!confirmStatus(entry, yes)) {
|
|
481
|
+
console.log('Installation cancelled.');
|
|
482
|
+
process.exit(0);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
installSingleFromUrl({ entry, scope });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function installSingleFromUrl({ entry, scope }) {
|
|
489
|
+
const [scopeName, ident] = entry.name.split('/');
|
|
490
|
+
const dest = domainDir(scopeName, ident);
|
|
491
|
+
const tmpFile = path.join(scopeDir(scopeName), `.${ident}-${Date.now()}.kdna.tmp`);
|
|
492
|
+
|
|
493
|
+
console.log(` Downloading ${entry.name}@${entry.version}...`);
|
|
494
|
+
ensureDir(scopeDir(scopeName));
|
|
495
|
+
try {
|
|
496
|
+
downloadFile(entry.kdna_url, tmpFile);
|
|
497
|
+
} catch {
|
|
498
|
+
error(`Failed to download ${entry.kdna_url}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// sha256 check
|
|
502
|
+
const actual = sha256File(tmpFile);
|
|
503
|
+
if (entry.sha256 && actual !== entry.sha256) {
|
|
504
|
+
try {
|
|
505
|
+
fs.unlinkSync(tmpFile);
|
|
506
|
+
} catch {
|
|
507
|
+
/* ignore */
|
|
508
|
+
}
|
|
509
|
+
error(`sha256 mismatch for ${entry.name}: expected ${entry.sha256}, got ${actual}`);
|
|
510
|
+
}
|
|
511
|
+
console.log(` ✓ sha256 verified`);
|
|
512
|
+
|
|
513
|
+
// Replace existing install atomically-ish
|
|
514
|
+
if (fs.existsSync(dest)) {
|
|
515
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
516
|
+
}
|
|
517
|
+
ensureDir(dest);
|
|
518
|
+
|
|
519
|
+
extractKdna(tmpFile, dest);
|
|
520
|
+
try {
|
|
521
|
+
fs.unlinkSync(tmpFile);
|
|
522
|
+
} catch {
|
|
523
|
+
/* ignore */
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
verifySignature({ destDir: dest, scope, entry, lenient: true });
|
|
527
|
+
|
|
528
|
+
// Stamp install metadata
|
|
529
|
+
const manifest = readJson(path.join(dest, 'kdna.json')) || {};
|
|
530
|
+
manifest._source = {
|
|
531
|
+
type: 'registry',
|
|
532
|
+
name: entry.name,
|
|
533
|
+
version: entry.version,
|
|
534
|
+
kdna_url: entry.kdna_url,
|
|
535
|
+
sha256: entry.sha256,
|
|
536
|
+
installed_at: new Date().toISOString(),
|
|
537
|
+
};
|
|
538
|
+
fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
539
|
+
|
|
540
|
+
console.log(`✓ Installed ${entry.name}@${entry.version}`);
|
|
541
|
+
console.log(` Location: ${dest}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function installCluster(clusterEntry, resolver, _yes) {
|
|
545
|
+
const subdomains = clusterEntry.cluster?.domains || [];
|
|
546
|
+
if (!subdomains.length) {
|
|
547
|
+
error(`Cluster ${clusterEntry.name} has no sub-domains listed.`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
console.log(`Cluster ${clusterEntry.name} → ${subdomains.length} sub-domains`);
|
|
551
|
+
|
|
552
|
+
for (const sub of subdomains) {
|
|
553
|
+
try {
|
|
554
|
+
const resolved = resolver.resolve(sub);
|
|
555
|
+
if (!resolved.entry.kdna_url) {
|
|
556
|
+
console.warn(` ⚠ ${sub}: no kdna_url (skipping)`);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
console.log('');
|
|
560
|
+
installSingleFromUrl({ entry: resolved.entry, scope: resolved.scope });
|
|
561
|
+
} catch (e) {
|
|
562
|
+
console.warn(` ⚠ ${sub}: ${e.message.split('\n')[0]}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Record the cluster itself
|
|
567
|
+
const [scopeName, ident] = clusterEntry.name.split('/');
|
|
568
|
+
const clusterDest = domainDir(scopeName, ident);
|
|
569
|
+
ensureDir(clusterDest);
|
|
570
|
+
fs.writeFileSync(
|
|
571
|
+
path.join(clusterDest, 'cluster.json'),
|
|
572
|
+
JSON.stringify(
|
|
573
|
+
{
|
|
574
|
+
name: clusterEntry.name,
|
|
575
|
+
version: clusterEntry.version,
|
|
576
|
+
type: 'cluster',
|
|
577
|
+
domains: subdomains,
|
|
578
|
+
composition_rules: clusterEntry.cluster.composition_rules || [],
|
|
579
|
+
installed_at: new Date().toISOString(),
|
|
580
|
+
},
|
|
581
|
+
null,
|
|
582
|
+
2,
|
|
583
|
+
) + '\n',
|
|
584
|
+
);
|
|
585
|
+
console.log('');
|
|
586
|
+
console.log(`✓ Cluster ${clusterEntry.name} installed`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function installFromLocalFile(filePath, _yes) {
|
|
590
|
+
const abs = path.resolve(filePath);
|
|
591
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) error(`Not a file: ${abs}`);
|
|
592
|
+
|
|
593
|
+
const tmpDir = path.join(INSTALL_DIR, '.local-tmp-' + Date.now());
|
|
594
|
+
ensureDir(tmpDir);
|
|
595
|
+
extractKdna(abs, tmpDir);
|
|
596
|
+
|
|
597
|
+
const manifest = readJson(path.join(tmpDir, 'kdna.json'));
|
|
598
|
+
const declared = manifest?.name;
|
|
599
|
+
if (!declared || !/^@[a-z][a-z0-9-]*\/[a-z][a-z0-9_]*$/.test(declared)) {
|
|
600
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
601
|
+
error(
|
|
602
|
+
`Package kdna.json.name "${declared || '?'}" must be @scope/name format.\n` +
|
|
603
|
+
`(v0.7 requires scoped names.)`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
const [scopeName, ident] = declared.split('/');
|
|
607
|
+
const dest = domainDir(scopeName, ident);
|
|
608
|
+
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
609
|
+
ensureDir(path.dirname(dest));
|
|
610
|
+
fs.renameSync(tmpDir, dest);
|
|
611
|
+
|
|
612
|
+
const destManifest = readJson(path.join(dest, 'kdna.json')) || {};
|
|
613
|
+
destManifest._source = {
|
|
614
|
+
type: 'local-file',
|
|
615
|
+
path: abs,
|
|
616
|
+
installed_at: new Date().toISOString(),
|
|
617
|
+
};
|
|
618
|
+
fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(destManifest, null, 2) + '\n');
|
|
619
|
+
|
|
620
|
+
console.log(`✓ Installed ${declared} from local file`);
|
|
621
|
+
console.log(` Location: ${dest}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function installFromLocalDir(dirPath, _yes) {
|
|
625
|
+
const abs = path.resolve(dirPath);
|
|
626
|
+
const manifest = readJson(path.join(abs, 'kdna.json'));
|
|
627
|
+
const declared = manifest?.name;
|
|
628
|
+
if (!declared || !/^@[a-z][a-z0-9-]*\/[a-z][a-z0-9_]*$/.test(declared)) {
|
|
629
|
+
error(`Source kdna.json.name "${declared || '?'}" must be @scope/name format.`);
|
|
630
|
+
}
|
|
631
|
+
const [scopeName, ident] = declared.split('/');
|
|
632
|
+
const dest = domainDir(scopeName, ident);
|
|
633
|
+
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
634
|
+
ensureDir(path.dirname(dest));
|
|
635
|
+
fs.cpSync(abs, dest, { recursive: true });
|
|
636
|
+
|
|
637
|
+
const destManifest = readJson(path.join(dest, 'kdna.json')) || {};
|
|
638
|
+
destManifest._source = {
|
|
639
|
+
type: 'local-dir',
|
|
640
|
+
path: abs,
|
|
641
|
+
installed_at: new Date().toISOString(),
|
|
642
|
+
};
|
|
643
|
+
fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(destManifest, null, 2) + '\n');
|
|
644
|
+
|
|
645
|
+
console.log(`✓ Installed ${declared} from local directory (dev mode)`);
|
|
646
|
+
console.log(` Location: ${dest}`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ─── Remove ─────────────────────────────────────────────────────────────
|
|
650
|
+
|
|
651
|
+
function cmdRemove(input) {
|
|
652
|
+
warnLegacy();
|
|
653
|
+
const parsed = parseName(input);
|
|
654
|
+
if (!parsed) error(`Invalid name "${input}". Use @scope/name or bare name.`);
|
|
655
|
+
const dest = domainDir(parsed.scope, parsed.ident);
|
|
656
|
+
if (!fs.existsSync(dest)) {
|
|
657
|
+
console.log(`${parsed.full} is not installed.`);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
661
|
+
console.log(`✓ Removed ${parsed.full}`);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ─── Info ───────────────────────────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
function cmdInfo(input) {
|
|
667
|
+
warnLegacy();
|
|
668
|
+
const parsed = parseName(input);
|
|
669
|
+
if (!parsed) error(`Invalid name "${input}".`);
|
|
670
|
+
const dest = domainDir(parsed.scope, parsed.ident);
|
|
671
|
+
if (!fs.existsSync(dest)) error(`${parsed.full} is not installed.`);
|
|
672
|
+
|
|
673
|
+
const manifest = readJson(path.join(dest, 'kdna.json'));
|
|
674
|
+
const core = readJson(path.join(dest, 'KDNA_Core.json'));
|
|
675
|
+
const pat = readJson(path.join(dest, 'KDNA_Patterns.json'));
|
|
676
|
+
const source = manifest?._source || {};
|
|
677
|
+
|
|
678
|
+
// ─── Header ─────────────────────────────────────────────────────
|
|
679
|
+
console.log('═'.repeat(64));
|
|
680
|
+
console.log(` ${parsed.full}`);
|
|
681
|
+
console.log('═'.repeat(64));
|
|
682
|
+
console.log(` Version: ${manifest?.version || core?.meta?.version || '?'}`);
|
|
683
|
+
if (manifest?.judgment_version) {
|
|
684
|
+
console.log(` Judgment version: ${manifest.judgment_version}`);
|
|
685
|
+
}
|
|
686
|
+
console.log(` Status: ${manifest?.status || '?'}`);
|
|
687
|
+
console.log(` License: ${manifest?.license?.type || '?'}`);
|
|
688
|
+
console.log(` Author: ${manifest?.author?.name || '?'}`);
|
|
689
|
+
|
|
690
|
+
// ─── Identity & trust ──────────────────────────────────────────
|
|
691
|
+
console.log('');
|
|
692
|
+
console.log(' ── Identity & trust ──');
|
|
693
|
+
if (manifest?.author?.pubkey) {
|
|
694
|
+
console.log(` Author pubkey: ${manifest.author.pubkey.slice(0, 28)}…`);
|
|
695
|
+
}
|
|
696
|
+
if (manifest?.author?.public_key_pem) {
|
|
697
|
+
console.log(` Embedded PEM: yes (full Ed25519 verify available)`);
|
|
698
|
+
} else {
|
|
699
|
+
console.log(` Embedded PEM: no (legacy pre-v0.7.1 package)`);
|
|
700
|
+
}
|
|
701
|
+
if (source.kdna_url) console.log(` Source URL: ${source.kdna_url}`);
|
|
702
|
+
if (source.sha256) console.log(` Source sha256: ${source.sha256.slice(0, 32)}…`);
|
|
703
|
+
console.log(` Installed: ${source.installed_at || '?'}`);
|
|
704
|
+
console.log(` Path: ${dest}`);
|
|
705
|
+
|
|
706
|
+
// ─── Judgment surface ──────────────────────────────────────────
|
|
707
|
+
console.log('');
|
|
708
|
+
console.log(' ── Judgment surface ──');
|
|
709
|
+
const axiomCount = (core?.axioms || []).length;
|
|
710
|
+
const ontologyCount = (core?.ontology || []).length;
|
|
711
|
+
const stanceCount = (core?.stances || []).length;
|
|
712
|
+
const misCount = (pat?.misunderstandings || []).length;
|
|
713
|
+
const selfCheckCount = (pat?.self_check || []).length;
|
|
714
|
+
console.log(` Axioms: ${axiomCount}`);
|
|
715
|
+
console.log(` Ontology: ${ontologyCount}`);
|
|
716
|
+
console.log(` Stances: ${stanceCount}`);
|
|
717
|
+
console.log(` Misunderstandings: ${misCount}`);
|
|
718
|
+
console.log(` Self-checks: ${selfCheckCount}`);
|
|
719
|
+
|
|
720
|
+
// ─── v2.1 governance score ─────────────────────────────────────
|
|
721
|
+
if (axiomCount > 0) {
|
|
722
|
+
const withApplies = (core?.axioms || []).filter(
|
|
723
|
+
(a) => Array.isArray(a.applies_when) && a.applies_when.length,
|
|
724
|
+
).length;
|
|
725
|
+
const withDoesNotApply = (core?.axioms || []).filter(
|
|
726
|
+
(a) => Array.isArray(a.does_not_apply_when) && a.does_not_apply_when.length,
|
|
727
|
+
).length;
|
|
728
|
+
const withFailureRisk = (core?.axioms || []).filter((a) => a.failure_risk).length;
|
|
729
|
+
const pct = Math.round(
|
|
730
|
+
((withApplies + withDoesNotApply + withFailureRisk) / (axiomCount * 3)) * 100,
|
|
731
|
+
);
|
|
732
|
+
console.log('');
|
|
733
|
+
console.log(' ── v2.1 governance ──');
|
|
734
|
+
console.log(` axioms with applies_when: ${withApplies}/${axiomCount}`);
|
|
735
|
+
console.log(` axioms with does_not_apply: ${withDoesNotApply}/${axiomCount}`);
|
|
736
|
+
console.log(` axioms with failure_risk: ${withFailureRisk}/${axiomCount}`);
|
|
737
|
+
console.log(` governance coverage: ${pct}%`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ─── Eval cases ────────────────────────────────────────────────
|
|
741
|
+
const evalDir = path.join(dest, 'evals');
|
|
742
|
+
if (fs.existsSync(evalDir)) {
|
|
743
|
+
const evalFiles = fs.readdirSync(evalDir).filter((f) => f.endsWith('.json'));
|
|
744
|
+
let totalCases = 0;
|
|
745
|
+
for (const f of evalFiles) {
|
|
746
|
+
const data = readJson(path.join(evalDir, f));
|
|
747
|
+
if (data?.cases) totalCases += data.cases.length;
|
|
748
|
+
}
|
|
749
|
+
console.log('');
|
|
750
|
+
console.log(' ── Eval cases ──');
|
|
751
|
+
console.log(` Files: ${evalFiles.length}`);
|
|
752
|
+
console.log(` Total cases: ${totalCases}`);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ─── Known risks (from kdna.json or axioms) ────────────────────
|
|
756
|
+
const risks = [];
|
|
757
|
+
if (core?.axioms) {
|
|
758
|
+
for (const a of core.axioms) {
|
|
759
|
+
if (a.failure_risk) risks.push({ source: a.id, text: a.failure_risk });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (risks.length) {
|
|
763
|
+
console.log('');
|
|
764
|
+
console.log(' ── Known failure risks ──');
|
|
765
|
+
for (const r of risks.slice(0, 4)) {
|
|
766
|
+
const short = r.text.length > 110 ? r.text.slice(0, 107) + '…' : r.text;
|
|
767
|
+
console.log(` ⚠ [${r.source}]`);
|
|
768
|
+
console.log(` ${short}`);
|
|
769
|
+
}
|
|
770
|
+
if (risks.length > 4) console.log(` (+ ${risks.length - 4} more — see KDNA_Core.json)`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ─── Files ─────────────────────────────────────────────────────
|
|
774
|
+
const expected = [
|
|
775
|
+
'KDNA_Core.json',
|
|
776
|
+
'KDNA_Patterns.json',
|
|
777
|
+
'KDNA_Scenarios.json',
|
|
778
|
+
'KDNA_Cases.json',
|
|
779
|
+
'KDNA_Reasoning.json',
|
|
780
|
+
'KDNA_Evolution.json',
|
|
781
|
+
];
|
|
782
|
+
const present = expected.filter((f) => fs.existsSync(path.join(dest, f)));
|
|
783
|
+
console.log('');
|
|
784
|
+
console.log(` Files: ${present.length}/${expected.length} (${present.join(', ') || 'none'})`);
|
|
785
|
+
|
|
786
|
+
console.log('');
|
|
787
|
+
console.log(` Run 'kdna verify ${parsed.full}' for full structure/trust/judgment scoring.`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ─── Update ─────────────────────────────────────────────────────────────
|
|
791
|
+
|
|
792
|
+
function cmdUpdate(input) {
|
|
793
|
+
warnLegacy();
|
|
794
|
+
const parsed = parseName(input);
|
|
795
|
+
if (!parsed) error(`Invalid name "${input}".`);
|
|
796
|
+
const dest = domainDir(parsed.scope, parsed.ident);
|
|
797
|
+
if (!fs.existsSync(dest)) {
|
|
798
|
+
console.log(`${parsed.full} not installed. Run: kdna install ${input}`);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const manifest = readJson(path.join(dest, 'kdna.json')) || {};
|
|
802
|
+
const installedVersion = manifest.version || manifest._source?.version || '?';
|
|
803
|
+
|
|
804
|
+
const resolver = new RegistryResolver({ allowNetwork: true, refresh: true });
|
|
805
|
+
let entry;
|
|
806
|
+
try {
|
|
807
|
+
({ entry } = resolver.resolve(parsed.full));
|
|
808
|
+
} catch (e) {
|
|
809
|
+
error(e.message);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (entry.version === installedVersion) {
|
|
813
|
+
console.log(`${parsed.full}@${installedVersion} is up to date.`);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
console.log(`Updating ${parsed.full}: ${installedVersion} → ${entry.version}`);
|
|
817
|
+
cmdInstallExtended(parsed.full, ['--yes']);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function cmdUpdateAll() {
|
|
821
|
+
warnLegacy();
|
|
822
|
+
if (!fs.existsSync(INSTALL_DIR)) {
|
|
823
|
+
console.log('No installs.');
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const scopes = fs.readdirSync(INSTALL_DIR).filter((d) => d.startsWith('@'));
|
|
827
|
+
for (const scope of scopes) {
|
|
828
|
+
const sd = path.join(INSTALL_DIR, scope);
|
|
829
|
+
if (!fs.statSync(sd).isDirectory()) continue;
|
|
830
|
+
for (const ident of fs.readdirSync(sd)) {
|
|
831
|
+
if (ident.startsWith('.')) continue;
|
|
832
|
+
try {
|
|
833
|
+
cmdUpdate(`${scope}/${ident}`);
|
|
834
|
+
} catch (e) {
|
|
835
|
+
console.warn(` ⚠ ${scope}/${ident}: ${e.message.split('\n')[0]}`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
console.log('');
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
module.exports = {
|
|
843
|
+
cmdInstallExtended,
|
|
844
|
+
cmdRemove,
|
|
845
|
+
cmdInfo,
|
|
846
|
+
cmdUpdate,
|
|
847
|
+
cmdUpdateAll,
|
|
848
|
+
INSTALL_DIR,
|
|
849
|
+
};
|