FTS.Cli
0.11.0
dotnet tool install --global FTS.Cli --version 0.11.0
dotnet new tool-manifest
dotnet tool install --local FTS.Cli --version 0.11.0
#tool dotnet:?package=FTS.Cli&version=0.11.0
nuke :add-package FTS.Cli --version 0.11.0
FTS.Cli — fts
Six commands for a project that consumes the FTS platform packages. Six is the ceiling, and no
more without deleting one: a command that plain dotnet can already do would make this a wrapper, and
a wrapper is a thing to keep up to date rather than a thing that answers a question. Each command
below carries its own one-line answer to "why is this not just a dotnet command?", and "it is
shorter" does not count. fts ui is the newest (Phase 13 of docs/plans/fts-ui.md): a front end for
the other five, not a new kind of operation. fts update (Phase 12 of
docs/plans/platform-update.md) is the newest of the other five: the rest scaffold or diagnose a
project once; fts update is the one command that keeps bringing it forward.
dotnet tool install -g FTS.Cli
fts --help
Every other command quoted below is one scripts/verify-cli.ps1 actually runs, against a real feed or
a real folder feed. -g above is the exception: the verification installs to a --tool-path instead,
because a global install is machine state a proof should not leave behind.
The tool declares no package dependency and no project reference at all. That is not minimalism
for its own sake: a dependency on any FTS.Platform.* package would make installing it from a public
feed require the very private feed it exists to set up, and a third-party dependency would become the
supply-chain surface of a globally installed tool.
Why these six commands exist
PostgreSQL-only infrastructure
fts new api Ordering --infrastructure single-db selects PostgreSQL database, cache, audit,
feature flags, jobs and notification outbox. Conflicting explicit provider choices refuse
before generation; use the default --infrastructure custom for mixed providers. Security,
permissions, files and transaction effects stay independent. Best-effort effects remain the
default; atomic widgets use --effects atomic --notification-transport webhook with explicit
delivery configuration. External identity and notification services are not replaced by a database.
For only the cache choice, use --db postgres --cache postgres. Generated CACHE-POSTGRES.md
documents schema setup, bounded pool/quotas/cleanup, optional connection overrides and namespace
cutover. Local Compose initializes the cache schema on a fresh volume. Neither generation nor
the running application starts Docker. This candidate must pass installed-provider CI before release.
An earlier GitHub Packages observation illustrates why a successful index request is insufficient; it is not a universal credential diagnostic:
| Credential | index.json |
package download |
|---|---|---|
| none | 401 | — |
token without read:packages |
200 | 403 |
PAT with read:packages |
200 | 200 |
The middle row is the trap. A half-scoped token passes an index check and fails everything after it, and NuGet reports the authentication failure as a warning followed by a retry loop, so the error that finally stops the restore names neither the cause nor the fix. Worse, the warning text is byte-identical in the 401 and the 403 case, so the HTTP status is the only thing that tells them apart.
fts feed init
fts feed init # the org feed
fts feed init --feed <url|folder> # somewhere else, a local folder feed included
fts feed init --no-token # what a machine with no credential sees
fts feed init --probe-package <id> # probe with a different package
Writes this directory's nuget.config, then proves it works by attempting a real package restore —
never by fetching index.json. An index can be accessible while package downloads are denied.
HTTP status identifies the observed refusal, not its exact credential or policy cause:
| Result | Exit | What it says |
|---|---|---|
| authentication refused (401) | 3 | No usable credential was accepted; check absent, invalid or expired credentials. |
| access forbidden (403) | 4 | Check package visibility, organization policy and permissions; a GitHub PAT may need read:packages. |
| it restored | 0 | Names the package and the version that came down. |
why not just
dotnet?dotnet nuget add sourcewrites a source and cannot tell you that the token you just gave it will pass the index and fail every download.
No token is ever written to the file. The credentials block holds %GITHUB_ACTOR% and
%GITHUB_TOKEN%, exactly as the platform docs sample it, and the resolved token is handed to the
restore in its environment — so the probe tests the file as written, supplied the way the file says
it will be. The type that writes the file takes no token parameter at all, so this is a property of
the code's shape and not of a search over its output. A folder feed gets no credentials block, because
it needs none.
Running it twice duplicates nothing and clobbers nothing: an entry is found by key and updated in
place, another source and its credentials survive untouched, and a <clear /> is never inserted into
a file that did not have one.
The token comes from FTS_PACKAGES_TOKEN, then GITHUB_TOKEN, then gh auth token if the
GitHub CLI is installed. When it came from one of the first or last, the command prints the one line
that makes the file work on the next plain dotnet restore. The value is never printed — only its
origin and its length.
fts new api
--cache none|memory|redis|postgres selects an optional tenant-scoped read cache (default none).
Redis generation includes compose.redis.yml and REDIS.md; it never runs Docker. The deployment
owns Redis and supplies Cache__Redis__ConnectionString. No Redis packages or Docker are needed
for none or memory. See cache usage and validation.
With CLI 0.9.0/templates 0.13.0/platform 0.14.0, --permissions claims|sql selects signed application permissions or SQL role mapping (requires SQLite or PostgreSQL, plus JWT). Modules use the same permission declarations in both modes.
The source candidate supports --db postgres; this is not a claim that these packages are published.
It emits POSTGRES.md, POSTGRES-TESTS.md and optional compose.postgres.yml. Generation never
starts Docker. Supply ConnectionStrings__Postgres and the separate JWT settings before running.
For transactional widget effects, use matching database, audit and jobs providers:
fts new api Ordering --db postgres --audit postgres --jobs postgres --effects atomic
fts add module inherits PostgreSQL and its effects setting. PostgreSQL module names have a
46-character ASCII budget and case-folding collision checks. SQLite remains the default.
--identity-provider generic|keycloak|supabase|clerk selects an identity recipe;
--auth-infra auto|external|local selects infrastructure output. The default auto resolves to
local for Keycloak/Supabase and external for generic/Clerk; the provider still defaults to generic.
Keycloak/local generates development Compose, a realm/client import and a keycloak launch profile.
Supabase supports auto or local: it generates an auth-only Compose stack, secure
bootstrap and a Development-only supabase launch profile. The generated AUTH.md explains login
and secrets. No service is started automatically. --identity-provider supabase --auth-infra external
generates a strict hosted password-session profile, SQL hook and SUPABASE.md, with no identity Docker.
Install/enable the hook and configure issuer/JWKS before starting the supabase-hosted launch profile.
This requires claims tenancy; email links, social login and recovery sessions do not grant application access.
Clerk supports auto or external, requires JWT and claim-based organization tenancy, and emits
CLERK.md, src/<name>/appsettings.clerk.json and a clerk launch profile. Configure the public
issuer/JWKS URI, audience and exact browser origin, then run with --launch-profile clerk.
No Clerk Docker, backend secret or frontend is generated. Published hosts set Auth__UseClerk=true.
Initialize trusted membership permissions to []; unset metadata emits null and is rejected.
Example: fts new api MyApi --identity-provider clerk --auth-infra auto --feed <candidate-folder> --no-token.
This working-tree candidate is not a production certification or a published release.
Keycloak without --auth-infra now emits local development infrastructure; pass external to
retain its previous behavior. Update CLI/templates together: the CLI explicitly forwards auto
and checks resolved metadata, so older templates cannot silently select a different default.
fts new api Ordering # stage, restore, then promote
fts new api Ordering -o services/ordering # somewhere else
fts new api Ordering --db sqlite --auth jwt # the ADR 0007 seams, passed straight through
fts new api Ordering --profile protected --auth jwt --tenancy claims
fts new api Ordering --feed <url|folder> # verify a different feed
fts new api Ordering --skip-feed-check # generation only; no restore proof
The default is protected JWT endpoints, claims tenancy and SQLite widget persistence.
Configure the emitted Auth:* settings before starting; no usable signing secret is generated.
Anonymous process-local demos require --profile demo --auth none --tenancy fixed --db none.
Each seam remains independent: --profile demo alone does not disable JWT or SQLite.
Stages the actual solution, restores it, then promotes it to the destination. CLI 0.7.0 requires
a versioned template compatibility contract (templates 0.13.0 support platform >=0.14.0 and <0.15.0).
Every generated direct package reference becomes an exact singleton range. Preflight restores the
host and generated tests with the selected sources, an isolated package cache and a five-minute
process bound. A failed restore returns 3 / 4 / 5; incompatible templates/pins return 6.
The destination remains absent on these failures; temporary staging files are cleaned up. Successful
restore is not a build/test or deployment certification. Run the generated scripts/verify.ps1 next.
Existing destinations, linked ancestors and non-identifier names are refused. Names must be ASCII
C# identifiers without keywords, dots or hyphens; output directory paths can contain spaces.
Template options — --auth, --db, --tenancy, --audit, --events, --features,
--files, --jobs, --notifications, --profile, --effects, --platform-version, --port — are
forwarded as separate process arguments. The CLI also evaluates the resulting platform pin
and checks template compatibility. The fts-api template already
refuses the two illegal combinations by name, at scaffold time, with exit 102, and that exit code
and that guard name reach you unchanged; the same division of labour covers --port, which the
template declares integer and refuses non-numerically itself. What this command does check is the
option name: an unrecognised token is a usage error rather than a puzzling dotnet new message.
--profile selects endpoint protection. --platform-version pins the emitted package references and
--port sets the dev-run port in the emitted launchSettings.json (default 5080, chosen so a
scaffold does not land on ASP.NET Core's already-crowded 5000).
--events is the newest of the five and the reason the platform went to 0.4.0: until then
InProcessEventPublisher lived in FTS.Platform.Sqlite, so --db decided the event seam and no
scaffold could have events without a database.
--feed selects both the preflight source and the emitted nuget.config source. Relative folders
are made absolute. URLs containing credentials, query parameters or fragments are refused. No
credential block is emitted. For authenticated subsequent restores, set
NuGetPackageSourceCredentials_fts=Username=<actor>;Password=<token>;ValidAuthenticationTypes=Basic
in the process environment (the generated CI workflow supplies this). Preflight resolves tokens
only for HTTPS unless --no-token is supplied. --skip-feed-check still validates template
compatibility and pins dependencies, but explicitly reports dependencies as unverified.
why not just
dotnet?dotnet new fts-apihappily writes a project against a feed nobody has configured, and the restore that then fails names neither the cause nor the fix.
fts add module
fts add module Billing # run from the solution directory
fts add module Billing --module-id billing-core # a multi-word IModule.Id
fts add module Inventory --contracts # separate public exchange assembly
fts add module Billing --platform-version 0.10.0 # must match the evaluated host pin
--contracts adds Contracts/<Name>.Contracts.csproj, stable exchange models, a query interface
and an Application adapter. The CLI pins and registers all three projects; the host references only
the implementation. Other modules may reference Contracts, never the implementation. The query
interface is a trusted in-process boundary, not an authorization endpoint: callers must supply an
authorized tenant. Existing modules are not silently migrated by adding this flag. Prepared recovery
retains the Contracts files and solution entry; registered checks verify its evaluated module reference.
The host's evaluated direct platform references determine the module pin, including imported,
conditional and central package versions and version overrides. The evaluated FtsTemplateProfile
also determines endpoint protection; missing/unknown profiles and existing module/profile mismatches
are refused. Merely editing the profile property does not migrate existing endpoint policies.
The evaluated FtsTemplateDatabase likewise determines none versus sqlite store generation.
Missing/unknown database metadata or a registered module with a different choice is refused;
changing metadata is not a store/schema migration.
--effects atomic --db sqlite --jobs sqlite --audit sqlite commits generated widget state, required
audit and event intent together. Keep --events none --notifications none; required events use
versioned per-module worker routes and idempotent local receipts, not the optional publisher.
add module inherits evaluated FtsTemplateEffects and refuses missing/unknown choices or mismatches.
Older hosts must explicitly review/adopt best-effort or implement the atomic wiring before setting
this property. Neither changing metadata nor selecting atomicity supplies authentication or external
delivery. The default remains best-effort; combine atomic with the protected profile for JWT endpoints.
The packed foundation harness verifies this option against generated API and custom-ID modules.
Mixed, floating, missing and
multi-target host pins are refused. Generated projects opt out of central package management to
retain their explicit pins. Existing transitive module pins are not comprehensively audited here.
Template output is staged and compatibility-checked before edits. Program.cs is replaced via an
adjacent temporary file with an original-content check. This is not a multi-file transaction:
later failures can leave completed solution/project edits. Before promotion, an exclusive
.fts-add-module.pending marker and byte-exact solution/host/Program.cs snapshots are created.
An exclusive OS file lease on the shared .fts-edit.lock file prevents concurrent writers/resume on supported Windows/Linux local filesystems -- the same lease fts add provider contends for, via its own .fts-add-provider.pending marker, so a concurrent module and provider edit against one consumer cannot both proceed. This empty lock file persists after success or interruption; never delete or replace it while an operation may be alive. Process termination releases the lease automatically; the file's presence alone does not mean an operation is running. Generated APIs ignore lock/recovery files in Git; existing consumers should add equivalent ignore entries.
The marker records flushed before/after checkpoints; .fts-recovery-<id>/manifest.json identifies
original paths, backup files, hashes and the intended module destination. Success removes the marker
and owned snapshots. Failure preserves them and blocks subsequent additions before project evaluation.
fts add provider uses its own .fts-add-provider.pending marker and .fts-provider-recovery-<id>/
directory, with the same checkpoint/manifest shape adapted to a variable-length file set.
Interrupted module edits
New .slnx additions prepare all replacement bytes and the generated module before changing the
consumer. After stopping the interrupted operation and its children, run
fts add module Billing --resume. Resume validates snapshot hashes, module files and every current
target against its original or prepared bytes before proceeding. It finishes forward without duplicate
entries; it never guesses how to merge later edits. Module changes (including new build output),
damaged snapshots or mismatched targets require manual reconciliation. A regular rerun still refuses
while the marker exists. This is per-file atomic replacement, not a multi-file transaction.
Legacy .sln operations and interruptions before a complete plan exists retain the manual procedure;
there is no automatic rollback. For conflicts or unsupported plans:
- Stop any active CLI operation and its child processes before inspecting or changing recovery files.
- Read
.fts-add-module.pendingand its recovery directory'smanifest.json. Abeforecheckpoint withoutafteris ambiguous: the child may have applied some or all of that edit. - Compare current solution, host project and
Program.cswith the snapshots. Preserve any later user edits; do not blindly overwrite them. Reconcile both module/test solution entries, the host reference, registration and generated module directory together. Backups reflect input bytes, not necessarily a valid project. An empty/truncated marker still requires review; inspect.fts-recovery-*directories. - Either finish those edits by hand, or restore only the reviewed CLI changes and move the generated
module out of
src/Modulesto a separate archive. Preserve any user changes inside it. - Only after reconciliation, archive/remove the pending marker. For the restored-to-before option,
rerun
fts add module; then run the generatedscripts/verify.ps1. For.slnx, analready registeredresponse checks both solution entries, the test-project file and the evaluated host reference, but does not establish a successful build or runtime verification. Legacy.slnintegrity requires manual review.
Recovery files inherit local directory permissions and can contain source/project secrets: do not
commit or publish them. Interrupted staging before journal acquisition does not edit the host but may
leave .fts-stage-* files. Snapshots cover the three intended input files, not arbitrary MSBuild or
template side effects. Flushes are not a filesystem-wide power-loss guarantee. This is a trusted,
single-worktree safeguard, not protection against malicious concurrent filesystem changes.
Four edits, and the last one is the reason this command exists:
dotnet new fts-module -n Billing -o src/Modules/Billingdotnet sln add src/Modules/Billing/Billing.csprojand itsTests/Billing.Tests.csprojdotnet add src/<host>/<host>.csproj reference src/Modules/Billing/Billing.csprojnew Billing.BillingModule()into the host's existingAddModules(...)call
The diff in Program.cs is exactly the inserted argument. The file is read as bytes, the analysis
runs over the decoded text, and the bytes written back are the original bytes with the encoded
insertion spliced in at one point — so line endings, a BOM and trailing whitespace survive by
construction rather than by careful re-encoding. Nothing is reflowed and no other line moves. The
argument is namespace-qualified and no using is added, so there is one insertion point instead of
two, and the emitted template comment documents the same edit.
It refuses rather than guesses, with exit 6. Discovery/registration prechecks are read-only;
those refusals leave existing project files untouched and print the five edits to make by hand.
Failures after promotion report completed edits rather than claiming rollback. It refuses when the working directory does not hold exactly one
solution and one host project, and when Program.cs has no AddModules( call, more than one, an
unclosed one, or an empty argument list. Calls inside string literals and comments are not counted —
the source is classified as code or not-code first, which is the thing a regex cannot do.
Running it twice changes nothing: existing registrations are checked against module metadata,
both .slnx entries and the evaluated host reference before reporting success. No second template
generation or registration edit occurs. Missing/duplicated entries are refused instead of hidden by
the existing AddModules call.
why not just
dotnet?dotnet new fts-modulewrites the files and cannot touch a host it did not create — an item template that reached out of its output directory is one you could not run twice or read the diff of. The registration is the edit no template may make.
fts add provider
fts add provider <capability> <providerId> [--replace] [--files-migrated] [--dry-run] [--platform-version <v>]
fts add provider <capability> --resume
fts add provider <capability> custom --name <Name> [--dry-run]
fts add provider --list
fts add provider <capability> --list
fts add provider # interactive, terminal only
fts add provider --completion <powershell|bash|zsh>
Run from the generated project's directory. Every choice comes from the one catalogue embedded in the
tool (provider-catalog.json), the same one fts new api validates its flags against. The catalogue
classifies every ordered (capability, from, to) pair as safe-later, migration-required or
unsupported, each with a reason; discovery, completion and refusals all read that classification —
see docs/tasks/provider-transition-matrix-38c09ba8/ for the full matrix.
Changing a provider. <capability> <providerId> edits the host's existing Program.cs/csproj
registration. The set of implemented transitions is computed from the catalogue and printed by both
--help and any refusal; today it includes the original cache: none -> redis and
files: none -> custom, nine more safe-later additions from none (cache: none -> memory/postgres,
files: none -> filesystem/s3/azure-blob, observability: none -> otlp,
notifications: none -> console, features: none -> configuration, events: none -> inproc), six cache
replacements among memory/redis/postgres behind --replace, and six files replacements among
filesystem/s3/azure-blob behind --replace --files-migrated. Every other combination is refused,
classification-aware: a migration-required pair (e.g. db: sqlite -> postgres,
identity-provider: generic -> keycloak, auth: none -> jwt) names the runbook that documents its
preconditions and manual data-migration procedure; an unsupported pair names its reason directly.
--replace never bypasses a migration-required or unsupported transition. Changing an existing non-none
selection requires --replace. Repeating the current selection is a no-op. --dry-run prints the exact
files, packages, caveats and warnings and writes, restores and scaffolds nothing. A real edit validates a
staged copy (build plus the recipe's targeted test) before anything is written, and uses the same
project-wide .fts-edit.lock lease, .fts-add-provider.pending marker and hash-gated snapshots as
fts add module; after an interruption, fts add provider <capability> --resume finishes only when
every file matches its original or prepared bytes. --platform-version, when given, must equal the
host's evaluated pin.
Cache replacements. fts add provider cache <memory|redis|postgres> --replace swaps the cache
backend among the three non-none providers in either direction. It is a registration-only edit — no
warmed value is carried over, and both dry-run output and the real edit print caveats: warmed values in
the old cache are not migrated (the new cache starts cold), and when redis is on either side, that its
shared-lock/idempotency guarantee may change. The .fts-add-provider.pending journal's "prepared"
checkpoint records the same caveats in its Detail.
Files replacements. fts add provider files <filesystem|s3|azure-blob> --replace --files-migrated
swaps the file store among the three non-none/non-custom providers. This is registration-only too —
the CLI never reads, copies or deletes a single file byte — so it refuses without the explicit
--files-migrated flag, naming docs/operations/files-storage-migration.md's Inventory/Copy
strategy/Integrity verification steps as the precondition the flag confirms you already completed for
every tenant. --files-migrated is valid only with this exact form; any other capability or a files
command without --replace refuses it as a usage error.
An unknown provider id is refused with a suggestion (fts add provider cache redsi answers
"Did you mean 'redis'?"). It is never silently scaffolded as custom.
Custom providers. fts add provider files custom --name AcmeStorage (from files: none) adds an
application-owned AcmeStorageFileStore stub wired as the scoped IFileStore, sets
FtsTemplateFiles to custom, and adds CustomFileStoreContractTests, which runs the shared
file-store conformance contract against the stub. No vendor SDK or other package is added, and no
configuration placeholder is needed because the stub has none. Both stub members throw
NotImplementedException, so the first real request fails immediately and the contract test stays
red until the store is implemented; the staged validation confirms that red failure and refuses a
scaffold whose test would pass. --name must be a non-keyword ASCII identifier (no paths, spaces or
shell characters); it reaches dotnet new as a single process argument, never through a shell.
Every custom-capable capability (cache, files, jobs, events, features, audit,
notifications, auth, permissions, tenancy) supports <capability> custom --name <Name>.
--platform-version and --resume are refused on this form (the custom scaffold always uses the
host's evaluated pin, and resume uses <capability> --resume); renaming an existing custom provider is
not supported. --replace and --migrated/--files-migrated ARE now accepted here (Phase 11): custom
scaffolding reaches most capabilities from more than one current provider (e.g. cache redis custom --replace), and a migration-required source (e.g. audit sqlite -> custom) additionally requires
--migrated, naming its runbook's ## Custom section — see the per-capability sibling runbooks under
docs/operations/. Reversing direction (e.g. fts add provider cache postgres --replace from a current
custom selection) routes through the ordinary add provider form above, not this one, and leaves the
custom stub/contract test in place, unused. Providers whose generated registration is a block shaped by
other selections (jobs: sqlite/inline, notifications: sqlite/postgres, tenancy: claims) are
matched against the template itself: the tool renders dotnet new fts-api for the project's own
selections in a throwaway directory outside the project (once for each provider), and refuses with
expected/found, writing nothing, unless every differing region is in Program.cs verbatim; it also
refuses while another selection still requires the provider being left (e.g. jobs: sqlite -> custom
under notifications: sqlite). The one pair left unsupported is auth: jwt <-> custom, for safety
rather than matching (removing token authentication would silently open module endpoints that rely on
its fallback policy; adding it needs identity-provider configuration); --list and the catalogue's
reason: say so. Resume an interrupted custom scaffold with
fts add provider <capability> --resume. fts new api --files custom produces the same stub at
generation time, named after the project. fts doctor --checks composition lists each selected custom
provider as an advisory; it cannot prove the implementation.
Discovery. --list prints every capability with the current selection (when run inside a
generated project) and its choices; <capability> --list adds each provider's description and, for a
provider the current selection cannot reach, marks (not yet supported for later addition) alongside the
pairwise catalogue's reason: for why and, when the pair is migration-required, the runbook: path
that documents its preconditions — so --list never claims a path add provider would refuse without
also saying why. Both are read-only.
Interactive. Bare fts add provider prompts for capability, provider, (for custom) a name and
dry-run versus apply, then runs the same command you could have typed. It only prompts when both
standard input and output are a terminal; in CI, pipelines or redirected sessions it refuses and asks
for explicit arguments.
Completion. fts add provider --completion powershell (or bash, zsh) prints a completion
script to standard output; it writes no file and edits no profile. Add it yourself, for example
fts add provider --completion powershell | Out-String | Invoke-Expression in your PowerShell
profile, source <(fts add provider --completion bash) in .bashrc, or the same with zsh after
compinit. Completion covers only fts add provider's grammar (capabilities, provider ids,
custom, and its options), not feed, new api, add module or doctor. The scripts call
fts add provider --complete -- <words>: what the generated scripts call, not a command to run by hand.
No package, module or service is involved.
fts update
fts update --check # report only; writes nothing; exits non-zero when anything is behind
fts update # both halves: platform pins, then template recipes, then one build/test
fts update platform [--to <v>] # just the pin rewrite
fts update template # just the template recipes
fts update --dry-run # prints the files it would edit/create; writes nothing
fts update --resume # completes an interrupted apply from its saved plan.json
fts update --accept-test-failures
A project generated by fts new api (or carrying fts add module shells) depends on the platform two
different ways: it references the FTS.Platform.* packages at an exact pin, and it owns a copy
of everything the templates produced (Program.cs, compose files, appsettings*.json, docs, module
shells). fts update brings both forward to what the installed CLI/templates would generate today, in
one command.
The platform half rewrites every FTS.Platform.* PackageReference in every *.csproj under the
project to one consistent target version — by default, the version the installed templates pin; --to
names another version, but only inside the project's recorded fts-template.json compatibility range.
It never touches a non-FTS.Platform.* reference, in any form (floating, ranged, exact): a 8.*
reference beside the platform ones is never a reason to refuse or crash.
The template half applies a small registry of recipes, keyed by template-version range, built on the
same engine fts add provider uses: each recipe knows the files it owns and the project selections it
applies to, and compares what the project actually has against the shape the old and new templates
produce. A file the team customized is refused by name, with expected/found, exactly like a
provider transition refusal — the other recipes still apply, and nothing is ever merged (the one
exception is appsettings*.json, which only gains missing keys and never changes a value you already
have). Module shells from fts add module are covered the same way, per module.
Before applying anything, one restore, one staged build and the project's own tests run in a private
workspace; a failure refuses the whole update with nothing written, unless --accept-test-failures
records the failing names in the journal and proceeds. fts-template.json is rewritten last, since it
is the record that the rest of the update already landed. Interrupted updates keep their prepared
snapshots under a .fts-update.pending marker (which add module/add provider also refuse to start
over) for fts update --resume; a repeat run with nothing left to do prints up to date per half.
fts update never runs a database migration itself: it lists the platform migrations the new packages
embed that the old ones did not, and prints the same backup reminder docs/platform-operations.md
gives — "Stop old writers and rehearse a backup restore." — so you can act on it before anything changes.
fts update never updates the fts tool or the templates themselves (the same rule that keeps every
other command from touching global .NET state); --check/fts doctor --checks update print the exact
dotnet tool update/dotnet new install commands when those are behind the feed.
why not just dotnet: nothing in the SDK rewrites an existing project's pins and template-owned files to match a newer template/platform release without regenerating (and losing) the project.
fts doctor
fts doctor # this directory
fts doctor <path> # somewhere else
fts doctor --source <name> # a source key other than `fts`
fts doctor --no-token # what a machine with no credential sees
Evaluated and strict checks
fts doctor --evaluated --no-feed evaluates trusted single-target C# projects in Release, then
freshly restores each project and reads its actual resolved direct/transitive package versions.
Imports, conditions and central versions participate. Failed evaluation/restore never falls back
to stale assets as successful evidence. --configuration Debug selects the other supported configuration.
This opt-in executes MSBuild and may update assets, caches and lock files; use only in a trusted tree.
fts doctor --strict --no-feed implies evaluated mode and returns 7 unless both evaluation
and audit pass. --checks evaluation,audit,feed adds latest-version availability; subsets are allowed.
--checks update adds fts update's own report (current → target per half, applicable recipes, behind
or current) without running it — the read-only half of fts update --check, folded into one doctor
run. Missing, duplicate or unknown selected evidence cannot pass. Without --strict, doctor remains
report-only and returns 0 when it produces a report, even if checks fail.
The resolved audit uses NuGet's affected-version data for all package dependencies, forces auditing
on, clears warning suppression and promotes NU1900–NU1905 to errors. Explicit NuGetAuditSuppress
entries require review and cannot satisfy a strict audit. A failed/unavailable audit is not a clean
result. Source-only mode retains the dated offline advisory hints, not a resolved safety claim.
This checks configured advisory sources, not every possible vulnerability or deployed native binary.
--no-feed skips only latest-version queries; evaluated restores/audits can still access the network.
--no-token clears the selected source's supplied credential variable as well as the tool's token
variables; independently configured credential providers/sources remain the consumer's responsibility.
Architecture source mentions remain distinct from current test execution: run the generated
scripts/verify.ps1 to execute its required architecture/runtime census. Doctor does not certify that
tests ran, nor does it accept an old TRX as current proof. Multi-target doctor evidence is explicitly unsupported.
Answers what am I on, what is current, what applies to me: the platform versions the whole tree
references — every *.csproj plus Directory.Packages.props, so a test project's reference counts
too — the latest each has on the feed, whether they differ, and potential advisories based on declared
platform versions. XML scanning is unevaluated: imports, conditions and transitive overrides may
change the actual graph. This is not a substitute for restore plus a resolved dependency audit.
The historical SQLite advisory remains a candidate for platform versions before 0.10.0 or unknown pins, but not known fixed platform versions. The fix is native bundle 2.1.13; generated SQLite tests also check the loaded native library. Unknown/mixed pins do not silently hide an affected version.
It degrades honestly. With no feed access it reports the local half in full and says plainly what is missing and why — the 403 and the 401 get different sentences — and it never says "up to date" on no evidence. A version whose latest could not be fetched carries no marker at all.
why not just
dotnet?dotnet list package --vulnerable --include-transitiveneeds a successful restore — the thing that is broken when the feed is not set up — and it names the transitive package rather than the platform package you chose, so it cannot tell you which composition avoids the problem.
Latest-version selection is a real NuGet version comparison, not a position in the response and not a
string maximum: document order belongs to the source. Measured, a folder feed returns 0.1.0,
0.10.0, 0.9.0 for those three packages while nuget.org returns version order.
fts ui
fts ui # this directory; a free port, http://localhost:5080 for the API
fts ui --port 5173 # a fixed port
fts ui --api http://localhost:5080 # a different running consumer API
fts ui --no-browser # print the URL instead of opening it
fts ui --run-api # always start the API itself, even if one already answers
fts ui --no-run-api # never start it, even if none answers
A loopback-only, token-guarded local web page over the other five commands (Phase 13 of
docs/plans/fts-ui.md): it is a front end for the other five, not a new kind of operation. It refuses,
by the same ProjectAdoption wording doctor prints, when run outside a supported generated project.
It binds 127.0.0.1 only, never a routable address, and mints a random token for the session; the
printed URL (and every subsequent JSON/stream/proxy request the page's own JS makes) carries it.
Nothing the page holds — the token, the directory, anything it reads or shows — is ever written to
disk by this process.
Six screens, laid out in a sidebar/header app shell with a CSS-custom-property design system
(Phase 14 of docs/plans/fts-ui-2.md) — light and dark themes, following the OS preference until the
header's toggle picks one explicitly (persisted to localStorage), and shared loading/empty/error
states so every screen's async read looks and behaves the same way. Each screen is a structured read of
what the terminal already knows: Overview (project name, platform pin, template version, update
status), Providers (a searchable, grouped dashboard over every capability's current and reachable
providers — see below), Modules (source
modules cross-checked against the running API's own registration), Endpoints (the running API's live
route table), Health (fts doctor's checks grouped by name, the same update status panel as Overview,
and the same pending-edits panel as Activity, all in one place), and Activity (pending journal markers,
--resume, one edit at a time). Every action card shows the literal fts ... command line it runs —
never a hidden second code path: a mutation always goes through the same parser, the same EditLock
lease and the same staged build/test validation the terminal command does, dry run first, then an
explicit apply.
The server caches /api/inventory, /api/providers and /api/update per project, keyed on a content
hash of the host .csproj, fts-template.json and the host's Program.cs: revisiting a screen (or
switching between screens that share one of these reads) answers in well under 100 ms instead of
re-running the underlying build-dependent report, and every successful apply invalidates the cache
before the page can see it, so a mutation is never followed by a stale read.
Providers is a searchable, grouped dashboard (Phase 14 slice 14d): one card per capability, grouped
into Data/Messaging/Security/Operations, each showing the current provider and a badge counting its
reachable alternatives; a search box filters cards and providers by capability, provider id or summary
text. Clicking a card opens a side drawer with the same reachability/reason/runbook detail
fts add provider --list renders, plus two catalogue fields new to this phase — each provider's
configuration keys (what it reads from appsettings/environment) and its Compose service name, or
none when it needs neither — and the dry-run-then-apply action flow, relocated here unchanged in
substance (same ActionCard, same /api/actions/* calls). A new architecture rule
(ProviderConfigurationComposeDriftRule) cross-checks both fields against the real platform/template
source so they cannot silently rot. When a capability's current provider is custom, the drawer also
shows a workspace: the stub's file path and class name, its shared contract test's case names (from the
catalogue's own requiredTests), and a "Run contract test" button that runs
dotnet test <project> --filter <the stub's contract test class> through
POST /api/actions/contract-test/run — one action kind with no dry-run/apply split and no edit-lock
contention, since running a test mutates nothing, so it can run beside an in-flight provider/module/update
apply — and renders each case's pass/fail (red for an unimplemented stub, as expected) once the run's SSE
stream finishes.
A proxy under /.proxy/* forwards arbitrary requests to the running consumer API unchanged — headers,
body, status code, nothing added, removed or logged — so the page (and scripts/verify-fts-ui.ps1) can
reach the API's own /health and the two Development-only introspection routes
(FTS.Platform.Hosting's /.fts/modules//.fts/endpoints) through one loopback origin.
The Endpoints explorer is schema-aware when the consumer's own /openapi/v1.json answers through
the proxy (Phase 14 slice 14c): a fresh fts-api scaffold carries a Development-only
AddOpenApi()/MapOpenApi("/openapi/v1.json") pair from templates 0.15.0 onward, and an existing
project picks it up through fts update template's openapi-addition recipe (a narrow, idempotent text
insertion into Program.cs, exactly like the observability-none-addition recipe it sits beside — see
CHANGELOG.md's Templates section). When the document is reachable, selecting a listed operation renders
required-parameter markers, a body pre-filled from the operation's schema or example, and its response
schema read-only; when it is not (404, a proxy 502, or JSON that is not an OpenAPI document), the screen
falls back to the plain /.fts/endpoints-driven manual composer unchanged. Request history (last 50),
saved requests and header presets (masked on screen, plaintext in storage) live in the browser's own
localStorage, scoped per project so switching projects never mixes one project's scratch data into
another's — the server never stores or logs them, including through the proxy. A "Copy as curl" button
turns the composed request into an equivalent curl command line.
API process management (Phase 14 slice 14b): by default fts ui probes --api once and starts the
consumer API itself (dotnet run --project <host> --no-launch-profile) only when nothing already answers
there; --run-api always starts it, --no-run-api never does. The header's API status chip polls
/api/app/status and shows Start/Stop/Restart — Stop and Restart are refused (and the buttons disabled)
for a process this fts ui did not start, exactly as the terminal only ever manages what it launched.
After a successful provider/module/update apply, the action card offers a "Restart now" button and an
opt-in "restart API after apply" checkbox that calls restart automatically once the apply finishes; either
way it is a real restart of the process fts ui owns, not a hint to run a command yourself. A process this
fts ui did not start (or one it started but a later rebuild outran) is reported as RestartNeeded rather
than stopped out from under you. It still never runs dotnet new, never acquires a bearer token on your
behalf, and never serves more than one project at a time — see Decision 9 of docs/plans/fts-ui.md for the
rest of the out-of-scope list (Decision 4 of docs/plans/fts-ui-2.md is what replaced its "never restarts
the API" clause). Still zero new package or project reference: System.Net.HttpListener, HttpClient
and System.Text.Json are all in the shared framework, and the embedded page is a separately built,
standalone React SPA (platform-cli/ui, Nx project @fts/ui) baked into the tool's own .nupkg at pack
time — the CLI's dependency graph is exactly as empty as it was for the other five commands.
Screenshots of every screen, in both themes, are saved under
docs/tasks/fts-ui-2-3e8a30d8/screenshots/ (Playwright's visual pass, e2e/fts-ui-visual.spec.ts) —
not embedded here, since nothing else in this README inlines an image.
Written down rather than built
Two more commands would be useful. They are recorded here instead of shipped, because the ceiling is
six, and no more without deleting one — only what plain dotnet cannot do, or does materially
worse, is the whole discipline of this tool:
fts feed check— verify without writing.feed initis already idempotent, so this is a flag at most.fts advisories— the embedded table without the project scan.
The advisory table
Embedded in the assembly, not queried over the network: a live advisory API would mean network, authentication and a second failure mode inside a command whose job is to be useful when the feed is not answering. The report prints the snapshot date and marks evidence older than 30 days or dated in the future as stale. Historical rows retain their known fix even after a suppression is removed. Source mentions of architecture rules are reported as mentions, never proof of assertions or test execution. Report-only exit behavior is unchanged; strict/evaluated/resolved-graph modes remain open.
Verifying it
The tool is exercised end to end by scripts/verify-cli.ps1 in its repository: it packs the tool,
installs it to a local tool path, scaffolds three projects by hand plus a fourth through fts new api
itself and a module into that one through fts add module, and runs all five commands against a live
feed and against a local folder feed, reporting which of the three diagnoses were produced live and
which locally.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.