thufir 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.js +52 -1
- package/dist/scaffold.js +65 -2
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
|
-
import { writeFileSync } from 'node:fs';
|
|
4
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { showVersionCheck } from '../version-check.js';
|
|
7
7
|
import { analyzeRepo } from '../analyze.js';
|
|
@@ -162,6 +162,57 @@ export async function runInit() {
|
|
|
162
162
|
const result = scaffold(cwd, answers, analysis);
|
|
163
163
|
createInitialCommit(cwd, result.filesCreated, answers.colleagueName);
|
|
164
164
|
s.stop(`${answers.colleagueName} is ready.`);
|
|
165
|
+
// Install skill packages
|
|
166
|
+
if (answers.skillPackages.length > 0) {
|
|
167
|
+
const skillSpinner = p.spinner();
|
|
168
|
+
for (const pkg of answers.skillPackages) {
|
|
169
|
+
if (pkg === 'gstack') {
|
|
170
|
+
const gstackPath = join(process.env.HOME || '~', '.claude', 'skills', 'gstack');
|
|
171
|
+
if (existsSync(gstackPath)) {
|
|
172
|
+
p.log.info('gstack already installed at ~/.claude/skills/gstack');
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
skillSpinner.start('Installing gstack (headless browser, QA, shipping)...');
|
|
176
|
+
try {
|
|
177
|
+
execSync('git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack', { stdio: 'ignore', timeout: 60000 });
|
|
178
|
+
// Run setup if bun is available
|
|
179
|
+
try {
|
|
180
|
+
execSync('cd ~/.claude/skills/gstack && ./setup', { stdio: 'ignore', timeout: 30000 });
|
|
181
|
+
}
|
|
182
|
+
catch { /* setup is optional — browse binary may not build without bun */ }
|
|
183
|
+
skillSpinner.stop('gstack installed.');
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
skillSpinner.stop('gstack installation failed.');
|
|
187
|
+
p.log.warn('Install manually: git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (pkg === 'superpowers') {
|
|
192
|
+
const skillsDir = join(process.env.HOME || '~', '.claude', 'skills');
|
|
193
|
+
const spSkills = ['superpowers-brainstorming', 'superpowers-systematic-debugging', 'superpowers-test-driven-development'];
|
|
194
|
+
const installed = spSkills.filter(s => existsSync(join(skillsDir, s)));
|
|
195
|
+
if (installed.length >= spSkills.length) {
|
|
196
|
+
p.log.info(`Superpowers already installed (${installed.length} skills detected)`);
|
|
197
|
+
}
|
|
198
|
+
else if (installed.length > 0) {
|
|
199
|
+
p.log.info(`Superpowers partially installed (${installed.length}/${spSkills.length} core skills)`);
|
|
200
|
+
p.log.warn('Run `claude /install-skills superpowers` to install missing skills');
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Try installing via superpowers-extended-cc if available
|
|
204
|
+
const extPath = join(skillsDir, 'superpowers-extended-cc');
|
|
205
|
+
if (existsSync(extPath)) {
|
|
206
|
+
p.log.info('Superpowers Extended (CC) detected — skills available');
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
p.log.warn('Superpowers not found. Install via Claude Code:');
|
|
210
|
+
console.log(pc.dim(' Open Claude Code and run: /install superpowers'));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
165
216
|
// GitHub setup
|
|
166
217
|
if (answers.setupGitHub !== 'skip' && analysis.hasGhCli) {
|
|
167
218
|
setupGitHub(cwd, answers.setupGitHub, answers.repoName);
|
package/dist/scaffold.js
CHANGED
|
@@ -156,7 +156,55 @@ const THUFIR_DOCS = [
|
|
|
156
156
|
'plans/_meta.md',
|
|
157
157
|
'workflows/_meta.md',
|
|
158
158
|
'workflows/getting-started.md',
|
|
159
|
+
'entities/_meta.md',
|
|
159
160
|
];
|
|
161
|
+
/**
|
|
162
|
+
* Merge existing AGENTS.md with new template.
|
|
163
|
+
* Strategy: Use the new template as the base, then append any sections
|
|
164
|
+
* from the existing file that don't exist in the new template.
|
|
165
|
+
* This preserves user-added sections (like "Data Querying") while
|
|
166
|
+
* updating Thufir-managed sections (personality, triggers, etc.)
|
|
167
|
+
*/
|
|
168
|
+
function mergeAgentsMd(existing, fresh) {
|
|
169
|
+
// Extract H2 sections from both
|
|
170
|
+
const sectionPattern = /^## .+$/gm;
|
|
171
|
+
const extractSections = (content) => {
|
|
172
|
+
const sections = new Map();
|
|
173
|
+
const lines = content.split('\n');
|
|
174
|
+
let currentHeading = '__preamble__';
|
|
175
|
+
let currentContent = [];
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
if (line.match(/^## /)) {
|
|
178
|
+
if (currentContent.length > 0) {
|
|
179
|
+
sections.set(currentHeading, currentContent.join('\n').trim());
|
|
180
|
+
}
|
|
181
|
+
currentHeading = line.trim();
|
|
182
|
+
currentContent = [line];
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
currentContent.push(line);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (currentContent.length > 0) {
|
|
189
|
+
sections.set(currentHeading, currentContent.join('\n').trim());
|
|
190
|
+
}
|
|
191
|
+
return sections;
|
|
192
|
+
};
|
|
193
|
+
const existingSections = extractSections(existing);
|
|
194
|
+
const freshSections = extractSections(fresh);
|
|
195
|
+
// Start with the fresh template
|
|
196
|
+
let result = fresh.trimEnd();
|
|
197
|
+
// Append any sections from existing that aren't in fresh
|
|
198
|
+
for (const [heading, content] of existingSections) {
|
|
199
|
+
if (heading === '__preamble__')
|
|
200
|
+
continue; // Skip preamble — use fresh version
|
|
201
|
+
if (!freshSections.has(heading)) {
|
|
202
|
+
// This is a user-added section — preserve it
|
|
203
|
+
result += '\n\n' + content;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return result + '\n';
|
|
207
|
+
}
|
|
160
208
|
export function scaffold(cwd, answers, analysis, isRealign = false) {
|
|
161
209
|
const files = [];
|
|
162
210
|
let gitInitialized = false;
|
|
@@ -166,7 +214,22 @@ export function scaffold(cwd, answers, analysis, isRealign = false) {
|
|
|
166
214
|
}
|
|
167
215
|
// AGENTS.md
|
|
168
216
|
const agentsTemplate = readTemplate('agents/base.md');
|
|
169
|
-
|
|
217
|
+
const newAgents = substitute(agentsTemplate, answers);
|
|
218
|
+
if (isRealign) {
|
|
219
|
+
// Merge: keep user-added sections from existing AGENTS.md
|
|
220
|
+
const existingPath = join(cwd, 'AGENTS.md');
|
|
221
|
+
if (existsSync(existingPath)) {
|
|
222
|
+
const existing = readFileSync(existingPath, 'utf-8');
|
|
223
|
+
const merged = mergeAgentsMd(existing, newAgents);
|
|
224
|
+
writeFile(cwd, 'AGENTS.md', merged);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
writeFile(cwd, 'AGENTS.md', newAgents);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
writeFile(cwd, 'AGENTS.md', newAgents);
|
|
232
|
+
}
|
|
170
233
|
files.push('AGENTS.md');
|
|
171
234
|
// CLAUDE.md
|
|
172
235
|
writeFile(cwd, 'CLAUDE.md', readTemplate('claude-md.md'));
|
|
@@ -220,7 +283,7 @@ export function scaffold(cwd, answers, analysis, isRealign = false) {
|
|
|
220
283
|
// .thufir marker — full settings
|
|
221
284
|
const date = new Date().toISOString().split('T')[0];
|
|
222
285
|
const marker = JSON.stringify({
|
|
223
|
-
version: '1.
|
|
286
|
+
version: '2.1.0',
|
|
224
287
|
initialized: isRealign ? answers._originalInitialized || date : date,
|
|
225
288
|
last_realigned: date,
|
|
226
289
|
colleague_name: answers.colleagueName,
|