@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.
@@ -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
- kdna_spec: '1.0-rc',
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
- language: 'en',
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: system zip command
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 = fs.readFileSync(path.join(srcDir, f));
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 or legacy merged JSON) ────────────
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
- let core, patterns, manifest;
486
- const presentFiles = [];
487
-
488
- if (isZip) {
489
- // ZIP container — extract to temp, read files
490
- const os = require('os');
491
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-inspect-'));
492
- try {
493
- const tmpInspectPy = path.join(
494
- fs.existsSync('/tmp') ? '/tmp' : require('os').tmpdir(),
495
- `kdna-inspect-${Date.now()}.py`,
496
- );
497
- try {
498
- const script = `import zipfile, os
499
- zf = zipfile.ZipFile(${JSON.stringify(abs)}, 'r')
500
- zf.extractall(${JSON.stringify(tmpDir)})
501
- zf.close()
502
- `;
503
- fs.writeFileSync(tmpInspectPy, script);
504
- execSync(`python3 ${tmpInspectPy}`, { stdio: 'pipe' });
505
- } finally {
506
- try {
507
- fs.unlinkSync(tmpInspectPy);
508
- } catch {
509
- /* cleanup */
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) error('KDNA_Core.json not found in container');
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: isZip ? 'kdna-zip' : 'legacy-merged',
584
- spec: m.spec_version || m.kdna_spec || null,
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 ${isZip ? '(ZIP container)' : '(legacy merged)'}`);
612
- console.log(` Spec: ${m.spec_version || m.kdna_spec || '0.4'}`);
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
- // Directory existing logic
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
- // ─── Encrypted Container (.kdnae) ─────────────────────────────────────
781
+ // ─── KDNA Card (locale-aware) ────────────────────────────────────
874
782
 
875
- function cmdPackEncrypt(dir, args = []) {
876
- const abs = path.resolve(dir);
877
- if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
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
- const core = readJson(path.join(abs, 'KDNA_Core.json'));
882
- const pat = readJson(path.join(abs, 'KDNA_Patterns.json'));
883
- if (!core) error('KDNA_Core.json not found or invalid');
884
- if (!pat) error('KDNA_Patterns.json not found or invalid');
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
- const domainName = core.meta?.domain || path.basename(abs);
794
+ return card;
795
+ }
887
796
 
888
- let manifest = readJson(path.join(abs, 'kdna.json'));
889
- if (!manifest) {
890
- const jsonCount = fs
891
- .readdirSync(abs)
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
- // Get encryption key
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
- 'Use --license <license.json> or --key <secret> to provide encryption key',
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
- const outputDir = args.includes('--output') ? args[args.indexOf('--output') + 1] : null;
935
- const outName = `${domainName}.kdnae`;
936
- const outPath = outputDir ? path.join(outputDir, outName) : path.join(process.cwd(), outName);
937
- if (outputDir && !fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
938
-
939
- // Build encrypted ZIP
940
- const zlib = require('zlib');
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
- const crc = crc32(raw);
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('Use --license <license.json> or --key <secret> to decrypt', EXIT.INPUT_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
  };