oh-my-customcode 0.31.1 → 0.32.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.
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}}",
@@ -876,13 +900,32 @@ function getDefaultWorkflow() {
876
900
  // src/core/installer.ts
877
901
  init_fs();
878
902
  import { readFile as fsReadFile, writeFile as fsWriteFile, rename } from "node:fs/promises";
879
- import { basename as basename2, join as join4 } from "node:path";
903
+ import { basename as basename2, join as join5 } from "node:path";
880
904
 
881
905
  // src/core/file-preservation.ts
882
906
  init_fs();
883
907
  import { basename, join as join3 } from "node:path";
884
908
  var DEFAULT_CRITICAL_FILES = ["settings.json", "settings.local.json"];
885
909
  var DEFAULT_CRITICAL_DIRECTORIES = ["agent-memory", "agent-memory-local"];
910
+ var PROTECTED_FRAMEWORK_FILES = ["CLAUDE.md", "AGENTS.md"];
911
+ var PROTECTED_RULE_PATTERNS = ["rules/MUST-*.md"];
912
+ function isProtectedFile(relativePath) {
913
+ const basename2 = relativePath.split("/").pop() ?? "";
914
+ if (PROTECTED_FRAMEWORK_FILES.includes(basename2)) {
915
+ return true;
916
+ }
917
+ for (const pattern of PROTECTED_RULE_PATTERNS) {
918
+ if (matchesGlobPattern(relativePath, pattern)) {
919
+ return true;
920
+ }
921
+ }
922
+ return false;
923
+ }
924
+ function matchesGlobPattern(filePath, pattern) {
925
+ const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
926
+ const regex = new RegExp(`(^|/)${regexStr}$`);
927
+ return regex.test(filePath);
928
+ }
886
929
  async function extractSingleFile(fileName, rootDir, tempDir, result) {
887
930
  const srcPath = join3(rootDir, fileName);
888
931
  const destPath = join3(tempDir, fileName);
@@ -1041,11 +1084,139 @@ function getComponentPath(component) {
1041
1084
  return `.claude/${component}`;
1042
1085
  }
1043
1086
 
1087
+ // src/core/lockfile.ts
1088
+ init_fs();
1089
+ import { createHash } from "node:crypto";
1090
+ import { createReadStream } from "node:fs";
1091
+ import { readdir, stat } from "node:fs/promises";
1092
+ import { join as join4, relative as relative2 } from "node:path";
1093
+ var LOCKFILE_NAME = ".omcustom.lock.json";
1094
+ var LOCKFILE_VERSION = 1;
1095
+ var LOCKFILE_COMPONENTS = [
1096
+ "rules",
1097
+ "agents",
1098
+ "skills",
1099
+ "hooks",
1100
+ "contexts",
1101
+ "ontology",
1102
+ "guides"
1103
+ ];
1104
+ var COMPONENT_PATHS = LOCKFILE_COMPONENTS.map((component) => [getComponentPath(component), component]);
1105
+ function computeFileHash(filePath) {
1106
+ return new Promise((resolve2, reject) => {
1107
+ const hash = createHash("sha256");
1108
+ const stream = createReadStream(filePath);
1109
+ stream.on("error", (err) => {
1110
+ reject(err);
1111
+ });
1112
+ stream.on("data", (chunk) => {
1113
+ hash.update(chunk);
1114
+ });
1115
+ stream.on("end", () => {
1116
+ resolve2(hash.digest("hex"));
1117
+ });
1118
+ });
1119
+ }
1120
+ async function writeLockfile(targetDir, lockfile) {
1121
+ const lockfilePath = join4(targetDir, LOCKFILE_NAME);
1122
+ await writeJsonFile(lockfilePath, lockfile);
1123
+ debug("lockfile.written", { path: lockfilePath });
1124
+ }
1125
+ function resolveComponent(relativePath) {
1126
+ const normalized = relativePath.replace(/\\/g, "/");
1127
+ for (const [prefix, component] of COMPONENT_PATHS) {
1128
+ if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
1129
+ return component;
1130
+ }
1131
+ }
1132
+ return "unknown";
1133
+ }
1134
+ async function collectFiles(dir, projectRoot, isTopLevel) {
1135
+ const results = [];
1136
+ let entries;
1137
+ try {
1138
+ entries = await readdir(dir);
1139
+ } catch {
1140
+ return results;
1141
+ }
1142
+ for (const entry of entries) {
1143
+ if (isTopLevel && entry.startsWith(".") && entry !== ".claude") {
1144
+ continue;
1145
+ }
1146
+ const fullPath = join4(dir, entry);
1147
+ let fileStat;
1148
+ try {
1149
+ fileStat = await stat(fullPath);
1150
+ } catch {
1151
+ continue;
1152
+ }
1153
+ if (fileStat.isDirectory()) {
1154
+ const subFiles = await collectFiles(fullPath, projectRoot, false);
1155
+ results.push(...subFiles);
1156
+ } else if (fileStat.isFile()) {
1157
+ results.push(fullPath);
1158
+ }
1159
+ }
1160
+ return results;
1161
+ }
1162
+ async function generateLockfile(targetDir, generatorVersion, templateVersion) {
1163
+ const files = {};
1164
+ const componentRoots = COMPONENT_PATHS.map(([prefix]) => join4(targetDir, prefix));
1165
+ for (const componentRoot of componentRoots) {
1166
+ const exists = await fileExists(componentRoot);
1167
+ if (!exists) {
1168
+ debug("lockfile.component_dir_missing", { path: componentRoot });
1169
+ continue;
1170
+ }
1171
+ const allFiles = await collectFiles(componentRoot, targetDir, false);
1172
+ for (const absolutePath of allFiles) {
1173
+ const relativePath = relative2(targetDir, absolutePath).replace(/\\/g, "/");
1174
+ let hash;
1175
+ let size;
1176
+ try {
1177
+ hash = await computeFileHash(absolutePath);
1178
+ const fileStat = await stat(absolutePath);
1179
+ size = fileStat.size;
1180
+ } catch (err) {
1181
+ warn("lockfile.hash_failed", { path: absolutePath, error: String(err) });
1182
+ continue;
1183
+ }
1184
+ const component = resolveComponent(relativePath);
1185
+ files[relativePath] = {
1186
+ templateHash: hash,
1187
+ size,
1188
+ component
1189
+ };
1190
+ debug("lockfile.entry_added", { path: relativePath, component });
1191
+ }
1192
+ }
1193
+ return {
1194
+ lockfileVersion: LOCKFILE_VERSION,
1195
+ generatorVersion,
1196
+ generatedAt: new Date().toISOString(),
1197
+ templateVersion,
1198
+ files
1199
+ };
1200
+ }
1201
+ async function generateAndWriteLockfileForDir(targetDir) {
1202
+ try {
1203
+ const packageRoot = getPackageRoot();
1204
+ const manifest = await readJsonFile(join4(packageRoot, "templates", "manifest.json"));
1205
+ const { version: generatorVersion } = await readJsonFile(join4(packageRoot, "package.json"));
1206
+ const lockfile = await generateLockfile(targetDir, generatorVersion, manifest.version);
1207
+ await writeLockfile(targetDir, lockfile);
1208
+ return { fileCount: Object.keys(lockfile.files).length };
1209
+ } catch (err) {
1210
+ const msg = err instanceof Error ? err.message : String(err);
1211
+ return { fileCount: 0, warning: `Lockfile generation failed: ${msg}` };
1212
+ }
1213
+ }
1214
+
1044
1215
  // src/core/installer.ts
1045
1216
  var DEFAULT_LANGUAGE = "en";
1046
1217
  function getTemplateDir() {
1047
1218
  const packageRoot = getPackageRoot();
1048
- return join4(packageRoot, "templates");
1219
+ return join5(packageRoot, "templates");
1049
1220
  }
1050
1221
  function createInstallResult(targetDir) {
1051
1222
  return {
@@ -1067,7 +1238,7 @@ async function handleBackup(targetDir, shouldBackup, result) {
1067
1238
  if (!shouldBackup)
1068
1239
  return null;
1069
1240
  const layout = getProviderLayout();
1070
- const rootDir = join4(targetDir, layout.rootDir);
1241
+ const rootDir = join5(targetDir, layout.rootDir);
1071
1242
  let preservation = null;
1072
1243
  if (await fileExists(rootDir)) {
1073
1244
  const { createTempDir: createTempDir2 } = await Promise.resolve().then(() => (init_fs(), exports_fs));
@@ -1124,8 +1295,8 @@ async function installSingleComponent(targetDir, component, options, result) {
1124
1295
  }
1125
1296
  async function installStatusline(targetDir, options, _result) {
1126
1297
  const layout = getProviderLayout();
1127
- const srcPath = resolveTemplatePath(join4(layout.rootDir, "statusline.sh"));
1128
- const destPath = join4(targetDir, layout.rootDir, "statusline.sh");
1298
+ const srcPath = resolveTemplatePath(join5(layout.rootDir, "statusline.sh"));
1299
+ const destPath = join5(targetDir, layout.rootDir, "statusline.sh");
1129
1300
  if (!await fileExists(srcPath)) {
1130
1301
  debug("install.statusline_not_found", { path: srcPath });
1131
1302
  return;
@@ -1143,7 +1314,7 @@ async function installStatusline(targetDir, options, _result) {
1143
1314
  }
1144
1315
  async function installSettingsLocal(targetDir, result) {
1145
1316
  const layout = getProviderLayout();
1146
- const settingsPath = join4(targetDir, layout.rootDir, "settings.local.json");
1317
+ const settingsPath = join5(targetDir, layout.rootDir, "settings.local.json");
1147
1318
  const statusLineConfig = {
1148
1319
  statusLine: {
1149
1320
  type: "command",
@@ -1200,7 +1371,7 @@ async function install(options) {
1200
1371
  await installEntryDocWithTracking(options.targetDir, options, result);
1201
1372
  if (preservation) {
1202
1373
  const layout = getProviderLayout();
1203
- const rootDir = join4(options.targetDir, layout.rootDir);
1374
+ const rootDir = join5(options.targetDir, layout.rootDir);
1204
1375
  const restoration = await restoreCriticalFiles(rootDir, preservation);
1205
1376
  if (restoration.restoredFiles.length > 0 || restoration.restoredDirs.length > 0) {
1206
1377
  info("install.restored", {
@@ -1216,6 +1387,13 @@ async function install(options) {
1216
1387
  await cleanupPreservation(preservation.tempDir);
1217
1388
  }
1218
1389
  await updateInstallConfig(options.targetDir, options, result.installedComponents);
1390
+ const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir);
1391
+ if (lockfileResult.warning) {
1392
+ result.warnings.push(lockfileResult.warning);
1393
+ warn("install.lockfile_failed", { error: lockfileResult.warning });
1394
+ } else {
1395
+ info("install.lockfile_generated", { files: String(lockfileResult.fileCount) });
1396
+ }
1219
1397
  result.success = true;
1220
1398
  success("install.success");
1221
1399
  } catch (err) {
@@ -1227,7 +1405,7 @@ async function install(options) {
1227
1405
  }
1228
1406
  async function copyTemplates(targetDir, templatePath, options) {
1229
1407
  const srcPath = resolveTemplatePath(templatePath);
1230
- const destPath = join4(targetDir, templatePath);
1408
+ const destPath = join5(targetDir, templatePath);
1231
1409
  await copyDirectory(srcPath, destPath, {
1232
1410
  overwrite: options?.overwrite ?? false,
1233
1411
  preserveSymlinks: options?.preserveSymlinks ?? true,
@@ -1237,14 +1415,14 @@ async function copyTemplates(targetDir, templatePath, options) {
1237
1415
  async function createDirectoryStructure(targetDir) {
1238
1416
  const layout = getProviderLayout();
1239
1417
  for (const dir of layout.directoryStructure) {
1240
- const fullPath = join4(targetDir, dir);
1418
+ const fullPath = join5(targetDir, dir);
1241
1419
  await ensureDirectory(fullPath);
1242
1420
  }
1243
1421
  }
1244
1422
  async function getTemplateManifest() {
1245
1423
  const packageRoot = getPackageRoot();
1246
1424
  const layout = getProviderLayout();
1247
- const manifestPath = join4(packageRoot, "templates", layout.manifestFile);
1425
+ const manifestPath = join5(packageRoot, "templates", layout.manifestFile);
1248
1426
  if (await fileExists(manifestPath)) {
1249
1427
  return readJsonFile(manifestPath);
1250
1428
  }
@@ -1268,7 +1446,7 @@ async function installComponent(targetDir, component, options) {
1268
1446
  return false;
1269
1447
  }
1270
1448
  const templatePath = getComponentPath(component);
1271
- const destPath = join4(targetDir, templatePath);
1449
+ const destPath = join5(targetDir, templatePath);
1272
1450
  const destExists = await fileExists(destPath);
1273
1451
  if (destExists && !options.force && !options.backup) {
1274
1452
  debug("install.component_skipped", { component });
@@ -1296,7 +1474,7 @@ async function installEntryDoc(targetDir, language, overwrite = false) {
1296
1474
  const layout = getProviderLayout();
1297
1475
  const templateFile = getEntryTemplateName(language);
1298
1476
  const srcPath = resolveTemplatePath(templateFile);
1299
- const destPath = join4(targetDir, layout.entryFile);
1477
+ const destPath = join5(targetDir, layout.entryFile);
1300
1478
  if (!await fileExists(srcPath)) {
1301
1479
  warn("install.entry_md_not_found", { language, path: srcPath, entry: layout.entryFile });
1302
1480
  return false;
@@ -1317,7 +1495,7 @@ async function installEntryDoc(targetDir, language, overwrite = false) {
1317
1495
  }
1318
1496
  async function backupExisting(sourcePath, backupDir) {
1319
1497
  const name = basename2(sourcePath);
1320
- const backupPath = join4(backupDir, name);
1498
+ const backupPath = join5(backupDir, name);
1321
1499
  await rename(sourcePath, backupPath);
1322
1500
  return backupPath;
1323
1501
  }
@@ -1326,7 +1504,7 @@ async function checkExistingPaths(targetDir) {
1326
1504
  const pathsToCheck = [layout.entryFile, layout.rootDir, "guides"];
1327
1505
  const existingPaths = [];
1328
1506
  for (const relativePath of pathsToCheck) {
1329
- const fullPath = join4(targetDir, relativePath);
1507
+ const fullPath = join5(targetDir, relativePath);
1330
1508
  if (await fileExists(fullPath)) {
1331
1509
  existingPaths.push(relativePath);
1332
1510
  }
@@ -1340,11 +1518,11 @@ async function backupExistingInstallation(targetDir) {
1340
1518
  return [];
1341
1519
  }
1342
1520
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1343
- const backupDir = join4(targetDir, `${layout.backupDirPrefix}${timestamp}`);
1521
+ const backupDir = join5(targetDir, `${layout.backupDirPrefix}${timestamp}`);
1344
1522
  await ensureDirectory(backupDir);
1345
1523
  const backedUpPaths = [];
1346
1524
  for (const relativePath of existingPaths) {
1347
- const fullPath = join4(targetDir, relativePath);
1525
+ const fullPath = join5(targetDir, relativePath);
1348
1526
  try {
1349
1527
  const backupPath = await backupExisting(fullPath, backupDir);
1350
1528
  backedUpPaths.push(backupPath);
@@ -1367,7 +1545,7 @@ async function detectProvider(_options = {}) {
1367
1545
  }
1368
1546
  // src/core/updater.ts
1369
1547
  init_fs();
1370
- import { join as join5 } from "node:path";
1548
+ import { join as join6 } from "node:path";
1371
1549
 
1372
1550
  // src/core/entry-merger.ts
1373
1551
  var MANAGED_START = "<!-- omcustom:start -->";
@@ -1616,7 +1794,7 @@ function resolveCustomizations(customizations, configPreserveFiles, targetDir) {
1616
1794
  }
1617
1795
  async function updateEntryDoc(targetDir, config, options) {
1618
1796
  const layout = getProviderLayout();
1619
- const entryPath = join5(targetDir, layout.entryFile);
1797
+ const entryPath = join6(targetDir, layout.entryFile);
1620
1798
  const templateName = getEntryTemplateName2(config.language);
1621
1799
  const templatePath = resolveTemplatePath(templateName);
1622
1800
  if (!await fileExists(templatePath)) {
@@ -1696,6 +1874,15 @@ async function update(options) {
1696
1874
  const components = options.components || getAllUpdateComponents();
1697
1875
  await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config);
1698
1876
  await runFullUpdatePostProcessing(options, result, config);
1877
+ const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir);
1878
+ if (lockfileResult.warning) {
1879
+ result.warnings.push(lockfileResult.warning);
1880
+ warn("update.lockfile_failed", { error: lockfileResult.warning });
1881
+ } else {
1882
+ debug("update.lockfile_regenerated", {
1883
+ files: String(lockfileResult.fileCount)
1884
+ });
1885
+ }
1699
1886
  } catch (err) {
1700
1887
  const message = err instanceof Error ? err.message : String(err);
1701
1888
  result.error = message;
@@ -1729,8 +1916,8 @@ async function checkForUpdates(targetDir) {
1729
1916
  async function applyUpdates(targetDir, updates) {
1730
1917
  const fs = await import("node:fs/promises");
1731
1918
  for (const update2 of updates) {
1732
- const fullPath = join5(targetDir, update2.path);
1733
- await ensureDirectory(join5(fullPath, ".."));
1919
+ const fullPath = join6(targetDir, update2.path);
1920
+ await ensureDirectory(join6(fullPath, ".."));
1734
1921
  await fs.writeFile(fullPath, update2.content, "utf-8");
1735
1922
  debug("update.file_applied", { path: update2.path });
1736
1923
  }
@@ -1739,7 +1926,7 @@ async function preserveCustomizations(targetDir, customizations) {
1739
1926
  const preserved = new Map;
1740
1927
  const fs = await import("node:fs/promises");
1741
1928
  for (const filePath of customizations) {
1742
- const fullPath = join5(targetDir, filePath);
1929
+ const fullPath = join6(targetDir, filePath);
1743
1930
  if (await fileExists(fullPath)) {
1744
1931
  const content = await fs.readFile(fullPath, "utf-8");
1745
1932
  preserved.set(filePath, content);
@@ -1767,11 +1954,55 @@ async function componentHasUpdate(_targetDir, component, config) {
1767
1954
  const latestVersion = await getLatestVersion();
1768
1955
  return installedVersion !== latestVersion;
1769
1956
  }
1957
+ async function collectProtectedSkipPaths(srcPath, destPath, componentPath, forceOverwriteAll) {
1958
+ if (forceOverwriteAll) {
1959
+ const warnedPaths = await findProtectedFilesInDir(srcPath, componentPath);
1960
+ return { skipPaths: [], warnedPaths };
1961
+ }
1962
+ const protectedRelative = await findProtectedFilesInDir(srcPath, componentPath);
1963
+ const path = await import("node:path");
1964
+ const skipPaths = protectedRelative.map((p) => path.relative(destPath, join6(destPath, p)));
1965
+ return { skipPaths, warnedPaths: protectedRelative };
1966
+ }
1967
+ function isEntryProtected(relPath, componentRelativePrefix) {
1968
+ if (isProtectedFile(relPath)) {
1969
+ return true;
1970
+ }
1971
+ const componentPrefixed = componentRelativePrefix ? `${componentRelativePrefix}/${relPath}` : relPath;
1972
+ return isProtectedFile(componentPrefixed);
1973
+ }
1974
+ async function safeReaddir(dir, fs) {
1975
+ try {
1976
+ return await fs.readdir(dir, { withFileTypes: true });
1977
+ } catch {
1978
+ return [];
1979
+ }
1980
+ }
1981
+ async function findProtectedFilesInDir(dirPath, componentRelativePrefix) {
1982
+ const fs = await import("node:fs/promises");
1983
+ const path = await import("node:path");
1984
+ const protected_ = [];
1985
+ const queue = [{ dir: dirPath, relDir: "" }];
1986
+ while (queue.length > 0) {
1987
+ const { dir, relDir } = queue.shift();
1988
+ const entries = await safeReaddir(dir, fs);
1989
+ for (const entry of entries) {
1990
+ const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
1991
+ const fullPath = path.join(dir, entry.name);
1992
+ if (entry.isDirectory()) {
1993
+ queue.push({ dir: fullPath, relDir: relPath });
1994
+ } else if (entry.isFile() && isEntryProtected(relPath, componentRelativePrefix)) {
1995
+ protected_.push(relPath);
1996
+ }
1997
+ }
1998
+ }
1999
+ return protected_;
2000
+ }
1770
2001
  async function updateComponent(targetDir, component, customizations, options, config) {
1771
2002
  const preservedFiles = [];
1772
2003
  const componentPath = getComponentPath2(component);
1773
2004
  const srcPath = resolveTemplatePath(componentPath);
1774
- const destPath = join5(targetDir, componentPath);
2005
+ const destPath = join6(targetDir, componentPath);
1775
2006
  const customComponents = config.customComponents || [];
1776
2007
  const skipPaths = [];
1777
2008
  if (customizations && !options.forceOverwriteAll) {
@@ -1784,15 +2015,34 @@ async function updateComponent(targetDir, component, customizations, options, co
1784
2015
  skipPaths.push(cc.path);
1785
2016
  }
1786
2017
  }
2018
+ const { skipPaths: protectedSkipPaths, warnedPaths: protectedWarnedPaths } = await collectProtectedSkipPaths(srcPath, destPath, componentPath, !!options.forceOverwriteAll);
2019
+ for (const protectedPath of protectedWarnedPaths) {
2020
+ if (options.forceOverwriteAll) {
2021
+ warn("update.protected_file_force_overwrite", {
2022
+ file: protectedPath,
2023
+ component,
2024
+ hint: "File contains AI behavioral constraints. Overwriting because --force-overwrite-all was set."
2025
+ });
2026
+ } else {
2027
+ warn("update.protected_file_skipped", {
2028
+ file: protectedPath,
2029
+ component,
2030
+ hint: "File contains AI behavioral constraints and was not updated. Use --force-overwrite-all to override."
2031
+ });
2032
+ }
2033
+ }
2034
+ skipPaths.push(...protectedSkipPaths);
1787
2035
  const path = await import("node:path");
1788
- const normalizedSkipPaths = skipPaths.map((p) => path.relative(destPath, join5(targetDir, p)));
2036
+ const normalizedSkipPaths = skipPaths.map((p) => path.relative(destPath, join6(targetDir, p)));
2037
+ const uniqueSkipPaths = [...new Set(normalizedSkipPaths)];
1789
2038
  await copyDirectory(srcPath, destPath, {
1790
2039
  overwrite: true,
1791
- skipPaths: normalizedSkipPaths.length > 0 ? normalizedSkipPaths : undefined
2040
+ skipPaths: uniqueSkipPaths.length > 0 ? uniqueSkipPaths : undefined
1792
2041
  });
1793
2042
  debug("update.component_updated", {
1794
2043
  component,
1795
- skippedPaths: String(normalizedSkipPaths.length)
2044
+ skippedPaths: String(uniqueSkipPaths.length),
2045
+ protectedSkipped: String(protectedSkipPaths.length)
1796
2046
  });
1797
2047
  return preservedFiles;
1798
2048
  }
@@ -1805,12 +2055,12 @@ async function syncRootLevelFiles(targetDir, options) {
1805
2055
  const layout = getProviderLayout();
1806
2056
  const synced = [];
1807
2057
  for (const fileName of ROOT_LEVEL_FILES) {
1808
- const srcPath = resolveTemplatePath(join5(layout.rootDir, fileName));
2058
+ const srcPath = resolveTemplatePath(join6(layout.rootDir, fileName));
1809
2059
  if (!await fileExists(srcPath)) {
1810
2060
  continue;
1811
2061
  }
1812
- const destPath = join5(targetDir, layout.rootDir, fileName);
1813
- await ensureDirectory(join5(destPath, ".."));
2062
+ const destPath = join6(targetDir, layout.rootDir, fileName);
2063
+ await ensureDirectory(join6(destPath, ".."));
1814
2064
  await fs.copyFile(srcPath, destPath);
1815
2065
  if (fileName.endsWith(".sh")) {
1816
2066
  await fs.chmod(destPath, 493);
@@ -1845,7 +2095,7 @@ async function removeDeprecatedFiles(targetDir, options) {
1845
2095
  });
1846
2096
  continue;
1847
2097
  }
1848
- const fullPath = join5(targetDir, entry.path);
2098
+ const fullPath = join6(targetDir, entry.path);
1849
2099
  if (await fileExists(fullPath)) {
1850
2100
  await fs.unlink(fullPath);
1851
2101
  removed.push(entry.path);
@@ -1869,26 +2119,26 @@ function getComponentPath2(component) {
1869
2119
  }
1870
2120
  async function backupInstallation(targetDir) {
1871
2121
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1872
- const backupDir = join5(targetDir, `.omcustom-backup-${timestamp}`);
2122
+ const backupDir = join6(targetDir, `.omcustom-backup-${timestamp}`);
1873
2123
  const fs = await import("node:fs/promises");
1874
2124
  await ensureDirectory(backupDir);
1875
2125
  const layout = getProviderLayout();
1876
2126
  const dirsToBackup = [layout.rootDir, "guides"];
1877
2127
  for (const dir of dirsToBackup) {
1878
- const srcPath = join5(targetDir, dir);
2128
+ const srcPath = join6(targetDir, dir);
1879
2129
  if (await fileExists(srcPath)) {
1880
- const destPath = join5(backupDir, dir);
2130
+ const destPath = join6(backupDir, dir);
1881
2131
  await copyDirectory(srcPath, destPath, { overwrite: true });
1882
2132
  }
1883
2133
  }
1884
- const entryPath = join5(targetDir, layout.entryFile);
2134
+ const entryPath = join6(targetDir, layout.entryFile);
1885
2135
  if (await fileExists(entryPath)) {
1886
- await fs.copyFile(entryPath, join5(backupDir, layout.entryFile));
2136
+ await fs.copyFile(entryPath, join6(backupDir, layout.entryFile));
1887
2137
  }
1888
2138
  return backupDir;
1889
2139
  }
1890
2140
  async function loadCustomizationManifest(targetDir) {
1891
- const manifestPath = join5(targetDir, CUSTOMIZATION_MANIFEST_FILE);
2141
+ const manifestPath = join6(targetDir, CUSTOMIZATION_MANIFEST_FILE);
1892
2142
  if (await fileExists(manifestPath)) {
1893
2143
  return readJsonFile(manifestPath);
1894
2144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-customcode",
3
- "version": "0.31.1",
3
+ "version": "0.32.0",
4
4
  "description": "Batteries-included agent harness for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -82,6 +82,40 @@ if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/n
82
82
  fi
83
83
  fi
84
84
 
85
+ # Update availability check (local cache only — no network calls)
86
+ OMCUSTOM_UPDATE_STATUS="unknown"
87
+ INSTALLED_VERSION=""
88
+ CACHED_LATEST=""
89
+
90
+ # Read installed version from .omcustomrc.json
91
+ if [ -f ".omcustomrc.json" ]; then
92
+ INSTALLED_VERSION=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' .omcustomrc.json 2>/dev/null | head -1 | grep -o '"[^"]*"$' | tr -d '"')
93
+ fi
94
+
95
+ # Read cached latest version (no network call)
96
+ CACHE_FILE="$HOME/.oh-my-customcode/self-update-cache.json"
97
+ if [ -f "$CACHE_FILE" ]; then
98
+ CACHED_LATEST=$(grep -o '"latestVersion"[[:space:]]*:[[:space:]]*"[^"]*"' "$CACHE_FILE" 2>/dev/null | grep -o '"[^"]*"$' | tr -d '"')
99
+ fi
100
+
101
+ if [ -n "$INSTALLED_VERSION" ] && [ -n "$CACHED_LATEST" ]; then
102
+ if [ "$INSTALLED_VERSION" != "$CACHED_LATEST" ]; then
103
+ # Simple version comparison using sort -V
104
+ OLDER=$(printf '%s\n' "$INSTALLED_VERSION" "$CACHED_LATEST" | sort -V | head -1)
105
+ if [ "$OLDER" = "$INSTALLED_VERSION" ] && [ "$INSTALLED_VERSION" != "$CACHED_LATEST" ]; then
106
+ OMCUSTOM_UPDATE_STATUS="available"
107
+ else
108
+ OMCUSTOM_UPDATE_STATUS="up-to-date"
109
+ fi
110
+ else
111
+ OMCUSTOM_UPDATE_STATUS="up-to-date"
112
+ fi
113
+ elif [ -n "$INSTALLED_VERSION" ]; then
114
+ OMCUSTOM_UPDATE_STATUS="no-cache"
115
+ else
116
+ OMCUSTOM_UPDATE_STATUS="not-installed"
117
+ fi
118
+
85
119
  # Write status to file for other hooks to reference
86
120
  STATUS_FILE="/tmp/.claude-env-status-${PPID}"
87
121
  cat > "$STATUS_FILE" << ENVEOF
@@ -91,6 +125,7 @@ git_branch=${CURRENT_BRANCH}
91
125
  claude_version=${CLAUDE_VERSION}
92
126
  compat_status=${COMPAT_STATUS}
93
127
  drift_status=${DRIFT_STATUS}
128
+ omcustom_update=${OMCUSTOM_UPDATE_STATUS}
94
129
  ENVEOF
95
130
 
96
131
  # Report to stderr (visible in conversation)
@@ -138,6 +173,23 @@ case "$DRIFT_STATUS" in
138
173
  esac
139
174
  echo "------------------------------------" >&2
140
175
 
176
+ # Update Check report
177
+ echo "" >&2
178
+ echo " [Update Check]" >&2
179
+ if [ -n "$INSTALLED_VERSION" ] && [ -n "$CACHED_LATEST" ]; then
180
+ if [ "$OMCUSTOM_UPDATE_STATUS" = "available" ]; then
181
+ echo " ⚡ oh-my-customcode v${CACHED_LATEST} available (current: v${INSTALLED_VERSION})" >&2
182
+ echo " Run 'omcustom update' to apply" >&2
183
+ else
184
+ echo " ✓ oh-my-customcode is up to date (v${INSTALLED_VERSION})" >&2
185
+ fi
186
+ elif [ -n "$INSTALLED_VERSION" ]; then
187
+ echo " ℹ oh-my-customcode v${INSTALLED_VERSION} (run 'omcustom doctor --updates' to check for updates)" >&2
188
+ else
189
+ echo " ℹ oh-my-customcode not detected in this project" >&2
190
+ fi
191
+ echo "------------------------------------" >&2
192
+
141
193
  # Pass through
142
194
  echo "$input"
143
195
  exit 0
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.31.1",
2
+ "version": "0.32.0",
3
3
  "lastUpdated": "2026-03-09T00:00:00.000Z",
4
4
  "components": [
5
5
  {