Conventions and guardrails for humans and AI agents working in this repo. Keep this
file short; the full design narrative lives in docs/ARCHITECTURE.md.
An ESM TypeScript CLI (decodo) that wraps the Decodo Web Scraping API via
@decodo/sdk-ts. It turns each API
scrape target into a subcommand and adds shell-native output modes. Built with
Commander, bundled as ESM, run on Node >= 18
(developed and CI-tested on Node 24).
src/ is split into feature modules. Each module owns one concern and follows the
same internal shape:
src/<module>/
commands/ Commander command factories (createXCommand) and option wiring
services/ Pure logic, IO, SDK calls — one responsibility per file
types/ Shared TypeScript types/interfaces for the module
errors/ Custom Error subclasses
constants.ts Module-level constant values
Modules:
cli/— root command registration, ordering, global options, verbose loggingauth/— token resolution (flag > env > config file), setup/reset/whoami commandsscrape/— scrape/search/screenshot commands, schema loading, request building, the SDK clientoutput/— output format handling (text, JSON, NDJSON, PNG), request defaultsplatform/— OS-level concerns: config paths, file/binary writes, hidden prompts, central error handler
- New scrape target — nothing to do; targets are generated from the API schema at
runtime in
scrape/commands/codegen-target-commands.ts. Don't hand-write a command per target. - New top-level command — add a
createXCommandfactory under the owning module'scommands/, register it incli/register.ts(orscrape/register.ts), and add its name toROOT_COMMAND_ORDERincli/services/sort-commands-by-order.ts. - New reusable logic — a new single-purpose file in the relevant
services/. - New error type — subclass
Errorin the module'serrors/, then map it to an exit code inplatform/services/handle-cli-error.ts.
- Keep each file to one responsibility; prefer small files over multi-purpose ones.
- Write self-explanatory code — no comments (names and structure carry intent).
- Use
.jsextensions on all relative imports (ESM +forceJsExtensionslint rule). - Throw typed errors (
ValidationError,AuthRequiredError, SDK errors) and lethandleCliErrorrender them and pick the exit code — seeEXITinplatform/constants.ts. - Write user-facing output to stdout; write logs, warnings, and verbose lines to stderr
(use
verboseLog). The CLI must stay pipe-friendly. - Add a mirrored test under
tests/for every newsrc/file (see Testing).
- Don't add comments or leave commented-out code.
- Don't hardcode scrape targets, parameters, or option flags — derive them from the schema.
- Don't
console.logfor diagnostics orprocess.exitoutsidehandle-cli-error.ts/configure-commander-exit.ts. - Don't read or write
~/.configpaths directly — go throughplatform/services/paths.tsand theauth/services/config.tshelpers (config is written0o600). - Don't omit the
.jsimport extension (lint will fail).
- Tests live in
tests/and mirrorsrc/1:1 (src/auth/services/config.ts→tests/auth/services/config.test.ts). - Use Vitest. Import the unit under test with a dynamic
import()aftervi.resetModules()when module state or env matters (seetests/auth/services/resolve-token.test.ts). - Isolate filesystem/config side effects with the helper in
tests/platform/helpers/.
pnpm build # tsc -> build/esm
pnpm typecheck # tsc --noEmit
pnpm lint # ultracite check (Biome)
pnpm fix # ultracite fix
pnpm test # vitest run (runs pnpm build first)CI runs lint, typecheck, build, and test on every PR — all four must pass.