AppSurface Search
Guide

RazorWire CLI

Source of truth

View source Edit this page

Last updated

The RazorWire CLI is a command-line tool for managing RazorWire projects. Its primary feature is exporting a reactive RazorWire site into CDN-ready static files by default, with an opt-in hybrid mode for deployments that still provide server routing.

The CLI uses AppSurface's command-first console mode. That means help and validation output are intentionally quiet, without Generic Host lifecycle banners, while real export runs still emit useful progress logs.

Start here: export the sample

During repository development, the fastest confidence check is to export the RazorWire MVC sample from source:

dotnet run --project Web/ForgeTrust.RazorWire.Cli -- export -o ./dist -p ./examples/razorwire-mvc/RazorWireWebExample.csproj
test -f ./dist/index.html

A successful run publishes the sample, starts it on an ephemeral loopback URL, crawls the discovered pages, writes static files under ./dist, and shuts the target app down automatically. Inspect ./dist/index.html first; it proves the exporter emitted the root artifact.

Installation

The RazorWire CLI project is configured as a .NET tool with the command name razorwire. Use an exact package version when running release builds so exports are reproducible. The package chooser still excludes ForgeTrust.RazorWire.Cli from the direct-install matrix until issue #171 lands stable public tool packaging, so the commands in this section require the package to exist on one of your configured NuGet sources or for you to pass an explicit local package source. During normal repository development, prefer the source-run command below.

Run a published package without permanently installing it:

dnx ForgeTrust.RazorWire.Cli@<version> --yes -- export -o ./dist -p ./examples/razorwire-mvc/RazorWireWebExample.csproj

The equivalent SDK spelling is:

dotnet tool execute ForgeTrust.RazorWire.Cli@<version> --yes -- export -o ./dist -p ./examples/razorwire-mvc/RazorWireWebExample.csproj

Install the tool when you want a stable razorwire command on your PATH:

dotnet tool install --global ForgeTrust.RazorWire.Cli --version <version>
razorwire export -o ./dist -p ./examples/razorwire-mvc/RazorWireWebExample.csproj

During repository development, run the CLI directly from source:

dotnet run --project Web/ForgeTrust.RazorWire.Cli -- [command] [options]

When testing an unpublished package from a local folder, pack it first, pass that folder as the package source, and keep the version exact:

dotnet pack Web/ForgeTrust.RazorWire.Cli -c Release -o ./artifacts/packages /p:PackageVersion=0.0.0-local.1
dnx ForgeTrust.RazorWire.Cli@0.0.0-local.1 --yes --source ./artifacts/packages -- --help

Do not combine --version and --prerelease for exact tool installs on recent SDKs; exact prerelease versions install without the extra flag.

Commands

Help and validation behavior

  • Root help (--help) and command help (export --help) are command-first by design.
  • Validation failures, such as invalid flags or missing source options, should surface actionable CLI output without host startup and shutdown chatter.
  • Source validation failures include a concrete recovery path. When no source or multiple sources are provided, the error points to a single-source example such as razorwire export --project ./MyApp.csproj --output ./dist and reminds developers to run razorwire export --help.
  • Successful export runs still keep command-owned progress output so long-running work remains understandable.

export

Exports a RazorWire application to a static directory.

Options:

  • -o|--output <path>: Output directory where the static files will be saved (default: dist).
  • -r|--seeds <path>: Optional path to a file containing seed routes to crawl.
  • --publish-root-extras <path>: Optional YAML manifest for explicit single-file deployment extras copied into the publish root after generated output is proven collision-free.
  • -u|--url <url>: Base URL of a running application used for crawling.
  • -p|--project <path.csproj>: Path to a .NET project to run automatically and export.
  • -d|--dll <path.dll>: Path to a .NET DLL to run automatically and export.
  • -f|--framework <TFM>: Target framework for project exports. Required when --project points at a multi-targeted project.
  • -m|--mode <cdn|hybrid>: Export mode. cdn is the default and rewrites managed internal URLs to emitted static artifacts. hybrid preserves application-style internal URLs for hosts that still route extensionless paths.
  • --live-origin <origin>: Optional live origin for split-origin hybrid exports, such as https://api.example.com. The value must be an http or https origin only, with no path, query, fragment, or userinfo.
  • --hybrid-credentials <auto|include|omit>: Credential behavior for RazorWire-managed live calls. auto is the default and includes credentials when --live-origin is set; use omit only for intentionally anonymous live endpoints.
  • --app-args <token>: Repeatable app-argument token to pass through when launching --project or --dll.
  • --no-build: Project mode only. Skips the release publish step and reuses existing published output.

