@aikdna/kdna-cli 0.9.0 → 0.11.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/src/install.js CHANGED
@@ -19,6 +19,7 @@ const path = require('path');
19
19
  const crypto = require('crypto');
20
20
  const { execSync, execFileSync } = require('child_process');
21
21
  const { RegistryResolver, parseName } = require('./registry');
22
+ const { EXIT, error } = require('./cmds/_common');
22
23
 
23
24
  const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
24
25
  const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
@@ -139,11 +140,6 @@ function ensureLoaderSkill() {
139
140
  }
140
141
  }
141
142
 
142
- function error(msg) {
143
- console.error(`Error: ${msg}`);
144
- process.exit(1);
145
- }
146
-
147
143
  function ensureDir(dir) {
148
144
  fs.mkdirSync(dir, { recursive: true });
149
145
  }
@@ -296,7 +292,7 @@ function verifySignature({ destDir, scope, entry, lenient = true }) {
296
292
  console.warn(' ⚠ No kdna.json — cannot verify signature.');
297
293
  return;
298
294
  }
299
- error('No kdna.json in package — cannot verify signature.');
295
+ error('No kdna.json in package — cannot verify signature.', EXIT.TRUST_FAILED);
300
296
  }
301
297
 
302
298
  const trustKey = scope.trust_pubkey;
