@paradow/ledge 0.34.0 → 0.35.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.
Files changed (36) hide show
  1. package/dist/cli/index.js +39003 -39
  2. package/dist/cli/{chunk-IWY7DSSO.js → reduceWorker.js} +749 -1188
  3. package/package.json +4 -1
  4. package/dist/cli/action-K42H5BDA.js +0 -27
  5. package/dist/cli/attach-KEE4A5YP.js +0 -87
  6. package/dist/cli/bridge-X327AZY4.js +0 -70
  7. package/dist/cli/chunk-25RVT5MR.js +0 -623
  8. package/dist/cli/chunk-3OJCWJAA.js +0 -144
  9. package/dist/cli/chunk-4OYVM4MT.js +0 -49
  10. package/dist/cli/chunk-4TCSADFR.js +0 -252
  11. package/dist/cli/chunk-53ISXX2R.js +0 -39
  12. package/dist/cli/chunk-54KF5RBY.js +0 -17
  13. package/dist/cli/chunk-7NHQGP6J.js +0 -95
  14. package/dist/cli/chunk-G7XPEVCG.js +0 -118
  15. package/dist/cli/chunk-KPDTYX7F.js +0 -60
  16. package/dist/cli/chunk-OLLQBK6N.js +0 -41
  17. package/dist/cli/chunk-RPH773TY.js +0 -6
  18. package/dist/cli/chunk-RX5CJ7YH.js +0 -43
  19. package/dist/cli/chunk-T547FLQN.js +0 -128
  20. package/dist/cli/chunk-U67V476Y.js +0 -35
  21. package/dist/cli/chunk-UJ2WBGLW.js +0 -305
  22. package/dist/cli/chunk-ULOMKEB6.js +0 -85
  23. package/dist/cli/chunk-V4XKKL6K.js +0 -205
  24. package/dist/cli/daemon-RO43S3S5.js +0 -11449
  25. package/dist/cli/decide-KZFYL3HY.js +0 -59
  26. package/dist/cli/handover-BWNMI2RA.js +0 -17
  27. package/dist/cli/hook-ZLWXI5EQ.js +0 -473
  28. package/dist/cli/launch-GCX7TXOH.js +0 -28
  29. package/dist/cli/send-SXZAPVRH.js +0 -19
  30. package/dist/cli/setup-TJ32FMVJ.js +0 -38
  31. package/dist/cli/status-YDVRES4E.js +0 -28
  32. package/dist/cli/task-TUSOVJK7.js +0 -19
  33. package/dist/cli/tui-TCVIRZCQ.js +0 -19348
  34. package/dist/cli/uninstall-QO7YMSLA.js +0 -162
  35. package/dist/cli/version-BAYSYWFP.js +0 -7
  36. package/dist/cli/who-6AZYSP7T.js +0 -12
