FTS.Cli 0.7.0

There is a newer version of this package available.
See the version list below for details.
dotnet tool install --global FTS.Cli --version 0.7.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local FTS.Cli --version 0.7.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=FTS.Cli&version=0.7.0
                    
nuke :add-package FTS.Cli --version 0.7.0
                    

FTS.Cli — fts

Four commands for a project that consumes the FTS platform packages. Four 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.

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 four commands exist

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 source writes 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

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.9.0 support platform >=0.10.0 and <0.11.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-api happily 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 .fts-add-module.lock prevents concurrent writers/resume on supported Windows/Linux local filesystems. 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.

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:

  1. Stop any active CLI operation and its child processes before inspecting or changing recovery files.
  2. Read .fts-add-module.pending and its recovery directory's manifest.json. A before checkpoint without after is ambiguous: the child may have applied some or all of that edit.
  3. Compare current solution, host project and Program.cs with 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.
  4. Either finish those edits by hand, or restore only the reviewed CLI changes and move the generated module out of src/Modules to a separate archive. Preserve any user changes inside it.
  5. Only after reconciliation, archive/remove the pending marker. For the restored-to-before option, rerun fts add module; then run the generated scripts/verify.ps1. For .slnx, an already registered response checks both solution entries, the test-project file and the evaluated host reference, but does not establish a successful build or runtime verification. Legacy .sln integrity 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:

  1. dotnet new fts-module -n Billing -o src/Modules/Billing
  2. dotnet sln add src/Modules/Billing/Billing.csproj and its Tests/Billing.Tests.csproj
  3. dotnet add src/<host>/<host>.csproj reference src/Modules/Billing/Billing.csproj
  4. new Billing.BillingModule() into the host's existing AddModules(...) 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-module writes 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 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. 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-transitive needs 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.

Written down rather than built

Two more commands would be useful. They are recorded here instead of shipped, because the ceiling is four, 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 init is 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 four commands against a live feed and against a local folder feed, reporting which of the three diagnoses were produced live and which locally.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.11.0 0 10/5/2026
0.7.0 0 9/15/2026
0.6.0 0 9/12/2026
0.4.0 0 9/9/2026
0.2.0 0 9/8/2026
0.1.0 0 9/8/2026