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.
Files changed (67) hide show
  1. package/bin/cli.js +14 -14
  2. package/dist/attach.d.ts +3 -0
  3. package/dist/attach.js +14 -0
  4. package/dist/config.d.ts +5 -0
  5. package/dist/config.js +269 -0
  6. package/dist/detect.d.ts +15 -0
  7. package/dist/detect.js +228 -0
  8. package/dist/doctor.d.ts +3 -0
  9. package/dist/doctor.js +66 -0
  10. package/dist/init.d.ts +4 -0
  11. package/dist/init.js +67 -0
  12. package/dist/inspect.d.ts +54 -0
  13. package/dist/inspect.js +118 -0
  14. package/dist/launch.d.ts +22 -0
  15. package/dist/launch.js +174 -0
  16. package/dist/lib/dot-path.d.ts +2 -0
  17. package/dist/lib/dot-path.js +17 -0
  18. package/dist/lib/errors.d.ts +29 -0
  19. package/dist/lib/errors.js +37 -0
  20. package/dist/lib/launch-plan.d.ts +6 -0
  21. package/dist/lib/launch-plan.js +40 -0
  22. package/dist/lib/output.d.ts +9 -0
  23. package/dist/lib/output.js +65 -0
  24. package/dist/lib/session-monitor.d.ts +12 -0
  25. package/dist/lib/session-monitor.js +160 -0
  26. package/dist/lib/session-options.d.ts +13 -0
  27. package/dist/lib/session-options.js +85 -0
  28. package/dist/lib/sizes.d.ts +16 -0
  29. package/dist/lib/sizes.js +36 -0
  30. package/dist/lib/tmux.d.ts +42 -0
  31. package/dist/lib/tmux.js +225 -0
  32. package/dist/lib/yaml-io.d.ts +10 -0
  33. package/dist/lib/yaml-io.js +24 -0
  34. package/dist/ls.d.ts +3 -0
  35. package/dist/ls.js +35 -0
  36. package/dist/restart.d.ts +4 -0
  37. package/dist/restart.js +13 -0
  38. package/dist/status.d.ts +3 -0
  39. package/dist/status.js +29 -0
  40. package/dist/stop.d.ts +3 -0
  41. package/dist/stop.js +21 -0
  42. package/dist/types.d.ts +41 -0
  43. package/dist/types.js +1 -0
  44. package/dist/validate.d.ts +4 -0
  45. package/dist/validate.js +172 -0
  46. package/package.json +16 -10
  47. package/src/attach.js +0 -17
  48. package/src/config.js +0 -341
  49. package/src/detect.js +0 -232
  50. package/src/doctor.js +0 -91
  51. package/src/init.js +0 -73
  52. package/src/inspect.js +0 -127
  53. package/src/launch.js +0 -245
  54. package/src/lib/dot-path.js +0 -16
  55. package/src/lib/errors.js +0 -35
  56. package/src/lib/launch-plan.js +0 -49
  57. package/src/lib/output.js +0 -68
  58. package/src/lib/session-monitor.js +0 -179
  59. package/src/lib/session-options.js +0 -95
  60. package/src/lib/sizes.js +0 -36
  61. package/src/lib/tmux.js +0 -237
  62. package/src/lib/yaml-io.js +0 -26
  63. package/src/ls.js +0 -40
  64. package/src/restart.js +0 -16
  65. package/src/status.js +0 -35
  66. package/src/stop.js +0 -25
  67. 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 {};
@@ -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
+ }
@@ -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,2 @@
1
+ export declare function getByPath(obj: Record<string, any>, path: string): unknown;
2
+ export declare function setByPath(obj: Record<string, any>, path: string, value: unknown): void;
@@ -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 {};