cli-scripts
Standard pattern for writing CLI scripts using Commander.js
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Standard pattern for writing CLI scripts using Commander.js
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Generate WordPress-style crawlers for public infrastructure disruption sources
Guidelines for logging in the ingest and web packages
Reminders and checks when working on onboarding/on boarding/on-boarding features
Color system, button styling, and theme conventions for the web package
| name | cli-scripts |
| description | Standard pattern for writing CLI scripts using Commander.js |
| version | 1.0.0 |
| keywords | ["cli","script","commander","argv","process.argv"] |
IMPORTANT: Using CLI Scripts skill! All JS/TS scripts that accept command-line arguments must use Commander.js. Never use raw process.argv parsing.
Every CLI script in ingest/ follows this structure:
#!/usr/bin/env node
import { Command } from "commander";
import dotenv from "dotenv";
import { resolve } from "node:path";
import { logger } from "@/lib/logger";
const program = new Command();
program
.name("script-name")
.description("One-line description of what this script does")
.option("--dry-run", "Preview changes without writing")
.option("-s, --source <type>", "Filter by source type")
.option("-l, --limit <n>", "Maximum items to process", Number.parseInt)
.addHelpText(
"after",
`
Examples:
$ pnpm tsx ingest/scripts/script-name
$ pnpm tsx ingest/scripts/script-name --dry-run
`,
)
.action(async (opts) => {
// Load env INSIDE the action — never at module level
dotenv.config({ path: resolve(process.cwd(), ".env.local") });
try {
await run(opts);
} catch (error) {
logger.error("Fatal error", {
error: error instanceof Error ? error.message : String(error),
});
process.exit(1);
}
});
program.parse();
For scripts with async actions use program.parseAsync() instead of program.parse().
process.argv.includes(), process.argv.find(), or manual argument loops.dotenv.config() inside .action() — never at the top level of the file. This prevents env vars from loading during import/parse time (critical for Firebase Admin)..action() — any module that transitively imports firebase-admin or other heavy deps must be imported inside the action, not at the top of the file.program.parse() at the bottom — the last statement of top-level scripts.program.parseAsync().catch(...) if the action is async and you need top-level error handling.ingest()), wrap the Commander setup in if (require.main === module) { ... } so the module stays importable without side effects.ingest/crawl.ts — required option, dynamic import, program.parse()ingest/pipeline.ts — multiple boolean flags, addHelpText with dynamic contentingest/sources-clean.ts — requiredOption, --dry-runingest/notify.ts — no options, simple actioningest/messageIngest/from-sources.ts — wrapped in require.main === module, exports ingest()ingest/scripts/reprocess-failed-messages.ts — --execute flag (inverted dry-run), --days <n>import { Command } from "commander" is presentprocess.argv access anywhere in the filedotenv.config() is called inside .action(), not at module level.action()program.parse() or program.parseAsync() is the last statement.name() and .description() are setAll one-off / maintenance scripts live in ingest/scripts/. Before writing a new script, check whether one already exists.
Every script in that directory has its purpose, steps, and usage documented in the first ~20 lines as a JSDoc/block comment. To discover what's available, read just those headers:
# List all scripts
ls ingest/scripts/
# Skim the header of any candidate
head -20 ingest/scripts/<name>.ts
When asked to perform a data operation (delete messages, reprocess sources, seed the emulator, manage geocode cache, etc.), always list ingest/scripts/ and read the top of relevant files before writing anything new.