Skip to content

Commit be01d39

Browse files
committed
docs: split architecture.md into a lean index plus docs/architecture/
architecture.md becomes a short index (intro, tech stack, project structure, a 'read this when…' link table) so agents load only the sub-doc they need: - docs/architecture/abstractions.md — Stack/config, FeatureDefinition, operations, shell, security - docs/architecture/data-flow.md — non-interactive + interactive flows - docs/architecture/extending.md — add a stack / feature / operation Design specs at docs/ root stay local-only; these reference docs are committed.
1 parent 0ae6681 commit be01d39

4 files changed

Lines changed: 192 additions & 170 deletions

File tree

architecture.md

Lines changed: 12 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Architecture Overview
22

3+
Index for the dAppBooster installer architecture. The detail lives in focused sub-docs under
4+
[`docs/architecture/`](./docs/architecture/) — open only the one you need rather than loading
5+
everything.
6+
7+
| Doc | Read it when you're… | Covers |
8+
|---|---|---|
9+
| [abstractions](./docs/architecture/abstractions.md) | touching the config model, operations, or shell exec | `Stack`/`StackConfig`, `FeatureDefinition` (`paths`, `requires`), operations layer, `exec`/`execFile`, security |
10+
| [data-flow](./docs/architecture/data-flow.md) | changing CLI routing or the step sequence | non-interactive validation/execution order, JSON output, interactive step flow |
11+
| [extending](./docs/architecture/extending.md) | adding a stack, feature, or operation | step-by-step checklists for each |
12+
313
## Tech Stack
414

