aontu 0.64.0 → 0.65.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 (61) 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 +15 -2
  7. package/dist/cli.js +523 -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/package.json +1 -1
  39. package/skill/SKILL.md +3 -3
  40. package/skill/error-codes.md +4 -4
  41. package/skill/examples.md +4 -4
  42. package/skill/grammar-card.md +1 -1
  43. package/skill/init/check.sh +2 -2
  44. package/skill/tasks.md +6 -6
  45. package/src/agentsmd.ts +3 -3
  46. package/src/aontu.ts +1 -1
  47. package/src/cli.ts +610 -150
  48. package/src/compat.ts +148 -0
  49. package/src/helpdoc.ts +5 -5
  50. package/src/hints.ts +11 -7
  51. package/src/lang.ts +9 -1
  52. package/src/mod.ts +142 -51
  53. package/src/pkg-net.ts +1945 -0
  54. package/src/pkg-zip.ts +228 -0
  55. package/src/pkg.ts +1018 -0
  56. package/src/report-sarif.ts +1 -1
  57. package/src/subsume.ts +2 -2
  58. package/dist/mod-tool.d.ts +0 -58
  59. package/dist/mod-tool.js +0 -418
  60. package/dist/mod-tool.js.map +0 -1
  61. package/src/mod-tool.ts +0 -578
package/src/cli.ts CHANGED
@@ -29,11 +29,23 @@ import { sarifReport } from './report-sarif'
29
29
  import { main as lspMain } from './lsp-server'
30
30
  import { main as mcpMain } from './mcp-server'
31
31
  import { jsonSchema } from './jsonschema'
32
- import { modTidy, modVerify, modVendor, modManifest } from './mod-tool'
32
+ import {
33
+ pkgTidy, pkgVerify, pkgVendor, pkgManifest, pkgRefreeze, pkgTree,
34
+ versionCompare,
35
+ } from './pkg'
36
+ import {
37
+ pkgSync, pkgGet, pkgRemove, pkgWhy, pkgPublish, pkgOutdated, startServe,
38
+ defaultHttp, keygen, PUBLISH_PATH,
39
+ } from './pkg-net'
33
40
  import type {
34
- ModTidyReport, ModVerifyReport, ModVendorReport, ModManifestReport,
35
- } from './mod-tool'
36
- import { modCacheDir } from './mod'
41
+ PkgSyncReport, PkgChangeReport, PkgWhyReport, PkgPublishReport,
42
+ PkgOutdatedReport, Served, PkgHttp,
43
+ } from './pkg-net'
44
+ import type {
45
+ PkgTidyReport, PkgVerifyReport, PkgVendorReport, PkgManifestReport,
46
+ PkgRefreezeReport, PkgTreeReport, PkgToolOptions,
47
+ } from './pkg'
48
+ import { modCacheDir, PKG_FILE, LOCK_FILE, META_DIR } from './mod'
37
49
  import { VET_MAX_ERRORS } from './vet'
