dappbooster 3.1.5 → 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 +1 -6
  42. package/readme.md +139 -77
package/dist/app.d.ts CHANGED
@@ -1,2 +1,7 @@
1
- declare const App: () => import("react/jsx-runtime").JSX.Element;
1
+ import { type FC } from 'react';
2
+ import type { Stack } from './constants/config.js';
3
+ interface Props {
4
+ preselectedStack?: Stack;
5
+ }
6
+ declare const App: FC<Props>;
2
7
  export default App;
package/dist/app.js CHANGED
@@ -3,47 +3,87 @@ import { Box } from 'ink';
3
3
  import { useCallback, useMemo, useState } from 'react';
4
4
  import MainTitle from './components/MainTitle.js';
5
5
  import CloneRepo from './components/steps/CloneRepo/CloneRepo.js';
6
+ import Confirmation from './components/steps/Confirmation.js';
6
7
  import FileCleanup from './components/steps/FileCleanup.js';
7
8
  import Install from './components/steps/Install/Install.js';
8
9
  import InstallationMode from './components/steps/InstallationMode.js';
9
10
  import OptionalPackages from './components/steps/OptionalPackages.js';
10
11
  import PostInstall from './components/steps/PostInstall.js';
11
12
  import ProjectName from './components/steps/ProjectName.js';
