@orkestrel/scaffold 0.0.107 → 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 (164) 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 +368 -27
  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 +26 -20
  25. package/dist/host/agents/orchestration.md +98 -58
  26. package/dist/host/agents/platforms/posix.md +15 -0
  27. package/dist/host/agents/platforms/windows.md +37 -0
  28. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  29. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  30. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  31. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  32. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  33. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  34. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  35. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  36. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  37. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +12 -14
  38. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +36 -24
  39. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +14 -11
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  48. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +428 -27
  49. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  50. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  51. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  52. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  53. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  54. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  55. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  56. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  58. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  59. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  60. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  63. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  64. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  65. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  66. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  67. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  68. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  69. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +91 -30
  70. package/dist/host/agents/skills/orkestrel-parallel/agents/openai.yaml +2 -2
  71. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +34 -0
  72. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +29 -0
  73. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +29 -0
  74. package/dist/host/agents/skills/orkestrel-parallel/references/t3.md +31 -0
  75. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  76. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  77. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  78. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  79. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  80. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  81. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  82. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -10
  83. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  86. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  87. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  88. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  89. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  90. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  91. package/dist/host/agents/templates/brief.md +1 -1
  92. package/dist/host/agents/transports/claude-cli.md +3 -3
  93. package/dist/host/agents/transports/codex.md +3 -8
  94. package/dist/host/agents/transports/cursor.md +2 -3
  95. package/dist/host/claude/AGENTS.md +7 -10
  96. package/dist/host/claude/agents/analyst.md +3 -3
  97. package/dist/host/claude/agents/astra.md +3 -3
  98. package/dist/host/claude/agents/builder.md +1 -1
  99. package/dist/host/claude/agents/checker.md +1 -1
  100. package/dist/host/claude/agents/distiller.md +1 -1
  101. package/dist/host/claude/agents/grok.md +1 -1
  102. package/dist/host/claude/agents/opus.md +2 -2
  103. package/dist/host/claude/agents/orkestrel.md +57 -90
  104. package/dist/host/claude/agents/researcher.md +1 -1
  105. package/dist/host/claude/agents/reviewer.md +4 -0
  106. package/dist/host/claude/agents/scout.md +1 -1
  107. package/dist/host/claude/agents/verifier.md +1 -1
  108. package/dist/host/claude/rules/application.md +14 -21
  109. package/dist/host/claude/rules/architecture.md +34 -44
  110. package/dist/host/claude/rules/browser.md +3 -4
  111. package/dist/host/claude/rules/campaign.md +55 -0
  112. package/dist/host/claude/rules/documentation.md +26 -11
  113. package/dist/host/claude/rules/names.md +9 -4
  114. package/dist/host/claude/rules/patterns.md +6 -7
  115. package/dist/host/claude/rules/portability.md +2 -3
  116. package/dist/host/claude/rules/quality.md +4 -5
  117. package/dist/host/claude/rules/styles.md +2 -2
  118. package/dist/host/claude/rules/tests.md +57 -114
  119. package/dist/host/claude/rules/typescript.md +8 -10
  120. package/dist/host/claude/rules/workspace.md +21 -19
  121. package/dist/host/claude/rules/writing.md +3 -3
  122. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  123. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  124. package/dist/host/claude/skills/orkestrel-parallel/SKILL.md +1 -1
  125. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  126. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  127. package/dist/host/codex/agents/analyst.toml +9 -3
  128. package/dist/host/codex/agents/astra.toml +3 -4
  129. package/dist/host/codex/agents/builder.toml +1 -1
  130. package/dist/host/codex/agents/opus.toml +3 -4
  131. package/dist/host/codex/agents/orkestrel.toml +1 -1
  132. package/dist/host/codex/agents/researcher.toml +2 -2
  133. package/dist/host/codex/agents/verifier.toml +1 -1
  134. package/dist/host/codex/config.toml +3 -3
  135. package/dist/host/configs/helpers.ts +21 -1
  136. package/dist/host/configs/policy.ts +405 -16
  137. package/dist/host/cursor/rules/orchestration.mdc +1 -0
  138. package/dist/host/dotfiles/oxlintrc.json +141 -5
  139. package/dist/host/guides/agent.md +111 -134
  140. package/dist/host/guides/capture.md +801 -0
  141. package/dist/host/guides/probe.md +27 -8
  142. package/dist/host/guides/scaffold.md +152 -19
  143. package/dist/host/guides/study.md +534 -440
  144. package/dist/host/guides/test.md +111 -44
  145. package/dist/host/manifest.json +199 -242
  146. package/dist/host/tests/config.test.ts +1558 -219
  147. package/dist/host/tests/policy.test.ts +127 -1
  148. package/dist/host/tests/setupPolicy.ts +1389 -39
  149. package/dist/src/core/index.cjs +239 -87
  150. package/dist/src/core/index.cjs.map +1 -1
  151. package/dist/src/core/index.d.cts +123 -39
  152. package/dist/src/core/index.d.ts +123 -39
  153. package/dist/src/core/index.js +234 -86
  154. package/dist/src/core/index.js.map +1 -1
  155. package/dist/src/server/index.cjs +20 -27
  156. package/dist/src/server/index.cjs.map +1 -1
  157. package/dist/src/server/index.js +21 -28
  158. package/dist/src/server/index.js.map +1 -1
  159. package/package.json +11 -11
  160. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  161. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  162. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  163. package/dist/host/agents/skills/orkestrel-parallel/references/harnesses.md +0 -19
  164. package/dist/host/guides/supervisor.md +0 -4890
@@ -1,66 +1,45 @@
1
- // Pin a consumer to fresh local tarballs of the packages it depends on, report every pin against the
2
- // ledger that records it, and restore the ranges the pins replaced. Run from any directory:
3
- // node .agents/skills/orkestrel-publish/scripts/swap.ts --pin --consumer DIR --source DIR [--source DIR ...] [--override NAME ...] [--no-build] [--dry-run] [--json]
4
- // node .agents/skills/orkestrel-publish/scripts/swap.ts --restore --consumer DIR [--package NAME ...] [--json]
5
- // node .agents/skills/orkestrel-publish/scripts/swap.ts --status --consumer DIR [--json]
6
- // Give exactly one of --pin, --restore, and --status; --source, --override, and --package repeat.
7
- // --pin refuses a source whose packed manifest names a local dependency in any dependency field:
8
- // npm's local forms, a file: or link: prefix, a path start (., ~/, /, \, or a drive letter), or an
9
- // archive name ending in .tgz, .tar.gz, or .tar that carries no protocol and is not a user/repo
10
- // shorthand. It builds each source (`npm run build`, unless --no-build), and packs it with
11
- // --ignore-scripts, a source that carries a ledger of its own packing with the ranges that ledger
12
- // replaced. Each archive moves to
13
- // <consumer>/tmp/tarballs/<scope>-<name>-<version>-<12 hex of its SHA-256>.tgz. The ledger
14
- // <consumer>/tmp/tarballs/pins.json records the replaced range, the tarball and its SHA-256, the
15
- // source with its commit and dirty flag, whether the overrides block existed before the first pin,
16
- // and the time, and is written before the manifest. The field that declares each package then takes
17
- // the tarball's absolute file: spec, because a relative spec breaks after the consumer is itself
18
- // packed and installed elsewhere. --override NAME sets overrides[NAME] to $NAME when the consumer
19
- // declares NAME, else to the tarball spec of the source named NAME, and a created block lands where
20
- // oxfmt sorts it. The install runs with --ignore-scripts and deletes node_modules/.vite, then the
21
- // proof requires each pinned package, an override-only one included, to be a directory whose file
22
- // set is its pack list and whose bytes are its source's, no other copy of it to be installed, and
23
- // every other top-level @orkestrel package to be byte-unchanged; a proved pin records the digest of
24
- // its installed bytes. --dry-run builds, packs, and writes nothing, and prints the plan.
25
- // --restore puts back each recorded range (all, or each --package) and every override the pins
26
- // replaced, keeps an overrides block that existed before the pin, drops the lock entry and the
27
- // installed copy of each package it restores from the registry or whose override it drops so the
28
- // install resolves them again, installs, and proves each package is installed at what its range
29
- // names. Before it writes anything, --restore refuses with 64 a ledger whose package or override
30
- // keys are not package names, and a removal whose install path resolves outside node_modules.
31
- // --status writes nothing and reports each drift between the ledger, the manifest, the tarballs, the
32
- // installed bytes, and the source commits; a manifest spec holds its tarball only when it names the
33
- // ledger archive's full location.
34
- // --pin and --restore write a receipt create-only to tmp/units/swap-<consumer>-<time>.json under the
35
- // working directory. Every mode prints text lines, or one JSON line with --json.
36
- // Exit 0 on success, 1 when a command or a proof failed, 3 when --status reports drift, 64 on usage.
37
1
  import { spawnSync } from 'node:child_process';
38
2
  import { createHash } from 'node:crypto';
