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
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]), []);
21
- 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: {
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]);
38
+ const skipFeatures = setupType?.value === 'full' || setupType?.value === 'default';
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, { stack: stack, 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,38 @@
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
- --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
19
+ --mode <full|default|custom> Installation mode (default mode is Canton-only)
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
+ github GitHub issue/PR templates and workflows
29
+ precommit Pre-commit hooks (Husky, lint-staged, commitlint)
30
+ carpincho Carpincho browser-extension wallet
31
+ llm LLM and agent artifacts (.claude, AGENTS.md, …)
32
+ Canton's default mode keeps carpincho + llm and removes
33
+ github + precommit; full keeps everything; custom lets you
34
+ pick (github + precommit start unchecked).
20
35
  --non-interactive, --ni Run without prompts (auto-enabled when not a TTY)
21
- --info Output feature metadata as JSON
36
+ --info Output feature metadata as JSON (filter with --stack)
22
37
  --help Show this help
23
38
  --version Show version
24
39
 
@@ -28,24 +43,38 @@ const cli = meow(`
28
43
  Use --ni to force non-interactive mode in a TTY environment.
29
44
 
30
45
  AI agents: non-interactive mode activates automatically. Run --info
31
- to discover available features, then pass --name and --mode flags.
46
+ to discover available stacks and features (including each feature's
47
+ "requires"), then pass --canton or --evm plus --name and --mode flags.
48
+ Feature dependencies are resolved automatically, so the returned
49
+ "features" list may include extras pulled in by your selection.
32
50
  Output is JSON for easy parsing.
33
51
 
34
52
  Examples
35
- Interactive:
53
+ Interactive (prompts for stack and options):
36
54
  $ dappbooster
37
55
 
38
- Full install (non-interactive):
39
- $ dappbooster --ni --name my_dapp --mode full
56
+ Canton stack, recommended install (non-interactive):
57
+ $ dappbooster --canton --ni --name my_dapp --mode default
40
58
 
41
- Custom install with specific features:
42
- $ dappbooster --ni --name my_dapp --mode custom --features demo,subgraph
59
+ EVM stack, custom install:
60
+ $ dappbooster --evm --ni --name my_dapp --mode custom --features demo,subgraph
43
61
 
44
- Get feature metadata:
45
- $ dappbooster --info
62
+ Discover canton features:
63
+ $ dappbooster --info --stack canton
46
64
  `, {
47
65
  importMeta: import.meta,
48
66
  flags: {
67
+ stack: {
68
+ type: 'string',
69
+ },
70
+ canton: {
71
+ type: 'boolean',
72
+ default: false,
73
+ },
74
+ evm: {
75
+ type: 'boolean',
76
+ default: false,
77
+ },
49
78
  name: {
50
79
  type: 'string',
51
80
  },
@@ -69,11 +98,46 @@ const cli = meow(`
69
98
  },
70
99
  },
71
100
  });
72
- if (cli.flags.info) {
73
- console.log(getInfoOutput());
101
+ function reportFlagError(error) {
102
+ console.log(JSON.stringify({ success: false, error }, null, 2));
103
+ process.exitCode = 1;
104
+ }
105
+ function resolveStackFlag(flags) {
106
+ const explicit = [];
107
+ if (flags.canton) {
108
+ explicit.push('canton');
109
+ }
110
+ if (flags.evm) {
111
+ explicit.push('evm');
112
+ }
113
+ if (flags.stack) {
114
+ explicit.push(flags.stack);
115
+ }
116
+ const unique = Array.from(new Set(explicit));
117
+ if (unique.length > 1) {
118
+ reportFlagError(`Conflicting stack flags: ${unique.join(', ')}. Pick exactly one of --canton, --evm, or --stack.`);
119
+ return undefined;
120
+ }
121
+ const candidate = unique[0];
122
+ if (candidate === undefined) {
123
+ return undefined;
124
+ }
125
+ if (!isStackName(candidate)) {
126
+ reportFlagError(`Invalid stack: '${candidate}'. Valid stacks: ${stackNames.join(', ')}`);
127
+ return undefined;
128
+ }
129
+ return candidate;
130
+ }
131
+ const resolvedStack = resolveStackFlag(cli.flags);
132
+ if (process.exitCode === 1) {
133
+ // Stack-flag error already reported.
134
+ }
135
+ else if (cli.flags.info) {
136
+ console.log(getInfoOutput(resolvedStack));
74
137
  }
75
138
  else if (cli.flags.nonInteractive || cli.flags.ni || !process.stdout.isTTY) {
76
139
  runNonInteractive({
140
+ stack: resolvedStack,
77
141
  name: cli.flags.name,
78
142
  mode: cli.flags.mode,
79
143
  features: cli.flags.features,
@@ -91,7 +155,7 @@ else {
91
155
  console.clear();
92
156
  const { render } = await import('ink');
93
157
  const { default: App } = await import('./app.js');
94
- render(_jsx(App, {}));
158
+ render(_jsx(App, { preselectedStack: resolvedStack }));
95
159
  };
96
160
  run().catch(console.error);
97
161
  }
@@ -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 { deriveStepDisplay, getProjectFolder } from '../../utils/utils.js';
5
+ import { completeInstall } from '../../operations/installGuard.js';
6
+ import { deriveStepDisplay, getProjectFolder, resolveModeFeatures } 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([]);
@@ -14,9 +15,13 @@ const FileCleanup = ({ onCompletion, installationConfig, projectName }) => {
14
15
  setSteps((prev) => [...prev, step]);
15
16
  }, []);
16
17
  useEffect(() => {
17
- const features = selectedFeatures?.map((f) => f.value) ?? [];
18
- cleanupFiles(projectFolder, installationType ?? 'full', features, handleProgress)
18
+ const mode = installationType ?? 'full';
19
+ const selectedNames = selectedFeatures?.map((f) => f.value) ?? [];
20
+ const features = resolveModeFeatures(stack, mode, selectedNames);
21
+ cleanupFiles(stack, projectFolder, mode, features, handleProgress)
19
22
  .then(() => {
23
+ // Scaffold is complete — an interrupt from here on must not delete the finished project.
24
+ completeInstall();
20
25
  setStatus('done');
21
26
  onCompletion();
22
27
  })
@@ -24,8 +29,8 @@ const FileCleanup = ({ onCompletion, installationConfig, projectName }) => {
24
29
  setStatus('error');
25
30
  setErrorMessage(error instanceof Error ? error.message : String(error));
26
31
  });
27
- }, [projectFolder, installationType, selectedFeatures, onCompletion, handleProgress]);
32
+ }, [stack, projectFolder, installationType, selectedFeatures, onCompletion, handleProgress]);
28
33
  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] })] }));
34
+ 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
35
  };
31
36
  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>;
@@ -3,9 +3,9 @@ import { Text } from 'ink';
3
3
  import { useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import { createEnvFile } from '../../../operations/createEnvFile.js';
5
5
  import { installPackages } from '../../../operations/installPackages.js';
6
- import { deriveStepDisplay, getProjectFolder } from '../../../utils/utils.js';
6
+ import { deriveStepDisplay, getProjectFolder, resolveModeFeatures } 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([]);
@@ -18,11 +18,13 @@ const Install = ({ projectName, onCompletion, installationConfig }) => {
18
18
  setSteps((prev) => [...prev, step]);
19
19
  }, []);
20
20
  useEffect(() => {
21
- const features = selectedFeatures?.map((f) => f.value) ?? [];
21
+ const mode = installationType ?? 'full';
22
+ const selectedNames = selectedFeatures?.map((f) => f.value) ?? [];
23
+ const features = resolveModeFeatures(stack, mode, selectedNames);
22
24
  const run = async () => {
23
- handleProgress('Creating .env.local file');
24
- await createEnvFile(projectFolder);
25
- await installPackages(projectFolder, installationType ?? 'full', features, handleProgress);
25
+ handleProgress('Creating env files');
26
+ await createEnvFile(stack, projectFolder, features);
27
+ await installPackages(stack, projectFolder, mode, features, handleProgress);
26
28
  };
27
29
  run()
28
30
  .then(() => {
@@ -33,8 +35,8 @@ const Install = ({ projectName, onCompletion, installationConfig }) => {
33
35
  setStatus('error');
34
36
  setErrorMessage(error instanceof Error ? error.message : String(error));
35
37
  });
36
- }, [projectFolder, installationType, selectedFeatures, onCompletion, handleProgress]);
38
+ }, [stack, projectFolder, installationType, selectedFeatures, onCompletion, handleProgress]);
37
39
  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] })] }));
40
+ 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
41
  };
40
42
  export default Install;
@@ -1,6 +1,8 @@
1
1
  import { type FC } from 'react';
2
+ import { type Stack } from '../../constants/config.js';
2
3
  import type { InstallationSelectItem } from '../../types/types.js';
3
4
  interface Props {
5
+ stack: Stack;
4
6
  onCompletion: () => void;
5
7
  onSelect: (item: InstallationSelectItem) => void;
6
8
  }
@@ -1,26 +1,23 @@
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';
5
5
  import { useState } from 'react';
6
+ import { getInstallationModes } from '../../constants/config.js';
6
7
  import Divider from '../Divider.js';
7
- const installationTypeItems = [
8
- {
9
- label: 'Full',
10
- value: 'full',
11
- },
12
- {
13
- label: 'Custom',
14
- value: 'custom',
15
- },
16
- ];
17
- const InstallationMode = ({ onCompletion, onSelect }) => {
18
- const [isFocused, setIsFocused] = useState(true);
8
+ const MODE_LABELS = {
9
+ default: 'Default (recommended)',
10
+ full: 'Full',
11
+ custom: 'Custom',
12
+ };
13
+ const InstallationMode = ({ stack, onCompletion, onSelect }) => {
14
+ const [selected, setSelected] = useState();
15
+ const installationTypeItems = getInstallationModes(stack).map((value) => ({ label: MODE_LABELS[value], value }));
19
16
  const handleSelect = (item) => {
20
17
  onSelect(item);
18
+ setSelected(item);
21
19
  onCompletion();
22
- setIsFocused(false);
23
20
  };
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 })] }));
21
+ 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
22
  };
26
23
  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,31 @@
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
+ // Pre-check only default:true features (e.g. Canton's github/precommit start unchecked).
17
+ const defaultSelected = useMemo(() => {
18
+ const features = getStackConfig(stack).features;
19
+ return customPackages.filter((pkg) => features[pkg.value]?.default);
20
+ }, [stack, customPackages]);
21
+ // Keep the selection dependency-consistent as the user toggles (resolves any feature `requires`).
22
+ const transformSelection = useCallback((nextSelected, toggledItem, action) => {
23
+ const selectedValues = nextSelected.map((item) => item.value);
24
+ const resolved = applyFeatureToggle(stack, selectedValues, toggledItem.value, action);
25
+ return resolved
26
+ .map((value) => customPackages.find((pkg) => pkg.value === value))
27
+ .filter((pkg) => pkg !== undefined);
28
+ }, [stack, customPackages]);
12
29
  // biome-ignore lint/correctness/useExhaustiveDependencies: Run this only once, no matter what
13
30
  useEffect(() => {
14
31
  if (skip) {
@@ -17,9 +34,15 @@ const OptionalPackages = ({ onCompletion, onSubmit, skip = false }) => {
17
34
  }, []);
18
35
  const onHandleSubmit = (selectedItems) => {
19
36
  onSubmit(selectedItems);
20
- setIsFocused(false);
37
+ setSubmitted(selectedItems);
21
38
  onCompletion();
22
39
  };
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 })] }));
40
+ if (skip) {
41
+ return null;
42
+ }
43
+ if (submitted) {
44
+ return (_jsxs(Text, { children: ["Optional packages:", ' ', _jsx(Text, { bold: true, color: 'green', children: submitted.length > 0 ? submitted.map((item) => item.label).join(', ') : 'none' })] }));
45
+ }
46
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: 'whiteBright', children: "Choose optional packages" }), _jsx(MultiSelect, { defaultSelected: defaultSelected, focus: true, items: customPackages, onSubmit: onHandleSubmit, transformSelection: transformSelection })] }));
24
47
  };
25
48
  export default OptionalPackages;