rigline 1.0.0-alpha.4 → 1.0.0-alpha.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # rigline
2
2
 
3
- The command-line surface for [Rigline](https://github.com/Rigline/Rigline), a plugin layer for the
4
- Claude Code VS Code extension.
3
+ The command for [Rigline](https://github.com/Rigline/Rigline), a plugin layer for the Claude Code
4
+ VS Code extension.
5
5
 
6
6
  Rigline injects a small loader into the installed extension's webview, gives plugins a
7
7
  capability-scoped context to write against, and harvests the identifiers those plugins depend on
@@ -13,6 +13,11 @@ rather than a breakage to chase.
13
13
  npm install -g rigline
14
14
  rigline install
15
15
 
16
+ The first command you run fetches the engine, [`@rigline/core`](https://www.npmjs.com/package/@rigline/core),
17
+ into `~/.rigline/engine` and runs it from there. It takes a few seconds and needs the registry;
18
+ after that nothing reaches the network unless you ask it to. If that directory is ever in a bad
19
+ state, delete it and run any command again.
20
+
16
21
  `install` finds every installed version of the extension, harvests it, bakes the discovered plugins
17
22
  into a payload and injects the loader. It keeps a byte-faithful backup of each bundle it touches,
18
23
  and `rigline restore` puts everything back using nothing but Node and the backup.
@@ -30,8 +35,10 @@ directory until *Developer: Reload Window*.
30
35
 
31
36
  rigline add SPEC install a plugin from a directory or npm, and re-inject
32
37
  rigline remove N delete a plugin rigline installed, and re-inject
33
- rigline update move each npm plugin to what its tag resolves to
34
- rigline list every plugin, in load order: origin, source, switch, what it can do
38
+ rigline disable N switch a plugin off, and re-inject
39
+ rigline enable N switch it back on, and re-inject
40
+ rigline update move the engine and every npm plugin to what its tag resolves to
41
+ rigline list every plugin, in load order: version, origin, source, switch, what it can do
35
42
 
36
43
  rigline build build a plugin to one browser ES module
37
44
  rigline dev rebuild and re-inject as you edit
@@ -39,9 +46,25 @@ directory until *Developer: Reload Window*.
39
46
  rigline diff A B identifier drift between two extension directories
40
47
  rigline doctor install state per version, as a pasteable report
41
48
 
42
- `add` never runs a package manager: a published plugin is one bundled ES module and a manifest, so
43
- there is nothing to resolve. A version must reach a minimum age (a day, by default) before `add` or
44
- `update` will take it, and a withheld version is named rather than skipped in silence.
49
+ rigline --version this command's version and the installed engine's
50
+ rigline --help the whole surface, from the engine
51
+
52
+ `add` never runs a package manager for a plugin: a published plugin is one bundled ES module and a
53
+ manifest, so there is nothing to resolve. A version must reach a minimum age (a day, by default)
54
+ before `add` or `update` will take it, and a withheld version is named rather than skipped in
55
+ silence. `update` applies the same rule to the engine, and says which version it moved from so
56
+ going back is one command.
57
+
58
+ ## How it is put together
59
+
60
+ `rigline` is a small retrieval layer. It fetches bytes — a plugin tarball, the engine — checks them,
61
+ and hands them over; `@rigline/core` does everything else and answers every verb above. The split is
62
+ not decoration: a process cannot replace the package it is running out of, so whatever performs an
63
+ update has to sit above the thing being updated.
64
+
65
+ **It belongs in no project's dependencies.** If you are writing plugins, your workspace declares
66
+ `@rigline/core` and runs `rigline-engine`; `npm create rigline-plugin` scaffolds it that way. A
67
+ project that can declare dependencies does not need a delivery mechanism.
45
68
 
46
69
  ## Writing a plugin
47
70
 
package/dist/engine.js ADDED
@@ -0,0 +1,238 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { UserError } from "./errors.js";
7
+ import { releaseAgeProblem, resolveVersion } from "./registry.js";
8
+ export const ENGINE_PACKAGE = "@rigline/core";
9
+ export const ENGINE_BIN = "rigline-engine";
10
+ export function riglineHome(env = process.env) {
11
+ const override = env.RIGLINE_HOME;
12
+ return override && override.length > 0 ? override : join(homedir(), ".rigline");
13
+ }
14
+ export function engineDir(home = riglineHome()) {
15
+ return join(home, "engine");
16
+ }
17
+ export function wrapperVersion() {
18
+ const manifest = fileURLToPath(new URL("../package.json", import.meta.url));
19
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
20
+ const version = parsed.version;
21
+ if (typeof version !== "string")
22
+ throw new Error(`${manifest} has no version`);
23
+ return version;
24
+ }
25
+ export function majorOf(version) {
26
+ const match = /^(\d+)\./.exec(version);
27
+ return match === null ? null : Number(match[1]);
28
+ }
29
+ export function readEngineState(prefix) {
30
+ const dir = join(prefix, "node_modules", ENGINE_PACKAGE);
31
+ const manifest = join(dir, "package.json");
32
+ if (!existsSync(manifest))
33
+ return { kind: "none" };
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(readFileSync(manifest, "utf8"));
37
+ }
38
+ catch {
39
+ return { kind: "unusable", version: "unreadable", why: `${manifest} is not readable JSON` };
40
+ }
41
+ const version = typeof parsed.version === "string" ? parsed.version : "unreadable";
42
+ const bin = parsed.bin;
43
+ const named = typeof bin === "object" && bin !== null
44
+ ? bin[ENGINE_BIN]
45
+ : undefined;
46
+ if (typeof named !== "string") {
47
+ return { kind: "unusable", version, why: `it has no ${ENGINE_BIN} command` };
48
+ }
49
+ const entry = resolve(dir, named);
50
+ if (!existsSync(entry)) {
51
+ return { kind: "unusable", version, why: `its ${ENGINE_BIN} entry ${entry} is missing` };
52
+ }
53
+ return { kind: "ready", version, entry };
54
+ }
55
+ export function majorProblem(wrapper, engine) {
56
+ const ours = majorOf(wrapper);
57
+ const theirs = majorOf(engine);
58
+ if (ours === null || theirs === null || ours === theirs)
59
+ return null;
60
+ return (`rigline ${ours}.x cannot run engine ${theirs}.x (${ENGINE_PACKAGE} ${engine}). ` +
61
+ "Run npm i -g rigline@latest.");
62
+ }
63
+ export function findNpmCli(execPath = process.execPath, exists = existsSync) {
64
+ const here = dirname(execPath);
65
+ const candidates = [
66
+ join(here, "node_modules", "npm", "bin", "npm-cli.js"),
67
+ join(here, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
68
+ ];
69
+ const found = candidates.find(exists);
70
+ if (found === undefined) {
71
+ throw new UserError(`no npm was found beside ${execPath}, so the engine cannot be installed. ` +
72
+ `Run: npm install --prefix ${engineDir()} --save-exact --ignore-scripts ${ENGINE_PACKAGE}@latest`);
73
+ }
74
+ return found;
75
+ }
76
+ export function engineInstallArgv(options) {
77
+ return [
78
+ options.npmCli,
79
+ "install",
80
+ "--prefix",
81
+ options.prefix,
82
+ "--save-exact",
83
+ "--ignore-scripts",
84
+ "--no-audit",
85
+ "--no-fund",
86
+ "--loglevel=error",
87
+ ...options.specs,
88
+ ];
89
+ }
90
+ const nodeSpawn = (command, args, options) => spawn(command, [...args], { stdio: [...options.stdio] });
91
+ export async function ensureEngine(options = {}) {
92
+ const prefix = engineDir(options.home ?? riglineHome());
93
+ const version = options.version ?? wrapperVersion();
94
+ const state = readEngineState(prefix);
95
+ if (state.kind === "ready") {
96
+ const problem = majorProblem(version, state.version);
97
+ if (problem !== null)
98
+ throw new UserError(problem);
99
+ return engineAt(state, options);
100
+ }
101
+ const resolved = await resolveEngine(options);
102
+ if (state.kind === "unusable" && resolved.version === state.version) {
103
+ throw new UserError(`the engine in ${prefix} is ${ENGINE_PACKAGE} ${state.version} and ${state.why}, ` +
104
+ `and that is what ${ENGINE_PACKAGE}@${options.tag ?? "latest"} resolves to. ` +
105
+ `Delete ${prefix} and try again, or install a version that has one.`);
106
+ }
107
+ const problem = majorProblem(version, resolved.version);
108
+ if (problem !== null)
109
+ throw new UserError(problem);
110
+ console.log(`rigline: installing the engine, ${ENGINE_PACKAGE} ${resolved.version}`);
111
+ await installEngine(prefix, `${ENGINE_PACKAGE}@${resolved.version}`, options);
112
+ const installed = readEngineState(prefix);
113
+ if (installed.kind !== "ready") {
114
+ throw new UserError(`${ENGINE_PACKAGE} ${resolved.version} was installed into ${prefix}, but ` +
115
+ (installed.kind === "unusable" ? installed.why : "nothing is there"));
116
+ }
117
+ return engineAt(installed, options);
118
+ }
119
+ async function resolveEngine(options) {
120
+ return await resolveVersion({ name: ENGINE_PACKAGE, version: null, tag: options.tag ?? "latest" }, options.registry);
121
+ }
122
+ async function installEngine(prefix, spec, options) {
123
+ mkdirSync(prefix, { recursive: true });
124
+ const argv = engineInstallArgv({ npmCli: findNpmCli(), prefix, specs: [spec] });
125
+ const run = await capture(process.execPath, argv, options.spawnImpl);
126
+ if (run.code !== 0) {
127
+ throw new UserError(`installing ${spec} into ${prefix} failed (npm exited ${run.code}).\n${run.output.trim()}`);
128
+ }
129
+ }
130
+ function engineAt(state, options) {
131
+ const spawnImpl = options.spawnImpl ?? nodeSpawn;
132
+ return {
133
+ version: state.version,
134
+ entry: state.entry,
135
+ run: (argv) => new Promise((done, fail) => {
136
+ const child = spawnImpl(process.execPath, [state.entry, ...argv], {
137
+ stdio: ["inherit", "inherit", "inherit"],
138
+ });
139
+ child.on("error", fail);
140
+ child.on("close", (code) => done(code ?? 1));
141
+ }),
142
+ json: async (argv) => {
143
+ const run = await capture(process.execPath, [state.entry, ...argv], spawnImpl, "inherit");
144
+ if (run.code !== 0) {
145
+ throw new UserError(`the engine exited ${run.code} for \`${argv.join(" ")}\``);
146
+ }
147
+ try {
148
+ return JSON.parse(run.output);
149
+ }
150
+ catch {
151
+ throw new UserError(`the engine answered \`${argv.join(" ")}\` with something that is not JSON`);
152
+ }
153
+ },
154
+ };
155
+ }
156
+ async function capture(command, argv, spawnImpl = nodeSpawn, stderr = "pipe") {
157
+ return await new Promise((done, fail) => {
158
+ const child = spawnImpl(command, argv, { stdio: ["ignore", "pipe", stderr] });
159
+ let output = "";
160
+ child.stdout?.setEncoding("utf8");
161
+ child.stdout?.on("data", (chunk) => {
162
+ output += chunk;
163
+ });
164
+ child.stderr?.setEncoding("utf8");
165
+ child.stderr?.on("data", (chunk) => {
166
+ output += chunk;
167
+ });
168
+ child.on("error", fail);
169
+ child.on("close", (code) => done({ code: code ?? 1, output }));
170
+ });
171
+ }
172
+ export async function updateEngine(options = {}) {
173
+ const prefix = engineDir(options.home ?? riglineHome());
174
+ const state = readEngineState(prefix);
175
+ const installed = state.kind === "none" ? null : state.version;
176
+ let resolved;
177
+ try {
178
+ resolved = await resolveEngine(options);
179
+ }
180
+ catch (error) {
181
+ return {
182
+ outcome: "failed",
183
+ ...(installed === null ? {} : { from: installed }),
184
+ reason: error instanceof Error ? error.message : String(error),
185
+ };
186
+ }
187
+ if (state.kind === "ready" && resolved.version === state.version) {
188
+ return { outcome: "current", from: state.version };
189
+ }
190
+ const problem = majorProblem(options.version ?? wrapperVersion(), resolved.version);
191
+ if (problem !== null) {
192
+ return {
193
+ outcome: "failed",
194
+ ...(installed === null ? {} : { from: installed }),
195
+ reason: problem,
196
+ };
197
+ }
198
+ const withheld = releaseAgeProblem(resolved, options.registry);
199
+ if (withheld !== null) {
200
+ return {
201
+ outcome: "withheld",
202
+ ...(installed === null ? {} : { from: installed }),
203
+ to: resolved.version,
204
+ reason: withheld,
205
+ };
206
+ }
207
+ try {
208
+ await installEngine(prefix, `${ENGINE_PACKAGE}@${resolved.version}`, options);
209
+ }
210
+ catch (error) {
211
+ return {
212
+ outcome: "failed",
213
+ ...(installed === null ? {} : { from: installed }),
214
+ to: resolved.version,
215
+ reason: error instanceof Error ? error.message : String(error),
216
+ };
217
+ }
218
+ return installed === null
219
+ ? { outcome: "installed", to: resolved.version }
220
+ : { outcome: "moved", from: installed, to: resolved.version };
221
+ }
222
+ export function formatEngineUpdate(update, home = riglineHome()) {
223
+ switch (update.outcome) {
224
+ case "installed":
225
+ return `engine: installed ${update.to}`;
226
+ case "moved":
227
+ return (`engine: ${update.from} -> ${update.to}\n` +
228
+ ` to go back: npm install --prefix ${engineDir(home)} --save-exact ` +
229
+ `${ENGINE_PACKAGE}@${update.from}`);
230
+ case "current":
231
+ return `engine: ${update.from}, which is what its tag resolves to`;
232
+ case "withheld":
233
+ return `engine: staying on ${update.from} — ${update.reason}`;
234
+ default:
235
+ return `engine: FAILED — ${update.reason}`;
236
+ }
237
+ }
238
+ //# sourceMappingURL=engine.js.map
package/dist/errors.js ADDED
@@ -0,0 +1,4 @@
1
+ export class UserError extends Error {
2
+ name = "UserError";
3
+ }
4
+ //# sourceMappingURL=errors.js.map
package/dist/index.js CHANGED
@@ -1,110 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, watch as fsWatch, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { basename, dirname, join, relative, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
2
  import { parseArgs } from "node:util";
6
- import { addFromNpm, addPlugin, CORE_VERSION, check, collect, diffScans, EXTENSIONS_DIR, extensionVersion, findExtension, formatDiff, formatDoctor, formatFlow, formatPlugins, formatUpdates, generate, harvestAll, hostVerdict, inspect, install, installedExtensions, listPlugins, readAnchorOverrides, readBundles, removePlugin, restoreAll, riglinePaths, scanOf, UserError, update, updatePlugins, verdict, watch, } from "@rigline/core";
7
- import { buildPlugin } from "./build.js";
8
- const USAGE = `rigline ${CORE_VERSION}
9
-
10
- rigline codegen [DIR] [--check] [--out FILE]
11
- Harvest the installed extension (or DIR) and write ./generated.ts: the identifier
12
- augmentation your plugins compile against, and the baseline install diffs. Commit it.
13
- --check compares instead of writing and exits 1 when the file is out of date. Either
14
- way it exits 1 if a curated anchor claims to name one element and this version
15
- applies its class in more than one place. It reads the shipped anchor table, never
16
- ~/.rigline/anchors.json: its verdict is about this repository's table.
17
-
18
- rigline diff DIR_A DIR_B
19
- Compare the identifier layers of two extension directories.
20
-
21
- rigline build [DIR] [--source FILE]
22
- Bundle a plugin's src/index.ts (or --source) into the entry its rigline.json names.
23
-
24
- rigline install [--ext DIR] [--payload DIR]
25
- Inject the loader into every installed extension version (or DIR), baking the enabled
26
- plugins, and report what moved since the baseline and which plugins each version
27
- refuses. Any entry in ~/.rigline/anchors.json is applied and named, with what this
28
- version makes of it. Rewrites ./generated.ts when the directory has one and records
29
- the new baseline; never commits either. Run it after an extension update, and reload
30
- webviews afterwards. Exits 1 when a person is needed.
31
-
32
- rigline check [--ext DIR]
33
- Read-only. Per installed version (or DIR): what moved since the baseline, which
34
- plugins this version would refuse and by which identifier, which curated anchors it
35
- lacks, and what it makes of each entry in ~/.rigline/anchors.json. Exits 1 when a
36
- person is needed.
37
-
38
- rigline watch [--interval SECONDS]
39
- install, and again whenever the set of installed extension directories changes,
40
- which is what an extension update looks like from outside VS Code.
41
-
42
- rigline dev [DIR...]
43
- Build the named plugin directories (or every first-party one), re-inject, and rebuild
44
- on every source change. Reload webviews after each one.
45
-
46
- rigline add PATH|SPEC [--now]
47
- Install a plugin into ~/.rigline/plugins, say what it can do, and re-inject. PATH is a
48
- directory; SPEC is an npm package, optionally @version or @tag. A published version
49
- must be a day old before it is installed; --now takes it anyway. No package manager
50
- runs, nothing is resolved, and none of the plugin's own code is evaluated.
51
-
52
- rigline update [NAME...] [--now]
53
- Move each plugin installed from npm to whatever its tag resolves to now, and
54
- re-inject. A plugin pinned to a version, added from a directory, or placed by hand is
55
- reported and left alone, as is a newer version too young to install.
56
-
57
- rigline remove NAME
58
- Delete a plugin rigline installed, and re-inject. A plugin you did not install this
59
- way is switched off in ~/.rigline/config.json instead; this will not delete it.
60
-
61
- rigline list
62
- Every plugin found, in the order they load: where it came from, whether it is
63
- switched off, and what its manifest says it can do.
64
-
65
- rigline status
66
- Per installed version: is each bundle vanilla or patched, judged against its backup.
67
-
68
- rigline restore
69
- Every installed version back to the extension's own bytes. Needs neither VS Code nor
70
- the extension to be working.
71
-
72
- rigline doctor [--out FILE] [--ext DIR]
73
- A markdown diagnostic to paste into a bug report: per installed version, whether it is
74
- patched, what the payload holds, which plugins are baked in and what any host patch
75
- did. For what the panel itself was doing, copy the probe's report from the RIG badge.
76
- `;
77
- function defaultPayloadDir() {
78
- const here = dirname(fileURLToPath(import.meta.url));
79
- return resolve(here, "..", "..", "host", "dist");
80
- }
81
- function repoPluginsDir() {
82
- const here = dirname(fileURLToPath(import.meta.url));
83
- return resolve(here, "..", "..", "..", "plugins");
84
- }
85
- function pluginOptions() {
86
- const paths = riglinePaths();
87
- return { roots: [repoPluginsDir(), paths.plugins], last: ["probe"], configPath: paths.config };
88
- }
89
- function reinject(options = {}) {
90
- const targets = options.exts ?? installedExtensions();
91
- if (targets.length === 0) {
92
- if (options.required)
93
- throw new UserError("no Claude Code extension is installed");
94
- console.log("No Claude Code extension is installed, so nothing was injected.");
95
- return 0;
96
- }
97
- const report = update({
98
- exts: targets,
99
- payloadDir: options.payloadDir ?? defaultPayloadDir(),
100
- plugins: pluginOptions(),
101
- codegen: true,
102
- });
103
- console.log(formatFlow(report));
104
- console.log(report.versions.some((v) => v.hostChanged)
105
- ? `\nA host patch changed: run Developer: Reload Window (this ends the window's sessions).`
106
- : `\nReload with Developer: Reload Webviews (current window only).`);
107
- return report.attention.length > 0 ? 1 : 0;
3
+ import { ENGINE_PACKAGE, engineDir, ensureEngine, formatEngineUpdate, readEngineState, updateEngine, wrapperVersion, } from "./engine.js";
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("\\");
108
10
  }
109
11
  async function addCommand(args) {
110
12
  const { values, positionals } = parseArgs({
@@ -115,383 +17,70 @@ async function addCommand(args) {
115
17
  if (positionals.length !== 1) {
116
18
  throw new UserError("add needs exactly one plugin directory or npm package");
117
19
  }
118
- const paths = riglinePaths();
119
20
  const spec = positionals[0];
120
- const common = {
121
- pluginsDir: paths.plugins,
122
- configPath: paths.config,
123
- otherRoots: [repoPluginsDir()],
124
- };
125
- const result = isPathSpec(spec)
126
- ? addPlugin({ from: spec, ...common })
127
- : await addFromNpm({ spec, ...common, registry: { ignoreReleaseAge: values.now } });
128
- console.log(`${result.replaced ? "replaced" : "added"} ${result.name} — ${result.dir}`);
129
- console.log(` from ${result.from}`);
130
- if (result.manifest.description)
131
- console.log(` ${result.manifest.description}`);
132
- for (const sentence of result.can)
133
- console.log(` - ${sentence}`);
134
- for (const patch of result.manifest.patches) {
135
- console.log(` - patches extension.js${patch.required ? " (required)" : ""}: ${patch.why}`);
136
- }
137
- if (result.disabled) {
138
- console.log(` "${result.name}" is switched off in ${paths.config}, so it will not load until you remove it from "disabled"`);
139
- }
140
- console.log("");
141
- return reinject();
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
+ });
142
29
  }
143
- function isPathSpec(spec) {
144
- if (spec.startsWith("@"))
145
- return false;
146
- if (spec.startsWith(".") || spec.includes("/") || spec.includes("\\"))
147
- return true;
148
- return existsSync(join(resolve(spec), "rigline.json"));
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;
149
35
  }
150
36
  async function updateCommand(args) {
151
37
  const { values, positionals } = parseArgs({
152
38
  args,
153
- options: { now: { type: "boolean", default: false } },
39
+ options: { now: { type: "boolean", default: false }, tag: { type: "string" } },
154
40
  allowPositionals: true,
155
41
  });
156
- const paths = riglinePaths();
42
+ const registry = { ignoreReleaseAge: values.now };
43
+ const engineUpdate = await updateEngine({ registry, ...(values.tag ? { tag: values.tag } : {}) });
44
+ console.log(formatEngineUpdate(engineUpdate));
45
+ const engine = await ensureEngine({ registry });
157
46
  const updates = await updatePlugins({
158
- pluginsDir: paths.plugins,
159
- configPath: paths.config,
160
- otherRoots: [repoPluginsDir()],
161
- names: positionals.length > 0 ? positionals : undefined,
162
- registry: { ignoreReleaseAge: values.now },
163
- });
164
- console.log(formatUpdates(updates));
165
- const moved = updates.some((u) => u.outcome === "updated");
166
- if (!moved) {
167
- return updates.some((u) => u.outcome === "failed") ? 1 : 0;
168
- }
169
- console.log("");
170
- const code = reinject();
171
- return updates.some((u) => u.outcome === "failed") ? 1 : code;
172
- }
173
- function removeCommand(args) {
174
- const { positionals } = parseArgs({ args, options: {}, allowPositionals: true });
175
- if (positionals.length !== 1)
176
- throw new UserError("remove needs exactly one plugin name");
177
- const paths = riglinePaths();
178
- const result = removePlugin({
179
- name: positionals[0],
180
- pluginsDir: paths.plugins,
181
- configPath: paths.config,
182
- otherRoots: [repoPluginsDir()],
183
- });
184
- console.log(`removed ${result.name} — ${result.dir}`);
185
- if (!result.hadSource)
186
- console.log(" it had no source record, so it was placed here by hand");
187
- if (result.wasDisabled)
188
- console.log(` and dropped from "disabled" in ${paths.config}`);
189
- console.log("");
190
- return reinject();
191
- }
192
- function installCommand(args) {
193
- const { values } = parseArgs({
194
- args,
195
- options: { ext: { type: "string" }, payload: { type: "string" } },
196
- allowPositionals: false,
47
+ listed: await listed(engine),
48
+ engine: engine.run,
49
+ ...(positionals.length > 0 ? { names: positionals } : {}),
50
+ registry,
197
51
  });
198
- return reinject({
199
- exts: values.ext ? [values.ext] : undefined,
200
- payloadDir: values.payload,
201
- required: true,
202
- });
203
- }
204
- function listCommand() {
205
- const paths = riglinePaths();
206
- console.log(formatPlugins(listPlugins({
207
- roots: [
208
- { label: "this checkout", path: repoPluginsDir() },
209
- { label: paths.plugins, path: paths.plugins, managed: true },
210
- ],
211
- last: ["probe"],
212
- configPath: paths.config,
213
- })));
214
- return 0;
215
- }
216
- function statusCommand() {
217
- const targets = installedExtensions();
218
- if (targets.length === 0)
219
- throw new UserError("no Claude Code extension is installed");
220
- for (const ext of targets) {
221
- const state = inspect(ext);
222
- console.log(`${extensionVersion(ext)}: webview ${verdict(state)}${state.markerPresent ? " (marker present)" : ""}` +
223
- `${state.backupExists ? "" : ", no backup"}; host ${hostVerdict(state)}` +
224
- `${state.hostBackupExists ? " (backup present)" : ""}`);
225
- console.log(` ${ext}`);
226
- }
227
- return 0;
228
- }
229
- function doctorCommand(args) {
230
- const { values } = parseArgs({
231
- args,
232
- options: { out: { type: "string" }, ext: { type: "string" } },
233
- allowPositionals: false,
234
- });
235
- const report = collect({ exts: values.ext ? [resolve(values.ext)] : undefined });
236
- const markdown = formatDoctor(report);
237
- if (values.out === undefined) {
238
- process.stdout.write(markdown);
239
- return 0;
240
- }
241
- const out = resolve(values.out);
242
- writeFileSync(out, markdown);
243
- const versions = report.installs.length;
244
- console.log(`wrote ${out}: ${versions} extension ${versions === 1 ? "version" : "versions"}`);
245
- return 0;
246
- }
247
- function restoreCommand() {
248
- const results = restoreAll(installedExtensions());
249
- let noBackup = 0;
250
- let hostFailed = 0;
251
- for (const result of results) {
252
- if (result.restored)
253
- console.log(`restored: ${result.ext}`);
254
- else {
255
- noBackup++;
256
- console.log(`NOT restored: ${result.ext} (${result.reason})`);
257
- }
258
- if (result.hostReason) {
259
- hostFailed++;
260
- console.log(` extension.js NOT restored: ${result.hostReason}`);
261
- }
52
+ let injected = 0;
53
+ if (engineUpdate.outcome === "moved" || engineUpdate.outcome === "installed") {
54
+ injected = await engine.run(["install"]);
262
55
  }
263
- if (noBackup > 0) {
264
- console.log("\nA version without a backup is recovered by uninstalling and reinstalling Claude Code from the Extensions view.");
265
- }
266
- if (hostFailed > 0) {
267
- console.log("\nAn extension.js left patched still runs the substitution a plugin declared. Reinstalling Claude Code from the Extensions view replaces it.");
268
- }
269
- console.log("Reload the window afterwards.");
270
- return noBackup + hostFailed > 0 ? 1 : 0;
271
- }
272
- function defaultGeneratedPath() {
273
- return resolve(process.cwd(), "generated.ts");
274
- }
275
- function codegen(args) {
276
- const { values, positionals } = parseArgs({
277
- args,
278
- options: { check: { type: "boolean", default: false }, out: { type: "string" } },
279
- allowPositionals: true,
280
- });
281
- if (positionals.length > 1)
282
- throw new UserError(`expected at most one directory: ${positionals}`);
283
- const ext = positionals[0] ?? findExtension();
284
- const out = values.out ?? defaultGeneratedPath();
285
- const generated = generate(harvestAll(readBundles(ext)));
286
- const label = relative(process.cwd(), out) || out;
287
- const ambiguous = reportAmbiguousAnchors(generated);
288
- if (values.check) {
289
- const current = existsSync(out) ? readFileSync(out, "utf8") : "";
290
- if (current !== generated.source) {
291
- console.log(`${label} is out of date for ${generated.tables.version}. Run: rigline codegen`);
292
- return 1;
293
- }
294
- console.log(`${label} is up to date for ${generated.tables.version}: ${generated.counts}`);
295
- return ambiguous ? 1 : 0;
296
- }
297
- writeFileSync(out, generated.source);
298
- console.log(`source: ${ext}`);
299
- console.log(`${generated.tables.version}: ${generated.counts}`);
300
- for (const module of generated.unreachableModules) {
301
- console.log(` unreachable stylesheet module: ${module}`);
302
- }
303
- for (const name of generated.anchors.unverified) {
304
- console.log(` unverified anchor: ${name} (its module's class map was never counted)`);
305
- }
306
- console.log(`wrote: ${label}`);
307
- return ambiguous ? 1 : 0;
308
- }
309
- function reportAmbiguousAnchors(generated) {
310
- if (generated.anchors.ambiguous.length === 0)
311
- return false;
312
- for (const { name, sites } of generated.anchors.ambiguous) {
313
- console.error(` ambiguous anchor: ${name} names one element, and ${generated.tables.version} applies its class at ${sites} places`);
314
- }
315
- console.error("Refine each in packages/plugin-api/src/anchors.ts, or change its kind to collection if it was never one element.");
316
- return true;
317
- }
318
- function diff(args) {
319
- if (args.length !== 2)
320
- throw new UserError("diff needs exactly two extension directories");
321
- const [a, b] = args;
322
- const from = scanOf(harvestAll(readBundles(a)));
323
- const to = scanOf(harvestAll(readBundles(b)));
324
- console.log(formatDiff(from, to, diffScans(from, to)));
325
- return 0;
326
- }
327
- async function build(args) {
328
- const { values, positionals } = parseArgs({
329
- args,
330
- options: { source: { type: "string" } },
331
- allowPositionals: true,
332
- });
333
- if (positionals.length > 1)
334
- throw new UserError(`expected at most one directory: ${positionals}`);
335
- const built = await buildPlugin({ dir: positionals[0], source: values.source });
336
- console.log(`built ${built.input} -> ${built.output}`);
56
+ console.log(formatUpdates(updates));
57
+ const failed = updates.some((u) => u.outcome === "failed") || engineUpdate.outcome === "failed";
58
+ return failed || injected !== 0 ? 1 : 0;
59
+ }
60
+ function versionCommand() {
61
+ const prefix = engineDir();
62
+ const state = readEngineState(prefix);
63
+ console.log(`rigline ${wrapperVersion()}`);
64
+ console.log(state.kind === "none"
65
+ ? `engine: none in ${prefix} — the next command installs one`
66
+ : `engine: ${ENGINE_PACKAGE} ${state.version} in ${prefix}` +
67
+ (state.kind === "unusable" ? ` (unusable: ${state.why})` : ""));
337
68
  return 0;
338
69
  }
339
- function extOption(args) {
340
- const { values } = parseArgs({ args, options: { ext: { type: "string" } } });
341
- return values.ext ? [resolve(values.ext)] : undefined;
342
- }
343
- function checkCommand(args) {
344
- const report = check({ exts: extOption(args), plugins: pluginOptions() });
345
- console.log(formatFlow(report));
346
- return report.attention.length > 0 ? 1 : 0;
347
- }
348
- function watchCommand(args) {
349
- const { values } = parseArgs({
350
- args,
351
- options: { interval: { type: "string" } },
352
- allowPositionals: false,
353
- });
354
- const seconds = values.interval === undefined ? 30 : Number(values.interval);
355
- if (!Number.isFinite(seconds) || seconds < 1) {
356
- throw new UserError(`--interval needs a number of seconds, got ${values.interval}`);
357
- }
358
- return new Promise((resolveWith) => {
359
- let code = 0;
360
- const watcher = watch({
361
- payloadDir: defaultPayloadDir(),
362
- plugins: pluginOptions(),
363
- codegen: true,
364
- intervalMs: seconds * 1000,
365
- onReport(report) {
366
- console.log(`
367
- [${new Date().toISOString()}]`);
368
- console.log(formatFlow(report));
369
- code = report.attention.length > 0 ? 1 : 0;
370
- },
371
- onError(error) {
372
- console.error(`rigline watch: ${error instanceof Error ? error.message : String(error)}`);
373
- code = 1;
374
- },
375
- });
376
- console.log(`watching ${EXTENSIONS_DIR} every ${seconds}s; Ctrl-C to stop`);
377
- const stop = () => {
378
- watcher.stop();
379
- resolveWith(code);
380
- };
381
- process.once("SIGINT", stop);
382
- process.once("SIGTERM", stop);
383
- });
384
- }
385
- async function dev(args) {
386
- const { positionals } = parseArgs({ args, options: {}, allowPositionals: true });
387
- const dirs = positionals.length > 0
388
- ? positionals.map((d) => resolve(d))
389
- : readdirSync(repoPluginsDir(), { withFileTypes: true })
390
- .filter((e) => e.isDirectory() && existsSync(join(repoPluginsDir(), e.name, "rigline.json")))
391
- .map((e) => join(repoPluginsDir(), e.name));
392
- if (dirs.length === 0)
393
- throw new UserError("no plugin directories to develop");
394
- const exts = installedExtensions();
395
- if (exts.length === 0)
396
- throw new UserError("no Claude Code extension is installed");
397
- const overrides = readAnchorOverrides();
398
- for (const problem of overrides.problems)
399
- console.error(`rigline dev: ${problem}`);
400
- async function once() {
401
- for (const dir of dirs) {
402
- const built = await buildPlugin({ dir });
403
- console.log(`built ${basename(dir)}: ${built.input} -> ${built.output}`);
404
- }
405
- let hostChanged = false;
406
- for (const ext of exts) {
407
- const report = install(ext, {
408
- payloadDir: defaultPayloadDir(),
409
- plugins: pluginOptions(),
410
- anchors: overrides,
411
- });
412
- hostChanged ||= report.hostChanged;
413
- for (const override of report.anchorOverrides) {
414
- console.log(` anchor override ${override.name}: ${overrides.path}`);
415
- }
416
- for (const verdict of report.verdicts) {
417
- if (verdict.refusal)
418
- console.log(` REFUSED ${verdict.plugin}: ${verdict.refusal}`);
419
- }
420
- }
421
- console.log(hostChanged
422
- ? "Developer: Reload Window (a host patch changed; this ends the window's sessions)"
423
- : "Developer: Reload Webviews");
424
- }
425
- await once();
426
- let pending = null;
427
- let building = false;
428
- const schedule = () => {
429
- if (pending)
430
- clearTimeout(pending);
431
- pending = setTimeout(() => {
432
- pending = null;
433
- if (building)
434
- return;
435
- building = true;
436
- once()
437
- .catch((error) => {
438
- console.error(`rigline dev: ${error instanceof Error ? error.message : String(error)}`);
439
- })
440
- .finally(() => {
441
- building = false;
442
- });
443
- }, 150);
444
- };
445
- for (const dir of dirs) {
446
- const src = join(dir, "src");
447
- if (!existsSync(src))
448
- continue;
449
- fsWatch(src, { recursive: true }, schedule);
450
- }
451
- console.log(`watching ${dirs.length} plugin source director${dirs.length === 1 ? "y" : "ies"}; Ctrl-C to stop`);
452
- return new Promise((resolveWith) => {
453
- process.once("SIGINT", () => resolveWith(0));
454
- process.once("SIGTERM", () => resolveWith(0));
455
- });
456
- }
457
70
  async function main(argv) {
458
71
  const [command, ...rest] = argv;
459
72
  switch (command) {
460
- case "codegen":
461
- return codegen(rest);
462
- case "diff":
463
- return diff(rest);
464
- case "build":
465
- return build(rest);
466
- case "install":
467
- return installCommand(rest);
468
- case "check":
469
- return checkCommand(rest);
470
- case "watch":
471
- return watchCommand(rest);
472
- case "dev":
473
- return dev(rest);
73
+ case "--version":
74
+ case "-v":
75
+ return versionCommand();
474
76
  case "add":
475
- return addCommand(rest);
77
+ return await addCommand(rest);
476
78
  case "update":
477
- return updateCommand(rest);
478
- case "remove":
479
- return removeCommand(rest);
480
- case "list":
481
- return listCommand();
482
- case "status":
483
- return statusCommand();
484
- case "restore":
485
- return restoreCommand();
486
- case "doctor":
487
- return doctorCommand(rest);
488
- case undefined:
489
- case "--help":
490
- case "-h":
491
- console.log(USAGE);
492
- return 0;
493
- default:
494
- throw new UserError(`unknown command "${command}"\n\n${USAGE}`);
79
+ return await updateCommand(rest);
80
+ default: {
81
+ const engine = await ensureEngine();
82
+ return await engine.run(argv);
83
+ }
495
84
  }
496
85
  }
497
86
  main(process.argv.slice(2)).then((code) => {
@@ -0,0 +1,134 @@
1
+ import { createHash } from "node:crypto";
2
+ import { UserError } from "./errors.js";
3
+ export const DEFAULT_REGISTRY = "https://registry.npmjs.org";
4
+ 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
+ export async function resolveVersion(spec, options = {}) {
20
+ const packument = await fetchPackument(spec.name, options);
21
+ const tag = spec.version === null ? (spec.tag ?? "latest") : null;
22
+ const version = spec.version ?? distTag(packument, spec.name, tag);
23
+ const release = packument.versions?.[version];
24
+ if (release === undefined) {
25
+ throw new UserError(`${spec.name} has no version ${version} on ${registryOf(options)}`);
26
+ }
27
+ const published = packument.time?.[version];
28
+ const ageMinutes = typeof published === "string" && !Number.isNaN(Date.parse(published))
29
+ ? Math.floor(((options.clock?.() ?? Date.now()) - Date.parse(published)) / 60_000)
30
+ : null;
31
+ const tarball = release.dist?.tarball;
32
+ if (typeof tarball !== "string" || tarball.length === 0) {
33
+ throw new UserError(`${spec.name}@${version} has no tarball on ${registryOf(options)}`);
34
+ }
35
+ return {
36
+ name: spec.name,
37
+ version,
38
+ tag,
39
+ tarball,
40
+ integrity: integrityOf(release, `${spec.name}@${version}`),
41
+ ageMinutes,
42
+ };
43
+ }
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
+ export function releaseAgeProblem(resolved, options = {}) {
72
+ if (options.ignoreReleaseAge)
73
+ return null;
74
+ const minimum = options.minimumReleaseAge ?? MINIMUM_RELEASE_AGE_MINUTES;
75
+ if (resolved.ageMinutes === null || resolved.ageMinutes >= minimum)
76
+ return null;
77
+ return (`${resolved.name}@${resolved.version} was published ${describeAge(resolved.ageMinutes)} ago, ` +
78
+ `and rigline waits ${describeAge(minimum)} before installing a version. That window is where ` +
79
+ "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 (D44). " +
81
+ "Wait, or pass --now.");
82
+ }
83
+ export function describeAge(minutes) {
84
+ if (minutes < 60)
85
+ return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
86
+ const hours = Math.floor(minutes / 60);
87
+ if (hours < 48)
88
+ return `${hours} ${hours === 1 ? "hour" : "hours"}`;
89
+ return `${Math.floor(hours / 24)} days`;
90
+ }
91
+ function distTag(packument, name, tag) {
92
+ const version = packument["dist-tags"]?.[tag];
93
+ if (typeof version === "string" && version.length > 0)
94
+ return version;
95
+ const tags = Object.keys(packument["dist-tags"] ?? {});
96
+ throw new UserError(`${name} has no "${tag}" tag${tags.length > 0 ? `; it has ${tags.join(", ")}` : ""}`);
97
+ }
98
+ function integrityOf(release, label) {
99
+ const { integrity, shasum } = release.dist ?? {};
100
+ if (typeof integrity === "string" && integrity.length > 0)
101
+ return integrity;
102
+ if (typeof shasum === "string" && /^[0-9a-f]{40}$/.test(shasum)) {
103
+ return `sha1-${Buffer.from(shasum, "hex").toString("base64")}`;
104
+ }
105
+ throw new UserError(`${label} carries no integrity hash, so its bytes cannot be checked`);
106
+ }
107
+ function registryOf(options) {
108
+ return (options.registry ?? process.env.npm_config_registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
109
+ }
110
+ async function fetchPackument(name, options) {
111
+ const url = `${registryOf(options)}/${name.replace("/", "%2F")}`;
112
+ const response = await request(url, options, { Accept: "application/json" });
113
+ return (await response.json());
114
+ }
115
+ async function request(url, options, headers) {
116
+ const impl = options.fetchImpl ?? globalThis.fetch;
117
+ if (typeof impl !== "function") {
118
+ throw new UserError("this Node has no fetch, so nothing can be installed from a registry");
119
+ }
120
+ let response;
121
+ try {
122
+ response = await impl(url, headers ? { headers } : undefined);
123
+ }
124
+ catch (error) {
125
+ throw new UserError(`could not reach ${url}: ${error.message}`);
126
+ }
127
+ if (!response.ok) {
128
+ throw new UserError(response.status === 404
129
+ ? `${url} is not there (404). Check the name, and that it is published.`
130
+ : `${url} answered ${response.status}`);
131
+ }
132
+ return response;
133
+ }
134
+ //# sourceMappingURL=registry.js.map
package/dist/remote.js ADDED
@@ -0,0 +1,132 @@
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 are installed";
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
@@ -0,0 +1,125 @@
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rigline",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.6",
4
4
  "description": "Install, update and develop plugins for the Claude Code VS Code extension",
5
5
  "keywords": [
6
6
  "rigline",
@@ -29,12 +29,8 @@
29
29
  "bin": {
30
30
  "rigline": "./dist/index.js"
31
31
  },
32
- "dependencies": {
33
- "@rigline/core": "1.0.0-alpha.4",
34
- "rolldown": "^1.2.8"
35
- },
36
32
  "scripts": {
37
- "build": "tsc -p tsconfig.build.json",
33
+ "build": "node ../../scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
38
34
  "typecheck": "tsc -p tsconfig.json"
39
35
  }
40
36
  }
package/dist/build.js DELETED
@@ -1,25 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join, relative, resolve } from "node:path";
3
- import { UserError } from "@rigline/core";
4
- import { build as bundle } from "rolldown";
5
- export async function buildPlugin(options = {}) {
6
- const dir = resolve(options.dir ?? process.cwd());
7
- const manifestPath = join(dir, "rigline.json");
8
- if (!existsSync(manifestPath))
9
- throw new UserError(`${dir} has no rigline.json`);
10
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
11
- if (typeof manifest.entry !== "string" || manifest.entry.length === 0) {
12
- throw new UserError(`${manifestPath} has no "entry"`);
13
- }
14
- const input = resolve(dir, options.source ?? "src/index.ts");
15
- if (!existsSync(input))
16
- throw new UserError(`no plugin source at ${input}`);
17
- const output = resolve(dir, manifest.entry);
18
- await bundle({
19
- input,
20
- platform: "browser",
21
- output: { file: output, format: "esm", sourcemap: "inline", codeSplitting: false },
22
- });
23
- return { input: relative(dir, input), output: relative(dir, output) };
24
- }
25
- //# sourceMappingURL=build.js.map