39
- import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, writeFileSync, } from 'node:fs';
3
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
40
4
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
41
- import { isRecord, normalizeSlashes, parseJsonRecord, readMissingFlags, readOption, readOptions, readString, removeTree, runNpm, scanLocalSpecs, } from "../../orkestrel-dispatch/scripts/helpers.js";
42
- const FIELDS = ['dependencies', 'devDependencies'];
43
- const MODES = ['pin', 'restore', 'status'];
44
- const TARBALL_DIRECTORY = 'tmp/tarballs';
45
- const LEDGER_FILE = 'pins.json';
46
- const RECEIPT_DIRECTORY = 'tmp/units';
47
- const EXIT_FAILURE = 1;
48
- const EXIT_DRIFT = 3;
49
- const EXIT_USAGE = 64;
50
- const FORMAT_PROBLEM = 'package.json is not tab-indented JSON with a trailing newline; the script would reformat it';
51
- const VALUE_FLAGS = {
5
+ /** Lists the manifest fields a pin rewrites. */
6
+ export const FIELDS = ['dependencies', 'devDependencies'];
7
+ /** Lists the modes in the order the script checks them. */
8
+ export const MODES = ['pin', 'restore', 'status'];
9
+ /** Names the consumer directory that holds the tarballs and the ledger. */
10
+ export const TARBALL_DIRECTORY = 'tmp/tarballs';
11
+ /** Names the ledger file inside the tarball directory. */
12
+ export const LEDGER_FILE = 'pins.json';
13
+ /** Names the working-directory folder that receives the receipts. */
14
+ export const RECEIPT_DIRECTORY = 'tmp/units';
15
+ /** Holds the exit code for a command or a proof that failed. */
16
+ export const EXIT_FAILURE = 1;
17
+ /** Holds the exit code for a status that reports drift. */
18
+ export const EXIT_DRIFT = 3;
19
+ /** Holds the exit code for a usage refusal. */
20
+ export const EXIT_USAGE = 64;
21
+ /** Holds the refusal for a manifest the script would reformat. */
22
+ export const FORMAT_PROBLEM = 'package.json is not tab-indented JSON with a trailing newline; the script would reformat it';
23
+ /** Lists the value flags each mode reads. */
24
+ export const VALUE_FLAGS = {
52
25
  pin: ['--consumer', '--source', '--override'],
53
26
  restore: ['--consumer', '--package'],
54
27
  status: ['--consumer'],
55
28
  };
56
- const SWITCH_FLAGS = {
29
+ /** Lists the switch flags each mode reads. */
30
+ export const SWITCH_FLAGS = {
57
31
  pin: ['--pin', '--no-build', '--dry-run', '--json'],
58
32
  restore: ['--restore', '--json'],
59
33
  status: ['--status', '--json'],
60
34
  };
61
- // oxfmt 0.72.0 sorts these `package.json` keys ahead of `overrides`; every other key it knows, and
62
- // every key it does not know, follows it.
63
- const OVERRIDE_PRECEDERS = [
35
+ /**
36
+ * Lists the `package.json` keys oxfmt sorts ahead of `overrides`.
37
+ *
38
+ * @remarks
39
+ * Derived for the installed oxfmt, 0.74.0; every other key it knows, and every key it does not know,
40
+ * follows `overrides`. `swap.test.ts` proves each key against the installed formatter.
41
+ */
42
+ export const OVERRIDE_PRECEDERS = [
64
43
  '$schema',
65
44
  'name',
66
45
  'displayName',
@@ -123,22 +102,224 @@ const OVERRIDE_PRECEDERS = [
123
102
  'bundleDependencies',
124
103
  'resolutions',
125
104
  ];
126
- // An optional `@scope/` and a name of the characters a registry URL carries untouched; a path
127
- // separator beyond the scope's one `/`, an absolute form, and a drive form fall outside it.
128
- const PACKAGE_NAME_PATTERN = /^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/u;
129
- const PACKAGE_NAME_LIMIT = 214;
130
- const USAGE = [
105
+ /**
106
+ * Matches a package name.
107
+ *
108
+ * @remarks
109
+ * An optional `@scope/` and a name of the characters a registry URL carries untouched; a path
110
+ * separator beyond the scope's one `/`, an absolute form, and a drive form fall outside it.
111
+ */
112
+ export const PACKAGE_NAME_PATTERN = /^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/u;
113
+ /** Holds the longest package name a registry accepts. */
114
+ export const PACKAGE_NAME_LIMIT = 214;
115
+ /** Holds the usage text printed on a usage refusal. */
116
+ export const USAGE = [
131
117
  'usage: swap.ts --pin --consumer DIR --source DIR [--source DIR ...] [--override NAME ...] [--no-build] [--dry-run] [--json]',
132
118
  ' swap.ts --restore --consumer DIR [--package NAME ...] [--json]',
133
119
  ' swap.ts --status --consumer DIR [--json]',
134
120
  ].join('\n');
135
- function isPackageName(name) {
121
+ /** Names the manifest fields that hold specs, including nested overrides. */
122
+ export const SPEC_FIELDS = Object.freeze([
123
+ 'dependencies',
124
+ 'devDependencies',
125
+ 'peerDependencies',
126
+ 'optionalDependencies',
127
+ 'overrides',
128
+ ]);
129
+ /** Matches npm local prefixes and path starts, including drive-relative paths. */
130
+ export const LOCAL_SPEC_PATTERN = /^(?:file:|link:|\.|~[\\/]|[\\/]|[A-Za-z]:)/iu;
131
+ /** Matches archive specs with no protocol and a tgz, tar.gz, or tar extension. */
132
+ export const ARCHIVE_SPEC_PATTERN = /^[^:]+\.(?:tgz|tar\.gz|tar)$/iu;
133
+ /** Matches hosted user/repository shorthand even when its name ends in an archive extension. */
134
+ export const SHORTHAND_SPEC_PATTERN = /^[^:@%/\s.-][^:@%/\s]*\/[^:@\s/%]+$/u;
135
+ /**
136
+ * Checks whether a value is a non-null, non-array object.
137
+ *
138
+ * @param value - The value to inspect.
139
+ * @returns True if the value is an object other than null or an array; false otherwise.
140
+ */
141
+ export function isRecord(value) {
142
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
143
+ }
144
+ /**
145
+ * Checks whether npm resolves a spec from the local host.
146
+ *
147
+ * @param spec - The package spec, with leading whitespace ignored.
148
+ * @returns True if the spec names a local path or archive; false otherwise.
149
+ */
150
+ export function matchesLocalSpec(spec) {
151
+ const text = spec.trimStart();
152
+ return (LOCAL_SPEC_PATTERN.test(text) ||
153
+ (ARCHIVE_SPEC_PATTERN.test(text) && !SHORTHAND_SPEC_PATTERN.test(text)));
154
+ }
155
+ /**
156
+ * Collects string specs with the dotted path of each nested record member.
157
+ *
158
+ * @param path - The field path that prefixes each finding.
159
+ * @param value - A string or finite, acyclic manifest record; other values contribute no specs.
160
+ * @returns The path and spec pairs in record order.
161
+ */
162
+ export function collectSpecs(path, value) {
163
+ if (typeof value === 'string')
164
+ return [[path, value]];
165
+ if (!isRecord(value))
166
+ return [];
167
+ return Object.entries(value).flatMap(([key, child]) => collectSpecs(`${path}.${key}`, child));
168
+ }
169
+ /**
170
+ * Finds local specs in the manifest's spec fields, including nested overrides.
171
+ *
172
+ * @param manifest - A finite, acyclic package manifest record.
173
+ * @returns Each local spec's dotted field path and spec, in field and record order.
174
+ */
175
+ export function scanLocalSpecs(manifest) {
176
+ return SPEC_FIELDS.flatMap((field) => collectSpecs(field, manifest[field]).filter(([, spec]) => matchesLocalSpec(spec)));
177
+ }
178
+ /**
179
+ * Removes a tree with bounded retries for transient host removal errors.
180
+ *
181
+ * @param path - The directory to remove; a missing path succeeds.
182
+ * @returns Nothing after the removal succeeds.
183
+ * @throws Thrown when removal fails after the bounded retries or the host refuses the path.
184
+ */
185
+ export function removeTree(path) {
186
+ rmSync(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
187
+ }
188
+ /**
189
+ * Normalizes a host path to forward slashes.
190
+ *
191
+ * @param path - A host path.
192
+ * @returns The same path with every backslash replaced by a slash.
193
+ */
194
+ export function normalizeSlashes(path) {
195
+ return path.split('\\').join('/');
196
+ }
197
+ /**
198
+ * Reads the value that follows a flag.
199
+ *
200
+ * @remarks
201
+ * Reads `--flag value` pairs: the first occurrence of a flag wins, a following value that opens
202
+ * with `--` is a missing value, and the `--flag=value` form is not read.
203
+ *
204
+ * @param argv - The arguments after the script path.
205
+ * @param name - The flag, such as `--consumer`.
206
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
207
+ * another flag.
208
+ */
209
+ export function readOption(argv, name) {
210
+ const index = argv.indexOf(name);
211
+ const value = index === -1 ? undefined : argv[index + 1];
212
+ return value === undefined || value.startsWith('--') ? undefined : value;
213
+ }
214
+ /**
215
+ * Names the value flags a caller gave with no value.
216
+ *
217
+ * @param argv - The arguments after the script path.
218
+ * @param names - The flags that take a value.
219
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
220
+ */
221
+ export function readMissingFlags(argv, names) {
222
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
223
+ }
224
+ /**
225
+ * Reads every value that follows a repeatable flag.
226
+ *
227
+ * @param argv - The arguments after the script path.
228
+ * @param name - The flag, such as `--source`.
229
+ * @returns The values in order, empty when the flag never appears.
230
+ */
231
+ export function readOptions(argv, name) {
232
+ return argv.flatMap((argument, index) => {
233
+ const value = argv[index + 1];
234
+ return argument === name && value !== undefined && !value.startsWith('--') ? [value] : [];
235
+ });
236
+ }
237
+ /**
238
+ * Parses a whole text as one JSON object.
239
+ *
240
+ * @param text - A JSON document, formatted or not, such as a manifest.
241
+ * @returns The parsed object, or undefined when the text is not a JSON object.
242
+ */
243
+ export function parseJsonRecord(text) {
244
+ try {
245
+ const parsed = JSON.parse(text);
246
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
247
+ return Object.fromEntries(Object.entries(parsed));
248
+ }
249
+ }
250
+ catch { }
251
+ return undefined;
252
+ }
253
+ /**
254
+ * Reads a string member of a record.
255
+ *
256
+ * @param record - The record, or undefined.
257
+ * @param key - The member name.
258
+ * @returns The string, or undefined when the member is absent or not a string.
259
+ */
260
+ export function readString(record, key) {
261
+ const value = record?.[key];
262
+ return typeof value === 'string' ? value : undefined;
263
+ }
264
+ /**
265
+ * Resolves the npm command for this host.
266
+ *
267
+ * @remarks
268
+ * The `npm-cli.js` entry beside the running Node binary spawns without a shell on every host; the
269
+ * `npm` shim on Windows is a `.cmd` file that needs one, so that path is the fallback alone.
270
+ *
271
+ * @returns The file to spawn, the arguments that precede npm's own, and whether a shell is needed.
272
+ */
273
+ export function resolveNpm() {
274
+ const cli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
275
+ if (existsSync(cli))
276
+ return { file: process.execPath, prefix: [cli], shell: false };
277
+ return { file: 'npm', prefix: [], shell: process.platform === 'win32' };
278
+ }
279
+ /**
280
+ * Runs npm with the given arguments and returns the finished child.
281
+ *
282
+ * @param args - The npm arguments, such as `['install']`.
283
+ * @param cwd - The working directory. Default: the current directory.
284
+ * @returns The spawn result with `utf8` stdout and stderr.
285
+ */
286
+ export function runNpm(args, cwd) {
287
+ const npm = resolveNpm();
288
+ return spawnSync(npm.file, [...npm.prefix, ...args], {
289
+ encoding: 'utf8',
290
+ windowsHide: true,
291
+ shell: npm.shell,
292
+ maxBuffer: 64 * 1024 * 1024,
293
+ ...(cwd === undefined ? {} : { cwd }),
294
+ });
295
+ }
296
+ /**
297
+ * Checks whether a ledger key is a package name that stays inside `node_modules`.
298
+ *
299
+ * @param name - The ledger key.
300
+ * @returns True if the key is an optional scope and a name of registry-safe characters within the
301
+ * registry's length limit and holding no `..`; false otherwise.
302
+ */
303
+ export function isPackageName(name) {
136
304
  return (name.length <= PACKAGE_NAME_LIMIT && !name.includes('..') && PACKAGE_NAME_PATTERN.test(name));
137
305
  }
138
- function isManifestField(value) {
306
+ /**
307
+ * Checks whether a value names a manifest field a pin rewrites.
308
+ *
309
+ * @param value - The value to inspect.
310
+ * @returns True if the value is `dependencies` or `devDependencies`; false otherwise.
311
+ */
312
+ export function isManifestField(value) {
139
313
  return value === 'dependencies' || value === 'devDependencies';
140
314
  }
141
- function isLedgerEntry(value) {
315
+ /**
316
+ * Checks whether a value is a package row of the ledger.
317
+ *
318
+ * @param value - The value to inspect.
319
+ * @returns True if the value carries the tarball, hash, source, and time strings and each optional
320
+ * member in its declared type; false otherwise.
321
+ */
322
+ export function isLedgerEntry(value) {
142
323
  return (isRecord(value) &&
143
324
  typeof value.tarball === 'string' &&
144
325
  typeof value.sha256 === 'string' &&
@@ -150,14 +331,29 @@ function isLedgerEntry(value) {
150
331
  (value.dirty === undefined || typeof value.dirty === 'boolean') &&
151
332
  (value.inventory === undefined || typeof value.inventory === 'string'));
152
333
  }
153
- function isOverrideEntry(value) {
334
+ /**
335
+ * Checks whether a value is an override row of the ledger.
336
+ *
337
+ * @param value - The value to inspect.
338
+ * @returns True if the value carries the value and time strings and an optional replaced string;
339
+ * false otherwise.
340
+ */
341
+ export function isOverrideEntry(value) {
154
342
  return (isRecord(value) &&
155
343
  typeof value.value === 'string' &&
156
344
  typeof value.at === 'string' &&
157
345
  (value.replaced === undefined || typeof value.replaced === 'string'));
158
346
  }
159
- // A ledger with one malformed row is refused whole, so a restore never drops a range it cannot read.
160
- function parseLedger(value) {
347
+ /**
348
+ * Parses a ledger record.
349
+ *
350
+ * @remarks
351
+ * A ledger with one malformed row is refused whole, so a restore never drops a range it cannot read.
352
+ *
353
+ * @param value - The parsed ledger file.
354
+ * @returns The ledger, or undefined when a block is missing or any row is malformed.
355
+ */
356
+ export function parseLedger(value) {
161
357
  if (!isRecord(value) || !isRecord(value.packages) || !isRecord(value.overrides))
162
358
  return undefined;
163
359
  const rows = Object.entries(value.packages);
@@ -174,31 +370,79 @@ function parseLedger(value) {
174
370
  ...(value.blockExisted === undefined ? {} : { blockExisted: value.blockExisted }),
175
371
  };
176
372
  }
177
- function serializeJSON(value) {
373
+ /**
374
+ * Serializes a value as every Orkestrel manifest is written.
375
+ *
376
+ * @param value - The value to serialize.
377
+ * @returns Tab-indented JSON with a trailing newline.
378
+ */
379
+ export function serializeJSON(value) {
178
380
  return `${JSON.stringify(value, null, '\t')}\n`;
179
381
  }
180
- function readBlock(manifest, field) {
382
+ /**
383
+ * Reads one dependency-style block of a manifest.
384
+ *
385
+ * @param manifest - The manifest.
386
+ * @param field - The block's field name.
387
+ * @returns The block, or an empty record when the field is absent or not a record.
388
+ */
389
+ export function readBlock(manifest, field) {
181
390
  const block = manifest[field];
182
391
  return isRecord(block) ? block : {};
183
392
  }
184
- function findDeclaration(manifest, name) {
393
+ /**
394
+ * Finds the field that declares a package.
395
+ *
396
+ * @param manifest - The manifest.
397
+ * @param name - The package name.
398
+ * @returns The field and spec of the first of `dependencies` and `devDependencies` that holds a
399
+ * string spec for the name, or undefined when neither does.
400
+ */
401
+ export function findDeclaration(manifest, name) {
185
402
  const field = FIELDS.find((candidate) => typeof readBlock(manifest, candidate)[name] === 'string');
186
403
  const spec = field === undefined ? undefined : readBlock(manifest, field)[name];
187
404
  return field === undefined || typeof spec !== 'string' ? undefined : { field, spec };
188
405
  }
189
- function compareKeys(left, right) {
406
+ /**
407
+ * Orders two keys by their code units.
408
+ *
409
+ * @param left - One key.
410
+ * @param right - The other key.
411
+ * @returns A negative number, 0, or a positive number as the left key sorts before, equal to, or
412
+ * after the right.
413
+ */
414
+ export function compareKeys(left, right) {
190
415
  return left < right ? -1 : left > right ? 1 : 0;
191
416
  }
192
- // A block whose keys are sorted stays sorted, because oxfmt sorts a dependency or override block;
193
- // any other block takes the added keys last.
194
- function mergeEntries(kept, added) {
417
+ /**
418
+ * Merges added entries into a block's kept entries.
419
+ *
420
+ * @remarks
421
+ * A block whose keys are sorted stays sorted, because oxfmt sorts a dependency or override block;
422
+ * any other block takes the added keys last.
423
+ *
424
+ * @param kept - The entries the block keeps, in order.
425
+ * @param added - The entries the block gains.
426
+ * @returns The merged entries.
427
+ */
428
+ export function mergeEntries(kept, added) {
195
429
  const sorted = kept.every(([name], index) => index === 0 || compareKeys(kept[index - 1]?.[0] ?? '', name) <= 0);
196
430
  const merged = [...kept, ...added];
197
431
  return sorted ? merged.toSorted(([left], [right]) => compareKeys(left, right)) : merged;
198
432
  }
199
- // A created block lands before the first key oxfmt sorts after `overrides`, or last when every key
200
- // sorts ahead of it.
201
- function placeBlock(manifest, field, block) {
433
+ /**
434
+ * Inserts a created block where oxfmt sorts it.
435
+ *
436
+ * @remarks
437
+ * A created block lands before the first key oxfmt sorts after `overrides`, or last when every key
438
+ * sorts ahead of it.
439
+ *
440
+ * @param manifest - The manifest.
441
+ * @param field - The new block's field name.
442
+ * @param block - The new block.
443
+ * @returns The manifest with the block inserted.
444
+ */
445
+ export function placeBlock(manifest, field, block) {
202
446
  const entries = Object.entries(manifest);
203
447
  const follower = entries.findIndex(([key]) => !OVERRIDE_PRECEDERS.includes(key));
204
448
  const position = follower === -1 ? entries.length : follower;
@@ -208,10 +452,21 @@ function placeBlock(manifest, field, block) {
208
452
  ...entries.slice(position),
209
453
  ]);
210
454
  }
211
- // A changed key keeps its position, a key mapped to undefined is removed, and an `overrides` block
212
- // left empty is removed unless `keepEmpty` is set. A block that is absent and gains nothing is not
213
- // created.
214
- function rewriteBlock(manifest, field, changes, keepEmpty) {
455
+ /**
456
+ * Rewrites one block of a manifest.
457
+ *
458
+ * @remarks
459
+ * A changed key keeps its position, a key mapped to undefined is removed, and an `overrides` block
460
+ * left empty is removed unless `keepEmpty` is set. A block that is absent and gains nothing is not
461
+ * created.
462
+ *
463
+ * @param manifest - The manifest.
464
+ * @param field - The block's field name.
465
+ * @param changes - The new spec for each changed key, or undefined to remove the key.
466
+ * @param keepEmpty - If `true`, an `overrides` block left empty stays; if `false`, it is removed.
467
+ * @returns The rewritten manifest.
468
+ */
469
+ export function rewriteBlock(manifest, field, changes, keepEmpty) {
215
470
  const present = Object.entries(readBlock(manifest, field));
216
471
  const kept = present.flatMap(([name, value]) => {
217
472
  if (!changes.has(name))
@@ -232,12 +487,26 @@ function rewriteBlock(manifest, field, changes, keepEmpty) {
232
487
  : [[key, Object.fromEntries(next)]];
233
488
  }));
234
489
  }
235
- function renderValue(value) {
490
+ /**
491
+ * Renders a manifest spec for a change line.
492
+ *
493
+ * @param value - The spec, or undefined for an absent entry.
494
+ * @returns The string itself, the JSON of any other value, or undefined for undefined.
495
+ */
496
+ export function renderValue(value) {
236
497
  if (value === undefined)
237
498
  return undefined;
238
499
  return typeof value === 'string' ? value : JSON.stringify(value);
239
500
  }
240
- function diffManifests(before, after) {
501
+ /**
502
+ * Lists the entries that differ between two manifests.
503
+ *
504
+ * @param before - The manifest before.
505
+ * @param after - The manifest after.
506
+ * @returns One change per entry of `dependencies`, `devDependencies`, and `overrides` whose rendered
507
+ * spec differs.
508
+ */
509
+ export function diffManifests(before, after) {
241
510
  return ['dependencies', 'devDependencies', 'overrides'].flatMap((block) => {
242
511
  const from = readBlock(before, block);
243
512
  const to = readBlock(after, block);
@@ -249,24 +518,60 @@ function diffManifests(before, after) {
249
518
  });
250
519
  });
251
520
  }
252
- function buildTarballName(name, version, sha256) {
521
+ /**
522
+ * Builds a tarball's file name.
523
+ *
524
+ * @param name - The package name.
525
+ * @param version - The package version.
526
+ * @param sha256 - The archive's SHA-256 digest.
527
+ * @returns The scope and name joined by a dash, the version, and the first twelve hex digits of the
528
+ * digest.
529
+ */
530
+ export function buildTarballName(name, version, sha256) {
253
531
  return `${name.replace(/^@/u, '').replace('/', '-')}-${version}-${sha256.slice(0, 12)}.tgz`;
254
532
  }
255
- // npm resolves a dependency's relative file: spec against that dependency's own directory, so only an
256
- // absolute spec resolves inside a packed consumer installed elsewhere.
257
- function buildTarballSpec(consumer, file) {
533
+ /**
534
+ * Builds the spec a manifest holds for a tarball.
535
+ *
536
+ * @remarks
537
+ * npm resolves a dependency's relative file: spec against that dependency's own directory, so only
538
+ * an absolute spec resolves inside a packed consumer installed elsewhere.
539
+ *
540
+ * @param consumer - The consumer directory.
541
+ * @param file - The tarball's file name.
542
+ * @returns The absolute `file:` spec with forward slashes.
543
+ */
544
+ export function buildTarballSpec(consumer, file) {
258
545
  return `file:${normalizeSlashes(resolve(consumer, ...TARBALL_DIRECTORY.split('/'), file))}`;
259
546
  }
260
- // The relative `file:tmp/tarballs/<file>` form is what earlier pins wrote, so a re-pin over it keeps
261
- // the range the first pin replaced.
262
- function isTarballSpec(spec) {
547
+ /**
548
+ * Checks whether a spec names a tarball in a tarball directory.
549
+ *
550
+ * @remarks
551
+ * The relative `file:tmp/tarballs/<file>` form is what earlier pins wrote, so a re-pin over it keeps
552
+ * the range the first pin replaced.
553
+ *
554
+ * @param spec - The manifest spec.
555
+ * @returns True if the spec is a `file:` spec into the relative or an absolute tarball directory;
556
+ * false otherwise.
557
+ */
558
+ export function isTarballSpec(spec) {
263
559
  if (!spec.startsWith('file:'))
264
560
  return false;
265
561
  const path = normalizeSlashes(spec.slice('file:'.length));
266
562
  return (path.startsWith(`${TARBALL_DIRECTORY}/`) ||
267
563
  (isAbsolute(path) && path.includes(`/${TARBALL_DIRECTORY}/`)));
268
564
  }
269
- function buildLedgerEntry(manifest, ledger, tarball, at) {
565
+ /**
566
+ * Builds the ledger row for one packed source.
567
+ *
568
+ * @param manifest - The consumer manifest before the pin.
569
+ * @param ledger - The ledger before the pin.
570
+ * @param tarball - The packed source.
571
+ * @param at - The time of the pin.
572
+ * @returns The row; a re-pin over a tarball spec keeps the range the first pin replaced.
573
+ */
574
+ export function buildLedgerEntry(manifest, ledger, tarball, at) {
270
575
  const declaration = findDeclaration(manifest, tarball.source.name);
271
576
  const prior = ledger.packages[tarball.source.name];
272
577
  const repinned = prior !== undefined && declaration !== undefined && isTarballSpec(declaration.spec);
@@ -282,11 +587,23 @@ function buildLedgerEntry(manifest, ledger, tarball, at) {
282
587
  at,
283
588
  };
284
589
  }
285
- // The replaced range is kept from the first pin while the manifest still holds a tarball spec, and a
286
- // package no field declares is recorded without a field and takes effect through its override alone.
287
- // The first pin records whether the manifest held an overrides block, so a restore keeps one that
288
- // existed and removes one the pin created.
289
- function planPin(manifest, ledger, tarballs, overrides, at) {
590
+ /**
591
+ * Plans a pin.
592
+ *
593
+ * @remarks
594
+ * The replaced range is kept from the first pin while the manifest still holds a tarball spec, and a
595
+ * package no field declares is recorded without a field and takes effect through its override alone.
596
+ * The first pin records whether the manifest held an overrides block, so a restore keeps one that
597
+ * existed and removes one the pin created.
598
+ *
599
+ * @param manifest - The consumer manifest.
600
+ * @param ledger - The ledger before the pin.
601
+ * @param tarballs - The packed or planned sources.
602
+ * @param overrides - The names to override.
603
+ * @param at - The time of the pin.
604
+ * @returns The manifest and ledger to write and the changes between the manifests.
605
+ */
606
+ export function planPin(manifest, ledger, tarballs, overrides, at) {
290
607
  const rows = tarballs.map((tarball) => ({
291
608
  name: tarball.source.name,
292
609
  spec: tarball.spec,
@@ -322,8 +639,18 @@ function planPin(manifest, ledger, tarballs, overrides, at) {
322
639
  changes: diffManifests(manifest, next),
323
640
  };
324
641
  }
325
- // An empty name list restores every recorded package and override.
326
- function planRestore(manifest, ledger, names) {
642
+ /**
643
+ * Plans a restore.
644
+ *
645
+ * @remarks
646
+ * An empty name list restores every recorded package and override.
647
+ *
648
+ * @param manifest - The consumer manifest.
649
+ * @param ledger - The ledger before the restore.
650
+ * @param names - The packages to restore, or none for every one.
651
+ * @returns The manifest and ledger to write, the restored rows, and the overrides dropped.
652
+ */
653
+ export function planRestore(manifest, ledger, names) {
327
654
  const targets = names.length === 0 ? Object.keys(ledger.packages) : names;
328
655
  const selected = new Set(targets);
329
656
  const rows = targets.flatMap((name) => {
@@ -353,7 +680,17 @@ function planRestore(manifest, ledger, names) {
353
680
  dropped,
354
681
  };
355
682
  }
356
- function scanPinRequest(text, manifest, ledger, sources, overrides) {
683
+ /**
684
+ * Lists the problems that refuse a pin request.
685
+ *
686
+ * @param text - The manifest file's text.
687
+ * @param manifest - The parsed manifest.
688
+ * @param ledger - The ledger before the pin.
689
+ * @param sources - The sources to pin.
690
+ * @param overrides - The names to override.
691
+ * @returns One message per problem, empty when the request is usable.
692
+ */
693
+ export function scanPinRequest(text, manifest, ledger, sources, overrides) {
357
694
  const names = sources.map((source) => source.name);
358
695
  const current = readBlock(manifest, 'overrides');
359
696
  return [
@@ -386,7 +723,14 @@ function scanPinRequest(text, manifest, ledger, sources, overrides) {
386
723
  ]),
387
724
  ];
388
725
  }
389
- function parseVersion(text) {
726
+ /**
727
+ * Parses a full semantic version.
728
+ *
729
+ * @param text - A version such as `1.2.3`, `v1.2.3-rc.1`, or `1.2.3+build`, with surrounding
730
+ * whitespace ignored.
731
+ * @returns The version, or undefined when the text is not a full version. Build metadata is dropped.
732
+ */
733
+ export function parseVersion(text) {
390
734
  const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(text.trim());
391
735
  if (match === null)
392
736
  return undefined;
@@ -397,7 +741,16 @@ function parseVersion(text) {
397
741
  pre: match[4],
398
742
  };
399
743
  }
400
- function comparePrerelease(left, right) {
744
+ /**
745
+ * Orders two prerelease identifier lists.
746
+ *
747
+ * @param left - One prerelease text, such as `rc.1`.
748
+ * @param right - The other.
749
+ * @returns A negative number, 0, or a positive number as the left sorts before, equal to, or after
750
+ * the right: numeric identifiers compare as numbers and sort before alphanumeric ones, and a longer
751
+ * list sorts after its prefix.
752
+ */
753
+ export function comparePrerelease(left, right) {
401
754
  const leftParts = left.split('.');
402
755
  const rightParts = right.split('.');
403
756
  const first = leftParts
@@ -416,7 +769,15 @@ function comparePrerelease(left, right) {
416
769
  .find((order) => order !== 0);
417
770
  return first ?? leftParts.length - rightParts.length;
418
771
  }
419
- function compareVersions(left, right) {
772
+ /**
773
+ * Orders two versions.
774
+ *
775
+ * @param left - One version.
776
+ * @param right - The other.
777
+ * @returns A negative number, 0, or a positive number as the left sorts before, equal to, or after
778
+ * the right; a release sorts after its prerelease.
779
+ */
780
+ export function compareVersions(left, right) {
420
781
  const core = [left.major - right.major, left.minor - right.minor, left.patch - right.patch].find((order) => order !== 0);
421
782
  if (core !== undefined)
422
783
  return core;
@@ -428,9 +789,17 @@ function compareVersions(left, right) {
428
789
  return -1;
429
790
  return comparePrerelease(left.pre, right.pre);
430
791
  }
431
- // Reads `*`, `x`, an exact version, `^`, `~`, and the `>=`, `>`, `<=`, `<`, and `=` operators on full
432
- // versions; an empty list matches every version.
433
- function parseComparators(token) {
792
+ /**
793
+ * Parses one token of a range into comparators.
794
+ *
795
+ * @remarks
796
+ * Reads `*`, `x`, an exact version, `^`, `~`, and the `>=`, `>`, `<=`, `<`, and `=` operators on full
797
+ * versions; an empty list matches every version.
798
+ *
799
+ * @param token - One whitespace-free token of a range.
800
+ * @returns The comparators the token means, or undefined when the token is not in the subset.
801
+ */
802
+ export function parseComparators(token) {
434
803
  if (token === '*' || token === 'x' || token === 'X' || token === '')
435
804
  return [];
436
805
  const operator = /^(\^|~|>=|<=|>|<|=)?(.+)$/u.exec(token);
@@ -463,7 +832,14 @@ function parseComparators(token) {
463
832
  }
464
833
  return [{ operator: '=', version }];
465
834
  }
466
- function matchesComparator(version, comparator) {
835
+ /**
836
+ * Checks whether a version satisfies one comparator.
837
+ *
838
+ * @param version - The version.
839
+ * @param comparator - The comparator.
840
+ * @returns True if the version stands in the comparator's relation to its bound; false otherwise.
841
+ */
842
+ export function matchesComparator(version, comparator) {
467
843
  const order = compareVersions(version, comparator.version);
468
844
  if (comparator.operator === '>=')
469
845
  return order >= 0;
@@ -475,15 +851,32 @@ function matchesComparator(version, comparator) {
475
851
  return order < 0;
476
852
  return order === 0;
477
853
  }
478
- function supportsRange(range) {
854
+ /**
855
+ * Checks whether the script reads every token of a range.
856
+ *
857
+ * @param range - A registry range.
858
+ * @returns True if every token of every `||` set is in the subset {@link parseComparators} reads;
859
+ * false otherwise.
860
+ */
861
+ export function supportsRange(range) {
479
862
  return range.split('||').every((set) => set
480
863
  .trim()
481
864
  .split(/\s+/u)
482
865
  .every((token) => parseComparators(token) !== undefined));
483
866
  }
484
- // A prerelease satisfies a set only when one of its comparators names a prerelease of the same
485
- // `major.minor.patch`, and an unreadable range is satisfied by nothing.
486
- function matchesRange(text, range) {
867
+ /**
868
+ * Checks whether a version satisfies a range.
869
+ *
870
+ * @remarks
871
+ * A prerelease satisfies a set only when one of its comparators names a prerelease of the same
872
+ * `major.minor.patch`, and an unreadable range is satisfied by nothing.
873
+ *
874
+ * @param text - The version.
875
+ * @param range - A registry range.
876
+ * @returns True if the version parses and satisfies every comparator of at least one `||` set;
877
+ * false otherwise.
878
+ */
879
+ export function matchesRange(text, range) {
487
880
  const version = parseVersion(text);
488
881
  if (version === undefined)
489
882
  return false;
@@ -503,7 +896,26 @@ function matchesRange(text, range) {
503
896
  return admitted && comparators.every((comparator) => matchesComparator(version, comparator));
504
897
  });
505
898
  }
506
- function parseCommand(argv) {
899
+ /**
900
+ * Parses the command line.
901
+ *
902
+ * @param argv - The arguments after the script path.
903
+ * @returns The command, or the reason the line is refused: no mode or two modes, a flag the mode does
904
+ * not read (the `--flag=value` form included), a flag with no value, no consumer, or a pin with no
905
+ * source.
906
+ */
907
+ export function parseCommand(argv) {
908
+ const known = MODES.flatMap((candidate) => [
909
+ `--${candidate}`,
910
+ ...VALUE_FLAGS[candidate],
911
+ ...SWITCH_FLAGS[candidate],
912
+ ]);
913
+ // A bare `--` ends the arguments the script reads; nothing after it is a flag of this script.
914
+ const end = argv.indexOf('--');
915
+ const foreign = (end === -1 ? argv : argv.slice(0, end)).filter((token) => token.startsWith('--') && !known.includes(token));
916
+ if (foreign.length > 0) {
917
+ return { ok: false, message: `${foreign.join(', ')} is not a flag this script reads` };
918
+ }
507
919
  const given = MODES.filter((candidate) => argv.includes(`--${candidate}`));
508
920
  const mode = MODES.find((candidate) => argv.includes(`--${candidate}`));
509
921
  if (mode === undefined)
@@ -547,20 +959,47 @@ function parseCommand(argv) {
547
959
  },
548
960
  };
549
961
  }
550
- function computeHash(value) {
962
+ /**
963
+ * Computes the SHA-256 digest of bytes.
964
+ *
965
+ * @param value - The bytes.
966
+ * @returns The digest as lowercase hexadecimal.
967
+ */
968
+ export function computeHash(value) {
551
969
  return createHash('sha256').update(value).digest('hex');
552
970
  }
553
- function loadJSONRecord(path) {
971
+ /**
972
+ * Loads a JSON file as a record.
973
+ *
974
+ * @param path - The file.
975
+ * @returns The record, or undefined when the file is absent or not a JSON object.
976
+ */
977
+ export function loadJSONRecord(path) {
554
978
  return existsSync(path) ? parseJsonRecord(readFileSync(path, 'utf8')) : undefined;
555
979
  }
556
- function loadSource(directory) {
980
+ /**
981
+ * Loads a source package's name and version.
982
+ *
983
+ * @param directory - The source directory.
984
+ * @returns The source, or undefined when its manifest lacks a name or a version.
985
+ */
986
+ export function loadSource(directory) {
557
987
  const manifest = loadJSONRecord(join(directory, 'package.json'));
558
988
  const name = readString(manifest, 'name');
559
989
  const version = readString(manifest, 'version');
560
990
  return name === undefined || version === undefined ? undefined : { directory, name, version };
561
991
  }
562
- // npm 11 prints an array and npm 12 an object keyed by package name.
563
- function readPack(text) {
992
+ /**
993
+ * Reads the archive name and file list out of `npm pack --json` output.
994
+ *
995
+ * @remarks
996
+ * npm 11 prints an array and npm 12 an object keyed by package name.
997
+ *
998
+ * @param text - npm's standard output.
999
+ * @returns The archive's file name and its sorted, forward-slash file paths.
1000
+ * @throws Thrown when the output is not JSON or lacks a file name or a file list.
1001
+ */
1002
+ export function readPack(text) {
564
1003
  const parsed = JSON.parse(text);
565
1004
  const entry = Array.isArray(parsed)
566
1005
  ? parsed[0]
@@ -578,7 +1017,15 @@ function readPack(text) {
578
1017
  });
579
1018
  return { filename: entry.filename, files: files.toSorted() };
580
1019
  }
581
- function invokeNpm(args, cwd) {
1020
+ /**
1021
+ * Runs npm and returns its standard output.
1022
+ *
1023
+ * @param args - The npm arguments.
1024
+ * @param cwd - The working directory.
1025
+ * @returns npm's standard output.
1026
+ * @throws Thrown when npm cannot start or exits nonzero, naming the command and the stderr tail.
1027
+ */
1028
+ export function invokeNpm(args, cwd) {
582
1029
  const result = runNpm(args, cwd);
583
1030
  if (result.error !== undefined) {
584
1031
  throw new Error(`npm ${args.join(' ')} could not start in ${cwd}: ${result.error.message}`);
@@ -588,7 +1035,13 @@ function invokeNpm(args, cwd) {
588
1035
  }
589
1036
  return result.stdout;
590
1037
  }
591
- function inspectCheckout(directory) {
1038
+ /**
1039
+ * Reads a checkout's commit and whether it is dirty.
1040
+ *
1041
+ * @param directory - The checkout.
1042
+ * @returns The HEAD commit and the dirty flag, each undefined when git cannot answer.
1043
+ */
1044
+ export function inspectCheckout(directory) {
592
1045
  const head = spawnSync('git', ['rev-parse', 'HEAD'], {
593
1046
  cwd: directory,
594
1047
  encoding: 'utf8',
@@ -604,18 +1057,44 @@ function inspectCheckout(directory) {
604
1057
  dirty: status.status === 0 ? status.stdout.trim() !== '' : undefined,
605
1058
  };
606
1059
  }
607
- function replaceFile(path, text) {
1060
+ /**
1061
+ * Replaces a file by renaming a temporary file onto it.
1062
+ *
1063
+ * @param path - The file.
1064
+ * @param text - Its new text.
1065
+ * @returns Nothing after the rename.
1066
+ * @throws Thrown when the write or the rename fails.
1067
+ */
1068
+ export function replaceFile(path, text) {
608
1069
  const temporary = `${path}.${String(process.pid)}.tmp`;
609
1070
  writeFileSync(temporary, text);
610
1071
  renameSync(temporary, path);
611
1072
  }
612
- // Vite keys its pre-bundle on the installed version, so a same-version tarball would serve stale bytes.
613
- function installConsumer(consumer) {
1073
+ /**
1074
+ * Installs a consumer's dependencies and clears the Vite pre-bundle.
1075
+ *
1076
+ * @remarks
1077
+ * Vite keys its pre-bundle on the installed version, so a same-version tarball would serve stale bytes.
1078
+ *
1079
+ * @param consumer - The consumer directory.
1080
+ * @returns Nothing after the install.
1081
+ * @throws Thrown when the install fails.
1082
+ */
1083
+ export function installConsumer(consumer) {
614
1084
  invokeNpm(['install', '--ignore-scripts', '--no-audit', '--no-fund'], consumer);
615
1085
  removeTree(join(consumer, 'node_modules', '.vite'));
616
1086
  }
617
- // The package's own `node_modules` is skipped, and a link is listed as `path (link)`.
618
- function collectFiles(directory, prefix = '') {
1087
+ /**
1088
+ * Lists the files of an installed package.
1089
+ *
1090
+ * @remarks
1091
+ * The package's own `node_modules` is skipped, and a link is listed as `path (link)`.
1092
+ *
1093
+ * @param directory - The package directory.
1094
+ * @param prefix - The path of `directory` inside the package. Default: none.
1095
+ * @returns The sorted forward-slash paths.
1096
+ */
1097
+ export function collectFiles(directory, prefix = '') {
619
1098
  return readdirSync(directory, { withFileTypes: true })
620
1099
  .flatMap((entry) => {
621
1100
  const path = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
@@ -630,14 +1109,27 @@ function collectFiles(directory, prefix = '') {
630
1109
  })
631
1110
  .toSorted();
632
1111
  }
633
- function computeInventory(directory) {
1112
+ /**
1113
+ * Computes the digest of an installed package's bytes.
1114
+ *
1115
+ * @param directory - The package directory.
1116
+ * @returns The SHA-256 of the sorted path and file-digest lines.
1117
+ */
1118
+ export function computeInventory(directory) {
634
1119
  const lines = collectFiles(directory).map((path) => {
635
1120
  const file = join(directory, ...path.split('/'));
636
1121
  return `${path}\t${existsSync(file) ? computeHash(readFileSync(file)) : 'unreadable'}`;
637
1122
  });
638
1123
  return computeHash(Buffer.from(lines.join('\n')));
639
1124
  }
640
- function computeScope(consumer, excluded) {
1125
+ /**
1126
+ * Computes the digest of every top-level `@orkestrel` package the pin does not name.
1127
+ *
1128
+ * @param consumer - The consumer directory.
1129
+ * @param excluded - The package names to leave out.
1130
+ * @returns The digest of each package by name, `link` for a link, empty when the scope is absent.
1131
+ */
1132
+ export function computeScope(consumer, excluded) {
641
1133
  const scope = join(consumer, 'node_modules', '@orkestrel');
642
1134
  if (!existsSync(scope))
643
1135
  return {};
@@ -649,7 +1141,14 @@ function computeScope(consumer, excluded) {
649
1141
  return [[name, lstatSync(path).isSymbolicLink() ? 'link' : computeInventory(path)]];
650
1142
  }));
651
1143
  }
652
- function describeMoved(before, after) {
1144
+ /**
1145
+ * Names the packages an install added, removed, or changed.
1146
+ *
1147
+ * @param before - The scope digests before the install.
1148
+ * @param after - The scope digests after.
1149
+ * @returns One message per package whose digest differs.
1150
+ */
1151
+ export function describeMoved(before, after) {
653
1152
  return [...new Set([...Object.keys(before), ...Object.keys(after)])].flatMap((name) => {
654
1153
  const was = before[name];
655
1154
  const now = after[name];
@@ -659,8 +1158,17 @@ function describeMoved(before, after) {
659
1158
  return [`${name} was ${verb} by the install`];
660
1159
  });
661
1160
  }
662
- // A link is reported and not entered.
663
- function scanModules(directory, names) {
1161
+ /**
1162
+ * Finds every installed copy of the named packages.
1163
+ *
1164
+ * @remarks
1165
+ * A link is reported and not entered.
1166
+ *
1167
+ * @param directory - A `node_modules` directory.
1168
+ * @param names - The package names to find.
1169
+ * @returns Each copy at any depth of nested `node_modules`.
1170
+ */
1171
+ export function scanModules(directory, names) {
664
1172
  if (!existsSync(directory))
665
1173
  return [];
666
1174
  return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
@@ -690,13 +1198,31 @@ function scanModules(directory, names) {
690
1198
  });
691
1199
  });
692
1200
  }
693
- function locateInstalled(consumer, name) {
1201
+ /**
1202
+ * Locates a package's top-level install path.
1203
+ *
1204
+ * @param consumer - The consumer directory.
1205
+ * @param name - The package name.
1206
+ * @returns The path under the consumer's `node_modules`.
1207
+ */
1208
+ export function locateInstalled(consumer, name) {
694
1209
  return join(consumer, 'node_modules', ...name.split('/'));
695
1210
  }
696
- // A source that is itself pinned was packed with its registry ranges, so its installed manifest is
697
- // compared to those bytes rather than to the source's own manifest. Every pinned source must be
698
- // installed at the top level, so an override that selects no installed dependency fails the proof.
699
- function proveInstalled(consumer, tarball, declared, copies) {
1211
+ /**
1212
+ * Lists the problems with one pinned package's install.
1213
+ *
1214
+ * @remarks
1215
+ * A source that is itself pinned was packed with its registry ranges, so its installed manifest is
1216
+ * compared to those bytes rather than to the source's own manifest. Every pinned source must be
1217
+ * installed at the top level, so an override that selects no installed dependency fails the proof.
1218
+ *
1219
+ * @param consumer - The consumer directory.
1220
+ * @param tarball - The packed source.
1221
+ * @param declared - If `true`, the consumer declares the package; if `false`, only an override names it.
1222
+ * @param copies - Every installed copy of the pinned packages.
1223
+ * @returns One message per problem, empty when the install proves the pin.
1224
+ */
1225
+ export function proveInstalled(consumer, tarball, declared, copies) {
700
1226
  const name = tarball.source.name;
701
1227
  const top = locateInstalled(consumer, name);
702
1228
  const named = copies.filter((copy) => copy.name === name);
@@ -728,15 +1254,32 @@ function proveInstalled(consumer, tarball, declared, copies) {
728
1254
  ...nested.map((copy) => `${name} has another copy at ${normalizeSlashes(relative(consumer, copy.path))} (version ${copy.version ?? 'unknown'}); add an --override`),
729
1255
  ];
730
1256
  }
731
- function loadResolved(consumer, name) {
1257
+ /**
1258
+ * Reads the `resolved` address the lockfile records for a package.
1259
+ *
1260
+ * @param consumer - The consumer directory.
1261
+ * @param name - The package name.
1262
+ * @returns The address, or undefined when the lockfile is absent or records none.
1263
+ */
1264
+ export function loadResolved(consumer, name) {
732
1265
  const packages = loadJSONRecord(join(consumer, 'package-lock.json'))?.packages;
733
1266
  const entry = isRecord(packages) ? packages[`node_modules/${name}`] : undefined;
734
1267
  return readString(isRecord(entry) ? entry : undefined, 'resolved');
735
1268
  }
736
- // A registry range needs a real directory whose version the range accepts and a lockfile `resolved`
737
- // that is a registry URL; a `file:` directory range needs a link to that directory or a directory of
738
- // its version; any other spec is reported as unproven.
739
- function proveRestored(consumer, name, range) {
1269
+ /**
1270
+ * Lists the problems with one restored package's install.
1271
+ *
1272
+ * @remarks
1273
+ * A registry range needs a real directory whose version the range accepts and a lockfile `resolved`
1274
+ * that is a registry URL; a `file:` directory range needs a link to that directory or a directory of
1275
+ * its version; any other spec is reported as unproven.
1276
+ *
1277
+ * @param consumer - The consumer directory.
1278
+ * @param name - The package name.
1279
+ * @param range - The restored range.
1280
+ * @returns One message per problem, empty when the install proves the restore.
1281
+ */
1282
+ export function proveRestored(consumer, name, range) {
740
1283
  const installed = locateInstalled(consumer, name);
741
1284
  if (!existsSync(installed))
742
1285
  return [`${name} is not installed`];
@@ -763,65 +1306,152 @@ function proveRestored(consumer, name, range) {
763
1306
  : []),
764
1307
  ];
765
1308
  }
766
- function locateLedger(consumer) {
1309
+ /**
1310
+ * Locates a consumer's ledger file.
1311
+ *
1312
+ * @param consumer - The consumer directory.
1313
+ * @returns The ledger's path.
1314
+ */
1315
+ export function locateLedger(consumer) {
767
1316
  return join(consumer, ...TARBALL_DIRECTORY.split('/'), LEDGER_FILE);
768
1317
  }
769
- // An absent ledger is an empty one; a malformed ledger is undefined.
770
- function loadLedger(consumer) {
1318
+ /**
1319
+ * Loads a consumer's ledger.
1320
+ *
1321
+ * @remarks
1322
+ * An absent ledger is an empty one; a malformed ledger is undefined.
1323
+ *
1324
+ * @param consumer - The consumer directory.
1325
+ * @returns The ledger, or undefined when the file is malformed.
1326
+ */
1327
+ export function loadLedger(consumer) {
771
1328
  const path = locateLedger(consumer);
772
1329
  if (!existsSync(path))
773
1330
  return { packages: {}, overrides: {} };
774
1331
  return parseLedger(loadJSONRecord(path));
775
1332
  }
776
- function readLedger(consumer) {
1333
+ /**
1334
+ * Reads a consumer's ledger.
1335
+ *
1336
+ * @param consumer - The consumer directory.
1337
+ * @returns The ledger; an absent file reads as an empty one.
1338
+ * @throws Thrown when the ledger file is malformed.
1339
+ */
1340
+ export function readLedger(consumer) {
777
1341
  const ledger = loadLedger(consumer);
778
1342
  if (ledger === undefined)
779
1343
  throw new Error(`${normalizeSlashes(locateLedger(consumer))} is malformed`);
780
1344
  return ledger;
781
1345
  }
782
- function renderStamp(at) {
1346
+ /**
1347
+ * Renders a time as a file-name stamp.
1348
+ *
1349
+ * @param at - An ISO time.
1350
+ * @returns The time with each colon and dot replaced by a dash.
1351
+ */
1352
+ export function renderStamp(at) {
783
1353
  return at.replace(/[:.]/gu, '-');
784
1354
  }
785
- function writeReceipt(consumer, at, receipt) {
1355
+ /**
1356
+ * Writes a receipt create-only under the working directory.
1357
+ *
1358
+ * @param consumer - The consumer directory, whose name the file carries.
1359
+ * @param at - The time of the run.
1360
+ * @param receipt - The receipt record.
1361
+ * @returns The receipt's forward-slash path.
1362
+ * @throws Thrown when the file already exists or cannot be written.
1363
+ */
1364
+ export function writeReceipt(consumer, at, receipt) {
786
1365
  mkdirSync(RECEIPT_DIRECTORY, { recursive: true });
787
1366
  const path = join(RECEIPT_DIRECTORY, `swap-${basename(consumer)}-${renderStamp(at)}.json`);
788
1367
  writeFileSync(path, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx' });
789
1368
  return normalizeSlashes(path);
790
1369
  }
791
- function printReport(json, lines, report) {
1370
+ /**
1371
+ * Prints a report.
1372
+ *
1373
+ * @param json - If `true`, prints the report as one JSON line; if `false`, prints the text lines.
1374
+ * @param lines - The text lines.
1375
+ * @param report - The JSON report.
1376
+ * @returns Nothing after the write.
1377
+ */
1378
+ export function printReport(json, lines, report) {
792
1379
  process.stdout.write(json ? `${JSON.stringify(report)}\n` : `${lines.join('\n')}\n`);
793
1380
  }
794
- function reportUsage(message) {
1381
+ /**
1382
+ * Prints a usage refusal.
1383
+ *
1384
+ * @param message - What was refused.
1385
+ * @returns The usage exit code.
1386
+ */
1387
+ export function reportUsage(message) {
795
1388
  process.stderr.write(`swap: ${message}\n${USAGE}\n`);
796
1389
  return EXIT_USAGE;
797
1390
  }
798
- function hasPins(ledger) {
1391
+ /**
1392
+ * Checks whether a ledger records any pin.
1393
+ *
1394
+ * @param ledger - The ledger.
1395
+ * @returns True if it holds a package or an override row; false otherwise.
1396
+ */
1397
+ export function hasPins(ledger) {
799
1398
  return Object.keys(ledger.packages).length > 0 || Object.keys(ledger.overrides).length > 0;
800
1399
  }
801
- // The manifest a source is packed with: its own, with the ranges and overrides its ledger replaced
802
- // put back when it carries pins.
803
- function loadPackedManifest(directory) {
1400
+ /**
1401
+ * Loads the manifest a source is packed with.
1402
+ *
1403
+ * @remarks
1404
+ * It is the source's own, with the ranges and overrides its ledger replaced put back when it carries
1405
+ * pins.
1406
+ *
1407
+ * @param directory - The source directory.
1408
+ * @returns The manifest to pack.
1409
+ * @throws Thrown when the manifest is unreadable or the ledger is malformed.
1410
+ */
1411
+ export function loadPackedManifest(directory) {
804
1412
  const manifest = loadJSONRecord(join(directory, 'package.json'));
805
1413
  if (manifest === undefined)
806
1414
  throw new Error(`${directory}/package.json is unreadable`);
807
1415
  const ledger = readLedger(directory);
808
1416
  return hasPins(ledger) ? planRestore(manifest, ledger, []).manifest : manifest;
809
1417
  }
810
- // The text a pinned source's manifest is rewritten to for the pack; undefined when the source carries
811
- // no pins and packs its own manifest.
812
- function readPackedManifest(directory) {
1418
+ /**
1419
+ * Reads the text a pinned source's manifest is rewritten to for the pack.
1420
+ *
1421
+ * @param directory - The source directory.
1422
+ * @returns The text, or undefined when the source carries no pins and packs its own manifest.
1423
+ */
1424
+ export function readPackedManifest(directory) {
813
1425
  return hasPins(readLedger(directory)) ? serializeJSON(loadPackedManifest(directory)) : undefined;
814
1426
  }
815
- // A source that packs a local dependency spec ships a tarball that resolves only on this host, whether
816
- // or not a ledger recorded the spec.
817
- function scanPackedSpecs(source) {
1427
+ /**
1428
+ * Lists the local dependency specs a source would pack.
1429
+ *
1430
+ * @remarks
1431
+ * A source that packs a local dependency spec ships a tarball that resolves only on this host,
1432
+ * whether or not a ledger recorded the spec.
1433
+ *
1434
+ * @param source - The source.
1435
+ * @returns One message per local spec in the manifest the source packs.
1436
+ */
1437
+ export function scanPackedSpecs(source) {
818
1438
  const manifest = loadPackedManifest(source.directory);
819
1439
  return scanLocalSpecs(manifest).map(([path, spec]) => `${source.name} would pack a local dependency spec: ${path} is ${spec}`);
820
1440
  }
821
- // A source that carries a ledger of its own has its manifest rewritten to the recorded ranges for the
822
- // pack and restored byte for byte whether the pack succeeds or throws, so a consumer never receives a
823
- // tarball spec that resolves only inside the source.
824
- function packSource(directory, scratch) {
1441
+ /**
1442
+ * Packs a source.
1443
+ *
1444
+ * @remarks
1445
+ * A source that carries a ledger of its own has its manifest rewritten to the recorded ranges for
1446
+ * the pack and restored byte for byte whether the pack succeeds or throws, so a consumer never
1447
+ * receives a tarball spec that resolves only inside the source.
1448
+ *
1449
+ * @param directory - The source directory.
1450
+ * @param scratch - The directory that receives the archive.
1451
+ * @returns npm's `pack --json` output.
1452
+ * @throws Thrown when the pack fails.
1453
+ */
1454
+ export function packSource(directory, scratch) {
825
1455
  const args = ['pack', '--ignore-scripts', '--json', '--pack-destination', scratch];
826
1456
  const packed = readPackedManifest(directory);
827
1457
  if (packed === undefined)
@@ -836,7 +1466,16 @@ function packSource(directory, scratch) {
836
1466
  writeFileSync(path, original);
837
1467
  }
838
1468
  }
839
- function packSources(sources, build, consumer) {
1469
+ /**
1470
+ * Builds and packs each source into the consumer's tarball directory.
1471
+ *
1472
+ * @param sources - The sources.
1473
+ * @param build - If `true`, runs `npm run build` in each source first; if `false`, packs it as is.
1474
+ * @param consumer - The consumer directory.
1475
+ * @returns One packed tarball per source.
1476
+ * @throws Thrown when a build or a pack fails.
1477
+ */
1478
+ export function packSources(sources, build, consumer) {
840
1479
  const tarballs = join(consumer, ...TARBALL_DIRECTORY.split('/'));
841
1480
  mkdirSync(tarballs, { recursive: true });
842
1481
  const scratch = mkdtempSync(join(tarballs, '.scratch-'));
@@ -863,7 +1502,14 @@ function packSources(sources, build, consumer) {
863
1502
  removeTree(scratch);
864
1503
  }
865
1504
  }
866
- function planTarballs(sources, consumer) {
1505
+ /**
1506
+ * Plans the tarballs of a dry run without building or packing.
1507
+ *
1508
+ * @param sources - The sources.
1509
+ * @param consumer - The consumer directory.
1510
+ * @returns One planned tarball per source, with a pending hash and no files.
1511
+ */
1512
+ export function planTarballs(sources, consumer) {
867
1513
  return sources.map((source) => {
868
1514
  const file = buildTarballName(source.name, source.version, '<sha256:12>');
869
1515
  return {
@@ -876,11 +1522,27 @@ function planTarballs(sources, consumer) {
876
1522
  };
877
1523
  });
878
1524
  }
879
- function renderChange(change) {
1525
+ /**
1526
+ * Renders a manifest change as a text line.
1527
+ *
1528
+ * @param change - The change.
1529
+ * @returns The line, naming the block, the package, and both specs.
1530
+ */
1531
+ export function renderChange(change) {
880
1532
  return `swap: ${change.block}.${change.name} ${change.from ?? '(absent)'} -> ${change.to ?? '(absent)'}`;
881
1533
  }
882
- // A proved pin records the digest of its installed bytes, which `--status` compares the install to.
883
- function recordInventory(consumer, ledger, tarballs) {
1534
+ /**
1535
+ * Records the digest of each proved install in the ledger.
1536
+ *
1537
+ * @remarks
1538
+ * A proved pin records the digest of its installed bytes, which `--status` compares the install to.
1539
+ *
1540
+ * @param consumer - The consumer directory.
1541
+ * @param ledger - The ledger.
1542
+ * @param tarballs - The pinned tarballs.
1543
+ * @returns The ledger with an inventory on each package that is installed as a real directory.
1544
+ */
1545
+ export function recordInventory(consumer, ledger, tarballs) {
884
1546
  const proved = new Map(tarballs.flatMap((tarball) => {
885
1547
  const top = locateInstalled(consumer, tarball.source.name);
886
1548
  return existsSync(top) && !lstatSync(top).isSymbolicLink()
@@ -895,7 +1557,14 @@ function recordInventory(consumer, ledger, tarballs) {
895
1557
  })),
896
1558
  };
897
1559
  }
898
- function executePin(command) {
1560
+ /**
1561
+ * Runs the pin mode.
1562
+ *
1563
+ * @param command - The parsed command.
1564
+ * @returns 0 when the pin installed and proved, 1 when a proof failed, 64 on a usage refusal.
1565
+ * @throws Thrown when a build, a pack, or the install fails.
1566
+ */
1567
+ export function executePin(command) {
899
1568
  const manifestPath = join(command.consumer, 'package.json');
900
1569
  if (!existsSync(manifestPath))
901
1570
  return reportUsage(`${command.consumer} has no package.json`);
@@ -978,9 +1647,18 @@ function executePin(command) {
978
1647
  ], { ...receipt, receipt: path });
979
1648
  return failures.length === 0 ? 0 : EXIT_FAILURE;
980
1649
  }
981
- // The re-pin arguments name every source the restored rows recorded and every override they
982
- // dropped, so the pins can come back after a verb that needed the registry ranges.
983
- function buildRepin(consumer, plan) {
1650
+ /**
1651
+ * Builds the arguments that pin again what a restore undid.
1652
+ *
1653
+ * @remarks
1654
+ * The re-pin arguments name every source the restored rows recorded and every override they
1655
+ * dropped, so the pins can come back after a verb that needed the registry ranges.
1656
+ *
1657
+ * @param consumer - The consumer directory.
1658
+ * @param plan - The restore plan.
1659
+ * @returns The `--pin` argument list.
1660
+ */
1661
+ export function buildRepin(consumer, plan) {
984
1662
  return [
985
1663
  '--pin',
986
1664
  '--consumer',
@@ -989,10 +1667,19 @@ function buildRepin(consumer, plan) {
989
1667
  ...plan.dropped.flatMap((name) => ['--override', name]),
990
1668
  ];
991
1669
  }
992
- // npm keeps a lock entry whose version satisfies the restored range, so the tarball `resolved` of a
993
- // pinned package outlives the restore; removing the entry and the installed copy makes the install
994
- // resolve the package again from the registry.
995
- function removePinnedInstalls(consumer, names) {
1670
+ /**
1671
+ * Removes the lock entries and installed copies of the named packages.
1672
+ *
1673
+ * @remarks
1674
+ * npm keeps a lock entry whose version satisfies the restored range, so the tarball `resolved` of a
1675
+ * pinned package outlives the restore; removing the entry and the installed copy makes the install
1676
+ * resolve the package again from the registry.
1677
+ *
1678
+ * @param consumer - The consumer directory.
1679
+ * @param names - The package names.
1680
+ * @returns Nothing after the removal.
1681
+ */
1682
+ export function removePinnedInstalls(consumer, names) {
996
1683
  const path = join(consumer, 'package-lock.json');
997
1684
  const text = existsSync(path) ? readFileSync(path, 'utf8') : undefined;
998
1685
  const lock = text === undefined ? undefined : parseJsonRecord(text);
@@ -1004,9 +1691,13 @@ function removePinnedInstalls(consumer, names) {
1004
1691
  for (const name of names)
1005
1692
  removeTree(locateInstalled(consumer, name));
1006
1693
  }
1007
- // The packages whose lock entry and installed copy the restore removes: each restored row that
1008
- // returns to a registry range, and each dropped override.
1009
- function listRemovals(plan) {
1694
+ /**
1695
+ * Lists the packages whose lock entry and installed copy a restore removes.
1696
+ *
1697
+ * @param plan - The restore plan.
1698
+ * @returns Each restored row that returns to a registry range, and each dropped override.
1699
+ */
1700
+ export function listRemovals(plan) {
1010
1701
  return [
1011
1702
  ...new Set([
1012
1703
  ...plan.rows.flatMap((row) => row.entry.field === undefined ||
@@ -1018,33 +1709,72 @@ function listRemovals(plan) {
1018
1709
  ]),
1019
1710
  ];
1020
1711
  }
1021
- // Every ledger key becomes a manifest key, a lock key, and an install path, so each must be a package
1022
- // name before the restore writes anything.
1023
- function scanLedgerNames(ledger) {
1712
+ /**
1713
+ * Lists the ledger keys that are not package names.
1714
+ *
1715
+ * @remarks
1716
+ * Every ledger key becomes a manifest key, a lock key, and an install path, so each must be a package
1717
+ * name before the restore writes anything.
1718
+ *
1719
+ * @param ledger - The ledger.
1720
+ * @returns One message per package or override key that is not a package name.
1721
+ */
1722
+ export function scanLedgerNames(ledger) {
1024
1723
  return [
1025
1724
  ...scanNames('packages', Object.keys(ledger.packages)),
1026
1725
  ...scanNames('overrides', Object.keys(ledger.overrides)),
1027
1726
  ];
1028
1727
  }
1029
- function scanNames(block, names) {
1728
+ /**
1729
+ * Lists the keys of one ledger block that are not package names.
1730
+ *
1731
+ * @param block - The block's name.
1732
+ * @param names - The block's keys.
1733
+ * @returns One message per key that is not a package name, with the key JSON-encoded.
1734
+ */
1735
+ export function scanNames(block, names) {
1030
1736
  return names
1031
1737
  .filter((name) => !isPackageName(name))
1032
1738
  .map((name) => `The ledger ${block} key ${JSON.stringify(name)} is not a package name`);
1033
1739
  }
1034
- // A name that passes the package-name rules still resolves outside `node_modules` when a scope
1035
- // directory on its way is a link, so the target's real parent is compared to the real `node_modules`.
1036
- function isContained(root, target) {
1740
+ /**
1741
+ * Checks whether a target stays inside a root.
1742
+ *
1743
+ * @remarks
1744
+ * A name that passes the package-name rules still resolves outside `node_modules` when a scope
1745
+ * directory on its way is a link, so the target's real parent is compared to the real `node_modules`.
1746
+ *
1747
+ * @param root - The `node_modules` directory.
1748
+ * @param target - The path to remove.
1749
+ * @returns True if the target's real location is strictly inside the root's real location; false
1750
+ * otherwise.
1751
+ */
1752
+ export function isContained(root, target) {
1037
1753
  const parent = dirname(target);
1038
1754
  const inside = relative(existsSync(root) ? realpathSync(root) : resolve(root), join(existsSync(parent) ? realpathSync(parent) : resolve(parent), basename(target)));
1039
1755
  return inside !== '' && !isAbsolute(inside) && inside.split(/[\\/]/u)[0] !== '..';
1040
1756
  }
1041
- function scanRemovals(consumer, names) {
1757
+ /**
1758
+ * Lists the removals that would leave `node_modules`.
1759
+ *
1760
+ * @param consumer - The consumer directory.
1761
+ * @param names - The packages to remove.
1762
+ * @returns One message per package whose install path resolves outside `node_modules`.
1763
+ */
1764
+ export function scanRemovals(consumer, names) {
1042
1765
  const root = join(consumer, 'node_modules');
1043
1766
  return names
1044
1767
  .filter((name) => !isContained(root, locateInstalled(consumer, name)))
1045
1768
  .map((name) => `The install of ${name} resolves outside node_modules`);
1046
1769
  }
1047
- function executeRestore(command) {
1770
+ /**
1771
+ * Runs the restore mode.
1772
+ *
1773
+ * @param command - The parsed command.
1774
+ * @returns 0 when the restore installed and proved, 1 when a proof failed, 64 on a usage refusal.
1775
+ * @throws Thrown when the ledger is malformed or the install fails.
1776
+ */
1777
+ export function executeRestore(command) {
1048
1778
  const manifestPath = join(command.consumer, 'package.json');
1049
1779
  if (!existsSync(manifestPath))
1050
1780
  return reportUsage(`${command.consumer} has no package.json`);
@@ -1067,9 +1797,9 @@ function executeRestore(command) {
1067
1797
  const at = new Date().toISOString();
1068
1798
  const plan = planRestore(manifest, ledger, command.packages);
1069
1799
  const removals = listRemovals(plan);
1070
- const escaping = scanRemovals(command.consumer, removals);
1071
- if (escaping.length > 0)
1072
- return reportUsage(escaping.join('\n'));
1800
+ const refusals = scanRemovals(command.consumer, removals);
1801
+ if (refusals.length > 0)
1802
+ return reportUsage(refusals.join('\n'));
1073
1803
  replaceFile(manifestPath, serializeJSON(plan.manifest));
1074
1804
  replaceFile(locateLedger(command.consumer), serializeJSON(plan.ledger));
1075
1805
  removePinnedInstalls(command.consumer, removals);
@@ -1111,12 +1841,31 @@ function executeRestore(command) {
1111
1841
  ], { ...receipt, receipt: path });
1112
1842
  return failures.length === 0 ? 0 : EXIT_FAILURE;
1113
1843
  }
1114
- // The manifest spec of a tarball resolved to the location it names, relative specs against the
1115
- // consumer, so a spec holds its tarball only when it names the ledger archive itself.
1116
- function locateSpecTarget(consumer, spec) {
1844
+ /**
1845
+ * Resolves a manifest tarball spec to the location it names.
1846
+ *
1847
+ * @remarks
1848
+ * Relative specs resolve against the consumer, so a spec holds its tarball only when it names the
1849
+ * ledger archive itself.
1850
+ *
1851
+ * @param consumer - The consumer directory.
1852
+ * @param spec - A `file:` spec.
1853
+ * @returns The location with forward slashes.
1854
+ */
1855
+ export function locateSpecTarget(consumer, spec) {
1117
1856
  return normalizeSlashes(resolve(consumer, spec.slice('file:'.length)));
1118
1857
  }
1119
- function describeStatus(name, entry, archive, report) {
1858
+ /**
1859
+ * Lists the drifts one pinned package reports.
1860
+ *
1861
+ * @param name - The package name.
1862
+ * @param entry - Its ledger row.
1863
+ * @param archive - The ledger archive's forward-slash location.
1864
+ * @param report - What the status read: whether the manifest holds the archive, whether the archive
1865
+ * matches its recorded digest, how the package is installed, and whether its source commit moved.
1866
+ * @returns One message per drift, empty when the pin holds.
1867
+ */
1868
+ export function describeStatus(name, entry, archive, report) {
1120
1869
  return [
1121
1870
  ...(report.pinned ? [] : [`${name}: the manifest does not hold ${archive}`]),
1122
1871
  ...(report.tarball
@@ -1135,7 +1884,14 @@ function describeStatus(name, entry, archive, report) {
1135
1884
  ...(report.moved ? [`${name}: ${entry.source} moved from ${entry.commit ?? '?'}; re-pin`] : []),
1136
1885
  ];
1137
1886
  }
1138
- function executeStatus(command) {
1887
+ /**
1888
+ * Runs the status mode.
1889
+ *
1890
+ * @param command - The parsed command.
1891
+ * @returns 0 when nothing drifted, 3 when something did, 64 on a usage refusal.
1892
+ * @throws Thrown when the ledger is malformed.
1893
+ */
1894
+ export function executeStatus(command) {
1139
1895
  const manifest = loadJSONRecord(join(command.consumer, 'package.json'));
1140
1896
  if (manifest === undefined)
1141
1897
  return reportUsage(`${command.consumer} has no readable package.json`);
@@ -1218,7 +1974,14 @@ function executeStatus(command) {
1218
1974
  });
1219
1975
  return problems.length === 0 ? 0 : EXIT_DRIFT;
1220
1976
  }
1221
- function main(argv) {
1977
+ /**
1978
+ * Runs the mode the arguments name.
1979
+ *
1980
+ * @param argv - The arguments after the script path.
1981
+ * @returns The exit code: 0 on success, 1 when a command or a proof failed, 3 when the status reports
1982
+ * drift, 64 on usage.
1983
+ */
1984
+ export function main(argv) {
1222
1985
  const parsed = parseCommand(argv);
1223
1986
  if (!parsed.ok)
1224
1987
  return reportUsage(parsed.message);
@@ -1234,4 +1997,6 @@ function main(argv) {
1234
1997
  return EXIT_FAILURE;
1235
1998
  }
1236
1999
  }
1237
- process.exitCode = main(process.argv.slice(2));
2000
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
2001
+ process.exitCode = main(process.argv.slice(2));
2002
+ }