Stack-agnostic scaffolding CLI with a plugin-based generator system.
Three plugins implemented and tested:
electron-ts— Electron + TypeScript desktop app shell (sandboxed windows, external-link policy, narrow preload bridge), with optional Settings (Dark Mode), Skins, and About/Help features.web-app-spa— client-side app (React, Vue, or vanilla + Vite) that calls APIs over fetch; no SSR.web-app-standalone-html— a single, dependency-free HTML file — no server, no network call after the initial load.
Built to stop re-solving the same app shell from scratch for each new
personal project (starting from the md-view / md-presenter lineage
for electron-ts), but designed stack-agnostic from day one: the CLI
core doesn't know about Electron, React, Vue, or plain HTML — each lives
entirely in its own plugin. Adding support for another stack later means
writing another plugin, not touching the core.
Clean-architecture-ish layering, dependencies point inward:
src/
core/ domain types + pure orchestration logic (no I/O)
plugins/ one folder per supported stack, each implementing the
plugin contract (electron-ts, web-app-spa,
web-app-standalone-html)
infra/ adapters: file system, git (execa), prompts (@inquirer),
plugin discovery (cosmiconfig), expression evaluation
(json-logic-js)
cli/ composition root — commander wiring, entry point
core/ has zero dependencies on infra/ or plugins/; it only depends
on its own types. cli/ is the only layer allowed to wire concrete
adapters into the orchestrator.
Four ports define the boundary between core/ and infra/: Prompter,
FileWriter, ProcessRunner, and ExpressionEvaluator. The last
resolves an expression-shaped value (e.g. { "var": "appName" }) against
an answers context; its sole adapter, infra/expressionEvaluator.ts, is a
thin wrapper over json-logic-js —
a restricted, non-Turing-complete rule format, never an eval.
core/authoring.ts adds a second way to produce a StackfoldPlugin: a
ScaffoldDefinition — plain JSON describing questions, an ordered list
of ActionSteps, and optional lifecycle hooks (preInit, postWrite,
postInit) — compiles via compileScaffold() into a plugin
indistinguishable at runtime from a hand-written one.
Built-in action types: mkdir, writeFile, runCommand, npmInstall,
gitInit (copyFile was removed — it was identical to writeFile with
no real copy semantics). File content in writeFile is templated with
Handlebars. New
action types can be added via registerAction() without touching the
orchestrator.
This is internal and experimental — none of the three plugins in
Status use it yet; they remain hand-written via the
questions()/buildBlueprint() contract, and there is no forced
migration. The declarative authoring system is the source of truth going
forward: ProjectBlueprint is just one possible outcome it produces.
Readers who want the full rationale can look up ADR SK-ADR-002 in this
project's governance docs (not included in this repository).
- commander — CLI framework
- @inquirer/prompts — interactive prompts
- execa — process execution
- cosmiconfig — plugin/config discovery
- zod — validation
- handlebars — file content templating in the declarative authoring system
- json-logic-js — expression evaluation behind the
ExpressionEvaluatorport - TypeScript 6.x (deliberately not 7's native compiler yet —
typescript-eslintsupport isn't there for it as of this writing)
npm install
npm run dev -- init electron-ts # run the CLI without building
npm run dev -- init web-app-spa
npm run dev -- init web-app-standalone-html
npm run build # tsc -> dist/
npm test # vitest
npm run typecheck # tsc --noEmitinit also runs non-interactively via --config <path> (answers loaded
from a json/yaml/js file through cosmiconfig) and/or repeatable
--answer key=value flags (--answer wins over --config on a shared
key; neither flag given keeps the normal interactive prompts):
stackfold init electron-ts ./out --config config.json
stackfold init electron-ts ./out --answer appName="CI Smoke Test" --answer includeSettings=falsestackfold list shows every registered plugin, and stackfold info <stack>
shows one plugin's detailed help (usage notes, prerequisites, the
questions it will ask). A plugin's richer help (display name, details
prose) is optional in the plugin contract — stackfold --help/-h and
the two commands above work the same with or without it, falling back to
the plugin's id and its mandatory one-line description:
stackfold list
stackfold info electron-tsMIT — see LICENSE.