Updating Projects
sf update is how a project you generated weeks or months ago stays in sync with SaaSFoundryAI as the platform evolves. It propagates new templates, scripts, and skill bundles — without overwriting the changes you have made to your code.
This page explains what sf update does, what it does not touch, and how to resolve conflicts when they occur.
What sf update actually does
sf update runs three independent flows in one command:
- Template update — detects that the CLI has a newer version than your project's manifest (
.saasfoundry.json) and propagates scaffold evolutions (e.g. a new skill file, an improved NestJS config, a security fix in a generated middleware). - Module addition — lets you add modules that weren't installed at generation time (email, storage, analytics, optional skills). This flow is independent of the version check and runs every time.
- Managed capability transition — adds the missing technical stack or collaboration harness so an eligible managed project reaches the
fullprofile.
All flows are driven by the manifest and the canonical capability classification, never by guessing from modules.harness alone. A multirepo project created by the verified saasfoundry-cli@1.0.0-beta release can first create that manifest through the explicit legacy-adoption flow below. The published beta disabled monorepo generation, so there is no authentic beta monorepo layout to adopt. Other projects without a manifest are refused.
Adopt a project created before manifest support
Legacy adoption is separate from updating templates or adding modules. It supports only the integrity-pinned multirepo output of the published saasfoundry-cli@1.0.0-beta package. It does not infer that an arbitrary NestJS/React repository came from SaaSFoundryAI, and it does not accept reconstructed or synthetic monorepo layouts. Start with a read-only, release-specific inspection:
sf update --adopt-legacy --dry-run --json \
--project-name my-app \
--main-branch mainThe report verifies the complete historical API/web file inventory against the integrity-pinned npm release, requires the critical signatures plus at least 90% of eligible template files to match, classifies the remaining customized files as user-owned, and returns a fingerprint. The fingerprint covers every inspected file, including user-owned files, so an edit between preview and apply invalidates the plan. Apply only that reviewed plan:
sf update --adopt-legacy \
--project-name my-app \
--main-branch main \
--adopt-plan <fingerprint>This command creates only .saasfoundry.json. It never replaces an existing manifest and it refuses missing historical paths, insufficient release matches, changed critical signatures, links, hard links, special files, case collisions, or a different plan fingerprint. Run a normal sf update --dry-run --json afterwards to review template changes and optional modules. Adoption flags cannot be mixed with module, workflow, technical-stack, SRS, or credential options; adopt first and update in a second command. The adopted manifest records a pending initial refresh, so this comparison still runs when the historical package version and the current CLI version happen to have the same text. That marker is cleared only after the refresh completes without unresolved conflicts.
Promote an existing managed project to full
Start with the canonical status report, then preview the transition:
sf status --json --no-network
sf update --target-profile full --dry-run --json- A
harnessproject plans a technical stack. Interactive mode collects the topology and technical choices, then explains that the resulting profile will befullbefore one confirmation. - A
stackproject adds the managed collaboration harness.sf update --add-modules harnessremains compatible and reaches the same result through the same capability decision. - A
fullproject is already complete, so the request is a no-op. - A multirepo child
projectiondelegates technical profile transitions to the root coordinator project. - An
unknownorinconsistentproject is left unchanged until the reported manifest remediation is applied.
The technical-stack plan is atomic. Any unmanaged file at a generated path, unsafe link or special file, case collision, or file/directory conflict blocks the entire transition. replace, force, and .saasfoundry.new sidecars cannot override this initial-adoption guard. A blocked JSON report says "mutated": false, lists the paths, and gives executable remediation.
Byte-identical files that already existed remain user-owned and are recorded in unmanagedPaths; later module additions and template refreshes do not absorb them into SaaSFoundry ownership. SaaSFoundry commands coordinate through one project lock. Avoid running another local tool that renames or replaces project paths while a profile transition is applying.
Managed project, external product, or POC?
- Managed harness project with no product stack at the candidate paths: preview
sf update --target-profile full. - External repository whose existing application remains the product: install or retain the
harnessprofile. The transition does not merge an arbitrary application, dependency tree, database, or runtime data into the generated stack. - Throwaway POC to rebuild: use the POC intake and approved preservation flow, move the experiment under
POC/, then create a cleanfullproject beside it. Never runsf new --profile fullinside the POC directory.
The V1 transition runs on macOS, Linux, and Windows through WSL. Native Windows execution is rejected before mutation.
The three-way merge
The template update is the non-trivial part. SaaSFoundryAI treats your project as a three-way merge:
| Input | What it is | Where it comes from |
|---|---|---|
| base | The hash of each file as originally generated by the old CLI version | .saasfoundry.json → fileHashes |
| current | The hash of the file right now in your project | Computed on the fly when sf update runs |
| target | The hash of the file as the new CLI would generate it for your manifest | Regenerated into a temp directory |
For each file, the comparison produces one of four actions:
| Condition | Action | What happens |
|---|---|---|
base == target | noop | Template hasn't changed. Nothing to do. |
base != target AND current == base | update | Template evolved, you never touched the file → auto-apply. |
base != target AND current != base, target | conflict | Template evolved AND you modified the file → conflict strategy. |
!base AND target AND !current | add | New file in the template, you don't have it → copy in. |
!base AND target AND current != target | conflict | A user-owned file already occupies the new template path. |
base AND !target AND current == base | remove | Template removed the file, you didn't touch it → flag only. |
Why this matters
The merge is conservative by design:
- Your edits are never overwritten silently. If the hash of a file no longer matches
base, it is treated as "user-modified" and will never be auto-updated. - New files never clobber your files. An
addaction only fires when the file is absent in your project. A same-name user file is reported as a conflict and follows the selected conflict strategy. - Removed files are flagged, never deleted. Even if the new CLI no longer generates a file you also didn't touch,
sf updatewill only warn you; removal is your call.
Conflict strategies
When a conflict is detected (both you and the template modified the same file), sf update follows the --conflict-strategy flag. There are three options:
| Strategy | Behavior | When to use |
|---|---|---|
save-new (default) | Writes the new template version to <file>.saasfoundry.new. Your file is untouched. | Safe default. You review the sidecar and merge manually. |
keep | Leaves your file as-is. No sidecar, no diff. | When you're confident your local edits are the source of truth. |
replace | Overwrites your file with the template version. Destructive. | Only when you deliberately want to reset a file to the template. |
replace is destructive
The replace strategy writes the template version directly over your file. There is no .bak and no undo — your changes are lost. Use it only in scripted contexts where you have just committed.
Dry-run before you apply
Use --dry-run --json to receive one versioned JSON object without touching any project or external resource:
sf update --dry-run --json --add-modules emailHuman-readable diagnostics go to stderr; stdout remains parseable. For example, a profile-transition report includes:
{
"version": 1,
"mutated": false,
"profileTransition": {
"targetProfile": "full",
"currentCapabilities": {
"technicalStack": "absent",
"collaborationHarness": "managed",
"effectiveProfile": "harness"
},
"status": "ready",
"plan": {
"version": 1,
"mutated": false,
"topology": "monorepo",
"canApply": true
}
}
}Pair this with sf update --dry-run --json > report.json in CI to surface upcoming changes before they reach the working branch.
Adding modules post-generation
The second flow is independent of the template update. Modules you skipped at sf new stay available forever — sf update detects them and offers to install:
# Interactive (menu driven)
sf update
# Scripted
sf update --non-interactive \
--add-modules email,storage \
--mailersend-api-key $MAILERSEND_KEY \
--s3-setup dockerThe --add-modules flag accepts a comma-separated list:
email— MailerSend transactional mailstorage— S3-compatible object storage (Docker MinIO or external credentials)analytics— Umami self-hosted analyticspwa— installable web app support, icons, manifest, and service workerharness— collaboration skills and process; scripted installs can select--workflow solo|saasfoundry|nonesrs— Software Requirements Specifications (Notion backend today ; Confluence + local-markdown on the roadmap)sf-skill-context7— Context7 library docs skillsf-skill-atlassian— Jira / Confluence skillsf-skill-notion— Notion skillsf-skill-figma— Figma skill
Stack modules are prepared in an isolated staging directory before any project file is changed. If keep or save-new leaves even one collision unresolved, SaaSFoundry keeps the module absent from the manifest and skips dependency installation. Resolve the listed paths (or intentionally rerun with --conflict-strategy replace), then rerun sf update; the already-applied non-conflicting files are detected as current target content and are not duplicated.
Each module has its own credential flags. See sf update for the full option table.
Enable SRS on an existing project
The SRS module ships a pluggable specifications system with a Notion backend (V1). Enabling it on a project that was generated without SRS uses the same module-addition flow as any other optional feature :
# Interactive (menu picks 'srs' from the module list)
sf update
# Scripted
sf update --non-interactive \
--add-modules srs \
--srs-backend notion \
--srs-parent-page-input "https://www.notion.so/your-workspace/SRS-root-abc123" \
--notion-api-token "secret_..."What the installer does :
- Installs the
sf-srsskill under.claude/skills/sf-srs/(templates, scripts, dispatcher) - Installs
sf-tool-notionif not already present (the SRS V1 backend) - Bootstraps the Epic root page on Notion via
adapter.init()— sharing the parent page with the Notion integration is a prerequisite - Writes
tools.srs.*into.saasfoundry.json(enabled, backend, rootPage)
Ingesting existing notes (one-shot)
If your team already has free-form spec notes on Notion that you want to bootstrap the SRS from, point the CLI at them :
sf update --non-interactive \
--add-modules srs \
--srs-backend notion \
--srs-parent-page-input "https://www.notion.so/your-workspace/SRS-root" \
--srs-ingest-enable \
--srs-ingest-parent-input "https://www.notion.so/your-workspace/Legacy-notes" \
--notion-api-token "secret_..."This sets tools.srs.pendingIngestion in the manifest. The flag is ephemeral — on the next supported coding-agent session, sf-srs drives a conversational loop to pick which legacy pages to draft into structured Epic / FR specs, then clears the flag after srs-cli.sh write succeeds.
See the SRS walkthrough for a complete end-to-end tutorial.
Typical upgrade recipe
A safe, reproducible recipe for an existing project:
# 1. Make sure your tree is clean and a backup branch exists
git status
git checkout -b backup/pre-sf-update
git checkout -
# 2. Upgrade the CLI
npm install -g saasfoundryai-cli@latest
# 3. Preview what would change
sf update --dry-run --json
# 4. Apply (save-new strategy so conflicts land in sidecar files)
sf update --accept-template-updates
# 5. Review sidecars
git status # .saasfoundry.new files should appear for conflicts
find . -name "*.saasfoundry.new"
# 6. Merge each sidecar by hand, then remove it
# (diff tool of choice against the original file)
rm **/*.saasfoundry.new
# 7. Re-run tests and commit
npm test
git add -A && git commit -m "chore: sf update $(sf --version)"The .saasfoundry.json manifest is rewritten at the end of a successful sf update, so the next run starts from a fresh base.
What sf update does NOT do
Be clear about the boundaries:
- It does not run
npm installorprisma generatefor you. Module addition flows may install dependencies; straight template updates do not. Run them yourself after reviewing the diff. - It does not migrate your database. Prisma schema changes in the template are propagated as files only. You run
npx prisma db push(schema sync) ornpm run db:setup:dev(full rebuild + re-seed, destructive) separately. - It does not touch your git history. No commits are created. The tree is left dirty for you to review and commit.
- It does not upgrade your installed npm packages.
package.jsonis three-way merged like any other file;package-lock.jsonis usually excluded. If the template bumps a dependency, you'll see it as anupdateorconflictonpackage.json.
Troubleshooting
"Your project was generated with SaaSFoundryAI v{X} (before hash tracking)"
Projects generated with early SaaSFoundryAI versions don't have fileHashes in their manifest. Template updates are skipped — only the module addition flow runs. To opt back in, regenerate fileHashes by running sf new into a temp directory with the same options, copying the fileHashes block over, and committing.
All my files show up as conflicts
You probably ran a global formatter (Prettier, ESLint --fix) after generation. Your current hashes no longer match base, so every file looks user-modified.
Workarounds:
- Run
sf update --conflict-strategy replaceon a clean branch if you're confident your local edits were formatting only. - Or: regenerate
fileHashesby re-runningsf newwith the same options in a temp dir and copying the hashes over.
The new CLI removed a file I didn't touch
sf update flags it but never deletes. Delete it manually:
git rm path/to/removed-file.tsWhere are the tests for this merge?
src/__tests__/integration/commands/update.spec.tssrc/__tests__/integration/commands/update.non-interactive.spec.tssrc/__tests__/e2e/update-command.spec.ts
Together they cover dry-run, conflict strategies, module addition, and the non-interactive path.
See also
sf updateCLI reference — full flag table and examples- Module System — what each module installs
- Project Structure — the directories
sf updatewrites into