38
50
  import type {
39
51
  VetReport, VetFinding, VetVerdict, VetCoverage,
@@ -86,10 +98,18 @@ const HELP = `Usage: aontu [options] [file]
86
98
  aontu trace [--at <path>] [--format json] [--marker <token>]
87
99
  [--profile <file>] <file>
88
100
  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>
101
+ aontu sync [--frozen] [options] [dir]
102
+ aontu add <pkg>[@<version>] [options] [dir]
103
+ aontu get <pkg>[@<version>] [options] [dir]
104
+ aontu remove <pkg> [options] [dir]
105
+ aontu why <pkg> [options] [dir]
106
+ aontu publish [--yes] [--to <dir>] [options] [dir]
107
+ aontu pkg tidy|verify|vendor|manifest|refreeze|tree|outdated|serve
108
+ [options] [dir]
109
+ aontu pkg keygen <file>
110
+ aontu model get <path> [options] <file>
111
+ aontu model why <path> [options] <file>
112
+ aontu model set <path>=<value>... --entry <file> --overlay <file>
93
113
  aontu allow --role <role> [--at <path>] <roles-file> <path>...
94
114
  aontu agentsmd [--write <AGENTS.md>] [--depth <n>] <file>
95
115
  aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>]
@@ -158,18 +178,57 @@ Options:
158
178
  named format keeps its meaning, and .js stays
159
179
  refused. Every verb takes it
160
180
 
161
- Mod options:
181
+ Package verbs (a module is imported; a package is published):
182
+ sync Make the project correct: resolve by minimum version
183
+ selection, fetch what is missing, write aontu_meta/pkg-lock.aon,
184
+ vendor, verify. --frozen refuses to change the lockfile
185
+ add Take on a dependency the project does not have, then sync;
186
+ refuses one it has and names get
187
+ get Add a dependency or raise its minimum, then sync. Without a
188
+ version, the newest version outside the cooldown
189
+ remove Drop a dependency, then sync
190
+ why Why is this package in the closure: every path of
191
+ requirements that reaches it
192
+ publish Publish this package. A dry run without --yes; --to <dir>
193
+ writes the repository layout into a directory instead
194
+
195
+ Package options:
162
196
  --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)
197
+ --frozen sync: fail if the lockfile would change (the CI mode)
198
+ --yes publish: send it (the default is a dry run)
199
+ --to <dir> publish: write the read-path layout into a directory
200
+ --key <file> publish: the Ed25519 private key (PKCS#8 PEM) that
201
+ signs the manifest, for the key provider
202
+ --token <file> publish: the forge's OIDC token, read from a file
203
+ --base <url> the repository to read from (repeatable; overrides
204
+ pkg.aon repo.base)
205
+ --write <url> publish: the write path (overrides pkg.aon repo.write)
206
+ --against <dir> manifest: a prior version's tree, to gate on
207
+ --upstream <u> serve: fetch on miss from this repository (repeatable)
208
+ --listen <a> serve: the address to listen on (default 127.0.0.1:8017)
209
+
210
+ pkg subcommands (the rest of the package operations):
211
+ tidy Resolve the closure by minimum version selection and
212
+ rewrite aontu_meta/pkg-lock.aon in canonical form
213
+ verify Check every locked package still is and still means what
214
+ the lockfile pins, bytes before meaning, and change nothing
215
+ (the CI gate; tidy rewrites)
170
216
  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
217
+ manifest Print the manifest a publish would send, gated on the
218
+ compatibility check against --against
219
+ refreeze Recompute every canon pin and nothing else, after a
220
+ canonical-form change in the engine
221
+ tree The locked closure as a graph
222
+ outdated What could move, and what would move with it
223
+ serve Serve a repository directory; with --upstream, a caching
224
+ proxy in front of another repository
225
+ keygen Write a new Ed25519 signing key (PKCS#8 PEM) to a file,
226
+ once, and print the signer id a consumer names
227
+
228
+ model subcommands (one document, interrogated or edited):
229
+ get What the document says at a path
230
+ why Every contribution to the value at a path
231
+ set Append a path-flattened conjunct to an overlay
173
232
 
174
233
  Vet options:
175
234
  --at <path> Validate against this path of the schema ($.a.b)
@@ -248,21 +307,21 @@ Hash options:
248
307
  Hash exit codes: 0 hashed, 2 usage, 4 the document does not stand up
249
308
  on its own.
250
309
 
251
- Get options:
310
+ Model get options:
252
311
  -c, --canon Canonical-form fragment (default: generated JSON)
253
312
  --keys Keys at the node, one per line
254
313
  --types Shape view: concrete leaves lifted to their kinds
255
314
  --depth <n> Structure to depth n; deeper nodes render as top
256
315
  --format <f> text (default) or json
257
316
 
258
- Get exit codes: 0 rendered, 1 the path names nothing, 2 usage, 4 the
259
- document does not stand up on its own.
317
+ Model get exit codes: 0 rendered, 1 the path names nothing, 2 usage, 4
318
+ the document does not stand up on its own.
260
319
 
261
- Why options:
320
+ Model why options:
262
321
  --format <f> text (default) or json
263
322
 
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.
323
+ Model why exit codes mirror get's: 0 explained, 1 the path names
324
+ nothing, 2 usage, 4 the document does not stand up on its own.
266
325
 
267
326
  View kinds: doc, lattice, tree, matrix, graph, layer, sets, layers,
268
327
  ladder, poset (the poset takes several files). The figure goes to stdout, the loss
@@ -352,7 +411,7 @@ every other line is a line of output.
352
411
 
353
412
  Template exit codes: 0 written, 1 --check drift, 2 usage or I/O.
354
413
 
355
- Set options:
414
+ Model set options:
356
415
  --entry <file> The document the change is checked against
357
416
  --overlay <file> The file the change is appended to (created if
358
417
  absent; not written when the change does not hold)
@@ -368,7 +427,7 @@ Set options:
368
427
  --format <f> text (default) or json
369
428
 
370
429
  Set exit codes are vet's verdict classes: 0 valid, 1 invalid (the
371
- change contradicts a pinned value -- aontu why locates it, and
430
+ change contradicts a pinned value -- aontu model why locates it, and
372
431
  --in-place rewrites it), 2 usage, 3 incomplete, 4 the entry does not
373
432
  stand up on its own.
374
433
 
@@ -601,7 +660,7 @@ function trustOpts(trust: TrustArg, entryRoot: string): any {
601
660
  }
602
661
 
603
662
 
604
- function takeTrust(argv: string[]):
663
+ function takeTrust(argv: string[], io: Io = PROCESS_IO):
605
664
  { argv: string[], trust: TrustArg } | undefined {
606
665
  const rest: string[] = []
607
666
  let trust: TrustArg = { kind: 'system-warn', textExt: [] }
@@ -611,7 +670,7 @@ function takeTrust(argv: string[]):
611
670
  if ('--trust' === arg) {
612
671
  const parsed = null == argv[i + 1] ? undefined : parseTrustArg(argv[++i])
613
672
  if (null == parsed) {
614
- process.stderr.write(
673
+ io.err(
615
674
  'aontu: --trust needs system, none, or root[:dir]\n')
616
675
  return undefined
617
676
  }
@@ -620,7 +679,7 @@ function takeTrust(argv: string[]):
620
679
  else if ('--include-root' === arg) {
621
680
  const dir = argv[++i]
622
681
  if (null == dir) {
623
- process.stderr.write('aontu: --include-root needs a directory\n')
682
+ io.err('aontu: --include-root needs a directory\n')
624
683
  return undefined
625
684
  }
626
685
  trust = { kind: 'root', dir, textExt }
@@ -628,7 +687,7 @@ function takeTrust(argv: string[]):
628
687
  else if ('--text-ext' === arg) {
629
688
  const list = null == argv[i + 1] ? undefined : parseTextExt(argv[++i])
630
689
  if (null == list) {
631
- process.stderr.write(
690
+ io.err(
632
691
  'aontu: --text-ext needs extensions, without dots' +
633
692
  ' (--text-ext md,sql)\n')
634
693
  return undefined
@@ -644,7 +703,7 @@ function takeTrust(argv: string[]):
644
703
 
645
704
 
646
705
  // `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
706
+ // reader who has just written `@"./notes.txt"` reaches for one. An empty
648
707
  // element, or anything that is not an extension, is a usage error
649
708
  // rather than a silently ignored word: a flag that quietly does
650
709
  // nothing is how a document ends up refused with no reason visible.
@@ -2048,110 +2107,186 @@ const VIEW_USAGE_CODES = [
2048
2107
  'view_document_shape', 'view_style_profile', 'view_style_unknown',
2049
2108
  ]
2050
2109
 
2051
- const MOD_HELP = 'aontu mod tidy|verify|vendor|manifest [dir] (try --help)'
2110
+ const PKG_HELP =
2111
+ 'aontu pkg tidy|verify|vendor|manifest|refreeze|tree|outdated|serve [dir] | keygen <file> (try --help)'
2052
2112
 
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
2113
+ const PKG_SUBS = [
2114
+ 'tidy', 'verify', 'vendor', 'manifest', 'refreeze', 'tree', 'outdated',
2115
+ 'serve', 'keygen',
2116
+ ]
2117
+
2118
+ // The options every package verb parses, so `sync`, `get`, `publish`
2119
+ // and the `pkg` subcommands read one table.
2120
+ type PkgArgs = {
2121
+ rest: string[]
2122
+ format: SubsumeFormat
2123
+ against?: string
2124
+ frozen: boolean
2125
+ yes: boolean
2126
+ to?: string
2127
+ key?: string
2128
+ token?: string
2129
+ base: string[]
2130
+ write?: string
2131
+ upstream: string[]
2132
+ listen?: string
2133
+ }
2063
2134
 
2135
+ function parsePkgArgs(argv: string[], verb: string, io: Io): PkgArgs | undefined {
2136
+ const out: PkgArgs = {
2137
+ rest: [], format: 'text', frozen: false, yes: false, base: [], upstream: [],
2138
+ }
2139
+ const value = (name: string, i: number): string | undefined => {
2140
+ const v = argv[i]
2141
+ if (null == v) {
2142
+ io.err(`aontu: ${name} needs a value\n`)
2143
+ }
2144
+ return v
2145
+ }
2064
2146
  for (let i = 0; i < argv.length; i++) {
2065
2147
  const arg = argv[i]
2066
2148
  if ('-h' === arg || '--help' === arg) {
2067
- process.stdout.write(HELP)
2068
- return 0
2149
+ io.out(HELP)
2150
+ return undefined
2069
2151
  }
2070
2152
  if ('--format' === arg) {
2071
2153
  const f = argv[++i]
2072
2154
  if ('text' !== f && 'json' !== f) {
2073
- process.stderr.write('aontu: --format needs text or json\n')
2074
- return 2
2155
+ io.err('aontu: --format needs text or json\n')
2156
+ return undefined
2075
2157
  }
2076
- format = f
2158
+ out.format = f
2077
2159
  }
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
2160
+ else if ('--frozen' === arg) {
2161
+ out.frozen = true
2162
+ }
2163
+ else if ('--yes' === arg) {
2164
+ out.yes = true
2165
+ }
2166
+ else if ('--against' === arg || '--to' === arg || '--key' === arg ||
2167
+ '--token' === arg || '--write' === arg || '--listen' === arg) {
2168
+ const v = value(arg, ++i)
2169
+ if (null == v) {
2170
+ return undefined
2171
+ }
2172
+ out['--against' === arg ? 'against' : '--to' === arg ? 'to' :
2173
+ '--key' === arg ? 'key' : '--token' === arg ? 'token' :
2174
+ '--write' === arg ? 'write' : 'listen'] = v
2175
+ }
2176
+ else if ('--base' === arg || '--upstream' === arg) {
2177
+ const v = value(arg, ++i)
2178
+ if (null == v) {
2179
+ return undefined
2083
2180
  }
2084
- against = a
2181
+ out['--base' === arg ? 'base' : 'upstream'].push(v)
2085
2182
  }
2086
2183
  else if (arg.startsWith('-')) {
2087
- process.stderr.write(`aontu: unknown mod option ${arg} (try --help)\n`)
2088
- return 2
2184
+ io.err(`aontu: unknown ${verb} option ${arg} (try --help)\n`)
2185
+ return undefined
2089
2186
  }
2090
2187
  else {
2091
- rest.push(arg)
2188
+ out.rest.push(arg)
2092
2189
  }
2093
2190
  }
2191
+ return out
2192
+ }
2193
+
2194
+
2195
+ // A verb that finds the older layout names the current one, once, and
2196
+ // reads nothing from it.
2197
+ function nameOldLayout(dir: string, io: Io): void {
2198
+ const old = ['aon_vendor', 'mod-lock.aon', 'mod.aon',
2199
+ join(META_DIR, 'mod-lock.aon')].filter((f) => existsSync(join(dir, f)))
2200
+ if (0 < old.length) {
2201
+ io.err(
2202
+ 'aontu: ' + old.join(', ') + ' belong to an older layout: the package ' +
2203
+ 'file is ' + PKG_FILE + ', the lockfile ' + join(META_DIR, LOCK_FILE) +
2204
+ ' and the vendor tree ' + join(META_DIR, 'vendor') + '; rename ' +
2205
+ PKG_FILE + '\'s `mod` block to `pkg`, then run aontu sync\n')
2206
+ }
2207
+ }
2094
2208
 
2095
- const sub = rest[0]
2096
- const dir = rest[1] ?? '.'
2097
2209
 
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")
2210
+ function runPkg(argv: string[], servers: Servers): number | Promise<number> {
2211
+ const io = servers.io ?? PROCESS_IO
2212
+ const trusted = takeTrust(argv, io)
2213
+ if (null == trusted) {
2103
2214
  return 2
2104
2215
  }
2216
+ const args = parsePkgArgs(trusted.argv, 'pkg', io)
2217
+ if (null == args) {
2218
+ return '-h' === trusted.argv[0] || trusted.argv.includes('--help') ? 0 : 2
2219
+ }
2220
+ const trust = trusted.trust
2221
+ const sub = args.rest[0]
2222
+ const dir = args.rest[1] ?? '.'
2105
2223
 
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`)
2224
+ if (!PKG_SUBS.includes(sub) || 2 < args.rest.length) {
2225
+ io.err(
2226
+ `aontu: pkg needs one of ${PKG_SUBS.join(', ')}\n${PKG_HELP}\n`)
2109
2227
  return 2
2110
2228
  }
2111
2229
 
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')
2230
+ if ('keygen' === sub) {
2231
+ if (2 !== args.rest.length) {
2232
+ io.err('aontu: pkg keygen needs the file to write\naontu pkg keygen <file>\n')
2233
+ return 2
2234
+ }
2235
+ const made = keygen(args.rest[1])
2236
+ if (null != made.refused) {
2237
+ io.err('aontu: ' + made.refused + '\n')
2238
+ return 2
2239
+ }
2240
+ io.out('signer: ' + made.signer + '\n')
2241
+ return 0
2116
2242
  }
2117
2243
 
2118
- // `--against` gates a manifest and means nothing to the other two;
2244
+ nameOldLayout(dir, io)
2245
+
2246
+ // `--against` gates a manifest and means nothing to the others;
2119
2247
  // 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')
2248
+ if (null != args.against && 'manifest' !== sub) {
2249
+ io.err('aontu: --against is a manifest option\n')
2122
2250
  return 2
2123
2251
  }
2124
2252
 
2125
- const modopts = modToolOptions(trust, resolve(dir))
2253
+ const opts = pkgToolOptions(trust, resolve(dir))
2126
2254
 
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)
2255
+ if ('outdated' === sub || 'serve' === sub) {
2256
+ return runPkgNet(sub, dir, args, opts, servers, io)
2257
+ }
2132
2258
 
2133
- process.stdout.write(('json' === format ?
2134
- exactJSON({ aontu: { version: version(), verb: 'mod ' + sub }, ...report },
2259
+ const report =
2260
+ 'tidy' === sub ? pkgTidy(dir, opts) :
2261
+ 'verify' === sub ? pkgVerify(dir, opts) :
2262
+ 'vendor' === sub ? pkgVendor(dir, opts) :
2263
+ 'refreeze' === sub ? pkgRefreeze(dir, opts) :
2264
+ 'tree' === sub ? pkgTree(dir, opts) :
2265
+ pkgManifest(dir, opts, args.against)
2266
+
2267
+ io.out(('json' === args.format ?
2268
+ exactJSON({ aontu: { version: version(), verb: 'pkg ' + sub }, ...report },
2135
2269
  2) :
2136
- modText(sub, report)) + '\n')
2270
+ pkgText(sub, report)) + '\n')
2137
2271
 
2138
- return MOD_EXIT[report.verdict]
2272
+ return PKG_EXIT[report.verdict]
2139
2273
  }
2140
2274
 
2141
2275
 
2142
- const MOD_SUBS = ['tidy', 'verify', 'vendor', 'manifest']
2143
-
2144
2276
  // The verdict classes: `ok` 0, a refused gate 1, an open question 3, a
2145
2277
  // document that does not stand up 4 -- `subsume`'s classes, because a
2146
2278
  // manifest gate IS a subsumption check and a caller reading exit codes
2147
2279
  // 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> = {
2280
+ type PkgVerdict =
2281
+ PkgTidyReport['verdict'] |
2282
+ PkgVerifyReport['verdict'] |
2283
+ PkgVendorReport['verdict'] |
2284
+ PkgManifestReport['verdict'] |
2285
+ PkgRefreezeReport['verdict'] |
2286
+ PkgTreeReport['verdict'] |
2287
+ 'frozen' | 'refused' | 'sent' | 'dry-run' | 'outdated' | 'current'
2288
+
2289
+ const PKG_EXIT: Record<PkgVerdict, number> = {
2155
2290
  ok: 0,
2156
2291
  missing: 1,
2157
2292
  mismatch: 1,
@@ -2159,6 +2294,12 @@ const MOD_EXIT: Record<ModVerdict, number> = {
2159
2294
  // nothing to check, which is a refusal and not a pass.
2160
2295
  unlocked: 1,
2161
2296
  breaking: 1,
2297
+ frozen: 1,
2298
+ refused: 1,
2299
+ outdated: 1,
2300
+ sent: 0,
2301
+ 'dry-run': 0,
2302
+ current: 0,
2162
2303
  undecided: 3,
2163
2304
  error: 4,
2164
2305
  }
@@ -2167,7 +2308,7 @@ const MOD_EXIT: Record<ModVerdict, number> = {
2167
2308
  // The tooling's evaluator: the same standalone evaluation the module
2168
2309
  // resolver verifies with (ts/src/mod.ts), and for the same reason —
2169
2310
  // only the engine can say what a module MEANS.
2170
- function modToolOptions(trust: TrustArg, entryRoot: string) {
2311
+ function pkgToolOptions(trust: TrustArg, entryRoot: string): PkgToolOptions {
2171
2312
  const opts = verbOpts(trust, entryRoot)
2172
2313
  // The user cache lives outside any confinement root, so a confined
2173
2314
  // run reads the vendor tree only -- as the evaluator's own module
@@ -2184,7 +2325,7 @@ function modToolOptions(trust: TrustArg, entryRoot: string) {
2184
2325
  hash: canonHash(val),
2185
2326
  canon: val.canon,
2186
2327
  // The same question `aontu hash` asks before it will answer:
2187
- // did this document stand up ON ITS OWN? See ModToolEval.
2328
+ // did this document stand up ON ITS OWN? See PkgToolEval.
2188
2329
  ok: 0 === ctx.err.length && true !== val.isNil,
2189
2330
  }
2190
2331
  },
@@ -2192,19 +2333,54 @@ function modToolOptions(trust: TrustArg, entryRoot: string) {
2192
2333
  }
2193
2334
 
2194
2335
 
2195
- function modText(sub: string, report: any): string {
2336
+ function pkgLockLines(entries: any[]): string[] {
2337
+ return entries.map((e) => e.key + ' ' + e.v + ' ' + e.canon)
2338
+ }
2339
+
2340
+ // The renderers verify, tidy and sync share, as the Go port's do.
2341
+ function pkgMismatchLines(mismatched: any[]): string[] {
2342
+ return mismatched.map((m) => 'canon' === m.pin ?
2343
+ m.key + ': pinned ' + m.want + ' but the store means ' +
2344
+ ('' === m.got ? 'nothing (it does not evaluate)' : m.got) :
2345
+ m.key + ': pinned ' + m.pin + ' ' + m.want + ' but the store holds ' + m.got)
2346
+ }
2347
+
2348
+ // NOT a fetch: the package may well be sitting in the store. What is
2349
+ // absent is the PIN, and only a sync writes one.
2350
+ function pkgUnlockedLines(unlocked: string[]): string[] {
2351
+ return unlocked.map((key) => key + ': not in the lockfile (run: aontu sync)')
2352
+ }
2353
+
2354
+ function pkgForbiddenLines(forbidden: string[]): string[] {
2355
+ return forbidden.map((f) => f + ': not admitted in a package')
2356
+ }
2357
+
2358
+
2359
+ function pkgText(sub: string, report: any): string {
2196
2360
  const lines = ['verdict: ' + report.verdict]
2197
2361
 
2198
2362
  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)
2363
+ const m = report.manifest
2364
+ if (null != m) {
2365
+ lines.push(m.package + ' ' + m.version + ' ' + m.publish)
2366
+ lines.push('archive: ' + m.archive.digest + ' (' + m.archive.files.length +
2367
+ ' files, ' + m.archive.size + ' bytes)')
2368
+ for (const mod of m.modules) {
2369
+ lines.push('module: ' + mod.path + ' ' + mod.main + ' ' + mod.canon)
2370
+ }
2371
+ for (const key of Object.keys(m.deps).sort(cmpCodePoint)) {
2372
+ lines.push('dep: ' + key + ' ' + m.deps[key].v +
2373
+ (null == m.deps[key].pkg ? '' : ' (' + m.deps[key].pkg + ')'))
2374
+ }
2375
+ for (const v of m.retract ?? []) {
2376
+ lines.push('retract: ' + v)
2377
+ }
2378
+ if (null != m.moved) {
2379
+ lines.push('moved: ' + m.moved)
2380
+ }
2381
+ for (const f of m.archive.files) {
2382
+ lines.push('file: ' + f.path + ' ' + f.digest + ' ' + f.size)
2383
+ }
2208
2384
  }
2209
2385
  for (const f of report.findings) {
2210
2386
  lines.push(f.path + ': ' + f.message)
@@ -2212,48 +2388,313 @@ function modText(sub: string, report: any): string {
2212
2388
  for (const miss of report.missing) {
2213
2389
  lines.push(miss + ': missing')
2214
2390
  }
2391
+ lines.push(...pkgForbiddenLines(report.forbidden))
2215
2392
  return lines.join('\n')
2216
2393
  }
2217
2394
 
2218
2395
  if ('verify' === sub) {
2219
- for (const mod of report.verified) {
2220
- lines.push(mod + ': verified')
2396
+ for (const key of report.verified) {
2397
+ lines.push(key + ': verified')
2398
+ }
2399
+ lines.push(...pkgMismatchLines(report.mismatched), ...pkgUnlockedLines(report.unlocked))
2400
+ for (const miss of report.missing) {
2401
+ lines.push(miss + ': not fetched (run: aontu sync)')
2402
+ }
2403
+ return lines.join('\n')
2404
+ }
2405
+
2406
+ if ('refreeze' === sub) {
2407
+ for (const r of report.repinned) {
2408
+ lines.push(r.key + ': ' + r.from + ' -> ' + r.to)
2221
2409
  }
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))
2410
+ for (const key of report.unchanged) {
2411
+ lines.push(key + ': unchanged')
2225
2412
  }
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)')
2413
+ for (const bad of report.unevaluable) {
2414
+ lines.push(bad + ': does not evaluate on its own; nothing to pin')
2230
2415
  }
2231
2416
  for (const miss of report.missing) {
2232
- lines.push(miss + ': not fetched (run: aontu mod get)')
2417
+ lines.push(miss + ': not fetched (run: aontu sync)')
2233
2418
  }
2234
2419
  return lines.join('\n')
2235
2420
  }
2236
2421
 
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)
2422
+ if ('tree' === sub) {
2423
+ const byKey = new Map(report.nodes.map((n: any) => [n.key, n]))
2424
+ const seen = new Set<string>()
2425
+ const walk = (key: string, depth: number): void => {
2426
+ const node: any = byKey.get(key)
2427
+ const again = seen.has(key)
2428
+ seen.add(key)
2429
+ lines.push(' '.repeat(depth) + key +
2430
+ (null == node || '' === node.v ? '' : ' ' + node.v) +
2431
+ (again ? ' (above)' : null == node ? ' (not locked)' : ''))
2432
+ if (again || null == node) {
2433
+ return
2434
+ }
2435
+ for (const dep of node.deps) {
2436
+ walk(dep, depth + 1)
2437
+ }
2438
+ }
2439
+ walk(report.root, 0)
2440
+ for (const miss of report.missing) {
2441
+ lines.push(miss + ': not fetched (run: aontu sync)')
2442
+ }
2443
+ return lines.join('\n')
2241
2444
  }
2242
- // A module that is PRESENT but does not stand up. Named separately
2445
+
2446
+ const done: any[] = 'tidy' === sub ? pkgLockLines(report.lock) : report.vendored
2447
+ lines.push(...done)
2448
+ // A package that is PRESENT but does not stand up. Named separately
2243
2449
  // 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
2450
+ // help, the package itself has to be fixed (or its own dependencies
2245
2451
  // vendored beside it). Before the missing tail, as the Go port's
2246
2452
  // shared renderer orders them.
2247
2453
  for (const bad of report.unevaluable ?? []) {
2248
2454
  lines.push(bad + ': does not evaluate on its own; nothing to pin')
2249
2455
  }
2456
+ lines.push(...pkgForbiddenLines(report.forbidden ?? []))
2250
2457
  for (const miss of report.missing) {
2251
- lines.push(miss + ': not fetched (run: aontu mod get)')
2458
+ lines.push(miss + ': not fetched (run: aontu sync)')
2252
2459
  }
2253
2460
  return lines.join('\n')
2254
2461
  }
2255
2462
 
2256
2463
 
2464
+ // The two `pkg` subcommands that reach a repository.
2465
+ async function runPkgNet(sub: string, dir: string, args: PkgArgs,
2466
+ opts: PkgToolOptions, servers: Servers, io: Io): Promise<number> {
2467
+ if ('serve' === sub) {
2468
+ const served = await startServe({
2469
+ dir: resolve(dir), upstream: args.upstream,
2470
+ listen: args.listen ?? '127.0.0.1:8017', http: servers.http(),
2471
+ })
2472
+ io.out('serving ' + resolve(dir) + ' at ' + served.url + '\n' +
2473
+ args.upstream.map((u) => 'upstream: ' + u + '\n').join(''))
2474
+ await servers.serve(served)
2475
+ await served.close()
2476
+ return 0
2477
+ }
2478
+ if (null == opts.cache) {
2479
+ io.err(NO_CACHE)
2480
+ return 2
2481
+ }
2482
+ const report = await pkgOutdated(dir, opts, servers.http(), { base: netBase(args) })
2483
+ io.out(pkgReportText('pkg outdated', args.format, report) + '\n')
2484
+ return PKG_EXIT[report.verdict]
2485
+ }
2486
+
2487
+ const NO_CACHE = 'aontu: this verb reads and writes the user cache, which a ' +
2488
+ 'confined run (--trust root) does not reach and this host does not name ' +
2489
+ '(set XDG_CACHE_HOME or HOME)\n'
2490
+
2491
+ function netBase(args: PkgArgs): string[] | undefined {
2492
+ return 0 === args.base.length ? undefined : args.base
2493
+ }
2494
+
2495
+ const PACKAGE_VERBS = ['sync', 'add', 'get', 'remove', 'why', 'publish']
2496
+
2497
+ const PACKAGE_HELP: Record<string, string> = {
2498
+ sync: 'aontu sync [--frozen] [dir] (try --help)',
2499
+ add: 'aontu add <pkg> [dir] (try --help)',
2500
+ get: 'aontu get <pkg>[@<version>] [dir] (try --help)',
2501
+ remove: 'aontu remove <pkg> [dir] (try --help)',
2502
+ why: 'aontu why <pkg> [dir] (try --help)',
2503
+ publish: 'aontu publish [--yes] [--to <dir>] [--key <file>] [dir] (try --help)',
2504
+ }
2505
+
2506
+ // The top-level package verbs (ADR-039 part 5). Every one but `why`
2507
+ // reaches a repository, so every one runs behind the seam.
2508
+ async function runPackageVerb(verb: string, argv: string[], servers: Servers):
2509
+ Promise<number> {
2510
+ const io = servers.io ?? PROCESS_IO
2511
+ const trusted = takeTrust(argv, io)
2512
+ if (null == trusted) {
2513
+ return 2
2514
+ }
2515
+ const args = parsePkgArgs(trusted.argv, verb, io)
2516
+ if (null == args) {
2517
+ return '-h' === trusted.argv[0] || trusted.argv.includes('--help') ? 0 : 2
2518
+ }
2519
+ const wantsPkg = 'sync' !== verb && 'publish' !== verb
2520
+ const dir = args.rest[wantsPkg ? 1 : 0] ?? '.'
2521
+ if (args.rest.length > (wantsPkg ? 2 : 1) || (wantsPkg && 0 === args.rest.length)) {
2522
+ io.err('aontu: ' + verb + (wantsPkg ? ' needs a package' : ' takes a directory') +
2523
+ '\n' + PACKAGE_HELP[verb] + '\n')
2524
+ return 2
2525
+ }
2526
+ nameOldLayout(dir, io)
2527
+ const opts = pkgToolOptions(trusted.trust, resolve(dir))
2528
+ const http = servers.http()
2529
+
2530
+ if ('why' === verb) {
2531
+ const report = pkgWhy(dir, opts, args.rest[0])
2532
+ io.out(pkgReportText('why', args.format, report) + '\n')
2533
+ return PKG_EXIT[report.verdict]
2534
+ }
2535
+
2536
+ if ('publish' === verb) {
2537
+ for (const f of [args.key, args.token]) {
2538
+ if (null != f && !existsSync(f)) {
2539
+ io.err('aontu: cannot read ' + f + '\n')
2540
+ return 2
2541
+ }
2542
+ }
2543
+ if (true === args.yes && null == args.key) {
2544
+ io.err('aontu: publish --yes needs --key <file>, the Ed25519 key that signs\n')
2545
+ return 2
2546
+ }
2547
+ const report = await pkgPublish(dir, opts, http, {
2548
+ yes: args.yes, to: null == args.to ? undefined : resolve(args.to), key: args.key,
2549
+ token: args.token, against: args.against, base: netBase(args), write: args.write,
2550
+ })
2551
+ io.out(pkgReportText('publish', args.format, report) + '\n')
2552
+ return PKG_EXIT[report.verdict]
2553
+ }
2554
+
2555
+ if (null == opts.cache) {
2556
+ io.err(NO_CACHE)
2557
+ return 2
2558
+ }
2559
+ const net = { base: netBase(args) }
2560
+ const report: PkgSyncReport | PkgChangeReport | string =
2561
+ 'sync' === verb ? await pkgSync(dir, opts, http, { ...net, frozen: args.frozen }) :
2562
+ 'remove' === verb ? await pkgRemove(dir, opts, http, args.rest[0], net) :
2563
+ await pkgGet(dir, opts, http, args.rest[0], { ...net, mode: verb as 'add' | 'get' })
2564
+ if ('string' === typeof report) {
2565
+ io.err('aontu: ' + report + '\n')
2566
+ return 2
2567
+ }
2568
+ io.out(pkgReportText(verb, args.format, report) + '\n')
2569
+ return PKG_EXIT[report.verdict]
2570
+ }
2571
+
2572
+
2573
+ function pkgReportText(verb: string, fmt: SubsumeFormat, report: any): string {
2574
+ return 'json' === fmt ?
2575
+ exactJSON({ aontu: { version: version(), verb }, ...report }, 2) :
2576
+ pkgNetText(verb, report)
2577
+ }
2578
+
2579
+
2580
+ function pkgNetText(verb: string, report: any): string {
2581
+ const lines = ['verdict: ' + report.verdict]
2582
+ const tail = (): void => {
2583
+ for (const e of report.events ?? []) {
2584
+ lines.push(e.code + ': ' + e.message)
2585
+ }
2586
+ if (null != report.refusal) {
2587
+ lines.push('refused: ' + report.refusal.code + ': ' + report.refusal.message)
2588
+ }
2589
+ }
2590
+
2591
+ if ('why' === verb) {
2592
+ const why = report as PkgWhyReport
2593
+ for (const p of why.paths) {
2594
+ lines.push(p.join(' -> '))
2595
+ }
2596
+ if ('missing' === why.verdict) {
2597
+ lines.push(why.pkg + ': not in the closure')
2598
+ }
2599
+ return lines.join('\n')
2600
+ }
2601
+
2602
+ if ('publish' === verb) {
2603
+ const pub = report as PkgPublishReport
2604
+ lines.push(...pkgText('manifest', pub).split('\n').slice(1))
2605
+ if (null != pub.digest) {
2606
+ lines.push('digest: ' + pub.digest)
2607
+ }
2608
+ if (null != pub.signer) {
2609
+ lines.push('signer: ' + pub.signer)
2610
+ }
2611
+ if (null != pub.against) {
2612
+ lines.push('against: ' + pub.against)
2613
+ }
2614
+ if (null != pub.to) {
2615
+ lines.push('to: ' + pub.to)
2616
+ }
2617
+ if (null != pub.write) {
2618
+ lines.push('write: ' + pub.write + PUBLISH_PATH)
2619
+ }
2620
+ if ('dry-run' === pub.verdict) {
2621
+ lines.push('dry run: nothing sent (add --yes)')
2622
+ }
2623
+ if ('sent' === pub.verdict) {
2624
+ lines.push('sent')
2625
+ }
2626
+ tail()
2627
+ return lines.join('\n')
2628
+ }
2629
+
2630
+ if ('pkg outdated' === verb) {
2631
+ const out = report as PkgOutdatedReport
2632
+ for (const e of out.locked) {
2633
+ if (0 < versionCompare(e.newest, e.v)) {
2634
+ lines.push(e.key + ' ' + e.v + ' -> ' + e.newest)
2635
+ for (const m of e.moves) {
2636
+ lines.push(' ' + m)
2637
+ }
2638
+ }
2639
+ else {
2640
+ lines.push(e.key + ' ' + e.v + ': current')
2641
+ }
2642
+ if (null != e.retracted) {
2643
+ lines.push(e.key + ' ' + e.v + ': retracted by ' + e.retracted)
2644
+ }
2645
+ }
2646
+ tail()
2647
+ return lines.join('\n')
2648
+ }
2649
+
2650
+ const sync = report as PkgChangeReport
2651
+ if (null != sync.change) {
2652
+ lines.push('change: ' + sync.change)
2653
+ }
2654
+ for (const f of sync.fetched) {
2655
+ lines.push('fetched: ' + f)
2656
+ }
2657
+ lines.push(...pkgLockLines(sync.lock))
2658
+ for (const bad of sync.unevaluable) {
2659
+ lines.push(bad + ': does not evaluate on its own; nothing to pin')
2660
+ }
2661
+ lines.push(...pkgForbiddenLines(sync.forbidden), ...pkgMismatchLines(sync.mismatched),
2662
+ ...pkgUnlockedLines(sync.unlocked))
2663
+ for (const miss of sync.missing) {
2664
+ lines.push(miss + ': not fetched (run: aontu sync)')
2665
+ }
2666
+ for (const c of sync.changes) {
2667
+ lines.push('lockfile would change: ' + c)
2668
+ }
2669
+ tail()
2670
+ return lines.join('\n')
2671
+ }
2672
+
2673
+
2674
+ const MODEL_HELP = 'aontu model get|why|set ... (try --help)'
2675
+
2676
+ // One document, interrogated or edited (ADR-039 part 5).
2677
+ function runModel(argv: string[]): number {
2678
+ const sub = argv[0]
2679
+ if ('-h' === sub || '--help' === sub) {
2680
+ process.stdout.write(HELP)
2681
+ return 0
2682
+ }
2683
+ if ('get' === sub) {
2684
+ return runGet(argv.slice(1))
2685
+ }
2686
+ if ('why' === sub) {
2687
+ return runWhy(argv.slice(1))
2688
+ }
2689
+ if ('set' === sub) {
2690
+ return runSet(argv.slice(1))
2691
+ }
2692
+ process.stderr.write(`aontu: model needs get, why or set\n${MODEL_HELP}\n`)
2693
+ return 2
2694
+ }
2695
+
2696
+
2697
+
2257
2698
  function vacuous(what: string, why: string): void {
2258
2699
  process.stderr.write(`aontu: ${what}: ${why}\n`)
2259
2700
  }
@@ -3272,7 +3713,7 @@ function runHash(argv: string[]): number {
3272
3713
  }
3273
3714
 
3274
3715
 
3275
- const GET_HELP = 'aontu get <path> <file> (try --help)'
3716
+ const GET_HELP = 'aontu model get <path> <file> (try --help)'
3276
3717
 
3277
3718
  function runGet(argv: string[]): number {
3278
3719
  const trusted = takeTrust(argv)
@@ -3318,7 +3759,7 @@ function runGet(argv: string[]): number {
3318
3759
  format = f
3319
3760
  }
3320
3761
  else if (arg.startsWith('-')) {
3321
- process.stderr.write(`aontu: unknown get option ${arg} (try --help)\n`)
3762
+ process.stderr.write(`aontu: unknown model get option ${arg} (try --help)\n`)
3322
3763
  return 2
3323
3764
  }
3324
3765
  else {
@@ -3327,7 +3768,7 @@ function runGet(argv: string[]): number {
3327
3768
  }
3328
3769
 
3329
3770
  if (2 !== rest.length) {
3330
- process.stderr.write(`aontu: get needs a path and one file\n${GET_HELP}\n`)
3771
+ process.stderr.write(`aontu: model get needs a path and one file\n${GET_HELP}\n`)
3331
3772
  return 2
3332
3773
  }
3333
3774
  const [path, file] = rest
@@ -3355,7 +3796,7 @@ function runGet(argv: string[]): number {
3355
3796
  })
3356
3797
  if ('json' === format) {
3357
3798
  process.stdout.write(exactJSON({
3358
- aontu: { version: version(), verb: 'get' },
3799
+ aontu: { version: version(), verb: 'model get' },
3359
3800
  findings: report.findings,
3360
3801
  ok: report.ok,
3361
3802
  out: report.out,
@@ -3384,7 +3825,7 @@ function runGet(argv: string[]): number {
3384
3825
  // The positive twin of the vet report: errors explain what failed to
3385
3826
  // unify, this explains what did.
3386
3827
 
3387
- const WHY_HELP = 'aontu why <path> <file> (try --help)'
3828
+ const WHY_HELP = 'aontu model why <path> <file> (try --help)'
3388
3829
 
3389
3830
  function runWhy(argv: string[]): number {
3390
3831
  const trusted = takeTrust(argv)
@@ -3411,7 +3852,7 @@ function runWhy(argv: string[]): number {
3411
3852
  format = f
3412
3853
  }
3413
3854
  else if (arg.startsWith('-')) {
3414
- process.stderr.write(`aontu: unknown why option ${arg} (try --help)\n`)
3855
+ process.stderr.write(`aontu: unknown model why option ${arg} (try --help)\n`)
3415
3856
  return 2
3416
3857
  }
3417
3858
  else {
@@ -3420,7 +3861,7 @@ function runWhy(argv: string[]): number {
3420
3861
  }
3421
3862
 
3422
3863
  if (2 !== rest.length) {
3423
- process.stderr.write(`aontu: why needs a path and one file\n${WHY_HELP}\n`)
3864
+ process.stderr.write(`aontu: model why needs a path and one file\n${WHY_HELP}\n`)
3424
3865
  return 2
3425
3866
  }
3426
3867
  const [path, file] = rest
@@ -3439,7 +3880,7 @@ function runWhy(argv: string[]): number {
3439
3880
  })
3440
3881
  if ('json' === format) {
3441
3882
  process.stdout.write(exactJSON({
3442
- aontu: { version: version(), verb: 'why' },
3883
+ aontu: { version: version(), verb: 'model why' },
3443
3884
  findings: report.findings,
3444
3885
  ok: report.ok,
3445
3886
  ...(null == report.record ? {} : { record: report.record }),
@@ -3477,7 +3918,7 @@ function renderWhyText(record: WhyRecord): string {
3477
3918
 
3478
3919
 
3479
3920
  const SET_HELP =
3480
- 'aontu set <path>=<value> --entry <file> --overlay <file> (try --help)'
3921
+ 'aontu model set <path>=<value> --entry <file> --overlay <file> (try --help)'
3481
3922
 
3482
3923
  function runSet(argv: string[]): number {
3483
3924
  const trusted = takeTrust(argv)
@@ -3520,7 +3961,7 @@ function runSet(argv: string[]): number {
3520
3961
  format = f
3521
3962
  }
3522
3963
  else if (arg.startsWith('-')) {
3523
- process.stderr.write(`aontu: unknown set option ${arg} (try --help)\n`)
3964
+ process.stderr.write(`aontu: unknown model set option ${arg} (try --help)\n`)
3524
3965
  return 2
3525
3966
  }
3526
3967
  else {
@@ -3530,7 +3971,7 @@ function runSet(argv: string[]): number {
3530
3971
 
3531
3972
  if (0 === assignments.length || null == entry || null == overlayFile) {
3532
3973
  process.stderr.write(
3533
- `aontu: set needs assignments, --entry and --overlay\n${SET_HELP}\n`)
3974
+ `aontu: model set needs assignments, --entry and --overlay\n${SET_HELP}\n`)
3534
3975
  return 2
3535
3976
  }
3536
3977
 
@@ -3582,7 +4023,7 @@ function runSet(argv: string[]): number {
3582
4023
 
3583
4024
  if ('json' === format) {
3584
4025
  process.stdout.write(exactJSON({
3585
- aontu: { version: version(), verb: 'set' },
4026
+ aontu: { version: version(), verb: 'model set' },
3586
4027
  appended: report.appended,
3587
4028
  findings: report.findings,
3588
4029
  overlay: report.overlay,
@@ -4082,17 +4523,38 @@ function fmtOne(
4082
4523
  }
4083
4524
 
4084
4525
 
4526
+ // Where a verb writes. The package verbs are asynchronous, so a test
4527
+ // cannot capture the process streams around them; it hands these in.
4528
+ type Io = { out: (s: string) => void, err: (s: string) => void }
4529
+
4530
+ const PROCESS_IO: Io = {
4531
+ out: (s) => void process.stdout.write(s),
4532
+ err: (s) => void process.stderr.write(s),
4533
+ }
4534
+
4085
4535
  type Servers = {
4086
4536
  lsp: () => void
4087
4537
  mcp: (argv: string[]) => void
4538
+ // `pkg serve` runs until this settles: the process's SIGINT, or a
4539
+ // test's say-so.
4540
+ serve: (served: Served) => Promise<void>
4541
+ http: () => PkgHttp
4542
+ io?: Io
4543
+ }
4544
+
4545
+ // `pkg serve` runs until the process is interrupted.
4546
+ function serveUntilInterrupted(): Promise<void> {
4547
+ return new Promise((done) => process.once('SIGINT', () => done()))
4088
4548
  }
4089
4549
 
4090
4550
  // Excluded: the real pair takes the process stdio, so ts/test/cli.test.ts
4091
4551
  // drives each server through a child process instead.
4092
- /* node:coverage ignore next 4 */
4552
+ /* node:coverage ignore next 6 */
4093
4553
  const SERVERS: Servers = {
4094
4554
  lsp: () => void lspMain(),
4095
4555
  mcp: (argv) => void mcpMain(undefined, undefined, undefined, undefined, argv),
4556
+ serve: serveUntilInterrupted,
4557
+ http: defaultHttp,
4096
4558
  }
4097
4559
 
4098
4560
  // undefined: the server took the process; a number: an answer the CLI
@@ -4416,10 +4878,10 @@ function runInit(argv: string[]): number {
4416
4878
 
4417
4879
 
4418
4880
  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',
4881
+ 'add', 'agentsmd', 'allow', 'breaking', 'explain', 'fmt', 'get', 'hash',
4882
+ 'help', 'init', 'jsonschema', 'lsp', 'mcp', 'model', 'pkg', 'publish',
4883
+ 'reaches', 'relations', 'remove', 'subsume', 'sync', 'template', 'trace',
4884
+ 'trim', 'vet', 'view', 'why',
4423
4885
  ]
4424
4886
 
4425
4887
 
@@ -4514,20 +4976,22 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4514
4976
  return servers.mcp(argv.slice(3))
4515
4977
  }
4516
4978
 
4517
- if ('set' === argv[2]) {
4518
- return finish(runSet(argv.slice(3)))
4519
- }
4520
4979
 
4521
4980
  if ('allow' === argv[2]) {
4522
4981
  return finish(runAllow(argv.slice(3)))
4523
4982
  }
4524
4983
 
4525
- if ('why' === argv[2]) {
4526
- return finish(runWhy(argv.slice(3)))
4984
+ if ('model' === argv[2]) {
4985
+ return finish(runModel(argv.slice(3)))
4986
+ }
4987
+
4988
+ if ('pkg' === argv[2]) {
4989
+ const r = runPkg(argv.slice(3), servers)
4990
+ return 'number' === typeof r ? finish(r) : void r.then(finish)
4527
4991
  }
4528
4992
 
4529
- if ('get' === argv[2]) {
4530
- return finish(runGet(argv.slice(3)))
4993
+ if (PACKAGE_VERBS.includes(argv[2])) {
4994
+ return void runPackageVerb(argv[2], argv.slice(3), servers).then(finish)
4531
4995
  }
4532
4996
 
4533
4997
  if ('hash' === argv[2]) {
@@ -4548,10 +5012,6 @@ function main(argv: string[], servers: Servers = SERVERS): void {
4548
5012
  return finish(runInit(argv.slice(3)))
4549
5013
  }
4550
5014
 
4551
- if ('mod' === argv[2]) {
4552
- return finish(runMod(argv.slice(3)))
4553
- }
4554
-
4555
5015
  if ('relations' === argv[2]) {
4556
5016
  return finish(runRelations(argv.slice(3)))
4557
5017
  }
@@ -4686,7 +5146,7 @@ export {
4686
5146
  runJsonSchema,
4687
5147
  runTemplate,
4688
5148
  runTrace,
4689
- runMod,
5149
+ runPkg, runModel, runPackageVerb, pkgToolOptions, serveUntilInterrupted,
4690
5150
  runHash, runGet, runHelp, runExplain, runInit, nearestVerb,
4691
5151
  looksLikeVerb,
4692
5152
  KNOWN_VERBS,