@aikdna/kdna-cli 0.16.10 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -75
- package/package.json +5 -5
- package/skills/kdna-loader/SKILL.md +5 -6
- package/src/agent.js +489 -79
- package/src/cli.js +112 -62
- package/src/cmds/_common.js +32 -16
- package/src/cmds/badge.js +7 -7
- package/src/cmds/changelog.js +1 -1
- package/src/cmds/cluster.js +16 -48
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +213 -443
- package/src/cmds/explain.js +122 -0
- package/src/cmds/legacy.js +8 -8
- package/src/cmds/license.js +483 -26
- package/src/cmds/quality.js +14 -2
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +4 -5
- package/src/cmds/test.js +4 -4
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +28 -22
- package/src/diff.js +11 -13
- package/src/init.js +2 -2
- package/src/install.js +138 -460
- package/src/loader.js +10 -10
- package/src/package-store.js +229 -0
- package/src/paths.js +44 -0
- package/src/publish.js +184 -22
- package/src/registry.js +76 -9
- package/src/setup.js +19 -20
- package/src/verify.js +275 -121
- package/templates/standard-domain/kdna.json +2 -1
- package/validators/kdna-lint.js +37 -3
- package/validators/kdna-validate.js +3 -2
- package/src/cmds/encrypt.js +0 -199
package/src/cmds/domain.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
22
|
+
parseErrors.push(`${f}: ${e.message}`);
|
|
41
23
|
}
|
|
42
24
|
}
|
|
25
|
+
return { dataMap, parseErrors };
|
|
26
|
+
}
|
|
43
27
|
|
|
44
|
-
|
|
45
|
-
const
|
|
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
|
|
57
|
-
const schemaPath = path.join(
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
78
|
-
const
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
// ───
|
|
771
|
+
// ─── KDNA Card (locale-aware) ────────────────────────────────────
|
|
779
772
|
|
|
780
|
-
function
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
784
|
+
return card;
|
|
785
|
+
}
|
|
792
786
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
};
|