@mohantn/scaffold-core
v0.2.13
Published
Deterministic, LLM-agnostic scaffolding CLI: renders Handlebars templates and injects marker-based boilerplate into existing files for any coding agent to drive.
Maintainers
Readme
@mohantn/scaffold-core
A deterministic, LLM-agnostic scaffolding CLI (scaffold). It never calls an LLM: it renders Handlebars templates from a versioned template pack and injects marker-based boilerplate into existing files, then prints a TOON (or JSON) report a host coding agent can read back. See the monorepo root README for the overall architecture.
Install
npm install -g @mohantn/scaffold-coreor invoke it without installing via npx @mohantn/scaffold-core.
Commands
scaffold init [--project-type <type>] [--pack <name>=<path>@<version> ...]— writes.scaffold/config.json. Without--project-type, the target repo is sniffed (*.csproj/*.sln→ dotnet, areact/next/etc. dependency → js-family,go.mod→ go,pyproject.toml/requirements.txt→ python) with a single interactive prompt as a last resort.--packseeds thepacksmap (repeatable, one per stack name) as a local-directory entry, e.g.--pack backend=packages/templates-dotnet@v8-controller; a git-URL-shaped spec is rejected with a pointer to this syntax.scaffold templates sync [--update]— for aurl-based pack, clones or reuses it into a local cache keyed bysha256(normalizedUrl)/<resolvedSha>, and--updatemoves the pinned SHA forward to the remote's current HEAD. For apath-based pack (whatscaffold initwrites), this is a no-op — there is no clone or cache, the pack is read straight off disk on everygenerate— but still safe to run.scaffold templates list— lists the version folders available in the configured pack(s): for apath-based pack, the directory's own version folders; for aurl-based pack, its cached checkout.scaffold generate --manifest <file.toon|.json> [--dry-run] [--force] [--json]— validates the intent manifest and the resolved pack's descriptor, renderscreate-mode targets, injects registration snippets at paired text markers, and prints a report of what was created/injected plus anyAI_IMPLEMENTATIONblocks still needing a host agent's attention. Rejects anyoverwrite-mode target that doesn't already exist on disk (create a new file via acreateorskip-if-existstarget first); exits non-zero and writes nothing on this gate rejection, ensuring a misaligned pack descriptor and target repo can never create new files in overwrite mode.scaffold status [--json]— rescans.scaffold/pending/*.json; exits non-zero while any trackedAI_IMPLEMENTATIONblock from a priorgenerateis still unfilled. A block is tracked when it shipped empty, or when the pack tagged its start marker:required(SCAFFOLD:AI_IMPLEMENTATION:START:required/AI_IMPLEMENTATION_START:required) — the business-logic seams the host agent must complete even though the shipped placeholder already compiles. A block resolves once its content changes from the shipped placeholder.scaffold validate-pack --pack <dir> [--pack-version <version>] --manifest <file> [--json]— smoke-tests a local template pack by running a real generate against a synthesized throwaway target repo (host-provided injection targets likeProgram.csare stood up with empty marker pairs first). Unlike a render-only check it exercises injection-path resolution, the comment-syntax table, the marker scanner, and the descriptorrequirescheck. Validates every version folder unless--pack-versionnarrows it; exits non-zero if any version fails.scaffold undo <changesetId> [--force]— reverts a priorgeneraterun: deletes files it created, restores files it modified to their exact prior content. Refuses on a hash mismatch (something else edited the file since) or if a later changeset also touched the same file, unless--force.scaffold bootstrap-markers [--pack-version <version>] [--dry-run] [--json]— bootstraps emptySCAFFOLD:<marker>:START/ENDpairs into a brownfield repo's existing source files, one-time and idempotent, so a plainscaffold generatecan later find and fill them. Without--pack-version, it reads every entry in.scaffold/config.json'spacksmap and runs one pass per configured pack;--pack-versionruns a single pass against that version directly (no config file required). Exits non-zero while any marker is leftneeds-manual.The catalog is keyed by the exact configured pack version, not the coarse
projectTypebucket, since the marker set andProgram.cszones differ between a base pack and its GCP sibling. Four versions are known, matchingpackages/templates-dotnet's own marker table:| Pack version |
Program.csbuilder-zone markers |Program.csapp-zone markers | |---|---|---| |v8-controller|DI| (none) | |v10-minimal-api|DI|MIDDLEWARE,ROUTES| |v8-controller-gcp|GSM,DI,PUBSUB,SAGAS| (none) | |v10-minimal-api-gcp|GSM,DI,PUBSUB,SAGAS|MIDDLEWARE,ROUTES|Every version also places
DBSETSintoAppDbContext.csandREPOSITORIESintoApplicationServiceCollectionExtensions.cs, both found by locating the class's own opening brace rather than a single-line anchor (no universal single-line anchor exists in either file). Within a zone, markers are always placed as one contiguous block in the order listed above:GSMprecedesDIbecauseInfrastructureServiceCollectionExtensions.cs'sAddInfrastructurereads a connection-string config key at registration time thatGSMpopulates, andMIDDLEWAREprecedesROUTESto match request-pipeline ordering.Placement never guesses: zero or multiple candidate files for a marker group, an ambiguous anchor line, or a one-sided/duplicated existing marker all fall back to a
needs-manualreport entry (with a reason) rather than writing anything for that marker. A marker already present anywhere in the file — even hand-moved by a developer — is left untouched and reportedalready-present, never duplicated. Inside a git working tree, a target file must be tracked and clean (git status --porcelainempty) before it's touched; a dirty or untracked file is reportedneeds-manualwith a git-state reason instead. Outside a git working tree this check is skipped entirely.A configured pack slot whose version has no catalog entry at all (e.g. a
frontendslot pointing at a non-dotnet pack) is reported under a separateunsupportedPacksfield, neverneedsManual— there is no per-marker action to take for a slot the catalog doesn't cover, so it never blocks a clean exit once every actionable marker elsewhere is resolved.scaffold -v/--version— prints the installed version.
.scaffold/config.json
{
"projectType": "dotnet",
"packs": {
"backend": { "path": "packages/templates-dotnet", "version": "v10-minimal-api" }
}
}A pack entry is either path (a local directory, read straight off disk — what scaffold init writes) or url (a git-clonable remote, resolved through templates sync into a local cache and pinned by pinnedSha) — never both. Either way it's one folder per target-stack version, each holding Handlebars templates plus a manifest.templates.json descriptor. See the plan document / root README for the full descriptor and intent-manifest schemas.
