riftri 0.3.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Riftri contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # Riftri
2
+
3
+ **Lightweight Git workspaces for parallel development.**
4
+
5
+ Riftri creates real Git worktrees without eagerly storing another full physical
6
+ copy of every unchanged project file. It is designed for developers and coding
7
+ agents working on several tasks at once.
8
+
9
+ [![CI](https://github.com/assistant-ui/riftri/actions/workflows/ci.yml/badge.svg)](https://github.com/assistant-ui/riftri/actions/workflows/ci.yml)
10
+
11
+ > [!WARNING]
12
+ > Riftri is experimental, pre-release software. Keep important work committed or
13
+ > backed up. Optimized worktrees need APFS on macOS, Btrfs or reflink-enabled
14
+ > XFS or OverlayFS on Linux, or ReFS on Windows — see
15
+ > [what works today](#what-works-today).
16
+
17
+ ## Why Riftri?
18
+
19
+ Git linked worktrees share repository history, but each worktree normally gets
20
+ its own complete checkout. That can consume significant disk space when several
21
+ developers or agents work on a large repository in parallel.
22
+
23
+ Riftri keeps normal Git behavior while making those checkouts lightweight:
24
+
25
+ - Every workspace is a real Git linked worktree.
26
+ - Unchanged files share native copy-on-write storage blocks.
27
+ - Changes remain private to each worktree.
28
+ - Editors, build tools, and agents use ordinary files and Git commands.
29
+ - Interrupted creation and cleanup operations can be recovered safely.
30
+
31
+ Riftri does not replace Git, manage branches, or sit between applications and
32
+ the filesystem. For how it compares to plain `git worktree`, reference clones,
33
+ manual reflink copies, and per-agent containers, see the
34
+ [comparison with alternatives](docs/comparison.md).
35
+
36
+ ## Install
37
+
38
+ macOS and Linux:
39
+
40
+ ```sh
41
+ curl -fsSL https://riftri.dev/install.sh | bash
42
+ ```
43
+
44
+ Windows PowerShell:
45
+
46
+ ```powershell
47
+ $Installer = Join-Path $env:TEMP 'riftri-install.ps1'
48
+ Invoke-WebRequest https://riftri.dev/install.ps1 -OutFile $Installer
49
+ & $Installer
50
+ ```
51
+
52
+ Both installers verify SHA-256 and the binary version, install to a per-user
53
+ directory, and never use sudo, edit shell profiles, or activate Git
54
+ interception. Node.js is not required. Read
55
+ [`install.sh`](package/install.sh) or [`install.ps1`](package/install.ps1)
56
+ first if you prefer, and see the [installation guide](docs/install.md) for all
57
+ eight targets, manual downloads, and checksum verification.
58
+
59
+ Or build from source:
60
+
61
+ ```console
62
+ $ cargo build --release -p riftri-cli
63
+ $ ./target/release/riftri doctor
64
+ ```
65
+
66
+ The interactive `riftri setup` terminal UI is behind an off-by-default `tui`
67
+ feature; add `--features tui` to build it in (the published binaries do). The
68
+ default build keeps `setup` working through plain text prompts.
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.
73
+
74
+ ## Quick start
75
+
76
+ For an interactive walkthrough (v0.3.1 and later), run
77
+ `riftri setup` inside your existing repository. It checks the destination,
78
+ confirms a new worktree at `HEAD`, and then asks whether to open Claude Code,
79
+ Codex, another installed executable, or no agent. Repository enablement and
80
+ agent launch require a separate confirmation. See the
81
+ [setup reference](docs/cli.md#riftri-setup-options).
82
+
83
+ The explicit commands below work in the current release and in automation.
84
+
85
+ Check that the repository and destination are compatible — this creates
86
+ nothing:
87
+
88
+ ```console
89
+ $ riftri doctor --destination ../app-auth
90
+ ```
91
+
92
+ Then create a worktree:
93
+
94
+ ```console
95
+ $ riftri worktree add ../app-auth -b feature/auth main
96
+ $ cd ../app-auth
97
+ $ git status
98
+ ```
99
+
100
+ To attach an existing branch instead, drop the `-b` flag:
101
+
102
+ ```console
103
+ $ riftri worktree add ../app-auth feature/auth
104
+ ```
105
+
106
+ The new directory behaves like any other Git worktree.
107
+
108
+ ## Use normal `git worktree` commands
109
+
110
+ Activate the Git shim in your shell, then opt in each repository:
111
+
112
+ ```console
113
+ $ eval "$(riftri shell hook zsh)"
114
+ $ cd app
115
+ $ riftri enable
116
+ $ git worktree add -b feature/auth ../app-auth main
117
+ ```
118
+
119
+ For a single agent or command tree, skip the shell hook entirely:
120
+
121
+ ```console
122
+ $ riftri enable
123
+ $ riftri exec -- claude
124
+ ```
125
+
126
+ Activating the shell hook does **not** enable Riftri everywhere: `riftri enable`
127
+ is still required per repository, and other repositories go straight to real
128
+ Git. Use `riftri disable` to opt out and `riftri shell status` to inspect
129
+ activation.
130
+
131
+ PowerShell setup, IDE and container edge cases, and agent harness integration
132
+ are covered in [global activation](docs/global-activation.md) and the
133
+ [agent integration guide](docs/agent-integration.md).
134
+
135
+ ## What works today
136
+
137
+ | Platform | Backend | Status |
138
+ | --- | --- | --- |
139
+ | macOS | APFS clones | Supported |
140
+ | Linux | Btrfs / reflink-enabled XFS | Supported |
141
+ | Linux | OverlayFS | Experimental |
142
+ | Windows | ReFS block clones | Supported |
143
+
144
+ On those backends Riftri creates real linked worktrees, intercepts Git per
145
+ repository or per process, removes, moves, prunes, and compacts them, reuses
146
+ immutable bases with disk accounting, and recovers from interruption through
147
+ journaled repair and garbage collection.
148
+
149
+ OverlayFS additionally needs either a mount-capable namespace or a one-time
150
+ helper. The helper is installed system-wide but intercepts nothing on its own,
151
+ and `riftri enable` remains a separate per-repository choice:
152
+
153
+ ```console
154
+ $ sudo riftri overlayfs install-helper
155
+ ```
156
+
157
+ **Riftri fails closed.** When a checkout cannot be reproduced exactly — custom
158
+ filters, sparse checkout, submodules, external attributes, checkout hooks, or non-canonical
159
+ [Git LFS](docs/git-lfs.md) setups — it stops before changing anything rather
160
+ than silently falling back to a full copy. See
161
+ [how Riftri stays safe](docs/safety.md).
162
+
163
+ ## Common commands
164
+
165
+ ```console
166
+ $ riftri doctor # check compatibility
167
+ $ riftri worktree list # managed worktrees and their disk use
168
+ $ riftri status # bases, references, and accounting
169
+ $ riftri worktree compact ../app-auth
170
+ $ riftri repair # resume or roll back interrupted work
171
+ $ riftri gc --apply # delete unreferenced bases
172
+ ```
173
+
174
+ Explicit lifecycle and diagnostic commands accept `--json` and `--json-errors`
175
+ for structured success and failure receipts, so automation does not have to
176
+ scrape human text. Interactive `setup` is for terminal users, not automation.
177
+ The full reference is in [docs/cli.md](docs/cli.md).
178
+
179
+ ## Optional agent skill
180
+
181
+ The repository includes a reviewable
182
+ [`riftri-worktrees` skill](skills/riftri-worktrees/SKILL.md) for agents that need
183
+ to create or use Riftri worktrees. It explains explicit creation, process-scoped
184
+ Git interception, and safe cleanup; it is not required for copy-on-write safety
185
+ and does not install Riftri or enable a repository.
186
+
187
+ Install it through the [skills CLI](https://skills.sh/docs/cli):
188
+
189
+ ```sh
190
+ npx skills add assistant-ui/riftri --skill riftri-worktrees
191
+ ```
192
+
193
+ Review the skill first and choose the agent and installation scope in the CLI.
194
+ The skill files are hosted here on GitHub; [skills.sh](https://skills.sh/docs/faq)
195
+ discovers and ranks skills through CLI installation telemetry. A directory
196
+ listing is not a separate package publication or a guarantee of inclusion.
197
+
198
+ ## Documentation
199
+
200
+ [docs/README.md](docs/README.md) indexes everything. Start with:
201
+
202
+ - [Architecture](docs/architecture.md) — immutable bases, transactions, journals
203
+ - [How Riftri stays safe](docs/safety.md) — fail-closed rules and real-filesystem CI
204
+ - [Troubleshooting](docs/troubleshooting.md) — symptom-first fixes
205
+ - [CLI reference](docs/cli.md) · [Installation](docs/install.md) · [Benchmarks](docs/benchmarks.md)
206
+ - [Project definition](PROJECT.md) · [Roadmap](ROADMAP.md) · [Releasing](RELEASING.md)
207
+
208
+ ## Contributing
209
+
210
+ Run the complete local quality gate before opening a pull request:
211
+
212
+ ```console
213
+ $ cargo fmt --all --check
214
+ $ cargo clippy --workspace --all-targets --all-features -- -D warnings
215
+ $ cargo test --workspace
216
+ $ npm test
217
+ $ npm run smoke:installed
218
+ ```
219
+
220
+ Riftri is licensed under the [MIT License](LICENSE).
package/bin/riftri.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const { spawn } = require("node:child_process");
6
+ const { resolveBinary } = require("../lib/platform.js");
7
+ const { signalExitCode } = require("../lib/signals.js");
8
+
9
+ function fail(message) {
10
+ process.stderr.write(`riftri: ${message}\n`);
11
+ process.exit(1);
12
+ }
13
+
14
+ let binary;
15
+ try {
16
+ binary = resolveBinary();
17
+ } catch (error) {
18
+ fail(error instanceof Error ? error.message : String(error));
19
+ }
20
+
21
+ const child = spawn(binary, process.argv.slice(2), {
22
+ stdio: "inherit",
23
+ windowsHide: false,
24
+ });
25
+
26
+ // Mirror the termination contract of native `riftri exec` (docs/cli.md) while
27
+ // the native process runs. The launcher survives SIGINT (Ctrl-C) and SIGQUIT
28
+ // (Ctrl-\): a terminal delivers both to the whole foreground process group,
29
+ // so the native process receives its own copy and — like a shell waiting on a
30
+ // foreground job — the command alone decides whether the interrupt is fatal.
31
+ // Dying here instead would return the shell prompt while riftri and a command
32
+ // that caught the interrupt keep running on the terminal. PID-directed
33
+ // SIGTERM and SIGHUP are forwarded to the native process, which applies the
34
+ // same contract downstream to the scoped command. Windows has no equivalent
35
+ // to preserve: the console already delivers Ctrl-C events to every attached
36
+ // process, and a hard TerminateProcess cannot be intercepted.
37
+ const ignoredSignals = ["SIGINT", "SIGQUIT"];
38
+ const forwardedSignals = ["SIGTERM", "SIGHUP"];
39
+ const ignoreSignal = () => {};
40
+ const forwardSignal = (signal) => {
41
+ child.kill(signal);
42
+ };
43
+
44
+ if (process.platform !== "win32") {
45
+ for (const signal of ignoredSignals) {
46
+ process.on(signal, ignoreSignal);
47
+ }
48
+ for (const signal of forwardedSignals) {
49
+ process.on(signal, forwardSignal);
50
+ }
51
+ }
52
+
53
+ function releaseSignals() {
54
+ for (const signal of ignoredSignals) {
55
+ process.removeListener(signal, ignoreSignal);
56
+ }
57
+ for (const signal of forwardedSignals) {
58
+ process.removeListener(signal, forwardSignal);
59
+ }
60
+ }
61
+
62
+ child.on("error", (error) => {
63
+ releaseSignals();
64
+ fail(`could not start the native executable at ${binary}: ${error.message}`);
65
+ });
66
+
67
+ child.on("exit", (code, signal) => {
68
+ releaseSignals();
69
+ if (signal !== null) {
70
+ const exitCode = signalExitCode(signal);
71
+ if (exitCode === null) {
72
+ fail(`native executable stopped after unknown signal ${signal}`);
73
+ }
74
+ process.exit(exitCode);
75
+ }
76
+ process.exit(code ?? 1);
77
+ });
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ const PLATFORM_PACKAGES = Object.freeze({
7
+ "darwin-arm64": "riftri-darwin-arm64",
8
+ "darwin-x64": "riftri-darwin-x64",
9
+ "linux-arm64-gnu": "riftri-linux-arm64-gnu",
10
+ "linux-arm64-musl": "riftri-linux-arm64-musl",
11
+ "linux-x64-gnu": "riftri-linux-x64-gnu",
12
+ "linux-x64-musl": "riftri-linux-x64-musl",
13
+ "win32-arm64": "riftri-win32-arm64",
14
+ "win32-x64": "riftri-win32-x64",
15
+ });
16
+
17
+ function detectLinuxLibc(report = process.report) {
18
+ if (!report || typeof report.getReport !== "function") {
19
+ return "unknown";
20
+ }
21
+
22
+ const header = report.getReport()?.header;
23
+ return header?.glibcVersionRuntime ? "gnu" : "musl";
24
+ }
25
+
26
+ function platformKey(
27
+ platform = process.platform,
28
+ architecture = process.arch,
29
+ report = process.report,
30
+ ) {
31
+ if (platform === "linux") {
32
+ return `${platform}-${architecture}-${detectLinuxLibc(report)}`;
33
+ }
34
+ return `${platform}-${architecture}`;
35
+ }
36
+
37
+ function packageNameForPlatform(platform, architecture, report) {
38
+ return PLATFORM_PACKAGES[platformKey(platform, architecture, report)] ?? null;
39
+ }
40
+
41
+ function binaryName(platform = process.platform) {
42
+ return platform === "win32" ? "riftri.exe" : "riftri";
43
+ }
44
+
45
+ function assertUsableBinary(candidate, source) {
46
+ try {
47
+ fs.accessSync(
48
+ candidate,
49
+ process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK,
50
+ );
51
+ } catch {
52
+ throw new Error(`${source} does not contain an executable Riftri binary: ${candidate}`);
53
+ }
54
+ return candidate;
55
+ }
56
+
57
+ function launcherVersion() {
58
+ for (const candidate of [
59
+ path.join(__dirname, "..", "package.json"),
60
+ path.join(__dirname, "..", "..", "package.json"),
61
+ ]) {
62
+ try {
63
+ const manifest = JSON.parse(fs.readFileSync(candidate, "utf8"));
64
+ if (manifest.name === "riftri" && typeof manifest.version === "string") {
65
+ return manifest.version;
66
+ }
67
+ } catch {
68
+ // The source and staged package layouts put the root manifest at different depths.
69
+ }
70
+ }
71
+ throw new Error("could not read the riftri launcher version");
72
+ }
73
+
74
+ function assertCompatiblePackageManifest(manifest, packageName, expectedVersion) {
75
+ if (manifest.name !== packageName || manifest.version !== expectedVersion) {
76
+ throw new Error(
77
+ `the optional native package ${packageName} does not match riftri ${expectedVersion}; reinstall riftri and its optional dependencies together`,
78
+ );
79
+ }
80
+ }
81
+
82
+ function resolveBinary(options = {}) {
83
+ const environment = options.environment ?? process.env;
84
+ const platform = options.platform ?? process.platform;
85
+ const architecture = options.architecture ?? process.arch;
86
+ const report = options.report ?? process.report;
87
+
88
+ if (environment.RIFTRI_BINARY) {
89
+ const override = path.resolve(environment.RIFTRI_BINARY);
90
+ return assertUsableBinary(override, "RIFTRI_BINARY");
91
+ }
92
+
93
+ const packageName = packageNameForPlatform(platform, architecture, report);
94
+ if (!packageName) {
95
+ const key = platformKey(platform, architecture, report);
96
+ throw new Error(
97
+ `no prebuilt native package is available for ${key}; supported packages: ${Object.keys(PLATFORM_PACKAGES).join(", ")}`,
98
+ );
99
+ }
100
+
101
+ let packageJson;
102
+ try {
103
+ packageJson = require.resolve(`${packageName}/package.json`);
104
+ } catch {
105
+ throw new Error(
106
+ `the optional native package ${packageName} is missing; reinstall without --omit=optional, or build Riftri from source`,
107
+ );
108
+ }
109
+
110
+ let packageManifest;
111
+ try {
112
+ packageManifest = JSON.parse(fs.readFileSync(packageJson, "utf8"));
113
+ } catch {
114
+ throw new Error(`the optional native package ${packageName} has an invalid manifest`);
115
+ }
116
+ assertCompatiblePackageManifest(
117
+ packageManifest,
118
+ packageName,
119
+ options.expectedVersion ?? launcherVersion(),
120
+ );
121
+
122
+ const candidate = path.join(path.dirname(packageJson), "bin", binaryName(platform));
123
+ return assertUsableBinary(candidate, packageName);
124
+ }
125
+
126
+ module.exports = {
127
+ PLATFORM_PACKAGES,
128
+ assertCompatiblePackageManifest,
129
+ binaryName,
130
+ detectLinuxLibc,
131
+ packageNameForPlatform,
132
+ platformKey,
133
+ resolveBinary,
134
+ };
package/lib/signals.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ const os = require("node:os");
4
+
5
+ /**
6
+ * Exit code the launcher reports for a native process that died from `signal`,
7
+ * following the shell convention native `riftri exec` itself uses: 128 plus
8
+ * the signal's number on this platform. The code is computed numerically —
9
+ * never by re-raising the signal through `process.kill`, which is a silent
10
+ * no-op for signals Node ignores or reserves (SIGPIPE, SIGUSR1, SIGWINCH,
11
+ * SIGCHLD, SIGURG) and would turn a killed run into exit 0.
12
+ *
13
+ * Returns `null` when the name is not a signal this platform knows, so the
14
+ * caller can report the failure instead of guessing a number.
15
+ */
16
+ function signalExitCode(signal) {
17
+ if (typeof signal !== "string") {
18
+ return null;
19
+ }
20
+ const number = os.constants.signals[signal];
21
+ if (!Number.isInteger(number) || number <= 0) {
22
+ return null;
23
+ }
24
+ return 128 + number;
25
+ }
26
+
27
+ module.exports = { signalExitCode };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "riftri",
3
+ "version": "0.3.5",
4
+ "description": "Experimental lightweight Git workspaces for parallel development",
5
+ "license": "MIT",
6
+ "author": "Assistant UI contributors",
7
+ "type": "commonjs",
8
+ "bin": {
9
+ "riftri": "bin/riftri.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "lib",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18.18"
19
+ },
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"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/assistant-ui/riftri.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/assistant-ui/riftri/issues"
36
+ },
37
+ "homepage": "https://riftri.dev",
38
+ "keywords": [
39
+ "git",
40
+ "worktree",
41
+ "copy-on-write",
42
+ "developer-tools",
43
+ "coding-agents"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "provenance": true
48
+ }
49
+ }