thufir 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,352 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from 'node:fs';
2
+ import { join, dirname, basename } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { execSync } from 'node:child_process';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ function getTemplatesDir() {
7
+ const distTemplates = join(__dirname, 'templates');
8
+ if (existsSync(distTemplates))
9
+ return distTemplates;
10
+ return join(__dirname, '..', 'src', 'templates');
11
+ }
12
+ function readTemplate(relativePath) {
13
+ return readFileSync(join(getTemplatesDir(), relativePath), 'utf-8');
14
+ }
15
+ const TRAIT_FILES = {
16
+ 'mentat-core': 'traits/mentat-core.md',
17
+ 'consultant': 'traits/consultant.md',
18
+ 'psychology': 'traits/psychology.md',
19
+ // Legacy trait IDs (map to new packages for backward compat)
20
+ 'thufir': 'traits/mentat-core.md',
21
+ 'first-principles': 'traits/mentat-core.md',
22
+ 'mbb': 'traits/consultant.md',
23
+ 'zelazny': 'traits/consultant.md',
24
+ 'top-down': 'traits/consultant.md',
25
+ 'compassion': 'traits/psychology.md',
26
+ 'systemic': 'traits/psychology.md',
27
+ 'parts': 'traits/psychology.md',
28
+ };
29
+ const SKILL_FILES = {
30
+ 'superpowers': 'skills/superpowers.md',
31
+ 'gstack': 'skills/gstack.md',
32
+ };
33
+ const TRIGGER_LINES = {
34
+ 'brainstorming': { situation: 'Creative work starting — new feature, component, or behavior change', skill: 'Brainstorming', how: 'Use the brainstorming skill before writing any code' },
35
+ 'plans': { situation: 'Multi-step task with a spec or requirements', skill: 'Implementation Plans', how: 'Create a detailed implementation plan before coding' },
36
+ 'ceo-review': { situation: 'Reviewing product requirements or finding better solutions', skill: 'CEO/Product Review', how: '`/plan-ceo-review` — founder-mode review, finds the 10-star product' },
37
+ 'eng-review': { situation: 'Locking architecture, mapping edge cases', skill: 'Engineering Review', how: '`/plan-eng-review` — architecture and technical planning' },
38
+ 'tdd': { situation: 'Implementing a feature or bugfix', skill: 'Test-Driven Development', how: 'Write failing tests first, then implement' },
39
+ 'code-review': { situation: 'Completing a task or receiving feedback', skill: 'Code Review', how: 'Review work against requirements; verify feedback before implementing' },
40
+ 'verification': { situation: 'About to claim work is complete', skill: 'Verification', how: 'Run verification commands, confirm output before claiming success' },
41
+ 'debugging': { situation: 'Bug, test failure, or unexpected behavior', skill: 'Systematic Debugging', how: "Follow the debugging skill's scientific method" },
42
+ 'browse': { situation: 'Need to test a frontend feature or verify a deployment', skill: 'Browser Automation', how: '`/gstack` or `/browse` — opens persistent headless browser' },
43
+ 'qa': { situation: 'Frontend feature complete, needs systematic testing', skill: 'QA Testing', how: '`/qa` — tests app, finds bugs, fixes them, re-verifies' },
44
+ 'design-review': { situation: 'Design audit needed', skill: 'Design Audit', how: '`/plan-design-review` — 80-item design audit' },
45
+ 'shipping': { situation: 'Ready to create a PR and ship', skill: 'Shipping', how: '`/ship` — runs tests, resolves comments, pushes, opens PR' },
46
+ 'parallel': { situation: '2+ independent tasks that can run in parallel', skill: 'Parallel Agents', how: 'Use subagents for independent work streams' },
47
+ 'retro': { situation: 'Want a development retrospective', skill: 'Retrospective', how: '`/retro` — analyzes commit history, work patterns, per-person metrics' },
48
+ };
49
+ function buildTraitsSection(selectedTraits) {
50
+ // Always include mentat-core
51
+ const allTraits = ['mentat-core', ...selectedTraits.filter(t => t !== 'mentat-core')];
52
+ // Deduplicate file paths (legacy IDs may map to same file)
53
+ const seen = new Set();
54
+ const traitContents = allTraits
55
+ .filter((t) => t in TRAIT_FILES)
56
+ .filter((t) => {
57
+ const file = TRAIT_FILES[t];
58
+ if (seen.has(file))
59
+ return false;
60
+ seen.add(file);
61
+ return true;
62
+ })
63
+ .map((t) => readTemplate(TRAIT_FILES[t]).trim())
64
+ .join('\n\n');
65
+ if (!traitContents)
66
+ return '';
67
+ return `---
68
+
69
+ ## Personality & Thinking
70
+
71
+ These traits guide how your AI colleague thinks and communicates.
72
+
73
+ ${traitContents}
74
+
75
+ `;
76
+ }
77
+ function buildSkillTriggersSection(selectedPackages, selectedTriggers) {
78
+ if (!selectedPackages || selectedPackages.length === 0)
79
+ return '';
80
+ if (!selectedTriggers || selectedTriggers.length === 0)
81
+ return '';
82
+ const hasSP = selectedPackages.includes('superpowers');
83
+ const hasGS = selectedPackages.includes('gstack');
84
+ // Categorize triggers
85
+ const planningTriggers = ['brainstorming', 'plans', 'ceo-review', 'eng-review'].filter(t => selectedTriggers.includes(t));
86
+ const qualityTriggers = ['tdd', 'code-review', 'verification', 'debugging'].filter(t => selectedTriggers.includes(t));
87
+ const browserTriggers = ['browse', 'qa', 'design-review'].filter(t => selectedTriggers.includes(t));
88
+ const workflowTriggers = ['shipping', 'parallel', 'retro'].filter(t => selectedTriggers.includes(t));
89
+ // Filter to only triggers relevant to selected packages
90
+ // superpowers: brainstorming, plans, tdd, code-review, verification, debugging, parallel
91
+ // gstack: ceo-review, eng-review, browse, qa, design-review, shipping, retro
92
+ // Some triggers are useful with either package
93
+ const spTriggers = new Set(['brainstorming', 'plans', 'tdd', 'code-review', 'verification', 'debugging', 'parallel']);
94
+ const gsTriggers = new Set(['ceo-review', 'eng-review', 'browse', 'qa', 'design-review', 'shipping', 'retro']);
95
+ const isRelevant = (t) => (hasSP && spTriggers.has(t)) || (hasGS && gsTriggers.has(t));
96
+ let content = '---\n\n## Skill Invocation Guide\n\nWhen these situations arise, invoke the relevant skill:\n\n';
97
+ const sections = [
98
+ { name: 'Planning & Strategy', triggers: planningTriggers.filter(isRelevant) },
99
+ { name: 'Code Quality', triggers: qualityTriggers.filter(isRelevant) },
100
+ { name: 'Browser & Testing', triggers: browserTriggers.filter(isRelevant) },
101
+ { name: 'Workflow', triggers: workflowTriggers.filter(isRelevant) },
102
+ ];
103
+ let hasContent = false;
104
+ for (const section of sections) {
105
+ if (section.triggers.length === 0)
106
+ continue;
107
+ hasContent = true;
108
+ content += `### ${section.name}\n\n`;
109
+ content += '| Situation | Skill to invoke | How |\n';
110
+ content += '|-----------|----------------|-----|\n';
111
+ for (const id of section.triggers) {
112
+ const t = TRIGGER_LINES[id];
113
+ if (t)
114
+ content += `| ${t.situation} | ${t.skill} | ${t.how} |\n`;
115
+ }
116
+ content += '\n';
117
+ }
118
+ if (!hasContent)
119
+ return '';
120
+ if (hasGS) {
121
+ content += '**Note:** gstack requires Bun. Run: `git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup`\n\n';
122
+ }
123
+ return content;
124
+ }
125
+ function substitute(content, answers) {
126
+ const date = new Date().toISOString().split('T')[0];
127
+ const traitsSection = buildTraitsSection(answers.traits);
128
+ const skillTriggersSection = buildSkillTriggersSection(answers.skillPackages, answers.skillTriggers);
129
+ return content
130
+ .replace(/\{\{colleague_name\}\}/g, answers.colleagueName)
131
+ .replace(/\{\{user_name\}\}/g, answers.userName)
132
+ .replace(/\{\{project_description\}\}/g, answers.projectDescription)
133
+ .replace(/\{\{project_type\}\}/g, answers.projectFocus)
134
+ .replace(/\{\{traits_section\}\}/g, traitsSection)
135
+ .replace(/\{\{skill_triggers_section\}\}/g, skillTriggersSection)
136
+ .replace(/\{\{created_date\}\}/g, date);
137
+ }
138
+ function writeFile(cwd, relativePath, content) {
139
+ const fullPath = join(cwd, relativePath);
140
+ mkdirSync(dirname(fullPath), { recursive: true });
141
+ writeFileSync(fullPath, content, 'utf-8');
142
+ }
143
+ /** Files that are always Thufir-managed and safe to overwrite */
144
+ const THUFIR_MANAGED_FILES = ['AGENTS.md', 'CLAUDE.md', '.cursorrules'];
145
+ /** Doc files that should be skipped during realign if they already exist (user content) */
146
+ const USER_CONTENT_DOCS = [
147
+ 'business/overview.md',
148
+ 'business/audience.md',
149
+ 'decisions/001-project-kickoff.md',
150
+ ];
151
+ /** Doc files that are always regenerated (indexes, Thufir content) */
152
+ const THUFIR_DOCS = [
153
+ '_meta.md',
154
+ 'business/_meta.md',
155
+ 'decisions/_meta.md',
156
+ 'plans/_meta.md',
157
+ 'workflows/_meta.md',
158
+ 'workflows/getting-started.md',
159
+ ];
160
+ export function scaffold(cwd, answers, analysis, isRealign = false) {
161
+ const files = [];
162
+ let gitInitialized = false;
163
+ if (!analysis.isGitRepo) {
164
+ execSync('git init', { cwd, stdio: 'ignore' });
165
+ gitInitialized = true;
166
+ }
167
+ // AGENTS.md
168
+ const agentsTemplate = readTemplate('agents/base.md');
169
+ writeFile(cwd, 'AGENTS.md', substitute(agentsTemplate, answers));
170
+ files.push('AGENTS.md');
171
+ // CLAUDE.md
172
+ writeFile(cwd, 'CLAUDE.md', readTemplate('claude-md.md'));
173
+ files.push('CLAUDE.md');
174
+ // .cursorrules
175
+ writeFile(cwd, '.cursorrules', readTemplate('cursorrules.md'));
176
+ files.push('.cursorrules');
177
+ // .gitignore
178
+ const gitignorePath = join(cwd, '.gitignore');
179
+ const gitignoreContent = readTemplate('gitignore.md');
180
+ if (existsSync(gitignorePath)) {
181
+ const existing = readFileSync(gitignorePath, 'utf-8');
182
+ if (!existing.includes('# Thufir defaults')) {
183
+ appendFileSync(gitignorePath, '\n# Thufir defaults\n' + gitignoreContent + '\n');
184
+ }
185
+ // Ensure .thufir.local is gitignored
186
+ if (!existing.includes('.thufir.local')) {
187
+ appendFileSync(gitignorePath, '\n# Thufir local config (tokens)\n.thufir.local\n');
188
+ }
189
+ }
190
+ else {
191
+ writeFile(cwd, '.gitignore', gitignoreContent + '\n\n# Thufir local config (tokens)\n.thufir.local\n');
192
+ }
193
+ files.push('.gitignore');
194
+ // docs/ structure
195
+ if (isRealign) {
196
+ // During realign: always regenerate indexes and Thufir content
197
+ for (const docFile of THUFIR_DOCS) {
198
+ const template = readTemplate('docs/' + docFile);
199
+ writeFile(cwd, 'docs/' + docFile, substitute(template, answers));
200
+ files.push('docs/' + docFile);
201
+ }
202
+ // Only create user content docs if they don't already exist
203
+ for (const docFile of USER_CONTENT_DOCS) {
204
+ if (!existsSync(join(cwd, 'docs', docFile))) {
205
+ const template = readTemplate('docs/' + docFile);
206
+ writeFile(cwd, 'docs/' + docFile, substitute(template, answers));
207
+ files.push('docs/' + docFile);
208
+ }
209
+ }
210
+ }
211
+ else {
212
+ // Fresh install: create everything
213
+ const allDocFiles = [...THUFIR_DOCS, ...USER_CONTENT_DOCS];
214
+ for (const docFile of allDocFiles) {
215
+ const template = readTemplate('docs/' + docFile);
216
+ writeFile(cwd, 'docs/' + docFile, substitute(template, answers));
217
+ files.push('docs/' + docFile);
218
+ }
219
+ }
220
+ // .thufir marker — full settings
221
+ const date = new Date().toISOString().split('T')[0];
222
+ const marker = JSON.stringify({
223
+ version: '1.3.0',
224
+ initialized: isRealign ? answers._originalInitialized || date : date,
225
+ last_realigned: date,
226
+ colleague_name: answers.colleagueName,
227
+ user_name: answers.userName,
228
+ project_mode: answers.projectMode,
229
+ project_focus: answers.projectFocus,
230
+ project_description: answers.projectDescription,
231
+ traits: answers.traits,
232
+ skillPackages: answers.skillPackages,
233
+ skillTriggers: answers.skillTriggers,
234
+ }, null, 2);
235
+ writeFile(cwd, '.thufir', marker);
236
+ files.push('.thufir');
237
+ // .thufir.local — token storage (not committed)
238
+ const localConfig = JSON.stringify({ token: answers.token }, null, 2);
239
+ writeFile(cwd, '.thufir.local', localConfig);
240
+ return { filesCreated: files, gitInitialized };
241
+ }
242
+ export function backupAgentFiles(cwd) {
243
+ const backupDir = join(cwd, '.thufir-backup');
244
+ mkdirSync(backupDir, { recursive: true });
245
+ const filesToBackup = ['AGENTS.md', 'CLAUDE.md', '.cursorrules', '.cursor/rules/onboarding.mdc', '.cursor/rules/documentation.mdc', '.cursor/rules/project-context.mdc'];
246
+ const backedUp = [];
247
+ for (const file of filesToBackup) {
248
+ const src = join(cwd, file);
249
+ if (existsSync(src)) {
250
+ const dest = join(backupDir, file.replace(/\//g, '__'));
251
+ const content = readFileSync(src, 'utf-8');
252
+ writeFileSync(dest, content, 'utf-8');
253
+ backedUp.push(file);
254
+ }
255
+ }
256
+ // Also backup any existing docs/_meta.md files
257
+ const docsDir = join(cwd, 'docs');
258
+ if (existsSync(docsDir)) {
259
+ try {
260
+ const metaContent = readFileSync(join(docsDir, '_meta.md'), 'utf-8');
261
+ writeFileSync(join(backupDir, 'docs___meta.md'), metaContent, 'utf-8');
262
+ backedUp.push('docs/_meta.md');
263
+ }
264
+ catch { }
265
+ }
266
+ return backedUp;
267
+ }
268
+ export function generateMergePrompt(cwd, answers, backedUpFiles) {
269
+ const backupDir = join(cwd, '.thufir-backup');
270
+ // Read all backup contents to include in the prompt
271
+ let backupSummary = '';
272
+ for (const file of backedUpFiles) {
273
+ const backupPath = join(backupDir, file.replace(/\//g, '__'));
274
+ if (existsSync(backupPath)) {
275
+ const content = readFileSync(backupPath, 'utf-8');
276
+ backupSummary += `\n### Original ${file}\n\`\`\`\n${content.slice(0, 3000)}\n\`\`\`\n`;
277
+ }
278
+ }
279
+ return `# Thufir Merge — Reorganization Plan
280
+
281
+ You are helping merge Thufir conventions into this existing repository. Thufir has already:
282
+ 1. Backed up the original agent files to \`.thufir-backup/\`
283
+ 2. Written new Thufir-managed files (AGENTS.md, CLAUDE.md, .cursorrules)
284
+ 3. Created the docs/ structure with entity system
285
+
286
+ ## Your Task
287
+
288
+ Review the backed-up original files and merge their intentions into the new Thufir structure. The original files contain valuable context that should NOT be lost.
289
+
290
+ ## Backed-Up Original Files
291
+ ${backupSummary}
292
+
293
+ ## What to Do
294
+
295
+ 1. **Read the new AGENTS.md** that Thufir just created. It has the standard structure.
296
+
297
+ 2. **Read each backed-up file** in \`.thufir-backup/\`. Extract:
298
+ - Custom instructions and rules the user had
299
+ - Project-specific context (tech stack, conventions, preferences)
300
+ - Any documented workflows or procedures
301
+ - References to specific files, APIs, or tools
302
+
303
+ 3. **Merge extracted content into the new structure:**
304
+ - Custom instructions → Add to AGENTS.md in the appropriate section (e.g., "When the Project Needs Code" for tech stack info)
305
+ - Workflows → Create files in \`docs/workflows/\`
306
+ - Project context → Update \`docs/business/overview.md\`
307
+ - Decision-like content → Create decision records in \`docs/decisions/\`
308
+
309
+ 4. **Preserve the Thufir structure.** The new AGENTS.md sections must stay intact. Add custom content WITHIN sections, don't restructure.
310
+
311
+ 5. **Minimal reorganization.** Keep things where they make sense. Don't move files unless they clearly belong elsewhere.
312
+
313
+ ## Key Principle
314
+
315
+ The user's original agent files had intentions — they wrote those instructions for a reason. Every instruction should find a home in the new structure. Nothing should be lost.
316
+
317
+ ## Colleague Details
318
+ - Name: ${answers.colleagueName}
319
+ - User: ${answers.userName}
320
+ - Project: ${answers.projectDescription}
321
+ - Focus: ${answers.projectFocus}
322
+
323
+ ## When Done
324
+
325
+ After merging, check:
326
+ - [ ] All original custom instructions are preserved somewhere in the new structure
327
+ - [ ] AGENTS.md is coherent and not duplicated
328
+ - [ ] docs/ structure has _meta.md indexes updated
329
+ - [ ] No files were deleted (only reorganized)
330
+
331
+ Commit your changes with: \`git add -A && git commit -m "feat: merge Thufir conventions into existing repo"\`
332
+ `;
333
+ }
334
+ export function createInitialCommit(cwd, files, colleagueName) {
335
+ for (const file of files) {
336
+ execSync('git add "' + file + '"', { cwd, stdio: 'ignore' });
337
+ }
338
+ execSync('git commit -m "feat: initialize ' + colleagueName + ' AI colleague"', { cwd, stdio: 'ignore' });
339
+ }
340
+ export function setupGitHub(cwd, mode, repoName) {
341
+ try {
342
+ if (mode === 'create') {
343
+ const name = repoName || basename(cwd);
344
+ execSync('gh repo create ' + name + ' --private --source=. --push', { cwd, stdio: 'inherit' });
345
+ return true;
346
+ }
347
+ return false;
348
+ }
349
+ catch {
350
+ return false;
351
+ }
352
+ }