@factoidal/core 0.2.0 → 0.4.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 (132) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/NOTICE +31 -0
  3. package/README.md +131 -1
  4. package/bin/engine.mjs +124 -0
  5. package/bin/factoidal.mjs +894 -0
  6. package/bin/pack-host.mjs +341 -0
  7. package/bin/pack-worker.mjs +31 -0
  8. package/bin/pack.mjs +174 -0
  9. package/bin/store.mjs +238 -0
  10. package/l4-assets/l4factoidal.js +125 -5
  11. package/l4-assets/l4factoidal.mjs +1 -1
  12. package/l4-assets/l4factoidal.wasm +0 -0
  13. package/l4-assets/package.json +4 -0
  14. package/l4-assets/version.json +5 -5
  15. package/l4.d.ts +14 -0
  16. package/l4.js +15 -1
  17. package/package.json +17 -2
  18. package/sample-store/CURRENT +1 -0
  19. package/sample-store/gen-1/manifest.sbm2 +0 -0
  20. package/sample-store/gen-1/manifest.tsv +14 -0
  21. package/sample-store/gen-1/predicate-0.ibk3 +0 -0
  22. package/sample-store/gen-1/predicate-0.ibk3.merkle +1 -0
  23. package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
  24. package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +1 -0
  25. package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
  26. package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +1 -0
  27. package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
  28. package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
  29. package/sample-store/gen-1/predicate-1.ibk3 +0 -0
  30. package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
  31. package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
  32. package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
  33. package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
  34. package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +1 -0
  35. package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
  36. package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +1 -0
  37. package/sample-store/gen-1/predicate-10.ibk3 +0 -0
  38. package/sample-store/gen-1/predicate-10.ibk3.merkle +1 -0
  39. package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
  40. package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
  41. package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
  42. package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +1 -0
  43. package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
  44. package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +1 -0
  45. package/sample-store/gen-1/predicate-11.ibk3 +0 -0
  46. package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
  47. package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
  48. package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +1 -0
  49. package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
  50. package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +1 -0
  51. package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
  52. package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +1 -0
  53. package/sample-store/gen-1/predicate-12.ibk3 +0 -0
  54. package/sample-store/gen-1/predicate-12.ibk3.merkle +1 -0
  55. package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
  56. package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +1 -0
  57. package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
  58. package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +1 -0
  59. package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
  60. package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +1 -0
  61. package/sample-store/gen-1/predicate-2.ibk3 +0 -0
  62. package/sample-store/gen-1/predicate-2.ibk3.merkle +1 -0
  63. package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
  64. package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +1 -0
  65. package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
  66. package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
  67. package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
  68. package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
  69. package/sample-store/gen-1/predicate-3.ibk3 +0 -0
  70. package/sample-store/gen-1/predicate-3.ibk3.merkle +1 -0
  71. package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
  72. package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +1 -0
  73. package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
  74. package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +1 -0
  75. package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
  76. package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +1 -0
  77. package/sample-store/gen-1/predicate-4.ibk3 +0 -0
  78. package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
  79. package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
  80. package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +1 -0
  81. package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
  82. package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +1 -0
  83. package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
  84. package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +2 -0
  85. package/sample-store/gen-1/predicate-5.ibk3 +0 -0
  86. package/sample-store/gen-1/predicate-5.ibk3.merkle +1 -0
  87. package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
  88. package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +1 -0
  89. package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
  90. package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +1 -0
  91. package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
  92. package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +1 -0
  93. package/sample-store/gen-1/predicate-6.ibk3 +0 -0
  94. package/sample-store/gen-1/predicate-6.ibk3.merkle +1 -0
  95. package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
  96. package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +1 -0
  97. package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
  98. package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +1 -0
  99. package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
  100. package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
  101. package/sample-store/gen-1/predicate-7.ibk3 +0 -0
  102. package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
  103. package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
  104. package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +2 -0
  105. package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
  106. package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +1 -0
  107. package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
  108. package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +1 -0
  109. package/sample-store/gen-1/predicate-8.ibk3 +0 -0
  110. package/sample-store/gen-1/predicate-8.ibk3.merkle +1 -0
  111. package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
  112. package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +1 -0
  113. package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
  114. package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +1 -0
  115. package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
  116. package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +1 -0
  117. package/sample-store/gen-1/predicate-9.ibk3 +0 -0
  118. package/sample-store/gen-1/predicate-9.ibk3.merkle +1 -0
  119. package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
  120. package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +1 -0
  121. package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
  122. package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +3 -0
  123. package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
  124. package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +2 -0
  125. package/sample-store.d.ts +16 -0
  126. package/sample-store.mjs +37 -0
  127. package/store-host/deno.mjs +262 -0
  128. package/store-host/errors.mjs +55 -0
  129. package/store-host/index.mjs +247 -0
  130. package/store-host/node.mjs +273 -0
  131. package/store-host/paths.mjs +77 -0
  132. package/version.json +22 -21
