Skip to content

Commit 93f8914

Browse files
authored
Merge pull request #2 from BootNodeDev/feat/non-interactive-cli
2 parents 9d43f35 + 1fd205b commit 93f8914

37 files changed

Lines changed: 3324 additions & 657 deletions

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## Summary
2+
3+
<!-- Why this change? What problem does it solve? Link motivation, not just mechanics. -->
4+
5+
Closes #
6+
7+
## Changes
8+
9+
<!-- Brief description of what was changed. Bullet points work well. -->
10+
11+
-
12+
13+
## Acceptance criteria
14+
15+
<!-- Mirror the criteria from the linked issue. Check them off as you go. -->
16+
17+
- [ ]
18+
19+
## Test plan
20+
21+
<!-- How was this tested? Commands, screenshots, recordings — show your work. -->
22+
23+
## Breaking changes
24+
25+
<!-- If none, delete this section. If any, describe what breaks and migration steps. -->
26+
27+
None.
28+
29+
## Checklist
30+
31+
- [ ] Self-reviewed my own diff
32+
- [ ] Tests added or updated
33+
- [ ] Docs updated (if applicable)
34+
- [ ] No unrelated changes bundled in

AGENTS.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Agent Configuration
2+
3+
> `CLAUDE.md` points to this file so Claude Code picks it up automatically. Other agents (Cursor, Windsurf, etc.) read `AGENTS.md` natively.
4+
5+
---
6+
7+
## What This Is
8+
9+
A CLI installer tool for dAppBooster projects. It supports two modes:
10+
11+
- **Interactive** (default): React + Ink TUI that walks users through project naming, repo cloning, installation mode selection, optional packages, and post-install steps.
12+
- **Non-interactive**: Flag-driven mode (`--ni` or auto-detected when not a TTY) for AI agents and CI. Outputs JSON to stdout. Run `--info` for feature discovery, then `--name` + `--mode` [+ `--features`] to install.
13+
14+
## Stack & Conventions
15+
16+
| Category | Technology | Notes |
17+
|----------|-----------|-------|
18+
| Language | TypeScript (strict mode) | Extends `@sindresorhus/tsconfig` |
19+
| Framework | React + Ink | Terminal UI framework |
20+
| Arg parsing | meow | CLI flag parsing for non-interactive mode |
21+
| Package manager | pnpm | Never npm or yarn |
22+
| Linting/formatting | Biome | Run `pnpm lint` before committing |
23+
| Testing | Vitest + @vitest/coverage-v8 | |
24+
| Node | v20+ | See `.nvmrc` |
25+
| Naming | camelCase vars/functions, PascalCase components/types | |
26+
27+
## Code Style
28+
29+
- **Semicolons:** as needed (Biome `asNeeded` — omitted unless required by ASI)
30+
- **Quotes:** single
31+
- **Print width:** 100
32+
- **Trailing commas:** all (Biome default)
33+
- **Indent:** spaces, width 2
34+
- **Imports:** explicit `.js` extensions (ESM, `"type": "module"`)
35+
36+
## Working Rules
37+
38+
- Use **pnpm** only (never npm or yarn)
39+
- Treat `dist/` as build output — never edit directly
40+
- User input (`projectName`) must never be interpolated into shell command strings — use `execFile` (args array) instead
41+
- `source/constants/config.ts` is the single source of truth for feature metadata — all programmatic consumers read from it (CLI `--help` text maintains its own copy)
42+
- Components are presentation-only — business logic lives in `source/operations/`
43+
44+
## Architecture
45+
46+
See [architecture.md](./architecture.md) for the full architecture guide, including data flow, how to add features, and security patterns.
47+
48+
Entry: `source/cli.tsx` — parses args with `meow`, routes between interactive and non-interactive paths.
49+
50+
- **Interactive path**: `source/app.tsx` — step-based state machine that renders each installer step in sequence via React + Ink
51+
- **Non-interactive path**: `source/nonInteractive.ts` — validates flags, runs operations sequentially, outputs JSON
52+
53+
Key directories:
54+
55+
- `source/operations/` — business logic as plain async functions, shared by both paths
56+
- `source/components/steps/` — TUI step components, presentation-only
57+
- `source/components/` — reusable UI components (Ask, Divider, MainTitle, Multiselect)
58+
- `source/__tests__/` — vitest test suite
59+
60+
## Testing
61+
62+
- **Framework:** Vitest + V8 coverage
63+
- **Run tests:** `pnpm test` / `pnpm test:coverage`
64+
- **Structure:** `source/__tests__/` mirrors `source/` layout. Operations tests live in `source/__tests__/operations/`
65+
- **What to test:** Non-interactive agentic flow (validation, JSON output), operations (correct shell commands), config, utils
66+
- **What not to test:** React/Ink components
67+
- **Mocking pattern:** Operations tests mock `exec`/`execFile` from `source/operations/exec.js`. `exec.test.ts` mocks `child_process.spawn` directly to test the helpers themselves. Non-interactive tests mock the entire operations layer
68+
- **Coverage:** Focus on the agentic interface. Test files and `source/components/` are excluded from coverage
69+
70+
## Guardrails
71+
72+
- Do not commit secrets, API keys, or credentials
73+
- Do not modify CI/CD pipelines without team review
74+
- Do not skip tests or linting to make a build pass
75+
- When in doubt, ask — don't assume
76+
77+
## Change Strategy
78+
79+
- Prefer small, focused diffs over broad refactors
80+
- Preserve existing UX unless the task explicitly changes it
81+
- Avoid introducing new patterns when a project pattern already exists
82+
- Update docs only when behavior or workflow changes
83+
84+
## Validation Checklist
85+
86+
- `pnpm build`
87+
- `pnpm lint`
88+
- `pnpm test`
89+
90+
## Release
91+
92+
GitHub Actions workflow (`.github/workflows/release.yml`) triggers on GitHub release events. Pre-releases do a dry-run; full releases publish to npm.

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# CLAUDE.md
2+
3+
See [AGENTS.md](./AGENTS.md) for all project guidance.

