@vigent/cli 0.0.0-stage → 0.2.1
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/LICENSE +21 -0
- package/README.md +261 -2
- package/dist/agents.js +65 -0
- package/dist/app.js +2487 -0
- package/dist/builtin-themes.js +352 -0
- package/dist/choices.js +73 -0
- package/dist/cli-engines.js +537 -0
- package/dist/cli.js +440 -0
- package/dist/clipboard.js +56 -0
- package/dist/color.js +126 -0
- package/dist/config-edit.js +61 -0
- package/dist/config.js +406 -0
- package/dist/demo.js +81 -0
- package/dist/diffview.js +94 -0
- package/dist/engines.js +195 -0
- package/dist/export.js +29 -0
- package/dist/float.js +182 -0
- package/dist/import.js +260 -0
- package/dist/keys.js +589 -0
- package/dist/markdown.js +80 -0
- package/dist/palette.js +83 -0
- package/dist/picker.js +301 -0
- package/dist/review.js +158 -0
- package/dist/riftri.js +284 -0
- package/dist/session.js +460 -0
- package/dist/settings.js +174 -0
- package/dist/suggest.js +31 -0
- package/dist/theme-new.js +49 -0
- package/dist/theme.js +487 -0
- package/dist/tree.js +115 -0
- package/dist/version.js +45 -0
- package/dist/vimcolors.js +206 -0
- package/dist/worktree.js +357 -0
- package/package.json +64 -4
package/dist/riftri.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight agent worktrees with riftri (https://riftri.dev).
|
|
3
|
+
*
|
|
4
|
+
* riftri creates real git linked worktrees whose unchanged files share
|
|
5
|
+
* copy-on-write storage (APFS clones, Btrfs/XFS reflinks, ReFS block clones,
|
|
6
|
+
* or OverlayFS), so ten agents on one repo cost about one checkout of disk.
|
|
7
|
+
* vigent drives riftri's CLI through its machine-readable contract: one JSON
|
|
8
|
+
* report on stdout with --json, one JSON receipt on stderr with --json-errors.
|
|
9
|
+
*
|
|
10
|
+
* With `[worktree] backend = "auto"` (the default) vigent uses riftri when it's
|
|
11
|
+
* installed and falls back to plain `git worktree` only when riftri declines
|
|
12
|
+
* with a policy receipt that says nothing was changed. Every other failure is
|
|
13
|
+
* reported, never papered over.
|
|
14
|
+
*/
|
|
15
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { onPath } from "./engines.js";
|
|
20
|
+
export const WORKTREE_BACKENDS = ["auto", "riftri", "git"];
|
|
21
|
+
export const RIFTRI_INSTALL = "npm install -g riftri (or: curl -fsSL https://riftri.dev/install.sh | bash)";
|
|
22
|
+
let cachedBinary;
|
|
23
|
+
/**
|
|
24
|
+
* The riftri executable: $RIFTRI_BINARY, the `riftri` npm package (installed
|
|
25
|
+
* with vigent), or `riftri` on PATH. Undefined when there's none.
|
|
26
|
+
*/
|
|
27
|
+
export function riftriBinary(env = process.env) {
|
|
28
|
+
const key = `${env.RIFTRI_BINARY ?? ""}\0${env.PATH ?? ""}`;
|
|
29
|
+
if (cachedBinary?.key === key)
|
|
30
|
+
return cachedBinary.bin;
|
|
31
|
+
let bin;
|
|
32
|
+
if (env.RIFTRI_BINARY)
|
|
33
|
+
bin = fs.existsSync(env.RIFTRI_BINARY) ? env.RIFTRI_BINARY : undefined;
|
|
34
|
+
else {
|
|
35
|
+
try {
|
|
36
|
+
// the package resolves and verifies the native binary for this platform
|
|
37
|
+
const pkg = createRequire(import.meta.url)("riftri");
|
|
38
|
+
bin = pkg.resolveBinary();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
bin = onPath("riftri", env) ? "riftri" : undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
cachedBinary = { key, bin };
|
|
45
|
+
return bin;
|
|
46
|
+
}
|
|
47
|
+
function parseReceipt(stderr) {
|
|
48
|
+
const lines = stderr.trim().split("\n").reverse();
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (!line.trim().startsWith("{"))
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(line);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// not the receipt
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function failure(err) {
|
|
62
|
+
const exitCode = typeof err?.status === "number" ? err.status : typeof err?.code === "number" ? err.code : 1;
|
|
63
|
+
const stderr = String(err?.stderr ?? "");
|
|
64
|
+
const receipt = parseReceipt(stderr);
|
|
65
|
+
const message = receipt?.message ??
|
|
66
|
+
(err?.code === "ENOENT" ? "riftri isn't installed" : stderr.trim().split("\n").pop() || err?.message || `exited with code ${exitCode}`);
|
|
67
|
+
// only a policy receipt that confirms no cleanup is needed makes falling back safe
|
|
68
|
+
const declined = exitCode === 3 && receipt?.category === "policy" && receipt.cleanup === "not-needed" && receipt.code !== "storage-full";
|
|
69
|
+
return { ok: false, exitCode, receipt, message, declined };
|
|
70
|
+
}
|
|
71
|
+
function parseReport(stdout) {
|
|
72
|
+
const text = stdout.trim();
|
|
73
|
+
return text ? JSON.parse(text) : null;
|
|
74
|
+
}
|
|
75
|
+
/** Run riftri and parse its report or receipt. Never throws. */
|
|
76
|
+
export function riftriSync(args, cwd, env = process.env, bin = riftriBinary(env)) {
|
|
77
|
+
if (!bin)
|
|
78
|
+
return { ok: false, exitCode: 127, message: "riftri isn't installed", declined: false };
|
|
79
|
+
try {
|
|
80
|
+
const out = execFileSync(bin, ["--json-errors", ...args], {
|
|
81
|
+
cwd,
|
|
82
|
+
env,
|
|
83
|
+
encoding: "utf8",
|
|
84
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
85
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
86
|
+
timeout: 10 * 60 * 1000,
|
|
87
|
+
});
|
|
88
|
+
return { ok: true, report: parseReport(out) };
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return failure(err);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** The same, without blocking the UI (doctor and status walk the disk). */
|
|
95
|
+
export function riftriAsync(args, cwd, env = process.env, bin = riftriBinary(env)) {
|
|
96
|
+
if (!bin)
|
|
97
|
+
return Promise.resolve({ ok: false, exitCode: 127, message: "riftri isn't installed", declined: false });
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
execFile(bin, ["--json-errors", ...args], { cwd, env, maxBuffer: 64 * 1024 * 1024, timeout: 2 * 60 * 1000 }, (err, stdout, stderr) => {
|
|
100
|
+
if (err)
|
|
101
|
+
return resolve(failure({ ...err, status: err.code, stderr }));
|
|
102
|
+
try {
|
|
103
|
+
resolve({ ok: true, report: parseReport(String(stdout)) });
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
resolve({ ok: false, exitCode: 1, message: `riftri printed something that isn't JSON: ${e.message}`, declined: false });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** A failed riftri step as one line, with the command riftri suggests. */
|
|
112
|
+
export function describeFailure(r) {
|
|
113
|
+
const next = r.receipt?.nextCommand;
|
|
114
|
+
return `${r.message}${next ? ` (riftri suggests: ${next})` : ""}`;
|
|
115
|
+
}
|
|
116
|
+
/** `riftri worktree add <folder> -b <branch> <base>` */
|
|
117
|
+
export function riftriAdd(root, folder, branch, base, env) {
|
|
118
|
+
return riftriSync(["worktree", "add", folder, "-b", branch, base, "--json", "--no-progress"], root, env);
|
|
119
|
+
}
|
|
120
|
+
/** The repository's riftri state folder (<common git dir>/riftri), if riftri has ever been used here. */
|
|
121
|
+
export function riftriStateDir(root) {
|
|
122
|
+
try {
|
|
123
|
+
const common = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
124
|
+
cwd: root,
|
|
125
|
+
encoding: "utf8",
|
|
126
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
127
|
+
}).trim();
|
|
128
|
+
const dir = path.join(common, "riftri");
|
|
129
|
+
return fs.existsSync(dir) ? dir : undefined;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Does riftri manage this worktree? Answered without running riftri when
|
|
137
|
+
* the repository has no riftri state. An ownership error is not "unmanaged":
|
|
138
|
+
* riftri's contract is to never turn it into ordinary git cleanup.
|
|
139
|
+
*/
|
|
140
|
+
export function riftriOwnership(root, worktreePath, env) {
|
|
141
|
+
if (!riftriStateDir(root))
|
|
142
|
+
return { managed: false };
|
|
143
|
+
if (!riftriBinary(env)) {
|
|
144
|
+
return { managed: false, error: `this repository has riftri worktrees, but riftri isn't installed: ${RIFTRI_INSTALL}` };
|
|
145
|
+
}
|
|
146
|
+
const r = riftriSync(["worktree", "inspect", "--json", "--", worktreePath], root, env);
|
|
147
|
+
if (!r.ok)
|
|
148
|
+
return { managed: false, error: describeFailure(r) };
|
|
149
|
+
const w = r.report?.worktrees?.[0];
|
|
150
|
+
return w?.state_directory ? { managed: true, backend: w.backend ?? undefined } : { managed: false };
|
|
151
|
+
}
|
|
152
|
+
/** `riftri worktree remove [--force]`; dirty views are refused unless forced. */
|
|
153
|
+
export function riftriRemove(root, worktreePath, force, env) {
|
|
154
|
+
return riftriSync(["worktree", "remove", ...(force ? ["--force"] : []), worktreePath, "--json", "--no-progress"], root, env);
|
|
155
|
+
}
|
|
156
|
+
/** Reclaim bases no worktree uses any more (`riftri gc --apply`). Best effort. */
|
|
157
|
+
export function riftriGc(root, env) {
|
|
158
|
+
return riftriSync(["gc", "--apply", "--json", "--no-progress"], root, env);
|
|
159
|
+
}
|
|
160
|
+
const READY_MOUNT = new Set([null, undefined, "active"]);
|
|
161
|
+
/**
|
|
162
|
+
* Before reopening agents: inspect their worktrees, repair views whose
|
|
163
|
+
* OverlayFS mount went away (a reboot), and inspect again. A repair that
|
|
164
|
+
* succeeds is not proof by itself; only the second inspection is.
|
|
165
|
+
*/
|
|
166
|
+
export function prepareViews(root, paths, env) {
|
|
167
|
+
const out = new Map();
|
|
168
|
+
for (const p of paths)
|
|
169
|
+
out.set(p, { path: p, ready: true });
|
|
170
|
+
if (paths.length === 0 || !riftriStateDir(root) || !riftriBinary(env))
|
|
171
|
+
return out;
|
|
172
|
+
const inspect = () => riftriSync(["worktree", "inspect", "--json", "--", ...paths], root, env);
|
|
173
|
+
let r = inspect();
|
|
174
|
+
if (!r.ok) {
|
|
175
|
+
for (const p of paths)
|
|
176
|
+
out.set(p, { path: p, ready: false, reason: describeFailure(r) });
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
const needsRepair = new Set(r.report.worktrees.filter((w) => w.mount_status === "recovery-required" && w.state_directory).map((w) => w.state_directory));
|
|
180
|
+
if (needsRepair.size > 0) {
|
|
181
|
+
for (const dir of needsRepair)
|
|
182
|
+
riftriSync(["repair", "--state-dir", dir, "--json", "--no-progress"], root, env);
|
|
183
|
+
r = inspect();
|
|
184
|
+
if (!r.ok) {
|
|
185
|
+
for (const p of paths)
|
|
186
|
+
out.set(p, { path: p, ready: false, reason: describeFailure(r) });
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
r.report.worktrees.forEach((w, i) => {
|
|
191
|
+
const p = paths[i] ?? w.path;
|
|
192
|
+
const ready = READY_MOUNT.has(w.mount_status);
|
|
193
|
+
out.set(p, {
|
|
194
|
+
path: p,
|
|
195
|
+
ready,
|
|
196
|
+
backend: w.backend ?? undefined,
|
|
197
|
+
reason: ready ? undefined : `riftri reports its mount as ${w.mount_status}; run: riftri repair --state-dir '${w.state_directory}'`,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
/** Which of these worktrees riftri manages, and with which backend (no disk walk). */
|
|
203
|
+
export function riftriBackends(root, paths, env) {
|
|
204
|
+
const out = new Map();
|
|
205
|
+
if (paths.length === 0 || !riftriStateDir(root) || !riftriBinary(env))
|
|
206
|
+
return out;
|
|
207
|
+
const r = riftriSync(["worktree", "inspect", "--json", "--", ...paths], root, env);
|
|
208
|
+
if (!r.ok)
|
|
209
|
+
return out;
|
|
210
|
+
r.report.worktrees.forEach((w, i) => {
|
|
211
|
+
if (w.state_directory && paths[i])
|
|
212
|
+
out.set(paths[i], w.backend ?? "riftri");
|
|
213
|
+
});
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
/** Ask riftri whether new agent worktrees in `dir` can be copy-on-write, and what the existing ones cost. */
|
|
217
|
+
export async function riftriHealth(root, dir, backend, env = process.env) {
|
|
218
|
+
const bin = riftriBinary(env);
|
|
219
|
+
if (backend === "git")
|
|
220
|
+
return { installed: Boolean(bin), ready: false, detail: "off ([worktree] backend = \"git\")", blockers: [] };
|
|
221
|
+
if (!bin)
|
|
222
|
+
return { installed: false, ready: false, detail: "not installed: agent worktrees are full copies", blockers: [] };
|
|
223
|
+
const v = await new Promise((resolve) => execFile(bin, ["--version"], { env, timeout: 10_000 }, (err, stdout) => resolve(err ? undefined : /\d+\.\d+\.\d+[\w.-]*/.exec(String(stdout))?.[0])));
|
|
224
|
+
if (!root || !dir)
|
|
225
|
+
return { installed: true, version: v, ready: false, detail: "not in a git repository", blockers: [] };
|
|
226
|
+
// doctor needs a destination that doesn't exist yet
|
|
227
|
+
const probe = path.join(dir, `.vigent-probe-${process.pid}`);
|
|
228
|
+
const doc = await riftriAsync(["doctor", "--destination", probe, "--json"], root, env, bin);
|
|
229
|
+
if (!doc.ok)
|
|
230
|
+
return { installed: true, version: v, ready: false, detail: describeFailure(doc), blockers: [] };
|
|
231
|
+
const rd = doc.report?.destination_readiness ?? {};
|
|
232
|
+
// needs-activation only concerns riftri's git shim; vigent calls riftri directly
|
|
233
|
+
const blockers = (rd.blockers ?? [])
|
|
234
|
+
.filter((b) => b.kind !== "repository-activation")
|
|
235
|
+
.map((b) => `${b.explanation}${b.remedy ? ` · ${b.remedy}` : ""}`);
|
|
236
|
+
const ready = (rd.status === "ready" || rd.status === "needs-activation") && Boolean(rd.copy_on_write);
|
|
237
|
+
const health = {
|
|
238
|
+
installed: true,
|
|
239
|
+
version: v,
|
|
240
|
+
ready,
|
|
241
|
+
backend: rd.backend ?? undefined,
|
|
242
|
+
detail: ready ? `copy-on-write worktrees (${backendName(rd.backend)})` : blockers.length ? "can't optimize this repository yet" : `not available here (${rd.status ?? "blocked"})`,
|
|
243
|
+
blockers,
|
|
244
|
+
next: rd.status === "needs-activation" ? undefined : (rd.next_command ?? undefined),
|
|
245
|
+
};
|
|
246
|
+
if (riftriStateDir(root)) {
|
|
247
|
+
const st = await riftriAsync(["status", "--json"], root, env, bin);
|
|
248
|
+
if (st.ok && st.report) {
|
|
249
|
+
health.views = st.report.operations?.active_views ?? st.report.worktrees?.length ?? 0;
|
|
250
|
+
health.logicalBytes = st.report.total_logical_bytes;
|
|
251
|
+
health.allocatedBytes = st.report.total_allocated_bytes;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return health;
|
|
255
|
+
}
|
|
256
|
+
export function backendName(backend) {
|
|
257
|
+
switch (backend) {
|
|
258
|
+
case "reflink":
|
|
259
|
+
return "Btrfs/XFS reflinks";
|
|
260
|
+
case "apfs-clone":
|
|
261
|
+
case "apfs":
|
|
262
|
+
return "APFS clones";
|
|
263
|
+
case "refs-block-clone":
|
|
264
|
+
case "refs":
|
|
265
|
+
return "ReFS block clones";
|
|
266
|
+
case "overlay-fs":
|
|
267
|
+
return "OverlayFS";
|
|
268
|
+
default:
|
|
269
|
+
return backend ?? "unknown";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** 16211491 -> "15.5 MB" */
|
|
273
|
+
export function formatBytes(n) {
|
|
274
|
+
if (n === undefined || !Number.isFinite(n))
|
|
275
|
+
return "?";
|
|
276
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
277
|
+
let i = 0;
|
|
278
|
+
let v = n;
|
|
279
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
280
|
+
v /= 1024;
|
|
281
|
+
i++;
|
|
282
|
+
}
|
|
283
|
+
return `${v >= 100 || i === 0 ? Math.round(v) : v.toFixed(1)} ${units[i]}`;
|
|
284
|
+
}
|