@aikdna/kdna-cli 0.17.0 → 0.19.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/README.md +120 -101
- package/SECURITY.md +1 -1
- package/package.json +6 -4
- package/skills/kdna-loader/SKILL.md +23 -22
- package/src/agent.js +290 -159
- package/src/cli.js +117 -67
- package/src/cmds/_common.js +40 -18
- package/src/cmds/badge.js +14 -9
- package/src/cmds/changelog.js +32 -12
- package/src/cmds/cluster.js +80 -85
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +114 -427
- package/src/cmds/explain.js +119 -0
- package/src/cmds/governance.js +111 -42
- package/src/cmds/legacy.js +8 -9
- package/src/cmds/license.js +491 -26
- package/src/cmds/quality.js +10 -3
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +99 -47
- package/src/cmds/test.js +9 -6
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +41 -22
- package/src/diff.js +38 -24
- package/src/identity.js +9 -7
- package/src/init.js +2 -2
- package/src/install.js +147 -459
- package/src/loader.js +10 -10
- package/src/package-store.js +232 -0
- package/src/paths.js +44 -0
- package/src/publish.js +150 -51
- package/src/registry.js +81 -9
- package/src/setup.js +19 -20
- package/src/verify.js +293 -140
- package/src/version.js +15 -7
- package/templates/minimal-domain/kdna.json +7 -7
- package/templates/standard-domain/README.md +10 -10
- package/templates/standard-domain/kdna.json +7 -3
- package/validators/kdna-lint.js +45 -11
- package/src/cmds/encrypt.js +0 -199
package/src/install.js
CHANGED
|
@@ -5,31 +5,33 @@
|
|
|
5
5
|
* kdna install <bare> → @aikdna/<bare>, from registry
|
|
6
6
|
* kdna install @scope/name → from registry (any scope)
|
|
7
7
|
* kdna install @scope/name@1.2.3 → version pinned (TODO post-v0.7.0)
|
|
8
|
-
* kdna install ./folder → local directory (dev)
|
|
9
8
|
* kdna install ./file.kdna → local .kdna file
|
|
10
9
|
*
|
|
11
10
|
* Removed in v0.7 (breaking): github:user/repo, --from-git, cluster:github:...,
|
|
12
11
|
* tarball/SSH fallbacks. Install is now strictly .kdna-driven from the registry.
|
|
13
12
|
*
|
|
14
|
-
* Schema
|
|
13
|
+
* Schema v3.0 — see kdna-registry/SCHEMA.md
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
16
|
const fs = require('fs');
|
|
18
17
|
const path = require('path');
|
|
19
|
-
const crypto = require('crypto');
|
|
20
18
|
const { execSync, execFileSync } = require('child_process');
|
|
21
19
|
const { RegistryResolver, parseName } = require('./registry');
|
|
22
20
|
const { EXIT, error } = require('./cmds/_common');
|
|
21
|
+
const PATHS = require('./paths');
|
|
23
22
|
const {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
installAsset,
|
|
24
|
+
getInstalled,
|
|
25
|
+
listInstalled,
|
|
26
|
+
removeInstalled,
|
|
27
|
+
sha256File,
|
|
28
|
+
readContainer,
|
|
29
|
+
readContainerJson,
|
|
30
|
+
verifyAsset,
|
|
31
|
+
} = require('./package-store');
|
|
32
|
+
|
|
33
|
+
const USER_KDNA_DIR = PATHS.root;
|
|
34
|
+
const INSTALL_DIR = PATHS.packages;
|
|
33
35
|
|
|
34
36
|
// Agent skill directories (search order)
|
|
35
37
|
const AGENT_SKILL_DIRS = [
|
|
@@ -151,77 +153,33 @@ function ensureDir(dir) {
|
|
|
151
153
|
fs.mkdirSync(dir, { recursive: true });
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
function readJson(p) {
|
|
155
|
-
try {
|
|
156
|
-
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
157
|
-
} catch {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function sha256File(filePath) {
|
|
163
|
-
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function scopeDir(scope) {
|
|
167
|
-
return path.join(INSTALL_DIR, scope);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function domainDir(scope, ident) {
|
|
171
|
-
return path.join(INSTALL_DIR, scope, ident);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ─── Legacy detection ───────────────────────────────────────────────────
|
|
175
|
-
|
|
176
|
-
function detectLegacyInstalls() {
|
|
177
|
-
if (!fs.existsSync(INSTALL_DIR)) return [];
|
|
178
|
-
const entries = fs.readdirSync(INSTALL_DIR);
|
|
179
|
-
// Legacy: any direct child of INSTALL_DIR that is a directory AND does NOT start with @
|
|
180
|
-
return entries.filter((e) => {
|
|
181
|
-
if (e.startsWith('@') || e.startsWith('.')) return false;
|
|
182
|
-
try {
|
|
183
|
-
return fs.statSync(path.join(INSTALL_DIR, e)).isDirectory();
|
|
184
|
-
} catch {
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function warnLegacy() {
|
|
191
|
-
const legacy = detectLegacyInstalls();
|
|
192
|
-
if (!legacy.length) return;
|
|
193
|
-
console.error('');
|
|
194
|
-
console.error('═'.repeat(64));
|
|
195
|
-
console.error(' v0.7 breaking change: legacy (un-scoped) domains detected');
|
|
196
|
-
console.error('═'.repeat(64));
|
|
197
|
-
console.error('');
|
|
198
|
-
console.error(' These directories use the old un-scoped path layout:');
|
|
199
|
-
legacy.forEach((d) => console.error(` ~/.kdna/domains/${d}/`));
|
|
200
|
-
console.error('');
|
|
201
|
-
console.error(' Run: kdna remove <name> then kdna install <name>');
|
|
202
|
-
console.error(' (CLI will not read or update legacy directories.)');
|
|
203
|
-
console.error('');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
156
|
// ─── Source parsing ─────────────────────────────────────────────────────
|
|
207
157
|
|
|
208
158
|
function parseSource(input) {
|
|
209
|
-
// Local file (.kdna
|
|
159
|
+
// Local file (.kdna)
|
|
210
160
|
if (
|
|
211
|
-
|
|
212
|
-
(input.startsWith('./') ||
|
|
161
|
+
input.endsWith('.kdna') &&
|
|
162
|
+
(input.startsWith('./') ||
|
|
163
|
+
input.startsWith('/') ||
|
|
164
|
+
input.startsWith('~/') ||
|
|
165
|
+
fs.existsSync(input))
|
|
213
166
|
) {
|
|
214
167
|
const resolved = path.resolve(input.replace(/^~/, process.env.HOME || ''));
|
|
215
168
|
if (!fs.existsSync(resolved)) error(`Local file not found: ${resolved}`);
|
|
216
169
|
return { type: 'local-file', path: resolved };
|
|
217
170
|
}
|
|
218
171
|
|
|
219
|
-
// Local directory
|
|
220
172
|
if (input.startsWith('./') || input.startsWith('/') || input.startsWith('~/')) {
|
|
221
173
|
const resolved = path.resolve(input.replace(/^~/, process.env.HOME || ''));
|
|
222
174
|
if (!fs.existsSync(resolved)) error(`Local path not found: ${resolved}`);
|
|
223
|
-
if (
|
|
224
|
-
|
|
175
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
176
|
+
error(
|
|
177
|
+
`Directory install is not supported. KDNA installs .kdna assets only.\n` +
|
|
178
|
+
`Use: kdna dev pack ${resolved} --output <dir>, then kdna install <file.kdna>`,
|
|
179
|
+
EXIT.INPUT_ERROR,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
error(`Not a .kdna file: ${resolved}`, EXIT.INPUT_ERROR);
|
|
225
183
|
}
|
|
226
184
|
|
|
227
185
|
// Registry name (bare or @scope/name)
|
|
@@ -231,7 +189,6 @@ function parseSource(input) {
|
|
|
231
189
|
`Cannot parse "${input}". Use:\n` +
|
|
232
190
|
` kdna install <name> # @aikdna/<name>\n` +
|
|
233
191
|
` kdna install @scope/name # any scope\n` +
|
|
234
|
-
` kdna install ./folder # local directory\n` +
|
|
235
192
|
` kdna install ./file.kdna # local .kdna file`,
|
|
236
193
|
);
|
|
237
194
|
}
|
|
@@ -266,90 +223,10 @@ function downloadFile(url, dest) {
|
|
|
266
223
|
throw new Error(`download failed after 3 attempts: ${stderr}`);
|
|
267
224
|
}
|
|
268
225
|
|
|
269
|
-
// ─── Extraction ────────────────────────────────────────────────────────
|
|
270
|
-
|
|
271
|
-
function extractKdna(kdnaPath, destDir) {
|
|
272
|
-
ensureDir(destDir);
|
|
273
|
-
const script = `import zipfile
|
|
274
|
-
zf = zipfile.ZipFile(${JSON.stringify(kdnaPath)}, 'r')
|
|
275
|
-
zf.extractall(${JSON.stringify(destDir)})
|
|
276
|
-
zf.close()
|
|
277
|
-
print('ok')
|
|
278
|
-
`;
|
|
279
|
-
try {
|
|
280
|
-
execSync(`python3 -c ${JSON.stringify(script)}`, { stdio: 'pipe' });
|
|
281
|
-
return;
|
|
282
|
-
} catch {
|
|
283
|
-
/* try unzip */
|
|
284
|
-
}
|
|
285
|
-
try {
|
|
286
|
-
execSync(`unzip -q -o "${kdnaPath}" -d "${destDir}"`, { stdio: 'pipe' });
|
|
287
|
-
return;
|
|
288
|
-
} catch {
|
|
289
|
-
error('Cannot extract .kdna file. Install python3 or unzip.');
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function extractAndDecrypt(kdnaPath, destDir, licenseKey) {
|
|
294
|
-
extractKdna(kdnaPath, destDir);
|
|
295
|
-
const fp = machineFingerprint();
|
|
296
|
-
const key = deriveKey(licenseKey, fp);
|
|
297
|
-
|
|
298
|
-
for (const f of fs.readdirSync(destDir)) {
|
|
299
|
-
if (ENCRYPTED_FILES.includes(f)) {
|
|
300
|
-
try {
|
|
301
|
-
const fullPath = path.join(destDir, f);
|
|
302
|
-
const encrypted = fs.readFileSync(fullPath);
|
|
303
|
-
const decrypted = decrypt(encrypted, key);
|
|
304
|
-
fs.writeFileSync(fullPath, decrypted);
|
|
305
|
-
} catch (err) {
|
|
306
|
-
fs.rmSync(destDir, { recursive: true, force: true });
|
|
307
|
-
error(`Failed to decrypt ${f}: ${err.message}. Wrong license key?`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function findLicense(domainName) {
|
|
314
|
-
const licenseDir = path.join(USER_KDNA_DIR, 'licenses');
|
|
315
|
-
const licensePath = path.join(
|
|
316
|
-
licenseDir,
|
|
317
|
-
`${domainName.replace(/^@/, '').replace('/', '-')}.json`,
|
|
318
|
-
);
|
|
319
|
-
if (fs.existsSync(licensePath)) {
|
|
320
|
-
try {
|
|
321
|
-
return JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
322
|
-
} catch {
|
|
323
|
-
/* invalid license */
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function findLicenseForDomain(domainFull) {
|
|
330
|
-
const licenseDir = path.join(USER_KDNA_DIR, 'licenses');
|
|
331
|
-
if (!fs.existsSync(licenseDir)) return null;
|
|
332
|
-
// Try exact match: @aikdna/writing → @aikdna-writing.json
|
|
333
|
-
const candidates = [domainFull.replace(/^@/, '').replace('/', '-')];
|
|
334
|
-
// Also try just the domain name
|
|
335
|
-
candidates.push(domainFull.split('/').pop());
|
|
336
|
-
for (const c of candidates) {
|
|
337
|
-
const p = path.join(licenseDir, `${c}.json`);
|
|
338
|
-
if (fs.existsSync(p)) {
|
|
339
|
-
try {
|
|
340
|
-
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
341
|
-
} catch {
|
|
342
|
-
/* skip */
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
226
|
// ─── Signature verification ────────────────────────────────────────────
|
|
350
227
|
|
|
351
|
-
function verifySignature({
|
|
352
|
-
const manifest =
|
|
228
|
+
function verifySignature({ assetPath, scope, entry, lenient = true }) {
|
|
229
|
+
const manifest = readContainerJson(assetPath, 'kdna.json');
|
|
353
230
|
if (!manifest) {
|
|
354
231
|
if (lenient) {
|
|
355
232
|
console.warn(' ⚠ No kdna.json — cannot verify signature.');
|
|
@@ -359,20 +236,8 @@ function verifySignature({ destDir, scope, entry, lenient = true }) {
|
|
|
359
236
|
}
|
|
360
237
|
|
|
361
238
|
const trustKey = scope.trust_pubkey;
|
|
362
|
-
const isPlaceholder = !trustKey || trustKey.includes('PLACEHOLDER');
|
|
363
|
-
|
|
364
|
-
// v0.7 bootstrap: signatures may be absent. Warn but allow.
|
|
365
239
|
if (!entry.signature || !manifest.signature) {
|
|
366
|
-
|
|
367
|
-
console.warn(
|
|
368
|
-
` ⚠ Bootstrap mode: scope ${entry.name.split('/')[0]} has placeholder trust key. Signature not verified.`,
|
|
369
|
-
);
|
|
370
|
-
} else {
|
|
371
|
-
console.warn(
|
|
372
|
-
` ⚠ ${entry.name}: no signature on package. (Will be required post-bootstrap.)`,
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
return;
|
|
240
|
+
error(`${entry.name}: registry and .kdna manifest signatures are required.`, EXIT.TRUST_FAILED);
|
|
376
241
|
}
|
|
377
242
|
|
|
378
243
|
// Author pubkey fingerprint must match scope trust_pubkey
|
|
@@ -384,64 +249,26 @@ function verifySignature({ destDir, scope, entry, lenient = true }) {
|
|
|
384
249
|
}
|
|
385
250
|
|
|
386
251
|
// Full Ed25519 verify (requires public_key_pem embedded in the package)
|
|
387
|
-
|
|
388
|
-
if (!pem) {
|
|
389
|
-
// Legacy package (signed but no embedded PEM). Trust the fingerprint match.
|
|
390
|
-
console.log(' ✓ Signature OK (legacy fingerprint-only mode — no PEM)');
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// 1. Confirm the embedded PEM hashes to the claimed pubkey fingerprint
|
|
395
|
-
const computedFingerprint = 'ed25519:' + crypto.createHash('sha256').update(pem).digest('hex');
|
|
396
|
-
if (computedFingerprint !== manifest.author.pubkey) {
|
|
252
|
+
if (!manifest.author?.public_key_pem) {
|
|
397
253
|
error(
|
|
398
|
-
`${entry.name}:
|
|
254
|
+
`${entry.name}: manifest author.public_key_pem is required for Ed25519 verification.`,
|
|
399
255
|
EXIT.TRUST_FAILED,
|
|
400
256
|
);
|
|
401
257
|
}
|
|
402
258
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
// - others: raw bytes
|
|
408
|
-
// - hash each, format "name:hex", join with "\n"
|
|
409
|
-
const sigHex = manifest.signature.replace(/^ed25519:/, '');
|
|
410
|
-
try {
|
|
411
|
-
const files = fs
|
|
412
|
-
.readdirSync(destDir)
|
|
413
|
-
.filter((f) => f.endsWith('.json'))
|
|
414
|
-
.sort();
|
|
415
|
-
const parts = [];
|
|
416
|
-
for (const f of files) {
|
|
417
|
-
const full = path.join(destDir, f);
|
|
418
|
-
let buf;
|
|
419
|
-
if (f === 'kdna.json') {
|
|
420
|
-
const obj = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
421
|
-
delete obj.signature;
|
|
422
|
-
delete obj._source; // install-time metadata, not part of signed payload
|
|
423
|
-
buf = Buffer.from(JSON.stringify(obj));
|
|
424
|
-
} else {
|
|
425
|
-
buf = fs.readFileSync(full);
|
|
426
|
-
}
|
|
427
|
-
const hash = crypto.createHash('sha256').update(buf).digest('hex');
|
|
428
|
-
parts.push(`${f}:${hash}`);
|
|
259
|
+
const result = verifyAsset(assetPath, { requireSignature: true });
|
|
260
|
+
for (const e of result.errors || []) {
|
|
261
|
+
if (e.includes('signature') || e.includes('public_key') || e.includes('fingerprint')) {
|
|
262
|
+
error(`${entry.name}: ${e}. Refusing to install.`, EXIT.TRUST_FAILED);
|
|
429
263
|
}
|
|
430
|
-
const payload = parts.join('\n');
|
|
431
|
-
|
|
432
|
-
const publicKey = crypto.createPublicKey(pem);
|
|
433
|
-
const ok = crypto.verify(null, Buffer.from(payload), publicKey, Buffer.from(sigHex, 'hex'));
|
|
434
|
-
if (!ok) {
|
|
435
|
-
error(
|
|
436
|
-
`${entry.name}: Ed25519 signature INVALID. Package may be tampered. Refusing.`,
|
|
437
|
-
EXIT.TRUST_FAILED,
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
console.log(' ✓ Signature OK (Ed25519 verified)');
|
|
441
|
-
} catch (e) {
|
|
442
|
-
if (e.message?.includes('INVALID')) throw e;
|
|
443
|
-
error(`${entry.name}: signature verification failed: ${e.message}`, EXIT.TRUST_FAILED);
|
|
444
264
|
}
|
|
265
|
+
if (!result.signature_valid) {
|
|
266
|
+
error(
|
|
267
|
+
`${entry.name}: Ed25519 signature INVALID. Package may be tampered. Refusing.`,
|
|
268
|
+
EXIT.TRUST_FAILED,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
console.log(' ✓ Signature OK (Ed25519 verified)');
|
|
445
272
|
}
|
|
446
273
|
|
|
447
274
|
// ─── Status confirmation (interactive) ─────────────────────────────────
|
|
@@ -465,31 +292,12 @@ function confirmStatus(entry, yes) {
|
|
|
465
292
|
// ─── Cleanup stale temps ───────────────────────────────────────────────
|
|
466
293
|
|
|
467
294
|
function cleanStaleTemps() {
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
for (const scopeName of fs.readdirSync(INSTALL_DIR)) {
|
|
471
|
-
if (!scopeName.startsWith('@')) continue;
|
|
472
|
-
const sd = path.join(INSTALL_DIR, scopeName);
|
|
473
|
-
if (!fs.statSync(sd).isDirectory()) continue;
|
|
474
|
-
for (const child of fs.readdirSync(sd)) {
|
|
475
|
-
if (child.endsWith('.tmp') || child.endsWith('.kdna.tmp')) {
|
|
476
|
-
try {
|
|
477
|
-
fs.rmSync(path.join(sd, child), { recursive: true, force: true });
|
|
478
|
-
} catch {
|
|
479
|
-
/* ignore */
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
} catch {
|
|
485
|
-
/* ignore */
|
|
486
|
-
}
|
|
295
|
+
ensureDir(INSTALL_DIR);
|
|
487
296
|
}
|
|
488
297
|
|
|
489
298
|
// ─── Main install ──────────────────────────────────────────────────────
|
|
490
299
|
|
|
491
300
|
function cmdInstallExtended(input, args = []) {
|
|
492
|
-
warnLegacy();
|
|
493
301
|
ensureDir(INSTALL_DIR);
|
|
494
302
|
cleanStaleTemps();
|
|
495
303
|
|
|
@@ -505,11 +313,21 @@ function cmdInstallExtended(input, args = []) {
|
|
|
505
313
|
return installFromRegistry(source.parsed, yes, jsonMode);
|
|
506
314
|
case 'local-file':
|
|
507
315
|
return installFromLocalFile(source.path, yes, jsonMode);
|
|
508
|
-
case 'local-dir':
|
|
509
|
-
return installFromLocalDir(source.path, yes, jsonMode);
|
|
510
316
|
}
|
|
511
317
|
}
|
|
512
318
|
|
|
319
|
+
function scopedNameError(sourceLabel, declared) {
|
|
320
|
+
return (
|
|
321
|
+
`Invalid domain name in ${sourceLabel}: "${declared || '?'}"\n\n` +
|
|
322
|
+
`KDNA v0.7+ requires scoped domain names.\n` +
|
|
323
|
+
`Expected format: @scope/name\n` +
|
|
324
|
+
`Example: @aikdna/my_domain\n\n` +
|
|
325
|
+
`Fix:\n` +
|
|
326
|
+
`- update kdna.json name to "@aikdna/my_domain"\n` +
|
|
327
|
+
`- or initialize a new scoped domain, then copy your content into it`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
513
331
|
function installFromRegistry(parsed, yes, jsonMode = false) {
|
|
514
332
|
const resolver = new RegistryResolver({ allowNetwork: true });
|
|
515
333
|
let scope, entry;
|
|
@@ -541,14 +359,17 @@ function installFromRegistry(parsed, yes, jsonMode = false) {
|
|
|
541
359
|
return installCluster(entry, resolver, yes, jsonMode);
|
|
542
360
|
}
|
|
543
361
|
|
|
544
|
-
if (!entry.
|
|
362
|
+
if (!entry.asset_url) {
|
|
545
363
|
error(
|
|
546
|
-
`${entry.name}@${entry.version} has no
|
|
364
|
+
`${entry.name}@${entry.version} has no asset_url in registry.\n` +
|
|
547
365
|
`release_status: ${entry.release_status || 'unknown'}\n` +
|
|
548
|
-
`(
|
|
366
|
+
`(Registry v3 publishes canonical .kdna assets through asset_url only.)`,
|
|
549
367
|
EXIT.REGISTRY_ERROR,
|
|
550
368
|
);
|
|
551
369
|
}
|
|
370
|
+
if (!entry.asset_digest) {
|
|
371
|
+
error(`${entry.name}@${entry.version} has no asset_digest in registry.`, EXIT.REGISTRY_ERROR);
|
|
372
|
+
}
|
|
552
373
|
|
|
553
374
|
if (!confirmStatus(entry, yes)) {
|
|
554
375
|
console.log('Installation cancelled.');
|
|
@@ -559,70 +380,68 @@ function installFromRegistry(parsed, yes, jsonMode = false) {
|
|
|
559
380
|
}
|
|
560
381
|
|
|
561
382
|
function installSingleFromUrl({ entry, scope }, jsonMode = false) {
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
const tmpFile = path.join(
|
|
383
|
+
const ident = entry.name.split('/')[1];
|
|
384
|
+
const tmpDir = path.join(USER_KDNA_DIR, 'cache', 'downloads');
|
|
385
|
+
const tmpFile = path.join(tmpDir, `.${ident}-${Date.now()}.kdna.tmp`);
|
|
386
|
+
const assetUrl = entry.asset_url;
|
|
565
387
|
|
|
566
388
|
if (!jsonMode) console.log(` Downloading ${entry.name}@${entry.version}...`);
|
|
567
|
-
ensureDir(
|
|
389
|
+
ensureDir(tmpDir);
|
|
568
390
|
try {
|
|
569
|
-
downloadFile(
|
|
391
|
+
downloadFile(assetUrl, tmpFile);
|
|
570
392
|
} catch {
|
|
571
|
-
error(`Failed to download ${
|
|
393
|
+
error(`Failed to download ${assetUrl}`, EXIT.REGISTRY_ERROR);
|
|
572
394
|
}
|
|
573
395
|
|
|
574
|
-
//
|
|
396
|
+
// asset digest check
|
|
575
397
|
const actual = sha256File(tmpFile);
|
|
576
|
-
|
|
398
|
+
const expectedDigest = entry.asset_digest;
|
|
399
|
+
const actualDigest = `sha256:${actual}`;
|
|
400
|
+
if (expectedDigest && actualDigest !== expectedDigest) {
|
|
577
401
|
try {
|
|
578
402
|
fs.unlinkSync(tmpFile);
|
|
579
403
|
} catch {
|
|
580
404
|
/* ignore */
|
|
581
405
|
}
|
|
582
|
-
error(
|
|
406
|
+
error(
|
|
407
|
+
`asset digest mismatch for ${entry.name}: expected ${expectedDigest}, got ${actualDigest}`,
|
|
408
|
+
);
|
|
583
409
|
}
|
|
584
|
-
if (!jsonMode) console.log(` ✓
|
|
410
|
+
if (!jsonMode) console.log(` ✓ asset digest verified`);
|
|
585
411
|
|
|
586
|
-
|
|
587
|
-
if (fs.existsSync(dest)) {
|
|
588
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
589
|
-
}
|
|
590
|
-
ensureDir(dest);
|
|
412
|
+
verifySignature({ assetPath: tmpFile, scope, entry, lenient: true });
|
|
591
413
|
|
|
592
|
-
|
|
414
|
+
const installed = installAsset({
|
|
415
|
+
sourcePath: tmpFile,
|
|
416
|
+
name: entry.name,
|
|
417
|
+
version: entry.version,
|
|
418
|
+
source: {
|
|
419
|
+
type: 'registry',
|
|
420
|
+
name: entry.name,
|
|
421
|
+
version: entry.version,
|
|
422
|
+
asset_url: assetUrl,
|
|
423
|
+
asset_digest: expectedDigest,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
593
426
|
try {
|
|
594
427
|
fs.unlinkSync(tmpFile);
|
|
595
428
|
} catch {
|
|
596
429
|
/* ignore */
|
|
597
430
|
}
|
|
598
431
|
|
|
599
|
-
verifySignature({ destDir: dest, scope, entry, lenient: true });
|
|
600
|
-
|
|
601
|
-
// Stamp install metadata
|
|
602
|
-
const manifest = readJson(path.join(dest, 'kdna.json')) || {};
|
|
603
|
-
manifest._source = {
|
|
604
|
-
type: 'registry',
|
|
605
|
-
name: entry.name,
|
|
606
|
-
version: entry.version,
|
|
607
|
-
kdna_url: entry.kdna_url,
|
|
608
|
-
sha256: entry.sha256,
|
|
609
|
-
installed_at: new Date().toISOString(),
|
|
610
|
-
};
|
|
611
|
-
fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
612
|
-
|
|
613
432
|
if (jsonMode) {
|
|
614
433
|
console.log(
|
|
615
434
|
JSON.stringify({
|
|
616
435
|
name: entry.name,
|
|
617
436
|
version: entry.version,
|
|
618
437
|
installed: true,
|
|
619
|
-
path:
|
|
438
|
+
path: installed.asset_path,
|
|
620
439
|
type: entry.type || 'domain',
|
|
621
440
|
}),
|
|
622
441
|
);
|
|
623
442
|
} else {
|
|
624
443
|
console.log(`✓ Installed ${entry.name}@${entry.version}`);
|
|
625
|
-
console.log(`
|
|
444
|
+
console.log(` Asset: ${installed.asset_path}`);
|
|
626
445
|
}
|
|
627
446
|
}
|
|
628
447
|
|
|
@@ -637,8 +456,8 @@ function installCluster(clusterEntry, resolver, _yes, jsonMode = false) {
|
|
|
637
456
|
for (const sub of subdomains) {
|
|
638
457
|
try {
|
|
639
458
|
const resolved = resolver.resolve(sub);
|
|
640
|
-
if (!resolved.entry.
|
|
641
|
-
if (!jsonMode) console.warn(` ⚠ ${sub}: no
|
|
459
|
+
if (!resolved.entry.asset_url) {
|
|
460
|
+
if (!jsonMode) console.warn(` ⚠ ${sub}: no asset_url (skipping)`);
|
|
642
461
|
continue;
|
|
643
462
|
}
|
|
644
463
|
if (!jsonMode) console.log('');
|
|
@@ -648,26 +467,6 @@ function installCluster(clusterEntry, resolver, _yes, jsonMode = false) {
|
|
|
648
467
|
}
|
|
649
468
|
}
|
|
650
469
|
|
|
651
|
-
// Record the cluster itself
|
|
652
|
-
const [scopeName, ident] = clusterEntry.name.split('/');
|
|
653
|
-
const clusterDest = domainDir(scopeName, ident);
|
|
654
|
-
ensureDir(clusterDest);
|
|
655
|
-
fs.writeFileSync(
|
|
656
|
-
path.join(clusterDest, 'cluster.json'),
|
|
657
|
-
JSON.stringify(
|
|
658
|
-
{
|
|
659
|
-
name: clusterEntry.name,
|
|
660
|
-
version: clusterEntry.version,
|
|
661
|
-
type: 'cluster',
|
|
662
|
-
domains: subdomains,
|
|
663
|
-
composition_rules: clusterEntry.cluster.composition_rules || [],
|
|
664
|
-
installed_at: new Date().toISOString(),
|
|
665
|
-
},
|
|
666
|
-
null,
|
|
667
|
-
2,
|
|
668
|
-
) + '\n',
|
|
669
|
-
);
|
|
670
|
-
|
|
671
470
|
if (jsonMode) {
|
|
672
471
|
console.log(
|
|
673
472
|
JSON.stringify({
|
|
@@ -675,7 +474,6 @@ function installCluster(clusterEntry, resolver, _yes, jsonMode = false) {
|
|
|
675
474
|
version: clusterEntry.version,
|
|
676
475
|
type: 'cluster',
|
|
677
476
|
installed: true,
|
|
678
|
-
path: clusterDest,
|
|
679
477
|
subdomains: subdomains.length,
|
|
680
478
|
}),
|
|
681
479
|
);
|
|
@@ -688,163 +486,64 @@ function installCluster(clusterEntry, resolver, _yes, jsonMode = false) {
|
|
|
688
486
|
function installFromLocalFile(filePath, _yes, jsonMode = false) {
|
|
689
487
|
const abs = path.resolve(filePath);
|
|
690
488
|
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) error(`Not a file: ${abs}`);
|
|
489
|
+
if (!abs.endsWith('.kdna')) error(`Not a .kdna asset: ${abs}`, EXIT.INPUT_ERROR);
|
|
691
490
|
|
|
692
|
-
const
|
|
693
|
-
const tmpDir = path.join(INSTALL_DIR, '.local-tmp-' + Date.now());
|
|
694
|
-
ensureDir(tmpDir);
|
|
695
|
-
|
|
696
|
-
if (isEncrypted) {
|
|
697
|
-
// Find license for this .kdnae file
|
|
698
|
-
// First check the license directory, then ask for --license flag from args
|
|
699
|
-
const licenseFromArgs = process.argv.includes('--license')
|
|
700
|
-
? process.argv[process.argv.indexOf('--license') + 1]
|
|
701
|
-
: null;
|
|
702
|
-
let licenseKey = null;
|
|
703
|
-
|
|
704
|
-
if (licenseFromArgs && fs.existsSync(licenseFromArgs)) {
|
|
705
|
-
try {
|
|
706
|
-
const lic = JSON.parse(fs.readFileSync(licenseFromArgs, 'utf8'));
|
|
707
|
-
licenseKey = lic.license_id;
|
|
708
|
-
} catch {
|
|
709
|
-
/* invalid license file */
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (!licenseKey) {
|
|
714
|
-
// Try to auto-discover from ~/.kdna/licenses/
|
|
715
|
-
const manifest = readJson(path.join(tmpDir, 'kdna.json'));
|
|
716
|
-
// We need to extract just the manifest first to get the domain name
|
|
717
|
-
extractKdna(abs, tmpDir);
|
|
718
|
-
const mf = readJson(path.join(tmpDir, 'kdna.json'));
|
|
719
|
-
if (mf?.name) {
|
|
720
|
-
const lic = findLicenseForDomain(mf.name);
|
|
721
|
-
if (lic) licenseKey = lic.license_id;
|
|
722
|
-
}
|
|
723
|
-
if (!licenseKey) {
|
|
724
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
725
|
-
error(
|
|
726
|
-
`Cannot install encrypted .kdnae without a license.\n` +
|
|
727
|
-
`Save the license to ~/.kdna/licenses/ or use --license <file>.`,
|
|
728
|
-
EXIT.TRUST_FAILED,
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
// Re-extract for decryption
|
|
732
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
733
|
-
ensureDir(tmpDir);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
console.log(' Decrypting .kdnae container...');
|
|
737
|
-
extractAndDecrypt(abs, tmpDir, licenseKey);
|
|
738
|
-
} else {
|
|
739
|
-
extractKdna(abs, tmpDir);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const manifest = readJson(path.join(tmpDir, 'kdna.json'));
|
|
491
|
+
const manifest = readContainerJson(abs, 'kdna.json');
|
|
743
492
|
const declared = manifest?.name;
|
|
744
493
|
if (!declared || !/^@[a-z][a-z0-9-]*\/[a-z][a-z0-9_]*$/.test(declared)) {
|
|
745
|
-
|
|
746
|
-
error(
|
|
747
|
-
`Package kdna.json.name "${declared || '?'}" must be @scope/name format.\n` +
|
|
748
|
-
`(v0.7 requires scoped names.)`,
|
|
749
|
-
);
|
|
494
|
+
error(scopedNameError('package kdna.json.name', declared), EXIT.INPUT_ERROR);
|
|
750
495
|
}
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const destManifest = readJson(path.join(dest, 'kdna.json')) || {};
|
|
758
|
-
destManifest._source = {
|
|
759
|
-
type: 'local-file',
|
|
760
|
-
path: abs,
|
|
761
|
-
encrypted: isEncrypted,
|
|
762
|
-
installed_at: new Date().toISOString(),
|
|
763
|
-
};
|
|
764
|
-
fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(destManifest, null, 2) + '\n');
|
|
496
|
+
const installed = installAsset({
|
|
497
|
+
sourcePath: abs,
|
|
498
|
+
name: declared,
|
|
499
|
+
version: manifest.version,
|
|
500
|
+
source: { type: 'local-file', path: abs },
|
|
501
|
+
});
|
|
765
502
|
|
|
766
503
|
if (jsonMode) {
|
|
767
504
|
console.log(
|
|
768
505
|
JSON.stringify({
|
|
769
506
|
name: declared,
|
|
770
507
|
installed: true,
|
|
771
|
-
path:
|
|
508
|
+
path: installed.asset_path,
|
|
509
|
+
receipt_path: installed.receipt_path,
|
|
510
|
+
asset_digest: installed.asset_digest,
|
|
511
|
+
content_digest: installed.content_digest,
|
|
772
512
|
source: 'local-file',
|
|
773
513
|
source_path: abs,
|
|
774
|
-
encrypted: isEncrypted,
|
|
775
|
-
}),
|
|
776
|
-
);
|
|
777
|
-
} else {
|
|
778
|
-
console.log(`✓ Installed ${declared} from ${isEncrypted ? 'encrypted' : 'local'} file`);
|
|
779
|
-
console.log(` Location: ${dest}`);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
function installFromLocalDir(dirPath, _yes, jsonMode = false) {
|
|
784
|
-
const abs = path.resolve(dirPath);
|
|
785
|
-
const manifest = readJson(path.join(abs, 'kdna.json'));
|
|
786
|
-
const declared = manifest?.name;
|
|
787
|
-
if (!declared || !/^@[a-z][a-z0-9-]*\/[a-z][a-z0-9_]*$/.test(declared)) {
|
|
788
|
-
error(`Source kdna.json.name "${declared || '?'}" must be @scope/name format.`);
|
|
789
|
-
}
|
|
790
|
-
const [scopeName, ident] = declared.split('/');
|
|
791
|
-
const dest = domainDir(scopeName, ident);
|
|
792
|
-
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
793
|
-
ensureDir(path.dirname(dest));
|
|
794
|
-
fs.cpSync(abs, dest, { recursive: true });
|
|
795
|
-
|
|
796
|
-
const destManifest = readJson(path.join(dest, 'kdna.json')) || {};
|
|
797
|
-
destManifest._source = {
|
|
798
|
-
type: 'local-dir',
|
|
799
|
-
path: abs,
|
|
800
|
-
installed_at: new Date().toISOString(),
|
|
801
|
-
};
|
|
802
|
-
fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(destManifest, null, 2) + '\n');
|
|
803
|
-
|
|
804
|
-
if (jsonMode) {
|
|
805
|
-
console.log(
|
|
806
|
-
JSON.stringify({
|
|
807
|
-
name: declared,
|
|
808
|
-
installed: true,
|
|
809
|
-
path: dest,
|
|
810
|
-
source: 'local-dir',
|
|
811
|
-
source_path: abs,
|
|
812
514
|
}),
|
|
813
515
|
);
|
|
814
516
|
} else {
|
|
815
|
-
console.log(`✓ Installed ${declared} from local
|
|
816
|
-
console.log(`
|
|
517
|
+
console.log(`✓ Installed ${declared} from local .kdna asset`);
|
|
518
|
+
console.log(` Asset: ${installed.asset_path}`);
|
|
817
519
|
}
|
|
818
520
|
}
|
|
819
521
|
|
|
820
522
|
// ─── Remove ─────────────────────────────────────────────────────────────
|
|
821
523
|
|
|
822
524
|
function cmdRemove(input) {
|
|
823
|
-
warnLegacy();
|
|
824
525
|
const parsed = parseName(input);
|
|
825
526
|
if (!parsed) error(`Invalid name "${input}". Use @scope/name or bare name.`);
|
|
826
|
-
|
|
827
|
-
if (!fs.existsSync(dest)) {
|
|
527
|
+
if (!removeInstalled(parsed.full)) {
|
|
828
528
|
console.log(`${parsed.full} is not installed.`);
|
|
829
529
|
return;
|
|
830
530
|
}
|
|
831
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
832
531
|
console.log(`✓ Removed ${parsed.full}`);
|
|
833
532
|
}
|
|
834
533
|
|
|
835
534
|
// ─── Info ───────────────────────────────────────────────────────────────
|
|
836
535
|
|
|
837
536
|
function cmdInfo(input, jsonMode = false) {
|
|
838
|
-
warnLegacy();
|
|
839
537
|
const parsed = parseName(input);
|
|
840
538
|
if (!parsed) error(`Invalid name "${input}".`, EXIT.INPUT_ERROR);
|
|
841
|
-
const
|
|
842
|
-
if (!
|
|
539
|
+
const installed = getInstalled(parsed.full);
|
|
540
|
+
if (!installed) error(`${parsed.full} is not installed.`, EXIT.INPUT_ERROR);
|
|
843
541
|
|
|
844
|
-
const
|
|
845
|
-
const
|
|
846
|
-
const
|
|
847
|
-
const
|
|
542
|
+
const container = readContainer(installed.asset_path);
|
|
543
|
+
const manifest = container.manifest || {};
|
|
544
|
+
const core = container.core || {};
|
|
545
|
+
const pat = container.patterns || {};
|
|
546
|
+
const source = installed.source || {};
|
|
848
547
|
|
|
849
548
|
// ─── Judgment surface (computed for both modes) ────────────────────
|
|
850
549
|
const axiomCount = (core?.axioms || []).length;
|
|
@@ -870,17 +569,7 @@ function cmdInfo(input, jsonMode = false) {
|
|
|
870
569
|
}
|
|
871
570
|
|
|
872
571
|
// ─── Eval cases ────────────────────────────────────────────────────
|
|
873
|
-
const
|
|
874
|
-
let evalInfo = null;
|
|
875
|
-
if (fs.existsSync(evalDir)) {
|
|
876
|
-
const evalFiles = fs.readdirSync(evalDir).filter((f) => f.endsWith('.json'));
|
|
877
|
-
let totalCases = 0;
|
|
878
|
-
for (const f of evalFiles) {
|
|
879
|
-
const data = readJson(path.join(evalDir, f));
|
|
880
|
-
if (data?.cases) totalCases += data.cases.length;
|
|
881
|
-
}
|
|
882
|
-
evalInfo = { files: evalFiles.length, totalCases };
|
|
883
|
-
}
|
|
572
|
+
const evalInfo = null;
|
|
884
573
|
|
|
885
574
|
// ─── Known risks ───────────────────────────────────────────────────
|
|
886
575
|
const risks = [];
|
|
@@ -899,7 +588,7 @@ function cmdInfo(input, jsonMode = false) {
|
|
|
899
588
|
'KDNA_Reasoning.json',
|
|
900
589
|
'KDNA_Evolution.json',
|
|
901
590
|
];
|
|
902
|
-
const present = expected.filter((f) =>
|
|
591
|
+
const present = expected.filter((f) => container.files.includes(f));
|
|
903
592
|
|
|
904
593
|
// ─── JSON mode: emit structured output only, then exit ─────────────
|
|
905
594
|
if (jsonMode) {
|
|
@@ -912,10 +601,12 @@ function cmdInfo(input, jsonMode = false) {
|
|
|
912
601
|
author: manifest?.author?.name || '?',
|
|
913
602
|
pubkey: manifest?.author?.pubkey || null,
|
|
914
603
|
has_pem: !!manifest?.author?.public_key_pem,
|
|
915
|
-
source_url: source.
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
604
|
+
source_url: source.asset_url || null,
|
|
605
|
+
asset_digest: installed.asset_digest || source.asset_digest || null,
|
|
606
|
+
content_digest: installed.content_digest || null,
|
|
607
|
+
receipt_path: installed.receipt_path || null,
|
|
608
|
+
installed_at: installed.installed_at || null,
|
|
609
|
+
path: installed.asset_path,
|
|
919
610
|
axioms: axiomCount,
|
|
920
611
|
ontology: ontologyCount,
|
|
921
612
|
stances: stanceCount,
|
|
@@ -951,12 +642,18 @@ function cmdInfo(input, jsonMode = false) {
|
|
|
951
642
|
if (manifest?.author?.public_key_pem) {
|
|
952
643
|
console.log(` Embedded PEM: yes (full Ed25519 verify available)`);
|
|
953
644
|
} else {
|
|
954
|
-
console.log(` Embedded PEM: no
|
|
645
|
+
console.log(` Embedded PEM: no`);
|
|
955
646
|
}
|
|
956
|
-
if (source.
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
647
|
+
if (source.asset_url) {
|
|
648
|
+
console.log(` Source URL: ${source.asset_url}`);
|
|
649
|
+
}
|
|
650
|
+
if (installed.asset_digest)
|
|
651
|
+
console.log(` Asset digest: ${installed.asset_digest.slice(0, 39)}…`);
|
|
652
|
+
if (installed.content_digest)
|
|
653
|
+
console.log(` Content digest: ${installed.content_digest.slice(0, 39)}…`);
|
|
654
|
+
if (installed.receipt_path) console.log(` Receipt: ${installed.receipt_path}`);
|
|
655
|
+
console.log(` Installed: ${installed.installed_at || '?'}`);
|
|
656
|
+
console.log(` Asset: ${installed.asset_path}`);
|
|
960
657
|
|
|
961
658
|
// ─── Judgment surface ──────────────────────────────────────────
|
|
962
659
|
console.log('');
|
|
@@ -1008,16 +705,14 @@ function cmdInfo(input, jsonMode = false) {
|
|
|
1008
705
|
// ─── Update ─────────────────────────────────────────────────────────────
|
|
1009
706
|
|
|
1010
707
|
function cmdUpdate(input) {
|
|
1011
|
-
warnLegacy();
|
|
1012
708
|
const parsed = parseName(input);
|
|
1013
709
|
if (!parsed) error(`Invalid name "${input}".`);
|
|
1014
|
-
const
|
|
1015
|
-
if (!
|
|
710
|
+
const installed = getInstalled(parsed.full);
|
|
711
|
+
if (!installed) {
|
|
1016
712
|
console.log(`${parsed.full} not installed. Run: kdna install ${input}`);
|
|
1017
713
|
return;
|
|
1018
714
|
}
|
|
1019
|
-
const
|
|
1020
|
-
const installedVersion = manifest.version || manifest._source?.version || '?';
|
|
715
|
+
const installedVersion = installed.version || '?';
|
|
1021
716
|
|
|
1022
717
|
const resolver = new RegistryResolver({ allowNetwork: true, refresh: true });
|
|
1023
718
|
let entry;
|
|
@@ -1036,24 +731,17 @@ function cmdUpdate(input) {
|
|
|
1036
731
|
}
|
|
1037
732
|
|
|
1038
733
|
function cmdUpdateAll() {
|
|
1039
|
-
|
|
1040
|
-
if (!
|
|
734
|
+
const installed = listInstalled();
|
|
735
|
+
if (!installed.length) {
|
|
1041
736
|
console.log('No installs.');
|
|
1042
737
|
return;
|
|
1043
738
|
}
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
if (ident.startsWith('.')) continue;
|
|
1050
|
-
try {
|
|
1051
|
-
cmdUpdate(`${scope}/${ident}`);
|
|
1052
|
-
} catch (e) {
|
|
1053
|
-
console.warn(` ⚠ ${scope}/${ident}: ${e.message.split('\n')[0]}`);
|
|
1054
|
-
}
|
|
739
|
+
for (const entry of installed) {
|
|
740
|
+
try {
|
|
741
|
+
cmdUpdate(entry.full);
|
|
742
|
+
} catch (e) {
|
|
743
|
+
console.warn(` ⚠ ${entry.full}: ${e.message.split('\n')[0]}`);
|
|
1055
744
|
}
|
|
1056
|
-
console.log('');
|
|
1057
745
|
}
|
|
1058
746
|
}
|
|
1059
747
|
|