Skip to content

Latest commit

 

History

History
244 lines (211 loc) · 16.3 KB

File metadata and controls

244 lines (211 loc) · 16.3 KB

Seamless CLI Agent Guide

This repository is the Seamless Auth command-line tool (published as seamless-cli, invoked as seamless or npx create-seamless). It does two things:

  • Scaffold a working Seamless Auth project (seamless init): generates a React frontend, an Express adapter, the auth server, a Docker Compose file, and config.
  • Verify the whole auth surface (seamless verify): a cross-package conformance harness that runs an api / adapter / react matrix against the ecosystem.

Use this file as the fast path. The verify harness has its own moving parts under verify/ (a Docker Compose stack plus a Playwright harness).

Working Standards (fells-code baseline)

These rules apply to every repository in the fells-code org. Repo-specific guidance may extend them but must not contradict them.

Attribution

  • Commit and open PRs solely under the repository owner's identity. Never commit under an agent or assistant identity.
  • Never attribute work to an AI assistant: no Co-Authored-By: Claude (or any assistant) trailers, no "Generated with" / "Created with Claude" notes, and no assistant branding or emoji anywhere in commit messages, PR or issue titles and descriptions, changesets, code comments, or docs.

Comments

  • Comment only when the code genuinely needs explaining: a non-obvious reason, a gotcha, or an invariant. Never narrate what the code plainly does.

TODOs

  • Every TODO/FIXME must reference a ticket, e.g. // TODO(#123): .... Do not leave a bare TODO. If no ticket exists, create one first.

Commits & branches

  • Conventional Commits (feat:, fix:, chore:, docs:, ci:, test:).
  • Descriptive branch names (feat/..., fix/...); never a claude/ or other tool-generated prefix.

Public-facing text

  • No em dashes in commit messages, code comments, PR or issue text, changesets, or docs. Use a comma, parentheses, or a separate sentence.

Before declaring work done

  • All code quality checks must pass before you open a PR or call the work done. Run them and report the real output; do not open a PR while any check is failing.
  • Commands: npm run build (runs tsc, which type-checks) and npm test (vitest run); npm run coverage enforces the coverage thresholds. There is no separate lint/format tooling configured yet. Never claim a change works without running these.
  • Match the surrounding code's style, naming, and comment density.

Start Here

  • Install dependencies: npm install
  • Build (type-check and emit): npm run build (tsc, output in dist/)
  • Run from source: npm run dev -- <command> (tsx); or after building, node dist/index.js <command>
  • Commands: init [name], templates, check, verify [flags], apps, and the instance-management commands profile, login, whoami, logout, sessions, config, users, org (all dispatched from src/index.ts)

The entry point is src/index.ts, which dispatches to a command module in src/commands/.

