From 310488b0501e17a6e92db646575cdef54251f880 Mon Sep 17 00:00:00 2001 From: lbianlbian <83378530+lbianlbian@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:15:06 +0000 Subject: [PATCH] fix(dx): unblock lint and the Claude pre-commit hook on Windows (#6619) - .claude/settings.json: the typecheck step piped through grep inside single quotes, which cmd.exe splits into a broken pipeline, so the hook denied every commit on Windows. On every OS, `|| true` also made the step pass regardless of type errors. Test-file errors are now filtered in JS and any remaining `error TS` line fails the step. - scripts/check-studio-copy-dashes.mjs: report paths with forward slashes so EXEMPT entries match on Windows. Co-authored-by: Claude Opus 5.5 --- .claude/settings.json | 2 +- scripts/check-studio-copy-dashes.mjs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 5daf570fce..df0fa3976d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -8,7 +8,7 @@ "type": "command", "timeout": 180, "statusMessage": "Running build + lint + typecheck before commit…", - "command": "node -e \"\nconst chunks = [];\nprocess.stdin.on('data', d => chunks.push(d));\nprocess.stdin.on('end', () => {\n const input = JSON.parse(Buffer.concat(chunks).toString());\n const cmd = input.tool_input?.command || '';\n if (!/git\\s+commit\\b/.test(cmd)) process.exit(0);\n const { execSync } = require('child_process');\n const fs = require('fs');\n const path = require('path');\n const os = require('os');\n // A leading 'cd ' in the command is the real target directory:\n // Claude Code prefixes this whenever the session's tracked cwd differs\n // from this hook process's own cwd (e.g. a sibling repo worktree) — the\n // hook process itself always inherits the primary working directory,\n // never the command's actual target.\n function resolveTargetDir() {\n const m = cmd.match(/^\\s*cd\\s+('([^']+)'|(\\S+))\\s*(?:&&|;|\\n|$)/);\n if (m) {\n let dir = m[2] || m[3];\n if (dir.startsWith('~')) dir = path.join(os.homedir(), dir.slice(1));\n dir = path.resolve(process.cwd(), dir);\n if (fs.existsSync(dir)) return dir;\n }\n return process.cwd();\n }\n const targetDir = resolveTargetDir();\n let repoRoot;\n try {\n repoRoot = execSync('git rev-parse --show-toplevel', { cwd: targetDir, encoding: 'utf8' }).trim();\n } catch {\n process.exit(0);\n }\n // Only this repo's own bun build/lint/typecheck applies — skip silently\n // for any other repo (e.g. a sibling Go/Python repo touched in the same session).\n const pkgPath = path.join(repoRoot, 'package.json');\n if (!fs.existsSync(pkgPath)) process.exit(0);\n let pkg;\n try {\n pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));\n } catch {\n process.exit(0);\n }\n if (pkg.name !== 'hyperframes-monorepo') process.exit(0);\n const steps = [\n ['bun run build', 'Build'],\n ['bun run lint', 'Lint'],\n ['bun run --filter \\'*\\' typecheck 2>&1 | grep -v \\'vitest\\\\|test\\\\.ts\\' || true', 'Typecheck'],\n ];\n const failures = [];\n for (const [script, label] of steps) {\n try {\n execSync(script, { cwd: repoRoot, stdio: 'pipe' });\n } catch (e) {\n failures.push(label + ':\\n' + (e.stdout?.toString() || e.message).slice(0, 400));\n }\n }\n if (failures.length > 0) {\n process.stdout.write(JSON.stringify({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: '❌ Pre-commit checks failed:\\n\\n' + failures.join('\\n\\n') + '\\n\\nFix the issues above, then retry the commit.',\n },\n }));\n }\n});\"" + "command": "node -e \"\nconst chunks = [];\nprocess.stdin.on('data', d => chunks.push(d));\nprocess.stdin.on('end', () => {\n const input = JSON.parse(Buffer.concat(chunks).toString());\n const cmd = input.tool_input?.command || '';\n if (!/git\\s+commit\\b/.test(cmd)) process.exit(0);\n const { execSync } = require('child_process');\n const fs = require('fs');\n const path = require('path');\n const os = require('os');\n // A leading 'cd ' in the command is the real target directory:\n // Claude Code prefixes this whenever the session's tracked cwd differs\n // from this hook process's own cwd (e.g. a sibling repo worktree) — the\n // hook process itself always inherits the primary working directory,\n // never the command's actual target.\n function resolveTargetDir() {\n const m = cmd.match(/^\\s*cd\\s+('([^']+)'|(\\S+))\\s*(?:&&|;|\\n|$)/);\n if (m) {\n let dir = m[2] || m[3];\n if (dir.startsWith('~')) dir = path.join(os.homedir(), dir.slice(1));\n dir = path.resolve(process.cwd(), dir);\n if (fs.existsSync(dir)) return dir;\n }\n return process.cwd();\n }\n const targetDir = resolveTargetDir();\n let repoRoot;\n try {\n repoRoot = execSync('git rev-parse --show-toplevel', { cwd: targetDir, encoding: 'utf8' }).trim();\n } catch {\n process.exit(0);\n }\n // Only this repo's own bun build/lint/typecheck applies — skip silently\n // for any other repo (e.g. a sibling Go/Python repo touched in the same session).\n const pkgPath = path.join(repoRoot, 'package.json');\n if (!fs.existsSync(pkgPath)) process.exit(0);\n let pkg;\n try {\n pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));\n } catch {\n process.exit(0);\n }\n if (pkg.name !== 'hyperframes-monorepo') process.exit(0);\n // Type errors in test files (vitest globals, *.test.ts) don't block a commit.\n // Filtered here, not by a shell pipe: execSync runs cmd.exe on Windows,\n // where single quotes don't quote, so a quoted pipe would split the command.\n function typeErrors(output) {\n return output\n .split('\\n')\n .filter((line) => /error TS\\d+/.test(line) && !/vitest|test\\.ts/.test(line))\n .join('\\n');\n }\n const steps = [\n ['bun run build', 'Build'],\n ['bun run lint', 'Lint'],\n ['bun run --filter=* typecheck', 'Typecheck', typeErrors],\n ];\n const failures = [];\n for (const [script, label, filter] of steps) {\n try {\n execSync(script, { cwd: repoRoot, stdio: 'pipe' });\n } catch (e) {\n const output = (e.stdout?.toString() || '') + (e.stderr?.toString() || '');\n const detail = filter ? filter(output) : e.stdout?.toString() || e.message;\n if (detail) failures.push(label + ':\\n' + detail.slice(0, 400));\n }\n }\n if (failures.length > 0) {\n process.stdout.write(JSON.stringify({\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: '❌ Pre-commit checks failed:\\n\\n' + failures.join('\\n\\n') + '\\n\\nFix the issues above, then retry the commit.',\n },\n }));\n }\n});\"" } ] } diff --git a/scripts/check-studio-copy-dashes.mjs b/scripts/check-studio-copy-dashes.mjs index cc24ef69e5..8abc236229 100644 --- a/scripts/check-studio-copy-dashes.mjs +++ b/scripts/check-studio-copy-dashes.mjs @@ -2,7 +2,7 @@ // Covers Studio and every package whose messages it shows; the CLI's terminal-only output is out. // Reads the TypeScript AST, so strings, template text and JSX text count and comments never do. import { readFileSync, readdirSync } from "node:fs"; -import { extname, join, relative } from "node:path"; +import { extname, join, relative, sep } from "node:path"; import { fileURLToPath } from "node:url"; import ts from "typescript"; @@ -73,7 +73,8 @@ export function checkStudioCopyDashes(root = ROOT) { .flatMap((path) => listDashedText( readFileSync(join(sourceRoot, path), "utf8"), - relative(root, join(sourceRoot, path)), + // Forward slashes on every OS, so EXEMPT's file paths match on Windows too. + relative(root, join(sourceRoot, path)).split(sep).join("/"), ), ); });