mirror of
https://github.com/vercel/vercel-plugin.git
synced 2026-09-14 15:39:47 +08:00
206 lines
6.8 KiB
JavaScript
206 lines
6.8 KiB
JavaScript
// hooks/src/hook-env.mts
|
|
import { createHash, randomUUID } from "crypto";
|
|
import {
|
|
appendFileSync,
|
|
closeSync,
|
|
mkdirSync,
|
|
openSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
writeFileSync
|
|
} from "fs";
|
|
import { homedir, tmpdir } from "os";
|
|
import { dirname, join, resolve, sep } from "path";
|
|
import { fileURLToPath } from "url";
|
|
import { createLogger, logCaughtError } from "./logger.mjs";
|
|
var log = createLogger();
|
|
function pluginRoot(metaUrl) {
|
|
const base = metaUrl ?? import.meta.url;
|
|
return resolve(dirname(fileURLToPath(base)), "..");
|
|
}
|
|
function resolveAuditLogPath(hookInputCwd) {
|
|
const cwdFromHookInput = typeof hookInputCwd === "string" && hookInputCwd.trim() !== "" ? hookInputCwd : null;
|
|
const projectRoot = process.env.CLAUDE_PROJECT_ROOT || cwdFromHookInput || process.cwd();
|
|
const configuredPath = process.env.VERCEL_PLUGIN_AUDIT_LOG_FILE;
|
|
if (configuredPath === "off") {
|
|
return null;
|
|
}
|
|
if (typeof configuredPath === "string" && configuredPath.trim() !== "") {
|
|
return resolve(projectRoot, configuredPath);
|
|
}
|
|
const projectSlug = projectRoot.replaceAll("/", "-");
|
|
return join(homedir(), ".claude", "projects", projectSlug, "vercel-plugin", "skill-injections.jsonl");
|
|
}
|
|
function appendAuditLog(record, hookInputCwd) {
|
|
const auditLogPath = resolveAuditLogPath(hookInputCwd);
|
|
if (auditLogPath === null) return;
|
|
try {
|
|
mkdirSync(dirname(auditLogPath), { recursive: true });
|
|
const payload = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...record };
|
|
appendFileSync(auditLogPath, `${JSON.stringify(payload)}
|
|
`, "utf-8");
|
|
} catch (error) {
|
|
logCaughtError(log, "hook-env:append-audit-log-failed", error, { auditLogPath });
|
|
}
|
|
}
|
|
function getDedupScopeId(payload) {
|
|
if (payload && typeof payload === "object" && "agent_id" in payload && typeof payload.agent_id === "string" && payload.agent_id.length > 0) {
|
|
return payload.agent_id;
|
|
}
|
|
return "main";
|
|
}
|
|
var SAFE_SESSION_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
function dedupSessionIdSegment(sessionId) {
|
|
if (SAFE_SESSION_ID_RE.test(sessionId)) {
|
|
return sessionId;
|
|
}
|
|
return createHash("sha256").update(sessionId).digest("hex");
|
|
}
|
|
function dedupScopeIdSegment(scopeId) {
|
|
if (SAFE_SESSION_ID_RE.test(scopeId)) {
|
|
return scopeId;
|
|
}
|
|
return createHash("sha256").update(scopeId).digest("hex");
|
|
}
|
|
function resolveDedupTempPath(sessionId, basename, scopeId) {
|
|
const tempRoot = resolve(tmpdir());
|
|
const scopeSegment = scopeId ? `-${dedupScopeIdSegment(scopeId)}` : "";
|
|
const candidate = resolve(join(tempRoot, `vercel-plugin-${dedupSessionIdSegment(sessionId)}${scopeSegment}-${basename}`));
|
|
const tempPrefix = tempRoot.endsWith(sep) ? tempRoot : `${tempRoot}${sep}`;
|
|
if (!candidate.startsWith(tempPrefix)) {
|
|
throw new Error(`dedup temp path escaped tmpdir: tempRoot=${tempRoot} candidate=${candidate}`);
|
|
}
|
|
return candidate;
|
|
}
|
|
function dedupFilePath(sessionId, kind, scopeId) {
|
|
return resolveDedupTempPath(sessionId, `${kind}.txt`, scopeId);
|
|
}
|
|
function dedupClaimDirPath(sessionId, kind, scopeId) {
|
|
return resolveDedupTempPath(sessionId, `${kind}.d`, scopeId);
|
|
}
|
|
function readSessionFile(sessionId, kind, scopeId) {
|
|
try {
|
|
return readFileSync(dedupFilePath(sessionId, kind, scopeId), "utf-8");
|
|
} catch (error) {
|
|
logCaughtError(log, "hook-env:read-session-file-failed", error, { sessionId, kind, scopeId });
|
|
return "";
|
|
}
|
|
}
|
|
function writeSessionFile(sessionId, kind, value, scopeId) {
|
|
try {
|
|
writeFileSync(dedupFilePath(sessionId, kind, scopeId), value, "utf-8");
|
|
} catch (error) {
|
|
logCaughtError(log, "hook-env:write-session-file-failed", error, { sessionId, kind, scopeId });
|
|
}
|
|
}
|
|
function tryClaimSessionKey(sessionId, kind, key, scopeId) {
|
|
try {
|
|
const claimDir = dedupClaimDirPath(sessionId, kind, scopeId);
|
|
mkdirSync(claimDir, { recursive: true });
|
|
const file = join(claimDir, encodeURIComponent(key));
|
|
const fd = openSync(file, "wx");
|
|
closeSync(fd);
|
|
return true;
|
|
} catch (error) {
|
|
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
function listSessionKeys(sessionId, kind, scopeId) {
|
|
try {
|
|
return readdirSync(dedupClaimDirPath(sessionId, kind, scopeId)).map((entry) => decodeURIComponent(entry)).filter((entry) => entry !== "").sort();
|
|
} catch (error) {
|
|
logCaughtError(log, "hook-env:list-session-keys-failed", error, { sessionId, kind, scopeId });
|
|
return [];
|
|
}
|
|
}
|
|
function syncSessionFileFromClaims(sessionId, kind, scopeId) {
|
|
const value = listSessionKeys(sessionId, kind, scopeId).join(",");
|
|
writeSessionFile(sessionId, kind, value, scopeId);
|
|
return value;
|
|
}
|
|
function removeSessionClaimDir(sessionId, kind, scopeId) {
|
|
try {
|
|
rmSync(dedupClaimDirPath(sessionId, kind, scopeId), { recursive: true, force: true });
|
|
} catch (error) {
|
|
logCaughtError(log, "hook-env:remove-session-claim-dir-failed", error, { sessionId, kind, scopeId });
|
|
}
|
|
}
|
|
function removeAllSessionDedupArtifacts(sessionId) {
|
|
const result = { removedFiles: 0, removedDirs: 0 };
|
|
const tempRoot = resolve(tmpdir());
|
|
const prefix = `vercel-plugin-${dedupSessionIdSegment(sessionId)}-`;
|
|
let entries;
|
|
try {
|
|
entries = readdirSync(tempRoot).filter(
|
|
(name) => name.startsWith(prefix) && (name.endsWith("-seen-skills.d") || name.endsWith("-seen-skills.txt"))
|
|
);
|
|
} catch {
|
|
return result;
|
|
}
|
|
for (const entry of entries) {
|
|
const fullPath = join(tempRoot, entry);
|
|
if (entry.endsWith(".d")) {
|
|
try {
|
|
rmSync(fullPath, { recursive: true, force: true });
|
|
result.removedDirs++;
|
|
} catch (error) {
|
|
logCaughtError(log, "hook-env:remove-all-session-dedup-artifacts-dir", error, { fullPath });
|
|
}
|
|
} else {
|
|
try {
|
|
rmSync(fullPath);
|
|
result.removedFiles++;
|
|
} catch (error) {
|
|
logCaughtError(log, "hook-env:remove-all-session-dedup-artifacts-file", error, { fullPath });
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function profileCachePath(sessionId) {
|
|
return resolveDedupTempPath(sessionId, "profile.json");
|
|
}
|
|
function generateVerificationId() {
|
|
return randomUUID();
|
|
}
|
|
function safeReadFile(path) {
|
|
try {
|
|
return readFileSync(path, "utf-8");
|
|
} catch (error) {
|
|
logCaughtError(log, "hook-env:safe-read-file-failed", error, { path });
|
|
return null;
|
|
}
|
|
}
|
|
function safeReadJson(path) {
|
|
const content = safeReadFile(path);
|
|
if (content === null) return null;
|
|
try {
|
|
return JSON.parse(content);
|
|
} catch (error) {
|
|
logCaughtError(log, "hook-env:safe-read-json-failed", error, { path });
|
|
return null;
|
|
}
|
|
}
|
|
export {
|
|
appendAuditLog,
|
|
dedupClaimDirPath,
|
|
dedupFilePath,
|
|
generateVerificationId,
|
|
getDedupScopeId,
|
|
listSessionKeys,
|
|
pluginRoot,
|
|
profileCachePath,
|
|
readSessionFile,
|
|
removeAllSessionDedupArtifacts,
|
|
removeSessionClaimDir,
|
|
safeReadFile,
|
|
safeReadJson,
|
|
syncSessionFileFromClaims,
|
|
tryClaimSessionKey,
|
|
writeSessionFile
|
|
};
|