Spaces:
Configuration error
Configuration error
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import { spawnSync } from "node:child_process"; | |
| import { fileURLToPath } from "node:url"; | |
| import { setupGitHooks } from "./setup-git-hooks.js"; | |
| function detectPackageManager(ua = process.env.npm_config_user_agent ?? "") { | |
| // Examples: | |
| // - "pnpm/10.23.0 npm/? node/v22.21.1 darwin arm64" | |
| // - "npm/10.9.4 node/v22.12.0 linux x64" | |
| // - "bun/1.2.2" | |
| const normalized = String(ua).trim(); | |
| if (normalized.startsWith("pnpm/")) return "pnpm"; | |
| if (normalized.startsWith("bun/")) return "bun"; | |
| if (normalized.startsWith("npm/")) return "npm"; | |
| if (normalized.startsWith("yarn/")) return "yarn"; | |
| return "unknown"; | |
| } | |
| function shouldApplyPnpmPatchedDependenciesFallback(pm = detectPackageManager()) { | |
| // pnpm already applies pnpm.patchedDependencies itself; re-applying would fail. | |
| return pm !== "pnpm"; | |
| } | |
| function getRepoRoot() { | |
| const here = path.dirname(fileURLToPath(import.meta.url)); | |
| return path.resolve(here, ".."); | |
| } | |
| function ensureExecutable(targetPath) { | |
| if (process.platform === "win32") return; | |
| if (!fs.existsSync(targetPath)) return; | |
| try { | |
| const mode = fs.statSync(targetPath).mode & 0o777; | |
| if (mode & 0o100) return; | |
| fs.chmodSync(targetPath, 0o755); | |
| } catch (err) { | |
| console.warn(`[postinstall] chmod failed: ${err}`); | |
| } | |
| } | |
| function hasGit(repoRoot) { | |
| const result = spawnSync("git", ["--version"], { cwd: repoRoot, stdio: "ignore" }); | |
| return result.status === 0; | |
| } | |
| function extractPackageName(key) { | |
| if (key.startsWith("@")) { | |
| const idx = key.indexOf("@", 1); | |
| if (idx === -1) return key; | |
| return key.slice(0, idx); | |
| } | |
| const idx = key.lastIndexOf("@"); | |
| if (idx <= 0) return key; | |
| return key.slice(0, idx); | |
| } | |
| function stripPrefix(p) { | |
| if (p.startsWith("a/") || p.startsWith("b/")) return p.slice(2); | |
| return p; | |
| } | |
| function parseRange(segment) { | |
| // segment: "-12,5" or "+7" | |
| const [startRaw, countRaw] = segment.slice(1).split(","); | |
| const start = Number.parseInt(startRaw, 10); | |
| const count = countRaw ? Number.parseInt(countRaw, 10) : 1; | |
| if (Number.isNaN(start) || Number.isNaN(count)) { | |
| throw new Error(`invalid hunk range: ${segment}`); | |
| } | |
| return { start, count }; | |
| } | |
| function parsePatch(patchText) { | |
| const lines = patchText.split("\n"); | |
| const files = []; | |
| let i = 0; | |
| while (i < lines.length) { | |
| if (!lines[i].startsWith("diff --git ")) { | |
| i += 1; | |
| continue; | |
| } | |
| const file = { oldPath: null, newPath: null, hunks: [] }; | |
| i += 1; | |
| // Skip index line(s) | |
| while (i < lines.length && lines[i].startsWith("index ")) i += 1; | |
| if (i < lines.length && lines[i].startsWith("--- ")) { | |
| file.oldPath = stripPrefix(lines[i].slice(4).trim()); | |
| i += 1; | |
| } | |
| if (i < lines.length && lines[i].startsWith("+++ ")) { | |
| file.newPath = stripPrefix(lines[i].slice(4).trim()); | |
| i += 1; | |
| } | |
| while (i < lines.length && lines[i].startsWith("@@")) { | |
| const header = lines[i]; | |
| const match = /^@@\s+(-\d+(?:,\d+)?)\s+(\+\d+(?:,\d+)?)\s+@@/.exec(header); | |
| if (!match) throw new Error(`invalid hunk header: ${header}`); | |
| const oldRange = parseRange(match[1]); | |
| const newRange = parseRange(match[2]); | |
| i += 1; | |
| const hunkLines = []; | |
| while (i < lines.length) { | |
| const line = lines[i]; | |
| if (line.startsWith("@@") || line.startsWith("diff --git ")) break; | |
| if (line === "") { | |
| i += 1; | |
| continue; | |
| } | |
| if (line.startsWith("\\ No newline at end of file")) { | |
| i += 1; | |
| continue; | |
| } | |
| hunkLines.push(line); | |
| i += 1; | |
| } | |
| file.hunks.push({ | |
| oldStart: oldRange.start, | |
| oldLines: oldRange.count, | |
| newStart: newRange.start, | |
| newLines: newRange.count, | |
| lines: hunkLines, | |
| }); | |
| } | |
| if (file.newPath && file.hunks.length > 0) { | |
| files.push(file); | |
| } | |
| } | |
| return files; | |
| } | |
| function readFileLines(targetPath) { | |
| if (!fs.existsSync(targetPath)) { | |
| throw new Error(`target file missing: ${targetPath}`); | |
| } | |
| const raw = fs.readFileSync(targetPath, "utf-8"); | |
| const hasTrailingNewline = raw.endsWith("\n"); | |
| const parts = raw.split("\n"); | |
| if (hasTrailingNewline) parts.pop(); | |
| return { lines: parts, hasTrailingNewline }; | |
| } | |
| function writeFileLines(targetPath, lines, hadTrailingNewline) { | |
| const content = lines.join("\n") + (hadTrailingNewline ? "\n" : ""); | |
| fs.writeFileSync(targetPath, content, "utf-8"); | |
| } | |
| function applyHunk(lines, hunk, offset) { | |
| let cursor = hunk.oldStart - 1 + offset; | |
| const expected = []; | |
| for (const raw of hunk.lines) { | |
| const marker = raw[0]; | |
| if (marker === " " || marker === "+") { | |
| expected.push(raw.slice(1)); | |
| } | |
| } | |
| if (cursor >= 0 && cursor + expected.length <= lines.length) { | |
| let alreadyApplied = true; | |
| for (let i = 0; i < expected.length; i += 1) { | |
| if (lines[cursor + i] !== expected[i]) { | |
| alreadyApplied = false; | |
| break; | |
| } | |
| } | |
| if (alreadyApplied) { | |
| const delta = hunk.newLines - hunk.oldLines; | |
| return offset + delta; | |
| } | |
| } | |
| for (const raw of hunk.lines) { | |
| const marker = raw[0]; | |
| const text = raw.slice(1); | |
| if (marker === " ") { | |
| if (lines[cursor] !== text) { | |
| throw new Error( | |
| `context mismatch at line ${cursor + 1}: expected "${text}", found "${lines[cursor] ?? "<eof>"}"`, | |
| ); | |
| } | |
| cursor += 1; | |
| } else if (marker === "-") { | |
| if (lines[cursor] !== text) { | |
| throw new Error( | |
| `delete mismatch at line ${cursor + 1}: expected "${text}", found "${lines[cursor] ?? "<eof>"}"`, | |
| ); | |
| } | |
| lines.splice(cursor, 1); | |
| } else if (marker === "+") { | |
| lines.splice(cursor, 0, text); | |
| cursor += 1; | |
| } else { | |
| throw new Error(`unexpected hunk marker: ${marker}`); | |
| } | |
| } | |
| const delta = hunk.newLines - hunk.oldLines; | |
| return offset + delta; | |
| } | |
| function applyPatchToFile(targetDir, filePatch) { | |
| if (filePatch.newPath === "/dev/null") { | |
| // deletion not needed for our patches | |
| return; | |
| } | |
| const relPath = stripPrefix(filePatch.newPath ?? filePatch.oldPath ?? ""); | |
| const targetPath = path.join(targetDir, relPath); | |
| const { lines, hasTrailingNewline } = readFileLines(targetPath); | |
| let offset = 0; | |
| for (const hunk of filePatch.hunks) { | |
| offset = applyHunk(lines, hunk, offset); | |
| } | |
| writeFileLines(targetPath, lines, hasTrailingNewline); | |
| } | |
| function applyPatchSet({ patchText, targetDir }) { | |
| let resolvedTarget = path.resolve(targetDir); | |
| if (!fs.existsSync(resolvedTarget) || !fs.statSync(resolvedTarget).isDirectory()) { | |
| console.warn(`[postinstall] skip missing target: ${resolvedTarget}`); | |
| return; | |
| } | |
| resolvedTarget = fs.realpathSync(resolvedTarget); | |
| const files = parsePatch(patchText); | |
| if (files.length === 0) return; | |
| for (const filePatch of files) { | |
| applyPatchToFile(resolvedTarget, filePatch); | |
| } | |
| } | |
| function applyPatchFile({ patchPath, targetDir }) { | |
| const absPatchPath = path.resolve(patchPath); | |
| if (!fs.existsSync(absPatchPath)) { | |
| throw new Error(`missing patch: ${absPatchPath}`); | |
| } | |
| const patchText = fs.readFileSync(absPatchPath, "utf-8"); | |
| applyPatchSet({ patchText, targetDir }); | |
| } | |
| function main() { | |
| const repoRoot = getRepoRoot(); | |
| process.chdir(repoRoot); | |
| ensureExecutable(path.join(repoRoot, "dist", "entry.js")); | |
| setupGitHooks({ repoRoot }); | |
| if (!shouldApplyPnpmPatchedDependenciesFallback()) { | |
| return; | |
| } | |
| const pkgPath = path.join(repoRoot, "package.json"); | |
| const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); | |
| const patched = pkg?.pnpm?.patchedDependencies ?? {}; | |
| // Bun does not support pnpm.patchedDependencies. Apply these patch files to | |
| // node_modules packages as a best-effort compatibility layer. | |
| for (const [key, relPatchPath] of Object.entries(patched)) { | |
| if (typeof relPatchPath !== "string" || !relPatchPath.trim()) continue; | |
| const pkgName = extractPackageName(String(key)); | |
| if (!pkgName) continue; | |
| applyPatchFile({ | |
| targetDir: path.join("node_modules", ...pkgName.split("/")), | |
| patchPath: relPatchPath, | |
| }); | |
| } | |
| } | |
| try { | |
| const skip = | |
| process.env.CLAWDBOT_SKIP_POSTINSTALL === "1" || | |
| process.env.VITEST === "true" || | |
| process.env.NODE_ENV === "test"; | |
| if (!skip) { | |
| main(); | |
| } | |
| } catch (err) { | |
| console.error(String(err)); | |
| process.exit(1); | |
| } | |
| export { | |
| applyPatchFile, | |
| applyPatchSet, | |
| applyPatchToFile, | |
| detectPackageManager, | |
| parsePatch, | |
| shouldApplyPnpmPatchedDependenciesFallback, | |
| }; | |