riftri 0.6.1 → 0.6.3

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/lib/client.d.ts CHANGED
@@ -5,6 +5,9 @@ export const EXIT_OPERATIONAL: 1;
5
5
  export const EXIT_USAGE: 2;
6
6
  export const EXIT_POLICY: 3;
7
7
 
8
+ /** Resolve and validate the installed native executable, honoring RIFTRI_BINARY. Does not spawn. */
9
+ export function resolveBinary(): string;
10
+
8
11
  /** One versioned failure receipt, as emitted by `--json-errors`. */
9
12
  export interface FailureReceipt {
10
13
  schemaVersion: number;
@@ -37,7 +40,9 @@ export class RiftriError extends Error {
37
40
  /** The process was killed rather than exiting on its own. */
38
41
  readonly wasSignalled: boolean;
39
42
  readonly receipt: FailureReceipt | null;
40
- /** Riftri declined before touching anything; falling back is safe. */
43
+ /** Parsed stdout on a nonzero exit (for example a failed post-checkout hook), or null. Validate before use. */
44
+ readonly report: unknown;
45
+ /** A native policy receipt confirms no mutation; an exit code alone is insufficient. */
41
46
  readonly isPolicyRefusal: boolean;
42
47
  /** The request was malformed; never retry it unchanged. */
43
48
  readonly isUsageError: boolean;
@@ -110,6 +115,28 @@ export interface WorktreeInventory {
110
115
  native_path_encoding: string;
111
116
  }
112
117
 
118
+ export interface WorktreeOwner {
119
+ schema_version: 1;
120
+ /** Null only when no registered state claims the path. Errors reject. */
121
+ state_directory: string | null;
122
+ state_directory_native_hex: string | null;
123
+ native_path_encoding: string;
124
+ }
125
+
126
+ export interface WorktreeInspectionReport {
127
+ schema_version: 1;
128
+ native_path_encoding: string;
129
+ worktrees: Array<{
130
+ path: string;
131
+ path_native_hex: string;
132
+ state_directory: string | null;
133
+ state_directory_native_hex: string | null;
134
+ backend: string | null;
135
+ /** Null unless the managed backend needs a mount. Inspection never repairs. */
136
+ mount_status: "active" | "recovery-required" | "different-namespace" | "foreign" | "unavailable" | null;
137
+ }>;
138
+ }
139
+
113
140
  export interface StateDiagnosticIssue {
114
141
  path: string;
115
142
  path_native_hex: string;
@@ -239,6 +266,10 @@ export class Riftri {
239
266
 
240
267
  readonly worktree: {
241
268
  add(destination: string, options?: AddOptions): Promise<AddReport>;
269
+ /** Advisory ownership lookup without disk accounting; mutations revalidate. */
270
+ owner(destination: string): Promise<WorktreeOwner>;
271
+ /** One read-only discovery pass; paths retain the requested order. */
272
+ inspect(destinations: readonly string[]): Promise<WorktreeInspectionReport>;
242
273
  list(options: { allStates: true }): Promise<AllStatesWorktreeInventory>;
243
274
  list(options?: { allStates?: false }): Promise<WorktreeInventory>;
244
275
  /** Narrow schema_version when allStates is a runtime boolean. */
package/lib/client.js CHANGED
@@ -29,7 +29,7 @@ const REPORTING = new Set([
29
29
  ]);
30
30
 
31
31
  class RiftriError extends Error {
32
- constructor(exitCode, receipt, stderr, signal = null) {
32
+ constructor(exitCode, receipt, stderr, signal = null, report = null) {
33
33
  super(
34
34
  // JSON is not a guarantee of a valid receipt. Coercing an object here
35
35
  // can throw inside the child close handler instead of rejecting run().
@@ -43,6 +43,7 @@ class RiftriError extends Error {
43
43
  this.exitCode = exitCode;
44
44
  this.signal = signal;
45
45
  this.receipt = receipt ?? null;
46
+ this.report = report;
46
47
  }
47
48
 
48
49
  /** The process was killed rather than exiting on its own. */
@@ -52,7 +53,12 @@ class RiftriError extends Error {
52
53
 
53
54
  /** Riftri declined before touching anything. Falling back is safe. */
54
55
  get isPolicyRefusal() {
55
- return this.exitCode === EXIT_POLICY;
56
+ // Hooks and `exec` children can return 3 after performing work. Only a
57
+ // native refusal receipt proves the operation stopped before mutation.
58
+ return this.exitCode === EXIT_POLICY && this.signal === null && this.report === null &&
59
+ this.receipt?.schemaVersion === 1 && this.receipt?.outcome === "failed" &&
60
+ this.receipt?.category === "policy" && this.receipt?.cleanup === "not-needed" &&
61
+ typeof this.receipt?.code === "string";
56
62
  }
57
63
 
58
64
  /** The request itself was malformed. Never retry unchanged. */
@@ -120,6 +126,8 @@ class Riftri {
120
126
  this.worktree = {
121
127
  add: this.#worktreeAdd.bind(this),
122
128
  list: this.#worktreeList.bind(this),
129
+ owner: this.#worktreeOwner.bind(this),
130
+ inspect: this.#worktreeInspect.bind(this),
123
131
  remove: this.#worktreeRemove.bind(this),
124
132
  move: this.#worktreeMove.bind(this),
125
133
  compact: this.#worktreeCompact.bind(this),
@@ -200,8 +208,11 @@ class Riftri {
200
208
  // child wrote there first (a hook's output, a Git warning) must not
201
209
  // cost the caller the receipt's code and category.
202
210
  let receipt = null;
203
- const lines = stderr.trimEnd().split("\n");
204
- for (const candidate of [lines.at(-1), stderr]) {
211
+ // Preserve the full diagnostic fallback without allocating an array
212
+ // and a string entry for every line of a verbose child's output.
213
+ const trimmed = stderr.trimEnd();
214
+ const lastLine = trimmed.slice(trimmed.lastIndexOf("\n") + 1);
215
+ for (const candidate of [lastLine, stderr]) {
205
216
  try {
206
217
  receipt = JSON.parse(candidate);
207
218
  break;
@@ -209,7 +220,18 @@ class Riftri {
209
220
  // Plain text, as from a binary that predates usage receipts.
210
221
  }
211
222
  }
212
- reject(new RiftriError(exitCode, receipt, stderr));
223
+ let report = null;
224
+ if (json && stdout.trim()) {
225
+ // A failed post-checkout hook leaves a created worktree in place.
226
+ // Preserve that report even though the hook's status rejects run().
227
+ try {
228
+ report = JSON.parse(stdout);
229
+ } catch {
230
+ // Malformed output cannot prove that a fallback is safe either.
231
+ }
232
+ receipt = null;
233
+ }
234
+ reject(new RiftriError(exitCode, receipt, stderr, null, report));
213
235
  });
214
236
  });
215
237
  }
@@ -297,6 +319,15 @@ class Riftri {
297
319
  return this.run(["worktree", "list", ...flagsFor(options), ...state]);
298
320
  }
299
321
 
322
+ #worktreeOwner(destination) {
323
+ // Ownership always consults every registered state, not this.stateDir.
324
+ return this.run(["worktree", "owner", "--", destination]);
325
+ }
326
+
327
+ #worktreeInspect(destinations) {
328
+ return this.run(["worktree", "inspect", "--", ...destinations]);
329
+ }
330
+
300
331
  #worktreeRemove(destination, { force = false } = {}) {
301
332
  const args = ["worktree", "remove"];
302
333
  if (force) args.push("--force", "--yes");
@@ -319,6 +350,7 @@ class Riftri {
319
350
  module.exports = {
320
351
  Riftri,
321
352
  RiftriError,
353
+ resolveBinary,
322
354
  EXIT_SUCCESS,
323
355
  EXIT_OPERATIONAL,
324
356
  EXIT_USAGE,
package/lib/platform.js CHANGED
@@ -3,6 +3,9 @@
3
3
  const fs = require("node:fs");
4
4
  const path = require("node:path");
5
5
 
6
+ // Kept in step with the ELF release gate and standalone installer by tests.
7
+ const GLIBC_FLOOR = "2.34";
8
+
6
9
  const PLATFORM_PACKAGES = Object.freeze({
7
10
  "darwin-arm64": "riftri-darwin-arm64",
8
11
  "darwin-x64": "riftri-darwin-x64",
@@ -20,7 +23,22 @@ function detectLinuxLibc(report = process.report) {
20
23
  }
21
24
 
22
25
  const header = report.getReport()?.header;
23
- return header?.glibcVersionRuntime ? "gnu" : "musl";
26
+ const version = header?.glibcVersionRuntime;
27
+ if (!version) return "musl";
28
+ const parts = typeof version === "string" && /^(\d+)\.(\d+)(?:\.\d+)?$/.exec(version);
29
+ const [major, minor] = parts ? parts.slice(1, 3).map(Number) : [];
30
+ const [floorMajor, floorMinor] = GLIBC_FLOOR.split(".").map(Number);
31
+ if (!Number.isSafeInteger(major) || !Number.isSafeInteger(minor) ||
32
+ major < floorMajor || (major === floorMajor && minor < floorMinor)) {
33
+ // npm excludes musl optional dependencies on glibc hosts. Do not select
34
+ // an absent package or attempt to execute an incompatible GNU binary.
35
+ throw new Error(
36
+ `Riftri's GNU npm binary requires glibc ${GLIBC_FLOOR} or newer (detected ${version}); ` +
37
+ "use the standalone installer at https://riftri.dev/install.sh for the static musl binary, " +
38
+ "then set RIFTRI_BINARY to its absolute path when using the Node SDK",
39
+ );
40
+ }
41
+ return "gnu";
24
42
  }
25
43
 
26
44
  function platformKey(
@@ -44,6 +62,9 @@ function binaryName(platform = process.platform) {
44
62
 
45
63
  function assertUsableBinary(candidate, source) {
46
64
  try {
65
+ // Follow executable symlinks, but reject directories and special files
66
+ // that can pass an access check without being runnable binaries.
67
+ if (!fs.statSync(candidate).isFile()) throw new Error("not a regular file");
47
68
  fs.accessSync(
48
69
  candidate,
49
70
  process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK,
@@ -54,6 +75,17 @@ function assertUsableBinary(candidate, source) {
54
75
  return candidate;
55
76
  }
56
77
 
78
+ function unpackedElectronBinary(candidate) {
79
+ if (!process.versions.electron) return candidate;
80
+ const segments = candidate.split(path.sep);
81
+ const archive = segments.findLastIndex((segment) => segment.endsWith(".asar"));
82
+ if (archive < 0) return candidate;
83
+ // Electron can read an archived executable, but the OS cannot spawn it.
84
+ // Validate the physical file below instead of relying on Electron's virtual fs.
85
+ segments[archive] += ".unpacked";
86
+ return segments.join(path.sep);
87
+ }
88
+
57
89
  function launcherVersion() {
58
90
  for (const candidate of [
59
91
  path.join(__dirname, "..", "package.json"),
@@ -120,10 +152,11 @@ function resolveBinary(options = {}) {
120
152
  );
121
153
 
122
154
  const candidate = path.join(path.dirname(packageJson), "bin", binaryName(platform));
123
- return assertUsableBinary(candidate, packageName);
155
+ return assertUsableBinary(unpackedElectronBinary(candidate), packageName);
124
156
  }
125
157
 
126
158
  module.exports = {
159
+ GLIBC_FLOOR,
127
160
  PLATFORM_PACKAGES,
128
161
  assertCompatiblePackageManifest,
129
162
  binaryName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "riftri",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Experimental lightweight Git workspaces for parallel development",
5
5
  "license": "MIT",
6
6
  "author": "Assistant UI contributors",
@@ -22,14 +22,14 @@
22
22
  "typescript": "5.9.3"
23
23
  },
24
24
  "optionalDependencies": {
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"
25
+ "riftri-darwin-arm64": "0.6.3",
26
+ "riftri-darwin-x64": "0.6.3",
27
+ "riftri-linux-arm64-gnu": "0.6.3",
28
+ "riftri-linux-arm64-musl": "0.6.3",
29
+ "riftri-linux-x64-gnu": "0.6.3",
30
+ "riftri-linux-x64-musl": "0.6.3",
31
+ "riftri-win32-arm64": "0.6.3",
32
+ "riftri-win32-x64": "0.6.3"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",