architecture.md

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Architecture Overview
2+
3+
## Tech Stack
4+
5+
| Category | Technology | Notes |
6+
|----------|-----------|-------|
7+
| Framework | React + Ink | Terminal UI for interactive mode |
8+
| Language | TypeScript (strict mode) | Extends `@sindresorhus/tsconfig` |
9+
| Arg parsing | meow | CLI flag parsing, non-interactive mode |
10+
| Styling | Ink primitives | `<Box>`, `<Text>`, ink-gradient, ink-big-text |
11+
| Testing | Vitest + @vitest/coverage-v8 | |
12+
| Node | v20+ | See `.nvmrc` |
13+
14+
## Project Structure
15+
16+
```
17+
source/
18+
cli.tsx Entry point: meow arg parsing, mode routing
19+
app.tsx Interactive TUI: step-based state machine
20+
nonInteractive.ts Non-interactive: validate flags → run operations → JSON
21+
info.ts --info JSON output for agent discovery
22+
constants/
23+
config.ts Single source of truth: feature definitions, repo URL
24+
operations/
25+
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
30+
index.ts Barrel export
31+
components/
32+
steps/ TUI step components (presentation-only)
33+
ProjectName.tsx Prompt for project name
34+
CloneRepo/CloneRepo.tsx Clone progress display
35+
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
40+
Ask.tsx Text input with validation
41+
Divider.tsx Section divider
42+
MainTitle.tsx Gradient title banner
43+
Multiselect/ Checkbox multiselect component
44+
types/
45+
types.ts Shared TypeScript types
46+
utils/
47+
utils.ts Validation, path helpers, package resolution
48+
__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+
```
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

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"scripts": {
1111
"build": "tsc",
1212
"dev": "tsc --watch",
13+
"test": "vitest run",
14+
"test:coverage": "vitest run --coverage",
1315
"lint": "pnpm biome check",
1416
"lint:fix": "pnpm biome check --write"
1517
},
@@ -22,17 +24,19 @@
2224
"ink-gradient": "^3.0.0",
2325
"ink-link": "^4.1.0",
2426
"ink-select-input": "^6.2.0",
25-
"ink-spawn": "^0.1.4",
2627
"ink-text-input": "^6.0.0",
28+
"meow": "^14.1.0",
2729
"react": "^18.3.1"
2830
},
2931
"devDependencies": {
3032
"@biomejs/biome": "^1.9.4",
3133
"@sindresorhus/tsconfig": "^7.0.0",
3234
"@types/node": "^22.15.21",
3335
"@types/react": "^18.3.22",
36+
"@vitest/coverage-v8": "^4.1.0",
3437
"ts-node": "^10.9.1",
35-
"typescript": "^5.8.3"
38+
"typescript": "^5.8.3",
39+
"vitest": "^4.1.0"
3640
},
3741
"pnpm": {
3842
"onlyBuiltDependencies": ["@biomejs/biome"]

0 commit comments

Comments
 (0)