dappbooster 3.1.5 → 3.3.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 (44) hide show
  1. package/dist/app.d.ts +6 -1
  2. package/dist/app.js +55 -15
  3. package/dist/cli.js +84 -20
  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 +11 -6
  12. package/dist/components/steps/Install/Install.d.ts +2 -0
  13. package/dist/components/steps/Install/Install.js +10 -8
  14. package/dist/components/steps/InstallationMode.d.ts +2 -0
  15. package/dist/components/steps/InstallationMode.js +12 -15
  16. package/dist/components/steps/OptionalPackages.d.ts +2 -0
  17. package/dist/components/steps/OptionalPackages.js +34 -11
  18. package/dist/components/steps/PostInstall.d.ts +2 -0
  19. package/dist/components/steps/PostInstall.js +13 -6
  20. package/dist/components/steps/ProjectName.d.ts +0 -7
  21. package/dist/components/steps/ProjectName.js +43 -17
  22. package/dist/components/steps/StackSelection.d.ts +8 -0
  23. package/dist/components/steps/StackSelection.js +21 -0
  24. package/dist/constants/config.d.ts +32 -4
  25. package/dist/constants/config.js +156 -44
  26. package/dist/info.d.ts +4 -1
  27. package/dist/info.js +37 -11
  28. package/dist/nonInteractive.d.ts +1 -0
  29. package/dist/nonInteractive.js +40 -17
  30. package/dist/operations/cleanupFiles.d.ts +2 -2
  31. package/dist/operations/cleanupFiles.js +169 -19
  32. package/dist/operations/cloneRepo.d.ts +2 -1
  33. package/dist/operations/cloneRepo.js +35 -11
  34. package/dist/operations/createEnvFile.d.ts +2 -1
  35. package/dist/operations/createEnvFile.js +9 -2
  36. package/dist/operations/installGuard.d.ts +8 -0
  37. package/dist/operations/installGuard.js +35 -0
  38. package/dist/operations/installPackages.d.ts +2 -2
  39. package/dist/operations/installPackages.js +14 -6
  40. package/dist/types/types.d.ts +1 -1
  41. package/dist/utils/utils.d.ts +9 -3
  42. package/dist/utils/utils.js +78 -6
  43. package/package.json +2 -9
  44. package/readme.md +143 -77
