snapshot of https://github.com/marmelab/atomic-crm.git (main, 2026-09-18; blobs > 131072B omitted)
Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
This commit is contained in:
170
.claude/hooks/lib/context.mjs
Normal file
170
.claude/hooks/lib/context.mjs
Normal file
@@ -0,0 +1,170 @@
|
||||
// Build a hook context from a hook's stdin payload: identity, session-scoped
|
||||
// paths, a logger, and terminal accept/block/fail helpers. The `name` is the
|
||||
// hook's label; log() and error() prefix every line with `[name]`, and
|
||||
// accept/block/fail add the `ACCEPT|BLOCK|FAIL` verb on top — so call sites pass
|
||||
// only the detail and never repeat the name.
|
||||
|
||||
import { appendFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { basename, dirname, isAbsolute, join } from "node:path";
|
||||
import { decisionBlock } from "./io.mjs";
|
||||
import { REPO, TMP_ROOT, sanitizePath } from "./paths.mjs";
|
||||
import { exec } from "./process.mjs";
|
||||
import { loadConfig, worktreeProvision } from "./config.mjs";
|
||||
|
||||
/**
|
||||
* @param {string | Record<string, unknown>} input
|
||||
* @param {string} [name]
|
||||
* @returns {object}
|
||||
*/
|
||||
export function createHookContext(input, name = "hook") {
|
||||
const i = typeof input === "string" ? JSON.parse(input) : input || {};
|
||||
const clean = (s) => String(s ?? "").replace(/[\t\n]/g, " ");
|
||||
|
||||
const agentId = clean(i.agent_id);
|
||||
// agentName: env var carries the suffixed runtime name (e.g. developer-TASK-001)
|
||||
// in PostToolUse/PreToolUse contexts; for SubagentStart the env is the parent's,
|
||||
// so fall back to i.agent_name which Claude Code populates with the child's name.
|
||||
const agentName = process.env.CLAUDE_AGENT_NAME || clean(i.agent_name) || "";
|
||||
const chatSessionId = process.env.CHAT_SESSION_DIR ? basename(process.env.CHAT_SESSION_DIR) : "";
|
||||
|
||||
const sessionId =
|
||||
clean(i.session_id) || process.env.CLAUDE_CODE_SESSION_ID || chatSessionId || "default";
|
||||
const agentType = clean(i.agent_type) || agentName;
|
||||
|
||||
const sessionShort = sessionId.split("-")[0];
|
||||
const sessionDir = join(TMP_ROOT, sanitizePath(REPO), sessionId);
|
||||
const logFile = join(sessionDir, "hooks.log");
|
||||
|
||||
/**
|
||||
* @param {...unknown} parts
|
||||
* @returns {void}
|
||||
*/
|
||||
const log = (...parts) => {
|
||||
try {
|
||||
mkdirSync(dirname(logFile), { recursive: true });
|
||||
appendFileSync(
|
||||
logFile,
|
||||
`[${new Date().toISOString()}] [${name}] ${parts.join(" ")}\n`,
|
||||
);
|
||||
} catch {
|
||||
// logging must never break a hook
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Write to stderr (the agent-visible channel), prefixed with `[${name}]` the
|
||||
* same way log() prefixes each log line. The prefix is added once, so
|
||||
* multi-line payloads (compiler/test output) stay readable; a trailing
|
||||
* newline is ensured.
|
||||
* @param {...unknown} parts
|
||||
* @returns {void}
|
||||
*/
|
||||
const error = (...parts) => {
|
||||
const text = parts.join(" ");
|
||||
process.stderr.write(`[${name}] ${text}${text.endsWith("\n") ? "" : "\n"}`);
|
||||
};
|
||||
|
||||
const verdict = (verb, detail) => log(detail ? `${verb} ${detail}` : verb);
|
||||
|
||||
// The `npm-link` provisioning builtin: mirror $REPO/node_modules into the
|
||||
// worktree. See provisionWorktree below for the config-driven dispatcher.
|
||||
const linkNodeModules = (wt) => {
|
||||
const target = join(wt, "node_modules");
|
||||
if (existsSync(target)) return;
|
||||
const source = join(REPO, "node_modules");
|
||||
if (!existsSync(source)) return;
|
||||
// Fast path: hardlink tree (cp -al) when the worktree base shares a
|
||||
// filesystem with the repo. In dev containers /tmp and /workspaces are
|
||||
// different mounts (cross-device), so cp -al fails; fall back to a full
|
||||
// real copy. A symlinked node_modules is NOT an option: vitest browser
|
||||
// mode (Chromium) hangs on it. See memory worktree-node-modules-provisioning.
|
||||
if (exec("cp", ["-al", source, target]).status === 0) return;
|
||||
rmSync(target, { recursive: true, force: true });
|
||||
if (exec("cp", ["-a", source, target]).status !== 0) {
|
||||
rmSync(target, { recursive: true, force: true });
|
||||
throw new Error(`node_modules provisioning failed for ${wt} - cp -al and cp -a both failed`);
|
||||
}
|
||||
};
|
||||
|
||||
// Config-driven worktree provisioning (config.worktree.provision): the
|
||||
// `npm-link` builtin (default, mirrors node_modules), `none` (no provisioning,
|
||||
// for a stack that needs none), or a script path (run with the worktree as its
|
||||
// one argument). Removes the npm assumption from worktree setup. Fail-closed on
|
||||
// a provision-script error, like the npm-link builtin.
|
||||
const provisionWorktree = (wt) => {
|
||||
let mode = "npm-link";
|
||||
try {
|
||||
mode = worktreeProvision(loadConfig());
|
||||
} catch {
|
||||
mode = "npm-link";
|
||||
}
|
||||
if (mode === "none") {
|
||||
log(`provision skipped (none) wt=${wt}`);
|
||||
return;
|
||||
}
|
||||
if (mode === "npm-link") {
|
||||
linkNodeModules(wt);
|
||||
return;
|
||||
}
|
||||
const script = isAbsolute(mode) ? mode : join(REPO, mode);
|
||||
const r = exec("bash", [script, wt], { cwd: REPO });
|
||||
if (r.status !== 0) {
|
||||
throw new Error(
|
||||
`worktree provision script failed (${script}): ${(r.stderr || r.stdout || "").slice(0, 300)}`,
|
||||
);
|
||||
}
|
||||
log(`provisioned via script ${script} wt=${wt}`);
|
||||
};
|
||||
|
||||
return {
|
||||
name,
|
||||
repo: REPO,
|
||||
sessionId,
|
||||
agentType,
|
||||
agentName,
|
||||
agentId,
|
||||
sessionShort,
|
||||
sessionDir,
|
||||
worktreeBase: sessionDir,
|
||||
logFile,
|
||||
ticketsDir: process.env.TICKETS_DIR || join(sessionDir, "tickets"),
|
||||
|
||||
log,
|
||||
error,
|
||||
|
||||
/**
|
||||
* @param {string} [detail]
|
||||
* @returns {never}
|
||||
*/
|
||||
accept(detail) {
|
||||
verdict("ACCEPT", detail);
|
||||
process.exit(0);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {{ reason: string, log?: string }} options
|
||||
* @returns {never}
|
||||
*/
|
||||
block({ reason, log: detail }) {
|
||||
verdict("BLOCK", detail);
|
||||
decisionBlock(reason);
|
||||
process.exit(0);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {{ log?: string }} [options]
|
||||
* @returns {never}
|
||||
*/
|
||||
fail(message, { log: detail } = {}) {
|
||||
verdict("FAIL", detail);
|
||||
error(message);
|
||||
process.exit(2);
|
||||
},
|
||||
|
||||
// Worktree provisioning: the config-driven dispatcher plus the npm-link
|
||||
// builtin it wraps (both defined as closures above).
|
||||
provisionWorktree,
|
||||
linkNodeModules,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user