@aikdna/kdna-cli 0.9.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +73 -0
- package/package.json +58 -0
- package/skills/kdna-loader/SKILL.md +257 -0
- package/src/agent.js +434 -0
- package/src/cli.js +260 -0
- package/src/cluster.js +235 -0
- package/src/cmds/_common.js +100 -0
- package/src/cmds/cluster.js +235 -0
- package/src/cmds/domain.js +638 -0
- package/src/cmds/identity.js +31 -0
- package/src/cmds/legacy.js +83 -0
- package/src/cmds/quality.js +87 -0
- package/src/cmds/registry.js +114 -0
- package/src/cmds/setup.js +8 -0
- package/src/compare.js +324 -0
- package/src/diff.js +288 -0
- package/src/identity.js +211 -0
- package/src/init.js +168 -0
- package/src/install.js +849 -0
- package/src/loader.js +70 -0
- package/src/publish.js +600 -0
- package/src/registry.js +258 -0
- package/src/search.js +73 -0
- package/src/setup.js +197 -0
- package/src/verify.js +423 -0
- package/src/version.js +112 -0
- package/templates/cluster/KDNA_Cluster.json +25 -0
- package/templates/cluster/README.md +32 -0
- package/templates/minimal-domain/KDNA_Core.json +54 -0
- package/templates/minimal-domain/KDNA_Patterns.json +37 -0
- package/templates/minimal-domain/kdna.json +31 -0
- package/templates/minimal-domain/tests/before-after.json +16 -0
- package/templates/standard-domain/KDNA_Core.json +76 -0
- package/templates/standard-domain/KDNA_Patterns.json +44 -0
- package/templates/standard-domain/README.md +74 -0
- package/templates/standard-domain/USAGE.md +59 -0
- package/templates/standard-domain/evals/1_excluded_case.json +16 -0
- package/templates/standard-domain/evals/3_boundary_cases.json +38 -0
- package/templates/standard-domain/evals/3_core_cases.json +35 -0
- package/templates/standard-domain/evals/3_failure_cases.json +35 -0
- package/templates/standard-domain/evals/scoring.json +60 -0
- package/templates/standard-domain/kdna.json +28 -0
- package/validators/kdna-lint.js +53 -0
- package/validators/kdna-validate.js +92 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { error, readJson, writeJson } = require('./_common');
|
|
5
|
+
|
|
6
|
+
// ─── Validate ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function cmdValidate(dir, schemaOnly) {
|
|
9
|
+
const abs = path.resolve(dir);
|
|
10
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
11
|
+
error(`Not a directory: ${abs}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { lintDomain, validateDomainSchema, validateCrossFile } = require('@aikdna/kdna-core');
|
|
15
|
+
|
|
16
|
+
// Resolve schemas from @aikdna/kdna-core package
|
|
17
|
+
const SCHEMA_DIR = path.join(
|
|
18
|
+
path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
|
|
19
|
+
'schema',
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Read all KDNA JSON files
|
|
23
|
+
const files = fs.readdirSync(abs).filter((f) => f.endsWith('.json') && f !== 'kdna.json');
|
|
24
|
+
const dataMap = {};
|
|
25
|
+
const schemaMap = {};
|
|
26
|
+
|
|
27
|
+
for (const f of files) {
|
|
28
|
+
try {
|
|
29
|
+
dataMap[f] = JSON.parse(fs.readFileSync(path.join(abs, f), 'utf8'));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
dataMap[f] = null;
|
|
32
|
+
console.error(` JSON parse error in ${f}: ${e.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Schema validation — always load all available schemas
|
|
37
|
+
const FILE_TO_SCHEMA = {
|
|
38
|
+
'KDNA_Core.json': 'KDNA_Core.schema.json',
|
|
39
|
+
'KDNA_Patterns.json': 'KDNA_Patterns.schema.json',
|
|
40
|
+
'KDNA_Scenarios.json': 'KDNA_Scenarios.schema.json',
|
|
41
|
+
'KDNA_Cases.json': 'KDNA_Cases.schema.json',
|
|
42
|
+
'KDNA_Reasoning.json': 'KDNA_Reasoning.schema.json',
|
|
43
|
+
'KDNA_Evolution.json': 'KDNA_Evolution.schema.json',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const loadedSchemas = [];
|
|
47
|
+
const missingSchemas = [];
|
|
48
|
+
for (const [, schemaFile] of Object.entries(FILE_TO_SCHEMA)) {
|
|
49
|
+
const schemaPath = path.join(SCHEMA_DIR, schemaFile);
|
|
50
|
+
if (fs.existsSync(schemaPath)) {
|
|
51
|
+
try {
|
|
52
|
+
schemaMap[schemaFile] = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
53
|
+
loadedSchemas.push(schemaFile);
|
|
54
|
+
} catch {
|
|
55
|
+
missingSchemas.push(schemaFile);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
missingSchemas.push(schemaFile);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (missingSchemas.length) {
|
|
63
|
+
console.log(
|
|
64
|
+
` Note: ${missingSchemas.length} schema file(s) not found (optional file schemas): ${missingSchemas.join(', ')}`,
|
|
65
|
+
);
|
|
66
|
+
console.log(` Schema dir: ${SCHEMA_DIR}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validation layers
|
|
70
|
+
const errors = [];
|
|
71
|
+
const warnings = [];
|
|
72
|
+
|
|
73
|
+
// Layer 1: Lint (structural + content checks)
|
|
74
|
+
if (!schemaOnly) {
|
|
75
|
+
const lintResult = lintDomain(dataMap);
|
|
76
|
+
errors.push(...lintResult.errors);
|
|
77
|
+
warnings.push(...lintResult.warnings);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Layer 2: JSON Schema validation against loaded schemas
|
|
81
|
+
const schemaResult = validateDomainSchema(dataMap, schemaMap);
|
|
82
|
+
errors.push(...schemaResult.errors);
|
|
83
|
+
warnings.push(...schemaResult.warnings);
|
|
84
|
+
|
|
85
|
+
// Layer 3: Cross-file consistency
|
|
86
|
+
const crossResult = validateCrossFile(dataMap);
|
|
87
|
+
errors.push(...crossResult.errors);
|
|
88
|
+
warnings.push(...crossResult.warnings);
|
|
89
|
+
|
|
90
|
+
if (warnings.length) {
|
|
91
|
+
console.log('Warnings:');
|
|
92
|
+
warnings.forEach((w) => console.log(` - ${w}`));
|
|
93
|
+
}
|
|
94
|
+
if (errors.length) {
|
|
95
|
+
console.error('Errors:');
|
|
96
|
+
errors.forEach((e) => console.error(` - ${e}`));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const validCount = Object.keys(dataMap).filter((k) => dataMap[k]).length;
|
|
101
|
+
const schemaInfo = schemaOnly
|
|
102
|
+
? ` (schema-only mode, ${loadedSchemas.length} schemas loaded)`
|
|
103
|
+
: '';
|
|
104
|
+
console.log(`✓ KDNA domain valid: ${abs} (${validCount} files, schema OK${schemaInfo})`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Pack / Unpack (.kdna ZIP container) ──────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function cmdPack(dir, outputDir) {
|
|
110
|
+
const abs = path.resolve(dir);
|
|
111
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
112
|
+
error(`Not a directory: ${abs}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const core = readJson(path.join(abs, 'KDNA_Core.json'));
|
|
116
|
+
const pat = readJson(path.join(abs, 'KDNA_Patterns.json'));
|
|
117
|
+
if (!core) error('KDNA_Core.json not found or invalid');
|
|
118
|
+
if (!pat) error('KDNA_Patterns.json not found or invalid');
|
|
119
|
+
|
|
120
|
+
const domainName = core.meta?.domain || path.basename(abs);
|
|
121
|
+
|
|
122
|
+
// Ensure kdna.json manifest exists (generate if missing)
|
|
123
|
+
let manifest = readJson(path.join(abs, 'kdna.json'));
|
|
124
|
+
if (!manifest) {
|
|
125
|
+
const jsonCount = fs
|
|
126
|
+
.readdirSync(abs)
|
|
127
|
+
.filter((f) => f.endsWith('.json') && f !== 'kdna.json').length;
|
|
128
|
+
manifest = {
|
|
129
|
+
kdna_spec: '0.4',
|
|
130
|
+
name: domainName,
|
|
131
|
+
version: core.meta?.version || '0.1.0',
|
|
132
|
+
status: 'experimental',
|
|
133
|
+
access: 'open',
|
|
134
|
+
language: 'en',
|
|
135
|
+
author: { name: '', id: '' },
|
|
136
|
+
license: { type: 'CC-BY-4.0' },
|
|
137
|
+
description: core.meta?.purpose || `${domainName} domain cognition`,
|
|
138
|
+
file_count: jsonCount,
|
|
139
|
+
created: core.meta?.created || new Date().toISOString().slice(0, 10),
|
|
140
|
+
updated: new Date().toISOString().slice(0, 10),
|
|
141
|
+
};
|
|
142
|
+
writeJson(path.join(abs, 'kdna.json'), manifest);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Create ZIP container — try python3, then zip command, then Node.js native
|
|
146
|
+
const outName = `${domainName}.kdna`;
|
|
147
|
+
const outPath = outputDir ? path.join(outputDir, outName) : path.join(process.cwd(), outName);
|
|
148
|
+
if (outputDir && !fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
149
|
+
|
|
150
|
+
let packed = false;
|
|
151
|
+
|
|
152
|
+
// Strategy 1: python3 zipfile (built-in on macOS, most Linux) — use temp file
|
|
153
|
+
const tmpPyFile = path.join(
|
|
154
|
+
fs.existsSync('/tmp') ? '/tmp' : require('os').tmpdir(),
|
|
155
|
+
`kdna-pack-${Date.now()}.py`,
|
|
156
|
+
);
|
|
157
|
+
try {
|
|
158
|
+
const pyScript = `import zipfile, os
|
|
159
|
+
src = ${JSON.stringify(abs)}
|
|
160
|
+
out = ${JSON.stringify(outPath)}
|
|
161
|
+
with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
162
|
+
for f in sorted(os.listdir(src)):
|
|
163
|
+
fp = os.path.join(src, f)
|
|
164
|
+
if os.path.isfile(fp) and (f.endswith('.json') or f in ('README.md', 'LICENSE', 'kdna.json')):
|
|
165
|
+
zf.write(fp, f)
|
|
166
|
+
`;
|
|
167
|
+
fs.writeFileSync(tmpPyFile, pyScript);
|
|
168
|
+
execSync(`python3 ${tmpPyFile}`, { stdio: 'pipe' });
|
|
169
|
+
packed = true;
|
|
170
|
+
} catch {
|
|
171
|
+
/* Strategy 1 failed, try next */
|
|
172
|
+
} finally {
|
|
173
|
+
try {
|
|
174
|
+
fs.unlinkSync(tmpPyFile);
|
|
175
|
+
} catch {
|
|
176
|
+
/* cleanup */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Strategy 2: system zip command
|
|
181
|
+
if (!packed) {
|
|
182
|
+
const cwd = process.cwd();
|
|
183
|
+
try {
|
|
184
|
+
process.chdir(abs);
|
|
185
|
+
execSync(
|
|
186
|
+
`zip -q -r "${outPath}" *.json README.md LICENSE 2>/dev/null || zip -q -r "${outPath}" *.json`,
|
|
187
|
+
{ stdio: 'pipe' },
|
|
188
|
+
);
|
|
189
|
+
process.chdir(cwd);
|
|
190
|
+
packed = true;
|
|
191
|
+
} catch {
|
|
192
|
+
process.chdir(cwd);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// #22: Strategy 3 — Node.js native ZIP (no external dependencies)
|
|
197
|
+
if (!packed) {
|
|
198
|
+
try {
|
|
199
|
+
createNodeZip(abs, outPath);
|
|
200
|
+
packed = true;
|
|
201
|
+
} catch {
|
|
202
|
+
/* last attempt failed */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!packed) {
|
|
207
|
+
const platform = process.platform;
|
|
208
|
+
const hints = {
|
|
209
|
+
darwin: 'macOS includes python3 — ensure it is in PATH.',
|
|
210
|
+
linux: 'Install python3 or zip: apt install python3 / yum install python3 / apk add python3',
|
|
211
|
+
win32: 'Install python3 from python.org, or use WSL.',
|
|
212
|
+
};
|
|
213
|
+
error(`Cannot create ZIP.\n${hints[platform] || 'Install python3 or zip command.'}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const fileCount = manifest.file_count || 0;
|
|
217
|
+
console.log(`✓ Packed: ${outPath}`);
|
|
218
|
+
console.log(` Domain: ${domainName} v${manifest.version}`);
|
|
219
|
+
console.log(` Files: ${fileCount} KDNA JSONs`);
|
|
220
|
+
console.log(` Container: ZIP (DEFLATE)`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// #22: Node.js-native ZIP creator (zero dependencies, fallback when python3/zip unavailable)
|
|
224
|
+
function createNodeZip(srcDir, outPath) {
|
|
225
|
+
const zlib = require('zlib');
|
|
226
|
+
const files = fs
|
|
227
|
+
.readdirSync(srcDir)
|
|
228
|
+
.filter((f) => f.endsWith('.json'))
|
|
229
|
+
.concat(['README.md', 'LICENSE'].filter((f) => fs.existsSync(path.join(srcDir, f))));
|
|
230
|
+
|
|
231
|
+
const centralDir = [];
|
|
232
|
+
const fileData = [];
|
|
233
|
+
let offset = 0;
|
|
234
|
+
|
|
235
|
+
for (const f of files) {
|
|
236
|
+
const raw = fs.readFileSync(path.join(srcDir, f));
|
|
237
|
+
const crc = crc32(raw);
|
|
238
|
+
const compressed = zlib.deflateRawSync(raw);
|
|
239
|
+
const useStore = compressed.length >= raw.length;
|
|
240
|
+
|
|
241
|
+
const nameBytes = Buffer.from(f, 'utf8');
|
|
242
|
+
const localHeader = Buffer.alloc(30);
|
|
243
|
+
localHeader.writeUInt32LE(0x04034b50, 0); // local file header signature
|
|
244
|
+
localHeader.writeUInt16LE(20, 4); // version needed
|
|
245
|
+
localHeader.writeUInt16LE(0x0800, 6); // general purpose bit flag (UTF-8)
|
|
246
|
+
localHeader.writeUInt16LE(useStore ? 0 : 8, 8); // compression method: stored or deflated
|
|
247
|
+
// skip mod time, mod date
|
|
248
|
+
localHeader.writeUInt32LE(crc, 14);
|
|
249
|
+
localHeader.writeUInt32LE(useStore ? raw.length : compressed.length, 18); // compressed size
|
|
250
|
+
localHeader.writeUInt32LE(raw.length, 22); // uncompressed size
|
|
251
|
+
localHeader.writeUInt16LE(nameBytes.length, 26);
|
|
252
|
+
|
|
253
|
+
const stored = useStore ? raw : compressed;
|
|
254
|
+
|
|
255
|
+
fileData.push(Buffer.concat([localHeader, nameBytes, stored]));
|
|
256
|
+
offset += localHeader.length + nameBytes.length + stored.length;
|
|
257
|
+
|
|
258
|
+
// Central directory entry
|
|
259
|
+
const cdEntry = Buffer.alloc(46);
|
|
260
|
+
cdEntry.writeUInt32LE(0x02014b50, 0); // central dir signature
|
|
261
|
+
cdEntry.writeUInt16LE(20, 4); // version made by
|
|
262
|
+
cdEntry.writeUInt16LE(20, 6); // version needed
|
|
263
|
+
cdEntry.writeUInt16LE(0x0800, 8); // UTF-8
|
|
264
|
+
cdEntry.writeUInt16LE(useStore ? 0 : 8, 10);
|
|
265
|
+
cdEntry.writeUInt32LE(crc, 16);
|
|
266
|
+
cdEntry.writeUInt32LE(useStore ? raw.length : compressed.length, 20);
|
|
267
|
+
cdEntry.writeUInt32LE(raw.length, 24);
|
|
268
|
+
cdEntry.writeUInt16LE(nameBytes.length, 28);
|
|
269
|
+
cdEntry.writeUInt32LE(offset - stored.length - nameBytes.length - localHeader.length, 42);
|
|
270
|
+
centralDir.push(Buffer.concat([cdEntry, nameBytes]));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const cdOffset = offset;
|
|
274
|
+
const cdSize = centralDir.reduce((s, e) => s + e.length, 0);
|
|
275
|
+
const eocd = Buffer.alloc(22);
|
|
276
|
+
eocd.writeUInt32LE(0x06054b50, 0); // EOCD signature
|
|
277
|
+
eocd.writeUInt16LE(0, 4); // disk number
|
|
278
|
+
eocd.writeUInt16LE(0, 6); // disk with CD
|
|
279
|
+
eocd.writeUInt16LE(files.length, 8); // entries on disk
|
|
280
|
+
eocd.writeUInt16LE(files.length, 10); // total entries
|
|
281
|
+
eocd.writeUInt32LE(cdSize, 12); // CD size
|
|
282
|
+
eocd.writeUInt32LE(cdOffset, 16); // CD offset
|
|
283
|
+
eocd.writeUInt16LE(0, 20); // comment length
|
|
284
|
+
|
|
285
|
+
const all = Buffer.concat([...fileData, ...centralDir, eocd]);
|
|
286
|
+
fs.writeFileSync(outPath, all);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function crc32(buf) {
|
|
290
|
+
let crc = 0xffffffff;
|
|
291
|
+
for (let i = 0; i < buf.length; i++) {
|
|
292
|
+
crc ^= buf[i];
|
|
293
|
+
for (let j = 0; j < 8; j++) {
|
|
294
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function cmdUnpack(filePath, force) {
|
|
301
|
+
const abs = path.resolve(filePath);
|
|
302
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
303
|
+
error(`Not a file: ${abs}`);
|
|
304
|
+
}
|
|
305
|
+
if (!abs.endsWith('.kdna')) {
|
|
306
|
+
error(`Not a .kdna file: ${abs}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const domainName = path.basename(abs, '.kdna');
|
|
310
|
+
const outDir = path.join(path.dirname(abs), domainName);
|
|
311
|
+
|
|
312
|
+
if (fs.existsSync(outDir)) {
|
|
313
|
+
if (!force) error(`Directory already exists: ${outDir}\nUse --force to overwrite.`);
|
|
314
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
318
|
+
|
|
319
|
+
// Unzip using python3 zipfile (built-in) — use temp file to avoid -c multiline escaping issues
|
|
320
|
+
const tmpUnpackPy = path.join(
|
|
321
|
+
fs.existsSync('/tmp') ? '/tmp' : require('os').tmpdir(),
|
|
322
|
+
`kdna-unpack-${Date.now()}.py`,
|
|
323
|
+
);
|
|
324
|
+
try {
|
|
325
|
+
const script = `import zipfile, os
|
|
326
|
+
zf = zipfile.ZipFile(${JSON.stringify(abs)}, 'r')
|
|
327
|
+
zf.extractall(${JSON.stringify(outDir)})
|
|
328
|
+
zf.close()
|
|
329
|
+
`;
|
|
330
|
+
fs.writeFileSync(tmpUnpackPy, script);
|
|
331
|
+
execSync(`python3 ${tmpUnpackPy}`, { stdio: 'pipe' });
|
|
332
|
+
} catch {
|
|
333
|
+
// Fallback: use system unzip
|
|
334
|
+
try {
|
|
335
|
+
execSync(`unzip -q -o "${abs}" -d "${outDir}"`, { stdio: 'pipe' });
|
|
336
|
+
} catch {
|
|
337
|
+
error('Cannot unpack ZIP. Install python3 or unzip command.');
|
|
338
|
+
}
|
|
339
|
+
} finally {
|
|
340
|
+
try {
|
|
341
|
+
fs.unlinkSync(tmpUnpackPy);
|
|
342
|
+
} catch {
|
|
343
|
+
/* cleanup */
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log(`✓ Unpacked: ${outDir}`);
|
|
348
|
+
const files = fs.readdirSync(outDir);
|
|
349
|
+
console.log(` Files: ${files.length}`);
|
|
350
|
+
files.forEach((f) => console.log(` ${f}`));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ─── Inspect .kdna file (ZIP container or legacy merged JSON) ────────────
|
|
354
|
+
|
|
355
|
+
function inspectKdnaFile(filePath) {
|
|
356
|
+
const abs = path.resolve(filePath);
|
|
357
|
+
fs.statSync(abs); // verify file exists
|
|
358
|
+
|
|
359
|
+
// Detect format: ZIP container (binary header PK\x03\x04) vs text
|
|
360
|
+
const head = Buffer.alloc(4);
|
|
361
|
+
const fd = fs.openSync(abs, 'r');
|
|
362
|
+
fs.readSync(fd, head, 0, 4, 0);
|
|
363
|
+
fs.closeSync(fd);
|
|
364
|
+
const isZip = head[0] === 0x50 && head[1] === 0x4b;
|
|
365
|
+
|
|
366
|
+
let core, patterns, manifest;
|
|
367
|
+
const presentFiles = [];
|
|
368
|
+
|
|
369
|
+
if (isZip) {
|
|
370
|
+
// ZIP container — extract to temp, read files
|
|
371
|
+
const os = require('os');
|
|
372
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-inspect-'));
|
|
373
|
+
try {
|
|
374
|
+
const tmpInspectPy = path.join(
|
|
375
|
+
fs.existsSync('/tmp') ? '/tmp' : require('os').tmpdir(),
|
|
376
|
+
`kdna-inspect-${Date.now()}.py`,
|
|
377
|
+
);
|
|
378
|
+
try {
|
|
379
|
+
const script = `import zipfile, os
|
|
380
|
+
zf = zipfile.ZipFile(${JSON.stringify(abs)}, 'r')
|
|
381
|
+
zf.extractall(${JSON.stringify(tmpDir)})
|
|
382
|
+
zf.close()
|
|
383
|
+
`;
|
|
384
|
+
fs.writeFileSync(tmpInspectPy, script);
|
|
385
|
+
execSync(`python3 ${tmpInspectPy}`, { stdio: 'pipe' });
|
|
386
|
+
} finally {
|
|
387
|
+
try {
|
|
388
|
+
fs.unlinkSync(tmpInspectPy);
|
|
389
|
+
} catch {
|
|
390
|
+
/* cleanup */
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
try {
|
|
395
|
+
execSync(`unzip -q -o "${abs}" -d "${tmpDir}"`, { stdio: 'pipe' });
|
|
396
|
+
} catch {
|
|
397
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
398
|
+
error('Cannot read .kdna container. Install python3 or unzip.');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
core = readJson(path.join(tmpDir, 'KDNA_Core.json'));
|
|
403
|
+
patterns = readJson(path.join(tmpDir, 'KDNA_Patterns.json'));
|
|
404
|
+
manifest = readJson(path.join(tmpDir, 'kdna.json'));
|
|
405
|
+
|
|
406
|
+
for (const f of fs.readdirSync(tmpDir)) {
|
|
407
|
+
if (f.startsWith('KDNA_') && f.endsWith('.json')) {
|
|
408
|
+
presentFiles.push(f);
|
|
409
|
+
}
|
|
410
|
+
if (f === 'README.md' || f === 'LICENSE') presentFiles.push(f);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
414
|
+
} else {
|
|
415
|
+
// Legacy merged JSON/YAML format (deprecated)
|
|
416
|
+
const raw = fs.readFileSync(abs, 'utf8');
|
|
417
|
+
let data;
|
|
418
|
+
try {
|
|
419
|
+
data = JSON.parse(raw);
|
|
420
|
+
} catch {
|
|
421
|
+
data = parseSimpleYaml(raw);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (!data || !data.meta) error(`Invalid .kdna file: missing meta section`);
|
|
425
|
+
|
|
426
|
+
const m = data.meta || {};
|
|
427
|
+
manifest = {
|
|
428
|
+
name: m.name || m.domain,
|
|
429
|
+
version: m.version || '?',
|
|
430
|
+
status: data.status || '?',
|
|
431
|
+
access: data.access || '?',
|
|
432
|
+
language: data.language || '?',
|
|
433
|
+
author: data.author || { name: '?' },
|
|
434
|
+
license: data.license || { type: '?' },
|
|
435
|
+
description: data.description || m.purpose || '?',
|
|
436
|
+
spec_version: m.spec_version || data.kdna_spec || '?',
|
|
437
|
+
};
|
|
438
|
+
core = data.core || {};
|
|
439
|
+
patterns = data.patterns || {};
|
|
440
|
+
presentFiles.push('.kdna (legacy merged format)');
|
|
441
|
+
if (data.scenarios) {
|
|
442
|
+
presentFiles.push('scenarios (inline)');
|
|
443
|
+
}
|
|
444
|
+
if (data.cases) {
|
|
445
|
+
presentFiles.push('cases (inline)');
|
|
446
|
+
}
|
|
447
|
+
if (data.reasoning) {
|
|
448
|
+
presentFiles.push('reasoning (inline)');
|
|
449
|
+
}
|
|
450
|
+
if (data.evolution) {
|
|
451
|
+
presentFiles.push('evolution (inline)');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!core) error('KDNA_Core.json not found in container');
|
|
456
|
+
|
|
457
|
+
const m = manifest || {};
|
|
458
|
+
const c = core;
|
|
459
|
+
const p = patterns || {};
|
|
460
|
+
|
|
461
|
+
console.log('═'.repeat(50));
|
|
462
|
+
console.log(` ${m.name || c.meta?.domain || path.basename(abs, '.kdna')} — KDNA Domain`);
|
|
463
|
+
console.log('═'.repeat(50));
|
|
464
|
+
console.log('');
|
|
465
|
+
console.log(` Format: .kdna ${isZip ? '(ZIP container)' : '(legacy merged)'}`);
|
|
466
|
+
console.log(` Spec: ${m.spec_version || m.kdna_spec || '0.4'}`);
|
|
467
|
+
console.log(` Version: ${m.version || '?'}`);
|
|
468
|
+
console.log(` Status: ${m.status || 'experimental'}`);
|
|
469
|
+
console.log(` Access: ${m.access || 'open'}`);
|
|
470
|
+
console.log(` Author: ${m.author?.name || '?'}`);
|
|
471
|
+
console.log(` License: ${m.license?.type || '?'}`);
|
|
472
|
+
console.log(` Created: ${m.created || c.meta?.created || '?'}`);
|
|
473
|
+
console.log(` Description: ${m.description || c.meta?.purpose || '?'}`);
|
|
474
|
+
console.log('');
|
|
475
|
+
console.log(' ── Content ──');
|
|
476
|
+
console.log(` Axioms: ${(c.axioms || []).length}`);
|
|
477
|
+
console.log(` Ontology concepts: ${(c.ontology || []).length}`);
|
|
478
|
+
console.log(` Frameworks: ${(c.frameworks || []).length}`);
|
|
479
|
+
console.log(` Stances: ${(c.stances || []).length}`);
|
|
480
|
+
console.log(` Banned terms: ${(p.terminology?.banned_terms || []).length}`);
|
|
481
|
+
console.log(` Misunderstandings: ${(p.misunderstandings || []).length}`);
|
|
482
|
+
console.log(` Self-checks: ${(p.self_check || []).length}`);
|
|
483
|
+
console.log('');
|
|
484
|
+
console.log(' ── Files ──');
|
|
485
|
+
presentFiles.forEach((f) => console.log(` ${f}`));
|
|
486
|
+
console.log('');
|
|
487
|
+
console.log('═'.repeat(50));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function parseSimpleYaml(raw) {
|
|
491
|
+
// Parse a simple subset of YAML (no nesting beyond 1 level for sections)
|
|
492
|
+
const result = {};
|
|
493
|
+
let currentSection = null;
|
|
494
|
+
|
|
495
|
+
const lines = raw.split('\n');
|
|
496
|
+
for (const line of lines) {
|
|
497
|
+
const trimmed = line.trim();
|
|
498
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
499
|
+
|
|
500
|
+
// Section header: "core:" or " core:" etc
|
|
501
|
+
if (/^[a-z_]+:$/.test(trimmed)) {
|
|
502
|
+
currentSection = trimmed.slice(0, -1);
|
|
503
|
+
if (!result[currentSection]) result[currentSection] = {};
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Key: value
|
|
508
|
+
const kv = trimmed.match(/^([a-z_]+):\s*(.*)/i);
|
|
509
|
+
if (kv && !kv[1].startsWith('-')) {
|
|
510
|
+
const key = kv[1];
|
|
511
|
+
const val = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
512
|
+
if (currentSection) {
|
|
513
|
+
if (key === 'version' && typeof result[currentSection] === 'object') {
|
|
514
|
+
result[currentSection][key] = val;
|
|
515
|
+
} else if (!result[currentSection][key]) {
|
|
516
|
+
result[currentSection][key] = val;
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
result[key] = val;
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Array item: "- value"
|
|
525
|
+
if (trimmed.startsWith('- ') && currentSection) {
|
|
526
|
+
// For counts only, we don't parse full arrays
|
|
527
|
+
if (currentSection === 'axioms' || currentSection === 'stances') {
|
|
528
|
+
if (!result.core) result.core = {};
|
|
529
|
+
if (!result.core[currentSection]) result.core[currentSection] = [];
|
|
530
|
+
result.core[currentSection].push({ _parsed: true });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ─── Inspect ───────────────────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
function cmdInspect(dir) {
|
|
541
|
+
const abs = path.resolve(dir);
|
|
542
|
+
const stat = fs.existsSync(abs) ? fs.statSync(abs) : null;
|
|
543
|
+
if (!stat) error(`Path not found: ${abs}`);
|
|
544
|
+
|
|
545
|
+
// Single .kdna file
|
|
546
|
+
if (stat.isFile() && abs.endsWith('.kdna')) {
|
|
547
|
+
inspectKdnaFile(abs);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Directory — existing logic
|
|
552
|
+
if (!stat.isDirectory()) error(`Not a KDNA domain: ${abs}`);
|
|
553
|
+
|
|
554
|
+
const core = readJson(path.join(abs, 'KDNA_Core.json'));
|
|
555
|
+
const manifest = readJson(path.join(abs, 'kdna.json'));
|
|
556
|
+
|
|
557
|
+
if (!core) {
|
|
558
|
+
error(`Not a KDNA domain (KDNA_Core.json not found in ${abs})`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const m = manifest || {};
|
|
562
|
+
const c = core;
|
|
563
|
+
|
|
564
|
+
console.log('═'.repeat(50));
|
|
565
|
+
console.log(` ${m.name || c.meta?.domain || path.basename(abs)} — KDNA Domain`);
|
|
566
|
+
console.log('═'.repeat(50));
|
|
567
|
+
console.log('');
|
|
568
|
+
console.log(` Version: ${m.version || c.meta?.version || '?'}`);
|
|
569
|
+
console.log(` Status: ${m.status || 'experimental'}`);
|
|
570
|
+
console.log(` Access: ${m.access || 'open'}`);
|
|
571
|
+
console.log(` Language: ${m.language || c.meta?.language || '?'}`);
|
|
572
|
+
console.log(` Author: ${m.author?.name || '?'}`);
|
|
573
|
+
if (m.author?.id) console.log(` ${m.author.id}`);
|
|
574
|
+
console.log(` License: ${m.license?.type || '?'}`);
|
|
575
|
+
console.log(` Created: ${c.meta?.created || '?'}`);
|
|
576
|
+
console.log(` Description: ${m.description || c.meta?.purpose || '?'}`);
|
|
577
|
+
console.log('');
|
|
578
|
+
|
|
579
|
+
const expected = [
|
|
580
|
+
'KDNA_Core.json',
|
|
581
|
+
'KDNA_Patterns.json',
|
|
582
|
+
'KDNA_Scenarios.json',
|
|
583
|
+
'KDNA_Cases.json',
|
|
584
|
+
'KDNA_Reasoning.json',
|
|
585
|
+
'KDNA_Evolution.json',
|
|
586
|
+
];
|
|
587
|
+
|
|
588
|
+
console.log(' ── File Set ──');
|
|
589
|
+
for (const f of expected) {
|
|
590
|
+
const exists = fs.existsSync(path.join(abs, f));
|
|
591
|
+
console.log(` ${exists ? '✓' : '○'} ${f}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
console.log('');
|
|
595
|
+
console.log(' ── Content ──');
|
|
596
|
+
console.log(` Axioms: ${(c.axioms || []).length}`);
|
|
597
|
+
console.log(` Ontology concepts: ${(c.ontology || []).length}`);
|
|
598
|
+
console.log(` Frameworks: ${(c.frameworks || []).length}`);
|
|
599
|
+
console.log(` Core structures: ${(c.core_structure || []).length}`);
|
|
600
|
+
console.log(` Stances: ${(c.stances || []).length}`);
|
|
601
|
+
|
|
602
|
+
const pat = readJson(path.join(abs, 'KDNA_Patterns.json'));
|
|
603
|
+
if (pat) {
|
|
604
|
+
const preferred = pat.terminology?.preferred_terms || pat.terminology?.standard_terms || [];
|
|
605
|
+
console.log(` Preferred terms: ${preferred.length}`);
|
|
606
|
+
console.log(` Banned terms: ${(pat.terminology?.banned_terms || []).length}`);
|
|
607
|
+
console.log(` Misunderstandings: ${(pat.misunderstandings || []).length}`);
|
|
608
|
+
console.log(` Self-checks: ${(pat.self_check || []).length}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const sce = readJson(path.join(abs, 'KDNA_Scenarios.json'));
|
|
612
|
+
if (sce) console.log(` Scenarios: ${(sce.scenes || []).length}`);
|
|
613
|
+
|
|
614
|
+
const cas = readJson(path.join(abs, 'KDNA_Cases.json'));
|
|
615
|
+
if (cas) console.log(` Cases: ${(cas.cases || []).length}`);
|
|
616
|
+
|
|
617
|
+
const rea = readJson(path.join(abs, 'KDNA_Reasoning.json'));
|
|
618
|
+
if (rea) console.log(` Reasoning chains: ${(rea.reasoning_chains || []).length}`);
|
|
619
|
+
|
|
620
|
+
const evo = readJson(path.join(abs, 'KDNA_Evolution.json'));
|
|
621
|
+
if (evo) console.log(` Evolution stages: ${(evo.stages || []).length}`);
|
|
622
|
+
|
|
623
|
+
console.log('');
|
|
624
|
+
console.log(' ── Axioms ──');
|
|
625
|
+
for (const a of c.axioms || []) {
|
|
626
|
+
console.log(` • ${a.one_sentence}`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
console.log('');
|
|
630
|
+
console.log('═'.repeat(50));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
module.exports = {
|
|
634
|
+
cmdValidate,
|
|
635
|
+
cmdPack,
|
|
636
|
+
cmdUnpack,
|
|
637
|
+
cmdInspect,
|
|
638
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { error } = require('./_common');
|
|
2
|
+
|
|
3
|
+
function cmdIdentity(args) {
|
|
4
|
+
const {
|
|
5
|
+
cmdIdentityInit,
|
|
6
|
+
cmdIdentityShow,
|
|
7
|
+
cmdIdentityExport,
|
|
8
|
+
cmdIdentityImport,
|
|
9
|
+
} = require('../identity');
|
|
10
|
+
const sub = args[1];
|
|
11
|
+
if (sub === 'init') {
|
|
12
|
+
cmdIdentityInit();
|
|
13
|
+
} else if (sub === 'show') {
|
|
14
|
+
cmdIdentityShow();
|
|
15
|
+
} else if (sub === 'export') {
|
|
16
|
+
const outIdx = args.indexOf('--out');
|
|
17
|
+
cmdIdentityExport(outIdx >= 0 ? args[outIdx + 1] : null);
|
|
18
|
+
} else if (sub === 'import') {
|
|
19
|
+
const target = args[2];
|
|
20
|
+
if (!target) error('Usage: kdna identity import <file>');
|
|
21
|
+
cmdIdentityImport(target);
|
|
22
|
+
} else {
|
|
23
|
+
error(
|
|
24
|
+
`Usage: kdna identity init\n kdna identity show\n kdna identity export [--out <file>]\n kdna identity import <file>`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
cmdIdentity,
|
|
31
|
+
};
|