@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/cmds/domain.js
CHANGED
|
@@ -2,15 +2,6 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const { error, readJson, writeJson, EXIT } = require('./_common');
|
|
5
|
-
const {
|
|
6
|
-
encrypt,
|
|
7
|
-
decrypt,
|
|
8
|
-
deriveKey,
|
|
9
|
-
machineFingerprint,
|
|
10
|
-
isEncryptable,
|
|
11
|
-
ENCRYPTED_FILES,
|
|
12
|
-
} = require('./encrypt');
|
|
13
|
-
|
|
14
5
|
const KDNA_DOMAIN_FILES = new Set([
|
|
15
6
|
'KDNA_Core.json',
|
|
16
7
|
'KDNA_Patterns.json',
|
|
@@ -244,12 +235,17 @@ function cmdPack(dir, outputDir) {
|
|
|
244
235
|
.readdirSync(abs)
|
|
245
236
|
.filter((f) => f.endsWith('.json') && f !== 'kdna.json').length;
|
|
246
237
|
manifest = {
|
|
247
|
-
|
|
238
|
+
format: 'kdna',
|
|
239
|
+
format_version: '1.0',
|
|
240
|
+
spec_version: '1.0-rc',
|
|
248
241
|
name: domainName,
|
|
249
242
|
version: core.meta?.version || '0.1.0',
|
|
243
|
+
judgment_version: core.meta?.version || '0.1.0',
|
|
250
244
|
status: 'experimental',
|
|
245
|
+
quality_badge: 'untested',
|
|
251
246
|
access: 'open',
|
|
252
|
-
|
|
247
|
+
languages: ['en'],
|
|
248
|
+
default_language: 'en',
|
|
253
249
|
author: { name: '', id: '' },
|
|
254
250
|
license: { type: 'CC-BY-4.0' },
|
|
255
251
|
description: core.meta?.purpose || `${domainName} domain cognition`,
|
|
@@ -277,6 +273,7 @@ function cmdPack(dir, outputDir) {
|
|
|
277
273
|
src = ${JSON.stringify(abs)}
|
|
278
274
|
out = ${JSON.stringify(outPath)}
|
|
279
275
|
with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
276
|
+
zf.writestr(zipfile.ZipInfo('mimetype'), 'application/vnd.aikdna.kdna+zip', compress_type=zipfile.ZIP_STORED)
|
|
280
277
|
for f in sorted(os.listdir(src)):
|
|
281
278
|
fp = os.path.join(src, f)
|
|
282
279
|
if os.path.isfile(fp) and (f.endswith('.json') or f in ('README.md', 'LICENSE', 'kdna.json')):
|
|
@@ -295,7 +292,17 @@ with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
|
295
292
|
}
|
|
296
293
|
}
|
|
297
294
|
|
|
298
|
-
// Strategy 2:
|
|
295
|
+
// Strategy 2: Node.js native ZIP, which preserves the required mimetype entry
|
|
296
|
+
if (!packed) {
|
|
297
|
+
try {
|
|
298
|
+
createNodeZip(abs, outPath);
|
|
299
|
+
packed = true;
|
|
300
|
+
} catch {
|
|
301
|
+
/* try external zip last */
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Strategy 3: system zip command
|
|
299
306
|
if (!packed) {
|
|
300
307
|
const cwd = process.cwd();
|
|
301
308
|
try {
|
|
@@ -311,16 +318,6 @@ with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
|
311
318
|
}
|
|
312
319
|
}
|
|
313
320
|
|
|
314
|
-
// #22: Strategy 3 — Node.js native ZIP (no external dependencies)
|
|
315
|
-
if (!packed) {
|
|
316
|
-
try {
|
|
317
|
-
createNodeZip(abs, outPath);
|
|
318
|
-
packed = true;
|
|
319
|
-
} catch {
|
|
320
|
-
/* last attempt failed */
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
321
|
if (!packed) {
|
|
325
322
|
const platform = process.platform;
|
|
326
323
|
const hints = {
|
|
@@ -350,11 +347,14 @@ function createNodeZip(srcDir, outPath) {
|
|
|
350
347
|
const fileData = [];
|
|
351
348
|
let offset = 0;
|
|
352
349
|
|
|
353
|
-
for (const f of files) {
|
|
354
|
-
const raw =
|
|
350
|
+
for (const f of ['mimetype', ...files]) {
|
|
351
|
+
const raw =
|
|
352
|
+
f === 'mimetype'
|
|
353
|
+
? Buffer.from('application/vnd.aikdna.kdna+zip')
|
|
354
|
+
: fs.readFileSync(path.join(srcDir, f));
|
|
355
355
|
const crc = crc32(raw);
|
|
356
356
|
const compressed = zlib.deflateRawSync(raw);
|
|
357
|
-
const useStore = compressed.length >= raw.length;
|
|
357
|
+
const useStore = f === 'mimetype' || compressed.length >= raw.length;
|
|
358
358
|
|
|
359
359
|
const nameBytes = Buffer.from(f, 'utf8');
|
|
360
360
|
const localHeader = Buffer.alloc(30);
|
|
@@ -469,7 +469,7 @@ zf.close()
|
|
|
469
469
|
files.forEach((f) => console.log(` ${f}`));
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
-
// ─── Inspect .kdna file (ZIP container
|
|
472
|
+
// ─── Inspect .kdna file (ZIP container) ──────────────────────────────────
|
|
473
473
|
|
|
474
474
|
function inspectKdnaFile(filePath, jsonMode = false) {
|
|
475
475
|
const abs = path.resolve(filePath);
|
|
@@ -481,107 +481,48 @@ function inspectKdnaFile(filePath, jsonMode = false) {
|
|
|
481
481
|
fs.readSync(fd, head, 0, 4, 0);
|
|
482
482
|
fs.closeSync(fd);
|
|
483
483
|
const isZip = head[0] === 0x50 && head[1] === 0x4b;
|
|
484
|
+
if (!isZip) error('Invalid .kdna asset: expected ZIP container');
|
|
484
485
|
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
} catch {
|
|
513
|
-
try {
|
|
514
|
-
execSync(`unzip -q -o "${abs}" -d "${tmpDir}"`, { stdio: 'pipe' });
|
|
515
|
-
} catch {
|
|
516
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
517
|
-
error('Cannot read .kdna container. Install python3 or unzip.');
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
core = readJson(path.join(tmpDir, 'KDNA_Core.json'));
|
|
522
|
-
patterns = readJson(path.join(tmpDir, 'KDNA_Patterns.json'));
|
|
523
|
-
manifest = readJson(path.join(tmpDir, 'kdna.json'));
|
|
524
|
-
|
|
525
|
-
for (const f of fs.readdirSync(tmpDir)) {
|
|
526
|
-
if (f.startsWith('KDNA_') && f.endsWith('.json')) {
|
|
527
|
-
presentFiles.push(f);
|
|
528
|
-
}
|
|
529
|
-
if (f === 'README.md' || f === 'LICENSE') presentFiles.push(f);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
533
|
-
} else {
|
|
534
|
-
// Legacy merged JSON/YAML format (deprecated)
|
|
535
|
-
const raw = fs.readFileSync(abs, 'utf8');
|
|
536
|
-
let data;
|
|
537
|
-
try {
|
|
538
|
-
data = JSON.parse(raw);
|
|
539
|
-
} catch {
|
|
540
|
-
data = parseSimpleYaml(raw);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (!data || !data.meta) error(`Invalid .kdna file: missing meta section`);
|
|
544
|
-
|
|
545
|
-
const m = data.meta || {};
|
|
546
|
-
manifest = {
|
|
547
|
-
name: m.name || m.domain,
|
|
548
|
-
version: m.version || '?',
|
|
549
|
-
status: data.status || '?',
|
|
550
|
-
access: data.access || '?',
|
|
551
|
-
language: data.language || '?',
|
|
552
|
-
author: data.author || { name: '?' },
|
|
553
|
-
license: data.license || { type: '?' },
|
|
554
|
-
description: data.description || m.purpose || '?',
|
|
555
|
-
spec_version: m.spec_version || data.kdna_spec || '?',
|
|
556
|
-
};
|
|
557
|
-
core = data.core || {};
|
|
558
|
-
patterns = data.patterns || {};
|
|
559
|
-
presentFiles.push('.kdna (legacy merged format)');
|
|
560
|
-
if (data.scenarios) {
|
|
561
|
-
presentFiles.push('scenarios (inline)');
|
|
562
|
-
}
|
|
563
|
-
if (data.cases) {
|
|
564
|
-
presentFiles.push('cases (inline)');
|
|
565
|
-
}
|
|
566
|
-
if (data.reasoning) {
|
|
567
|
-
presentFiles.push('reasoning (inline)');
|
|
568
|
-
}
|
|
569
|
-
if (data.evolution) {
|
|
570
|
-
presentFiles.push('evolution (inline)');
|
|
571
|
-
}
|
|
486
|
+
const { listContainerEntries, readContainerJson } = require('../package-store');
|
|
487
|
+
const { licenseDecryptOptionsForManifest } = require('./license');
|
|
488
|
+
const presentFiles = listContainerEntries(abs).filter(
|
|
489
|
+
(f) => (f.startsWith('KDNA_') && f.endsWith('.json')) || f === 'README.md' || f === 'LICENSE',
|
|
490
|
+
);
|
|
491
|
+
const manifest = readContainerJson(abs, 'kdna.json') || {};
|
|
492
|
+
const encryptedEntries = Array.isArray(manifest.encryption?.encrypted_entries)
|
|
493
|
+
? manifest.encryption.encrypted_entries
|
|
494
|
+
: [];
|
|
495
|
+
let decryptOptions = {};
|
|
496
|
+
let decryptError = null;
|
|
497
|
+
if (encryptedEntries.length) {
|
|
498
|
+
const licensed = licenseDecryptOptionsForManifest(manifest);
|
|
499
|
+
if (licensed.ok) decryptOptions = { decryptEntry: licensed.decryptEntry };
|
|
500
|
+
else decryptError = licensed.error;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
let core = null;
|
|
504
|
+
let patterns = null;
|
|
505
|
+
try {
|
|
506
|
+
core = decryptError ? null : readContainerJson(abs, 'KDNA_Core.json', decryptOptions);
|
|
507
|
+
patterns = decryptError ? null : readContainerJson(abs, 'KDNA_Patterns.json', decryptOptions);
|
|
508
|
+
} catch (e) {
|
|
509
|
+
if (!encryptedEntries.length) error(`Cannot inspect .kdna asset: ${e.message}`);
|
|
510
|
+
decryptError = e.message;
|
|
572
511
|
}
|
|
573
512
|
|
|
574
|
-
if (!core
|
|
513
|
+
if (!core && !encryptedEntries.includes('KDNA_Core.json')) {
|
|
514
|
+
error('KDNA_Core.json not found in container');
|
|
515
|
+
}
|
|
575
516
|
|
|
576
517
|
const m = manifest || {};
|
|
577
|
-
const c = core;
|
|
518
|
+
const c = core || {};
|
|
578
519
|
const p = patterns || {};
|
|
579
520
|
|
|
580
521
|
if (jsonMode) {
|
|
581
522
|
const result = {
|
|
582
523
|
name: m.name || c.meta?.domain || path.basename(abs, '.kdna'),
|
|
583
|
-
format:
|
|
584
|
-
spec: m.spec_version ||
|
|
524
|
+
format: 'kdna-zip',
|
|
525
|
+
spec: m.spec_version || null,
|
|
585
526
|
version: m.version || null,
|
|
586
527
|
status: m.status || 'experimental',
|
|
587
528
|
access: m.access || 'open',
|
|
@@ -589,6 +530,9 @@ zf.close()
|
|
|
589
530
|
license: m.license?.type || null,
|
|
590
531
|
created: m.created || c.meta?.created || null,
|
|
591
532
|
description: m.description || c.meta?.purpose || null,
|
|
533
|
+
protected: encryptedEntries.length > 0,
|
|
534
|
+
encrypted_entries: encryptedEntries,
|
|
535
|
+
license_required: !!decryptError,
|
|
592
536
|
content: {
|
|
593
537
|
axioms: (c.axioms || []).length,
|
|
594
538
|
ontology: (c.ontology || []).length,
|
|
@@ -608,8 +552,8 @@ zf.close()
|
|
|
608
552
|
console.log(` ${m.name || c.meta?.domain || path.basename(abs, '.kdna')} — KDNA Domain`);
|
|
609
553
|
console.log('═'.repeat(50));
|
|
610
554
|
console.log('');
|
|
611
|
-
console.log(` Format: .kdna
|
|
612
|
-
console.log(` Spec: ${m.spec_version ||
|
|
555
|
+
console.log(` Format: .kdna (ZIP container)`);
|
|
556
|
+
console.log(` Spec: ${m.spec_version || '?'}`);
|
|
613
557
|
console.log(` Version: ${m.version || '?'}`);
|
|
614
558
|
console.log(` Status: ${m.status || 'experimental'}`);
|
|
615
559
|
console.log(` Access: ${m.access || 'open'}`);
|
|
@@ -617,6 +561,10 @@ zf.close()
|
|
|
617
561
|
console.log(` License: ${m.license?.type || '?'}`);
|
|
618
562
|
console.log(` Created: ${m.created || c.meta?.created || '?'}`);
|
|
619
563
|
console.log(` Description: ${m.description || c.meta?.purpose || '?'}`);
|
|
564
|
+
if (encryptedEntries.length) {
|
|
565
|
+
console.log(` Protected: ${encryptedEntries.join(', ')}`);
|
|
566
|
+
if (decryptError) console.log(` Activation: required (${decryptError})`);
|
|
567
|
+
}
|
|
620
568
|
console.log('');
|
|
621
569
|
console.log(' ── Content ──');
|
|
622
570
|
console.log(` Axioms: ${(c.axioms || []).length}`);
|
|
@@ -633,57 +581,9 @@ zf.close()
|
|
|
633
581
|
console.log('═'.repeat(50));
|
|
634
582
|
}
|
|
635
583
|
|
|
636
|
-
function parseSimpleYaml(raw) {
|
|
637
|
-
// Parse a simple subset of YAML (no nesting beyond 1 level for sections)
|
|
638
|
-
const result = {};
|
|
639
|
-
let currentSection = null;
|
|
640
|
-
|
|
641
|
-
const lines = raw.split('\n');
|
|
642
|
-
for (const line of lines) {
|
|
643
|
-
const trimmed = line.trim();
|
|
644
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
645
|
-
|
|
646
|
-
// Section header: "core:" or " core:" etc
|
|
647
|
-
if (/^[a-z_]+:$/.test(trimmed)) {
|
|
648
|
-
currentSection = trimmed.slice(0, -1);
|
|
649
|
-
if (!result[currentSection]) result[currentSection] = {};
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Key: value
|
|
654
|
-
const kv = trimmed.match(/^([a-z_]+):\s*(.*)/i);
|
|
655
|
-
if (kv && !kv[1].startsWith('-')) {
|
|
656
|
-
const key = kv[1];
|
|
657
|
-
const val = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
658
|
-
if (currentSection) {
|
|
659
|
-
if (key === 'version' && typeof result[currentSection] === 'object') {
|
|
660
|
-
result[currentSection][key] = val;
|
|
661
|
-
} else if (!result[currentSection][key]) {
|
|
662
|
-
result[currentSection][key] = val;
|
|
663
|
-
}
|
|
664
|
-
} else {
|
|
665
|
-
result[key] = val;
|
|
666
|
-
}
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Array item: "- value"
|
|
671
|
-
if (trimmed.startsWith('- ') && currentSection) {
|
|
672
|
-
// For counts only, we don't parse full arrays
|
|
673
|
-
if (currentSection === 'axioms' || currentSection === 'stances') {
|
|
674
|
-
if (!result.core) result.core = {};
|
|
675
|
-
if (!result.core[currentSection]) result.core[currentSection] = [];
|
|
676
|
-
result.core[currentSection].push({ _parsed: true });
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
return result;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
584
|
// ─── Inspect ───────────────────────────────────────────────────────────
|
|
685
585
|
|
|
686
|
-
function cmdInspect(dir, jsonMode = false, locale = null) {
|
|
586
|
+
function cmdInspect(dir, jsonMode = false, locale = null, options = {}) {
|
|
687
587
|
const abs = path.resolve(dir);
|
|
688
588
|
const stat = fs.existsSync(abs) ? fs.statSync(abs) : null;
|
|
689
589
|
if (!stat) error(`Path not found: ${abs}`, EXIT.INPUT_ERROR);
|
|
@@ -694,7 +594,14 @@ function cmdInspect(dir, jsonMode = false, locale = null) {
|
|
|
694
594
|
return;
|
|
695
595
|
}
|
|
696
596
|
|
|
697
|
-
|
|
597
|
+
if (stat.isDirectory() && !options.allowDirectory) {
|
|
598
|
+
error(
|
|
599
|
+
'Directory inspection is a dev-only operation. Use: kdna dev inspect <source-dir>',
|
|
600
|
+
EXIT.INPUT_ERROR,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Dev source directory
|
|
698
605
|
if (!stat.isDirectory()) error(`Not a KDNA domain: ${abs}`, EXIT.INPUT_ERROR);
|
|
699
606
|
|
|
700
607
|
const core = readJson(path.join(abs, 'KDNA_Core.json'));
|
|
@@ -722,6 +629,16 @@ function cmdInspect(dir, jsonMode = false, locale = null) {
|
|
|
722
629
|
];
|
|
723
630
|
const filesPresent = expected.filter((f) => fs.existsSync(path.join(abs, f)));
|
|
724
631
|
|
|
632
|
+
// Governance metadata (with locale support)
|
|
633
|
+
let kdnaCard = readJson(path.join(abs, 'KDNA_CARD.json'));
|
|
634
|
+
if (locale && !kdnaCard) {
|
|
635
|
+
kdnaCard = readJson(path.join(abs, 'locales', locale, 'KDNA_CARD.json'));
|
|
636
|
+
}
|
|
637
|
+
if (locale && kdnaCard) {
|
|
638
|
+
const localeCard = readJson(path.join(abs, 'locales', locale, 'KDNA_CARD.json'));
|
|
639
|
+
if (localeCard) kdnaCard = localeCard;
|
|
640
|
+
}
|
|
641
|
+
|
|
725
642
|
if (jsonMode) {
|
|
726
643
|
const result = {
|
|
727
644
|
name: m.name || c.meta?.domain || path.basename(abs),
|
|
@@ -827,15 +744,6 @@ function cmdInspect(dir, jsonMode = false, locale = null) {
|
|
|
827
744
|
|
|
828
745
|
if (evo) console.log(` Evolution stages: ${(evo.stages || []).length}`);
|
|
829
746
|
|
|
830
|
-
// Governance metadata (with locale support)
|
|
831
|
-
let kdnaCard = readJson(path.join(abs, 'KDNA_CARD.json'));
|
|
832
|
-
if (locale && !kdnaCard) {
|
|
833
|
-
kdnaCard = readJson(path.join(abs, 'locales', locale, 'KDNA_CARD.json'));
|
|
834
|
-
}
|
|
835
|
-
if (locale && kdnaCard) {
|
|
836
|
-
const localeCard = readJson(path.join(abs, 'locales', locale, 'KDNA_CARD.json'));
|
|
837
|
-
if (localeCard) kdnaCard = localeCard;
|
|
838
|
-
}
|
|
839
747
|
if (kdnaCard) {
|
|
840
748
|
const displayName = kdnaCard.display_name || '';
|
|
841
749
|
const summary = kdnaCard.summary || '';
|
|
@@ -870,265 +778,46 @@ function cmdInspect(dir, jsonMode = false, locale = null) {
|
|
|
870
778
|
console.log('═'.repeat(50));
|
|
871
779
|
}
|
|
872
780
|
|
|
873
|
-
// ───
|
|
781
|
+
// ─── KDNA Card (locale-aware) ────────────────────────────────────
|
|
874
782
|
|
|
875
|
-
function
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
|
|
879
|
-
}
|
|
783
|
+
function readCardFromDirectory(abs, locale = null) {
|
|
784
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) return null;
|
|
785
|
+
let card = readJson(path.join(abs, 'KDNA_CARD.json'));
|
|
880
786
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
787
|
+
if (locale) {
|
|
788
|
+
const localeCard = readJson(path.join(abs, 'locales', locale, 'KDNA_CARD.json'));
|
|
789
|
+
if (localeCard) {
|
|
790
|
+
card = { ...card, ...localeCard };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
885
793
|
|
|
886
|
-
|
|
794
|
+
return card;
|
|
795
|
+
}
|
|
887
796
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
.filter((f) => f.endsWith('.json') && f !== 'kdna.json').length;
|
|
893
|
-
manifest = {
|
|
894
|
-
kdna_spec: '1.0-rc',
|
|
895
|
-
name: domainName,
|
|
896
|
-
version: core.meta?.version || '0.1.0',
|
|
897
|
-
status: 'experimental',
|
|
898
|
-
access: 'licensed',
|
|
899
|
-
language: 'en',
|
|
900
|
-
author: { name: '', id: '' },
|
|
901
|
-
license: { type: 'KCL-1.0' },
|
|
902
|
-
description: core.meta?.purpose || `${domainName} domain cognition`,
|
|
903
|
-
file_count: jsonCount,
|
|
904
|
-
created: new Date().toISOString().slice(0, 10),
|
|
905
|
-
updated: new Date().toISOString().slice(0, 10),
|
|
906
|
-
};
|
|
907
|
-
writeJson(path.join(abs, 'kdna.json'), manifest);
|
|
908
|
-
}
|
|
797
|
+
function cmdCard(dir, jsonMode = false, locale = null, options = {}) {
|
|
798
|
+
const abs = path.resolve(dir);
|
|
799
|
+
const stat = fs.existsSync(abs) ? fs.statSync(abs) : null;
|
|
800
|
+
if (!stat) error(`Path not found: ${abs}`, EXIT.INPUT_ERROR);
|
|
909
801
|
|
|
910
|
-
|
|
911
|
-
const licenseIdx = args.indexOf('--license');
|
|
912
|
-
const keyIdx = args.indexOf('--key');
|
|
913
|
-
let encKey;
|
|
914
|
-
|
|
915
|
-
if (licenseIdx >= 0) {
|
|
916
|
-
const licensePath = args[licenseIdx + 1];
|
|
917
|
-
if (!licensePath || !fs.existsSync(licensePath))
|
|
918
|
-
error('License file not found', EXIT.INPUT_ERROR);
|
|
919
|
-
const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
920
|
-
const licenseKey = license.license_id;
|
|
921
|
-
const fp = machineFingerprint();
|
|
922
|
-
encKey = deriveKey(licenseKey, fp);
|
|
923
|
-
} else if (keyIdx >= 0) {
|
|
924
|
-
const rawKey = args[keyIdx + 1];
|
|
925
|
-
if (!rawKey) error('--key requires a value', EXIT.INPUT_ERROR);
|
|
926
|
-
encKey = deriveKey(rawKey, machineFingerprint());
|
|
927
|
-
} else {
|
|
802
|
+
if (stat.isDirectory() && !options.allowDirectory) {
|
|
928
803
|
error(
|
|
929
|
-
'
|
|
804
|
+
'Directory card inspection is a dev-only operation. Use: kdna dev card <source-dir>',
|
|
930
805
|
EXIT.INPUT_ERROR,
|
|
931
806
|
);
|
|
932
807
|
}
|
|
933
808
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const files = fs.readdirSync(abs).filter((f) => {
|
|
942
|
-
if (f.startsWith('.')) return false;
|
|
943
|
-
const ext = path.extname(f);
|
|
944
|
-
return ext === '.json' || f === 'README.md' || f === 'LICENSE' || f === 'kdna.json';
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
const centralDir = [];
|
|
948
|
-
const fileData = [];
|
|
949
|
-
let offset = 0;
|
|
950
|
-
|
|
951
|
-
for (const f of files.sort()) {
|
|
952
|
-
let raw = fs.readFileSync(path.join(abs, f));
|
|
953
|
-
let storedName = f;
|
|
954
|
-
|
|
955
|
-
if (isEncryptable(f)) {
|
|
956
|
-
try {
|
|
957
|
-
const encrypted = encrypt(raw.toString('utf8'), encKey);
|
|
958
|
-
raw = encrypted;
|
|
959
|
-
storedName = f; // Keep original name, content is encrypted
|
|
960
|
-
} catch (err) {
|
|
961
|
-
error(`Failed to encrypt ${f}: ${err.message}`);
|
|
962
|
-
}
|
|
809
|
+
let card = null;
|
|
810
|
+
if (stat.isFile() && abs.endsWith('.kdna')) {
|
|
811
|
+
const { readContainerJson } = require('../package-store');
|
|
812
|
+
card = readContainerJson(abs, 'KDNA_CARD.json') || null;
|
|
813
|
+
if (locale) {
|
|
814
|
+
const localeCard = readContainerJson(abs, `locales/${locale}/KDNA_CARD.json`) || null;
|
|
815
|
+
if (localeCard) card = { ...card, ...localeCard };
|
|
963
816
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const compressed = zlib.deflateRawSync(raw);
|
|
967
|
-
const useStore = compressed.length >= raw.length;
|
|
968
|
-
const stored = useStore ? raw : compressed;
|
|
969
|
-
|
|
970
|
-
const nameBytes = Buffer.from(storedName, 'utf8');
|
|
971
|
-
const localHeader = Buffer.alloc(30);
|
|
972
|
-
localHeader.writeUInt32LE(0x04034b50, 0);
|
|
973
|
-
localHeader.writeUInt16LE(20, 4);
|
|
974
|
-
localHeader.writeUInt16LE(0x0800, 6);
|
|
975
|
-
localHeader.writeUInt16LE(useStore ? 0 : 8, 8);
|
|
976
|
-
localHeader.writeUInt32LE(crc, 14);
|
|
977
|
-
localHeader.writeUInt32LE(useStore ? raw.length : compressed.length, 18);
|
|
978
|
-
localHeader.writeUInt32LE(raw.length, 22);
|
|
979
|
-
localHeader.writeUInt16LE(nameBytes.length, 26);
|
|
980
|
-
|
|
981
|
-
fileData.push(Buffer.concat([localHeader, nameBytes, stored]));
|
|
982
|
-
offset += localHeader.length + nameBytes.length + stored.length;
|
|
983
|
-
|
|
984
|
-
const cdEntry = Buffer.alloc(46);
|
|
985
|
-
cdEntry.writeUInt32LE(0x02014b50, 0);
|
|
986
|
-
cdEntry.writeUInt16LE(20, 4);
|
|
987
|
-
cdEntry.writeUInt16LE(20, 6);
|
|
988
|
-
cdEntry.writeUInt16LE(0x0800, 8);
|
|
989
|
-
cdEntry.writeUInt16LE(useStore ? 0 : 8, 10);
|
|
990
|
-
cdEntry.writeUInt32LE(crc, 16);
|
|
991
|
-
cdEntry.writeUInt32LE(useStore ? raw.length : compressed.length, 20);
|
|
992
|
-
cdEntry.writeUInt32LE(raw.length, 24);
|
|
993
|
-
cdEntry.writeUInt16LE(nameBytes.length, 28);
|
|
994
|
-
cdEntry.writeUInt32LE(offset - stored.length - nameBytes.length - localHeader.length, 42);
|
|
995
|
-
centralDir.push(Buffer.concat([cdEntry, nameBytes]));
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const cdOffset = offset;
|
|
999
|
-
const cdSize = centralDir.reduce((s, e) => s + e.length, 0);
|
|
1000
|
-
const eocd = Buffer.alloc(22);
|
|
1001
|
-
eocd.writeUInt32LE(0x06054b50, 0);
|
|
1002
|
-
eocd.writeUInt16LE(0, 4);
|
|
1003
|
-
eocd.writeUInt16LE(0, 6);
|
|
1004
|
-
eocd.writeUInt16LE(files.length, 8);
|
|
1005
|
-
eocd.writeUInt16LE(files.length, 10);
|
|
1006
|
-
eocd.writeUInt32LE(cdSize, 12);
|
|
1007
|
-
eocd.writeUInt32LE(cdOffset, 16);
|
|
1008
|
-
eocd.writeUInt16LE(0, 20);
|
|
1009
|
-
|
|
1010
|
-
const all = Buffer.concat([...fileData, ...centralDir, eocd]);
|
|
1011
|
-
fs.writeFileSync(outPath, all);
|
|
1012
|
-
|
|
1013
|
-
console.log(`✓ Encrypted pack: ${outPath}`);
|
|
1014
|
-
console.log(` Domain: ${domainName} v${manifest.version}`);
|
|
1015
|
-
console.log(
|
|
1016
|
-
` Files: ${files.length} (${files.filter(isEncryptable).length} encrypted, ${files.filter((f) => !isEncryptable(f)).length} plaintext)`,
|
|
1017
|
-
);
|
|
1018
|
-
console.log(` Container: AES-256-GCM .kdnae`);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function cmdUnpackEncrypt(filePath, args = []) {
|
|
1022
|
-
const abs = path.resolve(filePath);
|
|
1023
|
-
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
1024
|
-
error(`Not a file: ${abs}`, EXIT.INPUT_ERROR);
|
|
1025
|
-
}
|
|
1026
|
-
if (!abs.endsWith('.kdnae')) {
|
|
1027
|
-
error(`Not a .kdnae file: ${abs}`, EXIT.INPUT_ERROR);
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const licenseIdx = args.indexOf('--license');
|
|
1031
|
-
const keyIdx = args.indexOf('--key');
|
|
1032
|
-
let encKey;
|
|
1033
|
-
|
|
1034
|
-
if (licenseIdx >= 0) {
|
|
1035
|
-
const licensePath = args[licenseIdx + 1];
|
|
1036
|
-
if (!licensePath || !fs.existsSync(licensePath))
|
|
1037
|
-
error('License file not found', EXIT.INPUT_ERROR);
|
|
1038
|
-
const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
1039
|
-
const licenseKey = license.license_id;
|
|
1040
|
-
const fp = machineFingerprint();
|
|
1041
|
-
encKey = deriveKey(licenseKey, fp);
|
|
1042
|
-
} else if (keyIdx >= 0) {
|
|
1043
|
-
const rawKey = args[keyIdx + 1];
|
|
1044
|
-
if (!rawKey) error('--key requires a value', EXIT.INPUT_ERROR);
|
|
1045
|
-
encKey = deriveKey(rawKey, machineFingerprint());
|
|
817
|
+
} else if (stat.isDirectory()) {
|
|
818
|
+
card = readCardFromDirectory(abs, locale);
|
|
1046
819
|
} else {
|
|
1047
|
-
error(
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
const domainName = path.basename(abs, '.kdnae');
|
|
1051
|
-
const outDir = path.join(path.dirname(abs), domainName);
|
|
1052
|
-
const force = args.includes('--force');
|
|
1053
|
-
|
|
1054
|
-
if (fs.existsSync(outDir)) {
|
|
1055
|
-
if (!force)
|
|
1056
|
-
error(`Directory already exists: ${outDir}\nUse --force to overwrite.`, EXIT.INPUT_ERROR);
|
|
1057
|
-
fs.rmSync(outDir, { recursive: true, force: true });
|
|
1058
|
-
}
|
|
1059
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
1060
|
-
|
|
1061
|
-
// Extract ZIP first, then decrypt KDNA JSON files
|
|
1062
|
-
const os = require('os');
|
|
1063
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdnae-unpack-'));
|
|
1064
|
-
|
|
1065
|
-
try {
|
|
1066
|
-
const tmpPy = path.join(os.tmpdir(), `kdnae-unpack-${Date.now()}.py`);
|
|
1067
|
-
try {
|
|
1068
|
-
const script = `import zipfile, os
|
|
1069
|
-
zf = zipfile.ZipFile(${JSON.stringify(abs)}, 'r')
|
|
1070
|
-
zf.extractall(${JSON.stringify(tmpDir)})
|
|
1071
|
-
zf.close()
|
|
1072
|
-
`;
|
|
1073
|
-
fs.writeFileSync(tmpPy, script);
|
|
1074
|
-
execSync(`python3 ${tmpPy}`, { stdio: 'pipe' });
|
|
1075
|
-
} catch {
|
|
1076
|
-
try {
|
|
1077
|
-
execSync(`unzip -q -o "${abs}" -d "${tmpDir}"`, { stdio: 'pipe' });
|
|
1078
|
-
} catch {
|
|
1079
|
-
error('Cannot unpack .kdnae container');
|
|
1080
|
-
}
|
|
1081
|
-
} finally {
|
|
1082
|
-
try {
|
|
1083
|
-
fs.unlinkSync(tmpPy);
|
|
1084
|
-
} catch {
|
|
1085
|
-
/* cleanup */
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// Copy plaintext files, decrypt KDNA files
|
|
1090
|
-
const extracted = fs.readdirSync(tmpDir);
|
|
1091
|
-
for (const f of extracted) {
|
|
1092
|
-
const src = path.join(tmpDir, f);
|
|
1093
|
-
const dest = path.join(outDir, f);
|
|
1094
|
-
|
|
1095
|
-
if (ENCRYPTED_FILES.includes(f)) {
|
|
1096
|
-
try {
|
|
1097
|
-
const encrypted = fs.readFileSync(src);
|
|
1098
|
-
const decrypted = decrypt(encrypted, encKey);
|
|
1099
|
-
fs.writeFileSync(dest, decrypted);
|
|
1100
|
-
} catch (err) {
|
|
1101
|
-
error(`Failed to decrypt ${f}: ${err.message}. Wrong license key?`);
|
|
1102
|
-
}
|
|
1103
|
-
} else {
|
|
1104
|
-
fs.copyFileSync(src, dest);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
} finally {
|
|
1108
|
-
try {
|
|
1109
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1110
|
-
} catch {
|
|
1111
|
-
/* cleanup */
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
console.log(`✓ Decrypted: ${outDir}`);
|
|
1116
|
-
const files = fs.readdirSync(outDir);
|
|
1117
|
-
console.log(` Files: ${files.length}`);
|
|
1118
|
-
files.forEach((f) => console.log(` ${f}`));
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// ─── KDNA Card (locale-aware) ────────────────────────────────────
|
|
1122
|
-
|
|
1123
|
-
function cmdCard(dir, jsonMode = false, locale = null) {
|
|
1124
|
-
const abs = path.resolve(dir);
|
|
1125
|
-
let card = readJson(path.join(abs, 'KDNA_CARD.json'));
|
|
1126
|
-
|
|
1127
|
-
if (locale) {
|
|
1128
|
-
const localeCard = readJson(path.join(abs, 'locales', locale, 'KDNA_CARD.json'));
|
|
1129
|
-
if (localeCard) {
|
|
1130
|
-
card = { ...card, ...localeCard };
|
|
1131
|
-
}
|
|
820
|
+
error(`Not a .kdna asset: ${abs}`, EXIT.INPUT_ERROR);
|
|
1132
821
|
}
|
|
1133
822
|
|
|
1134
823
|
if (!card) {
|
|
@@ -1195,9 +884,7 @@ function cmdCard(dir, jsonMode = false, locale = null) {
|
|
|
1195
884
|
module.exports = {
|
|
1196
885
|
cmdValidate,
|
|
1197
886
|
cmdPack,
|
|
1198
|
-
cmdPackEncrypt,
|
|
1199
887
|
cmdUnpack,
|
|
1200
|
-
cmdUnpackEncrypt,
|
|
1201
888
|
cmdInspect,
|
|
1202
889
|
cmdCard,
|
|
1203
890
|
};
|