@@ -0,0 +1,894 @@
1
+ #!/usr/bin/env node
2
+ // The `factoidal` command: pack, activate, query, update and compact a
3
+ // persisted Shardborough store from Node or Deno, with no native binary.
4
+ // https://github.com/danbri/factoidal/issues/641
5
+ //
6
+ // STATE TODAY. The argument surface below is complete and settled, so that
7
+ // wiring the remaining subcommands changes no user-visible syntax.
8
+ // `version`, `inspect` and `query` run: the first needs only host file
9
+ // I/O, and the other two drive the three WebAssembly store operations of
10
+ // `formal/lean4/Wasm/Ops/Store.lean`. `pack`, `activate`, `update` and
11
+ // `compact` parse their arguments, report what they would do, and exit 3;
12
+ // they need operations that do not exist yet.
13
+ //
14
+ // This file reads files by name, moves bytes and renders what the engine
15
+ // answered. It parses no manifest, verifies no digest, decodes no block
16
+ // and chooses no artifact -- every one of those is a format decision and
17
+ // it lives in the Lean source (iron rule 7). The table renderer is a
18
+ // human display of the engine's own SPARQL Query Results JSON, not a
19
+ // second serializer: `--format json`, `--format nquads` and
20
+ // `--format turtle` all print documents the engine produced.
21
+
22
+ import {
23
+ StoreHostError, atomicReplace, listGeneration, makeDirectory, readWhole,
24
+ runtime
25
+ } from '../store-host/index.mjs'
26
+ import { fileUrlToPath, joinPath } from '../store-host/paths.mjs'
27
+ import { loadEngine } from './engine.mjs'
28
+ import { sampleStoreFacts, sampleStorePath } from '../sample-store.mjs'
29
+ import { PackError, packSupported, verifyGeneration } from './pack.mjs'
30
+ import { denoReexec, isStackOverflow, runPack } from './pack-host.mjs'
31
+ import {
32
+ STACK_REMEDY, StoreOperationError, inspectManifest, openStore, planQuery,
33
+ queryStore, stackLimitAdvice, turtleOfNQuads
34
+ } from './store.mjs'
35
+
36
+ const EXIT_OK = 0
37
+ const EXIT_FAILURE = 1
38
+ const EXIT_USAGE = 2
39
+ const EXIT_NOT_WIRED = 3
40
+
41
+ // Progress is reported about every 16 MiB; the packer feeds 65,536 bytes
42
+ // a time, so this is the window that catches one feed per report.
43
+ const FEED_PROGRESS = 65536
44
+
45
+ const PACK_LAYOUTS = ['ibk3', 'ibk4']
46
+ const PACK_SYNTAXES = ['turtle', 'trig', 'nquads', 'ntriples']
47
+ const PACK_SUFFIXES = [
48
+ ['.ttl', 'turtle'], ['.turtle', 'turtle'],
49
+ ['.trig', 'trig'],
50
+ ['.nq', 'nquads'], ['.nquads', 'nquads'],
51
+ ['.nt', 'ntriples'], ['.ntriples', 'ntriples']
52
+ ]
53
+
54
+ // Suffixes the engine parses elsewhere but the packer's streaming fold does
55
+ // not read. Naming them is better than "cannot tell the syntax from its
56
+ // name", which sends the reader looking for a --syntax value that does not
57
+ // exist.
58
+ const PACK_UNSUPPORTED_SUFFIXES = [
59
+ ['.rdf', 'RDF/XML'], ['.owl', 'RDF/XML'], ['.xml', 'RDF/XML'],
60
+ ['.jsonld', 'JSON-LD'], ['.json', 'JSON-LD'], ['.n3', 'Notation3'],
61
+ ['.csv', 'CSV'], ['.tsv', 'TSV'], ['.hdt', 'HDT']
62
+ ]
63
+
64
+ /** The parent of a path, and its last component. The `activate` hint
65
+ * printed after a pack needs both; neither is a format decision. */
66
+ function dirOf (path) {
67
+ const cut = path.replace(/[/\\]+$/, '').lastIndexOf('/')
68
+ return cut <= 0 ? '.' : path.slice(0, cut)
69
+ }
70
+ function nameOf (path) {
71
+ const trimmed = path.replace(/[/\\]+$/, '')
72
+ const cut = trimmed.lastIndexOf('/')
73
+ return cut < 0 ? trimmed : trimmed.slice(cut + 1)
74
+ }
75
+
76
+ const ISSUE = 'https://github.com/danbri/factoidal/issues/641'
77
+
78
+ const isDeno = typeof globalThis.Deno !== 'undefined'
79
+ const argv = isDeno ? globalThis.Deno.args.slice() : process.argv.slice(2)
80
+
81
+ // Node writes to a PIPE asynchronously, and `process.exit()` drops
82
+ // whatever is still buffered. A SELECT that prints a few megabytes of
83
+ // SPARQL Query Results JSON into `| jq` therefore arrived truncated at
84
+ // the 64 KiB pipe boundary (measured 2026-09-03, 6455 rows). Setting the
85
+ // exit code and letting the process end on its own flushes it.
86
+ function exit (code) {
87
+ if (isDeno) globalThis.Deno.exit(code)
88
+ else process.exitCode = code
89
+ }
90
+
91
+ function out (line) { console.log(line) }
92
+ function err (line) { console.error(line) }
93
+
94
+ // ---------------------------------------------------------------- usage
95
+
96
+ const USAGE = `factoidal - the Factoidal persisted store, from Node or Deno
97
+
98
+ usage: factoidal <command> [options]
99
+
100
+ commands:
101
+ version print the package and engine versions
102
+ sample-store print the path of the bundled sample store
103
+ inspect STORE report what the activated manifest commits
104
+ query STORE [QUERY] evaluate a SPARQL query against a store
105
+ pack INPUT OUTPUT build one immutable generation from an RDF file
106
+ activate STORE GENERATION make one generation the activated generation
107
+ update STORE [UPDATE] apply a SPARQL Update through the delta log
108
+ compact STORE GENERATION fold the delta log into a new generation
109
+
110
+ global options:
111
+ -h, --help print this text, or a command's own help
112
+ --json machine-readable output where a command has it
113
+ --quiet print results only, no progress lines
114
+
115
+ exit codes:
116
+ 0 success 1 failure 2 usage error 3 not yet wired (${ISSUE})
117
+
118
+ STORE is a collection root: the directory that holds CURRENT. This package
119
+ carries one, so the first query needs no other download:
120
+
121
+ factoidal query "$(factoidal sample-store)" 'SELECT * WHERE { ?s ?p ?o } LIMIT 5'`
122
+
123
+ const COMMAND_USAGE = {
124
+ version: `factoidal version - print the package and engine versions
125
+
126
+ usage: factoidal version [--json]
127
+
128
+ Prints the npm package version, the Lean engine's WebAssembly digest as
129
+ recorded by its build, and which host-I/O implementation is loaded.`,
130
+
131
+ 'sample-store': `factoidal sample-store - print the bundled store's path
132
+
133
+ usage: factoidal sample-store [--json]
134
+
135
+ Prints the collection root of the Shardborough store this package
136
+ carries, so a fresh install can query something at once:
137
+
138
+ factoidal inspect "$(factoidal sample-store)"
139
+ factoidal query "$(factoidal sample-store)" \\
140
+ 'SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }'
141
+
142
+ The store holds five IPTC NewsCodes vocabularies (CC BY 4.0; see NOTICE)
143
+ packed into IBK3 predicate blocks. --json adds what was recorded when it
144
+ was packed.`,
145
+
146
+ inspect: `factoidal inspect - report what a store's manifest commits
147
+
148
+ usage: factoidal inspect STORE [--json] [--generation NAME]
149
+
150
+ Reads CURRENT, hands the manifest bytes to the engine's
151
+ storeManifestInspect operation, and prints what it decoded: the wire
152
+ version, the layout, the blank-node publication profile, the term-registry
153
+ version, whether the manifest carries a fixed-chunk Merkle commitment, and
154
+ one row per entry with its predicate, row count, byte length, block kind
155
+ and graph set.
156
+
157
+ options:
158
+ --generation NAME inspect this generation instead of the activated one
159
+ --json print the operation's envelope unchanged`,
160
+
161
+ query: `factoidal query - evaluate a SPARQL query against a store
162
+
163
+ usage: factoidal query STORE [QUERY] [options]
164
+
165
+ QUERY is the query text. Give it as the second argument, or with --query,
166
+ or in a file with --file.
167
+
168
+ The command reads CURRENT and the manifest, asks the engine which
169
+ artifacts the query needs, reads exactly those, and hands their bytes to
170
+ the engine's storeQuery operation. The engine verifies every artifact
171
+ against the SHA-256 the manifest commits before it answers.
172
+
173
+ options:
174
+ --query TEXT the query text
175
+ --file PATH read the query text from a file
176
+ --format FORMAT table (default), json, nquads, turtle
177
+ --limit N print at most N table rows; the total is always named
178
+ --explain print the artifact plan instead of the results
179
+ --generation NAME query this generation instead of the activated one
180
+ --json shorthand for --format json
181
+ --quiet print the result only, no plan line on stderr
182
+
183
+ formats:
184
+ table a human display of the engine's SPARQL Query Results JSON;
185
+ ASK prints true or false, CONSTRUCT prints its N-Triples
186
+ json SELECT prints the engine's SPARQL 1.1 Query Results JSON
187
+ document; ASK and CONSTRUCT print the operation's envelope,
188
+ because the operation answers those two with a boolean and a
189
+ serialized graph rather than with a results document
190
+ nquads CONSTRUCT only: the graph the engine serialized
191
+ turtle CONSTRUCT only: that graph through the engine's own Turtle
192
+ writer, which flattens named graphs into the default graph
193
+
194
+ not available, and why:
195
+ --base IRI the store query operation takes no base argument;
196
+ put a BASE clause in the query text instead
197
+ xml, csv, tsv the engine has no operation that writes the SPARQL
198
+ Results XML, CSV or TSV documents, and writing one
199
+ here would be a second serializer (iron rule 7)
200
+ DESCRIBE the engine answers "DESCRIBE is not supported by the
201
+ npm entry yet"
202
+
203
+ A store carrying a non-empty delta log is not served by this path: the
204
+ operation reads the manifest's committed artifacts only. Use the native
205
+ l4block-* tools for a store with uncompacted updates.`,
206
+
207
+ pack: `factoidal pack - build one immutable generation from an RDF file
208
+
209
+ usage: factoidal pack INPUT OUTPUT [options]
210
+
211
+ INPUT is an RDF file. OUTPUT is the generation directory to create; it is
212
+ normally STORE/gen-N. Packing does not activate: run activate next.
213
+
214
+ options:
215
+ --layout LAYOUT ibk3 (triples, default) or ibk4 (quads)
216
+ --syntax SYNTAX turtle, trig or nquads; default from the file extension
217
+ --chunk-bytes N Merkle chunk size; default is the engine's
218
+ --json emit one JSON object
219
+ --no-worker pack in this process instead of on a worker thread
220
+
221
+ The pack fold recurses deeper than either runtime's default call stack
222
+ allows, so it runs on a worker thread with a raised stack under Node, and
223
+ under Deno the command re-executes itself once with a raised V8 stack
224
+ (https://github.com/danbri/factoidal/issues/649). --no-worker turns both
225
+ off; a pack above about half a megabyte of input then overflows.`,
226
+
227
+ activate: `factoidal activate - make one generation the activated generation
228
+
229
+ usage: factoidal activate STORE GENERATION [--json]
230
+
231
+ Verifies every artifact of GENERATION and every cross-artifact relation,
232
+ then atomically replaces STORE/CURRENT. A generation that fails
233
+ verification never becomes current.`,
234
+
235
+ update: `factoidal update - apply a SPARQL Update through the delta log
236
+
237
+ usage: factoidal update STORE [UPDATE] [options]
238
+
239
+ UPDATE is the update text. Give it as the second argument, or with
240
+ --update, or in a file with --file. The batch is appended to the
241
+ activated generation's delta log and is visible to the next query.
242
+
243
+ options:
244
+ --update TEXT the update text
245
+ --file PATH read the update text from a file
246
+ --json emit one JSON object`,
247
+
248
+ compact: `factoidal compact - fold the delta log into a new generation
249
+
250
+ usage: factoidal compact STORE GENERATION [options]
251
+
252
+ Reads the activated generation and its committed delta batches, writes
253
+ GENERATION as a new immutable generation under STORE, and records its
254
+ compacted epoch. It does not activate unless --activate is given.
255
+
256
+ options:
257
+ --activate activate the new generation when it verifies
258
+ --json emit one JSON object`
259
+ }
260
+
261
+ // ------------------------------------------------------------- arguments
262
+
263
+ /**
264
+ * Split argv into positional arguments and options. `--name value` and
265
+ * `--name=value` both work; a flag with no value is `true`.
266
+ */
267
+ function parseArguments (args, valueOptions) {
268
+ const positional = []
269
+ const options = Object.create(null)
270
+ for (let index = 0; index < args.length; index += 1) {
271
+ const argument = args[index]
272
+ if (argument === '--') {
273
+ positional.push(...args.slice(index + 1))
274
+ break
275
+ }
276
+ if (argument === '-h') {
277
+ options.help = true
278
+ continue
279
+ }
280
+ if (!argument.startsWith('--')) {
281
+ positional.push(argument)
282
+ continue
283
+ }
284
+ const equals = argument.indexOf('=')
285
+ const name = equals < 0 ? argument.slice(2) : argument.slice(2, equals)
286
+ if (equals >= 0) {
287
+ options[name] = argument.slice(equals + 1)
288
+ continue
289
+ }
290
+ if (valueOptions.has(name)) {
291
+ index += 1
292
+ if (index >= args.length) {
293
+ throw new UsageError(`option --${name} needs a value`)
294
+ }
295
+ options[name] = args[index]
296
+ continue
297
+ }
298
+ options[name] = true
299
+ }
300
+ return { positional, options }
301
+ }
302
+
303
+ class UsageError extends Error {}
304
+
305
+ const VALUE_OPTIONS = {
306
+ version: new Set([]),
307
+ 'sample-store': new Set([]),
308
+ inspect: new Set(['generation']),
309
+ query: new Set(['query', 'file', 'format', 'limit', 'base', 'generation']),
310
+ pack: new Set(['layout', 'syntax', 'chunk-bytes', 'base']),
311
+ activate: new Set([]),
312
+ update: new Set(['update', 'file']),
313
+ compact: new Set([])
314
+ }
315
+
316
+ // ------------------------------------------------------------- commands
317
+
318
+ function packageDirectory () {
319
+ return fileUrlToPath(new URL('..', import.meta.url).href)
320
+ }
321
+
322
+ function readJsonFile (path) {
323
+ const bytes = readWhole(path)
324
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
325
+ }
326
+
327
+ function commandVersion (options) {
328
+ const root = packageDirectory()
329
+ const own = readJsonFile(joinPath(root, 'package.json'))
330
+ let engine = null
331
+ try {
332
+ engine = readJsonFile(joinPath(root, 'l4-assets/version.json'))
333
+ } catch (_error) {
334
+ engine = null
335
+ }
336
+ const report = {
337
+ package: own.name,
338
+ version: own.version,
339
+ host: runtime,
340
+ engine: engine === null
341
+ ? null
342
+ : {
343
+ engine: engine.engine,
344
+ version: engine.version,
345
+ gitSha: engine.gitSha,
346
+ leanToolchain: engine.leanToolchain,
347
+ wasmSha256: engine.wasmSha256,
348
+ wasmBytes: engine.wasmBytes
349
+ }
350
+ }
351
+ if (options.json) {
352
+ out(JSON.stringify(report, null, 2))
353
+ return EXIT_OK
354
+ }
355
+ out(`${report.package} ${report.version}`)
356
+ out(`host-io ${report.host}`)
357
+ if (report.engine !== null) {
358
+ out(`engine ${report.engine.engine} ${report.engine.version} (${report.engine.leanToolchain})`)
359
+ out(`wasm sha256 ${report.engine.wasmSha256} (${report.engine.wasmBytes} bytes)`)
360
+ } else {
361
+ out('engine (no l4-assets/version.json in this install)')
362
+ }
363
+ return EXIT_OK
364
+ }
365
+
366
+ function commandSampleStore (options) {
367
+ const path = sampleStorePath()
368
+ if (options.json === true) {
369
+ out(JSON.stringify({ path, ...sampleStoreFacts }, null, 2))
370
+ return EXIT_OK
371
+ }
372
+ out(path)
373
+ return EXIT_OK
374
+ }
375
+
376
+ // ------------------------------------------------------------ rendering
377
+
378
+ /**
379
+ * One graph name as the manifest reports it. `{"kind":"default"}` is the
380
+ * default graph; the operation marks it that way so a host never has to
381
+ * recognise a reserved IRI.
382
+ */
383
+ function graphLabel (graph) {
384
+ if (graph.kind === 'default') return 'default'
385
+ if (graph.kind === 'iri') return `<${graph.value}>`
386
+ if (graph.kind === 'bnode') return `_:${graph.value}`
387
+ return JSON.stringify(graph)
388
+ }
389
+
390
+ /**
391
+ * One SPARQL Query Results JSON term as a table cell. This is a display,
392
+ * not a serialization: `--format json` prints the engine's own document
393
+ * and `--format nquads` prints the engine's own N-Triples.
394
+ */
395
+ function cellOfTerm (term) {
396
+ if (term === undefined || term === null) return ''
397
+ if (term.type === 'uri') return `<${term.value}>`
398
+ if (term.type === 'bnode') return `_:${term.value}`
399
+ if (term.type === 'literal') {
400
+ const lang = term['xml:lang']
401
+ if (typeof lang === 'string' && lang.length > 0) return `"${term.value}"@${lang}`
402
+ if (typeof term.datatype === 'string') return `"${term.value}"^^<${term.datatype}>`
403
+ return `"${term.value}"`
404
+ }
405
+ return JSON.stringify(term)
406
+ }
407
+
408
+ function plural (count, noun) {
409
+ return `${count} ${noun}${count === 1 ? '' : 's'}`
410
+ }
411
+
412
+ /** Print rows as columns padded to their widest cell. */
413
+ function printTable (headers, rows) {
414
+ const widths = headers.map((header) => header.length)
415
+ for (const row of rows) {
416
+ for (let index = 0; index < row.length; index += 1) {
417
+ if (row[index].length > widths[index]) widths[index] = row[index].length
418
+ }
419
+ }
420
+ const line = (cells) => cells
421
+ .map((cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index]))
422
+ .join(' ')
423
+ out(line(headers))
424
+ for (const row of rows) out(line(row))
425
+ }
426
+
427
+ // ------------------------------------------------------------- inspect
428
+
429
+ async function commandInspect (positional, options) {
430
+ if (positional.length !== 1) throw new UsageError('inspect needs exactly one STORE')
431
+ const root = positional[0]
432
+ const named = typeof options.generation === 'string' ? options.generation : null
433
+ const store = openStore(root, named)
434
+ const engine = await loadEngine()
435
+ const envelope = inspectManifest(engine, store)
436
+
437
+ if (options.json) {
438
+ // The operation's envelope, unchanged.
439
+ out(JSON.stringify(envelope, null, 2))
440
+ return EXIT_OK
441
+ }
442
+
443
+ const files = listGeneration(store.generationDir)
444
+ let directoryBytes = 0
445
+ for (const file of files) directoryBytes += file.size
446
+
447
+ out(`store ${root}`)
448
+ out(`generation ${store.generation}${store.activated ? ' (activated through CURRENT)' : ' (named on the command line)'}`)
449
+ out(`manifest ${store.manifestName}, ${store.manifest.length} bytes, wire version ${envelope.wireVersion}`)
450
+ out(`layout ${envelope.layout}`)
451
+ out(`blank-node profile ${envelope.blankNodeProfile === '' ? '(none recorded)' : envelope.blankNodeProfile}`)
452
+ out(`term registry ${envelope.termRegistryVersion === '' ? '(none recorded)' : envelope.termRegistryVersion}`)
453
+ out(`fixed-chunk Merkle commitment ${envelope.rangeCommitted ? 'yes' : 'no'}`)
454
+ out(`${envelope.entries.length} ${envelope.entries.length === 1 ? 'entry' : 'entries'}, ${envelope.totalBytes} bytes, ${plural(envelope.totalRows, 'row')}`)
455
+ out(`generation directory holds ${files.length} files, ${directoryBytes} bytes`)
456
+ out('')
457
+ printTable(
458
+ ['#', 'rows', 'bytes', 'kind', 'graphs', 'predicate'],
459
+ envelope.entries.map((entry) => [
460
+ String(entry.ordinal),
461
+ String(entry.rows),
462
+ String(entry.bytes),
463
+ entry.blockKind,
464
+ entry.graphs.length === 0 ? '-' : entry.graphs.map(graphLabel).join(' '),
465
+ entry.predicate
466
+ ]))
467
+ return EXIT_OK
468
+ }
469
+
470
+ function notWired (command, detail) {
471
+ err(`factoidal ${command}: not yet wired. ${detail}`)
472
+ err(`This needs a WebAssembly operation from the Lean engine; see ${ISSUE}.`)
473
+ return EXIT_NOT_WIRED
474
+ }
475
+
476
+ const QUERY_FORMATS = ['table', 'json', 'nquads', 'turtle']
477
+ const UNAVAILABLE_FORMATS = {
478
+ xml: 'the SPARQL Results XML document',
479
+ csv: 'the SPARQL Results CSV document',
480
+ tsv: 'the SPARQL Results TSV document'
481
+ }
482
+
483
+ function queryText (positional, options) {
484
+ if (typeof options.file === 'string') {
485
+ return new TextDecoder('utf-8', { fatal: true }).decode(readWhole(options.file))
486
+ }
487
+ if (positional.length > 1) return positional.slice(1).join(' ')
488
+ if (typeof options.query === 'string') return options.query
489
+ throw new UsageError('query needs QUERY, --query TEXT or --file PATH')
490
+ }
491
+
492
+ function queryFormat (options) {
493
+ if (options.json === true && typeof options.format !== 'string') return 'json'
494
+ if (typeof options.format !== 'string') return 'table'
495
+ const format = options.format.toLowerCase()
496
+ if (QUERY_FORMATS.indexOf(format) >= 0) return format
497
+ if (Object.prototype.hasOwnProperty.call(UNAVAILABLE_FORMATS, format)) {
498
+ throw new UsageError(
499
+ `--format ${format} needs an engine operation that writes ` +
500
+ `${UNAVAILABLE_FORMATS[format]}; there is none, and writing one here ` +
501
+ 'would be a second serializer. Use --format json.')
502
+ }
503
+ throw new UsageError(`--format ${options.format} is not one of ${QUERY_FORMATS.join(', ')}`)
504
+ }
505
+
506
+ function queryLimit (options, format) {
507
+ if (typeof options.limit !== 'string') return null
508
+ const limit = Number(options.limit)
509
+ if (!Number.isSafeInteger(limit) || limit < 0) {
510
+ throw new UsageError('--limit needs a non-negative whole number')
511
+ }
512
+ if (format !== 'table') {
513
+ throw new UsageError(
514
+ '--limit truncates the printed table only; it is not carried into the ' +
515
+ "engine's own documents. Put a LIMIT clause in the query instead.")
516
+ }
517
+ return limit
518
+ }
519
+
520
+ /** Report a refusal the store operations made, and what to do about it. */
521
+ function reportStoreFailure (error) {
522
+ err(`factoidal query: ${error.message}`)
523
+ if (error.capLimit !== null) {
524
+ err(`This query needs more of the store than one WebAssembly call may read: ${error.capValue} against a cap of ${error.capLimit}.`)
525
+ err('Narrow the query - bind a predicate, or restrict the graph - or use the native l4block-* tools.')
526
+ } else if (error.stackLimit) {
527
+ for (const line of stackLimitAdvice(STACK_REMEDY.query)) err(line)
528
+ } else if (error.digestKey !== null) {
529
+ err(`The bytes of '${error.digestKey}' in the generation directory are not the bytes the manifest commits.`)
530
+ err('The generation is damaged or was edited after it was packed; repack or restore it.')
531
+ }
532
+ return EXIT_FAILURE
533
+ }
534
+
535
+ async function commandQuery (positional, options) {
536
+ if (positional.length < 1) throw new UsageError('query needs a STORE')
537
+ if (typeof options.base === 'string') {
538
+ throw new UsageError(
539
+ 'the store query operation takes no base argument; put a BASE clause ' +
540
+ 'in the query text instead')
541
+ }
542
+ const root = positional[0]
543
+ const sparql = queryText(positional, options)
544
+ const format = queryFormat(options)
545
+ const limit = queryLimit(options, format)
546
+ const quiet = options.quiet === true
547
+
548
+ const named = typeof options.generation === 'string' ? options.generation : null
549
+ const store = openStore(root, named)
550
+ const engine = await loadEngine()
551
+
552
+ if (options.explain === true) {
553
+ let plan
554
+ try {
555
+ plan = planQuery(engine, store, sparql)
556
+ } catch (error) {
557
+ if (error instanceof StoreOperationError) return reportStoreFailure(error)
558
+ throw error
559
+ }
560
+ if (format === 'json' || options.json === true) {
561
+ out(JSON.stringify(plan, null, 2))
562
+ return EXIT_OK
563
+ }
564
+ out(`layout ${plan.layout} (wire version ${plan.wireVersion})`)
565
+ out(`mode ${plan.mode}`)
566
+ out(`${plural(plan.shards, 'artifact')}, ${plan.bytes} bytes, ${plural(plan.rows, 'row')}`)
567
+ for (const key of plan.keys) out(` ${key}`)
568
+ return EXIT_OK
569
+ }
570
+
571
+ let answer
572
+ try {
573
+ answer = queryStore(engine, store, sparql)
574
+ } catch (error) {
575
+ if (error instanceof StoreOperationError) return reportStoreFailure(error)
576
+ throw error
577
+ }
578
+ const { plan, result, blobBytes } = answer
579
+ if (!quiet) {
580
+ err(`mode ${result.mode}, ${plural(result.shards, 'artifact')}, ${blobBytes} bytes read, plan declares ${plural(plan.rows, 'block row')}`)
581
+ }
582
+ return renderQueryResult(engine, result, format, limit, quiet)
583
+ }
584
+
585
+ function renderQueryResult (engine, result, format, limit, quiet) {
586
+ if (result.kind === 'select') {
587
+ const vars = result.srj.head.vars
588
+ const bindings = result.srj.results.bindings
589
+ if (format === 'json') {
590
+ out(JSON.stringify(result.srj, null, 2))
591
+ return EXIT_OK
592
+ }
593
+ if (format !== 'table') {
594
+ err(`factoidal query: --format ${format} needs a CONSTRUCT query; this one is a SELECT`)
595
+ return EXIT_FAILURE
596
+ }
597
+ const shown = limit === null ? bindings : bindings.slice(0, limit)
598
+ printTable(vars, shown.map((row) => vars.map((name) => cellOfTerm(row[name]))))
599
+ if (shown.length < bindings.length) {
600
+ err(`showing ${shown.length} of ${bindings.length} rows (--limit ${limit})`)
601
+ } else if (!quiet) {
602
+ err(plural(bindings.length, 'row'))
603
+ }
604
+ return EXIT_OK
605
+ }
606
+ if (result.kind === 'ask') {
607
+ if (format === 'json') {
608
+ // The operation answers ASK with a boolean, not with a results
609
+ // document, so the envelope is what there is to print.
610
+ out(JSON.stringify(result, null, 2))
611
+ return EXIT_OK
612
+ }
613
+ if (format !== 'table') {
614
+ err(`factoidal query: --format ${format} needs a CONSTRUCT query; this one is an ASK`)
615
+ return EXIT_FAILURE
616
+ }
617
+ out(result.boolean ? 'true' : 'false')
618
+ return EXIT_OK
619
+ }
620
+ if (result.kind === 'construct') {
621
+ if (format === 'json') {
622
+ out(JSON.stringify(result, null, 2))
623
+ return EXIT_OK
624
+ }
625
+ if (format === 'turtle') {
626
+ out(turtleOfNQuads(engine, result.nquads))
627
+ return EXIT_OK
628
+ }
629
+ // table and nquads both print what the engine serialized.
630
+ if (result.nquads.length > 0) out(result.nquads.replace(/\n$/, ''))
631
+ return EXIT_OK
632
+ }
633
+ err(`factoidal query: the engine answered an unknown result kind "${result.kind}"`)
634
+ return EXIT_FAILURE
635
+ }
636
+
637
+ /** The syntax tag for an input, from --syntax or from the file name. The
638
+ * engine is what actually decides how to read the bytes; this only picks
639
+ * which of its parsers to name. */
640
+ function packSyntax (input, options) {
641
+ if (typeof options.syntax === 'string') {
642
+ const syntax = options.syntax.toLowerCase()
643
+ if (PACK_SYNTAXES.indexOf(syntax) >= 0) return syntax
644
+ throw new UsageError(`--syntax ${options.syntax} is not one of ${PACK_SYNTAXES.join(', ')}`)
645
+ }
646
+ const lower = input.toLowerCase()
647
+ for (const [suffix, syntax] of PACK_SUFFIXES) {
648
+ if (lower.endsWith(suffix)) return syntax
649
+ }
650
+ for (const [suffix, name] of PACK_UNSUPPORTED_SUFFIXES) {
651
+ if (lower.endsWith(suffix)) {
652
+ throw new UsageError(
653
+ `pack does not read ${name}. The packer's streaming fold reads ` +
654
+ `${PACK_SYNTAXES.join(', ')} only. Convert the file first, for ` +
655
+ "example with: factoidal parse FILE --out nquads")
656
+ }
657
+ }
658
+ throw new UsageError(
659
+ `cannot tell the syntax of ${input} from its name; give --syntax ` +
660
+ `(${PACK_SYNTAXES.join(', ')})`)
661
+ }
662
+
663
+ /**
664
+ * The base IRI relative IRIs in the source resolve against.
665
+ *
666
+ * The native packer uses `file://<input>`, so this matches it by default
667
+ * and byte-identical output needs no flag. `--base` overrides it, and
668
+ * `--base ''` asks for no base, which turns a relative IRI into a parse
669
+ * error rather than a silently different term.
670
+ */
671
+ function packBase (input, options) {
672
+ if (typeof options.base === 'string') return options.base
673
+ const absolute = input.startsWith('/') ? input : joinPath(currentDirectory(), input)
674
+ return 'file://' + absolute
675
+ }
676
+
677
+ /** The process's working directory, on Node and on Deno. */
678
+ function currentDirectory () {
679
+ if (isDeno) return globalThis.Deno.cwd()
680
+ return process.cwd()
681
+ }
682
+
683
+ function packLayout (options) {
684
+ if (typeof options.layout !== 'string') return 'ibk3'
685
+ const layout = options.layout.toLowerCase()
686
+ if (PACK_LAYOUTS.indexOf(layout) >= 0) return layout
687
+ throw new UsageError(`--layout ${options.layout} is not one of ${PACK_LAYOUTS.join(', ')}`)
688
+ }
689
+
690
+ async function commandPack (positional, options) {
691
+ if (positional.length !== 2) throw new UsageError('pack needs INPUT and OUTPUT')
692
+ const [input, output] = positional
693
+ const syntax = packSyntax(input, options)
694
+ const layout = packLayout(options)
695
+ // The pack fold needs a bigger call stack than either runtime gives by
696
+ // default (https://github.com/danbri/factoidal/issues/649). Under Node
697
+ // the work runs on a worker thread with a raised stack; under Deno the
698
+ // command re-executes itself once with --v8-flags=--stack-size, and
699
+ // this is where that happens, before any file is opened. --no-worker
700
+ // keeps the in-process path testable.
701
+ const host = { worker: options['no-worker'] !== true }
702
+ const reexec = await denoReexec(host)
703
+ if (reexec !== null) return reexec
704
+ makeDirectory(output)
705
+ const quiet = options.quiet === true
706
+ let answer
707
+ try {
708
+ answer = await runPack(
709
+ { kind: 'pack', input, output, syntax, layout, base: packBase(input, options) },
710
+ quiet
711
+ ? undefined
712
+ : (progress) => {
713
+ if (progress.bytesRead % (16 * 1024 * 1024) < FEED_PROGRESS) {
714
+ err(`${progress.pass}: ${progress.bytesRead} bytes read, ${progress.artifacts} artifacts written`)
715
+ }
716
+ },
717
+ host)
718
+ } catch (error) {
719
+ if (error instanceof PackError || error instanceof StoreHostError) {
720
+ err(`factoidal pack: ${error.message}`)
721
+ return EXIT_FAILURE
722
+ }
723
+ // Everything the engine refuses -- an unknown grammar tag, a parse
724
+ // error, a cap -- arrives as a plain Error carrying the engine's own
725
+ // words. A stack trace here would hide them.
726
+ if (error instanceof Error && typeof error.message === 'string') {
727
+ err(`factoidal pack: ${error.message.replace(/^l4factoidal:\s*/, '')}`)
728
+ // The raised stack was refused, unavailable, or still not enough.
729
+ if (isStackOverflow(error)) {
730
+ for (const line of stackLimitAdvice(STACK_REMEDY.pack)) err(line)
731
+ }
732
+ return EXIT_FAILURE
733
+ }
734
+ throw error
735
+ }
736
+ if (answer.notWired === true) {
737
+ return notWired('pack',
738
+ 'This install carries an engine built before the streaming pack ' +
739
+ 'operations. Update @factoidal/core, or set FACTOIDAL_L4_ASSETS to ' +
740
+ 'a newer build.')
741
+ }
742
+ const report = answer.report
743
+ if (options.json === true) {
744
+ out(JSON.stringify(report, null, 2))
745
+ return EXIT_OK
746
+ }
747
+ out(`packed ${report.bytesRead} bytes of ${syntax} into ${output}`)
748
+ out(`${report.written.length} artifacts, ${report.bytesWritten} bytes, layout ${layout}`)
749
+ if (typeof report.rows === 'number') out(`${plural(report.rows, 'row')}`)
750
+ out(`activate it with: factoidal activate ${dirOf(output)} ${nameOf(output)}`)
751
+ return EXIT_OK
752
+ }
753
+
754
+ async function commandActivate (positional, options) {
755
+ if (positional.length !== 2) throw new UsageError('activate needs STORE and GENERATION')
756
+ const [root, generation] = positional
757
+ // Verification decodes the same blocks the pack encoded, so it recurses
758
+ // as deep and needs the same raised stack. Measured 2026-09-04: a
759
+ // 112,742-row generation packed successfully and then failed to
760
+ // activate with `Maximum call stack size exceeded`, leaving a store
761
+ // that could be built and not opened
762
+ // (https://github.com/danbri/factoidal/issues/649).
763
+ const host = { worker: options['no-worker'] !== true }
764
+ const reexec = await denoReexec(host)
765
+ if (reexec !== null) return reexec
766
+ let answer
767
+ try {
768
+ answer = await runPack({ kind: 'activate', root, generation }, undefined, host)
769
+ } catch (error) {
770
+ if (error instanceof PackError || error instanceof StoreHostError) {
771
+ err(`factoidal activate: ${error.code ? error.code + ': ' : ''}${error.message}`)
772
+ return EXIT_FAILURE
773
+ }
774
+ if (error instanceof Error && typeof error.message === 'string') {
775
+ err(`factoidal activate: ${error.message.replace(/^l4factoidal:\s*/, '')}`)
776
+ if (isStackOverflow(error)) {
777
+ for (const line of stackLimitAdvice(STACK_REMEDY.pack)) err(line)
778
+ }
779
+ return EXIT_FAILURE
780
+ }
781
+ throw error
782
+ }
783
+ if (answer.notWired === true) {
784
+ return notWired('activate',
785
+ 'This install carries an engine built before the activation ' +
786
+ 'verification operation. Update @factoidal/core.')
787
+ }
788
+ const verdict = answer.report
789
+ if (verdict.ok !== true) {
790
+ err(`factoidal activate: ${verdict.error}`)
791
+ err('The generation is NOT activated; CURRENT is unchanged.')
792
+ return EXIT_FAILURE
793
+ }
794
+ // Only now does the pointer move, and it moves atomically.
795
+ const pointer = new TextEncoder().encode(generation)
796
+ const synced = atomicReplace(joinPath(root, 'CURRENT'), pointer)
797
+ if (options.json === true) {
798
+ out(JSON.stringify({ ...verdict, generation, directorySynced: synced }, null, 2))
799
+ return EXIT_OK
800
+ }
801
+ out(`activated ${generation}: ${verdict.artifacts} artifacts verified, ${verdict.bytes} bytes`)
802
+ if (!synced) {
803
+ err('CURRENT was replaced, but the directory entry was not synced; a ' +
804
+ 'crash now could lose the pointer update.')
805
+ }
806
+ return EXIT_OK
807
+ }
808
+
809
+ function commandUpdate (positional, options) {
810
+ if (positional.length < 1) throw new UsageError('update needs a STORE')
811
+ const text = positional.length > 1
812
+ ? positional.slice(1).join(' ')
813
+ : (typeof options.update === 'string' ? options.update : null)
814
+ if (text === null && typeof options.file !== 'string') {
815
+ throw new UsageError('update needs UPDATE, --update TEXT or --file PATH')
816
+ }
817
+ return notWired('update', 'The delta-log operations are stage 4 of the milestone.')
818
+ }
819
+
820
+ function commandCompact (positional, _options) {
821
+ if (positional.length !== 2) throw new UsageError('compact needs STORE and GENERATION')
822
+ return notWired('compact', 'Compaction is stage 4 of the milestone.')
823
+ }
824
+
825
+ const COMMANDS = {
826
+ version: (positional, options) => commandVersion(options),
827
+ 'sample-store': (positional, options) => commandSampleStore(options),
828
+ inspect: commandInspect,
829
+ query: commandQuery,
830
+ pack: commandPack,
831
+ activate: commandActivate,
832
+ update: commandUpdate,
833
+ compact: commandCompact
834
+ }
835
+
836
+ // ----------------------------------------------------------------- main
837
+
838
+ async function main () {
839
+ if (argv.length === 0) {
840
+ out(USAGE)
841
+ return EXIT_USAGE
842
+ }
843
+ const command = argv[0]
844
+ if (command === '--help' || command === '-h' || command === 'help') {
845
+ const topic = argv[1]
846
+ if (typeof topic === 'string' && Object.prototype.hasOwnProperty.call(COMMAND_USAGE, topic)) {
847
+ out(COMMAND_USAGE[topic])
848
+ } else {
849
+ out(USAGE)
850
+ }
851
+ return EXIT_OK
852
+ }
853
+ if (command === '--version') return commandVersion({})
854
+ if (!Object.prototype.hasOwnProperty.call(COMMANDS, command)) {
855
+ err(`factoidal: unknown command "${command}"`)
856
+ err(USAGE)
857
+ return EXIT_USAGE
858
+ }
859
+ let parsed
860
+ try {
861
+ parsed = parseArguments(argv.slice(1), VALUE_OPTIONS[command])
862
+ } catch (error) {
863
+ if (error instanceof UsageError) {
864
+ err(`factoidal ${command}: ${error.message}`)
865
+ return EXIT_USAGE
866
+ }
867
+ throw error
868
+ }
869
+ if (parsed.options.help) {
870
+ out(COMMAND_USAGE[command])
871
+ return EXIT_OK
872
+ }
873
+ try {
874
+ return await COMMANDS[command](parsed.positional, parsed.options)
875
+ } catch (error) {
876
+ if (error instanceof UsageError) {
877
+ err(`factoidal ${command}: ${error.message}`)
878
+ err('')
879
+ err(COMMAND_USAGE[command])
880
+ return EXIT_USAGE
881
+ }
882
+ if (error instanceof StoreHostError) {
883
+ err(`factoidal ${command}: ${error.code}: ${error.message}`)
884
+ return EXIT_FAILURE
885
+ }
886
+ if (error instanceof StoreOperationError) {
887
+ err(`factoidal ${command}: ${error.message}`)
888
+ return EXIT_FAILURE
889
+ }
890
+ throw error
891
+ }
892
+ }
893
+
894
+ exit(await main())