Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Agent Configuration
# Agent configuration

This project's agent configuration lives in [`CLAUDE.md`](./CLAUDE.md).

Claude Code reads `CLAUDE.md` natively. If your agent reads `AGENTS.md` (Cursor, Windsurf, etc.), load rules from `CLAUDE.md` -- it is the single source of truth for this project's conventions, stack, and working rules.
Claude Code reads `CLAUDE.md` natively. If your agent reads `AGENTS.md` (Cursor, Windsurf, etc.), load rules from `CLAUDE.md`. It is the single source of truth for this project's conventions, stack, and working rules.
144 changes: 75 additions & 69 deletions CLAUDE.md

Large diffs are not rendered by default.

265 changes: 106 additions & 159 deletions architecture.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 14 additions & 36 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# dAppBooster installer

Scaffolds a new project from [dAppBooster](https://dappbooster.dev/), our blockchain boilerplate for EVM projects.

Looking for Canton? It has its own installer: [`create-canton-dappbooster`](https://github.com/BootNodeDev/canton-dappbooster/tree/main/canton-create).

## Requirements

- Node >= 24.15.0
- pnpm

Requirements for each dAppBooster stack might differ, check the [EVM](https://github.com/bootnodedev/dappbooster/) and [Canton](https://github.com/BootNodeDev/canton-dappbooster) repos for more info.

## Interactive mode

<img src="./demo.svg" width="600" height="355" alt="Terminal recording of the dAppBooster wizard.">
Expand All @@ -15,44 +17,26 @@ Requirements for each dAppBooster stack might differ, check the [EVM](https://gi
pnpm dlx dappbooster
```

The wizard will guide you through the different choices available: project name, stack, features, etc.
The wizard guides you through the choices: project name, installation mode and features.

Alternatively you can start with a pre-selected stack:

- **EVM:** our [blockchain boilerplate](https://dappbooster.dev/) built to quickly get you started with your next EVM project.
- **Canton:** our [local development stack](https://www.dappbooster.cc/) for the Canton network.

```shell
pnpm dlx dappbooster --evm # EVM stack
pnpm dlx dappbooster --canton # Canton stack
```

## Non-interactive mode (Agents & CI)
## Non-interactive mode (agents & CI)

| Flag | Purpose |
|---|---|
| `--canton` / `--evm` | Pick the stack (mutually exclusive shortcuts) |
| `--stack <evm\|canton>` | Pick the stack by name (useful when scripting) |
| `--name <name>` | Project directory name (`/^[a-zA-Z0-9_][a-zA-Z0-9_-]*$/`, so no leading dash) |
| `--mode <full\|custom>` | `full` installs every feature; `custom` needs `--features`. Only for a stack whose `modes` list is not empty |
| `--mode <full\|custom>` | `full` installs every feature; `custom` needs `--features` |
| `--features <a,b,c>` | Comma-separated feature keys (custom mode only) |
| `--info` | Print stacks and features as JSON and exit. Filter with `--stack`, `--canton` or `--evm` |
| `--ni`, `--non-interactive` | Force non-interactive mode. Auto-enabled when stdout is not a TTY |
| `--info` | Print the features as JSON and exit |
| `--ni`, `--non-interactive` | Force non-interactive mode. It turns on by itself when stdout is not a TTY |
| `--help` | Show the usage text |
| `--version` | Show the installer version |

Example: discover stacks and features first, then install.
Example: discover the features first, then install.

```shell
pnpm dlx dappbooster --info # all stacks + features as JSON
pnpm dlx dappbooster --info --stack canton # filter to one stack (or --info --canton)
```

### EVM stack

```shell
pnpm dlx dappbooster --evm --ni --name my_dapp --mode full
pnpm dlx dappbooster --evm --ni --name my_dapp --mode custom --features demo,subgraph
pnpm dlx dappbooster --info
pnpm dlx dappbooster --ni --name my_dapp --mode full
pnpm dlx dappbooster --ni --name my_dapp --mode custom --features demo,subgraph
```

**Feature options:**
Expand All @@ -65,12 +49,6 @@ pnpm dlx dappbooster --evm --ni --name my_dapp --mode custom --features demo,sub
| Vocs | `vocs` | ✓ | Vocs documentation site |
| Husky | `husky` | ✓ | Git hooks with Husky, lint-staged, and commitlint |

### Canton stack

```shell
pnpm dlx dappbooster --canton --ni --name my_canton_dapp
```

## Installer development

```shell
Expand All @@ -81,4 +59,4 @@ corepack enable
pnpm i
pnpm build
node dist/cli.js
```
```
4 changes: 0 additions & 4 deletions scripts/record-demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,6 @@ def type_out(text: str, per_char: float = 0.085) -> None:
pump(0.6)
send("\r")

expect(r"Which stack", 15, "stack prompt")
pump(1.3)
send("\r") # EVM is first

expect(r"Choose installation type", 15, "mode prompt")
pump(1.3)
send("\r") # Full is first
Expand Down
57 changes: 8 additions & 49 deletions source/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,18 @@
import { describe, expect, it } from 'vitest'
import {
getDefaultFeatureNames,
getFeatureNames,
getInstallationModes,
stackDefinitions,
} from '../stacks/index.js'
import { defaultFeatureNames, featureDefinitions, featureNames } from '../config.js'

describe('getDefaultFeatureNames', () => {
describe('defaultFeatureNames', () => {
it('returns only features whose default flag is true, in config order', () => {
for (const stack of ['evm', 'canton'] as const) {
const expected = Object.entries(stackDefinitions[stack].features)
.filter(([, def]) => def.default)
.map(([name]) => name)
expect(getDefaultFeatureNames(stack)).toEqual(expected)
}
const expected = Object.entries(featureDefinitions)
.filter(([, def]) => def.default)
.map(([name]) => name)
expect(defaultFeatureNames).toEqual(expected)
})

it('is a subset of all feature names', () => {
const all = new Set(getFeatureNames('evm'))
for (const name of getDefaultFeatureNames('evm')) {
const all = new Set(featureNames)
for (const name of defaultFeatureNames) {
expect(all.has(name)).toBe(true)
}
})
})

describe('getInstallationModes', () => {
it('offers full/custom for evm, where every feature is on by default', () => {
expect(getInstallationModes('evm')).toEqual(['full', 'custom'])
})

it('offers none for canton, which has no features to choose', () => {
expect(getInstallationModes('canton')).toEqual([])
})
})

describe('canton stack config', () => {
it('clones canton-dappbooster at its latest tag with pnpm', () => {
expect(stackDefinitions.canton.repoUrl).toBe(
'https://github.com/BootNodeDev/canton-dappbooster.git',
)
expect(stackDefinitions.canton).not.toHaveProperty('ref')
expect(stackDefinitions.canton.packageManager).toBe('pnpm')
})

it('copies a single root env file', () => {
expect(stackDefinitions.canton.envFiles).toEqual([{ from: '.env.example', to: '.env' }])
})

it('declares the Node floor the scaffolded project needs', () => {
expect(stackDefinitions.canton.minNodeVersion).toBe('24.15.0')
})

it('deletes the three library folders, so pnpm resolves them from npm', () => {
for (const library of ['canton-connect', 'canton-dappbooster', 'canton-theme']) {
expect(stackDefinitions.canton.prepare.paths).toContain(library)
}
})
})
132 changes: 23 additions & 109 deletions source/__tests__/info.test.ts
Original file line number Diff line number Diff line change
@@ -1,144 +1,58 @@
import { describe, expect, it } from 'vitest'
import { featureEntries, featureNames } from '../config.js'
import { getInfoOutput } from '../info.js'
import {
getFeatureEntries,
getInstallationModes,
stackDefinitions,
stackNames,
} from '../stacks/index.js'

describe('getInfoOutput — no filter', () => {
describe('getInfoOutput', () => {
it('returns valid JSON', () => {
expect(() => JSON.parse(getInfoOutput())).not.toThrow()
})

it('has stacks and modes top-level keys', () => {
it('has features and modes top-level keys', () => {
const output = JSON.parse(getInfoOutput())
expect(output).toHaveProperty('stacks')
expect(output).toHaveProperty('modes')
expect(Object.keys(output)).toEqual(['features', 'modes'])
})

it('includes every defined stack', () => {
it('lists every feature', () => {
const output = JSON.parse(getInfoOutput())
expect(Object.keys(output.stacks)).toEqual(stackNames)
})

it('each stack lists its features', () => {
const output = JSON.parse(getInfoOutput())

for (const stack of stackNames) {
const expected = Object.keys(stackDefinitions[stack].features)
expect(Object.keys(output.stacks[stack].features)).toEqual(expected)
}
})

it('each stack reports its package manager and label', () => {
const output = JSON.parse(getInfoOutput())

for (const stack of stackNames) {
expect(output.stacks[stack].label).toBe(stackDefinitions[stack].label)
expect(output.stacks[stack].packageManager).toBe(stackDefinitions[stack].packageManager)
}
expect(Object.keys(output.features)).toEqual(featureNames)
})

it('each feature has description and default', () => {
const output = JSON.parse(getInfoOutput())

for (const stack of stackNames) {
for (const name of Object.keys(stackDefinitions[stack].features)) {
const feature = output.stacks[stack].features[name]
expect(feature).toHaveProperty('description')
expect(feature).toHaveProperty('default')
expect(typeof feature.description).toBe('string')
expect(typeof feature.default).toBe('boolean')
}
for (const name of featureNames) {
const feature = output.features[name]
expect(typeof feature.description).toBe('string')
expect(typeof feature.default).toBe('boolean')
}
})

it('includes postInstall only for features that declare it', () => {
const output = JSON.parse(getInfoOutput())

for (const stack of stackNames) {
for (const [name, def] of getFeatureEntries(stack)) {
const feature = output.stacks[stack].features[name]
if (def.postInstall) {
expect(feature.postInstall).toEqual(def.postInstall)
} else {
expect(feature).not.toHaveProperty('postInstall')
}
}
}
})

it('surfaces requires only for features that declare a dependency', () => {
const output = JSON.parse(getInfoOutput())

for (const stack of stackNames) {
for (const [name, def] of getFeatureEntries(stack)) {
const feature = output.stacks[stack].features[name]
if (def.requires) {
expect(feature.requires).toEqual(def.requires)
} else {
expect(feature).not.toHaveProperty('requires')
}
for (const [name, def] of featureEntries) {
const feature = output.features[name]
if (def.postInstall) {
expect(feature.postInstall).toEqual(def.postInstall)
} else {
expect(feature).not.toHaveProperty('postInstall')
}
}
})

it('does not leak label, packages, or paths into feature output', () => {
const output = JSON.parse(getInfoOutput())

for (const stack of stackNames) {
for (const name of Object.keys(stackDefinitions[stack].features)) {
const feature = output.stacks[stack].features[name]
expect(feature).not.toHaveProperty('label')
expect(feature).not.toHaveProperty('packages')
expect(feature).not.toHaveProperty('paths')
}
}
})

it('modes contains full and custom', () => {
const output = JSON.parse(getInfoOutput())
expect(output.modes).toHaveProperty('full')
expect(output.modes).toHaveProperty('custom')
})

it('modes documents the default mode', () => {
const output = JSON.parse(getInfoOutput())
expect(output.modes).toHaveProperty('default')
})

it('lists the modes each stack accepts, so agents never send a rejected one', () => {
const output = JSON.parse(getInfoOutput())

for (const stack of stackNames) {
expect(output.stacks[stack].modes).toEqual(getInstallationModes(stack))
for (const name of featureNames) {
const feature = output.features[name]
expect(feature).not.toHaveProperty('label')
expect(feature).not.toHaveProperty('packages')
expect(feature).not.toHaveProperty('paths')
}

expect(output.stacks.evm.modes).not.toContain('default')
})

it('reports canton as a stack with no features and no modes', () => {
it('documents exactly the full and custom modes', () => {
const output = JSON.parse(getInfoOutput())

expect(output.stacks.canton.features).toEqual({})
expect(output.stacks.canton.modes).toEqual([])
})
})

describe('getInfoOutput — filter by stack', () => {
it('returns only the requested stack', () => {
const output = JSON.parse(getInfoOutput('canton'))
expect(Object.keys(output.stacks)).toEqual(['canton'])
})

it('throws on unknown stack filter', () => {
expect(() => getInfoOutput('does-not-exist')).toThrow(/Unknown stack 'does-not-exist'/)
})

it('filtering by evm hides canton', () => {
const output = JSON.parse(getInfoOutput('evm'))
expect(Object.keys(output.stacks)).toEqual(['evm'])
expect(Object.keys(output.modes)).toEqual(['full', 'custom'])
})
})
Loading
Loading