tmux-ide 1.0.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/LICENSE +21 -0
- package/README.md +95 -0
- package/bin/cli.js +168 -0
- package/package.json +44 -0
- package/scripts/postinstall.js +13 -0
- package/skill/SKILL.md +238 -0
- package/src/attach.js +22 -0
- package/src/config.js +259 -0
- package/src/detect.js +178 -0
- package/src/detect.test.js +115 -0
- package/src/doctor.js +82 -0
- package/src/init.js +81 -0
- package/src/integration.test.js +94 -0
- package/src/launch.js +238 -0
- package/src/lib/dot-path.js +16 -0
- package/src/lib/dot-path.test.js +67 -0
- package/src/lib/output.js +72 -0
- package/src/lib/sizes.js +36 -0
- package/src/lib/sizes.test.js +74 -0
- package/src/lib/yaml-io.js +26 -0
- package/src/ls.js +42 -0
- package/src/restart.js +23 -0
- package/src/status.js +53 -0
- package/src/stop.js +23 -0
- package/src/validate.js +170 -0
- package/src/validate.test.js +141 -0
- package/templates/agent-team-monorepo.yml +31 -0
- package/templates/agent-team-nextjs.yml +27 -0
- package/templates/agent-team.yml +23 -0
- package/templates/convex.yml +19 -0
- package/templates/default.yml +20 -0
- package/templates/go.yml +15 -0
- package/templates/nextjs.yml +17 -0
- package/templates/python.yml +15 -0
- package/templates/vite.yml +15 -0
|
@@ -0,0 +1,26 @@
|
|
|
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 config.name ?? basename(dir);
|
|
23
|
+
} catch {
|
|
24
|
+
return basename(dir);
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/ls.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
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(
|
|
39
|
+
s.name.padEnd(24) + date.padEnd(22) + (s.attached ? "yes" : "no")
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/restart.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { getSessionName } from "./lib/yaml-io.js";
|
|
4
|
+
import { launch } from "./launch.js";
|
|
5
|
+
|
|
6
|
+
export async function restart(targetDir) {
|
|
7
|
+
const dir = resolve(targetDir ?? ".");
|
|
8
|
+
const session = getSessionName(dir);
|
|
9
|
+
|
|
10
|
+
let wasRunning = false;
|
|
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) {
|
|
19
|
+
console.log(`Stopped session "${session}"`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await launch(dir);
|
|
23
|
+
}
|
package/src/status.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { getSessionName } from "./lib/yaml-io.js";
|
|
5
|
+
|
|
6
|
+
export async function status(targetDir, { json } = {}) {
|
|
7
|
+
const dir = resolve(targetDir ?? ".");
|
|
8
|
+
const session = getSessionName(dir);
|
|
9
|
+
const configExists = existsSync(resolve(dir, "ide.yml"));
|
|
10
|
+
|
|
11
|
+
let running = false;
|
|
12
|
+
let panes = [];
|
|
13
|
+
|
|
14
|
+
try {
|
|
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 {}
|
|
34
|
+
|
|
35
|
+
const data = { session, running, configExists, panes };
|
|
36
|
+
|
|
37
|
+
if (json) {
|
|
38
|
+
console.log(JSON.stringify(data, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`Session: ${session}`);
|
|
43
|
+
console.log(`Running: ${running ? "yes" : "no"}`);
|
|
44
|
+
console.log(`Config: ${configExists ? "ide.yml found" : "no ide.yml"}`);
|
|
45
|
+
|
|
46
|
+
if (panes.length > 0) {
|
|
47
|
+
console.log(`\nPanes:`);
|
|
48
|
+
for (const p of panes) {
|
|
49
|
+
const active = p.active ? " (active)" : "";
|
|
50
|
+
console.log(` ${p.index}: ${p.title} [${p.width}x${p.height}]${active}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/stop.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { getSessionName } from "./lib/yaml-io.js";
|
|
4
|
+
|
|
5
|
+
export async function stop(targetDir, { json } = {}) {
|
|
6
|
+
const dir = resolve(targetDir ?? ".");
|
|
7
|
+
const session = getSessionName(dir);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
execSync(`tmux kill-session -t "${session}"`, { stdio: "inherit" });
|
|
11
|
+
if (json) {
|
|
12
|
+
console.log(JSON.stringify({ stopped: session }));
|
|
13
|
+
} else {
|
|
14
|
+
console.log(`Stopped session "${session}"`);
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
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
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
// 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
|
+
if (config.theme !== undefined) {
|
|
114
|
+
if (config.theme == null || typeof config.theme !== "object" || Array.isArray(config.theme)) {
|
|
115
|
+
errors.push("'theme' must be an object");
|
|
116
|
+
} else {
|
|
117
|
+
for (const key of ["accent", "border", "bg", "fg"]) {
|
|
118
|
+
if (config.theme[key] !== undefined && typeof config.theme[key] !== "string") {
|
|
119
|
+
errors.push(`theme.${key} must be a string`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return errors;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateSize(value, path, errors) {
|
|
129
|
+
const s = String(value);
|
|
130
|
+
if (!/^\d+%$/.test(s)) {
|
|
131
|
+
errors.push(`${path} "${value}" must be a percentage (e.g. "50%")`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const num = parseInt(s, 10);
|
|
135
|
+
if (num === 0) {
|
|
136
|
+
errors.push(`${path} must not be 0%`);
|
|
137
|
+
} else if (num > 100) {
|
|
138
|
+
errors.push(`${path} must not exceed 100%`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function validate(targetDir, { json } = {}) {
|
|
143
|
+
const dir = resolve(targetDir ?? ".");
|
|
144
|
+
let config;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
({ config } = readConfig(dir));
|
|
148
|
+
} catch (e) {
|
|
149
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const errors = validateConfig(config);
|
|
154
|
+
const valid = errors.length === 0;
|
|
155
|
+
|
|
156
|
+
if (json) {
|
|
157
|
+
console.log(JSON.stringify({ valid, errors }, null, 2));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (valid) {
|
|
162
|
+
console.log("✓ ide.yml is valid");
|
|
163
|
+
} else {
|
|
164
|
+
console.log("✗ ide.yml has errors:");
|
|
165
|
+
for (const e of errors) {
|
|
166
|
+
console.log(` - ${e}`);
|
|
167
|
+
}
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { validateConfig } from "./validate.js";
|
|
4
|
+
|
|
5
|
+
describe("validateConfig", () => {
|
|
6
|
+
it("accepts a valid minimal config", () => {
|
|
7
|
+
const errors = validateConfig({ rows: [{ panes: [{}] }] });
|
|
8
|
+
assert.deepStrictEqual(errors, []);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("accepts a valid full config", () => {
|
|
12
|
+
const errors = validateConfig({
|
|
13
|
+
name: "my-project",
|
|
14
|
+
before: "pnpm install",
|
|
15
|
+
rows: [
|
|
16
|
+
{
|
|
17
|
+
size: "70%",
|
|
18
|
+
panes: [
|
|
19
|
+
{ title: "Editor", command: "vim", dir: "src", size: "60%", focus: true, env: { PORT: 3000, HOST: "localhost" } },
|
|
20
|
+
{ title: "Shell" },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
panes: [{ title: "Dev", command: "pnpm dev" }],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
theme: { accent: "colour75", border: "colour238", bg: "colour235", fg: "colour248" },
|
|
28
|
+
});
|
|
29
|
+
assert.deepStrictEqual(errors, []);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("rejects null config", () => {
|
|
33
|
+
const errors = validateConfig(null);
|
|
34
|
+
assert.deepStrictEqual(errors, ["config must be an object"]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("rejects string config", () => {
|
|
38
|
+
const errors = validateConfig("hello");
|
|
39
|
+
assert.deepStrictEqual(errors, ["config must be an object"]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects array config", () => {
|
|
43
|
+
const errors = validateConfig([]);
|
|
44
|
+
assert.deepStrictEqual(errors, ["config must be an object"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("requires rows to be an array", () => {
|
|
48
|
+
const errors = validateConfig({ rows: "nope" });
|
|
49
|
+
assert.ok(errors.includes("'rows' must be an array"));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("requires rows to be non-empty", () => {
|
|
53
|
+
const errors = validateConfig({ rows: [] });
|
|
54
|
+
assert.ok(errors.includes("'rows' must not be empty"));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("requires rows to have a panes array", () => {
|
|
58
|
+
const errors = validateConfig({ rows: [{}] });
|
|
59
|
+
assert.ok(errors.includes("rows[0].panes must be an array"));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("requires panes to be non-empty", () => {
|
|
63
|
+
const errors = validateConfig({ rows: [{ panes: [] }] });
|
|
64
|
+
assert.ok(errors.includes("rows[0].panes must not be empty"));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("rejects non-string name", () => {
|
|
68
|
+
const errors = validateConfig({ name: 123, rows: [{ panes: [{}] }] });
|
|
69
|
+
assert.ok(errors.includes("'name' must be a string"));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects non-string before", () => {
|
|
73
|
+
const errors = validateConfig({ before: true, rows: [{ panes: [{}] }] });
|
|
74
|
+
assert.ok(errors.includes("'before' must be a string"));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("rejects non-string pane.title", () => {
|
|
78
|
+
const errors = validateConfig({ rows: [{ panes: [{ title: 42 }] }] });
|
|
79
|
+
assert.ok(errors.includes("rows[0].panes[0].title must be a string"));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("rejects non-string pane.command", () => {
|
|
83
|
+
const errors = validateConfig({ rows: [{ panes: [{ command: false }] }] });
|
|
84
|
+
assert.ok(errors.includes("rows[0].panes[0].command must be a string"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("rejects non-string pane.dir", () => {
|
|
88
|
+
const errors = validateConfig({ rows: [{ panes: [{ dir: [] }] }] });
|
|
89
|
+
assert.ok(errors.includes("rows[0].panes[0].dir must be a string"));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("rejects non-boolean pane.focus", () => {
|
|
93
|
+
const errors = validateConfig({ rows: [{ panes: [{ focus: "yes" }] }] });
|
|
94
|
+
assert.ok(errors.includes("rows[0].panes[0].focus must be a boolean"));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("rejects non-object pane.env", () => {
|
|
98
|
+
const errors = validateConfig({ rows: [{ panes: [{ env: "PORT=3000" }] }] });
|
|
99
|
+
assert.ok(errors.includes("rows[0].panes[0].env must be an object"));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("rejects invalid env values", () => {
|
|
103
|
+
const errors = validateConfig({ rows: [{ panes: [{ env: { PORT: true } }] }] });
|
|
104
|
+
assert.ok(errors.includes("rows[0].panes[0].env.PORT must be a string or number"));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("rejects size without % suffix", () => {
|
|
108
|
+
const errors = validateConfig({ rows: [{ size: "70", panes: [{}] }] });
|
|
109
|
+
assert.ok(errors.some((e) => e.includes('must be a percentage')));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("rejects 0% size", () => {
|
|
113
|
+
const errors = validateConfig({ rows: [{ size: "0%", panes: [{}] }] });
|
|
114
|
+
assert.ok(errors.some((e) => e.includes("must not be 0%")));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("rejects >100% size", () => {
|
|
118
|
+
const errors = validateConfig({ rows: [{ panes: [{ size: "150%" }] }] });
|
|
119
|
+
assert.ok(errors.some((e) => e.includes("must not exceed 100%")));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("validates theme fields as strings", () => {
|
|
123
|
+
const errors = validateConfig({ rows: [{ panes: [{}] }], theme: { accent: 123 } });
|
|
124
|
+
assert.ok(errors.includes("theme.accent must be a string"));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects non-object theme", () => {
|
|
128
|
+
const errors = validateConfig({ rows: [{ panes: [{}] }], theme: "blue" });
|
|
129
|
+
assert.ok(errors.includes("'theme' must be an object"));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("collects multiple errors at once", () => {
|
|
133
|
+
const errors = validateConfig({
|
|
134
|
+
name: 123,
|
|
135
|
+
before: [],
|
|
136
|
+
rows: [{ panes: [{ title: 42, focus: "yes", size: "0%" }] }],
|
|
137
|
+
theme: "nope",
|
|
138
|
+
});
|
|
139
|
+
assert.ok(errors.length >= 4);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: my-monorepo
|
|
2
|
+
|
|
3
|
+
team:
|
|
4
|
+
name: my-monorepo
|
|
5
|
+
|
|
6
|
+
rows:
|
|
7
|
+
- size: 70%
|
|
8
|
+
panes:
|
|
9
|
+
- title: Lead — Architect
|
|
10
|
+
command: claude
|
|
11
|
+
role: lead
|
|
12
|
+
focus: true
|
|
13
|
+
- title: Frontend Agent
|
|
14
|
+
command: claude
|
|
15
|
+
role: teammate
|
|
16
|
+
task: "Work on the frontend application"
|
|
17
|
+
dir: apps/web
|
|
18
|
+
- title: Backend Agent
|
|
19
|
+
command: claude
|
|
20
|
+
role: teammate
|
|
21
|
+
task: "Work on the backend services"
|
|
22
|
+
dir: apps/api
|
|
23
|
+
|
|
24
|
+
- panes:
|
|
25
|
+
- title: Frontend
|
|
26
|
+
command: pnpm dev
|
|
27
|
+
dir: apps/web
|
|
28
|
+
- title: Backend
|
|
29
|
+
command: pnpm dev
|
|
30
|
+
dir: apps/api
|
|
31
|
+
- title: Shell
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: my-app
|
|
2
|
+
|
|
3
|
+
team:
|
|
4
|
+
name: my-app
|
|
5
|
+
|
|
6
|
+
before: pnpm install
|
|
7
|
+
|
|
8
|
+
rows:
|
|
9
|
+
- size: 70%
|
|
10
|
+
panes:
|
|
11
|
+
- title: Lead
|
|
12
|
+
command: claude
|
|
13
|
+
role: lead
|
|
14
|
+
focus: true
|
|
15
|
+
- title: Frontend
|
|
16
|
+
command: claude
|
|
17
|
+
role: teammate
|
|
18
|
+
task: "Work on React components and pages"
|
|
19
|
+
- title: Backend
|
|
20
|
+
command: claude
|
|
21
|
+
role: teammate
|
|
22
|
+
task: "Work on API routes and server logic"
|
|
23
|
+
|
|
24
|
+
- panes:
|
|
25
|
+
- title: Next.js
|
|
26
|
+
command: pnpm dev
|
|
27
|
+
- title: Shell
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: my-project
|
|
2
|
+
|
|
3
|
+
team:
|
|
4
|
+
name: my-project
|
|
5
|
+
|
|
6
|
+
rows:
|
|
7
|
+
- size: 70%
|
|
8
|
+
panes:
|
|
9
|
+
- title: Lead
|
|
10
|
+
command: claude
|
|
11
|
+
role: lead
|
|
12
|
+
focus: true
|
|
13
|
+
- title: Teammate 1
|
|
14
|
+
command: claude
|
|
15
|
+
role: teammate
|
|
16
|
+
- title: Teammate 2
|
|
17
|
+
command: claude
|
|
18
|
+
role: teammate
|
|
19
|
+
|
|
20
|
+
- panes:
|
|
21
|
+
- title: Dev Server
|
|
22
|
+
# command: npm run dev
|
|
23
|
+
- title: Shell
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: my-app
|
|
2
|
+
|
|
3
|
+
rows:
|
|
4
|
+
- size: 70%
|
|
5
|
+
panes:
|
|
6
|
+
- title: Claude 1 — feature
|
|
7
|
+
command: claude
|
|
8
|
+
- title: Claude 2 — review
|
|
9
|
+
command: claude
|
|
10
|
+
- title: Claude 3 — explore
|
|
11
|
+
command: claude
|
|
12
|
+
|
|
13
|
+
- panes:
|
|
14
|
+
- title: Next.js
|
|
15
|
+
command: pnpm dev
|
|
16
|
+
- title: Convex
|
|
17
|
+
command: npx convex dev
|
|
18
|
+
- title: Shell
|
|
19
|
+
focus: true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: my-project
|
|
2
|
+
|
|
3
|
+
# before: npm install # optional pre-launch hook
|
|
4
|
+
|
|
5
|
+
rows:
|
|
6
|
+
- size: 70%
|
|
7
|
+
panes:
|
|
8
|
+
- title: Claude 1
|
|
9
|
+
command: claude
|
|
10
|
+
- title: Claude 2
|
|
11
|
+
command: claude
|
|
12
|
+
|
|
13
|
+
- panes:
|
|
14
|
+
- title: Dev Server
|
|
15
|
+
# command: npm run dev
|
|
16
|
+
# dir: apps/web # per-pane working directory
|
|
17
|
+
# env: # per-pane environment variables
|
|
18
|
+
# PORT: 3000
|
|
19
|
+
- title: Shell
|
|
20
|
+
# focus: true # give this pane initial focus
|
package/templates/go.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: my-app
|
|
2
|
+
|
|
3
|
+
rows:
|
|
4
|
+
- size: 70%
|
|
5
|
+
panes:
|
|
6
|
+
- title: Claude 1 — feature
|
|
7
|
+
command: claude
|
|
8
|
+
- title: Claude 2 — review
|
|
9
|
+
command: claude
|
|
10
|
+
- title: Claude 3 — explore
|
|
11
|
+
command: claude
|
|
12
|
+
|
|
13
|
+
- panes:
|
|
14
|
+
- title: Next.js
|
|
15
|
+
command: pnpm dev
|
|
16
|
+
- title: Shell
|
|
17
|
+
focus: true
|