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,28 @@
1
+ export interface ThufirSettings {
2
+ version: string;
3
+ initialized: string;
4
+ last_realigned: string;
5
+ colleague_name: string;
6
+ user_name: string;
7
+ project_mode: 'single' | 'multi';
8
+ project_focus: 'code' | 'planning' | 'hybrid';
9
+ project_description: string;
10
+ traits: string[];
11
+ skillPackages?: string[];
12
+ skillTriggers?: string[];
13
+ }
14
+ export interface RepoAnalysis {
15
+ isGitRepo: boolean;
16
+ hasAgentsMd: boolean;
17
+ hasClaudeMd: boolean;
18
+ hasCursorRules: boolean;
19
+ hasDocsDir: boolean;
20
+ hasSkillsDir: boolean;
21
+ hasGitHubRemote: boolean;
22
+ gitHubRemoteUrl: string | null;
23
+ hasGhCli: boolean;
24
+ hasThufirMarker: boolean;
25
+ thufirSettings: ThufirSettings | null;
26
+ savedToken: string | null;
27
+ }
28
+ export declare function analyzeRepo(cwd: string): RepoAnalysis;
@@ -0,0 +1,62 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ function commandExists(cmd) {
5
+ try {
6
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ function getGitRemote(cwd) {
14
+ try {
15
+ const output = execSync('git remote get-url origin', { encoding: 'utf-8', cwd, stdio: ['pipe', 'pipe', 'ignore'] }).trim();
16
+ return output || null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function analyzeRepo(cwd) {
23
+ let isGitRepo = false;
24
+ try {
25
+ execSync('git rev-parse --git-dir', { cwd, stdio: 'ignore' });
26
+ isGitRepo = true;
27
+ }
28
+ catch { /* not a git repo */ }
29
+ const remote = isGitRepo ? getGitRemote(cwd) : null;
30
+ const hasThufirMarker = existsSync(join(cwd, '.thufir'));
31
+ let thufirSettings = null;
32
+ if (hasThufirMarker) {
33
+ try {
34
+ thufirSettings = JSON.parse(readFileSync(join(cwd, '.thufir'), 'utf-8'));
35
+ }
36
+ catch { /* corrupt or unreadable */ }
37
+ }
38
+ let savedToken = null;
39
+ const localPath = join(cwd, '.thufir.local');
40
+ if (existsSync(localPath)) {
41
+ try {
42
+ const local = JSON.parse(readFileSync(localPath, 'utf-8'));
43
+ if (local.token)
44
+ savedToken = local.token;
45
+ }
46
+ catch { /* ignore */ }
47
+ }
48
+ return {
49
+ isGitRepo,
50
+ hasAgentsMd: existsSync(join(cwd, 'AGENTS.md')),
51
+ hasClaudeMd: existsSync(join(cwd, 'CLAUDE.md')),
52
+ hasCursorRules: existsSync(join(cwd, '.cursorrules')),
53
+ hasDocsDir: existsSync(join(cwd, 'docs')),
54
+ hasSkillsDir: existsSync(join(cwd, '.claude', 'skills')),
55
+ hasGitHubRemote: remote !== null,
56
+ gitHubRemoteUrl: remote,
57
+ hasGhCli: commandExists('gh'),
58
+ hasThufirMarker,
59
+ thufirSettings,
60
+ savedToken,
61
+ };
62
+ }
package/dist/api.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ interface ValidateResponse {
2
+ valid: boolean;
3
+ owner?: string;
4
+ projects_remaining?: number;
5
+ expires?: string;
6
+ error?: string;
7
+ }
8
+ export declare function validateToken(token: string): Promise<ValidateResponse>;
9
+ export declare function reportUsage(token: string, repoId: string): Promise<void>;
10
+ export declare function checkVersion(currentVersion: string): Promise<{
11
+ updateAvailable: boolean;
12
+ latest: string;
13
+ changelog: string;
14
+ }>;
15
+ export {};
package/dist/api.js ADDED
@@ -0,0 +1,30 @@
1
+ const API_BASE = 'https://thufir-api.krisztian-596.workers.dev';
2
+ export async function validateToken(token) {
3
+ const res = await fetch(`${API_BASE}/validate`, {
4
+ method: 'POST',
5
+ headers: { 'Content-Type': 'application/json' },
6
+ body: JSON.stringify({ token }),
7
+ });
8
+ return res.json();
9
+ }
10
+ export async function reportUsage(token, repoId) {
11
+ await fetch(`${API_BASE}/use`, {
12
+ method: 'POST',
13
+ headers: { 'Content-Type': 'application/json' },
14
+ body: JSON.stringify({ token, repo_id: repoId }),
15
+ });
16
+ }
17
+ export async function checkVersion(currentVersion) {
18
+ try {
19
+ const res = await fetch(`${API_BASE}/version`);
20
+ const data = await res.json();
21
+ return {
22
+ updateAvailable: data.latest !== currentVersion,
23
+ latest: data.latest,
24
+ changelog: data.changelog,
25
+ };
26
+ }
27
+ catch {
28
+ return { updateAvailable: false, latest: currentVersion, changelog: '' };
29
+ }
30
+ }
@@ -0,0 +1 @@
1
+ export declare function runInit(): Promise<void>;
@@ -0,0 +1,183 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { execSync } from 'node:child_process';
4
+ import { writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { showVersionCheck } from '../version-check.js';
7
+ import { analyzeRepo } from '../analyze.js';
8
+ import { runPrompts, runRealignPrompts, runMergePrompts } from '../prompts.js';
9
+ import { printCompleteScreen, printGetStarted, printCommitScreen } from '../ui/ascii.js';
10
+ import { scaffold, createInitialCommit, setupGitHub, backupAgentFiles, generateMergePrompt } from '../scaffold.js';
11
+ import { reportUsage } from '../api.js';
12
+ function commandExists(cmd) {
13
+ try {
14
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ export async function runInit() {
22
+ const cwd = process.cwd();
23
+ // Version check (non-blocking)
24
+ await showVersionCheck();
25
+ // Analyze repo first
26
+ const analysis = analyzeRepo(cwd);
27
+ // --- Mode B: Realign (.thufir exists) ---
28
+ if (analysis.hasThufirMarker && analysis.thufirSettings) {
29
+ const result = await runRealignPrompts(analysis.thufirSettings, analysis.savedToken);
30
+ if (!result) {
31
+ console.log(pc.dim(' Cancelled.'));
32
+ process.exit(0);
33
+ }
34
+ // Commit screen
35
+ printCommitScreen(result.answers.colleagueName, result.action === 'reconfigure' ? 'reconfigure' : 'realign');
36
+ const commitChoice = await p.select({
37
+ message: pc.yellow(pc.bold('Execute the command?')),
38
+ options: [
39
+ { value: 'execute', label: '⚔ Execute', hint: 'commit to this action' },
40
+ { value: 'abort', label: '↩ Abort', hint: 'return without changes' },
41
+ ],
42
+ });
43
+ if (p.isCancel(commitChoice) || commitChoice === 'abort') {
44
+ console.log(pc.dim(' No changes made.'));
45
+ process.exit(0);
46
+ }
47
+ // Scaffold in realign mode
48
+ const s = p.spinner();
49
+ s.start(`Realigning ${result.answers.colleagueName}...`);
50
+ // Preserve original initialized date
51
+ result.answers._originalInitialized = analysis.thufirSettings.initialized;
52
+ const scaffoldResult = scaffold(cwd, result.answers, analysis, true);
53
+ s.stop(`${result.answers.colleagueName} realigned.`);
54
+ // Report usage (realign doesn't count as new project use)
55
+ const repoId = analysis.gitHubRemoteUrl || cwd;
56
+ await reportUsage(result.answers.token, repoId).catch(() => { });
57
+ // Complete screen + summary
58
+ printCompleteScreen(result.answers.colleagueName);
59
+ for (const file of scaffoldResult.filesCreated) {
60
+ console.log(` ${pc.green('✓')} Updated ${file}`);
61
+ }
62
+ console.log();
63
+ console.log(pc.dim(` Review changes with ${pc.bold('git diff')}, then commit when ready.`));
64
+ console.log();
65
+ printGetStarted(result.answers.colleagueName);
66
+ return;
67
+ }
68
+ // --- Mode C: Merge (agent files exist but no .thufir) ---
69
+ if ((analysis.hasAgentsMd || analysis.hasClaudeMd || analysis.hasCursorRules) && !analysis.hasThufirMarker) {
70
+ const mergeAnswers = await runMergePrompts(analysis);
71
+ if (!mergeAnswers) {
72
+ console.log(pc.dim(' Cancelled.'));
73
+ process.exit(0);
74
+ }
75
+ const s = p.spinner();
76
+ // Step 1: Backup
77
+ s.start('Backing up existing agent files...');
78
+ const backedUp = backupAgentFiles(cwd);
79
+ s.stop(`Backed up ${backedUp.length} files to .thufir-backup/`);
80
+ // Step 2: Scaffold (overwrites agent files with Thufir versions)
81
+ s.start(`Setting up ${mergeAnswers.colleagueName}...`);
82
+ const scaffoldResult = scaffold(cwd, mergeAnswers, analysis);
83
+ s.stop('Thufir files written.');
84
+ // Step 3: Generate merge prompt
85
+ s.start('Generating merge plan...');
86
+ const mergePrompt = generateMergePrompt(cwd, mergeAnswers, backedUp);
87
+ const promptPath = join(cwd, '.thufir-backup', 'merge-prompt.md');
88
+ writeFileSync(promptPath, mergePrompt, 'utf-8');
89
+ s.stop('Merge plan ready.');
90
+ // Step 4: Detect and run local agent
91
+ const hasClaude = commandExists('claude');
92
+ const hasCursor = commandExists('cursor');
93
+ // Report usage
94
+ const repoId = analysis.gitHubRemoteUrl || cwd;
95
+ await reportUsage(mergeAnswers.token, repoId).catch(() => { });
96
+ printCompleteScreen(mergeAnswers.colleagueName);
97
+ for (const file of backedUp) {
98
+ console.log(` ${pc.yellow('↗')} Backed up ${file}`);
99
+ }
100
+ for (const file of scaffoldResult.filesCreated) {
101
+ console.log(` ${pc.green('✓')} Created ${file}`);
102
+ }
103
+ console.log(` ${pc.blue('📋')} Merge plan: .thufir-backup/merge-prompt.md`);
104
+ console.log();
105
+ if (hasClaude) {
106
+ const runAgent = await p.confirm({
107
+ message: 'Claude Code CLI detected. Run the merge agent now?',
108
+ });
109
+ if (!p.isCancel(runAgent) && runAgent) {
110
+ p.log.step('Running merge agent via Claude Code...');
111
+ try {
112
+ execSync(`claude -p "$(cat .thufir-backup/merge-prompt.md)"`, {
113
+ cwd,
114
+ stdio: 'inherit',
115
+ timeout: 300000, // 5 min timeout
116
+ });
117
+ p.log.success('Merge agent completed.');
118
+ }
119
+ catch {
120
+ p.log.warn('Merge agent exited. You can re-run manually:');
121
+ console.log(pc.dim(` claude -p "$(cat .thufir-backup/merge-prompt.md)"`));
122
+ }
123
+ }
124
+ else {
125
+ p.log.info('Run the merge agent manually when ready:');
126
+ console.log(pc.dim(` claude -p "$(cat .thufir-backup/merge-prompt.md)"`));
127
+ }
128
+ }
129
+ else if (hasCursor) {
130
+ p.log.info('Open this project in Cursor and paste the contents of:');
131
+ console.log(pc.dim(` .thufir-backup/merge-prompt.md`));
132
+ }
133
+ else {
134
+ p.log.info('Open this project in Claude Code or Cursor and paste the contents of:');
135
+ console.log(pc.dim(` .thufir-backup/merge-prompt.md`));
136
+ }
137
+ printGetStarted(mergeAnswers.colleagueName);
138
+ return;
139
+ }
140
+ // --- Mode A: Fresh install ---
141
+ const answers = await runPrompts(analysis, analysis.savedToken);
142
+ if (!answers) {
143
+ console.log(pc.dim(' Cancelled.'));
144
+ process.exit(0);
145
+ }
146
+ // Commit screen
147
+ printCommitScreen(answers.colleagueName, 'install');
148
+ const commitChoice = await p.select({
149
+ message: pc.yellow(pc.bold('Execute the command?')),
150
+ options: [
151
+ { value: 'execute', label: '⚔ Execute', hint: 'commit to this action' },
152
+ { value: 'abort', label: '↩ Abort', hint: 'return without changes' },
153
+ ],
154
+ });
155
+ if (p.isCancel(commitChoice) || commitChoice === 'abort') {
156
+ console.log(pc.dim(' No changes made.'));
157
+ process.exit(0);
158
+ }
159
+ // Scaffold
160
+ const s = p.spinner();
161
+ s.start(`Setting up ${answers.colleagueName}...`);
162
+ const result = scaffold(cwd, answers, analysis);
163
+ createInitialCommit(cwd, result.filesCreated, answers.colleagueName);
164
+ s.stop(`${answers.colleagueName} is ready.`);
165
+ // GitHub setup
166
+ if (answers.setupGitHub !== 'skip' && analysis.hasGhCli) {
167
+ setupGitHub(cwd, answers.setupGitHub, answers.repoName);
168
+ }
169
+ // Report usage to API (after files written — safe)
170
+ const repoId = analysis.gitHubRemoteUrl || cwd;
171
+ await reportUsage(answers.token, repoId).catch(() => { });
172
+ // Summary
173
+ printCompleteScreen(answers.colleagueName);
174
+ if (result.gitInitialized) {
175
+ console.log(` ${pc.green('✓')} Initialized git repository`);
176
+ }
177
+ for (const file of result.filesCreated) {
178
+ console.log(` ${pc.green('✓')} Created ${file}`);
179
+ }
180
+ console.log(` ${pc.green('✓')} Created initial commit`);
181
+ console.log();
182
+ printGetStarted(answers.colleagueName);
183
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { runInit } from './commands/init.js';
3
+ const command = process.argv[2];
4
+ if (command === 'init') {
5
+ runInit();
6
+ }
7
+ else {
8
+ console.log('Usage: thufir init');
9
+ process.exit(1);
10
+ }
@@ -0,0 +1,22 @@
1
+ import { type RepoAnalysis, type ThufirSettings } from './analyze.js';
2
+ export interface InitAnswers {
3
+ token: string;
4
+ userName: string;
5
+ colleagueName: string;
6
+ projectMode: 'single' | 'multi';
7
+ projectDescription: string;
8
+ projectFocus: 'code' | 'planning' | 'hybrid';
9
+ traits: string[];
10
+ skillPackages: string[];
11
+ skillTriggers: string[];
12
+ setupGitHub: 'create' | 'connect' | 'skip';
13
+ repoName: string;
14
+ }
15
+ export declare function runPrompts(analysis: RepoAnalysis, savedToken?: string | null): Promise<InitAnswers | null>;
16
+ export declare function runMergePrompts(analysis: RepoAnalysis): Promise<InitAnswers | null>;
17
+ export type RealignAction = 'realign' | 'reconfigure' | 'cancel';
18
+ export interface RealignResult {
19
+ action: RealignAction;
20
+ answers: InitAnswers;
21
+ }
22
+ export declare function runRealignPrompts(settings: ThufirSettings, savedToken: string | null): Promise<RealignResult | null>;