Exactly one source option is required: --url, --project, or --dll.

Export modes

cdn mode is the default because most razorwire export runs produce folders that are uploaded to plain static hosting or a CDN. It emits extension-backed artifact URLs for exporter-managed internal references:

  • / is emitted as index.html and may still be referenced as /.
  • /about is emitted as about.html and internal references rewrite to /about.html.
  • /docs/start is emitted as docs/start.html and internal references rewrite to /docs/start.html.
  • Dotted page slugs still follow page-route rules: /docs/web/forgetrust.razorwire is emitted as docs/web/forgetrust.razorwire.html, while assets that return non-HTML content keep their real extension.
  • AppSurface Docs content frames also emit .partial.html artifacts when a doc-content frame exists, so static frame navigation can fetch the content island.
  • Host-registered seed routes are crawled in addition to configured seed files or in-memory defaults. This lets a host with its own route graph export public pages that are not linked from the initial crawl roots.
  • Redirect aliases registered by the host are validated after their canonical routes are proven. The default html redirect strategy emits tiny alias HTML fallback files after canonical artifacts; provider integrations can instead select netlify to write exact root _redirects rules and skip alias HTML files. If a host registers aliases, it should also register or otherwise expose their canonical routes as crawl seeds so validation can prove the canonical artifacts exist.
  • Assets that already have extensions, such as /css/site.css, /img/logo.png, or /_content/.../razorwire.js, keep their path. Cache-busting query strings on assets are allowed only when the query-free path maps to an exported file.
  • The conventional /_appsurface/errors/404 page, when available, is emitted as 404.html and participates in the same CDN validation and URL rewriting.
  • Exporter-managed artifact fetches own their HTTP redirect chain before reading response bodies. Same-scheme, same-host, same-port redirects are allowed only when the final response stays under the configured --url or launched app base path; redirects outside that export origin and app path fail with RWEXPORT008. This is separate from host-registered redirect aliases (RWEXPORT005) and from URL-source readiness probing, which only checks whether a running app responds.

Generated export artifact boundary

Generated artifacts are written only inside the physical output tree. Before creating artifact parents, opening final files, enumerating release archive entries, reading file lengths, or hashing bytes, export checks the output root, existing parent segments, and existing targets for symlinks, junctions, or other reparse points. This guard covers HTML, CSS, binary assets, root 404.html, AppSurface Docs .partial.html files, redirect alias HTML, Netlify _redirects, .appsurface-docs-route-manifest.json, and .appsurface-docs-release-manifest.json.

Failures use RWEXPORT009 with stable reason labels: output-root-reparse, artifact-parent-reparse, artifact-target-reparse, archive-entry-reparse, and artifact-outside-root. The diagnostic includes the problem, cause, fix, artifact kind, route when available, output-relative path, unsafe segment, operation (create-directory, open-write, archive-enumerate, or archive-hash), and this docs anchor. It deliberately reports the unsafe output segment rather than resolving or printing the external symlink target.

This is distinct from RWEXPORT008: RWEXPORT008 protects artifact HTTP redirect provenance before response bodies are read, while RWEXPORT009 protects the local filesystem boundary before generated bytes are written or release archive bytes are read. Hostile concurrent mutation after validation is outside the exporter contract; run export into an operator-owned build directory that other processes do not modify.

Use this quick chooser before exporting:

Goal Mode
Upload one self-contained folder to GitHub Pages, Netlify, S3, or a plain CDN cdn
Serve static pages behind app-aware routing or live RazorWire frames, forms, streams, and islands hybrid
Make a missing CSS, image, script, stylesheet, module preload, icon, font, or other browser asset stop failing Fix or externalize the asset; hybrid still validates browser-delivered assets

Static website deployment extras

