@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.
Files changed (46) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +73 -0
  4. package/package.json +58 -0
  5. package/skills/kdna-loader/SKILL.md +257 -0
  6. package/src/agent.js +434 -0
  7. package/src/cli.js +260 -0
  8. package/src/cluster.js +235 -0
  9. package/src/cmds/_common.js +100 -0
  10. package/src/cmds/cluster.js +235 -0
  11. package/src/cmds/domain.js +638 -0
  12. package/src/cmds/identity.js +31 -0
  13. package/src/cmds/legacy.js +83 -0
  14. package/src/cmds/quality.js +87 -0
  15. package/src/cmds/registry.js +114 -0
  16. package/src/cmds/setup.js +8 -0
  17. package/src/compare.js +324 -0
  18. package/src/diff.js +288 -0
  19. package/src/identity.js +211 -0
  20. package/src/init.js +168 -0
  21. package/src/install.js +849 -0
  22. package/src/loader.js +70 -0
  23. package/src/publish.js +600 -0
  24. package/src/registry.js +258 -0
  25. package/src/search.js +73 -0
  26. package/src/setup.js +197 -0
  27. package/src/verify.js +423 -0
  28. package/src/version.js +112 -0
  29. package/templates/cluster/KDNA_Cluster.json +25 -0
  30. package/templates/cluster/README.md +32 -0
  31. package/templates/minimal-domain/KDNA_Core.json +54 -0
  32. package/templates/minimal-domain/KDNA_Patterns.json +37 -0
  33. package/templates/minimal-domain/kdna.json +31 -0
  34. package/templates/minimal-domain/tests/before-after.json +16 -0
  35. package/templates/standard-domain/KDNA_Core.json +76 -0
  36. package/templates/standard-domain/KDNA_Patterns.json +44 -0
  37. package/templates/standard-domain/README.md +74 -0
  38. package/templates/standard-domain/USAGE.md +59 -0
  39. package/templates/standard-domain/evals/1_excluded_case.json +16 -0
  40. package/templates/standard-domain/evals/3_boundary_cases.json +38 -0
  41. package/templates/standard-domain/evals/3_core_cases.json +35 -0
  42. package/templates/standard-domain/evals/3_failure_cases.json +35 -0
  43. package/templates/standard-domain/evals/scoring.json +60 -0
  44. package/templates/standard-domain/kdna.json +28 -0
  45. package/validators/kdna-lint.js +53 -0
  46. package/validators/kdna-validate.js +92 -0
