dappbooster 3.0.2 → 3.1.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.
package/dist/app.js CHANGED
@@ -18,13 +18,12 @@ const App = () => {
18
18
  const finishStep = useCallback(() => setCurrentStep((prevStep) => prevStep + 1), []);
19
19
  const onSelectSetupType = useCallback((item) => setSetupType(item), []);
20
20
  const onSelectSelectedFeatures = useCallback((selectedItems) => setSelectedFeatures([...selectedItems]), []);
21
+ const skipFeatures = setupType?.value === 'full';
21
22
  const steps = useMemo(() => [
22
23
  _jsx(ProjectName, { onCompletion: finishStep, onSubmit: setProjectName, projectName: projectName }, 1),
23
24
  _jsx(CloneRepo, { onCompletion: finishStep, projectName: projectName }, 2),
24
25
  _jsx(InstallationMode, { onCompletion: finishStep, onSelect: onSelectSetupType }, 3),
25
- // TODO: add a skip parameter to all (or most) steps
26
- // to allow skipping when testing, etc.
27
- _jsx(OptionalPackages, { onCompletion: finishStep, onSubmit: onSelectSelectedFeatures, skip: setupType?.value === 'full' }, 4),
26
+ _jsx(OptionalPackages, { onCompletion: finishStep, onSubmit: onSelectSelectedFeatures, skip: skipFeatures }, 4),
28
27
  _jsx(Install, { installationConfig: {
29
28
  installationType: setupType?.value,
30
29
  selectedFeatures: selectedFeatures,
@@ -44,6 +43,7 @@ const App = () => {
44
43
  selectedFeatures,
45
44
  onSelectSetupType,
46
45
  projectName,
46
+ skipFeatures,
47
47
  ]);
48
48
  return (_jsxs(Box, { flexDirection: 'column', rowGap: 1, width: 80, children: [_jsx(MainTitle, {}), steps.map((item, index) => canShowStep(currentStep, index + 1) && item)] }));
49
49
  };
package/dist/cli.js CHANGED
@@ -1,6 +1,97 @@
1
1
  #!/usr/bin/env node
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { render } from 'ink';
4
- import App from './app.js';
5
- console.clear();
6
- render(_jsx(App, {}));
3
+ import process from 'node:process';
4
+ import meow from 'meow';
5
+ import { getInfoOutput } from './info.js';
6
+ import { runNonInteractive } from './nonInteractive.js';
7
+ const cli = meow(`
8
+ Usage
9
+ $ dappbooster [options]
10
+
11
+ Options
12
+ --name <string> Project name (alphanumeric, underscores)
13
+ --mode <full|custom> Installation mode
14
+ --features <list> Comma-separated features (with --mode=custom):
15
+ demo Component demos and example pages
16
+ subgraph TheGraph subgraph integration (requires API key)
17
+ typedoc TypeDoc API documentation generation
18
+ vocs Vocs documentation site
19
+ husky Git hooks with Husky, lint-staged, commitlint
20
+ --non-interactive, --ni Run without prompts (auto-enabled when not a TTY)
21
+ --info Output feature metadata as JSON
22
+ --help Show this help
23
+ --version Show version
24
+
25
+ Non-interactive mode
26
+ Requires --name and --mode. Outputs JSON to stdout.
27
+ Activates automatically when stdout is not a TTY.
28
+ Use --ni to force non-interactive mode in a TTY environment.
29
+
30
+ AI agents: non-interactive mode activates automatically. Run --info
31
+ to discover available features, then pass --name and --mode flags.
32
+ Output is JSON for easy parsing.
33
+
34
+ Examples
35
+ Interactive:
36
+ $ dappbooster
37
+
38
+ Full install (non-interactive):
39
+ $ dappbooster --ni --name my_dapp --mode full
40
+
41
+ Custom install with specific features:
42
+ $ dappbooster --ni --name my_dapp --mode custom --features demo,subgraph
43
+
44
+ Get feature metadata:
45
+ $ dappbooster --info
46
+ `, {
47
+ importMeta: import.meta,
48
+ flags: {
49
+ name: {
50
+ type: 'string',
51
+ },
52
+ mode: {
53
+ type: 'string',
54
+ },
55
+ features: {
56
+ type: 'string',
57
+ },
58
+ nonInteractive: {
59
+ type: 'boolean',
60
+ default: false,
61
+ },
62
+ ni: {
63
+ type: 'boolean',
64
+ default: false,
65
+ },
66
+ info: {
67
+ type: 'boolean',
68
+ default: false,
69
+ },
70
+ },
71
+ });
72
+ if (cli.flags.info) {
73
+ console.log(getInfoOutput());
74
+ }
75
+ else if (cli.flags.nonInteractive || cli.flags.ni || !process.stdout.isTTY) {
76
+ runNonInteractive({
77
+ name: cli.flags.name,
78
+ mode: cli.flags.mode,
79
+ features: cli.flags.features,
80
+ }).catch((error) => {
81
+ if (process.exitCode === 1) {
82
+ return;
83
+ }
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ console.log(JSON.stringify({ success: false, error: message }, null, 2));
86
+ process.exitCode = 1;
87
+ });
88
+ }
89
+ else {
90
+ const run = async () => {
91
+ console.clear();
92
+ const { render } = await import('ink');
93
+ const { default: App } = await import('./app.js');
94
+ render(_jsx(App, {}));
95
+ };
96
+ run().catch(console.error);
97
+ }
@@ -1,13 +1,7 @@
1
- import type { FC } from 'react';
1
+ import { type FC } from 'react';
2
2
  interface Props {
3
3
  projectName: string;
4
4
  onCompletion: () => void;
5
5
  }
6
- /**
7
- * Step for cloning the repository.
8
- * @param projectName
9
- * @param onCompletion
10
- * @constructor
11
- */
12
6
  declare const CloneRepo: FC<Props>;
13
7
  export default CloneRepo;
@@ -1,11 +1,28 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { cloneRepo } from '../../../operations/index.js';
5
+ import { deriveStepDisplay } from '../../../utils/utils.js';
2
6
  import Divider from '../../Divider.js';
3
- import Commands from './Commands.js';
4
- /**
5
- * Step for cloning the repository.
6
- * @param projectName
7
- * @param onCompletion
8
- * @constructor
9
- */
10
- const CloneRepo = ({ projectName, onCompletion }) => (_jsxs(_Fragment, { children: [_jsx(Divider, { title: 'Git tasks' }), _jsx(Commands, { projectName: projectName, onCompletion: onCompletion })] }));
7
+ const CloneRepo = ({ projectName, onCompletion }) => {
8
+ const [steps, setSteps] = useState([]);
9
+ const [status, setStatus] = useState('running');
10
+ const [errorMessage, setErrorMessage] = useState('');
11
+ const handleProgress = useCallback((step) => {
12
+ setSteps((prev) => [...prev, step]);
13
+ }, []);
14
+ useEffect(() => {
15
+ cloneRepo(projectName, handleProgress)
16
+ .then(() => {
17
+ setStatus('done');
18
+ onCompletion();
19
+ })
20
+ .catch((error) => {
21
+ setStatus('error');
22
+ setErrorMessage(error instanceof Error ? error.message : String(error));
23
+ });
24
+ }, [projectName, onCompletion, handleProgress]);
25
+ const { completedSteps, currentStep, failedStep } = deriveStepDisplay(steps, status);
26
+ return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: 'Git tasks' }), completedSteps.map((step) => (_jsxs(Text, { children: [_jsx(Text, { color: 'green', children: '\u2714' }), " ", step] }, step))), currentStep && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: '\u25CB' }), " ", currentStep, " ", _jsx(Text, { dimColor: true, children: "Working..." })] })), failedStep && (_jsxs(Text, { children: [_jsx(Text, { color: 'red', children: '\u2717' }), " ", failedStep, " ", _jsx(Text, { color: 'red', children: "Error" })] })), status === 'error' && _jsxs(Text, { color: 'red', children: ["Failed to clone: ", errorMessage] })] }));
27
+ };
11
28
  export default CloneRepo;
