vde-layout 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +17 -0
  2. package/bin/vde-layout +9 -1
  3. package/dist/index.js +1459 -22
  4. package/dist/index.js.map +1 -1
  5. package/package.json +22 -11
  6. package/dist/cli.d.ts +0 -27
  7. package/dist/cli.d.ts.map +0 -1
  8. package/dist/cli.js +0 -138
  9. package/dist/cli.js.map +0 -1
  10. package/dist/config/loader.d.ts +0 -14
  11. package/dist/config/loader.d.ts.map +0 -1
  12. package/dist/config/loader.js +0 -117
  13. package/dist/config/loader.js.map +0 -1
  14. package/dist/config/validator.d.ts +0 -3
  15. package/dist/config/validator.d.ts.map +0 -1
  16. package/dist/config/validator.js +0 -168
  17. package/dist/config/validator.js.map +0 -1
  18. package/dist/executor/dry-run-executor.d.ts +0 -16
  19. package/dist/executor/dry-run-executor.d.ts.map +0 -1
  20. package/dist/executor/dry-run-executor.js +0 -45
  21. package/dist/executor/dry-run-executor.js.map +0 -1
  22. package/dist/executor/index.d.ts +0 -6
  23. package/dist/executor/index.d.ts.map +0 -1
  24. package/dist/executor/index.js +0 -10
  25. package/dist/executor/index.js.map +0 -1
  26. package/dist/executor/mock-executor.d.ts +0 -21
  27. package/dist/executor/mock-executor.d.ts.map +0 -1
  28. package/dist/executor/mock-executor.js +0 -73
  29. package/dist/executor/mock-executor.js.map +0 -1
  30. package/dist/executor/real-executor.d.ts +0 -16
  31. package/dist/executor/real-executor.d.ts.map +0 -1
  32. package/dist/executor/real-executor.js +0 -58
  33. package/dist/executor/real-executor.js.map +0 -1
  34. package/dist/index.d.ts +0 -3
  35. package/dist/index.d.ts.map +0 -1
  36. package/dist/interfaces/command-executor.d.ts +0 -7
  37. package/dist/interfaces/command-executor.d.ts.map +0 -1
  38. package/dist/interfaces/command-executor.js +0 -3
  39. package/dist/interfaces/command-executor.js.map +0 -1
  40. package/dist/interfaces/index.d.ts +0 -31
  41. package/dist/interfaces/index.d.ts.map +0 -1
  42. package/dist/interfaces/index.js +0 -3
  43. package/dist/interfaces/index.js.map +0 -1
  44. package/dist/layout/engine.d.ts +0 -18
  45. package/dist/layout/engine.d.ts.map +0 -1
  46. package/dist/layout/engine.js +0 -174
  47. package/dist/layout/engine.js.map +0 -1
  48. package/dist/layout/preset.d.ts +0 -13
  49. package/dist/layout/preset.d.ts.map +0 -1
  50. package/dist/layout/preset.js +0 -55
  51. package/dist/layout/preset.js.map +0 -1
  52. package/dist/models/schema.d.ts +0 -144
  53. package/dist/models/schema.d.ts.map +0 -1
  54. package/dist/models/schema.js +0 -95
  55. package/dist/models/schema.js.map +0 -1
  56. package/dist/models/types.d.ts +0 -34
  57. package/dist/models/types.d.ts.map +0 -1
  58. package/dist/models/types.js +0 -16
  59. package/dist/models/types.js.map +0 -1
  60. package/dist/tmux/commands.d.ts +0 -14
  61. package/dist/tmux/commands.d.ts.map +0 -1
  62. package/dist/tmux/commands.js +0 -59
  63. package/dist/tmux/commands.js.map +0 -1
  64. package/dist/tmux/executor.d.ts +0 -20
  65. package/dist/tmux/executor.d.ts.map +0 -1
  66. package/dist/tmux/executor.js +0 -64
  67. package/dist/tmux/executor.js.map +0 -1
  68. package/dist/utils/errors.d.ts +0 -36
  69. package/dist/utils/errors.d.ts.map +0 -1
  70. package/dist/utils/errors.js +0 -131
  71. package/dist/utils/errors.js.map +0 -1
  72. package/dist/utils/logger.d.ts +0 -25
  73. package/dist/utils/logger.d.ts.map +0 -1
  74. package/dist/utils/logger.js +0 -67
  75. package/dist/utils/logger.js.map +0 -1
  76. package/dist/utils/ratio.d.ts +0 -3
  77. package/dist/utils/ratio.d.ts.map +0 -1
  78. package/dist/utils/ratio.js +0 -44
  79. package/dist/utils/ratio.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,24 +1,1461 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- const cli_1 = require("./cli");
