Skip to content

Commit 9879e5a

Browse files
committed
fix(runtime): split plan-to-git hook templates
1 parent 8a14af1 commit 9879e5a

4 files changed

Lines changed: 466 additions & 422 deletions

File tree

packages/app/src/lib/core/templates-entrypoint/git-hooks.ts

Lines changed: 10 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { renderEntrypointGitPostPushWrapperInstall } from "./git-post-push-wrapper.js"
2+
import {
3+
renderPlanToGitAgentHooksInstall,
4+
renderPlanToGitHookPaths,
5+
renderPlanToGitPostPushSync,
6+
renderPlanToGitSyncHelperInstall
7+
} from "./plan-to-git.js"
28
import { renderPostPushPrEnsure } from "./post-push-pr.js"
39

410
const entrypointGitHooksTemplate = String
511
.raw`# 3) Install global git hooks to protect main/master + managed AGENTS context
612
HOOKS_DIR="/opt/docker-git/hooks"
713
PRE_PUSH_HOOK="$HOOKS_DIR/pre-push"
814
POST_PUSH_ACTION="$HOOKS_DIR/post-push"
9-
PLAN_TO_GIT_SYNC_HELPER="$HOOKS_DIR/plan-to-git-sync"
10-
PLAN_TO_GIT_CODEX_HOOK="$HOOKS_DIR/plan-to-git-codex-hook"
11-
PLAN_TO_GIT_CLAUDE_HOOK="$HOOKS_DIR/plan-to-git-claude-hook"
12-
CODEX_REQUIREMENTS_FILE="/etc/codex/requirements.toml"
13-
CLAUDE_PLAN_TO_GIT_SETTINGS_FILE="$CLAUDE_CONFIG_DIR/settings.json"
15+
${renderPlanToGitHookPaths()}
1416
mkdir -p "$HOOKS_DIR"
1517
1618
cat <<'EOF' > "$PRE_PUSH_HOOK"
@@ -138,74 +140,7 @@ done
138140
EOF
139141
chmod 0755 "$PRE_PUSH_HOOK"
140142
141-
cat <<'EOF' > "$PLAN_TO_GIT_SYNC_HELPER"
142-
#!/usr/bin/env bash
143-
set -euo pipefail
144-
145-
if [ "${"${"}DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" = "1" ]; then
146-
exit 0
147-
fi
148-
149-
if ! command -v plan-to-git >/dev/null 2>&1; then
150-
echo "[plan-to-git] Error: plan-to-git not found" >&2
151-
exit 1
152-
fi
153-
154-
export PLAN_TO_GIT_STATE_DIR="${"${"}PLAN_TO_GIT_STATE_DIR:-/tmp/plan-to-git}"
155-
156-
docker_git_plan_to_git_explicit_pr_supported() {
157-
plan-to-git sync --help 2>/dev/null | grep -q -- "--pr <PR>"
158-
}
159-
160-
docker_git_plan_to_git_resolve_pr_number() {
161-
local candidate=""
162-
local key=""
163-
for key in DOCKER_GIT_PR_NUMBER PR_NUMBER GITHUB_PR_NUMBER; do
164-
candidate="${"${"}!key:-}"
165-
if [[ "$candidate" =~ ^[0-9]+$ ]]; then
166-
printf "%s\n" "$candidate"
167-
return 0
168-
fi
169-
done
170-
171-
candidate="${"${"}REPO_REF:-}"
172-
if [[ "$candidate" =~ ^refs/pull/([0-9]+)/head$ ]]; then
173-
printf "%s\n" "${"${"}BASH_REMATCH[1]}"
174-
return 0
175-
fi
176-
if [[ "$candidate" =~ ^pull/([0-9]+)$ ]]; then
177-
printf "%s\n" "${"${"}BASH_REMATCH[1]}"
178-
return 0
179-
fi
180-
181-
if command -v gh >/dev/null 2>&1; then
182-
candidate="$(gh pr view --json number --jq .number 2>/dev/null || true)"
183-
if [[ "$candidate" =~ ^[0-9]+$ ]]; then
184-
printf "%s\n" "$candidate"
185-
return 0
186-
fi
187-
fi
188-
189-
return 0
190-
}
191-
192-
docker_git_plan_to_git_sync() {
193-
local pr_number=""
194-
pr_number="$(docker_git_plan_to_git_resolve_pr_number || true)"
195-
196-
if [[ -n "$pr_number" ]] && docker_git_plan_to_git_explicit_pr_supported; then
197-
echo "[plan-to-git] Syncing queued agent plans to PR #$pr_number"
198-
plan-to-git sync --pr "$pr_number"
199-
return 0
200-
fi
201-
202-
echo "[plan-to-git] Syncing queued agent plans via current branch discovery"
203-
plan-to-git sync
204-
}
205-
206-
docker_git_plan_to_git_sync
207-
EOF
208-
chmod 0755 "$PLAN_TO_GIT_SYNC_HELPER"
143+
${renderPlanToGitSyncHelperInstall()}
209144
210145
cat <<'EOF' > "$POST_PUSH_ACTION"
211146
#!/usr/bin/env bash
@@ -220,25 +155,7 @@ cd "$REPO_ROOT"
220155
221156
${renderPostPushPrEnsure()}
222157
223-
# CHANGE: backfill agent session plans before syncing the current branch or explicit PR.
224-
# WHY: live agent hooks can be unavailable in already-running sessions; session logs are the durable fallback.
225-
# QUOTE(ТЗ): "что бы всё уходило на гитхаб автоматически"
226-
# REF: issue-397
227-
if [ "${"${"}DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" != "1" ]; then
228-
if ! command -v plan-to-git >/dev/null 2>&1; then
229-
echo "[plan-to-git] Error: plan-to-git not found" >&2
230-
exit 1
231-
fi
232-
plan-to-git import-codex --no-sync
233-
plan-to-git import-claude --no-sync
234-
PLAN_TO_GIT_SYNC_HELPER="${"${"}DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER:-/opt/docker-git/hooks/plan-to-git-sync}"
235-
if [[ -x "$PLAN_TO_GIT_SYNC_HELPER" ]]; then
236-
"$PLAN_TO_GIT_SYNC_HELPER"
237-
else
238-
echo "[plan-to-git] Sync helper not found; falling back to current branch discovery" >&2
239-
plan-to-git sync
240-
fi
241-
fi
158+
${renderPlanToGitPostPushSync()}
242159
243160
# CHANGE: keep post-push backup logic in a reusable action script
244161
# WHY: git has no client-side post-push hook, so the global git wrapper
@@ -258,125 +175,7 @@ fi
258175
EOF
259176
chmod 0755 "$POST_PUSH_ACTION"
260177
261-
cat <<'EOF' > "$PLAN_TO_GIT_CODEX_HOOK"
262-
#!/usr/bin/env bash
263-
set -euo pipefail
264-
265-
if [ "${"${"}DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" = "1" ]; then
266-
exit 0
267-
fi
268-
269-
if ! command -v plan-to-git >/dev/null 2>&1; then
270-
echo "[plan-to-git] Error: plan-to-git not found" >&2
271-
exit 1
272-
fi
273-
274-
export PLAN_TO_GIT_STATE_DIR="${"${"}PLAN_TO_GIT_STATE_DIR:-/tmp/plan-to-git}"
275-
plan-to-git hook --source codex
276-
PLAN_TO_GIT_SYNC_HELPER="${"${"}DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER:-/opt/docker-git/hooks/plan-to-git-sync}"
277-
"$PLAN_TO_GIT_SYNC_HELPER" >&2 || true
278-
EOF
279-
chmod 0755 "$PLAN_TO_GIT_CODEX_HOOK"
280-
281-
cat <<'EOF' > "$PLAN_TO_GIT_CLAUDE_HOOK"
282-
#!/usr/bin/env bash
283-
set -euo pipefail
284-
285-
if [ "${"${"}DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" = "1" ]; then
286-
exit 0
287-
fi
288-
289-
if ! command -v plan-to-git >/dev/null 2>&1; then
290-
echo "[plan-to-git] Error: plan-to-git not found" >&2
291-
exit 1
292-
fi
293-
294-
export PLAN_TO_GIT_STATE_DIR="${"${"}PLAN_TO_GIT_STATE_DIR:-/tmp/plan-to-git}"
295-
plan-to-git hook --source claude
296-
PLAN_TO_GIT_SYNC_HELPER="${"${"}DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER:-/opt/docker-git/hooks/plan-to-git-sync}"
297-
"$PLAN_TO_GIT_SYNC_HELPER" >&2 || true
298-
EOF
299-
chmod 0755 "$PLAN_TO_GIT_CLAUDE_HOOK"
300-
301-
mkdir -p "$(dirname "$CODEX_REQUIREMENTS_FILE")"
302-
cat <<'EOF' > "$CODEX_REQUIREMENTS_FILE"
303-
# docker-git managed Codex requirements
304-
305-
[features]
306-
hooks = true
307-
308-
[hooks]
309-
managed_dir = "/opt/docker-git/hooks"
310-
311-
[[hooks.UserPromptSubmit]]
312-
[[hooks.UserPromptSubmit.hooks]]
313-
type = "command"
314-
command = "/opt/docker-git/hooks/plan-to-git-codex-hook"
315-
statusMessage = "Capturing plan decision"
316-
317-
[[hooks.Stop]]
318-
[[hooks.Stop.hooks]]
319-
type = "command"
320-
command = "/opt/docker-git/hooks/plan-to-git-codex-hook"
321-
statusMessage = "Capturing agent plan"
322-
EOF
323-
chmod 0644 "$CODEX_REQUIREMENTS_FILE"
324-
325-
docker_git_install_claude_plan_to_git_hooks() {
326-
if [ "${"${"}DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" = "1" ]; then
327-
return 0
328-
fi
329-
330-
CLAUDE_PLAN_TO_GIT_SETTINGS_FILE="${"${"}CLAUDE_PLAN_TO_GIT_SETTINGS_FILE:-${"${"}CLAUDE_CONFIG_DIR:-/home/dev/.claude}/settings.json}"
331-
CLAUDE_PLAN_TO_GIT_SETTINGS_FILE="$CLAUDE_PLAN_TO_GIT_SETTINGS_FILE" PLAN_TO_GIT_CLAUDE_HOOK="$PLAN_TO_GIT_CLAUDE_HOOK" node - <<'NODE'
332-
const fs = require("node:fs")
333-
const path = require("node:path")
334-
335-
const settingsPath = process.env.CLAUDE_PLAN_TO_GIT_SETTINGS_FILE
336-
const hookCommand = process.env.PLAN_TO_GIT_CLAUDE_HOOK || "/opt/docker-git/hooks/plan-to-git-claude-hook"
337-
if (typeof settingsPath !== "string" || settingsPath.length === 0) {
338-
process.exit(0)
339-
}
340-
341-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
342-
343-
let settings = {}
344-
try {
345-
const parsed = JSON.parse(fs.readFileSync(settingsPath, "utf8"))
346-
settings = isRecord(parsed) ? parsed : {}
347-
} catch {
348-
settings = {}
349-
}
350-
351-
const currentHooks = isRecord(settings.hooks) ? settings.hooks : {}
352-
const nextHooks = { ...currentHooks }
353-
const managedHook = { type: "command", command: hookCommand }
354-
const ensureEventHook = (eventName) => {
355-
const currentEventHooks = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : []
356-
const alreadyInstalled = currentEventHooks.some((entry) =>
357-
isRecord(entry) &&
358-
Array.isArray(entry.hooks) &&
359-
entry.hooks.some((hook) => isRecord(hook) && hook.type === "command" && hook.command === hookCommand)
360-
)
361-
nextHooks[eventName] = alreadyInstalled ? currentEventHooks : [...currentEventHooks, { hooks: [managedHook] }]
362-
}
363-
364-
ensureEventHook("UserPromptSubmit")
365-
ensureEventHook("Stop")
366-
367-
const nextSettings = { ...settings, hooks: nextHooks }
368-
if (JSON.stringify(settings) === JSON.stringify(nextSettings)) {
369-
process.exit(0)
370-
}
371-
372-
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
373-
fs.writeFileSync(settingsPath, JSON.stringify(nextSettings, null, 2) + "\n", { mode: 0o600 })
374-
NODE
375-
chmod 0600 "$CLAUDE_PLAN_TO_GIT_SETTINGS_FILE" 2>/dev/null || true
376-
chown 1000:1000 "$CLAUDE_PLAN_TO_GIT_SETTINGS_FILE" 2>/dev/null || true
377-
}
378-
379-
docker_git_install_claude_plan_to_git_hooks
178+
${renderPlanToGitAgentHooksInstall()}
380179
381180
${renderEntrypointGitPostPushWrapperInstall()}
382181

0 commit comments

Comments
 (0)