Skip to content

Commit 8bc75fb

Browse files
authored
fix(uncheck): review the whole package: safer hooks, tsc selection like tsc, faster runs, clearer docs (#12)
* fix(uncheck): harden the shared runner, file resolution and package-manager commands - globs match dot files (picomatch) and exclusions on their own apply to everything - git file names are taken literally in staged and hooks run; names starting with ! or - reach tools intact - a run given files that no check handles passes in staged and hooks run - tools killed or failing to spawn fail their check instead of crashing the run - argv batches fit Windows' command line; Yarn PnP installs resolve tools - hooks never download: npx --no, bunx --no-install; yarn run --silent keeps agent JSON clean - sherif only reports in staged, since its fixes reach beyond the commit - colours follow Node's rules; SIGHUP puts unstaged changes back; platform errors print cleanly - minified bundle with a compile-cached entry; dead --wizard and --log-level flags hidden * fix(uncheck): typecheck standalone projects with --noEmit, several at once tsc -p no longer writes JavaScript or declarations during a check, and the standalone projects of a monorepo are checked up to four at a time with their output kept in plan order. * test(uncheck): cover dot-file globs, exclusion-only paths, literal git paths, argv batches and killed tools Also lets the middleapi tsconfig preset accept the .ts imports Node's type stripping needs. * docs(uncheck): describe dot-file globs, PnP, --noEmit, sherif in staged and preset versions * fix(uncheck): check only a commit's own files in `staged` and put unstaged changes back intact - A staged symlink is no longer checked or fixed through to the file it points to (outside the repository, untracked or unstaged); a symlink turned into a regular file is now checked. - During a merge, only files that differ from the side merged in are checked and fixed, so merge commits no longer carry fixes to the other side's work. - With --fix only the files the fixes changed are staged, so a merge or squash bringing in files outside a sparse checkout can be committed, and a conflicting fix is still undone there. - A partially staged file stored with CRLF under text=auto or core.autocrlf is no longer reported as a conflicting fix. - Run from a package folder, the restored file keeps the line endings and encoding its .gitattributes set. - Setting unstaged changes aside and undoing fixes no longer runs, or fails with, the post-checkout hook. * fix(uncheck): keep the pre-commit hook prepare writes safe for every repository and every package - A core.hooksPath from the global or system git config is left alone and reported, so one install no longer adds this repository's line to the hook every repository runs - Packages of a workspace that prepare at the same time all keep their line - In a hook that already has commands, the line runs before them, so their failure still blocks the commit and an `exec` or `exit` no longer skips it - A hook in another language (Node, Python, Ruby) is left alone and prepare says which line it should run, instead of breaking every commit - A package folder whose name sh would expand inside double quotes gets no line rather than a broken or ever-growing one - A hooks folder only counts as the husky 9 or Vite+ dispatcher when it is their `_` folder - The folder a line enters comes from git itself, so it no longer depends on how the path to the package was spelled - Lines older versions wrote with plain npx or yarn are upgraded in place, and hand-written lines that chain or soften the command stay as they are * fix(uncheck): agent hooks keep your settings, check the right directory and reach every agent - `hooks install` refuses a config that is not valid JSON(C) or not an object, naming the file and leaving every file as it was, instead of rewriting it with settings lost - Reinstalling updates only uncheck's own hook entry, keeping keys added to it; permission rules, notes and chained commands that merely mention `uncheck hooks run` stay as they are, and a Stop hook is still added next to them - Hook entries carry a 600 s timeout, so Copilot, VS Code and CodeBuddy no longer kill a typecheck at their 30 s or 60 s default; existing installs are upgraded on the next install - The hook checks the top of the repository whichever directory the agent `cd`'d into; installed below the top it carries `--dir=<path>` and checks that directory - Copilot is sent back through its Claude-format entry too, not only warned - When checks still fail after the one retry, Claude Code and CodeBuddy show the user a warning instead of ending the turn silently - A change no selected check covers passes the hook, and a deleted file whose name looks like a pattern no longer checks its neighbours - Windsurf is no longer offered: its hook event can neither show the report nor send Cascade back * fix(uncheck): typecheck every project a change reaches, the way tsc sees it - staged and given files select referenced configs of any name, so a Vite, Nx or Angular solution layout (tsconfig.app.json) is no longer skipped - a changed tsconfig, or a base it extends, selects the projects it applies to, also when the base is a workspace package linked into node_modules - a selected project that others reference builds the roots depending on it with tsc -b, so a library change that breaks its dependents fails the commit; unrelated roots stay out - project inputs match tsc: a base shared by two extends branches applies in both, declarationDir is excluded, `*` skips .min.js, extensions are case sensitive and an explicit node_modules include counts - backslashes in extends and references read as separators, and references keep ${configDir} as written, like tsc - a reference cycle anywhere fails runs on given files too * fix(uncheck): keep literal file lists in their given order; document the staged, prepare, hooks and tsc changes * fix(uncheck): address the final review of the branch - globs read [!x] as POSIX negation and parentheses literally, as before picomatch - staged: a CRLF file under text=auto is undone rather than turned to LF, fixes outside a sparse checkout are staged, core.safecrlf no longer blocks, messages name only what applies - prepare: the line goes after the hook's PATH and environment setup, git before 2.26 still refuses a global core.hooksPath, an unreadable or binary hook is never overwritten - hooks: only the stop event's entry is uncheck's, a timeout the user set is kept, a stale --dir is reported, Yarn 2+ root installs run the root binary, Copilot is refused below the top, Cursor turns the user stopped are left alone - tsc: dependents found without a quadratic walk, backslashes in include/exclude, smaller cycle search - Yarn PnP resolves tools from every ancestor workspace * test(uncheck): cover the final review fixes and correct the docs they touch * refactor(uncheck): simplify and speed up the branch's code - globs use minimatch with dot matching, the engine path.matchesGlob already used, instead of patching picomatch back to its semantics - shared helpers for the rev-parse location, reading a file that may not exist, platform error messages and batched git calls; the Yarn 2+ -T rule lives in detectExec - staged runs its independent git calls together, writes a tree only when it sets changes aside, and merges files concurrently; hooks run lists changed files concurrently - existence and node_modules filters no longer cost a fiber or an array per file - tsc resolves real paths only when a config is among the given files - tests share the Stop payload, the CLI path and the side-branch setup * fix(uncheck): address the review of the latest commits - staged: an edit saved to a partially staged file while the checks run is merged back again; only a CRLF blob git keeps as it is refuses the merge - a newline in the repository's path no longer shifts the prefix staged, prepare and install use - prepare puts its line before a setup line that also runs a command or continues onto the next, and the old-git scope check reads included config files - hooks install in a Yarn 2+ package without its own uncheck runs the root's - a linked node_modules stays out of the project files; globs drop `.` inside braces again - Yarn PnP falls back to node_modules for folders its resolver does not cover * test(uncheck): cover the latest review fixes and the gaps it found Adds tests for an edit saved during the checks, a put-back that fails, a first commit with fixes, a repository path with a newline, other tools' Stop hooks and --require/--skip in hook commands, Yarn 2+ package installs, setup lines that run a command or continue, included git config on old git, tsc -b before parallel tsc -p, Yarn PnP resolution, linked node_modules, braces with `.`, and the CLI's error output and global flags. The fixture now refuses to write into a symlinked tool, which would change the shared pnpm store. * refactor(uncheck): keep the oxlint and oxfmt checks separate They are different tools and will grow apart, so each keeps its own file as on main. * build(uncheck): ship the bundle unminified so crash traces stay readable A stack trace with real names is what users can report back. * chore(uncheck): require Node 22.20 or later, nothing more path.matchesGlob, the reason for skipping 23.x and early 24.x, is no longer used. * docs(uncheck): rewrite the README to be clear, concise and task-first Organised by what a reader wants to do: check a project, check some files, run it before every commit, run it after every agent turn, then monorepos, presets and troubleshooting. Every example was run against the CLI. Also fixes the licence badge, which linked to a missing file. * docs(uncheck): say that a tsconfig.json without TypeScript fails rather than being skipped
1 parent 2c035ee commit 8bc75fb

28 files changed

Lines changed: 2738 additions & 899 deletions

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@
3737
}
3838
},
3939
"engines": {
40-
"node": ">=22.20 <23 || >=24.8"
40+
"node": ">=22.20"
4141
}
4242
}

