vde-layout 0.0.4 → 0.0.6
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/README.md +133 -177
- package/dist/index.js +1126 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ import fs from "fs-extra";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import os from "node:os";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
+
import { createInterface } from "node:readline/promises";
|
|
12
|
+
import { stdin, stdout } from "node:process";
|
|
11
13
|
import { execa } from "execa";
|
|
12
14
|
import { createHash } from "node:crypto";
|
|
13
15
|
|
|
@@ -26,7 +28,12 @@ const ErrorCodes = {
|
|
|
26
28
|
NOT_IN_TMUX: "NOT_IN_TMUX",
|
|
27
29
|
TMUX_NOT_FOUND: "TMUX_NOT_FOUND",
|
|
28
30
|
TMUX_NOT_INSTALLED: "TMUX_NOT_INSTALLED",
|
|
29
|
-
UNSUPPORTED_TMUX_VERSION: "UNSUPPORTED_TMUX_VERSION"
|
|
31
|
+
UNSUPPORTED_TMUX_VERSION: "UNSUPPORTED_TMUX_VERSION",
|
|
32
|
+
BACKEND_NOT_FOUND: "BACKEND_NOT_FOUND",
|
|
33
|
+
TERMINAL_COMMAND_FAILED: "TERMINAL_COMMAND_FAILED",
|
|
34
|
+
WEZTERM_NOT_FOUND: "WEZTERM_NOT_FOUND",
|
|
35
|
+
UNSUPPORTED_WEZTERM_VERSION: "UNSUPPORTED_WEZTERM_VERSION",
|
|
36
|
+
USER_CANCELLED: "USER_CANCELLED"
|
|
30
37
|
};
|
|
31
38
|
const createBaseError = (name, message, code, details = {}) => {
|
|
32
39
|
const error = new Error(message);
|
|
@@ -44,6 +51,9 @@ const createValidationError = (message, code, details = {}) => {
|
|
|
44
51
|
const createTmuxError = (message, code, details = {}) => {
|
|
45
52
|
return createBaseError("TmuxError", message, code, details);
|
|
46
53
|
};
|
|
54
|
+
const createEnvironmentError = (message, code, details = {}) => {
|
|
55
|
+
return createBaseError("EnvironmentError", message, code, details);
|
|
56
|
+
};
|
|
47
57
|
const isVDELayoutError = (error) => {
|
|
48
58
|
if (typeof error !== "object" || error === null) return false;
|
|
49
59
|
if (!("code" in error)) return false;
|
|
@@ -73,11 +83,36 @@ const formatters = {
|
|
|
73
83
|
const requiredVersion = error.details.requiredVersion;
|
|
74
84
|
if (typeof requiredVersion !== "string") return "";
|
|
75
85
|
return `\nRequired tmux version: ${requiredVersion} or higher\n`;
|
|
86
|
+
},
|
|
87
|
+
[ErrorCodes.BACKEND_NOT_FOUND]: (error) => {
|
|
88
|
+
const backend = typeof error.details.backend === "string" ? error.details.backend : "terminal backend";
|
|
89
|
+
const binary = typeof error.details.binary === "string" ? error.details.binary : backend;
|
|
90
|
+
const suggestion = backend === "wezterm" ? [
|
|
91
|
+
"",
|
|
92
|
+
`${backend} is required but not installed.`,
|
|
93
|
+
"Install wezterm using your package manager:",
|
|
94
|
+
" - macOS: brew install --cask wezterm",
|
|
95
|
+
" - Ubuntu/Debian: sudo apt-get install wezterm",
|
|
96
|
+
" - Fedora: sudo dnf install wezterm"
|
|
97
|
+
].join("\n") : "";
|
|
98
|
+
return `\nMissing binary: ${binary}${suggestion}`;
|
|
99
|
+
},
|
|
100
|
+
[ErrorCodes.WEZTERM_NOT_FOUND]: () => {
|
|
101
|
+
return "\nwezterm command was not found.\nInstall wezterm using your package manager:\n - macOS: brew install --cask wezterm\n - Ubuntu/Debian: sudo apt-get install wezterm\n - Fedora: sudo dnf install wezterm\n";
|
|
102
|
+
},
|
|
103
|
+
[ErrorCodes.UNSUPPORTED_WEZTERM_VERSION]: (error) => {
|
|
104
|
+
const requiredVersion = typeof error.details.requiredVersion === "string" ? error.details.requiredVersion : "";
|
|
105
|
+
const detected = typeof error.details.detectedVersion === "string" ? error.details.detectedVersion : "";
|
|
106
|
+
const lines = ["", "Unsupported wezterm version detected."];
|
|
107
|
+
if (detected) lines.push(`Detected version: ${detected}`);
|
|
108
|
+
if (requiredVersion) lines.push(`Required version: ${requiredVersion} or higher`);
|
|
109
|
+
return lines.join("\n");
|
|
76
110
|
}
|
|
77
111
|
};
|
|
78
112
|
|
|
79
113
|
//#endregion
|
|
80
114
|
//#region src/models/schema.ts
|
|
115
|
+
const WindowModeSchema = z.enum(["new-window", "current-window"]);
|
|
81
116
|
const TerminalPaneSchema = z.object({
|
|
82
117
|
name: z.string().min(1),
|
|
83
118
|
command: z.string().optional(),
|
|
@@ -102,9 +137,13 @@ const PresetSchema = z.object({
|
|
|
102
137
|
name: z.string().min(1),
|
|
103
138
|
description: z.string().optional(),
|
|
104
139
|
layout: LayoutSchema.optional(),
|
|
105
|
-
command: z.string().optional()
|
|
140
|
+
command: z.string().optional(),
|
|
141
|
+
windowMode: WindowModeSchema.optional()
|
|
142
|
+
});
|
|
143
|
+
const ConfigSchema = z.object({
|
|
144
|
+
defaults: z.object({ windowMode: WindowModeSchema.optional() }).optional(),
|
|
145
|
+
presets: z.record(PresetSchema)
|
|
106
146
|
});
|
|
107
|
-
const ConfigSchema = z.object({ presets: z.record(PresetSchema) });
|
|
108
147
|
|
|
109
148
|
//#endregion
|
|
110
149
|
//#region src/config/validator.ts
|
|
@@ -231,7 +270,7 @@ const createConfigLoader = (options = {}) => {
|
|
|
231
270
|
const config = validateYAML(content);
|
|
232
271
|
mergedConfig = mergeConfigs(mergedConfig, config);
|
|
233
272
|
}
|
|
234
|
-
return mergedConfig;
|
|
273
|
+
return applyDefaults(mergedConfig);
|
|
235
274
|
};
|
|
236
275
|
return {
|
|
237
276
|
loadYAML: async () => {
|
|
@@ -290,10 +329,26 @@ const safeReadFile = async (filePath) => {
|
|
|
290
329
|
}
|
|
291
330
|
};
|
|
292
331
|
const mergeConfigs = (base, override) => {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
332
|
+
const mergedPresets = { ...base.presets };
|
|
333
|
+
for (const [presetKey, overridePreset] of Object.entries(override.presets)) {
|
|
334
|
+
const basePreset = base.presets[presetKey];
|
|
335
|
+
if (basePreset !== void 0 && basePreset.windowMode !== void 0 && overridePreset.windowMode !== void 0 && basePreset.windowMode !== overridePreset.windowMode) console.warn(`[vde-layout] Preset "${presetKey}" windowMode conflict: "${basePreset.windowMode}" overridden by "${overridePreset.windowMode}"`);
|
|
336
|
+
mergedPresets[presetKey] = overridePreset;
|
|
337
|
+
}
|
|
338
|
+
const baseDefaults = base.defaults;
|
|
339
|
+
const overrideDefaults = override.defaults;
|
|
340
|
+
if (baseDefaults?.windowMode !== void 0 && overrideDefaults?.windowMode !== void 0 && baseDefaults.windowMode !== overrideDefaults.windowMode) console.warn(`[vde-layout] defaults.windowMode conflict: "${baseDefaults.windowMode}" overridden by "${overrideDefaults.windowMode}"`);
|
|
341
|
+
const mergedDefaults = baseDefaults !== void 0 || overrideDefaults !== void 0 ? {
|
|
342
|
+
...baseDefaults ?? {},
|
|
343
|
+
...overrideDefaults ?? {}
|
|
344
|
+
} : void 0;
|
|
345
|
+
return mergedDefaults === void 0 ? { presets: mergedPresets } : {
|
|
346
|
+
defaults: mergedDefaults,
|
|
347
|
+
presets: mergedPresets
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
const applyDefaults = (config) => {
|
|
351
|
+
return config;
|
|
297
352
|
};
|
|
298
353
|
|
|
299
354
|
//#endregion
|
|
@@ -333,12 +388,16 @@ const createState = (options = {}) => {
|
|
|
333
388
|
if (typeof firstKey !== "string" || firstKey.length === 0) throw createConfigError("No presets defined", ErrorCodes.PRESET_NOT_FOUND);
|
|
334
389
|
return config.presets[firstKey];
|
|
335
390
|
};
|
|
391
|
+
const getDefaults = () => {
|
|
392
|
+
return ensureConfig().defaults;
|
|
393
|
+
};
|
|
336
394
|
return {
|
|
337
395
|
setConfigPath,
|
|
338
396
|
loadConfig,
|
|
339
397
|
getPreset,
|
|
340
398
|
listPresets,
|
|
341
|
-
getDefaultPreset
|
|
399
|
+
getDefaultPreset,
|
|
400
|
+
getDefaults
|
|
342
401
|
};
|
|
343
402
|
};
|
|
344
403
|
const createPresetManager = (options = {}) => {
|
|
@@ -348,7 +407,57 @@ const createPresetManager = (options = {}) => {
|
|
|
348
407
|
loadConfig: state.loadConfig,
|
|
349
408
|
getPreset: state.getPreset,
|
|
350
409
|
listPresets: state.listPresets,
|
|
351
|
-
getDefaultPreset: state.getDefaultPreset
|
|
410
|
+
getDefaultPreset: state.getDefaultPreset,
|
|
411
|
+
getDefaults: state.getDefaults
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/cli/window-mode.ts
|
|
417
|
+
const resolveWindowMode = ({ cli, preset, defaults }) => {
|
|
418
|
+
if (cli !== void 0) return {
|
|
419
|
+
mode: cli,
|
|
420
|
+
source: "cli"
|
|
421
|
+
};
|
|
422
|
+
if (preset !== void 0) return {
|
|
423
|
+
mode: preset,
|
|
424
|
+
source: "preset"
|
|
425
|
+
};
|
|
426
|
+
if (defaults !== void 0) return {
|
|
427
|
+
mode: defaults,
|
|
428
|
+
source: "defaults"
|
|
429
|
+
};
|
|
430
|
+
return {
|
|
431
|
+
mode: "new-window",
|
|
432
|
+
source: "fallback"
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/cli/user-prompt.ts
|
|
438
|
+
const createPaneKillPrompter = (logger) => {
|
|
439
|
+
return async ({ panesToClose, dryRun }) => {
|
|
440
|
+
if (panesToClose.length === 0) return true;
|
|
441
|
+
const paneList = panesToClose.join(", ");
|
|
442
|
+
if (dryRun) {
|
|
443
|
+
logger.warn(`[DRY RUN] Would close panes: ${paneList}`);
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
logger.warn(`This operation will close the following panes: ${paneList}`);
|
|
447
|
+
if (stdin.isTTY !== true || stdout.isTTY !== true) {
|
|
448
|
+
logger.error("Cannot prompt for confirmation because the terminal is not interactive");
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
const rl = createInterface({
|
|
452
|
+
input: stdin,
|
|
453
|
+
output: stdout
|
|
454
|
+
});
|
|
455
|
+
try {
|
|
456
|
+
const normalized = (await rl.question("Continue? [y/N]: ")).trim().toLowerCase();
|
|
457
|
+
return normalized === "y" || normalized === "yes";
|
|
458
|
+
} finally {
|
|
459
|
+
await rl.close();
|
|
460
|
+
}
|
|
352
461
|
};
|
|
353
462
|
};
|
|
354
463
|
|
|
@@ -406,10 +515,10 @@ const createLogger = (options = {}) => {
|
|
|
406
515
|
|
|
407
516
|
//#endregion
|
|
408
517
|
//#region src/executor/real-executor.ts
|
|
409
|
-
const parseCommand$
|
|
518
|
+
const parseCommand$2 = (commandOrArgs) => {
|
|
410
519
|
return typeof commandOrArgs === "string" ? commandOrArgs.split(" ").filter((segment) => segment.length > 0).slice(1) : commandOrArgs;
|
|
411
520
|
};
|
|
412
|
-
const toCommandString$
|
|
521
|
+
const toCommandString$2 = (args) => {
|
|
413
522
|
return ["tmux", ...args].join(" ");
|
|
414
523
|
};
|
|
415
524
|
const createRealExecutor = (options = {}) => {
|
|
@@ -419,8 +528,8 @@ const createRealExecutor = (options = {}) => {
|
|
|
419
528
|
prefix: "[tmux]"
|
|
420
529
|
});
|
|
421
530
|
const execute = async (commandOrArgs) => {
|
|
422
|
-
const args = parseCommand$
|
|
423
|
-
const commandString = toCommandString$
|
|
531
|
+
const args = parseCommand$2(commandOrArgs);
|
|
532
|
+
const commandString = toCommandString$2(args);
|
|
424
533
|
logger.info(`Executing: ${commandString}`);
|
|
425
534
|
try {
|
|
426
535
|
return (await execa("tmux", args)).stdout;
|
|
@@ -449,10 +558,10 @@ const createRealExecutor = (options = {}) => {
|
|
|
449
558
|
|
|
450
559
|
//#endregion
|
|
451
560
|
//#region src/executor/dry-run-executor.ts
|
|
452
|
-
const parseCommand = (commandOrArgs) => {
|
|
561
|
+
const parseCommand$1 = (commandOrArgs) => {
|
|
453
562
|
return typeof commandOrArgs === "string" ? commandOrArgs.split(" ").filter((segment) => segment.length > 0).slice(1) : commandOrArgs;
|
|
454
563
|
};
|
|
455
|
-
const toCommandString = (args) => {
|
|
564
|
+
const toCommandString$1 = (args) => {
|
|
456
565
|
return ["tmux", ...args].join(" ");
|
|
457
566
|
};
|
|
458
567
|
const createDryRunExecutor = (options = {}) => {
|
|
@@ -462,8 +571,8 @@ const createDryRunExecutor = (options = {}) => {
|
|
|
462
571
|
prefix: "[tmux] [DRY RUN]"
|
|
463
572
|
});
|
|
464
573
|
const execute = async (commandOrArgs) => {
|
|
465
|
-
const args = parseCommand(commandOrArgs);
|
|
466
|
-
const commandString = toCommandString(args);
|
|
574
|
+
const args = parseCommand$1(commandOrArgs);
|
|
575
|
+
const commandString = toCommandString$1(args);
|
|
467
576
|
logger.info(`Would execute: ${commandString}`);
|
|
468
577
|
return "";
|
|
469
578
|
};
|
|
@@ -481,6 +590,130 @@ const createDryRunExecutor = (options = {}) => {
|
|
|
481
590
|
};
|
|
482
591
|
};
|
|
483
592
|
|
|
593
|
+
//#endregion
|
|
594
|
+
//#region src/executor/mock-executor.ts
|
|
595
|
+
const parseCommand = (commandOrArgs) => {
|
|
596
|
+
return typeof commandOrArgs === "string" ? commandOrArgs.split(" ").filter((segment) => segment.length > 0).slice(1) : commandOrArgs;
|
|
597
|
+
};
|
|
598
|
+
const isInTmuxSession = () => {
|
|
599
|
+
return Boolean(process.env.TMUX);
|
|
600
|
+
};
|
|
601
|
+
const toCommandString = (args) => {
|
|
602
|
+
return ["tmux", ...args].join(" ");
|
|
603
|
+
};
|
|
604
|
+
const createMockExecutor = () => {
|
|
605
|
+
let mockPaneCounter = 0;
|
|
606
|
+
let mockPaneIds = ["%0"];
|
|
607
|
+
let executedCommands = [];
|
|
608
|
+
const execute = async (commandOrArgs) => {
|
|
609
|
+
const args = parseCommand(commandOrArgs);
|
|
610
|
+
executedCommands.push(args);
|
|
611
|
+
if (args[0] === "new-window") {
|
|
612
|
+
mockPaneCounter = 0;
|
|
613
|
+
mockPaneIds = ["%0"];
|
|
614
|
+
return "%0";
|
|
615
|
+
}
|
|
616
|
+
if (args.includes("display-message") && args.includes("#{pane_id}")) return mockPaneIds[0] ?? "%0";
|
|
617
|
+
if (args.includes("list-panes") && args.includes("#{pane_id}")) return mockPaneIds.join("\n");
|
|
618
|
+
if (args[0] === "kill-pane" && args.includes("-a")) {
|
|
619
|
+
const targetIndex = args.indexOf("-t");
|
|
620
|
+
const targetPane = (targetIndex >= 0 && targetIndex + 1 < args.length ? args[targetIndex + 1] : mockPaneIds[0]) ?? "%0";
|
|
621
|
+
mockPaneIds = [targetPane];
|
|
622
|
+
const parsedCounter = Number(targetPane.replace("%", ""));
|
|
623
|
+
if (!Number.isNaN(parsedCounter)) mockPaneCounter = parsedCounter;
|
|
624
|
+
return "";
|
|
625
|
+
}
|
|
626
|
+
if (args.includes("split-window")) {
|
|
627
|
+
mockPaneCounter += 1;
|
|
628
|
+
const newPaneId = `%${mockPaneCounter}`;
|
|
629
|
+
mockPaneIds = [...mockPaneIds, newPaneId];
|
|
630
|
+
}
|
|
631
|
+
return "";
|
|
632
|
+
};
|
|
633
|
+
return {
|
|
634
|
+
execute,
|
|
635
|
+
async executeMany(commandsList) {
|
|
636
|
+
for (const args of commandsList) await execute(args);
|
|
637
|
+
},
|
|
638
|
+
isDryRun() {
|
|
639
|
+
return true;
|
|
640
|
+
},
|
|
641
|
+
logCommand() {},
|
|
642
|
+
getExecutedCommands() {
|
|
643
|
+
return executedCommands;
|
|
644
|
+
},
|
|
645
|
+
clearExecutedCommands() {
|
|
646
|
+
executedCommands = [];
|
|
647
|
+
},
|
|
648
|
+
setMockPaneIds(paneIds) {
|
|
649
|
+
mockPaneIds = [...paneIds];
|
|
650
|
+
},
|
|
651
|
+
getPaneIds() {
|
|
652
|
+
return mockPaneIds;
|
|
653
|
+
},
|
|
654
|
+
isInTmuxSession,
|
|
655
|
+
async verifyTmuxEnvironment() {
|
|
656
|
+
if (!isInTmuxSession()) throw createEnvironmentError("Must be run inside a tmux session", ErrorCodes.NOT_IN_TMUX, { hint: "Please start a tmux session and try again" });
|
|
657
|
+
},
|
|
658
|
+
getCommandString: toCommandString,
|
|
659
|
+
async getCurrentSessionName() {
|
|
660
|
+
return "mock-session";
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/tmux/executor.ts
|
|
667
|
+
const createTmuxExecutor = (options = {}) => {
|
|
668
|
+
const executor = resolveExecutor(options);
|
|
669
|
+
const isInTmuxSession$1 = () => {
|
|
670
|
+
return Boolean(process.env.TMUX);
|
|
671
|
+
};
|
|
672
|
+
const verifyTmuxEnvironment = async () => {
|
|
673
|
+
if (!isInTmuxSession$1()) throw createEnvironmentError("Must be run inside a tmux session", ErrorCodes.NOT_IN_TMUX, { hint: "Please start a tmux session and try again" });
|
|
674
|
+
if (executor.isDryRun()) return;
|
|
675
|
+
try {
|
|
676
|
+
await execa("tmux", ["-V"]);
|
|
677
|
+
} catch (_error) {
|
|
678
|
+
throw createEnvironmentError("tmux is not installed", ErrorCodes.TMUX_NOT_FOUND, { hint: "Please install tmux" });
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const execute = async (commandOrArgs) => {
|
|
682
|
+
return executor.execute(commandOrArgs);
|
|
683
|
+
};
|
|
684
|
+
const executeMany = async (commandsList) => {
|
|
685
|
+
for (const command of commandsList) await execute(command);
|
|
686
|
+
};
|
|
687
|
+
const getCommandString = (args) => {
|
|
688
|
+
return ["tmux", ...args].join(" ");
|
|
689
|
+
};
|
|
690
|
+
const getCurrentSessionName = async () => {
|
|
691
|
+
return execute([
|
|
692
|
+
"display-message",
|
|
693
|
+
"-p",
|
|
694
|
+
"#{session_name}"
|
|
695
|
+
]);
|
|
696
|
+
};
|
|
697
|
+
return {
|
|
698
|
+
verifyTmuxEnvironment,
|
|
699
|
+
execute,
|
|
700
|
+
executeMany,
|
|
701
|
+
isInTmuxSession: isInTmuxSession$1,
|
|
702
|
+
getCurrentSessionName,
|
|
703
|
+
getCommandString,
|
|
704
|
+
getExecutor: () => executor
|
|
705
|
+
};
|
|
706
|
+
};
|
|
707
|
+
const resolveExecutor = (options) => {
|
|
708
|
+
if (options.executor) return options.executor;
|
|
709
|
+
if (options.dryRun === true) return createDryRunExecutor({ verbose: options.verbose });
|
|
710
|
+
if (isTestEnvironment()) return createMockExecutor();
|
|
711
|
+
return createRealExecutor({ verbose: options.verbose });
|
|
712
|
+
};
|
|
713
|
+
const isTestEnvironment = () => {
|
|
714
|
+
return process.env.VDE_TEST_MODE === "true" || process.env.NODE_ENV === "test" || process.env.VITEST === "true";
|
|
715
|
+
};
|
|
716
|
+
|
|
484
717
|
//#endregion
|
|
485
718
|
//#region src/core/errors.ts
|
|
486
719
|
const createFunctionalError = (kind, error) => ({
|
|
@@ -501,25 +734,66 @@ const isFunctionalCoreError = (value) => {
|
|
|
501
734
|
//#region src/executor/plan-runner.ts
|
|
502
735
|
const DOUBLE_QUOTE = "\"";
|
|
503
736
|
const ESCAPED_DOUBLE_QUOTE = "\\\"";
|
|
504
|
-
const executePlan = async ({ emission, executor, windowName }) => {
|
|
737
|
+
const executePlan = async ({ emission, executor, windowName, windowMode, onConfirmKill }) => {
|
|
505
738
|
const initialVirtualPaneId = emission.summary.initialPaneId;
|
|
506
739
|
if (typeof initialVirtualPaneId !== "string" || initialVirtualPaneId.length === 0) raiseExecutionError("INVALID_PLAN", {
|
|
507
740
|
message: "Plan emission is missing initial pane metadata",
|
|
508
741
|
path: "plan.initialPaneId"
|
|
509
742
|
});
|
|
510
743
|
const paneMap = /* @__PURE__ */ new Map();
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
744
|
+
const isDryRun = executor.isDryRun();
|
|
745
|
+
let initialPaneId;
|
|
746
|
+
if (windowMode === "current-window") {
|
|
747
|
+
const currentPaneId = await resolveCurrentPaneId({
|
|
748
|
+
executor,
|
|
749
|
+
contextPath: initialVirtualPaneId,
|
|
750
|
+
isDryRun
|
|
751
|
+
});
|
|
752
|
+
const panesToClose = (await listWindowPaneIds(executor, initialVirtualPaneId)).filter((paneId) => paneId !== currentPaneId);
|
|
753
|
+
if (panesToClose.length > 0) {
|
|
754
|
+
let confirmed = true;
|
|
755
|
+
if (onConfirmKill !== void 0) confirmed = await onConfirmKill({
|
|
756
|
+
panesToClose,
|
|
757
|
+
dryRun: isDryRun
|
|
758
|
+
});
|
|
759
|
+
if (confirmed !== true) throw createFunctionalError("execution", {
|
|
760
|
+
code: ErrorCodes.USER_CANCELLED,
|
|
761
|
+
message: "Aborted layout application for current window",
|
|
762
|
+
path: initialVirtualPaneId,
|
|
763
|
+
details: { panes: panesToClose }
|
|
764
|
+
});
|
|
765
|
+
await executeCommand(executor, [
|
|
766
|
+
"kill-pane",
|
|
767
|
+
"-a",
|
|
768
|
+
"-t",
|
|
769
|
+
currentPaneId
|
|
770
|
+
], {
|
|
771
|
+
code: ErrorCodes.TMUX_COMMAND_FAILED,
|
|
772
|
+
message: "Failed to close existing panes",
|
|
773
|
+
path: initialVirtualPaneId,
|
|
774
|
+
details: { command: [
|
|
775
|
+
"kill-pane",
|
|
776
|
+
"-a",
|
|
777
|
+
"-t",
|
|
778
|
+
currentPaneId
|
|
779
|
+
] }
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
initialPaneId = normalizePaneId(currentPaneId);
|
|
783
|
+
} else {
|
|
784
|
+
const newWindowCommand = [
|
|
785
|
+
"new-window",
|
|
786
|
+
"-P",
|
|
787
|
+
"-F",
|
|
788
|
+
"#{pane_id}"
|
|
789
|
+
];
|
|
790
|
+
if (typeof windowName === "string" && windowName.trim().length > 0) newWindowCommand.push("-n", windowName.trim());
|
|
791
|
+
initialPaneId = normalizePaneId(await executeCommand(executor, newWindowCommand, {
|
|
792
|
+
code: ErrorCodes.TMUX_COMMAND_FAILED,
|
|
793
|
+
message: "Failed to create tmux window",
|
|
794
|
+
path: initialVirtualPaneId
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
523
797
|
registerPane(paneMap, initialVirtualPaneId, initialPaneId);
|
|
524
798
|
let executedSteps = 0;
|
|
525
799
|
for (const step of emission.steps) {
|
|
@@ -570,7 +844,7 @@ const executeSplitStep = async ({ step, executor, paneMap }) => {
|
|
|
570
844
|
details: { command: splitCommand }
|
|
571
845
|
});
|
|
572
846
|
const panesAfter = await listPaneIds(executor, step);
|
|
573
|
-
const newPaneId = ensureNonEmpty(findNewPaneId(panesBefore, panesAfter), () => raiseExecutionError("UNKNOWN_PANE", {
|
|
847
|
+
const newPaneId = ensureNonEmpty(findNewPaneId$1(panesBefore, panesAfter), () => raiseExecutionError("UNKNOWN_PANE", {
|
|
574
848
|
message: "Unable to determine newly created pane",
|
|
575
849
|
path: step.id
|
|
576
850
|
}));
|
|
@@ -664,7 +938,27 @@ const executeCommand = async (executor, command, context) => {
|
|
|
664
938
|
});
|
|
665
939
|
}
|
|
666
940
|
};
|
|
667
|
-
const
|
|
941
|
+
const resolveCurrentPaneId = async ({ executor, contextPath, isDryRun }) => {
|
|
942
|
+
const envPaneId = process.env.TMUX_PANE;
|
|
943
|
+
if (typeof envPaneId === "string" && envPaneId.trim().length > 0) return normalizePaneId(envPaneId);
|
|
944
|
+
if (isDryRun) return "%0";
|
|
945
|
+
const paneId = (await executeCommand(executor, [
|
|
946
|
+
"display-message",
|
|
947
|
+
"-p",
|
|
948
|
+
"#{pane_id}"
|
|
949
|
+
], {
|
|
950
|
+
code: ErrorCodes.TMUX_COMMAND_FAILED,
|
|
951
|
+
message: "Failed to resolve current tmux pane",
|
|
952
|
+
path: contextPath
|
|
953
|
+
})).trim();
|
|
954
|
+
if (paneId.length === 0) throw createFunctionalError("execution", {
|
|
955
|
+
code: ErrorCodes.NOT_IN_TMUX_SESSION,
|
|
956
|
+
message: "Unable to determine current tmux pane",
|
|
957
|
+
path: contextPath
|
|
958
|
+
});
|
|
959
|
+
return normalizePaneId(paneId);
|
|
960
|
+
};
|
|
961
|
+
const listWindowPaneIds = async (executor, contextPath) => {
|
|
668
962
|
return (await executeCommand(executor, [
|
|
669
963
|
"list-panes",
|
|
670
964
|
"-F",
|
|
@@ -672,10 +966,13 @@ const listPaneIds = async (executor, step) => {
|
|
|
672
966
|
], {
|
|
673
967
|
code: ErrorCodes.TMUX_COMMAND_FAILED,
|
|
674
968
|
message: "Failed to list tmux panes",
|
|
675
|
-
path:
|
|
969
|
+
path: contextPath
|
|
676
970
|
})).split("\n").map((pane) => pane.trim()).filter((pane) => pane.length > 0);
|
|
677
971
|
};
|
|
678
|
-
const
|
|
972
|
+
const listPaneIds = async (executor, step) => {
|
|
973
|
+
return listWindowPaneIds(executor, step.id);
|
|
974
|
+
};
|
|
975
|
+
const findNewPaneId$1 = (before, after) => {
|
|
679
976
|
const beforeSet = new Set(before);
|
|
680
977
|
return after.find((id) => !beforeSet.has(id));
|
|
681
978
|
};
|
|
@@ -728,6 +1025,749 @@ const raiseExecutionError = (code, error) => {
|
|
|
728
1025
|
});
|
|
729
1026
|
};
|
|
730
1027
|
|
|
1028
|
+
//#endregion
|
|
1029
|
+
//#region src/executor/backends/tmux-backend.ts
|
|
1030
|
+
const createTmuxBackend = (context) => {
|
|
1031
|
+
const tmuxExecutor = createTmuxExecutor({
|
|
1032
|
+
executor: context.executor,
|
|
1033
|
+
verbose: context.verbose,
|
|
1034
|
+
dryRun: context.dryRun
|
|
1035
|
+
});
|
|
1036
|
+
const buildDryRunSteps$1 = (emission) => {
|
|
1037
|
+
return emission.steps.map((step) => ({
|
|
1038
|
+
backend: "tmux",
|
|
1039
|
+
summary: step.summary,
|
|
1040
|
+
command: tmuxExecutor.getCommandString([...step.command])
|
|
1041
|
+
}));
|
|
1042
|
+
};
|
|
1043
|
+
const verifyEnvironment = async () => {
|
|
1044
|
+
if (context.dryRun) return;
|
|
1045
|
+
await tmuxExecutor.verifyTmuxEnvironment();
|
|
1046
|
+
};
|
|
1047
|
+
const applyPlan = async ({ emission, windowMode, windowName }) => {
|
|
1048
|
+
return {
|
|
1049
|
+
executedSteps: (await executePlan({
|
|
1050
|
+
emission,
|
|
1051
|
+
executor: tmuxExecutor.getExecutor(),
|
|
1052
|
+
windowMode,
|
|
1053
|
+
windowName,
|
|
1054
|
+
onConfirmKill: context.prompt
|
|
1055
|
+
})).executedSteps,
|
|
1056
|
+
focusPaneId: emission.summary.focusPaneId
|
|
1057
|
+
};
|
|
1058
|
+
};
|
|
1059
|
+
return {
|
|
1060
|
+
verifyEnvironment,
|
|
1061
|
+
applyPlan,
|
|
1062
|
+
getDryRunSteps: buildDryRunSteps$1
|
|
1063
|
+
};
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
//#endregion
|
|
1067
|
+
//#region src/wezterm/cli.ts
|
|
1068
|
+
const WEZTERM_BINARY = "wezterm";
|
|
1069
|
+
const MINIMUM_VERSION = "20220624-141144-bd1b7c5d";
|
|
1070
|
+
const VERSION_REGEX = /(\d{8})-(\d{6})-([0-9a-fA-F]+)/i;
|
|
1071
|
+
const verifyWeztermAvailability = async () => {
|
|
1072
|
+
let stdout$1;
|
|
1073
|
+
try {
|
|
1074
|
+
stdout$1 = (await execa(WEZTERM_BINARY, ["--version"])).stdout;
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
const execaError = error;
|
|
1077
|
+
if (execaError.code === "ENOENT") throw createEnvironmentError("wezterm is not installed", ErrorCodes.BACKEND_NOT_FOUND, {
|
|
1078
|
+
backend: "wezterm",
|
|
1079
|
+
binary: WEZTERM_BINARY
|
|
1080
|
+
});
|
|
1081
|
+
throw createEnvironmentError("Failed to execute wezterm --version", ErrorCodes.WEZTERM_NOT_FOUND, {
|
|
1082
|
+
backend: "wezterm",
|
|
1083
|
+
binary: WEZTERM_BINARY,
|
|
1084
|
+
stderr: execaError.stderr
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
const detectedVersion = extractVersion(stdout$1);
|
|
1088
|
+
if (detectedVersion === void 0) throw createEnvironmentError("Unable to determine wezterm version", ErrorCodes.UNSUPPORTED_WEZTERM_VERSION, {
|
|
1089
|
+
requiredVersion: MINIMUM_VERSION,
|
|
1090
|
+
detectedVersion: stdout$1.trim()
|
|
1091
|
+
});
|
|
1092
|
+
if (!isVersionSupported(detectedVersion, MINIMUM_VERSION)) throw createEnvironmentError("Unsupported wezterm version", ErrorCodes.UNSUPPORTED_WEZTERM_VERSION, {
|
|
1093
|
+
requiredVersion: MINIMUM_VERSION,
|
|
1094
|
+
detectedVersion
|
|
1095
|
+
});
|
|
1096
|
+
return { version: detectedVersion };
|
|
1097
|
+
};
|
|
1098
|
+
const runWeztermCli = async (args, errorContext) => {
|
|
1099
|
+
try {
|
|
1100
|
+
return (await execa(WEZTERM_BINARY, ["cli", ...args])).stdout;
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
const execaError = error;
|
|
1103
|
+
throw createFunctionalError("execution", {
|
|
1104
|
+
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
|
|
1105
|
+
message: errorContext.message,
|
|
1106
|
+
path: errorContext.path,
|
|
1107
|
+
details: {
|
|
1108
|
+
command: [
|
|
1109
|
+
WEZTERM_BINARY,
|
|
1110
|
+
"cli",
|
|
1111
|
+
...args
|
|
1112
|
+
],
|
|
1113
|
+
stderr: execaError.stderr,
|
|
1114
|
+
exitCode: execaError.exitCode,
|
|
1115
|
+
backend: "wezterm",
|
|
1116
|
+
...errorContext.details ?? {}
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
const listWeztermWindows = async () => {
|
|
1122
|
+
const stdout$1 = await runWeztermCli([
|
|
1123
|
+
"list",
|
|
1124
|
+
"--format",
|
|
1125
|
+
"json"
|
|
1126
|
+
], { message: "Failed to list wezterm panes" });
|
|
1127
|
+
const result = parseListResult(stdout$1);
|
|
1128
|
+
if (result === void 0) throw createFunctionalError("execution", {
|
|
1129
|
+
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
|
|
1130
|
+
message: "Invalid wezterm list output",
|
|
1131
|
+
details: { stdout: stdout$1 }
|
|
1132
|
+
});
|
|
1133
|
+
return result;
|
|
1134
|
+
};
|
|
1135
|
+
const killWeztermPane = async (paneId) => {
|
|
1136
|
+
await runWeztermCli([
|
|
1137
|
+
"kill-pane",
|
|
1138
|
+
"--pane-id",
|
|
1139
|
+
paneId
|
|
1140
|
+
], {
|
|
1141
|
+
message: `Failed to kill wezterm pane ${paneId}`,
|
|
1142
|
+
path: paneId
|
|
1143
|
+
});
|
|
1144
|
+
};
|
|
1145
|
+
const extractVersion = (raw) => {
|
|
1146
|
+
const match = raw.match(VERSION_REGEX);
|
|
1147
|
+
if (!match) return;
|
|
1148
|
+
const date = match[1];
|
|
1149
|
+
const time = match[2];
|
|
1150
|
+
const commit = match[3];
|
|
1151
|
+
if (date === void 0 || time === void 0 || commit === void 0) return;
|
|
1152
|
+
return `${date}-${time}-${commit.toLowerCase()}`;
|
|
1153
|
+
};
|
|
1154
|
+
const isVersionSupported = (detected, minimum) => {
|
|
1155
|
+
const parse$1 = (version) => {
|
|
1156
|
+
const match = version.match(VERSION_REGEX);
|
|
1157
|
+
if (!match) return;
|
|
1158
|
+
const date = match[1];
|
|
1159
|
+
const time = match[2];
|
|
1160
|
+
const commit = match[3];
|
|
1161
|
+
if (date === void 0 || time === void 0 || commit === void 0) return;
|
|
1162
|
+
const build = Number(`${date}${time}`);
|
|
1163
|
+
if (Number.isNaN(build)) return;
|
|
1164
|
+
return {
|
|
1165
|
+
build,
|
|
1166
|
+
commit: commit.toLowerCase()
|
|
1167
|
+
};
|
|
1168
|
+
};
|
|
1169
|
+
const detectedInfo = parse$1(detected);
|
|
1170
|
+
const minimumInfo = parse$1(minimum);
|
|
1171
|
+
if (detectedInfo === void 0 || minimumInfo === void 0) return false;
|
|
1172
|
+
if (detectedInfo.build > minimumInfo.build) return true;
|
|
1173
|
+
if (detectedInfo.build < minimumInfo.build) return false;
|
|
1174
|
+
return detectedInfo.commit >= minimumInfo.commit;
|
|
1175
|
+
};
|
|
1176
|
+
const toIdString = (value) => {
|
|
1177
|
+
if (typeof value === "string") return value;
|
|
1178
|
+
if (typeof value === "number") return value.toString();
|
|
1179
|
+
};
|
|
1180
|
+
const isNonEmptyString = (value) => {
|
|
1181
|
+
return typeof value === "string" && value.length > 0;
|
|
1182
|
+
};
|
|
1183
|
+
const parseListResult = (stdout$1) => {
|
|
1184
|
+
try {
|
|
1185
|
+
const parsed = JSON.parse(stdout$1);
|
|
1186
|
+
if (Array.isArray(parsed)) {
|
|
1187
|
+
const windowMap = /* @__PURE__ */ new Map();
|
|
1188
|
+
for (const entry of parsed) {
|
|
1189
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
1190
|
+
const listEntry = entry;
|
|
1191
|
+
const windowIdRaw = toIdString(listEntry.window_id);
|
|
1192
|
+
const paneIdRaw = toIdString(listEntry.pane_id);
|
|
1193
|
+
const tabIdRaw = toIdString(listEntry.tab_id) ?? windowIdRaw;
|
|
1194
|
+
if (!isNonEmptyString(windowIdRaw) || !isNonEmptyString(tabIdRaw) || !isNonEmptyString(paneIdRaw)) continue;
|
|
1195
|
+
const windowId = windowIdRaw;
|
|
1196
|
+
const tabId = tabIdRaw;
|
|
1197
|
+
const paneId = paneIdRaw;
|
|
1198
|
+
let windowRecord = windowMap.get(windowId);
|
|
1199
|
+
if (!windowRecord) {
|
|
1200
|
+
windowRecord = {
|
|
1201
|
+
windowId,
|
|
1202
|
+
isActive: false,
|
|
1203
|
+
tabs: /* @__PURE__ */ new Map()
|
|
1204
|
+
};
|
|
1205
|
+
windowMap.set(windowId, windowRecord);
|
|
1206
|
+
}
|
|
1207
|
+
let tabRecord = windowRecord.tabs.get(tabId);
|
|
1208
|
+
if (!tabRecord) {
|
|
1209
|
+
tabRecord = {
|
|
1210
|
+
tabId,
|
|
1211
|
+
isActive: false,
|
|
1212
|
+
panes: []
|
|
1213
|
+
};
|
|
1214
|
+
windowRecord.tabs.set(tabId, tabRecord);
|
|
1215
|
+
}
|
|
1216
|
+
const pane = {
|
|
1217
|
+
paneId,
|
|
1218
|
+
isActive: listEntry.is_active === true
|
|
1219
|
+
};
|
|
1220
|
+
windowRecord.isActive ||= listEntry.is_active === true;
|
|
1221
|
+
tabRecord.isActive ||= listEntry.is_active === true;
|
|
1222
|
+
tabRecord.panes.push(pane);
|
|
1223
|
+
}
|
|
1224
|
+
return { windows: Array.from(windowMap.values()).map((windowRecord) => ({
|
|
1225
|
+
windowId: windowRecord.windowId,
|
|
1226
|
+
isActive: windowRecord.isActive,
|
|
1227
|
+
tabs: Array.from(windowRecord.tabs.values()).map((tabRecord) => ({
|
|
1228
|
+
tabId: tabRecord.tabId,
|
|
1229
|
+
isActive: tabRecord.isActive,
|
|
1230
|
+
panes: tabRecord.panes.map((pane) => ({
|
|
1231
|
+
paneId: pane.paneId,
|
|
1232
|
+
isActive: pane.isActive
|
|
1233
|
+
}))
|
|
1234
|
+
}))
|
|
1235
|
+
})) };
|
|
1236
|
+
}
|
|
1237
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
1238
|
+
const candidate = parsed;
|
|
1239
|
+
const windows = Array.isArray(candidate.windows) ? candidate.windows : [];
|
|
1240
|
+
const mappedWindows = [];
|
|
1241
|
+
for (const window of windows) {
|
|
1242
|
+
if (typeof window !== "object" || window === null) continue;
|
|
1243
|
+
const rawWindow = window;
|
|
1244
|
+
const windowIdRaw = toIdString(rawWindow.window_id);
|
|
1245
|
+
if (!isNonEmptyString(windowIdRaw)) continue;
|
|
1246
|
+
const windowId = windowIdRaw;
|
|
1247
|
+
const mappedTabs = [];
|
|
1248
|
+
const tabs = Array.isArray(rawWindow.tabs) ? rawWindow.tabs : [];
|
|
1249
|
+
for (const tab of tabs) {
|
|
1250
|
+
if (typeof tab !== "object" || tab === null) continue;
|
|
1251
|
+
const rawTab = tab;
|
|
1252
|
+
const tabIdRaw = toIdString(rawTab.tab_id);
|
|
1253
|
+
if (!isNonEmptyString(tabIdRaw)) continue;
|
|
1254
|
+
const tabId = tabIdRaw;
|
|
1255
|
+
const paneRecords = Array.isArray(rawTab.panes) ? rawTab.panes : [];
|
|
1256
|
+
const mappedPanes = [];
|
|
1257
|
+
for (const pane of paneRecords) {
|
|
1258
|
+
if (typeof pane !== "object" || pane === null) continue;
|
|
1259
|
+
const rawPane = pane;
|
|
1260
|
+
const paneIdRaw = toIdString(rawPane.pane_id);
|
|
1261
|
+
if (!isNonEmptyString(paneIdRaw)) continue;
|
|
1262
|
+
const paneId = paneIdRaw;
|
|
1263
|
+
mappedPanes.push({
|
|
1264
|
+
paneId,
|
|
1265
|
+
isActive: rawPane.is_active === true
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
mappedTabs.push({
|
|
1269
|
+
tabId,
|
|
1270
|
+
isActive: rawTab.is_active === true,
|
|
1271
|
+
panes: mappedPanes
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
mappedWindows.push({
|
|
1275
|
+
windowId,
|
|
1276
|
+
isActive: rawWindow.is_active === true,
|
|
1277
|
+
tabs: mappedTabs
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
return { windows: mappedWindows };
|
|
1281
|
+
}
|
|
1282
|
+
return;
|
|
1283
|
+
} catch {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
//#endregion
|
|
1289
|
+
//#region src/executor/backends/wezterm-backend.ts
|
|
1290
|
+
const PANE_REGISTRATION_RETRIES = 5;
|
|
1291
|
+
const PANE_REGISTRATION_DELAY_MS = 100;
|
|
1292
|
+
const ensureVirtualPaneId = (emission) => {
|
|
1293
|
+
const { initialPaneId } = emission.summary;
|
|
1294
|
+
if (typeof initialPaneId !== "string" || initialPaneId.length === 0) throw createFunctionalError("execution", {
|
|
1295
|
+
code: ErrorCodes.INVALID_PANE,
|
|
1296
|
+
message: "Plan emission is missing initial pane metadata",
|
|
1297
|
+
path: "plan.initialPaneId"
|
|
1298
|
+
});
|
|
1299
|
+
return initialPaneId;
|
|
1300
|
+
};
|
|
1301
|
+
const registerPaneWithAncestors = (map, virtualId, realId) => {
|
|
1302
|
+
map.set(virtualId, realId);
|
|
1303
|
+
let ancestor = virtualId;
|
|
1304
|
+
while (ancestor.includes(".")) {
|
|
1305
|
+
ancestor = ancestor.slice(0, ancestor.lastIndexOf("."));
|
|
1306
|
+
if (!map.has(ancestor)) map.set(ancestor, realId);
|
|
1307
|
+
else break;
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
const resolveRealPaneId = (paneMap, virtualId, context) => {
|
|
1311
|
+
const direct = paneMap.get(virtualId);
|
|
1312
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
1313
|
+
let ancestor = virtualId;
|
|
1314
|
+
while (ancestor.includes(".")) {
|
|
1315
|
+
ancestor = ancestor.slice(0, ancestor.lastIndexOf("."));
|
|
1316
|
+
const candidate = paneMap.get(ancestor);
|
|
1317
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
1318
|
+
paneMap.set(virtualId, candidate);
|
|
1319
|
+
return candidate;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
for (const [key, value] of paneMap.entries()) if (key.startsWith(`${virtualId}.`)) {
|
|
1323
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1324
|
+
paneMap.set(virtualId, value);
|
|
1325
|
+
return value;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
throw createFunctionalError("execution", {
|
|
1329
|
+
code: ErrorCodes.INVALID_PANE,
|
|
1330
|
+
message: `Unknown wezterm pane mapping for ${virtualId}`,
|
|
1331
|
+
path: context.stepId
|
|
1332
|
+
});
|
|
1333
|
+
};
|
|
1334
|
+
const buildDryRunSteps = (emission) => {
|
|
1335
|
+
const steps = [];
|
|
1336
|
+
for (const step of emission.steps) {
|
|
1337
|
+
if (step.kind === "split") {
|
|
1338
|
+
const target = step.targetPaneId ?? "<unknown>";
|
|
1339
|
+
const args = buildSplitArguments({
|
|
1340
|
+
targetPaneId: target,
|
|
1341
|
+
percent: extractPercent(step.command),
|
|
1342
|
+
horizontal: isHorizontalSplit(step.command)
|
|
1343
|
+
});
|
|
1344
|
+
steps.push({
|
|
1345
|
+
backend: "wezterm",
|
|
1346
|
+
summary: step.summary,
|
|
1347
|
+
command: `wezterm cli ${args.join(" ")}`
|
|
1348
|
+
});
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
if (step.kind === "focus") {
|
|
1352
|
+
const target = step.targetPaneId ?? "<unknown>";
|
|
1353
|
+
steps.push({
|
|
1354
|
+
backend: "wezterm",
|
|
1355
|
+
summary: step.summary,
|
|
1356
|
+
command: `wezterm cli activate-pane --pane-id ${target}`
|
|
1357
|
+
});
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
steps.push({
|
|
1361
|
+
backend: "wezterm",
|
|
1362
|
+
summary: step.summary,
|
|
1363
|
+
command: `wezterm cli # ${step.command.join(" ")}`
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
for (const terminal of emission.terminals) {
|
|
1367
|
+
const paneId = terminal.virtualPaneId;
|
|
1368
|
+
if (typeof terminal.cwd === "string" && terminal.cwd.length > 0) {
|
|
1369
|
+
const cwdCommand = `cd "${terminal.cwd.split("\"").join("\\\"")}"`;
|
|
1370
|
+
steps.push({
|
|
1371
|
+
backend: "wezterm",
|
|
1372
|
+
summary: `set cwd for ${paneId}`,
|
|
1373
|
+
command: `wezterm cli send-text --pane-id ${paneId} --no-paste -- '${cwdCommand.replace(/'/g, "\\'")}'`
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
if (terminal.env !== void 0) for (const [key, value] of Object.entries(terminal.env)) {
|
|
1377
|
+
const envCommand = `export ${key}="${String(value).split("\"").join("\\\"")}"`;
|
|
1378
|
+
steps.push({
|
|
1379
|
+
backend: "wezterm",
|
|
1380
|
+
summary: `set env ${key} for ${paneId}`,
|
|
1381
|
+
command: `wezterm cli send-text --pane-id ${paneId} --no-paste -- '${envCommand.replace(/'/g, "\\'")}'`
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
if (typeof terminal.command === "string" && terminal.command.length > 0) steps.push({
|
|
1385
|
+
backend: "wezterm",
|
|
1386
|
+
summary: `run command for ${paneId}`,
|
|
1387
|
+
command: `wezterm cli send-text --pane-id ${paneId} --no-paste -- '${terminal.command.replace(/'/g, "\\'")}'`
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
return steps;
|
|
1391
|
+
};
|
|
1392
|
+
const resolveCurrentWindow = async (context) => {
|
|
1393
|
+
const activeWindow = context.list.windows.find((window) => window.isActive) ?? context.list.windows[0];
|
|
1394
|
+
if (!activeWindow) throw createFunctionalError("execution", {
|
|
1395
|
+
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
|
|
1396
|
+
message: "No active wezterm window detected",
|
|
1397
|
+
details: { hint: "Launch wezterm and ensure a window is focused, or run with --new-window." }
|
|
1398
|
+
});
|
|
1399
|
+
const activeTab = activeWindow.tabs.find((tab) => tab.isActive) ?? activeWindow.tabs[0];
|
|
1400
|
+
if (!activeTab) throw createFunctionalError("execution", {
|
|
1401
|
+
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
|
|
1402
|
+
message: "No active wezterm tab detected",
|
|
1403
|
+
path: activeWindow.windowId,
|
|
1404
|
+
details: { hint: "Ensure a wezterm tab is focused before using --current-window." }
|
|
1405
|
+
});
|
|
1406
|
+
const activePane = activeTab.panes.find((pane) => pane.isActive) ?? activeTab.panes[0];
|
|
1407
|
+
if (!activePane) throw createFunctionalError("execution", {
|
|
1408
|
+
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
|
|
1409
|
+
message: "No active wezterm pane detected",
|
|
1410
|
+
path: activeTab.tabId,
|
|
1411
|
+
details: { hint: "Ensure a wezterm pane is active before using --current-window." }
|
|
1412
|
+
});
|
|
1413
|
+
const panesToClose = activeTab.panes.filter((pane) => pane.paneId !== activePane.paneId).map((pane) => pane.paneId);
|
|
1414
|
+
if (panesToClose.length > 0) {
|
|
1415
|
+
let confirmed = true;
|
|
1416
|
+
if (context.prompt) confirmed = await context.prompt({
|
|
1417
|
+
panesToClose,
|
|
1418
|
+
dryRun: context.dryRun
|
|
1419
|
+
});
|
|
1420
|
+
if (confirmed !== true) throw createFunctionalError("execution", {
|
|
1421
|
+
code: ErrorCodes.USER_CANCELLED,
|
|
1422
|
+
message: "Aborted layout application for current wezterm window",
|
|
1423
|
+
path: activePane.paneId,
|
|
1424
|
+
details: { panes: panesToClose }
|
|
1425
|
+
});
|
|
1426
|
+
for (const paneId of panesToClose) {
|
|
1427
|
+
context.logCommand([
|
|
1428
|
+
"kill-pane",
|
|
1429
|
+
"--pane-id",
|
|
1430
|
+
paneId
|
|
1431
|
+
]);
|
|
1432
|
+
await killWeztermPane(paneId);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return {
|
|
1436
|
+
paneId: activePane.paneId,
|
|
1437
|
+
windowId: activeWindow.windowId,
|
|
1438
|
+
panesToClose
|
|
1439
|
+
};
|
|
1440
|
+
};
|
|
1441
|
+
const findActiveWindow = (list) => {
|
|
1442
|
+
return list.windows.find((window) => window.isActive) ?? list.windows[0];
|
|
1443
|
+
};
|
|
1444
|
+
const findWindowContainingPane = (list, paneId) => {
|
|
1445
|
+
for (const window of list.windows) for (const tab of window.tabs) for (const pane of tab.panes) if (pane.paneId === paneId) return window.windowId;
|
|
1446
|
+
};
|
|
1447
|
+
const delay = (ms) => {
|
|
1448
|
+
return new Promise((resolve) => {
|
|
1449
|
+
setTimeout(resolve, ms);
|
|
1450
|
+
});
|
|
1451
|
+
};
|
|
1452
|
+
const waitForPaneRegistration = async ({ paneId, listWindows, windowHint }) => {
|
|
1453
|
+
for (let attempt = 0; attempt < PANE_REGISTRATION_RETRIES; attempt += 1) {
|
|
1454
|
+
const snapshot = await listWindows();
|
|
1455
|
+
if (typeof windowHint === "string") try {
|
|
1456
|
+
if (collectPaneIdsForWindow(snapshot, windowHint).has(paneId)) return windowHint;
|
|
1457
|
+
} catch {}
|
|
1458
|
+
const located = findWindowContainingPane(snapshot, paneId);
|
|
1459
|
+
if (typeof located === "string" && located.length > 0) return located;
|
|
1460
|
+
if (attempt < PANE_REGISTRATION_RETRIES - 1) await delay(PANE_REGISTRATION_DELAY_MS);
|
|
1461
|
+
}
|
|
1462
|
+
throw createFunctionalError("execution", {
|
|
1463
|
+
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
|
|
1464
|
+
message: "Unable to locate spawned wezterm window",
|
|
1465
|
+
details: {
|
|
1466
|
+
paneId,
|
|
1467
|
+
hint: "Verify that wezterm is running and the CLI client can connect."
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
};
|
|
1471
|
+
const resolveInitialPane = async ({ windowMode, prompt, dryRun, listWindows, runCommand, logCommand }) => {
|
|
1472
|
+
if (windowMode === "current-window") {
|
|
1473
|
+
const list = await listWindows();
|
|
1474
|
+
return resolveCurrentWindow({
|
|
1475
|
+
list,
|
|
1476
|
+
prompt,
|
|
1477
|
+
dryRun,
|
|
1478
|
+
logCommand
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
const existing = await listWindows();
|
|
1482
|
+
const activeWindow = findActiveWindow(existing);
|
|
1483
|
+
if (activeWindow) {
|
|
1484
|
+
const spawnOutput$1 = await runCommand([
|
|
1485
|
+
"spawn",
|
|
1486
|
+
"--window-id",
|
|
1487
|
+
activeWindow.windowId
|
|
1488
|
+
], { message: "Failed to spawn wezterm tab" });
|
|
1489
|
+
const paneId$1 = extractSpawnPaneId(spawnOutput$1);
|
|
1490
|
+
if (paneId$1.length === 0) throw createFunctionalError("execution", {
|
|
1491
|
+
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
|
|
1492
|
+
message: "wezterm spawn did not return a pane id",
|
|
1493
|
+
details: { stdout: spawnOutput$1 }
|
|
1494
|
+
});
|
|
1495
|
+
const windowId = await waitForPaneRegistration({
|
|
1496
|
+
paneId: paneId$1,
|
|
1497
|
+
listWindows,
|
|
1498
|
+
windowHint: activeWindow.windowId
|
|
1499
|
+
});
|
|
1500
|
+
return {
|
|
1501
|
+
paneId: paneId$1,
|
|
1502
|
+
windowId
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
const spawnOutput = await runCommand(["spawn", "--new-window"], { message: "Failed to spawn wezterm window" });
|
|
1506
|
+
const paneId = extractSpawnPaneId(spawnOutput);
|
|
1507
|
+
if (paneId.length === 0) throw createFunctionalError("execution", {
|
|
1508
|
+
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
|
|
1509
|
+
message: "wezterm spawn did not return a pane id",
|
|
1510
|
+
details: { stdout: spawnOutput }
|
|
1511
|
+
});
|
|
1512
|
+
const newWindowId = await waitForPaneRegistration({
|
|
1513
|
+
paneId,
|
|
1514
|
+
listWindows
|
|
1515
|
+
});
|
|
1516
|
+
return {
|
|
1517
|
+
paneId,
|
|
1518
|
+
windowId: newWindowId
|
|
1519
|
+
};
|
|
1520
|
+
};
|
|
1521
|
+
const collectPaneIdsForWindow = (list, windowId) => {
|
|
1522
|
+
const targetWindow = list.windows.find((window) => window.windowId === windowId);
|
|
1523
|
+
if (!targetWindow) throw createFunctionalError("execution", {
|
|
1524
|
+
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
|
|
1525
|
+
message: `Wezterm window ${windowId} not found`,
|
|
1526
|
+
details: { windowId }
|
|
1527
|
+
});
|
|
1528
|
+
const paneIds = targetWindow.tabs.flatMap((tab) => tab.panes.map((pane) => pane.paneId));
|
|
1529
|
+
return new Set(paneIds);
|
|
1530
|
+
};
|
|
1531
|
+
const extractSpawnPaneId = (output) => {
|
|
1532
|
+
const trimmed = output.trim();
|
|
1533
|
+
if (trimmed.length === 0) return "";
|
|
1534
|
+
const tokens = (trimmed.split("\n").pop() ?? "").split(/\s+/).filter((segment) => segment.length > 0);
|
|
1535
|
+
if (tokens.length === 0) return "";
|
|
1536
|
+
const [paneId] = tokens;
|
|
1537
|
+
if (typeof paneId !== "string") return "";
|
|
1538
|
+
return paneId.trim();
|
|
1539
|
+
};
|
|
1540
|
+
const findNewPaneId = (before, after) => {
|
|
1541
|
+
for (const paneId of after) if (!before.has(paneId)) return paneId;
|
|
1542
|
+
};
|
|
1543
|
+
const isHorizontalSplit = (command) => {
|
|
1544
|
+
return command.includes("-h");
|
|
1545
|
+
};
|
|
1546
|
+
const extractPercent = (command) => {
|
|
1547
|
+
const index = command.findIndex((segment) => segment === "-p");
|
|
1548
|
+
if (index >= 0 && index + 1 < command.length) {
|
|
1549
|
+
const value = command[index + 1];
|
|
1550
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
1551
|
+
}
|
|
1552
|
+
return "50";
|
|
1553
|
+
};
|
|
1554
|
+
const buildSplitArguments = (params) => {
|
|
1555
|
+
return [
|
|
1556
|
+
"split-pane",
|
|
1557
|
+
params.horizontal ? "--right" : "--bottom",
|
|
1558
|
+
"--percent",
|
|
1559
|
+
params.percent,
|
|
1560
|
+
"--pane-id",
|
|
1561
|
+
params.targetPaneId
|
|
1562
|
+
];
|
|
1563
|
+
};
|
|
1564
|
+
const applyFocusStep = async ({ step, paneMap, runCommand }) => {
|
|
1565
|
+
const targetVirtualId = step.targetPaneId;
|
|
1566
|
+
if (typeof targetVirtualId !== "string" || targetVirtualId.length === 0) throw createFunctionalError("execution", {
|
|
1567
|
+
code: ErrorCodes.INVALID_PANE,
|
|
1568
|
+
message: "Focus step missing target pane metadata",
|
|
1569
|
+
path: step.id
|
|
1570
|
+
});
|
|
1571
|
+
const targetRealId = resolveRealPaneId(paneMap, targetVirtualId, { stepId: step.id });
|
|
1572
|
+
await runCommand([
|
|
1573
|
+
"activate-pane",
|
|
1574
|
+
"--pane-id",
|
|
1575
|
+
targetRealId
|
|
1576
|
+
], {
|
|
1577
|
+
message: `Failed to execute focus step ${step.id}`,
|
|
1578
|
+
path: step.id
|
|
1579
|
+
});
|
|
1580
|
+
};
|
|
1581
|
+
const escapeDoubleQuotes = (value) => {
|
|
1582
|
+
return value.split("\"").join("\\\"");
|
|
1583
|
+
};
|
|
1584
|
+
const appendCarriageReturn = (value) => {
|
|
1585
|
+
return value.endsWith("\r") ? value : `${value}\r`;
|
|
1586
|
+
};
|
|
1587
|
+
const sendTextToPane = async ({ paneId, text, runCommand, context }) => {
|
|
1588
|
+
await runCommand([
|
|
1589
|
+
"send-text",
|
|
1590
|
+
"--pane-id",
|
|
1591
|
+
paneId,
|
|
1592
|
+
"--no-paste",
|
|
1593
|
+
"--",
|
|
1594
|
+
appendCarriageReturn(text)
|
|
1595
|
+
], context);
|
|
1596
|
+
};
|
|
1597
|
+
const applyTerminalCommands = async ({ terminals, paneMap, runCommand }) => {
|
|
1598
|
+
for (const terminal of terminals) {
|
|
1599
|
+
const realPaneId = resolveRealPaneId(paneMap, terminal.virtualPaneId, { stepId: terminal.virtualPaneId });
|
|
1600
|
+
if (typeof terminal.cwd === "string" && terminal.cwd.length > 0) {
|
|
1601
|
+
const escapedCwd = escapeDoubleQuotes(terminal.cwd);
|
|
1602
|
+
await sendTextToPane({
|
|
1603
|
+
paneId: realPaneId,
|
|
1604
|
+
text: `cd "${escapedCwd}"`,
|
|
1605
|
+
runCommand,
|
|
1606
|
+
context: {
|
|
1607
|
+
message: `Failed to change directory for pane ${terminal.virtualPaneId}`,
|
|
1608
|
+
path: terminal.virtualPaneId,
|
|
1609
|
+
details: { cwd: terminal.cwd }
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
if (terminal.env !== void 0) for (const [key, value] of Object.entries(terminal.env)) {
|
|
1614
|
+
const escapedValue = escapeDoubleQuotes(String(value));
|
|
1615
|
+
await sendTextToPane({
|
|
1616
|
+
paneId: realPaneId,
|
|
1617
|
+
text: `export ${key}="${escapedValue}"`,
|
|
1618
|
+
runCommand,
|
|
1619
|
+
context: {
|
|
1620
|
+
message: `Failed to set environment variable ${key}`,
|
|
1621
|
+
path: terminal.virtualPaneId
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
if (typeof terminal.command === "string" && terminal.command.length > 0) await sendTextToPane({
|
|
1626
|
+
paneId: realPaneId,
|
|
1627
|
+
text: terminal.command,
|
|
1628
|
+
runCommand,
|
|
1629
|
+
context: {
|
|
1630
|
+
message: `Failed to execute command for pane ${terminal.virtualPaneId}`,
|
|
1631
|
+
path: terminal.virtualPaneId,
|
|
1632
|
+
details: { command: terminal.command }
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
const applySplitStep = async ({ step, paneMap, windowId, runCommand, listWindows, logPaneMapping }) => {
|
|
1638
|
+
const targetVirtualId = step.targetPaneId;
|
|
1639
|
+
if (typeof targetVirtualId !== "string" || targetVirtualId.length === 0) throw createFunctionalError("execution", {
|
|
1640
|
+
code: ErrorCodes.INVALID_PANE,
|
|
1641
|
+
message: "Split step missing target pane metadata",
|
|
1642
|
+
path: step.id
|
|
1643
|
+
});
|
|
1644
|
+
const targetRealId = resolveRealPaneId(paneMap, targetVirtualId, { stepId: step.id });
|
|
1645
|
+
const beforeList = await listWindows();
|
|
1646
|
+
const beforePaneIds = collectPaneIdsForWindow(beforeList, windowId);
|
|
1647
|
+
const args = buildSplitArguments({
|
|
1648
|
+
targetPaneId: targetRealId,
|
|
1649
|
+
percent: extractPercent(step.command),
|
|
1650
|
+
horizontal: isHorizontalSplit(step.command)
|
|
1651
|
+
});
|
|
1652
|
+
await runCommand(args, {
|
|
1653
|
+
message: `Failed to execute split step ${step.id}`,
|
|
1654
|
+
path: step.id
|
|
1655
|
+
});
|
|
1656
|
+
const afterList = await listWindows();
|
|
1657
|
+
const afterPaneIds = collectPaneIdsForWindow(afterList, windowId);
|
|
1658
|
+
const newPaneId = findNewPaneId(beforePaneIds, afterPaneIds);
|
|
1659
|
+
if (typeof newPaneId !== "string" || newPaneId.length === 0) throw createFunctionalError("execution", {
|
|
1660
|
+
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
|
|
1661
|
+
message: "Unable to determine newly created wezterm pane",
|
|
1662
|
+
path: step.id
|
|
1663
|
+
});
|
|
1664
|
+
if (typeof step.createdPaneId === "string" && step.createdPaneId.length > 0) {
|
|
1665
|
+
registerPaneWithAncestors(paneMap, step.createdPaneId, newPaneId);
|
|
1666
|
+
logPaneMapping(step.createdPaneId, newPaneId);
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
const createWeztermBackend = (context) => {
|
|
1670
|
+
const formatCommand = (args) => {
|
|
1671
|
+
return `wezterm cli ${args.join(" ")}`;
|
|
1672
|
+
};
|
|
1673
|
+
const logCommand = (args) => {
|
|
1674
|
+
const message = `[wezterm] ${formatCommand(args)}`;
|
|
1675
|
+
if (context.verbose) context.logger.info(message);
|
|
1676
|
+
else context.logger.debug(message);
|
|
1677
|
+
};
|
|
1678
|
+
const logPaneMapping = (virtualId, realId) => {
|
|
1679
|
+
const message = `[wezterm] pane ${virtualId} -> ${realId}`;
|
|
1680
|
+
if (context.verbose) context.logger.info(message);
|
|
1681
|
+
else context.logger.debug(message);
|
|
1682
|
+
};
|
|
1683
|
+
const runCommand = async (args, errorContext) => {
|
|
1684
|
+
const commandArgs = [...args];
|
|
1685
|
+
logCommand(commandArgs);
|
|
1686
|
+
return runWeztermCli(commandArgs, errorContext);
|
|
1687
|
+
};
|
|
1688
|
+
const listWindows = async () => {
|
|
1689
|
+
logCommand([
|
|
1690
|
+
"list",
|
|
1691
|
+
"--format",
|
|
1692
|
+
"json"
|
|
1693
|
+
]);
|
|
1694
|
+
return listWeztermWindows();
|
|
1695
|
+
};
|
|
1696
|
+
const verifyEnvironment = async () => {
|
|
1697
|
+
if (context.dryRun) return;
|
|
1698
|
+
await verifyWeztermAvailability();
|
|
1699
|
+
};
|
|
1700
|
+
const applyPlan = async ({ emission, windowMode }) => {
|
|
1701
|
+
const initialVirtualPaneId = ensureVirtualPaneId(emission);
|
|
1702
|
+
const paneMap = /* @__PURE__ */ new Map();
|
|
1703
|
+
const { paneId: initialPaneId, windowId } = await resolveInitialPane({
|
|
1704
|
+
windowMode,
|
|
1705
|
+
prompt: context.prompt,
|
|
1706
|
+
dryRun: context.dryRun,
|
|
1707
|
+
listWindows,
|
|
1708
|
+
runCommand,
|
|
1709
|
+
logCommand
|
|
1710
|
+
});
|
|
1711
|
+
registerPaneWithAncestors(paneMap, initialVirtualPaneId, initialPaneId);
|
|
1712
|
+
logPaneMapping(initialVirtualPaneId, initialPaneId);
|
|
1713
|
+
let executedSteps = 0;
|
|
1714
|
+
for (const step of emission.steps) if (step.kind === "split") {
|
|
1715
|
+
await applySplitStep({
|
|
1716
|
+
step,
|
|
1717
|
+
paneMap,
|
|
1718
|
+
windowId,
|
|
1719
|
+
runCommand,
|
|
1720
|
+
listWindows,
|
|
1721
|
+
logPaneMapping
|
|
1722
|
+
});
|
|
1723
|
+
executedSteps += 1;
|
|
1724
|
+
} else if (step.kind === "focus") {
|
|
1725
|
+
await applyFocusStep({
|
|
1726
|
+
step,
|
|
1727
|
+
paneMap,
|
|
1728
|
+
runCommand
|
|
1729
|
+
});
|
|
1730
|
+
executedSteps += 1;
|
|
1731
|
+
}
|
|
1732
|
+
await applyTerminalCommands({
|
|
1733
|
+
terminals: emission.terminals,
|
|
1734
|
+
paneMap,
|
|
1735
|
+
runCommand
|
|
1736
|
+
});
|
|
1737
|
+
const focusVirtual = emission.summary.focusPaneId;
|
|
1738
|
+
const focusPaneId = typeof focusVirtual === "string" ? paneMap.get(focusVirtual) : void 0;
|
|
1739
|
+
return {
|
|
1740
|
+
executedSteps,
|
|
1741
|
+
focusPaneId
|
|
1742
|
+
};
|
|
1743
|
+
};
|
|
1744
|
+
return {
|
|
1745
|
+
verifyEnvironment,
|
|
1746
|
+
applyPlan,
|
|
1747
|
+
getDryRunSteps: buildDryRunSteps
|
|
1748
|
+
};
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
//#endregion
|
|
1752
|
+
//#region src/executor/backend-factory.ts
|
|
1753
|
+
const createTerminalBackend = (kind, context) => {
|
|
1754
|
+
if (kind === "tmux") return createTmuxBackend(context);
|
|
1755
|
+
if (kind === "wezterm") return createWeztermBackend(context);
|
|
1756
|
+
throw new Error(`Unsupported backend "${kind}"`);
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
//#endregion
|
|
1760
|
+
//#region src/executor/backend-resolver.ts
|
|
1761
|
+
const KNOWN_BACKENDS = ["tmux", "wezterm"];
|
|
1762
|
+
const resolveTerminalBackendKind = ({ cliFlag, env }) => {
|
|
1763
|
+
if (cliFlag !== void 0) {
|
|
1764
|
+
if (!KNOWN_BACKENDS.includes(cliFlag)) throw new Error(`Unknown backend "${cliFlag}"`);
|
|
1765
|
+
return cliFlag;
|
|
1766
|
+
}
|
|
1767
|
+
if (typeof env.TMUX === "string" && env.TMUX.trim().length > 0) return "tmux";
|
|
1768
|
+
return "tmux";
|
|
1769
|
+
};
|
|
1770
|
+
|
|
731
1771
|
//#endregion
|
|
732
1772
|
//#region src/core/diagnostics.ts
|
|
733
1773
|
const severityRank = {
|
|
@@ -1277,11 +2317,10 @@ const createCli = (options = {}) => {
|
|
|
1277
2317
|
console.log("");
|
|
1278
2318
|
}
|
|
1279
2319
|
};
|
|
1280
|
-
const renderDryRun = (
|
|
1281
|
-
console.log(chalk.bold("\nPlanned
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
console.log(` ${index + 1}. ${step.summary}: tmux ${commandString}`);
|
|
2320
|
+
const renderDryRun = (steps) => {
|
|
2321
|
+
console.log(chalk.bold("\nPlanned terminal steps (dry-run)"));
|
|
2322
|
+
steps.forEach((step, index) => {
|
|
2323
|
+
console.log(` ${index + 1}. [${step.backend}] ${step.summary}: ${step.command}`);
|
|
1285
2324
|
});
|
|
1286
2325
|
};
|
|
1287
2326
|
const buildPresetDocument = (preset, presetName) => {
|
|
@@ -1297,6 +2336,11 @@ const createCli = (options = {}) => {
|
|
|
1297
2336
|
const buildPresetSource = (presetName) => {
|
|
1298
2337
|
return typeof presetName === "string" && presetName.length > 0 ? `preset://${presetName}` : "preset://default";
|
|
1299
2338
|
};
|
|
2339
|
+
const determineCliWindowMode = (options$1) => {
|
|
2340
|
+
if (options$1.currentWindow === true && options$1.newWindow === true) throw new Error("Cannot use --current-window and --new-window at the same time");
|
|
2341
|
+
if (options$1.currentWindow === true) return "current-window";
|
|
2342
|
+
if (options$1.newWindow === true) return "new-window";
|
|
2343
|
+
};
|
|
1300
2344
|
const handleFunctionalError = (error) => {
|
|
1301
2345
|
const header = [`[${error.kind}]`, `[${error.code}]`];
|
|
1302
2346
|
if (typeof error.path === "string" && error.path.length > 0) header.push(`[${error.path}]`);
|
|
@@ -1304,8 +2348,8 @@ const createCli = (options = {}) => {
|
|
|
1304
2348
|
if (typeof error.source === "string" && error.source.length > 0) lines.push(`source: ${error.source}`);
|
|
1305
2349
|
const commandDetail = error.details?.command;
|
|
1306
2350
|
if (Array.isArray(commandDetail)) {
|
|
1307
|
-
const
|
|
1308
|
-
if (
|
|
2351
|
+
const parts = commandDetail.filter((segment) => typeof segment === "string");
|
|
2352
|
+
if (parts.length > 0) lines.push(`command: ${parts.join(" ")}`);
|
|
1309
2353
|
} else if (typeof commandDetail === "string" && commandDetail.length > 0) lines.push(`command: ${commandDetail}`);
|
|
1310
2354
|
const stderrDetail = error.details?.stderr;
|
|
1311
2355
|
if (typeof stderrDetail === "string" && stderrDetail.length > 0) lines.push(`stderr: ${stderrDetail}`);
|
|
@@ -1361,12 +2405,36 @@ const createCli = (options = {}) => {
|
|
|
1361
2405
|
try {
|
|
1362
2406
|
await presetManager.loadConfig();
|
|
1363
2407
|
const preset = typeof presetName === "string" && presetName.length > 0 ? presetManager.getPreset(presetName) : presetManager.getDefaultPreset();
|
|
1364
|
-
const
|
|
1365
|
-
|
|
2408
|
+
const cliWindowMode = determineCliWindowMode({
|
|
2409
|
+
currentWindow: options$1.currentWindow,
|
|
2410
|
+
newWindow: options$1.newWindow
|
|
2411
|
+
});
|
|
2412
|
+
const defaults = presetManager.getDefaults();
|
|
2413
|
+
const windowModeResolution = resolveWindowMode({
|
|
2414
|
+
cli: cliWindowMode,
|
|
2415
|
+
preset: preset.windowMode,
|
|
2416
|
+
defaults: defaults?.windowMode
|
|
2417
|
+
});
|
|
2418
|
+
const windowMode = windowModeResolution.mode;
|
|
2419
|
+
logger.info(`Window mode: ${windowMode} (source: ${windowModeResolution.source})`);
|
|
2420
|
+
const confirmPaneClosure = createPaneKillPrompter(logger);
|
|
1366
2421
|
const executor = createCommandExecutor({
|
|
1367
2422
|
verbose: options$1.verbose,
|
|
1368
2423
|
dryRun: options$1.dryRun
|
|
1369
2424
|
});
|
|
2425
|
+
const backendKind = resolveTerminalBackendKind({
|
|
2426
|
+
cliFlag: options$1.backend,
|
|
2427
|
+
env: process.env
|
|
2428
|
+
});
|
|
2429
|
+
logger.info(`Terminal backend: ${backendKind}`);
|
|
2430
|
+
const backend = createTerminalBackend(backendKind, {
|
|
2431
|
+
executor,
|
|
2432
|
+
logger,
|
|
2433
|
+
dryRun: options$1.dryRun,
|
|
2434
|
+
verbose: options$1.verbose,
|
|
2435
|
+
prompt: confirmPaneClosure
|
|
2436
|
+
});
|
|
2437
|
+
await backend.verifyEnvironment();
|
|
1370
2438
|
if (options$1.dryRun === true) console.log("[DRY RUN] No actual commands will be executed");
|
|
1371
2439
|
let compileResult;
|
|
1372
2440
|
let planResult;
|
|
@@ -1381,14 +2449,16 @@ const createCli = (options = {}) => {
|
|
|
1381
2449
|
} catch (error) {
|
|
1382
2450
|
return handlePipelineFailure(error);
|
|
1383
2451
|
}
|
|
1384
|
-
if (options$1.dryRun === true)
|
|
1385
|
-
|
|
1386
|
-
|
|
2452
|
+
if (options$1.dryRun === true) {
|
|
2453
|
+
const dryRunSteps = backend.getDryRunSteps(emission);
|
|
2454
|
+
renderDryRun(dryRunSteps);
|
|
2455
|
+
} else try {
|
|
2456
|
+
const executionResult = await backend.applyPlan({
|
|
1387
2457
|
emission,
|
|
1388
|
-
|
|
2458
|
+
windowMode,
|
|
1389
2459
|
windowName: preset.name ?? presetName ?? "vde-layout"
|
|
1390
2460
|
});
|
|
1391
|
-
logger.info(`Executed ${executionResult.executedSteps}
|
|
2461
|
+
logger.info(`Executed ${executionResult.executedSteps} ${backendKind} steps`);
|
|
1392
2462
|
} catch (error) {
|
|
1393
2463
|
return handlePipelineFailure(error);
|
|
1394
2464
|
}
|
|
@@ -1403,7 +2473,10 @@ const createCli = (options = {}) => {
|
|
|
1403
2473
|
program.option("--verbose", "Show detailed logs", false);
|
|
1404
2474
|
program.option("-V", "Show version (deprecated; use -v)");
|
|
1405
2475
|
program.option("--dry-run", "Display commands without executing", false);
|
|
2476
|
+
program.option("--backend <backend>", "Select terminal backend (tmux or wezterm)");
|
|
1406
2477
|
program.option("--config <path>", "Path to configuration file");
|
|
2478
|
+
program.option("--current-window", "Use the current tmux window for layout (kills other panes)", false);
|
|
2479
|
+
program.option("--new-window", "Always create a new tmux window for layout", false);
|
|
1407
2480
|
program.command("list").description("List available presets").action(async () => {
|
|
1408
2481
|
await listPresets();
|
|
1409
2482
|
});
|
|
@@ -1414,7 +2487,10 @@ const createCli = (options = {}) => {
|
|
|
1414
2487
|
const opts = program.opts();
|
|
1415
2488
|
await executePreset(presetName, {
|
|
1416
2489
|
verbose: opts.verbose === true,
|
|
1417
|
-
dryRun: opts.dryRun === true
|
|
2490
|
+
dryRun: opts.dryRun === true,
|
|
2491
|
+
currentWindow: opts.currentWindow === true,
|
|
2492
|
+
newWindow: opts.newWindow === true,
|
|
2493
|
+
backend: opts.backend
|
|
1418
2494
|
});
|
|
1419
2495
|
});
|
|
1420
2496
|
};
|