riftri 0.4.0 → 0.5.0
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 +5 -3
- package/bin/riftri.js +6 -10
- package/lib/client.d.ts +19 -1
- package/lib/client.js +72 -8
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -162,9 +162,11 @@ $ sudo riftri overlayfs install-helper
|
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
**Riftri fails closed.** When a checkout cannot be reproduced exactly — custom
|
|
165
|
-
filters, sparse
|
|
166
|
-
[Git LFS](docs/git-lfs.md) setups
|
|
167
|
-
than silently falling back to a full
|
|
165
|
+
filters, a repository's own sparse-checkout profile, submodules, external
|
|
166
|
+
attributes, or non-canonical [Git LFS](docs/git-lfs.md) setups
|
|
167
|
+
— it stops before changing anything rather than silently falling back to a full
|
|
168
|
+
copy. Sparse worktrees are supported when you ask for them explicitly, with
|
|
169
|
+
[`--sparse-dir` cone selection](docs/sparse-checkout.md). See
|
|
168
170
|
[how Riftri stays safe](docs/safety.md).
|
|
169
171
|
|
|
170
172
|
## Common commands
|
package/bin/riftri.js
CHANGED
|
@@ -50,22 +50,18 @@ if (process.platform !== "win32") {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
}
|
|
53
|
+
// The handlers deliberately stay installed until the process exits. Removing
|
|
54
|
+
// the last listener for a signal restores its default disposition, so a SIGINT
|
|
55
|
+
// still in flight — the common case, since the terminal sent it to the whole
|
|
56
|
+
// foreground group — would kill the launcher in the window before
|
|
57
|
+
// `process.exit`, losing the command's real status. `process.exit` does not
|
|
58
|
+
// need them removed.
|
|
61
59
|
|
|
62
60
|
child.on("error", (error) => {
|
|
63
|
-
releaseSignals();
|
|
64
61
|
fail(`could not start the native executable at ${binary}: ${error.message}`);
|
|
65
62
|
});
|
|
66
63
|
|
|
67
64
|
child.on("exit", (code, signal) => {
|
|
68
|
-
releaseSignals();
|
|
69
65
|
if (signal !== null) {
|
|
70
66
|
const exitCode = signalExitCode(signal);
|
|
71
67
|
if (exitCode === null) {
|
package/lib/client.d.ts
CHANGED
|
@@ -20,7 +20,15 @@ export interface FailureReceipt {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export class RiftriError extends Error {
|
|
23
|
+
/**
|
|
24
|
+
* Always a number. A process killed by a signal reports 128 plus the signal
|
|
25
|
+
* number, the convention native `riftri exec` uses for its own children.
|
|
26
|
+
*/
|
|
23
27
|
readonly exitCode: number;
|
|
28
|
+
/** The signal that killed the process, or null if it exited on its own. */
|
|
29
|
+
readonly signal: NodeJS.Signals | null;
|
|
30
|
+
/** The process was killed rather than exiting on its own. */
|
|
31
|
+
readonly wasSignalled: boolean;
|
|
24
32
|
readonly receipt: FailureReceipt | null;
|
|
25
33
|
/** Riftri declined before touching anything; falling back is safe. */
|
|
26
34
|
readonly isPolicyRefusal: boolean;
|
|
@@ -136,7 +144,17 @@ export class Riftri {
|
|
|
136
144
|
/** Inspect Git and storage. Creates nothing. */
|
|
137
145
|
doctor(options?: { destination?: string }): Promise<DoctorReport>;
|
|
138
146
|
backends(path?: string): Promise<unknown>;
|
|
139
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* True when this destination can be optimized.
|
|
149
|
+
*
|
|
150
|
+
* Resolves `false` only when Riftri answers the question: no active
|
|
151
|
+
* copy-on-write backend, a destination Riftri reports as `blocked`, or an
|
|
152
|
+
* outright refusal. A missing or non-executable binary, an unreadable
|
|
153
|
+
* repository, or output that is not JSON rejects instead.
|
|
154
|
+
*
|
|
155
|
+
* A `needs-activation` destination is optimizable: `worktree.add` works
|
|
156
|
+
* without `enable()`, which only gates Git interception.
|
|
157
|
+
*/
|
|
140
158
|
isOptimizable(destination?: string): Promise<boolean>;
|
|
141
159
|
|
|
142
160
|
enable(): Promise<null>;
|
package/lib/client.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const { spawn } = require("node:child_process");
|
|
11
11
|
const { resolveBinary } = require("./platform.js");
|
|
12
|
+
const { signalExitCode } = require("./signals.js");
|
|
12
13
|
|
|
13
14
|
/** Process exit codes. Documented in docs/cli.md. */
|
|
14
15
|
const EXIT_SUCCESS = 0;
|
|
@@ -27,13 +28,25 @@ const REPORTING = new Set([
|
|
|
27
28
|
]);
|
|
28
29
|
|
|
29
30
|
class RiftriError extends Error {
|
|
30
|
-
constructor(exitCode, receipt, stderr) {
|
|
31
|
-
super(
|
|
31
|
+
constructor(exitCode, receipt, stderr, signal = null) {
|
|
32
|
+
super(
|
|
33
|
+
receipt?.message ||
|
|
34
|
+
stderr.trim() ||
|
|
35
|
+
(signal ? `riftri terminated by ${signal}` : `riftri exited ${exitCode}`),
|
|
36
|
+
);
|
|
32
37
|
this.name = "RiftriError";
|
|
38
|
+
// Always a number. A signalled process reports 128 + the signal number,
|
|
39
|
+
// the convention native `riftri exec` already uses for its own children.
|
|
33
40
|
this.exitCode = exitCode;
|
|
41
|
+
this.signal = signal;
|
|
34
42
|
this.receipt = receipt ?? null;
|
|
35
43
|
}
|
|
36
44
|
|
|
45
|
+
/** The process was killed rather than exiting on its own. */
|
|
46
|
+
get wasSignalled() {
|
|
47
|
+
return this.signal !== null;
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
/** Riftri declined before touching anything. Falling back is safe. */
|
|
38
51
|
get isPolicyRefusal() {
|
|
39
52
|
return this.exitCode === EXIT_POLICY;
|
|
@@ -55,6 +68,18 @@ class RiftriError extends Error {
|
|
|
55
68
|
}
|
|
56
69
|
}
|
|
57
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Place Riftri's own flags before any `--`. Appending them instead puts them
|
|
73
|
+
* inside the payload `riftri exec` hands to the child, which then fails on a
|
|
74
|
+
* flag meant for Riftri.
|
|
75
|
+
*/
|
|
76
|
+
function withRiftriFlags(args, flags) {
|
|
77
|
+
if (flags.length === 0) return [...args];
|
|
78
|
+
const boundary = args.indexOf("--");
|
|
79
|
+
if (boundary === -1) return [...args, ...flags];
|
|
80
|
+
return [...args.slice(0, boundary), ...flags, ...args.slice(boundary)];
|
|
81
|
+
}
|
|
82
|
+
|
|
58
83
|
function flagsFor(options) {
|
|
59
84
|
const flags = [];
|
|
60
85
|
for (const [key, value] of Object.entries(options)) {
|
|
@@ -104,7 +129,7 @@ class Riftri {
|
|
|
104
129
|
return new Promise((resolve, reject) => {
|
|
105
130
|
let child;
|
|
106
131
|
try {
|
|
107
|
-
child = spawn(this.#executable(),
|
|
132
|
+
child = spawn(this.#executable(), withRiftriFlags(args, flags), {
|
|
108
133
|
cwd,
|
|
109
134
|
stdio: ["ignore", "pipe", "pipe"],
|
|
110
135
|
});
|
|
@@ -117,9 +142,33 @@ class Riftri {
|
|
|
117
142
|
child.stdout.on("data", (chunk) => (stdout += chunk));
|
|
118
143
|
child.stderr.on("data", (chunk) => (stderr += chunk));
|
|
119
144
|
child.on("error", reject);
|
|
120
|
-
child.on("close", (exitCode) => {
|
|
145
|
+
child.on("close", (exitCode, signal) => {
|
|
146
|
+
// A signalled child reports a null exit code. Translate it rather than
|
|
147
|
+
// letting `null` reach callers through a field typed as a number.
|
|
148
|
+
if (signal) {
|
|
149
|
+
const code = signalExitCode(signal) ?? EXIT_OPERATIONAL;
|
|
150
|
+
reject(new RiftriError(code, null, stderr, signal));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
121
153
|
if (exitCode === EXIT_SUCCESS) {
|
|
122
|
-
|
|
154
|
+
if (!json || !stdout.trim()) {
|
|
155
|
+
resolve(null);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
resolve(JSON.parse(stdout));
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// Thrown from a close handler this would be uncatchable and would
|
|
162
|
+
// take the host process down, so it has to reject instead.
|
|
163
|
+
reject(
|
|
164
|
+
new RiftriError(
|
|
165
|
+
EXIT_OPERATIONAL,
|
|
166
|
+
null,
|
|
167
|
+
`riftri ${args.join(" ")} returned output that is not JSON: ` +
|
|
168
|
+
`${error.message}\n${stdout.slice(0, 2000)}`,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
123
172
|
return;
|
|
124
173
|
}
|
|
125
174
|
let receipt = null;
|
|
@@ -148,13 +197,28 @@ class Riftri {
|
|
|
148
197
|
return this.run(["backends", ...target]);
|
|
149
198
|
}
|
|
150
199
|
|
|
151
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* True when this destination can get an optimized worktree.
|
|
202
|
+
*
|
|
203
|
+
* `false` is an answer from Riftri: either the report says no backend is
|
|
204
|
+
* active, or Riftri refused this destination outright. A broken
|
|
205
|
+
* installation, an unreadable repository, or any other unexpected failure
|
|
206
|
+
* rejects instead, so a caller can tell "not supported here" apart from
|
|
207
|
+
* "this client cannot run at all".
|
|
208
|
+
*/
|
|
152
209
|
async isOptimizable(destination) {
|
|
153
210
|
try {
|
|
154
211
|
const report = await this.doctor(destination ? { destination } : {});
|
|
155
|
-
|
|
212
|
+
if (!report?.cow_backend_active) return false;
|
|
213
|
+
// cow_backend_active is a fact about the volume, not about this
|
|
214
|
+
// destination. Outside a Git repository it is still true while
|
|
215
|
+
// `worktree add` cannot possibly succeed, so the readiness verdict
|
|
216
|
+
// decides. `needs-activation` stays optimizable: the explicit interface
|
|
217
|
+
// works without `enable`, which only gates Git interception.
|
|
218
|
+
const readiness = report.destination_readiness?.status;
|
|
219
|
+
return readiness === undefined || readiness !== "blocked";
|
|
156
220
|
} catch (error) {
|
|
157
|
-
if (error
|
|
221
|
+
if (error instanceof RiftriError && error.isPolicyRefusal) return false;
|
|
158
222
|
throw error;
|
|
159
223
|
}
|
|
160
224
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "riftri",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Experimental lightweight Git workspaces for parallel development",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Assistant UI contributors",
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"node": ">=18.18"
|
|
19
19
|
},
|
|
20
20
|
"optionalDependencies": {
|
|
21
|
-
"riftri-darwin-arm64": "0.
|
|
22
|
-
"riftri-darwin-x64": "0.
|
|
23
|
-
"riftri-linux-arm64-gnu": "0.
|
|
24
|
-
"riftri-linux-arm64-musl": "0.
|
|
25
|
-
"riftri-linux-x64-gnu": "0.
|
|
26
|
-
"riftri-linux-x64-musl": "0.
|
|
27
|
-
"riftri-win32-arm64": "0.
|
|
28
|
-
"riftri-win32-x64": "0.
|
|
21
|
+
"riftri-darwin-arm64": "0.5.0",
|
|
22
|
+
"riftri-darwin-x64": "0.5.0",
|
|
23
|
+
"riftri-linux-arm64-gnu": "0.5.0",
|
|
24
|
+
"riftri-linux-arm64-musl": "0.5.0",
|
|
25
|
+
"riftri-linux-x64-gnu": "0.5.0",
|
|
26
|
+
"riftri-linux-x64-musl": "0.5.0",
|
|
27
|
+
"riftri-win32-arm64": "0.5.0",
|
|
28
|
+
"riftri-win32-x64": "0.5.0"
|
|
29
29
|
},
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|