vde-layout
Version:
Terminal multiplexer layout management tool for VDE (Vibe Coding Development Environment)
4,340 lines • 152 kB
JavaScript
#!/usr/bin/env node
import { createRequire } from "node:module";
import { defineCommand, parseArgs, renderUsage } from "citty";
import fs from "fs-extra";
import path from "node:path";
import os from "node:os";
import * as YAML from "yaml";
import { parse } from "yaml";
import { z } from "zod";
import { createHash } from "node:crypto";
import chalk, { Chalk } from "chalk";
import { execFileSync } from "node:child_process";
import { execa } from "execa";
import { createInterface } from "node:readline/promises";
import { stdin, stdout } from "node:process";
import stringWidth from "string-width";
//#region src/utils/errors.ts
const ErrorCodes = {
CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
CONFIG_PARSE_ERROR: "CONFIG_PARSE_ERROR",
CONFIG_PERMISSION_ERROR: "CONFIG_PERMISSION_ERROR",
INVALID_PRESET: "INVALID_PRESET",
PRESET_NOT_FOUND: "PRESET_NOT_FOUND",
INVALID_LAYOUT: "INVALID_LAYOUT",
INVALID_PANE: "INVALID_PANE",
INVALID_PLAN: "INVALID_PLAN",
MISSING_TARGET: "MISSING_TARGET",
TMUX_NOT_RUNNING: "TMUX_NOT_RUNNING",
TMUX_COMMAND_FAILED: "TMUX_COMMAND_FAILED",
NOT_IN_TMUX_SESSION: "NOT_IN_TMUX_SESSION",
TMUX_NOT_FOUND: "TMUX_NOT_FOUND",
TMUX_NOT_INSTALLED: "TMUX_NOT_INSTALLED",
UNSUPPORTED_TMUX_VERSION: "UNSUPPORTED_TMUX_VERSION",
BACKEND_NOT_FOUND: "BACKEND_NOT_FOUND",
TERMINAL_COMMAND_FAILED: "TERMINAL_COMMAND_FAILED",
TEMPLATE_TOKEN_ERROR: "TEMPLATE_TOKEN_ERROR",
WEZTERM_NOT_FOUND: "WEZTERM_NOT_FOUND",
UNSUPPORTED_WEZTERM_VERSION: "UNSUPPORTED_WEZTERM_VERSION",
USER_CANCELLED: "USER_CANCELLED",
SPLIT_SIZE_RESOLUTION_FAILED: "SPLIT_SIZE_RESOLUTION_FAILED"
};
const createBaseError = (name, message, code, details = {}) => {
const error = new Error(message);
error.name = name;
error.code = code;
error.details = details;
return error;
};
const createConfigError = (message, code, details = {}) => {
return createBaseError("ConfigError", message, code, details);
};
const createValidationError = (message, code, details = {}) => {
return createBaseError("ValidationError", message, code, details);
};
const createTmuxError = (message, code, details = {}) => {
return createBaseError("TmuxError", message, code, details);
};
const createEnvironmentError = (message, code, details = {}) => {
return createBaseError("EnvironmentError", message, code, details);
};
const isVDELayoutError = (error) => {
if (typeof error !== "object" || error === null) return false;
if (!("code" in error)) return false;
const { code } = error;
if (typeof code !== "string") return false;
if (!("details" in error)) return false;
return true;
};
const formatters = {
[ErrorCodes.CONFIG_NOT_FOUND]: (error) => {
const searchPaths = error.details.searchPaths;
if (!Array.isArray(searchPaths)) return "";
const lines = ["", "Searched in the following locations:"];
searchPaths.forEach((location) => lines.push(` - ${location}`));
lines.push("", "To create a configuration file, run:");
lines.push(" mkdir -p ~/.config/vde");
lines.push(" echo \"presets: {}\" > ~/.config/vde/layout.yml");
return lines.join("\n");
},
[ErrorCodes.NOT_IN_TMUX_SESSION]: () => {
return "\nThis command must be run inside a tmux session.\nStart tmux first with: tmux\n";
},
[ErrorCodes.TMUX_NOT_INSTALLED]: () => {
return "\ntmux is required but not installed.\nInstall tmux using your package manager:\n - macOS: brew install tmux\n - Ubuntu/Debian: sudo apt-get install tmux\n - Fedora: sudo dnf install tmux\n";
},
[ErrorCodes.UNSUPPORTED_TMUX_VERSION]: (error) => {
const requiredVersion = error.details.requiredVersion;
if (typeof requiredVersion !== "string") return "";
return `\nRequired tmux version: ${requiredVersion} or higher\n`;
},
[ErrorCodes.BACKEND_NOT_FOUND]: (error) => {
const backend = typeof error.details.backend === "string" ? error.details.backend : "terminal backend";
return `\nMissing binary: ${typeof error.details.binary === "string" ? error.details.binary : backend}${backend === "wezterm" ? [
"",
`${backend} is required but not installed.`,
"Install wezterm using your package manager:",
" - macOS: brew install --cask wezterm",
" - Ubuntu/Debian: sudo apt-get install wezterm",
" - Fedora: sudo dnf install wezterm"
].join("\n") : ""}`;
},
[ErrorCodes.WEZTERM_NOT_FOUND]: () => {
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";
},
[ErrorCodes.UNSUPPORTED_WEZTERM_VERSION]: (error) => {
const requiredVersion = typeof error.details.requiredVersion === "string" ? error.details.requiredVersion : "";
const detected = typeof error.details.detectedVersion === "string" ? error.details.detectedVersion : "";
const lines = ["", "Unsupported wezterm version detected."];
if (detected) lines.push(`Detected version: ${detected}`);
if (requiredVersion) lines.push(`Required version: ${requiredVersion} or higher`);
return lines.join("\n");
},
[ErrorCodes.SPLIT_SIZE_RESOLUTION_FAILED]: (error) => {
const paneId = typeof error.details.paneId === "string" ? error.details.paneId : "";
const paneCells = typeof error.details.paneCells === "number" ? String(error.details.paneCells) : "";
const detected = typeof error.details.detectedVersion === "string" ? error.details.detectedVersion : "";
const required = typeof error.details.requiredVersion === "string" ? error.details.requiredVersion : "";
const lines = ["", "Unable to resolve split size from pane dimensions."];
if (paneId) lines.push(`Pane ID: ${paneId}`);
if (paneCells) lines.push(`Pane cells: ${paneCells}`);
if (detected) lines.push(`Detected version: ${detected}`);
if (required) lines.push(`Required version: ${required}`);
return lines.join("\n");
}
};
//#endregion
//#region src/models/schema.ts
const WindowModeSchema = z.enum(["new-window", "current-window"]);
const TerminalBackendSchema = z.enum(["tmux", "wezterm"]);
const SELECT_UI_MODES = ["auto", "fzf"];
const SELECT_SURFACE_MODES = [
"auto",
"inline",
"tmux-popup"
];
const SelectUiModeSchema = z.enum(SELECT_UI_MODES);
const SelectSurfaceModeSchema = z.enum(SELECT_SURFACE_MODES);
const SelectorFzfSchema = z.object({ extraArgs: z.array(z.string().min(1)).optional() }).strict();
const HooksSchema = z.object({ afterApply: z.string().min(1).optional() }).strict();
const SelectorDefaultsSchema = z.object({
ui: SelectUiModeSchema.optional(),
surface: SelectSurfaceModeSchema.optional(),
tmuxPopupOpts: z.string().min(1).optional(),
fzf: SelectorFzfSchema.optional()
}).strict();
const RatioValueSchema = z.union([z.number().positive(), z.string().regex(/^[1-9][0-9]*c$/, { message: "ratio value must be a positive number or \"<positive-integer>c\"" })]);
const hasNumericWeight = (ratio) => {
return ratio.some((value) => typeof value === "number");
};
const LayoutIssueParamCodes = {
RATIO_LENGTH_MISMATCH: "RATIO_LENGTH_MISMATCH",
RATIO_WEIGHT_MISSING: "RATIO_WEIGHT_MISSING"
};
const TerminalPaneSchema = z.object({
name: z.string().min(1),
command: z.string().optional(),
cwd: z.string().optional(),
env: z.record(z.string()).optional(),
delay: z.number().int().positive().optional(),
title: z.string().optional(),
focus: z.boolean().optional(),
ephemeral: z.boolean().optional(),
closeOnError: z.boolean().optional()
}).strict();
const SplitPaneSchema = z.lazy(() => z.object({
type: z.enum(["horizontal", "vertical"]),
ratio: z.array(RatioValueSchema).min(1),
panes: z.array(PaneSchema).min(1)
}).strict().refine((data) => data.ratio.length === data.panes.length, {
message: "Number of elements in ratio array does not match number of elements in panes array",
params: { code: LayoutIssueParamCodes.RATIO_LENGTH_MISMATCH }
}).refine((data) => hasNumericWeight(data.ratio), {
message: "ratio must include at least one numeric weight",
params: { code: LayoutIssueParamCodes.RATIO_WEIGHT_MISSING }
}));
const PaneSchema = z.lazy(() => z.union([SplitPaneSchema, TerminalPaneSchema]));
const LayoutSchema = z.object({
type: z.enum(["horizontal", "vertical"]),
ratio: z.array(RatioValueSchema).min(1),
panes: z.array(PaneSchema).min(1)
}).refine((data) => data.ratio.length === data.panes.length, {
message: "Number of elements in ratio array does not match number of elements in panes array",
params: { code: LayoutIssueParamCodes.RATIO_LENGTH_MISMATCH }
}).refine((data) => hasNumericWeight(data.ratio), {
message: "ratio must include at least one numeric weight",
params: { code: LayoutIssueParamCodes.RATIO_WEIGHT_MISSING }
});
const PresetSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
layout: LayoutSchema.optional(),
command: z.string().optional(),
windowMode: WindowModeSchema.optional(),
backend: TerminalBackendSchema.optional(),
hooks: HooksSchema.optional()
});
const ConfigSchema = z.object({
defaults: z.object({
windowMode: WindowModeSchema.optional(),
selector: SelectorDefaultsSchema.optional()
}).optional(),
presets: z.record(PresetSchema)
});
//#endregion
//#region src/config/validator.ts
/**
* Parse YAML text into an object
* @param yamlText - YAML text to parse
* @returns Parsed object
* @throws {ValidationError} When YAML parsing fails
*/
const parseYAML = (yamlText) => {
if (!yamlText || typeof yamlText !== "string") throw createValidationError("YAML text not provided", ErrorCodes.CONFIG_PARSE_ERROR, { received: typeof yamlText });
try {
return YAML.parse(yamlText);
} catch (error) {
throw createValidationError("Failed to parse YAML", ErrorCodes.CONFIG_PARSE_ERROR, {
parseError: error instanceof Error ? error.message : String(error),
yamlSnippet: yamlText.substring(0, 200)
});
}
};
/**
* Validate basic configuration structure
* @param parsed - Parsed YAML object
* @throws {ValidationError} When structure is invalid
*/
const validateConfigStructure = (parsed) => {
if (parsed === null || parsed === void 0 || typeof parsed !== "object") throw createValidationError("YAML is empty or invalid format", ErrorCodes.CONFIG_PARSE_ERROR, { parsed });
const parsedObj = parsed;
if (!("presets" in parsedObj) || parsedObj.presets === void 0 || parsedObj.presets === null) throw createValidationError("presets field is required", ErrorCodes.INVALID_PRESET, { availableFields: Object.keys(parsedObj) });
const presetsObj = parsedObj.presets;
if (typeof presetsObj !== "object" || presetsObj === null || Object.keys(presetsObj).length === 0) throw createValidationError("At least one preset is required", ErrorCodes.INVALID_PRESET, { presets: presetsObj });
};
/**
* Format Zod validation errors into user-friendly messages
* @param error - Zod validation error
* @returns Formatted error issues
*/
const formatZodErrors = (error) => {
return error.issues.map((issue) => {
const path = issue.path.join(".");
let message = issue.message;
if (issue.code === "invalid_type") {
if (issue.path.includes("command") && issue.expected === "string") message = "command field must be a string";
else if (issue.path.includes("workingDirectory") && issue.expected === "string") message = "workingDirectory field must be a string";
else if (issue.received === "number" && issue.expected === "string") message = `${path} must be a string`;
else if (issue.received === "array" && issue.expected === "string") message = `${path} must be a string`;
} else if (issue.code === "invalid_union") {
const unionIssue = issue;
if (unionIssue.unionErrors !== void 0) if (unionIssue.unionErrors.find((e) => e.issues?.some((i) => i.path.includes("command") && i.code === "invalid_type") === true) !== void 0) message = "command field is required";
else if (unionIssue.unionErrors.find((e) => e.issues?.some((i) => i.path.includes("panes") && i.code === "invalid_type") === true) !== void 0) message = "panes field is required";
else message = "Pane type must be \"terminal\" or \"split\"";
else message = "Pane type must be \"terminal\" or \"split\"";
} else if (issue.code === "invalid_literal") {
if (issue.path.includes("direction")) message = "direction must be \"horizontal\" or \"vertical\"";
} else if (issue.message.includes("required")) message = issue.message;
else if (issue.code === "custom" && issue.message.includes("ratio array")) message = issue.message;
else if (issue.code === "too_small" && issue.message.includes("Array must contain at least")) if (path.includes("panes")) message = "panes array must contain at least 2 elements";
else if (path.includes("ratio")) message = "ratio array must contain at least 2 elements";
else message = issue.message;
return {
path,
message,
code: issue.code
};
});
};
/**
* Validates YAML text and converts it to a type-safe Config object
* @param yamlText - YAML text to validate
* @returns Validated Config object
* @throws {ValidationError} When YAML is invalid
*/
const validateYAML = (yamlText) => {
const parsed = parseYAML(yamlText);
validateConfigStructure(parsed);
try {
return ConfigSchema.parse(parsed);
} catch (error) {
if (error instanceof z.ZodError) {
const issues = formatZodErrors(error);
throw createValidationError(issues.length > 0 && issues[0] ? issues[0].message : "Configuration validation failed", ErrorCodes.CONFIG_PARSE_ERROR, {
issues,
rawErrors: error.issues
});
}
if (isVDELayoutError(error) && error.name === "ValidationError") throw error;
throw createValidationError("Unexpected validation error occurred", ErrorCodes.CONFIG_PARSE_ERROR, { error: error instanceof Error ? error.message : String(error) });
}
};
//#endregion
//#region src/config/loader.ts
const createConfigLoader = (options = {}) => {
const explicitConfigPaths = options.configPaths;
const emitWarning = options.onWarning ?? ((message) => console.warn(message));
const computeCachedSearchPaths = () => {
if (explicitConfigPaths && explicitConfigPaths.length > 0) return [...explicitConfigPaths];
const candidates = [];
const projectCandidate = findProjectConfigCandidate();
if (projectCandidate !== null) candidates.push(projectCandidate);
const defaultSearchPathGroups = buildDefaultSearchPathGroups();
candidates.push(...flattenSearchPathGroups(defaultSearchPathGroups));
return [...new Set(candidates)];
};
const loadConfig = async () => {
if (explicitConfigPaths && explicitConfigPaths.length > 0) {
const filePath = await findFirstExisting(explicitConfigPaths);
if (filePath === null) throw createConfigError("Configuration file not found", ErrorCodes.CONFIG_NOT_FOUND, { searchPaths: explicitConfigPaths });
return validateYAML(await safeReadFile(filePath));
}
const searchPaths = computeCachedSearchPaths();
const globalPaths = await resolveFirstExistingPaths(buildDefaultSearchPathGroups());
const projectPath = findProjectConfigCandidate();
const projectConfigExists = projectPath !== null ? await fs.pathExists(projectPath) : false;
if (globalPaths.length === 0 && !projectConfigExists) throw createConfigError("Configuration file not found", ErrorCodes.CONFIG_NOT_FOUND, { searchPaths });
let mergedConfig = { presets: {} };
for (const globalPath of globalPaths) {
const config = validateYAML(await safeReadFile(globalPath));
mergedConfig = mergeConfigs(mergedConfig, config, emitWarning);
}
if (projectPath !== null && projectConfigExists) {
const config = validateYAML(await safeReadFile(projectPath));
mergedConfig = mergeConfigs(mergedConfig, config, emitWarning);
}
return mergedConfig;
};
return {
loadYAML: async () => {
const config = await loadConfig();
return YAML.stringify(config);
},
loadConfig,
findConfigFile: async () => {
const searchPaths = explicitConfigPaths && explicitConfigPaths.length > 0 ? [...explicitConfigPaths] : computeCachedSearchPaths();
for (const searchPath of searchPaths) if (await fs.pathExists(searchPath)) return searchPath;
return null;
},
getSearchPaths: () => computeCachedSearchPaths()
};
};
const buildDefaultSearchPathGroups = () => {
const pathGroups = [];
const vdeConfigPath = process.env.VDE_CONFIG_PATH;
if (vdeConfigPath !== void 0) pathGroups.push([path.join(vdeConfigPath, "layout.yml")]);
const homeDir = process.env.HOME ?? os.homedir();
const xdgConfigHome = process.env.XDG_CONFIG_HOME ?? path.join(homeDir, ".config");
pathGroups.push([path.join(xdgConfigHome, "vde", "layout", "config.yml"), path.join(xdgConfigHome, "vde", "layout.yml")]);
return pathGroups.map((group) => [...new Set(group)]);
};
const flattenSearchPathGroups = (pathGroups) => {
const paths = [];
for (const group of pathGroups) paths.push(...group);
return [...new Set(paths)];
};
const resolveFirstExistingPaths = async (pathGroups) => {
const existingPaths = await Promise.all(pathGroups.map(async (group) => findFirstExisting(group)));
const seenPaths = /* @__PURE__ */ new Set();
const resolvedPaths = [];
for (const existingPath of existingPaths) if (existingPath !== null && !seenPaths.has(existingPath)) {
seenPaths.add(existingPath);
resolvedPaths.push(existingPath);
}
return resolvedPaths;
};
const findProjectConfigCandidate = () => {
let currentDir = process.cwd();
const { root } = path.parse(currentDir);
while (true) {
const candidates = [path.join(currentDir, ".vde", "layout", "config.yml"), path.join(currentDir, ".vde", "layout.yml")];
for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
if (currentDir === root) break;
const parent = path.dirname(currentDir);
if (parent === currentDir) break;
currentDir = parent;
}
return null;
};
const findFirstExisting = async (paths) => {
for (const candidate of paths) if (await fs.pathExists(candidate)) return candidate;
return null;
};
const safeReadFile = async (filePath) => {
try {
return await fs.readFile(filePath, "utf8");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw createConfigError(`Failed to read configuration file`, ErrorCodes.CONFIG_PERMISSION_ERROR, {
filePath,
error: errorMessage
});
}
};
const mergeConfigs = (base, override, emitWarning) => {
const mergedPresets = { ...base.presets };
for (const [presetKey, overridePreset] of Object.entries(override.presets)) {
const basePreset = base.presets[presetKey];
if (basePreset !== void 0 && basePreset.windowMode !== void 0 && overridePreset.windowMode !== void 0 && basePreset.windowMode !== overridePreset.windowMode) emitWarning(`[vde-layout] Preset "${presetKey}" windowMode conflict: "${basePreset.windowMode}" overridden by "${overridePreset.windowMode}"`);
mergedPresets[presetKey] = overridePreset;
}
const baseDefaults = base.defaults;
const overrideDefaults = override.defaults;
if (baseDefaults?.windowMode !== void 0 && overrideDefaults?.windowMode !== void 0 && baseDefaults.windowMode !== overrideDefaults.windowMode) emitWarning(`[vde-layout] defaults.windowMode conflict: "${baseDefaults.windowMode}" overridden by "${overrideDefaults.windowMode}"`);
const mergedSelectorDefaults = mergeSelectorDefaults({
baseSelector: baseDefaults?.selector,
overrideSelector: overrideDefaults?.selector,
emitWarning
});
const mergedDefaults = baseDefaults !== void 0 || overrideDefaults !== void 0 ? {
...baseDefaults ?? {},
...overrideDefaults ?? {},
...mergedSelectorDefaults !== void 0 ? { selector: mergedSelectorDefaults } : {}
} : void 0;
return mergedDefaults === void 0 ? { presets: mergedPresets } : {
defaults: mergedDefaults,
presets: mergedPresets
};
};
const mergeSelectorDefaults = ({ baseSelector, overrideSelector, emitWarning }) => {
if (baseSelector === void 0 && overrideSelector === void 0) return;
if (baseSelector?.ui !== void 0 && overrideSelector?.ui !== void 0 && baseSelector.ui !== overrideSelector.ui) emitWarning(`[vde-layout] defaults.selector.ui conflict: "${baseSelector.ui}" overridden by "${overrideSelector.ui}"`);
if (baseSelector?.surface !== void 0 && overrideSelector?.surface !== void 0 && baseSelector.surface !== overrideSelector.surface) emitWarning(`[vde-layout] defaults.selector.surface conflict: "${baseSelector.surface}" overridden by "${overrideSelector.surface}"`);
if (baseSelector?.tmuxPopupOpts !== void 0 && overrideSelector?.tmuxPopupOpts !== void 0 && baseSelector.tmuxPopupOpts !== overrideSelector.tmuxPopupOpts) emitWarning(`[vde-layout] defaults.selector.tmuxPopupOpts conflict: "${baseSelector.tmuxPopupOpts}" overridden by "${overrideSelector.tmuxPopupOpts}"`);
const baseExtraArgs = baseSelector?.fzf?.extraArgs;
const overrideExtraArgs = overrideSelector?.fzf?.extraArgs;
if (Array.isArray(baseExtraArgs) && Array.isArray(overrideExtraArgs) && JSON.stringify(baseExtraArgs) !== JSON.stringify(overrideExtraArgs)) emitWarning(`[vde-layout] defaults.selector.fzf.extraArgs conflict: global value overridden by project value`);
const mergedFzf = baseSelector?.fzf !== void 0 || overrideSelector?.fzf !== void 0 ? {
...baseSelector?.fzf ?? {},
...overrideSelector?.fzf ?? {}
} : void 0;
return {
...baseSelector ?? {},
...overrideSelector ?? {},
...mergedFzf !== void 0 ? { fzf: mergedFzf } : {}
};
};
//#endregion
//#region src/layout/preset.ts
const createState = (options = {}) => {
let loaderOptions = options;
let cachedConfig = null;
const setConfigPath = (filePath) => {
loaderOptions = { configPaths: [filePath] };
cachedConfig = null;
};
const loadConfig = async () => {
cachedConfig = await createConfigLoader(loaderOptions).loadConfig();
};
const ensureConfig = () => {
if (cachedConfig === null) throw createConfigError("Configuration not loaded", ErrorCodes.CONFIG_NOT_FOUND);
return cachedConfig;
};
const getPreset = (name) => {
const config = ensureConfig();
const preset = config.presets[name];
if (preset === void 0) throw createConfigError(`Preset "${name}" not found`, ErrorCodes.PRESET_NOT_FOUND, { availablePresets: Object.keys(config.presets) });
return preset;
};
const listPresets = () => {
if (cachedConfig === null) return [];
return Object.entries(cachedConfig.presets).map(([key, preset]) => ({
key,
name: preset.name,
description: preset.description
}));
};
const getDefaultPreset = () => {
const config = ensureConfig();
if (config.presets.default !== void 0) return config.presets.default;
const firstKey = Object.keys(config.presets)[0];
if (typeof firstKey !== "string" || firstKey.length === 0) throw createConfigError("No presets defined", ErrorCodes.PRESET_NOT_FOUND);
return config.presets[firstKey];
};
const getDefaults = () => {
return ensureConfig().defaults;
};
return {
setConfigPath,
loadConfig,
getPreset,
listPresets,
getDefaultPreset,
getDefaults
};
};
const createPresetManager = (options = {}) => {
const state = createState(options);
return {
setConfigPath: state.setConfigPath,
loadConfig: state.loadConfig,
getPreset: state.getPreset,
listPresets: state.listPresets,
getDefaultPreset: state.getDefaultPreset,
getDefaults: state.getDefaults
};
};
//#endregion
//#region src/cli/package-version.ts
const CANDIDATE_PATHS = ["../package.json", "../../package.json"];
const isModuleNotFoundError = (error) => {
return error instanceof Error && error.code === "MODULE_NOT_FOUND";
};
const loadPackageVersion = (requireFn) => {
let lastNotFound;
for (const path of CANDIDATE_PATHS) try {
return requireFn(path).version;
} catch (error) {
if (isModuleNotFoundError(error)) {
lastNotFound = error;
continue;
}
throw error;
}
throw lastNotFound ?? /* @__PURE__ */ new Error(`Unable to resolve package version from candidates: ${CANDIDATE_PATHS.join(", ")}`);
};
//#endregion
//#region src/core/errors.ts
const createCoreError = (kind, error) => ({
kind,
code: error.code,
message: error.message,
source: error.source,
path: error.path,
details: error.details
});
const isCoreError = (value) => {
if (typeof value !== "object" || value === null) return false;
const candidate = value;
return (candidate.kind === "compile" || candidate.kind === "plan" || candidate.kind === "emit" || candidate.kind === "execution") && typeof candidate.code === "string" && typeof candidate.message === "string";
};
//#endregion
//#region src/core/compile.ts
const compilePreset = ({ document, source }) => {
let parsed;
try {
parsed = parse(document);
} catch (error) {
throw compileError("PRESET_PARSE_ERROR", {
source,
message: `Failed to parse YAML: ${error.message}`,
details: { reason: error instanceof Error ? error.message : String(error) }
});
}
return compilePresetValue({
value: parsed,
source
});
};
const compilePresetFromValue = ({ value, source }) => {
return compilePresetValue({
value,
source
});
};
const FIXED_RATIO_PATTERN = /^([1-9][0-9]*)c$/;
const compilePresetValue = ({ value, source }) => {
const parsed = value;
if (!isRecord(parsed)) throw compileError("PRESET_INVALID_DOCUMENT", {
source,
message: "Preset definition is not an object",
path: "preset"
});
const name = typeof parsed.name === "string" && parsed.name.trim().length > 0 ? parsed.name : "Unnamed preset";
const layout = parseLayoutNode(validateLayoutDefinition(parsed.layout, {
source,
path: "preset.layout"
}), {
source,
path: "preset.layout"
});
return { preset: {
name,
version: "legacy",
command: typeof parsed.command === "string" ? parsed.command : void 0,
layout: layout ?? void 0,
hooks: parseHooks(parsed.hooks),
metadata: { source }
} };
};
const parseHooks = (hooks) => {
if (!isRecord(hooks)) return;
const afterApply = typeof hooks.afterApply === "string" && hooks.afterApply.length > 0 ? hooks.afterApply : void 0;
return afterApply === void 0 ? void 0 : { afterApply };
};
const validateLayoutDefinition = (layout, context) => {
if (layout === void 0 || layout === null) return layout;
if (!isRecord(layout) || !looksSplitLikeNode(layout)) return layout;
const normalizedLayout = sanitizeLayoutForSchemaValidation(layout);
const validated = LayoutSchema.safeParse(normalizedLayout);
if (validated.success) return layout;
const issue = validated.error.issues[0];
if (!issue) throw compileError("LAYOUT_INVALID_NODE", {
source: context.source,
message: "Layout node is invalid",
path: context.path,
details: { layout }
});
throw convertLayoutIssueToCompileError({
issue,
source: context.source,
basePath: context.path,
layout
});
};
const parseLayoutNode = (node, context) => {
if (node === void 0 || node === null) return null;
if (!isRecord(node)) throw compileError("LAYOUT_INVALID_NODE", {
source: context.source,
message: "Layout node is invalid",
path: context.path,
details: { node }
});
if ("type" in node || "ratio" in node || "panes" in node) return parseSplitPane(node, context);
if (typeof node.name === "string") return parseTerminalPane(node);
throw compileError("LAYOUT_INVALID_NODE", {
source: context.source,
message: "Layout node is invalid",
path: context.path,
details: { node }
});
};
const parseSplitPane = (node, context) => {
const orientation = node.type;
const panesInput = node.panes;
const ratioInput = node.ratio;
if (orientation !== "horizontal" && orientation !== "vertical" || !Array.isArray(panesInput) || !Array.isArray(ratioInput)) throw compileError("LAYOUT_INVALID_NODE", {
source: context.source,
message: "Layout node is invalid",
path: context.path,
details: { node }
});
const panes = panesInput.map((child, index) => parseLayoutNode(child, {
source: context.source,
path: `${context.path}.panes[${index}]`
}));
const ratio = ratioInput.map((value, index) => parseRatioEntry(value, {
source: context.source,
path: `${context.path}.ratio[${index}]`
}));
if (!ratio.some((entry) => entry.kind === "weight")) throw compileError("RATIO_WEIGHT_MISSING", {
source: context.source,
message: "ratio must include at least one numeric weight",
path: `${context.path}.ratio`,
details: { ratio: ratioInput }
});
return {
kind: "split",
orientation,
ratio,
panes: panes.filter((pane) => pane !== null)
};
};
const parseRatioEntry = (value, context) => {
if (typeof value === "number" && Number.isFinite(value) && value > 0) return {
kind: "weight",
weight: value
};
if (typeof value === "string") {
const match = value.match(FIXED_RATIO_PATTERN);
if (match?.[1] !== void 0) {
const parsed = Number(match[1]);
if (Number.isInteger(parsed) && parsed > 0) return {
kind: "fixed-cells",
cells: parsed
};
}
}
throw compileError("RATIO_INVALID_VALUE", {
source: context.source,
message: "ratio value must be a positive number or \"<positive-integer>c\"",
path: context.path,
details: { value }
});
};
const parseTerminalPane = (node) => {
const name = typeof node.name === "string" ? node.name : "";
const command = typeof node.command === "string" ? node.command : void 0;
const cwd = typeof node.cwd === "string" ? node.cwd : void 0;
const delay = typeof node.delay === "number" && Number.isFinite(node.delay) && node.delay > 0 ? node.delay : void 0;
const title = typeof node.title === "string" && node.title.length > 0 ? node.title : void 0;
const focus = node.focus === true ? true : void 0;
const ephemeral = node.ephemeral === true ? true : void 0;
const closeOnError = node.closeOnError === true ? true : void 0;
return {
kind: "terminal",
name,
command,
cwd,
env: normalizeEnv(node.env),
delay,
title,
focus,
ephemeral,
closeOnError,
options: collectOptions(node, new Set([
"name",
"command",
"cwd",
"env",
"focus",
"ephemeral",
"closeOnError",
"options",
"title",
"delay"
]))
};
};
const normalizeEnv = (env) => {
if (!isRecord(env)) return;
const entries = Object.entries(env).reduce((accumulator, [key, value]) => {
if (typeof value === "string") accumulator[key] = value;
return accumulator;
}, {});
return Object.keys(entries).length > 0 ? entries : void 0;
};
const collectOptions = (node, excludedKeys) => {
const optionsEntries = Object.entries(node).filter(([key]) => !excludedKeys.has(key));
if (optionsEntries.length === 0) return;
return optionsEntries.reduce((accumulator, [key, value]) => {
accumulator[key] = value;
return accumulator;
}, {});
};
const isRecord = (value) => {
return typeof value === "object" && value !== null;
};
const looksSplitLikeNode = (value) => {
return "type" in value || "ratio" in value || "panes" in value;
};
const sanitizeLayoutForSchemaValidation = (node) => {
if (!isRecord(node)) return node;
if (looksSplitLikeNode(node)) {
const panes = Array.isArray(node.panes) ? node.panes.map((child) => sanitizeLayoutForSchemaValidation(child)) : node.panes;
return {
type: node.type,
ratio: node.ratio,
panes
};
}
return {
name: node.name,
command: node.command,
cwd: node.cwd,
env: normalizeEnv(node.env),
delay: node.delay,
title: node.title,
focus: node.focus,
ephemeral: node.ephemeral,
closeOnError: node.closeOnError
};
};
const convertLayoutIssueToCompileError = ({ issue, source, basePath, layout }) => {
const issueParamCode = getIssueParamCode(issue);
if (isMissingArrayIssue(issue, "panes")) return compileError("LAYOUT_PANES_MISSING", {
source,
message: "panes array is missing or empty",
path: `${basePath}.panes`
});
if (isMissingArrayIssue(issue, "ratio")) return compileError("LAYOUT_RATIO_MISSING", {
source,
message: "ratio array is missing or empty",
path: `${basePath}.ratio`
});
if (issue.path.includes("type")) return compileError("LAYOUT_INVALID_ORIENTATION", {
source,
message: "layout.type must be horizontal or vertical",
path: `${basePath}.type`,
details: { type: getValueAtPath(layout, issue.path) }
});
if (issueParamCode === LayoutIssueParamCodes.RATIO_LENGTH_MISMATCH || issue.message.includes("Number of elements in ratio array does not match number of elements in panes array")) return compileError("LAYOUT_RATIO_MISMATCH", {
source,
message: "ratio and panes arrays must have the same length",
path: basePath,
details: getRatioLengthDetails(layout)
});
if (issueParamCode === LayoutIssueParamCodes.RATIO_WEIGHT_MISSING || issue.message.includes("ratio must include at least one numeric weight")) {
const ratio = isRecord(layout) ? layout.ratio : void 0;
return compileError("RATIO_WEIGHT_MISSING", {
source,
message: "ratio must include at least one numeric weight",
path: `${basePath}.ratio`,
details: { ratio }
});
}
if (issue.path.includes("ratio")) return compileError("RATIO_INVALID_VALUE", {
source,
message: "ratio value must be a positive number or \"<positive-integer>c\"",
path: formatPath(basePath, issue.path),
details: { value: getValueAtPath(layout, issue.path) }
});
return compileError("LAYOUT_INVALID_NODE", {
source,
message: "Layout node is invalid",
path: formatPath(basePath, issue.path),
details: {
issue: issue.message,
node: getValueAtPath(layout, issue.path)
}
});
};
const isMissingArrayIssue = (issue, field) => {
if (issue.path.length !== 1 || issue.path[0] !== field) return false;
return issue.code === "invalid_type" || issue.code === "too_small" && issue.type === "array";
};
const getIssueParamCode = (issue) => {
if (issue.code !== z.ZodIssueCode.custom) return;
const code = issue.params?.code;
return typeof code === "string" ? code : void 0;
};
const getRatioLengthDetails = (layout) => {
if (!isRecord(layout)) return;
const ratio = layout.ratio;
const panes = layout.panes;
if (!Array.isArray(ratio) || !Array.isArray(panes)) return;
return {
ratioLength: ratio.length,
panesLength: panes.length
};
};
const formatPath = (basePath, path) => {
if (path.length === 0) return basePath;
return path.reduce((accumulator, segment) => {
if (typeof segment === "number") return `${accumulator}[${segment}]`;
return `${accumulator}.${segment}`;
}, basePath);
};
const getValueAtPath = (value, path) => {
let current = value;
for (const segment of path) {
if (typeof segment === "number") {
if (!Array.isArray(current)) return;
current = current[segment];
continue;
}
if (!isRecord(current)) return;
current = current[segment];
}
return current;
};
const compileError = (code, error) => {
return createCoreError("compile", {
code,
message: error.message,
source: error.source,
path: error.path,
details: error.details
});
};
//#endregion
//#region src/core/planner.ts
const createLayoutPlan = ({ preset }) => {
if (!preset.layout) {
const terminal = createTerminalNode({
id: "root",
terminal: {
kind: "terminal",
name: preset.name,
command: preset.command
},
focusOverride: true
});
return { plan: {
root: terminal,
focusPaneId: terminal.id
} };
}
const { node, focusPaneIds, terminalPaneIds } = buildLayoutNode(preset.layout, {
parentId: "root",
path: "preset.layout",
source: preset.metadata.source
});
if (focusPaneIds.length > 1) throw planError("FOCUS_CONFLICT", {
message: "Multiple panes specify focus=true",
path: "preset.layout",
source: preset.metadata.source,
details: { focusPaneIds }
});
if (terminalPaneIds.length === 0) throw planError("NO_TERMINAL_PANES", {
message: "No terminal panes are defined",
path: "preset.layout",
source: preset.metadata.source
});
const focusPaneId = focusPaneIds[0] ?? terminalPaneIds[0];
return { plan: {
root: ensureFocus(node, focusPaneId),
focusPaneId
} };
};
const buildLayoutNode = (node, context) => {
if (node.kind === "split") return buildSplitNode(node, context);
return {
node: createTerminalNode({
id: context.parentId,
terminal: node
}),
focusPaneIds: node.focus === true ? [context.parentId] : [],
terminalPaneIds: [context.parentId]
};
};
const buildSplitNode = (node, context) => {
const ratio = validateRatioEntries(node.ratio, context);
const panes = [];
const focusPaneIds = [];
const terminalPaneIds = [];
for (let index = 0; index < node.panes.length; index += 1) {
const childContext = {
parentId: `${context.parentId}.${index}`,
path: `${context.path}.panes[${index}]`,
source: context.source
};
const childResult = buildLayoutNode(node.panes[index], childContext);
panes.push(childResult.node);
focusPaneIds.push(...childResult.focusPaneIds);
terminalPaneIds.push(...childResult.terminalPaneIds);
}
return {
node: {
kind: "split",
id: context.parentId,
orientation: node.orientation,
ratio,
panes
},
focusPaneIds,
terminalPaneIds
};
};
const createTerminalNode = ({ id, terminal, focusOverride }) => {
return {
kind: "terminal",
id,
name: terminal.name,
command: terminal.command,
cwd: terminal.cwd,
env: terminal.env,
delay: terminal.delay,
title: terminal.title,
options: terminal.options,
focus: focusOverride === true ? true : terminal.focus === true,
ephemeral: terminal.ephemeral,
closeOnError: terminal.closeOnError
};
};
const ensureFocus = (node, focusPaneId) => {
if (node.kind === "terminal") return {
...node,
focus: node.id === focusPaneId
};
return {
...node,
panes: node.panes.map((pane) => ensureFocus(pane, focusPaneId))
};
};
const validateRatioEntries = (ratio, context) => {
let hasWeight = false;
for (let index = 0; index < ratio.length; index += 1) {
const value = ratio[index];
if (value?.kind === "weight") {
if (!Number.isFinite(value.weight) || value.weight <= 0) throw planError("RATIO_INVALID_VALUE", {
message: "ratio value must be a positive number or \"<positive-integer>c\"",
path: `${context.path}.ratio[${index}]`,
source: context.source,
details: { value }
});
hasWeight = true;
continue;
}
if (value?.kind === "fixed-cells") {
if (!Number.isInteger(value.cells) || value.cells <= 0) throw planError("RATIO_INVALID_VALUE", {
message: "ratio value must be a positive number or \"<positive-integer>c\"",
path: `${context.path}.ratio[${index}]`,
source: context.source,
details: { value }
});
continue;
}
throw planError("RATIO_INVALID_VALUE", {
message: "ratio entry is invalid",
path: `${context.path}.ratio[${index}]`,
source: context.source,
details: { value }
});
}
if (!hasWeight) throw planError("RATIO_WEIGHT_MISSING", {
message: "ratio must include at least one numeric weight",
path: `${context.path}.ratio`,
source: context.source,
details: { ratio }
});
return ratio;
};
const planError = (code, error) => {
return createCoreError("plan", {
code,
message: error.message,
source: error.source,
path: error.path,
details: error.details
});
};
//#endregion
//#region src/core/emitter.ts
const emitPlan = ({ plan }) => {
const steps = [];
collectSplitSteps(plan.root, steps);
steps.push({
id: `${plan.focusPaneId}:focus`,
kind: "focus",
summary: `select pane ${plan.focusPaneId}`,
targetPaneId: plan.focusPaneId
});
const hash = createPlanHash(plan, steps);
const initialPaneId = determineInitialPaneId(plan.root);
const terminals = collectTerminals(plan.root);
return {
steps,
summary: {
stepsCount: steps.length,
focusPaneId: plan.focusPaneId,
initialPaneId
},
terminals,
hash
};
};
const collectSplitSteps = (node, steps) => {
if (node.kind === "terminal") return;
appendSplitSteps(node, steps);
node.panes.forEach((pane) => collectSplitSteps(pane, steps));
};
const appendSplitSteps = (node, steps) => {
const directionFlag = node.orientation === "horizontal" ? "-h" : "-v";
const hasFixedCells = node.ratio.some((entry) => entry.kind === "fixed-cells");
for (let index = 1; index < node.panes.length; index += 1) {
const targetPaneId = node.panes[index - 1]?.id ?? node.id;
const createdPaneId = node.panes[index]?.id;
const splitSizing = hasFixedCells ? buildDynamicSplitSizing(node.ratio, index) : buildPercentSplitSizing(node.ratio, index);
const percentage = splitSizing.mode === "percent" ? splitSizing.percentage : void 0;
steps.push({
id: `${node.id}:split:${index}`,
kind: "split",
summary: `split ${targetPaneId} (${directionFlag})`,
targetPaneId,
createdPaneId,
orientation: node.orientation,
percentage,
splitSizing
});
}
};
const buildPercentSplitSizing = (ratio, index) => {
const remainingIncludingTarget = ratio.slice(index - 1).reduce((sum, entry) => sum + (entry.kind === "weight" ? entry.weight : 0), 0);
const remainingAfterTarget = ratio.slice(index).reduce((sum, entry) => sum + (entry.kind === "weight" ? entry.weight : 0), 0);
return {
mode: "percent",
percentage: clampPercent(remainingIncludingTarget <= 0 ? 0 : remainingAfterTarget / remainingIncludingTarget * 100)
};
};
const buildDynamicSplitSizing = (ratio, index) => {
const target = ratio[index - 1];
const remaining = ratio.slice(index);
return {
mode: "dynamic-cells",
target,
remainingFixedCells: remaining.reduce((sum, entry) => {
return entry.kind === "fixed-cells" ? sum + entry.cells : sum;
}, 0),
remainingWeight: remaining.reduce((sum, entry) => {
return entry.kind === "weight" ? sum + entry.weight : sum;
}, 0),
remainingWeightPaneCount: remaining.reduce((count, entry) => {
return entry.kind === "weight" ? count + 1 : count;
}, 0)
};
};
const clampPercent = (value) => {
return Math.min(99, Math.max(1, Math.round(value)));
};
const collectTerminals = (node) => {
if (node.kind === "terminal") return [{
virtualPaneId: node.id,
command: node.command,
cwd: node.cwd,
env: node.env,
delay: node.delay,
title: node.title,
focus: node.focus,
name: node.name,
ephemeral: node.ephemeral,
closeOnError: node.closeOnError
}];
return node.panes.flatMap((pane) => collectTerminals(pane));
};
const determineInitialPaneId = (node) => {
if (node.kind === "terminal") return node.id;
let current = node;
while (current.kind === "split") current = current.panes[0];
return current.id;
};
const createPlanHash = (plan, steps) => {
const digest = createHash("sha256");
const normalized = {
focusPaneId: plan.focusPaneId,
root: plan.root,
steps
};
digest.update(JSON.stringify(normalized));
return digest.digest("hex");
};
//#endregion
//#region src/cli/error-handling.ts
const createCliErrorHandlers = ({ getLogger }) => {
const handleCoreError = (error) => {
const header = [`[${error.kind}]`, `[${error.code}]`];
if (typeof error.path === "string" && error.path.length > 0) header.push(`[${error.path}]`);
const lines = [`${header.join(" ")} ${error.message}`.trim()];
if (typeof error.source === "string" && error.source.length > 0) lines.push(`source: ${error.source}`);
const commandDetail = error.details?.command;
if (Array.isArray(commandDetail)) {
const parts = commandDetail.filter((segment) => typeof segment === "string");
if (parts.length > 0) lines.push(`command: ${parts.join(" ")}`);
} else if (typeof commandDetail === "string" && commandDetail.length > 0) lines.push(`command: ${commandDetail}`);
const stderrDetail = error.details?.stderr;
if (typeof stderrDetail === "string" && stderrDetail.length > 0) lines.push(`stderr: ${stderrDetail}`);
else if (stderrDetail !== void 0) lines.push(`stderr: ${String(stderrDetail)}`);
getLogger().error(lines.join("\n"));
return 1;
};
const handleError = (error) => {
if (error instanceof Error) getLogger().error(error.message, error);
else getLogger().error("An unexpected error occurred");
return 1;
};
const handlePipelineFailure = (error) => {
if (isCoreError(error)) return handleCoreError(error);
return handleError(error);
};
return {
handleCoreError,
handleError,
handlePipelineFailure
};
};
//#endregion
//#region src/utils/logger.ts
let LogLevel = /* @__PURE__ */ function(LogLevel) {
LogLevel[LogLevel["ERROR"] = 0] = "ERROR";
LogLevel[LogLevel["WARN"] = 1] = "WARN";
LogLevel[LogLevel["INFO"] = 2] = "INFO";
LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG";
return LogLevel;
}({});
const resolveDefaultLogLevel = () => {
if (process.env.VDE_DEBUG === "true") return LogLevel.DEBUG;
if (process.env.VDE_VERBOSE === "true") return LogLevel.INFO;
return LogLevel.WARN;
};
const formatMessage = (prefix, message) => {
return prefix ? `${prefix} ${message}` : message;
};
const createLogger = (options = {}) => {
const level = options.level ?? resolveDefaultLogLevel();
const prefix = options.prefix ?? "";
const build = (nextPrefix, nextLevel) => {
const resolvedPrefix = nextPrefix;
return {
level: nextLevel,
prefix: resolvedPrefix,
error(message, error) {
if (nextLevel >= LogLevel.ERROR) {
console.error(chalk.red(formatMessage(resolvedPrefix, `Error: ${message}`)));
if (error && process.env.VDE_DEBUG === "true") console.error(chalk.gray(error.stack));
}
},
warn(message) {
if (nextLevel >= LogLevel.WARN) console.warn(chalk.yellow(formatMessage(resolvedPrefix, message)));
},
info(message) {
if (nextLevel >= LogLevel.INFO) console.log(formatMessage(resolvedPrefix, message));
},
debug(message) {
if (nextLevel >= LogLevel.DEBUG) console.log(chalk.gray(formatMessage(resolvedPrefix, `[DEBUG] ${message}`)));
},
success(message) {
console.log(chalk.green(formatMessage(resolvedPrefix, message)));
},
createChild(suffix) {
return build(resolvedPrefix ? `${resolvedPrefix} ${suffix}` : suffix, nextLevel);
}
};
};
return build(prefix, level);
};
//#endregion
//#region src/cli/runtime-and-list.ts
const applyRuntimeOptions = ({ runtimeOptions, createLogger, presetManager }) => {
const logger = runtimeOptions.verbose === true ? createLogger({ level: LogLevel.INFO }) : createLogger();
if (typeof runtimeOptions.config === "string" && runtimeOptions.config.length > 0 && typeof presetManager.setConfigPath === "function") presetManager.setConfigPath(runtimeOptions.config);
return logger;
};
const listPresets = async ({ presetManager, logger, onError, output = (line) => console.log(line) }) => {
try {
await presetManager.loadConfig();
const presets = presetManager.listPresets();
if (presets.length === 0) {
logger.warn("No presets defined");
return 0;
}
output(chalk.bold("Available presets:\n"));
const maxKeyLength = Math.max(...presets.map((preset) => preset.key.length));
presets.forEach((preset) => {
const paddedKey = preset.key.padEnd(maxKeyLength + 2);
const description = preset.description ?? "";
output(` ${chalk.cyan(paddedKey)} ${description}`);
});
return 0;
} catch (error) {
return onError(error);
}
};
//#endregion
//#region src/backends/pane-tracking.ts
const updatePaneSizes = ({ paneSizes, targetPaneId, createdPaneId, orientation, targetCells, createdCells }) => {
const base = paneSizes.get(targetPaneId);
if (base === void 0) return;
if (orientation === "horizontal") {
paneSizes.set(targetPaneId, {
cols: targetCells,
rows: base.rows
});
paneSizes.set(createdPaneId, {
cols: createdCells,
rows: base.rows
});
return;
}
paneSizes.set(targetPaneId, {
cols: base.cols,
rows: targetCells
});
paneSizes.set(createdPaneId, {
cols: base.cols,
rows: createdCells
});
};
//#endregion
//#region src/executor/real-executor.ts
const parseCommand$2 = (commandOrArgs) => {
return typeof commandOrArgs === "string" ? commandOrArgs.split(" ").filter((segment) => segment.length > 0).slice(1) : commandOrArgs;
};
const toCommandString$2 = (args) => {
return ["tmux", ...args].join(" ");
};
const createRealExecutor = (options = {}) => {
const logger = createLogger({
level: options.verbose ?? false ? LogLevel.INFO : LogLevel.WARN,
prefix: "[tmux]"
});
const execute = async (commandOrArgs) => {
const args = parseCommand$2(commandOrArgs);
const commandString = toCommandString$2(args);
logger.info(`Executing: ${commandString}`);
try {
return (await execa("tmux", args)).stdout;
} catch (error) {
const execaError = error;
throw createTmuxError("Failed to execute tmux command", ErrorCodes.TMUX_COMMAND_FAILED, {
command: commandString,
exitCode: execaError.exitCode,
stderr: execaError.stderr
});
}
};
return {
execute,
async executeMany(commandsList) {
for (const args of commandsList) await execute(args);
},
isDryRun() {
return false;
},
logCommand(command) {
logger.info(`Executing: ${command}`);
}
};
};
//#endregion
//#region src/executor/dry-run-executor.ts
const parseCommand$1 = (commandOrArgs) => {
return typeof commandOrArgs === "string" ? commandOrArgs.split(" ").filter((segment) => segment.length > 0).slice(1) : commandOrArgs;
};
const toCommandString$1 = (args) => {
return ["tmux", ...args].join(" ");
};
const createDryRunExecutor = (options = {}) => {
const logger = createLogger({
level: options.verbose ?? false ? LogLevel.INFO : LogLevel.WARN,
prefix: "[tmux] [DRY RUN]"
});
const execute = async (commandOrArgs) => {
const commandString = toCommandString$1(parseCommand$1(commandOrArgs));
logger.info(`Would execute: ${commandString}`);
return "";
};
return {
execute,
async executeMany(commandsList) {
for (const args of commandsList) await execute(args);
},
isDryRun() {
return true;
},
logCommand(command) {
logger.info(`Would execute: ${command}`);
}
};
};
//#endregion
//#region src/executor/mock-executor.ts
const parseCommand = (commandOrArgs) => {
return typeof commandOrArgs === "string" ? commandOrArgs.split(" ").filter((segment) => segment.length > 0).slice(1) : commandOrArgs;
};
const isInTmuxSession = () => {
return Boolean(process.env.TMUX);
};
const toCommandString = (args) => {
return ["tmux", ...args].join(" ");
};
const createMockExecutor = () => {
let mockPaneCounter = 0;
let mockPaneIds = ["%0"];
let mockProtectedPaneIds = [];
let executedCommands = [];
const execute = async (commandOrArgs) => {
const args = parseCommand(commandOrArgs);
executedCommands.push(args);
if (args[0] === "new-window") {
mockPaneCounter = 0;
mockPaneIds = ["%0"];
return "%0";
}
if (args.includes("display-message")) {
const parts = [];
if (args.includes("#{pane_id}")) parts.push(mockPaneIds[0] ?? "%0");
if (args.includes("#{pane_width}")) parts.push("200");
if (args.includes("#{pane_height}")) parts.push("60");
if (parts.length > 0) return parts.join(" ");
}
if (args.includes("list-panes") && args.includes("#{pane_id} #{@vde_sidebar}")) {
const protectedPaneIds = new Set(mockProtectedPaneIds);
return mockPaneIds.map((paneId) => `${paneId}\t${protectedPaneIds.has(paneId) ? "1" : ""}`).join("\n");
}
if (args.includes("list-panes") && args.includes("#{pane_id}")) return mockPaneIds.join("\n");
if (args[0] === "kill-pane" && args.includes("-a")) {
const targetIndex = args.indexOf("-t");
const targetPane = (targetIndex >= 0 && targetIndex + 1 < args.length ? args[targetIndex + 1] : mockPaneIds[0]) ?? "%0";
const survivingPaneIds = mockPaneIds.filter((paneId) => paneId === targetPane || mockProtectedPaneIds.includes(paneId));
mockPaneIds = survivingPaneIds.includes(targetPane) ? survivingPaneIds : [targetPane, ...survivingPaneIds];
const parsedCounter = Number(targetPane.replace("%", ""));
if (!Number.isNaN(parsedCounter)) mockPaneCounter = parsedCounter;
return "";
}
if (args[0] === "kill-pane") {
const targetIndex = args.indexOf("-t");
const targetPane = targetIndex >= 0 && targetIndex + 1 < args.length ? args[targetIndex + 1] : void 0;
if (typeof targetPane === "string") mockPaneIds = mockPaneIds.filter((paneId) => paneId !== targetPane);
return "";
}
if (args.includes("split-window")) {
mockPaneCounter += 1;
const newPaneId = `%${mockPaneCounter}`;
mockPaneIds = [...mockPaneIds, newPaneId];
if (args.includes("-P") && args.includes("#{pane_id}")) return newPaneId;
}
return "";
};
return {
execute,
async executeMany(commandsList) {
for (const args of commandsList) await execute(args);
},
isDryRun() {
return true;
},
logCommand() {},
getExecutedCommands() {
return executedCommands;
},
clearExecutedCommands() {
executedCommands = [];
},
setMockPaneIds(paneIds) {
mockPaneIds = [...paneIds];
},
getPaneIds() {
return mockPaneIds;
},
setMockProtectedPaneIds(paneIds) {
mockProtectedPaneIds = [...paneIds];
},
isInTmuxSession,
async verifyTmuxEnvironment() {
if (!isInTmuxSession()) throw createEnvironmentError("Must be run inside a tmux session", ErrorCodes.NOT_IN_TMUX_SESSION, { hint: "Please start a tmux session and try again" });
},
getCommandString: toCommandString,
async getCurrentSessionName() {
return "mock-session";
}
};
};
//#endregion
//#region src/backends/tmux/executor.ts
const createTmuxExecutor = (options = {}) => {
const executor = resolveExecutor(options);
const isInTmuxSession = () => {
return Boolean(process.env.TMUX);
};
const verifyTmuxEnvironment = async () => {
if (!isInTmuxSession()) throw createEnvironmentError("Must be run inside a tmux session", ErrorCodes.NOT_IN_TMUX_SESSION, { hint: "Please start a tmux session and try again" });
if (executor.isDryRun()) return;
try {
await execa("tmux", ["-V"]);
} catch (_error) {
throw createEnvironmentError("tmux is not installed", ErrorCodes.TMUX_NOT_FOUND, { hint: "Please install tmux" });
}
};
const execute = async (commandOrArgs) => {
return executor.execute(commandOrArgs);
};
const executeMany = async (commandsList) => {
for (const command of commandsList) await execute(command);
};
const getCommandString = (args) => {
return ["tmux", ...args].join(" ");
};
const getCurrentSessionName = async () => {
return execute([
"display-message",
"-p",
"#{session_name}"
]);
};
return {
verifyTmuxEnvironment,
execute,
executeMany,
isInTmuxSession,
getCurrentSessionName,
getCommandString,
getExecutor: () => executor
};
};
const resolveExecutor = (options) => {
if (options.executor) return options.executor;
if (options.dryRun === true) return createDryRunExecutor({ verbose: options.verbose });
if (isTestEnvironment()) return createMockExecutor();
return createRealExecutor({ verbose: options.verbose });
};
const isTestEnvironment = () => {
return process.env.VDE_TEST_MODE === "true" || process.env.NODE_ENV === "test" || process.env.VITEST === "true";
};
//#endregion
//#region src/utils/async.ts
const waitForDelay = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
//#endregion
//#region src/utils/pane-map.ts
const resolvePaneMapping = (paneMap, virtualId) => {
const direct = paneMap.get(virtualId);
if (typeof direct === "string" && direct.length > 0) return direct;
let ancestor = virtualId;
while (ancestor.includes(".")) {
ancestor = ancestor.slice(0, ancestor.lastIndexOf("."));
const candidate = paneMap.get(ancestor);
if (typeof candidate === "string" && candidate.length > 0) {
paneMap.set(virtualId, candidate);
return candidate;
}
}
for (const [key, value] of paneMap.entries()) if (key.startsWith(`${virtualId}.`)) {
if (typeof value === "string" && value.length > 0) {
paneMap.set(virtualId, value);
return value;
}
}
};
//#endregion
//#region src/executor/split-step.ts
const asSplitStep = (step, field) => {
if (step.kind !== "split") throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: `Non-split step cannot resolve split ${field}`,
path: step.id,
details: { kind: step.kind }
});
return step;
};
const resolveSplitOrientation = (step) => {
const splitStep = asSplitStep(step, "orientation");
if (splitStep.orientation === "horizontal" || splitStep.orientation === "vertical") return splitStep.orientation;
throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: "Split step missing orientation metadata",
path: splitStep.id,
details: { orientation: splitStep.orientation }
});
};
const resolveLegacySplitSizing = (step) => {
if (typeof step.percentage === "number" && Number.isFinite(step.percentage)) return {
mode: "percent",
percentage: step.percentage
};
};
const resolveSplitSizingMetadata = (step) => {
if (step.splitSizing !== void 0) return step.splitSizing;
return resolveLegacySplitSizing(step);
};
const resolveSplitSize = (step, context = {}) => {
const splitStep = asSplitStep(step, "sizing");
const splitSizing = resolveSplitSizingMetadata(splitStep);
if (splitSizing === void 0) throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: "Split step missing sizing metadata",
path: splitStep.id,
details: {
splitSizing: splitStep.splitSizing,
percentage: splitStep.percentage
}
});
if (splitSizing.mode === "percent") {
if (Number.isFinite(splitSizing.percentage)) return {
mode: "percent",
percentage: String(clampSplitPercentage(splitSizing.percentage))
};
throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: "Split step missing percentage metadata",
path: splitStep.id,
details: { percentage: splitSizing.percentage }
});
}
const dynamic = splitSizing;
const resolvedPaneCells = context.paneCells;
if (typeof resolvedPaneCells !== "number" || !Number.isInteger(resolvedPaneCells) || resolvedPaneCells <= 0) throw createCoreError("execution", {
code: ErrorCodes.SPLIT_SIZE_RESOLUTION_FAILED,
message: "Pane size is unavailable for dynamic split sizing",
path: splitStep.id,
details: buildDynamicResolutionDetails(splitStep, dynamic, context)
});
const { remainingFixedCells, remainingWeight, remainingWeightPaneCount, target } = dynamic;
if (!Number.isInteger(remainingFixedCells) || remainingFixedCells < 0 || !Number.isFinite(remainingWeight) || remainingWeight < 0 || !Number.isInteger(remainingWeightPaneCount) || remainingWeightPaneCount < 0) throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: "Split step has invalid dynamic sizing metadata",
path: splitStep.id,
details: buildDynamicResolutionDetails(splitStep, dynamic, context)
});
const minTargetCells = target.kind === "fixed-cells" ? target.cells : 1;
const minCreatedCells = remainingFixedCells + remainingWeightPaneCount;
if (!Number.isInteger(minTargetCells) || minTargetCells <= 0 || !Number.isInteger(minCreatedCells) || minCreatedCells < 0) throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: "Split step has invalid minimum-cell constraints",
path: splitStep.id,
details: buildDynamicResolutionDetails(splitStep, dynamic, context)
});
if (resolvedPaneCells < minTargetCells + minCreatedCells) throw createCoreError("execution", {
code: ErrorCodes.SPLIT_SIZE_RESOLUTION_FAILED,
message: "Pane is too small for requested fixed and weighted splits",
path: splitStep.id,
details: buildDynamicResolutionDetails(splitStep, dynamic, context)
});
let targetCells;
if (target.kind === "fixed-cells") targetCells = target.cells;
else {
if (!Number.isFinite(target.weight) || target.weight <= 0) throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: "Split step has invalid weight metadata",
path: splitStep.id,
details: buildDynamicResolutionDetails(splitStep, dynamic, context)
});
const availableForWeights = resolvedPaneCells - remainingFixedCells;
const weightTotal = target.weight + remainingWeight;
if (!Number.isFinite(weightTotal) || weightTotal <= 0) throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: "Split step has invalid dynamic weight totals",
path: splitStep.id,
details: buildDynamicResolutionDetails(splitStep, dynamic, context)
});
targetCells = clamp(Math.round(availableForWeights * target.weight / weightTotal), minTargetCells, resolvedPaneCells - minCreatedCells);
}
const createdCells = resolvedPaneCells - targetCells;
if (targetCells < minTargetCells || createdCells < minCreatedCells || targetCells <= 0 || createdCells <= 0) throw createCoreError("execution", {
code: ErrorCodes.SPLIT_SIZE_RESOLUTION_FAILED,
message: "Unable to resolve split size without violating pane minimums",
path: splitStep.id,
details: buildDynamicResolutionDetails(splitStep, dynamic, context, {
targetCells,
createdCells
})
});
return {
mode: "cells",
cells: String(createdCells),
targetCells,
createdCells
};
};
const clampSplitPercentage = (value) => {
return Math.min(99, Math.max(1, Math.round(value)));
};
const clamp = (value, min, max) => {
return Math.min(max, Math.max(min, value));
};
const buildDynamicResolutionDetails = (splitStep, splitSizing, context, extra = {}) => {
return {
stepId: splitStep.id,
paneId: context.paneId,
paneCells: context.paneCells,
targetSpec: splitSizing.target,
remainingFixedCells: splitSizing.remainingFixedCells,
remainingWeight: splitSizing.remainingWeight,
remainingWeightPaneCount: splitSizing.remainingWeightPaneCount,
requiredVersion: context.requiredVersion,
detectedVersion: context.detectedVersion,
rawPaneRecord: context.rawPaneRecord,
...extra
};
};
//#endregion
//#region src/utils/template-tokens.ts
const TemplateTokenErrorImpl = function TemplateTokenError(message, tokenType, availablePanes) {
const error = new Error(message);
Object.setPrototypeOf(error, TemplateTokenErrorImpl.prototype);
error.name = "TemplateTokenError";
error.tokenType = tokenType;
error.availablePanes = availablePanes;
return error;
};
TemplateTokenErrorImpl.prototype = Object.create(Error.prototype);
TemplateTokenErrorImpl.prototype.constructor = TemplateTokenErrorImpl;
const TemplateTokenError = TemplateTokenErrorImpl;
/**
* Replaces template tokens in a command string with actual pane IDs.
*
* Uses a single-pass regex replacement to avoid nested token issues.
* All tokens are replaced in a single pass, preventing already-replaced
* values from being re-processed.
*
* @param input - Object containing the command and pane ID mappings
* @returns The command string with all template tokens replaced
* @throws TemplateTokenError if a referenced pane name is not found in the mapping,
* or if {{window_id}} is used but no `windowId` was supplied
*/
const replaceTemplateTokens = ({ command, currentPaneRealId, focusPaneRealId, nameToRealIdMap, windowId }) => {
return command.replace(/\{\{(this_pane|focus_pane|window_id|pane_id:([^}]+))\}\}/g, (match, tokenContent, paneName) => {
if (tokenContent === "this_pane") return currentPaneRealId;
if (tokenContent === "focus_pane") return focusPaneRealId;
if (tokenContent === "window_id") {
if (windowId === void 0) throw new TemplateTokenError("Window ID is not available. {{window_id}} can only be resolved in hooks.afterApply.", "window_id");
return windowId;
}
if (tokenContent.startsWith("pane_id:") && paneName !== void 0) {
const trimmedName = paneName.trim();
const paneId = nameToRealIdMap.get(trimmedName);
if (paneId === void 0) throw new TemplateTokenError(`Pane name "${trimmedName}" not found. Available panes: ${Array.from(nameToRealIdMap.keys()).join(", ")}`, "pane_id", Array.from(nameToRealIdMap.keys()));
return paneId;
}
return match;
});
};
/**
* Builds a mapping from pane names to real pane IDs.
*
* **Duplicate Name Handling:**
* If multiple panes share the same name, the last one in the terminals
* array wins. This follows the iteration order of the layout tree.
* It's recommended to use unique names for panes to avoid ambiguity
* in template token references.
*
* **Virtual to Real ID Resolution:**
* Only panes with successfully resolved real IDs (present in paneMap)
* are included in the resulting map. This ensures that template tokens
* only reference panes that have been properly created.
*
* @param terminals - Array of emitted terminals from the layout plan
* @param paneMap - Map from virtual pane IDs to real pane IDs (backend-specific)
* @returns A map from pane names to real pane IDs
*/
const buildNameToRealIdMap = (terminals, paneMap) => {
const nameToRealIdMap = /* @__PURE__ */ new Map();
for (const terminal of terminals) {
const realId = paneMap.get(terminal.virtualPaneId);
if (realId !== void 0) nameToRealIdMap.set(terminal.name, realId);
}
return nameToRealIdMap;
};
//#endregion
//#region src/executor/terminal-command-preparation.ts
const SINGLE_QUOTE$1 = "'";
const SHELL_SINGLE_QUOTE_ESCAPE$1 = `'"'"'`;
const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
const shellQuoteLiteral = (value) => {
return `'${value.split(SINGLE_QUOTE$1).join(SHELL_SINGLE_QUOTE_ESCAPE$1)}'`;
};
const assertValidEnvKey = (key) => {
if (!ENV_KEY_PATTERN.test(key)) throw new Error(`Invalid environment variable name: ${key}`);
};
const normalizeDelay = (delay) => {
return typeof delay === "number" && Number.isFinite(delay) && delay > 0 ? delay : 0;
};
const applyEphemeralSuffix = (command, terminal) => {
if (terminal.ephemeral !== true) return command;
return terminal.closeOnError === true ? `${command}; exit` : `${command}; [ $? -eq 0 ] && exit`;
};
const prepareTerminalCommands = ({ terminals, focusPaneVirtualId, resolveRealPaneId, onTemplateTokenError }) => {
const paneMap = /* @__PURE__ */ new Map();
for (const terminal of terminals) paneMap.set(terminal.virtualPaneId, resolveRealPaneId(terminal.virtualPaneId));
const focusPaneRealId = paneMap.get(focusPaneVirtualId) ?? resolveRealPaneId(focusPaneVirtualId);
const nameToRealIdMap = buildNameToRealIdMap(terminals, paneMap);
return {
focusPaneRealId,
commands: terminals.map((terminal) => {
const realPaneId = paneMap.get(terminal.virtualPaneId);
if (realPaneId === void 0) throw new Error(`Unknown pane: ${terminal.virtualPaneId}`);
const cwdCommand = typeof terminal.cwd === "string" && terminal.cwd.length > 0 ? `cd -- ${shellQuoteLiteral(terminal.cwd)}` : void 0;
const envCommands = terminal.env === void 0 ? [] : Object.entries(terminal.env).map(([key, value]) => {
assertValidEnvKey(key);
return {
key,
command: `export ${key}=${shellQuoteLiteral(String(value))}`
};
});
const title = typeof terminal.title === "string" && terminal.title.length > 0 ? terminal.title : void 0;
let command;
if (typeof terminal.command === "string" && terminal.command.length > 0) try {
const commandUsesFocusToken = terminal.command.includes("{{focus_pane}}");
command = {
text: applyEphemeralSuffix(replaceTemplateTokens({
command: terminal.command,
currentPaneRealId: realPaneId,
focusPaneRealId: commandUsesFocusToken ? focusPaneRealId : "",
nameToRealIdMap
}), terminal),
delayMs: normalizeDelay(terminal.delay)
};
} catch (error) {
if (error instanceof TemplateTokenError) return onTemplateTokenError({
terminal,
error
});
throw error;
}
return {
terminal,
realPaneId,
cwdCommand,
envCommands,
title,
command
};
})
};
};
//#endregion
//#region src/executor/plan-runner-helpers.ts
const executeSplitStep = async ({ step, executor, paneMap, detectedVersion }) => {
const targetVirtualId = ensureNonEmpty(step.targetPaneId, () => raiseExecutionError(ErrorCodes.MISSING_TARGET, {
message: "Split step missing target pane metadata",
path: step.id
}));
const targetRealId = ensureNonEmpty(resolvePaneId(paneMap, targetVirtualId), () => raiseExecutionError(ErrorCodes.INVALID_PANE, {
message: `Unknown target pane: ${targetVirtualId}`,
path: step.id
}));
const panesBefore = await listPaneIds(executor, step);
const splitCommand = await buildSplitCommand({
step,
targetRealId,
executor,
detectedVersion
});
await executeCommand(executor, splitCommand, {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: `Failed to execute split step ${step.id}`,
path: step.id,
details: { command: splitCommand }
});
const newPaneId = ensureNonEmpty(findNewPaneId$1(panesBefore, await listPaneIds(executor, step)), () => raiseExecutionError(ErrorCodes.INVALID_PANE, {
message: "Unable to determine newly created pane",
path: step.id
}));
const createdVirtualId = step.createdPaneId;
if (typeof createdVirtualId === "string" && createdVirtualId.length > 0) registerPane(paneMap, createdVirtualId, newPaneId);
};
const executeFocusStep = async ({ step, executor, paneMap }) => {
const targetVirtualId = ensureNonEmpty(step.targetPaneId, () => raiseExecutionError(ErrorCodes.MISSING_TARGET, {
message: "Focus step missing target pane metadata",
path: step.id
}));
const command = buildFocusCommand(ensureNonEmpty(resolvePaneId(paneMap, targetVirtualId), () => raiseExecutionError(ErrorCodes.INVALID_PANE, {
message: `Unknown focus pane: ${targetVirtualId}`,
path: step.id
})));
await executeCommand(executor, command, {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: `Failed to execute focus step ${step.id}`,
path: step.id,
details: { command }
});
};
const executeTerminalCommands = async ({ terminals, executor, paneMap, focusPaneVirtualId }) => {
if (!paneMap.has(focusPaneVirtualId)) raiseExecutionError(ErrorCodes.INVALID_PANE, {
message: `Unknown focus pane: ${focusPaneVirtualId}`,
path: focusPaneVirtualId
});
ensureNonEmpty(resolvePaneId(paneMap, focusPaneVirtualId), () => raiseExecutionError(ErrorCodes.INVALID_PANE, {
message: `Unknown focus pane: ${focusPaneVirtualId}`,
path: focusPaneVirtualId
}));
const resolveRealPaneId = (virtualPaneId) => {
return ensureNonEmpty(resolvePaneId(paneMap, virtualPaneId), () => raiseExecutionError(ErrorCodes.INVALID_PANE, {
message: `Unknown terminal pane: ${virtualPaneId}`,
path: virtualPaneId
}));
};
const prepared = prepareTerminalCommands({
terminals,
focusPaneVirtualId,
resolveRealPaneId,
onTemplateTokenError: ({ terminal, error }) => {
throw createCoreError("execution", {
code: ErrorCodes.TEMPLATE_TOKEN_ERROR,
message: `Template token resolution failed for pane ${terminal.virtualPaneId}: ${error.message}`,
path: terminal.virtualPaneId,
details: {
command: terminal.command,
tokenType: error.tokenType,
availablePanes: error.availablePanes
}
});
}
});
for (const commandSet of prepared.commands) {
const { terminal, realPaneId } = commandSet;
if (typeof commandSet.cwdCommand === "string") await executeCommand(executor, [
"send-keys",
"-t",
realPaneId,
commandSet.cwdCommand,
"Enter"
], {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: `Failed to change directory for pane ${terminal.virtualPaneId}`,
path: terminal.virtualPaneId,
details: { cwd: terminal.cwd }
});
for (const envEntry of commandSet.envCommands) await executeCommand(executor, [
"send-keys",
"-t",
realPaneId,
envEntry.command,
"Enter"
], {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: `Failed to set environment variable ${envEntry.key}`,
path: terminal.virtualPaneId
});
if (typeof commandSet.title === "string") await executeCommand(executor, [
"select-pane",
"-t",
realPaneId,
"-T",
commandSet.title
], {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: `Failed to set pane title for pane ${terminal.virtualPaneId}`,
path: terminal.virtualPaneId,
details: { title: commandSet.title }
});
if (commandSet.command !== void 0) {
if (commandSet.command.delayMs > 0) await waitForDelay(commandSet.command.delayMs);
await executeCommand(executor, [
"send-keys",
"-t",
realPaneId,
commandSet.command.text,
"Enter"
], {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: `Failed to execute command for pane ${terminal.virtualPaneId}`,
path: terminal.virtualPaneId,
details: { command: terminal.command }
});
}
}
};
const executeCommand = async (executor, command, context) => {
try {
return await executor.execute([...command]);
} catch (error) {
if (error instanceof Error && "code" in error && "message" in error) {
const candidate = error;
throw createCoreError("execution", {
code: typeof candidate.code === "string" ? candidate.code : context.code,
message: candidate.message ?? context.message,
path: context.path,
details: candidate.details ?? context.details
});
}
throw createCoreError("execution", {
code: context.code,
message: context.message,
path: context.path,
details: context.details
});
}
};
const resolveCurrentPaneId = async ({ executor, contextPath, isDryRun }) => {
const envPaneId = process.env.TMUX_PANE;
if (typeof envPaneId === "string" && envPaneId.trim().length > 0) return normalizePaneId(envPaneId);
if (isDryRun) return "%0";
const paneId = (await executeCommand(executor, [
"display-message",
"-p",
"#{pane_id}"
], {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: "Failed to resolve current tmux pane",
path: contextPath
})).trim();
if (paneId.length === 0) throw createCoreError("execution", {
code: ErrorCodes.NOT_IN_TMUX_SESSION,
message: "Unable to determine current tmux pane",
path: contextPath
});
return normalizePaneId(paneId);
};
const listWindowPaneIds = async (executor, contextPath) => {
return (await executeCommand(executor, [
"list-panes",
"-F",
"#{pane_id}"
], {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: "Failed to list tmux panes",
path: contextPath
})).split("\n").map((pane) => pane.trim()).filter((pane) => pane.length > 0);
};
const listPaneIds = async (executor, step) => {
return listWindowPaneIds(executor, step.id);
};
const findNewPaneId$1 = (before, after) => {
const beforeSet = new Set(before);
return after.find((id) => !beforeSet.has(id));
};
const buildSplitCommand = async ({ step, targetRealId, executor, detectedVersion }) => {
const orientation = resolveSplitOrientation(step);
const directionFlag = orientation === "horizontal" ? "-h" : "-v";
if (isDynamicSplit(step)) {
const splitSize = resolveSplitSize(step, {
paneCells: await resolveTmuxPaneCells({
executor,
step,
targetRealId,
orientation,
detectedVersion
}),
paneId: targetRealId,
detectedVersion,
rawPaneRecord: {
backend: "tmux",
paneId: targetRealId,
sourceFormat: "tmux-format"
}
});
if (splitSize.mode === "cells") return [
"split-window",
directionFlag,
"-t",
targetRealId,
"-l",
splitSize.cells
];
return raiseExecutionError(ErrorCodes.INVALID_PLAN, {
message: "Dynamic split resolved to a non-cell sizing mode",
path: step.id,
details: { splitSize }
});
}
const splitSize = resolveSplitSize(step, {
paneId: targetRealId,
detectedVersion
});
if (splitSize.mode === "percent") return [
"split-window",
directionFlag,
"-t",
targetRealId,
"-p",
splitSize.percentage
];
return raiseExecutionError(ErrorCodes.INVALID_PLAN, {
message: "Percent split resolved to a non-percent sizing mode",
path: step.id,
details: { splitSize }
});
};
const buildFocusCommand = (targetRealId) => {
return [
"select-pane",
"-t",
targetRealId
];
};
const normalizePaneId = (raw) => {
const trimmed = raw.trim();
return trimmed.length === 0 ? "%0" : trimmed;
};
const registerPane = (paneMap, virtualId, realId) => {
paneMap.set(virtualId, realId);
};
const resolvePaneId = (paneMap, virtualId) => {
return resolvePaneMapping(paneMap, virtualId);
};
const resolveTmuxPaneCells = async ({ executor, step, targetRealId, orientation, detectedVersion }) => {
const format = orientation === "horizontal" ? "#{pane_width}" : "#{pane_height}";
const output = await executeCommand(executor, [
"display-message",
"-p",
"-t",
targetRealId,
format
], {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: "Failed to resolve tmux pane size",
path: step.id,
details: { command: [
"display-message",
"-p",
"-t",
targetRealId,
format
] }
});
const value = Number.parseInt(output.trim(), 10);
if (!Number.isInteger(value) || value <= 0) raiseExecutionError(ErrorCodes.SPLIT_SIZE_RESOLUTION_FAILED, {
message: "Unable to parse tmux pane size",
path: step.id,
details: {
paneId: targetRealId,
orientation,
output,
detectedVersion
}
});
return value;
};
const isDynamicSplit = (step) => {
return step.kind === "split" && step.splitSizing?.mode === "dynamic-cells";
};
const ensureNonEmpty = (value, buildError) => {
if (value === void 0 || value.length === 0) return buildError();
return value;
};
const raiseExecutionError = (code, error) => {
throw createCoreError("execution", {
code,
message: error.message,
path: error.path,
details: error.details
});
};
//#endregion
//#region src/executor/sidebar-detection.ts
const SIDEBAR_LIST_PANES_FORMAT = "#{pane_id} #{@vde_sidebar}";
const classifyWindowPanes = async (executor, contextPath, targetPaneId) => {
const output = await executeCommand(executor, typeof targetPaneId === "string" && targetPaneId.length > 0 ? [
"list-panes",
"-t",
targetPaneId,
"-F",
SIDEBAR_LIST_PANES_FORMAT
] : [
"list-panes",
"-F",
SIDEBAR_LIST_PANES_FORMAT
], {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: "Failed to list tmux panes for sidebar detection",
path: contextPath
});
const sidebarPanes = [];
const normalPanes = [];
for (const line of output.split("\n")) {
const [paneId, sidebarFlag] = line.split(" ");
if (typeof paneId !== "string" || paneId.trim().length === 0) continue;
if (sidebarFlag?.trim() === "1") sidebarPanes.push(paneId.trim());
else normalPanes.push(paneId.trim());
}
return {
sidebarPanes,
normalPanes
};
};
//#endregion
//#region src/executor/plan-runner.ts
const executePlan = async ({ emission, executor, windowName, windowMode, onConfirmKill, detectedVersion }) => {
const initialVirtualPaneId = emission.summary.initialPaneId;
if (typeof initialVirtualPaneId !== "string" || initialVirtualPaneId.length === 0) raiseExecutionError(ErrorCodes.INVALID_PLAN, {
message: "Plan emission is missing initial pane metadata",
path: "plan.initialPaneId"
});
const paneMap = /* @__PURE__ */ new Map();
const isDryRun = executor.isDryRun();
let initialPaneId;
if (windowMode === "current-window") {
const currentPaneId = await resolveCurrentPaneId({
executor,
contextPath: initialVirtualPaneId,
isDryRun
});
const { sidebarPanes, normalPanes } = await classifyWindowPanes(executor, initialVirtualPaneId, currentPaneId);
const sidebarPaneIds = new Set(sidebarPanes);
let originPaneId;
if (sidebarPaneIds.has(currentPaneId)) {
const [firstNormalPane] = normalPanes;
originPaneId = firstNormalPane !== void 0 ? firstNormalPane : await splitPaneBesideSidebar({
executor,
sidebarPaneId: currentPaneId,
contextPath: initialVirtualPaneId
});
} else originPaneId = currentPaneId;
const panesToClose = normalPanes.filter((paneId) => paneId !== originPaneId);
if (panesToClose.length > 0) {
let confirmed = true;
if (onConfirmKill !== void 0) confirmed = await onConfirmKill({
panesToClose,
dryRun: isDryRun
});
if (confirmed !== true) raiseExecutionError(ErrorCodes.USER_CANCELLED, {
message: "Aborted layout application for current window",
path: initialVirtualPaneId,
details: { panes: panesToClose }
});
for (const paneId of panesToClose) await executeCommand(executor, [
"kill-pane",
"-t",
paneId
], {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: "Failed to close existing panes",
path: initialVirtualPaneId,
details: { command: [
"kill-pane",
"-t",
paneId
] }
});
}
initialPaneId = normalizePaneId(originPaneId);
} else {
const newWindowCommand = [
"new-window",
"-P",
"-F",
"#{pane_id}"
];
if (typeof windowName === "string" && windowName.trim().length > 0) newWindowCommand.push("-n", windowName.trim());
initialPaneId = normalizePaneId(await executeCommand(executor, newWindowCommand, {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: "Failed to create tmux window",
path: initialVirtualPaneId
}));
}
registerPane(paneMap, initialVirtualPaneId, initialPaneId);
let executedSteps = 0;
for (const step of emission.steps) if (step.kind === "split") {
await executeSplitStep({
step,
executor,
paneMap,
detectedVersion
});
executedSteps += 1;
} else if (step.kind === "focus") {
await executeFocusStep({
step,
executor,
paneMap
});
executedSteps += 1;
} else raiseExecutionError(ErrorCodes.INVALID_PLAN, {
message: `Unsupported step kind in emission: ${String(step.kind)}`,
path: step.id
});
await executeTerminalCommands({
terminals: emission.terminals,
executor,
paneMap,
focusPaneVirtualId: emission.summary.focusPaneId
});
const finalRealFocus = resolvePaneId(paneMap, emission.summary.focusPaneId);
if (typeof finalRealFocus === "string" && finalRealFocus.length > 0) await executeCommand(executor, [
"select-pane",
"-t",
finalRealFocus
], {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: "Failed to restore focus",
path: emission.summary.focusPaneId
});
return {
executedSteps,
paneMap
};
};
/**
* Splits a fresh pane beside the sidebar so it can be used as the layout's origin
* pane. Only reached when the current window contains nothing but sidebar panes
* (no normal pane to reuse). The split direction is fixed to a horizontal split
* away from the sidebar (i.e. the new pane appears on the opposite side).
*
* Uses `-P -F "#{pane_id}"` to have tmux report the newly created pane id directly
* in the split-window output, rather than diffing a follow-up `list-panes` call
* against the pre-split pane set (which was fragile under concurrent pane changes).
*/
const splitPaneBesideSidebar = async ({ executor, sidebarPaneId, contextPath }) => {
const command = [
"split-window",
"-h",
"-P",
"-F",
"#{pane_id}",
"-t",
sidebarPaneId
];
const newPaneId = (await executeCommand(executor, command, {
code: ErrorCodes.TMUX_COMMAND_FAILED,
message: "Failed to split a pane beside the sidebar",
path: contextPath,
details: { command }
})).trim();
if (newPaneId.length === 0) return raiseExecutionError(ErrorCodes.INVALID_PANE, {
message: "Unable to determine the pane created beside the sidebar",
path: contextPath
});
return newPaneId;
};
//#endregion
//#region src/executor/step-target.ts
const getStepLabel = (step) => {
if (step.kind === "split") return "Split";
if (step.kind === "focus") return "Focus";
return "Step";
};
const resolveRequiredStepTargetPaneId = (step) => {
if (typeof step.targetPaneId === "string" && step.targetPaneId.length > 0) return step.targetPaneId;
throw createCoreError("execution", {
code: ErrorCodes.MISSING_TARGET,
message: `${getStepLabel(step)} step missing target pane metadata`,
path: step.id
});
};
//#endregion
//#region src/executor/unsupported-step-kind.ts
const createUnsupportedStepKindError = (step) => {
return createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: `Unsupported step kind in emission: ${String(step.kind)}`,
path: step.id
});
};
//#endregion
//#region src/backends/tmux/backend.ts
const TMUX_VERSION_REGEX = /^tmux\s+(.+)$/;
const createTmuxBackend = (context) => {
const tmuxExecutor = createTmuxExecutor({
executor: context.executor,
verbose: context.verbose,
dryRun: context.dryRun
});
let detectedVersion;
const buildDryRunSteps = (emission) => {
const paneSizes = /* @__PURE__ */ new Map();
const initialPane = emission.summary.initialPaneId;
const initialPaneSize = resolveInitialTmuxPaneSize();
if (initialPaneSize !== void 0) paneSizes.set(initialPane, initialPaneSize);
return emission.steps.map((step) => ({
backend: "tmux",
summary: step.summary,
command: tmuxExecutor.getCommandString(buildTmuxCommand({
step,
paneSizes,
detectedVersion
}))
}));
};
const verifyEnvironment = async () => {
if (context.dryRun) return;
await tmuxExecutor.verifyTmuxEnvironment();
detectedVersion = await detectTmuxVersion(tmuxExecutor);
};
const applyPlan = async ({ emission, windowMode, windowName }) => {
const executionResult = await executePlan({
emission,
executor: tmuxExecutor.getExecutor(),
windowMode,
windowName,
onConfirmKill: context.prompt,
detectedVersion
});
const paneMap = executionResult.paneMap ?? /* @__PURE__ */ new Map();
const focusPaneId = paneMap.get(emission.summary.focusPaneId);
return {
executedSteps: executionResult.executedSteps,
focusPaneId,
paneNameToRealId: buildNameToRealIdMap(emission.terminals, paneMap),
windowId: await resolveTmuxWindowId({
tmuxExecutor,
focusPaneId
})
};
};
return {
verifyEnvironment,
applyPlan,
getDryRunSteps: buildDryRunSteps
};
};
const resolveTmuxWindowId = async ({ tmuxExecutor, focusPaneId }) => {
if (focusPaneId === void 0) return;
try {
const trimmed = (await tmuxExecutor.execute([
"display-message",
"-p",
"-t",
focusPaneId,
"#{window_id}"
])).trim();
return trimmed.length > 0 ? trimmed : void 0;
} catch {
return;
}
};
const buildTmuxCommand = ({ step, paneSizes, detectedVersion }) => {
if (step.kind === "split") {
const target = resolveRequiredStepTargetPaneId(step);
const orientation = resolveSplitOrientation(step);
const direction = orientation === "horizontal" ? "-h" : "-v";
if (step.splitSizing?.mode === "dynamic-cells") {
const paneSize = paneSizes.get(target);
if (paneSize !== void 0) {
const paneCells = orientation === "horizontal" ? paneSize.cols : paneSize.rows;
try {
const splitSize = resolveSplitSize(step, {
paneCells,
paneId: target,
detectedVersion,
rawPaneRecord: {
backend: "tmux",
paneId: target,
sourceFormat: "tmux-format",
size: {
cols: paneSize.cols,
rows: paneSize.rows
}
}
});
if (splitSize.mode === "cells") {
if (typeof step.createdPaneId === "string" && step.createdPaneId.length > 0) updatePaneSizes({
paneSizes,
targetPaneId: target,
createdPaneId: step.createdPaneId,
orientation,
targetCells: splitSize.targetCells,
createdCells: splitSize.createdCells
});
return [
"split-window",
direction,
"-t",
target,
"-l",
splitSize.cells
];
}
} catch (error) {
if (isCoreError(error) && error.code === ErrorCodes.SPLIT_SIZE_RESOLUTION_FAILED) {} else throw error;
}
}
return [
"split-window",
direction,
"-t",
target,
"-l",
"<dynamic>"
];
}
const splitSize = resolveSplitSize(step, {
paneId: target,
detectedVersion
});
if (splitSize.mode !== "percent") throw createUnsupportedStepKindError(step);
return [
"split-window",
direction,
"-t",
target,
"-p",
splitSize.percentage
];
}
if (step.kind === "focus") return [
"select-pane",
"-t",
resolveRequiredStepTargetPaneId(step)
];
throw createUnsupportedStepKindError(step);
};
const detectTmuxVersion = async (tmuxExecutor) => {
try {
return (await tmuxExecutor.execute(["-V"])).trim().match(TMUX_VERSION_REGEX)?.[1]?.trim();
} catch {
return;
}
};
const resolveInitialTmuxPaneSize = () => {
const tmuxPane = process.env.TMUX_PANE;
const tmuxSession = process.env.TMUX;
if (typeof tmuxSession !== "string" || tmuxSession.length === 0 || typeof tmuxPane !== "string" || tmuxPane.length === 0) return;
const currentPane = queryTmuxCurrentPaneSizeAndSidebarFlag(tmuxPane);
if (currentPane === void 0) return;
if (!currentPane.isSidebar) return {
cols: currentPane.cols,
rows: currentPane.rows
};
const originPaneId = resolveDryRunOriginPaneId(tmuxPane);
if (originPaneId === void 0) return;
return queryTmuxPaneSize(originPaneId);
};
const queryTmuxCurrentPaneSizeAndSidebarFlag = (paneId) => {
try {
const [colsRaw = "", rowsRaw = "", sidebarFlag = ""] = execFileSync("tmux", [
"display-message",
"-p",
"-t",
paneId,
"#{pane_width} #{pane_height} #{@vde_sidebar}"
], {
encoding: "utf8",
stdio: [
"ignore",
"pipe",
"ignore"
]
}).trim().split(/\s+/, 3);
const cols = Number.parseInt(colsRaw, 10);
const rows = Number.parseInt(rowsRaw, 10);
if (!Number.isInteger(cols) || cols <= 0 || !Number.isInteger(rows) || rows <= 0) return;
return {
cols,
rows,
isSidebar: sidebarFlag === "1"
};
} catch {
return;
}
};
const resolveDryRunOriginPaneId = (targetPaneId) => {
try {
const output = execFileSync("tmux", [
"list-panes",
"-t",
targetPaneId,
"-F",
SIDEBAR_LIST_PANES_FORMAT
], {
encoding: "utf8",
stdio: [
"ignore",
"pipe",
"ignore"
]
});
for (const line of output.split("\n")) {
const [paneId, sidebarFlag] = line.split(" ");
if (typeof paneId === "string" && paneId.trim().length > 0 && sidebarFlag?.trim() !== "1") return paneId.trim();
}
return;
} catch {
return;
}
};
const queryTmuxPaneSize = (paneId) => {
try {
const [colsRaw = "", rowsRaw = ""] = execFileSync("tmux", [
"display-message",
"-p",
"-t",
paneId,
"#{pane_width} #{pane_height}"
], {
encoding: "utf8",
stdio: [
"ignore",
"pipe",
"ignore"
]
}).trim().split(/\s+/, 2);
const cols = Number.parseInt(colsRaw, 10);
const rows = Number.parseInt(rowsRaw, 10);
if (!Number.isInteger(cols) || cols <= 0 || !Number.isInteger(rows) || rows <= 0) return;
return {
cols,
rows
};
} catch {
return;
}
};
//#endregion
//#region src/backends/wezterm/list-parser.ts
const toIdString = (value) => {
if (typeof value === "string") return value;
if (typeof value === "number") return value.toString();
};
const isNonEmptyString = (value) => {
return typeof value === "string" && value.length > 0;
};
const toWorkspaceString = (value) => {
if (typeof value === "string" && value.length > 0) return value;
};
const toPositiveInteger = (value) => {
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return;
return value;
};
const toPaneSize = (value) => {
if (typeof value !== "object" || value === null) return;
const raw = value;
const cols = toPositiveInteger(raw.cols);
const rows = toPositiveInteger(raw.rows);
if (cols === void 0 && rows === void 0) return;
return {
cols,
rows
};
};
const toImmutablePanes = (panes) => {
return panes.map((pane) => ({
paneId: pane.paneId,
isActive: pane.isActive,
...pane.size !== void 0 ? { size: pane.size } : {},
rawPaneRecord: pane.rawPaneRecord
}));
};
const toImmutableTabs = (tabs) => {
return Array.from(tabs.values()).map((tabRecord) => ({
tabId: tabRecord.tabId,
isActive: tabRecord.isActive,
panes: toImmutablePanes(tabRecord.panes)
}));
};
const toImmutableWindows = (windows) => {
return Array.from(windows.values()).map((windowRecord) => ({
windowId: windowRecord.windowId,
isActive: windowRecord.isActive,
workspace: windowRecord.workspace,
tabs: toImmutableTabs(windowRecord.tabs)
}));
};
const normalizeArrayEntry = (entry) => {
if (typeof entry !== "object" || entry === null) return;
const listEntry = entry;
const windowIdRaw = toIdString(listEntry.window_id);
const paneIdRaw = toIdString(listEntry.pane_id);
const tabIdRaw = toIdString(listEntry.tab_id) ?? windowIdRaw;
if (!isNonEmptyString(windowIdRaw) || !isNonEmptyString(tabIdRaw) || !isNonEmptyString(paneIdRaw)) return;
return {
windowId: windowIdRaw,
tabId: tabIdRaw,
paneId: paneIdRaw,
workspace: toWorkspaceString(listEntry.workspace),
isActive: listEntry.is_active === true,
size: toPaneSize(listEntry.size)
};
};
const getOrCreateWindowRecord = (windows, entry) => {
const existingWindow = windows.get(entry.windowId);
if (existingWindow) {
if (entry.workspace !== void 0 && existingWindow.workspace === void 0) existingWindow.workspace = entry.workspace;
return existingWindow;
}
const createdWindow = {
windowId: entry.windowId,
isActive: false,
workspace: entry.workspace,
tabs: /* @__PURE__ */ new Map()
};
windows.set(entry.windowId, createdWindow);
return createdWindow;
};
const getOrCreateTabRecord = (windowRecord, tabId) => {
const existingTab = windowRecord.tabs.get(tabId);
if (existingTab) return existingTab;
const createdTab = {
tabId,
isActive: false,
panes: []
};
windowRecord.tabs.set(tabId, createdTab);
return createdTab;
};
const createArrayRawPaneRecord = (entry) => {
return {
backend: "wezterm",
windowId: entry.windowId,
tabId: entry.tabId,
paneId: entry.paneId,
isActive: entry.isActive,
size: entry.size,
sourceFormat: "wezterm-array"
};
};
const createObjectRawPaneRecord = ({ rawPane, context, paneId, size }) => {
return {
backend: "wezterm",
windowId: context.windowId,
tabId: context.tabId,
paneId,
isActive: rawPane.is_active === true,
size,
sourceFormat: "wezterm-object"
};
};
const parseArrayResponse = (parsed) => {
if (!Array.isArray(parsed)) return;
const windowMap = /* @__PURE__ */ new Map();
for (const entry of parsed) {
const normalizedEntry = normalizeArrayEntry(entry);
if (!normalizedEntry) continue;
const windowRecord = getOrCreateWindowRecord(windowMap, normalizedEntry);
const tabRecord = getOrCreateTabRecord(windowRecord, normalizedEntry.tabId);
windowRecord.isActive ||= normalizedEntry.isActive;
tabRecord.isActive ||= normalizedEntry.isActive;
tabRecord.panes.push({
paneId: normalizedEntry.paneId,
isActive: normalizedEntry.isActive,
...normalizedEntry.size !== void 0 ? { size: normalizedEntry.size } : {},
rawPaneRecord: createArrayRawPaneRecord(normalizedEntry)
});
}
return { windows: toImmutableWindows(windowMap) };
};
const parseObjectPane = (pane, context) => {
if (typeof pane !== "object" || pane === null) return;
const rawPane = pane;
const paneIdRaw = toIdString(rawPane.pane_id);
if (!isNonEmptyString(paneIdRaw)) return;
const size = toPaneSize(rawPane.size);
return {
paneId: paneIdRaw,
isActive: rawPane.is_active === true,
...size !== void 0 ? { size } : {},
rawPaneRecord: createObjectRawPaneRecord({
rawPane,
context,
paneId: paneIdRaw,
size
})
};
};
const parseObjectTab = (tab, context) => {
if (typeof tab !== "object" || tab === null) return;
const rawTab = tab;
const tabIdRaw = toIdString(rawTab.tab_id);
if (!isNonEmptyString(tabIdRaw)) return;
const paneRecords = Array.isArray(rawTab.panes) ? rawTab.panes : [];
const panes = [];
for (const pane of paneRecords) {
const mappedPane = parseObjectPane(pane, {
windowId: context.windowId,
tabId: tabIdRaw
});
if (mappedPane) panes.push(mappedPane);
}
return {
tabId: tabIdRaw,
isActive: rawTab.is_active === true,
panes
};
};
const parseObjectWindow = (window) => {
if (typeof window !== "object" || window === null) return;
const rawWindow = window;
const windowIdRaw = toIdString(rawWindow.window_id);
if (!isNonEmptyString(windowIdRaw)) return;
const tabs = [];
const rawTabs = Array.isArray(rawWindow.tabs) ? rawWindow.tabs : [];
for (const tab of rawTabs) {
const mappedTab = parseObjectTab(tab, { windowId: windowIdRaw });
if (mappedTab) tabs.push(mappedTab);
}
return {
windowId: windowIdRaw,
isActive: rawWindow.is_active === true,
workspace: toWorkspaceString(rawWindow.workspace),
tabs
};
};
const parseObjectResponse = (parsed) => {
if (typeof parsed !== "object" || parsed === null) return;
const candidate = parsed;
const rawWindows = Array.isArray(candidate.windows) ? candidate.windows : [];
const windows = [];
for (const window of rawWindows) {
const mappedWindow = parseObjectWindow(window);
if (mappedWindow) windows.push(mappedWindow);
}
return { windows };
};
const parseWeztermListResult = (stdout) => {
try {
const parsed = JSON.parse(stdout);
return parseArrayResponse(parsed) ?? parseObjectResponse(parsed);
} catch {
return;
}
};
//#endregion
//#region src/backends/wezterm/cli.ts
const WEZTERM_BINARY = "wezterm";
const WEZTERM_MINIMUM_VERSION = "20220624-141144-bd1b7c5d";
const VERSION_REGEX = /(\d{8})-(\d{6})-([0-9a-fA-F]+)/i;
const verifyWeztermAvailability = async () => {
let stdout;
try {
stdout = (await execa(WEZTERM_BINARY, ["--version"])).stdout;
} catch (error) {
const execaError = error;
if (execaError.code === "ENOENT") throw createEnvironmentError("wezterm is not installed", ErrorCodes.BACKEND_NOT_FOUND, {
backend: "wezterm",
binary: WEZTERM_BINARY
});
throw createEnvironmentError("Failed to execute wezterm --version", ErrorCodes.WEZTERM_NOT_FOUND, {
backend: "wezterm",
binary: WEZTERM_BINARY,
stderr: execaError.stderr
});
}
const detectedVersion = extractVersion(stdout);
if (detectedVersion === void 0) throw createEnvironmentError("Unable to determine wezterm version", ErrorCodes.UNSUPPORTED_WEZTERM_VERSION, {
requiredVersion: WEZTERM_MINIMUM_VERSION,
detectedVersion: stdout.trim()
});
if (!isVersionSupported(detectedVersion, WEZTERM_MINIMUM_VERSION)) throw createEnvironmentError("Unsupported wezterm version", ErrorCodes.UNSUPPORTED_WEZTERM_VERSION, {
requiredVersion: WEZTERM_MINIMUM_VERSION,
detectedVersion
});
return { version: detectedVersion };
};
const runWeztermCli = async (args, errorContext) => {
try {
return (await execa(WEZTERM_BINARY, ["cli", ...args])).stdout;
} catch (error) {
const execaError = error;
throw createCoreError("execution", {
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
message: errorContext.message,
path: errorContext.path,
details: {
command: [
WEZTERM_BINARY,
"cli",
...args
],
stderr: execaError.stderr,
exitCode: execaError.exitCode,
backend: "wezterm",
...errorContext.details ?? {}
}
});
}
};
const listWeztermWindows = async () => {
const stdout = await runWeztermCli([
"list",
"--format",
"json"
], { message: "Failed to list wezterm panes" });
const result = parseWeztermListResult(stdout);
if (result === void 0) throw createCoreError("execution", {
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
message: "Invalid wezterm list output",
details: { stdout }
});
return result;
};
const killWeztermPane = async (paneId) => {
await runWeztermCli([
"kill-pane",
"--pane-id",
paneId
], {
message: `Failed to kill wezterm pane ${paneId}`,
path: paneId
});
};
const extractVersion = (raw) => {
const match = raw.match(VERSION_REGEX);
if (!match) return;
const date = match[1];
const time = match[2];
const commit = match[3];
if (date === void 0 || time === void 0 || commit === void 0) return;
return `${date}-${time}-${commit.toLowerCase()}`;
};
const isVersionSupported = (detected, minimum) => {
const parse = (version) => {
const match = version.match(VERSION_REGEX);
if (!match) return;
const date = match[1];
const time = match[2];
const commit = match[3];
if (date === void 0 || time === void 0 || commit === void 0) return;
const build = Number(`${date}${time}`);
if (Number.isNaN(build)) return;
return {
build,
commit: commit.toLowerCase()
};
};
const detectedInfo = parse(detected);
const minimumInfo = parse(minimum);
if (detectedInfo === void 0 || minimumInfo === void 0) return false;
if (detectedInfo.build > minimumInfo.build) return true;
if (detectedInfo.build < minimumInfo.build) return false;
return detectedInfo.commit >= minimumInfo.commit;
};
//#endregion
//#region src/backends/wezterm/dry-run.ts
const SINGLE_QUOTE = "'";
const SHELL_SINGLE_QUOTE_ESCAPE = `'"'"'`;
const buildSplitArguments = (params) => {
const directionFlag = params.horizontal ? "--right" : "--bottom";
if (params.splitSize.mode === "percent") return [
"split-pane",
directionFlag,
"--percent",
params.splitSize.percentage,
"--pane-id",
params.targetPaneId
];
if (params.splitSize.mode === "cells") return [
"split-pane",
directionFlag,
"--cells",
params.splitSize.cells,
"--pane-id",
params.targetPaneId
];
return [
"split-pane",
directionFlag,
"--cells",
params.splitSize.cellsPlaceholder,
"--pane-id",
params.targetPaneId
];
};
const buildDryRunSteps = (emission, options = {}) => {
const steps = [];
const paneSizes = /* @__PURE__ */ new Map();
if (options.initialPaneId !== void 0 && options.initialPaneSize !== void 0) paneSizes.set(options.initialPaneId, options.initialPaneSize);
for (const step of emission.steps) {
if (step.kind === "split") {
const target = resolveRequiredStepTargetPaneId(step);
const horizontal = resolveSplitOrientation(step) === "horizontal";
const args = buildSplitArguments({
targetPaneId: target,
splitSize: resolveDryRunSplitSize({
step,
targetPaneId: target,
paneSizes,
orientation: horizontal ? "horizontal" : "vertical",
detectedVersion: options.detectedVersion
}),
horizontal
});
const summary = step.splitSizing?.mode === "dynamic-cells" ? `${step.summary} [dynamic-cells]` : step.summary;
steps.push({
backend: "wezterm",
summary,
command: `wezterm cli ${args.join(" ")}`
});
continue;
}
if (step.kind === "focus") {
const target = resolveRequiredStepTargetPaneId(step);
steps.push({
backend: "wezterm",
summary: step.summary,
command: `wezterm cli activate-pane --pane-id ${target}`
});
continue;
}
throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: `Unsupported step kind in emission: ${String(step.kind)}`,
path: step.id
});
}
const prepared = prepareTerminalCommands({
terminals: emission.terminals,
focusPaneVirtualId: emission.summary.focusPaneId,
resolveRealPaneId: (virtualPaneId) => virtualPaneId,
onTemplateTokenError: ({ terminal, error }) => {
throw createCoreError("execution", {
code: ErrorCodes.TEMPLATE_TOKEN_ERROR,
message: `Template token resolution failed for pane ${terminal.virtualPaneId}: ${error.message}`,
path: terminal.virtualPaneId,
details: {
command: terminal.command,
tokenType: error.tokenType,
availablePanes: error.availablePanes
}
});
}
});
const quoteForShellDisplay = (value) => {
return `${SINGLE_QUOTE}${value.split(SINGLE_QUOTE).join(SHELL_SINGLE_QUOTE_ESCAPE)}${SINGLE_QUOTE}`;
};
for (const commandSet of prepared.commands) {
const paneId = commandSet.terminal.virtualPaneId;
if (typeof commandSet.cwdCommand === "string") steps.push({
backend: "wezterm",
summary: `set cwd for ${paneId}`,
command: `wezterm cli send-text --pane-id ${paneId} --no-paste -- ${quoteForShellDisplay(commandSet.cwdCommand)}`
});
for (const envEntry of commandSet.envCommands) steps.push({
backend: "wezterm",
summary: `set env ${envEntry.key} for ${paneId}`,
command: `wezterm cli send-text --pane-id ${paneId} --no-paste -- ${quoteForShellDisplay(envEntry.command)}`
});
if (commandSet.command !== void 0) steps.push({
backend: "wezterm",
summary: `run command for ${paneId}`,
command: `wezterm cli send-text --pane-id ${paneId} --no-paste -- ${quoteForShellDisplay(commandSet.command.text)}`
});
}
return steps;
};
const resolveDryRunSplitSize = ({ step, targetPaneId, paneSizes, orientation, detectedVersion }) => {
if (step.kind !== "split") throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: "Dry-run split sizing requested for non-split step",
path: step.id
});
if (step.splitSizing?.mode === "dynamic-cells") {
const paneSize = paneSizes.get(targetPaneId);
const paneCells = paneSize ? orientation === "horizontal" ? paneSize.cols : paneSize.rows : void 0;
if (typeof paneCells !== "number") return {
mode: "cells-placeholder",
cellsPlaceholder: "<dynamic>"
};
try {
const splitSize = resolveSplitSize(step, {
paneCells,
paneId: targetPaneId,
detectedVersion,
rawPaneRecord: {
backend: "wezterm",
paneId: targetPaneId,
sourceFormat: "unknown",
size: paneSize
}
});
if (splitSize.mode === "cells" && typeof step.createdPaneId === "string" && step.createdPaneId.length > 0) updatePaneSizes({
paneSizes,
targetPaneId,
createdPaneId: step.createdPaneId,
orientation,
targetCells: splitSize.targetCells,
createdCells: splitSize.createdCells
});
if (splitSize.mode === "cells") return splitSize;
} catch (error) {
if (isCoreError(error) && error.code === ErrorCodes.SPLIT_SIZE_RESOLUTION_FAILED) return {
mode: "cells-placeholder",
cellsPlaceholder: "<dynamic>"
};
throw error;
}
return {
mode: "cells-placeholder",
cellsPlaceholder: "<dynamic>"
};
}
return resolveSplitSize(step, {
paneId: targetPaneId,
detectedVersion
});
};
//#endregion
//#region src/backends/wezterm/layout-resolution.ts
const PANE_REGISTRATION_RETRIES = 5;
const PANE_REGISTRATION_DELAY_MS = 100;
const resolveCurrentWindow = async (context) => {
const preferredPaneId = typeof context.preferredPaneId === "string" && context.preferredPaneId.length > 0 ? context.preferredPaneId : void 0;
const preferredWindowId = preferredPaneId !== void 0 ? findWindowContainingPane(context.list, preferredPaneId) : void 0;
const activeWindow = (preferredWindowId !== void 0 ? context.list.windows.find((window) => window.windowId === preferredWindowId) : void 0) ?? context.list.windows.find((window) => window.isActive) ?? context.list.windows[0];
if (!activeWindow) throw createCoreError("execution", {
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
message: "No active wezterm window detected",
details: { hint: "Launch wezterm and ensure a window is focused, or run with --new-window." }
});
const activeTab = (preferredPaneId !== void 0 ? activeWindow.tabs.find((tab) => tab.panes.some((pane) => pane.paneId === preferredPaneId)) : void 0) ?? activeWindow.tabs.find((tab) => tab.isActive) ?? activeWindow.tabs[0];
if (!activeTab) throw createCoreError("execution", {
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
message: "No active wezterm tab detected",
path: activeWindow.windowId,
details: { hint: "Ensure a wezterm tab is focused before using --current-window." }
});
const activePane = (preferredPaneId !== void 0 ? activeTab.panes.find((pane) => pane.paneId === preferredPaneId) : void 0) ?? activeTab.panes.find((pane) => pane.isActive) ?? activeTab.panes[0];
if (!activePane) throw createCoreError("execution", {
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
message: "No active wezterm pane detected",
path: activeTab.tabId,
details: { hint: "Ensure a wezterm pane is active before using --current-window." }
});
const panesToClose = activeTab.panes.filter((pane) => pane.paneId !== activePane.paneId).map((pane) => pane.paneId);
if (panesToClose.length > 0) {
let confirmed = true;
if (context.prompt) confirmed = await context.prompt({
panesToClose,
dryRun: context.dryRun
});
if (confirmed !== true) throw createCoreError("execution", {
code: ErrorCodes.USER_CANCELLED,
message: "Aborted layout application for current wezterm window",
path: activePane.paneId,
details: { panes: panesToClose }
});
for (const paneId of panesToClose) {
context.logCommand([
"kill-pane",
"--pane-id",
paneId
]);
await killWeztermPane(paneId);
}
}
return {
paneId: activePane.paneId,
windowId: activeWindow.windowId,
panesToClose
};
};
const findActiveWindow = (list) => {
return list.windows.find((window) => window.isActive) ?? list.windows[0];
};
const findWindowContainingPane = (list, paneId) => {
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;
};
const findWorkspaceForPane = (list, paneId) => {
for (const window of list.windows) for (const tab of window.tabs) for (const pane of tab.panes) if (pane.paneId === paneId) return window.workspace;
};
const filterWindowsByWorkspace = (list, workspace) => {
if (workspace === void 0 || workspace.length === 0) return list;
const scoped = list.windows.filter((window) => window.workspace === workspace);
if (scoped.length === 0) return list;
return { windows: scoped };
};
const collectPaneIdsForWindow = (list, windowId) => {
const targetWindow = list.windows.find((window) => window.windowId === windowId);
if (!targetWindow) throw createCoreError("execution", {
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
message: `Wezterm window ${windowId} not found`,
details: { windowId }
});
const paneIds = targetWindow.tabs.flatMap((tab) => tab.panes.map((pane) => pane.paneId));
return new Set(paneIds);
};
const waitForPaneRegistration = async ({ paneId, listWindows, windowHint }) => {
for (let attempt = 0; attempt < PANE_REGISTRATION_RETRIES; attempt += 1) {
const snapshot = await listWindows();
if (typeof windowHint === "string") try {
if (collectPaneIdsForWindow(snapshot, windowHint).has(paneId)) return windowHint;
} catch {}
const located = findWindowContainingPane(snapshot, paneId);
if (typeof located === "string" && located.length > 0) return located;
if (attempt < PANE_REGISTRATION_RETRIES - 1) await waitForDelay(PANE_REGISTRATION_DELAY_MS);
}
throw createCoreError("execution", {
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
message: "Unable to locate spawned wezterm window",
details: {
paneId,
hint: "Verify that wezterm is running and the CLI client can connect."
}
});
};
const extractSpawnPaneId = (output) => {
const trimmed = output.trim();
if (trimmed.length === 0) return "";
const tokens = (trimmed.split("\n").pop() ?? "").split(/\s+/).filter((segment) => segment.length > 0);
if (tokens.length === 0) return "";
const [paneId] = tokens;
if (typeof paneId !== "string") return "";
return paneId.trim();
};
const resolveInitialPane = async ({ windowMode, prompt, dryRun, listWindows, runCommand, logCommand, initialCwd, workspaceHint, initialList, preferredPaneId }) => {
const preferredPaneIdValue = typeof preferredPaneId === "string" && preferredPaneId.length > 0 ? preferredPaneId : void 0;
if (windowMode === "current-window") return resolveCurrentWindow({
list: filterWindowsByWorkspace(initialList ?? await listWindows(), workspaceHint),
prompt,
dryRun,
logCommand,
preferredPaneId: preferredPaneIdValue
});
const scopedExisting = filterWindowsByWorkspace(initialList ?? await listWindows(), workspaceHint);
const scopedPreferredWindowId = preferredPaneIdValue !== void 0 ? findWindowContainingPane(scopedExisting, preferredPaneIdValue) : void 0;
const activeWindow = (scopedPreferredWindowId !== void 0 ? scopedExisting.windows.find((window) => window.windowId === scopedPreferredWindowId) : void 0) ?? findActiveWindow(scopedExisting);
if (activeWindow) {
const args = [
"spawn",
"--window-id",
activeWindow.windowId
];
if (typeof initialCwd === "string" && initialCwd.length > 0) args.push("--cwd", initialCwd);
const spawnOutput = await runCommand(args, { message: "Failed to spawn wezterm tab" });
const paneId = extractSpawnPaneId(spawnOutput);
if (paneId.length === 0) throw createCoreError("execution", {
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
message: "wezterm spawn did not return a pane id",
details: { stdout: spawnOutput }
});
return {
paneId,
windowId: await waitForPaneRegistration({
paneId,
listWindows,
windowHint: activeWindow.windowId
})
};
}
const args = ["spawn", "--new-window"];
if (typeof initialCwd === "string" && initialCwd.length > 0) args.push("--cwd", initialCwd);
if (typeof workspaceHint === "string" && workspaceHint.length > 0) args.push("--workspace", workspaceHint);
const spawnOutput = await runCommand(args, { message: "Failed to spawn wezterm window" });
const paneId = extractSpawnPaneId(spawnOutput);
if (paneId.length === 0) throw createCoreError("execution", {
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
message: "wezterm spawn did not return a pane id",
details: { stdout: spawnOutput }
});
return {
paneId,
windowId: await waitForPaneRegistration({
paneId,
listWindows
})
};
};
//#endregion
//#region src/backends/wezterm/pane-map.ts
const registerPaneWithAncestors = (map, virtualId, realId) => {
map.set(virtualId, realId);
let ancestor = virtualId;
while (ancestor.includes(".")) {
ancestor = ancestor.slice(0, ancestor.lastIndexOf("."));
if (!map.has(ancestor)) map.set(ancestor, realId);
else break;
}
};
const resolveRealPaneId = (paneMap, virtualId, context) => {
const resolved = resolvePaneMapping(paneMap, virtualId);
if (typeof resolved === "string" && resolved.length > 0) return resolved;
throw createCoreError("execution", {
code: ErrorCodes.INVALID_PANE,
message: `Unknown wezterm pane mapping for ${virtualId}`,
path: context.stepId
});
};
//#endregion
//#region src/backends/wezterm/step-execution.ts
const findNewPaneId = (before, after) => {
for (const paneId of after) if (!before.has(paneId)) return paneId;
};
const appendCarriageReturn = (value) => {
return value.endsWith("\r") ? value : `${value}\r`;
};
const sendTextToPane = async ({ paneId, text, runCommand, context }) => {
await runCommand([
"send-text",
"--pane-id",
paneId,
"--no-paste",
"--",
appendCarriageReturn(text)
], context);
};
const applyFocusStep = async ({ step, paneMap, runCommand }) => {
const targetVirtualId = step.targetPaneId;
if (typeof targetVirtualId !== "string" || targetVirtualId.length === 0) throw createCoreError("execution", {
code: ErrorCodes.INVALID_PANE,
message: "Focus step missing target pane metadata",
path: step.id
});
await runCommand([
"activate-pane",
"--pane-id",
resolveRealPaneId(paneMap, targetVirtualId, { stepId: step.id })
], {
message: `Failed to execute focus step ${step.id}`,
path: step.id
});
};
const applySplitStep = async ({ step, paneMap, windowId, runCommand, listWindows, logPaneMapping, detectedVersion }) => {
const targetVirtualId = step.targetPaneId;
if (typeof targetVirtualId !== "string" || targetVirtualId.length === 0) throw createCoreError("execution", {
code: ErrorCodes.INVALID_PANE,
message: "Split step missing target pane metadata",
path: step.id
});
const targetRealId = resolveRealPaneId(paneMap, targetVirtualId, { stepId: step.id });
const beforeList = await listWindows();
const beforePaneIds = collectPaneIdsForWindow(beforeList, windowId);
const orientation = resolveSplitOrientation(step);
const targetPane = findPaneById(beforeList, targetRealId);
await runCommand(buildSplitArguments({
targetPaneId: targetRealId,
splitSize: resolveSplitSize(step, {
paneCells: resolvePaneCellsForOrientation(targetPane?.size, orientation),
paneId: targetRealId,
requiredVersion: WEZTERM_MINIMUM_VERSION,
detectedVersion,
rawPaneRecord: targetPane?.rawPaneRecord
}),
horizontal: orientation === "horizontal"
}), {
message: `Failed to execute split step ${step.id}`,
path: step.id
});
const newPaneId = findNewPaneId(beforePaneIds, collectPaneIdsForWindow(await listWindows(), windowId));
if (typeof newPaneId !== "string" || newPaneId.length === 0) throw createCoreError("execution", {
code: ErrorCodes.TERMINAL_COMMAND_FAILED,
message: "Unable to determine newly created wezterm pane",
path: step.id
});
if (typeof step.createdPaneId === "string" && step.createdPaneId.length > 0) {
registerPaneWithAncestors(paneMap, step.createdPaneId, newPaneId);
logPaneMapping(step.createdPaneId, newPaneId);
}
};
const findPaneById = (list, paneId) => {
for (const window of list.windows) for (const tab of window.tabs) for (const pane of tab.panes) if (pane.paneId === paneId) return {
size: pane.size,
rawPaneRecord: pane.rawPaneRecord
};
};
const resolvePaneCellsForOrientation = (size, orientation) => {
if (orientation === "horizontal") return size?.cols;
return size?.rows;
};
const applyTerminalCommands = async ({ terminals, paneMap, runCommand, focusPaneVirtualId }) => {
if (!paneMap.has(focusPaneVirtualId)) throw createCoreError("execution", {
code: ErrorCodes.INVALID_PANE,
message: `Unknown focus pane: ${focusPaneVirtualId}`,
path: focusPaneVirtualId
});
const prepared = prepareTerminalCommands({
terminals,
focusPaneVirtualId,
resolveRealPaneId: (virtualPaneId) => resolveRealPaneId(paneMap, virtualPaneId, { stepId: virtualPaneId }),
onTemplateTokenError: ({ terminal, error }) => {
throw createCoreError("execution", {
code: ErrorCodes.TEMPLATE_TOKEN_ERROR,
message: `Template token resolution failed for pane ${terminal.virtualPaneId}: ${error.message}`,
path: terminal.virtualPaneId,
details: {
command: terminal.command,
tokenType: error.tokenType,
availablePanes: error.availablePanes
}
});
}
});
for (const commandSet of prepared.commands) {
const { terminal, realPaneId } = commandSet;
if (typeof commandSet.cwdCommand === "string") await sendTextToPane({
paneId: realPaneId,
text: commandSet.cwdCommand,
runCommand,
context: {
message: `Failed to change directory for pane ${terminal.virtualPaneId}`,
path: terminal.virtualPaneId,
details: { cwd: terminal.cwd }
}
});
for (const envEntry of commandSet.envCommands) await sendTextToPane({
paneId: realPaneId,
text: envEntry.command,
runCommand,
context: {
message: `Failed to set environment variable ${envEntry.key}`,
path: terminal.virtualPaneId
}
});
if (commandSet.command !== void 0) {
if (commandSet.command.delayMs > 0) await waitForDelay(commandSet.command.delayMs);
await sendTextToPane({
paneId: realPaneId,
text: commandSet.command.text,
runCommand,
context: {
message: `Failed to execute command for pane ${terminal.virtualPaneId}`,
path: terminal.virtualPaneId,
details: { command: terminal.command }
}
});
}
}
};
//#endregion
//#region src/backends/wezterm/backend.ts
const ensureVirtualPaneId = (emission) => {
const { initialPaneId } = emission.summary;
if (typeof initialPaneId !== "string" || initialPaneId.length === 0) throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: "Plan emission is missing initial pane metadata",
path: "plan.initialPaneId"
});
return initialPaneId;
};
const createWeztermBackend = (context) => {
let detectedVersion;
const formatCommand = (args) => {
return `wezterm cli ${args.join(" ")}`;
};
const logCommand = (args) => {
const message = `[wezterm] ${formatCommand(args)}`;
if (context.verbose) context.logger.info(message);
else context.logger.debug(message);
};
const logPaneMapping = (virtualId, realId) => {
const message = `[wezterm] pane ${virtualId} -> ${realId}`;
if (context.verbose) context.logger.info(message);
else context.logger.debug(message);
};
const runCommand = async (args, errorContext) => {
const commandArgs = [...args];
logCommand(commandArgs);
return runWeztermCli(commandArgs, errorContext);
};
const listWindows = async () => {
logCommand([
"list",
"--format",
"json"
]);
return listWeztermWindows();
};
const verifyEnvironment = async () => {
if (context.dryRun) return;
detectedVersion = (await verifyWeztermAvailability()).version;
};
const applyPlan = async ({ emission, windowMode }) => {
const initialVirtualPaneId = ensureVirtualPaneId(emission);
const paneMap = /* @__PURE__ */ new Map();
const initialTerminal = emission.terminals.find((terminal) => terminal.virtualPaneId === initialVirtualPaneId);
const initialCwd = typeof initialTerminal?.cwd === "string" && initialTerminal.cwd.length > 0 ? initialTerminal.cwd : context.cwd;
let cachedInitialList;
let workspaceHint;
if (typeof context.paneId === "string" && context.paneId.length > 0) try {
cachedInitialList = await listWindows();
workspaceHint = findWorkspaceForPane(cachedInitialList, context.paneId);
} catch {
cachedInitialList = void 0;
workspaceHint = void 0;
}
const { paneId: initialPaneId, windowId } = await resolveInitialPane({
windowMode,
prompt: context.prompt,
dryRun: context.dryRun,
listWindows,
runCommand,
logCommand,
initialCwd,
workspaceHint,
initialList: cachedInitialList,
preferredPaneId: context.paneId
});
registerPaneWithAncestors(paneMap, initialVirtualPaneId, initialPaneId);
logPaneMapping(initialVirtualPaneId, initialPaneId);
let executedSteps = 0;
for (const step of emission.steps) if (step.kind === "split") {
await applySplitStep({
step,
paneMap,
windowId,
runCommand,
listWindows,
logPaneMapping,
detectedVersion
});
executedSteps += 1;
} else if (step.kind === "focus") {
await applyFocusStep({
step,
paneMap,
runCommand
});
executedSteps += 1;
} else throw createCoreError("execution", {
code: ErrorCodes.INVALID_PLAN,
message: `Unsupported step kind in emission: ${String(step.kind)}`,
path: step.id
});
await applyTerminalCommands({
terminals: emission.terminals,
paneMap,
runCommand,
focusPaneVirtualId: emission.summary.focusPaneId
});
const focusVirtual = emission.summary.focusPaneId;
const focusPaneId = typeof focusVirtual === "string" ? paneMap.get(focusVirtual) : void 0;
return {
executedSteps,
focusPaneId,
paneNameToRealId: buildNameToRealIdMap(emission.terminals, paneMap),
windowId
};
};
return {
verifyEnvironment,
applyPlan,
getDryRunSteps: (emission) => {
const initialPaneSize = resolveInitialWeztermPaneSize({
paneId: typeof context.paneId === "string" && context.paneId.length > 0 ? context.paneId : process.env.WEZTERM_PANE,
logger: context.logger
});
return buildDryRunSteps(emission, {
initialPaneId: emission.summary.initialPaneId,
initialPaneSize,
detectedVersion
});
}
};
};
const resolveInitialWeztermPaneSize = ({ paneId, logger }) => {
if (typeof paneId !== "string" || paneId.length === 0) return;
try {
const parsed = parseWeztermListResult(execFileSync("wezterm", [
"cli",
"list",
"--format",
"json"
], {
encoding: "utf8",
stdio: [
"ignore",
"pipe",
"ignore"
]
}));
if (parsed === void 0) {
logger.debug(`[wezterm] Failed to parse pane list while resolving initial pane size for pane ${paneId}`);
return;
}
for (const window of parsed.windows) for (const tab of window.tabs) for (const pane of tab.panes) if (pane.paneId === paneId) {
if (typeof pane.size?.cols === "number" && typeof pane.size?.rows === "number") return {
cols: pane.size.cols,
rows: pane.size.rows
};
return;
}
return;
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.debug(`[wezterm] Failed to resolve initial pane size for pane ${paneId}: ${reason}`);
return;
}
};
//#endregion
//#region src/executor/backend-factory.ts
function createTerminalBackend(kind, context) {
if (kind === "tmux") {
if (!("executor" in context)) throw new Error("tmux backend requires executor context");
return createTmuxBackend(context);
}
if (kind === "wezterm") return createWeztermBackend(context);
throw new Error(`Unsupported backend "${kind}"`);
}
//#endregion
//#region src/executor/backend-resolver.ts
const KNOWN_BACKENDS = ["tmux", "wezterm"];
const resolveTerminalBackendKind = ({ cliFlag, presetBackend, env }) => {
const selectedBackend = cliFlag ?? presetBackend;
if (selectedBackend !== void 0) {
if (!KNOWN_BACKENDS.includes(selectedBackend)) throw new Error(`Unknown backend "${selectedBackend}"`);
return selectedBackend;
}
if (typeof env.TMUX === "string" && env.TMUX.trim().length > 0) return "tmux";
return "tmux";
};
//#endregion
//#region src/cli/after-apply-hook.ts
const AFTER_APPLY_HOOK_TIMEOUT_MS = 3e4;
const FOCUS_PANE_TOKEN_PATTERN = /\{\{(?:this_pane|focus_pane)\}\}/;
const WINDOW_ID_TOKEN_PATTERN = /\{\{window_id\}\}/;
/**
* Runs the `hooks.afterApply` preset command once, after a preset has been applied
* successfully. Failures (token resolution or command execution) are logged as
* warnings and never rejected/thrown, so they cannot affect the CLI's exit code -
* the preset apply itself has already succeeded by the time this runs.
*/
const runAfterApplyHook = async ({ hookCommand, context, logger, runHostCommand = createDefaultRunHostCommand() }) => {
if (typeof hookCommand !== "string" || hookCommand.length === 0) return;
if (context.focusPaneId === void 0 && FOCUS_PANE_TOKEN_PATTERN.test(hookCommand)) {
logger.warn("hooks.afterApply skipped: failed to resolve template tokens ({{this_pane}}/{{focus_pane}} require a focus pane id, but none was available)");
return;
}
if (context.windowId === void 0 && WINDOW_ID_TOKEN_PATTERN.test(hookCommand)) {
logger.warn("hooks.afterApply skipped: failed to resolve template tokens ({{window_id}} requires a window id, but none was available)");
return;
}
let resolvedCommand;
try {
resolvedCommand = replaceTemplateTokens({
command: hookCommand,
currentPaneRealId: context.focusPaneId ?? "",
focusPaneRealId: context.focusPaneId ?? "",
nameToRealIdMap: context.paneNameToRealId ?? /* @__PURE__ */ new Map(),
windowId: context.windowId
});
} catch (error) {
const reason = error instanceof TemplateTokenError ? error.message : String(error);
logger.warn(`hooks.afterApply skipped: failed to resolve template tokens (${reason})`);
return;
}
logger.info(`Executing: ${resolvedCommand}`);
try {
await runHostCommand(resolvedCommand, { cwd: context.cwd });
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.warn(`hooks.afterApply failed: ${reason}`);
}
};
/**
* Executes the resolved hook command through the host shell (equivalent to
* `sh -c <command>`), rather than splitting it into argv the way tmux commands
* are executed. hooks.afterApply commands are user-authored, free-form shell
* text (e.g. "vde-tmux-sidebar open {{pane_id:sidebar}} | logger") that may rely
* on pipes, redirection, or shell expansion, so argv-style execution would break
* common use cases.
*/
const createDefaultRunHostCommand = () => {
return async (command, { cwd }) => {
await execa(command, {
shell: true,
cwd,
timeout: AFTER_APPLY_HOOK_TIMEOUT_MS
});
};
};
//#endregion
//#region src/cli/command-helpers.ts
const renderDryRun = (steps, output = (message) => console.log(message)) => {
output(chalk.bold("\nPlanned terminal steps (dry-run)"));
steps.forEach((step, index) => {
output(` ${index + 1}. [${step.backend}] ${step.summary}: ${step.command}`);
});
};
const renderDryRunHook = (afterApply, output = (message) => console.log(message)) => {
if (typeof afterApply !== "string" || afterApply.length === 0) return;
output(chalk.bold("\nPlanned hooks (dry-run)"));
output(` 1. [afterApply] ${afterApply}`);
};
const buildPresetSource = (presetName) => {
return typeof presetName === "string" && presetName.length > 0 ? `preset://${presetName}` : "preset://default";
};
const determineCliWindowMode = (options) => {
if (options.currentWindow === true && options.newWindow === true) throw new Error("Cannot use --current-window and --new-window at the same time");
if (options.currentWindow === true) return "current-window";
if (options.newWindow === true) return "new-window";
};
//#endregion
//#region src/cli/user-prompt.ts
const createPaneKillPrompter = (logger) => {
return async ({ panesToClose, dryRun }) => {
if (panesToClose.length === 0) return true;
const paneList = panesToClose.join(", ");
if (dryRun) {
logger.warn(`[DRY RUN] Would close panes: ${paneList}`);
return true;
}
logger.warn(`This operation will close the following panes: ${paneList}`);
if (stdin.isTTY !== true || stdout.isTTY !== true) {
logger.error("Cannot prompt for confirmation because the terminal is not interactive");
return false;
}
const rl = createInterface({
input: stdin,
output: stdout
});
try {
const normalized = (await rl.question("Continue? [y/N]: ")).trim().toLowerCase();
return normalized === "y" || normalized === "yes";
} finally {
rl.close();
}
};
};
//#endregion
//#region src/cli/window-mode.ts
const resolveWindowMode = ({ cli, preset, defaults }) => {
if (cli !== void 0) return {
mode: cli,
source: "cli"
};
if (preset !== void 0) return {
mode: preset,
source: "preset"
};
if (defaults !== void 0) return {
mode: defaults,
source: "defaults"
};
return {
mode: "new-window",
source: "fallback"
};
};
//#endregion
//#region src/cli/preset-execution.ts
const executePreset = async ({ presetName, options, skipLoadConfig = false, presetManager, createCommandExecutor, core, logger, handleError, handlePipelineFailure, output = (line) => console.log(line), cwd = process.cwd(), env = process.env }) => {
try {
if (skipLoadConfig !== true) await presetManager.loadConfig();
const preset = typeof presetName === "string" && presetName.length > 0 ? presetManager.getPreset(presetName) : presetManager.getDefaultPreset();
const windowModeResolution = resolveWindowModeForPreset({
presetManager,
options,
presetWindowMode: preset.windowMode
});
const windowMode = windowModeResolution.mode;
logger.info(`Window mode: ${windowMode} (source: ${windowModeResolution.source})`);
const confirmPaneClosure = createPaneKillPrompter(logger);
const executor = createCommandExecutor({
verbose: options.verbose,
dryRun: options.dryRun
});
const backendKind = resolveTerminalBackendKind({
cliFlag: options.backend,
presetBackend: preset.backend,
env
});
logger.info(`Terminal backend: ${backendKind}`);
const backendContextBase = {
logger,
dryRun: options.dryRun,
verbose: options.verbose,
prompt: confirmPaneClosure,
cwd,
paneId: env.WEZTERM_PANE
};
const backend = backendKind === "tmux" ? createTerminalBackend("tmux", {
...backendContextBase,
executor
}) : createTerminalBackend("wezterm", backendContextBase);
await backend.verifyEnvironment();
if (options.dryRun === true) output("[DRY RUN] No actual commands will be executed");
let emission;
let compiledPreset;
try {
const built = buildPlanEmission({
core,
preset,
presetName
});
emission = built.emission;
compiledPreset = built.compiledPreset;
} catch (error) {
return handlePipelineFailure(error);
}
if (options.dryRun === true) {
renderDryRun(backend.getDryRunSteps(emission), output);
renderDryRunHook(compiledPreset.hooks?.afterApply, output);
} else try {
const executionResult = await backend.applyPlan({
emission,
windowMode,
windowName: resolveWindowName({
presetName,
presetDisplayName: preset.name
})
});
logger.info(`Executed ${executionResult.executedSteps} ${backendKind} steps`);
const afterApplyCommand = compiledPreset.hooks?.afterApply;
if (afterApplyCommand !== void 0) await runAfterApplyHook({
hookCommand: afterApplyCommand,
context: {
cwd,
focusPaneId: executionResult.focusPaneId,
paneNameToRealId: executionResult.paneNameToRealId,
windowId: executionResult.windowId
},
logger
});
} catch (error) {
return handlePipelineFailure(error);
}
logger.success(`Applied preset "${preset.name}"`);
return 0;
} catch (error) {
return handleError(error);
}
};
const buildPlanEmission = ({ core, preset, presetName }) => {
const compileResult = core.compilePresetFromValue({
value: preset,
source: buildPresetSource(presetName)
});
const planResult = core.createLayoutPlan({ preset: compileResult.preset });
return {
emission: core.emitPlan({ plan: planResult.plan }),
compiledPreset: compileResult.preset
};
};
const resolveWindowName = ({ presetName, presetDisplayName }) => {
return presetDisplayName ?? presetName ?? "vde-layout";
};
const resolveWindowModeForPreset = ({ presetManager, options, presetWindowMode }) => {
return resolveWindowMode({
cli: determineCliWindowMode({
currentWindow: options.currentWindow,
newWindow: options.newWindow
}),
preset: presetWindowMode,
defaults: presetManager.getDefaults()?.windowMode
});
};
//#endregion
//#region src/cli/select-args.ts
const selectUiModes = SELECT_UI_MODES;
const selectSurfaceModes = SELECT_SURFACE_MODES;
const selectUiModeSet = new Set(selectUiModes);
const selectSurfaceModeSet = new Set(selectSurfaceModes);
const isSelectUiMode = (value) => {
return selectUiModeSet.has(value);
};
const isSelectSurfaceMode = (value) => {
return selectSurfaceModeSet.has(value);
};
const normalizeSelectArgs = (args) => {
const normalized = [];
for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (typeof token !== "string") continue;
if (token === "--") {
normalized.push(...args.slice(index));
break;
}
if (typeof token === "string" && token.startsWith("--select=")) {
const mode = token.slice(9);
normalized.push("--select");
if (mode.length > 0) normalized.push("--select-ui", mode);
continue;
}
if (token === "--select") {
const nextToken = args[index + 1];
if (typeof nextToken === "string" && isSelectUiMode(nextToken)) {
normalized.push("--select", "--select-ui", nextToken);
index += 1;
continue;
}
}
normalized.push(token);
}
return normalized;
};
const resolveSelectUiMode = (uiValue) => {
if (uiValue === void 0) return "auto";
if (isSelectUiMode(uiValue)) return uiValue;
throw new Error(`Invalid value for --select-ui: "${uiValue}". Expected one of: ${selectUiModes.join(", ")}`);
};
const resolveSelectSurfaceMode = (surfaceValue) => {
if (surfaceValue === void 0) return "auto";
if (isSelectSurfaceMode(surfaceValue)) return surfaceValue;
throw new Error(`Invalid value for --select-surface: "${surfaceValue}". Expected one of: ${selectSurfaceModes.join(", ")}`);
};
//#endregion
//#region src/cli/preset-selector.ts
const FZF_BINARY = "fzf";
const FZF_CHECK_TIMEOUT_MS = 5e3;
const MAX_PREVIEW_BASE64_LENGTH = 64 * 1024;
const RESERVED_FZF_ARGS = new Set([
"delimiter",
"with-nth",
"ansi",
"preview",
"preview-window",
"tmux"
]);
const selectorChalk = new Chalk({ level: 1 });
const sanitizeTsvCell = (value) => {
return (value ?? "").replace(/[\t\r\n]+/g, " ");
};
const padDisplayCell = (value, width) => {
const paddingLength = Math.max(0, width - stringWidth(value));
return `${value}${" ".repeat(paddingLength)}`;
};
const buildPresetPreviewYaml = ({ presetKey, preset }) => {
return YAML.stringify({ presets: { [presetKey]: preset } });
};
const defaultCheckFzfAvailability = async () => {
try {
await execa(FZF_BINARY, ["--version"], { timeout: FZF_CHECK_TIMEOUT_MS });
return true;
} catch (error) {
const execaError = error;
if (execaError.code === "ENOENT" || execaError.code === "ETIMEDOUT" || execaError.code === "ERR_EXECA_TIMEOUT" || execaError.timedOut === true) return false;
throw error;
}
};
const defaultRunFzf = async ({ args, input, cwd, env }) => {
return { stdout: (await execa(FZF_BINARY, args, {
input,
cwd,
env,
stderr: "inherit"
})).stdout };
};
const ensureFzfAvailable = async (checkFzfAvailability) => {
if (await checkFzfAvailability()) return;
throw createEnvironmentError("fzf is required for preset selection UI", ErrorCodes.BACKEND_NOT_FOUND, {
backend: "fzf",
binary: FZF_BINARY
});
};
const isTmuxSession = (env) => {
return typeof env.TMUX === "string" && env.TMUX.length > 0;
};
const resolveSurfaceMode = ({ surfaceMode, env }) => {
if (surfaceMode === "auto") return isTmuxSession(env) ? "tmux-popup" : "inline";
return surfaceMode;
};
const validateExtraFzfArgs = (fzfExtraArgs) => {
for (const arg of fzfExtraArgs) {
if (typeof arg !== "string" || arg.length === 0) throw new Error("Empty value is not allowed for --fzf-arg");
if (!arg.startsWith("--")) continue;
const withoutPrefix = arg.slice(2);
if (withoutPrefix.length === 0) continue;
const optionName = withoutPrefix.split("=")[0];
if (optionName !== void 0 && RESERVED_FZF_ARGS.has(optionName)) throw new Error(`--fzf-arg cannot override reserved fzf option: --${optionName}`);
}
};
const buildFzfArgs = ({ surfaceMode, tmuxPopupOptions, fzfExtraArgs, env }) => {
validateExtraFzfArgs(fzfExtraArgs);
const resolvedSurfaceMode = resolveSurfaceMode({
surfaceMode,
env
});
if (resolvedSurfaceMode === "tmux-popup" && isTmuxSession(env) !== true) throw new Error("tmux popup selector surface requires running inside tmux");
return [
"--delimiter=\\t",
"--ansi",
"--with-nth=2",
"--prompt=preset> ",
"--layout=reverse",
"--height=80%",
"--border",
"--preview=node -e 'process.stdout.write(Buffer.from(process.argv[1], \"base64\").toString(\"utf8\"))' {3}",
"--preview-window=right,60%,border-left,wrap",
...resolvedSurfaceMode === "tmux-popup" ? [tmuxPopupOptions !== void 0 ? `--tmux=${tmuxPopupOptions}` : "--tmux"] : [],
...fzfExtraArgs
];
};
const toPresetRows = ({ presetInfos, presetManager }) => {
const rows = presetInfos.map((presetInfo) => {
const key = sanitizeTsvCell(presetInfo.key);
const name = sanitizeTsvCell(presetInfo.name);
const description = sanitizeTsvCell(presetInfo.description);
const preset = presetManager.getPreset(presetInfo.key);
const previewYaml = buildPresetPreviewYaml({
presetKey: presetInfo.key,
preset
});
const previewBase64 = Buffer.from(previewYaml, "utf8").toString("base64");
if (previewBase64.length > MAX_PREVIEW_BASE64_LENGTH) throw new Error(`Preset preview is too large for fzf inline preview payload: "${presetInfo.key}" (${previewBase64.length} bytes)`);
return {
key,
name,
description,
previewBase64
};
});
const keyColumnWidth = rows.reduce((maxWidth, row) => Math.max(maxWidth, stringWidth(row.key)), 0);
const nameColumnWidth = rows.reduce((maxWidth, row) => Math.max(maxWidth, stringWidth(row.name)), 0);
return rows.map((row) => {
const key = padDisplayCell(row.key, keyColumnWidth);
const name = padDisplayCell(row.name, nameColumnWidth);
const description = row.description.length > 0 ? row.description : selectorChalk.gray("(no description)");
const display = `${selectorChalk.cyan(key)} ${selectorChalk.bold(name)} ${selectorChalk.dim(description)}`;
return {
key: row.key,
name: row.name,
description: row.description,
display,
previewBase64: row.previewBase64
};
});
};
const buildFzfInput = (rows) => {
return rows.map((row, index) => {
return [
String(index),
row.display,
row.previewBase64
].join(" ");
}).join("\n");
};
const parseSelectedPresetName = ({ selectedLine, rows }) => {
const trimmed = selectedLine.trim();
if (trimmed.length === 0) return null;
const idCell = trimmed.split(" ")[0];
const id = Number(idCell);
if (!Number.isInteger(id) || id < 0 || id >= rows.length) throw new Error("Invalid selection returned from fzf");
return rows[id]?.key ?? null;
};
const runFzfSelector = async ({ rows, fzfArgs, runFzf, cwd, env }) => {
try {
const presetName = parseSelectedPresetName({
selectedLine: (await runFzf({
input: buildFzfInput(rows),
args: [...fzfArgs],
cwd,
env
})).stdout,
rows
});
if (presetName === null) return { status: "cancelled" };
return {
status: "selected",
presetName
};
} catch (error) {
if (error.exitCode === 130) return { status: "cancelled" };
throw error;
}
};
const selectPreset = async ({ uiMode, surfaceMode, tmuxPopupOptions, fzfExtraArgs = [], presetManager, logger, skipLoadConfig = false, cwd = process.cwd(), env = process.env, isInteractive = () => process.stdin.isTTY === true && process.stdout.isTTY === true && process.stderr.isTTY === true, checkFzfAvailability = defaultCheckFzfAvailability, runFzf = defaultRunFzf }) => {
if (isInteractive() !== true) throw new Error("Preset selection requires an interactive terminal");
await ensureFzfAvailable(checkFzfAvailability);
if (skipLoadConfig !== true) await presetManager.loadConfig();
const presetInfos = presetManager.listPresets();
if (presetInfos.length === 0) throw new Error("No presets defined");
const fzfArgs = buildFzfArgs({
surfaceMode,
tmuxPopupOptions,
fzfExtraArgs,
env
});
logger.debug(`Preset selection UI: ${uiMode}`);
return runFzfSelector({
rows: toPresetRows({
presetInfos,
presetManager
}),
fzfArgs,
runFzf,
cwd,
env
});
};
//#endregion
//#region src/cli/index.ts
const backendValues = ["tmux", "wezterm"];
const listCommandName = "list";
const EXIT_CODE_CANCELLED = 130;
const optionNamesAllowOptionLikeValue = new Set(["fzfArg", "fzf-arg"]);
const toKebabCase = (value) => {
return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
};
const toOptionSpec = (kind, optionName) => {
return {
kind,
allowOptionLikeValue: optionNamesAllowOptionLikeValue.has(optionName)
};
};
const buildOptionSpecs = (argsDef) => {
const longOptions = /* @__PURE__ */ new Map();
const shortOptions = /* @__PURE__ */ new Map();
for (const [argName, arg] of Object.entries(argsDef)) {
if (arg.type === "positional") continue;
const valueKind = arg.type === "boolean" ? "boolean" : "value";
const kebabName = toKebabCase(argName);
longOptions.set(argName, toOptionSpec(valueKind, argName));
longOptions.set(kebabName, toOptionSpec(valueKind, kebabName));
const aliases = "alias" in arg ? Array.isArray(arg.alias) ? arg.alias : typeof arg.alias === "string" ? [arg.alias] : [] : [];
for (const alias of aliases) {
if (alias.length === 1) {
shortOptions.set(alias, toOptionSpec(valueKind, alias));
continue;
}
longOptions.set(alias, toOptionSpec(valueKind, alias));
const kebabAlias = toKebabCase(alias);
longOptions.set(kebabAlias, toOptionSpec(valueKind, kebabAlias));
}
}
return {
longOptions,
shortOptions
};
};
const validateRawOptions = (args, optionSpecs) => {
for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (typeof token !== "string") continue;
if (token === "--") break;
if (!token.startsWith("-") || token === "-") continue;
if (token.startsWith("--")) {
const value = token.slice(2);
if (value.length === 0) continue;
const separatorIndex = value.indexOf("=");
const rawOptionName = separatorIndex >= 0 ? value.slice(0, separatorIndex) : value;
const optionName = rawOptionName.startsWith("no-") ? rawOptionName.slice(3) : rawOptionName;
const optionSpec = optionSpecs.longOptions.get(optionName);
const kind = optionSpec?.kind;
if (kind === void 0) throw new Error(`Unknown option: --${rawOptionName}`);
if (kind === "value") if (separatorIndex >= 0) {
if (value.slice(separatorIndex + 1).length === 0) throw new Error(`Missing value for option: --${optionName}`);
} else {
const nextToken = args[index + 1];
if (typeof nextToken !== "string" || nextToken.length === 0) throw new Error(`Missing value for option: --${optionName}`);
if (nextToken.startsWith("-") && optionSpec?.allowOptionLikeValue !== true) throw new Error(`Missing value for option: --${optionName}`);
index += 1;
}
continue;
}
const shortFlags = token.slice(1);
for (let flagIndex = 0; flagIndex < shortFlags.length; flagIndex += 1) {
const option = shortFlags[flagIndex];
if (typeof option !== "string" || option.length === 0) continue;
const optionSpec = optionSpecs.shortOptions.get(option);
const kind = optionSpec?.kind;
if (kind === void 0) throw new Error(`Unknown option: -${option}`);
if (kind === "value") {
if (flagIndex < shortFlags.length - 1) break;
const nextToken = args[index + 1];
if (typeof nextToken !== "string" || nextToken.length === 0) throw new Error(`Missing value for option: -${option}`);
if (nextToken.startsWith("-") && optionSpec?.allowOptionLikeValue !== true) throw new Error(`Missing value for option: -${option}`);
index += 1;
break;
}
}
}
};
const getPositionals = (args) => {
return args._.filter((value) => typeof value === "string");
};
const collectOptionValues = ({ args, optionNames }) => {
const values = [];
const optionNameSet = new Set(optionNames);
for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (typeof token !== "string") continue;
if (token === "--") break;
if (!token.startsWith("--")) continue;
const eqIndex = token.indexOf("=");
const rawName = eqIndex >= 0 ? token.slice(2, eqIndex) : token.slice(2);
if (optionNameSet.has(rawName) !== true) continue;
if (eqIndex >= 0) {
values.push(token.slice(eqIndex + 1));
continue;
}
const nextToken = args[index + 1];
if (typeof nextToken === "string") {
values.push(nextToken);
index += 1;
}
}
return values;
};
const createCli = (options = {}) => {
const presetManager = options.presetManager ?? createPresetManager();
const createCommandExecutor = options.createCommandExecutor ?? ((opts) => {
if (opts.dryRun) return createDryRunExecutor({ verbose: opts.verbose });
return createRealExecutor({ verbose: opts.verbose });
});
const selectPreset$1 = options.selectPreset ?? selectPreset;
const core = options.core ?? {
compilePreset,
compilePresetFromValue,
createLayoutPlan,
emitPlan
};
const version = loadPackageVersion(createRequire(import.meta.url));
let logger = createLogger();
const errorHandlers = createCliErrorHandlers({ getLogger: () => logger });
const rootArgsDef = {
preset: {
type: "positional",
description: "Preset name (defaults to \"default\" preset when omitted)",
required: false
},
verbose: {
type: "boolean",
description: "Show detailed logs"
},
dryRun: {
type: "boolean",
description: "Display commands without executing"
},
backend: {
type: "enum",
options: [...backendValues],
description: "Select terminal backend (tmux or wezterm)"
},
config: {
type: "string",
valueHint: "path",
description: "Path to configuration file"
},
currentWindow: {
type: "boolean",
description: "Use the current tmux window for layout (kills other panes)"
},
newWindow: {
type: "boolean",
description: "Always create a new tmux window for layout"
},
select: {
type: "boolean",
description: "Select preset from interactive UI"
},
selectUi: {
type: "enum",
options: [...selectUiModes],
description: "Select preset UI backend (auto or fzf)"
},
selectSurface: {
type: "enum",
options: [...selectSurfaceModes],
description: "Select selector surface mode (auto, inline, or tmux-popup)"
},
selectTmuxPopupOpts: {
type: "string",
valueHint: "opts",
description: "tmux popup options used for fzf --tmux=<opts> (example: 80%,70%)"
},
fzfArg: {
type: "string",
valueHint: "arg",
description: "Additional argument passed to fzf selector (repeatable)"
},
help: {
type: "boolean",
alias: "h",
description: "Show help"
},
version: {
type: "boolean",
alias: "v",
description: "Show version"
}
};
const listCommand = defineCommand({ meta: {
name: listCommandName,
description: "List available presets"
} });
const rootCommand = defineCommand({
meta: {
name: "vde-layout",
description: "VDE (Vibrant Development Environment) Layout Manager - tmux pane layout management tool",
version
},
args: rootArgsDef,
subCommands: { [listCommandName]: listCommand }
});
const optionSpecs = buildOptionSpecs(rootArgsDef);
const run = async (args = process.argv.slice(2)) => {
logger = createLogger();
try {
const normalizedArgs = normalizeSelectArgs(args);
validateRawOptions(normalizedArgs, optionSpecs);
const parsedArgs = parseArgs(normalizedArgs, rootArgsDef);
const fzfCliArgs = collectOptionValues({
args: normalizedArgs,
optionNames: ["fzf-arg", "fzfArg"]
});
const positionals = getPositionals(parsedArgs);
const headPositional = positionals[0];
if (parsedArgs.help === true) {
const usage = headPositional === listCommandName ? await renderUsage(listCommand, rootCommand) : await renderUsage(rootCommand);
console.log(`${usage}\n`);
return 0;
}
if (parsedArgs.version === true) {
console.log(version);
return 0;
}
logger = applyRuntimeOptions({
runtimeOptions: {
verbose: parsedArgs.verbose === true,
config: typeof parsedArgs.config === "string" ? parsedArgs.config : void 0
},
createLogger,
presetManager
});
if (headPositional === listCommandName) {
const extraArgs = positionals.slice(1);
if (extraArgs.length > 0) throw new Error(`too many arguments for '${listCommandName}'. Expected 0 arguments but got ${extraArgs.length}.`);
return await listPresets({
presetManager,
logger,
onError: errorHandlers.handleError
});
}
if (positionals.length > 1) throw new Error(`too many arguments. Expected at most 1 argument but got ${positionals.length}.`);
if (parsedArgs.selectUi !== void 0 && parsedArgs.select !== true) throw new Error("--select-ui requires --select");
if (parsedArgs.selectSurface !== void 0 && parsedArgs.select !== true) throw new Error("--select-surface requires --select");
if (parsedArgs.selectTmuxPopupOpts !== void 0 && parsedArgs.select !== true) throw new Error("--select-tmux-popup-opts requires --select");
if (fzfCliArgs.length > 0 && parsedArgs.select !== true) throw new Error("--fzf-arg requires --select");
if (parsedArgs.select === true && typeof headPositional === "string" && headPositional.length > 0) throw new Error("Cannot use preset argument with --select");
let resolvedPresetName = headPositional;
let configLoaded = false;
if (parsedArgs.select === true) {
await presetManager.loadConfig();
configLoaded = true;
const selectorDefaults = presetManager.getDefaults()?.selector;
const selection = await selectPreset$1({
uiMode: resolveSelectUiMode(typeof parsedArgs.selectUi === "string" ? parsedArgs.selectUi : selectorDefaults?.ui),
surfaceMode: resolveSelectSurfaceMode(typeof parsedArgs.selectSurface === "string" ? parsedArgs.selectSurface : selectorDefaults?.surface),
tmuxPopupOptions: typeof parsedArgs.selectTmuxPopupOpts === "string" ? parsedArgs.selectTmuxPopupOpts : selectorDefaults?.tmuxPopupOpts,
fzfExtraArgs: [...selectorDefaults?.fzf?.extraArgs ?? [], ...fzfCliArgs],
presetManager,
logger,
skipLoadConfig: true
});
if (selection.status === "cancelled") return EXIT_CODE_CANCELLED;
resolvedPresetName = selection.presetName;
}
return await executePreset({
presetName: resolvedPresetName,
skipLoadConfig: configLoaded,
options: {
verbose: parsedArgs.verbose === true,
dryRun: parsedArgs.dryRun === true,
currentWindow: parsedArgs.currentWindow === true,
newWindow: parsedArgs.newWindow === true,
backend: typeof parsedArgs.backend === "string" ? parsedArgs.backend : void 0
},
presetManager,
createCommandExecutor,
core,
logger,
handleError: errorHandlers.handleError,
handlePipelineFailure: errorHandlers.handlePipelineFailure
});
} catch (error) {
return errorHandlers.handleError(error);
}
};
return { run };
};
//#endregion
//#region src/index.ts
/**
* Main entry point
* Launches the CLI application
*/
const main = async () => {
const cli = createCli();
try {
const exitCode = await cli.run(process.argv.slice(2));
if (typeof exitCode === "number" && exitCode !== 0) process.exit(exitCode);
} catch (error) {
if (error instanceof Error) {
console.error("Error:", error.message);
if (process.env.VDE_DEBUG === "true") console.error(error.stack);
} else console.error("An unexpected error occurred:", String(error));
process.exit(1);
}
};
main();
//#endregion
export { };
//# sourceMappingURL=index.mjs.map