Utility Script Standards
Rules for one-off and operational scripts requiring idempotency, a default dry-run mode, structured output, and meaningful exit codes.
Use when: Attach to the scripts directory of any Node/TypeScript repo so data fixes, backfills, and ops scripts are safe to rerun and easy to audit.
The skill file
Cursor · .cursor/rules/*.mdc.cursor/rules/*.mdc3348 chars
---
description: Safety and output conventions for files under scripts/
globs: "scripts/**"
alwaysApply: false
---
# Script rules
Scripts here touch real data and run unattended in CI or by tired humans at 2am. Every script follows these rules — no exceptions for "quick one-offs", because one-offs get rerun.
## Idempotency
- Running a script twice must produce the same end state as running it once. Design for it explicitly:
- inserts use upserts or existence checks, never blind `INSERT`;
- file generation writes to a temp path then renames atomically;
- external calls (emails, webhooks, charges) check a recorded marker before sending and record completion after.
- Process records in stable order with a resumable cursor (last processed id), so a crash mid-run can resume instead of restarting. Print the cursor on exit.
- Batch mutations: chunks of ≤ 500 with progress output per chunk. Never one transaction around the whole run.
## Dry-run by default
- Mutating scripts run in dry-run mode unless `--apply` (or `--no-dry-run`) is passed. Dry-run executes the full read path and prints exactly what WOULD change: counts, sample ids, before→after values.
- The first line of output always states the mode: `mode=dry-run` or `mode=APPLY`. In apply mode against a production `DATABASE_URL`, require `--yes` or an interactive confirmation naming the target (`Type "production" to continue`).
- Parse flags with `node:util` `parseArgs` (or the repo's CLI lib) — no hand-rolled `process.argv[2]` positional guessing. Every script supports `--help` listing flags, env vars read, and an example invocation.
## Structured output
- Progress and results go to stdout as one JSON object per line: `{"level":"info","event":"chunk_done","processed":500,"failed":0}`. Human-only decoration goes to stderr.
- Always end with a single summary line: `{"event":"summary","mode":"dry-run","scanned":12000,"changed":340,"failed":2,"duration_ms":8120}` — this is what CI greps and what gets pasted into the incident channel.
- Collect per-record failures (id + reason) and report them in the summary; do not abort the whole run on the first bad record unless `--fail-fast` is passed.
- No bare `console.log("done")`, no `console.table` in scripts meant for CI.
## Exit codes
- `0` — completed, zero failures. `1` — completed with per-record failures (summary still printed). `2` — bad usage: unknown flag, missing env var, failed precondition (exit before touching anything). `3+` — fatal runtime error mid-run.
- Validate every required env var and connectivity precondition up front; fail with exit 2 and a message naming the missing variable before any mutation starts.
- Handle `SIGINT`/`SIGTERM`: stop taking new chunks, print the summary with current cursor, exit `130`. A Ctrl-C must never leave a half-applied chunk uncommitted-but-unknown.
- Top-level code is wrapped in `main().catch(err => { print error event; process.exit(3); })` — an unhandled rejection that exits 0 is the worst possible outcome.
## Misc
- Scripts are TypeScript, run via the repo's runner (`tsx`/`bun`), and import shared db/env modules from `src/` — never duplicate a connection string or schema definition inside a script.
- Each script starts with a comment header: purpose, example invocation, owner, and date — delete the script when its purpose is done.
Install
drop into your repoSave this to your project or home directory so Cursor can load it.
path./.cursor/rules/node-scripts-rules.mdc
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…