thought-cabinet 0.0.3 → 0.1.0
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 +34 -0
- package/.thought-cabinet/hooks.json +14 -0
- package/LICENSE +188 -2
- package/README.md +306 -74
- package/dist/index.js +1445 -472
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/agent-assets/commands/code-simplifier.md +52 -0
package/dist/index.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/thoughts/init.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import { execSync as
|
|
10
|
-
import
|
|
7
|
+
import fs7 from "fs";
|
|
8
|
+
import path10 from "path";
|
|
9
|
+
import { execSync as execSync4 } from "child_process";
|
|
10
|
+
import chalk5 from "chalk";
|
|
11
11
|
import * as p from "@clack/prompts";
|
|
12
12
|
|
|
13
13
|
// src/config.ts
|
|
@@ -80,42 +80,147 @@ function saveThoughtsConfig(thoughtsConfig, options = {}) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// src/commands/thoughts/utils/paths.ts
|
|
83
|
-
import
|
|
83
|
+
import path3 from "path";
|
|
84
84
|
import os from "os";
|
|
85
|
+
|
|
86
|
+
// src/git.ts
|
|
87
|
+
import { execFileSync } from "child_process";
|
|
88
|
+
import path2 from "path";
|
|
89
|
+
function runGitCommand(args, opts = {}) {
|
|
90
|
+
return execFileSync("git", args, {
|
|
91
|
+
cwd: opts.cwd,
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
94
|
+
}).trim();
|
|
95
|
+
}
|
|
96
|
+
function runGitCommandOrThrow(args, opts = {}) {
|
|
97
|
+
execFileSync("git", args, {
|
|
98
|
+
cwd: opts.cwd,
|
|
99
|
+
stdio: "inherit"
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function isGitRepo(cwd) {
|
|
103
|
+
try {
|
|
104
|
+
runGitCommand(["rev-parse", "--git-dir"], { cwd });
|
|
105
|
+
return true;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function getMainRepoPath() {
|
|
111
|
+
try {
|
|
112
|
+
const gitCommonDir = runGitCommand(["rev-parse", "--git-common-dir"]);
|
|
113
|
+
const gitDir = runGitCommand(["rev-parse", "--git-dir"]);
|
|
114
|
+
if (gitCommonDir !== gitDir && gitCommonDir !== ".git") {
|
|
115
|
+
const mainRepoPath = path2.dirname(path2.resolve(gitCommonDir));
|
|
116
|
+
return mainRepoPath;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function validateWorktreeHandle(name) {
|
|
124
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Invalid worktree name '${name}'. Use only [A-Za-z0-9._-] and start with a letter/number.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function parseWorktreeListPorcelain(output) {
|
|
131
|
+
const blocks = output.trim().length === 0 ? [] : output.trim().split(/\n\n+/);
|
|
132
|
+
const out = [];
|
|
133
|
+
for (const block of blocks) {
|
|
134
|
+
let worktreePath = "";
|
|
135
|
+
let branch = "";
|
|
136
|
+
let detached = false;
|
|
137
|
+
for (const line of block.split("\n")) {
|
|
138
|
+
if (line.startsWith("worktree ")) {
|
|
139
|
+
worktreePath = line.slice("worktree ".length).trim();
|
|
140
|
+
} else if (line.startsWith("branch refs/heads/")) {
|
|
141
|
+
branch = line.slice("branch refs/heads/".length).trim();
|
|
142
|
+
} else if (line.trim() === "detached") {
|
|
143
|
+
detached = true;
|
|
144
|
+
branch = "(detached)";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (worktreePath && branch) {
|
|
148
|
+
out.push({ worktreePath, branch, detached });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
function getMainWorktreeRoot(cwd) {
|
|
154
|
+
const list = runGitCommand(["worktree", "list", "--porcelain"], { cwd });
|
|
155
|
+
const entries = parseWorktreeListPorcelain(list);
|
|
156
|
+
if (entries.length === 0) {
|
|
157
|
+
throw new Error("No git worktrees found");
|
|
158
|
+
}
|
|
159
|
+
return entries[0].worktreePath;
|
|
160
|
+
}
|
|
161
|
+
function getWorktreesBaseDir(mainWorktreeRoot) {
|
|
162
|
+
const repoName = path2.basename(mainWorktreeRoot);
|
|
163
|
+
const parent = path2.dirname(mainWorktreeRoot);
|
|
164
|
+
return path2.join(parent, `${repoName}__worktrees`);
|
|
165
|
+
}
|
|
166
|
+
function findWorktree(nameOrBranch, cwd) {
|
|
167
|
+
const list = runGitCommand(["worktree", "list", "--porcelain"], { cwd });
|
|
168
|
+
const entries = parseWorktreeListPorcelain(list);
|
|
169
|
+
for (const e of entries) {
|
|
170
|
+
if (path2.basename(e.worktreePath) === nameOrBranch) {
|
|
171
|
+
return e;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const e of entries) {
|
|
175
|
+
if (e.branch === nameOrBranch) {
|
|
176
|
+
return e;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
throw new Error(`Worktree not found: ${nameOrBranch}`);
|
|
180
|
+
}
|
|
181
|
+
function hasUncommittedChanges(repoPath) {
|
|
182
|
+
const status = runGitCommand(["status", "--porcelain"], { cwd: repoPath });
|
|
183
|
+
return status.trim().length > 0;
|
|
184
|
+
}
|
|
185
|
+
function setBranchBase(branch, base, cwd) {
|
|
186
|
+
runGitCommandOrThrow(["config", "--local", `branch.${branch}.thc-base`, base], { cwd });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/commands/thoughts/utils/paths.ts
|
|
85
190
|
function getDefaultThoughtsRepo() {
|
|
86
|
-
return
|
|
191
|
+
return path3.join(os.homedir(), "thoughts");
|
|
87
192
|
}
|
|
88
193
|
function expandPath(filePath) {
|
|
89
194
|
if (filePath.startsWith("~/")) {
|
|
90
|
-
return
|
|
195
|
+
return path3.join(os.homedir(), filePath.slice(2));
|
|
91
196
|
}
|
|
92
|
-
return
|
|
197
|
+
return path3.resolve(filePath);
|
|
93
198
|
}
|
|
94
199
|
function getCurrentRepoPath() {
|
|
95
200
|
return process.cwd();
|
|
96
201
|
}
|
|
97
202
|
function getRepoNameFromPath(repoPath) {
|
|
98
|
-
const parts = repoPath.split(
|
|
203
|
+
const parts = repoPath.split(path3.sep);
|
|
99
204
|
return parts[parts.length - 1] || "unnamed_repo";
|
|
100
205
|
}
|
|
101
206
|
function getRepoThoughtsPath(thoughtsRepoOrConfig, reposDirOrRepoName, repoName) {
|
|
102
207
|
if (typeof thoughtsRepoOrConfig === "string") {
|
|
103
|
-
return
|
|
208
|
+
return path3.join(expandPath(thoughtsRepoOrConfig), reposDirOrRepoName, repoName);
|
|
104
209
|
}
|
|
105
210
|
const config = thoughtsRepoOrConfig;
|
|
106
|
-
return
|
|
211
|
+
return path3.join(expandPath(config.thoughtsRepo), config.reposDir, reposDirOrRepoName);
|
|
107
212
|
}
|
|
108
213
|
function getGlobalThoughtsPath(thoughtsRepoOrConfig, globalDir) {
|
|
109
214
|
if (typeof thoughtsRepoOrConfig === "string") {
|
|
110
|
-
return
|
|
215
|
+
return path3.join(expandPath(thoughtsRepoOrConfig), globalDir);
|
|
111
216
|
}
|
|
112
217
|
const config = thoughtsRepoOrConfig;
|
|
113
|
-
return
|
|
218
|
+
return path3.join(expandPath(config.thoughtsRepo), config.globalDir);
|
|
114
219
|
}
|
|
115
220
|
|
|
116
221
|
// src/commands/thoughts/utils/repository.ts
|
|
117
222
|
import fs2 from "fs";
|
|
118
|
-
import
|
|
223
|
+
import path5 from "path";
|
|
119
224
|
import { execSync } from "child_process";
|
|
120
225
|
|
|
121
226
|
// src/templates/gitignore.ts
|
|
@@ -158,7 +263,7 @@ This directory contains thoughts and notes that apply across all repositories.
|
|
|
158
263
|
}
|
|
159
264
|
|
|
160
265
|
// src/templates/agentMd.ts
|
|
161
|
-
import
|
|
266
|
+
import path4 from "path";
|
|
162
267
|
import os2 from "os";
|
|
163
268
|
function generateAgentMd({
|
|
164
269
|
thoughtsRepo,
|
|
@@ -167,8 +272,8 @@ function generateAgentMd({
|
|
|
167
272
|
user,
|
|
168
273
|
productName
|
|
169
274
|
}) {
|
|
170
|
-
const reposPath =
|
|
171
|
-
const globalPath =
|
|
275
|
+
const reposPath = path4.join(thoughtsRepo, reposDir, repoName).replace(os2.homedir(), "~");
|
|
276
|
+
const globalPath = path4.join(thoughtsRepo, "global").replace(os2.homedir(), "~");
|
|
172
277
|
return `# Thoughts Directory Structure
|
|
173
278
|
|
|
174
279
|
This directory contains developer thoughts and notes for the ${repoName} repository.
|
|
@@ -288,20 +393,20 @@ function ensureThoughtsRepoExists(configOrThoughtsRepo, reposDir, globalDir) {
|
|
|
288
393
|
if (!fs2.existsSync(expandedRepo)) {
|
|
289
394
|
fs2.mkdirSync(expandedRepo, { recursive: true });
|
|
290
395
|
}
|
|
291
|
-
const expandedRepos =
|
|
292
|
-
const expandedGlobal =
|
|
396
|
+
const expandedRepos = path5.join(expandedRepo, effectiveReposDir);
|
|
397
|
+
const expandedGlobal = path5.join(expandedRepo, effectiveGlobalDir);
|
|
293
398
|
if (!fs2.existsSync(expandedRepos)) {
|
|
294
399
|
fs2.mkdirSync(expandedRepos, { recursive: true });
|
|
295
400
|
}
|
|
296
401
|
if (!fs2.existsSync(expandedGlobal)) {
|
|
297
402
|
fs2.mkdirSync(expandedGlobal, { recursive: true });
|
|
298
403
|
}
|
|
299
|
-
const gitPath =
|
|
300
|
-
const
|
|
301
|
-
if (!
|
|
404
|
+
const gitPath = path5.join(expandedRepo, ".git");
|
|
405
|
+
const isGitRepo2 = fs2.existsSync(gitPath) && (fs2.statSync(gitPath).isDirectory() || fs2.statSync(gitPath).isFile());
|
|
406
|
+
if (!isGitRepo2) {
|
|
302
407
|
execSync("git init", { cwd: expandedRepo });
|
|
303
408
|
const gitignore = generateGitignore();
|
|
304
|
-
fs2.writeFileSync(
|
|
409
|
+
fs2.writeFileSync(path5.join(expandedRepo, ".gitignore"), gitignore);
|
|
305
410
|
execSync("git add .gitignore", { cwd: expandedRepo });
|
|
306
411
|
execSync('git commit -m "Initial thoughts repository setup"', { cwd: expandedRepo });
|
|
307
412
|
}
|
|
@@ -328,11 +433,11 @@ function createThoughtsDirectoryStructure(configOrThoughtsRepo, reposDirOrRepoNa
|
|
|
328
433
|
resolvedConfig.reposDir,
|
|
329
434
|
effectiveRepoName
|
|
330
435
|
);
|
|
331
|
-
const repoUserPath =
|
|
332
|
-
const repoSharedPath =
|
|
436
|
+
const repoUserPath = path5.join(repoThoughtsPath, effectiveUser);
|
|
437
|
+
const repoSharedPath = path5.join(repoThoughtsPath, "shared");
|
|
333
438
|
const globalPath = getGlobalThoughtsPath(resolvedConfig.thoughtsRepo, resolvedConfig.globalDir);
|
|
334
|
-
const globalUserPath =
|
|
335
|
-
const globalSharedPath =
|
|
439
|
+
const globalUserPath = path5.join(globalPath, effectiveUser);
|
|
440
|
+
const globalSharedPath = path5.join(globalPath, "shared");
|
|
336
441
|
for (const dir of [repoUserPath, repoSharedPath, globalUserPath, globalSharedPath]) {
|
|
337
442
|
if (!fs2.existsSync(dir)) {
|
|
338
443
|
fs2.mkdirSync(dir, { recursive: true });
|
|
@@ -345,17 +450,17 @@ function createThoughtsDirectoryStructure(configOrThoughtsRepo, reposDirOrRepoNa
|
|
|
345
450
|
const globalReadme = generateGlobalReadme({
|
|
346
451
|
user: effectiveUser
|
|
347
452
|
});
|
|
348
|
-
if (!fs2.existsSync(
|
|
349
|
-
fs2.writeFileSync(
|
|
453
|
+
if (!fs2.existsSync(path5.join(repoThoughtsPath, "README.md"))) {
|
|
454
|
+
fs2.writeFileSync(path5.join(repoThoughtsPath, "README.md"), repoReadme);
|
|
350
455
|
}
|
|
351
|
-
if (!fs2.existsSync(
|
|
352
|
-
fs2.writeFileSync(
|
|
456
|
+
if (!fs2.existsSync(path5.join(globalPath, "README.md"))) {
|
|
457
|
+
fs2.writeFileSync(path5.join(globalPath, "README.md"), globalReadme);
|
|
353
458
|
}
|
|
354
459
|
}
|
|
355
460
|
|
|
356
461
|
// src/commands/thoughts/utils/symlinks.ts
|
|
357
462
|
import fs3 from "fs";
|
|
358
|
-
import
|
|
463
|
+
import path6 from "path";
|
|
359
464
|
function updateSymlinksForNewUsers(currentRepoPath, configOrThoughtsRepo, reposDirOrRepoName, repoNameOrCurrentUser, currentUser) {
|
|
360
465
|
let resolvedConfig;
|
|
361
466
|
let effectiveRepoName;
|
|
@@ -372,7 +477,7 @@ function updateSymlinksForNewUsers(currentRepoPath, configOrThoughtsRepo, reposD
|
|
|
372
477
|
effectiveRepoName = reposDirOrRepoName;
|
|
373
478
|
effectiveUser = repoNameOrCurrentUser;
|
|
374
479
|
}
|
|
375
|
-
const thoughtsDir =
|
|
480
|
+
const thoughtsDir = path6.join(currentRepoPath, "thoughts");
|
|
376
481
|
const repoThoughtsPath = getRepoThoughtsPath(
|
|
377
482
|
resolvedConfig.thoughtsRepo,
|
|
378
483
|
resolvedConfig.reposDir,
|
|
@@ -385,8 +490,8 @@ function updateSymlinksForNewUsers(currentRepoPath, configOrThoughtsRepo, reposD
|
|
|
385
490
|
const entries = fs3.readdirSync(repoThoughtsPath, { withFileTypes: true });
|
|
386
491
|
const userDirs = entries.filter((entry) => entry.isDirectory() && entry.name !== "shared" && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
387
492
|
for (const userName of userDirs) {
|
|
388
|
-
const symlinkPath =
|
|
389
|
-
const targetPath =
|
|
493
|
+
const symlinkPath = path6.join(thoughtsDir, userName);
|
|
494
|
+
const targetPath = path6.join(repoThoughtsPath, userName);
|
|
390
495
|
if (!fs3.existsSync(symlinkPath) && userName !== effectiveUser) {
|
|
391
496
|
try {
|
|
392
497
|
fs3.symlinkSync(targetPath, symlinkPath, "dir");
|
|
@@ -398,6 +503,12 @@ function updateSymlinksForNewUsers(currentRepoPath, configOrThoughtsRepo, reposD
|
|
|
398
503
|
return addedSymlinks;
|
|
399
504
|
}
|
|
400
505
|
|
|
506
|
+
// src/commands/thoughts/utils/cleanup.ts
|
|
507
|
+
import fs4 from "fs";
|
|
508
|
+
import path7 from "path";
|
|
509
|
+
import { execSync as execSync2 } from "child_process";
|
|
510
|
+
import chalk2 from "chalk";
|
|
511
|
+
|
|
401
512
|
// src/commands/thoughts/profile/utils.ts
|
|
402
513
|
function resolveProfileForRepo(config, repoPath) {
|
|
403
514
|
const mapping = config.repoMappings[repoPath];
|
|
@@ -451,104 +562,436 @@ function sanitizeProfileName(name) {
|
|
|
451
562
|
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
452
563
|
}
|
|
453
564
|
|
|
454
|
-
// src/commands/thoughts/
|
|
455
|
-
function
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
565
|
+
// src/commands/thoughts/utils/cleanup.ts
|
|
566
|
+
function cleanupThoughtsDirectory({
|
|
567
|
+
repoPath,
|
|
568
|
+
config,
|
|
569
|
+
force = false,
|
|
570
|
+
verbose = true
|
|
571
|
+
}) {
|
|
572
|
+
const thoughtsDir = path7.join(repoPath, "thoughts");
|
|
573
|
+
const result = {
|
|
574
|
+
thoughtsRemoved: false,
|
|
575
|
+
configRemoved: false
|
|
576
|
+
};
|
|
460
577
|
if (!fs4.existsSync(thoughtsDir)) {
|
|
461
|
-
return
|
|
578
|
+
return result;
|
|
462
579
|
}
|
|
463
|
-
|
|
464
|
-
|
|
580
|
+
const mapping = config.repoMappings[repoPath];
|
|
581
|
+
const mappedName = getRepoNameFromMapping(mapping);
|
|
582
|
+
const profileName = getProfileNameFromMapping(mapping);
|
|
583
|
+
result.mappedName = mappedName;
|
|
584
|
+
result.profileName = profileName;
|
|
585
|
+
if (!mappedName && !force) {
|
|
586
|
+
return result;
|
|
465
587
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
588
|
+
const searchableDir = path7.join(thoughtsDir, "searchable");
|
|
589
|
+
if (fs4.existsSync(searchableDir)) {
|
|
590
|
+
if (verbose) {
|
|
591
|
+
console.log(chalk2.gray("Removing searchable directory..."));
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
execSync2(`chmod -R 755 "${searchableDir}"`, { stdio: "pipe" });
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
fs4.rmSync(searchableDir, { recursive: true, force: true });
|
|
472
598
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const globalPath = path6.join(thoughtsDir, "global");
|
|
476
|
-
const hasUser = fs4.existsSync(userPath) && fs4.lstatSync(userPath).isSymbolicLink();
|
|
477
|
-
const hasShared = fs4.existsSync(sharedPath) && fs4.lstatSync(sharedPath).isSymbolicLink();
|
|
478
|
-
const hasGlobal = fs4.existsSync(globalPath) && fs4.lstatSync(globalPath).isSymbolicLink();
|
|
479
|
-
if (!hasUser || !hasShared || !hasGlobal) {
|
|
480
|
-
return {
|
|
481
|
-
exists: true,
|
|
482
|
-
isValid: false,
|
|
483
|
-
message: "thoughts directory exists but symlinks are missing or broken"
|
|
484
|
-
};
|
|
599
|
+
if (verbose) {
|
|
600
|
+
console.log(chalk2.gray("Removing thoughts directory..."));
|
|
485
601
|
}
|
|
486
|
-
|
|
602
|
+
try {
|
|
603
|
+
fs4.rmSync(thoughtsDir, { recursive: true, force: true });
|
|
604
|
+
result.thoughtsRemoved = true;
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (verbose) {
|
|
607
|
+
console.error(chalk2.red(`Error removing thoughts directory: ${error}`));
|
|
608
|
+
}
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
if (mappedName) {
|
|
612
|
+
if (verbose) {
|
|
613
|
+
console.log(chalk2.gray("Removing repository from thoughts configuration..."));
|
|
614
|
+
}
|
|
615
|
+
delete config.repoMappings[repoPath];
|
|
616
|
+
result.configRemoved = true;
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/commands/thoughts/init-core.ts
|
|
622
|
+
import fs5 from "fs";
|
|
623
|
+
import path8 from "path";
|
|
624
|
+
import { execSync as execSync3 } from "child_process";
|
|
625
|
+
function setupThoughtsDirectory(options) {
|
|
626
|
+
const { repoPath, profileConfig, mappedName, user, createSearchable = false, setupHooks = false } = options;
|
|
627
|
+
const thoughtsDir = path8.join(repoPath, "thoughts");
|
|
628
|
+
if (fs5.existsSync(thoughtsDir)) {
|
|
629
|
+
const searchableDir = path8.join(thoughtsDir, "searchable");
|
|
630
|
+
if (fs5.existsSync(searchableDir)) {
|
|
631
|
+
try {
|
|
632
|
+
execSync3(`chmod -R 755 "${searchableDir}"`, { stdio: "pipe" });
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
fs5.rmSync(thoughtsDir, { recursive: true, force: true });
|
|
637
|
+
}
|
|
638
|
+
fs5.mkdirSync(thoughtsDir);
|
|
639
|
+
const repoTarget = getRepoThoughtsPath(profileConfig, mappedName);
|
|
640
|
+
const globalTarget = getGlobalThoughtsPath(profileConfig);
|
|
641
|
+
fs5.symlinkSync(path8.join(repoTarget, user), path8.join(thoughtsDir, user), "dir");
|
|
642
|
+
fs5.symlinkSync(path8.join(repoTarget, "shared"), path8.join(thoughtsDir, "shared"), "dir");
|
|
643
|
+
fs5.symlinkSync(globalTarget, path8.join(thoughtsDir, "global"), "dir");
|
|
644
|
+
const otherUsers = updateSymlinksForNewUsers(
|
|
645
|
+
repoPath,
|
|
646
|
+
profileConfig,
|
|
647
|
+
mappedName,
|
|
648
|
+
user
|
|
649
|
+
);
|
|
650
|
+
const claudeMd = generateClaudeMd({
|
|
651
|
+
thoughtsRepo: profileConfig.thoughtsRepo,
|
|
652
|
+
reposDir: profileConfig.reposDir,
|
|
653
|
+
repoName: mappedName,
|
|
654
|
+
user
|
|
655
|
+
});
|
|
656
|
+
fs5.writeFileSync(path8.join(thoughtsDir, "CLAUDE.md"), claudeMd);
|
|
657
|
+
let hooksUpdated = [];
|
|
658
|
+
if (setupHooks) {
|
|
659
|
+
const hookResult = setupGitHooks(repoPath);
|
|
660
|
+
hooksUpdated = hookResult.updated;
|
|
661
|
+
}
|
|
662
|
+
if (createSearchable) {
|
|
663
|
+
createSearchableIndex(thoughtsDir);
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
thoughtsDir,
|
|
667
|
+
otherUsers,
|
|
668
|
+
hooksUpdated
|
|
669
|
+
};
|
|
487
670
|
}
|
|
488
671
|
function setupGitHooks(repoPath) {
|
|
489
672
|
const updated = [];
|
|
490
673
|
let gitCommonDir;
|
|
491
674
|
try {
|
|
492
|
-
gitCommonDir =
|
|
675
|
+
gitCommonDir = execSync3("git rev-parse --git-common-dir", {
|
|
493
676
|
cwd: repoPath,
|
|
494
677
|
encoding: "utf8",
|
|
495
678
|
stdio: "pipe"
|
|
496
679
|
}).trim();
|
|
497
|
-
if (!
|
|
498
|
-
gitCommonDir =
|
|
680
|
+
if (!path8.isAbsolute(gitCommonDir)) {
|
|
681
|
+
gitCommonDir = path8.join(repoPath, gitCommonDir);
|
|
499
682
|
}
|
|
500
683
|
} catch (error) {
|
|
501
684
|
throw new Error(`Failed to find git common directory: ${error}`);
|
|
502
685
|
}
|
|
503
|
-
const hooksDir =
|
|
504
|
-
if (!
|
|
505
|
-
|
|
686
|
+
const hooksDir = path8.join(gitCommonDir, "hooks");
|
|
687
|
+
if (!fs5.existsSync(hooksDir)) {
|
|
688
|
+
fs5.mkdirSync(hooksDir, { recursive: true });
|
|
506
689
|
}
|
|
507
|
-
const preCommitPath =
|
|
690
|
+
const preCommitPath = path8.join(hooksDir, "pre-commit");
|
|
508
691
|
const preCommitContent = generatePreCommitHook({ hookPath: preCommitPath });
|
|
509
|
-
const postCommitPath =
|
|
692
|
+
const postCommitPath = path8.join(hooksDir, "post-commit");
|
|
510
693
|
const postCommitContent = generatePostCommitHook({ hookPath: postCommitPath });
|
|
511
694
|
const hookNeedsUpdate = (hookPath) => {
|
|
512
|
-
if (!
|
|
513
|
-
const content =
|
|
695
|
+
if (!fs5.existsSync(hookPath)) return true;
|
|
696
|
+
const content = fs5.readFileSync(hookPath, "utf8");
|
|
514
697
|
if (!content.includes("ThoughtCabinet thoughts")) return false;
|
|
515
698
|
const versionMatch = content.match(/# Version: (\d+)/);
|
|
516
699
|
if (!versionMatch) return true;
|
|
517
700
|
const currentVersion = parseInt(versionMatch[1]);
|
|
518
701
|
return currentVersion < parseInt(HOOK_VERSION);
|
|
519
702
|
};
|
|
520
|
-
if (
|
|
521
|
-
const content =
|
|
703
|
+
if (fs5.existsSync(preCommitPath)) {
|
|
704
|
+
const content = fs5.readFileSync(preCommitPath, "utf8");
|
|
522
705
|
if (!content.includes("ThoughtCabinet thoughts") || hookNeedsUpdate(preCommitPath)) {
|
|
523
706
|
if (!content.includes("ThoughtCabinet thoughts")) {
|
|
524
|
-
|
|
707
|
+
fs5.renameSync(preCommitPath, `${preCommitPath}.old`);
|
|
525
708
|
} else {
|
|
526
|
-
|
|
709
|
+
fs5.unlinkSync(preCommitPath);
|
|
527
710
|
}
|
|
528
711
|
}
|
|
529
712
|
}
|
|
530
|
-
if (
|
|
531
|
-
const content =
|
|
713
|
+
if (fs5.existsSync(postCommitPath)) {
|
|
714
|
+
const content = fs5.readFileSync(postCommitPath, "utf8");
|
|
532
715
|
if (!content.includes("ThoughtCabinet thoughts") || hookNeedsUpdate(postCommitPath)) {
|
|
533
716
|
if (!content.includes("ThoughtCabinet thoughts")) {
|
|
534
|
-
|
|
717
|
+
fs5.renameSync(postCommitPath, `${postCommitPath}.old`);
|
|
535
718
|
} else {
|
|
536
|
-
|
|
719
|
+
fs5.unlinkSync(postCommitPath);
|
|
537
720
|
}
|
|
538
721
|
}
|
|
539
722
|
}
|
|
540
|
-
if (!
|
|
541
|
-
|
|
542
|
-
|
|
723
|
+
if (!fs5.existsSync(preCommitPath) || hookNeedsUpdate(preCommitPath)) {
|
|
724
|
+
fs5.writeFileSync(preCommitPath, preCommitContent);
|
|
725
|
+
fs5.chmodSync(preCommitPath, "755");
|
|
543
726
|
updated.push("pre-commit");
|
|
544
727
|
}
|
|
545
|
-
if (!
|
|
546
|
-
|
|
547
|
-
|
|
728
|
+
if (!fs5.existsSync(postCommitPath) || hookNeedsUpdate(postCommitPath)) {
|
|
729
|
+
fs5.writeFileSync(postCommitPath, postCommitContent);
|
|
730
|
+
fs5.chmodSync(postCommitPath, "755");
|
|
548
731
|
updated.push("post-commit");
|
|
549
732
|
}
|
|
550
733
|
return { updated };
|
|
551
734
|
}
|
|
735
|
+
function createSearchableIndex(thoughtsDir) {
|
|
736
|
+
const searchDir = path8.join(thoughtsDir, "searchable");
|
|
737
|
+
if (fs5.existsSync(searchDir)) {
|
|
738
|
+
try {
|
|
739
|
+
execSync3(`chmod -R 755 "${searchDir}"`, { stdio: "pipe" });
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
fs5.rmSync(searchDir, { recursive: true, force: true });
|
|
743
|
+
}
|
|
744
|
+
fs5.mkdirSync(searchDir, { recursive: true });
|
|
745
|
+
function findFilesFollowingSymlinks(dir, baseDir = dir, visited = /* @__PURE__ */ new Set()) {
|
|
746
|
+
const files = [];
|
|
747
|
+
const realPath = fs5.realpathSync(dir);
|
|
748
|
+
if (visited.has(realPath)) {
|
|
749
|
+
return files;
|
|
750
|
+
}
|
|
751
|
+
visited.add(realPath);
|
|
752
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
753
|
+
for (const entry of entries) {
|
|
754
|
+
const fullPath = path8.join(dir, entry.name);
|
|
755
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
756
|
+
files.push(...findFilesFollowingSymlinks(fullPath, baseDir, visited));
|
|
757
|
+
} else if (entry.isSymbolicLink() && !entry.name.startsWith(".")) {
|
|
758
|
+
try {
|
|
759
|
+
const stat = fs5.statSync(fullPath);
|
|
760
|
+
if (stat.isDirectory()) {
|
|
761
|
+
files.push(...findFilesFollowingSymlinks(fullPath, baseDir, visited));
|
|
762
|
+
} else if (stat.isFile() && path8.basename(fullPath) !== "CLAUDE.md") {
|
|
763
|
+
files.push(path8.relative(baseDir, fullPath));
|
|
764
|
+
}
|
|
765
|
+
} catch {
|
|
766
|
+
}
|
|
767
|
+
} else if (entry.isFile() && !entry.name.startsWith(".") && entry.name !== "CLAUDE.md") {
|
|
768
|
+
files.push(path8.relative(baseDir, fullPath));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return files;
|
|
772
|
+
}
|
|
773
|
+
const allFiles = findFilesFollowingSymlinks(thoughtsDir);
|
|
774
|
+
let linkedCount = 0;
|
|
775
|
+
for (const relPath of allFiles) {
|
|
776
|
+
const sourcePath = path8.join(thoughtsDir, relPath);
|
|
777
|
+
const targetPath = path8.join(searchDir, relPath);
|
|
778
|
+
const targetDir = path8.dirname(targetPath);
|
|
779
|
+
if (!fs5.existsSync(targetDir)) {
|
|
780
|
+
fs5.mkdirSync(targetDir, { recursive: true });
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
const realSourcePath = fs5.realpathSync(sourcePath);
|
|
784
|
+
fs5.linkSync(realSourcePath, targetPath);
|
|
785
|
+
linkedCount++;
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return linkedCount;
|
|
790
|
+
}
|
|
791
|
+
function pullThoughtsFromRemote(thoughtsRepo) {
|
|
792
|
+
const expandedRepo = expandPath(thoughtsRepo);
|
|
793
|
+
try {
|
|
794
|
+
execSync3("git remote get-url origin", { cwd: expandedRepo, stdio: "pipe" });
|
|
795
|
+
try {
|
|
796
|
+
execSync3("git pull --rebase", {
|
|
797
|
+
stdio: "pipe",
|
|
798
|
+
cwd: expandedRepo
|
|
799
|
+
});
|
|
800
|
+
return true;
|
|
801
|
+
} catch {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
} catch {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/hooks/loader.ts
|
|
810
|
+
import fs6 from "fs";
|
|
811
|
+
import path9 from "path";
|
|
812
|
+
import chalk3 from "chalk";
|
|
813
|
+
var HOOKS_CONFIG_DIR = ".thought-cabinet";
|
|
814
|
+
var HOOKS_CONFIG_FILE = `${HOOKS_CONFIG_DIR}/hooks.json`;
|
|
815
|
+
function loadHooksConfig(repoPath) {
|
|
816
|
+
const configPath = path9.join(repoPath, HOOKS_CONFIG_FILE);
|
|
817
|
+
if (!fs6.existsSync(configPath)) {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const content = fs6.readFileSync(configPath, "utf8");
|
|
822
|
+
const config = JSON.parse(content);
|
|
823
|
+
if (!config.hooks || typeof config.hooks !== "object") {
|
|
824
|
+
console.error(
|
|
825
|
+
chalk3.yellow(`Warning: Invalid hooks config at ${configPath}: missing 'hooks' object`)
|
|
826
|
+
);
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
return config;
|
|
830
|
+
} catch (error) {
|
|
831
|
+
console.error(
|
|
832
|
+
chalk3.yellow(
|
|
833
|
+
`Warning: Could not parse hooks config at ${configPath}: ${error.message}`
|
|
834
|
+
)
|
|
835
|
+
);
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function getHooksForEvent(config, event) {
|
|
840
|
+
const hookGroups = config?.hooks[event];
|
|
841
|
+
if (!hookGroups) {
|
|
842
|
+
return [];
|
|
843
|
+
}
|
|
844
|
+
return hookGroups.flatMap((group) => group.hooks ?? []);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/hooks/executor.ts
|
|
848
|
+
import { spawn } from "child_process";
|
|
849
|
+
import chalk4 from "chalk";
|
|
850
|
+
var DEFAULT_TIMEOUT_SECONDS = 60;
|
|
851
|
+
function resolveExitCode(exitCode, timedOut) {
|
|
852
|
+
if (exitCode !== null) {
|
|
853
|
+
return exitCode;
|
|
854
|
+
}
|
|
855
|
+
if (timedOut) {
|
|
856
|
+
return 124;
|
|
857
|
+
}
|
|
858
|
+
return 1;
|
|
859
|
+
}
|
|
860
|
+
async function executeHook(hook, input, env = {}) {
|
|
861
|
+
const startTime = Date.now();
|
|
862
|
+
const timeoutMs = (hook.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
|
|
863
|
+
return new Promise((resolve) => {
|
|
864
|
+
let stdout = "";
|
|
865
|
+
let stderr = "";
|
|
866
|
+
let timedOut = false;
|
|
867
|
+
const child = spawn("bash", ["-c", hook.command], {
|
|
868
|
+
cwd: input.cwd,
|
|
869
|
+
env: { ...process.env, ...env },
|
|
870
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
871
|
+
});
|
|
872
|
+
const timer = setTimeout(() => {
|
|
873
|
+
timedOut = true;
|
|
874
|
+
child.kill("SIGTERM");
|
|
875
|
+
setTimeout(() => {
|
|
876
|
+
if (!child.killed) {
|
|
877
|
+
child.kill("SIGKILL");
|
|
878
|
+
}
|
|
879
|
+
}, 5e3);
|
|
880
|
+
}, timeoutMs);
|
|
881
|
+
child.stdin.write(JSON.stringify(input, null, 2));
|
|
882
|
+
child.stdin.end();
|
|
883
|
+
child.stdout.on("data", (data) => {
|
|
884
|
+
stdout += data.toString();
|
|
885
|
+
});
|
|
886
|
+
child.stderr.on("data", (data) => {
|
|
887
|
+
stderr += data.toString();
|
|
888
|
+
});
|
|
889
|
+
child.on("close", (exitCode) => {
|
|
890
|
+
clearTimeout(timer);
|
|
891
|
+
const duration = Date.now() - startTime;
|
|
892
|
+
resolve({
|
|
893
|
+
success: exitCode === 0 && !timedOut,
|
|
894
|
+
exitCode: resolveExitCode(exitCode, timedOut),
|
|
895
|
+
stdout: stdout.trim(),
|
|
896
|
+
stderr: stderr.trim(),
|
|
897
|
+
timedOut,
|
|
898
|
+
duration
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
child.on("error", (error) => {
|
|
902
|
+
clearTimeout(timer);
|
|
903
|
+
const duration = Date.now() - startTime;
|
|
904
|
+
resolve({
|
|
905
|
+
success: false,
|
|
906
|
+
exitCode: 1,
|
|
907
|
+
stdout: stdout.trim(),
|
|
908
|
+
stderr: `Failed to execute hook: ${error.message}`,
|
|
909
|
+
timedOut: false,
|
|
910
|
+
duration
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function displayHookResult(hook, result, verbose) {
|
|
916
|
+
const timeoutSeconds = hook.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
917
|
+
if (result.timedOut) {
|
|
918
|
+
console.log(chalk4.yellow(`Warning: Hook timed out after ${timeoutSeconds}s: ${hook.command}`));
|
|
919
|
+
if (result.stderr) {
|
|
920
|
+
console.log(chalk4.yellow(result.stderr));
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
switch (result.exitCode) {
|
|
925
|
+
case 0:
|
|
926
|
+
console.log(chalk4.gray(`Hook completed (${result.duration}ms): ${hook.command}`));
|
|
927
|
+
if (verbose && result.stdout) {
|
|
928
|
+
console.log(chalk4.gray(result.stdout));
|
|
929
|
+
}
|
|
930
|
+
break;
|
|
931
|
+
case 2:
|
|
932
|
+
console.log(chalk4.red(`Hook failed with exit code 2: ${hook.command}`));
|
|
933
|
+
if (result.stderr) {
|
|
934
|
+
console.log(chalk4.red(result.stderr));
|
|
935
|
+
}
|
|
936
|
+
break;
|
|
937
|
+
default:
|
|
938
|
+
console.log(
|
|
939
|
+
chalk4.yellow(`Warning: Hook failed with exit code ${result.exitCode}: ${hook.command}`)
|
|
940
|
+
);
|
|
941
|
+
if (verbose && result.stderr) {
|
|
942
|
+
console.log(chalk4.yellow(result.stderr));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
async function executeHooks(hooks, input, env = {}, verbose = false) {
|
|
947
|
+
if (hooks.length === 0) {
|
|
948
|
+
return [];
|
|
949
|
+
}
|
|
950
|
+
if (verbose) {
|
|
951
|
+
console.log(chalk4.gray(`
|
|
952
|
+
Executing ${hooks.length} hook(s) for ${input.hook_event_name}...`));
|
|
953
|
+
}
|
|
954
|
+
const results = await Promise.all(hooks.map((hook) => executeHook(hook, input, env)));
|
|
955
|
+
for (let i = 0; i < results.length; i++) {
|
|
956
|
+
displayHookResult(hooks[i], results[i], verbose);
|
|
957
|
+
}
|
|
958
|
+
return results;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/commands/thoughts/init.ts
|
|
962
|
+
function sanitizeDirectoryName(name) {
|
|
963
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
964
|
+
}
|
|
965
|
+
function checkExistingSetup(config) {
|
|
966
|
+
const thoughtsDir = path10.join(process.cwd(), "thoughts");
|
|
967
|
+
if (!fs7.existsSync(thoughtsDir)) {
|
|
968
|
+
return { exists: false, isValid: false };
|
|
969
|
+
}
|
|
970
|
+
if (!fs7.lstatSync(thoughtsDir).isDirectory()) {
|
|
971
|
+
return { exists: true, isValid: false, message: "thoughts exists but is not a directory" };
|
|
972
|
+
}
|
|
973
|
+
if (!config) {
|
|
974
|
+
return {
|
|
975
|
+
exists: true,
|
|
976
|
+
isValid: false,
|
|
977
|
+
message: "thoughts directory exists but configuration is missing"
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
const userPath = path10.join(thoughtsDir, config.user);
|
|
981
|
+
const sharedPath = path10.join(thoughtsDir, "shared");
|
|
982
|
+
const globalPath = path10.join(thoughtsDir, "global");
|
|
983
|
+
const hasUser = fs7.existsSync(userPath) && fs7.lstatSync(userPath).isSymbolicLink();
|
|
984
|
+
const hasShared = fs7.existsSync(sharedPath) && fs7.lstatSync(sharedPath).isSymbolicLink();
|
|
985
|
+
const hasGlobal = fs7.existsSync(globalPath) && fs7.lstatSync(globalPath).isSymbolicLink();
|
|
986
|
+
if (!hasUser || !hasShared || !hasGlobal) {
|
|
987
|
+
return {
|
|
988
|
+
exists: true,
|
|
989
|
+
isValid: false,
|
|
990
|
+
message: "thoughts directory exists but symlinks are missing or broken"
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
return { exists: true, isValid: true };
|
|
994
|
+
}
|
|
552
995
|
async function thoughtsInitCommand(options) {
|
|
553
996
|
try {
|
|
554
997
|
if (!options.directory && !process.stdin.isTTY) {
|
|
@@ -558,18 +1001,18 @@ async function thoughtsInitCommand(options) {
|
|
|
558
1001
|
}
|
|
559
1002
|
const currentRepo = getCurrentRepoPath();
|
|
560
1003
|
try {
|
|
561
|
-
|
|
1004
|
+
execSync4("git rev-parse --git-dir", { stdio: "pipe" });
|
|
562
1005
|
} catch {
|
|
563
1006
|
p.log.error("Not in a git repository");
|
|
564
1007
|
process.exit(1);
|
|
565
1008
|
}
|
|
566
1009
|
let config = loadThoughtsConfig(options);
|
|
567
1010
|
if (!config) {
|
|
568
|
-
p.intro(
|
|
1011
|
+
p.intro(chalk5.blue("Initial Thoughts Setup"));
|
|
569
1012
|
p.log.info("First, let's configure your global thoughts system.");
|
|
570
1013
|
const defaultRepo = getDefaultThoughtsRepo();
|
|
571
1014
|
p.log.message(
|
|
572
|
-
|
|
1015
|
+
chalk5.gray("This is where all your thoughts across all projects will be stored.")
|
|
573
1016
|
);
|
|
574
1017
|
const thoughtsRepoInput = await p.text({
|
|
575
1018
|
message: "Thoughts repository location:",
|
|
@@ -581,9 +1024,9 @@ async function thoughtsInitCommand(options) {
|
|
|
581
1024
|
process.exit(0);
|
|
582
1025
|
}
|
|
583
1026
|
const thoughtsRepo = thoughtsRepoInput || defaultRepo;
|
|
584
|
-
p.log.message(
|
|
585
|
-
p.log.message(
|
|
586
|
-
p.log.message(
|
|
1027
|
+
p.log.message(chalk5.gray("Your thoughts will be organized into two main directories:"));
|
|
1028
|
+
p.log.message(chalk5.gray("- Repository-specific thoughts (one subdirectory per project)"));
|
|
1029
|
+
p.log.message(chalk5.gray("- Global thoughts (shared across all projects)"));
|
|
587
1030
|
const reposDirInput = await p.text({
|
|
588
1031
|
message: "Directory name for repository-specific thoughts:",
|
|
589
1032
|
initialValue: "repos",
|
|
@@ -632,9 +1075,9 @@ async function thoughtsInitCommand(options) {
|
|
|
632
1075
|
repoMappings: {}
|
|
633
1076
|
};
|
|
634
1077
|
p.note(
|
|
635
|
-
`${
|
|
636
|
-
\u251C\u2500\u2500 ${
|
|
637
|
-
\u2514\u2500\u2500 ${
|
|
1078
|
+
`${chalk5.cyan(thoughtsRepo)}/
|
|
1079
|
+
\u251C\u2500\u2500 ${chalk5.cyan(reposDir2)}/ ${chalk5.gray("(project-specific thoughts)")}
|
|
1080
|
+
\u2514\u2500\u2500 ${chalk5.cyan(globalDir)}/ ${chalk5.gray("(cross-project thoughts)")}`,
|
|
638
1081
|
"Creating thoughts structure"
|
|
639
1082
|
);
|
|
640
1083
|
ensureThoughtsRepoExists(thoughtsRepo, reposDir2, globalDir);
|
|
@@ -644,16 +1087,16 @@ async function thoughtsInitCommand(options) {
|
|
|
644
1087
|
if (options.profile) {
|
|
645
1088
|
if (!validateProfile(config, options.profile)) {
|
|
646
1089
|
p.log.error(`Profile "${options.profile}" does not exist.`);
|
|
647
|
-
p.log.message(
|
|
1090
|
+
p.log.message(chalk5.gray("Available profiles:"));
|
|
648
1091
|
if (config.profiles) {
|
|
649
1092
|
Object.keys(config.profiles).forEach((name) => {
|
|
650
|
-
p.log.message(
|
|
1093
|
+
p.log.message(chalk5.gray(` - ${name}`));
|
|
651
1094
|
});
|
|
652
1095
|
} else {
|
|
653
|
-
p.log.message(
|
|
1096
|
+
p.log.message(chalk5.gray(" (none)"));
|
|
654
1097
|
}
|
|
655
1098
|
p.log.warn("Create a profile first:");
|
|
656
|
-
p.log.message(
|
|
1099
|
+
p.log.message(chalk5.gray(` thoughtcabinet profile create ${options.profile}`));
|
|
657
1100
|
process.exit(1);
|
|
658
1101
|
}
|
|
659
1102
|
}
|
|
@@ -692,8 +1135,8 @@ async function thoughtsInitCommand(options) {
|
|
|
692
1135
|
}
|
|
693
1136
|
}
|
|
694
1137
|
}
|
|
695
|
-
|
|
696
|
-
if (!
|
|
1138
|
+
let expandedRepo = expandPath(tempProfileConfig.thoughtsRepo);
|
|
1139
|
+
if (!fs7.existsSync(expandedRepo)) {
|
|
697
1140
|
p.log.error(`Thoughts repository not found at ${tempProfileConfig.thoughtsRepo}`);
|
|
698
1141
|
p.log.warn("The thoughts repository may have been moved or deleted.");
|
|
699
1142
|
const recreate = await p.confirm({
|
|
@@ -710,16 +1153,24 @@ async function thoughtsInitCommand(options) {
|
|
|
710
1153
|
tempProfileConfig.globalDir
|
|
711
1154
|
);
|
|
712
1155
|
}
|
|
713
|
-
const reposDir =
|
|
714
|
-
if (!
|
|
715
|
-
|
|
1156
|
+
const reposDir = path10.join(expandedRepo, tempProfileConfig.reposDir);
|
|
1157
|
+
if (!fs7.existsSync(reposDir)) {
|
|
1158
|
+
fs7.mkdirSync(reposDir, { recursive: true });
|
|
716
1159
|
}
|
|
717
|
-
const existingRepos =
|
|
718
|
-
const fullPath =
|
|
719
|
-
return
|
|
1160
|
+
const existingRepos = fs7.readdirSync(reposDir).filter((name) => {
|
|
1161
|
+
const fullPath = path10.join(reposDir, name);
|
|
1162
|
+
return fs7.statSync(fullPath).isDirectory() && !name.startsWith(".");
|
|
720
1163
|
});
|
|
721
1164
|
const existingMapping = config.repoMappings[currentRepo];
|
|
722
1165
|
let mappedName = getRepoNameFromMapping(existingMapping);
|
|
1166
|
+
let mainRepoMapping;
|
|
1167
|
+
let mainRepoPath = null;
|
|
1168
|
+
if (!mappedName) {
|
|
1169
|
+
mainRepoPath = getMainRepoPath();
|
|
1170
|
+
if (mainRepoPath && config.repoMappings[mainRepoPath]) {
|
|
1171
|
+
mainRepoMapping = config.repoMappings[mainRepoPath];
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
723
1174
|
if (!mappedName) {
|
|
724
1175
|
if (options.directory) {
|
|
725
1176
|
const sanitizedDir = sanitizeDirectoryName(options.directory);
|
|
@@ -728,7 +1179,7 @@ async function thoughtsInitCommand(options) {
|
|
|
728
1179
|
p.log.error("In non-interactive mode (--directory), you must specify a directory");
|
|
729
1180
|
p.log.error("name that already exists in the thoughts repository.");
|
|
730
1181
|
p.log.warn("Available directories:");
|
|
731
|
-
existingRepos.forEach((repo) => p.log.message(
|
|
1182
|
+
existingRepos.forEach((repo) => p.log.message(chalk5.gray(` - ${repo}`)));
|
|
732
1183
|
process.exit(1);
|
|
733
1184
|
}
|
|
734
1185
|
mappedName = sanitizedDir;
|
|
@@ -736,22 +1187,36 @@ async function thoughtsInitCommand(options) {
|
|
|
736
1187
|
`Using existing: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/${mappedName}`
|
|
737
1188
|
);
|
|
738
1189
|
} else {
|
|
739
|
-
p.intro(
|
|
740
|
-
p.log.info(`Setting up thoughts for: ${
|
|
1190
|
+
p.intro(chalk5.blue("Repository Setup"));
|
|
1191
|
+
p.log.info(`Setting up thoughts for: ${chalk5.cyan(currentRepo)}`);
|
|
741
1192
|
p.log.message(
|
|
742
|
-
|
|
1193
|
+
chalk5.gray(
|
|
743
1194
|
`This will create a subdirectory in ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/`
|
|
744
1195
|
)
|
|
745
1196
|
);
|
|
746
|
-
p.log.message(
|
|
747
|
-
if (existingRepos.length > 0) {
|
|
748
|
-
const selectOptions = [
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1197
|
+
p.log.message(chalk5.gray("to store thoughts specific to this repository."));
|
|
1198
|
+
if (existingRepos.length > 0 || mainRepoMapping) {
|
|
1199
|
+
const selectOptions = [];
|
|
1200
|
+
let initialValue;
|
|
1201
|
+
const mainRepoMappedName = getRepoNameFromMapping(mainRepoMapping);
|
|
1202
|
+
if (mainRepoMappedName && existingRepos.includes(mainRepoMappedName)) {
|
|
1203
|
+
selectOptions.push({
|
|
1204
|
+
value: mainRepoMappedName,
|
|
1205
|
+
label: `Use existing: ${mainRepoMappedName} (from main repository)`
|
|
1206
|
+
});
|
|
1207
|
+
initialValue = mainRepoMappedName;
|
|
1208
|
+
}
|
|
1209
|
+
existingRepos.filter((repo) => repo !== mainRepoMappedName).forEach((repo) => {
|
|
1210
|
+
selectOptions.push({ value: repo, label: `Use existing: ${repo}` });
|
|
1211
|
+
});
|
|
1212
|
+
selectOptions.push({ value: "__create_new__", label: "Create new directory" });
|
|
1213
|
+
if (!initialValue) {
|
|
1214
|
+
initialValue = "__create_new__";
|
|
1215
|
+
}
|
|
752
1216
|
const selection = await p.select({
|
|
753
1217
|
message: "Select or create a thoughts directory for this repository:",
|
|
754
|
-
options: selectOptions
|
|
1218
|
+
options: selectOptions,
|
|
1219
|
+
initialValue
|
|
755
1220
|
});
|
|
756
1221
|
if (p.isCancel(selection)) {
|
|
757
1222
|
p.cancel("Operation cancelled.");
|
|
@@ -760,7 +1225,7 @@ async function thoughtsInitCommand(options) {
|
|
|
760
1225
|
if (selection === "__create_new__") {
|
|
761
1226
|
const defaultName = getRepoNameFromPath(currentRepo);
|
|
762
1227
|
p.log.message(
|
|
763
|
-
|
|
1228
|
+
chalk5.gray(
|
|
764
1229
|
`This name will be used for the directory: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/[name]`
|
|
765
1230
|
)
|
|
766
1231
|
);
|
|
@@ -780,6 +1245,13 @@ async function thoughtsInitCommand(options) {
|
|
|
780
1245
|
);
|
|
781
1246
|
} else {
|
|
782
1247
|
mappedName = selection;
|
|
1248
|
+
if (mainRepoMapping && mappedName === mainRepoMappedName) {
|
|
1249
|
+
const inheritedProfile2 = getProfileNameFromMapping(mainRepoMapping);
|
|
1250
|
+
if (inheritedProfile2 && !options.profile) {
|
|
1251
|
+
options.profile = inheritedProfile2;
|
|
1252
|
+
p.log.info(`Inheriting profile "${inheritedProfile2}" from main repository`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
783
1255
|
p.log.success(
|
|
784
1256
|
`Will use existing: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/${mappedName}`
|
|
785
1257
|
);
|
|
@@ -787,7 +1259,7 @@ async function thoughtsInitCommand(options) {
|
|
|
787
1259
|
} else {
|
|
788
1260
|
const defaultName = getRepoNameFromPath(currentRepo);
|
|
789
1261
|
p.log.message(
|
|
790
|
-
|
|
1262
|
+
chalk5.gray(
|
|
791
1263
|
`This name will be used for the directory: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/[name]`
|
|
792
1264
|
)
|
|
793
1265
|
);
|
|
@@ -816,29 +1288,36 @@ async function thoughtsInitCommand(options) {
|
|
|
816
1288
|
config.repoMappings[currentRepo] = mappedName;
|
|
817
1289
|
}
|
|
818
1290
|
saveThoughtsConfig(config, options);
|
|
1291
|
+
const inheritedProfile = getProfileNameFromMapping(mainRepoMapping);
|
|
1292
|
+
if (inheritedProfile && options.profile === inheritedProfile && tempProfileConfig.profileName !== inheritedProfile) {
|
|
1293
|
+
const profileSettings = config.profiles?.[inheritedProfile];
|
|
1294
|
+
if (profileSettings) {
|
|
1295
|
+
expandedRepo = expandPath(profileSettings.thoughtsRepo);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
819
1298
|
}
|
|
820
1299
|
if (!mappedName) {
|
|
821
1300
|
mappedName = getRepoNameFromMapping(config.repoMappings[currentRepo]);
|
|
822
1301
|
}
|
|
823
1302
|
const profileConfig = resolveProfileForRepo(config, currentRepo);
|
|
824
1303
|
createThoughtsDirectoryStructure(profileConfig, mappedName, config.user);
|
|
825
|
-
const thoughtsDir =
|
|
826
|
-
if (
|
|
827
|
-
const searchableDir =
|
|
828
|
-
if (
|
|
1304
|
+
const thoughtsDir = path10.join(currentRepo, "thoughts");
|
|
1305
|
+
if (fs7.existsSync(thoughtsDir)) {
|
|
1306
|
+
const searchableDir = path10.join(thoughtsDir, "searchable");
|
|
1307
|
+
if (fs7.existsSync(searchableDir)) {
|
|
829
1308
|
try {
|
|
830
|
-
|
|
1309
|
+
execSync4(`chmod -R 755 "${searchableDir}"`, { stdio: "pipe" });
|
|
831
1310
|
} catch {
|
|
832
1311
|
}
|
|
833
1312
|
}
|
|
834
|
-
|
|
1313
|
+
fs7.rmSync(thoughtsDir, { recursive: true, force: true });
|
|
835
1314
|
}
|
|
836
|
-
|
|
1315
|
+
fs7.mkdirSync(thoughtsDir);
|
|
837
1316
|
const repoTarget = getRepoThoughtsPath(profileConfig, mappedName);
|
|
838
1317
|
const globalTarget = getGlobalThoughtsPath(profileConfig);
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1318
|
+
fs7.symlinkSync(path10.join(repoTarget, config.user), path10.join(thoughtsDir, config.user), "dir");
|
|
1319
|
+
fs7.symlinkSync(path10.join(repoTarget, "shared"), path10.join(thoughtsDir, "shared"), "dir");
|
|
1320
|
+
fs7.symlinkSync(globalTarget, path10.join(thoughtsDir, "global"), "dir");
|
|
842
1321
|
const otherUsers = updateSymlinksForNewUsers(
|
|
843
1322
|
currentRepo,
|
|
844
1323
|
profileConfig,
|
|
@@ -848,18 +1327,8 @@ async function thoughtsInitCommand(options) {
|
|
|
848
1327
|
if (otherUsers.length > 0) {
|
|
849
1328
|
p.log.success(`Added symlinks for other users: ${otherUsers.join(", ")}`);
|
|
850
1329
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
try {
|
|
854
|
-
execSync2("git pull --rebase", {
|
|
855
|
-
stdio: "pipe",
|
|
856
|
-
cwd: expandedRepo
|
|
857
|
-
});
|
|
858
|
-
p.log.success("Pulled latest thoughts from remote");
|
|
859
|
-
} catch (error) {
|
|
860
|
-
p.log.warn(`Could not pull latest thoughts: ${error.message}`);
|
|
861
|
-
}
|
|
862
|
-
} catch {
|
|
1330
|
+
if (pullThoughtsFromRemote(expandedRepo)) {
|
|
1331
|
+
p.log.success("Pulled latest thoughts from remote");
|
|
863
1332
|
}
|
|
864
1333
|
const claudeMd = generateClaudeMd({
|
|
865
1334
|
thoughtsRepo: profileConfig.thoughtsRepo,
|
|
@@ -867,34 +1336,55 @@ async function thoughtsInitCommand(options) {
|
|
|
867
1336
|
repoName: mappedName,
|
|
868
1337
|
user: config.user
|
|
869
1338
|
});
|
|
870
|
-
|
|
1339
|
+
fs7.writeFileSync(path10.join(thoughtsDir, "CLAUDE.md"), claudeMd);
|
|
871
1340
|
const hookResult = setupGitHooks(currentRepo);
|
|
872
1341
|
if (hookResult.updated.length > 0) {
|
|
873
1342
|
p.log.step(`Updated git hooks: ${hookResult.updated.join(", ")}`);
|
|
874
1343
|
}
|
|
875
1344
|
p.log.success("Thoughts setup complete!");
|
|
876
|
-
const structureText = `${
|
|
1345
|
+
const structureText = `${chalk5.cyan(currentRepo)}/
|
|
877
1346
|
\u2514\u2500\u2500 thoughts/
|
|
878
|
-
\u251C\u2500\u2500 ${config.user}/ ${
|
|
879
|
-
\u251C\u2500\u2500 shared/ ${
|
|
880
|
-
\u2514\u2500\u2500 global/ ${
|
|
881
|
-
\u251C\u2500\u2500 ${config.user}/ ${
|
|
882
|
-
\u2514\u2500\u2500 shared/ ${
|
|
1347
|
+
\u251C\u2500\u2500 ${config.user}/ ${chalk5.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.reposDir}/${mappedName}/${config.user}/`)}
|
|
1348
|
+
\u251C\u2500\u2500 shared/ ${chalk5.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.reposDir}/${mappedName}/shared/`)}
|
|
1349
|
+
\u2514\u2500\u2500 global/ ${chalk5.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.globalDir}/`)}
|
|
1350
|
+
\u251C\u2500\u2500 ${config.user}/ ${chalk5.gray("(your cross-repo notes)")}
|
|
1351
|
+
\u2514\u2500\u2500 shared/ ${chalk5.gray("(team cross-repo notes)")}`;
|
|
883
1352
|
p.note(structureText, "Repository structure created");
|
|
884
1353
|
p.note(
|
|
885
|
-
`${
|
|
886
|
-
${
|
|
1354
|
+
`${chalk5.green("\u2713")} Pre-commit hook: Prevents committing thoughts/
|
|
1355
|
+
${chalk5.green("\u2713")} Post-commit hook: Auto-syncs thoughts after commits`,
|
|
887
1356
|
"Protection enabled"
|
|
888
1357
|
);
|
|
1358
|
+
const hooksConfig = loadHooksConfig(currentRepo);
|
|
1359
|
+
const postInitHooks = getHooksForEvent(hooksConfig, "PostThoughtsInit");
|
|
1360
|
+
if (postInitHooks.length > 0) {
|
|
1361
|
+
const hookInput = {
|
|
1362
|
+
hook_event_name: "PostThoughtsInit",
|
|
1363
|
+
cwd: currentRepo,
|
|
1364
|
+
thoughts_repo: profileConfig.thoughtsRepo,
|
|
1365
|
+
repos_dir: profileConfig.reposDir,
|
|
1366
|
+
global_dir: profileConfig.globalDir,
|
|
1367
|
+
mapped_name: mappedName,
|
|
1368
|
+
user: config.user
|
|
1369
|
+
};
|
|
1370
|
+
const hookEnv = {
|
|
1371
|
+
THC_THOUGHTS_REPO: profileConfig.thoughtsRepo,
|
|
1372
|
+
THC_REPOS_DIR: profileConfig.reposDir,
|
|
1373
|
+
THC_GLOBAL_DIR: profileConfig.globalDir,
|
|
1374
|
+
THC_MAPPED_NAME: mappedName,
|
|
1375
|
+
THC_USER: config.user
|
|
1376
|
+
};
|
|
1377
|
+
await executeHooks(postInitHooks, hookInput, hookEnv, true);
|
|
1378
|
+
}
|
|
889
1379
|
p.outro(
|
|
890
|
-
|
|
891
|
-
` 1. Run ${
|
|
1380
|
+
chalk5.gray("Next steps:\n") + chalk5.gray(
|
|
1381
|
+
` 1. Run ${chalk5.cyan("thoughtcabinet sync")} to create the searchable index
|
|
892
1382
|
`
|
|
893
|
-
) +
|
|
894
|
-
` 2. Create markdown files in ${
|
|
1383
|
+
) + chalk5.gray(
|
|
1384
|
+
` 2. Create markdown files in ${chalk5.cyan(`thoughts/${config.user}/`)} for your notes
|
|
895
1385
|
`
|
|
896
|
-
) +
|
|
897
|
-
`) +
|
|
1386
|
+
) + chalk5.gray(` 3. Your thoughts will sync automatically when you commit code
|
|
1387
|
+
`) + chalk5.gray(` 4. Run ${chalk5.cyan("thoughtcabinet status")} to check sync status`)
|
|
898
1388
|
);
|
|
899
1389
|
} catch (error) {
|
|
900
1390
|
p.log.error(`Error during thoughts init: ${error}`);
|
|
@@ -903,81 +1393,84 @@ ${chalk2.green("\u2713")} Post-commit hook: Auto-syncs thoughts after commits`,
|
|
|
903
1393
|
}
|
|
904
1394
|
|
|
905
1395
|
// src/commands/thoughts/destroy.ts
|
|
906
|
-
import
|
|
907
|
-
import
|
|
908
|
-
import
|
|
909
|
-
import chalk3 from "chalk";
|
|
1396
|
+
import fs8 from "fs";
|
|
1397
|
+
import path11 from "path";
|
|
1398
|
+
import chalk6 from "chalk";
|
|
910
1399
|
async function thoughtsDestoryCommand(options) {
|
|
911
1400
|
try {
|
|
912
1401
|
const currentRepo = getCurrentRepoPath();
|
|
913
|
-
const thoughtsDir =
|
|
914
|
-
if (!
|
|
915
|
-
console.error(
|
|
916
|
-
process.exit(1);
|
|
917
|
-
}
|
|
918
|
-
const config = loadThoughtsConfig(options);
|
|
919
|
-
if (!config) {
|
|
920
|
-
console.error(chalk3.red("Error: Thoughts configuration not found."));
|
|
921
|
-
process.exit(1);
|
|
922
|
-
}
|
|
923
|
-
const mapping = config.repoMappings[currentRepo];
|
|
924
|
-
const mappedName = getRepoNameFromMapping(mapping);
|
|
925
|
-
const profileName = getProfileNameFromMapping(mapping);
|
|
926
|
-
if (!mappedName && !options.force) {
|
|
927
|
-
console.error(chalk3.red("Error: This repository is not in the thoughts configuration."));
|
|
928
|
-
console.error(chalk3.yellow("Use --force to remove the thoughts directory anyway."));
|
|
929
|
-
process.exit(1);
|
|
930
|
-
}
|
|
931
|
-
console.log(chalk3.blue("Removing thoughts setup from current repository..."));
|
|
932
|
-
const searchableDir = path7.join(thoughtsDir, "searchable");
|
|
933
|
-
if (fs5.existsSync(searchableDir)) {
|
|
934
|
-
console.log(chalk3.gray("Removing searchable directory..."));
|
|
935
|
-
try {
|
|
936
|
-
execSync3(`chmod -R 755 "${searchableDir}"`, { stdio: "pipe" });
|
|
937
|
-
} catch {
|
|
938
|
-
}
|
|
939
|
-
fs5.rmSync(searchableDir, { recursive: true, force: true });
|
|
1402
|
+
const thoughtsDir = path11.join(currentRepo, "thoughts");
|
|
1403
|
+
if (!fs8.existsSync(thoughtsDir)) {
|
|
1404
|
+
console.error(chalk6.red("Error: Thoughts not initialized for this repository."));
|
|
1405
|
+
process.exit(1);
|
|
940
1406
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
} catch (error) {
|
|
945
|
-
console.error(chalk3.red(`Error removing thoughts directory: ${error}`));
|
|
946
|
-
console.error(chalk3.yellow("You may need to manually remove: " + thoughtsDir));
|
|
1407
|
+
const config = loadThoughtsConfig(options);
|
|
1408
|
+
if (!config) {
|
|
1409
|
+
console.error(chalk6.red("Error: Thoughts configuration not found."));
|
|
947
1410
|
process.exit(1);
|
|
948
1411
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1412
|
+
const mapping = config.repoMappings[currentRepo];
|
|
1413
|
+
if (!mapping && !options.force) {
|
|
1414
|
+
console.error(chalk6.red("Error: This repository is not in the thoughts configuration."));
|
|
1415
|
+
console.error(chalk6.yellow("Use --force to remove the thoughts directory anyway."));
|
|
1416
|
+
process.exit(1);
|
|
1417
|
+
}
|
|
1418
|
+
console.log(chalk6.blue("Removing thoughts setup from current repository..."));
|
|
1419
|
+
const result = cleanupThoughtsDirectory({
|
|
1420
|
+
repoPath: currentRepo,
|
|
1421
|
+
config,
|
|
1422
|
+
force: options.force,
|
|
1423
|
+
verbose: true
|
|
1424
|
+
});
|
|
1425
|
+
if (result.configRemoved) {
|
|
952
1426
|
saveThoughtsConfig(config, options);
|
|
953
1427
|
}
|
|
954
|
-
console.log(
|
|
955
|
-
if (mappedName) {
|
|
1428
|
+
console.log(chalk6.green("\u2705 Thoughts removed from repository"));
|
|
1429
|
+
if (result.mappedName) {
|
|
956
1430
|
console.log("");
|
|
957
|
-
console.log(
|
|
958
|
-
if (profileName && config.profiles && config.profiles[profileName]) {
|
|
959
|
-
const profile = config.profiles[profileName];
|
|
960
|
-
console.log(
|
|
961
|
-
console.log(
|
|
1431
|
+
console.log(chalk6.gray("Note: Your thoughts content remains safe in:"));
|
|
1432
|
+
if (result.profileName && config.profiles && config.profiles[result.profileName]) {
|
|
1433
|
+
const profile = config.profiles[result.profileName];
|
|
1434
|
+
console.log(chalk6.gray(` ${profile.thoughtsRepo}/${profile.reposDir}/${result.mappedName}`));
|
|
1435
|
+
console.log(chalk6.gray(` (profile: ${result.profileName})`));
|
|
962
1436
|
} else {
|
|
963
|
-
console.log(
|
|
1437
|
+
console.log(chalk6.gray(` ${config.thoughtsRepo}/${config.reposDir}/${result.mappedName}`));
|
|
964
1438
|
}
|
|
965
|
-
console.log(
|
|
1439
|
+
console.log(chalk6.gray("Only the local symlinks and configuration were removed."));
|
|
1440
|
+
}
|
|
1441
|
+
const hooksConfig = loadHooksConfig(currentRepo);
|
|
1442
|
+
const postDestroyHooks = getHooksForEvent(hooksConfig, "PostThoughtsDestroy");
|
|
1443
|
+
if (postDestroyHooks.length > 0) {
|
|
1444
|
+
const hookInput = {
|
|
1445
|
+
hook_event_name: "PostThoughtsDestroy",
|
|
1446
|
+
cwd: currentRepo,
|
|
1447
|
+
thoughts_removed: result.thoughtsRemoved,
|
|
1448
|
+
config_removed: result.configRemoved,
|
|
1449
|
+
mapped_name: result.mappedName,
|
|
1450
|
+
profile_name: result.profileName
|
|
1451
|
+
};
|
|
1452
|
+
const hookEnv = {
|
|
1453
|
+
THC_THOUGHTS_REMOVED: result.thoughtsRemoved ? "true" : "false",
|
|
1454
|
+
THC_CONFIG_REMOVED: result.configRemoved ? "true" : "false",
|
|
1455
|
+
THC_MAPPED_NAME: result.mappedName || "",
|
|
1456
|
+
THC_PROFILE_NAME: result.profileName || ""
|
|
1457
|
+
};
|
|
1458
|
+
await executeHooks(postDestroyHooks, hookInput, hookEnv, true);
|
|
966
1459
|
}
|
|
967
1460
|
} catch (error) {
|
|
968
|
-
console.error(
|
|
1461
|
+
console.error(chalk6.red(`Error during thoughts destroy: ${error}`));
|
|
969
1462
|
process.exit(1);
|
|
970
1463
|
}
|
|
971
1464
|
}
|
|
972
1465
|
|
|
973
1466
|
// src/commands/thoughts/sync.ts
|
|
974
|
-
import
|
|
975
|
-
import
|
|
976
|
-
import { execSync as
|
|
977
|
-
import
|
|
1467
|
+
import fs9 from "fs";
|
|
1468
|
+
import path12 from "path";
|
|
1469
|
+
import { execSync as execSync5, execFileSync as execFileSync2 } from "child_process";
|
|
1470
|
+
import chalk7 from "chalk";
|
|
978
1471
|
function checkGitStatus(repoPath) {
|
|
979
1472
|
try {
|
|
980
|
-
const status =
|
|
1473
|
+
const status = execSync5("git status --porcelain", {
|
|
981
1474
|
cwd: repoPath,
|
|
982
1475
|
encoding: "utf8",
|
|
983
1476
|
stdio: "pipe"
|
|
@@ -990,115 +1483,59 @@ function checkGitStatus(repoPath) {
|
|
|
990
1483
|
function syncThoughts(thoughtsRepo, message) {
|
|
991
1484
|
const expandedRepo = expandPath(thoughtsRepo);
|
|
992
1485
|
try {
|
|
993
|
-
|
|
1486
|
+
execSync5("git add -A", { cwd: expandedRepo, stdio: "pipe" });
|
|
994
1487
|
const hasChanges = checkGitStatus(expandedRepo);
|
|
995
1488
|
if (hasChanges) {
|
|
996
1489
|
const commitMessage = message || `Sync thoughts - ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
997
|
-
|
|
998
|
-
console.log(
|
|
1490
|
+
execFileSync2("git", ["commit", "-m", commitMessage], { cwd: expandedRepo, stdio: "pipe" });
|
|
1491
|
+
console.log(chalk7.green("\u2705 Thoughts synchronized"));
|
|
999
1492
|
} else {
|
|
1000
|
-
console.log(
|
|
1493
|
+
console.log(chalk7.gray("No changes to commit"));
|
|
1001
1494
|
}
|
|
1002
1495
|
try {
|
|
1003
|
-
|
|
1496
|
+
execSync5("git pull --rebase", {
|
|
1004
1497
|
stdio: "pipe",
|
|
1005
1498
|
cwd: expandedRepo
|
|
1006
1499
|
});
|
|
1007
1500
|
} catch (error) {
|
|
1008
1501
|
const errorStr = error.toString();
|
|
1009
1502
|
if (errorStr.includes("CONFLICT (") || errorStr.includes("Automatic merge failed") || errorStr.includes("Patch failed at") || errorStr.includes('When you have resolved this problem, run "git rebase --continue"')) {
|
|
1010
|
-
console.error(
|
|
1011
|
-
console.error(
|
|
1012
|
-
console.error(
|
|
1503
|
+
console.error(chalk7.red("Error: Merge conflict detected in thoughts repository"));
|
|
1504
|
+
console.error(chalk7.red("Please resolve conflicts manually in:"), expandedRepo);
|
|
1505
|
+
console.error(chalk7.red('Then run "git rebase --continue" and "thoughtcabinet sync" again'));
|
|
1013
1506
|
process.exit(1);
|
|
1014
1507
|
} else {
|
|
1015
|
-
console.warn(
|
|
1508
|
+
console.warn(chalk7.yellow("Warning: Could not pull latest changes:"), error.message);
|
|
1016
1509
|
}
|
|
1017
1510
|
}
|
|
1018
1511
|
try {
|
|
1019
|
-
|
|
1020
|
-
console.log(
|
|
1512
|
+
execSync5("git remote get-url origin", { cwd: expandedRepo, stdio: "pipe" });
|
|
1513
|
+
console.log(chalk7.gray("Pushing to remote..."));
|
|
1021
1514
|
try {
|
|
1022
|
-
|
|
1023
|
-
console.log(
|
|
1515
|
+
execSync5("git push", { cwd: expandedRepo, stdio: "pipe" });
|
|
1516
|
+
console.log(chalk7.green("\u2705 Pushed to remote"));
|
|
1024
1517
|
} catch {
|
|
1025
|
-
console.log(
|
|
1518
|
+
console.log(chalk7.yellow("\u26A0\uFE0F Could not push to remote. You may need to push manually."));
|
|
1026
1519
|
}
|
|
1027
1520
|
} catch {
|
|
1028
|
-
console.log(
|
|
1521
|
+
console.log(chalk7.yellow("\u2139\uFE0F No remote configured for thoughts repository"));
|
|
1029
1522
|
}
|
|
1030
1523
|
} catch (error) {
|
|
1031
|
-
console.error(
|
|
1524
|
+
console.error(chalk7.red(`Error syncing thoughts: ${error}`));
|
|
1032
1525
|
process.exit(1);
|
|
1033
1526
|
}
|
|
1034
1527
|
}
|
|
1035
|
-
function createSearchDirectory(thoughtsDir) {
|
|
1036
|
-
const searchDir = path8.join(thoughtsDir, "searchable");
|
|
1037
|
-
if (fs6.existsSync(searchDir)) {
|
|
1038
|
-
try {
|
|
1039
|
-
execSync4(`chmod -R 755 "${searchDir}"`, { stdio: "pipe" });
|
|
1040
|
-
} catch {
|
|
1041
|
-
}
|
|
1042
|
-
fs6.rmSync(searchDir, { recursive: true, force: true });
|
|
1043
|
-
}
|
|
1044
|
-
fs6.mkdirSync(searchDir, { recursive: true });
|
|
1045
|
-
function findFilesFollowingSymlinks(dir, baseDir = dir, visited = /* @__PURE__ */ new Set()) {
|
|
1046
|
-
const files = [];
|
|
1047
|
-
const realPath = fs6.realpathSync(dir);
|
|
1048
|
-
if (visited.has(realPath)) {
|
|
1049
|
-
return files;
|
|
1050
|
-
}
|
|
1051
|
-
visited.add(realPath);
|
|
1052
|
-
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1053
|
-
for (const entry of entries) {
|
|
1054
|
-
const fullPath = path8.join(dir, entry.name);
|
|
1055
|
-
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
1056
|
-
files.push(...findFilesFollowingSymlinks(fullPath, baseDir, visited));
|
|
1057
|
-
} else if (entry.isSymbolicLink() && !entry.name.startsWith(".")) {
|
|
1058
|
-
try {
|
|
1059
|
-
const stat = fs6.statSync(fullPath);
|
|
1060
|
-
if (stat.isDirectory()) {
|
|
1061
|
-
files.push(...findFilesFollowingSymlinks(fullPath, baseDir, visited));
|
|
1062
|
-
} else if (stat.isFile() && path8.basename(fullPath) !== "CLAUDE.md") {
|
|
1063
|
-
files.push(path8.relative(baseDir, fullPath));
|
|
1064
|
-
}
|
|
1065
|
-
} catch {
|
|
1066
|
-
}
|
|
1067
|
-
} else if (entry.isFile() && !entry.name.startsWith(".") && entry.name !== "CLAUDE.md") {
|
|
1068
|
-
files.push(path8.relative(baseDir, fullPath));
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
return files;
|
|
1072
|
-
}
|
|
1073
|
-
const allFiles = findFilesFollowingSymlinks(thoughtsDir);
|
|
1074
|
-
let linkedCount = 0;
|
|
1075
|
-
for (const relPath of allFiles) {
|
|
1076
|
-
const sourcePath = path8.join(thoughtsDir, relPath);
|
|
1077
|
-
const targetPath = path8.join(searchDir, relPath);
|
|
1078
|
-
const targetDir = path8.dirname(targetPath);
|
|
1079
|
-
if (!fs6.existsSync(targetDir)) {
|
|
1080
|
-
fs6.mkdirSync(targetDir, { recursive: true });
|
|
1081
|
-
}
|
|
1082
|
-
try {
|
|
1083
|
-
const realSourcePath = fs6.realpathSync(sourcePath);
|
|
1084
|
-
fs6.linkSync(realSourcePath, targetPath);
|
|
1085
|
-
linkedCount++;
|
|
1086
|
-
} catch {
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
console.log(chalk4.gray(`Created ${linkedCount} hard links in searchable directory`));
|
|
1090
|
-
}
|
|
1091
1528
|
async function thoughtsSyncCommand(options) {
|
|
1092
1529
|
try {
|
|
1093
1530
|
const config = loadThoughtsConfig(options);
|
|
1094
1531
|
if (!config) {
|
|
1095
|
-
console.error(
|
|
1532
|
+
console.error(chalk7.red('Error: Thoughts not configured. Run "thoughtcabinet init" first.'));
|
|
1096
1533
|
process.exit(1);
|
|
1097
1534
|
}
|
|
1098
1535
|
const currentRepo = getCurrentRepoPath();
|
|
1099
|
-
const thoughtsDir =
|
|
1100
|
-
if (!
|
|
1101
|
-
console.error(
|
|
1536
|
+
const thoughtsDir = path12.join(currentRepo, "thoughts");
|
|
1537
|
+
if (!fs9.existsSync(thoughtsDir)) {
|
|
1538
|
+
console.error(chalk7.red("Error: Thoughts not initialized for this repository."));
|
|
1102
1539
|
console.error('Run "thoughtcabinet init" to set up thoughts.');
|
|
1103
1540
|
process.exit(1);
|
|
1104
1541
|
}
|
|
@@ -1113,27 +1550,45 @@ async function thoughtsSyncCommand(options) {
|
|
|
1113
1550
|
config.user
|
|
1114
1551
|
);
|
|
1115
1552
|
if (newUsers.length > 0) {
|
|
1116
|
-
console.log(
|
|
1553
|
+
console.log(chalk7.green(`\u2713 Added symlinks for new users: ${newUsers.join(", ")}`));
|
|
1117
1554
|
}
|
|
1118
1555
|
}
|
|
1119
|
-
console.log(
|
|
1120
|
-
|
|
1121
|
-
console.log(
|
|
1556
|
+
console.log(chalk7.blue("Creating searchable index..."));
|
|
1557
|
+
const linkedCount = createSearchableIndex(thoughtsDir);
|
|
1558
|
+
console.log(chalk7.gray(`Created ${linkedCount} hard links in searchable directory`));
|
|
1559
|
+
console.log(chalk7.blue("Syncing thoughts..."));
|
|
1122
1560
|
syncThoughts(profileConfig.thoughtsRepo, options.message || "");
|
|
1561
|
+
const hooksConfig = loadHooksConfig(currentRepo);
|
|
1562
|
+
const postSyncHooks = getHooksForEvent(hooksConfig, "PostThoughtsSync");
|
|
1563
|
+
if (postSyncHooks.length > 0) {
|
|
1564
|
+
const hookInput = {
|
|
1565
|
+
hook_event_name: "PostThoughtsSync",
|
|
1566
|
+
cwd: currentRepo,
|
|
1567
|
+
thoughts_repo: profileConfig.thoughtsRepo,
|
|
1568
|
+
has_changes: true,
|
|
1569
|
+
searchable_created: true
|
|
1570
|
+
};
|
|
1571
|
+
const hookEnv = {
|
|
1572
|
+
THC_THOUGHTS_REPO: profileConfig.thoughtsRepo,
|
|
1573
|
+
THC_HAS_CHANGES: "true",
|
|
1574
|
+
THC_SEARCHABLE_CREATED: "true"
|
|
1575
|
+
};
|
|
1576
|
+
await executeHooks(postSyncHooks, hookInput, hookEnv, true);
|
|
1577
|
+
}
|
|
1123
1578
|
} catch (error) {
|
|
1124
|
-
console.error(
|
|
1579
|
+
console.error(chalk7.red(`Error during thoughts sync: ${error}`));
|
|
1125
1580
|
process.exit(1);
|
|
1126
1581
|
}
|
|
1127
1582
|
}
|
|
1128
1583
|
|
|
1129
1584
|
// src/commands/thoughts/status.ts
|
|
1130
|
-
import
|
|
1131
|
-
import
|
|
1132
|
-
import { execSync as
|
|
1133
|
-
import
|
|
1585
|
+
import fs10 from "fs";
|
|
1586
|
+
import path13 from "path";
|
|
1587
|
+
import { execSync as execSync6 } from "child_process";
|
|
1588
|
+
import chalk8 from "chalk";
|
|
1134
1589
|
function getGitStatus(repoPath) {
|
|
1135
1590
|
try {
|
|
1136
|
-
return
|
|
1591
|
+
return execSync6("git status -sb", {
|
|
1137
1592
|
cwd: repoPath,
|
|
1138
1593
|
encoding: "utf8",
|
|
1139
1594
|
stdio: "pipe"
|
|
@@ -1144,7 +1599,7 @@ function getGitStatus(repoPath) {
|
|
|
1144
1599
|
}
|
|
1145
1600
|
function getUncommittedChanges(repoPath) {
|
|
1146
1601
|
try {
|
|
1147
|
-
const output =
|
|
1602
|
+
const output = execSync6("git status --porcelain", {
|
|
1148
1603
|
cwd: repoPath,
|
|
1149
1604
|
encoding: "utf8",
|
|
1150
1605
|
stdio: "pipe"
|
|
@@ -1158,7 +1613,7 @@ function getUncommittedChanges(repoPath) {
|
|
|
1158
1613
|
else if (status[0] === "D") statusText = "deleted";
|
|
1159
1614
|
else if (status[0] === "?") statusText = "untracked";
|
|
1160
1615
|
else if (status[0] === "R") statusText = "renamed";
|
|
1161
|
-
return ` ${
|
|
1616
|
+
return ` ${chalk8.yellow(statusText.padEnd(10))} ${file}`;
|
|
1162
1617
|
});
|
|
1163
1618
|
} catch {
|
|
1164
1619
|
return [];
|
|
@@ -1166,7 +1621,7 @@ function getUncommittedChanges(repoPath) {
|
|
|
1166
1621
|
}
|
|
1167
1622
|
function getLastCommit(repoPath) {
|
|
1168
1623
|
try {
|
|
1169
|
-
return
|
|
1624
|
+
return execSync6('git log -1 --pretty=format:"%h %s (%cr)"', {
|
|
1170
1625
|
cwd: repoPath,
|
|
1171
1626
|
encoding: "utf8",
|
|
1172
1627
|
stdio: "pipe"
|
|
@@ -1177,64 +1632,64 @@ function getLastCommit(repoPath) {
|
|
|
1177
1632
|
}
|
|
1178
1633
|
function getRemoteStatus(repoPath) {
|
|
1179
1634
|
try {
|
|
1180
|
-
|
|
1635
|
+
execSync6("git remote get-url origin", { cwd: repoPath, stdio: "pipe" });
|
|
1181
1636
|
try {
|
|
1182
|
-
|
|
1637
|
+
execSync6("git fetch", { cwd: repoPath, stdio: "pipe" });
|
|
1183
1638
|
} catch {
|
|
1184
1639
|
}
|
|
1185
|
-
const status =
|
|
1640
|
+
const status = execSync6("git status -sb", {
|
|
1186
1641
|
cwd: repoPath,
|
|
1187
1642
|
encoding: "utf8",
|
|
1188
1643
|
stdio: "pipe"
|
|
1189
1644
|
});
|
|
1190
1645
|
if (status.includes("ahead")) {
|
|
1191
1646
|
const ahead = status.match(/ahead (\d+)/)?.[1] || "?";
|
|
1192
|
-
return
|
|
1647
|
+
return chalk8.yellow(`${ahead} commits ahead of remote`);
|
|
1193
1648
|
} else if (status.includes("behind")) {
|
|
1194
1649
|
const behind = status.match(/behind (\d+)/)?.[1] || "?";
|
|
1195
1650
|
try {
|
|
1196
|
-
|
|
1651
|
+
execSync6("git pull --rebase", {
|
|
1197
1652
|
stdio: "pipe",
|
|
1198
1653
|
cwd: repoPath
|
|
1199
1654
|
});
|
|
1200
|
-
console.log(
|
|
1201
|
-
const newStatus =
|
|
1655
|
+
console.log(chalk8.green("\u2713 Automatically pulled latest changes"));
|
|
1656
|
+
const newStatus = execSync6("git status -sb", {
|
|
1202
1657
|
encoding: "utf8",
|
|
1203
1658
|
cwd: repoPath,
|
|
1204
1659
|
stdio: "pipe"
|
|
1205
1660
|
});
|
|
1206
1661
|
if (newStatus.includes("behind")) {
|
|
1207
1662
|
const newBehind = newStatus.match(/behind (\d+)/)?.[1] || "?";
|
|
1208
|
-
return
|
|
1663
|
+
return chalk8.yellow(`${newBehind} commits behind remote (after pull)`);
|
|
1209
1664
|
} else {
|
|
1210
|
-
return
|
|
1665
|
+
return chalk8.green("Up to date with remote (after pull)");
|
|
1211
1666
|
}
|
|
1212
1667
|
} catch {
|
|
1213
|
-
return
|
|
1668
|
+
return chalk8.yellow(`${behind} commits behind remote`);
|
|
1214
1669
|
}
|
|
1215
1670
|
} else {
|
|
1216
|
-
return
|
|
1671
|
+
return chalk8.green("Up to date with remote");
|
|
1217
1672
|
}
|
|
1218
1673
|
} catch {
|
|
1219
|
-
return
|
|
1674
|
+
return chalk8.gray("No remote configured");
|
|
1220
1675
|
}
|
|
1221
1676
|
}
|
|
1222
1677
|
async function thoughtsStatusCommand(options) {
|
|
1223
1678
|
try {
|
|
1224
1679
|
const config = loadThoughtsConfig(options);
|
|
1225
1680
|
if (!config) {
|
|
1226
|
-
console.error(
|
|
1681
|
+
console.error(chalk8.red('Error: Thoughts not configured. Run "thoughtcabinet init" first.'));
|
|
1227
1682
|
process.exit(1);
|
|
1228
1683
|
}
|
|
1229
|
-
console.log(
|
|
1230
|
-
console.log(
|
|
1684
|
+
console.log(chalk8.blue("Thoughts Repository Status"));
|
|
1685
|
+
console.log(chalk8.gray("=".repeat(50)));
|
|
1231
1686
|
console.log("");
|
|
1232
|
-
console.log(
|
|
1233
|
-
console.log(` Repository: ${
|
|
1234
|
-
console.log(` Repos directory: ${
|
|
1235
|
-
console.log(` Global directory: ${
|
|
1236
|
-
console.log(` User: ${
|
|
1237
|
-
console.log(` Mapped repos: ${
|
|
1687
|
+
console.log(chalk8.yellow("Configuration:"));
|
|
1688
|
+
console.log(` Repository: ${chalk8.cyan(config.thoughtsRepo)}`);
|
|
1689
|
+
console.log(` Repos directory: ${chalk8.cyan(config.reposDir)}`);
|
|
1690
|
+
console.log(` Global directory: ${chalk8.cyan(config.globalDir)}`);
|
|
1691
|
+
console.log(` User: ${chalk8.cyan(config.user)}`);
|
|
1692
|
+
console.log(` Mapped repos: ${chalk8.cyan(Object.keys(config.repoMappings).length)}`);
|
|
1238
1693
|
console.log("");
|
|
1239
1694
|
const currentRepo = getCurrentRepoPath();
|
|
1240
1695
|
const currentMapping = config.repoMappings[currentRepo];
|
|
@@ -1242,28 +1697,28 @@ async function thoughtsStatusCommand(options) {
|
|
|
1242
1697
|
const profileName = getProfileNameFromMapping(currentMapping);
|
|
1243
1698
|
const profileConfig = resolveProfileForRepo(config, currentRepo);
|
|
1244
1699
|
if (mappedName) {
|
|
1245
|
-
console.log(
|
|
1246
|
-
console.log(` Path: ${
|
|
1247
|
-
console.log(` Thoughts directory: ${
|
|
1700
|
+
console.log(chalk8.yellow("Current Repository:"));
|
|
1701
|
+
console.log(` Path: ${chalk8.cyan(currentRepo)}`);
|
|
1702
|
+
console.log(` Thoughts directory: ${chalk8.cyan(`${profileConfig.reposDir}/${mappedName}`)}`);
|
|
1248
1703
|
if (profileName) {
|
|
1249
|
-
console.log(` Profile: ${
|
|
1704
|
+
console.log(` Profile: ${chalk8.cyan(profileName)}`);
|
|
1250
1705
|
} else {
|
|
1251
|
-
console.log(` Profile: ${
|
|
1706
|
+
console.log(` Profile: ${chalk8.gray("(default)")}`);
|
|
1252
1707
|
}
|
|
1253
|
-
const thoughtsDir =
|
|
1254
|
-
if (
|
|
1255
|
-
console.log(` Status: ${
|
|
1708
|
+
const thoughtsDir = path13.join(currentRepo, "thoughts");
|
|
1709
|
+
if (fs10.existsSync(thoughtsDir)) {
|
|
1710
|
+
console.log(` Status: ${chalk8.green("\u2713 Initialized")}`);
|
|
1256
1711
|
} else {
|
|
1257
|
-
console.log(` Status: ${
|
|
1712
|
+
console.log(` Status: ${chalk8.red("\u2717 Not initialized")}`);
|
|
1258
1713
|
}
|
|
1259
1714
|
} else {
|
|
1260
|
-
console.log(
|
|
1715
|
+
console.log(chalk8.yellow("Current repository not mapped to thoughts"));
|
|
1261
1716
|
}
|
|
1262
1717
|
console.log("");
|
|
1263
1718
|
const expandedRepo = expandPath(profileConfig.thoughtsRepo);
|
|
1264
|
-
console.log(
|
|
1719
|
+
console.log(chalk8.yellow("Thoughts Repository Git Status:"));
|
|
1265
1720
|
if (profileName) {
|
|
1266
|
-
console.log(
|
|
1721
|
+
console.log(chalk8.gray(` (using profile: ${profileName})`));
|
|
1267
1722
|
}
|
|
1268
1723
|
console.log(` ${getGitStatus(expandedRepo)}`);
|
|
1269
1724
|
console.log(` Remote: ${getRemoteStatus(expandedRepo)}`);
|
|
@@ -1271,33 +1726,33 @@ async function thoughtsStatusCommand(options) {
|
|
|
1271
1726
|
console.log("");
|
|
1272
1727
|
const changes = getUncommittedChanges(expandedRepo);
|
|
1273
1728
|
if (changes.length > 0) {
|
|
1274
|
-
console.log(
|
|
1729
|
+
console.log(chalk8.yellow("Uncommitted changes:"));
|
|
1275
1730
|
changes.forEach((change) => console.log(change));
|
|
1276
1731
|
console.log("");
|
|
1277
|
-
console.log(
|
|
1732
|
+
console.log(chalk8.gray('Run "thoughtcabinet sync" to commit these changes'));
|
|
1278
1733
|
} else {
|
|
1279
|
-
console.log(
|
|
1734
|
+
console.log(chalk8.green("\u2713 No uncommitted changes"));
|
|
1280
1735
|
}
|
|
1281
1736
|
} catch (error) {
|
|
1282
|
-
console.error(
|
|
1737
|
+
console.error(chalk8.red(`Error checking thoughts status: ${error}`));
|
|
1283
1738
|
process.exit(1);
|
|
1284
1739
|
}
|
|
1285
1740
|
}
|
|
1286
1741
|
|
|
1287
1742
|
// src/commands/thoughts/config.ts
|
|
1288
|
-
import { spawn } from "child_process";
|
|
1289
|
-
import
|
|
1743
|
+
import { spawn as spawn2 } from "child_process";
|
|
1744
|
+
import chalk9 from "chalk";
|
|
1290
1745
|
async function thoughtsConfigCommand(options) {
|
|
1291
1746
|
try {
|
|
1292
1747
|
const configPath = options.configFile || getDefaultConfigPath();
|
|
1293
1748
|
if (options.edit) {
|
|
1294
1749
|
const editor = process.env.EDITOR || "vi";
|
|
1295
|
-
|
|
1750
|
+
spawn2(editor, [configPath], { stdio: "inherit" });
|
|
1296
1751
|
return;
|
|
1297
1752
|
}
|
|
1298
1753
|
const config = loadThoughtsConfig(options);
|
|
1299
1754
|
if (!config) {
|
|
1300
|
-
console.error(
|
|
1755
|
+
console.error(chalk9.red("No thoughts configuration found."));
|
|
1301
1756
|
console.error('Run "thoughtcabinet init" to create one.');
|
|
1302
1757
|
process.exit(1);
|
|
1303
1758
|
}
|
|
@@ -1305,52 +1760,113 @@ async function thoughtsConfigCommand(options) {
|
|
|
1305
1760
|
console.log(JSON.stringify(config, null, 2));
|
|
1306
1761
|
return;
|
|
1307
1762
|
}
|
|
1308
|
-
console.log(
|
|
1309
|
-
console.log(
|
|
1763
|
+
console.log(chalk9.blue("Thoughts Configuration"));
|
|
1764
|
+
console.log(chalk9.gray("=".repeat(50)));
|
|
1310
1765
|
console.log("");
|
|
1311
|
-
console.log(
|
|
1312
|
-
console.log(` Config file: ${
|
|
1313
|
-
console.log(` Thoughts repository: ${
|
|
1314
|
-
console.log(` Repos directory: ${
|
|
1315
|
-
console.log(` Global directory: ${
|
|
1316
|
-
console.log(` User: ${
|
|
1766
|
+
console.log(chalk9.yellow("Settings:"));
|
|
1767
|
+
console.log(` Config file: ${chalk9.cyan(configPath)}`);
|
|
1768
|
+
console.log(` Thoughts repository: ${chalk9.cyan(config.thoughtsRepo)}`);
|
|
1769
|
+
console.log(` Repos directory: ${chalk9.cyan(config.reposDir)}`);
|
|
1770
|
+
console.log(` Global directory: ${chalk9.cyan(config.globalDir)}`);
|
|
1771
|
+
console.log(` User: ${chalk9.cyan(config.user)}`);
|
|
1317
1772
|
console.log("");
|
|
1318
|
-
console.log(
|
|
1773
|
+
console.log(chalk9.yellow("Repository Mappings:"));
|
|
1319
1774
|
const mappings = Object.entries(config.repoMappings);
|
|
1320
1775
|
if (mappings.length === 0) {
|
|
1321
|
-
console.log(
|
|
1776
|
+
console.log(chalk9.gray(" No repositories mapped yet"));
|
|
1322
1777
|
} else {
|
|
1323
1778
|
mappings.forEach(([repo, mapping]) => {
|
|
1324
1779
|
const repoName = getRepoNameFromMapping(mapping);
|
|
1325
1780
|
const profileName = getProfileNameFromMapping(mapping);
|
|
1326
|
-
console.log(` ${
|
|
1327
|
-
console.log(` \u2192 ${
|
|
1781
|
+
console.log(` ${chalk9.cyan(repo)}`);
|
|
1782
|
+
console.log(` \u2192 ${chalk9.green(`${config.reposDir}/${repoName}`)}`);
|
|
1328
1783
|
if (profileName) {
|
|
1329
|
-
console.log(` Profile: ${
|
|
1784
|
+
console.log(` Profile: ${chalk9.yellow(profileName)}`);
|
|
1330
1785
|
} else {
|
|
1331
|
-
console.log(` Profile: ${
|
|
1786
|
+
console.log(` Profile: ${chalk9.gray("(default)")}`);
|
|
1332
1787
|
}
|
|
1333
1788
|
});
|
|
1334
1789
|
}
|
|
1335
1790
|
console.log("");
|
|
1336
|
-
console.log(
|
|
1791
|
+
console.log(chalk9.yellow("Profiles:"));
|
|
1337
1792
|
if (!config.profiles || Object.keys(config.profiles).length === 0) {
|
|
1338
|
-
console.log(
|
|
1793
|
+
console.log(chalk9.gray(" No profiles configured"));
|
|
1339
1794
|
} else {
|
|
1340
1795
|
Object.keys(config.profiles).forEach((name) => {
|
|
1341
|
-
console.log(` ${
|
|
1796
|
+
console.log(` ${chalk9.cyan(name)}`);
|
|
1342
1797
|
});
|
|
1343
1798
|
}
|
|
1344
1799
|
console.log("");
|
|
1345
|
-
console.log(
|
|
1800
|
+
console.log(chalk9.gray("To edit configuration, run: thoughtcabinet config --edit"));
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
console.error(chalk9.red(`Error showing thoughts config: ${error}`));
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/commands/thoughts/prune.ts
|
|
1808
|
+
import fs11 from "fs";
|
|
1809
|
+
import chalk10 from "chalk";
|
|
1810
|
+
async function thoughtsPruneCommand(options) {
|
|
1811
|
+
try {
|
|
1812
|
+
const config = loadThoughtsConfig(options);
|
|
1813
|
+
if (!config) {
|
|
1814
|
+
console.error(chalk10.red("Error: Thoughts configuration not found."));
|
|
1815
|
+
console.error('Run "thoughtcabinet init" to create one.');
|
|
1816
|
+
process.exit(1);
|
|
1817
|
+
}
|
|
1818
|
+
const mappings = Object.entries(config.repoMappings);
|
|
1819
|
+
if (mappings.length === 0) {
|
|
1820
|
+
console.log(chalk10.gray("No repository mappings configured."));
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
const staleEntries = [];
|
|
1824
|
+
for (const [repoPath, mapping] of mappings) {
|
|
1825
|
+
if (!fs11.existsSync(repoPath)) {
|
|
1826
|
+
staleEntries.push({
|
|
1827
|
+
repoPath,
|
|
1828
|
+
mappedName: getRepoNameFromMapping(mapping),
|
|
1829
|
+
profileName: getProfileNameFromMapping(mapping)
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (staleEntries.length === 0) {
|
|
1834
|
+
console.log(chalk10.green("No stale repository mappings found."));
|
|
1835
|
+
console.log(chalk10.gray(`All ${mappings.length} mapped repositories exist.`));
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
console.log(chalk10.yellow(`Found ${staleEntries.length} stale repository mapping(s):`));
|
|
1839
|
+
console.log("");
|
|
1840
|
+
for (const entry of staleEntries) {
|
|
1841
|
+
console.log(` ${chalk10.red("\u2717")} ${chalk10.cyan(entry.repoPath)}`);
|
|
1842
|
+
console.log(` \u2192 ${chalk10.gray(entry.mappedName || "(unknown)")}`);
|
|
1843
|
+
if (entry.profileName) {
|
|
1844
|
+
console.log(` Profile: ${chalk10.yellow(entry.profileName)}`);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
console.log("");
|
|
1848
|
+
if (options.apply) {
|
|
1849
|
+
console.log(chalk10.blue("Removing stale entries from configuration..."));
|
|
1850
|
+
for (const entry of staleEntries) {
|
|
1851
|
+
delete config.repoMappings[entry.repoPath];
|
|
1852
|
+
}
|
|
1853
|
+
saveThoughtsConfig(config, options);
|
|
1854
|
+
console.log(chalk10.green(`\u2705 Removed ${staleEntries.length} stale mapping(s).`));
|
|
1855
|
+
console.log("");
|
|
1856
|
+
console.log(chalk10.gray("Note: The thoughts content in your thoughts repository was not modified."));
|
|
1857
|
+
console.log(chalk10.gray("Only the configuration entries pointing to non-existent directories were removed."));
|
|
1858
|
+
} else {
|
|
1859
|
+
console.log(chalk10.gray("This is a dry run. No changes were made."));
|
|
1860
|
+
console.log(chalk10.gray("Run with --apply to remove these stale entries."));
|
|
1861
|
+
}
|
|
1346
1862
|
} catch (error) {
|
|
1347
|
-
console.error(
|
|
1863
|
+
console.error(chalk10.red(`Error during thoughts prune: ${error}`));
|
|
1348
1864
|
process.exit(1);
|
|
1349
1865
|
}
|
|
1350
1866
|
}
|
|
1351
1867
|
|
|
1352
1868
|
// src/commands/thoughts/profile/create.ts
|
|
1353
|
-
import
|
|
1869
|
+
import chalk11 from "chalk";
|
|
1354
1870
|
import * as p2 from "@clack/prompts";
|
|
1355
1871
|
async function profileCreateCommand(profileName, options) {
|
|
1356
1872
|
try {
|
|
@@ -1371,7 +1887,7 @@ async function profileCreateCommand(profileName, options) {
|
|
|
1371
1887
|
if (sanitizedName !== profileName) {
|
|
1372
1888
|
p2.log.warn(`Profile name sanitized: "${profileName}" \u2192 "${sanitizedName}"`);
|
|
1373
1889
|
}
|
|
1374
|
-
p2.intro(
|
|
1890
|
+
p2.intro(chalk11.blue(`Creating Profile: ${sanitizedName}`));
|
|
1375
1891
|
if (validateProfile(config, sanitizedName)) {
|
|
1376
1892
|
p2.log.error(`Profile "${sanitizedName}" already exists.`);
|
|
1377
1893
|
p2.log.info("Use a different name or delete the existing profile first.");
|
|
@@ -1432,15 +1948,15 @@ async function profileCreateCommand(profileName, options) {
|
|
|
1432
1948
|
ensureThoughtsRepoExists(profileConfig);
|
|
1433
1949
|
p2.log.success(`Profile "${sanitizedName}" created successfully!`);
|
|
1434
1950
|
p2.note(
|
|
1435
|
-
`Name: ${
|
|
1436
|
-
Thoughts repository: ${
|
|
1437
|
-
Repos directory: ${
|
|
1438
|
-
Global directory: ${
|
|
1951
|
+
`Name: ${chalk11.cyan(sanitizedName)}
|
|
1952
|
+
Thoughts repository: ${chalk11.cyan(thoughtsRepo)}
|
|
1953
|
+
Repos directory: ${chalk11.cyan(reposDir)}
|
|
1954
|
+
Global directory: ${chalk11.cyan(globalDir)}`,
|
|
1439
1955
|
"Profile Configuration"
|
|
1440
1956
|
);
|
|
1441
1957
|
p2.outro(
|
|
1442
|
-
|
|
1443
|
-
`) +
|
|
1958
|
+
chalk11.gray("Next steps:\n") + chalk11.gray(` 1. Run "thoughtcabinet init --profile ${sanitizedName}" in a repository
|
|
1959
|
+
`) + chalk11.gray(` 2. Your thoughts will sync to the profile's repository`)
|
|
1444
1960
|
);
|
|
1445
1961
|
} catch (error) {
|
|
1446
1962
|
p2.log.error(`Error creating profile: ${error}`);
|
|
@@ -1449,35 +1965,35 @@ Global directory: ${chalk7.cyan(globalDir)}`,
|
|
|
1449
1965
|
}
|
|
1450
1966
|
|
|
1451
1967
|
// src/commands/thoughts/profile/list.ts
|
|
1452
|
-
import
|
|
1968
|
+
import chalk12 from "chalk";
|
|
1453
1969
|
async function profileListCommand(options) {
|
|
1454
1970
|
try {
|
|
1455
1971
|
const config = loadThoughtsConfig(options);
|
|
1456
1972
|
if (!config) {
|
|
1457
|
-
console.error(
|
|
1973
|
+
console.error(chalk12.red("Error: Thoughts not configured."));
|
|
1458
1974
|
process.exit(1);
|
|
1459
1975
|
}
|
|
1460
1976
|
if (options.json) {
|
|
1461
1977
|
console.log(JSON.stringify(config.profiles || {}, null, 2));
|
|
1462
1978
|
return;
|
|
1463
1979
|
}
|
|
1464
|
-
console.log(
|
|
1465
|
-
console.log(
|
|
1980
|
+
console.log(chalk12.blue("Thoughts Profiles"));
|
|
1981
|
+
console.log(chalk12.gray("=".repeat(50)));
|
|
1466
1982
|
console.log("");
|
|
1467
|
-
console.log(
|
|
1468
|
-
console.log(` Thoughts repository: ${
|
|
1469
|
-
console.log(` Repos directory: ${
|
|
1470
|
-
console.log(` Global directory: ${
|
|
1983
|
+
console.log(chalk12.yellow("Default Configuration:"));
|
|
1984
|
+
console.log(` Thoughts repository: ${chalk12.cyan(config.thoughtsRepo)}`);
|
|
1985
|
+
console.log(` Repos directory: ${chalk12.cyan(config.reposDir)}`);
|
|
1986
|
+
console.log(` Global directory: ${chalk12.cyan(config.globalDir)}`);
|
|
1471
1987
|
console.log("");
|
|
1472
1988
|
if (!config.profiles || Object.keys(config.profiles).length === 0) {
|
|
1473
|
-
console.log(
|
|
1989
|
+
console.log(chalk12.gray("No profiles configured."));
|
|
1474
1990
|
console.log("");
|
|
1475
|
-
console.log(
|
|
1991
|
+
console.log(chalk12.gray("Create a profile with: thoughtcabinet profile create <name>"));
|
|
1476
1992
|
} else {
|
|
1477
|
-
console.log(
|
|
1993
|
+
console.log(chalk12.yellow(`Profiles (${Object.keys(config.profiles).length}):`));
|
|
1478
1994
|
console.log("");
|
|
1479
1995
|
Object.entries(config.profiles).forEach(([name, profile]) => {
|
|
1480
|
-
console.log(
|
|
1996
|
+
console.log(chalk12.cyan(` ${name}:`));
|
|
1481
1997
|
console.log(` Thoughts repository: ${profile.thoughtsRepo}`);
|
|
1482
1998
|
console.log(` Repos directory: ${profile.reposDir}`);
|
|
1483
1999
|
console.log(` Global directory: ${profile.globalDir}`);
|
|
@@ -1485,30 +2001,30 @@ async function profileListCommand(options) {
|
|
|
1485
2001
|
});
|
|
1486
2002
|
}
|
|
1487
2003
|
} catch (error) {
|
|
1488
|
-
console.error(
|
|
2004
|
+
console.error(chalk12.red(`Error listing profiles: ${error}`));
|
|
1489
2005
|
process.exit(1);
|
|
1490
2006
|
}
|
|
1491
2007
|
}
|
|
1492
2008
|
|
|
1493
2009
|
// src/commands/thoughts/profile/show.ts
|
|
1494
|
-
import
|
|
2010
|
+
import chalk13 from "chalk";
|
|
1495
2011
|
async function profileShowCommand(profileName, options) {
|
|
1496
2012
|
try {
|
|
1497
2013
|
const config = loadThoughtsConfig(options);
|
|
1498
2014
|
if (!config) {
|
|
1499
|
-
console.error(
|
|
2015
|
+
console.error(chalk13.red("Error: Thoughts not configured."));
|
|
1500
2016
|
process.exit(1);
|
|
1501
2017
|
}
|
|
1502
2018
|
if (!validateProfile(config, profileName)) {
|
|
1503
|
-
console.error(
|
|
2019
|
+
console.error(chalk13.red(`Error: Profile "${profileName}" not found.`));
|
|
1504
2020
|
console.error("");
|
|
1505
|
-
console.error(
|
|
2021
|
+
console.error(chalk13.gray("Available profiles:"));
|
|
1506
2022
|
if (config.profiles) {
|
|
1507
2023
|
Object.keys(config.profiles).forEach((name) => {
|
|
1508
|
-
console.error(
|
|
2024
|
+
console.error(chalk13.gray(` - ${name}`));
|
|
1509
2025
|
});
|
|
1510
2026
|
} else {
|
|
1511
|
-
console.error(
|
|
2027
|
+
console.error(chalk13.gray(" (none)"));
|
|
1512
2028
|
}
|
|
1513
2029
|
process.exit(1);
|
|
1514
2030
|
}
|
|
@@ -1517,13 +2033,13 @@ async function profileShowCommand(profileName, options) {
|
|
|
1517
2033
|
console.log(JSON.stringify(profile, null, 2));
|
|
1518
2034
|
return;
|
|
1519
2035
|
}
|
|
1520
|
-
console.log(
|
|
1521
|
-
console.log(
|
|
2036
|
+
console.log(chalk13.blue(`Profile: ${profileName}`));
|
|
2037
|
+
console.log(chalk13.gray("=".repeat(50)));
|
|
1522
2038
|
console.log("");
|
|
1523
|
-
console.log(
|
|
1524
|
-
console.log(` Thoughts repository: ${
|
|
1525
|
-
console.log(` Repos directory: ${
|
|
1526
|
-
console.log(` Global directory: ${
|
|
2039
|
+
console.log(chalk13.yellow("Configuration:"));
|
|
2040
|
+
console.log(` Thoughts repository: ${chalk13.cyan(profile.thoughtsRepo)}`);
|
|
2041
|
+
console.log(` Repos directory: ${chalk13.cyan(profile.reposDir)}`);
|
|
2042
|
+
console.log(` Global directory: ${chalk13.cyan(profile.globalDir)}`);
|
|
1527
2043
|
console.log("");
|
|
1528
2044
|
let repoCount = 0;
|
|
1529
2045
|
Object.values(config.repoMappings).forEach((mapping) => {
|
|
@@ -1531,16 +2047,16 @@ async function profileShowCommand(profileName, options) {
|
|
|
1531
2047
|
repoCount++;
|
|
1532
2048
|
}
|
|
1533
2049
|
});
|
|
1534
|
-
console.log(
|
|
1535
|
-
console.log(` Repositories using this profile: ${
|
|
2050
|
+
console.log(chalk13.yellow("Usage:"));
|
|
2051
|
+
console.log(` Repositories using this profile: ${chalk13.cyan(repoCount)}`);
|
|
1536
2052
|
} catch (error) {
|
|
1537
|
-
console.error(
|
|
2053
|
+
console.error(chalk13.red(`Error showing profile: ${error}`));
|
|
1538
2054
|
process.exit(1);
|
|
1539
2055
|
}
|
|
1540
2056
|
}
|
|
1541
2057
|
|
|
1542
2058
|
// src/commands/thoughts/profile/delete.ts
|
|
1543
|
-
import
|
|
2059
|
+
import chalk14 from "chalk";
|
|
1544
2060
|
import * as p3 from "@clack/prompts";
|
|
1545
2061
|
async function profileDeleteCommand(profileName, options) {
|
|
1546
2062
|
try {
|
|
@@ -1549,7 +2065,7 @@ async function profileDeleteCommand(profileName, options) {
|
|
|
1549
2065
|
p3.log.info("Use --force flag to delete without confirmation.");
|
|
1550
2066
|
process.exit(1);
|
|
1551
2067
|
}
|
|
1552
|
-
p3.intro(
|
|
2068
|
+
p3.intro(chalk14.blue(`Delete Profile: ${profileName}`));
|
|
1553
2069
|
const config = loadThoughtsConfig(options);
|
|
1554
2070
|
if (!config) {
|
|
1555
2071
|
p3.log.error("Thoughts not configured.");
|
|
@@ -1568,19 +2084,19 @@ async function profileDeleteCommand(profileName, options) {
|
|
|
1568
2084
|
if (usingRepos.length > 0 && !options.force) {
|
|
1569
2085
|
p3.log.error(`Profile "${profileName}" is in use by ${usingRepos.length} repository(ies):`);
|
|
1570
2086
|
usingRepos.forEach((repo) => {
|
|
1571
|
-
p3.log.message(
|
|
2087
|
+
p3.log.message(chalk14.gray(` - ${repo}`));
|
|
1572
2088
|
});
|
|
1573
2089
|
p3.log.warn("Options:");
|
|
1574
|
-
p3.log.message(
|
|
2090
|
+
p3.log.message(chalk14.gray(' 1. Run "thoughtcabinet destroy" in each repository'));
|
|
1575
2091
|
p3.log.message(
|
|
1576
|
-
|
|
2092
|
+
chalk14.gray(" 2. Use --force to delete anyway (repos will fall back to default config)")
|
|
1577
2093
|
);
|
|
1578
2094
|
process.exit(1);
|
|
1579
2095
|
}
|
|
1580
2096
|
if (!options.force) {
|
|
1581
|
-
p3.log.warn(`You are about to delete profile: ${
|
|
1582
|
-
p3.log.message(
|
|
1583
|
-
p3.log.message(
|
|
2097
|
+
p3.log.warn(`You are about to delete profile: ${chalk14.cyan(profileName)}`);
|
|
2098
|
+
p3.log.message(chalk14.gray("This will remove the profile configuration."));
|
|
2099
|
+
p3.log.message(chalk14.gray("The thoughts repository files will NOT be deleted."));
|
|
1584
2100
|
const confirmDelete = await p3.confirm({
|
|
1585
2101
|
message: `Delete profile "${profileName}"?`,
|
|
1586
2102
|
initialValue: false
|
|
@@ -1599,7 +2115,7 @@ async function profileDeleteCommand(profileName, options) {
|
|
|
1599
2115
|
if (usingRepos.length > 0) {
|
|
1600
2116
|
p3.log.warn("Repositories using this profile will fall back to default config");
|
|
1601
2117
|
}
|
|
1602
|
-
p3.outro(
|
|
2118
|
+
p3.outro(chalk14.green("Done"));
|
|
1603
2119
|
} catch (error) {
|
|
1604
2120
|
p3.log.error(`Error deleting profile: ${error}`);
|
|
1605
2121
|
process.exit(1);
|
|
@@ -1617,6 +2133,7 @@ function thoughtsCommand(program2) {
|
|
|
1617
2133
|
cmd.command("sync").description("Manually sync thoughts to thoughts repository").option("-m, --message <message>", "Commit message for sync").option("--config-file <path>", "Path to config file").action(thoughtsSyncCommand);
|
|
1618
2134
|
cmd.command("status").description("Show status of thoughts repository").option("--config-file <path>", "Path to config file").action(thoughtsStatusCommand);
|
|
1619
2135
|
cmd.command("config").description("View or edit thoughts configuration").option("--edit", "Open configuration in editor").option("--json", "Output configuration as JSON").option("--config-file <path>", "Path to config file").action(thoughtsConfigCommand);
|
|
2136
|
+
cmd.command("prune").description("Remove stale repository mappings (directories that no longer exist)").option("--apply", "Apply changes (default is dry-run)").option("--config-file <path>", "Path to config file").action(thoughtsPruneCommand);
|
|
1620
2137
|
const profile = cmd.command("profile").description("Manage thoughts profiles");
|
|
1621
2138
|
profile.command("create <name>").description("Create a new thoughts profile").option("--repo <path>", "Thoughts repository path").option("--repos-dir <name>", "Repos directory name").option("--global-dir <name>", "Global directory name").option("--config-file <path>", "Path to config file").action(profileCreateCommand);
|
|
1622
2139
|
profile.command("list").description("List all thoughts profiles").option("--json", "Output as JSON").option("--config-file <path>", "Path to config file").action(profileListCommand);
|
|
@@ -1625,38 +2142,38 @@ function thoughtsCommand(program2) {
|
|
|
1625
2142
|
}
|
|
1626
2143
|
|
|
1627
2144
|
// src/commands/agent/init.ts
|
|
1628
|
-
import
|
|
1629
|
-
import
|
|
1630
|
-
import
|
|
2145
|
+
import fs12 from "fs";
|
|
2146
|
+
import path14 from "path";
|
|
2147
|
+
import chalk15 from "chalk";
|
|
1631
2148
|
import * as p4 from "@clack/prompts";
|
|
1632
2149
|
import { fileURLToPath } from "url";
|
|
1633
2150
|
import { dirname } from "path";
|
|
1634
2151
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
1635
2152
|
var __dirname2 = dirname(__filename2);
|
|
1636
2153
|
function ensureGitignoreEntry(targetDir, entry, productName) {
|
|
1637
|
-
const gitignorePath =
|
|
2154
|
+
const gitignorePath = path14.join(targetDir, ".gitignore");
|
|
1638
2155
|
let gitignoreContent = "";
|
|
1639
|
-
if (
|
|
1640
|
-
gitignoreContent =
|
|
2156
|
+
if (fs12.existsSync(gitignorePath)) {
|
|
2157
|
+
gitignoreContent = fs12.readFileSync(gitignorePath, "utf8");
|
|
1641
2158
|
}
|
|
1642
2159
|
const lines = gitignoreContent.split("\n");
|
|
1643
2160
|
if (lines.some((line) => line.trim() === entry)) {
|
|
1644
2161
|
return;
|
|
1645
2162
|
}
|
|
1646
2163
|
const newContent = gitignoreContent + (gitignoreContent && !gitignoreContent.endsWith("\n") ? "\n" : "") + "\n# " + productName + " local settings\n" + entry + "\n";
|
|
1647
|
-
|
|
2164
|
+
fs12.writeFileSync(gitignorePath, newContent);
|
|
1648
2165
|
}
|
|
1649
2166
|
function copyDirectoryRecursive(sourceDir, targetDir) {
|
|
1650
2167
|
let filesCopied = 0;
|
|
1651
|
-
|
|
1652
|
-
const entries =
|
|
2168
|
+
fs12.mkdirSync(targetDir, { recursive: true });
|
|
2169
|
+
const entries = fs12.readdirSync(sourceDir, { withFileTypes: true });
|
|
1653
2170
|
for (const entry of entries) {
|
|
1654
|
-
const sourcePath =
|
|
1655
|
-
const targetPath =
|
|
2171
|
+
const sourcePath = path14.join(sourceDir, entry.name);
|
|
2172
|
+
const targetPath = path14.join(targetDir, entry.name);
|
|
1656
2173
|
if (entry.isDirectory()) {
|
|
1657
2174
|
filesCopied += copyDirectoryRecursive(sourcePath, targetPath);
|
|
1658
2175
|
} else {
|
|
1659
|
-
|
|
2176
|
+
fs12.copyFileSync(sourcePath, targetPath);
|
|
1660
2177
|
filesCopied++;
|
|
1661
2178
|
}
|
|
1662
2179
|
}
|
|
@@ -1665,23 +2182,23 @@ function copyDirectoryRecursive(sourceDir, targetDir) {
|
|
|
1665
2182
|
async function agentInitCommand(options) {
|
|
1666
2183
|
const { product } = options;
|
|
1667
2184
|
try {
|
|
1668
|
-
p4.intro(
|
|
2185
|
+
p4.intro(chalk15.blue(`Initialize ${product.name} Configuration`));
|
|
1669
2186
|
if (!process.stdin.isTTY && !options.all) {
|
|
1670
2187
|
p4.log.error("Not running in interactive terminal.");
|
|
1671
2188
|
p4.log.info("Use --all flag to copy all files without prompting.");
|
|
1672
2189
|
process.exit(1);
|
|
1673
2190
|
}
|
|
1674
2191
|
const targetDir = process.cwd();
|
|
1675
|
-
const agentTargetDir =
|
|
2192
|
+
const agentTargetDir = path14.join(targetDir, product.dirName);
|
|
1676
2193
|
const possiblePaths = [
|
|
1677
2194
|
// When installed via npm: package root is one level up from dist
|
|
1678
|
-
|
|
2195
|
+
path14.resolve(__dirname2, "..", product.sourceDirName),
|
|
1679
2196
|
// When running from repo: repo root is two levels up from dist
|
|
1680
|
-
|
|
2197
|
+
path14.resolve(__dirname2, "../..", product.sourceDirName)
|
|
1681
2198
|
];
|
|
1682
2199
|
let sourceAgentDir = null;
|
|
1683
2200
|
for (const candidatePath of possiblePaths) {
|
|
1684
|
-
if (
|
|
2201
|
+
if (fs12.existsSync(candidatePath)) {
|
|
1685
2202
|
sourceAgentDir = candidatePath;
|
|
1686
2203
|
break;
|
|
1687
2204
|
}
|
|
@@ -1695,7 +2212,7 @@ async function agentInitCommand(options) {
|
|
|
1695
2212
|
p4.log.info("Are you running from the thoughtcabinet repository or npm package?");
|
|
1696
2213
|
process.exit(1);
|
|
1697
2214
|
}
|
|
1698
|
-
if (
|
|
2215
|
+
if (fs12.existsSync(agentTargetDir) && !options.force) {
|
|
1699
2216
|
const overwrite = await p4.confirm({
|
|
1700
2217
|
message: `${product.dirName} directory already exists. Overwrite?`,
|
|
1701
2218
|
initialValue: false
|
|
@@ -1711,18 +2228,18 @@ async function agentInitCommand(options) {
|
|
|
1711
2228
|
} else {
|
|
1712
2229
|
let commandsCount = 0;
|
|
1713
2230
|
let agentsCount = 0;
|
|
1714
|
-
const commandsDir =
|
|
1715
|
-
const agentsDir =
|
|
1716
|
-
if (
|
|
1717
|
-
commandsCount =
|
|
2231
|
+
const commandsDir = path14.join(sourceAgentDir, "commands");
|
|
2232
|
+
const agentsDir = path14.join(sourceAgentDir, "agents");
|
|
2233
|
+
if (fs12.existsSync(commandsDir)) {
|
|
2234
|
+
commandsCount = fs12.readdirSync(commandsDir).length;
|
|
1718
2235
|
}
|
|
1719
|
-
if (
|
|
1720
|
-
agentsCount =
|
|
2236
|
+
if (fs12.existsSync(agentsDir)) {
|
|
2237
|
+
agentsCount = fs12.readdirSync(agentsDir).length;
|
|
1721
2238
|
}
|
|
1722
2239
|
let skillsCount = 0;
|
|
1723
|
-
const skillsDir =
|
|
1724
|
-
if (
|
|
1725
|
-
skillsCount =
|
|
2240
|
+
const skillsDir = path14.join(sourceAgentDir, "skills");
|
|
2241
|
+
if (fs12.existsSync(skillsDir)) {
|
|
2242
|
+
skillsCount = fs12.readdirSync(skillsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).length;
|
|
1726
2243
|
}
|
|
1727
2244
|
p4.note(
|
|
1728
2245
|
"Use \u2191/\u2193 to move, press Space to select/deselect, press A to select/deselect all, press Enter to confirm. (Subsequent multi-selects apply; Ctrl+C to exit)",
|
|
@@ -1765,15 +2282,15 @@ async function agentInitCommand(options) {
|
|
|
1765
2282
|
process.exit(0);
|
|
1766
2283
|
}
|
|
1767
2284
|
}
|
|
1768
|
-
|
|
2285
|
+
fs12.mkdirSync(agentTargetDir, { recursive: true });
|
|
1769
2286
|
let filesCopied = 0;
|
|
1770
2287
|
let filesSkipped = 0;
|
|
1771
2288
|
const filesToCopyByCategory = {};
|
|
1772
2289
|
if (!options.all) {
|
|
1773
2290
|
if (selectedCategories.includes("commands")) {
|
|
1774
|
-
const sourceDir =
|
|
1775
|
-
if (
|
|
1776
|
-
const allFiles =
|
|
2291
|
+
const sourceDir = path14.join(sourceAgentDir, "commands");
|
|
2292
|
+
if (fs12.existsSync(sourceDir)) {
|
|
2293
|
+
const allFiles = fs12.readdirSync(sourceDir);
|
|
1777
2294
|
const fileSelection = await p4.multiselect({
|
|
1778
2295
|
message: "Select command files to copy:",
|
|
1779
2296
|
options: allFiles.map((file) => ({
|
|
@@ -1794,9 +2311,9 @@ async function agentInitCommand(options) {
|
|
|
1794
2311
|
}
|
|
1795
2312
|
}
|
|
1796
2313
|
if (selectedCategories.includes("agents")) {
|
|
1797
|
-
const sourceDir =
|
|
1798
|
-
if (
|
|
1799
|
-
const allFiles =
|
|
2314
|
+
const sourceDir = path14.join(sourceAgentDir, "agents");
|
|
2315
|
+
if (fs12.existsSync(sourceDir)) {
|
|
2316
|
+
const allFiles = fs12.readdirSync(sourceDir);
|
|
1800
2317
|
const fileSelection = await p4.multiselect({
|
|
1801
2318
|
message: "Select agent files to copy:",
|
|
1802
2319
|
options: allFiles.map((file) => ({
|
|
@@ -1817,9 +2334,9 @@ async function agentInitCommand(options) {
|
|
|
1817
2334
|
}
|
|
1818
2335
|
}
|
|
1819
2336
|
if (selectedCategories.includes("skills")) {
|
|
1820
|
-
const sourceDir =
|
|
1821
|
-
if (
|
|
1822
|
-
const allSkills =
|
|
2337
|
+
const sourceDir = path14.join(sourceAgentDir, "skills");
|
|
2338
|
+
if (fs12.existsSync(sourceDir)) {
|
|
2339
|
+
const allSkills = fs12.readdirSync(sourceDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
1823
2340
|
if (allSkills.length > 0) {
|
|
1824
2341
|
const skillSelection = await p4.multiselect({
|
|
1825
2342
|
message: "Select skills to copy:",
|
|
@@ -1866,13 +2383,13 @@ async function agentInitCommand(options) {
|
|
|
1866
2383
|
}
|
|
1867
2384
|
for (const category of selectedCategories) {
|
|
1868
2385
|
if (category === "commands" || category === "agents") {
|
|
1869
|
-
const sourceDir =
|
|
1870
|
-
const targetCategoryDir =
|
|
1871
|
-
if (!
|
|
2386
|
+
const sourceDir = path14.join(sourceAgentDir, category);
|
|
2387
|
+
const targetCategoryDir = path14.join(agentTargetDir, category);
|
|
2388
|
+
if (!fs12.existsSync(sourceDir)) {
|
|
1872
2389
|
p4.log.warn(`${category} directory not found in source, skipping`);
|
|
1873
2390
|
continue;
|
|
1874
2391
|
}
|
|
1875
|
-
const allFiles =
|
|
2392
|
+
const allFiles = fs12.readdirSync(sourceDir);
|
|
1876
2393
|
let filesToCopy = allFiles;
|
|
1877
2394
|
if (!options.all && filesToCopyByCategory[category]) {
|
|
1878
2395
|
filesToCopy = filesToCopyByCategory[category];
|
|
@@ -1880,20 +2397,20 @@ async function agentInitCommand(options) {
|
|
|
1880
2397
|
if (filesToCopy.length === 0) {
|
|
1881
2398
|
continue;
|
|
1882
2399
|
}
|
|
1883
|
-
|
|
2400
|
+
fs12.mkdirSync(targetCategoryDir, { recursive: true });
|
|
1884
2401
|
for (const file of filesToCopy) {
|
|
1885
|
-
const sourcePath =
|
|
1886
|
-
const targetPath =
|
|
1887
|
-
|
|
2402
|
+
const sourcePath = path14.join(sourceDir, file);
|
|
2403
|
+
const targetPath = path14.join(targetCategoryDir, file);
|
|
2404
|
+
fs12.copyFileSync(sourcePath, targetPath);
|
|
1888
2405
|
filesCopied++;
|
|
1889
2406
|
}
|
|
1890
2407
|
filesSkipped += allFiles.length - filesToCopy.length;
|
|
1891
2408
|
p4.log.success(`Copied ${filesToCopy.length} ${category} file(s)`);
|
|
1892
2409
|
} else if (category === "settings") {
|
|
1893
|
-
const settingsPath =
|
|
1894
|
-
const targetSettingsPath =
|
|
1895
|
-
if (
|
|
1896
|
-
const settingsContent =
|
|
2410
|
+
const settingsPath = path14.join(sourceAgentDir, "settings.template.json");
|
|
2411
|
+
const targetSettingsPath = path14.join(agentTargetDir, "settings.json");
|
|
2412
|
+
if (fs12.existsSync(settingsPath)) {
|
|
2413
|
+
const settingsContent = fs12.readFileSync(settingsPath, "utf8");
|
|
1897
2414
|
const settings = JSON.parse(settingsContent);
|
|
1898
2415
|
if (maxThinkingTokens !== void 0) {
|
|
1899
2416
|
if (!settings.env) {
|
|
@@ -1907,20 +2424,20 @@ async function agentInitCommand(options) {
|
|
|
1907
2424
|
for (const [key, value] of Object.entries(product.defaultEnvVars)) {
|
|
1908
2425
|
settings.env[key] = value;
|
|
1909
2426
|
}
|
|
1910
|
-
|
|
2427
|
+
fs12.writeFileSync(targetSettingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1911
2428
|
filesCopied++;
|
|
1912
2429
|
p4.log.success(`Copied settings.json (maxTokens: ${maxThinkingTokens})`);
|
|
1913
2430
|
} else {
|
|
1914
2431
|
p4.log.warn("settings.json not found in source, skipping");
|
|
1915
2432
|
}
|
|
1916
2433
|
} else if (category === "skills") {
|
|
1917
|
-
const sourceDir =
|
|
1918
|
-
const targetCategoryDir =
|
|
1919
|
-
if (!
|
|
2434
|
+
const sourceDir = path14.join(sourceAgentDir, "skills");
|
|
2435
|
+
const targetCategoryDir = path14.join(agentTargetDir, "skills");
|
|
2436
|
+
if (!fs12.existsSync(sourceDir)) {
|
|
1920
2437
|
p4.log.warn("skills directory not found in source, skipping");
|
|
1921
2438
|
continue;
|
|
1922
2439
|
}
|
|
1923
|
-
const allSkills =
|
|
2440
|
+
const allSkills = fs12.readdirSync(sourceDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
1924
2441
|
let skillsToCopy = allSkills;
|
|
1925
2442
|
if (!options.all && filesToCopyByCategory["skills"]) {
|
|
1926
2443
|
skillsToCopy = filesToCopyByCategory["skills"];
|
|
@@ -1928,11 +2445,11 @@ async function agentInitCommand(options) {
|
|
|
1928
2445
|
if (skillsToCopy.length === 0) {
|
|
1929
2446
|
continue;
|
|
1930
2447
|
}
|
|
1931
|
-
|
|
2448
|
+
fs12.mkdirSync(targetCategoryDir, { recursive: true });
|
|
1932
2449
|
let skillFilesCopied = 0;
|
|
1933
2450
|
for (const skill of skillsToCopy) {
|
|
1934
|
-
const sourceSkillPath =
|
|
1935
|
-
const targetSkillPath =
|
|
2451
|
+
const sourceSkillPath = path14.join(sourceDir, skill);
|
|
2452
|
+
const targetSkillPath = path14.join(targetCategoryDir, skill);
|
|
1936
2453
|
skillFilesCopied += copyDirectoryRecursive(sourceSkillPath, targetSkillPath);
|
|
1937
2454
|
}
|
|
1938
2455
|
filesCopied += skillFilesCopied;
|
|
@@ -1946,10 +2463,10 @@ async function agentInitCommand(options) {
|
|
|
1946
2463
|
}
|
|
1947
2464
|
let message = `Successfully copied ${filesCopied} file(s) to ${agentTargetDir}`;
|
|
1948
2465
|
if (filesSkipped > 0) {
|
|
1949
|
-
message +=
|
|
2466
|
+
message += chalk15.gray(`
|
|
1950
2467
|
Skipped ${filesSkipped} file(s)`);
|
|
1951
2468
|
}
|
|
1952
|
-
message +=
|
|
2469
|
+
message += chalk15.gray(`
|
|
1953
2470
|
You can now use these commands in ${product.name}.`);
|
|
1954
2471
|
p4.outro(message);
|
|
1955
2472
|
} catch (error) {
|
|
@@ -2009,27 +2526,27 @@ function agentCommand(program2) {
|
|
|
2009
2526
|
}
|
|
2010
2527
|
|
|
2011
2528
|
// src/commands/metadata/metadata.ts
|
|
2012
|
-
import { execSync as
|
|
2529
|
+
import { execSync as execSync7 } from "child_process";
|
|
2013
2530
|
function getGitInfo() {
|
|
2014
2531
|
try {
|
|
2015
|
-
|
|
2532
|
+
execSync7("git rev-parse --is-inside-work-tree", {
|
|
2016
2533
|
encoding: "utf8",
|
|
2017
2534
|
stdio: "pipe"
|
|
2018
2535
|
});
|
|
2019
|
-
const repoRoot =
|
|
2536
|
+
const repoRoot = execSync7("git rev-parse --show-toplevel", {
|
|
2020
2537
|
encoding: "utf8",
|
|
2021
2538
|
stdio: "pipe"
|
|
2022
2539
|
}).trim();
|
|
2023
2540
|
const repoName = repoRoot.split("/").pop() || "";
|
|
2024
2541
|
let branch = "";
|
|
2025
2542
|
try {
|
|
2026
|
-
branch =
|
|
2543
|
+
branch = execSync7("git branch --show-current", {
|
|
2027
2544
|
encoding: "utf8",
|
|
2028
2545
|
stdio: "pipe"
|
|
2029
2546
|
}).trim();
|
|
2030
2547
|
} catch {
|
|
2031
2548
|
try {
|
|
2032
|
-
branch =
|
|
2549
|
+
branch = execSync7("git rev-parse --abbrev-ref HEAD", {
|
|
2033
2550
|
encoding: "utf8",
|
|
2034
2551
|
stdio: "pipe"
|
|
2035
2552
|
}).trim();
|
|
@@ -2039,7 +2556,7 @@ function getGitInfo() {
|
|
|
2039
2556
|
}
|
|
2040
2557
|
let commit = "";
|
|
2041
2558
|
try {
|
|
2042
|
-
commit =
|
|
2559
|
+
commit = execSync7("git rev-parse HEAD", {
|
|
2043
2560
|
encoding: "utf8",
|
|
2044
2561
|
stdio: "pipe"
|
|
2045
2562
|
}).trim();
|
|
@@ -2095,6 +2612,460 @@ function metadataCommand(program2) {
|
|
|
2095
2612
|
program2.command("metadata").description("Output metadata for current repository (branch, commit, timestamp, etc.)").action(specMetadataCommand);
|
|
2096
2613
|
}
|
|
2097
2614
|
|
|
2615
|
+
// src/commands/worktree.ts
|
|
2616
|
+
import fs14 from "fs";
|
|
2617
|
+
import path16 from "path";
|
|
2618
|
+
import chalk16 from "chalk";
|
|
2619
|
+
|
|
2620
|
+
// src/tmux.ts
|
|
2621
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2622
|
+
function sessionNameForHandle(handle) {
|
|
2623
|
+
return `thc-${handle}`;
|
|
2624
|
+
}
|
|
2625
|
+
function legacySessionNameForHandle(handle) {
|
|
2626
|
+
return `thc:${handle}`;
|
|
2627
|
+
}
|
|
2628
|
+
function allSessionNamesForHandle(handle) {
|
|
2629
|
+
return [sessionNameForHandle(handle), legacySessionNameForHandle(handle)];
|
|
2630
|
+
}
|
|
2631
|
+
function listTmuxSessions() {
|
|
2632
|
+
try {
|
|
2633
|
+
const out = execFileSync3("tmux", ["list-sessions", "-F", "#{session_name}"], {
|
|
2634
|
+
encoding: "utf8",
|
|
2635
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2636
|
+
}).trim();
|
|
2637
|
+
return out.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2638
|
+
} catch {
|
|
2639
|
+
return [];
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
function tmuxHasSession(sessionName) {
|
|
2643
|
+
try {
|
|
2644
|
+
execFileSync3("tmux", ["has-session", "-t", sessionName], {
|
|
2645
|
+
stdio: "ignore"
|
|
2646
|
+
});
|
|
2647
|
+
return true;
|
|
2648
|
+
} catch {
|
|
2649
|
+
return false;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
function tmuxNewSession(sessionName, cwd) {
|
|
2653
|
+
execFileSync3("tmux", ["new-session", "-d", "-s", sessionName, "-c", cwd], {
|
|
2654
|
+
stdio: "inherit"
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
function tmuxKillSession(sessionName) {
|
|
2658
|
+
try {
|
|
2659
|
+
execFileSync3("tmux", ["kill-session", "-t", sessionName], { stdio: "ignore" });
|
|
2660
|
+
} catch {
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// src/agent-config.ts
|
|
2665
|
+
import fs13 from "fs";
|
|
2666
|
+
import path15 from "path";
|
|
2667
|
+
var AGENT_CONFIG_DIRS = [".claude", ".codebuddy"];
|
|
2668
|
+
function copyAgentConfigDirs(options) {
|
|
2669
|
+
const { sourceDir, targetDir, configDirs = AGENT_CONFIG_DIRS } = options;
|
|
2670
|
+
const copied = [];
|
|
2671
|
+
const skipped = [];
|
|
2672
|
+
for (const dirName of configDirs) {
|
|
2673
|
+
const sourcePath = path15.join(sourceDir, dirName);
|
|
2674
|
+
const targetPath = path15.join(targetDir, dirName);
|
|
2675
|
+
if (!fs13.existsSync(sourcePath)) {
|
|
2676
|
+
skipped.push(dirName);
|
|
2677
|
+
continue;
|
|
2678
|
+
}
|
|
2679
|
+
fs13.cpSync(sourcePath, targetPath, { recursive: true });
|
|
2680
|
+
copied.push(dirName);
|
|
2681
|
+
}
|
|
2682
|
+
return { copied, skipped };
|
|
2683
|
+
}
|
|
2684
|
+
|
|
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 = {
|
|
2710
|
+
hook_event_name: "PreWorktreeAdd",
|
|
2711
|
+
cwd: mainRoot,
|
|
2712
|
+
worktree_path: worktreePath,
|
|
2713
|
+
worktree_name: name,
|
|
2714
|
+
worktree_branch: options.detached ? "" : options.branch ?? name,
|
|
2715
|
+
main_root: mainRoot,
|
|
2716
|
+
session_name: sessionName,
|
|
2717
|
+
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
|
|
2744
|
+
});
|
|
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 = {
|
|
2783
|
+
hook_event_name: "PostWorktreeAdd",
|
|
2784
|
+
cwd: worktreePath,
|
|
2785
|
+
worktree_path: worktreePath,
|
|
2786
|
+
worktree_name: name,
|
|
2787
|
+
worktree_branch: options.detached ? "" : options.branch ?? name,
|
|
2788
|
+
main_root: mainRoot,
|
|
2789
|
+
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);
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
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))
|
|
2847
|
+
};
|
|
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}`));
|
|
2859
|
+
process.exit(1);
|
|
2860
|
+
}
|
|
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 = {
|
|
2898
|
+
hook_event_name: "PreWorktreeMerge",
|
|
2899
|
+
cwd: mainRoot,
|
|
2900
|
+
worktree_path: wtResolved,
|
|
2901
|
+
worktree_name: name,
|
|
2902
|
+
worktree_branch: wtEntry.branch,
|
|
2903
|
+
target_branch: targetBranch,
|
|
2904
|
+
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)) {
|
|
2939
|
+
console.error(
|
|
2940
|
+
chalk16.red(
|
|
2941
|
+
"Error: worktree has uncommitted changes. Commit/stash first or use --force."
|
|
2942
|
+
)
|
|
2943
|
+
);
|
|
2944
|
+
process.exit(1);
|
|
2945
|
+
}
|
|
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",
|
|
2987
|
+
cwd: mainRoot,
|
|
2988
|
+
worktree_path: wtResolved,
|
|
2989
|
+
worktree_name: name,
|
|
2990
|
+
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}`));
|
|
3012
|
+
process.exit(1);
|
|
3013
|
+
}
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
// src/commands/hooks/init.ts
|
|
3018
|
+
import fs15 from "fs";
|
|
3019
|
+
import path17 from "path";
|
|
3020
|
+
import chalk17 from "chalk";
|
|
3021
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3022
|
+
import { dirname as dirname2 } from "path";
|
|
3023
|
+
var __filename3 = fileURLToPath2(import.meta.url);
|
|
3024
|
+
var __dirname3 = dirname2(__filename3);
|
|
3025
|
+
async function hooksInitCommand() {
|
|
3026
|
+
try {
|
|
3027
|
+
const repoPath = process.cwd();
|
|
3028
|
+
const configDir = path17.join(repoPath, HOOKS_CONFIG_DIR);
|
|
3029
|
+
const configPath = path17.join(repoPath, HOOKS_CONFIG_FILE);
|
|
3030
|
+
if (fs15.existsSync(configPath)) {
|
|
3031
|
+
console.log(chalk17.yellow(`${HOOKS_CONFIG_FILE} already exists.`));
|
|
3032
|
+
console.log(chalk17.gray(`Edit the file directly: ${configPath}`));
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
const possiblePaths = [
|
|
3036
|
+
// When running from built dist: one level up from dist/
|
|
3037
|
+
path17.resolve(__dirname3, "..", ".thought-cabinet/hooks.example.json"),
|
|
3038
|
+
// When installed via npm: one level up from dist/
|
|
3039
|
+
path17.resolve(__dirname3, "../..", ".thought-cabinet/hooks.example.json")
|
|
3040
|
+
];
|
|
3041
|
+
let examplePath = null;
|
|
3042
|
+
for (const candidatePath of possiblePaths) {
|
|
3043
|
+
if (fs15.existsSync(candidatePath)) {
|
|
3044
|
+
examplePath = candidatePath;
|
|
3045
|
+
break;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
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}`)));
|
|
3052
|
+
process.exit(1);
|
|
3053
|
+
}
|
|
3054
|
+
fs15.mkdirSync(configDir, { recursive: true });
|
|
3055
|
+
fs15.copyFileSync(examplePath, configPath);
|
|
3056
|
+
console.log(chalk17.green(`Created ${HOOKS_CONFIG_FILE}`));
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
console.error(chalk17.red(`Error during hooks init: ${error}`));
|
|
3059
|
+
process.exit(1);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// src/commands/hooks.ts
|
|
3064
|
+
function hooksCommand(program2) {
|
|
3065
|
+
const hooks = program2.command("hooks").description("Manage hook configuration");
|
|
3066
|
+
hooks.command("init").description("Initialize hooks configuration in current repository").action(hooksInitCommand);
|
|
3067
|
+
}
|
|
3068
|
+
|
|
2098
3069
|
// src/index.ts
|
|
2099
3070
|
import dotenv2 from "dotenv";
|
|
2100
3071
|
import { createRequire } from "module";
|
|
@@ -2108,5 +3079,7 @@ program.name("thoughtcabinet").description(
|
|
|
2108
3079
|
thoughtsCommand(program);
|
|
2109
3080
|
agentCommand(program);
|
|
2110
3081
|
metadataCommand(program);
|
|
3082
|
+
worktreeCommand(program);
|
|
3083
|
+
hooksCommand(program);
|
|
2111
3084
|
program.parse(process.argv);
|
|
2112
3085
|
//# sourceMappingURL=index.js.map
|