oh-my-customcode 0.31.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +9 -7
  2. package/dist/cli/index.js +502 -81
  3. package/dist/index.js +337 -41
  4. package/package.json +1 -1
  5. package/templates/.claude/hooks/scripts/session-env-check.sh +52 -0
  6. package/templates/.claude/rules/MUST-agent-design.md +11 -0
  7. package/templates/.claude/rules/MUST-agent-identification.md +4 -4
  8. package/templates/.claude/rules/MUST-agent-teams.md +17 -18
  9. package/templates/.claude/rules/MUST-continuous-improvement.md +3 -3
  10. package/templates/.claude/rules/MUST-orchestrator-coordination.md +24 -42
  11. package/templates/.claude/rules/MUST-parallel-execution.md +18 -40
  12. package/templates/.claude/rules/MUST-sync-verification.md +6 -6
  13. package/templates/.claude/rules/MUST-tool-identification.md +39 -10
  14. package/templates/.claude/rules/SHOULD-ontology-rag-routing.md +49 -0
  15. package/templates/.claude/skills/airflow-best-practices/SKILL.md +1 -0
  16. package/templates/.claude/skills/analysis/SKILL.md +2 -1
  17. package/templates/.claude/skills/audit-agents/SKILL.md +4 -3
  18. package/templates/.claude/skills/aws-best-practices/SKILL.md +1 -0
  19. package/templates/.claude/skills/claude-code-bible/SKILL.md +1 -0
  20. package/templates/.claude/skills/codex-exec/SKILL.md +5 -4
  21. package/templates/.claude/skills/create-agent/SKILL.md +1 -0
  22. package/templates/.claude/skills/cve-triage/SKILL.md +1 -0
  23. package/templates/.claude/skills/dag-orchestration/SKILL.md +1 -0
  24. package/templates/.claude/skills/dbt-best-practices/SKILL.md +1 -0
  25. package/templates/.claude/skills/de-lead-routing/SKILL.md +1 -0
  26. package/templates/.claude/skills/dev-lead-routing/SKILL.md +1 -0
  27. package/templates/.claude/skills/dev-refactor/SKILL.md +1 -0
  28. package/templates/.claude/skills/dev-review/SKILL.md +1 -0
  29. package/templates/.claude/skills/django-best-practices/SKILL.md +1 -0
  30. package/templates/.claude/skills/docker-best-practices/SKILL.md +1 -0
  31. package/templates/.claude/skills/fastapi-best-practices/SKILL.md +1 -0
  32. package/templates/.claude/skills/fix-refs/SKILL.md +1 -0
  33. package/templates/.claude/skills/flutter-best-practices/SKILL.md +1 -0
  34. package/templates/.claude/skills/go-backend-best-practices/SKILL.md +1 -0
  35. package/templates/.claude/skills/go-best-practices/SKILL.md +1 -0
  36. package/templates/.claude/skills/help/SKILL.md +1 -0
  37. package/templates/.claude/skills/intent-detection/SKILL.md +1 -0
  38. package/templates/.claude/skills/java21-best-practices/SKILL.md +305 -0
  39. package/templates/.claude/skills/jinja2-prompts/SKILL.md +1 -0
  40. package/templates/.claude/skills/kafka-best-practices/SKILL.md +1 -0
  41. package/templates/.claude/skills/kotlin-best-practices/SKILL.md +1 -0
  42. package/templates/.claude/skills/lists/SKILL.md +1 -0
  43. package/templates/.claude/skills/memory-management/SKILL.md +1 -0
  44. package/templates/.claude/skills/memory-recall/SKILL.md +1 -0
  45. package/templates/.claude/skills/memory-save/SKILL.md +1 -0
  46. package/templates/.claude/skills/model-escalation/SKILL.md +1 -0
  47. package/templates/.claude/skills/monitoring-setup/SKILL.md +1 -0
  48. package/templates/.claude/skills/multi-model-verification/SKILL.md +1 -0
  49. package/templates/.claude/skills/npm-audit/SKILL.md +1 -0
  50. package/templates/.claude/skills/npm-publish/SKILL.md +1 -0
  51. package/templates/.claude/skills/npm-version/SKILL.md +1 -0
  52. package/templates/.claude/skills/optimize-analyze/SKILL.md +1 -0
  53. package/templates/.claude/skills/optimize-bundle/SKILL.md +1 -0
  54. package/templates/.claude/skills/optimize-report/SKILL.md +1 -0
  55. package/templates/.claude/skills/pipeline-architecture-patterns/SKILL.md +1 -0
  56. package/templates/.claude/skills/pipeline-guards/SKILL.md +2 -0
  57. package/templates/.claude/skills/postgres-best-practices/SKILL.md +1 -0
  58. package/templates/.claude/skills/pr-auto-improve/SKILL.md +1 -0
  59. package/templates/.claude/skills/python-best-practices/SKILL.md +1 -0
  60. package/templates/.claude/skills/qa-lead-routing/SKILL.md +1 -0
  61. package/templates/.claude/skills/react-best-practices/SKILL.md +1 -0
  62. package/templates/.claude/skills/redis-best-practices/SKILL.md +1 -0
  63. package/templates/.claude/skills/research/SKILL.md +1 -0
  64. package/templates/.claude/skills/result-aggregation/SKILL.md +1 -0
  65. package/templates/.claude/skills/rust-best-practices/SKILL.md +1 -0
  66. package/templates/.claude/skills/sauron-watch/SKILL.md +1 -0
  67. package/templates/.claude/skills/secretary-routing/SKILL.md +1 -0
  68. package/templates/.claude/skills/skills-sh-search/SKILL.md +1 -0
  69. package/templates/.claude/skills/snowflake-best-practices/SKILL.md +1 -0
  70. package/templates/.claude/skills/spark-best-practices/SKILL.md +1 -0
  71. package/templates/.claude/skills/springboot-best-practices/SKILL.md +1 -0
  72. package/templates/.claude/skills/status/SKILL.md +1 -0
  73. package/templates/.claude/skills/structured-dev-cycle/SKILL.md +1 -0
  74. package/templates/.claude/skills/stuck-recovery/SKILL.md +1 -0
  75. package/templates/.claude/skills/supabase-postgres-best-practices/SKILL.md +2 -1
  76. package/templates/.claude/skills/task-decomposition/SKILL.md +2 -0
  77. package/templates/.claude/skills/typescript-best-practices/SKILL.md +1 -0
  78. package/templates/.claude/skills/update-docs/SKILL.md +4 -3
  79. package/templates/.claude/skills/update-external/SKILL.md +1 -0
  80. package/templates/.claude/skills/vercel-deploy/SKILL.md +1 -0
  81. package/templates/.claude/skills/web-design-guidelines/SKILL.md +1 -0
  82. package/templates/.claude/skills/worker-reviewer-pipeline/SKILL.md +2 -0
  83. package/templates/.claude/skills/writing-clearly-and-concisely/SKILL.md +3 -2
  84. package/templates/CLAUDE.md.en +6 -5
  85. package/templates/CLAUDE.md.ko +6 -5
  86. package/templates/manifest.json +4 -4