Commands

  • init (src/commands/init.ts) scaffolds a project, driven by src/prompts/. The web and api starters come from the registry-driven template source (src/core/templates.ts): it reads registry.json from the fells-code/seamless-templates monorepo (pinned by SEAMLESS_TEMPLATES_REF in src/core/images.ts), downloads the selected templates, and applies each template's template.json env contract. The auth, docker, and config pieces are still generated locally in src/generators/*. Override the template source for development with SEAMLESS_TEMPLATES_DIR (a local checkout) or SEAMLESS_TEMPLATES_REF (a different ref).
    • A --<id> or --<alias> flag (e.g. seamless init --react-oauth, seamless init --oauth) preselects the matching template and skips that layer's prompt. Both spellings live in the registry, so no per-flag code. resolveTemplateAliases runs in runCLI before the project directory is created and before the non-empty-directory confirmation, so an unknown flag can never route through a destructive prompt on its way to an error.
    • --yes runs the whole thing without prompting: every question has a flag (--web, --api, --email, --auth, --admin) and anything unspecified falls back to the option the prompt marks "(recommended)". --yes is never enough for a destructive step: overwriting a non-empty directory and rotating an existing service token both require --force, and choosing between a managed application and a local stack requires --app or --local. Flag parsing lives in parseInitArgs (src/index.ts); everything it produces is validated in runCLI before a directory is created.
    • Every prompt is fronted by requireInteractive (src/core/tty.ts), so a run without a TTY on stdin fails naming the flag that answers the question instead of rendering a prompt nobody can answer. This holds across every command, not just init. When adding a prompt anywhere, guard it the same way.
    • templates (src/commands/templates.ts) lists the registry (seamless templates list [--json]) so those ids and flags are discoverable without a checkout. It reads the same source init does and needs no login.
    • A template can declare setup.oauth in its template.json to trigger the OAuth provider prompts (src/prompts/oauthSetup.ts, catalog in src/core/oauthProviders.ts). The chosen providers are wired into the auth server env (OAUTH_PROVIDERS, per-provider *_CLIENT_SECRET, the oauth login method) by buildAuthEnv in src/generators/docker/docker.ts.
  • destructive confirmations go through confirmDestructive (src/core/confirmAction.ts), which answers itself when --force is set and otherwise asks. --force is the standing spelling for "do it without asking"; hasForceFlag also accepts --yes and -y, because config oauth-providers remove --yes shipped before the convention existed. --yes means something narrower on init (answer the ordinary questions, never the destructive ones), so do not add --yes alone to a destructive step. Cancelling a confirmation reads as declining, not as an error.
  • check health-checks a running stack (local or managed).
  • verify (src/commands/verify.ts) runs the conformance harness (below).
  • instance managementprofile (targets, plus profile login), logout/whoami, sessions, config (system config + OAuth providers), users, and org all talk to a running instance and are authenticated by the stored session.
  • helpseamless --help, seamless <command> -h/--help, and seamless help <command> all render from the single registry in src/commands/helpTopics.ts (src/commands/help.ts does the formatting, and COMMANDS there is also the dispatcher's known-command list). Document a new command or flag in that registry, not in the help template. src/index.ts answers the help flag before a command parses its own args.
  • portallogin signs in to the Seamless portal, a separate account from any instance profile. Its session lives beside the profile map in config.json and is the only one init uses to connect a managed application (src/core/authClient.ts exposes createPortalClient for it).

The verify harness

seamless verify stands up the ecosystem with Docker Compose and runs a Playwright matrix, then prints a flow x layer pass/fail grid (plus JUnit and HTML reports).

  • verify/docker-compose.verify.yml: postgres, the auth API, and both adapters, plus the React starter behind the react compose profile. The mock OIDC provider runs in-process in global-setup (it is not a container).
  • verify/adapter-app (port 3000) and verify/adapter-fastify-app (port 3001): minimal adopter backends on @seamless-auth/express and @seamless-auth/fastify, each with a capture transport so the harness can read OTP / magic-link codes the adapter would otherwise strip. They are deliberately twins: the same routes on the same env contract, so a spec cannot tell which one answered and any difference in behaviour is a real one. Keep them in step when either changes.
  • verify/harness: the Playwright projects (api, adapter, adapter-fastify, react), lib/ helpers, mock-oidc.ts, global-setup.ts, and lib/matrixReporter.ts (the printed grid). It has its own node_modules and browsers.
    • The two adapter projects run the same specs from ./adapter; only the adapterUrl project option differs (lib/fixtures.ts). Adding an adopter framework is a project entry plus a compose service, never a copy of the suite. Because they share a directory, matrixReporter takes the layer from the Playwright project name, not the spec's path.

Modes and sibling repos:

  • --local builds the @seamless-auth/* packages from source (pre-publish contract testing); the default uses the published packages.
  • The browser layer runs once per web template, not once. verify reads the templates registry and drives every kind: web entry that is not coming-soon, each served at :5173 in turn and scoped to the flow tags its template.json declares in verify.flows (the whole suite when it declares none). So react-oauth runs only @oauth, and react-vite runs everything.
  • The sibling repos are resolved relative to this repo, overridable with SEAMLESS_API_DIR, SEAMLESS_SERVER_DIR, SEAMLESS_REACT_SDK_DIR (the React SDK), and SEAMLESS_TEMPLATES_DIR (the templates checkout the web templates come from, defaulting to ../seamless-templates). SEAMLESS_REACT_DIR is the narrower override: it names a single template directory and runs that one instead of the registry's set.
  • Useful flags: --api-only, --no-react, --filter=<flow> (the = form; a space-separated --filter <flow> is not parsed), --keep-up.

Important Folders

  • src/commands: one file per CLI command
  • src/generators: locally generated scaffolding (auth, docker, config)
  • src/core: shared helpers (templates, exec, env, fetch, secrets, paths, package manager, output)
  • src/prompts: interactive setup prompts (@clack/prompts)
  • verify: the conformance harness (shipped with the package)

Templates are not in this repo — they live in the seamless-templates monorepo (SEAMLESS_TEMPLATES_REPO) and are fetched at scaffold time.

Conventions

  • TypeScript, ESM ("type": "module"). Local imports use .js extensions (NodeNext resolution).
  • Commit, comment, TODO, and attribution rules live in Working Standards above.
  • Releases use Changesets. A user-facing change needs a changeset (npm run changeset). A push to main opens a "version packages" PR that bumps the version and writes CHANGELOG.md; merging that PR publishes to npm. Do not hand-edit the version or CHANGELOG.md.
  • npm publish token. The release workflow publishes with the NPM_TOKEN repo secret. It must be a classic Automation token (full publish rights, bypasses 2FA) owned by an account with publish access to seamless-cli; a granular token restricted to a package allowlist cannot create or publish it and the registry returns a confusing E404 on the PUT.
  • Templates ref bump. Shipping a change that depends on a new templates release is a two-step, cross-repo dance: release seamless-templates first, then bump SEAMLESS_TEMPLATES_REF (src/core/images.ts) to that tag.
  • Coverage badge. README.md shows a line-coverage badge (resources/coverage-badge.svg) regenerated locally by a Husky pre-commit hook (.husky/pre-commit): it runs npm run coverage (src/**/*.test.ts only, so it never sweeps the Playwright specs under verify/), then npm run coverage:badge (scripts/updateCoverageBadge.mjs) to rewrite the SVG from coverage/coverage-summary.json, stages it, and rebuilds. We standardized on the pre-commit hook (matching seamless-auth-api) rather than a CI staleness check, so the committed badge always reflects the latest local run. If you change coverage, let the hook regenerate the badge; do not hand-edit the SVG.

