Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions .template.config/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
"description": "Include the React admin + dashboard client apps (clients/).",
"defaultValue": "true"
},
"agents": {
"type": "parameter",
"datatype": "bool",
"description": "Include the AI coding guides (AGENTS.md, CLAUDE.md, GEMINI.md and .agents/).",
"defaultValue": "true"
},
"skipRestore": {
"type": "parameter",
"datatype": "bool",
Expand Down Expand Up @@ -112,6 +118,23 @@
"underscoreForm": { "identifier": "replace", "pattern": "\\.", "replacement": "_" },
"displayForm": { "identifier": "replace", "pattern": "[._-]+", "replacement": " " }
},
"SpecialCustomOperations": {
"**/*.md": {
"operations": [
{
"type": "conditional",
"configuration": {
"if": ["<!--#if"],
"else": ["<!--#else"],
"elseif": ["<!--#elseif"],
"endif": ["<!--#endif"],
"trim": "true",
"wholeLine": "true"
}
}
]
}
},
"sources": [
{
"source": "./",
Expand All @@ -122,10 +145,11 @@
"**/.vscode/**",
"**/.vs/**",
".github/**",
".agents/**",
".claude/**",
".devcontainer/**",
".git/**",
".git",
"superpowers/**",
"templates/**/*",
"demo/**",
"docs/**",
Expand All @@ -150,8 +174,6 @@
"**/obj/**",
".mcp.json",
"global.json",
"CLAUDE.md",
"GEMINI.md",
"README.md",
"LICENSE"
],
Expand All @@ -165,6 +187,15 @@
"src/Host/FSH.Starter.AppHost/**"
]
},
{
"condition": "(!agents)",
"exclude": [
"AGENTS.md",
"CLAUDE.md",
"GEMINI.md",
".agents/**"
]
},
{
"condition": "(!frontend)",
"exclude": [
Expand Down
9 changes: 8 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ front-ends and a CLI. Multitenancy, auth, auditing, billing, files, chat and mor
| `src/Host/FSH.Starter.DbMigrator` | One-shot migrate/seed runner. DB is **not** migrated at API startup. |
| `src/Host/FSH.Starter.Migrations.PostgreSQL` | All EF migrations, organized per-module by folder. |
| `src/Tests/` | Per-module tests, `Architecture.Tests` (NetArchTest), `Integration.Tests` (Testcontainers). |
| `src/Tools/CLI` | The `fsh` CLI (Spectre.Console). |
| `clients/admin`, `clients/dashboard` | The two React apps. |
| `deploy/` | Infra (docker, terraform, dokploy). |

<!--#if (includeTools) -->
The `fsh` CLI (Spectre.Console) lives in `src/Tools/CLI`.

<!--#endif -->
## Tech stack

| Backend | | Frontend | |
Expand Down Expand Up @@ -70,10 +73,12 @@ dotnet run --project src/Host/FSH.Starter.DbMigrator -- list-pending

**Ports:** API 7030 (https)/5030 (http) · admin 5173 · dashboard 5174 · Postgres 5432 · pgAdmin 5050 · Valkey 6379 · RustFS 9000/9001 (S3 API/console).

<!--#if (includeTools) -->
## Branching & PRs

Single long-lived branch: **`main`** (the default) — there is **no `develop`**. Branch from and target `main`; stable releases are cut from `v*` tags. CI is split into path-scoped **Backend CI** (`src/**`) and **Frontend CI** (`clients/**`) workflows; branch protection requires only those two gate checks — never the individual jobs, which are skipped on the other side's PRs.

<!--#endif -->
## Golden rules (do not break)

1. **Module boundaries** — a module references another module only through its `.Contracts` project, never its runtime project. Enforced by `Architecture.Tests`.
Expand All @@ -85,7 +90,9 @@ Single long-lived branch: **`main`** (the default) — there is **no `develop`**
7. **Propagate `CancellationToken`** into every EF/IO call; add as `= default` on public service methods.
8. **Every command handler + paginated query handler needs a validator** (`{Name}Validator`). Enforced by `Architecture.Tests`.
9. **Frontend: pass per-call data through `mutate(arg)`**, never via state the mutation callbacks close over (execute-time race). See `frontend/shared.md`.
<!--#if (includeTools) -->
10. **Docs + changelog travel with the change** — a user-facing change (feature, endpoint, config, infra, breaking change) isn't done until the **separate docs repo** (`github.com/fullstackhero/docs`, the Astro site) is updated to match **and** a changelog entry is added (`src/content/docs/changelog/`). Don't let the docs drift from the code.
<!--#endif -->

## Rules index — read the relevant file before you work

Expand Down
19 changes: 14 additions & 5 deletions src/Tools/CLI/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public sealed class Settings : CommandSettings
[DefaultValue(false)]
public bool NoFrontend { get; init; }

[Description("Exclude the AI coding guides (AGENTS.md, CLAUDE.md, GEMINI.md, .agents/).")]
[CommandOption("--no-agents")]
[DefaultValue(false)]
public bool NoAgents { get; init; }

[Description("Skip 'npm install' for the React apps after scaffolding.")]
[CommandOption("--skip-install")]
[DefaultValue(false)]
Expand Down Expand Up @@ -69,6 +74,8 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings

bool frontend = await ResolveFrontendAsync(settings, cancellationToken).ConfigureAwait(false);

bool agents = !settings.NoAgents;

string output = settings.Output ?? Path.GetFullPath(name);

// 2. Check for existing directory
Expand All @@ -89,7 +96,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
}

// 3. Print summary
PrintSummary(name, aspire, frontend, output, settings.DryRun);
PrintSummary(name, aspire, frontend, agents, output, settings.DryRun);

if (settings.DryRun)
{
Expand All @@ -102,7 +109,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
return 1;

// 5. Scaffold project
int result = await ScaffoldProjectAsync(name, aspire, frontend, output, cancellationToken).ConfigureAwait(false);
int result = await ScaffoldProjectAsync(name, aspire, frontend, agents, output, cancellationToken).ConfigureAwait(false);
if (result != 0)
{
AnsiConsole.MarkupLine($"[{FshConstants.ErrorColor}]Scaffolding failed. Check the output above for errors.[/]");
Expand Down Expand Up @@ -179,14 +186,15 @@ private static async Task<bool> ResolveFrontendAsync(Settings settings, Cancella
.ShowAsync(AnsiConsole.Console, cancellationToken).ConfigureAwait(false);
}

private static void PrintSummary(string name, bool aspire, bool frontend, string output, bool dryRun)
private static void PrintSummary(string name, bool aspire, bool frontend, bool agents, string output, bool dryRun)
{
AnsiConsole.WriteLine();

string mode = dryRun ? " [yellow](dry run)[/]" : "";
AnsiConsole.MarkupLine($"[bold]Creating project:[/] {name.EscapeMarkup()}{mode}");
AnsiConsole.MarkupLine($" [{FshConstants.DimColor}]Aspire:[/] {(aspire ? "yes" : "no")}");
AnsiConsole.MarkupLine($" [{FshConstants.DimColor}]Frontend:[/] {(frontend ? "yes (admin + dashboard)" : "no")}");
AnsiConsole.MarkupLine($" [{FshConstants.DimColor}]AI guides:[/] {(agents ? "yes (AGENTS.md, CLAUDE.md, GEMINI.md, .agents/)" : "no")}");
AnsiConsole.MarkupLine($" [{FshConstants.DimColor}]Output:[/] {output.EscapeMarkup()}");
AnsiConsole.WriteLine();
}
Expand Down Expand Up @@ -224,7 +232,7 @@ await ProcessRunner.RunAsync(
}

private static async Task<int> ScaffoldProjectAsync(
string name, bool aspire, bool frontend, string output, CancellationToken cancellationToken)
string name, bool aspire, bool frontend, bool agents, string output, CancellationToken cancellationToken)
{
return await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
Expand All @@ -233,7 +241,8 @@ private static async Task<int> ScaffoldProjectAsync(
{
string aspireFlag = aspire ? "true" : "false";
string frontendFlag = frontend ? "true" : "false";
string args = $"new {FshConstants.TemplateShortName} -n {name} -o \"{output}\" --aspire {aspireFlag} --frontend {frontendFlag} --force";
string agentsFlag = agents ? "true" : "false";
string args = $"new {FshConstants.TemplateShortName} -n {name} -o \"{output}\" --aspire {aspireFlag} --frontend {frontendFlag} --agents {agentsFlag} --force";
await ProcessRunner.RunAsync("dotnet", args, showOutput: false, cancellationToken: cancellationToken)
.ConfigureAwait(false);

Expand Down
Loading