riftri 0.5.1 → 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 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
- const ignoredSignals = ["SIGINT", "SIGQUIT"];
33
- const forwardedSignals = ["SIGTERM", "SIGHUP"];
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
- operation: string;
12
+ /** Null for a usage error: parsing failed before a command was chosen. */
13
+ operation: string | null;
13
14
  code: string;
14
- category: "policy" | "operational";
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
  }
@@ -133,7 +142,10 @@ export interface AddOptions {
133
142
  export interface RiftriOptions {
134
143
  /** Working directory for every command. Defaults to `process.cwd()`. */
135
144
  repository?: string;
136
- /** Explicit executable; defaults to the installed platform binary. */
145
+ /**
146
+ * Explicit executable; defaults to the installed platform binary. A
147
+ * relative path is relative to `process.cwd()`, not `repository`.
148
+ */
137
149
  binary?: string;
138
150
  /** Passed as `--state-dir` where the command supports it. */
139
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
 
@@ -90,9 +91,11 @@ function flagsFor(options) {
90
91
  for (const [key, value] of Object.entries(options)) {
91
92
  if (value === undefined || value === null || value === false) continue;
92
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.
93
96
  if (value === true) flags.push(flag);
94
- else if (Array.isArray(value)) for (const item of value) flags.push(flag, String(item));
95
- else flags.push(flag, String(value));
97
+ else if (Array.isArray(value)) for (const item of value) flags.push(`${flag}=${item}`);
98
+ else flags.push(`${flag}=${value}`);
96
99
  }
97
100
  return flags;
98
101
  }
@@ -106,7 +109,11 @@ class Riftri {
106
109
  */
107
110
  constructor(options = {}) {
108
111
  this.repository = options.repository ?? process.cwd();
109
- this.binary = options.binary ?? null;
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;
110
117
  this.stateDir = options.stateDir ?? null;
111
118
  this.worktree = {
112
119
  add: this.#worktreeAdd.bind(this),
@@ -176,11 +183,18 @@ class Riftri {
176
183
  }
177
184
  return;
178
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.
179
189
  let receipt = null;
180
- try {
181
- receipt = JSON.parse(stderr);
182
- } catch {
183
- // Usage errors come from the argument parser and carry no receipt.
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
+ }
184
198
  }
185
199
  reject(new RiftriError(exitCode, receipt, stderr));
186
200
  });
@@ -188,7 +202,7 @@ class Riftri {
188
202
  }
189
203
 
190
204
  #stateFlag() {
191
- return this.stateDir ? ["--state-dir", this.stateDir] : [];
205
+ return this.stateDir ? [`--state-dir=${this.stateDir}`] : [];
192
206
  }
193
207
 
194
208
  /** Inspect Git and storage without creating anything. */
@@ -198,7 +212,7 @@ class Riftri {
198
212
 
199
213
  /** Probe storage backends for a destination volume. */
200
214
  backends(pathOrOptions = {}) {
201
- const target = typeof pathOrOptions === "string" ? [pathOrOptions] : [];
215
+ const target = typeof pathOrOptions === "string" ? ["--", pathOrOptions] : [];
202
216
  return this.run(["backends", ...target]);
203
217
  }
204
218
 
@@ -254,29 +268,34 @@ class Riftri {
254
268
  return this.run(args);
255
269
  }
256
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.
257
273
  #worktreeAdd(destination, options = {}) {
258
274
  const { revision, ...rest } = options;
259
- const args = ["worktree", "add", destination];
260
- if (revision) args.push(revision);
261
- return this.run([...args, ...flagsFor(rest), ...this.#stateFlag()]);
275
+ const positionals = ["--", destination];
276
+ if (revision) positionals.push(revision);
277
+ return this.run(["worktree", "add", ...flagsFor(rest), ...this.#stateFlag(), ...positionals]);
262
278
  }
263
279
 
264
280
  #worktreeList(options = {}) {
265
- return this.run(["worktree", "list", ...flagsFor(options), ...this.#stateFlag()]);
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]);
266
285
  }
267
286
 
268
287
  #worktreeRemove(destination, { force = false } = {}) {
269
- const args = ["worktree", "remove", destination];
288
+ const args = ["worktree", "remove"];
270
289
  if (force) args.push("--force", "--yes");
271
- return this.run([...args, ...this.#stateFlag()]);
290
+ return this.run([...args, ...this.#stateFlag(), "--", destination]);
272
291
  }
273
292
 
274
293
  #worktreeMove(source, destination) {
275
- return this.run(["worktree", "move", source, destination, ...this.#stateFlag()]);
294
+ return this.run(["worktree", "move", ...this.#stateFlag(), "--", source, destination]);
276
295
  }
277
296
 
278
297
  #worktreeCompact(destination) {
279
- return this.run(["worktree", "compact", destination, ...this.#stateFlag()]);
298
+ return this.run(["worktree", "compact", ...this.#stateFlag(), "--", destination]);
280
299
  }
281
300
 
282
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
- unpublished
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.5.1",
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.5.1",
22
- "riftri-darwin-x64": "0.5.1",
23
- "riftri-linux-arm64-gnu": "0.5.1",
24
- "riftri-linux-arm64-musl": "0.5.1",
25
- "riftri-linux-x64-gnu": "0.5.1",
26
- "riftri-linux-x64-musl": "0.5.1",
27
- "riftri-win32-arm64": "0.5.1",
28
- "riftri-win32-x64": "0.5.1"
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",