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.
- package/README.md +9 -7
- package/dist/cli/index.js +502 -81
- package/dist/index.js +337 -41
- package/package.json +1 -1
- package/templates/.claude/hooks/scripts/session-env-check.sh +52 -0
- package/templates/.claude/rules/MUST-agent-design.md +11 -0
- package/templates/.claude/rules/MUST-agent-identification.md +4 -4
- package/templates/.claude/rules/MUST-agent-teams.md +17 -18
- package/templates/.claude/rules/MUST-continuous-improvement.md +3 -3
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +24 -42
- package/templates/.claude/rules/MUST-parallel-execution.md +18 -40
- package/templates/.claude/rules/MUST-sync-verification.md +6 -6
- package/templates/.claude/rules/MUST-tool-identification.md +39 -10
- package/templates/.claude/rules/SHOULD-ontology-rag-routing.md +49 -0
- package/templates/.claude/skills/airflow-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/analysis/SKILL.md +2 -1
- package/templates/.claude/skills/audit-agents/SKILL.md +4 -3
- package/templates/.claude/skills/aws-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/claude-code-bible/SKILL.md +1 -0
- package/templates/.claude/skills/codex-exec/SKILL.md +5 -4
- package/templates/.claude/skills/create-agent/SKILL.md +1 -0
- package/templates/.claude/skills/cve-triage/SKILL.md +1 -0
- package/templates/.claude/skills/dag-orchestration/SKILL.md +1 -0
- package/templates/.claude/skills/dbt-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/de-lead-routing/SKILL.md +1 -0
- package/templates/.claude/skills/dev-lead-routing/SKILL.md +1 -0
- package/templates/.claude/skills/dev-refactor/SKILL.md +1 -0
- package/templates/.claude/skills/dev-review/SKILL.md +1 -0
- package/templates/.claude/skills/django-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/docker-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/fastapi-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/fix-refs/SKILL.md +1 -0
- package/templates/.claude/skills/flutter-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/go-backend-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/go-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/help/SKILL.md +1 -0
- package/templates/.claude/skills/intent-detection/SKILL.md +1 -0
- package/templates/.claude/skills/java21-best-practices/SKILL.md +305 -0
- package/templates/.claude/skills/jinja2-prompts/SKILL.md +1 -0
- package/templates/.claude/skills/kafka-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/kotlin-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/lists/SKILL.md +1 -0
- package/templates/.claude/skills/memory-management/SKILL.md +1 -0
- package/templates/.claude/skills/memory-recall/SKILL.md +1 -0
- package/templates/.claude/skills/memory-save/SKILL.md +1 -0
- package/templates/.claude/skills/model-escalation/SKILL.md +1 -0
- package/templates/.claude/skills/monitoring-setup/SKILL.md +1 -0
- package/templates/.claude/skills/multi-model-verification/SKILL.md +1 -0
- package/templates/.claude/skills/npm-audit/SKILL.md +1 -0
- package/templates/.claude/skills/npm-publish/SKILL.md +1 -0
- package/templates/.claude/skills/npm-version/SKILL.md +1 -0
- package/templates/.claude/skills/optimize-analyze/SKILL.md +1 -0
- package/templates/.claude/skills/optimize-bundle/SKILL.md +1 -0
- package/templates/.claude/skills/optimize-report/SKILL.md +1 -0
- package/templates/.claude/skills/pipeline-architecture-patterns/SKILL.md +1 -0
- package/templates/.claude/skills/pipeline-guards/SKILL.md +2 -0
- package/templates/.claude/skills/postgres-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/pr-auto-improve/SKILL.md +1 -0
- package/templates/.claude/skills/python-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/qa-lead-routing/SKILL.md +1 -0
- package/templates/.claude/skills/react-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/redis-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/research/SKILL.md +1 -0
- package/templates/.claude/skills/result-aggregation/SKILL.md +1 -0
- package/templates/.claude/skills/rust-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/sauron-watch/SKILL.md +1 -0
- package/templates/.claude/skills/secretary-routing/SKILL.md +1 -0
- package/templates/.claude/skills/skills-sh-search/SKILL.md +1 -0
- package/templates/.claude/skills/snowflake-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/spark-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/springboot-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/status/SKILL.md +1 -0
- package/templates/.claude/skills/structured-dev-cycle/SKILL.md +1 -0
- package/templates/.claude/skills/stuck-recovery/SKILL.md +1 -0
- package/templates/.claude/skills/supabase-postgres-best-practices/SKILL.md +2 -1
- package/templates/.claude/skills/task-decomposition/SKILL.md +2 -0
- package/templates/.claude/skills/typescript-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/update-docs/SKILL.md +4 -3
- package/templates/.claude/skills/update-external/SKILL.md +1 -0
- package/templates/.claude/skills/vercel-deploy/SKILL.md +1 -0
- package/templates/.claude/skills/web-design-guidelines/SKILL.md +1 -0
- package/templates/.claude/skills/worker-reviewer-pipeline/SKILL.md +2 -0
- package/templates/.claude/skills/writing-clearly-and-concisely/SKILL.md +3 -2
- package/templates/CLAUDE.md.en +6 -5
- package/templates/CLAUDE.md.ko +6 -5
- 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 {
|
|
879
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
1128
|
-
const destPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
1733
|
-
await ensureDirectory(
|
|
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 =
|
|
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 =
|
|
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,
|
|
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:
|
|
2086
|
+
skipPaths: uniqueSkipPaths.length > 0 ? uniqueSkipPaths : undefined
|
|
1792
2087
|
});
|
|
1793
2088
|
debug("update.component_updated", {
|
|
1794
2089
|
component,
|
|
1795
|
-
skippedPaths: String(
|
|
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(
|
|
2104
|
+
const srcPath = resolveTemplatePath(join6(layout.rootDir, fileName));
|
|
1809
2105
|
if (!await fileExists(srcPath)) {
|
|
1810
2106
|
continue;
|
|
1811
2107
|
}
|
|
1812
|
-
const destPath =
|
|
1813
|
-
await ensureDirectory(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2174
|
+
const srcPath = join6(targetDir, dir);
|
|
1879
2175
|
if (await fileExists(srcPath)) {
|
|
1880
|
-
const destPath =
|
|
2176
|
+
const destPath = join6(backupDir, dir);
|
|
1881
2177
|
await copyDirectory(srcPath, destPath, { overwrite: true });
|
|
1882
2178
|
}
|
|
1883
2179
|
}
|
|
1884
|
-
const entryPath =
|
|
2180
|
+
const entryPath = join6(targetDir, layout.entryFile);
|
|
1885
2181
|
if (await fileExists(entryPath)) {
|
|
1886
|
-
await fs.copyFile(entryPath,
|
|
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 =
|
|
2187
|
+
const manifestPath = join6(targetDir, CUSTOMIZATION_MANIFEST_FILE);
|
|
1892
2188
|
if (await fileExists(manifestPath)) {
|
|
1893
2189
|
return readJsonFile(manifestPath);
|
|
1894
2190
|
}
|