dappbooster 3.1.4 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/app.d.ts +6 -1
  2. package/dist/app.js +54 -14
  3. package/dist/cli.js +82 -19
  4. package/dist/components/Multiselect/MultiSelect.d.ts +2 -1
  5. package/dist/components/Multiselect/MultiSelect.js +12 -7
  6. package/dist/components/steps/CloneRepo/CloneRepo.d.ts +2 -0
  7. package/dist/components/steps/CloneRepo/CloneRepo.js +8 -5
  8. package/dist/components/steps/Confirmation.d.ts +8 -0
  9. package/dist/components/steps/Confirmation.js +26 -0
  10. package/dist/components/steps/FileCleanup.d.ts +2 -0
  11. package/dist/components/steps/FileCleanup.js +7 -4
  12. package/dist/components/steps/Install/Install.d.ts +2 -0
  13. package/dist/components/steps/Install/Install.js +6 -6
  14. package/dist/components/steps/InstallationMode.js +4 -4
  15. package/dist/components/steps/OptionalPackages.d.ts +2 -0
  16. package/dist/components/steps/OptionalPackages.js +29 -11
  17. package/dist/components/steps/PostInstall.d.ts +2 -0
  18. package/dist/components/steps/PostInstall.js +12 -4
  19. package/dist/components/steps/ProjectName.d.ts +0 -7
  20. package/dist/components/steps/ProjectName.js +43 -17
  21. package/dist/components/steps/StackSelection.d.ts +8 -0
  22. package/dist/components/steps/StackSelection.js +21 -0
  23. package/dist/constants/config.d.ts +28 -4
  24. package/dist/constants/config.js +151 -46
  25. package/dist/info.d.ts +4 -1
  26. package/dist/info.js +36 -11
  27. package/dist/nonInteractive.d.ts +1 -0
  28. package/dist/nonInteractive.js +29 -14
  29. package/dist/operations/cleanupFiles.d.ts +2 -2
  30. package/dist/operations/cleanupFiles.js +159 -19
  31. package/dist/operations/cloneRepo.d.ts +2 -1
  32. package/dist/operations/cloneRepo.js +35 -11
  33. package/dist/operations/createEnvFile.d.ts +2 -1
  34. package/dist/operations/createEnvFile.js +9 -2
  35. package/dist/operations/installGuard.d.ts +8 -0
  36. package/dist/operations/installGuard.js +35 -0
  37. package/dist/operations/installPackages.d.ts +2 -2
  38. package/dist/operations/installPackages.js +14 -6
  39. package/dist/utils/utils.d.ts +7 -3
  40. package/dist/utils/utils.js +62 -6
  41. package/package.json +21 -6
  42. package/readme.md +141 -70
@@ -1,26 +1,148 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { copyFile, mkdir, rm } from 'node:fs/promises';
3
3
  import { resolve } from 'node:path';
4
+ import { getStackConfig } from '../constants/config.js';
4
5
  import { isFeatureSelected } from '../utils/utils.js';
