@cynodia/axiom-cli 0.16.0-alpha.2 → 0.16.0-alpha.4

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 (3) hide show
  1. package/README.md +26 -4
  2. package/dist/index.js +69 -13
  3. package/package.json +7 -6
package/README.md CHANGED
@@ -65,10 +65,32 @@ axiom --help
65
65
 
66
66
  ## Machine-readable output
67
67
 
68
- Pass `--json` on `explain`, `analyze` or `diff` for structured output — parseable,
69
- deterministic, and semantically identical to the corresponding `AgentAPI` result (no human
70
- terminal decoration reaches `--json` mode). Everything else prints a concise human-readable
71
- rendering.
68
+ Pass `--json` on `explain`, `analyze`, `diff` or `validate` for structured output —
69
+ parseable, deterministic, and (for `explain`/`analyze`/`diff`) semantically identical to
70
+ the corresponding `AgentAPI` result (no human terminal decoration reaches `--json` mode).
71
+ Everything else prints a concise human-readable rendering.
72
+
73
+ **`--json` also governs failure, not only success.** Every CLI-owned error path — an
74
+ unknown node id, a missing required flag, a model file that will not load, an unparseable
75
+ command line — emits one JSON value on stdout instead of prose, and never a native stack
76
+ trace:
77
+
78
+ ```json
79
+ {
80
+ "ok": false,
81
+ "error": {
82
+ "code": "UNKNOWN_NODE",
83
+ "message": "No action node \"foo\" in this graph"
84
+ }
85
+ }
86
+ ```
87
+
88
+ `error.code` is one of `INVALID_ARGUMENTS`, `UNKNOWN_COMMAND`, `UNKNOWN_NODE`,
89
+ `MISSING_ARGUMENT`, `MODEL_LOAD_FAILED` or `COMMAND_FAILED` (an uncategorized failure).
90
+ Match on `code`, never on `message` — the same discipline as every other structured
91
+ diagnostic in Axiom. Exit code is always nonzero on failure, in `--json` mode and out of
92
+ it. Without `--json`, error text is unchanged prose on stderr (or, for a handful of
93
+ pre-existing "not found"-style results, stdout — see the source), exactly as before.
72
94
 
73
95
  ## Exit codes
74
96
 
package/dist/index.js CHANGED
@@ -13,6 +13,30 @@ const SUBCOMMANDS = {
13
13
  schema: new Set(['status', 'diff']),
14
14
  migrate: new Set(['plan', 'status']),
15
15
  };
