@paradow/ledge 0.34.0 → 0.36.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 (37) hide show
  1. package/dist/cli/index.js +40782 -39
  2. package/dist/cli/reduceWorker.js +2216 -0
  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-IWY7DSSO.js +0 -2476
  16. package/dist/cli/chunk-KPDTYX7F.js +0 -60
  17. package/dist/cli/chunk-OLLQBK6N.js +0 -41
  18. package/dist/cli/chunk-RPH773TY.js +0 -6
  19. package/dist/cli/chunk-RX5CJ7YH.js +0 -43
  20. package/dist/cli/chunk-T547FLQN.js +0 -128
  21. package/dist/cli/chunk-U67V476Y.js +0 -35
  22. package/dist/cli/chunk-UJ2WBGLW.js +0 -305
  23. package/dist/cli/chunk-ULOMKEB6.js +0 -85
  24. package/dist/cli/chunk-V4XKKL6K.js +0 -205
  25. package/dist/cli/daemon-RO43S3S5.js +0 -11449
  26. package/dist/cli/decide-KZFYL3HY.js +0 -59
  27. package/dist/cli/handover-BWNMI2RA.js +0 -17
  28. package/dist/cli/hook-ZLWXI5EQ.js +0 -473
  29. package/dist/cli/launch-GCX7TXOH.js +0 -28
  30. package/dist/cli/send-SXZAPVRH.js +0 -19
  31. package/dist/cli/setup-TJ32FMVJ.js +0 -38
  32. package/dist/cli/status-YDVRES4E.js +0 -28
  33. package/dist/cli/task-TUSOVJK7.js +0 -19
  34. package/dist/cli/tui-TCVIRZCQ.js +0 -19348
  35. package/dist/cli/uninstall-QO7YMSLA.js +0 -162
  36. package/dist/cli/version-BAYSYWFP.js +0 -7
  37. package/dist/cli/who-6AZYSP7T.js +0 -12