@@ -318,7 +314,7 @@ function verifySignature({ destDir, scope, entry, lenient = true }) {
318
314
 
319
315
  // Author pubkey fingerprint must match scope trust_pubkey
320
316
  if (manifest.author?.pubkey !== trustKey) {
321
- error(`${entry.name}: author.pubkey does not match scope trust key. Refusing to install.`);
317
+ error(`${entry.name}: author.pubkey does not match scope trust key. Refusing to install.`, EXIT.TRUST_FAILED);
322
318
  }
323
319
 
324
320
  // Full Ed25519 verify (requires public_key_pem embedded in the package)
@@ -334,6 +330,7 @@ function verifySignature({ destDir, scope, entry, lenient = true }) {
334
330
  if (computedFingerprint !== manifest.author.pubkey) {
335
331
  error(
336
332
  `${entry.name}: embedded public_key_pem does not match author.pubkey fingerprint. Refusing.`,
333
+ EXIT.TRUST_FAILED,
337
334
  );
338
335
  }
339
336
 
@@ -369,12 +366,12 @@ function verifySignature({ destDir, scope, entry, lenient = true }) {
369
366
  const publicKey = crypto.createPublicKey(pem);
370
367
  const ok = crypto.verify(null, Buffer.from(payload), publicKey, Buffer.from(sigHex, 'hex'));
371
368
  if (!ok) {
372
- error(`${entry.name}: Ed25519 signature INVALID. Package may be tampered. Refusing.`);
369
+ error(`${entry.name}: Ed25519 signature INVALID. Package may be tampered. Refusing.`, EXIT.TRUST_FAILED);
373
370
  }
374
371
  console.log(' ✓ Signature OK (Ed25519 verified)');
375
372
  } catch (e) {
376
373
  if (e.message?.includes('INVALID')) throw e;
377
- error(`${entry.name}: signature verification failed: ${e.message}`);
374
+ error(`${entry.name}: signature verification failed: ${e.message}`, EXIT.TRUST_FAILED);
378
375
  }
379
376
  }
380
377
 
@@ -431,42 +428,45 @@ function cmdInstallExtended(input, args = []) {
431
428
  ensureLoaderSkill();
432
429
 
433
430
  const yes = args.includes('--yes');
431
+ const jsonMode = args.includes('--json');
434
432
  const source = parseSource(input);
435
433
 
436
434
  switch (source.type) {
437
435
  case 'registry':
438
- return installFromRegistry(source.parsed, yes);
436
+ return installFromRegistry(source.parsed, yes, jsonMode);
439
437
  case 'local-file':
440
- return installFromLocalFile(source.path, yes);
438
+ return installFromLocalFile(source.path, yes, jsonMode);
441
439
  case 'local-dir':
442
- return installFromLocalDir(source.path, yes);
440
+ return installFromLocalDir(source.path, yes, jsonMode);
443
441
  }
444
442
  }
445
443
 
446
- function installFromRegistry(parsed, yes) {
444
+ function installFromRegistry(parsed, yes, jsonMode = false) {
447
445
  const resolver = new RegistryResolver({ allowNetwork: true });
448
446
  let scope, entry;
449
447
  try {
450
448
  ({ scope, entry } = resolver.resolve(parsed.full));
451
449
  } catch (e) {
452
- error(e.message);
450
+ error(e.message, EXIT.REGISTRY_ERROR);
453
451
  }
454
452
 
455
453
  if (parsed.wasShort) {
456
- console.log(` Resolved "${parsed.ident}" → ${entry.name}`);
454
+ if (!jsonMode) console.log(` Resolved "${parsed.ident}" → ${entry.name}`);
457
455
  }
458
456
 
459
457
  if (entry.deprecated) {
460
- console.warn(
461
- ` ⚠ ${entry.name} is deprecated.${entry.replaced_by ? ` Use ${entry.replaced_by} instead.` : ''}`,
462
- );
458
+ if (!jsonMode) {
459
+ console.warn(
460
+ ` ⚠ ${entry.name} is deprecated.${entry.replaced_by ? ` Use ${entry.replaced_by} instead.` : ''}`,
461
+ );
462
+ }
463
463
  }
464
464
  if (entry.access && entry.access !== 'open') {
465
- error(`${entry.name} requires "${entry.access}" access. Not installable via CLI yet.`);
465
+ error(`${entry.name} requires "${entry.access}" access. Not installable via CLI yet.`, EXIT.POLICY_VIOLATION);
466
466
  }
467
467
 
468
468
  if (entry.type === 'cluster') {
469
- return installCluster(entry, resolver, yes);
469
+ return installCluster(entry, resolver, yes, jsonMode);
470
470
  }
471
471
 
472
472
  if (!entry.kdna_url) {
@@ -474,6 +474,7 @@ function installFromRegistry(parsed, yes) {
474
474
  `${entry.name}@${entry.version} has no kdna_url in registry.\n` +
475
475
  `release_status: ${entry.release_status || 'unknown'}\n` +
476
476
  `(This domain has not been published as a .kdna file yet. It will be available after v0.7 republish.)`,
477
+ EXIT.REGISTRY_ERROR,
477
478
  );
478
479
  }
479
480
 
@@ -482,20 +483,20 @@ function installFromRegistry(parsed, yes) {
482
483
  process.exit(0);
483
484
  }
484
485
 
485
- installSingleFromUrl({ entry, scope });
486
+ installSingleFromUrl({ entry, scope }, jsonMode);
486
487
  }
487
488
 
488
- function installSingleFromUrl({ entry, scope }) {
489
+ function installSingleFromUrl({ entry, scope }, jsonMode = false) {
489
490
  const [scopeName, ident] = entry.name.split('/');
490
491
  const dest = domainDir(scopeName, ident);
491
492
  const tmpFile = path.join(scopeDir(scopeName), `.${ident}-${Date.now()}.kdna.tmp`);
492
493
 
493
- console.log(` Downloading ${entry.name}@${entry.version}...`);
494
+ if (!jsonMode) console.log(` Downloading ${entry.name}@${entry.version}...`);
494
495
  ensureDir(scopeDir(scopeName));
495
496
  try {
496
497
  downloadFile(entry.kdna_url, tmpFile);
497
498
  } catch {
498
- error(`Failed to download ${entry.kdna_url}`);
499
+ error(`Failed to download ${entry.kdna_url}`, EXIT.REGISTRY_ERROR);
499
500
  }
500
501
 
501
502
  // sha256 check
@@ -508,7 +509,7 @@ function installSingleFromUrl({ entry, scope }) {
508
509
  }
509
510
  error(`sha256 mismatch for ${entry.name}: expected ${entry.sha256}, got ${actual}`);
510
511
  }
511
- console.log(` ✓ sha256 verified`);
512
+ if (!jsonMode) console.log(` ✓ sha256 verified`);
512
513
 
513
514
  // Replace existing install atomically-ish
514
515
  if (fs.existsSync(dest)) {
@@ -537,29 +538,39 @@ function installSingleFromUrl({ entry, scope }) {
537
538
  };
538
539
  fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(manifest, null, 2) + '\n');
539
540
 
540
- console.log(`✓ Installed ${entry.name}@${entry.version}`);
541
- console.log(` Location: ${dest}`);
541
+ if (jsonMode) {
542
+ console.log(JSON.stringify({
543
+ name: entry.name,
544
+ version: entry.version,
545
+ installed: true,
546
+ path: dest,
547
+ type: entry.type || 'domain',
548
+ }));
549
+ } else {
550
+ console.log(`✓ Installed ${entry.name}@${entry.version}`);
551
+ console.log(` Location: ${dest}`);
552
+ }
542
553
  }
543
554
 
544
- function installCluster(clusterEntry, resolver, _yes) {
555
+ function installCluster(clusterEntry, resolver, _yes, jsonMode = false) {
545
556
  const subdomains = clusterEntry.cluster?.domains || [];
546
557
  if (!subdomains.length) {
547
558
  error(`Cluster ${clusterEntry.name} has no sub-domains listed.`);
548
559
  }
549
560
 
550
- console.log(`Cluster ${clusterEntry.name} → ${subdomains.length} sub-domains`);
561
+ if (!jsonMode) console.log(`Cluster ${clusterEntry.name} → ${subdomains.length} sub-domains`);
551
562
 
552
563
  for (const sub of subdomains) {
553
564
  try {
554
565
  const resolved = resolver.resolve(sub);
555
566
  if (!resolved.entry.kdna_url) {
556
- console.warn(` ⚠ ${sub}: no kdna_url (skipping)`);
567
+ if (!jsonMode) console.warn(` ⚠ ${sub}: no kdna_url (skipping)`);
557
568
  continue;
558
569
  }
559
- console.log('');
560
- installSingleFromUrl({ entry: resolved.entry, scope: resolved.scope });
570
+ if (!jsonMode) console.log('');
571
+ installSingleFromUrl({ entry: resolved.entry, scope: resolved.scope }, jsonMode);
561
572
  } catch (e) {
562
- console.warn(` ⚠ ${sub}: ${e.message.split('\n')[0]}`);
573
+ if (!jsonMode) console.warn(` ⚠ ${sub}: ${e.message.split('\n')[0]}`);
563
574
  }
564
575
  }
565
576
 
@@ -582,11 +593,23 @@ function installCluster(clusterEntry, resolver, _yes) {
582
593
  2,
583
594
  ) + '\n',
584
595
  );
585
- console.log('');
586
- console.log(`✓ Cluster ${clusterEntry.name} installed`);
596
+
597
+ if (jsonMode) {
598
+ console.log(JSON.stringify({
599
+ name: clusterEntry.name,
600
+ version: clusterEntry.version,
601
+ type: 'cluster',
602
+ installed: true,
603
+ path: clusterDest,
604
+ subdomains: subdomains.length,
605
+ }));
606
+ } else {
607
+ console.log('');
608
+ console.log(`✓ Cluster ${clusterEntry.name} installed`);
609
+ }
587
610
  }
588
611
 
589
- function installFromLocalFile(filePath, _yes) {
612
+ function installFromLocalFile(filePath, _yes, jsonMode = false) {
590
613
  const abs = path.resolve(filePath);
591
614
  if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) error(`Not a file: ${abs}`);
592
615
 
@@ -617,11 +640,21 @@ function installFromLocalFile(filePath, _yes) {
617
640
  };
618
641
  fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(destManifest, null, 2) + '\n');
619
642
 
620
- console.log(`✓ Installed ${declared} from local file`);
621
- console.log(` Location: ${dest}`);
643
+ if (jsonMode) {
644
+ console.log(JSON.stringify({
645
+ name: declared,
646
+ installed: true,
647
+ path: dest,
648
+ source: 'local-file',
649
+ source_path: abs,
650
+ }));
651
+ } else {
652
+ console.log(`✓ Installed ${declared} from local file`);
653
+ console.log(` Location: ${dest}`);
654
+ }
622
655
  }
623
656
 
624
- function installFromLocalDir(dirPath, _yes) {
657
+ function installFromLocalDir(dirPath, _yes, jsonMode = false) {
625
658
  const abs = path.resolve(dirPath);
626
659
  const manifest = readJson(path.join(abs, 'kdna.json'));
627
660
  const declared = manifest?.name;
@@ -642,8 +675,18 @@ function installFromLocalDir(dirPath, _yes) {
642
675
  };
643
676
  fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(destManifest, null, 2) + '\n');
644
677
 
645
- console.log(`✓ Installed ${declared} from local directory (dev mode)`);
646
- console.log(` Location: ${dest}`);
678
+ if (jsonMode) {
679
+ console.log(JSON.stringify({
680
+ name: declared,
681
+ installed: true,
682
+ path: dest,
683
+ source: 'local-dir',
684
+ source_path: abs,
685
+ }));
686
+ } else {
687
+ console.log(`✓ Installed ${declared} from local directory (dev mode)`);
688
+ console.log(` Location: ${dest}`);
689
+ }
647
690
  }
648
691
 
649
692
  // ─── Remove ─────────────────────────────────────────────────────────────
@@ -663,18 +706,102 @@ function cmdRemove(input) {
663
706
 
664
707
  // ─── Info ───────────────────────────────────────────────────────────────
665
708
 
666
- function cmdInfo(input) {
709
+ function cmdInfo(input, jsonMode = false) {
667
710
  warnLegacy();
668
711
  const parsed = parseName(input);
669
- if (!parsed) error(`Invalid name "${input}".`);
712
+ if (!parsed) error(`Invalid name "${input}".`, EXIT.INPUT_ERROR);
670
713
  const dest = domainDir(parsed.scope, parsed.ident);
671
- if (!fs.existsSync(dest)) error(`${parsed.full} is not installed.`);
714
+ if (!fs.existsSync(dest)) error(`${parsed.full} is not installed.`, EXIT.INPUT_ERROR);
672
715
 
673
716
  const manifest = readJson(path.join(dest, 'kdna.json'));
674
717
  const core = readJson(path.join(dest, 'KDNA_Core.json'));
675
718
  const pat = readJson(path.join(dest, 'KDNA_Patterns.json'));
676
719
  const source = manifest?._source || {};
677
720
 
721
+ // ─── Judgment surface (computed for both modes) ────────────────────
722
+ const axiomCount = (core?.axioms || []).length;
723
+ const ontologyCount = (core?.ontology || []).length;
724
+ const stanceCount = (core?.stances || []).length;
725
+ const misCount = (pat?.misunderstandings || []).length;
726
+ const selfCheckCount = (pat?.self_check || []).length;
727
+
728
+ // ─── v2.1 governance score ─────────────────────────────────────────
729
+ let governance = null;
730
+ if (axiomCount > 0) {
731
+ const withApplies = (core?.axioms || []).filter(
732
+ (a) => Array.isArray(a.applies_when) && a.applies_when.length,
733
+ ).length;
734
+ const withDoesNotApply = (core?.axioms || []).filter(
735
+ (a) => Array.isArray(a.does_not_apply_when) && a.does_not_apply_when.length,
736
+ ).length;
737
+ const withFailureRisk = (core?.axioms || []).filter((a) => a.failure_risk).length;
738
+ const pct = Math.round(
739
+ ((withApplies + withDoesNotApply + withFailureRisk) / (axiomCount * 3)) * 100,
740
+ );
741
+ governance = { withApplies, withDoesNotApply, withFailureRisk, coverage: pct };
742
+ }
743
+
744
+ // ─── Eval cases ────────────────────────────────────────────────────
745
+ const evalDir = path.join(dest, 'evals');
746
+ let evalInfo = null;
747
+ if (fs.existsSync(evalDir)) {
748
+ const evalFiles = fs.readdirSync(evalDir).filter((f) => f.endsWith('.json'));
749
+ let totalCases = 0;
750
+ for (const f of evalFiles) {
751
+ const data = readJson(path.join(evalDir, f));
752
+ if (data?.cases) totalCases += data.cases.length;
753
+ }
754
+ evalInfo = { files: evalFiles.length, totalCases };
755
+ }
756
+
757
+ // ─── Known risks ───────────────────────────────────────────────────
758
+ const risks = [];
759
+ if (core?.axioms) {
760
+ for (const a of core.axioms) {
761
+ if (a.failure_risk) risks.push({ source: a.id, text: a.failure_risk });
762
+ }
763
+ }
764
+
765
+ // ─── Files ─────────────────────────────────────────────────────────
766
+ const expected = [
767
+ 'KDNA_Core.json',
768
+ 'KDNA_Patterns.json',
769
+ 'KDNA_Scenarios.json',
770
+ 'KDNA_Cases.json',
771
+ 'KDNA_Reasoning.json',
772
+ 'KDNA_Evolution.json',
773
+ ];
774
+ const present = expected.filter((f) => fs.existsSync(path.join(dest, f)));
775
+
776
+ // ─── JSON mode: emit structured output only, then exit ─────────────
777
+ if (jsonMode) {
778
+ const result = {
779
+ name: parsed.full,
780
+ version: manifest?.version || core?.meta?.version || '?',
781
+ judgment_version: manifest?.judgment_version || null,
782
+ status: manifest?.status || '?',
783
+ license: manifest?.license?.type || '?',
784
+ author: manifest?.author?.name || '?',
785
+ pubkey: manifest?.author?.pubkey || null,
786
+ has_pem: !!manifest?.author?.public_key_pem,
787
+ source_url: source.kdna_url || null,
788
+ sha256: source.sha256 || null,
789
+ installed_at: source.installed_at || null,
790
+ path: dest,
791
+ axioms: axiomCount,
792
+ ontology: ontologyCount,
793
+ stances: stanceCount,
794
+ misunderstandings: misCount,
795
+ self_checks: selfCheckCount,
796
+ governance,
797
+ evals: evalInfo,
798
+ risks: risks.slice(0, 10),
799
+ files: { present: present.length, total: expected.length, list: present },
800
+ };
801
+ console.log(JSON.stringify(result));
802
+ process.exit(EXIT.OK);
803
+ }
804
+
678
805
  // ─── Header ─────────────────────────────────────────────────────
679
806
  console.log('═'.repeat(64));
680
807
  console.log(` ${parsed.full}`);
@@ -706,11 +833,6 @@ function cmdInfo(input) {
706
833
  // ─── Judgment surface ──────────────────────────────────────────
707
834
  console.log('');
708
835
  console.log(' ── Judgment surface ──');
709
- const axiomCount = (core?.axioms || []).length;
710
- const ontologyCount = (core?.ontology || []).length;
711
- const stanceCount = (core?.stances || []).length;
712
- const misCount = (pat?.misunderstandings || []).length;
713
- const selfCheckCount = (pat?.self_check || []).length;
714
836
  console.log(` Axioms: ${axiomCount}`);
715
837
  console.log(` Ontology: ${ontologyCount}`);
716
838
  console.log(` Stances: ${stanceCount}`);
@@ -718,47 +840,24 @@ function cmdInfo(input) {
718
840
  console.log(` Self-checks: ${selfCheckCount}`);
719
841
 
720
842
  // ─── v2.1 governance score ─────────────────────────────────────
721
- if (axiomCount > 0) {
722
- const withApplies = (core?.axioms || []).filter(
723
- (a) => Array.isArray(a.applies_when) && a.applies_when.length,
724
- ).length;
725
- const withDoesNotApply = (core?.axioms || []).filter(
726
- (a) => Array.isArray(a.does_not_apply_when) && a.does_not_apply_when.length,
727
- ).length;
728
- const withFailureRisk = (core?.axioms || []).filter((a) => a.failure_risk).length;
729
- const pct = Math.round(
730
- ((withApplies + withDoesNotApply + withFailureRisk) / (axiomCount * 3)) * 100,
731
- );
843
+ if (governance) {
732
844
  console.log('');
733
845
  console.log(' ── v2.1 governance ──');
734
- console.log(` axioms with applies_when: ${withApplies}/${axiomCount}`);
735
- console.log(` axioms with does_not_apply: ${withDoesNotApply}/${axiomCount}`);
736
- console.log(` axioms with failure_risk: ${withFailureRisk}/${axiomCount}`);
737
- console.log(` governance coverage: ${pct}%`);
846
+ console.log(` axioms with applies_when: ${governance.withApplies}/${axiomCount}`);
847
+ console.log(` axioms with does_not_apply: ${governance.withDoesNotApply}/${axiomCount}`);
848
+ console.log(` axioms with failure_risk: ${governance.withFailureRisk}/${axiomCount}`);
849
+ console.log(` governance coverage: ${governance.coverage}%`);
738
850
  }
739
851
 
740
852
  // ─── Eval cases ────────────────────────────────────────────────
741
- const evalDir = path.join(dest, 'evals');
742
- if (fs.existsSync(evalDir)) {
743
- const evalFiles = fs.readdirSync(evalDir).filter((f) => f.endsWith('.json'));
744
- let totalCases = 0;
745
- for (const f of evalFiles) {
746
- const data = readJson(path.join(evalDir, f));
747
- if (data?.cases) totalCases += data.cases.length;
748
- }
853
+ if (evalInfo) {
749
854
  console.log('');
750
855
  console.log(' ── Eval cases ──');
751
- console.log(` Files: ${evalFiles.length}`);
752
- console.log(` Total cases: ${totalCases}`);
856
+ console.log(` Files: ${evalInfo.files}`);
857
+ console.log(` Total cases: ${evalInfo.totalCases}`);
753
858
  }
754
859
 
755
- // ─── Known risks (from kdna.json or axioms) ────────────────────
756
- const risks = [];
757
- if (core?.axioms) {
758
- for (const a of core.axioms) {
759
- if (a.failure_risk) risks.push({ source: a.id, text: a.failure_risk });
760
- }
761
- }
860
+ // ─── Known risks ───────────────────────────────────────────────
762
861
  if (risks.length) {
763
862
  console.log('');
764
863
  console.log(' ── Known failure risks ──');
@@ -771,15 +870,6 @@ function cmdInfo(input) {
771
870
  }
772
871
 
773
872
  // ─── Files ─────────────────────────────────────────────────────
774
- const expected = [
775
- 'KDNA_Core.json',
776
- 'KDNA_Patterns.json',
777
- 'KDNA_Scenarios.json',
778
- 'KDNA_Cases.json',
779
- 'KDNA_Reasoning.json',
780
- 'KDNA_Evolution.json',
781
- ];
782
- const present = expected.filter((f) => fs.existsSync(path.join(dest, f)));
783
873
  console.log('');
784
874
  console.log(` Files: ${present.length}/${expected.length} (${present.join(', ') || 'none'})`);
785
875
 
@@ -806,7 +896,7 @@ function cmdUpdate(input) {
806
896
  try {
807
897
  ({ entry } = resolver.resolve(parsed.full));
808
898
  } catch (e) {
809
- error(e.message);
899
+ error(e.message, EXIT.REGISTRY_ERROR);
810
900
  }
811
901
 
812
902
  if (entry.version === installedVersion) {
package/src/publish.js CHANGED
@@ -7,10 +7,11 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
+ const { EXIT } = require('./cmds/_common');
10
11
 
11
- function error(msg) {
12
+ function error(msg, code = EXIT.VALIDATION_FAILED) {
12
13
  console.error(`Error: ${msg}`);
13
- process.exit(1);
14
+ process.exit(code);
14
15
  }
15
16
 
16
17
  function readJson(filePath) {
@@ -373,7 +374,7 @@ function cmdPublishCheck(domainPath) {
373
374
  }
374
375
  console.log('═'.repeat(60));
375
376
 
376
- if (errors > 0) process.exit(1);
377
+ if (errors > 0) process.exit(EXIT.POLICY_VIOLATION);
377
378
  }
378
379
 
379
380
  // ═══════════════════════════════════════════════════════════════════════
package/src/search.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const { RegistryResolver } = require('./registry');
9
+ const { EXIT } = require('./cmds/_common');
9
10
 
10
11
  function matchScore(d, q) {
11
12
  const ql = q.toLowerCase();
@@ -23,17 +24,25 @@ function matchScore(d, q) {
23
24
  return score;
24
25
  }
25
26
 
26
- function cmdSearch(query) {
27
+ function cmdSearch(query, json) {
27
28
  if (!query) {
29
+ if (json) {
30
+ console.log(JSON.stringify({ error: 'Usage: kdna search <keyword>' }));
31
+ process.exit(EXIT.INPUT_ERROR);
32
+ }
28
33
  console.error('Usage: kdna search <keyword>');
29
34
  console.error(' kdna search "content strategy"');
30
- process.exit(1);
35
+ process.exit(EXIT.INPUT_ERROR);
31
36
  }
32
37
 
33
38
  const resolver = new RegistryResolver({ allowNetwork: true });
34
39
  const domains = resolver.listAllDomains() || [];
35
40
 
36
41
  if (!domains.length) {
42
+ if (json) {
43
+ console.log(JSON.stringify([]));
44
+ process.exit(EXIT.OK);
45
+ }
37
46
  console.log('No registry entries found. Run: kdna registry refresh');
38
47
  return;
39
48
  }
@@ -44,6 +53,10 @@ function cmdSearch(query) {
44
53
  .sort((a, b) => b.score - a.score);
45
54
 
46
55
  if (!matches.length) {
56
+ if (json) {
57
+ console.log(JSON.stringify([]));
58
+ process.exit(EXIT.OK);
59
+ }
47
60
  console.log(`No domains match "${query}".`);
48
61
  console.log('');
49
62
  console.log('Try:');
@@ -51,6 +64,24 @@ function cmdSearch(query) {
51
64
  return;
52
65
  }
53
66
 
67
+ if (json) {
68
+ const result = matches.map(({ d, score }) => ({
69
+ name: d.name || d.id || null,
70
+ version: d.version || null,
71
+ type: d.type || 'domain',
72
+ description: d.description || null,
73
+ core_insight: d.core_insight || null,
74
+ keywords: d.keywords || [],
75
+ domain_field: d.domain_field || [],
76
+ judgment_patterns: d.judgment_patterns || [],
77
+ yanked: d.yanked || false,
78
+ deprecated: d.deprecated || false,
79
+ score,
80
+ }));
81
+ console.log(JSON.stringify(result));
82
+ process.exit(EXIT.OK);
83
+ }
84
+
54
85
  console.log(`Found ${matches.length} matching domain(s) for "${query}":`);
55
86
  console.log('');
56
87