rigline 1.0.0-alpha.12 → 1.0.0-alpha.14
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/README.md +25 -15
- package/dist/commands.js +62 -0
- package/dist/engine.js +89 -31
- package/dist/floor.js +21 -0
- package/dist/index.js +9 -106
- package/dist/lock.js +3 -2
- package/dist/main.js +40 -0
- package/dist/registry.js +3 -43
- package/package.json +1 -1
- package/dist/remote.js +0 -132
- package/dist/tarball.js +0 -125
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@ rather than a breakage to chase.
|
|
|
10
10
|
|
|
11
11
|
**This modifies files belonging to Anthropic's extension, on your machine, and that is your call to
|
|
12
12
|
make.** [Anthropic compliance](https://github.com/Rigline/Rigline/blob/main/docs/anthropic-compliance.md)
|
|
13
|
-
is a straight account of what Rigline does and does not
|
|
14
|
-
rerouting of Claude usage, no redistribution — and how we read
|
|
13
|
+
is a straight account of what Rigline does to that extension and what it does not — no credentials,
|
|
14
|
+
no network calls from the panel, no rerouting of Claude usage, no redistribution — and how we read
|
|
15
|
+
Anthropic's terms. `rigline restore`
|
|
15
16
|
puts every install back to Anthropic's own bytes and needs nothing but Node.
|
|
16
17
|
|
|
17
18
|
## Install
|
|
@@ -21,7 +22,8 @@ puts every install back to Anthropic's own bytes and needs nothing but Node.
|
|
|
21
22
|
|
|
22
23
|
The first command you run fetches the engine, [`@rigline/core`](https://www.npmjs.com/package/@rigline/core),
|
|
23
24
|
into `~/.rigline/engine` and runs it from there. It takes a few seconds and needs the registry;
|
|
24
|
-
after that nothing reaches the network unless you ask it to
|
|
25
|
+
after that nothing reaches the network unless you ask it to, or install the companion below, which
|
|
26
|
+
asks the registry for a newer engine each time a window starts. If that directory is ever in a bad
|
|
25
27
|
state, delete it and run any command again.
|
|
26
28
|
|
|
27
29
|
`install` finds every installed version of the extension, harvests it, bakes the discovered plugins
|
|
@@ -34,14 +36,20 @@ directory until *Developer: Reload Window*.
|
|
|
34
36
|
|
|
35
37
|
**That is the whole of Rigline, and it needs nothing else.** Everything below is optional.
|
|
36
38
|
|
|
39
|
+
**Where it works:** VS Code on Windows, Linux and macOS, in a local window, with Node 22.12 or
|
|
40
|
+
newer. **Not supported yet:** remote windows, other editors built on VS Code, a portable VS Code, and
|
|
41
|
+
a custom extensions directory. In any of them, Claude Code's panel stays as it ships.
|
|
42
|
+
[Where Rigline works](https://github.com/Rigline/Rigline/blob/main/docs/support.md) lists each, and
|
|
43
|
+
how to ask for one.
|
|
44
|
+
|
|
37
45
|
## Optional: let an extension do the remembering
|
|
38
46
|
|
|
39
47
|
rigline vscode-setup
|
|
40
48
|
|
|
41
49
|
If running `install` after every update is a chore, this installs a companion extension that does it
|
|
42
|
-
for you — into
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
for you — into VS Code, found on your `PATH`, from a VSIX that ships inside the engine. Nothing is
|
|
51
|
+
downloaded, and the companion moves when the engine does. It injects as it goes, so it is a step
|
|
52
|
+
*instead of* `install` rather than after it.
|
|
45
53
|
|
|
46
54
|
From then on it watches for the extension update and re-injects behind it, so there is nothing to
|
|
47
55
|
remember after one. It goes into every VS Code profile that has Claude Code, and into any profile
|
|
@@ -58,11 +66,13 @@ your injection alone, and `rigline restore` is what undoes the injection itself.
|
|
|
58
66
|
|
|
59
67
|
rigline install inject every version, bake plugins, report drift, record the baseline
|
|
60
68
|
rigline check the same report, writing nothing
|
|
69
|
+
rigline watch install, and again whenever an extension update lands, until stopped
|
|
61
70
|
rigline status per version: vanilla or patched, by backup
|
|
62
71
|
rigline restore every version back to the extension's own bytes
|
|
72
|
+
rigline vscode-setup install the companion into VS Code; --remove takes it out
|
|
63
73
|
|
|
64
74
|
rigline add SPEC install a plugin from a directory or npm, and re-inject
|
|
65
|
-
rigline remove N delete a plugin rigline
|
|
75
|
+
rigline remove N delete a plugin from ~/.rigline/plugins, and re-inject
|
|
66
76
|
rigline disable N switch a plugin off, and re-inject
|
|
67
77
|
rigline enable N switch it back on, and re-inject
|
|
68
78
|
rigline update move the engine and every npm plugin to what its tag resolves to
|
|
@@ -79,17 +89,17 @@ your injection alone, and `rigline restore` is what undoes the injection itself.
|
|
|
79
89
|
rigline --help the whole surface, from the engine
|
|
80
90
|
|
|
81
91
|
`add` never runs a package manager for a plugin: a published plugin is one bundled ES module and a
|
|
82
|
-
manifest, so there is nothing to resolve. A version must
|
|
83
|
-
|
|
84
|
-
silence. `update` applies the same rule to the engine, and says which version it moved from so
|
|
92
|
+
manifest, so there is nothing to resolve. A version must be a day old before `add` or `update` will
|
|
93
|
+
take it, unless you pass `--now`, and a withheld version is named rather than skipped in silence. `update` applies the same rule to the engine, and says which version it moved from so
|
|
85
94
|
going back is one command.
|
|
86
95
|
|
|
87
96
|
## How it is put together
|
|
88
97
|
|
|
89
|
-
`rigline` is a small retrieval layer. It
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
`rigline` is a small retrieval layer. It installs the engine and runs it, and `@rigline/core` answers
|
|
99
|
+
every verb above, fetching plugins included. The split is not decoration: a process cannot replace
|
|
100
|
+
the package it is running out of, so `update` moves the engine from here and then hands the rest to
|
|
101
|
+
it. Nothing updates `rigline` itself, so when the engine you have is newer, `rigline` says so and
|
|
102
|
+
names the command.
|
|
93
103
|
|
|
94
104
|
**It belongs in no project's dependencies.** If you are writing plugins, your workspace declares
|
|
95
105
|
`@rigline/core` and runs `rigline-engine`; `npm create rigline-plugin` scaffolds it that way. A
|
|
@@ -97,7 +107,7 @@ project that can declare dependencies does not need a delivery mechanism.
|
|
|
97
107
|
|
|
98
108
|
## Writing a plugin
|
|
99
109
|
|
|
100
|
-
npm create rigline-plugin
|
|
110
|
+
npm create rigline-plugin my-plugin
|
|
101
111
|
|
|
102
112
|
[The authoring guide](https://github.com/Rigline/Rigline/blob/main/docs/authoring.md) is the long
|
|
103
113
|
form. [Anchors](https://github.com/Rigline/Rigline/blob/main/docs/anchors.md) is what to read when
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ENGINE_PACKAGE, engineDir, ensureEngine, formatEngineUpdate, handOffProblem, newerWrapper, readEngineState, riglineHome, updateEngine, wrapperVersion, } from "./engine.js";
|
|
2
|
+
import { UserError } from "./errors.js";
|
|
3
|
+
export function splitUpdateArgs(args) {
|
|
4
|
+
let tag;
|
|
5
|
+
let now = false;
|
|
6
|
+
const forward = [];
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const arg = args[i];
|
|
9
|
+
if (arg === "--") {
|
|
10
|
+
forward.push(...args.slice(i));
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
if (arg === "--tag" || arg.startsWith("--tag=")) {
|
|
14
|
+
tag = arg === "--tag" ? args[++i] : arg.slice("--tag=".length);
|
|
15
|
+
if (tag === undefined || tag === "")
|
|
16
|
+
throw new UserError("--tag needs a dist-tag");
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg === "--now")
|
|
20
|
+
now = true;
|
|
21
|
+
forward.push(arg);
|
|
22
|
+
}
|
|
23
|
+
return { ...(tag === undefined ? {} : { tag }), now, forward };
|
|
24
|
+
}
|
|
25
|
+
export async function updateCommand(args, options = {}) {
|
|
26
|
+
const { tag, now, forward } = splitUpdateArgs(args);
|
|
27
|
+
const given = {
|
|
28
|
+
...options,
|
|
29
|
+
registry: { ...options.registry, ignoreReleaseAge: now },
|
|
30
|
+
};
|
|
31
|
+
const moved = await updateEngine({ ...given, ...(tag === undefined ? {} : { tag }) });
|
|
32
|
+
console.log(formatEngineUpdate(moved, options.home ?? riglineHome()));
|
|
33
|
+
const engine = await ensureEngine(given);
|
|
34
|
+
const wrapper = options.version ?? wrapperVersion();
|
|
35
|
+
const problem = handOffProblem(wrapper, engine.version);
|
|
36
|
+
if (problem !== null)
|
|
37
|
+
throw new UserError(problem);
|
|
38
|
+
notice(engine.version, wrapper);
|
|
39
|
+
const code = await engine.run(["update", ...forward]);
|
|
40
|
+
const needsYou = moved.outcome === "failed" || (moved.outcome === "withheld" && moved.by === "node");
|
|
41
|
+
return needsYou ? 1 : code;
|
|
42
|
+
}
|
|
43
|
+
export function notice(engine, wrapper = wrapperVersion()) {
|
|
44
|
+
const line = newerWrapper(wrapper, engine);
|
|
45
|
+
if (line !== null)
|
|
46
|
+
console.error(line);
|
|
47
|
+
}
|
|
48
|
+
export function versionCommand() {
|
|
49
|
+
const prefix = engineDir();
|
|
50
|
+
const state = readEngineState(prefix);
|
|
51
|
+
const wrapper = wrapperVersion();
|
|
52
|
+
console.log(`rigline ${wrapper}`);
|
|
53
|
+
console.log(state.kind === "none"
|
|
54
|
+
? `engine: none in ${prefix} — the next command installs one`
|
|
55
|
+
: `engine: ${ENGINE_PACKAGE} ${state.version} in ${prefix}` +
|
|
56
|
+
(state.kind === "unusable" ? ` (unusable: ${state.why})` : ""));
|
|
57
|
+
const newer = state.kind === "none" ? null : newerWrapper(wrapper, state.version);
|
|
58
|
+
if (newer !== null)
|
|
59
|
+
console.log(newer);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=commands.js.map
|
package/dist/engine.js
CHANGED
|
@@ -4,8 +4,10 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { UserError } from "./errors.js";
|
|
7
|
+
import { belowFloor } from "./floor.js";
|
|
7
8
|
import { withHomeLock } from "./lock.js";
|
|
8
9
|
import { releaseAgeProblem, resolveVersion } from "./registry.js";
|
|
10
|
+
export { belowFloor } from "./floor.js";
|
|
9
11
|
export const ENGINE_PACKAGE = "@rigline/core";
|
|
10
12
|
export const ENGINE_BIN = "rigline-engine";
|
|
11
13
|
export function riglineHome(env = process.env) {
|
|
@@ -59,18 +61,71 @@ export function majorProblem(wrapper, engine) {
|
|
|
59
61
|
if (ours === null || theirs === null || ours === theirs)
|
|
60
62
|
return null;
|
|
61
63
|
return (`rigline ${ours}.x cannot run engine ${theirs}.x (${ENGINE_PACKAGE} ${engine}). ` +
|
|
62
|
-
"Run npm i -g rigline@latest.");
|
|
64
|
+
"Run npm i -g rigline@latest, then rigline vscode-setup if you use the companion.");
|
|
65
|
+
}
|
|
66
|
+
export function compareVersions(a, b) {
|
|
67
|
+
const x = parseVersion(a);
|
|
68
|
+
const y = parseVersion(b);
|
|
69
|
+
if (x === null || y === null)
|
|
70
|
+
return null;
|
|
71
|
+
for (let i = 0; i < 3; i++) {
|
|
72
|
+
const diff = x.release[i] - y.release[i];
|
|
73
|
+
if (diff !== 0)
|
|
74
|
+
return diff;
|
|
75
|
+
}
|
|
76
|
+
if (x.pre.length === 0 || y.pre.length === 0)
|
|
77
|
+
return y.pre.length - x.pre.length;
|
|
78
|
+
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
|
|
79
|
+
const p = x.pre[i];
|
|
80
|
+
const q = y.pre[i];
|
|
81
|
+
if (p === undefined)
|
|
82
|
+
return -1;
|
|
83
|
+
if (q === undefined)
|
|
84
|
+
return 1;
|
|
85
|
+
if (p === q)
|
|
86
|
+
continue;
|
|
87
|
+
const pNumeric = /^\d+$/.test(p);
|
|
88
|
+
const qNumeric = /^\d+$/.test(q);
|
|
89
|
+
if (pNumeric && qNumeric)
|
|
90
|
+
return Number(p) - Number(q);
|
|
91
|
+
if (pNumeric !== qNumeric)
|
|
92
|
+
return pNumeric ? -1 : 1;
|
|
93
|
+
return p < q ? -1 : 1;
|
|
94
|
+
}
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
function parseVersion(version) {
|
|
98
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
|
|
99
|
+
if (match === null)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
release: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
103
|
+
pre: match[4] === undefined ? [] : match[4].split("."),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function newerWrapper(wrapper, engine) {
|
|
107
|
+
const order = compareVersions(engine, wrapper);
|
|
108
|
+
if (order === null || order <= 0)
|
|
109
|
+
return null;
|
|
110
|
+
return `rigline ${engine} is out, and this is ${wrapper}: npm i -g rigline@${engine}`;
|
|
111
|
+
}
|
|
112
|
+
const LAST_WITHOUT_UPDATE = "1.0.0-alpha.12";
|
|
113
|
+
export function handOffProblem(wrapper, engine) {
|
|
114
|
+
const behind = compareVersions(engine, wrapper);
|
|
115
|
+
const old = compareVersions(engine, LAST_WITHOUT_UPDATE);
|
|
116
|
+
if (behind === null || old === null || behind >= 0 || old > 0)
|
|
117
|
+
return null;
|
|
118
|
+
return (`${ENGINE_PACKAGE} ${engine} does no update of its own, so no plugin was updated. ` +
|
|
119
|
+
`Run rigline update again once the engine has moved past ${LAST_WITHOUT_UPDATE}.`);
|
|
63
120
|
}
|
|
64
121
|
export function findNpmCli(execPath = process.execPath, exists = existsSync) {
|
|
65
122
|
const here = dirname(execPath);
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
join(here, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
69
|
-
];
|
|
123
|
+
const npmIn = (...dir) => join(here, ...dir, "node_modules", "npm", "bin", "npm-cli.js");
|
|
124
|
+
const candidates = [npmIn(), npmIn("..", "lib"), npmIn("..", "libexec", "lib")];
|
|
70
125
|
const found = candidates.find(exists);
|
|
71
126
|
if (found === undefined) {
|
|
72
127
|
throw new UserError(`no npm was found beside ${execPath}, so the engine cannot be installed. ` +
|
|
73
|
-
`Run: npm install --prefix ${engineDir()} --save-exact --ignore-scripts ${ENGINE_PACKAGE}@latest`);
|
|
128
|
+
`Run: npm install --prefix "${engineDir()}" --save-exact --ignore-scripts ${ENGINE_PACKAGE}@latest`);
|
|
74
129
|
}
|
|
75
130
|
return found;
|
|
76
131
|
}
|
|
@@ -88,10 +143,7 @@ export function engineInstallArgv(options) {
|
|
|
88
143
|
...options.specs,
|
|
89
144
|
];
|
|
90
145
|
}
|
|
91
|
-
const nodeSpawn = (command, args, options) => spawn(command, [...args], {
|
|
92
|
-
stdio: [...options.stdio],
|
|
93
|
-
...(options.env === undefined ? {} : { env: options.env }),
|
|
94
|
-
});
|
|
146
|
+
const nodeSpawn = (command, args, options) => spawn(command, [...args], { stdio: [...options.stdio], windowsHide: true });
|
|
95
147
|
export async function ensureEngine(options = {}) {
|
|
96
148
|
const prefix = engineDir(options.home ?? riglineHome());
|
|
97
149
|
const version = options.version ?? wrapperVersion();
|
|
@@ -111,7 +163,7 @@ export async function ensureEngine(options = {}) {
|
|
|
111
163
|
const problem = majorProblem(version, resolved.version);
|
|
112
164
|
if (problem !== null)
|
|
113
165
|
throw new UserError(problem);
|
|
114
|
-
console.
|
|
166
|
+
console.error(`rigline: installing the engine, ${ENGINE_PACKAGE} ${resolved.version}`);
|
|
115
167
|
await installEngine(prefix, `${ENGINE_PACKAGE}@${resolved.version}`, options);
|
|
116
168
|
const installed = readEngineState(prefix);
|
|
117
169
|
if (installed.kind !== "ready") {
|
|
@@ -141,32 +193,18 @@ function engineAt(state, options) {
|
|
|
141
193
|
return {
|
|
142
194
|
version: state.version,
|
|
143
195
|
entry: state.entry,
|
|
144
|
-
run: (argv
|
|
196
|
+
run: (argv) => new Promise((done, fail) => {
|
|
145
197
|
const child = spawnImpl(node, [state.entry, ...argv], {
|
|
146
198
|
stdio: ["inherit", "inherit", "inherit"],
|
|
147
|
-
...(env === undefined ? {} : { env: { ...process.env, ...env } }),
|
|
148
199
|
});
|
|
149
200
|
child.on("error", fail);
|
|
150
201
|
child.on("close", (code) => done(code ?? 1));
|
|
151
202
|
}),
|
|
152
|
-
json: async (argv, options) => {
|
|
153
|
-
const stderr = options?.quiet ? "ignore" : "inherit";
|
|
154
|
-
const run = await capture(node, [state.entry, ...argv], spawnImpl, stderr);
|
|
155
|
-
if (run.code !== 0) {
|
|
156
|
-
throw new UserError(`the engine exited ${run.code} for \`${argv.join(" ")}\``);
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
return JSON.parse(run.output);
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
throw new UserError(`the engine answered \`${argv.join(" ")}\` with something that is not JSON`);
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
203
|
};
|
|
166
204
|
}
|
|
167
|
-
async function capture(command, argv, spawnImpl = nodeSpawn
|
|
205
|
+
async function capture(command, argv, spawnImpl = nodeSpawn) {
|
|
168
206
|
return await new Promise((done, fail) => {
|
|
169
|
-
const child = spawnImpl(command, argv, { stdio: ["ignore", "pipe",
|
|
207
|
+
const child = spawnImpl(command, argv, { stdio: ["ignore", "pipe", "pipe"] });
|
|
170
208
|
let output = "";
|
|
171
209
|
child.stdout?.setEncoding("utf8");
|
|
172
210
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -208,9 +246,19 @@ export async function updateEngine(options = {}) {
|
|
|
208
246
|
};
|
|
209
247
|
}
|
|
210
248
|
if (installed !== null) {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
return {
|
|
249
|
+
const node = nodeFloorProblem(resolved, options);
|
|
250
|
+
if (node !== null) {
|
|
251
|
+
return {
|
|
252
|
+
outcome: "withheld",
|
|
253
|
+
from: installed,
|
|
254
|
+
to: resolved.version,
|
|
255
|
+
reason: node,
|
|
256
|
+
by: "node",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const age = releaseAgeProblem(resolved, options.registry);
|
|
260
|
+
if (age !== null) {
|
|
261
|
+
return { outcome: "withheld", from: installed, to: resolved.version, reason: age, by: "age" };
|
|
214
262
|
}
|
|
215
263
|
}
|
|
216
264
|
try {
|
|
@@ -228,6 +276,16 @@ export async function updateEngine(options = {}) {
|
|
|
228
276
|
? { outcome: "installed", to: resolved.version }
|
|
229
277
|
: { outcome: "moved", from: installed, to: resolved.version };
|
|
230
278
|
}
|
|
279
|
+
function nodeFloorProblem(resolved, options) {
|
|
280
|
+
const running = options.nodeVersion ?? (options.nodePath === undefined ? process.versions.node : null);
|
|
281
|
+
if (running === null || resolved.enginesNode === null)
|
|
282
|
+
return null;
|
|
283
|
+
const floor = belowFloor(running, resolved.enginesNode);
|
|
284
|
+
if (floor === null)
|
|
285
|
+
return null;
|
|
286
|
+
return (`${ENGINE_PACKAGE} ${resolved.version} needs Node ${floor} or newer, and ` +
|
|
287
|
+
`${options.nodePath ?? process.execPath} is Node ${running}. Install a newer Node to move to it.`);
|
|
288
|
+
}
|
|
231
289
|
export function formatEngineUpdate(update, home = riglineHome()) {
|
|
232
290
|
switch (update.outcome) {
|
|
233
291
|
case "installed":
|
package/dist/floor.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
export function belowFloor(running, range) {
|
|
3
|
+
const floor = /^>=\s*(\d+)\.(\d+)\.(\d+)$/.exec((range ?? "").trim());
|
|
4
|
+
if (floor === null)
|
|
5
|
+
return null;
|
|
6
|
+
const have = running.split(".").map(Number);
|
|
7
|
+
for (let at = 0; at < 3; at++) {
|
|
8
|
+
const want = Number(floor[at + 1]);
|
|
9
|
+
const got = have[at] ?? 0;
|
|
10
|
+
if (got > want)
|
|
11
|
+
return null;
|
|
12
|
+
if (got < want)
|
|
13
|
+
return floor.slice(1).join(".");
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
export function enginesNode() {
|
|
18
|
+
const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
19
|
+
return manifest?.engines?.node;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=floor.js.map
|
package/dist/index.js
CHANGED
|
@@ -1,110 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (spec.startsWith("@"))
|
|
8
|
-
return false;
|
|
9
|
-
return spec.startsWith(".") || spec.includes("/") || spec.includes("\\");
|
|
2
|
+
import { belowFloor, enginesNode } from "./floor.js";
|
|
3
|
+
const floor = belowFloor(process.versions.node, enginesNode());
|
|
4
|
+
if (floor === null) {
|
|
5
|
+
const { runWrapper } = await import("./main.js");
|
|
6
|
+
process.exitCode = await runWrapper(process.argv.slice(2));
|
|
10
7
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
allowPositionals: true,
|
|
16
|
-
});
|
|
17
|
-
if (positionals.length !== 1) {
|
|
18
|
-
throw new UserError("add needs exactly one plugin directory or npm package");
|
|
19
|
-
}
|
|
20
|
-
const spec = positionals[0];
|
|
21
|
-
const engine = await ensureEngine();
|
|
22
|
-
if (isPathSpec(spec))
|
|
23
|
-
return await engine.run(["add", spec]);
|
|
24
|
-
return await addFromNpm({
|
|
25
|
-
spec,
|
|
26
|
-
engine: engine.run,
|
|
27
|
-
registry: { ignoreReleaseAge: values.now },
|
|
28
|
-
});
|
|
8
|
+
else {
|
|
9
|
+
console.error(`rigline: Rigline needs Node ${floor} or newer, and ${process.execPath} is Node ` +
|
|
10
|
+
`${process.versions.node}; https://nodejs.org has one.`);
|
|
11
|
+
process.exitCode = 1;
|
|
29
12
|
}
|
|
30
|
-
async function listed(engine) {
|
|
31
|
-
const answer = await engine.json(["list", "--json"]);
|
|
32
|
-
if (!Array.isArray(answer))
|
|
33
|
-
throw new UserError("the engine's plugin list was not a list");
|
|
34
|
-
return answer;
|
|
35
|
-
}
|
|
36
|
-
const DEFER_INJECT = "RIGLINE_DEFER_INJECT";
|
|
37
|
-
async function updateCommand(args) {
|
|
38
|
-
const { values, positionals } = parseArgs({
|
|
39
|
-
args,
|
|
40
|
-
options: { now: { type: "boolean", default: false }, tag: { type: "string" } },
|
|
41
|
-
allowPositionals: true,
|
|
42
|
-
});
|
|
43
|
-
const registry = { ignoreReleaseAge: values.now };
|
|
44
|
-
const engineUpdate = await updateEngine({ registry, ...(values.tag ? { tag: values.tag } : {}) });
|
|
45
|
-
console.log(formatEngineUpdate(engineUpdate));
|
|
46
|
-
const engine = await ensureEngine({ registry });
|
|
47
|
-
const updates = await updatePlugins({
|
|
48
|
-
listed: await listed(engine),
|
|
49
|
-
engine: (argv) => engine.run(argv, { [DEFER_INJECT]: "1" }),
|
|
50
|
-
...(positionals.length > 0 ? { names: positionals } : {}),
|
|
51
|
-
registry,
|
|
52
|
-
});
|
|
53
|
-
console.log(formatUpdates(updates));
|
|
54
|
-
await addCompanions(engine);
|
|
55
|
-
const moved = engineUpdate.outcome === "moved" ||
|
|
56
|
-
engineUpdate.outcome === "installed" ||
|
|
57
|
-
updates.some((u) => u.outcome === "updated");
|
|
58
|
-
let injected = 0;
|
|
59
|
-
if (moved) {
|
|
60
|
-
console.log("");
|
|
61
|
-
injected = await engine.run(["install"]);
|
|
62
|
-
}
|
|
63
|
-
const failed = updates.some((u) => u.outcome === "failed") || engineUpdate.outcome === "failed";
|
|
64
|
-
return failed || injected !== 0 ? 1 : 0;
|
|
65
|
-
}
|
|
66
|
-
async function addCompanions(engine) {
|
|
67
|
-
const answer = (await engine.json(["companion-profiles"], { quiet: true }).catch(() => null));
|
|
68
|
-
if (answer?.v !== 1 || !Array.isArray(answer.lines))
|
|
69
|
-
return;
|
|
70
|
-
for (const line of answer.lines)
|
|
71
|
-
if (typeof line === "string")
|
|
72
|
-
console.log(line);
|
|
73
|
-
}
|
|
74
|
-
function versionCommand() {
|
|
75
|
-
const prefix = engineDir();
|
|
76
|
-
const state = readEngineState(prefix);
|
|
77
|
-
console.log(`rigline ${wrapperVersion()}`);
|
|
78
|
-
console.log(state.kind === "none"
|
|
79
|
-
? `engine: none in ${prefix} — the next command installs one`
|
|
80
|
-
: `engine: ${ENGINE_PACKAGE} ${state.version} in ${prefix}` +
|
|
81
|
-
(state.kind === "unusable" ? ` (unusable: ${state.why})` : ""));
|
|
82
|
-
return 0;
|
|
83
|
-
}
|
|
84
|
-
async function main(argv) {
|
|
85
|
-
const [command, ...rest] = argv;
|
|
86
|
-
switch (command) {
|
|
87
|
-
case "--version":
|
|
88
|
-
case "-v":
|
|
89
|
-
return versionCommand();
|
|
90
|
-
case "add":
|
|
91
|
-
return await addCommand(rest);
|
|
92
|
-
case "update":
|
|
93
|
-
return await updateCommand(rest);
|
|
94
|
-
default: {
|
|
95
|
-
const engine = await ensureEngine();
|
|
96
|
-
return await engine.run(argv);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
main(process.argv.slice(2)).then((code) => {
|
|
101
|
-
process.exitCode = code;
|
|
102
|
-
}, (error) => {
|
|
103
|
-
if (error instanceof UserError) {
|
|
104
|
-
console.error(`rigline: ${error.message}`);
|
|
105
|
-
process.exitCode = 1;
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
throw error;
|
|
109
|
-
});
|
|
110
13
|
//# sourceMappingURL=index.js.map
|
package/dist/lock.js
CHANGED
|
@@ -2,6 +2,7 @@ import { closeSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFi
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { UserError } from "./errors.js";
|
|
4
4
|
const STALE_MS = 60_000;
|
|
5
|
+
const HELD_MAX_MS = 10 * 60_000;
|
|
5
6
|
const WAIT_MS = 30_000;
|
|
6
7
|
const POLL_MS = 250;
|
|
7
8
|
export function lockPath(home) {
|
|
@@ -41,8 +42,8 @@ function writtenAt(path) {
|
|
|
41
42
|
}
|
|
42
43
|
export function isStale(holder, fileTime, now, staleMs, isAlive) {
|
|
43
44
|
const since = holder === null ? fileTime : Date.parse(holder.since);
|
|
44
|
-
const
|
|
45
|
-
return
|
|
45
|
+
const age = Number.isNaN(since) ? Number.POSITIVE_INFINITY : now - since;
|
|
46
|
+
return age >= HELD_MAX_MS || (age >= staleMs && (holder === null || !isAlive(holder.pid)));
|
|
46
47
|
}
|
|
47
48
|
export class HomeLockedError extends UserError {
|
|
48
49
|
holder;
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { notice, updateCommand, versionCommand } from "./commands.js";
|
|
3
|
+
import { ensureEngine } from "./engine.js";
|
|
4
|
+
import { UserError } from "./errors.js";
|
|
5
|
+
export async function runWrapper(argv) {
|
|
6
|
+
try {
|
|
7
|
+
return await dispatch(argv);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (!(error instanceof UserError))
|
|
11
|
+
throw error;
|
|
12
|
+
console.error(`rigline: ${error.message}`);
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function dispatch(argv) {
|
|
17
|
+
const [command, ...rest] = argv;
|
|
18
|
+
switch (command) {
|
|
19
|
+
case "--version":
|
|
20
|
+
case "-v":
|
|
21
|
+
noArguments(rest);
|
|
22
|
+
return versionCommand();
|
|
23
|
+
case "update":
|
|
24
|
+
return await updateCommand(rest);
|
|
25
|
+
default: {
|
|
26
|
+
const engine = await ensureEngine();
|
|
27
|
+
notice(engine.version);
|
|
28
|
+
return await engine.run(argv);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function noArguments(args) {
|
|
33
|
+
try {
|
|
34
|
+
parseArgs({ args: [...args], options: {}, strict: true, allowPositionals: false });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new UserError(`${error.message}\n\`rigline --help\` lists what each command takes.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=main.js.map
|
package/dist/registry.js
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { UserError } from "./errors.js";
|
|
3
2
|
export const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
4
3
|
export const MINIMUM_RELEASE_AGE_MINUTES = 1440;
|
|
5
|
-
export function parsePluginSpec(spec) {
|
|
6
|
-
const at = spec.lastIndexOf("@");
|
|
7
|
-
const scoped = spec.startsWith("@");
|
|
8
|
-
if (at <= 0 || (scoped && at === 0)) {
|
|
9
|
-
return { name: spec, version: null, tag: null };
|
|
10
|
-
}
|
|
11
|
-
const name = spec.slice(0, at);
|
|
12
|
-
const rest = spec.slice(at + 1);
|
|
13
|
-
if (rest.length === 0)
|
|
14
|
-
return { name, version: null, tag: null };
|
|
15
|
-
return /^\d+\.\d+\.\d+(?:[-+].*)?$/.test(rest)
|
|
16
|
-
? { name, version: rest, tag: null }
|
|
17
|
-
: { name, version: null, tag: rest };
|
|
18
|
-
}
|
|
19
4
|
export async function resolveVersion(spec, options = {}) {
|
|
20
5
|
const packument = await fetchPackument(spec.name, options);
|
|
21
6
|
const tag = spec.version === null ? (spec.tag ?? "latest") : null;
|
|
@@ -32,6 +17,7 @@ export async function resolveVersion(spec, options = {}) {
|
|
|
32
17
|
if (typeof tarball !== "string" || tarball.length === 0) {
|
|
33
18
|
throw new UserError(`${spec.name}@${version} has no tarball on ${registryOf(options)}`);
|
|
34
19
|
}
|
|
20
|
+
const node = release.engines?.node;
|
|
35
21
|
return {
|
|
36
22
|
name: spec.name,
|
|
37
23
|
version,
|
|
@@ -39,35 +25,9 @@ export async function resolveVersion(spec, options = {}) {
|
|
|
39
25
|
tarball,
|
|
40
26
|
integrity: integrityOf(release, `${spec.name}@${version}`),
|
|
41
27
|
ageMinutes,
|
|
28
|
+
enginesNode: typeof node === "string" ? node : null,
|
|
42
29
|
};
|
|
43
30
|
}
|
|
44
|
-
export async function fetchTarball(resolved, options = {}) {
|
|
45
|
-
const response = await request(resolved.tarball, options);
|
|
46
|
-
const bytes = Buffer.from(await response.arrayBuffer());
|
|
47
|
-
const problem = integrityProblem(bytes, resolved.integrity);
|
|
48
|
-
if (problem !== null) {
|
|
49
|
-
throw new UserError(`${resolved.name}@${resolved.version} from ${resolved.tarball} ${problem}. ` +
|
|
50
|
-
"Nothing was written.");
|
|
51
|
-
}
|
|
52
|
-
return bytes;
|
|
53
|
-
}
|
|
54
|
-
export function integrityProblem(bytes, integrity) {
|
|
55
|
-
const dash = integrity.indexOf("-");
|
|
56
|
-
if (dash <= 0)
|
|
57
|
-
return `has an integrity string this cannot read ("${integrity}")`;
|
|
58
|
-
const algorithm = integrity.slice(0, dash);
|
|
59
|
-
const expected = integrity.slice(dash + 1);
|
|
60
|
-
let actual;
|
|
61
|
-
try {
|
|
62
|
-
actual = createHash(algorithm).update(bytes).digest("base64");
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return `names a digest this cannot compute ("${algorithm}")`;
|
|
66
|
-
}
|
|
67
|
-
return actual === expected
|
|
68
|
-
? null
|
|
69
|
-
: `does not match its recorded integrity (${algorithm}-${actual} against ${integrity})`;
|
|
70
|
-
}
|
|
71
31
|
export function releaseAgeProblem(resolved, options = {}) {
|
|
72
32
|
if (options.ignoreReleaseAge)
|
|
73
33
|
return null;
|
|
@@ -77,7 +37,7 @@ export function releaseAgeProblem(resolved, options = {}) {
|
|
|
77
37
|
return (`${resolved.name}@${resolved.version} was published ${describeAge(resolved.ageMinutes)} ago, ` +
|
|
78
38
|
`and rigline waits ${describeAge(minimum)} before installing a version. That window is where ` +
|
|
79
39
|
"a compromised publish is usually caught, and it costs little here because the urgent repair " +
|
|
80
|
-
"— an anchor an extension update retired — is a local edit rather than a release
|
|
40
|
+
"— an anchor an extension update retired — is a local edit rather than a release. " +
|
|
81
41
|
"Wait, or pass --now.");
|
|
82
42
|
}
|
|
83
43
|
export function describeAge(minutes) {
|
package/package.json
CHANGED
package/dist/remote.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { UserError } from "./errors.js";
|
|
5
|
-
import { fetchTarball, parsePluginSpec, releaseAgeProblem, resolveVersion, } from "./registry.js";
|
|
6
|
-
import { readPackageTarball } from "./tarball.js";
|
|
7
|
-
export async function addFromNpm(options) {
|
|
8
|
-
const resolved = await resolveVersion(parsePluginSpec(options.spec), options.registry);
|
|
9
|
-
const withheld = releaseAgeProblem(resolved, options.registry);
|
|
10
|
-
if (withheld !== null)
|
|
11
|
-
throw new UserError(withheld);
|
|
12
|
-
return await stageAndAdd(resolved, options);
|
|
13
|
-
}
|
|
14
|
-
async function stageAndAdd(resolved, options) {
|
|
15
|
-
const label = `${resolved.name}@${resolved.version}`;
|
|
16
|
-
const files = readPackageTarball(await fetchTarball(resolved, options.registry), label);
|
|
17
|
-
const staged = mkdtempSync(join(tmpdir(), "rigline-add-"));
|
|
18
|
-
try {
|
|
19
|
-
for (const file of files) {
|
|
20
|
-
const target = join(staged, file.path);
|
|
21
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
22
|
-
writeFileSync(target, file.bytes);
|
|
23
|
-
}
|
|
24
|
-
const source = {
|
|
25
|
-
kind: "npm",
|
|
26
|
-
name: resolved.name,
|
|
27
|
-
version: resolved.version,
|
|
28
|
-
tag: resolved.tag,
|
|
29
|
-
integrity: resolved.integrity,
|
|
30
|
-
addedAt: (options.now?.() ?? new Date()).toISOString(),
|
|
31
|
-
};
|
|
32
|
-
return await options.engine(["add", staged, "--source", JSON.stringify(source)]);
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
rmSync(staged, { recursive: true, force: true });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
export async function updatePlugins(options) {
|
|
39
|
-
const managed = options.listed.filter((plugin) => plugin.managed);
|
|
40
|
-
const byName = new Map(managed.map((plugin) => [plugin.name, plugin]));
|
|
41
|
-
const names = options.names ?? managed.map((plugin) => plugin.name);
|
|
42
|
-
const updates = [];
|
|
43
|
-
for (const name of names) {
|
|
44
|
-
const plugin = byName.get(name);
|
|
45
|
-
if (plugin === undefined) {
|
|
46
|
-
updates.push({ name, outcome: "failed", reason: "not installed by rigline" });
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
const source = plugin.source;
|
|
50
|
-
if (source === null) {
|
|
51
|
-
updates.push({ name, outcome: "unmanaged" });
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (source.kind === "path") {
|
|
55
|
-
updates.push({
|
|
56
|
-
name,
|
|
57
|
-
outcome: "local",
|
|
58
|
-
reason: typeof source.from === "string" ? source.from : "a directory",
|
|
59
|
-
});
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (source.kind !== "npm") {
|
|
63
|
-
updates.push({ name, outcome: "unmanaged", reason: `source kind "${source.kind}"` });
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (source.name === undefined || source.version === undefined) {
|
|
67
|
-
updates.push({ name, outcome: "failed", reason: "its npm source is incomplete" });
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (source.tag === null || source.tag === undefined) {
|
|
71
|
-
updates.push({ name, outcome: "pinned", from: source.version });
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
updates.push(await updateOne(name, { name: source.name, version: source.version, tag: source.tag }, options));
|
|
75
|
-
}
|
|
76
|
-
return updates;
|
|
77
|
-
}
|
|
78
|
-
async function updateOne(name, source, options) {
|
|
79
|
-
try {
|
|
80
|
-
const resolved = await resolveVersion({ name: source.name, version: null, tag: source.tag }, options.registry);
|
|
81
|
-
if (resolved.version === source.version) {
|
|
82
|
-
return { name, outcome: "current", from: source.version };
|
|
83
|
-
}
|
|
84
|
-
const withheld = releaseAgeProblem(resolved, options.registry);
|
|
85
|
-
if (withheld !== null) {
|
|
86
|
-
return {
|
|
87
|
-
name,
|
|
88
|
-
outcome: "withheld",
|
|
89
|
-
from: source.version,
|
|
90
|
-
to: resolved.version,
|
|
91
|
-
reason: withheld,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
const code = await stageAndAdd(resolved, options);
|
|
95
|
-
if (code !== 0) {
|
|
96
|
-
return { name, outcome: "failed", from: source.version, reason: `the engine exited ${code}` };
|
|
97
|
-
}
|
|
98
|
-
return { name, outcome: "updated", from: source.version, to: resolved.version };
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
return {
|
|
102
|
-
name,
|
|
103
|
-
outcome: "failed",
|
|
104
|
-
from: source.version,
|
|
105
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
export function formatUpdates(updates) {
|
|
110
|
-
if (updates.length === 0)
|
|
111
|
-
return "no plugins to update — the bundled ones move with the engine";
|
|
112
|
-
return updates.map(updateLine).join("\n");
|
|
113
|
-
}
|
|
114
|
-
function updateLine(update) {
|
|
115
|
-
switch (update.outcome) {
|
|
116
|
-
case "updated":
|
|
117
|
-
return `${update.name}: ${update.from} -> ${update.to}`;
|
|
118
|
-
case "current":
|
|
119
|
-
return `${update.name}: ${update.from}, which is what its tag resolves to`;
|
|
120
|
-
case "pinned":
|
|
121
|
-
return `${update.name}: pinned to ${update.from}; add it again to move it`;
|
|
122
|
-
case "local":
|
|
123
|
-
return `${update.name}: added from ${update.reason}; run rigline add again to refresh it`;
|
|
124
|
-
case "unmanaged":
|
|
125
|
-
return `${update.name}: placed by hand, so there is nowhere to fetch a newer one from`;
|
|
126
|
-
case "withheld":
|
|
127
|
-
return `${update.name}: staying on ${update.from} — ${update.reason}`;
|
|
128
|
-
default:
|
|
129
|
-
return `${update.name}: FAILED — ${update.reason}`;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
//# sourceMappingURL=remote.js.map
|
package/dist/tarball.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { gunzipSync } from "node:zlib";
|
|
2
|
-
import { UserError } from "./errors.js";
|
|
3
|
-
const BLOCK = 512;
|
|
4
|
-
const TYPE_FILE = 0x30;
|
|
5
|
-
const TYPE_FILE_LEGACY = 0x00;
|
|
6
|
-
const TYPE_DIRECTORY = 0x35;
|
|
7
|
-
const TYPE_PAX_ENTRY = 0x78;
|
|
8
|
-
const TYPE_PAX_GLOBAL = 0x67;
|
|
9
|
-
const MAX_ENTRIES = 2000;
|
|
10
|
-
const MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
11
|
-
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
12
|
-
export function readPackageTarball(gzipped, label) {
|
|
13
|
-
let tar;
|
|
14
|
-
try {
|
|
15
|
-
tar = gunzipSync(gzipped);
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
throw new UserError(`${label} is not a gzipped tarball: ${error.message}`);
|
|
19
|
-
}
|
|
20
|
-
const members = [];
|
|
21
|
-
let total = 0;
|
|
22
|
-
let offset = 0;
|
|
23
|
-
while (offset + BLOCK <= tar.length) {
|
|
24
|
-
const header = tar.subarray(offset, offset + BLOCK);
|
|
25
|
-
if (isEndOfArchive(header))
|
|
26
|
-
break;
|
|
27
|
-
offset += BLOCK;
|
|
28
|
-
const name = readHeaderName(header, label);
|
|
29
|
-
const size = readOctal(header, 124, 12, `${label}: entry "${name}" has an unreadable size`);
|
|
30
|
-
const type = header[156] ?? 0;
|
|
31
|
-
const body = tar.subarray(offset, offset + size);
|
|
32
|
-
offset += Math.ceil(size / BLOCK) * BLOCK;
|
|
33
|
-
if (type === TYPE_PAX_ENTRY || type === TYPE_PAX_GLOBAL)
|
|
34
|
-
continue;
|
|
35
|
-
if (type === TYPE_DIRECTORY)
|
|
36
|
-
continue;
|
|
37
|
-
if (type !== TYPE_FILE && type !== TYPE_FILE_LEGACY) {
|
|
38
|
-
throw new UserError(`${label} carries "${name}", which is not a regular file (tar type ` +
|
|
39
|
-
`"${String.fromCharCode(type)}"). A plugin is files; nothing here unpacks a link, a ` +
|
|
40
|
-
"device or an archive member of any other kind.");
|
|
41
|
-
}
|
|
42
|
-
if (size > MAX_FILE_BYTES) {
|
|
43
|
-
throw new UserError(`${label}: "${name}" is ${size} bytes, over the ${MAX_FILE_BYTES} cap`);
|
|
44
|
-
}
|
|
45
|
-
total += size;
|
|
46
|
-
if (total > MAX_TOTAL_BYTES || members.length >= MAX_ENTRIES) {
|
|
47
|
-
throw new UserError(`${label} unpacks to more than ${MAX_ENTRIES} files or ${MAX_TOTAL_BYTES} bytes`);
|
|
48
|
-
}
|
|
49
|
-
if (body.length < size) {
|
|
50
|
-
throw new UserError(`${label} ends inside "${name}", so it is truncated`);
|
|
51
|
-
}
|
|
52
|
-
const path = safePath(name, label);
|
|
53
|
-
if (path === null)
|
|
54
|
-
continue;
|
|
55
|
-
members.push({ path, bytes: Buffer.from(body) });
|
|
56
|
-
}
|
|
57
|
-
return strip(members, label);
|
|
58
|
-
}
|
|
59
|
-
function strip(members, label) {
|
|
60
|
-
const first = members[0];
|
|
61
|
-
if (first === undefined) {
|
|
62
|
-
throw new UserError(`${label} has no files in it`);
|
|
63
|
-
}
|
|
64
|
-
const root = first.path.split("/")[0];
|
|
65
|
-
const files = [];
|
|
66
|
-
const seen = new Set();
|
|
67
|
-
for (const member of members) {
|
|
68
|
-
const segments = member.path.split("/");
|
|
69
|
-
if (segments[0] !== root) {
|
|
70
|
-
throw new UserError(`${label} carries "${member.path}", which is outside "${root}/"; every member of a ` +
|
|
71
|
-
"published tarball sits under one directory, and this one has two");
|
|
72
|
-
}
|
|
73
|
-
const path = segments.slice(1).join("/");
|
|
74
|
-
if (path.length === 0)
|
|
75
|
-
continue;
|
|
76
|
-
if (seen.has(path)) {
|
|
77
|
-
throw new UserError(`${label} carries "${path}" twice`);
|
|
78
|
-
}
|
|
79
|
-
seen.add(path);
|
|
80
|
-
files.push({ path, bytes: member.bytes });
|
|
81
|
-
}
|
|
82
|
-
if (files.length === 0) {
|
|
83
|
-
throw new UserError(`${label} has nothing under "${root}/"`);
|
|
84
|
-
}
|
|
85
|
-
return files;
|
|
86
|
-
}
|
|
87
|
-
function isEndOfArchive(header) {
|
|
88
|
-
for (const byte of header) {
|
|
89
|
-
if (byte !== 0)
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
function readHeaderName(header, label) {
|
|
95
|
-
const name = readString(header, 0, 100);
|
|
96
|
-
const prefix = readString(header, 345, 155);
|
|
97
|
-
if (name.length === 0) {
|
|
98
|
-
throw new UserError(`${label} carries an entry with no name`);
|
|
99
|
-
}
|
|
100
|
-
return prefix.length > 0 ? `${prefix}/${name}` : name;
|
|
101
|
-
}
|
|
102
|
-
function readString(header, at, length) {
|
|
103
|
-
const field = header.subarray(at, at + length);
|
|
104
|
-
const end = field.indexOf(0);
|
|
105
|
-
return field.subarray(0, end === -1 ? field.length : end).toString("utf8");
|
|
106
|
-
}
|
|
107
|
-
function readOctal(header, at, length, problem) {
|
|
108
|
-
const text = readString(header, at, length).trim();
|
|
109
|
-
const value = text.length === 0 ? 0 : Number.parseInt(text, 8);
|
|
110
|
-
if (!Number.isSafeInteger(value) || value < 0)
|
|
111
|
-
throw new UserError(problem);
|
|
112
|
-
return value;
|
|
113
|
-
}
|
|
114
|
-
function safePath(name, label) {
|
|
115
|
-
const path = name.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
116
|
-
if (path.length === 0)
|
|
117
|
-
return null;
|
|
118
|
-
const segments = path.split("/");
|
|
119
|
-
if (name.startsWith("/") ||
|
|
120
|
-
segments.some((segment) => segment === "" || segment === "." || segment === ".." || /^[a-zA-Z]:/.test(segment))) {
|
|
121
|
-
throw new UserError(`${label} carries "${name}", which does not stay inside the directory it unpacks into`);
|
|
122
|
-
}
|
|
123
|
-
return path;
|
|
124
|
-
}
|
|
125
|
-
//# sourceMappingURL=tarball.js.map
|