@@ -1,60 +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
- BRIEF_MAX
9
- } from "./chunk-IWY7DSSO.js";
10
-
11
- // src/cli/handover.ts
12
- import { randomUUID } from "crypto";
13
- var HANDOVER_USAGE = `ledge handover "<bilan>" ton bilan de rel\xE8ve, quand Ledge te le demande (fait, en cours, suite, pi\xE8ges, fichiers cl\xE9s)
14
- ledge handover show relire le bilan de la session dont tu as pris la rel\xE8ve`;
15
- var HANDOVER_MS = 4e3;
16
- function handoverArgs(args) {
17
- if (args.length === 1 && args[0] === "show") return { op: "show" };
18
- const text = args.join(" ").trim();
19
- if (!args.length || !text || text.length > BRIEF_MAX) return void 0;
20
- return { op: "brief", text };
21
- }
22
- async function handoverOnClient(client, args, by, ms) {
23
- const op = handoverArgs(args);
24
- if (!op) return { line: HANDOVER_USAGE, code: 1 };
25
- try {
26
- const rid = randomUUID();
27
- client.send(op.op === "show" ? { type: "handover.show", rid, by } : { type: "handover.brief", rid, by, text: op.text });
28
- const r = await client.next("handover.result", ms);
29
- if (r.reason !== void 0) return { line: `refus\xE9 : ${r.reason}`, code: 1 };
30
- return op.op === "show" ? { line: r.text ?? "", code: 0 } : { line: "bilan re\xE7u : Ledge lance la session qui prend ta rel\xE8ve et lui passe tes t\xE2ches. Arr\xEAte-toi l\xE0.", code: 0 };
31
- } catch (e) {
32
- return { line: `pas de r\xE9ponse de Ledge : ${e.message}`, code: 1 };
33
- }
34
- }
35
- async function runHandover(args, opts = {}) {
36
- if (!handoverArgs(args)) return { line: HANDOVER_USAGE, code: args.length ? 1 : 0 };
37
- const env = opts.env ?? process.env;
38
- const by = env[CODEX_HOOK_VAR] || env.LEDGE_SESSION;
39
- if (!by) return { line: "ledge handover ne marche que dans une session lanc\xE9e par Ledge", code: 1 };
40
- const deadline = Date.now() + (opts.timeoutMs ?? HANDOVER_MS);
41
- const left = () => Math.max(0, deadline - Date.now());
42
- let client;
43
- try {
44
- client = await connect({ port: opts.port, timeoutMs: left() });
45
- } catch {
46
- return { line: "daemon not running (start a session with `ledge claude`, or run `ledge daemon`)", code: 1 };
47
- }
48
- try {
49
- return await handoverOnClient(client, args, by, left());
50
- } finally {
51
- await client.close();
52
- }
53
- }
54
-
55
- export {
56
- HANDOVER_USAGE,
57
- handoverArgs,
58
- handoverOnClient,
59
- runHandover
60
- };
@@ -1,41 +0,0 @@
1
- // src/transport/frames.ts
2
- var MAX_FRAME = 4 * 1024 * 1024;
3
- function readFrames(input, frame, fail) {
4
- let buffer = "", stopped = false;
5
- input.setEncoding("utf8");
6
- const data = (chunk) => {
7
- if (stopped) return;
8
- buffer += chunk;
9
- try {
10
- let i;
11
- while ((i = buffer.indexOf("\n")) >= 0) {
12
- const line = buffer.slice(0, i);
13
- buffer = buffer.slice(i + 1);
14
- if (Buffer.byteLength(line) > MAX_FRAME) throw new Error("Ledge frame too large");
15
- if (line.trim()) frame(JSON.parse(line));
16
- }
17
- if (Buffer.byteLength(buffer) > MAX_FRAME) throw new Error("Ledge frame too large");
18
- } catch (e) {
19
- stopped = true;
20
- fail(e instanceof Error ? e : new Error(String(e)));
21
- }
22
- };
23
- const end = () => {
24
- if (!stopped && buffer.trim()) {
25
- stopped = true;
26
- fail(new Error("Incomplete Ledge frame"));
27
- }
28
- };
29
- input.on("data", data);
30
- input.on("end", end);
31
- return () => {
32
- stopped = true;
33
- input.off("data", data);
34
- input.off("end", end);
35
- };
36
- }
37
-
38
- export {
39
- MAX_FRAME,
40
- readFrames
41
- };
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var LEDGE_VERSION = true ? "0.34.0" : "0.0.0-dev";
3
-
4
- export {
5
- LEDGE_VERSION
6
- };
@@ -1,43 +0,0 @@
1
- import {
2
- connect
3
- } from "./chunk-7NHQGP6J.js";
4
- import {
5
- isArchived
6
- } from "./chunk-IWY7DSSO.js";
7
-
8
- // src/cli/who.ts
9
- import { basename } from "path";
10
- function formatWho(sessions, self) {
11
- const live = sessions.filter((s) => s.state !== "exited" && !isArchived(s));
12
- if (live.length === 0) return "aucune autre session";
13
- const rows = live.map((s) => [
14
- s.id === self ? "*" : " ",
15
- s.name,
16
- s.tool,
17
- s.state,
18
- `${s.repo ?? s.project}${s.worktree ? ` (${basename(s.cwd)})` : ""}`,
19
- s.branch ?? "-",
20
- `${s.topic ?? "-"}${s.source === "hook_only" ? " \xB7 lecture seule" : ""}`
21
- ]);
22
- const head = [" ", "NOM", "OUTIL", "\xC9TAT", "PROJET", "BRANCHE", "SUJET"];
23
- const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
24
- return [head, ...rows].map((r) => r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" ")).join("\n");
25
- }
26
- async function printWho(opts = {}) {
27
- let client;
28
- try {
29
- client = await connect({ port: opts.port, timeoutMs: 800 });
30
- } catch {
31
- return "daemon not running (start a session with `ledge claude`, or run `ledge daemon`)";
32
- }
33
- try {
34
- return formatWho((await client.next("snapshot")).sessions, opts.self ?? process.env.LEDGE_SESSION);
35
- } finally {
36
- await client.close();
37
- }
38
- }
39
-
40
- export {
41
- formatWho,
42
- printWho
43
- };
@@ -1,128 +0,0 @@
1
- import {
2
- log
3
- } from "./chunk-54KF5RBY.js";
4
- import {
5
- paths
6
- } from "./chunk-53ISXX2R.js";
7
-
8
- // src/tmux.ts
9
- import { execFile } from "child_process";
10
- import { existsSync } from "fs";
11
- import { join } from "path";
12
- function runFile(file, args, env, opts) {
13
- return new Promise((resolve) => {
14
- execFile(file, args, { maxBuffer: 4 * 1024 * 1024, env, ...opts?.timeoutMs ? { timeout: opts.timeoutMs, killSignal: "SIGTERM" } : {} }, (err, stdout, stderr) => {
15
- const code = err && opts?.timeoutMs && err.killed ? 124 : err && typeof err.code === "number" ? err.code : err ? 1 : 0;
16
- resolve({ code, stdout: String(stdout), stderr: String(stderr) });
17
- });
18
- });
19
- }
20
- var run = (args, env) => runFile("tmux", args, env);
21
- function systemdAvailable(env = process.env, exists = existsSync) {
22
- return !!env.XDG_RUNTIME_DIR && exists(join(env.XDG_RUNTIME_DIR, "systemd", "private"));
23
- }
24
- function newSessionCommand(tmuxArgs, scoped, unit) {
25
- if (!scoped) return { file: "tmux", args: tmuxArgs };
26
- return { file: "systemd-run", args: ["--user", "--scope", "--quiet", "--collect", `--unit=${unit}`, "--", "tmux", ...tmuxArgs] };
27
- }
28
- function realTmux(socket = "ledge", opts = {}) {
29
- const scope = opts.scope ?? (async () => systemdAvailable());
30
- const base = () => socket === null ? [] : ["-L", socket, "-f", paths().tmuxConf];
31
- const t = (args, env) => run([...base(), ...args], env);
32
- return {
33
- async newSession({ name, cwd, env, unset = [], command }) {
34
- const envArgs = Object.entries(env).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
35
- if (unset.length) command = ["env", ...unset.flatMap((v) => ["-u", v]), ...command];
36
- const clientEnv = env.PATH === void 0 ? void 0 : { ...process.env, PATH: env.PATH };
37
- const args = [...base(), "new-session", "-d", "-s", name, "-c", cwd, "-x", "200", "-y", "50", ...envArgs, "--", ...command];
38
- const serverUp = (await t(["list-sessions"])).code === 0;
39
- const scoped = !serverUp && await scope();
40
- const { file, args: argv } = newSessionCommand(args, scoped, `ledge-tmux-${Date.now()}`);
41
- let r = await runFile(file, argv, clientEnv);
42
- if (r.code !== 0 && scoped) {
43
- log(`tmux: scoped start failed (${r.stderr.trim()}), starting unscoped`);
44
- r = await runFile("tmux", args, clientEnv);
45
- }
46
- if (r.code !== 0) throw new Error(`tmux new-session failed: ${r.stderr.trim()}`);
47
- },
48
- async sendText(name, text) {
49
- if (/[\r\n]/.test(text)) throw new Error("sendText: text must not contain newlines; flatten it first");
50
- const arg = text.endsWith(";") ? `${text.slice(0, -1)}\\;` : text;
51
- const r = await t(["send-keys", "-t", `=${name}:`, "-l", "--", arg]);
52
- if (r.code !== 0) throw new Error(`tmux send-keys failed: ${r.stderr.trim()}`);
53
- },
54
- async sendKeys(name, keys) {
55
- const r = await t(["send-keys", "-t", `=${name}:`, ...keys]);
56
- if (r.code !== 0) throw new Error(`tmux send-keys failed: ${r.stderr.trim()}`);
57
- },
58
- async capture(name, lines) {
59
- const r = await t(["capture-pane", "-p", "-J", "-t", `=${name}:`, "-S", String(-lines)]);
60
- if (r.code !== 0) return null;
61
- return r.stdout.replace(/\s+$/g, "").split("\n").slice(-lines).join("\n");
62
- },
63
- async width(name) {
64
- const r = await t(["display-message", "-p", "-t", `=${name}:`, "#{pane_width}"]);
65
- const n = Number(r.stdout.trim());
66
- return r.code === 0 && Number.isInteger(n) && n > 0 ? n : null;
67
- },
68
- async hasSession(name) {
69
- return (await t(["has-session", "-t", `=${name}`])).code === 0;
70
- },
71
- async paneCwd(name) {
72
- const r = await t(["display-message", "-p", "-t", `=${name}:`, "#{pane_current_path}"]);
73
- const p = r.stdout.trim();
74
- return r.code === 0 && p.startsWith("/") ? p : null;
75
- },
76
- async listSessions() {
77
- const r = await t(["list-sessions", "-F", "#{session_name}"]);
78
- if (r.code === 0) return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
79
- if (/no server running|error connecting to/i.test(r.stderr)) return [];
80
- throw new Error(`tmux list-sessions failed: ${r.stderr.trim()}`);
81
- },
82
- async setOption(name, option, value) {
83
- const r = await t(["set-option", "-t", `=${name}:`, option, value]);
84
- if (r.code !== 0) throw new Error(`tmux set-option failed: ${r.stderr.trim()}`);
85
- },
86
- async killSession(name) {
87
- await t(["kill-session", "-t", `=${name}`]);
88
- },
89
- attachArgs(name, opts2) {
90
- return [...base(), "attach-session", ...opts2?.readOnly ? ["-r"] : [], "-t", `=${name}`];
91
- }
92
- };
93
- }
94
- var isSeatSession = (name) => name.includes("@");
95
- function routedTmux(own, seats) {
96
- const of = (name) => isSeatSession(name) ? seats : own;
97
- return {
98
- newSession: (o) => own.newSession(o),
99
- sendText: (n, text) => of(n).sendText(n, text),
100
- sendKeys: (n, keys) => of(n).sendKeys(n, keys),
101
- capture: (n, lines) => of(n).capture(n, lines),
102
- width: async (n) => {
103
- const t = of(n);
104
- return t.width ? t.width(n) : null;
105
- },
106
- paneCwd: async (n) => {
107
- const t = of(n);
108
- return t.paneCwd ? t.paneCwd(n) : null;
109
- },
110
- hasSession: (n) => of(n).hasSession(n),
111
- async listSessions() {
112
- const mine = await own.listSessions();
113
- const theirs = await seats.listSessions().catch(() => []);
114
- return [...mine, ...theirs.filter(isSeatSession)];
115
- },
116
- killSession: (n) => of(n).killSession(n),
117
- setOption: (n, option, value) => of(n).setOption(n, option, value),
118
- attachArgs: (n, opts) => of(n).attachArgs(n, opts)
119
- };
120
- }
121
-
122
- export {
123
- runFile,
124
- run,
125
- realTmux,
126
- isSeatSession,
127
- routedTmux
128
- };
@@ -1,35 +0,0 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __commonJS = (cb, mod) => function __require() {
8
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
- };
10
- var __export = (target, all) => {
11
- for (var name in all)
12
- __defProp(target, name, { get: all[name], enumerable: true });
13
- };
14
- var __copyProps = (to, from, except, desc) => {
15
- if (from && typeof from === "object" || typeof from === "function") {
16
- for (let key of __getOwnPropNames(from))
17
- if (!__hasOwnProp.call(to, key) && key !== except)
18
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
- }
20
- return to;
21
- };
22
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
23
- // If the importer is in node compatibility mode or this is not an ESM
24
- // file that has been converted to a CommonJS file using a Babel-
25
- // compatible transform (i.e. "__esModule" has not been set), then set
26
- // "default" to the CommonJS "module.exports" for node compatibility.
27
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
- mod
29
- ));
30
-
31
- export {
32
- __commonJS,
33
- __export,
34
- __toESM
35
- };
@@ -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
- };