tmux-ide 1.0.0 → 1.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/README.md +84 -35
- package/bin/cli.js +127 -85
- package/package.json +26 -5
- package/scripts/postinstall.js +46 -7
- package/skill/SKILL.md +28 -12
- package/src/attach.js +7 -10
- package/src/config.js +94 -7
- package/src/detect.js +77 -23
- package/src/doctor.js +23 -14
- package/src/init.js +16 -18
- package/src/inspect.js +127 -0
- package/src/launch.js +171 -177
- package/src/lib/launch-plan.js +83 -0
- package/src/lib/output.js +21 -4
- package/src/lib/tmux.js +200 -0
- package/src/ls.js +2 -4
- package/src/restart.js +5 -12
- package/src/status.js +4 -22
- package/src/stop.js +10 -9
- package/src/validate.js +0 -16
- package/src/detect.test.js +0 -115
- package/src/integration.test.js +0 -94
- package/src/lib/dot-path.test.js +0 -67
- package/src/lib/sizes.test.js +0 -74
- package/src/validate.test.js +0 -141
package/src/lib/tmux.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const SESSION_NOT_FOUND_PATTERNS = ["can't find session", "can't find window", "unknown target"];
|
|
4
|
+
|
|
5
|
+
const TMUX_UNAVAILABLE_PATTERNS = [
|
|
6
|
+
"failed to connect to server",
|
|
7
|
+
"no server running",
|
|
8
|
+
"error connecting to",
|
|
9
|
+
"connection refused",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export class TmuxError extends Error {
|
|
13
|
+
constructor(message, code, cause) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "TmuxError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.cause = cause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getSessionState(session) {
|
|
22
|
+
try {
|
|
23
|
+
runTmux(["has-session", "-t", session]);
|
|
24
|
+
return { running: true, reason: null };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof TmuxError) {
|
|
27
|
+
if (error.code === "SESSION_NOT_FOUND") {
|
|
28
|
+
return { running: false, reason: "SESSION_NOT_FOUND" };
|
|
29
|
+
}
|
|
30
|
+
if (error.code === "TMUX_UNAVAILABLE") {
|
|
31
|
+
return { running: false, reason: "TMUX_UNAVAILABLE" };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function attachSession(session) {
|
|
39
|
+
runTmux(["attach", "-t", session], { stdio: "inherit" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hasSession(session) {
|
|
43
|
+
try {
|
|
44
|
+
runTmux(["has-session", "-t", session]);
|
|
45
|
+
return true;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof TmuxError && error.code === "SESSION_NOT_FOUND") {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function killSession(session) {
|
|
55
|
+
try {
|
|
56
|
+
runTmux(["kill-session", "-t", session]);
|
|
57
|
+
return { stopped: true, reason: null };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof TmuxError) {
|
|
60
|
+
if (error.code === "SESSION_NOT_FOUND") {
|
|
61
|
+
return { stopped: false, reason: "SESSION_NOT_FOUND" };
|
|
62
|
+
}
|
|
63
|
+
if (error.code === "TMUX_UNAVAILABLE") {
|
|
64
|
+
return { stopped: false, reason: "TMUX_UNAVAILABLE" };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function listPanes(session) {
|
|
72
|
+
const raw = runTmux(
|
|
73
|
+
[
|
|
74
|
+
"list-panes",
|
|
75
|
+
"-t",
|
|
76
|
+
session,
|
|
77
|
+
"-F",
|
|
78
|
+
"#{pane_index}|#{pane_title}|#{pane_width}|#{pane_height}|#{pane_active}",
|
|
79
|
+
],
|
|
80
|
+
{ encoding: "utf-8" },
|
|
81
|
+
).trim();
|
|
82
|
+
|
|
83
|
+
if (!raw) return [];
|
|
84
|
+
|
|
85
|
+
return raw.split("\n").map((line) => {
|
|
86
|
+
const [index, title, width, height, active] = line.split("|");
|
|
87
|
+
return {
|
|
88
|
+
index: Number.parseInt(index, 10),
|
|
89
|
+
title,
|
|
90
|
+
width: Number.parseInt(width, 10),
|
|
91
|
+
height: Number.parseInt(height, 10),
|
|
92
|
+
active: active === "1",
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createDetachedSession(session, cwd, { cols, lines } = {}) {
|
|
98
|
+
return runTmux(
|
|
99
|
+
[
|
|
100
|
+
"new-session",
|
|
101
|
+
"-d",
|
|
102
|
+
"-P",
|
|
103
|
+
"-F",
|
|
104
|
+
"#{pane_id}",
|
|
105
|
+
"-s",
|
|
106
|
+
session,
|
|
107
|
+
"-c",
|
|
108
|
+
cwd,
|
|
109
|
+
"-x",
|
|
110
|
+
String(cols ?? 200),
|
|
111
|
+
"-y",
|
|
112
|
+
String(lines ?? 50),
|
|
113
|
+
],
|
|
114
|
+
{ encoding: "utf-8" },
|
|
115
|
+
).trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function setSessionEnvironment(session, key, value) {
|
|
119
|
+
runTmux(["set-environment", "-t", session, key, String(value)]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function splitPane(targetPane, direction, cwd, percent) {
|
|
123
|
+
return runTmux(
|
|
124
|
+
[
|
|
125
|
+
"split-window",
|
|
126
|
+
"-P",
|
|
127
|
+
"-F",
|
|
128
|
+
"#{pane_id}",
|
|
129
|
+
"-t",
|
|
130
|
+
targetPane,
|
|
131
|
+
direction === "vertical" ? "-v" : "-h",
|
|
132
|
+
"-c",
|
|
133
|
+
cwd,
|
|
134
|
+
"-p",
|
|
135
|
+
String(percent),
|
|
136
|
+
],
|
|
137
|
+
{ encoding: "utf-8" },
|
|
138
|
+
).trim();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function sendLiteral(targetPane, text) {
|
|
142
|
+
runTmux(["send-keys", "-t", targetPane, "-l", "--", text], { stdio: "inherit" });
|
|
143
|
+
runTmux(["send-keys", "-t", targetPane, "Enter"], { stdio: "inherit" });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getPaneCurrentCommand(targetPane) {
|
|
147
|
+
return runTmux(["display-message", "-p", "-t", targetPane, "#{pane_current_command}"], {
|
|
148
|
+
encoding: "utf-8",
|
|
149
|
+
}).trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function selectPane(targetPane) {
|
|
153
|
+
runTmux(["select-pane", "-t", targetPane], { stdio: "inherit" });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function setPaneTitle(targetPane, title) {
|
|
157
|
+
runTmux(["select-pane", "-t", targetPane, "-T", title], { stdio: "inherit" });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function runSessionCommand(args) {
|
|
161
|
+
runTmux(args, { stdio: "inherit" });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function runTmux(args, options = {}) {
|
|
165
|
+
const execOptions = {
|
|
166
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
167
|
+
...options,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
return execFileSync("tmux", args, execOptions);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw classifyTmuxError(error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function classifyTmuxError(error) {
|
|
178
|
+
const detail = getErrorDetail(error).toLowerCase();
|
|
179
|
+
|
|
180
|
+
if (SESSION_NOT_FOUND_PATTERNS.some((pattern) => detail.includes(pattern))) {
|
|
181
|
+
return new TmuxError("tmux session was not found", "SESSION_NOT_FOUND", error);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (TMUX_UNAVAILABLE_PATTERNS.some((pattern) => detail.includes(pattern))) {
|
|
185
|
+
return new TmuxError(
|
|
186
|
+
"tmux is unavailable or its socket is inaccessible",
|
|
187
|
+
"TMUX_UNAVAILABLE",
|
|
188
|
+
error,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return new TmuxError("tmux command failed", "TMUX_ERROR", error);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getErrorDetail(error) {
|
|
196
|
+
const stderr = error?.stderr;
|
|
197
|
+
if (typeof stderr === "string" && stderr.length > 0) return stderr;
|
|
198
|
+
if (Buffer.isBuffer(stderr) && stderr.length > 0) return stderr.toString("utf-8");
|
|
199
|
+
return error?.message ?? "";
|
|
200
|
+
}
|
package/src/ls.js
CHANGED
|
@@ -5,7 +5,7 @@ export async function ls({ json } = {}) {
|
|
|
5
5
|
try {
|
|
6
6
|
raw = execSync(
|
|
7
7
|
'tmux list-sessions -F "#{session_name}|#{session_created}|#{session_attached}"',
|
|
8
|
-
{ encoding: "utf-8" }
|
|
8
|
+
{ encoding: "utf-8" },
|
|
9
9
|
).trim();
|
|
10
10
|
} catch {
|
|
11
11
|
if (json) {
|
|
@@ -35,8 +35,6 @@ export async function ls({ json } = {}) {
|
|
|
35
35
|
console.log("─".repeat(54));
|
|
36
36
|
for (const s of sessions) {
|
|
37
37
|
const date = new Date(s.created).toLocaleString();
|
|
38
|
-
console.log(
|
|
39
|
-
s.name.padEnd(24) + date.padEnd(22) + (s.attached ? "yes" : "no")
|
|
40
|
-
);
|
|
38
|
+
console.log(s.name.padEnd(24) + date.padEnd(22) + (s.attached ? "yes" : "no"));
|
|
41
39
|
}
|
|
42
40
|
}
|
package/src/restart.js
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import { getSessionName } from "./lib/yaml-io.js";
|
|
4
3
|
import { launch } from "./launch.js";
|
|
4
|
+
import { killSession } from "./lib/tmux.js";
|
|
5
5
|
|
|
6
|
-
export async function restart(targetDir) {
|
|
6
|
+
export async function restart(targetDir, { json, attach } = {}) {
|
|
7
7
|
const dir = resolve(targetDir ?? ".");
|
|
8
8
|
const session = getSessionName(dir);
|
|
9
|
+
const result = killSession(session);
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
execSync(`tmux kill-session -t "${session}"`, { stdio: "ignore" });
|
|
13
|
-
wasRunning = true;
|
|
14
|
-
} catch {
|
|
15
|
-
// Session wasn't running — that's fine
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (wasRunning) {
|
|
11
|
+
if (result.stopped) {
|
|
19
12
|
console.log(`Stopped session "${session}"`);
|
|
20
13
|
}
|
|
21
14
|
|
|
22
|
-
await launch(dir);
|
|
15
|
+
await launch(dir, { json, attach });
|
|
23
16
|
}
|
package/src/status.js
CHANGED
|
@@ -1,36 +1,18 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { execSync } from "node:child_process";
|
|
4
3
|
import { getSessionName } from "./lib/yaml-io.js";
|
|
4
|
+
import { getSessionState, listPanes } from "./lib/tmux.js";
|
|
5
5
|
|
|
6
6
|
export async function status(targetDir, { json } = {}) {
|
|
7
7
|
const dir = resolve(targetDir ?? ".");
|
|
8
8
|
const session = getSessionName(dir);
|
|
9
9
|
const configExists = existsSync(resolve(dir, "ide.yml"));
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const state = getSessionState(session);
|
|
12
|
+
const running = state.running;
|
|
12
13
|
let panes = [];
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
execSync(`tmux has-session -t "${session}"`, { stdio: "ignore" });
|
|
16
|
-
running = true;
|
|
17
|
-
|
|
18
|
-
const raw = execSync(
|
|
19
|
-
`tmux list-panes -t "${session}" -F "#{pane_index}|#{pane_title}|#{pane_width}|#{pane_height}|#{pane_active}"`,
|
|
20
|
-
{ encoding: "utf-8" }
|
|
21
|
-
).trim();
|
|
22
|
-
|
|
23
|
-
panes = raw.split("\n").map((line) => {
|
|
24
|
-
const [index, title, width, height, active] = line.split("|");
|
|
25
|
-
return {
|
|
26
|
-
index: parseInt(index),
|
|
27
|
-
title,
|
|
28
|
-
width: parseInt(width),
|
|
29
|
-
height: parseInt(height),
|
|
30
|
-
active: active === "1",
|
|
31
|
-
};
|
|
32
|
-
});
|
|
33
|
-
} catch {}
|
|
15
|
+
if (running) panes = listPanes(session);
|
|
34
16
|
|
|
35
17
|
const data = { session, running, configExists, panes };
|
|
36
18
|
|
package/src/stop.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import { getSessionName } from "./lib/yaml-io.js";
|
|
3
|
+
import { outputError } from "./lib/output.js";
|
|
4
|
+
import { killSession } from "./lib/tmux.js";
|
|
4
5
|
|
|
5
6
|
export async function stop(targetDir, { json } = {}) {
|
|
6
7
|
const dir = resolve(targetDir ?? ".");
|
|
7
8
|
const session = getSessionName(dir);
|
|
9
|
+
const result = killSession(session);
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
execSync(`tmux kill-session -t "${session}"`, { stdio: "inherit" });
|
|
11
|
+
if (result.stopped) {
|
|
11
12
|
if (json) {
|
|
12
13
|
console.log(JSON.stringify({ stopped: session }));
|
|
13
14
|
} else {
|
|
14
15
|
console.log(`Stopped session "${session}"`);
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
-
if (json) {
|
|
18
|
-
console.error(JSON.stringify({ error: `No active session "${session}" found`, code: "NOT_RUNNING" }));
|
|
19
|
-
} else {
|
|
20
|
-
console.error(`No active session "${session}" found`);
|
|
21
|
-
}
|
|
17
|
+
return;
|
|
22
18
|
}
|
|
19
|
+
|
|
20
|
+
outputError(`No active session "${session}" found`, "NOT_RUNNING", {
|
|
21
|
+
json,
|
|
22
|
+
exitCode: 1,
|
|
23
|
+
});
|
|
23
24
|
}
|
package/src/validate.js
CHANGED
|
@@ -94,22 +94,6 @@ export function validateConfig(config) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// Cross-validate team roles
|
|
98
|
-
if (config.team && Array.isArray(config.rows)) {
|
|
99
|
-
let leadCount = 0;
|
|
100
|
-
for (const row of config.rows) {
|
|
101
|
-
if (!Array.isArray(row.panes)) continue;
|
|
102
|
-
for (const pane of row.panes) {
|
|
103
|
-
if (pane.role === "lead") leadCount++;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (leadCount === 0) {
|
|
107
|
-
errors.push("team config requires exactly one pane with role: lead");
|
|
108
|
-
} else if (leadCount > 1) {
|
|
109
|
-
errors.push("team config allows only one pane with role: lead");
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
97
|
if (config.theme !== undefined) {
|
|
114
98
|
if (config.theme == null || typeof config.theme !== "object" || Array.isArray(config.theme)) {
|
|
115
99
|
errors.push("'theme' must be an object");
|
package/src/detect.test.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { suggestConfig } from "./detect.js";
|
|
4
|
-
|
|
5
|
-
describe("suggestConfig", () => {
|
|
6
|
-
it("creates Next.js config with pnpm", () => {
|
|
7
|
-
const config = suggestConfig("/project", {
|
|
8
|
-
packageManager: "pnpm",
|
|
9
|
-
frameworks: ["next"],
|
|
10
|
-
devCommand: "pnpm dev",
|
|
11
|
-
language: "javascript",
|
|
12
|
-
});
|
|
13
|
-
assert.strictEqual(config.rows[0].size, "70%");
|
|
14
|
-
assert.strictEqual(config.rows[0].panes.length, 2);
|
|
15
|
-
const bottom = config.rows[1].panes;
|
|
16
|
-
assert.strictEqual(bottom[0].title, "Next.js");
|
|
17
|
-
assert.strictEqual(bottom[0].command, "pnpm dev");
|
|
18
|
-
assert.strictEqual(bottom[bottom.length - 1].title, "Shell");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("creates Next.js + Convex config with 3 Claude panes", () => {
|
|
22
|
-
const config = suggestConfig("/project", {
|
|
23
|
-
packageManager: "pnpm",
|
|
24
|
-
frameworks: ["next", "convex"],
|
|
25
|
-
devCommand: "pnpm dev",
|
|
26
|
-
language: "javascript",
|
|
27
|
-
});
|
|
28
|
-
assert.strictEqual(config.rows[0].panes.length, 3);
|
|
29
|
-
const bottom = config.rows[1].panes;
|
|
30
|
-
const titles = bottom.map((p) => p.title);
|
|
31
|
-
assert.ok(titles.includes("Next.js"));
|
|
32
|
-
assert.ok(titles.includes("Convex"));
|
|
33
|
-
assert.ok(titles.includes("Shell"));
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("creates Go config", () => {
|
|
37
|
-
const config = suggestConfig("/project", {
|
|
38
|
-
packageManager: null,
|
|
39
|
-
frameworks: ["go"],
|
|
40
|
-
devCommand: null,
|
|
41
|
-
language: "go",
|
|
42
|
-
});
|
|
43
|
-
const bottom = config.rows[1].panes;
|
|
44
|
-
assert.strictEqual(bottom[0].title, "Go");
|
|
45
|
-
assert.strictEqual(bottom[0].command, "go run .");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("creates Cargo config", () => {
|
|
49
|
-
const config = suggestConfig("/project", {
|
|
50
|
-
packageManager: null,
|
|
51
|
-
frameworks: ["cargo"],
|
|
52
|
-
devCommand: null,
|
|
53
|
-
language: "rust",
|
|
54
|
-
});
|
|
55
|
-
const bottom = config.rows[1].panes;
|
|
56
|
-
assert.strictEqual(bottom[0].title, "Cargo");
|
|
57
|
-
assert.strictEqual(bottom[0].command, "cargo watch -x run");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("falls back to dev command when no framework detected", () => {
|
|
61
|
-
const config = suggestConfig("/project", {
|
|
62
|
-
packageManager: "npm",
|
|
63
|
-
frameworks: [],
|
|
64
|
-
devCommand: "npm run dev",
|
|
65
|
-
language: "javascript",
|
|
66
|
-
});
|
|
67
|
-
const bottom = config.rows[1].panes;
|
|
68
|
-
assert.strictEqual(bottom[0].title, "Dev Server");
|
|
69
|
-
assert.strictEqual(bottom[0].command, "npm run dev");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("creates minimal config with just shell when nothing detected", () => {
|
|
73
|
-
const config = suggestConfig("/project", {
|
|
74
|
-
packageManager: null,
|
|
75
|
-
frameworks: [],
|
|
76
|
-
devCommand: null,
|
|
77
|
-
language: null,
|
|
78
|
-
});
|
|
79
|
-
const bottom = config.rows[1].panes;
|
|
80
|
-
assert.strictEqual(bottom.length, 1);
|
|
81
|
-
assert.strictEqual(bottom[0].title, "Shell");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("uses npm run for npm package manager", () => {
|
|
85
|
-
const config = suggestConfig("/project", {
|
|
86
|
-
packageManager: "npm",
|
|
87
|
-
frameworks: ["next"],
|
|
88
|
-
devCommand: "npm run dev",
|
|
89
|
-
language: "javascript",
|
|
90
|
-
});
|
|
91
|
-
const bottom = config.rows[1].panes;
|
|
92
|
-
assert.strictEqual(bottom[0].command, "npm run dev");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("uses bun for bun package manager", () => {
|
|
96
|
-
const config = suggestConfig("/project", {
|
|
97
|
-
packageManager: "bun",
|
|
98
|
-
frameworks: ["vite"],
|
|
99
|
-
devCommand: "bun dev",
|
|
100
|
-
language: "javascript",
|
|
101
|
-
});
|
|
102
|
-
const bottom = config.rows[1].panes;
|
|
103
|
-
assert.strictEqual(bottom[0].command, "bun dev");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("uses project directory basename as session name", () => {
|
|
107
|
-
const config = suggestConfig("/home/user/my-app", {
|
|
108
|
-
packageManager: null,
|
|
109
|
-
frameworks: [],
|
|
110
|
-
devCommand: null,
|
|
111
|
-
language: null,
|
|
112
|
-
});
|
|
113
|
-
assert.strictEqual(config.name, "my-app");
|
|
114
|
-
});
|
|
115
|
-
});
|
package/src/integration.test.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { execSync, execFileSync } from "node:child_process";
|
|
4
|
-
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
|
|
8
|
-
// Skip entire suite if tmux is not available
|
|
9
|
-
let tmuxAvailable = false;
|
|
10
|
-
try {
|
|
11
|
-
execSync("tmux -V", { stdio: "ignore" });
|
|
12
|
-
tmuxAvailable = true;
|
|
13
|
-
} catch {}
|
|
14
|
-
|
|
15
|
-
describe("integration", { skip: !tmuxAvailable && "tmux not available" }, () => {
|
|
16
|
-
let tmpDir;
|
|
17
|
-
const session = "tmux-ide-test-integration";
|
|
18
|
-
const cli = join(import.meta.dirname, "..", "bin", "cli.js");
|
|
19
|
-
|
|
20
|
-
function run(args) {
|
|
21
|
-
return execFileSync("node", [cli, ...args], { cwd: tmpDir, encoding: "utf-8" });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function runJSON(args) {
|
|
25
|
-
return JSON.parse(run([...args, "--json"]));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function killSession() {
|
|
29
|
-
try {
|
|
30
|
-
execSync(`tmux kill-session -t "${session}"`, { stdio: "ignore" });
|
|
31
|
-
} catch {}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function createSession() {
|
|
35
|
-
execSync(`tmux new-session -d -s "${session}" -x 80 -y 24`, { stdio: "ignore" });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
before(() => {
|
|
39
|
-
tmpDir = mkdtempSync(join(tmpdir(), "tmux-ide-test-"));
|
|
40
|
-
writeFileSync(
|
|
41
|
-
join(tmpDir, "ide.yml"),
|
|
42
|
-
`name: ${session}\nrows:\n - panes:\n - title: Shell\n`
|
|
43
|
-
);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
after(() => {
|
|
47
|
-
killSession();
|
|
48
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("status --json reports not running when no session exists", () => {
|
|
52
|
-
killSession();
|
|
53
|
-
const result = runJSON(["status"]);
|
|
54
|
-
assert.strictEqual(result.running, false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("status --json reports running after session is created", () => {
|
|
58
|
-
killSession();
|
|
59
|
-
createSession();
|
|
60
|
-
const result = runJSON(["status"]);
|
|
61
|
-
assert.strictEqual(result.running, true);
|
|
62
|
-
killSession();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("stop --json kills a running session", () => {
|
|
66
|
-
createSession();
|
|
67
|
-
run(["stop"]);
|
|
68
|
-
// Verify it's gone
|
|
69
|
-
const result = runJSON(["status"]);
|
|
70
|
-
assert.strictEqual(result.running, false);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("validate --json reports valid for our test config", () => {
|
|
74
|
-
const result = runJSON(["validate"]);
|
|
75
|
-
assert.strictEqual(result.valid, true);
|
|
76
|
-
assert.deepStrictEqual(result.errors, []);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("doctor --json passes checks", () => {
|
|
80
|
-
const result = runJSON(["doctor"]);
|
|
81
|
-
assert.strictEqual(result.ok, true);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("config --json dumps config", () => {
|
|
85
|
-
const result = runJSON(["config"]);
|
|
86
|
-
assert.strictEqual(result.name, session);
|
|
87
|
-
assert.ok(Array.isArray(result.rows));
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("ls --json returns sessions list", () => {
|
|
91
|
-
const result = runJSON(["ls"]);
|
|
92
|
-
assert.ok(Array.isArray(result.sessions));
|
|
93
|
-
});
|
|
94
|
-
});
|
package/src/lib/dot-path.test.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { getByPath, setByPath } from "./dot-path.js";
|
|
4
|
-
|
|
5
|
-
describe("getByPath", () => {
|
|
6
|
-
it("gets a top-level key", () => {
|
|
7
|
-
assert.strictEqual(getByPath({ name: "test" }, "name"), "test");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("gets a nested value", () => {
|
|
11
|
-
const obj = { rows: [{ panes: [{ title: "Shell" }] }] };
|
|
12
|
-
assert.strictEqual(getByPath(obj, "rows.0.panes.0.title"), "Shell");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("returns undefined for non-existent path", () => {
|
|
16
|
-
assert.strictEqual(getByPath({}, "a.b.c"), undefined);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("returns undefined for partially valid path", () => {
|
|
20
|
-
assert.strictEqual(getByPath({ a: { b: 1 } }, "a.b.c"), undefined);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("setByPath", () => {
|
|
25
|
-
it("sets a top-level key", () => {
|
|
26
|
-
const obj = {};
|
|
27
|
-
setByPath(obj, "name", "test");
|
|
28
|
-
assert.deepStrictEqual(obj, { name: "test" });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("sets a nested value", () => {
|
|
32
|
-
const obj = { rows: [{ panes: [{ title: "old" }] }] };
|
|
33
|
-
setByPath(obj, "rows.0.panes.0.title", "new");
|
|
34
|
-
assert.strictEqual(obj.rows[0].panes[0].title, "new");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("creates arrays for numeric keys", () => {
|
|
38
|
-
const obj = {};
|
|
39
|
-
setByPath(obj, "rows.0.title", "test");
|
|
40
|
-
assert.ok(Array.isArray(obj.rows));
|
|
41
|
-
assert.strictEqual(obj.rows[0].title, "test");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("creates objects for string keys", () => {
|
|
45
|
-
const obj = {};
|
|
46
|
-
setByPath(obj, "a.b.c", "val");
|
|
47
|
-
assert.deepStrictEqual(obj, { a: { b: { c: "val" } } });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("handles duplicate segment values (regression)", () => {
|
|
51
|
-
// Bug: rows.0.panes.0 — both "0" segments caused indexOf to return
|
|
52
|
-
// the same index, breaking the lookahead for array vs object creation
|
|
53
|
-
const obj = {};
|
|
54
|
-
setByPath(obj, "rows.0.panes.0", "value");
|
|
55
|
-
assert.ok(Array.isArray(obj.rows), "rows should be an array");
|
|
56
|
-
assert.ok(Array.isArray(obj.rows[0].panes), "panes should be an array");
|
|
57
|
-
assert.strictEqual(obj.rows[0].panes[0], "value");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("handles deeper duplicate segments", () => {
|
|
61
|
-
const obj = {};
|
|
62
|
-
setByPath(obj, "a.0.b.0.c", "deep");
|
|
63
|
-
assert.ok(Array.isArray(obj.a));
|
|
64
|
-
assert.ok(Array.isArray(obj.a[0].b));
|
|
65
|
-
assert.strictEqual(obj.a[0].b[0].c, "deep");
|
|
66
|
-
});
|
|
67
|
-
});
|
package/src/lib/sizes.test.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { computeSizes, toSplitPercents } from "./sizes.js";
|
|
4
|
-
|
|
5
|
-
describe("computeSizes", () => {
|
|
6
|
-
it("distributes remaining space to items without size", () => {
|
|
7
|
-
const result = computeSizes([{ size: "70%" }, {}]);
|
|
8
|
-
assert.deepStrictEqual(result, [70, 30]);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("passes through all explicit sizes", () => {
|
|
12
|
-
const result = computeSizes([{ size: "60%" }, { size: "40%" }]);
|
|
13
|
-
assert.deepStrictEqual(result, [60, 40]);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("splits equally when no sizes specified", () => {
|
|
17
|
-
const result = computeSizes([{}, {}, {}]);
|
|
18
|
-
const expected = [100 / 3, 100 / 3, 100 / 3];
|
|
19
|
-
assert.deepStrictEqual(result, expected);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("handles 3 items with mixed sizes", () => {
|
|
23
|
-
const result = computeSizes([{ size: "60%" }, { size: "25%" }, {}]);
|
|
24
|
-
assert.deepStrictEqual(result, [60, 25, 15]);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("handles single item without size", () => {
|
|
28
|
-
const result = computeSizes([{}]);
|
|
29
|
-
assert.deepStrictEqual(result, [100]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("handles single item with size", () => {
|
|
33
|
-
const result = computeSizes([{ size: "50%" }]);
|
|
34
|
-
assert.deepStrictEqual(result, [50]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("handles empty array", () => {
|
|
38
|
-
const result = computeSizes([]);
|
|
39
|
-
assert.deepStrictEqual(result, []);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("clamps remaining to zero when sizes exceed 100", () => {
|
|
43
|
-
const result = computeSizes([{ size: "80%" }, { size: "30%" }, {}]);
|
|
44
|
-
assert.deepStrictEqual(result, [80, 30, 0]);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe("toSplitPercents", () => {
|
|
49
|
-
it("converts 70/30 to [30]", () => {
|
|
50
|
-
assert.deepStrictEqual(toSplitPercents([70, 30]), [30]);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("converts 60/25/15 correctly", () => {
|
|
54
|
-
const result = toSplitPercents([60, 25, 15]);
|
|
55
|
-
assert.deepStrictEqual(result, [40, 38]);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("converts equal 50/50 to [50]", () => {
|
|
59
|
-
assert.deepStrictEqual(toSplitPercents([50, 50]), [50]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("converts equal thirds", () => {
|
|
63
|
-
const result = toSplitPercents([100 / 3, 100 / 3, 100 / 3]);
|
|
64
|
-
assert.deepStrictEqual(result, [67, 50]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("returns empty array for single item", () => {
|
|
68
|
-
assert.deepStrictEqual(toSplitPercents([100]), []);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("returns empty array for empty input", () => {
|
|
72
|
-
assert.deepStrictEqual(toSplitPercents([]), []);
|
|
73
|
-
});
|
|
74
|
-
});
|