@@ -1,305 +0,0 @@
1
- import {
2
- run
3
- } from "./chunk-T547FLQN.js";
4
- import {
5
- ensureLedgeDir,
6
- paths
7
- } from "./chunk-53ISXX2R.js";
8
-
9
- // src/cli/setup.ts
10
- import { readFile as readFile2, writeFile, mkdir, rename, realpath, rm, stat, lstat, readlink } from "fs/promises";
11
- import { execFile } from "child_process";
12
- import { homedir, userInfo } from "os";
13
- import { join, dirname } from "path";
14
-
15
- // src/update/guard.ts
16
- import { readFile } from "fs/promises";
17
- var inDaemonService = (cgroup) => cgroup.split("\n").some((l) => l.split("/").includes(SERVICE));
18
- async function tmuxServerAtRisk(opts = {}) {
19
- const runTmux = opts.runTmux ?? run;
20
- const readCgroup = opts.readCgroup ?? ((pid2) => readFile(`/proc/${pid2}/cgroup`, "utf8"));
21
- const r = await runTmux(["-L", opts.socket ?? "ledge", "list-sessions", "-F", "#{pid}"]);
22
- if (r.code !== 0) return false;
23
- const pid = r.stdout.split("\n").map((s) => s.trim()).find(Boolean);
24
- if (!pid) return false;
25
- try {
26
- return inDaemonService(await readCgroup(pid));
27
- } catch {
28
- return true;
29
- }
30
- }
31
-
32
- // src/cli/setup.ts
33
- var CLAUDE_EVENTS = [
34
- "SessionStart",
35
- "UserPromptSubmit",
36
- "PreToolUse",
37
- "PermissionRequest",
38
- "Notification",
39
- "Stop",
40
- "SessionEnd",
41
- "PostToolUse",
42
- "PostToolUseFailure",
43
- "StopFailure",
44
- "SubagentStart",
45
- "SubagentStop"
46
- ];
47
- var CODEX_EVENTS = [
48
- "SessionStart",
49
- "UserPromptSubmit",
50
- "PreToolUse",
51
- "PermissionRequest",
52
- "PostToolUse",
53
- "Stop",
54
- "SubagentStart",
55
- "SubagentStop",
56
- "Interrupt",
57
- "SessionEnd"
58
- ];
59
- function mergeHooks(settings, events, command, file) {
60
- if (settings === void 0) settings = {};
61
- if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error(`${file} root is not an object, not modified`);
62
- const base = settings;
63
- if (base.hooks !== void 0 && (!base.hooks || typeof base.hooks !== "object" || Array.isArray(base.hooks))) throw new Error(`${file} "hooks" is not an object, not modified`);
64
- const hooks = { ...base.hooks ?? {} };
65
- let changed = false;
66
- for (const ev of events) {
67
- if (hooks[ev] !== void 0 && !Array.isArray(hooks[ev])) throw new Error(`${file} hooks.${ev} is not an array, not modified`);
68
- const list = [...hooks[ev] ?? []];
69
- const mine = `${command} ${ev}`;
70
- if (!list.some((e) => e.hooks?.some((h) => h.command === mine))) {
71
- list.push({ hooks: [{ type: "command", command: mine, timeout: 5 }] });
72
- changed = true;
73
- }
74
- hooks[ev] = list;
75
- }
76
- return { settings: { ...base, hooks }, changed };
77
- }
78
- function mergeClaudeHooks(settings, command = "ledge hook") {
79
- return mergeHooks(settings, CLAUDE_EVENTS, command, "settings.json");
80
- }
81
- function mergeCodexHooks(hooks, command = "ledge hook codex") {
82
- return mergeHooks(hooks, CODEX_EVENTS, command, "hooks.json");
83
- }
84
- var CURSOR_EVENTS = [
85
- "sessionStart",
86
- "beforeSubmitPrompt",
87
- "preToolUse",
88
- "postToolUse",
89
- "postToolUseFailure",
90
- "stop",
91
- "afterAgentResponse",
92
- "sessionEnd"
93
- ];
94
- function mergeCursorHooks(config, command = "ledge hook cursor") {
95
- if (config === void 0) config = {};
96
- if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("hooks.json root is not an object, not modified");
97
- const base = config;
98
- if (base.hooks !== void 0 && (!base.hooks || typeof base.hooks !== "object" || Array.isArray(base.hooks))) throw new Error('hooks.json "hooks" is not an object, not modified');
99
- const hooks = { ...base.hooks ?? {} };
100
- let changed = base.version === void 0;
101
- for (const ev of CURSOR_EVENTS) {
102
- if (hooks[ev] !== void 0 && !Array.isArray(hooks[ev])) throw new Error(`hooks.json hooks.${ev} is not an array, not modified`);
103
- const list = [...hooks[ev] ?? []];
104
- const mine = `${command} ${ev}`;
105
- if (!list.some((e) => e?.command === mine)) {
106
- list.push({ command: mine, timeout: 5 });
107
- changed = true;
108
- }
109
- hooks[ev] = list;
110
- }
111
- return { settings: { version: 1, ...base, hooks }, changed };
112
- }
113
- async function mergeJsonFile(path, merge, name) {
114
- let raw = null;
115
- try {
116
- raw = await readFile2(path, "utf8");
117
- } catch (e) {
118
- if (e.code !== "ENOENT") throw e;
119
- }
120
- let current;
121
- if (raw !== null) {
122
- try {
123
- current = JSON.parse(raw);
124
- } catch {
125
- throw new Error(`${name} is not valid JSON, not modified`);
126
- }
127
- }
128
- const { settings, changed } = merge(current);
129
- if (!changed) return false;
130
- await mkdir(dirname(path), { recursive: true });
131
- const tmp = `${path}.ledge-${process.pid}.tmp`;
132
- await writeFile(tmp, JSON.stringify(settings, null, 2) + "\n");
133
- await rename(tmp, path);
134
- return true;
135
- }
136
- var SERVICE = "ledge-daemon.service";
137
- var execRunner = (cmd, args) => new Promise((resolve) => {
138
- execFile(cmd, args, (err, stdout, stderr) => {
139
- const code = err && typeof err.code === "number" ? err.code : err ? 1 : 0;
140
- resolve({ code, stdout: String(stdout), stderr: String(stderr) });
141
- });
142
- });
143
- function systemdUnit(opts) {
144
- return `[Unit]
145
- Description=Ledge daemon (agent sessions for the Windows overlay)
146
-
147
- [Service]
148
- ExecStart=${opts.node} ${opts.script} daemon
149
- Restart=on-failure
150
- RestartSec=2
151
-
152
- [Install]
153
- WantedBy=default.target
154
- `;
155
- }
156
- async function writeIfChanged(path, content) {
157
- let current = null;
158
- try {
159
- current = await readFile2(path, "utf8");
160
- } catch (e) {
161
- if (e.code !== "ENOENT") throw e;
162
- }
163
- if (current === content) return false;
164
- await mkdir(dirname(path), { recursive: true });
165
- const tmp = `${path}.ledge-${process.pid}.tmp`;
166
- await writeFile(tmp, content);
167
- await rename(tmp, path);
168
- return true;
169
- }
170
- async function stopDetachedDaemon(pidFile) {
171
- let pid;
172
- try {
173
- pid = Number((await readFile2(pidFile, "utf8")).trim());
174
- } catch {
175
- return null;
176
- }
177
- if (!Number.isInteger(pid) || pid <= 0) return null;
178
- try {
179
- process.kill(pid, 0);
180
- } catch {
181
- await rm(pidFile, { force: true });
182
- return null;
183
- }
184
- try {
185
- process.kill(pid, "SIGTERM");
186
- } catch {
187
- return null;
188
- }
189
- const deadline = Date.now() + 3e3;
190
- while (Date.now() < deadline) {
191
- await new Promise((r) => setTimeout(r, 50));
192
- try {
193
- process.kill(pid, 0);
194
- } catch {
195
- return pid;
196
- }
197
- }
198
- return pid;
199
- }
200
- async function installService(opts = {}) {
201
- const out = [];
202
- const run2 = opts.run ?? execRunner;
203
- const unitDir = opts.unitDir ?? join(homedir(), ".config", "systemd", "user");
204
- const node = opts.node ?? await realpath(process.execPath);
205
- const script = opts.script ?? await realpath(process.argv[1]);
206
- const changed = await writeIfChanged(join(unitDir, SERVICE), systemdUnit({ node, script }));
207
- const reload = await run2("systemctl", ["--user", "daemon-reload"]);
208
- if (reload.code !== 0) throw new Error(`systemd user session unavailable (systemctl --user daemon-reload failed: ${reload.stderr.trim() || reload.code}); the daemon will still be started on demand by ledge claude`);
209
- const active = (await run2("systemctl", ["--user", "is-active", SERVICE])).code === 0;
210
- if (active && changed && await (opts.atRisk ?? tmuxServerAtRisk)()) {
211
- out.push(`${SERVICE} unit updated but not restarted: the tmux server of your agents runs inside the service and would stop with it; close the sessions, then run ledge setup again (or systemctl --user restart ${SERVICE})`);
212
- } else if (active && changed) {
213
- const r = await run2("systemctl", ["--user", "restart", SERVICE]);
214
- if (r.code !== 0) throw new Error(`systemctl --user restart ${SERVICE} failed: ${r.stderr.trim()}`);
215
- out.push(`${SERVICE} updated and restarted`);
216
- } else if (active) {
217
- out.push(`${SERVICE} already running`);
218
- } else {
219
- const pid = await stopDetachedDaemon(paths().pid);
220
- if (pid !== null) out.push(`detached daemon (pid ${pid}) stopped, the service takes over`);
221
- const r = await run2("systemctl", ["--user", "enable", "--now", SERVICE]);
222
- if (r.code !== 0) throw new Error(`systemctl --user enable --now ${SERVICE} failed: ${r.stderr.trim()}`);
223
- out.push(`${SERVICE} installed and started (journalctl --user -u ${SERVICE} for its output)`);
224
- }
225
- const user = process.env.USER ?? userInfo().username;
226
- const linger = await run2("loginctl", ["show-user", user, "-p", "Linger"]);
227
- if (linger.code === 0 && /^Linger=yes/m.test(linger.stdout)) return out;
228
- const enable = await run2("loginctl", ["enable-linger"]);
229
- if (enable.code === 0) out.push("linger enabled: the daemon starts with WSL, before any shell is open");
230
- else out.push(`linger not enabled (${enable.stderr.trim() || `exit ${enable.code}`}): the daemon starts with your first WSL shell; run \`sudo loginctl enable-linger ${user}\` to start it at WSL boot`);
231
- return out;
232
- }
233
- async function isDir(path) {
234
- try {
235
- return (await stat(path)).isDirectory();
236
- } catch {
237
- return false;
238
- }
239
- }
240
- var BIN_MARK = "#!/bin/sh\n# Written by `ledge setup`";
241
- var shQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
242
- async function writeBin(opts) {
243
- const st = await lstat(opts.link).catch(() => null);
244
- const was = st?.isSymbolicLink() ? await readlink(opts.link) : null;
245
- if (was !== null && !was.endsWith("/dist/cli/index.js")) return null;
246
- const current = st && was === null && st.isFile() ? await readFile2(opts.link, "utf8").catch(() => "") : "";
247
- if (st && was === null && !current.startsWith(BIN_MARK)) return null;
248
- const script = `${BIN_MARK}: the hooks run \`ledge\` wherever node is not on the PATH.
249
- exec ${shQuote(opts.node)} ${shQuote(opts.target)} "$@"
250
- `;
251
- if (current === script) return null;
252
- await mkdir(dirname(opts.link), { recursive: true });
253
- const tmp = `${opts.link}.tmp`;
254
- await writeFile(tmp, script, { mode: 493 });
255
- await rename(tmp, opts.link);
256
- return `${opts.link} runs ${opts.target} with ${opts.node}${was !== null ? ` (was a link to ${was})` : ""}`;
257
- }
258
- async function runSetup(opts = {}) {
259
- const out = [];
260
- await ensureLedgeDir();
261
- out.push("~/.ledge ready (tmux.conf written if missing)");
262
- const settingsPath = opts.settingsPath ?? join(homedir(), ".claude", "settings.json");
263
- if (await mergeJsonFile(settingsPath, mergeClaudeHooks, "settings.json")) out.push(`Claude Code hooks added in ${settingsPath}`);
264
- else out.push("Claude Code hooks already present");
265
- const codexDir = opts.codexDir ?? process.env.CODEX_HOME ?? join(homedir(), ".codex");
266
- if (!await isDir(codexDir)) out.push(`Codex not found (${codexDir} missing), Codex hooks skipped`);
267
- else if (await mergeJsonFile(join(codexDir, "hooks.json"), mergeCodexHooks, "hooks.json")) {
268
- out.push(`Codex hooks added in ${join(codexDir, "hooks.json")}; Codex asks to review them on its next start: trust them there or from Ledge`);
269
- } else out.push("Codex hooks already present");
270
- const cursorDir = opts.cursorDir ?? join(homedir(), ".cursor");
271
- if (!await isDir(cursorDir)) out.push(`Cursor Agent not found (${cursorDir} missing), Cursor Agent hooks skipped`);
272
- else if (await mergeJsonFile(join(cursorDir, "hooks.json"), mergeCursorHooks, "hooks.json")) out.push(`Cursor Agent hooks added in ${join(cursorDir, "hooks.json")}`);
273
- else out.push("Cursor Agent hooks already present");
274
- if (opts.service ?? true) {
275
- const cli = opts.cli ?? process.argv[1] ?? "";
276
- if (cli.endsWith(".ts")) out.push("systemd service skipped: run setup from the built CLI (pnpm --filter @paradow/ledge build, then ledge setup)");
277
- else {
278
- const script = await realpath(cli);
279
- const line = await writeBin({ link: opts.link ?? join(homedir(), ".local", "bin", "ledge"), node: process.execPath, target: script });
280
- if (line) out.push(line);
281
- out.push(...await installService({ script, unitDir: opts.unitDir, run: opts.run }));
282
- }
283
- }
284
- return out;
285
- }
286
-
287
- export {
288
- CLAUDE_EVENTS,
289
- CODEX_EVENTS,
290
- mergeClaudeHooks,
291
- mergeCodexHooks,
292
- CURSOR_EVENTS,
293
- mergeCursorHooks,
294
- SERVICE,
295
- execRunner,
296
- systemdUnit,
297
- stopDetachedDaemon,
298
- installService,
299
- isDir,
300
- BIN_MARK,
301
- writeBin,
302
- runSetup,
303
- inDaemonService,
304
- tmuxServerAtRisk
305
- };
@@ -1,85 +0,0 @@
1
- import {
2
- CODEX_HOOK_VAR
3
- } from "./chunk-4TCSADFR.js";
4
- import {
5
- connect
6
- } from "./chunk-7NHQGP6J.js";
7
- import {
8
- SESSION_KEY_VAR,
9
- actionById
10
- } from "./chunk-IWY7DSSO.js";
11
-
12
- // src/cli/action.ts
13
- import { randomUUID } from "crypto";
14
- var CLOSE_USAGE = "ledge close <session>\u2026 [--json] fermer proprement des sessions (leur conversation reste reprenable)";
15
- var DAEMON_DOWN = "daemon not running (start a session with `ledge claude`, or run `ledge daemon`)";
16
- var RUN_MS = 6e4;
17
- function actionArgs(id, args) {
18
- const parsed = actionById(id).args.safeParse(args);
19
- return parsed.success ? args : void 0;
20
- }
21
- function callerOf(env) {
22
- const by = env[CODEX_HOOK_VAR] || env.LEDGE_SESSION;
23
- const key = env[SESSION_KEY_VAR];
24
- return by ? { by, ...key ? { key } : {} } : void 0;
25
- }
26
- async function runOnClient(client, action, args, opts = {}) {
27
- const rid = randomUUID();
28
- const by = opts.by;
29
- client.send({ type: "action.run", rid, action, args, ...by ? { by: by.by, ...by.key ? { key: by.key } : {} } : {}, ...opts.confirmed ? { confirmed: true } : {} });
30
- const deadline = Date.now() + (opts.ms ?? RUN_MS);
31
- for (; ; ) {
32
- const { type: _t, rid: got, ...r } = await client.next("action.result", Math.max(0, deadline - Date.now()));
33
- if (got === rid) return r;
34
- }
35
- }
36
- var plain = (s) => s.replace(/\*\*/g, "");
37
- function resultLine(r, json = false) {
38
- const ok = r.reason === void 0;
39
- if (json) return { line: JSON.stringify({ ok, ...r }), code: ok ? 0 : 1 };
40
- if (ok) return { line: `fait : ${plain(r.row?.summary ?? r.action)}`, code: 0 };
41
- if (r.code === "confirm_required") return { line: `${plain(r.row?.summary ?? r.action)} : irr\xE9versible ; relance avec --yes pour confirmer`, code: 1 };
42
- return { line: `${r.code === "failed" ? "\xE9chec" : "refus\xE9"} : ${r.reason}`, code: 1 };
43
- }
44
- function closeArgs(args) {
45
- const json = args.includes("--json");
46
- const sessions = args.filter((a) => a !== "--json");
47
- if (!sessions.length || sessions.some((s) => s.startsWith("--") || !actionArgs("session.close", { session: s }))) return void 0;
48
- return { sessions, ...json ? { json: true } : {} };
49
- }
50
- async function runClose(args, opts = {}) {
51
- const parsed = closeArgs(args);
52
- if (!parsed) return { line: CLOSE_USAGE, code: args.length ? 1 : 0 };
53
- let client;
54
- try {
55
- client = await connect({ port: opts.port, timeoutMs: 1500 });
56
- } catch {
57
- return { line: DAEMON_DOWN, code: 1 };
58
- }
59
- const by = callerOf(opts.env ?? process.env);
60
- const lines = [];
61
- try {
62
- for (const session of parsed.sessions) {
63
- const r = resultLine(await runOnClient(client, "session.close", { session }, { by }), parsed.json);
64
- lines.push(r.line);
65
- if (r.code) return { line: lines.join("\n"), code: 1 };
66
- }
67
- return { line: lines.join("\n"), code: 0 };
68
- } catch (e) {
69
- return { line: [...lines, `pas de r\xE9ponse de Ledge : ${e.message}`].join("\n"), code: 1 };
70
- } finally {
71
- await client.close();
72
- }
73
- }
74
-
75
- export {
76
- CLOSE_USAGE,
77
- DAEMON_DOWN,
78
- RUN_MS,
79
- actionArgs,
80
- callerOf,
81
- runOnClient,
82
- resultLine,
83
- closeArgs,
84
- runClose
85
- };
@@ -1,205 +0,0 @@
1
- import {
2
- exitFile,
3
- spawnSession
4
- } from "./chunk-4TCSADFR.js";
5
- import {
6
- DaemonRefusedError,
7
- connect
8
- } from "./chunk-7NHQGP6J.js";
9
- import {
10
- PROTOCOL_PORT,
11
- namesInUse
12
- } from "./chunk-IWY7DSSO.js";
13
- import {
14
- realTmux
15
- } from "./chunk-T547FLQN.js";
16
- import {
17
- log
18
- } from "./chunk-54KF5RBY.js";
19
- import {
20
- ensureLedgeDir
21
- } from "./chunk-53ISXX2R.js";
22
-
23
- // src/cli/launch.ts
24
- import { spawn } from "child_process";
25
- import { readFile, rm } from "fs/promises";
26
- import { createConnection } from "net";
27
- var TMUX_SOCKET = "ledge";
28
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29
- function parseLaunchArgs(argv) {
30
- const args = [];
31
- let name;
32
- for (let i = 0; i < argv.length; i++) {
33
- const a = argv[i];
34
- if (a === "--name") {
35
- name = argv[++i];
36
- continue;
37
- }
38
- if (a === "--") {
39
- args.push(...argv.slice(i + 1));
40
- break;
41
- }
42
- args.push(a);
43
- }
44
- return { name, args };
45
- }
46
- function portOpen(port) {
47
- return new Promise((res) => {
48
- const s = createConnection({ host: "127.0.0.1", port });
49
- s.once("connect", () => {
50
- s.end();
51
- res(true);
52
- });
53
- s.once("error", () => res(false));
54
- });
55
- }
56
- function spawnDetachedDaemon() {
57
- const child = spawn(process.execPath, [process.argv[1], "daemon"], { detached: true, stdio: "ignore" });
58
- child.unref();
59
- }
60
- var closeQuietly = (c) => Promise.race([c.close(), sleep(500)]);
61
- async function probe(port, timeoutMs) {
62
- let client;
63
- try {
64
- client = await connect({ port, timeoutMs });
65
- } catch (e) {
66
- if (e instanceof DaemonRefusedError) return { client: null, state: "unauthorized" };
67
- if (e.message === "connect timeout") return { client: null, state: "timeout" };
68
- return { client: null, state: await portOpen(port) ? "foreign" : "refused" };
69
- }
70
- try {
71
- await client.next("snapshot", timeoutMs);
72
- return { client };
73
- } catch {
74
- void closeQuietly(client);
75
- return { client: null, state: "foreign" };
76
- }
77
- }
78
- async function ensureDaemon(opts = {}) {
79
- await ensureLedgeDir();
80
- const port = opts.port ?? PROTOCOL_PORT;
81
- const timeoutMs = opts.timeoutMs ?? 2e3;
82
- const first = await probe(port, timeoutMs);
83
- if (first.client) return { client: first.client };
84
- if (first.state === "foreign") {
85
- log(`port ${port} is open but does not speak the Ledge protocol`);
86
- return { client: null, reason: "foreign" };
87
- }
88
- if (first.state === "unauthorized") {
89
- log(`the daemon on port ${port} refused this client's token`);
90
- return { client: null, reason: "unauthorized" };
91
- }
92
- if (first.state === "timeout") {
93
- log(`port ${port} accepted a connection but did not answer within ${timeoutMs} ms`);
94
- return { client: null, reason: "unresponsive" };
95
- }
96
- (opts.spawnDaemon ?? spawnDetachedDaemon)();
97
- for (let i = 0; i < 3; i++) {
98
- await sleep(opts.retryDelayMs ?? 500);
99
- const r = await probe(port, timeoutMs);
100
- if (r.client) return { client: r.client };
101
- }
102
- return { client: null, reason: "unreachable" };
103
- }
104
- var DaemonError = class extends Error {
105
- };
106
- async function createSession(opts) {
107
- const names = [];
108
- const ids = [];
109
- if (opts.client) {
110
- let snap;
111
- try {
112
- opts.client.send({ type: "snapshot.request" });
113
- snap = await opts.client.next("snapshot");
114
- } catch (e) {
115
- throw new DaemonError(`daemon did not answer: ${e.message}`);
116
- }
117
- names.push(...namesInUse(snap.sessions));
118
- for (const s of snap.sessions) ids.push(s.id);
119
- }
120
- const session = await spawnSession({ ...opts, taken: { names, ids } });
121
- try {
122
- opts.client?.send({ type: "session.register", session });
123
- } catch (e) {
124
- log(`register failed for ${session.id}: ${e.message}`);
125
- }
126
- return session;
127
- }
128
- function attachEnv(env) {
129
- const { TMUX: _ignored, ...rest } = env;
130
- return rest;
131
- }
132
- function attachInherit(args, env) {
133
- return new Promise((resolve) => {
134
- const child = spawn("tmux", args, { stdio: "inherit", env });
135
- child.on("error", () => resolve(1));
136
- child.on("exit", (c) => resolve(c ?? 1));
137
- });
138
- }
139
- async function readExitCode(id) {
140
- const file = exitFile(id);
141
- let raw = null;
142
- try {
143
- raw = await readFile(file, "utf8");
144
- } catch {
145
- }
146
- await rm(file, { force: true });
147
- const t = raw?.trim();
148
- return t && /^\d+$/.test(t) ? Number(t) : null;
149
- }
150
- var UNTRACKED = {
151
- foreign: (port) => `ledge: port ${port} is used by another program, session not tracked`,
152
- unauthorized: () => "ledge: the daemon refused this client's token (missing or changed), session not tracked; restart the daemon: systemctl --user restart ledge-daemon",
153
- unresponsive: () => "ledge: daemon not responding, session not tracked",
154
- unreachable: () => "ledge: daemon not running, session not tracked"
155
- };
156
- async function launch(tool, argv, opts = {}) {
157
- await ensureLedgeDir();
158
- const { name, args } = parseLaunchArgs(argv);
159
- const port = opts.port ?? PROTOCOL_PORT;
160
- const socket = opts.tmuxSocket ?? TMUX_SOCKET;
161
- const tmux = realTmux(socket);
162
- const cwd = opts.cwd ?? process.cwd();
163
- const daemon = await ensureDaemon({ port, spawnDaemon: opts.spawnDaemon, retryDelayMs: opts.retryDelayMs });
164
- let client = daemon.client;
165
- if (!daemon.client) console.error(UNTRACKED[daemon.reason](port));
166
- const create = (c) => createSession({ tool, args, command: opts.command, recordExit: true, name, cwd, tmux, client: c });
167
- let session;
168
- try {
169
- session = await create(client);
170
- } catch (e) {
171
- if (!(e instanceof DaemonError)) throw e;
172
- log(e.message);
173
- if (client) void closeQuietly(client);
174
- client = null;
175
- console.error(UNTRACKED.unresponsive(port));
176
- session = await create(null);
177
- }
178
- if (client) await closeQuietly(client);
179
- const tmuxName = session.tmuxSession;
180
- if (process.stdout.isTTY) process.stdout.write(`\x1B]0;${session.title}\x07`);
181
- const attachCode = await (opts.attach ?? attachInherit)(tmux.attachArgs(tmuxName), attachEnv(process.env));
182
- if (await tmux.hasSession(tmuxName)) {
183
- if (attachCode !== 0) console.error(`ledge: could not attach; ${session.name} keeps running in tmux session ${tmuxName} (tmux -L ${socket} attach -t ${tmuxName})`);
184
- return attachCode;
185
- }
186
- const exitCode = await readExitCode(session.id);
187
- if (client) {
188
- try {
189
- const c = await connect({ port, timeoutMs: 800 });
190
- c.send({ type: "session.unregister", id: session.id, exitCode });
191
- await closeQuietly(c);
192
- } catch {
193
- }
194
- }
195
- return exitCode ?? 1;
196
- }
197
-
198
- export {
199
- parseLaunchArgs,
200
- ensureDaemon,
201
- DaemonError,
202
- createSession,
203
- attachEnv,
204
- launch
205
- };