12
- import { canShowStep } from './utils/utils.js';
13
- const App = () => {
13
+ import StackSelection from './components/steps/StackSelection.js';
14
+ import { canShowStep, describeInstallPlan } from './utils/utils.js';
15
+ const App = ({ preselectedStack }) => {
16
+ const [stack, setStack] = useState(preselectedStack);
14
17
  const [projectName, setProjectName] = useState('');
15
18
  const [currentStep, setCurrentStep] = useState(1);
16
19
  const [setupType, setSetupType] = useState();
17
20
  const [selectedFeatures, setSelectedFeatures] = useState();
21
+ // Bumped when the user cancels at the confirmation step; re-keys every step so they re-mount
22
+ // fresh for a clean re-run of the wizard.
23
+ const [attempt, setAttempt] = useState(0);
18
24
  const finishStep = useCallback(() => setCurrentStep((prevStep) => prevStep + 1), []);
25
+ const onSelectStack = useCallback((value) => setStack(value), []);
19
26
  const onSelectSetupType = useCallback((item) => setSetupType(item), []);
20
27
  const onSelectSelectedFeatures = useCallback((selectedItems) => setSelectedFeatures([...selectedItems]), []);
28
+ // Confirmation "No": discard the answers and return to the first question. No disk work has
29
+ // happened yet, so this is a clean restart.
30
+ const restart = useCallback(() => {
31
+ setProjectName('');
32
+ setSetupType(undefined);
33
+ setSelectedFeatures(undefined);
34
+ setStack(preselectedStack);
35
+ setCurrentStep(1);
36
+ setAttempt((prev) => prev + 1);
37
+ }, [preselectedStack]);
21
38
  const skipFeatures = setupType?.value === 'full';
22
- const steps = useMemo(() => [
23
- _jsx(ProjectName, { onCompletion: finishStep, onSubmit: setProjectName, projectName: projectName }, 1),
24
- _jsx(CloneRepo, { onCompletion: finishStep, projectName: projectName }, 2),
25
- _jsx(InstallationMode, { onCompletion: finishStep, onSelect: onSelectSetupType }, 3),
26
- _jsx(OptionalPackages, { onCompletion: finishStep, onSubmit: onSelectSelectedFeatures, skip: skipFeatures }, 4),
27
- _jsx(Install, { installationConfig: {
39
+ const mode = setupType?.value ?? 'full';
40
+ const planFeatures = selectedFeatures?.map((item) => item.value) ?? [];
41
+ const planSummary = stack === undefined ? '' : describeInstallPlan(stack, projectName, mode, planFeatures);
42
+ const steps = useMemo(() => {
43
+ // Questions first (no disk writes), operations last. This way an interrupt while answering
44
+ // leaves nothing behind, and all cloning/installing happens only after the confirmation.
45
+ const orderedSteps = [
46
+ _jsx(ProjectName, { onCompletion: finishStep, onSubmit: setProjectName }, `project-name-${attempt}`),
47
+ ];
48
+ if (!preselectedStack) {
49
+ orderedSteps.push(_jsx(StackSelection, { onCompletion: finishStep, onSelect: onSelectStack }, `stack-selection-${attempt}`));
50
+ }
51
+ if (stack === undefined) {
52
+ return orderedSteps;
53
+ }
54
+ // --- remaining questions (need the stack) ---
55
+ orderedSteps.push(_jsx(InstallationMode, { onCompletion: finishStep, onSelect: onSelectSetupType }, `installation-mode-${attempt}`));
56
+ orderedSteps.push(_jsx(OptionalPackages, { stack: stack, onCompletion: finishStep, onSubmit: onSelectSelectedFeatures, skip: skipFeatures }, `optional-packages-${attempt}`));
57
+ orderedSteps.push(_jsx(Confirmation, { summary: planSummary, onConfirm: finishStep, onCancel: restart }, `confirmation-${attempt}`));
58
+ // --- operations (disk writes) ---
59
+ orderedSteps.push(_jsx(CloneRepo, { stack: stack, onCompletion: finishStep, projectName: projectName }, `clone-repo-${attempt}`));
60
+ orderedSteps.push(_jsx(Install, { stack: stack, installationConfig: {
28
61
  installationType: setupType?.value,
29
62
  selectedFeatures: selectedFeatures,
30
- }, onCompletion: finishStep, projectName: projectName }, 5),
31
- _jsx(FileCleanup, { installationConfig: {
63
+ }, onCompletion: finishStep, projectName: projectName }, `install-${attempt}`));
64
+ orderedSteps.push(_jsx(FileCleanup, { stack: stack, installationConfig: {
32
65
  installationType: setupType?.value,
33
66
  selectedFeatures: selectedFeatures,
34
- }, onCompletion: finishStep, projectName: projectName }, 6),
35
- _jsx(PostInstall, { projectName: projectName, installationConfig: {
67
+ }, onCompletion: finishStep, projectName: projectName }, `file-cleanup-${attempt}`));
68
+ orderedSteps.push(_jsx(PostInstall, { stack: stack, projectName: projectName, installationConfig: {
36
69
  installationType: setupType?.value,
37
70
  selectedFeatures: selectedFeatures,
38
- } }, 7),
39
- ], [
71
+ } }, `post-install-${attempt}`));
72
+ return orderedSteps;
73
+ }, [
40
74
  finishStep,
75
+ onSelectStack,
41
76
  onSelectSelectedFeatures,
42
77
  setupType?.value,
43
78
  selectedFeatures,
44
79
  onSelectSetupType,
45
80
  projectName,
46
81
  skipFeatures,
82
+ stack,
83
+ preselectedStack,
84
+ attempt,
85
+ planSummary,
86
+ restart,
47
87
  ]);
48
88
  return (_jsxs(Box, { flexDirection: 'column', rowGap: 1, width: 80, children: [_jsx(MainTitle, {}), steps.map((item, index) => canShowStep(currentStep, index + 1) && item)] }));
49
89
  };
package/dist/cli.js CHANGED
@@ -2,23 +2,37 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import process from 'node:process';
4
4
  import meow from 'meow';
5
+ import { isStackName, stackNames } from './constants/config.js';
5
6
  import { getInfoOutput } from './info.js';
6
7
  import { runNonInteractive } from './nonInteractive.js';
7
8
  const cli = meow(`
8
9
  Usage
9
10
  $ dappbooster [options]
10
11
 
11
- Options
12
+ Stack selection (mutually exclusive)
13
+ --canton Use the Canton stack (Daml, Carpincho wallet, off-chain services)
14
+ --evm Use the EVM stack (Ethereum, Polygon, Base, …) [default]
15
+ --stack <evm|canton> Explicit stack name (alternative to --canton/--evm)
16
+
17
+ Common options
12
18
  --name <string> Project name (alphanumeric, underscores)
13
19
  --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
+ --features <list> Comma-separated features (with --mode=custom)
21
+ EVM:
22
+ demo Component demos and example pages
23
+ subgraph TheGraph subgraph integration
24
+ typedoc TypeDoc API documentation
25
+ vocs Vocs documentation site
26
+ husky Git hooks (Husky, lint-staged, commitlint)
27
+ Canton:
28
+ counter Counter demo dapp
29
+ e2e Playwright end-to-end tests (requires counter)
30
+ carpincho Carpincho browser-extension wallet
31
+ llm LLM and agent artifacts (.claude, AGENTS.md, …)
32
+ Dependencies are auto-resolved: requesting e2e
33
+ also pulls in counter.
20
34
  --non-interactive, --ni Run without prompts (auto-enabled when not a TTY)
21
- --info Output feature metadata as JSON
35
+ --info Output feature metadata as JSON (filter with --stack)
22
36
  --help Show this help
23
37
  --version Show version
24
38
 
@@ -28,24 +42,38 @@ const cli = meow(`
28
42
  Use --ni to force non-interactive mode in a TTY environment.
29
43
 
30
44
  AI agents: non-interactive mode activates automatically. Run --info
31
- to discover available features, then pass --name and --mode flags.
45
+ to discover available stacks and features (including each feature's
46
+ "requires"), then pass --canton or --evm plus --name and --mode flags.
47
+ Feature dependencies are resolved automatically, so the returned
48
+ "features" list may include extras pulled in by your selection.
32
49
  Output is JSON for easy parsing.
33
50
 
34
51
  Examples
35
- Interactive:
52
+ Interactive (prompts for stack and options):
36
53
  $ dappbooster
37
54
 
38
- Full install (non-interactive):
39
- $ dappbooster --ni --name my_dapp --mode full
55
+ Canton stack, full install (non-interactive):
56
+ $ dappbooster --canton --ni --name my_dapp --mode full
40
57
 
41
- Custom install with specific features:
42
- $ dappbooster --ni --name my_dapp --mode custom --features demo,subgraph
58
+ EVM stack, custom install:
59
+ $ dappbooster --evm --ni --name my_dapp --mode custom --features demo,subgraph
43
60
 
44
- Get feature metadata:
45
- $ dappbooster --info
61
+ Discover canton features:
62
+ $ dappbooster --info --stack canton
46
63
  `, {
47
64
  importMeta: import.meta,
48
65
  flags: {
66
+ stack: {
67
+ type: 'string',
68
+ },
69
+ canton: {
70
+ type: 'boolean',
71
+ default: false,
72
+ },
73
+ evm: {
74
+ type: 'boolean',
75
+ default: false,
76
+ },
49
77
  name: {
50
78
  type: 'string',
51
79
  },
@@ -69,11 +97,46 @@ const cli = meow(`
69
97
  },
70
98
  },
71
99
  });
