diff --git a/docs/scripts.md b/docs/scripts.md index 6fc05c464a..56d74df888 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -17,11 +17,6 @@ Use these when a task is clearly tied to a script; otherwise prefer the CLI. - Prefer CLI surfaces when they exist (example: auth monitoring uses `openclaw models status --check`). - Assume scripts are host‑specific; read them before running on a new machine. -## Git hooks - -- `scripts/setup-git-hooks.js`: best-effort setup for `core.hooksPath` when inside a git repo. -- `scripts/format-staged.js`: pre-commit formatter for staged `src/` and `test/` files. - ## Auth monitoring scripts Auth monitoring scripts are documented here: diff --git a/docs/zh-CN/scripts.md b/docs/zh-CN/scripts.md index c8f459f365..09ee5ce27b 100644 --- a/docs/zh-CN/scripts.md +++ b/docs/zh-CN/scripts.md @@ -24,11 +24,6 @@ x-i18n: - 当 CLI 接口存在时优先使用(例如:认证监控使用 `openclaw models status --check`)。 - 假定脚本与特定主机相关;在新机器上运行前请先阅读脚本内容。 -## Git 钩子 - -- `scripts/setup-git-hooks.js`:在 git 仓库中尽力设置 `core.hooksPath`。 -- `scripts/format-staged.js`:用于暂存的 `src/` 和 `test/` 文件的预提交格式化工具。 - ## 认证监控脚本 认证监控脚本的文档请参阅: diff --git a/git-hooks/pre-commit b/git-hooks/pre-commit index c2fe5149b6..e34398e51d 100755 --- a/git-hooks/pre-commit +++ b/git-hooks/pre-commit @@ -1,4 +1,7 @@ #!/bin/sh -ROOT=$(git rev-parse --show-toplevel 2>/dev/null) -[ -z "$ROOT" ] && exit 0 -node "$ROOT/scripts/format-staged.js" +FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') +[ -z "$FILES" ] && exit 0 +echo "$FILES" | xargs pnpm format:fix --no-error-on-unmatched-pattern +echo "$FILES" | xargs git add + +exit 0 diff --git a/package.json b/package.json index 7e385e0c9d..a437f0816c 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "openclaw:rpc": "node scripts/run-node.mjs agent --mode rpc --json", "plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts", "prepack": "pnpm build && pnpm ui:build", + "prepare": "command -v git >/dev/null 2>&1 && git config core.hooksPath git-hooks || exit 0", "protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/OpenClawProtocol/GatewayModels.swift", "protocol:gen": "node --import tsx scripts/protocol-gen.ts", "protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts", diff --git a/scripts/docker/cleanup-smoke/Dockerfile b/scripts/docker/cleanup-smoke/Dockerfile index 73f1ac81e5..683dfbea9d 100644 --- a/scripts/docker/cleanup-smoke/Dockerfile +++ b/scripts/docker/cleanup-smoke/Dockerfile @@ -9,7 +9,6 @@ RUN apt-get update \ WORKDIR /repo COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -COPY scripts/setup-git-hooks.js ./scripts/setup-git-hooks.js RUN corepack enable \ && pnpm install --frozen-lockfile diff --git a/scripts/format-staged.js b/scripts/format-staged.js deleted file mode 100644 index 4c5249dd89..0000000000 --- a/scripts/format-staged.js +++ /dev/null @@ -1,149 +0,0 @@ -import { spawnSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const OXFMT_EXTENSIONS = new Set([".cjs", ".js", ".json", ".jsonc", ".jsx", ".mjs", ".ts", ".tsx"]); - -function getRepoRoot() { - const here = path.dirname(fileURLToPath(import.meta.url)); - return path.resolve(here, ".."); -} - -function runGitCommand(args, options = {}) { - return spawnSync("git", args, { - cwd: options.cwd, - encoding: "utf-8", - stdio: options.stdio ?? "pipe", - }); -} - -function splitNullDelimited(value) { - if (!value) { - return []; - } - const text = String(value); - return text.split("\0").filter(Boolean); -} - -function normalizeGitPath(filePath) { - return filePath.replace(/\\/g, "/"); -} - -function filterOxfmtTargets(paths) { - return paths - .map(normalizeGitPath) - .filter( - (filePath) => - (filePath.startsWith("src/") || filePath.startsWith("test/")) && - OXFMT_EXTENSIONS.has(path.posix.extname(filePath)), - ); -} - -function findPartiallyStagedFiles(stagedFiles, unstagedFiles) { - const unstaged = new Set(unstagedFiles.map(normalizeGitPath)); - return stagedFiles.filter((filePath) => unstaged.has(normalizeGitPath(filePath))); -} - -function filterOutPartialTargets(targets, partialTargets) { - if (partialTargets.length === 0) { - return targets; - } - const partial = new Set(partialTargets.map(normalizeGitPath)); - return targets.filter((filePath) => !partial.has(normalizeGitPath(filePath))); -} - -function resolveOxfmtCommand(repoRoot) { - const binName = process.platform === "win32" ? "oxfmt.cmd" : "oxfmt"; - const local = path.join(repoRoot, "node_modules", ".bin", binName); - if (fs.existsSync(local)) { - return { command: local, args: [] }; - } - - const result = spawnSync("oxfmt", ["--version"], { stdio: "ignore" }); - if (result.status === 0) { - return { command: "oxfmt", args: [] }; - } - - return null; -} - -function getGitPaths(args, repoRoot) { - const result = runGitCommand(args, { cwd: repoRoot }); - if (result.status !== 0) { - return []; - } - return splitNullDelimited(result.stdout ?? ""); -} - -function formatFiles(repoRoot, oxfmt, files) { - const result = spawnSync(oxfmt.command, ["--write", ...oxfmt.args, ...files], { - cwd: repoRoot, - stdio: "inherit", - }); - return result.status === 0; -} - -function stageFiles(repoRoot, files) { - if (files.length === 0) { - return true; - } - const result = runGitCommand(["add", "--", ...files], { cwd: repoRoot, stdio: "inherit" }); - return result.status === 0; -} - -function main() { - const repoRoot = getRepoRoot(); - const staged = getGitPaths( - ["diff", "--cached", "--name-only", "-z", "--diff-filter=ACMR"], - repoRoot, - ); - const targets = filterOxfmtTargets(staged); - if (targets.length === 0) { - return; - } - - const unstaged = getGitPaths(["diff", "--name-only", "-z"], repoRoot); - const partial = findPartiallyStagedFiles(targets, unstaged); - if (partial.length > 0) { - process.stderr.write("[pre-commit] Skipping partially staged files:\n"); - for (const filePath of partial) { - process.stderr.write(`- ${filePath}\n`); - } - process.stderr.write("Stage full files to format them automatically.\n"); - } - - const filteredTargets = filterOutPartialTargets(targets, partial); - if (filteredTargets.length === 0) { - return; - } - - const oxfmt = resolveOxfmtCommand(repoRoot); - if (!oxfmt) { - process.stderr.write("[pre-commit] oxfmt not found; skipping format.\n"); - return; - } - - if (!formatFiles(repoRoot, oxfmt, filteredTargets)) { - process.exitCode = 1; - return; - } - - if (!stageFiles(repoRoot, filteredTargets)) { - process.exitCode = 1; - } -} - -export { - filterOxfmtTargets, - filterOutPartialTargets, - findPartiallyStagedFiles, - getRepoRoot, - normalizeGitPath, - resolveOxfmtCommand, - splitNullDelimited, -}; - -if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { - main(); -} diff --git a/scripts/setup-git-hooks.js b/scripts/setup-git-hooks.js deleted file mode 100644 index a9023b9dc1..0000000000 --- a/scripts/setup-git-hooks.js +++ /dev/null @@ -1,104 +0,0 @@ -import { spawnSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const DEFAULT_HOOKS_PATH = "git-hooks"; -const PRE_COMMIT_HOOK = "pre-commit"; - -function getRepoRoot() { - const here = path.dirname(fileURLToPath(import.meta.url)); - return path.resolve(here, ".."); -} - -function runGitCommand(args, options = {}) { - return spawnSync("git", args, { - cwd: options.cwd, - encoding: "utf-8", - stdio: options.stdio ?? "pipe", - }); -} - -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(`[setup-git-hooks] chmod failed: ${err}`); - } -} - -function isGitAvailable({ repoRoot = getRepoRoot(), runGit = runGitCommand } = {}) { - const result = runGit(["--version"], { cwd: repoRoot, stdio: "ignore" }); - return result.status === 0; -} - -function isGitRepo({ repoRoot = getRepoRoot(), runGit = runGitCommand } = {}) { - const result = runGit(["rev-parse", "--is-inside-work-tree"], { - cwd: repoRoot, - stdio: "pipe", - }); - if (result.status !== 0) { - return false; - } - return String(result.stdout ?? "").trim() === "true"; -} - -function setHooksPath({ - repoRoot = getRepoRoot(), - hooksPath = DEFAULT_HOOKS_PATH, - runGit = runGitCommand, -} = {}) { - const result = runGit(["config", "core.hooksPath", hooksPath], { - cwd: repoRoot, - stdio: "ignore", - }); - return result.status === 0; -} - -function setupGitHooks({ - repoRoot = getRepoRoot(), - hooksPath = DEFAULT_HOOKS_PATH, - runGit = runGitCommand, -} = {}) { - if (!isGitAvailable({ repoRoot, runGit })) { - return { ok: false, reason: "git-missing" }; - } - - if (!isGitRepo({ repoRoot, runGit })) { - return { ok: false, reason: "not-repo" }; - } - - if (!setHooksPath({ repoRoot, hooksPath, runGit })) { - return { ok: false, reason: "config-failed" }; - } - - ensureExecutable(path.join(repoRoot, hooksPath, PRE_COMMIT_HOOK)); - - return { ok: true }; -} - -export { - DEFAULT_HOOKS_PATH, - PRE_COMMIT_HOOK, - ensureExecutable, - getRepoRoot, - isGitAvailable, - isGitRepo, - runGitCommand, - setHooksPath, - setupGitHooks, -}; - -if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { - setupGitHooks(); -} diff --git a/src/git-hooks.test.ts b/src/git-hooks.test.ts deleted file mode 100644 index 569f9fcbbd..0000000000 --- a/src/git-hooks.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; -import { - filterOxfmtTargets, - filterOutPartialTargets, - findPartiallyStagedFiles, - splitNullDelimited, -} from "../scripts/format-staged.js"; -import { setupGitHooks } from "../scripts/setup-git-hooks.js"; - -function makeTempDir() { - return fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-hooks-")); -} - -describe("format-staged helpers", () => { - it("splits null-delimited output", () => { - expect(splitNullDelimited("a\0b\0")).toEqual(["a", "b"]); - expect(splitNullDelimited("")).toEqual([]); - }); - - it("filters oxfmt targets", () => { - const targets = filterOxfmtTargets([ - "src/app.ts", - "src/app.md", - "test/foo.tsx", - "scripts/dev.ts", - "test\\bar.js", - ]); - expect(targets).toEqual(["src/app.ts", "test/foo.tsx", "test/bar.js"]); - }); - - it("detects partially staged files", () => { - const partial = findPartiallyStagedFiles( - ["src/a.ts", "test/b.tsx"], - ["src/a.ts", "docs/readme.md"], - ); - expect(partial).toEqual(["src/a.ts"]); - }); - - it("filters out partial targets", () => { - const filtered = filterOutPartialTargets( - ["src/a.ts", "test/b.tsx", "test/c.ts"], - ["test/b.tsx"], - ); - expect(filtered).toEqual(["src/a.ts", "test/c.ts"]); - }); -}); - -describe("setupGitHooks", () => { - it("returns git-missing when git is unavailable", () => { - const runGit = vi.fn(() => ({ status: 1, stdout: "" })); - const result = setupGitHooks({ repoRoot: "/tmp", runGit }); - expect(result).toEqual({ ok: false, reason: "git-missing" }); - expect(runGit).toHaveBeenCalled(); - }); - - it("returns not-repo when not inside a work tree", () => { - const runGit = vi.fn((args) => { - if (args[0] === "--version") { - return { status: 0, stdout: "git version" }; - } - if (args[0] === "rev-parse") { - return { status: 0, stdout: "false" }; - } - return { status: 1, stdout: "" }; - }); - - const result = setupGitHooks({ repoRoot: "/tmp", runGit }); - expect(result).toEqual({ ok: false, reason: "not-repo" }); - }); - - it("configures hooks path when inside a repo", () => { - const repoRoot = makeTempDir(); - const hooksDir = path.join(repoRoot, "git-hooks"); - fs.mkdirSync(hooksDir, { recursive: true }); - const hookPath = path.join(hooksDir, "pre-commit"); - fs.writeFileSync(hookPath, "#!/bin/sh\n", "utf-8"); - fs.chmodSync(hookPath, 0o644); - - const runGit = vi.fn((args) => { - if (args[0] === "--version") { - return { status: 0, stdout: "git version" }; - } - if (args[0] === "rev-parse") { - return { status: 0, stdout: "true" }; - } - if (args[0] === "config") { - return { status: 0, stdout: "" }; - } - return { status: 1, stdout: "" }; - }); - - const result = setupGitHooks({ repoRoot, runGit }); - expect(result).toEqual({ ok: true }); - expect(runGit.mock.calls.some(([args]) => args[0] === "config")).toBe(true); - - if (process.platform !== "win32") { - const mode = fs.statSync(hookPath).mode & 0o777; - expect(mode & 0o100).toBeTruthy(); - } - - fs.rmSync(repoRoot, { recursive: true, force: true }); - }); -});