DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Four byte-identical copies of the same argv parser, and the dry-run habit that only our two newest scripts have

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_URL to override; note that .env.local is 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.ts runs exactly the query the nightly aggregation cron runs, for every game, and writes nothing; its header says READ-ONLY in capitals. check-exercises.ts validates content in the library. run-affiliate-payouts.ts with no flags prints what is owed.
  • Dry run by default, writes only with --apply (2). create-unlocked-account.ts and rescore-mst-ace.ts.
  • Writes on invocation (the rest). pnpm affiliate:create --name "..." --email "..." mints a code and prints it. pnpm db:seed-population-stats inserts. pnpm mp:create-products creates Stripe products, guarded by a check that refuses to run against a live Stripe key unless ALLOW_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;
}
Enter fullscreen mode Exit fullscreen mode

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>'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)