72
- if (cli.flags.info) {
73
- console.log(getInfoOutput());
100
+ function reportFlagError(error) {
101
+ console.log(JSON.stringify({ success: false, error }, null, 2));
102
+ process.exitCode = 1;
103
+ }
104
+ function resolveStackFlag(flags) {
105
+ const explicit = [];
106
+ if (flags.canton) {
107
+ explicit.push('canton');
108
+ }
109
+ if (flags.evm) {
110
+ explicit.push('evm');
111
+ }
112
+ if (flags.stack) {
113
+ explicit.push(flags.stack);
114
+ }
115
+ const unique = Array.from(new Set(explicit));
116
+ if (unique.length > 1) {
117
+ reportFlagError(`Conflicting stack flags: ${unique.join(', ')}. Pick exactly one of --canton, --evm, or --stack.`);
118
+ return undefined;
119
+ }
120
+ const candidate = unique[0];
121
+ if (candidate === undefined) {
122
+ return undefined;
123
+ }
124
+ if (!isStackName(candidate)) {
125
+ reportFlagError(`Invalid stack: '${candidate}'. Valid stacks: ${stackNames.join(', ')}`);
126
+ return undefined;
127
+ }
128
+ return candidate;
129
+ }
130
+ const resolvedStack = resolveStackFlag(cli.flags);
131
+ if (process.exitCode === 1) {
132
+ // Stack-flag error already reported.
133
+ }
134
+ else if (cli.flags.info) {
135
+ console.log(getInfoOutput(resolvedStack));
74
136
  }
75
137
  else if (cli.flags.nonInteractive || cli.flags.ni || !process.stdout.isTTY) {
76
138
  runNonInteractive({
139
+ stack: resolvedStack,
77
140
  name: cli.flags.name,
78
141
  mode: cli.flags.mode,
79
142
  features: cli.flags.features,
@@ -91,7 +154,7 @@ else {
91
154
  console.clear();
92
155
  const { render } = await import('ink');
93
156
  const { default: App } = await import('./app.js');
94
- render(_jsx(App, {}));
157
+ render(_jsx(App, { preselectedStack: resolvedStack }));
95
158
  };
96
159
  run().catch(console.error);
97
160
  }
@@ -26,7 +26,8 @@ type MultiSelectProps<T> = {
26
26
  onUnselect?: (unselectedItem: Item<T>) => void;
27
27
  onSubmit?: (selectedItems: Item<T>[]) => void;
28
28
  onHighlight?: (highlightedItem: Item<T>) => void;
29
+ transformSelection?: (nextSelected: Item<T>[], toggledItem: Item<T>, action: 'select' | 'unselect') => Item<T>[];
29
30
  };
30
- declare const MultiSelect: <T>({ items, defaultSelected, focus, initialIndex, indicatorComponent, checkboxComponent, itemComponent, limit, onSelect, onUnselect, onSubmit, onHighlight, }: MultiSelectProps<T>) => import("react/jsx-runtime").JSX.Element;
31
+ declare const MultiSelect: <T>({ items, defaultSelected, focus, initialIndex, indicatorComponent, checkboxComponent, itemComponent, limit, onSelect, onUnselect, onSubmit, onHighlight, transformSelection, }: MultiSelectProps<T>) => import("react/jsx-runtime").JSX.Element;
31
32
  export default MultiSelect;
32
33
  export { Indicator, ItemComponent, CheckBox, type Item };
@@ -4,7 +4,7 @@ import { createElement, useCallback, useState } from 'react';
4
4
  import CheckBox from './components/Checkbox.js';
5
5
  import Indicator from './components/Indicator.js';
6
6
  import ItemComponent from './components/Item.js';
7
- const MultiSelect = ({ items = [], defaultSelected = [], focus = true, initialIndex = 0, indicatorComponent = Indicator, checkboxComponent = CheckBox, itemComponent = ItemComponent, limit = null, onSelect = () => { }, onUnselect = () => { }, onSubmit = () => { }, onHighlight = () => { }, }) => {
7
+ const MultiSelect = ({ items = [], defaultSelected = [], focus = true, initialIndex = 0, indicatorComponent = Indicator, checkboxComponent = CheckBox, itemComponent = ItemComponent, limit = null, onSelect = () => { }, onUnselect = () => { }, onSubmit = () => { }, onHighlight = () => { }, transformSelection, }) => {
8
8
  const [highlightedIndex, setHighlightedIndex] = useState(initialIndex);
9
9
  const [selectedItems, setSelectedItems] = useState(defaultSelected);
10
10
  const hasLimit = limit !== null && limit < items.length;
@@ -13,17 +13,22 @@ const MultiSelect = ({ items = [], defaultSelected = [], focus = true, initialIn
13
13
  return (selectedItems.filter((selectedItem) => selectedItem.value === item.value && selectedItem.label === item.label).length > 0);
14
14
  }, []);
15
15
  const handleSelect = useCallback((item) => {
16
- if (includesItems(item, selectedItems)) {
17
- const newSelectedItems = selectedItems.filter((selectedItem) => selectedItem.value !== item.value && selectedItem.label !== item.label);
18
- setSelectedItems(newSelectedItems);
16
+ const isCurrentlySelected = includesItems(item, selectedItems);
17
+ const action = isCurrentlySelected ? 'unselect' : 'select';
18
+ const naiveSelection = isCurrentlySelected
19
+ ? selectedItems.filter((selectedItem) => selectedItem.value !== item.value && selectedItem.label !== item.label)
20
+ : [...selectedItems, item];
21
+ const nextSelection = transformSelection
22
+ ? transformSelection(naiveSelection, item, action)
23
+ : naiveSelection;
24
+ setSelectedItems(nextSelection);
25
+ if (isCurrentlySelected) {
19
26
  onUnselect(item);
20
27
  }
21
28
  else {
22
- const newSelectedItems = [...selectedItems, item];
23
- setSelectedItems(newSelectedItems);
24
29
  onSelect(item);
25
30
  }
26
- }, [selectedItems, onSelect, onUnselect, includesItems]);
31
+ }, [selectedItems, onSelect, onUnselect, includesItems, transformSelection]);
27
32
  const handleSubmit = useCallback(() => {
28
33
  onSubmit(selectedItems);
29
34
  }, [selectedItems, onSubmit]);
@@ -1,5 +1,7 @@
1
1
  import { type FC } from 'react';
2
+ import type { Stack } from '../../../constants/config.js';
2
3
  interface Props {
4
+ stack: Stack;
3
5
  projectName: string;
4
6
  onCompletion: () => void;
5
7
  }
@@ -2,9 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Text } from 'ink';
3
3
  import { useCallback, useEffect, useState } from 'react';
4
4
  import { cloneRepo } from '../../../operations/index.js';
5
- import { deriveStepDisplay } from '../../../utils/utils.js';
5
+ import { beginInstall } from '../../../operations/installGuard.js';
6
+ import { deriveStepDisplay, getProjectFolder } from '../../../utils/utils.js';
6
7
  import Divider from '../../Divider.js';
7
- const CloneRepo = ({ projectName, onCompletion }) => {
8
+ const CloneRepo = ({ stack, projectName, onCompletion }) => {
8
9
  const [steps, setSteps] = useState([]);
9
10
  const [status, setStatus] = useState('running');
10
11
  const [errorMessage, setErrorMessage] = useState('');
@@ -12,7 +13,9 @@ const CloneRepo = ({ projectName, onCompletion }) => {
12
13
  setSteps((prev) => [...prev, step]);
13
14
  }, []);
14
15
  useEffect(() => {
15
- cloneRepo(projectName, handleProgress)
16
+ // Disk work starts here, so arm the interrupt guard before cloning.
17
+ beginInstall(getProjectFolder(projectName));
18
+ cloneRepo(stack, projectName, handleProgress)
16
19
  .then(() => {
17
20
  setStatus('done');
18
21
  onCompletion();
@@ -21,8 +24,8 @@ const CloneRepo = ({ projectName, onCompletion }) => {
21
24
  setStatus('error');
22
25
  setErrorMessage(error instanceof Error ? error.message : String(error));
23
26
  });
24
- }, [projectName, onCompletion, handleProgress]);
27
+ }, [stack, projectName, onCompletion, handleProgress]);
25
28
  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] })] }));
