aontu 0.64.0 → 0.66.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 (64) hide show
  1. package/README.md +2 -2
  2. package/dist/agentsmd.js +3 -3
  3. package/dist/agentsmd.js.map +1 -1
  4. package/dist/aontu.d.ts +1 -1
  5. package/dist/aontu.js +1 -1
  6. package/dist/cli.d.ts +16 -2
  7. package/dist/cli.js +741 -137
  8. package/dist/cli.js.map +1 -1
  9. package/dist/compat.d.ts +7 -0
  10. package/dist/compat.js +113 -0
  11. package/dist/compat.js.map +1 -0
  12. package/dist/helpdoc.js +5 -5
  13. package/dist/helpdoc.js.map +1 -1
  14. package/dist/hints.js +11 -5
  15. package/dist/hints.js.map +1 -1
  16. package/dist/lang.d.ts +1 -0
  17. package/dist/lang.js +8 -0
  18. package/dist/lang.js.map +1 -1
  19. package/dist/lsp.d.ts +1 -1
  20. package/dist/mod.d.ts +18 -3
  21. package/dist/mod.js +117 -46
  22. package/dist/mod.js.map +1 -1
  23. package/dist/pkg-net.d.ts +227 -0
  24. package/dist/pkg-net.js +1598 -0
  25. package/dist/pkg-net.js.map +1 -0
  26. package/dist/pkg-zip.d.ts +8 -0
  27. package/dist/pkg-zip.js +196 -0
  28. package/dist/pkg-zip.js.map +1 -0
  29. package/dist/pkg.d.ts +159 -0
  30. package/dist/pkg.js +767 -0
  31. package/dist/pkg.js.map +1 -0
  32. package/dist/report-sarif.js +1 -1
  33. package/dist/report-sarif.js.map +1 -1
  34. package/dist/subsume.d.ts +3 -1
  35. package/dist/subsume.js +2 -0
  36. package/dist/subsume.js.map +1 -1
  37. package/dist/tsconfig.tsbuildinfo +1 -1
  38. package/grammar/aontu.gbnf +8 -18
  39. package/grammar/aontu.lark +6 -14
  40. package/grammar/aontu.tmLanguage.json +4 -6
  41. package/package.json +3 -2
  42. package/skill/SKILL.md +3 -3
  43. package/skill/error-codes.md +4 -4
  44. package/skill/examples.md +4 -4
  45. package/skill/grammar-card.md +1 -1
  46. package/skill/init/check.sh +2 -2
  47. package/skill/tasks.md +17 -12
  48. package/src/agentsmd.ts +3 -3
  49. package/src/aontu.ts +1 -1
  50. package/src/cli.ts +851 -151
  51. package/src/compat.ts +148 -0
  52. package/src/helpdoc.ts +5 -5
  53. package/src/hints.ts +11 -7
  54. package/src/lang.ts +9 -1
  55. package/src/mod.ts +142 -51
  56. package/src/pkg-net.ts +1945 -0
  57. package/src/pkg-zip.ts +228 -0
  58. package/src/pkg.ts +1018 -0
  59. package/src/report-sarif.ts +1 -1
  60. package/src/subsume.ts +2 -2
  61. package/dist/mod-tool.d.ts +0 -58
  62. package/dist/mod-tool.js +0 -418
  63. package/dist/mod-tool.js.map +0 -1
  64. package/src/mod-tool.ts +0 -578
package/src/cli.ts CHANGED
@@ -5,8 +5,10 @@
5
5
  import { evalFailure } from './query'
6
6
  // __importStar downlevel helper, whose branches no supported Node takes.
7
7
  import {
8
- existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync,
8
+ existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync,
9
+ writeFileSync,
9
10
  } from 'node:fs'
11
+ import type { Dirent } from 'node:fs'
10
12
  import { basename, dirname, join, resolve } from 'node:path'
11
13
  import { tmpdir } from 'node:os'
12
14
  import { createInterface } from 'node:readline'
@@ -29,11 +31,23 @@ import { sarifReport } from './report-sarif'
29
31
  import { main as lspMain } from './lsp-server'
30
32
  import { main as mcpMain } from './mcp-server'
31
33
  import { jsonSchema } from './jsonschema'
32
- import { modTidy, modVerify, modVendor, modManifest } from './mod-tool'
34
+ import {
35
+ pkgTidy, pkgVerify, pkgVendor, pkgManifest, pkgRefreeze, pkgTree,
36
+ versionCompare,
37
+ } from './pkg'
38
+ import {
39
+ pkgSync, pkgGet, pkgRemove, pkgWhy, pkgPublish, pkgOutdated, startServe,
40
+ defaultHttp, keygen, PUBLISH_PATH,
41
+ } from './pkg-net'
42
+ import type {
43
+ PkgSyncReport, PkgChangeReport, PkgWhyReport, PkgPublishReport,
44
+ PkgOutdatedReport, Served, PkgHttp,
45
+ } from './pkg-net'
33
46
  import type {
34
- ModTidyReport, ModVerifyReport, ModVendorReport, ModManifestReport,
35
- } from './mod-tool'
36
- import { modCacheDir } from './mod'
47
+ PkgTidyReport, PkgVerifyReport, PkgVendorReport, PkgManifestReport,
48
+ PkgRefreezeReport, PkgTreeReport, PkgToolOptions,
49
+ } from './pkg'
50
+ import { modCacheDir, PKG_FILE, LOCK_FILE, META_DIR } from './mod'
37
51
  import { VET_MAX_ERRORS } from './vet'