package/src/diff.js ADDED
@@ -0,0 +1,288 @@
1
+ /**
2
+ * kdna diff <name>@<ver1> <name>@<ver2> — Judgment-level diff between versions.
3
+ *
4
+ * Downloads two .kdna packages from the registry, extracts to temp dirs,
5
+ * compares axioms / misunderstandings / banned_terms / stances / boundary,
6
+ * and surfaces what would change for an agent loading the new version.
7
+ *
8
+ * Not a structural file diff — a judgment diff:
9
+ * - Added/removed/changed axioms with their applies_when boundaries
10
+ * - Added/removed/changed misunderstandings
11
+ * - Added/removed banned terms
12
+ * - judgment_version bump (if declared)
13
+ *
14
+ * Usage:
15
+ * kdna diff @aikdna/writing@0.7.1 @aikdna/writing@0.7.2
16
+ * kdna diff @aikdna/writing # latest vs installed
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { execSync, execFileSync } = require('child_process');
22
+ const { RegistryResolver, parseName } = require('./registry');
23
+
24
+ const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
25
+ const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
26
+ const TMP_DIR = '/tmp';
27
+
28
+ function error(msg) {
29
+ console.error(`Error: ${msg}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ function readJson(p) {
34
+ try {
35
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ // ─── Parse "<name>@<ver>" ──────────────────────────────────────────────
42
+
43
+ function parseNameVersion(input) {
44
+ // accept @scope/name@version OR @scope/name OR bare@ver OR bare
45
+ const atSplit = input.split('@');
46
+ if (input.startsWith('@')) {
47
+ // @scope/name OR @scope/name@version
48
+ if (atSplit.length === 2) return { full: input, version: null };
49
+ if (atSplit.length === 3) return { full: '@' + atSplit[1], version: atSplit[2] };
50
+ } else {
51
+ if (atSplit.length === 1) return { full: input, version: null };
52
+ if (atSplit.length === 2) return { full: atSplit[0], version: atSplit[1] };
53
+ }
54
+ return null;
55
+ }
56
+
57
+ // ─── Download specific version ────────────────────────────────────────
58
+
59
+ function downloadVersion(entry, version, destDir) {
60
+ // entry.kdna_url is for the registry-current version. For older versions
61
+ // we infer the URL pattern from the registry-current URL.
62
+ if (entry.version === version) {
63
+ return downloadAndExtract(entry.kdna_url, destDir);
64
+ }
65
+
66
+ // Infer pattern: replace v<current> in the URL with v<requested>
67
+ const inferredUrl = entry.kdna_url
68
+ .replace(`/v${entry.version}/`, `/v${version}/`)
69
+ .replace(`-${entry.version}.kdna`, `-${version}.kdna`);
70
+
71
+ return downloadAndExtract(inferredUrl, destDir);
72
+ }
73
+
74
+ function downloadAndExtract(url, destDir) {
75
+ const tmpFile = `${destDir}.kdna.tmp`;
76
+ try {
77
+ execFileSync('curl', ['-fsSL', '--retry', '2', '-o', tmpFile, url], {
78
+ timeout: 60000,
79
+ stdio: 'pipe',
80
+ });
81
+ } catch (e) {
82
+ error(`Failed to download ${url}: ${e.stderr?.toString().trim() || e.message}`);
83
+ }
84
+
85
+ fs.mkdirSync(destDir, { recursive: true });
86
+ try {
87
+ execSync(`unzip -q -o "${tmpFile}" -d "${destDir}"`, { stdio: 'pipe' });
88
+ } catch {
89
+ const script = `import zipfile
90
+ zf = zipfile.ZipFile(${JSON.stringify(tmpFile)}, 'r')
91
+ zf.extractall(${JSON.stringify(destDir)})`;
92
+ execSync(`python3 -c ${JSON.stringify(script)}`, { stdio: 'pipe' });
93
+ }
94
+ fs.unlinkSync(tmpFile);
95
+ return destDir;
96
+ }
97
+
98
+ // ─── Extract judgment artifacts ────────────────────────────────────────
99
+
100
+ function loadJudgment(domainDir) {
101
+ const core = readJson(path.join(domainDir, 'KDNA_Core.json')) || {};
102
+ const pat = readJson(path.join(domainDir, 'KDNA_Patterns.json')) || {};
103
+ const manifest = readJson(path.join(domainDir, 'kdna.json')) || {};
104
+
105
+ return {
106
+ version: manifest.version || '?',
107
+ judgment_version: manifest.judgment_version || null,
108
+ axioms: Object.fromEntries((core.axioms || []).map((a) => [a.id, a])),
109
+ ontology: Object.fromEntries((core.ontology || []).map((o) => [o.id, o])),
110
+ stances: (core.stances || [])
111
+ .map((s) => (typeof s === 'string' ? s : s.stance))
112
+ .filter(Boolean),
113
+ misunderstandings: Object.fromEntries((pat.misunderstandings || []).map((m) => [m.id, m])),
114
+ banned_terms: Object.fromEntries((pat.terminology?.banned_terms || []).map((t) => [t.term, t])),
115
+ };
116
+ }
117
+
118
+ // ─── Set diff helpers ─────────────────────────────────────────────────
119
+
120
+ function diffMaps(label, oldMap, newMap, render) {
121
+ const oldIds = new Set(Object.keys(oldMap));
122
+ const newIds = new Set(Object.keys(newMap));
123
+ const added = [...newIds].filter((id) => !oldIds.has(id));
124
+ const removed = [...oldIds].filter((id) => !newIds.has(id));
125
+ const both = [...newIds].filter((id) => oldIds.has(id));
126
+ const changed = both.filter((id) => JSON.stringify(oldMap[id]) !== JSON.stringify(newMap[id]));
127
+
128
+ const out = { label, added, removed, changed };
129
+
130
+ console.log('');
131
+ console.log('─'.repeat(64));
132
+ console.log(` ${label.toUpperCase()}`);
133
+ console.log(` added:${added.length} removed:${removed.length} changed:${changed.length}`);
134
+ console.log('─'.repeat(64));
135
+
136
+ for (const id of added) {
137
+ console.log(` + ${id}`);
138
+ if (render) console.log(` "${render(newMap[id])}"`);
139
+ }
140
+ for (const id of removed) {
141
+ console.log(` - ${id}`);
142
+ if (render) console.log(` (was) "${render(oldMap[id])}"`);
143
+ }
144
+ for (const id of changed) {
145
+ console.log(` ~ ${id}`);
146
+ if (render) {
147
+ console.log(` was: "${render(oldMap[id])}"`);
148
+ console.log(` now: "${render(newMap[id])}"`);
149
+ }
150
+ // Surface key diffs in v2.1 boundary fields
151
+ const a = oldMap[id],
152
+ b = newMap[id];
153
+ for (const field of ['applies_when', 'does_not_apply_when', 'failure_risk', 'confidence']) {
154
+ const before = JSON.stringify(a[field] ?? null);
155
+ const after = JSON.stringify(b[field] ?? null);
156
+ if (before !== after) {
157
+ console.log(` [${field}] ${before} → ${after}`);
158
+ }
159
+ }
160
+ }
161
+
162
+ return out;
163
+ }
164
+
165
+ function diffStanceList(oldList, newList) {
166
+ const oldSet = new Set(oldList);
167
+ const newSet = new Set(newList);
168
+ const added = newList.filter((s) => !oldSet.has(s));
169
+ const removed = oldList.filter((s) => !newSet.has(s));
170
+ console.log('');
171
+ console.log('─'.repeat(64));
172
+ console.log(` STANCES added:${added.length} removed:${removed.length}`);
173
+ console.log('─'.repeat(64));
174
+ for (const s of added) console.log(` + "${s}"`);
175
+ for (const s of removed) console.log(` - "${s}"`);
176
+ }
177
+
178
+ // ─── Main ──────────────────────────────────────────────────────────────
179
+
180
+ async function cmdDiff(a, b) {
181
+ if (!a) error('Usage: kdna diff <name>@<v1> <name>@<v2> or kdna diff <name>');
182
+
183
+ const aParsed = parseNameVersion(a);
184
+ const bParsed = b ? parseNameVersion(b) : null;
185
+ if (!aParsed) error(`Cannot parse "${a}"`);
186
+
187
+ const resolver = new RegistryResolver({ allowNetwork: true });
188
+ let entryA;
189
+ try {
190
+ ({ entry: entryA } = resolver.resolve(aParsed.full));
191
+ } catch (e) {
192
+ error(e.message);
193
+ }
194
+
195
+ // Determine targets
196
+ let oldVersion, newVersion, oldEntry, newEntry;
197
+ if (bParsed) {
198
+ let entryB;
199
+ try {
200
+ ({ entry: entryB } = resolver.resolve(bParsed.full));
201
+ } catch (e) {
202
+ error(e.message);
203
+ }
204
+ if (aParsed.full !== bParsed.full)
205
+ error('Comparing across different domains is not supported.');
206
+ oldVersion = aParsed.version || entryA.version;
207
+ newVersion = bParsed.version || entryB.version;
208
+ oldEntry = entryA;
209
+ newEntry = entryB;
210
+ } else {
211
+ // single-arg form: installed vs registry-current
212
+ const parsed = parseName(aParsed.full);
213
+ const localDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
214
+ if (!fs.existsSync(localDir)) {
215
+ error(`${aParsed.full} not installed. Run: kdna install ${aParsed.full}`);
216
+ }
217
+ const localManifest = readJson(path.join(localDir, 'kdna.json'));
218
+ oldVersion = localManifest?.version || '?';
219
+ newVersion = entryA.version;
220
+ oldEntry = entryA;
221
+ newEntry = entryA;
222
+ if (oldVersion === newVersion) {
223
+ console.log(
224
+ `${aParsed.full}@${oldVersion}: only one version found.\n` +
225
+ `To compare across versions, specify two: kdna diff ${aParsed.full}@${oldVersion} ${aParsed.full}@<other>`,
226
+ );
227
+ return;
228
+ }
229
+ }
230
+
231
+ console.log('═'.repeat(64));
232
+ console.log(` kdna diff ${aParsed.full}`);
233
+ console.log(` ${oldVersion} → ${newVersion}`);
234
+ console.log('═'.repeat(64));
235
+
236
+ // Download both versions to temp dirs
237
+ const tmpOld = path.join(TMP_DIR, `kdna-diff-${Date.now()}-old`);
238
+ const tmpNew = path.join(TMP_DIR, `kdna-diff-${Date.now()}-new`);
239
+
240
+ console.log('Downloading old version...');
241
+ downloadVersion(oldEntry, oldVersion, tmpOld);
242
+ console.log('Downloading new version...');
243
+ downloadVersion(newEntry, newVersion, tmpNew);
244
+
245
+ const oldJ = loadJudgment(tmpOld);
246
+ const newJ = loadJudgment(tmpNew);
247
+
248
+ console.log('');
249
+ console.log(
250
+ ' judgment_version: ' +
251
+ (oldJ.judgment_version || '(not declared)') +
252
+ ' → ' +
253
+ (newJ.judgment_version || '(not declared)'),
254
+ );
255
+
256
+ diffMaps('axioms', oldJ.axioms, newJ.axioms, (a) => a.one_sentence || a.id);
257
+ diffMaps('ontology', oldJ.ontology, newJ.ontology, (o) => o.one_sentence || o.id);
258
+ diffMaps(
259
+ 'misunderstandings',
260
+ oldJ.misunderstandings,
261
+ newJ.misunderstandings,
262
+ (m) => m.wrong || m.id,
263
+ );
264
+ diffMaps('banned_terms', oldJ.banned_terms, newJ.banned_terms, (t) => t.term || '');
265
+ diffStanceList(oldJ.stances, newJ.stances);
266
+
267
+ // Cleanup
268
+ try {
269
+ fs.rmSync(tmpOld, { recursive: true, force: true });
270
+ } catch {
271
+ /* ignore */
272
+ }
273
+ try {
274
+ fs.rmSync(tmpNew, { recursive: true, force: true });
275
+ } catch {
276
+ /* ignore */
277
+ }
278
+
279
+ console.log('');
280
+ console.log('═'.repeat(64));
281
+ const drift = Object.keys(newJ.axioms).length - Object.keys(oldJ.axioms).length;
282
+ const note = drift !== 0 ? ` (axiom count drift: ${drift > 0 ? '+' : ''}${drift})` : '';
283
+ console.log(` Judgment surface change: ${oldVersion} → ${newVersion}${note}`);
284
+ console.log(` Agent loading the new version may classify, diagnose, or recommend differently.`);
285
+ console.log('═'.repeat(64));
286
+ }
287
+
288
+ module.exports = { cmdDiff, loadJudgment, parseNameVersion };
@@ -0,0 +1,211 @@
1
+ /**
2
+ * KDNA Identity — Ed25519 key pair generation and management.
3
+ *
4
+ * Commands:
5
+ * identity init Generate key pair
6
+ * identity show Display public key and buyer_id
7
+ * identity export [--out] Backup private key (age-encrypted)
8
+ * identity import <file> Restore from backup
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const crypto = require('crypto');
14
+
15
+ const IDENTITY_DIR =
16
+ process.env.KDNA_IDENTITY_DIR ||
17
+ path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna', 'identity');
18
+
19
+ const PRIVATE_KEY_PATH = path.join(IDENTITY_DIR, 'kdna.key');
20
+ const PUBLIC_KEY_PATH = path.join(IDENTITY_DIR, 'kdna.pub');
21
+
22
+ function error(msg) {
23
+ console.error(`Error: ${msg}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ // ─── Key Generation ────────────────────────────────────────────────────
28
+
29
+ function generateKeyPair() {
30
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519', {
31
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
32
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
33
+ });
34
+ return { publicKey, privateKey };
35
+ }
36
+
37
+ function deriveBuyerId(publicKeyPem) {
38
+ return crypto.createHash('sha256').update(publicKeyPem).digest('hex').substring(0, 16);
39
+ }
40
+
41
+ function fingerprint(publicKeyPem) {
42
+ return crypto.createHash('sha256').update(publicKeyPem).digest('hex').substring(0, 12);
43
+ }
44
+
45
+ function cmdIdentityInit() {
46
+ if (fs.existsSync(PRIVATE_KEY_PATH)) {
47
+ const pub = fs.readFileSync(PUBLIC_KEY_PATH, 'utf8');
48
+ const id = deriveBuyerId(pub);
49
+ console.log(`Identity already exists.`);
50
+ console.log(` Buyer ID: ${id}`);
51
+ console.log(` Public: ${PUBLIC_KEY_PATH}`);
52
+ console.log(` Private: ${PRIVATE_KEY_PATH}`);
53
+ return;
54
+ }
55
+
56
+ fs.mkdirSync(IDENTITY_DIR, { recursive: true });
57
+
58
+ const { publicKey, privateKey } = generateKeyPair();
59
+
60
+ fs.writeFileSync(PRIVATE_KEY_PATH, privateKey, { mode: 0o600 });
61
+ fs.writeFileSync(PUBLIC_KEY_PATH, publicKey, { mode: 0o644 });
62
+
63
+ const id = deriveBuyerId(publicKey);
64
+ const fp = fingerprint(publicKey);
65
+
66
+ console.log(`✓ Identity created.`);
67
+ console.log(` Buyer ID: ${id}`);
68
+ console.log(` Fingerprint: ${fp}`);
69
+ console.log(` Public key: ${PUBLIC_KEY_PATH}`);
70
+ console.log(` Private key: ${PRIVATE_KEY_PATH} (chmod 600)`);
71
+ console.log('');
72
+ console.log(` Backup your private key immediately:`);
73
+ console.log(` kdna identity export --out kdna-identity-backup.age`);
74
+ }
75
+
76
+ // ─── Show ──────────────────────────────────────────────────────────────
77
+
78
+ function cmdIdentityShow() {
79
+ if (!fs.existsSync(PUBLIC_KEY_PATH)) {
80
+ error('No identity found. Run: kdna identity init');
81
+ }
82
+
83
+ const pub = fs.readFileSync(PUBLIC_KEY_PATH, 'utf8');
84
+ const id = deriveBuyerId(pub);
85
+ const fp = fingerprint(pub);
86
+
87
+ console.log(`Buyer ID: ${id}`);
88
+ console.log(`Fingerprint: ${fp}`);
89
+ console.log(`Public key: ${PUBLIC_KEY_PATH}`);
90
+ console.log(
91
+ `Private key: ${PRIVATE_KEY_PATH} ${fs.existsSync(PRIVATE_KEY_PATH) ? '(exists)' : '(MISSING!)'}`,
92
+ );
93
+ }
94
+
95
+ // ─── Export (backup) ───────────────────────────────────────────────────
96
+
97
+ function cmdIdentityExport(outFile) {
98
+ if (!fs.existsSync(PRIVATE_KEY_PATH)) {
99
+ error('No private key found. Run: kdna identity init');
100
+ }
101
+
102
+ const outPath = path.resolve(outFile || 'kdna-identity-backup.age');
103
+ const privateKey = fs.readFileSync(PRIVATE_KEY_PATH, 'utf8');
104
+
105
+ // Simple passphrase-encrypted backup using built-in crypto
106
+ // Uses AES-256-CBC with PBKDF2 key derivation
107
+ const readline = require('readline');
108
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
109
+
110
+ rl.question('Enter passphrase (or leave empty to abort): ', (passphrase) => {
111
+ rl.close();
112
+ if (!passphrase) {
113
+ console.log('Aborted.');
114
+ process.exit(0);
115
+ }
116
+
117
+ const salt = crypto.randomBytes(16);
118
+ const key = crypto.pbkdf2Sync(passphrase, salt, 100000, 32, 'sha256');
119
+ const iv = crypto.randomBytes(16);
120
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
121
+
122
+ const header = JSON.stringify({
123
+ alg: 'pbkdf2+aes-256-cbc',
124
+ salt: salt.toString('base64'),
125
+ iv: iv.toString('base64'),
126
+ });
127
+ const headerB64 = Buffer.from(header).toString('base64');
128
+
129
+ const encrypted = Buffer.concat([cipher.update(privateKey, 'utf8'), cipher.final()]);
130
+
131
+ const output = `-----BEGIN KDNA IDENTITY BACKUP-----\n${headerB64}\n${encrypted.toString('base64')}\n-----END KDNA IDENTITY BACKUP-----\n`;
132
+
133
+ fs.writeFileSync(outPath, output, { mode: 0o600 });
134
+ console.log(`✓ Identity exported to: ${outPath}`);
135
+ console.log(` Keep this file safe. It is encrypted with your passphrase.`);
136
+ process.exit(0);
137
+ });
138
+ }
139
+
140
+ // ─── Import (restore) ──────────────────────────────────────────────────
141
+
142
+ function cmdIdentityImport(filePath) {
143
+ const abs = path.resolve(filePath);
144
+ if (!fs.existsSync(abs)) error(`File not found: ${abs}`);
145
+
146
+ const content = fs.readFileSync(abs, 'utf8');
147
+ const match = content.match(
148
+ /-----BEGIN KDNA IDENTITY BACKUP-----\n(.+?)\n(.+?)\n-----END KDNA IDENTITY BACKUP-----/s,
149
+ );
150
+ if (!match) error('Invalid backup file format.');
151
+
152
+ const header = JSON.parse(Buffer.from(match[1], 'base64').toString('utf8'));
153
+ if (header.alg !== 'pbkdf2+aes-256-cbc') error(`Unsupported algorithm: ${header.alg}`);
154
+
155
+ const readline = require('readline');
156
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
157
+
158
+ rl.question('Enter passphrase: ', (passphrase) => {
159
+ rl.close();
160
+ if (!passphrase) {
161
+ console.log('Aborted.');
162
+ process.exit(0);
163
+ }
164
+
165
+ const salt = Buffer.from(header.salt, 'base64');
166
+ const iv = Buffer.from(header.iv, 'base64');
167
+ const key = crypto.pbkdf2Sync(passphrase, salt, 100000, 32, 'sha256');
168
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
169
+
170
+ try {
171
+ const encrypted = Buffer.from(match[2], 'base64');
172
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString(
173
+ 'utf8',
174
+ );
175
+
176
+ fs.mkdirSync(IDENTITY_DIR, { recursive: true });
177
+ fs.writeFileSync(PRIVATE_KEY_PATH, decrypted, { mode: 0o600 });
178
+
179
+ // Derive public key from private
180
+ const privateKeyObj = crypto.createPrivateKey({
181
+ key: decrypted,
182
+ format: 'pem',
183
+ type: 'pkcs8',
184
+ });
185
+ const publicKey = crypto
186
+ .createPublicKey(privateKeyObj)
187
+ .export({ type: 'spki', format: 'pem' });
188
+ fs.writeFileSync(PUBLIC_KEY_PATH, publicKey, { mode: 0o644 });
189
+
190
+ const id = deriveBuyerId(publicKey);
191
+ console.log(`✓ Identity restored.`);
192
+ console.log(` Buyer ID: ${id}`);
193
+ } catch {
194
+ console.log('Error: Incorrect passphrase or corrupted backup.');
195
+ process.exit(1);
196
+ }
197
+ process.exit(0);
198
+ });
199
+ }
200
+
201
+ module.exports = {
202
+ cmdIdentityInit,
203
+ cmdIdentityShow,
204
+ cmdIdentityExport,
205
+ cmdIdentityImport,
206
+ PRIVATE_KEY_PATH,
207
+ PUBLIC_KEY_PATH,
208
+ IDENTITY_DIR,
209
+ fingerprint,
210
+ deriveBuyerId,
211
+ };
package/src/init.js ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * kdna init <name> — Scaffold a new KDNA domain from template.
3
+ * kdna cluster init <name> — Scaffold a new KDNA cluster from template.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * Recursively copy a directory, applying string replacements.
11
+ */
12
+ function copyRecursive(src, dest, replacements) {
13
+ const entries = fs.readdirSync(src);
14
+ for (const entry of entries) {
15
+ const srcPath = path.join(src, entry);
16
+ const destPath = path.join(dest, entry);
17
+ if (fs.statSync(srcPath).isDirectory()) {
18
+ fs.mkdirSync(destPath, { recursive: true });
19
+ copyRecursive(srcPath, destPath, replacements);
20
+ } else {
21
+ let content = fs.readFileSync(srcPath, 'utf8');
22
+ for (const [pattern, to] of Object.entries(replacements)) {
23
+ content = content.replaceAll(pattern, to);
24
+ }
25
+ fs.writeFileSync(destPath, content);
26
+ }
27
+ }
28
+ }
29
+
30
+ function cmdInit(name) {
31
+ if (!name) {
32
+ console.error('Error: Domain name required. Usage: kdna init <name>');
33
+ process.exit(1);
34
+ }
35
+
36
+ if (!/^[a-z][a-z0-9_]*$/.test(name)) {
37
+ console.error(
38
+ `Error: Invalid domain name "${name}". Must be lowercase letters, numbers, underscores. Start with a letter.`,
39
+ );
40
+ process.exit(1);
41
+ }
42
+
43
+ const templateDir = path.resolve(__dirname, '..', 'templates', 'minimal-domain');
44
+ const targetDir = path.resolve(name);
45
+
46
+ if (fs.existsSync(targetDir)) {
47
+ console.error(`Error: Directory already exists: ${targetDir}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ const today = new Date().toISOString().slice(0, 10);
52
+
53
+ fs.mkdirSync(targetDir, { recursive: true });
54
+ copyRecursive(templateDir, targetDir, {
55
+ example_domain: name,
56
+ 'YYYY-MM-DD': today,
57
+ });
58
+
59
+ console.log(`✓ Created KDNA domain: ${targetDir}/`);
60
+ console.log(` Files: KDNA_Core.json, KDNA_Patterns.json, kdna.json, tests/before-after.json`);
61
+
62
+ // Run structural validation (lint + schema only). Content quality checks
63
+ // are for publish time, not scaffold time — the template contains placeholders
64
+ // marked [TODO] that the author is expected to replace.
65
+ try {
66
+ const { execSync } = require('child_process');
67
+ const cli = process.argv[1];
68
+ const validCmd = `node "${cli}" validate "${targetDir}"`;
69
+ const { status } = execSync(validCmd, { stdio: 'pipe', encoding: 'utf8' });
70
+ // Structural validation passed (non-zero exit means purely structural failure)
71
+ if (status === null || status === undefined) {
72
+ console.log(` ✓ Structural validation passed (lint + schema OK)`);
73
+ }
74
+ } catch (e) {
75
+ console.error(` ⚠ Structural validation had issues:`);
76
+ const stderr = e.stderr?.toString() || e.stdout?.toString() || '';
77
+ if (stderr)
78
+ console.error(
79
+ stderr
80
+ .trim()
81
+ .split('\n')
82
+ .map((l) => ` ${l}`)
83
+ .join('\n'),
84
+ );
85
+ }
86
+
87
+ console.log('');
88
+ console.log(`Next steps:`);
89
+ console.log(` 1. Edit ${targetDir}/KDNA_Core.json — replace all [TODO] placeholders`);
90
+ console.log(
91
+ ` 2. Edit ${targetDir}/KDNA_Patterns.json — replace terminology and misunderstandings`,
92
+ );
93
+ console.log(` 3. Edit ${targetDir}/kdna.json — set author, description, repo`);
94
+ console.log(` 4. Run: kdna validate ${name} (structural check)`);
95
+ console.log(` 5. Run: kdna publish --check ${name} (content quality gate)`);
96
+ console.log(` 6. Run: kdna verify ${name} (full judgment scoring)`);
97
+ }
98
+
99
+ /**
100
+ * kdna cluster init <name> — Scaffold a new KDNA cluster from template.
101
+ */
102
+ function cmdClusterInit(name) {
103
+ if (!name) {
104
+ console.error('Error: Cluster name required. Usage: kdna cluster init <name>');
105
+ process.exit(1);
106
+ }
107
+
108
+ if (!/^[a-z][a-z0-9_]*$/.test(name)) {
109
+ console.error(
110
+ `Error: Invalid cluster name "${name}". Must be lowercase letters, numbers, underscores. Start with a letter.`,
111
+ );
112
+ process.exit(1);
113
+ }
114
+
115
+ const clusterTemplateDir = path.resolve(__dirname, '..', 'templates', 'cluster');
116
+ const domainTemplateDir = path.resolve(__dirname, '..', 'templates', 'minimal-domain');
117
+ const targetDir = path.resolve(name);
118
+
119
+ if (fs.existsSync(targetDir)) {
120
+ console.error(`Error: Directory already exists: ${targetDir}`);
121
+ process.exit(1);
122
+ }
123
+
124
+ const today = new Date().toISOString().slice(0, 10);
125
+
126
+ // Copy cluster manifest with replacements
127
+ fs.mkdirSync(targetDir, { recursive: true });
128
+
129
+ let clusterContent = fs.readFileSync(path.join(clusterTemplateDir, 'KDNA_Cluster.json'), 'utf8');
130
+ clusterContent = clusterContent.replace(/example_cluster/g, name);
131
+ clusterContent = clusterContent.replace(
132
+ /sub_domain_/g,
133
+ `${name.replace(/_cluster$/, '')}_domain_`,
134
+ );
135
+ fs.writeFileSync(path.join(targetDir, 'KDNA_Cluster.json'), clusterContent);
136
+
137
+ // Copy cluster README
138
+ const clusterReadme = fs.readFileSync(path.join(clusterTemplateDir, 'README.md'), 'utf8');
139
+ fs.writeFileSync(
140
+ path.join(targetDir, 'README.md'),
141
+ clusterReadme.replace(/example_cluster/g, name),
142
+ );
143
+
144
+ // Create first example sub-domain from domain template
145
+ const subDir = path.join(targetDir, 'domain_one');
146
+ fs.mkdirSync(subDir, { recursive: true });
147
+ copyRecursive(domainTemplateDir, subDir, {
148
+ example_domain: `${name.replace(/_cluster$/, '')}_domain_one`,
149
+ 'YYYY-MM-DD': today,
150
+ });
151
+
152
+ console.log(`✅ Created KDNA cluster: ${targetDir}/`);
153
+ console.log(` Files: KDNA_Cluster.json, domain_one/ (6 KDNA files + kdna.json + tests/)`);
154
+ console.log('');
155
+ console.log(`Next steps:`);
156
+ console.log(
157
+ ` 1. Edit ${targetDir}/KDNA_Cluster.json — set packages, composition rules, routing`,
158
+ );
159
+ console.log(
160
+ ` 2. Edit ${targetDir}/domain_one/KDNA_Core.json — fill in axioms, concepts, stances`,
161
+ );
162
+ console.log(
163
+ ` 3. Add more sub-domains: cp -r ${targetDir}/domain_one ${targetDir}/your_new_domain`,
164
+ );
165
+ console.log(` 4. Run: kdna validate ${name} (check all sub-domains)`);
166
+ }
167
+
168
+ module.exports = { cmdInit, cmdClusterInit };