Development
How to contribute to the SaaSFoundryAI CLI itself — set up the repo locally, run the test matrix, and stay in line with the conventions Husky enforces on every commit.
If you only want to use the CLI on a generated project, the page you want is Quick start. This page is for people changing the scaffold engine (the templates, installers, and migrations that generate user projects).
Prerequisites
- Node.js ≥ 22 — the repository's
.nvmrccurrently pins22.15.0;package.jsonenforces Node>=22.0.0.nvm usereads the file directly. - npm ≥ 10 — also enforced via
devEngines.packageManager. - Docker + docker-compose — required for the database in dev (
docker-compose.db.yml) and for the Docker-based E2E test matrix intests/docker/. - Docker network — create the shared external network once:
docker network create saasfoundry-network. Without it,docker compose upfails on the API service in any generated project. - GitHub CLI (
gh) — needed to drive the workflow CLI (workflow-cli.sh) andgithub-projects-cli.sh. Auth once withgh auth login.
Lifecycle lane resolution uses the repository's TypeScript runtime and has no separate jq dependency.
Repository layout
The high-level shape is documented in CLAUDE.md — the same file Claude Code reads at session start. Key directories:
| Path | Purpose |
|---|---|
src/commands/ | CLI entry points wired to Commander (new.ts, update.ts, workflow.ts, srs.ts, status.ts, …) |
src/builders/ | Scaffold builders — monorepo.builder.ts, multirepo.builder.ts, db.builder.ts, etc. |
src/installers/ | Reusable module installers (email, storage, analytics, srs) |
src/migrations/ | Manifest + module migration registries — see Migration framework |
scaffolds/blueprints/ | Base project templates (api/, web/, db/, s3/) |
scaffolds/overlays/ | Topology overrides (monorepo/, multirepo/) and module overlays (modules/email/, modules/storage/) |
scaffolds/skills-templates/ | Skill templates that ship into generated projects (sf-srs, sf-workflow, sf-integration-rules, …) |
tests/docker/ | Real lifecycle E2E lanes — browser → API → PostgreSQL for fresh generation and update |
.claude/skills/ | Local copies of the same skills the scaffolds ship — drift-guarded against scaffolds/skills-templates/ |
.claude/docs/ | Reference docs the agent reads during dev (architecture-modules, architecture-skills, migration-framework) |
For deeper guidance on how to add or modify a module or skill, read the architecture docs the agent uses:
.claude/docs/architecture-modules.md— how modules compose (blueprint markers + overlay files + installer).claude/docs/architecture-skills.md— skill template shape and the drift-guard contract.claude/docs/migration-framework.md— every breaking manifest or module change must ship through this
The immutable previous-release lifecycle fixture has a separate maintainer runbook in tests/docker/fixtures/previous-release/1.0.0-beta/README.md. Follow it when adding or refreshing historical update coverage.
Local setup
# 1. Clone
git clone https://github.com/DiamondForgeFr/SaasFoundryAI.git
cd SaaSFoundryAI
# 2. Install
nvm use # reads .nvmrc → Node 22.15.0
npm install
# 3. Build (one-shot)
npm run build
# 4. Watch the CLI while you work
npm run devTo run your local checkout against a real project:
npm link # publishes the in-repo `sf` binary on your PATH
cd /tmp
sf new --project-name local-test --structure monoreponpm unlink -g saasfoundryai-cli reverts to whatever you had installed globally before.
Build, test, format, lint
| Command | What it does | When to run |
|---|---|---|
npm run build | tsc — emits dist/ | Before publishing or running the CLI standalone |
npm run dev | tsc -w — incremental compile while editing | While developing |
npm run format | Writes Prettier formatting for sources and documentation | Before staging a formatting change |
npm run format:check | Checks Prettier formatting without modifying files | Read-only formatting validation |
npm run lint | ESLint with the flat config in eslint.config.mjs | Before pushing |
npm test | Jest across all four projects (unit, integration, e2e, smoke) | While iterating |
npm run test:unit | Just the unit project (fastest) | Quick local feedback |
npm run test:integration | Just integration tests (filesystem builders, scaffolds, installers) | When changing builders / installers |
npm run test:e2e | E2E tests (CLI command surface) | When changing command wiring |
npm run test:staged | Classifies the staged snapshot and runs only its configured validation lanes | What Husky runs before every commit |
npm run test:impact -- --base <ref> --head <ref> | Classifies a Git range and executes its configured validation lanes | Targeted local or CI-equivalent validation |
npm run test:pre-commit | Compatibility alias for test:staged | Existing local tooling |
npm run test:pre-push | Exact normal lifecycle lane: fresh monorepo full, fresh multirepo full, previous-release update smoke | Explicitly during AI Testing before Human Testing; record the results |
npm run test:full | Format check, lint, build, package check, all Jest projects, and the full Docker lifecycle matrix | Releases, classifier changes, and final deep validation |
npm run test:docker / npm run test:docker:full | Exhaustive four-check lane: fresh generation and update for both topologies | Release, scheduled, or deep local validation |
npm run test:docker:normal | The same three-check lane used by ordinary non-draft PRs | Required AI Testing validation |
npm run test:docker:list -- --lane normal | Lists stable check name, scenario, browser depth, and outer budget | Inspecting the CI contract |
npm run test:docker:scenario -- <name> --depth full | Builds the shared image once and runs one lifecycle | Targeted reproduction |
The impact classifier fails wide: lockfiles, root build configuration, workflow/classifier changes, unknown paths, and invalid ranges select full validation. See Impact-aware validation for the path matrix and dry-run commands.
Conventional commits + commitlint
Every commit message goes through @commitlint/cli with the rules in commitlint.config.js. The pattern is:
<type>(#<ticket>): <description><type>— one offeat,fix,docs,style,refactor,perf,test,chore,ci,build,revert(#<ticket>)— required scope. Every change is tied to a GitHub issue (the workflow CLI enforces this end-to-end). Use the delivery parent or child ticket number as appropriate.- Header length — capped at 100 characters. Keep the description short; put detail in the body.
Examples:
feat(#317): SRS intent-detector calibration — dataset + tuning + failure-mode doc
fix(#292): apply prettier normalization on color rejection case
docs(#388): enforce migration framework on breaking changes (CLAUDE.md)A chore: without a ticket is rejected. Use chore(#000): ... only for genuinely repository-wide housekeeping (CI tweaks, lock-file bumps) and document why in the body.
Husky hooks
Husky installs three hooks under .husky/:
| Hook | What it runs | How to bypass |
|---|---|---|
commit-msg | commitlint — rejects commits that don't match the convention above | Don't. Fix the message. |
pre-commit | npm run test:staged — read-only validation selected from staged paths | --no-verify on git commit. Reserve for emergencies. |
pre-push | RC version management and WIP checks; no automatic Docker run | Keep enabled; run heavy validation explicitly during AI Testing. |
The pre-commit hook never rewrites files. If format:check fails, run npm run format, inspect and stage the result, then create the commit again.
RC branches and tags, protected release targets, weekly schedules, and manual runs execute the exhaustive lane. Ordinary working-branch pushes and ready PRs classify their Git range and start only the selected lanes. Run and record the ticket's required lifecycle validation during AI Testing before opening the Human Testing draft PR.
Draft PRs provide the diff and manual test plan without running test/build CI. After human approval, push the required non-regression tests and use workflow-cli.sh ready-pr <ticket> to start full CI. Later ready-PR pushes rerun it; draft-pr <ticket> returns the PR to draft for further human testing and cancels obsolete CI.
Migration framework — non-negotiable
The migration framework (Epic #310) only delivers value if every breaking change runs through it. Inline shims in sf update, ad-hoc type mutations, and "the user can fix their manifest manually" shortcuts reintroduce exactly the cross-version drift the framework was built to prevent.
- Manifest shape changes — Any breaking change to
SaaSFoundryManifest(renaming a field, removing one, restructuring a sub-block) MUST ship as a numbered migration insrc/migrations/manifest/NNN-<name>.ts, registered insrc/migrations/manifest/index.ts, with a JSON-schema delta inschemas/saasfoundry-manifest.schema.jsonand a golden fixture pair undersrc/__tests__/unit/migrations/fixtures/NNN-<name>/. Never mutate manifests inline in commands; never bumpmanifestVersionwithout registering a migration. - Module file-set changes — Any breaking change to a module's installed file set (renaming a service file, splitting an installer's deposited files, requiring a new env var) MUST bump the installer's
currentVersionin<name>.installer.tsAND ship aModuleMigrationon itsmigrationsarray. UsewriteMigratedFilefromsrc/migrations/module/conflict.tsso user-edited files fall back to a.saasfoundry.newsidecar.
Read .claude/docs/migration-framework.md before editing src/types.ts, schemas/saasfoundry-manifest.schema.json, any installer's deposited templates, or anything under src/migrations/. It covers the registry pattern, the file-naming convention, the conflict-aware writer, and worked examples for both manifest renames and module file splits.
Workflow — never bypass statuses
We dogfood our own ticketing flow. Every code change moves through:
Backlog → Ready → In progress → AI testing → Human testing → In review → Done- Read
.claude/skills/sf-workflow/statuses/<N>-<name>.mdbefore any transition — the file lists the mandatory entry actions and exit conditions. - Use the workflow CLI:
.claude/skills/sf-workflow/workflow-cli.sh update-status <ticket> <status> --reason "...". Don't drag cards in the GitHub Projects UI manually — the CLI runs the guards (PR existence, complexity label, parent-status mirror, …) the UI doesn't know about. - Child tickets are native GitHub sub-issues, not checkboxes. Create them via
.claude/skills/sf-tool-github-projects/github-projects-cli.sh create-subtask. - Normal children own a branch and PR. A bundled child is one atomic commit on its delivery parent's branch, carries
nature:bundled-pr, and may go AI Testing → Done after validation — see.claude/skills/sf-workflow/SKILL.md"Nature axis".
Full guidance: Workflow skill SKILL.md.
Adding a module or a skill
These flows are documented in dedicated reference docs because they have their own constraints (drift-guard for skills, blueprint markers + overlay files + installer for modules):
- Adding a new module — read
.claude/docs/architecture-modules.md. Cover the markers, the overlay layout, the installer'scurrentVersion+migrations, the manifest delta, and the lifecycle/fast-test signal mapping you'll update undertests/docker/. - Adding or editing a skill — read
.claude/docs/architecture-skills.md. Cover thescaffolds/skills-templates/<name>/source tree, the drift-guard test that pins it byte-equal to.claude/skills/<name>/, and the SKILL.md / scripts split.
Releasing
Releases run from master. RC Git hooks validate state but never mutate commits or create tags:
- From
develop, branchrc-X.Y.Z(e.g.rc-2.0.0). - Before the first push, run
npm version X.Y.Z --no-git-tag-version, update the changelog, and commit both with the release ticket scope. - Push the RC branch and open its PR to
master. The branch version must matchX.Y.Z, and the full Docker matrix must pass. - Merge with a merge commit, update the local
master, verify its exact commit and package contents, then create and push the annotatedvX.Y.Ztag. - Wait for the tag CI, then dispatch Publish stable package from and for the exact
vX.Y.Ztag withgh workflow run publish-stable.yml --ref vX.Y.Z -f tag=vX.Y.Z. The protectednpm-productionenvironment must hold the one-use or granularNPM_TOKEN; the workflow verifies the event ref, tag and package identity before runningnpm publish --provenance --access public --tag latest. - Synchronize the release commit back to
developand verify a clean global install.
The npm-production environment should require a maintainer review. The first publish needs the token to claim the package; after the package exists, configure npm trusted publishing and remove the stored token. Never dispatch the workflow from a branch or before the tag's exhaustive CI is green.
The v1 release also has divergent legacy master history. Its RC branch must first record that history with git merge -s ours --no-ff origin/master; see release ticket #488 for the verified commands. Do not use -X ours, which can retain non-conflicting stale files.
For the v1.0 / v2.0 acceptance rubric, see .claude/docs/release-objectives.md.
Reporting bugs and proposing features
- Bug: open an issue with the
buglabel and a minimal reproduction. The maintainer will triage tocomplexity: bugand route through the workflow. - Feature: open an issue describing the user-visible outcome first, the implementation second. Big proposals usually become an Epic with child Stories.
- Security: do not file a public issue. Email the maintainer at the address in
package.json.
See also
CLAUDE.md— the agent's session prompt; mirrors most of this page in operational form- Workflow skill — full reference for the workflow CLI and status transitions
- Module system — user-facing view (what modules are and how to add them to a project)
- Architecture: modules — internal reference for adding new modules
- Architecture: skills — internal reference for adding new skills
- Migration framework — required reading before any breaking change
Synchronizing the review button with GitHub Projects
The Ready for review button triggers .github/workflows/pr-review-sync.yml. It checks the live PR and its linked ticket, then uses the guarded workflow CLI to move the ticket to In review. Configure the SF_PROJECTS_TOKEN Actions secret with repository access and write access to the organization Project. GitHub's default Actions token cannot access Projects; see GitHub's Projects automation documentation.
The listener must first be merged into the repository default branch. It never runs code from the PR head. It supports same-repository PRs, requires the native closing-issue link to agree with the configured branch ticket, and preserves all status guards. Missing credentials or linkage fails visibly; rerun after correcting configuration. Use the CLI manually for fork PRs or clicks made before installation. Existing custom listeners are preserved during skill installation.