@@ -8,11 +8,5 @@ interface Props {
8
8
  selectedFeatures?: Array<MultiSelectItem>;
9
9
  };
10
10
  }
11
- /**
12
- * Performs file cleanup after the installation process
13
- * @param onCompletion
14
- * @param installation
15
- * @param projectName
16
- */
17
11
  declare const FileCleanup: FC<Props>;
18
12
  export default FileCleanup;
@@ -1,43 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { readFileSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { Box, Text } from 'ink';
5
- import { Script, Spawn } from 'ink-spawn';
6
- import { useMemo } from 'react';
7
- import { featureSelected, getProjectFolder } from '../../utils/utils.js';
2
+ import { Text } from 'ink';
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { cleanupFiles } from '../../operations/index.js';
5
+ import { deriveStepDisplay, getProjectFolder } from '../../utils/utils.js';
8
6
  import Divider from '../Divider.js';
9
- const packageJSONCleanup = (projectFolder, selectedFeatures) => {
10
- const packageJSONPath = join(projectFolder, 'package.json');
11
- const packageJSON = JSON.parse(readFileSync(packageJSONPath, 'utf8'));
12
- if (!featureSelected('subgraph', selectedFeatures)) {
13
- packageJSON.scripts['subgraph-codegen'] = undefined;
14
- }
15
- if (!featureSelected('typedoc', selectedFeatures)) {
16
- packageJSON.scripts['typedoc:build'] = undefined;
17
- }
18
- if (!featureSelected('vocs', selectedFeatures)) {
19
- packageJSON.scripts['docs:build'] = undefined;
20
- packageJSON.scripts['docs:dev'] = undefined;
21
- packageJSON.scripts['docs:preview'] = undefined;
22
- }
23
- if (!featureSelected('husky', selectedFeatures)) {
24
- packageJSON.scripts.prepare = undefined;
25
- }
26
- writeFileSync(packageJSONPath, `${JSON.stringify(packageJSON, null, 2)}\n`);
27
- };
28
- /**
29
- * Performs file cleanup after the installation process
30
- * @param onCompletion
31
- * @param installation
32
- * @param projectName
33
- */
34
7
  const FileCleanup = ({ onCompletion, installationConfig, projectName }) => {
35
8
  const { installationType, selectedFeatures } = installationConfig;
36
9
  const projectFolder = useMemo(() => getProjectFolder(projectName), [projectName]);
37
- const currentHomeFolder = `${projectFolder}/src/components/pageComponents/home`;
38
- return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: 'File cleanup' }), _jsx(Box, { flexDirection: 'column', gap: 0, children: _jsxs(Script, { children: [installationType === 'custom' && (_jsxs(Script, { onCompletion: () => packageJSONCleanup(projectFolder, selectedFeatures), children: [!featureSelected('demo', selectedFeatures) && (_jsxs(Script, { children: [_jsx(Text, { color: 'whiteBright', children: "Component demos" }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: ['-rf', currentHomeFolder], runningText: 'Removing home files...', successText: 'Done!', failureText: 'Error...' }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "mkdir", args: ['-p', currentHomeFolder], runningText: 'Creating home folder...', successText: 'Done!', failureText: 'Error...' }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "cp", args: ['./.install-files/home/index.tsx', currentHomeFolder], runningText: 'Creating new home page index file...', successText: 'Done!', failureText: 'Error...' })] })), !featureSelected('subgraph', selectedFeatures) && (_jsxs(Script, { children: [_jsx(Text, { color: 'whiteBright', children: "Subgraph" }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: ['-rf', './src/subgraphs'], runningText: 'Removing subgraphs folder...', successText: 'Done!', failureText: 'Error...' }), featureSelected('demo', selectedFeatures) && (_jsxs(Script, { children: [_jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: ['-rf', `${currentHomeFolder}/Examples/demos/subgraphs`], runningText: 'Removing subgraphs demos folder...', successText: 'Done!', failureText: 'Error...' }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: [`${currentHomeFolder}/Examples/index.tsx`], runningText: 'Removing examples index file...', successText: 'Done!', failureText: 'Error...' }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "cp", args: [
39
- './.install-files/home/Examples/index.tsx',
40
- `${currentHomeFolder}/Examples/index.tsx`,
41
- ], runningText: 'Creating new examples index file...', successText: 'Done!', failureText: 'Error...' })] }))] })), !featureSelected('typedoc', selectedFeatures) && (_jsxs(Script, { children: [_jsx(Text, { color: 'whiteBright', children: "Typedoc" }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: ['./typedoc.json'], runningText: 'Removing config...', successText: 'Done!', failureText: 'Error...' })] })), !featureSelected('vocs', selectedFeatures) && (_jsxs(Script, { children: [_jsx(Text, { color: 'whiteBright', children: "Vocs" }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: ['./vocs.config.ts'], runningText: 'Removing config...', successText: 'Done!', failureText: 'Error...' }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: ['-rf', './docs'], runningText: 'Removing docs folder...', successText: 'Done!', failureText: 'Error...' })] })), !featureSelected('husky', selectedFeatures) && (_jsxs(Script, { children: [_jsx(Text, { color: 'whiteBright', children: "Husky" }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: ['-rf', './.husky'], runningText: 'Removing Husky folder...', successText: 'Done!', failureText: 'Error...' }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: ['./.lintstagedrc.mjs'], runningText: 'Removing lint-staged config...', successText: 'Done!', failureText: 'Error...' }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: ['./commitlint.config.js'], runningText: 'Removing commitlint config...', successText: 'Done!', failureText: 'Error...' })] }))] })), _jsx(Text, { color: 'whiteBright', children: "Install script" }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: "rm", args: ['-rf', './.install-files'], runningText: 'Removing folder...', successText: 'Done!', failureText: 'Error...', onCompletion: onCompletion })] }) })] }));
10
+ const [steps, setSteps] = useState([]);
11
+ const [status, setStatus] = useState('running');
12
+ const [errorMessage, setErrorMessage] = useState('');
13
+ const handleProgress = useCallback((step) => {
14
+ setSteps((prev) => [...prev, step]);
15
+ }, []);
16
+ useEffect(() => {
17
+ const features = selectedFeatures?.map((f) => f.value) ?? [];
18
+ cleanupFiles(projectFolder, installationType ?? 'full', features, handleProgress)
19
+ .then(() => {
20
+ setStatus('done');
21
+ onCompletion();
22
+ })
23
+ .catch((error) => {
24
+ setStatus('error');
25
+ setErrorMessage(error instanceof Error ? error.message : String(error));
26
+ });
27
+ }, [projectFolder, installationType, selectedFeatures, onCompletion, handleProgress]);
28
+ const { completedSteps, currentStep, failedStep } = deriveStepDisplay(steps, status);
29
+ return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: 'File cleanup' }), completedSteps.map((step) => (_jsxs(Text, { children: [_jsx(Text, { color: 'green', children: '\u2714' }), " ", step] }, step))), currentStep && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: '\u25CB' }), " ", currentStep, " ", _jsx(Text, { dimColor: true, children: "Working..." })] })), failedStep && (_jsxs(Text, { children: [_jsx(Text, { color: 'red', children: '\u2717' }), " ", failedStep, " ", _jsx(Text, { color: 'red', children: "Error" })] })), status === 'error' && _jsxs(Text, { color: 'red', children: ["Cleanup failed: ", errorMessage] })] }));
42
30
  };
