oh-my-harness 0.7.0 → 0.9.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/tdd-guard.js +5 -2
- 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/cli/tui/init-flow.js +15 -3
- package/dist/cli/tui/provider-setup.d.ts +2 -0
- package/dist/cli/tui/provider-setup.js +85 -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/node.js +1 -1
- 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/dist/nl/config-store.d.ts +11 -0
- package/dist/nl/config-store.js +33 -0
- package/dist/nl/parse-intent.d.ts +8 -2
- package/dist/nl/parse-intent.js +17 -30
- package/dist/nl/provider-registry.d.ts +22 -0
- package/dist/nl/provider-registry.js +90 -0
- package/dist/nl/providers/claude-api.d.ts +2 -0
- package/dist/nl/providers/claude-api.js +42 -0
- package/dist/nl/providers/claude-cli.d.ts +2 -0
- package/dist/nl/providers/claude-cli.js +40 -0
- package/dist/nl/providers/gemini-api.d.ts +2 -0
- package/dist/nl/providers/gemini-api.js +44 -0
- package/dist/nl/providers/openai-api.d.ts +2 -0
- package/dist/nl/providers/openai-api.js +41 -0
- 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
|
+
};
|
|
@@ -72,8 +72,11 @@ fi
|
|
|
72
72
|
|
|
73
73
|
# edit-history에서 테스트 파일 검색
|
|
74
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
|
|
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
|
|
77
80
|
if [[ -n "\$UPDATED" ]]; then
|
|
78
81
|
echo "\$UPDATED" > "\$HISTORY_FILE"
|
|
79
82
|
fi
|
|
@@ -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 }));
|
|
@@ -9,7 +9,9 @@ import { PresetRegistry } from "../../core/preset-registry.js";
|
|
|
9
9
|
import { loadAndMergePresets, writeHarnessState } from "../commands/init.js";
|
|
10
10
|
import { mergePresets } from "../../core/config-merger.js";
|
|
11
11
|
import { generate } from "../../core/generator.js";
|
|
12
|
-
import { generateHarnessConfig } from "../../nl/parse-intent.js";
|
|
12
|
+
import { generateHarnessConfig, createDefaultRunner } from "../../nl/parse-intent.js";
|
|
13
|
+
import { hasProviderConfig } from "../../nl/config-store.js";
|
|
14
|
+
import { runProviderSetup } from "./provider-setup.js";
|
|
13
15
|
import { mergeEnforcementAndHooks } from "../../core/harness-converter.js";
|
|
14
16
|
import { HarnessConfigSchema } from "../../core/harness-schema.js";
|
|
15
17
|
import { detectProject } from "../../detector/project-detector.js";
|
|
@@ -187,7 +189,17 @@ export async function runInitTUI(options) {
|
|
|
187
189
|
let harnessConfig;
|
|
188
190
|
let presetNames;
|
|
189
191
|
if (mode === "nl") {
|
|
190
|
-
// Step 4a: NL Mode
|
|
192
|
+
// Step 4a: NL Mode — check provider config
|
|
193
|
+
const hasConfig = await hasProviderConfig();
|
|
194
|
+
if (!hasConfig) {
|
|
195
|
+
p.log.info("No AI provider configured yet. Let's set one up.");
|
|
196
|
+
const providerConfig = await runProviderSetup();
|
|
197
|
+
if (!providerConfig) {
|
|
198
|
+
p.cancel("Provider setup cancelled. Use preset mode instead.");
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const runner = await createDefaultRunner();
|
|
191
203
|
const description = await p.text({
|
|
192
204
|
message: "Describe your project:",
|
|
193
205
|
placeholder: "e.g., Next.js e-commerce app with Stripe and Tailwind",
|
|
@@ -208,7 +220,7 @@ export async function runInitTUI(options) {
|
|
|
208
220
|
description: b.description,
|
|
209
221
|
params: b.params.map((pp) => ({ name: pp.name, required: pp.required, default: pp.default, description: pp.description })),
|
|
210
222
|
}));
|
|
211
|
-
harnessConfig = await generateHarnessConfig(description,
|
|
223
|
+
harnessConfig = await generateHarnessConfig(description, runner, catalogBlocks, projectFacts);
|
|
212
224
|
genSpinner.stop("Configuration generated");
|
|
213
225
|
}
|
|
214
226
|
catch (err) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { getAvailableProviders, getProviderDefinition, } from "../../nl/provider-registry.js";
|
|
3
|
+
import { saveProviderConfig, } from "../../nl/config-store.js";
|
|
4
|
+
export async function runProviderSetup() {
|
|
5
|
+
p.intro("AI Provider Setup");
|
|
6
|
+
const providers = getAvailableProviders();
|
|
7
|
+
// Step 1: Select provider
|
|
8
|
+
const providerName = await p.select({
|
|
9
|
+
message: "Select AI provider for natural language mode:",
|
|
10
|
+
options: providers.map((prov) => ({
|
|
11
|
+
value: prov.name,
|
|
12
|
+
label: prov.displayName,
|
|
13
|
+
})),
|
|
14
|
+
});
|
|
15
|
+
if (p.isCancel(providerName)) {
|
|
16
|
+
p.cancel("Provider setup cancelled.");
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const def = getProviderDefinition(providerName);
|
|
20
|
+
// Step 2: Select method (CLI or API)
|
|
21
|
+
let method;
|
|
22
|
+
if (def.supportsCli && def.supportsApi) {
|
|
23
|
+
const selected = await p.select({
|
|
24
|
+
message: "How would you like to connect?",
|
|
25
|
+
options: [
|
|
26
|
+
{ value: "cli", label: `CLI tool (${def.cliCommand ?? def.name})` },
|
|
27
|
+
{ value: "api", label: "API Key" },
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
if (p.isCancel(selected)) {
|
|
31
|
+
p.cancel("Provider setup cancelled.");
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
method = selected;
|
|
35
|
+
}
|
|
36
|
+
else if (def.supportsCli) {
|
|
37
|
+
method = "cli";
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
method = "api";
|
|
41
|
+
}
|
|
42
|
+
const config = {
|
|
43
|
+
provider: providerName,
|
|
44
|
+
method,
|
|
45
|
+
};
|
|
46
|
+
// Step 3: Get API key if needed
|
|
47
|
+
if (method === "api") {
|
|
48
|
+
const apiKey = await p.text({
|
|
49
|
+
message: `Enter your ${def.displayName} API key:`,
|
|
50
|
+
placeholder: "sk-...",
|
|
51
|
+
validate: (value) => {
|
|
52
|
+
if (!value || !value.trim())
|
|
53
|
+
return "API key is required";
|
|
54
|
+
return undefined;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
if (p.isCancel(apiKey)) {
|
|
58
|
+
p.cancel("Provider setup cancelled.");
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
config.apiKey = apiKey;
|
|
62
|
+
// Select model from available list
|
|
63
|
+
const selectedModel = await p.select({
|
|
64
|
+
message: "Select model:",
|
|
65
|
+
options: def.availableModels.map((m) => ({
|
|
66
|
+
value: m.id,
|
|
67
|
+
label: m.label,
|
|
68
|
+
hint: m.id === def.defaultModel ? "default" : undefined,
|
|
69
|
+
})),
|
|
70
|
+
initialValue: def.defaultModel,
|
|
71
|
+
});
|
|
72
|
+
if (p.isCancel(selectedModel)) {
|
|
73
|
+
p.cancel("Provider setup cancelled.");
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
config.model = selectedModel;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
config.cliCommand = def.cliCommand ?? def.name;
|
|
80
|
+
}
|
|
81
|
+
// Save config
|
|
82
|
+
await saveProviderConfig(config);
|
|
83
|
+
p.log.success(`Provider saved: ${def.displayName} (${method})`);
|
|
84
|
+
return config;
|
|
85
|
+
}
|