dappbooster 3.1.4 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +6 -1
- package/dist/app.js +54 -14
- package/dist/cli.js +82 -19
- package/dist/components/Multiselect/MultiSelect.d.ts +2 -1
- package/dist/components/Multiselect/MultiSelect.js +12 -7
- package/dist/components/steps/CloneRepo/CloneRepo.d.ts +2 -0
- package/dist/components/steps/CloneRepo/CloneRepo.js +8 -5
- package/dist/components/steps/Confirmation.d.ts +8 -0
- package/dist/components/steps/Confirmation.js +26 -0
- package/dist/components/steps/FileCleanup.d.ts +2 -0
- package/dist/components/steps/FileCleanup.js +7 -4
- package/dist/components/steps/Install/Install.d.ts +2 -0
- package/dist/components/steps/Install/Install.js +6 -6
- package/dist/components/steps/InstallationMode.js +4 -4
- package/dist/components/steps/OptionalPackages.d.ts +2 -0
- package/dist/components/steps/OptionalPackages.js +29 -11
- package/dist/components/steps/PostInstall.d.ts +2 -0
- package/dist/components/steps/PostInstall.js +12 -4
- package/dist/components/steps/ProjectName.d.ts +0 -7
- package/dist/components/steps/ProjectName.js +43 -17
- package/dist/components/steps/StackSelection.d.ts +8 -0
- package/dist/components/steps/StackSelection.js +21 -0
- package/dist/constants/config.d.ts +28 -4
- package/dist/constants/config.js +151 -46
- package/dist/info.d.ts +4 -1
- package/dist/info.js +36 -11
- package/dist/nonInteractive.d.ts +1 -0
- package/dist/nonInteractive.js +29 -14
- package/dist/operations/cleanupFiles.d.ts +2 -2
- package/dist/operations/cleanupFiles.js +159 -19
- package/dist/operations/cloneRepo.d.ts +2 -1
- package/dist/operations/cloneRepo.js +35 -11
- package/dist/operations/createEnvFile.d.ts +2 -1
- package/dist/operations/createEnvFile.js +9 -2
- package/dist/operations/installGuard.d.ts +8 -0
- package/dist/operations/installGuard.js +35 -0
- package/dist/operations/installPackages.d.ts +2 -2
- package/dist/operations/installPackages.js +14 -6
- package/dist/utils/utils.d.ts +7 -3
- package/dist/utils/utils.js +62 -6
- package/package.json +21 -6
- package/readme.md +141 -70
package/dist/app.d.ts
CHANGED
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
|
|
13
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 },
|
|
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 },
|
|
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
|
-
} },
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
console.log(
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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]);
|
|
@@ -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 {
|
|
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
|
-
|
|
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: '
|
|
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,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: '
|
|
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
|
|
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: '
|
|
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,
|
|
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 [
|
|
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 })),
|
|
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,
|
|
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 {
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
32
|
+
setSubmitted(selectedItems);
|
|
21
33
|
onCompletion();
|
|
22
34
|
};
|
|
23
|
-
|
|
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>;
|