UNPKG

worktree-tool

Version:

A command-line tool for managing Git worktrees with integrated tmux/shell session management

245 lines 8.82 kB
import { promises as fs } from "fs"; import * as path from "path"; import { getErrorMessage } from "../utils/error-handler.js"; import { ConfigError, FileSystemError } from "../utils/errors.js"; import { findProjectRoot } from "../utils/find-root.js"; import { validateCommand } from "../utils/validation.js"; const CONFIG_FILENAME = ".worktree-config.json"; const CONFIG_VERSION = "1.0.0"; /** * Load the worktree configuration from the current directory * @returns The configuration or null if not found */ export async function loadConfig() { try { const projectRoot = await findProjectRoot(); if (!projectRoot) { return null; } const configPath = path.join(projectRoot, CONFIG_FILENAME); const content = await fs.readFile(configPath, "utf-8"); const data = JSON.parse(content); if (!validateConfig(data)) { throw new ConfigError("Invalid configuration format"); } // Validate commands if present if (data.commands) { for (const [name, command] of Object.entries(data.commands)) { try { // Extract the actual command string for validation const commandStr = typeof command === "string" ? command : command.command; validateCommand(name, commandStr); } catch (error) { throw new ConfigError(error instanceof Error ? error.message : "Invalid command"); } } } return data; } catch (error) { if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { // Config file doesn't exist, which is okay return null; } if (error instanceof ConfigError) { throw error; } if (error instanceof SyntaxError) { throw new ConfigError("Invalid JSON in configuration file", error); } throw new FileSystemError(`Failed to read configuration: ${getErrorMessage(error)}`); } } /** * Save the worktree configuration to the current directory * @param config The configuration to save */ export async function saveConfig(config) { try { // During init, there's no existing config, so save in current directory // Otherwise, find the project root const projectRoot = await findProjectRoot() ?? process.cwd(); const configPath = path.join(projectRoot, CONFIG_FILENAME); const content = JSON.stringify(config, null, 2); await fs.writeFile(configPath, content, "utf-8"); } catch (error) { if (error instanceof ConfigError) { throw error; } throw new FileSystemError(`Failed to save configuration: ${getErrorMessage(error)}`); } } /** * Get the default configuration for a project * @param projectName The name of the project * @returns Default configuration */ export function getDefaultConfig(projectName) { return { version: CONFIG_VERSION, projectName, mainBranch: "main", baseDir: ".worktrees", tmux: true, commands: {}, }; } /** * Validate port range format * @param range The port range string to validate * @returns True if valid, false otherwise */ function validatePortRange(range) { const regex = /^(\d+)-(\d+)$/; const match = regex.exec(range); if (!match?.[1] || !match[2]) { return false; } const start = parseInt(match[1]); const end = parseInt(match[2]); return start < end && start >= 1024 && end <= 65535; } /** * Validate that an unknown object is a valid WorktreeConfig * @param config The object to validate * @returns True if valid, false otherwise */ export function validateConfig(config) { if (!config || typeof config !== "object") { return false; } const obj = config; // Check required fields if (typeof obj.version !== "string") { return false; } if (typeof obj.projectName !== "string" || obj.projectName.trim() === "") { return false; } if (typeof obj.mainBranch !== "string" || obj.mainBranch.trim() === "") { return false; } if (typeof obj.baseDir !== "string" || obj.baseDir.trim() === "") { return false; } if (typeof obj.tmux !== "boolean") { return false; } // Validate optional autoSort field if (obj.autoSort !== undefined && typeof obj.autoSort !== "boolean") { return false; } // Validate optional availablePorts field if (obj.availablePorts !== undefined) { if (typeof obj.availablePorts !== "string" || !validatePortRange(obj.availablePorts)) { return false; } } // Validate optional autoRemove field if (obj.autoRemove !== undefined && typeof obj.autoRemove !== "boolean") { return false; } // Validate optional commands field if (obj.commands !== undefined) { if (typeof obj.commands !== "object" || obj.commands === null || Array.isArray(obj.commands)) { return false; } // Check that all values are valid CommandConfig (string or object) for (const value of Object.values(obj.commands)) { if (typeof value === "string") { // String format is valid continue; } else if (typeof value === "object" && value !== null && !Array.isArray(value)) { // Object format - validate structure const cmdObj = value; // Command property is required and must be string if (typeof cmdObj.command !== "string") { return false; } // Mode property is optional but must be valid if present if (cmdObj.mode !== undefined) { const validModes = ["window", "inline", "background", "exit"]; if (!validModes.includes(cmdObj.mode)) { return false; } } // autoRun property is optional but must be boolean if present if (cmdObj.autoRun !== undefined && typeof cmdObj.autoRun !== "boolean") { return false; } // numPorts property is optional but must be non-negative number if present if (cmdObj.numPorts !== undefined && (typeof cmdObj.numPorts !== "number" || cmdObj.numPorts < 0)) { return false; } } else { // Invalid format return false; } } } // Validate optional autoRemove field if (obj.autoRemove !== undefined && typeof obj.autoRemove !== "boolean") { return false; } return true; } /** * Check if a configuration already exists in the current directory */ export async function configExists() { try { const projectRoot = await findProjectRoot(); if (!projectRoot) { return false; } const configPath = path.join(projectRoot, CONFIG_FILENAME); await fs.access(configPath); return true; } catch { return false; } } /** * Update the .gitignore file to ignore the worktrees directory * @param baseDir The base directory for worktrees */ export async function updateGitignore(baseDir) { try { const projectRoot = await findProjectRoot() ?? process.cwd(); const gitignorePath = path.join(projectRoot, ".gitignore"); let content = ""; try { content = await fs.readFile(gitignorePath, "utf-8"); } catch (error) { if (error && typeof error === "object" && "code" in error && error.code !== "ENOENT") { throw error; } // File doesn't exist, we'll create it } // Check if the baseDir is already in .gitignore const lines = content.split("\n"); const baseDirPattern = `${baseDir}/`; if (!lines.some((line) => line.trim() === baseDirPattern || line.trim() === baseDir)) { // Add baseDir to .gitignore if (content && !content.endsWith("\n")) { content += "\n"; } // Add a comment if this is the first wtt entry if (!content.includes("wtt")) { content += "\n# wtt worktrees\n"; } content += `${baseDirPattern}\n`; await fs.writeFile(gitignorePath, content, "utf-8"); } } catch (error) { throw new FileSystemError(`Failed to update .gitignore: ${getErrorMessage(error)}`); } } //# sourceMappingURL=config.js.map