29
+ return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: 'Git tasks' }), completedSteps.map((step) => (_jsxs(Text, { children: [_jsx(Text, { color: 'green', children: '' }), " ", step] }, step))), currentStep && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: '' }), " ", currentStep, " ", _jsx(Text, { dimColor: true, children: "Working..." })] })), failedStep && (_jsxs(Text, { children: [_jsx(Text, { color: 'red', children: '' }), " ", failedStep, " ", _jsx(Text, { color: 'red', children: "Error" })] })), status === 'error' && _jsxs(Text, { color: 'red', children: ["Failed to clone: ", errorMessage] })] }));
27
30
  };
28
31
  export default CloneRepo;
@@ -0,0 +1,8 @@
1
+ import { type FC } from 'react';
2
+ interface Props {
3
+ summary: string;
4
+ onConfirm: () => void;
5
+ onCancel: () => void;
6
+ }
7
+ declare const Confirmation: FC<Props>;
8
+ export default Confirmation;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import figures from 'figures';
3
+ import { Text } from 'ink';
4
+ import SelectInput from 'ink-select-input';
5
+ import { useState } from 'react';
6
+ import Divider from '../Divider.js';
7
+ const confirmItems = [
8
+ { label: 'Yes, scaffold it', value: 'yes' },
9
+ { label: 'No, start over', value: 'no' },
10
+ ];
11
+ // Last side-effect-free step: nothing has touched the disk yet. Confirming starts the operations;
12
+ // cancelling loops back to re-answer the questions.
13
+ const Confirmation = ({ summary, onConfirm, onCancel }) => {
14
+ const [confirmed, setConfirmed] = useState(false);
15
+ const handleSelect = (item) => {
16
+ if (item.value === 'yes') {
17
+ setConfirmed(true);
18
+ onConfirm();
19
+ }
20
+ else {
21
+ onCancel();
22
+ }
23
+ };
24
+ return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: 'Review' }), _jsx(Text, { children: summary }), confirmed ? (_jsxs(Text, { children: [_jsx(Text, { color: 'green', children: figures.tick }), " Scaffolding\u2026"] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: 'whiteBright', children: "Proceed with these settings?" }), _jsx(SelectInput, { indicatorComponent: ({ isSelected }) => (_jsx(Text, { color: "green", children: isSelected ? `${figures.pointer} ` : ' ' })), itemComponent: ({ label, isSelected }) => (_jsx(Text, { color: isSelected ? 'green' : 'white', bold: isSelected, children: label })), items: confirmItems, onSelect: handleSelect })] }))] }));
25
+ };
26
+ export default Confirmation;
@@ -1,6 +1,8 @@
1
1
  import { type FC } from 'react';
