jun-claude-code 0.6.0 → 0.6.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.
Files changed (2) hide show
  1. package/dist/copy.js +224 -52
  2. package/package.json +3 -2
package/dist/copy.js CHANGED
@@ -40,25 +40,10 @@ exports.mergeSettingsJson = mergeSettingsJson;
40
40
  exports.copyClaudeFiles = copyClaudeFiles;
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
- const readline = __importStar(require("readline"));
44
43
  const crypto = __importStar(require("crypto"));
45
44
  const chalk_1 = __importDefault(require("chalk"));
46
- /**
47
- * Prompt user for confirmation using readline
48
- */
49
- function askConfirmation(question) {
50
- const rl = readline.createInterface({
51
- input: process.stdin,
52
- output: process.stdout,
53
- });
54
- return new Promise((resolve) => {
55
- rl.question(question, (answer) => {
56
- rl.close();
57
- const normalized = answer.toLowerCase().trim();
58
- resolve(normalized === 'y' || normalized === 'yes');
59
- });
60
- });
61
- }
45
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
46
+ const { MultiSelect } = require('enquirer');
62
47
  /**
63
48
  * Calculate SHA-256 hash of a file
64
49
  */
@@ -119,6 +104,137 @@ function getDestClaudeDir() {
119
104
  }
120
105
  return path.join(homeDir, '.claude');
121
106
  }
