pakhale 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/assets/instructions/AGENTS.md +29 -0
- package/assets/statusline/claude-code.sh +100 -0
- package/dist/cli.js +2037 -0
- package/package.json +50 -0
- package/skills/architecture/SKILL.md +14 -0
- package/skills/consistency-check/DIMENSIONS.md +91 -0
- package/skills/consistency-check/SKILL.md +74 -0
- package/skills/consistency-check/scripts/consistency-workflow.js +158 -0
- package/skills/deslop/SKILL.md +153 -0
- package/skills/deslop/references/python.md +65 -0
- package/skills/deslop/references/react.md +71 -0
- package/skills/deslop/references/typescript.md +62 -0
- package/skills/sitedrop/SKILL.md +40 -0
- package/skills/stackup/SKILL.md +96 -0
- package/skills/stackup/references/hooks-ci.md +132 -0
- package/skills/stackup/references/python.md +81 -0
- package/skills/stackup/references/react.md +79 -0
- package/skills/stackup/references/typescript.md +114 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# React / Next.js slop
|
|
2
|
+
|
|
3
|
+
Stack-specific tells. Run these alongside the main hunt, not instead of it.
|
|
4
|
+
|
|
5
|
+
## Effects
|
|
6
|
+
|
|
7
|
+
`rg -n "useEffect" <changed files>` and triage every one the diff added. Reference:
|
|
8
|
+
https://react.dev/learn/you-might-not-need-an-effect
|
|
9
|
+
|
|
10
|
+
| The effect… | Verdict |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| sets state derived from props/state already in scope | delete — compute during render |
|
|
13
|
+
| runs in response to a click/submit/change | delete — move into the event handler |
|
|
14
|
+
| resets state when a prop changes | delete — pass a `key` instead |
|
|
15
|
+
| fetches, then sets state, with no cleanup | keep but fix — needs abort/ignore flag, or move to the framework's data layer |
|
|
16
|
+
| subscribes to the DOM, a store, a socket, an observer | keep — this is what effects are for |
|
|
17
|
+
| has an empty dep array but reads props/state | bug — stale closure |
|
|
18
|
+
|
|
19
|
+
Also: an effect whose dep array was hand-trimmed to silence the lint rule is a bug, not a
|
|
20
|
+
style choice. And every subscription/listener/interval/observer needs a cleanup return —
|
|
21
|
+
missing one is a leak.
|
|
22
|
+
|
|
23
|
+
## Memo theater
|
|
24
|
+
|
|
25
|
+
`useMemo` / `useCallback` / `React.memo` added around cheap work is noise that costs more
|
|
26
|
+
than it saves. Tell: memoizing a string concat, an object literal of primitives, a `.filter`
|
|
27
|
+
over a handful of items.
|
|
28
|
+
|
|
29
|
+
Ask: what re-render does this actually prevent? Nothing measurable → remove it.
|
|
30
|
+
|
|
31
|
+
The inverse is a real finding: a memoized child that re-renders anyway because a parent
|
|
32
|
+
passes a fresh object/array/arrow prop each render. Fix the prop, not the child.
|
|
33
|
+
|
|
34
|
+
## Client/server boundary (Next.js App Router)
|
|
35
|
+
|
|
36
|
+
- `'use client'` added to a component that renders no interactivity, no hooks, no browser
|
|
37
|
+
API — remove it and let it stay a server component.
|
|
38
|
+
- `'use client'` on a high-level layout or page, dragging its whole subtree client-side.
|
|
39
|
+
Push the directive down to the leaf that actually needs it.
|
|
40
|
+
- Server-only imports (`fs`, DB client, secret env) reachable from a client component —
|
|
41
|
+
that's a leak and a build error waiting to happen.
|
|
42
|
+
- `useEffect` + `fetch` for data a server component or route handler could fetch directly.
|
|
43
|
+
- Server Actions: no auth check, no input validation, or returning raw DB rows to the client.
|
|
44
|
+
- Data fetched but never revalidated/invalidated after a mutating action.
|
|
45
|
+
|
|
46
|
+
## State
|
|
47
|
+
|
|
48
|
+
- State duplicating a prop (`useState(props.x)`) that then drifts — derive it instead.
|
|
49
|
+
- Multiple `useState` calls always set together — one object or reducer.
|
|
50
|
+
- State that's never read, or only read by the effect that set it.
|
|
51
|
+
- A ref used where state is required (value drives render) or state where a ref would do
|
|
52
|
+
(value never rendered).
|
|
53
|
+
- Context provider added for a value consumed by one component two levels down.
|
|
54
|
+
|
|
55
|
+
## Rendering & markup
|
|
56
|
+
|
|
57
|
+
- Missing or index-based `key` in a list that can reorder or filter.
|
|
58
|
+
- Conditional rendering with `&&` on a number or a possibly-`""` value — renders `0`.
|
|
59
|
+
- Deeply nested ternaries in JSX; extract or early-return.
|
|
60
|
+
- A wrapper `<div>` added for no styling reason — use a fragment.
|
|
61
|
+
- New interactive behavior on a `<div>`/`<span>` instead of `<button>`/`<a>` — kills keyboard
|
|
62
|
+
and screen-reader access. Also check: focus moved into new modals/drawers and returned on close.
|
|
63
|
+
- Async UI with no loading and no error branch — a permanently blank region on failure.
|
|
64
|
+
- `dangerouslySetInnerHTML` on anything derived from user input.
|
|
65
|
+
|
|
66
|
+
## Visual consistency
|
|
67
|
+
|
|
68
|
+
Before reading the new component's code, find the closest existing one and read *that*. Then
|
|
69
|
+
diff. Most UI slop is invisible in isolation and obvious side by side.
|
|
70
|
+
|
|
71
|
+
First understand what part of UI you are building, then look for existing similar sections, components, sub-components and patterns established in our app, and try to reuse the UI as much as possible and try to keep the typography, spacings, layouting everything consistent.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# TypeScript / Node slop
|
|
2
|
+
|
|
3
|
+
Stack-specific tells. Run these alongside the main hunt, not instead of it.
|
|
4
|
+
|
|
5
|
+
## Escape hatches
|
|
6
|
+
|
|
7
|
+
`rg -n ": any|as any|as unknown as|@ts-ignore|@ts-expect-error|!\." <changed files>`
|
|
8
|
+
|
|
9
|
+
Every hit is a claim the author couldn't prove. For each: is the real type knowable here?
|
|
10
|
+
Usually yes — a generic, a narrowing check, or a proper return type on the function upstream.
|
|
11
|
+
`as any` on a test mock is fine; `as any` to make a call site compile is a bug in waiting.
|
|
12
|
+
|
|
13
|
+
Non-null `!` on something the diff didn't just check is the same category — it's an assertion
|
|
14
|
+
that the next reader will trust and the runtime won't.
|
|
15
|
+
|
|
16
|
+
## Type slop
|
|
17
|
+
|
|
18
|
+
- A type declared and used once, inline-able at its single use site.
|
|
19
|
+
- A generic parameter appearing exactly once in the signature — it isn't doing anything.
|
|
20
|
+
- `interface` vs `type` inconsistent with the surrounding file.
|
|
21
|
+
- A hand-written type duplicating something already derived elsewhere — prefer
|
|
22
|
+
`z.infer`, `ReturnType`, `Awaited`, `typeof`, or the ORM's generated types over a
|
|
23
|
+
parallel definition that will drift.
|
|
24
|
+
- Optional fields (`x?: T`) everywhere because the shape wasn't decided — each one forces a
|
|
25
|
+
guard at every consumer.
|
|
26
|
+
- A union whose members are never discriminated at any call site.
|
|
27
|
+
- `Record<string, any>` where the keys are actually known.
|
|
28
|
+
|
|
29
|
+
## Redundant narrowing
|
|
30
|
+
|
|
31
|
+
- `if (x)` where `x` is already `NonNullable` per its type.
|
|
32
|
+
- `typeof x === 'string'` on a `string`.
|
|
33
|
+
- `?.` chained on a value the line above proved non-null.
|
|
34
|
+
- `x ?? default` where `x`'s type excludes null/undefined.
|
|
35
|
+
- A type guard function that duplicates a check the caller just performed.
|
|
36
|
+
|
|
37
|
+
## Async
|
|
38
|
+
|
|
39
|
+
- `await` inside a `for` loop over independent items — `Promise.all` (or a bounded pool).
|
|
40
|
+
- A promise created and never awaited; a floating `.then()` with no `.catch`.
|
|
41
|
+
- `try/catch` wrapping an entire function body to log-and-rethrow.
|
|
42
|
+
- `catch (e)` that swallows, or that returns a success-shaped value on failure.
|
|
43
|
+
- An `async` function with no `await` in it.
|
|
44
|
+
- No timeout / no abort signal on outbound `fetch`.
|
|
45
|
+
- Sequential DB round-trips in a loop (N+1) where one query with an `IN` clause works.
|
|
46
|
+
|
|
47
|
+
## Structure & modules
|
|
48
|
+
|
|
49
|
+
- A barrel `index.ts` added purely to re-export one module.
|
|
50
|
+
- A `utils.ts` / `helpers.ts` grab-bag created for a single function — put it next to its use.
|
|
51
|
+
- A constant defined in the new file that already exists in the shared constants module —
|
|
52
|
+
`rg` the literal value, not just the name.
|
|
53
|
+
- Env vars read via `process.env.X` inline when the project has a validated config module.
|
|
54
|
+
- A new dependency doing what the stdlib or an existing dep already does. Check `package.json`
|
|
55
|
+
before accepting any new import.
|
|
56
|
+
|
|
57
|
+
## Validation
|
|
58
|
+
|
|
59
|
+
- A Zod/Valibot schema and a TS interface maintained side by side — infer one from the other.
|
|
60
|
+
- External input (request body, query params, webhook payload, JSON file) typed by assertion
|
|
61
|
+
instead of parsed.
|
|
62
|
+
- `.parse()` in a request path with no error handling — a malformed body becomes a 500.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sitedrop
|
|
3
|
+
description: Deploy static HTML pages or sites to the user's personal sitedrop service for easy sharing, then open the returned URL. Use when the user asks to deploy, publish, share, or "put up" an HTML page, artifact, prototype, or static site — or after building one, when they want a shareable link use this one instead of default confiured for you.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sitedrop
|
|
7
|
+
|
|
8
|
+
Publish static files to a subdomain using the `sitedrop` CLI. The endpoint and password are already configured via `SITEDROP_ENDPOINT` and `SITEDROP_PASSWORD` env vars — never ask the user for them and never pass `-e`/`-p` manually.
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
sitedrop <path...> [-n <subdomain>]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- A path may be a folder, a `.zip`, or a file; mix them freely.
|
|
17
|
+
- Folder/archive contents land at the site root (a single wrapper directory is stripped).
|
|
18
|
+
- A lone `.html` file is renamed to `index.html` automatically.
|
|
19
|
+
- `-n, --name <subdomain>` sets the site name; omitted → random subdomain.
|
|
20
|
+
- `-f, --force` publishes without an `index.html` (root will 404) — only use when intentional.
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
1. Make sure the site is self-contained (inline or relative assets — no paths outside the deployed folder).
|
|
25
|
+
2. Pick a short, descriptive kebab-case name from the page's purpose (e.g. `-n perf-dashboard`). Omit `-n` if the user wants something unguessable/private.
|
|
26
|
+
3. Deploy and capture output:
|
|
27
|
+
```bash
|
|
28
|
+
out=$(sitedrop ./path -n my-page); echo "$out"
|
|
29
|
+
```
|
|
30
|
+
4. Extract the site URL from the output and open it in the browser:
|
|
31
|
+
```bash
|
|
32
|
+
open "$(echo "$out" | grep -Eo 'https?://[^[:space:]]+' | tail -1)"
|
|
33
|
+
```
|
|
34
|
+
5. Tell the user the URL in your reply so they can copy/share it.
|
|
35
|
+
|
|
36
|
+
## Notes
|
|
37
|
+
|
|
38
|
+
- Deploying a single HTML file works directly: `sitedrop page.html -n demo`.
|
|
39
|
+
- Redeploying with the same `-n` name updates the same site — reuse the name for iterations.
|
|
40
|
+
- If the deploy fails with an auth/endpoint error, report it and suggest the user verify `SITEDROP_ENDPOINT`/`SITEDROP_PASSWORD` in their shell profile — do not guess values.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stackup
|
|
3
|
+
description: "Stack up a new or under-tooled side project with the current best tooling — package manager, type checker, linter, formatter, dead-code detection, tests, git hooks, and CI — picking one tool per job so nothing conflicts. Use when the user says 'stackup', 'stack up this project', 'set up tooling', 'bootstrap this repo', 'add linting/formatting/hooks', or when starting a new project or finding a repo with no lint/format/typecheck scripts."
|
|
4
|
+
metadata:
|
|
5
|
+
author: pratikpakhale
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Stackup
|
|
10
|
+
|
|
11
|
+
Stack a project up with the current best tool for each job. **One tool per job** — the failure
|
|
12
|
+
mode this skill exists to prevent is two tools owning the same job and fighting each other
|
|
13
|
+
(ESLint next to oxlint, Prettier next to oxfmt, black next to ruff, husky next to lefthook).
|
|
14
|
+
|
|
15
|
+
## Rules
|
|
16
|
+
|
|
17
|
+
- **Detect, don't ask.** Read the repo first — `package.json`, `pyproject.toml`, lockfiles,
|
|
18
|
+
existing configs. Install only what's missing; never re-pin what's already working.
|
|
19
|
+
- **Replace, don't stack.** If a job already has an owner you're replacing, remove the old
|
|
20
|
+
one in the same change — config file, dependency, and scripts. A half-migration is worse
|
|
21
|
+
than either tool alone.
|
|
22
|
+
- **Every tool gets a script and a hook.** A linter nobody runs is dead weight. Each tool
|
|
23
|
+
lands in `package.json` scripts (or `pyproject.toml`), then in the hook config, then in CI.
|
|
24
|
+
- **Pin the toolchain.** The versions in CI, in hooks, and on the machine must match.
|
|
25
|
+
- **Green before you leave.** Run the full check once at the end. Config that has never been
|
|
26
|
+
executed is not configured.
|
|
27
|
+
|
|
28
|
+
## The stack
|
|
29
|
+
|
|
30
|
+
| Job | Tool | Never also install |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| Runtime, package manager, workspaces | `bun` | npm/yarn/pnpm, `tsx`, `ts-node` |
|
|
33
|
+
| Type checking | `typescript@7` (`tsc`) | `tsgo`, `@typescript/native-preview` |
|
|
34
|
+
| Linting | `oxlint` + `oxlint-tsgolint` | ESLint, Biome |
|
|
35
|
+
| Formatting | `oxfmt` | Prettier, Biome, dprint |
|
|
36
|
+
| Unused files/exports/deps | `knip` | depcheck, ts-prune |
|
|
37
|
+
| Tests | `bun test` (`vitest` for DOM) | jest, mocha |
|
|
38
|
+
| Python everything | `uv` | pip, poetry, pyenv, pipx |
|
|
39
|
+
| Python lint + format | `ruff` | black, isort, flake8, autopep8 |
|
|
40
|
+
| Python type checking | `ty` | mypy, pyright (see reference) |
|
|
41
|
+
| Git hooks | `lefthook` | husky, lint-staged, pre-commit, prek |
|
|
42
|
+
| Secret scanning | `gitleaks` | — |
|
|
43
|
+
| Toolchain versions | `mise` | nvm, pyenv, asdf |
|
|
44
|
+
|
|
45
|
+
`typescript@7` is the Go compiler, shipped stable July 2026 — it is the `typescript` package
|
|
46
|
+
and the `tsc` binary. `tsgo` and `@typescript/native-preview` were the preview channel and are
|
|
47
|
+
obsolete; if you find them in a repo, migrate off.
|
|
48
|
+
|
|
49
|
+
## Order of operations
|
|
50
|
+
|
|
51
|
+
Do these in order — later steps depend on scripts the earlier ones create.
|
|
52
|
+
|
|
53
|
+
1. **Pin the toolchain** — `mise.toml` at the repo root (see [references/hooks-ci.md](references/hooks-ci.md)).
|
|
54
|
+
2. **Install per-stack deps and configs** — read the reference for each stack present:
|
|
55
|
+
|
|
56
|
+
| Stack | Read |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `.ts`, `.js`, Node, Bun | [references/typescript.md](references/typescript.md) |
|
|
59
|
+
| `.tsx`, React, Next.js | [references/react.md](references/react.md) |
|
|
60
|
+
| `.py` | [references/python.md](references/python.md) |
|
|
61
|
+
|
|
62
|
+
3. **Write the scripts** — every tool reachable by name, plus one aggregate `check`.
|
|
63
|
+
4. **Wire hooks and CI** — [references/hooks-ci.md](references/hooks-ci.md).
|
|
64
|
+
5. **Run `bun run check` and fix the fallout.** Expect the first run to be noisy on an
|
|
65
|
+
existing codebase: auto-fix what's mechanical, and for the rest either fix it or
|
|
66
|
+
downgrade the specific rule in config with a one-line comment saying why. Do not
|
|
67
|
+
blanket-disable a plugin to get to green.
|
|
68
|
+
|
|
69
|
+
## Fast vs. whole-project
|
|
70
|
+
|
|
71
|
+
This split matters everywhere — hooks, CI, and which script you reach for.
|
|
72
|
+
|
|
73
|
+
- **Per-file, milliseconds** — formatting, non-type-aware lint, secret scan. Runs on staged
|
|
74
|
+
files at `pre-commit`.
|
|
75
|
+
- **Whole-program, seconds** — `tsc`, type-aware lint, `ty`, `knip`, tests. These cannot be
|
|
76
|
+
scoped to staged files without being wrong. Runs at `pre-push` and in CI.
|
|
77
|
+
|
|
78
|
+
Putting `tsc` in `pre-commit` is the most common way to make a team disable hooks entirely.
|
|
79
|
+
|
|
80
|
+
## Also worth setting up
|
|
81
|
+
|
|
82
|
+
Raise these with the user rather than installing silently — they are project-shaped, not
|
|
83
|
+
universal:
|
|
84
|
+
|
|
85
|
+
- **Conventional commits** — `commitlint` on the `commit-msg` hook. Cheap, and it's the
|
|
86
|
+
commit convention already in use.
|
|
87
|
+
- **`AGENTS.md`** at the repo root with a `CLAUDE.md` symlink to it — the project's own
|
|
88
|
+
instructions for coding agents. Worth writing on day one, while the architecture is a
|
|
89
|
+
decision rather than an archaeology problem.
|
|
90
|
+
- **Env var validation** — parse `process.env` once at startup through a schema
|
|
91
|
+
(`arktype` or `zod`) and export the typed object. Only for projects with real config.
|
|
92
|
+
- **Publishing** (libraries only) — `tsdown` to build, `publint` and
|
|
93
|
+
`@arethetypeswrong/cli` to verify the published shape.
|
|
94
|
+
- **Dependency freshness** — `bun outdated` / `bun audit`, `uv lock --upgrade`. Renovate
|
|
95
|
+
only if the project will outlive your attention span.
|
|
96
|
+
- **`.editorconfig`** — one file, stops editors from re-indenting what `oxfmt` formatted.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Hooks, CI, and toolchain pinning
|
|
2
|
+
|
|
3
|
+
## Toolchain versions — mise
|
|
4
|
+
|
|
5
|
+
One file pins every language runtime, so the machine, the hooks, and CI agree. This replaces
|
|
6
|
+
nvm, pyenv, and asdf, and it handles a polyglot repo that `.nvmrc` alone can't.
|
|
7
|
+
|
|
8
|
+
```toml
|
|
9
|
+
# mise.toml
|
|
10
|
+
[tools]
|
|
11
|
+
bun = "1.3"
|
|
12
|
+
python = "3.13"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`mise install` on a fresh clone. CI uses `jdx/mise-action`, so the pin is honoured there too
|
|
16
|
+
rather than being re-declared in the workflow.
|
|
17
|
+
|
|
18
|
+
## Git hooks — lefthook
|
|
19
|
+
|
|
20
|
+
A single Go binary, one YAML file, parallel execution, and no dependency on Node or Python —
|
|
21
|
+
which is what makes it the right pick for a repo with both. It replaces husky **and**
|
|
22
|
+
lint-staged; `pre-commit`/`prek` cover the same ground but drag in the Python toolchain and
|
|
23
|
+
run sequentially. Don't install two hook managers — they fight over the same
|
|
24
|
+
`.git/hooks` entries and the loser silently stops running.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bun add -D lefthook && bunx lefthook install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`lefthook install` writes `.git/hooks`. It has to be re-run on a fresh clone — put it in a
|
|
31
|
+
`prepare` script so `bun install` does it.
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
# lefthook.yml
|
|
35
|
+
pre-commit:
|
|
36
|
+
parallel: true
|
|
37
|
+
jobs:
|
|
38
|
+
- name: format
|
|
39
|
+
glob: "*.{ts,tsx,js,jsx,json,css,md}"
|
|
40
|
+
run: bunx oxfmt {staged_files}
|
|
41
|
+
stage_fixed: true
|
|
42
|
+
|
|
43
|
+
- name: lint
|
|
44
|
+
glob: "*.{ts,tsx,js,jsx}"
|
|
45
|
+
run: bunx oxlint --fix {staged_files}
|
|
46
|
+
stage_fixed: true
|
|
47
|
+
|
|
48
|
+
- name: python
|
|
49
|
+
glob: "*.py"
|
|
50
|
+
run: uv run ruff check --fix {staged_files} && uv run ruff format {staged_files}
|
|
51
|
+
stage_fixed: true
|
|
52
|
+
|
|
53
|
+
- name: secrets
|
|
54
|
+
run: gitleaks git --staged --redact --no-banner
|
|
55
|
+
|
|
56
|
+
pre-push:
|
|
57
|
+
parallel: true
|
|
58
|
+
jobs:
|
|
59
|
+
- name: check
|
|
60
|
+
run: bun run check
|
|
61
|
+
- name: python
|
|
62
|
+
run: uv run ty check && uv run pytest
|
|
63
|
+
- name: test
|
|
64
|
+
run: bun test
|
|
65
|
+
|
|
66
|
+
commit-msg:
|
|
67
|
+
jobs:
|
|
68
|
+
- name: conventional
|
|
69
|
+
run: bunx commitlint --edit {1}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Why the split: `pre-commit` holds only per-file work that finishes in milliseconds, and
|
|
73
|
+
`stage_fixed: true` re-stages what the fixers rewrote so the commit contains the formatted
|
|
74
|
+
version. `tsc`, type-aware lint, `ty`, `knip`, and tests need the whole program — they can't
|
|
75
|
+
be scoped to staged files without being wrong, so they run at `pre-push`.
|
|
76
|
+
|
|
77
|
+
A `pre-commit` hook that takes ten seconds is a hook that gets bypassed with `--no-verify`
|
|
78
|
+
within a week. Keep it fast and the rest at push.
|
|
79
|
+
|
|
80
|
+
## Secret scanning — gitleaks
|
|
81
|
+
|
|
82
|
+
`brew install gitleaks`. The `git --staged` invocation above scans only what's being
|
|
83
|
+
committed, so it costs nothing per commit. This is the one hook worth keeping even in a repo
|
|
84
|
+
where you skip everything else — a leaked key in git history is not fixable by a later commit.
|
|
85
|
+
|
|
86
|
+
Add a `.gitleaksignore` for the false positives (test fixtures, example configs) rather than
|
|
87
|
+
loosening the rules.
|
|
88
|
+
|
|
89
|
+
## Conventional commits — commitlint
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
bun add -D @commitlint/cli @commitlint/config-conventional
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
// commitlint.config.js
|
|
97
|
+
export default { extends: ['@commitlint/config-conventional'] }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Optional, and worth it on anything that will get a changelog. It's the one hook that rejects
|
|
101
|
+
rather than fixes, so it's also the most annoying — skip it on scratch repos.
|
|
102
|
+
|
|
103
|
+
## CI — run the same commands
|
|
104
|
+
|
|
105
|
+
The workflow must not invent its own commands. It runs what the hooks run, so passing locally
|
|
106
|
+
means passing in CI.
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
# .github/workflows/ci.yml
|
|
110
|
+
name: ci
|
|
111
|
+
on: [push, pull_request]
|
|
112
|
+
|
|
113
|
+
jobs:
|
|
114
|
+
check:
|
|
115
|
+
runs-on: ubuntu-latest
|
|
116
|
+
steps:
|
|
117
|
+
- uses: actions/checkout@v5
|
|
118
|
+
with: { fetch-depth: 0 } # gitleaks needs history
|
|
119
|
+
- uses: jdx/mise-action@v3
|
|
120
|
+
- run: bun install --frozen-lockfile
|
|
121
|
+
- run: bun run check
|
|
122
|
+
- run: bun test
|
|
123
|
+
- run: uv sync --frozen && uv run ty check && uv run pytest
|
|
124
|
+
- run: gitleaks git --redact --no-banner
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`--frozen-lockfile` and `uv sync --frozen` are what make CI meaningful: an out-of-date
|
|
128
|
+
lockfile fails the build instead of being quietly resolved around, which is exactly the
|
|
129
|
+
drift that makes "works on my machine" happen.
|
|
130
|
+
|
|
131
|
+
Drop the `uv` line for a JS-only repo and the `bun` lines for a Python-only one — but keep
|
|
132
|
+
both `check` and the lockfile flags.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
|
|
3
|
+
## uv owns everything
|
|
4
|
+
|
|
5
|
+
`uv` replaces pip, pip-tools, pipx, poetry, virtualenv, and pyenv. There is no reason to
|
|
6
|
+
install any of them alongside it, and no reason to activate a venv by hand — `uv run` does it.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
uv init # new project: pyproject.toml + .python-version + .venv
|
|
10
|
+
uv add --dev ruff ty pytest
|
|
11
|
+
uv run pytest # runs in the project env, syncing it first if stale
|
|
12
|
+
uvx ruff check # one-off tool run, no project install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`uv.lock` is committed. `uv sync --frozen` in CI, so a stale lockfile fails the build instead
|
|
16
|
+
of being silently resolved around.
|
|
17
|
+
|
|
18
|
+
Single-file scripts get inline dependencies (PEP 723) rather than a project — `uv run
|
|
19
|
+
script.py` reads the header and builds the env:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# /// script
|
|
23
|
+
# requires-python = ">=3.13"
|
|
24
|
+
# dependencies = ["httpx"]
|
|
25
|
+
# ///
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## ruff owns lint and format
|
|
29
|
+
|
|
30
|
+
One tool, one config, no ordering conflicts. black, isort, flake8, pyupgrade, pydocstyle, and
|
|
31
|
+
autopep8 are all subsumed — if any are in the project, they're being removed.
|
|
32
|
+
|
|
33
|
+
```toml
|
|
34
|
+
# pyproject.toml
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
line-length = 100
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint]
|
|
39
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "RUF", "ASYNC", "S", "PTH"]
|
|
40
|
+
ignore = ["E501"] # the formatter owns line length
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint.per-file-ignores]
|
|
43
|
+
"tests/**" = ["S101"] # assert is the point in tests
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`E501` is ignored because `ruff format` already wraps — leaving it on means the linter
|
|
47
|
+
complains about lines the formatter deliberately left long (URLs, strings).
|
|
48
|
+
|
|
49
|
+
## ty for type checking
|
|
50
|
+
|
|
51
|
+
Astral's checker, Rust, 10–100× faster than mypy and pyright, and it shares uv's and ruff's
|
|
52
|
+
config surface.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv run ty check
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
It's **beta** as of 2026, targeting 1.0. That's fine for a side project — fast feedback beats
|
|
59
|
+
completeness — but know the tradeoff: expect occasional missing features and false positives
|
|
60
|
+
on heavy metaprogramming. If ty chokes on a specific project, `basedpyright` is the fallback
|
|
61
|
+
(strictly better defaults than pyright, same engine lineage). Swap it in wholesale; don't run
|
|
62
|
+
two checkers.
|
|
63
|
+
|
|
64
|
+
Annotate as you go rather than retrofitting. A checker on unannotated code reports almost
|
|
65
|
+
nothing and gives false confidence.
|
|
66
|
+
|
|
67
|
+
## Scripts
|
|
68
|
+
|
|
69
|
+
Python has no `package.json` scripts. Either define them in `pyproject.toml` under a task
|
|
70
|
+
runner, or — simpler for a side project, and what the hooks call anyway — keep them as plain
|
|
71
|
+
commands in the lefthook config and the README:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv run ruff check --fix .
|
|
75
|
+
uv run ruff format .
|
|
76
|
+
uv run ty check
|
|
77
|
+
uv run pytest
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
In a mixed JS/Python repo, put these behind `bun run check` too, so one command still covers
|
|
81
|
+
the whole repo.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# React
|
|
2
|
+
|
|
3
|
+
Everything in [typescript.md](typescript.md) applies first. This adds what's React-specific.
|
|
4
|
+
|
|
5
|
+
## Linting — no ESLint needed
|
|
6
|
+
|
|
7
|
+
oxlint's `react` plugin covers `eslint-plugin-react`, `eslint-plugin-react-hooks`,
|
|
8
|
+
`eslint-plugin-react-refresh`, **and** the React Compiler rules. `jsx-a11y` is a separate
|
|
9
|
+
plugin. So a React project needs no ESLint and no `eslint-plugin-*` dependencies at all —
|
|
10
|
+
if you find them, they're being replaced, not kept alongside.
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"plugins": ["typescript", "unicorn", "oxc", "import", "promise", "react", "jsx-a11y"],
|
|
15
|
+
"categories": { "correctness": "error", "suspicious": "warn", "perf": "warn" }
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Note that listing `plugins` overwrites the defaults — `typescript`, `unicorn`, and `oxc` are
|
|
20
|
+
in the list because they'd otherwise be dropped.
|
|
21
|
+
|
|
22
|
+
## The three dev tools
|
|
23
|
+
|
|
24
|
+
All three are development-only and none of them belong in a production bundle. They do
|
|
25
|
+
different jobs; installing one is not a reason to skip the others.
|
|
26
|
+
|
|
27
|
+
### react-doctor — static audit, runs in CI
|
|
28
|
+
|
|
29
|
+
Scans the codebase and returns a health score with file-level diagnostics across ~60 rules:
|
|
30
|
+
state and effects, performance, architecture, security, bundle size, accessibility, dead code.
|
|
31
|
+
It's a CLI, so it's the only one of the three that can gate a build.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bunx react-doctor@latest
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Wire it as a `doctor` script. Treat the score as a trend line, not a gate — it moves when
|
|
38
|
+
rules change upstream. If you do gate on it, gate on "no new *errors*", never on the number.
|
|
39
|
+
|
|
40
|
+
### react-scan — render performance, runs in the browser
|
|
41
|
+
|
|
42
|
+
Highlights components that re-render and why, live in the running app. This is the tool for
|
|
43
|
+
"the page feels slow" — it shows you the actual offending component instead of you guessing
|
|
44
|
+
at `memo` placement.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// entry file, before React renders
|
|
48
|
+
if (import.meta.env.DEV) import('react-scan').then(({ scan }) => scan())
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Import it **before** React, or it can't instrument the renderer. There's also
|
|
52
|
+
`bunx react-scan@latest <url>` to point it at a running app without touching the code.
|
|
53
|
+
|
|
54
|
+
### react-grab — hand UI to the agent
|
|
55
|
+
|
|
56
|
+
Hover an element in the running app, `⌘C`, and the component stack with source locations
|
|
57
|
+
(`<a> in LoginForm (at components/login-form.tsx:46:19)`) lands on the clipboard, ready to
|
|
58
|
+
paste into an agent prompt. It's the fastest way to answer "which file is this button in".
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bunx grab@latest init
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The init command wires it for the detected framework. Whatever it writes, verify the setup is
|
|
65
|
+
dev-guarded — `import.meta.env.DEV` for Vite, `process.env.NODE_ENV === "development"` for
|
|
66
|
+
Next.js.
|
|
67
|
+
|
|
68
|
+
## React Compiler
|
|
69
|
+
|
|
70
|
+
If the project is on the compiler, oxlint's react plugin already reports compiler rule
|
|
71
|
+
violations, so you don't need a separate healthcheck pass in CI. If it isn't on the compiler,
|
|
72
|
+
enabling it is usually a better performance investment than hand-placed `memo`/`useMemo` —
|
|
73
|
+
raise it, but don't turn it on unasked; it changes runtime behavior.
|
|
74
|
+
|
|
75
|
+
## Testing
|
|
76
|
+
|
|
77
|
+
Component tests need a DOM, which `bun test` can provide via `happy-dom`. That's enough for
|
|
78
|
+
render-and-assert. Use `vitest` with browser mode instead when the tests need real layout,
|
|
79
|
+
real events, or visual assertions — that's the one case where the extra dependency pays.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# TypeScript / JavaScript
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun add -D typescript oxlint oxlint-tsgolint oxfmt knip
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`knip` ships a `knip-bun` binary — use that one in a Bun project, it resolves Bun's module
|
|
10
|
+
graph correctly.
|
|
11
|
+
|
|
12
|
+
## `tsconfig.json`
|
|
13
|
+
|
|
14
|
+
TypeScript 7 changed the defaults. `strict` is now **on** by default, and `types` defaults to
|
|
15
|
+
`[]` instead of hoovering up every `@types` package in `node_modules` — list what you actually
|
|
16
|
+
need. Gone entirely: `target: es5`, AMD/UMD/SystemJS modules, `baseUrl`, classic module
|
|
17
|
+
resolution. If a repo's config uses any of them, it needs migrating, not copying.
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"compilerOptions": {
|
|
22
|
+
"lib": ["ESNext", "DOM"],
|
|
23
|
+
"module": "preserve",
|
|
24
|
+
"moduleResolution": "bundler",
|
|
25
|
+
"target": "ESNext",
|
|
26
|
+
"types": ["bun"],
|
|
27
|
+
"noEmit": true,
|
|
28
|
+
"verbatimModuleSyntax": true,
|
|
29
|
+
"noUncheckedIndexedAccess": true,
|
|
30
|
+
"noImplicitOverride": true,
|
|
31
|
+
"erasableSyntaxOnly": true
|
|
32
|
+
},
|
|
33
|
+
"include": ["**/*.ts"],
|
|
34
|
+
"exclude": ["node_modules", "dist"]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`noUncheckedIndexedAccess` is the one strict-adjacent flag `strict` doesn't turn on and the
|
|
39
|
+
one that catches the most real bugs. `erasableSyntaxOnly` keeps the code runnable by anything
|
|
40
|
+
that only strips types — no `enum`, no parameter properties, no namespaces.
|
|
41
|
+
|
|
42
|
+
## `.oxlintrc.json`
|
|
43
|
+
|
|
44
|
+
`plugins` **overwrites** the default set, so list every plugin you want, including the ones
|
|
45
|
+
that were on by default (`typescript`, `unicorn`, `oxc`).
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
|
|
50
|
+
"plugins": ["typescript", "unicorn", "oxc", "import", "promise", "node"],
|
|
51
|
+
"categories": { "correctness": "error", "suspicious": "warn", "perf": "warn" },
|
|
52
|
+
"options": { "typeAware": true },
|
|
53
|
+
"ignorePatterns": ["dist", "node_modules"]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Type-aware linting is stable and covers 59 of typescript-eslint's 61 type-aware rules. It
|
|
58
|
+
needs `oxlint-tsgolint` installed, and it is the slow path — see the fast/whole-project split
|
|
59
|
+
in `SKILL.md`. On a very large codebase watch memory; if it thrashes, keep `typeAware` out of
|
|
60
|
+
the config and pass `--type-aware` only in the CI script.
|
|
61
|
+
|
|
62
|
+
## Formatting
|
|
63
|
+
|
|
64
|
+
`oxfmt` is Prettier-compatible in output and needs no config for the default style. It is
|
|
65
|
+
beta as of 2026, and it still delegates Markdown to Prettier internally — fine, but if a
|
|
66
|
+
project is mostly Markdown, or `oxfmt` mangles something, Prettier alone is the fallback.
|
|
67
|
+
**Never run both** — pick one and delete the other's config.
|
|
68
|
+
|
|
69
|
+
## Scripts
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"scripts": {
|
|
74
|
+
"typecheck": "tsc --noEmit",
|
|
75
|
+
"lint": "oxlint",
|
|
76
|
+
"lint:fix": "oxlint --fix",
|
|
77
|
+
"format": "oxfmt",
|
|
78
|
+
"format:check": "oxfmt --check",
|
|
79
|
+
"knip": "knip-bun",
|
|
80
|
+
"test": "bun test",
|
|
81
|
+
"check": "bun run typecheck && bun run lint && bun run format:check && bun run knip"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`check` is the single command hooks, CI, and the user all run. Keep it that way — the moment
|
|
87
|
+
CI runs something the local `check` doesn't, the hooks stop being trustworthy.
|
|
88
|
+
|
|
89
|
+
## Testing
|
|
90
|
+
|
|
91
|
+
`bun test` is the default: no dependency, no config, Jest-compatible API, and it runs
|
|
92
|
+
TypeScript directly. Reach for `vitest` only when you need what Bun's runner doesn't have —
|
|
93
|
+
component/DOM testing, browser mode, or a rich mocking surface. Adding `vitest` for a project
|
|
94
|
+
of pure logic tests is a dependency you'll maintain for nothing.
|
|
95
|
+
|
|
96
|
+
## Libraries
|
|
97
|
+
|
|
98
|
+
Only when the package is published:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
bun add -D tsdown publint @arethetypeswrong/cli
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`tsdown` builds and emits declarations; `publint` checks the package manifest, and
|
|
105
|
+
`attw --pack .` checks that the types actually resolve for consumers under every module
|
|
106
|
+
resolution mode. Both belong in the `check` script for a library — a broken `exports` map is
|
|
107
|
+
invisible until someone else installs it.
|
|
108
|
+
|
|
109
|
+
## Knip
|
|
110
|
+
|
|
111
|
+
The first `knip` run on an existing codebase reports a lot, and much of it is intentional
|
|
112
|
+
public API rather than dead code. Triage it: real removals go, deliberate entry points go in
|
|
113
|
+
`knip.json` under `entry`/`ignore` — with the reason in a comment. Don't wire it into a
|
|
114
|
+
blocking hook until it reports clean once.
|