tmux-ide 1.2.1 → 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 -237
- 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/dist/status.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
export async function status(targetDir, { json } = {}) {
|
|
6
|
+
const dir = resolve(targetDir ?? ".");
|
|
7
|
+
const { name: session } = getSessionName(dir);
|
|
8
|
+
const configExists = existsSync(resolve(dir, "ide.yml"));
|
|
9
|
+
const state = getSessionState(session);
|
|
10
|
+
const running = state.running;
|
|
11
|
+
let panes = [];
|
|
12
|
+
if (running)
|
|
13
|
+
panes = listPanes(session);
|
|
14
|
+
const data = { session, running, configExists, panes };
|
|
15
|
+
if (json) {
|
|
16
|
+
console.log(JSON.stringify(data, null, 2));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
console.log(`Session: ${session}`);
|
|
20
|
+
console.log(`Running: ${running ? "yes" : "no"}`);
|
|
21
|
+
console.log(`Config: ${configExists ? "ide.yml found" : "no ide.yml"}`);
|
|
22
|
+
if (panes.length > 0) {
|
|
23
|
+
console.log(`\nPanes:`);
|
|
24
|
+
for (const p of panes) {
|
|
25
|
+
const active = p.active ? " (active)" : "";
|
|
26
|
+
console.log(` ${p.index}: ${p.title} [${p.width}x${p.height}]${active}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/stop.d.ts
ADDED
package/dist/stop.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
export async function stop(targetDir, { json } = {}) {
|
|
6
|
+
const dir = resolve(targetDir ?? ".");
|
|
7
|
+
const { name: session } = getSessionName(dir);
|
|
8
|
+
// Stop the session monitor before killing the session
|
|
9
|
+
stopSessionMonitor(session);
|
|
10
|
+
const result = killSession(session);
|
|
11
|
+
if (result.stopped) {
|
|
12
|
+
if (json) {
|
|
13
|
+
console.log(JSON.stringify({ stopped: session }));
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log(`Stopped session "${session}"`);
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
outputError(`No active session "${session}" found`, "NOT_RUNNING");
|
|
21
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface IdeConfig {
|
|
2
|
+
name?: string;
|
|
3
|
+
before?: string;
|
|
4
|
+
team?: {
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
rows: Row[];
|
|
8
|
+
theme?: ThemeConfig;
|
|
9
|
+
}
|
|
10
|
+
export interface Row {
|
|
11
|
+
size?: string;
|
|
12
|
+
panes: Pane[];
|
|
13
|
+
}
|
|
14
|
+
export interface Pane {
|
|
15
|
+
title?: string;
|
|
16
|
+
command?: string;
|
|
17
|
+
dir?: string;
|
|
18
|
+
size?: string;
|
|
19
|
+
focus?: boolean;
|
|
20
|
+
env?: Record<string, string | number>;
|
|
21
|
+
role?: "lead" | "teammate";
|
|
22
|
+
task?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ThemeConfig {
|
|
25
|
+
accent?: string;
|
|
26
|
+
border?: string;
|
|
27
|
+
bg?: string;
|
|
28
|
+
fg?: string;
|
|
29
|
+
}
|
|
30
|
+
export type TmuxCommand = string[];
|
|
31
|
+
export interface PaneAction {
|
|
32
|
+
targetPane: string;
|
|
33
|
+
title: string | null;
|
|
34
|
+
chdir: string | null;
|
|
35
|
+
exports: string[];
|
|
36
|
+
command: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface SessionState {
|
|
39
|
+
running: boolean;
|
|
40
|
+
reason: string | null;
|
|
41
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { readConfig } from "./lib/yaml-io.js";
|
|
3
|
+
import { outputError } from "./lib/output.js";
|
|
4
|
+
export function validateConfig(config) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
if (config == null || typeof config !== "object" || Array.isArray(config)) {
|
|
7
|
+
errors.push("config must be an object");
|
|
8
|
+
return errors;
|
|
9
|
+
}
|
|
10
|
+
const cfg = config;
|
|
11
|
+
if (cfg.name !== undefined && typeof cfg.name !== "string") {
|
|
12
|
+
errors.push("'name' must be a string");
|
|
13
|
+
}
|
|
14
|
+
if (cfg.before !== undefined && typeof cfg.before !== "string") {
|
|
15
|
+
errors.push("'before' must be a string");
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(cfg.rows)) {
|
|
18
|
+
errors.push("'rows' must be an array");
|
|
19
|
+
}
|
|
20
|
+
else if (cfg.rows.length === 0) {
|
|
21
|
+
errors.push("'rows' must not be empty");
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
for (let i = 0; i < cfg.rows.length; i++) {
|
|
25
|
+
const row = cfg.rows[i];
|
|
26
|
+
if (!row.panes || !Array.isArray(row.panes)) {
|
|
27
|
+
errors.push(`rows[${i}].panes must be an array`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (row.panes.length === 0) {
|
|
31
|
+
errors.push(`rows[${i}].panes must not be empty`);
|
|
32
|
+
}
|
|
33
|
+
if (row.size !== undefined) {
|
|
34
|
+
validateSize(row.size, `rows[${i}].size`, errors);
|
|
35
|
+
}
|
|
36
|
+
for (let j = 0; j < row.panes.length; j++) {
|
|
37
|
+
const pane = row.panes[j];
|
|
38
|
+
if (pane.title !== undefined && typeof pane.title !== "string") {
|
|
39
|
+
errors.push(`rows[${i}].panes[${j}].title must be a string`);
|
|
40
|
+
}
|
|
41
|
+
if (pane.command !== undefined && typeof pane.command !== "string") {
|
|
42
|
+
errors.push(`rows[${i}].panes[${j}].command must be a string`);
|
|
43
|
+
}
|
|
44
|
+
if (pane.dir !== undefined && typeof pane.dir !== "string") {
|
|
45
|
+
errors.push(`rows[${i}].panes[${j}].dir must be a string`);
|
|
46
|
+
}
|
|
47
|
+
if (pane.focus !== undefined && typeof pane.focus !== "boolean") {
|
|
48
|
+
errors.push(`rows[${i}].panes[${j}].focus must be a boolean`);
|
|
49
|
+
}
|
|
50
|
+
if (pane.env !== undefined) {
|
|
51
|
+
if (pane.env == null || typeof pane.env !== "object" || Array.isArray(pane.env)) {
|
|
52
|
+
errors.push(`rows[${i}].panes[${j}].env must be an object`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
for (const [key, val] of Object.entries(pane.env)) {
|
|
56
|
+
if (typeof val !== "string" && typeof val !== "number") {
|
|
57
|
+
errors.push(`rows[${i}].panes[${j}].env.${key} must be a string or number`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (pane.size !== undefined) {
|
|
63
|
+
validateSize(pane.size, `rows[${i}].panes[${j}].size`, errors);
|
|
64
|
+
}
|
|
65
|
+
if (pane.role !== undefined) {
|
|
66
|
+
if (pane.role !== "lead" && pane.role !== "teammate") {
|
|
67
|
+
errors.push(`rows[${i}].panes[${j}].role must be "lead" or "teammate"`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (pane.task !== undefined && typeof pane.task !== "string") {
|
|
71
|
+
errors.push(`rows[${i}].panes[${j}].task must be a string`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Check multiple focus panes in this row
|
|
75
|
+
const focusCount = row.panes.filter((p) => p.focus === true).length;
|
|
76
|
+
if (focusCount > 1) {
|
|
77
|
+
errors.push(`Row ${i} has ${focusCount} panes with focus: true (max 1)`);
|
|
78
|
+
}
|
|
79
|
+
// Check pane sizes sum within this row
|
|
80
|
+
const paneSizes = row.panes
|
|
81
|
+
.map((p) => p.size)
|
|
82
|
+
.filter((s) => typeof s === "string" && /^[1-9]\d*%$/.test(s))
|
|
83
|
+
.map((s) => parseInt(s, 10));
|
|
84
|
+
const paneSum = paneSizes.reduce((a, b) => a + b, 0);
|
|
85
|
+
if (paneSum > 100) {
|
|
86
|
+
errors.push(`Row ${i} pane sizes sum to ${paneSum}%, which exceeds 100%`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Check row sizes sum
|
|
90
|
+
const rowSizes = cfg.rows
|
|
91
|
+
.map((r) => r.size)
|
|
92
|
+
.filter((s) => typeof s === "string" && /^[1-9]\d*%$/.test(s))
|
|
93
|
+
.map((s) => parseInt(s, 10));
|
|
94
|
+
const rowSum = rowSizes.reduce((a, b) => a + b, 0);
|
|
95
|
+
if (rowSum > 100) {
|
|
96
|
+
errors.push(`Row sizes sum to ${rowSum}%, which exceeds 100%`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Validate team config
|
|
100
|
+
if (cfg.team !== undefined) {
|
|
101
|
+
if (cfg.team == null || typeof cfg.team !== "object" || Array.isArray(cfg.team)) {
|
|
102
|
+
errors.push("'team' must be an object");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const team = cfg.team;
|
|
106
|
+
if (team.name === undefined) {
|
|
107
|
+
errors.push("'team.name' is required when team is specified");
|
|
108
|
+
}
|
|
109
|
+
else if (typeof team.name !== "string") {
|
|
110
|
+
errors.push("'team.name' must be a string");
|
|
111
|
+
}
|
|
112
|
+
if (team.model !== undefined && typeof team.model !== "string") {
|
|
113
|
+
errors.push("'team.model' must be a string");
|
|
114
|
+
}
|
|
115
|
+
if (team.permissions !== undefined && !Array.isArray(team.permissions)) {
|
|
116
|
+
errors.push("'team.permissions' must be an array");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (cfg.theme !== undefined) {
|
|
121
|
+
if (cfg.theme == null || typeof cfg.theme !== "object" || Array.isArray(cfg.theme)) {
|
|
122
|
+
errors.push("'theme' must be an object");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const theme = cfg.theme;
|
|
126
|
+
for (const key of ["accent", "border", "bg", "fg"]) {
|
|
127
|
+
if (theme[key] !== undefined && typeof theme[key] !== "string") {
|
|
128
|
+
errors.push(`theme.${key} must be a string`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return errors;
|
|
134
|
+
}
|
|
135
|
+
function validateSize(value, path, errors) {
|
|
136
|
+
const s = String(value);
|
|
137
|
+
if (!/^[1-9]\d*%$/.test(s)) {
|
|
138
|
+
errors.push(`${path} "${value}" must be a percentage (e.g. "50%")`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const num = parseInt(s, 10);
|
|
142
|
+
if (num > 100) {
|
|
143
|
+
errors.push(`${path} must not exceed 100%`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export async function validate(targetDir, { json } = {}) {
|
|
147
|
+
const dir = resolve(targetDir ?? ".");
|
|
148
|
+
let config;
|
|
149
|
+
try {
|
|
150
|
+
({ config } = readConfig(dir));
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const errors = validateConfig(config);
|
|
157
|
+
const valid = errors.length === 0;
|
|
158
|
+
if (json) {
|
|
159
|
+
console.log(JSON.stringify({ valid, errors }, null, 2));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (valid) {
|
|
163
|
+
console.log("✓ ide.yml is valid");
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.log("✗ ide.yml has errors:");
|
|
167
|
+
for (const e of errors) {
|
|
168
|
+
console.log(` - ${e}`);
|
|
169
|
+
}
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-ide",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Turn any project into a tmux-powered terminal IDE with a simple ide.yml",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,17 +8,19 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
|
-
"
|
|
12
|
-
"!src/**/*.test.js",
|
|
11
|
+
"dist",
|
|
13
12
|
"scripts",
|
|
14
13
|
"skill",
|
|
15
14
|
"templates"
|
|
16
15
|
],
|
|
17
16
|
"scripts": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
17
|
+
"clean": "rm -rf dist",
|
|
18
|
+
"build": "pnpm run clean && tsc",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"dev": "node --experimental-strip-types bin/cli.js",
|
|
21
|
+
"test": "node --experimental-strip-types --test src/*.test.ts src/lib/*.test.ts",
|
|
22
|
+
"test:unit": "node --experimental-strip-types --test src/cli.test.ts src/config-cli.test.ts src/config.test.ts src/detect.test.ts src/doctor.test.ts src/init.test.ts src/inspect.test.ts src/launch.test.ts src/ls.test.ts src/stop.test.ts src/validate.test.ts src/lib/*.test.ts",
|
|
23
|
+
"test:integration": "node --experimental-strip-types --test src/integration.test.ts",
|
|
22
24
|
"lint": "eslint .",
|
|
23
25
|
"lint:workspace": "turbo run lint",
|
|
24
26
|
"format": "prettier --write .",
|
|
@@ -27,8 +29,8 @@
|
|
|
27
29
|
"typecheck:workspace": "turbo run typecheck",
|
|
28
30
|
"docs:build": "turbo run build --filter=@tmux-ide/docs",
|
|
29
31
|
"pack:check": "npm pack --dry-run --cache /tmp/tmux-ide-npm-cache > /dev/null",
|
|
30
|
-
"check": "pnpm run lint:workspace && pnpm run format:check && pnpm run test:unit && pnpm run docs:build && pnpm run pack:check",
|
|
31
|
-
"prepublishOnly": "pnpm run
|
|
32
|
+
"check": "pnpm run lint:workspace && pnpm run format:check && pnpm run build && pnpm run test:unit && pnpm run docs:build && pnpm run pack:check",
|
|
33
|
+
"prepublishOnly": "pnpm run build",
|
|
32
34
|
"postinstall": "node scripts/postinstall.js",
|
|
33
35
|
"docs": "turbo run dev --filter=@tmux-ide/docs"
|
|
34
36
|
},
|
|
@@ -57,9 +59,13 @@
|
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
59
61
|
"@eslint/js": "^10.0.1",
|
|
62
|
+
"@types/node": "^25.5.0",
|
|
63
|
+
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
|
64
|
+
"@typescript-eslint/parser": "^8.57.1",
|
|
60
65
|
"eslint": "^10.0.3",
|
|
61
66
|
"globals": "^17.4.0",
|
|
62
67
|
"prettier": "^3.8.1",
|
|
63
|
-
"turbo": "^2.3.3"
|
|
68
|
+
"turbo": "^2.3.3",
|
|
69
|
+
"typescript": "^5.9.3"
|
|
64
70
|
}
|
|
65
71
|
}
|
package/src/attach.js
DELETED
|
@@ -1,17 +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 { attachSession, getSessionState } from "./lib/tmux.js";
|
|
5
|
-
|
|
6
|
-
export async function attach(targetDir, { json: _json } = {}) {
|
|
7
|
-
const dir = resolve(targetDir ?? ".");
|
|
8
|
-
const { name: session } = getSessionName(dir);
|
|
9
|
-
const state = getSessionState(session);
|
|
10
|
-
|
|
11
|
-
if (!state.running) {
|
|
12
|
-
outputError(`Session "${session}" is not running. Start it with: tmux-ide`, "NOT_RUNNING");
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
attachSession(session);
|
|
17
|
-
}
|
package/src/config.js
DELETED
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { readConfig, writeConfig } from "./lib/yaml-io.js";
|
|
3
|
-
import { setByPath } from "./lib/dot-path.js";
|
|
4
|
-
import { outputError } from "./lib/output.js";
|
|
5
|
-
|
|
6
|
-
export async function config(targetDir, { json, action, args } = {}) {
|
|
7
|
-
const dir = resolve(targetDir ?? ".");
|
|
8
|
-
|
|
9
|
-
switch (action) {
|
|
10
|
-
case "dump":
|
|
11
|
-
return dumpConfig(dir, { json });
|
|
12
|
-
case "set":
|
|
13
|
-
return setConfig(dir, args, { json });
|
|
14
|
-
case "add-pane":
|
|
15
|
-
return addPane(dir, args, { json });
|
|
16
|
-
case "remove-pane":
|
|
17
|
-
return removePane(dir, args, { json });
|
|
18
|
-
case "add-row":
|
|
19
|
-
return addRow(dir, args, { json });
|
|
20
|
-
case "enable-team":
|
|
21
|
-
return enableTeam(dir, args, { json });
|
|
22
|
-
case "disable-team":
|
|
23
|
-
return disableTeam(dir, { json });
|
|
24
|
-
default:
|
|
25
|
-
return dumpConfig(dir, { json });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function dumpConfig(dir, { json }) {
|
|
30
|
-
let cfg;
|
|
31
|
-
try {
|
|
32
|
-
({ config: cfg } = readConfig(dir));
|
|
33
|
-
} catch (e) {
|
|
34
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (json) {
|
|
39
|
-
console.log(JSON.stringify(cfg, null, 2));
|
|
40
|
-
} else {
|
|
41
|
-
// Pretty print for humans
|
|
42
|
-
console.log(JSON.stringify(cfg, null, 2));
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function setConfig(dir, args, { json }) {
|
|
47
|
-
const [dotpath, ...rest] = args;
|
|
48
|
-
if (!dotpath || rest.length === 0) {
|
|
49
|
-
outputError("Usage: tmux-ide config set <dotpath> <value>", "USAGE");
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let cfg;
|
|
54
|
-
try {
|
|
55
|
-
({ config: cfg } = readConfig(dir));
|
|
56
|
-
} catch (e) {
|
|
57
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!isConfigObject(cfg)) {
|
|
62
|
-
outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let value = rest.join(" ");
|
|
67
|
-
// Try to parse as number or boolean
|
|
68
|
-
if (value === "true") value = true;
|
|
69
|
-
else if (value === "false") value = false;
|
|
70
|
-
else if (/^\d+$/.test(value)) value = parseInt(value);
|
|
71
|
-
|
|
72
|
-
setByPath(cfg, dotpath, value);
|
|
73
|
-
writeConfig(dir, cfg);
|
|
74
|
-
|
|
75
|
-
if (json) {
|
|
76
|
-
console.log(JSON.stringify({ ok: true, path: dotpath, value }, null, 2));
|
|
77
|
-
} else {
|
|
78
|
-
console.log(`Set ${dotpath} = ${JSON.stringify(value)}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function addPane(dir, args, { json }) {
|
|
83
|
-
const { row, title, command, size } = parseNamedArgs(args);
|
|
84
|
-
if (row === undefined) {
|
|
85
|
-
outputError(
|
|
86
|
-
"Usage: tmux-ide config add-pane --row <N> --title <T> [--command <C>] [--size <S>]",
|
|
87
|
-
"USAGE",
|
|
88
|
-
);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
let cfg;
|
|
93
|
-
try {
|
|
94
|
-
({ config: cfg } = readConfig(dir));
|
|
95
|
-
} catch (e) {
|
|
96
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!Array.isArray(cfg?.rows)) {
|
|
101
|
-
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const rowIdx = parseIndex(row);
|
|
106
|
-
if (rowIdx == null) {
|
|
107
|
-
outputError(`Invalid row index "${row}"`, "USAGE");
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (!cfg.rows[rowIdx]) {
|
|
112
|
-
outputError(`Row ${rowIdx} does not exist`, "INVALID_ROW");
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!Array.isArray(cfg.rows[rowIdx].panes)) {
|
|
117
|
-
outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG");
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const pane = {};
|
|
122
|
-
if (title) pane.title = title;
|
|
123
|
-
if (command) pane.command = command;
|
|
124
|
-
if (size) pane.size = size;
|
|
125
|
-
|
|
126
|
-
cfg.rows[rowIdx].panes.push(pane);
|
|
127
|
-
writeConfig(dir, cfg);
|
|
128
|
-
|
|
129
|
-
if (json) {
|
|
130
|
-
console.log(JSON.stringify({ ok: true, row: rowIdx, pane }, null, 2));
|
|
131
|
-
} else {
|
|
132
|
-
console.log(`Added pane "${title ?? "untitled"}" to row ${rowIdx}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function removePane(dir, args, { json }) {
|
|
137
|
-
const { row, pane } = parseNamedArgs(args);
|
|
138
|
-
if (row === undefined || pane === undefined) {
|
|
139
|
-
outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE");
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let cfg;
|
|
144
|
-
try {
|
|
145
|
-
({ config: cfg } = readConfig(dir));
|
|
146
|
-
} catch (e) {
|
|
147
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (!Array.isArray(cfg?.rows)) {
|
|
152
|
-
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const rowIdx = parseIndex(row);
|
|
157
|
-
const paneIdx = parseIndex(pane);
|
|
158
|
-
if (rowIdx == null || paneIdx == null) {
|
|
159
|
-
outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE");
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (!Array.isArray(cfg.rows[rowIdx]?.panes)) {
|
|
164
|
-
outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG");
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!cfg.rows[rowIdx].panes[paneIdx]) {
|
|
169
|
-
outputError(`Pane ${paneIdx} in row ${rowIdx} does not exist`, "INVALID_PANE");
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const removed = cfg.rows[rowIdx].panes.splice(paneIdx, 1)[0];
|
|
174
|
-
writeConfig(dir, cfg);
|
|
175
|
-
|
|
176
|
-
if (json) {
|
|
177
|
-
console.log(JSON.stringify({ ok: true, row: rowIdx, pane: paneIdx, removed }, null, 2));
|
|
178
|
-
} else {
|
|
179
|
-
console.log(`Removed pane ${paneIdx} ("${removed.title ?? "untitled"}") from row ${rowIdx}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function addRow(dir, args, { json }) {
|
|
184
|
-
const { size } = parseNamedArgs(args);
|
|
185
|
-
|
|
186
|
-
let cfg;
|
|
187
|
-
try {
|
|
188
|
-
({ config: cfg } = readConfig(dir));
|
|
189
|
-
} catch (e) {
|
|
190
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (!isConfigObject(cfg)) {
|
|
195
|
-
outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
|
|
200
|
-
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const row = { panes: [{ title: "Shell" }] };
|
|
205
|
-
if (size) row.size = size;
|
|
206
|
-
|
|
207
|
-
cfg.rows = cfg.rows ?? [];
|
|
208
|
-
cfg.rows.push(row);
|
|
209
|
-
writeConfig(dir, cfg);
|
|
210
|
-
|
|
211
|
-
const rowIdx = cfg.rows.length - 1;
|
|
212
|
-
if (json) {
|
|
213
|
-
console.log(JSON.stringify({ ok: true, row: rowIdx, size: size ?? null }, null, 2));
|
|
214
|
-
} else {
|
|
215
|
-
console.log(`Added row ${rowIdx}${size ? ` (${size})` : ""}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function enableTeam(dir, args, { json }) {
|
|
220
|
-
const { name } = parseNamedArgs(args);
|
|
221
|
-
|
|
222
|
-
let cfg;
|
|
223
|
-
try {
|
|
224
|
-
({ config: cfg } = readConfig(dir));
|
|
225
|
-
} catch (e) {
|
|
226
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!isConfigObject(cfg)) {
|
|
231
|
-
outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
|
|
236
|
-
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const teamName = name ?? cfg.name ?? "my-team";
|
|
241
|
-
cfg.team = { name: teamName };
|
|
242
|
-
|
|
243
|
-
// Find claude panes and assign roles: first as lead, rest as teammates
|
|
244
|
-
let leadAssigned = false;
|
|
245
|
-
for (const row of cfg.rows ?? []) {
|
|
246
|
-
for (const pane of row.panes ?? []) {
|
|
247
|
-
if (pane.command === "claude" || pane.role === "lead" || pane.role === "teammate") {
|
|
248
|
-
if (!leadAssigned) {
|
|
249
|
-
pane.role = "lead";
|
|
250
|
-
leadAssigned = true;
|
|
251
|
-
} else {
|
|
252
|
-
pane.role = "teammate";
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (!leadAssigned) {
|
|
258
|
-
delete cfg.team;
|
|
259
|
-
outputError("Cannot enable agent team: no Claude panes found", "INVALID_CONFIG");
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
writeConfig(dir, cfg);
|
|
264
|
-
|
|
265
|
-
if (json) {
|
|
266
|
-
console.log(JSON.stringify({ ok: true, team: cfg.team, roles: summarizeRoles(cfg) }, null, 2));
|
|
267
|
-
} else {
|
|
268
|
-
console.log(`Enabled agent team "${teamName}"`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function disableTeam(dir, { json }) {
|
|
273
|
-
let cfg;
|
|
274
|
-
try {
|
|
275
|
-
({ config: cfg } = readConfig(dir));
|
|
276
|
-
} catch (e) {
|
|
277
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (!isConfigObject(cfg)) {
|
|
282
|
-
outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
|
|
287
|
-
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
delete cfg.team;
|
|
292
|
-
for (const row of cfg.rows ?? []) {
|
|
293
|
-
if (!Array.isArray(row?.panes)) continue;
|
|
294
|
-
for (const pane of row.panes ?? []) {
|
|
295
|
-
delete pane.role;
|
|
296
|
-
delete pane.task;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
writeConfig(dir, cfg);
|
|
301
|
-
|
|
302
|
-
if (json) {
|
|
303
|
-
console.log(JSON.stringify({ ok: true, disabled: true }, null, 2));
|
|
304
|
-
} else {
|
|
305
|
-
console.log("Disabled agent team");
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function summarizeRoles(cfg) {
|
|
310
|
-
const roles = [];
|
|
311
|
-
for (let i = 0; i < (cfg.rows ?? []).length; i++) {
|
|
312
|
-
for (let j = 0; j < (cfg.rows[i].panes ?? []).length; j++) {
|
|
313
|
-
const p = cfg.rows[i].panes[j];
|
|
314
|
-
if (p.role) {
|
|
315
|
-
roles.push({ row: i, pane: j, title: p.title ?? null, role: p.role });
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return roles;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function parseNamedArgs(args) {
|
|
323
|
-
const result = {};
|
|
324
|
-
for (let i = 0; i < args.length; i++) {
|
|
325
|
-
if (args[i].startsWith("--") && i + 1 < args.length) {
|
|
326
|
-
const key = args[i].slice(2);
|
|
327
|
-
result[key] = args[i + 1];
|
|
328
|
-
i++;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return result;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function isConfigObject(value) {
|
|
335
|
-
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function parseIndex(value) {
|
|
339
|
-
if (!/^\d+$/.test(String(value))) return null;
|
|
340
|
-
return Number.parseInt(value, 10);
|
|
341
|
-
}
|