‎packages/uncheck/README.md‎

Lines changed: 142 additions & 89 deletions
Large diffs are not rendered by default.

‎packages/uncheck/package.json‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,16 @@
5555
"@effect/platform-node": "^4.0.0-rc.116",
5656
"effect": "^4.0.0-rc.116",
5757
"jsonc-parser": "^3.3.1",
58+
"minimatch": "^10.2.6",
5859
"oxfmt": "^0.68.0",
5960
"oxlint": "^1.83.0",
6061
"sherif": "^1.13.0",
6162
"typescript": "^7.0.2"
6263
},
6364
"peerDependencies": {
6465
"oxfmt": "*",
65-
"oxlint": "*",
66-
"sherif": "*",
66+
"oxlint": ">=1.60.0",
67+
"sherif": ">=1.10.0",
6768
"typescript": "*"
6869
},
6970
"peerDependenciesMeta": {
@@ -81,6 +82,6 @@
8182
}
8283
},
8384
"engines": {
84-
"node": ">=22.20 <23 || >=24.8"
85+
"node": ">=22.20"
8586
}
8687
}

‎packages/uncheck/src/bin.ts‎

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,8 @@
11
#!/usr/bin/env node
22

3-
import process from 'node:process'
3+
import { enableCompileCache } from 'node:module'
44

