|
1 | 1 | # Architecture Overview |
2 | 2 |
|
| 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 | + |
3 | 13 | ## Tech Stack |
4 | 14 |
|
5 | 15 | | Category | Technology | Notes | |
|
15 | 25 |
|
16 | 26 | ``` |
17 | 27 | source/ |
18 | | - cli.tsx Entry point: meow arg parsing, mode routing |
19 | | - app.tsx Interactive TUI: step-based state machine |
| 28 | + cli.tsx Entry point: meow arg parsing, stack resolution, mode routing |
| 29 | + app.tsx Interactive TUI: step-based state machine, threads `stack` through every step |
20 | 30 | nonInteractive.ts Non-interactive: validate flags → run operations → JSON |
21 | | - info.ts --info JSON output for agent discovery |
| 31 | + info.ts --info JSON output for agent discovery (optionally filtered by stack) |
22 | 32 | constants/ |
23 | | - config.ts Single source of truth: feature definitions, repo URL |
| 33 | + config.ts Single source of truth: Stack type, stackDefinitions, env-var overrides |
24 | 34 | operations/ |
25 | 35 | exec.ts exec (shell) and execFile (no shell) helpers |
26 | | - cloneRepo.ts Shallow clone, checkout latest tag, reinit git |
27 | | - createEnvFile.ts Copy .env.example → .env.local |
28 | | - installPackages.ts pnpm install / remove based on mode and features |
29 | | - cleanupFiles.ts Remove files for deselected features, patch package.json |
| 36 | + cloneRepo.ts Clone (tag-latest OR branch), apply stack.removeAfterClone, rm .git, git init |
| 37 | + createEnvFile.ts Copy each stack's envFiles (with optional ifFeature gate) |
| 38 | + installPackages.ts Stack-aware: uses stack.packageManager (pnpm or npm) |
| 39 | + cleanupFiles.ts Dispatches to per-stack cleanup (cleanupEvmFiles / cleanupCantonFiles) |
| 40 | + installGuard.ts Removes the partial project dir if interrupted mid-scaffold |
30 | 41 | index.ts Barrel export |
31 | 42 | components/ |
32 | 43 | steps/ TUI step components (presentation-only) |
| 44 | + StackSelection.tsx First step: pick a stack (skipped if preselectedStack is passed) |
33 | 45 | ProjectName.tsx Prompt for project name |
34 | | - CloneRepo/CloneRepo.tsx Clone progress display |
| 46 | + CloneRepo/CloneRepo.tsx Clone progress display (receives stack) |
35 | 47 | InstallationMode.tsx Full / Custom selection |
36 | | - OptionalPackages.tsx Feature multiselect |
37 | | - Install/Install.tsx Install progress display |
38 | | - FileCleanup.tsx Cleanup progress display |
39 | | - PostInstall.tsx Post-install instructions |
| 48 | + OptionalPackages.tsx Feature multiselect (per-stack, enforces feature dependencies) |
| 49 | + Install/Install.tsx Install progress display (receives stack) |
| 50 | + FileCleanup.tsx Cleanup progress display (receives stack) |
| 51 | + PostInstall.tsx Post-install instructions, stack-specific |
40 | 52 | Ask.tsx Text input with validation |
41 | 53 | Divider.tsx Section divider |
42 | 54 | MainTitle.tsx Gradient title banner |
43 | 55 | Multiselect/ Checkbox multiselect component |
44 | 56 | types/ |
45 | 57 | types.ts Shared TypeScript types |
46 | 58 | utils/ |
47 | | - utils.ts Validation, path helpers, package resolution |
| 59 | + utils.ts Stack-aware helpers, feature-dependency resolution, validation, path helpers |
48 | 60 | __tests__/ Mirrors source/ layout |
49 | | - nonInteractive.test.ts |
50 | | - info.test.ts |
51 | | - utils.test.ts |
52 | | - operations/ |
53 | | - exec.test.ts |
54 | | - cloneRepo.test.ts |
55 | | - createEnvFile.test.ts |
56 | | - installPackages.test.ts |
57 | | - cleanupFiles.test.ts |
58 | | -``` |
59 | | - |
60 | | -## Key Abstractions |
61 | | - |
62 | | -### Feature Definitions (`source/constants/config.ts`) |
63 | | - |
64 | | -Single source of truth for feature metadata. All programmatic consumers (`--info`, validation, TUI multiselect, operations) read from here. CLI `--help` text maintains its own copy. |
65 | | - |
66 | | -```ts |
67 | | -featureDefinitions: Record<FeatureName, { |
68 | | - description: string // --info output |
69 | | - label: string // TUI multiselect display |
70 | | - packages: string[] // pnpm packages to remove when deselected |
71 | | - default: boolean // --info output |
72 | | - postInstall?: string[] // post-install instructions for non-interactive JSON output |
73 | | -}> |
74 | | -``` |
75 | | - |
76 | | -`featureNames` is derived as `Object.keys(featureDefinitions)`. |
77 | | - |
78 | | -When adding a new feature, add it here. Programmatic consumers (validation, info output, TUI selection) pick it up automatically — except `cleanupFiles.ts` (which needs explicit cleanup rules) and the CLI `--help` text in `cli.tsx` (which maintains its own copy). |
79 | | - |
80 | | -### Operations Layer (`source/operations/`) |
81 | | - |
82 | | -Plain async functions with no UI dependencies. Each operation receives explicit arguments (project folder, mode, features) and performs file system or shell work. Multi-step operations accept an optional `onProgress` callback that the TUI uses to render per-step progress; the non-interactive path omits it. |
83 | | - |
84 | | -| Function | What it does | |
85 | | -|---|---| |
86 | | -| `cloneRepo(projectName, onProgress?)` | Shallow clone, fetch tags, checkout latest tag, rm .git, git init. Uses `execFile` (no shell) for git commands except `git checkout $(...)` which needs shell substitution. Uses `fs.rm` for .git removal. | |
87 | | -| `createEnvFile(projectFolder)` | Copy .env.example to .env.local via `fs.copyFile` | |
88 | | -| `installPackages(projectFolder, mode, features, onProgress?)` | Full: `pnpm i`. Custom with packages to remove: `pnpm remove` + postinstall. Custom with all features: `pnpm i`. Uses `execFile` exclusively (no shell). | |
89 | | -| `cleanupFiles(projectFolder, mode, features, onProgress?)` | Remove files/folders for deselected features, patch package.json scripts, remove .install-files. Uses `node:fs/promises` (`rm`, `mkdir`, `copyFile`) for async operations; `patchPackageJson` uses sync `node:fs`. | |
90 | | - |
91 | | -### Shell Execution (`source/operations/exec.ts`) |
92 | | - |
93 | | -Two helpers with different security profiles: |
94 | | - |
95 | | -- **`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. |
96 | | -- **`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. |
97 | | - |
98 | | -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. |
99 | | - |
100 | | -## Data Flow |
101 | | - |
102 | | -### Non-interactive (agent) |
103 | | - |
104 | | -``` |
105 | | -CLI flags (string) |
106 | | - → meow parses to typed flags |
107 | | - → validate() converts to { name, mode, features: FeatureName[] } |
108 | | - → operations receive typed args |
109 | | - → JSON output to stdout |
110 | | -``` |
111 | | - |
112 | | -**Routing:** `source/cli.tsx` |
113 | | - |
114 | | -``` |
115 | | ---info → source/info.ts → print JSON → exit 0 |
116 | | ---ni / !isTTY → source/nonInteractive.ts → validate → operations → JSON |
117 | | -default → dynamic import ink + App → TUI |
118 | | -``` |
119 | | - |
120 | | -**Non-interactive validation order:** |
121 | | -1. `--name` required |
122 | | -2. `--mode` required |
123 | | -3. `--name` matches `/^[a-zA-Z0-9_]+$/` |
124 | | -4. `--mode` is `full` or `custom` |
125 | | -5. Full mode: skip to step 9 (features ignored, all installed) |
126 | | -6. `--features` required for custom mode |
127 | | -7. Parsed features list is non-empty (rejects trailing commas, whitespace-only entries) |
128 | | -8. All feature names are valid keys in `featureDefinitions` |
129 | | -9. Project directory does not already exist |
130 | | - |
131 | | -**Non-interactive execution order:** |
132 | | -`cloneRepo` → `createEnvFile` → `installPackages` → `cleanupFiles` → success JSON |
133 | | - |
134 | | -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. |
135 | | - |
136 | | -**Success output:** |
137 | | -```json |
138 | | -{ |
139 | | - "success": true, |
140 | | - "projectName": "...", |
141 | | - "mode": "full|custom", |
142 | | - "features": ["..."], |
143 | | - "path": "/absolute/path", |
144 | | - "postInstall": ["..."] |
145 | | -} |
146 | 61 | ``` |
147 | | - |
148 | | -For full mode, `features` lists all feature names. For custom mode, only the selected ones. |
149 | | - |
150 | | -### Interactive (human) |
151 | | - |
152 | | -``` |
153 | | -User input via Ink components |
154 | | - → useState in App.tsx |
155 | | - → passed as props to step components |
156 | | - → components convert MultiSelectItem[] → FeatureName[] |
157 | | - → operations receive typed args |
158 | | - → Ink renders progress/status |
159 | | -``` |
160 | | - |
161 | | -Steps: ProjectName → CloneRepo → InstallationMode → OptionalPackages → Install → FileCleanup → PostInstall |
162 | | - |
163 | | -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. |
164 | | - |
165 | | -## How to Add a New Feature |
166 | | - |
167 | | -1. **`source/constants/config.ts`** — add entry to `featureDefinitions` with description, label, packages, default, and optional postInstall. Add the name to the `FeatureName` union type. |
168 | | - |
169 | | -2. **`source/operations/cleanupFiles.ts`** — add a cleanup function and call it from `cleanupFiles()` when the feature is deselected. If the feature has scripts in package.json, add removal to `patchPackageJson`. |
170 | | - |
171 | | -3. **`source/components/steps/PostInstall.tsx`** — if the feature has post-install instructions, add TUI rendering here. The component hardcodes its own display (richer than the `postInstall` strings in config), so new features with post-install steps need manual JSX. |
172 | | - |
173 | | -4. **`source/cli.tsx`** — update the `--help` text to include the new feature name and description. |
174 | | - |
175 | | -5. **Tests** — add test cases in `source/__tests__/operations/cleanupFiles.test.ts` for the new cleanup rules. The nonInteractive, info, installPackages, and utils tests pick up new features automatically since they read from `featureDefinitions`. |
176 | | - |
177 | | -6. **Verify** — `pnpm build && pnpm lint && pnpm test` |
178 | | - |
179 | | -Steps 1 and 6 are always required. Steps 2-5 depend on whether the feature has cleanup rules, post-install instructions, or descriptions for `--help`. |
180 | | - |
181 | | -## How to Add a New Operation |
182 | | - |
183 | | -1. Create `source/operations/newOperation.ts` — export an async function. Use `execFile` for commands with user input, `exec` only when shell features are needed. |
184 | | - |
185 | | -2. Export from `source/operations/index.ts`. |
186 | | - |
187 | | -3. Call from `source/nonInteractive.ts` (in the execution sequence) and from the relevant TUI component. |
188 | | - |
189 | | -4. Add tests in `source/__tests__/operations/newOperation.test.ts` — mock `exec`/`execFile` to verify correct commands. |
190 | | - |
191 | | -## Security |
192 | | - |
193 | | -- User input (`projectName`) is validated against `/^[a-zA-Z0-9_]+$/` before any use |
194 | | -- Operations use `execFile` (no shell) for commands that include user input |
195 | | -- `exec` (shell) is reserved for commands needing shell substitution, and never receives user input in the command string |
196 | | -- 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