diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 3ba6bcc..dba8705 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -2,9 +2,19 @@ # Native Git hook - enable with: git config core.hooksPath .githooks set -euo pipefail -git diff --cached --check +ignored_staged_paths=( + ":(exclude).agents/**" + ":(exclude).claude/**" +) + +git diff --cached --check -- . "${ignored_staged_paths[@]}" has_staged_changes() { + if [[ "$#" -eq 0 ]]; then + git diff --cached --quiet -- . "${ignored_staged_paths[@]}" || return 0 + return 1 + fi + git diff --cached --quiet -- "$@" || return 0 return 1 } @@ -13,6 +23,10 @@ if [[ ! -f package.json ]]; then exit 0 fi +if ! has_staged_changes; then + exit 0 +fi + # `npm pkg fix` normalizes package.json (e.g., sorts keys, trims invalid # fields). Only run it when package.json is already staged, so the hook never # pulls unrelated working-tree edits into a commit. @@ -21,52 +35,61 @@ if has_staged_changes package.json && command -v npm >/dev/null 2>&1; then git add package.json 2>/dev/null || true fi -run_script() { +package_script_exists() { local script_name="$1" # Check if the script exists in package.json (node - reads from stdin, arg becomes argv[2]) - if node - "$script_name" <<'EOF' + node - "$script_name" <<'EOF' const fs = require("fs"); const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); const scripts = pkg.scripts || {}; process.exit(scripts[process.argv[2]] ? 0 : 1); EOF - then - if command -v pnpm >/dev/null 2>&1; then - pnpm -s run "$script_name" - elif command -v npm >/dev/null 2>&1; then - npm run --silent "$script_name" - else - echo "No package manager available to run script: $script_name" >&2 - exit 1 - fi +} + +run_package_script() { + local script_name="$1" + if command -v pnpm >/dev/null 2>&1; then + pnpm run "$script_name" + elif command -v npm >/dev/null 2>&1; then + npm run "$script_name" + else + # return, not exit: callers redirect this function's output into a log + # they only dump on failure — exit here would abort before the dump, + # leaving the commit to die with no visible error. + echo "No package manager available to run script: $script_name" >&2 + return 127 + fi +} + +script_log=$(mktemp) +trap 'rm -f "$script_log"' EXIT + +# Silent on success; on failure, dump captured output and exit non-zero. +run_script() { + local script_name="$1" + if ! package_script_exists "$script_name"; then + return 0 + fi + echo "==> $script_name" >&2 + if run_package_script "$script_name" > "$script_log" 2>&1; then + return 0 fi + cat "$script_log" >&2 + exit 1 } if [[ -f pnpm-lock.yaml ]] && command -v pnpm >/dev/null 2>&1; then if has_staged_changes package.json pnpm-lock.yaml pnpm-workspace.yaml; then # Order matters: dedupe is the cheaper read-only check, so fail fast on # lockfile bloat before the heavier lockfile-vs-package.json sync check. - pnpm dedupe --check + pnpm dedupe --check --config.confirm-modules-purge=false pnpm install --frozen-lockfile --ignore-scripts --config.confirm-modules-purge=false fi fi -if has_staged_changes; then - run_script "format:check" -fi - +run_script "format:check" run_script "knip" run_script "typecheck" run_script "lint" run_script "fta" - -# Run tests quietly - only show output on failure -test_log=$(mktemp) -trap 'rm -f "$test_log"' EXIT - -if run_script "test" > "$test_log" 2>&1; then - : # Success, stay silent -else - cat "$test_log" >&2 - exit 1 -fi +run_script "test" diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 906701f..f900953 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -9,12 +9,8 @@ on: jobs: quality-checks: - # Private dev dependencies require NPM_TOKEN, which fork PRs cannot access. - if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest timeout-minutes: 10 - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -28,9 +24,10 @@ jobs: node-version-file: "package.json" cache: "pnpm" - - name: Configure npm.j4k.dev auth - if: ${{ env.NPM_TOKEN != '' }} - run: printf "registry=https://npm.j4k.dev/\n//npm.j4k.dev/:_authToken=%s\n" "${NPM_TOKEN}" > .npmrc + - name: Configure registry auth + run: echo "//npm.j4k.dev/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Install dependencies run: pnpm install --frozen-lockfile @@ -50,8 +47,8 @@ jobs: - name: Run fta run: pnpm fta - - name: Run tests - run: pnpm run test - - name: Run build run: pnpm build + + - name: Run tests + run: pnpm run test diff --git a/.github/workflows/dedupe-check.yml b/.github/workflows/dedupe-check.yml index 1fe1d8b..08d0eba 100644 --- a/.github/workflows/dedupe-check.yml +++ b/.github/workflows/dedupe-check.yml @@ -13,12 +13,8 @@ on: jobs: dedupe-check: - # Private dev dependencies require NPM_TOKEN, which fork PRs cannot access. - if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest timeout-minutes: 5 - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -32,9 +28,10 @@ jobs: node-version-file: "package.json" cache: "pnpm" - - name: Configure npm.j4k.dev auth - if: ${{ env.NPM_TOKEN != '' }} - run: printf "registry=https://npm.j4k.dev/\n//npm.j4k.dev/:_authToken=%s\n" "${NPM_TOKEN}" > .npmrc + - name: Configure registry auth + run: echo "//npm.j4k.dev/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index c80422b..c7cd2dd 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -42,22 +42,19 @@ jobs: uses: actions/setup-node@v6 with: node-version-file: "package.json" - registry-url: https://registry.npmjs.org cache: "pnpm" - - name: Configure npm.j4k.dev auth + - name: Configure registry auth (install) + run: echo "//npm.j4k.dev/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - if [ -z "${NPM_TOKEN}" ]; then - echo "NPM_TOKEN is required to install private dependencies." >&2 - exit 1 - fi - printf "registry=https://npm.j4k.dev/\n//npm.j4k.dev/:_authToken=%s\n" "${NPM_TOKEN}" > .npmrc + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Configure public npm registry (publish) + run: echo "registry=https://registry.npmjs.org/" >> ~/.npmrc + - name: Release id: release env: diff --git a/.vscode/settings.json b/.vscode/settings.json index aba257d..eb3b23d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,3 @@ { - "js/ts.experimental.useTsgo": true, - "typescript.native-preview.tsdk": "./node_modules/@typescript/native-preview" + "js/ts.experimental.useTsgo": true } diff --git a/AGENTS.md b/AGENTS.md index f5ea230..2defed1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,42 +117,6 @@ email.bulkSend(generateExpiryEmails(getExpiredUsers(db.getUsers(), new Date()))) Test the functional core, not the shell. Core tests are fast, deterministic, and need no mocks; the shell becomes thin orchestration where bugs are easy to spot through review. If shell tests are explicitly requested, prefer integration tests over unit tests with mocks. -# Rule: Inline Obvious Code - -Keep simple, self-explanatory code inline rather than extracting it into functions. Every abstraction carries cognitive cost—readers must jump to another location, parse a signature, and track context. For obvious logic, this overhead exceeds any benefit. - -Extracting code into a function is not inherently virtuous. A function should exist because it encapsulates meaningful complexity, not because code appears twice. - -```ts -// GOOD: Inline obvious logic -if (removedFrom.length === 0) { - return { ok: true, message: "No credentials found" }; -} -return { ok: true, message: `Removed from ${removedFrom.join(" and ")}` }; - -// BAD: Extraction hides obvious logic behind indirection -return formatRemovalResult(removedFrom); -``` - -## When to extract - -Extract when duplication causes real maintenance risk, not merely because code appears twice: - -- A name clarifies complex intent -- Multiple call sites must stay in lockstep and silent divergence would be a bug -- The function encapsulates a coherent standalone concept -- Testing it in isolation provides value - -Don't extract for hypothetical reuse: - -- For a single caller -- Because "we might need this elsewhere" -- When the name describes implementation rather than purpose - -## The wrong abstraction - -Abstractions decay when requirements diverge: programmer A extracts duplication into a shared function, programmer B adds a parameter for different behavior, and this repeats until the "abstraction" is a mess of conditionals. When an abstraction proves wrong, re-introduce duplication and let the code show you what's actually shared. Duplication is far cheaper than the wrong abstraction. - # Rule: No Logic in Tests Write test assertions as concrete input/output examples, not computed values. Avoid operators, string concatenation, loops, and conditionals in test bodies—these obscure bugs and make tests harder to verify at a glance. diff --git a/package.json b/package.json index 5007092..8772388 100644 --- a/package.json +++ b/package.json @@ -81,5 +81,5 @@ "engines": { "node": ">=24.0.0" }, - "packageManager": "pnpm@10.33.0" + "packageManager": "pnpm@11.5.1" } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a95e5cf..c813e2e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ allowBuilds: esbuild: true enableGlobalVirtualStore: true +registries: + default: https://npm.j4k.dev/