43
31
  export default FileCleanup;
@@ -1,17 +1,40 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { Script, Spawn } from 'ink-spawn';
4
- import { useMemo } from 'react';
5
- import { getProjectFolder } from '../../../utils/utils.js';
2
+ import { Text } from 'ink';
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { createEnvFile } from '../../../operations/createEnvFile.js';
5
+ import { installPackages } from '../../../operations/installPackages.js';
6
+ import { deriveStepDisplay, getProjectFolder } from '../../../utils/utils.js';
6
7
  import Divider from '../../Divider.js';
7
- import CustomInstallation from './CustomInstallation.js';
8
- import FullInstallation from './FullInstallation.js';
9
8
  const Install = ({ projectName, onCompletion, installationConfig }) => {
10
9
  const { installationType, selectedFeatures } = installationConfig;
11
10
  const projectFolder = useMemo(() => getProjectFolder(projectName), [projectName]);
11
+ const [steps, setSteps] = useState([]);
12
+ const [status, setStatus] = useState('running');
13
+ const [errorMessage, setErrorMessage] = useState('');
12
14
  const title = installationType
13
15
  ? installationType[0]?.toUpperCase() + installationType.slice(1)
14
- : '';
15
- return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: `${title ?? 'Full'} installation` }), _jsx(Box, { flexDirection: 'column', gap: 0, children: _jsxs(Script, { children: [_jsx(Box, { columnGap: 1, children: _jsxs(Text, { color: 'whiteBright', children: ["Creating", ' ', _jsx(Text, { italic: true, color: 'white', children: ".env.local" }), ' ', "file"] }) }), _jsx(Spawn, { shell: true, cwd: projectFolder, silent: true, command: 'cp', args: ['.env.example', '.env.local'], runningText: 'Working...', successText: 'Done!', failureText: 'Error...' }), installationType === 'full' && (_jsx(FullInstallation, { onCompletion: onCompletion, projectFolder: projectFolder })), installationType === 'custom' && (_jsx(CustomInstallation, { selectedFeatures: selectedFeatures, onCompletion: onCompletion, projectFolder: projectFolder }))] }) })] }));
16
+ : 'Full';
17
+ const handleProgress = useCallback((step) => {
18
+ setSteps((prev) => [...prev, step]);
19
+ }, []);
20
+ useEffect(() => {
21
+ const features = selectedFeatures?.map((f) => f.value) ?? [];
22
+ const run = async () => {
23
+ handleProgress('Creating .env.local file');
24
+ await createEnvFile(projectFolder);
25
+ await installPackages(projectFolder, installationType ?? 'full', features, handleProgress);
26
+ };
27
+ run()
28
+ .then(() => {
29
+ setStatus('done');
30
+ onCompletion();
31
+ })
32
+ .catch((error) => {
33
+ setStatus('error');
34
+ setErrorMessage(error instanceof Error ? error.message : String(error));
35
+ });
36
+ }, [projectFolder, installationType, selectedFeatures, onCompletion, handleProgress]);
37
+ const { completedSteps, currentStep, failedStep } = deriveStepDisplay(steps, status);
38
+ return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: `${title} installation` }), completedSteps.map((step) => (_jsxs(Text, { children: [_jsx(Text, { color: 'green', children: '\u2714' }), " ", step] }, step))), currentStep && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: '\u25CB' }), " ", currentStep, " ", _jsx(Text, { dimColor: true, children: "Working..." })] })), failedStep && (_jsxs(Text, { children: [_jsx(Text, { color: 'red', children: '\u2717' }), " ", failedStep, " ", _jsx(Text, { color: 'red', children: "Error" })] })), status === 'error' && _jsxs(Text, { color: 'red', children: ["Installation failed: ", errorMessage] })] }));
16
39
  };
