@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.
- package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
- package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
- package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
- package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
- package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
- package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
- package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
- package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
- package/dist/agents/templates/brief.md +1 -1
- package/dist/bin/main.js +51 -46
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +23 -21
- package/dist/host/agents/orchestration.md +43 -18
- package/dist/host/agents/platforms/windows.md +3 -3
- package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
- package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
- package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
- package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
- package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
- package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
- package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
- package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
- package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
- package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
- package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
- package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
- package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
- package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
- package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
- package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
- package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
- package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
- package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
- package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
- package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
- package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
- package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
- package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
- package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
- package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
- package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
- package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
- package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
- package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
- package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
- package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
- package/dist/host/agents/templates/brief.md +1 -1
- package/dist/host/agents/transports/claude-cli.md +3 -3
- package/dist/host/agents/transports/codex.md +2 -7
- package/dist/host/agents/transports/cursor.md +2 -3
- package/dist/host/claude/AGENTS.md +2 -2
- package/dist/host/claude/agents/analyst.md +3 -3
- package/dist/host/claude/agents/astra.md +3 -3
- package/dist/host/claude/agents/builder.md +1 -1
- package/dist/host/claude/agents/checker.md +1 -1
- package/dist/host/claude/agents/distiller.md +1 -1
- package/dist/host/claude/agents/grok.md +1 -1
- package/dist/host/claude/agents/opus.md +2 -2
- package/dist/host/claude/agents/orkestrel.md +57 -90
- package/dist/host/claude/agents/researcher.md +1 -1
- package/dist/host/claude/agents/reviewer.md +4 -0
- package/dist/host/claude/agents/scout.md +1 -1
- package/dist/host/claude/agents/verifier.md +1 -1
- package/dist/host/claude/rules/application.md +14 -21
- package/dist/host/claude/rules/architecture.md +34 -44
- package/dist/host/claude/rules/browser.md +3 -4
- package/dist/host/claude/rules/campaign.md +2 -2
- package/dist/host/claude/rules/documentation.md +25 -12
- package/dist/host/claude/rules/names.md +9 -4
- package/dist/host/claude/rules/patterns.md +6 -7
- package/dist/host/claude/rules/portability.md +1 -1
- package/dist/host/claude/rules/quality.md +4 -5
- package/dist/host/claude/rules/styles.md +2 -2
- package/dist/host/claude/rules/tests.md +57 -114
- package/dist/host/claude/rules/typescript.md +7 -9
- package/dist/host/claude/rules/workspace.md +21 -19
- package/dist/host/claude/rules/writing.md +2 -5
- package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
- package/dist/host/codex/agents/analyst.toml +9 -3
- package/dist/host/codex/agents/astra.toml +3 -4
- package/dist/host/codex/agents/builder.toml +1 -1
- package/dist/host/codex/agents/opus.toml +3 -4
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/codex/agents/researcher.toml +2 -2
- package/dist/host/codex/agents/verifier.toml +1 -1
- package/dist/host/configs/helpers.ts +21 -1
- package/dist/host/configs/policy.ts +299 -15
- package/dist/host/dotfiles/oxlintrc.json +137 -3
- package/dist/host/guides/agent.md +111 -134
- package/dist/host/guides/capture.md +801 -0
- package/dist/host/guides/probe.md +27 -8
- package/dist/host/guides/scaffold.md +96 -18
- package/dist/host/guides/study.md +534 -440
- package/dist/host/guides/test.md +111 -44
- package/dist/host/manifest.json +160 -240
- package/dist/host/tests/config.test.ts +971 -187
- package/dist/host/tests/policy.test.ts +127 -1
- package/dist/host/tests/setupPolicy.ts +893 -33
- package/dist/src/core/index.cjs +214 -86
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +122 -38
- package/dist/src/core/index.d.ts +122 -38
- package/dist/src/core/index.js +209 -85
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +20 -27
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.js +21 -28
- package/dist/src/server/index.js.map +1 -1
- package/package.json +11 -11
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
- package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
- package/dist/host/guides/supervisor.md +0 -4890
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
// the tarball's absolute file: spec, because a relative spec breaks after the consumer is itself
|
|
18
18
|
// packed and installed elsewhere. --override NAME sets overrides[NAME] to $NAME when the consumer
|
|
19
19
|
// declares NAME, else to the tarball spec of the source named NAME, and a created block lands where
|
|
20
|
-
// oxfmt sorts it
|
|
20
|
+
// oxfmt sorts it (OVERRIDE_PRECEDERS, proved key by key against the installed formatter). The install
|
|
21
|
+
// runs with --ignore-scripts and deletes node_modules/.vite, then the
|
|
21
22
|
// proof requires each pinned package, an override-only one included, to be a directory whose file
|
|
22
23
|
// set is its pack list and whose bytes are its source's, no other copy of it to be installed, and
|
|
23
24
|
// every other top-level @orkestrel package to be byte-unchanged; a proved pin records the digest of
|
|
@@ -33,7 +34,9 @@
|
|
|
33
34
|
// ledger archive's full location.
|
|
34
35
|
// --pin and --restore write a receipt create-only to tmp/units/swap-<consumer>-<time>.json under the
|
|
35
36
|
// 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
|
+
// Exit 0 on success, 1 when a command or a proof failed, 3 when --status reports drift, 64 on usage,
|
|
38
|
+
// including a flag the mode does not read and the `--flag=value` form.
|
|
39
|
+
import type { SpawnSyncReturns } from 'node:child_process'
|
|
37
40
|
import { spawnSync } from 'node:child_process'
|
|
38
41
|
import { createHash } from 'node:crypto'
|
|
39
42
|
import {
|
|
@@ -45,30 +48,22 @@ import {
|
|
|
45
48
|
readFileSync,
|
|
46
49
|
realpathSync,
|
|
47
50
|
renameSync,
|
|
51
|
+
rmSync,
|
|
48
52
|
writeFileSync,
|
|
49
53
|
} from 'node:fs'
|
|
50
54
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
type Manifest = Readonly<Record<string, unknown>>
|
|
65
|
-
|
|
66
|
-
// The manifest fields a pin rewrites; `peerDependencies` stays a registry range.
|
|
67
|
-
type ManifestField = 'dependencies' | 'devDependencies'
|
|
68
|
-
|
|
69
|
-
type SwapMode = 'pin' | 'restore' | 'status'
|
|
70
|
-
|
|
71
|
-
interface Command {
|
|
55
|
+
|
|
56
|
+
/** Describes a package manifest as a read-only record. */
|
|
57
|
+
export type Manifest = Readonly<Record<string, unknown>>
|
|
58
|
+
|
|
59
|
+
/** Names the manifest fields a pin rewrites; `peerDependencies` stays a registry range. */
|
|
60
|
+
export type ManifestField = 'dependencies' | 'devDependencies'
|
|
61
|
+
|
|
62
|
+
/** Names the three modes of the script. */
|
|
63
|
+
export type SwapMode = 'pin' | 'restore' | 'status'
|
|
64
|
+
|
|
65
|
+
/** Describes a parsed command line. */
|
|
66
|
+
export interface Command {
|
|
72
67
|
readonly mode: SwapMode
|
|
73
68
|
readonly consumer: string
|
|
74
69
|
readonly sources: readonly string[]
|
|
@@ -79,23 +74,26 @@ interface Command {
|
|
|
79
74
|
readonly json: boolean
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
|
|
77
|
+
/** Describes a command line that parsed or the reason it did not. */
|
|
78
|
+
export type CommandResult =
|
|
83
79
|
| { readonly ok: true; readonly command: Command }
|
|
84
80
|
| { readonly ok: false; readonly message: string }
|
|
85
81
|
|
|
86
|
-
|
|
82
|
+
/** Describes a source package a pin packs. */
|
|
83
|
+
export interface Source {
|
|
87
84
|
readonly directory: string
|
|
88
85
|
readonly name: string
|
|
89
86
|
readonly version: string
|
|
90
87
|
}
|
|
91
88
|
|
|
92
|
-
|
|
89
|
+
/** Describes the field and range a manifest declares a package with. */
|
|
90
|
+
export interface Declaration {
|
|
93
91
|
readonly field: ManifestField
|
|
94
92
|
readonly spec: string
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
|
|
98
|
-
interface Tarball {
|
|
95
|
+
/** Describes one source after its pack, or planned with a `pending` hash under --dry-run. */
|
|
96
|
+
export interface Tarball {
|
|
99
97
|
readonly source: Source
|
|
100
98
|
readonly file: string
|
|
101
99
|
readonly spec: string
|
|
@@ -105,7 +103,8 @@ interface Tarball {
|
|
|
105
103
|
readonly dirty: boolean | undefined
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
|
|
106
|
+
/** Describes one package row of the ledger. */
|
|
107
|
+
export interface LedgerEntry {
|
|
109
108
|
readonly field?: ManifestField
|
|
110
109
|
readonly replaced?: string
|
|
111
110
|
readonly tarball: string
|
|
@@ -117,32 +116,37 @@ interface LedgerEntry {
|
|
|
117
116
|
readonly at: string
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
|
|
119
|
+
/** Describes one override row of the ledger. */
|
|
120
|
+
export interface OverrideEntry {
|
|
121
121
|
readonly value: string
|
|
122
122
|
readonly replaced?: string
|
|
123
123
|
readonly at: string
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
/** Describes the ledger that records every pin. */
|
|
127
|
+
export interface Ledger {
|
|
127
128
|
readonly packages: Readonly<Record<string, LedgerEntry>>
|
|
128
129
|
readonly overrides: Readonly<Record<string, OverrideEntry>>
|
|
129
130
|
readonly blockExisted?: boolean
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
/** Describes one manifest entry a plan changes. */
|
|
134
|
+
export interface Change {
|
|
133
135
|
readonly block: string
|
|
134
136
|
readonly name: string
|
|
135
137
|
readonly from: string | undefined
|
|
136
138
|
readonly to: string | undefined
|
|
137
139
|
}
|
|
138
140
|
|
|
139
|
-
|
|
141
|
+
/** Describes the manifest and ledger a pin writes. */
|
|
142
|
+
export interface PinPlan {
|
|
140
143
|
readonly manifest: Manifest
|
|
141
144
|
readonly ledger: Ledger
|
|
142
145
|
readonly changes: readonly Change[]
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
|
|
148
|
+
/** Describes the manifest and ledger a restore writes and the rows it restores. */
|
|
149
|
+
export interface RestorePlan {
|
|
146
150
|
readonly manifest: Manifest
|
|
147
151
|
readonly ledger: Ledger
|
|
148
152
|
readonly changes: readonly Change[]
|
|
@@ -150,51 +154,85 @@ interface RestorePlan {
|
|
|
150
154
|
readonly dropped: readonly string[]
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
|
|
157
|
+
/** Describes one installed copy of a named package. */
|
|
158
|
+
export interface Copy {
|
|
154
159
|
readonly name: string
|
|
155
160
|
readonly path: string
|
|
156
161
|
readonly version: string | undefined
|
|
157
162
|
readonly link: boolean
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
|
|
165
|
+
/** Describes a parsed semantic version. */
|
|
166
|
+
export interface Version {
|
|
161
167
|
readonly major: number
|
|
162
168
|
readonly minor: number
|
|
163
169
|
readonly patch: number
|
|
164
170
|
readonly pre: string | undefined
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
|
|
173
|
+
/** Describes one comparison a range makes against a version. */
|
|
174
|
+
export interface Comparator {
|
|
168
175
|
readonly operator: '>=' | '>' | '<=' | '<' | '='
|
|
169
176
|
readonly version: Version
|
|
170
177
|
}
|
|
171
178
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
179
|
+
/** Describes how to spawn npm on this host without a shell where the CLI entry is present. */
|
|
180
|
+
export interface NpmCommand {
|
|
181
|
+
readonly file: string
|
|
182
|
+
readonly prefix: readonly string[]
|
|
183
|
+
readonly shell: boolean
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Lists the manifest fields a pin rewrites. */
|
|
187
|
+
export const FIELDS: readonly ManifestField[] = ['dependencies', 'devDependencies']
|
|
188
|
+
|
|
189
|
+
/** Lists the modes in the order the script checks them. */
|
|
190
|
+
export const MODES: readonly SwapMode[] = ['pin', 'restore', 'status']
|
|
191
|
+
|
|
192
|
+
/** Names the consumer directory that holds the tarballs and the ledger. */
|
|
193
|
+
export const TARBALL_DIRECTORY = 'tmp/tarballs'
|
|
194
|
+
|
|
195
|
+
/** Names the ledger file inside the tarball directory. */
|
|
196
|
+
export const LEDGER_FILE = 'pins.json'
|
|
197
|
+
|
|
198
|
+
/** Names the working-directory folder that receives the receipts. */
|
|
199
|
+
export const RECEIPT_DIRECTORY = 'tmp/units'
|
|
200
|
+
|
|
201
|
+
/** Holds the exit code for a command or a proof that failed. */
|
|
202
|
+
export const EXIT_FAILURE = 1
|
|
203
|
+
|
|
204
|
+
/** Holds the exit code for a status that reports drift. */
|
|
205
|
+
export const EXIT_DRIFT = 3
|
|
206
|
+
|
|
207
|
+
/** Holds the exit code for a usage refusal. */
|
|
208
|
+
export const EXIT_USAGE = 64
|
|
209
|
+
|
|
210
|
+
/** Holds the refusal for a manifest the script would reformat. */
|
|
211
|
+
export const FORMAT_PROBLEM =
|
|
181
212
|
'package.json is not tab-indented JSON with a trailing newline; the script would reformat it'
|
|
182
213
|
|
|
183
|
-
|
|
214
|
+
/** Lists the value flags each mode reads. */
|
|
215
|
+
export const VALUE_FLAGS: Readonly<Record<SwapMode, readonly string[]>> = {
|
|
184
216
|
pin: ['--consumer', '--source', '--override'],
|
|
185
217
|
restore: ['--consumer', '--package'],
|
|
186
218
|
status: ['--consumer'],
|
|
187
219
|
}
|
|
188
220
|
|
|
189
|
-
|
|
221
|
+
/** Lists the switch flags each mode reads. */
|
|
222
|
+
export const SWITCH_FLAGS: Readonly<Record<SwapMode, readonly string[]>> = {
|
|
190
223
|
pin: ['--pin', '--no-build', '--dry-run', '--json'],
|
|
191
224
|
restore: ['--restore', '--json'],
|
|
192
225
|
status: ['--status', '--json'],
|
|
193
226
|
}
|
|
194
227
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Lists the `package.json` keys oxfmt sorts ahead of `overrides`.
|
|
230
|
+
*
|
|
231
|
+
* @remarks
|
|
232
|
+
* Derived for the installed oxfmt, 0.74.0; every other key it knows, and every key it does not know,
|
|
233
|
+
* follows `overrides`. `swap.test.ts` proves each key against the installed formatter.
|
|
234
|
+
*/
|
|
235
|
+
export const OVERRIDE_PRECEDERS: readonly string[] = [
|
|
198
236
|
'$schema',
|
|
199
237
|
'name',
|
|
200
238
|
'displayName',
|
|
@@ -258,29 +296,260 @@ const OVERRIDE_PRECEDERS: readonly string[] = [
|
|
|
258
296
|
'resolutions',
|
|
259
297
|
]
|
|
260
298
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Matches a package name.
|
|
301
|
+
*
|
|
302
|
+
* @remarks
|
|
303
|
+
* An optional `@scope/` and a name of the characters a registry URL carries untouched; a path
|
|
304
|
+
* separator beyond the scope's one `/`, an absolute form, and a drive form fall outside it.
|
|
305
|
+
*/
|
|
306
|
+
export const PACKAGE_NAME_PATTERN =
|
|
307
|
+
/^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/u
|
|
308
|
+
|
|
309
|
+
/** Holds the longest package name a registry accepts. */
|
|
310
|
+
export const PACKAGE_NAME_LIMIT = 214
|
|
311
|
+
|
|
312
|
+
/** Holds the usage text printed on a usage refusal. */
|
|
313
|
+
export const USAGE = [
|
|
268
314
|
'usage: swap.ts --pin --consumer DIR --source DIR [--source DIR ...] [--override NAME ...] [--no-build] [--dry-run] [--json]',
|
|
269
315
|
' swap.ts --restore --consumer DIR [--package NAME ...] [--json]',
|
|
270
316
|
' swap.ts --status --consumer DIR [--json]',
|
|
271
317
|
].join('\n')
|
|
272
318
|
|
|
273
|
-
|
|
319
|
+
/** Names the manifest fields that hold specs, including nested overrides. */
|
|
320
|
+
export const SPEC_FIELDS: readonly string[] = Object.freeze([
|
|
321
|
+
'dependencies',
|
|
322
|
+
'devDependencies',
|
|
323
|
+
'peerDependencies',
|
|
324
|
+
'optionalDependencies',
|
|
325
|
+
'overrides',
|
|
326
|
+
])
|
|
327
|
+
|
|
328
|
+
/** Matches npm local prefixes and path starts, including drive-relative paths. */
|
|
329
|
+
export const LOCAL_SPEC_PATTERN = /^(?:file:|link:|\.|~[\\/]|[\\/]|[A-Za-z]:)/iu
|
|
330
|
+
|
|
331
|
+
/** Matches archive specs with no protocol and a tgz, tar.gz, or tar extension. */
|
|
332
|
+
export const ARCHIVE_SPEC_PATTERN = /^[^:]+\.(?:tgz|tar\.gz|tar)$/iu
|
|
333
|
+
|
|
334
|
+
/** Matches hosted user/repository shorthand even when its name ends in an archive extension. */
|
|
335
|
+
export const SHORTHAND_SPEC_PATTERN = /^[^:@%/\s.-][^:@%/\s]*\/[^:@\s/%]+$/u
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Checks whether a value is a non-null, non-array object.
|
|
339
|
+
*
|
|
340
|
+
* @param value - The value to inspect.
|
|
341
|
+
* @returns True if the value is an object other than null or an array; false otherwise.
|
|
342
|
+
*/
|
|
343
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
344
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Checks whether npm resolves a spec from the local host.
|
|
349
|
+
*
|
|
350
|
+
* @param spec - The package spec, with leading whitespace ignored.
|
|
351
|
+
* @returns True if the spec names a local path or archive; false otherwise.
|
|
352
|
+
*/
|
|
353
|
+
export function matchesLocalSpec(spec: string): boolean {
|
|
354
|
+
const text = spec.trimStart()
|
|
355
|
+
return (
|
|
356
|
+
LOCAL_SPEC_PATTERN.test(text) ||
|
|
357
|
+
(ARCHIVE_SPEC_PATTERN.test(text) && !SHORTHAND_SPEC_PATTERN.test(text))
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Collects string specs with the dotted path of each nested record member.
|
|
363
|
+
*
|
|
364
|
+
* @param path - The field path that prefixes each finding.
|
|
365
|
+
* @param value - A string or finite, acyclic manifest record; other values contribute no specs.
|
|
366
|
+
* @returns The path and spec pairs in record order.
|
|
367
|
+
*/
|
|
368
|
+
export function collectSpecs(
|
|
369
|
+
path: string,
|
|
370
|
+
value: unknown,
|
|
371
|
+
): ReadonlyArray<readonly [string, string]> {
|
|
372
|
+
if (typeof value === 'string') return [[path, value]]
|
|
373
|
+
if (!isRecord(value)) return []
|
|
374
|
+
return Object.entries(value).flatMap(([key, child]) => collectSpecs(`${path}.${key}`, child))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Finds local specs in the manifest's spec fields, including nested overrides.
|
|
379
|
+
*
|
|
380
|
+
* @param manifest - A finite, acyclic package manifest record.
|
|
381
|
+
* @returns Each local spec's dotted field path and spec, in field and record order.
|
|
382
|
+
*/
|
|
383
|
+
export function scanLocalSpecs(
|
|
384
|
+
manifest: Readonly<Record<string, unknown>>,
|
|
385
|
+
): ReadonlyArray<readonly [string, string]> {
|
|
386
|
+
return SPEC_FIELDS.flatMap((field) =>
|
|
387
|
+
collectSpecs(field, manifest[field]).filter(([, spec]) => matchesLocalSpec(spec)),
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Removes a tree with bounded retries for transient host removal errors.
|
|
393
|
+
*
|
|
394
|
+
* @param path - The directory to remove; a missing path succeeds.
|
|
395
|
+
* @returns Nothing after the removal succeeds.
|
|
396
|
+
* @throws Thrown when removal fails after the bounded retries or the host refuses the path.
|
|
397
|
+
*/
|
|
398
|
+
export function removeTree(path: string): void {
|
|
399
|
+
rmSync(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Normalizes a host path to forward slashes.
|
|
404
|
+
*
|
|
405
|
+
* @param path - A host path.
|
|
406
|
+
* @returns The same path with every backslash replaced by a slash.
|
|
407
|
+
*/
|
|
408
|
+
export function normalizeSlashes(path: string): string {
|
|
409
|
+
return path.split('\\').join('/')
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Reads the value that follows a flag.
|
|
414
|
+
*
|
|
415
|
+
* @remarks
|
|
416
|
+
* Reads `--flag value` pairs: the first occurrence of a flag wins, a following value that opens
|
|
417
|
+
* with `--` is a missing value, and the `--flag=value` form is not read.
|
|
418
|
+
*
|
|
419
|
+
* @param argv - The arguments after the script path.
|
|
420
|
+
* @param name - The flag, such as `--consumer`.
|
|
421
|
+
* @returns The value after the flag, or undefined when the flag is absent, last, or followed by
|
|
422
|
+
* another flag.
|
|
423
|
+
*/
|
|
424
|
+
export function readOption(argv: readonly string[], name: string): string | undefined {
|
|
425
|
+
const index = argv.indexOf(name)
|
|
426
|
+
const value = index === -1 ? undefined : argv[index + 1]
|
|
427
|
+
return value === undefined || value.startsWith('--') ? undefined : value
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Names the value flags a caller gave with no value.
|
|
432
|
+
*
|
|
433
|
+
* @param argv - The arguments after the script path.
|
|
434
|
+
* @param names - The flags that take a value.
|
|
435
|
+
* @returns Each named flag that appears last or is followed by another flag, in argument order.
|
|
436
|
+
*/
|
|
437
|
+
export function readMissingFlags(
|
|
438
|
+
argv: readonly string[],
|
|
439
|
+
names: readonly string[],
|
|
440
|
+
): readonly string[] {
|
|
441
|
+
return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Reads every value that follows a repeatable flag.
|
|
446
|
+
*
|
|
447
|
+
* @param argv - The arguments after the script path.
|
|
448
|
+
* @param name - The flag, such as `--source`.
|
|
449
|
+
* @returns The values in order, empty when the flag never appears.
|
|
450
|
+
*/
|
|
451
|
+
export function readOptions(argv: readonly string[], name: string): readonly string[] {
|
|
452
|
+
return argv.flatMap((argument, index) => {
|
|
453
|
+
const value = argv[index + 1]
|
|
454
|
+
return argument === name && value !== undefined && !value.startsWith('--') ? [value] : []
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Parses a whole text as one JSON object.
|
|
460
|
+
*
|
|
461
|
+
* @param text - A JSON document, formatted or not, such as a manifest.
|
|
462
|
+
* @returns The parsed object, or undefined when the text is not a JSON object.
|
|
463
|
+
*/
|
|
464
|
+
export function parseJsonRecord(text: string): Record<string, unknown> | undefined {
|
|
465
|
+
try {
|
|
466
|
+
const parsed: unknown = JSON.parse(text)
|
|
467
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
468
|
+
return Object.fromEntries(Object.entries(parsed))
|
|
469
|
+
}
|
|
470
|
+
} catch {}
|
|
471
|
+
return undefined
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Reads a string member of a record.
|
|
476
|
+
*
|
|
477
|
+
* @param record - The record, or undefined.
|
|
478
|
+
* @param key - The member name.
|
|
479
|
+
* @returns The string, or undefined when the member is absent or not a string.
|
|
480
|
+
*/
|
|
481
|
+
export function readString(
|
|
482
|
+
record: Record<string, unknown> | undefined,
|
|
483
|
+
key: string,
|
|
484
|
+
): string | undefined {
|
|
485
|
+
const value = record?.[key]
|
|
486
|
+
return typeof value === 'string' ? value : undefined
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Resolves the npm command for this host.
|
|
491
|
+
*
|
|
492
|
+
* @remarks
|
|
493
|
+
* The `npm-cli.js` entry beside the running Node binary spawns without a shell on every host; the
|
|
494
|
+
* `npm` shim on Windows is a `.cmd` file that needs one, so that path is the fallback alone.
|
|
495
|
+
*
|
|
496
|
+
* @returns The file to spawn, the arguments that precede npm's own, and whether a shell is needed.
|
|
497
|
+
*/
|
|
498
|
+
export function resolveNpm(): NpmCommand {
|
|
499
|
+
const cli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js')
|
|
500
|
+
if (existsSync(cli)) return { file: process.execPath, prefix: [cli], shell: false }
|
|
501
|
+
return { file: 'npm', prefix: [], shell: process.platform === 'win32' }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Runs npm with the given arguments and returns the finished child.
|
|
506
|
+
*
|
|
507
|
+
* @param args - The npm arguments, such as `['install']`.
|
|
508
|
+
* @param cwd - The working directory. Default: the current directory.
|
|
509
|
+
* @returns The spawn result with `utf8` stdout and stderr.
|
|
510
|
+
*/
|
|
511
|
+
export function runNpm(args: readonly string[], cwd?: string): SpawnSyncReturns<string> {
|
|
512
|
+
const npm = resolveNpm()
|
|
513
|
+
return spawnSync(npm.file, [...npm.prefix, ...args], {
|
|
514
|
+
encoding: 'utf8',
|
|
515
|
+
windowsHide: true,
|
|
516
|
+
shell: npm.shell,
|
|
517
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
518
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Checks whether a ledger key is a package name that stays inside `node_modules`.
|
|
524
|
+
*
|
|
525
|
+
* @param name - The ledger key.
|
|
526
|
+
* @returns True if the key is an optional scope and a name of registry-safe characters within the
|
|
527
|
+
* registry's length limit and holding no `..`; false otherwise.
|
|
528
|
+
*/
|
|
529
|
+
export function isPackageName(name: string): boolean {
|
|
274
530
|
return (
|
|
275
531
|
name.length <= PACKAGE_NAME_LIMIT && !name.includes('..') && PACKAGE_NAME_PATTERN.test(name)
|
|
276
532
|
)
|
|
277
533
|
}
|
|
278
534
|
|
|
279
|
-
|
|
535
|
+
/**
|
|
536
|
+
* Checks whether a value names a manifest field a pin rewrites.
|
|
537
|
+
*
|
|
538
|
+
* @param value - The value to inspect.
|
|
539
|
+
* @returns True if the value is `dependencies` or `devDependencies`; false otherwise.
|
|
540
|
+
*/
|
|
541
|
+
export function isManifestField(value: unknown): value is ManifestField {
|
|
280
542
|
return value === 'dependencies' || value === 'devDependencies'
|
|
281
543
|
}
|
|
282
544
|
|
|
283
|
-
|
|
545
|
+
/**
|
|
546
|
+
* Checks whether a value is a package row of the ledger.
|
|
547
|
+
*
|
|
548
|
+
* @param value - The value to inspect.
|
|
549
|
+
* @returns True if the value carries the tarball, hash, source, and time strings and each optional
|
|
550
|
+
* member in its declared type; false otherwise.
|
|
551
|
+
*/
|
|
552
|
+
export function isLedgerEntry(value: unknown): value is LedgerEntry {
|
|
284
553
|
return (
|
|
285
554
|
isRecord(value) &&
|
|
286
555
|
typeof value.tarball === 'string' &&
|
|
@@ -295,7 +564,14 @@ function isLedgerEntry(value: unknown): value is LedgerEntry {
|
|
|
295
564
|
)
|
|
296
565
|
}
|
|
297
566
|
|
|
298
|
-
|
|
567
|
+
/**
|
|
568
|
+
* Checks whether a value is an override row of the ledger.
|
|
569
|
+
*
|
|
570
|
+
* @param value - The value to inspect.
|
|
571
|
+
* @returns True if the value carries the value and time strings and an optional replaced string;
|
|
572
|
+
* false otherwise.
|
|
573
|
+
*/
|
|
574
|
+
export function isOverrideEntry(value: unknown): value is OverrideEntry {
|
|
299
575
|
return (
|
|
300
576
|
isRecord(value) &&
|
|
301
577
|
typeof value.value === 'string' &&
|
|
@@ -304,8 +580,16 @@ function isOverrideEntry(value: unknown): value is OverrideEntry {
|
|
|
304
580
|
)
|
|
305
581
|
}
|
|
306
582
|
|
|
307
|
-
|
|
308
|
-
|
|
583
|
+
/**
|
|
584
|
+
* Parses a ledger record.
|
|
585
|
+
*
|
|
586
|
+
* @remarks
|
|
587
|
+
* A ledger with one malformed row is refused whole, so a restore never drops a range it cannot read.
|
|
588
|
+
*
|
|
589
|
+
* @param value - The parsed ledger file.
|
|
590
|
+
* @returns The ledger, or undefined when a block is missing or any row is malformed.
|
|
591
|
+
*/
|
|
592
|
+
export function parseLedger(value: unknown): Ledger | undefined {
|
|
309
593
|
if (!isRecord(value) || !isRecord(value.packages) || !isRecord(value.overrides)) return undefined
|
|
310
594
|
const rows = Object.entries(value.packages)
|
|
311
595
|
const packages = rows.flatMap(([name, entry]): Array<[string, LedgerEntry]> =>
|
|
@@ -324,28 +608,66 @@ function parseLedger(value: unknown): Ledger | undefined {
|
|
|
324
608
|
}
|
|
325
609
|
}
|
|
326
610
|
|
|
327
|
-
|
|
611
|
+
/**
|
|
612
|
+
* Serializes a value as every Orkestrel manifest is written.
|
|
613
|
+
*
|
|
614
|
+
* @param value - The value to serialize.
|
|
615
|
+
* @returns Tab-indented JSON with a trailing newline.
|
|
616
|
+
*/
|
|
617
|
+
export function serializeJSON(value: unknown): string {
|
|
328
618
|
return `${JSON.stringify(value, null, '\t')}\n`
|
|
329
619
|
}
|
|
330
620
|
|
|
331
|
-
|
|
621
|
+
/**
|
|
622
|
+
* Reads one dependency-style block of a manifest.
|
|
623
|
+
*
|
|
624
|
+
* @param manifest - The manifest.
|
|
625
|
+
* @param field - The block's field name.
|
|
626
|
+
* @returns The block, or an empty record when the field is absent or not a record.
|
|
627
|
+
*/
|
|
628
|
+
export function readBlock(manifest: Manifest, field: string): Readonly<Record<string, unknown>> {
|
|
332
629
|
const block = manifest[field]
|
|
333
630
|
return isRecord(block) ? block : {}
|
|
334
631
|
}
|
|
335
632
|
|
|
336
|
-
|
|
633
|
+
/**
|
|
634
|
+
* Finds the field that declares a package.
|
|
635
|
+
*
|
|
636
|
+
* @param manifest - The manifest.
|
|
637
|
+
* @param name - The package name.
|
|
638
|
+
* @returns The field and spec of the first of `dependencies` and `devDependencies` that holds a
|
|
639
|
+
* string spec for the name, or undefined when neither does.
|
|
640
|
+
*/
|
|
641
|
+
export function findDeclaration(manifest: Manifest, name: string): Declaration | undefined {
|
|
337
642
|
const field = FIELDS.find((candidate) => typeof readBlock(manifest, candidate)[name] === 'string')
|
|
338
643
|
const spec = field === undefined ? undefined : readBlock(manifest, field)[name]
|
|
339
644
|
return field === undefined || typeof spec !== 'string' ? undefined : { field, spec }
|
|
340
645
|
}
|
|
341
646
|
|
|
342
|
-
|
|
647
|
+
/**
|
|
648
|
+
* Orders two keys by their code units.
|
|
649
|
+
*
|
|
650
|
+
* @param left - One key.
|
|
651
|
+
* @param right - The other key.
|
|
652
|
+
* @returns A negative number, 0, or a positive number as the left key sorts before, equal to, or
|
|
653
|
+
* after the right.
|
|
654
|
+
*/
|
|
655
|
+
export function compareKeys(left: string, right: string): number {
|
|
343
656
|
return left < right ? -1 : left > right ? 1 : 0
|
|
344
657
|
}
|
|
345
658
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
659
|
+
/**
|
|
660
|
+
* Merges added entries into a block's kept entries.
|
|
661
|
+
*
|
|
662
|
+
* @remarks
|
|
663
|
+
* A block whose keys are sorted stays sorted, because oxfmt sorts a dependency or override block;
|
|
664
|
+
* any other block takes the added keys last.
|
|
665
|
+
*
|
|
666
|
+
* @param kept - The entries the block keeps, in order.
|
|
667
|
+
* @param added - The entries the block gains.
|
|
668
|
+
* @returns The merged entries.
|
|
669
|
+
*/
|
|
670
|
+
export function mergeEntries(
|
|
349
671
|
kept: ReadonlyArray<readonly [string, unknown]>,
|
|
350
672
|
added: ReadonlyArray<readonly [string, unknown]>,
|
|
351
673
|
): ReadonlyArray<readonly [string, unknown]> {
|
|
@@ -356,9 +678,23 @@ function mergeEntries(
|
|
|
356
678
|
return sorted ? merged.toSorted(([left], [right]) => compareKeys(left, right)) : merged
|
|
357
679
|
}
|
|
358
680
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Inserts a created block where oxfmt sorts it.
|
|
683
|
+
*
|
|
684
|
+
* @remarks
|
|
685
|
+
* A created block lands before the first key oxfmt sorts after `overrides`, or last when every key
|
|
686
|
+
* sorts ahead of it.
|
|
687
|
+
*
|
|
688
|
+
* @param manifest - The manifest.
|
|
689
|
+
* @param field - The new block's field name.
|
|
690
|
+
* @param block - The new block.
|
|
691
|
+
* @returns The manifest with the block inserted.
|
|
692
|
+
*/
|
|
693
|
+
export function placeBlock(
|
|
694
|
+
manifest: Manifest,
|
|
695
|
+
field: string,
|
|
696
|
+
block: Record<string, unknown>,
|
|
697
|
+
): Manifest {
|
|
362
698
|
const entries = Object.entries(manifest)
|
|
363
699
|
const follower = entries.findIndex(([key]) => !OVERRIDE_PRECEDERS.includes(key))
|
|
364
700
|
const position = follower === -1 ? entries.length : follower
|
|
@@ -369,10 +705,21 @@ function placeBlock(manifest: Manifest, field: string, block: Record<string, unk
|
|
|
369
705
|
])
|
|
370
706
|
}
|
|
371
707
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
708
|
+
/**
|
|
709
|
+
* Rewrites one block of a manifest.
|
|
710
|
+
*
|
|
711
|
+
* @remarks
|
|
712
|
+
* A changed key keeps its position, a key mapped to undefined is removed, and an `overrides` block
|
|
713
|
+
* left empty is removed unless `keepEmpty` is set. A block that is absent and gains nothing is not
|
|
714
|
+
* created.
|
|
715
|
+
*
|
|
716
|
+
* @param manifest - The manifest.
|
|
717
|
+
* @param field - The block's field name.
|
|
718
|
+
* @param changes - The new spec for each changed key, or undefined to remove the key.
|
|
719
|
+
* @param keepEmpty - If `true`, an `overrides` block left empty stays; if `false`, it is removed.
|
|
720
|
+
* @returns The rewritten manifest.
|
|
721
|
+
*/
|
|
722
|
+
export function rewriteBlock(
|
|
376
723
|
manifest: Manifest,
|
|
377
724
|
field: string,
|
|
378
725
|
changes: ReadonlyMap<string, string | undefined>,
|
|
@@ -401,12 +748,26 @@ function rewriteBlock(
|
|
|
401
748
|
)
|
|
402
749
|
}
|
|
403
750
|
|
|
404
|
-
|
|
751
|
+
/**
|
|
752
|
+
* Renders a manifest spec for a change line.
|
|
753
|
+
*
|
|
754
|
+
* @param value - The spec, or undefined for an absent entry.
|
|
755
|
+
* @returns The string itself, the JSON of any other value, or undefined for undefined.
|
|
756
|
+
*/
|
|
757
|
+
export function renderValue(value: unknown): string | undefined {
|
|
405
758
|
if (value === undefined) return undefined
|
|
406
759
|
return typeof value === 'string' ? value : JSON.stringify(value)
|
|
407
760
|
}
|
|
408
761
|
|
|
409
|
-
|
|
762
|
+
/**
|
|
763
|
+
* Lists the entries that differ between two manifests.
|
|
764
|
+
*
|
|
765
|
+
* @param before - The manifest before.
|
|
766
|
+
* @param after - The manifest after.
|
|
767
|
+
* @returns One change per entry of `dependencies`, `devDependencies`, and `overrides` whose rendered
|
|
768
|
+
* spec differs.
|
|
769
|
+
*/
|
|
770
|
+
export function diffManifests(before: Manifest, after: Manifest): readonly Change[] {
|
|
410
771
|
return ['dependencies', 'devDependencies', 'overrides'].flatMap((block) => {
|
|
411
772
|
const from = readBlock(before, block)
|
|
412
773
|
const to = readBlock(after, block)
|
|
@@ -419,19 +780,46 @@ function diffManifests(before: Manifest, after: Manifest): readonly Change[] {
|
|
|
419
780
|
})
|
|
420
781
|
}
|
|
421
782
|
|
|
422
|
-
|
|
783
|
+
/**
|
|
784
|
+
* Builds a tarball's file name.
|
|
785
|
+
*
|
|
786
|
+
* @param name - The package name.
|
|
787
|
+
* @param version - The package version.
|
|
788
|
+
* @param sha256 - The archive's SHA-256 digest.
|
|
789
|
+
* @returns The scope and name joined by a dash, the version, and the first twelve hex digits of the
|
|
790
|
+
* digest.
|
|
791
|
+
*/
|
|
792
|
+
export function buildTarballName(name: string, version: string, sha256: string): string {
|
|
423
793
|
return `${name.replace(/^@/u, '').replace('/', '-')}-${version}-${sha256.slice(0, 12)}.tgz`
|
|
424
794
|
}
|
|
425
795
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
796
|
+
/**
|
|
797
|
+
* Builds the spec a manifest holds for a tarball.
|
|
798
|
+
*
|
|
799
|
+
* @remarks
|
|
800
|
+
* npm resolves a dependency's relative file: spec against that dependency's own directory, so only
|
|
801
|
+
* an absolute spec resolves inside a packed consumer installed elsewhere.
|
|
802
|
+
*
|
|
803
|
+
* @param consumer - The consumer directory.
|
|
804
|
+
* @param file - The tarball's file name.
|
|
805
|
+
* @returns The absolute `file:` spec with forward slashes.
|
|
806
|
+
*/
|
|
807
|
+
export function buildTarballSpec(consumer: string, file: string): string {
|
|
429
808
|
return `file:${normalizeSlashes(resolve(consumer, ...TARBALL_DIRECTORY.split('/'), file))}`
|
|
430
809
|
}
|
|
431
810
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
811
|
+
/**
|
|
812
|
+
* Checks whether a spec names a tarball in a tarball directory.
|
|
813
|
+
*
|
|
814
|
+
* @remarks
|
|
815
|
+
* The relative `file:tmp/tarballs/<file>` form is what earlier pins wrote, so a re-pin over it keeps
|
|
816
|
+
* the range the first pin replaced.
|
|
817
|
+
*
|
|
818
|
+
* @param spec - The manifest spec.
|
|
819
|
+
* @returns True if the spec is a `file:` spec into the relative or an absolute tarball directory;
|
|
820
|
+
* false otherwise.
|
|
821
|
+
*/
|
|
822
|
+
export function isTarballSpec(spec: string): boolean {
|
|
435
823
|
if (!spec.startsWith('file:')) return false
|
|
436
824
|
const path = normalizeSlashes(spec.slice('file:'.length))
|
|
437
825
|
return (
|
|
@@ -440,7 +828,16 @@ function isTarballSpec(spec: string): boolean {
|
|
|
440
828
|
)
|
|
441
829
|
}
|
|
442
830
|
|
|
443
|
-
|
|
831
|
+
/**
|
|
832
|
+
* Builds the ledger row for one packed source.
|
|
833
|
+
*
|
|
834
|
+
* @param manifest - The consumer manifest before the pin.
|
|
835
|
+
* @param ledger - The ledger before the pin.
|
|
836
|
+
* @param tarball - The packed source.
|
|
837
|
+
* @param at - The time of the pin.
|
|
838
|
+
* @returns The row; a re-pin over a tarball spec keeps the range the first pin replaced.
|
|
839
|
+
*/
|
|
840
|
+
export function buildLedgerEntry(
|
|
444
841
|
manifest: Manifest,
|
|
445
842
|
ledger: Ledger,
|
|
446
843
|
tarball: Tarball,
|
|
@@ -463,11 +860,23 @@ function buildLedgerEntry(
|
|
|
463
860
|
}
|
|
464
861
|
}
|
|
465
862
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
863
|
+
/**
|
|
864
|
+
* Plans a pin.
|
|
865
|
+
*
|
|
866
|
+
* @remarks
|
|
867
|
+
* The replaced range is kept from the first pin while the manifest still holds a tarball spec, and a
|
|
868
|
+
* package no field declares is recorded without a field and takes effect through its override alone.
|
|
869
|
+
* The first pin records whether the manifest held an overrides block, so a restore keeps one that
|
|
870
|
+
* existed and removes one the pin created.
|
|
871
|
+
*
|
|
872
|
+
* @param manifest - The consumer manifest.
|
|
873
|
+
* @param ledger - The ledger before the pin.
|
|
874
|
+
* @param tarballs - The packed or planned sources.
|
|
875
|
+
* @param overrides - The names to override.
|
|
876
|
+
* @param at - The time of the pin.
|
|
877
|
+
* @returns The manifest and ledger to write and the changes between the manifests.
|
|
878
|
+
*/
|
|
879
|
+
export function planPin(
|
|
471
880
|
manifest: Manifest,
|
|
472
881
|
ledger: Ledger,
|
|
473
882
|
tarballs: readonly Tarball[],
|
|
@@ -531,8 +940,22 @@ function planPin(
|
|
|
531
940
|
}
|
|
532
941
|
}
|
|
533
942
|
|
|
534
|
-
|
|
535
|
-
|
|
943
|
+
/**
|
|
944
|
+
* Plans a restore.
|
|
945
|
+
*
|
|
946
|
+
* @remarks
|
|
947
|
+
* An empty name list restores every recorded package and override.
|
|
948
|
+
*
|
|
949
|
+
* @param manifest - The consumer manifest.
|
|
950
|
+
* @param ledger - The ledger before the restore.
|
|
951
|
+
* @param names - The packages to restore, or none for every one.
|
|
952
|
+
* @returns The manifest and ledger to write, the restored rows, and the overrides dropped.
|
|
953
|
+
*/
|
|
954
|
+
export function planRestore(
|
|
955
|
+
manifest: Manifest,
|
|
956
|
+
ledger: Ledger,
|
|
957
|
+
names: readonly string[],
|
|
958
|
+
): RestorePlan {
|
|
536
959
|
const targets = names.length === 0 ? Object.keys(ledger.packages) : names
|
|
537
960
|
const selected = new Set(targets)
|
|
538
961
|
const rows = targets.flatMap((name): Array<{ name: string; entry: LedgerEntry }> => {
|
|
@@ -590,7 +1013,17 @@ function planRestore(manifest: Manifest, ledger: Ledger, names: readonly string[
|
|
|
590
1013
|
}
|
|
591
1014
|
}
|
|
592
1015
|
|
|
593
|
-
|
|
1016
|
+
/**
|
|
1017
|
+
* Lists the problems that refuse a pin request.
|
|
1018
|
+
*
|
|
1019
|
+
* @param text - The manifest file's text.
|
|
1020
|
+
* @param manifest - The parsed manifest.
|
|
1021
|
+
* @param ledger - The ledger before the pin.
|
|
1022
|
+
* @param sources - The sources to pin.
|
|
1023
|
+
* @param overrides - The names to override.
|
|
1024
|
+
* @returns One message per problem, empty when the request is usable.
|
|
1025
|
+
*/
|
|
1026
|
+
export function scanPinRequest(
|
|
594
1027
|
text: string,
|
|
595
1028
|
manifest: Manifest,
|
|
596
1029
|
ledger: Ledger,
|
|
@@ -632,7 +1065,14 @@ function scanPinRequest(
|
|
|
632
1065
|
]
|
|
633
1066
|
}
|
|
634
1067
|
|
|
635
|
-
|
|
1068
|
+
/**
|
|
1069
|
+
* Parses a full semantic version.
|
|
1070
|
+
*
|
|
1071
|
+
* @param text - A version such as `1.2.3`, `v1.2.3-rc.1`, or `1.2.3+build`, with surrounding
|
|
1072
|
+
* whitespace ignored.
|
|
1073
|
+
* @returns The version, or undefined when the text is not a full version. Build metadata is dropped.
|
|
1074
|
+
*/
|
|
1075
|
+
export function parseVersion(text: string): Version | undefined {
|
|
636
1076
|
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(
|
|
637
1077
|
text.trim(),
|
|
638
1078
|
)
|
|
@@ -645,7 +1085,16 @@ function parseVersion(text: string): Version | undefined {
|
|
|
645
1085
|
}
|
|
646
1086
|
}
|
|
647
1087
|
|
|
648
|
-
|
|
1088
|
+
/**
|
|
1089
|
+
* Orders two prerelease identifier lists.
|
|
1090
|
+
*
|
|
1091
|
+
* @param left - One prerelease text, such as `rc.1`.
|
|
1092
|
+
* @param right - The other.
|
|
1093
|
+
* @returns A negative number, 0, or a positive number as the left sorts before, equal to, or after
|
|
1094
|
+
* the right: numeric identifiers compare as numbers and sort before alphanumeric ones, and a longer
|
|
1095
|
+
* list sorts after its prefix.
|
|
1096
|
+
*/
|
|
1097
|
+
export function comparePrerelease(left: string, right: string): number {
|
|
649
1098
|
const leftParts = left.split('.')
|
|
650
1099
|
const rightParts = right.split('.')
|
|
651
1100
|
const first = leftParts
|
|
@@ -661,7 +1110,15 @@ function comparePrerelease(left: string, right: string): number {
|
|
|
661
1110
|
return first ?? leftParts.length - rightParts.length
|
|
662
1111
|
}
|
|
663
1112
|
|
|
664
|
-
|
|
1113
|
+
/**
|
|
1114
|
+
* Orders two versions.
|
|
1115
|
+
*
|
|
1116
|
+
* @param left - One version.
|
|
1117
|
+
* @param right - The other.
|
|
1118
|
+
* @returns A negative number, 0, or a positive number as the left sorts before, equal to, or after
|
|
1119
|
+
* the right; a release sorts after its prerelease.
|
|
1120
|
+
*/
|
|
1121
|
+
export function compareVersions(left: Version, right: Version): number {
|
|
665
1122
|
const core = [left.major - right.major, left.minor - right.minor, left.patch - right.patch].find(
|
|
666
1123
|
(order) => order !== 0,
|
|
667
1124
|
)
|
|
@@ -672,9 +1129,17 @@ function compareVersions(left: Version, right: Version): number {
|
|
|
672
1129
|
return comparePrerelease(left.pre, right.pre)
|
|
673
1130
|
}
|
|
674
1131
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1132
|
+
/**
|
|
1133
|
+
* Parses one token of a range into comparators.
|
|
1134
|
+
*
|
|
1135
|
+
* @remarks
|
|
1136
|
+
* Reads `*`, `x`, an exact version, `^`, `~`, and the `>=`, `>`, `<=`, `<`, and `=` operators on full
|
|
1137
|
+
* versions; an empty list matches every version.
|
|
1138
|
+
*
|
|
1139
|
+
* @param token - One whitespace-free token of a range.
|
|
1140
|
+
* @returns The comparators the token means, or undefined when the token is not in the subset.
|
|
1141
|
+
*/
|
|
1142
|
+
export function parseComparators(token: string): readonly Comparator[] | undefined {
|
|
678
1143
|
if (token === '*' || token === 'x' || token === 'X' || token === '') return []
|
|
679
1144
|
const operator = /^(\^|~|>=|<=|>|<|=)?(.+)$/u.exec(token)
|
|
680
1145
|
const version = operator === null ? undefined : parseVersion(operator[2] ?? '')
|
|
@@ -707,7 +1172,14 @@ function parseComparators(token: string): readonly Comparator[] | undefined {
|
|
|
707
1172
|
return [{ operator: '=', version }]
|
|
708
1173
|
}
|
|
709
1174
|
|
|
710
|
-
|
|
1175
|
+
/**
|
|
1176
|
+
* Checks whether a version satisfies one comparator.
|
|
1177
|
+
*
|
|
1178
|
+
* @param version - The version.
|
|
1179
|
+
* @param comparator - The comparator.
|
|
1180
|
+
* @returns True if the version stands in the comparator's relation to its bound; false otherwise.
|
|
1181
|
+
*/
|
|
1182
|
+
export function matchesComparator(version: Version, comparator: Comparator): boolean {
|
|
711
1183
|
const order = compareVersions(version, comparator.version)
|
|
712
1184
|
if (comparator.operator === '>=') return order >= 0
|
|
713
1185
|
if (comparator.operator === '>') return order > 0
|
|
@@ -716,7 +1188,14 @@ function matchesComparator(version: Version, comparator: Comparator): boolean {
|
|
|
716
1188
|
return order === 0
|
|
717
1189
|
}
|
|
718
1190
|
|
|
719
|
-
|
|
1191
|
+
/**
|
|
1192
|
+
* Checks whether the script reads every token of a range.
|
|
1193
|
+
*
|
|
1194
|
+
* @param range - A registry range.
|
|
1195
|
+
* @returns True if every token of every `||` set is in the subset {@link parseComparators} reads;
|
|
1196
|
+
* false otherwise.
|
|
1197
|
+
*/
|
|
1198
|
+
export function supportsRange(range: string): boolean {
|
|
720
1199
|
return range.split('||').every((set) =>
|
|
721
1200
|
set
|
|
722
1201
|
.trim()
|
|
@@ -725,9 +1204,19 @@ function supportsRange(range: string): boolean {
|
|
|
725
1204
|
)
|
|
726
1205
|
}
|
|
727
1206
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1207
|
+
/**
|
|
1208
|
+
* Checks whether a version satisfies a range.
|
|
1209
|
+
*
|
|
1210
|
+
* @remarks
|
|
1211
|
+
* A prerelease satisfies a set only when one of its comparators names a prerelease of the same
|
|
1212
|
+
* `major.minor.patch`, and an unreadable range is satisfied by nothing.
|
|
1213
|
+
*
|
|
1214
|
+
* @param text - The version.
|
|
1215
|
+
* @param range - A registry range.
|
|
1216
|
+
* @returns True if the version parses and satisfies every comparator of at least one `||` set;
|
|
1217
|
+
* false otherwise.
|
|
1218
|
+
*/
|
|
1219
|
+
export function matchesRange(text: string, range: string): boolean {
|
|
731
1220
|
const version = parseVersion(text)
|
|
732
1221
|
if (version === undefined) return false
|
|
733
1222
|
return range.split('||').some((set) => {
|
|
@@ -750,7 +1239,28 @@ function matchesRange(text: string, range: string): boolean {
|
|
|
750
1239
|
})
|
|
751
1240
|
}
|
|
752
1241
|
|
|
753
|
-
|
|
1242
|
+
/**
|
|
1243
|
+
* Parses the command line.
|
|
1244
|
+
*
|
|
1245
|
+
* @param argv - The arguments after the script path.
|
|
1246
|
+
* @returns The command, or the reason the line is refused: no mode or two modes, a flag the mode does
|
|
1247
|
+
* not read (the `--flag=value` form included), a flag with no value, no consumer, or a pin with no
|
|
1248
|
+
* source.
|
|
1249
|
+
*/
|
|
1250
|
+
export function parseCommand(argv: readonly string[]): CommandResult {
|
|
1251
|
+
const known = MODES.flatMap((candidate) => [
|
|
1252
|
+
`--${candidate}`,
|
|
1253
|
+
...VALUE_FLAGS[candidate],
|
|
1254
|
+
...SWITCH_FLAGS[candidate],
|
|
1255
|
+
])
|
|
1256
|
+
// A bare `--` ends the arguments the script reads; nothing after it is a flag of this script.
|
|
1257
|
+
const end = argv.indexOf('--')
|
|
1258
|
+
const foreign = (end === -1 ? argv : argv.slice(0, end)).filter(
|
|
1259
|
+
(token) => token.startsWith('--') && !known.includes(token),
|
|
1260
|
+
)
|
|
1261
|
+
if (foreign.length > 0) {
|
|
1262
|
+
return { ok: false, message: `${foreign.join(', ')} is not a flag this script reads` }
|
|
1263
|
+
}
|
|
754
1264
|
const given = MODES.filter((candidate) => argv.includes(`--${candidate}`))
|
|
755
1265
|
const mode = MODES.find((candidate) => argv.includes(`--${candidate}`))
|
|
756
1266
|
if (mode === undefined)
|
|
@@ -796,23 +1306,53 @@ function parseCommand(argv: readonly string[]): CommandResult {
|
|
|
796
1306
|
}
|
|
797
1307
|
}
|
|
798
1308
|
|
|
799
|
-
|
|
1309
|
+
/**
|
|
1310
|
+
* Computes the SHA-256 digest of bytes.
|
|
1311
|
+
*
|
|
1312
|
+
* @param value - The bytes.
|
|
1313
|
+
* @returns The digest as lowercase hexadecimal.
|
|
1314
|
+
*/
|
|
1315
|
+
export function computeHash(value: Uint8Array): string {
|
|
800
1316
|
return createHash('sha256').update(value).digest('hex')
|
|
801
1317
|
}
|
|
802
1318
|
|
|
803
|
-
|
|
1319
|
+
/**
|
|
1320
|
+
* Loads a JSON file as a record.
|
|
1321
|
+
*
|
|
1322
|
+
* @param path - The file.
|
|
1323
|
+
* @returns The record, or undefined when the file is absent or not a JSON object.
|
|
1324
|
+
*/
|
|
1325
|
+
export function loadJSONRecord(path: string): Record<string, unknown> | undefined {
|
|
804
1326
|
return existsSync(path) ? parseJsonRecord(readFileSync(path, 'utf8')) : undefined
|
|
805
1327
|
}
|
|
806
1328
|
|
|
807
|
-
|
|
1329
|
+
/**
|
|
1330
|
+
* Loads a source package's name and version.
|
|
1331
|
+
*
|
|
1332
|
+
* @param directory - The source directory.
|
|
1333
|
+
* @returns The source, or undefined when its manifest lacks a name or a version.
|
|
1334
|
+
*/
|
|
1335
|
+
export function loadSource(directory: string): Source | undefined {
|
|
808
1336
|
const manifest = loadJSONRecord(join(directory, 'package.json'))
|
|
809
1337
|
const name = readString(manifest, 'name')
|
|
810
1338
|
const version = readString(manifest, 'version')
|
|
811
1339
|
return name === undefined || version === undefined ? undefined : { directory, name, version }
|
|
812
1340
|
}
|
|
813
1341
|
|
|
814
|
-
|
|
815
|
-
|
|
1342
|
+
/**
|
|
1343
|
+
* Reads the archive name and file list out of `npm pack --json` output.
|
|
1344
|
+
*
|
|
1345
|
+
* @remarks
|
|
1346
|
+
* npm 11 prints an array and npm 12 an object keyed by package name.
|
|
1347
|
+
*
|
|
1348
|
+
* @param text - npm's standard output.
|
|
1349
|
+
* @returns The archive's file name and its sorted, forward-slash file paths.
|
|
1350
|
+
* @throws Thrown when the output is not JSON or lacks a file name or a file list.
|
|
1351
|
+
*/
|
|
1352
|
+
export function readPack(text: string): {
|
|
1353
|
+
readonly filename: string
|
|
1354
|
+
readonly files: readonly string[]
|
|
1355
|
+
} {
|
|
816
1356
|
const parsed: unknown = JSON.parse(text)
|
|
817
1357
|
const entry: unknown = Array.isArray(parsed)
|
|
818
1358
|
? parsed[0]
|
|
@@ -831,7 +1371,15 @@ function readPack(text: string): { readonly filename: string; readonly files: re
|
|
|
831
1371
|
return { filename: entry.filename, files: files.toSorted() }
|
|
832
1372
|
}
|
|
833
1373
|
|
|
834
|
-
|
|
1374
|
+
/**
|
|
1375
|
+
* Runs npm and returns its standard output.
|
|
1376
|
+
*
|
|
1377
|
+
* @param args - The npm arguments.
|
|
1378
|
+
* @param cwd - The working directory.
|
|
1379
|
+
* @returns npm's standard output.
|
|
1380
|
+
* @throws Thrown when npm cannot start or exits nonzero, naming the command and the stderr tail.
|
|
1381
|
+
*/
|
|
1382
|
+
export function invokeNpm(args: readonly string[], cwd: string): string {
|
|
835
1383
|
const result = runNpm(args, cwd)
|
|
836
1384
|
if (result.error !== undefined) {
|
|
837
1385
|
throw new Error(`npm ${args.join(' ')} could not start in ${cwd}: ${result.error.message}`)
|
|
@@ -844,7 +1392,13 @@ function invokeNpm(args: readonly string[], cwd: string): string {
|
|
|
844
1392
|
return result.stdout
|
|
845
1393
|
}
|
|
846
1394
|
|
|
847
|
-
|
|
1395
|
+
/**
|
|
1396
|
+
* Reads a checkout's commit and whether it is dirty.
|
|
1397
|
+
*
|
|
1398
|
+
* @param directory - The checkout.
|
|
1399
|
+
* @returns The HEAD commit and the dirty flag, each undefined when git cannot answer.
|
|
1400
|
+
*/
|
|
1401
|
+
export function inspectCheckout(directory: string): {
|
|
848
1402
|
readonly commit: string | undefined
|
|
849
1403
|
readonly dirty: boolean | undefined
|
|
850
1404
|
} {
|
|
@@ -864,20 +1418,46 @@ function inspectCheckout(directory: string): {
|
|
|
864
1418
|
}
|
|
865
1419
|
}
|
|
866
1420
|
|
|
867
|
-
|
|
1421
|
+
/**
|
|
1422
|
+
* Replaces a file by renaming a temporary file onto it.
|
|
1423
|
+
*
|
|
1424
|
+
* @param path - The file.
|
|
1425
|
+
* @param text - Its new text.
|
|
1426
|
+
* @returns Nothing after the rename.
|
|
1427
|
+
* @throws Thrown when the write or the rename fails.
|
|
1428
|
+
*/
|
|
1429
|
+
export function replaceFile(path: string, text: string): void {
|
|
868
1430
|
const temporary = `${path}.${String(process.pid)}.tmp`
|
|
869
1431
|
writeFileSync(temporary, text)
|
|
870
1432
|
renameSync(temporary, path)
|
|
871
1433
|
}
|
|
872
1434
|
|
|
873
|
-
|
|
874
|
-
|
|
1435
|
+
/**
|
|
1436
|
+
* Installs a consumer's dependencies and clears the Vite pre-bundle.
|
|
1437
|
+
*
|
|
1438
|
+
* @remarks
|
|
1439
|
+
* Vite keys its pre-bundle on the installed version, so a same-version tarball would serve stale bytes.
|
|
1440
|
+
*
|
|
1441
|
+
* @param consumer - The consumer directory.
|
|
1442
|
+
* @returns Nothing after the install.
|
|
1443
|
+
* @throws Thrown when the install fails.
|
|
1444
|
+
*/
|
|
1445
|
+
export function installConsumer(consumer: string): void {
|
|
875
1446
|
invokeNpm(['install', '--ignore-scripts', '--no-audit', '--no-fund'], consumer)
|
|
876
1447
|
removeTree(join(consumer, 'node_modules', '.vite'))
|
|
877
1448
|
}
|
|
878
1449
|
|
|
879
|
-
|
|
880
|
-
|
|
1450
|
+
/**
|
|
1451
|
+
* Lists the files of an installed package.
|
|
1452
|
+
*
|
|
1453
|
+
* @remarks
|
|
1454
|
+
* The package's own `node_modules` is skipped, and a link is listed as `path (link)`.
|
|
1455
|
+
*
|
|
1456
|
+
* @param directory - The package directory.
|
|
1457
|
+
* @param prefix - The path of `directory` inside the package. Default: none.
|
|
1458
|
+
* @returns The sorted forward-slash paths.
|
|
1459
|
+
*/
|
|
1460
|
+
export function collectFiles(directory: string, prefix = ''): readonly string[] {
|
|
881
1461
|
return readdirSync(directory, { withFileTypes: true })
|
|
882
1462
|
.flatMap((entry): readonly string[] => {
|
|
883
1463
|
const path = prefix === '' ? entry.name : `${prefix}/${entry.name}`
|
|
@@ -892,7 +1472,13 @@ function collectFiles(directory: string, prefix = ''): readonly string[] {
|
|
|
892
1472
|
.toSorted()
|
|
893
1473
|
}
|
|
894
1474
|
|
|
895
|
-
|
|
1475
|
+
/**
|
|
1476
|
+
* Computes the digest of an installed package's bytes.
|
|
1477
|
+
*
|
|
1478
|
+
* @param directory - The package directory.
|
|
1479
|
+
* @returns The SHA-256 of the sorted path and file-digest lines.
|
|
1480
|
+
*/
|
|
1481
|
+
export function computeInventory(directory: string): string {
|
|
896
1482
|
const lines = collectFiles(directory).map((path) => {
|
|
897
1483
|
const file = join(directory, ...path.split('/'))
|
|
898
1484
|
return `${path}\t${existsSync(file) ? computeHash(readFileSync(file)) : 'unreadable'}`
|
|
@@ -900,7 +1486,14 @@ function computeInventory(directory: string): string {
|
|
|
900
1486
|
return computeHash(Buffer.from(lines.join('\n')))
|
|
901
1487
|
}
|
|
902
1488
|
|
|
903
|
-
|
|
1489
|
+
/**
|
|
1490
|
+
* Computes the digest of every top-level `@orkestrel` package the pin does not name.
|
|
1491
|
+
*
|
|
1492
|
+
* @param consumer - The consumer directory.
|
|
1493
|
+
* @param excluded - The package names to leave out.
|
|
1494
|
+
* @returns The digest of each package by name, `link` for a link, empty when the scope is absent.
|
|
1495
|
+
*/
|
|
1496
|
+
export function computeScope(
|
|
904
1497
|
consumer: string,
|
|
905
1498
|
excluded: ReadonlySet<string>,
|
|
906
1499
|
): Readonly<Record<string, string>> {
|
|
@@ -916,7 +1509,14 @@ function computeScope(
|
|
|
916
1509
|
)
|
|
917
1510
|
}
|
|
918
1511
|
|
|
919
|
-
|
|
1512
|
+
/**
|
|
1513
|
+
* Names the packages an install added, removed, or changed.
|
|
1514
|
+
*
|
|
1515
|
+
* @param before - The scope digests before the install.
|
|
1516
|
+
* @param after - The scope digests after.
|
|
1517
|
+
* @returns One message per package whose digest differs.
|
|
1518
|
+
*/
|
|
1519
|
+
export function describeMoved(
|
|
920
1520
|
before: Readonly<Record<string, string>>,
|
|
921
1521
|
after: Readonly<Record<string, string>>,
|
|
922
1522
|
): readonly string[] {
|
|
@@ -929,8 +1529,17 @@ function describeMoved(
|
|
|
929
1529
|
})
|
|
930
1530
|
}
|
|
931
1531
|
|
|
932
|
-
|
|
933
|
-
|
|
1532
|
+
/**
|
|
1533
|
+
* Finds every installed copy of the named packages.
|
|
1534
|
+
*
|
|
1535
|
+
* @remarks
|
|
1536
|
+
* A link is reported and not entered.
|
|
1537
|
+
*
|
|
1538
|
+
* @param directory - A `node_modules` directory.
|
|
1539
|
+
* @param names - The package names to find.
|
|
1540
|
+
* @returns Each copy at any depth of nested `node_modules`.
|
|
1541
|
+
*/
|
|
1542
|
+
export function scanModules(directory: string, names: ReadonlySet<string>): readonly Copy[] {
|
|
934
1543
|
if (!existsSync(directory)) return []
|
|
935
1544
|
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
936
1545
|
if (entry.name.startsWith('.')) return []
|
|
@@ -959,14 +1568,32 @@ function scanModules(directory: string, names: ReadonlySet<string>): readonly Co
|
|
|
959
1568
|
})
|
|
960
1569
|
}
|
|
961
1570
|
|
|
962
|
-
|
|
1571
|
+
/**
|
|
1572
|
+
* Locates a package's top-level install path.
|
|
1573
|
+
*
|
|
1574
|
+
* @param consumer - The consumer directory.
|
|
1575
|
+
* @param name - The package name.
|
|
1576
|
+
* @returns The path under the consumer's `node_modules`.
|
|
1577
|
+
*/
|
|
1578
|
+
export function locateInstalled(consumer: string, name: string): string {
|
|
963
1579
|
return join(consumer, 'node_modules', ...name.split('/'))
|
|
964
1580
|
}
|
|
965
1581
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1582
|
+
/**
|
|
1583
|
+
* Lists the problems with one pinned package's install.
|
|
1584
|
+
*
|
|
1585
|
+
* @remarks
|
|
1586
|
+
* A source that is itself pinned was packed with its registry ranges, so its installed manifest is
|
|
1587
|
+
* compared to those bytes rather than to the source's own manifest. Every pinned source must be
|
|
1588
|
+
* installed at the top level, so an override that selects no installed dependency fails the proof.
|
|
1589
|
+
*
|
|
1590
|
+
* @param consumer - The consumer directory.
|
|
1591
|
+
* @param tarball - The packed source.
|
|
1592
|
+
* @param declared - If `true`, the consumer declares the package; if `false`, only an override names it.
|
|
1593
|
+
* @param copies - Every installed copy of the pinned packages.
|
|
1594
|
+
* @returns One message per problem, empty when the install proves the pin.
|
|
1595
|
+
*/
|
|
1596
|
+
export function proveInstalled(
|
|
970
1597
|
consumer: string,
|
|
971
1598
|
tarball: Tarball,
|
|
972
1599
|
declared: boolean,
|
|
@@ -1008,16 +1635,33 @@ function proveInstalled(
|
|
|
1008
1635
|
]
|
|
1009
1636
|
}
|
|
1010
1637
|
|
|
1011
|
-
|
|
1638
|
+
/**
|
|
1639
|
+
* Reads the `resolved` address the lockfile records for a package.
|
|
1640
|
+
*
|
|
1641
|
+
* @param consumer - The consumer directory.
|
|
1642
|
+
* @param name - The package name.
|
|
1643
|
+
* @returns The address, or undefined when the lockfile is absent or records none.
|
|
1644
|
+
*/
|
|
1645
|
+
export function loadResolved(consumer: string, name: string): string | undefined {
|
|
1012
1646
|
const packages = loadJSONRecord(join(consumer, 'package-lock.json'))?.packages
|
|
1013
1647
|
const entry = isRecord(packages) ? packages[`node_modules/${name}`] : undefined
|
|
1014
1648
|
return readString(isRecord(entry) ? entry : undefined, 'resolved')
|
|
1015
1649
|
}
|
|
1016
1650
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1651
|
+
/**
|
|
1652
|
+
* Lists the problems with one restored package's install.
|
|
1653
|
+
*
|
|
1654
|
+
* @remarks
|
|
1655
|
+
* A registry range needs a real directory whose version the range accepts and a lockfile `resolved`
|
|
1656
|
+
* that is a registry URL; a `file:` directory range needs a link to that directory or a directory of
|
|
1657
|
+
* its version; any other spec is reported as unproven.
|
|
1658
|
+
*
|
|
1659
|
+
* @param consumer - The consumer directory.
|
|
1660
|
+
* @param name - The package name.
|
|
1661
|
+
* @param range - The restored range.
|
|
1662
|
+
* @returns One message per problem, empty when the install proves the restore.
|
|
1663
|
+
*/
|
|
1664
|
+
export function proveRestored(consumer: string, name: string, range: string): readonly string[] {
|
|
1021
1665
|
const installed = locateInstalled(consumer, name)
|
|
1022
1666
|
if (!existsSync(installed)) return [`${name} is not installed`]
|
|
1023
1667
|
const link = lstatSync(installed).isSymbolicLink()
|
|
@@ -1044,29 +1688,65 @@ function proveRestored(consumer: string, name: string, range: string): readonly
|
|
|
1044
1688
|
]
|
|
1045
1689
|
}
|
|
1046
1690
|
|
|
1047
|
-
|
|
1691
|
+
/**
|
|
1692
|
+
* Locates a consumer's ledger file.
|
|
1693
|
+
*
|
|
1694
|
+
* @param consumer - The consumer directory.
|
|
1695
|
+
* @returns The ledger's path.
|
|
1696
|
+
*/
|
|
1697
|
+
export function locateLedger(consumer: string): string {
|
|
1048
1698
|
return join(consumer, ...TARBALL_DIRECTORY.split('/'), LEDGER_FILE)
|
|
1049
1699
|
}
|
|
1050
1700
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1701
|
+
/**
|
|
1702
|
+
* Loads a consumer's ledger.
|
|
1703
|
+
*
|
|
1704
|
+
* @remarks
|
|
1705
|
+
* An absent ledger is an empty one; a malformed ledger is undefined.
|
|
1706
|
+
*
|
|
1707
|
+
* @param consumer - The consumer directory.
|
|
1708
|
+
* @returns The ledger, or undefined when the file is malformed.
|
|
1709
|
+
*/
|
|
1710
|
+
export function loadLedger(consumer: string): Ledger | undefined {
|
|
1053
1711
|
const path = locateLedger(consumer)
|
|
1054
1712
|
if (!existsSync(path)) return { packages: {}, overrides: {} }
|
|
1055
1713
|
return parseLedger(loadJSONRecord(path))
|
|
1056
1714
|
}
|
|
1057
1715
|
|
|
1058
|
-
|
|
1716
|
+
/**
|
|
1717
|
+
* Reads a consumer's ledger.
|
|
1718
|
+
*
|
|
1719
|
+
* @param consumer - The consumer directory.
|
|
1720
|
+
* @returns The ledger; an absent file reads as an empty one.
|
|
1721
|
+
* @throws Thrown when the ledger file is malformed.
|
|
1722
|
+
*/
|
|
1723
|
+
export function readLedger(consumer: string): Ledger {
|
|
1059
1724
|
const ledger = loadLedger(consumer)
|
|
1060
1725
|
if (ledger === undefined)
|
|
1061
1726
|
throw new Error(`${normalizeSlashes(locateLedger(consumer))} is malformed`)
|
|
1062
1727
|
return ledger
|
|
1063
1728
|
}
|
|
1064
1729
|
|
|
1065
|
-
|
|
1730
|
+
/**
|
|
1731
|
+
* Renders a time as a file-name stamp.
|
|
1732
|
+
*
|
|
1733
|
+
* @param at - An ISO time.
|
|
1734
|
+
* @returns The time with each colon and dot replaced by a dash.
|
|
1735
|
+
*/
|
|
1736
|
+
export function renderStamp(at: string): string {
|
|
1066
1737
|
return at.replace(/[:.]/gu, '-')
|
|
1067
1738
|
}
|
|
1068
1739
|
|
|
1069
|
-
|
|
1740
|
+
/**
|
|
1741
|
+
* Writes a receipt create-only under the working directory.
|
|
1742
|
+
*
|
|
1743
|
+
* @param consumer - The consumer directory, whose name the file carries.
|
|
1744
|
+
* @param at - The time of the run.
|
|
1745
|
+
* @param receipt - The receipt record.
|
|
1746
|
+
* @returns The receipt's forward-slash path.
|
|
1747
|
+
* @throws Thrown when the file already exists or cannot be written.
|
|
1748
|
+
*/
|
|
1749
|
+
export function writeReceipt(
|
|
1070
1750
|
consumer: string,
|
|
1071
1751
|
at: string,
|
|
1072
1752
|
receipt: Readonly<Record<string, unknown>>,
|
|
@@ -1077,7 +1757,15 @@ function writeReceipt(
|
|
|
1077
1757
|
return normalizeSlashes(path)
|
|
1078
1758
|
}
|
|
1079
1759
|
|
|
1080
|
-
|
|
1760
|
+
/**
|
|
1761
|
+
* Prints a report.
|
|
1762
|
+
*
|
|
1763
|
+
* @param json - If `true`, prints the report as one JSON line; if `false`, prints the text lines.
|
|
1764
|
+
* @param lines - The text lines.
|
|
1765
|
+
* @param report - The JSON report.
|
|
1766
|
+
* @returns Nothing after the write.
|
|
1767
|
+
*/
|
|
1768
|
+
export function printReport(
|
|
1081
1769
|
json: boolean,
|
|
1082
1770
|
lines: readonly string[],
|
|
1083
1771
|
report: Readonly<Record<string, unknown>>,
|
|
@@ -1085,43 +1773,86 @@ function printReport(
|
|
|
1085
1773
|
process.stdout.write(json ? `${JSON.stringify(report)}\n` : `${lines.join('\n')}\n`)
|
|
1086
1774
|
}
|
|
1087
1775
|
|
|
1088
|
-
|
|
1776
|
+
/**
|
|
1777
|
+
* Prints a usage refusal.
|
|
1778
|
+
*
|
|
1779
|
+
* @param message - What was refused.
|
|
1780
|
+
* @returns The usage exit code.
|
|
1781
|
+
*/
|
|
1782
|
+
export function reportUsage(message: string): number {
|
|
1089
1783
|
process.stderr.write(`swap: ${message}\n${USAGE}\n`)
|
|
1090
1784
|
return EXIT_USAGE
|
|
1091
1785
|
}
|
|
1092
1786
|
|
|
1093
|
-
|
|
1787
|
+
/**
|
|
1788
|
+
* Checks whether a ledger records any pin.
|
|
1789
|
+
*
|
|
1790
|
+
* @param ledger - The ledger.
|
|
1791
|
+
* @returns True if it holds a package or an override row; false otherwise.
|
|
1792
|
+
*/
|
|
1793
|
+
export function hasPins(ledger: Ledger): boolean {
|
|
1094
1794
|
return Object.keys(ledger.packages).length > 0 || Object.keys(ledger.overrides).length > 0
|
|
1095
1795
|
}
|
|
1096
1796
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1797
|
+
/**
|
|
1798
|
+
* Loads the manifest a source is packed with.
|
|
1799
|
+
*
|
|
1800
|
+
* @remarks
|
|
1801
|
+
* It is the source's own, with the ranges and overrides its ledger replaced put back when it carries
|
|
1802
|
+
* pins.
|
|
1803
|
+
*
|
|
1804
|
+
* @param directory - The source directory.
|
|
1805
|
+
* @returns The manifest to pack.
|
|
1806
|
+
* @throws Thrown when the manifest is unreadable or the ledger is malformed.
|
|
1807
|
+
*/
|
|
1808
|
+
export function loadPackedManifest(directory: string): Manifest {
|
|
1100
1809
|
const manifest = loadJSONRecord(join(directory, 'package.json'))
|
|
1101
1810
|
if (manifest === undefined) throw new Error(`${directory}/package.json is unreadable`)
|
|
1102
1811
|
const ledger = readLedger(directory)
|
|
1103
1812
|
return hasPins(ledger) ? planRestore(manifest, ledger, []).manifest : manifest
|
|
1104
1813
|
}
|
|
1105
1814
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1815
|
+
/**
|
|
1816
|
+
* Reads the text a pinned source's manifest is rewritten to for the pack.
|
|
1817
|
+
*
|
|
1818
|
+
* @param directory - The source directory.
|
|
1819
|
+
* @returns The text, or undefined when the source carries no pins and packs its own manifest.
|
|
1820
|
+
*/
|
|
1821
|
+
export function readPackedManifest(directory: string): string | undefined {
|
|
1109
1822
|
return hasPins(readLedger(directory)) ? serializeJSON(loadPackedManifest(directory)) : undefined
|
|
1110
1823
|
}
|
|
1111
1824
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1825
|
+
/**
|
|
1826
|
+
* Lists the local dependency specs a source would pack.
|
|
1827
|
+
*
|
|
1828
|
+
* @remarks
|
|
1829
|
+
* A source that packs a local dependency spec ships a tarball that resolves only on this host,
|
|
1830
|
+
* whether or not a ledger recorded the spec.
|
|
1831
|
+
*
|
|
1832
|
+
* @param source - The source.
|
|
1833
|
+
* @returns One message per local spec in the manifest the source packs.
|
|
1834
|
+
*/
|
|
1835
|
+
export function scanPackedSpecs(source: Source): readonly string[] {
|
|
1115
1836
|
const manifest = loadPackedManifest(source.directory)
|
|
1116
1837
|
return scanLocalSpecs(manifest).map(
|
|
1117
1838
|
([path, spec]) => `${source.name} would pack a local dependency spec: ${path} is ${spec}`,
|
|
1118
1839
|
)
|
|
1119
1840
|
}
|
|
1120
1841
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1842
|
+
/**
|
|
1843
|
+
* Packs a source.
|
|
1844
|
+
*
|
|
1845
|
+
* @remarks
|
|
1846
|
+
* A source that carries a ledger of its own has its manifest rewritten to the recorded ranges for
|
|
1847
|
+
* the pack and restored byte for byte whether the pack succeeds or throws, so a consumer never
|
|
1848
|
+
* receives a tarball spec that resolves only inside the source.
|
|
1849
|
+
*
|
|
1850
|
+
* @param directory - The source directory.
|
|
1851
|
+
* @param scratch - The directory that receives the archive.
|
|
1852
|
+
* @returns npm's `pack --json` output.
|
|
1853
|
+
* @throws Thrown when the pack fails.
|
|
1854
|
+
*/
|
|
1855
|
+
export function packSource(directory: string, scratch: string): string {
|
|
1125
1856
|
const args = ['pack', '--ignore-scripts', '--json', '--pack-destination', scratch]
|
|
1126
1857
|
const packed = readPackedManifest(directory)
|
|
1127
1858
|
if (packed === undefined) return invokeNpm(args, directory)
|
|
@@ -1135,7 +1866,16 @@ function packSource(directory: string, scratch: string): string {
|
|
|
1135
1866
|
}
|
|
1136
1867
|
}
|
|
1137
1868
|
|
|
1138
|
-
|
|
1869
|
+
/**
|
|
1870
|
+
* Builds and packs each source into the consumer's tarball directory.
|
|
1871
|
+
*
|
|
1872
|
+
* @param sources - The sources.
|
|
1873
|
+
* @param build - If `true`, runs `npm run build` in each source first; if `false`, packs it as is.
|
|
1874
|
+
* @param consumer - The consumer directory.
|
|
1875
|
+
* @returns One packed tarball per source.
|
|
1876
|
+
* @throws Thrown when a build or a pack fails.
|
|
1877
|
+
*/
|
|
1878
|
+
export function packSources(
|
|
1139
1879
|
sources: readonly Source[],
|
|
1140
1880
|
build: boolean,
|
|
1141
1881
|
consumer: string,
|
|
@@ -1165,7 +1905,14 @@ function packSources(
|
|
|
1165
1905
|
}
|
|
1166
1906
|
}
|
|
1167
1907
|
|
|
1168
|
-
|
|
1908
|
+
/**
|
|
1909
|
+
* Plans the tarballs of a dry run without building or packing.
|
|
1910
|
+
*
|
|
1911
|
+
* @param sources - The sources.
|
|
1912
|
+
* @param consumer - The consumer directory.
|
|
1913
|
+
* @returns One planned tarball per source, with a pending hash and no files.
|
|
1914
|
+
*/
|
|
1915
|
+
export function planTarballs(sources: readonly Source[], consumer: string): readonly Tarball[] {
|
|
1169
1916
|
return sources.map((source) => {
|
|
1170
1917
|
const file = buildTarballName(source.name, source.version, '<sha256:12>')
|
|
1171
1918
|
return {
|
|
@@ -1179,12 +1926,32 @@ function planTarballs(sources: readonly Source[], consumer: string): readonly Ta
|
|
|
1179
1926
|
})
|
|
1180
1927
|
}
|
|
1181
1928
|
|
|
1182
|
-
|
|
1929
|
+
/**
|
|
1930
|
+
* Renders a manifest change as a text line.
|
|
1931
|
+
*
|
|
1932
|
+
* @param change - The change.
|
|
1933
|
+
* @returns The line, naming the block, the package, and both specs.
|
|
1934
|
+
*/
|
|
1935
|
+
export function renderChange(change: Change): string {
|
|
1183
1936
|
return `swap: ${change.block}.${change.name} ${change.from ?? '(absent)'} -> ${change.to ?? '(absent)'}`
|
|
1184
1937
|
}
|
|
1185
1938
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1939
|
+
/**
|
|
1940
|
+
* Records the digest of each proved install in the ledger.
|
|
1941
|
+
*
|
|
1942
|
+
* @remarks
|
|
1943
|
+
* A proved pin records the digest of its installed bytes, which `--status` compares the install to.
|
|
1944
|
+
*
|
|
1945
|
+
* @param consumer - The consumer directory.
|
|
1946
|
+
* @param ledger - The ledger.
|
|
1947
|
+
* @param tarballs - The pinned tarballs.
|
|
1948
|
+
* @returns The ledger with an inventory on each package that is installed as a real directory.
|
|
1949
|
+
*/
|
|
1950
|
+
export function recordInventory(
|
|
1951
|
+
consumer: string,
|
|
1952
|
+
ledger: Ledger,
|
|
1953
|
+
tarballs: readonly Tarball[],
|
|
1954
|
+
): Ledger {
|
|
1188
1955
|
const proved = new Map(
|
|
1189
1956
|
tarballs.flatMap((tarball): Array<[string, string]> => {
|
|
1190
1957
|
const top = locateInstalled(consumer, tarball.source.name)
|
|
@@ -1204,7 +1971,14 @@ function recordInventory(consumer: string, ledger: Ledger, tarballs: readonly Ta
|
|
|
1204
1971
|
}
|
|
1205
1972
|
}
|
|
1206
1973
|
|
|
1207
|
-
|
|
1974
|
+
/**
|
|
1975
|
+
* Runs the pin mode.
|
|
1976
|
+
*
|
|
1977
|
+
* @param command - The parsed command.
|
|
1978
|
+
* @returns 0 when the pin installed and proved, 1 when a proof failed, 64 on a usage refusal.
|
|
1979
|
+
* @throws Thrown when a build, a pack, or the install fails.
|
|
1980
|
+
*/
|
|
1981
|
+
export function executePin(command: Command): number {
|
|
1208
1982
|
const manifestPath = join(command.consumer, 'package.json')
|
|
1209
1983
|
if (!existsSync(manifestPath)) return reportUsage(`${command.consumer} has no package.json`)
|
|
1210
1984
|
const text = readFileSync(manifestPath, 'utf8')
|
|
@@ -1308,9 +2082,18 @@ function executePin(command: Command): number {
|
|
|
1308
2082
|
return failures.length === 0 ? 0 : EXIT_FAILURE
|
|
1309
2083
|
}
|
|
1310
2084
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
2085
|
+
/**
|
|
2086
|
+
* Builds the arguments that pin again what a restore undid.
|
|
2087
|
+
*
|
|
2088
|
+
* @remarks
|
|
2089
|
+
* The re-pin arguments name every source the restored rows recorded and every override they
|
|
2090
|
+
* dropped, so the pins can come back after a verb that needed the registry ranges.
|
|
2091
|
+
*
|
|
2092
|
+
* @param consumer - The consumer directory.
|
|
2093
|
+
* @param plan - The restore plan.
|
|
2094
|
+
* @returns The `--pin` argument list.
|
|
2095
|
+
*/
|
|
2096
|
+
export function buildRepin(consumer: string, plan: RestorePlan): readonly string[] {
|
|
1314
2097
|
return [
|
|
1315
2098
|
'--pin',
|
|
1316
2099
|
'--consumer',
|
|
@@ -1320,10 +2103,19 @@ function buildRepin(consumer: string, plan: RestorePlan): readonly string[] {
|
|
|
1320
2103
|
]
|
|
1321
2104
|
}
|
|
1322
2105
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
2106
|
+
/**
|
|
2107
|
+
* Removes the lock entries and installed copies of the named packages.
|
|
2108
|
+
*
|
|
2109
|
+
* @remarks
|
|
2110
|
+
* npm keeps a lock entry whose version satisfies the restored range, so the tarball `resolved` of a
|
|
2111
|
+
* pinned package outlives the restore; removing the entry and the installed copy makes the install
|
|
2112
|
+
* resolve the package again from the registry.
|
|
2113
|
+
*
|
|
2114
|
+
* @param consumer - The consumer directory.
|
|
2115
|
+
* @param names - The package names.
|
|
2116
|
+
* @returns Nothing after the removal.
|
|
2117
|
+
*/
|
|
2118
|
+
export function removePinnedInstalls(consumer: string, names: readonly string[]): void {
|
|
1327
2119
|
const path = join(consumer, 'package-lock.json')
|
|
1328
2120
|
const text = existsSync(path) ? readFileSync(path, 'utf8') : undefined
|
|
1329
2121
|
const lock = text === undefined ? undefined : parseJsonRecord(text)
|
|
@@ -1340,9 +2132,13 @@ function removePinnedInstalls(consumer: string, names: readonly string[]): void
|
|
|
1340
2132
|
for (const name of names) removeTree(locateInstalled(consumer, name))
|
|
1341
2133
|
}
|
|
1342
2134
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
2135
|
+
/**
|
|
2136
|
+
* Lists the packages whose lock entry and installed copy a restore removes.
|
|
2137
|
+
*
|
|
2138
|
+
* @param plan - The restore plan.
|
|
2139
|
+
* @returns Each restored row that returns to a registry range, and each dropped override.
|
|
2140
|
+
*/
|
|
2141
|
+
export function listRemovals(plan: RestorePlan): readonly string[] {
|
|
1346
2142
|
return [
|
|
1347
2143
|
...new Set([
|
|
1348
2144
|
...plan.rows.flatMap((row) =>
|
|
@@ -1357,24 +2153,49 @@ function listRemovals(plan: RestorePlan): readonly string[] {
|
|
|
1357
2153
|
]
|
|
1358
2154
|
}
|
|
1359
2155
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
2156
|
+
/**
|
|
2157
|
+
* Lists the ledger keys that are not package names.
|
|
2158
|
+
*
|
|
2159
|
+
* @remarks
|
|
2160
|
+
* Every ledger key becomes a manifest key, a lock key, and an install path, so each must be a package
|
|
2161
|
+
* name before the restore writes anything.
|
|
2162
|
+
*
|
|
2163
|
+
* @param ledger - The ledger.
|
|
2164
|
+
* @returns One message per package or override key that is not a package name.
|
|
2165
|
+
*/
|
|
2166
|
+
export function scanLedgerNames(ledger: Ledger): readonly string[] {
|
|
1363
2167
|
return [
|
|
1364
2168
|
...scanNames('packages', Object.keys(ledger.packages)),
|
|
1365
2169
|
...scanNames('overrides', Object.keys(ledger.overrides)),
|
|
1366
2170
|
]
|
|
1367
2171
|
}
|
|
1368
2172
|
|
|
1369
|
-
|
|
2173
|
+
/**
|
|
2174
|
+
* Lists the keys of one ledger block that are not package names.
|
|
2175
|
+
*
|
|
2176
|
+
* @param block - The block's name.
|
|
2177
|
+
* @param names - The block's keys.
|
|
2178
|
+
* @returns One message per key that is not a package name, with the key JSON-encoded.
|
|
2179
|
+
*/
|
|
2180
|
+
export function scanNames(block: string, names: readonly string[]): readonly string[] {
|
|
1370
2181
|
return names
|
|
1371
2182
|
.filter((name) => !isPackageName(name))
|
|
1372
2183
|
.map((name) => `The ledger ${block} key ${JSON.stringify(name)} is not a package name`)
|
|
1373
2184
|
}
|
|
1374
2185
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
2186
|
+
/**
|
|
2187
|
+
* Checks whether a target stays inside a root.
|
|
2188
|
+
*
|
|
2189
|
+
* @remarks
|
|
2190
|
+
* A name that passes the package-name rules still resolves outside `node_modules` when a scope
|
|
2191
|
+
* directory on its way is a link, so the target's real parent is compared to the real `node_modules`.
|
|
2192
|
+
*
|
|
2193
|
+
* @param root - The `node_modules` directory.
|
|
2194
|
+
* @param target - The path to remove.
|
|
2195
|
+
* @returns True if the target's real location is strictly inside the root's real location; false
|
|
2196
|
+
* otherwise.
|
|
2197
|
+
*/
|
|
2198
|
+
export function isContained(root: string, target: string): boolean {
|
|
1378
2199
|
const parent = dirname(target)
|
|
1379
2200
|
const inside = relative(
|
|
1380
2201
|
existsSync(root) ? realpathSync(root) : resolve(root),
|
|
@@ -1383,14 +2204,28 @@ function isContained(root: string, target: string): boolean {
|
|
|
1383
2204
|
return inside !== '' && !isAbsolute(inside) && inside.split(/[\\/]/u)[0] !== '..'
|
|
1384
2205
|
}
|
|
1385
2206
|
|
|
1386
|
-
|
|
2207
|
+
/**
|
|
2208
|
+
* Lists the removals that would leave `node_modules`.
|
|
2209
|
+
*
|
|
2210
|
+
* @param consumer - The consumer directory.
|
|
2211
|
+
* @param names - The packages to remove.
|
|
2212
|
+
* @returns One message per package whose install path resolves outside `node_modules`.
|
|
2213
|
+
*/
|
|
2214
|
+
export function scanRemovals(consumer: string, names: readonly string[]): readonly string[] {
|
|
1387
2215
|
const root = join(consumer, 'node_modules')
|
|
1388
2216
|
return names
|
|
1389
2217
|
.filter((name) => !isContained(root, locateInstalled(consumer, name)))
|
|
1390
2218
|
.map((name) => `The install of ${name} resolves outside node_modules`)
|
|
1391
2219
|
}
|
|
1392
2220
|
|
|
1393
|
-
|
|
2221
|
+
/**
|
|
2222
|
+
* Runs the restore mode.
|
|
2223
|
+
*
|
|
2224
|
+
* @param command - The parsed command.
|
|
2225
|
+
* @returns 0 when the restore installed and proved, 1 when a proof failed, 64 on a usage refusal.
|
|
2226
|
+
* @throws Thrown when the ledger is malformed or the install fails.
|
|
2227
|
+
*/
|
|
2228
|
+
export function executeRestore(command: Command): number {
|
|
1394
2229
|
const manifestPath = join(command.consumer, 'package.json')
|
|
1395
2230
|
if (!existsSync(manifestPath)) return reportUsage(`${command.consumer} has no package.json`)
|
|
1396
2231
|
const text = readFileSync(manifestPath, 'utf8')
|
|
@@ -1408,8 +2243,8 @@ function executeRestore(command: Command): number {
|
|
|
1408
2243
|
const at = new Date().toISOString()
|
|
1409
2244
|
const plan = planRestore(manifest, ledger, command.packages)
|
|
1410
2245
|
const removals = listRemovals(plan)
|
|
1411
|
-
const
|
|
1412
|
-
if (
|
|
2246
|
+
const refusals = scanRemovals(command.consumer, removals)
|
|
2247
|
+
if (refusals.length > 0) return reportUsage(refusals.join('\n'))
|
|
1413
2248
|
replaceFile(manifestPath, serializeJSON(plan.manifest))
|
|
1414
2249
|
replaceFile(locateLedger(command.consumer), serializeJSON(plan.ledger))
|
|
1415
2250
|
removePinnedInstalls(command.consumer, removals)
|
|
@@ -1461,13 +2296,32 @@ function executeRestore(command: Command): number {
|
|
|
1461
2296
|
return failures.length === 0 ? 0 : EXIT_FAILURE
|
|
1462
2297
|
}
|
|
1463
2298
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
2299
|
+
/**
|
|
2300
|
+
* Resolves a manifest tarball spec to the location it names.
|
|
2301
|
+
*
|
|
2302
|
+
* @remarks
|
|
2303
|
+
* Relative specs resolve against the consumer, so a spec holds its tarball only when it names the
|
|
2304
|
+
* ledger archive itself.
|
|
2305
|
+
*
|
|
2306
|
+
* @param consumer - The consumer directory.
|
|
2307
|
+
* @param spec - A `file:` spec.
|
|
2308
|
+
* @returns The location with forward slashes.
|
|
2309
|
+
*/
|
|
2310
|
+
export function locateSpecTarget(consumer: string, spec: string): string {
|
|
1467
2311
|
return normalizeSlashes(resolve(consumer, spec.slice('file:'.length)))
|
|
1468
2312
|
}
|
|
1469
2313
|
|
|
1470
|
-
|
|
2314
|
+
/**
|
|
2315
|
+
* Lists the drifts one pinned package reports.
|
|
2316
|
+
*
|
|
2317
|
+
* @param name - The package name.
|
|
2318
|
+
* @param entry - Its ledger row.
|
|
2319
|
+
* @param archive - The ledger archive's forward-slash location.
|
|
2320
|
+
* @param report - What the status read: whether the manifest holds the archive, whether the archive
|
|
2321
|
+
* matches its recorded digest, how the package is installed, and whether its source commit moved.
|
|
2322
|
+
* @returns One message per drift, empty when the pin holds.
|
|
2323
|
+
*/
|
|
2324
|
+
export function describeStatus(
|
|
1471
2325
|
name: string,
|
|
1472
2326
|
entry: LedgerEntry,
|
|
1473
2327
|
archive: string,
|
|
@@ -1497,7 +2351,14 @@ function describeStatus(
|
|
|
1497
2351
|
]
|
|
1498
2352
|
}
|
|
1499
2353
|
|
|
1500
|
-
|
|
2354
|
+
/**
|
|
2355
|
+
* Runs the status mode.
|
|
2356
|
+
*
|
|
2357
|
+
* @param command - The parsed command.
|
|
2358
|
+
* @returns 0 when nothing drifted, 3 when something did, 64 on a usage refusal.
|
|
2359
|
+
* @throws Thrown when the ledger is malformed.
|
|
2360
|
+
*/
|
|
2361
|
+
export function executeStatus(command: Command): number {
|
|
1501
2362
|
const manifest = loadJSONRecord(join(command.consumer, 'package.json'))
|
|
1502
2363
|
if (manifest === undefined) return reportUsage(`${command.consumer} has no readable package.json`)
|
|
1503
2364
|
const ledger = readLedger(command.consumer)
|
|
@@ -1595,7 +2456,14 @@ function executeStatus(command: Command): number {
|
|
|
1595
2456
|
return problems.length === 0 ? 0 : EXIT_DRIFT
|
|
1596
2457
|
}
|
|
1597
2458
|
|
|
1598
|
-
|
|
2459
|
+
/**
|
|
2460
|
+
* Runs the mode the arguments name.
|
|
2461
|
+
*
|
|
2462
|
+
* @param argv - The arguments after the script path.
|
|
2463
|
+
* @returns The exit code: 0 on success, 1 when a command or a proof failed, 3 when the status reports
|
|
2464
|
+
* drift, 64 on usage.
|
|
2465
|
+
*/
|
|
2466
|
+
export function main(argv: readonly string[]): number {
|
|
1599
2467
|
const parsed = parseCommand(argv)
|
|
1600
2468
|
if (!parsed.ok) return reportUsage(parsed.message)
|
|
1601
2469
|
try {
|
|
@@ -1608,4 +2476,6 @@ function main(argv: readonly string[]): number {
|
|
|
1608
2476
|
}
|
|
1609
2477
|
}
|
|
1610
2478
|
|
|
1611
|
-
process.
|
|
2479
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
2480
|
+
process.exitCode = main(process.argv.slice(2))
|
|
2481
|
+
}
|