@@ -1,26 +1,156 @@
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
+ // Strip the husky/lint-staged/commitlint tooling scripts and dependencies from a parsed
33
+ // package.json (mutates in place). Returns whether anything was removed.
34
+ function stripToolingEntries(packageJson, scripts) {
35
+ let changed = false;
36
+ if (scripts) {
37
+ for (const scriptName of TOOLING_SCRIPTS_TO_REMOVE) {
38
+ if (scripts[scriptName] !== undefined) {
39
+ scripts[scriptName] = undefined;
40
+ changed = true;
41
+ }
42
+ }
43
+ }
44
+ const dependencyGroups = [
45
+ packageJson.dependencies,
46
+ packageJson.devDependencies,
47
+ packageJson.optionalDependencies,
48
+ packageJson.peerDependencies,
49
+ ];
50
+ for (const group of dependencyGroups) {
51
+ if (removePackageKeys(group, TOOLING_PACKAGES_TO_REMOVE)) {
52
+ changed = true;
53
+ }
54
+ }
55
+ return changed;
56
+ }
57
+ function sanitizeRepositoryPackageJson(projectFolder) {
58
+ const packageJsonPath = resolve(projectFolder, 'package.json');
59
+ try {
60
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
61
+ const scripts = packageJson.scripts;
62
+ if (stripToolingEntries(packageJson, scripts)) {
63
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
64
+ }
65
+ }
66
+ catch {
67
+ // Some templates may not include a package.json at this level.
68
+ }
69
+ }
70
+ async function removePaths(projectFolder, relativePaths) {
71
+ for (const relativePath of relativePaths) {
72
+ await rm(resolve(projectFolder, relativePath), { recursive: true, force: true });
73
+ }
74
+ }
75
+ // EVM-only hygiene: always strip CI metadata, agent metadata, and git automation. Canton models
76
+ // .github and pre-commit hooks as optional features instead (see cleanupCantonFiles).
77
+ async function cleanupRepositoryHygiene(projectFolder, onProgress) {
78
+ onProgress?.('Repository metadata');
79
+ await removePaths(projectFolder, [...EVM_METADATA_PATHS, ...CI_PATHS]);
80
+ onProgress?.('Git hooks and commit linting');
81
+ await removePaths(projectFolder, AUTOMATION_PATHS);
82
+ sanitizeRepositoryPackageJson(projectFolder);
83
+ }
84
+ function patchPackageJsonEvm(projectFolder, features) {
6
85
  const packageJsonPath = resolve(projectFolder, 'package.json');
7
86
  const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
87
+ const scripts = packageJson.scripts;
88
+ if (!scripts) {
89
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
90
+ return;
91
+ }
8
92
  if (!isFeatureSelected('subgraph', features)) {
9
- packageJson.scripts['subgraph-codegen'] = undefined;
93
+ scripts['subgraph-codegen'] = undefined;
10
94
  }
11
95
  if (!isFeatureSelected('typedoc', features)) {
12
- packageJson.scripts['typedoc:build'] = undefined;
96
+ scripts['typedoc:build'] = undefined;
13
97
  }
14
98
  if (!isFeatureSelected('vocs', features)) {
15
- packageJson.scripts['docs:build'] = undefined;
16
- packageJson.scripts['docs:dev'] = undefined;
17
- packageJson.scripts['docs:preview'] = undefined;
99
+ scripts['docs:build'] = undefined;
100
+ scripts['docs:dev'] = undefined;
101
+ scripts['docs:preview'] = undefined;
102
+ }
103
+ // biome-ignore lint/complexity/useLiteralKeys: TS index-signature compatibility in strict mode
104
+ scripts['prepare'] = undefined;
105
+ // biome-ignore lint/complexity/useLiteralKeys: TS index-signature compatibility in strict mode
106
+ scripts['commitlint'] = undefined;
107
+ scripts['commitlint:check'] = undefined;
108
+ scripts['commitlint:ci'] = undefined;
109
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
110
+ }
111
+ // Strip scripts by what they run (e.g. `npm --prefix carpincho-wallet ...`)
112
+ // rather than by name, so cleanup tracks directory removal even as scripts change.
113
+ function scriptTargetsRemovedDir(command, removedDirs) {
114
+ const tokens = command.split(/\s+/);
115
+ return removedDirs.some((dir) => tokens.some((token) => token === dir || token.startsWith(`${dir}/`)));
116
+ }
117
+ function patchPackageJsonCanton(projectFolder, removedDirs, precommitRemoved) {
118
+ const packageJsonPath = resolve(projectFolder, 'package.json');
119
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
120
+ const scripts = packageJson.scripts;
121
+ if (scripts) {
122
+ for (const [name, command] of Object.entries(scripts)) {
123
+ if (command !== undefined && scriptTargetsRemovedDir(command, removedDirs)) {
124
+ scripts[name] = undefined;
125
+ }
126
+ }
18
127
  }
19
- if (!isFeatureSelected('husky', features)) {
20
- packageJson.scripts.prepare = undefined;
128
+ // The husky tooling (prepare/commitlint scripts + husky/lint-staged/commitlint deps) only leaves
129
+ // with the pre-commit feature.
130
+ if (precommitRemoved) {
131
+ stripToolingEntries(packageJson, scripts);
21
132
  }
22
133
  writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
23
134
  }
135
+ async function createInitialCommit(projectFolder) {
136
+ await execFile('git', ['add', '.'], { cwd: projectFolder });
137
+ // --no-verify: the scaffold's baseline commit must not run the project's own git hooks. When the
138
+ // `precommit` feature is kept, husky's pre-commit/commit-msg hooks are present and would lint and
139
+ // test the freshly-cloned tree (and fail), blocking the install. The user's later commits still
140
+ // run hooks normally.
141
+ await execFile('git', [
142
+ '-c',
143
+ 'user.name=dAppBooster',
144
+ '-c',
145
+ 'user.email=no-reply@dappbooster.dev',
146
+ '-c',
147
+ 'commit.gpgsign=false',
148
+ 'commit',
149
+ '--no-verify',
150
+ '-m',
151
+ 'chore: initial commit',
152
+ ], { cwd: projectFolder });
153
+ }
24
154
  async function cleanupDemo(projectFolder) {
25
155
  const homeFolder = resolve(projectFolder, 'src/components/pageComponents/home');
26
156
  await rm(homeFolder, { recursive: true, force: true });
@@ -43,12 +173,7 @@ async function cleanupVocs(projectFolder) {
43
173
  await rm(resolve(projectFolder, 'vocs.config.ts'), { force: true });
44
174
  await rm(resolve(projectFolder, 'docs'), { recursive: true, force: true });
45
175
  }
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) {
176
+ async function cleanupEvmFiles(projectFolder, mode, features, onProgress) {
52
177
  if (mode === 'custom') {
53
178
  if (!isFeatureSelected('demo', features)) {
54
179
  onProgress?.('Component demos');
@@ -66,12 +191,37 @@ export async function cleanupFiles(projectFolder, mode, features = [], onProgres
66
191
  onProgress?.('Vocs');
67
192
  await cleanupVocs(projectFolder);
68
193
  }
69
- if (!isFeatureSelected('husky', features)) {
70
- onProgress?.('Husky');
71
- await cleanupHusky(projectFolder);
72
- }
73
- patchPackageJson(projectFolder, features);
194
+ patchPackageJsonEvm(projectFolder, features);
74
195
  }
75
196
  onProgress?.('Install script');
76
197
  await rm(resolve(projectFolder, '.install-files'), { recursive: true, force: true });
77
198
  }
199
+ async function cleanupCantonFiles(projectFolder, mode, features, onProgress) {
200
+ const cantonFeatures = getStackConfig('canton').features;
201
+ // Each deselected feature contributes its paths to removal. `default` and `custom` remove;
202
+ // `full` keeps everything. Directory paths also feed script stripping, so a removed feature's
203
+ // package.json scripts disappear with it.
204
+ const removedDirs = [];
205
+ if (mode !== 'full') {
206
+ for (const [name, definition] of Object.entries(cantonFeatures)) {
207
+ if (isFeatureSelected(name, features) || !definition.paths || definition.paths.length === 0) {
208
+ continue;
209
+ }
210
+ onProgress?.(definition.label);
211
+ await removePaths(projectFolder, definition.paths);
212
+ removedDirs.push(...definition.paths);
213
+ }
214
+ }
215
+ const precommitRemoved = mode !== 'full' && !isFeatureSelected('precommit', features);
216
+ patchPackageJsonCanton(projectFolder, removedDirs, precommitRemoved);
217
+ }
218
+ export async function cleanupFiles(stack, projectFolder, mode, features = [], onProgress) {
219
+ if (stack === 'canton') {
220
+ await cleanupCantonFiles(projectFolder, mode, features, onProgress);
221
+ onProgress?.('Initial commit');
222
+ await createInitialCommit(projectFolder);
223
+ return;
224
+ }
225
+ await cleanupRepositoryHygiene(projectFolder, onProgress);
226
+ await cleanupEvmFiles(projectFolder, mode, features, onProgress);
227
+ }
@@ -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
  }
@@ -3,7 +3,7 @@ export type SelectItem = {
3
3
  value: string;
4
4
  };
5
5
  export type MultiSelectItem = SelectItem;
6
- export type InstallationType = 'full' | 'custom';
6
+ export type InstallationType = 'full' | 'default' | 'custom';
7
7
  export type InstallationSelectItem = {
8
8
  label: string;
9
9
  value: InstallationType;
@@ -1,11 +1,17 @@
1
- import { type FeatureName } from '../constants/config.js';
1
+ import { type FeatureName, type Stack } from '../constants/config.js';
2
+ import type { InstallationType } from '../types/types.js';
2
3
  export declare function getProjectFolder(projectName: string): string;
3
4
  export declare function isValidName(name: string): boolean;
4
5
  export declare function isAnswerConfirmed(answer?: string, errorMessage?: string): boolean;
5
6
  export declare function canShowStep(currentStep: number, stepToShow: number): boolean;
6
7
  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[];
8
+ type FeatureToggleAction = 'select' | 'unselect';
9
+ export declare function resolveSelectedFeatures(stack: Stack, selectedFeatures: FeatureName[]): FeatureName[];
10
+ export declare function applyFeatureToggle(stack: Stack, selectedFeatures: FeatureName[], toggledFeature: FeatureName, action: FeatureToggleAction): FeatureName[];
11
+ export declare function describeInstallPlan(stack: Stack, projectName: string, mode: InstallationType, selectedFeatures: FeatureName[]): string;
12
+ export declare function getPackagesToRemove(stack: Stack, selectedFeatures: FeatureName[]): string[];
13
+ export declare function getPostInstallMessages(stack: Stack, mode: InstallationType, selectedFeatures: FeatureName[]): string[];
14
+ export declare function resolveModeFeatures(stack: Stack, mode: InstallationType, customSelection?: FeatureName[]): FeatureName[];
9
15
  export declare function projectDirectoryExists(projectName: string): boolean;
10
16
  type StepStatus = 'running' | 'done' | 'error';
11
17
  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 { getDefaultFeatureNames, getFeatureNames, getStackConfig, } from '../constants/config.js';
5
5
  export function getProjectFolder(projectName) {
6
6
  return join(process.cwd(), projectName);
7
7
  }
@@ -17,16 +17,88 @@ 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
+ if (mode === 'default') {
72
+ return `${head} · Mode: default (recommended)`;
73
+ }
74
+ const features = selectedFeatures.length > 0 ? selectedFeatures.join(', ') : 'none';
75
+ return `${head} · Mode: custom · Features: ${features}`;
76
+ }
77
+ export function getPackagesToRemove(stack, selectedFeatures) {
78
+ const features = getStackConfig(stack).features;
79
+ return Object.entries(features)
22
80
  .filter(([name]) => !selectedFeatures.includes(name))
23
81
  .flatMap(([, def]) => def.packages);
24
82
  }
25
- export function getPostInstallMessages(mode, selectedFeatures) {
83
+ export function getPostInstallMessages(stack, mode, selectedFeatures) {
84
+ const config = getStackConfig(stack);
85
+ const features = config.features;
86
+ const stackLevel = config.postInstall ?? [];
87
+ const kept = resolveModeFeatures(stack, mode, selectedFeatures);
88
+ const featureMessages = kept.flatMap((name) => features[name]?.postInstall ?? []);
89
+ return [...stackLevel, ...featureMessages];
90
+ }
91
+ // Resolves the kept-feature list for a mode: full → all, default → default:true set,
92
+ // custom → the user's selection (transitive requires resolved). Shared by the non-interactive
93
+ // path and the interactive Install/FileCleanup/PostInstall steps.
94
+ export function resolveModeFeatures(stack, mode, customSelection = []) {
26
95
  if (mode === 'full') {
27
- return Object.values(featureDefinitions).flatMap((def) => def.postInstall ?? []);
96
+ return getFeatureNames(stack);
97
+ }
98
+ if (mode === 'default') {
99
+ return getDefaultFeatureNames(stack);
28
100
  }
29
- return selectedFeatures.flatMap((name) => featureDefinitions[name]?.postInstall ?? []);
101
+ return resolveSelectedFeatures(stack, customSelection);
30
102
  }
31
103
  export function projectDirectoryExists(projectName) {
32
104
  return existsSync(getProjectFolder(projectName));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dappbooster",
3
- "version": "3.1.5",
3
+ "version": "3.3.0",
4
4
  "description": "Agent-friendly dAppBooster installer that scaffolds Web3 dApps via TUI or non-interactive CLI/CI.",
5
5
  "keywords": [
6
6
  "dappbooster",
@@ -39,9 +39,7 @@
39
39
  "lint": "pnpm biome check",
40
40
  "lint:fix": "pnpm biome check --write"
41
41
  },
42
- "files": [
43
- "dist"
44
- ],
42
+ "files": ["dist"],
45
43
  "dependencies": {
46
44
  "figures": "^6.1.0",
47
45
  "ink": "^5.2.1",
@@ -63,10 +61,5 @@
63
61
  "ts-node": "^10.9.1",
64
62
  "typescript": "^5.8.3",
65
63
  "vitest": "^4.1.0"
66
- },
67
- "pnpm": {
68
- "onlyBuiltDependencies": [
69
- "@biomejs/biome"
70
- ]
71
64
  }
72
65
  }