16
+ /** Thrown for a CLI-level failure that must carry one of the codes above under `--json`. */
17
+ class CliError extends Error {
18
+ code;
19
+ constructor(code, message) {
20
+ super(message);
21
+ this.code = code;
22
+ }
23
+ }
24
+ /**
25
+ * The one shape every CLI-owned failure takes under `--json` (spec16pt4 §4): structured,
26
+ * deterministic, machine-readable, and — unlike a bare thrown error reaching the top —
27
+ * carries no native stack trace. Printed to stdout, like every other `--json` result, so a
28
+ * caller parsing stdout as JSON never has to distinguish success from failure by channel.
29
+ * Human mode (`options.json` false) is untouched (spec16pt4 §6).
30
+ */
31
+ function fail(json, code, message) {
32
+ if (json) {
33
+ console.log(JSON.stringify({ ok: false, error: { code, message } }, null, 2));
34
+ }
35
+ else {
36
+ console.error(message);
37
+ }
38
+ process.exitCode = 1;
39
+ }
16
40
  function parseArguments(argv) {
17
41
  const positional = [];
18
42
  let exportName;
@@ -110,7 +134,13 @@ async function loadGraph(options) {
110
134
  async function loadGraphModule(modelFile, exportName) {
111
135
  const options = { modelFile, exportName };
112
136
  const resolved = path.resolve(process.cwd(), options.modelFile);
113
- const module = (await import(pathToFileURL(resolved).href));
137
+ let module;
138
+ try {
139
+ module = (await import(pathToFileURL(resolved).href));
140
+ }
141
+ catch (error) {
142
+ throw new CliError('MODEL_LOAD_FAILED', `Could not load ${options.modelFile}: ${error instanceof Error ? error.message : String(error)}`);
143
+ }
114
144
  const names = options.exportName
115
145
  ? [options.exportName]
116
146
  : GRAPH_EXPORT_CANDIDATES.filter((name) => name in module);
@@ -131,12 +161,12 @@ async function loadGraphModule(modelFile, exportName) {
131
161
  }
132
162
  }
133
163
  if (builders.length > 1) {
134
- throw new Error(`${options.modelFile} exports several candidates. Choose one with --export=<name>: ${builders
164
+ throw new CliError('MODEL_LOAD_FAILED', `${options.modelFile} exports several candidates. Choose one with --export=<name>: ${builders
135
165
  .map(([name]) => name)
136
166
  .join(', ')}`);
137
167
  }
138
168
  }
139
- throw new Error(`Could not load an application graph from ${options.modelFile}`);
169
+ throw new CliError('MODEL_LOAD_FAILED', `Could not load an application graph from ${options.modelFile}`);
140
170
  }
141
171
  const SECTIONS = [
142
172
  ['Entities', 'entity'],
@@ -484,7 +514,7 @@ function formatExplanation(kind, result) {
484
514
  }
485
515
  async function explainCommand(options) {
486
516
  if (!options.kind || !options.targetId) {
487
- throw new Error('usage: axiom explain <action|query|workflow|state> <id> <modelFile>');
517
+ throw new CliError('INVALID_ARGUMENTS', 'usage: axiom explain <action|query|workflow|state> <id> <modelFile>');
488
518
  }
489
519
  const agent = new AgentAPI(await loadGraph(options));
490
520
  let result;
@@ -502,11 +532,15 @@ async function explainCommand(options) {
502
532
  result = agent.explainState(options.targetId);
503
533
  break;
504
534
  default:
505
- throw new Error(`explain: unknown kind "${options.kind}" (expected action, query, workflow or state)`);
535
+ throw new CliError('INVALID_ARGUMENTS', `explain: unknown kind "${options.kind}" (expected action, query, workflow or state)`);
506
536
  }
507
537
  if (!result) {
538
+ const message = `No ${options.kind} node "${options.targetId}" in this graph`;
539
+ if (options.json) {
540
+ throw new CliError('UNKNOWN_NODE', message);
541
+ }
508
542
  process.exitCode = 1;
509
- return `No ${options.kind} node "${options.targetId}" in this graph`;
543
+ return message;
510
544
  }
511
545
  return options.json ? JSON.stringify(result, null, 2) : formatExplanation(options.kind, result);
512
546
  }
@@ -530,7 +564,7 @@ async function analyzeCommand(options) {
530
564
  }
531
565
  async function diffCommand(options) {
532
566
  if (!options.against) {
533
- throw new Error('diff needs a --against=<file> naming the graph to compare against');
567
+ throw new CliError('MISSING_ARGUMENT', 'diff needs a --against=<file> naming the graph to compare against');
534
568
  }
535
569
  const before = await loadGraphModule(options.against, options.againstExport);
536
570
  const after = await loadGraph(options);
@@ -578,16 +612,34 @@ const USAGE = [
578
612
  ].join('\n');
579
613
  async function main() {
580
614
  const argv = process.argv.slice(2);
615
+ // `--json` may be present even when parsing fails outright (a missing model file, an
616
+ // unparseable command line), so it is read directly from argv rather than from the
617
+ // `Options` parseArguments would otherwise have produced (spec16pt4 §3, "missing model
618
+ // file" / "invalid/missing command arguments").
619
+ const wantsJson = argv.includes('--json');
581
620
  if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) {
582
621
  console.log(USAGE);
583
622
  return;
584
623
  }
585
624
  const options = parseArguments(argv);
586
625
  if (!options) {
587
- console.error(USAGE);
588
- process.exitCode = 1;
626
+ if (wantsJson) {
627
+ fail(true, 'INVALID_ARGUMENTS', 'Missing or invalid command-line arguments. Run "axiom --help" for usage.');
628
+ }
629
+ else {
630
+ console.error(USAGE);
631
+ process.exitCode = 1;
632
+ }
589
633
  return;
590
634
  }
635
+ try {
636
+ await runCommand(options);
637
+ }
638
+ catch (error) {
639
+ fail(options.json === true, error instanceof CliError ? error.code : 'COMMAND_FAILED', error instanceof Error ? error.message : String(error));
640
+ }
641
+ }
642
+ async function runCommand(options) {
591
643
  switch (options.command) {
592
644
  case 'schema status':
593
645
  console.log(schemaStatus(await loadGraph(options)));
@@ -632,11 +684,15 @@ async function main() {
632
684
  console.log(await diffCommand(options));
633
685
  return;
634
686
  default:
635
- console.error(`Unknown command: ${options.command}`);
636
- process.exitCode = 1;
687
+ if (options.json) {
688
+ fail(true, 'UNKNOWN_COMMAND', `Unknown command: ${options.command}`);
689
+ }
690
+ else {
691
+ console.error(`Unknown command: ${options.command}`);
692
+ process.exitCode = 1;
693
+ }
637
694
  }
638
695
  }
639
696
  main().catch((error) => {
640
- console.error(error instanceof Error ? error.message : String(error));
641
- process.exitCode = 1;
697
+ fail(process.argv.slice(2).includes('--json'), error instanceof CliError ? error.code : 'COMMAND_FAILED', error instanceof Error ? error.message : String(error));
642
698
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-cli",
3
- "version": "0.16.0-alpha.2",
3
+ "version": "0.16.0-alpha.4",
4
4
  "description": "Command line tools for inspecting, validating and serving Axiom applications.",
5
5
  "license": "MIT",
6
6
  "author": "AskTech AS",
@@ -34,12 +34,13 @@
34
34
  "axiom": "./dist/index.js"
35
35
  },
36
36
  "dependencies": {
37
- "@cynodia/axiom-core": "0.16.0-alpha.2",
38
- "@cynodia/axiom-compiler": "0.16.0-alpha.2",
39
- "@cynodia/axiom-server": "0.16.0-alpha.2",
40
- "@cynodia/axiom-agent-api": "0.16.0-alpha.2"
37
+ "@cynodia/axiom-core": "0.16.0-alpha.4",
38
+ "@cynodia/axiom-compiler": "0.16.0-alpha.4",
39
+ "@cynodia/axiom-server": "0.16.0-alpha.4",
40
+ "@cynodia/axiom-agent-api": "0.16.0-alpha.4"
41
41
  },
42
42
  "scripts": {
43
- "build": "tsc -b tsconfig.json"
43
+ "build": "tsc -b tsconfig.json tsconfig.test.json",
44
+ "test": "node --test dist-test/**/*.test.js"
44
45
  }
45
46
  }