jun-claude-code 0.6.3 → 0.6.7
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/__tests__/metadata.test.d.ts +1 -0
- package/dist/__tests__/metadata.test.js +178 -0
- package/dist/__tests__/update.test.d.ts +1 -0
- package/dist/__tests__/update.test.js +154 -0
- package/dist/cli.js +25 -0
- package/dist/copy.d.ts +36 -0
- package/dist/copy.js +26 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/metadata.d.ts +29 -0
- package/dist/metadata.js +112 -0
- package/dist/update.d.ts +16 -0
- package/dist/update.js +433 -0
- package/package.json +1 -1
- package/templates/global/CLAUDE.md +32 -0
- package/templates/global/hooks/workflow-enforced.sh +1 -0
- package/templates/global/skills/Backend/SKILL.md +3 -76
- package/templates/global/skills/TypeORM/SKILL.md +189 -0
package/dist/update.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.computeUpdateStatus = computeUpdateStatus;
|
|
40
|
+
exports.updateClaudeFiles = updateClaudeFiles;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
44
|
+
const copy_1 = require("./copy");
|
|
45
|
+
const metadata_1 = require("./metadata");
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
47
|
+
const { MultiSelect } = require('enquirer');
|
|
48
|
+
/**
|
|
49
|
+
* Compute 3-way update status for a file.
|
|
50
|
+
* Compares base (from metadata), current (on disk), and new (from template).
|
|
51
|
+
*/
|
|
52
|
+
function computeUpdateStatus(file, sourceDir, destDir, metadata) {
|
|
53
|
+
const sourcePath = path.join(sourceDir, file);
|
|
54
|
+
const destPath = path.join(destDir, file);
|
|
55
|
+
const newHash = (0, copy_1.getFileHash)(sourcePath);
|
|
56
|
+
const baseHash = metadata?.files[file]?.hash ?? null;
|
|
57
|
+
const currentExists = fs.existsSync(destPath);
|
|
58
|
+
const currentHash = currentExists ? (0, copy_1.getFileHash)(destPath) : null;
|
|
59
|
+
// No base metadata for this file
|
|
60
|
+
if (!baseHash) {
|
|
61
|
+
if (!currentExists)
|
|
62
|
+
return 'new-file';
|
|
63
|
+
if (currentHash === newHash)
|
|
64
|
+
return 'unchanged';
|
|
65
|
+
return 'user-modified';
|
|
66
|
+
}
|
|
67
|
+
// Base exists but user deleted the file
|
|
68
|
+
if (!currentExists)
|
|
69
|
+
return 'new-file';
|
|
70
|
+
// All three hashes available
|
|
71
|
+
if (baseHash === currentHash && currentHash === newHash)
|
|
72
|
+
return 'unchanged';
|
|
73
|
+
if (baseHash === currentHash)
|
|
74
|
+
return 'update-available';
|
|
75
|
+
if (baseHash === newHash)
|
|
76
|
+
return 'user-modified';
|
|
77
|
+
return 'conflict';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Fetch latest version from npm registry
|
|
81
|
+
*/
|
|
82
|
+
async function fetchLatestVersion(packageName) {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
85
|
+
if (!response.ok)
|
|
86
|
+
return null;
|
|
87
|
+
const data = (await response.json());
|
|
88
|
+
return data.version ?? null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Format update status for MultiSelect hint
|
|
96
|
+
*/
|
|
97
|
+
function updateStatusLabel(status) {
|
|
98
|
+
switch (status) {
|
|
99
|
+
case 'update-available': return chalk_1.default.green('update');
|
|
100
|
+
case 'new-file': return chalk_1.default.green('new');
|
|
101
|
+
case 'user-modified': return chalk_1.default.yellow('customized');
|
|
102
|
+
case 'conflict': return chalk_1.default.red('conflict');
|
|
103
|
+
case 'unchanged': return chalk_1.default.gray('unchanged');
|
|
104
|
+
case 'removed-upstream': return chalk_1.default.gray('removed upstream');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Format update status bracket for log output
|
|
109
|
+
*/
|
|
110
|
+
function updateStatusBracket(status) {
|
|
111
|
+
switch (status) {
|
|
112
|
+
case 'update-available': return chalk_1.default.green('[update]');
|
|
113
|
+
case 'new-file': return chalk_1.default.green('[new]');
|
|
114
|
+
case 'user-modified': return chalk_1.default.yellow('[skip]');
|
|
115
|
+
case 'conflict': return chalk_1.default.red('[conflict]');
|
|
116
|
+
case 'unchanged': return chalk_1.default.gray('[unchanged]');
|
|
117
|
+
case 'removed-upstream': return chalk_1.default.gray('[removed]');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get aggregate update status for a skill directory
|
|
122
|
+
*/
|
|
123
|
+
function getSkillUpdateStatus(skillName, allStatuses) {
|
|
124
|
+
const skillFiles = allStatuses.filter(f => f.file.startsWith(`skills/${skillName}/`));
|
|
125
|
+
if (skillFiles.length === 0)
|
|
126
|
+
return 'unchanged';
|
|
127
|
+
if (skillFiles.some(f => f.status === 'conflict'))
|
|
128
|
+
return 'conflict';
|
|
129
|
+
if (skillFiles.some(f => f.status === 'update-available'))
|
|
130
|
+
return 'update-available';
|
|
131
|
+
if (skillFiles.some(f => f.status === 'new-file'))
|
|
132
|
+
return 'new-file';
|
|
133
|
+
if (skillFiles.some(f => f.status === 'user-modified'))
|
|
134
|
+
return 'user-modified';
|
|
135
|
+
return 'unchanged';
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* MultiSelect prompt for update items
|
|
139
|
+
*/
|
|
140
|
+
async function selectUpdateItems(category, items) {
|
|
141
|
+
if (items.length === 0)
|
|
142
|
+
return [];
|
|
143
|
+
const choices = items.map(item => ({
|
|
144
|
+
name: item.file,
|
|
145
|
+
message: item.file.startsWith('agents/')
|
|
146
|
+
? path.basename(item.file, '.md')
|
|
147
|
+
: item.file,
|
|
148
|
+
hint: updateStatusLabel(item.status) +
|
|
149
|
+
((item.status === 'user-modified' || item.status === 'conflict')
|
|
150
|
+
? ' \u2014 will overwrite your changes' : ''),
|
|
151
|
+
enabled: item.status === 'update-available' || item.status === 'new-file',
|
|
152
|
+
}));
|
|
153
|
+
const prompt = new MultiSelect({
|
|
154
|
+
name: category,
|
|
155
|
+
message: `Select ${category} to update`,
|
|
156
|
+
choices,
|
|
157
|
+
hint: '(\u2191\u2193 navigate, <space> toggle, <a> select all, <enter> confirm)',
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
return await prompt.run();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
console.log(chalk_1.default.yellow('\nUpdate cancelled.'));
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* MultiSelect prompt for skill sub-files across multiple skills
|
|
169
|
+
*/
|
|
170
|
+
async function selectSkillUpdateFiles(skills) {
|
|
171
|
+
const choices = [];
|
|
172
|
+
for (const { skillName, files } of skills) {
|
|
173
|
+
const actionable = files.filter(f => f.status !== 'unchanged');
|
|
174
|
+
if (actionable.length === 0)
|
|
175
|
+
continue;
|
|
176
|
+
choices.push({ role: 'separator', message: chalk_1.default.cyan(`\u2500\u2500 ${skillName} \u2500\u2500`) });
|
|
177
|
+
for (const item of actionable) {
|
|
178
|
+
choices.push({
|
|
179
|
+
name: item.file,
|
|
180
|
+
message: ` ${path.basename(item.file)}`,
|
|
181
|
+
hint: updateStatusLabel(item.status) +
|
|
182
|
+
((item.status === 'user-modified' || item.status === 'conflict')
|
|
183
|
+
? ' \u2014 will overwrite' : ''),
|
|
184
|
+
enabled: item.status === 'update-available' || item.status === 'new-file',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (choices.filter(c => !c.role).length === 0)
|
|
189
|
+
return [];
|
|
190
|
+
const prompt = new MultiSelect({
|
|
191
|
+
name: 'skill-files',
|
|
192
|
+
message: 'Select skill files to update',
|
|
193
|
+
choices,
|
|
194
|
+
hint: '(\u2191\u2193 navigate, <space> toggle, <a> select all, <enter> confirm)',
|
|
195
|
+
});
|
|
196
|
+
try {
|
|
197
|
+
return await prompt.run();
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
console.log(chalk_1.default.yellow('\nUpdate cancelled.'));
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Update installed templates while preserving user customizations
|
|
206
|
+
*/
|
|
207
|
+
async function updateClaudeFiles(options = {}) {
|
|
208
|
+
const { dryRun = false, force = false, project = false } = options;
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
210
|
+
const currentVersion = require('../package.json').version;
|
|
211
|
+
const sourceDir = (0, copy_1.getSourceGlobalDir)();
|
|
212
|
+
const destDir = project ? path.join(process.cwd(), '.claude') : (0, copy_1.getDestClaudeDir)();
|
|
213
|
+
const targetLabel = project ? 'project' : 'global';
|
|
214
|
+
// Step 1: Version info
|
|
215
|
+
const metadata = (0, metadata_1.loadMetadata)(destDir);
|
|
216
|
+
const latestVersion = await fetchLatestVersion('jun-claude-code');
|
|
217
|
+
console.log(chalk_1.default.cyan('Version Info:'));
|
|
218
|
+
console.log(` Package version: ${chalk_1.default.bold(currentVersion)}`);
|
|
219
|
+
if (metadata?.version) {
|
|
220
|
+
console.log(` Installed version: ${chalk_1.default.bold(metadata.version)}`);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
console.log(` Installed version: ${chalk_1.default.gray('(no metadata found)')}`);
|
|
224
|
+
}
|
|
225
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
226
|
+
console.log(` Latest on npm: ${chalk_1.default.bold(latestVersion)}`);
|
|
227
|
+
console.log(` ${chalk_1.default.yellow('Tip:')} npm update -g jun-claude-code`);
|
|
228
|
+
}
|
|
229
|
+
console.log();
|
|
230
|
+
// Check source exists
|
|
231
|
+
if (!fs.existsSync(sourceDir)) {
|
|
232
|
+
console.error(chalk_1.default.red('Error:'), 'Source templates/global directory not found');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
// Step 2: Collect file lists
|
|
236
|
+
const allFiles = (0, copy_1.getAllFiles)(sourceDir);
|
|
237
|
+
const files = allFiles.filter(file => {
|
|
238
|
+
if (copy_1.EXCLUDE_ALWAYS.includes(file))
|
|
239
|
+
return false;
|
|
240
|
+
if (project && copy_1.EXCLUDE_FROM_PROJECT.includes(file))
|
|
241
|
+
return false;
|
|
242
|
+
return true;
|
|
243
|
+
});
|
|
244
|
+
if (files.length === 0) {
|
|
245
|
+
console.log(chalk_1.default.yellow('No template files found.'));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Step 3: Compute 3-way status for each file
|
|
249
|
+
const fileStatuses = files.map(file => ({
|
|
250
|
+
file,
|
|
251
|
+
status: computeUpdateStatus(file, sourceDir, destDir, metadata),
|
|
252
|
+
}));
|
|
253
|
+
// Check for removed-upstream files (in metadata but not in new template)
|
|
254
|
+
if (metadata) {
|
|
255
|
+
for (const file of Object.keys(metadata.files)) {
|
|
256
|
+
if (!files.includes(file) && !copy_1.EXCLUDE_ALWAYS.includes(file)) {
|
|
257
|
+
fileStatuses.push({ file, status: 'removed-upstream' });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Group by status
|
|
262
|
+
const updateAvailable = fileStatuses.filter(f => f.status === 'update-available');
|
|
263
|
+
const newFiles = fileStatuses.filter(f => f.status === 'new-file');
|
|
264
|
+
const userModified = fileStatuses.filter(f => f.status === 'user-modified');
|
|
265
|
+
const conflicts = fileStatuses.filter(f => f.status === 'conflict');
|
|
266
|
+
const unchanged = fileStatuses.filter(f => f.status === 'unchanged');
|
|
267
|
+
const removedUpstream = fileStatuses.filter(f => f.status === 'removed-upstream');
|
|
268
|
+
// Step 4: Show summary
|
|
269
|
+
const installedVer = metadata?.version ?? '(unknown)';
|
|
270
|
+
console.log(chalk_1.default.cyan(`Update Summary (${installedVer} \u2192 ${currentVersion}):`));
|
|
271
|
+
console.log(chalk_1.default.blue('Destination:'), `${destDir} ${chalk_1.default.gray(`(${targetLabel})`)}`);
|
|
272
|
+
console.log();
|
|
273
|
+
if (updateAvailable.length > 0) {
|
|
274
|
+
console.log(chalk_1.default.green(` Safe to update (${updateAvailable.length}):`));
|
|
275
|
+
for (const f of updateAvailable) {
|
|
276
|
+
console.log(` ${updateStatusBracket(f.status)} ${f.file}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (newFiles.length > 0) {
|
|
280
|
+
console.log(chalk_1.default.green(` New files (${newFiles.length}):`));
|
|
281
|
+
for (const f of newFiles) {
|
|
282
|
+
console.log(` ${updateStatusBracket(f.status)} ${f.file}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (userModified.length > 0) {
|
|
286
|
+
console.log(chalk_1.default.yellow(` User-modified \u2014 preserved (${userModified.length}):`));
|
|
287
|
+
for (const f of userModified) {
|
|
288
|
+
console.log(` ${updateStatusBracket(f.status)} ${f.file} ${chalk_1.default.gray('(customized)')}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (conflicts.length > 0) {
|
|
292
|
+
console.log(chalk_1.default.red(` Conflicts (${conflicts.length}):`));
|
|
293
|
+
for (const f of conflicts) {
|
|
294
|
+
console.log(` ${updateStatusBracket(f.status)} ${f.file}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (removedUpstream.length > 0) {
|
|
298
|
+
console.log(chalk_1.default.gray(` Removed upstream (${removedUpstream.length}):`));
|
|
299
|
+
for (const f of removedUpstream) {
|
|
300
|
+
console.log(` ${updateStatusBracket(f.status)} ${f.file}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (unchanged.length > 0) {
|
|
304
|
+
console.log(chalk_1.default.gray(` Unchanged (${unchanged.length}): ...`));
|
|
305
|
+
}
|
|
306
|
+
console.log();
|
|
307
|
+
// Check if there are actionable files
|
|
308
|
+
const actionableStatuses = ['update-available', 'new-file', 'user-modified', 'conflict'];
|
|
309
|
+
const actionableFiles = fileStatuses.filter(f => actionableStatuses.includes(f.status));
|
|
310
|
+
if (actionableFiles.length === 0) {
|
|
311
|
+
console.log(chalk_1.default.green('Everything is up to date!'));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// Dry run: stop here
|
|
315
|
+
if (dryRun) {
|
|
316
|
+
console.log(chalk_1.default.yellow('No files were changed (dry run mode)'));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// Step 5: Determine files to copy
|
|
320
|
+
let filesToCopy;
|
|
321
|
+
if (force) {
|
|
322
|
+
filesToCopy = files;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
filesToCopy = [];
|
|
326
|
+
// Legacy install warning
|
|
327
|
+
if (!metadata) {
|
|
328
|
+
console.log(chalk_1.default.yellow('No installation metadata found. All existing files will be treated as user-modified.'));
|
|
329
|
+
console.log(chalk_1.default.yellow('Use --force to overwrite all files.'));
|
|
330
|
+
console.log();
|
|
331
|
+
}
|
|
332
|
+
// Others: auto-include update-available and new-file
|
|
333
|
+
const categorized = (0, copy_1.categorizeFiles)(actionableFiles.map(f => f.file));
|
|
334
|
+
for (const file of categorized.others) {
|
|
335
|
+
const info = actionableFiles.find(f => f.file === file);
|
|
336
|
+
if (info && (info.status === 'update-available' || info.status === 'new-file')) {
|
|
337
|
+
filesToCopy.push(file);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Agents: MultiSelect
|
|
341
|
+
const agentInfos = actionableFiles.filter(f => f.file.startsWith('agents/'));
|
|
342
|
+
if (agentInfos.length > 0) {
|
|
343
|
+
const selected = await selectUpdateItems('Agents', agentInfos);
|
|
344
|
+
filesToCopy.push(...selected);
|
|
345
|
+
}
|
|
346
|
+
// Skills: 2-step MultiSelect
|
|
347
|
+
const allSkillStatuses = fileStatuses.filter(f => f.file.startsWith('skills/'));
|
|
348
|
+
const skillDirs = new Set();
|
|
349
|
+
for (const f of allSkillStatuses) {
|
|
350
|
+
const parts = f.file.split('/');
|
|
351
|
+
if (parts.length >= 2 && parts[1])
|
|
352
|
+
skillDirs.add(parts[1]);
|
|
353
|
+
}
|
|
354
|
+
// Step 1: Select skill directories
|
|
355
|
+
const skillDirItems = [];
|
|
356
|
+
for (const skillName of Array.from(skillDirs).sort()) {
|
|
357
|
+
const status = getSkillUpdateStatus(skillName, allSkillStatuses);
|
|
358
|
+
if (status !== 'unchanged') {
|
|
359
|
+
skillDirItems.push({ file: skillName, status });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (skillDirItems.length > 0) {
|
|
363
|
+
const selectedSkillDirs = await selectUpdateItems('Skills', skillDirItems);
|
|
364
|
+
// Step 2: For selected skills, show sub-file selection
|
|
365
|
+
const singleFileSkills = [];
|
|
366
|
+
const multiFileSkills = [];
|
|
367
|
+
for (const skillName of selectedSkillDirs) {
|
|
368
|
+
const skillFiles = allSkillStatuses.filter(f => f.file.startsWith(`skills/${skillName}/`));
|
|
369
|
+
const actionableSkillFiles = skillFiles.filter(f => f.status !== 'unchanged');
|
|
370
|
+
if (actionableSkillFiles.length <= 1) {
|
|
371
|
+
// Auto-include single actionable file
|
|
372
|
+
singleFileSkills.push(...actionableSkillFiles.map(f => f.file));
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
multiFileSkills.push({ skillName, files: skillFiles });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
filesToCopy.push(...singleFileSkills);
|
|
379
|
+
if (multiFileSkills.length > 0) {
|
|
380
|
+
const selectedSubFiles = await selectSkillUpdateFiles(multiFileSkills);
|
|
381
|
+
filesToCopy.push(...selectedSubFiles);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Step 6: Copy files
|
|
386
|
+
let copiedCount = 0;
|
|
387
|
+
for (const file of filesToCopy) {
|
|
388
|
+
const sourcePath = path.join(sourceDir, file);
|
|
389
|
+
const destPath = path.join(destDir, file);
|
|
390
|
+
const exists = fs.existsSync(destPath);
|
|
391
|
+
(0, copy_1.copyFile)(sourcePath, destPath);
|
|
392
|
+
const label = exists ? chalk_1.default.yellow('[overwritten]') : chalk_1.default.green('[created]');
|
|
393
|
+
console.log(` ${label} ${file}`);
|
|
394
|
+
copiedCount++;
|
|
395
|
+
}
|
|
396
|
+
// Merge settings.json
|
|
397
|
+
(0, copy_1.mergeSettingsJson)(sourceDir, destDir, { project });
|
|
398
|
+
// Update metadata
|
|
399
|
+
const updatedMeta = (0, metadata_1.mergeMetadata)(metadata, filesToCopy, sourceDir, currentVersion);
|
|
400
|
+
// Refresh hashes for unchanged files
|
|
401
|
+
for (const fi of unchanged) {
|
|
402
|
+
const sourcePath = path.join(sourceDir, fi.file);
|
|
403
|
+
if (fs.existsSync(sourcePath)) {
|
|
404
|
+
updatedMeta.files[fi.file] = { hash: (0, copy_1.getFileHash)(sourcePath) };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
(0, metadata_1.saveMetadata)(destDir, updatedMeta);
|
|
408
|
+
// Step 7: Summary
|
|
409
|
+
console.log();
|
|
410
|
+
const newCount = filesToCopy.filter(f => {
|
|
411
|
+
const info = fileStatuses.find(i => i.file === f);
|
|
412
|
+
return info?.status === 'new-file';
|
|
413
|
+
}).length;
|
|
414
|
+
const updateCount = copiedCount - newCount;
|
|
415
|
+
const preservedCount = userModified.length + conflicts.length -
|
|
416
|
+
filesToCopy.filter(f => {
|
|
417
|
+
const info = fileStatuses.find(i => i.file === f);
|
|
418
|
+
return info && (info.status === 'user-modified' || info.status === 'conflict');
|
|
419
|
+
}).length;
|
|
420
|
+
const parts = [];
|
|
421
|
+
if (updateCount > 0)
|
|
422
|
+
parts.push(`updated ${updateCount} files`);
|
|
423
|
+
if (newCount > 0)
|
|
424
|
+
parts.push(`added ${newCount} new files`);
|
|
425
|
+
if (preservedCount > 0)
|
|
426
|
+
parts.push(`preserved ${preservedCount} customized files`);
|
|
427
|
+
if (parts.length > 0) {
|
|
428
|
+
console.log(chalk_1.default.green(`Done! ${parts.join(', ')}.`));
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
console.log(chalk_1.default.green('Done! No files were updated.'));
|
|
432
|
+
}
|
|
433
|
+
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Claude Code 작업 가이드
|
|
2
2
|
|
|
3
|
+
## Monorepo 주의사항
|
|
4
|
+
|
|
5
|
+
Monorepo 환경에서는 **반드시 repo root에서 Claude Code CLI를 실행**해야 hooks가 정상 작동합니다. 서브 패키지 디렉토리에서 실행하면 `.claude/` 경로를 찾지 못해 hooks가 무시됩니다.
|
|
6
|
+
|
|
3
7
|
## Context 절약 원칙
|
|
4
8
|
|
|
5
9
|
Main Agent의 Context Window는 제한적입니다. Subagent가 할 수 있는 작업은 Subagent에 위임합니다.
|
|
@@ -188,6 +192,34 @@ Plan 파일의 Context 섹션에 위 내용을 명시하여 작업 목적이 희
|
|
|
188
192
|
|
|
189
193
|
<checklist>
|
|
190
194
|
|
|
195
|
+
### Step 3.0: BFF 구현 순서 -- FE/BE/DB가 모두 관련된 기능인 경우
|
|
196
|
+
|
|
197
|
+
> FE가 필요로 하는 기능을 먼저 정의하고, 그에 맞춰 DB와 API를 설계한다.
|
|
198
|
+
|
|
199
|
+
구현 순서:
|
|
200
|
+
1. FE 구현 (UI + 인터랙션)
|
|
201
|
+
2. DB 설계 (FE가 필요로 하는 데이터 구조 기반)
|
|
202
|
+
3. Decision Gate: DB 구현 타당성 평가
|
|
203
|
+
4. API 구현 (FE와 DB를 연결)
|
|
204
|
+
|
|
205
|
+
Decision Gate 판단 기준:
|
|
206
|
+
|
|
207
|
+
| 판단 항목 | 타당 (진행) | 비타당 (분리 또는 FE 수정) |
|
|
208
|
+
|-----------|------------|--------------------------|
|
|
209
|
+
| DB 구현 난이도 | 기존 스키마 확장 수준 | 대규모 스키마 변경, 마이그레이션 리스크 |
|
|
210
|
+
| 유지보수 비용 | 장기적으로 감당 가능 | 사용자 가치 대비 유지보수 비용이 큼 |
|
|
211
|
+
| 사용자 가치 | 핵심 기능, 즉시 필요 | nice-to-have, 후순위 가능 |
|
|
212
|
+
|
|
213
|
+
Decision Gate 결과별 액션:
|
|
214
|
+
- **타당** → DB 구현 후 API 구현으로 진행
|
|
215
|
+
- **FE 수정** → DB에 맞게 FE를 조정한 후 API 구현 (같은 Phase 내)
|
|
216
|
+
- **Phase 분리** → GitHub Issue를 생성하고, 현재 Phase에서 해당 기능 제외
|
|
217
|
+
|
|
218
|
+
적용 범위:
|
|
219
|
+
- FE/BE/DB가 모두 관련된 **신규 기능 구현**에 적용
|
|
220
|
+
- FE-only, BE-only, DB-only 작업에는 건너뛰고 Step 3.1로 진행
|
|
221
|
+
- 단순 CRUD처럼 DB 설계가 자명한 경우에도 건너뛰기 가능
|
|
222
|
+
|
|
191
223
|
### Step 3.1: 작은 단위로 코드 수정
|
|
192
224
|
|
|
193
225
|
필수 원칙:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Backend
|
|
3
|
-
description: NestJS
|
|
4
|
-
keywords: [Backend, 백엔드, 레이어, DTO, Entity, Service, Controller,
|
|
3
|
+
description: NestJS 백엔드 개발 시 사용. 레이어 객체 변환 규칙, BDD 테스트 작성 규칙 제공.
|
|
4
|
+
keywords: [Backend, 백엔드, 레이어, DTO, Entity, Service, Controller, test, BDD, 테스트, Jest]
|
|
5
5
|
user-invocable: false
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -90,78 +90,6 @@ async findOne(@Param('id') id: number) {
|
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
93
|
-
<rules>
|
|
94
|
-
|
|
95
|
-
## TypeORM 사용 규칙
|
|
96
|
-
|
|
97
|
-
> **find 메서드를 기본으로 사용하고, QueryBuilder는 필요한 경우에만 사용한다.**
|
|
98
|
-
|
|
99
|
-
### find 메서드 우선 사용
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
// ✅ 기본 조회
|
|
103
|
-
const user = await this.userRepository.findOneBy({ id });
|
|
104
|
-
const users = await this.userRepository.find({
|
|
105
|
-
where: { status: 'active' },
|
|
106
|
-
relations: ['orders'],
|
|
107
|
-
order: { createdAt: 'DESC' },
|
|
108
|
-
take: 10,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ✅ 조건 조합
|
|
112
|
-
const users = await this.userRepository.find({
|
|
113
|
-
where: [
|
|
114
|
-
{ status: 'active', role: 'admin' },
|
|
115
|
-
{ status: 'active', role: 'manager' },
|
|
116
|
-
],
|
|
117
|
-
});
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### QueryBuilder 허용 케이스
|
|
121
|
-
|
|
122
|
-
**다음 경우에만 QueryBuilder 사용:**
|
|
123
|
-
|
|
124
|
-
| 케이스 | 예시 |
|
|
125
|
-
|--------|------|
|
|
126
|
-
| **groupBy** | 집계 쿼리 |
|
|
127
|
-
| **getRawMany/getRawOne** | 원시 데이터 필요 |
|
|
128
|
-
| **복잡한 서브쿼리** | 중첩 쿼리 |
|
|
129
|
-
| **복잡한 JOIN 조건** | ON 절 커스텀 |
|
|
130
|
-
|
|
131
|
-
</rules>
|
|
132
|
-
|
|
133
|
-
<examples>
|
|
134
|
-
<example type="good">
|
|
135
|
-
```typescript
|
|
136
|
-
// ✅ QueryBuilder 허용: groupBy + getRawMany
|
|
137
|
-
const stats = await this.orderRepository
|
|
138
|
-
.createQueryBuilder('order')
|
|
139
|
-
.select('order.status', 'status')
|
|
140
|
-
.addSelect('COUNT(*)', 'count')
|
|
141
|
-
.addSelect('SUM(order.amount)', 'total')
|
|
142
|
-
.groupBy('order.status')
|
|
143
|
-
.getRawMany();
|
|
144
|
-
```
|
|
145
|
-
</example>
|
|
146
|
-
<example type="bad">
|
|
147
|
-
```typescript
|
|
148
|
-
// ❌ 불필요한 QueryBuilder 사용
|
|
149
|
-
const user = await this.userRepository
|
|
150
|
-
.createQueryBuilder('user')
|
|
151
|
-
.where('user.id = :id', { id })
|
|
152
|
-
.getOne();
|
|
153
|
-
```
|
|
154
|
-
</example>
|
|
155
|
-
<example type="good">
|
|
156
|
-
```typescript
|
|
157
|
-
// ✅ find로 대체
|
|
158
|
-
const user = await this.userRepository.findOneBy({ id });
|
|
159
|
-
```
|
|
160
|
-
</example>
|
|
161
|
-
</examples>
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
93
|
<checklist>
|
|
166
94
|
|
|
167
95
|
## 체크리스트
|
|
@@ -170,8 +98,6 @@ const user = await this.userRepository.findOneBy({ id });
|
|
|
170
98
|
- [ ] Service에서 Entity가 필요한 시점에 변환하는가?
|
|
171
99
|
- [ ] Service의 return은 Entity 또는 일반 객체인가?
|
|
172
100
|
- [ ] Controller에서 Response DTO/Schema로 변환하는가?
|
|
173
|
-
- [ ] TypeORM find 메서드를 우선 사용하는가?
|
|
174
|
-
- [ ] QueryBuilder는 groupBy, getRawMany 등 필요한 경우에만 사용하는가?
|
|
175
101
|
|
|
176
102
|
</checklist>
|
|
177
103
|
|
|
@@ -181,6 +107,7 @@ const user = await this.userRepository.findOneBy({ id });
|
|
|
181
107
|
|
|
182
108
|
| 주제 | 위치 | 설명 |
|
|
183
109
|
|-----|------|------|
|
|
110
|
+
| TypeORM 사용 규칙 | `TypeORM/SKILL.md` | find vs queryBuilder 선택 기준 |
|
|
184
111
|
| BDD 테스트 | `bdd-testing.md` | NestJS + Jest BDD 스타일 테스트 작성 규칙 |
|
|
185
112
|
|
|
186
113
|
</reference>
|