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/.thought-cabinet/hooks.example.json +10 -0
- package/.thought-cabinet/hooks.json +14 -0
- package/dist/index.js +723 -372
- package/dist/index.js.map +1 -1
- package/dist/installer-LJ3SJXPW.js +37 -0
- package/dist/installer-LJ3SJXPW.js.map +1 -0
- package/package.json +4 -6
- package/src/agent-assets/commands/commit.md +7 -18
- package/src/agent-assets/commands/create_plan.md +3 -274
- package/src/agent-assets/commands/implement_plan.md +2 -87
- package/src/agent-assets/commands/iterate_plan.md +2 -249
- package/src/agent-assets/commands/research_codebase.md +2 -102
- package/src/agent-assets/commands/validate_plan.md +3 -175
- package/src/agent-assets/skills/creating-plan/SKILL.md +229 -0
- package/src/agent-assets/skills/{writing-plan/SKILL.md → creating-plan/plan-template.md} +6 -18
- package/src/agent-assets/skills/implementing-plan/SKILL.md +123 -0
- package/src/agent-assets/skills/iterating-plan/SKILL.md +151 -0
- package/src/agent-assets/skills/researching-codebase/SKILL.md +144 -0
- package/src/agent-assets/skills/{generating-research-document/document_template.md → researching-codebase/research-template.md} +5 -10
- package/src/agent-assets/skills/validating-plan/SKILL.md +148 -0
- package/src/agent-assets/skills/generating-research-document/SKILL.md +0 -41
- /package/src/agent-assets/{commands → agents}/code-simplifier.md +0 -0
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(
|
|
2127
|
-
const cmd =
|
|
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(
|
|
2517
|
-
const agent =
|
|
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
|
|
2587
|
+
import path15 from "path";
|
|
2530
2588
|
function getGitInfo() {
|
|
2589
|
+
if (!isGitRepo()) {
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2531
2592
|
try {
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
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(
|
|
2612
|
-
|
|
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
|
|
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
|
|
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 =
|
|
2674
|
-
const targetPath =
|
|
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
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
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:
|
|
2754
|
+
worktree_branch: branch,
|
|
2715
2755
|
main_root: mainRoot,
|
|
2716
2756
|
session_name: sessionName,
|
|
2717
2757
|
base_ref: options.base
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
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
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
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:
|
|
2800
|
+
worktree_branch: branch,
|
|
2788
2801
|
main_root: mainRoot,
|
|
2789
2802
|
session_name: sessionName
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
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
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
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
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
)
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
console.
|
|
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
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
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:
|
|
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
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
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
|
-
|
|
2941
|
-
|
|
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
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
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:
|
|
3119
|
+
worktree_path: wtPath,
|
|
2989
3120
|
worktree_name: name,
|
|
2990
3121
|
worktree_branch: wtEntry.branch,
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
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
|
|
3020
|
-
import
|
|
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 =
|
|
3029
|
-
const configPath =
|
|
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(
|
|
3032
|
-
console.log(
|
|
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
|
-
|
|
3211
|
+
path22.resolve(__dirname3, "..", ".thought-cabinet/hooks.example.json"),
|
|
3038
3212
|
// When installed via npm: one level up from dist/
|
|
3039
|
-
|
|
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(
|
|
3050
|
-
console.log(
|
|
3051
|
-
possiblePaths.forEach((p5) => console.log(
|
|
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(
|
|
3230
|
+
console.log(chalk21.green(`Created ${HOOKS_CONFIG_FILE}`));
|
|
3057
3231
|
} catch (error) {
|
|
3058
|
-
console.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(
|
|
3065
|
-
const hooks =
|
|
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
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
program
|
|
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
|