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/init.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve, basename, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
import { detectStack, suggestConfig } from "./detect.js";
|
|
6
|
+
import { outputError, printLayout } from "./lib/output.js";
|
|
7
|
+
export async function init({ template, json, } = {}) {
|
|
8
|
+
const dir = process.cwd();
|
|
9
|
+
const configPath = resolve(dir, "ide.yml");
|
|
10
|
+
if (existsSync(configPath)) {
|
|
11
|
+
outputError("ide.yml already exists in this directory", "EXISTS");
|
|
12
|
+
}
|
|
13
|
+
// If a specific template is requested, use it
|
|
14
|
+
if (template) {
|
|
15
|
+
const templatePath = resolve(__dirname, "..", "templates", `${template}.yml`);
|
|
16
|
+
if (!existsSync(templatePath)) {
|
|
17
|
+
outputError(`Template "${template}" not found`, "NOT_FOUND");
|
|
18
|
+
}
|
|
19
|
+
let content = readFileSync(templatePath, "utf-8");
|
|
20
|
+
const name = basename(dir);
|
|
21
|
+
content = content.replace(/^name: .+/m, `name: ${name}`);
|
|
22
|
+
writeFileSync(configPath, content);
|
|
23
|
+
if (json) {
|
|
24
|
+
console.log(JSON.stringify({ created: true, template, name }));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(`Created ide.yml from "${template}" template for "${name}"`);
|
|
28
|
+
const yaml = (await import("js-yaml")).default;
|
|
29
|
+
printLayout(yaml.load(content));
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Smart detection
|
|
34
|
+
const detected = detectStack(dir);
|
|
35
|
+
const name = basename(dir);
|
|
36
|
+
if (detected.frameworks.length > 0) {
|
|
37
|
+
// Use detected stack to generate config
|
|
38
|
+
const config = suggestConfig(dir, detected);
|
|
39
|
+
const yaml = (await import("js-yaml")).default;
|
|
40
|
+
writeFileSync(configPath, yaml.dump(config, { lineWidth: -1, noRefs: true, quotingType: '"' }));
|
|
41
|
+
const desc = detected.frameworks.join(" + ");
|
|
42
|
+
if (json) {
|
|
43
|
+
console.log(JSON.stringify({ created: true, detected: detected.frameworks, name }));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log(`Detected ${desc}. Created ide.yml for "${name}".`);
|
|
47
|
+
printLayout(config);
|
|
48
|
+
console.log("Edit it to customize, then run: tmux-ide");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Fallback to default template
|
|
53
|
+
const templatePath = resolve(__dirname, "..", "templates", "default.yml");
|
|
54
|
+
let content = readFileSync(templatePath, "utf-8");
|
|
55
|
+
content = content.replace(/^name: .+/m, `name: ${name}`);
|
|
56
|
+
writeFileSync(configPath, content);
|
|
57
|
+
if (json) {
|
|
58
|
+
console.log(JSON.stringify({ created: true, template: "default", name }));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(`Created ide.yml for "${name}"`);
|
|
62
|
+
const yaml = (await import("js-yaml")).default;
|
|
63
|
+
printLayout(yaml.load(content));
|
|
64
|
+
console.log("Edit it to configure your workspace, then run: tmux-ide");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { listPanes } from "./lib/tmux.ts";
|
|
2
|
+
import type { IdeConfig } from "./types.ts";
|
|
3
|
+
interface ResolvedPane {
|
|
4
|
+
index: number;
|
|
5
|
+
title: string | null;
|
|
6
|
+
command: string | null;
|
|
7
|
+
dir: string;
|
|
8
|
+
size: string | null;
|
|
9
|
+
focus: boolean;
|
|
10
|
+
role: string | null;
|
|
11
|
+
task: string | null;
|
|
12
|
+
env: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
interface ResolvedRow {
|
|
15
|
+
index: number;
|
|
16
|
+
size: string | null;
|
|
17
|
+
panes: ResolvedPane[];
|
|
18
|
+
}
|
|
19
|
+
interface Inspection {
|
|
20
|
+
dir: string;
|
|
21
|
+
configPath: string;
|
|
22
|
+
valid: boolean;
|
|
23
|
+
errors: string[];
|
|
24
|
+
session: string;
|
|
25
|
+
before: string | null;
|
|
26
|
+
summary: {
|
|
27
|
+
rows: number;
|
|
28
|
+
panes: number;
|
|
29
|
+
focus: string | null;
|
|
30
|
+
};
|
|
31
|
+
team: IdeConfig["team"] | null;
|
|
32
|
+
theme: IdeConfig["theme"] | null;
|
|
33
|
+
focus: {
|
|
34
|
+
row: number;
|
|
35
|
+
pane: number;
|
|
36
|
+
title: string | null;
|
|
37
|
+
} | null;
|
|
38
|
+
rows: ResolvedRow[];
|
|
39
|
+
rawConfig: IdeConfig;
|
|
40
|
+
tmux: {
|
|
41
|
+
running: boolean;
|
|
42
|
+
panes: ReturnType<typeof listPanes>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export declare function buildInspection(dir: string, { config, configPath, running, panes, }: {
|
|
46
|
+
config: IdeConfig;
|
|
47
|
+
configPath: string;
|
|
48
|
+
running: boolean;
|
|
49
|
+
panes: ReturnType<typeof listPanes>;
|
|
50
|
+
}): Inspection;
|
|
51
|
+
export declare function inspect(targetDir: string | undefined, { json }?: {
|
|
52
|
+
json?: boolean;
|
|
53
|
+
}): Promise<void>;
|
|
54
|
+
export {};
|
package/dist/inspect.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { resolve, basename } from "node:path";
|
|
2
|
+
import { readConfig } from "./lib/yaml-io.js";
|
|
3
|
+
import { validateConfig } from "./validate.js";
|
|
4
|
+
import { outputError } from "./lib/output.js";
|
|
5
|
+
import { getSessionState, listPanes } from "./lib/tmux.js";
|
|
6
|
+
export function buildInspection(dir, { config, configPath, running, panes, }) {
|
|
7
|
+
const errors = validateConfig(config);
|
|
8
|
+
const rows = Array.isArray(config?.rows) ? config.rows : [];
|
|
9
|
+
const resolvedRows = rows.map((row, rowIndex) => ({
|
|
10
|
+
index: rowIndex,
|
|
11
|
+
size: row.size ?? null,
|
|
12
|
+
panes: (Array.isArray(row?.panes) ? row.panes : []).map((pane, paneIndex) => ({
|
|
13
|
+
index: paneIndex,
|
|
14
|
+
title: pane.title ?? null,
|
|
15
|
+
command: pane.command ?? null,
|
|
16
|
+
dir: pane.dir ?? ".",
|
|
17
|
+
size: pane.size ?? null,
|
|
18
|
+
focus: pane.focus === true,
|
|
19
|
+
role: pane.role ?? null,
|
|
20
|
+
task: pane.task ?? null,
|
|
21
|
+
env: pane.env ?? {},
|
|
22
|
+
})),
|
|
23
|
+
}));
|
|
24
|
+
const focusPane = resolvedRows
|
|
25
|
+
.flatMap((row) => row.panes.map((pane) => ({ row: row.index, pane })))
|
|
26
|
+
.find(({ pane }) => pane.focus) ?? null;
|
|
27
|
+
const session = config?.name ?? basename(dir);
|
|
28
|
+
return {
|
|
29
|
+
dir,
|
|
30
|
+
configPath,
|
|
31
|
+
valid: errors.length === 0,
|
|
32
|
+
errors,
|
|
33
|
+
session,
|
|
34
|
+
before: config?.before ?? null,
|
|
35
|
+
summary: {
|
|
36
|
+
rows: resolvedRows.length,
|
|
37
|
+
panes: resolvedRows.reduce((sum, row) => sum + row.panes.length, 0),
|
|
38
|
+
focus: focusPane ? `rows.${focusPane.row}.panes.${focusPane.pane.index}` : null,
|
|
39
|
+
},
|
|
40
|
+
team: config?.team ?? null,
|
|
41
|
+
theme: config?.theme ?? null,
|
|
42
|
+
focus: focusPane
|
|
43
|
+
? {
|
|
44
|
+
row: focusPane.row,
|
|
45
|
+
pane: focusPane.pane.index,
|
|
46
|
+
title: focusPane.pane.title,
|
|
47
|
+
}
|
|
48
|
+
: null,
|
|
49
|
+
rows: resolvedRows,
|
|
50
|
+
rawConfig: config,
|
|
51
|
+
tmux: {
|
|
52
|
+
running,
|
|
53
|
+
panes,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function inspect(targetDir, { json } = {}) {
|
|
58
|
+
const dir = resolve(targetDir ?? ".");
|
|
59
|
+
let config;
|
|
60
|
+
let configPath;
|
|
61
|
+
try {
|
|
62
|
+
({ config, configPath } = readConfig(dir));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const session = config?.name ?? basename(dir);
|
|
69
|
+
const state = getSessionState(session);
|
|
70
|
+
const panes = state.running ? listPanes(session) : [];
|
|
71
|
+
const data = buildInspection(dir, { config, configPath, running: state.running, panes });
|
|
72
|
+
if (json) {
|
|
73
|
+
console.log(JSON.stringify(data, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(`Directory: ${data.dir}`);
|
|
77
|
+
console.log(`Config: ${data.configPath}`);
|
|
78
|
+
console.log(`Valid: ${data.valid ? "yes" : "no"}`);
|
|
79
|
+
console.log(`Session: ${data.session}`);
|
|
80
|
+
console.log(`Running: ${data.tmux.running ? "yes" : "no"}`);
|
|
81
|
+
console.log(`Rows: ${data.summary.rows}`);
|
|
82
|
+
console.log(`Panes: ${data.summary.panes}`);
|
|
83
|
+
console.log(`Team: ${data.team ? data.team.name : "disabled"}`);
|
|
84
|
+
if (data.focus) {
|
|
85
|
+
console.log(`Focus: row ${data.focus.row}, pane ${data.focus.pane}${data.focus.title ? ` (${data.focus.title})` : ""}`);
|
|
86
|
+
}
|
|
87
|
+
if (!data.valid) {
|
|
88
|
+
console.log("\nValidation Errors:");
|
|
89
|
+
for (const error of data.errors) {
|
|
90
|
+
console.log(` - ${error}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
console.log("\nResolved Layout:");
|
|
94
|
+
for (const row of data.rows) {
|
|
95
|
+
console.log(` Row ${row.index}${row.size ? ` (${row.size})` : ""}`);
|
|
96
|
+
for (const pane of row.panes) {
|
|
97
|
+
const parts = [];
|
|
98
|
+
if (pane.title)
|
|
99
|
+
parts.push(pane.title);
|
|
100
|
+
if (pane.command)
|
|
101
|
+
parts.push(`cmd=${pane.command}`);
|
|
102
|
+
if (pane.dir && pane.dir !== ".")
|
|
103
|
+
parts.push(`dir=${pane.dir}`);
|
|
104
|
+
if (pane.role)
|
|
105
|
+
parts.push(`role=${pane.role}`);
|
|
106
|
+
if (pane.focus)
|
|
107
|
+
parts.push("focus");
|
|
108
|
+
console.log(` - pane ${pane.index}: ${parts.join(" | ") || "shell"}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (data.tmux.running && data.tmux.panes.length > 0) {
|
|
112
|
+
console.log("\nLive Panes:");
|
|
113
|
+
for (const pane of data.tmux.panes) {
|
|
114
|
+
const active = pane.active ? " (active)" : "";
|
|
115
|
+
console.log(` ${pane.index}: ${pane.title} [${pane.width}x${pane.height}]${active}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
package/dist/launch.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Row } from "./types.ts";
|
|
2
|
+
interface SplitPaneArgs {
|
|
3
|
+
targetPane: string;
|
|
4
|
+
direction: "vertical" | "horizontal";
|
|
5
|
+
cwd: string;
|
|
6
|
+
percent: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function waitForPaneCommand(targetPane: string, expectedCommands: string[], { attempts, delayMs, getCurrentCommand, sleep, }?: {
|
|
9
|
+
attempts?: number;
|
|
10
|
+
delayMs?: number;
|
|
11
|
+
getCurrentCommand?: (pane: string) => string;
|
|
12
|
+
sleep?: (ms: number) => void;
|
|
13
|
+
}): boolean;
|
|
14
|
+
export declare function buildPaneMap(rows: Row[], dir: string, rootPaneId: string, splitPaneFn: (args: SplitPaneArgs) => string): {
|
|
15
|
+
paneMap: string[][];
|
|
16
|
+
firstPanesOfRows: Set<string>;
|
|
17
|
+
};
|
|
18
|
+
export declare function launch(targetDir: string | undefined, { json, attach }?: {
|
|
19
|
+
json?: boolean;
|
|
20
|
+
attach?: boolean;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
export {};
|
package/dist/launch.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { resolve, dirname } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readConfig, getSessionName } from "./lib/yaml-io.js";
|
|
6
|
+
import { computeSizes, toSplitPercents } from "./lib/sizes.js";
|
|
7
|
+
import { outputError } from "./lib/output.js";
|
|
8
|
+
import { collectPaneStartupPlan } from "./lib/launch-plan.js";
|
|
9
|
+
import { buildSessionOptions } from "./lib/session-options.js";
|
|
10
|
+
import { attachSession, createDetachedSession, getPaneCurrentCommand, getSessionVariable, hasSession, runSessionCommand, selectPane, sendLiteral, setPaneTitle, setSessionEnvironment, setSessionVariable, splitPane, startSessionMonitor, } from "./lib/tmux.js";
|
|
11
|
+
import { validateConfig } from "./validate.js";
|
|
12
|
+
function sleepMs(ms) {
|
|
13
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
14
|
+
}
|
|
15
|
+
function configHash(config) {
|
|
16
|
+
return createHash("sha256").update(JSON.stringify(config)).digest("hex").slice(0, 12);
|
|
17
|
+
}
|
|
18
|
+
export function waitForPaneCommand(targetPane, expectedCommands, { attempts = 20, delayMs = 100, getCurrentCommand = getPaneCurrentCommand, sleep = sleepMs, } = {}) {
|
|
19
|
+
const allowed = new Set(expectedCommands.map((command) => command.toLowerCase()));
|
|
20
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
21
|
+
try {
|
|
22
|
+
const current = getCurrentCommand(targetPane)?.trim().toLowerCase();
|
|
23
|
+
if (current && allowed.has(current))
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Fall through to retry; tmux can briefly report transitional state.
|
|
28
|
+
}
|
|
29
|
+
if (attempt < attempts - 1) {
|
|
30
|
+
sleep(delayMs);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
export function buildPaneMap(rows, dir, rootPaneId, splitPaneFn) {
|
|
36
|
+
const rowSizes = computeSizes(rows);
|
|
37
|
+
const rowSplitPercents = toSplitPercents(rowSizes);
|
|
38
|
+
// Create all rows vertically first so each row spans the full width.
|
|
39
|
+
const rowPaneIds = [rootPaneId];
|
|
40
|
+
for (let rowIdx = 1; rowIdx < rows.length; rowIdx++) {
|
|
41
|
+
const splitFrom = rowPaneIds[rowIdx - 1];
|
|
42
|
+
const newPaneId = splitPaneFn({
|
|
43
|
+
targetPane: splitFrom,
|
|
44
|
+
direction: "vertical",
|
|
45
|
+
cwd: dir,
|
|
46
|
+
percent: rowSplitPercents[rowIdx - 1],
|
|
47
|
+
});
|
|
48
|
+
rowPaneIds.push(newPaneId);
|
|
49
|
+
}
|
|
50
|
+
const paneMap = [];
|
|
51
|
+
const firstPanesOfRows = new Set(rowPaneIds);
|
|
52
|
+
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
|
53
|
+
const row = rows[rowIdx];
|
|
54
|
+
const panes = row.panes ?? [];
|
|
55
|
+
const rowPaneId = rowPaneIds[rowIdx];
|
|
56
|
+
const rowPanes = [rowPaneId];
|
|
57
|
+
const paneSizes = computeSizes(panes);
|
|
58
|
+
const paneSplitPercents = toSplitPercents(paneSizes);
|
|
59
|
+
for (let paneIdx = 1; paneIdx < panes.length; paneIdx++) {
|
|
60
|
+
const pane = panes[paneIdx];
|
|
61
|
+
const targetPane = rowPanes[paneIdx - 1];
|
|
62
|
+
const paneDir = pane.dir ? resolve(dir, pane.dir) : dir;
|
|
63
|
+
const newPaneId = splitPaneFn({
|
|
64
|
+
targetPane,
|
|
65
|
+
direction: "horizontal",
|
|
66
|
+
cwd: paneDir,
|
|
67
|
+
percent: paneSplitPercents[paneIdx - 1],
|
|
68
|
+
});
|
|
69
|
+
rowPanes.push(newPaneId);
|
|
70
|
+
}
|
|
71
|
+
paneMap.push(rowPanes);
|
|
72
|
+
}
|
|
73
|
+
return { paneMap, firstPanesOfRows };
|
|
74
|
+
}
|
|
75
|
+
function loadLaunchConfig(dir) {
|
|
76
|
+
let config;
|
|
77
|
+
try {
|
|
78
|
+
({ config } = readConfig(dir));
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error?.code === "ENOENT") {
|
|
82
|
+
outputError(`No ide.yml found in ${dir}. Run "tmux-ide init" or "tmux-ide detect --write" to create one.`, "CONFIG_NOT_FOUND");
|
|
83
|
+
}
|
|
84
|
+
outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR");
|
|
85
|
+
}
|
|
86
|
+
const errors = validateConfig(config);
|
|
87
|
+
if (errors.length > 0) {
|
|
88
|
+
outputError(`Invalid ide.yml in ${dir}. Run "tmux-ide validate" for details.`, "INVALID_CONFIG");
|
|
89
|
+
}
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
function runBeforeHook(command, dir) {
|
|
93
|
+
if (!command)
|
|
94
|
+
return;
|
|
95
|
+
console.log(`Running: ${command}`);
|
|
96
|
+
try {
|
|
97
|
+
execSync(command, { cwd: dir, stdio: "inherit" });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
outputError(`The before hook failed: ${command}`, "BEFORE_HOOK_FAILED");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function launch(targetDir, { json = false, attach = true } = {}) {
|
|
104
|
+
const dir = resolve(targetDir ?? ".");
|
|
105
|
+
const config = loadLaunchConfig(dir);
|
|
106
|
+
const { name: fallbackName } = getSessionName(dir);
|
|
107
|
+
const session = config.name ?? fallbackName;
|
|
108
|
+
const rows = config.rows;
|
|
109
|
+
const theme = config.theme ?? {};
|
|
110
|
+
const team = config.team ?? null;
|
|
111
|
+
runBeforeHook(config.before, dir);
|
|
112
|
+
// If session already exists, check for config drift and attach
|
|
113
|
+
if (hasSession(session)) {
|
|
114
|
+
const currentHash = configHash(config);
|
|
115
|
+
const storedHash = getSessionVariable(session, "@config_hash");
|
|
116
|
+
const configChanged = Boolean(storedHash && currentHash !== storedHash);
|
|
117
|
+
if (json) {
|
|
118
|
+
console.log(JSON.stringify({ session, running: true, configChanged }));
|
|
119
|
+
}
|
|
120
|
+
else if (configChanged) {
|
|
121
|
+
console.log(`Session "${session}" is running but ide.yml has changed.`);
|
|
122
|
+
console.log(`Run "tmux-ide restart" to apply changes.`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.log(`Session "${session}" is already running. Attaching...`);
|
|
126
|
+
}
|
|
127
|
+
if (attach) {
|
|
128
|
+
attachSession(session);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Get terminal dimensions
|
|
133
|
+
const cols = process.stdout.columns ?? 200;
|
|
134
|
+
const lines = process.stdout.rows ?? 50;
|
|
135
|
+
// Create session with first pane
|
|
136
|
+
const rootPaneId = createDetachedSession(session, dir, { cols, lines });
|
|
137
|
+
// Set agent teams env var if team config is present
|
|
138
|
+
if (team) {
|
|
139
|
+
setSessionEnvironment(session, "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", "1");
|
|
140
|
+
}
|
|
141
|
+
const { paneMap, firstPanesOfRows } = buildPaneMap(rows, dir, rootPaneId, ({ targetPane, direction, cwd, percent }) => splitPane(targetPane, direction, cwd, percent));
|
|
142
|
+
const { focusPane, paneActions } = collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir);
|
|
143
|
+
for (const action of paneActions) {
|
|
144
|
+
if (action.title) {
|
|
145
|
+
setPaneTitle(action.targetPane, action.title);
|
|
146
|
+
}
|
|
147
|
+
if (action.chdir) {
|
|
148
|
+
sendLiteral(action.targetPane, `cd ${action.chdir}`);
|
|
149
|
+
}
|
|
150
|
+
for (const exportCommand of action.exports) {
|
|
151
|
+
sendLiteral(action.targetPane, exportCommand);
|
|
152
|
+
}
|
|
153
|
+
if (action.command) {
|
|
154
|
+
sendLiteral(action.targetPane, action.command);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const command of buildSessionOptions(session, { theme })) {
|
|
158
|
+
runSessionCommand(command);
|
|
159
|
+
}
|
|
160
|
+
// Store config hash for drift detection on re-launch
|
|
161
|
+
setSessionVariable(session, "@config_hash", configHash(config));
|
|
162
|
+
// Start background session monitor (port detection + agent status)
|
|
163
|
+
const monitorScript = resolve(dirname(fileURLToPath(import.meta.url)), "lib", "session-monitor.js");
|
|
164
|
+
startSessionMonitor(session, monitorScript);
|
|
165
|
+
// Focus the correct pane
|
|
166
|
+
selectPane(focusPane);
|
|
167
|
+
// Launch summary
|
|
168
|
+
const totalPanes = rows.reduce((sum, r) => sum + (r.panes?.length ?? 0), 0);
|
|
169
|
+
console.log(`Starting "${session}" (${rows.length} row${rows.length === 1 ? "" : "s"}, ${totalPanes} pane${totalPanes === 1 ? "" : "s"})...`);
|
|
170
|
+
// Attach
|
|
171
|
+
if (attach) {
|
|
172
|
+
attachSession(session);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
export function getByPath(obj, path) {
|
|
3
|
+
return path.split(".").reduce((o, k) => o?.[k], obj);
|
|
4
|
+
}
|
|
5
|
+
export function setByPath(obj, path, value) {
|
|
6
|
+
const keys = path.split(".");
|
|
7
|
+
const last = keys.pop();
|
|
8
|
+
let i = 0;
|
|
9
|
+
const target = keys.reduce((o, k) => {
|
|
10
|
+
const nextKey = keys[i + 1] ?? last;
|
|
11
|
+
if (o[k] === undefined)
|
|
12
|
+
o[k] = /^\d+$/.test(nextKey) ? [] : {};
|
|
13
|
+
i++;
|
|
14
|
+
return o[k];
|
|
15
|
+
}, obj);
|
|
16
|
+
target[last] = value;
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare class IdeError extends Error {
|
|
2
|
+
code: string | undefined;
|
|
3
|
+
exitCode: number;
|
|
4
|
+
constructor(message: string, { code, exitCode, cause }?: {
|
|
5
|
+
code?: string;
|
|
6
|
+
exitCode?: number;
|
|
7
|
+
cause?: Error;
|
|
8
|
+
});
|
|
9
|
+
toJSON(): {
|
|
10
|
+
error: string;
|
|
11
|
+
code: string | undefined;
|
|
12
|
+
cause?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare class ConfigError extends IdeError {
|
|
16
|
+
constructor(message: string, code: string, { cause }?: {
|
|
17
|
+
cause?: Error;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export declare class TmuxError extends IdeError {
|
|
21
|
+
constructor(message: string, code: string, { cause }?: {
|
|
22
|
+
cause?: Error;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export declare class SessionError extends IdeError {
|
|
26
|
+
constructor(message: string, code: string, { cause }?: {
|
|
27
|
+
cause?: Error;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class IdeError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
exitCode;
|
|
4
|
+
constructor(message, { code, exitCode = 1, cause } = {}) {
|
|
5
|
+
super(message, { cause });
|
|
6
|
+
this.name = "IdeError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.exitCode = exitCode;
|
|
9
|
+
}
|
|
10
|
+
toJSON() {
|
|
11
|
+
const obj = {
|
|
12
|
+
error: this.message,
|
|
13
|
+
code: this.code,
|
|
14
|
+
};
|
|
15
|
+
if (this.cause)
|
|
16
|
+
obj.cause = this.cause.message;
|
|
17
|
+
return obj;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class ConfigError extends IdeError {
|
|
21
|
+
constructor(message, code, { cause } = {}) {
|
|
22
|
+
super(message, { code, exitCode: 1, cause });
|
|
23
|
+
this.name = "ConfigError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class TmuxError extends IdeError {
|
|
27
|
+
constructor(message, code, { cause } = {}) {
|
|
28
|
+
super(message, { code, exitCode: 1, cause });
|
|
29
|
+
this.name = "TmuxError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class SessionError extends IdeError {
|
|
33
|
+
constructor(message, code, { cause } = {}) {
|
|
34
|
+
super(message, { code, exitCode: 1, cause });
|
|
35
|
+
this.name = "SessionError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Pane, Row, PaneAction } from "../types.ts";
|
|
2
|
+
export declare function buildPaneCommand(pane: Pane): string | null;
|
|
3
|
+
export declare function collectPaneStartupPlan(rows: Row[], paneMap: string[][], firstPanesOfRows: Set<string>, dir: string): {
|
|
4
|
+
focusPane: string;
|
|
5
|
+
paneActions: PaneAction[];
|
|
6
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
export function buildPaneCommand(pane) {
|
|
3
|
+
if (!pane.command)
|
|
4
|
+
return null;
|
|
5
|
+
return pane.command;
|
|
6
|
+
}
|
|
7
|
+
export function collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir) {
|
|
8
|
+
let focusPane = paneMap[0][0];
|
|
9
|
+
const paneActions = [];
|
|
10
|
+
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
|
11
|
+
const row = rows[rowIdx];
|
|
12
|
+
const panes = row.panes ?? [];
|
|
13
|
+
for (let paneIdx = 0; paneIdx < panes.length; paneIdx++) {
|
|
14
|
+
const pane = panes[paneIdx];
|
|
15
|
+
const tmuxPane = paneMap[rowIdx][paneIdx];
|
|
16
|
+
const action = {
|
|
17
|
+
targetPane: tmuxPane,
|
|
18
|
+
title: pane.title ?? null,
|
|
19
|
+
chdir: null,
|
|
20
|
+
exports: [],
|
|
21
|
+
command: null,
|
|
22
|
+
};
|
|
23
|
+
if (pane.dir && firstPanesOfRows.has(tmuxPane)) {
|
|
24
|
+
action.chdir = resolve(dir, pane.dir);
|
|
25
|
+
}
|
|
26
|
+
if (pane.env && typeof pane.env === "object") {
|
|
27
|
+
action.exports = Object.entries(pane.env).map(([key, value]) => `export ${key}=${value}`);
|
|
28
|
+
}
|
|
29
|
+
const command = buildPaneCommand(pane);
|
|
30
|
+
if (command) {
|
|
31
|
+
action.command = command;
|
|
32
|
+
}
|
|
33
|
+
if (pane.focus) {
|
|
34
|
+
focusPane = tmuxPane;
|
|
35
|
+
}
|
|
36
|
+
paneActions.push(action);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { focusPane, paneActions };
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { IdeError } from "./errors.ts";
|
|
2
|
+
import type { IdeConfig } from "../types.ts";
|
|
3
|
+
export declare function printLayout(config: IdeConfig): void;
|
|
4
|
+
export declare function outputError(message: string, code: string, { exitCode }?: {
|
|
5
|
+
exitCode?: number;
|
|
6
|
+
}): never;
|
|
7
|
+
export declare function printCommandError(error: IdeError, { json }?: {
|
|
8
|
+
json?: boolean;
|
|
9
|
+
}): never;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { IdeError } from "./errors.js";
|
|
2
|
+
export function printLayout(config) {
|
|
3
|
+
const INNER = 40;
|
|
4
|
+
const rows = config.rows ?? [];
|
|
5
|
+
if (rows.length === 0)
|
|
6
|
+
return;
|
|
7
|
+
for (let r = 0; r < rows.length; r++) {
|
|
8
|
+
const panes = rows[r].panes ?? [];
|
|
9
|
+
const count = panes.length || 1;
|
|
10
|
+
const widths = [];
|
|
11
|
+
let remaining = INNER;
|
|
12
|
+
for (let i = 0; i < count; i++) {
|
|
13
|
+
const w = i < count - 1 ? Math.floor(INNER / count) : remaining;
|
|
14
|
+
widths.push(w);
|
|
15
|
+
remaining -= w;
|
|
16
|
+
}
|
|
17
|
+
// Top border or mid divider
|
|
18
|
+
if (r === 0) {
|
|
19
|
+
let top = " \u250c";
|
|
20
|
+
for (let i = 0; i < count; i++) {
|
|
21
|
+
top += "\u2500".repeat(widths[i]);
|
|
22
|
+
top += i < count - 1 ? "\u252c" : "\u2510";
|
|
23
|
+
}
|
|
24
|
+
console.log(top);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(" \u251c" + "\u2500".repeat(INNER + count - 1) + "\u2524");
|
|
28
|
+
}
|
|
29
|
+
// Content line
|
|
30
|
+
const sizeLabel = rows[r].size ?? "";
|
|
31
|
+
let line = " \u2502";
|
|
32
|
+
for (let i = 0; i < count; i++) {
|
|
33
|
+
const title = panes[i]?.title ?? "";
|
|
34
|
+
const w = widths[i];
|
|
35
|
+
const pad = Math.max(0, w - title.length);
|
|
36
|
+
const left = Math.floor(pad / 2);
|
|
37
|
+
const right = pad - left;
|
|
38
|
+
line += " ".repeat(left) + title + " ".repeat(right) + "\u2502";
|
|
39
|
+
}
|
|
40
|
+
if (sizeLabel)
|
|
41
|
+
line += " " + sizeLabel;
|
|
42
|
+
console.log(line);
|
|
43
|
+
// Bottom border (last row only)
|
|
44
|
+
if (r === rows.length - 1) {
|
|
45
|
+
let bot = " \u2514";
|
|
46
|
+
for (let i = 0; i < count; i++) {
|
|
47
|
+
bot += "\u2500".repeat(widths[i]);
|
|
48
|
+
bot += i < count - 1 ? "\u2534" : "\u2518";
|
|
49
|
+
}
|
|
50
|
+
console.log(bot);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function outputError(message, code, { exitCode = 1 } = {}) {
|
|
55
|
+
throw new IdeError(message, { code, exitCode });
|
|
56
|
+
}
|
|
57
|
+
export function printCommandError(error, { json = false } = {}) {
|
|
58
|
+
if (json) {
|
|
59
|
+
console.error(JSON.stringify(error.toJSON(), null, 2));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.error(error.message);
|
|
63
|
+
}
|
|
64
|
+
process.exit(error.exitCode ?? 1);
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface MonitorPane {
|
|
2
|
+
id: string;
|
|
3
|
+
pid: string;
|
|
4
|
+
cmd?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function computePortPanes(panes: MonitorPane[], { listeners, tree }?: {
|
|
8
|
+
listeners?: Set<string>;
|
|
9
|
+
tree?: Map<string, string>;
|
|
10
|
+
}): Set<string>;
|
|
11
|
+
export declare function computeAgentStates(panes: MonitorPane[]): Map<string, "busy" | "idle" | null>;
|
|
12
|
+
export {};
|