oh-my-customcode 1.1.106 → 2.0.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 +8 -9
- package/dist/cli/index.js +188 -178
- package/dist/index.js +181 -125
- package/package.json +1 -1
- package/templates/.claude/hooks/hooks.json +0 -10
- package/templates/.claude/hooks/scripts/cost-cap-advisor.sh +107 -52
- package/templates/.claude/hooks/scripts/session-env-check.sh +0 -8
- package/templates/.claude/rules/MUST-agent-design.md +1 -1
- package/templates/.claude/rules/SHOULD-memory-integration.md +1 -1
- package/templates/.claude/settings.json +0 -10
- package/templates/.claude/skills/intent-detection/patterns/agent-triggers.yaml +0 -8
- package/templates/CLAUDE.md +1 -1
- package/templates/CLAUDE.md.en +1 -1
- package/templates/CLAUDE.md.ko +1 -1
- package/templates/README.md +4 -4
- package/templates/guides/external-tools/ecc-absorption-decisions.md +2 -0
- package/templates/guides/multi-provider-exec/README.md +7 -18
- package/templates/manifest.json +3 -3
- package/templates/.claude/hooks/scripts/rtk-intercept.sh +0 -77
- package/templates/.claude/skills/rtk-exec/SKILL.md +0 -199
- package/templates/.claude/skills/rtk-exec/scripts/rtk-wrapper.cjs +0 -377
package/dist/index.js
CHANGED
|
@@ -430,18 +430,10 @@ var MESSAGES = {
|
|
|
430
430
|
"codex.installing_npm": "Installing Codex CLI via npm...",
|
|
431
431
|
"codex.install_failed": "Failed to install Codex CLI: {{error}}",
|
|
432
432
|
"codex.unsupported_os": "Codex CLI auto-install is not supported on {{os}}",
|
|
433
|
-
"rtk.already_installed": "RTK is already installed",
|
|
434
|
-
"rtk.installing_brew": "Installing RTK via Homebrew...",
|
|
435
|
-
"rtk.installing_curl": "Installing RTK via install script...",
|
|
436
|
-
"rtk.install_failed": "Failed to install RTK: {{error}}",
|
|
437
|
-
"rtk.unsupported_os": "RTK auto-install is not supported on {{os}}",
|
|
438
433
|
"mcp.ontology_rag_configured": "ontology-rag MCP server configured successfully",
|
|
439
434
|
"install.codex_installing": "Installing Codex CLI...",
|
|
440
435
|
"install.codex_success": "Codex CLI installed successfully",
|
|
441
436
|
"install.codex_already": "Codex CLI is already installed",
|
|
442
|
-
"install.rtk_installing": "Installing RTK...",
|
|
443
|
-
"install.rtk_success": "RTK installed successfully",
|
|
444
|
-
"install.rtk_already": "RTK is already installed",
|
|
445
437
|
"install.preserved": "Preserved {{files}} file(s) and {{dirs}} directory(ies) from the existing installation",
|
|
446
438
|
"install.restored": "Restored {{files}} file(s) and {{dirs}} directory(ies) after installation",
|
|
447
439
|
"install.backup_failed": "Failed to back up {{path}}: {{error}}",
|
|
@@ -453,7 +445,6 @@ var MESSAGES = {
|
|
|
453
445
|
"preserve.restore_failed": "Failed to restore file {{file}}: {{error}}",
|
|
454
446
|
"preserve_files.invalid_path": "Ignored invalid preserve_files entry from {{source}}: {{path}} ({{reason}})",
|
|
455
447
|
"update.codex_missing": "Codex CLI is not installed",
|
|
456
|
-
"update.rtk_missing": "RTK is not installed",
|
|
457
448
|
"update.self_update_skipped": "Skipped update: source project cannot update itself",
|
|
458
449
|
"update.deprecated_file_invalid_path": "Skipped deprecated file with invalid path: {{path}} ({{reason}})",
|
|
459
450
|
"update.deprecated_file_removed": "Removed deprecated file: {{path}} ({{reason}})",
|
|
@@ -552,18 +543,10 @@ var MESSAGES = {
|
|
|
552
543
|
"codex.installing_npm": "npm으로 Codex CLI를 설치합니다...",
|
|
553
544
|
"codex.install_failed": "Codex CLI 설치에 실패했습니다: {{error}}",
|
|
554
545
|
"codex.unsupported_os": "{{os}}에서는 Codex CLI 자동 설치를 지원하지 않습니다",
|
|
555
|
-
"rtk.already_installed": "RTK가 이미 설치되어 있습니다",
|
|
556
|
-
"rtk.installing_brew": "Homebrew로 RTK를 설치합니다...",
|
|
557
|
-
"rtk.installing_curl": "설치 스크립트로 RTK를 설치합니다...",
|
|
558
|
-
"rtk.install_failed": "RTK 설치에 실패했습니다: {{error}}",
|
|
559
|
-
"rtk.unsupported_os": "{{os}}에서는 RTK 자동 설치를 지원하지 않습니다",
|
|
560
546
|
"mcp.ontology_rag_configured": "ontology-rag MCP 서버 구성을 완료했습니다",
|
|
561
547
|
"install.codex_installing": "Codex CLI를 설치합니다...",
|
|
562
548
|
"install.codex_success": "Codex CLI를 설치했습니다",
|
|
563
549
|
"install.codex_already": "Codex CLI가 이미 설치되어 있습니다",
|
|
564
|
-
"install.rtk_installing": "RTK를 설치합니다...",
|
|
565
|
-
"install.rtk_success": "RTK를 설치했습니다",
|
|
566
|
-
"install.rtk_already": "RTK가 이미 설치되어 있습니다",
|
|
567
550
|
"install.preserved": "기존 설치에서 파일 {{files}}개와 디렉토리 {{dirs}}개를 보존했습니다",
|
|
568
551
|
"install.restored": "설치 후 파일 {{files}}개와 디렉토리 {{dirs}}개를 복원했습니다",
|
|
569
552
|
"install.backup_failed": "{{path}} 백업에 실패했습니다: {{error}}",
|
|
@@ -575,7 +558,6 @@ var MESSAGES = {
|
|
|
575
558
|
"preserve.restore_failed": "파일 {{file}} 복원에 실패했습니다: {{error}}",
|
|
576
559
|
"preserve_files.invalid_path": "{{source}}의 유효하지 않은 preserve_files 항목을 무시했습니다: {{path}} ({{reason}})",
|
|
577
560
|
"update.codex_missing": "Codex CLI가 설치되어 있지 않습니다",
|
|
578
|
-
"update.rtk_missing": "RTK가 설치되어 있지 않습니다",
|
|
579
561
|
"update.self_update_skipped": "업데이트를 건너뜁니다: 소스 프로젝트는 스스로 업데이트할 수 없습니다",
|
|
580
562
|
"update.deprecated_file_invalid_path": "경로가 유효하지 않아 지원 중단 파일을 건너뜁니다: {{path}} ({{reason}})",
|
|
581
563
|
"update.deprecated_file_removed": "지원 중단 파일을 삭제했습니다: {{path}} ({{reason}})",
|
|
@@ -1173,6 +1155,7 @@ var LEGACY_OMCUSTOM_DESCRIPTIONS = [
|
|
|
1173
1155
|
];
|
|
1174
1156
|
var STANDALONE_SCRIPT_COMMAND = /^(?:bash )?(?:"(?:\$\{CLAUDE_PROJECT_DIR:-\.\}\/|\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)"|(?:\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh))$/;
|
|
1175
1157
|
var SCRIPT_REFERENCE = /\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)/g;
|
|
1158
|
+
var RETIRED_RTK_DESCRIPTION = "RTK auto-intercept — transparently rewrites CLI commands through RTK proxy when available (R013 advisory)";
|
|
1176
1159
|
function matcherKey(group) {
|
|
1177
1160
|
return typeof group.matcher === "string" ? group.matcher : "";
|
|
1178
1161
|
}
|
|
@@ -1234,15 +1217,52 @@ function isOwnedCommand(hook, owned) {
|
|
|
1234
1217
|
const script = match?.[1] ?? match?.[2];
|
|
1235
1218
|
return script !== undefined && owned.scripts.has(script);
|
|
1236
1219
|
}
|
|
1237
|
-
function
|
|
1220
|
+
function isRetiredRtkCommand(hook, event, matcher) {
|
|
1221
|
+
if (event !== "PreToolUse" || matcher !== "Bash" || !isRecord(hook) || hook.type !== "command" || typeof hook.command !== "string") {
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
const match = STANDALONE_SCRIPT_COMMAND.exec(hook.command);
|
|
1225
|
+
return (match?.[1] ?? match?.[2]) === "scripts/rtk-intercept.sh";
|
|
1226
|
+
}
|
|
1227
|
+
function retireRtkGroup(group, event) {
|
|
1228
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) {
|
|
1229
|
+
return [group];
|
|
1230
|
+
}
|
|
1231
|
+
const remaining = group.hooks.filter((hook) => !isRetiredRtkCommand(hook, event, matcherKey(group)));
|
|
1232
|
+
if (remaining.length === group.hooks.length) {
|
|
1233
|
+
return [group];
|
|
1234
|
+
}
|
|
1235
|
+
return remaining.length === 0 ? [] : [{ ...group, hooks: remaining }];
|
|
1236
|
+
}
|
|
1237
|
+
function retireRtkHookBlocks(existing) {
|
|
1238
|
+
if (!isRecord(existing)) {
|
|
1239
|
+
return existing;
|
|
1240
|
+
}
|
|
1241
|
+
const updates = {};
|
|
1242
|
+
for (const [event, value] of Object.entries(existing)) {
|
|
1243
|
+
if (!Array.isArray(value)) {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
const residual = value.flatMap((group) => retireRtkGroup(group, event));
|
|
1247
|
+
if (residual.length !== value.length || residual.some((group, index) => group !== value[index])) {
|
|
1248
|
+
updates[event] = residual;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return Object.keys(updates).length === 0 ? existing : { ...existing, ...updates };
|
|
1252
|
+
}
|
|
1253
|
+
function userResidual(group, event, owned, ownership) {
|
|
1238
1254
|
if (!isRecord(group) || !Array.isArray(group.hooks)) {
|
|
1239
1255
|
return [group];
|
|
1240
1256
|
}
|
|
1257
|
+
const matcher = matcherKey(group);
|
|
1258
|
+
if (group.description === RETIRED_RTK_DESCRIPTION) {
|
|
1259
|
+
return retireRtkGroup(group, event);
|
|
1260
|
+
}
|
|
1241
1261
|
if (typeof group.description === "string" && ownership.descriptions.has(group.description)) {
|
|
1242
1262
|
return [];
|
|
1243
1263
|
}
|
|
1244
|
-
const ownedForMatcher = owned?.get(
|
|
1245
|
-
const remaining = group.hooks.filter((hook) => !isOwnedCommand(hook, ownedForMatcher));
|
|
1264
|
+
const ownedForMatcher = owned?.get(matcher);
|
|
1265
|
+
const remaining = group.hooks.filter((hook) => !isRetiredRtkCommand(hook, event, matcher) && !isOwnedCommand(hook, ownedForMatcher));
|
|
1246
1266
|
if (remaining.length === group.hooks.length) {
|
|
1247
1267
|
return [group];
|
|
1248
1268
|
}
|
|
@@ -1265,7 +1285,7 @@ function mergeHookBlocks(existing, generated) {
|
|
|
1265
1285
|
continue;
|
|
1266
1286
|
}
|
|
1267
1287
|
const owned = ownership.events.get(event);
|
|
1268
|
-
const residual = value.flatMap((group) => userResidual(group, owned, ownership));
|
|
1288
|
+
const residual = value.flatMap((group) => userResidual(group, event, owned, ownership));
|
|
1269
1289
|
if (residual.length === 0) {
|
|
1270
1290
|
continue;
|
|
1271
1291
|
}
|
|
@@ -2221,8 +2241,23 @@ function sortFilesByPath(files) {
|
|
|
2221
2241
|
const sorted = Object.keys(files).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
2222
2242
|
return Object.fromEntries(sorted.map((key) => [key, files[key]]));
|
|
2223
2243
|
}
|
|
2244
|
+
var RETIRED_RTK_PATHS = [
|
|
2245
|
+
".claude/skills/rtk-exec/SKILL.md",
|
|
2246
|
+
".claude/skills/rtk-exec/scripts/rtk-wrapper.cjs",
|
|
2247
|
+
".claude/hooks/scripts/rtk-intercept.sh"
|
|
2248
|
+
];
|
|
2249
|
+
function getRetiredPaths(options) {
|
|
2250
|
+
const retiredPaths = new Set([...RETIRED_RTK_PATHS, ...options.retiredPaths ?? []]);
|
|
2251
|
+
for (const path of retiredPaths) {
|
|
2252
|
+
if (typeof path !== "string" || /[\\:*?[\]{}\0]/.test(path) || path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
2253
|
+
throw new Error("retiredPaths must contain exact canonical relative file paths");
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return retiredPaths;
|
|
2257
|
+
}
|
|
2224
2258
|
async function generateLockfile(targetDir, generatorVersion, templateVersion, options = {}) {
|
|
2225
2259
|
const files = {};
|
|
2260
|
+
const retiredPaths = getRetiredPaths(options);
|
|
2226
2261
|
const gitVisible = options.excludeGitIgnored ? await listGitVisiblePaths(targetDir) : null;
|
|
2227
2262
|
const componentRoots = COMPONENT_PATHS.map(([prefix]) => join4(targetDir, prefix));
|
|
2228
2263
|
for (const componentRoot of componentRoots) {
|
|
@@ -2231,7 +2266,7 @@ async function generateLockfile(targetDir, generatorVersion, templateVersion, op
|
|
|
2231
2266
|
debug("lockfile.component_dir_missing", { path: componentRoot });
|
|
2232
2267
|
continue;
|
|
2233
2268
|
}
|
|
2234
|
-
const allFiles = await collectFiles(componentRoot, targetDir, false);
|
|
2269
|
+
const allFiles = (await collectFiles(componentRoot, targetDir, false)).filter((absolutePath) => !retiredPaths.has(relative2(targetDir, absolutePath).replace(/\\/g, "/")));
|
|
2235
2270
|
for (const absolutePath of allFiles) {
|
|
2236
2271
|
const relativePath = relative2(targetDir, absolutePath).replace(/\\/g, "/");
|
|
2237
2272
|
if (gitVisible !== null && !isGitVisible(gitVisible, relativePath)) {
|
|
@@ -2314,65 +2349,6 @@ async function generateAndWriteLockfileForDir(targetDir, options = {}) {
|
|
|
2314
2349
|
}
|
|
2315
2350
|
}
|
|
2316
2351
|
|
|
2317
|
-
// src/core/rtk-installer.ts
|
|
2318
|
-
import { execSync as execSync2 } from "node:child_process";
|
|
2319
|
-
import { platform as platform2 } from "node:os";
|
|
2320
|
-
var defaultDeps2 = {
|
|
2321
|
-
exec: execSync2,
|
|
2322
|
-
getPlatform: platform2
|
|
2323
|
-
};
|
|
2324
|
-
function isRtkInstalled(deps = defaultDeps2) {
|
|
2325
|
-
try {
|
|
2326
|
-
deps.exec("which rtk", { stdio: "pipe", timeout: 3000 });
|
|
2327
|
-
return true;
|
|
2328
|
-
} catch {
|
|
2329
|
-
return false;
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
function installRtk(deps = defaultDeps2) {
|
|
2333
|
-
if (process.env.CI || false || false) {
|
|
2334
|
-
return false;
|
|
2335
|
-
}
|
|
2336
|
-
if (isRtkInstalled(deps)) {
|
|
2337
|
-
info("rtk.already_installed");
|
|
2338
|
-
return true;
|
|
2339
|
-
}
|
|
2340
|
-
const os = deps.getPlatform();
|
|
2341
|
-
try {
|
|
2342
|
-
if (os === "darwin") {
|
|
2343
|
-
try {
|
|
2344
|
-
info("rtk.installing_brew");
|
|
2345
|
-
deps.exec("brew install rtk-ai/tap/rtk", {
|
|
2346
|
-
stdio: "inherit",
|
|
2347
|
-
timeout: 120000
|
|
2348
|
-
});
|
|
2349
|
-
return true;
|
|
2350
|
-
} catch {
|
|
2351
|
-
info("rtk.installing_curl");
|
|
2352
|
-
deps.exec("curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh", {
|
|
2353
|
-
stdio: "inherit",
|
|
2354
|
-
timeout: 120000
|
|
2355
|
-
});
|
|
2356
|
-
return isRtkInstalled(deps);
|
|
2357
|
-
}
|
|
2358
|
-
} else if (os === "linux") {
|
|
2359
|
-
info("rtk.installing_curl");
|
|
2360
|
-
deps.exec("curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh", {
|
|
2361
|
-
stdio: "inherit",
|
|
2362
|
-
timeout: 120000
|
|
2363
|
-
});
|
|
2364
|
-
return isRtkInstalled(deps);
|
|
2365
|
-
} else {
|
|
2366
|
-
warn("rtk.unsupported_os", { os });
|
|
2367
|
-
return false;
|
|
2368
|
-
}
|
|
2369
|
-
} catch (err) {
|
|
2370
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2371
|
-
warn("rtk.install_failed", { error: message });
|
|
2372
|
-
return false;
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
2352
|
// src/core/scope-filter.ts
|
|
2377
2353
|
function getSkillScope(content) {
|
|
2378
2354
|
const cleaned = content.replace(/^\uFEFF/, "");
|
|
@@ -2661,19 +2637,6 @@ async function updateInstallConfig(targetDir, options, installedComponents) {
|
|
|
2661
2637
|
config.installedComponents = installedComponents;
|
|
2662
2638
|
await saveConfig(targetDir, config);
|
|
2663
2639
|
}
|
|
2664
|
-
function installRtkIfNeeded(result) {
|
|
2665
|
-
if (!isRtkInstalled()) {
|
|
2666
|
-
info("install.rtk_installing");
|
|
2667
|
-
const rtkInstalled = installRtk();
|
|
2668
|
-
if (rtkInstalled) {
|
|
2669
|
-
info("install.rtk_success");
|
|
2670
|
-
} else {
|
|
2671
|
-
result.warnings.push("RTK installation failed — install manually: brew install rtk-ai/tap/rtk");
|
|
2672
|
-
}
|
|
2673
|
-
} else {
|
|
2674
|
-
info("install.rtk_already");
|
|
2675
|
-
}
|
|
2676
|
-
}
|
|
2677
2640
|
function installCodexIfNeeded(result) {
|
|
2678
2641
|
if (!isCodexInstalled()) {
|
|
2679
2642
|
info("install.codex_installing");
|
|
@@ -2728,7 +2691,6 @@ async function install(options) {
|
|
|
2728
2691
|
} else {
|
|
2729
2692
|
info("install.lockfile_generated", { files: String(lockfileResult.fileCount) });
|
|
2730
2693
|
}
|
|
2731
|
-
installRtkIfNeeded(result);
|
|
2732
2694
|
installCodexIfNeeded(result);
|
|
2733
2695
|
result.success = true;
|
|
2734
2696
|
success("install.success");
|
|
@@ -2936,14 +2898,15 @@ async function detectProvider(_options = {}) {
|
|
|
2936
2898
|
};
|
|
2937
2899
|
}
|
|
2938
2900
|
// src/core/updater.ts
|
|
2939
|
-
import {
|
|
2901
|
+
import { lstat, realpath as realpath2, unlink } from "node:fs/promises";
|
|
2902
|
+
import { isAbsolute as isAbsolute3, join as join8, relative as relative3, resolve as resolve3, sep as sep4 } from "node:path";
|
|
2940
2903
|
// package.json
|
|
2941
2904
|
var package_default = {
|
|
2942
2905
|
name: "oh-my-customcode",
|
|
2943
2906
|
workspaces: [
|
|
2944
2907
|
"packages/*"
|
|
2945
2908
|
],
|
|
2946
|
-
version: "
|
|
2909
|
+
version: "2.0.0",
|
|
2947
2910
|
description: "Batteries-included agent harness for Claude Code",
|
|
2948
2911
|
type: "module",
|
|
2949
2912
|
bin: {
|
|
@@ -5589,6 +5552,86 @@ ${MANAGED_END}`;
|
|
|
5589
5552
|
|
|
5590
5553
|
// src/core/updater.ts
|
|
5591
5554
|
var CUSTOMIZATION_MANIFEST_FILE = ".omcustom-customizations.json";
|
|
5555
|
+
var RETIRED_RTK_FILES = [
|
|
5556
|
+
{ path: ".claude/skills/rtk-exec/SKILL.md", component: "skills" },
|
|
5557
|
+
{ path: ".claude/skills/rtk-exec/scripts/rtk-wrapper.cjs", component: "skills" },
|
|
5558
|
+
{ path: ".claude/hooks/scripts/rtk-intercept.sh", component: "hooks" }
|
|
5559
|
+
];
|
|
5560
|
+
function getRtkRetirement(result) {
|
|
5561
|
+
result.rtkRetirement ??= {
|
|
5562
|
+
removed: [],
|
|
5563
|
+
wouldRemove: [],
|
|
5564
|
+
preserved: [],
|
|
5565
|
+
settingsChanged: [],
|
|
5566
|
+
wouldChangeSettings: [],
|
|
5567
|
+
settingsConflicts: []
|
|
5568
|
+
};
|
|
5569
|
+
return result.rtkRetirement;
|
|
5570
|
+
}
|
|
5571
|
+
async function validateRtkTarget(targetDir, path) {
|
|
5572
|
+
const validation = validatePreserveFilePath(path, targetDir);
|
|
5573
|
+
if (!validation.valid)
|
|
5574
|
+
throw new Error("path escapes project");
|
|
5575
|
+
const fullPath = resolve3(targetDir, path);
|
|
5576
|
+
const fileStat = await lstat(fullPath);
|
|
5577
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink())
|
|
5578
|
+
throw new Error("not a regular owned file");
|
|
5579
|
+
const root = await realpath2(targetDir);
|
|
5580
|
+
const resolvedFile = await realpath2(fullPath);
|
|
5581
|
+
const within = relative3(root, resolvedFile);
|
|
5582
|
+
if (within === "" || within === ".." || within.startsWith(`..${sep4}`) || isAbsolute3(within)) {
|
|
5583
|
+
throw new Error("resolved path escapes project");
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
function isRtkPathPreserved(path, config, customizations) {
|
|
5587
|
+
const paths = [
|
|
5588
|
+
...config.preserveFiles ?? [],
|
|
5589
|
+
...customizations?.preserveFiles ?? [],
|
|
5590
|
+
...customizations?.modifiedFiles ?? [],
|
|
5591
|
+
...(config.customComponents ?? []).map((component) => component.path)
|
|
5592
|
+
];
|
|
5593
|
+
return paths.some((preserved) => path === preserved || path.startsWith(`${preserved}/`));
|
|
5594
|
+
}
|
|
5595
|
+
async function retireRtkFile(path, options, lockfile, result, preserved) {
|
|
5596
|
+
const retirement = getRtkRetirement(result);
|
|
5597
|
+
try {
|
|
5598
|
+
await validateRtkTarget(options.targetDir, path);
|
|
5599
|
+
if (preserved)
|
|
5600
|
+
throw new Error("explicitly preserved");
|
|
5601
|
+
if (retirement.settingsConflicts.length > 0)
|
|
5602
|
+
throw new Error("settings retirement conflict");
|
|
5603
|
+
const entry = lockfile?.files[path];
|
|
5604
|
+
if (!entry || typeof entry.templateHash !== "string" || !/^[a-f0-9]{64}$/.test(entry.templateHash)) {
|
|
5605
|
+
throw new Error("missing or invalid old ownership");
|
|
5606
|
+
}
|
|
5607
|
+
const fullPath = join8(options.targetDir, path);
|
|
5608
|
+
if (await computeFileHash(fullPath) !== entry.templateHash)
|
|
5609
|
+
throw new Error("user-modified file");
|
|
5610
|
+
if (options.dryRun) {
|
|
5611
|
+
retirement.wouldRemove.push(path);
|
|
5612
|
+
return;
|
|
5613
|
+
}
|
|
5614
|
+
await validateRtkTarget(options.targetDir, path);
|
|
5615
|
+
await unlink(fullPath);
|
|
5616
|
+
retirement.removed.push(path);
|
|
5617
|
+
result.removedDeprecatedFiles.push(path);
|
|
5618
|
+
} catch (err) {
|
|
5619
|
+
if (err.code === "ENOENT")
|
|
5620
|
+
return;
|
|
5621
|
+
const reason = err instanceof Error ? err.message : "unreadable ownership or file";
|
|
5622
|
+
retirement.preserved.push({ path, reason });
|
|
5623
|
+
result.preservedFiles.push(path);
|
|
5624
|
+
result.warnings.push(`RTK retirement preserved ${path}: ${reason}`);
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
async function retireRtkFiles(options, lockfile, result, config, customizations) {
|
|
5628
|
+
const fullUpdate = !options.components || options.components.length === 0;
|
|
5629
|
+
const files = RETIRED_RTK_FILES.filter((file) => fullUpdate || options.components?.includes(file.component));
|
|
5630
|
+
for (const file of files) {
|
|
5631
|
+
await retireRtkFile(file.path, options, lockfile, result, isRtkPathPreserved(file.path, config, customizations));
|
|
5632
|
+
}
|
|
5633
|
+
return RETIRED_RTK_FILES.map((file) => file.path);
|
|
5634
|
+
}
|
|
5592
5635
|
function createUpdateResult() {
|
|
5593
5636
|
return {
|
|
5594
5637
|
success: false,
|
|
@@ -5776,32 +5819,52 @@ async function backfillStatusLineRefreshInterval(targetDir, options) {
|
|
|
5776
5819
|
});
|
|
5777
5820
|
}
|
|
5778
5821
|
}
|
|
5779
|
-
async function migrateHookCommandsInSettingsLocal(targetDir, options) {
|
|
5780
|
-
|
|
5781
|
-
|
|
5822
|
+
async function migrateHookCommandsInSettingsLocal(targetDir, options, result) {
|
|
5823
|
+
for (const name of ["settings.json", "settings.local.json"]) {
|
|
5824
|
+
await migrateRtkSettingsFile(targetDir, options, result, name);
|
|
5782
5825
|
}
|
|
5826
|
+
}
|
|
5827
|
+
async function migrateRtkSettingsFile(targetDir, options, result, name) {
|
|
5783
5828
|
const layout = getProviderLayout();
|
|
5784
|
-
const
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
}
|
|
5829
|
+
const path = `${layout.rootDir}/${name}`;
|
|
5830
|
+
const settingsPath = join8(targetDir, path);
|
|
5831
|
+
const retirement = getRtkRetirement(result);
|
|
5788
5832
|
try {
|
|
5833
|
+
await validateRtkTarget(targetDir, path);
|
|
5789
5834
|
const raw = await readTextFile(settingsPath);
|
|
5790
5835
|
const bom = raw.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
5791
5836
|
const parsed = JSON.parse(bom ? raw.slice(bom.length) : raw);
|
|
5792
5837
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
5838
|
+
throw new Error("unknown settings shape");
|
|
5839
|
+
}
|
|
5840
|
+
const current = parsed;
|
|
5841
|
+
const hooks = retireRtkHookBlocks(current.hooks);
|
|
5842
|
+
const retired = hooks !== current.hooks;
|
|
5843
|
+
const input = retired ? { ...current, hooks } : current;
|
|
5844
|
+
const { settings, rewritten } = name === "settings.local.json" ? migrateHookCommands(input) : { settings: input, rewritten: 0 };
|
|
5845
|
+
if (JSON.stringify(settings.hooks)?.includes("rtk-intercept.sh")) {
|
|
5846
|
+
retirement.settingsConflicts.push(path);
|
|
5847
|
+
result.warnings.push(`RTK retirement retained user or unknown wiring in ${path}`);
|
|
5848
|
+
}
|
|
5849
|
+
if (!retired && rewritten === 0) {
|
|
5793
5850
|
return;
|
|
5794
5851
|
}
|
|
5795
|
-
|
|
5796
|
-
|
|
5852
|
+
if (options.dryRun) {
|
|
5853
|
+
retirement.wouldChangeSettings.push(path);
|
|
5797
5854
|
return;
|
|
5798
5855
|
}
|
|
5799
5856
|
const trailingNewline = raw.endsWith(`
|
|
5800
5857
|
`) ? `
|
|
5801
5858
|
` : "";
|
|
5859
|
+
await validateRtkTarget(targetDir, path);
|
|
5802
5860
|
await writeTextFile(settingsPath, `${bom}${JSON.stringify(settings, null, 2)}${trailingNewline}`);
|
|
5861
|
+
retirement.settingsChanged.push(path);
|
|
5803
5862
|
info("update.hook_commands_migrated", { count: String(rewritten) });
|
|
5804
|
-
} catch {
|
|
5863
|
+
} catch (err) {
|
|
5864
|
+
if (err.code === "ENOENT")
|
|
5865
|
+
return;
|
|
5866
|
+
retirement.settingsConflicts.push(path);
|
|
5867
|
+
result.warnings.push(`RTK retirement could not safely migrate ${path}`);
|
|
5805
5868
|
warn("update.hook_commands_migration_failed", {
|
|
5806
5869
|
path: settingsPath
|
|
5807
5870
|
});
|
|
@@ -5821,7 +5884,7 @@ async function runFullUpdatePostProcessing(options, result, config, customizatio
|
|
|
5821
5884
|
}
|
|
5822
5885
|
}
|
|
5823
5886
|
if (isFullUpdate || options.components?.includes("hooks")) {
|
|
5824
|
-
await migrateHookCommandsInSettingsLocal(options.targetDir, options);
|
|
5887
|
+
await migrateHookCommandsInSettingsLocal(options.targetDir, options, result);
|
|
5825
5888
|
}
|
|
5826
5889
|
if (!options.dryRun) {
|
|
5827
5890
|
config.version = result.newVersion;
|
|
@@ -5848,24 +5911,16 @@ function compareSemver(a, b) {
|
|
|
5848
5911
|
}
|
|
5849
5912
|
return 0;
|
|
5850
5913
|
}
|
|
5851
|
-
function checkAndInstallRtkAfterUpdate() {
|
|
5852
|
-
if (!isRtkInstalled()) {
|
|
5853
|
-
warn("update.rtk_missing");
|
|
5854
|
-
console.log(i18n.t("cli.update.rtkMissing"));
|
|
5855
|
-
const rtkInstalled = installRtk();
|
|
5856
|
-
if (rtkInstalled) {
|
|
5857
|
-
console.log(i18n.t("cli.update.rtkInstalled"));
|
|
5858
|
-
}
|
|
5859
|
-
}
|
|
5860
|
-
}
|
|
5861
5914
|
async function updateProjectRegistry(targetDir, newVersion) {
|
|
5862
5915
|
try {
|
|
5863
5916
|
await Promise.resolve().then(() => init_registry());
|
|
5864
5917
|
await registerProject(targetDir, newVersion);
|
|
5865
5918
|
} catch {}
|
|
5866
5919
|
}
|
|
5867
|
-
async function regenerateLockfile(
|
|
5868
|
-
|
|
5920
|
+
async function regenerateLockfile(options, result, retiredPaths) {
|
|
5921
|
+
if (options.dryRun)
|
|
5922
|
+
return;
|
|
5923
|
+
const lockfileResult = await generateAndWriteLockfileForDir(options.targetDir, { retiredPaths });
|
|
5869
5924
|
if (lockfileResult.warning) {
|
|
5870
5925
|
result.warnings.push(lockfileResult.warning);
|
|
5871
5926
|
warn("update.lockfile_failed", { error: lockfileResult.warning });
|
|
@@ -5922,7 +5977,7 @@ async function update(options) {
|
|
|
5922
5977
|
result.skippedComponents = options.components || getAllUpdateComponents();
|
|
5923
5978
|
return result;
|
|
5924
5979
|
}
|
|
5925
|
-
await handleBackupIfRequested(options.targetDir, !!options.backup, result);
|
|
5980
|
+
await handleBackupIfRequested(options.targetDir, !!options.backup && !options.dryRun, result);
|
|
5926
5981
|
const manifestCustomizations = await resolveManifestCustomizations(options, options.targetDir);
|
|
5927
5982
|
const configPreserveFiles = resolveConfigPreserveFiles(options, config);
|
|
5928
5983
|
const customizations = resolveCustomizations(manifestCustomizations, configPreserveFiles, options.targetDir);
|
|
@@ -5930,9 +5985,10 @@ async function update(options) {
|
|
|
5930
5985
|
const components = options.components || getAllUpdateComponents();
|
|
5931
5986
|
await updateAllComponents(options.targetDir, components, updateCheck, customizations, options, result, config, lockfile);
|
|
5932
5987
|
await runFullUpdatePostProcessing(options, result, config, customizations);
|
|
5933
|
-
await
|
|
5934
|
-
|
|
5935
|
-
|
|
5988
|
+
const retiredPaths = await retireRtkFiles(options, lockfile, result, config, customizations);
|
|
5989
|
+
await regenerateLockfile(options, result, retiredPaths);
|
|
5990
|
+
if (!options.dryRun)
|
|
5991
|
+
checkAndInstallCodexAfterUpdate();
|
|
5936
5992
|
if (result.success && !options.dryRun) {
|
|
5937
5993
|
await updateProjectRegistry(options.targetDir, result.newVersion);
|
|
5938
5994
|
}
|
package/package.json
CHANGED
|
@@ -82,16 +82,6 @@
|
|
|
82
82
|
],
|
|
83
83
|
"description": "Schema-based tool input validation — Phase 1 advisory only"
|
|
84
84
|
},
|
|
85
|
-
{
|
|
86
|
-
"matcher": "tool == \"Bash\"",
|
|
87
|
-
"hooks": [
|
|
88
|
-
{
|
|
89
|
-
"type": "command",
|
|
90
|
-
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/rtk-intercept.sh\""
|
|
91
|
-
}
|
|
92
|
-
],
|
|
93
|
-
"description": "RTK auto-intercept — transparently rewrites CLI commands through RTK proxy when available (R013 advisory)"
|
|
94
|
-
},
|
|
95
85
|
{
|
|
96
86
|
"matcher": "tool == \"Bash\" && tool_input.command matches \"git (reset|clean|checkout|restore|switch|rebase|merge)\"",
|
|
97
87
|
"hooks": [
|