cursor-route 0.1.1

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/src/tmux.ts ADDED
@@ -0,0 +1,123 @@
1
+ import { spawnSync, execSync } from "node:child_process";
2
+ import { config, sessionName } from "./config.ts";
3
+ import { shellQuote } from "./util.ts";
4
+ import { markCompleteInvoker } from "./runtime.ts";
5
+
6
+ export function isTmuxAvailable(): boolean {
7
+ try {
8
+ execSync("command -v tmux", { stdio: "ignore" });
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ export function sessionExists(name: string): boolean {
16
+ try {
17
+ execSync(`tmux has-session -t ${shellQuote(name)} 2>/dev/null`, {
18
+ stdio: "ignore",
19
+ });
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ export function listManagedSessions(): string[] {
27
+ try {
28
+ const out = execSync('tmux list-sessions -F "#{session_name}" 2>/dev/null', {
29
+ encoding: "utf8",
30
+ stdio: ["ignore", "pipe", "ignore"],
31
+ }).trim();
32
+ if (!out) return [];
33
+ return out.split("\n").filter((n) => n.startsWith(`${config.tmuxPrefix}-`));
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ export function capturePane(name: string, lines = 50): string {
40
+ try {
41
+ return execSync(
42
+ `tmux capture-pane -t ${shellQuote(name)} -p -S -${Math.max(1, lines)}`,
43
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] },
44
+ );
45
+ } catch {
46
+ return "";
47
+ }
48
+ }
49
+
50
+ export function sendKeys(name: string, message: string): boolean {
51
+ if (!sessionExists(name)) return false;
52
+ try {
53
+ execSync(`tmux send-keys -t ${shellQuote(name)} -- ${shellQuote(message)}`, {
54
+ stdio: "ignore",
55
+ });
56
+ spawnSync("sleep", ["0.25"]);
57
+ execSync(`tmux send-keys -t ${shellQuote(name)} Enter`, { stdio: "ignore" });
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ export function killSession(name: string): boolean {
65
+ try {
66
+ execSync(`tmux kill-session -t ${shellQuote(name)}`, { stdio: "ignore" });
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ export function attachHint(jobId: string): string {
74
+ return `tmux attach -t ${sessionName(jobId)}`;
75
+ }
76
+
77
+ /**
78
+ * Create a detached tmux session that runs `workerCmd` via `sh -c` under `script`,
79
+ * then marks the job complete and exits the session.
80
+ * Always quote the shell expression so macOS BSD `script` does not split `cd && …`.
81
+ */
82
+ export function createWorkerSession(options: {
83
+ jobId: string;
84
+ cwd: string;
85
+ workerCmd: string;
86
+ logFile: string;
87
+ jobFile: string;
88
+ markCompleteScript: string;
89
+ }): { ok: true; session: string } | { ok: false; error: string } {
90
+ const name = sessionName(options.jobId);
91
+ const isLinux = process.platform === "linux";
92
+ const invoker = markCompleteInvoker(options.markCompleteScript);
93
+
94
+ const completion = [
95
+ `exit_code=$?`,
96
+ `${invoker} ${shellQuote(options.jobFile)} "$exit_code" ${shellQuote(options.logFile)}`,
97
+ `echo ""`,
98
+ `echo "[cursor-route: session complete — closing in 5s]"`,
99
+ `sleep 5`,
100
+ `tmux kill-session -t ${shellQuote(name)} 2>/dev/null || true`,
101
+ ].join("; ");
102
+
103
+ // Always run workerCmd under sh -c so `cd … && …` stays one expression.
104
+ // Linux script: script -q -e -c '<cmd>' <logfile>
105
+ // macOS script: script -q <logfile> <cmd> <args...>
106
+ const wrapped = isLinux
107
+ ? `script -q -e -c ${shellQuote(`/bin/sh -c ${shellQuote(options.workerCmd)}`)} ${shellQuote(options.logFile)}; ${completion}`
108
+ : `script -q ${shellQuote(options.logFile)} /bin/sh -c ${shellQuote(options.workerCmd)}; ${completion}`;
109
+
110
+ const r = spawnSync(
111
+ "tmux",
112
+ ["new-session", "-d", "-s", name, "-c", options.cwd, wrapped],
113
+ { encoding: "utf8", cwd: options.cwd },
114
+ );
115
+
116
+ if (r.status !== 0) {
117
+ return {
118
+ ok: false,
119
+ error: (r.stderr || r.stdout || "tmux new-session failed").toString().trim(),
120
+ };
121
+ }
122
+ return { ok: true, session: name };
123
+ }
package/src/util.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ /** Shell-escape a string for single-quoted POSIX use. */
4
+ export function shellQuote(value: string): string {
5
+ return `'${value.replace(/'/g, `'\\''`)}'`;
6
+ }
7
+
8
+ /** Short job id (8 hex chars). */
9
+ export function newJobId(): string {
10
+ const bytes = crypto.getRandomValues(new Uint8Array(4));
11
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
12
+ }
13
+
14
+ export function commandExists(cmd: string): boolean {
15
+ try {
16
+ const r = spawnSync("sh", ["-c", `command -v ${cmd}`], { encoding: "utf8" });
17
+ return r.status === 0 && Boolean(r.stdout?.trim());
18
+ } catch {
19
+ return false;
20
+ }
21
+ }