2
+ import type { Stack } from '../../constants/config.js';
2
3
  import type { InstallationType, MultiSelectItem } from '../../types/types.js';
3
4
  interface Props {
5
+ stack: Stack;
4
6
  onCompletion: () => void;
5
7
  projectName: string;
6
8
  installationConfig: {
@@ -2,9 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Text } from 'ink';
3
3
  import { useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import { cleanupFiles } from '../../operations/index.js';
5
+ import { completeInstall } from '../../operations/installGuard.js';
5
6
  import { deriveStepDisplay, getProjectFolder } from '../../utils/utils.js';
6
7
  import Divider from '../Divider.js';
7
- const FileCleanup = ({ onCompletion, installationConfig, projectName }) => {
8
+ const FileCleanup = ({ stack, onCompletion, installationConfig, projectName }) => {
8
9
  const { installationType, selectedFeatures } = installationConfig;
9
10
  const projectFolder = useMemo(() => getProjectFolder(projectName), [projectName]);
10
11
  const [steps, setSteps] = useState([]);
@@ -15,8 +16,10 @@ const FileCleanup = ({ onCompletion, installationConfig, projectName }) => {
15
16
  }, []);
16
17
  useEffect(() => {
17
18
  const features = selectedFeatures?.map((f) => f.value) ?? [];
18
- cleanupFiles(projectFolder, installationType ?? 'full', features, handleProgress)
19
+ cleanupFiles(stack, projectFolder, installationType ?? 'full', features, handleProgress)
19
20
  .then(() => {
21
+ // Scaffold is complete — an interrupt from here on must not delete the finished project.
22
+ completeInstall();
20
23
  setStatus('done');
21
24
  onCompletion();
22
25
  })
@@ -24,8 +27,8 @@ const FileCleanup = ({ onCompletion, installationConfig, projectName }) => {
24
27
  setStatus('error');
25
28
  setErrorMessage(error instanceof Error ? error.message : String(error));
26
29
  });
27
- }, [projectFolder, installationType, selectedFeatures, onCompletion, handleProgress]);
30
+ }, [stack, projectFolder, installationType, selectedFeatures, onCompletion, handleProgress]);
28
31
  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] })] }));
