oh-my-customcode 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +25 -30
  2. package/dist/cli/index.js +1315 -568
  3. package/dist/index.js +234 -17
  4. package/package.json +1 -1
  5. package/templates/.claude/agents/arch-documenter.md +7 -71
  6. package/templates/.claude/agents/arch-speckit-agent.md +21 -108
  7. package/templates/.claude/agents/be-express-expert.md +8 -58
  8. package/templates/.claude/agents/be-nestjs-expert.md +6 -38
  9. package/templates/.claude/agents/be-springboot-expert.md +11 -56
  10. package/templates/.claude/agents/db-postgres-expert.md +10 -80
  11. package/templates/.claude/agents/db-redis-expert.md +10 -75
  12. package/templates/.claude/agents/db-supabase-expert.md +12 -48
  13. package/templates/.claude/agents/de-airflow-expert.md +8 -45
  14. package/templates/.claude/agents/de-dbt-expert.md +8 -46
  15. package/templates/.claude/agents/de-kafka-expert.md +10 -10
  16. package/templates/.claude/agents/de-pipeline-expert.md +9 -69
  17. package/templates/.claude/agents/de-snowflake-expert.md +9 -62
  18. package/templates/.claude/agents/de-spark-expert.md +10 -54
  19. package/templates/.claude/agents/fe-svelte-agent.md +5 -41
  20. package/templates/.claude/agents/fe-vercel-agent.md +9 -41
  21. package/templates/.claude/agents/fe-vuejs-agent.md +7 -42
  22. package/templates/.claude/agents/lang-java21-expert.md +11 -37
  23. package/templates/.claude/agents/mgr-claude-code-bible.md +22 -207
  24. package/templates/.claude/agents/mgr-creator.md +7 -88
  25. package/templates/.claude/agents/mgr-gitnerd.md +8 -76
  26. package/templates/.claude/agents/mgr-sauron.md +27 -20
  27. package/templates/.claude/agents/mgr-supplier.md +11 -96
  28. package/templates/.claude/agents/mgr-sync-checker.md +9 -70
  29. package/templates/.claude/agents/mgr-updater.md +9 -79
  30. package/templates/.claude/agents/qa-engineer.md +8 -72
  31. package/templates/.claude/agents/qa-planner.md +2 -3
  32. package/templates/.claude/agents/qa-writer.md +6 -76
  33. package/templates/.claude/agents/sys-memory-keeper.md +13 -87
  34. package/templates/.claude/agents/sys-naggy.md +9 -62
  35. package/templates/.claude/agents/tool-bun-expert.md +7 -52
  36. package/templates/.claude/agents/tool-npm-expert.md +6 -64
  37. package/templates/.claude/agents/tool-optimizer.md +7 -60
  38. package/templates/.claude/rules/MAY-optimization.md +16 -80
  39. package/templates/.claude/rules/MUST-agent-design.md +29 -134
  40. package/templates/.claude/rules/MUST-agent-identification.md +9 -88
  41. package/templates/.claude/rules/MUST-continuous-improvement.md +10 -117
  42. package/templates/.claude/rules/MUST-intent-transparency.md +14 -171
  43. package/templates/.claude/rules/MUST-language-policy.md +11 -46
  44. package/templates/.claude/rules/MUST-orchestrator-coordination.md +82 -425
  45. package/templates/.claude/rules/MUST-parallel-execution.md +33 -405
  46. package/templates/.claude/rules/MUST-permissions.md +14 -68
  47. package/templates/.claude/rules/MUST-safety.md +11 -57
  48. package/templates/.claude/rules/MUST-sync-verification.md +49 -205
  49. package/templates/.claude/rules/MUST-tool-identification.md +21 -134
  50. package/templates/.claude/rules/SHOULD-agent-teams.md +22 -166
  51. package/templates/.claude/rules/SHOULD-ecomode.md +15 -123
  52. package/templates/.claude/rules/SHOULD-error-handling.md +19 -88
  53. package/templates/.claude/rules/SHOULD-hud-statusline.md +9 -89
  54. package/templates/.claude/rules/SHOULD-interaction.md +18 -87
  55. package/templates/.claude/rules/SHOULD-memory-integration.md +25 -118
  56. package/templates/.claude/skills/dev-lead-routing/SKILL.md +70 -243
  57. package/templates/.claude/skills/springboot-best-practices/SKILL.md +180 -319
  58. package/templates/.codex/agents/arch-documenter.md +7 -71
  59. package/templates/.codex/agents/arch-speckit-agent.md +21 -108
  60. package/templates/.codex/agents/be-express-expert.md +8 -58
  61. package/templates/.codex/agents/be-nestjs-expert.md +6 -38
  62. package/templates/.codex/agents/be-springboot-expert.md +11 -56
  63. package/templates/.codex/agents/db-postgres-expert.md +10 -80
  64. package/templates/.codex/agents/db-redis-expert.md +10 -75
  65. package/templates/.codex/agents/db-supabase-expert.md +12 -48
  66. package/templates/.codex/agents/de-airflow-expert.md +8 -45
  67. package/templates/.codex/agents/de-dbt-expert.md +8 -46
  68. package/templates/.codex/agents/de-kafka-expert.md +10 -10
  69. package/templates/.codex/agents/de-pipeline-expert.md +9 -69
  70. package/templates/.codex/agents/de-snowflake-expert.md +9 -62
  71. package/templates/.codex/agents/de-spark-expert.md +10 -54
  72. package/templates/.codex/agents/fe-svelte-agent.md +5 -41
  73. package/templates/.codex/agents/fe-vercel-agent.md +9 -41
  74. package/templates/.codex/agents/fe-vuejs-agent.md +7 -42
  75. package/templates/.codex/agents/infra-aws-expert.md +1 -1
  76. package/templates/.codex/agents/infra-docker-expert.md +1 -1
  77. package/templates/.codex/agents/lang-java21-expert.md +11 -37
  78. package/templates/.codex/agents/mgr-claude-code-bible.md +25 -210
  79. package/templates/.codex/agents/mgr-creator.md +7 -88
  80. package/templates/.codex/agents/mgr-gitnerd.md +8 -76
  81. package/templates/.codex/agents/mgr-sauron.md +30 -23
  82. package/templates/.codex/agents/mgr-supplier.md +11 -96
  83. package/templates/.codex/agents/mgr-sync-checker.md +12 -73
  84. package/templates/.codex/agents/mgr-updater.md +9 -79
  85. package/templates/.codex/agents/qa-engineer.md +8 -72
  86. package/templates/.codex/agents/qa-planner.md +2 -3
  87. package/templates/.codex/agents/qa-writer.md +6 -76
  88. package/templates/.codex/agents/sys-memory-keeper.md +13 -87
  89. package/templates/.codex/agents/sys-naggy.md +9 -62
  90. package/templates/.codex/agents/tool-bun-expert.md +7 -52
  91. package/templates/.codex/agents/tool-npm-expert.md +6 -64
  92. package/templates/.codex/agents/tool-optimizer.md +7 -60
  93. package/templates/.codex/rules/MAY-optimization.md +16 -80
  94. package/templates/.codex/rules/MUST-agent-design.md +29 -134
  95. package/templates/.codex/rules/MUST-agent-identification.md +9 -88
  96. package/templates/.codex/rules/MUST-continuous-improvement.md +10 -117
  97. package/templates/.codex/rules/MUST-intent-transparency.md +14 -171
  98. package/templates/.codex/rules/MUST-language-policy.md +11 -46
  99. package/templates/.codex/rules/MUST-orchestrator-coordination.md +82 -425
  100. package/templates/.codex/rules/MUST-parallel-execution.md +33 -405
  101. package/templates/.codex/rules/MUST-permissions.md +14 -68
  102. package/templates/.codex/rules/MUST-safety.md +11 -57
  103. package/templates/.codex/rules/MUST-sync-verification.md +58 -214
  104. package/templates/.codex/rules/MUST-tool-identification.md +22 -135
  105. package/templates/.codex/rules/SHOULD-agent-teams.md +22 -166
  106. package/templates/.codex/rules/SHOULD-ecomode.md +15 -123
  107. package/templates/.codex/rules/SHOULD-error-handling.md +19 -88
  108. package/templates/.codex/rules/SHOULD-hud-statusline.md +9 -89
  109. package/templates/.codex/rules/SHOULD-interaction.md +18 -87
  110. package/templates/.codex/rules/SHOULD-memory-integration.md +25 -118
  111. package/templates/.codex/skills/dev-lead-routing/SKILL.md +70 -243
  112. package/templates/.codex/skills/springboot-best-practices/SKILL.md +180 -319
package/dist/cli/index.js CHANGED
@@ -9033,6 +9033,214 @@ var {
9033
9033
  Help
9034
9034
  } = import__.default;
9035
9035
 
9036
+ // src/core/preflight.ts
9037
+ import { execSync } from "node:child_process";
9038
+ function isCI() {
9039
+ const ciEnvVars = ["CI", "GITHUB_ACTIONS", "OMCUSTOM_SKIP_PREFLIGHT"];
9040
+ return ciEnvVars.some((envVar) => process.env[envVar] === "true");
9041
+ }
9042
+ function hasHomebrew() {
9043
+ try {
9044
+ execSync("which brew", { encoding: "utf-8", stdio: "pipe" });
9045
+ return true;
9046
+ } catch {
9047
+ return false;
9048
+ }
9049
+ }
9050
+ function getToolInfoFromBrew(toolName) {
9051
+ const tool = {
9052
+ name: toolName,
9053
+ installed: false,
9054
+ currentVersion: null,
9055
+ latestVersion: null,
9056
+ updateAvailable: false,
9057
+ installMethod: "homebrew"
9058
+ };
9059
+ try {
9060
+ const infoOutput = execSync(`brew info --json=v2 ${toolName}`, {
9061
+ encoding: "utf-8",
9062
+ stdio: "pipe",
9063
+ timeout: 3000
9064
+ });
9065
+ const info = JSON.parse(infoOutput);
9066
+ if (info.casks && info.casks.length > 0) {
9067
+ const cask = info.casks[0];
9068
+ tool.latestVersion = cask.version;
9069
+ tool.currentVersion = cask.installed || null;
9070
+ tool.installed = cask.installed !== null;
9071
+ }
9072
+ if (!tool.installed && info.formulae && info.formulae.length > 0) {
9073
+ const formula = info.formulae[0];
9074
+ tool.latestVersion = formula.versions.stable;
9075
+ if (formula.installed && formula.installed.length > 0) {
9076
+ tool.currentVersion = formula.installed[0].version;
9077
+ tool.installed = true;
9078
+ }
9079
+ }
9080
+ if (tool.installed && tool.currentVersion && tool.latestVersion) {
9081
+ tool.updateAvailable = tool.currentVersion !== tool.latestVersion;
9082
+ }
9083
+ } catch {}
9084
+ return tool;
9085
+ }
9086
+ function getToolInfoFromNpm(toolName) {
9087
+ const tool = {
9088
+ name: toolName,
9089
+ installed: false,
9090
+ currentVersion: null,
9091
+ latestVersion: null,
9092
+ updateAvailable: false,
9093
+ installMethod: "npm"
9094
+ };
9095
+ try {
9096
+ const versionOutput = execSync(`npx ${toolName} --version`, {
9097
+ encoding: "utf-8",
9098
+ stdio: "pipe",
9099
+ timeout: 3000
9100
+ });
9101
+ const version = versionOutput.trim();
9102
+ if (version) {
9103
+ tool.installed = true;
9104
+ tool.currentVersion = version;
9105
+ }
9106
+ } catch {}
9107
+ return tool;
9108
+ }
9109
+ function getToolInfo(toolName) {
9110
+ if (hasHomebrew()) {
9111
+ const brewTool = getToolInfoFromBrew(toolName);
9112
+ if (brewTool.installed) {
9113
+ return brewTool;
9114
+ }
9115
+ }
9116
+ const npmTool = getToolInfoFromNpm(toolName);
9117
+ if (npmTool.installed) {
9118
+ return npmTool;
9119
+ }
9120
+ return {
9121
+ name: toolName,
9122
+ installed: false,
9123
+ currentVersion: null,
9124
+ latestVersion: null,
9125
+ updateAvailable: false,
9126
+ installMethod: "unknown"
9127
+ };
9128
+ }
9129
+ function checkOutdated(tools) {
9130
+ if (!hasHomebrew())
9131
+ return;
9132
+ try {
9133
+ const toolNames = tools.map((t) => t.name).join(" ");
9134
+ const outdatedOutput = execSync(`brew outdated --json=v2 ${toolNames}`, {
9135
+ encoding: "utf-8",
9136
+ stdio: "pipe",
9137
+ timeout: 3000
9138
+ });
9139
+ const outdated = JSON.parse(outdatedOutput);
9140
+ const outdatedCasks = outdated.casks || [];
9141
+ const outdatedFormulae = outdated.formulae || [];
9142
+ for (const tool of tools) {
9143
+ const outdatedCask = outdatedCasks.find((c) => c.name === tool.name);
9144
+ const outdatedFormula = outdatedFormulae.find((f) => f.name === tool.name);
9145
+ if (outdatedCask) {
9146
+ tool.latestVersion = outdatedCask.current_version;
9147
+ tool.updateAvailable = true;
9148
+ } else if (outdatedFormula) {
9149
+ tool.latestVersion = outdatedFormula.current_version;
9150
+ tool.updateAvailable = true;
9151
+ }
9152
+ }
9153
+ } catch {}
9154
+ }
9155
+ async function runPreflightCheck(options = {}) {
9156
+ const { skip = false, tools: toolNames = ["claude-code", "codex"], timeout = 5000 } = options;
9157
+ if (skip) {
9158
+ return {
9159
+ tools: [],
9160
+ hasUpdates: false,
9161
+ warnings: [],
9162
+ skipped: true,
9163
+ skipReason: "Skipped by --skip-version-check flag"
9164
+ };
9165
+ }
9166
+ if (isCI()) {
9167
+ return {
9168
+ tools: [],
9169
+ hasUpdates: false,
9170
+ warnings: [],
9171
+ skipped: true,
9172
+ skipReason: "CI environment detected"
9173
+ };
9174
+ }
9175
+ if (!hasHomebrew()) {
9176
+ return {
9177
+ tools: [],
9178
+ hasUpdates: false,
9179
+ warnings: ["Homebrew not found, skipping version check"],
9180
+ skipped: true,
9181
+ skipReason: "Homebrew not available"
9182
+ };
9183
+ }
9184
+ return new Promise((resolve) => {
9185
+ const timeoutId = setTimeout(() => {
9186
+ resolve({
9187
+ tools: [],
9188
+ hasUpdates: false,
9189
+ warnings: ["Version check timed out"],
9190
+ skipped: true,
9191
+ skipReason: "Timeout"
9192
+ });
9193
+ }, timeout);
9194
+ try {
9195
+ const tools = [];
9196
+ for (const toolName of toolNames) {
9197
+ const tool = getToolInfo(toolName);
9198
+ tools.push(tool);
9199
+ }
9200
+ checkOutdated(tools);
9201
+ const hasUpdates = tools.some((t) => t.updateAvailable);
9202
+ const warnings = [];
9203
+ clearTimeout(timeoutId);
9204
+ resolve({
9205
+ tools,
9206
+ hasUpdates,
9207
+ warnings,
9208
+ skipped: false
9209
+ });
9210
+ } catch (error) {
9211
+ clearTimeout(timeoutId);
9212
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
9213
+ resolve({
9214
+ tools: [],
9215
+ hasUpdates: false,
9216
+ warnings: [`Pre-flight check failed: ${errorMessage}`],
9217
+ skipped: true,
9218
+ skipReason: "Error during check"
9219
+ });
9220
+ }
9221
+ });
9222
+ }
9223
+ function formatPreflightWarnings(result) {
9224
+ if (!result.hasUpdates) {
9225
+ return "";
9226
+ }
9227
+ const lines = [];
9228
+ const updatesAvailable = result.tools.filter((t) => t.updateAvailable);
9229
+ if (updatesAvailable.length === 1) {
9230
+ const tool = updatesAvailable[0];
9231
+ lines.push(`⚠ ${tool.name} ${tool.latestVersion} is available (current: ${tool.currentVersion})`);
9232
+ lines.push(` Run: brew upgrade ${tool.name}`);
9233
+ } else if (updatesAvailable.length > 1) {
9234
+ lines.push("Run the following to upgrade:");
9235
+ for (const tool of updatesAvailable) {
9236
+ lines.push(` brew upgrade ${tool.name} # ${tool.latestVersion} available (current: ${tool.currentVersion})`);
9237
+ }
9238
+ }
9239
+ lines.push(" Use --skip-version-check to skip this check");
9240
+ return lines.join(`
9241
+ `);
9242
+ }
9243
+
9036
9244
  // node_modules/i18next/dist/esm/i18next.js
