riftri 0.3.5 → 0.4.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
@@ -67,9 +67,16 @@ The interactive `riftri setup` terminal UI is behind an off-by-default `tui`
67
67
  feature; add `--features tui` to build it in (the published binaries do). The
68
68
  default build keeps `setup` working through plain text prompts.
69
69
 
70
- The npm launcher is a separate distribution channel. It is not currently
71
- available because the initial publication is incomplete; use the verified
72
- standalone installer or GitHub release assets for now.
70
+ Or install through npm, which fetches the binary for your platform:
71
+
72
+ ```sh
73
+ npm install --global riftri
74
+ ```
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.
73
80
 
74
81
  ## Quick start
75
82
 
@@ -0,0 +1,161 @@
1
+ /** Programmatic API for the Riftri CLI. */
2
+
3
+ export const EXIT_SUCCESS: 0;
4
+ export const EXIT_OPERATIONAL: 1;
5
+ export const EXIT_USAGE: 2;
6
+ export const EXIT_POLICY: 3;
7
+
8
+ /** One versioned failure receipt, as emitted by `--json-errors`. */
9
+ export interface FailureReceipt {
10
+ schemaVersion: number;
11
+ outcome: "failed";
12
+ operation: string;
13
+ code: string;
14
+ category: "policy" | "operational";
15
+ message: string;
16
+ phase: string | null;
17
+ cleanup: string;
18
+ recovery: "required" | "not-required" | "retry" | "inspect" | "unknown";
19
+ nextCommand: string | null;
20
+ }
21
+
22
+ export class RiftriError extends Error {
23
+ readonly exitCode: number;
24
+ readonly receipt: FailureReceipt | null;
25
+ /** Riftri declined before touching anything; falling back is safe. */
26
+ readonly isPolicyRefusal: boolean;
27
+ /** The request was malformed; never retry it unchanged. */
28
+ readonly isUsageError: boolean;
29
+ /** A live process holds the lock; waiting and retrying is correct. */
30
+ readonly isBusy: boolean;
31
+ /** Run `receipt.nextCommand` before continuing. */
32
+ readonly needsRepair: boolean;
33
+ }
34
+
35
+ export interface DoctorReport {
36
+ project_stage: string;
37
+ operating_system: string;
38
+ architecture: string;
39
+ /** Whether a usable copy-on-write backend exists here. */
40
+ cow_backend_active: boolean;
41
+ /** Whether this repository has opted in with `enable()`. */
42
+ repository_enabled: boolean;
43
+ git_shim_active: boolean;
44
+ destination_readiness: {
45
+ destination: string;
46
+ status: string;
47
+ backend: string | null;
48
+ copy_on_write: boolean;
49
+ };
50
+ storage_capabilities: unknown[];
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ export interface AddReport {
55
+ schema_version: number;
56
+ backend: string;
57
+ destination: string;
58
+ destination_native_hex: string;
59
+ commit: string;
60
+ tree: string;
61
+ base_path: string;
62
+ /** False when this add materialized a new immutable base. */
63
+ reused_base: boolean;
64
+ journal_path: string;
65
+ native_path_encoding: string;
66
+ }
67
+
68
+ export interface ManagedWorktree {
69
+ path: string;
70
+ path_native_hex: string;
71
+ repository: string;
72
+ head: string;
73
+ branch: string | null;
74
+ detached: boolean;
75
+ backend: string;
76
+ base_path: string;
77
+ /** Filesystem-accounted bytes this worktree actually occupies. */
78
+ allocated_bytes: number;
79
+ /** Bytes the same content would occupy without sharing. */
80
+ logical_bytes: number;
81
+ locked_reason: string | null;
82
+ prunable_reason: string | null;
83
+ }
84
+
85
+ export interface WorktreeInventory {
86
+ schema_version: number;
87
+ state_directory: string;
88
+ worktrees: ManagedWorktree[];
89
+ native_path_encoding: string;
90
+ }
91
+
92
+ export interface StatusReport {
93
+ schema_version: number;
94
+ state_directory: string;
95
+ bases: unknown[];
96
+ worktrees: unknown[];
97
+ operations: unknown[];
98
+ diagnostic_issues: unknown[];
99
+ total_allocated_bytes: number;
100
+ total_logical_bytes: number;
101
+ [key: string]: unknown;
102
+ }
103
+
104
+ export interface RepairReport {
105
+ schema_version: number;
106
+ scanned: number;
107
+ active: number;
108
+ errors: unknown[];
109
+ [key: string]: unknown;
110
+ }
111
+
112
+ export interface AddOptions {
113
+ /** Create and check out a new branch. */
114
+ branch?: string;
115
+ /** Create a detached worktree instead of a branch. */
116
+ detach?: boolean;
117
+ /** Commit-ish to start from; defaults to HEAD. */
118
+ revision?: string;
119
+ /** Cone-mode sparse directories to include. */
120
+ sparseDir?: string[];
121
+ }
122
+
123
+ export interface RiftriOptions {
124
+ /** Working directory for every command. Defaults to `process.cwd()`. */
125
+ repository?: string;
126
+ /** Explicit executable; defaults to the installed platform binary. */
127
+ binary?: string;
128
+ /** Passed as `--state-dir` where the command supports it. */
129
+ stateDir?: string;
130
+ }
131
+
132
+ export class Riftri {
133
+ constructor(options?: RiftriOptions);
134
+ repository: string;
135
+
136
+ /** Inspect Git and storage. Creates nothing. */
137
+ doctor(options?: { destination?: string }): Promise<DoctorReport>;
138
+ backends(path?: string): Promise<unknown>;
139
+ /** Convenience: true when this destination can be optimized. */
140
+ isOptimizable(destination?: string): Promise<boolean>;
141
+
142
+ enable(): Promise<null>;
143
+ disable(): Promise<null>;
144
+
145
+ status(): Promise<StatusReport>;
146
+ repair(): Promise<RepairReport>;
147
+ gc(options?: { apply?: boolean }): Promise<unknown>;
148
+
149
+ /** Escape hatch: run any command and parse its report. */
150
+ run(args: string[], options?: { json?: boolean; cwd?: string }): Promise<unknown>;
151
+
152
+ readonly worktree: {
153
+ add(destination: string, options?: AddOptions): Promise<AddReport>;
154
+ list(options?: { allStates?: boolean }): Promise<WorktreeInventory>;
155
+ /** Refuses a dirty worktree unless `force` is set. */
156
+ remove(destination: string, options?: { force?: boolean }): Promise<unknown>;
157
+ move(source: string, destination: string): Promise<unknown>;
158
+ compact(destination: string): Promise<unknown>;
159
+ prune(): Promise<unknown>;
160
+ };
161
+ }
package/lib/client.js ADDED
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+
3
+ // Programmatic API for the Riftri CLI.
4
+ //
5
+ // The binary is the implementation; this is a typed, promise-returning wrapper
6
+ // around it. Every method runs one command, returns its parsed `--json` report,
7
+ // and throws a RiftriError carrying the parsed `--json-errors` receipt on
8
+ // failure. Nothing here reimplements Riftri behaviour.
9
+
10
+ const { spawn } = require("node:child_process");
11
+ const { resolveBinary } = require("./platform.js");
12
+
13
+ /** Process exit codes. Documented in docs/cli.md. */
14
+ const EXIT_SUCCESS = 0;
15
+ const EXIT_OPERATIONAL = 1;
16
+ const EXIT_USAGE = 2;
17
+ const EXIT_POLICY = 3;
18
+
19
+ /** Commands that accept `--json`. The rest print human text on success. */
20
+ const REPORTING = new Set([
21
+ "doctor",
22
+ "backends",
23
+ "status",
24
+ "repair",
25
+ "gc",
26
+ "worktree",
27
+ ]);
28
+
29
+ class RiftriError extends Error {
30
+ constructor(exitCode, receipt, stderr) {
31
+ super(receipt?.message || stderr.trim() || `riftri exited ${exitCode}`);
32
+ this.name = "RiftriError";
33
+ this.exitCode = exitCode;
34
+ this.receipt = receipt ?? null;
35
+ }
36
+
37
+ /** Riftri declined before touching anything. Falling back is safe. */
38
+ get isPolicyRefusal() {
39
+ return this.exitCode === EXIT_POLICY;
40
+ }
41
+
42
+ /** The request itself was malformed. Never retry unchanged. */
43
+ get isUsageError() {
44
+ return this.exitCode === EXIT_USAGE;
45
+ }
46
+
47
+ /** A live process holds the lock; unlike a refusal, waiting helps. */
48
+ get isBusy() {
49
+ return this.receipt?.code === "worktree-busy";
50
+ }
51
+
52
+ /** Run `nextCommand` (usually `riftri repair`) before continuing. */
53
+ get needsRepair() {
54
+ return this.receipt?.recovery === "required";
55
+ }
56
+ }
57
+
58
+ function flagsFor(options) {
59
+ const flags = [];
60
+ for (const [key, value] of Object.entries(options)) {
61
+ if (value === undefined || value === null || value === false) continue;
62
+ const flag = `--${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
63
+ if (value === true) flags.push(flag);
64
+ else if (Array.isArray(value)) for (const item of value) flags.push(flag, String(item));
65
+ else flags.push(flag, String(value));
66
+ }
67
+ return flags;
68
+ }
69
+
70
+ class Riftri {
71
+ /**
72
+ * @param {object} [options]
73
+ * @param {string} [options.repository] working directory for every command
74
+ * @param {string} [options.binary] explicit riftri executable
75
+ * @param {string} [options.stateDir] passed as --state-dir where supported
76
+ */
77
+ constructor(options = {}) {
78
+ this.repository = options.repository ?? process.cwd();
79
+ this.binary = options.binary ?? null;
80
+ this.stateDir = options.stateDir ?? null;
81
+ this.worktree = {
82
+ add: this.#worktreeAdd.bind(this),
83
+ list: this.#worktreeList.bind(this),
84
+ remove: this.#worktreeRemove.bind(this),
85
+ move: this.#worktreeMove.bind(this),
86
+ compact: this.#worktreeCompact.bind(this),
87
+ prune: this.#worktreePrune.bind(this),
88
+ };
89
+ }
90
+
91
+ /** Resolve the executable: explicit, else the installed platform package. */
92
+ #executable() {
93
+ if (this.binary) return this.binary;
94
+ this.binary = resolveBinary();
95
+ return this.binary;
96
+ }
97
+
98
+ /**
99
+ * Run one command and resolve with its parsed report, or null when the
100
+ * command does not support `--json`.
101
+ */
102
+ run(args, { json = REPORTING.has(args[0]), cwd = this.repository } = {}) {
103
+ const flags = json ? ["--json", "--json-errors"] : ["--json-errors"];
104
+ return new Promise((resolve, reject) => {
105
+ let child;
106
+ try {
107
+ child = spawn(this.#executable(), [...args, ...flags], {
108
+ cwd,
109
+ stdio: ["ignore", "pipe", "pipe"],
110
+ });
111
+ } catch (error) {
112
+ reject(error); // no installed binary for this platform
113
+ return;
114
+ }
115
+ let stdout = "";
116
+ let stderr = "";
117
+ child.stdout.on("data", (chunk) => (stdout += chunk));
118
+ child.stderr.on("data", (chunk) => (stderr += chunk));
119
+ child.on("error", reject);
120
+ child.on("close", (exitCode) => {
121
+ if (exitCode === EXIT_SUCCESS) {
122
+ resolve(json && stdout.trim() ? JSON.parse(stdout) : null);
123
+ return;
124
+ }
125
+ let receipt = null;
126
+ try {
127
+ receipt = JSON.parse(stderr);
128
+ } catch {
129
+ // Usage errors come from the argument parser and carry no receipt.
130
+ }
131
+ reject(new RiftriError(exitCode, receipt, stderr));
132
+ });
133
+ });
134
+ }
135
+
136
+ #stateFlag() {
137
+ return this.stateDir ? ["--state-dir", this.stateDir] : [];
138
+ }
139
+
140
+ /** Inspect Git and storage without creating anything. */
141
+ doctor(options = {}) {
142
+ return this.run(["doctor", ...flagsFor(options)]);
143
+ }
144
+
145
+ /** Probe storage backends for a destination volume. */
146
+ backends(pathOrOptions = {}) {
147
+ const target = typeof pathOrOptions === "string" ? [pathOrOptions] : [];
148
+ return this.run(["backends", ...target]);
149
+ }
150
+
151
+ /** True when this destination can get an optimized worktree. */
152
+ async isOptimizable(destination) {
153
+ try {
154
+ const report = await this.doctor(destination ? { destination } : {});
155
+ return Boolean(report?.cow_backend_active);
156
+ } catch (error) {
157
+ if (error.code === "ENOENT" || error instanceof RiftriError === false) return false;
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ /** Opt one repository in. Required before interception applies. */
163
+ enable() {
164
+ return this.run(["enable"], { json: false });
165
+ }
166
+
167
+ disable() {
168
+ return this.run(["disable"], { json: false });
169
+ }
170
+
171
+ /** Bases, reference counts, storage use, and pending operations. */
172
+ status() {
173
+ return this.run(["status", ...this.#stateFlag()]);
174
+ }
175
+
176
+ /** Resume or roll back interrupted operations. Safe to run repeatedly. */
177
+ repair() {
178
+ return this.run(["repair", ...this.#stateFlag()]);
179
+ }
180
+
181
+ /** Plan base collection; pass `{ apply: true }` to delete. */
182
+ gc({ apply = false } = {}) {
183
+ const args = ["gc", ...this.#stateFlag()];
184
+ if (apply) args.push("--apply", "--yes");
185
+ return this.run(args);
186
+ }
187
+
188
+ #worktreeAdd(destination, options = {}) {
189
+ const { revision, ...rest } = options;
190
+ const args = ["worktree", "add", destination];
191
+ if (revision) args.push(revision);
192
+ return this.run([...args, ...flagsFor(rest), ...this.#stateFlag()]);
193
+ }
194
+
195
+ #worktreeList(options = {}) {
196
+ return this.run(["worktree", "list", ...flagsFor(options), ...this.#stateFlag()]);
197
+ }
198
+
199
+ #worktreeRemove(destination, { force = false } = {}) {
200
+ const args = ["worktree", "remove", destination];
201
+ if (force) args.push("--force", "--yes");
202
+ return this.run([...args, ...this.#stateFlag()]);
203
+ }
204
+
205
+ #worktreeMove(source, destination) {
206
+ return this.run(["worktree", "move", source, destination, ...this.#stateFlag()]);
207
+ }
208
+
209
+ #worktreeCompact(destination) {
210
+ return this.run(["worktree", "compact", destination, ...this.#stateFlag()]);
211
+ }
212
+
213
+ #worktreePrune() {
214
+ return this.run(["worktree", "prune", ...this.#stateFlag()]);
215
+ }
216
+ }
217
+
218
+ module.exports = {
219
+ Riftri,
220
+ RiftriError,
221
+ EXIT_SUCCESS,
222
+ EXIT_OPERATIONAL,
223
+ EXIT_USAGE,
224
+ EXIT_POLICY,
225
+ };
package/lib/platform.js CHANGED
@@ -14,6 +14,15 @@ 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
+
17
26
  function detectLinuxLibc(report = process.report) {
18
27
  if (!report || typeof report.getReport !== "function") {
19
28
  return "unknown";
@@ -102,8 +111,13 @@ function resolveBinary(options = {}) {
102
111
  try {
103
112
  packageJson = require.resolve(`${packageName}/package.json`);
104
113
  } 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];
105
117
  throw new Error(
106
- `the optional native package ${packageName} is missing; reinstall without --omit=optional, or build Riftri from source`,
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`,
107
121
  );
108
122
  }
109
123
 
@@ -125,6 +139,7 @@ function resolveBinary(options = {}) {
125
139
 
126
140
  module.exports = {
127
141
  PLATFORM_PACKAGES,
142
+ UNPUBLISHED_PACKAGES,
128
143
  assertCompatiblePackageManifest,
129
144
  binaryName,
130
145
  detectLinuxLibc,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "riftri",
3
- "version": "0.3.5",
3
+ "version": "0.4.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.3.5",
22
- "riftri-darwin-x64": "0.3.5",
23
- "riftri-linux-arm64-gnu": "0.3.5",
24
- "riftri-linux-arm64-musl": "0.3.5",
25
- "riftri-linux-x64-gnu": "0.3.5",
26
- "riftri-linux-x64-musl": "0.3.5",
27
- "riftri-win32-arm64": "0.3.5",
28
- "riftri-win32-x64": "0.3.5"
21
+ "riftri-darwin-arm64": "0.4.0",
22
+ "riftri-darwin-x64": "0.4.0",
23
+ "riftri-linux-arm64-gnu": "0.4.0",
24
+ "riftri-linux-arm64-musl": "0.4.0",
25
+ "riftri-linux-x64-gnu": "0.4.0",
26
+ "riftri-linux-x64-musl": "0.4.0",
27
+ "riftri-win32-arm64": "0.4.0",
28
+ "riftri-win32-x64": "0.4.0"
29
29
  },
30
30
  "repository": {
31
31
  "type": "git",
@@ -45,5 +45,14 @@
45
45
  "publishConfig": {
46
46
  "access": "public",
47
47
  "provenance": true
48
+ },
49
+ "main": "lib/client.js",
50
+ "types": "lib/client.d.ts",
51
+ "exports": {
52
+ ".": {
53
+ "types": "./lib/client.d.ts",
54
+ "require": "./lib/client.js",
55
+ "default": "./lib/client.js"
56
+ }
48
57
  }
49
58
  }