feat(cli): opt-in framework packages, .agents scaffolding, and fork-local templates - #1372
feat(cli): opt-in framework packages, .agents scaffolding, and fork-local templates#1372maxiar wants to merge 9 commits into
Conversation
Testcontainers 4.11.0 depends on SSH.NET 2025.1.0, which carries a high-severity advisory (GHSA-q939-rpr3-3284: ScpClient recursive download allows arbitrary file write via server-controlled SCP filenames). NuGetAudit raises NU1903 and, under TreatWarningsAsErrors, fails restore for Integration.Tests and Integration.Middleware.Tests. Testcontainers 4.14.0 already depends on the patched SSH.NET 2026.0.0, so bumping the three Testcontainers.* packages clears the advisory without a transitive pin that would later need removing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Testcontainers 4.11.0 depends on SSH.NET 2025.1.0, which carries a high-severity advisory (GHSA-q939-rpr3-3284: ScpClient recursive download allows arbitrary file write via server-controlled SCP filenames). NuGetAudit raises NU1903 and, under TreatWarningsAsErrors, fails restore for Integration.Tests and Integration.Middleware.Tests. Testcontainers 4.14.0 already depends on the patched SSH.NET 2026.0.0, so bumping the three Testcontainers.* packages clears the advisory without a transitive pin that would later need removing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…otnet-starter-kit into fix/testcontainers-ssh-net-advisory
fix(deps): bump Testcontainers to 4.14.0 to clear the SSH.NET advisory
…ocal templates
Three additions to the `fsh` CLI, all opt-in. Default `fsh new` output is
unchanged — same files, same modes — and the template smoke test proves it.
Template acquisition
`fsh new` accepted whatever FSH template was already installed and never
upgraded it, with no way to point at anything else. Adds --template-path
(checkout, .nupkg, or folder of nupkgs), --template-version,
--template-source and --refresh-template, each with an env-var fallback,
so a contributor can scaffold from their own fork.
.agents kit
`.agents/` was excluded twice (pack pathspec + template.json) while
AGENTS.md shipped on its own, leaving every scaffold with a rules index
pointing at files that were never copied. `--agents` now ships all four;
without it, AGENTS.md is correctly withheld too.
Framework packages
`--framework-packages` consumes src/BuildingBlocks as FSH.Framework.*
NuGet packages from a local feed instead of copying its source into every
project. src/Directory.Build.targets rewrites BuildingBlocks
ProjectReferences into PackageReferences, keyed off the absence of the
source tree, so the 33 project files are identical in both modes.
New commands: `fsh framework pack|list|swap|clean-cache`, plus
`fsh self install` to build the CLI from source as the global tool.
Packages carry an embedded PDB with embedded sources: a folder feed does not
serve .snupkg, so embedding is what actually makes step-into debugging work.
Each pack stamps a unique 10.0.0-local.<timestamp> version because NuGet
caches by id+version and would otherwise serve stale bits.
Also fixes, all pre-existing:
- FullStackHero.CLI never shipped a .snupkg. The packaging block in
Directory.Build.props is conditioned on $(IsPackable), but that file is
imported before the csproj sets it, so it never applied. Moved to
Directory.Build.targets, where the value is final.
- ProcessRunner.CaptureAsync redirected stderr without draining it, which
can deadlock a child that fills the pipe buffer.
- `fsh doctor` failed any SDK not starting with "10." despite promising
.NET 10+.
- The update check compared against AssemblyVersion, pinned to 10.0.0.0,
so every patch build nagged about itself.
- `new` reported "Scaffolding failed" while discarding the dotnet output
that explained why. Unknown options were silently ignored rather than
rejected; strict parsing is now on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… template `fsh new` creates a project and `fsh framework swap` changes its mode, but there was no way to bring an already-generated project up to date with the template it came from. Re-scaffolding overwrites; hand-merging a template that renames every token is impractical. This closes that gap. The upgrade is a three-way merge rather than a re-scaffold. The pristine scaffold commit `fsh new` created is the common ancestor, and a freshly generated scaffold of the same project — same name, same options — is the new state. Committing that on a branch rooted at the ancestor lets git merge it forward, so local work is preserved and genuine collisions surface as ordinary conflicts. It runs in a temporary worktree, so the caller's checkout is untouched until they merge. Scaffold options are recovered from the shape of the baseline tree rather than restated, so an upgrade cannot regenerate a differently-shaped project: an AppHost directory means aspire, clients/ means frontend, .agents/ means the agents kit, and a missing src/BuildingBlocks means framework packages. Two files need special handling because `fsh new` writes them after the template runs and they are committed in the baseline. Regenerating plainly would delete NuGet.config and revert the per-project dev signing key to the shared placeholder, so both are carried forward and never appear in the diff. Also adds ProcessRunner.CaptureWithErrorAsync(trimOutput:) — restoring a file from git needs its bytes intact, and trimming dropped the trailing newline, which showed the restored file as modified in every later diff. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ages Two BuildingBlocksIndependenceTests read the BuildingBlocks .csproj files from disk. A project scaffolded with --framework-packages has no src/BuildingBlocks at all, so those tests threw from the moment such a project was created - a gap in the framework-packages feature, not something the projects did wrong. They now carry [KernelSourceOnlyFact] and report as skipped in package mode. Assembly-level architecture checks in the same class are untouched and keep running in both modes, because those assemblies come from the packages. Uses a FactAttribute subclass rather than Assert.Skip, which needs xUnit v3; this mirrors the PostgresOnlyFact/SqlServerOnlyFact pattern the MSSQL work introduced in Integration.Tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`fsh upgrade` merges the owned source, but on a framework-packages project the kernel arrives through the feed instead. Documenting only the command leaves the most common failure undocumented: merging the template without republishing the packages, which produces a project that no longer compiles. Adds the three-step runbook (republish kernel, merge template, move the pin), records the FSH_LOCAL_FEED footgun that silently publishes to the wrong feed, and explains which post-upgrade test failures are the expected signal rather than a regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed three more commits (37 → 42 files). They close a gap the original PR left open: it added a way to create a project on framework packages, but no way to bring an existing one forward.
A bug fix in the feature this PR already shipped. Two Docs — an end-to-end upgrade runbook. Worth calling out because the non-obvious part is that Exercised on a real projectNot a synthetic test: a ~100-commit application on framework packages, upgraded to a template revision that touched 145 files. Result was one conflict, in Heads-up on CIThis branch will fail to build on 🤖 Generated with Claude Code |
…not a constant
BuildingBlocks hard-codes the Data Protection application name ("FSH.Starter")
and the root tenant's issuer ("mukesh.murugan"). In source mode the template
renames both when scaffolding, so each project gets its own and there is no
problem — which is why this has never mattered on main.
It matters once the framework also ships as the compiled FSH.Framework.*
packages this PR introduces, because the template's token substitution cannot
reach a literal inside a DLL. Verified in the published binaries:
fsh.framework.caching -> contains "FSH.Starter"
fsh.framework.shared -> contains "mukesh.murugan"
Data Protection isolates keys BY the application name, so every project built on
the packages shared one key ring: two of them pointed at the same Redis could
decrypt each other's auth cookies, password-reset and confirmation tokens.
Both values now come from appsettings.json, which IS scaffolded source and is
renamed per project in both distribution modes. The constants remain only as
last-resort defaults, and the application name falls back to the entry assembly
name rather than a shared literal, so a host that forgot the setting gets its
own key ring instead of silently joining someone else's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed one more commit: the Data Protection application name is now read from configuration instead of being a constant in BuildingBlocks. It belongs in this PR rather than as a standalone fix, because on Data Protection isolates keys by the application name, so every project built on those packages shared one key ring — two of them pointed at the same Redis could decrypt each other's auth cookies and password-reset tokens. The root tenant's issuer leaked the same way. Both now come from Related: #1376 builds on this to add a database-backed key store, and depends on this PR for |
Three opt-in additions to the
fshCLI, aimed at running several products off one kernel.Check Open Discussion: #1373
The default path does not move.
fsh new MyAppproduces exactly what it produced before — every new behaviour sits behind a template symbol that defaults tofalseand a CLI flag that is off. A newtemplate-smoke.ymljob proves it, and the existing two jobs are untouched.1. Scaffolding from your own fork
fsh newaccepted whatever FSH template happened to be installed and never upgraded it (EnsureTemplateInstalledAsyncshort-circuits on any match), installingFullStackHero.NET.StarterKitwith a bare package id — no version, no source. A contributor working on a fork had no way to scaffold from their own branch.Each has an env-var fallback (
FSH_TEMPLATE_PATH, …). Overriding also uninstalls the previous template first — two packages sharing the identityFullStackHero.NET.StarterKitmakedotnet newfail withSequence contains more than one matching element.2. Shipping the
.agentskit.agents/was excluded twice — the pack pathspec intemplates/FullStackHero.NET.StarterKit.csprojandtemplate.json— whileAGENTS.mdwas in neither exclude list. Every scaffolded project therefore shipped a guide whose entire rules index pointed at files that were never copied.--agentsnow ships.agents/(43 files) plusAGENTS.md,CLAUDE.md,GEMINI.md. Without it,AGENTS.mdis correctly withheld too, closing the dangling reference in the current default output.3. BuildingBlocks as
FSH.Framework.*packagessrc/Directory.Build.targets(new) rewrites anyProjectReferenceintoBuildingBlocksas the matchingPackageReference, so all 33 project files are byte-identical in both modes and new modules get it for free. It keys offsrc/BuildingBlocksbeing absent — which is exactly the shape a package-mode scaffold has — so it cannot drift from how the project was created.-p:UseFrameworkPackages=true|falseforces either way.Two decisions worth reviewing
Packages carry an embedded PDB with embedded sources, not a
.snupkg. A folder feed does not serve symbol packages — only the NuGet.org symbol server does — so a.snupkgcannot give step-into debugging locally. Embedding puts the PDB and the sources inside the DLL, so F11 works offline with no source checkout. Thepublicprofile (--profile public) keeps the conventional.snupkg+ SourceLink for real feeds.swap --to sourceregenerates the kernel from the template rather than copying it from a kit clone. The template rewrites tokens inside BuildingBlocks, and not all are cosmetic —MultitenancyConstants.Issueris derived from the project name. A raw copy would silently install the starter kit's own JWT issuer into someone else's project. The command scaffolds a throwaway copy under the target project's name and lifts the kernel from that. Verified: swappingFS.ProxyyieldsIssuer = "fs-proxy", not"mukesh.murugan".Pre-existing bugs fixed along the way
FullStackHero.CLIhas never shipped a.snupkg.Directory.Build.propsdeclares "Reproducible, debuggable packages for the projects that DO ship (CLI)" behindCondition="'$(IsPackable)' == 'true'"— but that file is imported before the csproj setsIsPackable, and MSBuild evaluates properties in document order, soIncludeSymbolsstayed empty andSymbolPackageFormatfell back to the SDK default. Moved toDirectory.Build.targets, where the value is final. The CLI now packs a real.snupkg.ProcessRunner.CaptureAsyncredirected stderr without draining it — a latent deadlock for any child that fills the pipe buffer.fsh doctorfailed any SDK not starting with"10.", despite the message promising ".NET 10+".AssemblyVersion, pinned to10.0.0.0in the csproj, so a10.0.1build nagged about upgrading to itself.newprinted "Scaffolding failed" while discarding thedotnet newoutput that explained why, and unknown options were silently dropped into Spectre's remaining-args bag rather than rejected — a mistyped flag quietly produced the wrong project. Strict parsing is now on;--agents/--framework-packagesaccept both the bare flag and an explicittrue/false.Verification
BuildingBlocks, packages from a local feed):dotnet build -warnaserror→ 0 warnings, 0 errors.agents/NuGet.config/README-CLI.mdcorrectly absentFSH.Framework.Core.dllcarries the embedded PDB (32,256 vs 15,360 bytes)swapround-trip on three projects (born-in-packages, born-in-source, and a fresh example), each direction building clean; idempotent, guarded outside a projectNot included
Golden rule 10: this is user-facing, so the separate docs repo (
github.com/fullstackhero/docs) and asrc/content/docs/changelog/entry still need updating.README-CLI.mdis written to port into it.Note for upstreaming
src/Directory.Build.propsdocuments source ownership as the distribution model, andbackend.ymlrecords that per-module packs were removed once for contradicting it. This does not reverse that: source ownership stays the default and framework packaging is opt-in. If proposed upstream, items 1 and 2 are uncontroversial and should go first as separate PRs; item 3 is the one that needs the conversation.🤖 Generated with Claude Code