Skip to content

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 .nvmrc currently pins 22.15.0; package.json enforces Node >=22.0.0. nvm use reads 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 in tests/docker/.
  • Docker network — create the shared external network once: docker network create saasfoundry-network. Without it, docker compose up fails on the API service in any generated project.
  • GitHub CLI (gh) — needed to drive the workflow CLI (workflow-cli.sh) and github-projects-cli.sh. Auth once with gh 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:

PathPurpose
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 ​

bash
# 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 dev

To run your local checkout against a real project:

bash
npm link                       # publishes the in-repo `sf` binary on your PATH
cd /tmp
sf new --project-name local-test --structure monorepo

npm unlink -g saasfoundryai-cli reverts to whatever you had installed globally before.

Build, test, format, lint ​

CommandWhat it doesWhen to run
npm run buildtsc — emits dist/Before publishing or running the CLI standalone
npm run devtsc -w — incremental compile while editingWhile developing
npm run formatWrites Prettier formatting for sources and documentationBefore staging a formatting change
npm run format:checkChecks Prettier formatting without modifying filesRead-only formatting validation
npm run lintESLint with the flat config in eslint.config.mjsBefore pushing
npm testJest across all four projects (unit, integration, e2e, smoke)While iterating
npm run test:unitJust the unit project (fastest)Quick local feedback
npm run test:integrationJust integration tests (filesystem builders, scaffolds, installers)When changing builders / installers
npm run test:e2eE2E tests (CLI command surface)When changing command wiring
npm run test:stagedClassifies the staged snapshot and runs only its configured validation lanesWhat Husky runs before every commit
npm run test:impact -- --base <ref> --head <ref>Classifies a Git range and executes its configured validation lanesTargeted local or CI-equivalent validation
npm run test:pre-commitCompatibility alias for test:stagedExisting local tooling
npm run test:pre-pushExact normal lifecycle lane: fresh monorepo full, fresh multirepo full, previous-release update smokeExplicitly during AI Testing before Human Testing; record the results
npm run test:fullFormat check, lint, build, package check, all Jest projects, and the full Docker lifecycle matrixReleases, classifier changes, and final deep validation
npm run test:docker / npm run test:docker:fullExhaustive four-check lane: fresh generation and update for both topologiesRelease, scheduled, or deep local validation
npm run test:docker:normalThe same three-check lane used by ordinary non-draft PRsRequired AI Testing validation
npm run test:docker:list -- --lane normalLists stable check name, scenario, browser depth, and outer budgetInspecting the CI contract
npm run test:docker:scenario -- <name> --depth fullBuilds the shared image once and runs one lifecycleTargeted 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 of feat, 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/:

HookWhat it runsHow to bypass
commit-msgcommitlint — rejects commits that don't match the convention aboveDon't. Fix the message.
pre-commitnpm run test:staged — read-only validation selected from staged paths--no-verify on git commit. Reserve for emergencies.
pre-pushRC version management and WIP checks; no automatic Docker runKeep 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 in src/migrations/manifest/NNN-<name>.ts, registered in src/migrations/manifest/index.ts, with a JSON-schema delta in schemas/saasfoundry-manifest.schema.json and a golden fixture pair under src/__tests__/unit/migrations/fixtures/NNN-<name>/. Never mutate manifests inline in commands; never bump manifestVersion without 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 currentVersion in <name>.installer.ts AND ship a ModuleMigration on its migrations array. Use writeMigratedFile from src/migrations/module/conflict.ts so user-edited files fall back to a .saasfoundry.new sidecar.

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>.md before 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's currentVersion + migrations, the manifest delta, and the lifecycle/fast-test signal mapping you'll update under tests/docker/.
  • Adding or editing a skill — read .claude/docs/architecture-skills.md. Cover the scaffolds/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:

  1. From develop, branch rc-X.Y.Z (e.g. rc-2.0.0).
  2. 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.
  3. Push the RC branch and open its PR to master. The branch version must match X.Y.Z, and the full Docker matrix must pass.
  4. Merge with a merge commit, update the local master, verify its exact commit and package contents, then create and push the annotated vX.Y.Z tag.
  5. Wait for the tag CI, then dispatch Publish stable package from and for the exact vX.Y.Z tag with gh workflow run publish-stable.yml --ref vX.Y.Z -f tag=vX.Y.Z. The protected npm-production environment must hold the one-use or granular NPM_TOKEN; the workflow verifies the event ref, tag and package identity before running npm publish --provenance --access public --tag latest.
  6. Synchronize the release commit back to develop and 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 bug label and a minimal reproduction. The maintainer will triage to complexity: bug and 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 ​

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.

Released under the MIT License.