rigline 1.0.0-alpha.12 → 1.0.0-alpha.13
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 +13 -10
- package/dist/commands.js +61 -0
- package/dist/engine.js +60 -22
- package/dist/index.js +3 -83
- package/dist/registry.js +0 -42
- 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
|
|
@@ -39,9 +41,9 @@ directory until *Developer: Reload Window*.
|
|
|
39
41
|
rigline vscode-setup
|
|
40
42
|
|
|
41
43
|
If running `install` after every update is a chore, this installs a companion extension that does it
|
|
42
|
-
for you — into
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
for you — into VS Code, found on your `PATH`, from a VSIX that ships inside the engine. Nothing is
|
|
45
|
+
downloaded, and the companion moves when the engine does. It injects as it goes, so it is a step
|
|
46
|
+
*instead of* `install` rather than after it.
|
|
45
47
|
|
|
46
48
|
From then on it watches for the extension update and re-injects behind it, so there is nothing to
|
|
47
49
|
remember after one. It goes into every VS Code profile that has Claude Code, and into any profile
|
|
@@ -86,10 +88,11 @@ going back is one command.
|
|
|
86
88
|
|
|
87
89
|
## How it is put together
|
|
88
90
|
|
|
89
|
-
`rigline` is a small retrieval layer. It
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
`rigline` is a small retrieval layer. It installs the engine and runs it, and `@rigline/core` answers
|
|
92
|
+
every verb above, fetching plugins included. The split is not decoration: a process cannot replace
|
|
93
|
+
the package it is running out of, so `update` moves the engine from here and then hands the rest to
|
|
94
|
+
it. Nothing updates `rigline` itself, so when the engine you have is newer, `rigline` says so and
|
|
95
|
+
names the command.
|
|
93
96
|
|
|
94
97
|
**It belongs in no project's dependencies.** If you are writing plugins, your workspace declares
|
|
95
98
|
`@rigline/core` and runs `rigline-engine`; `npm create rigline-plugin` scaffolds it that way. A
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
return moved.outcome === "failed" ? 1 : code;
|
|
41
|
+
}
|
|
42
|
+
export function notice(engine, wrapper = wrapperVersion()) {
|
|
43
|
+
const line = newerWrapper(wrapper, engine);
|
|
44
|
+
if (line !== null)
|
|
45
|
+
console.error(line);
|
|
46
|
+
}
|
|
47
|
+
export function versionCommand() {
|
|
48
|
+
const prefix = engineDir();
|
|
49
|
+
const state = readEngineState(prefix);
|
|
50
|
+
const wrapper = wrapperVersion();
|
|
51
|
+
console.log(`rigline ${wrapper}`);
|
|
52
|
+
console.log(state.kind === "none"
|
|
53
|
+
? `engine: none in ${prefix} — the next command installs one`
|
|
54
|
+
: `engine: ${ENGINE_PACKAGE} ${state.version} in ${prefix}` +
|
|
55
|
+
(state.kind === "unusable" ? ` (unusable: ${state.why})` : ""));
|
|
56
|
+
const newer = state.kind === "none" ? null : newerWrapper(wrapper, state.version);
|
|
57
|
+
if (newer !== null)
|
|
58
|
+
console.log(newer);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=commands.js.map
|
package/dist/engine.js
CHANGED
|
@@ -61,6 +61,61 @@ export function majorProblem(wrapper, engine) {
|
|
|
61
61
|
return (`rigline ${ours}.x cannot run engine ${theirs}.x (${ENGINE_PACKAGE} ${engine}). ` +
|
|
62
62
|
"Run npm i -g rigline@latest.");
|
|
63
63
|
}
|
|
64
|
+
export function compareVersions(a, b) {
|
|
65
|
+
const x = parseVersion(a);
|
|
66
|
+
const y = parseVersion(b);
|
|
67
|
+
if (x === null || y === null)
|
|
68
|
+
return null;
|
|
69
|
+
for (let i = 0; i < 3; i++) {
|
|
70
|
+
const diff = x.release[i] - y.release[i];
|
|
71
|
+
if (diff !== 0)
|
|
72
|
+
return diff;
|
|
73
|
+
}
|
|
74
|
+
if (x.pre.length === 0 || y.pre.length === 0)
|
|
75
|
+
return y.pre.length - x.pre.length;
|
|
76
|
+
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
|
|
77
|
+
const p = x.pre[i];
|
|
78
|
+
const q = y.pre[i];
|
|
79
|
+
if (p === undefined)
|
|
80
|
+
return -1;
|
|
81
|
+
if (q === undefined)
|
|
82
|
+
return 1;
|
|
83
|
+
if (p === q)
|
|
84
|
+
continue;
|
|
85
|
+
const pNumeric = /^\d+$/.test(p);
|
|
86
|
+
const qNumeric = /^\d+$/.test(q);
|
|
87
|
+
if (pNumeric && qNumeric)
|
|
88
|
+
return Number(p) - Number(q);
|
|
89
|
+
if (pNumeric !== qNumeric)
|
|
90
|
+
return pNumeric ? -1 : 1;
|
|
91
|
+
return p < q ? -1 : 1;
|
|
92
|
+
}
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
function parseVersion(version) {
|
|
96
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
|
|
97
|
+
if (match === null)
|
|
98
|
+
return null;
|
|
99
|
+
return {
|
|
100
|
+
release: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
101
|
+
pre: match[4] === undefined ? [] : match[4].split("."),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function newerWrapper(wrapper, engine) {
|
|
105
|
+
const order = compareVersions(engine, wrapper);
|
|
106
|
+
if (order === null || order <= 0)
|
|
107
|
+
return null;
|
|
108
|
+
return `rigline ${engine} is out, and this is ${wrapper}: npm i -g rigline@${engine}`;
|
|
109
|
+
}
|
|
110
|
+
const LAST_WITHOUT_UPDATE = "1.0.0-alpha.12";
|
|
111
|
+
export function handOffProblem(wrapper, engine) {
|
|
112
|
+
const behind = compareVersions(engine, wrapper);
|
|
113
|
+
const old = compareVersions(engine, LAST_WITHOUT_UPDATE);
|
|
114
|
+
if (behind === null || old === null || behind >= 0 || old > 0)
|
|
115
|
+
return null;
|
|
116
|
+
return (`${ENGINE_PACKAGE} ${engine} does no update of its own, so no plugin was updated. ` +
|
|
117
|
+
`Run rigline update again once the engine has moved past ${LAST_WITHOUT_UPDATE}.`);
|
|
118
|
+
}
|
|
64
119
|
export function findNpmCli(execPath = process.execPath, exists = existsSync) {
|
|
65
120
|
const here = dirname(execPath);
|
|
66
121
|
const candidates = [
|
|
@@ -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) => {
|
package/dist/index.js
CHANGED
|
@@ -1,98 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { notice, updateCommand, versionCommand } from "./commands.js";
|
|
3
|
+
import { ensureEngine } from "./engine.js";
|
|
4
4
|
import { UserError } from "./errors.js";
|
|
5
|
-
import { addFromNpm, formatUpdates, updatePlugins } from "./remote.js";
|
|
6
|
-
function isPathSpec(spec) {
|
|
7
|
-
if (spec.startsWith("@"))
|
|
8
|
-
return false;
|
|
9
|
-
return spec.startsWith(".") || spec.includes("/") || spec.includes("\\");
|
|
10
|
-
}
|
|
11
|
-
async function addCommand(args) {
|
|
12
|
-
const { values, positionals } = parseArgs({
|
|
13
|
-
args,
|
|
14
|
-
options: { now: { type: "boolean", default: false } },
|
|
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
|
-
});
|
|
29
|
-
}
|
|
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
5
|
async function main(argv) {
|
|
85
6
|
const [command, ...rest] = argv;
|
|
86
7
|
switch (command) {
|
|
87
8
|
case "--version":
|
|
88
9
|
case "-v":
|
|
89
10
|
return versionCommand();
|
|
90
|
-
case "add":
|
|
91
|
-
return await addCommand(rest);
|
|
92
11
|
case "update":
|
|
93
12
|
return await updateCommand(rest);
|
|
94
13
|
default: {
|
|
95
14
|
const engine = await ensureEngine();
|
|
15
|
+
notice(engine.version);
|
|
96
16
|
return await engine.run(argv);
|
|
97
17
|
}
|
|
98
18
|
}
|
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;
|
|
@@ -41,33 +26,6 @@ export async function resolveVersion(spec, options = {}) {
|
|
|
41
26
|
ageMinutes,
|
|
42
27
|
};
|
|
43
28
|
}
|
|
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
29
|
export function releaseAgeProblem(resolved, options = {}) {
|
|
72
30
|
if (options.ignoreReleaseAge)
|
|
73
31
|
return null;
|
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
|