package/dist/index.js CHANGED
@@ -355,6 +355,16 @@ var MESSAGES = {
355
355
  "install.entry_md_installed": "{{entry}} installed ({{language}})",
356
356
  "install.entry_md_not_found": "{{entry}} template not found for {{language}}",
357
357
  "install.entry_md_skipped": "{{entry}} skipped ({{reason}})",
358
+ "install.lockfile_generated": "Lockfile generated ({{files}} files tracked)",
359
+ "install.lockfile_failed": "Failed to generate lockfile: {{error}}",
360
+ "lockfile.not_found": "Lockfile not found: {{path}}",
361
+ "lockfile.invalid_version": "Invalid lockfile version: {{path}}",
362
+ "lockfile.invalid_structure": "Invalid lockfile structure: {{path}}",
363
+ "lockfile.read_failed": "Failed to read lockfile: {{path}} — {{error}}",
364
+ "lockfile.written": "Lockfile written: {{path}}",
365
+ "lockfile.component_dir_missing": "Component directory missing: {{path}}",
366
+ "lockfile.hash_failed": "Failed to hash file: {{path}} — {{error}}",
367
+ "lockfile.entry_added": "Lockfile entry added: {{path}} ({{component}})",
358
368
  "update.start": "Checking for updates...",
359
369
  "update.success": "Updated from {{from}} to {{to}}",
360
370
  "update.components_synced": "Components synced (version {{version}}): {{components}}",
@@ -364,6 +374,8 @@ var MESSAGES = {
364
374
  "update.dry_run": "Would update {{component}}",
365
375
  "update.component_updated": "Updated {{component}}",
366
376
  "update.file_applied": "Applied update to {{path}}",
377
+ "update.lockfile_regenerated": "Lockfile regenerated ({{files}} files tracked)",
378
+ "update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
367
379
  "config.load_failed": "Failed to load config: {{error}}",
368
380
  "config.not_found": "Config not found at {{path}}, using defaults",
369
381
  "config.saved": "Config saved to {{path}}",
@@ -387,6 +399,16 @@ var MESSAGES = {
387
399
  "install.entry_md_installed": "{{entry}} 설치 완료 ({{language}})",
388
400
  "install.entry_md_not_found": "{{language}}용 {{entry}} 템플릿 없음",
389
401
  "install.entry_md_skipped": "{{entry}} 건너뜀 ({{reason}})",
402
+ "install.lockfile_generated": "잠금 파일 생성 완료 ({{files}}개 파일 추적)",
403
+ "install.lockfile_failed": "잠금 파일 생성 실패: {{error}}",
404
+ "lockfile.not_found": "잠금 파일 없음: {{path}}",
405
+ "lockfile.invalid_version": "잠금 파일 버전 유효하지 않음: {{path}}",
406
+ "lockfile.invalid_structure": "잠금 파일 구조 유효하지 않음: {{path}}",
407
+ "lockfile.read_failed": "잠금 파일 읽기 실패: {{path}} — {{error}}",
408
+ "lockfile.written": "잠금 파일 기록됨: {{path}}",
409
+ "lockfile.component_dir_missing": "컴포넌트 디렉토리 없음: {{path}}",
410
+ "lockfile.hash_failed": "파일 해시 실패: {{path}} — {{error}}",
411
+ "lockfile.entry_added": "잠금 파일 항목 추가: {{path}} ({{component}})",
390
412
  "update.start": "업데이트 확인 중...",
391
413
  "update.success": "{{from}}에서 {{to}}로 업데이트 완료",
392
414
  "update.components_synced": "컴포넌트 동기화 완료 (버전 {{version}}): {{components}}",
@@ -396,6 +418,8 @@ var MESSAGES = {
396
418
  "update.dry_run": "{{component}} 업데이트 예정",
397
419
  "update.component_updated": "{{component}} 업데이트 완료",
398
420
  "update.file_applied": "{{path}} 업데이트 적용",
421
+ "update.lockfile_regenerated": "잠금 파일 재생성 완료 ({{files}}개 파일 추적)",
422
+ "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
399
423
  "config.load_failed": "설정 로드 실패: {{error}}",
400
424
  "config.not_found": "{{path}}에 설정 없음, 기본값 사용",
401
425
  "config.saved": "설정 저장: {{path}}",
@@ -875,14 +899,39 @@ function getDefaultWorkflow() {
875
899
  }
876
900
  // src/core/installer.ts
877
901
  init_fs();
878
- import { readFile as fsReadFile, writeFile as fsWriteFile, rename } from "node:fs/promises";
879
- import { basename as basename2, join as join4 } from "node:path";
902
+ import {
903
+ readFile as fsReadFile,
904
+ writeFile as fsWriteFile,
905
+ readdir as readdir2,
906
+ rename,
907
+ stat as stat2
908
+ } from "node:fs/promises";
909
+ import { basename as basename2, join as join5 } from "node:path";
880
910
 
881
911
  // src/core/file-preservation.ts
882
912
  init_fs();
883
913
  import { basename, join as join3 } from "node:path";
884
914
  var DEFAULT_CRITICAL_FILES = ["settings.json", "settings.local.json"];
885
915
  var DEFAULT_CRITICAL_DIRECTORIES = ["agent-memory", "agent-memory-local"];
916
+ var PROTECTED_FRAMEWORK_FILES = ["CLAUDE.md", "AGENTS.md"];
917
+ var PROTECTED_RULE_PATTERNS = ["rules/MUST-*.md"];
918
+ function isProtectedFile(relativePath) {
919
+ const basename2 = relativePath.split("/").pop() ?? "";
920
+ if (PROTECTED_FRAMEWORK_FILES.includes(basename2)) {
921
+ return true;
922
+ }
923
+ for (const pattern of PROTECTED_RULE_PATTERNS) {
924
+ if (matchesGlobPattern(relativePath, pattern)) {
925
+ return true;
926
+ }
927
+ }
928
+ return false;
929
+ }
930
+ function matchesGlobPattern(filePath, pattern) {
931
+ const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
932
+ const regex = new RegExp(`(^|/)${regexStr}$`);
933
+ return regex.test(filePath);
934
+ }
886
935
  async function extractSingleFile(fileName, rootDir, tempDir, result) {
887
936
  const srcPath = join3(rootDir, fileName);
888
937
  const destPath = join3(tempDir, fileName);
@@ -1041,11 +1090,152 @@ function getComponentPath(component) {
1041
1090
  return `.claude/${component}`;
1042
1091
  }
1043
1092
 
1093
+ // src/core/lockfile.ts
1094
+ init_fs();
1095
+ import { createHash } from "node:crypto";
1096
+ import { createReadStream } from "node:fs";
1097
+ import { readdir, stat } from "node:fs/promises";
1098
+ import { join as join4, relative as relative2 } from "node:path";
1099
+ var LOCKFILE_NAME = ".omcustom.lock.json";
1100
+ var LOCKFILE_VERSION = 1;
1101
+ var LOCKFILE_COMPONENTS = [
1102
+ "rules",
1103
+ "agents",
1104
+ "skills",
1105
+ "hooks",
1106
+ "contexts",
1107
+ "ontology",
1108
+ "guides"
1109
+ ];
1110
+ var COMPONENT_PATHS = LOCKFILE_COMPONENTS.map((component) => [getComponentPath(component), component]);
1111
+ function computeFileHash(filePath) {
1112
+ return new Promise((resolve2, reject) => {
1113
+ const hash = createHash("sha256");
1114
+ const stream = createReadStream(filePath);
1115
+ stream.on("error", (err) => {
1116
+ reject(err);
1117
+ });
1118
+ stream.on("data", (chunk) => {
1119
+ hash.update(chunk);
1120
+ });
1121
+ stream.on("end", () => {
1122
+ resolve2(hash.digest("hex"));
1123
+ });
1124
+ });
1125
+ }
1126
+ async function writeLockfile(targetDir, lockfile) {
1127
+ const lockfilePath = join4(targetDir, LOCKFILE_NAME);
1128
+ await writeJsonFile(lockfilePath, lockfile);
1129
+ debug("lockfile.written", { path: lockfilePath });
1130
+ }
1131
+ function resolveComponent(relativePath) {
1132
+ const normalized = relativePath.replace(/\\/g, "/");
1133
+ for (const [prefix, component] of COMPONENT_PATHS) {
1134
+ if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
1135
+ return component;
1136
+ }
1137
+ }
1138
+ return "unknown";
1139
+ }
1140
+ async function collectFiles(dir, projectRoot, isTopLevel) {
1141
+ const results = [];
1142
+ let entries;
1143
+ try {
1144
+ entries = await readdir(dir);
1145
+ } catch {
1146
+ return results;
1147
+ }
1148
+ for (const entry of entries) {
1149
+ if (isTopLevel && entry.startsWith(".") && entry !== ".claude") {
1150
+ continue;
1151
+ }
1152
+ const fullPath = join4(dir, entry);
1153
+ let fileStat;
1154
+ try {
1155
+ fileStat = await stat(fullPath);
1156
+ } catch {
1157
+ continue;
1158
+ }
1159
+ if (fileStat.isDirectory()) {
1160
+ const subFiles = await collectFiles(fullPath, projectRoot, false);
1161
+ results.push(...subFiles);
1162
+ } else if (fileStat.isFile()) {
1163
+ results.push(fullPath);
1164
+ }
1165
+ }
1166
+ return results;
1167
+ }
1168
+ async function generateLockfile(targetDir, generatorVersion, templateVersion) {
1169
+ const files = {};
1170
+ const componentRoots = COMPONENT_PATHS.map(([prefix]) => join4(targetDir, prefix));
1171
+ for (const componentRoot of componentRoots) {
1172
+ const exists = await fileExists(componentRoot);
1173
+ if (!exists) {
1174
+ debug("lockfile.component_dir_missing", { path: componentRoot });
1175
+ continue;
1176
+ }
1177
+ const allFiles = await collectFiles(componentRoot, targetDir, false);
1178
+ for (const absolutePath of allFiles) {
1179
+ const relativePath = relative2(targetDir, absolutePath).replace(/\\/g, "/");
1180
+ let hash;
1181
+ let size;
1182
+ try {
1183
+ hash = await computeFileHash(absolutePath);
1184
+ const fileStat = await stat(absolutePath);
1185
+ size = fileStat.size;
1186
+ } catch (err) {
1187
+ warn("lockfile.hash_failed", { path: absolutePath, error: String(err) });
1188
+ continue;
1189
+ }
1190
+ const component = resolveComponent(relativePath);
1191
+ files[relativePath] = {
1192
+ templateHash: hash,
1193
+ size,
1194
+ component
1195
+ };
1196
+ debug("lockfile.entry_added", { path: relativePath, component });
1197
+ }
1198
+ }
1199
+ return {
1200
+ lockfileVersion: LOCKFILE_VERSION,
1201
+ generatorVersion,
1202
+ generatedAt: new Date().toISOString(),
1203
+ templateVersion,
1204
+ files
1205
+ };
1206
+ }
1207
+ async function generateAndWriteLockfileForDir(targetDir) {
1208
+ try {
1209
+ const packageRoot = getPackageRoot();
1210
+ const manifest = await readJsonFile(join4(packageRoot, "templates", "manifest.json"));
1211
+ const { version: generatorVersion } = await readJsonFile(join4(packageRoot, "package.json"));
1212
+ const lockfile = await generateLockfile(targetDir, generatorVersion, manifest.version);
1213
+ await writeLockfile(targetDir, lockfile);
1214
+ return { fileCount: Object.keys(lockfile.files).length };
1215
+ } catch (err) {
1216
+ const msg = err instanceof Error ? err.message : String(err);
1217
+ return { fileCount: 0, warning: `Lockfile generation failed: ${msg}` };
1218
+ }
1219
+ }
1220
+
1221
+ // src/core/scope-filter.ts
1222
+ function getSkillScope(content) {
1223
+ const cleaned = content.replace(/^\uFEFF/, "");
1224
+ const frontmatter = cleaned.match(/^---\r?\n([\s\S]*?)\r?\n---/);
1225
+ if (!frontmatter)
1226
+ return "core";
1227
+ const match = frontmatter[1].match(/^scope:\s*(core|harness|package)\s*$/m);
1228
+ return match?.[1] ?? "core";
1229
+ }
1230
+ function shouldInstallSkill(scope) {
1231
+ return scope !== "package";
1232
+ }
1233
+
1044
1234
  // src/core/installer.ts
1045
1235
  var DEFAULT_LANGUAGE = "en";
1046
1236
  function getTemplateDir() {
1047
1237
  const packageRoot = getPackageRoot();
1048
- return join4(packageRoot, "templates");
1238
+ return join5(packageRoot, "templates");
1049
1239
  }
1050
1240
  function createInstallResult(targetDir) {
1051
1241
  return {
@@ -1067,7 +1257,7 @@ async function handleBackup(targetDir, shouldBackup, result) {
1067
1257
  if (!shouldBackup)
1068
1258
  return null;
1069
1259
  const layout = getProviderLayout();
1070
- const rootDir = join4(targetDir, layout.rootDir);
1260
+ const rootDir = join5(targetDir, layout.rootDir);
1071
1261
  let preservation = null;
1072
1262
  if (await fileExists(rootDir)) {
1073
1263
  const { createTempDir: createTempDir2 } = await Promise.resolve().then(() => (init_fs(), exports_fs));
@@ -1124,8 +1314,8 @@ async function installSingleComponent(targetDir, component, options, result) {
1124
1314
  }
1125
1315
  async function installStatusline(targetDir, options, _result) {
1126
1316
  const layout = getProviderLayout();
1127
- const srcPath = resolveTemplatePath(join4(layout.rootDir, "statusline.sh"));
1128
- const destPath = join4(targetDir, layout.rootDir, "statusline.sh");
1317
+ const srcPath = resolveTemplatePath(join5(layout.rootDir, "statusline.sh"));
1318
+ const destPath = join5(targetDir, layout.rootDir, "statusline.sh");
1129
1319
  if (!await fileExists(srcPath)) {
1130
1320
  debug("install.statusline_not_found", { path: srcPath });
1131
1321
  return;
@@ -1143,7 +1333,7 @@ async function installStatusline(targetDir, options, _result) {
1143
1333
  }
1144
1334
  async function installSettingsLocal(targetDir, result) {
1145
1335
  const layout = getProviderLayout();
1146
- const settingsPath = join4(targetDir, layout.rootDir, "settings.local.json");
1336
+ const settingsPath = join5(targetDir, layout.rootDir, "settings.local.json");
1147
1337
  const statusLineConfig = {
1148
1338
  statusLine: {
1149
1339
  type: "command",
@@ -1200,7 +1390,7 @@ async function install(options) {
1200
1390
  await installEntryDocWithTracking(options.targetDir, options, result);
1201
1391
  if (preservation) {
1202
1392
  const layout = getProviderLayout();
1203
- const rootDir = join4(options.targetDir, layout.rootDir);
1393
+ const rootDir = join5(options.targetDir, layout.rootDir);
1204
1394
  const restoration = await restoreCriticalFiles(rootDir, preservation);
1205
1395
  if (restoration.restoredFiles.length > 0 || restoration.restoredDirs.length > 0) {
1206
1396
  info("install.restored", {
@@ -1216,6 +1406,13 @@ async function install(options) {
1216
1406
  await cleanupPreservation(preservation.tempDir);
1217
1407
  }
1218
1408
  await updateInstallConfig(options.targetDir, options, result.installedComponents);
1409
+ const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir);
1410
+ if (lockfileResult.warning) {
1411
+ result.warnings.push(lockfileResult.warning);
1412
+ warn("install.lockfile_failed", { error: lockfileResult.warning });
1413
+ } else {
1414
+ info("install.lockfile_generated", { files: String(lockfileResult.fileCount) });
1415
+ }
1219
1416
  result.success = true;
1220
1417
  success("install.success");
1221
1418
  } catch (err) {
@@ -1227,7 +1424,7 @@ async function install(options) {
1227
1424
  }
1228
1425
  async function copyTemplates(targetDir, templatePath, options) {
1229
1426
  const srcPath = resolveTemplatePath(templatePath);
1230
- const destPath = join4(targetDir, templatePath);
1427
+ const destPath = join5(targetDir, templatePath);
1231
1428
  await copyDirectory(srcPath, destPath, {
1232
1429
  overwrite: options?.overwrite ?? false,
1233
1430
  preserveSymlinks: options?.preserveSymlinks ?? true,
@@ -1237,14 +1434,14 @@ async function copyTemplates(targetDir, templatePath, options) {
1237
1434
  async function createDirectoryStructure(targetDir) {
1238
1435
  const layout = getProviderLayout();
1239
1436
  for (const dir of layout.directoryStructure) {
1240
- const fullPath = join4(targetDir, dir);
1437
+ const fullPath = join5(targetDir, dir);
1241
1438
  await ensureDirectory(fullPath);
1242
1439
  }
1243
1440
  }
1244
1441
  async function getTemplateManifest() {
1245
1442
  const packageRoot = getPackageRoot();
1246
1443
  const layout = getProviderLayout();
1247
- const manifestPath = join4(packageRoot, "templates", layout.manifestFile);
1444
+ const manifestPath = join5(packageRoot, "templates", layout.manifestFile);
1248
1445
  if (await fileExists(manifestPath)) {
1249
1446
  return readJsonFile(manifestPath);
1250
1447
  }
@@ -1263,12 +1460,35 @@ async function getTemplateManifest() {
1263
1460
  function getAllComponents() {
1264
1461
  return ["rules", "agents", "skills", "guides", "hooks", "contexts", "ontology"];
1265
1462
  }
1463
+ async function installSkillsWithScopeFilter(srcPath, destPath, options) {
1464
+ await ensureDirectory(destPath);
1465
+ const entries = await readdir2(srcPath);
1466
+ for (const entry of entries) {
1467
+ const entrySrcPath = join5(srcPath, entry);
1468
+ if (!(await stat2(entrySrcPath)).isDirectory())
1469
+ continue;
1470
+ const skillMdPath = join5(entrySrcPath, "SKILL.md");
1471
+ if (await fileExists(skillMdPath)) {
1472
+ const content = await fsReadFile(skillMdPath, "utf-8");
1473
+ const scope = getSkillScope(content);
1474
+ if (!shouldInstallSkill(scope)) {
1475
+ debug("install.skill_scope_excluded", { skill: entry, scope });
1476
+ continue;
1477
+ }
1478
+ }
1479
+ await copyDirectory(entrySrcPath, join5(destPath, entry), {
1480
+ overwrite: !!(options.force || options.backup),
1481
+ preserveSymlinks: true,
1482
+ preserveTimestamps: true
1483
+ });
1484
+ }
1485
+ }
1266
1486
  async function installComponent(targetDir, component, options) {
1267
1487
  if (component === "entry-md") {
1268
1488
  return false;
1269
1489
  }
1270
1490
  const templatePath = getComponentPath(component);
1271
- const destPath = join4(targetDir, templatePath);
1491
+ const destPath = join5(targetDir, templatePath);
1272
1492
  const destExists = await fileExists(destPath);
1273
1493
  if (destExists && !options.force && !options.backup) {
1274
1494
  debug("install.component_skipped", { component });
@@ -1279,11 +1499,15 @@ async function installComponent(targetDir, component, options) {
1279
1499
  warn("install.template_not_found", { component, path: srcPath });
1280
1500
  return false;
1281
1501
  }
1282
- await copyDirectory(srcPath, destPath, {
1283
- overwrite: !!(options.force || options.backup),
1284
- preserveSymlinks: true,
1285
- preserveTimestamps: true
1286
- });
1502
+ if (component === "skills") {
1503
+ await installSkillsWithScopeFilter(srcPath, destPath, options);
1504
+ } else {
1505
+ await copyDirectory(srcPath, destPath, {
1506
+ overwrite: !!(options.force || options.backup),
1507
+ preserveSymlinks: true,
1508
+ preserveTimestamps: true
1509
+ });
1510
+ }
1287
1511
  debug("install.component_installed", { component });
1288
1512
  return true;
1289
1513
  }
@@ -1296,7 +1520,7 @@ async function installEntryDoc(targetDir, language, overwrite = false) {
1296
1520
  const layout = getProviderLayout();
1297
1521
  const templateFile = getEntryTemplateName(language);
1298
1522
  const srcPath = resolveTemplatePath(templateFile);
1299
- const destPath = join4(targetDir, layout.entryFile);
1523
+ const destPath = join5(targetDir, layout.entryFile);
1300
1524
  if (!await fileExists(srcPath)) {
1301
1525
  warn("install.entry_md_not_found", { language, path: srcPath, entry: layout.entryFile });
1302
1526
  return false;
@@ -1317,7 +1541,7 @@ async function installEntryDoc(targetDir, language, overwrite = false) {
1317
1541
  }
1318
1542
  async function backupExisting(sourcePath, backupDir) {
1319
1543
  const name = basename2(sourcePath);
1320
- const backupPath = join4(backupDir, name);
1544
+ const backupPath = join5(backupDir, name);
1321
1545
  await rename(sourcePath, backupPath);
1322
1546
  return backupPath;
1323
1547
  }
@@ -1326,7 +1550,7 @@ async function checkExistingPaths(targetDir) {
1326
1550
  const pathsToCheck = [layout.entryFile, layout.rootDir, "guides"];
1327
1551
  const existingPaths = [];
1328
1552
  for (const relativePath of pathsToCheck) {
1329
- const fullPath = join4(targetDir, relativePath);
1553
+ const fullPath = join5(targetDir, relativePath);
1330
1554
  if (await fileExists(fullPath)) {
1331
1555
  existingPaths.push(relativePath);
1332
1556
  }
@@ -1340,11 +1564,11 @@ async function backupExistingInstallation(targetDir) {
1340
1564
  return [];
1341
1565
  }
1342
1566
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1343
- const backupDir = join4(targetDir, `${layout.backupDirPrefix}${timestamp}`);
1567
+ const backupDir = join5(targetDir, `${layout.backupDirPrefix}${timestamp}`);
1344
1568
  await ensureDirectory(backupDir);
1345
1569
  const backedUpPaths = [];
1346
1570
  for (const relativePath of existingPaths) {
1347
- const fullPath = join4(targetDir, relativePath);
1571
+ const fullPath = join5(targetDir, relativePath);
1348
1572
  try {
1349
1573
  const backupPath = await backupExisting(fullPath, backupDir);
1350
1574
  backedUpPaths.push(backupPath);
@@ -1367,7 +1591,7 @@ async function detectProvider(_options = {}) {
1367
1591
  }
1368
1592
  // src/core/updater.ts
1369
1593
  init_fs();
1370
- import { join as join5 } from "node:path";
1594
+ import { join as join6 } from "node:path";
1371
1595
 
1372
1596
  // src/core/entry-merger.ts
1373
1597
  var MANAGED_START = "<!-- omcustom:start -->";
@@ -1616,7 +1840,7 @@ function resolveCustomizations(customizations, configPreserveFiles, targetDir) {
1616
1840
  }
1617
1841
  async function updateEntryDoc(targetDir, config, options) {
1618
1842
  const layout = getProviderLayout();
1619
- const entryPath = join5(targetDir, layout.entryFile);
1843
+ const entryPath = join6(targetDir, layout.entryFile);
1620
1844
  const templateName = getEntryTemplateName2(config.language);
1621
1845
  const templatePath = resolveTemplatePath(templateName);
1622
1846
  if (!await fileExists(templatePath)) {
@@ -1696,6 +1920,15 @@ async function update(options) {
1696
1920
  const components = options.components || getAllUpdateComponents();
1697
1921
  await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config);
1698
1922
  await runFullUpdatePostProcessing(options, result, config);
1923
+ const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir);
1924
+ if (lockfileResult.warning) {
1925
+ result.warnings.push(lockfileResult.warning);
1926
+ warn("update.lockfile_failed", { error: lockfileResult.warning });
1927
+ } else {
1928
+ debug("update.lockfile_regenerated", {
1929
+ files: String(lockfileResult.fileCount)
1930
+ });
1931
+ }
1699
1932
  } catch (err) {
1700
1933
  const message = err instanceof Error ? err.message : String(err);
1701
1934
  result.error = message;
@@ -1729,8 +1962,8 @@ async function checkForUpdates(targetDir) {
1729
1962
  async function applyUpdates(targetDir, updates) {
1730
1963
  const fs = await import("node:fs/promises");
1731
1964
  for (const update2 of updates) {
1732
- const fullPath = join5(targetDir, update2.path);
1733
- await ensureDirectory(join5(fullPath, ".."));
1965
+ const fullPath = join6(targetDir, update2.path);
1966
+ await ensureDirectory(join6(fullPath, ".."));
1734
1967
  await fs.writeFile(fullPath, update2.content, "utf-8");
1735
1968
  debug("update.file_applied", { path: update2.path });
1736
1969
  }
@@ -1739,7 +1972,7 @@ async function preserveCustomizations(targetDir, customizations) {
1739
1972
  const preserved = new Map;
1740
1973
  const fs = await import("node:fs/promises");
1741
1974
  for (const filePath of customizations) {
1742
- const fullPath = join5(targetDir, filePath);
1975
+ const fullPath = join6(targetDir, filePath);
1743
1976
  if (await fileExists(fullPath)) {
1744
1977
  const content = await fs.readFile(fullPath, "utf-8");
1745
1978
  preserved.set(filePath, content);
@@ -1767,11 +2000,55 @@ async function componentHasUpdate(_targetDir, component, config) {
1767
2000
  const latestVersion = await getLatestVersion();
1768
2001
  return installedVersion !== latestVersion;
1769
2002
  }
2003
+ async function collectProtectedSkipPaths(srcPath, destPath, componentPath, forceOverwriteAll) {
2004
+ if (forceOverwriteAll) {
2005
+ const warnedPaths = await findProtectedFilesInDir(srcPath, componentPath);
2006
+ return { skipPaths: [], warnedPaths };
2007
+ }
2008
+ const protectedRelative = await findProtectedFilesInDir(srcPath, componentPath);
2009
+ const path = await import("node:path");
2010
+ const skipPaths = protectedRelative.map((p) => path.relative(destPath, join6(destPath, p)));
2011
+ return { skipPaths, warnedPaths: protectedRelative };
2012
+ }
2013
+ function isEntryProtected(relPath, componentRelativePrefix) {
2014
+ if (isProtectedFile(relPath)) {
2015
+ return true;
2016
+ }
2017
+ const componentPrefixed = componentRelativePrefix ? `${componentRelativePrefix}/${relPath}` : relPath;
2018
+ return isProtectedFile(componentPrefixed);
2019
+ }
2020
+ async function safeReaddir(dir, fs) {
2021
+ try {
2022
+ return await fs.readdir(dir, { withFileTypes: true });
2023
+ } catch {
2024
+ return [];
2025
+ }
2026
+ }
2027
+ async function findProtectedFilesInDir(dirPath, componentRelativePrefix) {
2028
+ const fs = await import("node:fs/promises");
2029
+ const path = await import("node:path");
2030
+ const protected_ = [];
2031
+ const queue = [{ dir: dirPath, relDir: "" }];
2032
+ while (queue.length > 0) {
2033
+ const { dir, relDir } = queue.shift();
2034
+ const entries = await safeReaddir(dir, fs);
2035
+ for (const entry of entries) {
2036
+ const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
2037
+ const fullPath = path.join(dir, entry.name);
2038
+ if (entry.isDirectory()) {
2039
+ queue.push({ dir: fullPath, relDir: relPath });
2040
+ } else if (entry.isFile() && isEntryProtected(relPath, componentRelativePrefix)) {
2041
+ protected_.push(relPath);
2042
+ }
2043
+ }
2044
+ }
2045
+ return protected_;
2046
+ }
1770
2047
  async function updateComponent(targetDir, component, customizations, options, config) {
1771
2048
  const preservedFiles = [];
1772
2049
  const componentPath = getComponentPath2(component);
1773
2050
  const srcPath = resolveTemplatePath(componentPath);
1774
- const destPath = join5(targetDir, componentPath);
2051
+ const destPath = join6(targetDir, componentPath);
1775
2052
  const customComponents = config.customComponents || [];
1776
2053
  const skipPaths = [];
1777
2054
  if (customizations && !options.forceOverwriteAll) {
@@ -1784,15 +2061,34 @@ async function updateComponent(targetDir, component, customizations, options, co
1784
2061
  skipPaths.push(cc.path);
1785
2062
  }
1786
2063
  }
2064
+ const { skipPaths: protectedSkipPaths, warnedPaths: protectedWarnedPaths } = await collectProtectedSkipPaths(srcPath, destPath, componentPath, !!options.forceOverwriteAll);
2065
+ for (const protectedPath of protectedWarnedPaths) {
2066
+ if (options.forceOverwriteAll) {
2067
+ warn("update.protected_file_force_overwrite", {
2068
+ file: protectedPath,
2069
+ component,
2070
+ hint: "File contains AI behavioral constraints. Overwriting because --force-overwrite-all was set."
2071
+ });
2072
+ } else {
2073
+ warn("update.protected_file_skipped", {
2074
+ file: protectedPath,
2075
+ component,
2076
+ hint: "File contains AI behavioral constraints and was not updated. Use --force-overwrite-all to override."
2077
+ });
2078
+ }
2079
+ }
2080
+ skipPaths.push(...protectedSkipPaths);
1787
2081
  const path = await import("node:path");
1788
- const normalizedSkipPaths = skipPaths.map((p) => path.relative(destPath, join5(targetDir, p)));
2082
+ const normalizedSkipPaths = skipPaths.map((p) => path.relative(destPath, join6(targetDir, p)));
2083
+ const uniqueSkipPaths = [...new Set(normalizedSkipPaths)];
1789
2084
  await copyDirectory(srcPath, destPath, {
1790
2085
  overwrite: true,
1791
- skipPaths: normalizedSkipPaths.length > 0 ? normalizedSkipPaths : undefined
2086
+ skipPaths: uniqueSkipPaths.length > 0 ? uniqueSkipPaths : undefined
1792
2087
  });
1793
2088
  debug("update.component_updated", {
1794
2089
  component,
1795
- skippedPaths: String(normalizedSkipPaths.length)
2090
+ skippedPaths: String(uniqueSkipPaths.length),
2091
+ protectedSkipped: String(protectedSkipPaths.length)
1796
2092
  });
1797
2093
  return preservedFiles;
1798
2094
  }
@@ -1805,12 +2101,12 @@ async function syncRootLevelFiles(targetDir, options) {
1805
2101
  const layout = getProviderLayout();
1806
2102
  const synced = [];
1807
2103
  for (const fileName of ROOT_LEVEL_FILES) {
1808
- const srcPath = resolveTemplatePath(join5(layout.rootDir, fileName));
2104
+ const srcPath = resolveTemplatePath(join6(layout.rootDir, fileName));
1809
2105
  if (!await fileExists(srcPath)) {
1810
2106
  continue;
1811
2107
  }
1812
- const destPath = join5(targetDir, layout.rootDir, fileName);
1813
- await ensureDirectory(join5(destPath, ".."));
2108
+ const destPath = join6(targetDir, layout.rootDir, fileName);
2109
+ await ensureDirectory(join6(destPath, ".."));
1814
2110
  await fs.copyFile(srcPath, destPath);
1815
2111
  if (fileName.endsWith(".sh")) {
1816
2112
  await fs.chmod(destPath, 493);
@@ -1845,7 +2141,7 @@ async function removeDeprecatedFiles(targetDir, options) {
1845
2141
  });
1846
2142
  continue;
1847
2143
  }
1848
- const fullPath = join5(targetDir, entry.path);
2144
+ const fullPath = join6(targetDir, entry.path);
1849
2145
  if (await fileExists(fullPath)) {
1850
2146
  await fs.unlink(fullPath);
1851
2147
  removed.push(entry.path);
@@ -1869,26 +2165,26 @@ function getComponentPath2(component) {
1869
2165
  }
1870
2166
  async function backupInstallation(targetDir) {
1871
2167
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1872
- const backupDir = join5(targetDir, `.omcustom-backup-${timestamp}`);
2168
+ const backupDir = join6(targetDir, `.omcustom-backup-${timestamp}`);
1873
2169
  const fs = await import("node:fs/promises");
1874
2170
  await ensureDirectory(backupDir);
1875
2171
  const layout = getProviderLayout();
1876
2172
  const dirsToBackup = [layout.rootDir, "guides"];
1877
2173
  for (const dir of dirsToBackup) {
1878
- const srcPath = join5(targetDir, dir);
2174
+ const srcPath = join6(targetDir, dir);
1879
2175
  if (await fileExists(srcPath)) {
1880
- const destPath = join5(backupDir, dir);
2176
+ const destPath = join6(backupDir, dir);
1881
2177
  await copyDirectory(srcPath, destPath, { overwrite: true });
1882
2178
  }
1883
2179
  }
1884
- const entryPath = join5(targetDir, layout.entryFile);
2180
+ const entryPath = join6(targetDir, layout.entryFile);
1885
2181
  if (await fileExists(entryPath)) {
1886
- await fs.copyFile(entryPath, join5(backupDir, layout.entryFile));
2182
+ await fs.copyFile(entryPath, join6(backupDir, layout.entryFile));
1887
2183
  }
1888
2184
  return backupDir;
1889
2185
  }
1890
2186
  async function loadCustomizationManifest(targetDir) {
1891
- const manifestPath = join5(targetDir, CUSTOMIZATION_MANIFEST_FILE);
2187
+ const manifestPath = join6(targetDir, CUSTOMIZATION_MANIFEST_FILE);
1892
2188
  if (await fileExists(manifestPath)) {
1893
2189
  return readJsonFile(manifestPath);
1894
2190
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-customcode",
3
- "version": "0.31.1",
3
+ "version": "0.33.0",
4
4
  "description": "Batteries-included agent harness for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {