@aikdna/kdna-cli 0.16.10 → 0.18.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 +158 -75
- package/package.json +5 -5
- package/skills/kdna-loader/SKILL.md +5 -6
- package/src/agent.js +489 -79
- package/src/cli.js +112 -62
- package/src/cmds/_common.js +32 -16
- package/src/cmds/badge.js +7 -7
- package/src/cmds/changelog.js +1 -1
- package/src/cmds/cluster.js +16 -48
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +213 -443
- package/src/cmds/explain.js +122 -0
- package/src/cmds/legacy.js +8 -8
- package/src/cmds/license.js +483 -26
- package/src/cmds/quality.js +14 -2
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +4 -5
- package/src/cmds/test.js +4 -4
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +28 -22
- package/src/diff.js +11 -13
- package/src/init.js +2 -2
- package/src/install.js +138 -460
- package/src/loader.js +10 -10
- package/src/package-store.js +229 -0
- package/src/paths.js +44 -0
- package/src/publish.js +184 -22
- package/src/registry.js +76 -9
- package/src/setup.js +19 -20
- package/src/verify.js +275 -121
- package/templates/standard-domain/kdna.json +2 -1
- package/validators/kdna-lint.js +37 -3
- package/validators/kdna-validate.js +3 -2
- 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,87 +153,39 @@ 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('./') || input.startsWith('/') || input.startsWith('~/'))
|
|
161
|
+
input.endsWith('.kdna') &&
|
|
162
|
+
(input.startsWith('./') || input.startsWith('/') || input.startsWith('~/') || fs.existsSync(input))
|
|
213
163
|
) {
|
|
214
164
|
const resolved = path.resolve(input.replace(/^~/, process.env.HOME || ''));
|
|
215
165
|
if (!fs.existsSync(resolved)) error(`Local file not found: ${resolved}`);
|
|
216
166
|
return { type: 'local-file', path: resolved };
|
|
217
167
|
}
|
|
218
168
|
|
|
219
|
-
// Local directory
|
|
220
169
|
if (input.startsWith('./') || input.startsWith('/') || input.startsWith('~/')) {
|
|
221
170
|
const resolved = path.resolve(input.replace(/^~/, process.env.HOME || ''));
|
|
222
171
|
if (!fs.existsSync(resolved)) error(`Local path not found: ${resolved}`);
|
|
223
|
-
if (
|
|
224
|
-
|
|
172
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
173
|
+
error(
|
|
174
|
+
`Directory install is not supported. KDNA installs .kdna assets only.\n` +
|
|
175
|
+
`Use: kdna dev pack ${resolved} --output <dir>, then kdna install <file.kdna>`,
|
|
176
|
+
EXIT.INPUT_ERROR,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
error(`Not a .kdna file: ${resolved}`, EXIT.INPUT_ERROR);
|
|
225
180
|
}
|
|
226
181
|
|
|
227
182
|
// Registry name (bare or @scope/name)
|
|
228
183
|
const parsed = parseName(input);
|
|
229
184
|
if (!parsed) {
|
|
230
185
|
error(
|
|
231
|
-
|
|
186
|
+
`Cannot parse "${input}". Use:\n` +
|
|
232
187
|
` kdna install <name> # @aikdna/<name>\n` +
|
|
233
188
|
` kdna install @scope/name # any scope\n` +
|
|
234
|
-
` kdna install ./folder # local directory\n` +
|
|
235
189
|
` kdna install ./file.kdna # local .kdna file`,
|
|
236
190
|
);
|
|
237
191
|
}
|
|
@@ -266,90 +220,10 @@ function downloadFile(url, dest) {
|
|
|
266
220
|
throw new Error(`download failed after 3 attempts: ${stderr}`);
|
|
267
221
|
}
|
|
268
222
|
|
|
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
223
|
// ─── Signature verification ────────────────────────────────────────────
|
|
350
224
|
|
|
351
|
-
function verifySignature({
|
|
352
|
-
const manifest =
|
|
225
|
+
function verifySignature({ assetPath, scope, entry, lenient = true }) {
|
|
226
|
+
const manifest = readContainerJson(assetPath, 'kdna.json');
|
|
353
227
|
if (!manifest) {
|
|
354
228
|
if (lenient) {
|
|
355
229
|
console.warn(' ⚠ No kdna.json — cannot verify signature.');
|
|
@@ -359,20 +233,8 @@ function verifySignature({ destDir, scope, entry, lenient = true }) {
|
|
|
359
233
|
}
|
|
360
234
|
|
|
361
235
|
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
236
|
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;
|
|
237
|
+
error(`${entry.name}: registry and .kdna manifest signatures are required.`, EXIT.TRUST_FAILED);
|
|
376
238
|
}
|
|
377
239
|
|
|
378
240
|
// Author pubkey fingerprint must match scope trust_pubkey
|
|
@@ -384,64 +246,23 @@ function verifySignature({ destDir, scope, entry, lenient = true }) {
|
|
|
384
246
|
}
|
|
385
247
|
|
|
386
248
|
// Full Ed25519 verify (requires public_key_pem embedded in the package)
|
|
387
|
-
|
|
388
|
-
|
|
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;
|
|
249
|
+
if (!manifest.author?.public_key_pem) {
|
|
250
|
+
error(`${entry.name}: manifest author.public_key_pem is required for Ed25519 verification.`, EXIT.TRUST_FAILED);
|
|
392
251
|
}
|
|
393
252
|
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
253
|
+
const result = verifyAsset(assetPath, { requireSignature: true });
|
|
254
|
+
for (const e of result.errors || []) {
|
|
255
|
+
if (e.includes('signature') || e.includes('public_key') || e.includes('fingerprint')) {
|
|
256
|
+
error(`${entry.name}: ${e}. Refusing to install.`, EXIT.TRUST_FAILED);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (!result.signature_valid) {
|
|
397
260
|
error(
|
|
398
|
-
`${entry.name}:
|
|
261
|
+
`${entry.name}: Ed25519 signature INVALID. Package may be tampered. Refusing.`,
|
|
399
262
|
EXIT.TRUST_FAILED,
|
|
400
263
|
);
|
|
401
264
|
}
|
|
402
|
-
|
|
403
|
-
// 2. Verify the Ed25519 signature over the canonical payload
|
|
404
|
-
// Canonical payload reconstruction must match publish.js exactly:
|
|
405
|
-
// - sorted .json filenames
|
|
406
|
-
// - for kdna.json: strip "signature" field before hashing
|
|
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}`);
|
|
429
|
-
}
|
|
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
|
-
}
|
|
265
|
+
console.log(' ✓ Signature OK (Ed25519 verified)');
|
|
445
266
|
}
|
|
446
267
|
|
|
447
268
|
// ─── Status confirmation (interactive) ─────────────────────────────────
|
|
@@ -465,31 +286,12 @@ function confirmStatus(entry, yes) {
|
|
|
465
286
|
// ─── Cleanup stale temps ───────────────────────────────────────────────
|
|
466
287
|
|
|
467
288
|
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
|
-
}
|
|
289
|
+
ensureDir(INSTALL_DIR);
|
|
487
290
|
}
|
|
488
291
|
|
|
489
292
|
// ─── Main install ──────────────────────────────────────────────────────
|
|
490
293
|
|
|
491
294
|
function cmdInstallExtended(input, args = []) {
|
|
492
|
-
warnLegacy();
|
|
493
295
|
ensureDir(INSTALL_DIR);
|
|
494
296
|
cleanStaleTemps();
|
|
495
297
|
|
|
@@ -505,11 +307,21 @@ function cmdInstallExtended(input, args = []) {
|
|
|
505
307
|
return installFromRegistry(source.parsed, yes, jsonMode);
|
|
506
308
|
case 'local-file':
|
|
507
309
|
return installFromLocalFile(source.path, yes, jsonMode);
|
|
508
|
-
case 'local-dir':
|
|
509
|
-
return installFromLocalDir(source.path, yes, jsonMode);
|
|
510
310
|
}
|
|
511
311
|
}
|
|
512
312
|
|
|
313
|
+
function scopedNameError(sourceLabel, declared) {
|
|
314
|
+
return (
|
|
315
|
+
`Invalid domain name in ${sourceLabel}: "${declared || '?'}"\n\n` +
|
|
316
|
+
`KDNA v0.7+ requires scoped domain names.\n` +
|
|
317
|
+
`Expected format: @scope/name\n` +
|
|
318
|
+
`Example: @aikdna/my_domain\n\n` +
|
|
319
|
+
`Fix:\n` +
|
|
320
|
+
`- update kdna.json name to "@aikdna/my_domain"\n` +
|
|
321
|
+
`- or initialize a new scoped domain, then copy your content into it`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
513
325
|
function installFromRegistry(parsed, yes, jsonMode = false) {
|
|
514
326
|
const resolver = new RegistryResolver({ allowNetwork: true });
|
|
515
327
|
let scope, entry;
|
|
@@ -541,14 +353,17 @@ function installFromRegistry(parsed, yes, jsonMode = false) {
|
|
|
541
353
|
return installCluster(entry, resolver, yes, jsonMode);
|
|
542
354
|
}
|
|
543
355
|
|
|
544
|
-
if (!entry.
|
|
356
|
+
if (!entry.asset_url) {
|
|
545
357
|
error(
|
|
546
|
-
`${entry.name}@${entry.version} has no
|
|
358
|
+
`${entry.name}@${entry.version} has no asset_url in registry.\n` +
|
|
547
359
|
`release_status: ${entry.release_status || 'unknown'}\n` +
|
|
548
|
-
`(
|
|
360
|
+
`(Registry v3 publishes canonical .kdna assets through asset_url only.)`,
|
|
549
361
|
EXIT.REGISTRY_ERROR,
|
|
550
362
|
);
|
|
551
363
|
}
|
|
364
|
+
if (!entry.asset_digest) {
|
|
365
|
+
error(`${entry.name}@${entry.version} has no asset_digest in registry.`, EXIT.REGISTRY_ERROR);
|
|
366
|
+
}
|
|
552
367
|
|
|
553
368
|
if (!confirmStatus(entry, yes)) {
|
|
554
369
|
console.log('Installation cancelled.');
|
|
@@ -559,70 +374,66 @@ function installFromRegistry(parsed, yes, jsonMode = false) {
|
|
|
559
374
|
}
|
|
560
375
|
|
|
561
376
|
function installSingleFromUrl({ entry, scope }, jsonMode = false) {
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
const tmpFile = path.join(
|
|
377
|
+
const ident = entry.name.split('/')[1];
|
|
378
|
+
const tmpDir = path.join(USER_KDNA_DIR, 'cache', 'downloads');
|
|
379
|
+
const tmpFile = path.join(tmpDir, `.${ident}-${Date.now()}.kdna.tmp`);
|
|
380
|
+
const assetUrl = entry.asset_url;
|
|
565
381
|
|
|
566
382
|
if (!jsonMode) console.log(` Downloading ${entry.name}@${entry.version}...`);
|
|
567
|
-
ensureDir(
|
|
383
|
+
ensureDir(tmpDir);
|
|
568
384
|
try {
|
|
569
|
-
downloadFile(
|
|
385
|
+
downloadFile(assetUrl, tmpFile);
|
|
570
386
|
} catch {
|
|
571
|
-
error(`Failed to download ${
|
|
387
|
+
error(`Failed to download ${assetUrl}`, EXIT.REGISTRY_ERROR);
|
|
572
388
|
}
|
|
573
389
|
|
|
574
|
-
//
|
|
390
|
+
// asset digest check
|
|
575
391
|
const actual = sha256File(tmpFile);
|
|
576
|
-
|
|
392
|
+
const expectedDigest = entry.asset_digest;
|
|
393
|
+
const actualDigest = `sha256:${actual}`;
|
|
394
|
+
if (expectedDigest && actualDigest !== expectedDigest) {
|
|
577
395
|
try {
|
|
578
396
|
fs.unlinkSync(tmpFile);
|
|
579
397
|
} catch {
|
|
580
398
|
/* ignore */
|
|
581
399
|
}
|
|
582
|
-
error(`
|
|
400
|
+
error(`asset digest mismatch for ${entry.name}: expected ${expectedDigest}, got ${actualDigest}`);
|
|
583
401
|
}
|
|
584
|
-
if (!jsonMode) console.log(` ✓
|
|
402
|
+
if (!jsonMode) console.log(` ✓ asset digest verified`);
|
|
585
403
|
|
|
586
|
-
|
|
587
|
-
if (fs.existsSync(dest)) {
|
|
588
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
589
|
-
}
|
|
590
|
-
ensureDir(dest);
|
|
404
|
+
verifySignature({ assetPath: tmpFile, scope, entry, lenient: true });
|
|
591
405
|
|
|
592
|
-
|
|
406
|
+
const installed = installAsset({
|
|
407
|
+
sourcePath: tmpFile,
|
|
408
|
+
name: entry.name,
|
|
409
|
+
version: entry.version,
|
|
410
|
+
source: {
|
|
411
|
+
type: 'registry',
|
|
412
|
+
name: entry.name,
|
|
413
|
+
version: entry.version,
|
|
414
|
+
asset_url: assetUrl,
|
|
415
|
+
asset_digest: expectedDigest,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
593
418
|
try {
|
|
594
419
|
fs.unlinkSync(tmpFile);
|
|
595
420
|
} catch {
|
|
596
421
|
/* ignore */
|
|
597
422
|
}
|
|
598
423
|
|
|
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
424
|
if (jsonMode) {
|
|
614
425
|
console.log(
|
|
615
426
|
JSON.stringify({
|
|
616
427
|
name: entry.name,
|
|
617
428
|
version: entry.version,
|
|
618
429
|
installed: true,
|
|
619
|
-
path:
|
|
430
|
+
path: installed.asset_path,
|
|
620
431
|
type: entry.type || 'domain',
|
|
621
432
|
}),
|
|
622
433
|
);
|
|
623
434
|
} else {
|
|
624
435
|
console.log(`✓ Installed ${entry.name}@${entry.version}`);
|
|
625
|
-
console.log(`
|
|
436
|
+
console.log(` Asset: ${installed.asset_path}`);
|
|
626
437
|
}
|
|
627
438
|
}
|
|
628
439
|
|
|
@@ -637,8 +448,8 @@ function installCluster(clusterEntry, resolver, _yes, jsonMode = false) {
|
|
|
637
448
|
for (const sub of subdomains) {
|
|
638
449
|
try {
|
|
639
450
|
const resolved = resolver.resolve(sub);
|
|
640
|
-
if (!resolved.entry.
|
|
641
|
-
if (!jsonMode) console.warn(` ⚠ ${sub}: no
|
|
451
|
+
if (!resolved.entry.asset_url) {
|
|
452
|
+
if (!jsonMode) console.warn(` ⚠ ${sub}: no asset_url (skipping)`);
|
|
642
453
|
continue;
|
|
643
454
|
}
|
|
644
455
|
if (!jsonMode) console.log('');
|
|
@@ -648,26 +459,6 @@ function installCluster(clusterEntry, resolver, _yes, jsonMode = false) {
|
|
|
648
459
|
}
|
|
649
460
|
}
|
|
650
461
|
|
|
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
462
|
if (jsonMode) {
|
|
672
463
|
console.log(
|
|
673
464
|
JSON.stringify({
|
|
@@ -675,7 +466,6 @@ function installCluster(clusterEntry, resolver, _yes, jsonMode = false) {
|
|
|
675
466
|
version: clusterEntry.version,
|
|
676
467
|
type: 'cluster',
|
|
677
468
|
installed: true,
|
|
678
|
-
path: clusterDest,
|
|
679
469
|
subdomains: subdomains.length,
|
|
680
470
|
}),
|
|
681
471
|
);
|
|
@@ -688,163 +478,64 @@ function installCluster(clusterEntry, resolver, _yes, jsonMode = false) {
|
|
|
688
478
|
function installFromLocalFile(filePath, _yes, jsonMode = false) {
|
|
689
479
|
const abs = path.resolve(filePath);
|
|
690
480
|
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) error(`Not a file: ${abs}`);
|
|
481
|
+
if (!abs.endsWith('.kdna')) error(`Not a .kdna asset: ${abs}`, EXIT.INPUT_ERROR);
|
|
691
482
|
|
|
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'));
|
|
483
|
+
const manifest = readContainerJson(abs, 'kdna.json');
|
|
743
484
|
const declared = manifest?.name;
|
|
744
485
|
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
|
-
);
|
|
486
|
+
error(scopedNameError('package kdna.json.name', declared), EXIT.INPUT_ERROR);
|
|
750
487
|
}
|
|
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');
|
|
488
|
+
const installed = installAsset({
|
|
489
|
+
sourcePath: abs,
|
|
490
|
+
name: declared,
|
|
491
|
+
version: manifest.version,
|
|
492
|
+
source: { type: 'local-file', path: abs },
|
|
493
|
+
});
|
|
765
494
|
|
|
766
495
|
if (jsonMode) {
|
|
767
496
|
console.log(
|
|
768
497
|
JSON.stringify({
|
|
769
498
|
name: declared,
|
|
770
499
|
installed: true,
|
|
771
|
-
path:
|
|
500
|
+
path: installed.asset_path,
|
|
501
|
+
receipt_path: installed.receipt_path,
|
|
502
|
+
asset_digest: installed.asset_digest,
|
|
503
|
+
content_digest: installed.content_digest,
|
|
772
504
|
source: 'local-file',
|
|
773
505
|
source_path: abs,
|
|
774
|
-
encrypted: isEncrypted,
|
|
775
506
|
}),
|
|
776
507
|
);
|
|
777
508
|
} else {
|
|
778
|
-
console.log(`✓ Installed ${declared} from
|
|
779
|
-
console.log(`
|
|
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
|
-
}),
|
|
813
|
-
);
|
|
814
|
-
} else {
|
|
815
|
-
console.log(`✓ Installed ${declared} from local directory (dev mode)`);
|
|
816
|
-
console.log(` Location: ${dest}`);
|
|
509
|
+
console.log(`✓ Installed ${declared} from local .kdna asset`);
|
|
510
|
+
console.log(` Asset: ${installed.asset_path}`);
|
|
817
511
|
}
|
|
818
512
|
}
|
|
819
513
|
|
|
820
514
|
// ─── Remove ─────────────────────────────────────────────────────────────
|
|
821
515
|
|
|
822
516
|
function cmdRemove(input) {
|
|
823
|
-
warnLegacy();
|
|
824
517
|
const parsed = parseName(input);
|
|
825
518
|
if (!parsed) error(`Invalid name "${input}". Use @scope/name or bare name.`);
|
|
826
|
-
|
|
827
|
-
if (!fs.existsSync(dest)) {
|
|
519
|
+
if (!removeInstalled(parsed.full)) {
|
|
828
520
|
console.log(`${parsed.full} is not installed.`);
|
|
829
521
|
return;
|
|
830
522
|
}
|
|
831
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
832
523
|
console.log(`✓ Removed ${parsed.full}`);
|
|
833
524
|
}
|
|
834
525
|
|
|
835
526
|
// ─── Info ───────────────────────────────────────────────────────────────
|
|
836
527
|
|
|
837
528
|
function cmdInfo(input, jsonMode = false) {
|
|
838
|
-
warnLegacy();
|
|
839
529
|
const parsed = parseName(input);
|
|
840
530
|
if (!parsed) error(`Invalid name "${input}".`, EXIT.INPUT_ERROR);
|
|
841
|
-
const
|
|
842
|
-
if (!
|
|
531
|
+
const installed = getInstalled(parsed.full);
|
|
532
|
+
if (!installed) error(`${parsed.full} is not installed.`, EXIT.INPUT_ERROR);
|
|
843
533
|
|
|
844
|
-
const
|
|
845
|
-
const
|
|
846
|
-
const
|
|
847
|
-
const
|
|
534
|
+
const container = readContainer(installed.asset_path);
|
|
535
|
+
const manifest = container.manifest || {};
|
|
536
|
+
const core = container.core || {};
|
|
537
|
+
const pat = container.patterns || {};
|
|
538
|
+
const source = installed.source || {};
|
|
848
539
|
|
|
849
540
|
// ─── Judgment surface (computed for both modes) ────────────────────
|
|
850
541
|
const axiomCount = (core?.axioms || []).length;
|
|
@@ -870,17 +561,7 @@ function cmdInfo(input, jsonMode = false) {
|
|
|
870
561
|
}
|
|
871
562
|
|
|
872
563
|
// ─── 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
|
-
}
|
|
564
|
+
const evalInfo = null;
|
|
884
565
|
|
|
885
566
|
// ─── Known risks ───────────────────────────────────────────────────
|
|
886
567
|
const risks = [];
|
|
@@ -899,7 +580,7 @@ function cmdInfo(input, jsonMode = false) {
|
|
|
899
580
|
'KDNA_Reasoning.json',
|
|
900
581
|
'KDNA_Evolution.json',
|
|
901
582
|
];
|
|
902
|
-
const present = expected.filter((f) =>
|
|
583
|
+
const present = expected.filter((f) => container.files.includes(f));
|
|
903
584
|
|
|
904
585
|
// ─── JSON mode: emit structured output only, then exit ─────────────
|
|
905
586
|
if (jsonMode) {
|
|
@@ -912,10 +593,12 @@ function cmdInfo(input, jsonMode = false) {
|
|
|
912
593
|
author: manifest?.author?.name || '?',
|
|
913
594
|
pubkey: manifest?.author?.pubkey || null,
|
|
914
595
|
has_pem: !!manifest?.author?.public_key_pem,
|
|
915
|
-
source_url: source.
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
596
|
+
source_url: source.asset_url || null,
|
|
597
|
+
asset_digest: installed.asset_digest || source.asset_digest || null,
|
|
598
|
+
content_digest: installed.content_digest || null,
|
|
599
|
+
receipt_path: installed.receipt_path || null,
|
|
600
|
+
installed_at: installed.installed_at || null,
|
|
601
|
+
path: installed.asset_path,
|
|
919
602
|
axioms: axiomCount,
|
|
920
603
|
ontology: ontologyCount,
|
|
921
604
|
stances: stanceCount,
|
|
@@ -953,10 +636,14 @@ function cmdInfo(input, jsonMode = false) {
|
|
|
953
636
|
} else {
|
|
954
637
|
console.log(` Embedded PEM: no (legacy pre-v0.7.1 package)`);
|
|
955
638
|
}
|
|
956
|
-
if (source.
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
console.log(`
|
|
639
|
+
if (source.asset_url) {
|
|
640
|
+
console.log(` Source URL: ${source.asset_url}`);
|
|
641
|
+
}
|
|
642
|
+
if (installed.asset_digest) console.log(` Asset digest: ${installed.asset_digest.slice(0, 39)}…`);
|
|
643
|
+
if (installed.content_digest) console.log(` Content digest: ${installed.content_digest.slice(0, 39)}…`);
|
|
644
|
+
if (installed.receipt_path) console.log(` Receipt: ${installed.receipt_path}`);
|
|
645
|
+
console.log(` Installed: ${installed.installed_at || '?'}`);
|
|
646
|
+
console.log(` Asset: ${installed.asset_path}`);
|
|
960
647
|
|
|
961
648
|
// ─── Judgment surface ──────────────────────────────────────────
|
|
962
649
|
console.log('');
|
|
@@ -1008,16 +695,14 @@ function cmdInfo(input, jsonMode = false) {
|
|
|
1008
695
|
// ─── Update ─────────────────────────────────────────────────────────────
|
|
1009
696
|
|
|
1010
697
|
function cmdUpdate(input) {
|
|
1011
|
-
warnLegacy();
|
|
1012
698
|
const parsed = parseName(input);
|
|
1013
699
|
if (!parsed) error(`Invalid name "${input}".`);
|
|
1014
|
-
const
|
|
1015
|
-
if (!
|
|
700
|
+
const installed = getInstalled(parsed.full);
|
|
701
|
+
if (!installed) {
|
|
1016
702
|
console.log(`${parsed.full} not installed. Run: kdna install ${input}`);
|
|
1017
703
|
return;
|
|
1018
704
|
}
|
|
1019
|
-
const
|
|
1020
|
-
const installedVersion = manifest.version || manifest._source?.version || '?';
|
|
705
|
+
const installedVersion = installed.version || '?';
|
|
1021
706
|
|
|
1022
707
|
const resolver = new RegistryResolver({ allowNetwork: true, refresh: true });
|
|
1023
708
|
let entry;
|
|
@@ -1036,24 +721,17 @@ function cmdUpdate(input) {
|
|
|
1036
721
|
}
|
|
1037
722
|
|
|
1038
723
|
function cmdUpdateAll() {
|
|
1039
|
-
|
|
1040
|
-
if (!
|
|
724
|
+
const installed = listInstalled();
|
|
725
|
+
if (!installed.length) {
|
|
1041
726
|
console.log('No installs.');
|
|
1042
727
|
return;
|
|
1043
728
|
}
|
|
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
|
-
}
|
|
729
|
+
for (const entry of installed) {
|
|
730
|
+
try {
|
|
731
|
+
cmdUpdate(entry.full);
|
|
732
|
+
} catch (e) {
|
|
733
|
+
console.warn(` ⚠ ${entry.full}: ${e.message.split('\n')[0]}`);
|
|
1055
734
|
}
|
|
1056
|
-
console.log('');
|
|
1057
735
|
}
|
|
1058
736
|
}
|
|
1059
737
|
|