The scaffold smoke job

.github/workflows/scaffold-smoke.yml runs scripts/scaffold-smoke.sh on every PR: it scaffolds with init --local --yes --auth=docker --admin=none, brings up db and auth from the generated compose, then writes a row, recreates the container, and reads it back.

It exists because every other job asserts the generated files as strings. This is the only one that hands them to Docker, so it is the only one that can catch a compose file that is well-formed and wrong. Bring-up alone is not enough: a volume mounted where the image does not store data leaves a database that starts, passes its healthcheck, serves queries, and quietly writes to the container layer. The read-back is what catches that.

If you change src/generators/docker/docker.ts, expect this job to be the one that fails. Run it locally with ./scripts/scaffold-smoke.sh; set SMOKE_EXTRA_COMPOSE to an override file if you already have something on 5432 or 5312, since the generated compose pins both the ports and the container names.

Before You Finish A Change

  • Run npm run build (the root package's only build step).
  • If you touched the harness: cd verify/harness && npx tsc --noEmit, then run seamless verify (--local to exercise local SDK source, or --api-only for a fast pass).
  • Add a changeset for any user-facing change.

Known Maintenance Traps

  • Sibling-repo branches: every sibling repo (api, server, react SDK, seamless-templates) is checked out at its default branch (main) when no explicit *-ref is passed to the verify CI workflow.
  • --local needs SDK dependencies: it builds the server (pnpm) and the React SDK (npm) from source on the host, so those repos must have their dependencies installed first. CI installs them explicitly.
  • OAuth mock networking: the in-process mock OIDC is reached by the browser and harness via localhost, but by the API container via host.docker.internal, so the provider config splits the authorize URL from the token / userinfo URLs.
  • Adapter OTP limiter: the adapter funnels all OTP through one client IP, so the API's per-IP OTP limiter (10 per 15 minutes, hardcoded) bounds adapter / react OTP traffic. Keep specs off it where possible (for example, magic-link login instead of a second email-OTP round trip).
  • Version pins: verify/adapter-app pins @seamless-auth/express, verify/adapter-fastify-app pins @seamless-auth/fastify, and each web template pins @seamless-auth/react. Bump these when new versions publish.
  • The four pins in src/core/images.ts are what a scaffold gets, and each drifts on its own: SEAMLESS_AUTH_API_VERSION (the auth server image), the admin dashboard image and ref, and SEAMLESS_TEMPLATES_REF. Check them against the sibling repos' latest tags before a release; nothing fails when they lag, the scaffold just quietly ships an older stack.
  • --auth=local is not pinned: it git clones seamless-auth-api at its default branch (src/generators/auth/auth.ts) while --auth=docker runs the pinned image, so the two auth modes can scaffold different servers from the same CLI version.
  • Config keys ahead of the API: WRITABLE_KEYS in src/core/systemConfig.ts mirrors the instance's strict patch schema. magic_link_redirect_uris is currently ahead of it (defined in @seamless-auth/types, not yet released there, and not yet read by the auth API), so an instance rejects that key today. Adding a key here before the API accepts it makes config set fail against every live instance.