5-
import * as NodeRuntime from '@effect/platform-node/NodeRuntime'
6-
import * as NodeServices from '@effect/platform-node/NodeServices'
7-
import { Effect } from 'effect'
8-
import { Command } from 'effect/unstable/cli'
5+
// The cache only covers modules compiled after it is on, hence the import that follows it.
6+
enableCompileCache()
97

10-
import pkg from '../package.json'
11-
import { hooks } from './commands/hooks'
12-
import { prepare } from './commands/prepare'
13-
import { staged } from './commands/staged'
14-
import { uncheck } from './commands/uncheck'
15-
16-
// Any failed write (a reader gone after `| head`, a closed terminal, a full disk) would otherwise end
17-
// the run before `staged` puts unstaged changes back, so output errors are ignored.
18-
for (const stream of [process.stdout, process.stderr]) {
19-
stream.on('error', () => {})
20-
}
21-
22-
Command.run(uncheck.pipe(Command.withSubcommands([staged, prepare, hooks])), {
23-
version: pkg.version,
24-
}).pipe(
25-
Effect.catchTag('CheckFailed', () =>
26-
Effect.sync(() => {
27-
process.exitCode = 1
28-
}),
29-
),
30-
Effect.catchTag('StopBlocked', () =>
31-
Effect.sync(() => {
32-
process.exitCode = 2
33-
}),
34-
),
35-
Effect.provide(NodeServices.layer),
36-
NodeRuntime.runMain,
37-
)
8+
await import('./cli')

‎packages/uncheck/src/checks/oxfmt.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Check } from '../types'
66

77
export const oxfmt: Check = {
88
name: 'oxfmt',
9-
fixes: true,
9+
fixes: 'files',
1010
plan: Effect.fn(function* ({ cwd, fix, files }) {
1111
const bin = yield* resolveBin('oxfmt', cwd)
1212

@@ -20,7 +20,7 @@ export const oxfmt: Check = {
2020
return [{ bin, args }]
2121
}
2222

23-
// Given files may include ones oxfmt does not handle (a Markdown file), which is not a failure.
23+
// Given files may include ones oxfmt does not handle (a .txt file), which is not a failure.
2424
return argvBatches(files).map((batch) => ({
2525
bin,
2626
args: [...args, '--no-error-on-unmatched-pattern'],

‎packages/uncheck/src/checks/oxlint.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Check } from '../types'
66

77
export const oxlint: Check = {
88
name: 'oxlint',
9-
fixes: true,
9+
fixes: 'files',
1010
plan: Effect.fn(function* ({ cwd, fix, files }) {
1111
const bin = yield* resolveBin('oxlint', cwd)
1212

‎packages/uncheck/src/checks/sherif.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const WORKSPACE_FILES = new Set(['package.json', 'pnpm-workspace.yaml'])
1111

1212
export const sherif: Check = {
1313
name: 'sherif',
14-
fixes: true,
14+
fixes: 'workspace',
1515
plan: Effect.fn(function* ({ cwd, fix, files }) {
1616
const fs = yield* FileSystem.FileSystem
1717
const path = yield* Path.Path

0 commit comments

Comments
 (0)