RazorWire export owns the app route graph and exporter-managed provider artifacts. It does not copy arbitrary project-root files into the output directory. Use this boundary when preparing folders for static hosts:

  • --seeds are routes, not local paths. A seed file line such as /pricing asks the exporter to crawl the app route /pricing; a line such as deploy/CNAME is not a request to copy ./deploy/CNAME.
  • --publish-root-extras are deployment-owned publish-root files, not routes. The option reads a checked-in YAML manifest and copies only the named regular files after export proves each target path is not generated, reserved, preexisting, or hidden inside an exact release archive.
  • The conventional AppSurface 404 page uses the reserved /_appsurface/errors/404 route. When that route is available and returns HTML, the exporter stages it as root 404.html before normal crawl processing and validates it like any other CDN artifact.
  • CNAME, .nojekyll, and /.well-known/security.txt are deployment-owned extras when your host needs them. Keep them in a deployment directory such as deploy/ and name each file explicitly in deploy/export-extras.yml.
  • /_redirects and /_headers are reserved in v1. _redirects is exporter-owned when the export strategy is Netlify, for example through a host integration using ExportRedirectStrategy.Netlify or through appsurface docs export --redirects netlify. Future provider headers and redirects should be structured exporter features, not raw copy-through files.

The manifest schema is intentionally small:

version: 1
extras:
  - source: CNAME
    publishPath: /CNAME

source resolves relative to the manifest directory and must stay under that directory without symlink or reparse traversal. publishPath is a publish-root file path, not a route: it must start with /, must not contain traversal, encoded slashes, query strings, fragments, protocols, control characters, or invalid filesystem segments, and is matched case-insensitively against exporter-owned paths.

For GitHub Pages branch or folder publishing with a custom domain:

mkdir -p deploy
printf '%s\n' 'www.example.com' > deploy/CNAME
cat > deploy/export-extras.yml <<'YAML'
version: 1
extras:
  - source: CNAME
    publishPath: /CNAME
YAML
razorwire export --project ./MySite.csproj --output ./dist --publish-root-extras ./deploy/export-extras.yml
test -f ./dist/CNAME

When the site also needs an app-served /404.html route, add /404.html to --seeds only when the app intentionally serves that route and is not relying on the conventional AppSurface 404 route. When the app uses AppSurface Web's conventional browser status pages, let /_appsurface/errors/404 stage root 404.html; adding /404.html to the seed file will not override the already staged conventional page.

Custom GitHub Actions Pages artifact workflows can still assemble the final upload around the exported folder. Keep the extras manifest in the repository, run razorwire export with --publish-root-extras, then upload the resulting publish root; do not add a later broad copy step that can overwrite exporter-owned files.

Need Recommended path
Conventional AppSurface 404 page Serve /_appsurface/errors/404; export stages root 404.html when the route returns HTML.
Host-served custom /404.html route without conventional AppSurface 404 Add /404.html to the seed route file.
GitHub Pages custom domain Add deploy/CNAME to deploy/export-extras.yml with publishPath: /CNAME, then export with --publish-root-extras.
GitHub Pages Jekyll bypass Add deploy/.nojekyll with publishPath: /.nojekyll.
Security contact file Add deploy/.well-known/security.txt with publishPath: /.well-known/security.txt.
Netlify redirect aliases Use ExportRedirectStrategy.Netlify in the host integration, or appsurface docs export --redirects netlify for AppSurface Docs, so the exporter owns _redirects.
Netlify headers Reserved in v1; do not copy /_headers through deployment extras.
AppSurface Docs exact release archive plus deployment extras Keep exact release trees clean for .appsurface-docs-release-manifest.json; copy publish-root extras such as CNAME into the surrounding publish root outside the immutable exact release tree.

Why not public/? RazorWire exports from the running app and validates the generated artifact graph before writing deployment extras. A framework-style public/ overlay would blur route-owned files, provider-owned files, stale output, secrets, and generated artifacts. Use deploy/export-extras.yml instead: every extra is named, validated, copied without overwrite, and rejected if it collides with a generated route artifact, provider file, 404.html, docs archive manifest, or existing final target.

Migration examples:

Old step New step
install -m 0644 ./deploy/CNAME ./dist/CNAME Add source: CNAME, publishPath: /CNAME to deploy/export-extras.yml; pass --publish-root-extras ./deploy/export-extras.yml.
cp -R public/* dist/ Move only needed single files into deploy/, list each one in the manifest, and let export reject collisions.
printf '/old /new 301!' > dist/_redirects Register redirect aliases and select ExportRedirectStrategy.Netlify, or use appsurface docs export --redirects netlify for AppSurface Docs.

Do not copy the repository root, broad globs, symlinks, local config, secrets, generated credentials, or unrelated deployment folders into ./dist. Deployment extras are explicit single regular files only; v1 has no directories, globs, symlink following, overlays, overwrite mode, provider profiles, or raw provider headers/redirects.

Troubleshooting:

Symptom Cause Fix
CNAME is missing from the uploaded site Export crawled app routes but no extras manifest registered the deployment-owned file. Add deploy/export-extras.yml, pass --publish-root-extras, and verify test -f ./dist/CNAME in CI.
A seed file contains deploy/CNAME, but the export still succeeds without CNAME Seeds are routes, not local paths; if no valid seed routes remain, export can fall back to /. Put route paths such as / and /404.html in the seed file, then register CNAME with --publish-root-extras.
404.html is missing or not the page you expected The app neither served the conventional /_appsurface/errors/404 HTML route nor exposed the intended /404.html route as a seed. Use the conventional route for AppSurface 404 pages, or seed /404.html for a deliberate app-served page and verify test -f ./dist/404.html.
RWEXPORT007 [target-reserved] reports /_redirects or /_headers The manifest tried to raw-copy a provider file reserved by the exporter. Remove the raw extra and select the structured redirect/header feature when available; for Netlify redirects, use the Netlify redirect strategy.
RWEXPORT007 [target-generated-collision] reports an extra path The manifest target matches a generated route artifact, redirect artifact, 404 page, provider file, docs manifest, or existing output file. Change the deployment file path or remove the extra; generated output owns that publish path.
RWEXPORT009 [artifact-parent-reparse], [artifact-target-reparse], or [archive-entry-reparse] reports a generated artifact The output tree contains a symlink, junction, or reparse point in a generated artifact path or release archive entry. Export to a clean regular directory tree, remove the reparse point, or copy deployment-owned files into the surrounding publish root after export.
AppSurface Docs exact release verification fails after adding hidden deployment files The .appsurface-docs-release-manifest.json archive contract expects a clean exact release tree. Export exact releases to a clean directory, pin the manifest, then copy deployment extras into the surrounding publish root.

CDN validation fails the export when exporter-managed dependencies cannot be represented as static artifacts. Diagnostics use stable codes and include the discovered surface, such as <img src>, <a href>, stylesheet url(), or stylesheet @import string, plus the normalized path the exporter tried to prove:

  • RWEXPORT001: a server-fetched frame route did not materialize. Add or seed the frame route, or switch to --mode hybrid when a live server owns it.
  • RWEXPORT002: a query-bearing frame route cannot be represented as one static artifact. Export a query-free route, split the frame into static pages, or keep server routing with --mode hybrid.
  • RWEXPORT003: a required internal asset did not materialize. Add the asset route, correct path casing, make the reference external/data/hash-only, or use --mode hybrid only when the missing reference is not a browser-delivered asset.
  • RWEXPORT004: a managed internal URL could not be rewritten to an emitted artifact URL. Seed or expose the target route so it emits an artifact, or mark authoring-only anchors with data-rw-export-ignore.
  • RWEXPORT005: a registered redirect alias cannot safely point at its canonical route, collides with another route or provider redirect output, or was already crawled as a normal HTML page. Fix the host redirect registration so aliases map to exported canonical routes without artifact collisions.
  • RWEXPORT006: an export found anti-forgery behavior that cannot run safely in the selected mode. CDN mode rejects any anti-forgery surface because a plain static host cannot mint runtime tokens. Hybrid RazorWire-managed forms are auto-converted to lazy token refresh when they have a safe app-owned action and credentialed split-origin live calls are enabled. Export fails early when the form opts out, posts to an external action, uses --hybrid-credentials omit with --live-origin, or is not managed by RazorWire.
  • RWEXPORT007: a publish-root deployment extra could not be accepted. Stable reason labels include schema, source-outside-root, source-missing, source-directory, source-symlink, target-invalid, target-duplicate, target-reserved, target-generated-collision, target-exists, target-parent-symlink, copy-failed, and release-archive-incompatible.
  • RWEXPORT008: an exporter-managed artifact fetch was redirected outside the configured export origin and app path, returned an invalid redirect, or exceeded the redirect limit. Keep export redirects on the same scheme, host, port, and base path; otherwise make the reference external instead of exporter-managed. The diagnostic names the original route and rejected target without reading or writing the redirected response body.
  • RWEXPORT009: a generated artifact path or release archive entry would cross the physical output-root boundary through a symlink, junction, reparse point, or lexical escape. Stable reason labels include output-root-reparse, artifact-parent-reparse, artifact-target-reparse, archive-entry-reparse, and artifact-outside-root.
  • RWEXPORT010: static auth projection found protected allowed content, missing fallback content, auth diagnostics, unsafe auth metadata, or DevAuth/test persona markers in a generated text artifact. Stable reason labels include auth-missing-fallback, auth-private-content, auth-unsafe-metadata, auth-diagnostics, and auth-artifact-leak. Add an explicit rw:auth-anonymous fallback, move protected UI behind live rendering, disable diagnostics/DevAuth for exported routes, or remove the route from static export. See Static Auth Projection.

Route normalization validates requested URLs before the exporter fetches them. Redirect enforcement validates the response chain before artifact content is read or written. The generated artifact boundary then validates the local output path before directory creation, final writes, archive enumeration, metadata reads, and hashing.

hybrid mode preserves the older application-style URL behavior. Use it when the exported directory will still be served by infrastructure that resolves extensionless URLs, dynamic frame endpoints, or other live-server behavior. Hybrid mode tolerates missing page and live-behavior references that server-backed infrastructure may own, but it still fails missing browser-delivered static assets with RWEXPORT003: scripts, stylesheets, module preloads, icons, images, srcset candidates, CSS url(...), CSS @import, and asset-shaped preload or prefetch hints. Hybrid export also fails unsafe artifact redirects with RWEXPORT008; same-origin redirects remain supported, but redirected artifact responses must stay inside the configured export origin and app path. Canonical metadata, DNS hints, anchors, and ambiguous page/API-shaped preload or prefetch hints are not hybrid static-asset failures. Safe RazorWire forms with static anti-forgery tokens are converted to lazy runtime refresh in hybrid mode even without --live-origin; this supports same-origin CDN passthrough to the backend for RazorWire endpoints.

Split-origin hybrid export is enabled by adding --live-origin to --mode hybrid:

razorwire export --mode hybrid \
  --live-origin https://api.example.com \
  --project ./MyApp.csproj

With a live origin, the exporter rewrites RazorWire-owned runtime surfaces to the live app: rw-stream-source URLs, server-backed island frame sources, safe RazorWire forms, and path-base-aware lazy anti-forgery endpoints. If a RazorWire form contains crawler-minted __RequestVerificationToken inputs, export removes those stale static token inputs and marks the form data-rw-antiforgery="lazy" so the browser fetches a fresh token from /_rw/antiforgery/token on first intent or immediately before submit. When the crawl target is mounted under a path base, the exporter strips that crawl path base from data-rw-antiforgery-endpoint before the runtime combines it with --live-origin; otherwise split-origin deployments would call the crawler path instead of the live app root. The runtime wakes the live origin only when the user interacts with the form, not on every static page visit.

For a local proof path, Cloud Run live-origin deployment recipe, CORS setup, cold-start tradeoffs, and AppSurface Docs export example, see Hybrid Hosting With Cloud Run.

The per-form rw-antiforgery="lazy" TagHelper attribute is optional. Use it as an assertion when authoring a form that must lazy-refresh, but split-origin export applies the safe conversion automatically. rw-antiforgery="off" is an explicit opt-out and will fail export if the form still contains a static anti-forgery token.

Redirect strategy is a host-integration setting, not a generic razorwire export CLI option. ExportContext.AddRedirectAlias(...) is the preferred API for registering alias-to-canonical relationships; AddRedirectArtifact(...) remains as a compatibility wrapper for older integrations. ExportRedirectStrategy.Html works on generic static hosts such as GitHub Pages. ExportRedirectStrategy.Netlify is for CDN exports published to Netlify-compatible providers: it writes one publish-root _redirects file, serializes exact site-local paths with per-segment percent encoding, uses 301!, de-duplicates exact serialized source/target pairs, rejects self-redirects after serialization, rejects same-source/different-target rules, rejects aliases that conflict with exported _redirects, and never uses PublicOrigin or emitted .html artifact URLs as rule targets.

CDN mode validates the static references the exporter owns and can see while crawling HTML and CSS: parser-discovered page links, Turbo Frame sources, supported HTML asset references, <link rel="canonical"> values that point at managed app routes, <img> and <source> srcset candidates, CSS url(...) references, and both @import url(...) and string-form @import "..." stylesheet dependencies. Hybrid mode runs the browser-asset part of that validation without requiring every page or live route to become a static artifact. It does not prove arbitrary app-authored JavaScript fetch calls, form posts, Server-Sent Events, import maps, or other runtime behavior that is not represented as exporter-managed markup or CSS references.

Parser-backed discovery can surface valid HTML or CSS references that older exporter versions missed. After upgrading, a new RWEXPORT### failure may be correct rather than a regression: export the route or asset, fix path casing, mark authoring-only anchors with data-rw-export-ignore, or choose --mode hybrid when the dependency is intentionally served by live infrastructure. Do not use data-rw-export-ignore for missing static assets.

For a missing asset such as background-image: url('/img/map-image.png'), fix the export by copying the asset into the crawled app, correcting path casing, changing the URL to an external asset, or removing the browser dependency. link rel="modulepreload" always validates as a static module asset. link rel="preload" and link rel="prefetch" fail only when the hint is clearly asset-shaped, such as as="script", as="style", as="image", as="font", or a URL ending in a known static extension. Page-shaped hints such as <link rel="prefetch" href="/docs/next"> remain live/page references in hybrid mode.

CDN export skips relative anchors that point at common source or project file extensions, such as ./Program.cs or ../Project.csproj, because those links are usually for GitHub and editor navigation rather than static-site dependencies. For other authoring-only anchors in app-rendered HTML, use data-rw-export-ignore; the anchor remains rendered and clickable, but CDN export will not crawl, validate, or rewrite its href.

When launched app processes are started by the CLI (--project or --dll), they run in production environment (DOTNET_ENVIRONMENT=Production, ASPNETCORE_ENVIRONMENT=Production).

When --project is used:

  • Project mode publishes a release build by default.
  • The publish probe disables persistent build servers so command output capture cannot be held open by reused MSBuild nodes.
  • Multi-targeted projects must pass -f|--framework <TFM> to select the target framework, for example -f net6.0 or --framework net7.0; omitting it causes a CLI error before publish. -f|--framework can be combined with --project and --no-build when reusing existing published output.
  • Project mode resolves the published app DLL and launches that DLL for crawling.
  • Add --no-build to skip publishing and reuse existing published output.

When --dll is used:

  • The CLI launches the provided DLL directly (no build or DLL resolution step).

For both --project and --dll:

  • If you do not pass --urls via --app-args, the CLI appends --urls http://127.0.0.1:0.
  • The launched app inherits the parent process environment, while the CLI forces ASPNETCORE_ENVIRONMENT=Production and DOTNET_ENVIRONMENT=Production for deployed-runtime semantics.
  • The CLI waits for startup, crawls the app, then shuts the process down automatically.
  • Pass multiple target-app arguments by repeating --app-args once per token. For example, --app-args --urls --app-args http://127.0.0.1:5009 launches the app with --urls http://127.0.0.1:5009.

If export fails

Process failures are reported with the command stage, exit code or startup exception when available, captured target-app stdout/stderr, and the next recovery step. Target-app output is fully captured for the current export attempt; the CLI does not apply a line-count or byte-count truncation limit. Common branches:

  • Missing source option or multiple sources: choose exactly one of --url, --project, or --dll; run razorwire export --help for the current command shape.
  • Multi-targeted project without --framework: pass -f|--framework <TFM>, such as --framework net10.0, so publish and DLL resolution use the same target.
  • Project publish fails: read the captured dotnet publish stdout/stderr, fix the build error, and rerun the same export command.
  • Target app exits before listening: inspect the captured target-app output in the error. The app failed during startup before the exporter could discover a base URL.
  • Readiness timeout after a listening URL: verify the app can serve requests at the emitted loopback URL and that startup work is not blocking the first response.
  • Cancellation or interrupted export: the CLI performs best-effort shutdown of the launched target app before returning.

AppSurface Docs versioned export notes

When the target app hosts AppSurface Docs:

  • Use appsurface docs export --repo . for AppSurface's own repository docs surface. That command starts the AppSurface Docs standalone host in-process, uses the same packaged static-asset fallbacks as preview, derives default seeds from AppSurface Docs routing, and keeps -r reserved for --repo.
  • Export the live unreleased preview surface from its configured live docs root, such as /docs when versioning is off, /docs/next when versioning is on with defaults, or /foo/bar/next when the host sets AppSurfaceDocs:Routing:RouteRootPath to /foo/bar.
  • Export exact published release trees as standalone static subtrees that already contain their own .appsurface-docs-route-manifest.json, .appsurface-docs-release-manifest.json, index.html, search.html, search-index.json, search.css, search-client.js, minisearch.min.js, section routes, and detail pages.
  • Keep exact-tree search-index.json document paths canonical and deployment-independent. Valid release search rows use root-relative /docs/... paths; they do not include request path bases, custom route roots, origins, executable schemes, traversal, or docs operational routes. AppSurface Docs rebases those canonical paths when the archive is mounted at {RouteRootPath}, {RouteRootPath}/v/{version}, a custom route root, or a virtual directory.
  • The hidden .appsurface-docs-route-manifest.json file freezes canonical routes plus source-shaped and declared aliases from the release being exported. AppSurface Docs uses it when mounting exact archives so old aliases redirect to the archive-local canonical page instead of using whatever route rules current docs have later.
  • The hidden .appsurface-docs-release-manifest.json file records the final exported files and SHA-256 digests after materialization. Export logs the manifest SHA-256 and a copy-ready "releaseManifestSha256": "..." snippet for the AppSurface Docs version catalog.
  • Export fails with ASDOCSARCHIVE005 when the output directory contains unsupported hidden paths such as .nojekyll or .well-known/...; export to a clean directory before pinning the manifest digest.
  • Plain razorwire export remains generic and does not emit .appsurface-docs-release-manifest.json; use appsurface docs export for AppSurface Docs exact release archives that need a catalog pin.
  • Treat those exact release trees as immutable publish artifacts. The AppSurface Docs runtime mounts them later under {RouteRootPath}/v/{version} and may also mount the recommended one at {RouteRootPath}.
  • The exporter recognizes custom-root AppSurface Docs pages by their AppSurface Docs client configuration or doc-content frame, not just by a /docs URL prefix. This keeps static partial generation working for mounted roots such as /foo/bar/next.
  • Use --seeds when you want deterministic seeds for docs-specific surfaces instead of relying only on crawl discovery.
  • For release publishing, set AppSurfaceDocs__Harvest__FailOnFailure=true in the parent environment before razorwire export --project or razorwire export --dll. The launched target app inherits that setting, fails before listening when aggregate harvest health is Failed, and the export command surfaces the target app's startup output. The strict exception summary is redacted, but ordinary target host logs may still contain operator diagnostics such as repository paths or raw exception messages.

Keep razorwire export for arbitrary RazorWire apps where the CLI must launch a --project, launch a --dll, or crawl a pre-running --url. Use appsurface docs export when the AppSurface CLI owns the AppSurface Docs repository host and should avoid the generic child-process startup path.

Example:

dotnet run --project Web/ForgeTrust.RazorWire.Cli -- export -o ./dist -u http://localhost:5233
dotnet run --project Web/ForgeTrust.RazorWire.Cli -- export -o ./dist -p ./examples/razorwire-mvc/RazorWireWebExample.csproj
dotnet run --project Web/ForgeTrust.RazorWire.Cli -- export -o ./dist -d ./bin/Release/net10.0/MyApp.dll --app-args --urls --app-args http://127.0.0.1:5009

Contributor process execution policy

RazorWire CLI owns two process boundaries:

Boundary Use for Contract
Command executor Finite commands such as publish probes and MSBuild property reads. Return exit code, stdout, and stderr as data; do not throw for non-zero exits or ordinary startup failures; propagate cancellation.
Target app process Long-running app launched for crawling. Raise stdout/stderr as non-empty lines, surface startup failure before readiness timeout, raise exit after output drain when possible, and perform best-effort process-tree cleanup on disposal.

Keep command arguments tokenized as ordered argument lists. Do not build shell command strings. User-facing docs should describe observable behavior and recovery steps; implementation dependencies belong in contributor docs and tests.