UNPKG

sync-worktrees

Version:

Automatically synchronize Git worktrees with remote branches - perfect for multi-branch development workflows

928 lines (918 loc) 178 kB
#!/usr/bin/env node // src/index.ts import * as path10 from "path"; import { confirm as confirm2 } from "@inquirer/prompts"; import * as cron2 from "node-cron"; import pLimit2 from "p-limit"; // src/constants.ts var GIT_CONSTANTS = { REMOTE_PREFIX: "origin/", REMOTE_NAME: "origin", HEAD_REF: "/HEAD", DEFAULT_BRANCH: "main", COMMON_DEFAULT_BRANCHES: ["main", "master", "develop", "trunk"], BARE_DIR_NAME: ".bare", DIVERGED_DIR_NAME: ".diverged", LFS_HEADER: "version https://git-lfs.github.com/spec/", SUBMODULE_STATUS_ADDED: "+", SUBMODULE_STATUS_REMOVED: "-", GITDIR_PREFIX: "gitdir:", GIT_CHECK_IGNORE_NO_MATCH: "exit code: 1", REFS: { HEADS: "refs/heads/", REMOTES: "refs/remotes/origin", REMOTES_ORIGIN: "refs/remotes/origin/*" }, FETCH_CONFIG: "+refs/heads/*:refs/remotes/origin/*" }; var GIT_OPERATIONS = { MERGE_HEAD: "MERGE_HEAD", CHERRY_PICK_HEAD: "CHERRY_PICK_HEAD", REVERT_HEAD: "REVERT_HEAD", BISECT_LOG: "BISECT_LOG", REBASE_MERGE: "rebase-merge", REBASE_APPLY: "rebase-apply" }; var DEFAULT_CONFIG = { CRON_SCHEDULE: "0 * * * *", RETRY: { MAX_ATTEMPTS: 3, MAX_LFS_RETRIES: 2, INITIAL_DELAY_MS: 1e3, MAX_DELAY_MS: 3e4, BACKOFF_MULTIPLIER: 2, JITTER_MS: 500 }, PARALLELISM: { MAX_REPOSITORIES: 2, MAX_WORKTREE_CREATION: 1, MAX_WORKTREE_UPDATES: 3, MAX_WORKTREE_REMOVAL: 3, MAX_STATUS_CHECKS: 20, MAX_SAFE_TOTAL_CONCURRENT_OPS: 100 }, UPDATE_EXISTING_WORKTREES: true }; var ERROR_MESSAGES = { GIT_NOT_INITIALIZED: "Git service not initialized. Call initialize() first.", ALREADY_EXISTS: "already exists", ALREADY_REGISTERED: "already registered worktree", FAST_FORWARD_FAILED: [ "Not possible to fast-forward", "fatal: Not possible to fast-forward, aborting", "cannot fast-forward" ], NO_UPSTREAM: [ "fatal: no upstream configured", "no upstream configured for branch", "fatal: ambiguous argument", "unknown revision or path" ], LFS_ERROR: ["smudge filter lfs failed", "git-lfs", "LFS"], EXDEV: "EXDEV" }; var ENV_CONSTANTS = { GIT_LFS_SKIP_SMUDGE: "GIT_LFS_SKIP_SMUDGE", NODE_ENV_TEST: "test" }; var PATH_CONSTANTS = { GIT_DIR: ".git", README: "README" }; var CONFIG_CONSTANTS = { WILDCARD_PATTERN: ".*" }; var METADATA_CONSTANTS = { MAX_HISTORY_ENTRIES: 10, METADATA_FILENAME: "sync-metadata.json", WORKTREE_METADATA_PATH: ".git/worktrees", DIVERGED_INFO_FILE: ".diverged-info.json", DIVERGED_REASON: "diverged-history-with-changes", ACTION_CREATED: "created", ACTION_UPDATED: "updated", ACTION_FETCHED: "fetched" }; // src/services/config-loader.service.ts import * as fs from "fs/promises"; import * as path from "path"; import { pathToFileURL } from "url"; var ConfigLoaderService = class { async loadConfigFile(configPath) { const absolutePath = path.resolve(configPath); try { await fs.access(absolutePath); } catch { throw new Error(`Config file not found: ${absolutePath}`); } try { const fileUrl = pathToFileURL(absolutePath); fileUrl.searchParams.set("t", Date.now().toString()); const configModule = await import(fileUrl.href); const config = configModule.default; if (!config) { throw new Error("Config file must use 'export default' syntax"); } this.validateConfigFile(config); return config; } catch (error) { if (error instanceof Error && error.message.includes("Config file not found")) { throw error; } throw new Error(`Failed to load config file: ${error.message}`); } } validateConfigFile(config) { if (!config || typeof config !== "object") { throw new Error("Config file must export an object"); } const configObj = config; if (!Array.isArray(configObj.repositories)) { throw new Error("Config file must have a 'repositories' array"); } if (configObj.repositories.length === 0) { throw new Error("Config file must have at least one repository"); } const seenNames = /* @__PURE__ */ new Set(); configObj.repositories.forEach((repo, index) => { if (!repo || typeof repo !== "object") { throw new Error(`Repository at index ${index} must be an object`); } const repoObj = repo; if (!repoObj.name || typeof repoObj.name !== "string") { throw new Error(`Repository at index ${index} must have a 'name' property`); } if (seenNames.has(repoObj.name)) { throw new Error(`Duplicate repository name: ${repoObj.name}`); } seenNames.add(repoObj.name); if (!repoObj.repoUrl || typeof repoObj.repoUrl !== "string") { throw new Error(`Repository '${repoObj.name}' must have a 'repoUrl' property`); } if (!repoObj.worktreeDir || typeof repoObj.worktreeDir !== "string") { throw new Error(`Repository '${repoObj.name}' must have a 'worktreeDir' property`); } if (repoObj.bareRepoDir !== void 0 && typeof repoObj.bareRepoDir !== "string") { throw new Error(`Repository '${repoObj.name}' has invalid 'bareRepoDir' property`); } if (repoObj.cronSchedule !== void 0 && typeof repoObj.cronSchedule !== "string") { throw new Error(`Repository '${repoObj.name}' has invalid 'cronSchedule' property`); } if (repoObj.runOnce !== void 0 && typeof repoObj.runOnce !== "boolean") { throw new Error(`Repository '${repoObj.name}' has invalid 'runOnce' property`); } if (repoObj.filesToCopyOnBranchCreate !== void 0) { this.validateFilesToCopyConfig(repoObj.filesToCopyOnBranchCreate, `Repository '${repoObj.name}'`); } }); if (configObj.defaults) { if (typeof configObj.defaults !== "object") { throw new Error("'defaults' must be an object"); } const defaults = configObj.defaults; if (defaults.cronSchedule !== void 0 && typeof defaults.cronSchedule !== "string") { throw new Error("Invalid 'cronSchedule' in defaults"); } if (defaults.runOnce !== void 0 && typeof defaults.runOnce !== "boolean") { throw new Error("Invalid 'runOnce' in defaults"); } if (defaults.retry !== void 0 && typeof defaults.retry !== "object") { throw new Error("Invalid 'retry' in defaults"); } if (defaults.filesToCopyOnBranchCreate !== void 0) { this.validateFilesToCopyConfig(defaults.filesToCopyOnBranchCreate, "defaults"); } } if (configObj.retry !== void 0) { if (typeof configObj.retry !== "object") { throw new Error("'retry' must be an object"); } const retry2 = configObj.retry; if (retry2.maxAttempts !== void 0) { if (retry2.maxAttempts !== "unlimited" && (typeof retry2.maxAttempts !== "number" || retry2.maxAttempts < 1)) { throw new Error("Invalid 'maxAttempts' in retry config. Must be 'unlimited' or a positive number"); } } if (retry2.maxLfsRetries !== void 0) { if (typeof retry2.maxLfsRetries !== "number" || retry2.maxLfsRetries < 0) { throw new Error("Invalid 'maxLfsRetries' in retry config. Must be a non-negative number"); } } if (retry2.initialDelayMs !== void 0 && (typeof retry2.initialDelayMs !== "number" || retry2.initialDelayMs < 0)) { throw new Error("Invalid 'initialDelayMs' in retry config"); } if (retry2.maxDelayMs !== void 0 && (typeof retry2.maxDelayMs !== "number" || retry2.maxDelayMs < 0)) { throw new Error("Invalid 'maxDelayMs' in retry config"); } if (retry2.backoffMultiplier !== void 0 && (typeof retry2.backoffMultiplier !== "number" || retry2.backoffMultiplier < 1)) { throw new Error("Invalid 'backoffMultiplier' in retry config"); } } if (configObj.parallelism !== void 0) { this.validateParallelismConfig(configObj.parallelism, "global"); } if (configObj.defaults && typeof configObj.defaults === "object") { const defaults = configObj.defaults; if (defaults.parallelism !== void 0) { this.validateParallelismConfig(defaults.parallelism, "defaults"); } } } validateParallelismConfig(parallelism, context) { if (typeof parallelism !== "object" || parallelism === null) { throw new Error(`'parallelism' in ${context} must be an object`); } const config = parallelism; if (config.maxRepositories !== void 0) { if (typeof config.maxRepositories !== "number" || config.maxRepositories < 1) { throw new Error(`Invalid 'maxRepositories' in ${context} parallelism config. Must be a positive number`); } } if (config.maxWorktreeCreation !== void 0) { if (typeof config.maxWorktreeCreation !== "number" || config.maxWorktreeCreation < 1) { throw new Error(`Invalid 'maxWorktreeCreation' in ${context} parallelism config. Must be a positive number`); } } if (config.maxWorktreeUpdates !== void 0) { if (typeof config.maxWorktreeUpdates !== "number" || config.maxWorktreeUpdates < 1) { throw new Error(`Invalid 'maxWorktreeUpdates' in ${context} parallelism config. Must be a positive number`); } } if (config.maxWorktreeRemoval !== void 0) { if (typeof config.maxWorktreeRemoval !== "number" || config.maxWorktreeRemoval < 1) { throw new Error(`Invalid 'maxWorktreeRemoval' in ${context} parallelism config. Must be a positive number`); } } if (config.maxStatusChecks !== void 0) { if (typeof config.maxStatusChecks !== "number" || config.maxStatusChecks < 1) { throw new Error(`Invalid 'maxStatusChecks' in ${context} parallelism config. Must be a positive number`); } } const maxRepos = config.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES; const maxCreation = config.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION; const maxUpdates = config.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES; const maxRemoval = config.maxWorktreeRemoval ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_REMOVAL; const maxStatus = config.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS; const maxPerRepoOps = maxCreation + maxUpdates + maxRemoval + maxStatus; const totalMaxConcurrent = maxRepos * maxPerRepoOps; if (totalMaxConcurrent > DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS) { const safeMaxRepos = Math.floor(DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS / maxPerRepoOps); throw new Error( `Total concurrent operations (${totalMaxConcurrent}) exceeds safe limit (${DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS}). With current per-repository limits (creation: ${maxCreation}, updates: ${maxUpdates}, removal: ${maxRemoval}, status: ${maxStatus}), maximum safe maxRepositories is ${safeMaxRepos}. Consider reducing maxRepositories or lowering per-operation limits.` ); } } validateFilesToCopyConfig(filesToCopy, context) { if (!Array.isArray(filesToCopy)) { throw new Error(`'filesToCopyOnBranchCreate' in ${context} must be an array`); } for (let i = 0; i < filesToCopy.length; i++) { const pattern = filesToCopy[i]; if (typeof pattern !== "string" || pattern.trim() === "") { throw new Error( `'filesToCopyOnBranchCreate' in ${context} must contain only non-empty strings (invalid at index ${i})` ); } } } resolveRepositoryConfig(repo, defaults, configDir, globalRetry) { const resolved = { name: repo.name, repoUrl: repo.repoUrl, worktreeDir: this.resolvePath(repo.worktreeDir, configDir), cronSchedule: repo.cronSchedule ?? defaults?.cronSchedule ?? DEFAULT_CONFIG.CRON_SCHEDULE, runOnce: repo.runOnce ?? defaults?.runOnce ?? false }; if (repo.bareRepoDir) { resolved.bareRepoDir = this.resolvePath(repo.bareRepoDir, configDir); } if (repo.branchMaxAge || defaults?.branchMaxAge) { resolved.branchMaxAge = repo.branchMaxAge ?? defaults?.branchMaxAge; } if (repo.skipLfs !== void 0 || defaults?.skipLfs !== void 0) { resolved.skipLfs = repo.skipLfs ?? defaults?.skipLfs ?? false; } if (repo.retry || defaults?.retry || globalRetry) { resolved.retry = { ...globalRetry || {}, ...defaults?.retry || {}, ...repo.retry || {} }; } if (repo.parallelism || defaults?.parallelism) { resolved.parallelism = { ...defaults?.parallelism || {}, ...repo.parallelism || {} }; } if (repo.updateExistingWorktrees !== void 0 || defaults?.updateExistingWorktrees !== void 0) { resolved.updateExistingWorktrees = repo.updateExistingWorktrees ?? defaults?.updateExistingWorktrees ?? true; } if (repo.filesToCopyOnBranchCreate || defaults?.filesToCopyOnBranchCreate) { resolved.filesToCopyOnBranchCreate = repo.filesToCopyOnBranchCreate ?? defaults?.filesToCopyOnBranchCreate; } return resolved; } resolvePath(inputPath, baseDir) { if (path.isAbsolute(inputPath)) { return inputPath; } return path.resolve(baseDir || process.cwd(), inputPath); } filterRepositories(repositories, filter) { if (!filter) { return repositories; } const patterns = filter.split(",").map((p) => p.trim()); return repositories.filter((repo) => { return patterns.some((pattern) => { if (pattern.includes("*")) { const regex = new RegExp("^" + pattern.replace(/\*/g, CONFIG_CONSTANTS.WILDCARD_PATTERN) + "$"); return regex.test(repo.name); } return repo.name === pattern; }); }); } }; // src/services/InteractiveUIService.tsx import React7 from "react"; import * as path7 from "path"; import { render } from "ink"; import * as cron from "node-cron"; import { spawn } from "child_process"; // src/components/App.tsx import React6, { useState as useState5, useEffect as useEffect5, useCallback as useCallback3, useRef as useRef3 } from "react"; import { Box as Box6, useInput as useInput5, useStdout } from "ink"; // src/components/StatusBar.tsx import React, { useState, useEffect } from "react"; import { Box, Text } from "ink"; import { CronExpressionParser } from "cron-parser"; var StatusBar = ({ status, repositoryCount, lastSyncTime, cronSchedule, diskSpaceUsed }) => { const [nextSyncTime, setNextSyncTime] = useState(null); useEffect(() => { if (!cronSchedule) { setNextSyncTime(null); return void 0; } try { const interval = CronExpressionParser.parse(cronSchedule); setNextSyncTime(interval.next().toDate()); const timer = setInterval(() => { const nextInterval = CronExpressionParser.parse(cronSchedule); setNextSyncTime(nextInterval.next().toDate()); }, 6e4); return () => clearInterval(timer); } catch (error) { setNextSyncTime(null); return void 0; } }, [cronSchedule]); const formatTime = (date) => { if (!date) return "N/A"; return date.toLocaleTimeString(); }; const getStatusColor = () => { return status === "syncing" ? "yellow" : "green"; }; const getStatusIcon = () => { return status === "syncing" ? "\u27F3" : "\u2713"; }; return /* @__PURE__ */ React.createElement(Box, { borderStyle: "single", paddingX: 1 }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: "100%" }, /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, { bold: true }, getStatusIcon(), " Status:", " ", /* @__PURE__ */ React.createElement(Text, { color: getStatusColor() }, status === "syncing" ? "Syncing..." : "Running")), /* @__PURE__ */ React.createElement(Text, null, "Repositories: ", /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, repositoryCount))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Last Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(lastSyncTime))), cronSchedule && /* @__PURE__ */ React.createElement(Text, null, "Next Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(nextSyncTime)))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Disk Space: ", /* @__PURE__ */ React.createElement(Text, { color: "magenta" }, diskSpaceUsed || "Calculating...")), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "s"), "ync", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "c"), "reate", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "o"), "pen", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "r"), "eload", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "?"), "help", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "q"), "uit")))); }; var StatusBar_default = StatusBar; // src/components/HelpModal.tsx import React2 from "react"; import { Box as Box2, Text as Text2, useInput } from "ink"; var HelpModal = ({ onClose }) => { useInput(() => { onClose(); }); return /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", alignItems: "center", flexDirection: "column", marginTop: 2, marginBottom: 2 }, /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u{1F333} sync-worktrees - Keyboard Shortcuts")), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green", dimColor: true }, "Navigation"), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "j"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u2193")), /* @__PURE__ */ React2.createElement(Text2, null, "Scroll down one line")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "k"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u2191")), /* @__PURE__ */ React2.createElement(Text2, null, "Scroll up one line")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "gg")), /* @__PURE__ */ React2.createElement(Text2, null, "Jump to top")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "G")), /* @__PURE__ */ React2.createElement(Text2, null, "Jump to bottom (re-enables auto-scroll)")), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green", dimColor: true }, "Actions")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "s")), /* @__PURE__ */ React2.createElement(Text2, null, "Manually trigger sync for all repositories")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "c")), /* @__PURE__ */ React2.createElement(Text2, null, "Create a new branch")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "o")), /* @__PURE__ */ React2.createElement(Text2, null, "Open editor in worktree")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "r")), /* @__PURE__ */ React2.createElement(Text2, null, "Reload configuration and re-sync all repos")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "?"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "h")), /* @__PURE__ */ React2.createElement(Text2, null, "Toggle this help screen")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "q"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "Esc")), /* @__PURE__ */ React2.createElement(Text2, null, "Gracefully quit"))), /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Press any key to close")))); }; var HelpModal_default = HelpModal; // src/components/BranchCreationWizard.tsx import React3, { useState as useState2, useEffect as useEffect2, useCallback } from "react"; import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink"; var isValidGitBranchName = (name) => { if (!name.trim()) { return { valid: false, error: "Branch name cannot be empty" }; } if (name.startsWith("-")) { return { valid: false, error: "Branch name cannot start with '-'" }; } if (name.endsWith(".lock")) { return { valid: false, error: "Branch name cannot end with '.lock'" }; } if (name.includes("..")) { return { valid: false, error: "Branch name cannot contain '..'" }; } if (name.includes("@{")) { return { valid: false, error: "Branch name cannot contain '@{'" }; } if (name.startsWith(".") || name.endsWith(".")) { return { valid: false, error: "Branch name cannot start or end with '.'" }; } if (name.includes("//")) { return { valid: false, error: "Branch name cannot contain consecutive slashes" }; } if (/[\x00-\x1f\x7f~^:?*\[\\]/.test(name)) { return { valid: false, error: "Branch name contains invalid characters" }; } return { valid: true }; }; var BranchCreationWizard = ({ repositories, getBranchesForRepo, getDefaultBranchForRepo, createAndPushBranch, onClose, onComplete }) => { const [step, setStep] = useState2(repositories.length > 1 ? "SELECT_PROJECT" : "SELECT_BRANCH"); const [selectedProjectIndex, setSelectedProjectIndex] = useState2(repositories.length === 1 ? 0 : 0); const [branches, setBranches] = useState2([]); const [defaultBranch, setDefaultBranch] = useState2(""); const [selectedBranchIndex, setSelectedBranchIndex] = useState2(0); const [branchName, setBranchName] = useState2(""); const [existingSuffix, setExistingSuffix] = useState2(null); const [validationError, setValidationError] = useState2(null); const [result, setResult] = useState2(null); const [loading, setLoading] = useState2(false); const loadBranches = useCallback( async (repoIndex) => { setLoading(true); try { const branchList = await getBranchesForRepo(repoIndex); const defaultBr = getDefaultBranchForRepo(repoIndex); setBranches(branchList); setDefaultBranch(defaultBr); const defaultIndex = branchList.indexOf(defaultBr); setSelectedBranchIndex(defaultIndex >= 0 ? defaultIndex : 0); } catch { setBranches([]); } setLoading(false); }, [getBranchesForRepo, getDefaultBranchForRepo] ); const checkBranchExists = useCallback( (name) => { if (!name.trim()) { setExistingSuffix(null); setValidationError(null); return; } const validation = isValidGitBranchName(name); if (!validation.valid) { setValidationError(validation.error ?? null); setExistingSuffix(null); return; } setValidationError(null); let suffix = 0; let testName = name; while (branches.includes(testName)) { suffix++; testName = `${name}-${suffix}`; } setExistingSuffix(suffix > 0 ? suffix : null); }, [branches] ); useEffect2(() => { if (step === "SELECT_BRANCH" && branches.length === 0 && !loading) { loadBranches(selectedProjectIndex); } }, [step, selectedProjectIndex, branches.length, loading, loadBranches]); useEffect2(() => { if (step === "ENTER_NAME") { checkBranchExists(branchName); } }, [branchName, step, checkBranchExists]); const handleCreateBranch = async () => { const trimmedName = branchName.trim(); if (!trimmedName) return; const validation = isValidGitBranchName(trimmedName); if (!validation.valid) { setValidationError(validation.error ?? null); return; } setStep("CREATING"); const baseBranch = branches[selectedBranchIndex]; const createResult = await createAndPushBranch(selectedProjectIndex, baseBranch, trimmedName); setResult(createResult); setStep("RESULT"); }; useInput2((input2, key) => { if (step === "CREATING") return; if (key.escape) { if (step === "SELECT_PROJECT") { onClose(); } else if (step === "SELECT_BRANCH") { if (repositories.length > 1) { setBranches([]); setStep("SELECT_PROJECT"); } else { onClose(); } } else if (step === "ENTER_NAME") { setBranchName(""); setExistingSuffix(null); setStep("SELECT_BRANCH"); } else if (step === "RESULT") { const context = result?.success ? { repoIndex: selectedProjectIndex, baseBranch: branches[selectedBranchIndex], newBranch: result.finalName } : void 0; onComplete(result?.success ?? false, context); } return; } if (step === "SELECT_PROJECT") { if (key.upArrow) { setSelectedProjectIndex((prev) => Math.max(0, prev - 1)); } else if (key.downArrow) { setSelectedProjectIndex((prev) => Math.min(repositories.length - 1, prev + 1)); } else if (key.return) { loadBranches(selectedProjectIndex); setStep("SELECT_BRANCH"); } } else if (step === "SELECT_BRANCH") { if (key.upArrow) { setSelectedBranchIndex((prev) => Math.max(0, prev - 1)); } else if (key.downArrow) { setSelectedBranchIndex((prev) => Math.min(branches.length - 1, prev + 1)); } else if (key.return && branches.length > 0) { setStep("ENTER_NAME"); } } else if (step === "ENTER_NAME") { if (key.return && branchName.trim()) { void handleCreateBranch(); } else if (key.backspace || key.delete) { setBranchName((prev) => prev.slice(0, -1)); } else if (input2 && !key.ctrl && !key.meta) { const validChar = /^[a-zA-Z0-9/_-]$/.test(input2); if (validChar) { setBranchName((prev) => prev + input2); } } } else if (step === "RESULT") { const context = result?.success ? { repoIndex: selectedProjectIndex, baseBranch: branches[selectedBranchIndex], newBranch: result.finalName } : void 0; onComplete(result?.success ?? false, context); } }); const getStepNumber = () => { if (repositories.length === 1) { if (step === "SELECT_BRANCH") return 1; if (step === "ENTER_NAME") return 2; return 2; } if (step === "SELECT_PROJECT") return 1; if (step === "SELECT_BRANCH") return 2; if (step === "ENTER_NAME") return 3; return 3; }; const getTotalSteps = () => repositories.length === 1 ? 2 : 3; const renderProjectSelection = () => /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Select repository:"), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, repositories.map((repo, idx) => /* @__PURE__ */ React3.createElement(Box3, { key: repo.index }, /* @__PURE__ */ React3.createElement(Text3, { color: idx === selectedProjectIndex ? "cyan" : void 0 }, idx === selectedProjectIndex ? "> " : " ", repo.name))))); const renderBranchSelection = () => { if (loading) { return /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Loading branches..."); } if (branches.length === 0) { return /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, "No branches found"); } const visibleCount = 8; const halfVisible = Math.floor(visibleCount / 2); let startIdx = Math.max(0, selectedBranchIndex - halfVisible); const endIdx = Math.min(branches.length, startIdx + visibleCount); if (endIdx - startIdx < visibleCount) { startIdx = Math.max(0, endIdx - visibleCount); } const visibleBranches = branches.slice(startIdx, endIdx); return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Select base branch:"), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, startIdx > 0 && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ..."), visibleBranches.map((branch, idx) => { const actualIdx = startIdx + idx; const isSelected = actualIdx === selectedBranchIndex; const isDefault = branch === defaultBranch; return /* @__PURE__ */ React3.createElement(Box3, { key: branch }, /* @__PURE__ */ React3.createElement(Text3, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", branch, isDefault && /* @__PURE__ */ React3.createElement(Text3, { color: "green" }, " (default)"))); }), endIdx < branches.length && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ..."))); }; const renderNameInput = () => { const baseBranch = branches[selectedBranchIndex] || ""; const finalName = existingSuffix !== null ? `${branchName}-${existingSuffix}` : branchName; const endsWithSlash = branchName.endsWith("/"); return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Base branch: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, baseBranch)), /* @__PURE__ */ React3.createElement(Text3, null, "Enter new branch name:"), /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, "> "), /* @__PURE__ */ React3.createElement(Text3, null, branchName), /* @__PURE__ */ React3.createElement(Text3, { color: "gray" }, "|")), validationError && /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, validationError), !validationError && endsWithSlash && /* @__PURE__ */ React3.createElement(Text3, { color: "yellow", dimColor: true }, "Hint: consecutive slashes (//) are not allowed"), !validationError && !endsWithSlash && existingSuffix !== null && branchName && /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Name exists, will create: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, finalName))); }; const renderCreating = () => /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Creating branch..."), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Please wait while the branch is created and pushed to remote.")); const renderResult = () => { if (!result) return null; if (result.success) { return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "green" }, "Branch created successfully!"), /* @__PURE__ */ React3.createElement(Text3, null, "Created: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, result.finalName)), /* @__PURE__ */ React3.createElement(Text3, null, "From: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, branches[selectedBranchIndex])), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Syncing now to create the worktree...")); } return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, "Failed to create branch"), /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, result.error)); }; const renderContent = () => { switch (step) { case "SELECT_PROJECT": return renderProjectSelection(); case "SELECT_BRANCH": return renderBranchSelection(); case "ENTER_NAME": return renderNameInput(); case "CREATING": return renderCreating(); case "RESULT": return renderResult(); } }; const renderFooter = () => { if (step === "CREATING") return null; if (step === "RESULT") { return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Press any key to continue"); } if (step === "ENTER_NAME") { return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Enter to create \u2022 ESC to go back"); } return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u2191/\u2193 to navigate \u2022 Enter to select \u2022 ESC to cancel"); }; return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Box3, { borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "\u{1F33F} Create New Branch", " ", step !== "CREATING" && step !== "RESULT" && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "(Step ", getStepNumber(), "/", getTotalSteps(), ")"))), repositories.length > 1 && step !== "SELECT_PROJECT" && step !== "CREATING" && step !== "RESULT" && /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Repository: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, repositories[selectedProjectIndex].name))), renderContent(), /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, renderFooter()))); }; var BranchCreationWizard_default = BranchCreationWizard; // src/components/OpenEditorWizard.tsx import React4, { useState as useState3, useEffect as useEffect3, useMemo, useCallback as useCallback2, useRef } from "react"; import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink"; var OpenEditorWizard = ({ repositories, getWorktreesForRepo, openEditorInWorktree, onClose }) => { const [step, setStep] = useState3(repositories.length > 1 ? "SELECT_PROJECT" : "SELECT_WORKTREE"); const [selectedProjectIndex, setSelectedProjectIndex] = useState3(0); const [projectFilter, setProjectFilter] = useState3(""); const selectedRepoIndexRef = useRef(repositories.length === 1 ? 0 : -1); const [worktrees, setWorktrees] = useState3([]); const [selectedWorktreeIndex, setSelectedWorktreeIndex] = useState3(0); const [worktreeFilter, setWorktreeFilter] = useState3(""); const [loading, setLoading] = useState3(false); const [error, setError] = useState3(null); const filteredProjects = useMemo(() => { if (!projectFilter) return repositories; const lowerFilter = projectFilter.toLowerCase(); return repositories.filter((repo) => repo.name.toLowerCase().includes(lowerFilter)); }, [repositories, projectFilter]); const filteredWorktrees = useMemo(() => { if (!worktreeFilter) return worktrees; const lowerFilter = worktreeFilter.toLowerCase(); return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerFilter)); }, [worktrees, worktreeFilter]); const loadWorktrees = useCallback2( async (repoIndex) => { setLoading(true); try { const wts = await getWorktreesForRepo(repoIndex); setWorktrees(wts); setSelectedWorktreeIndex(0); } catch (err) { setError(`Failed to load worktrees: ${err}`); setStep("ERROR"); } setLoading(false); }, [getWorktreesForRepo] ); useEffect3(() => { if (step === "SELECT_WORKTREE" && worktrees.length === 0 && !loading && selectedRepoIndexRef.current >= 0) { loadWorktrees(selectedRepoIndexRef.current); } }, [step, worktrees.length, loading, loadWorktrees]); const handleOpenEditor = () => { const worktree = filteredWorktrees[selectedWorktreeIndex]; if (!worktree) return; setStep("OPENING"); const result = openEditorInWorktree(worktree.path); if (result.success) { onClose(); } else { setError(result.error || "Failed to open editor"); setStep("ERROR"); } }; useInput3((input2, key) => { if (step === "OPENING") return; if (key.escape) { if (step === "SELECT_PROJECT") { onClose(); } else if (step === "SELECT_WORKTREE") { if (repositories.length > 1) { setWorktrees([]); setWorktreeFilter(""); selectedRepoIndexRef.current = -1; setStep("SELECT_PROJECT"); } else { onClose(); } } else if (step === "ERROR") { onClose(); } return; } if (step === "SELECT_PROJECT") { if (key.upArrow) { setSelectedProjectIndex((prev) => Math.max(0, prev - 1)); } else if (key.downArrow) { setSelectedProjectIndex((prev) => Math.min(filteredProjects.length - 1, prev + 1)); } else if (key.return && filteredProjects.length > 0) { const selectedRepo = filteredProjects[selectedProjectIndex]; if (selectedRepo) { selectedRepoIndexRef.current = selectedRepo.index; setStep("SELECT_WORKTREE"); loadWorktrees(selectedRepo.index); } } else if (key.backspace || key.delete) { setProjectFilter((prev) => prev.slice(0, -1)); setSelectedProjectIndex(0); } else if (input2 && !key.ctrl && !key.meta) { setProjectFilter((prev) => prev + input2); setSelectedProjectIndex(0); } } else if (step === "SELECT_WORKTREE") { if (key.upArrow) { setSelectedWorktreeIndex((prev) => Math.max(0, prev - 1)); } else if (key.downArrow) { setSelectedWorktreeIndex((prev) => Math.min(filteredWorktrees.length - 1, prev + 1)); } else if (key.return && filteredWorktrees.length > 0) { handleOpenEditor(); } else if (key.backspace || key.delete) { setWorktreeFilter((prev) => prev.slice(0, -1)); setSelectedWorktreeIndex(0); } else if (input2 && !key.ctrl && !key.meta) { setWorktreeFilter((prev) => prev + input2); setSelectedWorktreeIndex(0); } } else if (step === "ERROR") { onClose(); } }); const getStepNumber = () => { if (repositories.length === 1) { return 1; } return step === "SELECT_PROJECT" ? 1 : 2; }; const getTotalSteps = () => repositories.length === 1 ? 1 : 2; const renderProjectSelection = () => { const visibleCount = 8; const halfVisible = Math.floor(visibleCount / 2); let startIdx = Math.max(0, selectedProjectIndex - halfVisible); const endIdx = Math.min(filteredProjects.length, startIdx + visibleCount); if (endIdx - startIdx < visibleCount) { startIdx = Math.max(0, endIdx - visibleCount); } const visibleProjects = filteredProjects.slice(startIdx, endIdx); return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Select repository:"), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, null, "Filter: "), /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, projectFilter || "_"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ", "(", filteredProjects.length, "/", repositories.length, " matches)")), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, filteredProjects.length === 0 ? /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "No matches") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, startIdx > 0 && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."), visibleProjects.map((repo, idx) => { const actualIdx = startIdx + idx; const isSelected = actualIdx === selectedProjectIndex; return /* @__PURE__ */ React4.createElement(Box4, { key: repo.index }, /* @__PURE__ */ React4.createElement(Text4, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", repo.name)); }), endIdx < filteredProjects.length && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ...")))); }; const renderWorktreeSelection = () => { if (loading) { return /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "Loading worktrees..."); } if (worktrees.length === 0) { return /* @__PURE__ */ React4.createElement(Text4, { color: "red" }, "No worktrees found"); } const visibleCount = 8; const halfVisible = Math.floor(visibleCount / 2); let startIdx = Math.max(0, selectedWorktreeIndex - halfVisible); const endIdx = Math.min(filteredWorktrees.length, startIdx + visibleCount); if (endIdx - startIdx < visibleCount) { startIdx = Math.max(0, endIdx - visibleCount); } const visibleWorktrees = filteredWorktrees.slice(startIdx, endIdx); return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Select worktree:"), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, null, "Filter: "), /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, worktreeFilter || "_"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ", "(", filteredWorktrees.length, "/", worktrees.length, " matches)")), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, filteredWorktrees.length === 0 ? /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "No matches") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, startIdx > 0 && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."), visibleWorktrees.map((wt, idx) => { const actualIdx = startIdx + idx; const isSelected = actualIdx === selectedWorktreeIndex; return /* @__PURE__ */ React4.createElement(Box4, { key: wt.path }, /* @__PURE__ */ React4.createElement(Text4, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", wt.branch)); }), endIdx < filteredWorktrees.length && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ...")))); }; const renderOpening = () => /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "Opening editor...")); const renderError = () => /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "red" }, "Error: ", error), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Press any key to close")); const renderContent = () => { switch (step) { case "SELECT_PROJECT": return renderProjectSelection(); case "SELECT_WORKTREE": return renderWorktreeSelection(); case "OPENING": return renderOpening(); case "ERROR": return renderError(); } }; const renderFooter = () => { if (step === "OPENING") return null; if (step === "ERROR") return null; return /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "\u2191/\u2193 navigate \u2022 Type to filter \u2022 Enter to select \u2022 ESC to cancel"); }; return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: "blue", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "blue" }, "\u{1F4C2} Open in Editor", " ", step !== "OPENING" && step !== "ERROR" && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "(Step ", getStepNumber(), "/", getTotalSteps(), ")"))), repositories.length > 1 && step === "SELECT_WORKTREE" && !loading && selectedRepoIndexRef.current >= 0 && /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Repository: ", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, repositories.find((r) => r.index === selectedRepoIndexRef.current)?.name))), renderContent(), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1 }, renderFooter()))); }; var OpenEditorWizard_default = OpenEditorWizard; // src/components/LogPanel.tsx import React5, { useState as useState4, useEffect as useEffect4, useRef as useRef2 } from "react"; import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink"; var LogPanel = ({ logs, height, isActive }) => { const [scrollOffset, setScrollOffset] = useState4(0); const [autoScroll, setAutoScroll] = useState4(true); const [pendingG, setPendingG] = useState4(false); const gTimeoutRef = useRef2(null); const borderLines = 2; const headerLine = 1; const visibleLines = Math.max(1, height - borderLines - headerLine); const maxOffset = Math.max(0, logs.length - visibleLines); useEffect4(() => { if (autoScroll) { setScrollOffset(maxOffset); } }, [logs.length, maxOffset, autoScroll]); useEffect4(() => { return () => { if (gTimeoutRef.current) { clearTimeout(gTimeoutRef.current); } }; }, []); useInput4( (input2, key) => { if (!isActive) return; if (key.upArrow || input2 === "k") { setScrollOffset((prev) => Math.max(0, prev - 1)); setAutoScroll(false); setPendingG(false); } else if (key.downArrow || input2 === "j") { setScrollOffset((prev) => { const newOffset = Math.min(maxOffset, prev + 1); if (newOffset >= maxOffset) { setAutoScroll(true); } return newOffset; }); setPendingG(false); } else if (key.pageUp) { setScrollOffset((prev) => Math.max(0, prev - visibleLines)); setAutoScroll(false); setPendingG(false); } else if (key.pageDown) { setScrollOffset((prev) => { const newOffset = Math.min(maxOffset, prev + visibleLines); if (newOffset >= maxOffset) { setAutoScroll(true); } return newOffset; }); setPendingG(false); } else if (input2 === "g") { if (pendingG) { setScrollOffset(0); setAutoScroll(false); setPendingG(false); if (gTimeoutRef.current) { clearTimeout(gTimeoutRef.current); gTimeoutRef.current = null; } } else { setPendingG(true); gTimeoutRef.current = setTimeout(() => { setPendingG(false); }, 500); } } else if (input2 === "G") { setScrollOffset(maxOffset); setAutoScroll(true); setPendingG(false); } }, { isActive } ); const getLogColor = (level) => { switch (level) { case "error": return "red"; case "warn": return "yellow"; default: return void 0; } }; const visibleLogs = logs.slice(scrollOffset, scrollOffset + visibleLines); const hasMoreAbove = scrollOffset > 0; const hasMoreBelow = scrollOffset + visibleLines < logs.length; const aboveCount = scrollOffset; const belowCount = logs.length - scrollOffset - visibleLines; const emptyLines = Math.max(0, visibleLines - visibleLogs.length); return /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "single", flexDirection: "column", flexGrow: 1, paddingX: 1 }, /* @__PURE__ */ React5.createElement(Box5, { justifyContent: "space-between" }, /* @__PURE__ */ React5.createElement(Text5, { bold: true }, "\u{1F4CB} Logs ", logs.length > 0 && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(", logs.length, " entries)")), isActive && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, hasMoreAbove || hasMoreBelow ? "\u2191/\u2193 scroll" : "", " ", autoScroll ? "(auto)" : "")), hasMoreAbove && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2191 ", aboveCount, " more above"), visibleLogs.map((log) => /* @__PURE__ */ React5.createElement(Text5, { key: log.id, color: getLogColor(log.level), wrap: "truncate" }, log.message)), Array.from({ length: emptyLines }).map((_, i) => /* @__PURE__ */ React5.createElement(Text5, { key: `empty-${i}` }, " ")), hasMoreBelow && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2193 ",