9037
9245
  var isString = (obj) => typeof obj === "string";
9038
9246
  var defer = () => {
@@ -11461,6 +11669,17 @@ var en_default = {
11461
11669
  error: {
11462
11670
  unexpected: "An unexpected error occurred:"
11463
11671
  },
11672
+ preflight: {
11673
+ checking: "Checking CLI tool versions...",
11674
+ updateAvailable: "⚠ {{name}} {{latest}} is available (current: {{current}})",
11675
+ upgradeCommand: " Run: brew upgrade {{name}}",
11676
+ multipleUpdates: "Run the following to upgrade:",
11677
+ skipFlag: " Use --skip-version-check to skip this check",
11678
+ skippedCi: "Skipping version check (CI environment detected)",
11679
+ skippedFlag: "Skipping version check (--skip-version-check)",
11680
+ homebrewNotFound: "Homebrew not found, skipping version check",
11681
+ timeout: "Version check timed out, continuing..."
11682
+ },
11464
11683
  init: {
11465
11684
  description: "Initialize oh-my-customcode in the current directory",
11466
11685
  langOption: "Language for templates (en or ko)",
@@ -11503,7 +11722,25 @@ var en_default = {
11503
11722
  latestVersion: "Latest version: {{version}}",
11504
11723
  changelog: "Changelog",
11505
11724
  promptUpdate: "Do you want to update?",
11506
- skipped: "Update skipped"
11725
+ skipped: "Update skipped",
11726
+ dryRunOption: "Show what would be updated without making changes",
11727
+ forceOption: "Force update even if already at latest version",
11728
+ backupOption: "Create backup before updating",
11729
+ agentsOption: "Update only agents",
11730
+ skillsOption: "Update only skills",
11731
+ rulesOption: "Update only rules",
11732
+ guidesOption: "Update only guides",
11733
+ hooksOption: "Update only hooks",
11734
+ contextsOption: "Update only contexts",
11735
+ providerOption: "Provider to update (auto, claude, codex)",
11736
+ dryRunHeader: "Dry run - no changes will be made:",
11737
+ componentUpdated: "✓ {{component}} updated",
11738
+ componentSkipped: "- {{component}} skipped (no changes)",
11739
+ componentFailed: "✗ {{component}} failed: {{error}}",
11740
+ preservedFiles: "Preserved {{count}} user-customized files",
11741
+ backupCreated: "Backup created at {{path}}",
11742
+ summary: "Update complete: {{updated}} updated, {{skipped}} skipped",
11743
+ summaryFailed: "Update failed: {{error}}"
11507
11744
  },
11508
11745
  list: {
11509
11746
  description: "List installed components",
@@ -11715,6 +11952,17 @@ var ko_default = {
11715
11952
  error: {
11716
11953
  unexpected: "예기치 않은 오류가 발생했습니다:"
11717
11954
  },
11955
+ preflight: {
11956
+ checking: "CLI 도구 버전 확인 중...",
11957
+ updateAvailable: "⚠ {{name}} {{latest}} 사용 가능 (현재: {{current}})",
11958
+ upgradeCommand: " 실행: brew upgrade {{name}}",
11959
+ multipleUpdates: "다음을 실행하여 업그레이드하세요:",
11960
+ skipFlag: " --skip-version-check 옵션으로 이 검사를 건너뛸 수 있습니다",
11961
+ skippedCi: "버전 확인 건너뜀 (CI 환경 감지)",
11962
+ skippedFlag: "버전 확인 건너뜀 (--skip-version-check)",
11963
+ homebrewNotFound: "Homebrew를 찾을 수 없어 버전 확인을 건너뜁니다",
11964
+ timeout: "버전 확인 시간 초과, 계속 진행합니다..."
11965
+ },
11718
11966
  init: {
11719
11967
  description: "현재 디렉토리에 oh-my-customcode 초기화",
11720
11968
  langOption: "템플릿 언어 (en 또는 ko)",
@@ -11757,7 +12005,25 @@ var ko_default = {
11757
12005
  latestVersion: "최신 버전: {{version}}",
11758
12006
  changelog: "변경 내역",
11759
12007
  promptUpdate: "업데이트하시겠습니까?",
11760
- skipped: "업데이트 건너뜀"
12008
+ skipped: "업데이트 건너뜀",
12009
+ dryRunOption: "변경 없이 업데이트할 내용 표시",
12010
+ forceOption: "이미 최신 버전이어도 강제 업데이트",
12011
+ backupOption: "업데이트 전 백업 생성",
12012
+ agentsOption: "에이전트만 업데이트",
12013
+ skillsOption: "스킬만 업데이트",
12014
+ rulesOption: "규칙만 업데이트",
12015
+ guidesOption: "가이드만 업데이트",
12016
+ hooksOption: "훅만 업데이트",
12017
+ contextsOption: "컨텍스트만 업데이트",
12018
+ providerOption: "업데이트할 제공자 (auto, claude, codex)",
12019
+ dryRunHeader: "드라이 런 - 변경사항 없음:",
12020
+ componentUpdated: "✓ {{component}} 업데이트됨",
12021
+ componentSkipped: "- {{component}} 건너뜀 (변경사항 없음)",
12022
+ componentFailed: "✗ {{component}} 실패: {{error}}",
12023
+ preservedFiles: "사용자 커스터마이징 파일 {{count}}개 보존됨",
12024
+ backupCreated: "백업 생성됨: {{path}}",
12025
+ summary: "업데이트 완료: {{updated}}개 업데이트, {{skipped}}개 건너뜀",
12026
+ summaryFailed: "업데이트 실패: {{error}}"
11761
12027
  },
11762
12028
  list: {
11763
12029
  description: "설치된 컴포넌트 목록 표시",
@@ -12029,69 +12295,11 @@ var $stringify = publicApi.stringify;
12029
12295
  var $visit = visit.visit;
12030
12296
  var $visitAsync = visit.visitAsync;
12031
12297
 
12032
- // src/core/layout.ts
12033
- var PROVIDER_LAYOUTS = {
12034
- claude: {
12035
- provider: "claude",
12036
- rootDir: ".claude",
12037
- entryFile: "CLAUDE.md",
12038
- entryTemplatePrefix: "CLAUDE.md",
12039
- manifestFile: "manifest.json",
12040
- backupDirPrefix: ".claude-backup-",
12041
- directoryStructure: [
12042
- ".claude",
12043
- ".claude/rules",
12044
- ".claude/hooks",
12045
- ".claude/contexts",
12046
- ".claude/agents",
12047
- ".claude/skills",
12048
- "guides"
12049
- ]
12050
- },
12051
- codex: {
12052
- provider: "codex",
12053
- rootDir: ".codex",
12054
- entryFile: "AGENTS.md",
12055
- entryTemplatePrefix: "AGENTS.md",
12056
- manifestFile: "manifest.codex.json",
12057
- backupDirPrefix: ".codex-backup-",
12058
- directoryStructure: [
12059
- ".codex",
12060
- ".codex/rules",
12061
- ".codex/hooks",
12062
- ".codex/contexts",
12063
- ".codex/agents",
12064
- ".codex/skills",
12065
- "guides"
12066
- ]
12067
- }
12068
- };
12069
- function getProviderLayout(provider) {
12070
- return PROVIDER_LAYOUTS[provider];
12071
- }
12072
- function getEntryTemplateName(provider, language) {
12073
- const layout = getProviderLayout(provider);
12074
- return `${layout.entryTemplatePrefix}.${language}`;
12075
- }
12076
- function getComponentPath(provider, component) {
12077
- const layout = getProviderLayout(provider);
12078
- if (component === "entry-md") {
12079
- return layout.entryFile;
12080
- }
12081
- if (component === "guides") {
12082
- return "guides";
12083
- }
12084
- return `${layout.rootDir}/${component}`;
12085
- }
12086
- function getDefaultProvider() {
12087
- return "claude";
12088
- }
12089
-
12090
- // src/core/provider.ts
12298
+ // src/core/config.ts
12091
12299
  import { join as join2 } from "node:path";
12092
12300
 
12093
12301
  // src/utils/fs.ts
12094
- import { dirname, join, resolve } from "node:path";
12302
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12095
12303
  import { fileURLToPath } from "node:url";
12096
12304
  async function fileExists(path) {
12097
12305
  const fs = await import("node:fs/promises");
@@ -12156,6 +12364,25 @@ async function handleFile(srcPath, destPath, options, fs) {
12156
12364
  await fs.utimes(destPath, stats.atime, stats.mtime);
12157
12365
  }
12158
12366
  }
12367
+ function shouldSkipPath(destPath, destRoot, skipPaths) {
12368
+ if (!skipPaths || skipPaths.length === 0) {
12369
+ return false;
12370
+ }
12371
+ const relativePath = relative(destRoot, destPath);
12372
+ for (const skipPath of skipPaths) {
12373
+ if (skipPath.endsWith("/")) {
12374
+ const dirPath = skipPath.slice(0, -1);
12375
+ if (relativePath === dirPath || relativePath.startsWith(dirPath + sep)) {
12376
+ return true;
12377
+ }
12378
+ } else {
12379
+ if (relativePath === skipPath) {
12380
+ return true;
12381
+ }
12382
+ }
12383
+ }
12384
+ return false;
12385
+ }
12159
12386
  async function copyDirectory(src, dest, options = {}) {
12160
12387
  const fs = await import("node:fs/promises");
12161
12388
  const path = await import("node:path");
@@ -12167,6 +12394,9 @@ async function copyDirectory(src, dest, options = {}) {
12167
12394
  }
12168
12395
  const srcPath = path.join(src, entry.name);
12169
12396
  const destPath = path.join(dest, entry.name);
12397
+ if (shouldSkipPath(destPath, dest, options.skipPaths)) {
12398
+ continue;
12399
+ }
12170
12400
  if (entry.isSymbolicLink()) {
12171
12401
  await handleSymlink(srcPath, destPath, options, fs);
12172
12402
  } else if (entry.isDirectory()) {
@@ -12190,6 +12420,11 @@ async function readTextFile(path) {
12190
12420
  const fs = await import("node:fs/promises");
12191
12421
  return fs.readFile(path, "utf-8");
12192
12422
  }
12423
+ async function writeTextFile(path, content) {
12424
+ const fs = await import("node:fs/promises");
12425
+ await ensureDirectory(dirname(path));
12426
+ await fs.writeFile(path, content, "utf-8");
12427
+ }
12193
12428
  function getPackageRoot() {
12194
12429
  const currentFile = fileURLToPath(import.meta.url);
12195
12430
  const currentDir = dirname(currentFile);
@@ -12223,124 +12458,479 @@ function matchesPattern(filename, pattern) {
12223
12458
  return regex.test(filename);
12224
12459
  }
12225
12460
 
12226
- // src/core/provider.ts
12227
- var ENV_SIGNALS = {
12228
- claude: [
12229
- "ANTHROPIC_API_KEY",
12230
- "CLAUDE_CODE",
12231
- "CLAUDE_CODE_EFFORT_LEVEL",
12232
- "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
12233
- "CLAUDE_CODE_ENABLE_TELEMETRY"
12234
- ],
12235
- codex: ["OPENAI_API_KEY", "OPENAI_ORG_ID", "OPENAI_PROJECT", "CODEX_HOME", "CODEX_PROJECT"]
12461
+ // src/utils/logger.ts
12462
+ var currentOptions = {
12463
+ level: "info",
12464
+ colors: true,
12465
+ locale: "en",
12466
+ timestamps: false
12236
12467
  };
12237
- var PROVIDER_ENV_OVERRIDES = ["OMCUSTOM_PROVIDER", "LLM_SERVICE"];
12238
- function normalizeProvider(value) {
12239
- if (!value)
12240
- return null;
12241
- const normalized = value.toLowerCase().trim();
12242
- if (normalized === "claude" || normalized === "codex" || normalized === "auto") {
12243
- return normalized;
12244
- }
12245
- return null;
12246
- }
12247
- function detectFromEnv(env) {
12248
- for (const key of PROVIDER_ENV_OVERRIDES) {
12249
- const override = normalizeProvider(env[key]);
12250
- if (override && override !== "auto") {
12251
- return {
12252
- provider: override,
12253
- source: "override",
12254
- confidence: "high",
12255
- reason: `env:${key}`
12256
- };
12257
- }
12258
- }
12259
- const claudeSignals = ENV_SIGNALS.claude.filter((key) => Boolean(env[key]));
12260
- const codexSignals = ENV_SIGNALS.codex.filter((key) => Boolean(env[key]));
12261
- const hasClaude = claudeSignals.length > 0;
12262
- const hasCodex = codexSignals.length > 0;
12263
- if (hasClaude && !hasCodex) {
12264
- return {
12265
- provider: "claude",
12266
- source: "env",
12267
- confidence: "medium",
12268
- reason: `env:${claudeSignals[0]}`
12269
- };
12270
- }
12271
- if (hasCodex && !hasClaude) {
12272
- return {
12273
- provider: "codex",
12274
- source: "env",
12275
- confidence: "medium",
12276
- reason: `env:${codexSignals[0]}`
12277
- };
12278
- }
12279
- return null;
12280
- }
12281
- async function detectFromProject(targetDir) {
12282
- const claudeMarkers = [join2(targetDir, "CLAUDE.md"), join2(targetDir, ".claude")];
12283
- const codexMarkers = [join2(targetDir, "AGENTS.md"), join2(targetDir, ".codex")];
12284
- const claudeFound = await Promise.all(claudeMarkers.map((path) => fileExists(path)));
12285
- const codexFound = await Promise.all(codexMarkers.map((path) => fileExists(path)));
12286
- const hasClaude = claudeFound.some(Boolean);
12287
- const hasCodex = codexFound.some(Boolean);
12288
- if (hasClaude && !hasCodex) {
12289
- return {
12290
- provider: "claude",
12291
- source: "project",
12292
- confidence: "medium",
12293
- reason: "project:claude"
12294
- };
12295
- }
12296
- if (hasCodex && !hasClaude) {
12297
- return {
12298
- provider: "codex",
12299
- source: "project",
12300
- confidence: "medium",
12301
- reason: "project:codex"
12302
- };
12303
- }
12304
- return null;
12305
- }
12306
- async function detectFromConfig(targetDir) {
12307
- const configPath = join2(targetDir, ".omcustomrc.json");
12308
- if (!await fileExists(configPath)) {
12309
- return null;
12310
- }
12311
- try {
12312
- const config = await readJsonFile(configPath);
12313
- const provider = normalizeProvider(config.provider);
12314
- if (provider && provider !== "auto") {
12315
- return {
12316
- provider,
12317
- source: "config",
12318
- confidence: "high",
12319
- reason: "config:provider"
12320
- };
12321
- }
12322
- } catch {}
12323
- return null;
12324
- }
12325
- async function detectProvider(options = {}) {
12326
- const env = options.env ?? process.env;
12327
- const override = options.override;
12328
- const normalizedOverride = normalizeProvider(override);
12329
- if (normalizedOverride && normalizedOverride !== "auto") {
12330
- return {
12331
- provider: normalizedOverride,
12332
- source: "override",
12333
- confidence: "high",
12334
- reason: "override:option"
12335
- };
12336
- }
12337
- if (options.targetDir) {
12338
- const fromConfig = await detectFromConfig(options.targetDir);
12339
- if (fromConfig) {
12340
- return fromConfig;
12341
- }
12342
- }
12343
- if (options.targetDir && options.preferProject) {
12468
+ var LOG_LEVELS = {
12469
+ debug: 0,
12470
+ info: 1,
12471
+ warn: 2,
12472
+ error: 3
12473
+ };
12474
+ var COLORS = {
12475
+ reset: "\x1B[0m",
12476
+ bold: "\x1B[1m",
12477
+ dim: "\x1B[2m",
12478
+ black: "\x1B[30m",
12479
+ red: "\x1B[31m",
12480
+ green: "\x1B[32m",
12481
+ yellow: "\x1B[33m",
12482
+ blue: "\x1B[34m",
12483
+ magenta: "\x1B[35m",
12484
+ cyan: "\x1B[36m",
12485
+ white: "\x1B[37m",
12486
+ brightRed: "\x1B[91m",
12487
+ brightGreen: "\x1B[92m",
12488
+ brightYellow: "\x1B[93m",
12489
+ brightBlue: "\x1B[94m",
12490
+ brightMagenta: "\x1B[95m",
12491
+ brightCyan: "\x1B[96m"
12492
+ };
12493
+ var LEVEL_COLORS = {
12494
+ debug: COLORS.dim,
12495
+ info: COLORS.blue,
12496
+ warn: COLORS.yellow,
12497
+ error: COLORS.red
12498
+ };
12499
+ var LEVEL_ICONS = {
12500
+ debug: "\uD83D\uDD0D",
12501
+ info: "ℹ️",
12502
+ warn: "⚠️",
12503
+ error: "❌"
12504
+ };
12505
+ var MESSAGES = {
12506
+ en: {
12507
+ "install.start": "Initializing oh-my-customcode...",
12508
+ "install.success": "Successfully initialized!",
12509
+ "install.failed": "Installation failed: {{error}}",
12510
+ "install.exists": "Existing {{rootDir}} directory found",
12511
+ "install.backup": "Backed up existing files to: {{path}}",
12512
+ "install.directories_created": "Directory structure created",
12513
+ "install.component_skipped": "Skipped {{component}} (already exists)",
12514
+ "install.component_installed": "Installed {{component}}",
12515
+ "install.template_not_found": "Template not found for {{component}}: {{path}}",
12516
+ "install.claude_md_installed": "CLAUDE.md installed ({{language}})",
12517
+ "install.claude_md_not_found": "CLAUDE.md template not found for {{language}}",
12518
+ "install.entry_md_installed": "{{entry}} installed ({{language}})",
12519
+ "install.entry_md_not_found": "{{entry}} template not found for {{language}}",
12520
+ "install.entry_md_skipped": "{{entry}} skipped ({{reason}})",
12521
+ "update.start": "Checking for updates...",
12522
+ "update.success": "Updated from {{from}} to {{to}}",
12523
+ "update.failed": "Update failed: {{error}}",
12524
+ "update.no_updates": "Already up to date",
12525
+ "update.backup_created": "Backup created at: {{path}}",
12526
+ "update.dry_run": "Would update {{component}}",
12527
+ "update.component_updated": "Updated {{component}}",
12528
+ "update.file_applied": "Applied update to {{path}}",
12529
+ "config.load_failed": "Failed to load config: {{error}}",
12530
+ "config.not_found": "Config not found at {{path}}, using defaults",
12531
+ "config.saved": "Config saved to {{path}}",
12532
+ "config.deleted": "Config deleted from {{path}}",
12533
+ "general.done": "Done!",
12534
+ "general.failed": "Failed",
12535
+ "general.skipped": "Skipped"
12536
+ },
12537
+ ko: {
12538
+ "install.start": "oh-my-customcode 초기화 중...",
12539
+ "install.success": "초기화 완료!",
12540
+ "install.failed": "설치 실패: {{error}}",
12541
+ "install.exists": "기존 {{rootDir}} 디렉토리 발견",
12542
+ "install.backup": "기존 파일 백업 완료: {{path}}",
12543
+ "install.directories_created": "디렉토리 구조 생성 완료",
12544
+ "install.component_skipped": "{{component}} 건너뜀 (이미 존재)",
12545
+ "install.component_installed": "{{component}} 설치 완료",
12546
+ "install.template_not_found": "{{component}} 템플릿 없음: {{path}}",
12547
+ "install.claude_md_installed": "CLAUDE.md 설치 완료 ({{language}})",
12548
+ "install.claude_md_not_found": "{{language}}용 CLAUDE.md 템플릿 없음",
12549
+ "install.entry_md_installed": "{{entry}} 설치 완료 ({{language}})",
12550
+ "install.entry_md_not_found": "{{language}}용 {{entry}} 템플릿 없음",
12551
+ "install.entry_md_skipped": "{{entry}} 건너뜀 ({{reason}})",
12552
+ "update.start": "업데이트 확인 중...",
12553
+ "update.success": "{{from}}에서 {{to}}로 업데이트 완료",
12554
+ "update.failed": "업데이트 실패: {{error}}",
12555
+ "update.no_updates": "이미 최신 버전입니다",
12556
+ "update.backup_created": "백업 생성됨: {{path}}",
12557
+ "update.dry_run": "{{component}} 업데이트 예정",
12558
+ "update.component_updated": "{{component}} 업데이트 완료",
12559
+ "update.file_applied": "{{path}} 업데이트 적용",
12560
+ "config.load_failed": "설정 로드 실패: {{error}}",
12561
+ "config.not_found": "{{path}}에 설정 없음, 기본값 사용",
12562
+ "config.saved": "설정 저장: {{path}}",
12563
+ "config.deleted": "설정 삭제: {{path}}",
12564
+ "general.done": "완료!",
12565
+ "general.failed": "실패",
12566
+ "general.skipped": "건너뜀"
12567
+ }
12568
+ };
12569
+ function getMessage(key, params) {
12570
+ const messages = MESSAGES[currentOptions.locale] || MESSAGES.en;
12571
+ let message = messages[key] || key;
12572
+ if (params) {
12573
+ for (const [k, v] of Object.entries(params)) {
12574
+ message = message.replace(new RegExp(`{{${k}}}`, "g"), v);
12575
+ }
12576
+ }
12577
+ return message;
12578
+ }
12579
+ function formatMessage(level, message) {
12580
+ const parts = [];
12581
+ if (currentOptions.timestamps) {
12582
+ const timestamp = new Date().toISOString().slice(11, 19);
12583
+ if (currentOptions.colors) {
12584
+ parts.push(`${COLORS.dim}[${timestamp}]${COLORS.reset}`);
12585
+ } else {
12586
+ parts.push(`[${timestamp}]`);
12587
+ }
12588
+ }
12589
+ if (currentOptions.prefix) {
12590
+ if (currentOptions.colors) {
12591
+ parts.push(`${COLORS.cyan}[${currentOptions.prefix}]${COLORS.reset}`);
12592
+ } else {
12593
+ parts.push(`[${currentOptions.prefix}]`);
12594
+ }
12595
+ }
12596
+ if (currentOptions.colors) {
12597
+ const color = LEVEL_COLORS[level];
12598
+ const icon = LEVEL_ICONS[level];
12599
+ parts.push(`${color}${icon}${COLORS.reset}`);
12600
+ } else {
12601
+ parts.push(`[${level.toUpperCase()}]`);
12602
+ }
12603
+ if (currentOptions.colors && level === "error") {
12604
+ parts.push(`${COLORS.red}${message}${COLORS.reset}`);
12605
+ } else if (currentOptions.colors && level === "warn") {
12606
+ parts.push(`${COLORS.yellow}${message}${COLORS.reset}`);
12607
+ } else {
12608
+ parts.push(message);
12609
+ }
12610
+ return parts.join(" ");
12611
+ }
12612
+ function shouldLog(level) {
12613
+ return LOG_LEVELS[level] >= LOG_LEVELS[currentOptions.level];
12614
+ }
12615
+ function debug(messageKey, params) {
12616
+ if (shouldLog("debug")) {
12617
+ const message = getMessage(messageKey, params);
12618
+ console.debug(formatMessage("debug", message));
12619
+ }
12620
+ }
12621
+ function info(messageKey, params) {
12622
+ if (shouldLog("info")) {
12623
+ const message = getMessage(messageKey, params);
12624
+ console.info(formatMessage("info", message));
12625
+ }
12626
+ }
12627
+ function warn(messageKey, params) {
12628
+ if (shouldLog("warn")) {
12629
+ const message = getMessage(messageKey, params);
12630
+ console.warn(formatMessage("warn", message));
12631
+ }
12632
+ }
12633
+ function error(messageKey, params) {
12634
+ if (shouldLog("error")) {
12635
+ const message = getMessage(messageKey, params);
12636
+ console.error(formatMessage("error", message));
12637
+ }
12638
+ }
12639
+ function success(messageKey, params) {
12640
+ if (shouldLog("info")) {
12641
+ const message = getMessage(messageKey, params);
12642
+ if (currentOptions.colors) {
12643
+ console.info(`${COLORS.green}✓${COLORS.reset} ${message}`);
12644
+ } else {
12645
+ console.info(`[SUCCESS] ${message}`);
12646
+ }
12647
+ }
12648
+ }
12649
+
12650
+ // src/core/config.ts
12651
+ var CONFIG_FILE = ".omcustomrc.json";
12652
+ var CURRENT_CONFIG_VERSION = 1;
12653
+ function getDefaultConfig() {
12654
+ return {
12655
+ configVersion: CURRENT_CONFIG_VERSION,
12656
+ version: "0.0.0",
12657
+ language: "en",
12658
+ provider: "auto",
12659
+ installedAt: "",
12660
+ lastUpdated: "",
12661
+ installedComponents: [],
12662
+ componentVersions: {},
12663
+ agents: {},
12664
+ preferences: getDefaultPreferences(),
12665
+ sourceRepo: "https://github.com/baekenough/oh-my-customcode",
12666
+ autoUpdate: {
12667
+ enabled: false,
12668
+ checkIntervalHours: 24,
12669
+ autoApplyMinor: false
12670
+ },
12671
+ preserveFiles: [],
12672
+ customComponents: []
12673
+ };
12674
+ }
12675
+ function getDefaultPreferences() {
12676
+ return {
12677
+ logLevel: "info",
12678
+ colors: true,
12679
+ showProgress: true,
12680
+ confirmPrompts: true,
12681
+ autoBackup: true
12682
+ };
12683
+ }
12684
+ function getConfigPath(targetDir) {
12685
+ return join2(targetDir, CONFIG_FILE);
12686
+ }
12687
+ async function loadConfig(targetDir) {
12688
+ const configPath = getConfigPath(targetDir);
12689
+ if (await fileExists(configPath)) {
12690
+ try {
12691
+ const config = await readJsonFile(configPath);
12692
+ const merged = mergeConfig(getDefaultConfig(), config);
12693
+ if (merged.configVersion < CURRENT_CONFIG_VERSION) {
12694
+ const migrated = migrateConfig(merged);
12695
+ await saveConfig(targetDir, migrated);
12696
+ return migrated;
12697
+ }
12698
+ return merged;
12699
+ } catch (err) {
12700
+ warn("config.load_failed", { error: String(err) });
12701
+ return getDefaultConfig();
12702
+ }
12703
+ }
12704
+ debug("config.not_found", { path: configPath });
12705
+ return getDefaultConfig();
12706
+ }
12707
+ async function saveConfig(targetDir, config) {
12708
+ const configPath = getConfigPath(targetDir);
12709
+ await ensureDirectory(targetDir);
12710
+ config.lastUpdated = new Date().toISOString();
12711
+ await writeJsonFile(configPath, config);
12712
+ debug("config.saved", { path: configPath });
12713
+ }
12714
+ function deduplicateCustomComponents(components) {
12715
+ const seen = new Map;
12716
+ for (const c of components) {
12717
+ seen.set(c.path, c);
12718
+ }
12719
+ return [...seen.values()];
12720
+ }
12721
+ function mergeConfig(defaults, overrides) {
12722
+ return {
12723
+ ...defaults,
12724
+ ...overrides,
12725
+ preferences: overrides.preferences ? { ...defaults.preferences, ...overrides.preferences } : defaults.preferences,
12726
+ autoUpdate: overrides.autoUpdate ? { ...defaults.autoUpdate, ...overrides.autoUpdate } : defaults.autoUpdate,
12727
+ componentVersions: {
12728
+ ...defaults.componentVersions,
12729
+ ...overrides.componentVersions
12730
+ },
12731
+ agents: {
12732
+ ...defaults.agents,
12733
+ ...overrides.agents
12734
+ },
12735
+ preserveFiles: overrides.preserveFiles ? [...new Set([...defaults.preserveFiles || [], ...overrides.preserveFiles])] : defaults.preserveFiles,
12736
+ customComponents: overrides.customComponents ? deduplicateCustomComponents([
12737
+ ...defaults.customComponents || [],
12738
+ ...overrides.customComponents
12739
+ ]) : defaults.customComponents
12740
+ };
12741
+ }
12742
+ function migrateConfig(config) {
12743
+ const migrated = { ...config };
12744
+ if (config.configVersion < 1) {
12745
+ migrated.configVersion = 1;
12746
+ migrated.preferences = getDefaultPreferences();
12747
+ migrated.autoUpdate = {
12748
+ enabled: false,
12749
+ checkIntervalHours: 24,
12750
+ autoApplyMinor: false
12751
+ };
12752
+ }
12753
+ migrated.configVersion = CURRENT_CONFIG_VERSION;
12754
+ return migrated;
12755
+ }
12756
+
12757
+ // src/core/layout.ts
12758
+ var PROVIDER_LAYOUTS = {
12759
+ claude: {
12760
+ provider: "claude",
12761
+ rootDir: ".claude",
12762
+ entryFile: "CLAUDE.md",
12763
+ entryTemplatePrefix: "CLAUDE.md",
12764
+ manifestFile: "manifest.json",
12765
+ backupDirPrefix: ".claude-backup-",
12766
+ directoryStructure: [
12767
+ ".claude",
12768
+ ".claude/rules",
12769
+ ".claude/hooks",
12770
+ ".claude/contexts",
12771
+ ".claude/agents",
12772
+ ".claude/skills",
12773
+ "guides"
12774
+ ]
12775
+ },
12776
+ codex: {
12777
+ provider: "codex",
12778
+ rootDir: ".codex",
12779
+ entryFile: "AGENTS.md",
12780
+ entryTemplatePrefix: "AGENTS.md",
12781
+ manifestFile: "manifest.codex.json",
12782
+ backupDirPrefix: ".codex-backup-",
12783
+ directoryStructure: [
12784
+ ".codex",
12785
+ ".codex/rules",
12786
+ ".codex/hooks",
12787
+ ".codex/contexts",
12788
+ ".codex/agents",
12789
+ ".codex/skills",
12790
+ "guides"
12791
+ ]
12792
+ }
12793
+ };
12794
+ function getProviderLayout(provider) {
12795
+ return PROVIDER_LAYOUTS[provider];
12796
+ }
12797
+ function getEntryTemplateName(provider, language) {
12798
+ const layout = getProviderLayout(provider);
12799
+ return `${layout.entryTemplatePrefix}.${language}`;
12800
+ }
12801
+ function getComponentPath(provider, component) {
12802
+ const layout = getProviderLayout(provider);
12803
+ if (component === "entry-md") {
12804
+ return layout.entryFile;
12805
+ }
12806
+ if (component === "guides") {
12807
+ return "guides";
12808
+ }
12809
+ return `${layout.rootDir}/${component}`;
12810
+ }
12811
+ function getDefaultProvider() {
12812
+ return "claude";
12813
+ }
12814
+
12815
+ // src/core/provider.ts
12816
+ import { join as join3 } from "node:path";
12817
+ var ENV_SIGNALS = {
12818
+ claude: [
12819
+ "ANTHROPIC_API_KEY",
12820
+ "CLAUDE_CODE",
12821
+ "CLAUDE_CODE_EFFORT_LEVEL",
12822
+ "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
12823
+ "CLAUDE_CODE_ENABLE_TELEMETRY"
12824
+ ],
12825
+ codex: ["OPENAI_API_KEY", "OPENAI_ORG_ID", "OPENAI_PROJECT", "CODEX_HOME", "CODEX_PROJECT"]
12826
+ };
12827
+ var PROVIDER_ENV_OVERRIDES = ["OMCUSTOM_PROVIDER", "LLM_SERVICE"];
12828
+ function normalizeProvider(value) {
12829
+ if (!value)
12830
+ return null;
12831
+ const normalized = value.toLowerCase().trim();
12832
+ if (normalized === "claude" || normalized === "codex" || normalized === "auto") {
12833
+ return normalized;
12834
+ }
12835
+ return null;
12836
+ }
12837
+ function detectFromEnv(env) {
12838
+ for (const key of PROVIDER_ENV_OVERRIDES) {
12839
+ const override = normalizeProvider(env[key]);
12840
+ if (override && override !== "auto") {
12841
+ return {
12842
+ provider: override,
12843
+ source: "override",
12844
+ confidence: "high",
12845
+ reason: `env:${key}`
12846
+ };
12847
+ }
12848
+ }
12849
+ const claudeSignals = ENV_SIGNALS.claude.filter((key) => Boolean(env[key]));
12850
+ const codexSignals = ENV_SIGNALS.codex.filter((key) => Boolean(env[key]));
12851
+ const hasClaude = claudeSignals.length > 0;
12852
+ const hasCodex = codexSignals.length > 0;
12853
+ if (hasClaude && !hasCodex) {
12854
+ return {
12855
+ provider: "claude",
12856
+ source: "env",
12857
+ confidence: "medium",
12858
+ reason: `env:${claudeSignals[0]}`
12859
+ };
12860
+ }
12861
+ if (hasCodex && !hasClaude) {
12862
+ return {
12863
+ provider: "codex",
12864
+ source: "env",
12865
+ confidence: "medium",
12866
+ reason: `env:${codexSignals[0]}`
12867
+ };
12868
+ }
12869
+ return null;
12870
+ }
12871
+ async function detectFromProject(targetDir) {
12872
+ const claudeMarkers = [join3(targetDir, "CLAUDE.md"), join3(targetDir, ".claude")];
12873
+ const codexMarkers = [join3(targetDir, "AGENTS.md"), join3(targetDir, ".codex")];
12874
+ const claudeFound = await Promise.all(claudeMarkers.map((path) => fileExists(path)));
12875
+ const codexFound = await Promise.all(codexMarkers.map((path) => fileExists(path)));
12876
+ const hasClaude = claudeFound.some(Boolean);
12877
+ const hasCodex = codexFound.some(Boolean);
12878
+ if (hasClaude && !hasCodex) {
12879
+ return {
12880
+ provider: "claude",
12881
+ source: "project",
12882
+ confidence: "medium",
12883
+ reason: "project:claude"
12884
+ };
12885
+ }
12886
+ if (hasCodex && !hasClaude) {
12887
+ return {
12888
+ provider: "codex",
12889
+ source: "project",
12890
+ confidence: "medium",
12891
+ reason: "project:codex"
12892
+ };
12893
+ }
12894
+ return null;
12895
+ }
12896
+ async function detectFromConfig(targetDir) {
12897
+ const configPath = join3(targetDir, ".omcustomrc.json");
12898
+ if (!await fileExists(configPath)) {
12899
+ return null;
12900
+ }
12901
+ try {
12902
+ const config = await readJsonFile(configPath);
12903
+ const provider = normalizeProvider(config.provider);
12904
+ if (provider && provider !== "auto") {
12905
+ return {
12906
+ provider,
12907
+ source: "config",
12908
+ confidence: "high",
12909
+ reason: "config:provider"
12910
+ };
12911
+ }
12912
+ } catch {}
12913
+ return null;
12914
+ }
12915
+ async function detectProvider(options = {}) {
12916
+ const env = options.env ?? process.env;
12917
+ const override = options.override;
12918
+ const normalizedOverride = normalizeProvider(override);
12919
+ if (normalizedOverride && normalizedOverride !== "auto") {
12920
+ return {
12921
+ provider: normalizedOverride,
12922
+ source: "override",
12923
+ confidence: "high",
12924
+ reason: "override:option"
12925
+ };
12926
+ }
12927
+ if (options.targetDir) {
12928
+ const fromConfig = await detectFromConfig(options.targetDir);
12929
+ if (fromConfig) {
12930
+ return fromConfig;
12931
+ }
12932
+ }
12933
+ if (options.targetDir && options.preferProject) {
12344
12934
  const fromProject = await detectFromProject(options.targetDir);
12345
12935
  if (fromProject) {
12346
12936
  return fromProject;
@@ -12690,6 +13280,49 @@ async function checkContexts(targetDir, rootDir = ".claude") {
12690
13280
  fixable: false
12691
13281
  };
12692
13282
  }
13283
+ async function checkCustomComponents(targetDir, _rootDir = ".claude") {
13284
+ try {
13285
+ const config = await loadConfig(targetDir);
13286
+ const customComponents = config.customComponents || [];
13287
+ if (customComponents.length === 0) {
13288
+ return {
13289
+ name: "Custom components",
13290
+ status: "pass",
13291
+ message: "No custom components configured",
13292
+ fixable: false
13293
+ };
13294
+ }
13295
+ const missing = [];
13296
+ for (const component of customComponents) {
13297
+ const fullPath = path.join(targetDir, component.path);
13298
+ if (!await pathExists(fullPath)) {
13299
+ missing.push(component.path);
13300
+ }
13301
+ }
13302
+ if (missing.length > 0) {
13303
+ return {
13304
+ name: "Custom components",
13305
+ status: "warn",
13306
+ message: `Custom components: ${customComponents.length} items (${missing.length} missing)`,
13307
+ fixable: false,
13308
+ details: missing
13309
+ };
13310
+ }
13311
+ return {
13312
+ name: "Custom components",
13313
+ status: "pass",
13314
+ message: `Custom components: ${customComponents.length} items (managed: false)`,
13315
+ fixable: false
13316
+ };
13317
+ } catch {
13318
+ return {
13319
+ name: "Custom components",
13320
+ status: "pass",
13321
+ message: "No config file found",
13322
+ fixable: false
13323
+ };
13324
+ }
13325
+ }
12693
13326
  async function fixBrokenSymlinks(_targetDir, brokenSymlinks) {
12694
13327
  let fixed = 0;
12695
13328
  for (const symlink of brokenSymlinks) {
@@ -12777,7 +13410,8 @@ async function doctorCommand(options = {}) {
12777
13410
  checkIndexFiles(targetDir),
12778
13411
  checkGuides(targetDir),
12779
13412
  checkHooks(targetDir, layout.rootDir),
12780
- checkContexts(targetDir, layout.rootDir)
13413
+ checkContexts(targetDir, layout.rootDir),
13414
+ checkCustomComponents(targetDir, layout.rootDir)
12781
13415
  ]);
12782
13416
  if (options.fix) {
12783
13417
  const hasFixableIssues = checks.some((c) => c.status === "fail" && c.fixable);
@@ -12790,332 +13424,47 @@ async function doctorCommand(options = {}) {
12790
13424
  }
12791
13425
  for (const check of checks) {
12792
13426
  if (!options.quiet || check.status !== "pass") {
12793
- printCheck(check);
12794
- }
12795
- }
12796
- const passCount = checks.filter((c) => c.status === "pass" || c.fixed).length;
12797
- const warnCount = checks.filter((c) => c.status === "warn").length;
12798
- const failCount = checks.filter((c) => c.status === "fail" && !c.fixed).length;
12799
- const fixedCount = checks.filter((c) => c.fixed).length;
12800
- console.log("");
12801
- if (failCount === 0) {
12802
- console.log(i18n.t("cli.doctor.passed"));
12803
- } else {
12804
- console.log(i18n.t("cli.doctor.failed"));
12805
- if (!options.fix) {
12806
- const fixableCount = checks.filter((c) => c.status === "fail" && c.fixable).length;
12807
- if (fixableCount > 0) {
12808
- console.log(i18n.t("cli.doctor.runWithFix", { count: fixableCount }));
12809
- }
12810
- }
12811
- }
12812
- console.log(i18n.t("cli.doctor.summary", {
12813
- pass: passCount,
12814
- warn: warnCount,
12815
- fail: failCount,
12816
- fixed: fixedCount
12817
- }));
12818
- return {
12819
- success: failCount === 0,
12820
- checks,
12821
- passCount,
12822
- warnCount,
12823
- failCount,
12824
- fixedCount
12825
- };
12826
- }
12827
-
12828
- // src/cli/init.ts
12829
- import { join as join5 } from "node:path";
12830
-
12831
- // src/core/installer.ts
12832
- import { copyFile as fsCopyFile, rename } from "node:fs/promises";
12833
- import { basename, join as join4 } from "node:path";
12834
-
12835
- // src/utils/logger.ts
12836
- var currentOptions = {
12837
- level: "info",
12838
- colors: true,
12839
- locale: "en",
12840
- timestamps: false
12841
- };
12842
- var LOG_LEVELS = {
12843
- debug: 0,
12844
- info: 1,
12845
- warn: 2,
12846
- error: 3
12847
- };
12848
- var COLORS = {
12849
- reset: "\x1B[0m",
12850
- bold: "\x1B[1m",
12851
- dim: "\x1B[2m",
12852
- black: "\x1B[30m",
12853
- red: "\x1B[31m",
12854
- green: "\x1B[32m",
12855
- yellow: "\x1B[33m",
12856
- blue: "\x1B[34m",
12857
- magenta: "\x1B[35m",
12858
- cyan: "\x1B[36m",
12859
- white: "\x1B[37m",
12860
- brightRed: "\x1B[91m",
12861
- brightGreen: "\x1B[92m",
12862
- brightYellow: "\x1B[93m",
12863
- brightBlue: "\x1B[94m",
12864
- brightMagenta: "\x1B[95m",
12865
- brightCyan: "\x1B[96m"
12866
- };
12867
- var LEVEL_COLORS = {
12868
- debug: COLORS.dim,
12869
- info: COLORS.blue,
12870
- warn: COLORS.yellow,
12871
- error: COLORS.red
12872
- };
12873
- var LEVEL_ICONS = {
12874
- debug: "\uD83D\uDD0D",
12875
- info: "ℹ️",
12876
- warn: "⚠️",
12877
- error: "❌"
12878
- };
12879
- var MESSAGES = {
12880
- en: {
12881
- "install.start": "Initializing oh-my-customcode...",
12882
- "install.success": "Successfully initialized!",
12883
- "install.failed": "Installation failed: {{error}}",
12884
- "install.exists": "Existing {{rootDir}} directory found",
12885
- "install.backup": "Backed up existing files to: {{path}}",
12886
- "install.directories_created": "Directory structure created",
12887
- "install.component_skipped": "Skipped {{component}} (already exists)",
12888
- "install.component_installed": "Installed {{component}}",
12889
- "install.template_not_found": "Template not found for {{component}}: {{path}}",
12890
- "install.claude_md_installed": "CLAUDE.md installed ({{language}})",
12891
- "install.claude_md_not_found": "CLAUDE.md template not found for {{language}}",
12892
- "install.entry_md_installed": "{{entry}} installed ({{language}})",
12893
- "install.entry_md_not_found": "{{entry}} template not found for {{language}}",
12894
- "install.entry_md_skipped": "{{entry}} skipped ({{reason}})",
12895
- "update.start": "Checking for updates...",
12896
- "update.success": "Updated from {{from}} to {{to}}",
12897
- "update.failed": "Update failed: {{error}}",
12898
- "update.no_updates": "Already up to date",
12899
- "update.backup_created": "Backup created at: {{path}}",
12900
- "update.dry_run": "Would update {{component}}",
12901
- "update.component_updated": "Updated {{component}}",
12902
- "update.file_applied": "Applied update to {{path}}",
12903
- "config.load_failed": "Failed to load config: {{error}}",
12904
- "config.not_found": "Config not found at {{path}}, using defaults",
12905
- "config.saved": "Config saved to {{path}}",
12906
- "config.deleted": "Config deleted from {{path}}",
12907
- "general.done": "Done!",
12908
- "general.failed": "Failed",
12909
- "general.skipped": "Skipped"
12910
- },
12911
- ko: {
12912
- "install.start": "oh-my-customcode 초기화 중...",
12913
- "install.success": "초기화 완료!",
12914
- "install.failed": "설치 실패: {{error}}",
12915
- "install.exists": "기존 {{rootDir}} 디렉토리 발견",
12916
- "install.backup": "기존 파일 백업 완료: {{path}}",
12917
- "install.directories_created": "디렉토리 구조 생성 완료",
12918
- "install.component_skipped": "{{component}} 건너뜀 (이미 존재)",
12919
- "install.component_installed": "{{component}} 설치 완료",
12920
- "install.template_not_found": "{{component}} 템플릿 없음: {{path}}",
12921
- "install.claude_md_installed": "CLAUDE.md 설치 완료 ({{language}})",
12922
- "install.claude_md_not_found": "{{language}}용 CLAUDE.md 템플릿 없음",
12923
- "install.entry_md_installed": "{{entry}} 설치 완료 ({{language}})",
12924
- "install.entry_md_not_found": "{{language}}용 {{entry}} 템플릿 없음",
12925
- "install.entry_md_skipped": "{{entry}} 건너뜀 ({{reason}})",
12926
- "update.start": "업데이트 확인 중...",
12927
- "update.success": "{{from}}에서 {{to}}로 업데이트 완료",
12928
- "update.failed": "업데이트 실패: {{error}}",
12929
- "update.no_updates": "이미 최신 버전입니다",
12930
- "update.backup_created": "백업 생성됨: {{path}}",
12931
- "update.dry_run": "{{component}} 업데이트 예정",
12932
- "update.component_updated": "{{component}} 업데이트 완료",
12933
- "update.file_applied": "{{path}} 업데이트 적용",
12934
- "config.load_failed": "설정 로드 실패: {{error}}",
12935
- "config.not_found": "{{path}}에 설정 없음, 기본값 사용",
12936
- "config.saved": "설정 저장: {{path}}",
12937
- "config.deleted": "설정 삭제: {{path}}",
12938
- "general.done": "완료!",
12939
- "general.failed": "실패",
12940
- "general.skipped": "건너뜀"
12941
- }
12942
- };
12943
- function getMessage(key, params) {
12944
- const messages = MESSAGES[currentOptions.locale] || MESSAGES.en;
12945
- let message = messages[key] || key;
12946
- if (params) {
12947
- for (const [k, v] of Object.entries(params)) {
12948
- message = message.replace(new RegExp(`{{${k}}}`, "g"), v);
12949
- }
12950
- }
12951
- return message;
12952
- }
12953
- function formatMessage(level, message) {
12954
- const parts = [];
12955
- if (currentOptions.timestamps) {
12956
- const timestamp = new Date().toISOString().slice(11, 19);
12957
- if (currentOptions.colors) {
12958
- parts.push(`${COLORS.dim}[${timestamp}]${COLORS.reset}`);
12959
- } else {
12960
- parts.push(`[${timestamp}]`);
12961
- }
12962
- }
12963
- if (currentOptions.prefix) {
12964
- if (currentOptions.colors) {
12965
- parts.push(`${COLORS.cyan}[${currentOptions.prefix}]${COLORS.reset}`);
12966
- } else {
12967
- parts.push(`[${currentOptions.prefix}]`);
12968
- }
12969
- }
12970
- if (currentOptions.colors) {
12971
- const color = LEVEL_COLORS[level];
12972
- const icon = LEVEL_ICONS[level];
12973
- parts.push(`${color}${icon}${COLORS.reset}`);
12974
- } else {
12975
- parts.push(`[${level.toUpperCase()}]`);
12976
- }
12977
- if (currentOptions.colors && level === "error") {
12978
- parts.push(`${COLORS.red}${message}${COLORS.reset}`);
12979
- } else if (currentOptions.colors && level === "warn") {
12980
- parts.push(`${COLORS.yellow}${message}${COLORS.reset}`);
12981
- } else {
12982
- parts.push(message);
12983
- }
12984
- return parts.join(" ");
12985
- }
12986
- function shouldLog(level) {
12987
- return LOG_LEVELS[level] >= LOG_LEVELS[currentOptions.level];
12988
- }
12989
- function debug(messageKey, params) {
12990
- if (shouldLog("debug")) {
12991
- const message = getMessage(messageKey, params);
12992
- console.debug(formatMessage("debug", message));
12993
- }
12994
- }
12995
- function info(messageKey, params) {
12996
- if (shouldLog("info")) {
12997
- const message = getMessage(messageKey, params);
12998
- console.info(formatMessage("info", message));
12999
- }
13000
- }
13001
- function warn(messageKey, params) {
13002
- if (shouldLog("warn")) {
13003
- const message = getMessage(messageKey, params);
13004
- console.warn(formatMessage("warn", message));
13005
- }
13006
- }
13007
- function error(messageKey, params) {
13008
- if (shouldLog("error")) {
13009
- const message = getMessage(messageKey, params);
13010
- console.error(formatMessage("error", message));
13011
- }
13012
- }
13013
- function success(messageKey, params) {
13014
- if (shouldLog("info")) {
13015
- const message = getMessage(messageKey, params);
13016
- if (currentOptions.colors) {
13017
- console.info(`${COLORS.green}✓${COLORS.reset} ${message}`);
13018
- } else {
13019
- console.info(`[SUCCESS] ${message}`);
13020
- }
13021
- }
13022
- }
13023
-
13024
- // src/core/config.ts
13025
- import { join as join3 } from "node:path";
13026
- var CONFIG_FILE = ".omcustomrc.json";
13027
- var CURRENT_CONFIG_VERSION = 1;
13028
- function getDefaultConfig() {
13029
- return {
13030
- configVersion: CURRENT_CONFIG_VERSION,
13031
- version: "0.0.0",
13032
- language: "en",
13033
- provider: "auto",
13034
- installedAt: "",
13035
- lastUpdated: "",
13036
- installedComponents: [],
13037
- componentVersions: {},
13038
- agents: {},
13039
- preferences: getDefaultPreferences(),
13040
- sourceRepo: "https://github.com/baekenough/oh-my-customcode",
13041
- autoUpdate: {
13042
- enabled: false,
13043
- checkIntervalHours: 24,
13044
- autoApplyMinor: false
13427
+ printCheck(check);
13045
13428
  }
13046
- };
13047
- }
13048
- function getDefaultPreferences() {
13049
- return {
13050
- logLevel: "info",
13051
- colors: true,
13052
- showProgress: true,
13053
- confirmPrompts: true,
13054
- autoBackup: true
13055
- };
13056
- }
13057
- function getConfigPath(targetDir) {
13058
- return join3(targetDir, CONFIG_FILE);
13059
- }
13060
- async function loadConfig(targetDir) {
13061
- const configPath = getConfigPath(targetDir);
13062
- if (await fileExists(configPath)) {
13063
- try {
13064
- const config = await readJsonFile(configPath);
13065
- const merged = mergeConfig(getDefaultConfig(), config);
13066
- if (merged.configVersion < CURRENT_CONFIG_VERSION) {
13067
- const migrated = migrateConfig(merged);
13068
- await saveConfig(targetDir, migrated);
13069
- return migrated;
13429
+ }
13430
+ const passCount = checks.filter((c) => c.status === "pass" || c.fixed).length;
13431
+ const warnCount = checks.filter((c) => c.status === "warn").length;
13432
+ const failCount = checks.filter((c) => c.status === "fail" && !c.fixed).length;
13433
+ const fixedCount = checks.filter((c) => c.fixed).length;
13434
+ console.log("");
13435
+ if (failCount === 0) {
13436
+ console.log(i18n.t("cli.doctor.passed"));
13437
+ } else {
13438
+ console.log(i18n.t("cli.doctor.failed"));
13439
+ if (!options.fix) {
13440
+ const fixableCount = checks.filter((c) => c.status === "fail" && c.fixable).length;
13441
+ if (fixableCount > 0) {
13442
+ console.log(i18n.t("cli.doctor.runWithFix", { count: fixableCount }));
13070
13443
  }
13071
- return merged;
13072
- } catch (err) {
13073
- warn("config.load_failed", { error: String(err) });
13074
- return getDefaultConfig();
13075
13444
  }
13076
13445
  }
13077
- debug("config.not_found", { path: configPath });
13078
- return getDefaultConfig();
13079
- }
13080
- async function saveConfig(targetDir, config) {
13081
- const configPath = getConfigPath(targetDir);
13082
- await ensureDirectory(targetDir);
13083
- config.lastUpdated = new Date().toISOString();
13084
- await writeJsonFile(configPath, config);
13085
- debug("config.saved", { path: configPath });
13086
- }
13087
- function mergeConfig(defaults, overrides) {
13446
+ console.log(i18n.t("cli.doctor.summary", {
13447
+ pass: passCount,
13448
+ warn: warnCount,
13449
+ fail: failCount,
13450
+ fixed: fixedCount
13451
+ }));
13088
13452
  return {
13089
- ...defaults,
13090
- ...overrides,
13091
- preferences: overrides.preferences ? { ...defaults.preferences, ...overrides.preferences } : defaults.preferences,
13092
- autoUpdate: overrides.autoUpdate ? { ...defaults.autoUpdate, ...overrides.autoUpdate } : defaults.autoUpdate,
13093
- componentVersions: {
13094
- ...defaults.componentVersions,
13095
- ...overrides.componentVersions
13096
- },
13097
- agents: {
13098
- ...defaults.agents,
13099
- ...overrides.agents
13100
- }
13453
+ success: failCount === 0,
13454
+ checks,
13455
+ passCount,
13456
+ warnCount,
13457
+ failCount,
13458
+ fixedCount
13101
13459
  };
13102
13460
  }
13103
- function migrateConfig(config) {
13104
- const migrated = { ...config };
13105
- if (config.configVersion < 1) {
13106
- migrated.configVersion = 1;
13107
- migrated.preferences = getDefaultPreferences();
13108
- migrated.autoUpdate = {
13109
- enabled: false,
13110
- checkIntervalHours: 24,
13111
- autoApplyMinor: false
13112
- };
13113
- }
13114
- migrated.configVersion = CURRENT_CONFIG_VERSION;
13115
- return migrated;
13116
- }
13461
+
13462
+ // src/cli/init.ts
13463
+ import { join as join5 } from "node:path";
13117
13464
 
13118
13465
  // src/core/installer.ts
13466
+ import { copyFile as fsCopyFile, rename } from "node:fs/promises";
13467
+ import { basename, join as join4 } from "node:path";
13119
13468
  var DEFAULT_LANGUAGE2 = "en";
13120
13469
  function getTemplateDir() {
13121
13470
  const packageRoot = getPackageRoot();
@@ -13397,7 +13746,7 @@ async function initCommand(options) {
13397
13746
  }
13398
13747
 
13399
13748
  // src/cli/list.ts
13400
- import { basename as basename2, dirname as dirname2, join as join6, relative } from "node:path";
13749
+ import { basename as basename2, dirname as dirname2, join as join6, relative as relative2 } from "node:path";
13401
13750
  var ALLOWED_TOP_LEVEL_KEYS = new Set(["name", "type", "description", "version", "category"]);
13402
13751
  function parseKeyValue(line) {
13403
13752
  const colonIndex = line.indexOf(":");
@@ -13461,12 +13810,12 @@ function extractAgentTypeFromFilename(filename) {
13461
13810
  return prefixMap[prefix] || "unknown";
13462
13811
  }
13463
13812
  function extractSkillCategoryFromPath(skillPath, baseDir, rootDir) {
13464
- const relativePath = relative(join6(baseDir, rootDir, "skills"), skillPath);
13813
+ const relativePath = relative2(join6(baseDir, rootDir, "skills"), skillPath);
13465
13814
  const parts = relativePath.split("/").filter(Boolean);
13466
13815
  return parts[0] || "unknown";
13467
13816
  }
13468
13817
  function extractGuideCategoryFromPath(guidePath, baseDir) {
13469
- const relativePath = relative(join6(baseDir, "guides"), guidePath);
13818
+ const relativePath = relative2(join6(baseDir, "guides"), guidePath);
13470
13819
  const parts = relativePath.split("/").filter(Boolean);
13471
13820
  return parts[0] || "unknown";
13472
13821
  }
@@ -13564,17 +13913,22 @@ async function getAgents(targetDir, rootDir = ".claude") {
13564
13913
  if (!await fileExists(agentsDir))
13565
13914
  return [];
13566
13915
  try {
13916
+ const config = await loadConfig(targetDir);
13917
+ const customComponents = config.customComponents || [];
13918
+ const customAgentPaths = new Set(customComponents.filter((c) => c.type === "agent").map((c) => c.path));
13567
13919
  const agentMdFiles = await listFiles(agentsDir, { recursive: false, pattern: "*.md" });
13568
13920
  const agents = await Promise.all(agentMdFiles.map(async (agentMdPath) => {
13569
13921
  const filename = basename2(agentMdPath);
13570
13922
  const name = basename2(filename, ".md");
13571
13923
  const description = await tryExtractMarkdownDescription(agentMdPath);
13924
+ const relativePath = relative2(targetDir, agentMdPath);
13572
13925
  return {
13573
13926
  name,
13574
13927
  type: extractAgentTypeFromFilename(filename),
13575
- path: relative(targetDir, agentMdPath),
13928
+ path: relativePath,
13576
13929
  description,
13577
- version: undefined
13930
+ version: undefined,
13931
+ managed: !customAgentPaths.has(relativePath)
13578
13932
  };
13579
13933
  }));
13580
13934
  return agents.sort((a, b) => a.name.localeCompare(b.name));
@@ -13587,18 +13941,23 @@ async function getSkills(targetDir, rootDir = ".claude") {
13587
13941
  if (!await fileExists(skillsDir))
13588
13942
  return [];
13589
13943
  try {
13944
+ const config = await loadConfig(targetDir);
13945
+ const customComponents = config.customComponents || [];
13946
+ const customSkillPaths = new Set(customComponents.filter((c) => c.type === "skill").map((c) => c.path));
13590
13947
  const skillMdFiles = await listFiles(skillsDir, { recursive: true, pattern: "SKILL.md" });
13591
13948
  const skills = await Promise.all(skillMdFiles.map(async (skillMdPath) => {
13592
13949
  const skillDir = dirname2(skillMdPath);
13593
13950
  const indexYamlPath = join6(skillDir, "index.yaml");
13594
13951
  const { description, version } = await tryReadIndexYamlMetadata(indexYamlPath);
13952
+ const relativePath = relative2(targetDir, skillDir);
13595
13953
  return {
13596
13954
  name: basename2(skillDir),
13597
13955
  type: "skill",
13598
13956
  category: extractSkillCategoryFromPath(skillDir, targetDir, rootDir),
13599
- path: relative(targetDir, skillDir),
13957
+ path: relativePath,
13600
13958
  description,
13601
- version
13959
+ version,
13960
+ managed: !customSkillPaths.has(relativePath)
13602
13961
  };
13603
13962
  }));
13604
13963
  return skills.sort((a, b) => a.name.localeCompare(b.name));
@@ -13611,15 +13970,20 @@ async function getGuides(targetDir) {
13611
13970
  if (!await fileExists(guidesDir))
13612
13971
  return [];
13613
13972
  try {
13973
+ const config = await loadConfig(targetDir);
13974
+ const customComponents = config.customComponents || [];
13975
+ const customGuidePaths = new Set(customComponents.filter((c) => c.type === "guide").map((c) => c.path));
13614
13976
  const guideMdFiles = await listFiles(guidesDir, { recursive: true, pattern: "*.md" });
13615
13977
  const guides = await Promise.all(guideMdFiles.map(async (guideMdPath) => {
13616
13978
  const description = await tryExtractMarkdownDescription(guideMdPath, { maxLength: 100 });
13979
+ const relativePath = relative2(targetDir, guideMdPath);
13617
13980
  return {
13618
13981
  name: basename2(guideMdPath, ".md"),
13619
13982
  type: "guide",
13620
13983
  category: extractGuideCategoryFromPath(guideMdPath, targetDir),
13621
- path: relative(targetDir, guideMdPath),
13622
- description
13984
+ path: relativePath,
13985
+ description,
13986
+ managed: !customGuidePaths.has(relativePath)
13623
13987
  };
13624
13988
  }));
13625
13989
  return guides.sort((a, b) => a.name.localeCompare(b.name));
@@ -13633,17 +13997,22 @@ async function getRules(targetDir, rootDir = ".claude") {
13633
13997
  if (!await fileExists(rulesDir))
13634
13998
  return [];
13635
13999
  try {
14000
+ const config = await loadConfig(targetDir);
14001
+ const customComponents = config.customComponents || [];
14002
+ const customRulePaths = new Set(customComponents.filter((c) => c.type === "rule").map((c) => c.path));
13636
14003
  const ruleMdFiles = await listFiles(rulesDir, { recursive: false, pattern: "*.md" });
13637
14004
  const rules = await Promise.all(ruleMdFiles.map(async (ruleMdPath) => {
13638
14005
  const filename = basename2(ruleMdPath);
13639
14006
  const description = await tryExtractMarkdownDescription(ruleMdPath, {
13640
14007
  cleanFormatting: true
13641
14008
  });
14009
+ const relativePath = relative2(targetDir, ruleMdPath);
13642
14010
  return {
13643
14011
  name: basename2(ruleMdPath, ".md"),
13644
14012
  type: extractRulePriorityFromFilename(filename),
13645
- path: relative(targetDir, ruleMdPath),
13646
- description
14013
+ path: relativePath,
14014
+ description,
14015
+ managed: !customRulePaths.has(relativePath)
13647
14016
  };
13648
14017
  }));
13649
14018
  return rules.sort((a, b) => {
@@ -13669,7 +14038,8 @@ function formatAsTable(components, type) {
13669
14038
  console.log(` ${nameHeader} ${typeHeader} Description`);
13670
14039
  console.log(` ${"─".repeat(nameWidth)} ${"─".repeat(typeWidth)} ${"─".repeat(40)}`);
13671
14040
  for (const component of components) {
13672
- const name = component.name.padEnd(nameWidth);
14041
+ const managedTag = component.managed === false ? " [custom]" : "";
14042
+ const name = `${component.name}${managedTag}`.padEnd(nameWidth);
13673
14043
  const typeOrCategory = (component.category || component.type).padEnd(typeWidth);
13674
14044
  const description = component.description ? component.description.substring(0, 40) : "";
13675
14045
  console.log(` ${name} ${typeOrCategory} ${description}`);
@@ -13687,7 +14057,8 @@ function formatAsSimple(components, type) {
13687
14057
  ${type} (${components.length}):`);
13688
14058
  for (const component of components) {
13689
14059
  const typeInfo = component.category || component.type;
13690
- console.log(` ${component.name} [${typeInfo}]`);
14060
+ const managedTag = component.managed === false ? " [custom]" : "";
14061
+ console.log(` ${component.name}${managedTag} [${typeInfo}]`);
13691
14062
  }
13692
14063
  }
13693
14064
  function formatAsJson(components) {
@@ -13705,7 +14076,7 @@ async function getHooks(targetDir, rootDir = ".claude") {
13705
14076
  return allFiles.map((hookPath) => ({
13706
14077
  name: basename2(hookPath),
13707
14078
  type: "hook",
13708
- path: relative(targetDir, hookPath)
14079
+ path: relative2(targetDir, hookPath)
13709
14080
  })).sort((a, b) => a.name.localeCompare(b.name));
13710
14081
  } catch {
13711
14082
  return [];
@@ -13725,7 +14096,7 @@ async function getContexts(targetDir, rootDir = ".claude") {
13725
14096
  return {
13726
14097
  name: basename2(ctxPath, ext),
13727
14098
  type: "context",
13728
- path: relative(targetDir, ctxPath),
14099
+ path: relative2(targetDir, ctxPath),
13729
14100
  description
13730
14101
  };
13731
14102
  }));
@@ -13795,73 +14166,439 @@ async function listCommand(type = "all", options = {}) {
13795
14166
  }
13796
14167
  }
13797
14168
 
13798
- // src/cli/update.ts
13799
- async function getCurrentVersion() {
13800
- return null;
14169
+ // src/core/updater.ts
14170
+ import { join as join7 } from "node:path";
14171
+
14172
+ // src/core/entry-merger.ts
14173
+ var MANAGED_START = "<!-- omcustom:start -->";
14174
+ var MANAGED_END = "<!-- omcustom:end -->";
14175
+ function parseEntryDoc(content) {
14176
+ const sections = [];
14177
+ const lines = content.split(`
14178
+ `);
14179
+ let currentSection = null;
14180
+ let currentLines = [];
14181
+ for (const line of lines) {
14182
+ if (line.trim() === MANAGED_START) {
14183
+ if (currentLines.length > 0) {
14184
+ sections.push({
14185
+ type: "custom",
14186
+ content: currentLines.join(`
14187
+ `)
14188
+ });
14189
+ currentLines = [];
14190
+ }
14191
+ currentSection = { type: "managed", content: "" };
14192
+ continue;
14193
+ }
14194
+ if (line.trim() === MANAGED_END) {
14195
+ if (currentSection && currentSection.type === "managed") {
14196
+ currentSection.content = currentLines.join(`
14197
+ `);
14198
+ sections.push(currentSection);
14199
+ currentSection = null;
14200
+ currentLines = [];
14201
+ }
14202
+ continue;
14203
+ }
14204
+ currentLines.push(line);
14205
+ }
14206
+ if (currentLines.length > 0) {
14207
+ sections.push({
14208
+ type: "custom",
14209
+ content: currentLines.join(`
14210
+ `)
14211
+ });
14212
+ }
14213
+ return { sections };
13801
14214
  }
13802
- async function getLatestVersion() {
14215
+ function mergeEntryDoc(existingContent, templateContent) {
14216
+ const warnings = [];
14217
+ const { sections } = parseEntryDoc(existingContent);
14218
+ const hasManagedSections = sections.some((s) => s.type === "managed");
14219
+ if (!hasManagedSections) {
14220
+ const wrapped = wrapInManagedMarkers(templateContent);
14221
+ return {
14222
+ content: wrapped,
14223
+ managedSections: 1,
14224
+ customSections: 0,
14225
+ warnings: ["No managed sections found in existing content, wrapping template entirely"]
14226
+ };
14227
+ }
14228
+ const mergedSections = [];
14229
+ let managedCount = 0;
14230
+ let customCount = 0;
14231
+ let templateInserted = false;
14232
+ for (const section of sections) {
14233
+ if (section.type === "managed") {
14234
+ if (!templateInserted) {
14235
+ mergedSections.push(MANAGED_START);
14236
+ mergedSections.push(templateContent);
14237
+ mergedSections.push(MANAGED_END);
14238
+ templateInserted = true;
14239
+ managedCount++;
14240
+ } else {
14241
+ warnings.push("Multiple managed sections found, keeping only the first one");
14242
+ }
14243
+ } else {
14244
+ mergedSections.push(section.content);
14245
+ customCount++;
14246
+ }
14247
+ }
13803
14248
  return {
13804
- version: "0.0.0",
13805
- lastUpdated: new Date().toISOString(),
13806
- source: "https://github.com/baekenough/oh-my-customcode"
14249
+ content: mergedSections.join(`
14250
+ `),
14251
+ managedSections: managedCount,
14252
+ customSections: customCount,
14253
+ warnings
13807
14254
  };
13808
14255
  }
13809
- async function checkUpdateAvailable() {
13810
- const current = await getCurrentVersion();
13811
- const latest = await getLatestVersion();
13812
- if (!current) {
14256
+ function wrapInManagedMarkers(content) {
14257
+ return `${MANAGED_START}
14258
+ ${content}
14259
+ ${MANAGED_END}`;
14260
+ }
14261
+
14262
+ // src/core/updater.ts
14263
+ var CUSTOMIZATION_MANIFEST_FILE = ".omcustom-customizations.json";
14264
+ function createUpdateResult() {
14265
+ return {
14266
+ success: false,
14267
+ updatedComponents: [],
14268
+ skippedComponents: [],
14269
+ preservedFiles: [],
14270
+ backedUpPaths: [],
14271
+ previousVersion: "",
14272
+ newVersion: "",
14273
+ warnings: []
14274
+ };
14275
+ }
14276
+ async function handleBackupIfRequested(targetDir, provider, backup, result) {
14277
+ if (!backup)
14278
+ return;
14279
+ const backupPath = await backupInstallation(targetDir, provider);
14280
+ result.backedUpPaths.push(backupPath);
14281
+ info("update.backup_created", { path: backupPath });
14282
+ }
14283
+ async function processComponentUpdate(targetDir, provider, component, updateCheck, customizations, options, result) {
14284
+ const componentUpdate = updateCheck.updatableComponents.find((c) => c.name === component);
14285
+ if (!componentUpdate && !options.force) {
14286
+ result.skippedComponents.push(component);
14287
+ return;
14288
+ }
14289
+ if (options.dryRun) {
14290
+ debug("update.dry_run", { component });
14291
+ result.updatedComponents.push(component);
14292
+ return;
14293
+ }
14294
+ try {
14295
+ const preserved = await updateComponent(targetDir, provider, component, customizations, options);
14296
+ result.updatedComponents.push(component);
14297
+ result.preservedFiles.push(...preserved);
14298
+ } catch (err) {
14299
+ const message = err instanceof Error ? err.message : String(err);
14300
+ result.warnings.push(`Failed to update ${component}: ${message}`);
14301
+ result.skippedComponents.push(component);
14302
+ }
14303
+ }
14304
+ async function updateAllComponents(targetDir, provider, components, updateCheck, customizations, options, result) {
14305
+ for (const component of components) {
14306
+ await processComponentUpdate(targetDir, provider, component, updateCheck, customizations, options, result);
14307
+ }
14308
+ }
14309
+ function getEntryTemplateName2(provider, language) {
14310
+ const layout = getProviderLayout(provider);
14311
+ const baseName = layout.entryFile.replace(".md", "");
14312
+ return language === "ko" ? `${baseName}.md.ko` : `${baseName}.md.en`;
14313
+ }
14314
+ async function backupFile(filePath) {
14315
+ const fs2 = await import("node:fs/promises");
14316
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
14317
+ const backupPath = `${filePath}.backup-${timestamp}`;
14318
+ if (await fileExists(filePath)) {
14319
+ await fs2.copyFile(filePath, backupPath);
14320
+ debug("update.file_backed_up", { path: filePath, backup: backupPath });
14321
+ }
14322
+ }
14323
+ function resolveCustomizations(customizations, configPreserveFiles) {
14324
+ if (!customizations && configPreserveFiles.length === 0) {
14325
+ return null;
14326
+ }
14327
+ if (customizations && configPreserveFiles.length > 0) {
14328
+ customizations.preserveFiles = [
14329
+ ...new Set([...customizations.preserveFiles, ...configPreserveFiles])
14330
+ ];
14331
+ return customizations;
14332
+ }
14333
+ if (configPreserveFiles.length > 0) {
14334
+ return {
14335
+ modifiedFiles: [],
14336
+ preserveFiles: configPreserveFiles,
14337
+ customComponents: [],
14338
+ lastUpdated: new Date().toISOString()
14339
+ };
14340
+ }
14341
+ return customizations;
14342
+ }
14343
+ async function updateEntryDoc(targetDir, provider, config, options) {
14344
+ const layout = getProviderLayout(provider);
14345
+ const entryPath = join7(targetDir, layout.entryFile);
14346
+ const templateName = getEntryTemplateName2(provider, config.language);
14347
+ const templatePath = resolveTemplatePath(templateName);
14348
+ if (!await fileExists(templatePath)) {
14349
+ warn("update.entry_template_not_found", { template: templateName });
14350
+ return;
14351
+ }
14352
+ const templateContent = await readTextFile(templatePath);
14353
+ if (await fileExists(entryPath)) {
14354
+ if (options.force) {
14355
+ await backupFile(entryPath);
14356
+ await writeTextFile(entryPath, templateContent);
14357
+ info("update.entry_doc_force_updated", { path: layout.entryFile });
14358
+ } else {
14359
+ const existingContent = await readTextFile(entryPath);
14360
+ const mergeResult = mergeEntryDoc(existingContent, templateContent);
14361
+ await writeTextFile(entryPath, mergeResult.content);
14362
+ debug("update.entry_doc_merged", {
14363
+ path: layout.entryFile,
14364
+ managed: String(mergeResult.managedSections),
14365
+ custom: String(mergeResult.customSections)
14366
+ });
14367
+ if (mergeResult.warnings.length > 0) {
14368
+ for (const warning of mergeResult.warnings) {
14369
+ warn("update.entry_merge_warning", { warning });
14370
+ }
14371
+ }
14372
+ }
14373
+ } else {
14374
+ await writeTextFile(entryPath, wrapInManagedMarkers(templateContent));
14375
+ info("update.entry_doc_created", { path: layout.entryFile });
14376
+ }
14377
+ }
14378
+ async function update(options) {
14379
+ const result = createUpdateResult();
14380
+ try {
14381
+ info("update.start", { targetDir: options.targetDir });
14382
+ const config = await loadConfig(options.targetDir);
14383
+ const provider = options.provider ?? (config.provider === "codex" ? "codex" : "claude");
14384
+ result.previousVersion = config.version;
14385
+ const updateCheck = await checkForUpdates(options.targetDir, provider);
14386
+ result.newVersion = updateCheck.latestVersion;
14387
+ if (!updateCheck.hasUpdates && !options.force) {
14388
+ info("update.no_updates");
14389
+ result.success = true;
14390
+ result.skippedComponents = options.components || getAllUpdateComponents();
14391
+ return result;
14392
+ }
14393
+ await handleBackupIfRequested(options.targetDir, provider, !!options.backup, result);
14394
+ const manifestCustomizations = options.preserveCustomizations !== false ? await loadCustomizationManifest(options.targetDir) : null;
14395
+ const configPreserveFiles = config.preserveFiles || [];
14396
+ const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles);
14397
+ const components = options.components || getAllUpdateComponents();
14398
+ await updateAllComponents(options.targetDir, provider, components, updateCheck, customizations, options, result);
14399
+ if (!options.components || options.components.length === 0) {
14400
+ await updateEntryDoc(options.targetDir, provider, config, options);
14401
+ }
14402
+ config.version = result.newVersion;
14403
+ config.lastUpdated = new Date().toISOString();
14404
+ await saveConfig(options.targetDir, config);
14405
+ result.success = true;
14406
+ success("update.success", { from: result.previousVersion, to: result.newVersion });
14407
+ } catch (err) {
14408
+ const message = err instanceof Error ? err.message : String(err);
14409
+ result.error = message;
14410
+ error("update.failed", { error: message });
14411
+ }
14412
+ return result;
14413
+ }
14414
+ async function checkForUpdates(targetDir, provider = "claude") {
14415
+ const config = await loadConfig(targetDir);
14416
+ const currentVersion = config.version;
14417
+ const latestVersion = await getLatestVersion(provider);
14418
+ const updatableComponents = [];
14419
+ for (const component of getAllUpdateComponents()) {
14420
+ const hasUpdate = await componentHasUpdate(targetDir, provider, component, config);
14421
+ if (hasUpdate) {
14422
+ updatableComponents.push({
14423
+ name: component,
14424
+ currentVersion: config.componentVersions?.[component] || "0.0.0",
14425
+ latestVersion
14426
+ });
14427
+ }
14428
+ }
14429
+ return {
14430
+ hasUpdates: updatableComponents.length > 0 || currentVersion !== latestVersion,
14431
+ currentVersion,
14432
+ latestVersion,
14433
+ updatableComponents,
14434
+ checkedAt: new Date().toISOString()
14435
+ };
14436
+ }
14437
+ function getAllUpdateComponents() {
14438
+ return ["rules", "agents", "skills", "guides", "hooks", "contexts"];
14439
+ }
14440
+ async function getLatestVersion(provider) {
14441
+ const layout = getProviderLayout(provider);
14442
+ const manifestPath = resolveTemplatePath(layout.manifestFile);
14443
+ if (await fileExists(manifestPath)) {
14444
+ const manifest = await readJsonFile(manifestPath);
14445
+ return manifest.version;
14446
+ }
14447
+ return "0.0.0";
14448
+ }
14449
+ async function componentHasUpdate(_targetDir, provider, component, config) {
14450
+ const installedVersion = config.componentVersions?.[component];
14451
+ if (!installedVersion) {
13813
14452
  return true;
13814
14453
  }
13815
- return current.version !== latest.version;
14454
+ const latestVersion = await getLatestVersion(provider);
14455
+ return installedVersion !== latestVersion;
14456
+ }
14457
+ async function updateComponent(targetDir, provider, component, customizations, options) {
14458
+ const preservedFiles = [];
14459
+ const componentPath = getComponentPath2(provider, component);
14460
+ const srcPath = resolveTemplatePath(componentPath);
14461
+ const destPath = join7(targetDir, componentPath);
14462
+ const config = await loadConfig(targetDir);
14463
+ const customComponents = config.customComponents || [];
14464
+ const skipPaths = [];
14465
+ if (customizations && options.preserveCustomizations !== false) {
14466
+ const toPreserve = customizations.preserveFiles.filter((f) => f.startsWith(componentPath));
14467
+ preservedFiles.push(...toPreserve);
14468
+ skipPaths.push(...toPreserve);
14469
+ }
14470
+ for (const cc of customComponents) {
14471
+ if (cc.path.startsWith(componentPath)) {
14472
+ skipPaths.push(cc.path);
14473
+ }
14474
+ }
14475
+ const path2 = await import("node:path");
14476
+ const normalizedSkipPaths = skipPaths.map((p) => path2.relative(destPath, join7(targetDir, p)));
14477
+ await copyDirectory(srcPath, destPath, {
14478
+ overwrite: true,
14479
+ skipPaths: normalizedSkipPaths.length > 0 ? normalizedSkipPaths : undefined
14480
+ });
14481
+ debug("update.component_updated", {
14482
+ component,
14483
+ skippedPaths: String(normalizedSkipPaths.length)
14484
+ });
14485
+ return preservedFiles;
14486
+ }
14487
+ function getComponentPath2(provider, component) {
14488
+ const layout = getProviderLayout(provider);
14489
+ if (component === "guides") {
14490
+ return "guides";
14491
+ }
14492
+ return `${layout.rootDir}/${component}`;
14493
+ }
14494
+ async function backupInstallation(targetDir, provider) {
14495
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
14496
+ const backupDir = join7(targetDir, `.omcustom-backup-${timestamp}`);
14497
+ const fs2 = await import("node:fs/promises");
14498
+ await ensureDirectory(backupDir);
14499
+ const layout = getProviderLayout(provider);
14500
+ const dirsToBackup = [layout.rootDir, "guides"];
14501
+ for (const dir2 of dirsToBackup) {
14502
+ const srcPath = join7(targetDir, dir2);
14503
+ if (await fileExists(srcPath)) {
14504
+ const destPath = join7(backupDir, dir2);
14505
+ await copyDirectory(srcPath, destPath, { overwrite: true });
14506
+ }
14507
+ }
14508
+ const entryPath = join7(targetDir, layout.entryFile);
14509
+ if (await fileExists(entryPath)) {
14510
+ await fs2.copyFile(entryPath, join7(backupDir, layout.entryFile));
14511
+ }
14512
+ return backupDir;
13816
14513
  }
13817
- async function applyUpdates(_options) {
13818
- const updatedComponents = [];
13819
- return updatedComponents;
14514
+ async function loadCustomizationManifest(targetDir) {
14515
+ const manifestPath = join7(targetDir, CUSTOMIZATION_MANIFEST_FILE);
14516
+ if (await fileExists(manifestPath)) {
14517
+ return readJsonFile(manifestPath);
14518
+ }
14519
+ return null;
13820
14520
  }
14521
+
14522
+ // src/cli/update.ts
13821
14523
  async function updateCommand(options = {}) {
13822
- console.log(i18n.t("cli.update.checking"));
13823
14524
  try {
13824
- const currentVersion = await getCurrentVersion();
13825
- const latestVersion = await getLatestVersion();
13826
- if (!currentVersion) {
13827
- console.log(i18n.t("cli.update.notInstalled"));
13828
- return {
13829
- success: false,
13830
- message: i18n.t("cli.update.notInstalled"),
13831
- errors: [i18n.t("cli.update.runInitFirst")]
13832
- };
13833
- }
13834
- const updateAvailable = await checkUpdateAvailable();
13835
- if (!updateAvailable && !options.force) {
13836
- console.log(i18n.t("cli.update.alreadyLatest"));
13837
- return {
13838
- success: true,
13839
- message: i18n.t("cli.update.alreadyLatest"),
13840
- currentVersion: currentVersion.version,
13841
- newVersion: latestVersion.version
13842
- };
14525
+ const targetDir = process.cwd();
14526
+ const detection = await detectProvider({
14527
+ targetDir,
14528
+ override: options.provider
14529
+ });
14530
+ const provider = detection.provider;
14531
+ const components = buildComponentsList(options);
14532
+ if (options.dryRun) {
14533
+ console.log(i18n.t("cli.update.dryRunHeader"));
13843
14534
  }
13844
- console.log(i18n.t("cli.update.updating", {
13845
- from: currentVersion.version,
13846
- to: latestVersion.version
13847
- }));
13848
- const updatedComponents = await applyUpdates(options);
13849
- console.log(i18n.t("cli.update.success"));
13850
- return {
13851
- success: true,
13852
- message: i18n.t("cli.update.success"),
13853
- updatedComponents,
13854
- currentVersion: currentVersion.version,
13855
- newVersion: latestVersion.version
14535
+ const updateOptions = {
14536
+ targetDir,
14537
+ provider,
14538
+ components,
14539
+ force: options.force,
14540
+ preserveCustomizations: true,
14541
+ dryRun: options.dryRun,
14542
+ backup: options.backup
13856
14543
  };
14544
+ const result = await update(updateOptions);
14545
+ printUpdateResults(result);
14546
+ if (!result.success) {
14547
+ process.exit(1);
14548
+ }
13857
14549
  } catch (error2) {
13858
14550
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
13859
- console.error(i18n.t("cli.update.failed"), errorMessage);
13860
- return {
13861
- success: false,
13862
- message: i18n.t("cli.update.failed"),
13863
- errors: [errorMessage]
13864
- };
14551
+ console.error(i18n.t("cli.update.summaryFailed", { error: errorMessage }));
14552
+ process.exit(1);
14553
+ }
14554
+ }
14555
+ function buildComponentsList(options) {
14556
+ const components = [];
14557
+ if (options.agents) {
14558
+ components.push("agents");
14559
+ }
14560
+ if (options.skills) {
14561
+ components.push("skills");
14562
+ }
14563
+ if (options.rules) {
14564
+ components.push("rules");
14565
+ }
14566
+ if (options.guides) {
14567
+ components.push("guides");
14568
+ }
14569
+ if (options.hooks) {
14570
+ components.push("hooks");
14571
+ }
14572
+ if (options.contexts) {
14573
+ components.push("contexts");
14574
+ }
14575
+ return components.length > 0 ? components : undefined;
14576
+ }
14577
+ function printUpdateResults(result) {
14578
+ for (const component of result.updatedComponents) {
14579
+ console.log(i18n.t("cli.update.componentUpdated", { component }));
14580
+ }
14581
+ for (const component of result.skippedComponents) {
14582
+ console.log(i18n.t("cli.update.componentSkipped", { component }));
14583
+ }
14584
+ if (result.preservedFiles.length > 0) {
14585
+ console.log(i18n.t("cli.update.preservedFiles", { count: result.preservedFiles.length }));
14586
+ }
14587
+ if (result.backedUpPaths.length > 0) {
14588
+ for (const path2 of result.backedUpPaths) {
14589
+ console.log(i18n.t("cli.update.backupCreated", { path: path2 }));
14590
+ }
14591
+ }
14592
+ for (const warning of result.warnings) {
14593
+ console.warn(warning);
14594
+ }
14595
+ if (result.success) {
14596
+ console.log(i18n.t("cli.update.summary", {
14597
+ updated: result.updatedComponents.length,
14598
+ skipped: result.skippedComponents.length
14599
+ }));
14600
+ } else if (result.error) {
14601
+ console.error(i18n.t("cli.update.summaryFailed", { error: result.error }));
13865
14602
  }
13866
14603
  }
13867
14604
 
@@ -13870,12 +14607,12 @@ var require2 = createRequire2(import.meta.url);
13870
14607
  var packageJson = require2("../../package.json");
13871
14608
  function createProgram() {
13872
14609
  const program2 = new Command;
13873
- program2.name("omcustom").description(i18n.t("cli.description")).version(packageJson.version, "-v, --version", i18n.t("cli.versionOption"));
14610
+ program2.name("omcustom").description(i18n.t("cli.description")).version(packageJson.version, "-v, --version", i18n.t("cli.versionOption")).option("--skip-version-check", "Skip CLI version pre-flight check");
13874
14611
  program2.command("init").description(i18n.t("cli.init.description")).option("-l, --lang <language>", i18n.t("cli.init.langOption"), "en").option("-p, --provider <provider>", i18n.t("cli.init.providerOption"), "auto").action(async (options) => {
13875
14612
  await initCommand(options);
13876
14613
  });
13877
- program2.command("update").description(i18n.t("cli.update.description")).action(async () => {
13878
- await updateCommand();
14614
+ program2.command("update").description(i18n.t("cli.update.description")).option("--dry-run", i18n.t("cli.update.dryRunOption")).option("--force", i18n.t("cli.update.forceOption")).option("--backup", i18n.t("cli.update.backupOption")).option("--agents", i18n.t("cli.update.agentsOption")).option("--skills", i18n.t("cli.update.skillsOption")).option("--rules", i18n.t("cli.update.rulesOption")).option("--guides", i18n.t("cli.update.guidesOption")).option("--hooks", i18n.t("cli.update.hooksOption")).option("--contexts", i18n.t("cli.update.contextsOption")).option("-p, --provider <provider>", i18n.t("cli.update.providerOption"), "auto").action(async (options) => {
14615
+ await updateCommand(options);
13879
14616
  });
13880
14617
  program2.command("list").description(i18n.t("cli.list.description")).argument("[type]", i18n.t("cli.list.typeArgument"), "all").option("-f, --format <format>", "Output format: table, json, or simple", "table").option("--verbose", "Show detailed information").option("-p, --provider <provider>", i18n.t("cli.list.providerOption"), "auto").action(async (type, options) => {
13881
14618
  await listCommand(type, {
@@ -13887,6 +14624,16 @@ function createProgram() {
13887
14624
  program2.command("doctor").description(i18n.t("cli.doctor.description")).option("--fix", i18n.t("cli.doctor.fixOption")).option("-p, --provider <provider>", i18n.t("cli.doctor.providerOption"), "auto").action(async (options) => {
13888
14625
  await doctorCommand(options);
13889
14626
  });
14627
+ program2.hook("preAction", async (thisCommand) => {
14628
+ const opts = thisCommand.optsWithGlobals();
14629
+ const skipCheck = opts.skipVersionCheck || false;
14630
+ const result = await runPreflightCheck({ skip: skipCheck });
14631
+ if (result.hasUpdates) {
14632
+ const warnings = formatPreflightWarnings(result);
14633
+ console.warn(warnings);
14634
+ console.warn("");
14635
+ }
14636
+ });
13890
14637
  return program2;
13891
14638
  }
13892
14639
  async function main() {