38
52
  import type {
39
53
  VetReport, VetFinding, VetVerdict, VetCoverage,
@@ -85,11 +99,22 @@ const HELP = `Usage: aontu [options] [file]
85
99
  [--profile <file>] <file>
86
100
  aontu trace [--at <path>] [--format json] [--marker <token>]
87
101
  [--profile <file>] <file>
102
+ aontu render [--check] [--at <path>] [--format json]
103
+ [--marker <token>] [--profile <file>]
104
+ <file|folder> <path>
88
105
  aontu hash [options] <file>
89
- aontu mod tidy|verify|vendor|manifest [options] [dir]
90
- aontu get <path> [options] <file>
91
- aontu why <path> [options] <file>
92
- aontu set <path>=<value>... --entry <file> --overlay <file>
106
+ aontu sync [--frozen] [options] [dir]
107
+ aontu add <pkg>[@<version>] [options] [dir]
108
+ aontu get <pkg>[@<version>] [options] [dir]
109
+ aontu remove <pkg> [options] [dir]
110
+ aontu why <pkg> [options] [dir]
111
+ aontu publish [--yes] [--to <dir>] [options] [dir]
112
+ aontu pkg tidy|verify|vendor|manifest|refreeze|tree|outdated|serve
113
+ [options] [dir]
114
+ aontu pkg keygen <file>
115
+ aontu model get <path> [options] <file>
116
+ aontu model why <path> [options] <file>
117
+ aontu model set <path>=<value>... --entry <file> --overlay <file>
93
118
  aontu allow --role <role> [--at <path>] <roles-file> <path>...
94
119
  aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file>
95
120
  aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>]
@@ -158,18 +183,57 @@ Options:
158
183
  named format keeps its meaning, and .js stays
159
184
  refused. Every verb takes it
160
185
 
161
- Mod options:
186
+ Package verbs (a module is imported; a package is published):
187
+ sync Make the project correct: resolve by minimum version
188
+ selection, fetch what is missing, write aontu_meta/pkg-lock.aon,
189
+ vendor, verify. --frozen refuses to change the lockfile
190
+ add Take on a dependency the project does not have, then sync;
191
+ refuses one it has and names get
192
+ get Add a dependency or raise its minimum, then sync. Without a
193
+ version, the newest version outside the cooldown
194
+ remove Drop a dependency, then sync
195
+ why Why is this package in the closure: every path of
196
+ requirements that reaches it
197
+ publish Publish this package. A dry run without --yes; --to <dir>
198
+ writes the repository layout into a directory instead
199
+
200
+ Package options:
162
201
  --format <f> text (default) or json
163
- --against <dir> manifest: a prior version's module tree, to gate on
164
-
165
- Mod subcommands:
166
- tidy Resolve the module closure by minimum version selection and
167
- rewrite aontu_meta/mod-lock.aon in canonical form
168
- verify Check every locked module still means what the lockfile
169
- pins, and change nothing (the CI gate; tidy rewrites)
202
+ --frozen sync: fail if the lockfile would change (the CI mode)
203
+ --yes publish: send it (the default is a dry run)
204
+ --to <dir> publish: write the read-path layout into a directory
205
+ --key <file> publish: the Ed25519 private key (PKCS#8 PEM) that
206
+ signs the manifest, for the key provider
207
+ --token <file> publish: the forge's OIDC token, read from a file
208
+ --base <url> the repository to read from (repeatable; overrides
209
+ pkg.aon repo.base)
210
+ --write <url> publish: the write path (overrides pkg.aon repo.write)
211
+ --against <dir> manifest: a prior version's tree, to gate on
212
+ --upstream <u> serve: fetch on miss from this repository (repeatable)
213
+ --listen <a> serve: the address to listen on (default 127.0.0.1:8017)
214
+
215
+ pkg subcommands (the rest of the package operations):
216
+ tidy Resolve the closure by minimum version selection and
217
+ rewrite aontu_meta/pkg-lock.aon in canonical form
218
+ verify Check every locked package still is and still means what
219
+ the lockfile pins, bytes before meaning, and change nothing
220
+ (the CI gate; tidy rewrites)
170
221
  vendor Materialise the locked closure into aontu_meta/vendor/
171
- manifest Print the OCI artifact a publish would push, gated on the
172
- breaking check against --against
222
+ manifest Print the manifest a publish would send, gated on the
223
+ compatibility check against --against
224
+ refreeze Recompute every canon pin and nothing else, after a
225
+ canonical-form change in the engine
226
+ tree The locked closure as a graph
227
+ outdated What could move, and what would move with it
228
+ serve Serve a repository directory; with --upstream, a caching
229
+ proxy in front of another repository
230
+ keygen Write a new Ed25519 signing key (PKCS#8 PEM) to a file,
231
+ once, and print the signer id a consumer names
232
+
233
+ model subcommands (one document, interrogated or edited):
234
+ get What the document says at a path
235
+ why Every contribution to the value at a path
236
+ set Append a path-flattened conjunct to an overlay
173
237
 
174
238
  Vet options:
175
239
  --at <path> Validate against this path of the schema ($.a.b)
@@ -248,21 +312,21 @@ Hash options:
248
312
  Hash exit codes: 0 hashed, 2 usage, 4 the document does not stand up
249
313
  on its own.
250
314
 
251
- Get options:
315
+ Model get options:
252
316
  -c, --canon Canonical-form fragment (default: generated JSON)
253
317
  --keys Keys at the node, one per line
254
318
  --types Shape view: concrete leaves lifted to their kinds
255
319
  --depth <n> Structure to depth n; deeper nodes render as top
256
320
  --format <f> text (default) or json
257
321
 
258
- Get exit codes: 0 rendered, 1 the path names nothing, 2 usage, 4 the
259
- document does not stand up on its own.
322
+ Model get exit codes: 0 rendered, 1 the path names nothing, 2 usage, 4
323
+ the document does not stand up on its own.
260
324
 
261
- Why options:
325
+ Model why options:
262
326
  --format <f> text (default) or json
263
327
 
264
- Why exit codes mirror get's: 0 explained, 1 the path names nothing,
265
- 2 usage, 4 the document does not stand up on its own.
328
+ Model why exit codes mirror get's: 0 explained, 1 the path names
329
+ nothing, 2 usage, 4 the document does not stand up on its own.
266
330
 
267
331
  View kinds: doc, lattice, tree, matrix, graph, layer, sets, layers,
268
332
  ladder, poset (the poset takes several files). The figure goes to stdout, the loss
@@ -352,7 +416,32 @@ every other line is a line of output.
352
416
 
353
417
  Template exit codes: 0 written, 1 --check drift, 2 usage or I/O.
354
418
 
355
- Set options:
419
+ Render options:
420
+ --check Compare what the generator writes with what <path>
421
+ holds, write nothing, and exit 1 on drift
422
+ --at <path> Where the component tree lives in the document
423
+ (default $.out)
424
+ --format json Print the files written, or the drift, as JSON
425
+ --marker <t> The file is a generator, and this is its marker
426
+ (default //-, and #- --- /*- <!--- by extension)
427
+ --profile <f> A profile file, whose template.ext names the
428
+ extensions it marks and template.marker the marker
429
+
430
+ The render verb writes the component tree a generator answers. The
431
+ tree is handed to jostraca, the generator runtime, which writes the
432
+ files: a tree that is one file is written to <path> itself, unless
433
+ <path> is a directory, and any other tree is written below <path>.
434
+ With --check nothing is written and <path> is compared with what the
435
+ generator writes, one "kind: file" line per difference.
436
+ A folder as the generator is a set: every regular file directly in
437
+ it, dotfiles aside, in name order, and their trees are written below
438
+ <path> as one run, so a path two of them claim is refused. A file with
439
+ no marker line in it is refused by name.
440
+
441
+ Render exit codes: 0 written or clean, 1 --check drift, 2 usage or
442
+ I/O, 4 the document does not stand up, or --at names nothing.
443
+
444
+ Model set options:
356
445
  --entry <file> The document the change is checked against
357
446
  --overlay <file> The file the change is appended to (created if
358
447
  absent; not written when the change does not hold)
@@ -368,7 +457,7 @@ Set options:
368
457
  --format <f> text (default) or json
369
458
 
370
459
  Set exit codes are vet's verdict classes: 0 valid, 1 invalid (the
371
- change contradicts a pinned value -- aontu why locates it, and
460
+ change contradicts a pinned value -- aontu model why locates it, and
372
461
  --in-place rewrites it), 2 usage, 3 incomplete, 4 the entry does not
373
462
  stand up on its own.
374
463
 
@@ -601,7 +690,7 @@ function trustOpts(trust: TrustArg, entryRoot: string): any {
601
690
  }
602
691
 
603
692
 
604
- function takeTrust(argv: string[]):
693
+ function takeTrust(argv: string[], io: Io = PROCESS_IO):
605
694
  { argv: string[], trust: TrustArg } | undefined {
606
695
  const rest: string[] = []
607
696
  let trust: TrustArg = { kind: 'system-warn', textExt: [] }
@@ -611,7 +700,7 @@ function takeTrust(argv: string[]):
611
700
  if ('--trust' === arg) {
612
701
  const parsed = null == argv[i + 1] ? undefined : parseTrustArg(argv[++i])
613
702
  if (null == parsed) {
614
- process.stderr.write(
703
+ io.err(
615
704
  'aontu: --trust needs system, none, or root[:dir]\n')
616
705
  return undefined
617
706
  }
@@ -620,7 +709,7 @@ function takeTrust(argv: string[]):
620
709
  else if ('--include-root' === arg) {
621
710
  const dir = argv[++i]
622
711
  if (null == dir) {
623
- process.stderr.write('aontu: --include-root needs a directory\n')
712
+ io.err('aontu: --include-root needs a directory\n')
624
713
  return undefined
625
714
  }
626
715
  trust = { kind: 'root', dir, textExt }
@@ -628,7 +717,7 @@ function takeTrust(argv: string[]):
628
717
  else if ('--text-ext' === arg) {
629
718
  const list = null == argv[i + 1] ? undefined : parseTextExt(argv[++i])
630
719
  if (null == list) {
631
- process.stderr.write(
720
+ io.err(
632
721
  'aontu: --text-ext needs extensions, without dots' +
633
722
  ' (--text-ext md,sql)\n')
634
723
  return undefined
@@ -644,7 +733,7 @@ function takeTrust(argv: string[]):
644
733
 
645
734
 
646
735
  // `md,sql` or `.md,.sql` -- the dot is accepted and dropped, because a
647
- // reader who has just written `@"notes.txt"` reaches for one. An empty
736
+ // reader who has just written `@"./notes.txt"` reaches for one. An empty
648
737
  // element, or anything that is not an extension, is a usage error
649
738
  // rather than a silently ignored word: a flag that quietly does
650
739
  // nothing is how a document ends up refused with no reason visible.
@@ -2048,110 +2137,186 @@ const VIEW_USAGE_CODES = [
2048
2137
  'view_document_shape', 'view_style_profile', 'view_style_unknown',
2049
2138
  ]
2050
2139
 
2051
- const MOD_HELP = 'aontu mod tidy|verify|vendor|manifest [dir] (try --help)'
2140
+ const PKG_HELP =
2141
+ 'aontu pkg tidy|verify|vendor|manifest|refreeze|tree|outdated|serve [dir] | keygen <file> (try --help)'
2052
2142
 
2053
- function runMod(argv: string[]): number {
2054
- const trusted = takeTrust(argv)
2055
- if (null == trusted) {
2056
- return 2
2057
- }
2058
- argv = trusted.argv
2059
- const trust = trusted.trust
2060
- const rest: string[] = []
2061
- let format: SubsumeFormat = 'text'
2062
- let against: string | undefined
2143
+ const PKG_SUBS = [
2144
+ 'tidy', 'verify', 'vendor', 'manifest', 'refreeze', 'tree', 'outdated',
2145
+ 'serve', 'keygen',
2146
+ ]
2147
+
2148
+ // The options every package verb parses, so `sync`, `get`, `publish`
2149
+ // and the `pkg` subcommands read one table.
2150
+ type PkgArgs = {
2151
+ rest: string[]
2152
+ format: SubsumeFormat
2153
+ against?: string
2154
+ frozen: boolean
2155
+ yes: boolean
2156
+ to?: string
2157
+ key?: string
2158
+ token?: string
2159
+ base: string[]
2160
+ write?: string
2161
+ upstream: string[]
2162
+ listen?: string
2163
+ }
2063
2164
 
2165
+ function parsePkgArgs(argv: string[], verb: string, io: Io): PkgArgs | undefined {
2166
+ const out: PkgArgs = {
2167
+ rest: [], format: 'text', frozen: false, yes: false, base: [], upstream: [],
2168
+ }
2169
+ const value = (name: string, i: number): string | undefined => {
2170
+ const v = argv[i]
2171
+ if (null == v) {
2172
+ io.err(`aontu: ${name} needs a value\n`)
2173
+ }
2174
+ return v
2175
+ }
2064
2176
  for (let i = 0; i < argv.length; i++) {
2065
2177
  const arg = argv[i]
2066
2178
  if ('-h' === arg || '--help' === arg) {
2067
- process.stdout.write(HELP)
2068
- return 0
2179
+ io.out(HELP)
2180
+ return undefined
2069
2181
  }
2070
2182
  if ('--format' === arg) {
2071
2183
  const f = argv[++i]
2072
2184
  if ('text' !== f && 'json' !== f) {
2073
- process.stderr.write('aontu: --format needs text or json\n')
2074
- return 2
2185
+ io.err('aontu: --format needs text or json\n')
2186
+ return undefined
2075
2187
  }
2076
- format = f
2188
+ out.format = f
2077
2189
  }
2078
- else if ('--against' === arg) {
2079
- const a = argv[++i]
2080
- if (null == a) {
2081
- process.stderr.write('aontu: --against needs a module directory\n')
2082
- return 2
2190
+ else if ('--frozen' === arg) {
2191
+ out.frozen = true
2192
+ }
2193
+ else if ('--yes' === arg) {
2194
+ out.yes = true
2195
+ }
2196
+ else if ('--against' === arg || '--to' === arg || '--key' === arg ||
2197
+ '--token' === arg || '--write' === arg || '--listen' === arg) {
2198
+ const v = value(arg, ++i)
2199
+ if (null == v) {
2200
+ return undefined
2083
2201
  }
2084
- against = a
2202
+ out['--against' === arg ? 'against' : '--to' === arg ? 'to' :
2203
+ '--key' === arg ? 'key' : '--token' === arg ? 'token' :
2204
+ '--write' === arg ? 'write' : 'listen'] = v
2205
+ }
2206
+ else if ('--base' === arg || '--upstream' === arg) {
2207
+ const v = value(arg, ++i)
2208
+ if (null == v) {
2209
+ return undefined
2210
+ }
2211
+ out['--base' === arg ? 'base' : 'upstream'].push(v)
2085
2212
  }
2086
2213
  else if (arg.startsWith('-')) {
2087
- process.stderr.write(`aontu: unknown mod option ${arg} (try --help)\n`)
2088
- return 2
2214
+ io.err(`aontu: unknown ${verb} option ${arg} (try --help)\n`)
2215
+ return undefined
2089
2216
  }
2090
2217
  else {
2091
- rest.push(arg)
2218
+ out.rest.push(arg)
2092
2219
  }
2093
2220
  }
2221
+ return out
2222
+ }
2094
2223
 
2095
- const sub = rest[0]
2096
- const dir = rest[1] ?? '.'
2097
2224
 
2098
- if ('get' === sub || 'publish' === sub) {
2099
- process.stderr.write(
2100
- 'aontu: mod ' + sub + ' needs a registry client, which this build ' +
2101
- 'does not ship; vendor the module by hand and run ' +
2102
- "'aontu mod tidy'\n")
2225
+ // A verb that finds the older layout names the current one, once, and
2226
+ // reads nothing from it.
2227
+ function nameOldLayout(dir: string, io: Io): void {
2228
+ const old = ['aon_vendor', 'mod-lock.aon', 'mod.aon',
2229
+ join(META_DIR, 'mod-lock.aon')].filter((f) => existsSync(join(dir, f)))
2230
+ if (0 < old.length) {
2231
+ io.err(
2232
+ 'aontu: ' + old.join(', ') + ' belong to an older layout: the package ' +
2233
+ 'file is ' + PKG_FILE + ', the lockfile ' + join(META_DIR, LOCK_FILE) +
2234
+ ' and the vendor tree ' + join(META_DIR, 'vendor') + '; rename ' +
2235
+ PKG_FILE + '\'s `mod` block to `pkg`, then run aontu sync\n')
2236
+ }
2237
+ }
2238
+
2239
+
2240
+ function runPkg(argv: string[], servers: Servers): number | Promise<number> {
2241
+ const io = servers.io ?? PROCESS_IO
2242
+ const trusted = takeTrust(argv, io)
2243
+ if (null == trusted) {
2103
2244
  return 2
2104
2245
  }
2246
+ const args = parsePkgArgs(trusted.argv, 'pkg', io)
2247
+ if (null == args) {
2248
+ return '-h' === trusted.argv[0] || trusted.argv.includes('--help') ? 0 : 2
2249
+ }
2250
+ const trust = trusted.trust
2251
+ const sub = args.rest[0]
2252
+ const dir = args.rest[1] ?? '.'
2105
2253
 
2106
- if (!MOD_SUBS.includes(sub) || 2 < rest.length) {
2107
- process.stderr.write(
2108
- `aontu: mod needs tidy, verify, vendor or manifest\n${MOD_HELP}\n`)
2254
+ if (!PKG_SUBS.includes(sub) || 2 < args.rest.length) {
2255
+ io.err(
2256
+ `aontu: pkg needs one of ${PKG_SUBS.join(', ')}\n${PKG_HELP}\n`)
2109
2257
  return 2
2110
2258
  }
2111
2259
 
2112
- if (existsSync(join(dir, 'aon_vendor')) || existsSync(join(dir, 'mod-lock.aon'))) {
2113
- process.stderr.write(
2114
- 'aontu: aon_vendor/ and mod-lock.aon now live under aontu_meta/: ' +
2115
- 'move them, or run aontu mod tidy and aontu mod vendor\n')
2260
+ if ('keygen' === sub) {
2261
+ if (2 !== args.rest.length) {
2262
+ io.err('aontu: pkg keygen needs the file to write\naontu pkg keygen <file>\n')
2263
+ return 2
2264
+ }
2265
+ const made = keygen(args.rest[1])
2266
+ if (null != made.refused) {
2267
+ io.err('aontu: ' + made.refused + '\n')
2268
+ return 2
2269
+ }
2270
+ io.out('signer: ' + made.signer + '\n')
2271
+ return 0
2116
2272
  }
2117
2273
 
2118
- // `--against` gates a manifest and means nothing to the other two;
2274
+ nameOldLayout(dir, io)
2275
+
2276
+ // `--against` gates a manifest and means nothing to the others;
2119
2277
  // accepting it there would say it had been honoured.
2120
- if (null != against && 'manifest' !== sub) {
2121
- process.stderr.write('aontu: --against is a manifest option\n')
2278
+ if (null != args.against && 'manifest' !== sub) {
2279
+ io.err('aontu: --against is a manifest option\n')
2122
2280
  return 2
2123
2281
  }
2124
2282
 
2125
- const modopts = modToolOptions(trust, resolve(dir))
2283
+ const opts = pkgToolOptions(trust, resolve(dir))
2126
2284
 
2127
- const report =
2128
- 'tidy' === sub ? modTidy(dir, modopts) :
2129
- 'verify' === sub ? modVerify(dir, modopts) :
2130
- 'vendor' === sub ? modVendor(dir, modopts) :
2131
- modManifest(dir, modopts, against)
2285
+ if ('outdated' === sub || 'serve' === sub) {
2286
+ return runPkgNet(sub, dir, args, opts, servers, io)
2287
+ }
2132
2288
 
2133
- process.stdout.write(('json' === format ?
2134
- exactJSON({ aontu: { version: version(), verb: 'mod ' + sub }, ...report },
2289
+ const report =
2290
+ 'tidy' === sub ? pkgTidy(dir, opts) :
2291
+ 'verify' === sub ? pkgVerify(dir, opts) :
2292
+ 'vendor' === sub ? pkgVendor(dir, opts) :
2293
+ 'refreeze' === sub ? pkgRefreeze(dir, opts) :
2294
+ 'tree' === sub ? pkgTree(dir, opts) :
2295
+ pkgManifest(dir, opts, args.against)
2296
+
2297
+ io.out(('json' === args.format ?
2298
+ exactJSON({ aontu: { version: version(), verb: 'pkg ' + sub }, ...report },
2135
2299
  2) :
2136
- modText(sub, report)) + '\n')
2300
+ pkgText(sub, report)) + '\n')
2137
2301
 
2138
- return MOD_EXIT[report.verdict]
2302
+ return PKG_EXIT[report.verdict]
2139
2303
  }
2140
2304
 
2141
2305
 
2142
- const MOD_SUBS = ['tidy', 'verify', 'vendor', 'manifest']
2143
-
2144
2306
  // The verdict classes: `ok` 0, a refused gate 1, an open question 3, a
2145
2307
  // document that does not stand up 4 -- `subsume`'s classes, because a
2146
2308
  // manifest gate IS a subsumption check and a caller reading exit codes
2147
2309
  // should not have to learn a second table.
2148
- type ModVerdict =
2149
- ModTidyReport['verdict'] |
2150
- ModVerifyReport['verdict'] |
2151
- ModVendorReport['verdict'] |
2152
- ModManifestReport['verdict']
2153
-
2154
- const MOD_EXIT: Record<ModVerdict, number> = {
2310
+ type PkgVerdict =
2311
+ PkgTidyReport['verdict'] |
2312
+ PkgVerifyReport['verdict'] |
2313
+ PkgVendorReport['verdict'] |
2314
+ PkgManifestReport['verdict'] |
2315
+ PkgRefreezeReport['verdict'] |
2316
+ PkgTreeReport['verdict'] |
2317
+ 'frozen' | 'refused' | 'sent' | 'dry-run' | 'outdated' | 'current'
2318
+
2319
+ const PKG_EXIT: Record<PkgVerdict, number> = {
2155
2320
  ok: 0,
2156
2321
  missing: 1,
2157
2322
  mismatch: 1,
@@ -2159,6 +2324,12 @@ const MOD_EXIT: Record<ModVerdict, number> = {
2159
2324
  // nothing to check, which is a refusal and not a pass.
2160
2325
  unlocked: 1,
2161
2326
  breaking: 1,
2327
+ frozen: 1,
2328
+ refused: 1,
2329
+ outdated: 1,
2330
+ sent: 0,
2331
+ 'dry-run': 0,
2332
+ current: 0,
2162
2333
  undecided: 3,
2163
2334
  error: 4,
2164
2335
  }
@@ -2167,7 +2338,7 @@ const MOD_EXIT: Record<ModVerdict, number> = {
2167
2338
  // The tooling's evaluator: the same standalone evaluation the module
2168
2339
  // resolver verifies with (ts/src/mod.ts), and for the same reason —
2169
2340
  // only the engine can say what a module MEANS.
2170
- function modToolOptions(trust: TrustArg, entryRoot: string) {
2341
+ function pkgToolOptions(trust: TrustArg, entryRoot: string): PkgToolOptions {
2171
2342
  const opts = verbOpts(trust, entryRoot)
2172
2343
  // The user cache lives outside any confinement root, so a confined
2173
2344
  // run reads the vendor tree only -- as the evaluator's own module
@@ -2184,7 +2355,7 @@ function modToolOptions(trust: TrustArg, entryRoot: string) {
2184
2355
  hash: canonHash(val),
2185
2356
  canon: val.canon,
2186
2357
  // The same question `aontu hash` asks before it will answer:
2187
- // did this document stand up ON ITS OWN? See ModToolEval.
2358
+ // did this document stand up ON ITS OWN? See PkgToolEval.
2188
2359
  ok: 0 === ctx.err.length && true !== val.isNil,
2189
2360
  }
2190
2361
  },
@@ -2192,19 +2363,54 @@ function modToolOptions(trust: TrustArg, entryRoot: string) {
2192
2363
  }
2193
2364
 
2194
2365
 
2195
- function modText(sub: string, report: any): string {
2366
+ function pkgLockLines(entries: any[]): string[] {
2367
+ return entries.map((e) => e.key + ' ' + e.v + ' ' + e.canon)
2368
+ }
2369
+
2370
+ // The renderers verify, tidy and sync share, as the Go port's do.
2371
+ function pkgMismatchLines(mismatched: any[]): string[] {
2372
+ return mismatched.map((m) => 'canon' === m.pin ?
2373
+ m.key + ': pinned ' + m.want + ' but the store means ' +
2374
+ ('' === m.got ? 'nothing (it does not evaluate)' : m.got) :
2375
+ m.key + ': pinned ' + m.pin + ' ' + m.want + ' but the store holds ' + m.got)
2376
+ }
2377
+
2378
+ // NOT a fetch: the package may well be sitting in the store. What is
2379
+ // absent is the PIN, and only a sync writes one.
2380
+ function pkgUnlockedLines(unlocked: string[]): string[] {
2381
+ return unlocked.map((key) => key + ': not in the lockfile (run: aontu sync)')
2382
+ }
2383
+
2384
+ function pkgForbiddenLines(forbidden: string[]): string[] {
2385
+ return forbidden.map((f) => f + ': not admitted in a package')
2386
+ }
2387
+
2388
+
2389
+ function pkgText(sub: string, report: any): string {
2196
2390
  const lines = ['verdict: ' + report.verdict]
2197
2391
 
2198
2392
  if ('manifest' === sub) {
2199
- if ('' !== report.mod) {
2200
- lines.push(report.mod + ' ' + report.version)
2201
- lines.push('config: ' + report.config)
2202
- }
2203
- for (const key of Object.keys(report.annotations).sort()) {
2204
- lines.push(key + ': ' + report.annotations[key])
2205
- }
2206
- for (const file of report.files) {
2207
- lines.push('layer: ' + file)
2393
+ const m = report.manifest
2394
+ if (null != m) {
2395
+ lines.push(m.package + ' ' + m.version + ' ' + m.publish)
2396
+ lines.push('archive: ' + m.archive.digest + ' (' + m.archive.files.length +
2397
+ ' files, ' + m.archive.size + ' bytes)')
2398
+ for (const mod of m.modules) {
2399
+ lines.push('module: ' + mod.path + ' ' + mod.main + ' ' + mod.canon)
2400
+ }
2401
+ for (const key of Object.keys(m.deps).sort(cmpCodePoint)) {
2402
+ lines.push('dep: ' + key + ' ' + m.deps[key].v +
2403
+ (null == m.deps[key].pkg ? '' : ' (' + m.deps[key].pkg + ')'))
2404
+ }
2405
+ for (const v of m.retract ?? []) {
2406
+ lines.push('retract: ' + v)
2407
+ }
2408
+ if (null != m.moved) {
2409
+ lines.push('moved: ' + m.moved)
2410
+ }
2411
+ for (const f of m.archive.files) {
2412
+ lines.push('file: ' + f.path + ' ' + f.digest + ' ' + f.size)
2413
+ }
2208
2414
  }
2209
2415
  for (const f of report.findings) {
2210
2416
  lines.push(f.path + ': ' + f.message)
@@ -2212,48 +2418,313 @@ function modText(sub: string, report: any): string {
2212
2418
  for (const miss of report.missing) {
2213
2419
  lines.push(miss + ': missing')
2214
2420
  }
2421
+ lines.push(...pkgForbiddenLines(report.forbidden))
2215
2422
  return lines.join('\n')
2216
2423
  }
2217
2424
 
2218
2425
  if ('verify' === sub) {
2219
- for (const mod of report.verified) {
2220
- lines.push(mod + ': verified')
2426
+ for (const key of report.verified) {
2427
+ lines.push(key + ': verified')
2221
2428
  }
2222
- for (const m of report.mismatched) {
2223
- lines.push(m.mod + ': pinned ' + m.want + ' but the store means ' +
2224
- ('' === m.got ? 'nothing (it does not evaluate)' : m.got))
2429
+ lines.push(...pkgMismatchLines(report.mismatched), ...pkgUnlockedLines(report.unlocked))
2430
+ for (const miss of report.missing) {
2431
+ lines.push(miss + ': not fetched (run: aontu sync)')
2225
2432
  }
2226
- // NOT a fetch: the module may well be sitting in the store. What
2227
- // is absent is the PIN, and only a tidy writes one.
2228
- for (const mod of report.unlocked) {
2229
- lines.push(mod + ': not in the lockfile (run: aontu mod tidy)')
2433
+ return lines.join('\n')
2434
+ }
2435
+
2436
+ if ('refreeze' === sub) {
2437
+ for (const r of report.repinned) {
2438
+ lines.push(r.key + ': ' + r.from + ' -> ' + r.to)
2439
+ }
2440
+ for (const key of report.unchanged) {
2441
+ lines.push(key + ': unchanged')
2442
+ }
2443
+ for (const bad of report.unevaluable) {
2444
+ lines.push(bad + ': does not evaluate on its own; nothing to pin')
2230
2445
  }
2231
2446
  for (const miss of report.missing) {
2232
- lines.push(miss + ': not fetched (run: aontu mod get)')
2447
+ lines.push(miss + ': not fetched (run: aontu sync)')
2233
2448
  }
2234
2449
  return lines.join('\n')
2235
2450
  }
2236
2451
 
2237
- const done: any[] = 'tidy' === sub ? report.lock : report.vendored
2238
- for (const item of done) {
2239
- lines.push('tidy' === sub ?
2240
- item.mod + ' ' + item.v + ' ' + item.canon : '' + item)
2452
+ if ('tree' === sub) {
2453
+ const byKey = new Map(report.nodes.map((n: any) => [n.key, n]))
2454
+ const seen = new Set<string>()
2455
+ const walk = (key: string, depth: number): void => {
2456
+ const node: any = byKey.get(key)
2457
+ const again = seen.has(key)
2458
+ seen.add(key)
2459
+ lines.push(' '.repeat(depth) + key +
2460
+ (null == node || '' === node.v ? '' : ' ' + node.v) +
2461
+ (again ? ' (above)' : null == node ? ' (not locked)' : ''))
2462
+ if (again || null == node) {
2463
+ return
2464
+ }
2465
+ for (const dep of node.deps) {
2466
+ walk(dep, depth + 1)
2467
+ }
2468
+ }
2469
+ walk(report.root, 0)
2470
+ for (const miss of report.missing) {
2471
+ lines.push(miss + ': not fetched (run: aontu sync)')
2472
+ }
2473
+ return lines.join('\n')
2241
2474
  }
2242
- // A module that is PRESENT but does not stand up. Named separately
2475
+
2476
+ const done: any[] = 'tidy' === sub ? pkgLockLines(report.lock) : report.vendored
2477
+ lines.push(...done)
2478
+ // A package that is PRESENT but does not stand up. Named separately
2243
2479
  // from a missing one because the repair is different: a fetch cannot
2244
- // help, the module itself has to be fixed (or its own dependencies
2480
+ // help, the package itself has to be fixed (or its own dependencies
2245
2481
  // vendored beside it). Before the missing tail, as the Go port's
2246
2482
  // shared renderer orders them.
2247
2483
  for (const bad of report.unevaluable ?? []) {
2248
2484
  lines.push(bad + ': does not evaluate on its own; nothing to pin')
2249
2485
  }
2486
+ lines.push(...pkgForbiddenLines(report.forbidden ?? []))
2250
2487
  for (const miss of report.missing) {
2251
- lines.push(miss + ': not fetched (run: aontu mod get)')
2488
+ lines.push(miss + ': not fetched (run: aontu sync)')
2252
2489
  }
2253
2490
  return lines.join('\n')
2254
2491
  }
2255
2492
 
2256
2493
 
2494
+ // The two `pkg` subcommands that reach a repository.
2495
+ async function runPkgNet(sub: string, dir: string, args: PkgArgs,
2496
+ opts: PkgToolOptions, servers: Servers, io: Io): Promise<number> {
2497
+ if ('serve' === sub) {
2498
+ const served = await startServe({
2499
+ dir: resolve(dir), upstream: args.upstream,
2500
+ listen: args.listen ?? '127.0.0.1:8017', http: servers.http(),
2501
+ })
2502
+ io.out('serving ' + resolve(dir) + ' at ' + served.url + '\n' +
2503
+ args.upstream.map((u) => 'upstream: ' + u + '\n').join(''))
2504
+ await servers.serve(served)
2505
+ await served.close()
2506
+ return 0
2507
+ }
2508
+ if (null == opts.cache) {
2509
+ io.err(NO_CACHE)
2510
+ return 2
2511
+ }
2512
+ const report = await pkgOutdated(dir, opts, servers.http(), { base: netBase(args) })
2513
+ io.out(pkgReportText('pkg outdated', args.format, report) + '\n')
2514
+ return PKG_EXIT[report.verdict]
2515
+ }
2516
+
2517
+ const NO_CACHE = 'aontu: this verb reads and writes the user cache, which a ' +
2518
+ 'confined run (--trust root) does not reach and this host does not name ' +
2519
+ '(set XDG_CACHE_HOME or HOME)\n'
2520
+
2521
+ function netBase(args: PkgArgs): string[] | undefined {
2522
+ return 0 === args.base.length ? undefined : args.base
2523
+ }
2524
+
2525
+ const PACKAGE_VERBS = ['sync', 'add', 'get', 'remove', 'why', 'publish']
2526
+
2527
+ const PACKAGE_HELP: Record<string, string> = {
2528
+ sync: 'aontu sync [--frozen] [dir] (try --help)',
2529
+ add: 'aontu add <pkg> [dir] (try --help)',
2530
+ get: 'aontu get <pkg>[@<version>] [dir] (try --help)',
2531
+ remove: 'aontu remove <pkg> [dir] (try --help)',
2532
+ why: 'aontu why <pkg> [dir] (try --help)',
2533
+ publish: 'aontu publish [--yes] [--to <dir>] [--key <file>] [dir] (try --help)',
2534
+ }
2535
+
2536
+ // The top-level package verbs (ADR-039 part 5). Every one but `why`
2537
+ // reaches a repository, so every one runs behind the seam.
2538
+ async function runPackageVerb(verb: string, argv: string[], servers: Servers):
2539
+ Promise<number> {
2540
+ const io = servers.io ?? PROCESS_IO
2541
+ const trusted = takeTrust(argv, io)
2542
+ if (null == trusted) {
2543
+ return 2
2544
+ }
2545
+ const args = parsePkgArgs(trusted.argv, verb, io)
2546
+ if (null == args) {
2547
+ return '-h' === trusted.argv[0] || trusted.argv.includes('--help') ? 0 : 2
2548
+ }
2549
+ const wantsPkg = 'sync' !== verb && 'publish' !== verb
2550
+ const dir = args.rest[wantsPkg ? 1 : 0] ?? '.'
2551
+ if (args.rest.length > (wantsPkg ? 2 : 1) || (wantsPkg && 0 === args.rest.length)) {
2552
+ io.err('aontu: ' + verb + (wantsPkg ? ' needs a package' : ' takes a directory') +
2553
+ '\n' + PACKAGE_HELP[verb] + '\n')
2554
+ return 2
2555
+ }
2556
+ nameOldLayout(dir, io)
2557
+ const opts = pkgToolOptions(trusted.trust, resolve(dir))
2558
+ const http = servers.http()
2559
+
2560
+ if ('why' === verb) {
2561
+ const report = pkgWhy(dir, opts, args.rest[0])
2562
+ io.out(pkgReportText('why', args.format, report) + '\n')
2563
+ return PKG_EXIT[report.verdict]
2564
+ }
2565
+
2566
+ if ('publish' === verb) {
2567
+ for (const f of [args.key, args.token]) {
2568
+ if (null != f && !existsSync(f)) {
2569
+ io.err('aontu: cannot read ' + f + '\n')
2570
+ return 2
2571
+ }
2572
+ }
2573
+ if (true === args.yes && null == args.key) {
2574
+ io.err('aontu: publish --yes needs --key <file>, the Ed25519 key that signs\n')
2575
+ return 2
2576
+ }
2577
+ const report = await pkgPublish(dir, opts, http, {
2578
+ yes: args.yes, to: null == args.to ? undefined : resolve(args.to), key: args.key,
2579
+ token: args.token, against: args.against, base: netBase(args), write: args.write,
2580
+ })
2581
+ io.out(pkgReportText('publish', args.format, report) + '\n')
2582
+ return PKG_EXIT[report.verdict]
2583
+ }
2584
+
2585
+ if (null == opts.cache) {
2586
+ io.err(NO_CACHE)
2587
+ return 2
2588
+ }
2589
+ const net = { base: netBase(args) }
2590
+ const report: PkgSyncReport | PkgChangeReport | string =
2591
+ 'sync' === verb ? await pkgSync(dir, opts, http, { ...net, frozen: args.frozen }) :
2592
+ 'remove' === verb ? await pkgRemove(dir, opts, http, args.rest[0], net) :
2593
+ await pkgGet(dir, opts, http, args.rest[0], { ...net, mode: verb as 'add' | 'get' })
2594
+ if ('string' === typeof report) {
2595
+ io.err('aontu: ' + report + '\n')
2596
+ return 2
2597
+ }
2598
+ io.out(pkgReportText(verb, args.format, report) + '\n')
2599
+ return PKG_EXIT[report.verdict]
2600
+ }
2601
+
2602
+
2603
+ function pkgReportText(verb: string, fmt: SubsumeFormat, report: any): string {
2604
+ return 'json' === fmt ?
2605
+ exactJSON({ aontu: { version: version(), verb }, ...report }, 2) :
2606
+ pkgNetText(verb, report)
2607
+ }
2608
+
2609
+
2610
+ function pkgNetText(verb: string, report: any): string {
2611
+ const lines = ['verdict: ' + report.verdict]
2612
+ const tail = (): void => {
2613
+ for (const e of report.events ?? []) {
2614
+ lines.push(e.code + ': ' + e.message)
2615
+ }
2616
+ if (null != report.refusal) {
2617
+ lines.push('refused: ' + report.refusal.code + ': ' + report.refusal.message)
2618
+ }
2619
+ }
2620
+
2621
+ if ('why' === verb) {
2622
+ const why = report as PkgWhyReport
2623
+ for (const p of why.paths) {
2624
+ lines.push(p.join(' -> '))
2625
+ }
2626
+ if ('missing' === why.verdict) {
2627
+ lines.push(why.pkg + ': not in the closure')
2628
+ }
2629
+ return lines.join('\n')
2630
+ }
2631
+
2632
+ if ('publish' === verb) {
2633
+ const pub = report as PkgPublishReport
2634
+ lines.push(...pkgText('manifest', pub).split('\n').slice(1))
2635
+ if (null != pub.digest) {
2636
+ lines.push('digest: ' + pub.digest)
2637
+ }
2638
+ if (null != pub.signer) {
2639
+ lines.push('signer: ' + pub.signer)
2640
+ }
2641
+ if (null != pub.against) {
2642
+ lines.push('against: ' + pub.against)
2643
+ }
2644
+ if (null != pub.to) {
2645
+ lines.push('to: ' + pub.to)
2646
+ }
2647
+ if (null != pub.write) {
2648
+ lines.push('write: ' + pub.write + PUBLISH_PATH)
2649
+ }
2650
+ if ('dry-run' === pub.verdict) {
2651
+ lines.push('dry run: nothing sent (add --yes)')
2652
+ }
2653
+ if ('sent' === pub.verdict) {
2654
+ lines.push('sent')
2655
+ }
2656
+ tail()
2657
+ return lines.join('\n')
2658
+ }
2659
+
2660
+ if ('pkg outdated' === verb) {
2661
+ const out = report as PkgOutdatedReport
2662
+ for (const e of out.locked) {
2663
+ if (0 < versionCompare(e.newest, e.v)) {
2664
+ lines.push(e.key + ' ' + e.v + ' -> ' + e.newest)
2665
+ for (const m of e.moves) {
2666
+ lines.push(' ' + m)
2667
+ }
2668
+ }
2669
+ else {
2670
+ lines.push(e.key + ' ' + e.v + ': current')
2671
+ }
2672
+ if (null != e.retracted) {
2673
+ lines.push(e.key + ' ' + e.v + ': retracted by ' + e.retracted)
2674
+ }
2675
+ }
2676
+ tail()
2677
+ return lines.join('\n')
2678
+ }
2679
+
2680
+ const sync = report as PkgChangeReport
2681
+ if (null != sync.change) {
2682
+ lines.push('change: ' + sync.change)
2683
+ }
2684
+ for (const f of sync.fetched) {
2685
+ lines.push('fetched: ' + f)
2686
+ }
2687
+ lines.push(...pkgLockLines(sync.lock))
2688
+ for (const bad of sync.unevaluable) {
2689
+ lines.push(bad + ': does not evaluate on its own; nothing to pin')
2690
+ }
2691
+ lines.push(...pkgForbiddenLines(sync.forbidden), ...pkgMismatchLines(sync.mismatched),
2692
+ ...pkgUnlockedLines(sync.unlocked))
2693
+ for (const miss of sync.missing) {
2694
+ lines.push(miss + ': not fetched (run: aontu sync)')
2695
+ }
2696
+ for (const c of sync.changes) {
2697
+ lines.push('lockfile would change: ' + c)
2698
+ }
2699
+ tail()
2700
+ return lines.join('\n')
2701
+ }
2702
+
2703
+
2704
+ const MODEL_HELP = 'aontu model get|why|set ... (try --help)'
2705
+
2706
+ // One document, interrogated or edited (ADR-039 part 5).
2707
+ function runModel(argv: string[]): number {
2708
+ const sub = argv[0]
2709
+ if ('-h' === sub || '--help' === sub) {
2710
+ process.stdout.write(HELP)
2711
+ return 0
2712
+ }
2713
+ if ('get' === sub) {
2714
+ return runGet(argv.slice(1))
2715
+ }
2716
+ if ('why' === sub) {
2717
+ return runWhy(argv.slice(1))
2718
+ }
2719
+ if ('set' === sub) {
2720
+ return runSet(argv.slice(1))
2721
+ }
2722
+ process.stderr.write(`aontu: model needs get, why or set\n${MODEL_HELP}\n`)
2723
+ return 2
2724
+ }
2725
+
2726
+
2727
+
2257
2728
  function vacuous(what: string, why: string): void {
2258
2729
  process.stderr.write(`aontu: ${what}: ${why}\n`)
2259
2730
  }
@@ -2439,6 +2910,211 @@ function runTrace(argv: string[]): number {
2439
2910
  }
2440
2911
 
2441
2912
 
2913
+ // ---------------------------------------------------------------------
2914
+ // The writer (ADR-040). A generator answers a component tree; this verb
2915
+ // hands it to jostraca, which writes the files below a path or holds
2916
+ // them to it.
2917
+
2918
+ const RENDER_HELP =
2919
+ 'aontu render [--check] [--at <path>] [--format json] ' +
2920
+ '[--marker <token>] [--profile <file>] <file|folder> <path> (try --help)'
2921
+
2922
+
2923
+ // Loaded at the call, so every other verb starts without it.
2924
+ function generatorRuntime(): any {
2925
+ return require('jostraca')
2926
+ }
2927
+
2928
+
2929
+ function isDirectory(path: string): boolean {
2930
+ return true === statSync(path, { throwIfNoEntry: false })?.isDirectory()
2931
+ }
2932
+
2933
+
2934
+ async function runRender(argv: string[]): Promise<number> {
2935
+ const trusted = takeTrust(argv)
2936
+ if (null == trusted) {
2937
+ return 2
2938
+ }
2939
+ argv = trusted.argv
2940
+ const trust = trusted.trust
2941
+ const rest: string[] = []
2942
+ const profileFiles: string[] = []
2943
+ let format: SubsumeFormat = 'text'
2944
+ let at: string | undefined = undefined
2945
+ let marker: string | undefined = undefined
2946
+ let check = false
2947
+
2948
+ for (let i = 0; i < argv.length; i++) {
2949
+ const arg = argv[i]
2950
+ if ('-h' === arg || '--help' === arg) {
2951
+ process.stdout.write(HELP)
2952
+ return 0
2953
+ }
2954
+ if ('--check' === arg) {
2955
+ check = true
2956
+ }
2957
+ else if ('--format' === arg) {
2958
+ const f = argv[++i]
2959
+ if ('text' !== f && 'json' !== f) {
2960
+ process.stderr.write('aontu: --format needs text or json\n')
2961
+ return 2
2962
+ }
2963
+ format = f
2964
+ }
2965
+ else if ('--at' === arg) {
2966
+ at = argv[++i]
2967
+ if (null == at || '' === at) {
2968
+ process.stderr.write('aontu: --at needs a path\n')
2969
+ return 2
2970
+ }
2971
+ }
2972
+ else if ('--marker' === arg) {
2973
+ marker = argv[++i]
2974
+ if (null == marker || '' === marker) {
2975
+ process.stderr.write('aontu: --marker needs a token\n')
2976
+ return 2
2977
+ }
2978
+ }
2979
+ else if ('--profile' === arg) {
2980
+ const pf = argv[++i]
2981
+ if (null == pf || '' === pf) {
2982
+ process.stderr.write('aontu: --profile needs a file\n')
2983
+ return 2
2984
+ }
2985
+ profileFiles.push(pf)
2986
+ }
2987
+ else if (arg.startsWith('-')) {
2988
+ process.stderr.write(`aontu: unknown render option ${arg} (try --help)\n`)
2989
+ return 2
2990
+ }
2991
+ else {
2992
+ rest.push(arg)
2993
+ }
2994
+ }
2995
+
2996
+ if (2 !== rest.length) {
2997
+ process.stderr.write(`aontu: render needs a file and a path\n${RENDER_HELP}\n`)
2998
+ return 2
2999
+ }
3000
+ const [file, dest] = rest
3001
+
3002
+ const declared = loadProfiles(profileFiles, trust)
3003
+ if ('number' === typeof declared) {
3004
+ return declared
3005
+ }
3006
+
3007
+ // A FOLDER IS A SET OF GENERATORS: every regular file directly in it,
3008
+ // dotfiles aside, in code-point order, written as one tree.
3009
+ let entries: Dirent[] | undefined
3010
+ try {
3011
+ entries = readdirSync(file, { withFileTypes: true })
3012
+ }
3013
+ catch (err: any) {
3014
+ entries = undefined
3015
+ }
3016
+ const set = undefined !== entries
3017
+ const files = undefined === entries ? [file] :
3018
+ entries.filter((e) => e.isFile() && !e.name.startsWith('.'))
3019
+ .map((e) => e.name).sort(cmpCodePoint).map((n) => join(file, n))
3020
+ if (0 === files.length) {
3021
+ process.stderr.write(`aontu: ${file} holds no generator\n`)
3022
+ return 2
3023
+ }
3024
+
3025
+ const trees: any[] = []
3026
+ for (const f of files) {
3027
+ let src: string
3028
+ try {
3029
+ src = readFileSync(f, 'utf8')
3030
+ }
3031
+ catch (err: any) {
3032
+ process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
3033
+ return 2
3034
+ }
3035
+ if (!/[.](aon|aontu)$/.test(f)) {
3036
+ const mark = marker ?? markerFromProfiles(declared, f) ?? markerFor(f)
3037
+ if (!templateOutputs(src, mark).some((out) => !out)) {
3038
+ process.stderr.write(`aontu: ${f} carries no ${mark} marker line, ` +
3039
+ 'so there is no aontu in it to render\n')
3040
+ return 2
3041
+ }
3042
+ src = desugarTemplate(src, mark)
3043
+ }
3044
+ const report = get(src, at ?? '$.out', {
3045
+ view: 'json', path: f, ...verbOpts(trust, entryRootOf(f)),
3046
+ })
3047
+ if (!report.ok) {
3048
+ process.stderr.write(report.findings.map(renderFinding).join('\n') + '\n')
3049
+ return 4
3050
+ }
3051
+ const tree = JSON.parse(report.out)
3052
+ // A `File` without a name is refused: the runtime ports disagree
3053
+ // about it.
3054
+ if ('File' === tree?.cmp && 'string' !== typeof tree.props?.name) {
3055
+ process.stderr.write(`aontu: ${f}: the file at ${at ?? '$.out'} has no name\n`)
3056
+ return 4
3057
+ }
3058
+ trees.push(tree)
3059
+ }
3060
+
3061
+ // ONE FILE GOES TO THE PATH ITSELF, unless the path is a directory; a
3062
+ // set is written below the path whatever its trees are.
3063
+ let folder = dest
3064
+ const tree: any = set ? trees.flatMap((t) => Array.isArray(t) ? t : [t]) : trees[0]
3065
+ if (!set && 'File' === tree?.cmp && !isDirectory(dest)) {
3066
+ tree.props.name = basename(dest)
3067
+ folder = dirname(dest)
3068
+ }
3069
+
3070
+ const { cmpTree, Jostraca } = generatorRuntime()
3071
+ let root: any
3072
+ try {
3073
+ root = cmpTree(tree, { raw: true })
3074
+ }
3075
+ catch (err: any) {
3076
+ process.stderr.write(`aontu: ${file}: ${err.message}\n`)
3077
+ return 4
3078
+ }
3079
+ const runtime = Jostraca()
3080
+
3081
+ try {
3082
+ if (check) {
3083
+ const res = await runtime.check({ folder }, root)
3084
+ const drift = res.drift.map((d: any) => ({ kind: d.kind, path: d.path }))
3085
+ if ('json' === format) {
3086
+ process.stdout.write(exactJSON({
3087
+ aontu: { version: version(), verb: 'render' },
3088
+ verdict: 0 === drift.length ? 'ok' : 'drift',
3089
+ checked: res.checked,
3090
+ drift,
3091
+ }, 2) + '\n')
3092
+ }
3093
+ else {
3094
+ for (const d of drift) {
3095
+ process.stdout.write(`${d.kind}: ${d.path}\n`)
3096
+ }
3097
+ }
3098
+ return 0 === drift.length ? 0 : 1
3099
+ }
3100
+
3101
+ const res = await runtime.generate({ folder }, root)
3102
+ if ('json' === format) {
3103
+ process.stdout.write(exactJSON({
3104
+ aontu: { version: version(), verb: 'render' },
3105
+ verdict: 'ok',
3106
+ files: res.files,
3107
+ }, 2) + '\n')
3108
+ }
3109
+ return 0
3110
+ }
3111
+ catch (err: any) {
3112
+ process.stderr.write(`aontu: ${err.message}\n`)
3113
+ return 2
3114
+ }
3115
+ }
3116
+
3117
+
2442
3118
  function runReaches(argv: string[]): number {
2443
3119
  const trusted = takeTrust(argv)
2444
3120
  if (null == trusted) {
@@ -3272,7 +3948,7 @@ function runHash(argv: string[]): number {
3272
3948
  }
3273
3949
 
3274
3950
 
3275
- const GET_HELP = 'aontu get <path> <file> (try --help)'
3951
+ const GET_HELP = 'aontu model get <path> <file> (try --help)'
3276
3952
 
3277
3953
  function runGet(argv: string[]): number {
3278
3954
  const trusted = takeTrust(argv)
@@ -3318,7 +3994,7 @@ function runGet(argv: string[]): number {
3318
3994
  format = f
3319
3995
  }
3320
3996
  else if (arg.startsWith('-')) {
3321
- process.stderr.write(`aontu: unknown get option ${arg} (try --help)\n`)
3997
+ process.stderr.write(`aontu: unknown model get option ${arg} (try --help)\n`)
3322
3998
  return 2
3323
3999
  }
3324
4000
  else {
@@ -3327,7 +4003,7 @@ function runGet(argv: string[]): number {
3327
4003
  }
3328
4004
 
3329
4005
  if (2 !== rest.length) {
3330
- process.stderr.write(`aontu: get needs a path and one file\n${GET_HELP}\n`)
4006
+ process.stderr.write(`aontu: model get needs a path and one file\n${GET_HELP}\n`)
3331
4007
  return 2
3332
4008
  }
3333
4009
  const [path, file] = rest
@@ -3355,7 +4031,7 @@ function runGet(argv: string[]): number {
3355
4031
  })
3356
4032
  if ('json' === format) {
3357
4033
  process.stdout.write(exactJSON({
3358
- aontu: { version: version(), verb: 'get' },
4034
+ aontu: { version: version(), verb: 'model get' },
3359
4035
  findings: report.findings,
3360
4036
  ok: report.ok,
3361
4037
  out: report.out,
@@ -3384,7 +4060,7 @@ function runGet(argv: string[]): number {
3384
4060
  // The positive twin of the vet report: errors explain what failed to
3385
4061
  // unify, this explains what did.
3386
4062
 
3387
- const WHY_HELP = 'aontu why <path> <file> (try --help)'
4063
+ const WHY_HELP = 'aontu model why <path> <file> (try --help)'
3388
4064
 
3389
4065
  function runWhy(argv: string[]): number {
3390
4066
  const trusted = takeTrust(argv)
@@ -3411,7 +4087,7 @@ function runWhy(argv: string[]): number {
3411
4087
  format = f
3412
4088
  }
3413
4089
  else if (arg.startsWith('-')) {
3414
- process.stderr.write(`aontu: unknown why option ${arg} (try --help)\n`)
4090
+ process.stderr.write(`aontu: unknown model why option ${arg} (try --help)\n`)
3415
4091
  return 2
3416
4092
  }
3417
4093
  else {
@@ -3420,7 +4096,7 @@ function runWhy(argv: string[]): number {
3420
4096
  }
3421
4097
 
3422
4098
  if (2 !== rest.length) {
3423
- process.stderr.write(`aontu: why needs a path and one file\n${WHY_HELP}\n`)
4099
+ process.stderr.write(`aontu: model why needs a path and one file\n${WHY_HELP}\n`)
3424
4100
  return 2
3425
4101
  }
3426
4102
  const [path, file] = rest
@@ -3439,7 +4115,7 @@ function runWhy(argv: string[]): number {
3439
4115
  })
3440
4116
  if ('json' === format) {
3441
4117
  process.stdout.write(exactJSON({
3442
- aontu: { version: version(), verb: 'why' },
4118
+ aontu: { version: version(), verb: 'model why' },
3443
4119
  findings: report.findings,
3444
4120
  ok: report.ok,
3445
4121
  ...(null == report.record ? {} : { record: report.record }),
@@ -3477,7 +4153,7 @@ function renderWhyText(record: WhyRecord): string {
3477
4153
 
3478
4154
 
3479
4155
  const SET_HELP =
3480
- 'aontu set <path>=<value> --entry <file> --overlay <file> (try --help)'
4156
+ 'aontu model set <path>=<value> --entry <file> --overlay <file> (try --help)'
3481
4157
 
3482
4158
  function runSet(argv: string[]): number {
3483
4159
  const trusted = takeTrust(argv)
@@ -3520,7 +4196,7 @@ function runSet(argv: string[]): number {
3520
4196
  format = f
3521
4197
  }
3522
4198
  else if (arg.startsWith('-')) {
3523
- process.stderr.write(`aontu: unknown set option ${arg} (try --help)\n`)
4199
+ process.stderr.write(`aontu: unknown model set option ${arg} (try --help)\n`)
3524
4200
  return 2
3525
4201
  }
3526
4202
  else {
@@ -3530,7 +4206,7 @@ function runSet(argv: string[]): number {
3530
4206
 
3531
4207
  if (0 === assignments.length || null == entry || null == overlayFile) {
3532
4208
  process.stderr.write(
3533
- `aontu: set needs assignments, --entry and --overlay\n${SET_HELP}\n`)
4209
+ `aontu: model set needs assignments, --entry and --overlay\n${SET_HELP}\n`)
3534
4210
  return 2
3535
4211
  }
3536
4212
 
@@ -3582,7 +4258,7 @@ function runSet(argv: string[]): number {
3582
4258
 
3583
4259
  if ('json' === format) {
3584
4260
  process.stdout.write(exactJSON({
3585
- aontu: { version: version(), verb: 'set' },
4261
+ aontu: { version: version(), verb: 'model set' },
3586
4262
  appended: report.appended,
3587
4263
  findings: report.findings,
3588
4264
  overlay: report.overlay,
@@ -4082,17 +4758,38 @@ function fmtOne(
4082
4758
  }
4083
4759
 
4084
4760
 
4761
+ // Where a verb writes. The package verbs are asynchronous, so a test
4762
+ // cannot capture the process streams around them; it hands these in.
4763
+ type Io = { out: (s: string) => void, err: (s: string) => void }
4764
+
4765
+ const PROCESS_IO: Io = {
4766
+ out: (s) => void process.stdout.write(s),
4767
+ err: (s) => void process.stderr.write(s),
4768
+ }
4769
+
4085
4770
  type Servers = {
4086
4771
  lsp: () => void
4087
4772
  mcp: (argv: string[]) => void
4773
+ // `pkg serve` runs until this settles: the process's SIGINT, or a
4774
+ // test's say-so.
4775
+ serve: (served: Served) => Promise<void>
4776
+ http: () => PkgHttp
4777
+ io?: Io
4778
+ }
4779
+
4780
+ // `pkg serve` runs until the process is interrupted.
4781
+ function serveUntilInterrupted(): Promise<void> {
4782
+ return new Promise((done) => process.once('SIGINT', () => done()))
4088
4783
  }
4089
4784
 
4090
4785
  // Excluded: the real pair takes the process stdio, so ts/test/cli.test.ts
4091
4786
  // drives each server through a child process instead.
4092
- /* node:coverage ignore next 4 */
4787
+ /* node:coverage ignore next 6 */
4093
4788
  const SERVERS: Servers = {
4094
4789
  lsp: () => void lspMain(),
4095
4790
  mcp: (argv) => void mcpMain(undefined, undefined, undefined, undefined, argv),
4791
+ serve: serveUntilInterrupted,
4792
+ http: defaultHttp,
4096
4793
  }
4097
4794
 
4098
4795
  // undefined: the server took the process; a number: an answer the CLI
@@ -4416,10 +5113,10 @@ function runInit(argv: string[]): number {
4416
5113
 
4417
5114
 
4418
5115
  const KNOWN_VERBS = [
4419
- 'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
4420
- 'help', 'init', 'jsonschema', 'lsp', 'mcp', 'mod', 'reaches',
4421
- 'relations', 'set', 'subsume', 'template', 'trace', 'trim',
4422
- 'vet', 'view', 'why',
5116
+ 'add', 'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
5117
+ 'help', 'init', 'jsonschema', 'lsp', 'mcp', 'model', 'pkg', 'publish',
5118
+ 'reaches', 'relations', 'remove', 'render', 'subsume', 'sync', 'template',
5119
+ 'trace', 'trim', 'vet', 'view', 'why',
4423
5120
  ]
4424
5121
 
4425
5122
 
@@ -4514,20 +5211,22 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4514
5211
  return servers.mcp(argv.slice(3))
4515
5212
  }
4516
5213
 
4517
- if ('set' === argv[2]) {
4518
- return finish(runSet(argv.slice(3)))
4519
- }
4520
5214
 
4521
5215
  if ('allow' === argv[2]) {
4522
5216
  return finish(runAllow(argv.slice(3)))
4523
5217
  }
4524
5218
 
4525
- if ('why' === argv[2]) {
4526
- return finish(runWhy(argv.slice(3)))
5219
+ if ('model' === argv[2]) {
5220
+ return finish(runModel(argv.slice(3)))
5221
+ }
5222
+
5223
+ if ('pkg' === argv[2]) {
5224
+ const r = runPkg(argv.slice(3), servers)
5225
+ return 'number' === typeof r ? finish(r) : void r.then(finish)
4527
5226
  }
4528
5227
 
4529
- if ('get' === argv[2]) {
4530
- return finish(runGet(argv.slice(3)))
5228
+ if (PACKAGE_VERBS.includes(argv[2])) {
5229
+ return void runPackageVerb(argv[2], argv.slice(3), servers).then(finish)
4531
5230
  }
4532
5231
 
4533
5232
  if ('hash' === argv[2]) {
@@ -4548,10 +5247,6 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4548
5247
  return finish(runInit(argv.slice(3)))
4549
5248
  }
4550
5249
 
4551
- if ('mod' === argv[2]) {
4552
- return finish(runMod(argv.slice(3)))
4553
- }
4554
-
4555
5250
  if ('relations' === argv[2]) {
4556
5251
  return finish(runRelations(argv.slice(3)))
4557
5252
  }
@@ -4569,6 +5264,10 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4569
5264
  return finish(runTrace(argv.slice(3)))
4570
5265
  }
4571
5266
 
5267
+ if ('render' === argv[2]) {
5268
+ return void runRender(argv.slice(3)).then(finish)
5269
+ }
5270
+
4572
5271
  if ('reaches' === argv[2]) {
4573
5272
  return finish(runReaches(argv.slice(3)))
4574
5273
  }
@@ -4686,7 +5385,8 @@ export {
4686
5385
  runJsonSchema,
4687
5386
  runTemplate,
4688
5387
  runTrace,
4689
- runMod,
5388
+ runRender,
5389
+ runPkg, runModel, runPackageVerb, pkgToolOptions, serveUntilInterrupted,
4690
5390
  runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
4691
5391
  looksLikeVerb,
4692
5392
  KNOWN_VERBS,