claude-garage 0.1.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/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/garage.js +143 -0
- package/daemon/package.json +13 -0
- package/daemon/src/diff.js +465 -0
- package/daemon/src/editor.js +63 -0
- package/daemon/src/events.js +54 -0
- package/daemon/src/hooks.js +241 -0
- package/daemon/src/index.js +76 -0
- package/daemon/src/notify.js +80 -0
- package/daemon/src/picker.js +48 -0
- package/daemon/src/poller.js +134 -0
- package/daemon/src/registry.js +189 -0
- package/daemon/src/security.js +39 -0
- package/daemon/src/sessions.js +255 -0
- package/daemon/src/status.js +59 -0
- package/daemon/src/term.js +94 -0
- package/daemon/src/tmux.js +124 -0
- package/daemon/src/workspaces.js +139 -0
- package/daemon/src/worktrees.js +180 -0
- package/package.json +57 -0
- package/ui/dist/apple-touch-icon.png +0 -0
- package/ui/dist/assets/google-sans-code-latin-400-normal-C-TpydLV.woff +0 -0
- package/ui/dist/assets/google-sans-code-latin-400-normal-CqaqKeEp.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-latin-600-normal-BUCrGHE5.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-latin-600-normal-CKee-OZj.woff +0 -0
- package/ui/dist/assets/google-sans-code-latin-700-normal-0vT4k3S3.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-latin-700-normal-BvnkA064.woff +0 -0
- package/ui/dist/assets/google-sans-code-latin-ext-400-normal-CALCk2lB.woff +0 -0
- package/ui/dist/assets/google-sans-code-latin-ext-400-normal-xZllot2U.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-latin-ext-600-normal-CNNK3O9-.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-latin-ext-600-normal-DDkmmTp6.woff +0 -0
- package/ui/dist/assets/google-sans-code-latin-ext-700-normal-CxgnixdG.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-latin-ext-700-normal-bxgt_uDG.woff +0 -0
- package/ui/dist/assets/google-sans-code-math-400-normal-C2EgcUvD.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-math-400-normal-CgFzjhah.woff +0 -0
- package/ui/dist/assets/google-sans-code-math-600-normal-B5PK-qTM.woff +0 -0
- package/ui/dist/assets/google-sans-code-math-600-normal-DT_RT86i.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-math-700-normal-C65M3EbO.woff +0 -0
- package/ui/dist/assets/google-sans-code-math-700-normal-CgTZgjvl.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-symbols-400-normal-BtBR4e7S.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-symbols-400-normal-NSSmFXdP.woff +0 -0
- package/ui/dist/assets/google-sans-code-symbols-600-normal-DdWLfSlP.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-symbols-600-normal-QtnQvwms.woff +0 -0
- package/ui/dist/assets/google-sans-code-symbols-700-normal-Ceb9rWtn.woff +0 -0
- package/ui/dist/assets/google-sans-code-symbols-700-normal-DDa4ndCu.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-symbols2-400-normal-CicwJrzf.woff +0 -0
- package/ui/dist/assets/google-sans-code-symbols2-600-normal-BtzXZASM.woff +0 -0
- package/ui/dist/assets/google-sans-code-symbols2-700-normal-Cci1DrKv.woff +0 -0
- package/ui/dist/assets/google-sans-code-vietnamese-400-normal-CWRUr5CO.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-vietnamese-400-normal-DhSdRbI2.woff +0 -0
- package/ui/dist/assets/google-sans-code-vietnamese-600-normal-DBBK0KXU.woff +0 -0
- package/ui/dist/assets/google-sans-code-vietnamese-600-normal-zsb-D4cZ.woff2 +0 -0
- package/ui/dist/assets/google-sans-code-vietnamese-700-normal-DDJUOtHZ.woff +0 -0
- package/ui/dist/assets/google-sans-code-vietnamese-700-normal-jZ3jhCNl.woff2 +0 -0
- package/ui/dist/assets/index-B9ScE6Rx.css +32 -0
- package/ui/dist/assets/index-BjoMvOWt.js +70 -0
- package/ui/dist/favicon-32.png +0 -0
- package/ui/dist/index.html +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saiful Islam
|
|
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,184 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="docs/banner.png" alt="claude-garage: a pit wall for your Claude Code agents" width="100%" />
|
|
4
|
+
|
|
5
|
+
**Multiple Claude agents driving you crazy? Park them all in one
|
|
6
|
+
garage.** Every session, every project, on one live wall: no window
|
|
7
|
+
juggling, no tab hunting, and you know the instant one needs you.
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/claude-garage)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](package.json)
|
|
12
|
+
[](#local-only-by-design)
|
|
13
|
+
|
|
14
|
+
<img src="docs/hero.png" alt="The pit wall: two workspaces, a session asking for permission (amber), a finished worktree session, and the diff pane" width="100%" />
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
Running multiple Claude Code sessions across multiple projects means juggling
|
|
21
|
+
terminal windows and editor windows. There is no single place to see:
|
|
22
|
+
|
|
23
|
+
- **which sessions exist**, per project
|
|
24
|
+
- **which one is blocked waiting for your input**. The real pain isn't window
|
|
25
|
+
count, it's attention routing
|
|
26
|
+
- **what each session changed**, reviewable without hunting
|
|
27
|
+
|
|
28
|
+
claude-garage is that single place: the garage your agents are parked in,
|
|
29
|
+
one screen, built around four things that rarely coexist.
|
|
30
|
+
|
|
31
|
+
1. 🔌 **Real terminals that survive the tool.** tmux owns every session, not
|
|
32
|
+
the app. Close the tab, kill the daemon, reboot the Mac:
|
|
33
|
+
`tmux attach -t garage/<workspace>/<label>` still works, and dead sessions
|
|
34
|
+
restore with their full conversation (`claude --resume`) in one click.
|
|
35
|
+
2. 🖥️ **Every session of a project on screen at once.** Not a switcher, a
|
|
36
|
+
live grid of interactive terminals. Split, resize, maximize, float, or
|
|
37
|
+
detach into standalone views, VS Code-style.
|
|
38
|
+
3. 🚨 **Needs-input triage as a first-class queue.** Blocked sessions sort
|
|
39
|
+
first everywhere, light up amber, count into the header badge and the tab
|
|
40
|
+
title; `a` jumps to whichever agent is waiting, across every project.
|
|
41
|
+
Notifications reach you even when the wall isn't visible.
|
|
42
|
+
4. 📋 **File-by-file diff review with an editor jump.** Per-workspace or
|
|
43
|
+
per-worktree changes (committed and uncommitted), a full-screen review mode
|
|
44
|
+
with viewed-tracking, and `o` to open your editor at the exact line.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx claude-garage
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Opens the pit wall at `http://127.0.0.1:4747`. Add a workspace, spawn
|
|
53
|
+
sessions with the `+` next to its name, and press `?` for the keys.
|
|
54
|
+
|
|
55
|
+
**Requirements**
|
|
56
|
+
|
|
57
|
+
- macOS
|
|
58
|
+
- [tmux](https://github.com/tmux/tmux) ≥ 3.2 (garage offers to `brew install` it if missing)
|
|
59
|
+
- [Claude Code](https://docs.claude.com/en/docs/claude-code) CLI on `PATH`
|
|
60
|
+
- Node.js ≥ 20
|
|
61
|
+
|
|
62
|
+
**Hooks (recommended):** status updates poll every 2s by default. Click
|
|
63
|
+
**install hooks for me** in the banner for instant detection. The daemon
|
|
64
|
+
merges Claude Code's hooks into `~/.claude/settings.json` (backup kept,
|
|
65
|
+
idempotent).
|
|
66
|
+
|
|
67
|
+
## Local-only by design
|
|
68
|
+
|
|
69
|
+
Everything runs on your machine and stays there.
|
|
70
|
+
|
|
71
|
+
- The daemon binds to `127.0.0.1` only. Nothing listens on your network.
|
|
72
|
+
- **No telemetry, no analytics, no accounts.** garage collects nothing and
|
|
73
|
+
phones home to no one.
|
|
74
|
+
- All state is a single local file (`~/.garage/state.json`) plus your own
|
|
75
|
+
tmux server and git repos.
|
|
76
|
+
- Your sessions talk to Claude exactly as they would without garage. The
|
|
77
|
+
wall is a viewer, not a middleman.
|
|
78
|
+
|
|
79
|
+
## Session states
|
|
80
|
+
|
|
81
|
+
| Glyph | State | Meaning |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `●` | **needs-input** | Claude is waiting on *you*: permission, question, plan approval. Never fades; sorts first everywhere. |
|
|
84
|
+
| `◐` | working | Claude is running. |
|
|
85
|
+
| `✓` | done | Finished a turn since you last looked (fades after 2 min). |
|
|
86
|
+
| `○` | idle | Waiting for you to *ask*, not to *answer*. |
|
|
87
|
+
| `⟳` | restorable | tmux died (reboot?). One click resurrects the conversation. |
|
|
88
|
+
|
|
89
|
+
## Also on the wall
|
|
90
|
+
|
|
91
|
+
- 🌳 **Worktree sessions**: spawn in an isolated git worktree on a
|
|
92
|
+
`garage/<label>` branch; on close, **merge / discard / keep**.
|
|
93
|
+
- 🎨 **Themes**: garage, claude dark, claude light, or follow the OS.
|
|
94
|
+
Terminals re-skin in place, full ANSI palettes included.
|
|
95
|
+
- 🔔 **Notifications**: badge + tab title in-app, opt-in browser
|
|
96
|
+
notifications (click to jump) when the tab is hidden, and a macOS
|
|
97
|
+
notification when no page is open (clickable with
|
|
98
|
+
[`terminal-notifier`](https://github.com/julienXX/terminal-notifier)).
|
|
99
|
+
|
|
100
|
+
## The pit pet 🐈
|
|
101
|
+
|
|
102
|
+
An optional ASCII companion on the key strip whose mood *is* the wall:
|
|
103
|
+
asleep when all is quiet, watching while agents run, **sprinting toward the
|
|
104
|
+
rail with a `!`** the moment a session needs you (click it, that's the `a`
|
|
105
|
+
jump), hiding in a box if the daemon drops, and celebrating when the last
|
|
106
|
+
blocked session is answered. **Clicking it when nothing is wrong pets it.**
|
|
107
|
+
It appreciates this.
|
|
108
|
+
|
|
109
|
+
<div align="center">
|
|
110
|
+
<img src="docs/pet-cat.png" width="130" alt="Arthur the shop cat" />
|
|
111
|
+
<img src="docs/pet-duck.png" width="130" alt="Papito the rubber duck" />
|
|
112
|
+
<img src="docs/pet-pup.png" width="130" alt="Segan the pit pup" />
|
|
113
|
+
|
|
114
|
+
<b>Arthur</b> <b>Papito</b> <b>Segan</b>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
Turn it on (or off again) anytime in ⚙ settings under *pit pet*. It's
|
|
118
|
+
**off by default**: nobody gets a surprise Papito. Each species plays its
|
|
119
|
+
state in character:
|
|
120
|
+
|
|
121
|
+
| Pet | Personality |
|
|
122
|
+
|---|---|
|
|
123
|
+
| **Arthur** (shop cat) | saunters, barely deigns to bounce, ignores about 40% of your strolls, and celebrates by kneading in place. Cats don't jump for joy in front of you |
|
|
124
|
+
| **Papito** (rubber duck) | deadpan: waddles, **never** bounces, and alerts with a single motionless stare; celebration is exactly one flap |
|
|
125
|
+
| **Segan** (pit pup) | maximum enthusiasm: fastest runner, biggest bounce, and celebrates with zoomies across the strip |
|
|
126
|
+
|
|
127
|
+
## Keybindings
|
|
128
|
+
|
|
129
|
+
| Key | Action |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `1`–`9` | switch focused workspace |
|
|
132
|
+
| `[` / `]` | cycle terminals within the workspace |
|
|
133
|
+
| `a` | jump to a session that needs input, anywhere |
|
|
134
|
+
| `\` | split the focused cell (new session beside it) |
|
|
135
|
+
| `m` | maximize the focused cell ⇄ restore |
|
|
136
|
+
| `Tab` | changes pane: toggle list ⇄ diff emphasis |
|
|
137
|
+
| `j` / `k` | next / previous changed file |
|
|
138
|
+
| `r` | enter full-screen review mode |
|
|
139
|
+
| `v` | (review mode) mark file viewed, advance to next unviewed |
|
|
140
|
+
| `o` | open the selected file, or the workspace root, in your editor |
|
|
141
|
+
| `Shift+Enter` | newline in Claude Code's composer (no `/terminal-setup` needed) |
|
|
142
|
+
| `Ctrl+\`` | release keys from the terminal back to garage |
|
|
143
|
+
| `?` | keybindings + status legend |
|
|
144
|
+
|
|
145
|
+
Bindings pause while a terminal has keyboard focus. The header chip always
|
|
146
|
+
shows where your keys go.
|
|
147
|
+
|
|
148
|
+
## How it works
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
tmux (persistence, source of truth)
|
|
152
|
+
└─ small Node daemon (spawn / list / bridge / diff / hooks)
|
|
153
|
+
└─ browser UI (React + xterm.js)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The daemon is a thin Fastify process that shells out to `tmux`/`git`/`claude`
|
|
157
|
+
rather than re-implementing them; diffs are computed read-only; hook events
|
|
158
|
+
are token-authed. The UI is a viewer over SSE + WebSockets. tmux is the
|
|
159
|
+
registry: garage can be deleted and your sessions won't notice.
|
|
160
|
+
|
|
161
|
+
## Development
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm install
|
|
165
|
+
npm run dev # daemon :4747 + Vite :5173
|
|
166
|
+
npm test # node:test suite (status, poller, hooks, layout, views)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Built through spec-driven phases ([`openspec/`](openspec/)), each verified
|
|
170
|
+
end-to-end on a real system.
|
|
171
|
+
|
|
172
|
+
## Roadmap
|
|
173
|
+
|
|
174
|
+
- `claude-garage attach`: adopt an existing tmux session onto the wall
|
|
175
|
+
- Phone push (ntfy/webhook) for when you're away from the machine
|
|
176
|
+
- View renaming and drag-between-views
|
|
177
|
+
- Linux support
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
[MIT](LICENSE) © Saiful Islam
|
|
182
|
+
|
|
183
|
+
*claude-garage is a community project, not affiliated with or endorsed by
|
|
184
|
+
Anthropic. "Claude" and "Claude Code" are Anthropic trademarks.*
|
package/bin/garage.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// D-packaging: the `npx claude-garage` entrypoint. Checks prerequisites,
|
|
3
|
+
// starts the daemon in-process with GARAGE_SERVE_UI=1 (same-process
|
|
4
|
+
// UI+API serving — see daemon/src/index.js), then prints and best-effort
|
|
5
|
+
// opens the URL. Never touches tmux on shutdown — SIGINT/SIGTERM close the
|
|
6
|
+
// HTTP server only, so live garage sessions survive the process exiting.
|
|
7
|
+
import { execFile, spawn } from "node:child_process";
|
|
8
|
+
import readline from "node:readline/promises";
|
|
9
|
+
|
|
10
|
+
const PORT = Number(process.env.GARAGE_PORT ?? 4747);
|
|
11
|
+
const HEALTH_URL = `http://127.0.0.1:${PORT}/api/health`;
|
|
12
|
+
const UI_URL = `http://127.0.0.1:${PORT}`;
|
|
13
|
+
|
|
14
|
+
function execFileP(cmd, args) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
execFile(cmd, args, (err, stdout, stderr) => {
|
|
17
|
+
if (err) reject(err);
|
|
18
|
+
else resolve({ stdout, stderr });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Presence-only checks (D-packaging Open Questions: version-gate later if a
|
|
24
|
+
// version-specific bug report ever surfaces). Missing binary produces a
|
|
25
|
+
// readable, actionable error rather than letting the daemon fail deeper and
|
|
26
|
+
// more cryptically the first time it shells out to a missing binary.
|
|
27
|
+
async function requireBinary(bin, versionArgs, installHint) {
|
|
28
|
+
try {
|
|
29
|
+
await execFileP(bin, versionArgs);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err && err.code === "ENOENT") {
|
|
32
|
+
console.error(`${bin} not found — install: ${installHint}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
// Any other failure (non-zero exit, odd version-flag behavior) is
|
|
36
|
+
// treated as "present" — we only gate on outright absence.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// tmux missing: offer to install it — consent-gated, never silent (the
|
|
41
|
+
// README promises garage touches nothing without asking). Only when this
|
|
42
|
+
// is an interactive terminal AND Homebrew is present; any other situation
|
|
43
|
+
// falls back to the plain instruction. Streams brew's own output so the
|
|
44
|
+
// user watches exactly what runs.
|
|
45
|
+
async function offerTmuxInstall() {
|
|
46
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
47
|
+
try {
|
|
48
|
+
await execFileP("brew", ["--version"]);
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
53
|
+
let answer;
|
|
54
|
+
try {
|
|
55
|
+
answer = (await rl.question("tmux not found. Install it now with Homebrew? [y/N] "))
|
|
56
|
+
.trim()
|
|
57
|
+
.toLowerCase();
|
|
58
|
+
} finally {
|
|
59
|
+
rl.close();
|
|
60
|
+
}
|
|
61
|
+
if (answer !== "y" && answer !== "yes") return false;
|
|
62
|
+
console.log("→ brew install tmux");
|
|
63
|
+
const ok = await new Promise((resolve) => {
|
|
64
|
+
const child = spawn("brew", ["install", "tmux"], { stdio: "inherit" });
|
|
65
|
+
child.on("exit", (code) => resolve(code === 0));
|
|
66
|
+
child.on("error", () => resolve(false));
|
|
67
|
+
});
|
|
68
|
+
if (!ok) return false;
|
|
69
|
+
try {
|
|
70
|
+
await execFileP("tmux", ["-V"]);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function main() {
|
|
78
|
+
try {
|
|
79
|
+
await execFileP("tmux", ["-V"]);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err && err.code === "ENOENT") {
|
|
82
|
+
const installed = await offerTmuxInstall();
|
|
83
|
+
if (!installed) {
|
|
84
|
+
console.error("tmux not found — install: brew install tmux");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// any other failure (odd version-flag behavior) — treat as present,
|
|
89
|
+
// same discipline as requireBinary below
|
|
90
|
+
}
|
|
91
|
+
await requireBinary(
|
|
92
|
+
"claude",
|
|
93
|
+
["--version"],
|
|
94
|
+
"see https://docs.claude.com/en/docs/claude-code for install instructions"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
process.env.GARAGE_SERVE_UI = "1";
|
|
98
|
+
|
|
99
|
+
// Relative to this file's URL, not cwd — resolves correctly whether run
|
|
100
|
+
// from a checkout or an installed package (see root package.json `files`).
|
|
101
|
+
const { app } = await import("../daemon/src/index.js");
|
|
102
|
+
|
|
103
|
+
const shutdown = async () => {
|
|
104
|
+
// app.close() drains connections — but SSE streams and terminal
|
|
105
|
+
// WebSockets never end on their own, so a polite close hangs forever
|
|
106
|
+
// when a browser tab is open. Race it against a hard deadline: tmux
|
|
107
|
+
// owns everything that matters, so force-exiting loses nothing.
|
|
108
|
+
const deadline = new Promise((r) => setTimeout(r, 1500));
|
|
109
|
+
try {
|
|
110
|
+
await Promise.race([app.close(), deadline]);
|
|
111
|
+
} catch {
|
|
112
|
+
// best-effort — we're exiting regardless
|
|
113
|
+
} finally {
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
process.on("SIGINT", shutdown);
|
|
118
|
+
process.on("SIGTERM", shutdown);
|
|
119
|
+
|
|
120
|
+
setTimeout(async () => {
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(HEALTH_URL);
|
|
123
|
+
if (!res.ok) throw new Error(`health check returned ${res.status}`);
|
|
124
|
+
|
|
125
|
+
console.log(`claude-garage pit wall → ${UI_URL}`);
|
|
126
|
+
|
|
127
|
+
if (process.platform === "darwin") {
|
|
128
|
+
// Best-effort convenience, same "never throw for a non-critical
|
|
129
|
+
// extra" discipline as notify.js's osascript call — swallow any
|
|
130
|
+
// failure silently.
|
|
131
|
+
execFile("open", [UI_URL], () => {});
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
console.error(
|
|
135
|
+
`claude-garage did not come up on port ${PORT} — see the log above ` +
|
|
136
|
+
`(port already in use? set GARAGE_PORT to pick a different one)`
|
|
137
|
+
);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}, 600);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
main();
|