@cynodia/axiom-cli 0.16.0-alpha.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AskTech AS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Axiom CLI
2
+
3
+ Part of [Axiom](https://github.com/cynodia/axiom), an AI-native semantic web application
4
+ framework.
5
+
6
+ **Status: experimental / alpha.** The API may change between alpha releases.
7
+
8
+ **AI agents:** read `docs/AGENT_REFERENCE.md` and `docs/AGENT_API.md` inside the installed
9
+ `@cynodia/axiom` package before authoring or inspecting an application. This tool is a thin
10
+ renderer over that package's `AgentAPI` — it implements no semantic analysis of its own
11
+ (spec16pt2 §53).
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install --global @cynodia/axiom-cli
17
+ # or, without installing:
18
+ npx @cynodia/axiom-cli --help
19
+ ```
20
+
21
+ This installs the `axiom` executable. Every command takes a **compiled** (built) JavaScript
22
+ module — `<modelFile>` — that exports an `ApplicationGraph`, or a function that builds one
23
+ (`export function createGraph() { … }`, `export default …`); pass `--export=<name>` when a
24
+ module exports more than one candidate.
25
+
26
+ ## Commands
27
+
28
+ ```text
29
+ axiom build <modelFile> [--export=name]
30
+ axiom inspect <modelFile> [--export=name]
31
+ axiom validate <modelFile> [--export=name] [--json]
32
+ axiom serve <modelFile> [--export=name] [--port=3000] [--store=state.db]
33
+
34
+ axiom schema status <modelFile> [--export=name]
35
+ axiom schema diff <modelFile> --against=<prevFile> [--export=name] [--against-export=name]
36
+ axiom migrate plan <modelFile> [--export=name] [--from=<version>]
37
+ axiom migrate <modelFile> [--export=name] [--from=<version>] [--approve=op1,op2] [--sqlite=<path>]
38
+ axiom migrate status <modelFile> --sqlite=<path>
39
+
40
+ axiom explain <action|query|workflow|state> <id> <modelFile> [--json]
41
+ axiom analyze <modelFile> [--json]
42
+ axiom diff <modelFile> --against=<prevFile> [--export=name] [--against-export=name] [--json]
43
+
44
+ axiom --help
45
+ ```
46
+
47
+ `explain`, `analyze` and `diff` are the canonical tooling surface spec16 defines
48
+ (`docs/AGENT_API.md`'s "Semantic inventory, dependencies and explanation" section):
49
+
50
+ - **`explain`** renders `AgentAPI.explainAction` / `.explainQuery` / `.explainWorkflow` /
51
+ `.explainState` for one node — reads/writes, authorization, invokers, live capability,
52
+ and (for actions) whether static analysis is complete or a `NativeOperation` boundary
53
+ prevents it.
54
+ - **`analyze`** renders a structural graph summary, required runtime capabilities, the
55
+ `NativeOperation` inventory and the authorization coverage audit
56
+ (`AgentAPI.explainGraph` / `.analyzeCapabilities` / `.summarizeNativeOperations` /
57
+ `.analyzeAuthorization`).
58
+ - **`diff`** renders `AgentAPI.semanticDiff` between two graphs: every categorized change
59
+ (`semantic` / `authorization` / `schema` / `provider` / `workflow` / `query` /
60
+ `presentation` / `metadata`) and the compatibility impact — does it move
61
+ `semanticFingerprint`, `schemaFingerprint`, or the required Server IR contract. An
62
+ authorization-relevant change (attaching, detaching or editing a policy — including a
63
+ `QueryDef`'s row-level `readPolicyId`) is always tagged `authorization`, in addition to
64
+ its own category, never in place of it.
65
+
66
+ ## Machine-readable output
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.
72
+
73
+ ## Exit codes
74
+
75
+ ```text
76
+ 0 the requested operation completed (including a successful `explain`/`analyze`/`diff`,
77
+ and `validate` on a graph that is valid)
78
+ nonzero invalid input, an invalid graph (`validate`), or a tooling failure
79
+ ```
80
+
81
+ `--help` (or no arguments) always prints usage and exits `0`.
82
+
83
+ ## Side effects
84
+
85
+ `explain`, `analyze`, `diff`, `inspect` and `validate` are read-only static analysis: they
86
+ load the graph and never invoke an action, start a workflow, create an effect or write
87
+ persistence. `build` writes a compiled HTML file to `./dist`; `migrate` (without `--sqlite`)
88
+ only plans and prints — it executes only when given `--sqlite=<path>`; `serve` starts a real
89
+ HTTP server and, if the graph has server authority, a real authoritative runtime.
90
+
91
+ ## Scope
92
+
93
+ This is a development and inspection tool, not a production hosting mechanism. Publishing
94
+ it does not imply exposing `AgentAPI` over an unauthenticated network endpoint — `serve`'s
95
+ own semantic endpoint is the only network surface any command opens, and only when asked to
96
+ `serve` in the first place.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,642 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from 'node:http';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { compileToHtml, compileToIR, compileToServerIR, hasServerAuthority } from '@cynodia/axiom-compiler';
7
+ import { createAxiomServer, createMemoryPersistence, createServerHost, createSqliteMigrationStore, createSqlitePersistence, createSqliteRowStore, dispatch, executeMigration, explainMigration, getMigrationStatus, isSqliteAvailable, migrationAuthority, planMigration, } from '@cynodia/axiom-server';
8
+ import { ApplicationGraph, diffSchema, formatLocation, semanticContextFromGraph, semanticDiff, validateGraph } from '@cynodia/axiom-core';
9
+ import { AgentAPI, explainSchemaDiff, inspectSchema, migrationImpact } from '@cynodia/axiom-agent-api';
10
+ const GRAPH_EXPORT_CANDIDATES = ['default', 'createGraph', 'createApplicationGraph'];
11
+ /** `<group> <sub>` command forms. `migrate` alone (no sub) is also valid. */
12
+ const SUBCOMMANDS = {
13
+ schema: new Set(['status', 'diff']),
14
+ migrate: new Set(['plan', 'status']),
15
+ };
16
+ function parseArguments(argv) {
17
+ const positional = [];
18
+ let exportName;
19
+ let port = 3000;
20
+ let store;
21
+ let from;
22
+ let approve;
23
+ let sqlite;
24
+ let against;
25
+ let againstExport;
26
+ let json = false;
27
+ for (const argument of argv) {
28
+ if (argument.startsWith('--export=')) {
29
+ exportName = argument.slice('--export='.length);
30
+ }
31
+ else if (argument.startsWith('--port=')) {
32
+ port = Number(argument.slice('--port='.length)) || port;
33
+ }
34
+ else if (argument.startsWith('--store=')) {
35
+ store = argument.slice('--store='.length);
36
+ }
37
+ else if (argument.startsWith('--from=')) {
38
+ from = Number(argument.slice('--from='.length));
39
+ }
40
+ else if (argument.startsWith('--approve=')) {
41
+ approve = argument.slice('--approve='.length).split(',').filter(Boolean);
42
+ }
43
+ else if (argument.startsWith('--sqlite=')) {
44
+ sqlite = argument.slice('--sqlite='.length);
45
+ }
46
+ else if (argument.startsWith('--against=')) {
47
+ against = argument.slice('--against='.length);
48
+ }
49
+ else if (argument.startsWith('--against-export=')) {
50
+ againstExport = argument.slice('--against-export='.length);
51
+ }
52
+ else if (argument === '--json') {
53
+ json = true;
54
+ }
55
+ else {
56
+ positional.push(argument);
57
+ }
58
+ }
59
+ let [command, modelFile] = positional;
60
+ let kind;
61
+ let targetId;
62
+ if (command === 'explain') {
63
+ // `axiom explain <kind> <id> <modelFile>` — three positionals after the command itself.
64
+ kind = positional[1];
65
+ targetId = positional[2];
66
+ modelFile = positional[3];
67
+ }
68
+ else if (command && SUBCOMMANDS[command]?.has(positional[1] ?? '')) {
69
+ command = `${command} ${positional[1]}`;
70
+ modelFile = positional[2];
71
+ }
72
+ if (!command || !modelFile) {
73
+ return null;
74
+ }
75
+ return {
76
+ command,
77
+ modelFile,
78
+ exportName,
79
+ port,
80
+ ...(store ? { store } : {}),
81
+ ...(from !== undefined ? { from } : {}),
82
+ ...(approve ? { approve } : {}),
83
+ ...(sqlite ? { sqlite } : {}),
84
+ ...(against ? { against } : {}),
85
+ ...(againstExport ? { againstExport } : {}),
86
+ ...(kind ? { kind } : {}),
87
+ ...(targetId ? { targetId } : {}),
88
+ ...(json ? { json } : {}),
89
+ };
90
+ }
91
+ function toGraph(candidate) {
92
+ if (candidate instanceof ApplicationGraph) {
93
+ return candidate;
94
+ }
95
+ if (typeof candidate === 'string') {
96
+ return ApplicationGraph.deserialize(candidate);
97
+ }
98
+ if (candidate && typeof candidate === 'object' && 'nodes' in candidate && 'edges' in candidate) {
99
+ return ApplicationGraph.deserialize(candidate);
100
+ }
101
+ return null;
102
+ }
103
+ /**
104
+ * Loads an application graph from a module. The module may export the graph directly or
105
+ * a function that builds it; nothing about the application itself is assumed.
106
+ */
107
+ async function loadGraph(options) {
108
+ return loadGraphModule(options.modelFile, options.exportName);
109
+ }
110
+ async function loadGraphModule(modelFile, exportName) {
111
+ const options = { modelFile, exportName };
112
+ const resolved = path.resolve(process.cwd(), options.modelFile);
113
+ const module = (await import(pathToFileURL(resolved).href));
114
+ const names = options.exportName
115
+ ? [options.exportName]
116
+ : GRAPH_EXPORT_CANDIDATES.filter((name) => name in module);
117
+ for (const name of names) {
118
+ const exported = module[name];
119
+ const value = typeof exported === 'function' ? exported() : exported;
120
+ const graph = toGraph(value);
121
+ if (graph) {
122
+ return graph;
123
+ }
124
+ }
125
+ if (!options.exportName) {
126
+ const builders = Object.entries(module).filter(([, value]) => typeof value === 'function');
127
+ if (builders.length === 1) {
128
+ const graph = toGraph(builders[0][1]());
129
+ if (graph) {
130
+ return graph;
131
+ }
132
+ }
133
+ if (builders.length > 1) {
134
+ throw new Error(`${options.modelFile} exports several candidates. Choose one with --export=<name>: ${builders
135
+ .map(([name]) => name)
136
+ .join(', ')}`);
137
+ }
138
+ }
139
+ throw new Error(`Could not load an application graph from ${options.modelFile}`);
140
+ }
141
+ const SECTIONS = [
142
+ ['Entities', 'entity'],
143
+ ['State', 'state'],
144
+ ['Actions', 'action'],
145
+ ['Constraints', 'constraint'],
146
+ ['Routes', 'route'],
147
+ ['Views', 'view'],
148
+ ];
149
+ function describe(node) {
150
+ const label = node.name ? `${node.name} (${node.id})` : node.id;
151
+ if (node.kind === 'entity') {
152
+ const fields = node.fields.map((field) => field.name ?? field.id).join(', ');
153
+ return `${label}${fields ? ` — fields: ${fields}` : ''}`;
154
+ }
155
+ if (node.kind === 'route') {
156
+ return `${label} — ${node.path}`;
157
+ }
158
+ return label;
159
+ }
160
+ /** Locations are stored by id and resolved to names only for human inspection. */
161
+ function describeOperation(operation, semantics) {
162
+ switch (operation.kind) {
163
+ case 'set':
164
+ return `set ${formatLocation(operation.target, semantics)}`;
165
+ case 'insert':
166
+ return `insert into ${formatLocation(operation.target, semantics)}`;
167
+ case 'remove':
168
+ return `remove ${formatLocation(operation.target, semantics)}`;
169
+ case 'invoke':
170
+ return `invoke ${semantics.getName?.(operation.actionId) ?? operation.actionId}`;
171
+ case 'navigate':
172
+ return `navigate ${operation.path ?? semantics.getName?.(operation.routeId) ?? operation.routeId}`;
173
+ case 'native':
174
+ return `native ${operation.implementationId}`;
175
+ default:
176
+ return 'unknown operation';
177
+ }
178
+ }
179
+ function inspect(graph) {
180
+ const semantics = semanticContextFromGraph(graph);
181
+ const lines = [`${graph.name} (${graph.id}) v${graph.version}`, ''];
182
+ const fieldNames = (edge) => {
183
+ const fieldIds = edge.metadata?.fieldIds;
184
+ if (!Array.isArray(fieldIds) || fieldIds.length === 0) {
185
+ return '';
186
+ }
187
+ return ` (${fieldIds.map((id) => graph.getField(id)?.field.name ?? String(id)).join(', ')})`;
188
+ };
189
+ for (const [title, kind] of SECTIONS) {
190
+ const nodes = graph.getNodesByKind(kind);
191
+ lines.push(title);
192
+ if (nodes.length === 0) {
193
+ lines.push('- none');
194
+ }
195
+ for (const node of nodes) {
196
+ const edges = graph
197
+ .getOutgoingEdges(node.id)
198
+ .map((edge) => `${edge.kind} → ${graph.getNode(edge.to)?.name ?? edge.to}${fieldNames(edge)}`)
199
+ .join(', ');
200
+ lines.push(`- ${describe(node)}${edges ? ` [${edges}]` : ''}`);
201
+ if (node.kind === 'action') {
202
+ for (const operation of node.operations ?? []) {
203
+ lines.push(` ${describeOperation(operation, semantics)}`);
204
+ }
205
+ }
206
+ }
207
+ lines.push('');
208
+ }
209
+ const uiCount = graph.listNodes().filter((node) => !SECTIONS.some(([, kind]) => kind === node.kind)).length;
210
+ lines.push(`UI nodes: ${uiCount}`, `Edges: ${graph.semanticEdges().length}`);
211
+ return lines.join('\n');
212
+ }
213
+ function formatValidation(result) {
214
+ const lines = [];
215
+ for (const problem of result.errors) {
216
+ lines.push(`error [${problem.code}] ${problem.message}`);
217
+ }
218
+ for (const problem of result.warnings) {
219
+ lines.push(`warning [${problem.code}] ${problem.message}`);
220
+ }
221
+ lines.push(result.valid
222
+ ? `Graph is valid (${result.warnings.length} warning${result.warnings.length === 1 ? '' : 's'}).`
223
+ : `Graph is invalid: ${result.errors.length} error${result.errors.length === 1 ? '' : 's'}.`);
224
+ return lines.join('\n');
225
+ }
226
+ async function build(options) {
227
+ const graph = await loadGraph(options);
228
+ const html = compileToHtml(graph);
229
+ const outputDir = path.resolve(process.cwd(), 'dist');
230
+ const outputFile = path.join(outputDir, `${graph.id}.html`);
231
+ await mkdir(outputDir, { recursive: true });
232
+ await writeFile(outputFile, html, 'utf8');
233
+ console.log(`Built ${outputFile}`);
234
+ }
235
+ /** The authoritative half, when the graph has one. No application code is involved. */
236
+ async function startAuthority(graph, options) {
237
+ if (!hasServerAuthority(graph)) {
238
+ return null;
239
+ }
240
+ let persistence;
241
+ if (options.store && (await isSqliteAvailable())) {
242
+ persistence = await createSqlitePersistence({ location: options.store });
243
+ console.log(`Authoritative state persists to ${options.store}`);
244
+ }
245
+ else {
246
+ if (options.store) {
247
+ console.warn('node:sqlite is unavailable; authoritative state is held in memory only');
248
+ }
249
+ persistence = createMemoryPersistence();
250
+ }
251
+ const server = createAxiomServer({
252
+ ir: compileToServerIR(graph),
253
+ persistence,
254
+ host: createServerHost({
255
+ // Authentication belongs to a host. This one reads a bearer credential and treats it
256
+ // as the caller's identity, which is enough to demonstrate the boundary and no more.
257
+ authenticate: (credential) => credential ? { [PRINCIPAL_IDENTITY]: credential } : null,
258
+ report: (event) => {
259
+ if (event.kind !== 'snapshot') {
260
+ console.log(`[axiom] ${event.kind} ${event.actionId ?? ''} ${event.ok === undefined ? '' : event.ok ? 'ok' : 'refused'}`.trim());
261
+ }
262
+ },
263
+ }),
264
+ });
265
+ await server.start();
266
+ return server;
267
+ }
268
+ /** The identity field of the graph's principal entity, resolved at startup. */
269
+ let PRINCIPAL_IDENTITY = 'id';
270
+ async function serve(options) {
271
+ const graph = await loadGraph(options);
272
+ const ir = compileToIR(graph);
273
+ const html = compileToHtml(graph);
274
+ const principalEntity = graph.principalEntityId
275
+ ? graph.getNode(graph.principalEntityId)
276
+ : undefined;
277
+ if (principalEntity?.kind === 'entity' && principalEntity.identityFieldId) {
278
+ PRINCIPAL_IDENTITY = String(principalEntity.identityFieldId);
279
+ }
280
+ const authority = await startAuthority(graph, options);
281
+ const matches = (pathname) => ir.routes.some((route) => {
282
+ const parts = pathname.split('?')[0].split('/').filter(Boolean);
283
+ return (route.segments.length === parts.length &&
284
+ route.segments.every((segment, index) => segment.kind === 'parameter' || segment.value === parts[index]));
285
+ });
286
+ const server = createServer((request, response) => {
287
+ void (async () => {
288
+ // One semantic endpoint, the same for every application. No route is declared here
289
+ // and none is generated: the client asks for actions, not for URLs.
290
+ if (authority && request.method === 'POST' && (request.url ?? '').split('?')[0] === SEMANTIC_ENDPOINT) {
291
+ const chunks = [];
292
+ for await (const chunk of request) {
293
+ chunks.push(chunk);
294
+ }
295
+ let body = null;
296
+ try {
297
+ body = chunks.length > 0 ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : null;
298
+ }
299
+ catch {
300
+ response.writeHead(400, { 'content-type': 'application/json' });
301
+ response.end('{"kind":"error","diagnostics":[]}');
302
+ return;
303
+ }
304
+ const answer = await dispatch(authority, body);
305
+ response.writeHead(200, { 'content-type': 'application/json' });
306
+ response.end(JSON.stringify(answer));
307
+ return;
308
+ }
309
+ if (request.url && matches(request.url)) {
310
+ response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
311
+ response.end(html);
312
+ return;
313
+ }
314
+ response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
315
+ response.end('No route matches this path');
316
+ })();
317
+ });
318
+ server.listen(options.port, '127.0.0.1', () => {
319
+ console.log(`${graph.name} available at http://127.0.0.1:${options.port}`);
320
+ if (authority) {
321
+ console.log(`Authoritative runtime at http://127.0.0.1:${options.port}${SEMANTIC_ENDPOINT} — ` +
322
+ `${Object.keys(compileToServerIR(graph).actions).length} server actions`);
323
+ }
324
+ });
325
+ }
326
+ /** The one endpoint every Axiom authority answers on. */
327
+ const SEMANTIC_ENDPOINT = '/axiom';
328
+ // --- Schema evolution (spec11 §89-91) --------------------------------------
329
+ // Thin wrappers over the already-tested library functions. The CLI is a consumer,
330
+ // never the definition of behaviour.
331
+ function schemaStatus(graph) {
332
+ const s = inspectSchema(graph);
333
+ const lines = [
334
+ `semantic schema version : ${s.schemaVersion}`,
335
+ `schema fingerprint : ${s.schemaFingerprint}`,
336
+ `migration chain : ${s.chainComplete ? 'complete (1 → ' + s.schemaVersion + ')' : 'INCOMPLETE'}`,
337
+ `entities : ${s.entities.length}`,
338
+ `persisted states : ${s.persistedStates.length}`,
339
+ `relationships : ${s.relationships.length}`,
340
+ `read policies : ${s.readPolicies.length}`,
341
+ ];
342
+ if (s.migrations.length > 0) {
343
+ lines.push('migrations:');
344
+ for (const migration of s.migrations) {
345
+ lines.push(` ${migration.fromSchema} → ${migration.toSchema} ${migration.id} (${migration.operationCount} ops, ${migration.destructiveOperationCount} destructive)`);
346
+ }
347
+ }
348
+ return lines.join('\n');
349
+ }
350
+ async function schemaDiff(options) {
351
+ if (!options.against) {
352
+ throw new Error('schema diff needs a --against=<file> naming the previous schema');
353
+ }
354
+ const previous = await loadGraphModule(options.against, options.againstExport);
355
+ const next = await loadGraph(options);
356
+ const diff = diffSchema(previous, next);
357
+ const impact = migrationImpact(previous, next);
358
+ const parts = [
359
+ explainSchemaDiff(diff),
360
+ '',
361
+ `verdict : ${impact.verdict}`,
362
+ `data loss possible : ${impact.dataLossPossible}`,
363
+ `migration covers it: ${impact.covered}${impact.covered ? '' : ' — uncovered: ' + impact.uncovered.map((e) => e.fieldId ?? e.entityId).join(', ')}`,
364
+ `affected queries : ${impact.affectedQueries.length}`,
365
+ `affected actions : ${impact.affectedActions.length}`,
366
+ `affected constraints: ${impact.affectedConstraints.length}`,
367
+ `affected UI nodes : ${impact.affectedUiNodes.length}`,
368
+ ];
369
+ if (impact.authorizationChanges.length > 0) {
370
+ parts.push(`authorization changes: ${impact.authorizationChanges.join(', ')}`);
371
+ }
372
+ return parts.join('\n');
373
+ }
374
+ async function migratePlan(options) {
375
+ const graph = await loadGraph(options);
376
+ const ir = compileToServerIR(graph, { validate: false });
377
+ const from = options.from ?? 1;
378
+ const result = planMigration(ir, { fromVersion: from });
379
+ if (!result.ok) {
380
+ process.exitCode = 1;
381
+ return result.diagnostics.map((d) => `${d.code}: ${d.message}`).join('\n');
382
+ }
383
+ return explainMigration(result.plan);
384
+ }
385
+ async function migrateStatus(options) {
386
+ if (!options.sqlite) {
387
+ throw new Error('migrate status needs --sqlite=<path> to read the provider metadata');
388
+ }
389
+ if (!(await isSqliteAvailable())) {
390
+ throw new Error('this Node build has no node:sqlite');
391
+ }
392
+ const metadata = await createSqliteMigrationStore({ location: options.sqlite });
393
+ const status = await getMigrationStatus(metadata);
394
+ return [
395
+ `schema version : ${status.schemaVersion ?? '(unstamped)'}`,
396
+ `fingerprint : ${status.schemaFingerprint ?? '(none)'}`,
397
+ `phase : ${status.phase}`,
398
+ `lock : ${status.lock ? status.lock.holder : 'free'}`,
399
+ `checkpoint : ${status.checkpoint ? `op ${status.checkpoint.operationIndex}, ${status.checkpoint.rowsProcessed} rows` : 'none'}`,
400
+ `history : ${status.history.map((h) => `${h.fromSchema}→${h.toSchema}`).join(', ') || 'none'}`,
401
+ ].join('\n');
402
+ }
403
+ async function migrateRun(options) {
404
+ if (!options.sqlite) {
405
+ return `${await migratePlan(options)}\n\n(supply --sqlite=<path> to execute this migration against a SQLite database)`;
406
+ }
407
+ if (!(await isSqliteAvailable())) {
408
+ throw new Error('this Node build has no node:sqlite');
409
+ }
410
+ const graph = await loadGraph(options);
411
+ const ir = compileToServerIR(graph, { validate: false });
412
+ const rows = await createSqliteRowStore({ location: options.sqlite, ir });
413
+ const metadata = await createSqliteMigrationStore({
414
+ location: options.sqlite,
415
+ database: rows.database,
416
+ });
417
+ const result = await executeMigration({
418
+ ir,
419
+ metadata,
420
+ rows,
421
+ principal: migrationAuthority('axiom-cli'),
422
+ ...(options.from !== undefined ? { fromVersion: options.from } : {}),
423
+ approveDestructive: options.approve ?? [],
424
+ });
425
+ if (!result.ok) {
426
+ process.exitCode = 1;
427
+ return `${result.code}: ${result.message}`;
428
+ }
429
+ return [
430
+ `migrated ${result.plan.fromVersion} → ${result.plan.toVersion}`,
431
+ `rows transformed : ${result.run.rowsTransformed}`,
432
+ `resumed : ${result.run.resumed}`,
433
+ `gate now : ${result.gate.status}`,
434
+ ].join('\n');
435
+ }
436
+ // --- Explainability & AI authoring tooling (spec16 §106-112) ---------------
437
+ // Thin renderers over AgentAPI / core, exactly like the schema commands above:
438
+ // the CLI is a consumer of the canonical analysis, never a second place it lives.
439
+ function formatExplanation(kind, result) {
440
+ const lines = [];
441
+ switch (kind) {
442
+ case 'action': {
443
+ const reads = result.reads;
444
+ const writes = result.writes;
445
+ const authorization = result.authorization;
446
+ const invokedBy = result.invokedBy;
447
+ lines.push(`action ${result.actionId}${result.name ? ` (${result.name})` : ''}`);
448
+ lines.push(` reads : ${reads.stateIds.join(', ') || 'none'}`);
449
+ lines.push(` writes : ${writes.stateIds.join(', ') || 'none'}`);
450
+ lines.push(` authorization: ${authorization.kind}`);
451
+ lines.push(` invoked by: triggers [${invokedBy.triggers.join(', ')}], workflows [${invokedBy.workflowSteps.join(', ')}]`);
452
+ if (result.analysisComplete === false) {
453
+ lines.push(` INCOMPLETE — ${result.analysisGaps.join('; ')}`);
454
+ }
455
+ break;
456
+ }
457
+ case 'query': {
458
+ const authorization = result.authorization;
459
+ lines.push(`query ${result.queryId}`);
460
+ lines.push(` source : ${result.source}`);
461
+ lines.push(` authorization : ${authorization.kind}`);
462
+ lines.push(` live capability: ${result.liveCapability}`);
463
+ break;
464
+ }
465
+ case 'workflow': {
466
+ const steps = result.steps;
467
+ lines.push(`workflow ${result.workflowId}`);
468
+ lines.push(` steps : ${steps.map((s) => `${s.id}:${s.type}`).join(', ')}`);
469
+ lines.push(` start policy: ${result.startPolicyId ?? 'public'}`);
470
+ lines.push(` privilege-review actions: ${result.privilegeReviewActions.join(', ') || 'none'}`);
471
+ break;
472
+ }
473
+ case 'state': {
474
+ lines.push(`state ${result.stateId}`);
475
+ lines.push(` authority : ${result.authority} derived: ${result.derived} draft: ${result.draft}`);
476
+ lines.push(` writers : ${result.writers.join(', ') || 'none'}`);
477
+ lines.push(` readers : ${result.readers.join(', ') || 'none'}`);
478
+ break;
479
+ }
480
+ default:
481
+ lines.push(JSON.stringify(result, null, 2));
482
+ }
483
+ return lines.join('\n');
484
+ }
485
+ async function explainCommand(options) {
486
+ if (!options.kind || !options.targetId) {
487
+ throw new Error('usage: axiom explain <action|query|workflow|state> <id> <modelFile>');
488
+ }
489
+ const agent = new AgentAPI(await loadGraph(options));
490
+ let result;
491
+ switch (options.kind) {
492
+ case 'action':
493
+ result = agent.explainAction(options.targetId);
494
+ break;
495
+ case 'query':
496
+ result = agent.explainQuery(options.targetId);
497
+ break;
498
+ case 'workflow':
499
+ result = agent.explainWorkflow(options.targetId);
500
+ break;
501
+ case 'state':
502
+ result = agent.explainState(options.targetId);
503
+ break;
504
+ default:
505
+ throw new Error(`explain: unknown kind "${options.kind}" (expected action, query, workflow or state)`);
506
+ }
507
+ if (!result) {
508
+ process.exitCode = 1;
509
+ return `No ${options.kind} node "${options.targetId}" in this graph`;
510
+ }
511
+ return options.json ? JSON.stringify(result, null, 2) : formatExplanation(options.kind, result);
512
+ }
513
+ async function analyzeCommand(options) {
514
+ const agent = new AgentAPI(await loadGraph(options));
515
+ const summary = agent.explainGraph();
516
+ const capabilities = agent.analyzeCapabilities();
517
+ const native = agent.summarizeNativeOperations();
518
+ const authorization = agent.analyzeAuthorization();
519
+ if (options.json) {
520
+ return JSON.stringify({ summary, capabilities, native, unprotected: authorization.unprotected }, null, 2);
521
+ }
522
+ return [
523
+ `nodes by kind : ${Object.entries(summary.nodeCountsByKind).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'}`,
524
+ `executable roots: ${summary.executableRoots.actions.length} client-invocable action(s), ${summary.executableRoots.workflows.length} workflow(s), ${summary.executableRoots.queries.length} quer(y/ies)`,
525
+ `security : ${summary.securityBoundaries.protectedActions} protected / ${summary.securityBoundaries.publicActions} public action(s); ${summary.securityBoundaries.protectedQueries} protected / ${summary.securityBoundaries.publicQueries} public quer(y/ies)`,
526
+ `native ops : ${native.count} (${native.opaqueCount} opaque — static analysis cannot see past them)`,
527
+ `capabilities : ${capabilities.requiredCapabilities.join(', ') || 'none required'}`,
528
+ `unprotected : ${authorization.unprotected.length} surface(s) with no explicit authorization boundary`,
529
+ ].join('\n');
530
+ }
531
+ async function diffCommand(options) {
532
+ if (!options.against) {
533
+ throw new Error('diff needs a --against=<file> naming the graph to compare against');
534
+ }
535
+ const before = await loadGraphModule(options.against, options.againstExport);
536
+ const after = await loadGraph(options);
537
+ const diff = semanticDiff(before, after);
538
+ if (options.json) {
539
+ return JSON.stringify(diff, null, 2);
540
+ }
541
+ const lines = [
542
+ `${diff.entries.length} node change(s), ${diff.schema.entries.length} schema change(s)`,
543
+ ...diff.entries.map((entry) => ` ${entry.changeKind[0].toUpperCase()} ${entry.nodeKind} ${entry.nodeId} [${entry.categories.join(', ')}]`),
544
+ ...diff.schema.entries.map((entry) => ` ~ schema: ${entry.message}`),
545
+ '',
546
+ `semanticFingerprint changed : ${diff.compatibility.semanticFingerprintChanged}`,
547
+ `schemaFingerprint changed : ${diff.compatibility.schemaFingerprintChanged}`,
548
+ `server contract : ${diff.compatibility.serverContractBefore} -> ${diff.compatibility.serverContractAfter}`,
549
+ ];
550
+ return lines.join('\n');
551
+ }
552
+ /**
553
+ * The one usage text, printed both for `--help` (exit 0) and for missing/malformed
554
+ * arguments (exit 1) — a fresh consumer must be able to discover every command and its
555
+ * `--json` / exit behavior from this alone, without repository access (spec16pt2 §63-64).
556
+ */
557
+ const USAGE = [
558
+ 'axiom — inspect, validate, explain and analyze an Axiom application graph.',
559
+ '',
560
+ 'Usage:',
561
+ ' axiom <build|inspect|serve> <modelFile> [--export=name] [--port=3000] [--store=state.db]',
562
+ ' axiom validate <modelFile> [--export=name] [--json]',
563
+ ' axiom schema status <modelFile> [--export=name]',
564
+ ' axiom schema diff <modelFile> --against=<prevFile> [--export=name] [--against-export=name]',
565
+ ' axiom migrate plan <modelFile> [--export=name] [--from=<version>]',
566
+ ' axiom migrate <modelFile> [--export=name] [--from=<version>] [--approve=op1,op2] [--sqlite=<path>]',
567
+ ' axiom migrate status <modelFile> --sqlite=<path>',
568
+ ' axiom explain <action|query|workflow|state> <id> <modelFile> [--json]',
569
+ ' axiom analyze <modelFile> [--json]',
570
+ ' axiom diff <modelFile> --against=<prevFile> [--export=name] [--against-export=name] [--json]',
571
+ ' axiom --help',
572
+ '',
573
+ '<modelFile> is a compiled (built) JavaScript module exporting an ApplicationGraph or a',
574
+ 'function that builds one — see docs/AGENT_API.md and docs/AGENT_REFERENCE.md in',
575
+ '@cynodia/axiom for the semantic contract every command renders.',
576
+ '',
577
+ 'Exit codes: 0 on success; nonzero on invalid input, an invalid graph, or a tooling failure.',
578
+ ].join('\n');
579
+ async function main() {
580
+ const argv = process.argv.slice(2);
581
+ if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) {
582
+ console.log(USAGE);
583
+ return;
584
+ }
585
+ const options = parseArguments(argv);
586
+ if (!options) {
587
+ console.error(USAGE);
588
+ process.exitCode = 1;
589
+ return;
590
+ }
591
+ switch (options.command) {
592
+ case 'schema status':
593
+ console.log(schemaStatus(await loadGraph(options)));
594
+ return;
595
+ case 'schema diff':
596
+ console.log(await schemaDiff(options));
597
+ return;
598
+ case 'migrate plan':
599
+ console.log(await migratePlan(options));
600
+ return;
601
+ case 'migrate status':
602
+ console.log(await migrateStatus(options));
603
+ return;
604
+ case 'migrate':
605
+ console.log(await migrateRun(options));
606
+ return;
607
+ case 'build':
608
+ await build(options);
609
+ return;
610
+ case 'inspect': {
611
+ console.log(inspect(await loadGraph(options)));
612
+ return;
613
+ }
614
+ case 'validate': {
615
+ const result = validateGraph(await loadGraph(options));
616
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatValidation(result));
617
+ if (!result.valid) {
618
+ process.exitCode = 1;
619
+ }
620
+ return;
621
+ }
622
+ case 'serve':
623
+ await serve(options);
624
+ return;
625
+ case 'explain':
626
+ console.log(await explainCommand(options));
627
+ return;
628
+ case 'analyze':
629
+ console.log(await analyzeCommand(options));
630
+ return;
631
+ case 'diff':
632
+ console.log(await diffCommand(options));
633
+ return;
634
+ default:
635
+ console.error(`Unknown command: ${options.command}`);
636
+ process.exitCode = 1;
637
+ }
638
+ }
639
+ main().catch((error) => {
640
+ console.error(error instanceof Error ? error.message : String(error));
641
+ process.exitCode = 1;
642
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@cynodia/axiom-cli",
3
+ "version": "0.16.0-alpha.2",
4
+ "description": "Command line tools for inspecting, validating and serving Axiom applications.",
5
+ "license": "MIT",
6
+ "author": "AskTech AS",
7
+ "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/cynodia/axiom.git"
11
+ },
12
+ "homepage": "https://github.com/cynodia/axiom",
13
+ "bugs": {
14
+ "url": "https://github.com/cynodia/axiom/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "files": [
20
+ "dist/**/*.js",
21
+ "dist/**/*.d.ts",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ }
32
+ },
33
+ "bin": {
34
+ "axiom": "./dist/index.js"
35
+ },
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"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -b tsconfig.json"
44
+ }
45
+ }