From 5d084c645caaf0bb3671a196dce1e84cde465960 Mon Sep 17 00:00:00 2001 From: manvkaur <67894494+manvkaur@users.noreply.github.com> Date: Mon, 18 May 2026 14:39:39 -0700 Subject: [PATCH 1/7] add templates e2e command --- proposed/templates-e2e.md | 883 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 883 insertions(+) create mode 100644 proposed/templates-e2e.md diff --git a/proposed/templates-e2e.md b/proposed/templates-e2e.md new file mode 100644 index 000000000..413cd54d5 --- /dev/null +++ b/proposed/templates-e2e.md @@ -0,0 +1,883 @@ +# Core Tools vNext: `func new templates` — CDN-Backed Template Discovery + +**Author:** manvkaur +**Date:** 2026-05-18 +**Status:** Draft +**Work Item:** TBD + +--- + +## Table of Contents + +- [Problem Statement](#problem-statement) +- [Goals / Non-Goals](#goals--non-goals) +- [Proposed Design](#proposed-design) + - [Command Tree](#command-tree) + - [New Services](#new-services) + - [ITemplateManifestService](#itemplatemanifestservice) + - [Runtime-to-Language Mapping](#runtime-to-language-mapping) + - [ITemplateFunctionScaffolder](#itemplatefunctionscaffolder) + - [User Experience Flow](#user-experience-flow) + - [Execution Flow](#execution-flow) + - [Manifest Cache Flow](#manifest-cache-flow) + - [Template Download Strategy](#template-download-strategy) + - [New Commands](#new-commands) + - [TemplatesCommand](#templatescommand) + - [TemplatesListCommand](#templateslistcommand) + - [TemplatesInfoCommand](#templatesinfocommand) + - [NewCommand Change](#newcommand-change) + - [BuiltInCommands Change](#builtincommands-change) + - [File Layout](#file-layout) +- [IInteractionService Prompt Gap](#iinteractionservice-prompt-gap) +- [Error Messaging](#error-messaging) +- [Incremental: `--env` Flag](#incremental---env-flag) +- [Open Questions](#open-questions) +- [Appendix](#appendix) + - [References](#references) + - [vnext Architecture](#vnext-architecture-what-already-exists) + - [fnx init — Source Implementation](#fnx-init--source-implementation) + +--- + +## Problem Statement + +Today's function app templates are scattered across multiple sources — the CLI binary, extension bundle, VS Code extension, and Maven repository — each shipping on its own cadence. Getting a template in front of a developer requires a coordinated release across several of these channels, with a minimum turnaround of 6–8 weeks from merge to broad availability. Templates are also bundled inside the CLI binary itself, meaning every addition or update requires a full CLI release. + +This design introduces `func new templates`: a command that downloads **complete, immediately runnable function app templates** directly from GitHub. Templates are discovered via a live manifest hosted on the Azure Functions CDN. Adding a new template is as simple as publishing a GitHub repo and adding an entry to the manifest — no CLI release required. The manifest is versioned independently; the CLI picks up new templates automatically on the next manifest refresh. + +Three pain points this solves: + +- **Release cycle friction** — new templates are available as soon as the manifest is updated, not after a 6–8 week CLI release cycle +- **Maintenance burden** — templates live as standalone GitHub repos, not embedded in the CLI binary; they can be updated, PR'd, and iterated on independently +- **Scaffolding gap in vnext** — the vnext `func new` stub exits with code 1 if no workload is installed; `func new templates` gives developers a working template experience from a clean install, no workload required + +--- + +## Goals / Non-Goals + +### Goals + +- Add `func new templates` as a built-in subcommand of `NewCommand` on the vnext branch +- Download and create a **complete, runnable function app** from a GitHub template — all function code, config, and dependencies included; `func start` works immediately after scaffolding +- CDN-backed template manifest with ETag caching (24h TTL) +- `func new templates list` — non-interactive table of available templates, filterable by `--language`, `--resource`, `--iac`, and keyword (`--search`); `--search` is a **case-insensitive substring match** (not semantic/fuzzy) against `id`, `displayName`, `resource`, `tags`, and `shortDescription` +- `func new templates` (bare invocation) — interactive flow: worker runtime → (Node sub-prompt: JS/TS) → trigger → scaffold; the runtime prompt matches the existing `func new` UX (`dotnet (isolated worker model)`, `Node`, `Python`, `Java`, `Powershell`); trigger selection supports incremental keyword filtering; powered by the existing `IInteractionService` / Spectre.Console already in vnext +- **.NET isolated worker model only** — the manifest `CSharp` language maps exclusively to the .NET isolated worker model; the in-process model is not supported and will not be added (it is on a deprecation path) +- **Agent and CI friendly** — all v4 `func new` flags preserved (`--language`, `--template`); `--language` accepts runtime names (`python`, `node`, `java`, `dotnet-isolated`, `javascript`, `typescript`, `powershell`); `--yes` accepts remaining defaults non-interactively but errors clearly if `--language` is not supplied; non-TTY falls back to numbered list +- Template download via git sparse-checkout or GitHub zip API (no bundling in binary) +- `--path` to specify target directory (absolute or relative); created if absent, accepted if it exists and is empty — error if it exists and is not empty + +### Non-Goals + +- Adding a single function file to an existing project (that is v4 `func new` behaviour — out of scope here) +- Replacing or changing `func init` (separate command, workload-driven) +- Changing the workload model or `IProjectInitializer` interface +- Automatic environment setup (venv, npm install, dotnet restore) — **deferred to `--env` incremental** (see [Incremental: `--env` Flag](#incremental---env-flag)) +- File conflict handling and `--force` flag — **deferred to next iteration of this command**; v1 requires target directory to be empty + +> **Key difference from v4 `func new`:** v4 adds a single function file into an *existing* project. `func new templates` downloads a complete, runnable function app from a GitHub template — function code, host.json, local.settings.json, .gitignore, and all dependencies — into a target directory. `func start` works immediately. This is why `--language` is required with no auto-detect fallback (target is assumed empty before scaffolding). + +--- + +## Proposed Design + +> **Command keyword note:** `templates` is the current choice for the subcommand name (`func new templates`). If the command is later promoted to top-level, this name becomes the top-level verb — the right name should be confirmed during design review before vNext ships. + +### Command Tree + +```bash +func new # NewCommand (existing) + func new templates # TemplatesCommand (new — interactive flow when bare) + func new templates list # TemplatesListCommand (new — table output) + func new templates info # TemplatesInfoCommand (new — detailed template info) +``` + +> **Promotion path** — `TemplatesCommand` is designed to be re-parented as a top-level command (`func `) with zero logic changes. All business logic lives in `ITemplateManifestService` and `ITemplateFunctionScaffolder`; the command is a thin shell. When/if promoted: +> +> - Change registration in `BuiltInCommands` from `services.AddSingleton()` to `services.AddSingleton()` +> - Remove `Subcommands.Add(templatesCommand)` from `NewCommand` +> - No changes to services, scaffolding, prompts, or tests + +**User experience:** + +```bash +# Interactive scaffold — arrow keys to navigate, type to live-filter +$ func new templates + + Use the up/down arrow keys to select a worker runtime: + dotnet (isolated worker model) + Node + Python + Java + Powershell + + # User picks Node → sub-prompt for language: + Use the up/down arrow keys to select a language: + JavaScript + TypeScript + + # User types "bl" — list narrows live as they type, arrow keys still work: + Use the up/down arrow keys to select a template: bl + Blob EventGrid Trigger (TypeScript + AZD + Bicep) + + # User clears search, navigates with arrows: + Use the up/down arrow keys to select a template: + HTTP Trigger (TypeScript + AZD + Bicep) + Timer Trigger (TypeScript + AZD + Bicep) + Blob EventGrid Trigger (TypeScript + AZD + Bicep) + ... + + Created http-trigger-typescript-azd in current directory + + Next steps: + 1. npm install + 2. npm run build + 3. func start + +# Interactive scaffold into a named folder (created if it doesn't exist) +$ func new templates --path ./my-new-api + + Use the up/down arrow keys to select a worker runtime: ... + + # (after selection) + Created in ./my-new-api + + Next steps: + 1. cd ./my-new-api + 2. + 3. func start + +# Scaffold into a specific folder (created if absent; also accepted if it exists and is empty) +$ func new templates --template blob-eventgrid-trigger-python-azd --path ./my-fn +$ func new templates --template blob-eventgrid-trigger-python-azd --path /home/user/projects/my-fn + +# Directory is not empty → error +$ func new templates --template http-trigger-python-azd --path ./existing-dir + Error: Target directory './existing-dir' is not empty. + Use an empty directory or a new --path. + +# Network failure — CDN unreachable (manifest fetch) +$ func new templates + Error: This feature requires a network connection to fetch the template catalog. + Please check your connection and try again. + +# Network failure — GitHub unreachable (template download) +$ func new templates --template http-trigger-python-azd + Error: Unable to reach GitHub to download the template. + Please check your network connection and try again. + +# --yes requires --language (target is empty — nothing to detect from) +$ func new templates --language python --yes + Created http-trigger-python-azd in current directory + + Next steps: + 1. python -m venv .venv + 2. .venv\Scripts\activate # macOS/Linux: source .venv/bin/activate + 3. pip install -r requirements.txt + 4. func start + +# --yes without --language → clear error: +# Error: --language is required. Target directory is empty; cannot auto-detect language. +``` + +**Post-scaffold success banner:** + +Every successful scaffold prints a success line followed by runtime-specific next steps. When `--path` is supplied, step 1 is `cd `. When scaffolding into the current directory, the `cd` step is omitted and remaining steps are renumbered. + +| Runtime | Next Steps | +| --- | --- | +| Python | `python -m venv .venv`, `.venv\Scripts\activate` (Windows) or `source .venv/bin/activate` (macOS/Linux), `pip install -r requirements.txt`, `func start` — the CLI detects the platform and shows the correct activation command | +| TypeScript | `npm install`, `npm run build`, `func start` | +| JavaScript | `npm install`, `func start` | +| .NET (isolated) | `dotnet restore`, `func start` | +| Java | `mvn clean package`, `func start` | +| PowerShell | `func start` (no dependency step) | + +```bash +# Filter by language — Language column omitted (redundant) +$ func new templates list --language python + + Id Resource IaC + ──────────────────────────────────────────────────────── + http-trigger-python-azd http bicep + timer-trigger-python-azd timer bicep + blob-eventgrid-trigger-python-azd blob bicep + eventhub-trigger-python-azd eventhub bicep + servicebus-trigger-python-azd servicebus bicep + ... + +# Filter by resource — Resource column omitted (redundant) +$ func new templates list --resource http + + Id Language IaC + ──────────────────────────────────────────────────────── + http-trigger-csharp-azd CSharp bicep + http-trigger-python-azd Python bicep + http-trigger-typescript-azd TypeScript bicep + http-trigger-javascript-azd JavaScript bicep + ... + +# Filter by iac — IaC column omitted (redundant) +$ func new templates list --iac bicep + + Id Resource Language + ──────────────────────────────────────────────────────────── + http-trigger-csharp-azd http CSharp + timer-trigger-python-azd timer Python + ... + +# Keyword search — all columns shown (no filter to omit) +$ func new templates list --search blob + + Id Resource Language IaC + ──────────────────────────────────────────────────────────────────────── + blob-eventgrid-trigger-csharp-azd blob CSharp bicep + blob-eventgrid-trigger-python-azd blob Python bicep + blob-eventgrid-trigger-typescript-azd blob TypeScript bicep + ... + +# Combined — Language and Resource columns omitted +$ func new templates list --language python --resource blob + +# Non-interactive scaffold with language + resource +$ func new templates --language python --resource http + +# Non-interactive scaffold with language + resource + iac +$ func new templates --language python --resource http --iac bicep + +# Non-interactive scaffold by exact template id (skips language + trigger prompts) +$ func new templates --template http-trigger-python-azd + +# Show detailed info about a specific template +$ func new templates info http-trigger-python-azd + + HTTP Trigger (Python + AZD + Bicep) + + Python Azure Function with an HttpTrigger. Runs on Flex Consumption plan + with managed identity authentication and VNet integration for secure + networking. Infrastructure provisioned with Bicep and deployed with azd. + + Language: Python + Resource: http + IaC: bicep + Repository: https://github.com/Azure-Samples/functions-quickstart-python-http-azd + + What's included: + - HTTP trigger function + - Bicep infrastructure files + - Azure Developer CLI (azd) configuration + - VNet integration setup + - VS Code debug configuration + - Local development configuration +``` + +--- + +### New Services + +#### `ITemplateManifestService` + +```csharp +// src/Func/Templates/ITemplateManifestService.cs +internal interface ITemplateManifestService +{ + Task GetManifestAsync(CancellationToken cancellationToken = default); +} +``` + +`TemplateManifestService` implementation (ported from `fnx/lib/init/manifest.js`): + +| Behaviour | Detail | +| ----------- | -------- | +| Primary URL | `https://cdn.functions.azure.com/public/templates-manifest/manifest.json` | +| Backup URL | `https://raw.githubusercontent.com/Azure/azure-functions-templates/dev/Functions.Templates/Template-Manifest/manifest.json` | +| Cache location | `~/.azure-functions-core-tools/cache/manifest.json` + `manifest-meta.json` | +| Cache key | ETag from CDN response | +| TTL | 24 hours (refresh ETag check on expiry) | +| 304 Not Modified | Update TTL timestamp, return cached manifest | +| Network failure | Log warning via `IInteractionService.WriteWarning`; fall back to bundled embedded manifest | +| Trusted-org filter | Strip any template whose `repositoryUrl` owner is not in `{"azure", "azure-samples", "microsoft"}` | +| IaC-only filter | Exclude templates where `language` is `ARM`, `Bicep`, or `Terraform` — these are infrastructure-as-code templates, not function app code. The manifest has 4 such entries (e.g. `iac-flex-consumption-bicep`); they are irrelevant to `func new templates`. | +| Manifest validation | Non-null `templates` array; each entry has `id`, `language`, `resource`, `repositoryUrl`, `folderPath` | + +#### Runtime-to-Language Mapping + +The interactive prompt shows **worker runtimes** (matching `func new` UX), but the manifest uses a `language` field. The mapping between prompt labels, `--language` flag values, and manifest `language` values: + +| Prompt Label | `--language` flag value(s) | Manifest `language` | Notes | +| --- | --- | --- | --- | +| dotnet (isolated worker model) | `dotnet-isolated`, `dotnet`, `csharp` | `CSharp` | Isolated worker model only — in-process is deprecated and not supported | +| Node | `node` | — | Sub-prompt: JavaScript or TypeScript | +| — | `javascript` | `JavaScript` | Direct flag bypasses Node sub-prompt | +| — | `typescript` | `TypeScript` | Direct flag bypasses Node sub-prompt | +| Python | `python` | `Python` | | +| Java | `java` | `Java` | | +| Powershell | `powershell` | `PowerShell` | | + +**Node sub-prompt:** When the user selects "Node" in the interactive runtime prompt, a follow-up prompt asks for JavaScript or TypeScript. When `--language node` is supplied non-interactively, the default is TypeScript (matching the modern v4 programming model). Use `--language javascript` or `--language typescript` to skip the sub-prompt entirely. + +#### `ITemplateFunctionScaffolder` + +```csharp +// src/Func/Templates/ITemplateFunctionScaffolder.cs +internal interface ITemplateFunctionScaffolder +{ + Task ScaffoldAsync( + TemplateEntry template, + string targetDirectory, + CancellationToken cancellationToken = default); +} +``` + +`TemplateFunctionScaffolder` implementation (ported from `fnx/lib/init/scaffold.js`): + +| Strategy | Condition | Detail | +| ---------- | ----------- | -------- | +| git sparse-checkout | `git` available on PATH | `git clone --filter=blob:none --sparse ` then `git sparse-checkout set ` | +| Tree API + raw URLs | No git (subfolder) | One API call to `GET /repos/{owner}/{repo}/git/trees/main?recursive=1` (60 req/hr) to enumerate files, then download each via `raw.githubusercontent.com` (~5,000 req/hr). Tree response capped at 5 MB and cached. | +| GitHub zip API | No git (whole repo) | `GET https://api.github.com/repos///zipball/` → extract and strip `repo-main/` prefix | +| Path traversal prevention | Always | Resolve each extracted path and assert it starts with `targetDirectory + Path.DirectorySeparatorChar` | +| Trusted org | Always | Validate `owner` from `repositoryUrl` before any network call | +| URL scheme | Always | Only `https://github.com/` allowed as repository base | + +--- + +### User Experience Flow + +What the user sees in the terminal across every path. + +```mermaid +flowchart TD + START(["$ func new templates [flags]"]) + START --> FETCH["Fetching template catalog..."] + + FETCH --> DIR_ERR{"Target dir empty?"} + DIR_ERR -- no --> DERR(["Error: Target directory is not empty.
Use an empty directory or a new --path."]) + DIR_ERR -- yes --> LANG + + LANG{"--language
supplied?"} + LANG -- yes --> TRIG + LANG -- "no + --yes" --> LERR(["Error: --language required
cannot auto-detect"]) + LANG -- "no, interactive" --> LPROMPT["Use the up/down arrow keys to select a worker runtime:
──────────────────────────
dotnet (isolated worker model)
Node
Python
Java
Powershell"] + + LPROMPT --> NODECHECK{"Node selected?"} + NODECHECK -- yes --> NODEPROMPT["Use the up/down arrow keys to select a language:
JavaScript
TypeScript"] + NODECHECK -- no --> TRIG + NODEPROMPT --> TRIG + + TRIG{"--template or
--resource set?"} + TRIG -- yes --> NAME + TRIG -- "no, interactive" --> TPROMPT["Use the up/down arrow keys to select a template:
──────────────────────────
HTTP Trigger
Blob Trigger
Timer Trigger
..."] + + TPROMPT --> NAME + + NAME["Resolve selected template"] --> DOWNLOAD + + DOWNLOAD["Downloading http-trigger-python-azd..."] + DOWNLOAD --> WRITE["Writing files..."] + + WRITE --> SUCCESS(["Created http-trigger-python-azd in target-dir"]) +``` + +--- + +### Execution Flow + +```mermaid +flowchart TD + A(["func new templates invoked"]) --> B["Fetch manifest
CDN - ETag cache - bundled fallback"] + B --> C{"--path supplied?"} + C -- yes --> D["Resolve target = --path"] + C -- no --> E["Resolve target = cwd"] + D & E --> F{"Target exists?"} + F -- no --> G["Create directory"] + F -- yes --> EMPTY{"Dir empty?"} + EMPTY -- no --> ERR0(["Error: Target directory is not empty"]) + EMPTY -- yes --> H{"--yes and
no --language?"} + G --> H + H -- yes --> ERR1(["Error: --language required
target is empty, cannot auto-detect"]) + H -- no --> I{"--language supplied?"} + I -- "no, interactive" --> J["Prompt: select language
arrow keys + live type-to-filter"] + I -- yes --> K["Use --language value"] + J --> K + K --> L{"--template supplied?"} + L -- yes --> M["Resolve template directly"] + L -- no --> N{"--resource supplied?"} + N -- yes --> O["Filter by resource type"] + N -- no --> P["Filter templates by language
apply --search if present"] + O & P --> Q{"Interactive?"} + Q -- yes --> R["Prompt: select trigger
arrow keys + live type-to-filter"] + Q -- "no or --yes" --> S["Use first match or default template"] + R --> M + S --> M + M --> Z["Download and write all files"] + Z --> BB(["Created template-id in target-dir"]) +``` + +#### Manifest Cache Flow + +```mermaid +flowchart TD + A(["Fetch manifest"]) --> B{"Local cache exists?"} + B -- no --> F["GET manifest.json from CDN"] + B -- yes --> C{"Cache age under 24h?"} + C -- yes --> DONE(["Return cached manifest"]) + C -- no --> D["GET manifest.json
If-None-Match: stored ETag"] + D --> E{"CDN response"} + E -- "304 Not Modified" --> G["Refresh timestamp
reuse cached body"] + E -- "200 OK new ETag" --> H["Replace cache
store new ETag and body"] + F --> I{"CDN reachable?"} + I -- yes --> H + I -- no --> J{"Bundled fallback available?"} + J -- yes --> K(["Return bundled manifest
warn: catalog may be stale"]) + J -- no --> ERR(["Error: This feature requires a network connection.
Please check your connection and try again."]) + G & H --> DONE +``` + +#### Template Download Strategy + +`folderPath` from the manifest determines what to download. When `folderPath` is `"."` or empty, the entire repo is the template. When it specifies a subfolder (e.g. `templates/python/BlobTrigger`), only that subtree is downloaded. + +```mermaid +flowchart TD + A(["ScaffoldAsync called
repoUrl + folderPath from manifest"]) --> B["Validate owner
trusted orgs: azure, azure-samples, microsoft"] + B --> BFAIL{"Trusted?"} + BFAIL -- no --> ERR1(["Error: untrusted repository owner"]) + BFAIL -- yes --> C["Validate URL scheme
https://github.com/ only"] + C --> CFAIL{"Valid scheme?"} + CFAIL -- no --> ERR2(["Error: only https://github.com/ repos allowed"]) + CFAIL -- yes --> FP{"folderPath is '.' or empty?"} + + FP -- "yes (whole repo)" --> WR{"git available?"} + WR -- yes --> WRG["git clone --depth 1 repoUrl tempDir
move all items except .git to targetDir"] + WR -- no --> WRZ["Download zip: /archive/refs/heads/main.zip
extract, strip repo-branch/ prefix
move contents to targetDir"] + WRG -- fail --> WRZ + WRZ -- fail --> NETERR(["Error: Unable to reach GitHub to download the template.
Please check your network connection and try again."]) + + FP -- "no (subfolder)" --> SF{"git available?"} + SF -- yes --> SFG["git clone --filter=blob:none --sparse --depth 1
git sparse-checkout set folderPath
move tempDir/folderPath/* to targetDir"] + SF -- no --> SFA["Tree API + raw URLs
GET /repos/owner/repo/git/trees/main?recursive=1
filter to folderPath, download each via raw.githubusercontent.com"] + SFG -- fail --> SFA + SFA -- fail --> NETERR + + WRG & WRZ --> G + SFG & SFA --> G + G["For each extracted file:
safePath check — resolved path
must start with targetDir"] + G --> GFAIL{"Path traversal detected?"} + GFAIL -- yes --> ERR3(["Error: path traversal in archive
discard all extracted files"]) + GFAIL -- no --> H(["Write files to target directory"]) +``` + +**Fallback chain**: git → zip/API → error. Each strategy falls back to the next if it fails (git not installed, sparse-checkout unsupported, zip download fails). + +| `folderPath` | Git available | Strategy | What's downloaded | +| --- | --- | --- | --- | +| `"."` or empty | yes | `git clone --depth 1` | Entire repo (minus `.git`) | +| `"."` or empty | no | GitHub zip (`/archive/refs/heads/main.zip`) | Entire repo, strip `repo-main/` prefix | +| `"src/templates/python"` | yes | Sparse checkout | Only `folderPath` subtree | +| `"src/templates/python"` | no | Tree API + raw URLs | 1 API call for tree listing, then raw URL per file (~5,000 req/hr) | + +> **Why Tree API + raw URLs instead of Contents API:** The GitHub Contents API costs 1 API call per file and has a 60 req/hr unauthenticated limit — a template with 20 files would burn a third of the budget. The Tree API enumerates the full repo in a single API call (cached 12h). Individual files are then fetched from `raw.githubusercontent.com` which has a separate, much higher rate limit (~5,000 req/hr). This matches the approach used in [microsoft/mcp#2071](https://github.com/microsoft/mcp/pull/2071). +> +> **Rate limit detection:** GitHub returns HTTP 403 (not 429) when rate-limited. We distinguish rate limiting from permission errors by checking `X-RateLimit-Remaining: 0`. The `X-RateLimit-Reset` header provides the reset timestamp. Tree responses are capped at 5 MB (`MaxTreeSizeBytes`) to prevent OOM. + +--- + +### New Commands + +#### `TemplatesCommand` + +> **Thin-command principle** — `TemplatesCommand` must not contain any manifest, scaffolding, or prompt logic itself. All of that lives in `ITemplateManifestService` and `ITemplateFunctionScaffolder`. This keeps the command re-parentable: it can live under `func new` today and be promoted to `func ` later by changing one DI registration, not the implementation. + +```csharp +// src/Func/Commands/New/TemplatesCommand.cs +internal sealed class TemplatesCommand : FuncCliCommand +{ + // Carried forward from v4 func new — preserve agent/CI compatibility + public Option LanguageOption { get; } = new("--language", "-l") + { + Description = "Worker runtime or language (e.g. python, node, java, dotnet-isolated, javascript, typescript)" + }; + public Option TemplateOption { get; } = new("--template", "-t") + { + Description = "Template id from the manifest (e.g. http-trigger-python-azd) — skips language and trigger selection" + }; + + // New in vnext + public Option PathOption { get; } = new("--path") + { + Description = "Target directory for the new project; accepts absolute or relative path. Created if absent, accepted if it exists and is empty." + }; + // --force deferred to future iteration; v1 requires empty target directory + public Option ResourceOption { get; } = new("--resource", "-r") + { + Description = "Filter by trigger/binding resource (e.g. http, timer, blob, eventhub, servicebus, cosmos, sql, mcp, durable)" + }; + public Option IacOption { get; } = new("--iac") + { + Description = "Filter by IaC type (e.g. bicep, none)" + }; + public Option SearchOption { get; } = new("--search", "-s") + { + Description = "Case-insensitive substring filter applied to trigger names and descriptions before prompting (not semantic/fuzzy search)" + }; + public Option YesOption { get; } = new("--yes", "-y") + { + Description = "Accept all defaults non-interactively. Requires --language (no auto-detect; target folder is empty)." + }; + + public TemplatesCommand( + TemplatesListCommand listCommand, + TemplatesInfoCommand infoCommand, + ITemplateManifestService manifestService, + ITemplateFunctionScaffolder scaffolder, + IInteractionService interaction) + : base("templates", "Browse and scaffold functions from the Azure Functions template catalog.") + { + Subcommands.Add(listCommand); + Subcommands.Add(infoCommand); + // ... store services + } + + protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + // Runtime/language resolution: + // → if --language missing in non-interactive/--yes mode: fail with clear error + // → if --language missing in interactive mode: prompt "Use the up/down arrow keys to select a worker runtime:" + // showing: dotnet (isolated), dotnet (in-process), Node, Python, Java, Powershell + // → if user selects "Node": sub-prompt for JavaScript vs TypeScript + // → map prompt selection to manifest `language` value (see Runtime-to-Language Mapping) + // → if --language supplied directly: map flag value to manifest language + // (e.g. "python" → "Python", "node" → sub-prompt or default TypeScript with --yes) + // Directory resolution: + // target = --path (absolute or relative) if supplied, else cwd + // if target does not exist: create it + // if target exists and is empty: use it as-is + // if target exists and is not empty: error (v1 — --force deferred to future iteration) + // Non-interactive path: --template supplied → resolve by id, scaffold directly + // --template supplied → skip both language and trigger selection + // --yes path: skip trigger prompt; errors if --language not supplied + // Interactive path: prompt via IInteractionService for missing runtime/trigger + // 1. Fetch manifest (verbose logging for cache hit/miss/refresh) + // manifest automatically excludes IaC-only templates (ARM, Bicep, Terraform) + // 2. Prompt worker runtime (if not --language) → IInteractionService.PromptSelectionAsync + // If Node selected → sub-prompt for JavaScript vs TypeScript + // 3. If --template: resolve by id directly; else filter by language + --search, prompt trigger + // In interactive mode: PromptSelectionAsync uses Spectre SearchEnabled=true so + // the user can also type to narrow the trigger list without a flag + // 4. ScaffoldAsync → log completion and exit + } +} +``` + +#### `TemplatesListCommand` + +```csharp +// src/Func/Commands/New/TemplatesListCommand.cs +internal sealed class TemplatesListCommand : FuncCliCommand +{ + public Option LanguageOption { get; } = new("--language", "-l") + { + Description = "Filter by worker runtime or language (e.g. python, node, java, dotnet-isolated)" + }; + public Option ResourceOption { get; } = new("--resource", "-r") + { + Description = "Filter by trigger/binding resource (e.g. http, timer, blob, eventhub, servicebus, cosmos, sql, mcp, durable)" + }; + public Option IacOption { get; } = new("--iac") + { + Description = "Filter by IaC type (e.g. bicep, none)" + }; + public Option SearchOption { get; } = new("--search", "-s") + { + Description = "Case-insensitive substring match against displayName, id, resource, tags, and shortDescription (not semantic/fuzzy)" + }; + + protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + // 1. Fetch manifest (ShowStatusAsync for spinner) + // IaC-only templates (ARM, Bicep, Terraform) already excluded by service + // 2. Map --language flag value to manifest language(s) via Runtime-to-Language Mapping + // "node" → filter to both JavaScript and TypeScript + // 3. Filter by --resource: exact match on resource field (case-insensitive) + // 4. Filter by --iac: exact match on iac field (case-insensitive) + // 5. Filter by --search: case-insensitive substring match on displayName, id, resource, tags, shortDescription + // if zero results after all filters: error "No templates found matching ''..." exit 1 + // 6. Sort by priority (ascending — lower priority value = higher in list) + // 7. Build column set: always include Id; omit Language if --language supplied, omit Resource if --resource supplied, omit IaC if --iac supplied + // 8. WriteTable(columns, rows) + } +} +``` + +#### `TemplatesInfoCommand` + +```csharp +// src/Func/Commands/New/TemplatesInfoCommand.cs +internal sealed class TemplatesInfoCommand : FuncCliCommand +{ + public Argument TemplateIdArgument { get; } = new("id") + { + Description = "Template id from the manifest (e.g. http-trigger-python-azd). Use 'func new templates list' to see available ids." + }; + + protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + // 1. Fetch manifest + // 2. Find template by id (case-insensitive match) + // 3. If not found: error with suggestion to run 'func new templates list' + // 4. Display detailed info: + // - displayName + // - longDescription (or shortDescription fallback) + // - language, resource, iac + // - repositoryUrl + // - whatsIncluded (bulleted list) + } +} +``` + +--- + +### `NewCommand` Change + +`NewCommand` gains `TemplatesCommand` as a constructor-injected subcommand: + +```csharp +// BEFORE +public NewCommand(IWorkloadHintRenderer hintRenderer) : base("new", "...") { ... } + +// AFTER +public NewCommand(IWorkloadHintRenderer hintRenderer, TemplatesCommand templatesCommand) + : base("new", "Create a new function from a template.") +{ + Subcommands.Add(templatesCommand); + // existing options unchanged +} +``` + +--- + +### `BuiltInCommands` Change + +```csharp +// Existing pattern (WorkloadCommand): +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); + +// New (TemplatesCommand, same pattern): +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); // not as FuncCliCommand — it's a subcommand +services.AddSingleton(); +services.AddSingleton(); +// NewCommand's registration already exists; it now also resolves TemplatesCommand via DI +``` + +--- + +### File Layout + +```text +src/Func/ + Commands/ + New/ + TemplatesCommand.cs ← new + TemplatesListCommand.cs ← new + TemplatesInfoCommand.cs ← new + NewCommand.cs ← modified (add TemplatesCommand subcommand) + Templates/ + ITemplateManifestService.cs ← new + TemplateManifestService.cs ← new + ITemplateFunctionScaffolder.cs ← new + TemplateFunctionScaffolder.cs ← new + TemplateManifest.cs ← new (model) + TemplateEntry.cs ← new (model) + ScaffoldResult.cs ← new (model) + Hosting/ + BuiltInCommands.cs ← modified (register new services + commands) + +test/Func.Tests/ + Commands/New/ + TemplatesCommandTests.cs ← new + TemplatesListCommandTests.cs ← new + TemplatesInfoCommandTests.cs ← new + Templates/ + TemplateManifestServiceTests.cs ← new + TemplateFunctionScaffolderTests.cs ← new +``` + +--- + +## `IInteractionService` Prompt Gap + +The current `IInteractionService` in vnext (`src/Func/Console/IInteractionService.cs`) does not yet have a selection-prompt method. `TemplatesCommand`'s interactive path needs one. Two options: + +1. **Extend `IInteractionService`** — add `Task PromptSelectionAsync(string title, IEnumerable<(T value, string label)> options, CancellationToken ct)`. Backed by `Spectre.Console.SelectionPrompt` with `SearchEnabled = true` in `SpectreInteractionService`, which gives the user: + - **Arrow keys** (↑↓) to move through the list + - **Type any character** to live-filter the visible items; backspace to clear + - Both work simultaneously — filter then navigate within the filtered set + - No additional code needed; this is standard Spectre.Console behaviour + + Respects `IsInteractive` (falls back to numbered list in CI/non-TTY). + + **Why this matters vs. v4 `func new`:** The current stable CLI uses raw `Console.ReadKey()` arrow navigation which hangs or crashes in agent, CI, and piped contexts because there is no TTY. The `IInteractionService.IsInteractive` check ensures the new design degrades gracefully: in a non-TTY context the prompt renders as a numbered list that accepts stdin, and when all flags are supplied no prompt is shown at all. + +2. **Use `IAnsiConsole` directly in `TemplatesCommand`** — inject `Spectre.Console.IAnsiConsole` alongside `IInteractionService` as an escape hatch. + +**Preferred: Option 1** — keeps all TUI calls behind `IInteractionService`. This makes `TemplatesCommand` fully testable with a substituted `IInteractionService` (NSubstitute). `SearchEnabled = true` on `SelectionPrompt` is a one-liner in the Spectre implementation and directly serves the in-prompt filter UX. + +> **Note on `--search` vs. in-prompt search:** These are complementary. `--search blob` on `func new templates` pre-filters the list *before* the prompt is shown (useful for scripting or reducing noise when you already know the keyword). The `SearchEnabled` interactive filter lets users type to narrow down after the prompt appears. Both apply the same case-insensitive substring match on template id, trigger name, and description. + +--- + +## Error Messaging + +All user-facing errors are surfaced as `GracefulException` (caught by `Program.Main` and printed cleanly with exit code 1). Internal/unexpected failures propagate as unhandled exceptions with a stack trace. + +| Category | Condition | Error Message | When Shown | +| ---------- | ----------- | --------------- | ------------ | +| **Network — manifest** | CDN returns non-2xx/304 or request times out | `This feature requires a network connection to fetch the template catalog. Please check your connection and try again.` | Any command (`templates`, `list`, `info`) when the CDN is unreachable **and** no cached/bundled manifest is available | +| **Network — manifest (degraded)** | CDN fails but a cached or bundled manifest exists | *(warning, not error)* `Unable to refresh the template catalog — using cached data.` | Same as above, but a stale manifest was found; command continues with stale data | +| **Network — download** | GitHub returns non-2xx, times out, or `git clone` fails with network error | `Unable to reach GitHub to download the template. Please check your network connection and try again.` | `templates` (scaffold) after template selection, when the download step fails | +| **GitHub rate limit** | Tree API returns 403 with `X-RateLimit-Remaining: 0`, or 429 | `GitHub API rate limit exceeded. Rate limit resets at . Try again later.` | `templates` (scaffold) during Tree API call for subfolder templates | +| **Template not found** | `--template ` does not match any entry in the manifest | `Template '' not found. Run 'func new templates list' to see available templates.` | `templates --template ` or `info ` | +| **Language required** | `--yes` supplied without `--language` | `--language is required. Target directory is empty; cannot auto-detect language.` | `templates --yes` (non-interactive mode with no language) | +| **No templates for language** | `--language` value is valid but no manifest templates match | `No templates found for language ''.` | `templates --language ` or `list --language ` | +| **Invalid language** | `--language` value doesn't map to any known runtime | `Unknown language ''. Valid values: dotnet-isolated, node, javascript, typescript, python, java, powershell.` | Any command that accepts `--language` | +| **No search results** | `--search` filter (or combined `--language`/`--resource`/`--iac` filters) produces zero matching templates | `No templates found matching ''. Run 'func new templates list' to see all available templates.` | `templates` and `templates list` after filtering | +| **Directory not empty** | Target directory contains any files or subdirectories | `Target directory '' is not empty. Use an empty directory or a new --path.` | `templates` (scaffold) before download, during directory validation | +| **Untrusted org** | Template's `repositoryUrl` owner is not in the trusted-org allowlist (`azure`, `azure-samples`, `microsoft`) | `Template references an untrusted repository (/). This template cannot be downloaded.` | `templates` (scaffold) before any network call to GitHub | +| **Path traversal** | An extracted file resolves outside the target directory | `Path traversal detected in template archive: . The template may be corrupted or malicious.` | `templates` (scaffold) during file extraction from staging to target | +| **Target is a file** | `--path` points to an existing file (not a directory) | `'' is a file, not a directory. --path must be a directory.` | `templates --path ` | +| **Permission denied** | Cannot write to the target directory | `Permission denied: cannot write to ''. Check directory permissions.` | `templates` (scaffold) when file copy fails with `UnauthorizedAccessException` | +| **Git sparse-checkout failed** | `git` is on PATH but sparse-checkout fails (non-network) | Falls through to Tree API + raw URL fallback; no user-visible error unless that also fails | `templates` (scaffold) — transparent fallback | +| **Manifest parse error** | CDN returns 200 but the body is not valid JSON or is missing required fields | `The template catalog is corrupted or has an unexpected format. Try again later, or file an issue.` | Any command, after manifest fetch | + +### Exception Strategy + +Following the Core Tools vnext error handling convention: + +- **Services** (`TemplateManifestService`, `TemplateFunctionScaffolder`) throw specific framework/domain exceptions at the failure site: + - `HttpRequestException` — network failures + - `FileNotFoundException` / `DirectoryNotFoundException` — missing paths + - `UnauthorizedAccessException` — permission issues + - `InvalidOperationException` — untrusted org in manifest, path traversal, parse errors + - `KeyNotFoundException` — template id not found + +- **Commands** (`TemplatesCommand`, `TemplatesListCommand`, `TemplatesInfoCommand`) are the boundary. Each catches the specific exceptions it expects from its direct service calls and wraps them in `GracefulException(message, isUserError: true)`, preserving the inner exception. The `try` is narrow — one service call per `try` block. + +- **Anything unexpected** is not caught by the command — it surfaces as an unhandled exception with a stack trace (runtime bug). + +--- + +## Incremental: `--env` Flag + +> **Status:** Deferred — not in v1 scope. Tracked as a follow-up after the core `func new templates` command. + +The `--env` flag would automatically set up the runtime environment after scaffolding, so `func start` works without any manual steps. + +**What `--env` does per runtime:** + +| Runtime | Actions | Detects | +| --- | --- | --- | +| Python | `python -m venv .venv`, activate venv, `pip install -r requirements.txt` | `requirements.txt` in template | +| TypeScript | `npm install`, `npm run build` | `package.json` + `tsconfig.json` in template | +| JavaScript | `npm install` | `package.json` in template | +| .NET (isolated) | `dotnet restore` | `*.csproj` in template | +| Java | `mvn clean package` | `pom.xml` in template | +| PowerShell | *(no-op — no dependency install needed)* | — | + +**UX with `--env`:** + +```bash +$ func new templates --language python --template http-trigger-python-azd --env + + Downloading http-trigger-python-azd... + Creating virtual environment (.venv)... + Installing dependencies (pip install -r requirements.txt)... + + Created http-trigger-python-azd in current directory + + Next steps: + 1. func start +``` + +When `--env` succeeds, the post-scaffold banner drops all install/activate steps — the environment is ready, so only `cd` (if `--path` was used) and `func start` remain. If `--env` partially fails (e.g. `pip install` fails), the banner falls back to showing the full manual steps with a warning that automatic setup was incomplete. + +--- + +## Open Questions + +- [ ] **Default language** — `--language` is currently required with no fallback (target dir is empty, nothing to detect from). Should there be a default so repeat users don't have to supply it every time? Options: + - **Detect from existing files** — run `ProjectDetector.DetectStackAndLanguage(targetDir)` on the resolved target before prompting; if detectable files exist (e.g. `requirements.txt`, `package.json`, `*.csproj`), use the detected language as the default; if target is empty, fall through to prompt or error. Already exists at `src/Func/Commands/ProjectDetector.cs`. Becomes more useful when `--force` is added in a future iteration (target dir may have project files). + - **Persist last-used language** — store in `~/.azure-functions-core-tools/config` after first scaffold; use as default on next run + - **Explicit `func config set default-language `** — user opts in deliberately; no implicit magic + - **Keep required** — simplest; interactive prompt is low friction anyway; scripts always supply it + - Consider: does a persisted or detected default interact badly with `--yes` in CI (wrong language silently used)? Detection from files is safer than persistence — it's scoped to the target dir, not global state. +- [ ] **`--search` empty-result behaviour in interactive mode** — when the in-prompt Spectre filter (not the `--search` flag) narrows to zero results, Spectre renders an empty list. Should the command detect this and show a message, or let the user backspace to widen the filter naturally? +- [ ] **`func new templates` vs `func new` interactive fallback** — should bare `func new` (when no workloads installed) redirect to `func new templates` interactive flow, rather than showing a hint? Or keep the hint to encourage workload installation? +- [ ] **`--output json` on `func new templates list`** — useful for tooling. Worth adding now or later? +- [ ] **Top-level command name** — If/when `TemplatesCommand` is promoted, what is the verb? Candidates: `func scaffold`, `func template`, `func create`. Decide before vNext ships so the subcommand name (`templates`) is chosen with the promotion in mind. + +--- + +## Appendix + +### References + +- `fnx init` orchestration: [func-emulate/fnx/lib/init.js](../../../repos/func-emulate/fnx/lib/init.js) +- Manifest fetch + cache: [func-emulate/fnx/lib/init/manifest.js](../../../repos/func-emulate/fnx/lib/init/manifest.js) +- Scaffold (git + zip): [func-emulate/fnx/lib/init/scaffold.js](../../../repos/func-emulate/fnx/lib/init/scaffold.js) +- Interactive prompts: [func-emulate/fnx/lib/init/prompts.js](../../../repos/func-emulate/fnx/lib/init/prompts.js) +- Runtime + trigger constants: [func-emulate/fnx/templates-mcp/src/templates.ts](../../../repos/func-emulate/fnx/templates-mcp/src/templates.ts) +- vnext `NewCommand`: [azure-functions-core-tools/src/Func/Commands/NewCommand.cs](../../../repos/azure-functions-core-tools/src/Func/Commands/NewCommand.cs) +- vnext `WorkloadCommand` (pattern): [azure-functions-core-tools/src/Func/Commands/Workload/WorkloadCommand.cs](../../../repos/azure-functions-core-tools/src/Func/Commands/Workload/WorkloadCommand.cs) +- vnext `IInteractionService`: [azure-functions-core-tools/src/Func/Console/IInteractionService.cs](../../../repos/azure-functions-core-tools/src/Func/Console/IInteractionService.cs) + +### vnext Architecture (What Already Exists) + +The vnext branch has been rebuilt from scratch on System.CommandLine. Key elements relevant to this design: + +| Type | Location | Role | +| --- | --- | --- | +| `FuncCliCommand` | `src/Func/Commands/FuncCliCommand.cs` | Base class for all commands. Subcommands injected via constructor. | +| `IBuiltInCommand` | `src/Func/Hosting/IBuiltInCommand.cs` | Marker interface — names are reserved, registered in `BuiltInCommands` | +| `NewCommand` | `src/Func/Commands/NewCommand.cs` | Skeleton `func new` — stub today, workload-driven long term | +| `IInteractionService` | `src/Func/Console/IInteractionService.cs` | Spectre.Console abstraction. Has `IsInteractive`, `WriteTable`, `ShowStatusAsync`, prompt methods. | +| `BuiltInCommands` | `src/Func/Hosting/BuiltInCommands.cs` | Registers all built-in commands with DI | +| `WorkloadCommand` | `src/Func/Commands/Workload/WorkloadCommand.cs` | Pattern reference — parent command with subcommands injected via constructor | +| `WorkloadListCommand` | `src/Func/Commands/Workload/WorkloadListCommand.cs` | Pattern reference — subcommand using `IInteractionService.WriteTable` | + +**`WorkloadCommand` pattern** (to follow exactly): + +- Parent command receives subcommands via constructor, calls `Subcommands.Add()` +- Each subcommand registered as its own concrete singleton in `BuiltInCommands`, **not** as `FuncCliCommand` (to prevent top-level registration) +- Parent registered as `FuncCliCommand` so it appears at top level + +### fnx init — Source Implementation + +| Component | Location | Role | +| --- | --- | --- | +| **Init orchestration** | `fnx/lib/init.js` | 9-step flow: manifest → prompts → download → scaffold | +| **Manifest fetching** | `fnx/lib/init/manifest.js` | CDN fetch, ETag caching, 24h TTL, bundled fallback | +| **Interactive prompts** | `fnx/lib/init/prompts.js` | Arrow-key selection via readline raw mode; numbered fallback for CI | +| **Scaffolding** | `fnx/lib/init/scaffold.js` | git sparse-checkout or GitHub zip API; `safePath()`, trusted-org filter | +| **Template definitions** | `fnx/templates-mcp/src/templates.ts` | `SUPPORTED_RUNTIMES`, trigger priority list, `TemplateParameter` | + +**`fnx init` flow (condensed):** + +1. Fetch manifest from `https://cdn.functions.azure.com/public/templates-manifest/manifest.json` (ETag-cached; bundled fallback on failure) +2. Prompt: **runtime** (Python / Node.js / .NET / Java / PowerShell) +3. For Node.js: prompt **language** (TypeScript / JavaScript) +4. Filter manifest by runtime → prompt **trigger** (priority-sorted: HTTP, Blob, Timer, Queue, …) +5. Prompt **project name** +6. Download template: `git clone --sparse` (if git available) else GitHub zip API +7. Generate `host.json`, `local.settings.json`, `.gitignore`; print success banner + +**Security primitives to port:** + +- `safePath(targetDir, fileName)` — resolve + prefix check to prevent path traversal +- `filterTrustedTemplates()` — allowlist of `azure`, `azure-samples`, `microsoft` GitHub orgs +- URL scheme validation before any network call From e69ad59a6e6e3f0e0c08364d1e256ad6be70c94e Mon Sep 17 00:00:00 2001 From: manvkaur <67894494+manvkaur@users.noreply.github.com> Date: Mon, 18 May 2026 15:44:20 -0700 Subject: [PATCH 2/7] review comments --- proposed/templates-e2e.md | 70 ++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/proposed/templates-e2e.md b/proposed/templates-e2e.md index 413cd54d5..be1bb84be 100644 --- a/proposed/templates-e2e.md +++ b/proposed/templates-e2e.md @@ -7,8 +7,29 @@ --- +## Command Verb — Decision Required + +> **This decision drives the rest of the document.** The verb chosen here determines the command or subcommand name, class names, CLI surface, and whether it collides with the in-flight templates workload. + +This design proposes **`templates`** as the subcommand verb (`func new templates`). However, there is a naming conflict: a separate templates workload is in flight that provides templating (engine, parameterization, etc.). Using `templates` for this command risks conceptual and CLI-surface collision with that workload. + +| Candidate | Full Command | Subcommands | Pros | Cons | +| --- | --- | --- | --- | --- | +| **`templates`** (proposed) | `func new templates` | `list`, `info` | Familiar term; matches industry use for e2e prod ready apps | Collides with templates workload; | +| **`sample`** | `func new sample` | `list`, `info` | These are sample repos; no overloading of "template" | Might imply "not production-ready" | +| **`quickstart`** | `func quickstart` | `list`, `info` | Clear intent; aligns with Azure Samples naming (`*-quickstart-*-azd`) | Longer to type; top-level only (not under `func new`) | +| **`bootstrap`** | `func new bootstrap` | `list`, `info` | Accurate for "set up a project from scratch" | Less discoverable; not a common Azure CLI verb | +| **`clone`** | `func new clone` | `list`, `info` | Technically accurate (it clones a repo) | Implies git; doesn't convey curation or discovery | + +**Recommendation:** `templates` — this is the industry used term for scaffolding a new project, including production-ready starting points. We will need resolve conflict with the in-flight templates workload and establish distinction. + +> **Impact of verb change:** The verb is used in the subcommand name, class names (`TemplatesCommand` → `SampleCommand`), service names, and all CLI examples throughout this document. The design is otherwise identical regardless of verb chosen. Swapping the verb is a find-and-replace — no architectural changes. + +--- + ## Table of Contents +- [Command Verb — Decision Required](#command-verb--decision-required) - [Problem Statement](#problem-statement) - [Goals / Non-Goals](#goals--non-goals) - [Proposed Design](#proposed-design) @@ -41,15 +62,16 @@ ## Problem Statement -Today's function app templates are scattered across multiple sources — the CLI binary, extension bundle, VS Code extension, and Maven repository — each shipping on its own cadence. Getting a template in front of a developer requires a coordinated release across several of these channels, with a minimum turnaround of 6–8 weeks from merge to broad availability. Templates are also bundled inside the CLI binary itself, meaning every addition or update requires a full CLI release. +In vnext, workload templates solve the per-function scaffolding story: `func new` adds a single function file to an existing project using templates shipped in workload packages. A complementary scenario is **complete-app scaffolding** — downloading a fully runnable function app (function code, host.json, local.settings.json, IaC configuration, and all dependencies) in a single step so that `func start` works immediately. + +Today, complete app templates live in GitHub repos (e.g. Azure-Samples) and developers discover them through docs, portal links, or search. This design brings that discovery and scaffolding workflow into the CLI itself. -This design introduces `func new templates`: a command that downloads **complete, immediately runnable function app templates** directly from GitHub. Templates are discovered via a live manifest hosted on the Azure Functions CDN. Adding a new template is as simple as publishing a GitHub repo and adding an entry to the manifest — no CLI release required. The manifest is versioned independently; the CLI picks up new templates automatically on the next manifest refresh. +This design introduces `func new templates`: a built-in command that downloads **complete, immediately runnable function app templates** directly from GitHub, complementing the workload-driven `func new` experience. Templates are discovered via a live manifest hosted on the Azure Functions CDN. Adding a new template is as simple as publishing a GitHub repo and adding an entry to the manifest — no CLI or workload release required. The manifest is versioned independently; the CLI picks up new templates automatically on the next manifest refresh. -Three pain points this solves: +How this complements workload templates: -- **Release cycle friction** — new templates are available as soon as the manifest is updated, not after a 6–8 week CLI release cycle -- **Maintenance burden** — templates live as standalone GitHub repos, not embedded in the CLI binary; they can be updated, PR'd, and iterated on independently -- **Scaffolding gap in vnext** — the vnext `func new` stub exits with code 1 if no workload is installed; `func new templates` gives developers a working template experience from a clean install, no workload required +- **Decoupled template lifecycle** — app templates live as standalone GitHub repos; they can be added, updated, and iterated on independently of both CLI and workload package releases +- **Built-in discovery** — `func new templates list` and the interactive flow give developers a way to find and filter available app templates without leaving the CLI --- @@ -61,9 +83,9 @@ Three pain points this solves: - Download and create a **complete, runnable function app** from a GitHub template — all function code, config, and dependencies included; `func start` works immediately after scaffolding - CDN-backed template manifest with ETag caching (24h TTL) - `func new templates list` — non-interactive table of available templates, filterable by `--language`, `--resource`, `--iac`, and keyword (`--search`); `--search` is a **case-insensitive substring match** (not semantic/fuzzy) against `id`, `displayName`, `resource`, `tags`, and `shortDescription` -- `func new templates` (bare invocation) — interactive flow: worker runtime → (Node sub-prompt: JS/TS) → trigger → scaffold; the runtime prompt matches the existing `func new` UX (`dotnet (isolated worker model)`, `Node`, `Python`, `Java`, `Powershell`); trigger selection supports incremental keyword filtering; powered by the existing `IInteractionService` / Spectre.Console already in vnext -- **.NET isolated worker model only** — the manifest `CSharp` language maps exclusively to the .NET isolated worker model; the in-process model is not supported and will not be added (it is on a deprecation path) -- **Agent and CI friendly** — all v4 `func new` flags preserved (`--language`, `--template`); `--language` accepts runtime names (`python`, `node`, `java`, `dotnet-isolated`, `javascript`, `typescript`, `powershell`); `--yes` accepts remaining defaults non-interactively but errors clearly if `--language` is not supplied; non-TTY falls back to numbered list +- `func new templates` (bare invocation) — interactive flow: worker runtime → (dotnet sub-prompt: C#/F#, Node sub-prompt: JS/TS) → trigger → scaffold; the runtime prompt matches the existing `func new` UX (`dotnet (isolated worker model)`, `Node`, `Python`, `Java`, `Powershell`); trigger selection supports incremental keyword filtering; powered by the existing `IInteractionService` / Spectre.Console already in vnext +- **.NET isolated worker model only** — the manifest `CSharp` and `FSharp` languages map exclusively to the .NET isolated worker model; the in-process model is not supported and will not be added (it is on a deprecation path). Note: no F# templates exist in the manifest today; the sub-prompt will show F# once templates are available +- **Agent and CI friendly** — all v4 `func new` flags preserved (`--language`, `--template`); `--language` accepts runtime names (`python`, `node`, `java`, `dotnet-isolated`, `csharp`, `fsharp`, `javascript`, `typescript`, `powershell`); `--yes` accepts remaining defaults non-interactively but errors clearly if `--language` is not supplied; non-TTY falls back to numbered list - Template download via git sparse-checkout or GitHub zip API (no bundling in binary) - `--path` to specify target directory (absolute or relative); created if absent, accepted if it exists and is empty — error if it exists and is not empty @@ -75,7 +97,7 @@ Three pain points this solves: - Automatic environment setup (venv, npm install, dotnet restore) — **deferred to `--env` incremental** (see [Incremental: `--env` Flag](#incremental---env-flag)) - File conflict handling and `--force` flag — **deferred to next iteration of this command**; v1 requires target directory to be empty -> **Key difference from v4 `func new`:** v4 adds a single function file into an *existing* project. `func new templates` downloads a complete, runnable function app from a GitHub template — function code, host.json, local.settings.json, .gitignore, and all dependencies — into a target directory. `func start` works immediately. This is why `--language` is required with no auto-detect fallback (target is assumed empty before scaffolding). +> **Why `--language` is required:** Workload-driven `func new` operates on an existing project and can detect the runtime from project files. `func new templates` scaffolds into an empty directory, so there is nothing to detect from — `--language` must be supplied explicitly. --- @@ -111,6 +133,11 @@ $ func new templates Java Powershell + # User picks dotnet → sub-prompt for language: + Use the up/down arrow keys to select a language: + C# + F# # (shown but no templates available yet) + # User picks Node → sub-prompt for language: Use the up/down arrow keys to select a language: JavaScript @@ -306,7 +333,9 @@ The interactive prompt shows **worker runtimes** (matching `func new` UX), but t | Prompt Label | `--language` flag value(s) | Manifest `language` | Notes | | --- | --- | --- | --- | -| dotnet (isolated worker model) | `dotnet-isolated`, `dotnet`, `csharp` | `CSharp` | Isolated worker model only — in-process is deprecated and not supported | +| dotnet (isolated worker model) | `dotnet-isolated`, `dotnet` | — | Sub-prompt: C# or F#; isolated worker model only — in-process is not supported | +| — | `csharp` | `CSharp` | Direct flag bypasses dotnet sub-prompt | +| — | `fsharp` | `FSharp` | Direct flag bypasses dotnet sub-prompt; no templates available yet | | Node | `node` | — | Sub-prompt: JavaScript or TypeScript | | — | `javascript` | `JavaScript` | Direct flag bypasses Node sub-prompt | | — | `typescript` | `TypeScript` | Direct flag bypasses Node sub-prompt | @@ -314,6 +343,8 @@ The interactive prompt shows **worker runtimes** (matching `func new` UX), but t | Java | `java` | `Java` | | | Powershell | `powershell` | `PowerShell` | | +**Dotnet sub-prompt:** When the user selects "dotnet (isolated worker model)" in the interactive runtime prompt, a follow-up prompt asks for C# or F#. When `--language dotnet-isolated` or `--language dotnet` is supplied non-interactively, the default is C#. Use `--language csharp` or `--language fsharp` to skip the sub-prompt entirely. Note: no F# templates exist in the manifest today; selecting F# will show an empty list until templates are published. + **Node sub-prompt:** When the user selects "Node" in the interactive runtime prompt, a follow-up prompt asks for JavaScript or TypeScript. When `--language node` is supplied non-interactively, the default is TypeScript (matching the modern v4 programming model). Use `--language javascript` or `--language typescript` to skip the sub-prompt entirely. #### `ITemplateFunctionScaffolder` @@ -360,7 +391,12 @@ flowchart TD LANG -- "no + --yes" --> LERR(["Error: --language required
cannot auto-detect"]) LANG -- "no, interactive" --> LPROMPT["Use the up/down arrow keys to select a worker runtime:
──────────────────────────
dotnet (isolated worker model)
Node
Python
Java
Powershell"] - LPROMPT --> NODECHECK{"Node selected?"} + LPROMPT --> DOTNETCHECK{"dotnet selected?"} + DOTNETCHECK -- yes --> DOTNETPROMPT["Use the up/down arrow keys to select a language:
C#
F#"] + DOTNETCHECK -- no --> NODECHECK + DOTNETPROMPT --> TRIG + + NODECHECK{"Node selected?"} NODECHECK -- yes --> NODEPROMPT["Use the up/down arrow keys to select a language:
JavaScript
TypeScript"] NODECHECK -- no --> TRIG NODEPROMPT --> TRIG @@ -496,7 +532,7 @@ internal sealed class TemplatesCommand : FuncCliCommand // Carried forward from v4 func new — preserve agent/CI compatibility public Option LanguageOption { get; } = new("--language", "-l") { - Description = "Worker runtime or language (e.g. python, node, java, dotnet-isolated, javascript, typescript)" + Description = "Worker runtime or language (e.g. python, node, java, dotnet-isolated, csharp, fsharp, javascript, typescript)" }; public Option TemplateOption { get; } = new("--template", "-t") { @@ -545,10 +581,12 @@ internal sealed class TemplatesCommand : FuncCliCommand // → if --language missing in non-interactive/--yes mode: fail with clear error // → if --language missing in interactive mode: prompt "Use the up/down arrow keys to select a worker runtime:" // showing: dotnet (isolated), dotnet (in-process), Node, Python, Java, Powershell + // → if user selects "dotnet (isolated)": sub-prompt for C# vs F# // → if user selects "Node": sub-prompt for JavaScript vs TypeScript // → map prompt selection to manifest `language` value (see Runtime-to-Language Mapping) // → if --language supplied directly: map flag value to manifest language - // (e.g. "python" → "Python", "node" → sub-prompt or default TypeScript with --yes) + // (e.g. "python" → "Python", "dotnet-isolated" → sub-prompt or default C# with --yes, + // "node" → sub-prompt or default TypeScript with --yes) // Directory resolution: // target = --path (absolute or relative) if supplied, else cwd // if target does not exist: create it @@ -561,6 +599,7 @@ internal sealed class TemplatesCommand : FuncCliCommand // 1. Fetch manifest (verbose logging for cache hit/miss/refresh) // manifest automatically excludes IaC-only templates (ARM, Bicep, Terraform) // 2. Prompt worker runtime (if not --language) → IInteractionService.PromptSelectionAsync + // If dotnet selected → sub-prompt for C# vs F# // If Node selected → sub-prompt for JavaScript vs TypeScript // 3. If --template: resolve by id directly; else filter by language + --search, prompt trigger // In interactive mode: PromptSelectionAsync uses Spectre SearchEnabled=true so @@ -819,7 +858,6 @@ When `--env` succeeds, the post-scaffold banner drops all install/activate steps - [ ] **`--search` empty-result behaviour in interactive mode** — when the in-prompt Spectre filter (not the `--search` flag) narrows to zero results, Spectre renders an empty list. Should the command detect this and show a message, or let the user backspace to widen the filter naturally? - [ ] **`func new templates` vs `func new` interactive fallback** — should bare `func new` (when no workloads installed) redirect to `func new templates` interactive flow, rather than showing a hint? Or keep the hint to encourage workload installation? - [ ] **`--output json` on `func new templates list`** — useful for tooling. Worth adding now or later? -- [ ] **Top-level command name** — If/when `TemplatesCommand` is promoted, what is the verb? Candidates: `func scaffold`, `func template`, `func create`. Decide before vNext ships so the subcommand name (`templates`) is chosen with the promotion in mind. --- @@ -844,7 +882,7 @@ The vnext branch has been rebuilt from scratch on System.CommandLine. Key elemen | --- | --- | --- | | `FuncCliCommand` | `src/Func/Commands/FuncCliCommand.cs` | Base class for all commands. Subcommands injected via constructor. | | `IBuiltInCommand` | `src/Func/Hosting/IBuiltInCommand.cs` | Marker interface — names are reserved, registered in `BuiltInCommands` | -| `NewCommand` | `src/Func/Commands/NewCommand.cs` | Skeleton `func new` — stub today, workload-driven long term | +| `NewCommand` | `src/Func/Commands/NewCommand.cs` | `func new` — delegates to workloads for per-function scaffolding | | `IInteractionService` | `src/Func/Console/IInteractionService.cs` | Spectre.Console abstraction. Has `IsInteractive`, `WriteTable`, `ShowStatusAsync`, prompt methods. | | `BuiltInCommands` | `src/Func/Hosting/BuiltInCommands.cs` | Registers all built-in commands with DI | | `WorkloadCommand` | `src/Func/Commands/Workload/WorkloadCommand.cs` | Pattern reference — parent command with subcommands injected via constructor | From 260c0cc51721e4449fb526a9174f7bb487631ea2 Mon Sep 17 00:00:00 2001 From: manvkaur <67894494+manvkaur@users.noreply.github.com> Date: Mon, 25 May 2026 18:41:25 -0700 Subject: [PATCH 3/7] proposal: address PR review feedback - Rename user-facing .NET label (drop 'isolated worker model') - Simplify --language flag: dotnet (not dotnet-isolated) - Remove --env section (out of scope, belongs in func start) - Add gitRef as required manifest field (no default-branch assumption) - Add Release & Compliance Model section (staging CDN, PR approval, FUNC_TEMPLATE_MANIFEST_URL override) - Add --fetch git|http flag with graceful fallback - Add Git Process Requirements (IGitRunner, timeout, cancellation, non-interactive, argument-array) - Align home directory with cli-profiles spec (~/.azure-functions//) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- proposed/templates-e2e.md | 117 ++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/proposed/templates-e2e.md b/proposed/templates-e2e.md index be1bb84be..1f4bf3e09 100644 --- a/proposed/templates-e2e.md +++ b/proposed/templates-e2e.md @@ -51,7 +51,6 @@ This design proposes **`templates`** as the subcommand verb (`func new templates - [File Layout](#file-layout) - [IInteractionService Prompt Gap](#iinteractionservice-prompt-gap) - [Error Messaging](#error-messaging) -- [Incremental: `--env` Flag](#incremental---env-flag) - [Open Questions](#open-questions) - [Appendix](#appendix) - [References](#references) @@ -62,7 +61,7 @@ This design proposes **`templates`** as the subcommand verb (`func new templates ## Problem Statement -In vnext, workload templates solve the per-function scaffolding story: `func new` adds a single function file to an existing project using templates shipped in workload packages. A complementary scenario is **complete-app scaffolding** — downloading a fully runnable function app (function code, host.json, local.settings.json, IaC configuration, and all dependencies) in a single step so that `func start` works immediately. +In vnext, workload templates solve the per-function scaffolding story: `func new` adds a single function file to an existing project using templates shipped in workload packages. A complementary scenario is **complete-app scaffolding** — downloading a fully runnable function app (function code, host.json, local.settings.json, IaC configuration, and all dependencies) in a single step. Runtime-specific setup (venv, npm install, dotnet restore) is handled separately by the developer or by `func start`. Today, complete app templates live in GitHub repos (e.g. Azure-Samples) and developers discover them through docs, portal links, or search. This design brings that discovery and scaffolding workflow into the CLI itself. @@ -80,12 +79,12 @@ How this complements workload templates: ### Goals - Add `func new templates` as a built-in subcommand of `NewCommand` on the vnext branch -- Download and create a **complete, runnable function app** from a GitHub template — all function code, config, and dependencies included; `func start` works immediately after scaffolding +- Download and create a **complete function app** from a GitHub template — all function code, config, and dependencies included - CDN-backed template manifest with ETag caching (24h TTL) - `func new templates list` — non-interactive table of available templates, filterable by `--language`, `--resource`, `--iac`, and keyword (`--search`); `--search` is a **case-insensitive substring match** (not semantic/fuzzy) against `id`, `displayName`, `resource`, `tags`, and `shortDescription` -- `func new templates` (bare invocation) — interactive flow: worker runtime → (dotnet sub-prompt: C#/F#, Node sub-prompt: JS/TS) → trigger → scaffold; the runtime prompt matches the existing `func new` UX (`dotnet (isolated worker model)`, `Node`, `Python`, `Java`, `Powershell`); trigger selection supports incremental keyword filtering; powered by the existing `IInteractionService` / Spectre.Console already in vnext +- `func new templates` (bare invocation) — interactive flow: worker runtime → (dotnet sub-prompt: C#/F#, Node sub-prompt: JS/TS) → trigger → scaffold; the runtime prompt matches the existing `func new` UX (`.NET`, `Node`, `Python`, `Java`, `Powershell`); trigger selection supports incremental keyword filtering; powered by the existing `IInteractionService` / Spectre.Console already in vnext - **.NET isolated worker model only** — the manifest `CSharp` and `FSharp` languages map exclusively to the .NET isolated worker model; the in-process model is not supported and will not be added (it is on a deprecation path). Note: no F# templates exist in the manifest today; the sub-prompt will show F# once templates are available -- **Agent and CI friendly** — all v4 `func new` flags preserved (`--language`, `--template`); `--language` accepts runtime names (`python`, `node`, `java`, `dotnet-isolated`, `csharp`, `fsharp`, `javascript`, `typescript`, `powershell`); `--yes` accepts remaining defaults non-interactively but errors clearly if `--language` is not supplied; non-TTY falls back to numbered list +- **Agent and CI friendly** — all v4 `func new` flags preserved (`--language`, `--template`); `--language` accepts runtime names (`python`, `node`, `java`, `dotnet`, `csharp`, `fsharp`, `javascript`, `typescript`, `powershell`); `--yes` accepts remaining defaults non-interactively but errors clearly if `--language` is not supplied; non-TTY falls back to numbered list - Template download via git sparse-checkout or GitHub zip API (no bundling in binary) - `--path` to specify target directory (absolute or relative); created if absent, accepted if it exists and is empty — error if it exists and is not empty @@ -94,7 +93,7 @@ How this complements workload templates: - Adding a single function file to an existing project (that is v4 `func new` behaviour — out of scope here) - Replacing or changing `func init` (separate command, workload-driven) - Changing the workload model or `IProjectInitializer` interface -- Automatic environment setup (venv, npm install, dotnet restore) — **deferred to `--env` incremental** (see [Incremental: `--env` Flag](#incremental---env-flag)) +- Automatic environment setup (venv, npm install, dotnet restore) — out of scope; belongs in `func start` workflow - File conflict handling and `--force` flag — **deferred to next iteration of this command**; v1 requires target directory to be empty > **Why `--language` is required:** Workload-driven `func new` operates on an existing project and can detect the runtime from project files. `func new templates` scaffolds into an empty directory, so there is nothing to detect from — `--language` must be supplied explicitly. @@ -127,7 +126,7 @@ func new # NewCommand (existing) $ func new templates Use the up/down arrow keys to select a worker runtime: - dotnet (isolated worker model) + .NET Node Python Java @@ -318,14 +317,29 @@ internal interface ITemplateManifestService | ----------- | -------- | | Primary URL | `https://cdn.functions.azure.com/public/templates-manifest/manifest.json` | | Backup URL | `https://raw.githubusercontent.com/Azure/azure-functions-templates/dev/Functions.Templates/Template-Manifest/manifest.json` | -| Cache location | `~/.azure-functions-core-tools/cache/manifest.json` + `manifest-meta.json` | +| Cache location | `~/.azure-functions//manifest.json` + `/manifest-meta.json` (where `` matches the chosen command name, e.g. `quickstart`) | | Cache key | ETag from CDN response | | TTL | 24 hours (refresh ETag check on expiry) | | 304 Not Modified | Update TTL timestamp, return cached manifest | | Network failure | Log warning via `IInteractionService.WriteWarning`; fall back to bundled embedded manifest | | Trusted-org filter | Strip any template whose `repositoryUrl` owner is not in `{"azure", "azure-samples", "microsoft"}` | | IaC-only filter | Exclude templates where `language` is `ARM`, `Bicep`, or `Terraform` — these are infrastructure-as-code templates, not function app code. The manifest has 4 such entries (e.g. `iac-flex-consumption-bicep`); they are irrelevant to `func new templates`. | -| Manifest validation | Non-null `templates` array; each entry has `id`, `language`, `resource`, `repositoryUrl`, `folderPath` | +| Manifest validation | Non-null `templates` array; each entry has `id`, `language`, `resource`, `repositoryUrl`, `folderPath`, `gitRef` | +| `gitRef` resolution | Each template entry **must** include `"gitRef": ""`. The CLI uses `gitRef` as the checkout/download target. This ensures templates are pinned to an immutable, reviewed release — no implicit default-branch resolution | +| URL override | `FUNC_TEMPLATE_MANIFEST_URL` environment variable overrides the primary URL. Used for testing against staging CDN or local dev manifests. Same schema expected | + +#### Release & Compliance Model + +The manifest source of truth lives in the [`Azure/azure-functions-templates`](https://github.com/Azure/azure-functions-templates) repository under `Functions.Templates/Template-Manifest/`. + +| Aspect | Detail | +| --- | --- | +| Source repo | `Azure/azure-functions-templates` — manifest changes require a PR and team approval | +| Immutability | Every template entry pins a `gitRef` (tag or commit SHA). No branch references allowed — content is frozen at a reviewed point-in-time | +| Staging | Staging CDN at `https://cdn-staging.functions.azure.com/staging/templates-manifest/manifest.json`. Used for pre-release validation. CLI can target it via `FUNC_TEMPLATE_MANIFEST_URL` | +| Production | Production CDN at `https://cdn.functions.azure.com/public/templates-manifest/manifest.json`. Updated only after staging validation passes | +| Promotion flow | PR merged → CI validates schema + smoke-tests each template's `gitRef` is resolvable → published to staging → manual promotion to production | +| Testing locally | Set `FUNC_TEMPLATE_MANIFEST_URL=file:///path/to/local/manifest.json` or point to staging CDN, schema must match | #### Runtime-to-Language Mapping @@ -333,7 +347,7 @@ The interactive prompt shows **worker runtimes** (matching `func new` UX), but t | Prompt Label | `--language` flag value(s) | Manifest `language` | Notes | | --- | --- | --- | --- | -| dotnet (isolated worker model) | `dotnet-isolated`, `dotnet` | — | Sub-prompt: C# or F#; isolated worker model only — in-process is not supported | +| .NET | `dotnet` | `csharp`, `fsharp` | Sub-prompt: C# or F#. Always isolated worker model — in-process is not supported in v5 | | — | `csharp` | `CSharp` | Direct flag bypasses dotnet sub-prompt | | — | `fsharp` | `FSharp` | Direct flag bypasses dotnet sub-prompt; no templates available yet | | Node | `node` | — | Sub-prompt: JavaScript or TypeScript | @@ -343,7 +357,7 @@ The interactive prompt shows **worker runtimes** (matching `func new` UX), but t | Java | `java` | `Java` | | | Powershell | `powershell` | `PowerShell` | | -**Dotnet sub-prompt:** When the user selects "dotnet (isolated worker model)" in the interactive runtime prompt, a follow-up prompt asks for C# or F#. When `--language dotnet-isolated` or `--language dotnet` is supplied non-interactively, the default is C#. Use `--language csharp` or `--language fsharp` to skip the sub-prompt entirely. Note: no F# templates exist in the manifest today; selecting F# will show an empty list until templates are published. +**Dotnet sub-prompt:** When the user selects ".NET" in the interactive runtime prompt, a follow-up prompt asks for C# or F#. When `--language dotnet` is supplied non-interactively, the default is C#. Use `--language csharp` or `--language fsharp` to skip the sub-prompt entirely. Note: no F# templates exist in the manifest today; selecting F# will show an empty list until templates are published. **Node sub-prompt:** When the user selects "Node" in the interactive runtime prompt, a follow-up prompt asks for JavaScript or TypeScript. When `--language node` is supplied non-interactively, the default is TypeScript (matching the modern v4 programming model). Use `--language javascript` or `--language typescript` to skip the sub-prompt entirely. @@ -362,15 +376,40 @@ internal interface ITemplateFunctionScaffolder `TemplateFunctionScaffolder` implementation (ported from `fnx/lib/init/scaffold.js`): +#### Download Strategy (`--fetch git|http`) + +| Value | Behaviour | Best for | +| --- | --- | --- | +| `git` **(default)** | Uses git sparse-checkout. If `git` is not on PATH, falls back to `http` with a warning: `git not found on PATH — using HTTP to download template.` | Developers with git on PATH (vast majority) | +| `http` | Uses GitHub REST API (zip for whole-repo, tree API + raw downloads for subfolders) | Environments without git, CI containers, air-gapped with proxy | + +The `--fetch` flag allows explicit override regardless of git availability. + +#### Download Details + | Strategy | Condition | Detail | | ---------- | ----------- | -------- | -| git sparse-checkout | `git` available on PATH | `git clone --filter=blob:none --sparse ` then `git sparse-checkout set ` | -| Tree API + raw URLs | No git (subfolder) | One API call to `GET /repos/{owner}/{repo}/git/trees/main?recursive=1` (60 req/hr) to enumerate files, then download each via `raw.githubusercontent.com` (~5,000 req/hr). Tree response capped at 5 MB and cached. | -| GitHub zip API | No git (whole repo) | `GET https://api.github.com/repos///zipball/` → extract and strip `repo-main/` prefix | +| git sparse-checkout | `--fetch git` + `folderPath` is subfolder | `git clone --filter=blob:none --sparse --branch ` then `git sparse-checkout set ` | +| git clone (whole repo) | `--fetch git` + `folderPath` is `"."` | `git clone --depth 1 --branch ` | +| Tree API + raw URLs | `--fetch http` + `folderPath` is subfolder | `GET /repos/{owner}/{repo}/git/trees/?recursive=1` to enumerate, then download each file via `raw.githubusercontent.com////`. Rate limit: 60 tree req/hr (unauthenticated), ~5,000 raw req/hr | +| GitHub zip API | `--fetch http` + `folderPath` is `"."` | `GET https://api.github.com/repos///zipball/` → extract and strip root prefix | | Path traversal prevention | Always | Resolve each extracted path and assert it starts with `targetDirectory + Path.DirectorySeparatorChar` | | Trusted org | Always | Validate `owner` from `repositoryUrl` before any network call | | URL scheme | Always | Only `https://github.com/` allowed as repository base | +#### Git Process Requirements + +All git child-process invocations must satisfy: + +| Requirement | Implementation | +| --- | --- | +| **Process abstraction** | Wrap behind `IGitRunner` interface — no direct `Process.Start` in business logic. Testable via `FakeGitRunner` in tests | +| **Argument-array invocation** | Pass arguments via `ProcessStartInfo.ArgumentList` (string array), never as a shell-concatenated string. Prevents injection if a URL or path contains special characters | +| **Non-interactive** | Set `GIT_TERMINAL_PROMPT=0` environment variable on the child process — git must never prompt for credentials. Private/inaccessible repos fail cleanly with an error message | +| **Timeout** | Configurable timeout (default 120s). Kill the process and report `Template download timed out. Try again or use --fetch http.` | +| **Cancellation** | `CancellationToken` from the command handler kills the git child process on Ctrl+C. Use `Process.Kill(entireProcessTree: true)` | +| **Temp directory cleanup** | Clone into a temp directory; move files to target on success. On failure or cancellation, delete the temp directory in a `finally` block | + --- ### User Experience Flow @@ -389,7 +428,7 @@ flowchart TD LANG{"--language
supplied?"} LANG -- yes --> TRIG LANG -- "no + --yes" --> LERR(["Error: --language required
cannot auto-detect"]) - LANG -- "no, interactive" --> LPROMPT["Use the up/down arrow keys to select a worker runtime:
──────────────────────────
dotnet (isolated worker model)
Node
Python
Java
Powershell"] + LANG -- "no, interactive" --> LPROMPT["Use the up/down arrow keys to select a worker runtime:
──────────────────────────
.NET
Node
Python
Java
Powershell"] LPROMPT --> DOTNETCHECK{"dotnet selected?"} DOTNETCHECK -- yes --> DOTNETPROMPT["Use the up/down arrow keys to select a language:
C#
F#"] @@ -532,7 +571,7 @@ internal sealed class TemplatesCommand : FuncCliCommand // Carried forward from v4 func new — preserve agent/CI compatibility public Option LanguageOption { get; } = new("--language", "-l") { - Description = "Worker runtime or language (e.g. python, node, java, dotnet-isolated, csharp, fsharp, javascript, typescript)" + Description = "Worker runtime or language (e.g. python, node, java, dotnet, csharp, fsharp, javascript, typescript)" }; public Option TemplateOption { get; } = new("--template", "-t") { @@ -580,12 +619,12 @@ internal sealed class TemplatesCommand : FuncCliCommand // Runtime/language resolution: // → if --language missing in non-interactive/--yes mode: fail with clear error // → if --language missing in interactive mode: prompt "Use the up/down arrow keys to select a worker runtime:" - // showing: dotnet (isolated), dotnet (in-process), Node, Python, Java, Powershell - // → if user selects "dotnet (isolated)": sub-prompt for C# vs F# + // showing: .NET, Node, Python, Java, Powershell + // → if user selects ".NET": sub-prompt for C# vs F# // → if user selects "Node": sub-prompt for JavaScript vs TypeScript // → map prompt selection to manifest `language` value (see Runtime-to-Language Mapping) // → if --language supplied directly: map flag value to manifest language - // (e.g. "python" → "Python", "dotnet-isolated" → sub-prompt or default C# with --yes, + // (e.g. "python" → "Python", "dotnet" → sub-prompt or default C# with --yes, // "node" → sub-prompt or default TypeScript with --yes) // Directory resolution: // target = --path (absolute or relative) if supplied, else cwd @@ -617,7 +656,7 @@ internal sealed class TemplatesListCommand : FuncCliCommand { public Option LanguageOption { get; } = new("--language", "-l") { - Description = "Filter by worker runtime or language (e.g. python, node, java, dotnet-isolated)" + Description = "Filter by worker runtime or language (e.g. python, node, java, dotnet)" }; public Option ResourceOption { get; } = new("--resource", "-r") { @@ -784,7 +823,7 @@ All user-facing errors are surfaced as `GracefulException` (caught by `Program.M | **Template not found** | `--template ` does not match any entry in the manifest | `Template '' not found. Run 'func new templates list' to see available templates.` | `templates --template ` or `info ` | | **Language required** | `--yes` supplied without `--language` | `--language is required. Target directory is empty; cannot auto-detect language.` | `templates --yes` (non-interactive mode with no language) | | **No templates for language** | `--language` value is valid but no manifest templates match | `No templates found for language ''.` | `templates --language ` or `list --language ` | -| **Invalid language** | `--language` value doesn't map to any known runtime | `Unknown language ''. Valid values: dotnet-isolated, node, javascript, typescript, python, java, powershell.` | Any command that accepts `--language` | +| **Invalid language** | `--language` value doesn't map to any known runtime | `Unknown language ''. Valid values: dotnet, node, javascript, typescript, python, java, powershell.` | Any command that accepts `--language` | | **No search results** | `--search` filter (or combined `--language`/`--resource`/`--iac` filters) produces zero matching templates | `No templates found matching ''. Run 'func new templates list' to see all available templates.` | `templates` and `templates list` after filtering | | **Directory not empty** | Target directory contains any files or subdirectories | `Target directory '' is not empty. Use an empty directory or a new --path.` | `templates` (scaffold) before download, during directory validation | | **Untrusted org** | Template's `repositoryUrl` owner is not in the trusted-org allowlist (`azure`, `azure-samples`, `microsoft`) | `Template references an untrusted repository (/). This template cannot be downloaded.` | `templates` (scaffold) before any network call to GitHub | @@ -811,47 +850,13 @@ Following the Core Tools vnext error handling convention: --- -## Incremental: `--env` Flag - -> **Status:** Deferred — not in v1 scope. Tracked as a follow-up after the core `func new templates` command. - -The `--env` flag would automatically set up the runtime environment after scaffolding, so `func start` works without any manual steps. - -**What `--env` does per runtime:** - -| Runtime | Actions | Detects | -| --- | --- | --- | -| Python | `python -m venv .venv`, activate venv, `pip install -r requirements.txt` | `requirements.txt` in template | -| TypeScript | `npm install`, `npm run build` | `package.json` + `tsconfig.json` in template | -| JavaScript | `npm install` | `package.json` in template | -| .NET (isolated) | `dotnet restore` | `*.csproj` in template | -| Java | `mvn clean package` | `pom.xml` in template | -| PowerShell | *(no-op — no dependency install needed)* | — | - -**UX with `--env`:** - -```bash -$ func new templates --language python --template http-trigger-python-azd --env - - Downloading http-trigger-python-azd... - Creating virtual environment (.venv)... - Installing dependencies (pip install -r requirements.txt)... - - Created http-trigger-python-azd in current directory - - Next steps: - 1. func start -``` - -When `--env` succeeds, the post-scaffold banner drops all install/activate steps — the environment is ready, so only `cd` (if `--path` was used) and `func start` remain. If `--env` partially fails (e.g. `pip install` fails), the banner falls back to showing the full manual steps with a warning that automatic setup was incomplete. - --- ## Open Questions - [ ] **Default language** — `--language` is currently required with no fallback (target dir is empty, nothing to detect from). Should there be a default so repeat users don't have to supply it every time? Options: - **Detect from existing files** — run `ProjectDetector.DetectStackAndLanguage(targetDir)` on the resolved target before prompting; if detectable files exist (e.g. `requirements.txt`, `package.json`, `*.csproj`), use the detected language as the default; if target is empty, fall through to prompt or error. Already exists at `src/Func/Commands/ProjectDetector.cs`. Becomes more useful when `--force` is added in a future iteration (target dir may have project files). - - **Persist last-used language** — store in `~/.azure-functions-core-tools/config` after first scaffold; use as default on next run + - **Persist last-used language** — store in `~/.azure-functions//preferences.json` after first scaffold; use as default on next run - **Explicit `func config set default-language `** — user opts in deliberately; no implicit magic - **Keep required** — simplest; interactive prompt is low friction anyway; scripts always supply it - Consider: does a persisted or detected default interact badly with `--yes` in CI (wrong language silently used)? Detection from files is safer than persistence — it's scoped to the target dir, not global state. From 6bc93793ffd51d6aea10e136a0c14b3e5c01f71f Mon Sep 17 00:00:00 2001 From: manvkaur <67894494+manvkaur@users.noreply.github.com> Date: Mon, 25 May 2026 18:50:10 -0700 Subject: [PATCH 4/7] chore: add .gitignore with out/ exclusion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 1 + proposed/templates-e2e.md | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..89f9ac04a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/proposed/templates-e2e.md b/proposed/templates-e2e.md index 1f4bf3e09..82a8813e3 100644 --- a/proposed/templates-e2e.md +++ b/proposed/templates-e2e.md @@ -7,37 +7,37 @@ --- -## Command Verb — Decision Required +## Command Verb — Under Discussion -> **This decision drives the rest of the document.** The verb chosen here determines the command or subcommand name, class names, CLI surface, and whether it collides with the in-flight templates workload. +> **This decision drives the rest of the document.** The verb chosen here determines the command name, class names, CLI surface, and whether it collides with the in-flight templates workload. -This design proposes **`templates`** as the subcommand verb (`func new templates`). However, there is a naming conflict: a separate templates workload is in flight that provides templating (engine, parameterization, etc.). Using `templates` for this command risks conceptual and CLI-surface collision with that workload. +Development is proceeding with **`quickstart`** as the working name. This is a **top-level command** (`func quickstart`) delivered as a workload — not a subcommand of `func new`. The final verb is still under discussion. | Candidate | Full Command | Subcommands | Pros | Cons | | --- | --- | --- | --- | --- | -| **`templates`** (proposed) | `func new templates` | `list`, `info` | Familiar term; matches industry use for e2e prod ready apps | Collides with templates workload; | -| **`sample`** | `func new sample` | `list`, `info` | These are sample repos; no overloading of "template" | Might imply "not production-ready" | -| **`quickstart`** | `func quickstart` | `list`, `info` | Clear intent; aligns with Azure Samples naming (`*-quickstart-*-azd`) | Longer to type; top-level only (not under `func new`) | -| **`bootstrap`** | `func new bootstrap` | `list`, `info` | Accurate for "set up a project from scratch" | Less discoverable; not a common Azure CLI verb | -| **`clone`** | `func new clone` | `list`, `info` | Technically accurate (it clones a repo) | Implies git; doesn't convey curation or discovery | +| **`quickstart`** (working name) | `func quickstart` | `list`, `info` | Clear intent; aligns with Azure Samples naming (`*-quickstart-*-azd`); top-level; no collision with templates workload | Longer to type; may imply "beginner-only" and limit room for advanced samples | +| **`templates`** | `func templates` | `list`, `info` | Industry standard (Vercel, bolt.new, AZD Templates, AI Template Gallery all use "templates"); VS Code extension already calls it "Template Gallery"; doesn't limit scope to quickstarts | Collides conceptually with in-flight templates workload that provides parameterized per-function scaffolding | +| **`sample`** | `func sample` | `list`, `info` | These are sample repos; no overloading of "template" | Might imply "not production-ready" | +| **`bootstrap`** | `func bootstrap` | `list`, `info` | Accurate for "set up a project from scratch" | Less discoverable; not a common Azure CLI verb | -**Recommendation:** `templates` — this is the industry used term for scaffolding a new project, including production-ready starting points. We will need resolve conflict with the in-flight templates workload and establish distinction. - -> **Impact of verb change:** The verb is used in the subcommand name, class names (`TemplatesCommand` → `SampleCommand`), service names, and all CLI examples throughout this document. The design is otherwise identical regardless of verb chosen. Swapping the verb is a find-and-replace — no architectural changes. +> **Impact of verb change:** The verb is used in the command name, class names (`QuickstartCommand` → `TemplatesCommand`), service names, and all CLI examples throughout this document. The design is otherwise identical regardless of verb chosen. Swapping the verb is a find-and-replace — no architectural changes. --- ## Table of Contents -- [Command Verb — Decision Required](#command-verb--decision-required) +- [Command Verb — Under Discussion](#command-verb--under-discussion) - [Problem Statement](#problem-statement) - [Goals / Non-Goals](#goals--non-goals) - [Proposed Design](#proposed-design) - [Command Tree](#command-tree) - [New Services](#new-services) - [ITemplateManifestService](#itemplatemanifestservice) + - [Release & Compliance Model](#release--compliance-model) - [Runtime-to-Language Mapping](#runtime-to-language-mapping) - [ITemplateFunctionScaffolder](#itemplatefunctionscaffolder) + - [Download Strategy](#download-strategy---fetch-githttp) + - [Git Process Requirements](#git-process-requirements) - [User Experience Flow](#user-experience-flow) - [Execution Flow](#execution-flow) - [Manifest Cache Flow](#manifest-cache-flow) From 0bd194dee4c2781619587d18fc7466aa4a984a58 Mon Sep 17 00:00:00 2001 From: manvkaur <67894494+manvkaur@users.noreply.github.com> Date: Tue, 26 May 2026 00:46:17 -0700 Subject: [PATCH 5/7] Remove --yes option from design doc No other CLI command has a --yes flag. Non-interactive use is covered by --template (skips all prompts) and non-TTY detection (errors if --language not supplied). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- proposed/templates-e2e.md | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/proposed/templates-e2e.md b/proposed/templates-e2e.md index 82a8813e3..030a9221f 100644 --- a/proposed/templates-e2e.md +++ b/proposed/templates-e2e.md @@ -84,7 +84,7 @@ How this complements workload templates: - `func new templates list` — non-interactive table of available templates, filterable by `--language`, `--resource`, `--iac`, and keyword (`--search`); `--search` is a **case-insensitive substring match** (not semantic/fuzzy) against `id`, `displayName`, `resource`, `tags`, and `shortDescription` - `func new templates` (bare invocation) — interactive flow: worker runtime → (dotnet sub-prompt: C#/F#, Node sub-prompt: JS/TS) → trigger → scaffold; the runtime prompt matches the existing `func new` UX (`.NET`, `Node`, `Python`, `Java`, `Powershell`); trigger selection supports incremental keyword filtering; powered by the existing `IInteractionService` / Spectre.Console already in vnext - **.NET isolated worker model only** — the manifest `CSharp` and `FSharp` languages map exclusively to the .NET isolated worker model; the in-process model is not supported and will not be added (it is on a deprecation path). Note: no F# templates exist in the manifest today; the sub-prompt will show F# once templates are available -- **Agent and CI friendly** — all v4 `func new` flags preserved (`--language`, `--template`); `--language` accepts runtime names (`python`, `node`, `java`, `dotnet`, `csharp`, `fsharp`, `javascript`, `typescript`, `powershell`); `--yes` accepts remaining defaults non-interactively but errors clearly if `--language` is not supplied; non-TTY falls back to numbered list +- **Agent and CI friendly** — all v4 `func new` flags preserved (`--language`, `--template`); `--language` accepts runtime names (`python`, `node`, `java`, `dotnet`, `csharp`, `fsharp`, `javascript`, `typescript`, `powershell`); non-TTY falls back to numbered list; `--template` skips all prompts for fully non-interactive use - Template download via git sparse-checkout or GitHub zip API (no bundling in binary) - `--path` to specify target directory (absolute or relative); created if absent, accepted if it exists and is empty — error if it exists and is not empty @@ -192,8 +192,8 @@ $ func new templates --template http-trigger-python-azd Error: Unable to reach GitHub to download the template. Please check your network connection and try again. -# --yes requires --language (target is empty — nothing to detect from) -$ func new templates --language python --yes +# Fully non-interactive: --template skips all prompts +$ func new templates --language python --template http-trigger-python-azd Created http-trigger-python-azd in current directory Next steps: @@ -201,9 +201,6 @@ $ func new templates --language python --yes 2. .venv\Scripts\activate # macOS/Linux: source .venv/bin/activate 3. pip install -r requirements.txt 4. func start - -# --yes without --language → clear error: -# Error: --language is required. Target directory is empty; cannot auto-detect language. ``` **Post-scaffold success banner:** @@ -427,7 +424,7 @@ flowchart TD LANG{"--language
supplied?"} LANG -- yes --> TRIG - LANG -- "no + --yes" --> LERR(["Error: --language required
cannot auto-detect"]) + LANG -- "no, non-TTY" --> LERR(["Error: --language required
cannot auto-detect"]) LANG -- "no, interactive" --> LPROMPT["Use the up/down arrow keys to select a worker runtime:
──────────────────────────
.NET
Node
Python
Java
Powershell"] LPROMPT --> DOTNETCHECK{"dotnet selected?"} @@ -468,12 +465,11 @@ flowchart TD F -- no --> G["Create directory"] F -- yes --> EMPTY{"Dir empty?"} EMPTY -- no --> ERR0(["Error: Target directory is not empty"]) - EMPTY -- yes --> H{"--yes and
no --language?"} + EMPTY -- yes --> H{"--language supplied?"} G --> H - H -- yes --> ERR1(["Error: --language required
target is empty, cannot auto-detect"]) - H -- no --> I{"--language supplied?"} - I -- "no, interactive" --> J["Prompt: select language
arrow keys + live type-to-filter"] - I -- yes --> K["Use --language value"] + H -- "no, non-TTY" --> ERR1(["Error: --language required
target is empty, cannot auto-detect"]) + H -- "no, interactive" --> J["Prompt: select language
arrow keys + live type-to-filter"] + H -- yes --> K["Use --language value"] J --> K K --> L{"--template supplied?"} L -- yes --> M["Resolve template directly"] @@ -482,7 +478,7 @@ flowchart TD N -- no --> P["Filter templates by language
apply --search if present"] O & P --> Q{"Interactive?"} Q -- yes --> R["Prompt: select trigger
arrow keys + live type-to-filter"] - Q -- "no or --yes" --> S["Use first match or default template"] + Q -- "no, non-TTY" --> S["Use first match or default template"] R --> M S --> M M --> Z["Download and write all files"] @@ -596,10 +592,6 @@ internal sealed class TemplatesCommand : FuncCliCommand { Description = "Case-insensitive substring filter applied to trigger names and descriptions before prompting (not semantic/fuzzy search)" }; - public Option YesOption { get; } = new("--yes", "-y") - { - Description = "Accept all defaults non-interactively. Requires --language (no auto-detect; target folder is empty)." - }; public TemplatesCommand( TemplatesListCommand listCommand, @@ -617,15 +609,15 @@ internal sealed class TemplatesCommand : FuncCliCommand protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { // Runtime/language resolution: - // → if --language missing in non-interactive/--yes mode: fail with clear error + // → if --language missing in non-TTY mode: fail with clear error // → if --language missing in interactive mode: prompt "Use the up/down arrow keys to select a worker runtime:" // showing: .NET, Node, Python, Java, Powershell // → if user selects ".NET": sub-prompt for C# vs F# // → if user selects "Node": sub-prompt for JavaScript vs TypeScript // → map prompt selection to manifest `language` value (see Runtime-to-Language Mapping) // → if --language supplied directly: map flag value to manifest language - // (e.g. "python" → "Python", "dotnet" → sub-prompt or default C# with --yes, - // "node" → sub-prompt or default TypeScript with --yes) + // (e.g. "python" → "Python", "dotnet" → sub-prompt for C#/F#, + // "node" → sub-prompt for JS/TS) // Directory resolution: // target = --path (absolute or relative) if supplied, else cwd // if target does not exist: create it @@ -633,7 +625,7 @@ internal sealed class TemplatesCommand : FuncCliCommand // if target exists and is not empty: error (v1 — --force deferred to future iteration) // Non-interactive path: --template supplied → resolve by id, scaffold directly // --template supplied → skip both language and trigger selection - // --yes path: skip trigger prompt; errors if --language not supplied + // --template path: skip both language and trigger prompts entirely // Interactive path: prompt via IInteractionService for missing runtime/trigger // 1. Fetch manifest (verbose logging for cache hit/miss/refresh) // manifest automatically excludes IaC-only templates (ARM, Bicep, Terraform) @@ -821,7 +813,7 @@ All user-facing errors are surfaced as `GracefulException` (caught by `Program.M | **Network — download** | GitHub returns non-2xx, times out, or `git clone` fails with network error | `Unable to reach GitHub to download the template. Please check your network connection and try again.` | `templates` (scaffold) after template selection, when the download step fails | | **GitHub rate limit** | Tree API returns 403 with `X-RateLimit-Remaining: 0`, or 429 | `GitHub API rate limit exceeded. Rate limit resets at . Try again later.` | `templates` (scaffold) during Tree API call for subfolder templates | | **Template not found** | `--template ` does not match any entry in the manifest | `Template '' not found. Run 'func new templates list' to see available templates.` | `templates --template ` or `info ` | -| **Language required** | `--yes` supplied without `--language` | `--language is required. Target directory is empty; cannot auto-detect language.` | `templates --yes` (non-interactive mode with no language) | +| **Language required** | Non-TTY mode without `--language` | `--language is required. Target directory is empty; cannot auto-detect language.` | `templates` in non-interactive/CI environment without --language | | **No templates for language** | `--language` value is valid but no manifest templates match | `No templates found for language ''.` | `templates --language ` or `list --language ` | | **Invalid language** | `--language` value doesn't map to any known runtime | `Unknown language ''. Valid values: dotnet, node, javascript, typescript, python, java, powershell.` | Any command that accepts `--language` | | **No search results** | `--search` filter (or combined `--language`/`--resource`/`--iac` filters) produces zero matching templates | `No templates found matching ''. Run 'func new templates list' to see all available templates.` | `templates` and `templates list` after filtering | @@ -859,7 +851,7 @@ Following the Core Tools vnext error handling convention: - **Persist last-used language** — store in `~/.azure-functions//preferences.json` after first scaffold; use as default on next run - **Explicit `func config set default-language `** — user opts in deliberately; no implicit magic - **Keep required** — simplest; interactive prompt is low friction anyway; scripts always supply it - - Consider: does a persisted or detected default interact badly with `--yes` in CI (wrong language silently used)? Detection from files is safer than persistence — it's scoped to the target dir, not global state. + - Consider: does a persisted or detected default interact badly with non-TTY/CI (wrong language silently used)? Detection from files is safer than persistence — it's scoped to the target dir, not global state. - [ ] **`--search` empty-result behaviour in interactive mode** — when the in-prompt Spectre filter (not the `--search` flag) narrows to zero results, Spectre renders an empty list. Should the command detect this and show a message, or let the user backspace to widen the filter naturally? - [ ] **`func new templates` vs `func new` interactive fallback** — should bare `func new` (when no workloads installed) redirect to `func new templates` interactive flow, rather than showing a hint? Or keep the hint to encourage workload installation? - [ ] **`--output json` on `func new templates list`** — useful for tooling. Worth adding now or later? From 240aa3898cbd8d15405f7e48caff6798aeca9014 Mon Sep 17 00:00:00 2001 From: manvkaur <67894494+manvkaur@users.noreply.github.com> Date: Tue, 26 May 2026 01:54:20 -0700 Subject: [PATCH 6/7] update to quickstart --- proposed/templates-e2e.md | 304 +++++++++++++++++++++++--------------- 1 file changed, 187 insertions(+), 117 deletions(-) diff --git a/proposed/templates-e2e.md b/proposed/templates-e2e.md index 030a9221f..9a759ab01 100644 --- a/proposed/templates-e2e.md +++ b/proposed/templates-e2e.md @@ -1,4 +1,4 @@ -# Core Tools vNext: `func new templates` — CDN-Backed Template Discovery +# Core Tools vNext: `func quickstart` — CDN-Backed Template Discovery **Author:** manvkaur **Date:** 2026-05-18 @@ -20,7 +20,7 @@ Development is proceeding with **`quickstart`** as the working name. This is a * | **`sample`** | `func sample` | `list`, `info` | These are sample repos; no overloading of "template" | Might imply "not production-ready" | | **`bootstrap`** | `func bootstrap` | `list`, `info` | Accurate for "set up a project from scratch" | Less discoverable; not a common Azure CLI verb | -> **Impact of verb change:** The verb is used in the command name, class names (`QuickstartCommand` → `TemplatesCommand`), service names, and all CLI examples throughout this document. The design is otherwise identical regardless of verb chosen. Swapping the verb is a find-and-replace — no architectural changes. +> **Impact of verb change:** The verb is used in the command name, class names, service names, and all CLI examples throughout this document. The design is otherwise identical regardless of verb chosen. Swapping the verb is a find-and-replace — no architectural changes. --- @@ -36,21 +36,25 @@ Development is proceeding with **`quickstart`** as the working name. This is a * - [Release & Compliance Model](#release--compliance-model) - [Runtime-to-Language Mapping](#runtime-to-language-mapping) - [ITemplateFunctionScaffolder](#itemplatefunctionscaffolder) - - [Download Strategy](#download-strategy---fetch-githttp) + - [Download Strategy](#download-strategy---fetch-autogithttp) - [Git Process Requirements](#git-process-requirements) + - [Repository Metadata Cleanup](#repository-metadata-cleanup) - [User Experience Flow](#user-experience-flow) - [Execution Flow](#execution-flow) - [Manifest Cache Flow](#manifest-cache-flow) - [Template Download Strategy](#template-download-strategy) - [New Commands](#new-commands) - - [TemplatesCommand](#templatescommand) - - [TemplatesListCommand](#templateslistcommand) - - [TemplatesInfoCommand](#templatesinfocommand) - - [NewCommand Change](#newcommand-change) + - [QuickstartCommand](#quickstartcommand) + - [QuickstartListCommand](#quickstartlistcommand) + - [QuickstartInfoCommand](#quickstartinfocommand) - [BuiltInCommands Change](#builtincommands-change) - [File Layout](#file-layout) - [IInteractionService Prompt Gap](#iinteractionservice-prompt-gap) - [Error Messaging](#error-messaging) +- [Security Envelope](#security-envelope) +- [Manifest Source](#manifest-source) +- [Exit Codes](#exit-codes) +- [Future Work](#future-work) - [Open Questions](#open-questions) - [Appendix](#appendix) - [References](#references) @@ -65,12 +69,12 @@ In vnext, workload templates solve the per-function scaffolding story: `func new Today, complete app templates live in GitHub repos (e.g. Azure-Samples) and developers discover them through docs, portal links, or search. This design brings that discovery and scaffolding workflow into the CLI itself. -This design introduces `func new templates`: a built-in command that downloads **complete, immediately runnable function app templates** directly from GitHub, complementing the workload-driven `func new` experience. Templates are discovered via a live manifest hosted on the Azure Functions CDN. Adding a new template is as simple as publishing a GitHub repo and adding an entry to the manifest — no CLI or workload release required. The manifest is versioned independently; the CLI picks up new templates automatically on the next manifest refresh. +This design introduces `func quickstart`: a built-in command that downloads **complete, immediately runnable function app templates** directly from GitHub, complementing the workload-driven `func new` experience. Templates are discovered via a live manifest hosted on the Azure Functions CDN. Adding a new template is as simple as publishing a GitHub repo and adding an entry to the manifest — no CLI or workload release required. The manifest is versioned independently; the CLI picks up new templates automatically on the next manifest refresh. How this complements workload templates: - **Decoupled template lifecycle** — app templates live as standalone GitHub repos; they can be added, updated, and iterated on independently of both CLI and workload package releases -- **Built-in discovery** — `func new templates list` and the interactive flow give developers a way to find and filter available app templates without leaving the CLI +- **Built-in discovery** — `func quickstart list` and the interactive flow give developers a way to find and filter available app templates without leaving the CLI --- @@ -78,11 +82,11 @@ How this complements workload templates: ### Goals -- Add `func new templates` as a built-in subcommand of `NewCommand` on the vnext branch +- Add `func quickstart` as a top-level command on the vnext branch - Download and create a **complete function app** from a GitHub template — all function code, config, and dependencies included - CDN-backed template manifest with ETag caching (24h TTL) -- `func new templates list` — non-interactive table of available templates, filterable by `--language`, `--resource`, `--iac`, and keyword (`--search`); `--search` is a **case-insensitive substring match** (not semantic/fuzzy) against `id`, `displayName`, `resource`, `tags`, and `shortDescription` -- `func new templates` (bare invocation) — interactive flow: worker runtime → (dotnet sub-prompt: C#/F#, Node sub-prompt: JS/TS) → trigger → scaffold; the runtime prompt matches the existing `func new` UX (`.NET`, `Node`, `Python`, `Java`, `Powershell`); trigger selection supports incremental keyword filtering; powered by the existing `IInteractionService` / Spectre.Console already in vnext +- `func quickstart list` — non-interactive table of available templates, filterable by `--language`, `--resource`, `--iac`, and keyword (`--search`); `--search` is a **case-insensitive substring match** (not semantic/fuzzy) against `id`, `displayName`, `resource`, `tags`, and `shortDescription` +- `func quickstart` (bare invocation) — interactive flow: worker runtime → (dotnet sub-prompt: C#/F#, Node sub-prompt: JS/TS) → trigger → scaffold; the runtime prompt matches the existing `func new` UX (`.NET`, `Node`, `Python`, `Java`, `Powershell`); trigger selection supports incremental keyword filtering; powered by the existing `IInteractionService` / Spectre.Console already in vnext - **.NET isolated worker model only** — the manifest `CSharp` and `FSharp` languages map exclusively to the .NET isolated worker model; the in-process model is not supported and will not be added (it is on a deprecation path). Note: no F# templates exist in the manifest today; the sub-prompt will show F# once templates are available - **Agent and CI friendly** — all v4 `func new` flags preserved (`--language`, `--template`); `--language` accepts runtime names (`python`, `node`, `java`, `dotnet`, `csharp`, `fsharp`, `javascript`, `typescript`, `powershell`); non-TTY falls back to numbered list; `--template` skips all prompts for fully non-interactive use - Template download via git sparse-checkout or GitHub zip API (no bundling in binary) @@ -96,34 +100,51 @@ How this complements workload templates: - Automatic environment setup (venv, npm install, dotnet restore) — out of scope; belongs in `func start` workflow - File conflict handling and `--force` flag — **deferred to next iteration of this command**; v1 requires target directory to be empty -> **Why `--language` is required:** Workload-driven `func new` operates on an existing project and can detect the runtime from project files. `func new templates` scaffolds into an empty directory, so there is nothing to detect from — `--language` must be supplied explicitly. +> **Why `--language` is required:** Workload-driven `func new` operates on an existing project and can detect the runtime from project files. `func quickstart` scaffolds into an empty directory, so there is nothing to detect from — `--language` must be supplied explicitly. --- ## Proposed Design -> **Command keyword note:** `templates` is the current choice for the subcommand name (`func new templates`). If the command is later promoted to top-level, this name becomes the top-level verb — the right name should be confirmed during design review before vNext ships. +> **Command keyword note:** `quickstart` is the working name for this top-level command (`func quickstart`). The final verb is still under discussion (see Command Verb section above). ### Command Tree ```bash -func new # NewCommand (existing) - func new templates # TemplatesCommand (new — interactive flow when bare) - func new templates list # TemplatesListCommand (new — table output) - func new templates info # TemplatesInfoCommand (new — detailed template info) +func quickstart [] [options] # QuickstartCommand (new — interactive flow when bare) + func quickstart list [options] # QuickstartListCommand (new — table output) + func quickstart info # QuickstartInfoCommand (new — detailed template info) ``` -> **Promotion path** — `TemplatesCommand` is designed to be re-parented as a top-level command (`func `) with zero logic changes. All business logic lives in `ITemplateManifestService` and `ITemplateFunctionScaffolder`; the command is a thin shell. When/if promoted: -> -> - Change registration in `BuiltInCommands` from `services.AddSingleton()` to `services.AddSingleton()` -> - Remove `Subcommands.Add(templatesCommand)` from `NewCommand` -> - No changes to services, scaffolding, prompts, or tests +#### Synopsis + +```bash +func quickstart [] [--language|-l ] [--template|-t ] + [--resource|-r ] [--iac ] [--search|-s ] + [--fetch auto|git|http] + +Subcommands: + list [--language] [--resource] [--iac] [--search] + info +``` + +| Option | Description | +| --- | --- | +| `` | Positional argument: target directory (consistent with `func init`, `func new`, `func start`). Defaults to current directory | +| `--language`, `-l` | Worker runtime or language (`python`, `node`, `java`, `dotnet`, `csharp`, `fsharp`, `javascript`, `typescript`, `powershell`) | +| `--template`, `-t` | Template id from manifest -- skips all prompts | +| `--resource`, `-r` | Filter by trigger/binding resource (`http`, `timer`, `blob`, `eventhub`, `servicebus`, `cosmos`, `sql`, `mcp`, `durable`) | +| `--iac` | Filter by infrastructure-as-code type (`bicep`, `terraform`, `none`) | +| `--search`, `-s` | Case-insensitive substring filter applied to IDs, display names, resources, tags, descriptions. Empty result -> exit 1 with guidance | +| `--fetch` | Controls how template payload is fetched: `auto` (default), `git`, `http`. See [Download Strategy](#download-strategy---fetch-autogithttp) | + +> **Thin-command design** — `QuickstartCommand` is a thin shell. All business logic lives in `ITemplateManifestService` and `ITemplateFunctionScaffolder`; no scaffolding, manifest, or prompt logic in the command itself. **User experience:** ```bash # Interactive scaffold — arrow keys to navigate, type to live-filter -$ func new templates +$ func quickstart Use the up/down arrow keys to select a worker runtime: .NET @@ -161,7 +182,7 @@ $ func new templates 3. func start # Interactive scaffold into a named folder (created if it doesn't exist) -$ func new templates --path ./my-new-api +$ func quickstart --path ./my-new-api Use the up/down arrow keys to select a worker runtime: ... @@ -174,26 +195,26 @@ $ func new templates --path ./my-new-api 3. func start # Scaffold into a specific folder (created if absent; also accepted if it exists and is empty) -$ func new templates --template blob-eventgrid-trigger-python-azd --path ./my-fn -$ func new templates --template blob-eventgrid-trigger-python-azd --path /home/user/projects/my-fn +$ func quickstart --template blob-eventgrid-trigger-python-azd --path ./my-fn +$ func quickstart --template blob-eventgrid-trigger-python-azd --path /home/user/projects/my-fn # Directory is not empty → error -$ func new templates --template http-trigger-python-azd --path ./existing-dir +$ func quickstart --template http-trigger-python-azd --path ./existing-dir Error: Target directory './existing-dir' is not empty. Use an empty directory or a new --path. # Network failure — CDN unreachable (manifest fetch) -$ func new templates +$ func quickstart Error: This feature requires a network connection to fetch the template catalog. Please check your connection and try again. # Network failure — GitHub unreachable (template download) -$ func new templates --template http-trigger-python-azd +$ func quickstart --template http-trigger-python-azd Error: Unable to reach GitHub to download the template. Please check your network connection and try again. # Fully non-interactive: --template skips all prompts -$ func new templates --language python --template http-trigger-python-azd +$ func quickstart --language python --template http-trigger-python-azd Created http-trigger-python-azd in current directory Next steps: @@ -218,7 +239,7 @@ Every successful scaffold prints a success line followed by runtime-specific nex ```bash # Filter by language — Language column omitted (redundant) -$ func new templates list --language python +$ func quickstart list --language python Id Resource IaC ──────────────────────────────────────────────────────── @@ -230,7 +251,7 @@ $ func new templates list --language python ... # Filter by resource — Resource column omitted (redundant) -$ func new templates list --resource http +$ func quickstart list --resource http Id Language IaC ──────────────────────────────────────────────────────── @@ -241,7 +262,7 @@ $ func new templates list --resource http ... # Filter by iac — IaC column omitted (redundant) -$ func new templates list --iac bicep +$ func quickstart list --iac bicep Id Resource Language ──────────────────────────────────────────────────────────── @@ -250,7 +271,7 @@ $ func new templates list --iac bicep ... # Keyword search — all columns shown (no filter to omit) -$ func new templates list --search blob +$ func quickstart list --search blob Id Resource Language IaC ──────────────────────────────────────────────────────────────────────── @@ -260,19 +281,19 @@ $ func new templates list --search blob ... # Combined — Language and Resource columns omitted -$ func new templates list --language python --resource blob +$ func quickstart list --language python --resource blob # Non-interactive scaffold with language + resource -$ func new templates --language python --resource http +$ func quickstart --language python --resource http # Non-interactive scaffold with language + resource + iac -$ func new templates --language python --resource http --iac bicep +$ func quickstart --language python --resource http --iac bicep # Non-interactive scaffold by exact template id (skips language + trigger prompts) -$ func new templates --template http-trigger-python-azd +$ func quickstart --template http-trigger-python-azd # Show detailed info about a specific template -$ func new templates info http-trigger-python-azd +$ func quickstart info http-trigger-python-azd HTTP Trigger (Python + AZD + Bicep) @@ -320,7 +341,7 @@ internal interface ITemplateManifestService | 304 Not Modified | Update TTL timestamp, return cached manifest | | Network failure | Log warning via `IInteractionService.WriteWarning`; fall back to bundled embedded manifest | | Trusted-org filter | Strip any template whose `repositoryUrl` owner is not in `{"azure", "azure-samples", "microsoft"}` | -| IaC-only filter | Exclude templates where `language` is `ARM`, `Bicep`, or `Terraform` — these are infrastructure-as-code templates, not function app code. The manifest has 4 such entries (e.g. `iac-flex-consumption-bicep`); they are irrelevant to `func new templates`. | +| IaC-only filter | Exclude templates where `language` is `ARM`, `Bicep`, or `Terraform` — these are infrastructure-as-code templates, not function app code. The manifest has 4 such entries (e.g. `iac-flex-consumption-bicep`); they are irrelevant to `func quickstart`. | | Manifest validation | Non-null `templates` array; each entry has `id`, `language`, `resource`, `repositoryUrl`, `folderPath`, `gitRef` | | `gitRef` resolution | Each template entry **must** include `"gitRef": ""`. The CLI uses `gitRef` as the checkout/download target. This ensures templates are pinned to an immutable, reviewed release — no implicit default-branch resolution | | URL override | `FUNC_TEMPLATE_MANIFEST_URL` environment variable overrides the primary URL. Used for testing against staging CDN or local dev manifests. Same schema expected | @@ -373,12 +394,13 @@ internal interface ITemplateFunctionScaffolder `TemplateFunctionScaffolder` implementation (ported from `fnx/lib/init/scaffold.js`): -#### Download Strategy (`--fetch git|http`) +#### Download Strategy (`--fetch auto|git|http`) | Value | Behaviour | Best for | | --- | --- | --- | -| `git` **(default)** | Uses git sparse-checkout. If `git` is not on PATH, falls back to `http` with a warning: `git not found on PATH — using HTTP to download template.` | Developers with git on PATH (vast majority) | -| `http` | Uses GitHub REST API (zip for whole-repo, tree API + raw downloads for subfolders) | Environments without git, CI containers, air-gapped with proxy | +| `auto` **(default)** | Probe `git --version`. If git >= 2.25 is available, use `git`; otherwise fall back to `http` silently | Most users (auto-selects the best available strategy) | +| `git` | Force git. Fails with exit 1 if git is unavailable: `git 2.25 or later was not found on PATH. Install a recent git or use '--fetch http'.` | When you want to guarantee git-based fetch | +| `http` | Force the GitHub archive zip endpoint. Never invokes git | Environments without git, CI containers, air-gapped with proxy | The `--fetch` flag allows explicit override regardless of git availability. @@ -387,26 +409,48 @@ The `--fetch` flag allows explicit override regardless of git availability. | Strategy | Condition | Detail | | ---------- | ----------- | -------- | | git sparse-checkout | `--fetch git` + `folderPath` is subfolder | `git clone --filter=blob:none --sparse --branch ` then `git sparse-checkout set ` | -| git clone (whole repo) | `--fetch git` + `folderPath` is `"."` | `git clone --depth 1 --branch ` | +| git clone (whole repo) | `--fetch git` + `folderPath` is `"."` | `git clone --depth 1 --branch --single-branch --config core.autocrlf=false -- ` | | Tree API + raw URLs | `--fetch http` + `folderPath` is subfolder | `GET /repos/{owner}/{repo}/git/trees/?recursive=1` to enumerate, then download each file via `raw.githubusercontent.com////`. Rate limit: 60 tree req/hr (unauthenticated), ~5,000 raw req/hr | -| GitHub zip API | `--fetch http` + `folderPath` is `"."` | `GET https://api.github.com/repos///zipball/` → extract and strip root prefix | +| GitHub zip API | `--fetch http` + `folderPath` is `"."` | `GET https://api.github.com/repos///zipball/` -> extract and strip root prefix | | Path traversal prevention | Always | Resolve each extracted path and assert it starts with `targetDirectory + Path.DirectorySeparatorChar` | | Trusted org | Always | Validate `owner` from `repositoryUrl` before any network call | | URL scheme | Always | Only `https://github.com/` allowed as repository base | +**Git clone notes:** + +- `--branch` is omitted when `gitRef` is null/empty/`"HEAD"`, so git uses the remote's default branch (handles both `main` and `master` repos) +- `--sparse` added only when the manifest entry has a non-root `folderPath` +- Requires git 2.25+ for `--sparse` cone mode (released January 2020) + +**Subfolder promotion:** + +After sparse-checkout, content lives at `//`. The scaffolder promotes it to `/` (deletes siblings, moves content up) so the final layout matches the http path. + #### Git Process Requirements All git child-process invocations must satisfy: | Requirement | Implementation | | --- | --- | -| **Process abstraction** | Wrap behind `IGitRunner` interface — no direct `Process.Start` in business logic. Testable via `FakeGitRunner` in tests | +| **Process abstraction** | Wrap behind `IGitRunner` interface -- no direct `Process.Start` in business logic. Testable via `FakeGitRunner` in tests | | **Argument-array invocation** | Pass arguments via `ProcessStartInfo.ArgumentList` (string array), never as a shell-concatenated string. Prevents injection if a URL or path contains special characters | -| **Non-interactive** | Set `GIT_TERMINAL_PROMPT=0` environment variable on the child process — git must never prompt for credentials. Private/inaccessible repos fail cleanly with an error message | -| **Timeout** | Configurable timeout (default 120s). Kill the process and report `Template download timed out. Try again or use --fetch http.` | +| **`--` end-of-options sentinel** | Always place `--` before positional args (URL, target path) -- blocks flag-injection via manifest values | +| **gitRef validation** | Reject if it starts with `-` (belt-and-braces over the `--` sentinel) | +| **folderPath validation** | Reject if it starts with `-` or contains `..` | +| **Non-interactive** | Set environment variables on the child process: `GIT_TERMINAL_PROMPT=0`, `GIT_ASKPASS=echo`, `SSH_ASKPASS=echo`, `GCM_INTERACTIVE=Never` -- git must never prompt for credentials | +| **Timeout** | 60-second timeout with linked `CancellationTokenSource`; user cancellation distinguished from timeout; process tree killed on timeout | | **Cancellation** | `CancellationToken` from the command handler kills the git child process on Ctrl+C. Use `Process.Kill(entireProcessTree: true)` | | **Temp directory cleanup** | Clone into a temp directory; move files to target on success. On failure or cancellation, delete the temp directory in a `finally` block | +#### Repository Metadata Cleanup + +After any successful fetch (both git and http paths), the scaffolder removes: + +- `/.git/` -- never useful for a fresh quickstart +- `/.github/` -- CI workflows belong to the source repo, not the user's project + +Read-only attributes (set by git on Windows for pack files) are cleared before deletion. + --- ### User Experience Flow @@ -415,7 +459,7 @@ What the user sees in the terminal across every path. ```mermaid flowchart TD - START(["$ func new templates [flags]"]) + START(["$ func quickstart [flags]"]) START --> FETCH["Fetching template catalog..."] FETCH --> DIR_ERR{"Target dir empty?"} @@ -457,7 +501,7 @@ flowchart TD ```mermaid flowchart TD - A(["func new templates invoked"]) --> B["Fetch manifest
CDN - ETag cache - bundled fallback"] + A(["func quickstart invoked"]) --> B["Fetch manifest
CDN - ETag cache - bundled fallback"] B --> C{"--path supplied?"} C -- yes --> D["Resolve target = --path"] C -- no --> E["Resolve target = cwd"] @@ -556,13 +600,13 @@ flowchart TD ### New Commands -#### `TemplatesCommand` +#### `QuickstartCommand` -> **Thin-command principle** — `TemplatesCommand` must not contain any manifest, scaffolding, or prompt logic itself. All of that lives in `ITemplateManifestService` and `ITemplateFunctionScaffolder`. This keeps the command re-parentable: it can live under `func new` today and be promoted to `func ` later by changing one DI registration, not the implementation. +> **Thin-command principle** — `QuickstartCommand` must not contain any manifest, scaffolding, or prompt logic itself. All of that lives in `ITemplateManifestService` and `ITemplateFunctionScaffolder`. ```csharp -// src/Func/Commands/New/TemplatesCommand.cs -internal sealed class TemplatesCommand : FuncCliCommand +// src/Func/Commands/Quickstart/QuickstartCommand.cs +internal sealed class QuickstartCommand : FuncCliCommand { // Carried forward from v4 func new — preserve agent/CI compatibility public Option LanguageOption { get; } = new("--language", "-l") @@ -593,13 +637,13 @@ internal sealed class TemplatesCommand : FuncCliCommand Description = "Case-insensitive substring filter applied to trigger names and descriptions before prompting (not semantic/fuzzy search)" }; - public TemplatesCommand( - TemplatesListCommand listCommand, - TemplatesInfoCommand infoCommand, + public QuickstartCommand( + QuickstartListCommand listCommand, + QuickstartInfoCommand infoCommand, ITemplateManifestService manifestService, ITemplateFunctionScaffolder scaffolder, IInteractionService interaction) - : base("templates", "Browse and scaffold functions from the Azure Functions template catalog.") + : base("quickstart", "Browse and scaffold functions from the Azure Functions template catalog.") { Subcommands.Add(listCommand); Subcommands.Add(infoCommand); @@ -640,11 +684,11 @@ internal sealed class TemplatesCommand : FuncCliCommand } ``` -#### `TemplatesListCommand` +#### `QuickstartListCommand` ```csharp -// src/Func/Commands/New/TemplatesListCommand.cs -internal sealed class TemplatesListCommand : FuncCliCommand +// src/Func/Commands/Quickstart/QuickstartListCommand.cs +internal sealed class QuickstartListCommand : FuncCliCommand { public Option LanguageOption { get; } = new("--language", "-l") { @@ -680,22 +724,22 @@ internal sealed class TemplatesListCommand : FuncCliCommand } ``` -#### `TemplatesInfoCommand` +#### `QuickstartInfoCommand` ```csharp -// src/Func/Commands/New/TemplatesInfoCommand.cs -internal sealed class TemplatesInfoCommand : FuncCliCommand +// src/Func/Commands/Quickstart/QuickstartInfoCommand.cs +internal sealed class QuickstartInfoCommand : FuncCliCommand { public Argument TemplateIdArgument { get; } = new("id") { - Description = "Template id from the manifest (e.g. http-trigger-python-azd). Use 'func new templates list' to see available ids." + Description = "Template id from the manifest (e.g. http-trigger-python-azd). Use 'func quickstart list' to see available ids." }; protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { // 1. Fetch manifest // 2. Find template by id (case-insensitive match) - // 3. If not found: error with suggestion to run 'func new templates list' + // 3. If not found: error with suggestion to run 'func quickstart list' // 4. Display detailed info: // - displayName // - longDescription (or shortDescription fallback) @@ -708,25 +752,6 @@ internal sealed class TemplatesInfoCommand : FuncCliCommand --- -### `NewCommand` Change - -`NewCommand` gains `TemplatesCommand` as a constructor-injected subcommand: - -```csharp -// BEFORE -public NewCommand(IWorkloadHintRenderer hintRenderer) : base("new", "...") { ... } - -// AFTER -public NewCommand(IWorkloadHintRenderer hintRenderer, TemplatesCommand templatesCommand) - : base("new", "Create a new function from a template.") -{ - Subcommands.Add(templatesCommand); - // existing options unchanged -} -``` - ---- - ### `BuiltInCommands` Change ```csharp @@ -736,13 +761,12 @@ services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); -// New (TemplatesCommand, same pattern): -services.AddSingleton(); -services.AddSingleton(); -services.AddSingleton(); // not as FuncCliCommand — it's a subcommand +// New (QuickstartCommand, same pattern — top-level command): +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); // top-level command services.AddSingleton(); services.AddSingleton(); -// NewCommand's registration already exists; it now also resolves TemplatesCommand via DI ``` --- @@ -752,11 +776,11 @@ services.AddSingleton() ```text src/Func/ Commands/ - New/ - TemplatesCommand.cs ← new - TemplatesListCommand.cs ← new - TemplatesInfoCommand.cs ← new - NewCommand.cs ← modified (add TemplatesCommand subcommand) + Quickstart/ + QuickstartCommand.cs ← new + QuickstartListCommand.cs ← new + QuickstartInfoCommand.cs ← new + NewCommand.cs ← unchanged (quickstart is top-level, not a subcommand) Templates/ ITemplateManifestService.cs ← new TemplateManifestService.cs ← new @@ -769,10 +793,10 @@ src/Func/ BuiltInCommands.cs ← modified (register new services + commands) test/Func.Tests/ - Commands/New/ - TemplatesCommandTests.cs ← new - TemplatesListCommandTests.cs ← new - TemplatesInfoCommandTests.cs ← new + Commands/Quickstart/ + QuickstartCommandTests.cs ← new + QuickstartListCommandTests.cs ← new + QuickstartInfoCommandTests.cs ← new Templates/ TemplateManifestServiceTests.cs ← new TemplateFunctionScaffolderTests.cs ← new @@ -782,7 +806,7 @@ test/Func.Tests/ ## `IInteractionService` Prompt Gap -The current `IInteractionService` in vnext (`src/Func/Console/IInteractionService.cs`) does not yet have a selection-prompt method. `TemplatesCommand`'s interactive path needs one. Two options: +The current `IInteractionService` in vnext (`src/Func/Console/IInteractionService.cs`) does not yet have a selection-prompt method. `QuickstartCommand`'s interactive path needs one. Two options: 1. **Extend `IInteractionService`** — add `Task PromptSelectionAsync(string title, IEnumerable<(T value, string label)> options, CancellationToken ct)`. Backed by `Spectre.Console.SelectionPrompt` with `SearchEnabled = true` in `SpectreInteractionService`, which gives the user: - **Arrow keys** (↑↓) to move through the list @@ -794,11 +818,11 @@ The current `IInteractionService` in vnext (`src/Func/Console/IInteractionServic **Why this matters vs. v4 `func new`:** The current stable CLI uses raw `Console.ReadKey()` arrow navigation which hangs or crashes in agent, CI, and piped contexts because there is no TTY. The `IInteractionService.IsInteractive` check ensures the new design degrades gracefully: in a non-TTY context the prompt renders as a numbered list that accepts stdin, and when all flags are supplied no prompt is shown at all. -2. **Use `IAnsiConsole` directly in `TemplatesCommand`** — inject `Spectre.Console.IAnsiConsole` alongside `IInteractionService` as an escape hatch. +2. **Use `IAnsiConsole` directly in `QuickstartCommand`** — inject `Spectre.Console.IAnsiConsole` alongside `IInteractionService` as an escape hatch. -**Preferred: Option 1** — keeps all TUI calls behind `IInteractionService`. This makes `TemplatesCommand` fully testable with a substituted `IInteractionService` (NSubstitute). `SearchEnabled = true` on `SelectionPrompt` is a one-liner in the Spectre implementation and directly serves the in-prompt filter UX. +**Preferred: Option 1** — keeps all TUI calls behind `IInteractionService`. This makes `QuickstartCommand` fully testable with a substituted `IInteractionService` (NSubstitute). `SearchEnabled = true` on `SelectionPrompt` is a one-liner in the Spectre implementation and directly serves the in-prompt filter UX. -> **Note on `--search` vs. in-prompt search:** These are complementary. `--search blob` on `func new templates` pre-filters the list *before* the prompt is shown (useful for scripting or reducing noise when you already know the keyword). The `SearchEnabled` interactive filter lets users type to narrow down after the prompt appears. Both apply the same case-insensitive substring match on template id, trigger name, and description. +> **Note on `--search` vs. in-prompt search:** These are complementary. `--search blob` on `func quickstart` pre-filters the list *before* the prompt is shown (useful for scripting or reducing noise when you already know the keyword). The `SearchEnabled` interactive filter lets users type to narrow down after the prompt appears. Both apply the same case-insensitive substring match on template id, trigger name, and description. --- @@ -812,11 +836,11 @@ All user-facing errors are surfaced as `GracefulException` (caught by `Program.M | **Network — manifest (degraded)** | CDN fails but a cached or bundled manifest exists | *(warning, not error)* `Unable to refresh the template catalog — using cached data.` | Same as above, but a stale manifest was found; command continues with stale data | | **Network — download** | GitHub returns non-2xx, times out, or `git clone` fails with network error | `Unable to reach GitHub to download the template. Please check your network connection and try again.` | `templates` (scaffold) after template selection, when the download step fails | | **GitHub rate limit** | Tree API returns 403 with `X-RateLimit-Remaining: 0`, or 429 | `GitHub API rate limit exceeded. Rate limit resets at . Try again later.` | `templates` (scaffold) during Tree API call for subfolder templates | -| **Template not found** | `--template ` does not match any entry in the manifest | `Template '' not found. Run 'func new templates list' to see available templates.` | `templates --template ` or `info ` | +| **Template not found** | `--template ` does not match any entry in the manifest | `Template '' not found. Run 'func quickstart list' to see available templates.` | `templates --template ` or `info ` | | **Language required** | Non-TTY mode without `--language` | `--language is required. Target directory is empty; cannot auto-detect language.` | `templates` in non-interactive/CI environment without --language | | **No templates for language** | `--language` value is valid but no manifest templates match | `No templates found for language ''.` | `templates --language ` or `list --language ` | | **Invalid language** | `--language` value doesn't map to any known runtime | `Unknown language ''. Valid values: dotnet, node, javascript, typescript, python, java, powershell.` | Any command that accepts `--language` | -| **No search results** | `--search` filter (or combined `--language`/`--resource`/`--iac` filters) produces zero matching templates | `No templates found matching ''. Run 'func new templates list' to see all available templates.` | `templates` and `templates list` after filtering | +| **No search results** | `--search` filter (or combined `--language`/`--resource`/`--iac` filters) produces zero matching templates | `No templates found matching ''. Run 'func quickstart list' to see all available templates.` | `templates` and `templates list` after filtering | | **Directory not empty** | Target directory contains any files or subdirectories | `Target directory '' is not empty. Use an empty directory or a new --path.` | `templates` (scaffold) before download, during directory validation | | **Untrusted org** | Template's `repositoryUrl` owner is not in the trusted-org allowlist (`azure`, `azure-samples`, `microsoft`) | `Template references an untrusted repository (/). This template cannot be downloaded.` | `templates` (scaffold) before any network call to GitHub | | **Path traversal** | An extracted file resolves outside the target directory | `Path traversal detected in template archive: . The template may be corrupted or malicious.` | `templates` (scaffold) during file extraction from staging to target | @@ -836,12 +860,58 @@ Following the Core Tools vnext error handling convention: - `InvalidOperationException` — untrusted org in manifest, path traversal, parse errors - `KeyNotFoundException` — template id not found -- **Commands** (`TemplatesCommand`, `TemplatesListCommand`, `TemplatesInfoCommand`) are the boundary. Each catches the specific exceptions it expects from its direct service calls and wraps them in `GracefulException(message, isUserError: true)`, preserving the inner exception. The `try` is narrow — one service call per `try` block. +- **Commands** (`QuickstartCommand`, `QuickstartListCommand`, `QuickstartInfoCommand`) are the boundary. Each catches the specific exceptions it expects from its direct service calls and wraps them in `GracefulException(message, isUserError: true)`, preserving the inner exception. The `try` is narrow — one service call per `try` block. - **Anything unexpected** is not caught by the command — it surfaces as an unhandled exception with a stack trace (runtime bug). --- +## Security Envelope + +| Concern | Mitigation | +| --- | --- | +| Malicious manifest entry pointing to attacker repo | URL allow-list: HTTPS scheme + `github.com` host + trusted org (`azure`, `azure-samples`, `microsoft`). Filtered in manifest client, re-checked in scaffolder | +| Zip slip / path traversal in archive | Every entry's resolved absolute path must start with `Path.GetFullPath(targetPath)` + `DirectorySeparator`. Mismatch -> `InvalidOperationException` | +| Flag injection via manifest values | Argv-array invocation + `--` end-of-options sentinel + reject `gitRef`/`folderPath` starting with `-` | +| Command-line injection via folderPath | `..` segments rejected | +| Interactive git credential prompts hanging the CLI | Four env vars set to disable every prompt path (see Git Process Requirements) | +| Runaway git process | 60s timeout with process-tree kill | + +**Trusted GitHub organizations** -- hard-coded allow-list in `QuickstartUrlValidator`: + +- `azure` +- `azure-samples` +- `microsoft` + +--- + +## Manifest Source + +| Aspect | Detail | +| --- | --- | +| Primary | `https://cdn.functions.azure.com/public/templates-manifest/manifest.json` | +| Fallback | Raw GitHub URL (`Azure/azure-functions-templates` main branch) | +| Override | `FUNC_TEMPLATE_MANIFEST_URL` env var -- accepts an `https://` URL or a `file://` path. Used for staging validation and local manifest authoring against unpublished entries | +| Caching | ETag-based; reduces CDN round-trips on repeat invocations within a session | + +The current CDN manifest does not yet pin `gitRef` per entry; entries without `gitRef` clone the remote's default branch (git path) or download `archive/HEAD.zip` (http path). Once the manifest moves to `gitRef`-pinned entries with signed tags, the security envelope tightens further (see Future Work). + +--- + +## Exit Codes + +| Code | Condition | +| --- | --- | +| 0 | Successful scaffold / list / info | +| 1 | User error (graceful): unknown template, non-empty target, empty filter result, manifest fetch failure, `--fetch git` with git missing, git/http fetch failure | +| Non-zero (uncaught) | Unexpected runtime bug -- stack trace surfaces | + +--- + +## Future Work + +- **GPG tag verification** -- when the manifest moves to `gitRef`-pinned entries and Azure-Samples/trusted orgs begin GPG-signing release tags, add an optional `git verify-tag` step. Default-on once signing is reliable; needs UX for unsigned/invalid, GPG-not-installed handling, public-key bundling or trust-on-first-use. Both `gitRef` enforcement and GPG verification become active together -- one without the other provides no guarantee. + --- ## Open Questions @@ -853,8 +923,8 @@ Following the Core Tools vnext error handling convention: - **Keep required** — simplest; interactive prompt is low friction anyway; scripts always supply it - Consider: does a persisted or detected default interact badly with non-TTY/CI (wrong language silently used)? Detection from files is safer than persistence — it's scoped to the target dir, not global state. - [ ] **`--search` empty-result behaviour in interactive mode** — when the in-prompt Spectre filter (not the `--search` flag) narrows to zero results, Spectre renders an empty list. Should the command detect this and show a message, or let the user backspace to widen the filter naturally? -- [ ] **`func new templates` vs `func new` interactive fallback** — should bare `func new` (when no workloads installed) redirect to `func new templates` interactive flow, rather than showing a hint? Or keep the hint to encourage workload installation? -- [ ] **`--output json` on `func new templates list`** — useful for tooling. Worth adding now or later? +- [ ] **`func quickstart` vs `func new` interactive fallback** — should bare `func new` (when no workloads installed) redirect to `func quickstart` interactive flow, rather than showing a hint? Or keep the hint to encourage workload installation? +- [ ] **`--output json` on `func quickstart list`** — useful for tooling. Worth adding now or later? --- @@ -862,14 +932,14 @@ Following the Core Tools vnext error handling convention: ### References -- `fnx init` orchestration: [func-emulate/fnx/lib/init.js](../../../repos/func-emulate/fnx/lib/init.js) -- Manifest fetch + cache: [func-emulate/fnx/lib/init/manifest.js](../../../repos/func-emulate/fnx/lib/init/manifest.js) -- Scaffold (git + zip): [func-emulate/fnx/lib/init/scaffold.js](../../../repos/func-emulate/fnx/lib/init/scaffold.js) -- Interactive prompts: [func-emulate/fnx/lib/init/prompts.js](../../../repos/func-emulate/fnx/lib/init/prompts.js) -- Runtime + trigger constants: [func-emulate/fnx/templates-mcp/src/templates.ts](../../../repos/func-emulate/fnx/templates-mcp/src/templates.ts) -- vnext `NewCommand`: [azure-functions-core-tools/src/Func/Commands/NewCommand.cs](../../../repos/azure-functions-core-tools/src/Func/Commands/NewCommand.cs) -- vnext `WorkloadCommand` (pattern): [azure-functions-core-tools/src/Func/Commands/Workload/WorkloadCommand.cs](../../../repos/azure-functions-core-tools/src/Func/Commands/Workload/WorkloadCommand.cs) -- vnext `IInteractionService`: [azure-functions-core-tools/src/Func/Console/IInteractionService.cs](../../../repos/azure-functions-core-tools/src/Func/Console/IInteractionService.cs) +- `fnx init` orchestration: `func-emulate/fnx/lib/init.js` (internal) +- Manifest fetch + cache: `func-emulate/fnx/lib/init/manifest.js` (internal) +- Scaffold (git + zip): `func-emulate/fnx/lib/init/scaffold.js` (internal) +- Interactive prompts: `func-emulate/fnx/lib/init/prompts.js` (internal) +- Runtime + trigger constants: `func-emulate/fnx/templates-mcp/src/templates.ts` (internal) +- vnext `NewCommand`: `src/Func/Commands/NewCommand.cs` +- vnext `WorkloadCommand` (pattern): `src/Func/Commands/Workload/WorkloadCommand.cs` +- vnext `IInteractionService`: `src/Func/Console/IInteractionService.cs` ### vnext Architecture (What Already Exists) From b7a9a70166312260caacd53ba22b27603dd3ed59 Mon Sep 17 00:00:00 2001 From: manvkaur <67894494+manvkaur@users.noreply.github.com> Date: Thu, 28 May 2026 03:21:40 -0700 Subject: [PATCH 7/7] Rename to quickstarts-e2e, align terminology and implementation - Rename templates-e2e.md to quickstarts-e2e.md - Resolve terminology: 'template' is the noun, 'quickstart' is the transitional command verb converging into func templates - Document existing func templates surface (templating engine, tokenization, snippets) and convergence intent - Update IInteractionService section from 'Prompt Gap' to 'Prompt Support' reflecting PR 4 additions (PromptForSelectionAsync, EnableSearch) - Align service names, file layout, env vars, and error messages with current implementation on quickstart-handler branch - Update --force from deferred to implemented Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 1 + proposed/quickstarts-e2e.md | 782 ++++++++++++++++++++++++++++ proposed/templates-e2e.md | 988 ------------------------------------ 3 files changed, 783 insertions(+), 988 deletions(-) create mode 100644 proposed/quickstarts-e2e.md delete mode 100644 proposed/templates-e2e.md diff --git a/.gitignore b/.gitignore index 89f9ac04a..2f422e0a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ out/ +artifacts/ diff --git a/proposed/quickstarts-e2e.md b/proposed/quickstarts-e2e.md new file mode 100644 index 000000000..8b7fc07db --- /dev/null +++ b/proposed/quickstarts-e2e.md @@ -0,0 +1,782 @@ +# Core Tools vNext: `func quickstart` — CDN-Backed Template Discovery + +**Author:** manvkaur +**Date:** 2026-05-18 +**Status:** Draft +**Work Item:** TBD + +--- + +## Command Verb & Terminology Resolution + +**Template** is the single noun and concept for this feature. Every user-facing string, design reference, and documentation entry uses "template" — the template catalog, template entries, template scaffolding, etc. + +**`quickstart`** is the current command verb (`func quickstart`). This is a transitional name. The CLI already has a use of `templates` surface (templating engine, tokenization, snippets) serving a related but distinct purpose. The long-term intent is to **converge both under a unified `func templates` command** — whole-app scaffolding (currently `func quickstart`) and the existing templating engine would share a single verb with subcommands. The details of that unification are not yet explored; until then, `quickstart` remains the verb to avoid churn during active development. + +- Command (current): `func quickstart` → converges into `func templates` +- What gets scaffolded: a **template** +- Collection: the **template catalog** +- Manifest item: a **template entry** + +Implementation note: class and interface names use the `Quickstart*` prefix to match the current command verb. These will be renamed alongside the command convergence. + +--- + +## Table of Contents + +- [Command Verb & Terminology Resolution](#command-verb--terminology-resolution) +- [Problem Statement](#problem-statement) +- [Goals / Non-Goals](#goals--non-goals) +- [Proposed Design](#proposed-design) + - [Command Tree](#command-tree) + - [New Services](#new-services) + - [IQuickstartManifestService](#iquickstartmanifestservice) + - [Release & Compliance Model](#release--compliance-model) + - [Runtime-to-Language Mapping](#runtime-to-language-mapping) + - [IQuickstartScaffolder](#iquickstartscaffolder) + - [Download Strategy](#download-strategy---fetch-autogithttp) + - [Git Process Requirements](#git-process-requirements) + - [Repository Metadata Cleanup](#repository-metadata-cleanup) + - [User Experience Flow](#user-experience-flow) + - [Execution Flow](#execution-flow) + - [Manifest Cache Flow](#manifest-cache-flow) + - [Template Download Strategy](#template-download-strategy) + - [New Commands](#new-commands) + - [QuickstartCommand](#quickstartcommand) + - [QuickstartListCommand](#quickstartlistcommand) + - [QuickstartInfoCommand](#quickstartinfocommand) + - [BuiltInCommands Change](#builtincommands-change) + - [File Layout](#file-layout) +- [IInteractionService Prompt Support](#iinteractionservice-prompt-support) +- [Error Messaging](#error-messaging) +- [Security Envelope](#security-envelope) +- [Manifest Source](#manifest-source) +- [Exit Codes](#exit-codes) +- [Future Work](#future-work) +- [Open Questions](#open-questions) +- [Appendix](#appendix) + - [References](#references) + - [vnext Architecture](#vnext-architecture-what-already-exists) + - [fnx init — Source Implementation](#fnx-init--source-implementation) + +--- + +## Problem Statement + +In vnext, workload templates solve the per-function scaffolding story: `func new` adds a single function file to an existing project using templates shipped in workload packages. A complementary scenario is **complete-app scaffolding** — downloading a fully runnable function app (function code, host.json, local.settings.json, IaC configuration, and all dependencies) in a single step. Runtime-specific setup (venv, npm install, dotnet restore) is handled separately by the developer or by `func start`. + +Today, complete app templates live in GitHub repos (e.g. Azure-Samples) and developers discover them through docs, portal links, or search. This design brings that discovery and scaffolding workflow into the CLI itself. + +This design introduces `func quickstart`: a built-in command that downloads **complete, immediately runnable function app templates** directly from GitHub, complementing the workload-driven `func new` experience. Templates are discovered via a live template catalog hosted on the Azure Functions CDN. Adding a new template is as simple as publishing a GitHub repo and adding a template entry to the manifest — no CLI or workload release required. The manifest is versioned independently; the CLI picks up new templates automatically on the next manifest refresh. + +How this complements workload templates: + +- **Decoupled template lifecycle** — app templates live as standalone GitHub repos; they can be added, updated, and iterated on independently of both CLI and workload package releases +- **Built-in discovery** — `func quickstart list` and the interactive flow give developers a way to find and filter available app templates without leaving the CLI + +--- + +## Goals / Non-Goals + +### Goals + +- Add `func quickstart` as a top-level command on the vnext branch +- Download and scaffold a **complete function app template** from GitHub — all function code, config, and dependencies included +- CDN-backed template catalog with ETag caching (24h TTL) and stale-cache fallback +- `func quickstart list` — list available templates with `--stack`, `--language`, `--resource`, `--iac`, `--search`, and `--json` +- `func quickstart` (bare invocation) — interactive flow: stack → language (when a stack supports multiple manifest languages) → template → scaffold +- Current stack coverage matches the implementation branch: `.NET`, `Node`, and `Python` providers are registered today; other stacks can participate later by contributing `IQuickstartProvider` implementations +- Agent and CI friendly — `--stack`, `--language`, `--template`, `--fetch`, and `--force` support non-interactive use; `--template` skips template selection once stack/language resolution is complete +- Template download via git tag fetch or GitHub archive download (no bundling in the binary) +- Positional `` target directory support; create it if absent, use it if empty, and allow overwrite with `--force` + +### Non-Goals + +- Adding a single function file to an existing project (that is v4 `func new` behaviour — out of scope here) +- Replacing or changing `func init` (separate command, workload-driven) +- Changing the workload model or `IProjectInitializer` interface +- Automatic environment setup (venv, npm install, dotnet restore) — out of scope; belongs in a later workflow +- Designing conflict-resolution modes beyond the current `--force` clear-and-scaffold behaviour + +> **Why stack/language resolution exists:** `func quickstart` scaffolds into an empty or newly created directory, so it cannot infer the stack from project files. The command resolves the stack from `--stack` or an interactive prompt, then resolves the language from `--language` or a stack-specific prompt when needed. + +--- + +## Proposed Design + +> **Terminology:** **template** is the noun everywhere — user-facing strings, docs, and design. `quickstart` is the current command verb, converging to `func templates`. Implementation types use `Quickstart*` until that rename. + +### Command Tree + +```bash +func quickstart [] [options] # QuickstartCommand (interactive when bare) + func quickstart list [options] # QuickstartListCommand + func quickstart info [--json] # QuickstartInfoCommand +``` + +#### Synopsis + +```bash +func quickstart [] [--stack|-s ] [--language|-l ] [--template|-t ] + [--resource|-r ] [--iac ] [--search ] + [--fetch auto|git|http] [--force] + +Subcommands: + list [--stack] [--language] [--resource] [--iac] [--search] [--json] + info [--json] +``` + +| Option | Description | +| --- | --- | +| `` | Positional argument: target directory (consistent with `func init`, `func new`, `func start`). Defaults to current directory | +| `--stack`, `-s` | Stack to use (`dotnet`, `node`, `python`). When omitted, the command auto-selects the sole installed provider or prompts interactively | +| `--language`, `-l` | Programming language within the selected stack (`csharp`, `javascript`, `typescript`, `python`) | +| `--template`, `-t` | Template id from the manifest — skips template selection | +| `--resource`, `-r` | Filter by trigger/binding resource (`http`, `timer`, `blob`, `eventhub`, `servicebus`, `cosmos`, `sql`, `mcp`, `durable`) | +| `--iac` | Filter by infrastructure-as-code type (`bicep`, `terraform`, `none`) | +| `--search` | Case-insensitive substring filter applied to ids, template names, resource type, infrastructure-as-code type, and descriptions | +| `--fetch` | Controls how template payload is fetched: `auto` (default), `git`, `http` | +| `--force` | Clears the target directory (except `.git`) before scaffolding | +| `--json` | Available on `list` and `info` for machine-readable output | + +> **Thin-command design** — `QuickstartCommand` is a thin shell. All business logic lives in `IQuickstartManifestService`, `IQuickstartScaffolder`, and `IQuickstartProviderResolver`; no catalog, scaffolding, or stack/language resolution logic lives in the command itself. + +**User experience:** + +```bash +# Interactive scaffold — stack is chosen first +$ func quickstart + + Select a stack: + .NET + Node + Python + + # Node currently exposes two manifest languages, so it prompts next: + Select a language: + TypeScript + JavaScript + + Select a template: + HTTP Trigger (TypeScript + AZD + Bicep) + Blob EventGrid Trigger (TypeScript + AZD + Bicep) + ... + + Created http-trigger-typescript-azd in current directory + +# Scaffold into a named folder (created if it doesn't exist) +$ func quickstart ./my-new-api + +# Non-interactive scaffold by exact template id +$ func quickstart ./my-fn --stack python --language python --template http-trigger-python-azd + +# Directory is not empty → error +$ func quickstart ./existing-dir --stack python --language python --template http-trigger-python-azd + Error: The target directory is not empty. Pass --force to overwrite, or choose a different path. + +# Overwrite an existing directory +$ func quickstart ./existing-dir --stack python --language python --template http-trigger-python-azd --force +``` + +**Post-scaffold success banner:** + +Every successful scaffold prints a success line followed by runtime-specific next steps. When a positional `` is supplied, step 1 is `cd `. When scaffolding into the current directory, the `cd` step is omitted and remaining steps are renumbered. + +| Stack / Language | Next Steps | +| --- | --- | +| Python | `pip install -r requirements.txt`, verify `local.settings.json`, `func run` | +| TypeScript | `npm install`, verify `local.settings.json`, `func run` | +| JavaScript | `npm install`, verify `local.settings.json`, `func run` | +| .NET / C# | verify `local.settings.json`, `func run` | + +```bash +# Filter by stack +$ func quickstart list --stack python + + ID Name Description + ---------------------------------------------------------------- + http-trigger-python-azd HTTP Trigger Python Azure Function with an HttpTrigger. + timer-trigger-python-azd Timer Trigger Python Azure Function with a TimerTrigger. + ... + +# Filter by stack + language + keyword +$ func quickstart list --stack node --language typescript --search blob + +# JSON output for automation +$ func quickstart list --stack python --json + +# Non-interactive scaffold by exact template id +$ func quickstart ./my-fn --stack python --language python --template http-trigger-python-azd + +# Show detailed info about a specific template +$ func quickstart info http-trigger-python-azd + + HTTP Trigger + + ID: http-trigger-python-azd + Language: Python + Resource: http + IaC: bicep + Git Ref: refs/tags/v1.0.0 + Repo: https://github.com/Azure-Samples/functions-quickstart-python-http-azd + Path: . +``` + +--- + +### New Services + +#### `IQuickstartManifestService` + +```csharp +// src/Abstractions/Quickstart/IQuickstartManifestService.cs +public interface IQuickstartManifestService +{ + public Task GetManifestAsync(CancellationToken cancellationToken = default); +} +``` + +`QuickstartManifestService` implementation: + +| Behaviour | Detail | +| ----------- | -------- | +| Primary URL | `https://cdn.functions.azure.com/public/templates-manifest/manifest.json` | +| Cache location | `~/.azure-functions/quickstart/manifest.json` + `~/.azure-functions/quickstart/manifest-meta.json` | +| Cache key | ETag from the CDN response | +| TTL | 24 hours | +| Fresh cache | Return the cached manifest without a network call | +| Stale cache | Revalidate with `If-None-Match`; on `304`, refresh cache metadata and keep the cached body | +| Network / parse failure | Use a stale cached manifest if available; otherwise fail | +| Trusted-org filter | Drop any template entry whose `repositoryUrl` owner is not in `{"azure", "azure-samples", "microsoft"}` | +| Manifest validation | Drop entries missing required fields (`id`, `displayName`, `language`, `resource`, `repositoryUrl`, `folderPath`, `gitRef`) | +| `gitRef` policy | Only tag refs are accepted: `refs/tags/*` | +| Override | `FUNC_QUICKSTART_MANIFEST_URL` overrides the primary source. Accepts an `https://` URL, a `file://` URI, or an absolute local file path | +| Serialization | `QuickstartManifestEnvelope` + `QuickstartJsonContext` handle JSON parsing | + +#### Release & Compliance Model + +The manifest source of truth lives in the [`Azure/azure-functions-templates`](https://github.com/Azure/azure-functions-templates) repository under `Functions.Templates/Template-Manifest/`. + +| Aspect | Detail | +| --- | --- | +| Source repo | `Azure/azure-functions-templates` — manifest changes require a PR and team approval | +| Immutability | Every template entry pins a `gitRef` (tag or commit SHA). No branch references allowed — content is frozen at a reviewed point-in-time | +| Staging | Staging CDN at `https://cdn-staging.functions.azure.com/staging/templates-manifest/manifest.json`. Used for pre-release validation. CLI can target it via `FUNC_QUICKSTART_MANIFEST_URL` | +| Production | Production CDN at `https://cdn.functions.azure.com/public/templates-manifest/manifest.json`. Updated only after staging validation passes | +| Promotion flow | PR merged → CI validates schema + smoke-tests each template's `gitRef` is resolvable → published to staging → manual promotion to production | +| Testing locally | Set `FUNC_QUICKSTART_MANIFEST_URL=file:///path/to/local/manifest.json` or point to staging CDN; schema must match | + +#### Runtime-to-Language Mapping + +The command resolves a **stack** first, then resolves a manifest `language` within that stack. + +| Stack prompt / `--stack` | `--language` value(s) | Manifest `language` | Notes | +| --- | --- | --- | --- | +| `.NET` / `dotnet` | `csharp` | `CSharp` | Current implementation exposes C# templates via `DotNetQuickstartProvider` | +| `Node` / `node` | `typescript` | `TypeScript` | Node templates can prompt between TypeScript and JavaScript when both are present | +| `Node` / `node` | `javascript` | `JavaScript` | Direct flag bypasses the language prompt | +| `Python` / `python` | `python` | `Python` | Single-language provider; auto-selects when stack is resolved | + +When a stack has only one available manifest language, the command auto-selects it. When a stack has multiple available languages, interactive mode prompts and non-interactive mode requires `--language`. + +#### `IQuickstartScaffolder` + +```csharp +// src/Abstractions/Quickstart/IQuickstartScaffolder.cs +public interface IQuickstartScaffolder +{ + public Task ScaffoldAsync( + QuickstartEntry entry, + string targetDirectory, + FetchMode fetchMode, + CancellationToken cancellationToken = default); +} +``` + +`QuickstartScaffolder` implementation: + +- No `ScaffoldResult` type exists in the current implementation; `ScaffoldAsync` returns `Task` and signals failures via exceptions. +- `QuickstartScaffolder` validates the template entry, resolves the effective `FetchMode`, delegates to an `ITemplateFetcher`, removes repo metadata, promotes `folderPath` when needed, and copies files into the target directory. + +#### Download Strategy (`--fetch auto|git|http`) + +| Value | Implementation | Behaviour | +| --- | --- | --- | +| `auto` **(default)** | `IFetchModeResolver` / `FetchModeResolver` | Probe for git; use `git` when available, otherwise `http` | +| `git` | `GitTemplateFetcher` | `git init` + `remote add` + explicit tag fetch + detached checkout; verifies tag integrity | +| `http` | `HttpTemplateFetcher` | Download `https://github.com///archive/.zip`, extract, then unwrap the archive root | + +#### Download Details + +| Concern | Detail | +| ---------- | -------- | +| Whole-template fetch | Both fetchers download the repository at the pinned tag, then `QuickstartScaffolder` promotes `folderPath` if the template lives in a subfolder | +| Fetch mode resolution | `auto` chooses once up front; explicit `git` and `http` do not silently switch modes | +| Metadata cleanup | Remove `.git/` and `.github/` after fetch | +| Target copy | Copy extracted files into the target directory after validation and subfolder promotion | +| Trusted repo policy | Repository URL must be `https://github.com/...` and belong to an allowed organization | +| Tag policy | `gitRef` must be `refs/tags/*`; branch refs are rejected | + +#### Git Process Requirements + +All git child-process invocations must satisfy: + +| Requirement | Implementation | +| --- | --- | +| **Process abstraction** | Wrap behind `IGitRunner` / `GitRunner`; tests use `FakeGitRunner` | +| **Argument-array invocation** | Pass arguments via `ProcessStartInfo.ArgumentList`, never a shell-concatenated string | +| **Exact tag fetch** | Fetch the explicit tag refspec; do not rely on branch names or default branches | +| **Tag integrity** | Verify the ref is an annotated tag and that the checked-out commit matches the tag target | +| **Non-interactive** | Disable all credential prompts | +| **Timeout / cancellation** | Honour the command `CancellationToken` and kill the process tree on cancellation | + +#### Repository Metadata Cleanup + +After any successful fetch, the scaffolder removes: + +- `/.git/` -- never useful for a fresh template project +- `/.github/` -- CI workflows belong to the source repo, not the user's project + +Read-only attributes (set by git on Windows for pack files) are cleared before deletion. + +--- + +### User Experience Flow + +What the user sees in the terminal across every path. + +```mermaid +flowchart TD + START(["$ func quickstart [flags]"]) --> STACK["Resolve stack
--stack or prompt"] + STACK --> FETCH["Fetching template catalog..."] + FETCH --> LANG["Resolve language
--language or prompt"] + LANG --> TEMPLATE["Resolve template
--template or prompt"] + TEMPLATE --> TARGET["Validate target directory
+ --force"] + TARGET --> WRITE["Fetch and write template files"] + WRITE --> SUCCESS(["Created selected template in target directory"]) +``` + +--- + +### Execution Flow + +```mermaid +flowchart TD + A(["func quickstart invoked"]) --> B["Resolve stack
--stack or prompt"] + B --> C["Fetch template catalog
CDN + ETag cache"] + C --> D["Resolve language
--language or prompt"] + D --> E["Select template
--template or filtered prompt"] + E --> F["Resolve target directory
+ --force rules"] + F --> G["Scaffold template
git or http fetch"] + G --> H(["Print next steps"]) +``` + +#### Manifest Cache Flow + +```mermaid +flowchart TD + A(["Fetch catalog"]) --> B{"Fresh cache exists?"} + B -- yes --> DONE(["Return cached manifest"]) + B -- no --> C["GET manifest.json
If-None-Match: stored ETag"] + C --> D{"CDN response"} + D -- "304 Not Modified" --> E["Refresh cache metadata"] + D -- "200 OK" --> F["Replace cache
store new ETag + body"] + D -- failure --> G{"Stale cache exists?"} + G -- yes --> H(["Return stale cached manifest"]) + G -- no --> ERR(["Fail: no catalog available"]) + E --> DONE + F --> DONE +``` + +#### Template Download Strategy + +```mermaid +flowchart TD + A(["ScaffoldAsync called"]) --> B["Validate template entry +URL + tag ref + folderPath"] + B --> C{"Fetch mode"} + C -- auto --> D["Resolve to git or http"] + C -- git --> E["GitTemplateFetcher +fetch exact tag"] + C -- http --> F["HttpTemplateFetcher +download tag archive"] + D --> E + D --> F + E --> G["Remove .git / .github"] + F --> G + G --> H["Promote folderPath if needed"] + H --> I(["Copy files to target directory"]) +``` + +--- + +### New Commands + +#### `QuickstartCommand` + +> **Thin-command principle** — `QuickstartCommand` must not contain catalog, scaffolding, or stack/language-resolution logic itself. All of that lives in `IQuickstartManifestService`, `IQuickstartScaffolder`, and `IQuickstartProviderResolver`. + +```csharp +// src/Func/Commands/Quickstart/QuickstartCommand.cs +internal sealed class QuickstartCommand : FuncCliCommand +{ + public Option StackOption { get; } = new("--stack", "-s") + { + Description = QuickstartMessages.StackOptionDescription + }; + + public Option LanguageOption { get; } = new("--language", "-l") + { + Description = "The programming language" + }; + + public Option TemplateOption { get; } = new("--template", "-t") + { + Description = "Template ID from the manifest (e.g. http-trigger-python-azd) — skips template selection prompts" + }; + + public Option ForceOption { get; } = new("--force") + { + Description = "Scaffolds even when the target folder isn't empty. Clears the folder (except .git) before scaffolding." + }; + + public QuickstartCommand( + QuickstartListCommand listCommand, + QuickstartInfoCommand infoCommand, + IInteractionService interaction, + IQuickstartProviderResolver resolver, + IQuickstartManifestService manifestService, + IQuickstartScaffolder scaffolder, + IEnumerable providers) + : base("quickstart", "Browse and scaffold complete function apps from the Azure Functions template catalog.") + { + AddPathArgument(); + Subcommands.Add(listCommand); + Subcommands.Add(infoCommand); + // ... store services and add options + } +} +``` + +#### `QuickstartListCommand` + +```csharp +// src/Func/Commands/Quickstart/QuickstartListCommand.cs +internal sealed class QuickstartListCommand : FuncCliCommand +{ + public Option StackOption { get; } = new("--stack", "-s"); + public Option LanguageOption { get; } = new("--language", "-l"); + public Option ResourceOption { get; } = new("--resource", "-r"); + public Option IacOption { get; } = new("--iac"); + public Option SearchOption { get; } = new("--search"); + public Option JsonOption { get; } = new("--json"); + + protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + // 1. Resolve stack → provider + // 2. Fetch catalog via IQuickstartManifestService + // 3. Resolve language via IQuickstartProviderResolver + // 4. Filter QuickstartManifest entries and render a table or JSON + } +} +``` + +#### `QuickstartInfoCommand` + +```csharp +// src/Func/Commands/Quickstart/QuickstartInfoCommand.cs +internal sealed class QuickstartInfoCommand : FuncCliCommand +{ + public Argument TemplateIdArgument { get; } = new("id") + { + Description = "Template ID from the manifest (e.g. http-trigger-python-azd). Use 'func quickstart list' to see available IDs." + }; + + public Option JsonOption { get; } = new("--json") + { + Description = "Emit machine-readable JSON instead of formatted output." + }; + + protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + // 1. Fetch catalog + // 2. Find the matching QuickstartEntry by id + // 3. Use IQuickstartProviderResolver to map manifest language to display language + // 4. Render formatted output or JSON + } +} +``` + +--- + +### `BuiltInCommands` Change + +```csharp +// src/Func/Hosting/BuiltInCommands.cs +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); +``` + +Quickstart services are registered outside `BuiltInCommands` in the host composition root: + +```csharp +// src/Func/Hosting/CliHostFactory.cs +builder.Services.AddQuickstartScaffolder(); +builder.Services.AddQuickstartManifest(); +``` + +Stack-specific providers are registered by workloads, not by `BuiltInCommands`: + +```csharp +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +``` + +--- + +### File Layout + +```text +src/ + Abstractions/ + Quickstart/ + FetchMode.cs + IQuickstartManifestService.cs + IQuickstartProvider.cs + IQuickstartScaffolder.cs + QuickstartConstants.cs + QuickstartEntry.cs + QuickstartManifest.cs + Func/ + Commands/ + Quickstart/ + QuickstartCommand.cs + QuickstartInfoCommand.cs + QuickstartListCommand.cs + QuickstartMessages.cs + Hosting/ + BuiltInCommands.cs + Quickstart/ + FetchModeResolver.cs + GitRunner.cs + GitRunnerException.cs + GitTemplateFetcher.cs + HttpTemplateFetcher.cs + IFetchModeResolver.cs + IGitRunner.cs + IManifestCache.cs + IQuickstartProviderResolver.cs + ITemplateFetcher.cs + ManifestCache.cs + ManifestCacheMeta.cs + QuickstartJsonContext.cs + QuickstartManifestEnvelope.cs + QuickstartManifestOptions.cs + QuickstartManifestService.cs + QuickstartProviderResolver.cs + QuickstartRegistration.cs + QuickstartScaffolder.cs + QuickstartScaffolderRegistration.cs + QuickstartUrlValidator.cs + Workloads/ + Stacks/ + DotNet/ + DotNetQuickstartProvider.cs + Node/ + NodeQuickstartProvider.cs + Python/ + PythonQuickstartProvider.cs + +test/Func.Tests/ + Commands/ + Quickstart/ + QuickstartCommandTests.cs + QuickstartInfoCommandTests.cs + QuickstartListCommandTests.cs + QuickstartTestHelpers.cs + Quickstart/ + FakeGitRunner.cs + HttpTemplateFetcherTests.cs + QuickstartManifestServiceTests.cs + QuickstartManifestTests.cs + QuickstartScaffolderTests.cs + QuickstartUrlValidatorTests.cs +``` + +--- + +## `IInteractionService` Prompt Support + +`IInteractionService` now includes selection-prompt methods added during the quickstart handler work (PR 4): + +```csharp +Task PromptForSelectionAsync(string title, IEnumerable choices, CancellationToken cancellationToken = default); +Task> PromptForMultiSelectionAsync(string title, IEnumerable choices, CancellationToken cancellationToken = default); +Task PromptForInputAsync(string prompt, string? defaultValue = null, CancellationToken cancellationToken = default); +``` + +The `SpectreInteractionService` implementation backs these with `Spectre.Console.SelectionPrompt` with `EnableSearch()`, giving the user: + +- **Arrow keys** (↑↓) to move through the list +- **Type any character** to live-filter the visible items; backspace to clear +- Both work simultaneously — filter then navigate within the filtered set + +Respects `IsInteractive` (falls back to numbered list in CI/non-TTY). + +**Why this matters vs. v4 `func new`:** The current stable CLI uses raw `Console.ReadKey()` arrow navigation which hangs or crashes in agent, CI, and piped contexts because there is no TTY. The `IInteractionService.IsInteractive` check ensures the new design degrades gracefully: in a non-TTY context the prompt renders as a numbered list that accepts stdin, and when all flags are supplied no prompt is shown at all. + +> **Note on `--search` vs. in-prompt search:** These are complementary. `--search blob` on `func quickstart` pre-filters the list *before* the prompt is shown (useful for scripting or reducing noise when you already know the keyword). The `EnableSearch()` interactive filter lets users type to narrow down after the prompt appears. Both apply the same case-insensitive substring matching over template metadata. + +--- + +## Error Messaging + +The implementation centralises shared user-facing strings in `QuickstartMessages.cs`. + +| Constant | Value | Used for | +| --- | --- | --- | +| `FetchingCatalogStatus` | `Fetching template catalog...` | Status spinner while the catalog is loading | +| `TemplateNotFoundHint` | `Run 'func quickstart list' to see available templates.` | Hint appended when a template id is missing | +| `DirectoryNotEmptyError` | `The target directory is not empty. Pass --force to overwrite, or choose a different path.` | Error before scaffolding into a non-empty directory | +| `CancelledHint` | `Quickstart cancelled. The directory was not modified.` | Hint shown when an interactive `--force` confirmation is declined | +| `NoMatchingFiltersError` | `No templates match the specified filters. Run 'func quickstart list' to see all available templates, or adjust --resource, --iac, or --search.` | `func quickstart` error after filtering | +| `MultipleMatchesError` | `Multiple templates match. Re-run with --template to select one, or add filters (--resource, --iac, --search) to narrow the results.` | `func quickstart` error when filters still produce multiple matches | +| `NoMatchingFiltersWarning` | `No templates match the specified filters.` | `func quickstart list` warning | + +### Exception Strategy + +Following the Core Tools vnext error handling convention: + +- **Services** (`QuickstartManifestService`, `QuickstartScaffolder`) throw specific framework/domain exceptions at the failure site. +- **Commands** (`QuickstartCommand`, `QuickstartListCommand`, `QuickstartInfoCommand`) are the boundary. Each catches the specific exceptions it expects from its direct service calls and wraps them in `GracefulException(message, isUserError: true)`, preserving the inner exception. The `try` is narrow — one service call per `try` block. +- **Anything unexpected** is not caught by the command — it surfaces as an unhandled exception with a stack trace (runtime bug). + +--- + +## Security Envelope + +| Concern | Mitigation | +| --- | --- | +| Malicious manifest entry pointing to attacker repo | URL allow-list: HTTPS scheme + `github.com` host + trusted org (`azure`, `azure-samples`, `microsoft`). Filtered in manifest client, re-checked in scaffolder | +| Zip slip / path traversal in archive | Every entry's resolved absolute path must start with `Path.GetFullPath(targetPath)` + `DirectorySeparator`. Mismatch -> `InvalidOperationException` | +| Flag injection via manifest values | Argv-array invocation + `--` end-of-options sentinel + reject `gitRef`/`folderPath` starting with `-` | +| Command-line injection via folderPath | `..` segments rejected | +| Interactive git credential prompts hanging the CLI | Four env vars set to disable every prompt path (see Git Process Requirements) | +| Runaway git process | 60s timeout with process-tree kill | + +**Trusted GitHub organizations** -- hard-coded allow-list in `QuickstartUrlValidator`: + +- `azure` +- `azure-samples` +- `microsoft` + +--- + +## Manifest Source + +| Aspect | Detail | +| --- | --- | +| Primary | `https://cdn.functions.azure.com/public/templates-manifest/manifest.json` | +| Override | `FUNC_QUICKSTART_MANIFEST_URL` env var — accepts an `https://` URL, a `file://` URI, or an absolute local file path | +| Cache directory | `~/.azure-functions/quickstart/` | +| Caching | ETag-based, with a 24-hour TTL and stale-cache fallback | +| Entry policy | Template entries must be tag-pinned (`refs/tags/*`) before they are eligible for scaffolding | + +The implementation reads the CDN manifest by default, honours a local or staging override via `FUNC_QUICKSTART_MANIFEST_URL`, and falls back only to the local cache when the network path is unavailable. + +--- + +## Exit Codes + +| Code | Condition | +| --- | --- | +| 0 | Successful scaffold / list / info | +| 1 | User error (graceful): unknown template, non-empty target, empty filter result, manifest fetch failure, `--fetch git` with git missing, git/http fetch failure | +| Non-zero (uncaught) | Unexpected runtime bug -- stack trace surfaces | + +--- + +## Future Work + +- **GPG tag verification** -- when the manifest moves to `gitRef`-pinned entries and Azure-Samples/trusted orgs begin GPG-signing release tags, add an optional `git verify-tag` step. Default-on once signing is reliable; needs UX for unsigned/invalid, GPG-not-installed handling, public-key bundling or trust-on-first-use. Both `gitRef` enforcement and GPG verification become active together -- one without the other provides no guarantee. + +--- + +## Open Questions + +- [ ] **Default language** — today the command auto-selects a language when the chosen stack has a single manifest language, and requires `--language` or an interactive prompt when the stack has multiple languages. Should there still be a persisted or inferred default for repeat users? Options: + - **Detect from existing files** — run `ProjectDetector.DetectStackAndLanguage(targetDir)` on the resolved target before prompting; if detectable files exist (e.g. `requirements.txt`, `package.json`, `*.csproj`), use the detected language as the default; if target is empty, fall through to prompt or error. Already exists at `src/Func/Commands/ProjectDetector.cs`. This becomes more useful now that `--force` can clear an existing target directory before scaffolding. + - **Persist last-used language** — store in `~/.azure-functions//preferences.json` after first scaffold; use as default on next run + - **Explicit `func config set default-language `** — user opts in deliberately; no implicit magic + - **Keep required** — simplest; interactive prompt is low friction anyway; scripts always supply it + - Consider: does a persisted or detected default interact badly with non-TTY/CI (wrong language silently used)? Detection from files is safer than persistence — it's scoped to the target dir, not global state. +- [ ] **`--search` empty-result behaviour in interactive mode** — when the in-prompt Spectre filter (not the `--search` flag) narrows to zero results, Spectre renders an empty list. Should the command detect this and show a message, or let the user backspace to widen the filter naturally? +- [ ] **`func quickstart` vs `func new` interactive fallback** — should bare `func new` (when no workloads installed) redirect to `func quickstart` interactive flow, rather than showing a hint? Or keep the hint to encourage workload installation? +- [ ] **`--output json` on `func quickstart list`** — useful for tooling. Worth adding now or later? + +--- + +## Appendix + +### References + +- `fnx init` orchestration: `func-emulate/fnx/lib/init.js` (internal) +- Manifest fetch + cache: `func-emulate/fnx/lib/init/manifest.js` (internal) +- Scaffold (git + zip): `func-emulate/fnx/lib/init/scaffold.js` (internal) +- Interactive prompts: `func-emulate/fnx/lib/init/prompts.js` (internal) +- Runtime + trigger constants: `func-emulate/fnx/templates-mcp/src/templates.ts` (internal) +- vnext `NewCommand`: `src/Func/Commands/NewCommand.cs` +- vnext `WorkloadCommand` (pattern): `src/Func/Commands/Workload/WorkloadCommand.cs` +- vnext `IInteractionService`: `src/Func/Console/IInteractionService.cs` + +### vnext Architecture (What Already Exists) + +The vnext branch has been rebuilt from scratch on System.CommandLine. Key elements relevant to this design: + +| Type | Location | Role | +| --- | --- | --- | +| `FuncCliCommand` | `src/Func/Commands/FuncCliCommand.cs` | Base class for all commands. Subcommands injected via constructor. | +| `IBuiltInCommand` | `src/Func/Hosting/IBuiltInCommand.cs` | Marker interface — names are reserved, registered in `BuiltInCommands` | +| `NewCommand` | `src/Func/Commands/NewCommand.cs` | `func new` — delegates to workloads for per-function scaffolding | +| `IInteractionService` | `src/Func/Console/IInteractionService.cs` | Spectre.Console abstraction. Has `IsInteractive`, `WriteTable`, `ShowStatusAsync`, prompt methods. | +| `IQuickstartManifestService` | `src/Abstractions/Quickstart/IQuickstartManifestService.cs` | Fetches and caches the template catalog. | +| `IQuickstartScaffolder` | `src/Abstractions/Quickstart/IQuickstartScaffolder.cs` | Downloads and writes a selected template into the target directory. | +| `IQuickstartProviderResolver` | `src/Func/Quickstart/IQuickstartProviderResolver.cs` | Resolves stack and language selection across installed providers. | +| `IQuickstartProvider` | `src/Abstractions/Quickstart/IQuickstartProvider.cs` | Stack contribution point for quickstart participation. | +| `BuiltInCommands` | `src/Func/Hosting/BuiltInCommands.cs` | Registers all built-in commands with DI | +| `WorkloadCommand` | `src/Func/Commands/Workload/WorkloadCommand.cs` | Pattern reference — parent command with subcommands injected via constructor | +| `WorkloadListCommand` | `src/Func/Commands/Workload/WorkloadListCommand.cs` | Pattern reference — subcommand using `IInteractionService.WriteTable` | + +**`WorkloadCommand` pattern** (to follow exactly): + +- Parent command receives subcommands via constructor, calls `Subcommands.Add()` +- Each subcommand registered as its own concrete singleton in `BuiltInCommands`, **not** as `FuncCliCommand` (to prevent top-level registration) +- Parent registered as `FuncCliCommand` so it appears at top level + +### fnx init — Source Implementation + +| Component | Location | Role | +| --- | --- | --- | +| **Init orchestration** | `fnx/lib/init.js` | 9-step flow: manifest → prompts → download → scaffold | +| **Manifest fetching** | `fnx/lib/init/manifest.js` | CDN fetch, ETag caching, 24h TTL, bundled fallback | +| **Interactive prompts** | `fnx/lib/init/prompts.js` | Arrow-key selection via readline raw mode; numbered fallback for CI | +| **Scaffolding** | `fnx/lib/init/scaffold.js` | Historical source inspiration for template download and safety checks; the current implementation uses `GitTemplateFetcher` and `HttpTemplateFetcher` | +| **Template definitions** | `fnx/templates-mcp/src/templates.ts` | `SUPPORTED_RUNTIMES`, trigger priority list, `TemplateParameter` | + +**`fnx init` flow (condensed):** + +1. Fetch manifest from `https://cdn.functions.azure.com/public/templates-manifest/manifest.json` (ETag-cached; bundled fallback on failure) +2. Prompt: **runtime** (Python / Node.js / .NET / Java / PowerShell) +3. For Node.js: prompt **language** (TypeScript / JavaScript) +4. Filter manifest by runtime → prompt **trigger** (priority-sorted: HTTP, Blob, Timer, Queue, …) +5. Prompt **project name** +6. Download template: `git clone --sparse` (if git available) else GitHub zip API +7. Generate `host.json`, `local.settings.json`, `.gitignore`; print success banner + +**Security primitives to port:** + +- `safePath(targetDir, fileName)` — resolve + prefix check to prevent path traversal +- `filterTrustedTemplates()` — allowlist of `azure`, `azure-samples`, `microsoft` GitHub orgs +- URL scheme validation before any network call diff --git a/proposed/templates-e2e.md b/proposed/templates-e2e.md deleted file mode 100644 index 9a759ab01..000000000 --- a/proposed/templates-e2e.md +++ /dev/null @@ -1,988 +0,0 @@ -# Core Tools vNext: `func quickstart` — CDN-Backed Template Discovery - -**Author:** manvkaur -**Date:** 2026-05-18 -**Status:** Draft -**Work Item:** TBD - ---- - -## Command Verb — Under Discussion - -> **This decision drives the rest of the document.** The verb chosen here determines the command name, class names, CLI surface, and whether it collides with the in-flight templates workload. - -Development is proceeding with **`quickstart`** as the working name. This is a **top-level command** (`func quickstart`) delivered as a workload — not a subcommand of `func new`. The final verb is still under discussion. - -| Candidate | Full Command | Subcommands | Pros | Cons | -| --- | --- | --- | --- | --- | -| **`quickstart`** (working name) | `func quickstart` | `list`, `info` | Clear intent; aligns with Azure Samples naming (`*-quickstart-*-azd`); top-level; no collision with templates workload | Longer to type; may imply "beginner-only" and limit room for advanced samples | -| **`templates`** | `func templates` | `list`, `info` | Industry standard (Vercel, bolt.new, AZD Templates, AI Template Gallery all use "templates"); VS Code extension already calls it "Template Gallery"; doesn't limit scope to quickstarts | Collides conceptually with in-flight templates workload that provides parameterized per-function scaffolding | -| **`sample`** | `func sample` | `list`, `info` | These are sample repos; no overloading of "template" | Might imply "not production-ready" | -| **`bootstrap`** | `func bootstrap` | `list`, `info` | Accurate for "set up a project from scratch" | Less discoverable; not a common Azure CLI verb | - -> **Impact of verb change:** The verb is used in the command name, class names, service names, and all CLI examples throughout this document. The design is otherwise identical regardless of verb chosen. Swapping the verb is a find-and-replace — no architectural changes. - ---- - -## Table of Contents - -- [Command Verb — Under Discussion](#command-verb--under-discussion) -- [Problem Statement](#problem-statement) -- [Goals / Non-Goals](#goals--non-goals) -- [Proposed Design](#proposed-design) - - [Command Tree](#command-tree) - - [New Services](#new-services) - - [ITemplateManifestService](#itemplatemanifestservice) - - [Release & Compliance Model](#release--compliance-model) - - [Runtime-to-Language Mapping](#runtime-to-language-mapping) - - [ITemplateFunctionScaffolder](#itemplatefunctionscaffolder) - - [Download Strategy](#download-strategy---fetch-autogithttp) - - [Git Process Requirements](#git-process-requirements) - - [Repository Metadata Cleanup](#repository-metadata-cleanup) - - [User Experience Flow](#user-experience-flow) - - [Execution Flow](#execution-flow) - - [Manifest Cache Flow](#manifest-cache-flow) - - [Template Download Strategy](#template-download-strategy) - - [New Commands](#new-commands) - - [QuickstartCommand](#quickstartcommand) - - [QuickstartListCommand](#quickstartlistcommand) - - [QuickstartInfoCommand](#quickstartinfocommand) - - [BuiltInCommands Change](#builtincommands-change) - - [File Layout](#file-layout) -- [IInteractionService Prompt Gap](#iinteractionservice-prompt-gap) -- [Error Messaging](#error-messaging) -- [Security Envelope](#security-envelope) -- [Manifest Source](#manifest-source) -- [Exit Codes](#exit-codes) -- [Future Work](#future-work) -- [Open Questions](#open-questions) -- [Appendix](#appendix) - - [References](#references) - - [vnext Architecture](#vnext-architecture-what-already-exists) - - [fnx init — Source Implementation](#fnx-init--source-implementation) - ---- - -## Problem Statement - -In vnext, workload templates solve the per-function scaffolding story: `func new` adds a single function file to an existing project using templates shipped in workload packages. A complementary scenario is **complete-app scaffolding** — downloading a fully runnable function app (function code, host.json, local.settings.json, IaC configuration, and all dependencies) in a single step. Runtime-specific setup (venv, npm install, dotnet restore) is handled separately by the developer or by `func start`. - -Today, complete app templates live in GitHub repos (e.g. Azure-Samples) and developers discover them through docs, portal links, or search. This design brings that discovery and scaffolding workflow into the CLI itself. - -This design introduces `func quickstart`: a built-in command that downloads **complete, immediately runnable function app templates** directly from GitHub, complementing the workload-driven `func new` experience. Templates are discovered via a live manifest hosted on the Azure Functions CDN. Adding a new template is as simple as publishing a GitHub repo and adding an entry to the manifest — no CLI or workload release required. The manifest is versioned independently; the CLI picks up new templates automatically on the next manifest refresh. - -How this complements workload templates: - -- **Decoupled template lifecycle** — app templates live as standalone GitHub repos; they can be added, updated, and iterated on independently of both CLI and workload package releases -- **Built-in discovery** — `func quickstart list` and the interactive flow give developers a way to find and filter available app templates without leaving the CLI - ---- - -## Goals / Non-Goals - -### Goals - -- Add `func quickstart` as a top-level command on the vnext branch -- Download and create a **complete function app** from a GitHub template — all function code, config, and dependencies included -- CDN-backed template manifest with ETag caching (24h TTL) -- `func quickstart list` — non-interactive table of available templates, filterable by `--language`, `--resource`, `--iac`, and keyword (`--search`); `--search` is a **case-insensitive substring match** (not semantic/fuzzy) against `id`, `displayName`, `resource`, `tags`, and `shortDescription` -- `func quickstart` (bare invocation) — interactive flow: worker runtime → (dotnet sub-prompt: C#/F#, Node sub-prompt: JS/TS) → trigger → scaffold; the runtime prompt matches the existing `func new` UX (`.NET`, `Node`, `Python`, `Java`, `Powershell`); trigger selection supports incremental keyword filtering; powered by the existing `IInteractionService` / Spectre.Console already in vnext -- **.NET isolated worker model only** — the manifest `CSharp` and `FSharp` languages map exclusively to the .NET isolated worker model; the in-process model is not supported and will not be added (it is on a deprecation path). Note: no F# templates exist in the manifest today; the sub-prompt will show F# once templates are available -- **Agent and CI friendly** — all v4 `func new` flags preserved (`--language`, `--template`); `--language` accepts runtime names (`python`, `node`, `java`, `dotnet`, `csharp`, `fsharp`, `javascript`, `typescript`, `powershell`); non-TTY falls back to numbered list; `--template` skips all prompts for fully non-interactive use -- Template download via git sparse-checkout or GitHub zip API (no bundling in binary) -- `--path` to specify target directory (absolute or relative); created if absent, accepted if it exists and is empty — error if it exists and is not empty - -### Non-Goals - -- Adding a single function file to an existing project (that is v4 `func new` behaviour — out of scope here) -- Replacing or changing `func init` (separate command, workload-driven) -- Changing the workload model or `IProjectInitializer` interface -- Automatic environment setup (venv, npm install, dotnet restore) — out of scope; belongs in `func start` workflow -- File conflict handling and `--force` flag — **deferred to next iteration of this command**; v1 requires target directory to be empty - -> **Why `--language` is required:** Workload-driven `func new` operates on an existing project and can detect the runtime from project files. `func quickstart` scaffolds into an empty directory, so there is nothing to detect from — `--language` must be supplied explicitly. - ---- - -## Proposed Design - -> **Command keyword note:** `quickstart` is the working name for this top-level command (`func quickstart`). The final verb is still under discussion (see Command Verb section above). - -### Command Tree - -```bash -func quickstart [] [options] # QuickstartCommand (new — interactive flow when bare) - func quickstart list [options] # QuickstartListCommand (new — table output) - func quickstart info # QuickstartInfoCommand (new — detailed template info) -``` - -#### Synopsis - -```bash -func quickstart [] [--language|-l ] [--template|-t ] - [--resource|-r ] [--iac ] [--search|-s ] - [--fetch auto|git|http] - -Subcommands: - list [--language] [--resource] [--iac] [--search] - info -``` - -| Option | Description | -| --- | --- | -| `` | Positional argument: target directory (consistent with `func init`, `func new`, `func start`). Defaults to current directory | -| `--language`, `-l` | Worker runtime or language (`python`, `node`, `java`, `dotnet`, `csharp`, `fsharp`, `javascript`, `typescript`, `powershell`) | -| `--template`, `-t` | Template id from manifest -- skips all prompts | -| `--resource`, `-r` | Filter by trigger/binding resource (`http`, `timer`, `blob`, `eventhub`, `servicebus`, `cosmos`, `sql`, `mcp`, `durable`) | -| `--iac` | Filter by infrastructure-as-code type (`bicep`, `terraform`, `none`) | -| `--search`, `-s` | Case-insensitive substring filter applied to IDs, display names, resources, tags, descriptions. Empty result -> exit 1 with guidance | -| `--fetch` | Controls how template payload is fetched: `auto` (default), `git`, `http`. See [Download Strategy](#download-strategy---fetch-autogithttp) | - -> **Thin-command design** — `QuickstartCommand` is a thin shell. All business logic lives in `ITemplateManifestService` and `ITemplateFunctionScaffolder`; no scaffolding, manifest, or prompt logic in the command itself. - -**User experience:** - -```bash -# Interactive scaffold — arrow keys to navigate, type to live-filter -$ func quickstart - - Use the up/down arrow keys to select a worker runtime: - .NET - Node - Python - Java - Powershell - - # User picks dotnet → sub-prompt for language: - Use the up/down arrow keys to select a language: - C# - F# # (shown but no templates available yet) - - # User picks Node → sub-prompt for language: - Use the up/down arrow keys to select a language: - JavaScript - TypeScript - - # User types "bl" — list narrows live as they type, arrow keys still work: - Use the up/down arrow keys to select a template: bl - Blob EventGrid Trigger (TypeScript + AZD + Bicep) - - # User clears search, navigates with arrows: - Use the up/down arrow keys to select a template: - HTTP Trigger (TypeScript + AZD + Bicep) - Timer Trigger (TypeScript + AZD + Bicep) - Blob EventGrid Trigger (TypeScript + AZD + Bicep) - ... - - Created http-trigger-typescript-azd in current directory - - Next steps: - 1. npm install - 2. npm run build - 3. func start - -# Interactive scaffold into a named folder (created if it doesn't exist) -$ func quickstart --path ./my-new-api - - Use the up/down arrow keys to select a worker runtime: ... - - # (after selection) - Created in ./my-new-api - - Next steps: - 1. cd ./my-new-api - 2. - 3. func start - -# Scaffold into a specific folder (created if absent; also accepted if it exists and is empty) -$ func quickstart --template blob-eventgrid-trigger-python-azd --path ./my-fn -$ func quickstart --template blob-eventgrid-trigger-python-azd --path /home/user/projects/my-fn - -# Directory is not empty → error -$ func quickstart --template http-trigger-python-azd --path ./existing-dir - Error: Target directory './existing-dir' is not empty. - Use an empty directory or a new --path. - -# Network failure — CDN unreachable (manifest fetch) -$ func quickstart - Error: This feature requires a network connection to fetch the template catalog. - Please check your connection and try again. - -# Network failure — GitHub unreachable (template download) -$ func quickstart --template http-trigger-python-azd - Error: Unable to reach GitHub to download the template. - Please check your network connection and try again. - -# Fully non-interactive: --template skips all prompts -$ func quickstart --language python --template http-trigger-python-azd - Created http-trigger-python-azd in current directory - - Next steps: - 1. python -m venv .venv - 2. .venv\Scripts\activate # macOS/Linux: source .venv/bin/activate - 3. pip install -r requirements.txt - 4. func start -``` - -**Post-scaffold success banner:** - -Every successful scaffold prints a success line followed by runtime-specific next steps. When `--path` is supplied, step 1 is `cd `. When scaffolding into the current directory, the `cd` step is omitted and remaining steps are renumbered. - -| Runtime | Next Steps | -| --- | --- | -| Python | `python -m venv .venv`, `.venv\Scripts\activate` (Windows) or `source .venv/bin/activate` (macOS/Linux), `pip install -r requirements.txt`, `func start` — the CLI detects the platform and shows the correct activation command | -| TypeScript | `npm install`, `npm run build`, `func start` | -| JavaScript | `npm install`, `func start` | -| .NET (isolated) | `dotnet restore`, `func start` | -| Java | `mvn clean package`, `func start` | -| PowerShell | `func start` (no dependency step) | - -```bash -# Filter by language — Language column omitted (redundant) -$ func quickstart list --language python - - Id Resource IaC - ──────────────────────────────────────────────────────── - http-trigger-python-azd http bicep - timer-trigger-python-azd timer bicep - blob-eventgrid-trigger-python-azd blob bicep - eventhub-trigger-python-azd eventhub bicep - servicebus-trigger-python-azd servicebus bicep - ... - -# Filter by resource — Resource column omitted (redundant) -$ func quickstart list --resource http - - Id Language IaC - ──────────────────────────────────────────────────────── - http-trigger-csharp-azd CSharp bicep - http-trigger-python-azd Python bicep - http-trigger-typescript-azd TypeScript bicep - http-trigger-javascript-azd JavaScript bicep - ... - -# Filter by iac — IaC column omitted (redundant) -$ func quickstart list --iac bicep - - Id Resource Language - ──────────────────────────────────────────────────────────── - http-trigger-csharp-azd http CSharp - timer-trigger-python-azd timer Python - ... - -# Keyword search — all columns shown (no filter to omit) -$ func quickstart list --search blob - - Id Resource Language IaC - ──────────────────────────────────────────────────────────────────────── - blob-eventgrid-trigger-csharp-azd blob CSharp bicep - blob-eventgrid-trigger-python-azd blob Python bicep - blob-eventgrid-trigger-typescript-azd blob TypeScript bicep - ... - -# Combined — Language and Resource columns omitted -$ func quickstart list --language python --resource blob - -# Non-interactive scaffold with language + resource -$ func quickstart --language python --resource http - -# Non-interactive scaffold with language + resource + iac -$ func quickstart --language python --resource http --iac bicep - -# Non-interactive scaffold by exact template id (skips language + trigger prompts) -$ func quickstart --template http-trigger-python-azd - -# Show detailed info about a specific template -$ func quickstart info http-trigger-python-azd - - HTTP Trigger (Python + AZD + Bicep) - - Python Azure Function with an HttpTrigger. Runs on Flex Consumption plan - with managed identity authentication and VNet integration for secure - networking. Infrastructure provisioned with Bicep and deployed with azd. - - Language: Python - Resource: http - IaC: bicep - Repository: https://github.com/Azure-Samples/functions-quickstart-python-http-azd - - What's included: - - HTTP trigger function - - Bicep infrastructure files - - Azure Developer CLI (azd) configuration - - VNet integration setup - - VS Code debug configuration - - Local development configuration -``` - ---- - -### New Services - -#### `ITemplateManifestService` - -```csharp -// src/Func/Templates/ITemplateManifestService.cs -internal interface ITemplateManifestService -{ - Task GetManifestAsync(CancellationToken cancellationToken = default); -} -``` - -`TemplateManifestService` implementation (ported from `fnx/lib/init/manifest.js`): - -| Behaviour | Detail | -| ----------- | -------- | -| Primary URL | `https://cdn.functions.azure.com/public/templates-manifest/manifest.json` | -| Backup URL | `https://raw.githubusercontent.com/Azure/azure-functions-templates/dev/Functions.Templates/Template-Manifest/manifest.json` | -| Cache location | `~/.azure-functions//manifest.json` + `/manifest-meta.json` (where `` matches the chosen command name, e.g. `quickstart`) | -| Cache key | ETag from CDN response | -| TTL | 24 hours (refresh ETag check on expiry) | -| 304 Not Modified | Update TTL timestamp, return cached manifest | -| Network failure | Log warning via `IInteractionService.WriteWarning`; fall back to bundled embedded manifest | -| Trusted-org filter | Strip any template whose `repositoryUrl` owner is not in `{"azure", "azure-samples", "microsoft"}` | -| IaC-only filter | Exclude templates where `language` is `ARM`, `Bicep`, or `Terraform` — these are infrastructure-as-code templates, not function app code. The manifest has 4 such entries (e.g. `iac-flex-consumption-bicep`); they are irrelevant to `func quickstart`. | -| Manifest validation | Non-null `templates` array; each entry has `id`, `language`, `resource`, `repositoryUrl`, `folderPath`, `gitRef` | -| `gitRef` resolution | Each template entry **must** include `"gitRef": ""`. The CLI uses `gitRef` as the checkout/download target. This ensures templates are pinned to an immutable, reviewed release — no implicit default-branch resolution | -| URL override | `FUNC_TEMPLATE_MANIFEST_URL` environment variable overrides the primary URL. Used for testing against staging CDN or local dev manifests. Same schema expected | - -#### Release & Compliance Model - -The manifest source of truth lives in the [`Azure/azure-functions-templates`](https://github.com/Azure/azure-functions-templates) repository under `Functions.Templates/Template-Manifest/`. - -| Aspect | Detail | -| --- | --- | -| Source repo | `Azure/azure-functions-templates` — manifest changes require a PR and team approval | -| Immutability | Every template entry pins a `gitRef` (tag or commit SHA). No branch references allowed — content is frozen at a reviewed point-in-time | -| Staging | Staging CDN at `https://cdn-staging.functions.azure.com/staging/templates-manifest/manifest.json`. Used for pre-release validation. CLI can target it via `FUNC_TEMPLATE_MANIFEST_URL` | -| Production | Production CDN at `https://cdn.functions.azure.com/public/templates-manifest/manifest.json`. Updated only after staging validation passes | -| Promotion flow | PR merged → CI validates schema + smoke-tests each template's `gitRef` is resolvable → published to staging → manual promotion to production | -| Testing locally | Set `FUNC_TEMPLATE_MANIFEST_URL=file:///path/to/local/manifest.json` or point to staging CDN, schema must match | - -#### Runtime-to-Language Mapping - -The interactive prompt shows **worker runtimes** (matching `func new` UX), but the manifest uses a `language` field. The mapping between prompt labels, `--language` flag values, and manifest `language` values: - -| Prompt Label | `--language` flag value(s) | Manifest `language` | Notes | -| --- | --- | --- | --- | -| .NET | `dotnet` | `csharp`, `fsharp` | Sub-prompt: C# or F#. Always isolated worker model — in-process is not supported in v5 | -| — | `csharp` | `CSharp` | Direct flag bypasses dotnet sub-prompt | -| — | `fsharp` | `FSharp` | Direct flag bypasses dotnet sub-prompt; no templates available yet | -| Node | `node` | — | Sub-prompt: JavaScript or TypeScript | -| — | `javascript` | `JavaScript` | Direct flag bypasses Node sub-prompt | -| — | `typescript` | `TypeScript` | Direct flag bypasses Node sub-prompt | -| Python | `python` | `Python` | | -| Java | `java` | `Java` | | -| Powershell | `powershell` | `PowerShell` | | - -**Dotnet sub-prompt:** When the user selects ".NET" in the interactive runtime prompt, a follow-up prompt asks for C# or F#. When `--language dotnet` is supplied non-interactively, the default is C#. Use `--language csharp` or `--language fsharp` to skip the sub-prompt entirely. Note: no F# templates exist in the manifest today; selecting F# will show an empty list until templates are published. - -**Node sub-prompt:** When the user selects "Node" in the interactive runtime prompt, a follow-up prompt asks for JavaScript or TypeScript. When `--language node` is supplied non-interactively, the default is TypeScript (matching the modern v4 programming model). Use `--language javascript` or `--language typescript` to skip the sub-prompt entirely. - -#### `ITemplateFunctionScaffolder` - -```csharp -// src/Func/Templates/ITemplateFunctionScaffolder.cs -internal interface ITemplateFunctionScaffolder -{ - Task ScaffoldAsync( - TemplateEntry template, - string targetDirectory, - CancellationToken cancellationToken = default); -} -``` - -`TemplateFunctionScaffolder` implementation (ported from `fnx/lib/init/scaffold.js`): - -#### Download Strategy (`--fetch auto|git|http`) - -| Value | Behaviour | Best for | -| --- | --- | --- | -| `auto` **(default)** | Probe `git --version`. If git >= 2.25 is available, use `git`; otherwise fall back to `http` silently | Most users (auto-selects the best available strategy) | -| `git` | Force git. Fails with exit 1 if git is unavailable: `git 2.25 or later was not found on PATH. Install a recent git or use '--fetch http'.` | When you want to guarantee git-based fetch | -| `http` | Force the GitHub archive zip endpoint. Never invokes git | Environments without git, CI containers, air-gapped with proxy | - -The `--fetch` flag allows explicit override regardless of git availability. - -#### Download Details - -| Strategy | Condition | Detail | -| ---------- | ----------- | -------- | -| git sparse-checkout | `--fetch git` + `folderPath` is subfolder | `git clone --filter=blob:none --sparse --branch ` then `git sparse-checkout set ` | -| git clone (whole repo) | `--fetch git` + `folderPath` is `"."` | `git clone --depth 1 --branch --single-branch --config core.autocrlf=false -- ` | -| Tree API + raw URLs | `--fetch http` + `folderPath` is subfolder | `GET /repos/{owner}/{repo}/git/trees/?recursive=1` to enumerate, then download each file via `raw.githubusercontent.com////`. Rate limit: 60 tree req/hr (unauthenticated), ~5,000 raw req/hr | -| GitHub zip API | `--fetch http` + `folderPath` is `"."` | `GET https://api.github.com/repos///zipball/` -> extract and strip root prefix | -| Path traversal prevention | Always | Resolve each extracted path and assert it starts with `targetDirectory + Path.DirectorySeparatorChar` | -| Trusted org | Always | Validate `owner` from `repositoryUrl` before any network call | -| URL scheme | Always | Only `https://github.com/` allowed as repository base | - -**Git clone notes:** - -- `--branch` is omitted when `gitRef` is null/empty/`"HEAD"`, so git uses the remote's default branch (handles both `main` and `master` repos) -- `--sparse` added only when the manifest entry has a non-root `folderPath` -- Requires git 2.25+ for `--sparse` cone mode (released January 2020) - -**Subfolder promotion:** - -After sparse-checkout, content lives at `//`. The scaffolder promotes it to `/` (deletes siblings, moves content up) so the final layout matches the http path. - -#### Git Process Requirements - -All git child-process invocations must satisfy: - -| Requirement | Implementation | -| --- | --- | -| **Process abstraction** | Wrap behind `IGitRunner` interface -- no direct `Process.Start` in business logic. Testable via `FakeGitRunner` in tests | -| **Argument-array invocation** | Pass arguments via `ProcessStartInfo.ArgumentList` (string array), never as a shell-concatenated string. Prevents injection if a URL or path contains special characters | -| **`--` end-of-options sentinel** | Always place `--` before positional args (URL, target path) -- blocks flag-injection via manifest values | -| **gitRef validation** | Reject if it starts with `-` (belt-and-braces over the `--` sentinel) | -| **folderPath validation** | Reject if it starts with `-` or contains `..` | -| **Non-interactive** | Set environment variables on the child process: `GIT_TERMINAL_PROMPT=0`, `GIT_ASKPASS=echo`, `SSH_ASKPASS=echo`, `GCM_INTERACTIVE=Never` -- git must never prompt for credentials | -| **Timeout** | 60-second timeout with linked `CancellationTokenSource`; user cancellation distinguished from timeout; process tree killed on timeout | -| **Cancellation** | `CancellationToken` from the command handler kills the git child process on Ctrl+C. Use `Process.Kill(entireProcessTree: true)` | -| **Temp directory cleanup** | Clone into a temp directory; move files to target on success. On failure or cancellation, delete the temp directory in a `finally` block | - -#### Repository Metadata Cleanup - -After any successful fetch (both git and http paths), the scaffolder removes: - -- `/.git/` -- never useful for a fresh quickstart -- `/.github/` -- CI workflows belong to the source repo, not the user's project - -Read-only attributes (set by git on Windows for pack files) are cleared before deletion. - ---- - -### User Experience Flow - -What the user sees in the terminal across every path. - -```mermaid -flowchart TD - START(["$ func quickstart [flags]"]) - START --> FETCH["Fetching template catalog..."] - - FETCH --> DIR_ERR{"Target dir empty?"} - DIR_ERR -- no --> DERR(["Error: Target directory is not empty.
Use an empty directory or a new --path."]) - DIR_ERR -- yes --> LANG - - LANG{"--language
supplied?"} - LANG -- yes --> TRIG - LANG -- "no, non-TTY" --> LERR(["Error: --language required
cannot auto-detect"]) - LANG -- "no, interactive" --> LPROMPT["Use the up/down arrow keys to select a worker runtime:
──────────────────────────
.NET
Node
Python
Java
Powershell"] - - LPROMPT --> DOTNETCHECK{"dotnet selected?"} - DOTNETCHECK -- yes --> DOTNETPROMPT["Use the up/down arrow keys to select a language:
C#
F#"] - DOTNETCHECK -- no --> NODECHECK - DOTNETPROMPT --> TRIG - - NODECHECK{"Node selected?"} - NODECHECK -- yes --> NODEPROMPT["Use the up/down arrow keys to select a language:
JavaScript
TypeScript"] - NODECHECK -- no --> TRIG - NODEPROMPT --> TRIG - - TRIG{"--template or
--resource set?"} - TRIG -- yes --> NAME - TRIG -- "no, interactive" --> TPROMPT["Use the up/down arrow keys to select a template:
──────────────────────────
HTTP Trigger
Blob Trigger
Timer Trigger
..."] - - TPROMPT --> NAME - - NAME["Resolve selected template"] --> DOWNLOAD - - DOWNLOAD["Downloading http-trigger-python-azd..."] - DOWNLOAD --> WRITE["Writing files..."] - - WRITE --> SUCCESS(["Created http-trigger-python-azd in target-dir"]) -``` - ---- - -### Execution Flow - -```mermaid -flowchart TD - A(["func quickstart invoked"]) --> B["Fetch manifest
CDN - ETag cache - bundled fallback"] - B --> C{"--path supplied?"} - C -- yes --> D["Resolve target = --path"] - C -- no --> E["Resolve target = cwd"] - D & E --> F{"Target exists?"} - F -- no --> G["Create directory"] - F -- yes --> EMPTY{"Dir empty?"} - EMPTY -- no --> ERR0(["Error: Target directory is not empty"]) - EMPTY -- yes --> H{"--language supplied?"} - G --> H - H -- "no, non-TTY" --> ERR1(["Error: --language required
target is empty, cannot auto-detect"]) - H -- "no, interactive" --> J["Prompt: select language
arrow keys + live type-to-filter"] - H -- yes --> K["Use --language value"] - J --> K - K --> L{"--template supplied?"} - L -- yes --> M["Resolve template directly"] - L -- no --> N{"--resource supplied?"} - N -- yes --> O["Filter by resource type"] - N -- no --> P["Filter templates by language
apply --search if present"] - O & P --> Q{"Interactive?"} - Q -- yes --> R["Prompt: select trigger
arrow keys + live type-to-filter"] - Q -- "no, non-TTY" --> S["Use first match or default template"] - R --> M - S --> M - M --> Z["Download and write all files"] - Z --> BB(["Created template-id in target-dir"]) -``` - -#### Manifest Cache Flow - -```mermaid -flowchart TD - A(["Fetch manifest"]) --> B{"Local cache exists?"} - B -- no --> F["GET manifest.json from CDN"] - B -- yes --> C{"Cache age under 24h?"} - C -- yes --> DONE(["Return cached manifest"]) - C -- no --> D["GET manifest.json
If-None-Match: stored ETag"] - D --> E{"CDN response"} - E -- "304 Not Modified" --> G["Refresh timestamp
reuse cached body"] - E -- "200 OK new ETag" --> H["Replace cache
store new ETag and body"] - F --> I{"CDN reachable?"} - I -- yes --> H - I -- no --> J{"Bundled fallback available?"} - J -- yes --> K(["Return bundled manifest
warn: catalog may be stale"]) - J -- no --> ERR(["Error: This feature requires a network connection.
Please check your connection and try again."]) - G & H --> DONE -``` - -#### Template Download Strategy - -`folderPath` from the manifest determines what to download. When `folderPath` is `"."` or empty, the entire repo is the template. When it specifies a subfolder (e.g. `templates/python/BlobTrigger`), only that subtree is downloaded. - -```mermaid -flowchart TD - A(["ScaffoldAsync called
repoUrl + folderPath from manifest"]) --> B["Validate owner
trusted orgs: azure, azure-samples, microsoft"] - B --> BFAIL{"Trusted?"} - BFAIL -- no --> ERR1(["Error: untrusted repository owner"]) - BFAIL -- yes --> C["Validate URL scheme
https://github.com/ only"] - C --> CFAIL{"Valid scheme?"} - CFAIL -- no --> ERR2(["Error: only https://github.com/ repos allowed"]) - CFAIL -- yes --> FP{"folderPath is '.' or empty?"} - - FP -- "yes (whole repo)" --> WR{"git available?"} - WR -- yes --> WRG["git clone --depth 1 repoUrl tempDir
move all items except .git to targetDir"] - WR -- no --> WRZ["Download zip: /archive/refs/heads/main.zip
extract, strip repo-branch/ prefix
move contents to targetDir"] - WRG -- fail --> WRZ - WRZ -- fail --> NETERR(["Error: Unable to reach GitHub to download the template.
Please check your network connection and try again."]) - - FP -- "no (subfolder)" --> SF{"git available?"} - SF -- yes --> SFG["git clone --filter=blob:none --sparse --depth 1
git sparse-checkout set folderPath
move tempDir/folderPath/* to targetDir"] - SF -- no --> SFA["Tree API + raw URLs
GET /repos/owner/repo/git/trees/main?recursive=1
filter to folderPath, download each via raw.githubusercontent.com"] - SFG -- fail --> SFA - SFA -- fail --> NETERR - - WRG & WRZ --> G - SFG & SFA --> G - G["For each extracted file:
safePath check — resolved path
must start with targetDir"] - G --> GFAIL{"Path traversal detected?"} - GFAIL -- yes --> ERR3(["Error: path traversal in archive
discard all extracted files"]) - GFAIL -- no --> H(["Write files to target directory"]) -``` - -**Fallback chain**: git → zip/API → error. Each strategy falls back to the next if it fails (git not installed, sparse-checkout unsupported, zip download fails). - -| `folderPath` | Git available | Strategy | What's downloaded | -| --- | --- | --- | --- | -| `"."` or empty | yes | `git clone --depth 1` | Entire repo (minus `.git`) | -| `"."` or empty | no | GitHub zip (`/archive/refs/heads/main.zip`) | Entire repo, strip `repo-main/` prefix | -| `"src/templates/python"` | yes | Sparse checkout | Only `folderPath` subtree | -| `"src/templates/python"` | no | Tree API + raw URLs | 1 API call for tree listing, then raw URL per file (~5,000 req/hr) | - -> **Why Tree API + raw URLs instead of Contents API:** The GitHub Contents API costs 1 API call per file and has a 60 req/hr unauthenticated limit — a template with 20 files would burn a third of the budget. The Tree API enumerates the full repo in a single API call (cached 12h). Individual files are then fetched from `raw.githubusercontent.com` which has a separate, much higher rate limit (~5,000 req/hr). This matches the approach used in [microsoft/mcp#2071](https://github.com/microsoft/mcp/pull/2071). -> -> **Rate limit detection:** GitHub returns HTTP 403 (not 429) when rate-limited. We distinguish rate limiting from permission errors by checking `X-RateLimit-Remaining: 0`. The `X-RateLimit-Reset` header provides the reset timestamp. Tree responses are capped at 5 MB (`MaxTreeSizeBytes`) to prevent OOM. - ---- - -### New Commands - -#### `QuickstartCommand` - -> **Thin-command principle** — `QuickstartCommand` must not contain any manifest, scaffolding, or prompt logic itself. All of that lives in `ITemplateManifestService` and `ITemplateFunctionScaffolder`. - -```csharp -// src/Func/Commands/Quickstart/QuickstartCommand.cs -internal sealed class QuickstartCommand : FuncCliCommand -{ - // Carried forward from v4 func new — preserve agent/CI compatibility - public Option LanguageOption { get; } = new("--language", "-l") - { - Description = "Worker runtime or language (e.g. python, node, java, dotnet, csharp, fsharp, javascript, typescript)" - }; - public Option TemplateOption { get; } = new("--template", "-t") - { - Description = "Template id from the manifest (e.g. http-trigger-python-azd) — skips language and trigger selection" - }; - - // New in vnext - public Option PathOption { get; } = new("--path") - { - Description = "Target directory for the new project; accepts absolute or relative path. Created if absent, accepted if it exists and is empty." - }; - // --force deferred to future iteration; v1 requires empty target directory - public Option ResourceOption { get; } = new("--resource", "-r") - { - Description = "Filter by trigger/binding resource (e.g. http, timer, blob, eventhub, servicebus, cosmos, sql, mcp, durable)" - }; - public Option IacOption { get; } = new("--iac") - { - Description = "Filter by IaC type (e.g. bicep, none)" - }; - public Option SearchOption { get; } = new("--search", "-s") - { - Description = "Case-insensitive substring filter applied to trigger names and descriptions before prompting (not semantic/fuzzy search)" - }; - - public QuickstartCommand( - QuickstartListCommand listCommand, - QuickstartInfoCommand infoCommand, - ITemplateManifestService manifestService, - ITemplateFunctionScaffolder scaffolder, - IInteractionService interaction) - : base("quickstart", "Browse and scaffold functions from the Azure Functions template catalog.") - { - Subcommands.Add(listCommand); - Subcommands.Add(infoCommand); - // ... store services - } - - protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) - { - // Runtime/language resolution: - // → if --language missing in non-TTY mode: fail with clear error - // → if --language missing in interactive mode: prompt "Use the up/down arrow keys to select a worker runtime:" - // showing: .NET, Node, Python, Java, Powershell - // → if user selects ".NET": sub-prompt for C# vs F# - // → if user selects "Node": sub-prompt for JavaScript vs TypeScript - // → map prompt selection to manifest `language` value (see Runtime-to-Language Mapping) - // → if --language supplied directly: map flag value to manifest language - // (e.g. "python" → "Python", "dotnet" → sub-prompt for C#/F#, - // "node" → sub-prompt for JS/TS) - // Directory resolution: - // target = --path (absolute or relative) if supplied, else cwd - // if target does not exist: create it - // if target exists and is empty: use it as-is - // if target exists and is not empty: error (v1 — --force deferred to future iteration) - // Non-interactive path: --template supplied → resolve by id, scaffold directly - // --template supplied → skip both language and trigger selection - // --template path: skip both language and trigger prompts entirely - // Interactive path: prompt via IInteractionService for missing runtime/trigger - // 1. Fetch manifest (verbose logging for cache hit/miss/refresh) - // manifest automatically excludes IaC-only templates (ARM, Bicep, Terraform) - // 2. Prompt worker runtime (if not --language) → IInteractionService.PromptSelectionAsync - // If dotnet selected → sub-prompt for C# vs F# - // If Node selected → sub-prompt for JavaScript vs TypeScript - // 3. If --template: resolve by id directly; else filter by language + --search, prompt trigger - // In interactive mode: PromptSelectionAsync uses Spectre SearchEnabled=true so - // the user can also type to narrow the trigger list without a flag - // 4. ScaffoldAsync → log completion and exit - } -} -``` - -#### `QuickstartListCommand` - -```csharp -// src/Func/Commands/Quickstart/QuickstartListCommand.cs -internal sealed class QuickstartListCommand : FuncCliCommand -{ - public Option LanguageOption { get; } = new("--language", "-l") - { - Description = "Filter by worker runtime or language (e.g. python, node, java, dotnet)" - }; - public Option ResourceOption { get; } = new("--resource", "-r") - { - Description = "Filter by trigger/binding resource (e.g. http, timer, blob, eventhub, servicebus, cosmos, sql, mcp, durable)" - }; - public Option IacOption { get; } = new("--iac") - { - Description = "Filter by IaC type (e.g. bicep, none)" - }; - public Option SearchOption { get; } = new("--search", "-s") - { - Description = "Case-insensitive substring match against displayName, id, resource, tags, and shortDescription (not semantic/fuzzy)" - }; - - protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) - { - // 1. Fetch manifest (ShowStatusAsync for spinner) - // IaC-only templates (ARM, Bicep, Terraform) already excluded by service - // 2. Map --language flag value to manifest language(s) via Runtime-to-Language Mapping - // "node" → filter to both JavaScript and TypeScript - // 3. Filter by --resource: exact match on resource field (case-insensitive) - // 4. Filter by --iac: exact match on iac field (case-insensitive) - // 5. Filter by --search: case-insensitive substring match on displayName, id, resource, tags, shortDescription - // if zero results after all filters: error "No templates found matching ''..." exit 1 - // 6. Sort by priority (ascending — lower priority value = higher in list) - // 7. Build column set: always include Id; omit Language if --language supplied, omit Resource if --resource supplied, omit IaC if --iac supplied - // 8. WriteTable(columns, rows) - } -} -``` - -#### `QuickstartInfoCommand` - -```csharp -// src/Func/Commands/Quickstart/QuickstartInfoCommand.cs -internal sealed class QuickstartInfoCommand : FuncCliCommand -{ - public Argument TemplateIdArgument { get; } = new("id") - { - Description = "Template id from the manifest (e.g. http-trigger-python-azd). Use 'func quickstart list' to see available ids." - }; - - protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) - { - // 1. Fetch manifest - // 2. Find template by id (case-insensitive match) - // 3. If not found: error with suggestion to run 'func quickstart list' - // 4. Display detailed info: - // - displayName - // - longDescription (or shortDescription fallback) - // - language, resource, iac - // - repositoryUrl - // - whatsIncluded (bulleted list) - } -} -``` - ---- - -### `BuiltInCommands` Change - -```csharp -// Existing pattern (WorkloadCommand): -services.AddSingleton(); -services.AddSingleton(); -services.AddSingleton(); -services.AddSingleton(); - -// New (QuickstartCommand, same pattern — top-level command): -services.AddSingleton(); -services.AddSingleton(); -services.AddSingleton(); // top-level command -services.AddSingleton(); -services.AddSingleton(); -``` - ---- - -### File Layout - -```text -src/Func/ - Commands/ - Quickstart/ - QuickstartCommand.cs ← new - QuickstartListCommand.cs ← new - QuickstartInfoCommand.cs ← new - NewCommand.cs ← unchanged (quickstart is top-level, not a subcommand) - Templates/ - ITemplateManifestService.cs ← new - TemplateManifestService.cs ← new - ITemplateFunctionScaffolder.cs ← new - TemplateFunctionScaffolder.cs ← new - TemplateManifest.cs ← new (model) - TemplateEntry.cs ← new (model) - ScaffoldResult.cs ← new (model) - Hosting/ - BuiltInCommands.cs ← modified (register new services + commands) - -test/Func.Tests/ - Commands/Quickstart/ - QuickstartCommandTests.cs ← new - QuickstartListCommandTests.cs ← new - QuickstartInfoCommandTests.cs ← new - Templates/ - TemplateManifestServiceTests.cs ← new - TemplateFunctionScaffolderTests.cs ← new -``` - ---- - -## `IInteractionService` Prompt Gap - -The current `IInteractionService` in vnext (`src/Func/Console/IInteractionService.cs`) does not yet have a selection-prompt method. `QuickstartCommand`'s interactive path needs one. Two options: - -1. **Extend `IInteractionService`** — add `Task PromptSelectionAsync(string title, IEnumerable<(T value, string label)> options, CancellationToken ct)`. Backed by `Spectre.Console.SelectionPrompt` with `SearchEnabled = true` in `SpectreInteractionService`, which gives the user: - - **Arrow keys** (↑↓) to move through the list - - **Type any character** to live-filter the visible items; backspace to clear - - Both work simultaneously — filter then navigate within the filtered set - - No additional code needed; this is standard Spectre.Console behaviour - - Respects `IsInteractive` (falls back to numbered list in CI/non-TTY). - - **Why this matters vs. v4 `func new`:** The current stable CLI uses raw `Console.ReadKey()` arrow navigation which hangs or crashes in agent, CI, and piped contexts because there is no TTY. The `IInteractionService.IsInteractive` check ensures the new design degrades gracefully: in a non-TTY context the prompt renders as a numbered list that accepts stdin, and when all flags are supplied no prompt is shown at all. - -2. **Use `IAnsiConsole` directly in `QuickstartCommand`** — inject `Spectre.Console.IAnsiConsole` alongside `IInteractionService` as an escape hatch. - -**Preferred: Option 1** — keeps all TUI calls behind `IInteractionService`. This makes `QuickstartCommand` fully testable with a substituted `IInteractionService` (NSubstitute). `SearchEnabled = true` on `SelectionPrompt` is a one-liner in the Spectre implementation and directly serves the in-prompt filter UX. - -> **Note on `--search` vs. in-prompt search:** These are complementary. `--search blob` on `func quickstart` pre-filters the list *before* the prompt is shown (useful for scripting or reducing noise when you already know the keyword). The `SearchEnabled` interactive filter lets users type to narrow down after the prompt appears. Both apply the same case-insensitive substring match on template id, trigger name, and description. - ---- - -## Error Messaging - -All user-facing errors are surfaced as `GracefulException` (caught by `Program.Main` and printed cleanly with exit code 1). Internal/unexpected failures propagate as unhandled exceptions with a stack trace. - -| Category | Condition | Error Message | When Shown | -| ---------- | ----------- | --------------- | ------------ | -| **Network — manifest** | CDN returns non-2xx/304 or request times out | `This feature requires a network connection to fetch the template catalog. Please check your connection and try again.` | Any command (`templates`, `list`, `info`) when the CDN is unreachable **and** no cached/bundled manifest is available | -| **Network — manifest (degraded)** | CDN fails but a cached or bundled manifest exists | *(warning, not error)* `Unable to refresh the template catalog — using cached data.` | Same as above, but a stale manifest was found; command continues with stale data | -| **Network — download** | GitHub returns non-2xx, times out, or `git clone` fails with network error | `Unable to reach GitHub to download the template. Please check your network connection and try again.` | `templates` (scaffold) after template selection, when the download step fails | -| **GitHub rate limit** | Tree API returns 403 with `X-RateLimit-Remaining: 0`, or 429 | `GitHub API rate limit exceeded. Rate limit resets at . Try again later.` | `templates` (scaffold) during Tree API call for subfolder templates | -| **Template not found** | `--template ` does not match any entry in the manifest | `Template '' not found. Run 'func quickstart list' to see available templates.` | `templates --template ` or `info ` | -| **Language required** | Non-TTY mode without `--language` | `--language is required. Target directory is empty; cannot auto-detect language.` | `templates` in non-interactive/CI environment without --language | -| **No templates for language** | `--language` value is valid but no manifest templates match | `No templates found for language ''.` | `templates --language ` or `list --language ` | -| **Invalid language** | `--language` value doesn't map to any known runtime | `Unknown language ''. Valid values: dotnet, node, javascript, typescript, python, java, powershell.` | Any command that accepts `--language` | -| **No search results** | `--search` filter (or combined `--language`/`--resource`/`--iac` filters) produces zero matching templates | `No templates found matching ''. Run 'func quickstart list' to see all available templates.` | `templates` and `templates list` after filtering | -| **Directory not empty** | Target directory contains any files or subdirectories | `Target directory '' is not empty. Use an empty directory or a new --path.` | `templates` (scaffold) before download, during directory validation | -| **Untrusted org** | Template's `repositoryUrl` owner is not in the trusted-org allowlist (`azure`, `azure-samples`, `microsoft`) | `Template references an untrusted repository (/). This template cannot be downloaded.` | `templates` (scaffold) before any network call to GitHub | -| **Path traversal** | An extracted file resolves outside the target directory | `Path traversal detected in template archive: . The template may be corrupted or malicious.` | `templates` (scaffold) during file extraction from staging to target | -| **Target is a file** | `--path` points to an existing file (not a directory) | `'' is a file, not a directory. --path must be a directory.` | `templates --path ` | -| **Permission denied** | Cannot write to the target directory | `Permission denied: cannot write to ''. Check directory permissions.` | `templates` (scaffold) when file copy fails with `UnauthorizedAccessException` | -| **Git sparse-checkout failed** | `git` is on PATH but sparse-checkout fails (non-network) | Falls through to Tree API + raw URL fallback; no user-visible error unless that also fails | `templates` (scaffold) — transparent fallback | -| **Manifest parse error** | CDN returns 200 but the body is not valid JSON or is missing required fields | `The template catalog is corrupted or has an unexpected format. Try again later, or file an issue.` | Any command, after manifest fetch | - -### Exception Strategy - -Following the Core Tools vnext error handling convention: - -- **Services** (`TemplateManifestService`, `TemplateFunctionScaffolder`) throw specific framework/domain exceptions at the failure site: - - `HttpRequestException` — network failures - - `FileNotFoundException` / `DirectoryNotFoundException` — missing paths - - `UnauthorizedAccessException` — permission issues - - `InvalidOperationException` — untrusted org in manifest, path traversal, parse errors - - `KeyNotFoundException` — template id not found - -- **Commands** (`QuickstartCommand`, `QuickstartListCommand`, `QuickstartInfoCommand`) are the boundary. Each catches the specific exceptions it expects from its direct service calls and wraps them in `GracefulException(message, isUserError: true)`, preserving the inner exception. The `try` is narrow — one service call per `try` block. - -- **Anything unexpected** is not caught by the command — it surfaces as an unhandled exception with a stack trace (runtime bug). - ---- - -## Security Envelope - -| Concern | Mitigation | -| --- | --- | -| Malicious manifest entry pointing to attacker repo | URL allow-list: HTTPS scheme + `github.com` host + trusted org (`azure`, `azure-samples`, `microsoft`). Filtered in manifest client, re-checked in scaffolder | -| Zip slip / path traversal in archive | Every entry's resolved absolute path must start with `Path.GetFullPath(targetPath)` + `DirectorySeparator`. Mismatch -> `InvalidOperationException` | -| Flag injection via manifest values | Argv-array invocation + `--` end-of-options sentinel + reject `gitRef`/`folderPath` starting with `-` | -| Command-line injection via folderPath | `..` segments rejected | -| Interactive git credential prompts hanging the CLI | Four env vars set to disable every prompt path (see Git Process Requirements) | -| Runaway git process | 60s timeout with process-tree kill | - -**Trusted GitHub organizations** -- hard-coded allow-list in `QuickstartUrlValidator`: - -- `azure` -- `azure-samples` -- `microsoft` - ---- - -## Manifest Source - -| Aspect | Detail | -| --- | --- | -| Primary | `https://cdn.functions.azure.com/public/templates-manifest/manifest.json` | -| Fallback | Raw GitHub URL (`Azure/azure-functions-templates` main branch) | -| Override | `FUNC_TEMPLATE_MANIFEST_URL` env var -- accepts an `https://` URL or a `file://` path. Used for staging validation and local manifest authoring against unpublished entries | -| Caching | ETag-based; reduces CDN round-trips on repeat invocations within a session | - -The current CDN manifest does not yet pin `gitRef` per entry; entries without `gitRef` clone the remote's default branch (git path) or download `archive/HEAD.zip` (http path). Once the manifest moves to `gitRef`-pinned entries with signed tags, the security envelope tightens further (see Future Work). - ---- - -## Exit Codes - -| Code | Condition | -| --- | --- | -| 0 | Successful scaffold / list / info | -| 1 | User error (graceful): unknown template, non-empty target, empty filter result, manifest fetch failure, `--fetch git` with git missing, git/http fetch failure | -| Non-zero (uncaught) | Unexpected runtime bug -- stack trace surfaces | - ---- - -## Future Work - -- **GPG tag verification** -- when the manifest moves to `gitRef`-pinned entries and Azure-Samples/trusted orgs begin GPG-signing release tags, add an optional `git verify-tag` step. Default-on once signing is reliable; needs UX for unsigned/invalid, GPG-not-installed handling, public-key bundling or trust-on-first-use. Both `gitRef` enforcement and GPG verification become active together -- one without the other provides no guarantee. - ---- - -## Open Questions - -- [ ] **Default language** — `--language` is currently required with no fallback (target dir is empty, nothing to detect from). Should there be a default so repeat users don't have to supply it every time? Options: - - **Detect from existing files** — run `ProjectDetector.DetectStackAndLanguage(targetDir)` on the resolved target before prompting; if detectable files exist (e.g. `requirements.txt`, `package.json`, `*.csproj`), use the detected language as the default; if target is empty, fall through to prompt or error. Already exists at `src/Func/Commands/ProjectDetector.cs`. Becomes more useful when `--force` is added in a future iteration (target dir may have project files). - - **Persist last-used language** — store in `~/.azure-functions//preferences.json` after first scaffold; use as default on next run - - **Explicit `func config set default-language `** — user opts in deliberately; no implicit magic - - **Keep required** — simplest; interactive prompt is low friction anyway; scripts always supply it - - Consider: does a persisted or detected default interact badly with non-TTY/CI (wrong language silently used)? Detection from files is safer than persistence — it's scoped to the target dir, not global state. -- [ ] **`--search` empty-result behaviour in interactive mode** — when the in-prompt Spectre filter (not the `--search` flag) narrows to zero results, Spectre renders an empty list. Should the command detect this and show a message, or let the user backspace to widen the filter naturally? -- [ ] **`func quickstart` vs `func new` interactive fallback** — should bare `func new` (when no workloads installed) redirect to `func quickstart` interactive flow, rather than showing a hint? Or keep the hint to encourage workload installation? -- [ ] **`--output json` on `func quickstart list`** — useful for tooling. Worth adding now or later? - ---- - -## Appendix - -### References - -- `fnx init` orchestration: `func-emulate/fnx/lib/init.js` (internal) -- Manifest fetch + cache: `func-emulate/fnx/lib/init/manifest.js` (internal) -- Scaffold (git + zip): `func-emulate/fnx/lib/init/scaffold.js` (internal) -- Interactive prompts: `func-emulate/fnx/lib/init/prompts.js` (internal) -- Runtime + trigger constants: `func-emulate/fnx/templates-mcp/src/templates.ts` (internal) -- vnext `NewCommand`: `src/Func/Commands/NewCommand.cs` -- vnext `WorkloadCommand` (pattern): `src/Func/Commands/Workload/WorkloadCommand.cs` -- vnext `IInteractionService`: `src/Func/Console/IInteractionService.cs` - -### vnext Architecture (What Already Exists) - -The vnext branch has been rebuilt from scratch on System.CommandLine. Key elements relevant to this design: - -| Type | Location | Role | -| --- | --- | --- | -| `FuncCliCommand` | `src/Func/Commands/FuncCliCommand.cs` | Base class for all commands. Subcommands injected via constructor. | -| `IBuiltInCommand` | `src/Func/Hosting/IBuiltInCommand.cs` | Marker interface — names are reserved, registered in `BuiltInCommands` | -| `NewCommand` | `src/Func/Commands/NewCommand.cs` | `func new` — delegates to workloads for per-function scaffolding | -| `IInteractionService` | `src/Func/Console/IInteractionService.cs` | Spectre.Console abstraction. Has `IsInteractive`, `WriteTable`, `ShowStatusAsync`, prompt methods. | -| `BuiltInCommands` | `src/Func/Hosting/BuiltInCommands.cs` | Registers all built-in commands with DI | -| `WorkloadCommand` | `src/Func/Commands/Workload/WorkloadCommand.cs` | Pattern reference — parent command with subcommands injected via constructor | -| `WorkloadListCommand` | `src/Func/Commands/Workload/WorkloadListCommand.cs` | Pattern reference — subcommand using `IInteractionService.WriteTable` | - -**`WorkloadCommand` pattern** (to follow exactly): - -- Parent command receives subcommands via constructor, calls `Subcommands.Add()` -- Each subcommand registered as its own concrete singleton in `BuiltInCommands`, **not** as `FuncCliCommand` (to prevent top-level registration) -- Parent registered as `FuncCliCommand` so it appears at top level - -### fnx init — Source Implementation - -| Component | Location | Role | -| --- | --- | --- | -| **Init orchestration** | `fnx/lib/init.js` | 9-step flow: manifest → prompts → download → scaffold | -| **Manifest fetching** | `fnx/lib/init/manifest.js` | CDN fetch, ETag caching, 24h TTL, bundled fallback | -| **Interactive prompts** | `fnx/lib/init/prompts.js` | Arrow-key selection via readline raw mode; numbered fallback for CI | -| **Scaffolding** | `fnx/lib/init/scaffold.js` | git sparse-checkout or GitHub zip API; `safePath()`, trusted-org filter | -| **Template definitions** | `fnx/templates-mcp/src/templates.ts` | `SUPPORTED_RUNTIMES`, trigger priority list, `TemplateParameter` | - -**`fnx init` flow (condensed):** - -1. Fetch manifest from `https://cdn.functions.azure.com/public/templates-manifest/manifest.json` (ETag-cached; bundled fallback on failure) -2. Prompt: **runtime** (Python / Node.js / .NET / Java / PowerShell) -3. For Node.js: prompt **language** (TypeScript / JavaScript) -4. Filter manifest by runtime → prompt **trigger** (priority-sorted: HTTP, Blob, Timer, Queue, …) -5. Prompt **project name** -6. Download template: `git clone --sparse` (if git available) else GitHub zip API -7. Generate `host.json`, `local.settings.json`, `.gitignore`; print success banner - -**Security primitives to port:** - -- `safePath(targetDir, fileName)` — resolve + prefix check to prevent path traversal -- `filterTrustedTemplates()` — allowlist of `azure`, `azure-samples`, `microsoft` GitHub orgs -- URL scheme validation before any network call