107
+ /**
108
+ * Categorize files into agents, skills (top-level dirs), and others
109
+ */
110
+ function categorizeFiles(files) {
111
+ const agents = [];
112
+ const skillDirs = new Set();
113
+ const others = [];
114
+ for (const file of files) {
115
+ if (file.startsWith('agents/')) {
116
+ agents.push(file);
117
+ }
118
+ else if (file.startsWith('skills/')) {
119
+ const parts = file.split('/');
120
+ if (parts.length >= 2 && parts[1]) {
121
+ skillDirs.add(parts[1]);
122
+ }
123
+ }
124
+ else {
125
+ others.push(file);
126
+ }
127
+ }
128
+ return {
129
+ agents: agents.sort(),
130
+ skills: Array.from(skillDirs).sort(),
131
+ others: others.sort(),
132
+ };
133
+ }
134
+ /**
135
+ * Get file status (new/changed/unchanged)
136
+ */
137
+ function getFileStatus(sourcePath, destPath) {
138
+ if (!fs.existsSync(destPath))
139
+ return 'new';
140
+ return getFileHash(sourcePath) === getFileHash(destPath) ? 'unchanged' : 'changed';
141
+ }
142
+ /**
143
+ * Get skill directory status by checking all files within
144
+ */
145
+ function getSkillStatus(skillName, sourceDir, destDir) {
146
+ const sourceSkillDir = path.join(sourceDir, 'skills', skillName);
147
+ const destSkillDir = path.join(destDir, 'skills', skillName);
148
+ if (!fs.existsSync(destSkillDir))
149
+ return 'new';
150
+ const sourceFiles = getAllFiles(sourceSkillDir, sourceSkillDir);
151
+ for (const file of sourceFiles) {
152
+ const src = path.join(sourceSkillDir, file);
153
+ const dst = path.join(destSkillDir, file);
154
+ if (!fs.existsSync(dst) || getFileHash(src) !== getFileHash(dst)) {
155
+ return 'changed';
156
+ }
157
+ }
158
+ return 'unchanged';
159
+ }
160
+ /**
161
+ * Format status label for display
162
+ */
163
+ function statusLabel(status) {
164
+ switch (status) {
165
+ case 'new': return chalk_1.default.green('new');
166
+ case 'changed': return chalk_1.default.yellow('changed');
167
+ case 'unchanged': return chalk_1.default.gray('unchanged');
168
+ }
169
+ }
170
+ /**
171
+ * Format status label with brackets for log output
172
+ */
173
+ function statusBracket(status) {
174
+ switch (status) {
175
+ case 'new': return chalk_1.default.green('[new]');
176
+ case 'changed': return chalk_1.default.yellow('[changed]');
177
+ case 'unchanged': return chalk_1.default.gray('[unchanged]');
178
+ }
179
+ }
180
+ /**
181
+ * Show MultiSelect prompt for a category
182
+ */
183
+ async function selectItems(category, items) {
184
+ if (items.length === 0)
185
+ return [];
186
+ const choices = items.map(item => ({
187
+ name: item.name,
188
+ message: item.displayName,
189
+ hint: statusLabel(item.status),
190
+ enabled: item.status !== 'unchanged',
191
+ }));
192
+ const prompt = new MultiSelect({
193
+ name: category,
194
+ message: `Select ${category} to install`,
195
+ choices,
196
+ hint: '(↑↓ navigate, <space> toggle, <a> select all, <enter> confirm)',
197
+ });
198
+ try {
199
+ return await prompt.run();
200
+ }
201
+ catch {
202
+ console.log(chalk_1.default.yellow('\nInstallation cancelled.'));
203
+ process.exit(0);
204
+ }
205
+ }
206
+ /**
207
+ * Show MultiSelect prompt for skill sub-files across multiple skills.
208
+ * Groups files by skill with separators.
209
+ */
210
+ async function selectSkillSubFiles(skills, sourceDir, destDir) {
211
+ const choices = [];
212
+ for (const { skillName, subFiles } of skills) {
213
+ choices.push({ role: 'separator', message: chalk_1.default.cyan(`── ${skillName} ──`) });
214
+ for (const file of subFiles) {
215
+ const status = getFileStatus(path.join(sourceDir, file), path.join(destDir, file));
216
+ choices.push({
217
+ name: file,
218
+ message: ` ${path.basename(file)}`,
219
+ hint: statusLabel(status),
220
+ enabled: status !== 'unchanged',
221
+ });
222
+ }
223
+ }
224
+ const prompt = new MultiSelect({
225
+ name: 'skill-files',
226
+ message: 'Select skill files to install',
227
+ choices,
228
+ hint: '(↑↓ navigate, <space> toggle, <a> select all, <enter> confirm)',
229
+ });
230
+ try {
231
+ return await prompt.run();
232
+ }
233
+ catch {
234
+ console.log(chalk_1.default.yellow('\nInstallation cancelled.'));
235
+ process.exit(0);
236
+ }
237
+ }
122
238
  /**
123
239
  * Merge settings.json from source into destination.
124
240
  * Hooks are merged per event key; duplicate hook entries (by deep equality) are skipped.
@@ -293,27 +409,37 @@ async function copyClaudeFiles(options = {}) {
293
409
  console.log(chalk_1.default.yellow('No files found in templates/global directory'));
294
410
  return;
295
411
  }
296
- console.log(chalk_1.default.cyan(`Found ${files.length} files to copy:`));
412
+ const categorized = categorizeFiles(files);
413
+ console.log(chalk_1.default.cyan(`Found ${files.length} files (${categorized.agents.length} agents, ${categorized.skills.length} skills, ${categorized.others.length} others)`));
297
414
  console.log();
298
- // Dry run mode - just show what would be copied
415
+ // Dry run mode - show categorized status
299
416
  if (dryRun) {
300
417
  console.log(chalk_1.default.yellow('[DRY RUN] Files that would be copied:'));
301
418
  console.log();
302
- for (const file of files) {
303
- const sourcePath = path.join(sourceDir, file);
304
- const destPath = path.join(destDir, file);
305
- const exists = fs.existsSync(destPath);
306
- if (exists) {
307
- const sourceHash = getFileHash(sourcePath);
308
- const destHash = getFileHash(destPath);
309
- const status = sourceHash === destHash ? chalk_1.default.gray('[unchanged]') : chalk_1.default.yellow('[overwrite]');
310
- console.log(` ${status} ${file}`);
419
+ if (categorized.agents.length > 0) {
420
+ console.log(chalk_1.default.cyan(' Agents:'));
421
+ for (const file of categorized.agents) {
422
+ const status = getFileStatus(path.join(sourceDir, file), path.join(destDir, file));
423
+ console.log(` ${statusBracket(status)} ${path.basename(file, '.md')}`);
311
424
  }
312
- else {
313
- console.log(` ${chalk_1.default.green('[new]')} ${file}`);
425
+ console.log();
426
+ }
427
+ if (categorized.skills.length > 0) {
428
+ console.log(chalk_1.default.cyan(' Skills:'));
429
+ for (const skill of categorized.skills) {
430
+ const status = getSkillStatus(skill, sourceDir, destDir);
431
+ console.log(` ${statusBracket(status)} ${skill}`);
432
+ }
433
+ console.log();
434
+ }
435
+ if (categorized.others.length > 0) {
436
+ console.log(chalk_1.default.cyan(' Others (auto-install):'));
437
+ for (const file of categorized.others) {
438
+ const status = getFileStatus(path.join(sourceDir, file), path.join(destDir, file));
439
+ console.log(` ${statusBracket(status)} ${file}`);
314
440
  }
441
+ console.log();
315
442
  }
316
- // settings.json merge indicator
317
443
  const sourceSettingsExists = fs.existsSync(path.join(sourceDir, 'settings.json'));
318
444
  if (sourceSettingsExists) {
319
445
  console.log(` ${chalk_1.default.blue('[merge]')} settings.json`);
@@ -322,34 +448,80 @@ async function copyClaudeFiles(options = {}) {
322
448
  console.log(chalk_1.default.yellow('No files were copied (dry run mode)'));
323
449
  return;
324
450
  }
325
- // Copy files
451
+ // Determine files to copy per category
452
+ let agentFiles = [];
453
+ let skillFiles = [];
454
+ let otherFiles = [];
455
+ if (force) {
456
+ agentFiles = categorized.agents;
457
+ skillFiles = files.filter(f => f.startsWith('skills/'));
458
+ otherFiles = categorized.others;
459
+ }
460
+ else {
461
+ // Others: auto-copy new/changed files
462
+ for (const file of categorized.others) {
463
+ const status = getFileStatus(path.join(sourceDir, file), path.join(destDir, file));
464
+ if (status !== 'unchanged') {
465
+ otherFiles.push(file);
466
+ }
467
+ }
468
+ // Agents: MultiSelect
469
+ if (categorized.agents.length > 0) {
470
+ const agentItems = categorized.agents.map(file => ({
471
+ name: file,
472
+ displayName: path.basename(file, '.md'),
473
+ status: getFileStatus(path.join(sourceDir, file), path.join(destDir, file)),
474
+ }));
475
+ agentFiles = await selectItems('Agents', agentItems);
476
+ }
477
+ // Skills: MultiSelect (2-step)
478
+ if (categorized.skills.length > 0) {
479
+ const skillItems = categorized.skills.map(skill => ({
480
+ name: skill,
481
+ displayName: skill,
482
+ status: getSkillStatus(skill, sourceDir, destDir),
483
+ }));
484
+ const selectedSkills = await selectItems('Skills', skillItems);
485
+ // Step 2: 선택된 스킬의 하위 파일 선택
486
+ const multiFileSkills = [];
487
+ for (const skillName of selectedSkills) {
488
+ const skillSubFiles = files.filter(f => f.startsWith(`skills/${skillName}/`));
489
+ if (skillSubFiles.length > 1) {
490
+ multiFileSkills.push({ skillName, subFiles: skillSubFiles });
491
+ }
492
+ else {
493
+ // 파일이 1개뿐이면 자동 포함
494
+ skillFiles.push(...skillSubFiles);
495
+ }
496
+ }
497
+ // 하위 파일 선택이 필요한 스킬이 있으면 한 번의 프롬프트로 표시
498
+ if (multiFileSkills.length > 0) {
499
+ const selectedSubFiles = await selectSkillSubFiles(multiFileSkills, sourceDir, destDir);
500
+ skillFiles.push(...selectedSubFiles);
501
+ }
502
+ }
503
+ }
504
+ // Copy all selected files
505
+ const allFilesToCopy = [...otherFiles, ...agentFiles, ...skillFiles];
326
506
  let copiedCount = 0;
327
- let skippedCount = 0;
328
- for (const file of files) {
329
- const sourcePath = path.join(sourceDir, file);
330
- const destPath = path.join(destDir, file);
331
- const exists = fs.existsSync(destPath);
332
- if (exists && !force) {
333
- const sourceHash = getFileHash(sourcePath);
334
- const destHash = getFileHash(destPath);
335
- if (sourceHash === destHash) {
507
+ if (otherFiles.length > 0 || categorized.others.length > 0) {
508
+ // Show unchanged others for context
509
+ for (const file of categorized.others) {
510
+ if (!otherFiles.includes(file)) {
336
511
  console.log(` ${chalk_1.default.gray('[unchanged]')} ${file}`);
337
- skippedCount++;
338
- continue;
339
- }
340
- // Hash differs - ask for confirmation
341
- const shouldOverwrite = await askConfirmation(chalk_1.default.yellow(`File changed: ${file}. Overwrite? (y/N): `));
342
- if (!shouldOverwrite) {
343
- console.log(chalk_1.default.gray(` Skipped: ${file}`));
344
- skippedCount++;
345
- continue;
346
512
  }
347
513
  }
514
+ }
515
+ for (const file of allFilesToCopy) {
516
+ const sourcePath = path.join(sourceDir, file);
517
+ const destPath = path.join(destDir, file);
518
+ const exists = fs.existsSync(destPath);
348
519
  copyFile(sourcePath, destPath);
349
- const status = exists ? chalk_1.default.yellow('[overwritten]') : chalk_1.default.green('[created]');
350
- console.log(` ${status} ${file}`);
520
+ const label = exists ? chalk_1.default.yellow('[overwritten]') : chalk_1.default.green('[created]');
521
+ console.log(` ${label} ${file}`);
351
522
  copiedCount++;
352
523
  }
524
+ const skippedCount = files.length - copiedCount;
353
525
  // Merge settings.json (hooks are merged, not overwritten)
354
526
  mergeSettingsJson(sourceDir, destDir, { project });
355
527
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jun-claude-code",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Claude Code configuration template - copy .claude settings to your project",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/cli.js",
@@ -16,7 +16,8 @@
16
16
  "packageManager": "yarn@3.8.7",
17
17
  "dependencies": {
18
18
  "chalk": "^4.1.2",
19
- "commander": "^11.1.0"
19
+ "commander": "^11.1.0",
20
+ "enquirer": "^2.4.1"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@types/node": "^20.10.0",