5
- async function main() {
6
- const cli = new cli_1.CLI();
7
- try {
8
- await cli.run(process.argv.slice(2));
9
- }
10
- catch (error) {
11
- if (error instanceof Error) {
12
- console.error("Error:", error.message);
13
- if (process.env.VDE_DEBUG === "true") {
14
- console.error(error.stack);
15
- }
16
- }
17
- else {
18
- console.error("An unexpected error occurred:", String(error));
19
- }
20
- process.exit(1);
21
- }
22
- }
23
- void main();
2
+ import { createRequire } from "node:module";
3
+ import { Command } from "commander";
4
+ import chalk from "chalk";
5
+ import * as YAML from "yaml";
6
+ import { parse, stringify } from "yaml";
7
+ import fs from "fs-extra";
8
+ import path from "node:path";
9
+ import os from "node:os";
10
+ import { z } from "zod";
11
+ import { execa } from "execa";
12
+ import { createHash } from "node:crypto";
13
+
14
+ //#region src/utils/errors.ts
15
+ const ErrorCodes = {
16
+ CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
17
+ CONFIG_PARSE_ERROR: "CONFIG_PARSE_ERROR",
18
+ CONFIG_PERMISSION_ERROR: "CONFIG_PERMISSION_ERROR",
19
+ INVALID_PRESET: "INVALID_PRESET",
20
+ PRESET_NOT_FOUND: "PRESET_NOT_FOUND",
21
+ INVALID_LAYOUT: "INVALID_LAYOUT",
22
+ INVALID_PANE: "INVALID_PANE",
23
+ TMUX_NOT_RUNNING: "TMUX_NOT_RUNNING",
24
+ TMUX_COMMAND_FAILED: "TMUX_COMMAND_FAILED",
25
+ NOT_IN_TMUX_SESSION: "NOT_IN_TMUX_SESSION",
26
+ NOT_IN_TMUX: "NOT_IN_TMUX",
27
+ TMUX_NOT_FOUND: "TMUX_NOT_FOUND",
28
+ TMUX_NOT_INSTALLED: "TMUX_NOT_INSTALLED",
29
+ UNSUPPORTED_TMUX_VERSION: "UNSUPPORTED_TMUX_VERSION"
30
+ };
31
+ const createBaseError = (name, message, code, details = {}) => {
32
+ const error = new Error(message);
33
+ error.name = name;
34
+ error.code = code;
35
+ error.details = details;
36
+ return error;
37
+ };
38
+ const createConfigError = (message, code, details = {}) => {
39
+ return createBaseError("ConfigError", message, code, details);
40
+ };
41
+ const createValidationError = (message, code, details = {}) => {
42
+ return createBaseError("ValidationError", message, code, details);
43
+ };
44
+ const createTmuxError = (message, code, details = {}) => {
45
+ return createBaseError("TmuxError", message, code, details);
46
+ };
47
+ const isVDELayoutError = (error) => {
48
+ if (typeof error !== "object" || error === null) return false;
49
+ if (!("code" in error)) return false;
50
+ const { code } = error;
51
+ if (typeof code !== "string") return false;
52
+ if (!("details" in error)) return false;
53
+ return true;
54
+ };
55
+ const formatters = {
56
+ [ErrorCodes.CONFIG_NOT_FOUND]: (error) => {
57
+ const searchPaths = error.details.searchPaths;
58
+ if (!Array.isArray(searchPaths)) return "";
59
+ const lines = ["", "Searched in the following locations:"];
60
+ searchPaths.forEach((location) => lines.push(` - ${location}`));
61
+ lines.push("", "To create a configuration file, run:");
62
+ lines.push(" mkdir -p ~/.config/vde");
63
+ lines.push(" echo \"presets: {}\" > ~/.config/vde/layout.yml");
64
+ return lines.join("\n");
65
+ },
66
+ [ErrorCodes.NOT_IN_TMUX_SESSION]: () => {
67
+ return "\nThis command must be run inside a tmux session.\nStart tmux first with: tmux\n";
68
+ },
69
+ [ErrorCodes.TMUX_NOT_INSTALLED]: () => {
70
+ 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";
71
+ },
72
+ [ErrorCodes.UNSUPPORTED_TMUX_VERSION]: (error) => {
73
+ const requiredVersion = error.details.requiredVersion;
74
+ if (typeof requiredVersion !== "string") return "";
75
+ return `\nRequired tmux version: ${requiredVersion} or higher\n`;
76
+ }
77
+ };
78
+
79
+ //#endregion
80
+ //#region src/models/schema.ts
81
+ const TerminalPaneSchema = z.object({
82
+ name: z.string().min(1),
83
+ command: z.string().optional(),
84
+ cwd: z.string().optional(),
85
+ env: z.record(z.string()).optional(),
86
+ delay: z.number().int().positive().optional(),
87
+ title: z.string().optional(),
88
+ focus: z.boolean().optional()
89
+ }).strict();
90
+ const SplitPaneSchema = z.lazy(() => z.object({
91
+ type: z.enum(["horizontal", "vertical"]),
92
+ ratio: z.array(z.number().positive()).min(1),
93
+ panes: z.array(PaneSchema).min(1)
94
+ }).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" }));
95
+ const PaneSchema = z.lazy(() => z.union([SplitPaneSchema, TerminalPaneSchema]));
96
+ const LayoutSchema = z.object({
97
+ type: z.enum(["horizontal", "vertical"]),
98
+ ratio: z.array(z.number().positive()).min(1),
99
+ panes: z.array(PaneSchema).min(1)
100
+ }).refine((data) => data.ratio.length === data.panes.length, { message: "Number of elements in ratio array does not match number of elements in panes array" });
101
+ const PresetSchema = z.object({
102
+ name: z.string().min(1),
103
+ description: z.string().optional(),
104
+ layout: LayoutSchema.optional(),
105
+ command: z.string().optional()
106
+ });
107
+ const ConfigSchema = z.object({ presets: z.record(PresetSchema) });
108
+
109
+ //#endregion
110
+ //#region src/config/validator.ts
111
+ /**
112
+ * Parse YAML text into an object
113
+ * @param yamlText - YAML text to parse
114
+ * @returns Parsed object
115
+ * @throws {ValidationError} When YAML parsing fails
116
+ */
117
+ const parseYAML = (yamlText) => {
118
+ if (!yamlText || typeof yamlText !== "string") throw createValidationError("YAML text not provided", ErrorCodes.CONFIG_PARSE_ERROR, { received: typeof yamlText });
119
+ try {
120
+ return YAML.parse(yamlText);
121
+ } catch (error) {
122
+ throw createValidationError("Failed to parse YAML", ErrorCodes.CONFIG_PARSE_ERROR, {
123
+ parseError: error instanceof Error ? error.message : String(error),
124
+ yamlSnippet: yamlText.substring(0, 200)
125
+ });
126
+ }
127
+ };
128
+ /**
129
+ * Validate basic configuration structure
130
+ * @param parsed - Parsed YAML object
131
+ * @throws {ValidationError} When structure is invalid
132
+ */
133
+ const validateConfigStructure = (parsed) => {
134
+ if (parsed === null || parsed === void 0 || typeof parsed !== "object") throw createValidationError("YAML is empty or invalid format", ErrorCodes.CONFIG_PARSE_ERROR, { parsed });
135
+ const parsedObj = parsed;
136
+ if (!("presets" in parsedObj) || parsedObj.presets === void 0 || parsedObj.presets === null) throw createValidationError("presets field is required", ErrorCodes.INVALID_PRESET, { availableFields: Object.keys(parsedObj) });
137
+ const presetsObj = parsedObj.presets;
138
+ if (typeof presetsObj !== "object" || presetsObj === null || Object.keys(presetsObj).length === 0) throw createValidationError("At least one preset is required", ErrorCodes.INVALID_PRESET, { presets: presetsObj });
139
+ };
140
+ /**
141
+ * Format Zod validation errors into user-friendly messages
142
+ * @param error - Zod validation error
143
+ * @returns Formatted error issues
144
+ */
145
+ const formatZodErrors = (error) => {
146
+ return error.issues.map((issue) => {
147
+ const path$1 = issue.path.join(".");
148
+ let message = issue.message;
149
+ if (issue.code === "invalid_type") {
150
+ if (issue.path.includes("command") && issue.expected === "string") message = "command field must be a string";
151
+ else if (issue.path.includes("workingDirectory") && issue.expected === "string") message = "workingDirectory field must be a string";
152
+ else if (issue.received === "number" && issue.expected === "string") message = `${path$1} must be a string`;
153
+ else if (issue.received === "array" && issue.expected === "string") message = `${path$1} must be a string`;
154
+ } else if (issue.code === "invalid_union") {
155
+ const unionIssue = issue;
156
+ 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";
157
+ 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";
158
+ else message = "Pane type must be \"terminal\" or \"split\"";
159
+ else message = "Pane type must be \"terminal\" or \"split\"";
160
+ } else if (issue.code === "invalid_literal") {
161
+ if (issue.path.includes("direction")) message = "direction must be \"horizontal\" or \"vertical\"";
162
+ } else if (issue.message.includes("required")) message = issue.message;
163
+ else if (issue.code === "custom" && issue.message.includes("ratio array")) message = issue.message;
164
+ else if (issue.code === "too_small" && issue.message.includes("Array must contain at least")) if (path$1.includes("panes")) message = "panes array must contain at least 2 elements";
165
+ else if (path$1.includes("ratio")) message = "ratio array must contain at least 2 elements";
166
+ else message = issue.message;
167
+ return {
168
+ path: path$1,
169
+ message,
170
+ code: issue.code
171
+ };
172
+ });
173
+ };
174
+ /**
175
+ * Validates YAML text and converts it to a type-safe Config object
176
+ * @param yamlText - YAML text to validate
177
+ * @returns Validated Config object
178
+ * @throws {ValidationError} When YAML is invalid
179
+ */
180
+ const validateYAML = (yamlText) => {
181
+ const parsed = parseYAML(yamlText);
182
+ validateConfigStructure(parsed);
183
+ try {
184
+ return ConfigSchema.parse(parsed);
185
+ } catch (error) {
186
+ if (error instanceof z.ZodError) {
187
+ const issues = formatZodErrors(error);
188
+ const primaryMessage = issues.length > 0 && issues[0] ? issues[0].message : "Configuration validation failed";
189
+ throw createValidationError(primaryMessage, ErrorCodes.CONFIG_PARSE_ERROR, {
190
+ issues,
191
+ rawErrors: error.issues
192
+ });
193
+ }
194
+ if (isVDELayoutError(error) && error.name === "ValidationError") throw error;
195
+ throw createValidationError("Unexpected validation error occurred", ErrorCodes.CONFIG_PARSE_ERROR, { error: error instanceof Error ? error.message : String(error) });
196
+ }
197
+ };
198
+
199
+ //#endregion
200
+ //#region src/config/loader.ts
201
+ const createConfigLoader = (options = {}) => {
202
+ const explicitConfigPaths = options.configPaths;
203
+ const computeCachedSearchPaths = () => {
204
+ if (explicitConfigPaths && explicitConfigPaths.length > 0) return [...explicitConfigPaths];
205
+ const candidates = [];
206
+ const projectCandidate = findProjectConfigCandidate();
207
+ if (projectCandidate !== null) candidates.push(projectCandidate);
208
+ candidates.push(...buildDefaultSearchPaths());
209
+ return [...new Set(candidates)];
210
+ };
211
+ const loadConfig = async () => {
212
+ if (explicitConfigPaths && explicitConfigPaths.length > 0) {
213
+ const filePath = await findFirstExisting(explicitConfigPaths);
214
+ if (filePath === null) throw createConfigError("Configuration file not found", ErrorCodes.CONFIG_NOT_FOUND, { searchPaths: explicitConfigPaths });
215
+ const content = await safeReadFile(filePath);
216
+ return validateYAML(content);
217
+ }
218
+ const searchPaths = computeCachedSearchPaths();
219
+ const existingPaths = await filterExistingPaths(searchPaths);
220
+ if (existingPaths.length === 0) throw createConfigError("Configuration file not found", ErrorCodes.CONFIG_NOT_FOUND, { searchPaths });
221
+ const projectPath = findProjectConfigCandidate();
222
+ const globalPaths = existingPaths.filter((filePath) => filePath !== projectPath);
223
+ let mergedConfig = { presets: {} };
224
+ for (const globalPath of globalPaths) {
225
+ const content = await safeReadFile(globalPath);
226
+ const config = validateYAML(content);
227
+ mergedConfig = mergeConfigs(mergedConfig, config);
228
+ }
229
+ if (projectPath !== null && await fs.pathExists(projectPath)) {
230
+ const content = await safeReadFile(projectPath);
231
+ const config = validateYAML(content);
232
+ mergedConfig = mergeConfigs(mergedConfig, config);
233
+ }
234
+ return mergedConfig;
235
+ };
236
+ return {
237
+ loadYAML: async () => {
238
+ const config = await loadConfig();
239
+ return YAML.stringify(config);
240
+ },
241
+ loadConfig,
242
+ findConfigFile: async () => {
243
+ const searchPaths = explicitConfigPaths && explicitConfigPaths.length > 0 ? [...explicitConfigPaths] : computeCachedSearchPaths();
244
+ for (const searchPath of searchPaths) if (await fs.pathExists(searchPath)) return searchPath;
245
+ return null;
246
+ },
247
+ getSearchPaths: () => computeCachedSearchPaths()
248
+ };
249
+ };
250
+ const buildDefaultSearchPaths = () => {
251
+ const paths = [];
252
+ const vdeConfigPath = process.env.VDE_CONFIG_PATH;
253
+ if (vdeConfigPath !== void 0) paths.push(path.join(vdeConfigPath, "layout.yml"));
254
+ const homeDir = process.env.HOME ?? os.homedir();
255
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME ?? path.join(homeDir, ".config");
256
+ paths.push(path.join(xdgConfigHome, "vde", "layout.yml"));
257
+ return [...new Set(paths)];
258
+ };
259
+ const findProjectConfigCandidate = () => {
260
+ let currentDir = process.cwd();
261
+ const { root } = path.parse(currentDir);
262
+ while (true) {
263
+ const candidate = path.join(currentDir, ".vde", "layout.yml");
264
+ if (fs.existsSync(candidate)) return candidate;
265
+ if (currentDir === root) break;
266
+ const parent = path.dirname(currentDir);
267
+ if (parent === currentDir) break;
268
+ currentDir = parent;
269
+ }
270
+ return null;
271
+ };
272
+ const findFirstExisting = async (paths) => {
273
+ for (const candidate of paths) if (await fs.pathExists(candidate)) return candidate;
274
+ return null;
275
+ };
276
+ const filterExistingPaths = async (paths) => {
277
+ const existing = [];
278
+ for (const candidate of paths) if (await fs.pathExists(candidate)) existing.push(candidate);
279
+ return existing;
280
+ };
281
+ const safeReadFile = async (filePath) => {
282
+ try {
283
+ return await fs.readFile(filePath, "utf8");
284
+ } catch (error) {
285
+ const errorMessage = error instanceof Error ? error.message : String(error);
286
+ throw createConfigError(`Failed to read configuration file`, ErrorCodes.CONFIG_PERMISSION_ERROR, {
287
+ filePath,
288
+ error: errorMessage
289
+ });
290
+ }
291
+ };
292
+ const mergeConfigs = (base, override) => {
293
+ return { presets: {
294
+ ...base.presets,
295
+ ...override.presets
296
+ } };
297
+ };
298
+
299
+ //#endregion
300
+ //#region src/layout/preset.ts
301
+ const createState = (options = {}) => {
302
+ let loaderOptions = options;
303
+ let cachedConfig = null;
304
+ const setConfigPath = (filePath) => {
305
+ loaderOptions = { configPaths: [filePath] };
306
+ cachedConfig = null;
307
+ };
308
+ const loadConfig = async () => {
309
+ cachedConfig = await createConfigLoader(loaderOptions).loadConfig();
310
+ };
311
+ const ensureConfig = () => {
312
+ if (cachedConfig === null) throw createConfigError("Configuration not loaded", ErrorCodes.CONFIG_NOT_FOUND);
313
+ return cachedConfig;
314
+ };
315
+ const getPreset = (name) => {
316
+ const config = ensureConfig();
317
+ const preset = config.presets[name];
318
+ if (preset === void 0) throw createConfigError(`Preset "${name}" not found`, ErrorCodes.PRESET_NOT_FOUND, { availablePresets: Object.keys(config.presets) });
319
+ return preset;
320
+ };
321
+ const listPresets = () => {
322
+ if (cachedConfig === null) return [];
323
+ return Object.entries(cachedConfig.presets).map(([key, preset]) => ({
324
+ key,
325
+ name: preset.name,
326
+ description: preset.description
327
+ }));
328
+ };
329
+ const getDefaultPreset = () => {
330
+ const config = ensureConfig();
331
+ if (config.presets.default !== void 0) return config.presets.default;
332
+ const firstKey = Object.keys(config.presets)[0];
333
+ if (typeof firstKey !== "string" || firstKey.length === 0) throw createConfigError("No presets defined", ErrorCodes.PRESET_NOT_FOUND);
334
+ return config.presets[firstKey];
335
+ };
336
+ return {
337
+ setConfigPath,
338
+ loadConfig,
339
+ getPreset,
340
+ listPresets,
341
+ getDefaultPreset
342
+ };
343
+ };
344
+ const createPresetManager = (options = {}) => {
345
+ const state = createState(options);
346
+ return {
347
+ setConfigPath: state.setConfigPath,
348
+ loadConfig: state.loadConfig,
349
+ getPreset: state.getPreset,
350
+ listPresets: state.listPresets,
351
+ getDefaultPreset: state.getDefaultPreset
352
+ };
353
+ };
354
+
355
+ //#endregion
356
+ //#region src/utils/logger.ts
357
+ let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
358
+ LogLevel$1[LogLevel$1["ERROR"] = 0] = "ERROR";
359
+ LogLevel$1[LogLevel$1["WARN"] = 1] = "WARN";
360
+ LogLevel$1[LogLevel$1["INFO"] = 2] = "INFO";
361
+ LogLevel$1[LogLevel$1["DEBUG"] = 3] = "DEBUG";
362
+ return LogLevel$1;
363
+ }({});
364
+ const resolveDefaultLogLevel = () => {
365
+ if (process.env.VDE_DEBUG === "true") return LogLevel.DEBUG;
366
+ if (process.env.VDE_VERBOSE === "true") return LogLevel.INFO;
367
+ return LogLevel.WARN;
368
+ };
369
+ const formatMessage = (prefix, message) => {
370
+ return prefix ? `${prefix} ${message}` : message;
371
+ };
372
+ const createLogger = (options = {}) => {
373
+ const level = options.level ?? resolveDefaultLogLevel();
374
+ const prefix = options.prefix ?? "";
375
+ const build = (nextPrefix, nextLevel) => {
376
+ const resolvedPrefix = nextPrefix;
377
+ return {
378
+ level: nextLevel,
379
+ prefix: resolvedPrefix,
380
+ error(message, error) {
381
+ if (nextLevel >= LogLevel.ERROR) {
382
+ console.error(chalk.red(formatMessage(resolvedPrefix, `Error: ${message}`)));
383
+ if (error && process.env.VDE_DEBUG === "true") console.error(chalk.gray(error.stack));
384
+ }
385
+ },
386
+ warn(message) {
387
+ if (nextLevel >= LogLevel.WARN) console.warn(chalk.yellow(formatMessage(resolvedPrefix, message)));
388
+ },
389
+ info(message) {
390
+ if (nextLevel >= LogLevel.INFO) console.log(formatMessage(resolvedPrefix, message));
391
+ },
392
+ debug(message) {
393
+ if (nextLevel >= LogLevel.DEBUG) console.log(chalk.gray(formatMessage(resolvedPrefix, `[DEBUG] ${message}`)));
394
+ },
395
+ success(message) {
396
+ console.log(chalk.green(formatMessage(resolvedPrefix, message)));
397
+ },
398
+ createChild(suffix) {
399
+ const childPrefix = resolvedPrefix ? `${resolvedPrefix} ${suffix}` : suffix;
400
+ return build(childPrefix, nextLevel);
401
+ }
402
+ };
403
+ };
404
+ return build(prefix, level);
405
+ };
406
+
407
+ //#endregion
408
+ //#region src/executor/real-executor.ts
409
+ const parseCommand$1 = (commandOrArgs) => {
410
+ return typeof commandOrArgs === "string" ? commandOrArgs.split(" ").filter((segment) => segment.length > 0).slice(1) : commandOrArgs;
411
+ };
412
+ const toCommandString$1 = (args) => {
413
+ return ["tmux", ...args].join(" ");
414
+ };
415
+ const createRealExecutor = (options = {}) => {
416
+ const verbose = options.verbose ?? false;
417
+ const logger = createLogger({
418
+ level: verbose ? LogLevel.INFO : LogLevel.WARN,
419
+ prefix: "[tmux]"
420
+ });
421
+ const execute = async (commandOrArgs) => {
422
+ const args = parseCommand$1(commandOrArgs);
423
+ const commandString = toCommandString$1(args);
424
+ logger.info(`Executing: ${commandString}`);
425
+ try {
426
+ return (await execa("tmux", args)).stdout;
427
+ } catch (error) {
428
+ const execaError = error;
429
+ throw createTmuxError("Failed to execute tmux command", ErrorCodes.TMUX_COMMAND_FAILED, {
430
+ command: commandString,
431
+ exitCode: execaError.exitCode,
432
+ stderr: execaError.stderr
433
+ });
434
+ }
435
+ };
436
+ return {
437
+ execute,
438
+ async executeMany(commandsList) {
439
+ for (const args of commandsList) await execute(args);
440
+ },
441
+ isDryRun() {
442
+ return false;
443
+ },
444
+ logCommand(command) {
445
+ logger.info(`Executing: ${command}`);
446
+ }
447
+ };
448
+ };
449
+
450
+ //#endregion
451
+ //#region src/executor/dry-run-executor.ts
452
+ const parseCommand = (commandOrArgs) => {
453
+ return typeof commandOrArgs === "string" ? commandOrArgs.split(" ").filter((segment) => segment.length > 0).slice(1) : commandOrArgs;
454
+ };
455
+ const toCommandString = (args) => {
456
+ return ["tmux", ...args].join(" ");
457
+ };
458
+ const createDryRunExecutor = (options = {}) => {
459
+ const verbose = options.verbose ?? false;
460
+ const logger = createLogger({
461
+ level: verbose ? LogLevel.INFO : LogLevel.WARN,
462
+ prefix: "[tmux] [DRY RUN]"
463
+ });
464
+ const execute = async (commandOrArgs) => {
465
+ const args = parseCommand(commandOrArgs);
466
+ const commandString = toCommandString(args);
467
+ logger.info(`Would execute: ${commandString}`);
468
+ return "";
469
+ };
470
+ return {
471
+ execute,
472
+ async executeMany(commandsList) {
473
+ for (const args of commandsList) await execute(args);
474
+ },
475
+ isDryRun() {
476
+ return true;
477
+ },
478
+ logCommand(command) {
479
+ logger.info(`Would execute: ${command}`);
480
+ }
481
+ };
482
+ };
483
+
484
+ //#endregion
485
+ //#region src/core/errors.ts
486
+ const createFunctionalError = (kind, error) => ({
487
+ kind,
488
+ code: error.code,
489
+ message: error.message,
490
+ source: error.source,
491
+ path: error.path,
492
+ details: error.details
493
+ });
494
+ const isFunctionalCoreError = (value) => {
495
+ if (typeof value !== "object" || value === null) return false;
496
+ const candidate = value;
497
+ return (candidate.kind === "compile" || candidate.kind === "plan" || candidate.kind === "emit" || candidate.kind === "execution") && typeof candidate.code === "string" && typeof candidate.message === "string";
498
+ };
499
+
500
+ //#endregion
501
+ //#region src/executor/plan-runner.ts
502
+ const DOUBLE_QUOTE = "\"";
503
+ const ESCAPED_DOUBLE_QUOTE = "\\\"";
504
+ const executePlan = async ({ emission, executor, windowName }) => {
505
+ const initialVirtualPaneId = emission.summary.initialPaneId;
506
+ if (typeof initialVirtualPaneId !== "string" || initialVirtualPaneId.length === 0) raiseExecutionError("INVALID_PLAN", {
507
+ message: "Plan emission is missing initial pane metadata",
508
+ path: "plan.initialPaneId"
509
+ });
510
+ const paneMap = /* @__PURE__ */ new Map();
511
+ const newWindowCommand = [
512
+ "new-window",
513
+ "-P",
514
+ "-F",
515
+ "#{pane_id}"
516
+ ];
517
+ if (typeof windowName === "string" && windowName.trim().length > 0) newWindowCommand.push("-n", windowName.trim());
518
+ const initialPaneId = normalizePaneId(await executeCommand(executor, newWindowCommand, {
519
+ code: ErrorCodes.TMUX_COMMAND_FAILED,
520
+ message: "Failed to create tmux window",
521
+ path: initialVirtualPaneId
522
+ }));
523
+ registerPane(paneMap, initialVirtualPaneId, initialPaneId);
524
+ let executedSteps = 0;
525
+ for (const step of emission.steps) {
526
+ if (step.kind === "split") await executeSplitStep({
527
+ step,
528
+ executor,
529
+ paneMap
530
+ });
531
+ else if (step.kind === "focus") await executeFocusStep({
532
+ step,
533
+ executor,
534
+ paneMap
535
+ });
536
+ executedSteps += 1;
537
+ }
538
+ await executeTerminalCommands({
539
+ terminals: emission.terminals,
540
+ executor,
541
+ paneMap
542
+ });
543
+ const finalRealFocus = resolvePaneId(paneMap, emission.summary.focusPaneId);
544
+ if (typeof finalRealFocus === "string" && finalRealFocus.length > 0) await executeCommand(executor, [
545
+ "select-pane",
546
+ "-t",
547
+ finalRealFocus
548
+ ], {
549
+ code: ErrorCodes.TMUX_COMMAND_FAILED,
550
+ message: "Failed to restore focus",
551
+ path: emission.summary.focusPaneId
552
+ });
553
+ return { executedSteps };
554
+ };
555
+ const executeSplitStep = async ({ step, executor, paneMap }) => {
556
+ const targetVirtualId = ensureNonEmpty(step.targetPaneId, () => raiseExecutionError("MISSING_TARGET", {
557
+ message: "Split step missing target pane metadata",
558
+ path: step.id
559
+ }));
560
+ const targetRealId = ensureNonEmpty(resolvePaneId(paneMap, targetVirtualId), () => raiseExecutionError("UNKNOWN_PANE", {
561
+ message: `Unknown target pane: ${targetVirtualId}`,
562
+ path: step.id
563
+ }));
564
+ const panesBefore = await listPaneIds(executor, step);
565
+ const splitCommand = replaceTarget(step.command, targetRealId);
566
+ await executeCommand(executor, splitCommand, {
567
+ code: ErrorCodes.TMUX_COMMAND_FAILED,
568
+ message: `Failed to execute split step ${step.id}`,
569
+ path: step.id,
570
+ details: { command: splitCommand }
571
+ });
572
+ const panesAfter = await listPaneIds(executor, step);
573
+ const newPaneId = ensureNonEmpty(findNewPaneId(panesBefore, panesAfter), () => raiseExecutionError("UNKNOWN_PANE", {
574
+ message: "Unable to determine newly created pane",
575
+ path: step.id
576
+ }));
577
+ const createdVirtualId = step.createdPaneId;
578
+ if (typeof createdVirtualId === "string" && createdVirtualId.length > 0) registerPane(paneMap, createdVirtualId, newPaneId);
579
+ };
580
+ const executeFocusStep = async ({ step, executor, paneMap }) => {
581
+ const targetVirtualId = ensureNonEmpty(step.targetPaneId, () => raiseExecutionError("MISSING_TARGET", {
582
+ message: "Focus step missing target pane metadata",
583
+ path: step.id
584
+ }));
585
+ const targetRealId = ensureNonEmpty(resolvePaneId(paneMap, targetVirtualId), () => raiseExecutionError("UNKNOWN_PANE", {
586
+ message: `Unknown focus pane: ${targetVirtualId}`,
587
+ path: step.id
588
+ }));
589
+ const command = replaceTarget(step.command, targetRealId);
590
+ await executeCommand(executor, command, {
591
+ code: ErrorCodes.TMUX_COMMAND_FAILED,
592
+ message: `Failed to execute focus step ${step.id}`,
593
+ path: step.id,
594
+ details: { command }
595
+ });
596
+ };
597
+ const executeTerminalCommands = async ({ terminals, executor, paneMap }) => {
598
+ for (const terminal of terminals) {
599
+ const realPaneId = ensureNonEmpty(resolvePaneId(paneMap, terminal.virtualPaneId), () => raiseExecutionError("UNKNOWN_PANE", {
600
+ message: `Unknown terminal pane: ${terminal.virtualPaneId}`,
601
+ path: terminal.virtualPaneId
602
+ }));
603
+ if (typeof terminal.cwd === "string" && terminal.cwd.length > 0) {
604
+ const escapedCwd = terminal.cwd.split(DOUBLE_QUOTE).join(ESCAPED_DOUBLE_QUOTE);
605
+ await executeCommand(executor, [
606
+ "send-keys",
607
+ "-t",
608
+ realPaneId,
609
+ `cd "${escapedCwd}"`,
610
+ "Enter"
611
+ ], {
612
+ code: ErrorCodes.TMUX_COMMAND_FAILED,
613
+ message: `Failed to change directory for pane ${terminal.virtualPaneId}`,
614
+ path: terminal.virtualPaneId,
615
+ details: { cwd: terminal.cwd }
616
+ });
617
+ }
618
+ if (terminal.env !== void 0) for (const [key, value] of Object.entries(terminal.env)) {
619
+ const escaped = String(value).split(DOUBLE_QUOTE).join(ESCAPED_DOUBLE_QUOTE);
620
+ await executeCommand(executor, [
621
+ "send-keys",
622
+ "-t",
623
+ realPaneId,
624
+ `export ${key}="${escaped}"`,
625
+ "Enter"
626
+ ], {
627
+ code: ErrorCodes.TMUX_COMMAND_FAILED,
628
+ message: `Failed to set environment variable ${key}`,
629
+ path: terminal.virtualPaneId
630
+ });
631
+ }
632
+ if (typeof terminal.command === "string" && terminal.command.length > 0) await executeCommand(executor, [
633
+ "send-keys",
634
+ "-t",
635
+ realPaneId,
636
+ terminal.command,
637
+ "Enter"
638
+ ], {
639
+ code: ErrorCodes.TMUX_COMMAND_FAILED,
640
+ message: `Failed to execute command for pane ${terminal.virtualPaneId}`,
641
+ path: terminal.virtualPaneId,
642
+ details: { command: terminal.command }
643
+ });
644
+ }
645
+ };
646
+ const executeCommand = async (executor, command, context) => {
647
+ try {
648
+ return await executor.execute([...command]);
649
+ } catch (error) {
650
+ if (error instanceof Error && "code" in error && "message" in error) {
651
+ const candidate = error;
652
+ throw createFunctionalError("execution", {
653
+ code: typeof candidate.code === "string" ? candidate.code : context.code,
654
+ message: candidate.message ?? context.message,
655
+ path: context.path,
656
+ details: candidate.details ?? context.details
657
+ });
658
+ }
659
+ throw createFunctionalError("execution", {
660
+ code: context.code,
661
+ message: context.message,
662
+ path: context.path,
663
+ details: context.details
664
+ });
665
+ }
666
+ };
667
+ const listPaneIds = async (executor, step) => {
668
+ return (await executeCommand(executor, [
669
+ "list-panes",
670
+ "-F",
671
+ "#{pane_id}"
672
+ ], {
673
+ code: ErrorCodes.TMUX_COMMAND_FAILED,
674
+ message: "Failed to list tmux panes",
675
+ path: step.id
676
+ })).split("\n").map((pane) => pane.trim()).filter((pane) => pane.length > 0);
677
+ };
678
+ const findNewPaneId = (before, after) => {
679
+ const beforeSet = new Set(before);
680
+ return after.find((id) => !beforeSet.has(id));
681
+ };
682
+ const replaceTarget = (command, realTarget) => {
683
+ const next = [...command];
684
+ const targetIndex = next.findIndex((value, index) => value === "-t" && index + 1 < next.length);
685
+ if (targetIndex >= 0) {
686
+ next[targetIndex + 1] = realTarget;
687
+ return next;
688
+ }
689
+ if (next.length > 0) next[next.length - 1] = realTarget;
690
+ return next;
691
+ };
692
+ const normalizePaneId = (raw) => {
693
+ const trimmed = raw.trim();
694
+ return trimmed.length === 0 ? "%0" : trimmed;
695
+ };
696
+ const registerPane = (paneMap, virtualId, realId) => {
697
+ paneMap.set(virtualId, realId);
698
+ };
699
+ const resolvePaneId = (paneMap, virtualId) => {
700
+ const direct = paneMap.get(virtualId);
701
+ if (typeof direct === "string" && direct.length > 0) return direct;
702
+ let ancestor = virtualId;
703
+ while (ancestor.includes(".")) {
704
+ ancestor = ancestor.slice(0, ancestor.lastIndexOf("."));
705
+ const candidate = paneMap.get(ancestor);
706
+ if (typeof candidate === "string" && candidate.length > 0) {
707
+ paneMap.set(virtualId, candidate);
708
+ return candidate;
709
+ }
710
+ }
711
+ for (const [key, value] of paneMap.entries()) if (key.startsWith(`${virtualId}.`)) {
712
+ if (typeof value === "string" && value.length > 0) {
713
+ paneMap.set(virtualId, value);
714
+ return value;
715
+ }
716
+ }
717
+ };
718
+ const ensureNonEmpty = (value, buildError) => {
719
+ if (value === void 0 || value.length === 0) return buildError();
720
+ return value;
721
+ };
722
+ const raiseExecutionError = (code, error) => {
723
+ throw createFunctionalError("execution", {
724
+ code,
725
+ message: error.message,
726
+ path: error.path,
727
+ details: error.details
728
+ });
729
+ };
730
+
731
+ //#endregion
732
+ //#region src/core/diagnostics.ts
733
+ const severityRank = {
734
+ high: 3,
735
+ medium: 2,
736
+ low: 1
737
+ };
738
+ const createAccumulator = () => {
739
+ const findings = [];
740
+ const nextSteps = /* @__PURE__ */ new Set();
741
+ const backlog = /* @__PURE__ */ new Map();
742
+ return {
743
+ findings,
744
+ nextSteps,
745
+ backlog,
746
+ add: ({ path: path$1, severity, description, nextStep }) => {
747
+ findings.push({
748
+ path: path$1,
749
+ severity,
750
+ description
751
+ });
752
+ if (typeof nextStep === "string" && nextStep.length > 0) nextSteps.add(nextStep);
753
+ const existing = backlog.get(path$1);
754
+ const existingActions = new Set(existing?.actions ?? []);
755
+ existingActions.add(description);
756
+ if (typeof nextStep === "string" && nextStep.length > 0) existingActions.add(nextStep);
757
+ const mergedSeverity = existing !== void 0 ? maxSeverity(existing.severity, severity) : severity;
758
+ const summary = existing !== void 0 && severityRank[existing.severity] >= severityRank[severity] ? existing.summary : description;
759
+ backlog.set(path$1, {
760
+ id: path$1,
761
+ severity: mergedSeverity,
762
+ summary,
763
+ actions: Array.from(existingActions)
764
+ });
765
+ }
766
+ };
767
+ };
768
+ const runDiagnostics = (input) => {
769
+ const accumulator = createAccumulator();
770
+ const knownIssues = input.knownIssues ?? [];
771
+ let parsedPreset;
772
+ try {
773
+ parsedPreset = parse(input.presetDocument);
774
+ } catch (error) {
775
+ accumulator.add({
776
+ path: "presetDocument",
777
+ severity: "high",
778
+ description: `プリセットYAMLの解析に失敗しました: ${error.message}`,
779
+ nextStep: "プリセットYAMLを構文チェックし、Functional Coreリライト前に整合性を確保する"
780
+ });
781
+ return {
782
+ findings: sortBySeverity(accumulator.findings),
783
+ nextSteps: Array.from(accumulator.nextSteps),
784
+ backlog: sortBacklog(accumulator.backlog)
785
+ };
786
+ }
787
+ if (parsedPreset === null || typeof parsedPreset !== "object") accumulator.add({
788
+ path: "preset",
789
+ severity: "high",
790
+ description: "プリセット定義がオブジェクト形式ではありません",
791
+ nextStep: "プリセットYAMLをオブジェクト構造に整形し整合性を確保する"
792
+ });
793
+ else {
794
+ const presetObject = parsedPreset;
795
+ checkFocusDuplications(presetObject, accumulator);
796
+ collectLowPrioritySignals(presetObject, accumulator);
797
+ }
798
+ knownIssues.forEach((issue, index) => {
799
+ const trimmed = issue.trim();
800
+ if (trimmed.length === 0) return;
801
+ accumulator.add({
802
+ path: `codebase.knownIssues[${index}]`,
803
+ severity: "medium",
804
+ description: trimmed,
805
+ nextStep: `tmux依存や副作用をFunctional Core境界で切り離す: ${trimmed}`
806
+ });
807
+ });
808
+ return {
809
+ findings: sortBySeverity(accumulator.findings),
810
+ nextSteps: Array.from(accumulator.nextSteps),
811
+ backlog: sortBacklog(accumulator.backlog)
812
+ };
813
+ };
814
+ const checkFocusDuplications = (preset, accumulator) => {
815
+ const layout = preset.layout;
816
+ if (layout === void 0 || layout === null) return;
817
+ if (countFocusFlags(layout) > 1) accumulator.add({
818
+ path: "preset.layout",
819
+ severity: "high",
820
+ description: "複数のペインでfocus: trueが指定されています",
821
+ nextStep: "focusは単一ペインに限定しPlan生成時に一貫するようFunctional Coreで制御する"
822
+ });
823
+ };
824
+ const collectLowPrioritySignals = (preset, accumulator) => {
825
+ const layout = preset.layout;
826
+ if (layout === void 0 || layout === null) {
827
+ accumulator.add({
828
+ path: "preset.layout",
829
+ severity: "low",
830
+ description: "layout定義が存在しません(単一ペイン運用が前提)",
831
+ nextStep: "単一ペイン前提でもPlan出力に影響しないことをFunctional Coreで確認する"
832
+ });
833
+ return;
834
+ }
835
+ if (!Array.isArray(layout.panes)) accumulator.add({
836
+ path: "preset.layout.panes",
837
+ severity: "low",
838
+ description: "panes配列が存在しないか配列ではありません",
839
+ nextStep: "レイアウト定義の構造を正規化し、Plan生成の入力契約を明示する"
840
+ });
841
+ };
842
+ const countFocusFlags = (node) => {
843
+ if (Array.isArray(node)) return node.reduce((sum, child) => sum + countFocusFlags(child), 0);
844
+ if (node === null || typeof node !== "object") return 0;
845
+ const record = node;
846
+ const selfFocus = record.focus === true ? 1 : 0;
847
+ const childFocus = Array.isArray(record.panes) ? record.panes.reduce((sum, child) => sum + countFocusFlags(child), 0) : 0;
848
+ return selfFocus + childFocus;
849
+ };
850
+ const sortBySeverity = (findings) => {
851
+ return [...findings].sort((a, b) => severityRank[b.severity] - severityRank[a.severity]);
852
+ };
853
+ const sortBacklog = (backlog) => {
854
+ return [...backlog.values()].sort((a, b) => severityRank[b.severity] - severityRank[a.severity]);
855
+ };
856
+ const maxSeverity = (left, right) => {
857
+ return severityRank[left] >= severityRank[right] ? left : right;
858
+ };
859
+
860
+ //#endregion
861
+ //#region src/core/compile.ts
862
+ const compilePreset = ({ document, source }) => {
863
+ let parsed;
864
+ try {
865
+ parsed = parse(document);
866
+ } catch (error) {
867
+ throw compileError("PRESET_PARSE_ERROR", {
868
+ source,
869
+ message: `YAMLの解析に失敗しました: ${error.message}`,
870
+ details: { reason: error instanceof Error ? error.message : String(error) }
871
+ });
872
+ }
873
+ if (!isRecord(parsed)) throw compileError("PRESET_INVALID_DOCUMENT", {
874
+ source,
875
+ message: "プリセット定義がオブジェクトではありません",
876
+ path: "preset"
877
+ });
878
+ const name = typeof parsed.name === "string" && parsed.name.trim().length > 0 ? parsed.name : "Unnamed preset";
879
+ const layout = parseLayoutNode(parsed.layout, {
880
+ source,
881
+ path: "preset.layout"
882
+ });
883
+ return { preset: {
884
+ name,
885
+ version: "legacy",
886
+ command: typeof parsed.command === "string" ? parsed.command : void 0,
887
+ layout: layout ?? void 0,
888
+ metadata: { source }
889
+ } };
890
+ };
891
+ const parseLayoutNode = (node, context) => {
892
+ if (node === void 0 || node === null) return null;
893
+ if (!isRecord(node)) throw compileError("LAYOUT_INVALID_NODE", {
894
+ source: context.source,
895
+ message: "レイアウトノードの形式が不正です",
896
+ path: context.path,
897
+ details: { node }
898
+ });
899
+ if (typeof node.type === "string" && Array.isArray(node.panes)) return parseSplitPane(node, context);
900
+ if (typeof node.name === "string") return parseTerminalPane(node);
901
+ throw compileError("LAYOUT_INVALID_NODE", {
902
+ source: context.source,
903
+ message: "レイアウトノードの形式が不正です",
904
+ path: context.path,
905
+ details: { node }
906
+ });
907
+ };
908
+ const parseSplitPane = (node, context) => {
909
+ const orientation = node.type;
910
+ if (orientation !== "horizontal" && orientation !== "vertical") throw compileError("LAYOUT_INVALID_ORIENTATION", {
911
+ source: context.source,
912
+ message: "layout.type は horizontal か vertical である必要があります",
913
+ path: `${context.path}.type`,
914
+ details: { type: orientation }
915
+ });
916
+ if (!Array.isArray(node.panes) || node.panes.length === 0) throw compileError("LAYOUT_PANES_MISSING", {
917
+ source: context.source,
918
+ message: "panes 配列が存在しません",
919
+ path: `${context.path}.panes`
920
+ });
921
+ if (!Array.isArray(node.ratio) || node.ratio.length === 0) throw compileError("LAYOUT_RATIO_MISSING", {
922
+ source: context.source,
923
+ message: "ratio 配列が存在しません",
924
+ path: `${context.path}.ratio`
925
+ });
926
+ if (node.ratio.length !== node.panes.length) throw compileError("LAYOUT_RATIO_MISMATCH", {
927
+ source: context.source,
928
+ message: "ratio 配列と panes 配列の長さが一致しません",
929
+ path: context.path,
930
+ details: {
931
+ ratioLength: node.ratio.length,
932
+ panesLength: node.panes.length
933
+ }
934
+ });
935
+ const ratio = node.ratio.map((value, index) => {
936
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw compileError("RATIO_INVALID_VALUE", {
937
+ source: context.source,
938
+ message: "ratio の値が正の数値ではありません",
939
+ path: `${context.path}.ratio[${index}]`,
940
+ details: { value }
941
+ });
942
+ return value;
943
+ });
944
+ const panes = node.panes.map((child, index) => parseLayoutNode(child, {
945
+ source: context.source,
946
+ path: `${context.path}.panes[${index}]`
947
+ }));
948
+ return {
949
+ kind: "split",
950
+ orientation,
951
+ ratio,
952
+ panes: panes.filter((pane) => pane !== null)
953
+ };
954
+ };
955
+ const parseTerminalPane = (node) => {
956
+ const name = typeof node.name === "string" ? node.name : "";
957
+ const command = typeof node.command === "string" ? node.command : void 0;
958
+ const cwd = typeof node.cwd === "string" ? node.cwd : void 0;
959
+ const focus = node.focus === true ? true : void 0;
960
+ const env = normalizeEnv(node.env);
961
+ const options = collectOptions(node, new Set([
962
+ "name",
963
+ "command",
964
+ "cwd",
965
+ "env",
966
+ "focus",
967
+ "options",
968
+ "title",
969
+ "delay"
970
+ ]));
971
+ return {
972
+ kind: "terminal",
973
+ name,
974
+ command,
975
+ cwd,
976
+ env,
977
+ focus,
978
+ options
979
+ };
980
+ };
981
+ const normalizeEnv = (env) => {
982
+ if (!isRecord(env)) return;
983
+ const entries = Object.entries(env).reduce((accumulator, [key, value]) => {
984
+ if (typeof value === "string") accumulator[key] = value;
985
+ return accumulator;
986
+ }, {});
987
+ return Object.keys(entries).length > 0 ? entries : void 0;
988
+ };
989
+ const collectOptions = (node, excludedKeys) => {
990
+ const optionsEntries = Object.entries(node).filter(([key]) => !excludedKeys.has(key));
991
+ if (optionsEntries.length === 0) return;
992
+ return optionsEntries.reduce((accumulator, [key, value]) => {
993
+ accumulator[key] = value;
994
+ return accumulator;
995
+ }, {});
996
+ };
997
+ const isRecord = (value) => {
998
+ return typeof value === "object" && value !== null;
999
+ };
1000
+ const compileError = (code, error) => {
1001
+ return createFunctionalError("compile", {
1002
+ code,
1003
+ message: error.message,
1004
+ source: error.source,
1005
+ path: error.path,
1006
+ details: error.details
1007
+ });
1008
+ };
1009
+
1010
+ //#endregion
1011
+ //#region src/core/planner.ts
1012
+ const createLayoutPlan = ({ preset }) => {
1013
+ if (!preset.layout) {
1014
+ const terminal = createTerminalNode({
1015
+ id: "root",
1016
+ terminal: {
1017
+ kind: "terminal",
1018
+ name: preset.name,
1019
+ command: preset.command
1020
+ },
1021
+ focusOverride: true
1022
+ });
1023
+ return { plan: {
1024
+ root: terminal,
1025
+ focusPaneId: terminal.id
1026
+ } };
1027
+ }
1028
+ const { node, focusPaneIds, terminalPaneIds } = buildLayoutNode(preset.layout, {
1029
+ parentId: "root",
1030
+ path: "preset.layout",
1031
+ source: preset.metadata.source
1032
+ });
1033
+ if (focusPaneIds.length > 1) throw planError("FOCUS_CONFLICT", {
1034
+ message: "複数のペインでfocusが指定されています",
1035
+ path: "preset.layout",
1036
+ source: preset.metadata.source,
1037
+ details: { focusPaneIds }
1038
+ });
1039
+ if (terminalPaneIds.length === 0) throw planError("NO_TERMINAL_PANES", {
1040
+ message: "ターミナルペインが存在しません",
1041
+ path: "preset.layout",
1042
+ source: preset.metadata.source
1043
+ });
1044
+ const focusPaneId = focusPaneIds[0] ?? terminalPaneIds[0];
1045
+ return { plan: {
1046
+ root: ensureFocus(node, focusPaneId),
1047
+ focusPaneId
1048
+ } };
1049
+ };
1050
+ const buildLayoutNode = (node, context) => {
1051
+ if (node.kind === "split") return buildSplitNode(node, context);
1052
+ return {
1053
+ node: createTerminalNode({
1054
+ id: context.parentId,
1055
+ terminal: node
1056
+ }),
1057
+ focusPaneIds: node.focus === true ? [context.parentId] : [],
1058
+ terminalPaneIds: [context.parentId]
1059
+ };
1060
+ };
1061
+ const buildSplitNode = (node, context) => {
1062
+ const ratio = normalizeRatio(node.ratio, context);
1063
+ const panes = [];
1064
+ const focusPaneIds = [];
1065
+ const terminalPaneIds = [];
1066
+ for (let index = 0; index < node.panes.length; index += 1) {
1067
+ const childContext = {
1068
+ parentId: `${context.parentId}.${index}`,
1069
+ path: `${context.path}.panes[${index}]`,
1070
+ source: context.source
1071
+ };
1072
+ const childResult = buildLayoutNode(node.panes[index], childContext);
1073
+ panes.push(childResult.node);
1074
+ focusPaneIds.push(...childResult.focusPaneIds);
1075
+ terminalPaneIds.push(...childResult.terminalPaneIds);
1076
+ }
1077
+ return {
1078
+ node: {
1079
+ kind: "split",
1080
+ id: context.parentId,
1081
+ orientation: node.orientation,
1082
+ ratio,
1083
+ panes
1084
+ },
1085
+ focusPaneIds,
1086
+ terminalPaneIds
1087
+ };
1088
+ };
1089
+ const createTerminalNode = ({ id, terminal, focusOverride }) => {
1090
+ return {
1091
+ kind: "terminal",
1092
+ id,
1093
+ name: terminal.name,
1094
+ command: terminal.command,
1095
+ cwd: terminal.cwd,
1096
+ env: terminal.env,
1097
+ options: terminal.options,
1098
+ focus: focusOverride === true ? true : terminal.focus === true
1099
+ };
1100
+ };
1101
+ const ensureFocus = (node, focusPaneId) => {
1102
+ if (node.kind === "terminal") return {
1103
+ ...node,
1104
+ focus: node.id === focusPaneId
1105
+ };
1106
+ return {
1107
+ ...node,
1108
+ panes: node.panes.map((pane) => ensureFocus(pane, focusPaneId))
1109
+ };
1110
+ };
1111
+ const normalizeRatio = (ratio, context) => {
1112
+ const total = ratio.reduce((sum, value, index) => {
1113
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw planError("RATIO_INVALID_VALUE", {
1114
+ message: "ratio の値が非負の数値ではありません",
1115
+ path: `${context.path}.ratio[${index}]`,
1116
+ source: context.source,
1117
+ details: { value }
1118
+ });
1119
+ return sum + value;
1120
+ }, 0);
1121
+ if (total === 0) return ratio.map(() => 1 / ratio.length);
1122
+ return ratio.map((value) => value / total);
1123
+ };
1124
+ const planError = (code, error) => {
1125
+ return createFunctionalError("plan", {
1126
+ code,
1127
+ message: error.message,
1128
+ source: error.source,
1129
+ path: error.path,
1130
+ details: error.details
1131
+ });
1132
+ };
1133
+
1134
+ //#endregion
1135
+ //#region src/core/emitter.ts
1136
+ const emitPlan = ({ plan }) => {
1137
+ const steps = [];
1138
+ collectSplitSteps(plan.root, steps);
1139
+ steps.push({
1140
+ id: `${plan.focusPaneId}:focus`,
1141
+ kind: "focus",
1142
+ command: [
1143
+ "select-pane",
1144
+ "-t",
1145
+ plan.focusPaneId
1146
+ ],
1147
+ summary: `select pane ${plan.focusPaneId}`,
1148
+ targetPaneId: plan.focusPaneId
1149
+ });
1150
+ const hash = createPlanHash(plan, steps);
1151
+ const initialPaneId = determineInitialPaneId(plan.root);
1152
+ const terminals = collectTerminals(plan.root);
1153
+ return {
1154
+ steps,
1155
+ summary: {
1156
+ stepsCount: steps.length,
1157
+ focusPaneId: plan.focusPaneId,
1158
+ initialPaneId
1159
+ },
1160
+ terminals,
1161
+ hash
1162
+ };
1163
+ };
1164
+ const collectSplitSteps = (node, steps) => {
1165
+ if (node.kind === "terminal") return;
1166
+ appendSplitSteps(node, steps);
1167
+ node.panes.forEach((pane) => collectSplitSteps(pane, steps));
1168
+ };
1169
+ const appendSplitSteps = (node, steps) => {
1170
+ const directionFlag = node.orientation === "horizontal" ? "-h" : "-v";
1171
+ for (let index = 1; index < node.panes.length; index += 1) {
1172
+ const remainingIncludingTarget = node.ratio.slice(index - 1).reduce((sum, value) => sum + value, 0);
1173
+ const remainingAfterTarget = node.ratio.slice(index).reduce((sum, value) => sum + value, 0);
1174
+ const desiredPercentage = remainingIncludingTarget === 0 ? 0 : remainingAfterTarget / remainingIncludingTarget * 100;
1175
+ const percentage = Math.max(1, Math.round(desiredPercentage));
1176
+ const targetPaneId = node.panes[index - 1]?.id ?? node.id;
1177
+ const createdPaneId = node.panes[index]?.id;
1178
+ steps.push({
1179
+ id: `${node.id}:split:${index}`,
1180
+ kind: "split",
1181
+ command: [
1182
+ "split-window",
1183
+ directionFlag,
1184
+ "-t",
1185
+ targetPaneId,
1186
+ "-p",
1187
+ String(Math.min(percentage, 99))
1188
+ ],
1189
+ summary: `split ${targetPaneId} (${directionFlag})`,
1190
+ targetPaneId,
1191
+ createdPaneId
1192
+ });
1193
+ }
1194
+ };
1195
+ const collectTerminals = (node) => {
1196
+ if (node.kind === "terminal") return [{
1197
+ virtualPaneId: node.id,
1198
+ command: node.command,
1199
+ cwd: node.cwd,
1200
+ env: node.env,
1201
+ focus: node.focus,
1202
+ name: node.name
1203
+ }];
1204
+ return node.panes.flatMap((pane) => collectTerminals(pane));
1205
+ };
1206
+ const determineInitialPaneId = (node) => {
1207
+ if (node.kind === "terminal") return node.id;
1208
+ let current = node;
1209
+ while (current.kind === "split") current = current.panes[0];
1210
+ return current.id;
1211
+ };
1212
+ const createPlanHash = (plan, steps) => {
1213
+ const digest = createHash("sha256");
1214
+ const normalized = {
1215
+ focusPaneId: plan.focusPaneId,
1216
+ root: plan.root,
1217
+ steps
1218
+ };
1219
+ digest.update(JSON.stringify(normalized));
1220
+ return digest.digest("hex");
1221
+ };
1222
+
1223
+ //#endregion
1224
+ //#region src/cli.ts
1225
+ const KNOWN_ISSUES = [
1226
+ "LayoutEngineがtmux依存とI/Oを同一クラスで扱っている",
1227
+ "dry-run実行と本番適用でPlan構造が共有されていない",
1228
+ "Loggerが境界層とFunctional Coreの責務を混在させている"
1229
+ ];
1230
+ const formatSeverityTag = (severity) => {
1231
+ switch (severity) {
1232
+ case "high": return chalk.red("[HIGH]");
1233
+ case "medium": return chalk.yellow("[MEDIUM]");
1234
+ case "low":
1235
+ default: return chalk.blue("[LOW]");
1236
+ }
1237
+ };
1238
+ const createCli = (options = {}) => {
1239
+ const presetManager = options.presetManager ?? createPresetManager();
1240
+ const createCommandExecutor = options.createCommandExecutor ?? ((opts) => {
1241
+ if (opts.dryRun) return createDryRunExecutor({ verbose: opts.verbose });
1242
+ return createRealExecutor({ verbose: opts.verbose });
1243
+ });
1244
+ const functionalCore = options.functionalCore ?? {
1245
+ compilePreset,
1246
+ createLayoutPlan,
1247
+ emitPlan
1248
+ };
1249
+ const program = new Command();
1250
+ const { version } = createRequire(import.meta.url)("../package.json");
1251
+ let logger = createLogger();
1252
+ const renderDiagnosticsReport = (report) => {
1253
+ console.log(chalk.bold("\nFunctional Core Diagnostics\n"));
1254
+ if (report.backlog.length > 0) {
1255
+ console.log(chalk.bold("改善バックログ"));
1256
+ report.backlog.forEach((item, index) => {
1257
+ const prefix = `${index + 1}. ${formatSeverityTag(item.severity)}`;
1258
+ console.log(`${prefix} ${item.summary}`);
1259
+ item.actions.forEach((action) => {
1260
+ console.log(` - ${action}`);
1261
+ });
1262
+ });
1263
+ console.log("");
1264
+ }
1265
+ if (report.findings.length > 0) {
1266
+ console.log(chalk.bold("診断結果"));
1267
+ report.findings.forEach((finding) => {
1268
+ console.log(`${formatSeverityTag(finding.severity)} ${finding.path} :: ${finding.description}`);
1269
+ });
1270
+ console.log("");
1271
+ }
1272
+ if (report.nextSteps.length > 0) {
1273
+ console.log(chalk.bold("次のアクション"));
1274
+ report.nextSteps.forEach((step) => {
1275
+ console.log(` - ${step}`);
1276
+ });
1277
+ console.log("");
1278
+ }
1279
+ };
1280
+ const renderDryRun = (emission) => {
1281
+ console.log(chalk.bold("\nPlanned tmux steps (dry-run)"));
1282
+ emission.steps.forEach((step, index) => {
1283
+ const commandString = step.command.join(" ");
1284
+ console.log(` ${index + 1}. ${step.summary}: tmux ${commandString}`);
1285
+ });
1286
+ };
1287
+ const buildPresetDocument = (preset, presetName) => {
1288
+ const document = {
1289
+ name: preset.name ?? presetName ?? "vde-layout",
1290
+ command: preset.command,
1291
+ layout: preset.layout
1292
+ };
1293
+ if (typeof preset.command !== "string" || preset.command.length === 0) delete document.command;
1294
+ if (preset.layout === void 0 || preset.layout === null) delete document.layout;
1295
+ return stringify(document);
1296
+ };
1297
+ const buildPresetSource = (presetName) => {
1298
+ return typeof presetName === "string" && presetName.length > 0 ? `preset://${presetName}` : "preset://default";
1299
+ };
1300
+ const handleFunctionalError = (error) => {
1301
+ const header = [`[${error.kind}]`, `[${error.code}]`];
1302
+ if (typeof error.path === "string" && error.path.length > 0) header.push(`[${error.path}]`);
1303
+ const lines = [`${header.join(" ")} ${error.message}`.trim()];
1304
+ if (typeof error.source === "string" && error.source.length > 0) lines.push(`source: ${error.source}`);
1305
+ const commandDetail = error.details?.command;
1306
+ if (Array.isArray(commandDetail)) {
1307
+ const tmuxCommand = commandDetail.filter((segment) => typeof segment === "string");
1308
+ if (tmuxCommand.length > 0) lines.push(`command: tmux ${tmuxCommand.join(" ")}`);
1309
+ } else if (typeof commandDetail === "string" && commandDetail.length > 0) lines.push(`command: ${commandDetail}`);
1310
+ const stderrDetail = error.details?.stderr;
1311
+ if (typeof stderrDetail === "string" && stderrDetail.length > 0) lines.push(`stderr: ${stderrDetail}`);
1312
+ else if (stderrDetail !== void 0) lines.push(`stderr: ${String(stderrDetail)}`);
1313
+ logger.error(lines.join("\n"));
1314
+ process.exit(1);
1315
+ };
1316
+ const handleError = (error) => {
1317
+ if (error instanceof Error) logger.error(error.message, error);
1318
+ else logger.error("An unexpected error occurred");
1319
+ process.exit(1);
1320
+ };
1321
+ const handlePipelineFailure = (error) => {
1322
+ if (isFunctionalCoreError(error)) return handleFunctionalError(error);
1323
+ return handleError(error);
1324
+ };
1325
+ const listPresets = async () => {
1326
+ try {
1327
+ await presetManager.loadConfig();
1328
+ const presets = presetManager.listPresets();
1329
+ if (presets.length === 0) {
1330
+ logger.warn("No presets defined");
1331
+ process.exit(0);
1332
+ }
1333
+ console.log(chalk.bold("Available presets:\n"));
1334
+ const maxKeyLength = Math.max(...presets.map((p) => p.key.length));
1335
+ presets.forEach((preset) => {
1336
+ const paddedKey = preset.key.padEnd(maxKeyLength + 2);
1337
+ const description = preset.description ?? "";
1338
+ console.log(` ${chalk.cyan(paddedKey)} ${description}`);
1339
+ });
1340
+ process.exit(0);
1341
+ } catch (error) {
1342
+ return handleError(error);
1343
+ }
1344
+ };
1345
+ const diagnosePreset = async (presetName) => {
1346
+ try {
1347
+ await presetManager.loadConfig();
1348
+ const preset = typeof presetName === "string" && presetName.length > 0 ? presetManager.getPreset(presetName) : presetManager.getDefaultPreset();
1349
+ const presetDocument = stringify(preset ?? {});
1350
+ const report = runDiagnostics({
1351
+ presetDocument,
1352
+ knownIssues: KNOWN_ISSUES
1353
+ });
1354
+ renderDiagnosticsReport(report);
1355
+ process.exit(0);
1356
+ } catch (error) {
1357
+ return handleError(error);
1358
+ }
1359
+ };
1360
+ const executePreset = async (presetName, options$1) => {
1361
+ try {
1362
+ await presetManager.loadConfig();
1363
+ const preset = typeof presetName === "string" && presetName.length > 0 ? presetManager.getPreset(presetName) : presetManager.getDefaultPreset();
1364
+ const tmuxEnv = process.env.TMUX;
1365
+ if (!(typeof tmuxEnv === "string" && tmuxEnv.length > 0) && options$1.dryRun !== true) throw new Error("Must be run inside a tmux session");
1366
+ const executor = createCommandExecutor({
1367
+ verbose: options$1.verbose,
1368
+ dryRun: options$1.dryRun
1369
+ });
1370
+ if (options$1.dryRun === true) console.log("[DRY RUN] No actual commands will be executed");
1371
+ let compileResult;
1372
+ let planResult;
1373
+ let emission;
1374
+ try {
1375
+ compileResult = functionalCore.compilePreset({
1376
+ document: buildPresetDocument(preset, presetName),
1377
+ source: buildPresetSource(presetName)
1378
+ });
1379
+ planResult = functionalCore.createLayoutPlan({ preset: compileResult.preset });
1380
+ emission = functionalCore.emitPlan({ plan: planResult.plan });
1381
+ } catch (error) {
1382
+ return handlePipelineFailure(error);
1383
+ }
1384
+ if (options$1.dryRun === true) renderDryRun(emission);
1385
+ else try {
1386
+ const executionResult = await executePlan({
1387
+ emission,
1388
+ executor,
1389
+ windowName: preset.name ?? presetName ?? "vde-layout"
1390
+ });
1391
+ logger.info(`Executed ${executionResult.executedSteps} tmux steps`);
1392
+ } catch (error) {
1393
+ return handlePipelineFailure(error);
1394
+ }
1395
+ logger.success(`✓ Applied preset "${preset.name}"`);
1396
+ process.exit(0);
1397
+ } catch (error) {
1398
+ return handleError(error);
1399
+ }
1400
+ };
1401
+ const setupProgram = () => {
1402
+ program.name("vde-layout").description("VDE (Vibrant Development Environment) Layout Manager - tmux pane layout management tool").version(version, "-V, --version", "Show version").helpOption("-h, --help", "Show help");
1403
+ program.option("-v, --verbose", "Show detailed logs", false);
1404
+ program.option("--dry-run", "Display commands without executing", false);
1405
+ program.option("--config <path>", "Path to configuration file");
1406
+ program.command("list").description("List available presets").action(async () => {
1407
+ await listPresets();
1408
+ });
1409
+ program.command("diagnose").description("Functional Coreリライトに向けた診断レポートを表示する").argument("[preset]", "Preset name (defaults to \"default\" preset when omitted)").action(async (presetName) => {
1410
+ await diagnosePreset(presetName);
1411
+ });
1412
+ program.argument("[preset]", "Preset name (defaults to \"default\" preset when omitted)").action(async (presetName) => {
1413
+ const opts = program.opts();
1414
+ await executePreset(presetName, {
1415
+ verbose: opts.verbose === true,
1416
+ dryRun: opts.dryRun === true
1417
+ });
1418
+ });
1419
+ };
1420
+ setupProgram();
1421
+ const run = async (args = process.argv.slice(2)) => {
1422
+ const requestedVersion = args.includes("--version") || args.includes("-V");
1423
+ const requestedHelp = args.includes("--help") || args.includes("-h");
1424
+ try {
1425
+ await program.parseAsync(args, { from: "user" });
1426
+ const opts = program.opts();
1427
+ if (requestedVersion || requestedHelp) return;
1428
+ if (opts.verbose === true) logger = createLogger({ level: LogLevel.INFO });
1429
+ else logger = createLogger();
1430
+ if (typeof opts.config === "string" && opts.config.length > 0 && typeof presetManager.setConfigPath === "function") presetManager.setConfigPath(opts.config);
1431
+ } catch (error) {
1432
+ if (error instanceof Error && error.message.includes("Process exited")) throw error;
1433
+ handleError(error);
1434
+ }
1435
+ };
1436
+ return { run };
1437
+ };
1438
+
1439
+ //#endregion
1440
+ //#region src/index.ts
1441
+ /**
1442
+ * Main entry point
1443
+ * Launches the CLI application
1444
+ */
1445
+ const main = async () => {
1446
+ const cli = createCli();
1447
+ try {
1448
+ await cli.run(process.argv.slice(2));
1449
+ } catch (error) {
1450
+ if (error instanceof Error) {
1451
+ console.error("Error:", error.message);
1452
+ if (process.env.VDE_DEBUG === "true") console.error(error.stack);
1453
+ } else console.error("An unexpected error occurred:", String(error));
1454
+ process.exit(1);
1455
+ }
1456
+ };
1457
+ main();
1458
+
1459
+ //#endregion
1460
+ export { };
24
1461
  //# sourceMappingURL=index.js.map