oh-my-harness 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog/blocks/compact-context.d.ts +2 -0
- package/dist/catalog/blocks/compact-context.js +27 -0
- package/dist/catalog/blocks/config-audit.d.ts +2 -0
- package/dist/catalog/blocks/config-audit.js +29 -0
- package/dist/catalog/blocks/desktop-notify.d.ts +2 -0
- package/dist/catalog/blocks/desktop-notify.js +21 -0
- package/dist/catalog/blocks/index.js +12 -0
- package/dist/catalog/blocks/sql-guard.d.ts +2 -0
- package/dist/catalog/blocks/sql-guard.js +34 -0
- package/dist/catalog/blocks/test-on-save.d.ts +2 -0
- package/dist/catalog/blocks/test-on-save.js +37 -0
- package/dist/catalog/blocks/worktree-setup.d.ts +2 -0
- package/dist/catalog/blocks/worktree-setup.js +91 -0
- package/dist/catalog/converter.js +5 -7
- package/dist/catalog/types.d.ts +8 -8
- package/dist/catalog/types.js +2 -2
- package/dist/cli/commands/catalog.js +1 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +2 -1
- package/dist/cli/commands/init.js +11 -1
- package/dist/cli/index.js +4 -1
- package/dist/cli/stats/index.js +4 -0
- package/dist/core/config-merger.js +28 -0
- package/dist/core/harness-converter-v2.js +26 -13
- package/dist/core/harness-converter.js +4 -0
- package/dist/core/preset-types.d.ts +368 -0
- package/dist/core/preset-types.js +4 -0
- package/dist/detector/detectors/index.js +2 -0
- package/dist/detector/detectors/terraform.d.ts +2 -0
- package/dist/detector/detectors/terraform.js +46 -0
- package/dist/generators/hooks.js +17 -5
- package/dist/generators/settings.js +21 -1
- package/package.json +1 -1
- package/presets/terraform/preset.yaml +65 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const compactContext = {
|
|
2
|
+
id: "compact-context",
|
|
3
|
+
name: "Compact Context",
|
|
4
|
+
description: "Re-injects project context after context compaction",
|
|
5
|
+
category: "automation",
|
|
6
|
+
event: "SessionStart",
|
|
7
|
+
matcher: "compact",
|
|
8
|
+
canBlock: false,
|
|
9
|
+
params: [
|
|
10
|
+
{
|
|
11
|
+
name: "contextFile",
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Path to the context file to re-inject",
|
|
14
|
+
default: "CLAUDE.md",
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
tags: ["context", "compaction", "session", "automation"],
|
|
19
|
+
template: `#!/bin/bash
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
INPUT=$(cat)
|
|
22
|
+
CONTEXT_FILE="{{{contextFile}}}"
|
|
23
|
+
if [[ -f "$CONTEXT_FILE" ]]; then
|
|
24
|
+
cat "$CONTEXT_FILE"
|
|
25
|
+
fi
|
|
26
|
+
exit 0`,
|
|
27
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const configAudit = {
|
|
2
|
+
id: "config-audit",
|
|
3
|
+
name: "Config Audit",
|
|
4
|
+
description: "Logs configuration changes for audit trail",
|
|
5
|
+
category: "audit",
|
|
6
|
+
event: "ConfigChange",
|
|
7
|
+
matcher: "",
|
|
8
|
+
canBlock: false,
|
|
9
|
+
params: [
|
|
10
|
+
{
|
|
11
|
+
name: "logFile",
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Path to the audit log file",
|
|
14
|
+
default: ".claude/hooks/.state/config-audit.log",
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
tags: ["audit", "config", "logging"],
|
|
19
|
+
template: `#!/bin/bash
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
INPUT=$(cat)
|
|
22
|
+
LOG_FILE="{{{logFile}}}"
|
|
23
|
+
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
|
24
|
+
SOURCE=$(echo "$INPUT" | jq -r '.source // "unknown"' 2>/dev/null)
|
|
25
|
+
FILE=$(echo "$INPUT" | jq -r '.file_path // "unknown"' 2>/dev/null)
|
|
26
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
27
|
+
echo "{\\"ts\\":\\"$TS\\",\\"source\\":\\"$SOURCE\\",\\"file\\":\\"$FILE\\"}" >> "$LOG_FILE"
|
|
28
|
+
exit 0`,
|
|
29
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const desktopNotify = {
|
|
2
|
+
id: "desktop-notify",
|
|
3
|
+
name: "Desktop Notify",
|
|
4
|
+
description: "Sends desktop notification when Claude needs attention",
|
|
5
|
+
category: "notification",
|
|
6
|
+
event: "Notification",
|
|
7
|
+
matcher: "",
|
|
8
|
+
canBlock: false,
|
|
9
|
+
params: [],
|
|
10
|
+
tags: ["notification", "desktop", "alert"],
|
|
11
|
+
template: `#!/bin/bash
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
MESSAGE=$(echo "$INPUT" | jq -r '.message // "Claude Code needs your attention"' 2>/dev/null)
|
|
15
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
16
|
+
osascript -e "display notification \\"$MESSAGE\\" with title \\"Claude Code\\"" 2>/dev/null || true
|
|
17
|
+
elif command -v notify-send &>/dev/null; then
|
|
18
|
+
notify-send "Claude Code" "$MESSAGE" 2>/dev/null || true
|
|
19
|
+
fi
|
|
20
|
+
exit 0`,
|
|
21
|
+
};
|
|
@@ -9,6 +9,12 @@ import { formatOnSave } from "./format-on-save.js";
|
|
|
9
9
|
import { autoPr } from "./auto-pr.js";
|
|
10
10
|
import { secretFileGuard } from "./secret-file-guard.js";
|
|
11
11
|
import { tddGuard } from "./tdd-guard.js";
|
|
12
|
+
import { sqlGuard } from "./sql-guard.js";
|
|
13
|
+
import { testOnSave } from "./test-on-save.js";
|
|
14
|
+
import { desktopNotify } from "./desktop-notify.js";
|
|
15
|
+
import { configAudit } from "./config-audit.js";
|
|
16
|
+
import { compactContext } from "./compact-context.js";
|
|
17
|
+
import { worktreeSetup } from "./worktree-setup.js";
|
|
12
18
|
export const builtinBlocks = [
|
|
13
19
|
branchGuard,
|
|
14
20
|
commitTestGate,
|
|
@@ -21,4 +27,10 @@ export const builtinBlocks = [
|
|
|
21
27
|
autoPr,
|
|
22
28
|
secretFileGuard,
|
|
23
29
|
tddGuard,
|
|
30
|
+
sqlGuard,
|
|
31
|
+
testOnSave,
|
|
32
|
+
desktopNotify,
|
|
33
|
+
configAudit,
|
|
34
|
+
compactContext,
|
|
35
|
+
worktreeSetup,
|
|
24
36
|
];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const sqlGuard = {
|
|
2
|
+
id: "sql-guard",
|
|
3
|
+
name: "SQL Guard",
|
|
4
|
+
description: "Blocks dangerous SQL commands in shell",
|
|
5
|
+
category: "security",
|
|
6
|
+
event: "PreToolUse",
|
|
7
|
+
matcher: "Bash",
|
|
8
|
+
canBlock: true,
|
|
9
|
+
params: [
|
|
10
|
+
{
|
|
11
|
+
name: "patterns",
|
|
12
|
+
type: "string[]",
|
|
13
|
+
description: "Dangerous SQL patterns to block",
|
|
14
|
+
default: ["DROP TABLE", "DROP DATABASE", "TRUNCATE TABLE", "DELETE FROM", "DROP COLUMN", "DROP INDEX"],
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
tags: ["security", "sql", "guard", "database"],
|
|
19
|
+
template: `#!/bin/bash
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
INPUT=$(cat)
|
|
22
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
23
|
+
[[ -z "$COMMAND" ]] && exit 0
|
|
24
|
+
COMMAND_LOWER=$(echo "$COMMAND" | tr '[:upper:]' '[:lower:]')
|
|
25
|
+
PATTERNS=({{#each patterns}}"{{{this}}}" {{/each}})
|
|
26
|
+
for PATTERN in "\${PATTERNS[@]}"; do
|
|
27
|
+
PATTERN_LOWER=$(echo "$PATTERN" | tr '[:upper:]' '[:lower:]')
|
|
28
|
+
if echo "$COMMAND_LOWER" | grep -qF -- "$PATTERN_LOWER"; then
|
|
29
|
+
echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"oh-my-harness: SQL command matches blocked pattern: $PATTERN\\"}"
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
done
|
|
33
|
+
exit 0`,
|
|
34
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const testOnSave = {
|
|
2
|
+
id: "test-on-save",
|
|
3
|
+
name: "Test on Save",
|
|
4
|
+
description: "Runs related tests after source file edits",
|
|
5
|
+
category: "quality",
|
|
6
|
+
event: "PostToolUse",
|
|
7
|
+
matcher: "Edit|Write",
|
|
8
|
+
canBlock: false,
|
|
9
|
+
params: [
|
|
10
|
+
{
|
|
11
|
+
name: "testCommand",
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Command to run tests (e.g. npx vitest run)",
|
|
14
|
+
required: true,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "filePattern",
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Regex pattern to match source files",
|
|
20
|
+
default: "\\.(ts|tsx|js|jsx|py)$",
|
|
21
|
+
required: false,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
tags: ["quality", "testing", "auto-run", "save"],
|
|
25
|
+
template: `#!/bin/bash
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)
|
|
29
|
+
[[ -z "$FILE_PATH" ]] && exit 0
|
|
30
|
+
FILE_RE='{{{filePattern}}}'
|
|
31
|
+
FILE_RE="\${FILE_RE//\\\\\\\\/\\\\}"
|
|
32
|
+
if [[ "$FILE_PATH" =~ $FILE_RE ]]; then
|
|
33
|
+
echo "oh-my-harness: Running tests after edit..." >&2
|
|
34
|
+
{{{testCommand}}} >&2 2>&1 || true
|
|
35
|
+
fi
|
|
36
|
+
exit 0`,
|
|
37
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const worktreeSetup = {
|
|
2
|
+
id: "worktree-setup",
|
|
3
|
+
name: "Worktree Setup",
|
|
4
|
+
description: "Initializes new git worktrees with symlinked dependencies and copied config files",
|
|
5
|
+
category: "automation",
|
|
6
|
+
event: "WorktreeCreate",
|
|
7
|
+
matcher: "",
|
|
8
|
+
canBlock: false,
|
|
9
|
+
params: [
|
|
10
|
+
{
|
|
11
|
+
name: "symlinkPaths",
|
|
12
|
+
type: "string[]",
|
|
13
|
+
description: "Paths to symlink from main worktree (heavy dependencies)",
|
|
14
|
+
required: false,
|
|
15
|
+
default: ["node_modules", ".venv", "vendor", "target", ".build"],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "copyPaths",
|
|
19
|
+
type: "string[]",
|
|
20
|
+
description: "Paths to hard-copy from main worktree (config files)",
|
|
21
|
+
required: false,
|
|
22
|
+
default: [".env", ".env.local"],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "installCommand",
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Package install command to run if symlink source missing",
|
|
28
|
+
required: false,
|
|
29
|
+
default: "",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
tags: ["worktree", "git", "dependencies", "symlink", "automation"],
|
|
33
|
+
template: `#!/bin/bash
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
INPUT=$(cat)
|
|
36
|
+
|
|
37
|
+
# Parse worktree info from stdin
|
|
38
|
+
WORKTREE_PATH=$(echo "$INPUT" | jq -r '.worktree_path // empty' 2>/dev/null)
|
|
39
|
+
if [[ -z "$WORKTREE_PATH" ]]; then
|
|
40
|
+
echo "oh-my-harness: worktree-setup — no worktree_path in input" >&2
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Find the main worktree (parent repo)
|
|
45
|
+
MAIN_WORKTREE=$(git -C "$WORKTREE_PATH" worktree list --porcelain 2>/dev/null | head -1 | sed 's/^worktree //')
|
|
46
|
+
if [[ -z "$MAIN_WORKTREE" ]]; then
|
|
47
|
+
echo "oh-my-harness: worktree-setup — cannot determine main worktree" >&2
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
echo "oh-my-harness: worktree-setup — setting up $WORKTREE_PATH"
|
|
52
|
+
|
|
53
|
+
# Symlink heavy directories from main worktree
|
|
54
|
+
SYMLINKS=({{#each symlinkPaths}}"{{{this}}}" {{/each}})
|
|
55
|
+
for rel in "\${SYMLINKS[@]}"; do
|
|
56
|
+
[[ -z "$rel" ]] && continue
|
|
57
|
+
src="$MAIN_WORKTREE/$rel"
|
|
58
|
+
dst="$WORKTREE_PATH/$rel"
|
|
59
|
+
if [[ -e "$src" && ! -e "$dst" ]]; then
|
|
60
|
+
mkdir -p "$(dirname "$dst")"
|
|
61
|
+
ln -s "$src" "$dst"
|
|
62
|
+
echo " symlinked: $rel"
|
|
63
|
+
fi
|
|
64
|
+
done
|
|
65
|
+
|
|
66
|
+
# Hard-copy config files from main worktree
|
|
67
|
+
COPIES=({{#each copyPaths}}"{{{this}}}" {{/each}})
|
|
68
|
+
for rel in "\${COPIES[@]}"; do
|
|
69
|
+
[[ -z "$rel" ]] && continue
|
|
70
|
+
src="$MAIN_WORKTREE/$rel"
|
|
71
|
+
dst="$WORKTREE_PATH/$rel"
|
|
72
|
+
if [[ -f "$src" && ! -f "$dst" ]]; then
|
|
73
|
+
mkdir -p "$(dirname "$dst")"
|
|
74
|
+
cp "$src" "$dst"
|
|
75
|
+
echo " copied: $rel"
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
# Run install command if provided and no symlinked deps exist
|
|
80
|
+
INSTALL_CMD="{{{installCommand}}}"
|
|
81
|
+
if [[ -n "$INSTALL_CMD" ]]; then
|
|
82
|
+
first_symlink="\${SYMLINKS[0]}"
|
|
83
|
+
if [[ -n "$first_symlink" && ! -e "$WORKTREE_PATH/$first_symlink" ]]; then
|
|
84
|
+
echo " running: $INSTALL_CMD"
|
|
85
|
+
cd "$WORKTREE_PATH" && eval "$INSTALL_CMD"
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
echo "oh-my-harness: worktree-setup complete"
|
|
90
|
+
exit 0`,
|
|
91
|
+
};
|
|
@@ -4,7 +4,7 @@ export async function convertHookEntries(entries, registry, projectDir) {
|
|
|
4
4
|
const hooksConfig = {};
|
|
5
5
|
const scripts = new Map();
|
|
6
6
|
const errors = [];
|
|
7
|
-
const
|
|
7
|
+
const blockInstanceCount = new Map();
|
|
8
8
|
for (const entry of entries) {
|
|
9
9
|
const block = registry.get(entry.block);
|
|
10
10
|
if (!block) {
|
|
@@ -17,11 +17,6 @@ export async function convertHookEntries(entries, registry, projectDir) {
|
|
|
17
17
|
errors.push(...paramErrors);
|
|
18
18
|
continue;
|
|
19
19
|
}
|
|
20
|
-
if (seenBlockIds.has(entry.block)) {
|
|
21
|
-
errors.push(`Duplicate block id skipped: "${entry.block}"`);
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
seenBlockIds.add(entry.block);
|
|
25
20
|
let scriptContent;
|
|
26
21
|
try {
|
|
27
22
|
scriptContent = renderTemplate(block.template, resolvedParams);
|
|
@@ -30,7 +25,10 @@ export async function convertHookEntries(entries, registry, projectDir) {
|
|
|
30
25
|
errors.push(`Failed to render block "${entry.block}": ${err.message}`);
|
|
31
26
|
continue;
|
|
32
27
|
}
|
|
33
|
-
|
|
28
|
+
// Support multiple instances of the same block with different params
|
|
29
|
+
const count = blockInstanceCount.get(entry.block) ?? 0;
|
|
30
|
+
blockInstanceCount.set(entry.block, count + 1);
|
|
31
|
+
const scriptName = count === 0 ? `${entry.block}.sh` : `${entry.block}-${count}.sh`;
|
|
34
32
|
const scriptPath = path.join(projectDir, ".claude", "hooks", scriptName);
|
|
35
33
|
scripts.set(scriptPath, scriptContent);
|
|
36
34
|
const hookEntry = {
|
package/dist/catalog/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export type HookEvent = "PreToolUse" | "PostToolUse" | "PreCompact" | "PostCompact" | "Notification" | "Stop" | "SubagentStop" | "PreBash" | "PostBash" | "PreEdit" | "PostEdit" | "PreRead" | "PostRead" | "PreWrite" | "PostWrite" | "SessionStart" | "SessionEnd" | "PreToolResult" | "PostToolResult" | "UserPromptSubmit";
|
|
3
|
-
export type BuildingBlockCategory = "git" | "quality" | "security" | "notification" | "formatting" | "custom" | "auto-fix" | "automation" | "file-protection";
|
|
2
|
+
export type HookEvent = "PreToolUse" | "PostToolUse" | "PreCompact" | "PostCompact" | "Notification" | "Stop" | "SubagentStop" | "PreBash" | "PostBash" | "PreEdit" | "PostEdit" | "PreRead" | "PostRead" | "PreWrite" | "PostWrite" | "SessionStart" | "SessionEnd" | "PreToolResult" | "PostToolResult" | "UserPromptSubmit" | "ConfigChange" | "WorktreeCreate" | "WorktreeRemove";
|
|
3
|
+
export type BuildingBlockCategory = "git" | "quality" | "security" | "notification" | "formatting" | "custom" | "auto-fix" | "automation" | "file-protection" | "audit";
|
|
4
4
|
export interface ParamDefinition {
|
|
5
5
|
name: string;
|
|
6
6
|
type: "string" | "boolean" | "number" | "string[]";
|
|
@@ -47,8 +47,8 @@ export declare const BuildingBlockSchema: z.ZodObject<{
|
|
|
47
47
|
id: z.ZodString;
|
|
48
48
|
name: z.ZodString;
|
|
49
49
|
description: z.ZodString;
|
|
50
|
-
category: z.ZodEnum<["git", "quality", "security", "notification", "formatting", "custom", "auto-fix", "automation", "file-protection"]>;
|
|
51
|
-
event: z.ZodEnum<["PreToolUse", "PostToolUse", "PreCompact", "PostCompact", "Notification", "Stop", "SubagentStop", "PreBash", "PostBash", "PreEdit", "PostEdit", "PreRead", "PostRead", "PreWrite", "PostWrite", "SessionStart", "SessionEnd", "PreToolResult", "PostToolResult", "UserPromptSubmit"]>;
|
|
50
|
+
category: z.ZodEnum<["git", "quality", "security", "notification", "formatting", "custom", "auto-fix", "automation", "file-protection", "audit"]>;
|
|
51
|
+
event: z.ZodEnum<["PreToolUse", "PostToolUse", "PreCompact", "PostCompact", "Notification", "Stop", "SubagentStop", "PreBash", "PostBash", "PreEdit", "PostEdit", "PreRead", "PostRead", "PreWrite", "PostWrite", "SessionStart", "SessionEnd", "PreToolResult", "PostToolResult", "UserPromptSubmit", "ConfigChange", "WorktreeCreate", "WorktreeRemove"]>;
|
|
52
52
|
matcher: z.ZodOptional<z.ZodString>;
|
|
53
53
|
canBlock: z.ZodBoolean;
|
|
54
54
|
params: z.ZodArray<z.ZodObject<{
|
|
@@ -85,8 +85,8 @@ export declare const BuildingBlockSchema: z.ZodObject<{
|
|
|
85
85
|
template: string;
|
|
86
86
|
name: string;
|
|
87
87
|
tags: string[];
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
event: "PreToolUse" | "PostToolUse" | "SessionStart" | "Notification" | "ConfigChange" | "WorktreeCreate" | "PreCompact" | "PostCompact" | "Stop" | "SubagentStop" | "PreBash" | "PostBash" | "PreEdit" | "PostEdit" | "PreRead" | "PostRead" | "PreWrite" | "PostWrite" | "SessionEnd" | "PreToolResult" | "PostToolResult" | "UserPromptSubmit" | "WorktreeRemove";
|
|
89
|
+
category: "notification" | "custom" | "git" | "quality" | "security" | "formatting" | "auto-fix" | "automation" | "file-protection" | "audit";
|
|
90
90
|
canBlock: boolean;
|
|
91
91
|
matcher?: string | undefined;
|
|
92
92
|
}, {
|
|
@@ -102,8 +102,8 @@ export declare const BuildingBlockSchema: z.ZodObject<{
|
|
|
102
102
|
template: string;
|
|
103
103
|
name: string;
|
|
104
104
|
tags: string[];
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
event: "PreToolUse" | "PostToolUse" | "SessionStart" | "Notification" | "ConfigChange" | "WorktreeCreate" | "PreCompact" | "PostCompact" | "Stop" | "SubagentStop" | "PreBash" | "PostBash" | "PreEdit" | "PostEdit" | "PreRead" | "PostRead" | "PreWrite" | "PostWrite" | "SessionEnd" | "PreToolResult" | "PostToolResult" | "UserPromptSubmit" | "WorktreeRemove";
|
|
106
|
+
category: "notification" | "custom" | "git" | "quality" | "security" | "formatting" | "auto-fix" | "automation" | "file-protection" | "audit";
|
|
107
107
|
canBlock: boolean;
|
|
108
108
|
matcher?: string | undefined;
|
|
109
109
|
}>;
|
package/dist/catalog/types.js
CHANGED
|
@@ -10,8 +10,8 @@ export const BuildingBlockSchema = z.object({
|
|
|
10
10
|
id: z.string(),
|
|
11
11
|
name: z.string(),
|
|
12
12
|
description: z.string(),
|
|
13
|
-
category: z.enum(["git", "quality", "security", "notification", "formatting", "custom", "auto-fix", "automation", "file-protection"]),
|
|
14
|
-
event: z.enum(["PreToolUse", "PostToolUse", "PreCompact", "PostCompact", "Notification", "Stop", "SubagentStop", "PreBash", "PostBash", "PreEdit", "PostEdit", "PreRead", "PostRead", "PreWrite", "PostWrite", "SessionStart", "SessionEnd", "PreToolResult", "PostToolResult", "UserPromptSubmit"]),
|
|
13
|
+
category: z.enum(["git", "quality", "security", "notification", "formatting", "custom", "auto-fix", "automation", "file-protection", "audit"]),
|
|
14
|
+
event: z.enum(["PreToolUse", "PostToolUse", "PreCompact", "PostCompact", "Notification", "Stop", "SubagentStop", "PreBash", "PostBash", "PreEdit", "PostEdit", "PreRead", "PostRead", "PreWrite", "PostWrite", "SessionStart", "SessionEnd", "PreToolResult", "PostToolResult", "UserPromptSubmit", "ConfigChange", "WorktreeCreate", "WorktreeRemove"]),
|
|
15
15
|
matcher: z.string().optional(),
|
|
16
16
|
canBlock: z.boolean(),
|
|
17
17
|
params: z.array(ParamDefinitionSchema),
|
|
@@ -71,6 +71,7 @@ export async function doctorCommand(options = {}) {
|
|
|
71
71
|
checks.hooksExecutable = true;
|
|
72
72
|
}
|
|
73
73
|
const healthy = Object.values(checks).every(Boolean);
|
|
74
|
+
const exitCode = healthy ? 0 : 1;
|
|
74
75
|
if (healthy) {
|
|
75
76
|
console.log("oh-my-harness: all checks passed");
|
|
76
77
|
}
|
|
@@ -80,5 +81,5 @@ export async function doctorCommand(options = {}) {
|
|
|
80
81
|
console.log(` ${msg}`);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
|
-
return { healthy, checks, messages };
|
|
84
|
+
return { healthy, exitCode, checks, messages };
|
|
84
85
|
}
|
|
@@ -59,7 +59,17 @@ export async function initCommand(presetNames, options = {}) {
|
|
|
59
59
|
const presetsDir = options.presetsDir ?? getDefaultPresetsDir();
|
|
60
60
|
// If --preset flag is used, go through preset flow
|
|
61
61
|
if (options.preset && options.preset.length > 0) {
|
|
62
|
-
|
|
62
|
+
try {
|
|
63
|
+
await initWithPresets(options.preset, projectDir, presetsDir, options);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
67
|
+
console.error(`oh-my-harness: ${message}`);
|
|
68
|
+
if (!message.includes("Preset not found")) {
|
|
69
|
+
console.error("Run `oh-my-harness init --help` for usage information.");
|
|
70
|
+
}
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
}
|
|
63
73
|
return;
|
|
64
74
|
}
|
|
65
75
|
// If preset names are provided as positional args, use legacy flow
|
package/dist/cli/index.js
CHANGED
|
@@ -36,7 +36,10 @@ export function createCli() {
|
|
|
36
36
|
.description("Validate harness configuration health")
|
|
37
37
|
.action(async () => {
|
|
38
38
|
const { doctorCommand } = await import("./commands/doctor.js");
|
|
39
|
-
await doctorCommand();
|
|
39
|
+
const result = await doctorCommand();
|
|
40
|
+
if (result.exitCode !== 0) {
|
|
41
|
+
process.exitCode = result.exitCode;
|
|
42
|
+
}
|
|
40
43
|
});
|
|
41
44
|
program
|
|
42
45
|
.command("test")
|
package/dist/cli/stats/index.js
CHANGED
|
@@ -3,6 +3,10 @@ import React from "react";
|
|
|
3
3
|
import { loadStatsData } from "./data.js";
|
|
4
4
|
import { App } from "./App.js";
|
|
5
5
|
export async function statsCommand(options = {}) {
|
|
6
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
7
|
+
console.error("oh-my-harness: stats requires an interactive terminal (TTY). Run this command directly in your terminal.");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
6
10
|
const projectDir = options.projectDir ?? process.cwd();
|
|
7
11
|
const data = await loadStatsData(projectDir);
|
|
8
12
|
const { waitUntilExit } = render(React.createElement(App, { initialData: data, projectDir }));
|
|
@@ -3,6 +3,10 @@ export function mergePresets(presets) {
|
|
|
3
3
|
const sectionsMap = new Map();
|
|
4
4
|
const preToolUseMap = new Map();
|
|
5
5
|
const postToolUseMap = new Map();
|
|
6
|
+
const sessionStartMap = new Map();
|
|
7
|
+
const notificationMap = new Map();
|
|
8
|
+
const configChangeMap = new Map();
|
|
9
|
+
const worktreeCreateMap = new Map();
|
|
6
10
|
const allowSet = new Set();
|
|
7
11
|
const denySet = new Set();
|
|
8
12
|
const presetNames = [];
|
|
@@ -29,6 +33,26 @@ export function mergePresets(presets) {
|
|
|
29
33
|
postToolUseMap.set(hook.id, hook);
|
|
30
34
|
}
|
|
31
35
|
}
|
|
36
|
+
if (preset.hooks?.sessionStart) {
|
|
37
|
+
for (const hook of preset.hooks.sessionStart) {
|
|
38
|
+
sessionStartMap.set(hook.id, hook);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (preset.hooks?.notification) {
|
|
42
|
+
for (const hook of preset.hooks.notification) {
|
|
43
|
+
notificationMap.set(hook.id, hook);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (preset.hooks?.configChange) {
|
|
47
|
+
for (const hook of preset.hooks.configChange) {
|
|
48
|
+
configChangeMap.set(hook.id, hook);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (preset.hooks?.worktreeCreate) {
|
|
52
|
+
for (const hook of preset.hooks.worktreeCreate) {
|
|
53
|
+
worktreeCreateMap.set(hook.id, hook);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
32
56
|
// Merge settings (accumulate)
|
|
33
57
|
if (preset.settings?.permissions?.allow) {
|
|
34
58
|
for (const a of preset.settings.permissions.allow)
|
|
@@ -48,6 +72,10 @@ export function mergePresets(presets) {
|
|
|
48
72
|
hooks: {
|
|
49
73
|
preToolUse: Array.from(preToolUseMap.values()),
|
|
50
74
|
postToolUse: Array.from(postToolUseMap.values()),
|
|
75
|
+
sessionStart: Array.from(sessionStartMap.values()),
|
|
76
|
+
notification: Array.from(notificationMap.values()),
|
|
77
|
+
configChange: Array.from(configChangeMap.values()),
|
|
78
|
+
worktreeCreate: Array.from(worktreeCreateMap.values()),
|
|
51
79
|
},
|
|
52
80
|
settings: {
|
|
53
81
|
permissions: {
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { harnessToMergedConfig, mergeEnforcementAndHooks } from "./harness-converter.js";
|
|
2
2
|
import { createDefaultRegistry } from "../catalog/registry.js";
|
|
3
3
|
import { convertHookEntries } from "../catalog/converter.js";
|
|
4
|
+
/** Maps Claude Code event names to HooksConfig field names */
|
|
5
|
+
const eventToField = {
|
|
6
|
+
PreToolUse: "preToolUse",
|
|
7
|
+
PostToolUse: "postToolUse",
|
|
8
|
+
SessionStart: "sessionStart",
|
|
9
|
+
Notification: "notification",
|
|
10
|
+
ConfigChange: "configChange",
|
|
11
|
+
WorktreeCreate: "worktreeCreate",
|
|
12
|
+
};
|
|
4
13
|
export async function harnessToMergedConfigV2(harness, registry, projectDir) {
|
|
5
14
|
// Start with base conversion (rules, variables, permissions — no inline enforcement scripts)
|
|
6
15
|
const base = harnessToMergedConfig(harness);
|
|
@@ -15,9 +24,14 @@ export async function harnessToMergedConfigV2(harness, registry, projectDir) {
|
|
|
15
24
|
const catalogResult = await convertHookEntries(allHookEntries, resolvedRegistry, projectDir ?? ".");
|
|
16
25
|
// Convert hooksConfig entries from catalog into HookDefinition format.
|
|
17
26
|
// Errors are reported as warnings but don't block valid hooks.
|
|
18
|
-
const
|
|
19
|
-
const additionalPostToolUse = [];
|
|
27
|
+
const additionalHooks = {};
|
|
20
28
|
for (const [event, entries] of Object.entries(catalogResult.hooksConfig)) {
|
|
29
|
+
const field = eventToField[event];
|
|
30
|
+
if (!field)
|
|
31
|
+
continue; // unknown event — skip
|
|
32
|
+
if (!additionalHooks[field]) {
|
|
33
|
+
additionalHooks[field] = [];
|
|
34
|
+
}
|
|
21
35
|
for (const entry of entries) {
|
|
22
36
|
// Find the block id from the script path: .claude/hooks/<block-id>.sh
|
|
23
37
|
const blockId = entry.command.replace(/.*\/(.+)\.sh$/, "$1");
|
|
@@ -27,21 +41,20 @@ export async function harnessToMergedConfigV2(harness, registry, projectDir) {
|
|
|
27
41
|
description: `Catalog block: ${blockId}`,
|
|
28
42
|
inline: catalogResult.scripts.get(entry.command),
|
|
29
43
|
};
|
|
30
|
-
|
|
31
|
-
additionalPreToolUse.push(hookDef);
|
|
32
|
-
}
|
|
33
|
-
else if (event === "PostToolUse") {
|
|
34
|
-
additionalPostToolUse.push(hookDef);
|
|
35
|
-
}
|
|
36
|
-
// Other events (SessionStart, etc.) are not yet mapped to MergedConfig hooks
|
|
44
|
+
additionalHooks[field].push(hookDef);
|
|
37
45
|
}
|
|
38
46
|
}
|
|
47
|
+
const mergedHooks = {
|
|
48
|
+
preToolUse: [...base.hooks.preToolUse, ...(additionalHooks.preToolUse ?? [])],
|
|
49
|
+
postToolUse: [...base.hooks.postToolUse, ...(additionalHooks.postToolUse ?? [])],
|
|
50
|
+
sessionStart: [...(base.hooks.sessionStart ?? []), ...(additionalHooks.sessionStart ?? [])],
|
|
51
|
+
notification: [...(base.hooks.notification ?? []), ...(additionalHooks.notification ?? [])],
|
|
52
|
+
configChange: [...(base.hooks.configChange ?? []), ...(additionalHooks.configChange ?? [])],
|
|
53
|
+
worktreeCreate: [...(base.hooks.worktreeCreate ?? []), ...(additionalHooks.worktreeCreate ?? [])],
|
|
54
|
+
};
|
|
39
55
|
return {
|
|
40
56
|
...base,
|
|
41
|
-
hooks:
|
|
42
|
-
preToolUse: [...base.hooks.preToolUse, ...additionalPreToolUse],
|
|
43
|
-
postToolUse: [...base.hooks.postToolUse, ...additionalPostToolUse],
|
|
44
|
-
},
|
|
57
|
+
hooks: mergedHooks,
|
|
45
58
|
...(catalogResult.errors.length > 0 ? { catalogErrors: catalogResult.errors } : {}),
|
|
46
59
|
};
|
|
47
60
|
}
|
|
@@ -87,6 +87,94 @@ export declare const HooksConfigSchema: z.ZodObject<{
|
|
|
87
87
|
inline?: string | undefined;
|
|
88
88
|
variables?: Record<string, unknown> | undefined;
|
|
89
89
|
}>, "many">>;
|
|
90
|
+
sessionStart: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
91
|
+
id: z.ZodString;
|
|
92
|
+
matcher: z.ZodString;
|
|
93
|
+
description: z.ZodOptional<z.ZodString>;
|
|
94
|
+
script: z.ZodOptional<z.ZodString>;
|
|
95
|
+
inline: z.ZodOptional<z.ZodString>;
|
|
96
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
97
|
+
}, "strip", z.ZodTypeAny, {
|
|
98
|
+
id: string;
|
|
99
|
+
matcher: string;
|
|
100
|
+
description?: string | undefined;
|
|
101
|
+
script?: string | undefined;
|
|
102
|
+
inline?: string | undefined;
|
|
103
|
+
variables?: Record<string, unknown> | undefined;
|
|
104
|
+
}, {
|
|
105
|
+
id: string;
|
|
106
|
+
matcher: string;
|
|
107
|
+
description?: string | undefined;
|
|
108
|
+
script?: string | undefined;
|
|
109
|
+
inline?: string | undefined;
|
|
110
|
+
variables?: Record<string, unknown> | undefined;
|
|
111
|
+
}>, "many">>;
|
|
112
|
+
notification: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
113
|
+
id: z.ZodString;
|
|
114
|
+
matcher: z.ZodString;
|
|
115
|
+
description: z.ZodOptional<z.ZodString>;
|
|
116
|
+
script: z.ZodOptional<z.ZodString>;
|
|
117
|
+
inline: z.ZodOptional<z.ZodString>;
|
|
118
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
119
|
+
}, "strip", z.ZodTypeAny, {
|
|
120
|
+
id: string;
|
|
121
|
+
matcher: string;
|
|
122
|
+
description?: string | undefined;
|
|
123
|
+
script?: string | undefined;
|
|
124
|
+
inline?: string | undefined;
|
|
125
|
+
variables?: Record<string, unknown> | undefined;
|
|
126
|
+
}, {
|
|
127
|
+
id: string;
|
|
128
|
+
matcher: string;
|
|
129
|
+
description?: string | undefined;
|
|
130
|
+
script?: string | undefined;
|
|
131
|
+
inline?: string | undefined;
|
|
132
|
+
variables?: Record<string, unknown> | undefined;
|
|
133
|
+
}>, "many">>;
|
|
134
|
+
configChange: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
135
|
+
id: z.ZodString;
|
|
136
|
+
matcher: z.ZodString;
|
|
137
|
+
description: z.ZodOptional<z.ZodString>;
|
|
138
|
+
script: z.ZodOptional<z.ZodString>;
|
|
139
|
+
inline: z.ZodOptional<z.ZodString>;
|
|
140
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
141
|
+
}, "strip", z.ZodTypeAny, {
|
|
142
|
+
id: string;
|
|
143
|
+
matcher: string;
|
|
144
|
+
description?: string | undefined;
|
|
145
|
+
script?: string | undefined;
|
|
146
|
+
inline?: string | undefined;
|
|
147
|
+
variables?: Record<string, unknown> | undefined;
|
|
148
|
+
}, {
|
|
149
|
+
id: string;
|
|
150
|
+
matcher: string;
|
|
151
|
+
description?: string | undefined;
|
|
152
|
+
script?: string | undefined;
|
|
153
|
+
inline?: string | undefined;
|
|
154
|
+
variables?: Record<string, unknown> | undefined;
|
|
155
|
+
}>, "many">>;
|
|
156
|
+
worktreeCreate: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
157
|
+
id: z.ZodString;
|
|
158
|
+
matcher: z.ZodString;
|
|
159
|
+
description: z.ZodOptional<z.ZodString>;
|
|
160
|
+
script: z.ZodOptional<z.ZodString>;
|
|
161
|
+
inline: z.ZodOptional<z.ZodString>;
|
|
162
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
163
|
+
}, "strip", z.ZodTypeAny, {
|
|
164
|
+
id: string;
|
|
165
|
+
matcher: string;
|
|
166
|
+
description?: string | undefined;
|
|
167
|
+
script?: string | undefined;
|
|
168
|
+
inline?: string | undefined;
|
|
169
|
+
variables?: Record<string, unknown> | undefined;
|
|
170
|
+
}, {
|
|
171
|
+
id: string;
|
|
172
|
+
matcher: string;
|
|
173
|
+
description?: string | undefined;
|
|
174
|
+
script?: string | undefined;
|
|
175
|
+
inline?: string | undefined;
|
|
176
|
+
variables?: Record<string, unknown> | undefined;
|
|
177
|
+
}>, "many">>;
|
|
90
178
|
}, "strip", z.ZodTypeAny, {
|
|
91
179
|
preToolUse?: {
|
|
92
180
|
id: string;
|
|
@@ -104,6 +192,38 @@ export declare const HooksConfigSchema: z.ZodObject<{
|
|
|
104
192
|
inline?: string | undefined;
|
|
105
193
|
variables?: Record<string, unknown> | undefined;
|
|
106
194
|
}[] | undefined;
|
|
195
|
+
sessionStart?: {
|
|
196
|
+
id: string;
|
|
197
|
+
matcher: string;
|
|
198
|
+
description?: string | undefined;
|
|
199
|
+
script?: string | undefined;
|
|
200
|
+
inline?: string | undefined;
|
|
201
|
+
variables?: Record<string, unknown> | undefined;
|
|
202
|
+
}[] | undefined;
|
|
203
|
+
notification?: {
|
|
204
|
+
id: string;
|
|
205
|
+
matcher: string;
|
|
206
|
+
description?: string | undefined;
|
|
207
|
+
script?: string | undefined;
|
|
208
|
+
inline?: string | undefined;
|
|
209
|
+
variables?: Record<string, unknown> | undefined;
|
|
210
|
+
}[] | undefined;
|
|
211
|
+
configChange?: {
|
|
212
|
+
id: string;
|
|
213
|
+
matcher: string;
|
|
214
|
+
description?: string | undefined;
|
|
215
|
+
script?: string | undefined;
|
|
216
|
+
inline?: string | undefined;
|
|
217
|
+
variables?: Record<string, unknown> | undefined;
|
|
218
|
+
}[] | undefined;
|
|
219
|
+
worktreeCreate?: {
|
|
220
|
+
id: string;
|
|
221
|
+
matcher: string;
|
|
222
|
+
description?: string | undefined;
|
|
223
|
+
script?: string | undefined;
|
|
224
|
+
inline?: string | undefined;
|
|
225
|
+
variables?: Record<string, unknown> | undefined;
|
|
226
|
+
}[] | undefined;
|
|
107
227
|
}, {
|
|
108
228
|
preToolUse?: {
|
|
109
229
|
id: string;
|
|
@@ -121,6 +241,38 @@ export declare const HooksConfigSchema: z.ZodObject<{
|
|
|
121
241
|
inline?: string | undefined;
|
|
122
242
|
variables?: Record<string, unknown> | undefined;
|
|
123
243
|
}[] | undefined;
|
|
244
|
+
sessionStart?: {
|
|
245
|
+
id: string;
|
|
246
|
+
matcher: string;
|
|
247
|
+
description?: string | undefined;
|
|
248
|
+
script?: string | undefined;
|
|
249
|
+
inline?: string | undefined;
|
|
250
|
+
variables?: Record<string, unknown> | undefined;
|
|
251
|
+
}[] | undefined;
|
|
252
|
+
notification?: {
|
|
253
|
+
id: string;
|
|
254
|
+
matcher: string;
|
|
255
|
+
description?: string | undefined;
|
|
256
|
+
script?: string | undefined;
|
|
257
|
+
inline?: string | undefined;
|
|
258
|
+
variables?: Record<string, unknown> | undefined;
|
|
259
|
+
}[] | undefined;
|
|
260
|
+
configChange?: {
|
|
261
|
+
id: string;
|
|
262
|
+
matcher: string;
|
|
263
|
+
description?: string | undefined;
|
|
264
|
+
script?: string | undefined;
|
|
265
|
+
inline?: string | undefined;
|
|
266
|
+
variables?: Record<string, unknown> | undefined;
|
|
267
|
+
}[] | undefined;
|
|
268
|
+
worktreeCreate?: {
|
|
269
|
+
id: string;
|
|
270
|
+
matcher: string;
|
|
271
|
+
description?: string | undefined;
|
|
272
|
+
script?: string | undefined;
|
|
273
|
+
inline?: string | undefined;
|
|
274
|
+
variables?: Record<string, unknown> | undefined;
|
|
275
|
+
}[] | undefined;
|
|
124
276
|
}>;
|
|
125
277
|
export type HooksConfig = z.infer<typeof HooksConfigSchema>;
|
|
126
278
|
export declare const SettingsConfigSchema: z.ZodObject<{
|
|
@@ -238,6 +390,94 @@ export declare const PresetConfigSchema: z.ZodObject<{
|
|
|
238
390
|
inline?: string | undefined;
|
|
239
391
|
variables?: Record<string, unknown> | undefined;
|
|
240
392
|
}>, "many">>;
|
|
393
|
+
sessionStart: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
394
|
+
id: z.ZodString;
|
|
395
|
+
matcher: z.ZodString;
|
|
396
|
+
description: z.ZodOptional<z.ZodString>;
|
|
397
|
+
script: z.ZodOptional<z.ZodString>;
|
|
398
|
+
inline: z.ZodOptional<z.ZodString>;
|
|
399
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
400
|
+
}, "strip", z.ZodTypeAny, {
|
|
401
|
+
id: string;
|
|
402
|
+
matcher: string;
|
|
403
|
+
description?: string | undefined;
|
|
404
|
+
script?: string | undefined;
|
|
405
|
+
inline?: string | undefined;
|
|
406
|
+
variables?: Record<string, unknown> | undefined;
|
|
407
|
+
}, {
|
|
408
|
+
id: string;
|
|
409
|
+
matcher: string;
|
|
410
|
+
description?: string | undefined;
|
|
411
|
+
script?: string | undefined;
|
|
412
|
+
inline?: string | undefined;
|
|
413
|
+
variables?: Record<string, unknown> | undefined;
|
|
414
|
+
}>, "many">>;
|
|
415
|
+
notification: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
416
|
+
id: z.ZodString;
|
|
417
|
+
matcher: z.ZodString;
|
|
418
|
+
description: z.ZodOptional<z.ZodString>;
|
|
419
|
+
script: z.ZodOptional<z.ZodString>;
|
|
420
|
+
inline: z.ZodOptional<z.ZodString>;
|
|
421
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
422
|
+
}, "strip", z.ZodTypeAny, {
|
|
423
|
+
id: string;
|
|
424
|
+
matcher: string;
|
|
425
|
+
description?: string | undefined;
|
|
426
|
+
script?: string | undefined;
|
|
427
|
+
inline?: string | undefined;
|
|
428
|
+
variables?: Record<string, unknown> | undefined;
|
|
429
|
+
}, {
|
|
430
|
+
id: string;
|
|
431
|
+
matcher: string;
|
|
432
|
+
description?: string | undefined;
|
|
433
|
+
script?: string | undefined;
|
|
434
|
+
inline?: string | undefined;
|
|
435
|
+
variables?: Record<string, unknown> | undefined;
|
|
436
|
+
}>, "many">>;
|
|
437
|
+
configChange: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
438
|
+
id: z.ZodString;
|
|
439
|
+
matcher: z.ZodString;
|
|
440
|
+
description: z.ZodOptional<z.ZodString>;
|
|
441
|
+
script: z.ZodOptional<z.ZodString>;
|
|
442
|
+
inline: z.ZodOptional<z.ZodString>;
|
|
443
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
444
|
+
}, "strip", z.ZodTypeAny, {
|
|
445
|
+
id: string;
|
|
446
|
+
matcher: string;
|
|
447
|
+
description?: string | undefined;
|
|
448
|
+
script?: string | undefined;
|
|
449
|
+
inline?: string | undefined;
|
|
450
|
+
variables?: Record<string, unknown> | undefined;
|
|
451
|
+
}, {
|
|
452
|
+
id: string;
|
|
453
|
+
matcher: string;
|
|
454
|
+
description?: string | undefined;
|
|
455
|
+
script?: string | undefined;
|
|
456
|
+
inline?: string | undefined;
|
|
457
|
+
variables?: Record<string, unknown> | undefined;
|
|
458
|
+
}>, "many">>;
|
|
459
|
+
worktreeCreate: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
460
|
+
id: z.ZodString;
|
|
461
|
+
matcher: z.ZodString;
|
|
462
|
+
description: z.ZodOptional<z.ZodString>;
|
|
463
|
+
script: z.ZodOptional<z.ZodString>;
|
|
464
|
+
inline: z.ZodOptional<z.ZodString>;
|
|
465
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
466
|
+
}, "strip", z.ZodTypeAny, {
|
|
467
|
+
id: string;
|
|
468
|
+
matcher: string;
|
|
469
|
+
description?: string | undefined;
|
|
470
|
+
script?: string | undefined;
|
|
471
|
+
inline?: string | undefined;
|
|
472
|
+
variables?: Record<string, unknown> | undefined;
|
|
473
|
+
}, {
|
|
474
|
+
id: string;
|
|
475
|
+
matcher: string;
|
|
476
|
+
description?: string | undefined;
|
|
477
|
+
script?: string | undefined;
|
|
478
|
+
inline?: string | undefined;
|
|
479
|
+
variables?: Record<string, unknown> | undefined;
|
|
480
|
+
}>, "many">>;
|
|
241
481
|
}, "strip", z.ZodTypeAny, {
|
|
242
482
|
preToolUse?: {
|
|
243
483
|
id: string;
|
|
@@ -255,6 +495,38 @@ export declare const PresetConfigSchema: z.ZodObject<{
|
|
|
255
495
|
inline?: string | undefined;
|
|
256
496
|
variables?: Record<string, unknown> | undefined;
|
|
257
497
|
}[] | undefined;
|
|
498
|
+
sessionStart?: {
|
|
499
|
+
id: string;
|
|
500
|
+
matcher: string;
|
|
501
|
+
description?: string | undefined;
|
|
502
|
+
script?: string | undefined;
|
|
503
|
+
inline?: string | undefined;
|
|
504
|
+
variables?: Record<string, unknown> | undefined;
|
|
505
|
+
}[] | undefined;
|
|
506
|
+
notification?: {
|
|
507
|
+
id: string;
|
|
508
|
+
matcher: string;
|
|
509
|
+
description?: string | undefined;
|
|
510
|
+
script?: string | undefined;
|
|
511
|
+
inline?: string | undefined;
|
|
512
|
+
variables?: Record<string, unknown> | undefined;
|
|
513
|
+
}[] | undefined;
|
|
514
|
+
configChange?: {
|
|
515
|
+
id: string;
|
|
516
|
+
matcher: string;
|
|
517
|
+
description?: string | undefined;
|
|
518
|
+
script?: string | undefined;
|
|
519
|
+
inline?: string | undefined;
|
|
520
|
+
variables?: Record<string, unknown> | undefined;
|
|
521
|
+
}[] | undefined;
|
|
522
|
+
worktreeCreate?: {
|
|
523
|
+
id: string;
|
|
524
|
+
matcher: string;
|
|
525
|
+
description?: string | undefined;
|
|
526
|
+
script?: string | undefined;
|
|
527
|
+
inline?: string | undefined;
|
|
528
|
+
variables?: Record<string, unknown> | undefined;
|
|
529
|
+
}[] | undefined;
|
|
258
530
|
}, {
|
|
259
531
|
preToolUse?: {
|
|
260
532
|
id: string;
|
|
@@ -272,6 +544,38 @@ export declare const PresetConfigSchema: z.ZodObject<{
|
|
|
272
544
|
inline?: string | undefined;
|
|
273
545
|
variables?: Record<string, unknown> | undefined;
|
|
274
546
|
}[] | undefined;
|
|
547
|
+
sessionStart?: {
|
|
548
|
+
id: string;
|
|
549
|
+
matcher: string;
|
|
550
|
+
description?: string | undefined;
|
|
551
|
+
script?: string | undefined;
|
|
552
|
+
inline?: string | undefined;
|
|
553
|
+
variables?: Record<string, unknown> | undefined;
|
|
554
|
+
}[] | undefined;
|
|
555
|
+
notification?: {
|
|
556
|
+
id: string;
|
|
557
|
+
matcher: string;
|
|
558
|
+
description?: string | undefined;
|
|
559
|
+
script?: string | undefined;
|
|
560
|
+
inline?: string | undefined;
|
|
561
|
+
variables?: Record<string, unknown> | undefined;
|
|
562
|
+
}[] | undefined;
|
|
563
|
+
configChange?: {
|
|
564
|
+
id: string;
|
|
565
|
+
matcher: string;
|
|
566
|
+
description?: string | undefined;
|
|
567
|
+
script?: string | undefined;
|
|
568
|
+
inline?: string | undefined;
|
|
569
|
+
variables?: Record<string, unknown> | undefined;
|
|
570
|
+
}[] | undefined;
|
|
571
|
+
worktreeCreate?: {
|
|
572
|
+
id: string;
|
|
573
|
+
matcher: string;
|
|
574
|
+
description?: string | undefined;
|
|
575
|
+
script?: string | undefined;
|
|
576
|
+
inline?: string | undefined;
|
|
577
|
+
variables?: Record<string, unknown> | undefined;
|
|
578
|
+
}[] | undefined;
|
|
275
579
|
}>>;
|
|
276
580
|
settings: z.ZodOptional<z.ZodObject<{
|
|
277
581
|
permissions: z.ZodOptional<z.ZodObject<{
|
|
@@ -329,6 +633,38 @@ export declare const PresetConfigSchema: z.ZodObject<{
|
|
|
329
633
|
inline?: string | undefined;
|
|
330
634
|
variables?: Record<string, unknown> | undefined;
|
|
331
635
|
}[] | undefined;
|
|
636
|
+
sessionStart?: {
|
|
637
|
+
id: string;
|
|
638
|
+
matcher: string;
|
|
639
|
+
description?: string | undefined;
|
|
640
|
+
script?: string | undefined;
|
|
641
|
+
inline?: string | undefined;
|
|
642
|
+
variables?: Record<string, unknown> | undefined;
|
|
643
|
+
}[] | undefined;
|
|
644
|
+
notification?: {
|
|
645
|
+
id: string;
|
|
646
|
+
matcher: string;
|
|
647
|
+
description?: string | undefined;
|
|
648
|
+
script?: string | undefined;
|
|
649
|
+
inline?: string | undefined;
|
|
650
|
+
variables?: Record<string, unknown> | undefined;
|
|
651
|
+
}[] | undefined;
|
|
652
|
+
configChange?: {
|
|
653
|
+
id: string;
|
|
654
|
+
matcher: string;
|
|
655
|
+
description?: string | undefined;
|
|
656
|
+
script?: string | undefined;
|
|
657
|
+
inline?: string | undefined;
|
|
658
|
+
variables?: Record<string, unknown> | undefined;
|
|
659
|
+
}[] | undefined;
|
|
660
|
+
worktreeCreate?: {
|
|
661
|
+
id: string;
|
|
662
|
+
matcher: string;
|
|
663
|
+
description?: string | undefined;
|
|
664
|
+
script?: string | undefined;
|
|
665
|
+
inline?: string | undefined;
|
|
666
|
+
variables?: Record<string, unknown> | undefined;
|
|
667
|
+
}[] | undefined;
|
|
332
668
|
} | undefined;
|
|
333
669
|
settings?: {
|
|
334
670
|
permissions?: {
|
|
@@ -370,6 +706,38 @@ export declare const PresetConfigSchema: z.ZodObject<{
|
|
|
370
706
|
inline?: string | undefined;
|
|
371
707
|
variables?: Record<string, unknown> | undefined;
|
|
372
708
|
}[] | undefined;
|
|
709
|
+
sessionStart?: {
|
|
710
|
+
id: string;
|
|
711
|
+
matcher: string;
|
|
712
|
+
description?: string | undefined;
|
|
713
|
+
script?: string | undefined;
|
|
714
|
+
inline?: string | undefined;
|
|
715
|
+
variables?: Record<string, unknown> | undefined;
|
|
716
|
+
}[] | undefined;
|
|
717
|
+
notification?: {
|
|
718
|
+
id: string;
|
|
719
|
+
matcher: string;
|
|
720
|
+
description?: string | undefined;
|
|
721
|
+
script?: string | undefined;
|
|
722
|
+
inline?: string | undefined;
|
|
723
|
+
variables?: Record<string, unknown> | undefined;
|
|
724
|
+
}[] | undefined;
|
|
725
|
+
configChange?: {
|
|
726
|
+
id: string;
|
|
727
|
+
matcher: string;
|
|
728
|
+
description?: string | undefined;
|
|
729
|
+
script?: string | undefined;
|
|
730
|
+
inline?: string | undefined;
|
|
731
|
+
variables?: Record<string, unknown> | undefined;
|
|
732
|
+
}[] | undefined;
|
|
733
|
+
worktreeCreate?: {
|
|
734
|
+
id: string;
|
|
735
|
+
matcher: string;
|
|
736
|
+
description?: string | undefined;
|
|
737
|
+
script?: string | undefined;
|
|
738
|
+
inline?: string | undefined;
|
|
739
|
+
variables?: Record<string, unknown> | undefined;
|
|
740
|
+
}[] | undefined;
|
|
373
741
|
} | undefined;
|
|
374
742
|
settings?: {
|
|
375
743
|
permissions?: {
|
|
@@ -20,6 +20,10 @@ export const ClaudeMdSectionSchema = z.object({
|
|
|
20
20
|
export const HooksConfigSchema = z.object({
|
|
21
21
|
preToolUse: z.array(HookDefinitionSchema).optional(),
|
|
22
22
|
postToolUse: z.array(HookDefinitionSchema).optional(),
|
|
23
|
+
sessionStart: z.array(HookDefinitionSchema).optional(),
|
|
24
|
+
notification: z.array(HookDefinitionSchema).optional(),
|
|
25
|
+
configChange: z.array(HookDefinitionSchema).optional(),
|
|
26
|
+
worktreeCreate: z.array(HookDefinitionSchema).optional(),
|
|
23
27
|
});
|
|
24
28
|
// --- Settings Config ---
|
|
25
29
|
export const SettingsConfigSchema = z.object({
|
|
@@ -12,6 +12,7 @@ import { dartDetector } from "./dart.js";
|
|
|
12
12
|
import { elixirDetector } from "./elixir.js";
|
|
13
13
|
import { scalaDetector } from "./scala.js";
|
|
14
14
|
import { zigDetector } from "./zig.js";
|
|
15
|
+
import { terraformDetector } from "./terraform.js";
|
|
15
16
|
export const allDetectors = [
|
|
16
17
|
nodeDetector,
|
|
17
18
|
pythonDetector,
|
|
@@ -27,4 +28,5 @@ export const allDetectors = [
|
|
|
27
28
|
elixirDetector,
|
|
28
29
|
scalaDetector,
|
|
29
30
|
zigDetector,
|
|
31
|
+
terraformDetector,
|
|
30
32
|
];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const terraformDetector = {
|
|
4
|
+
name: "terraform",
|
|
5
|
+
detect: async (projectDir) => {
|
|
6
|
+
// Check for *.tf files
|
|
7
|
+
let hasTfFiles = false;
|
|
8
|
+
const detectedFiles = [];
|
|
9
|
+
try {
|
|
10
|
+
const entries = await fs.readdir(projectDir);
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
if (entry.endsWith(".tf")) {
|
|
13
|
+
hasTfFiles = true;
|
|
14
|
+
detectedFiles.push(entry);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
// Fallback: check for .terraform.lock.hcl
|
|
22
|
+
if (!hasTfFiles) {
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(path.join(projectDir, ".terraform.lock.hcl"));
|
|
25
|
+
hasTfFiles = true;
|
|
26
|
+
detectedFiles.push(".terraform.lock.hcl");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// not found
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!hasTfFiles) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
languages: ["hcl"],
|
|
37
|
+
frameworks: ["terraform"],
|
|
38
|
+
packageManagers: ["terraform"],
|
|
39
|
+
testCommands: ["terraform test"],
|
|
40
|
+
lintCommands: ["tflint"],
|
|
41
|
+
buildCommands: ["terraform plan"],
|
|
42
|
+
blockedPaths: [".terraform/", "*.tfstate", "*.tfstate.backup"],
|
|
43
|
+
detectedFiles,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
package/dist/generators/hooks.js
CHANGED
|
@@ -35,22 +35,34 @@ export function wrapWithLogger(script, event = "unknown") {
|
|
|
35
35
|
export async function generateHooks(options) {
|
|
36
36
|
const { projectDir, config } = options;
|
|
37
37
|
const hooksDir = join(projectDir, ".claude/hooks");
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const eventMap = [
|
|
39
|
+
["PreToolUse", config.hooks.preToolUse],
|
|
40
|
+
["PostToolUse", config.hooks.postToolUse],
|
|
41
|
+
["SessionStart", config.hooks.sessionStart ?? []],
|
|
42
|
+
["Notification", config.hooks.notification ?? []],
|
|
43
|
+
["ConfigChange", config.hooks.configChange ?? []],
|
|
44
|
+
["WorktreeCreate", config.hooks.worktreeCreate ?? []],
|
|
41
45
|
];
|
|
46
|
+
const allHooks = eventMap.flatMap(([event, hooks]) => hooks.map((h) => ({ ...h, event })));
|
|
42
47
|
if (allHooks.length === 0) {
|
|
43
48
|
return { hooksConfig: {}, generatedFiles: [] };
|
|
44
49
|
}
|
|
45
50
|
await mkdir(hooksDir, { recursive: true });
|
|
46
51
|
const generatedFiles = [];
|
|
47
52
|
const hooksConfig = {};
|
|
53
|
+
const usedScriptNames = new Set();
|
|
48
54
|
for (const hook of allHooks) {
|
|
49
55
|
if (!hook.inline) {
|
|
50
56
|
continue;
|
|
51
57
|
}
|
|
52
58
|
const safeId = hook.id.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
53
|
-
|
|
59
|
+
let scriptName = `${safeId}.sh`;
|
|
60
|
+
// Prevent collisions when different events share the same hook.id
|
|
61
|
+
if (usedScriptNames.has(scriptName)) {
|
|
62
|
+
const safeEvent = hook.event.replace(/[^a-zA-Z0-9_-]/g, "").toLowerCase();
|
|
63
|
+
scriptName = `${safeEvent}-${safeId}.sh`;
|
|
64
|
+
}
|
|
65
|
+
usedScriptNames.add(scriptName);
|
|
54
66
|
const scriptPath = join(hooksDir, scriptName);
|
|
55
67
|
const wrappedScript = wrapWithLogger(hook.inline, hook.event);
|
|
56
68
|
await writeFile(scriptPath, wrappedScript, "utf8");
|
|
@@ -58,7 +70,7 @@ export async function generateHooks(options) {
|
|
|
58
70
|
generatedFiles.push(scriptPath);
|
|
59
71
|
const entry = {
|
|
60
72
|
matcher: hook.matcher,
|
|
61
|
-
hooks: [{ type: "command", command: `bash .claude/hooks/${
|
|
73
|
+
hooks: [{ type: "command", command: `bash .claude/hooks/${scriptName}` }],
|
|
62
74
|
};
|
|
63
75
|
if (!hooksConfig[hook.event]) {
|
|
64
76
|
hooksConfig[hook.event] = [];
|
|
@@ -17,6 +17,9 @@ export async function generateSettings(options) {
|
|
|
17
17
|
const existingPermissions = (existing.permissions ?? {});
|
|
18
18
|
const mergedAllow = Array.from(new Set([...(existingPermissions.allow ?? []), ...(config.settings.permissions.allow ?? [])]));
|
|
19
19
|
const mergedDeny = Array.from(new Set([...(existingPermissions.deny ?? []), ...(config.settings.permissions.deny ?? [])]));
|
|
20
|
+
// Preserve managedAt if content is unchanged
|
|
21
|
+
const existingMeta = (existing._ohMyHarness ?? {});
|
|
22
|
+
const previousManagedAt = existingMeta.managedAt;
|
|
20
23
|
const result = {
|
|
21
24
|
...existing,
|
|
22
25
|
permissions: {
|
|
@@ -26,10 +29,27 @@ export async function generateSettings(options) {
|
|
|
26
29
|
},
|
|
27
30
|
hooks: hooksOutput.hooksConfig,
|
|
28
31
|
_ohMyHarness: {
|
|
29
|
-
managedAt:
|
|
32
|
+
managedAt: "__PLACEHOLDER__",
|
|
30
33
|
presets: config.presets,
|
|
31
34
|
},
|
|
32
35
|
};
|
|
36
|
+
// Compare content without timestamp to decide if managedAt should update
|
|
37
|
+
const newContent = JSON.stringify(result, null, 2) + "\n";
|
|
38
|
+
const oldMeta = existing._ohMyHarness && typeof existing._ohMyHarness === "object" && !Array.isArray(existing._ohMyHarness)
|
|
39
|
+
? { ...existing._ohMyHarness }
|
|
40
|
+
: {};
|
|
41
|
+
const oldResultForCompare = {
|
|
42
|
+
...existing,
|
|
43
|
+
_ohMyHarness: oldMeta,
|
|
44
|
+
};
|
|
45
|
+
if (oldMeta.managedAt) {
|
|
46
|
+
oldMeta.managedAt = "__PLACEHOLDER__";
|
|
47
|
+
}
|
|
48
|
+
const oldContent = JSON.stringify(oldResultForCompare, null, 2) + "\n";
|
|
49
|
+
const managedAt = newContent === oldContent && previousManagedAt
|
|
50
|
+
? previousManagedAt
|
|
51
|
+
: new Date().toISOString();
|
|
52
|
+
result._ohMyHarness.managedAt = managedAt;
|
|
33
53
|
await fs.mkdir(claudeDir, { recursive: true });
|
|
34
54
|
await fs.writeFile(settingsPath, JSON.stringify(result, null, 2) + "\n", "utf-8");
|
|
35
55
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: terraform
|
|
2
|
+
displayName: "Terraform"
|
|
3
|
+
description: "Infrastructure as Code with Terraform/OpenTofu, tflint, and tfsec"
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
extends: ["_base"]
|
|
6
|
+
tags: ["terraform", "iac", "infrastructure", "hcl"]
|
|
7
|
+
variables:
|
|
8
|
+
language: "hcl"
|
|
9
|
+
framework: "terraform"
|
|
10
|
+
testRunner: "terraform test"
|
|
11
|
+
linter: "tflint"
|
|
12
|
+
claudeMd:
|
|
13
|
+
sections:
|
|
14
|
+
- id: "terraform-rules"
|
|
15
|
+
title: "Terraform Rules"
|
|
16
|
+
content: |
|
|
17
|
+
## Terraform Development Rules
|
|
18
|
+
|
|
19
|
+
- Run `terraform fmt` before every commit
|
|
20
|
+
- Run `terraform validate` to check syntax before applying
|
|
21
|
+
- NEVER modify `.tfstate` files directly — use `terraform state` commands
|
|
22
|
+
- NEVER hardcode secrets in `.tf` or `.tfvars` files — use variables with sensitive flag
|
|
23
|
+
- Use `terraform plan` to preview changes before `terraform apply`
|
|
24
|
+
- NEVER use `terraform apply -auto-approve` without explicit user confirmation
|
|
25
|
+
- Use consistent naming: snake_case for resources, variables, and outputs
|
|
26
|
+
- Pin provider versions in `required_providers` block
|
|
27
|
+
- Use modules for reusable infrastructure components
|
|
28
|
+
priority: 20
|
|
29
|
+
- id: "terraform-testing"
|
|
30
|
+
title: "Terraform Testing"
|
|
31
|
+
content: |
|
|
32
|
+
## Terraform Testing Rules
|
|
33
|
+
|
|
34
|
+
- Write tests in `tests/` directory using `terraform test` (HCL-based)
|
|
35
|
+
- Validate all modules with `terraform validate` before commit
|
|
36
|
+
- Run `tflint` to catch common misconfigurations
|
|
37
|
+
- Run `tfsec` for security scanning
|
|
38
|
+
- Use `terraform plan` as a smoke test for syntax and dependency checks
|
|
39
|
+
priority: 21
|
|
40
|
+
- id: "terraform-structure"
|
|
41
|
+
title: "Terraform Structure"
|
|
42
|
+
content: |
|
|
43
|
+
## Terraform Project Structure
|
|
44
|
+
|
|
45
|
+
- `main.tf` — Primary resource definitions
|
|
46
|
+
- `variables.tf` — Input variable declarations
|
|
47
|
+
- `outputs.tf` — Output value declarations
|
|
48
|
+
- `providers.tf` — Provider configuration and version constraints
|
|
49
|
+
- `terraform.tfvars` — Variable values (DO NOT commit secrets)
|
|
50
|
+
- `modules/` — Reusable module definitions
|
|
51
|
+
- `tests/` — Terraform test files
|
|
52
|
+
priority: 22
|
|
53
|
+
settings:
|
|
54
|
+
permissions:
|
|
55
|
+
allow:
|
|
56
|
+
- "Bash(terraform *)"
|
|
57
|
+
- "Bash(tofu *)"
|
|
58
|
+
- "Bash(tflint *)"
|
|
59
|
+
- "Bash(tfsec *)"
|
|
60
|
+
- "Bash(terraform-docs *)"
|
|
61
|
+
deny:
|
|
62
|
+
- "Bash(sudo *)"
|
|
63
|
+
- "Bash(rm -rf *)"
|
|
64
|
+
- "Bash(terraform apply -auto-approve*)"
|
|
65
|
+
- "Bash(terraform apply --auto-approve*)"
|