32
+ return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: 'File cleanup' }), completedSteps.map((step) => (_jsxs(Text, { children: [_jsx(Text, { color: 'green', children: '' }), " ", step] }, step))), currentStep && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: '' }), " ", currentStep, " ", _jsx(Text, { dimColor: true, children: "Working..." })] })), failedStep && (_jsxs(Text, { children: [_jsx(Text, { color: 'red', children: '' }), " ", failedStep, " ", _jsx(Text, { color: 'red', children: "Error" })] })), status === 'error' && _jsxs(Text, { color: 'red', children: ["Cleanup failed: ", errorMessage] })] }));
30
33
  };
31
34
  export default FileCleanup;
@@ -1,6 +1,8 @@
1
1
  import { type FC } from 'react';
2
+ import type { Stack } from '../../../constants/config.js';
2
3
  import type { InstallationType, MultiSelectItem } from '../../../types/types.js';
3
4
  interface Props {
5
+ stack: Stack;
4
6
  installationConfig: {
5
7
  installationType: InstallationType | undefined;
6
8
  selectedFeatures?: Array<MultiSelectItem>;
@@ -5,7 +5,7 @@ import { createEnvFile } from '../../../operations/createEnvFile.js';
5
5
  import { installPackages } from '../../../operations/installPackages.js';
6
6
  import { deriveStepDisplay, getProjectFolder } from '../../../utils/utils.js';
7
7
  import Divider from '../../Divider.js';
8
- const Install = ({ projectName, onCompletion, installationConfig }) => {
8
+ const Install = ({ stack, projectName, onCompletion, installationConfig }) => {
9
9
  const { installationType, selectedFeatures } = installationConfig;
10
10
  const projectFolder = useMemo(() => getProjectFolder(projectName), [projectName]);
11
11
  const [steps, setSteps] = useState([]);
@@ -20,9 +20,9 @@ const Install = ({ projectName, onCompletion, installationConfig }) => {
20
20
  useEffect(() => {
21
21
  const features = selectedFeatures?.map((f) => f.value) ?? [];
22
22
  const run = async () => {
23
- handleProgress('Creating .env.local file');
24
- await createEnvFile(projectFolder);
25
- await installPackages(projectFolder, installationType ?? 'full', features, handleProgress);
23
+ handleProgress('Creating env files');
24
+ await createEnvFile(stack, projectFolder, features);
25
+ await installPackages(stack, projectFolder, installationType ?? 'full', features, handleProgress);
26
26
  };
27
27
  run()
28
28
  .then(() => {
@@ -33,8 +33,8 @@ const Install = ({ projectName, onCompletion, installationConfig }) => {
33
33
  setStatus('error');
34
34
  setErrorMessage(error instanceof Error ? error.message : String(error));
35
35
  });
36
- }, [projectFolder, installationType, selectedFeatures, onCompletion, handleProgress]);
36
+ }, [stack, projectFolder, installationType, selectedFeatures, onCompletion, handleProgress]);
37
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] })] }));
38
+ return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: `${title} installation` }), completedSteps.map((step) => (_jsxs(Text, { children: [_jsx(Text, { color: 'green', children: '' }), " ", step] }, step))), currentStep && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: '' }), " ", currentStep, " ", _jsx(Text, { dimColor: true, children: "Working..." })] })), failedStep && (_jsxs(Text, { children: [_jsx(Text, { color: 'red', children: '' }), " ", failedStep, " ", _jsx(Text, { color: 'red', children: "Error" })] })), status === 'error' && _jsxs(Text, { color: 'red', children: ["Installation failed: ", errorMessage] })] }));
39
39
  };
