riftri 0.5.1 → 0.6.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/README.md +0 -5
- package/bin/riftri.js +17 -2
- package/lib/client.d.ts +77 -6
- package/lib/client.js +52 -20
- package/lib/platform.js +1 -16
- package/package.json +13 -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
|
|
|
@@ -29,8 +30,21 @@ try {
|
|
|
29
30
|
// same contract downstream to the scoped command. Windows has no equivalent
|
|
30
31
|
// to preserve: the console already delivers Ctrl-C events to every attached
|
|
31
32
|
// process, and a hard TerminateProcess cannot be intercepted.
|
|
32
|
-
|
|
33
|
-
|
|
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"];
|
|
34
48
|
const ignoreSignal = () => {};
|
|
35
49
|
let child;
|
|
36
50
|
const pendingForwardedSignals = [];
|
|
@@ -59,6 +73,7 @@ if (process.platform !== "win32") {
|
|
|
59
73
|
child = spawn(binary, process.argv.slice(2), {
|
|
60
74
|
stdio: "inherit",
|
|
61
75
|
windowsHide: false,
|
|
76
|
+
detached: supervised,
|
|
62
77
|
});
|
|
63
78
|
for (const signal of pendingForwardedSignals) {
|
|
64
79
|
child.kill(signal);
|
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 {
|
|
@@ -71,6 +78,8 @@ export interface AddReport {
|
|
|
71
78
|
base_path: string;
|
|
72
79
|
/** False when this add materialized a new immutable base. */
|
|
73
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;
|
|
74
83
|
journal_path: string;
|
|
75
84
|
native_path_encoding: string;
|
|
76
85
|
}
|
|
@@ -93,18 +102,74 @@ export interface ManagedWorktree {
|
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
export interface WorktreeInventory {
|
|
96
|
-
schema_version:
|
|
105
|
+
schema_version: 1;
|
|
97
106
|
state_directory: string;
|
|
107
|
+
state_directory_native_hex: string;
|
|
98
108
|
worktrees: ManagedWorktree[];
|
|
109
|
+
diagnostic_issues: StateDiagnosticIssue[];
|
|
99
110
|
native_path_encoding: string;
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
export interface StateDiagnosticIssue {
|
|
114
|
+
path: string;
|
|
115
|
+
path_native_hex: string;
|
|
116
|
+
reason: string;
|
|
117
|
+
may_hide_base_reference: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface AllStatesManagedWorktree extends ManagedWorktree {
|
|
121
|
+
state_directory: string;
|
|
122
|
+
state_directory_native_hex: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface AllStatesDiagnosticIssue extends StateDiagnosticIssue {
|
|
126
|
+
/** Null for a registration issue that belongs to no usable state directory. */
|
|
127
|
+
state_directory: string | null;
|
|
128
|
+
state_directory_native_hex: string | null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface AllStatesWorktreeInventory {
|
|
132
|
+
schema_version: 2;
|
|
133
|
+
scope: "all-registered-states";
|
|
134
|
+
state_directories: {
|
|
135
|
+
path: string;
|
|
136
|
+
path_native_hex: string;
|
|
137
|
+
source: "default" | "registered";
|
|
138
|
+
}[];
|
|
139
|
+
worktrees: AllStatesManagedWorktree[];
|
|
140
|
+
diagnostic_issues: AllStatesDiagnosticIssue[];
|
|
141
|
+
native_path_encoding: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Lifecycle counters; lower bounds when StatusReport.counts_complete is false. */
|
|
145
|
+
export interface StatusOperations {
|
|
146
|
+
active_views: number;
|
|
147
|
+
pending_adds: number;
|
|
148
|
+
completed_removals: number;
|
|
149
|
+
cancelled_removals: number;
|
|
150
|
+
pending_removals: number;
|
|
151
|
+
completed_moves: number;
|
|
152
|
+
cancelled_moves: number;
|
|
153
|
+
pending_moves: number;
|
|
154
|
+
completed_compactions: number;
|
|
155
|
+
cancelled_compactions: number;
|
|
156
|
+
pending_compactions: number;
|
|
157
|
+
completed_prunes: number;
|
|
158
|
+
pending_prunes: number;
|
|
159
|
+
completed_collections: number;
|
|
160
|
+
cancelled_collections: number;
|
|
161
|
+
pending_collections: number;
|
|
162
|
+
coordination_locks: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
102
165
|
export interface StatusReport {
|
|
103
166
|
schema_version: number;
|
|
104
167
|
state_directory: string;
|
|
105
168
|
bases: unknown[];
|
|
106
169
|
worktrees: unknown[];
|
|
107
|
-
|
|
170
|
+
/** False when unreadable journals make operation counts lower bounds. */
|
|
171
|
+
counts_complete: boolean;
|
|
172
|
+
operations: StatusOperations;
|
|
108
173
|
diagnostic_issues: unknown[];
|
|
109
174
|
total_allocated_bytes: number;
|
|
110
175
|
total_logical_bytes: number;
|
|
@@ -133,7 +198,10 @@ export interface AddOptions {
|
|
|
133
198
|
export interface RiftriOptions {
|
|
134
199
|
/** Working directory for every command. Defaults to `process.cwd()`. */
|
|
135
200
|
repository?: string;
|
|
136
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Explicit executable; defaults to the installed platform binary. A
|
|
203
|
+
* relative path is relative to `process.cwd()`, not `repository`.
|
|
204
|
+
*/
|
|
137
205
|
binary?: string;
|
|
138
206
|
/** Passed as `--state-dir` where the command supports it. */
|
|
139
207
|
stateDir?: string;
|
|
@@ -171,7 +239,10 @@ export class Riftri {
|
|
|
171
239
|
|
|
172
240
|
readonly worktree: {
|
|
173
241
|
add(destination: string, options?: AddOptions): Promise<AddReport>;
|
|
174
|
-
list(options
|
|
242
|
+
list(options: { allStates: true }): Promise<AllStatesWorktreeInventory>;
|
|
243
|
+
list(options?: { allStates?: false }): Promise<WorktreeInventory>;
|
|
244
|
+
/** Narrow schema_version when allStates is a runtime boolean. */
|
|
245
|
+
list(options?: { allStates?: boolean }): Promise<WorktreeInventory | AllStatesWorktreeInventory>;
|
|
175
246
|
/** Refuses a dirty worktree unless `force` is set. */
|
|
176
247
|
remove(destination: string, options?: { force?: boolean }): Promise<unknown>;
|
|
177
248
|
move(source: string, destination: string): Promise<unknown>;
|
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
|
|
|
@@ -30,7 +31,9 @@ const REPORTING = new Set([
|
|
|
30
31
|
class RiftriError extends Error {
|
|
31
32
|
constructor(exitCode, receipt, stderr, signal = null) {
|
|
32
33
|
super(
|
|
33
|
-
receipt
|
|
34
|
+
// JSON is not a guarantee of a valid receipt. Coercing an object here
|
|
35
|
+
// can throw inside the child close handler instead of rejecting run().
|
|
36
|
+
(typeof receipt?.message === "string" ? receipt.message : "") ||
|
|
34
37
|
stderr.trim() ||
|
|
35
38
|
(signal ? `riftri terminated by ${signal}` : `riftri exited ${exitCode}`),
|
|
36
39
|
);
|
|
@@ -90,9 +93,11 @@ function flagsFor(options) {
|
|
|
90
93
|
for (const [key, value] of Object.entries(options)) {
|
|
91
94
|
if (value === undefined || value === null || value === false) continue;
|
|
92
95
|
const flag = `--${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
96
|
+
// `--flag=value`, so a value that starts with `-` (a branch, a path) is
|
|
97
|
+
// never parsed as another flag.
|
|
93
98
|
if (value === true) flags.push(flag);
|
|
94
|
-
else if (Array.isArray(value)) for (const item of value) flags.push(flag
|
|
95
|
-
else flags.push(flag
|
|
99
|
+
else if (Array.isArray(value)) for (const item of value) flags.push(`${flag}=${item}`);
|
|
100
|
+
else flags.push(`${flag}=${value}`);
|
|
96
101
|
}
|
|
97
102
|
return flags;
|
|
98
103
|
}
|
|
@@ -106,7 +111,11 @@ class Riftri {
|
|
|
106
111
|
*/
|
|
107
112
|
constructor(options = {}) {
|
|
108
113
|
this.repository = options.repository ?? process.cwd();
|
|
109
|
-
this
|
|
114
|
+
// A relative path is relative to this process, as RIFTRI_BINARY is.
|
|
115
|
+
// Otherwise spawn resolves it against `repository`, the child's cwd. A
|
|
116
|
+
// bare name is still looked up on PATH.
|
|
117
|
+
const binary = options.binary ?? null;
|
|
118
|
+
this.binary = binary && /[\\/]/.test(binary) ? path.resolve(binary) : binary;
|
|
110
119
|
this.stateDir = options.stateDir ?? null;
|
|
111
120
|
this.worktree = {
|
|
112
121
|
add: this.#worktreeAdd.bind(this),
|
|
@@ -144,7 +153,18 @@ class Riftri {
|
|
|
144
153
|
}
|
|
145
154
|
let stdout = "";
|
|
146
155
|
let stderr = "";
|
|
147
|
-
|
|
156
|
+
// Pipe chunks need not end on a UTF-8 character boundary. Let each
|
|
157
|
+
// stream retain incomplete bytes between chunks (and flush at EOF).
|
|
158
|
+
if (json) {
|
|
159
|
+
child.stdout.setEncoding("utf8");
|
|
160
|
+
child.stdout.on("data", (chunk) => (stdout += chunk));
|
|
161
|
+
} else {
|
|
162
|
+
// The API returns null for non-reporting commands. Drain their output
|
|
163
|
+
// without decoding or retaining it, so verbose children cannot grow
|
|
164
|
+
// the host heap in proportion to output that will be discarded.
|
|
165
|
+
child.stdout.resume();
|
|
166
|
+
}
|
|
167
|
+
child.stderr.setEncoding("utf8");
|
|
148
168
|
child.stderr.on("data", (chunk) => (stderr += chunk));
|
|
149
169
|
child.on("error", reject);
|
|
150
170
|
child.on("close", (exitCode, signal) => {
|
|
@@ -156,7 +176,7 @@ class Riftri {
|
|
|
156
176
|
return;
|
|
157
177
|
}
|
|
158
178
|
if (exitCode === EXIT_SUCCESS) {
|
|
159
|
-
if (!json
|
|
179
|
+
if (!json) {
|
|
160
180
|
resolve(null);
|
|
161
181
|
return;
|
|
162
182
|
}
|
|
@@ -176,11 +196,18 @@ class Riftri {
|
|
|
176
196
|
}
|
|
177
197
|
return;
|
|
178
198
|
}
|
|
199
|
+
// The receipt is one JSON line at the end of stderr. Anything else a
|
|
200
|
+
// child wrote there first (a hook's output, a Git warning) must not
|
|
201
|
+
// cost the caller the receipt's code and category.
|
|
179
202
|
let receipt = null;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
203
|
+
const lines = stderr.trimEnd().split("\n");
|
|
204
|
+
for (const candidate of [lines.at(-1), stderr]) {
|
|
205
|
+
try {
|
|
206
|
+
receipt = JSON.parse(candidate);
|
|
207
|
+
break;
|
|
208
|
+
} catch {
|
|
209
|
+
// Plain text, as from a binary that predates usage receipts.
|
|
210
|
+
}
|
|
184
211
|
}
|
|
185
212
|
reject(new RiftriError(exitCode, receipt, stderr));
|
|
186
213
|
});
|
|
@@ -188,7 +215,7 @@ class Riftri {
|
|
|
188
215
|
}
|
|
189
216
|
|
|
190
217
|
#stateFlag() {
|
|
191
|
-
return this.stateDir ? [
|
|
218
|
+
return this.stateDir ? [`--state-dir=${this.stateDir}`] : [];
|
|
192
219
|
}
|
|
193
220
|
|
|
194
221
|
/** Inspect Git and storage without creating anything. */
|
|
@@ -198,7 +225,7 @@ class Riftri {
|
|
|
198
225
|
|
|
199
226
|
/** Probe storage backends for a destination volume. */
|
|
200
227
|
backends(pathOrOptions = {}) {
|
|
201
|
-
const target = typeof pathOrOptions === "string" ? [pathOrOptions] : [];
|
|
228
|
+
const target = typeof pathOrOptions === "string" ? ["--", pathOrOptions] : [];
|
|
202
229
|
return this.run(["backends", ...target]);
|
|
203
230
|
}
|
|
204
231
|
|
|
@@ -254,29 +281,34 @@ class Riftri {
|
|
|
254
281
|
return this.run(args);
|
|
255
282
|
}
|
|
256
283
|
|
|
284
|
+
// Paths and revisions follow `--`: a relative path such as `-scratch` was
|
|
285
|
+
// otherwise parsed as flags and the call failed as a usage error.
|
|
257
286
|
#worktreeAdd(destination, options = {}) {
|
|
258
287
|
const { revision, ...rest } = options;
|
|
259
|
-
const
|
|
260
|
-
if (revision)
|
|
261
|
-
return this.run([
|
|
288
|
+
const positionals = ["--", destination];
|
|
289
|
+
if (revision) positionals.push(revision);
|
|
290
|
+
return this.run(["worktree", "add", ...flagsFor(rest), ...this.#stateFlag(), ...positionals]);
|
|
262
291
|
}
|
|
263
292
|
|
|
264
293
|
#worktreeList(options = {}) {
|
|
265
|
-
|
|
294
|
+
// `--all-states` already covers the configured state directory, and Riftri
|
|
295
|
+
// rejects it alongside `--state-dir` as a usage error.
|
|
296
|
+
const state = options.allStates ? [] : this.#stateFlag();
|
|
297
|
+
return this.run(["worktree", "list", ...flagsFor(options), ...state]);
|
|
266
298
|
}
|
|
267
299
|
|
|
268
300
|
#worktreeRemove(destination, { force = false } = {}) {
|
|
269
|
-
const args = ["worktree", "remove"
|
|
301
|
+
const args = ["worktree", "remove"];
|
|
270
302
|
if (force) args.push("--force", "--yes");
|
|
271
|
-
return this.run([...args, ...this.#stateFlag()]);
|
|
303
|
+
return this.run([...args, ...this.#stateFlag(), "--", destination]);
|
|
272
304
|
}
|
|
273
305
|
|
|
274
306
|
#worktreeMove(source, destination) {
|
|
275
|
-
return this.run(["worktree", "move",
|
|
307
|
+
return this.run(["worktree", "move", ...this.#stateFlag(), "--", source, destination]);
|
|
276
308
|
}
|
|
277
309
|
|
|
278
310
|
#worktreeCompact(destination) {
|
|
279
|
-
return this.run(["worktree", "compact",
|
|
311
|
+
return this.run(["worktree", "compact", ...this.#stateFlag(), "--", destination]);
|
|
280
312
|
}
|
|
281
313
|
|
|
282
314
|
#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.1",
|
|
4
4
|
"description": "Experimental lightweight Git workspaces for parallel development",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Assistant UI contributors",
|
|
@@ -17,15 +17,19 @@
|
|
|
17
17
|
"engines": {
|
|
18
18
|
"node": ">=18.18"
|
|
19
19
|
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "18.19.130",
|
|
22
|
+
"typescript": "5.9.3"
|
|
23
|
+
},
|
|
20
24
|
"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.
|
|
25
|
+
"riftri-darwin-arm64": "0.6.1",
|
|
26
|
+
"riftri-darwin-x64": "0.6.1",
|
|
27
|
+
"riftri-linux-arm64-gnu": "0.6.1",
|
|
28
|
+
"riftri-linux-arm64-musl": "0.6.1",
|
|
29
|
+
"riftri-linux-x64-gnu": "0.6.1",
|
|
30
|
+
"riftri-linux-x64-musl": "0.6.1",
|
|
31
|
+
"riftri-win32-arm64": "0.6.1",
|
|
32
|
+
"riftri-win32-x64": "0.6.1"
|
|
29
33
|
},
|
|
30
34
|
"repository": {
|
|
31
35
|
"type": "git",
|