17
40
  export default Install;
@@ -5,12 +5,5 @@ interface Props {
5
5
  onSubmit: (selectedItems: Array<MultiSelectItem>) => void;
6
6
  skip?: boolean;
7
7
  }
8
- /**
9
- * Step for selecting optional packages. Skipped if installation type is 'full'.
10
- * @param onCompletion
11
- * @param onSubmit
12
- * @param installation
13
- * @param skip
14
- */
15
8
  declare const OptionalPackages: FC<Props>;
16
9
  export default OptionalPackages;
@@ -1,41 +1,16 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Text } from 'ink';
3
3
  import { useEffect, useState } from 'react';
4
+ import { featureDefinitions, featureNames } from '../../constants/config.js';
4
5
  import MultiSelect from '../Multiselect/index.js';
5
- const customPackages = [
6
- {
7
- label: 'Component Demos',
8
- value: 'demo',
9
- },
10
- {
11
- label: 'Subgraph support',
12
- value: 'subgraph',
13
- },
14
- {
15
- label: 'Typedoc documentation support',
16
- value: 'typedoc',
17
- },
18
- {
19
- label: 'Vocs documentation support',
20
- value: 'vocs',
21
- },
22
- {
23
- label: 'Husky Git hooks support',
24
- value: 'husky',
25
- },
26
- ];
27
- /**
28
- * Step for selecting optional packages. Skipped if installation type is 'full'.
29
- * @param onCompletion
30
- * @param onSubmit
31
- * @param installation
32
- * @param skip
33
- */
6
+ const customPackages = featureNames.map((name) => ({
7
+ label: featureDefinitions[name].label,
8
+ value: name,
9
+ }));
34
10
  const OptionalPackages = ({ onCompletion, onSubmit, skip = false }) => {
35
11
  const [isFocused, setIsFocused] = useState(true);
36
12
  // biome-ignore lint/correctness/useExhaustiveDependencies: Run this only once, no matter what
37
13
  useEffect(() => {
38
- // full installation, do nothing
39
14
  if (skip) {
40
15
  onCompletion();
41
16
  }
@@ -7,10 +7,5 @@ interface Props {
7
7
  };
8
8
  projectName: string;
9
9
  }
10
- /**
11
- * Component to ask for the project name.
12
- * @param selectedFeatures
13
- * @param projectName
14
- */
15
10
  declare const PostInstall: FC<Props>;
16
11
  export default PostInstall;
@@ -2,18 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import figures from 'figures';
3
3
  import { Box, Text } from 'ink';
4
4
  import Link from 'ink-link';
5
- import { featureSelected } from '../../utils/utils.js';
5
+ import { isFeatureSelected } from '../../utils/utils.js';
6
6
  import Divider from '../Divider.js';
7
7
  const SubgraphWarningMessage = () => (_jsxs(Box, { flexDirection: 'column', rowGap: 1, children: [_jsx(Box, { alignItems: 'center', borderColor: 'yellow', borderStyle: 'bold', flexDirection: 'column', justifyContent: 'center', padding: 1, children: _jsxs(Text, { color: 'yellow', children: [figures.warning, figures.warning, " ", _jsx(Text, { bold: true, children: "WARNING:" }), " You ", _jsx(Text, { bold: true, children: "MUST" }), " finish the subgraph's configuration manually ", figures.warning, figures.warning] }) }), _jsx(Text, { color: 'whiteBright', children: "Follow these steps:" }), _jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { children: ["1- Provide your own API key for ", _jsx(Text, { color: 'gray', children: "PUBLIC_SUBGRAPHS_API_KEY" }), " in", ' ', _jsx(Text, { color: 'gray', children: ".env.local" }), " You can get one", ' ', _jsx(Link, { url: "https://thegraph.com/studio/apikeys", children: "here" })] }), _jsxs(Text, { children: ["2- After the API key is correctly configured, run", ' ', _jsx(Text, { color: 'gray', children: "pnpm subgraph-codegen" }), " in your console from the project's folder"] })] }), _jsxs(Text, { children: ["More configuration info in", ' ', _jsx(Link, { url: 'https://docs.dappbooster.dev/introduction/getting-started', children: "the docs" }), "."] }), _jsxs(Text, { color: 'yellow', bold: true, children: [figures.info, " Only after you have followed the previous steps you may proceed."] })] }));
