@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
@@ -1,38 +1,216 @@
1
- // Compare the rebuilt dist/ against the published tarball on material content. Run from the
2
- // publishing package's root after its build:
3
- // node .agents/skills/orkestrel-publish/scripts/compare.ts [--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]
4
- // The package defaults to the manifest's name and the version to what the registry serves; --tarball
5
- // compares against a local .tgz instead of fetching. Sourcemaps are excluded, a text file differs
6
- // only when it differs with all whitespace removed, and a binary file differs on any byte. The
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';
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
+ import { dirname, join, relative } from 'node:path';
11
4
  import { gunzipSync } from 'node:zlib';
12
- import { listFiles, readMissingFlags, readOption, parseJsonRecord, readString, runNpm, } from "../../orkestrel-dispatch/scripts/helpers.js";
13
- const BLOCK = 512;
14
- const PACKAGE_PREFIX = 'package/';
15
- function readManifest() {
16
- if (!existsSync('package.json'))
5
+ /** Holds the byte size of one tar block. */
6
+ export const BLOCK = 512;
7
+ /** Names the directory every npm tarball entry sits under. */
8
+ export const PACKAGE_PREFIX = 'package/';
9
+ /** Names the directories every tree walk skips by default. */
10
+ export const SKIPPED_DIRECTORIES = new Set([
11
+ 'node_modules',
12
+ '.git',
13
+ 'dist',
14
+ 'tmp',
15
+ '.orkestrel',
16
+ '.idea',
17
+ ]);
18
+ /** Holds the empty skip set, because a dist tree holds nothing a walk skips, so every emitted file counts. */
19
+ export const NOTHING_SKIPPED = new Set();
20
+ /** Lists every flag the script reads, value flags and switches alike. */
21
+ export const FLAGS = ['--package', '--version', '--tarball', '--dist', '--json'];
22
+ /** Holds the usage line the script prints on a usage refusal. */
23
+ export const USAGE = 'usage: compare.ts [--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]';
24
+ /**
25
+ * Normalizes a host path to forward slashes.
26
+ *
27
+ * @param path - A host path.
28
+ * @returns The same path with every backslash replaced by a slash.
29
+ */
30
+ export function normalizeSlashes(path) {
31
+ return path.split('\\').join('/');
32
+ }
33
+ /**
34
+ * Reads the value that follows a flag.
35
+ *
36
+ * @remarks
37
+ * Reads `--flag value` pairs: the first occurrence of a flag wins, a following value that opens
38
+ * with `--` is a missing value, and the `--flag=value` form is not read.
39
+ *
40
+ * @param argv - The arguments after the script path.
41
+ * @param name - The flag, such as `--package`.
42
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
43
+ * another flag.
44
+ */
45
+ export function readOption(argv, name) {
46
+ const index = argv.indexOf(name);
47
+ const value = index === -1 ? undefined : argv[index + 1];
48
+ return value === undefined || value.startsWith('--') ? undefined : value;
49
+ }
50
+ /**
51
+ * Names the value flags a caller gave with no value.
52
+ *
53
+ * @param argv - The arguments after the script path.
54
+ * @param names - The flags that take a value.
55
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
56
+ */
57
+ export function readMissingFlags(argv, names) {
58
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
59
+ }
60
+ /**
61
+ * Names the flags a caller gave that the script does not read.
62
+ *
63
+ * @remarks
64
+ * Reads the arguments before a bare `--`, which opens a command a script passes on. A `--flag=value`
65
+ * argument is unknown, because the script reads no such form.
66
+ *
67
+ * @param argv - The arguments after the script path.
68
+ * @param flags - Every flag the script reads, value flags and switches alike.
69
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
70
+ */
71
+ export function readUnknownFlags(argv, flags) {
72
+ const end = argv.indexOf('--');
73
+ return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
74
+ }
75
+ /**
76
+ * Lists every regular file beneath a directory, skipping the directories named.
77
+ *
78
+ * @remarks
79
+ * A skipped name matches a directory at any depth, exactly and case-sensitively.
80
+ *
81
+ * @param directory - The directory to walk.
82
+ * @param skipped - The directory names to skip. Default: {@link SKIPPED_DIRECTORIES}.
83
+ * @returns Each file's forward-slash path and modification time, in walk order.
84
+ * @throws Thrown when `directory` does not exist or cannot be read; check `existsSync` first.
85
+ */
86
+ export function listFiles(directory, skipped = SKIPPED_DIRECTORIES) {
87
+ const files = [];
88
+ const pending = [directory];
89
+ while (pending.length > 0) {
90
+ const current = pending.pop();
91
+ if (current === undefined)
92
+ break;
93
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
94
+ if (skipped.has(entry.name))
95
+ continue;
96
+ const path = join(current, entry.name);
97
+ if (entry.isDirectory())
98
+ pending.push(path);
99
+ else if (entry.isFile()) {
100
+ files.push({ path: normalizeSlashes(path), modified: statSync(path).mtimeMs });
101
+ }
102
+ }
103
+ }
104
+ return files;
105
+ }
106
+ /**
107
+ * Parses a whole text as one JSON object.
108
+ *
109
+ * @param text - A JSON document, formatted or not, such as a manifest.
110
+ * @returns The parsed object, or undefined when the text is not a JSON object.
111
+ */
112
+ export function parseJsonRecord(text) {
113
+ try {
114
+ const parsed = JSON.parse(text);
115
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
116
+ return Object.fromEntries(Object.entries(parsed));
117
+ }
118
+ }
119
+ catch { }
120
+ return undefined;
121
+ }
122
+ /**
123
+ * Reads a string member of a record.
124
+ *
125
+ * @param record - The record, or undefined.
126
+ * @param key - The member name.
127
+ * @returns The string, or undefined when the member is absent or not a string.
128
+ */
129
+ export function readString(record, key) {
130
+ const value = record?.[key];
131
+ return typeof value === 'string' ? value : undefined;
132
+ }
133
+ /**
134
+ * Resolves the npm command for this host.
135
+ *
136
+ * @remarks
137
+ * The `npm-cli.js` entry beside the running Node binary spawns without a shell on every host; the
138
+ * `npm` shim on Windows is a `.cmd` file that needs one, so that path is the fallback alone.
139
+ *
140
+ * @returns The file to spawn, the arguments that precede npm's own, and whether a shell is needed.
141
+ */
142
+ export function resolveNpm() {
143
+ const cli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
144
+ if (existsSync(cli))
145
+ return { file: process.execPath, prefix: [cli], shell: false };
146
+ return { file: 'npm', prefix: [], shell: process.platform === 'win32' };
147
+ }
148
+ /**
149
+ * Runs npm with the given arguments and returns the finished child.
150
+ *
151
+ * @param args - The npm arguments, such as `['view', '@orkestrel/scaffold', 'version']`.
152
+ * @param cwd - The working directory. Default: the current directory.
153
+ * @returns The spawn result with `utf8` stdout and stderr.
154
+ */
155
+ export function runNpm(args, cwd) {
156
+ const npm = resolveNpm();
157
+ return spawnSync(npm.file, [...npm.prefix, ...args], {
158
+ encoding: 'utf8',
159
+ windowsHide: true,
160
+ shell: npm.shell,
161
+ maxBuffer: 64 * 1024 * 1024,
162
+ ...(cwd === undefined ? {} : { cwd }),
163
+ });
164
+ }
165
+ /**
166
+ * Reads the name and version a directory's package manifest declares.
167
+ *
168
+ * @param directory - The directory that holds `package.json`.
169
+ * @returns The name and version, each undefined when the manifest is absent, unreadable, or lacks it.
170
+ */
171
+ export function readManifest(directory) {
172
+ const path = join(directory, 'package.json');
173
+ if (!existsSync(path))
17
174
  return { name: undefined, version: undefined };
18
175
  try {
19
- const record = parseJsonRecord(readFileSync('package.json', 'utf8'));
176
+ const record = parseJsonRecord(readFileSync(path, 'utf8'));
20
177
  return { name: readString(record, 'name'), version: readString(record, 'version') };
21
178
  }
22
179
  catch {
23
180
  return { name: undefined, version: undefined };
24
181
  }
25
182
  }
