@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.
- 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 +368 -27
- 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 +26 -20
- package/dist/host/agents/orchestration.md +98 -58
- package/dist/host/agents/platforms/posix.md +15 -0
- package/dist/host/agents/platforms/windows.md +37 -0
- 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 +12 -14
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +36 -24
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +14 -11
- 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 +428 -27
- 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 +91 -30
- package/dist/host/agents/skills/orkestrel-parallel/agents/openai.yaml +2 -2
- package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +34 -0
- package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +29 -0
- package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +29 -0
- package/dist/host/agents/skills/orkestrel-parallel/references/t3.md +31 -0
- 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 +12 -10
- 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 +3 -8
- package/dist/host/agents/transports/cursor.md +2 -3
- package/dist/host/claude/AGENTS.md +7 -10
- 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 +55 -0
- package/dist/host/claude/rules/documentation.md +26 -11
- 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 +2 -3
- 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 +8 -10
- package/dist/host/claude/rules/workspace.md +21 -19
- package/dist/host/claude/rules/writing.md +3 -3
- 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-parallel/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/codex/config.toml +3 -3
- package/dist/host/configs/helpers.ts +21 -1
- package/dist/host/configs/policy.ts +405 -16
- package/dist/host/cursor/rules/orchestration.mdc +1 -0
- package/dist/host/dotfiles/oxlintrc.json +141 -5
- 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 +152 -19
- package/dist/host/guides/study.md +534 -440
- package/dist/host/guides/test.md +111 -44
- package/dist/host/manifest.json +199 -242
- package/dist/host/tests/config.test.ts +1558 -219
- package/dist/host/tests/policy.test.ts +127 -1
- package/dist/host/tests/setupPolicy.ts +1389 -39
- package/dist/src/core/index.cjs +239 -87
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +123 -39
- package/dist/src/core/index.d.ts +123 -39
- package/dist/src/core/index.js +234 -86
- 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/agents/skills/orkestrel-parallel/references/harnesses.md +0 -19
- 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
|
-
|
|
42
|
-
const FIELDS = ['dependencies', 'devDependencies'];
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
613
|
-
|
|
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
|
-
|
|
618
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
663
|
-
|
|
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
|
-
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
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
|
-
|
|
770
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
883
|
-
|
|
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
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1071
|
-
if (
|
|
1072
|
-
return reportUsage(
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2000
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
2001
|
+
process.exitCode = main(process.argv.slice(2));
|
|
2002
|
+
}
|