FTS.Cli 0.1.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.1.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.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=FTS.Cli&version=0.1.0
                    
nuke :add-package FTS.Cli --version 0.1.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

Measured against a GitHub Packages feed, and this table is the whole justification:

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, because the index answers 200 for exactly the token that cannot restore. Three inputs, three diagnoses, three exit codes:

Result Exit What it says
no credential (401) 3 Nothing authenticated. Set a token.
token without read:packages (403) 4 Names the missing scope, and says the index answers 200 for this same token.
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                              # the org feed, verified first
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 --feed <url|folder>          # verify a different feed
fts new api Ordering --skip-feed-check            # scaffold without verifying anything

Verifies the feed first, and only then scaffolds. The pre-check is feed init's exact proof — a real package restore, run in a throwaway directory holding the config feed init would write — so a scaffold that could not have restored never becomes a directory. A failing pre-check returns the same 3 / 4 / 5 and prints the same diagnosis, and writes nothing at all.

The five seam flags — --auth, --db, --tenancy, --audit, --platform-version — are forwarded verbatim and their values are never inspected here. 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. What this command does check is the option name: an unrecognised token is a usage error rather than a puzzling dotnet new message.

--feed selects what is verified and nothing else. The template has no feed parameter, so the emitted nuget.config always names the org feed and deliberately carries no credential — which is why the command's last line tells you to run fts feed init inside the new directory.

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 Billing --platform-version 0.4.0         # pin the emitted references

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, and a refusal is inert: the analysis is a read and it runs before anything is scaffolded, so a refused run leaves the tree exactly as it found it and prints the five edits to make by hand. 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, and not by checking afterwards: an argument list that already constructs the module — qualified or unqualified — returns before the first child process runs, so there is no second dotnet new, no second solution entry and no duplicate ProjectReference by construction.

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

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 which advisories apply to what this project actually references.

That last clause is the point. FTS.Platform.Sqlite carries a high-severity advisory with no upstream fix; a project composed on Abstractions + Hosting + Defaults never acquires it, and saying so is useful where a generic list of every known advisory is noise.

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 staleness that buys is gated in the platform repository, where every row must still correspond to an advisory that repository suppresses by URL and to a package that still exists.

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.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