refactor: wave 2 - split atlas, auto-update-checker, session-recovery, todo-enforcer, background-task hooks
- Extract atlas/ into 15 focused modules (hook, event handler, tool policies, types, etc.) - Split auto-update-checker into checker/ and hook/ subdirectories with single-purpose files - Decompose session-recovery into separate recovery strategy files per error type - Extract todo-continuation-enforcer from monolith to directory with dedicated modules - Split background-task/tools.ts into individual tool creator files - Extract command-executor, tmux-utils into focused sub-modules - Split config/schema.ts into domain-specific schema files - Decompose cli/config-manager.ts into focused modules - Rollback skill-mcp-manager, model-availability, index.ts splits that broke tests - Fix all import path depths for moved files (../../ -> ../../../) - Add explicit type annotations to resolve TS7006 implicit any errors Typecheck: 0 errors Tests: 2359 pass, 5 fail (all pre-existing)
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
export interface CommandMatch {
|
||||
fullMatch: string
|
||||
command: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
const COMMAND_PATTERN = /!`([^`]+)`/g
|
||||
|
||||
export function findEmbeddedCommands(text: string): CommandMatch[] {
|
||||
const matches: CommandMatch[] = []
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
COMMAND_PATTERN.lastIndex = 0
|
||||
|
||||
while ((match = COMMAND_PATTERN.exec(text)) !== null) {
|
||||
matches.push({
|
||||
fullMatch: match[0],
|
||||
command: match[1],
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
})
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { exec } from "node:child_process"
|
||||
import { promisify } from "node:util"
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
type ExecError = { stdout?: Buffer; stderr?: Buffer; message?: string }
|
||||
|
||||
export async function executeCommand(command: string): Promise<string> {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command)
|
||||
|
||||
const out = stdout?.toString().trim() ?? ""
|
||||
const err = stderr?.toString().trim() ?? ""
|
||||
|
||||
if (err) {
|
||||
return out ? `${out}\n[stderr: ${err}]` : `[stderr: ${err}]`
|
||||
}
|
||||
|
||||
return out
|
||||
} catch (error: unknown) {
|
||||
const e = error as ExecError
|
||||
const stdout = e?.stdout?.toString().trim() ?? ""
|
||||
const stderr = e?.stderr?.toString().trim() ?? ""
|
||||
const errorMessage = stderr || e?.message || String(error)
|
||||
|
||||
return stdout ? `${stdout}\n[stderr: ${errorMessage}]` : `[stderr: ${errorMessage}]`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { spawn } from "node:child_process"
|
||||
import { getHomeDirectory } from "./home-directory"
|
||||
import { findBashPath, findZshPath } from "./shell-path"
|
||||
|
||||
export interface CommandResult {
|
||||
exitCode: number
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
}
|
||||
|
||||
export interface ExecuteHookOptions {
|
||||
forceZsh?: boolean
|
||||
zshPath?: string
|
||||
}
|
||||
|
||||
export async function executeHookCommand(
|
||||
command: string,
|
||||
stdin: string,
|
||||
cwd: string,
|
||||
options?: ExecuteHookOptions,
|
||||
): Promise<CommandResult> {
|
||||
const home = getHomeDirectory()
|
||||
|
||||
const expandedCommand = command
|
||||
.replace(/^~(?=\/|$)/g, home)
|
||||
.replace(/\s~(?=\/)/g, ` ${home}`)
|
||||
.replace(/\$CLAUDE_PROJECT_DIR/g, cwd)
|
||||
.replace(/\$\{CLAUDE_PROJECT_DIR\}/g, cwd)
|
||||
|
||||
let finalCommand = expandedCommand
|
||||
|
||||
if (options?.forceZsh) {
|
||||
const zshPath = findZshPath(options.zshPath)
|
||||
const escapedCommand = expandedCommand.replace(/'/g, "'\\''")
|
||||
if (zshPath) {
|
||||
finalCommand = `${zshPath} -lc '${escapedCommand}'`
|
||||
} else {
|
||||
const bashPath = findBashPath()
|
||||
if (bashPath) {
|
||||
finalCommand = `${bashPath} -lc '${escapedCommand}'`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn(finalCommand, {
|
||||
cwd,
|
||||
shell: true,
|
||||
env: { ...process.env, HOME: home, CLAUDE_PROJECT_DIR: cwd },
|
||||
})
|
||||
|
||||
let stdout = ""
|
||||
let stderr = ""
|
||||
|
||||
proc.stdout?.on("data", (data) => {
|
||||
stdout += data.toString()
|
||||
})
|
||||
|
||||
proc.stderr?.on("data", (data) => {
|
||||
stderr += data.toString()
|
||||
})
|
||||
|
||||
proc.stdin?.write(stdin)
|
||||
proc.stdin?.end()
|
||||
|
||||
proc.on("close", (code) => {
|
||||
resolve({
|
||||
exitCode: code ?? 0,
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
})
|
||||
})
|
||||
|
||||
proc.on("error", (err) => {
|
||||
resolve({ exitCode: 1, stderr: err.message })
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { homedir } from "node:os"
|
||||
|
||||
export function getHomeDirectory(): string {
|
||||
return process.env.HOME || process.env.USERPROFILE || homedir()
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { executeCommand } from "./execute-command"
|
||||
import { findEmbeddedCommands } from "./embedded-commands"
|
||||
|
||||
export async function resolveCommandsInText(
|
||||
text: string,
|
||||
depth: number = 0,
|
||||
maxDepth: number = 3,
|
||||
): Promise<string> {
|
||||
if (depth >= maxDepth) {
|
||||
return text
|
||||
}
|
||||
|
||||
const matches = findEmbeddedCommands(text)
|
||||
if (matches.length === 0) {
|
||||
return text
|
||||
}
|
||||
|
||||
const tasks = matches.map((m) => executeCommand(m.command))
|
||||
const results = await Promise.allSettled(tasks)
|
||||
|
||||
const replacements = new Map<string, string>()
|
||||
|
||||
matches.forEach((match, idx) => {
|
||||
const result = results[idx]
|
||||
if (result.status === "rejected") {
|
||||
replacements.set(
|
||||
match.fullMatch,
|
||||
`[error: ${
|
||||
result.reason instanceof Error
|
||||
? result.reason.message
|
||||
: String(result.reason)
|
||||
}]`,
|
||||
)
|
||||
} else {
|
||||
replacements.set(match.fullMatch, result.value)
|
||||
}
|
||||
})
|
||||
|
||||
let resolved = text
|
||||
for (const [pattern, replacement] of replacements.entries()) {
|
||||
resolved = resolved.split(pattern).join(replacement)
|
||||
}
|
||||
|
||||
if (findEmbeddedCommands(resolved).length > 0) {
|
||||
return resolveCommandsInText(resolved, depth + 1, maxDepth)
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { existsSync } from "node:fs"
|
||||
|
||||
const DEFAULT_ZSH_PATHS = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"]
|
||||
const DEFAULT_BASH_PATHS = ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash"]
|
||||
|
||||
function findShellPath(
|
||||
defaultPaths: string[],
|
||||
customPath?: string,
|
||||
): string | null {
|
||||
if (customPath && existsSync(customPath)) {
|
||||
return customPath
|
||||
}
|
||||
for (const path of defaultPaths) {
|
||||
if (existsSync(path)) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function findZshPath(customZshPath?: string): string | null {
|
||||
return findShellPath(DEFAULT_ZSH_PATHS, customZshPath)
|
||||
}
|
||||
|
||||
export function findBashPath(): string | null {
|
||||
return findShellPath(DEFAULT_BASH_PATHS)
|
||||
}
|
||||
Reference in New Issue
Block a user