515
| Category | Technology | Notes |
@@ -34,7 +44,7 @@ source/
3444
ProjectName.tsx Prompt for project name
3545
CloneRepo/CloneRepo.tsx Clone progress display (receives stack)
3646
InstallationMode.tsx Full / Custom selection
37-
OptionalPackages.tsx Feature multiselect (per-stack)
47+
OptionalPackages.tsx Feature multiselect (per-stack, enforces feature dependencies)
3848
Install/Install.tsx Install progress display (receives stack)
3949
FileCleanup.tsx Cleanup progress display (receives stack)
4050
PostInstall.tsx Post-install instructions, stack-specific
@@ -45,174 +55,6 @@ source/
4555
types/
4656
types.ts Shared TypeScript types
4757
utils/
48-
utils.ts Stack-aware helpers, validation, path helpers
58+
utils.ts Stack-aware helpers, feature-dependency resolution, validation, path helpers
4959
__tests__/ Mirrors source/ layout
5060
```
51-
52-
## Key Abstractions
53-
54-
### Stack (`source/constants/config.ts`)
55-
56-
```ts
57-
type Stack = 'evm' | 'canton'
58-
59-
type StackConfig = {
60-
label: string
61-
description: string
62-
repoUrl: string
63-
refType: 'tag-latest' | 'branch'
64-
ref?: string // required when refType === 'branch'
65-
packageManager: 'pnpm' | 'npm'
66-
removeAfterClone: string[] // paths nuked between clone and `git init` (empty for both stacks today)
67-
envFiles: Array<{ from: string; to: string; ifFeature?: string }>
68-
features: Record<string, FeatureDefinition>
69-
}
70-
```
71-
72-
`getStackConfig(stack)` reads the base config and overlays the env-var overrides `DAPPBOOSTER_<STACK>_REPO_URL` and `DAPPBOOSTER_<STACK>_REF` before returning — that's the single hook for retargeting either stack at a fork or pre-release branch without editing code.
73-
74-
`getFeatureNames(stack)` and `isFeatureNameValid(stack, name)` are the per-stack feature accessors. There is no global `featureDefinitions` export — that would imply a single stack.
75-
76-
### Feature Definitions
77-
78-
Stored inside each stack's `features` map. Shape:
79-
80-
```ts
81-
type FeatureDefinition = {
82-
description: string // --info output
83-
label: string // TUI multiselect display
84-
packages: string[] // package-manager packages to remove when deselected (empty for canton features today)
85-
default: boolean // --info output
86-
postInstall?: string[] // post-install instructions for non-interactive JSON output
87-
paths?: string[] // files/dirs removed when the feature is deselected (Canton, data-driven cleanup)
88-
requires?: FeatureName[] // features this one depends on (one-directional, transitive)
89-
}
90-
```
91-
92-
When adding a new feature, add it to the relevant stack's `features` map. Programmatic consumers pick it up automatically. Canton feature cleanup is fully data-driven from `paths` (see `cleanupFiles.ts` below), so a new Canton feature needs no cleanup code — only its `paths`. EVM features still need an explicit per-feature cleanup function. The CLI `--help` text in `cli.tsx` maintains its own copy in both cases.
93-
94-
**Feature dependencies (`requires`)** are resolved by pure helpers in `utils.ts`. `resolveSelectedFeatures(stack, selected)` expands a selection to include every transitive requirement (used by the non-interactive path, so `--features e2e` yields `[counter, e2e]`). `applyFeatureToggle(stack, selection, toggled, action)` keeps the interactive multiselect consistent: selecting a feature pulls its requirements in, deselecting one cascades its dependents out. `e2e requires counter` is the only dependency today. `--info` surfaces each feature's `requires` so agents can resolve dependencies themselves.
95-
96-
### Operations Layer (`source/operations/`)
97-
98-
Plain async functions, no UI dependencies. Each operation that varies per stack takes `stack: Stack` as its first argument. Multi-step operations accept an optional `onProgress` callback for the TUI; the non-interactive path omits it.
99-
100-
| Function | What it does |
101-
|---|---|
102-
| `cloneRepo(stack, projectName, onProgress?)` | Reads `stack.refType`. **tag-latest**: shallow clone with `--no-checkout`, `git fetch --tags`, then `git checkout $(git describe --tags …)` (shell required for `$()`). **branch**: shallow clone with `--branch <stack.ref> --single-branch` (no shell). After that, runs `fs.rm` for every entry in `stack.removeAfterClone` (empty for both stacks today), removes `.git`, and reinitializes with `git init`. Uses `execFile` everywhere except the tag-latest shell substitution. |
103-
| `createEnvFile(stack, projectFolder, features?)` | Copies every entry from `stack.envFiles`. Entries with `ifFeature` are skipped unless the named feature is in the selection (e.g. Canton's `carpincho-wallet/.env.local` only when `carpincho` is selected). |
104-
| `installPackages(stack, projectFolder, mode, features, onProgress?)` | Uses `stack.packageManager`. Full: `<pm> install`. Custom with packages to remove: `<pm> remove` (pnpm) or `<pm> uninstall` (npm) + `<pm> run postinstall`. Custom with all features: `<pm> install`. `execFile` only — never shell. |
105-
| `cleanupFiles(stack, projectFolder, mode, features, onProgress?)` | First runs **repository hygiene** (every stack/mode): both stacks always remove `.github` (CI) and the husky/commitlint automation (`.husky`, `.lintstagedrc.mjs`, `commitlint.config.js`) and sanitize tooling deps/scripts from `package.json`; **EVM additionally** always removes its own agent metadata (`.claude`, `AGENTS.md`, `CLAUDE.md`, `architecture.md`), whereas **Canton keeps that metadata** under the optional `llm` feature. Then dispatches to `cleanupEvmFiles` or `cleanupCantonFiles`. EVM removes deselected feature files via per-feature functions plus the `.install-files` staging directory, and patches `package.json` by feature name. Canton cleanup is **data-driven**: it loops the stack's features and, in custom mode, removes each deselected feature's `paths` (e.g. `counter/`, `e2e/`, `carpincho-wallet`, the `llm` artifact paths). The removed directories then drive `package.json` script stripping by **command target** — any script whose command invokes a removed directory is dropped (so deselecting `carpincho` strips `wallet:dev` / `carpincho:build:extension`). Command-based matching keeps cleanup correct as the upstream repo renames or adds scripts. In `full` mode no feature paths are removed, so a full Canton scaffold keeps `carpincho-wallet`, the agent docs, and every script. Canton then makes an initial `git` commit of the scaffold. |
106-
107-
### Shell Execution (`source/operations/exec.ts`)
108-
109-
Two helpers with different security profiles:
110-
111-
- **`execFile(file, args, options)`** — wraps `child_process.spawn` without a shell. Arguments are passed as an array, so user input cannot be interpreted as shell metacharacters. Use this whenever user-provided values (e.g., `projectName`) appear in the command.
112-
- **`exec(command, options)`** — wraps `child_process.spawn` to run `/bin/sh -c <command>` (spawns a shell). Only for commands that require shell features like `$(...)` substitution. Never interpolate user input into the command string.
113-
114-
Both helpers use `spawn` with stdout ignored and stderr piped. They do not capture or return stdout — output is not buffered for the caller. They throw on non-zero exit codes with the stderr message, or report the signal name when the process is killed by a signal.
115-
116-
## Data Flow
117-
118-
### Non-interactive (agent)
119-
120-
```
121-
CLI flags (string)
122-
meow parses to typed flags
123-
resolveStackFlag merges --canton / --evm / --stack and rejects conflicts
124-
validate() converts to { stack, name, mode, features: FeatureName[] }
125-
operations receive typed args (stack first)
126-
JSON output to stdout
127-
```
128-
129-
**Routing:** `source/cli.tsx`
130-
131-
```
132-
conflicting stack flagsJSON errorexit 1
133-
--infosource/info.tsprint JSON (optionally filtered by stack) → exit 0
134-
--ni / !isTTYsource/nonInteractive.tsvalidateoperationsJSON
135-
defaultdynamic import ink + App (preselectedStack passed if resolved) → TUI
136-
```
137-
138-
**Non-interactive validation order:**
139-
1. `--stack` (if explicit) is a valid stack name (else error). When unset, defaults to `evm`.
140-
2. `--name` required
141-
3. `--mode` required
142-
4. `--name` matches `/^[a-zA-Z0-9_]+$/`
143-
5. `--mode` is `full` or `custom`
144-
6. Full mode: skip to step 10 (features ignored, all stack features installed)
145-
7. `--features` required for custom mode
146-
8. Parsed features list is non-empty (rejects trailing commas, whitespace-only entries)
147-
9. Every feature name is valid **for the selected stack**
148-
10. Project directory does not already exist
149-
150-
**Non-interactive execution order:**
151-
`cloneRepo``createEnvFile``installPackages``cleanupFiles` → success JSON
152-
153-
Any error produces `{ "success": false, "error": "..." }` and exit code 1. Errors set `process.exitCode = 1` and throw rather than calling `process.exit()` directly, ensuring stdout flushes before the process terminates when piped.
154-
155-
**Success output:**
156-
```json
157-
{
158-
"success": true,
159-
"stack": "evm|canton",
160-
"projectName": "...",
161-
"mode": "full|custom",
162-
"features": ["..."],
163-
"path": "/absolute/path",
164-
"postInstall": ["..."]
165-
}
166-
```
167-
168-
For full mode, `features` lists all of the stack's feature names. For custom mode, only the selected ones.
169-
170-
### Interactive (human)
171-
172-
```
173-
User input via Ink components
174-
useState in App.tsx (stack, projectName, setupType, selectedFeatures)
175-
passed as props to step components
176-
components convert MultiSelectItem[] → FeatureName[]
177-
operations receive typed args (stack first)
178-
Ink renders progress/status
179-
```
180-
181-
Steps: StackSelection → ProjectName → CloneRepo → InstallationMode → OptionalPackages → Install → FileCleanup → PostInstall
182-
183-
When `cli.tsx` resolves a stack flag, it passes `preselectedStack` to `<App>`, which skips the StackSelection step by starting `currentStep` at 2.
184-
185-
Components are presentation-only — they call operations via `useEffect` and render status. Components receive `MultiSelectItem[]` for feature selection (TUI concern) and convert to `FeatureName[]` before calling operations. `PostInstall` renders stack-specific instructions; the EVM branch shows the subgraph warning when applicable, the Canton branch shows the `canton:up`/`app:dev` commands and — when the `carpincho` feature is selected (or full mode) — the Carpincho extension build/load instructions.
186-
187-
## How to Add a New Stack
188-
189-
1. **`source/constants/config.ts`** — add a `Stack` union member and a `stackDefinitions` entry: `label`, `description`, `repoUrl`, `refType`, optional `ref`, `packageManager`, `removeAfterClone`, `envFiles`, `features`.
190-
2. **`source/operations/cleanupFiles.ts`** — add a `cleanupXxxFiles` function and route to it from the top-level `cleanupFiles` dispatcher.
191-
3. **`source/components/steps/PostInstall.tsx`** — add stack-specific post-install JSX.
192-
4. **`source/cli.tsx`** — add a shortcut flag (e.g. `--myStack`) and extend `resolveStackFlag`; update `--help` text.
193-
5. **Tests** — add per-stack assertions to `nonInteractive.test.ts`, `info.test.ts`, `cloneRepo.test.ts`, `installPackages.test.ts`, `cleanupFiles.test.ts`, `createEnvFile.test.ts`.
194-
6. **Verify** — `pnpm build && pnpm lint && pnpm test`. Smoke-test with `DAPPBOOSTER_<STACK>_REPO_URL=file:///path/to/local/clone`.
195-
196-
## How to Add a New Feature to an Existing Stack
197-
198-
1. **`source/constants/config.ts`**add an entry to the stack's `features` map. For **Canton**, also list the feature's `paths`: cleanup is data-driven, so no cleanup code is needed and scripts that target a removed directory are stripped automatically. If it ships an env file, add an `ifFeature`-gated `envFiles` entry.
199-
2. **`source/operations/cleanupFiles.ts`****EVM only**: add a cleanup function for the feature and call it from `cleanupEvmFiles` when deselected; if it has scripts, add removal to `patchPackageJsonEvm`. Canton needs no change here.
200-
3. **`source/components/steps/PostInstall.tsx`**extend stack-specific instructions if needed.
201-
4. **`source/cli.tsx`**update the `--help` text.
202-
5. **Tests**add assertions in the relevant test files. nonInteractive, info, installPackages, and utils tests pick up new features automatically through `stackDefinitions`.
203-
6. **Verify**`pnpm build && pnpm lint && pnpm test`.
204-
205-
## How to Add a New Operation
206-
207-
1. Create `source/operations/newOperation.ts`export an async function. Use `execFile` for commands with user input, `exec` only when shell features are needed. If behavior depends on the stack, take `stack: Stack` as the first argument.
208-
2. Export from `source/operations/index.ts`.
209-
3. Call from `source/nonInteractive.ts` (in the execution sequence) and from the relevant TUI component.
210-
4. Add tests in `source/__tests__/operations/newOperation.test.ts`mock `exec`/`execFile` to verify correct commands.
211-
212-
## Security
213-
214-
- User input (`projectName`) is validated against `/^[a-zA-Z0-9_]+$/` before any use.
215-
- Operations use `execFile` (no shell) for commands that include user input or stack-config values.
216-
- `exec` (shell) is reserved for the EVM tag-latest checkout (`git checkout $(git describe …)`); it never receives user input in the command string.
217-
- Stack `repoUrl` and `ref` may come from the environment (`DAPPBOOSTER_<STACK>_REPO_URL`, `DAPPBOOSTER_<STACK>_REF`) but are passed to git via `execFile`, not interpolated into shell strings.
218-
- Child process stdout is ignored and stderr is piped (captured for error diagnostics only), guaranteeing clean JSON on the parent's stdout.

0 commit comments

Comments
 (0)