8
8
  const PostInstallMessage = ({ projectName }) => (_jsxs(Box, { flexDirection: 'column', rowGap: 1, paddingBottom: 2, children: [_jsx(Text, { color: 'whiteBright', children: "To start development on your project:" }), _jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { children: ["1- Move into the project's folder with ", _jsxs(Text, { color: 'gray', children: ["cd ", projectName] })] }), _jsxs(Text, { children: ["2- Start the development server with ", _jsx(Text, { color: 'gray', children: "pnpm dev" })] })] }), _jsx(Text, { color: 'whiteBright', children: "More info:" }), _jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { children: ["- Check out ", _jsx(Text, { color: 'gray', children: ".env.local" }), " for more configurations."] }), _jsxs(Text, { children: ["- Read ", _jsx(Link, { url: "https://docs.dappbooster.dev", children: "the docs" }), " to know more about", ' ', _jsx(Text, { color: 'gray', children: "dAppBooster" }), "!"] }), _jsxs(Text, { children: ["- Report issues with this installer", ' ', _jsx(Link, { url: "https://github.com/BootNodeDev/dAppBoosterInstallScript/issues", children: "here" })] })] })] }));
9
- /**
10
- * Component to ask for the project name.
11
- * @param selectedFeatures
12
- * @param projectName
13
- */
14
9
  const PostInstall = ({ installationConfig, projectName }) => {
15
10
  const { selectedFeatures, installationType } = installationConfig;
16
- const subgraphSupport = featureSelected('subgraph', selectedFeatures);
11
+ const features = selectedFeatures?.map((f) => f.value) ?? [];
12
+ const subgraphSupport = isFeatureSelected('subgraph', features);
17
13
  return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: 'Post-install instructions' }), _jsxs(Box, { flexDirection: 'column', rowGap: 2, children: [(subgraphSupport || installationType === 'full') && _jsx(SubgraphWarningMessage, {}), _jsx(PostInstallMessage, { projectName: projectName })] })] }));
18
14
  };
19
15
  export default PostInstall;
@@ -1,4 +1,11 @@
1
1
  export declare const repoUrl = "https://github.com/BootNodeDev/dAppBooster.git";
