ccstart 1.0.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.
- package/README.md +346 -0
- package/bin/create-project.bak.js +1062 -0
- package/bin/create-project.js +1107 -0
- package/package.json +47 -0
- package/template/claude/CLAUDE.md +104 -0
- package/template/claude/agents/README.md +35 -0
- package/template/claude/agents/backend.md +80 -0
- package/template/claude/agents/blockchain.md +81 -0
- package/template/claude/agents/checker.md +69 -0
- package/template/claude/agents/coder.md +46 -0
- package/template/claude/agents/frontend.md +73 -0
- package/template/claude/agents/planner.md +36 -0
- package/template/claude/agents/researcher.md +56 -0
- package/template/claude/agents/shadcn.md +106 -0
- package/template/claude/docs/ROADMAP.md +64 -0
- package/template/claude/docs/agent-orchestration.md +152 -0
- package/template/claude/plans/README.md +53 -0
- package/template/claude/tickets/README.md +39 -0
- package/template/claude/tickets/ticket-list.md +79 -0
@@ -0,0 +1,1062 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
const fs = require('fs');
|
4
|
+
const path = require('path');
|
5
|
+
const readline = require('readline');
|
6
|
+
|
7
|
+
// Parse CLI arguments
|
8
|
+
const args = process.argv.slice(2);
|
9
|
+
const flags = {
|
10
|
+
force: false,
|
11
|
+
dryRun: false,
|
12
|
+
help: false,
|
13
|
+
allAgents: false,
|
14
|
+
noAgents: false,
|
15
|
+
agents: false
|
16
|
+
};
|
17
|
+
|
18
|
+
let projectName = '.';
|
19
|
+
|
20
|
+
// Process arguments
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
22
|
+
const arg = args[i];
|
23
|
+
if (arg === '--force' || arg === '-f') {
|
24
|
+
flags.force = true;
|
25
|
+
} else if (arg === '--dry-run' || arg === '-d') {
|
26
|
+
flags.dryRun = true;
|
27
|
+
} else if (arg === '--help' || arg === '-h') {
|
28
|
+
flags.help = true;
|
29
|
+
} else if (arg === '--all-agents') {
|
30
|
+
flags.allAgents = true;
|
31
|
+
} else if (arg === '--no-agents') {
|
32
|
+
flags.noAgents = true;
|
33
|
+
} else if (arg === '--agents') {
|
34
|
+
flags.agents = true;
|
35
|
+
} else if (!arg.startsWith('-')) {
|
36
|
+
projectName = arg;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
// Show help if requested
|
41
|
+
if (flags.help) {
|
42
|
+
console.log(`
|
43
|
+
Usage: ccsetup [project-name] [options]
|
44
|
+
|
45
|
+
Options:
|
46
|
+
--force, -f Skip all prompts and overwrite existing files
|
47
|
+
--dry-run, -d Show what would be done without making changes
|
48
|
+
--agents Interactive agent selection mode
|
49
|
+
--all-agents Include all agents without prompting
|
50
|
+
--no-agents Skip agent selection entirely
|
51
|
+
--help, -h Show this help message
|
52
|
+
|
53
|
+
Examples:
|
54
|
+
ccsetup # Create in current directory
|
55
|
+
ccsetup my-project # Create in new directory
|
56
|
+
ccsetup . --force # Overwrite files in current directory
|
57
|
+
ccsetup my-app --dry-run # Preview changes without creating files
|
58
|
+
ccsetup --agents # Interactive agent selection only
|
59
|
+
ccsetup my-app --all-agents # Include all agents automatically
|
60
|
+
`);
|
61
|
+
process.exit(0);
|
62
|
+
}
|
63
|
+
|
64
|
+
// Validate project name
|
65
|
+
function validateProjectName(name) {
|
66
|
+
// Check for path traversal attempts
|
67
|
+
if (name.includes('..') || path.isAbsolute(name)) {
|
68
|
+
throw new Error('Invalid project name: Path traversal or absolute paths are not allowed');
|
69
|
+
}
|
70
|
+
|
71
|
+
// Check for invalid characters
|
72
|
+
const invalidChars = /[<>:"|?*\0]/;
|
73
|
+
if (invalidChars.test(name)) {
|
74
|
+
throw new Error('Invalid project name: Contains invalid characters');
|
75
|
+
}
|
76
|
+
|
77
|
+
return true;
|
78
|
+
}
|
79
|
+
|
80
|
+
// Validate conflicting flags
|
81
|
+
if (flags.allAgents && flags.noAgents) {
|
82
|
+
console.error('Error: Cannot use --all-agents and --no-agents together');
|
83
|
+
process.exit(1);
|
84
|
+
}
|
85
|
+
|
86
|
+
// Validate the project name
|
87
|
+
if (projectName !== '.') {
|
88
|
+
try {
|
89
|
+
validateProjectName(projectName);
|
90
|
+
} catch (error) {
|
91
|
+
console.error(`Error: ${error.message}`);
|
92
|
+
process.exit(1);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
97
|
+
const templateDir = path.join(__dirname, '..', 'template');
|
98
|
+
|
99
|
+
// Create readline interface only if needed
|
100
|
+
let rl = null;
|
101
|
+
if (!flags.force && !flags.dryRun) {
|
102
|
+
rl = readline.createInterface({
|
103
|
+
input: process.stdin,
|
104
|
+
output: process.stdout
|
105
|
+
});
|
106
|
+
}
|
107
|
+
|
108
|
+
function prompt(question) {
|
109
|
+
if (!rl) {
|
110
|
+
throw new Error('Readline interface not initialized');
|
111
|
+
}
|
112
|
+
return new Promise((resolve) => {
|
113
|
+
rl.question(question, (answer) => {
|
114
|
+
resolve(answer.toLowerCase().trim());
|
115
|
+
});
|
116
|
+
});
|
117
|
+
}
|
118
|
+
|
119
|
+
function normalizeConflictStrategy(input) {
|
120
|
+
const normalized = input.toLowerCase().trim();
|
121
|
+
|
122
|
+
// Accept abbreviations and variations
|
123
|
+
if (normalized === 's' || normalized === 'skip' || normalized === '1') return 'skip';
|
124
|
+
if (normalized === 'r' || normalized === 'rename' || normalized === '2') return 'rename';
|
125
|
+
if (normalized === 'o' || normalized === 'overwrite' || normalized === '3') return 'overwrite';
|
126
|
+
|
127
|
+
return null;
|
128
|
+
}
|
129
|
+
|
130
|
+
// Function to validate agent files for security
|
131
|
+
function validateAgentFile(file) {
|
132
|
+
// Ensure the file is just a basename without directory separators
|
133
|
+
const basename = path.basename(file);
|
134
|
+
if (file !== basename) {
|
135
|
+
return false;
|
136
|
+
}
|
137
|
+
// Additional validation: ensure it's a markdown file
|
138
|
+
if (!file.endsWith('.md')) {
|
139
|
+
return false;
|
140
|
+
}
|
141
|
+
// Reject files with suspicious patterns
|
142
|
+
if (file.includes('..') || file.includes('./') || file.includes('\\')) {
|
143
|
+
return false;
|
144
|
+
}
|
145
|
+
return true;
|
146
|
+
}
|
147
|
+
|
148
|
+
// Function to check if Claude Code is installed
|
149
|
+
function checkClaudeCode() {
|
150
|
+
const claudeDir = path.join(targetDir, '.claude');
|
151
|
+
const claudeCodeFile = path.join(targetDir, 'claude_code.txt');
|
152
|
+
const hasClaudeDir = fs.existsSync(claudeDir);
|
153
|
+
const hasClaudeCodeFile = fs.existsSync(claudeCodeFile);
|
154
|
+
|
155
|
+
return {
|
156
|
+
isInstalled: hasClaudeDir || hasClaudeCodeFile,
|
157
|
+
hasClaudeDir: hasClaudeDir,
|
158
|
+
hasClaudeCodeFile: hasClaudeCodeFile
|
159
|
+
};
|
160
|
+
}
|
161
|
+
|
162
|
+
// Function to initialize .claude directory structure
|
163
|
+
async function initializeClaudeDirectory(selectedAgentFiles, conflictStrategy, dryRun) {
|
164
|
+
const claudeDir = path.join(targetDir, '.claude');
|
165
|
+
const claudeAgentsDir = path.join(claudeDir, 'agents');
|
166
|
+
const templateClaudeDir = path.join(templateDir, '.claude');
|
167
|
+
|
168
|
+
const createdItems = [];
|
169
|
+
const skippedItems = [];
|
170
|
+
|
171
|
+
try {
|
172
|
+
// Create .claude directory
|
173
|
+
if (!fs.existsSync(claudeDir)) {
|
174
|
+
if (!dryRun) {
|
175
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
176
|
+
}
|
177
|
+
createdItems.push('.claude/');
|
178
|
+
if (dryRun) {
|
179
|
+
console.log(' š Would create directory: .claude/');
|
180
|
+
}
|
181
|
+
} else {
|
182
|
+
skippedItems.push('.claude/');
|
183
|
+
}
|
184
|
+
|
185
|
+
// Create .claude/agents directory
|
186
|
+
if (!fs.existsSync(claudeAgentsDir)) {
|
187
|
+
if (!dryRun) {
|
188
|
+
fs.mkdirSync(claudeAgentsDir, { recursive: true });
|
189
|
+
}
|
190
|
+
createdItems.push('.claude/agents/');
|
191
|
+
if (dryRun) {
|
192
|
+
console.log(' š Would create directory: .claude/agents/');
|
193
|
+
}
|
194
|
+
} else {
|
195
|
+
skippedItems.push('.claude/agents/');
|
196
|
+
}
|
197
|
+
|
198
|
+
// Copy .claude/README.md
|
199
|
+
const claudeReadmeSrc = path.join(templateClaudeDir, 'README.md');
|
200
|
+
const claudeReadmeDest = path.join(claudeDir, 'README.md');
|
201
|
+
if (fs.existsSync(claudeReadmeSrc)) {
|
202
|
+
if (!fs.existsSync(claudeReadmeDest)) {
|
203
|
+
if (!dryRun) {
|
204
|
+
fs.copyFileSync(claudeReadmeSrc, claudeReadmeDest);
|
205
|
+
}
|
206
|
+
createdItems.push('.claude/README.md');
|
207
|
+
if (dryRun) {
|
208
|
+
console.log(' ⨠Would copy: .claude/README.md');
|
209
|
+
}
|
210
|
+
} else {
|
211
|
+
if (conflictStrategy === 'overwrite') {
|
212
|
+
if (!dryRun) {
|
213
|
+
fs.copyFileSync(claudeReadmeSrc, claudeReadmeDest);
|
214
|
+
}
|
215
|
+
if (dryRun) {
|
216
|
+
console.log(' ā»ļø Would replace: .claude/README.md');
|
217
|
+
}
|
218
|
+
} else if (conflictStrategy === 'rename') {
|
219
|
+
const ext = path.extname(claudeReadmeDest);
|
220
|
+
const baseName = path.basename(claudeReadmeDest, ext);
|
221
|
+
const dirName = path.dirname(claudeReadmeDest);
|
222
|
+
let newDest = path.join(dirName, `${baseName}-ccsetup${ext}`);
|
223
|
+
let counter = 1;
|
224
|
+
while (fs.existsSync(newDest)) {
|
225
|
+
newDest = path.join(dirName, `${baseName}-ccsetup-${counter}${ext}`);
|
226
|
+
counter++;
|
227
|
+
}
|
228
|
+
if (!dryRun) {
|
229
|
+
fs.copyFileSync(claudeReadmeSrc, newDest);
|
230
|
+
}
|
231
|
+
const relativePath = path.relative(claudeDir, newDest);
|
232
|
+
createdItems.push(relativePath);
|
233
|
+
if (dryRun) {
|
234
|
+
console.log(` š Would create: ${relativePath}`);
|
235
|
+
} else {
|
236
|
+
console.log(` š Created: ${relativePath}`);
|
237
|
+
}
|
238
|
+
} else {
|
239
|
+
skippedItems.push('.claude/README.md');
|
240
|
+
if (dryRun) {
|
241
|
+
console.log(' āļø Would skip: .claude/README.md');
|
242
|
+
}
|
243
|
+
}
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
// Copy .claude/agents/README.md
|
248
|
+
const agentsReadmeSrc = path.join(templateClaudeDir, 'agents', 'README.md');
|
249
|
+
const agentsReadmeDest = path.join(claudeAgentsDir, 'README.md');
|
250
|
+
if (fs.existsSync(agentsReadmeSrc)) {
|
251
|
+
if (!fs.existsSync(agentsReadmeDest)) {
|
252
|
+
if (!dryRun) {
|
253
|
+
fs.copyFileSync(agentsReadmeSrc, agentsReadmeDest);
|
254
|
+
}
|
255
|
+
createdItems.push('.claude/agents/README.md');
|
256
|
+
if (dryRun) {
|
257
|
+
console.log(' ⨠Would copy: .claude/agents/README.md');
|
258
|
+
}
|
259
|
+
} else {
|
260
|
+
if (conflictStrategy === 'overwrite') {
|
261
|
+
if (!dryRun) {
|
262
|
+
fs.copyFileSync(agentsReadmeSrc, agentsReadmeDest);
|
263
|
+
}
|
264
|
+
if (dryRun) {
|
265
|
+
console.log(' ā»ļø Would replace: .claude/agents/README.md');
|
266
|
+
}
|
267
|
+
} else if (conflictStrategy === 'rename') {
|
268
|
+
const ext = path.extname(agentsReadmeDest);
|
269
|
+
const baseName = path.basename(agentsReadmeDest, ext);
|
270
|
+
const dirName = path.dirname(agentsReadmeDest);
|
271
|
+
let newDest = path.join(dirName, `${baseName}-ccsetup${ext}`);
|
272
|
+
let counter = 1;
|
273
|
+
while (fs.existsSync(newDest)) {
|
274
|
+
newDest = path.join(dirName, `${baseName}-ccsetup-${counter}${ext}`);
|
275
|
+
counter++;
|
276
|
+
}
|
277
|
+
if (!dryRun) {
|
278
|
+
fs.copyFileSync(agentsReadmeSrc, newDest);
|
279
|
+
}
|
280
|
+
const relativePath = path.relative(claudeDir, newDest);
|
281
|
+
createdItems.push(relativePath);
|
282
|
+
if (dryRun) {
|
283
|
+
console.log(` š Would create: ${relativePath}`);
|
284
|
+
} else {
|
285
|
+
console.log(` š Created: ${relativePath}`);
|
286
|
+
}
|
287
|
+
} else {
|
288
|
+
skippedItems.push('.claude/agents/README.md');
|
289
|
+
if (dryRun) {
|
290
|
+
console.log(' āļø Would skip: .claude/agents/README.md');
|
291
|
+
}
|
292
|
+
}
|
293
|
+
}
|
294
|
+
}
|
295
|
+
|
296
|
+
// Copy selected agents to .claude/agents
|
297
|
+
const templateAgentsDir = path.join(templateDir, 'agents');
|
298
|
+
let copiedAgents = 0;
|
299
|
+
let skippedAgents = 0;
|
300
|
+
|
301
|
+
for (const agentFile of selectedAgentFiles) {
|
302
|
+
// Validate agent file before processing
|
303
|
+
if (!validateAgentFile(agentFile)) {
|
304
|
+
console.warn(`ā ļø Skipping invalid agent file: ${agentFile}`);
|
305
|
+
continue;
|
306
|
+
}
|
307
|
+
|
308
|
+
// Use basename to ensure safety
|
309
|
+
const safeAgentFile = path.basename(agentFile);
|
310
|
+
const agentSrc = path.join(templateAgentsDir, safeAgentFile);
|
311
|
+
const agentDest = path.join(claudeAgentsDir, safeAgentFile);
|
312
|
+
|
313
|
+
// Additional validation: ensure source is within template directory
|
314
|
+
const normalizedAgentSrc = path.normalize(agentSrc);
|
315
|
+
const normalizedTemplateAgentsDir = path.normalize(templateAgentsDir);
|
316
|
+
if (!normalizedAgentSrc.startsWith(normalizedTemplateAgentsDir)) {
|
317
|
+
console.warn(`ā ļø Skipping agent file outside template directory: ${agentFile}`);
|
318
|
+
continue;
|
319
|
+
}
|
320
|
+
|
321
|
+
if (fs.existsSync(agentSrc)) {
|
322
|
+
if (!fs.existsSync(agentDest)) {
|
323
|
+
if (!dryRun) {
|
324
|
+
fs.copyFileSync(agentSrc, agentDest);
|
325
|
+
}
|
326
|
+
copiedAgents++;
|
327
|
+
createdItems.push(`.claude/agents/${safeAgentFile}`);
|
328
|
+
if (dryRun) {
|
329
|
+
console.log(` ⨠Would copy: .claude/agents/${safeAgentFile}`);
|
330
|
+
}
|
331
|
+
} else {
|
332
|
+
if (conflictStrategy === 'overwrite') {
|
333
|
+
if (!dryRun) {
|
334
|
+
fs.copyFileSync(agentSrc, agentDest);
|
335
|
+
}
|
336
|
+
copiedAgents++;
|
337
|
+
if (dryRun) {
|
338
|
+
console.log(` ā»ļø Would replace: .claude/agents/${safeAgentFile}`);
|
339
|
+
}
|
340
|
+
} else if (conflictStrategy === 'rename') {
|
341
|
+
const ext = path.extname(agentDest);
|
342
|
+
const baseName = path.basename(agentDest, ext);
|
343
|
+
const dirName = path.dirname(agentDest);
|
344
|
+
let newDest = path.join(dirName, `${baseName}-ccsetup${ext}`);
|
345
|
+
let counter = 1;
|
346
|
+
while (fs.existsSync(newDest)) {
|
347
|
+
newDest = path.join(dirName, `${baseName}-ccsetup-${counter}${ext}`);
|
348
|
+
counter++;
|
349
|
+
}
|
350
|
+
if (!dryRun) {
|
351
|
+
fs.copyFileSync(agentSrc, newDest);
|
352
|
+
}
|
353
|
+
copiedAgents++;
|
354
|
+
const relativePath = path.relative(claudeDir, newDest);
|
355
|
+
createdItems.push(relativePath);
|
356
|
+
if (dryRun) {
|
357
|
+
console.log(` š Would create: ${relativePath}`);
|
358
|
+
} else {
|
359
|
+
console.log(` š Created: ${relativePath}`);
|
360
|
+
}
|
361
|
+
} else {
|
362
|
+
skippedAgents++;
|
363
|
+
skippedItems.push(`.claude/agents/${safeAgentFile}`);
|
364
|
+
if (dryRun) {
|
365
|
+
console.log(` āļø Would skip: .claude/agents/${safeAgentFile}`);
|
366
|
+
}
|
367
|
+
}
|
368
|
+
}
|
369
|
+
}
|
370
|
+
}
|
371
|
+
|
372
|
+
return {
|
373
|
+
createdItems,
|
374
|
+
skippedItems,
|
375
|
+
copiedAgents,
|
376
|
+
skippedAgents
|
377
|
+
};
|
378
|
+
|
379
|
+
} catch (error) {
|
380
|
+
throw new Error(`Failed to initialize .claude directory: ${error.message}`);
|
381
|
+
}
|
382
|
+
}
|
383
|
+
|
384
|
+
// Function to parse agent frontmatter
|
385
|
+
function parseAgentFrontmatter(filePath) {
|
386
|
+
try {
|
387
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
388
|
+
const lines = content.split('\n');
|
389
|
+
|
390
|
+
if (lines[0] !== '---') {
|
391
|
+
return null;
|
392
|
+
}
|
393
|
+
|
394
|
+
let name = '';
|
395
|
+
let description = '';
|
396
|
+
let inFrontmatter = true;
|
397
|
+
let lineIndex = 1;
|
398
|
+
|
399
|
+
while (lineIndex < lines.length && inFrontmatter) {
|
400
|
+
const line = lines[lineIndex];
|
401
|
+
if (line === '---') {
|
402
|
+
inFrontmatter = false;
|
403
|
+
} else if (line.startsWith('name:')) {
|
404
|
+
name = line.substring(5).trim();
|
405
|
+
} else if (line.startsWith('description:')) {
|
406
|
+
description = line.substring(12).trim();
|
407
|
+
}
|
408
|
+
lineIndex++;
|
409
|
+
}
|
410
|
+
|
411
|
+
return { name, description };
|
412
|
+
} catch (error) {
|
413
|
+
return null;
|
414
|
+
}
|
415
|
+
}
|
416
|
+
|
417
|
+
// Function to get available agents
|
418
|
+
function getAvailableAgents() {
|
419
|
+
const agentsDir = path.join(templateDir, 'agents');
|
420
|
+
const agents = [];
|
421
|
+
|
422
|
+
try {
|
423
|
+
const files = fs.readdirSync(agentsDir);
|
424
|
+
for (const file of files) {
|
425
|
+
if (file.endsWith('.md') && file !== 'README.md') {
|
426
|
+
const filePath = path.join(agentsDir, file);
|
427
|
+
const metadata = parseAgentFrontmatter(filePath);
|
428
|
+
if (metadata && metadata.name) {
|
429
|
+
agents.push({
|
430
|
+
file,
|
431
|
+
name: metadata.name,
|
432
|
+
description: metadata.description || 'No description available'
|
433
|
+
});
|
434
|
+
}
|
435
|
+
}
|
436
|
+
}
|
437
|
+
} catch (error) {
|
438
|
+
console.error('Warning: Could not read agents directory:', error.message);
|
439
|
+
}
|
440
|
+
|
441
|
+
return agents.sort((a, b) => a.name.localeCompare(b.name));
|
442
|
+
}
|
443
|
+
|
444
|
+
// Dynamic import for ESM module
|
445
|
+
async function importCheckbox() {
|
446
|
+
try {
|
447
|
+
const module = await import('@inquirer/checkbox');
|
448
|
+
return module.default;
|
449
|
+
} catch (error) {
|
450
|
+
console.error('Error: Failed to load @inquirer/checkbox. Please ensure it is installed.');
|
451
|
+
console.error('Run: npm install @inquirer/checkbox');
|
452
|
+
process.exit(1);
|
453
|
+
}
|
454
|
+
}
|
455
|
+
|
456
|
+
async function selectAgents(availableAgents) {
|
457
|
+
const checkbox = await importCheckbox();
|
458
|
+
|
459
|
+
// ANSI color codes
|
460
|
+
const colors = {
|
461
|
+
cyan: '\x1b[36m',
|
462
|
+
yellow: '\x1b[33m',
|
463
|
+
green: '\x1b[32m',
|
464
|
+
blue: '\x1b[34m',
|
465
|
+
magenta: '\x1b[35m',
|
466
|
+
bold: '\x1b[1m',
|
467
|
+
dim: '\x1b[2m',
|
468
|
+
reset: '\x1b[0m'
|
469
|
+
};
|
470
|
+
|
471
|
+
const choices = availableAgents.map(agent => ({
|
472
|
+
name: `${colors.cyan}${colors.bold}${agent.name}${colors.reset}\n ${colors.dim}${agent.description}${colors.reset}`,
|
473
|
+
value: agent.file,
|
474
|
+
checked: false
|
475
|
+
}));
|
476
|
+
|
477
|
+
console.log('\nš¤ Select agents to include in your Claude Code project\n');
|
478
|
+
console.log(`${colors.dim}Use arrow keys to navigate, space to select/deselect, 'a' to toggle all${colors.reset}\n`);
|
479
|
+
|
480
|
+
const selectedFiles = await checkbox({
|
481
|
+
message: `${colors.bold}Choose your agents:${colors.reset}`,
|
482
|
+
choices,
|
483
|
+
pageSize: 10,
|
484
|
+
loop: false
|
485
|
+
});
|
486
|
+
|
487
|
+
// Validate selected files to prevent path traversal
|
488
|
+
const validatedFiles = selectedFiles.filter(file => {
|
489
|
+
// Use centralized validation function
|
490
|
+
if (!validateAgentFile(file)) {
|
491
|
+
console.warn(`ā ļø Skipping invalid agent file: ${file}`);
|
492
|
+
return false;
|
493
|
+
}
|
494
|
+
// Ensure it exists in available agents
|
495
|
+
if (!availableAgents.some(agent => agent.file === file)) {
|
496
|
+
console.warn(`ā ļø Skipping unknown agent file: ${file}`);
|
497
|
+
return false;
|
498
|
+
}
|
499
|
+
return true;
|
500
|
+
});
|
501
|
+
|
502
|
+
return validatedFiles;
|
503
|
+
}
|
504
|
+
|
505
|
+
async function main() {
|
506
|
+
try {
|
507
|
+
// Ensure template directory exists
|
508
|
+
if (!fs.existsSync(templateDir)) {
|
509
|
+
throw new Error(`Template directory not found: ${templateDir}`);
|
510
|
+
}
|
511
|
+
|
512
|
+
// Handle --agents flag for agent selection only
|
513
|
+
if (flags.agents) {
|
514
|
+
console.log('š¤ Interactive Agent Selection\n');
|
515
|
+
const availableAgents = getAvailableAgents();
|
516
|
+
|
517
|
+
if (availableAgents.length === 0) {
|
518
|
+
console.log('No agents available for selection.');
|
519
|
+
process.exit(0);
|
520
|
+
}
|
521
|
+
|
522
|
+
// Perform interactive selection
|
523
|
+
const selectedAgentFiles = await selectAgents(availableAgents);
|
524
|
+
|
525
|
+
if (selectedAgentFiles.length === 0) {
|
526
|
+
console.log('\nā No agents selected.');
|
527
|
+
} else {
|
528
|
+
// ANSI color codes
|
529
|
+
const colors = {
|
530
|
+
cyan: '\x1b[36m',
|
531
|
+
green: '\x1b[32m',
|
532
|
+
yellow: '\x1b[33m',
|
533
|
+
bold: '\x1b[1m',
|
534
|
+
dim: '\x1b[2m',
|
535
|
+
reset: '\x1b[0m'
|
536
|
+
};
|
537
|
+
|
538
|
+
console.log(`\n${colors.green}${colors.bold}ā
You selected ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'}:${colors.reset}\n`);
|
539
|
+
|
540
|
+
// Show selected agents with descriptions
|
541
|
+
selectedAgentFiles.forEach(file => {
|
542
|
+
const agent = availableAgents.find(a => a.file === file);
|
543
|
+
if (agent) {
|
544
|
+
console.log(` ${colors.cyan}${colors.bold}${agent.name}${colors.reset}`);
|
545
|
+
console.log(` ${colors.dim}${agent.description}${colors.reset}\n`);
|
546
|
+
}
|
547
|
+
});
|
548
|
+
|
549
|
+
console.log(`${colors.yellow}${colors.bold}š Next Steps:${colors.reset}`);
|
550
|
+
console.log(`${colors.dim}1. Make sure Claude Code is initialized: ${colors.reset}${colors.cyan}claude init${colors.reset}`);
|
551
|
+
console.log(`${colors.dim}2. Run: ${colors.reset}${colors.cyan}npx ccsetup${colors.reset}`);
|
552
|
+
console.log(`${colors.dim}3. Select the same agents when prompted${colors.reset}\n`);
|
553
|
+
}
|
554
|
+
|
555
|
+
process.exit(0);
|
556
|
+
}
|
557
|
+
|
558
|
+
// Additional path validation
|
559
|
+
const normalizedTarget = path.normalize(targetDir);
|
560
|
+
const normalizedCwd = path.normalize(process.cwd());
|
561
|
+
|
562
|
+
// Ensure target is within or equal to cwd for safety
|
563
|
+
if (projectName !== '.' && !normalizedTarget.startsWith(normalizedCwd)) {
|
564
|
+
throw new Error('Target directory must be within the current working directory');
|
565
|
+
}
|
566
|
+
|
567
|
+
if (flags.dryRun) {
|
568
|
+
console.log('š DRY RUN MODE - No files will be created or modified\n');
|
569
|
+
}
|
570
|
+
|
571
|
+
// Check for Claude Code installation
|
572
|
+
const claudeStatus = checkClaudeCode();
|
573
|
+
if (projectName === '.') {
|
574
|
+
if (flags.dryRun) {
|
575
|
+
console.log('ā ļø Note: Claude Code detection skipped in dry-run mode for current directory\n');
|
576
|
+
} else if (!claudeStatus.isInstalled && !claudeStatus.hasClaudeDir) {
|
577
|
+
// Offer to create .claude directory
|
578
|
+
if (flags.force) {
|
579
|
+
console.log('š Creating .claude directory structure...\n');
|
580
|
+
} else {
|
581
|
+
console.log('ā ļø Claude Code not detected in this project.');
|
582
|
+
console.log('ccsetup can create the .claude directory structure for you.\n');
|
583
|
+
const answer = await prompt('Would you like to create .claude directory? (yes/no): ');
|
584
|
+
if (answer !== 'yes') {
|
585
|
+
console.log('\nTo manually initialize Claude Code:');
|
586
|
+
console.log('1. Install Claude Code CLI: https://docs.anthropic.com/claude-code/quickstart');
|
587
|
+
console.log('2. Run \'claude init\' in your project directory');
|
588
|
+
console.log('3. Then run \'npx ccsetup\' again\n');
|
589
|
+
console.log('Aborting setup.');
|
590
|
+
if (rl) rl.close();
|
591
|
+
process.exit(0);
|
592
|
+
}
|
593
|
+
console.log('');
|
594
|
+
}
|
595
|
+
}
|
596
|
+
}
|
597
|
+
|
598
|
+
// Escape targetDir for safe display
|
599
|
+
const safeTargetDir = targetDir.replace(/[^\w\s\-./\\:]/g, '');
|
600
|
+
console.log(`${flags.dryRun ? 'Would create' : 'Creating'} Claude Code project in ${safeTargetDir}...`);
|
601
|
+
|
602
|
+
if (projectName !== '.') {
|
603
|
+
if (!fs.existsSync(targetDir)) {
|
604
|
+
if (!flags.dryRun) {
|
605
|
+
try {
|
606
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
607
|
+
} catch (error) {
|
608
|
+
if (error.code === 'EACCES') {
|
609
|
+
throw new Error(`Permission denied: Cannot create directory ${targetDir}`);
|
610
|
+
} else if (error.code === 'ENOSPC') {
|
611
|
+
throw new Error('No space left on device');
|
612
|
+
}
|
613
|
+
throw error;
|
614
|
+
}
|
615
|
+
} else {
|
616
|
+
console.log(`Would create directory: ${targetDir}`);
|
617
|
+
}
|
618
|
+
}
|
619
|
+
|
620
|
+
// Check Claude Code in new directory after creation
|
621
|
+
const newDirClaudeStatus = checkClaudeCode();
|
622
|
+
if (!flags.dryRun && !newDirClaudeStatus.isInstalled) {
|
623
|
+
console.log('\nā ļø Note: Claude Code is not initialized in the new project directory.');
|
624
|
+
console.log('After setup, remember to:');
|
625
|
+
console.log(`1. cd ${JSON.stringify(projectName)}`);
|
626
|
+
console.log('2. Run \'claude init\' to initialize Claude Code\n');
|
627
|
+
}
|
628
|
+
}
|
629
|
+
|
630
|
+
const fileConflicts = [];
|
631
|
+
const dirConflicts = [];
|
632
|
+
const allItems = [];
|
633
|
+
|
634
|
+
// Group conflicts by category
|
635
|
+
const conflictsByCategory = {
|
636
|
+
'CLAUDE.md': [],
|
637
|
+
'agents': [],
|
638
|
+
'docs': [],
|
639
|
+
'plans': [],
|
640
|
+
'tickets': []
|
641
|
+
};
|
642
|
+
|
643
|
+
// Store conflict strategies per category
|
644
|
+
const conflictStrategies = {
|
645
|
+
'CLAUDE.md': 'skip',
|
646
|
+
'agents': 'skip',
|
647
|
+
'docs': 'skip',
|
648
|
+
'plans': 'skip',
|
649
|
+
'tickets': 'skip'
|
650
|
+
};
|
651
|
+
|
652
|
+
// Get available agents for selection
|
653
|
+
const availableAgents = getAvailableAgents();
|
654
|
+
let selectedAgentFiles = [];
|
655
|
+
|
656
|
+
|
657
|
+
// Determine which agents to include
|
658
|
+
if (flags.noAgents) {
|
659
|
+
selectedAgentFiles = [];
|
660
|
+
console.log('\nāļø Skipping agent selection (--no-agents flag)');
|
661
|
+
} else if (flags.allAgents) {
|
662
|
+
selectedAgentFiles = availableAgents.map(a => a.file).filter(validateAgentFile);
|
663
|
+
console.log(`\nā
Including all ${selectedAgentFiles.length} agents (--all-agents flag)`);
|
664
|
+
} else if (!flags.dryRun) {
|
665
|
+
// Close readline interface before using inquirer
|
666
|
+
if (rl) {
|
667
|
+
rl.close();
|
668
|
+
rl = null;
|
669
|
+
}
|
670
|
+
|
671
|
+
// Interactive selection
|
672
|
+
selectedAgentFiles = await selectAgents(availableAgents);
|
673
|
+
console.log(`\nā
Selected ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'}`);
|
674
|
+
|
675
|
+
// Recreate readline interface after agent selection
|
676
|
+
if (!flags.force && !flags.dryRun) {
|
677
|
+
rl = readline.createInterface({
|
678
|
+
input: process.stdin,
|
679
|
+
output: process.stdout
|
680
|
+
});
|
681
|
+
}
|
682
|
+
} else {
|
683
|
+
// In dry-run mode, show what would happen
|
684
|
+
console.log(`\nWould prompt for agent selection from ${availableAgents.length} available agents`);
|
685
|
+
selectedAgentFiles = availableAgents.map(a => a.file).filter(validateAgentFile); // Include all for scanning purposes
|
686
|
+
}
|
687
|
+
|
688
|
+
function scanTemplate(src, dest, relativePath = '', skipAgents = false) {
|
689
|
+
try {
|
690
|
+
// Validate paths to prevent traversal
|
691
|
+
const normalizedSrc = path.normalize(src);
|
692
|
+
const normalizedDest = path.normalize(dest);
|
693
|
+
|
694
|
+
if (!normalizedSrc.startsWith(path.normalize(templateDir))) {
|
695
|
+
throw new Error('Source path escapes template directory');
|
696
|
+
}
|
697
|
+
|
698
|
+
if (!normalizedDest.startsWith(path.normalize(targetDir))) {
|
699
|
+
throw new Error('Destination path escapes target directory');
|
700
|
+
}
|
701
|
+
|
702
|
+
const exists = fs.existsSync(src);
|
703
|
+
const stats = exists && fs.statSync(src);
|
704
|
+
const isDirectory = exists && stats.isDirectory();
|
705
|
+
|
706
|
+
if (isDirectory) {
|
707
|
+
// Skip .claude directory as it will be handled separately
|
708
|
+
if (path.basename(src) === '.claude') {
|
709
|
+
return; // Don't process .claude directory in regular template scan
|
710
|
+
}
|
711
|
+
|
712
|
+
// Skip agents directory if we're handling it separately
|
713
|
+
if (skipAgents && path.basename(src) === 'agents') {
|
714
|
+
// Only scan selected agents
|
715
|
+
allItems.push({
|
716
|
+
src,
|
717
|
+
dest,
|
718
|
+
type: 'directory',
|
719
|
+
relativePath,
|
720
|
+
exists: fs.existsSync(dest)
|
721
|
+
});
|
722
|
+
|
723
|
+
if (fs.existsSync(dest)) {
|
724
|
+
dirConflicts.push(relativePath || '.');
|
725
|
+
}
|
726
|
+
|
727
|
+
// Add selected agent files
|
728
|
+
for (const agentFile of selectedAgentFiles) {
|
729
|
+
// Additional validation before processing
|
730
|
+
if (!validateAgentFile(agentFile)) {
|
731
|
+
console.warn(`ā ļø Skipping invalid agent file: ${agentFile}`);
|
732
|
+
continue;
|
733
|
+
}
|
734
|
+
|
735
|
+
// Ensure we're only dealing with basenames
|
736
|
+
const safeAgentFile = path.basename(agentFile);
|
737
|
+
const agentSrc = path.join(src, safeAgentFile);
|
738
|
+
const agentDest = path.join(dest, safeAgentFile);
|
739
|
+
const agentRelPath = path.join(relativePath, safeAgentFile);
|
740
|
+
|
741
|
+
// Validate that the source path is within the template agents directory
|
742
|
+
const normalizedAgentSrc = path.normalize(agentSrc);
|
743
|
+
const normalizedTemplateAgentsDir = path.normalize(src);
|
744
|
+
if (!normalizedAgentSrc.startsWith(normalizedTemplateAgentsDir)) {
|
745
|
+
console.warn(`ā ļø Skipping agent file outside template directory: ${agentFile}`);
|
746
|
+
continue;
|
747
|
+
}
|
748
|
+
|
749
|
+
if (fs.existsSync(agentSrc)) {
|
750
|
+
allItems.push({
|
751
|
+
src: agentSrc,
|
752
|
+
dest: agentDest,
|
753
|
+
type: 'file',
|
754
|
+
relativePath: agentRelPath,
|
755
|
+
exists: fs.existsSync(agentDest)
|
756
|
+
});
|
757
|
+
|
758
|
+
if (fs.existsSync(agentDest)) {
|
759
|
+
fileConflicts.push(agentRelPath);
|
760
|
+
conflictsByCategory['agents'].push(agentRelPath);
|
761
|
+
}
|
762
|
+
}
|
763
|
+
}
|
764
|
+
|
765
|
+
// Always include README.md
|
766
|
+
const readmeSrc = path.join(src, 'README.md');
|
767
|
+
const readmeDest = path.join(dest, 'README.md');
|
768
|
+
if (fs.existsSync(readmeSrc)) {
|
769
|
+
allItems.push({
|
770
|
+
src: readmeSrc,
|
771
|
+
dest: readmeDest,
|
772
|
+
type: 'file',
|
773
|
+
relativePath: path.join(relativePath, 'README.md'),
|
774
|
+
exists: fs.existsSync(readmeDest)
|
775
|
+
});
|
776
|
+
|
777
|
+
if (fs.existsSync(readmeDest)) {
|
778
|
+
const readmePath = path.join(relativePath, 'README.md');
|
779
|
+
fileConflicts.push(readmePath);
|
780
|
+
conflictsByCategory['agents'].push(readmePath);
|
781
|
+
}
|
782
|
+
}
|
783
|
+
|
784
|
+
return; // Don't recurse into agents directory
|
785
|
+
}
|
786
|
+
|
787
|
+
allItems.push({
|
788
|
+
src,
|
789
|
+
dest,
|
790
|
+
type: 'directory',
|
791
|
+
relativePath,
|
792
|
+
exists: fs.existsSync(dest)
|
793
|
+
});
|
794
|
+
|
795
|
+
if (fs.existsSync(dest)) {
|
796
|
+
dirConflicts.push(relativePath || '.');
|
797
|
+
}
|
798
|
+
|
799
|
+
fs.readdirSync(src).forEach(childItem => {
|
800
|
+
scanTemplate(
|
801
|
+
path.join(src, childItem),
|
802
|
+
path.join(dest, childItem),
|
803
|
+
path.join(relativePath, childItem),
|
804
|
+
skipAgents
|
805
|
+
);
|
806
|
+
});
|
807
|
+
} else {
|
808
|
+
allItems.push({
|
809
|
+
src,
|
810
|
+
dest,
|
811
|
+
type: 'file',
|
812
|
+
relativePath,
|
813
|
+
exists: fs.existsSync(dest)
|
814
|
+
});
|
815
|
+
|
816
|
+
if (fs.existsSync(dest)) {
|
817
|
+
fileConflicts.push(relativePath);
|
818
|
+
|
819
|
+
// Categorize the conflict
|
820
|
+
if (relativePath === 'CLAUDE.md') {
|
821
|
+
conflictsByCategory['CLAUDE.md'].push(relativePath);
|
822
|
+
} else if (relativePath.startsWith('agents/')) {
|
823
|
+
conflictsByCategory['agents'].push(relativePath);
|
824
|
+
} else if (relativePath.startsWith('docs/')) {
|
825
|
+
conflictsByCategory['docs'].push(relativePath);
|
826
|
+
} else if (relativePath.startsWith('plans/')) {
|
827
|
+
conflictsByCategory['plans'].push(relativePath);
|
828
|
+
} else if (relativePath.startsWith('tickets/')) {
|
829
|
+
conflictsByCategory['tickets'].push(relativePath);
|
830
|
+
}
|
831
|
+
}
|
832
|
+
}
|
833
|
+
} catch (error) {
|
834
|
+
throw new Error(`Error scanning template: ${error.message}`);
|
835
|
+
}
|
836
|
+
}
|
837
|
+
|
838
|
+
scanTemplate(templateDir, targetDir, '', true);
|
839
|
+
|
840
|
+
// Handle force flag
|
841
|
+
if (flags.force) {
|
842
|
+
// Set all strategies to overwrite
|
843
|
+
Object.keys(conflictStrategies).forEach(key => {
|
844
|
+
conflictStrategies[key] = 'overwrite';
|
845
|
+
});
|
846
|
+
if (fileConflicts.length > 0 || dirConflicts.length > 0) {
|
847
|
+
console.log('\nā ļø Force mode enabled - existing files will be overwritten');
|
848
|
+
}
|
849
|
+
} else {
|
850
|
+
// Show conflicts if not in force mode
|
851
|
+
if (dirConflicts.length > 0) {
|
852
|
+
console.log('\nā ļø The following directories already exist:');
|
853
|
+
dirConflicts.forEach(dir => console.log(` - ${dir}/`));
|
854
|
+
}
|
855
|
+
|
856
|
+
if (fileConflicts.length > 0) {
|
857
|
+
console.log('\nā ļø File conflicts detected. You will be asked how to handle each category.');
|
858
|
+
|
859
|
+
if (!flags.dryRun) {
|
860
|
+
// Ask for resolution strategy for each category with conflicts
|
861
|
+
const categories = [
|
862
|
+
{ key: 'CLAUDE.md', name: 'CLAUDE.md', emoji: 'š' },
|
863
|
+
{ key: 'agents', name: 'Agents', emoji: 'š¤' },
|
864
|
+
{ key: 'docs', name: 'Documentation', emoji: 'š' },
|
865
|
+
{ key: 'plans', name: 'Plans', emoji: 'š' },
|
866
|
+
{ key: 'tickets', name: 'Tickets', emoji: 'š«' }
|
867
|
+
];
|
868
|
+
|
869
|
+
for (const category of categories) {
|
870
|
+
if (conflictsByCategory[category.key].length > 0) {
|
871
|
+
console.log(`\n${category.emoji} ${category.name} conflicts:`);
|
872
|
+
conflictsByCategory[category.key].forEach(file => console.log(` - ${file}`));
|
873
|
+
|
874
|
+
console.log('\nConflict resolution options:');
|
875
|
+
console.log(' 1) skip (s) - Keep your existing files');
|
876
|
+
console.log(' 2) rename (r) - Save template files with -ccsetup suffix');
|
877
|
+
console.log(' 3) overwrite (o) - Replace with template versions');
|
878
|
+
|
879
|
+
const userInput = await prompt(`Your choice for ${category.name} [s/r/o]: `);
|
880
|
+
const strategy = normalizeConflictStrategy(userInput);
|
881
|
+
|
882
|
+
if (!strategy) {
|
883
|
+
console.log(`\nā Invalid option: "${userInput}". Please use: skip/s/1, rename/r/2, or overwrite/o/3`);
|
884
|
+
if (rl) rl.close();
|
885
|
+
process.exit(1);
|
886
|
+
}
|
887
|
+
|
888
|
+
if (strategy === 'overwrite' && category.key === 'CLAUDE.md') {
|
889
|
+
const confirm = await prompt('ā ļø Are you sure you want to overwrite CLAUDE.md? This will lose your project instructions! (yes/no): ');
|
890
|
+
if (confirm !== 'yes') {
|
891
|
+
conflictStrategies[category.key] = 'skip';
|
892
|
+
console.log('Keeping existing CLAUDE.md');
|
893
|
+
continue;
|
894
|
+
}
|
895
|
+
}
|
896
|
+
|
897
|
+
conflictStrategies[category.key] = strategy;
|
898
|
+
}
|
899
|
+
}
|
900
|
+
}
|
901
|
+
}
|
902
|
+
}
|
903
|
+
|
904
|
+
console.log(`\n⨠${flags.dryRun ? 'Would apply' : 'Applying'} conflict resolution strategies...`);
|
905
|
+
|
906
|
+
let skippedCount = 0;
|
907
|
+
let copiedCount = 0;
|
908
|
+
let renamedCount = 0;
|
909
|
+
let overwrittenCount = 0;
|
910
|
+
|
911
|
+
for (const item of allItems) {
|
912
|
+
try {
|
913
|
+
if (item.type === 'directory') {
|
914
|
+
if (!item.exists && !fs.existsSync(item.dest)) {
|
915
|
+
if (!flags.dryRun) {
|
916
|
+
fs.mkdirSync(item.dest, { recursive: true });
|
917
|
+
} else {
|
918
|
+
console.log(` š Would create directory: ${item.relativePath || '.'}/`);
|
919
|
+
}
|
920
|
+
}
|
921
|
+
} else {
|
922
|
+
if (item.exists) {
|
923
|
+
// Determine which category this file belongs to
|
924
|
+
let strategy = 'skip'; // default
|
925
|
+
if (item.relativePath === 'CLAUDE.md') {
|
926
|
+
strategy = conflictStrategies['CLAUDE.md'];
|
927
|
+
} else if (item.relativePath.startsWith('agents/')) {
|
928
|
+
strategy = conflictStrategies['agents'];
|
929
|
+
} else if (item.relativePath.startsWith('docs/')) {
|
930
|
+
strategy = conflictStrategies['docs'];
|
931
|
+
} else if (item.relativePath.startsWith('plans/')) {
|
932
|
+
strategy = conflictStrategies['plans'];
|
933
|
+
} else if (item.relativePath.startsWith('tickets/')) {
|
934
|
+
strategy = conflictStrategies['tickets'];
|
935
|
+
}
|
936
|
+
|
937
|
+
if (strategy === 'skip') {
|
938
|
+
skippedCount++;
|
939
|
+
if (flags.dryRun) {
|
940
|
+
console.log(` āļø Would skip: ${item.relativePath}`);
|
941
|
+
}
|
942
|
+
continue;
|
943
|
+
} else if (strategy === 'rename') {
|
944
|
+
const ext = path.extname(item.dest);
|
945
|
+
const baseName = path.basename(item.dest, ext);
|
946
|
+
const dirName = path.dirname(item.dest);
|
947
|
+
let newDest = path.join(dirName, `${baseName}-ccsetup${ext}`);
|
948
|
+
let counter = 1;
|
949
|
+
while (fs.existsSync(newDest)) {
|
950
|
+
newDest = path.join(dirName, `${baseName}-ccsetup-${counter}${ext}`);
|
951
|
+
counter++;
|
952
|
+
}
|
953
|
+
if (!flags.dryRun) {
|
954
|
+
fs.copyFileSync(item.src, newDest);
|
955
|
+
}
|
956
|
+
renamedCount++;
|
957
|
+
console.log(` š ${flags.dryRun ? 'Would create' : 'Created'}: ${path.relative(targetDir, newDest)}`);
|
958
|
+
} else if (strategy === 'overwrite') {
|
959
|
+
if (!flags.dryRun) {
|
960
|
+
fs.copyFileSync(item.src, item.dest);
|
961
|
+
}
|
962
|
+
overwrittenCount++;
|
963
|
+
console.log(` ā»ļø ${flags.dryRun ? 'Would replace' : 'Replaced'}: ${item.relativePath}`);
|
964
|
+
}
|
965
|
+
} else {
|
966
|
+
if (!flags.dryRun) {
|
967
|
+
// Ensure directory exists before copying file
|
968
|
+
const destDir = path.dirname(item.dest);
|
969
|
+
if (!fs.existsSync(destDir)) {
|
970
|
+
fs.mkdirSync(destDir, { recursive: true });
|
971
|
+
}
|
972
|
+
fs.copyFileSync(item.src, item.dest);
|
973
|
+
} else {
|
974
|
+
console.log(` ⨠Would copy: ${item.relativePath}`);
|
975
|
+
}
|
976
|
+
copiedCount++;
|
977
|
+
}
|
978
|
+
}
|
979
|
+
} catch (error) {
|
980
|
+
if (error.code === 'EACCES') {
|
981
|
+
throw new Error(`Permission denied: ${item.relativePath}`);
|
982
|
+
} else if (error.code === 'ENOSPC') {
|
983
|
+
throw new Error('No space left on device');
|
984
|
+
} else if (error.code === 'EISDIR') {
|
985
|
+
throw new Error(`Cannot overwrite directory with file: ${item.relativePath}`);
|
986
|
+
}
|
987
|
+
throw new Error(`Failed to process ${item.relativePath}: ${error.message}`);
|
988
|
+
}
|
989
|
+
}
|
990
|
+
|
991
|
+
// Initialize .claude directory and copy agents
|
992
|
+
let claudeInitResult = null;
|
993
|
+
// Always initialize .claude directory structure (it will handle existing directories)
|
994
|
+
console.log(`\nš§ ${claudeStatus.hasClaudeDir ? 'Updating' : 'Initializing'} .claude directory structure...`);
|
995
|
+
claudeInitResult = await initializeClaudeDirectory(selectedAgentFiles, conflictStrategies['agents'], flags.dryRun);
|
996
|
+
|
997
|
+
if (claudeInitResult.createdItems.length > 0) {
|
998
|
+
console.log(` ā
Created ${claudeInitResult.createdItems.length} items in .claude directory`);
|
999
|
+
}
|
1000
|
+
if (claudeInitResult.copiedAgents > 0) {
|
1001
|
+
console.log(` š¤ Copied ${claudeInitResult.copiedAgents} agents to .claude/agents`);
|
1002
|
+
}
|
1003
|
+
if (claudeInitResult.skippedAgents > 0) {
|
1004
|
+
console.log(` āļø Skipped ${claudeInitResult.skippedAgents} existing agents in .claude/agents`);
|
1005
|
+
}
|
1006
|
+
|
1007
|
+
console.log(`\nā
Claude Code project ${flags.dryRun ? 'would be' : ''} created successfully!`);
|
1008
|
+
|
1009
|
+
// Show summary of what happened
|
1010
|
+
if (fileConflicts.length > 0 || copiedCount > 0 || selectedAgentFiles.length > 0 || claudeInitResult) {
|
1011
|
+
console.log('\nš Summary:');
|
1012
|
+
if (copiedCount > 0) console.log(` ⨠${copiedCount} new files ${flags.dryRun ? 'would be' : ''} copied`);
|
1013
|
+
if (skippedCount > 0) console.log(` āļø ${skippedCount} existing files ${flags.dryRun ? 'would be' : ''} kept unchanged`);
|
1014
|
+
if (renamedCount > 0) console.log(` š ${renamedCount} template files ${flags.dryRun ? 'would be' : ''} saved with -ccsetup suffix`);
|
1015
|
+
if (overwrittenCount > 0) console.log(` ā»ļø ${overwrittenCount} files ${flags.dryRun ? 'would be' : ''} replaced with template versions`);
|
1016
|
+
if (!flags.noAgents && !flags.dryRun) {
|
1017
|
+
console.log(` š¤ ${selectedAgentFiles.length} agent${selectedAgentFiles.length === 1 ? '' : 's'} ${flags.dryRun ? 'would be' : ''} included in /agents`);
|
1018
|
+
}
|
1019
|
+
if (claudeInitResult && claudeInitResult.createdItems.length > 0) {
|
1020
|
+
console.log(` š ${claudeInitResult.createdItems.length} items created in .claude directory`);
|
1021
|
+
}
|
1022
|
+
}
|
1023
|
+
|
1024
|
+
if (!flags.dryRun) {
|
1025
|
+
console.log('\nNext steps:');
|
1026
|
+
if (projectName !== '.') {
|
1027
|
+
// Escape project name to prevent command injection
|
1028
|
+
const escapedProjectName = JSON.stringify(projectName);
|
1029
|
+
console.log(` cd ${escapedProjectName}`);
|
1030
|
+
const finalClaudeStatus = checkClaudeCode();
|
1031
|
+
if (!finalClaudeStatus.isInstalled) {
|
1032
|
+
console.log(' claude init # Initialize Claude Code in the project');
|
1033
|
+
}
|
1034
|
+
}
|
1035
|
+
console.log(' 1. Edit CLAUDE.md to add your project-specific instructions');
|
1036
|
+
console.log(' 2. Update docs/ROADMAP.md with your project goals');
|
1037
|
+
console.log(' 3. Start creating tickets in the tickets/ directory');
|
1038
|
+
|
1039
|
+
if (renamedCount > 0) {
|
1040
|
+
console.log('\nš” Tip: Review the -ccsetup files to see template examples');
|
1041
|
+
console.log(' You can compare them with your existing files or copy sections you need');
|
1042
|
+
}
|
1043
|
+
|
1044
|
+
console.log('\nHappy coding with Claude! š');
|
1045
|
+
}
|
1046
|
+
|
1047
|
+
} catch (error) {
|
1048
|
+
throw error;
|
1049
|
+
} finally {
|
1050
|
+
if (rl) {
|
1051
|
+
rl.close();
|
1052
|
+
}
|
1053
|
+
}
|
1054
|
+
}
|
1055
|
+
|
1056
|
+
main().catch(err => {
|
1057
|
+
console.error('Error:', err.message || err);
|
1058
|
+
if (rl) {
|
1059
|
+
rl.close();
|
1060
|
+
}
|
1061
|
+
process.exit(1);
|
1062
|
+
});
|