boilercraft 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/bin/boilercraft.js +34 -4
- package/cli/app.js +144 -27
- package/cli/ui.js +67 -1
- package/package.json +17 -5
- package/public/index.html +25 -1
- package/server.js +3 -1
- package/services/engine/extras.js +354 -0
- package/services/engine/postCreate.js +30 -0
- package/services/engine/scaffoldEngine.js +34 -8
- package/services/engine/versionCatalog.js +16 -0
- package/services/generate.js +2 -2
- package/services/presets.js +87 -0
- package/services/updateNotifier.js +50 -0
- package/stacks/django/overlays/common/templates/home.html +20 -0
- package/stacks/django/snippets/settings.py +27 -0
- package/stacks/django/stack.json +42 -0
- package/stacks/dotnet-core/stack.json +6 -0
- package/stacks/fastapi/overlays/common/app/__init__.py +0 -0
- package/stacks/fastapi/overlays/common/app/main.py +22 -0
- package/stacks/fastapi/overlays/common/app/static/index.html +20 -0
- package/stacks/fastapi/overlays/db/app/db.py +30 -0
- package/stacks/fastapi/stack.json +40 -0
- package/stacks/go-gin/overlays/common/main.go +32 -0
- package/stacks/go-gin/overlays/common/public/index.html +20 -0
- package/stacks/go-gin/stack.json +28 -0
- package/stacks/laravel/stack.json +12 -0
- package/stacks/nestjs/stack.json +46 -0
- package/stacks/nextjs/stack.json +12 -0
- package/stacks/node-express/stack.json +11 -0
- package/stacks/raw-php/stack.json +25 -0
- package/stacks/vite/stack.json +61 -0
- package/templates/raw-php-mysql/config/Database.php +1 -0
- package/docs/STACK_ENGINE.md +0 -93
- package/docs/assets/cli-create.svg +0 -52
- package/docs/assets/cli-menu.svg +0 -47
- package/docs/stacks/dotnet-core.md +0 -69
- package/docs/stacks/laravel-vue.md +0 -46
- package/docs/stacks/nextjs.md +0 -58
- package/docs/stacks/node-express.md +0 -58
- package/docs/stacks/raw-php.md +0 -130
package/README.md
CHANGED
|
@@ -27,8 +27,8 @@ npx boilercraft
|
|
|
27
27
|
|
|
28
28
|
Most boilerplates are a frozen copy of someone's project. They go stale the day a new framework version ships. BoilerCraft works differently:
|
|
29
29
|
|
|
30
|
-
- **Always the real thing.** The base project is created by the framework's own creator (`composer create-project`, `create-next-app`, `dotnet new`, `
|
|
31
|
-
- **Any version, including future ones.** Versions are fetched live from npm, Packagist and the .NET release index. A new release shows up in the menu without an update to BoilerCraft.
|
|
30
|
+
- **Always the real thing.** The base project is created by the framework's own creator (`composer create-project`, `create-next-app`, `nest new`, `create-vite`, `django-admin`, `dotnet new`, `go mod init`), so it matches the version you pick exactly.
|
|
31
|
+
- **Any version, including future ones.** Versions are fetched live from npm, Packagist, PyPI, the Go module proxy and the .NET release index. A new release shows up in the menu without an update to BoilerCraft.
|
|
32
32
|
- **Ready to build on.** Auth, database wiring, a themed landing page and UI components are added on top, so you start with a working app instead of a blank one.
|
|
33
33
|
- **One command, zero setup.** Everything happens inside `npx boilercraft`. Missing PHP, Composer or the .NET SDK? It offers to install them for you.
|
|
34
34
|
|
|
@@ -53,8 +53,13 @@ When it's done, pick **Start the dev server** or **Open in VS Code** from the sa
|
|
|
53
53
|
| **Laravel** | 10 – 13 | Blade · Vue 3 (Inertia) · React (Inertia) | MySQL · SQL Server | Sanctum API tokens, or full Breeze UI (login, register, profile) for Vue/React |
|
|
54
54
|
| **Next.js** | 14 – 16 | App Router · Pages Router | MongoDB · MySQL · SQL Server | JWT register/login API routes |
|
|
55
55
|
| **.NET** | 8 – 10 | MVC (Model · View · Controller) · Web API | SQL Server · MySQL | MVC: cookie login/register pages · API: JWT + Swagger |
|
|
56
|
+
| **NestJS** | 10 – 12 | — | PostgreSQL · MySQL · MongoDB · SQLite | — (add Prisma for the data layer) |
|
|
56
57
|
| **Node.js + Express** | 4 – 5 | — | MySQL · MongoDB · SQL Server | JWT middleware with roles |
|
|
57
|
-
| **
|
|
58
|
+
| **Vite** | 6 – 9 | React · Vue · Svelte, TypeScript or JavaScript | — (frontend) | — |
|
|
59
|
+
| **Django** | 4 – 6 | — | SQLite · PostgreSQL | Built-in admin + auth |
|
|
60
|
+
| **FastAPI** | 0.x | — | SQLite · PostgreSQL (SQLAlchemy) · none | — |
|
|
61
|
+
| **Go + Gin** | 1.x | — | — | — |
|
|
62
|
+
| **Raw PHP (MVC)** | 8 | — | MySQL · PostgreSQL · SQLite · SQL Server | Session auth |
|
|
58
63
|
|
|
59
64
|
Every project also gets:
|
|
60
65
|
- A **theme**: Slate Midnight, Cyber Emerald, Royal Indigo or Crimson Amber, in dark, light or both with a toggle.
|
|
@@ -63,6 +68,19 @@ Every project also gets:
|
|
|
63
68
|
|
|
64
69
|
The version list above is just what's verified today. Newer releases appear automatically and are marked *preview support* until verified.
|
|
65
70
|
|
|
71
|
+
### Extras
|
|
72
|
+
|
|
73
|
+
Pick any of these with space in the **Extras** step, or pass `-e docker,ci`:
|
|
74
|
+
|
|
75
|
+
| Extra | What you get | Available for |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| **Docker** | `Dockerfile`, `.dockerignore` and a `docker-compose.yml` that also starts your database (MySQL, PostgreSQL, MongoDB or SQL Server) | every framework |
|
|
78
|
+
| **GitHub Actions CI** | `.github/workflows/ci.yml` that installs, lints, builds and tests on every push and pull request | every framework |
|
|
79
|
+
| **ESLint + Prettier** | configs plus `npm run lint` and `npm run format` | Next.js · NestJS · Express · Vite |
|
|
80
|
+
| **Prisma ORM** | `prisma/schema.prisma`, the client, a `DATABASE_URL` for your database, `npm run db:migrate` | Next.js · NestJS · Express |
|
|
81
|
+
|
|
82
|
+
After the files are in place, BoilerCraft runs `git init` and makes an **initial commit**, so the project starts with a clean history (turn it off with `--no-git`).
|
|
83
|
+
|
|
66
84
|
## Missing a toolchain?
|
|
67
85
|
|
|
68
86
|
BoilerCraft checks what's installed and offers to set up the rest. You can also do it any time from **Check & install tools** in the menu.
|
|
@@ -72,10 +90,52 @@ BoilerCraft checks what's installed and offers to set up the rest. You can also
|
|
|
72
90
|
| .NET SDK | .NET | Microsoft's official `dotnet-install` script, exact version, user folder, **no admin rights** |
|
|
73
91
|
| Composer | Laravel | Official installer, signature-verified, user folder |
|
|
74
92
|
| PHP | Laravel, Raw PHP | `winget` (Windows) · Homebrew (macOS) · apt / dnf / pacman (Linux) |
|
|
75
|
-
| Node.js | Next.js, Express | Already there, since `npx` runs on it |
|
|
93
|
+
| Node.js | Next.js, NestJS, Express, Vite | Already there, since `npx` runs on it |
|
|
94
|
+
| Python 3 | Django, FastAPI | Install from [python.org](https://www.python.org/downloads) (BoilerCraft creates a `.venv` per project) |
|
|
95
|
+
| Go | Go + Gin | Install from [go.dev/dl](https://go.dev/dl) |
|
|
76
96
|
|
|
77
97
|
Tools installed into the user folder live in `~/.boilercraft/tools` and are added to your user PATH.
|
|
78
98
|
|
|
99
|
+
### `npx boilercraft doctor`
|
|
100
|
+
|
|
101
|
+
Doctor is a quick health check. Run it before your first project, when something fails, or paste its output into a bug report.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx boilercraft doctor
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
It prints three sections:
|
|
108
|
+
|
|
109
|
+
1. **System**: BoilerCraft version, Node.js version, OS and CPU, and where BoilerCraft keeps the tools it installs.
|
|
110
|
+
2. **Tools**: each tool BoilerCraft can use (Node.js, npm, PHP, Composer, .NET SDK, Python, Go, git, Docker), with the version it found or ✖ *not found*, and which frameworks need it.
|
|
111
|
+
3. **Frameworks**: every framework with ✔ ready or ✖ plus exactly what is missing and where to get it, e.g. `✖ Django · needs Python 3 (https://www.python.org/downloads)`.
|
|
112
|
+
|
|
113
|
+
Doctor only reads; it never installs or changes anything. It finishes with a line like `7/10 frameworks ready`. The exit code is `0` when every framework is ready and `1` otherwise, so you can use it in CI:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx boilercraft doctor || echo "some toolchains are missing"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
To fix what doctor reports, run `npx boilercraft tools` (installs PHP, Composer or the .NET SDK for you) or follow the links it prints. Doctor is also in the main menu as **Run doctor**.
|
|
120
|
+
|
|
121
|
+
## Presets
|
|
122
|
+
|
|
123
|
+
Creating the same kind of project often? Save your answers once and skip the questions next time.
|
|
124
|
+
|
|
125
|
+
- After a project is created, choose **Save these choices as a preset**, or add `--save-preset team-api` to a `new` command.
|
|
126
|
+
- Next time, the menu asks **Start from** a preset, or run `npx boilercraft new my-app --preset team-api`.
|
|
127
|
+
- A preset can also be a JSON file you commit and share with your team: `--preset ./boilercraft.json`.
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{ "stack": "nextjs", "version": "16", "router": "app", "db": "mysql", "extras": ["docker", "ci", "lint"], "theme": "emerald" }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Keys match the CLI flags. Saved presets live in `~/.boilercraft/presets`, and `npx boilercraft presets` lists them.
|
|
134
|
+
|
|
135
|
+
## Staying up to date
|
|
136
|
+
|
|
137
|
+
BoilerCraft checks npm for a newer release at most once a day (the result is cached) and shows a notice with the command to update: `npx boilercraft@latest`. Set `BOILERCRAFT_NO_UPDATE_CHECK=1` to turn this off. It is always off in CI.
|
|
138
|
+
|
|
79
139
|
## Web studio
|
|
80
140
|
|
|
81
141
|
Prefer a browser? Choose **Open the web studio** in the menu. You get the same generator as a visual form at `http://localhost:4800`.
|
|
@@ -91,6 +151,10 @@ Every question has a flag. Anything you leave out is asked interactively, or tak
|
|
|
91
151
|
npx boilercraft new shop -s laravel -v 13 --frontend vue -t emerald -d mysql -y
|
|
92
152
|
npx boilercraft new web -s nextjs --router pages --no-auth -d mongodb -y
|
|
93
153
|
npx boilercraft new admin -s dotnet-core -v 10 --template mvc -y --install-tools
|
|
154
|
+
npx boilercraft new ui -s vite --framework vue --language ts -e lint,docker -y
|
|
155
|
+
npx boilercraft new api -s nestjs -d postgresql -e prisma,docker,ci -y --open
|
|
156
|
+
npx boilercraft new blog -s django -d postgresql -e docker -y
|
|
157
|
+
npx boilercraft new team --preset ./boilercraft.json -y
|
|
94
158
|
npx boilercraft --help
|
|
95
159
|
```
|
|
96
160
|
|
|
@@ -127,7 +191,8 @@ BoilerCraft/
|
|
|
127
191
|
│ ├── generate.js # Shared entry for the CLI and web studio
|
|
128
192
|
│ ├── themeGenerator.js # Theme palettes and dark/light CSS
|
|
129
193
|
│ └── componentGenerator.js # Skeleton, spinner, modal and drawer components
|
|
130
|
-
├── templates/ # Offline fallback templates (
|
|
194
|
+
├── templates/ # Offline fallback templates (Raw PHP skeleton too)
|
|
195
|
+
├── test/ # node --test suite (npm test)
|
|
131
196
|
├── public/index.html # Web studio
|
|
132
197
|
└── server.js # Web studio API
|
|
133
198
|
```
|
package/bin/boilercraft.js
CHANGED
|
@@ -35,21 +35,36 @@ const HELP = `
|
|
|
35
35
|
${c.bold('Automation')} ${c.gray('(scripts / CI, no prompts)')}
|
|
36
36
|
npx boilercraft new <name> [flags]
|
|
37
37
|
|
|
38
|
-
-s, --stack <id> laravel | nextjs | node-express | dotnet-core
|
|
38
|
+
-s, --stack <id> laravel | nextjs | nestjs | vite | node-express | dotnet-core
|
|
39
|
+
django | fastapi | go-gin | raw-php
|
|
39
40
|
-v, --version <major> Framework major version (default: recommended)
|
|
40
41
|
--auth / --no-auth Authentication module (default: on)
|
|
41
42
|
--router <type> app | pages (Next.js, default: app)
|
|
42
43
|
--frontend <name> blade | vue | react (Laravel, default: blade)
|
|
43
44
|
--template <type> mvc | api (.NET, default: mvc)
|
|
45
|
+
--framework <name> react | vue | svelte (Vite, default: react)
|
|
46
|
+
--language <lang> ts | js (Vite, default: ts)
|
|
44
47
|
-t, --theme <name> midnight | emerald | royal | crimson
|
|
45
48
|
-m, --mode <mode> all | dark | light
|
|
46
|
-
-d, --db <name> mysql | mssql | mongodb
|
|
49
|
+
-d, --db <name> mysql | mssql | mongodb | postgresql | sqlite | none
|
|
47
50
|
--styling <name> tailwind | bootstrap | vanilla
|
|
51
|
+
-e, --extras <list> docker,ci,lint,prisma (comma separated)
|
|
52
|
+
--git / --no-git Initialize a git repository (default: on)
|
|
53
|
+
-p, --preset <name> Use a saved preset or a JSON file for the answers
|
|
54
|
+
--save-preset <n> Save this run's answers as a preset
|
|
55
|
+
--open Open the project in your editor when done
|
|
56
|
+
--editor <cmd> Editor for --open (default: code, e.g. cursor)
|
|
48
57
|
-o, --out <dir> Parent folder (default: current directory)
|
|
49
58
|
-y, --yes Defaults for anything not given
|
|
50
59
|
--install-tools Install missing PHP / Composer / .NET SDK automatically
|
|
51
60
|
--no-fallback Fail instead of using offline templates
|
|
52
61
|
-h, --help This help
|
|
62
|
+
|
|
63
|
+
${c.bold('Other commands')}
|
|
64
|
+
npx boilercraft doctor Report installed tools and which frameworks are ready
|
|
65
|
+
npx boilercraft tools Install PHP, Composer or the .NET SDK
|
|
66
|
+
npx boilercraft list Frameworks and live versions
|
|
67
|
+
npx boilercraft presets Saved presets
|
|
53
68
|
`;
|
|
54
69
|
|
|
55
70
|
async function main() {
|
|
@@ -65,6 +80,14 @@ async function main() {
|
|
|
65
80
|
router: { type: 'string' },
|
|
66
81
|
frontend: { type: 'string' },
|
|
67
82
|
template: { type: 'string' },
|
|
83
|
+
framework: { type: 'string' },
|
|
84
|
+
language: { type: 'string' },
|
|
85
|
+
extras: { type: 'string', short: 'e' },
|
|
86
|
+
git: { type: 'boolean' },
|
|
87
|
+
preset: { type: 'string', short: 'p' },
|
|
88
|
+
'save-preset': { type: 'string' },
|
|
89
|
+
open: { type: 'boolean' },
|
|
90
|
+
editor: { type: 'string' },
|
|
68
91
|
db: { type: 'string', short: 'd' },
|
|
69
92
|
styling: { type: 'string' },
|
|
70
93
|
port: { type: 'string' },
|
|
@@ -86,12 +109,19 @@ async function main() {
|
|
|
86
109
|
// Non-interactive / scripted use.
|
|
87
110
|
if (command === 'new' || command === 'create') {
|
|
88
111
|
app.printBanner();
|
|
89
|
-
const given = { ...values, name, installTools: values['install-tools'], noFallback: values.fallback === false };
|
|
112
|
+
const given = { ...values, name, installTools: values['install-tools'], noFallback: values.fallback === false, savePreset: values['save-preset'] };
|
|
90
113
|
await app.createFlow(given);
|
|
114
|
+
await app.notifyUpdate();
|
|
91
115
|
return app.goodbye();
|
|
92
116
|
}
|
|
93
117
|
if (command === 'list') return app.browseFlow();
|
|
94
|
-
|
|
118
|
+
// `doctor` used to open the interactive tools screen; it is now a plain report.
|
|
119
|
+
if (command === 'doctor') {
|
|
120
|
+
if (!(await app.doctor())) process.exitCode = 1;
|
|
121
|
+
return app.notifyUpdate();
|
|
122
|
+
}
|
|
123
|
+
if (command === 'tools') return app.toolsFlow();
|
|
124
|
+
if (command === 'presets') return app.listPresetsFlow();
|
|
95
125
|
|
|
96
126
|
// `npx boilercraft my-app` → menu-free create with the name filled in.
|
|
97
127
|
app.printBanner();
|
package/cli/app.js
CHANGED
|
@@ -7,6 +7,9 @@ const { describeStacks, checkRequirements, getStack } = require('../services/eng
|
|
|
7
7
|
const { installMissing, canInstall, toolEnv, RECIPES, TOOLS_HOME } = require('../services/engine/toolInstaller');
|
|
8
8
|
const { generate } = require('../services/generate');
|
|
9
9
|
const { THEMES } = require('../services/themeGenerator');
|
|
10
|
+
const { EXTRAS } = require('../services/engine/extras');
|
|
11
|
+
const presets = require('../services/presets');
|
|
12
|
+
const updateNotifier = require('../services/updateNotifier');
|
|
10
13
|
const pkg = require('../package.json');
|
|
11
14
|
|
|
12
15
|
const { c, log } = ui;
|
|
@@ -15,7 +18,9 @@ const AUTHOR = 'ARG RABBI';
|
|
|
15
18
|
const REPO_URL = 'https://github.com/itrabbi24/BoilerCraft';
|
|
16
19
|
const TAGLINE = 'Production-ready projects on the official tooling · any version · auth · themes';
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
// raw-php now has a manifest in stacks/raw-php and runs through the engine.
|
|
22
|
+
// Kept for reference in case a stack without a manifest is needed again.
|
|
23
|
+
/* const LEGACY_STACKS = [{
|
|
19
24
|
id: 'raw-php',
|
|
20
25
|
name: 'Raw PHP (MVC)',
|
|
21
26
|
databases: ['mysql'],
|
|
@@ -23,7 +28,7 @@ const LEGACY_STACKS = [{
|
|
|
23
28
|
versions: [],
|
|
24
29
|
requirements: { ok: true, missing: [] },
|
|
25
30
|
nextSteps: ['php -S localhost:8000 -t public']
|
|
26
|
-
}];
|
|
31
|
+
}]; */
|
|
27
32
|
|
|
28
33
|
const STACK_HINTS = {
|
|
29
34
|
'laravel': 'Blade · Vue · React',
|
|
@@ -32,7 +37,8 @@ const STACK_HINTS = {
|
|
|
32
37
|
'dotnet-core': 'MVC · Web API · EF Core',
|
|
33
38
|
'raw-php': 'No framework · PDO'
|
|
34
39
|
};
|
|
35
|
-
const
|
|
40
|
+
const stackHint = s => STACK_HINTS[s.id] || s.description || '';
|
|
41
|
+
const DB_LABELS = { mysql: 'MySQL', mssql: 'SQL Server', mongodb: 'MongoDB', postgresql: 'PostgreSQL', sqlite: 'SQLite', none: 'None' };
|
|
36
42
|
const STYLE_LABELS = { tailwind: 'Tailwind CSS', bootstrap: 'Bootstrap 5', vanilla: 'Plain CSS' };
|
|
37
43
|
const COLOR_MODES = [
|
|
38
44
|
{ value: 'all', label: 'Dark + Light', hint: 'with a toggle' },
|
|
@@ -47,7 +53,7 @@ const COLOR_MODES = [
|
|
|
47
53
|
let stacksPromise = null;
|
|
48
54
|
function loadStacks({ refresh = false } = {}) {
|
|
49
55
|
if (!stacksPromise || refresh) {
|
|
50
|
-
stacksPromise = describeStacks().then(list => [...list, ...LEGACY_STACKS])
|
|
56
|
+
stacksPromise = describeStacks(); // was: .then(list => [...list, ...LEGACY_STACKS])
|
|
51
57
|
}
|
|
52
58
|
return stacksPromise;
|
|
53
59
|
}
|
|
@@ -99,6 +105,12 @@ function isNewer(latest, current) {
|
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
async function checkForUpdate() {
|
|
108
|
+
return updateNotifier.checkForUpdate(pkg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Previous uncached check, replaced by services/updateNotifier.js (once a day, cached).
|
|
112
|
+
// eslint-disable-next-line no-unused-vars
|
|
113
|
+
async function checkForUpdateUncached() {
|
|
102
114
|
try {
|
|
103
115
|
const res = await axios.get('https://registry.npmjs.org/boilercraft/latest', { timeout: 1500 });
|
|
104
116
|
return res.data?.version && isNewer(res.data.version, pkg.version) ? res.data.version : null;
|
|
@@ -125,13 +137,32 @@ function versionOption(v) {
|
|
|
125
137
|
return { value: v.major, label: `${v.major}.x`, short: `${v.major}.x`, hint: `latest ${v.latest}${tags.length ? ' · ' + tags.join(', ') : ''}` };
|
|
126
138
|
}
|
|
127
139
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
// Asks only when there is a real choice; a single option is picked silently.
|
|
141
|
+
async function choose(ask, question, options, { initial = 0 } = {}) {
|
|
142
|
+
if (options.length === 1 || !ask) return options[options.length === 1 ? 0 : initial].value;
|
|
143
|
+
return ui.select(question, options, { initial });
|
|
144
|
+
}
|
|
131
145
|
|
|
146
|
+
async function askPreset(given) {
|
|
147
|
+
if (given.preset) return presets.loadPreset(given.preset);
|
|
148
|
+
if (given.yes || given.stack) return {};
|
|
149
|
+
const saved = presets.listPresets();
|
|
150
|
+
if (!saved.length) return {};
|
|
151
|
+
const pick = await ui.select('Start from', [
|
|
152
|
+
{ value: null, label: 'A fresh project', short: 'Fresh' },
|
|
153
|
+
...saved.map(p => ({ value: p.name, label: `Preset: ${p.name}`, short: p.name, hint: [p.stack, p.db, ...(p.extras || [])].filter(Boolean).join(' · ') }))
|
|
154
|
+
]);
|
|
155
|
+
return pick ? presets.loadPreset(pick) : {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function askProject(given = {}) {
|
|
132
159
|
console.log(ui.divider('New project'));
|
|
133
160
|
console.log('');
|
|
134
161
|
|
|
162
|
+
given = { ...(await askPreset(given)), ...given };
|
|
163
|
+
const cwd = path.resolve(given.out || process.cwd());
|
|
164
|
+
const ask = !given.yes;
|
|
165
|
+
|
|
135
166
|
const projectName = given.name
|
|
136
167
|
? (() => {
|
|
137
168
|
const err = validateName(given.name, cwd);
|
|
@@ -150,7 +181,7 @@ async function askProject(given = {}) {
|
|
|
150
181
|
value: s,
|
|
151
182
|
label: s.name,
|
|
152
183
|
short: s.name,
|
|
153
|
-
hint: s.requirements.ok ?
|
|
184
|
+
hint: s.requirements.ok ? stackHint(s) : c.yellow(`needs ${s.requirements.missing.map(m => m.name).join(', ')} · can install`)
|
|
154
185
|
})))
|
|
155
186
|
: stacks[0];
|
|
156
187
|
}
|
|
@@ -164,7 +195,7 @@ async function askProject(given = {}) {
|
|
|
164
195
|
throw new Error(`${stack.name} ${given.version} is not available. Options: ${stack.versions.map(v => v.major).join(', ')}`);
|
|
165
196
|
}
|
|
166
197
|
} else {
|
|
167
|
-
version =
|
|
198
|
+
version = await choose(ask, `${stack.name} version`, stack.versions.map(versionOption), { initial: rec });
|
|
168
199
|
}
|
|
169
200
|
}
|
|
170
201
|
|
|
@@ -176,12 +207,10 @@ async function askProject(given = {}) {
|
|
|
176
207
|
throw new Error(`Invalid ${opt.label.toLowerCase()} "${preset}". Options: ${opt.choices.map(ch => ch.value).join(', ')}`);
|
|
177
208
|
}
|
|
178
209
|
const initial = Math.max(0, opt.choices.findIndex(ch => ch.value === opt.default));
|
|
179
|
-
options[opt.id] = preset ?? (ask
|
|
180
|
-
? await ui.select(opt.label, opt.choices.map(ch => ({ ...ch, short: ch.label })), { initial })
|
|
181
|
-
: opt.choices[initial].value);
|
|
210
|
+
options[opt.id] = preset ?? await choose(ask, opt.label, opt.choices.map(ch => ({ ...ch, short: ch.label })), { initial });
|
|
182
211
|
}
|
|
183
212
|
|
|
184
|
-
const auth = given.auth !== undefined ? given.auth : ask
|
|
213
|
+
const auth = stack.auth === false ? false : given.auth !== undefined ? given.auth : ask
|
|
185
214
|
? await ui.select('Authentication', [
|
|
186
215
|
{ value: true, label: 'Include authentication', short: 'Included', hint: 'register · login · token' },
|
|
187
216
|
{ value: false, label: 'No authentication', short: 'None' }
|
|
@@ -202,12 +231,20 @@ async function askProject(given = {}) {
|
|
|
202
231
|
if (given.db && !stack.databases.includes(given.db)) {
|
|
203
232
|
throw new Error(`${stack.name} supports: ${stack.databases.join(', ')} (got "${given.db}")`);
|
|
204
233
|
}
|
|
205
|
-
const database = given.db || (ask
|
|
206
|
-
|
|
207
|
-
: stack.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
234
|
+
const database = given.db || await choose(ask, 'Database', stack.databases.map(d => ({ value: d, label: DB_LABELS[d] || d })));
|
|
235
|
+
if (given.styling && !stack.stylings.includes(given.styling)) {
|
|
236
|
+
throw new Error(`${stack.name} supports styling: ${stack.stylings.join(', ')} (got "${given.styling}")`);
|
|
237
|
+
}
|
|
238
|
+
const styling = given.styling || await choose(ask, 'Styling', stack.stylings.map(s => ({ value: s, label: STYLE_LABELS[s] || s })));
|
|
239
|
+
|
|
240
|
+
const supported = stack.extras || [];
|
|
241
|
+
const givenExtras = typeof given.extras === 'string' ? given.extras.split(',').map(x => x.trim()).filter(Boolean) : given.extras;
|
|
242
|
+
const unknown = (givenExtras || []).filter(x => !supported.includes(x));
|
|
243
|
+
if (unknown.length) throw new Error(`${stack.name} does not support: ${unknown.join(', ')}. Options: ${supported.join(', ') || 'none'}`);
|
|
244
|
+
const extras = givenExtras || (ask && supported.length
|
|
245
|
+
? await ui.multiSelect('Extras', supported.map(x => ({ value: x, label: EXTRAS[x].label, short: EXTRAS[x].label, hint: EXTRAS[x].hint })))
|
|
246
|
+
: []);
|
|
247
|
+
const git = given.git !== undefined ? given.git : ask ? await ui.confirm('Initialize a git repository?', true) : true;
|
|
211
248
|
|
|
212
249
|
return {
|
|
213
250
|
stack,
|
|
@@ -221,6 +258,8 @@ async function askProject(given = {}) {
|
|
|
221
258
|
colorMode,
|
|
222
259
|
database,
|
|
223
260
|
styling,
|
|
261
|
+
extras,
|
|
262
|
+
git,
|
|
224
263
|
port: given.port ? parseInt(given.port, 10) : undefined,
|
|
225
264
|
appTitle: given.title,
|
|
226
265
|
author: given.author,
|
|
@@ -241,10 +280,12 @@ function summaryBox(stack, config) {
|
|
|
241
280
|
['Project', c.bold(config.projectName)],
|
|
242
281
|
['Framework', `${stack.name}${version ? ` ${version}.x` : ''}`],
|
|
243
282
|
...(stack.options || []).map(opt => [opt.label, opt.choices.find(ch => ch.value === config.options?.[opt.id])?.label || '']),
|
|
244
|
-
['Auth', config.auth.enabled ? 'Included' : 'None'],
|
|
283
|
+
...(stack.auth === false ? [] : [['Auth', config.auth.enabled ? 'Included' : 'None']]),
|
|
245
284
|
['Theme', `${ui.swatch(THEMES[config.theme].primary)} ${THEMES[config.theme].name} · ${COLOR_MODES.find(m => m.value === config.colorMode)?.label}`],
|
|
246
285
|
['Database', DB_LABELS[config.database] || config.database],
|
|
247
286
|
['Styling', STYLE_LABELS[config.styling] || config.styling],
|
|
287
|
+
['Extras', config.extras.length ? config.extras.map(x => EXTRAS[x].label).join(', ') : 'None'],
|
|
288
|
+
['Git', config.git ? 'Initialize repository' : 'No'],
|
|
248
289
|
['Location', c.gray(displayPath(path.join(config.outputDir, config.projectName.toLowerCase())))]
|
|
249
290
|
];
|
|
250
291
|
return ui.box(rows.map(([k, v]) => `${c.gray(k.padEnd(10))} ${v}`), { title: 'Summary' });
|
|
@@ -312,7 +353,7 @@ async function runGeneration(config) {
|
|
|
312
353
|
return { ...result, seconds };
|
|
313
354
|
}
|
|
314
355
|
|
|
315
|
-
async function afterCreate(stack, result) {
|
|
356
|
+
async function afterCreate(stack, result, config) {
|
|
316
357
|
const rel = path.relative(process.cwd(), result.path) || '.';
|
|
317
358
|
const steps = result.nextSteps || (stack.nextSteps || []);
|
|
318
359
|
console.log('');
|
|
@@ -331,6 +372,7 @@ async function afterCreate(stack, result) {
|
|
|
331
372
|
{ value: 'run', label: 'Start the dev server', hint: steps.join(' → ') },
|
|
332
373
|
hasCode && { value: 'code', label: 'Open in VS Code' },
|
|
333
374
|
{ value: 'folder', label: 'Open the project folder' },
|
|
375
|
+
config && { value: 'preset', label: 'Save these choices as a preset', hint: 'reuse with --preset or from the menu' },
|
|
334
376
|
{ value: 'menu', label: 'Back to main menu' },
|
|
335
377
|
{ value: 'exit', label: 'Exit' }
|
|
336
378
|
].filter(Boolean);
|
|
@@ -356,6 +398,10 @@ async function afterCreate(stack, result) {
|
|
|
356
398
|
} else if (action === 'code') {
|
|
357
399
|
openInBackground(`code "${result.path}"`);
|
|
358
400
|
log.ok('Opened in VS Code');
|
|
401
|
+
} else if (action === 'preset') {
|
|
402
|
+
const name = await ui.input('Preset name', { initial: `${stack.id}-default`, validate: v => (presets.slug(v) ? null : 'Use letters or numbers') });
|
|
403
|
+
const file = presets.savePreset(name, presets.presetFromConfig(config, stack));
|
|
404
|
+
log.ok(`Saved ${c.gray(file)} · next time: ${c.cyan(`npx boilercraft new my-app --preset ${presets.slug(name)}`)}`);
|
|
359
405
|
} else if (action === 'folder') {
|
|
360
406
|
openInBackground(openerFor(result.path));
|
|
361
407
|
log.ok('Opened folder');
|
|
@@ -390,14 +436,25 @@ async function createFlow(given = {}) {
|
|
|
390
436
|
}
|
|
391
437
|
|
|
392
438
|
const result = await runGeneration(config);
|
|
439
|
+
if (given.savePreset) {
|
|
440
|
+
log.ok(`Preset saved: ${presets.savePreset(given.savePreset, presets.presetFromConfig(config, stack))}`);
|
|
441
|
+
}
|
|
442
|
+
if (given.open) openEditor(result.path, given.editor);
|
|
393
443
|
if (given.yes || !ui.isTTY) {
|
|
394
444
|
printPlainNextSteps(stack, result);
|
|
395
445
|
return 'exit';
|
|
396
446
|
}
|
|
397
|
-
return afterCreate(stack, result);
|
|
447
|
+
return afterCreate(stack, result, config);
|
|
398
448
|
}
|
|
399
449
|
}
|
|
400
450
|
|
|
451
|
+
// --open, with --editor <cmd> (VS Code by default, e.g. --editor cursor).
|
|
452
|
+
function openEditor(dir, editor) {
|
|
453
|
+
const cmd = typeof editor === 'string' && editor ? editor : 'code';
|
|
454
|
+
openInBackground(`${cmd} "${dir}"`);
|
|
455
|
+
log.ok(`Opening in ${cmd}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
401
458
|
function printPlainNextSteps(stack, result) {
|
|
402
459
|
const rel = path.relative(process.cwd(), result.path) || '.';
|
|
403
460
|
console.log('');
|
|
@@ -413,6 +470,13 @@ function printPlainNextSteps(stack, result) {
|
|
|
413
470
|
// ---------------------------------------------------------------------------
|
|
414
471
|
|
|
415
472
|
async function toolStatus() {
|
|
473
|
+
const first = out => (out ? (out.match(/(\d+\.\d+(\.\d+)?)/) || [])[1] || out.split('\n')[0] : null);
|
|
474
|
+
const [git, python, go, docker] = await Promise.all([
|
|
475
|
+
probe('git', ['--version']),
|
|
476
|
+
probe(process.platform === 'win32' ? 'python' : 'python3', ['--version']),
|
|
477
|
+
probe('go', ['version']),
|
|
478
|
+
probe('docker', ['--version'])
|
|
479
|
+
]);
|
|
416
480
|
const [npm, php, composer, sdks] = await Promise.all([
|
|
417
481
|
probe('npm', ['--version']),
|
|
418
482
|
probe('php', ['-r', '"echo PHP_VERSION;"']),
|
|
@@ -425,7 +489,11 @@ async function toolStatus() {
|
|
|
425
489
|
{ id: 'npm', name: 'npm', version: npm, usedBy: 'Next.js · Express' },
|
|
426
490
|
{ id: 'php', name: 'PHP', version: php, usedBy: 'Laravel · Raw PHP' },
|
|
427
491
|
{ id: 'composer', name: 'Composer', version: composer && (composer.match(/(\d+\.\d+\.\d+)/) || [])[1], usedBy: 'Laravel' },
|
|
428
|
-
{ id: 'dotnet', name: '.NET SDK', version: sdkList.length ? sdkList.join(', ') : null, usedBy: '.NET' }
|
|
492
|
+
{ id: 'dotnet', name: '.NET SDK', version: sdkList.length ? sdkList.join(', ') : null, usedBy: '.NET' },
|
|
493
|
+
{ id: 'python', name: 'Python', version: first(python), usedBy: 'Django · FastAPI', install: 'https://www.python.org/downloads' },
|
|
494
|
+
{ id: 'go', name: 'Go', version: first(go), usedBy: 'Go + Gin', install: 'https://go.dev/dl' },
|
|
495
|
+
{ id: 'git', name: 'git', version: first(git), usedBy: 'repository setup', install: 'https://git-scm.com' },
|
|
496
|
+
{ id: 'docker', name: 'Docker', version: first(docker), usedBy: 'Docker extra (optional)', install: 'https://docs.docker.com/get-docker' }
|
|
429
497
|
];
|
|
430
498
|
}
|
|
431
499
|
|
|
@@ -470,6 +538,34 @@ async function toolsFlow() {
|
|
|
470
538
|
}
|
|
471
539
|
}
|
|
472
540
|
|
|
541
|
+
/**
|
|
542
|
+
* `boilercraft doctor`: a plain, non-interactive health report. Lists every
|
|
543
|
+
* tool, which frameworks are ready right now, and exits 1 if a framework
|
|
544
|
+
* cannot be created (handy in CI and when reporting an issue).
|
|
545
|
+
*/
|
|
546
|
+
async function doctor() {
|
|
547
|
+
console.log(ui.divider('Doctor'));
|
|
548
|
+
console.log('');
|
|
549
|
+
const [tools, stacks] = await ui.spinner('Checking tools and frameworks…', () => Promise.all([toolStatus(), loadStacks()]));
|
|
550
|
+
console.log(ui.box([
|
|
551
|
+
`${c.gray('BoilerCraft'.padEnd(12))} v${pkg.version}`,
|
|
552
|
+
`${c.gray('Node.js'.padEnd(12))} ${process.version}`,
|
|
553
|
+
`${c.gray('Platform'.padEnd(12))} ${process.platform} ${process.arch}`,
|
|
554
|
+
`${c.gray('Tools home'.padEnd(12))} ${TOOLS_HOME}`
|
|
555
|
+
], { title: 'System' }));
|
|
556
|
+
console.log(ui.box(tools.map(t => {
|
|
557
|
+
const mark = t.version ? c.green('✔') : c.yellow('✖');
|
|
558
|
+
return `${mark} ${t.name.padEnd(10)} ${(t.version || c.yellow('not found')).padEnd(18)} ${c.gray(t.usedBy)}`;
|
|
559
|
+
}), { title: 'Tools' }));
|
|
560
|
+
console.log(ui.box(stacks.map(s => s.requirements.ok
|
|
561
|
+
? `${c.green('✔')} ${s.name}`
|
|
562
|
+
: `${c.yellow('✖')} ${s.name} ${c.gray('· needs ' + s.requirements.missing.map(m => `${m.name}${m.install ? ` (${m.install})` : ''}`).join(', '))}`
|
|
563
|
+
), { title: 'Frameworks' }));
|
|
564
|
+
const ready = stacks.filter(s => s.requirements.ok).length;
|
|
565
|
+
console.log(`\n ${ready}/${stacks.length} frameworks ready.${ready < stacks.length ? ` Run ${c.cyan('npx boilercraft tools')} to install PHP, Composer or .NET for you.` : ''}\n`);
|
|
566
|
+
return ready === stacks.length;
|
|
567
|
+
}
|
|
568
|
+
|
|
473
569
|
// ---------------------------------------------------------------------------
|
|
474
570
|
// Browse / About / Web studio
|
|
475
571
|
// ---------------------------------------------------------------------------
|
|
@@ -480,7 +576,7 @@ async function browseFlow() {
|
|
|
480
576
|
const stacks = await ui.spinner('Loading frameworks and latest versions…', () => loadStacks());
|
|
481
577
|
for (const s of stacks) {
|
|
482
578
|
const status = s.requirements.ok ? c.green('● ready') : c.yellow(`○ needs ${s.requirements.missing.map(m => m.name).join(', ')}`);
|
|
483
|
-
const lines = [`${status} ${c.gray(
|
|
579
|
+
const lines = [`${status} ${c.gray(stackHint(s))}`];
|
|
484
580
|
if (s.versions.length) {
|
|
485
581
|
lines.push('');
|
|
486
582
|
for (const v of s.versions) {
|
|
@@ -491,7 +587,7 @@ async function browseFlow() {
|
|
|
491
587
|
lines.push('', `${c.gray('Databases')} ${s.databases.map(d => DB_LABELS[d] || d).join(', ')}`);
|
|
492
588
|
console.log(ui.box(lines, { title: s.name }));
|
|
493
589
|
}
|
|
494
|
-
console.log(`\n ${c.gray('Versions are fetched live from npm, Packagist
|
|
590
|
+
console.log(`\n ${c.gray('Versions are fetched live from npm, Packagist, PyPI, the Go proxy and .NET,')}`);
|
|
495
591
|
console.log(` ${c.gray('so new framework releases appear here automatically.')}\n`);
|
|
496
592
|
await ui.select('Done?', [{ value: 'back', label: 'Back to main menu' }]);
|
|
497
593
|
}
|
|
@@ -564,8 +660,9 @@ async function home() {
|
|
|
564
660
|
let action;
|
|
565
661
|
try {
|
|
566
662
|
action = await ui.select('What would you like to do?', [
|
|
567
|
-
{ value: 'create', label: 'Create a new project', hint: 'Laravel · Next.js ·
|
|
663
|
+
{ value: 'create', label: 'Create a new project', hint: 'Laravel · Next.js · NestJS · Vite · Django · FastAPI · Go · .NET' },
|
|
568
664
|
{ value: 'tools', label: 'Check & install tools', hint: 'PHP · Composer · .NET SDK' },
|
|
665
|
+
{ value: 'doctor', label: 'Run doctor', hint: 'what is installed, what is ready' },
|
|
569
666
|
{ value: 'browse', label: 'Browse frameworks & versions' },
|
|
570
667
|
{ value: 'web', label: 'Open the web studio', hint: 'same features in your browser' },
|
|
571
668
|
{ value: 'about', label: 'About BoilerCraft' },
|
|
@@ -583,6 +680,7 @@ async function home() {
|
|
|
583
680
|
if ((await createFlow()) === 'exit') return goodbye();
|
|
584
681
|
} else if (action === 'tools') await toolsFlow();
|
|
585
682
|
else if (action === 'browse') await browseFlow();
|
|
683
|
+
else if (action === 'doctor') await doctor();
|
|
586
684
|
else if (action === 'web') await webFlow();
|
|
587
685
|
else if (action === 'about') await aboutFlow();
|
|
588
686
|
} catch (err) {
|
|
@@ -593,4 +691,23 @@ async function home() {
|
|
|
593
691
|
}
|
|
594
692
|
}
|
|
595
693
|
|
|
596
|
-
|
|
694
|
+
function listPresetsFlow() {
|
|
695
|
+
const saved = presets.listPresets();
|
|
696
|
+
console.log('');
|
|
697
|
+
if (!saved.length) {
|
|
698
|
+
log.info(`No presets yet. Create a project and choose "Save these choices as a preset", or pass ${c.cyan('--save-preset <name>')}.`);
|
|
699
|
+
} else {
|
|
700
|
+
console.log(ui.box(saved.map(p => `${c.bold(p.name.padEnd(18))} ${c.gray([p.stack, p.version && `v${p.version}`, p.db, ...(p.extras || [])].filter(Boolean).join(' · '))}`), { title: 'Presets' }));
|
|
701
|
+
console.log(`\n ${c.gray(`Stored in ${presets.PRESETS_DIR}`)}`);
|
|
702
|
+
console.log(` ${c.gray('Use:')} ${c.cyan(`npx boilercraft new my-app --preset ${saved[0].name}`)}`);
|
|
703
|
+
}
|
|
704
|
+
console.log('');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Shown after scripted commands; the menu shows it at the top instead.
|
|
708
|
+
async function notifyUpdate() {
|
|
709
|
+
const latest = await Promise.race([checkForUpdate(), new Promise(r => setTimeout(() => r(null), 1500))]);
|
|
710
|
+
if (latest) console.log(ui.box([`Update available ${c.gray(pkg.version)} → ${c.green(latest)}`, `Run ${c.cyan('npx boilercraft@latest')}`], { color: c.yellow }));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
module.exports = { home, createFlow, toolsFlow, browseFlow, doctor, notifyUpdate, listPresetsFlow, printBanner, goodbye, AUTHOR };
|
package/cli/ui.js
CHANGED
|
@@ -260,6 +260,72 @@ async function selectPlain(question, options, initial) {
|
|
|
260
260
|
throw new CancelError();
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Checkbox list. options: [{ value, label, hint, checked }]
|
|
265
|
+
* Space toggles, "a" toggles all, Enter confirms. Returns the chosen values.
|
|
266
|
+
*/
|
|
267
|
+
async function multiSelect(question, options) {
|
|
268
|
+
const picked = new Set(options.filter(o => o.checked).map(o => o.value));
|
|
269
|
+
if (!isTTY) {
|
|
270
|
+
console.log(` ${c.cyan('?')} ${c.bold(question)}`);
|
|
271
|
+
options.forEach((o, i) => console.log(` ${String(i + 1).padStart(2)}) ${o.label}${o.hint ? c.gray(` ${o.hint}`) : ''}`));
|
|
272
|
+
process.stdout.write(` ${c.gray('numbers, comma separated (blank = none):')} `);
|
|
273
|
+
const answer = await nextLine();
|
|
274
|
+
if (answer === null) throw new CancelError();
|
|
275
|
+
process.stdout.write(`${answer}\n`);
|
|
276
|
+
return answer.split(',').map(n => options[parseInt(n, 10) - 1]).filter(Boolean).map(o => o.value);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let index = 0;
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
const out = process.stdout;
|
|
282
|
+
readline.emitKeypressEvents(process.stdin);
|
|
283
|
+
process.stdin.setRawMode(true);
|
|
284
|
+
process.stdin.resume();
|
|
285
|
+
out.write('\x1b[?25l');
|
|
286
|
+
out.write(truncate(` ${c.cyan('?')} ${c.bold(question)} ${c.gray('· space to toggle, enter to confirm')}`, columns() - 1) + '\n');
|
|
287
|
+
const render = first => {
|
|
288
|
+
if (!first) out.write(`\x1b[${options.length}A`);
|
|
289
|
+
options.forEach((o, i) => {
|
|
290
|
+
const mark = picked.has(o.value) ? c.green('◉') : c.gray('◯');
|
|
291
|
+
const label = i === index ? c.cyan(c.bold(o.label)) : o.label;
|
|
292
|
+
const full = ` ${i === index ? c.cyan('❯') : ' '} ${mark} ${label}${o.hint ? ` ${c.gray(o.hint)}` : ''}`;
|
|
293
|
+
out.write(`\x1b[2K${width(full) <= columns() - 1 ? full : truncate(full, columns() - 1)}\n`);
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
render(true);
|
|
297
|
+
const finish = () => {
|
|
298
|
+
process.stdin.removeListener('keypress', onKey);
|
|
299
|
+
process.stdin.setRawMode(false);
|
|
300
|
+
process.stdin.pause();
|
|
301
|
+
out.write(`\x1b[${options.length + 1}A\x1b[0J\x1b[?25h`);
|
|
302
|
+
};
|
|
303
|
+
const onKey = (str, key = {}) => {
|
|
304
|
+
if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
|
|
305
|
+
finish();
|
|
306
|
+
return reject(new CancelError());
|
|
307
|
+
}
|
|
308
|
+
if (key.name === 'up' || key.name === 'k') index = (index - 1 + options.length) % options.length;
|
|
309
|
+
else if (key.name === 'down' || key.name === 'j' || key.name === 'tab') index = (index + 1) % options.length;
|
|
310
|
+
else if (key.name === 'space') {
|
|
311
|
+
const v = options[index].value;
|
|
312
|
+
if (picked.has(v)) picked.delete(v);
|
|
313
|
+
else picked.add(v);
|
|
314
|
+
} else if (str === 'a') {
|
|
315
|
+
if (picked.size === options.length) picked.clear();
|
|
316
|
+
else options.forEach(o => picked.add(o.value));
|
|
317
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
318
|
+
finish();
|
|
319
|
+
const chosen = options.filter(o => picked.has(o.value));
|
|
320
|
+
out.write(` ${c.green('✔')} ${c.bold(question)} ${c.gray('·')} ${c.cyan(chosen.length ? chosen.map(o => o.short || stripAnsi(o.label)).join(', ') : 'None')}\n`);
|
|
321
|
+
return resolve(chosen.map(o => o.value));
|
|
322
|
+
}
|
|
323
|
+
render(false);
|
|
324
|
+
};
|
|
325
|
+
process.stdin.on('keypress', onKey);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
263
329
|
async function confirm(question, initial = true) {
|
|
264
330
|
return select(question, [
|
|
265
331
|
{ value: true, label: 'Yes', short: 'Yes' },
|
|
@@ -353,4 +419,4 @@ const log = {
|
|
|
353
419
|
info: msg => console.log(` ${c.blue('ℹ')} ${msg}`)
|
|
354
420
|
};
|
|
355
421
|
|
|
356
|
-
module.exports = { truncate, c, isTTY, swatch, gradient, banner, box, divider, input, select, confirm, spinner, tasks, log, width, stripAnsi, CancelError };
|
|
422
|
+
module.exports = { truncate, c, isTTY, swatch, gradient, banner, box, divider, input, select, multiSelect, confirm, spinner, tasks, log, width, stripAnsi, CancelError };
|