2
- export declare const featurePackages: {
3
- [key: string]: string[];
2
+ export type FeatureName = 'demo' | 'subgraph' | 'typedoc' | 'vocs' | 'husky';
3
+ export type FeatureDefinition = {
4
+ description: string;
5
+ label: string;
6
+ packages: string[];
7
+ default: boolean;
8
+ postInstall?: string[];
4
9
  };
10
+ export declare const featureDefinitions: Record<FeatureName, FeatureDefinition>;
11
+ export declare const featureNames: FeatureName[];
@@ -1,18 +1,50 @@
1
1
  export const repoUrl = 'https://github.com/BootNodeDev/dAppBooster.git';
2
- export const featurePackages = {
3
- subgraph: [
4
- '@bootnodedev/db-subgraph',
5
- 'graphql graphql-request',
6
- '@graphql-codegen/cli',
7
- '@graphql-typed-document-node/core',
8
- ],
9
- typedoc: [
10
- 'typedoc',
11
- 'typedoc-github-theme',
12
- 'typedoc-plugin-inline-sources',
13
- 'typedoc-plugin-missing-exports',
14
- 'typedoc-plugin-rename-defaults',
15
- ],
16
- vocs: ['vocs'],
17
- husky: ['husky', 'lint-staged', '@commitlint/cli', '@commitlint/config-conventional'],
2
+ export const featureDefinitions = {
3
+ demo: {
4
+ description: 'Component demos and example pages',
5
+ label: 'Component Demos',
6
+ packages: [],
7
+ default: true,
8
+ },
9
+ subgraph: {
10
+ description: 'TheGraph subgraph integration',
11
+ label: 'Subgraph support',
12
+ packages: [
13
+ '@bootnodedev/db-subgraph',
14
+ 'graphql',
15
+ 'graphql-request',
16
+ '@graphql-codegen/cli',
17
+ '@graphql-typed-document-node/core',
18
+ ],
19
+ default: true,
20
+ postInstall: [
21
+ 'Provide your own API key for PUBLIC_SUBGRAPHS_API_KEY in .env.local',
22
+ 'Run pnpm subgraph-codegen from the project folder',
23
+ ],
24
+ },
25
+ typedoc: {
26
+ description: 'TypeDoc API documentation generation',
27
+ label: 'Typedoc documentation support',
28
+ packages: [
29
+ 'typedoc',
30
+ 'typedoc-github-theme',
31
+ 'typedoc-plugin-inline-sources',
32
+ 'typedoc-plugin-missing-exports',
33
+ 'typedoc-plugin-rename-defaults',
34
+ ],
35
+ default: true,
36
+ },
37
+ vocs: {
38
+ description: 'Vocs documentation site',
39
+ label: 'Vocs documentation support',
40
+ packages: ['vocs'],
41
+ default: true,
42
+ },
43
+ husky: {
44
+ description: 'Git hooks with Husky, lint-staged, and commitlint',
45
+ label: 'Husky Git hooks support',
46
+ packages: ['husky', 'lint-staged', '@commitlint/cli', '@commitlint/config-conventional'],
47
+ default: true,
48
+ },
18
49
  };
50
+ export const featureNames = Object.keys(featureDefinitions);
package/dist/info.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function getInfoOutput(): string;
package/dist/info.js ADDED
@@ -0,0 +1,18 @@
1
+ import { featureDefinitions } from './constants/config.js';
2
+ export function getInfoOutput() {
3
+ const features = Object.fromEntries(Object.entries(featureDefinitions).map(([name, def]) => [
4
+ name,
5
+ {
6
+ description: def.description,
7
+ default: def.default,
8
+ ...(def.postInstall ? { postInstall: def.postInstall } : {}),
9
+ },
10
+ ]));
11
+ return JSON.stringify({
12
+ features,
13
+ modes: {
14
+ full: 'Install all features',
15
+ custom: 'Choose features individually',
16
+ },
17
+ }, null, 2);
18
+ }
@@ -0,0 +1,5 @@
1
+ export declare function runNonInteractive(flags: {
2
+ name?: string;
3
+ mode?: string;
4
+ features?: string;
5
+ }): Promise<void>;
@@ -0,0 +1,85 @@
1
+ import process from 'node:process';
2
+ import { featureNames } from './constants/config.js';
3
+ import { cleanupFiles, cloneRepo, createEnvFile, installPackages } from './operations/index.js';
4
+ import { getPostInstallMessages, getProjectFolder, isValidName, projectDirectoryExists, } from './utils/utils.js';
5
+ function fail(error) {
6
+ const result = { success: false, error };
7
+ console.log(JSON.stringify(result, null, 2));
8
+ process.exitCode = 1;
9
+ throw new Error(error);
10
+ }
11
+ function parseFeatures(featuresFlag) {
12
+ if (!featuresFlag) {
13
+ return [];
14
+ }
15
+ const seen = new Set();
16
+ return featuresFlag
17
+ .split(',')
18
+ .map((f) => f.trim())
19
+ .filter((f) => {
20
+ if (f === '' || seen.has(f)) {
21
+ return false;
22
+ }
23
+ seen.add(f);
24
+ return true;
25
+ });
26
+ }
27
+ function validate(flags) {
28
+ if (!flags.name) {
29
+ fail('Missing required flag: --name');
30
+ }
31
+ if (!flags.mode) {
32
+ fail('Missing required flag: --mode');
33
+ }
34
+ if (!isValidName(flags.name)) {
35
+ fail('Invalid project name: only letters, numbers, and underscores are allowed');
36
+ }
37
+ if (flags.mode !== 'full' && flags.mode !== 'custom') {
38
+ fail("Invalid mode: must be 'full' or 'custom'");
39
+ }
40
+ // --mode=full ignores --features (everything is installed)
41
+ if (flags.mode === 'full') {
42
+ if (projectDirectoryExists(flags.name)) {
43
+ fail(`Project directory '${flags.name}' already exists`);
44
+ }
45
+ return { name: flags.name, mode: flags.mode, features: featureNames };
46
+ }
47
+ if (!flags.features) {
48
+ fail('--mode custom requires --features. Use --info to see available features.');
49
+ }
50
+ const features = parseFeatures(flags.features);
51
+ if (features.length === 0) {
52
+ fail('--features value is empty. Use --info to see available features.');
53
+ }
54
+ const invalidFeatures = features.filter((f) => !featureNames.includes(f));
55
+ if (invalidFeatures.length > 0) {
56
+ fail(`Unknown features: ${invalidFeatures.join(', ')}. Valid features: ${featureNames.join(', ')}`);
57
+ }
58
+ if (projectDirectoryExists(flags.name)) {
59
+ fail(`Project directory '${flags.name}' already exists`);
60
+ }
61
+ return { name: flags.name, mode: flags.mode, features };
62
+ }
63
+ export async function runNonInteractive(flags) {
64
+ const { name, mode, features } = validate(flags);
65
+ try {
66
+ await cloneRepo(name);
67
+ const projectFolder = getProjectFolder(name);
68
+ await createEnvFile(projectFolder);
69
+ await installPackages(projectFolder, mode, features);
70
+ await cleanupFiles(projectFolder, mode, features);
71
+ const result = {
72
+ success: true,
73
+ projectName: name,
74
+ mode,
75
+ features,
76
+ path: projectFolder,
77
+ postInstall: getPostInstallMessages(mode, features),
78
+ };
79
+ console.log(JSON.stringify(result, null, 2));
80
+ }
81
+ catch (error) {
82
+ const message = error instanceof Error ? error.message : String(error);
83
+ fail(message);
84
+ }
85
+ }
@@ -0,0 +1,3 @@
1
+ import type { FeatureName } from '../constants/config.js';
2
+ import type { InstallationType } from '../types/types.js';
3
+ export declare function cleanupFiles(projectFolder: string, mode: InstallationType, features?: FeatureName[], onProgress?: (step: string) => void): Promise<void>;
@@ -0,0 +1,77 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { copyFile, mkdir, rm } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { isFeatureSelected } from '../utils/utils.js';
5
+ function patchPackageJson(projectFolder, features) {
6
+ const packageJsonPath = resolve(projectFolder, 'package.json');
7
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
8
+ if (!isFeatureSelected('subgraph', features)) {
9
+ packageJson.scripts['subgraph-codegen'] = undefined;
10
+ }
11
+ if (!isFeatureSelected('typedoc', features)) {
12
+ packageJson.scripts['typedoc:build'] = undefined;
13
+ }
14
+ if (!isFeatureSelected('vocs', features)) {
15
+ packageJson.scripts['docs:build'] = undefined;
16
+ packageJson.scripts['docs:dev'] = undefined;
17
+ packageJson.scripts['docs:preview'] = undefined;
18
+ }
19
+ if (!isFeatureSelected('husky', features)) {
20
+ packageJson.scripts.prepare = undefined;
21
+ }
22
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
23
+ }
24
+ async function cleanupDemo(projectFolder) {
25
+ const homeFolder = resolve(projectFolder, 'src/components/pageComponents/home');
26
+ await rm(homeFolder, { recursive: true, force: true });
27
+ await mkdir(homeFolder, { recursive: true });
28
+ await copyFile(resolve(projectFolder, '.install-files/home/index.tsx'), resolve(homeFolder, 'index.tsx'));
29
+ }
30
+ async function cleanupSubgraph(projectFolder, features) {
31
+ await rm(resolve(projectFolder, 'src/subgraphs'), { recursive: true, force: true });
32
+ if (isFeatureSelected('demo', features)) {
33
+ const homeFolder = resolve(projectFolder, 'src/components/pageComponents/home');
34
+ await rm(resolve(homeFolder, 'Examples/demos/subgraphs'), { recursive: true, force: true });
35
+ await rm(resolve(homeFolder, 'Examples/index.tsx'), { force: true });
36
+ await copyFile(resolve(projectFolder, '.install-files/home/Examples/index.tsx'), resolve(homeFolder, 'Examples/index.tsx'));
37
+ }
38
+ }
39
+ async function cleanupTypedoc(projectFolder) {
40
+ await rm(resolve(projectFolder, 'typedoc.json'), { force: true });
41
+ }
42
+ async function cleanupVocs(projectFolder) {
43
+ await rm(resolve(projectFolder, 'vocs.config.ts'), { force: true });
44
+ await rm(resolve(projectFolder, 'docs'), { recursive: true, force: true });
45
+ }
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) {
52
+ if (mode === 'custom') {
53
+ if (!isFeatureSelected('demo', features)) {
54
+ onProgress?.('Component demos');
55
+ await cleanupDemo(projectFolder);
56
+ }
57
+ if (!isFeatureSelected('subgraph', features)) {
58
+ onProgress?.('Subgraph');
59
+ await cleanupSubgraph(projectFolder, features);
60
+ }
61
+ if (!isFeatureSelected('typedoc', features)) {
62
+ onProgress?.('Typedoc');
63
+ await cleanupTypedoc(projectFolder);
64
+ }
65
+ if (!isFeatureSelected('vocs', features)) {
66
+ onProgress?.('Vocs');
67
+ await cleanupVocs(projectFolder);
68
+ }
69
+ if (!isFeatureSelected('husky', features)) {
70
+ onProgress?.('Husky');
71
+ await cleanupHusky(projectFolder);
72
+ }
73
+ patchPackageJson(projectFolder, features);
74
+ }
75
+ onProgress?.('Install script');
76
+ await rm(resolve(projectFolder, '.install-files'), { recursive: true, force: true });
77
+ }
@@ -0,0 +1 @@
1
+ export declare function cloneRepo(projectName: string, onProgress?: (step: string) => void): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { rm } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { repoUrl } from '../constants/config.js';
4
+ import { getProjectFolder } from '../utils/utils.js';
5
+ import { exec, execFile } from './exec.js';
6
+ export async function cloneRepo(projectName, onProgress) {
7
+ 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
+ });
17
+ onProgress?.('Removing .git folder');
18
+ await rm(resolve(projectFolder, '.git'), { recursive: true, force: true });
19
+ onProgress?.('Initializing Git repository');
20
+ await execFile('git', ['init'], { cwd: projectFolder });
21
+ }
@@ -0,0 +1 @@
1
+ export declare function createEnvFile(projectFolder: string): Promise<void>;
@@ -0,0 +1,5 @@
1
+ import { copyFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ export async function createEnvFile(projectFolder) {
4
+ await copyFile(join(projectFolder, '.env.example'), join(projectFolder, '.env.local'));
5
+ }
@@ -0,0 +1,6 @@
1
+ export declare function exec(command: string, options?: {
2
+ cwd?: string;
3
+ }): Promise<void>;
4
+ export declare function execFile(file: string, args: string[], options?: {
5
+ cwd?: string;
6
+ }): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { spawn } from 'node:child_process';
2
+ function run(command, args, options) {
3
+ return new Promise((resolve, reject) => {
4
+ const child = spawn(command, args, {
5
+ ...options,
6
+ stdio: ['ignore', 'ignore', 'pipe'],
7
+ });
8
+ let stderr = '';
9
+ child.stderr?.on('data', (data) => {
10
+ stderr += data.toString();
11
+ });
12
+ child.on('close', (code, signal) => {
13
+ if (code === 0) {
14
+ resolve();
15
+ }
16
+ else {
17
+ const fallback = signal !== null
18
+ ? `Process killed by signal ${signal}`
19
+ : `Command failed with exit code ${code}`;
20
+ const message = stderr.trim() || fallback;
21
+ reject(new Error(message));
22
+ }
23
+ });
24
+ child.on('error', reject);
25
+ });
26
+ }
27
+ export async function exec(command, options = {}) {
28
+ await run('/bin/sh', ['-c', command], { cwd: options.cwd });
29
+ }
30
+ export async function execFile(file, args, options = {}) {
31
+ await run(file, args, { cwd: options.cwd });
32
+ }
@@ -0,0 +1,4 @@
1
+ export { cloneRepo } from './cloneRepo.js';
2
+ export { createEnvFile } from './createEnvFile.js';
3
+ export { installPackages } from './installPackages.js';
4
+ export { cleanupFiles } from './cleanupFiles.js';
@@ -0,0 +1,4 @@
1
+ export { cloneRepo } from './cloneRepo.js';
2
+ export { createEnvFile } from './createEnvFile.js';
3
+ export { installPackages } from './installPackages.js';
4
+ export { cleanupFiles } from './cleanupFiles.js';
@@ -0,0 +1,3 @@
1
+ import type { FeatureName } from '../constants/config.js';
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>;
@@ -0,0 +1,19 @@
1
+ import { getPackagesToRemove } from '../utils/utils.js';
2
+ import { execFile } from './exec.js';
3
+ export async function installPackages(projectFolder, mode, features = [], onProgress) {
4
+ if (mode === 'full') {
5
+ onProgress?.('Installing packages');
6
+ await execFile('pnpm', ['i'], { cwd: projectFolder });
7
+ return;
8
+ }
9
+ const packagesToRemove = getPackagesToRemove(features);
10
+ if (packagesToRemove.length === 0) {
11
+ onProgress?.('Installing packages');
12
+ await execFile('pnpm', ['i'], { cwd: projectFolder });
13
+ return;
14
+ }
15
+ onProgress?.('Installing packages');
16
+ await execFile('pnpm', ['remove', ...packagesToRemove], { cwd: projectFolder });
17
+ onProgress?.('Executing post-install scripts');
18
+ await execFile('pnpm', ['run', 'postinstall'], { cwd: projectFolder });
19
+ }
@@ -1,32 +1,17 @@
1
- import type { MultiSelectItem } from '../types/types.js';
1
+ import { type FeatureName } from '../constants/config.js';
2
2
  export declare function getProjectFolder(projectName: string): string;
3
- /**
4
- * Utility functions for import process
5
- * @param name
6
- */
7
3
  export declare function isValidName(name: string): boolean;
8
- /**
9
- * Checks if the answer is confirmed
10
- * @param answer
11
- * @param errorMessage
12
- */
13
4
  export declare function isAnswerConfirmed(answer?: string, errorMessage?: string): boolean;
14
- /**
15
- * Checks if the step can be shown
16
- * @param currentStep
17
- * @param stepToShow
18
- */
19
5
  export declare function canShowStep(currentStep: number, stepToShow: number): boolean;
20
- /**
21
- * Returns true a feature is selected in the features list
22
- * @param feature
23
- * @param featuresList
24
- */
25
- export declare function featureSelected(feature: string, featuresList: Array<MultiSelectItem> | undefined): boolean;
26
- /**
27
- * Returns the packages to remove checking first if the feature is selected or not.
28
- * Selected features are kept, unselected features are removed.
29
- * @param feature
30
- * @param featuresList
31
- */
32
- export declare function getPackages(feature: string, featuresList: Array<MultiSelectItem> | undefined): string[];
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[];
9
+ export declare function projectDirectoryExists(projectName: string): boolean;
10
+ type StepStatus = 'running' | 'done' | 'error';
11
+ type StepDisplay = {
12
+ completedSteps: string[];
13
+ currentStep: string | undefined;
14
+ failedStep: string | undefined;
15
+ };
16
+ export declare function deriveStepDisplay(steps: string[], status: StepStatus): StepDisplay;
17
+ export {};
@@ -1,47 +1,40 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { join } from 'node:path';
2
3
  import process from 'node:process';
3
- import { featurePackages } from '../constants/config.js';
4
+ import { featureDefinitions } from '../constants/config.js';
4
5
  export function getProjectFolder(projectName) {
5
6
  return join(process.cwd(), projectName);
6
7
  }
7
- /**
8
- * Utility functions for import process
9
- * @param name
10
- */
11
8
  export function isValidName(name) {
12
9
  return /^[a-zA-Z0-9_]+$/.test(name);
13
10
  }
14
- /**
15
- * Checks if the answer is confirmed
16
- * @param answer
17
- * @param errorMessage
18
- */
19
11
  export function isAnswerConfirmed(answer, errorMessage) {
20
12
  return (answer !== '' && answer !== undefined && (errorMessage === '' || errorMessage === undefined));
21
13
  }
22
- /**
23
- * Checks if the step can be shown
24
- * @param currentStep
25
- * @param stepToShow
26
- */
27
14
  export function canShowStep(currentStep, stepToShow) {
28
15
  return currentStep > stepToShow - 1;
29
16
  }
30
- /**
31
- * Returns true a feature is selected in the features list
32
- * @param feature
33
- * @param featuresList
34
- */
35
- export function featureSelected(feature, featuresList) {
36
- return !!featuresList?.find((item) => item.value === feature);
37
- }
38
- /**
39
- * Returns the packages to remove checking first if the feature is selected or not.
40
- * Selected features are kept, unselected features are removed.
41
- * @param feature
42
- * @param featuresList
43
- */
44
- export function getPackages(feature, featuresList) {
45
- const packages = featurePackages[feature];
46
- return featureSelected(feature, featuresList) ? [] : packages?.length ? packages : [];
17
+ export function isFeatureSelected(feature, selectedFeatures) {
18
+ return selectedFeatures.includes(feature);
19
+ }
20
+ export function getPackagesToRemove(selectedFeatures) {
21
+ return Object.entries(featureDefinitions)
22
+ .filter(([name]) => !selectedFeatures.includes(name))
23
+ .flatMap(([, def]) => def.packages);
24
+ }
25
+ export function getPostInstallMessages(mode, selectedFeatures) {
26
+ if (mode === 'full') {
27
+ return Object.values(featureDefinitions).flatMap((def) => def.postInstall ?? []);
28
+ }
29
+ return selectedFeatures.flatMap((name) => featureDefinitions[name]?.postInstall ?? []);
30
+ }
31
+ export function projectDirectoryExists(projectName) {
32
+ return existsSync(getProjectFolder(projectName));
33
+ }
34
+ export function deriveStepDisplay(steps, status) {
35
+ return {
36
+ completedSteps: status === 'done' ? steps : steps.slice(0, -1),
37
+ currentStep: status === 'running' ? steps.at(-1) : undefined,
38
+ failedStep: status === 'error' ? steps.at(-1) : undefined,
39
+ };
47
40
  }
package/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "dappbooster",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "license": "MIT",
5
5
  "bin": "dist/cli.js",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=20"
9
9
  },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "test": "vitest run",
14
+ "test:coverage": "vitest run --coverage",
15
+ "lint": "pnpm biome check",
16
+ "lint:fix": "pnpm biome check --write"
17
+ },
10
18
  "files": [
11
19
  "dist"
12
20
  ],
