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/cli/index.js +359 -59
- package/dist/index.js +285 -35
- package/package.json +1 -1
- package/templates/.claude/hooks/scripts/session-env-check.sh +52 -0
- package/templates/manifest.json +1 -1
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
|
|
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
|
|
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 =
|
|
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(
|
|
1128
|
-
const destPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
1733
|
-
await ensureDirectory(
|
|
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 =
|
|
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 =
|
|
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,
|
|
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:
|
|
2040
|
+
skipPaths: uniqueSkipPaths.length > 0 ? uniqueSkipPaths : undefined
|
|
1792
2041
|
});
|
|
1793
2042
|
debug("update.component_updated", {
|
|
1794
2043
|
component,
|
|
1795
|
-
skippedPaths: String(
|
|
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(
|
|
2058
|
+
const srcPath = resolveTemplatePath(join6(layout.rootDir, fileName));
|
|
1809
2059
|
if (!await fileExists(srcPath)) {
|
|
1810
2060
|
continue;
|
|
1811
2061
|
}
|
|
1812
|
-
const destPath =
|
|
1813
|
-
await ensureDirectory(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2128
|
+
const srcPath = join6(targetDir, dir);
|
|
1879
2129
|
if (await fileExists(srcPath)) {
|
|
1880
|
-
const destPath =
|
|
2130
|
+
const destPath = join6(backupDir, dir);
|
|
1881
2131
|
await copyDirectory(srcPath, destPath, { overwrite: true });
|
|
1882
2132
|
}
|
|
1883
2133
|
}
|
|
1884
|
-
const entryPath =
|
|
2134
|
+
const entryPath = join6(targetDir, layout.entryFile);
|
|
1885
2135
|
if (await fileExists(entryPath)) {
|
|
1886
|
-
await fs.copyFile(entryPath,
|
|
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 =
|
|
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
|
@@ -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
|
package/templates/manifest.json
CHANGED