aontu 0.63.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.
- package/README.md +8 -8
- package/dist/agentsmd.js +3 -3
- package/dist/agentsmd.js.map +1 -1
- package/dist/aontu.d.ts +3 -3
- package/dist/aontu.js +4 -6
- package/dist/aontu.js.map +1 -1
- package/dist/aontumodel.js +6 -12
- package/dist/aontumodel.js.map +1 -1
- package/dist/casing.d.ts +5 -0
- package/dist/casing.js +90 -0
- package/dist/casing.js.map +1 -0
- package/dist/cli.d.ts +16 -3
- package/dist/cli.js +653 -455
- package/dist/cli.js.map +1 -1
- package/dist/compat.d.ts +7 -0
- package/dist/compat.js +113 -0
- package/dist/compat.js.map +1 -0
- package/dist/helpdoc.js +5 -5
- package/dist/helpdoc.js.map +1 -1
- package/dist/hints.js +14 -5
- package/dist/hints.js.map +1 -1
- package/dist/lang.d.ts +1 -0
- package/dist/lang.js +8 -0
- package/dist/lang.js.map +1 -1
- package/dist/lsp.d.ts +1 -1
- package/dist/mcp.js +0 -36
- package/dist/mcp.js.map +1 -1
- package/dist/mod.d.ts +18 -3
- package/dist/mod.js +117 -46
- package/dist/mod.js.map +1 -1
- package/dist/pkg-net.d.ts +227 -0
- package/dist/pkg-net.js +1598 -0
- package/dist/pkg-net.js.map +1 -0
- package/dist/pkg-zip.d.ts +8 -0
- package/dist/pkg-zip.js +196 -0
- package/dist/pkg-zip.js.map +1 -0
- package/dist/pkg.d.ts +159 -0
- package/dist/pkg.js +767 -0
- package/dist/pkg.js.map +1 -0
- package/dist/profile.d.ts +9 -0
- package/dist/profile.js +28 -0
- package/dist/profile.js.map +1 -0
- package/dist/report-sarif.js +1 -1
- package/dist/report-sarif.js.map +1 -1
- package/dist/subsume.d.ts +3 -1
- package/dist/subsume.js +2 -0
- package/dist/subsume.js.map +1 -1
- package/dist/trace.d.ts +21 -0
- package/dist/trace.js +107 -0
- package/dist/trace.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/unify.js +1 -6
- package/dist/unify.js.map +1 -1
- package/dist/val/CmpFuncVal.d.ts +2 -0
- package/dist/val/CmpFuncVal.js +47 -13
- package/dist/val/CmpFuncVal.js.map +1 -1
- package/dist/val/NomFuncVal.js +11 -13
- package/dist/val/NomFuncVal.js.map +1 -1
- package/dist/val/ReferFuncVal.js +3 -0
- package/dist/val/ReferFuncVal.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +3 -3
- package/skill/error-codes.md +4 -4
- package/skill/examples.md +4 -4
- package/skill/grammar-card.md +1 -1
- package/skill/init/check.sh +2 -2
- package/skill/tasks.md +14 -12
- package/src/agentsmd.ts +3 -3
- package/src/aontu.ts +3 -8
- package/src/aontumodel.ts +6 -12
- package/src/casing.ts +95 -0
- package/src/cli.ts +758 -495
- package/src/compat.ts +148 -0
- package/src/helpdoc.ts +5 -5
- package/src/hints.ts +14 -7
- package/src/lang.ts +9 -1
- package/src/mcp.ts +0 -39
- package/src/mod.ts +142 -51
- package/src/pkg-net.ts +1945 -0
- package/src/pkg-zip.ts +228 -0
- package/src/pkg.ts +1018 -0
- package/src/profile.ts +42 -0
- package/src/report-sarif.ts +1 -1
- package/src/subsume.ts +2 -2
- package/src/trace.ts +157 -0
- package/src/unify.ts +1 -6
- package/src/val/CmpFuncVal.ts +66 -20
- package/src/val/NomFuncVal.ts +3 -5
- package/src/val/ReferFuncVal.ts +4 -0
- package/dist/lower.d.ts +0 -23
- package/dist/lower.js +0 -528
- package/dist/lower.js.map +0 -1
- package/dist/mod-tool.d.ts +0 -58
- package/dist/mod-tool.js +0 -403
- package/dist/mod-tool.js.map +0 -1
- package/dist/render.d.ts +0 -53
- package/dist/render.js +0 -432
- package/dist/render.js.map +0 -1
- package/src/lower.ts +0 -586
- package/src/mod-tool.ts +0 -561
- package/src/render.ts +0 -580
package/src/pkg.ts
ADDED
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync,
|
|
6
|
+
lstatSync, copyFileSync, unlinkSync, rmSync,
|
|
7
|
+
} from 'node:fs'
|
|
8
|
+
import { join as pathJoin, dirname as pathDirname } from 'node:path'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
parseModuleRef, validateModulePath, moduleDir, lockJson, isAlias,
|
|
12
|
+
cacheStoreDir, cacheDownloadDir, localFileExt, PKG_FILE, LOCK_FILE, META_DIR, VENDOR_DIR,
|
|
13
|
+
} from './mod'
|
|
14
|
+
import { zipCanonical, sha256Hex, cmpBytes } from './pkg-zip'
|
|
15
|
+
import { subsume } from './subsume'
|
|
16
|
+
import { compatOutcome } from './compat'
|
|
17
|
+
import { includeFormat } from './lang'
|
|
18
|
+
import type { ZipEntry } from './pkg-zip'
|
|
19
|
+
|
|
20
|
+
export const VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
|
|
21
|
+
|
|
22
|
+
// params.archive: what a consumer refuses to unpack, and so what a
|
|
23
|
+
// publisher refuses to mint. A record, so a test can lower them.
|
|
24
|
+
export const ARCHIVE_LIMITS = {
|
|
25
|
+
bytes: 16777216, unpacked: 67108864, files: 4096, fileBytes: 8388608,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// One entry of the lockfile, and of a tidy report. `key` is the lock
|
|
30
|
+
// key: a package path, or `alias:<name>` with `pkg` naming the package.
|
|
31
|
+
export type LockEntry = {
|
|
32
|
+
key: string
|
|
33
|
+
v: string
|
|
34
|
+
canon: string
|
|
35
|
+
// The canonical archive's digest: computable from any tree.
|
|
36
|
+
archive: string
|
|
37
|
+
// The signed manifest's digest, present only for a package acquired
|
|
38
|
+
// from a repository.
|
|
39
|
+
manifest?: string
|
|
40
|
+
pkg?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type PkgTidyReport = {
|
|
44
|
+
verdict: 'ok' | 'missing' | 'error'
|
|
45
|
+
// The resolved closure, sorted by key.
|
|
46
|
+
lock: LockEntry[]
|
|
47
|
+
missing: string[]
|
|
48
|
+
unevaluable: string[]
|
|
49
|
+
// Files the allowlist refuses, as `<key>: <file>`, sorted.
|
|
50
|
+
forbidden: string[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type PkgMismatch = {
|
|
54
|
+
key: string
|
|
55
|
+
pin: 'archive' | 'manifest' | 'canon'
|
|
56
|
+
want: string
|
|
57
|
+
got: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type PkgVerifyReport = {
|
|
61
|
+
verdict: 'ok' | 'mismatch' | 'unlocked' | 'missing'
|
|
62
|
+
verified: string[]
|
|
63
|
+
// What the lockfile pins against what the store now holds or means,
|
|
64
|
+
// bytes before meaning, sorted by key.
|
|
65
|
+
mismatched: PkgMismatch[]
|
|
66
|
+
// Dependencies the project declares that the lockfile does not name,
|
|
67
|
+
// sorted. A tidy is what fills them in.
|
|
68
|
+
unlocked: string[]
|
|
69
|
+
// Locked packages present in no store, sorted.
|
|
70
|
+
missing: string[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type PkgVendorReport = {
|
|
74
|
+
verdict: 'ok' | 'missing'
|
|
75
|
+
vendored: string[]
|
|
76
|
+
missing: string[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type PkgRefreezeReport = {
|
|
80
|
+
verdict: 'ok' | 'missing' | 'error'
|
|
81
|
+
repinned: { key: string, from: string, to: string }[]
|
|
82
|
+
unchanged: string[]
|
|
83
|
+
missing: string[]
|
|
84
|
+
unevaluable: string[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type PkgTreeNode = { key: string, v: string, deps: string[] }
|
|
88
|
+
|
|
89
|
+
export type PkgTreeReport = {
|
|
90
|
+
verdict: 'ok' | 'missing'
|
|
91
|
+
root: string
|
|
92
|
+
nodes: PkgTreeNode[]
|
|
93
|
+
missing: string[]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
export type PkgToolEval = (src: string, path: string) =>
|
|
98
|
+
{
|
|
99
|
+
gen: any, hash: string, canon: string,
|
|
100
|
+
ok: boolean,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
export type PkgToolOptions = {
|
|
105
|
+
// The content-addressed user cache. Empty means no cache is
|
|
106
|
+
// consulted, which is a store that misses rather than an error.
|
|
107
|
+
cache?: string
|
|
108
|
+
eval: PkgToolEval
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
export type Dependency = { v: string, pkg?: string }
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// The `dep` block a package file declares: key -> minimum version, and
|
|
116
|
+
// for an alias the package it names.
|
|
117
|
+
export function declaredDeps(file: string, options: PkgToolOptions):
|
|
118
|
+
Record<string, Dependency> {
|
|
119
|
+
if (!existsSync(file)) {
|
|
120
|
+
return {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const gen: any = options.eval(readFileSync(file, 'utf8'), file).gen
|
|
124
|
+
const dep = gen?.dep
|
|
125
|
+
if (null == dep || 'object' !== typeof dep) {
|
|
126
|
+
return {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const out: Record<string, Dependency> = {}
|
|
130
|
+
for (const key of Object.keys(dep)) {
|
|
131
|
+
const v = dep[key]?.v
|
|
132
|
+
if ('string' === typeof v && '' !== v) {
|
|
133
|
+
const pkg = dep[key]?.pkg
|
|
134
|
+
out[key] = { v, ...('string' === typeof pkg && '' !== pkg ? { pkg } : {}) }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
export function versionCompare(a: string, b: string): number {
|
|
142
|
+
const ap = a.split('.')
|
|
143
|
+
const bp = b.split('.')
|
|
144
|
+
for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
|
|
145
|
+
// A part the shorter version does not have is ZERO, so `1.2` and
|
|
146
|
+
// `1.2.0` are the same version -- which is what everyone means by
|
|
147
|
+
// them, and what a lockfile rewritten from either must agree on.
|
|
148
|
+
const x = ap[i] ?? '0'
|
|
149
|
+
const y = bp[i] ?? '0'
|
|
150
|
+
if (x === y) {
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
const xn = /^\d+$/.test(x)
|
|
154
|
+
const yn = /^\d+$/.test(y)
|
|
155
|
+
if (xn && yn) {
|
|
156
|
+
const xs = x.replace(/^0+(?=\d)/, '')
|
|
157
|
+
const ys = y.replace(/^0+(?=\d)/, '')
|
|
158
|
+
if (xs === ys) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
return xs.length !== ys.length ? (xs.length < ys.length ? -1 : 1) : (xs < ys ? -1 : 1)
|
|
162
|
+
}
|
|
163
|
+
if (xn !== yn) {
|
|
164
|
+
return xn ? -1 : 1
|
|
165
|
+
}
|
|
166
|
+
return x < y ? -1 : 1
|
|
167
|
+
}
|
|
168
|
+
return 0
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
// A last element with an extension the include table knows is a
|
|
173
|
+
// local file to the resolver, whatever declares it, so it names no
|
|
174
|
+
// package.
|
|
175
|
+
export function usableKey(key: string): boolean {
|
|
176
|
+
const ref = parseModuleRef(key)
|
|
177
|
+
if (undefined === ref || ref.path !== key) {
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
if (isAlias(key)) {
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
const ext = localFileExt(key)
|
|
184
|
+
return undefined === validateModulePath(key) &&
|
|
185
|
+
(undefined === ext || undefined === includeFormat(ext))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
// The directory a package is in, in the local stores: the project's
|
|
190
|
+
// vendor tree first, then the cache under the hash the lockfile pins,
|
|
191
|
+
// then the cache under the hash the repository's manifest for that
|
|
192
|
+
// version pins, which the consumer's own pin need not equal.
|
|
193
|
+
export function storeDir(
|
|
194
|
+
root: string, key: string, canon: string, pkg: string,
|
|
195
|
+
options: PkgToolOptions, v?: string,
|
|
196
|
+
): string | undefined {
|
|
197
|
+
const stores = [moduleDir(pathJoin(root, META_DIR, VENDOR_DIR), key)]
|
|
198
|
+
if (null != options.cache && '' !== pkg) {
|
|
199
|
+
if ('' !== canon) {
|
|
200
|
+
stores.push(cacheStoreDir(options.cache, canon, pkg))
|
|
201
|
+
}
|
|
202
|
+
const served = null == v ? undefined : downloadedCanon(options.cache, pkg, v)
|
|
203
|
+
if (undefined !== served && served !== canon) {
|
|
204
|
+
stores.push(cacheStoreDir(options.cache, served, pkg))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return stores.find((d) => existsSync(pathJoin(d, PKG_FILE)))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The canon the repository's manifest pins for a version this client
|
|
211
|
+
// downloaded, or undefined where none was.
|
|
212
|
+
export function downloadedCanon(cache: string, pkg: string, v: string): string | undefined {
|
|
213
|
+
const file = pathJoin(cacheDownloadDir(cache, pkg), v + '.manifest')
|
|
214
|
+
if (!existsSync(file)) {
|
|
215
|
+
return undefined
|
|
216
|
+
}
|
|
217
|
+
let doc: any
|
|
218
|
+
try {
|
|
219
|
+
doc = JSON.parse(readFileSync(file, 'utf8'))
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined
|
|
223
|
+
}
|
|
224
|
+
const canon = doc?.modules?.[0]?.canon
|
|
225
|
+
return 'string' === typeof canon ? canon : undefined
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
export function readLock(root: string): Record<string, LockEntry> {
|
|
230
|
+
const file = pathJoin(root, META_DIR, LOCK_FILE)
|
|
231
|
+
if (!existsSync(file)) {
|
|
232
|
+
return {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let lock: any
|
|
236
|
+
try {
|
|
237
|
+
lock = JSON.parse(lockJson(readFileSync(file, 'utf8')))
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return {}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const out: Record<string, LockEntry> = {}
|
|
244
|
+
for (const key of Object.keys(lock?.lock ?? {})) {
|
|
245
|
+
const e = lock.lock[key]
|
|
246
|
+
const str = (k: string): string => 'string' === typeof e?.[k] ? e[k] : ''
|
|
247
|
+
out[key] = {
|
|
248
|
+
key,
|
|
249
|
+
v: str('v'),
|
|
250
|
+
canon: str('canon'),
|
|
251
|
+
archive: str('archive'),
|
|
252
|
+
...('' === str('manifest') ? {} : { manifest: str('manifest') }),
|
|
253
|
+
...('' === str('pkg') ? {} : { pkg: str('pkg') }),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return out
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
export function lockText(entries: LockEntry[], options: PkgToolOptions): string {
|
|
261
|
+
const parts = entries.map((e) =>
|
|
262
|
+
JSON.stringify(e.key) + ':{' +
|
|
263
|
+
'"archive":' + JSON.stringify(e.archive) + ',' +
|
|
264
|
+
'"canon":' + JSON.stringify(e.canon) + ',' +
|
|
265
|
+
(null == e.manifest ? '' : '"manifest":' + JSON.stringify(e.manifest) + ',') +
|
|
266
|
+
(null == e.pkg ? '' : '"pkg":' + JSON.stringify(e.pkg) + ',') +
|
|
267
|
+
'"v":' + JSON.stringify(e.v) + '}')
|
|
268
|
+
return options.eval('{"lock":{' + parts.join(',') + '}}', LOCK_FILE).canon
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
// The generated-file header. A lockfile is machine-written, and the
|
|
273
|
+
// file says so where an editor will see it.
|
|
274
|
+
export const LOCK_HEADER =
|
|
275
|
+
'# pkg-lock.aon (generated by `aontu sync`; do not edit)\n'
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
export function writeLock(root: string, entries: LockEntry[],
|
|
279
|
+
options: PkgToolOptions): void {
|
|
280
|
+
mkdirSync(pathJoin(root, META_DIR), { recursive: true })
|
|
281
|
+
writeFileSync(pathJoin(root, META_DIR, LOCK_FILE),
|
|
282
|
+
LOCK_HEADER + lockText(entries, options) + '\n')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
// What a package file says about ITSELF. Distinct from `declaredDeps`,
|
|
287
|
+
// which reads what it says about others.
|
|
288
|
+
export type PackageSelf = {
|
|
289
|
+
path: string
|
|
290
|
+
version: string
|
|
291
|
+
main: string
|
|
292
|
+
publish: 'public' | 'private'
|
|
293
|
+
moved?: string
|
|
294
|
+
retract: string[]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function packageSelf(dir: string, options: PkgToolOptions): PackageSelf {
|
|
298
|
+
const file = pathJoin(dir, PKG_FILE)
|
|
299
|
+
const self: PackageSelf = {
|
|
300
|
+
path: '', version: '', main: 'main.aon', publish: 'private', retract: [],
|
|
301
|
+
}
|
|
302
|
+
if (!existsSync(file)) {
|
|
303
|
+
return self
|
|
304
|
+
}
|
|
305
|
+
const gen: any = options.eval(readFileSync(file, 'utf8'), file).gen
|
|
306
|
+
const pkg = gen?.pkg
|
|
307
|
+
const str = (v: any): string => 'string' === typeof v ? v : ''
|
|
308
|
+
self.path = str(pkg?.path)
|
|
309
|
+
self.version = str(pkg?.version)
|
|
310
|
+
self.main = '' === str(pkg?.main) ? 'main.aon' : str(pkg?.main)
|
|
311
|
+
self.publish = 'public' === gen?.publish ? 'public' : 'private'
|
|
312
|
+
if ('' !== str(gen?.moved)) {
|
|
313
|
+
self.moved = str(gen.moved)
|
|
314
|
+
}
|
|
315
|
+
if (Array.isArray(gen?.retract)) {
|
|
316
|
+
self.retract = gen.retract.filter((v: any) => 'string' === typeof v)
|
|
317
|
+
}
|
|
318
|
+
return self
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
// What a package may contain (REPOSITORY.0.md §9.4): an enumerated
|
|
323
|
+
// allowlist, refused by default. The data half is ADR-012's table.
|
|
324
|
+
export const ARCHIVE_SOURCE_EXT = ['aon', 'aontu']
|
|
325
|
+
export const ARCHIVE_DATA_EXT = [
|
|
326
|
+
'json', 'jsonld', 'jsonc', 'json5', 'jsonic', 'jsc', 'toml', 'yaml', 'yml',
|
|
327
|
+
'ini',
|
|
328
|
+
]
|
|
329
|
+
export const ARCHIVE_TEXT_EXT = ['md', 'txt']
|
|
330
|
+
export const ARCHIVE_NAMED = ['LICENSE', 'NOTICE']
|
|
331
|
+
|
|
332
|
+
const ADMITTED_EXT = new Set([
|
|
333
|
+
...ARCHIVE_SOURCE_EXT, ...ARCHIVE_DATA_EXT, ...ARCHIVE_TEXT_EXT])
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
export function archiveAdmits(rel: string): boolean {
|
|
337
|
+
const elems = rel.split('/')
|
|
338
|
+
if (elems.some((e) => e.startsWith('.'))) {
|
|
339
|
+
return false
|
|
340
|
+
}
|
|
341
|
+
const name = elems[elems.length - 1]
|
|
342
|
+
if (ARCHIVE_NAMED.includes(name)) {
|
|
343
|
+
return true
|
|
344
|
+
}
|
|
345
|
+
const m = /^[^.].*\.([^.]+)$/.exec(name)
|
|
346
|
+
return null != m && ADMITTED_EXT.has(m[1].toLowerCase())
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
export type ArchiveFile = { path: string, digest: string, size: number }
|
|
351
|
+
|
|
352
|
+
export type Archive = {
|
|
353
|
+
zip: Uint8Array
|
|
354
|
+
digest: string
|
|
355
|
+
size: number
|
|
356
|
+
files: ArchiveFile[]
|
|
357
|
+
// Entries the allowlist refuses, sorted: a symlink, an executable, a
|
|
358
|
+
// dotfile directory, or a name outside the table.
|
|
359
|
+
forbidden: string[]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
function walkTree(dir: string, prefix: string, out: ZipEntry[],
|
|
364
|
+
forbidden: string[]): void {
|
|
365
|
+
for (const name of readdirSync(dir).sort(cmpBytes)) {
|
|
366
|
+
if ('' === prefix && META_DIR === name) {
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
const full = pathJoin(dir, name)
|
|
370
|
+
const rel = '' === prefix ? name : prefix + '/' + name
|
|
371
|
+
const st = lstatSync(full)
|
|
372
|
+
if (st.isSymbolicLink()) {
|
|
373
|
+
forbidden.push(rel)
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
if (st.isDirectory()) {
|
|
377
|
+
if (name.startsWith('.')) {
|
|
378
|
+
forbidden.push(rel + '/')
|
|
379
|
+
continue
|
|
380
|
+
}
|
|
381
|
+
walkTree(full, rel, out, forbidden)
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
384
|
+
if (!st.isFile() || 0 !== (st.mode & 0o111) || !archiveAdmits(rel)) {
|
|
385
|
+
forbidden.push(rel)
|
|
386
|
+
continue
|
|
387
|
+
}
|
|
388
|
+
out.push({ path: rel, data: new Uint8Array(readFileSync(full)) })
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
// The canonical archive of a tree, and every file's own digest.
|
|
394
|
+
export function archiveOf(dir: string): Archive {
|
|
395
|
+
const entries: ZipEntry[] = []
|
|
396
|
+
const forbidden: string[] = []
|
|
397
|
+
walkTree(dir, '', entries, forbidden)
|
|
398
|
+
const zip = zipCanonical(entries)
|
|
399
|
+
return {
|
|
400
|
+
zip,
|
|
401
|
+
digest: sha256Hex(zip),
|
|
402
|
+
size: zip.length,
|
|
403
|
+
files: entries.map((e) => ({
|
|
404
|
+
path: e.path, digest: sha256Hex(e.data), size: e.data.length,
|
|
405
|
+
})),
|
|
406
|
+
forbidden: forbidden.sort(cmpBytes),
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
// The pin a locked tree carries beside it, when it was acquired from a
|
|
412
|
+
// repository: the served manifest, verbatim.
|
|
413
|
+
export function storedManifest(dir: string):
|
|
414
|
+
{ digest: string, files: ArchiveFile[] } | undefined {
|
|
415
|
+
const file = pathJoin(dir, META_DIR, 'manifest.aon')
|
|
416
|
+
if (!existsSync(file)) {
|
|
417
|
+
return undefined
|
|
418
|
+
}
|
|
419
|
+
const bytes = new Uint8Array(readFileSync(file))
|
|
420
|
+
let doc: any
|
|
421
|
+
try {
|
|
422
|
+
doc = JSON.parse(lockJson(new TextDecoder().decode(bytes)))
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return { digest: sha256Hex(bytes), files: [] }
|
|
426
|
+
}
|
|
427
|
+
const files = Array.isArray(doc?.archive?.files) ? doc.archive.files : []
|
|
428
|
+
return { digest: sha256Hex(bytes), files }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
function mainOf(dir: string, options: PkgToolOptions): string {
|
|
433
|
+
return packageSelf(dir, options).main
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
// The lock entry a store tree yields for a key: its version as
|
|
438
|
+
// selected, its canon-hash, its archive digest, and the manifest digest
|
|
439
|
+
// where a manifest is kept beside it.
|
|
440
|
+
function pinTree(
|
|
441
|
+
key: string, dir: string, v: string, pkg: string | undefined,
|
|
442
|
+
options: PkgToolOptions,
|
|
443
|
+
): { entry?: LockEntry, unevaluable?: true, forbidden: string[] } {
|
|
444
|
+
const main = pathJoin(dir, mainOf(dir, options))
|
|
445
|
+
const got = existsSync(main) ?
|
|
446
|
+
options.eval(readFileSync(main, 'utf8'), main) : undefined
|
|
447
|
+
if (null != got && !got.ok) {
|
|
448
|
+
return { unevaluable: true, forbidden: [] }
|
|
449
|
+
}
|
|
450
|
+
const archive = archiveOf(dir)
|
|
451
|
+
const manifest = storedManifest(dir)
|
|
452
|
+
return {
|
|
453
|
+
entry: {
|
|
454
|
+
key,
|
|
455
|
+
v,
|
|
456
|
+
canon: null == got ? '' : got.hash,
|
|
457
|
+
archive: archive.digest,
|
|
458
|
+
...(null == manifest ? {} : { manifest: manifest.digest }),
|
|
459
|
+
...(null == pkg ? {} : { pkg }),
|
|
460
|
+
},
|
|
461
|
+
forbidden: archive.forbidden.map((f) => key + ': ' + f),
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
// `aontu pkg tidy`: resolve the closure by MVS and rewrite the lockfile.
|
|
467
|
+
export function pkgTidy(root: string, options: PkgToolOptions): PkgTidyReport {
|
|
468
|
+
const report = pkgResolve(root, options)
|
|
469
|
+
if ('ok' === report.verdict) {
|
|
470
|
+
writeLock(root, report.lock, options)
|
|
471
|
+
}
|
|
472
|
+
return report
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
// The resolution alone, for a caller that decides whether to write.
|
|
477
|
+
export function pkgResolve(root: string, options: PkgToolOptions): PkgTidyReport {
|
|
478
|
+
const previous = readLock(root)
|
|
479
|
+
const selected: Record<string, Dependency> = {}
|
|
480
|
+
const missing: string[] = []
|
|
481
|
+
|
|
482
|
+
let frontier = declaredDeps(pathJoin(root, PKG_FILE), options)
|
|
483
|
+
for (; 0 < Object.keys(frontier).length;) {
|
|
484
|
+
const next: Record<string, Dependency> = {}
|
|
485
|
+
|
|
486
|
+
for (const key of Object.keys(frontier)) {
|
|
487
|
+
const want = frontier[key]
|
|
488
|
+
const have = selected[key]
|
|
489
|
+
if (null != have && 0 <= versionCompare(have.v, want.v)) {
|
|
490
|
+
continue
|
|
491
|
+
}
|
|
492
|
+
selected[key] = { ...have, ...want }
|
|
493
|
+
|
|
494
|
+
if (!usableKey(key)) {
|
|
495
|
+
// A key this tooling cannot act on names nothing any store
|
|
496
|
+
// can hold: the same answer as an absent package.
|
|
497
|
+
missing.push(key)
|
|
498
|
+
continue
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const dir = storeDir(root, key, previous[key]?.canon ?? '',
|
|
502
|
+
targetOf(key, selected[key], previous[key]), options, selected[key].v)
|
|
503
|
+
if (undefined === dir) {
|
|
504
|
+
missing.push(key)
|
|
505
|
+
continue
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const deps = declaredDeps(pathJoin(dir, PKG_FILE), options)
|
|
509
|
+
for (const dk of Object.keys(deps)) {
|
|
510
|
+
const bid = next[dk]
|
|
511
|
+
if (null == bid || 0 > versionCompare(bid.v, deps[dk].v)) {
|
|
512
|
+
next[dk] = { ...bid, ...deps[dk] }
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
frontier = next
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const lock: LockEntry[] = []
|
|
521
|
+
const unevaluable: string[] = []
|
|
522
|
+
const forbidden: string[] = []
|
|
523
|
+
for (const key of Object.keys(selected).sort(cmpBytes)) {
|
|
524
|
+
if (missing.includes(key)) {
|
|
525
|
+
continue
|
|
526
|
+
}
|
|
527
|
+
const pkg = targetOf(key, selected[key], previous[key])
|
|
528
|
+
const dir = storeDir(root, key, previous[key]?.canon ?? '', pkg,
|
|
529
|
+
options, selected[key].v) as string
|
|
530
|
+
const pinned = pinTree(key, dir, selected[key].v,
|
|
531
|
+
isAlias(key) ? pkg : undefined, options)
|
|
532
|
+
forbidden.push(...pinned.forbidden)
|
|
533
|
+
if (pinned.unevaluable) {
|
|
534
|
+
unevaluable.push(key)
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
lock.push(pinned.entry as LockEntry)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const uniqueMissing = [...new Set(missing)].sort(cmpBytes)
|
|
541
|
+
const uniqueUnevaluable = [...new Set(unevaluable)].sort(cmpBytes)
|
|
542
|
+
const held = 0 === uniqueMissing.length && 0 === uniqueUnevaluable.length &&
|
|
543
|
+
0 === forbidden.length
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
verdict: held ? 'ok' :
|
|
547
|
+
0 < uniqueUnevaluable.length || 0 < forbidden.length ? 'error' : 'missing',
|
|
548
|
+
lock,
|
|
549
|
+
missing: uniqueMissing,
|
|
550
|
+
unevaluable: uniqueUnevaluable,
|
|
551
|
+
forbidden: forbidden.sort(cmpBytes),
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
// The package an alias key names: from the declaration, else from the
|
|
557
|
+
// previous lock; a package path names itself.
|
|
558
|
+
export function targetOf(key: string, dep?: Dependency, prev?: LockEntry): string {
|
|
559
|
+
if (!isAlias(key)) {
|
|
560
|
+
return key
|
|
561
|
+
}
|
|
562
|
+
return dep?.pkg ?? prev?.pkg ?? ''
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
export function pkgVerify(root: string, options: PkgToolOptions):
|
|
567
|
+
PkgVerifyReport {
|
|
568
|
+
const locked = readLock(root)
|
|
569
|
+
const verified: string[] = []
|
|
570
|
+
const mismatched: PkgMismatch[] = []
|
|
571
|
+
const missing: string[] = []
|
|
572
|
+
|
|
573
|
+
const declared = declaredDeps(pathJoin(root, PKG_FILE), options)
|
|
574
|
+
const unlocked = Object.keys(declared)
|
|
575
|
+
.filter((key) => null == locked[key]).sort(cmpBytes)
|
|
576
|
+
|
|
577
|
+
for (const key of Object.keys(locked).sort(cmpBytes)) {
|
|
578
|
+
const entry = locked[key]
|
|
579
|
+
if (!usableKey(key)) {
|
|
580
|
+
missing.push(key)
|
|
581
|
+
continue
|
|
582
|
+
}
|
|
583
|
+
const dir = storeDir(root, key, entry.canon, entry.pkg ?? key, options, entry.v)
|
|
584
|
+
if (undefined === dir) {
|
|
585
|
+
missing.push(key)
|
|
586
|
+
continue
|
|
587
|
+
}
|
|
588
|
+
const main = pathJoin(dir, mainOf(dir, options))
|
|
589
|
+
if (!existsSync(main)) {
|
|
590
|
+
missing.push(key)
|
|
591
|
+
continue
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// BYTES BEFORE MEANING (ADR-019): the archive digest and the
|
|
595
|
+
// manifest's file list first, on fixed-size reads, then the one
|
|
596
|
+
// evaluation.
|
|
597
|
+
const before = mismatched.length
|
|
598
|
+
const archive = archiveOf(dir)
|
|
599
|
+
for (const f of archive.forbidden) {
|
|
600
|
+
mismatched.push({ key, pin: 'archive', want: entry.archive, got: 'forbidden: ' + f })
|
|
601
|
+
}
|
|
602
|
+
if (0 === archive.forbidden.length && entry.archive !== archive.digest) {
|
|
603
|
+
mismatched.push({ key, pin: 'archive', want: entry.archive, got: archive.digest })
|
|
604
|
+
}
|
|
605
|
+
const manifest = storedManifest(dir)
|
|
606
|
+
if (null == manifest) {
|
|
607
|
+
if (null != entry.manifest) {
|
|
608
|
+
mismatched.push({ key, pin: 'manifest', want: entry.manifest, got: '' })
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
if (null != entry.manifest && entry.manifest !== manifest.digest) {
|
|
613
|
+
mismatched.push({ key, pin: 'manifest', want: entry.manifest, got: manifest.digest })
|
|
614
|
+
}
|
|
615
|
+
const listed = new Map(manifest.files.map((f) => [f.path, f.digest]))
|
|
616
|
+
for (const f of archive.files) {
|
|
617
|
+
if (listed.get(f.path) !== f.digest) {
|
|
618
|
+
mismatched.push({ key, pin: 'manifest', want: listed.get(f.path) ?? '',
|
|
619
|
+
got: f.path + ' ' + f.digest })
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (before !== mismatched.length) {
|
|
624
|
+
continue
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const got = options.eval(readFileSync(main, 'utf8'), main)
|
|
628
|
+
const want = entry.canon
|
|
629
|
+
if (got.ok && want === got.hash) {
|
|
630
|
+
verified.push(key)
|
|
631
|
+
continue
|
|
632
|
+
}
|
|
633
|
+
mismatched.push({ key, pin: 'canon', want, got: got.ok ? got.hash : '' })
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
verdict: 0 < mismatched.length ? 'mismatch' :
|
|
638
|
+
0 < unlocked.length ? 'unlocked' :
|
|
639
|
+
0 < missing.length ? 'missing' : 'ok',
|
|
640
|
+
verified,
|
|
641
|
+
mismatched,
|
|
642
|
+
unlocked,
|
|
643
|
+
missing: missing.sort(cmpBytes),
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
// `aontu pkg vendor`: materialise the locked closure into `aontu_meta/vendor/`.
|
|
649
|
+
export function pkgVendor(root: string, options: PkgToolOptions):
|
|
650
|
+
PkgVendorReport {
|
|
651
|
+
const locked = readLock(root)
|
|
652
|
+
const vendored: string[] = []
|
|
653
|
+
const missing: string[] = []
|
|
654
|
+
|
|
655
|
+
const vendorRoot = pathJoin(root, META_DIR, VENDOR_DIR)
|
|
656
|
+
|
|
657
|
+
for (const key of Object.keys(locked).sort(cmpBytes)) {
|
|
658
|
+
const entry = locked[key]
|
|
659
|
+
if (!usableKey(key)) {
|
|
660
|
+
missing.push(key)
|
|
661
|
+
continue
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const from = storeDir(root, key, entry.canon, entry.pkg ?? key, options, entry.v)
|
|
665
|
+
if (undefined === from) {
|
|
666
|
+
missing.push(key)
|
|
667
|
+
continue
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const to = moduleDir(vendorRoot, key)
|
|
671
|
+
if (from !== to) {
|
|
672
|
+
vendorCopy(from, to)
|
|
673
|
+
}
|
|
674
|
+
vendored.push(key)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
verdict: 0 === missing.length ? 'ok' : 'missing',
|
|
679
|
+
vendored,
|
|
680
|
+
missing: missing.sort(cmpBytes),
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
// A store tree into the vendor tree: the whole directory, less the
|
|
686
|
+
// store's own lock, so the copy resolves against the consumer's.
|
|
687
|
+
export function vendorCopy(from: string, to: string): void {
|
|
688
|
+
rmSync(to, { recursive: true, force: true })
|
|
689
|
+
copyTree(from, to)
|
|
690
|
+
const lock = pathJoin(to, META_DIR, LOCK_FILE)
|
|
691
|
+
if (existsSync(lock)) {
|
|
692
|
+
unlinkSync(lock)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
// A whole package directory, copied as real files: the vendor tree is
|
|
698
|
+
// committed, and a link into a shared store is corruption waiting for
|
|
699
|
+
// an edit.
|
|
700
|
+
export function copyTree(from: string, to: string): void {
|
|
701
|
+
mkdirSync(to, { recursive: true })
|
|
702
|
+
for (const name of readdirSync(from).sort(cmpBytes)) {
|
|
703
|
+
const src = pathJoin(from, name)
|
|
704
|
+
const dst = pathJoin(to, name)
|
|
705
|
+
if (statSync(src).isDirectory()) {
|
|
706
|
+
copyTree(src, dst)
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
mkdirSync(pathDirname(dst), { recursive: true })
|
|
710
|
+
copyFileSync(src, dst)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
// Every consumer refuses an archive past params.archive at acquire, so
|
|
717
|
+
// a publisher refuses to mint one: the reasons, as the forbidden list.
|
|
718
|
+
export function archiveOverCaps(archive: Archive): string[] {
|
|
719
|
+
const over: string[] = []
|
|
720
|
+
if (ARCHIVE_LIMITS.bytes < archive.size) {
|
|
721
|
+
over.push('archive: ' + archive.size + ' bytes, over the cap of ' + ARCHIVE_LIMITS.bytes)
|
|
722
|
+
}
|
|
723
|
+
if (ARCHIVE_LIMITS.files < archive.files.length) {
|
|
724
|
+
over.push('archive: ' + archive.files.length + ' files, over the cap of ' + ARCHIVE_LIMITS.files)
|
|
725
|
+
}
|
|
726
|
+
let total = 0
|
|
727
|
+
for (const f of archive.files) {
|
|
728
|
+
total += f.size
|
|
729
|
+
if (ARCHIVE_LIMITS.fileBytes < f.size) {
|
|
730
|
+
over.push(f.path + ': ' + f.size + ' bytes, over the cap of ' + ARCHIVE_LIMITS.fileBytes)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (ARCHIVE_LIMITS.unpacked < total) {
|
|
734
|
+
over.push('archive: unpacks to ' + total + ' bytes, over the cap of ' + ARCHIVE_LIMITS.unpacked)
|
|
735
|
+
}
|
|
736
|
+
return over
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
const RESERVED_NAME_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i
|
|
741
|
+
export const REL_PATH_MAX_ELEMENTS = 32
|
|
742
|
+
|
|
743
|
+
// A path inside an archive: forward slashes, the element rules, never
|
|
744
|
+
// absolute, never escaping, never a name one platform reserves.
|
|
745
|
+
export function relPathError(p: string): string | undefined {
|
|
746
|
+
if ('' === p || 512 < p.length || p.startsWith('/') || p.endsWith('/')) {
|
|
747
|
+
return 'an entry path is empty, absolute or a directory'
|
|
748
|
+
}
|
|
749
|
+
const elements = p.split('/')
|
|
750
|
+
if (REL_PATH_MAX_ELEMENTS < elements.length) {
|
|
751
|
+
return 'an entry path has more than ' + REL_PATH_MAX_ELEMENTS + ' elements'
|
|
752
|
+
}
|
|
753
|
+
for (const e of elements) {
|
|
754
|
+
if ('' === e || '.' === e || '..' === e || e.startsWith('.') || e.endsWith('.')) {
|
|
755
|
+
return 'an entry path element is empty or begins or ends with a dot'
|
|
756
|
+
}
|
|
757
|
+
if (!/^[A-Za-z0-9._-]+$/.test(e)) {
|
|
758
|
+
return 'an entry path element is outside the alphabet'
|
|
759
|
+
}
|
|
760
|
+
if (RESERVED_NAME_RE.test(e)) {
|
|
761
|
+
return 'an entry path element is a name a platform reserves'
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return undefined
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
// `aontu pkg refreeze`: recompute every canon pin and nothing else,
|
|
769
|
+
// which is what a canonical-form change in the engine needs.
|
|
770
|
+
export function pkgRefreeze(root: string, options: PkgToolOptions):
|
|
771
|
+
PkgRefreezeReport {
|
|
772
|
+
const locked = readLock(root)
|
|
773
|
+
const repinned: { key: string, from: string, to: string }[] = []
|
|
774
|
+
const unchanged: string[] = []
|
|
775
|
+
const missing: string[] = []
|
|
776
|
+
const unevaluable: string[] = []
|
|
777
|
+
const lock: LockEntry[] = []
|
|
778
|
+
|
|
779
|
+
for (const key of Object.keys(locked).sort(cmpBytes)) {
|
|
780
|
+
const entry = locked[key]
|
|
781
|
+
const dir = usableKey(key) ?
|
|
782
|
+
storeDir(root, key, entry.canon, entry.pkg ?? key, options, entry.v) : undefined
|
|
783
|
+
const main = undefined === dir ? undefined : pathJoin(dir, mainOf(dir, options))
|
|
784
|
+
if (undefined === main || !existsSync(main)) {
|
|
785
|
+
missing.push(key)
|
|
786
|
+
lock.push(entry)
|
|
787
|
+
continue
|
|
788
|
+
}
|
|
789
|
+
const got = options.eval(readFileSync(main, 'utf8'), main)
|
|
790
|
+
if (!got.ok) {
|
|
791
|
+
unevaluable.push(key)
|
|
792
|
+
lock.push(entry)
|
|
793
|
+
continue
|
|
794
|
+
}
|
|
795
|
+
if (got.hash === entry.canon) {
|
|
796
|
+
unchanged.push(key)
|
|
797
|
+
lock.push(entry)
|
|
798
|
+
continue
|
|
799
|
+
}
|
|
800
|
+
repinned.push({ key, from: entry.canon, to: got.hash })
|
|
801
|
+
lock.push({ ...entry, canon: got.hash })
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const held = 0 === missing.length && 0 === unevaluable.length
|
|
805
|
+
if (held && 0 < repinned.length) {
|
|
806
|
+
writeLock(root, lock, options)
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return {
|
|
810
|
+
verdict: held ? 'ok' : 0 < unevaluable.length ? 'error' : 'missing',
|
|
811
|
+
repinned,
|
|
812
|
+
unchanged,
|
|
813
|
+
missing,
|
|
814
|
+
unevaluable,
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
// `aontu pkg tree`: the locked closure as a graph, each node's edges
|
|
820
|
+
// read from its own package file in the store.
|
|
821
|
+
export function pkgTree(root: string, options: PkgToolOptions): PkgTreeReport {
|
|
822
|
+
const locked = readLock(root)
|
|
823
|
+
const self = packageSelf(root, options)
|
|
824
|
+
const nodes: PkgTreeNode[] = []
|
|
825
|
+
const missing: string[] = []
|
|
826
|
+
|
|
827
|
+
nodes.push({
|
|
828
|
+
key: '' === self.path ? '.' : self.path,
|
|
829
|
+
v: self.version,
|
|
830
|
+
deps: Object.keys(declaredDeps(pathJoin(root, PKG_FILE), options)).sort(cmpBytes),
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
for (const key of Object.keys(locked).sort(cmpBytes)) {
|
|
834
|
+
const entry = locked[key]
|
|
835
|
+
const dir = usableKey(key) ?
|
|
836
|
+
storeDir(root, key, entry.canon, entry.pkg ?? key, options, entry.v) : undefined
|
|
837
|
+
if (undefined === dir) {
|
|
838
|
+
missing.push(key)
|
|
839
|
+
nodes.push({ key, v: entry.v, deps: [] })
|
|
840
|
+
continue
|
|
841
|
+
}
|
|
842
|
+
nodes.push({
|
|
843
|
+
key, v: entry.v,
|
|
844
|
+
deps: Object.keys(declaredDeps(pathJoin(dir, PKG_FILE), options)).sort(cmpBytes),
|
|
845
|
+
})
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return {
|
|
849
|
+
verdict: 0 === missing.length ? 'ok' : 'missing',
|
|
850
|
+
root: nodes[0].key,
|
|
851
|
+
nodes,
|
|
852
|
+
missing,
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
export const MANIFEST_SCHEMA = 'aontu-package/v1'
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
export type ManifestModule = { path: string, main: string, canon: string }
|
|
861
|
+
|
|
862
|
+
// The signed manifest a publish sends (T.Manifest in the specification),
|
|
863
|
+
// less what the act of publishing supplies: the time, and the publisher
|
|
864
|
+
// the token names.
|
|
865
|
+
export type PkgManifest = {
|
|
866
|
+
schema: typeof MANIFEST_SCHEMA
|
|
867
|
+
package: string
|
|
868
|
+
version: string
|
|
869
|
+
publish: 'public' | 'private'
|
|
870
|
+
archive: { format: 'zip', digest: string, size: number, files: ArchiveFile[] }
|
|
871
|
+
modules: ManifestModule[]
|
|
872
|
+
deps: Record<string, Dependency>
|
|
873
|
+
retract?: string[]
|
|
874
|
+
moved?: string
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export type PkgManifestReport = {
|
|
878
|
+
// `ok` the package may be published; `breaking`, `undecided` or
|
|
879
|
+
// `error` the gate refused, and no publish should follow.
|
|
880
|
+
verdict: 'ok' | 'breaking' | 'undecided' | 'error'
|
|
881
|
+
manifest?: PkgManifest
|
|
882
|
+
// What the package does not declare, sorted. A manifest cannot be
|
|
883
|
+
// minted without them.
|
|
884
|
+
missing: string[]
|
|
885
|
+
forbidden: string[]
|
|
886
|
+
// The gate's findings, when a prior version was named.
|
|
887
|
+
findings: any[]
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
// `aontu pkg manifest`: the manifest a publish would send, and the gate
|
|
892
|
+
// that decides whether it may be.
|
|
893
|
+
export function pkgManifest(
|
|
894
|
+
root: string, options: PkgToolOptions, against?: string,
|
|
895
|
+
): PkgManifestReport {
|
|
896
|
+
const self = packageSelf(root, options)
|
|
897
|
+
|
|
898
|
+
const missing: string[] = []
|
|
899
|
+
if ('' === self.path) {
|
|
900
|
+
missing.push('pkg.path')
|
|
901
|
+
}
|
|
902
|
+
else if (isAlias(self.path) || !usableKey(self.path)) {
|
|
903
|
+
missing.push('pkg.path (' + self.path + ' is not a package path)')
|
|
904
|
+
}
|
|
905
|
+
if ('' === self.version) {
|
|
906
|
+
missing.push('pkg.version')
|
|
907
|
+
}
|
|
908
|
+
else if (!VERSION_RE.test(self.version)) {
|
|
909
|
+
missing.push('pkg.version (' + self.version + ' is not MAJOR.MINOR.PATCH)')
|
|
910
|
+
}
|
|
911
|
+
const main = pathJoin(root, self.main)
|
|
912
|
+
if (!existsSync(main) || undefined !== relPathError(self.main)) {
|
|
913
|
+
missing.push(self.main)
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const refused = (why: string[], forbidden: string[] = []): PkgManifestReport => ({
|
|
917
|
+
verdict: 'error', missing: why.sort(cmpBytes), forbidden, findings: [],
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
if (0 < missing.length) {
|
|
921
|
+
return refused(missing)
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const newSrc = readFileSync(main, 'utf8')
|
|
925
|
+
const got = options.eval(newSrc, main)
|
|
926
|
+
|
|
927
|
+
// Nothing to pin: `tidy` refuses the same way.
|
|
928
|
+
if (!got.ok) {
|
|
929
|
+
return refused([self.main])
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const archive = archiveOf(root)
|
|
933
|
+
if (0 < archive.forbidden.length) {
|
|
934
|
+
return refused([], archive.forbidden)
|
|
935
|
+
}
|
|
936
|
+
const over = archiveOverCaps(archive)
|
|
937
|
+
if (0 < over.length) {
|
|
938
|
+
return refused([], over)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const report: PkgManifestReport = {
|
|
942
|
+
verdict: 'ok',
|
|
943
|
+
manifest: {
|
|
944
|
+
schema: MANIFEST_SCHEMA,
|
|
945
|
+
package: self.path,
|
|
946
|
+
version: self.version,
|
|
947
|
+
publish: self.publish,
|
|
948
|
+
archive: {
|
|
949
|
+
format: 'zip', digest: archive.digest, size: archive.size,
|
|
950
|
+
files: archive.files,
|
|
951
|
+
},
|
|
952
|
+
modules: [{ path: self.path, main: self.main, canon: got.hash }],
|
|
953
|
+
deps: declaredDeps(pathJoin(root, PKG_FILE), options),
|
|
954
|
+
...(0 === self.retract.length ? {} : { retract: self.retract }),
|
|
955
|
+
...(null == self.moved ? {} : { moved: self.moved }),
|
|
956
|
+
},
|
|
957
|
+
missing: [],
|
|
958
|
+
forbidden: [],
|
|
959
|
+
findings: [],
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (null == against) {
|
|
963
|
+
return report
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// THE PUBLISH-TIME COMPATIBILITY GATE: G3's subsumption
|
|
967
|
+
// (ts/src/subsume.ts), wired at the one place versions are minted,
|
|
968
|
+
// with no major to bump past it (ADR-022).
|
|
969
|
+
const prior = packageSelf(against, options)
|
|
970
|
+
const priorMain = pathJoin(against, prior.main)
|
|
971
|
+
if (!existsSync(priorMain)) {
|
|
972
|
+
report.verdict = 'error'
|
|
973
|
+
report.missing = [prior.main]
|
|
974
|
+
return report
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const priorSrc = readFileSync(priorMain, 'utf8')
|
|
978
|
+
const urls = {
|
|
979
|
+
generalUrl: main,
|
|
980
|
+
specificUrl: priorMain,
|
|
981
|
+
generalPath: main,
|
|
982
|
+
specificPath: priorMain,
|
|
983
|
+
}
|
|
984
|
+
const gate = subsume(newSrc, priorSrc, urls)
|
|
985
|
+
|
|
986
|
+
report.findings = gate.findings
|
|
987
|
+
report.verdict = MANIFEST_VERDICT[gate.verdict]
|
|
988
|
+
if ('error' === gate.verdict) {
|
|
989
|
+
return report
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Admission alone is not compatibility: what the prior version
|
|
993
|
+
// generated, the next must generate, and the same (ts/src/compat.ts).
|
|
994
|
+
const outcome = compatOutcome(newSrc, priorSrc, urls)
|
|
995
|
+
report.findings = report.findings.concat(outcome.findings)
|
|
996
|
+
if ('breaking' === outcome.verdict) {
|
|
997
|
+
report.verdict = 'breaking'
|
|
998
|
+
}
|
|
999
|
+
return report
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
const MANIFEST_VERDICT: Record<string, PkgManifestReport['verdict']> = {
|
|
1004
|
+
subsumes: 'ok',
|
|
1005
|
+
does_not_subsume: 'breaking',
|
|
1006
|
+
undecided: 'undecided',
|
|
1007
|
+
error: 'error',
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
// The manifest as the bytes a publish signs and a repository serves:
|
|
1012
|
+
// canonical aontu, one line, keys sorted, which for scalar leaves is
|
|
1013
|
+
// JSON.
|
|
1014
|
+
export function manifestText(m: PkgManifest, options: PkgToolOptions,
|
|
1015
|
+
extra?: Record<string, any>): string {
|
|
1016
|
+
const src = JSON.stringify({ ...m, ...(extra ?? {}) })
|
|
1017
|
+
return options.eval(src, 'manifest.aon').canon
|
|
1018
|
+
}
|