UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

320 lines (319 loc) 13.9 kB
#!/usr/bin/env node import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { WorktreeManager } from "../../core/worktree/worktree-manager.js"; import { ProjectManager } from "../../core/projects/project-manager.js"; import { FrameManager } from "../../core/context/index.js"; import chalk from "chalk"; import Table from "cli-table3"; import { existsSync } from "fs"; import Database from "better-sqlite3"; import { execFileSync } from "child_process"; import { z } from "zod"; const BranchNameSchema = z.string().min(1, "Branch name cannot be empty").max(100, "Branch name too long").regex(/^[a-zA-Z0-9._/-]+$/, "Branch name contains invalid characters").refine((name) => !name.includes(".."), 'Branch name cannot contain ".."').refine((name) => !name.includes(";"), 'Branch name cannot contain ";"').refine((name) => !name.includes("&"), 'Branch name cannot contain "&"').refine((name) => !name.includes("|"), 'Branch name cannot contain "|"').refine((name) => !name.includes("$"), 'Branch name cannot contain "$"').refine((name) => !name.includes("`"), 'Branch name cannot contain "`"'); const PathSchema = z.string().min(1, "Path cannot be empty").max(500, "Path too long").regex(/^[a-zA-Z0-9._/-]+$/, "Path contains invalid characters").refine((path) => !path.includes(".."), 'Path cannot contain ".."'); const CommitSchema = z.string().min(1, "Commit reference cannot be empty").max(100, "Commit reference too long").regex(/^[a-zA-Z0-9._/-]+$/, "Commit reference contains invalid characters"); function registerWorktreeCommands(program) { const worktree = program.command("worktree").alias("wt").description("Manage StackMemory across git worktrees"); worktree.command("enable").description("Enable worktree support").option( "--isolate", "Isolate contexts between worktrees (default: true)", true ).option("--auto-detect", "Auto-detect worktrees (default: true)", true).option("--sync-interval <minutes>", "Context sync interval", "15").action(async (options) => { const manager = WorktreeManager.getInstance(); manager.saveConfig({ enabled: true, autoDetect: options.autoDetect, isolateContexts: options.isolate, shareGlobalContext: false, syncInterval: parseInt(options.syncInterval) }); console.log(chalk.green("\u2713 Worktree support enabled")); const worktrees = manager.detectWorktrees(); if (worktrees.length > 0) { console.log(chalk.cyan(` Detected ${worktrees.length} worktree(s):`)); worktrees.forEach((wt) => { const marker = wt.isMainWorktree ? " (main)" : ""; console.log(chalk.gray(` - ${wt.branch}${marker} at ${wt.path}`)); }); } }); worktree.command("disable").description("Disable worktree support").action(() => { const manager = WorktreeManager.getInstance(); manager.setEnabled(false); console.log(chalk.yellow("\u26A0 Worktree support disabled")); }); worktree.command("list").alias("ls").description("List all git worktrees with StackMemory status").option("-v, --verbose", "Show detailed information").action((options) => { const manager = WorktreeManager.getInstance(); const worktrees = manager.detectWorktrees(); if (worktrees.length === 0) { console.log(chalk.yellow("No worktrees found in current repository")); return; } const table = new Table({ head: ["Branch", "Path", "Type", "Context", "Last Activity"], style: { head: ["cyan"] } }); for (const wt of worktrees) { const type = wt.isMainWorktree ? "Main" : wt.isDetached ? "Detached" : "Branch"; let contextStatus = "\u2014"; let lastActivity = "\u2014"; try { const context = manager.getWorktreeContext(wt.path); if (existsSync(context.dbPath)) { contextStatus = "\u2713 Active"; const db = new Database(context.dbPath); const lastEvent = db.prepare("SELECT MAX(created_at) as last FROM events").get(); if (lastEvent?.last) { const date = new Date(lastEvent.last); lastActivity = date.toLocaleDateString(); } db.close(); } else { contextStatus = "\u25CB Not initialized"; } } catch (error) { contextStatus = "\u2717 Error"; } table.push([ wt.branch || "detached", options.verbose ? wt.path : `.../${wt.path.split("/").slice(-2).join("/")}`, type, contextStatus, lastActivity ]); } console.log(chalk.cyan("\nGit Worktrees:\n")); console.log(table.toString()); if (manager.isEnabled()) { console.log(chalk.gray("\n\u2713 Worktree support is enabled")); const config = manager.getConfig(); if (config.isolateContexts) { console.log( chalk.gray(" - Contexts are isolated between worktrees") ); } if (config.autoDetect) { console.log(chalk.gray(" - Auto-detection is enabled")); } } else { console.log(chalk.gray("\n\u25CB Worktree support is disabled")); console.log( chalk.gray(' Run "stackmemory worktree enable" to activate') ); } }); worktree.command("status").description("Show status of current worktree").action(async () => { const manager = WorktreeManager.getInstance(); const currentPath = process.cwd(); const worktrees = manager.detectWorktrees(currentPath); const current = worktrees.find( (w) => currentPath.startsWith(w.path) ); if (!current) { console.log(chalk.yellow("Not in a git worktree")); return; } console.log(chalk.cyan("Current Worktree:\n")); console.log(chalk.gray(" Branch:"), current.branch || "detached"); console.log(chalk.gray(" Path:"), current.path); console.log( chalk.gray(" Type:"), current.isMainWorktree ? "Main" : "Branch" ); console.log(chalk.gray(" Commit:"), current.commit.substring(0, 8)); if (manager.isEnabled()) { try { const context = manager.getWorktreeContext(current.path); console.log(chalk.gray(" Context Path:"), context.contextPath); if (existsSync(context.dbPath)) { const db = new Database(context.dbPath); const stats = db.prepare( ` SELECT (SELECT COUNT(*) FROM frames) as frames, (SELECT COUNT(*) FROM events) as events, (SELECT COUNT(*) FROM contexts) as contexts ` ).get(); console.log(chalk.cyan("\nContext Statistics:")); console.log(chalk.gray(" Frames:"), stats.frames); console.log(chalk.gray(" Events:"), stats.events); console.log(chalk.gray(" Contexts:"), stats.contexts); db.close(); } else { console.log(chalk.yellow("\nContext not initialized")); console.log(chalk.gray(' Run "stackmemory init" to initialize')); } } catch (error) { console.log( chalk.red("\nError accessing context:"), error.message ); } } else { console.log(chalk.gray("\nWorktree support is disabled")); } }); worktree.command("create <branch>").description("Create new git worktree with StackMemory context").option("-p, --path <path>", "Worktree path (default: ../repo-branch)").option("--from <commit>", "Create branch from commit/branch").option("--init", "Initialize StackMemory immediately").action(async (branch, options) => { const manager = WorktreeManager.getInstance(); const projectManager = ProjectManager.getInstance(); try { const validatedBranch = BranchNameSchema.parse(branch); const project = await projectManager.detectProject(); const worktreePath = options.path || `../${project.name}-${validatedBranch}`; if (options.path) { PathSchema.parse(options.path); } if (options.from) { CommitSchema.parse(options.from); } const gitArgs = ["worktree", "add", "-b", validatedBranch, worktreePath]; if (options.from) { gitArgs.push(options.from); } console.log(chalk.gray(`Creating worktree: git ${gitArgs.join(" ")}`)); execFileSync("git", gitArgs, { stdio: "inherit" }); console.log(chalk.green(`\u2713 Created worktree at ${worktreePath}`)); if (manager.isEnabled()) { const context = manager.getWorktreeContext(worktreePath); console.log( chalk.green(`\u2713 Created isolated context at ${context.contextPath}`) ); if (options.init) { const db = new Database(context.dbPath); new FrameManager(db, project.id); db.close(); console.log(chalk.green("\u2713 StackMemory initialized in worktree")); } } console.log(chalk.cyan("\nNext steps:")); console.log(chalk.gray(` cd ${worktreePath}`)); if (!options.init && manager.isEnabled()) { console.log(chalk.gray(" stackmemory init")); } console.log(chalk.gray(" # Start working in isolated context")); } catch (error) { if (error instanceof z.ZodError) { console.error(chalk.red("Invalid input:")); error.errors.forEach((err) => { console.error(chalk.red(` ${err.path.join(".")}: ${err.message}`)); }); } else { console.error( chalk.red("Failed to create worktree:"), error.message ); } process.exit(1); } }); worktree.command("sync").description("Sync contexts between worktrees").option("-s, --source <branch>", "Source worktree branch").option("-t, --target <branch>", "Target worktree branch").option("--type <type>", "Sync type: push|pull|merge (default: merge)").action(async (options) => { const manager = WorktreeManager.getInstance(); if (!manager.isEnabled()) { console.log(chalk.yellow("Worktree support is not enabled")); console.log(chalk.gray('Run "stackmemory worktree enable" first')); return; } const worktrees = manager.detectWorktrees(); let source = worktrees.find((w) => w.branch === options.source); let target = worktrees.find((w) => w.branch === options.target); if (!source || !target) { const inquirer = await import("inquirer"); if (!source) { const { sourceBranch } = await inquirer.default.prompt([ { type: "list", name: "sourceBranch", message: "Select source worktree:", choices: worktrees.map((w) => ({ name: `${w.branch} (${w.path})`, value: w })) } ]); source = sourceBranch; } if (!target) { const { targetBranch } = await inquirer.default.prompt([ { type: "list", name: "targetBranch", message: "Select target worktree:", choices: worktrees.filter((w) => w.path !== source.path).map((w) => ({ name: `${w.branch} (${w.path})`, value: w })) } ]); target = targetBranch; } } console.log(chalk.cyan("Syncing contexts:")); console.log(chalk.gray(" Source:"), source.branch); console.log(chalk.gray(" Target:"), target.branch); console.log(chalk.gray(" Type:"), options.type || "merge"); try { await manager.syncContexts( source.path, target.path, options.type || "merge" ); console.log(chalk.green("\u2713 Context sync completed")); } catch (error) { console.error(chalk.red("Sync failed:"), error.message); process.exit(1); } }); worktree.command("cleanup").description("Clean up stale worktree contexts").option("--dry-run", "Show what would be cleaned without doing it").action((options) => { const manager = WorktreeManager.getInstance(); if (options.dryRun) { console.log(chalk.yellow("Dry run - no changes will be made")); } console.log(chalk.cyan("Checking for stale worktree contexts...")); if (!options.dryRun) { manager.cleanupStaleContexts(); console.log(chalk.green("\u2713 Cleanup completed")); } else { const active = manager.detectWorktrees(); const stored = manager.listActiveWorktrees(); const activePaths = new Set(active.map((w) => w.path)); const stale = stored.filter((w) => !activePaths.has(w.path)); if (stale.length === 0) { console.log(chalk.green("No stale contexts found")); } else { console.log(chalk.yellow(`Found ${stale.length} stale context(s):`)); stale.forEach((w) => { console.log(chalk.gray(` - ${w.branch} at ${w.path}`)); }); } } }); worktree.command("switch <branch>").description("Switch to a different worktree").action(async (branch) => { const manager = WorktreeManager.getInstance(); const worktrees = manager.detectWorktrees(); const target = worktrees.find((w) => w.branch === branch); if (!target) { console.log(chalk.red(`Worktree '${branch}' not found`)); console.log(chalk.gray("\nAvailable worktrees:")); worktrees.forEach((w) => { console.log(chalk.gray(` - ${w.branch}`)); }); process.exit(1); } console.log(chalk.cyan(`Switching to worktree: ${branch}`)); console.log(chalk.gray(`Path: ${target.path}`)); console.log(chalk.gray("\nRun this command to switch:")); console.log(chalk.green(` cd ${target.path}`)); if (manager.isEnabled() && !target.isMainWorktree) { console.log(chalk.gray("\nThis worktree has an isolated context")); } }); } export { registerWorktreeCommands }; //# sourceMappingURL=worktree.js.map