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.
- package/dist/copy.js +224 -52
- 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
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
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
|
|
350
|
-
console.log(` ${
|
|
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.
|
|
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",
|