Skip to content
This repository was archived by the owner on Jan 11, 2026. It is now read-only.

Commit 3adfa17

Browse files
rmurpheyclaude
andcommitted
feat: rewrite setup.js with intelligent conflict handling
- Removed claude-setup repo check (no longer needed) - Added command-line flags: --skip, --backup, --force, --help - Implemented conflict detection for existing files - Interactive mode prompts user when conflicts found - Safe copying using Node.js fs instead of shell commands - Backup functionality preserves existing customizations - Merge mode adds only non-conflicting files - Works safely in any repo including its own 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 979c30b commit 3adfa17

File tree

1 file changed

+287
-55
lines changed

1 file changed

+287
-55
lines changed

scripts/setup.js

Lines changed: 287 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,80 +3,312 @@
33
const fs = require('fs');
44
const path = require('path');
55
const { execSync } = require('child_process');
6+
const readline = require('readline');
7+
8+
// Parse command-line arguments
9+
const args = process.argv.slice(2);
10+
const options = {
11+
force: args.includes('--force'),
12+
skip: args.includes('--skip'),
13+
backup: args.includes('--backup'),
14+
help: args.includes('--help') || args.includes('-h'),
15+
interactive: !args.some(arg => ['--force', '--skip', '--backup'].includes(arg))
16+
};
17+
18+
// Show help if requested
19+
if (options.help) {
20+
console.log('🤖 Claude Code Commands Setup');
21+
console.log('==============================');
22+
console.log('');
23+
console.log('Usage: npx claude-setup [options]');
24+
console.log('');
25+
console.log('Options:');
26+
console.log(' --skip Skip existing files (preserve customizations)');
27+
console.log(' --backup Backup existing files before replacing');
28+
console.log(' --force Replace all files (creates backup)');
29+
console.log(' --help Show this help message');
30+
console.log('');
31+
console.log('Default behavior is interactive - you\'ll be prompted for conflicts.');
32+
process.exit(0);
33+
}
634

735
console.log('🤖 Claude Code Commands Setup');
836
console.log('==============================');
937

