@aikdna/kdna-cli 0.17.0 → 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 +42 -23
- package/package.json +5 -4
- package/skills/kdna-loader/SKILL.md +5 -6
- package/src/agent.js +145 -109
- package/src/cli.js +104 -60
- 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 +77 -400
- 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 +2 -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 +73 -22
- package/src/registry.js +76 -9
- package/src/setup.js +19 -20
- package/src/verify.js +233 -124
- package/templates/standard-domain/kdna.json +2 -1
- 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',
|
|
@@ -469,7 +460,7 @@ zf.close()
|
|
|
469
460
|
files.forEach((f) => console.log(` ${f}`));
|
|
470
461
|
}
|
|
471
462
|
|
|
472
|
-
// ─── Inspect .kdna file (ZIP container
|
|
463
|
+
// ─── Inspect .kdna file (ZIP container) ──────────────────────────────────
|
|
473
464
|
|
|
474
465
|
function inspectKdnaFile(filePath, jsonMode = false) {
|
|
475
466
|
const abs = path.resolve(filePath);
|
|
@@ -481,106 +472,47 @@ function inspectKdnaFile(filePath, jsonMode = false) {
|
|
|
481
472
|
fs.readSync(fd, head, 0, 4, 0);
|
|
482
473
|
fs.closeSync(fd);
|
|
483
474
|
const isZip = head[0] === 0x50 && head[1] === 0x4b;
|
|
475
|
+
if (!isZip) error('Invalid .kdna asset: expected ZIP container');
|
|
484
476
|
|
|
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
|
-
}
|
|
477
|
+
const { listContainerEntries, readContainerJson } = require('../package-store');
|
|
478
|
+
const { licenseDecryptOptionsForManifest } = require('./license');
|
|
479
|
+
const presentFiles = listContainerEntries(abs).filter(
|
|
480
|
+
(f) => (f.startsWith('KDNA_') && f.endsWith('.json')) || f === 'README.md' || f === 'LICENSE',
|
|
481
|
+
);
|
|
482
|
+
const manifest = readContainerJson(abs, 'kdna.json') || {};
|
|
483
|
+
const encryptedEntries = Array.isArray(manifest.encryption?.encrypted_entries)
|
|
484
|
+
? manifest.encryption.encrypted_entries
|
|
485
|
+
: [];
|
|
486
|
+
let decryptOptions = {};
|
|
487
|
+
let decryptError = null;
|
|
488
|
+
if (encryptedEntries.length) {
|
|
489
|
+
const licensed = licenseDecryptOptionsForManifest(manifest);
|
|
490
|
+
if (licensed.ok) decryptOptions = { decryptEntry: licensed.decryptEntry };
|
|
491
|
+
else decryptError = licensed.error;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let core = null;
|
|
495
|
+
let patterns = null;
|
|
496
|
+
try {
|
|
497
|
+
core = decryptError ? null : readContainerJson(abs, 'KDNA_Core.json', decryptOptions);
|
|
498
|
+
patterns = decryptError ? null : readContainerJson(abs, 'KDNA_Patterns.json', decryptOptions);
|
|
499
|
+
} catch (e) {
|
|
500
|
+
if (!encryptedEntries.length) error(`Cannot inspect .kdna asset: ${e.message}`);
|
|
501
|
+
decryptError = e.message;
|
|
572
502
|
}
|
|
573
503
|
|
|
574
|
-
if (!core
|
|
504
|
+
if (!core && !encryptedEntries.includes('KDNA_Core.json')) {
|
|
505
|
+
error('KDNA_Core.json not found in container');
|
|
506
|
+
}
|
|
575
507
|
|
|
576
508
|
const m = manifest || {};
|
|
577
|
-
const c = core;
|
|
509
|
+
const c = core || {};
|
|
578
510
|
const p = patterns || {};
|
|
579
511
|
|
|
580
512
|
if (jsonMode) {
|
|
581
513
|
const result = {
|
|
582
514
|
name: m.name || c.meta?.domain || path.basename(abs, '.kdna'),
|
|
583
|
-
format:
|
|
515
|
+
format: 'kdna-zip',
|
|
584
516
|
spec: m.spec_version || m.kdna_spec || null,
|
|
585
517
|
version: m.version || null,
|
|
586
518
|
status: m.status || 'experimental',
|
|
@@ -589,6 +521,9 @@ zf.close()
|
|
|
589
521
|
license: m.license?.type || null,
|
|
590
522
|
created: m.created || c.meta?.created || null,
|
|
591
523
|
description: m.description || c.meta?.purpose || null,
|
|
524
|
+
protected: encryptedEntries.length > 0,
|
|
525
|
+
encrypted_entries: encryptedEntries,
|
|
526
|
+
license_required: !!decryptError,
|
|
592
527
|
content: {
|
|
593
528
|
axioms: (c.axioms || []).length,
|
|
594
529
|
ontology: (c.ontology || []).length,
|
|
@@ -608,7 +543,7 @@ zf.close()
|
|
|
608
543
|
console.log(` ${m.name || c.meta?.domain || path.basename(abs, '.kdna')} — KDNA Domain`);
|
|
609
544
|
console.log('═'.repeat(50));
|
|
610
545
|
console.log('');
|
|
611
|
-
console.log(` Format: .kdna
|
|
546
|
+
console.log(` Format: .kdna (ZIP container)`);
|
|
612
547
|
console.log(` Spec: ${m.spec_version || m.kdna_spec || '0.4'}`);
|
|
613
548
|
console.log(` Version: ${m.version || '?'}`);
|
|
614
549
|
console.log(` Status: ${m.status || 'experimental'}`);
|
|
@@ -617,6 +552,10 @@ zf.close()
|
|
|
617
552
|
console.log(` License: ${m.license?.type || '?'}`);
|
|
618
553
|
console.log(` Created: ${m.created || c.meta?.created || '?'}`);
|
|
619
554
|
console.log(` Description: ${m.description || c.meta?.purpose || '?'}`);
|
|
555
|
+
if (encryptedEntries.length) {
|
|
556
|
+
console.log(` Protected: ${encryptedEntries.join(', ')}`);
|
|
557
|
+
if (decryptError) console.log(` Activation: required (${decryptError})`);
|
|
558
|
+
}
|
|
620
559
|
console.log('');
|
|
621
560
|
console.log(' ── Content ──');
|
|
622
561
|
console.log(` Axioms: ${(c.axioms || []).length}`);
|
|
@@ -633,57 +572,9 @@ zf.close()
|
|
|
633
572
|
console.log('═'.repeat(50));
|
|
634
573
|
}
|
|
635
574
|
|
|
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
575
|
// ─── Inspect ───────────────────────────────────────────────────────────
|
|
685
576
|
|
|
686
|
-
function cmdInspect(dir, jsonMode = false, locale = null) {
|
|
577
|
+
function cmdInspect(dir, jsonMode = false, locale = null, options = {}) {
|
|
687
578
|
const abs = path.resolve(dir);
|
|
688
579
|
const stat = fs.existsSync(abs) ? fs.statSync(abs) : null;
|
|
689
580
|
if (!stat) error(`Path not found: ${abs}`, EXIT.INPUT_ERROR);
|
|
@@ -694,7 +585,14 @@ function cmdInspect(dir, jsonMode = false, locale = null) {
|
|
|
694
585
|
return;
|
|
695
586
|
}
|
|
696
587
|
|
|
697
|
-
|
|
588
|
+
if (stat.isDirectory() && !options.allowDirectory) {
|
|
589
|
+
error(
|
|
590
|
+
'Directory inspection is a dev-only operation. Use: kdna dev inspect <source-dir>',
|
|
591
|
+
EXIT.INPUT_ERROR,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Dev source directory
|
|
698
596
|
if (!stat.isDirectory()) error(`Not a KDNA domain: ${abs}`, EXIT.INPUT_ERROR);
|
|
699
597
|
|
|
700
598
|
const core = readJson(path.join(abs, 'KDNA_Core.json'));
|
|
@@ -870,265 +768,46 @@ function cmdInspect(dir, jsonMode = false, locale = null) {
|
|
|
870
768
|
console.log('═'.repeat(50));
|
|
871
769
|
}
|
|
872
770
|
|
|
873
|
-
// ───
|
|
771
|
+
// ─── KDNA Card (locale-aware) ────────────────────────────────────
|
|
874
772
|
|
|
875
|
-
function
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
|
|
879
|
-
}
|
|
773
|
+
function readCardFromDirectory(abs, locale = null) {
|
|
774
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) return null;
|
|
775
|
+
let card = readJson(path.join(abs, 'KDNA_CARD.json'));
|
|
880
776
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
777
|
+
if (locale) {
|
|
778
|
+
const localeCard = readJson(path.join(abs, 'locales', locale, 'KDNA_CARD.json'));
|
|
779
|
+
if (localeCard) {
|
|
780
|
+
card = { ...card, ...localeCard };
|
|
781
|
+
}
|
|
782
|
+
}
|
|
885
783
|
|
|
886
|
-
|
|
784
|
+
return card;
|
|
785
|
+
}
|
|
887
786
|
|
|
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
|
-
}
|
|
787
|
+
function cmdCard(dir, jsonMode = false, locale = null, options = {}) {
|
|
788
|
+
const abs = path.resolve(dir);
|
|
789
|
+
const stat = fs.existsSync(abs) ? fs.statSync(abs) : null;
|
|
790
|
+
if (!stat) error(`Path not found: ${abs}`, EXIT.INPUT_ERROR);
|
|
909
791
|
|
|
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 {
|
|
792
|
+
if (stat.isDirectory() && !options.allowDirectory) {
|
|
928
793
|
error(
|
|
929
|
-
'
|
|
794
|
+
'Directory card inspection is a dev-only operation. Use: kdna dev card <source-dir>',
|
|
930
795
|
EXIT.INPUT_ERROR,
|
|
931
796
|
);
|
|
932
797
|
}
|
|
933
798
|
|
|
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
|
-
}
|
|
799
|
+
let card = null;
|
|
800
|
+
if (stat.isFile() && abs.endsWith('.kdna')) {
|
|
801
|
+
const { readContainerJson } = require('../package-store');
|
|
802
|
+
card = readContainerJson(abs, 'KDNA_CARD.json') || null;
|
|
803
|
+
if (locale) {
|
|
804
|
+
const localeCard = readContainerJson(abs, `locales/${locale}/KDNA_CARD.json`) || null;
|
|
805
|
+
if (localeCard) card = { ...card, ...localeCard };
|
|
963
806
|
}
|
|
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());
|
|
807
|
+
} else if (stat.isDirectory()) {
|
|
808
|
+
card = readCardFromDirectory(abs, locale);
|
|
1046
809
|
} 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
|
-
}
|
|
810
|
+
error(`Not a .kdna asset: ${abs}`, EXIT.INPUT_ERROR);
|
|
1132
811
|
}
|
|
1133
812
|
|
|
1134
813
|
if (!card) {
|
|
@@ -1195,9 +874,7 @@ function cmdCard(dir, jsonMode = false, locale = null) {
|
|
|
1195
874
|
module.exports = {
|
|
1196
875
|
cmdValidate,
|
|
1197
876
|
cmdPack,
|
|
1198
|
-
cmdPackEncrypt,
|
|
1199
877
|
cmdUnpack,
|
|
1200
|
-
cmdUnpackEncrypt,
|
|
1201
878
|
cmdInspect,
|
|
1202
879
|
cmdCard,
|
|
1203
880
|
};
|