5
- function patchPackageJson(projectFolder, features) {
6
+ import { execFile } from './exec.js';
7
+ // CI config is hygiene for both stacks. EVM additionally always strips its agent/LLM metadata;
8
+ // Canton keeps that metadata under the optional `llm` feature instead.
9
+ const CI_PATHS = ['.github'];
10
+ const EVM_METADATA_PATHS = ['.claude', 'AGENTS.md', 'CLAUDE.md', 'architecture.md'];
11
+ const AUTOMATION_PATHS = ['.husky', '.lintstagedrc.mjs', 'commitlint.config.js'];
12
+ const TOOLING_PACKAGES_TO_REMOVE = [
13
+ 'husky',
14
+ 'lint-staged',
15
+ '@commitlint/cli',
16
+ '@commitlint/config-conventional',
17
+ ];
18
+ const TOOLING_SCRIPTS_TO_REMOVE = ['prepare', 'commitlint', 'commitlint:check', 'commitlint:ci'];
19
+ function removePackageKeys(packageBlock, keys) {
20
+ if (!packageBlock) {
21
+ return false;
22
+ }
23
+ let changed = false;
24
+ for (const key of keys) {
25
+ if (key in packageBlock) {
26
+ delete packageBlock[key];
27
+ changed = true;
28
+ }
29
+ }
30
+ return changed;
31
+ }
32
+ function sanitizeRepositoryPackageJson(projectFolder) {
33
+ const packageJsonPath = resolve(projectFolder, 'package.json');
34
+ try {
35
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
36
+ const scripts = packageJson.scripts;
37
+ let changed = false;
38
+ if (scripts) {
39
+ for (const scriptName of TOOLING_SCRIPTS_TO_REMOVE) {
40
+ if (scripts[scriptName] !== undefined) {
41
+ scripts[scriptName] = undefined;
42
+ changed = true;
43
+ }
44
+ }
45
+ }
46
+ const dependencyGroups = [
47
+ packageJson.dependencies,
48
+ packageJson.devDependencies,
49
+ packageJson.optionalDependencies,
50
+ packageJson.peerDependencies,
51
+ ];
52
+ for (const group of dependencyGroups) {
53
+ if (removePackageKeys(group, TOOLING_PACKAGES_TO_REMOVE)) {
54
+ changed = true;
55
+ }
56
+ }
57
+ if (changed) {
58
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
59
+ }
60
+ }
61
+ catch {
62
+ // Some templates may not include a package.json at this level.
63
+ }
64
+ }
65
+ async function removePaths(projectFolder, relativePaths) {
66
+ for (const relativePath of relativePaths) {
67
+ await rm(resolve(projectFolder, relativePath), { recursive: true, force: true });
68
+ }
69
+ }
70
+ async function cleanupRepositoryHygiene(stack, projectFolder, onProgress) {
71
+ onProgress?.('Repository metadata');
72
+ const metadataPaths = stack === 'evm' ? [...EVM_METADATA_PATHS, ...CI_PATHS] : CI_PATHS;
73
+ await removePaths(projectFolder, metadataPaths);
74
+ onProgress?.('Git hooks and commit linting');
75
+ await removePaths(projectFolder, AUTOMATION_PATHS);
76
+ sanitizeRepositoryPackageJson(projectFolder);
77
+ }
78
+ function patchPackageJsonEvm(projectFolder, features) {
6
79
  const packageJsonPath = resolve(projectFolder, 'package.json');
7
80
  const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
81
+ const scripts = packageJson.scripts;
82
+ if (!scripts) {
83
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
84
+ return;
85
+ }
8
86
  if (!isFeatureSelected('subgraph', features)) {
9
- packageJson.scripts['subgraph-codegen'] = undefined;
87
+ scripts['subgraph-codegen'] = undefined;
10
88
  }
11
89
  if (!isFeatureSelected('typedoc', features)) {
12
- packageJson.scripts['typedoc:build'] = undefined;
90
+ scripts['typedoc:build'] = undefined;
13
91
  }
14
92
  if (!isFeatureSelected('vocs', features)) {
15
- packageJson.scripts['docs:build'] = undefined;
16
- packageJson.scripts['docs:dev'] = undefined;
17
- packageJson.scripts['docs:preview'] = undefined;
93
+ scripts['docs:build'] = undefined;
94
+ scripts['docs:dev'] = undefined;
95
+ scripts['docs:preview'] = undefined;
18
96
  }
19
- if (!isFeatureSelected('husky', features)) {
20
- packageJson.scripts.prepare = undefined;
97
+ // biome-ignore lint/complexity/useLiteralKeys: TS index-signature compatibility in strict mode
98
+ scripts['prepare'] = undefined;
99
+ // biome-ignore lint/complexity/useLiteralKeys: TS index-signature compatibility in strict mode
100
+ scripts['commitlint'] = undefined;
101
+ scripts['commitlint:check'] = undefined;
102
+ scripts['commitlint:ci'] = undefined;
103
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
104
+ }
105
+ // Strip scripts by what they run (e.g. `npm --prefix counter/frontend ...`)
106
+ // rather than by name, so cleanup tracks directory removal even as scripts change.
107
+ function scriptTargetsRemovedDir(command, removedDirs) {
108
+ const tokens = command.split(/\s+/);
109
+ return removedDirs.some((dir) => tokens.some((token) => token === dir || token.startsWith(`${dir}/`)));
110
+ }
111
+ function patchPackageJsonCanton(projectFolder, removedDirs) {
112
+ const packageJsonPath = resolve(projectFolder, 'package.json');
113
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
114
+ const scripts = packageJson.scripts;
115
+ if (!scripts) {
116
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
117
+ return;
21
118
  }
119
+ for (const [name, command] of Object.entries(scripts)) {
120
+ if (command !== undefined && scriptTargetsRemovedDir(command, removedDirs)) {
121
+ scripts[name] = undefined;
122
+ }
123
+ }
124
+ // biome-ignore lint/complexity/useLiteralKeys: TS index-signature compatibility in strict mode
125
+ scripts['prepare'] = undefined;
126
+ // biome-ignore lint/complexity/useLiteralKeys: TS index-signature compatibility in strict mode
127
+ scripts['commitlint'] = undefined;
128
+ scripts['commitlint:check'] = undefined;
129
+ scripts['commitlint:ci'] = undefined;
22
130
  writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
23
131
  }
132
+ async function createInitialCommit(projectFolder) {
133
+ await execFile('git', ['add', '.'], { cwd: projectFolder });
134
+ await execFile('git', [
135
+ '-c',
136
+ 'user.name=dAppBooster',
137
+ '-c',
138
+ 'user.email=no-reply@dappbooster.dev',
139
+ '-c',
140
+ 'commit.gpgsign=false',
141
+ 'commit',
142
+ '-m',
143
+ 'chore: initial commit',
144
+ ], { cwd: projectFolder });
145
+ }
24
146
  async function cleanupDemo(projectFolder) {
25
147
  const homeFolder = resolve(projectFolder, 'src/components/pageComponents/home');
26
148
  await rm(homeFolder, { recursive: true, force: true });
@@ -43,12 +165,7 @@ async function cleanupVocs(projectFolder) {
43
165
  await rm(resolve(projectFolder, 'vocs.config.ts'), { force: true });
44
166
  await rm(resolve(projectFolder, 'docs'), { recursive: true, force: true });
45
167
  }
46
- async function cleanupHusky(projectFolder) {
47
- await rm(resolve(projectFolder, '.husky'), { recursive: true, force: true });
48
- await rm(resolve(projectFolder, '.lintstagedrc.mjs'), { force: true });
49
- await rm(resolve(projectFolder, 'commitlint.config.js'), { force: true });
50
- }
51
- export async function cleanupFiles(projectFolder, mode, features = [], onProgress) {
168
+ async function cleanupEvmFiles(projectFolder, mode, features, onProgress) {
52
169
  if (mode === 'custom') {
53
170
  if (!isFeatureSelected('demo', features)) {
54
171
  onProgress?.('Component demos');
@@ -66,12 +183,35 @@ export async function cleanupFiles(projectFolder, mode, features = [], onProgres
66
183
  onProgress?.('Vocs');
67
184
  await cleanupVocs(projectFolder);
68
185
  }
69
- if (!isFeatureSelected('husky', features)) {
70
- onProgress?.('Husky');
71
- await cleanupHusky(projectFolder);
72
- }
73
- patchPackageJson(projectFolder, features);
186
+ patchPackageJsonEvm(projectFolder, features);
74
187
  }
75
188
  onProgress?.('Install script');
76
189
  await rm(resolve(projectFolder, '.install-files'), { recursive: true, force: true });
77
190
  }
191
+ async function cleanupCantonFiles(projectFolder, mode, features, onProgress) {
192
+ const cantonFeatures = getStackConfig('canton').features;
193
+ // Each deselected feature contributes its paths to removal (custom mode only). Directory paths
194
+ // also feed script stripping, so a removed feature's package.json scripts disappear with it.
195
+ const removedDirs = [];
196
+ if (mode === 'custom') {
197
+ for (const [name, definition] of Object.entries(cantonFeatures)) {
198
+ if (isFeatureSelected(name, features) || !definition.paths || definition.paths.length === 0) {
199
+ continue;
200
+ }
201
+ onProgress?.(definition.label);
202
+ await removePaths(projectFolder, definition.paths);
203
+ removedDirs.push(...definition.paths);
204
+ }
205
+ }
206
+ patchPackageJsonCanton(projectFolder, removedDirs);
207
+ }
208
+ export async function cleanupFiles(stack, projectFolder, mode, features = [], onProgress) {
209
+ await cleanupRepositoryHygiene(stack, projectFolder, onProgress);
210
+ if (stack === 'canton') {
211
+ await cleanupCantonFiles(projectFolder, mode, features, onProgress);
212
+ onProgress?.('Initial commit');
213
+ await createInitialCommit(projectFolder);
214
+ return;
215
+ }
216
+ await cleanupEvmFiles(projectFolder, mode, features, onProgress);
217
+ }
@@ -1 +1,2 @@
1
- export declare function cloneRepo(projectName: string, onProgress?: (step: string) => void): Promise<void>;
1
+ import { type Stack } from '../constants/config.js';
2
+ export declare function cloneRepo(stack: Stack, projectName: string, onProgress?: (step: string) => void): Promise<void>;
@@ -1,19 +1,43 @@
1
1
  import { rm } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
- import { repoUrl } from '../constants/config.js';
3
+ import { getStackConfig } from '../constants/config.js';
4
4
  import { getProjectFolder } from '../utils/utils.js';
5
5
  import { exec, execFile } from './exec.js';
6
- export async function cloneRepo(projectName, onProgress) {
6
+ export async function cloneRepo(stack, projectName, onProgress) {
7
+ const config = getStackConfig(stack);
7
8
  const projectFolder = getProjectFolder(projectName);
8
- onProgress?.(`Cloning dAppBooster in ${projectName}`);
9
- await execFile('git', ['clone', '--depth', '1', '--no-checkout', repoUrl, projectName]);
10
- onProgress?.('Fetching tags');
11
- await execFile('git', ['fetch', '--tags'], { cwd: projectFolder });
12
- onProgress?.('Checking out latest tag');
13
- // Shell required for $() command substitution
14
- await exec('git checkout $(git describe --tags $(git rev-list --tags --max-count=1))', {
15
- cwd: projectFolder,
16
- });
9
+ if (config.refType === 'branch') {
10
+ const branch = config.ref;
11
+ if (!branch) {
12
+ throw new Error(`Stack '${stack}' has refType 'branch' but no 'ref' configured`);
13
+ }
14
+ onProgress?.(`Cloning ${config.label} (branch ${branch}) in ${projectName}`);
15
+ await execFile('git', [
16
+ 'clone',
17
+ '--depth',
18
+ '1',
19
+ '--branch',
20
+ branch,
21
+ '--single-branch',
22
+ config.repoUrl,
23
+ projectName,
24
+ ]);
25
+ }
26
+ else {
27
+ onProgress?.(`Cloning ${config.label} in ${projectName}`);
28
+ await execFile('git', ['clone', '--depth', '1', '--no-checkout', config.repoUrl, projectName]);
29
+ onProgress?.('Fetching tags');
30
+ await execFile('git', ['fetch', '--tags'], { cwd: projectFolder });
31
+ onProgress?.('Checking out latest tag');
32
+ // Shell required for $() command substitution
33
+ await exec('git checkout $(git describe --tags $(git rev-list --tags --max-count=1))', {
34
+ cwd: projectFolder,
35
+ });
36
+ }
37
+ for (const dir of config.removeAfterClone) {
38
+ onProgress?.(`Removing ${dir}`);
39
+ await rm(resolve(projectFolder, dir), { recursive: true, force: true });
40
+ }
17
41
  onProgress?.('Removing .git folder');
18
42
  await rm(resolve(projectFolder, '.git'), { recursive: true, force: true });
19
43
  onProgress?.('Initializing Git repository');
@@ -1 +1,2 @@
1
- export declare function createEnvFile(projectFolder: string): Promise<void>;
1
+ import { type FeatureName, type Stack } from '../constants/config.js';
2
+ export declare function createEnvFile(stack: Stack, projectFolder: string, features?: FeatureName[]): Promise<void>;
@@ -1,5 +1,12 @@
1
1
  import { copyFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- export async function createEnvFile(projectFolder) {
4
- await copyFile(join(projectFolder, '.env.example'), join(projectFolder, '.env.local'));
3
+ import { getStackConfig } from '../constants/config.js';
4
+ export async function createEnvFile(stack, projectFolder, features = []) {
5
+ const envFiles = getStackConfig(stack).envFiles;
6
+ for (const file of envFiles) {
7
+ if (file.ifFeature !== undefined && !features.includes(file.ifFeature)) {
8
+ continue;
9
+ }
10
+ await copyFile(join(projectFolder, file.from), join(projectFolder, file.to));
11
+ }
5
12
  }
@@ -0,0 +1,8 @@
1
+ type RemoveDirectory = (path: string, options: {
2
+ recursive: boolean;
3
+ force: boolean;
4
+ }) => void;
5
+ export declare function removeActiveProject(rm?: RemoveDirectory): void;
6
+ export declare function beginInstall(projectFolder: string): void;
7
+ export declare function completeInstall(): void;
8
+ export {};
@@ -0,0 +1,35 @@
1
+ import { rmSync } from 'node:fs';
2
+ import process from 'node:process';
3
+ // Tracks the project folder currently being scaffolded so an interrupt can remove the partial
4
+ // directory. Only ever holds a folder the installer created this run (callers validate that the
5
+ // directory did not exist before starting), so removing it on abort never touches user data.
6
+ let activeProjectFolder;
7
+ let signalHandlersRegistered = false;
8
+ // Removes the in-progress project folder, if any, then clears the active reference so a finished
9
+ // or already-removed project is never deleted. `rm` is injectable for testing.
10
+ export function removeActiveProject(rm = rmSync) {
11
+ if (activeProjectFolder === undefined) {
12
+ return;
13
+ }
14
+ const folder = activeProjectFolder;
15
+ activeProjectFolder = undefined;
16
+ rm(folder, { recursive: true, force: true });
17
+ }
18
+ function handleAbort(signal) {
19
+ removeActiveProject();
20
+ // Conventional exit code for a signal is 128 + signal number (SIGINT 2 → 130, SIGTERM 15 → 143).
21
+ process.exit(signal === 'SIGTERM' ? 143 : 130);
22
+ }
23
+ // Marks the start of disk-writing work. Registers interrupt handlers on first use.
24
+ export function beginInstall(projectFolder) {
25
+ activeProjectFolder = projectFolder;
26
+ if (!signalHandlersRegistered) {
27
+ process.on('SIGINT', handleAbort);
28
+ process.on('SIGTERM', handleAbort);
29
+ signalHandlersRegistered = true;
30
+ }
31
+ }
32
+ // Marks the scaffold complete; an interrupt after this point leaves the finished project intact.
33
+ export function completeInstall() {
34
+ activeProjectFolder = undefined;
35
+ }
@@ -1,3 +1,3 @@
1
- import type { FeatureName } from '../constants/config.js';
1
+ import { type FeatureName, type Stack } from '../constants/config.js';
2
2
  import type { InstallationType } from '../types/types.js';
3
- export declare function installPackages(projectFolder: string, mode: InstallationType, features?: FeatureName[], onProgress?: (step: string) => void): Promise<void>;
3
+ export declare function installPackages(stack: Stack, projectFolder: string, mode: InstallationType, features?: FeatureName[], onProgress?: (step: string) => void): Promise<void>;
@@ -1,19 +1,27 @@
1
+ import { getStackConfig, } from '../constants/config.js';
1
2
  import { getPackagesToRemove } from '../utils/utils.js';
2
3
  import { execFile } from './exec.js';
3
- export async function installPackages(projectFolder, mode, features = [], onProgress) {
4
+ const removeCommand = {
5
+ pnpm: 'remove',
6
+ npm: 'uninstall',
7
+ };
8
+ export async function installPackages(stack, projectFolder, mode, features = [], onProgress) {
9
+ const { packageManager } = getStackConfig(stack);
4
10
  if (mode === 'full') {
5
11
  onProgress?.('Installing packages');
6
- await execFile('pnpm', ['i'], { cwd: projectFolder });
12
+ await execFile(packageManager, ['install'], { cwd: projectFolder });
7
13
  return;
8
14
  }
9
- const packagesToRemove = getPackagesToRemove(features);
15
+ const packagesToRemove = getPackagesToRemove(stack, features);
10
16
  if (packagesToRemove.length === 0) {
11
17
  onProgress?.('Installing packages');
12
- await execFile('pnpm', ['i'], { cwd: projectFolder });
18
+ await execFile(packageManager, ['install'], { cwd: projectFolder });
13
19
  return;
14
20
  }
15
21
  onProgress?.('Installing packages');
16
- await execFile('pnpm', ['remove', ...packagesToRemove], { cwd: projectFolder });
22
+ await execFile(packageManager, [removeCommand[packageManager], ...packagesToRemove], {
23
+ cwd: projectFolder,
24
+ });
17
25
  onProgress?.('Executing post-install scripts');
18
- await execFile('pnpm', ['run', 'postinstall'], { cwd: projectFolder });
26
+ await execFile(packageManager, ['run', 'postinstall'], { cwd: projectFolder });
19
27
  }
@@ -1,11 +1,15 @@
1
- import { type FeatureName } from '../constants/config.js';
1
+ import { type FeatureName, type Stack } from '../constants/config.js';
2
2
  export declare function getProjectFolder(projectName: string): string;
3
3
  export declare function isValidName(name: string): boolean;
4
4
  export declare function isAnswerConfirmed(answer?: string, errorMessage?: string): boolean;
5
5
  export declare function canShowStep(currentStep: number, stepToShow: number): boolean;
6
6
  export declare function isFeatureSelected(feature: FeatureName, selectedFeatures: FeatureName[]): boolean;
7
- export declare function getPackagesToRemove(selectedFeatures: FeatureName[]): string[];
8
- export declare function getPostInstallMessages(mode: 'full' | 'custom', selectedFeatures: FeatureName[]): string[];
7
+ type FeatureToggleAction = 'select' | 'unselect';
8
+ export declare function resolveSelectedFeatures(stack: Stack, selectedFeatures: FeatureName[]): FeatureName[];
9
+ export declare function applyFeatureToggle(stack: Stack, selectedFeatures: FeatureName[], toggledFeature: FeatureName, action: FeatureToggleAction): FeatureName[];
10
+ export declare function describeInstallPlan(stack: Stack, projectName: string, mode: 'full' | 'custom', selectedFeatures: FeatureName[]): string;
11
+ export declare function getPackagesToRemove(stack: Stack, selectedFeatures: FeatureName[]): string[];
12
+ export declare function getPostInstallMessages(stack: Stack, mode: 'full' | 'custom', selectedFeatures: FeatureName[]): string[];
9
13
  export declare function projectDirectoryExists(projectName: string): boolean;
10
14
  type StepStatus = 'running' | 'done' | 'error';
11
15
  type StepDisplay = {
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import process from 'node:process';
4
- import { featureDefinitions } from '../constants/config.js';
4
+ import { getFeatureNames, getStackConfig, } from '../constants/config.js';
5
5
  export function getProjectFolder(projectName) {
6
6
  return join(process.cwd(), projectName);
7
7
  }
@@ -17,16 +17,72 @@ export function canShowStep(currentStep, stepToShow) {
17
17
  export function isFeatureSelected(feature, selectedFeatures) {
18
18
  return selectedFeatures.includes(feature);
19
19
  }
20
- export function getPackagesToRemove(selectedFeatures) {
21
- return Object.entries(featureDefinitions)
20
+ // Walks a feature's `requires` chain, adding every (transitive) requirement to `accumulator`.
21
+ function collectRequiredFeatures(stack, feature, accumulator) {
22
+ const definition = getStackConfig(stack).features[feature];
23
+ if (!definition?.requires) {
24
+ return;
25
+ }
26
+ for (const required of definition.requires) {
27
+ if (!accumulator.has(required)) {
28
+ accumulator.add(required);
29
+ collectRequiredFeatures(stack, required, accumulator);
30
+ }
31
+ }
32
+ }
33
+ // Features that depend (transitively) on `target` — removing `target` should remove these too.
34
+ function getDependentFeatures(stack, target) {
35
+ const dependents = new Set();
36
+ for (const name of getFeatureNames(stack)) {
37
+ const required = new Set();
38
+ collectRequiredFeatures(stack, name, required);
39
+ if (required.has(target)) {
40
+ dependents.add(name);
41
+ }
42
+ }
43
+ return dependents;
44
+ }
45
+ // Expands a selection to include every transitive requirement, returned in config order.
46
+ export function resolveSelectedFeatures(stack, selectedFeatures) {
47
+ const resolved = new Set(selectedFeatures);
48
+ for (const feature of selectedFeatures) {
49
+ collectRequiredFeatures(stack, feature, resolved);
50
+ }
51
+ return getFeatureNames(stack).filter((name) => resolved.has(name));
52
+ }
53
+ // Interactive toggle that keeps the selection dependency-consistent: selecting a feature pulls
54
+ // its requirements in; unselecting one cascades its dependents out. Result is in config order.
55
+ export function applyFeatureToggle(stack, selectedFeatures, toggledFeature, action) {
56
+ if (action === 'select') {
57
+ return resolveSelectedFeatures(stack, [...selectedFeatures, toggledFeature]);
58
+ }
59
+ const toRemove = getDependentFeatures(stack, toggledFeature);
60
+ toRemove.add(toggledFeature);
61
+ return getFeatureNames(stack).filter((name) => selectedFeatures.includes(name) && !toRemove.has(name));
62
+ }
63
+ // One-line summary of an install plan, shown on the interactive confirmation step before any disk
64
+ // work begins.
65
+ export function describeInstallPlan(stack, projectName, mode, selectedFeatures) {
66
+ const stackLabel = getStackConfig(stack).label;
67
+ const head = `Stack: ${stackLabel} · Project: ${projectName}`;
68
+ if (mode === 'full') {
69
+ return `${head} · Mode: full (all features)`;
70
+ }
71
+ const features = selectedFeatures.length > 0 ? selectedFeatures.join(', ') : 'none';
72
+ return `${head} · Mode: custom · Features: ${features}`;
73
+ }
74
+ export function getPackagesToRemove(stack, selectedFeatures) {
75
+ const features = getStackConfig(stack).features;
76
+ return Object.entries(features)
22
77
  .filter(([name]) => !selectedFeatures.includes(name))
23
78
  .flatMap(([, def]) => def.packages);
24
79
  }
25
- export function getPostInstallMessages(mode, selectedFeatures) {
80
+ export function getPostInstallMessages(stack, mode, selectedFeatures) {
81
+ const features = getStackConfig(stack).features;
26
82
  if (mode === 'full') {
27
- return Object.values(featureDefinitions).flatMap((def) => def.postInstall ?? []);
83
+ return Object.values(features).flatMap((def) => def.postInstall ?? []);
28
84
  }
29
- return selectedFeatures.flatMap((name) => featureDefinitions[name]?.postInstall ?? []);
85
+ return selectedFeatures.flatMap((name) => features[name]?.postInstall ?? []);
30
86
  }
31
87
  export function projectDirectoryExists(projectName) {
32
88
  return existsSync(getProjectFolder(projectName));
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "dappbooster",
3
- "version": "3.1.4",
3
+ "version": "3.2.0",
4
+ "description": "Agent-friendly dAppBooster installer that scaffolds Web3 dApps via TUI or non-interactive CLI/CI.",
5
+ "keywords": [
6
+ "dappbooster",
7
+ "dapp",
8
+ "web3",
9
+ "blockchain",
10
+ "ethereum",
11
+ "evm",
12
+ "scaffolding",
13
+ "starter",
14
+ "template",
15
+ "cli",
16
+ "installer",
17
+ "generator",
18
+ "tui",
19
+ "non-interactive",
20
+ "ci",
21
+ "agent",
22
+ "automation"
23
+ ],
4
24
  "license": "MIT",
5
25
  "repository": {
6
26
  "type": "git",
@@ -43,10 +63,5 @@
43
63
  "ts-node": "^10.9.1",
44
64
  "typescript": "^5.8.3",
45
65
  "vitest": "^4.1.0"
46
- },
47
- "pnpm": {
48
- "onlyBuiltDependencies": [
49
- "@biomejs/biome"
50
- ]
51
66
  }
52
67
  }