@orkestrel/scaffold 0.0.108 → 0.0.109

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 (157) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
  9. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
  10. package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
  11. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
  12. package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
  13. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
  14. package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
  15. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
  16. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
  17. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
  18. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
  19. package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
  20. package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
  21. package/dist/agents/templates/brief.md +1 -1
  22. package/dist/bin/main.js +51 -46
  23. package/dist/bin/main.js.map +1 -1
  24. package/dist/host/AGENTS.md +23 -21
  25. package/dist/host/agents/orchestration.md +43 -18
  26. package/dist/host/agents/platforms/windows.md +3 -3
  27. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  28. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  29. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  30. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  31. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  32. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  33. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  34. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  35. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  36. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
  37. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
  38. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
  48. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  49. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  50. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  51. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  52. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  53. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  54. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  55. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  56. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  58. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  59. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  60. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  63. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  64. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  65. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  66. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  67. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  68. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
  69. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
  70. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
  71. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
  72. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  73. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  74. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  75. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  76. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  77. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  78. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  79. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
  80. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  81. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  82. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  83. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  86. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  87. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  88. package/dist/host/agents/templates/brief.md +1 -1
  89. package/dist/host/agents/transports/claude-cli.md +3 -3
  90. package/dist/host/agents/transports/codex.md +2 -7
  91. package/dist/host/agents/transports/cursor.md +2 -3
  92. package/dist/host/claude/AGENTS.md +2 -2
  93. package/dist/host/claude/agents/analyst.md +3 -3
  94. package/dist/host/claude/agents/astra.md +3 -3
  95. package/dist/host/claude/agents/builder.md +1 -1
  96. package/dist/host/claude/agents/checker.md +1 -1
  97. package/dist/host/claude/agents/distiller.md +1 -1
  98. package/dist/host/claude/agents/grok.md +1 -1
  99. package/dist/host/claude/agents/opus.md +2 -2
  100. package/dist/host/claude/agents/orkestrel.md +57 -90
  101. package/dist/host/claude/agents/researcher.md +1 -1
  102. package/dist/host/claude/agents/reviewer.md +4 -0
  103. package/dist/host/claude/agents/scout.md +1 -1
  104. package/dist/host/claude/agents/verifier.md +1 -1
  105. package/dist/host/claude/rules/application.md +14 -21
  106. package/dist/host/claude/rules/architecture.md +34 -44
  107. package/dist/host/claude/rules/browser.md +3 -4
  108. package/dist/host/claude/rules/campaign.md +2 -2
  109. package/dist/host/claude/rules/documentation.md +25 -12
  110. package/dist/host/claude/rules/names.md +9 -4
  111. package/dist/host/claude/rules/patterns.md +6 -7
  112. package/dist/host/claude/rules/portability.md +1 -1
  113. package/dist/host/claude/rules/quality.md +4 -5
  114. package/dist/host/claude/rules/styles.md +2 -2
  115. package/dist/host/claude/rules/tests.md +57 -114
  116. package/dist/host/claude/rules/typescript.md +7 -9
  117. package/dist/host/claude/rules/workspace.md +21 -19
  118. package/dist/host/claude/rules/writing.md +2 -5
  119. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  120. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  121. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  122. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  123. package/dist/host/codex/agents/analyst.toml +9 -3
  124. package/dist/host/codex/agents/astra.toml +3 -4
  125. package/dist/host/codex/agents/builder.toml +1 -1
  126. package/dist/host/codex/agents/opus.toml +3 -4
  127. package/dist/host/codex/agents/orkestrel.toml +1 -1
  128. package/dist/host/codex/agents/researcher.toml +2 -2
  129. package/dist/host/codex/agents/verifier.toml +1 -1
  130. package/dist/host/configs/helpers.ts +21 -1
  131. package/dist/host/configs/policy.ts +299 -15
  132. package/dist/host/dotfiles/oxlintrc.json +137 -3
  133. package/dist/host/guides/agent.md +111 -134
  134. package/dist/host/guides/capture.md +801 -0
  135. package/dist/host/guides/probe.md +27 -8
  136. package/dist/host/guides/scaffold.md +96 -18
  137. package/dist/host/guides/study.md +534 -440
  138. package/dist/host/guides/test.md +111 -44
  139. package/dist/host/manifest.json +160 -240
  140. package/dist/host/tests/config.test.ts +971 -187
  141. package/dist/host/tests/policy.test.ts +127 -1
  142. package/dist/host/tests/setupPolicy.ts +893 -33
  143. package/dist/src/core/index.cjs +214 -86
  144. package/dist/src/core/index.cjs.map +1 -1
  145. package/dist/src/core/index.d.cts +122 -38
  146. package/dist/src/core/index.d.ts +122 -38
  147. package/dist/src/core/index.js +209 -85
  148. package/dist/src/core/index.js.map +1 -1
  149. package/dist/src/server/index.cjs +20 -27
  150. package/dist/src/server/index.cjs.map +1 -1
  151. package/dist/src/server/index.js +21 -28
  152. package/dist/src/server/index.js.map +1 -1
  153. package/package.json +11 -11
  154. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  155. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  156. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  157. package/dist/host/guides/supervisor.md +0 -4890
