oh-my-customcode 0.9.4 → 0.10.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/README.md +18 -6
- package/dist/cli/index.js +197 -64
- package/dist/index.js +173 -60
- package/package.json +1 -1
- package/templates/.claude/skills/springboot-best-practices/SKILL.md +7 -152
- package/templates/.claude/skills/springboot-best-practices/examples/config-properties-example.java +22 -0
- package/templates/.claude/skills/springboot-best-practices/examples/controller-example.java +28 -0
- package/templates/.claude/skills/springboot-best-practices/examples/controller-test-example.java +33 -0
- package/templates/.claude/skills/springboot-best-practices/examples/entity-example.java +22 -0
- package/templates/.claude/skills/springboot-best-practices/examples/exception-handler-example.java +30 -0
- package/templates/.claude/skills/springboot-best-practices/examples/repository-example.java +17 -0
- package/templates/.claude/skills/springboot-best-practices/examples/repository-test-example.java +23 -0
- package/templates/.claude/skills/springboot-best-practices/examples/security-config-example.java +27 -0
- package/templates/.claude/skills/springboot-best-practices/examples/service-example.java +33 -0
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/oh-my-customcode)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://github.com/baekenough/oh-my-customcode/actions/workflows/ci.yml)
|
|
8
|
+
[](https://github.com/baekenough/oh-my-customcode/actions/workflows/security-audit.yml)
|
|
8
9
|
|
|
9
10
|
**[한국어 문서 (Korean)](./README_ko.md)**
|
|
10
11
|
|
|
@@ -16,7 +17,7 @@ Like oh-my-zsh transformed shell customization, oh-my-customcode makes personali
|
|
|
16
17
|
|
|
17
18
|
| Feature | Description |
|
|
18
19
|
|---------|-------------|
|
|
19
|
-
| **Batteries Included** | 42 agents,
|
|
20
|
+
| **Batteries Included** | 42 agents, 51 skills, 22 guides, 18 rules, 1 hook, 4 contexts - ready to use out of the box |
|
|
20
21
|
| **Sub-Agent Model** | Supports hierarchical agent orchestration with specialized roles |
|
|
21
22
|
| **Dead Simple Customization** | Create a folder + markdown file = new agent or skill |
|
|
22
23
|
| **Mix and Match** | Use built-in components, create your own, or combine both |
|
|
@@ -121,7 +122,7 @@ Claude Code selects the appropriate model and parallelizes independent tasks (up
|
|
|
121
122
|
| **QA** | 3 | qa-planner, qa-writer, qa-engineer |
|
|
122
123
|
| **Total** | **42** | |
|
|
123
124
|
|
|
124
|
-
### Skills (
|
|
125
|
+
### Skills (51)
|
|
125
126
|
|
|
126
127
|
Includes slash commands and capabilities:
|
|
127
128
|
|
|
@@ -157,9 +158,9 @@ Comprehensive reference documentation covering:
|
|
|
157
158
|
|
|
158
159
|
Event-driven automation for Claude Code lifecycle events (PreToolUse, PostToolUse, etc.).
|
|
159
160
|
|
|
160
|
-
### Contexts (
|
|
161
|
+
### Contexts (4)
|
|
161
162
|
|
|
162
|
-
Shared context
|
|
163
|
+
Shared context files for cross-agent knowledge and mode configurations.
|
|
163
164
|
|
|
164
165
|
---
|
|
165
166
|
|
|
@@ -194,13 +195,13 @@ your-project/
|
|
|
194
195
|
└── .claude/ # (or .codex/)
|
|
195
196
|
├── rules/ # Behavior rules (18 total)
|
|
196
197
|
├── hooks/ # Event hooks (1 total)
|
|
197
|
-
├── contexts/ # Context files (
|
|
198
|
+
├── contexts/ # Context files (4 total)
|
|
198
199
|
├── agents/ # Agent definitions (42 flat .md files)
|
|
199
200
|
│ ├── lang-golang-expert.md
|
|
200
201
|
│ ├── be-fastapi-expert.md
|
|
201
202
|
│ ├── mgr-creator.md
|
|
202
203
|
│ └── ...
|
|
203
|
-
├── skills/ # Skill modules (
|
|
204
|
+
├── skills/ # Skill modules (51 directories, each with SKILL.md)
|
|
204
205
|
│ ├── go-best-practices/
|
|
205
206
|
│ ├── react-best-practices/
|
|
206
207
|
│ ├── secretary-routing/
|
|
@@ -219,6 +220,17 @@ bun test # Run tests
|
|
|
219
220
|
bun run build # Build for production
|
|
220
221
|
```
|
|
221
222
|
|
|
223
|
+
### Quality Gates
|
|
224
|
+
|
|
225
|
+
| Gate | Tool | Threshold |
|
|
226
|
+
|------|------|-----------|
|
|
227
|
+
| Lint | Biome | Zero errors (complexity enforced) |
|
|
228
|
+
| Test Coverage | Bun test | 95% (pre-commit), 97% (CI) |
|
|
229
|
+
| Security Audit | bun pm audit | No high/critical vulnerabilities |
|
|
230
|
+
| Dependabot | GitHub | Weekly scans, auto-PR for updates |
|
|
231
|
+
|
|
232
|
+
Pre-commit hooks automatically enforce lint, test, and coverage gates before each commit.
|
|
233
|
+
|
|
222
234
|
### Requirements
|
|
223
235
|
|
|
224
236
|
- Node.js >= 18.0.0
|
package/dist/cli/index.js
CHANGED
|
@@ -11725,6 +11725,7 @@ var en_default = {
|
|
|
11725
11725
|
skipped: "Update skipped",
|
|
11726
11726
|
dryRunOption: "Show what would be updated without making changes",
|
|
11727
11727
|
forceOption: "Force update even if already at latest version",
|
|
11728
|
+
forceOverwriteAllOption: "Bypass all file preservation (manifest and config)",
|
|
11728
11729
|
backupOption: "Create backup before updating",
|
|
11729
11730
|
agentsOption: "Update only agents",
|
|
11730
11731
|
skillsOption: "Update only skills",
|
|
@@ -12008,6 +12009,7 @@ var ko_default = {
|
|
|
12008
12009
|
skipped: "업데이트 건너뜀",
|
|
12009
12010
|
dryRunOption: "변경 없이 업데이트할 내용 표시",
|
|
12010
12011
|
forceOption: "이미 최신 버전이어도 강제 업데이트",
|
|
12012
|
+
forceOverwriteAllOption: "모든 파일 보존 무시 (manifest 및 config)",
|
|
12011
12013
|
backupOption: "업데이트 전 백업 생성",
|
|
12012
12014
|
agentsOption: "에이전트만 업데이트",
|
|
12013
12015
|
skillsOption: "스킬만 업데이트",
|
|
@@ -12299,8 +12301,38 @@ var $visitAsync = visit.visitAsync;
|
|
|
12299
12301
|
import { join as join2 } from "node:path";
|
|
12300
12302
|
|
|
12301
12303
|
// src/utils/fs.ts
|
|
12302
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
12304
|
+
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
12303
12305
|
import { fileURLToPath } from "node:url";
|
|
12306
|
+
function validatePreserveFilePath(filePath, projectRoot) {
|
|
12307
|
+
if (!filePath || filePath.trim() === "") {
|
|
12308
|
+
return {
|
|
12309
|
+
valid: false,
|
|
12310
|
+
reason: "Path cannot be empty"
|
|
12311
|
+
};
|
|
12312
|
+
}
|
|
12313
|
+
if (isAbsolute(filePath)) {
|
|
12314
|
+
return {
|
|
12315
|
+
valid: false,
|
|
12316
|
+
reason: "Absolute paths are not allowed"
|
|
12317
|
+
};
|
|
12318
|
+
}
|
|
12319
|
+
const normalizedPath = normalize(filePath);
|
|
12320
|
+
if (normalizedPath.startsWith("..")) {
|
|
12321
|
+
return {
|
|
12322
|
+
valid: false,
|
|
12323
|
+
reason: "Path cannot traverse outside project root"
|
|
12324
|
+
};
|
|
12325
|
+
}
|
|
12326
|
+
const resolvedPath = resolve(projectRoot, normalizedPath);
|
|
12327
|
+
const relativePath = relative(projectRoot, resolvedPath);
|
|
12328
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
12329
|
+
return {
|
|
12330
|
+
valid: false,
|
|
12331
|
+
reason: "Resolved path escapes project root"
|
|
12332
|
+
};
|
|
12333
|
+
}
|
|
12334
|
+
return { valid: true };
|
|
12335
|
+
}
|
|
12304
12336
|
async function fileExists(path) {
|
|
12305
12337
|
const fs = await import("node:fs/promises");
|
|
12306
12338
|
try {
|
|
@@ -12689,7 +12721,7 @@ async function loadConfig(targetDir) {
|
|
|
12689
12721
|
if (await fileExists(configPath)) {
|
|
12690
12722
|
try {
|
|
12691
12723
|
const config = await readJsonFile(configPath);
|
|
12692
|
-
const merged = mergeConfig(getDefaultConfig(), config);
|
|
12724
|
+
const merged = mergeConfig(getDefaultConfig(), config, targetDir);
|
|
12693
12725
|
if (merged.configVersion < CURRENT_CONFIG_VERSION) {
|
|
12694
12726
|
const migrated = migrateConfig(merged);
|
|
12695
12727
|
await saveConfig(targetDir, migrated);
|
|
@@ -12718,7 +12750,30 @@ function deduplicateCustomComponents(components) {
|
|
|
12718
12750
|
}
|
|
12719
12751
|
return [...seen.values()];
|
|
12720
12752
|
}
|
|
12721
|
-
function mergeConfig(defaults, overrides) {
|
|
12753
|
+
function mergeConfig(defaults, overrides, targetDir) {
|
|
12754
|
+
let mergedPreserveFiles;
|
|
12755
|
+
if (overrides.preserveFiles) {
|
|
12756
|
+
const allFiles = [...new Set([...defaults.preserveFiles || [], ...overrides.preserveFiles])];
|
|
12757
|
+
if (targetDir) {
|
|
12758
|
+
const validatedFiles = [];
|
|
12759
|
+
for (const filePath of allFiles) {
|
|
12760
|
+
const validation = validatePreserveFilePath(filePath, targetDir);
|
|
12761
|
+
if (validation.valid) {
|
|
12762
|
+
validatedFiles.push(filePath);
|
|
12763
|
+
} else {
|
|
12764
|
+
warn("config.invalid_preserve_path", {
|
|
12765
|
+
path: filePath,
|
|
12766
|
+
reason: validation.reason ?? "Invalid path"
|
|
12767
|
+
});
|
|
12768
|
+
}
|
|
12769
|
+
}
|
|
12770
|
+
mergedPreserveFiles = validatedFiles;
|
|
12771
|
+
} else {
|
|
12772
|
+
mergedPreserveFiles = allFiles;
|
|
12773
|
+
}
|
|
12774
|
+
} else {
|
|
12775
|
+
mergedPreserveFiles = defaults.preserveFiles;
|
|
12776
|
+
}
|
|
12722
12777
|
return {
|
|
12723
12778
|
...defaults,
|
|
12724
12779
|
...overrides,
|
|
@@ -12732,7 +12787,7 @@ function mergeConfig(defaults, overrides) {
|
|
|
12732
12787
|
...defaults.agents,
|
|
12733
12788
|
...overrides.agents
|
|
12734
12789
|
},
|
|
12735
|
-
preserveFiles:
|
|
12790
|
+
preserveFiles: mergedPreserveFiles,
|
|
12736
12791
|
customComponents: overrides.customComponents ? deduplicateCustomComponents([
|
|
12737
12792
|
...defaults.customComponents || [],
|
|
12738
12793
|
...overrides.customComponents
|
|
@@ -13908,13 +13963,13 @@ async function tryExtractMarkdownDescription(mdPath, options = {}) {
|
|
|
13908
13963
|
return;
|
|
13909
13964
|
}
|
|
13910
13965
|
}
|
|
13911
|
-
async function getAgents(targetDir, rootDir = ".claude") {
|
|
13966
|
+
async function getAgents(targetDir, rootDir = ".claude", config) {
|
|
13912
13967
|
const agentsDir = join6(targetDir, rootDir, "agents");
|
|
13913
13968
|
if (!await fileExists(agentsDir))
|
|
13914
13969
|
return [];
|
|
13915
13970
|
try {
|
|
13916
|
-
const
|
|
13917
|
-
const customComponents =
|
|
13971
|
+
const resolvedConfig = config ?? await loadConfig(targetDir);
|
|
13972
|
+
const customComponents = resolvedConfig.customComponents || [];
|
|
13918
13973
|
const customAgentPaths = new Set(customComponents.filter((c) => c.type === "agent").map((c) => c.path));
|
|
13919
13974
|
const agentMdFiles = await listFiles(agentsDir, { recursive: false, pattern: "*.md" });
|
|
13920
13975
|
const agents = await Promise.all(agentMdFiles.map(async (agentMdPath) => {
|
|
@@ -13936,13 +13991,13 @@ async function getAgents(targetDir, rootDir = ".claude") {
|
|
|
13936
13991
|
return [];
|
|
13937
13992
|
}
|
|
13938
13993
|
}
|
|
13939
|
-
async function getSkills(targetDir, rootDir = ".claude") {
|
|
13994
|
+
async function getSkills(targetDir, rootDir = ".claude", config) {
|
|
13940
13995
|
const skillsDir = join6(targetDir, rootDir, "skills");
|
|
13941
13996
|
if (!await fileExists(skillsDir))
|
|
13942
13997
|
return [];
|
|
13943
13998
|
try {
|
|
13944
|
-
const
|
|
13945
|
-
const customComponents =
|
|
13999
|
+
const resolvedConfig = config ?? await loadConfig(targetDir);
|
|
14000
|
+
const customComponents = resolvedConfig.customComponents || [];
|
|
13946
14001
|
const customSkillPaths = new Set(customComponents.filter((c) => c.type === "skill").map((c) => c.path));
|
|
13947
14002
|
const skillMdFiles = await listFiles(skillsDir, { recursive: true, pattern: "SKILL.md" });
|
|
13948
14003
|
const skills = await Promise.all(skillMdFiles.map(async (skillMdPath) => {
|
|
@@ -13965,13 +14020,13 @@ async function getSkills(targetDir, rootDir = ".claude") {
|
|
|
13965
14020
|
return [];
|
|
13966
14021
|
}
|
|
13967
14022
|
}
|
|
13968
|
-
async function getGuides(targetDir) {
|
|
14023
|
+
async function getGuides(targetDir, config) {
|
|
13969
14024
|
const guidesDir = join6(targetDir, "guides");
|
|
13970
14025
|
if (!await fileExists(guidesDir))
|
|
13971
14026
|
return [];
|
|
13972
14027
|
try {
|
|
13973
|
-
const
|
|
13974
|
-
const customComponents =
|
|
14028
|
+
const resolvedConfig = config ?? await loadConfig(targetDir);
|
|
14029
|
+
const customComponents = resolvedConfig.customComponents || [];
|
|
13975
14030
|
const customGuidePaths = new Set(customComponents.filter((c) => c.type === "guide").map((c) => c.path));
|
|
13976
14031
|
const guideMdFiles = await listFiles(guidesDir, { recursive: true, pattern: "*.md" });
|
|
13977
14032
|
const guides = await Promise.all(guideMdFiles.map(async (guideMdPath) => {
|
|
@@ -13992,13 +14047,13 @@ async function getGuides(targetDir) {
|
|
|
13992
14047
|
}
|
|
13993
14048
|
}
|
|
13994
14049
|
var RULE_PRIORITY_ORDER = { MUST: 0, SHOULD: 1, MAY: 2 };
|
|
13995
|
-
async function getRules(targetDir, rootDir = ".claude") {
|
|
14050
|
+
async function getRules(targetDir, rootDir = ".claude", config) {
|
|
13996
14051
|
const rulesDir = join6(targetDir, rootDir, "rules");
|
|
13997
14052
|
if (!await fileExists(rulesDir))
|
|
13998
14053
|
return [];
|
|
13999
14054
|
try {
|
|
14000
|
-
const
|
|
14001
|
-
const customComponents =
|
|
14055
|
+
const resolvedConfig = config ?? await loadConfig(targetDir);
|
|
14056
|
+
const customComponents = resolvedConfig.customComponents || [];
|
|
14002
14057
|
const customRulePaths = new Set(customComponents.filter((c) => c.type === "rule").map((c) => c.path));
|
|
14003
14058
|
const ruleMdFiles = await listFiles(rulesDir, { recursive: false, pattern: "*.md" });
|
|
14004
14059
|
const rules = await Promise.all(ruleMdFiles.map(async (ruleMdPath) => {
|
|
@@ -14108,7 +14163,7 @@ async function getContexts(targetDir, rootDir = ".claude") {
|
|
|
14108
14163
|
var COMPONENT_GETTERS = {
|
|
14109
14164
|
agents: getAgents,
|
|
14110
14165
|
skills: getSkills,
|
|
14111
|
-
guides: async (dir2) => getGuides(dir2),
|
|
14166
|
+
guides: async (dir2, _rootDir, config) => getGuides(dir2, config),
|
|
14112
14167
|
rules: getRules,
|
|
14113
14168
|
hooks: getHooks,
|
|
14114
14169
|
contexts: getContexts
|
|
@@ -14122,12 +14177,12 @@ function displayComponents(components, type, format) {
|
|
|
14122
14177
|
formatAsTable(components, type);
|
|
14123
14178
|
}
|
|
14124
14179
|
}
|
|
14125
|
-
async function handleListAll(targetDir, rootDir, format) {
|
|
14180
|
+
async function handleListAll(targetDir, rootDir, format, config) {
|
|
14126
14181
|
const [agents, skills, guides, rules, hooks, contexts] = await Promise.all([
|
|
14127
|
-
getAgents(targetDir, rootDir),
|
|
14128
|
-
getSkills(targetDir, rootDir),
|
|
14129
|
-
getGuides(targetDir),
|
|
14130
|
-
getRules(targetDir, rootDir),
|
|
14182
|
+
getAgents(targetDir, rootDir, config),
|
|
14183
|
+
getSkills(targetDir, rootDir, config),
|
|
14184
|
+
getGuides(targetDir, config),
|
|
14185
|
+
getRules(targetDir, rootDir, config),
|
|
14131
14186
|
getHooks(targetDir, rootDir),
|
|
14132
14187
|
getContexts(targetDir, rootDir)
|
|
14133
14188
|
]);
|
|
@@ -14152,7 +14207,8 @@ async function listCommand(type = "all", options = {}) {
|
|
|
14152
14207
|
preferProject: true
|
|
14153
14208
|
});
|
|
14154
14209
|
const layout = getProviderLayout(detection.provider);
|
|
14155
|
-
const
|
|
14210
|
+
const config = await loadConfig(targetDir);
|
|
14211
|
+
const components = type === "all" ? await handleListAll(targetDir, layout.rootDir, format, config) : await COMPONENT_GETTERS[type](targetDir, layout.rootDir, config);
|
|
14156
14212
|
if (type === "all" && format === "json") {
|
|
14157
14213
|
formatAsJson(components);
|
|
14158
14214
|
} else if (type !== "all") {
|
|
@@ -14172,34 +14228,60 @@ import { join as join7 } from "node:path";
|
|
|
14172
14228
|
// src/core/entry-merger.ts
|
|
14173
14229
|
var MANAGED_START = "<!-- omcustom:start -->";
|
|
14174
14230
|
var MANAGED_END = "<!-- omcustom:end -->";
|
|
14231
|
+
function isCodeBlockDelimiter(line) {
|
|
14232
|
+
const trimmed = line.trim();
|
|
14233
|
+
return trimmed.startsWith("```") || trimmed.startsWith("~~~");
|
|
14234
|
+
}
|
|
14235
|
+
function handleManagedStart(currentLines, sections) {
|
|
14236
|
+
if (currentLines.length > 0) {
|
|
14237
|
+
sections.push({
|
|
14238
|
+
type: "custom",
|
|
14239
|
+
content: currentLines.join(`
|
|
14240
|
+
`)
|
|
14241
|
+
});
|
|
14242
|
+
}
|
|
14243
|
+
return {
|
|
14244
|
+
currentSection: { type: "managed", content: "" },
|
|
14245
|
+
currentLines: []
|
|
14246
|
+
};
|
|
14247
|
+
}
|
|
14248
|
+
function handleManagedEnd(currentSection, currentLines, sections) {
|
|
14249
|
+
if (currentSection && currentSection.type === "managed") {
|
|
14250
|
+
currentSection.content = currentLines.join(`
|
|
14251
|
+
`);
|
|
14252
|
+
sections.push(currentSection);
|
|
14253
|
+
return {
|
|
14254
|
+
currentSection: null,
|
|
14255
|
+
currentLines: []
|
|
14256
|
+
};
|
|
14257
|
+
}
|
|
14258
|
+
return { currentSection, currentLines };
|
|
14259
|
+
}
|
|
14175
14260
|
function parseEntryDoc(content) {
|
|
14176
14261
|
const sections = [];
|
|
14177
14262
|
const lines = content.split(`
|
|
14178
14263
|
`);
|
|
14179
14264
|
let currentSection = null;
|
|
14180
14265
|
let currentLines = [];
|
|
14266
|
+
let insideCodeBlock = false;
|
|
14181
14267
|
for (const line of lines) {
|
|
14182
|
-
if (line
|
|
14183
|
-
|
|
14184
|
-
|
|
14185
|
-
|
|
14186
|
-
|
|
14187
|
-
|
|
14188
|
-
|
|
14189
|
-
|
|
14268
|
+
if (isCodeBlockDelimiter(line)) {
|
|
14269
|
+
insideCodeBlock = !insideCodeBlock;
|
|
14270
|
+
}
|
|
14271
|
+
if (!insideCodeBlock) {
|
|
14272
|
+
const trimmed = line.trim();
|
|
14273
|
+
if (trimmed === MANAGED_START) {
|
|
14274
|
+
const result = handleManagedStart(currentLines, sections);
|
|
14275
|
+
currentSection = result.currentSection;
|
|
14276
|
+
currentLines = result.currentLines;
|
|
14277
|
+
continue;
|
|
14190
14278
|
}
|
|
14191
|
-
|
|
14192
|
-
|
|
14193
|
-
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
currentSection.content = currentLines.join(`
|
|
14197
|
-
`);
|
|
14198
|
-
sections.push(currentSection);
|
|
14199
|
-
currentSection = null;
|
|
14200
|
-
currentLines = [];
|
|
14279
|
+
if (trimmed === MANAGED_END) {
|
|
14280
|
+
const result = handleManagedEnd(currentSection, currentLines, sections);
|
|
14281
|
+
currentSection = result.currentSection;
|
|
14282
|
+
currentLines = result.currentLines;
|
|
14283
|
+
continue;
|
|
14201
14284
|
}
|
|
14202
|
-
continue;
|
|
14203
14285
|
}
|
|
14204
14286
|
currentLines.push(line);
|
|
14205
14287
|
}
|
|
@@ -14280,7 +14362,7 @@ async function handleBackupIfRequested(targetDir, provider, backup, result) {
|
|
|
14280
14362
|
result.backedUpPaths.push(backupPath);
|
|
14281
14363
|
info("update.backup_created", { path: backupPath });
|
|
14282
14364
|
}
|
|
14283
|
-
async function processComponentUpdate(targetDir, provider, component, updateCheck, customizations, options, result) {
|
|
14365
|
+
async function processComponentUpdate(targetDir, provider, component, updateCheck, customizations, options, result, config) {
|
|
14284
14366
|
const componentUpdate = updateCheck.updatableComponents.find((c) => c.name === component);
|
|
14285
14367
|
if (!componentUpdate && !options.force) {
|
|
14286
14368
|
result.skippedComponents.push(component);
|
|
@@ -14292,7 +14374,7 @@ async function processComponentUpdate(targetDir, provider, component, updateChec
|
|
|
14292
14374
|
return;
|
|
14293
14375
|
}
|
|
14294
14376
|
try {
|
|
14295
|
-
const preserved = await updateComponent(targetDir, provider, component, customizations, options);
|
|
14377
|
+
const preserved = await updateComponent(targetDir, provider, component, customizations, options, config);
|
|
14296
14378
|
result.updatedComponents.push(component);
|
|
14297
14379
|
result.preservedFiles.push(...preserved);
|
|
14298
14380
|
} catch (err) {
|
|
@@ -14301,9 +14383,9 @@ async function processComponentUpdate(targetDir, provider, component, updateChec
|
|
|
14301
14383
|
result.skippedComponents.push(component);
|
|
14302
14384
|
}
|
|
14303
14385
|
}
|
|
14304
|
-
async function updateAllComponents(targetDir, provider, components, updateCheck, customizations, options, result) {
|
|
14386
|
+
async function updateAllComponents(targetDir, provider, components, updateCheck, customizations, options, result, config) {
|
|
14305
14387
|
for (const component of components) {
|
|
14306
|
-
await processComponentUpdate(targetDir, provider, component, updateCheck, customizations, options, result);
|
|
14388
|
+
await processComponentUpdate(targetDir, provider, component, updateCheck, customizations, options, result, config);
|
|
14307
14389
|
}
|
|
14308
14390
|
}
|
|
14309
14391
|
function getEntryTemplateName2(provider, language) {
|
|
@@ -14320,25 +14402,76 @@ async function backupFile(filePath) {
|
|
|
14320
14402
|
debug("update.file_backed_up", { path: filePath, backup: backupPath });
|
|
14321
14403
|
}
|
|
14322
14404
|
}
|
|
14323
|
-
function
|
|
14324
|
-
if (
|
|
14405
|
+
async function resolveManifestCustomizations(options, targetDir) {
|
|
14406
|
+
if (options.forceOverwriteAll) {
|
|
14325
14407
|
return null;
|
|
14326
14408
|
}
|
|
14327
|
-
if (
|
|
14328
|
-
|
|
14329
|
-
|
|
14330
|
-
|
|
14331
|
-
|
|
14409
|
+
if (options.preserveCustomizations === false) {
|
|
14410
|
+
return null;
|
|
14411
|
+
}
|
|
14412
|
+
return loadCustomizationManifest(targetDir);
|
|
14413
|
+
}
|
|
14414
|
+
function resolveConfigPreserveFiles(options, config) {
|
|
14415
|
+
if (options.forceOverwriteAll) {
|
|
14416
|
+
return [];
|
|
14417
|
+
}
|
|
14418
|
+
const preserveFiles = config.preserveFiles || [];
|
|
14419
|
+
const validatedPaths = [];
|
|
14420
|
+
for (const filePath of preserveFiles) {
|
|
14421
|
+
const validation = validatePreserveFilePath(filePath, options.targetDir);
|
|
14422
|
+
if (validation.valid) {
|
|
14423
|
+
validatedPaths.push(filePath);
|
|
14424
|
+
} else {
|
|
14425
|
+
warn("preserve_files.invalid_path", {
|
|
14426
|
+
path: filePath,
|
|
14427
|
+
reason: validation.reason ?? "Invalid path"
|
|
14428
|
+
});
|
|
14429
|
+
}
|
|
14430
|
+
}
|
|
14431
|
+
return validatedPaths;
|
|
14432
|
+
}
|
|
14433
|
+
function resolveCustomizations(customizations, configPreserveFiles, targetDir) {
|
|
14434
|
+
const validatedManifestFiles = [];
|
|
14435
|
+
if (customizations && customizations.preserveFiles.length > 0) {
|
|
14436
|
+
for (const filePath of customizations.preserveFiles) {
|
|
14437
|
+
const validation = validatePreserveFilePath(filePath, targetDir);
|
|
14438
|
+
if (validation.valid) {
|
|
14439
|
+
validatedManifestFiles.push(filePath);
|
|
14440
|
+
} else {
|
|
14441
|
+
warn("preserve_files.invalid_path", {
|
|
14442
|
+
path: filePath,
|
|
14443
|
+
reason: validation.reason ?? "Invalid path",
|
|
14444
|
+
source: "manifest"
|
|
14445
|
+
});
|
|
14446
|
+
}
|
|
14447
|
+
}
|
|
14448
|
+
}
|
|
14449
|
+
if (validatedManifestFiles.length === 0 && configPreserveFiles.length === 0) {
|
|
14450
|
+
return customizations && customizations.modifiedFiles.length > 0 ? customizations : null;
|
|
14451
|
+
}
|
|
14452
|
+
if (validatedManifestFiles.length > 0 && configPreserveFiles.length > 0) {
|
|
14453
|
+
const merged = customizations || {
|
|
14454
|
+
modifiedFiles: [],
|
|
14455
|
+
preserveFiles: [],
|
|
14456
|
+
customComponents: [],
|
|
14457
|
+
lastUpdated: new Date().toISOString()
|
|
14458
|
+
};
|
|
14459
|
+
merged.preserveFiles = [...new Set([...validatedManifestFiles, ...configPreserveFiles])];
|
|
14460
|
+
return merged;
|
|
14332
14461
|
}
|
|
14333
14462
|
if (configPreserveFiles.length > 0) {
|
|
14334
14463
|
return {
|
|
14335
|
-
modifiedFiles: [],
|
|
14464
|
+
modifiedFiles: customizations?.modifiedFiles || [],
|
|
14336
14465
|
preserveFiles: configPreserveFiles,
|
|
14337
|
-
customComponents: [],
|
|
14466
|
+
customComponents: customizations?.customComponents || [],
|
|
14338
14467
|
lastUpdated: new Date().toISOString()
|
|
14339
14468
|
};
|
|
14340
14469
|
}
|
|
14341
|
-
|
|
14470
|
+
if (customizations) {
|
|
14471
|
+
customizations.preserveFiles = validatedManifestFiles;
|
|
14472
|
+
return customizations;
|
|
14473
|
+
}
|
|
14474
|
+
return null;
|
|
14342
14475
|
}
|
|
14343
14476
|
async function updateEntryDoc(targetDir, provider, config, options) {
|
|
14344
14477
|
const layout = getProviderLayout(provider);
|
|
@@ -14391,11 +14524,11 @@ async function update(options) {
|
|
|
14391
14524
|
return result;
|
|
14392
14525
|
}
|
|
14393
14526
|
await handleBackupIfRequested(options.targetDir, provider, !!options.backup, result);
|
|
14394
|
-
const manifestCustomizations =
|
|
14395
|
-
const configPreserveFiles = config
|
|
14396
|
-
const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles);
|
|
14527
|
+
const manifestCustomizations = await resolveManifestCustomizations(options, options.targetDir);
|
|
14528
|
+
const configPreserveFiles = resolveConfigPreserveFiles(options, config);
|
|
14529
|
+
const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles, options.targetDir);
|
|
14397
14530
|
const components = options.components || getAllUpdateComponents();
|
|
14398
|
-
await updateAllComponents(options.targetDir, provider, components, updateCheck, customizations, options, result);
|
|
14531
|
+
await updateAllComponents(options.targetDir, provider, components, updateCheck, customizations, options, result, config);
|
|
14399
14532
|
if (!options.components || options.components.length === 0) {
|
|
14400
14533
|
await updateEntryDoc(options.targetDir, provider, config, options);
|
|
14401
14534
|
}
|
|
@@ -14454,15 +14587,14 @@ async function componentHasUpdate(_targetDir, provider, component, config) {
|
|
|
14454
14587
|
const latestVersion = await getLatestVersion(provider);
|
|
14455
14588
|
return installedVersion !== latestVersion;
|
|
14456
14589
|
}
|
|
14457
|
-
async function updateComponent(targetDir, provider, component, customizations, options) {
|
|
14590
|
+
async function updateComponent(targetDir, provider, component, customizations, options, config) {
|
|
14458
14591
|
const preservedFiles = [];
|
|
14459
14592
|
const componentPath = getComponentPath2(provider, component);
|
|
14460
14593
|
const srcPath = resolveTemplatePath(componentPath);
|
|
14461
14594
|
const destPath = join7(targetDir, componentPath);
|
|
14462
|
-
const config = await loadConfig(targetDir);
|
|
14463
14595
|
const customComponents = config.customComponents || [];
|
|
14464
14596
|
const skipPaths = [];
|
|
14465
|
-
if (customizations && options.
|
|
14597
|
+
if (customizations && !options.forceOverwriteAll) {
|
|
14466
14598
|
const toPreserve = customizations.preserveFiles.filter((f) => f.startsWith(componentPath));
|
|
14467
14599
|
preservedFiles.push(...toPreserve);
|
|
14468
14600
|
skipPaths.push(...toPreserve);
|
|
@@ -14538,6 +14670,7 @@ async function updateCommand(options = {}) {
|
|
|
14538
14670
|
components,
|
|
14539
14671
|
force: options.force,
|
|
14540
14672
|
preserveCustomizations: true,
|
|
14673
|
+
forceOverwriteAll: options.forceOverwriteAll,
|
|
14541
14674
|
dryRun: options.dryRun,
|
|
14542
14675
|
backup: options.backup
|
|
14543
14676
|
};
|
|
@@ -14611,7 +14744,7 @@ function createProgram() {
|
|
|
14611
14744
|
program2.command("init").description(i18n.t("cli.init.description")).option("-l, --lang <language>", i18n.t("cli.init.langOption"), "en").option("-p, --provider <provider>", i18n.t("cli.init.providerOption"), "auto").action(async (options) => {
|
|
14612
14745
|
await initCommand(options);
|
|
14613
14746
|
});
|
|
14614
|
-
program2.command("update").description(i18n.t("cli.update.description")).option("--dry-run", i18n.t("cli.update.dryRunOption")).option("--force", i18n.t("cli.update.forceOption")).option("--backup", i18n.t("cli.update.backupOption")).option("--agents", i18n.t("cli.update.agentsOption")).option("--skills", i18n.t("cli.update.skillsOption")).option("--rules", i18n.t("cli.update.rulesOption")).option("--guides", i18n.t("cli.update.guidesOption")).option("--hooks", i18n.t("cli.update.hooksOption")).option("--contexts", i18n.t("cli.update.contextsOption")).option("-p, --provider <provider>", i18n.t("cli.update.providerOption"), "auto").action(async (options) => {
|
|
14747
|
+
program2.command("update").description(i18n.t("cli.update.description")).option("--dry-run", i18n.t("cli.update.dryRunOption")).option("--force", i18n.t("cli.update.forceOption")).option("--force-overwrite-all", i18n.t("cli.update.forceOverwriteAllOption")).option("--backup", i18n.t("cli.update.backupOption")).option("--agents", i18n.t("cli.update.agentsOption")).option("--skills", i18n.t("cli.update.skillsOption")).option("--rules", i18n.t("cli.update.rulesOption")).option("--guides", i18n.t("cli.update.guidesOption")).option("--hooks", i18n.t("cli.update.hooksOption")).option("--contexts", i18n.t("cli.update.contextsOption")).option("-p, --provider <provider>", i18n.t("cli.update.providerOption"), "auto").action(async (options) => {
|
|
14615
14748
|
await updateCommand(options);
|
|
14616
14749
|
});
|
|
14617
14750
|
program2.command("list").description(i18n.t("cli.list.description")).argument("[type]", i18n.t("cli.list.typeArgument"), "all").option("-f, --format <format>", "Output format: table, json, or simple", "table").option("--verbose", "Show detailed information").option("-p, --provider <provider>", i18n.t("cli.list.providerOption"), "auto").action(async (type, options) => {
|
package/dist/index.js
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
-
for (let key of __getOwnPropNames(mod))
|
|
11
|
-
if (!__hasOwnProp.call(to, key))
|
|
12
|
-
__defProp(to, key, {
|
|
13
|
-
get: () => mod[key],
|
|
14
|
-
enumerable: true
|
|
15
|
-
});
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
3
|
|
|
20
4
|
// src/core/config.ts
|
|
21
5
|
import { join as join2 } from "node:path";
|
|
22
6
|
|
|
23
7
|
// src/utils/fs.ts
|
|
24
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
|
+
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
25
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
function validatePreserveFilePath(filePath, projectRoot) {
|
|
11
|
+
if (!filePath || filePath.trim() === "") {
|
|
12
|
+
return {
|
|
13
|
+
valid: false,
|
|
14
|
+
reason: "Path cannot be empty"
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (isAbsolute(filePath)) {
|
|
18
|
+
return {
|
|
19
|
+
valid: false,
|
|
20
|
+
reason: "Absolute paths are not allowed"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const normalizedPath = normalize(filePath);
|
|
24
|
+
if (normalizedPath.startsWith("..")) {
|
|
25
|
+
return {
|
|
26
|
+
valid: false,
|
|
27
|
+
reason: "Path cannot traverse outside project root"
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const resolvedPath = resolve(projectRoot, normalizedPath);
|
|
31
|
+
const relativePath = relative(projectRoot, resolvedPath);
|
|
32
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
33
|
+
return {
|
|
34
|
+
valid: false,
|
|
35
|
+
reason: "Resolved path escapes project root"
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { valid: true };
|
|
39
|
+
}
|
|
26
40
|
async function fileExists(path) {
|
|
27
41
|
const fs = await import("node:fs/promises");
|
|
28
42
|
try {
|
|
@@ -402,7 +416,7 @@ async function loadConfig(targetDir) {
|
|
|
402
416
|
if (await fileExists(configPath)) {
|
|
403
417
|
try {
|
|
404
418
|
const config = await readJsonFile(configPath);
|
|
405
|
-
const merged = mergeConfig(getDefaultConfig(), config);
|
|
419
|
+
const merged = mergeConfig(getDefaultConfig(), config, targetDir);
|
|
406
420
|
if (merged.configVersion < CURRENT_CONFIG_VERSION) {
|
|
407
421
|
const migrated = migrateConfig(merged);
|
|
408
422
|
await saveConfig(targetDir, migrated);
|
|
@@ -431,7 +445,30 @@ function deduplicateCustomComponents(components) {
|
|
|
431
445
|
}
|
|
432
446
|
return [...seen.values()];
|
|
433
447
|
}
|
|
434
|
-
function mergeConfig(defaults, overrides) {
|
|
448
|
+
function mergeConfig(defaults, overrides, targetDir) {
|
|
449
|
+
let mergedPreserveFiles;
|
|
450
|
+
if (overrides.preserveFiles) {
|
|
451
|
+
const allFiles = [...new Set([...defaults.preserveFiles || [], ...overrides.preserveFiles])];
|
|
452
|
+
if (targetDir) {
|
|
453
|
+
const validatedFiles = [];
|
|
454
|
+
for (const filePath of allFiles) {
|
|
455
|
+
const validation = validatePreserveFilePath(filePath, targetDir);
|
|
456
|
+
if (validation.valid) {
|
|
457
|
+
validatedFiles.push(filePath);
|
|
458
|
+
} else {
|
|
459
|
+
warn("config.invalid_preserve_path", {
|
|
460
|
+
path: filePath,
|
|
461
|
+
reason: validation.reason ?? "Invalid path"
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
mergedPreserveFiles = validatedFiles;
|
|
466
|
+
} else {
|
|
467
|
+
mergedPreserveFiles = allFiles;
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
mergedPreserveFiles = defaults.preserveFiles;
|
|
471
|
+
}
|
|
435
472
|
return {
|
|
436
473
|
...defaults,
|
|
437
474
|
...overrides,
|
|
@@ -445,7 +482,7 @@ function mergeConfig(defaults, overrides) {
|
|
|
445
482
|
...defaults.agents,
|
|
446
483
|
...overrides.agents
|
|
447
484
|
},
|
|
448
|
-
preserveFiles:
|
|
485
|
+
preserveFiles: mergedPreserveFiles,
|
|
449
486
|
customComponents: overrides.customComponents ? deduplicateCustomComponents([
|
|
450
487
|
...defaults.customComponents || [],
|
|
451
488
|
...overrides.customComponents
|
|
@@ -904,34 +941,60 @@ import { join as join5 } from "node:path";
|
|
|
904
941
|
// src/core/entry-merger.ts
|
|
905
942
|
var MANAGED_START = "<!-- omcustom:start -->";
|
|
906
943
|
var MANAGED_END = "<!-- omcustom:end -->";
|
|
944
|
+
function isCodeBlockDelimiter(line) {
|
|
945
|
+
const trimmed = line.trim();
|
|
946
|
+
return trimmed.startsWith("```") || trimmed.startsWith("~~~");
|
|
947
|
+
}
|
|
948
|
+
function handleManagedStart(currentLines, sections) {
|
|
949
|
+
if (currentLines.length > 0) {
|
|
950
|
+
sections.push({
|
|
951
|
+
type: "custom",
|
|
952
|
+
content: currentLines.join(`
|
|
953
|
+
`)
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
currentSection: { type: "managed", content: "" },
|
|
958
|
+
currentLines: []
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function handleManagedEnd(currentSection, currentLines, sections) {
|
|
962
|
+
if (currentSection && currentSection.type === "managed") {
|
|
963
|
+
currentSection.content = currentLines.join(`
|
|
964
|
+
`);
|
|
965
|
+
sections.push(currentSection);
|
|
966
|
+
return {
|
|
967
|
+
currentSection: null,
|
|
968
|
+
currentLines: []
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
return { currentSection, currentLines };
|
|
972
|
+
}
|
|
907
973
|
function parseEntryDoc(content) {
|
|
908
974
|
const sections = [];
|
|
909
975
|
const lines = content.split(`
|
|
910
976
|
`);
|
|
911
977
|
let currentSection = null;
|
|
912
978
|
let currentLines = [];
|
|
979
|
+
let insideCodeBlock = false;
|
|
913
980
|
for (const line of lines) {
|
|
914
|
-
if (line
|
|
915
|
-
|
|
916
|
-
sections.push({
|
|
917
|
-
type: "custom",
|
|
918
|
-
content: currentLines.join(`
|
|
919
|
-
`)
|
|
920
|
-
});
|
|
921
|
-
currentLines = [];
|
|
922
|
-
}
|
|
923
|
-
currentSection = { type: "managed", content: "" };
|
|
924
|
-
continue;
|
|
981
|
+
if (isCodeBlockDelimiter(line)) {
|
|
982
|
+
insideCodeBlock = !insideCodeBlock;
|
|
925
983
|
}
|
|
926
|
-
if (
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
984
|
+
if (!insideCodeBlock) {
|
|
985
|
+
const trimmed = line.trim();
|
|
986
|
+
if (trimmed === MANAGED_START) {
|
|
987
|
+
const result = handleManagedStart(currentLines, sections);
|
|
988
|
+
currentSection = result.currentSection;
|
|
989
|
+
currentLines = result.currentLines;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (trimmed === MANAGED_END) {
|
|
993
|
+
const result = handleManagedEnd(currentSection, currentLines, sections);
|
|
994
|
+
currentSection = result.currentSection;
|
|
995
|
+
currentLines = result.currentLines;
|
|
996
|
+
continue;
|
|
933
997
|
}
|
|
934
|
-
continue;
|
|
935
998
|
}
|
|
936
999
|
currentLines.push(line);
|
|
937
1000
|
}
|
|
@@ -1012,7 +1075,7 @@ async function handleBackupIfRequested(targetDir, provider, backup, result) {
|
|
|
1012
1075
|
result.backedUpPaths.push(backupPath);
|
|
1013
1076
|
info("update.backup_created", { path: backupPath });
|
|
1014
1077
|
}
|
|
1015
|
-
async function processComponentUpdate(targetDir, provider, component, updateCheck, customizations, options, result) {
|
|
1078
|
+
async function processComponentUpdate(targetDir, provider, component, updateCheck, customizations, options, result, config) {
|
|
1016
1079
|
const componentUpdate = updateCheck.updatableComponents.find((c) => c.name === component);
|
|
1017
1080
|
if (!componentUpdate && !options.force) {
|
|
1018
1081
|
result.skippedComponents.push(component);
|
|
@@ -1024,7 +1087,7 @@ async function processComponentUpdate(targetDir, provider, component, updateChec
|
|
|
1024
1087
|
return;
|
|
1025
1088
|
}
|
|
1026
1089
|
try {
|
|
1027
|
-
const preserved = await updateComponent(targetDir, provider, component, customizations, options);
|
|
1090
|
+
const preserved = await updateComponent(targetDir, provider, component, customizations, options, config);
|
|
1028
1091
|
result.updatedComponents.push(component);
|
|
1029
1092
|
result.preservedFiles.push(...preserved);
|
|
1030
1093
|
} catch (err) {
|
|
@@ -1033,9 +1096,9 @@ async function processComponentUpdate(targetDir, provider, component, updateChec
|
|
|
1033
1096
|
result.skippedComponents.push(component);
|
|
1034
1097
|
}
|
|
1035
1098
|
}
|
|
1036
|
-
async function updateAllComponents(targetDir, provider, components, updateCheck, customizations, options, result) {
|
|
1099
|
+
async function updateAllComponents(targetDir, provider, components, updateCheck, customizations, options, result, config) {
|
|
1037
1100
|
for (const component of components) {
|
|
1038
|
-
await processComponentUpdate(targetDir, provider, component, updateCheck, customizations, options, result);
|
|
1101
|
+
await processComponentUpdate(targetDir, provider, component, updateCheck, customizations, options, result, config);
|
|
1039
1102
|
}
|
|
1040
1103
|
}
|
|
1041
1104
|
function getEntryTemplateName2(provider, language) {
|
|
@@ -1052,25 +1115,76 @@ async function backupFile(filePath) {
|
|
|
1052
1115
|
debug("update.file_backed_up", { path: filePath, backup: backupPath });
|
|
1053
1116
|
}
|
|
1054
1117
|
}
|
|
1055
|
-
function
|
|
1056
|
-
if (
|
|
1118
|
+
async function resolveManifestCustomizations(options, targetDir) {
|
|
1119
|
+
if (options.forceOverwriteAll) {
|
|
1057
1120
|
return null;
|
|
1058
1121
|
}
|
|
1059
|
-
if (
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1122
|
+
if (options.preserveCustomizations === false) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
return loadCustomizationManifest(targetDir);
|
|
1126
|
+
}
|
|
1127
|
+
function resolveConfigPreserveFiles(options, config) {
|
|
1128
|
+
if (options.forceOverwriteAll) {
|
|
1129
|
+
return [];
|
|
1130
|
+
}
|
|
1131
|
+
const preserveFiles = config.preserveFiles || [];
|
|
1132
|
+
const validatedPaths = [];
|
|
1133
|
+
for (const filePath of preserveFiles) {
|
|
1134
|
+
const validation = validatePreserveFilePath(filePath, options.targetDir);
|
|
1135
|
+
if (validation.valid) {
|
|
1136
|
+
validatedPaths.push(filePath);
|
|
1137
|
+
} else {
|
|
1138
|
+
warn("preserve_files.invalid_path", {
|
|
1139
|
+
path: filePath,
|
|
1140
|
+
reason: validation.reason ?? "Invalid path"
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return validatedPaths;
|
|
1145
|
+
}
|
|
1146
|
+
function resolveCustomizations(customizations, configPreserveFiles, targetDir) {
|
|
1147
|
+
const validatedManifestFiles = [];
|
|
1148
|
+
if (customizations && customizations.preserveFiles.length > 0) {
|
|
1149
|
+
for (const filePath of customizations.preserveFiles) {
|
|
1150
|
+
const validation = validatePreserveFilePath(filePath, targetDir);
|
|
1151
|
+
if (validation.valid) {
|
|
1152
|
+
validatedManifestFiles.push(filePath);
|
|
1153
|
+
} else {
|
|
1154
|
+
warn("preserve_files.invalid_path", {
|
|
1155
|
+
path: filePath,
|
|
1156
|
+
reason: validation.reason ?? "Invalid path",
|
|
1157
|
+
source: "manifest"
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (validatedManifestFiles.length === 0 && configPreserveFiles.length === 0) {
|
|
1163
|
+
return customizations && customizations.modifiedFiles.length > 0 ? customizations : null;
|
|
1164
|
+
}
|
|
1165
|
+
if (validatedManifestFiles.length > 0 && configPreserveFiles.length > 0) {
|
|
1166
|
+
const merged = customizations || {
|
|
1167
|
+
modifiedFiles: [],
|
|
1168
|
+
preserveFiles: [],
|
|
1169
|
+
customComponents: [],
|
|
1170
|
+
lastUpdated: new Date().toISOString()
|
|
1171
|
+
};
|
|
1172
|
+
merged.preserveFiles = [...new Set([...validatedManifestFiles, ...configPreserveFiles])];
|
|
1173
|
+
return merged;
|
|
1064
1174
|
}
|
|
1065
1175
|
if (configPreserveFiles.length > 0) {
|
|
1066
1176
|
return {
|
|
1067
|
-
modifiedFiles: [],
|
|
1177
|
+
modifiedFiles: customizations?.modifiedFiles || [],
|
|
1068
1178
|
preserveFiles: configPreserveFiles,
|
|
1069
|
-
customComponents: [],
|
|
1179
|
+
customComponents: customizations?.customComponents || [],
|
|
1070
1180
|
lastUpdated: new Date().toISOString()
|
|
1071
1181
|
};
|
|
1072
1182
|
}
|
|
1073
|
-
|
|
1183
|
+
if (customizations) {
|
|
1184
|
+
customizations.preserveFiles = validatedManifestFiles;
|
|
1185
|
+
return customizations;
|
|
1186
|
+
}
|
|
1187
|
+
return null;
|
|
1074
1188
|
}
|
|
1075
1189
|
async function updateEntryDoc(targetDir, provider, config, options) {
|
|
1076
1190
|
const layout = getProviderLayout(provider);
|
|
@@ -1123,11 +1237,11 @@ async function update(options) {
|
|
|
1123
1237
|
return result;
|
|
1124
1238
|
}
|
|
1125
1239
|
await handleBackupIfRequested(options.targetDir, provider, !!options.backup, result);
|
|
1126
|
-
const manifestCustomizations =
|
|
1127
|
-
const configPreserveFiles = config
|
|
1128
|
-
const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles);
|
|
1240
|
+
const manifestCustomizations = await resolveManifestCustomizations(options, options.targetDir);
|
|
1241
|
+
const configPreserveFiles = resolveConfigPreserveFiles(options, config);
|
|
1242
|
+
const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles, options.targetDir);
|
|
1129
1243
|
const components = options.components || getAllUpdateComponents();
|
|
1130
|
-
await updateAllComponents(options.targetDir, provider, components, updateCheck, customizations, options, result);
|
|
1244
|
+
await updateAllComponents(options.targetDir, provider, components, updateCheck, customizations, options, result, config);
|
|
1131
1245
|
if (!options.components || options.components.length === 0) {
|
|
1132
1246
|
await updateEntryDoc(options.targetDir, provider, config, options);
|
|
1133
1247
|
}
|
|
@@ -1207,15 +1321,14 @@ async function componentHasUpdate(_targetDir, provider, component, config) {
|
|
|
1207
1321
|
const latestVersion = await getLatestVersion(provider);
|
|
1208
1322
|
return installedVersion !== latestVersion;
|
|
1209
1323
|
}
|
|
1210
|
-
async function updateComponent(targetDir, provider, component, customizations, options) {
|
|
1324
|
+
async function updateComponent(targetDir, provider, component, customizations, options, config) {
|
|
1211
1325
|
const preservedFiles = [];
|
|
1212
1326
|
const componentPath = getComponentPath2(provider, component);
|
|
1213
1327
|
const srcPath = resolveTemplatePath(componentPath);
|
|
1214
1328
|
const destPath = join5(targetDir, componentPath);
|
|
1215
|
-
const config = await loadConfig(targetDir);
|
|
1216
1329
|
const customComponents = config.customComponents || [];
|
|
1217
1330
|
const skipPaths = [];
|
|
1218
|
-
if (customizations && options.
|
|
1331
|
+
if (customizations && !options.forceOverwriteAll) {
|
|
1219
1332
|
const toPreserve = customizations.preserveFiles.filter((f) => f.startsWith(componentPath));
|
|
1220
1333
|
preservedFiles.push(...toPreserve);
|
|
1221
1334
|
skipPaths.push(...toPreserve);
|
package/package.json
CHANGED
|
@@ -25,104 +25,22 @@ public class UserService {
|
|
|
25
25
|
### 3. REST API Design
|
|
26
26
|
@RestController + @RequestMapping. Use @Validated for input, ResponseEntity for responses, proper HTTP status codes.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
@RestController
|
|
30
|
-
@RequestMapping("/api/v1/users")
|
|
31
|
-
@RequiredArgsConstructor
|
|
32
|
-
public class UserController {
|
|
33
|
-
private final UserService userService;
|
|
34
|
-
|
|
35
|
-
@GetMapping("/{id}")
|
|
36
|
-
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
|
|
37
|
-
return ResponseEntity.ok(userService.findById(id));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
@PostMapping
|
|
41
|
-
@ResponseStatus(HttpStatus.CREATED)
|
|
42
|
-
public UserResponse createUser(@Valid @RequestBody UserRequest request) {
|
|
43
|
-
return userService.create(request);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
```
|
|
28
|
+
See `examples/controller-example.java` for reference implementation.
|
|
47
29
|
|
|
48
30
|
### 4. Service Layer
|
|
49
31
|
Business logic in services. @Transactional boundaries at service level. Interface + implementation pattern.
|
|
50
32
|
|
|
51
|
-
|
|
52
|
-
@Service
|
|
53
|
-
@Transactional(readOnly = true)
|
|
54
|
-
@RequiredArgsConstructor
|
|
55
|
-
public class UserServiceImpl implements UserService {
|
|
56
|
-
private final UserRepository userRepository;
|
|
57
|
-
|
|
58
|
-
@Override
|
|
59
|
-
public UserResponse findById(Long id) {
|
|
60
|
-
User user = userRepository.findById(id)
|
|
61
|
-
.orElseThrow(() -> new UserNotFoundException(id));
|
|
62
|
-
return userMapper.toResponse(user);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
@Override
|
|
66
|
-
@Transactional
|
|
67
|
-
public UserResponse create(UserRequest request) {
|
|
68
|
-
User user = userMapper.toEntity(request);
|
|
69
|
-
return userMapper.toResponse(userRepository.save(user));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
```
|
|
33
|
+
See `examples/service-example.java` for reference implementation.
|
|
73
34
|
|
|
74
35
|
### 5. Data Access
|
|
75
36
|
Spring Data JPA. @Query or method naming for custom queries. @Entity with proper JPA annotations.
|
|
76
37
|
|
|
77
|
-
|
|
78
|
-
public interface UserRepository extends JpaRepository<User, Long> {
|
|
79
|
-
Optional<User> findByEmail(String email);
|
|
80
|
-
|
|
81
|
-
@Query("SELECT u FROM User u WHERE u.status = :status")
|
|
82
|
-
List<User> findByStatus(@Param("status") UserStatus status);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@Entity
|
|
86
|
-
@Table(name = "users")
|
|
87
|
-
@Getter
|
|
88
|
-
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
|
89
|
-
public class User {
|
|
90
|
-
@Id
|
|
91
|
-
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
92
|
-
private Long id;
|
|
93
|
-
|
|
94
|
-
@Column(nullable = false, unique = true)
|
|
95
|
-
private String email;
|
|
96
|
-
|
|
97
|
-
@Enumerated(EnumType.STRING)
|
|
98
|
-
private UserStatus status;
|
|
99
|
-
}
|
|
100
|
-
```
|
|
38
|
+
See `examples/repository-example.java` and `examples/entity-example.java` for reference implementations.
|
|
101
39
|
|
|
102
40
|
### 6. Exception Handling
|
|
103
41
|
@RestControllerAdvice for global handling. Domain-specific exceptions with proper HTTP status mapping.
|
|
104
42
|
|
|
105
|
-
|
|
106
|
-
@RestControllerAdvice
|
|
107
|
-
public class GlobalExceptionHandler {
|
|
108
|
-
@ExceptionHandler(UserNotFoundException.class)
|
|
109
|
-
@ResponseStatus(HttpStatus.NOT_FOUND)
|
|
110
|
-
public ErrorResponse handleUserNotFound(UserNotFoundException ex) {
|
|
111
|
-
return new ErrorResponse("USER_NOT_FOUND", ex.getMessage());
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
115
|
-
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
|
116
|
-
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
|
|
117
|
-
List<String> errors = ex.getBindingResult()
|
|
118
|
-
.getFieldErrors()
|
|
119
|
-
.stream()
|
|
120
|
-
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
|
121
|
-
.toList();
|
|
122
|
-
return new ErrorResponse("VALIDATION_ERROR", errors);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
```
|
|
43
|
+
See `examples/exception-handler-example.java` for reference implementation.
|
|
126
44
|
|
|
127
45
|
### 7. Configuration
|
|
128
46
|
Profile-based: application-{profile}.yml. @ConfigurationProperties for type-safe config. Externalize sensitive values.
|
|
@@ -138,80 +56,17 @@ spring:
|
|
|
138
56
|
password: ${DATABASE_PASSWORD}
|
|
139
57
|
```
|
|
140
58
|
|
|
141
|
-
|
|
142
|
-
@Configuration
|
|
143
|
-
@ConfigurationProperties(prefix = "app")
|
|
144
|
-
@Validated
|
|
145
|
-
public class AppProperties {
|
|
146
|
-
@NotBlank
|
|
147
|
-
private String name;
|
|
148
|
-
|
|
149
|
-
@Min(1)
|
|
150
|
-
private int maxConnections;
|
|
151
|
-
}
|
|
152
|
-
```
|
|
59
|
+
See `examples/config-properties-example.java` for type-safe configuration properties.
|
|
153
60
|
|
|
154
61
|
### 8. Security
|
|
155
62
|
Spring Security with SecurityFilterChain. Externalize secrets. Proper authentication/authorization patterns.
|
|
156
63
|
|
|
157
|
-
|
|
158
|
-
@Configuration
|
|
159
|
-
@EnableWebSecurity
|
|
160
|
-
@RequiredArgsConstructor
|
|
161
|
-
public class SecurityConfig {
|
|
162
|
-
@Bean
|
|
163
|
-
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
164
|
-
return http
|
|
165
|
-
.csrf(csrf -> csrf.disable())
|
|
166
|
-
.sessionManagement(session ->
|
|
167
|
-
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
168
|
-
.authorizeHttpRequests(auth -> auth
|
|
169
|
-
.requestMatchers("/api/v1/auth/**").permitAll()
|
|
170
|
-
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
|
|
171
|
-
.anyRequest().authenticated())
|
|
172
|
-
.build();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
```
|
|
64
|
+
See `examples/security-config-example.java` for reference implementation.
|
|
176
65
|
|
|
177
66
|
### 9. Testing
|
|
178
67
|
@WebMvcTest (controller), @DataJpaTest (repository), @SpringBootTest (integration), @MockBean for mocking.
|
|
179
68
|
|
|
180
|
-
|
|
181
|
-
// Controller test
|
|
182
|
-
@WebMvcTest(UserController.class)
|
|
183
|
-
class UserControllerTest {
|
|
184
|
-
@Autowired
|
|
185
|
-
private MockMvc mockMvc;
|
|
186
|
-
|
|
187
|
-
@MockBean
|
|
188
|
-
private UserService userService;
|
|
189
|
-
|
|
190
|
-
@Test
|
|
191
|
-
void getUser_shouldReturnUser() throws Exception {
|
|
192
|
-
given(userService.findById(1L))
|
|
193
|
-
.willReturn(new UserResponse(1L, "test@example.com"));
|
|
194
|
-
|
|
195
|
-
mockMvc.perform(get("/api/v1/users/1"))
|
|
196
|
-
.andExpect(status().isOk())
|
|
197
|
-
.andExpect(jsonPath("$.email").value("test@example.com"));
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Repository test
|
|
202
|
-
@DataJpaTest
|
|
203
|
-
class UserRepositoryTest {
|
|
204
|
-
@Autowired
|
|
205
|
-
private UserRepository userRepository;
|
|
206
|
-
|
|
207
|
-
@Test
|
|
208
|
-
void findByEmail_shouldReturnUser() {
|
|
209
|
-
User user = userRepository.save(new User("test@example.com"));
|
|
210
|
-
Optional<User> found = userRepository.findByEmail("test@example.com");
|
|
211
|
-
assertThat(found).isPresent();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
```
|
|
69
|
+
See `examples/controller-test-example.java` and `examples/repository-test-example.java` for reference implementations.
|
|
215
70
|
|
|
216
71
|
## Application
|
|
217
72
|
|
package/templates/.claude/skills/springboot-best-practices/examples/config-properties-example.java
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.example.demo.config;
|
|
2
|
+
|
|
3
|
+
import jakarta.validation.constraints.Min;
|
|
4
|
+
import jakarta.validation.constraints.NotBlank;
|
|
5
|
+
import lombok.Getter;
|
|
6
|
+
import lombok.Setter;
|
|
7
|
+
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
8
|
+
import org.springframework.context.annotation.Configuration;
|
|
9
|
+
import org.springframework.validation.annotation.Validated;
|
|
10
|
+
|
|
11
|
+
@Configuration
|
|
12
|
+
@ConfigurationProperties(prefix = "app")
|
|
13
|
+
@Validated
|
|
14
|
+
@Getter
|
|
15
|
+
@Setter
|
|
16
|
+
public class AppProperties {
|
|
17
|
+
@NotBlank
|
|
18
|
+
private String name;
|
|
19
|
+
|
|
20
|
+
@Min(1)
|
|
21
|
+
private int maxConnections;
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.example.demo.controller;
|
|
2
|
+
|
|
3
|
+
import com.example.demo.dto.UserRequest;
|
|
4
|
+
import com.example.demo.dto.UserResponse;
|
|
5
|
+
import com.example.demo.service.UserService;
|
|
6
|
+
import jakarta.validation.Valid;
|
|
7
|
+
import lombok.RequiredArgsConstructor;
|
|
8
|
+
import org.springframework.http.HttpStatus;
|
|
9
|
+
import org.springframework.http.ResponseEntity;
|
|
10
|
+
import org.springframework.web.bind.annotation.*;
|
|
11
|
+
|
|
12
|
+
@RestController
|
|
13
|
+
@RequestMapping("/api/v1/users")
|
|
14
|
+
@RequiredArgsConstructor
|
|
15
|
+
public class UserController {
|
|
16
|
+
private final UserService userService;
|
|
17
|
+
|
|
18
|
+
@GetMapping("/{id}")
|
|
19
|
+
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
|
|
20
|
+
return ResponseEntity.ok(userService.findById(id));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@PostMapping
|
|
24
|
+
@ResponseStatus(HttpStatus.CREATED)
|
|
25
|
+
public UserResponse createUser(@Valid @RequestBody UserRequest request) {
|
|
26
|
+
return userService.create(request);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/templates/.claude/skills/springboot-best-practices/examples/controller-test-example.java
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.example.demo.controller;
|
|
2
|
+
|
|
3
|
+
import com.example.demo.dto.UserResponse;
|
|
4
|
+
import com.example.demo.service.UserService;
|
|
5
|
+
import org.junit.jupiter.api.Test;
|
|
6
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
7
|
+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
|
8
|
+
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
9
|
+
import org.springframework.test.web.servlet.MockMvc;
|
|
10
|
+
|
|
11
|
+
import static org.mockito.BDDMockito.given;
|
|
12
|
+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
13
|
+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
14
|
+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
15
|
+
|
|
16
|
+
@WebMvcTest(UserController.class)
|
|
17
|
+
class UserControllerTest {
|
|
18
|
+
@Autowired
|
|
19
|
+
private MockMvc mockMvc;
|
|
20
|
+
|
|
21
|
+
@MockBean
|
|
22
|
+
private UserService userService;
|
|
23
|
+
|
|
24
|
+
@Test
|
|
25
|
+
void getUser_shouldReturnUser() throws Exception {
|
|
26
|
+
given(userService.findById(1L))
|
|
27
|
+
.willReturn(new UserResponse(1L, "test@example.com"));
|
|
28
|
+
|
|
29
|
+
mockMvc.perform(get("/api/v1/users/1"))
|
|
30
|
+
.andExpect(status().isOk())
|
|
31
|
+
.andExpect(jsonPath("$.email").value("test@example.com"));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.example.demo.entity;
|
|
2
|
+
|
|
3
|
+
import jakarta.persistence.*;
|
|
4
|
+
import lombok.AccessLevel;
|
|
5
|
+
import lombok.Getter;
|
|
6
|
+
import lombok.NoArgsConstructor;
|
|
7
|
+
|
|
8
|
+
@Entity
|
|
9
|
+
@Table(name = "users")
|
|
10
|
+
@Getter
|
|
11
|
+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
|
12
|
+
public class User {
|
|
13
|
+
@Id
|
|
14
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
15
|
+
private Long id;
|
|
16
|
+
|
|
17
|
+
@Column(nullable = false, unique = true)
|
|
18
|
+
private String email;
|
|
19
|
+
|
|
20
|
+
@Enumerated(EnumType.STRING)
|
|
21
|
+
private UserStatus status;
|
|
22
|
+
}
|
package/templates/.claude/skills/springboot-best-practices/examples/exception-handler-example.java
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.example.demo.exception;
|
|
2
|
+
|
|
3
|
+
import com.example.demo.dto.ErrorResponse;
|
|
4
|
+
import org.springframework.http.HttpStatus;
|
|
5
|
+
import org.springframework.web.bind.MethodArgumentNotValidException;
|
|
6
|
+
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
7
|
+
import org.springframework.web.bind.annotation.ResponseStatus;
|
|
8
|
+
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
9
|
+
|
|
10
|
+
import java.util.List;
|
|
11
|
+
|
|
12
|
+
@RestControllerAdvice
|
|
13
|
+
public class GlobalExceptionHandler {
|
|
14
|
+
@ExceptionHandler(UserNotFoundException.class)
|
|
15
|
+
@ResponseStatus(HttpStatus.NOT_FOUND)
|
|
16
|
+
public ErrorResponse handleUserNotFound(UserNotFoundException ex) {
|
|
17
|
+
return new ErrorResponse("USER_NOT_FOUND", ex.getMessage());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
21
|
+
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
|
22
|
+
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
|
|
23
|
+
List<String> errors = ex.getBindingResult()
|
|
24
|
+
.getFieldErrors()
|
|
25
|
+
.stream()
|
|
26
|
+
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
|
27
|
+
.toList();
|
|
28
|
+
return new ErrorResponse("VALIDATION_ERROR", errors);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.example.demo.repository;
|
|
2
|
+
|
|
3
|
+
import com.example.demo.entity.User;
|
|
4
|
+
import com.example.demo.entity.UserStatus;
|
|
5
|
+
import org.springframework.data.jpa.repository.JpaRepository;
|
|
6
|
+
import org.springframework.data.jpa.repository.Query;
|
|
7
|
+
import org.springframework.data.repository.query.Param;
|
|
8
|
+
|
|
9
|
+
import java.util.List;
|
|
10
|
+
import java.util.Optional;
|
|
11
|
+
|
|
12
|
+
public interface UserRepository extends JpaRepository<User, Long> {
|
|
13
|
+
Optional<User> findByEmail(String email);
|
|
14
|
+
|
|
15
|
+
@Query("SELECT u FROM User u WHERE u.status = :status")
|
|
16
|
+
List<User> findByStatus(@Param("status") UserStatus status);
|
|
17
|
+
}
|
package/templates/.claude/skills/springboot-best-practices/examples/repository-test-example.java
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package com.example.demo.repository;
|
|
2
|
+
|
|
3
|
+
import com.example.demo.entity.User;
|
|
4
|
+
import org.junit.jupiter.api.Test;
|
|
5
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
6
|
+
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
|
7
|
+
|
|
8
|
+
import java.util.Optional;
|
|
9
|
+
|
|
10
|
+
import static org.assertj.core.api.Assertions.assertThat;
|
|
11
|
+
|
|
12
|
+
@DataJpaTest
|
|
13
|
+
class UserRepositoryTest {
|
|
14
|
+
@Autowired
|
|
15
|
+
private UserRepository userRepository;
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
void findByEmail_shouldReturnUser() {
|
|
19
|
+
User user = userRepository.save(new User("test@example.com"));
|
|
20
|
+
Optional<User> found = userRepository.findByEmail("test@example.com");
|
|
21
|
+
assertThat(found).isPresent();
|
|
22
|
+
}
|
|
23
|
+
}
|
package/templates/.claude/skills/springboot-best-practices/examples/security-config-example.java
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.example.demo.config;
|
|
2
|
+
|
|
3
|
+
import lombok.RequiredArgsConstructor;
|
|
4
|
+
import org.springframework.context.annotation.Bean;
|
|
5
|
+
import org.springframework.context.annotation.Configuration;
|
|
6
|
+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
7
|
+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
8
|
+
import org.springframework.security.config.http.SessionCreationPolicy;
|
|
9
|
+
import org.springframework.security.web.SecurityFilterChain;
|
|
10
|
+
|
|
11
|
+
@Configuration
|
|
12
|
+
@EnableWebSecurity
|
|
13
|
+
@RequiredArgsConstructor
|
|
14
|
+
public class SecurityConfig {
|
|
15
|
+
@Bean
|
|
16
|
+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
17
|
+
return http
|
|
18
|
+
.csrf(csrf -> csrf.disable())
|
|
19
|
+
.sessionManagement(session ->
|
|
20
|
+
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
21
|
+
.authorizeHttpRequests(auth -> auth
|
|
22
|
+
.requestMatchers("/api/v1/auth/**").permitAll()
|
|
23
|
+
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
|
|
24
|
+
.anyRequest().authenticated())
|
|
25
|
+
.build();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.example.demo.service;
|
|
2
|
+
|
|
3
|
+
import com.example.demo.dto.UserRequest;
|
|
4
|
+
import com.example.demo.dto.UserResponse;
|
|
5
|
+
import com.example.demo.entity.User;
|
|
6
|
+
import com.example.demo.exception.UserNotFoundException;
|
|
7
|
+
import com.example.demo.mapper.UserMapper;
|
|
8
|
+
import com.example.demo.repository.UserRepository;
|
|
9
|
+
import lombok.RequiredArgsConstructor;
|
|
10
|
+
import org.springframework.stereotype.Service;
|
|
11
|
+
import org.springframework.transaction.annotation.Transactional;
|
|
12
|
+
|
|
13
|
+
@Service
|
|
14
|
+
@Transactional(readOnly = true)
|
|
15
|
+
@RequiredArgsConstructor
|
|
16
|
+
public class UserServiceImpl implements UserService {
|
|
17
|
+
private final UserRepository userRepository;
|
|
18
|
+
private final UserMapper userMapper;
|
|
19
|
+
|
|
20
|
+
@Override
|
|
21
|
+
public UserResponse findById(Long id) {
|
|
22
|
+
User user = userRepository.findById(id)
|
|
23
|
+
.orElseThrow(() -> new UserNotFoundException(id));
|
|
24
|
+
return userMapper.toResponse(user);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Override
|
|
28
|
+
@Transactional
|
|
29
|
+
public UserResponse create(UserRequest request) {
|
|
30
|
+
User user = userMapper.toEntity(request);
|
|
31
|
+
return userMapper.toResponse(userRepository.save(user));
|
|
32
|
+
}
|
|
33
|
+
}
|