oh-my-harness 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog/blocks/tdd-guard.js +22 -30
- package/dist/cli/command-checker.js +1 -1
- package/dist/cli/event-logger.d.ts +3 -1
- package/dist/cli/event-logger.js +5 -2
- package/dist/cli/harness-tester.js +2 -2
- package/dist/generators/hooks.d.ts +1 -1
- package/dist/generators/hooks.js +10 -7
- package/package.json +1 -1
|
@@ -44,17 +44,14 @@ SRC_RE='{{{srcPattern}}}'
|
|
|
44
44
|
TEST_RE="\${TEST_RE//\\\\\\\\/\\\\}"
|
|
45
45
|
SRC_RE="\${SRC_RE//\\\\\\\\/\\\\}"
|
|
46
46
|
if [[ "\$FILE_PATH" =~ \$TEST_RE ]]; then
|
|
47
|
-
# 테스트 파일 수정 → 기록 + 통과 (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
UPDATED
|
|
54
|
-
|
|
55
|
-
echo "\$UPDATED" > "\$HISTORY_FILE"
|
|
56
|
-
fi
|
|
57
|
-
) 200>"\$HISTORY_FILE.lock"
|
|
47
|
+
# 테스트 파일 수정 → 기록 + 통과 (tmp+mv로 원자적 쓰기, 크로스 플랫폼)
|
|
48
|
+
if [[ ! -f "\$HISTORY_FILE" ]]; then
|
|
49
|
+
echo '{"edits":[]}' > "\$HISTORY_FILE"
|
|
50
|
+
fi
|
|
51
|
+
UPDATED=$(jq --arg f "\$FILE_PATH" '.edits += [$f] | .edits |= unique' "\$HISTORY_FILE" 2>/dev/null) || true
|
|
52
|
+
if [[ -n "\$UPDATED" ]]; then
|
|
53
|
+
echo "\$UPDATED" > "\$HISTORY_FILE.tmp" && mv "\$HISTORY_FILE.tmp" "\$HISTORY_FILE"
|
|
54
|
+
fi
|
|
58
55
|
exit 0
|
|
59
56
|
fi
|
|
60
57
|
|
|
@@ -73,25 +70,20 @@ if [[ ! -f "\$HISTORY_FILE" ]]; then
|
|
|
73
70
|
exit 0
|
|
74
71
|
fi
|
|
75
72
|
|
|
76
|
-
# edit-history에서 테스트 파일 검색 (
|
|
77
|
-
|
|
78
|
-
(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
else
|
|
91
|
-
echo "block"
|
|
92
|
-
fi
|
|
93
|
-
) 200>"\$HISTORY_FILE.lock"
|
|
94
|
-
)
|
|
73
|
+
# edit-history에서 테스트 파일 검색 (tmp+mv로 원자적 쓰기, 크로스 플랫폼)
|
|
74
|
+
if jq -e --arg b "\$BASENAME" '.edits[] | select(contains($b) and (contains(".test.") or contains(".spec.") or contains("test_")))' "\$HISTORY_FILE" >/dev/null 2>&1; then
|
|
75
|
+
# 테스트 먼저 수정됨 → 매칭 테스트 기록 소비(제거) + 소스 기록 + 통과
|
|
76
|
+
UPDATED=$(jq --arg b "\$BASENAME" --arg f "\$FILE_PATH" '
|
|
77
|
+
.edits |= [.[] | select((contains($b) and (contains(".test.") or contains(".spec.") or contains("test_"))) | not)]
|
|
78
|
+
| .edits += [$f] | .edits |= unique
|
|
79
|
+
' "\$HISTORY_FILE" 2>/dev/null) || true
|
|
80
|
+
if [[ -n "\$UPDATED" ]]; then
|
|
81
|
+
echo "\$UPDATED" > "\$HISTORY_FILE.tmp" && mv "\$HISTORY_FILE.tmp" "\$HISTORY_FILE"
|
|
82
|
+
fi
|
|
83
|
+
DECISION="allow"
|
|
84
|
+
else
|
|
85
|
+
DECISION="block"
|
|
86
|
+
fi
|
|
95
87
|
|
|
96
88
|
if [[ "\$DECISION" == "allow" ]]; then
|
|
97
89
|
exit 0
|
|
@@ -61,7 +61,7 @@ export async function checkHarnessCommands(hooks, projectDir) {
|
|
|
61
61
|
const fs = await import("node:fs/promises");
|
|
62
62
|
const path = await import("node:path");
|
|
63
63
|
for (const hook of hooks) {
|
|
64
|
-
const scriptPath = hook.command.replace(/^bash\s+/, "");
|
|
64
|
+
const scriptPath = hook.command.replace(/^bash\s+/, "").replace(/^"|"$/g, "");
|
|
65
65
|
const fullPath = path.join(projectDir, scriptPath);
|
|
66
66
|
let content;
|
|
67
67
|
try {
|
|
@@ -2,7 +2,7 @@ export interface HookEvent {
|
|
|
2
2
|
ts: string;
|
|
3
3
|
event: string;
|
|
4
4
|
hook: string;
|
|
5
|
-
decision: "block" | "allow";
|
|
5
|
+
decision: "block" | "allow" | "error";
|
|
6
6
|
reason?: string;
|
|
7
7
|
tool?: string;
|
|
8
8
|
}
|
|
@@ -13,9 +13,11 @@ export interface EventStats {
|
|
|
13
13
|
totalEvents: number;
|
|
14
14
|
blockCount: number;
|
|
15
15
|
allowCount: number;
|
|
16
|
+
errorCount: number;
|
|
16
17
|
byHook: Record<string, {
|
|
17
18
|
block: number;
|
|
18
19
|
allow: number;
|
|
20
|
+
error: number;
|
|
19
21
|
}>;
|
|
20
22
|
}
|
|
21
23
|
export declare function aggregateStats(events: HookEvent[]): EventStats;
|
package/dist/cli/event-logger.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function readEvents(projectDir) {
|
|
|
27
27
|
if (typeof parsed.ts === "string" &&
|
|
28
28
|
typeof parsed.event === "string" &&
|
|
29
29
|
typeof parsed.hook === "string" &&
|
|
30
|
-
(parsed.decision === "block" || parsed.decision === "allow")) {
|
|
30
|
+
(parsed.decision === "block" || parsed.decision === "allow" || parsed.decision === "error")) {
|
|
31
31
|
events.push(parsed);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -48,15 +48,18 @@ export function aggregateStats(events) {
|
|
|
48
48
|
totalEvents: events.length,
|
|
49
49
|
blockCount: 0,
|
|
50
50
|
allowCount: 0,
|
|
51
|
+
errorCount: 0,
|
|
51
52
|
byHook: {},
|
|
52
53
|
};
|
|
53
54
|
for (const e of events) {
|
|
54
55
|
if (e.decision === "block")
|
|
55
56
|
stats.blockCount++;
|
|
57
|
+
else if (e.decision === "error")
|
|
58
|
+
stats.errorCount++;
|
|
56
59
|
else
|
|
57
60
|
stats.allowCount++;
|
|
58
61
|
if (!stats.byHook[e.hook]) {
|
|
59
|
-
stats.byHook[e.hook] = { block: 0, allow: 0 };
|
|
62
|
+
stats.byHook[e.hook] = { block: 0, allow: 0, error: 0 };
|
|
60
63
|
}
|
|
61
64
|
stats.byHook[e.hook][e.decision]++;
|
|
62
65
|
}
|
|
@@ -125,11 +125,11 @@ export function generateBlockTestCases(hookEntries, blocks, currentBranch, regis
|
|
|
125
125
|
};
|
|
126
126
|
const aliases = blockIdAliases[block.id] ?? [block.id];
|
|
127
127
|
const matchedHook = registeredHooks?.find((h) => {
|
|
128
|
-
const scriptName = h.command.replace(/^bash\s+/, "");
|
|
128
|
+
const scriptName = h.command.replace(/^bash\s+/, "").replace(/^"|"$/g, "");
|
|
129
129
|
return aliases.some((alias) => scriptName.includes(alias));
|
|
130
130
|
});
|
|
131
131
|
const hookScript = matchedHook
|
|
132
|
-
? matchedHook.command.replace(/^bash\s+/, "")
|
|
132
|
+
? matchedHook.command.replace(/^bash\s+/, "").replace(/^"|"$/g, "")
|
|
133
133
|
: `.claude/hooks/catalog-${block.id}.sh`;
|
|
134
134
|
switch (block.id) {
|
|
135
135
|
case "path-guard": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MergedConfig } from "../core/preset-types.js";
|
|
2
|
-
export declare function wrapWithLogger(script: string, event?: string): string;
|
|
2
|
+
export declare function wrapWithLogger(script: string, event?: string, projectDir?: string): string;
|
|
3
3
|
export interface GenerateHooksOptions {
|
|
4
4
|
projectDir: string;
|
|
5
5
|
config: MergedConfig;
|
package/dist/generators/hooks.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { mkdir, writeFile, chmod, readFile, unlink } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
function buildLoggerSnippet(event) {
|
|
3
|
+
function buildLoggerSnippet(event, projectDir) {
|
|
4
|
+
const stateDir = projectDir
|
|
5
|
+
? `${projectDir}/.claude/hooks/.state`
|
|
6
|
+
: ".claude/hooks/.state";
|
|
4
7
|
return `# --- oh-my-harness event logger ---
|
|
5
|
-
_OMH_STATE_DIR="
|
|
8
|
+
_OMH_STATE_DIR="${stateDir}"
|
|
6
9
|
mkdir -p "$_OMH_STATE_DIR" 2>/dev/null || true
|
|
7
10
|
_OMH_HOOK_NAME="$(basename "$0")"
|
|
8
11
|
_OMH_EVENT="${event}"
|
|
@@ -14,11 +17,11 @@ _log_event() {
|
|
|
14
17
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$_OMH_EVENT" "$_OMH_HOOK_NAME" "$decision" "$reason" \\
|
|
15
18
|
>> "$_OMH_STATE_DIR/events.jsonl"
|
|
16
19
|
}
|
|
17
|
-
trap '[ "$_OMH_LOGGED" -eq 0 ]
|
|
20
|
+
trap '_OMH_EXIT_CODE=$?; if [ "$_OMH_LOGGED" -eq 0 ]; then if [ "$_OMH_EXIT_CODE" -ne 0 ]; then _log_event "error" "hook exited with code $_OMH_EXIT_CODE"; else _log_event "allow"; fi; fi' EXIT
|
|
18
21
|
# --- end logger ---`;
|
|
19
22
|
}
|
|
20
|
-
export function wrapWithLogger(script, event = "unknown") {
|
|
21
|
-
const snippet = buildLoggerSnippet(event);
|
|
23
|
+
export function wrapWithLogger(script, event = "unknown", projectDir) {
|
|
24
|
+
const snippet = buildLoggerSnippet(event, projectDir);
|
|
22
25
|
if (script.includes("INPUT=$(cat)")) {
|
|
23
26
|
return script.replace("INPUT=$(cat)", `INPUT=$(cat)\n\n${snippet}`);
|
|
24
27
|
}
|
|
@@ -108,13 +111,13 @@ export async function generateHooks(options) {
|
|
|
108
111
|
}
|
|
109
112
|
usedScriptNames.add(scriptName);
|
|
110
113
|
const scriptPath = join(hooksDir, scriptName);
|
|
111
|
-
const wrappedScript = wrapWithLogger(hook.inline, hook.event);
|
|
114
|
+
const wrappedScript = wrapWithLogger(hook.inline, hook.event, projectDir);
|
|
112
115
|
await writeFile(scriptPath, wrappedScript, "utf8");
|
|
113
116
|
await chmod(scriptPath, 0o755);
|
|
114
117
|
generatedFiles.push(scriptPath);
|
|
115
118
|
const entry = {
|
|
116
119
|
matcher: hook.matcher,
|
|
117
|
-
hooks: [{ type: "command", command: `bash
|
|
120
|
+
hooks: [{ type: "command", command: `bash "${scriptPath}"` }],
|
|
118
121
|
};
|
|
119
122
|
if (!hooksConfig[hook.event]) {
|
|
120
123
|
hooksConfig[hook.event] = [];
|