10-
// Safety check: Don't run setup in the claude-setup repo itself
11-
const packageJsonPath = path.join(process.cwd(), 'package.json');
12-
if (fs.existsSync(packageJsonPath)) {
13-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
14-
if (packageJson.name === 'claude-setup') {
15-
console.log('');
16-
console.log('⚠️ WARNING: Cannot run setup in the claude-setup repository itself!');
17-
console.log('');
18-
console.log('This script copies commands FROM this repo TO other projects.');
19-
console.log('Running it here would create duplicate directories.');
20-
console.log('');
21-
console.log('To use these commands in another project:');
22-
console.log('1. Navigate to your project directory');
23-
console.log('2. Run: npx claude-setup');
24-
console.log('');
25-
process.exit(1);
38+
// Check if we're in a git repository
39+
function isGitRepo() {
40+
try {
41+
execSync('git status', { stdio: 'ignore' });
42+
return true;
43+
} catch (e) {
44+
return false;
2645
}
2746
}
2847

29-
// Check if we're in a git repository
30-
try {
31-
execSync('git status', { stdio: 'ignore' });
32-
} catch (e) {
48+
if (!isGitRepo()) {
3349
console.log('❌ Not a git repository. Run "git init" first.');
3450
process.exit(1);
3551
}
3652

37-
// Create .claude directory
38-
if (!fs.existsSync('.claude')) {
39-
fs.mkdirSync('.claude', { recursive: true });
40-
console.log('✅ Created .claude directory');
53+
// Utility function to get all files recursively
54+
function getAllFiles(dir, files = []) {
55+
if (!fs.existsSync(dir)) return files;
56+
57+
const items = fs.readdirSync(dir);
58+
for (const item of items) {
59+
const fullPath = path.join(dir, item);
60+
if (fs.statSync(fullPath).isDirectory()) {
61+
getAllFiles(fullPath, files);
62+
} else {
63+
files.push(fullPath);
64+
}
65+
}
66+
return files;
67+
}
68+
69+
// Detect conflicts between source and target
70+
function detectConflicts(sourceDir, targetDir) {
71+
const conflicts = [];
72+
if (!fs.existsSync(targetDir)) return conflicts;
73+
74+
const sourceFiles = getAllFiles(sourceDir);
75+
for (const file of sourceFiles) {
76+
const relativePath = path.relative(sourceDir, file);
77+
const targetPath = path.join(targetDir, relativePath);
78+
if (fs.existsSync(targetPath)) {
79+
conflicts.push(relativePath);
80+
}
81+
}
82+
return conflicts;
4183
}
4284

43-
// Copy commands
44-
const sourceCommands = path.join(__dirname, '../.claude/commands');
45-
const targetCommands = '.claude/commands';
85+
// Prompt user for conflict resolution
86+
async function promptUser(conflicts) {
87+
console.log('');
88+
console.log(`⚠️ Found ${conflicts.length} existing file(s):`);
89+
// Show first 5 conflicts
90+
conflicts.slice(0, 5).forEach(file => {
91+
console.log(` • ${file}`);
92+
});
93+
if (conflicts.length > 5) {
94+
console.log(` ... and ${conflicts.length - 5} more`);
95+
}
96+
97+
console.log('');
98+
console.log('How would you like to proceed?');
99+
console.log(' 1. Skip - Keep your existing files (recommended)');
100+
console.log(' 2. Backup - Backup existing and install fresh');
101+
console.log(' 3. Merge - Add only non-conflicting files');
102+
console.log(' 4. Cancel - Exit without changes');
103+
console.log('');
104+
105+
const rl = readline.createInterface({
106+
input: process.stdin,
107+
output: process.stdout
108+
});
109+
110+
return new Promise((resolve) => {
111+
rl.question('Choice [1-4]: ', (answer) => {
112+
rl.close();
113+
switch (answer.trim()) {
114+
case '1': resolve('skip'); break;
115+
case '2': resolve('backup'); break;
116+
case '3': resolve('merge'); break;
117+
case '4': resolve('cancel'); break;
118+
default:
119+
console.log('Invalid choice, using "skip" as default');
120+
resolve('skip');
121+
}
122+
});
123+
});
124+
}
46125

47-
if (fs.existsSync(sourceCommands)) {
48-
execSync(`cp -r "${sourceCommands}" "${targetCommands}"`);
49-
console.log('✅ Copied commands');
50-
} else {
51-
console.log('❌ Source commands not found');
52-
process.exit(1);
126+
// Determine resolution strategy
127+
async function determineStrategy(conflicts, options) {
128+
if (conflicts.length === 0) return 'proceed';
129+
130+
if (options.skip) return 'skip';
131+
if (options.backup) return 'backup';
132+
if (options.force) return 'backup'; // Force is like backup but less interactive
133+
if (options.interactive) return await promptUser(conflicts);
134+
135+
return 'skip'; // Default safe behavior
53136
}
54137

55-
// Copy agents
56-
const sourceAgents = path.join(__dirname, '../.claude/agents');
57-
const targetAgents = '.claude/agents';
138+
// Copy directory recursively
139+
function copyRecursive(source, target, skipExisting = false) {
140+
// Create target directory if it doesn't exist
141+
if (!fs.existsSync(target)) {
142+
fs.mkdirSync(target, { recursive: true });
143+
}
144+
145+
const items = fs.readdirSync(source);
146+
let copied = 0;
147+
let skipped = 0;
148+
149+
for (const item of items) {
150+
const sourcePath = path.join(source, item);
151+
const targetPath = path.join(target, item);
152+
153+
if (fs.statSync(sourcePath).isDirectory()) {
154+
const result = copyRecursive(sourcePath, targetPath, skipExisting);
155+
copied += result.copied;
156+
skipped += result.skipped;
157+
} else {
158+
if (skipExisting && fs.existsSync(targetPath)) {
159+
skipped++;
160+
} else {
161+
fs.copyFileSync(sourcePath, targetPath);
162+
copied++;
163+
}
164+
}
165+
}
166+
167+
return { copied, skipped };
168+
}
58169

59-
if (fs.existsSync(sourceAgents)) {
60-
execSync(`cp -r "${sourceAgents}" "${targetAgents}"`);
61-
console.log('✅ Copied agents');
170+
// Backup existing directory
171+
function backupDirectory(dir) {
172+
if (!fs.existsSync(dir)) return null;
173+
174+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
175+
const backupPath = `${dir}.backup-${timestamp}`;
176+
fs.renameSync(dir, backupPath);
177+
return backupPath;
62178
}
63179

64-
// Copy essential files
65-
const filesToCopy = ['CLAUDE.md', 'AGENTS.md', 'ACTIVE_WORK.md'];
180+
// Safe copy with conflict resolution
181+
function safeCopy(source, target, strategy, name) {
182+
let result = { copied: 0, skipped: 0, backedUp: null };
183+
184+
switch (strategy) {
185+
case 'proceed':
186+
// No conflicts, just copy
187+
result = copyRecursive(source, target);
188+
break;
189+
190+
case 'skip':
191+
// Skip all conflicts
192+
console.log(` ⏭️ Preserving existing ${name}`);
193+
result.skipped = getAllFiles(source).length;
194+
break;
195+
196+
case 'backup':
197+
// Backup existing and copy fresh
198+
if (fs.existsSync(target)) {
199+
result.backedUp = backupDirectory(target);
200+
console.log(` 📦 Backed up to ${path.basename(result.backedUp)}`);
201+
}
202+
result = copyRecursive(source, target);
203+
break;
204+
205+
case 'merge':
206+
// Only copy non-conflicting files
207+
result = copyRecursive(source, target, true);
208+
break;
209+
210+
case 'cancel':
211+
console.log('❌ Setup cancelled');
212+
process.exit(0);
213+
break;
214+
}
215+
216+
return result;
217+
}
66218

67-
filesToCopy.forEach(file => {
68-
const sourcePath = path.join(__dirname, '..', file);
69-
if (fs.existsSync(sourcePath) && !fs.existsSync(file)) {
70-
fs.copyFileSync(sourcePath, file);
71-
console.log(`✅ Created ${file}`);
219+
// Main setup function
220+
async function main() {
221+
// Define source and target paths
222+
const sourceCommands = path.join(__dirname, '../.claude/commands');
223+
const targetCommands = '.claude/commands';
224+
const sourceAgents = path.join(__dirname, '../.claude/agents');
225+
const targetAgents = '.claude/agents';
226+
227+
// Check if source files exist
228+
if (!fs.existsSync(sourceCommands)) {
229+
console.log('❌ Source commands not found. Make sure you\'re running from the correct package.');
230+
process.exit(1);
231+
}
232+
233+
// Create .claude directory if it doesn't exist
234+
if (!fs.existsSync('.claude')) {
235+
fs.mkdirSync('.claude', { recursive: true });
236+
console.log('✅ Created .claude directory');
237+
}
238+
239+
// Detect conflicts
240+
const commandConflicts = detectConflicts(sourceCommands, targetCommands);
241+
const agentConflicts = detectConflicts(sourceAgents, targetAgents);
242+
const allConflicts = [...new Set([...commandConflicts, ...agentConflicts])];
243+
244+
// Determine strategy
245+
const strategy = await determineStrategy(allConflicts, options);
246+
247+
// Copy commands
248+
console.log('\n📦 Installing Claude commands...');
249+
const commandResult = safeCopy(sourceCommands, targetCommands, strategy, 'commands');
250+
if (commandResult.copied > 0) {
251+
console.log(` ✅ Installed ${commandResult.copied} command files`);
252+
}
253+
if (commandResult.skipped > 0) {
254+
console.log(` ⏭️ Skipped ${commandResult.skipped} existing files`);
72255
}
73-
});
256+
257+
// Copy agents
258+
let agentResult = { copied: 0, skipped: 0, backedUp: null };
259+
if (fs.existsSync(sourceAgents)) {
260+
console.log('\n📦 Installing Claude agents...');
261+
agentResult = safeCopy(sourceAgents, targetAgents, strategy, 'agents');
262+
if (agentResult.copied > 0) {
263+
console.log(` ✅ Installed ${agentResult.copied} agent files`);
264+
}
265+
if (agentResult.skipped > 0) {
266+
console.log(` ⏭️ Skipped ${agentResult.skipped} existing files`);
267+
}
268+
}
269+
270+
// Copy essential root files (only if they don't exist)
271+
console.log('\n📦 Setting up configuration files...');
272+
const filesToCopy = ['CLAUDE.md', 'AGENTS.md', 'ACTIVE_WORK.md'];
273+
let configCopied = 0;
274+
275+
filesToCopy.forEach(file => {
276+
const sourcePath = path.join(__dirname, '..', file);
277+
if (fs.existsSync(sourcePath) && !fs.existsSync(file)) {
278+
fs.copyFileSync(sourcePath, file);
279+
console.log(` ✅ Created ${file}`);
280+
configCopied++;
281+
}
282+
});
283+
284+
if (configCopied === 0) {
285+
console.log(' ℹ️ Configuration files already exist');
286+
}
287+
288+
// Final message
289+
console.log('');
290+
console.log('🎉 Setup complete!');
291+
console.log('');
292+
293+
if (strategy === 'skip' || strategy === 'merge') {
294+
console.log('ℹ️ Some files were preserved to keep your customizations');
295+
console.log('');
296+
}
297+
298+
console.log('Next steps:');
299+
console.log('• Review .claude/commands/ for available commands');
300+
console.log('• Use /hygiene to check project health');
301+
console.log('• Use /todo to manage tasks');
302+
console.log('• Customize commands for your specific needs');
303+
304+
if (commandResult.backedUp || agentResult.backedUp) {
305+
console.log('');
306+
console.log('📝 Note: Your previous setup was backed up');
307+
}
308+
}
74309

75-
console.log('');
76-
console.log('🎉 Setup complete!');
77-
console.log('');
78-
console.log('Next steps:');
79-
console.log('• Use /hygiene to check project health');
80-
console.log('• Use /todo to manage tasks');
81-
console.log('• Read AGENTS.md to understand agents vs commands');
82-
console.log('• Customize commands in .claude/commands/ for your project');
310+
// Run the setup
311+
main().catch(error => {
312+
console.error('❌ Setup failed:', error.message);
313+
process.exit(1);
314+
});

0 commit comments

Comments
 (0)