26
- function readField(text) {
183
+ /**
184
+ * Reads one value npm printed as JSON.
185
+ *
186
+ * @param text - npm's output with the surrounding whitespace removed.
187
+ * @returns The text without its enclosing quotes, or undefined when nothing remains.
188
+ */
189
+ export function readField(text) {
27
190
  const stripped = text.replace(/^"|"$/gu, '');
28
191
  return stripped === '' ? undefined : stripped;
29
192
  }
30
- function readHeaderText(header, start, length) {
193
+ /**
194
+ * Reads a NUL-terminated text field out of a tar header block.
195
+ *
196
+ * @param header - The header block.
197
+ * @param start - The field's first byte.
198
+ * @param length - The field's byte length.
199
+ * @returns The field's text up to its first NUL.
200
+ */
201
+ export function readHeaderText(header, start, length) {
31
202
  const slice = header.subarray(start, start + length);
32
203
  const end = slice.indexOf(0);
33
204
  return slice.subarray(0, end === -1 ? slice.length : end).toString('utf8');
34
205
  }
35
- function readPaxPath(data, offset = 0) {
206
+ /**
207
+ * Reads the `path` record out of a pax extended header.
208
+ *
209
+ * @param data - The header's data blocks, each record written as `LENGTH KEY=VALUE` and a line feed.
210
+ * @param offset - The byte where the search starts. Default: 0.
211
+ * @returns The path the first `path=` record names, or undefined when no record does or one is malformed.
212
+ */
213
+ export function readPaxPath(data, offset = 0) {
36
214
  if (offset >= data.length)
37
215
  return undefined;
38
216
  const space = data.indexOf(0x20, offset);
@@ -46,9 +224,20 @@ function readPaxPath(data, offset = 0) {
46
224
  ? record.slice('path='.length)
47
225
  : readPaxPath(data, offset + length);
48
226
  }
49
- // A tar entry occupies a header block and the data blocks its size names, so a step ignores every
50
- // block offset an earlier entry already consumed.
51
- function stepTar(archive, state, offset) {
227
+ /**
228
+ * Advances a tar reader by the block at one offset.
229
+ *
230
+ * @remarks
231
+ * A tar entry occupies a header block and the data blocks its size names, so a step ignores every
232
+ * block offset an earlier entry already consumed.
233
+ *
234
+ * @param archive - The uncompressed archive.
235
+ * @param state - The reader's state before this block.
236
+ * @param offset - The block's first byte.
237
+ * @returns The state after the block: a regular file added, a pax or GNU long name remembered for the
238
+ * next entry, or the reader done at the first all-zero block.
239
+ */
240
+ export function stepTar(archive, state, offset) {
52
241
  if (state.done || offset < state.next)
53
242
  return state;
54
243
  const header = archive.subarray(offset, offset + BLOCK);
@@ -73,9 +262,15 @@ function stepTar(archive, state, offset) {
73
262
  done: !Number.isFinite(next),
74
263
  };
75
264
  }
76
- function readTar(archive) {
265
+ /**
266
+ * Reads the regular files out of an uncompressed tar archive.
267
+ *
268
+ * @param archive - The uncompressed archive.
269
+ * @returns Each file's path and bytes; a later entry of the same name replaces an earlier one at the
270
+ * earlier position, as a map write does.
271
+ */
272
+ export function readTar(archive) {
77
273
  const offsets = Array.from({ length: Math.floor(archive.length / BLOCK) }, (_, index) => index * BLOCK);
78
- // A later entry of the same name replaces an earlier one at the earlier position, as a map write does.
79
274
  return new Map(offsets.reduce((state, offset) => stepTar(archive, state, offset), {
80
275
  next: 0,
81
276
  name: undefined,
@@ -83,7 +278,13 @@ function readTar(archive) {
83
278
  done: false,
84
279
  }).files);
85
280
  }
86
- function unpackTar(archive) {
281
+ /**
282
+ * Unpacks a gzip-compressed tar archive.
283
+ *
284
+ * @param archive - The compressed archive.
285
+ * @returns The regular files it holds, or the error that stopped the unpacking.
286
+ */
287
+ export function unpackTar(archive) {
87
288
  try {
88
289
  return readTar(gunzipSync(archive));
89
290
  }
@@ -91,7 +292,42 @@ function unpackTar(archive) {
91
292
  return error instanceof Error ? error : new Error(String(error));
92
293
  }
93
294
  }
94
- async function readSource(name, requested, tarball) {
295
+ /**
296
+ * Reads one value from npm's view of the registry.
297
+ *
298
+ * @param args - The npm arguments, such as `['view', 'name', 'version', '--json']`.
299
+ * @returns The value npm printed, or undefined when npm exited nonzero or printed nothing.
300
+ */
301
+ export function readNpmValue(args) {
302
+ const result = runNpm(args);
303
+ return result.status === 0 ? readField(result.stdout.trim()) : undefined;
304
+ }
305
+ /**
306
+ * Fetches a tarball over HTTP.
307
+ *
308
+ * @param url - The tarball's address.
309
+ * @returns The response body, or undefined when the request failed or answered a non-success status.
310
+ */
311
+ export async function fetchTarball(url) {
312
+ try {
313
+ const response = await fetch(url);
314
+ if (!response.ok)
315
+ return undefined;
316
+ return Buffer.from(await response.arrayBuffer());
317
+ }
318
+ catch {
319
+ return undefined;
320
+ }
321
+ }
322
+ /**
323
+ * Reads the tarball a run compares against.
324
+ *
325
+ * @param name - The package name to look up when no tarball file is given.
326
+ * @param requested - The version to compare, or undefined for the one the registry serves.
327
+ * @param tarball - A local `.tgz` path, or undefined to fetch from the registry.
328
+ * @returns The archive with its version, or the reason none could be read.
329
+ */
330
+ export async function readSource(name, requested, tarball) {
95
331
  if (tarball !== undefined) {
96
332
  return existsSync(tarball)
97
333
  ? { archive: readFileSync(tarball), version: requested, failure: undefined }
@@ -113,34 +349,54 @@ async function readSource(name, requested, tarball) {
113
349
  ? { archive: undefined, version, failure: `fetching ${url} failed` }
114
350
  : { archive: fetched, version, failure: undefined };
115
351
  }
116
- // A dist tree holds nothing a walk skips, so every emitted file counts.
117
- const NOTHING_SKIPPED = new Set();
118
- function readNpmValue(args) {
119
- const result = runNpm(args);
120
- return result.status === 0 ? readField(result.stdout.trim()) : undefined;
121
- }
122
- function isMaterial(path) {
352
+ /**
353
+ * Checks whether a published path counts in the comparison.
354
+ *
355
+ * @param path - A path inside the tarball or the dist tree.
356
+ * @returns True if the path is not a sourcemap; false otherwise.
357
+ */
358
+ export function isMaterial(path) {
123
359
  return !path.endsWith('.map');
124
360
  }
125
- function differs(left, right) {
361
+ /**
362
+ * Checks whether two versions of one file differ materially.
363
+ *
364
+ * @param left - One version's bytes.
365
+ * @param right - The other version's bytes.
366
+ * @returns True if a file holding a NUL byte differs on any byte, or a text file differs with all
367
+ * whitespace removed; false otherwise.
368
+ */
369
+ export function differs(left, right) {
126
370
  if (left.includes(0) || right.includes(0))
127
371
  return !left.equals(right);
128
372
  return left.toString('utf8').replace(/\s+/gu, '') !== right.toString('utf8').replace(/\s+/gu, '');
129
373
  }
130
- async function main(argv) {
374
+ /**
375
+ * Runs the comparison and prints its summary.
376
+ *
377
+ * @param argv - The arguments after the script path.
378
+ * @returns The exit code: 0 when nothing material differs, 3 when something does, 2 when the tarball
379
+ * cannot be read, 64 on usage.
380
+ */
381
+ export async function main(argv) {
382
+ const unknown = readUnknownFlags(argv, FLAGS);
383
+ if (unknown.length > 0) {
384
+ console.error(`compare: unknown flag ${unknown.join(', ')}; give each flag and its value as separate arguments\n${USAGE}`);
385
+ return 64;
386
+ }
131
387
  const missing = readMissingFlags(argv, ['--package', '--version', '--tarball', '--dist']);
132
388
  if (missing.length > 0) {
133
389
  console.error(`compare: ${missing.join(', ')} given with no value`);
134
390
  return 64;
135
391
  }
136
- const manifest = readManifest();
392
+ const manifest = readManifest('.');
137
393
  const name = readOption(argv, '--package') ?? manifest.name;
138
394
  const dist = readOption(argv, '--dist') ?? 'dist';
139
395
  const tarball = readOption(argv, '--tarball');
140
396
  if (name === undefined ||
141
397
  !existsSync(dist) ||
142
398
  (argv.includes('--tarball') && tarball === undefined)) {
143
- console.error('usage: compare.ts [--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]');
399
+ console.error(USAGE);
144
400
  return 64;
145
401
  }
146
402
  const source = await readSource(name, readOption(argv, '--version'), tarball);
@@ -159,7 +415,7 @@ async function main(argv) {
159
415
  ? [[path.slice(prefixed.length), content]]
160
416
  : []));
161
417
  const local = new Map(listFiles(dist, NOTHING_SKIPPED).flatMap(({ path }) => {
162
- const key = relative(dist, path).split('\\').join('/');
418
+ const key = normalizeSlashes(relative(dist, path));
163
419
  return isMaterial(key) ? [[key, readFileSync(path)]] : [];
164
420
  }));
165
421
  const added = [...local.keys()].filter((key) => !published.has(key)).sort();
@@ -196,17 +452,8 @@ async function main(argv) {
196
452
  }
197
453
  return material ? 3 : 0;
198
454
  }
199
- async function fetchTarball(url) {
200
- try {
201
- const response = await fetch(url);
202
- if (!response.ok)
203
- return undefined;
204
- return Buffer.from(await response.arrayBuffer());
205
- }
206
- catch {
207
- return undefined;
208
- }
455
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
456
+ main(process.argv.slice(2)).then((code) => {
457
+ process.exitCode = code;
458
+ });
209
459
  }
210
- main(process.argv.slice(2)).then((code) => {
211
- process.exitCode = code;
212
- });
@@ -8,21 +8,158 @@
8
8
  // range (`>=1.2.0 <2.0.0`, `8.x`) hits its literal bounded the same way and not preceded by another
9
9
  // range operator. Each matching line prints once as path:line: text, naming the first requested
10
10
  // literal that matched in the order --version then each --range, so the operator rules on it.
11
- // Exit 0 with no hits, 3 with hits, 64 on usage.
12
- import { readFileSync, statSync } from 'node:fs';
13
- import { listFiles, readMissingFlags, readOptions, } from "../../orkestrel-dispatch/scripts/helpers.js";
14
- const BINARY = /\.(png|jpg|jpeg|gif|webp|ico|woff2?|ttf|otf|eot|zip|tgz|gz|pdf|wasm)$/u;
15
- const COMPARATOR = /^(?:\^|~|>=|>|<=|<|=)?v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)$/u;
16
- function escapePattern(literal) {
11
+ // Exit 0 with no hits, 3 with hits, 64 on usage, including a flag the script does not read and the
12
+ // `--flag=value` form.
13
+ import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ /** Matches the file extensions the sweep skips as binary. */
16
+ export const BINARY = /\.(png|jpg|jpeg|gif|webp|ico|woff2?|ttf|otf|eot|zip|tgz|gz|pdf|wasm)$/u;
17
+ /** Matches a range that is one comparator over one full version and captures the version. */
18
+ export const COMPARATOR = /^(?:\^|~|>=|>|<=|<|=)?v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)$/u;
19
+ /** Names the directories every tree walk skips. */
20
+ export const SKIPPED_DIRECTORIES = new Set([
21
+ 'node_modules',
22
+ '.git',
23
+ 'dist',
24
+ 'tmp',
25
+ '.orkestrel',
26
+ '.idea',
27
+ ]);
28
+ /** Lists every flag the script reads, value flags and switches alike. */
29
+ export const FLAGS = ['--version', '--range', '--paths', '--json'];
30
+ /** Holds the usage line the script prints on a usage refusal. */
31
+ export const USAGE = 'usage: pins.ts --version PRIOR [--range PRIOR_RANGE ...] [--paths src,tests] [--json]';
32
+ /**
33
+ * Normalizes a host path to forward slashes.
34
+ *
35
+ * @param path - A host path.
36
+ * @returns The same path with every backslash replaced by a slash.
37
+ */
38
+ export function normalizeSlashes(path) {
39
+ return path.split('\\').join('/');
40
+ }
41
+ /**
42
+ * Reads the value that follows a flag.
43
+ *
44
+ * @remarks
45
+ * Reads `--flag value` pairs: the first occurrence of a flag wins, a following value that opens
46
+ * with `--` is a missing value, and the `--flag=value` form is not read.
47
+ *
48
+ * @param argv - The arguments after the script path.
49
+ * @param name - The flag, such as `--paths`.
50
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
51
+ * another flag.
52
+ */
53
+ export function readOption(argv, name) {
54
+ const index = argv.indexOf(name);
55
+ const value = index === -1 ? undefined : argv[index + 1];
56
+ return value === undefined || value.startsWith('--') ? undefined : value;
57
+ }
58
+ /**
59
+ * Names the value flags a caller gave with no value.
60
+ *
61
+ * @param argv - The arguments after the script path.
62
+ * @param names - The flags that take a value.
63
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
64
+ */
65
+ export function readMissingFlags(argv, names) {
66
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
67
+ }
68
+ /**
69
+ * Names the flags a caller gave that the script does not read.
70
+ *
71
+ * @remarks
72
+ * Reads the arguments before a bare `--`, which opens a command a script passes on. A `--flag=value`
73
+ * argument is unknown, because the script reads no such form.
74
+ *
75
+ * @param argv - The arguments after the script path.
76
+ * @param flags - Every flag the script reads, value flags and switches alike.
77
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
78
+ */
79
+ export function readUnknownFlags(argv, flags) {
80
+ const end = argv.indexOf('--');
81
+ return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
82
+ }
83
+ /**
84
+ * Reads every value that follows a repeatable flag.
85
+ *
86
+ * @param argv - The arguments after the script path.
87
+ * @param name - The flag, such as `--range`.
88
+ * @returns The values in order, empty when the flag never appears.
89
+ */
90
+ export function readOptions(argv, name) {
91
+ return argv.flatMap((argument, index) => {
92
+ const value = argv[index + 1];
93
+ return argument === name && value !== undefined && !value.startsWith('--') ? [value] : [];
94
+ });
95
+ }
96
+ /**
97
+ * Lists every regular file beneath a directory, skipping the directories the sweep never reads.
98
+ *
99
+ * @remarks
100
+ * A skipped name matches a directory at any depth, exactly and case-sensitively, so a
101
+ * `src/dist/` directory is skipped with the build output and a `Node_Modules/` directory is not.
102
+ *
103
+ * @param directory - The directory to walk.
104
+ * @param skipped - The directory names to skip. Default: {@link SKIPPED_DIRECTORIES}.
105
+ * @returns Each file's forward-slash path and modification time, in walk order.
106
+ * @throws Thrown when `directory` does not exist or cannot be read; check `statSync` first.
107
+ */
108
+ export function listFiles(directory, skipped = SKIPPED_DIRECTORIES) {
109
+ const files = [];
110
+ const pending = [directory];
111
+ while (pending.length > 0) {
112
+ const current = pending.pop();
113
+ if (current === undefined)
114
+ break;
115
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
116
+ if (skipped.has(entry.name))
117
+ continue;
118
+ const path = join(current, entry.name);
119
+ if (entry.isDirectory())
120
+ pending.push(path);
121
+ else if (entry.isFile()) {
122
+ files.push({ path: normalizeSlashes(path), modified: statSync(path).mtimeMs });
123
+ }
124
+ }
125
+ }
126
+ return files;
127
+ }
128
+ /**
129
+ * Escapes a literal for use inside a regular expression.
130
+ *
131
+ * @param literal - The text to match exactly.
132
+ * @returns The text with every regular expression metacharacter backslash-escaped.
133
+ */
134
+ export function escapePattern(literal) {
17
135
  return literal.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
18
136
  }
19
- function buildVersionPattern(version) {
137
+ /**
138
+ * Builds the pattern that finds a version bounded by characters other than digits and dots.
139
+ *
140
+ * @param version - The version literal.
141
+ * @returns The pattern.
142
+ */
143
+ export function buildVersionPattern(version) {
20
144
  return new RegExp(`(?<![0-9.])${escapePattern(version)}(?![0-9.])`, 'u');
21
145
  }
22
- function buildVersionLiteral(literal) {
146
+ /**
147
+ * Builds the literal for a requested version.
148
+ *
149
+ * @param literal - The version as the operator wrote it.
150
+ * @returns The literal with its bounded pattern.
151
+ */
152
+ export function buildVersionLiteral(literal) {
23
153
  return { literal, pattern: buildVersionPattern(literal) };
24
154
  }
25
- function buildRangeLiteral(literal) {
155
+ /**
156
+ * Builds the literal for a requested range.
157
+ *
158
+ * @param literal - The range as the operator wrote it.
159
+ * @returns A one-comparator range matched by its bare version in any form, any other range matched as
160
+ * its own text not preceded by a range operator.
161
+ */
162
+ export function buildRangeLiteral(literal) {
26
163
  const version = COMPARATOR.exec(literal)?.[1];
27
164
  if (version !== undefined)
28
165
  return { literal, pattern: buildVersionPattern(version) };
@@ -31,7 +168,14 @@ function buildRangeLiteral(literal) {
31
168
  pattern: new RegExp(`(?<![0-9.^~<>=])${escapePattern(literal)}(?![0-9.])`, 'u'),
32
169
  };
33
170
  }
34
- function sweepFile(path, literals) {
171
+ /**
172
+ * Finds the lines of one file that carry a requested literal.
173
+ *
174
+ * @param path - The file to read.
175
+ * @param literals - The literals to look for, in the order that names a line's literal.
176
+ * @returns One hit per matching line, naming the first literal that matched; none for a binary file.
177
+ */
178
+ export function sweepFile(path, literals) {
35
179
  const content = readFileSync(path);
36
180
  if (content.includes(0))
37
181
  return [];
@@ -45,7 +189,18 @@ function sweepFile(path, literals) {
45
189
  : [{ path, line: index + 1, literal: match.literal, text: text.trim() }];
46
190
  });
47
191
  }
48
- function main(argv) {
192
+ /**
193
+ * Runs the sweep and prints its hits.
194
+ *
195
+ * @param argv - The arguments after the script path.
196
+ * @returns The exit code: 0 with no hits, 3 with hits, 64 on usage.
197
+ */
198
+ export function main(argv) {
199
+ const unknown = readUnknownFlags(argv, FLAGS);
200
+ if (unknown.length > 0) {
201
+ console.error(`pins: unknown flag ${unknown.join(', ')}; give each flag and its value as separate arguments\n${USAGE}`);
202
+ return 64;
203
+ }
49
204
  const missing = readMissingFlags(argv, ['--version', '--range', '--paths']);
50
205
  if (missing.length > 0) {
51
206
  console.error(`pins: ${missing.join(', ')} given with no value`);
@@ -55,7 +210,7 @@ function main(argv) {
55
210
  const ranges = readOptions(argv, '--range');
56
211
  const paths = (readOptions(argv, '--paths')[0] ?? 'src,tests').split(',');
57
212
  if (version === undefined && ranges.length === 0) {
58
- console.error('usage: pins.ts --version PRIOR [--range PRIOR_RANGE ...] [--paths src,tests] [--json]');
213
+ console.error(USAGE);
59
214
  return 64;
60
215
  }
61
216
  const literals = [
@@ -88,4 +243,6 @@ function main(argv) {
88
243
  }
89
244
  return hits.length === 0 ? 0 : 3;
90
245
  }
91
- process.exitCode = main(process.argv.slice(2));
246
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
247
+ process.exitCode = main(process.argv.slice(2));
248
+ }