@@ -5,62 +5,283 @@
5
5
  // compares against a local .tgz instead of fetching. Sourcemaps are excluded, a text file differs
6
6
  // only when it differs with all whitespace removed, and a binary file differs on any byte. The
7
7
  // summary lists the paths added, removed, and changed. Exit 0 when nothing material differs, 3 when
8
- // something does, 2 when the tarball cannot be read, 64 on usage.
9
- import { existsSync, readFileSync } from 'node:fs'
10
- import { relative } from 'node:path'
8
+ // something does, 2 when the tarball cannot be read, 64 on usage, including a flag the script does
9
+ // not read and the `--flag=value` form.
10
+ import type { SpawnSyncReturns } from 'node:child_process'
11
+ import { spawnSync } from 'node:child_process'
12
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
13
+ import { dirname, join, relative } from 'node:path'
11
14
  import { gunzipSync } from 'node:zlib'
12
- import {
13
- listFiles,
14
- readMissingFlags,
15
- readOption,
16
- parseJsonRecord,
17
- readString,
18
- runNpm,
19
- } from '../../orkestrel-dispatch/scripts/helpers.ts'
20
-
21
- const BLOCK = 512
22
- const PACKAGE_PREFIX = 'package/'
23
-
24
- interface Manifest {
15
+
16
+ /** Holds the byte size of one tar block. */
17
+ export const BLOCK = 512
18
+
19
+ /** Names the directory every npm tarball entry sits under. */
20
+ export const PACKAGE_PREFIX = 'package/'
21
+
22
+ /** Names the directories every tree walk skips by default. */
23
+ export const SKIPPED_DIRECTORIES: ReadonlySet<string> = new Set([
24
+ 'node_modules',
25
+ '.git',
26
+ 'dist',
27
+ 'tmp',
28
+ '.orkestrel',
29
+ '.idea',
30
+ ])
31
+
32
+ /** Holds the empty skip set, because a dist tree holds nothing a walk skips, so every emitted file counts. */
33
+ export const NOTHING_SKIPPED: ReadonlySet<string> = new Set()
34
+
35
+ /** Lists every flag the script reads, value flags and switches alike. */
36
+ export const FLAGS: readonly string[] = ['--package', '--version', '--tarball', '--dist', '--json']
37
+
38
+ /** Holds the usage line the script prints on a usage refusal. */
39
+ export const USAGE =
40
+ 'usage: compare.ts [--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]'
41
+
42
+ /** Describes the name and version a package manifest declares. */
43
+ export interface Manifest {
25
44
  readonly name: string | undefined
26
45
  readonly version: string | undefined
27
46
  }
28
47
 
29
- interface Source {
48
+ /** Describes the tarball a run compares against, or why none could be read. */
49
+ export interface Source {
30
50
  readonly archive: Buffer | undefined
31
51
  readonly version: string | undefined
32
52
  readonly failure: string | undefined
33
53
  }
34
54
 
35
- interface TarState {
55
+ /** Describes the reader's position and the files found so far in one tar archive. */
56
+ export interface TarState {
36
57
  readonly next: number
37
58
  readonly name: string | undefined
38
59
  readonly files: ReadonlyArray<readonly [string, Buffer]>
39
60
  readonly done: boolean
40
61
  }
41
62
 
42
- function readManifest(): Manifest {
43
- if (!existsSync('package.json')) return { name: undefined, version: undefined }
63
+ /** Describes one file a tree walk found, with the modification time the walk read. */
64
+ export interface WalkedFile {
65
+ readonly path: string
66
+ readonly modified: number
67
+ }
68
+
69
+ /** Describes how to spawn npm on this host without a shell where the CLI entry is present. */
70
+ export interface NpmCommand {
71
+ readonly file: string
72
+ readonly prefix: readonly string[]
73
+ readonly shell: boolean
74
+ }
75
+
76
+ /**
77
+ * Normalizes a host path to forward slashes.
78
+ *
79
+ * @param path - A host path.
80
+ * @returns The same path with every backslash replaced by a slash.
81
+ */
82
+ export function normalizeSlashes(path: string): string {
83
+ return path.split('\\').join('/')
84
+ }
85
+
86
+ /**
87
+ * Reads the value that follows a flag.
88
+ *
89
+ * @remarks
90
+ * Reads `--flag value` pairs: the first occurrence of a flag wins, a following value that opens
91
+ * with `--` is a missing value, and the `--flag=value` form is not read.
92
+ *
93
+ * @param argv - The arguments after the script path.
94
+ * @param name - The flag, such as `--package`.
95
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
96
+ * another flag.
97
+ */
98
+ export function readOption(argv: readonly string[], name: string): string | undefined {
99
+ const index = argv.indexOf(name)
100
+ const value = index === -1 ? undefined : argv[index + 1]
101
+ return value === undefined || value.startsWith('--') ? undefined : value
102
+ }
103
+
104
+ /**
105
+ * Names the value flags a caller gave with no value.
106
+ *
107
+ * @param argv - The arguments after the script path.
108
+ * @param names - The flags that take a value.
109
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
110
+ */
111
+ export function readMissingFlags(
112
+ argv: readonly string[],
113
+ names: readonly string[],
114
+ ): readonly string[] {
115
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
116
+ }
117
+
118
+ /**
119
+ * Names the flags a caller gave that the script does not read.
120
+ *
121
+ * @remarks
122
+ * Reads the arguments before a bare `--`, which opens a command a script passes on. A `--flag=value`
123
+ * argument is unknown, because the script reads no such form.
124
+ *
125
+ * @param argv - The arguments after the script path.
126
+ * @param flags - Every flag the script reads, value flags and switches alike.
127
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
128
+ */
129
+ export function readUnknownFlags(
130
+ argv: readonly string[],
131
+ flags: readonly string[],
132
+ ): readonly string[] {
133
+ const end = argv.indexOf('--')
134
+ return (end === -1 ? argv : argv.slice(0, end)).filter(
135
+ (argument) => argument.startsWith('--') && !flags.includes(argument),
136
+ )
137
+ }
138
+
139
+ /**
140
+ * Lists every regular file beneath a directory, skipping the directories named.
141
+ *
142
+ * @remarks
143
+ * A skipped name matches a directory at any depth, exactly and case-sensitively.
144
+ *
145
+ * @param directory - The directory to walk.
146
+ * @param skipped - The directory names to skip. Default: {@link SKIPPED_DIRECTORIES}.
147
+ * @returns Each file's forward-slash path and modification time, in walk order.
148
+ * @throws Thrown when `directory` does not exist or cannot be read; check `existsSync` first.
149
+ */
150
+ export function listFiles(
151
+ directory: string,
152
+ skipped: ReadonlySet<string> = SKIPPED_DIRECTORIES,
153
+ ): readonly WalkedFile[] {
154
+ const files: WalkedFile[] = []
155
+ const pending = [directory]
156
+ while (pending.length > 0) {
157
+ const current = pending.pop()
158
+ if (current === undefined) break
159
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
160
+ if (skipped.has(entry.name)) continue
161
+ const path = join(current, entry.name)
162
+ if (entry.isDirectory()) pending.push(path)
163
+ else if (entry.isFile()) {
164
+ files.push({ path: normalizeSlashes(path), modified: statSync(path).mtimeMs })
165
+ }
166
+ }
167
+ }
168
+ return files
169
+ }
170
+
171
+ /**
172
+ * Parses a whole text as one JSON object.
173
+ *
174
+ * @param text - A JSON document, formatted or not, such as a manifest.
175
+ * @returns The parsed object, or undefined when the text is not a JSON object.
176
+ */
177
+ export function parseJsonRecord(text: string): Record<string, unknown> | undefined {
178
+ try {
179
+ const parsed: unknown = JSON.parse(text)
180
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
181
+ return Object.fromEntries(Object.entries(parsed))
182
+ }
183
+ } catch {}
184
+ return undefined
185
+ }
186
+
187
+ /**
188
+ * Reads a string member of a record.
189
+ *
190
+ * @param record - The record, or undefined.
191
+ * @param key - The member name.
192
+ * @returns The string, or undefined when the member is absent or not a string.
193
+ */
194
+ export function readString(
195
+ record: Record<string, unknown> | undefined,
196
+ key: string,
197
+ ): string | undefined {
198
+ const value = record?.[key]
199
+ return typeof value === 'string' ? value : undefined
200
+ }
201
+
202
+ /**
203
+ * Resolves the npm command for this host.
204
+ *
205
+ * @remarks
206
+ * The `npm-cli.js` entry beside the running Node binary spawns without a shell on every host; the
207
+ * `npm` shim on Windows is a `.cmd` file that needs one, so that path is the fallback alone.
208
+ *
209
+ * @returns The file to spawn, the arguments that precede npm's own, and whether a shell is needed.
210
+ */
211
+ export function resolveNpm(): NpmCommand {
212
+ const cli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js')
213
+ if (existsSync(cli)) return { file: process.execPath, prefix: [cli], shell: false }
214
+ return { file: 'npm', prefix: [], shell: process.platform === 'win32' }
215
+ }
216
+
217
+ /**
218
+ * Runs npm with the given arguments and returns the finished child.
219
+ *
220
+ * @param args - The npm arguments, such as `['view', '@orkestrel/scaffold', 'version']`.
221
+ * @param cwd - The working directory. Default: the current directory.
222
+ * @returns The spawn result with `utf8` stdout and stderr.
223
+ */
224
+ export function runNpm(args: readonly string[], cwd?: string): SpawnSyncReturns<string> {
225
+ const npm = resolveNpm()
226
+ return spawnSync(npm.file, [...npm.prefix, ...args], {
227
+ encoding: 'utf8',
228
+ windowsHide: true,
229
+ shell: npm.shell,
230
+ maxBuffer: 64 * 1024 * 1024,
231
+ ...(cwd === undefined ? {} : { cwd }),
232
+ })
233
+ }
234
+
235
+ /**
236
+ * Reads the name and version a directory's package manifest declares.
237
+ *
238
+ * @param directory - The directory that holds `package.json`.
239
+ * @returns The name and version, each undefined when the manifest is absent, unreadable, or lacks it.
240
+ */
241
+ export function readManifest(directory: string): Manifest {
242
+ const path = join(directory, 'package.json')
243
+ if (!existsSync(path)) return { name: undefined, version: undefined }
44
244
  try {
45
- const record = parseJsonRecord(readFileSync('package.json', 'utf8'))
245
+ const record = parseJsonRecord(readFileSync(path, 'utf8'))
46
246
  return { name: readString(record, 'name'), version: readString(record, 'version') }
47
247
  } catch {
48
248
  return { name: undefined, version: undefined }
49
249
  }
50
250
  }
51
251
 
52
- function readField(text: string): string | undefined {
252
+ /**
253
+ * Reads one value npm printed as JSON.
254
+ *
255
+ * @param text - npm's output with the surrounding whitespace removed.
256
+ * @returns The text without its enclosing quotes, or undefined when nothing remains.
257
+ */
258
+ export function readField(text: string): string | undefined {
53
259
  const stripped = text.replace(/^"|"$/gu, '')
54
260
  return stripped === '' ? undefined : stripped
55
261
  }
56
262
 
57
- function readHeaderText(header: Buffer, start: number, length: number): string {
263
+ /**
264
+ * Reads a NUL-terminated text field out of a tar header block.
265
+ *
266
+ * @param header - The header block.
267
+ * @param start - The field's first byte.
268
+ * @param length - The field's byte length.
269
+ * @returns The field's text up to its first NUL.
270
+ */
271
+ export function readHeaderText(header: Buffer, start: number, length: number): string {
58
272
  const slice = header.subarray(start, start + length)
59
273
  const end = slice.indexOf(0)
60
274
  return slice.subarray(0, end === -1 ? slice.length : end).toString('utf8')
61
275
  }
62
276
 
63
- function readPaxPath(data: Buffer, offset = 0): string | undefined {
277
+ /**
278
+ * Reads the `path` record out of a pax extended header.
279
+ *
280
+ * @param data - The header's data blocks, each record written as `LENGTH KEY=VALUE` and a line feed.
281
+ * @param offset - The byte where the search starts. Default: 0.
282
+ * @returns The path the first `path=` record names, or undefined when no record does or one is malformed.
283
+ */
284
+ export function readPaxPath(data: Buffer, offset = 0): string | undefined {
64
285
  if (offset >= data.length) return undefined
65
286
  const space = data.indexOf(0x20, offset)
66
287
  if (space === -1) return undefined
@@ -72,9 +293,20 @@ function readPaxPath(data: Buffer, offset = 0): string | undefined {
72
293
  : readPaxPath(data, offset + length)
73
294
  }
74
295
 
75
- // A tar entry occupies a header block and the data blocks its size names, so a step ignores every
76
- // block offset an earlier entry already consumed.
77
- function stepTar(archive: Buffer, state: TarState, offset: number): TarState {
296
+ /**
297
+ * Advances a tar reader by the block at one offset.
298
+ *
299
+ * @remarks
300
+ * A tar entry occupies a header block and the data blocks its size names, so a step ignores every
301
+ * block offset an earlier entry already consumed.
302
+ *
303
+ * @param archive - The uncompressed archive.
304
+ * @param state - The reader's state before this block.
305
+ * @param offset - The block's first byte.
306
+ * @returns The state after the block: a regular file added, a pax or GNU long name remembered for the
307
+ * next entry, or the reader done at the first all-zero block.
308
+ */
309
+ export function stepTar(archive: Buffer, state: TarState, offset: number): TarState {
78
310
  if (state.done || offset < state.next) return state
79
311
  const header = archive.subarray(offset, offset + BLOCK)
80
312
  if (header.every((byte) => byte === 0)) return { ...state, done: true }
@@ -100,12 +332,18 @@ function stepTar(archive: Buffer, state: TarState, offset: number): TarState {
100
332
  }
101
333
  }
102
334
 
103
- function readTar(archive: Buffer): ReadonlyMap<string, Buffer> {
335
+ /**
336
+ * Reads the regular files out of an uncompressed tar archive.
337
+ *
338
+ * @param archive - The uncompressed archive.
339
+ * @returns Each file's path and bytes; a later entry of the same name replaces an earlier one at the
340
+ * earlier position, as a map write does.
341
+ */
342
+ export function readTar(archive: Buffer): ReadonlyMap<string, Buffer> {
104
343
  const offsets = Array.from(
105
344
  { length: Math.floor(archive.length / BLOCK) },
106
345
  (_, index) => index * BLOCK,
107
346
  )
108
- // A later entry of the same name replaces an earlier one at the earlier position, as a map write does.
109
347
  return new Map(
110
348
  offsets.reduce<TarState>((state, offset) => stepTar(archive, state, offset), {
111
349
  next: 0,
@@ -116,7 +354,13 @@ function readTar(archive: Buffer): ReadonlyMap<string, Buffer> {
116
354
  )
117
355
  }
118
356
 
119
- function unpackTar(archive: Buffer): ReadonlyMap<string, Buffer> | Error {
357
+ /**
358
+ * Unpacks a gzip-compressed tar archive.
359
+ *
360
+ * @param archive - The compressed archive.
361
+ * @returns The regular files it holds, or the error that stopped the unpacking.
362
+ */
363
+ export function unpackTar(archive: Buffer): ReadonlyMap<string, Buffer> | Error {
120
364
  try {
121
365
  return readTar(gunzipSync(archive))
122
366
  } catch (error) {
@@ -124,7 +368,42 @@ function unpackTar(archive: Buffer): ReadonlyMap<string, Buffer> | Error {
124
368
  }
125
369
  }
126
370
 
127
- async function readSource(
371
+ /**
372
+ * Reads one value from npm's view of the registry.
373
+ *
374
+ * @param args - The npm arguments, such as `['view', 'name', 'version', '--json']`.
375
+ * @returns The value npm printed, or undefined when npm exited nonzero or printed nothing.
376
+ */
377
+ export function readNpmValue(args: readonly string[]): string | undefined {
378
+ const result = runNpm(args)
379
+ return result.status === 0 ? readField(result.stdout.trim()) : undefined
380
+ }
381
+
382
+ /**
383
+ * Fetches a tarball over HTTP.
384
+ *
385
+ * @param url - The tarball's address.
386
+ * @returns The response body, or undefined when the request failed or answered a non-success status.
387
+ */
388
+ export async function fetchTarball(url: string): Promise<Buffer | undefined> {
389
+ try {
390
+ const response = await fetch(url)
391
+ if (!response.ok) return undefined
392
+ return Buffer.from(await response.arrayBuffer())
393
+ } catch {
394
+ return undefined
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Reads the tarball a run compares against.
400
+ *
401
+ * @param name - The package name to look up when no tarball file is given.
402
+ * @param requested - The version to compare, or undefined for the one the registry serves.
403
+ * @param tarball - A local `.tgz` path, or undefined to fetch from the registry.
404
+ * @returns The archive with its version, or the reason none could be read.
405
+ */
406
+ export async function readSource(
128
407
  name: string,
129
408
  requested: string | undefined,
130
409
  tarball: string | undefined,
@@ -152,30 +431,50 @@ async function readSource(
152
431
  : { archive: fetched, version, failure: undefined }
153
432
  }
154
433
 
155
- // A dist tree holds nothing a walk skips, so every emitted file counts.
156
- const NOTHING_SKIPPED: ReadonlySet<string> = new Set()
157
-
158
- function readNpmValue(args: readonly string[]): string | undefined {
159
- const result = runNpm(args)
160
- return result.status === 0 ? readField(result.stdout.trim()) : undefined
161
- }
162
-
163
- function isMaterial(path: string): boolean {
434
+ /**
435
+ * Checks whether a published path counts in the comparison.
436
+ *
437
+ * @param path - A path inside the tarball or the dist tree.
438
+ * @returns True if the path is not a sourcemap; false otherwise.
439
+ */
440
+ export function isMaterial(path: string): boolean {
164
441
  return !path.endsWith('.map')
165
442
  }
166
443
 
167
- function differs(left: Buffer, right: Buffer): boolean {
444
+ /**
445
+ * Checks whether two versions of one file differ materially.
446
+ *
447
+ * @param left - One version's bytes.
448
+ * @param right - The other version's bytes.
449
+ * @returns True if a file holding a NUL byte differs on any byte, or a text file differs with all
450
+ * whitespace removed; false otherwise.
451
+ */
452
+ export function differs(left: Buffer, right: Buffer): boolean {
168
453
  if (left.includes(0) || right.includes(0)) return !left.equals(right)
169
454
  return left.toString('utf8').replace(/\s+/gu, '') !== right.toString('utf8').replace(/\s+/gu, '')
170
455
  }
171
456
 
172
- async function main(argv: readonly string[]): Promise<number> {
457
+ /**
458
+ * Runs the comparison and prints its summary.
459
+ *
460
+ * @param argv - The arguments after the script path.
461
+ * @returns The exit code: 0 when nothing material differs, 3 when something does, 2 when the tarball
462
+ * cannot be read, 64 on usage.
463
+ */
464
+ export async function main(argv: readonly string[]): Promise<number> {
465
+ const unknown = readUnknownFlags(argv, FLAGS)
466
+ if (unknown.length > 0) {
467
+ console.error(
468
+ `compare: unknown flag ${unknown.join(', ')}; give each flag and its value as separate arguments\n${USAGE}`,
469
+ )
470
+ return 64
471
+ }
173
472
  const missing = readMissingFlags(argv, ['--package', '--version', '--tarball', '--dist'])
174
473
  if (missing.length > 0) {
175
474
  console.error(`compare: ${missing.join(', ')} given with no value`)
176
475
  return 64
177
476
  }
178
- const manifest = readManifest()
477
+ const manifest = readManifest('.')
179
478
  const name = readOption(argv, '--package') ?? manifest.name
180
479
  const dist = readOption(argv, '--dist') ?? 'dist'
181
480
  const tarball = readOption(argv, '--tarball')
@@ -184,9 +483,7 @@ async function main(argv: readonly string[]): Promise<number> {
184
483
  !existsSync(dist) ||
185
484
  (argv.includes('--tarball') && tarball === undefined)
186
485
  ) {
187
- console.error(
188
- 'usage: compare.ts [--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]',
189
- )
486
+ console.error(USAGE)
190
487
  return 64
191
488
  }
192
489
  const source = await readSource(name, readOption(argv, '--version'), tarball)
@@ -210,7 +507,7 @@ async function main(argv: readonly string[]): Promise<number> {
210
507
  )
211
508
  const local = new Map(
212
509
  listFiles(dist, NOTHING_SKIPPED).flatMap(({ path }) => {
213
- const key = relative(dist, path).split('\\').join('/')
510
+ const key = normalizeSlashes(relative(dist, path))
214
511
  return isMaterial(key) ? [[key, readFileSync(path)] as const] : []
215
512
  }),
216
513
  )
@@ -247,16 +544,8 @@ async function main(argv: readonly string[]): Promise<number> {
247
544
  return material ? 3 : 0
248
545
  }
249
546
 
250
- async function fetchTarball(url: string): Promise<Buffer | undefined> {
251
- try {
252
- const response = await fetch(url)
253
- if (!response.ok) return undefined
254
- return Buffer.from(await response.arrayBuffer())
255
- } catch {
256
- return undefined
257
- }
547
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
548
+ main(process.argv.slice(2)).then((code) => {
549
+ process.exitCode = code
550
+ })
258
551
  }
259
-
260
- main(process.argv.slice(2)).then((code) => {
261
- process.exitCode = code
262
- })