tmux-ide 1.2.0 → 1.3.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/bin/cli.js +14 -14
- package/dist/attach.d.ts +3 -0
- package/dist/attach.js +14 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +269 -0
- package/dist/detect.d.ts +15 -0
- package/dist/detect.js +228 -0
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +66 -0
- package/dist/init.d.ts +4 -0
- package/dist/init.js +67 -0
- package/dist/inspect.d.ts +54 -0
- package/dist/inspect.js +118 -0
- package/dist/launch.d.ts +22 -0
- package/dist/launch.js +174 -0
- package/dist/lib/dot-path.d.ts +2 -0
- package/dist/lib/dot-path.js +17 -0
- package/dist/lib/errors.d.ts +29 -0
- package/dist/lib/errors.js +37 -0
- package/dist/lib/launch-plan.d.ts +6 -0
- package/dist/lib/launch-plan.js +40 -0
- package/dist/lib/output.d.ts +9 -0
- package/dist/lib/output.js +65 -0
- package/dist/lib/session-monitor.d.ts +12 -0
- package/dist/lib/session-monitor.js +160 -0
- package/dist/lib/session-options.d.ts +13 -0
- package/dist/lib/session-options.js +85 -0
- package/dist/lib/sizes.d.ts +16 -0
- package/dist/lib/sizes.js +36 -0
- package/dist/lib/tmux.d.ts +42 -0
- package/dist/lib/tmux.js +225 -0
- package/dist/lib/yaml-io.d.ts +10 -0
- package/dist/lib/yaml-io.js +24 -0
- package/dist/ls.d.ts +3 -0
- package/dist/ls.js +35 -0
- package/dist/restart.d.ts +4 -0
- package/dist/restart.js +13 -0
- package/dist/status.d.ts +3 -0
- package/dist/status.js +29 -0
- package/dist/stop.d.ts +3 -0
- package/dist/stop.js +21 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +1 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +172 -0
- package/package.json +16 -10
- package/src/attach.js +0 -17
- package/src/config.js +0 -341
- package/src/detect.js +0 -232
- package/src/doctor.js +0 -91
- package/src/init.js +0 -73
- package/src/inspect.js +0 -127
- package/src/launch.js +0 -245
- package/src/lib/dot-path.js +0 -16
- package/src/lib/errors.js +0 -35
- package/src/lib/launch-plan.js +0 -49
- package/src/lib/output.js +0 -68
- package/src/lib/session-monitor.js +0 -179
- package/src/lib/session-options.js +0 -95
- package/src/lib/sizes.js +0 -36
- package/src/lib/tmux.js +0 -234
- package/src/lib/yaml-io.js +0 -26
- package/src/ls.js +0 -40
- package/src/restart.js +0 -16
- package/src/status.js +0 -35
- package/src/stop.js +0 -25
- package/src/validate.js +0 -154
package/src/lib/sizes.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compute absolute sizes for items where some have explicit sizes and others don't.
|
|
3
|
-
* Items with `size` (e.g. "70%") keep their value; remaining space is split equally
|
|
4
|
-
* among items without a size.
|
|
5
|
-
*/
|
|
6
|
-
export function computeSizes(items) {
|
|
7
|
-
let claimed = 0;
|
|
8
|
-
let unclaimed = 0;
|
|
9
|
-
for (const item of items) {
|
|
10
|
-
if (item.size) {
|
|
11
|
-
claimed += parseFloat(item.size);
|
|
12
|
-
} else {
|
|
13
|
-
unclaimed++;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
const remaining = Math.max(0, 100 - claimed);
|
|
17
|
-
const defaultSize = unclaimed > 0 ? remaining / unclaimed : 0;
|
|
18
|
-
return items.map((item) => (item.size ? parseFloat(item.size) : defaultSize));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Convert absolute sizes (e.g. [70, 30]) to tmux split percentages for sequential splits.
|
|
23
|
-
* Returns array of -p values (one per split after the first item).
|
|
24
|
-
*
|
|
25
|
-
* Each tmux split divides the current pane. The percentage given to -p is
|
|
26
|
-
* the portion allocated to the NEW (bottom/right) pane.
|
|
27
|
-
*/
|
|
28
|
-
export function toSplitPercents(sizes) {
|
|
29
|
-
const percents = [];
|
|
30
|
-
for (let i = 1; i < sizes.length; i++) {
|
|
31
|
-
const remaining = sizes.slice(i - 1).reduce((a, b) => a + b, 0);
|
|
32
|
-
const topShare = sizes[i - 1];
|
|
33
|
-
percents.push(Math.round(((remaining - topShare) / remaining) * 100));
|
|
34
|
-
}
|
|
35
|
-
return percents;
|
|
36
|
-
}
|
package/src/lib/tmux.js
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
-
import { TmuxError } from "./errors.js";
|
|
3
|
-
|
|
4
|
-
const DEBUG = process.env.TMUX_IDE_DEBUG === "1";
|
|
5
|
-
|
|
6
|
-
const SESSION_NOT_FOUND_PATTERNS = ["can't find session", "can't find window", "unknown target"];
|
|
7
|
-
|
|
8
|
-
const TMUX_UNAVAILABLE_PATTERNS = [
|
|
9
|
-
"failed to connect to server",
|
|
10
|
-
"no server running",
|
|
11
|
-
"error connecting to",
|
|
12
|
-
"connection refused",
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
export { TmuxError };
|
|
16
|
-
|
|
17
|
-
export function getSessionState(session) {
|
|
18
|
-
try {
|
|
19
|
-
runTmux(["has-session", "-t", session]);
|
|
20
|
-
return { running: true, reason: null };
|
|
21
|
-
} catch (error) {
|
|
22
|
-
if (error instanceof TmuxError) {
|
|
23
|
-
if (error.code === "SESSION_NOT_FOUND") {
|
|
24
|
-
return { running: false, reason: "SESSION_NOT_FOUND" };
|
|
25
|
-
}
|
|
26
|
-
if (error.code === "TMUX_UNAVAILABLE") {
|
|
27
|
-
return { running: false, reason: "TMUX_UNAVAILABLE" };
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function attachSession(session) {
|
|
35
|
-
runTmux(["attach", "-t", session], { stdio: "inherit" });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function hasSession(session) {
|
|
39
|
-
try {
|
|
40
|
-
runTmux(["has-session", "-t", session]);
|
|
41
|
-
return true;
|
|
42
|
-
} catch (error) {
|
|
43
|
-
if (error instanceof TmuxError && error.code === "SESSION_NOT_FOUND") {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function killSession(session) {
|
|
51
|
-
try {
|
|
52
|
-
runTmux(["kill-session", "-t", session]);
|
|
53
|
-
return { stopped: true, reason: null };
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if (error instanceof TmuxError) {
|
|
56
|
-
if (error.code === "SESSION_NOT_FOUND") {
|
|
57
|
-
return { stopped: false, reason: "SESSION_NOT_FOUND" };
|
|
58
|
-
}
|
|
59
|
-
if (error.code === "TMUX_UNAVAILABLE") {
|
|
60
|
-
return { stopped: false, reason: "TMUX_UNAVAILABLE" };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function listPanes(session) {
|
|
68
|
-
const raw = runTmux(
|
|
69
|
-
[
|
|
70
|
-
"list-panes",
|
|
71
|
-
"-t",
|
|
72
|
-
session,
|
|
73
|
-
"-F",
|
|
74
|
-
"#{pane_index}|#{pane_title}|#{pane_width}|#{pane_height}|#{pane_active}",
|
|
75
|
-
],
|
|
76
|
-
{ encoding: "utf-8" },
|
|
77
|
-
).trim();
|
|
78
|
-
|
|
79
|
-
if (!raw) return [];
|
|
80
|
-
|
|
81
|
-
return raw.split("\n").map((line) => {
|
|
82
|
-
const [index, title, width, height, active] = line.split("|");
|
|
83
|
-
return {
|
|
84
|
-
index: Number.parseInt(index, 10),
|
|
85
|
-
title,
|
|
86
|
-
width: Number.parseInt(width, 10),
|
|
87
|
-
height: Number.parseInt(height, 10),
|
|
88
|
-
active: active === "1",
|
|
89
|
-
};
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function createDetachedSession(session, cwd, { cols, lines } = {}) {
|
|
94
|
-
return runTmux(
|
|
95
|
-
[
|
|
96
|
-
"new-session",
|
|
97
|
-
"-d",
|
|
98
|
-
"-P",
|
|
99
|
-
"-F",
|
|
100
|
-
"#{pane_id}",
|
|
101
|
-
"-s",
|
|
102
|
-
session,
|
|
103
|
-
"-c",
|
|
104
|
-
cwd,
|
|
105
|
-
"-x",
|
|
106
|
-
String(cols ?? 200),
|
|
107
|
-
"-y",
|
|
108
|
-
String(lines ?? 50),
|
|
109
|
-
],
|
|
110
|
-
{ encoding: "utf-8" },
|
|
111
|
-
).trim();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function setSessionEnvironment(session, key, value) {
|
|
115
|
-
runTmux(["set-environment", "-t", session, key, String(value)]);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function splitPane(targetPane, direction, cwd, percent) {
|
|
119
|
-
return runTmux(
|
|
120
|
-
[
|
|
121
|
-
"split-window",
|
|
122
|
-
"-P",
|
|
123
|
-
"-F",
|
|
124
|
-
"#{pane_id}",
|
|
125
|
-
"-t",
|
|
126
|
-
targetPane,
|
|
127
|
-
direction === "vertical" ? "-v" : "-h",
|
|
128
|
-
"-c",
|
|
129
|
-
cwd,
|
|
130
|
-
"-p",
|
|
131
|
-
String(percent),
|
|
132
|
-
],
|
|
133
|
-
{ encoding: "utf-8" },
|
|
134
|
-
).trim();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function sendLiteral(targetPane, text) {
|
|
138
|
-
runTmux(["send-keys", "-t", targetPane, "-l", "--", text], { stdio: "inherit" });
|
|
139
|
-
runTmux(["send-keys", "-t", targetPane, "Enter"], { stdio: "inherit" });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function getPaneCurrentCommand(targetPane) {
|
|
143
|
-
return runTmux(["display-message", "-p", "-t", targetPane, "#{pane_current_command}"], {
|
|
144
|
-
encoding: "utf-8",
|
|
145
|
-
}).trim();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function selectPane(targetPane) {
|
|
149
|
-
runTmux(["select-pane", "-t", targetPane], { stdio: "inherit" });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function setPaneTitle(targetPane, title) {
|
|
153
|
-
runTmux(["select-pane", "-t", targetPane, "-T", title], { stdio: "inherit" });
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function runSessionCommand(args) {
|
|
157
|
-
runTmux(args, { stdio: "inherit" });
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function startSessionMonitor(session, monitorScript) {
|
|
161
|
-
const child = spawn("node", [monitorScript, session], {
|
|
162
|
-
detached: true,
|
|
163
|
-
stdio: "ignore",
|
|
164
|
-
});
|
|
165
|
-
child.unref();
|
|
166
|
-
// Store PID as tmux session variable for later cleanup
|
|
167
|
-
runTmux(["set-option", "-t", session, "@monitor_pid", String(child.pid)]);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function stopSessionMonitor(session) {
|
|
171
|
-
try {
|
|
172
|
-
const pid = runTmux(["show-option", "-gqvt", session, "@monitor_pid"], {
|
|
173
|
-
encoding: "utf-8",
|
|
174
|
-
}).trim();
|
|
175
|
-
if (pid) process.kill(parseInt(pid, 10));
|
|
176
|
-
} catch {
|
|
177
|
-
/* session or process already gone */
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function getSessionVariable(session, name) {
|
|
182
|
-
try {
|
|
183
|
-
const raw = runTmux(["show-option", "-gqvt", session, name], {
|
|
184
|
-
encoding: "utf-8",
|
|
185
|
-
});
|
|
186
|
-
return raw.trim() || null;
|
|
187
|
-
} catch {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function setSessionVariable(session, name, value) {
|
|
193
|
-
runTmux(["set-option", "-t", session, name, value]);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function runTmux(args, options = {}) {
|
|
197
|
-
if (DEBUG || globalThis.__tmuxIdeVerbose) {
|
|
198
|
-
console.error(` [tmux] ${args.join(" ")}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const execOptions = {
|
|
202
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
203
|
-
...options,
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
return execFileSync("tmux", args, execOptions);
|
|
208
|
-
} catch (error) {
|
|
209
|
-
throw classifyTmuxError(error);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function classifyTmuxError(error) {
|
|
214
|
-
const detail = getErrorDetail(error).toLowerCase();
|
|
215
|
-
|
|
216
|
-
if (SESSION_NOT_FOUND_PATTERNS.some((pattern) => detail.includes(pattern))) {
|
|
217
|
-
return new TmuxError("tmux session was not found", "SESSION_NOT_FOUND", { cause: error });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (TMUX_UNAVAILABLE_PATTERNS.some((pattern) => detail.includes(pattern))) {
|
|
221
|
-
return new TmuxError("tmux is unavailable or its socket is inaccessible", "TMUX_UNAVAILABLE", {
|
|
222
|
-
cause: error,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return new TmuxError("tmux command failed", "TMUX_ERROR", { cause: error });
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function getErrorDetail(error) {
|
|
230
|
-
const stderr = error?.stderr;
|
|
231
|
-
if (typeof stderr === "string" && stderr.length > 0) return stderr;
|
|
232
|
-
if (Buffer.isBuffer(stderr) && stderr.length > 0) return stderr.toString("utf-8");
|
|
233
|
-
return error?.message ?? "";
|
|
234
|
-
}
|
package/src/lib/yaml-io.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { resolve, basename } from "node:path";
|
|
3
|
-
import yaml from "js-yaml";
|
|
4
|
-
|
|
5
|
-
export function readConfig(dir) {
|
|
6
|
-
const configPath = resolve(dir, "ide.yml");
|
|
7
|
-
const raw = readFileSync(configPath, "utf-8");
|
|
8
|
-
const config = yaml.load(raw);
|
|
9
|
-
return { config, configPath };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function writeConfig(dir, config) {
|
|
13
|
-
const configPath = resolve(dir, "ide.yml");
|
|
14
|
-
const out = yaml.dump(config, { lineWidth: -1, noRefs: true, quotingType: '"' });
|
|
15
|
-
writeFileSync(configPath, out);
|
|
16
|
-
return configPath;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function getSessionName(dir) {
|
|
20
|
-
try {
|
|
21
|
-
const { config } = readConfig(dir);
|
|
22
|
-
return { name: config.name ?? basename(dir), source: config.name ? "config" : "fallback" };
|
|
23
|
-
} catch {
|
|
24
|
-
return { name: basename(dir), source: "fallback" };
|
|
25
|
-
}
|
|
26
|
-
}
|
package/src/ls.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
export async function ls({ json } = {}) {
|
|
4
|
-
let raw;
|
|
5
|
-
try {
|
|
6
|
-
raw = execSync(
|
|
7
|
-
'tmux list-sessions -F "#{session_name}|#{session_created}|#{session_attached}"',
|
|
8
|
-
{ encoding: "utf-8" },
|
|
9
|
-
).trim();
|
|
10
|
-
} catch {
|
|
11
|
-
if (json) {
|
|
12
|
-
console.log(JSON.stringify({ sessions: [] }));
|
|
13
|
-
} else {
|
|
14
|
-
console.log("No tmux sessions running.");
|
|
15
|
-
}
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const sessions = raw.split("\n").map((line) => {
|
|
20
|
-
const [name, created, attached] = line.split("|");
|
|
21
|
-
return {
|
|
22
|
-
name,
|
|
23
|
-
created: new Date(parseInt(created) * 1000).toISOString(),
|
|
24
|
-
attached: attached !== "0",
|
|
25
|
-
};
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (json) {
|
|
29
|
-
console.log(JSON.stringify({ sessions }, null, 2));
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Table output
|
|
34
|
-
console.log("SESSION".padEnd(24) + "CREATED".padEnd(22) + "ATTACHED");
|
|
35
|
-
console.log("─".repeat(54));
|
|
36
|
-
for (const s of sessions) {
|
|
37
|
-
const date = new Date(s.created).toLocaleString();
|
|
38
|
-
console.log(s.name.padEnd(24) + date.padEnd(22) + (s.attached ? "yes" : "no"));
|
|
39
|
-
}
|
|
40
|
-
}
|
package/src/restart.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { getSessionName } from "./lib/yaml-io.js";
|
|
3
|
-
import { launch } from "./launch.js";
|
|
4
|
-
import { killSession } from "./lib/tmux.js";
|
|
5
|
-
|
|
6
|
-
export async function restart(targetDir, { json, attach } = {}) {
|
|
7
|
-
const dir = resolve(targetDir ?? ".");
|
|
8
|
-
const { name: session } = getSessionName(dir);
|
|
9
|
-
const result = killSession(session);
|
|
10
|
-
|
|
11
|
-
if (result.stopped) {
|
|
12
|
-
console.log(`Stopped session "${session}"`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
await launch(dir, { json, attach });
|
|
16
|
-
}
|
package/src/status.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { getSessionName } from "./lib/yaml-io.js";
|
|
4
|
-
import { getSessionState, listPanes } from "./lib/tmux.js";
|
|
5
|
-
|
|
6
|
-
export async function status(targetDir, { json } = {}) {
|
|
7
|
-
const dir = resolve(targetDir ?? ".");
|
|
8
|
-
const { name: session } = getSessionName(dir);
|
|
9
|
-
const configExists = existsSync(resolve(dir, "ide.yml"));
|
|
10
|
-
|
|
11
|
-
const state = getSessionState(session);
|
|
12
|
-
const running = state.running;
|
|
13
|
-
let panes = [];
|
|
14
|
-
|
|
15
|
-
if (running) panes = listPanes(session);
|
|
16
|
-
|
|
17
|
-
const data = { session, running, configExists, panes };
|
|
18
|
-
|
|
19
|
-
if (json) {
|
|
20
|
-
console.log(JSON.stringify(data, null, 2));
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
console.log(`Session: ${session}`);
|
|
25
|
-
console.log(`Running: ${running ? "yes" : "no"}`);
|
|
26
|
-
console.log(`Config: ${configExists ? "ide.yml found" : "no ide.yml"}`);
|
|
27
|
-
|
|
28
|
-
if (panes.length > 0) {
|
|
29
|
-
console.log(`\nPanes:`);
|
|
30
|
-
for (const p of panes) {
|
|
31
|
-
const active = p.active ? " (active)" : "";
|
|
32
|
-
console.log(` ${p.index}: ${p.title} [${p.width}x${p.height}]${active}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
package/src/stop.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { getSessionName } from "./lib/yaml-io.js";
|
|
3
|
-
import { outputError } from "./lib/output.js";
|
|
4
|
-
import { killSession, stopSessionMonitor } from "./lib/tmux.js";
|
|
5
|
-
|
|
6
|
-
export async function stop(targetDir, { json } = {}) {
|
|
7
|
-
const dir = resolve(targetDir ?? ".");
|
|
8
|
-
const { name: session } = getSessionName(dir);
|
|
9
|
-
|
|
10
|
-
// Stop the session monitor before killing the session
|
|
11
|
-
stopSessionMonitor(session);
|
|
12
|
-
|
|
13
|
-
const result = killSession(session);
|
|
14
|
-
|
|
15
|
-
if (result.stopped) {
|
|
16
|
-
if (json) {
|
|
17
|
-
console.log(JSON.stringify({ stopped: session }));
|
|
18
|
-
} else {
|
|
19
|
-
console.log(`Stopped session "${session}"`);
|
|
20
|
-
}
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
outputError(`No active session "${session}" found`, "NOT_RUNNING");
|
|
25
|
-
}
|
package/src/validate.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { readConfig } from "./lib/yaml-io.js";
|
|
3
|
-
import { outputError } from "./lib/output.js";
|
|
4
|
-
|
|
5
|
-
export function validateConfig(config) {
|
|
6
|
-
const errors = [];
|
|
7
|
-
|
|
8
|
-
if (config == null || typeof config !== "object" || Array.isArray(config)) {
|
|
9
|
-
errors.push("config must be an object");
|
|
10
|
-
return errors;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (config.name !== undefined && typeof config.name !== "string") {
|
|
14
|
-
errors.push("'name' must be a string");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (config.before !== undefined && typeof config.before !== "string") {
|
|
18
|
-
errors.push("'before' must be a string");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (!Array.isArray(config.rows)) {
|
|
22
|
-
errors.push("'rows' must be an array");
|
|
23
|
-
} else if (config.rows.length === 0) {
|
|
24
|
-
errors.push("'rows' must not be empty");
|
|
25
|
-
} else {
|
|
26
|
-
for (let i = 0; i < config.rows.length; i++) {
|
|
27
|
-
const row = config.rows[i];
|
|
28
|
-
if (!row.panes || !Array.isArray(row.panes)) {
|
|
29
|
-
errors.push(`rows[${i}].panes must be an array`);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (row.panes.length === 0) {
|
|
33
|
-
errors.push(`rows[${i}].panes must not be empty`);
|
|
34
|
-
}
|
|
35
|
-
if (row.size !== undefined) {
|
|
36
|
-
validateSize(row.size, `rows[${i}].size`, errors);
|
|
37
|
-
}
|
|
38
|
-
for (let j = 0; j < row.panes.length; j++) {
|
|
39
|
-
const pane = row.panes[j];
|
|
40
|
-
if (pane.title !== undefined && typeof pane.title !== "string") {
|
|
41
|
-
errors.push(`rows[${i}].panes[${j}].title must be a string`);
|
|
42
|
-
}
|
|
43
|
-
if (pane.command !== undefined && typeof pane.command !== "string") {
|
|
44
|
-
errors.push(`rows[${i}].panes[${j}].command must be a string`);
|
|
45
|
-
}
|
|
46
|
-
if (pane.dir !== undefined && typeof pane.dir !== "string") {
|
|
47
|
-
errors.push(`rows[${i}].panes[${j}].dir must be a string`);
|
|
48
|
-
}
|
|
49
|
-
if (pane.focus !== undefined && typeof pane.focus !== "boolean") {
|
|
50
|
-
errors.push(`rows[${i}].panes[${j}].focus must be a boolean`);
|
|
51
|
-
}
|
|
52
|
-
if (pane.env !== undefined) {
|
|
53
|
-
if (pane.env == null || typeof pane.env !== "object" || Array.isArray(pane.env)) {
|
|
54
|
-
errors.push(`rows[${i}].panes[${j}].env must be an object`);
|
|
55
|
-
} else {
|
|
56
|
-
for (const [key, val] of Object.entries(pane.env)) {
|
|
57
|
-
if (typeof val !== "string" && typeof val !== "number") {
|
|
58
|
-
errors.push(`rows[${i}].panes[${j}].env.${key} must be a string or number`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (pane.size !== undefined) {
|
|
64
|
-
validateSize(pane.size, `rows[${i}].panes[${j}].size`, errors);
|
|
65
|
-
}
|
|
66
|
-
if (pane.role !== undefined) {
|
|
67
|
-
if (pane.role !== "lead" && pane.role !== "teammate") {
|
|
68
|
-
errors.push(`rows[${i}].panes[${j}].role must be "lead" or "teammate"`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (pane.task !== undefined && typeof pane.task !== "string") {
|
|
72
|
-
errors.push(`rows[${i}].panes[${j}].task must be a string`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Validate team config
|
|
79
|
-
if (config.team !== undefined) {
|
|
80
|
-
if (config.team == null || typeof config.team !== "object" || Array.isArray(config.team)) {
|
|
81
|
-
errors.push("'team' must be an object");
|
|
82
|
-
} else {
|
|
83
|
-
if (config.team.name === undefined) {
|
|
84
|
-
errors.push("'team.name' is required when team is specified");
|
|
85
|
-
} else if (typeof config.team.name !== "string") {
|
|
86
|
-
errors.push("'team.name' must be a string");
|
|
87
|
-
}
|
|
88
|
-
if (config.team.model !== undefined && typeof config.team.model !== "string") {
|
|
89
|
-
errors.push("'team.model' must be a string");
|
|
90
|
-
}
|
|
91
|
-
if (config.team.permissions !== undefined && !Array.isArray(config.team.permissions)) {
|
|
92
|
-
errors.push("'team.permissions' must be an array");
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (config.theme !== undefined) {
|
|
98
|
-
if (config.theme == null || typeof config.theme !== "object" || Array.isArray(config.theme)) {
|
|
99
|
-
errors.push("'theme' must be an object");
|
|
100
|
-
} else {
|
|
101
|
-
for (const key of ["accent", "border", "bg", "fg"]) {
|
|
102
|
-
if (config.theme[key] !== undefined && typeof config.theme[key] !== "string") {
|
|
103
|
-
errors.push(`theme.${key} must be a string`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return errors;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function validateSize(value, path, errors) {
|
|
113
|
-
const s = String(value);
|
|
114
|
-
if (!/^\d+%$/.test(s)) {
|
|
115
|
-
errors.push(`${path} "${value}" must be a percentage (e.g. "50%")`);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const num = parseInt(s, 10);
|
|
119
|
-
if (num === 0) {
|
|
120
|
-
errors.push(`${path} must not be 0%`);
|
|
121
|
-
} else if (num > 100) {
|
|
122
|
-
errors.push(`${path} must not exceed 100%`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export async function validate(targetDir, { json } = {}) {
|
|
127
|
-
const dir = resolve(targetDir ?? ".");
|
|
128
|
-
let config;
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
({ config } = readConfig(dir));
|
|
132
|
-
} catch (e) {
|
|
133
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const errors = validateConfig(config);
|
|
138
|
-
const valid = errors.length === 0;
|
|
139
|
-
|
|
140
|
-
if (json) {
|
|
141
|
-
console.log(JSON.stringify({ valid, errors }, null, 2));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (valid) {
|
|
146
|
-
console.log("✓ ide.yml is valid");
|
|
147
|
-
} else {
|
|
148
|
-
console.log("✗ ide.yml has errors:");
|
|
149
|
-
for (const e of errors) {
|
|
150
|
-
console.log(` - ${e}`);
|
|
151
|
-
}
|
|
152
|
-
process.exitCode = 1;
|
|
153
|
-
}
|
|
154
|
-
}
|