thought-cabinet 0.0.2
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/LICENSE +15 -0
- package/README.md +145 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2113 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
- package/src/agent-assets/agents/codebase-analyzer.md +147 -0
- package/src/agent-assets/agents/codebase-locator.md +126 -0
- package/src/agent-assets/agents/codebase-pattern-finder.md +241 -0
- package/src/agent-assets/agents/thoughts-analyzer.md +154 -0
- package/src/agent-assets/agents/thoughts-locator.md +122 -0
- package/src/agent-assets/agents/web-search-researcher.md +113 -0
- package/src/agent-assets/commands/commit.md +46 -0
- package/src/agent-assets/commands/create_plan.md +278 -0
- package/src/agent-assets/commands/implement_plan.md +91 -0
- package/src/agent-assets/commands/iterate_plan.md +254 -0
- package/src/agent-assets/commands/research_codebase.md +107 -0
- package/src/agent-assets/commands/validate_plan.md +178 -0
- package/src/agent-assets/settings.template.json +7 -0
- package/src/agent-assets/skills/generating-research-document/SKILL.md +41 -0
- package/src/agent-assets/skills/generating-research-document/document_template.md +97 -0
- package/src/agent-assets/skills/writing-plan/SKILL.md +162 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/thoughts/init.ts
|
|
7
|
+
import fs4 from "fs";
|
|
8
|
+
import path6 from "path";
|
|
9
|
+
import { execSync as execSync2 } from "child_process";
|
|
10
|
+
import chalk2 from "chalk";
|
|
11
|
+
import * as p from "@clack/prompts";
|
|
12
|
+
|
|
13
|
+
// src/config.ts
|
|
14
|
+
import dotenv from "dotenv";
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
dotenv.config();
|
|
19
|
+
var _ConfigResolver = class _ConfigResolver {
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.configFile = this.loadConfigFile(options.configFile);
|
|
22
|
+
this.configFilePath = this.getConfigFilePath(options.configFile);
|
|
23
|
+
}
|
|
24
|
+
loadConfigFile(configFile) {
|
|
25
|
+
if (configFile) {
|
|
26
|
+
const configContent = fs.readFileSync(configFile, "utf8");
|
|
27
|
+
return JSON.parse(configContent);
|
|
28
|
+
}
|
|
29
|
+
const configPaths = [_ConfigResolver.DEFAULT_CONFIG_FILE, getDefaultConfigPath()];
|
|
30
|
+
for (const configPath of configPaths) {
|
|
31
|
+
try {
|
|
32
|
+
if (fs.existsSync(configPath)) {
|
|
33
|
+
const configContent = fs.readFileSync(configPath, "utf8");
|
|
34
|
+
return JSON.parse(configContent);
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(chalk.yellow(`Warning: Could not parse config file ${configPath}: ${error}`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
getConfigFilePath(configFile) {
|
|
43
|
+
if (configFile) return configFile;
|
|
44
|
+
const configPaths = [_ConfigResolver.DEFAULT_CONFIG_FILE, getDefaultConfigPath()];
|
|
45
|
+
for (const configPath of configPaths) {
|
|
46
|
+
try {
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
return configPath;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return getDefaultConfigPath();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
_ConfigResolver.DEFAULT_CONFIG_FILE = "config.json";
|
|
57
|
+
var ConfigResolver = _ConfigResolver;
|
|
58
|
+
function saveConfigFile(config, configFile) {
|
|
59
|
+
const configPath = configFile || getDefaultConfigPath();
|
|
60
|
+
console.log(chalk.yellow(`Writing config to ${configPath}`));
|
|
61
|
+
const configDir = path.dirname(configPath);
|
|
62
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
63
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
64
|
+
console.log(chalk.green("Config saved successfully"));
|
|
65
|
+
}
|
|
66
|
+
function getDefaultConfigPath() {
|
|
67
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || "", ".config");
|
|
68
|
+
return path.join(xdgConfigHome, "thought-cabinet", ConfigResolver.DEFAULT_CONFIG_FILE);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/commands/thoughts/utils/config.ts
|
|
72
|
+
function loadThoughtsConfig(options = {}) {
|
|
73
|
+
const resolver = new ConfigResolver(options);
|
|
74
|
+
return resolver.configFile.thoughts || null;
|
|
75
|
+
}
|
|
76
|
+
function saveThoughtsConfig(thoughtsConfig, options = {}) {
|
|
77
|
+
const resolver = new ConfigResolver(options);
|
|
78
|
+
resolver.configFile.thoughts = thoughtsConfig;
|
|
79
|
+
saveConfigFile(resolver.configFile, options.configFile);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/commands/thoughts/utils/paths.ts
|
|
83
|
+
import path2 from "path";
|
|
84
|
+
import os from "os";
|
|
85
|
+
function getDefaultThoughtsRepo() {
|
|
86
|
+
return path2.join(os.homedir(), "thoughts");
|
|
87
|
+
}
|
|
88
|
+
function expandPath(filePath) {
|
|
89
|
+
if (filePath.startsWith("~/")) {
|
|
90
|
+
return path2.join(os.homedir(), filePath.slice(2));
|
|
91
|
+
}
|
|
92
|
+
return path2.resolve(filePath);
|
|
93
|
+
}
|
|
94
|
+
function getCurrentRepoPath() {
|
|
95
|
+
return process.cwd();
|
|
96
|
+
}
|
|
97
|
+
function getRepoNameFromPath(repoPath) {
|
|
98
|
+
const parts = repoPath.split(path2.sep);
|
|
99
|
+
return parts[parts.length - 1] || "unnamed_repo";
|
|
100
|
+
}
|
|
101
|
+
function getRepoThoughtsPath(thoughtsRepoOrConfig, reposDirOrRepoName, repoName) {
|
|
102
|
+
if (typeof thoughtsRepoOrConfig === "string") {
|
|
103
|
+
return path2.join(expandPath(thoughtsRepoOrConfig), reposDirOrRepoName, repoName);
|
|
104
|
+
}
|
|
105
|
+
const config = thoughtsRepoOrConfig;
|
|
106
|
+
return path2.join(expandPath(config.thoughtsRepo), config.reposDir, reposDirOrRepoName);
|
|
107
|
+
}
|
|
108
|
+
function getGlobalThoughtsPath(thoughtsRepoOrConfig, globalDir) {
|
|
109
|
+
if (typeof thoughtsRepoOrConfig === "string") {
|
|
110
|
+
return path2.join(expandPath(thoughtsRepoOrConfig), globalDir);
|
|
111
|
+
}
|
|
112
|
+
const config = thoughtsRepoOrConfig;
|
|
113
|
+
return path2.join(expandPath(config.thoughtsRepo), config.globalDir);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/commands/thoughts/utils/repository.ts
|
|
117
|
+
import fs2 from "fs";
|
|
118
|
+
import path4 from "path";
|
|
119
|
+
import { execSync } from "child_process";
|
|
120
|
+
|
|
121
|
+
// src/templates/gitignore.ts
|
|
122
|
+
function generateGitignore() {
|
|
123
|
+
return `# OS files
|
|
124
|
+
.DS_Store
|
|
125
|
+
Thumbs.db
|
|
126
|
+
|
|
127
|
+
# Editor files
|
|
128
|
+
.vscode/
|
|
129
|
+
.idea/
|
|
130
|
+
*.swp
|
|
131
|
+
*.swo
|
|
132
|
+
*~
|
|
133
|
+
|
|
134
|
+
# Temporary files
|
|
135
|
+
*.tmp
|
|
136
|
+
*.bak
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/templates/readme.ts
|
|
141
|
+
function generateRepoReadme({ repoName, user }) {
|
|
142
|
+
return `# ${repoName} Thoughts
|
|
143
|
+
|
|
144
|
+
This directory contains thoughts and notes specific to the ${repoName} repository.
|
|
145
|
+
|
|
146
|
+
- \`${user}/\` - Your personal notes for this repository
|
|
147
|
+
- \`shared/\` - Team-shared notes for this repository
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
function generateGlobalReadme({ user }) {
|
|
151
|
+
return `# Global Thoughts
|
|
152
|
+
|
|
153
|
+
This directory contains thoughts and notes that apply across all repositories.
|
|
154
|
+
|
|
155
|
+
- \`${user}/\` - Your personal cross-repository notes
|
|
156
|
+
- \`shared/\` - Team-shared cross-repository notes
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/templates/agentMd.ts
|
|
161
|
+
import path3 from "path";
|
|
162
|
+
import os2 from "os";
|
|
163
|
+
function generateAgentMd({
|
|
164
|
+
thoughtsRepo,
|
|
165
|
+
reposDir,
|
|
166
|
+
repoName,
|
|
167
|
+
user,
|
|
168
|
+
productName
|
|
169
|
+
}) {
|
|
170
|
+
const reposPath = path3.join(thoughtsRepo, reposDir, repoName).replace(os2.homedir(), "~");
|
|
171
|
+
const globalPath = path3.join(thoughtsRepo, "global").replace(os2.homedir(), "~");
|
|
172
|
+
return `# Thoughts Directory Structure
|
|
173
|
+
|
|
174
|
+
This directory contains developer thoughts and notes for the ${repoName} repository.
|
|
175
|
+
It is managed by the ThoughtCabinet thoughts system and should not be committed to the code repository.
|
|
176
|
+
|
|
177
|
+
## Structure
|
|
178
|
+
|
|
179
|
+
- \`${user}/\` \u2192 Your personal notes for this repository (symlink to ${reposPath}/${user})
|
|
180
|
+
- \`shared/\` \u2192 Team-shared notes for this repository (symlink to ${reposPath}/shared)
|
|
181
|
+
- \`global/\` \u2192 Cross-repository thoughts (symlink to ${globalPath})
|
|
182
|
+
- \`${user}/\` - Your personal notes that apply across all repositories
|
|
183
|
+
- \`shared/\` - Team-shared notes that apply across all repositories
|
|
184
|
+
- \`searchable/\` \u2192 Hard links for search tools (auto-generated)
|
|
185
|
+
|
|
186
|
+
## Searching in Thoughts
|
|
187
|
+
|
|
188
|
+
The \`searchable/\` directory contains hard links to all thoughts files accessible in this repository. This allows search tools to find content without following symlinks.
|
|
189
|
+
|
|
190
|
+
**IMPORTANT**:
|
|
191
|
+
- Files in \`thoughts/searchable/\` are hard links to the original files (editing either updates both)
|
|
192
|
+
- For clarity and consistency, always reference files by their canonical path (e.g., \`thoughts/${user}/todo.md\`, not \`thoughts/searchable/${user}/todo.md\`)
|
|
193
|
+
- The \`searchable/\` directory is automatically updated when you run \`thoughtcabinet sync\`
|
|
194
|
+
|
|
195
|
+
This design ensures that:
|
|
196
|
+
1. Search tools can find all your thoughts content easily
|
|
197
|
+
2. The symlink structure remains intact for git operations
|
|
198
|
+
3. Files remain editable while maintaining consistent path references
|
|
199
|
+
|
|
200
|
+
## Usage
|
|
201
|
+
|
|
202
|
+
Create markdown files in these directories to document:
|
|
203
|
+
|
|
204
|
+
- Architecture decisions
|
|
205
|
+
- Design notes
|
|
206
|
+
- TODO items
|
|
207
|
+
- Investigation results
|
|
208
|
+
- Any other development thoughts
|
|
209
|
+
|
|
210
|
+
Quick access:
|
|
211
|
+
|
|
212
|
+
- \`thoughts/${user}/\` for your repo-specific notes (most common)
|
|
213
|
+
- \`thoughts/global/${user}/\` for your cross-repo notes
|
|
214
|
+
|
|
215
|
+
These files will be automatically synchronized with your thoughts repository when you commit code changes (when using ${productName}).
|
|
216
|
+
|
|
217
|
+
## Important
|
|
218
|
+
|
|
219
|
+
- Never commit the thoughts/ directory to your code repository
|
|
220
|
+
- The git pre-commit hook will prevent accidental commits
|
|
221
|
+
- Use \`thoughtcabinet sync\` to manually sync changes
|
|
222
|
+
- Use \`thoughtcabinet status\` to see sync status
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
function generateClaudeMd(params) {
|
|
226
|
+
return generateAgentMd({ ...params, productName: "Claude Code" });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/templates/gitHooks.ts
|
|
230
|
+
var HOOK_VERSION = "1";
|
|
231
|
+
function generatePreCommitHook({ hookPath }) {
|
|
232
|
+
return `#!/bin/bash
|
|
233
|
+
# ThoughtCabinet thoughts protection - prevent committing thoughts directory
|
|
234
|
+
# Version: ${HOOK_VERSION}
|
|
235
|
+
|
|
236
|
+
if git diff --cached --name-only | grep -q "^thoughts/"; then
|
|
237
|
+
echo "\u274C Cannot commit thoughts/ to code repository"
|
|
238
|
+
echo "The thoughts directory should only exist in your separate thoughts repository."
|
|
239
|
+
git reset HEAD -- thoughts/
|
|
240
|
+
exit 1
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# Call any existing pre-commit hook
|
|
244
|
+
if [ -f "${hookPath}.old" ]; then
|
|
245
|
+
"${hookPath}.old" "$@"
|
|
246
|
+
fi
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
function generatePostCommitHook({ hookPath }) {
|
|
250
|
+
return `#!/bin/bash
|
|
251
|
+
# ThoughtCabinet thoughts auto-sync
|
|
252
|
+
# Version: ${HOOK_VERSION}
|
|
253
|
+
|
|
254
|
+
# Check if we're in a worktree
|
|
255
|
+
if [ -f .git ]; then
|
|
256
|
+
# Skip auto-sync in worktrees to avoid repository boundary confusion
|
|
257
|
+
exit 0
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
# Get the commit message
|
|
261
|
+
COMMIT_MSG=$(git log -1 --pretty=%B)
|
|
262
|
+
|
|
263
|
+
# Auto-sync thoughts after each commit (only in non-worktree repos)
|
|
264
|
+
thoughtcabinet sync --message "Auto-sync with commit: $COMMIT_MSG" >/dev/null 2>&1 &
|
|
265
|
+
|
|
266
|
+
# Call any existing post-commit hook
|
|
267
|
+
if [ -f "${hookPath}.old" ]; then
|
|
268
|
+
"${hookPath}.old" "$@"
|
|
269
|
+
fi
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/commands/thoughts/utils/repository.ts
|
|
274
|
+
function ensureThoughtsRepoExists(configOrThoughtsRepo, reposDir, globalDir) {
|
|
275
|
+
let thoughtsRepo;
|
|
276
|
+
let effectiveReposDir;
|
|
277
|
+
let effectiveGlobalDir;
|
|
278
|
+
if (typeof configOrThoughtsRepo === "string") {
|
|
279
|
+
thoughtsRepo = configOrThoughtsRepo;
|
|
280
|
+
effectiveReposDir = reposDir;
|
|
281
|
+
effectiveGlobalDir = globalDir;
|
|
282
|
+
} else {
|
|
283
|
+
thoughtsRepo = configOrThoughtsRepo.thoughtsRepo;
|
|
284
|
+
effectiveReposDir = configOrThoughtsRepo.reposDir;
|
|
285
|
+
effectiveGlobalDir = configOrThoughtsRepo.globalDir;
|
|
286
|
+
}
|
|
287
|
+
const expandedRepo = expandPath(thoughtsRepo);
|
|
288
|
+
if (!fs2.existsSync(expandedRepo)) {
|
|
289
|
+
fs2.mkdirSync(expandedRepo, { recursive: true });
|
|
290
|
+
}
|
|
291
|
+
const expandedRepos = path4.join(expandedRepo, effectiveReposDir);
|
|
292
|
+
const expandedGlobal = path4.join(expandedRepo, effectiveGlobalDir);
|
|
293
|
+
if (!fs2.existsSync(expandedRepos)) {
|
|
294
|
+
fs2.mkdirSync(expandedRepos, { recursive: true });
|
|
295
|
+
}
|
|
296
|
+
if (!fs2.existsSync(expandedGlobal)) {
|
|
297
|
+
fs2.mkdirSync(expandedGlobal, { recursive: true });
|
|
298
|
+
}
|
|
299
|
+
const gitPath = path4.join(expandedRepo, ".git");
|
|
300
|
+
const isGitRepo = fs2.existsSync(gitPath) && (fs2.statSync(gitPath).isDirectory() || fs2.statSync(gitPath).isFile());
|
|
301
|
+
if (!isGitRepo) {
|
|
302
|
+
execSync("git init", { cwd: expandedRepo });
|
|
303
|
+
const gitignore = generateGitignore();
|
|
304
|
+
fs2.writeFileSync(path4.join(expandedRepo, ".gitignore"), gitignore);
|
|
305
|
+
execSync("git add .gitignore", { cwd: expandedRepo });
|
|
306
|
+
execSync('git commit -m "Initial thoughts repository setup"', { cwd: expandedRepo });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function createThoughtsDirectoryStructure(configOrThoughtsRepo, reposDirOrRepoName, globalDirOrUser, repoName, user) {
|
|
310
|
+
let resolvedConfig;
|
|
311
|
+
let effectiveRepoName;
|
|
312
|
+
let effectiveUser;
|
|
313
|
+
if (typeof configOrThoughtsRepo === "string") {
|
|
314
|
+
resolvedConfig = {
|
|
315
|
+
thoughtsRepo: configOrThoughtsRepo,
|
|
316
|
+
reposDir: reposDirOrRepoName,
|
|
317
|
+
globalDir: globalDirOrUser
|
|
318
|
+
};
|
|
319
|
+
effectiveRepoName = repoName;
|
|
320
|
+
effectiveUser = user;
|
|
321
|
+
} else {
|
|
322
|
+
resolvedConfig = configOrThoughtsRepo;
|
|
323
|
+
effectiveRepoName = reposDirOrRepoName;
|
|
324
|
+
effectiveUser = globalDirOrUser;
|
|
325
|
+
}
|
|
326
|
+
const repoThoughtsPath = getRepoThoughtsPath(
|
|
327
|
+
resolvedConfig.thoughtsRepo,
|
|
328
|
+
resolvedConfig.reposDir,
|
|
329
|
+
effectiveRepoName
|
|
330
|
+
);
|
|
331
|
+
const repoUserPath = path4.join(repoThoughtsPath, effectiveUser);
|
|
332
|
+
const repoSharedPath = path4.join(repoThoughtsPath, "shared");
|
|
333
|
+
const globalPath = getGlobalThoughtsPath(resolvedConfig.thoughtsRepo, resolvedConfig.globalDir);
|
|
334
|
+
const globalUserPath = path4.join(globalPath, effectiveUser);
|
|
335
|
+
const globalSharedPath = path4.join(globalPath, "shared");
|
|
336
|
+
for (const dir of [repoUserPath, repoSharedPath, globalUserPath, globalSharedPath]) {
|
|
337
|
+
if (!fs2.existsSync(dir)) {
|
|
338
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const repoReadme = generateRepoReadme({
|
|
342
|
+
repoName: effectiveRepoName,
|
|
343
|
+
user: effectiveUser
|
|
344
|
+
});
|
|
345
|
+
const globalReadme = generateGlobalReadme({
|
|
346
|
+
user: effectiveUser
|
|
347
|
+
});
|
|
348
|
+
if (!fs2.existsSync(path4.join(repoThoughtsPath, "README.md"))) {
|
|
349
|
+
fs2.writeFileSync(path4.join(repoThoughtsPath, "README.md"), repoReadme);
|
|
350
|
+
}
|
|
351
|
+
if (!fs2.existsSync(path4.join(globalPath, "README.md"))) {
|
|
352
|
+
fs2.writeFileSync(path4.join(globalPath, "README.md"), globalReadme);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/commands/thoughts/utils/symlinks.ts
|
|
357
|
+
import fs3 from "fs";
|
|
358
|
+
import path5 from "path";
|
|
359
|
+
function updateSymlinksForNewUsers(currentRepoPath, configOrThoughtsRepo, reposDirOrRepoName, repoNameOrCurrentUser, currentUser) {
|
|
360
|
+
let resolvedConfig;
|
|
361
|
+
let effectiveRepoName;
|
|
362
|
+
let effectiveUser;
|
|
363
|
+
if (typeof configOrThoughtsRepo === "string") {
|
|
364
|
+
resolvedConfig = {
|
|
365
|
+
thoughtsRepo: configOrThoughtsRepo,
|
|
366
|
+
reposDir: reposDirOrRepoName
|
|
367
|
+
};
|
|
368
|
+
effectiveRepoName = repoNameOrCurrentUser;
|
|
369
|
+
effectiveUser = currentUser;
|
|
370
|
+
} else {
|
|
371
|
+
resolvedConfig = configOrThoughtsRepo;
|
|
372
|
+
effectiveRepoName = reposDirOrRepoName;
|
|
373
|
+
effectiveUser = repoNameOrCurrentUser;
|
|
374
|
+
}
|
|
375
|
+
const thoughtsDir = path5.join(currentRepoPath, "thoughts");
|
|
376
|
+
const repoThoughtsPath = getRepoThoughtsPath(
|
|
377
|
+
resolvedConfig.thoughtsRepo,
|
|
378
|
+
resolvedConfig.reposDir,
|
|
379
|
+
effectiveRepoName
|
|
380
|
+
);
|
|
381
|
+
const addedSymlinks = [];
|
|
382
|
+
if (!fs3.existsSync(thoughtsDir) || !fs3.existsSync(repoThoughtsPath)) {
|
|
383
|
+
return addedSymlinks;
|
|
384
|
+
}
|
|
385
|
+
const entries = fs3.readdirSync(repoThoughtsPath, { withFileTypes: true });
|
|
386
|
+
const userDirs = entries.filter((entry) => entry.isDirectory() && entry.name !== "shared" && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
387
|
+
for (const userName of userDirs) {
|
|
388
|
+
const symlinkPath = path5.join(thoughtsDir, userName);
|
|
389
|
+
const targetPath = path5.join(repoThoughtsPath, userName);
|
|
390
|
+
if (!fs3.existsSync(symlinkPath) && userName !== effectiveUser) {
|
|
391
|
+
try {
|
|
392
|
+
fs3.symlinkSync(targetPath, symlinkPath, "dir");
|
|
393
|
+
addedSymlinks.push(userName);
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return addedSymlinks;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/commands/thoughts/profile/utils.ts
|
|
402
|
+
function resolveProfileForRepo(config, repoPath) {
|
|
403
|
+
const mapping = config.repoMappings[repoPath];
|
|
404
|
+
if (typeof mapping === "string") {
|
|
405
|
+
return {
|
|
406
|
+
thoughtsRepo: config.thoughtsRepo,
|
|
407
|
+
reposDir: config.reposDir,
|
|
408
|
+
globalDir: config.globalDir,
|
|
409
|
+
profileName: void 0
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (mapping && typeof mapping === "object") {
|
|
413
|
+
const profileName = mapping.profile;
|
|
414
|
+
if (profileName && config.profiles && config.profiles[profileName]) {
|
|
415
|
+
const profile = config.profiles[profileName];
|
|
416
|
+
return {
|
|
417
|
+
thoughtsRepo: profile.thoughtsRepo,
|
|
418
|
+
reposDir: profile.reposDir,
|
|
419
|
+
globalDir: profile.globalDir,
|
|
420
|
+
profileName
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
thoughtsRepo: config.thoughtsRepo,
|
|
425
|
+
reposDir: config.reposDir,
|
|
426
|
+
globalDir: config.globalDir,
|
|
427
|
+
profileName: void 0
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
thoughtsRepo: config.thoughtsRepo,
|
|
432
|
+
reposDir: config.reposDir,
|
|
433
|
+
globalDir: config.globalDir,
|
|
434
|
+
profileName: void 0
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function getRepoNameFromMapping(mapping) {
|
|
438
|
+
if (!mapping) return void 0;
|
|
439
|
+
if (typeof mapping === "string") return mapping;
|
|
440
|
+
return mapping.repo;
|
|
441
|
+
}
|
|
442
|
+
function getProfileNameFromMapping(mapping) {
|
|
443
|
+
if (!mapping) return void 0;
|
|
444
|
+
if (typeof mapping === "string") return void 0;
|
|
445
|
+
return mapping.profile;
|
|
446
|
+
}
|
|
447
|
+
function validateProfile(config, profileName) {
|
|
448
|
+
return !!(config.profiles && config.profiles[profileName]);
|
|
449
|
+
}
|
|
450
|
+
function sanitizeProfileName(name) {
|
|
451
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/commands/thoughts/init.ts
|
|
455
|
+
function sanitizeDirectoryName(name) {
|
|
456
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
457
|
+
}
|
|
458
|
+
function checkExistingSetup(config) {
|
|
459
|
+
const thoughtsDir = path6.join(process.cwd(), "thoughts");
|
|
460
|
+
if (!fs4.existsSync(thoughtsDir)) {
|
|
461
|
+
return { exists: false, isValid: false };
|
|
462
|
+
}
|
|
463
|
+
if (!fs4.lstatSync(thoughtsDir).isDirectory()) {
|
|
464
|
+
return { exists: true, isValid: false, message: "thoughts exists but is not a directory" };
|
|
465
|
+
}
|
|
466
|
+
if (!config) {
|
|
467
|
+
return {
|
|
468
|
+
exists: true,
|
|
469
|
+
isValid: false,
|
|
470
|
+
message: "thoughts directory exists but configuration is missing"
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const userPath = path6.join(thoughtsDir, config.user);
|
|
474
|
+
const sharedPath = path6.join(thoughtsDir, "shared");
|
|
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
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return { exists: true, isValid: true };
|
|
487
|
+
}
|
|
488
|
+
function setupGitHooks(repoPath) {
|
|
489
|
+
const updated = [];
|
|
490
|
+
let gitCommonDir;
|
|
491
|
+
try {
|
|
492
|
+
gitCommonDir = execSync2("git rev-parse --git-common-dir", {
|
|
493
|
+
cwd: repoPath,
|
|
494
|
+
encoding: "utf8",
|
|
495
|
+
stdio: "pipe"
|
|
496
|
+
}).trim();
|
|
497
|
+
if (!path6.isAbsolute(gitCommonDir)) {
|
|
498
|
+
gitCommonDir = path6.join(repoPath, gitCommonDir);
|
|
499
|
+
}
|
|
500
|
+
} catch (error) {
|
|
501
|
+
throw new Error(`Failed to find git common directory: ${error}`);
|
|
502
|
+
}
|
|
503
|
+
const hooksDir = path6.join(gitCommonDir, "hooks");
|
|
504
|
+
if (!fs4.existsSync(hooksDir)) {
|
|
505
|
+
fs4.mkdirSync(hooksDir, { recursive: true });
|
|
506
|
+
}
|
|
507
|
+
const preCommitPath = path6.join(hooksDir, "pre-commit");
|
|
508
|
+
const preCommitContent = generatePreCommitHook({ hookPath: preCommitPath });
|
|
509
|
+
const postCommitPath = path6.join(hooksDir, "post-commit");
|
|
510
|
+
const postCommitContent = generatePostCommitHook({ hookPath: postCommitPath });
|
|
511
|
+
const hookNeedsUpdate = (hookPath) => {
|
|
512
|
+
if (!fs4.existsSync(hookPath)) return true;
|
|
513
|
+
const content = fs4.readFileSync(hookPath, "utf8");
|
|
514
|
+
if (!content.includes("ThoughtCabinet thoughts")) return false;
|
|
515
|
+
const versionMatch = content.match(/# Version: (\d+)/);
|
|
516
|
+
if (!versionMatch) return true;
|
|
517
|
+
const currentVersion = parseInt(versionMatch[1]);
|
|
518
|
+
return currentVersion < parseInt(HOOK_VERSION);
|
|
519
|
+
};
|
|
520
|
+
if (fs4.existsSync(preCommitPath)) {
|
|
521
|
+
const content = fs4.readFileSync(preCommitPath, "utf8");
|
|
522
|
+
if (!content.includes("ThoughtCabinet thoughts") || hookNeedsUpdate(preCommitPath)) {
|
|
523
|
+
if (!content.includes("ThoughtCabinet thoughts")) {
|
|
524
|
+
fs4.renameSync(preCommitPath, `${preCommitPath}.old`);
|
|
525
|
+
} else {
|
|
526
|
+
fs4.unlinkSync(preCommitPath);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (fs4.existsSync(postCommitPath)) {
|
|
531
|
+
const content = fs4.readFileSync(postCommitPath, "utf8");
|
|
532
|
+
if (!content.includes("ThoughtCabinet thoughts") || hookNeedsUpdate(postCommitPath)) {
|
|
533
|
+
if (!content.includes("ThoughtCabinet thoughts")) {
|
|
534
|
+
fs4.renameSync(postCommitPath, `${postCommitPath}.old`);
|
|
535
|
+
} else {
|
|
536
|
+
fs4.unlinkSync(postCommitPath);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!fs4.existsSync(preCommitPath) || hookNeedsUpdate(preCommitPath)) {
|
|
541
|
+
fs4.writeFileSync(preCommitPath, preCommitContent);
|
|
542
|
+
fs4.chmodSync(preCommitPath, "755");
|
|
543
|
+
updated.push("pre-commit");
|
|
544
|
+
}
|
|
545
|
+
if (!fs4.existsSync(postCommitPath) || hookNeedsUpdate(postCommitPath)) {
|
|
546
|
+
fs4.writeFileSync(postCommitPath, postCommitContent);
|
|
547
|
+
fs4.chmodSync(postCommitPath, "755");
|
|
548
|
+
updated.push("post-commit");
|
|
549
|
+
}
|
|
550
|
+
return { updated };
|
|
551
|
+
}
|
|
552
|
+
async function thoughtsInitCommand(options) {
|
|
553
|
+
try {
|
|
554
|
+
if (!options.directory && !process.stdin.isTTY) {
|
|
555
|
+
p.log.error("Not running in interactive terminal.");
|
|
556
|
+
p.log.info("Use --directory flag to specify the repository directory name.");
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
const currentRepo = getCurrentRepoPath();
|
|
560
|
+
try {
|
|
561
|
+
execSync2("git rev-parse --git-dir", { stdio: "pipe" });
|
|
562
|
+
} catch {
|
|
563
|
+
p.log.error("Not in a git repository");
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
let config = loadThoughtsConfig(options);
|
|
567
|
+
if (!config) {
|
|
568
|
+
p.intro(chalk2.blue("Initial Thoughts Setup"));
|
|
569
|
+
p.log.info("First, let's configure your global thoughts system.");
|
|
570
|
+
const defaultRepo = getDefaultThoughtsRepo();
|
|
571
|
+
p.log.message(
|
|
572
|
+
chalk2.gray("This is where all your thoughts across all projects will be stored.")
|
|
573
|
+
);
|
|
574
|
+
const thoughtsRepoInput = await p.text({
|
|
575
|
+
message: "Thoughts repository location:",
|
|
576
|
+
initialValue: defaultRepo,
|
|
577
|
+
placeholder: defaultRepo
|
|
578
|
+
});
|
|
579
|
+
if (p.isCancel(thoughtsRepoInput)) {
|
|
580
|
+
p.cancel("Operation cancelled.");
|
|
581
|
+
process.exit(0);
|
|
582
|
+
}
|
|
583
|
+
const thoughtsRepo = thoughtsRepoInput || defaultRepo;
|
|
584
|
+
p.log.message(chalk2.gray("Your thoughts will be organized into two main directories:"));
|
|
585
|
+
p.log.message(chalk2.gray("- Repository-specific thoughts (one subdirectory per project)"));
|
|
586
|
+
p.log.message(chalk2.gray("- Global thoughts (shared across all projects)"));
|
|
587
|
+
const reposDirInput = await p.text({
|
|
588
|
+
message: "Directory name for repository-specific thoughts:",
|
|
589
|
+
initialValue: "repos",
|
|
590
|
+
placeholder: "repos"
|
|
591
|
+
});
|
|
592
|
+
if (p.isCancel(reposDirInput)) {
|
|
593
|
+
p.cancel("Operation cancelled.");
|
|
594
|
+
process.exit(0);
|
|
595
|
+
}
|
|
596
|
+
const reposDir2 = reposDirInput || "repos";
|
|
597
|
+
const globalDirInput = await p.text({
|
|
598
|
+
message: "Directory name for global thoughts:",
|
|
599
|
+
initialValue: "global",
|
|
600
|
+
placeholder: "global"
|
|
601
|
+
});
|
|
602
|
+
if (p.isCancel(globalDirInput)) {
|
|
603
|
+
p.cancel("Operation cancelled.");
|
|
604
|
+
process.exit(0);
|
|
605
|
+
}
|
|
606
|
+
const globalDir = globalDirInput || "global";
|
|
607
|
+
const defaultUser = process.env.USER || "user";
|
|
608
|
+
let user = "";
|
|
609
|
+
while (!user || user.toLowerCase() === "global") {
|
|
610
|
+
const userInput = await p.text({
|
|
611
|
+
message: "Your username:",
|
|
612
|
+
initialValue: defaultUser,
|
|
613
|
+
placeholder: defaultUser,
|
|
614
|
+
validate: (value) => {
|
|
615
|
+
if (value.toLowerCase() === "global") {
|
|
616
|
+
return `Username cannot be "global" as it's reserved for cross-project thoughts.`;
|
|
617
|
+
}
|
|
618
|
+
return void 0;
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
if (p.isCancel(userInput)) {
|
|
622
|
+
p.cancel("Operation cancelled.");
|
|
623
|
+
process.exit(0);
|
|
624
|
+
}
|
|
625
|
+
user = userInput || defaultUser;
|
|
626
|
+
}
|
|
627
|
+
config = {
|
|
628
|
+
thoughtsRepo,
|
|
629
|
+
reposDir: reposDir2,
|
|
630
|
+
globalDir,
|
|
631
|
+
user,
|
|
632
|
+
repoMappings: {}
|
|
633
|
+
};
|
|
634
|
+
p.note(
|
|
635
|
+
`${chalk2.cyan(thoughtsRepo)}/
|
|
636
|
+
\u251C\u2500\u2500 ${chalk2.cyan(reposDir2)}/ ${chalk2.gray("(project-specific thoughts)")}
|
|
637
|
+
\u2514\u2500\u2500 ${chalk2.cyan(globalDir)}/ ${chalk2.gray("(cross-project thoughts)")}`,
|
|
638
|
+
"Creating thoughts structure"
|
|
639
|
+
);
|
|
640
|
+
ensureThoughtsRepoExists(thoughtsRepo, reposDir2, globalDir);
|
|
641
|
+
saveThoughtsConfig(config, options);
|
|
642
|
+
p.log.success("Global thoughts configuration created");
|
|
643
|
+
}
|
|
644
|
+
if (options.profile) {
|
|
645
|
+
if (!validateProfile(config, options.profile)) {
|
|
646
|
+
p.log.error(`Profile "${options.profile}" does not exist.`);
|
|
647
|
+
p.log.message(chalk2.gray("Available profiles:"));
|
|
648
|
+
if (config.profiles) {
|
|
649
|
+
Object.keys(config.profiles).forEach((name) => {
|
|
650
|
+
p.log.message(chalk2.gray(` - ${name}`));
|
|
651
|
+
});
|
|
652
|
+
} else {
|
|
653
|
+
p.log.message(chalk2.gray(" (none)"));
|
|
654
|
+
}
|
|
655
|
+
p.log.warn("Create a profile first:");
|
|
656
|
+
p.log.message(chalk2.gray(` thoughtcabinet profile create ${options.profile}`));
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const tempProfileConfig = options.profile && config.profiles && config.profiles[options.profile] ? {
|
|
661
|
+
thoughtsRepo: config.profiles[options.profile].thoughtsRepo,
|
|
662
|
+
reposDir: config.profiles[options.profile].reposDir,
|
|
663
|
+
globalDir: config.profiles[options.profile].globalDir,
|
|
664
|
+
profileName: options.profile
|
|
665
|
+
} : {
|
|
666
|
+
thoughtsRepo: config.thoughtsRepo,
|
|
667
|
+
reposDir: config.reposDir,
|
|
668
|
+
globalDir: config.globalDir,
|
|
669
|
+
profileName: void 0
|
|
670
|
+
};
|
|
671
|
+
const setupStatus = checkExistingSetup(config);
|
|
672
|
+
if (setupStatus.exists && !options.force) {
|
|
673
|
+
if (setupStatus.isValid) {
|
|
674
|
+
p.log.warn("Thoughts directory already configured for this repository.");
|
|
675
|
+
const reconfigure = await p.confirm({
|
|
676
|
+
message: "Do you want to reconfigure?",
|
|
677
|
+
initialValue: false
|
|
678
|
+
});
|
|
679
|
+
if (p.isCancel(reconfigure) || !reconfigure) {
|
|
680
|
+
p.cancel("Setup cancelled.");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
p.log.warn(setupStatus.message || "Thoughts setup is incomplete");
|
|
685
|
+
const fix = await p.confirm({
|
|
686
|
+
message: "Do you want to fix the setup?",
|
|
687
|
+
initialValue: true
|
|
688
|
+
});
|
|
689
|
+
if (p.isCancel(fix) || !fix) {
|
|
690
|
+
p.cancel("Setup cancelled.");
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const expandedRepo = expandPath(tempProfileConfig.thoughtsRepo);
|
|
696
|
+
if (!fs4.existsSync(expandedRepo)) {
|
|
697
|
+
p.log.error(`Thoughts repository not found at ${tempProfileConfig.thoughtsRepo}`);
|
|
698
|
+
p.log.warn("The thoughts repository may have been moved or deleted.");
|
|
699
|
+
const recreate = await p.confirm({
|
|
700
|
+
message: "Do you want to recreate it?",
|
|
701
|
+
initialValue: true
|
|
702
|
+
});
|
|
703
|
+
if (p.isCancel(recreate) || !recreate) {
|
|
704
|
+
p.log.info("Please update your configuration or restore the thoughts repository.");
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
ensureThoughtsRepoExists(
|
|
708
|
+
tempProfileConfig.thoughtsRepo,
|
|
709
|
+
tempProfileConfig.reposDir,
|
|
710
|
+
tempProfileConfig.globalDir
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
const reposDir = path6.join(expandedRepo, tempProfileConfig.reposDir);
|
|
714
|
+
if (!fs4.existsSync(reposDir)) {
|
|
715
|
+
fs4.mkdirSync(reposDir, { recursive: true });
|
|
716
|
+
}
|
|
717
|
+
const existingRepos = fs4.readdirSync(reposDir).filter((name) => {
|
|
718
|
+
const fullPath = path6.join(reposDir, name);
|
|
719
|
+
return fs4.statSync(fullPath).isDirectory() && !name.startsWith(".");
|
|
720
|
+
});
|
|
721
|
+
const existingMapping = config.repoMappings[currentRepo];
|
|
722
|
+
let mappedName = getRepoNameFromMapping(existingMapping);
|
|
723
|
+
if (!mappedName) {
|
|
724
|
+
if (options.directory) {
|
|
725
|
+
const sanitizedDir = sanitizeDirectoryName(options.directory);
|
|
726
|
+
if (!existingRepos.includes(sanitizedDir)) {
|
|
727
|
+
p.log.error(`Directory "${sanitizedDir}" not found in thoughts repository.`);
|
|
728
|
+
p.log.error("In non-interactive mode (--directory), you must specify a directory");
|
|
729
|
+
p.log.error("name that already exists in the thoughts repository.");
|
|
730
|
+
p.log.warn("Available directories:");
|
|
731
|
+
existingRepos.forEach((repo) => p.log.message(chalk2.gray(` - ${repo}`)));
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
mappedName = sanitizedDir;
|
|
735
|
+
p.log.success(
|
|
736
|
+
`Using existing: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/${mappedName}`
|
|
737
|
+
);
|
|
738
|
+
} else {
|
|
739
|
+
p.intro(chalk2.blue("Repository Setup"));
|
|
740
|
+
p.log.info(`Setting up thoughts for: ${chalk2.cyan(currentRepo)}`);
|
|
741
|
+
p.log.message(
|
|
742
|
+
chalk2.gray(
|
|
743
|
+
`This will create a subdirectory in ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/`
|
|
744
|
+
)
|
|
745
|
+
);
|
|
746
|
+
p.log.message(chalk2.gray("to store thoughts specific to this repository."));
|
|
747
|
+
if (existingRepos.length > 0) {
|
|
748
|
+
const selectOptions = [
|
|
749
|
+
...existingRepos.map((repo) => ({ value: repo, label: `Use existing: ${repo}` })),
|
|
750
|
+
{ value: "__create_new__", label: "Create new directory" }
|
|
751
|
+
];
|
|
752
|
+
const selection = await p.select({
|
|
753
|
+
message: "Select or create a thoughts directory for this repository:",
|
|
754
|
+
options: selectOptions
|
|
755
|
+
});
|
|
756
|
+
if (p.isCancel(selection)) {
|
|
757
|
+
p.cancel("Operation cancelled.");
|
|
758
|
+
process.exit(0);
|
|
759
|
+
}
|
|
760
|
+
if (selection === "__create_new__") {
|
|
761
|
+
const defaultName = getRepoNameFromPath(currentRepo);
|
|
762
|
+
p.log.message(
|
|
763
|
+
chalk2.gray(
|
|
764
|
+
`This name will be used for the directory: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/[name]`
|
|
765
|
+
)
|
|
766
|
+
);
|
|
767
|
+
const nameInput = await p.text({
|
|
768
|
+
message: "Directory name for this project's thoughts:",
|
|
769
|
+
initialValue: defaultName,
|
|
770
|
+
placeholder: defaultName
|
|
771
|
+
});
|
|
772
|
+
if (p.isCancel(nameInput)) {
|
|
773
|
+
p.cancel("Operation cancelled.");
|
|
774
|
+
process.exit(0);
|
|
775
|
+
}
|
|
776
|
+
mappedName = nameInput || defaultName;
|
|
777
|
+
mappedName = sanitizeDirectoryName(mappedName);
|
|
778
|
+
p.log.success(
|
|
779
|
+
`Will create: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/${mappedName}`
|
|
780
|
+
);
|
|
781
|
+
} else {
|
|
782
|
+
mappedName = selection;
|
|
783
|
+
p.log.success(
|
|
784
|
+
`Will use existing: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/${mappedName}`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
const defaultName = getRepoNameFromPath(currentRepo);
|
|
789
|
+
p.log.message(
|
|
790
|
+
chalk2.gray(
|
|
791
|
+
`This name will be used for the directory: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/[name]`
|
|
792
|
+
)
|
|
793
|
+
);
|
|
794
|
+
const nameInput = await p.text({
|
|
795
|
+
message: "Directory name for this project's thoughts:",
|
|
796
|
+
initialValue: defaultName,
|
|
797
|
+
placeholder: defaultName
|
|
798
|
+
});
|
|
799
|
+
if (p.isCancel(nameInput)) {
|
|
800
|
+
p.cancel("Operation cancelled.");
|
|
801
|
+
process.exit(0);
|
|
802
|
+
}
|
|
803
|
+
mappedName = nameInput || defaultName;
|
|
804
|
+
mappedName = sanitizeDirectoryName(mappedName);
|
|
805
|
+
p.log.success(
|
|
806
|
+
`Will create: ${tempProfileConfig.thoughtsRepo}/${tempProfileConfig.reposDir}/${mappedName}`
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (options.profile) {
|
|
811
|
+
config.repoMappings[currentRepo] = {
|
|
812
|
+
repo: mappedName,
|
|
813
|
+
profile: options.profile
|
|
814
|
+
};
|
|
815
|
+
} else {
|
|
816
|
+
config.repoMappings[currentRepo] = mappedName;
|
|
817
|
+
}
|
|
818
|
+
saveThoughtsConfig(config, options);
|
|
819
|
+
}
|
|
820
|
+
if (!mappedName) {
|
|
821
|
+
mappedName = getRepoNameFromMapping(config.repoMappings[currentRepo]);
|
|
822
|
+
}
|
|
823
|
+
const profileConfig = resolveProfileForRepo(config, currentRepo);
|
|
824
|
+
createThoughtsDirectoryStructure(profileConfig, mappedName, config.user);
|
|
825
|
+
const thoughtsDir = path6.join(currentRepo, "thoughts");
|
|
826
|
+
if (fs4.existsSync(thoughtsDir)) {
|
|
827
|
+
const searchableDir = path6.join(thoughtsDir, "searchable");
|
|
828
|
+
if (fs4.existsSync(searchableDir)) {
|
|
829
|
+
try {
|
|
830
|
+
execSync2(`chmod -R 755 "${searchableDir}"`, { stdio: "pipe" });
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
fs4.rmSync(thoughtsDir, { recursive: true, force: true });
|
|
835
|
+
}
|
|
836
|
+
fs4.mkdirSync(thoughtsDir);
|
|
837
|
+
const repoTarget = getRepoThoughtsPath(profileConfig, mappedName);
|
|
838
|
+
const globalTarget = getGlobalThoughtsPath(profileConfig);
|
|
839
|
+
fs4.symlinkSync(path6.join(repoTarget, config.user), path6.join(thoughtsDir, config.user), "dir");
|
|
840
|
+
fs4.symlinkSync(path6.join(repoTarget, "shared"), path6.join(thoughtsDir, "shared"), "dir");
|
|
841
|
+
fs4.symlinkSync(globalTarget, path6.join(thoughtsDir, "global"), "dir");
|
|
842
|
+
const otherUsers = updateSymlinksForNewUsers(
|
|
843
|
+
currentRepo,
|
|
844
|
+
profileConfig,
|
|
845
|
+
mappedName,
|
|
846
|
+
config.user
|
|
847
|
+
);
|
|
848
|
+
if (otherUsers.length > 0) {
|
|
849
|
+
p.log.success(`Added symlinks for other users: ${otherUsers.join(", ")}`);
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
execSync2("git remote get-url origin", { cwd: expandedRepo, stdio: "pipe" });
|
|
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 {
|
|
863
|
+
}
|
|
864
|
+
const claudeMd = generateClaudeMd({
|
|
865
|
+
thoughtsRepo: profileConfig.thoughtsRepo,
|
|
866
|
+
reposDir: profileConfig.reposDir,
|
|
867
|
+
repoName: mappedName,
|
|
868
|
+
user: config.user
|
|
869
|
+
});
|
|
870
|
+
fs4.writeFileSync(path6.join(thoughtsDir, "CLAUDE.md"), claudeMd);
|
|
871
|
+
const hookResult = setupGitHooks(currentRepo);
|
|
872
|
+
if (hookResult.updated.length > 0) {
|
|
873
|
+
p.log.step(`Updated git hooks: ${hookResult.updated.join(", ")}`);
|
|
874
|
+
}
|
|
875
|
+
p.log.success("Thoughts setup complete!");
|
|
876
|
+
const structureText = `${chalk2.cyan(currentRepo)}/
|
|
877
|
+
\u2514\u2500\u2500 thoughts/
|
|
878
|
+
\u251C\u2500\u2500 ${config.user}/ ${chalk2.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.reposDir}/${mappedName}/${config.user}/`)}
|
|
879
|
+
\u251C\u2500\u2500 shared/ ${chalk2.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.reposDir}/${mappedName}/shared/`)}
|
|
880
|
+
\u2514\u2500\u2500 global/ ${chalk2.gray(`\u2192 ${profileConfig.thoughtsRepo}/${profileConfig.globalDir}/`)}
|
|
881
|
+
\u251C\u2500\u2500 ${config.user}/ ${chalk2.gray("(your cross-repo notes)")}
|
|
882
|
+
\u2514\u2500\u2500 shared/ ${chalk2.gray("(team cross-repo notes)")}`;
|
|
883
|
+
p.note(structureText, "Repository structure created");
|
|
884
|
+
p.note(
|
|
885
|
+
`${chalk2.green("\u2713")} Pre-commit hook: Prevents committing thoughts/
|
|
886
|
+
${chalk2.green("\u2713")} Post-commit hook: Auto-syncs thoughts after commits`,
|
|
887
|
+
"Protection enabled"
|
|
888
|
+
);
|
|
889
|
+
p.outro(
|
|
890
|
+
chalk2.gray("Next steps:\n") + chalk2.gray(
|
|
891
|
+
` 1. Run ${chalk2.cyan("thoughtcabinet sync")} to create the searchable index
|
|
892
|
+
`
|
|
893
|
+
) + chalk2.gray(
|
|
894
|
+
` 2. Create markdown files in ${chalk2.cyan(`thoughts/${config.user}/`)} for your notes
|
|
895
|
+
`
|
|
896
|
+
) + chalk2.gray(` 3. Your thoughts will sync automatically when you commit code
|
|
897
|
+
`) + chalk2.gray(` 4. Run ${chalk2.cyan("thoughtcabinet status")} to check sync status`)
|
|
898
|
+
);
|
|
899
|
+
} catch (error) {
|
|
900
|
+
p.log.error(`Error during thoughts init: ${error}`);
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/commands/thoughts/destroy.ts
|
|
906
|
+
import fs5 from "fs";
|
|
907
|
+
import path7 from "path";
|
|
908
|
+
import { execSync as execSync3 } from "child_process";
|
|
909
|
+
import chalk3 from "chalk";
|
|
910
|
+
async function thoughtsDestoryCommand(options) {
|
|
911
|
+
try {
|
|
912
|
+
const currentRepo = getCurrentRepoPath();
|
|
913
|
+
const thoughtsDir = path7.join(currentRepo, "thoughts");
|
|
914
|
+
if (!fs5.existsSync(thoughtsDir)) {
|
|
915
|
+
console.error(chalk3.red("Error: Thoughts not initialized for this repository."));
|
|
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 });
|
|
940
|
+
}
|
|
941
|
+
console.log(chalk3.gray("Removing thoughts directory (symlinks only)..."));
|
|
942
|
+
try {
|
|
943
|
+
fs5.rmSync(thoughtsDir, { recursive: true, force: true });
|
|
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));
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
if (mappedName) {
|
|
950
|
+
console.log(chalk3.gray("Removing repository from thoughts configuration..."));
|
|
951
|
+
delete config.repoMappings[currentRepo];
|
|
952
|
+
saveThoughtsConfig(config, options);
|
|
953
|
+
}
|
|
954
|
+
console.log(chalk3.green("\u2705 Thoughts removed from repository"));
|
|
955
|
+
if (mappedName) {
|
|
956
|
+
console.log("");
|
|
957
|
+
console.log(chalk3.gray("Note: Your thoughts content remains safe in:"));
|
|
958
|
+
if (profileName && config.profiles && config.profiles[profileName]) {
|
|
959
|
+
const profile = config.profiles[profileName];
|
|
960
|
+
console.log(chalk3.gray(` ${profile.thoughtsRepo}/${profile.reposDir}/${mappedName}`));
|
|
961
|
+
console.log(chalk3.gray(` (profile: ${profileName})`));
|
|
962
|
+
} else {
|
|
963
|
+
console.log(chalk3.gray(` ${config.thoughtsRepo}/${config.reposDir}/${mappedName}`));
|
|
964
|
+
}
|
|
965
|
+
console.log(chalk3.gray("Only the local symlinks and configuration were removed."));
|
|
966
|
+
}
|
|
967
|
+
} catch (error) {
|
|
968
|
+
console.error(chalk3.red(`Error during thoughts destroy: ${error}`));
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/commands/thoughts/sync.ts
|
|
974
|
+
import fs6 from "fs";
|
|
975
|
+
import path8 from "path";
|
|
976
|
+
import { execSync as execSync4, execFileSync } from "child_process";
|
|
977
|
+
import chalk4 from "chalk";
|
|
978
|
+
function checkGitStatus(repoPath) {
|
|
979
|
+
try {
|
|
980
|
+
const status = execSync4("git status --porcelain", {
|
|
981
|
+
cwd: repoPath,
|
|
982
|
+
encoding: "utf8",
|
|
983
|
+
stdio: "pipe"
|
|
984
|
+
});
|
|
985
|
+
return status.trim().length > 0;
|
|
986
|
+
} catch {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function syncThoughts(thoughtsRepo, message) {
|
|
991
|
+
const expandedRepo = expandPath(thoughtsRepo);
|
|
992
|
+
try {
|
|
993
|
+
execSync4("git add -A", { cwd: expandedRepo, stdio: "pipe" });
|
|
994
|
+
const hasChanges = checkGitStatus(expandedRepo);
|
|
995
|
+
if (hasChanges) {
|
|
996
|
+
const commitMessage = message || `Sync thoughts - ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
997
|
+
execFileSync("git", ["commit", "-m", commitMessage], { cwd: expandedRepo, stdio: "pipe" });
|
|
998
|
+
console.log(chalk4.green("\u2705 Thoughts synchronized"));
|
|
999
|
+
} else {
|
|
1000
|
+
console.log(chalk4.gray("No changes to commit"));
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
execSync4("git pull --rebase", {
|
|
1004
|
+
stdio: "pipe",
|
|
1005
|
+
cwd: expandedRepo
|
|
1006
|
+
});
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
const errorStr = error.toString();
|
|
1009
|
+
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(chalk4.red("Error: Merge conflict detected in thoughts repository"));
|
|
1011
|
+
console.error(chalk4.red("Please resolve conflicts manually in:"), expandedRepo);
|
|
1012
|
+
console.error(chalk4.red('Then run "git rebase --continue" and "thoughtcabinet sync" again'));
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
} else {
|
|
1015
|
+
console.warn(chalk4.yellow("Warning: Could not pull latest changes:"), error.message);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
try {
|
|
1019
|
+
execSync4("git remote get-url origin", { cwd: expandedRepo, stdio: "pipe" });
|
|
1020
|
+
console.log(chalk4.gray("Pushing to remote..."));
|
|
1021
|
+
try {
|
|
1022
|
+
execSync4("git push", { cwd: expandedRepo, stdio: "pipe" });
|
|
1023
|
+
console.log(chalk4.green("\u2705 Pushed to remote"));
|
|
1024
|
+
} catch {
|
|
1025
|
+
console.log(chalk4.yellow("\u26A0\uFE0F Could not push to remote. You may need to push manually."));
|
|
1026
|
+
}
|
|
1027
|
+
} catch {
|
|
1028
|
+
console.log(chalk4.yellow("\u2139\uFE0F No remote configured for thoughts repository"));
|
|
1029
|
+
}
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
console.error(chalk4.red(`Error syncing thoughts: ${error}`));
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
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
|
+
async function thoughtsSyncCommand(options) {
|
|
1092
|
+
try {
|
|
1093
|
+
const config = loadThoughtsConfig(options);
|
|
1094
|
+
if (!config) {
|
|
1095
|
+
console.error(chalk4.red('Error: Thoughts not configured. Run "thoughtcabinet init" first.'));
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
const currentRepo = getCurrentRepoPath();
|
|
1099
|
+
const thoughtsDir = path8.join(currentRepo, "thoughts");
|
|
1100
|
+
if (!fs6.existsSync(thoughtsDir)) {
|
|
1101
|
+
console.error(chalk4.red("Error: Thoughts not initialized for this repository."));
|
|
1102
|
+
console.error('Run "thoughtcabinet init" to set up thoughts.');
|
|
1103
|
+
process.exit(1);
|
|
1104
|
+
}
|
|
1105
|
+
const mapping = config.repoMappings[currentRepo];
|
|
1106
|
+
const mappedName = getRepoNameFromMapping(mapping);
|
|
1107
|
+
const profileConfig = resolveProfileForRepo(config, currentRepo);
|
|
1108
|
+
if (mappedName) {
|
|
1109
|
+
const newUsers = updateSymlinksForNewUsers(
|
|
1110
|
+
currentRepo,
|
|
1111
|
+
profileConfig,
|
|
1112
|
+
mappedName,
|
|
1113
|
+
config.user
|
|
1114
|
+
);
|
|
1115
|
+
if (newUsers.length > 0) {
|
|
1116
|
+
console.log(chalk4.green(`\u2713 Added symlinks for new users: ${newUsers.join(", ")}`));
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
console.log(chalk4.blue("Creating searchable index..."));
|
|
1120
|
+
createSearchDirectory(thoughtsDir);
|
|
1121
|
+
console.log(chalk4.blue("Syncing thoughts..."));
|
|
1122
|
+
syncThoughts(profileConfig.thoughtsRepo, options.message || "");
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
console.error(chalk4.red(`Error during thoughts sync: ${error}`));
|
|
1125
|
+
process.exit(1);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/commands/thoughts/status.ts
|
|
1130
|
+
import fs7 from "fs";
|
|
1131
|
+
import path9 from "path";
|
|
1132
|
+
import { execSync as execSync5 } from "child_process";
|
|
1133
|
+
import chalk5 from "chalk";
|
|
1134
|
+
function getGitStatus(repoPath) {
|
|
1135
|
+
try {
|
|
1136
|
+
return execSync5("git status -sb", {
|
|
1137
|
+
cwd: repoPath,
|
|
1138
|
+
encoding: "utf8",
|
|
1139
|
+
stdio: "pipe"
|
|
1140
|
+
}).trim();
|
|
1141
|
+
} catch {
|
|
1142
|
+
return "Not a git repository";
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function getUncommittedChanges(repoPath) {
|
|
1146
|
+
try {
|
|
1147
|
+
const output = execSync5("git status --porcelain", {
|
|
1148
|
+
cwd: repoPath,
|
|
1149
|
+
encoding: "utf8",
|
|
1150
|
+
stdio: "pipe"
|
|
1151
|
+
});
|
|
1152
|
+
return output.split("\n").filter((line) => line.trim()).map((line) => {
|
|
1153
|
+
const status = line.substring(0, 2);
|
|
1154
|
+
const file = line.substring(3);
|
|
1155
|
+
let statusText = "";
|
|
1156
|
+
if (status[0] === "M" || status[1] === "M") statusText = "modified";
|
|
1157
|
+
else if (status[0] === "A") statusText = "added";
|
|
1158
|
+
else if (status[0] === "D") statusText = "deleted";
|
|
1159
|
+
else if (status[0] === "?") statusText = "untracked";
|
|
1160
|
+
else if (status[0] === "R") statusText = "renamed";
|
|
1161
|
+
return ` ${chalk5.yellow(statusText.padEnd(10))} ${file}`;
|
|
1162
|
+
});
|
|
1163
|
+
} catch {
|
|
1164
|
+
return [];
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function getLastCommit(repoPath) {
|
|
1168
|
+
try {
|
|
1169
|
+
return execSync5('git log -1 --pretty=format:"%h %s (%cr)"', {
|
|
1170
|
+
cwd: repoPath,
|
|
1171
|
+
encoding: "utf8",
|
|
1172
|
+
stdio: "pipe"
|
|
1173
|
+
}).trim();
|
|
1174
|
+
} catch {
|
|
1175
|
+
return "No commits yet";
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
function getRemoteStatus(repoPath) {
|
|
1179
|
+
try {
|
|
1180
|
+
execSync5("git remote get-url origin", { cwd: repoPath, stdio: "pipe" });
|
|
1181
|
+
try {
|
|
1182
|
+
execSync5("git fetch", { cwd: repoPath, stdio: "pipe" });
|
|
1183
|
+
} catch {
|
|
1184
|
+
}
|
|
1185
|
+
const status = execSync5("git status -sb", {
|
|
1186
|
+
cwd: repoPath,
|
|
1187
|
+
encoding: "utf8",
|
|
1188
|
+
stdio: "pipe"
|
|
1189
|
+
});
|
|
1190
|
+
if (status.includes("ahead")) {
|
|
1191
|
+
const ahead = status.match(/ahead (\d+)/)?.[1] || "?";
|
|
1192
|
+
return chalk5.yellow(`${ahead} commits ahead of remote`);
|
|
1193
|
+
} else if (status.includes("behind")) {
|
|
1194
|
+
const behind = status.match(/behind (\d+)/)?.[1] || "?";
|
|
1195
|
+
try {
|
|
1196
|
+
execSync5("git pull --rebase", {
|
|
1197
|
+
stdio: "pipe",
|
|
1198
|
+
cwd: repoPath
|
|
1199
|
+
});
|
|
1200
|
+
console.log(chalk5.green("\u2713 Automatically pulled latest changes"));
|
|
1201
|
+
const newStatus = execSync5("git status -sb", {
|
|
1202
|
+
encoding: "utf8",
|
|
1203
|
+
cwd: repoPath,
|
|
1204
|
+
stdio: "pipe"
|
|
1205
|
+
});
|
|
1206
|
+
if (newStatus.includes("behind")) {
|
|
1207
|
+
const newBehind = newStatus.match(/behind (\d+)/)?.[1] || "?";
|
|
1208
|
+
return chalk5.yellow(`${newBehind} commits behind remote (after pull)`);
|
|
1209
|
+
} else {
|
|
1210
|
+
return chalk5.green("Up to date with remote (after pull)");
|
|
1211
|
+
}
|
|
1212
|
+
} catch {
|
|
1213
|
+
return chalk5.yellow(`${behind} commits behind remote`);
|
|
1214
|
+
}
|
|
1215
|
+
} else {
|
|
1216
|
+
return chalk5.green("Up to date with remote");
|
|
1217
|
+
}
|
|
1218
|
+
} catch {
|
|
1219
|
+
return chalk5.gray("No remote configured");
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function thoughtsStatusCommand(options) {
|
|
1223
|
+
try {
|
|
1224
|
+
const config = loadThoughtsConfig(options);
|
|
1225
|
+
if (!config) {
|
|
1226
|
+
console.error(chalk5.red('Error: Thoughts not configured. Run "thoughtcabinet init" first.'));
|
|
1227
|
+
process.exit(1);
|
|
1228
|
+
}
|
|
1229
|
+
console.log(chalk5.blue("Thoughts Repository Status"));
|
|
1230
|
+
console.log(chalk5.gray("=".repeat(50)));
|
|
1231
|
+
console.log("");
|
|
1232
|
+
console.log(chalk5.yellow("Configuration:"));
|
|
1233
|
+
console.log(` Repository: ${chalk5.cyan(config.thoughtsRepo)}`);
|
|
1234
|
+
console.log(` Repos directory: ${chalk5.cyan(config.reposDir)}`);
|
|
1235
|
+
console.log(` Global directory: ${chalk5.cyan(config.globalDir)}`);
|
|
1236
|
+
console.log(` User: ${chalk5.cyan(config.user)}`);
|
|
1237
|
+
console.log(` Mapped repos: ${chalk5.cyan(Object.keys(config.repoMappings).length)}`);
|
|
1238
|
+
console.log("");
|
|
1239
|
+
const currentRepo = getCurrentRepoPath();
|
|
1240
|
+
const currentMapping = config.repoMappings[currentRepo];
|
|
1241
|
+
const mappedName = getRepoNameFromMapping(currentMapping);
|
|
1242
|
+
const profileName = getProfileNameFromMapping(currentMapping);
|
|
1243
|
+
const profileConfig = resolveProfileForRepo(config, currentRepo);
|
|
1244
|
+
if (mappedName) {
|
|
1245
|
+
console.log(chalk5.yellow("Current Repository:"));
|
|
1246
|
+
console.log(` Path: ${chalk5.cyan(currentRepo)}`);
|
|
1247
|
+
console.log(` Thoughts directory: ${chalk5.cyan(`${profileConfig.reposDir}/${mappedName}`)}`);
|
|
1248
|
+
if (profileName) {
|
|
1249
|
+
console.log(` Profile: ${chalk5.cyan(profileName)}`);
|
|
1250
|
+
} else {
|
|
1251
|
+
console.log(` Profile: ${chalk5.gray("(default)")}`);
|
|
1252
|
+
}
|
|
1253
|
+
const thoughtsDir = path9.join(currentRepo, "thoughts");
|
|
1254
|
+
if (fs7.existsSync(thoughtsDir)) {
|
|
1255
|
+
console.log(` Status: ${chalk5.green("\u2713 Initialized")}`);
|
|
1256
|
+
} else {
|
|
1257
|
+
console.log(` Status: ${chalk5.red("\u2717 Not initialized")}`);
|
|
1258
|
+
}
|
|
1259
|
+
} else {
|
|
1260
|
+
console.log(chalk5.yellow("Current repository not mapped to thoughts"));
|
|
1261
|
+
}
|
|
1262
|
+
console.log("");
|
|
1263
|
+
const expandedRepo = expandPath(profileConfig.thoughtsRepo);
|
|
1264
|
+
console.log(chalk5.yellow("Thoughts Repository Git Status:"));
|
|
1265
|
+
if (profileName) {
|
|
1266
|
+
console.log(chalk5.gray(` (using profile: ${profileName})`));
|
|
1267
|
+
}
|
|
1268
|
+
console.log(` ${getGitStatus(expandedRepo)}`);
|
|
1269
|
+
console.log(` Remote: ${getRemoteStatus(expandedRepo)}`);
|
|
1270
|
+
console.log(` Last commit: ${getLastCommit(expandedRepo)}`);
|
|
1271
|
+
console.log("");
|
|
1272
|
+
const changes = getUncommittedChanges(expandedRepo);
|
|
1273
|
+
if (changes.length > 0) {
|
|
1274
|
+
console.log(chalk5.yellow("Uncommitted changes:"));
|
|
1275
|
+
changes.forEach((change) => console.log(change));
|
|
1276
|
+
console.log("");
|
|
1277
|
+
console.log(chalk5.gray('Run "thoughtcabinet sync" to commit these changes'));
|
|
1278
|
+
} else {
|
|
1279
|
+
console.log(chalk5.green("\u2713 No uncommitted changes"));
|
|
1280
|
+
}
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
console.error(chalk5.red(`Error checking thoughts status: ${error}`));
|
|
1283
|
+
process.exit(1);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/commands/thoughts/config.ts
|
|
1288
|
+
import { spawn } from "child_process";
|
|
1289
|
+
import chalk6 from "chalk";
|
|
1290
|
+
async function thoughtsConfigCommand(options) {
|
|
1291
|
+
try {
|
|
1292
|
+
const configPath = options.configFile || getDefaultConfigPath();
|
|
1293
|
+
if (options.edit) {
|
|
1294
|
+
const editor = process.env.EDITOR || "vi";
|
|
1295
|
+
spawn(editor, [configPath], { stdio: "inherit" });
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const config = loadThoughtsConfig(options);
|
|
1299
|
+
if (!config) {
|
|
1300
|
+
console.error(chalk6.red("No thoughts configuration found."));
|
|
1301
|
+
console.error('Run "thoughtcabinet init" to create one.');
|
|
1302
|
+
process.exit(1);
|
|
1303
|
+
}
|
|
1304
|
+
if (options.json) {
|
|
1305
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
console.log(chalk6.blue("Thoughts Configuration"));
|
|
1309
|
+
console.log(chalk6.gray("=".repeat(50)));
|
|
1310
|
+
console.log("");
|
|
1311
|
+
console.log(chalk6.yellow("Settings:"));
|
|
1312
|
+
console.log(` Config file: ${chalk6.cyan(configPath)}`);
|
|
1313
|
+
console.log(` Thoughts repository: ${chalk6.cyan(config.thoughtsRepo)}`);
|
|
1314
|
+
console.log(` Repos directory: ${chalk6.cyan(config.reposDir)}`);
|
|
1315
|
+
console.log(` Global directory: ${chalk6.cyan(config.globalDir)}`);
|
|
1316
|
+
console.log(` User: ${chalk6.cyan(config.user)}`);
|
|
1317
|
+
console.log("");
|
|
1318
|
+
console.log(chalk6.yellow("Repository Mappings:"));
|
|
1319
|
+
const mappings = Object.entries(config.repoMappings);
|
|
1320
|
+
if (mappings.length === 0) {
|
|
1321
|
+
console.log(chalk6.gray(" No repositories mapped yet"));
|
|
1322
|
+
} else {
|
|
1323
|
+
mappings.forEach(([repo, mapping]) => {
|
|
1324
|
+
const repoName = getRepoNameFromMapping(mapping);
|
|
1325
|
+
const profileName = getProfileNameFromMapping(mapping);
|
|
1326
|
+
console.log(` ${chalk6.cyan(repo)}`);
|
|
1327
|
+
console.log(` \u2192 ${chalk6.green(`${config.reposDir}/${repoName}`)}`);
|
|
1328
|
+
if (profileName) {
|
|
1329
|
+
console.log(` Profile: ${chalk6.yellow(profileName)}`);
|
|
1330
|
+
} else {
|
|
1331
|
+
console.log(` Profile: ${chalk6.gray("(default)")}`);
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
console.log("");
|
|
1336
|
+
console.log(chalk6.yellow("Profiles:"));
|
|
1337
|
+
if (!config.profiles || Object.keys(config.profiles).length === 0) {
|
|
1338
|
+
console.log(chalk6.gray(" No profiles configured"));
|
|
1339
|
+
} else {
|
|
1340
|
+
Object.keys(config.profiles).forEach((name) => {
|
|
1341
|
+
console.log(` ${chalk6.cyan(name)}`);
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
console.log("");
|
|
1345
|
+
console.log(chalk6.gray("To edit configuration, run: thoughtcabinet config --edit"));
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
console.error(chalk6.red(`Error showing thoughts config: ${error}`));
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// src/commands/thoughts/profile/create.ts
|
|
1353
|
+
import chalk7 from "chalk";
|
|
1354
|
+
import * as p2 from "@clack/prompts";
|
|
1355
|
+
async function profileCreateCommand(profileName, options) {
|
|
1356
|
+
try {
|
|
1357
|
+
if (!options.repo || !options.reposDir || !options.globalDir) {
|
|
1358
|
+
if (!process.stdin.isTTY) {
|
|
1359
|
+
p2.log.error("Not running in interactive terminal.");
|
|
1360
|
+
p2.log.info("Provide all options: --repo, --repos-dir, --global-dir");
|
|
1361
|
+
process.exit(1);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const config = loadThoughtsConfig(options);
|
|
1365
|
+
if (!config) {
|
|
1366
|
+
p2.log.error("Thoughts not configured.");
|
|
1367
|
+
p2.log.info('Run "thoughtcabinet init" first to set up the base configuration.');
|
|
1368
|
+
process.exit(1);
|
|
1369
|
+
}
|
|
1370
|
+
const sanitizedName = sanitizeProfileName(profileName);
|
|
1371
|
+
if (sanitizedName !== profileName) {
|
|
1372
|
+
p2.log.warn(`Profile name sanitized: "${profileName}" \u2192 "${sanitizedName}"`);
|
|
1373
|
+
}
|
|
1374
|
+
p2.intro(chalk7.blue(`Creating Profile: ${sanitizedName}`));
|
|
1375
|
+
if (validateProfile(config, sanitizedName)) {
|
|
1376
|
+
p2.log.error(`Profile "${sanitizedName}" already exists.`);
|
|
1377
|
+
p2.log.info("Use a different name or delete the existing profile first.");
|
|
1378
|
+
process.exit(1);
|
|
1379
|
+
}
|
|
1380
|
+
let thoughtsRepo;
|
|
1381
|
+
let reposDir;
|
|
1382
|
+
let globalDir;
|
|
1383
|
+
if (options.repo && options.reposDir && options.globalDir) {
|
|
1384
|
+
thoughtsRepo = options.repo;
|
|
1385
|
+
reposDir = options.reposDir;
|
|
1386
|
+
globalDir = options.globalDir;
|
|
1387
|
+
} else {
|
|
1388
|
+
const defaultRepo = getDefaultThoughtsRepo() + `-${sanitizedName}`;
|
|
1389
|
+
p2.log.info("Specify the thoughts repository location for this profile.");
|
|
1390
|
+
const repoInput = await p2.text({
|
|
1391
|
+
message: "Thoughts repository:",
|
|
1392
|
+
initialValue: defaultRepo,
|
|
1393
|
+
placeholder: defaultRepo
|
|
1394
|
+
});
|
|
1395
|
+
if (p2.isCancel(repoInput)) {
|
|
1396
|
+
p2.cancel("Operation cancelled.");
|
|
1397
|
+
process.exit(0);
|
|
1398
|
+
}
|
|
1399
|
+
thoughtsRepo = repoInput || defaultRepo;
|
|
1400
|
+
const reposDirInput = await p2.text({
|
|
1401
|
+
message: "Repository-specific thoughts directory:",
|
|
1402
|
+
initialValue: "repos",
|
|
1403
|
+
placeholder: "repos"
|
|
1404
|
+
});
|
|
1405
|
+
if (p2.isCancel(reposDirInput)) {
|
|
1406
|
+
p2.cancel("Operation cancelled.");
|
|
1407
|
+
process.exit(0);
|
|
1408
|
+
}
|
|
1409
|
+
reposDir = reposDirInput || "repos";
|
|
1410
|
+
const globalDirInput = await p2.text({
|
|
1411
|
+
message: "Global thoughts directory:",
|
|
1412
|
+
initialValue: "global",
|
|
1413
|
+
placeholder: "global"
|
|
1414
|
+
});
|
|
1415
|
+
if (p2.isCancel(globalDirInput)) {
|
|
1416
|
+
p2.cancel("Operation cancelled.");
|
|
1417
|
+
process.exit(0);
|
|
1418
|
+
}
|
|
1419
|
+
globalDir = globalDirInput || "global";
|
|
1420
|
+
}
|
|
1421
|
+
const profileConfig = {
|
|
1422
|
+
thoughtsRepo,
|
|
1423
|
+
reposDir,
|
|
1424
|
+
globalDir
|
|
1425
|
+
};
|
|
1426
|
+
if (!config.profiles) {
|
|
1427
|
+
config.profiles = {};
|
|
1428
|
+
}
|
|
1429
|
+
config.profiles[sanitizedName] = profileConfig;
|
|
1430
|
+
saveThoughtsConfig(config, options);
|
|
1431
|
+
p2.log.step("Initializing profile thoughts repository...");
|
|
1432
|
+
ensureThoughtsRepoExists(profileConfig);
|
|
1433
|
+
p2.log.success(`Profile "${sanitizedName}" created successfully!`);
|
|
1434
|
+
p2.note(
|
|
1435
|
+
`Name: ${chalk7.cyan(sanitizedName)}
|
|
1436
|
+
Thoughts repository: ${chalk7.cyan(thoughtsRepo)}
|
|
1437
|
+
Repos directory: ${chalk7.cyan(reposDir)}
|
|
1438
|
+
Global directory: ${chalk7.cyan(globalDir)}`,
|
|
1439
|
+
"Profile Configuration"
|
|
1440
|
+
);
|
|
1441
|
+
p2.outro(
|
|
1442
|
+
chalk7.gray("Next steps:\n") + chalk7.gray(` 1. Run "thoughtcabinet init --profile ${sanitizedName}" in a repository
|
|
1443
|
+
`) + chalk7.gray(` 2. Your thoughts will sync to the profile's repository`)
|
|
1444
|
+
);
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
p2.log.error(`Error creating profile: ${error}`);
|
|
1447
|
+
process.exit(1);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// src/commands/thoughts/profile/list.ts
|
|
1452
|
+
import chalk8 from "chalk";
|
|
1453
|
+
async function profileListCommand(options) {
|
|
1454
|
+
try {
|
|
1455
|
+
const config = loadThoughtsConfig(options);
|
|
1456
|
+
if (!config) {
|
|
1457
|
+
console.error(chalk8.red("Error: Thoughts not configured."));
|
|
1458
|
+
process.exit(1);
|
|
1459
|
+
}
|
|
1460
|
+
if (options.json) {
|
|
1461
|
+
console.log(JSON.stringify(config.profiles || {}, null, 2));
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
console.log(chalk8.blue("Thoughts Profiles"));
|
|
1465
|
+
console.log(chalk8.gray("=".repeat(50)));
|
|
1466
|
+
console.log("");
|
|
1467
|
+
console.log(chalk8.yellow("Default Configuration:"));
|
|
1468
|
+
console.log(` Thoughts repository: ${chalk8.cyan(config.thoughtsRepo)}`);
|
|
1469
|
+
console.log(` Repos directory: ${chalk8.cyan(config.reposDir)}`);
|
|
1470
|
+
console.log(` Global directory: ${chalk8.cyan(config.globalDir)}`);
|
|
1471
|
+
console.log("");
|
|
1472
|
+
if (!config.profiles || Object.keys(config.profiles).length === 0) {
|
|
1473
|
+
console.log(chalk8.gray("No profiles configured."));
|
|
1474
|
+
console.log("");
|
|
1475
|
+
console.log(chalk8.gray("Create a profile with: thoughtcabinet profile create <name>"));
|
|
1476
|
+
} else {
|
|
1477
|
+
console.log(chalk8.yellow(`Profiles (${Object.keys(config.profiles).length}):`));
|
|
1478
|
+
console.log("");
|
|
1479
|
+
Object.entries(config.profiles).forEach(([name, profile]) => {
|
|
1480
|
+
console.log(chalk8.cyan(` ${name}:`));
|
|
1481
|
+
console.log(` Thoughts repository: ${profile.thoughtsRepo}`);
|
|
1482
|
+
console.log(` Repos directory: ${profile.reposDir}`);
|
|
1483
|
+
console.log(` Global directory: ${profile.globalDir}`);
|
|
1484
|
+
console.log("");
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
console.error(chalk8.red(`Error listing profiles: ${error}`));
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// src/commands/thoughts/profile/show.ts
|
|
1494
|
+
import chalk9 from "chalk";
|
|
1495
|
+
async function profileShowCommand(profileName, options) {
|
|
1496
|
+
try {
|
|
1497
|
+
const config = loadThoughtsConfig(options);
|
|
1498
|
+
if (!config) {
|
|
1499
|
+
console.error(chalk9.red("Error: Thoughts not configured."));
|
|
1500
|
+
process.exit(1);
|
|
1501
|
+
}
|
|
1502
|
+
if (!validateProfile(config, profileName)) {
|
|
1503
|
+
console.error(chalk9.red(`Error: Profile "${profileName}" not found.`));
|
|
1504
|
+
console.error("");
|
|
1505
|
+
console.error(chalk9.gray("Available profiles:"));
|
|
1506
|
+
if (config.profiles) {
|
|
1507
|
+
Object.keys(config.profiles).forEach((name) => {
|
|
1508
|
+
console.error(chalk9.gray(` - ${name}`));
|
|
1509
|
+
});
|
|
1510
|
+
} else {
|
|
1511
|
+
console.error(chalk9.gray(" (none)"));
|
|
1512
|
+
}
|
|
1513
|
+
process.exit(1);
|
|
1514
|
+
}
|
|
1515
|
+
const profile = config.profiles[profileName];
|
|
1516
|
+
if (options.json) {
|
|
1517
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
console.log(chalk9.blue(`Profile: ${profileName}`));
|
|
1521
|
+
console.log(chalk9.gray("=".repeat(50)));
|
|
1522
|
+
console.log("");
|
|
1523
|
+
console.log(chalk9.yellow("Configuration:"));
|
|
1524
|
+
console.log(` Thoughts repository: ${chalk9.cyan(profile.thoughtsRepo)}`);
|
|
1525
|
+
console.log(` Repos directory: ${chalk9.cyan(profile.reposDir)}`);
|
|
1526
|
+
console.log(` Global directory: ${chalk9.cyan(profile.globalDir)}`);
|
|
1527
|
+
console.log("");
|
|
1528
|
+
let repoCount = 0;
|
|
1529
|
+
Object.values(config.repoMappings).forEach((mapping) => {
|
|
1530
|
+
if (typeof mapping === "object" && mapping.profile === profileName) {
|
|
1531
|
+
repoCount++;
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
console.log(chalk9.yellow("Usage:"));
|
|
1535
|
+
console.log(` Repositories using this profile: ${chalk9.cyan(repoCount)}`);
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
console.error(chalk9.red(`Error showing profile: ${error}`));
|
|
1538
|
+
process.exit(1);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/commands/thoughts/profile/delete.ts
|
|
1543
|
+
import chalk10 from "chalk";
|
|
1544
|
+
import * as p3 from "@clack/prompts";
|
|
1545
|
+
async function profileDeleteCommand(profileName, options) {
|
|
1546
|
+
try {
|
|
1547
|
+
if (!options.force && !process.stdin.isTTY) {
|
|
1548
|
+
p3.log.error("Not running in interactive terminal.");
|
|
1549
|
+
p3.log.info("Use --force flag to delete without confirmation.");
|
|
1550
|
+
process.exit(1);
|
|
1551
|
+
}
|
|
1552
|
+
p3.intro(chalk10.blue(`Delete Profile: ${profileName}`));
|
|
1553
|
+
const config = loadThoughtsConfig(options);
|
|
1554
|
+
if (!config) {
|
|
1555
|
+
p3.log.error("Thoughts not configured.");
|
|
1556
|
+
process.exit(1);
|
|
1557
|
+
}
|
|
1558
|
+
if (!validateProfile(config, profileName)) {
|
|
1559
|
+
p3.log.error(`Profile "${profileName}" not found.`);
|
|
1560
|
+
process.exit(1);
|
|
1561
|
+
}
|
|
1562
|
+
const usingRepos = [];
|
|
1563
|
+
Object.entries(config.repoMappings).forEach(([repoPath, mapping]) => {
|
|
1564
|
+
if (typeof mapping === "object" && mapping.profile === profileName) {
|
|
1565
|
+
usingRepos.push(repoPath);
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
if (usingRepos.length > 0 && !options.force) {
|
|
1569
|
+
p3.log.error(`Profile "${profileName}" is in use by ${usingRepos.length} repository(ies):`);
|
|
1570
|
+
usingRepos.forEach((repo) => {
|
|
1571
|
+
p3.log.message(chalk10.gray(` - ${repo}`));
|
|
1572
|
+
});
|
|
1573
|
+
p3.log.warn("Options:");
|
|
1574
|
+
p3.log.message(chalk10.gray(' 1. Run "thoughtcabinet destroy" in each repository'));
|
|
1575
|
+
p3.log.message(
|
|
1576
|
+
chalk10.gray(" 2. Use --force to delete anyway (repos will fall back to default config)")
|
|
1577
|
+
);
|
|
1578
|
+
process.exit(1);
|
|
1579
|
+
}
|
|
1580
|
+
if (!options.force) {
|
|
1581
|
+
p3.log.warn(`You are about to delete profile: ${chalk10.cyan(profileName)}`);
|
|
1582
|
+
p3.log.message(chalk10.gray("This will remove the profile configuration."));
|
|
1583
|
+
p3.log.message(chalk10.gray("The thoughts repository files will NOT be deleted."));
|
|
1584
|
+
const confirmDelete = await p3.confirm({
|
|
1585
|
+
message: `Delete profile "${profileName}"?`,
|
|
1586
|
+
initialValue: false
|
|
1587
|
+
});
|
|
1588
|
+
if (p3.isCancel(confirmDelete) || !confirmDelete) {
|
|
1589
|
+
p3.cancel("Deletion cancelled.");
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
delete config.profiles[profileName];
|
|
1594
|
+
if (Object.keys(config.profiles).length === 0) {
|
|
1595
|
+
delete config.profiles;
|
|
1596
|
+
}
|
|
1597
|
+
saveThoughtsConfig(config, options);
|
|
1598
|
+
p3.log.success(`Profile "${profileName}" deleted`);
|
|
1599
|
+
if (usingRepos.length > 0) {
|
|
1600
|
+
p3.log.warn("Repositories using this profile will fall back to default config");
|
|
1601
|
+
}
|
|
1602
|
+
p3.outro(chalk10.green("Done"));
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
p3.log.error(`Error deleting profile: ${error}`);
|
|
1605
|
+
process.exit(1);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// src/commands/thoughts.ts
|
|
1610
|
+
function thoughtsCommand(program2) {
|
|
1611
|
+
const cmd = program2;
|
|
1612
|
+
cmd.command("init").description("Initialize thoughts for current repository").option("--force", "Force reconfiguration even if already set up").option("--config-file <path>", "Path to config file").option(
|
|
1613
|
+
"--directory <name>",
|
|
1614
|
+
"Specify the repository directory name (skips interactive prompt)"
|
|
1615
|
+
).option("--profile <name>", "Use a specific thoughts profile").action(thoughtsInitCommand);
|
|
1616
|
+
cmd.command("destroy").description("Remove thoughts setup from current repository").option("--force", "Force removal even if not in configuration").option("--config-file <path>", "Path to config file").action(thoughtsDestoryCommand);
|
|
1617
|
+
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
|
+
cmd.command("status").description("Show status of thoughts repository").option("--config-file <path>", "Path to config file").action(thoughtsStatusCommand);
|
|
1619
|
+
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);
|
|
1620
|
+
const profile = cmd.command("profile").description("Manage thoughts profiles");
|
|
1621
|
+
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
|
+
profile.command("list").description("List all thoughts profiles").option("--json", "Output as JSON").option("--config-file <path>", "Path to config file").action(profileListCommand);
|
|
1623
|
+
profile.command("show <name>").description("Show details of a specific profile").option("--json", "Output as JSON").option("--config-file <path>", "Path to config file").action(profileShowCommand);
|
|
1624
|
+
profile.command("delete <name>").description("Delete a thoughts profile").option("--force", "Force deletion even if in use").option("--config-file <path>", "Path to config file").action(profileDeleteCommand);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// src/commands/agent/init.ts
|
|
1628
|
+
import fs8 from "fs";
|
|
1629
|
+
import path10 from "path";
|
|
1630
|
+
import chalk11 from "chalk";
|
|
1631
|
+
import * as p4 from "@clack/prompts";
|
|
1632
|
+
import { fileURLToPath } from "url";
|
|
1633
|
+
import { dirname } from "path";
|
|
1634
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
1635
|
+
var __dirname2 = dirname(__filename2);
|
|
1636
|
+
function ensureGitignoreEntry(targetDir, entry, productName) {
|
|
1637
|
+
const gitignorePath = path10.join(targetDir, ".gitignore");
|
|
1638
|
+
let gitignoreContent = "";
|
|
1639
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
1640
|
+
gitignoreContent = fs8.readFileSync(gitignorePath, "utf8");
|
|
1641
|
+
}
|
|
1642
|
+
const lines = gitignoreContent.split("\n");
|
|
1643
|
+
if (lines.some((line) => line.trim() === entry)) {
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const newContent = gitignoreContent + (gitignoreContent && !gitignoreContent.endsWith("\n") ? "\n" : "") + "\n# " + productName + " local settings\n" + entry + "\n";
|
|
1647
|
+
fs8.writeFileSync(gitignorePath, newContent);
|
|
1648
|
+
}
|
|
1649
|
+
function copyDirectoryRecursive(sourceDir, targetDir) {
|
|
1650
|
+
let filesCopied = 0;
|
|
1651
|
+
fs8.mkdirSync(targetDir, { recursive: true });
|
|
1652
|
+
const entries = fs8.readdirSync(sourceDir, { withFileTypes: true });
|
|
1653
|
+
for (const entry of entries) {
|
|
1654
|
+
const sourcePath = path10.join(sourceDir, entry.name);
|
|
1655
|
+
const targetPath = path10.join(targetDir, entry.name);
|
|
1656
|
+
if (entry.isDirectory()) {
|
|
1657
|
+
filesCopied += copyDirectoryRecursive(sourcePath, targetPath);
|
|
1658
|
+
} else {
|
|
1659
|
+
fs8.copyFileSync(sourcePath, targetPath);
|
|
1660
|
+
filesCopied++;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return filesCopied;
|
|
1664
|
+
}
|
|
1665
|
+
async function agentInitCommand(options) {
|
|
1666
|
+
const { product } = options;
|
|
1667
|
+
try {
|
|
1668
|
+
p4.intro(chalk11.blue(`Initialize ${product.name} Configuration`));
|
|
1669
|
+
if (!process.stdin.isTTY && !options.all) {
|
|
1670
|
+
p4.log.error("Not running in interactive terminal.");
|
|
1671
|
+
p4.log.info("Use --all flag to copy all files without prompting.");
|
|
1672
|
+
process.exit(1);
|
|
1673
|
+
}
|
|
1674
|
+
const targetDir = process.cwd();
|
|
1675
|
+
const agentTargetDir = path10.join(targetDir, product.dirName);
|
|
1676
|
+
const possiblePaths = [
|
|
1677
|
+
// When installed via npm: package root is one level up from dist
|
|
1678
|
+
path10.resolve(__dirname2, "..", product.sourceDirName),
|
|
1679
|
+
// When running from repo: repo root is two levels up from dist
|
|
1680
|
+
path10.resolve(__dirname2, "../..", product.sourceDirName)
|
|
1681
|
+
];
|
|
1682
|
+
let sourceAgentDir = null;
|
|
1683
|
+
for (const candidatePath of possiblePaths) {
|
|
1684
|
+
if (fs8.existsSync(candidatePath)) {
|
|
1685
|
+
sourceAgentDir = candidatePath;
|
|
1686
|
+
break;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (!sourceAgentDir) {
|
|
1690
|
+
p4.log.error(`Source ${product.dirName} directory not found in expected locations`);
|
|
1691
|
+
p4.log.info("Searched paths:");
|
|
1692
|
+
possiblePaths.forEach((candidatePath) => {
|
|
1693
|
+
p4.log.info(` - ${candidatePath}`);
|
|
1694
|
+
});
|
|
1695
|
+
p4.log.info("Are you running from the thoughtcabinet repository or npm package?");
|
|
1696
|
+
process.exit(1);
|
|
1697
|
+
}
|
|
1698
|
+
if (fs8.existsSync(agentTargetDir) && !options.force) {
|
|
1699
|
+
const overwrite = await p4.confirm({
|
|
1700
|
+
message: `${product.dirName} directory already exists. Overwrite?`,
|
|
1701
|
+
initialValue: false
|
|
1702
|
+
});
|
|
1703
|
+
if (p4.isCancel(overwrite) || !overwrite) {
|
|
1704
|
+
p4.cancel("Operation cancelled.");
|
|
1705
|
+
process.exit(0);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
let selectedCategories;
|
|
1709
|
+
if (options.all) {
|
|
1710
|
+
selectedCategories = ["commands", "agents", "skills", "settings"];
|
|
1711
|
+
} else {
|
|
1712
|
+
let commandsCount = 0;
|
|
1713
|
+
let agentsCount = 0;
|
|
1714
|
+
const commandsDir = path10.join(sourceAgentDir, "commands");
|
|
1715
|
+
const agentsDir = path10.join(sourceAgentDir, "agents");
|
|
1716
|
+
if (fs8.existsSync(commandsDir)) {
|
|
1717
|
+
commandsCount = fs8.readdirSync(commandsDir).length;
|
|
1718
|
+
}
|
|
1719
|
+
if (fs8.existsSync(agentsDir)) {
|
|
1720
|
+
agentsCount = fs8.readdirSync(agentsDir).length;
|
|
1721
|
+
}
|
|
1722
|
+
let skillsCount = 0;
|
|
1723
|
+
const skillsDir = path10.join(sourceAgentDir, "skills");
|
|
1724
|
+
if (fs8.existsSync(skillsDir)) {
|
|
1725
|
+
skillsCount = fs8.readdirSync(skillsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).length;
|
|
1726
|
+
}
|
|
1727
|
+
p4.note(
|
|
1728
|
+
"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)",
|
|
1729
|
+
"Multi-select instructions"
|
|
1730
|
+
);
|
|
1731
|
+
const selection = await p4.multiselect({
|
|
1732
|
+
message: "What would you like to copy?",
|
|
1733
|
+
options: [
|
|
1734
|
+
{
|
|
1735
|
+
value: "commands",
|
|
1736
|
+
label: "Commands",
|
|
1737
|
+
hint: `${commandsCount} workflow commands (planning, CI, research, etc.)`
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
value: "agents",
|
|
1741
|
+
label: "Agents",
|
|
1742
|
+
hint: `${agentsCount} specialized sub-agents for code analysis`
|
|
1743
|
+
},
|
|
1744
|
+
{
|
|
1745
|
+
value: "skills",
|
|
1746
|
+
label: "Skills",
|
|
1747
|
+
hint: `${skillsCount} specialized skill packages for extended capabilities`
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
value: "settings",
|
|
1751
|
+
label: "Settings",
|
|
1752
|
+
hint: "Project permissions configuration"
|
|
1753
|
+
}
|
|
1754
|
+
],
|
|
1755
|
+
initialValues: ["commands", "agents", "skills", "settings"],
|
|
1756
|
+
required: false
|
|
1757
|
+
});
|
|
1758
|
+
if (p4.isCancel(selection)) {
|
|
1759
|
+
p4.cancel("Operation cancelled.");
|
|
1760
|
+
process.exit(0);
|
|
1761
|
+
}
|
|
1762
|
+
selectedCategories = selection;
|
|
1763
|
+
if (selectedCategories.length === 0) {
|
|
1764
|
+
p4.cancel("No items selected.");
|
|
1765
|
+
process.exit(0);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
fs8.mkdirSync(agentTargetDir, { recursive: true });
|
|
1769
|
+
let filesCopied = 0;
|
|
1770
|
+
let filesSkipped = 0;
|
|
1771
|
+
const filesToCopyByCategory = {};
|
|
1772
|
+
if (!options.all) {
|
|
1773
|
+
if (selectedCategories.includes("commands")) {
|
|
1774
|
+
const sourceDir = path10.join(sourceAgentDir, "commands");
|
|
1775
|
+
if (fs8.existsSync(sourceDir)) {
|
|
1776
|
+
const allFiles = fs8.readdirSync(sourceDir);
|
|
1777
|
+
const fileSelection = await p4.multiselect({
|
|
1778
|
+
message: "Select command files to copy:",
|
|
1779
|
+
options: allFiles.map((file) => ({
|
|
1780
|
+
value: file,
|
|
1781
|
+
label: file
|
|
1782
|
+
})),
|
|
1783
|
+
initialValues: allFiles,
|
|
1784
|
+
required: false
|
|
1785
|
+
});
|
|
1786
|
+
if (p4.isCancel(fileSelection)) {
|
|
1787
|
+
p4.cancel("Operation cancelled.");
|
|
1788
|
+
process.exit(0);
|
|
1789
|
+
}
|
|
1790
|
+
filesToCopyByCategory["commands"] = fileSelection;
|
|
1791
|
+
if (filesToCopyByCategory["commands"].length === 0) {
|
|
1792
|
+
filesSkipped += allFiles.length;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (selectedCategories.includes("agents")) {
|
|
1797
|
+
const sourceDir = path10.join(sourceAgentDir, "agents");
|
|
1798
|
+
if (fs8.existsSync(sourceDir)) {
|
|
1799
|
+
const allFiles = fs8.readdirSync(sourceDir);
|
|
1800
|
+
const fileSelection = await p4.multiselect({
|
|
1801
|
+
message: "Select agent files to copy:",
|
|
1802
|
+
options: allFiles.map((file) => ({
|
|
1803
|
+
value: file,
|
|
1804
|
+
label: file
|
|
1805
|
+
})),
|
|
1806
|
+
initialValues: allFiles,
|
|
1807
|
+
required: false
|
|
1808
|
+
});
|
|
1809
|
+
if (p4.isCancel(fileSelection)) {
|
|
1810
|
+
p4.cancel("Operation cancelled.");
|
|
1811
|
+
process.exit(0);
|
|
1812
|
+
}
|
|
1813
|
+
filesToCopyByCategory["agents"] = fileSelection;
|
|
1814
|
+
if (filesToCopyByCategory["agents"].length === 0) {
|
|
1815
|
+
filesSkipped += allFiles.length;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
if (selectedCategories.includes("skills")) {
|
|
1820
|
+
const sourceDir = path10.join(sourceAgentDir, "skills");
|
|
1821
|
+
if (fs8.existsSync(sourceDir)) {
|
|
1822
|
+
const allSkills = fs8.readdirSync(sourceDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
1823
|
+
if (allSkills.length > 0) {
|
|
1824
|
+
const skillSelection = await p4.multiselect({
|
|
1825
|
+
message: "Select skills to copy:",
|
|
1826
|
+
options: allSkills.map((skill) => ({
|
|
1827
|
+
value: skill,
|
|
1828
|
+
label: skill
|
|
1829
|
+
})),
|
|
1830
|
+
initialValues: allSkills,
|
|
1831
|
+
required: false
|
|
1832
|
+
});
|
|
1833
|
+
if (p4.isCancel(skillSelection)) {
|
|
1834
|
+
p4.cancel("Operation cancelled.");
|
|
1835
|
+
process.exit(0);
|
|
1836
|
+
}
|
|
1837
|
+
filesToCopyByCategory["skills"] = skillSelection;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
let maxThinkingTokens = options.maxThinkingTokens;
|
|
1843
|
+
if (!options.all && selectedCategories.includes("settings")) {
|
|
1844
|
+
if (maxThinkingTokens === void 0) {
|
|
1845
|
+
const tokensPrompt = await p4.text({
|
|
1846
|
+
message: "Maximum thinking tokens:",
|
|
1847
|
+
initialValue: "32000",
|
|
1848
|
+
validate: (value) => {
|
|
1849
|
+
const num = parseInt(value, 10);
|
|
1850
|
+
if (isNaN(num) || num < 1e3) {
|
|
1851
|
+
return "Please enter a valid number (minimum 1000)";
|
|
1852
|
+
}
|
|
1853
|
+
return void 0;
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
if (p4.isCancel(tokensPrompt)) {
|
|
1857
|
+
p4.cancel("Operation cancelled.");
|
|
1858
|
+
process.exit(0);
|
|
1859
|
+
}
|
|
1860
|
+
maxThinkingTokens = parseInt(tokensPrompt, 10);
|
|
1861
|
+
}
|
|
1862
|
+
} else if (selectedCategories.includes("settings")) {
|
|
1863
|
+
if (maxThinkingTokens === void 0) {
|
|
1864
|
+
maxThinkingTokens = 32e3;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
for (const category of selectedCategories) {
|
|
1868
|
+
if (category === "commands" || category === "agents") {
|
|
1869
|
+
const sourceDir = path10.join(sourceAgentDir, category);
|
|
1870
|
+
const targetCategoryDir = path10.join(agentTargetDir, category);
|
|
1871
|
+
if (!fs8.existsSync(sourceDir)) {
|
|
1872
|
+
p4.log.warn(`${category} directory not found in source, skipping`);
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
const allFiles = fs8.readdirSync(sourceDir);
|
|
1876
|
+
let filesToCopy = allFiles;
|
|
1877
|
+
if (!options.all && filesToCopyByCategory[category]) {
|
|
1878
|
+
filesToCopy = filesToCopyByCategory[category];
|
|
1879
|
+
}
|
|
1880
|
+
if (filesToCopy.length === 0) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
fs8.mkdirSync(targetCategoryDir, { recursive: true });
|
|
1884
|
+
for (const file of filesToCopy) {
|
|
1885
|
+
const sourcePath = path10.join(sourceDir, file);
|
|
1886
|
+
const targetPath = path10.join(targetCategoryDir, file);
|
|
1887
|
+
fs8.copyFileSync(sourcePath, targetPath);
|
|
1888
|
+
filesCopied++;
|
|
1889
|
+
}
|
|
1890
|
+
filesSkipped += allFiles.length - filesToCopy.length;
|
|
1891
|
+
p4.log.success(`Copied ${filesToCopy.length} ${category} file(s)`);
|
|
1892
|
+
} else if (category === "settings") {
|
|
1893
|
+
const settingsPath = path10.join(sourceAgentDir, "settings.template.json");
|
|
1894
|
+
const targetSettingsPath = path10.join(agentTargetDir, "settings.json");
|
|
1895
|
+
if (fs8.existsSync(settingsPath)) {
|
|
1896
|
+
const settingsContent = fs8.readFileSync(settingsPath, "utf8");
|
|
1897
|
+
const settings = JSON.parse(settingsContent);
|
|
1898
|
+
if (maxThinkingTokens !== void 0) {
|
|
1899
|
+
if (!settings.env) {
|
|
1900
|
+
settings.env = {};
|
|
1901
|
+
}
|
|
1902
|
+
settings.env.MAX_THINKING_TOKENS = maxThinkingTokens.toString();
|
|
1903
|
+
}
|
|
1904
|
+
if (!settings.env) {
|
|
1905
|
+
settings.env = {};
|
|
1906
|
+
}
|
|
1907
|
+
for (const [key, value] of Object.entries(product.defaultEnvVars)) {
|
|
1908
|
+
settings.env[key] = value;
|
|
1909
|
+
}
|
|
1910
|
+
fs8.writeFileSync(targetSettingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1911
|
+
filesCopied++;
|
|
1912
|
+
p4.log.success(`Copied settings.json (maxTokens: ${maxThinkingTokens})`);
|
|
1913
|
+
} else {
|
|
1914
|
+
p4.log.warn("settings.json not found in source, skipping");
|
|
1915
|
+
}
|
|
1916
|
+
} else if (category === "skills") {
|
|
1917
|
+
const sourceDir = path10.join(sourceAgentDir, "skills");
|
|
1918
|
+
const targetCategoryDir = path10.join(agentTargetDir, "skills");
|
|
1919
|
+
if (!fs8.existsSync(sourceDir)) {
|
|
1920
|
+
p4.log.warn("skills directory not found in source, skipping");
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
const allSkills = fs8.readdirSync(sourceDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
1924
|
+
let skillsToCopy = allSkills;
|
|
1925
|
+
if (!options.all && filesToCopyByCategory["skills"]) {
|
|
1926
|
+
skillsToCopy = filesToCopyByCategory["skills"];
|
|
1927
|
+
}
|
|
1928
|
+
if (skillsToCopy.length === 0) {
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
fs8.mkdirSync(targetCategoryDir, { recursive: true });
|
|
1932
|
+
let skillFilesCopied = 0;
|
|
1933
|
+
for (const skill of skillsToCopy) {
|
|
1934
|
+
const sourceSkillPath = path10.join(sourceDir, skill);
|
|
1935
|
+
const targetSkillPath = path10.join(targetCategoryDir, skill);
|
|
1936
|
+
skillFilesCopied += copyDirectoryRecursive(sourceSkillPath, targetSkillPath);
|
|
1937
|
+
}
|
|
1938
|
+
filesCopied += skillFilesCopied;
|
|
1939
|
+
filesSkipped += allSkills.length - skillsToCopy.length;
|
|
1940
|
+
p4.log.success(`Copied ${skillsToCopy.length} skill(s) (${skillFilesCopied} files)`);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
if (selectedCategories.includes("settings")) {
|
|
1944
|
+
ensureGitignoreEntry(targetDir, product.gitignoreEntry, product.name);
|
|
1945
|
+
p4.log.info("Updated .gitignore to exclude settings.local.json");
|
|
1946
|
+
}
|
|
1947
|
+
let message = `Successfully copied ${filesCopied} file(s) to ${agentTargetDir}`;
|
|
1948
|
+
if (filesSkipped > 0) {
|
|
1949
|
+
message += chalk11.gray(`
|
|
1950
|
+
Skipped ${filesSkipped} file(s)`);
|
|
1951
|
+
}
|
|
1952
|
+
message += chalk11.gray(`
|
|
1953
|
+
You can now use these commands in ${product.name}.`);
|
|
1954
|
+
p4.outro(message);
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
p4.log.error(`Error during ${product.name} init: ${error}`);
|
|
1957
|
+
process.exit(1);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// src/commands/agent/registry.ts
|
|
1962
|
+
var AGENT_PRODUCTS = {
|
|
1963
|
+
claude: {
|
|
1964
|
+
id: "claude",
|
|
1965
|
+
name: "Claude Code",
|
|
1966
|
+
dirName: ".claude",
|
|
1967
|
+
sourceDirName: "src/agent-assets",
|
|
1968
|
+
envVarPrefix: "CLAUDE",
|
|
1969
|
+
defaultEnvVars: {
|
|
1970
|
+
MAX_THINKING_TOKENS: "32000",
|
|
1971
|
+
CLAUDE_BASH_MAINTAIN_WORKING_DIR: "1"
|
|
1972
|
+
},
|
|
1973
|
+
gitignoreEntry: ".claude/settings.local.json"
|
|
1974
|
+
},
|
|
1975
|
+
codebuddy: {
|
|
1976
|
+
id: "codebuddy",
|
|
1977
|
+
name: "CodeBuddy Code",
|
|
1978
|
+
dirName: ".codebuddy",
|
|
1979
|
+
sourceDirName: "src/agent-assets",
|
|
1980
|
+
envVarPrefix: "CODEBUDDY",
|
|
1981
|
+
defaultEnvVars: {
|
|
1982
|
+
MAX_THINKING_TOKENS: "32000",
|
|
1983
|
+
// Note: Current not implemented in CodeBuddy
|
|
1984
|
+
CODEBUDDY_BASH_MAINTAIN_PROJECT_WORKING_DIR: "1"
|
|
1985
|
+
},
|
|
1986
|
+
gitignoreEntry: ".codebuddy/settings.local.json"
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
function getAgentProduct(id) {
|
|
1990
|
+
const product = AGENT_PRODUCTS[id];
|
|
1991
|
+
if (!product) {
|
|
1992
|
+
const validIds = Object.keys(AGENT_PRODUCTS).join(", ");
|
|
1993
|
+
throw new Error(`Unknown agent product: ${id}. Valid options: ${validIds}`);
|
|
1994
|
+
}
|
|
1995
|
+
return product;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/commands/agent.ts
|
|
1999
|
+
function agentCommand(program2) {
|
|
2000
|
+
const agent = program2.command("agent").description("Manage coding agent configuration");
|
|
2001
|
+
agent.command("init").description("Initialize coding agent configuration in current directory").option("--force", "Force overwrite of existing agent directory").option("--all", "Copy all files without prompting").option(
|
|
2002
|
+
"--max-thinking-tokens <number>",
|
|
2003
|
+
"Maximum thinking tokens (default: 32000)",
|
|
2004
|
+
(value) => parseInt(value, 10)
|
|
2005
|
+
).option("--name <name>", "Agent name to configure (claude|codebuddy)", "claude").action(async (options) => {
|
|
2006
|
+
const product = getAgentProduct(options.name);
|
|
2007
|
+
await agentInitCommand({ ...options, product });
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// src/commands/metadata/metadata.ts
|
|
2012
|
+
import { execSync as execSync6 } from "child_process";
|
|
2013
|
+
function getGitInfo() {
|
|
2014
|
+
try {
|
|
2015
|
+
execSync6("git rev-parse --is-inside-work-tree", {
|
|
2016
|
+
encoding: "utf8",
|
|
2017
|
+
stdio: "pipe"
|
|
2018
|
+
});
|
|
2019
|
+
const repoRoot = execSync6("git rev-parse --show-toplevel", {
|
|
2020
|
+
encoding: "utf8",
|
|
2021
|
+
stdio: "pipe"
|
|
2022
|
+
}).trim();
|
|
2023
|
+
const repoName = repoRoot.split("/").pop() || "";
|
|
2024
|
+
let branch = "";
|
|
2025
|
+
try {
|
|
2026
|
+
branch = execSync6("git branch --show-current", {
|
|
2027
|
+
encoding: "utf8",
|
|
2028
|
+
stdio: "pipe"
|
|
2029
|
+
}).trim();
|
|
2030
|
+
} catch {
|
|
2031
|
+
try {
|
|
2032
|
+
branch = execSync6("git rev-parse --abbrev-ref HEAD", {
|
|
2033
|
+
encoding: "utf8",
|
|
2034
|
+
stdio: "pipe"
|
|
2035
|
+
}).trim();
|
|
2036
|
+
} catch {
|
|
2037
|
+
branch = "";
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
let commit = "";
|
|
2041
|
+
try {
|
|
2042
|
+
commit = execSync6("git rev-parse HEAD", {
|
|
2043
|
+
encoding: "utf8",
|
|
2044
|
+
stdio: "pipe"
|
|
2045
|
+
}).trim();
|
|
2046
|
+
} catch {
|
|
2047
|
+
commit = "";
|
|
2048
|
+
}
|
|
2049
|
+
return { repoRoot, repoName, branch, commit };
|
|
2050
|
+
} catch {
|
|
2051
|
+
return null;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
function formatDate(date) {
|
|
2055
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
2056
|
+
const year = date.getFullYear();
|
|
2057
|
+
const month = pad(date.getMonth() + 1);
|
|
2058
|
+
const day = pad(date.getDate());
|
|
2059
|
+
const hours = pad(date.getHours());
|
|
2060
|
+
const minutes = pad(date.getMinutes());
|
|
2061
|
+
const seconds = pad(date.getSeconds());
|
|
2062
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2063
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${tz}`;
|
|
2064
|
+
}
|
|
2065
|
+
function formatFilenameTimestamp(date) {
|
|
2066
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
2067
|
+
const year = date.getFullYear();
|
|
2068
|
+
const month = pad(date.getMonth() + 1);
|
|
2069
|
+
const day = pad(date.getDate());
|
|
2070
|
+
const hours = pad(date.getHours());
|
|
2071
|
+
const minutes = pad(date.getMinutes());
|
|
2072
|
+
const seconds = pad(date.getSeconds());
|
|
2073
|
+
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
|
|
2074
|
+
}
|
|
2075
|
+
async function specMetadataCommand() {
|
|
2076
|
+
const now = /* @__PURE__ */ new Date();
|
|
2077
|
+
console.log(`Current Date/Time (TZ): ${formatDate(now)}`);
|
|
2078
|
+
const gitInfo = getGitInfo();
|
|
2079
|
+
if (gitInfo) {
|
|
2080
|
+
if (gitInfo.commit) {
|
|
2081
|
+
console.log(`Current Git Commit Hash: ${gitInfo.commit}`);
|
|
2082
|
+
}
|
|
2083
|
+
if (gitInfo.branch) {
|
|
2084
|
+
console.log(`Current Branch Name: ${gitInfo.branch}`);
|
|
2085
|
+
}
|
|
2086
|
+
if (gitInfo.repoName) {
|
|
2087
|
+
console.log(`Repository Name: ${gitInfo.repoName}`);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
console.log(`Timestamp For Filename: ${formatFilenameTimestamp(now)}`);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// src/commands/metadata.ts
|
|
2094
|
+
function metadataCommand(program2) {
|
|
2095
|
+
const metadata = program2.description("Metadata utilities for current repository");
|
|
2096
|
+
metadata.command("metadata").description("Output metadata for current repository (branch, commit, timestamp, etc.)").action(specMetadataCommand);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// src/index.ts
|
|
2100
|
+
import dotenv2 from "dotenv";
|
|
2101
|
+
import { createRequire } from "module";
|
|
2102
|
+
dotenv2.config();
|
|
2103
|
+
var require2 = createRequire(import.meta.url);
|
|
2104
|
+
var { version } = require2("../package.json");
|
|
2105
|
+
var program = new Command();
|
|
2106
|
+
program.name("thoughtcabinet").description(
|
|
2107
|
+
"Thought Cabinet (thc) - thoughts management CLI for developer notes and documentation"
|
|
2108
|
+
).version(version);
|
|
2109
|
+
thoughtsCommand(program);
|
|
2110
|
+
agentCommand(program);
|
|
2111
|
+
metadataCommand(program);
|
|
2112
|
+
program.parse(process.argv);
|
|
2113
|
+
//# sourceMappingURL=index.js.map
|