CogniPrep has an scripts/ directory that has grown for four months without anyone auditing it. Today I counted it.
47 files. Ten of the directories inside it are per-provider content generators, which are the one part of this repo I do not write about. What is left is the operations surface: 12 TypeScript files totalling 3,468 lines, plus three small tool directories holding two Python files and one Bash script, 113 lines between them.
The repo is otherwise entirely TypeScript. Every .py and .sh file in it, 19 in total, lives under scripts/.
What is wired, and what is not
package.json has 25 script entries. Nine of them point at one of these 12 files. The other three are run by typing the path:
| Script | Lines | First committed | Reachable as |
|---|---|---|---|
seed-population-stats.ts | 1,262 | 19 Jul 2026 | pnpm db:seed-population-stats |
check-population-stats.ts | 142 | 10 Aug 2026 | pnpm db:check-popstats |
check-exercises.ts | 137 | 13 Aug 2026 | pnpm check:exercises |
create-affiliate-code.ts | 132 | 18 Aug 2026 | pnpm affiliate:create |
create-affiliate-invite.ts | 101 | 18 Aug 2026 | pnpm affiliate:invite |
create-managed-payments-products.ts | 219 | 18 Aug 2026 | pnpm mp:create-products |
run-affiliate-payouts.ts | 103 | 18 Aug 2026 | pnpm affiliate:payout |
seed-sector-questions.ts | 719 | 2 Sep 2026 | pnpm db:seed-sector-questions |
submit-indexnow.ts | 64 | 13 Sep 2026 | pnpm indexnow |
create-unlocked-account.ts | 144 | 7 Oct 2026 | path only |
game-screenshots.ts | 182 | 12 Oct 2026 | path only |
rescore-mst-ace.ts | 263 | 5 Nov 2026 | path only |
Seven of the nine entries are tsx --env-file=.env.local scripts/<name>.ts. The two that are not are the two scripts that never open a database connection, and in one case that is deliberate rather than incidental:
Submits against the production host by default. Set
NEXT_PUBLIC_WEBSITE_URLto override; note that.env.localis deliberately not loaded, because a localhost value in there would produce a submission IndexNow rejects.
A missing flag that looks like an oversight is sometimes the flag doing its job.
The dry-run gradient
Sort the 12 by what they do before you have finished pressing Enter:
- Read-only by design (3).
check-population-stats.tsruns exactly the query the nightly aggregation cron runs, for every game, and writes nothing; its header saysREAD-ONLYin capitals.check-exercises.tsvalidates content in the library.run-affiliate-payouts.tswith no flags prints what is owed. - Dry run by default, writes only with
--apply(2).create-unlocked-account.tsandrescore-mst-ace.ts. - Writes on invocation (the rest).
pnpm affiliate:create --name "..." --email "..."mints a code and prints it.pnpm db:seed-population-statsinserts.pnpm mp:create-productscreates Stripe products, guarded by a check that refuses to run against a live Stripe key unlessALLOW_LIVE=true.
The dates in that table are the whole story. The two scripts with an --apply gate were committed on 7 October and 5 November. Everything from the August cohort does its work immediately. Nobody decided that; the habit simply arrived later than the scripts did, and no one went back.
If you want one rule out of this post: the flag that costs nothing is the one that makes the destructive path explicit. --apply is four characters in process.argv and it converts "I ran the wrong script" into "I printed the wrong plan".
Four copies of six lines
None of the 12 files imports another. Seven of them read process.argv, and four contain this function, byte for byte identical in all four (same MD5, same whitespace):
function arg(flag: string): string | undefined { const index = process.argv.indexOf(`--${flag}`); if (index === -1) return undefined; const value = process.argv[index + 1]; return value && !value.startsWith('--') ? value : undefined; } Two of them also carry the same percent(flag, fallback) helper. There is no yargs, commander or minimist anywhere in the dependency tree, which is the right call for a handful of flags, and the copy count is the price of it being the right call four separate times.
The fifth parser is different: submit-indexnow.ts has a parseArgs(argv) that takes positional URLs as well as flags, because pnpm indexnow /blogs/my-new-post is the thing you actually want to type after publishing one article.
Nothing here is tested
377 test files, and not one of them imports anything from scripts/. Five test files mention a script in a comment, always to explain where a fixture came from. That is defensible for a one-off, and less so for seed-population-stats.ts, which at 1,262 lines is the largest file in the directory and the one whose output stands in for real population data on a game until enough real sessions exist to replace it.
See it
The output of one of these is public. submit-indexnow.ts reads its URL list from the sitemap, which is the same list the search engines get:
curl -s https://cogniprep.app/sitemap.xml | grep -c '<loc>' 530 today, and that is the list the script builds before it submits anything. Its --dry-run flag exists so you can read the list without telling four search engines that 530 pages changed.
The nightly aggregation that check-population-stats.ts rehearses has a public endpoint per game, so you can see the result of the write path without having a database:
curl -s https://cogniprep.app/api/games/population-stats/wlc-cognitive | head -c 200 That is the row the seeder seeds and the checker checks, and the Wonderlic hub is the page that reads it.
Count your own scripts/ directory this week. The interesting column is not what each file does; it is which ones you can run by accident.