@@ -18,8 +26,8 @@
18
26
  "ink-gradient": "^3.0.0",
19
27
  "ink-link": "^4.1.0",
20
28
  "ink-select-input": "^6.2.0",
21
- "ink-spawn": "^0.1.4",
22
29
  "ink-text-input": "^6.0.0",
30
+ "meow": "^14.1.0",
23
31
  "react": "^18.3.1"
24
32
  },
25
33
  "devDependencies": {
@@ -27,13 +35,14 @@
27
35
  "@sindresorhus/tsconfig": "^7.0.0",
28
36
  "@types/node": "^22.15.21",
29
37
  "@types/react": "^18.3.22",
38
+ "@vitest/coverage-v8": "^4.1.0",
30
39
  "ts-node": "^10.9.1",
31
- "typescript": "^5.8.3"
40
+ "typescript": "^5.8.3",
41
+ "vitest": "^4.1.0"
32
42
  },
33
- "scripts": {
34
- "build": "tsc",
35
- "dev": "tsc --watch",
36
- "lint": "pnpm biome check",
37
- "lint:fix": "pnpm biome check --write"
43
+ "pnpm": {
44
+ "onlyBuiltDependencies": [
45
+ "@biomejs/biome"
46
+ ]
38
47
  }
39
- }
48
+ }