riftri 0.5.0 → 0.6.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 +0 -5
- package/bin/riftri.js +37 -7
- package/lib/client.d.ts +17 -3
- package/lib/client.js +41 -17
- package/lib/platform.js +1 -16
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -73,11 +73,6 @@ Or install through npm, which fetches the binary for your platform:
|
|
|
73
73
|
npm install --global riftri
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
> **Windows on ARM64 is not yet available through npm.** Its platform package
|
|
77
|
-
> is held up in registry review, so npm installs there resolve without a
|
|
78
|
-
> binary. Use the PowerShell installer above until that clears. Every other
|
|
79
|
-
> platform — macOS, Linux, and Windows x64 — installs normally.
|
|
80
|
-
|
|
81
76
|
## Quick start
|
|
82
77
|
|
|
83
78
|
For an interactive walkthrough (v0.3.1 and later), run
|
package/bin/riftri.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
5
|
const { spawn } = require("node:child_process");
|
|
6
|
+
const tty = require("node:tty");
|
|
6
7
|
const { resolveBinary } = require("../lib/platform.js");
|
|
7
8
|
const { signalExitCode } = require("../lib/signals.js");
|
|
8
9
|
|
|
@@ -18,11 +19,6 @@ try {
|
|
|
18
19
|
fail(error instanceof Error ? error.message : String(error));
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
const child = spawn(binary, process.argv.slice(2), {
|
|
22
|
-
stdio: "inherit",
|
|
23
|
-
windowsHide: false,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
22
|
// Mirror the termination contract of native `riftri exec` (docs/cli.md) while
|
|
27
23
|
// the native process runs. The launcher survives SIGINT (Ctrl-C) and SIGQUIT
|
|
28
24
|
// (Ctrl-\): a terminal delivers both to the whole foreground process group,
|
|
@@ -34,13 +30,37 @@ const child = spawn(binary, process.argv.slice(2), {
|
|
|
34
30
|
// same contract downstream to the scoped command. Windows has no equivalent
|
|
35
31
|
// to preserve: the console already delivers Ctrl-C events to every attached
|
|
36
32
|
// process, and a hard TerminateProcess cannot be intercepted.
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
//
|
|
34
|
+
// Without a terminal (an agent harness, CI, or a script), native `riftri
|
|
35
|
+
// exec` forwards SIGINT and SIGQUIT as well and runs its child in its own
|
|
36
|
+
// process group. The launcher does the same there: otherwise a PID-directed
|
|
37
|
+
// SIGINT was ignored outright and the command ran on, where the native binary
|
|
38
|
+
// would have stopped it. The native process gets its own group so that a
|
|
39
|
+
// signal sent to the caller's whole group reaches it once, through here, and
|
|
40
|
+
// not a second time directly. `isatty` is used rather than `process.stdin`,
|
|
41
|
+
// whose initialization can switch the inherited descriptor to non-blocking.
|
|
42
|
+
const supervised =
|
|
43
|
+
process.platform !== "win32" && ![0, 1, 2].some((descriptor) => tty.isatty(descriptor));
|
|
44
|
+
const ignoredSignals = supervised ? [] : ["SIGINT", "SIGQUIT"];
|
|
45
|
+
const forwardedSignals = supervised
|
|
46
|
+
? ["SIGTERM", "SIGHUP", "SIGINT", "SIGQUIT"]
|
|
47
|
+
: ["SIGTERM", "SIGHUP"];
|
|
39
48
|
const ignoreSignal = () => {};
|
|
49
|
+
let child;
|
|
50
|
+
const pendingForwardedSignals = [];
|
|
40
51
|
const forwardSignal = (signal) => {
|
|
52
|
+
if (child === undefined) {
|
|
53
|
+
pendingForwardedSignals.push(signal);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
41
56
|
child.kill(signal);
|
|
42
57
|
};
|
|
43
58
|
|
|
59
|
+
// Arm the policy before starting the child. A fast child can write to the
|
|
60
|
+
// inherited terminal before `spawn` returns in the launcher process; callers
|
|
61
|
+
// commonly treat that output as readiness and may immediately signal the
|
|
62
|
+
// foreground process group. Installing these handlers after `spawn` left a
|
|
63
|
+
// window where that SIGINT still had its default, fatal disposition.
|
|
44
64
|
if (process.platform !== "win32") {
|
|
45
65
|
for (const signal of ignoredSignals) {
|
|
46
66
|
process.on(signal, ignoreSignal);
|
|
@@ -50,6 +70,16 @@ if (process.platform !== "win32") {
|
|
|
50
70
|
}
|
|
51
71
|
}
|
|
52
72
|
|
|
73
|
+
child = spawn(binary, process.argv.slice(2), {
|
|
74
|
+
stdio: "inherit",
|
|
75
|
+
windowsHide: false,
|
|
76
|
+
detached: supervised,
|
|
77
|
+
});
|
|
78
|
+
for (const signal of pendingForwardedSignals) {
|
|
79
|
+
child.kill(signal);
|
|
80
|
+
}
|
|
81
|
+
pendingForwardedSignals.length = 0;
|
|
82
|
+
|
|
53
83
|
// The handlers deliberately stay installed until the process exits. Removing
|
|
54
84
|
// the last listener for a signal restores its default disposition, so a SIGINT
|
|
55
85
|
// still in flight — the common case, since the terminal sent it to the whole
|
package/lib/client.d.ts
CHANGED
|
@@ -9,14 +9,21 @@ export const EXIT_POLICY: 3;
|
|
|
9
9
|
export interface FailureReceipt {
|
|
10
10
|
schemaVersion: number;
|
|
11
11
|
outcome: "failed";
|
|
12
|
-
|
|
12
|
+
/** Null for a usage error: parsing failed before a command was chosen. */
|
|
13
|
+
operation: string | null;
|
|
13
14
|
code: string;
|
|
14
|
-
|
|
15
|
+
/** Mirrors the exit code: 1 operational, 2 usage, 3 policy. */
|
|
16
|
+
category: "policy" | "operational" | "usage";
|
|
15
17
|
message: string;
|
|
16
18
|
phase: string | null;
|
|
17
19
|
cleanup: string;
|
|
18
20
|
recovery: "required" | "not-required" | "retry" | "inspect" | "unknown";
|
|
19
21
|
nextCommand: string | null;
|
|
22
|
+
repository: string | null;
|
|
23
|
+
repositoryNativeHex: string | null;
|
|
24
|
+
stateDirectory: string | null;
|
|
25
|
+
stateDirectoryNativeHex: string | null;
|
|
26
|
+
nativePathEncoding: string;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export class RiftriError extends Error {
|
|
@@ -36,6 +43,8 @@ export class RiftriError extends Error {
|
|
|
36
43
|
readonly isUsageError: boolean;
|
|
37
44
|
/** A live process holds the lock; waiting and retrying is correct. */
|
|
38
45
|
readonly isBusy: boolean;
|
|
46
|
+
/** Free space on the affected volume before following recovery guidance. */
|
|
47
|
+
readonly isStorageFull: boolean;
|
|
39
48
|
/** Run `receipt.nextCommand` before continuing. */
|
|
40
49
|
readonly needsRepair: boolean;
|
|
41
50
|
}
|
|
@@ -69,6 +78,8 @@ export interface AddReport {
|
|
|
69
78
|
base_path: string;
|
|
70
79
|
/** False when this add materialized a new immutable base. */
|
|
71
80
|
reused_base: boolean;
|
|
81
|
+
/** The `post-checkout` hook Git would run, or null when there is none. */
|
|
82
|
+
post_checkout: { hook: string; started: boolean; exit_code: number | null } | null;
|
|
72
83
|
journal_path: string;
|
|
73
84
|
native_path_encoding: string;
|
|
74
85
|
}
|
|
@@ -131,7 +142,10 @@ export interface AddOptions {
|
|
|
131
142
|
export interface RiftriOptions {
|
|
132
143
|
/** Working directory for every command. Defaults to `process.cwd()`. */
|
|
133
144
|
repository?: string;
|
|
134
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Explicit executable; defaults to the installed platform binary. A
|
|
147
|
+
* relative path is relative to `process.cwd()`, not `repository`.
|
|
148
|
+
*/
|
|
135
149
|
binary?: string;
|
|
136
150
|
/** Passed as `--state-dir` where the command supports it. */
|
|
137
151
|
stateDir?: string;
|
package/lib/client.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// failure. Nothing here reimplements Riftri behaviour.
|
|
9
9
|
|
|
10
10
|
const { spawn } = require("node:child_process");
|
|
11
|
+
const path = require("node:path");
|
|
11
12
|
const { resolveBinary } = require("./platform.js");
|
|
12
13
|
const { signalExitCode } = require("./signals.js");
|
|
13
14
|
|
|
@@ -62,6 +63,11 @@ class RiftriError extends Error {
|
|
|
62
63
|
return this.receipt?.code === "worktree-busy";
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/** The affected volume needs free space before recovery can proceed. */
|
|
67
|
+
get isStorageFull() {
|
|
68
|
+
return this.receipt?.code === "storage-full";
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
/** Run `nextCommand` (usually `riftri repair`) before continuing. */
|
|
66
72
|
get needsRepair() {
|
|
67
73
|
return this.receipt?.recovery === "required";
|
|
@@ -85,9 +91,11 @@ function flagsFor(options) {
|
|
|
85
91
|
for (const [key, value] of Object.entries(options)) {
|
|
86
92
|
if (value === undefined || value === null || value === false) continue;
|
|
87
93
|
const flag = `--${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
94
|
+
// `--flag=value`, so a value that starts with `-` (a branch, a path) is
|
|
95
|
+
// never parsed as another flag.
|
|
88
96
|
if (value === true) flags.push(flag);
|
|
89
|
-
else if (Array.isArray(value)) for (const item of value) flags.push(flag
|
|
90
|
-
else flags.push(flag
|
|
97
|
+
else if (Array.isArray(value)) for (const item of value) flags.push(`${flag}=${item}`);
|
|
98
|
+
else flags.push(`${flag}=${value}`);
|
|
91
99
|
}
|
|
92
100
|
return flags;
|
|
93
101
|
}
|
|
@@ -101,7 +109,11 @@ class Riftri {
|
|
|
101
109
|
*/
|
|
102
110
|
constructor(options = {}) {
|
|
103
111
|
this.repository = options.repository ?? process.cwd();
|
|
104
|
-
this
|
|
112
|
+
// A relative path is relative to this process, as RIFTRI_BINARY is.
|
|
113
|
+
// Otherwise spawn resolves it against `repository`, the child's cwd. A
|
|
114
|
+
// bare name is still looked up on PATH.
|
|
115
|
+
const binary = options.binary ?? null;
|
|
116
|
+
this.binary = binary && /[\\/]/.test(binary) ? path.resolve(binary) : binary;
|
|
105
117
|
this.stateDir = options.stateDir ?? null;
|
|
106
118
|
this.worktree = {
|
|
107
119
|
add: this.#worktreeAdd.bind(this),
|
|
@@ -171,11 +183,18 @@ class Riftri {
|
|
|
171
183
|
}
|
|
172
184
|
return;
|
|
173
185
|
}
|
|
186
|
+
// The receipt is one JSON line at the end of stderr. Anything else a
|
|
187
|
+
// child wrote there first (a hook's output, a Git warning) must not
|
|
188
|
+
// cost the caller the receipt's code and category.
|
|
174
189
|
let receipt = null;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
190
|
+
const lines = stderr.trimEnd().split("\n");
|
|
191
|
+
for (const candidate of [lines.at(-1), stderr]) {
|
|
192
|
+
try {
|
|
193
|
+
receipt = JSON.parse(candidate);
|
|
194
|
+
break;
|
|
195
|
+
} catch {
|
|
196
|
+
// Plain text, as from a binary that predates usage receipts.
|
|
197
|
+
}
|
|
179
198
|
}
|
|
180
199
|
reject(new RiftriError(exitCode, receipt, stderr));
|
|
181
200
|
});
|
|
@@ -183,7 +202,7 @@ class Riftri {
|
|
|
183
202
|
}
|
|
184
203
|
|
|
185
204
|
#stateFlag() {
|
|
186
|
-
return this.stateDir ? [
|
|
205
|
+
return this.stateDir ? [`--state-dir=${this.stateDir}`] : [];
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
/** Inspect Git and storage without creating anything. */
|
|
@@ -193,7 +212,7 @@ class Riftri {
|
|
|
193
212
|
|
|
194
213
|
/** Probe storage backends for a destination volume. */
|
|
195
214
|
backends(pathOrOptions = {}) {
|
|
196
|
-
const target = typeof pathOrOptions === "string" ? [pathOrOptions] : [];
|
|
215
|
+
const target = typeof pathOrOptions === "string" ? ["--", pathOrOptions] : [];
|
|
197
216
|
return this.run(["backends", ...target]);
|
|
198
217
|
}
|
|
199
218
|
|
|
@@ -249,29 +268,34 @@ class Riftri {
|
|
|
249
268
|
return this.run(args);
|
|
250
269
|
}
|
|
251
270
|
|
|
271
|
+
// Paths and revisions follow `--`: a relative path such as `-scratch` was
|
|
272
|
+
// otherwise parsed as flags and the call failed as a usage error.
|
|
252
273
|
#worktreeAdd(destination, options = {}) {
|
|
253
274
|
const { revision, ...rest } = options;
|
|
254
|
-
const
|
|
255
|
-
if (revision)
|
|
256
|
-
return this.run([
|
|
275
|
+
const positionals = ["--", destination];
|
|
276
|
+
if (revision) positionals.push(revision);
|
|
277
|
+
return this.run(["worktree", "add", ...flagsFor(rest), ...this.#stateFlag(), ...positionals]);
|
|
257
278
|
}
|
|
258
279
|
|
|
259
280
|
#worktreeList(options = {}) {
|
|
260
|
-
|
|
281
|
+
// `--all-states` already covers the configured state directory, and Riftri
|
|
282
|
+
// rejects it alongside `--state-dir` as a usage error.
|
|
283
|
+
const state = options.allStates ? [] : this.#stateFlag();
|
|
284
|
+
return this.run(["worktree", "list", ...flagsFor(options), ...state]);
|
|
261
285
|
}
|
|
262
286
|
|
|
263
287
|
#worktreeRemove(destination, { force = false } = {}) {
|
|
264
|
-
const args = ["worktree", "remove"
|
|
288
|
+
const args = ["worktree", "remove"];
|
|
265
289
|
if (force) args.push("--force", "--yes");
|
|
266
|
-
return this.run([...args, ...this.#stateFlag()]);
|
|
290
|
+
return this.run([...args, ...this.#stateFlag(), "--", destination]);
|
|
267
291
|
}
|
|
268
292
|
|
|
269
293
|
#worktreeMove(source, destination) {
|
|
270
|
-
return this.run(["worktree", "move",
|
|
294
|
+
return this.run(["worktree", "move", ...this.#stateFlag(), "--", source, destination]);
|
|
271
295
|
}
|
|
272
296
|
|
|
273
297
|
#worktreeCompact(destination) {
|
|
274
|
-
return this.run(["worktree", "compact",
|
|
298
|
+
return this.run(["worktree", "compact", ...this.#stateFlag(), "--", destination]);
|
|
275
299
|
}
|
|
276
300
|
|
|
277
301
|
#worktreePrune() {
|
package/lib/platform.js
CHANGED
|
@@ -14,15 +14,6 @@ const PLATFORM_PACKAGES = Object.freeze({
|
|
|
14
14
|
"win32-x64": "riftri-win32-x64",
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
// Native packages the registry has refused, mapped to the route that does work.
|
|
18
|
-
// A name only belongs here while it is genuinely absent from npm; remove the
|
|
19
|
-
// entry in the release that first publishes it.
|
|
20
|
-
const UNPUBLISHED_PACKAGES = Object.freeze({
|
|
21
|
-
"riftri-win32-arm64":
|
|
22
|
-
"install it with the PowerShell installer instead: " +
|
|
23
|
-
"https://riftri.dev/docs/installation",
|
|
24
|
-
});
|
|
25
|
-
|
|
26
17
|
function detectLinuxLibc(report = process.report) {
|
|
27
18
|
if (!report || typeof report.getReport !== "function") {
|
|
28
19
|
return "unknown";
|
|
@@ -111,13 +102,8 @@ function resolveBinary(options = {}) {
|
|
|
111
102
|
try {
|
|
112
103
|
packageJson = require.resolve(`${packageName}/package.json`);
|
|
113
104
|
} catch {
|
|
114
|
-
// Reinstalling cannot help when the package was never published, so say so
|
|
115
|
-
// instead of sending the user around the same loop.
|
|
116
|
-
const unpublished = UNPUBLISHED_PACKAGES[packageName];
|
|
117
105
|
throw new Error(
|
|
118
|
-
|
|
119
|
-
? `${packageName} is not published to npm, so riftri cannot run on this platform through npm; ${unpublished}`
|
|
120
|
-
: `the optional native package ${packageName} is missing; reinstall without --omit=optional, or build Riftri from source`,
|
|
106
|
+
`the optional native package ${packageName} is missing; reinstall without --omit=optional, or build Riftri from source`,
|
|
121
107
|
);
|
|
122
108
|
}
|
|
123
109
|
|
|
@@ -139,7 +125,6 @@ function resolveBinary(options = {}) {
|
|
|
139
125
|
|
|
140
126
|
module.exports = {
|
|
141
127
|
PLATFORM_PACKAGES,
|
|
142
|
-
UNPUBLISHED_PACKAGES,
|
|
143
128
|
assertCompatiblePackageManifest,
|
|
144
129
|
binaryName,
|
|
145
130
|
detectLinuxLibc,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "riftri",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.6.0",
|
|
22
|
+
"riftri-darwin-x64": "0.6.0",
|
|
23
|
+
"riftri-linux-arm64-gnu": "0.6.0",
|
|
24
|
+
"riftri-linux-arm64-musl": "0.6.0",
|
|
25
|
+
"riftri-linux-x64-gnu": "0.6.0",
|
|
26
|
+
"riftri-linux-x64-musl": "0.6.0",
|
|
27
|
+
"riftri-win32-arm64": "0.6.0",
|
|
28
|
+
"riftri-win32-x64": "0.6.0"
|
|
29
29
|
},
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|