@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.
@@ -2,47 +2,31 @@ 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
- // ─── Validate ────────────────────────────────────────────────────────
15
-
16
- function cmdValidate(dir, schemaOnly, jsonMode = false) {
17
- const abs = path.resolve(dir);
18
- if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
19
- error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
20
- }
21
-
22
- const { lintDomain, validateDomainSchema, validateCrossFile } = require('@aikdna/kdna-core');
23
-
24
- // Resolve schemas from @aikdna/kdna-core package
25
- const SCHEMA_DIR = path.join(
26
- path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
27
- 'schema',
28
- );
29
-
30
- // Read all KDNA JSON files
31
- const files = fs.readdirSync(abs).filter((f) => f.endsWith('.json') && f !== 'kdna.json');
5
+ const KDNA_DOMAIN_FILES = new Set([
6
+ 'KDNA_Core.json',
7
+ 'KDNA_Patterns.json',
8
+ 'KDNA_Scenarios.json',
9
+ 'KDNA_Cases.json',
10
+ 'KDNA_Reasoning.json',
11
+ 'KDNA_Evolution.json',
12
+ ]);
13
+
14
+ function readKDNAContentFiles(abs) {
32
15
  const dataMap = {};
33
- const schemaMap = {};
34
-
35
- for (const f of files) {
16
+ const parseErrors = [];
17
+ for (const f of fs.readdirSync(abs).filter((name) => KDNA_DOMAIN_FILES.has(name))) {
36
18
  try {
37
19
  dataMap[f] = JSON.parse(fs.readFileSync(path.join(abs, f), 'utf8'));
38
20
  } catch (e) {
39
21
  dataMap[f] = null;
40
- console.error(` JSON parse error in ${f}: ${e.message}`);
22
+ parseErrors.push(`${f}: ${e.message}`);
41
23
  }
42
24
  }
25
+ return { dataMap, parseErrors };
26
+ }
43
27
 
44
- // Schema validation — always load all available schemas
45
- const FILE_TO_SCHEMA = {
28
+ function loadSchemaMap(schemaDir) {
29
+ const fileToSchema = {
46
30
  'KDNA_Core.json': 'KDNA_Core.schema.json',
47
31
  'KDNA_Patterns.json': 'KDNA_Patterns.schema.json',
48
32
  'KDNA_Scenarios.json': 'KDNA_Scenarios.schema.json',
@@ -51,10 +35,11 @@ function cmdValidate(dir, schemaOnly, jsonMode = false) {
51
35
  'KDNA_Evolution.json': 'KDNA_Evolution.schema.json',
52
36
  };
53
37
 
38
+ const schemaMap = {};
54
39
  const loadedSchemas = [];
55
40
  const missingSchemas = [];
56
- for (const [, schemaFile] of Object.entries(FILE_TO_SCHEMA)) {
57
- const schemaPath = path.join(SCHEMA_DIR, schemaFile);
41
+ for (const schemaFile of Object.values(fileToSchema)) {
42
+ const schemaPath = path.join(schemaDir, schemaFile);
58
43
  if (fs.existsSync(schemaPath)) {
59
44
  try {
60
45
  schemaMap[schemaFile] = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
@@ -67,50 +52,132 @@ function cmdValidate(dir, schemaOnly, jsonMode = false) {
67
52
  }
68
53
  }
69
54
 
70
- if (missingSchemas.length) {
71
- console.log(
72
- ` Note: ${missingSchemas.length} schema file(s) not found (optional file schemas): ${missingSchemas.join(', ')}`,
73
- );
74
- console.log(` Schema dir: ${SCHEMA_DIR}`);
75
- }
55
+ return { schemaMap, loadedSchemas, missingSchemas };
56
+ }
76
57
 
77
- // Validation layers
78
- const errors = [];
58
+ function validateDomainDirectory(abs, schemaMap, schemaOnly) {
59
+ const { lintDomain, validateDomainSchema, validateCrossFile } = require('@aikdna/kdna-core');
60
+ const { dataMap, parseErrors } = readKDNAContentFiles(abs);
61
+ const errors = parseErrors.map((msg) => `JSON parse error in ${msg}`);
79
62
  const warnings = [];
80
63
 
81
- // Layer 1: Lint (structural + content checks)
82
64
  if (!schemaOnly) {
83
65
  const lintResult = lintDomain(dataMap);
84
66
  errors.push(...lintResult.errors);
85
67
  warnings.push(...lintResult.warnings);
86
68
  }
87
69
 
88
- // Layer 2: JSON Schema validation against loaded schemas
89
70
  const schemaResult = validateDomainSchema(dataMap, schemaMap);
90
71
  errors.push(...schemaResult.errors);
91
72
  warnings.push(...schemaResult.warnings);
92
73
 
93
- // Layer 3: Cross-file consistency
94
74
  const crossResult = validateCrossFile(dataMap);
95
75
  errors.push(...crossResult.errors);
96
76
  warnings.push(...crossResult.warnings);
97
77
 
98
- const validCount = Object.keys(dataMap).filter((k) => dataMap[k]).length;
78
+ return {
79
+ path: abs,
80
+ valid: errors.length === 0,
81
+ files: Object.keys(dataMap).filter((k) => dataMap[k]).length,
82
+ errors,
83
+ warnings,
84
+ };
85
+ }
86
+
87
+ function isClusterDirectory(abs) {
88
+ const manifest = readJson(path.join(abs, 'kdna.json'));
89
+ return !!(
90
+ manifest?.cluster ||
91
+ fs.existsSync(path.join(abs, 'KDNA_Cluster.json')) ||
92
+ fs.existsSync(path.join(abs, 'cluster_manifest.json'))
93
+ );
94
+ }
95
+
96
+ function validateClusterDirectory(abs, schemaMap, schemaOnly) {
97
+ const manifest = readJson(path.join(abs, 'kdna.json')) || {};
98
+ const clusterManifest = readJson(path.join(abs, 'KDNA_Cluster.json')) || {};
99
+ const fallbackManifest = readJson(path.join(abs, 'cluster_manifest.json')) || {};
100
+ const subDomains = manifest.sub_domains || fallbackManifest.domains || [];
101
+ const errors = [];
102
+ const warnings = [];
103
+
104
+ if (!Array.isArray(subDomains) || subDomains.length === 0) {
105
+ errors.push('Cluster has no sub_domains/domains list to validate');
106
+ }
107
+
108
+ const domains = [];
109
+ for (const name of subDomains) {
110
+ const domainPath = path.join(abs, name);
111
+ if (!fs.existsSync(domainPath) || !fs.statSync(domainPath).isDirectory()) {
112
+ errors.push(`Cluster sub-domain not found: ${name}`);
113
+ continue;
114
+ }
115
+ const result = validateDomainDirectory(domainPath, schemaMap, schemaOnly);
116
+ domains.push({ name, ...result });
117
+ warnings.push(...result.warnings.map((w) => `${name}: ${w}`));
118
+ errors.push(...result.errors.map((e) => `${name}: ${e}`));
119
+ }
120
+
121
+ if (!manifest.cluster && !clusterManifest.name && !fallbackManifest.name) {
122
+ warnings.push('Cluster metadata is minimal: no cluster marker or cluster name found');
123
+ }
124
+
125
+ return {
126
+ path: abs,
127
+ valid: errors.length === 0,
128
+ cluster: true,
129
+ domains,
130
+ files: domains.reduce((sum, d) => sum + d.files, 0),
131
+ errors,
132
+ warnings,
133
+ };
134
+ }
135
+
136
+ // ─── Validate ────────────────────────────────────────────────────────
137
+
138
+ function cmdValidate(dir, schemaOnly, jsonMode = false) {
139
+ const abs = path.resolve(dir);
140
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
141
+ error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
142
+ }
143
+
144
+ // Resolve schemas from @aikdna/kdna-core package
145
+ const SCHEMA_DIR = path.join(
146
+ path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
147
+ 'schema',
148
+ );
149
+
150
+ const { schemaMap, loadedSchemas, missingSchemas } = loadSchemaMap(SCHEMA_DIR);
151
+
152
+ if (missingSchemas.length) {
153
+ console.log(
154
+ ` Note: ${missingSchemas.length} schema file(s) not found (optional file schemas): ${missingSchemas.join(', ')}`,
155
+ );
156
+ console.log(` Schema dir: ${SCHEMA_DIR}`);
157
+ }
158
+
159
+ const result = isClusterDirectory(abs)
160
+ ? validateClusterDirectory(abs, schemaMap, schemaOnly)
161
+ : validateDomainDirectory(abs, schemaMap, schemaOnly);
162
+ const { errors, warnings } = result;
163
+ const validCount = result.files;
99
164
  const schemaInfo = schemaOnly
100
165
  ? ` (schema-only mode, ${loadedSchemas.length} schemas loaded)`
101
166
  : '';
102
167
 
103
168
  if (jsonMode) {
104
- const result = {
169
+ const payload = {
105
170
  path: abs,
106
171
  valid: errors.length === 0,
107
172
  files: validCount,
173
+ cluster: !!result.cluster,
174
+ domains: result.domains,
108
175
  schemas_loaded: loadedSchemas.length,
109
176
  schema_only: schemaOnly,
110
177
  errors,
111
178
  warnings,
112
179
  };
113
- console.log(JSON.stringify(result, null, 2));
180
+ console.log(JSON.stringify(payload, null, 2));
114
181
  process.exit(errors.length ? EXIT.VALIDATION_FAILED : EXIT.OK);
115
182
  }
116
183
 
@@ -124,7 +191,13 @@ function cmdValidate(dir, schemaOnly, jsonMode = false) {
124
191
  process.exit(EXIT.VALIDATION_FAILED);
125
192
  }
126
193
 
127
- console.log(`✓ KDNA domain valid: ${abs} (${validCount} files, schema OK${schemaInfo})`);
194
+ if (result.cluster) {
195
+ console.log(
196
+ `✓ KDNA cluster valid: ${abs} (${result.domains.length} domains, ${validCount} KDNA files, schema OK${schemaInfo})`,
197
+ );
198
+ } else {
199
+ console.log(`✓ KDNA domain valid: ${abs} (${validCount} files, schema OK${schemaInfo})`);
200
+ }
128
201
  }
129
202
 
130
203
  // ─── Pack / Unpack (.kdna ZIP container) ──────────────────────────────────
@@ -140,6 +213,19 @@ function cmdPack(dir, outputDir) {
140
213
  if (!core) error('KDNA_Core.json not found or invalid');
141
214
  if (!pat) error('KDNA_Patterns.json not found or invalid');
142
215
 
216
+ // Human Lock Gate — check judgment-class cards before packing
217
+ const { checkHumanLock } = require('../publish');
218
+ const hl = checkHumanLock(abs);
219
+ if (!hl.passed) {
220
+ console.error('Human Lock Gate: BLOCKED');
221
+ for (const issue of hl.issues) {
222
+ console.error(` ✗ ${issue}`);
223
+ }
224
+ console.error('Judgment-class cards must be locked with valid Human Lock before packing.');
225
+ console.error('Use kdna publish --check for details.');
226
+ process.exit(EXIT.HUMAN_LOCK_REQUIRED);
227
+ }
228
+
143
229
  const domainName = core.meta?.domain || path.basename(abs);
144
230
 
145
231
  // Ensure kdna.json manifest exists (generate if missing)
@@ -374,7 +460,7 @@ zf.close()
374
460
  files.forEach((f) => console.log(` ${f}`));
375
461
  }
376
462
 
377
- // ─── Inspect .kdna file (ZIP container or legacy merged JSON) ────────────
463
+ // ─── Inspect .kdna file (ZIP container) ──────────────────────────────────
378
464
 
379
465
  function inspectKdnaFile(filePath, jsonMode = false) {
380
466
  const abs = path.resolve(filePath);
@@ -386,106 +472,47 @@ function inspectKdnaFile(filePath, jsonMode = false) {
386
472
  fs.readSync(fd, head, 0, 4, 0);
387
473
  fs.closeSync(fd);
388
474
  const isZip = head[0] === 0x50 && head[1] === 0x4b;
475
+ if (!isZip) error('Invalid .kdna asset: expected ZIP container');
389
476
 
390
- let core, patterns, manifest;
391
- const presentFiles = [];
392
-
393
- if (isZip) {
394
- // ZIP container — extract to temp, read files
395
- const os = require('os');
396
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-inspect-'));
397
- try {
398
- const tmpInspectPy = path.join(
399
- fs.existsSync('/tmp') ? '/tmp' : require('os').tmpdir(),
400
- `kdna-inspect-${Date.now()}.py`,
401
- );
402
- try {
403
- const script = `import zipfile, os
404
- zf = zipfile.ZipFile(${JSON.stringify(abs)}, 'r')
405
- zf.extractall(${JSON.stringify(tmpDir)})
406
- zf.close()
407
- `;
408
- fs.writeFileSync(tmpInspectPy, script);
409
- execSync(`python3 ${tmpInspectPy}`, { stdio: 'pipe' });
410
- } finally {
411
- try {
412
- fs.unlinkSync(tmpInspectPy);
413
- } catch {
414
- /* cleanup */
415
- }
416
- }
417
- } catch {
418
- try {
419
- execSync(`unzip -q -o "${abs}" -d "${tmpDir}"`, { stdio: 'pipe' });
420
- } catch {
421
- fs.rmSync(tmpDir, { recursive: true, force: true });
422
- error('Cannot read .kdna container. Install python3 or unzip.');
423
- }
424
- }
425
-
426
- core = readJson(path.join(tmpDir, 'KDNA_Core.json'));
427
- patterns = readJson(path.join(tmpDir, 'KDNA_Patterns.json'));
428
- manifest = readJson(path.join(tmpDir, 'kdna.json'));
429
-
430
- for (const f of fs.readdirSync(tmpDir)) {
431
- if (f.startsWith('KDNA_') && f.endsWith('.json')) {
432
- presentFiles.push(f);
433
- }
434
- if (f === 'README.md' || f === 'LICENSE') presentFiles.push(f);
435
- }
436
-
437
- fs.rmSync(tmpDir, { recursive: true, force: true });
438
- } else {
439
- // Legacy merged JSON/YAML format (deprecated)
440
- const raw = fs.readFileSync(abs, 'utf8');
441
- let data;
442
- try {
443
- data = JSON.parse(raw);
444
- } catch {
445
- data = parseSimpleYaml(raw);
446
- }
447
-
448
- if (!data || !data.meta) error(`Invalid .kdna file: missing meta section`);
449
-
450
- const m = data.meta || {};
451
- manifest = {
452
- name: m.name || m.domain,
453
- version: m.version || '?',
454
- status: data.status || '?',
455
- access: data.access || '?',
456
- language: data.language || '?',
457
- author: data.author || { name: '?' },
458
- license: data.license || { type: '?' },
459
- description: data.description || m.purpose || '?',
460
- spec_version: m.spec_version || data.kdna_spec || '?',
461
- };
462
- core = data.core || {};
463
- patterns = data.patterns || {};
464
- presentFiles.push('.kdna (legacy merged format)');
465
- if (data.scenarios) {
466
- presentFiles.push('scenarios (inline)');
467
- }
468
- if (data.cases) {
469
- presentFiles.push('cases (inline)');
470
- }
471
- if (data.reasoning) {
472
- presentFiles.push('reasoning (inline)');
473
- }
474
- if (data.evolution) {
475
- presentFiles.push('evolution (inline)');
476
- }
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;
477
502
  }
478
503
 
479
- if (!core) error('KDNA_Core.json not found in container');
504
+ if (!core && !encryptedEntries.includes('KDNA_Core.json')) {
505
+ error('KDNA_Core.json not found in container');
506
+ }
480
507
 
481
508
  const m = manifest || {};
482
- const c = core;
509
+ const c = core || {};
483
510
  const p = patterns || {};
484
511
 
485
512
  if (jsonMode) {
486
513
  const result = {
487
514
  name: m.name || c.meta?.domain || path.basename(abs, '.kdna'),
488
- format: isZip ? 'kdna-zip' : 'legacy-merged',
515
+ format: 'kdna-zip',
489
516
  spec: m.spec_version || m.kdna_spec || null,
490
517
  version: m.version || null,
491
518
  status: m.status || 'experimental',
@@ -494,6 +521,9 @@ zf.close()
494
521
  license: m.license?.type || null,
495
522
  created: m.created || c.meta?.created || null,
496
523
  description: m.description || c.meta?.purpose || null,
524
+ protected: encryptedEntries.length > 0,
525
+ encrypted_entries: encryptedEntries,
526
+ license_required: !!decryptError,
497
527
  content: {
498
528
  axioms: (c.axioms || []).length,
499
529
  ontology: (c.ontology || []).length,
@@ -513,7 +543,7 @@ zf.close()
513
543
  console.log(` ${m.name || c.meta?.domain || path.basename(abs, '.kdna')} — KDNA Domain`);
514
544
  console.log('═'.repeat(50));
515
545
  console.log('');
516
- console.log(` Format: .kdna ${isZip ? '(ZIP container)' : '(legacy merged)'}`);
546
+ console.log(` Format: .kdna (ZIP container)`);
517
547
  console.log(` Spec: ${m.spec_version || m.kdna_spec || '0.4'}`);
518
548
  console.log(` Version: ${m.version || '?'}`);
519
549
  console.log(` Status: ${m.status || 'experimental'}`);
@@ -522,6 +552,10 @@ zf.close()
522
552
  console.log(` License: ${m.license?.type || '?'}`);
523
553
  console.log(` Created: ${m.created || c.meta?.created || '?'}`);
524
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
+ }
525
559
  console.log('');
526
560
  console.log(' ── Content ──');
527
561
  console.log(` Axioms: ${(c.axioms || []).length}`);
@@ -538,57 +572,9 @@ zf.close()
538
572
  console.log('═'.repeat(50));
539
573
  }
540
574
 
541
- function parseSimpleYaml(raw) {
542
- // Parse a simple subset of YAML (no nesting beyond 1 level for sections)
543
- const result = {};
544
- let currentSection = null;
545
-
546
- const lines = raw.split('\n');
547
- for (const line of lines) {
548
- const trimmed = line.trim();
549
- if (!trimmed || trimmed.startsWith('#')) continue;
550
-
551
- // Section header: "core:" or " core:" etc
552
- if (/^[a-z_]+:$/.test(trimmed)) {
553
- currentSection = trimmed.slice(0, -1);
554
- if (!result[currentSection]) result[currentSection] = {};
555
- continue;
556
- }
557
-
558
- // Key: value
559
- const kv = trimmed.match(/^([a-z_]+):\s*(.*)/i);
560
- if (kv && !kv[1].startsWith('-')) {
561
- const key = kv[1];
562
- const val = kv[2].trim().replace(/^["']|["']$/g, '');
563
- if (currentSection) {
564
- if (key === 'version' && typeof result[currentSection] === 'object') {
565
- result[currentSection][key] = val;
566
- } else if (!result[currentSection][key]) {
567
- result[currentSection][key] = val;
568
- }
569
- } else {
570
- result[key] = val;
571
- }
572
- continue;
573
- }
574
-
575
- // Array item: "- value"
576
- if (trimmed.startsWith('- ') && currentSection) {
577
- // For counts only, we don't parse full arrays
578
- if (currentSection === 'axioms' || currentSection === 'stances') {
579
- if (!result.core) result.core = {};
580
- if (!result.core[currentSection]) result.core[currentSection] = [];
581
- result.core[currentSection].push({ _parsed: true });
582
- }
583
- }
584
- }
585
-
586
- return result;
587
- }
588
-
589
575
  // ─── Inspect ───────────────────────────────────────────────────────────
590
576
 
591
- function cmdInspect(dir, jsonMode = false, locale = null) {
577
+ function cmdInspect(dir, jsonMode = false, locale = null, options = {}) {
592
578
  const abs = path.resolve(dir);
593
579
  const stat = fs.existsSync(abs) ? fs.statSync(abs) : null;
594
580
  if (!stat) error(`Path not found: ${abs}`, EXIT.INPUT_ERROR);
@@ -599,7 +585,14 @@ function cmdInspect(dir, jsonMode = false, locale = null) {
599
585
  return;
600
586
  }
601
587
 
602
- // Directory existing logic
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
603
596
  if (!stat.isDirectory()) error(`Not a KDNA domain: ${abs}`, EXIT.INPUT_ERROR);
604
597
 
605
598
  const core = readJson(path.join(abs, 'KDNA_Core.json'));
@@ -775,265 +768,46 @@ function cmdInspect(dir, jsonMode = false, locale = null) {
775
768
  console.log('═'.repeat(50));
776
769
  }
777
770
 
778
- // ─── Encrypted Container (.kdnae) ─────────────────────────────────────
771
+ // ─── KDNA Card (locale-aware) ────────────────────────────────────
779
772
 
780
- function cmdPackEncrypt(dir, args = []) {
781
- const abs = path.resolve(dir);
782
- if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
783
- error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
784
- }
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'));
785
776
 
786
- const core = readJson(path.join(abs, 'KDNA_Core.json'));
787
- const pat = readJson(path.join(abs, 'KDNA_Patterns.json'));
788
- if (!core) error('KDNA_Core.json not found or invalid');
789
- if (!pat) error('KDNA_Patterns.json not found or invalid');
777
+ if (locale) {
778
+ const localeCard = readJson(path.join(abs, 'locales', locale, 'KDNA_CARD.json'));
779
+ if (localeCard) {
780
+ card = { ...card, ...localeCard };
781
+ }
782
+ }
790
783
 
791
- const domainName = core.meta?.domain || path.basename(abs);
784
+ return card;
785
+ }
792
786
 
793
- let manifest = readJson(path.join(abs, 'kdna.json'));
794
- if (!manifest) {
795
- const jsonCount = fs
796
- .readdirSync(abs)
797
- .filter((f) => f.endsWith('.json') && f !== 'kdna.json').length;
798
- manifest = {
799
- kdna_spec: '1.0-rc',
800
- name: domainName,
801
- version: core.meta?.version || '0.1.0',
802
- status: 'experimental',
803
- access: 'licensed',
804
- language: 'en',
805
- author: { name: '', id: '' },
806
- license: { type: 'KCL-1.0' },
807
- description: core.meta?.purpose || `${domainName} domain cognition`,
808
- file_count: jsonCount,
809
- created: new Date().toISOString().slice(0, 10),
810
- updated: new Date().toISOString().slice(0, 10),
811
- };
812
- writeJson(path.join(abs, 'kdna.json'), manifest);
813
- }
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);
814
791
 
815
- // Get encryption key
816
- const licenseIdx = args.indexOf('--license');
817
- const keyIdx = args.indexOf('--key');
818
- let encKey;
819
-
820
- if (licenseIdx >= 0) {
821
- const licensePath = args[licenseIdx + 1];
822
- if (!licensePath || !fs.existsSync(licensePath))
823
- error('License file not found', EXIT.INPUT_ERROR);
824
- const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
825
- const licenseKey = license.license_id;
826
- const fp = machineFingerprint();
827
- encKey = deriveKey(licenseKey, fp);
828
- } else if (keyIdx >= 0) {
829
- const rawKey = args[keyIdx + 1];
830
- if (!rawKey) error('--key requires a value', EXIT.INPUT_ERROR);
831
- encKey = deriveKey(rawKey, machineFingerprint());
832
- } else {
792
+ if (stat.isDirectory() && !options.allowDirectory) {
833
793
  error(
834
- 'Use --license <license.json> or --key <secret> to provide encryption key',
794
+ 'Directory card inspection is a dev-only operation. Use: kdna dev card <source-dir>',
835
795
  EXIT.INPUT_ERROR,
836
796
  );
837
797
  }
838
798
 
839
- const outputDir = args.includes('--output') ? args[args.indexOf('--output') + 1] : null;
840
- const outName = `${domainName}.kdnae`;
841
- const outPath = outputDir ? path.join(outputDir, outName) : path.join(process.cwd(), outName);
842
- if (outputDir && !fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
843
-
844
- // Build encrypted ZIP
845
- const zlib = require('zlib');
846
- const files = fs.readdirSync(abs).filter((f) => {
847
- if (f.startsWith('.')) return false;
848
- const ext = path.extname(f);
849
- return ext === '.json' || f === 'README.md' || f === 'LICENSE' || f === 'kdna.json';
850
- });
851
-
852
- const centralDir = [];
853
- const fileData = [];
854
- let offset = 0;
855
-
856
- for (const f of files.sort()) {
857
- let raw = fs.readFileSync(path.join(abs, f));
858
- let storedName = f;
859
-
860
- if (isEncryptable(f)) {
861
- try {
862
- const encrypted = encrypt(raw.toString('utf8'), encKey);
863
- raw = encrypted;
864
- storedName = f; // Keep original name, content is encrypted
865
- } catch (err) {
866
- error(`Failed to encrypt ${f}: ${err.message}`);
867
- }
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 };
868
806
  }
869
-
870
- const crc = crc32(raw);
871
- const compressed = zlib.deflateRawSync(raw);
872
- const useStore = compressed.length >= raw.length;
873
- const stored = useStore ? raw : compressed;
874
-
875
- const nameBytes = Buffer.from(storedName, 'utf8');
876
- const localHeader = Buffer.alloc(30);
877
- localHeader.writeUInt32LE(0x04034b50, 0);
878
- localHeader.writeUInt16LE(20, 4);
879
- localHeader.writeUInt16LE(0x0800, 6);
880
- localHeader.writeUInt16LE(useStore ? 0 : 8, 8);
881
- localHeader.writeUInt32LE(crc, 14);
882
- localHeader.writeUInt32LE(useStore ? raw.length : compressed.length, 18);
883
- localHeader.writeUInt32LE(raw.length, 22);
884
- localHeader.writeUInt16LE(nameBytes.length, 26);
885
-
886
- fileData.push(Buffer.concat([localHeader, nameBytes, stored]));
887
- offset += localHeader.length + nameBytes.length + stored.length;
888
-
889
- const cdEntry = Buffer.alloc(46);
890
- cdEntry.writeUInt32LE(0x02014b50, 0);
891
- cdEntry.writeUInt16LE(20, 4);
892
- cdEntry.writeUInt16LE(20, 6);
893
- cdEntry.writeUInt16LE(0x0800, 8);
894
- cdEntry.writeUInt16LE(useStore ? 0 : 8, 10);
895
- cdEntry.writeUInt32LE(crc, 16);
896
- cdEntry.writeUInt32LE(useStore ? raw.length : compressed.length, 20);
897
- cdEntry.writeUInt32LE(raw.length, 24);
898
- cdEntry.writeUInt16LE(nameBytes.length, 28);
899
- cdEntry.writeUInt32LE(offset - stored.length - nameBytes.length - localHeader.length, 42);
900
- centralDir.push(Buffer.concat([cdEntry, nameBytes]));
901
- }
902
-
903
- const cdOffset = offset;
904
- const cdSize = centralDir.reduce((s, e) => s + e.length, 0);
905
- const eocd = Buffer.alloc(22);
906
- eocd.writeUInt32LE(0x06054b50, 0);
907
- eocd.writeUInt16LE(0, 4);
908
- eocd.writeUInt16LE(0, 6);
909
- eocd.writeUInt16LE(files.length, 8);
910
- eocd.writeUInt16LE(files.length, 10);
911
- eocd.writeUInt32LE(cdSize, 12);
912
- eocd.writeUInt32LE(cdOffset, 16);
913
- eocd.writeUInt16LE(0, 20);
914
-
915
- const all = Buffer.concat([...fileData, ...centralDir, eocd]);
916
- fs.writeFileSync(outPath, all);
917
-
918
- console.log(`✓ Encrypted pack: ${outPath}`);
919
- console.log(` Domain: ${domainName} v${manifest.version}`);
920
- console.log(
921
- ` Files: ${files.length} (${files.filter(isEncryptable).length} encrypted, ${files.filter((f) => !isEncryptable(f)).length} plaintext)`,
922
- );
923
- console.log(` Container: AES-256-GCM .kdnae`);
924
- }
925
-
926
- function cmdUnpackEncrypt(filePath, args = []) {
927
- const abs = path.resolve(filePath);
928
- if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
929
- error(`Not a file: ${abs}`, EXIT.INPUT_ERROR);
930
- }
931
- if (!abs.endsWith('.kdnae')) {
932
- error(`Not a .kdnae file: ${abs}`, EXIT.INPUT_ERROR);
933
- }
934
-
935
- const licenseIdx = args.indexOf('--license');
936
- const keyIdx = args.indexOf('--key');
937
- let encKey;
938
-
939
- if (licenseIdx >= 0) {
940
- const licensePath = args[licenseIdx + 1];
941
- if (!licensePath || !fs.existsSync(licensePath))
942
- error('License file not found', EXIT.INPUT_ERROR);
943
- const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
944
- const licenseKey = license.license_id;
945
- const fp = machineFingerprint();
946
- encKey = deriveKey(licenseKey, fp);
947
- } else if (keyIdx >= 0) {
948
- const rawKey = args[keyIdx + 1];
949
- if (!rawKey) error('--key requires a value', EXIT.INPUT_ERROR);
950
- encKey = deriveKey(rawKey, machineFingerprint());
807
+ } else if (stat.isDirectory()) {
808
+ card = readCardFromDirectory(abs, locale);
951
809
  } else {
952
- error('Use --license <license.json> or --key <secret> to decrypt', EXIT.INPUT_ERROR);
953
- }
954
-
955
- const domainName = path.basename(abs, '.kdnae');
956
- const outDir = path.join(path.dirname(abs), domainName);
957
- const force = args.includes('--force');
958
-
959
- if (fs.existsSync(outDir)) {
960
- if (!force)
961
- error(`Directory already exists: ${outDir}\nUse --force to overwrite.`, EXIT.INPUT_ERROR);
962
- fs.rmSync(outDir, { recursive: true, force: true });
963
- }
964
- fs.mkdirSync(outDir, { recursive: true });
965
-
966
- // Extract ZIP first, then decrypt KDNA JSON files
967
- const os = require('os');
968
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdnae-unpack-'));
969
-
970
- try {
971
- const tmpPy = path.join(os.tmpdir(), `kdnae-unpack-${Date.now()}.py`);
972
- try {
973
- const script = `import zipfile, os
974
- zf = zipfile.ZipFile(${JSON.stringify(abs)}, 'r')
975
- zf.extractall(${JSON.stringify(tmpDir)})
976
- zf.close()
977
- `;
978
- fs.writeFileSync(tmpPy, script);
979
- execSync(`python3 ${tmpPy}`, { stdio: 'pipe' });
980
- } catch {
981
- try {
982
- execSync(`unzip -q -o "${abs}" -d "${tmpDir}"`, { stdio: 'pipe' });
983
- } catch {
984
- error('Cannot unpack .kdnae container');
985
- }
986
- } finally {
987
- try {
988
- fs.unlinkSync(tmpPy);
989
- } catch {
990
- /* cleanup */
991
- }
992
- }
993
-
994
- // Copy plaintext files, decrypt KDNA files
995
- const extracted = fs.readdirSync(tmpDir);
996
- for (const f of extracted) {
997
- const src = path.join(tmpDir, f);
998
- const dest = path.join(outDir, f);
999
-
1000
- if (ENCRYPTED_FILES.includes(f)) {
1001
- try {
1002
- const encrypted = fs.readFileSync(src);
1003
- const decrypted = decrypt(encrypted, encKey);
1004
- fs.writeFileSync(dest, decrypted);
1005
- } catch (err) {
1006
- error(`Failed to decrypt ${f}: ${err.message}. Wrong license key?`);
1007
- }
1008
- } else {
1009
- fs.copyFileSync(src, dest);
1010
- }
1011
- }
1012
- } finally {
1013
- try {
1014
- fs.rmSync(tmpDir, { recursive: true, force: true });
1015
- } catch {
1016
- /* cleanup */
1017
- }
1018
- }
1019
-
1020
- console.log(`✓ Decrypted: ${outDir}`);
1021
- const files = fs.readdirSync(outDir);
1022
- console.log(` Files: ${files.length}`);
1023
- files.forEach((f) => console.log(` ${f}`));
1024
- }
1025
-
1026
- // ─── KDNA Card (locale-aware) ────────────────────────────────────
1027
-
1028
- function cmdCard(dir, locale = null) {
1029
- const abs = path.resolve(dir);
1030
- let card = readJson(path.join(abs, 'KDNA_CARD.json'));
1031
-
1032
- if (locale) {
1033
- const localeCard = readJson(path.join(abs, 'locales', locale, 'KDNA_CARD.json'));
1034
- if (localeCard) {
1035
- card = { ...card, ...localeCard };
1036
- }
810
+ error(`Not a .kdna asset: ${abs}`, EXIT.INPUT_ERROR);
1037
811
  }
1038
812
 
1039
813
  if (!card) {
@@ -1043,8 +817,6 @@ function cmdCard(dir, locale = null) {
1043
817
  );
1044
818
  }
1045
819
 
1046
- const jsonMode = process.argv.includes('--json');
1047
-
1048
820
  if (jsonMode) {
1049
821
  console.log(JSON.stringify(card, null, 2));
1050
822
  return;
@@ -1102,9 +874,7 @@ function cmdCard(dir, locale = null) {
1102
874
  module.exports = {
1103
875
  cmdValidate,
1104
876
  cmdPack,
1105
- cmdPackEncrypt,
1106
877
  cmdUnpack,
1107
- cmdUnpackEncrypt,
1108
878
  cmdInspect,
1109
879
  cmdCard,
1110
880
  };