40
40
  export default Install;
@@ -1,4 +1,4 @@
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
2
  import figures from 'figures';
3
3
  import { Text } from 'ink';
4
4
  import SelectInput from 'ink-select-input';
@@ -15,12 +15,12 @@ const installationTypeItems = [
15
15
  },
16
16
  ];
17
17
  const InstallationMode = ({ onCompletion, onSelect }) => {
18
- const [isFocused, setIsFocused] = useState(true);
18
+ const [selected, setSelected] = useState();
19
19
  const handleSelect = (item) => {
20
20
  onSelect(item);
21
+ setSelected(item);
21
22
  onCompletion();
22
- setIsFocused(false);
23
23
  };
24
- return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: 'Installation setup' }), _jsx(Text, { color: 'whiteBright', children: "Choose installation type" }), _jsx(SelectInput, { indicatorComponent: ({ isSelected }) => (_jsx(Text, { color: "green", children: isSelected ? `${figures.pointer} ` : ' ' })), itemComponent: ({ label, isSelected }) => (_jsx(Text, { color: isSelected ? 'green' : 'white', bold: isSelected, children: label })), isFocused: isFocused, items: installationTypeItems, onSelect: handleSelect })] }));
24
+ return (_jsxs(_Fragment, { children: [_jsx(Divider, { title: 'Installation setup' }), selected ? (_jsxs(Text, { children: ["Installation type:", ' ', _jsx(Text, { bold: true, color: 'green', children: selected.label })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: 'whiteBright', children: "Choose installation type" }), _jsx(SelectInput, { indicatorComponent: ({ isSelected }) => (_jsx(Text, { color: "green", children: isSelected ? `${figures.pointer} ` : ' ' })), itemComponent: ({ label, isSelected }) => (_jsx(Text, { color: isSelected ? 'green' : 'white', bold: isSelected, children: label })), items: installationTypeItems, onSelect: handleSelect })] }))] }));
25
25
  };
