@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
JavaScript
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