thought-cabinet 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -55,6 +55,10 @@ var _ConfigResolver = class _ConfigResolver {
55
55
  };
56
56
  _ConfigResolver.DEFAULT_CONFIG_FILE = "config.json";
57
57
  var ConfigResolver = _ConfigResolver;
58
+ function loadConfigFile(configFile) {
59
+ const resolver = new ConfigResolver({ configFile });
60
+ return resolver.loadConfigFile(configFile);
61
+ }
58
62
  function saveConfigFile(config, configFile) {
59
63
  const configPath = configFile || getDefaultConfigPath();
60
64
  console.log(chalk.yellow(`Writing config to ${configPath}`));
@@ -107,10 +111,10 @@ function isGitRepo(cwd) {
107
111
  return false;
108
112
  }
109
113
  }
110
- function getMainRepoPath() {
114
+ function getMainRepoPath(cwd) {
111
115
  try {
112
- const gitCommonDir = runGitCommand(["rev-parse", "--git-common-dir"]);
113
- const gitDir = runGitCommand(["rev-parse", "--git-dir"]);
116
+ const gitCommonDir = runGitCommand(["rev-parse", "--git-common-dir"], { cwd });
117
+ const gitDir = runGitCommand(["rev-parse", "--git-dir"], { cwd });
114
118
  if (gitCommonDir !== gitDir && gitCommonDir !== ".git") {
115
119
  const mainRepoPath = path2.dirname(path2.resolve(gitCommonDir));
116
120
  return mainRepoPath;
@@ -182,9 +186,63 @@ function hasUncommittedChanges(repoPath) {
182
186
  const status = runGitCommand(["status", "--porcelain"], { cwd: repoPath });
183
187
  return status.trim().length > 0;
184
188
  }
189
+ function hasUnmergedCommits(branch, targetBranch, cwd) {
190
+ try {
191
+ const count = runGitCommand(["rev-list", "--count", `${targetBranch}..${branch}`], { cwd });
192
+ return parseInt(count, 10) > 0;
193
+ } catch {
194
+ return true;
195
+ }
196
+ }
197
+ function branchExists(branch, cwd) {
198
+ try {
199
+ runGitCommand(["rev-parse", "--verify", branch], { cwd });
200
+ return true;
201
+ } catch {
202
+ return false;
203
+ }
204
+ }
205
+ function getDefaultBranch(cwd) {
206
+ try {
207
+ const remoteBranch = runGitCommand(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd });
208
+ return remoteBranch.replace("refs/remotes/origin/", "");
209
+ } catch {
210
+ }
211
+ if (branchExists("main", cwd)) return "main";
212
+ if (branchExists("master", cwd)) return "master";
213
+ return "main";
214
+ }
185
215
  function setBranchBase(branch, base, cwd) {
186
216
  runGitCommandOrThrow(["config", "--local", `branch.${branch}.thc-base`, base], { cwd });
187
217
  }
218
+ function getCurrentBranch(cwd) {
219
+ try {
220
+ return runGitCommand(["branch", "--show-current"], { cwd });
221
+ } catch {
222
+ try {
223
+ return runGitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd });
224
+ } catch {
225
+ return "";
226
+ }
227
+ }
228
+ }
229
+ function getCurrentCommit(cwd) {
230
+ try {
231
+ return runGitCommand(["rev-parse", "HEAD"], { cwd });
232
+ } catch {
233
+ return "";
234
+ }
235
+ }
236
+ function getWorktreeRoot(cwd) {
237
+ return runGitCommand(["rev-parse", "--show-toplevel"], { cwd });
238
+ }
239
+ function getRepoRoot(cwd) {
240
+ const mainRepoPath = getMainRepoPath(cwd);
241
+ if (mainRepoPath) {
242
+ return mainRepoPath;
243
+ }
244
+ return getWorktreeRoot(cwd);
245
+ }
188
246
 
189
247
  // src/commands/thoughts/utils/paths.ts
190
248
  function getDefaultThoughtsRepo() {
@@ -2123,8 +2181,8 @@ async function profileDeleteCommand(profileName, options) {
2123
2181
  }
2124
2182
 
2125
2183
  // src/commands/thoughts.ts
2126
- function thoughtsCommand(program2) {
2127
- const cmd = program2;
2184
+ function thoughtsCommand(program) {
2185
+ const cmd = program;
2128
2186
  cmd.command("init").description("Initialize thoughts for current repository").option("--force", "Force reconfiguration even if already set up").option("--config-file <path>", "Path to config file").option(
2129
2187
  "--directory <name>",
2130
2188
  "Specify the repository directory name (skips interactive prompt)"
@@ -2513,8 +2571,8 @@ function getAgentProduct(id) {
2513
2571
  }
2514
2572
 
2515
2573
  // src/commands/agent.ts
2516
- function agentCommand(program2) {
2517
- const agent = program2.command("agent").description("Manage coding agent configuration");
2574
+ function agentCommand(program) {
2575
+ const agent = program.command("agent").description("Manage coding agent configuration");
2518
2576
  agent.command("init").description("Initialize coding agent configuration in current directory").option("--force", "Force overwrite of existing agent directory").option("--all", "Copy all files without prompting").option(
2519
2577
  "--max-thinking-tokens <number>",
2520
2578
  "Maximum thinking tokens (default: 32000)",
@@ -2526,43 +2584,16 @@ function agentCommand(program2) {
2526
2584
  }
2527
2585
 
2528
2586
  // src/commands/metadata/metadata.ts
2529
- import { execSync as execSync7 } from "child_process";
2587
+ import path15 from "path";
2530
2588
  function getGitInfo() {
2589
+ if (!isGitRepo()) {
2590
+ return null;
2591
+ }
2531
2592
  try {
2532
- execSync7("git rev-parse --is-inside-work-tree", {
2533
- encoding: "utf8",
2534
- stdio: "pipe"
2535
- });
2536
- const repoRoot = execSync7("git rev-parse --show-toplevel", {
2537
- encoding: "utf8",
2538
- stdio: "pipe"
2539
- }).trim();
2540
- const repoName = repoRoot.split("/").pop() || "";
2541
- let branch = "";
2542
- try {
2543
- branch = execSync7("git branch --show-current", {
2544
- encoding: "utf8",
2545
- stdio: "pipe"
2546
- }).trim();
2547
- } catch {
2548
- try {
2549
- branch = execSync7("git rev-parse --abbrev-ref HEAD", {
2550
- encoding: "utf8",
2551
- stdio: "pipe"
2552
- }).trim();
2553
- } catch {
2554
- branch = "";
2555
- }
2556
- }
2557
- let commit = "";
2558
- try {
2559
- commit = execSync7("git rev-parse HEAD", {
2560
- encoding: "utf8",
2561
- stdio: "pipe"
2562
- }).trim();
2563
- } catch {
2564
- commit = "";
2565
- }
2593
+ const repoRoot = getRepoRoot();
2594
+ const repoName = path15.basename(repoRoot);
2595
+ const branch = getCurrentBranch();
2596
+ const commit = getCurrentCommit();
2566
2597
  return { repoRoot, repoName, branch, commit };
2567
2598
  } catch {
2568
2599
  return null;
@@ -2608,13 +2639,13 @@ async function specMetadataCommand() {
2608
2639
  }
2609
2640
 
2610
2641
  // src/commands/metadata.ts
2611
- function metadataCommand(program2) {
2612
- program2.command("metadata").description("Output metadata for current repository (branch, commit, timestamp, etc.)").action(specMetadataCommand);
2642
+ function metadataCommand(program) {
2643
+ program.command("metadata").description("Output metadata for current repository (branch, commit, timestamp, etc.)").action(specMetadataCommand);
2613
2644
  }
2614
2645
 
2615
- // src/commands/worktree.ts
2646
+ // src/commands/worktree/add.ts
2616
2647
  import fs14 from "fs";
2617
- import path16 from "path";
2648
+ import path17 from "path";
2618
2649
  import chalk16 from "chalk";
2619
2650
 
2620
2651
  // src/tmux.ts
@@ -2663,15 +2694,15 @@ function tmuxKillSession(sessionName) {
2663
2694
 
2664
2695
  // src/agent-config.ts
2665
2696
  import fs13 from "fs";
2666
- import path15 from "path";
2697
+ import path16 from "path";
2667
2698
  var AGENT_CONFIG_DIRS = [".claude", ".codebuddy"];
2668
2699
  function copyAgentConfigDirs(options) {
2669
2700
  const { sourceDir, targetDir, configDirs = AGENT_CONFIG_DIRS } = options;
2670
2701
  const copied = [];
2671
2702
  const skipped = [];
2672
2703
  for (const dirName of configDirs) {
2673
- const sourcePath = path15.join(sourceDir, dirName);
2674
- const targetPath = path15.join(targetDir, dirName);
2704
+ const sourcePath = path16.join(sourceDir, dirName);
2705
+ const targetPath = path16.join(targetDir, dirName);
2675
2706
  if (!fs13.existsSync(sourcePath)) {
2676
2707
  skipped.push(dirName);
2677
2708
  continue;
@@ -2682,342 +2713,485 @@ function copyAgentConfigDirs(options) {
2682
2713
  return { copied, skipped };
2683
2714
  }
2684
2715
 
2685
- // src/commands/worktree.ts
2686
- function worktreeCommand(program2) {
2687
- const wt = program2.command("worktree").description("Manage git worktrees bound to tmux sessions");
2688
- wt.command("add <name>").description("Create a git worktree and a tmux session for it").option("--branch <branch>", "Branch name (defaults to <name>)").option("--base <ref>", "Base ref/commit (default: HEAD)", "HEAD").option("--path <path>", "Worktree directory path (default: ../<repo>__worktrees/<name>)").option("--detached", "Create a detached worktree at <base> (no branch)").option("--no-thoughts", "Skip thoughts initialization").action(async (name, options) => {
2689
- try {
2690
- validateWorktreeHandle(name);
2691
- if (!isGitRepo()) {
2692
- console.error(chalk16.red("Error: not in a git repository"));
2693
- process.exit(1);
2694
- }
2695
- const mainRoot = getMainWorktreeRoot();
2696
- const baseDir = getWorktreesBaseDir(mainRoot);
2697
- const worktreePath = options.path ? path16.resolve(options.path) : path16.join(baseDir, name);
2698
- fs14.mkdirSync(path16.dirname(worktreePath), { recursive: true });
2699
- const sessionName = sessionNameForHandle(name);
2700
- const sessionCandidates = allSessionNamesForHandle(name);
2701
- const existing = sessionCandidates.find((s) => tmuxHasSession(s));
2702
- if (existing) {
2703
- console.error(chalk16.red(`Error: tmux session already exists: ${existing}`));
2704
- process.exit(1);
2705
- }
2706
- const hooksConfig = loadHooksConfig(mainRoot);
2707
- const preAddHooks = getHooksForEvent(hooksConfig, "PreWorktreeAdd");
2708
- if (preAddHooks.length > 0) {
2709
- const hookInput = {
2716
+ // src/commands/worktree/add.ts
2717
+ async function worktreeAddCommand(name, options) {
2718
+ try {
2719
+ validateWorktreeHandle(name);
2720
+ if (!isGitRepo()) {
2721
+ console.error(chalk16.red("Error: not in a git repository"));
2722
+ process.exit(1);
2723
+ }
2724
+ const mainRoot = getMainWorktreeRoot();
2725
+ const baseDir = getWorktreesBaseDir(mainRoot);
2726
+ const worktreePath = options.path ? path17.resolve(options.path) : path17.join(baseDir, name);
2727
+ const branch = options.detached ? "" : options.branch ?? name;
2728
+ const sessionName = sessionNameForHandle(name);
2729
+ fs14.mkdirSync(path17.dirname(worktreePath), { recursive: true });
2730
+ const sessionCandidates = allSessionNamesForHandle(name);
2731
+ const existingSession = sessionCandidates.find((s) => tmuxHasSession(s));
2732
+ if (existingSession) {
2733
+ console.error(chalk16.red(`Error: tmux session already exists: ${existingSession}`));
2734
+ process.exit(1);
2735
+ }
2736
+ const hooksConfig = loadHooksConfig(mainRoot);
2737
+ const preAddHooks = getHooksForEvent(hooksConfig, "PreWorktreeAdd");
2738
+ if (preAddHooks.length > 0) {
2739
+ const hookEnv = {
2740
+ THC_WORKTREE_PATH: worktreePath,
2741
+ THC_WORKTREE_NAME: name,
2742
+ THC_WORKTREE_BRANCH: branch,
2743
+ THC_MAIN_ROOT: mainRoot,
2744
+ THC_SESSION_NAME: sessionName,
2745
+ THC_BASE_REF: options.base
2746
+ };
2747
+ await executeHooks(
2748
+ preAddHooks,
2749
+ {
2710
2750
  hook_event_name: "PreWorktreeAdd",
2711
2751
  cwd: mainRoot,
2712
2752
  worktree_path: worktreePath,
2713
2753
  worktree_name: name,
2714
- worktree_branch: options.detached ? "" : options.branch ?? name,
2754
+ worktree_branch: branch,
2715
2755
  main_root: mainRoot,
2716
2756
  session_name: sessionName,
2717
2757
  base_ref: options.base
2718
- };
2719
- const hookEnv = {
2720
- THC_WORKTREE_PATH: worktreePath,
2721
- THC_WORKTREE_NAME: name,
2722
- THC_WORKTREE_BRANCH: hookInput.worktree_branch,
2723
- THC_MAIN_ROOT: mainRoot,
2724
- THC_SESSION_NAME: sessionName,
2725
- THC_BASE_REF: options.base
2726
- };
2727
- await executeHooks(preAddHooks, hookInput, hookEnv, true);
2728
- }
2729
- if (options.detached) {
2730
- runGitCommandOrThrow(["worktree", "add", "--detach", worktreePath, options.base], {
2731
- cwd: mainRoot
2732
- });
2733
- } else {
2734
- const branch = options.branch ?? name;
2735
- runGitCommandOrThrow(["worktree", "add", "-b", branch, worktreePath, options.base], {
2736
- cwd: mainRoot
2737
- });
2738
- setBranchBase(branch, options.base, worktreePath);
2739
- }
2740
- tmuxNewSession(sessionName, worktreePath);
2741
- const configResult = copyAgentConfigDirs({
2742
- sourceDir: mainRoot,
2743
- targetDir: worktreePath
2758
+ },
2759
+ hookEnv,
2760
+ true
2761
+ );
2762
+ }
2763
+ if (options.detached) {
2764
+ runGitCommandOrThrow(["worktree", "add", "--detach", worktreePath, options.base], {
2765
+ cwd: mainRoot
2744
2766
  });
2745
- if (configResult.copied.length > 0) {
2746
- console.log(chalk16.gray(`Copied config: ${configResult.copied.join(", ")}`));
2747
- }
2748
- if (options.thoughts !== false) {
2749
- const config = loadThoughtsConfig({});
2750
- if (config) {
2751
- const mainRepoMapping = config.repoMappings[mainRoot];
2752
- const mappedName = getRepoNameFromMapping(mainRepoMapping);
2753
- if (mappedName) {
2754
- config.repoMappings[worktreePath] = mainRepoMapping;
2755
- saveThoughtsConfig(config, {});
2756
- const profileConfig = resolveProfileForRepo(config, worktreePath);
2757
- createThoughtsDirectoryStructure(profileConfig, mappedName, config.user);
2758
- const result = setupThoughtsDirectory({
2759
- repoPath: worktreePath,
2760
- profileConfig,
2761
- mappedName,
2762
- user: config.user,
2763
- createSearchable: true,
2764
- setupHooks: true
2765
- });
2766
- console.log(chalk16.gray("Thoughts initialized"));
2767
- if (result.hooksUpdated.length > 0) {
2768
- console.log(chalk16.gray(`Updated git hooks: ${result.hooksUpdated.join(", ")}`));
2769
- }
2770
- if (pullThoughtsFromRemote(profileConfig.thoughtsRepo)) {
2771
- console.log(chalk16.gray("Pulled latest thoughts from remote"));
2772
- }
2773
- } else {
2774
- console.log(chalk16.yellow("Main repo not configured for thoughts, skipping"));
2775
- }
2776
- } else {
2777
- console.log(chalk16.yellow("Thoughts not configured globally, skipping"));
2778
- }
2779
- }
2780
- const postAddHooks = getHooksForEvent(hooksConfig, "PostWorktreeAdd");
2781
- if (postAddHooks.length > 0) {
2782
- const hookInput = {
2767
+ } else {
2768
+ runGitCommandOrThrow(["worktree", "add", "-b", branch, worktreePath, options.base], {
2769
+ cwd: mainRoot
2770
+ });
2771
+ setBranchBase(branch, options.base, worktreePath);
2772
+ }
2773
+ tmuxNewSession(sessionName, worktreePath);
2774
+ const configResult = copyAgentConfigDirs({
2775
+ sourceDir: mainRoot,
2776
+ targetDir: worktreePath
2777
+ });
2778
+ if (configResult.copied.length > 0) {
2779
+ console.log(chalk16.gray(`Copied config: ${configResult.copied.join(", ")}`));
2780
+ }
2781
+ if (options.thoughts !== false) {
2782
+ initializeWorktreeThoughts(mainRoot, worktreePath);
2783
+ }
2784
+ const postAddHooks = getHooksForEvent(hooksConfig, "PostWorktreeAdd");
2785
+ if (postAddHooks.length > 0) {
2786
+ const hookEnv = {
2787
+ THC_WORKTREE_PATH: worktreePath,
2788
+ THC_WORKTREE_NAME: name,
2789
+ THC_WORKTREE_BRANCH: branch,
2790
+ THC_MAIN_ROOT: mainRoot,
2791
+ THC_SESSION_NAME: sessionName
2792
+ };
2793
+ await executeHooks(
2794
+ postAddHooks,
2795
+ {
2783
2796
  hook_event_name: "PostWorktreeAdd",
2784
2797
  cwd: worktreePath,
2785
2798
  worktree_path: worktreePath,
2786
2799
  worktree_name: name,
2787
- worktree_branch: options.detached ? "" : options.branch ?? name,
2800
+ worktree_branch: branch,
2788
2801
  main_root: mainRoot,
2789
2802
  session_name: sessionName
2790
- };
2791
- const hookEnv = {
2792
- THC_WORKTREE_PATH: worktreePath,
2793
- THC_WORKTREE_NAME: name,
2794
- THC_WORKTREE_BRANCH: hookInput.worktree_branch,
2795
- THC_MAIN_ROOT: mainRoot,
2796
- THC_SESSION_NAME: sessionName
2797
- };
2798
- await executeHooks(postAddHooks, hookInput, hookEnv, true);
2799
- }
2800
- console.log(chalk16.green("\n\u2713 Worktree created"));
2801
- console.log(chalk16.gray(`Path: ${worktreePath}`));
2802
- console.log(chalk16.gray(`Tmux session: ${sessionName}`));
2803
- console.log(chalk16.gray(`Attach: tmux attach -t ${sessionName}`));
2804
- } catch (error) {
2805
- console.error(chalk16.red(`Error: ${error.message}`));
2806
- process.exit(1);
2803
+ },
2804
+ hookEnv,
2805
+ true
2806
+ );
2807
2807
  }
2808
+ console.log(chalk16.green("\n\u2713 Worktree created"));
2809
+ console.log(chalk16.gray(`Path: ${worktreePath}`));
2810
+ console.log(chalk16.gray(`Tmux session: ${sessionName}`));
2811
+ console.log(chalk16.gray(`Attach: tmux attach -t ${sessionName}`));
2812
+ } catch (error) {
2813
+ console.error(chalk16.red(`Error: ${error.message}`));
2814
+ process.exit(1);
2815
+ }
2816
+ }
2817
+ function initializeWorktreeThoughts(mainRoot, worktreePath) {
2818
+ const config = loadThoughtsConfig({});
2819
+ if (!config) {
2820
+ console.log(chalk16.yellow("Thoughts not configured globally, skipping"));
2821
+ return;
2822
+ }
2823
+ const mainRepoMapping = config.repoMappings[mainRoot];
2824
+ const mappedName = getRepoNameFromMapping(mainRepoMapping);
2825
+ if (!mappedName) {
2826
+ console.log(chalk16.yellow("Main repo not configured for thoughts, skipping"));
2827
+ return;
2828
+ }
2829
+ config.repoMappings[worktreePath] = mainRepoMapping;
2830
+ saveThoughtsConfig(config, {});
2831
+ const profileConfig = resolveProfileForRepo(config, worktreePath);
2832
+ createThoughtsDirectoryStructure(profileConfig, mappedName, config.user);
2833
+ const result = setupThoughtsDirectory({
2834
+ repoPath: worktreePath,
2835
+ profileConfig,
2836
+ mappedName,
2837
+ user: config.user,
2838
+ createSearchable: true,
2839
+ setupHooks: true
2808
2840
  });
2809
- wt.command("list").description("List thc-managed worktrees and their tmux sessions").option("--all", "Show all git worktrees (not just ../<repo>__worktrees)").action(async (options) => {
2810
- try {
2811
- if (!isGitRepo()) {
2812
- console.error(chalk16.red("Error: not in a git repository"));
2813
- process.exit(1);
2814
- }
2815
- const mainRoot = getMainWorktreeRoot();
2816
- const baseDir = getWorktreesBaseDir(mainRoot);
2817
- const baseDirResolved = path16.resolve(baseDir);
2818
- const entries = parseWorktreeListPorcelain(
2819
- runGitCommand(["worktree", "list", "--porcelain"], { cwd: mainRoot })
2820
- );
2821
- const sessions = new Set(listTmuxSessions());
2822
- const filtered = options.all ? entries : entries.filter((e) => {
2823
- const p5 = path16.resolve(e.worktreePath);
2824
- return p5 === path16.resolve(mainRoot) || p5.startsWith(baseDirResolved + path16.sep);
2825
- });
2826
- if (filtered.length === 0) {
2827
- console.log(chalk16.gray("No worktrees found."));
2828
- return;
2829
- }
2830
- const cwd = process.cwd();
2831
- const rows = filtered.map((e) => {
2832
- const name = path16.basename(e.worktreePath);
2833
- const sessionName = allSessionNamesForHandle(name).find((s) => sessions.has(s)) ?? "-";
2834
- const isCurrent = path16.resolve(e.worktreePath) === path16.resolve(cwd);
2835
- return {
2836
- name: isCurrent ? `* ${name}` : ` ${name}`,
2837
- branch: e.branch,
2838
- tmux: sessionName,
2839
- path: e.worktreePath,
2840
- isCurrent
2841
- };
2842
- });
2843
- const colWidths = {
2844
- name: Math.max("NAME".length + 2, ...rows.map((r) => r.name.length)),
2845
- branch: Math.max("BRANCH".length, ...rows.map((r) => r.branch.length)),
2846
- tmux: Math.max("TMUX".length, ...rows.map((r) => r.tmux.length))
2841
+ console.log(chalk16.gray("Thoughts initialized"));
2842
+ if (result.hooksUpdated.length > 0) {
2843
+ console.log(chalk16.gray(`Updated git hooks: ${result.hooksUpdated.join(", ")}`));
2844
+ }
2845
+ if (pullThoughtsFromRemote(profileConfig.thoughtsRepo)) {
2846
+ console.log(chalk16.gray("Pulled latest thoughts from remote"));
2847
+ }
2848
+ }
2849
+
2850
+ // src/commands/worktree/list.ts
2851
+ import path18 from "path";
2852
+ import chalk17 from "chalk";
2853
+ async function worktreeListCommand(options) {
2854
+ try {
2855
+ if (!isGitRepo()) {
2856
+ console.error(chalk17.red("Error: not in a git repository"));
2857
+ process.exit(1);
2858
+ }
2859
+ const mainRoot = getMainWorktreeRoot();
2860
+ const baseDir = path18.resolve(getWorktreesBaseDir(mainRoot));
2861
+ const cwd = path18.resolve(process.cwd());
2862
+ const entries = parseWorktreeListPorcelain(
2863
+ runGitCommand(["worktree", "list", "--porcelain"], { cwd: mainRoot })
2864
+ );
2865
+ const sessions = new Set(listTmuxSessions());
2866
+ const filtered = options.all ? entries : entries.filter((e) => {
2867
+ const p5 = path18.resolve(e.worktreePath);
2868
+ return p5 === path18.resolve(mainRoot) || p5.startsWith(baseDir + path18.sep);
2869
+ });
2870
+ if (filtered.length === 0) {
2871
+ console.log(chalk17.gray("No worktrees found."));
2872
+ return;
2873
+ }
2874
+ const rows = filtered.map((e) => {
2875
+ const name = path18.basename(e.worktreePath);
2876
+ const isCurrent = path18.resolve(e.worktreePath) === cwd;
2877
+ return {
2878
+ name: isCurrent ? `* ${name}` : ` ${name}`,
2879
+ branch: e.branch,
2880
+ tmux: allSessionNamesForHandle(name).find((s) => sessions.has(s)) ?? "-",
2881
+ path: e.worktreePath,
2882
+ isCurrent
2847
2883
  };
2848
- console.log(
2849
- chalk16.blue(
2850
- `${" NAME".padEnd(colWidths.name)} ${"BRANCH".padEnd(colWidths.branch)} ${"TMUX".padEnd(colWidths.tmux)} PATH`
2851
- )
2852
- );
2853
- for (const row of rows) {
2854
- const line = `${row.name.padEnd(colWidths.name)} ${row.branch.padEnd(colWidths.branch)} ${row.tmux.padEnd(colWidths.tmux)} ${row.path}`;
2855
- console.log(row.isCurrent ? chalk16.green(line) : line);
2856
- }
2857
- } catch (error) {
2858
- console.error(chalk16.red(`Error: ${error.message}`));
2884
+ });
2885
+ const colWidths = {
2886
+ name: Math.max(" NAME".length, ...rows.map((r) => r.name.length)),
2887
+ branch: Math.max("BRANCH".length, ...rows.map((r) => r.branch.length)),
2888
+ tmux: Math.max("TMUX".length, ...rows.map((r) => r.tmux.length))
2889
+ };
2890
+ const header = `${" NAME".padEnd(colWidths.name)} ${"BRANCH".padEnd(colWidths.branch)} ${"TMUX".padEnd(colWidths.tmux)} PATH`;
2891
+ console.log(chalk17.blue(header));
2892
+ for (const row of rows) {
2893
+ const line = `${row.name.padEnd(colWidths.name)} ${row.branch.padEnd(colWidths.branch)} ${row.tmux.padEnd(colWidths.tmux)} ${row.path}`;
2894
+ console.log(row.isCurrent ? chalk17.green(line) : line);
2895
+ }
2896
+ } catch (error) {
2897
+ console.error(chalk17.red(`Error: ${error.message}`));
2898
+ process.exit(1);
2899
+ }
2900
+ }
2901
+
2902
+ // src/commands/worktree/merge.ts
2903
+ import path20 from "path";
2904
+ import chalk19 from "chalk";
2905
+
2906
+ // src/commands/worktree/utils.ts
2907
+ import path19 from "path";
2908
+ import chalk18 from "chalk";
2909
+ function cleanupWorktreeThoughts(wtPath, options = {}) {
2910
+ const config = loadThoughtsConfig({});
2911
+ if (!config || !config.repoMappings[wtPath]) {
2912
+ return;
2913
+ }
2914
+ try {
2915
+ if (options.verbose) {
2916
+ console.log(chalk18.gray("Cleaning up thoughts directory..."));
2917
+ }
2918
+ const result = cleanupThoughtsDirectory({
2919
+ repoPath: wtPath,
2920
+ config,
2921
+ force: options.force,
2922
+ verbose: false
2923
+ });
2924
+ if (result.configRemoved) {
2925
+ saveThoughtsConfig(config, {});
2926
+ }
2927
+ if (result.thoughtsRemoved && options.verbose) {
2928
+ console.log(chalk18.gray("\u2713 Thoughts directory cleaned up"));
2929
+ }
2930
+ } catch (error) {
2931
+ if (options.verbose) {
2932
+ console.log(chalk18.yellow(`Warning: Could not clean up thoughts: ${error.message}`));
2933
+ }
2934
+ }
2935
+ }
2936
+ function cleanupWorktreeTmuxSession(wtPath) {
2937
+ const handle = path19.basename(wtPath);
2938
+ const sessionNames = allSessionNamesForHandle(handle);
2939
+ for (const s of sessionNames) {
2940
+ tmuxKillSession(s);
2941
+ }
2942
+ }
2943
+ function removeGitWorktree(wtPath, mainRoot, options = {}) {
2944
+ const removeArgs = ["worktree", "remove"];
2945
+ if (options.force) {
2946
+ removeArgs.push("--force");
2947
+ }
2948
+ removeArgs.push(wtPath);
2949
+ runGitCommandOrThrow(removeArgs, { cwd: mainRoot });
2950
+ try {
2951
+ runGitCommandOrThrow(["worktree", "prune"], { cwd: mainRoot });
2952
+ } catch {
2953
+ }
2954
+ }
2955
+ function deleteWorktreeBranch(branch, mainRoot, options = {}) {
2956
+ try {
2957
+ runGitCommandOrThrow(["branch", "-d", branch], { cwd: mainRoot });
2958
+ } catch {
2959
+ if (options.force) {
2960
+ runGitCommandOrThrow(["branch", "-D", branch], { cwd: mainRoot });
2961
+ } else {
2962
+ throw new Error(`Failed to delete branch '${branch}'. Re-run with --force to force delete.`);
2963
+ }
2964
+ }
2965
+ }
2966
+
2967
+ // src/commands/worktree/merge.ts
2968
+ async function worktreeMergeCommand(name, options) {
2969
+ try {
2970
+ if (!isGitRepo()) {
2971
+ console.error(chalk19.red("Error: not in a git repository"));
2859
2972
  process.exit(1);
2860
2973
  }
2861
- });
2862
- wt.command("merge <name>").description(
2863
- "Rebase worktree branch onto target, ff-merge, then clean up worktree + tmux session"
2864
- ).option(
2865
- "--into <branch>",
2866
- "Target branch to merge into (default: current branch in main worktree)"
2867
- ).option("--force", "Force cleanup even if uncommitted changes exist").option("--keep-session", "Do not kill the tmux session").option("--keep-worktree", "Do not remove the git worktree").option("--keep-branch", "Do not delete the source branch").action(async (name, options) => {
2868
- try {
2869
- if (!isGitRepo()) {
2870
- console.error(chalk16.red("Error: not in a git repository"));
2871
- process.exit(1);
2872
- }
2873
- const mainRoot = getMainWorktreeRoot();
2874
- const mainRootResolved = path16.resolve(mainRoot);
2875
- const wtEntry = findWorktree(name, mainRoot);
2876
- const wtResolved = path16.resolve(wtEntry.worktreePath);
2877
- if (wtResolved === mainRootResolved) {
2878
- console.error(chalk16.red("Error: refusing to merge/remove the main worktree"));
2879
- process.exit(1);
2880
- }
2881
- if (wtEntry.detached || wtEntry.branch === "(detached)") {
2882
- console.error(chalk16.red("Error: cannot merge a detached worktree"));
2883
- process.exit(1);
2884
- }
2885
- const targetBranch = options.into ?? runGitCommand(["branch", "--show-current"], { cwd: mainRoot });
2886
- if (!targetBranch) {
2887
- console.error(chalk16.red("Error: could not determine target branch. Use --into <branch>."));
2888
- process.exit(1);
2889
- }
2890
- if (targetBranch === wtEntry.branch) {
2891
- console.error(chalk16.red("Error: source and target branch are the same"));
2892
- process.exit(1);
2893
- }
2894
- const mergeHooksConfig = loadHooksConfig(mainRoot);
2895
- const preMergeHooks = getHooksForEvent(mergeHooksConfig, "PreWorktreeMerge");
2896
- if (preMergeHooks.length > 0) {
2897
- const hookInput = {
2974
+ const mainRoot = getMainWorktreeRoot();
2975
+ const wtEntry = findWorktree(name, mainRoot);
2976
+ const wtPath = path20.resolve(wtEntry.worktreePath);
2977
+ if (wtPath === path20.resolve(mainRoot)) {
2978
+ console.error(chalk19.red("Error: refusing to merge/remove the main worktree"));
2979
+ process.exit(1);
2980
+ }
2981
+ if (wtEntry.detached || wtEntry.branch === "(detached)") {
2982
+ console.error(chalk19.red("Error: cannot merge a detached worktree"));
2983
+ process.exit(1);
2984
+ }
2985
+ const targetBranch = options.into ?? runGitCommand(["branch", "--show-current"], { cwd: mainRoot });
2986
+ if (!targetBranch) {
2987
+ console.error(chalk19.red("Error: could not determine target branch. Use --into <branch>."));
2988
+ process.exit(1);
2989
+ }
2990
+ if (targetBranch === wtEntry.branch) {
2991
+ console.error(chalk19.red("Error: source and target branch are the same"));
2992
+ process.exit(1);
2993
+ }
2994
+ const hooksConfig = loadHooksConfig(mainRoot);
2995
+ const preHooks = getHooksForEvent(hooksConfig, "PreWorktreeMerge");
2996
+ if (preHooks.length > 0) {
2997
+ const hookEnv = {
2998
+ THC_WORKTREE_PATH: wtPath,
2999
+ THC_WORKTREE_NAME: name,
3000
+ THC_WORKTREE_BRANCH: wtEntry.branch,
3001
+ THC_TARGET_BRANCH: targetBranch,
3002
+ THC_MAIN_ROOT: mainRoot
3003
+ };
3004
+ await executeHooks(
3005
+ preHooks,
3006
+ {
2898
3007
  hook_event_name: "PreWorktreeMerge",
2899
3008
  cwd: mainRoot,
2900
- worktree_path: wtResolved,
3009
+ worktree_path: wtPath,
2901
3010
  worktree_name: name,
2902
3011
  worktree_branch: wtEntry.branch,
2903
3012
  target_branch: targetBranch,
2904
3013
  main_root: mainRoot
2905
- };
2906
- const hookEnv = {
2907
- THC_WORKTREE_PATH: wtResolved,
2908
- THC_WORKTREE_NAME: name,
2909
- THC_WORKTREE_BRANCH: wtEntry.branch,
2910
- THC_TARGET_BRANCH: targetBranch,
2911
- THC_MAIN_ROOT: mainRoot
2912
- };
2913
- await executeHooks(preMergeHooks, hookInput, hookEnv, true);
2914
- }
2915
- const config = loadThoughtsConfig({});
2916
- if (config && config.repoMappings[wtResolved]) {
2917
- try {
2918
- console.log(chalk16.gray("Cleaning up thoughts directory..."));
2919
- const result = cleanupThoughtsDirectory({
2920
- repoPath: wtResolved,
2921
- config,
2922
- force: options.force,
2923
- verbose: false
2924
- // Suppress detailed output during merge
2925
- });
2926
- if (result.configRemoved) {
2927
- saveThoughtsConfig(config, {});
2928
- }
2929
- if (result.thoughtsRemoved) {
2930
- console.log(chalk16.gray("\u2713 Thoughts directory cleaned up"));
2931
- }
2932
- } catch (error) {
2933
- console.log(
2934
- chalk16.yellow(`Warning: Could not clean up thoughts: ${error.message}`)
2935
- );
2936
- }
2937
- }
2938
- if (!options.force && hasUncommittedChanges(wtEntry.worktreePath)) {
3014
+ },
3015
+ hookEnv,
3016
+ true
3017
+ );
3018
+ }
3019
+ cleanupWorktreeThoughts(wtPath, { force: options.force, verbose: true });
3020
+ if (!options.force && hasUncommittedChanges(wtEntry.worktreePath)) {
3021
+ console.error(
3022
+ chalk19.red("Error: worktree has uncommitted changes. Commit/stash first or use --force.")
3023
+ );
3024
+ process.exit(1);
3025
+ }
3026
+ console.log(chalk19.blue(`Rebasing ${wtEntry.branch} onto ${targetBranch}...`));
3027
+ runGitCommandOrThrow(["rebase", targetBranch], { cwd: wtEntry.worktreePath });
3028
+ console.log(chalk19.blue(`Fast-forward merging into ${targetBranch}...`));
3029
+ runGitCommandOrThrow(["switch", targetBranch], { cwd: mainRoot });
3030
+ runGitCommandOrThrow(["merge", "--ff-only", wtEntry.branch], { cwd: mainRoot });
3031
+ if (!options.keepSession) {
3032
+ cleanupWorktreeTmuxSession(wtPath);
3033
+ }
3034
+ if (!options.keepWorktree) {
3035
+ removeGitWorktree(wtPath, mainRoot, { force: options.force });
3036
+ }
3037
+ if (!options.keepBranch) {
3038
+ deleteWorktreeBranch(wtEntry.branch, mainRoot, { force: options.force });
3039
+ }
3040
+ const postHooks = getHooksForEvent(hooksConfig, "PostWorktreeMerge");
3041
+ if (postHooks.length > 0) {
3042
+ const hookEnv = {
3043
+ THC_WORKTREE_PATH: wtPath,
3044
+ THC_WORKTREE_NAME: name,
3045
+ THC_WORKTREE_BRANCH: wtEntry.branch,
3046
+ THC_TARGET_BRANCH: targetBranch,
3047
+ THC_MAIN_ROOT: mainRoot,
3048
+ THC_KEPT_SESSION: options.keepSession ? "true" : "false",
3049
+ THC_KEPT_WORKTREE: options.keepWorktree ? "true" : "false",
3050
+ THC_KEPT_BRANCH: options.keepBranch ? "true" : "false"
3051
+ };
3052
+ await executeHooks(
3053
+ postHooks,
3054
+ {
3055
+ hook_event_name: "PostWorktreeMerge",
3056
+ cwd: mainRoot,
3057
+ worktree_path: wtPath,
3058
+ worktree_name: name,
3059
+ worktree_branch: wtEntry.branch,
3060
+ target_branch: targetBranch,
3061
+ main_root: mainRoot,
3062
+ kept_session: options.keepSession ?? false,
3063
+ kept_worktree: options.keepWorktree ?? false,
3064
+ kept_branch: options.keepBranch ?? false
3065
+ },
3066
+ hookEnv,
3067
+ true
3068
+ );
3069
+ }
3070
+ console.log(chalk19.green("\u2713 Merged and cleaned up"));
3071
+ } catch (error) {
3072
+ console.error(chalk19.red(`Error: ${error.message}`));
3073
+ process.exit(1);
3074
+ }
3075
+ }
3076
+
3077
+ // src/commands/worktree/remove.ts
3078
+ import path21 from "path";
3079
+ import chalk20 from "chalk";
3080
+ async function worktreeRemoveCommand(name, options) {
3081
+ try {
3082
+ if (!isGitRepo()) {
3083
+ console.error(chalk20.red("Error: not in a git repository"));
3084
+ process.exit(1);
3085
+ }
3086
+ const mainRoot = getMainWorktreeRoot();
3087
+ const wtEntry = findWorktree(name, mainRoot);
3088
+ const wtPath = path21.resolve(wtEntry.worktreePath);
3089
+ const hasBranch = !wtEntry.detached && wtEntry.branch !== "(detached)";
3090
+ if (wtPath === path21.resolve(mainRoot)) {
3091
+ console.error(chalk20.red("Error: refusing to remove the main worktree"));
3092
+ process.exit(1);
3093
+ }
3094
+ if (!options.force && hasBranch) {
3095
+ const defaultBranch = getDefaultBranch(mainRoot);
3096
+ if (hasUnmergedCommits(wtEntry.branch, defaultBranch, mainRoot)) {
2939
3097
  console.error(
2940
- chalk16.red(
2941
- "Error: worktree has uncommitted changes. Commit/stash first or use --force."
3098
+ chalk20.red(
3099
+ `Error: branch '${wtEntry.branch}' has commits not merged into '${defaultBranch}'. Merge first or use --force to discard.`
2942
3100
  )
2943
3101
  );
2944
3102
  process.exit(1);
2945
3103
  }
2946
- console.log(chalk16.blue(`Rebasing ${wtEntry.branch} onto ${targetBranch}...`));
2947
- runGitCommandOrThrow(["rebase", targetBranch], { cwd: wtEntry.worktreePath });
2948
- console.log(chalk16.blue(`Fast-forward merging into ${targetBranch}...`));
2949
- runGitCommandOrThrow(["switch", targetBranch], { cwd: mainRoot });
2950
- runGitCommandOrThrow(["merge", "--ff-only", wtEntry.branch], { cwd: mainRoot });
2951
- const handle = path16.basename(wtEntry.worktreePath);
2952
- const sessionNames = allSessionNamesForHandle(handle);
2953
- if (!options.keepSession) {
2954
- for (const s of sessionNames) {
2955
- tmuxKillSession(s);
2956
- }
2957
- }
2958
- if (!options.keepWorktree) {
2959
- const removeArgs = ["worktree", "remove"];
2960
- if (options.force) {
2961
- removeArgs.push("--force");
2962
- }
2963
- removeArgs.push(wtEntry.worktreePath);
2964
- runGitCommandOrThrow(removeArgs, { cwd: mainRoot });
2965
- try {
2966
- runGitCommandOrThrow(["worktree", "prune"], { cwd: mainRoot });
2967
- } catch {
2968
- }
2969
- }
2970
- if (!options.keepBranch) {
2971
- try {
2972
- runGitCommandOrThrow(["branch", "-d", wtEntry.branch], { cwd: mainRoot });
2973
- } catch {
2974
- if (options.force) {
2975
- runGitCommandOrThrow(["branch", "-D", wtEntry.branch], { cwd: mainRoot });
2976
- } else {
2977
- throw new Error(
2978
- `Failed to delete branch '${wtEntry.branch}'. Re-run with --force to delete it.`
2979
- );
2980
- }
2981
- }
2982
- }
2983
- const postMergeHooks = getHooksForEvent(mergeHooksConfig, "PostWorktreeMerge");
2984
- if (postMergeHooks.length > 0) {
2985
- const hookInput = {
2986
- hook_event_name: "PostWorktreeMerge",
3104
+ }
3105
+ const hooksConfig = loadHooksConfig(mainRoot);
3106
+ const preHooks = getHooksForEvent(hooksConfig, "PreWorktreeRemove");
3107
+ if (preHooks.length > 0) {
3108
+ const hookEnv = {
3109
+ THC_WORKTREE_PATH: wtPath,
3110
+ THC_WORKTREE_NAME: name,
3111
+ THC_WORKTREE_BRANCH: wtEntry.branch,
3112
+ THC_MAIN_ROOT: mainRoot
3113
+ };
3114
+ await executeHooks(
3115
+ preHooks,
3116
+ {
3117
+ hook_event_name: "PreWorktreeRemove",
2987
3118
  cwd: mainRoot,
2988
- worktree_path: wtResolved,
3119
+ worktree_path: wtPath,
2989
3120
  worktree_name: name,
2990
3121
  worktree_branch: wtEntry.branch,
2991
- target_branch: targetBranch,
2992
- main_root: mainRoot,
2993
- kept_session: options.keepSession || false,
2994
- kept_worktree: options.keepWorktree || false,
2995
- kept_branch: options.keepBranch || false
2996
- };
2997
- const hookEnv = {
2998
- THC_WORKTREE_PATH: wtResolved,
2999
- THC_WORKTREE_NAME: name,
3000
- THC_WORKTREE_BRANCH: wtEntry.branch,
3001
- THC_TARGET_BRANCH: targetBranch,
3002
- THC_MAIN_ROOT: mainRoot,
3003
- THC_KEPT_SESSION: options.keepSession ? "true" : "false",
3004
- THC_KEPT_WORKTREE: options.keepWorktree ? "true" : "false",
3005
- THC_KEPT_BRANCH: options.keepBranch ? "true" : "false"
3006
- };
3007
- await executeHooks(postMergeHooks, hookInput, hookEnv, true);
3008
- }
3009
- console.log(chalk16.green("\u2713 Merged and cleaned up"));
3010
- } catch (error) {
3011
- console.error(chalk16.red(`Error: ${error.message}`));
3122
+ main_root: mainRoot
3123
+ },
3124
+ hookEnv,
3125
+ true
3126
+ );
3127
+ }
3128
+ cleanupWorktreeThoughts(wtPath, { force: options.force, verbose: true });
3129
+ if (!options.force && hasUncommittedChanges(wtEntry.worktreePath)) {
3130
+ console.error(
3131
+ chalk20.red("Error: worktree has uncommitted changes. Commit/stash first or use --force.")
3132
+ );
3012
3133
  process.exit(1);
3013
3134
  }
3014
- });
3135
+ cleanupWorktreeTmuxSession(wtPath);
3136
+ console.log(chalk20.gray("Removing git worktree..."));
3137
+ removeGitWorktree(wtPath, mainRoot, { force: options.force });
3138
+ if (hasBranch) {
3139
+ console.log(chalk20.gray(`Deleting branch '${wtEntry.branch}'...`));
3140
+ try {
3141
+ deleteWorktreeBranch(wtEntry.branch, mainRoot, { force: options.force });
3142
+ } catch (error) {
3143
+ console.log(chalk20.yellow(`Warning: ${error.message}`));
3144
+ }
3145
+ }
3146
+ const postHooks = getHooksForEvent(hooksConfig, "PostWorktreeRemove");
3147
+ if (postHooks.length > 0) {
3148
+ const hookEnv = {
3149
+ THC_WORKTREE_PATH: wtPath,
3150
+ THC_WORKTREE_NAME: name,
3151
+ THC_WORKTREE_BRANCH: wtEntry.branch,
3152
+ THC_MAIN_ROOT: mainRoot
3153
+ };
3154
+ await executeHooks(
3155
+ postHooks,
3156
+ {
3157
+ hook_event_name: "PostWorktreeRemove",
3158
+ cwd: mainRoot,
3159
+ worktree_path: wtPath,
3160
+ worktree_name: name,
3161
+ worktree_branch: wtEntry.branch,
3162
+ main_root: mainRoot
3163
+ },
3164
+ hookEnv,
3165
+ true
3166
+ );
3167
+ }
3168
+ console.log(chalk20.green("\u2713 Worktree removed"));
3169
+ } catch (error) {
3170
+ console.error(chalk20.red(`Error: ${error.message}`));
3171
+ process.exit(1);
3172
+ }
3173
+ }
3174
+
3175
+ // src/commands/worktree.ts
3176
+ function worktreeCommand(program) {
3177
+ const wt = program.command("worktree").description("Manage git worktrees bound to tmux sessions");
3178
+ wt.command("add <name>").description("Create a git worktree and a tmux session for it").option("--branch <branch>", "Branch name (defaults to <name>)").option("--base <ref>", "Base ref/commit (default: HEAD)", "HEAD").option("--path <path>", "Worktree directory path (default: ../<repo>__worktrees/<name>)").option("--detached", "Create a detached worktree at <base> (no branch)").option("--no-thoughts", "Skip thoughts initialization").action(worktreeAddCommand);
3179
+ wt.command("list").description("List thc-managed worktrees and their tmux sessions").option("--all", "Show all git worktrees (not just ../<repo>__worktrees)").action(worktreeListCommand);
3180
+ wt.command("merge <name>").description(
3181
+ "Rebase worktree branch onto target, ff-merge, then clean up worktree + tmux session"
3182
+ ).option(
3183
+ "--into <branch>",
3184
+ "Target branch to merge into (default: current branch in main worktree)"
3185
+ ).option("--force", "Force cleanup even if uncommitted changes exist").option("--keep-session", "Do not kill the tmux session").option("--keep-worktree", "Do not remove the git worktree").option("--keep-branch", "Do not delete the source branch").action(worktreeMergeCommand);
3186
+ wt.command("remove <name>").description(
3187
+ "Remove a worktree and clean up associated resources (tmux session, thoughts, branch)"
3188
+ ).option("--force", "Force removal even with uncommitted changes or unmerged commits").action(worktreeRemoveCommand);
3015
3189
  }
3016
3190
 
3017
3191
  // src/commands/hooks/init.ts
3018
3192
  import fs15 from "fs";
3019
- import path17 from "path";
3020
- import chalk17 from "chalk";
3193
+ import path22 from "path";
3194
+ import chalk21 from "chalk";
3021
3195
  import { fileURLToPath as fileURLToPath2 } from "url";
3022
3196
  import { dirname as dirname2 } from "path";
3023
3197
  var __filename3 = fileURLToPath2(import.meta.url);
@@ -3025,18 +3199,18 @@ var __dirname3 = dirname2(__filename3);
3025
3199
  async function hooksInitCommand() {
3026
3200
  try {
3027
3201
  const repoPath = process.cwd();
3028
- const configDir = path17.join(repoPath, HOOKS_CONFIG_DIR);
3029
- const configPath = path17.join(repoPath, HOOKS_CONFIG_FILE);
3202
+ const configDir = path22.join(repoPath, HOOKS_CONFIG_DIR);
3203
+ const configPath = path22.join(repoPath, HOOKS_CONFIG_FILE);
3030
3204
  if (fs15.existsSync(configPath)) {
3031
- console.log(chalk17.yellow(`${HOOKS_CONFIG_FILE} already exists.`));
3032
- console.log(chalk17.gray(`Edit the file directly: ${configPath}`));
3205
+ console.log(chalk21.yellow(`${HOOKS_CONFIG_FILE} already exists.`));
3206
+ console.log(chalk21.gray(`Edit the file directly: ${configPath}`));
3033
3207
  return;
3034
3208
  }
3035
3209
  const possiblePaths = [
3036
3210
  // When running from built dist: one level up from dist/
3037
- path17.resolve(__dirname3, "..", ".thought-cabinet/hooks.example.json"),
3211
+ path22.resolve(__dirname3, "..", ".thought-cabinet/hooks.example.json"),
3038
3212
  // When installed via npm: one level up from dist/
3039
- path17.resolve(__dirname3, "../..", ".thought-cabinet/hooks.example.json")
3213
+ path22.resolve(__dirname3, "../..", ".thought-cabinet/hooks.example.json")
3040
3214
  ];
3041
3215
  let examplePath = null;
3042
3216
  for (const candidatePath of possiblePaths) {
@@ -3046,40 +3220,217 @@ async function hooksInitCommand() {
3046
3220
  }
3047
3221
  }
3048
3222
  if (!examplePath) {
3049
- console.error(chalk17.red("Error: hooks.example.json not found in expected locations"));
3050
- console.log(chalk17.gray("Searched paths:"));
3051
- possiblePaths.forEach((p5) => console.log(chalk17.gray(` - ${p5}`)));
3223
+ console.error(chalk21.red("Error: hooks.example.json not found in expected locations"));
3224
+ console.log(chalk21.gray("Searched paths:"));
3225
+ possiblePaths.forEach((p5) => console.log(chalk21.gray(` - ${p5}`)));
3052
3226
  process.exit(1);
3053
3227
  }
3054
3228
  fs15.mkdirSync(configDir, { recursive: true });
3055
3229
  fs15.copyFileSync(examplePath, configPath);
3056
- console.log(chalk17.green(`Created ${HOOKS_CONFIG_FILE}`));
3230
+ console.log(chalk21.green(`Created ${HOOKS_CONFIG_FILE}`));
3057
3231
  } catch (error) {
3058
- console.error(chalk17.red(`Error during hooks init: ${error}`));
3232
+ console.error(chalk21.red(`Error during hooks init: ${error}`));
3059
3233
  process.exit(1);
3060
3234
  }
3061
3235
  }
3062
3236
 
3063
3237
  // src/commands/hooks.ts
3064
- function hooksCommand(program2) {
3065
- const hooks = program2.command("hooks").description("Manage hook configuration");
3238
+ function hooksCommand(program) {
3239
+ const hooks = program.command("hooks").description("Manage hook configuration");
3066
3240
  hooks.command("init").description("Initialize hooks configuration in current repository").action(hooksInitCommand);
3067
3241
  }
3068
3242
 
3243
+ // src/commands/completion.ts
3244
+ function completionCommand(program) {
3245
+ const completion = program.command("completion").description("Manage shell completion for thoughtcabinet CLI");
3246
+ completion.command("install").description("Install shell completion scripts").action(async () => {
3247
+ const { install } = await import("./installer-LJ3SJXPW.js");
3248
+ await install();
3249
+ });
3250
+ completion.command("uninstall").description("Remove shell completion scripts").action(async () => {
3251
+ const { uninstall } = await import("./installer-LJ3SJXPW.js");
3252
+ await uninstall();
3253
+ });
3254
+ }
3255
+
3256
+ // src/completion/handler.ts
3257
+ import tabtab from "tabtab";
3258
+
3259
+ // src/completion/providers.ts
3260
+ import path23 from "path";
3261
+ function getProfileNames() {
3262
+ try {
3263
+ const config = loadConfigFile();
3264
+ if (!config.thoughts?.profiles) {
3265
+ return [];
3266
+ }
3267
+ return Object.keys(config.thoughts.profiles);
3268
+ } catch {
3269
+ return [];
3270
+ }
3271
+ }
3272
+ function getWorktreeNames() {
3273
+ try {
3274
+ if (!isGitRepo()) {
3275
+ return [];
3276
+ }
3277
+ const mainRoot = getMainWorktreeRoot();
3278
+ const baseDir = getWorktreesBaseDir(mainRoot);
3279
+ const output = runGitCommand(["worktree", "list", "--porcelain"]);
3280
+ const entries = parseWorktreeListPorcelain(output);
3281
+ return entries.filter((e) => {
3282
+ const p5 = path23.resolve(e.worktreePath);
3283
+ return p5.startsWith(baseDir + path23.sep);
3284
+ }).map((e) => path23.basename(e.worktreePath));
3285
+ } catch {
3286
+ return [];
3287
+ }
3288
+ }
3289
+ function getBranchNames() {
3290
+ try {
3291
+ if (!isGitRepo()) {
3292
+ return [];
3293
+ }
3294
+ const output = runGitCommand(["branch", "--format=%(refname:short)"]);
3295
+ return output.split("\n").filter(Boolean);
3296
+ } catch {
3297
+ return [];
3298
+ }
3299
+ }
3300
+ function getAgentNames() {
3301
+ return ["claude", "codebuddy"];
3302
+ }
3303
+
3304
+ // src/completion/handler.ts
3305
+ var TOP_LEVEL_COMMANDS = [
3306
+ "init",
3307
+ "destroy",
3308
+ "sync",
3309
+ "status",
3310
+ "config",
3311
+ "prune",
3312
+ "profile",
3313
+ "worktree",
3314
+ "agent",
3315
+ "metadata",
3316
+ "hooks",
3317
+ "completion"
3318
+ ];
3319
+ var SUBCOMMANDS = {
3320
+ profile: ["create", "list", "show", "delete"],
3321
+ worktree: ["add", "list", "merge", "remove"],
3322
+ agent: ["init"],
3323
+ hooks: ["init"],
3324
+ completion: ["install", "uninstall"]
3325
+ };
3326
+ var OPTIONS = {
3327
+ init: ["--force", "--config-file", "--directory", "--profile"],
3328
+ destroy: ["--force", "--config-file"],
3329
+ sync: ["-m", "--message", "--config-file"],
3330
+ status: ["--config-file"],
3331
+ config: ["--edit", "--json", "--config-file"],
3332
+ prune: ["--apply", "--config-file"],
3333
+ "profile create": ["--repo", "--repos-dir", "--global-dir", "--config-file"],
3334
+ "profile list": ["--json", "--config-file"],
3335
+ "profile show": ["--json", "--config-file"],
3336
+ "profile delete": ["--force", "--config-file"],
3337
+ "worktree add": ["--branch", "--base", "--path", "--detached", "--no-thoughts"],
3338
+ "worktree list": ["--all"],
3339
+ "worktree merge": ["--into", "--force", "--keep-session", "--keep-worktree", "--keep-branch"],
3340
+ "worktree remove": ["--force"],
3341
+ "agent init": ["--force", "--all", "--max-thinking-tokens", "--name"]
3342
+ };
3343
+ var DYNAMIC_ARGS = {
3344
+ "profile show": getProfileNames,
3345
+ "profile delete": getProfileNames,
3346
+ "worktree merge": getWorktreeNames,
3347
+ "worktree remove": getWorktreeNames
3348
+ };
3349
+ var DYNAMIC_OPTIONS = {
3350
+ "--profile": getProfileNames,
3351
+ "--branch": getBranchNames,
3352
+ "--base": getBranchNames,
3353
+ "--into": getBranchNames,
3354
+ "--name": getAgentNames
3355
+ };
3356
+ async function handleCompletion() {
3357
+ const env = tabtab.parseEnv(process.env);
3358
+ if (!env.complete) {
3359
+ return false;
3360
+ }
3361
+ const { line, prev } = env;
3362
+ const args = line.split(" ").filter(Boolean).slice(1);
3363
+ if (prev in DYNAMIC_OPTIONS) {
3364
+ const provider = DYNAMIC_OPTIONS[prev];
3365
+ await tabtab.log(provider());
3366
+ return true;
3367
+ }
3368
+ if (args.length === 0 || args.length === 1 && !line.endsWith(" ")) {
3369
+ const partial = args[0] || "";
3370
+ const matches = TOP_LEVEL_COMMANDS.filter((cmd) => cmd.startsWith(partial));
3371
+ await tabtab.log(matches);
3372
+ return true;
3373
+ }
3374
+ const firstArg = args[0];
3375
+ if (firstArg in SUBCOMMANDS) {
3376
+ const subcommands = SUBCOMMANDS[firstArg];
3377
+ if (args.length === 1 && line.endsWith(" ")) {
3378
+ await tabtab.log(subcommands);
3379
+ return true;
3380
+ }
3381
+ if (args.length === 2 && !line.endsWith(" ")) {
3382
+ const partial = args[1];
3383
+ const matches = subcommands.filter((sub) => sub.startsWith(partial));
3384
+ await tabtab.log(matches);
3385
+ return true;
3386
+ }
3387
+ }
3388
+ const commandKey = args.slice(0, 2).join(" ");
3389
+ if (commandKey in DYNAMIC_ARGS && args.length === 2 && line.endsWith(" ")) {
3390
+ const provider = DYNAMIC_ARGS[commandKey];
3391
+ await tabtab.log(provider());
3392
+ return true;
3393
+ }
3394
+ if (prev.startsWith("-")) {
3395
+ await tabtab.log([]);
3396
+ return true;
3397
+ }
3398
+ const options = OPTIONS[commandKey] || OPTIONS[firstArg] || [];
3399
+ if (line.endsWith(" ") || prev.startsWith("-")) {
3400
+ const usedOptions = args.filter((arg) => arg.startsWith("-"));
3401
+ const availableOptions = options.filter((opt) => !usedOptions.includes(opt));
3402
+ await tabtab.log(availableOptions);
3403
+ return true;
3404
+ }
3405
+ await tabtab.log([]);
3406
+ return true;
3407
+ }
3408
+
3069
3409
  // src/index.ts
3070
3410
  import dotenv2 from "dotenv";
3071
3411
  import { createRequire } from "module";
3072
- dotenv2.config();
3073
- var require2 = createRequire(import.meta.url);
3074
- var { version } = require2("../package.json");
3075
- var program = new Command();
3076
- program.name("thoughtcabinet").description(
3077
- "Thought Cabinet (thc) \u2014 CLI for structured AI coding workflows with filesystem-based memory and context management."
3078
- ).version(version);
3079
- thoughtsCommand(program);
3080
- agentCommand(program);
3081
- metadataCommand(program);
3082
- worktreeCommand(program);
3083
- hooksCommand(program);
3084
- program.parse(process.argv);
3412
+ async function main() {
3413
+ const completionHandled = await handleCompletion();
3414
+ if (completionHandled) {
3415
+ process.exit(0);
3416
+ }
3417
+ dotenv2.config();
3418
+ const require2 = createRequire(import.meta.url);
3419
+ const { version } = require2("../package.json");
3420
+ const program = new Command();
3421
+ program.name("thoughtcabinet").description(
3422
+ "Thought Cabinet (thc) \u2014 CLI for structured AI coding workflows with filesystem-based memory and context management."
3423
+ ).version(version);
3424
+ thoughtsCommand(program);
3425
+ agentCommand(program);
3426
+ metadataCommand(program);
3427
+ worktreeCommand(program);
3428
+ hooksCommand(program);
3429
+ completionCommand(program);
3430
+ program.parse(process.argv);
3431
+ }
3432
+ main().catch((err) => {
3433
+ console.error(err);
3434
+ process.exit(1);
3435
+ });
3085
3436
  //# sourceMappingURL=index.js.map