26
26
  export default InstallationMode;
@@ -1,6 +1,8 @@
1
1
  import { type FC } from 'react';
2
+ import { type Stack } from '../../constants/config.js';
2
3
  import type { MultiSelectItem } from '../../types/types.js';
3
4
  interface Props {
5
+ stack: Stack;
4
6
  onCompletion: () => void;
5
7
  onSubmit: (selectedItems: Array<MultiSelectItem>) => void;
6
8
  skip?: boolean;
@@ -1,14 +1,26 @@
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
2
  import { Text } from 'ink';
3
- import { useEffect, useState } from 'react';
4
- import { featureDefinitions, featureNames } from '../../constants/config.js';
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { getStackConfig } from '../../constants/config.js';
5
+ import { applyFeatureToggle } from '../../utils/utils.js';
5
6
  import MultiSelect from '../Multiselect/index.js';
6
- const customPackages = featureNames.map((name) => ({
7
- label: featureDefinitions[name].label,
8
- value: name,
9
- }));
10
- const OptionalPackages = ({ onCompletion, onSubmit, skip = false }) => {
11
- const [isFocused, setIsFocused] = useState(true);
7
+ const OptionalPackages = ({ stack, onCompletion, onSubmit, skip = false }) => {
8
+ const [submitted, setSubmitted] = useState();
9
+ const customPackages = useMemo(() => {
10
+ const features = getStackConfig(stack).features;
11
+ return Object.entries(features).map(([name, def]) => ({
12
+ label: def.label,
13
+ value: name,
14
+ }));
15
+ }, [stack]);
16
+ // Keep the selection dependency-consistent as the user toggles (e.g. e2e requires counter).
17
+ const transformSelection = useCallback((nextSelected, toggledItem, action) => {
18
+ const selectedValues = nextSelected.map((item) => item.value);
19
+ const resolved = applyFeatureToggle(stack, selectedValues, toggledItem.value, action);
20
+ return resolved
21
+ .map((value) => customPackages.find((pkg) => pkg.value === value))
22
+ .filter((pkg) => pkg !== undefined);
23
+ }, [stack, customPackages]);
12
24
  // biome-ignore lint/correctness/useExhaustiveDependencies: Run this only once, no matter what
13
25
  useEffect(() => {
14
26
  if (skip) {
@@ -17,9 +29,15 @@ const OptionalPackages = ({ onCompletion, onSubmit, skip = false }) => {
17
29
  }, []);
18
30
  const onHandleSubmit = (selectedItems) => {
19
31
  onSubmit(selectedItems);
20
- setIsFocused(false);
32
+ setSubmitted(selectedItems);
21
33
  onCompletion();
22
34
  };
23
- return skip ? null : (_jsxs(_Fragment, { children: [_jsx(Text, { color: 'whiteBright', children: "Choose optional packages" }), _jsx(MultiSelect, { defaultSelected: customPackages, focus: isFocused, items: customPackages, onSubmit: onHandleSubmit })] }));
35
+ if (skip) {
36
+ return null;
37
+ }
38
+ if (submitted) {
39
+ return (_jsxs(Text, { children: ["Optional packages:", ' ', _jsx(Text, { bold: true, color: 'green', children: submitted.length > 0 ? submitted.map((item) => item.label).join(', ') : 'none' })] }));
40
+ }
41
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: 'whiteBright', children: "Choose optional packages" }), _jsx(MultiSelect, { defaultSelected: customPackages, focus: true, items: customPackages, onSubmit: onHandleSubmit, transformSelection: transformSelection })] }));
24
42
  };
25
43
  export default OptionalPackages;
@@ -1,6 +1,8 @@
1
1
  import type { FC } from 'react';
2
+ import { type Stack } from '../../constants/config.js';
2
3
  import type { InstallationType, MultiSelectItem } from '../../types/types.js';
3
4
  interface Props {
5
+ stack: Stack;
4
6
  installationConfig: {
5
7
  installationType: InstallationType | undefined;
6
8
  selectedFeatures?: Array<MultiSelectItem>;