boilercraft 1.0.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/LICENSE +21 -0
- package/README.md +171 -0
- package/bin/boilercraft.js +110 -0
- package/cli/app.js +596 -0
- package/cli/ui.js +356 -0
- package/docs/STACK_ENGINE.md +93 -0
- package/docs/assets/cli-create.svg +52 -0
- package/docs/assets/cli-menu.svg +47 -0
- package/docs/stacks/dotnet-core.md +69 -0
- package/docs/stacks/laravel-vue.md +46 -0
- package/docs/stacks/nextjs.md +58 -0
- package/docs/stacks/node-express.md +58 -0
- package/docs/stacks/raw-php.md +130 -0
- package/main.js +82 -0
- package/package.json +86 -0
- package/public/index.html +721 -0
- package/public/logo.png +17 -0
- package/public/logo.svg +17 -0
- package/server.js +84 -0
- package/services/componentGenerator.js +195 -0
- package/services/engine/scaffoldEngine.js +517 -0
- package/services/engine/toolInstaller.js +232 -0
- package/services/engine/versionCatalog.js +115 -0
- package/services/generate.js +46 -0
- package/services/projectGenerator.js +686 -0
- package/services/readmeGenerator.js +221 -0
- package/services/themeGenerator.js +150 -0
- package/services/versionResolver.js +176 -0
- package/stacks/dotnet-core/overlays/api/Controllers/ProductsController.cs +33 -0
- package/stacks/dotnet-core/overlays/api/Program.cs +33 -0
- package/stacks/dotnet-core/overlays/api/wwwroot/index.html +38 -0
- package/stacks/dotnet-core/overlays/api-auth/Controllers/AuthController.cs +87 -0
- package/stacks/dotnet-core/overlays/common/Data/ApplicationDbContext.cs +25 -0
- package/stacks/dotnet-core/overlays/common/Models/Product.cs +19 -0
- package/stacks/dotnet-core/overlays/mvc/Controllers/ProductsController.cs +84 -0
- package/stacks/dotnet-core/overlays/mvc/Views/Home/Index.cshtml +23 -0
- package/stacks/dotnet-core/overlays/mvc/Views/Products/Create.cshtml +16 -0
- package/stacks/dotnet-core/overlays/mvc/Views/Products/Delete.cshtml +16 -0
- package/stacks/dotnet-core/overlays/mvc/Views/Products/Edit.cshtml +17 -0
- package/stacks/dotnet-core/overlays/mvc/Views/Products/Index.cshtml +46 -0
- package/stacks/dotnet-core/overlays/mvc/Views/Products/_Form.cshtml +19 -0
- package/stacks/dotnet-core/overlays/mvc-auth/Controllers/AccountController.cs +98 -0
- package/stacks/dotnet-core/overlays/mvc-auth/Models/AppUser.cs +48 -0
- package/stacks/dotnet-core/overlays/mvc-auth/Views/Account/Login.cshtml +35 -0
- package/stacks/dotnet-core/overlays/mvc-auth/Views/Account/Register.cshtml +41 -0
- package/stacks/dotnet-core/overlays/mvc-auth/Views/Shared/_AuthNav.cshtml +22 -0
- package/stacks/dotnet-core/snippets/packages-auth.xml +4 -0
- package/stacks/dotnet-core/snippets/packages-common.xml +5 -0
- package/stacks/dotnet-core/snippets/packages-ef.xml +4 -0
- package/stacks/dotnet-core/snippets/packages-mssql.xml +4 -0
- package/stacks/dotnet-core/snippets/packages-mysql.xml +4 -0
- package/stacks/dotnet-core/snippets/packages-swagger.xml +4 -0
- package/stacks/dotnet-core/stack.json +426 -0
- package/stacks/laravel/overlays/auth/app/Http/Controllers/Api/AuthController.php +67 -0
- package/stacks/laravel/overlays/common/app/Http/Controllers/BoilerCraftController.php +14 -0
- package/stacks/laravel/overlays/common/resources/views/boilercraft.blade.php +36 -0
- package/stacks/laravel/overlays/inertia-common/resources/views/app.blade.php +15 -0
- package/stacks/laravel/overlays/inertia-react/resources/js/Pages/Welcome.jsx +18 -0
- package/stacks/laravel/overlays/inertia-react/resources/js/app.jsx +13 -0
- package/stacks/laravel/overlays/inertia-vue/resources/js/Pages/Welcome.vue +23 -0
- package/stacks/laravel/overlays/inertia-vue/resources/js/app.js +13 -0
- package/stacks/laravel/snippets/api-routes.php +9 -0
- package/stacks/laravel/snippets/web-routes-inertia.php +7 -0
- package/stacks/laravel/snippets/web-routes.php +4 -0
- package/stacks/laravel/stack.json +477 -0
- package/stacks/nextjs/overlays/auth/src/lib/auth.js +36 -0
- package/stacks/nextjs/overlays/auth-app/src/app/api/auth/login/route.js +11 -0
- package/stacks/nextjs/overlays/auth-app/src/app/api/auth/register/route.js +14 -0
- package/stacks/nextjs/overlays/auth-pages/src/pages/api/auth/login.js +14 -0
- package/stacks/nextjs/overlays/auth-pages/src/pages/api/auth/register.js +17 -0
- package/stacks/nextjs/overlays/common/.env.example +3 -0
- package/stacks/nextjs/overlays/common/.env.local +3 -0
- package/stacks/nextjs/overlays/db/mongodb/src/lib/db.js +12 -0
- package/stacks/nextjs/overlays/db/mssql/src/lib/db.js +17 -0
- package/stacks/nextjs/overlays/db/mysql/src/lib/db.js +13 -0
- package/stacks/nextjs/overlays/router-app/src/app/api/health/route.js +10 -0
- package/stacks/nextjs/overlays/router-app/src/app/layout.js +16 -0
- package/stacks/nextjs/overlays/router-app/src/app/page.js +19 -0
- package/stacks/nextjs/overlays/router-pages/src/pages/_app.js +7 -0
- package/stacks/nextjs/overlays/router-pages/src/pages/_document.js +13 -0
- package/stacks/nextjs/overlays/router-pages/src/pages/api/health.js +8 -0
- package/stacks/nextjs/overlays/router-pages/src/pages/index.js +27 -0
- package/stacks/nextjs/stack.json +257 -0
- package/stacks/node-express/overlays/auth/src/controllers/authController.js +53 -0
- package/stacks/node-express/overlays/auth/src/middlewares/auth.js +36 -0
- package/stacks/node-express/overlays/auth/src/routes/authRoutes.js +16 -0
- package/stacks/node-express/overlays/common/.env.example +5 -0
- package/stacks/node-express/overlays/common/public/index.html +31 -0
- package/stacks/node-express/overlays/common/src/server.js +32 -0
- package/stacks/node-express/overlays/db/mongodb/src/db.js +13 -0
- package/stacks/node-express/overlays/db/mssql/src/db.js +22 -0
- package/stacks/node-express/overlays/db/mysql/src/db.js +22 -0
- package/stacks/node-express/stack.json +165 -0
- package/templates/dotnet-bootstrap-mssql/Controllers/AuthController.cs +74 -0
- package/templates/dotnet-bootstrap-mssql/Controllers/ProductsController.cs +33 -0
- package/templates/dotnet-bootstrap-mssql/Data/ApplicationDbContext.cs +24 -0
- package/templates/dotnet-bootstrap-mssql/Models/Product.cs +10 -0
- package/templates/dotnet-core/Controllers/AuthController.cs +74 -0
- package/templates/laravel-full/app/Http/Controllers/AuthController.php +79 -0
- package/templates/laravel-full/app/Http/Controllers/HomeController.php +22 -0
- package/templates/laravel-full/app/Models/User.php +49 -0
- package/templates/laravel-full/artisan +22 -0
- package/templates/laravel-full/bootstrap/app.php +21 -0
- package/templates/laravel-full/config/database.php +54 -0
- package/templates/laravel-full/database/migrations/2024_01_01_000001_create_users_table.php +27 -0
- package/templates/laravel-full/public/index.php +17 -0
- package/templates/laravel-full/resources/css/app.css +3 -0
- package/templates/laravel-full/resources/js/Pages/Home.vue +76 -0
- package/templates/laravel-full/resources/js/app.js +21 -0
- package/templates/laravel-full/resources/js/bootstrap.js +3 -0
- package/templates/laravel-full/resources/views/app.blade.php +15 -0
- package/templates/laravel-full/routes/api.php +8 -0
- package/templates/laravel-full/routes/console.php +7 -0
- package/templates/laravel-full/routes/web.php +24 -0
- package/templates/laravel-full/tailwind.config.js +12 -0
- package/templates/laravel-full/vite.config.js +20 -0
- package/templates/laravel-vue/app/Http/Controllers/Auth/AuthenticatedSessionController.php +43 -0
- package/templates/laravel-vue-mysql/app/Http/Controllers/HomeController.php +25 -0
- package/templates/laravel-vue-mysql/resources/js/Pages/Home.vue +70 -0
- package/templates/nextjs/src/app/api/auth/login/route.js +28 -0
- package/templates/nextjs-mongodb/src/app/api/health/route.js +17 -0
- package/templates/nextjs-mongodb/src/lib/mongodb.js +18 -0
- package/templates/nextjs-mssql/src/app/api/health/route.js +24 -0
- package/templates/nextjs-mssql/src/lib/mssql.js +38 -0
- package/templates/node-express/src/controllers/authController.js +53 -0
- package/templates/node-express/src/middlewares/auth.js +36 -0
- package/templates/node-express/src/routes/authRoutes.js +16 -0
- package/templates/node-mongodb/src/controllers/userController.js +30 -0
- package/templates/node-mongodb/src/middlewares/errorHandler.js +8 -0
- package/templates/node-mongodb/src/models/User.js +23 -0
- package/templates/node-mongodb/src/routes/api.js +9 -0
- package/templates/raw-php-mysql/app/Controllers/AuthController.php +73 -0
- package/templates/raw-php-mysql/app/Controllers/HomeController.php +65 -0
- package/templates/raw-php-mysql/app/Models/User.php +60 -0
- package/templates/raw-php-mysql/app/Views/404.php +15 -0
- package/templates/raw-php-mysql/app/Views/auth/login.php +66 -0
- package/templates/raw-php-mysql/app/Views/auth/register.php +71 -0
- package/templates/raw-php-mysql/app/Views/dashboard.php +50 -0
- package/templates/raw-php-mysql/app/Views/home.php +83 -0
- package/templates/raw-php-mysql/config/Auth.php +62 -0
- package/templates/raw-php-mysql/config/Database.php +100 -0
- package/templates/raw-php-mysql/config/Env.php +30 -0
- package/templates/raw-php-mysql/config/Middleware.php +53 -0
- package/templates/raw-php-mysql/config/QueryBuilder.php +109 -0
- package/templates/raw-php-mysql/config/Router.php +33 -0
- package/templates/raw-php-mysql/public/index.php +57 -0
package/cli/app.js
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
const ui = require('./ui');
|
|
6
|
+
const { describeStacks, checkRequirements, getStack } = require('../services/engine/scaffoldEngine');
|
|
7
|
+
const { installMissing, canInstall, toolEnv, RECIPES, TOOLS_HOME } = require('../services/engine/toolInstaller');
|
|
8
|
+
const { generate } = require('../services/generate');
|
|
9
|
+
const { THEMES } = require('../services/themeGenerator');
|
|
10
|
+
const pkg = require('../package.json');
|
|
11
|
+
|
|
12
|
+
const { c, log } = ui;
|
|
13
|
+
|
|
14
|
+
const AUTHOR = 'ARG RABBI';
|
|
15
|
+
const REPO_URL = 'https://github.com/itrabbi24/BoilerCraft';
|
|
16
|
+
const TAGLINE = 'Production-ready projects on the official tooling · any version · auth · themes';
|
|
17
|
+
|
|
18
|
+
const LEGACY_STACKS = [{
|
|
19
|
+
id: 'raw-php',
|
|
20
|
+
name: 'Raw PHP (MVC)',
|
|
21
|
+
databases: ['mysql'],
|
|
22
|
+
stylings: ['tailwind', 'bootstrap', 'vanilla'],
|
|
23
|
+
versions: [],
|
|
24
|
+
requirements: { ok: true, missing: [] },
|
|
25
|
+
nextSteps: ['php -S localhost:8000 -t public']
|
|
26
|
+
}];
|
|
27
|
+
|
|
28
|
+
const STACK_HINTS = {
|
|
29
|
+
'laravel': 'Blade · Vue · React',
|
|
30
|
+
'nextjs': 'React · App Router / Pages Router',
|
|
31
|
+
'node-express': 'REST API · JWT',
|
|
32
|
+
'dotnet-core': 'MVC · Web API · EF Core',
|
|
33
|
+
'raw-php': 'No framework · PDO'
|
|
34
|
+
};
|
|
35
|
+
const DB_LABELS = { mysql: 'MySQL', mssql: 'SQL Server', mongodb: 'MongoDB' };
|
|
36
|
+
const STYLE_LABELS = { tailwind: 'Tailwind CSS', bootstrap: 'Bootstrap 5', vanilla: 'Plain CSS' };
|
|
37
|
+
const COLOR_MODES = [
|
|
38
|
+
{ value: 'all', label: 'Dark + Light', hint: 'with a toggle' },
|
|
39
|
+
{ value: 'dark', label: 'Dark only' },
|
|
40
|
+
{ value: 'light', label: 'Light only' }
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Shared helpers
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
let stacksPromise = null;
|
|
48
|
+
function loadStacks({ refresh = false } = {}) {
|
|
49
|
+
if (!stacksPromise || refresh) {
|
|
50
|
+
stacksPromise = describeStacks().then(list => [...list, ...LEGACY_STACKS]);
|
|
51
|
+
}
|
|
52
|
+
return stacksPromise;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function probe(cmd, args) {
|
|
56
|
+
return new Promise(resolve => {
|
|
57
|
+
const child = spawn([cmd, ...args].join(' '), { shell: true, env: toolEnv() });
|
|
58
|
+
let out = '';
|
|
59
|
+
child.stdout.on('data', d => (out += d));
|
|
60
|
+
child.stderr.on('data', d => (out += d));
|
|
61
|
+
child.on('error', () => resolve(null));
|
|
62
|
+
child.on('close', code => resolve(code === 0 ? ui.stripAnsi(out).trim() : null));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Runs a command attached to the terminal (dev servers, editors). Ctrl+C stops
|
|
67
|
+
// the child and returns here instead of killing BoilerCraft.
|
|
68
|
+
function runAttached(command, cwd) {
|
|
69
|
+
return new Promise(resolve => {
|
|
70
|
+
const ignore = () => {};
|
|
71
|
+
process.on('SIGINT', ignore);
|
|
72
|
+
const child = spawn(command, { cwd, shell: true, stdio: 'inherit', env: toolEnv() });
|
|
73
|
+
child.on('error', () => resolve(1));
|
|
74
|
+
child.on('close', code => {
|
|
75
|
+
process.removeListener('SIGINT', ignore);
|
|
76
|
+
resolve(code ?? 0);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function openInBackground(command) {
|
|
82
|
+
const child = spawn(command, { shell: true, detached: true, stdio: 'ignore', env: toolEnv() });
|
|
83
|
+
child.on('error', () => {});
|
|
84
|
+
child.unref();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function openerFor(target) {
|
|
88
|
+
const quoted = `"${target}"`;
|
|
89
|
+
if (process.platform === 'win32') return `start "" ${quoted}`;
|
|
90
|
+
if (process.platform === 'darwin') return `open ${quoted}`;
|
|
91
|
+
return `xdg-open ${quoted}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isNewer(latest, current) {
|
|
95
|
+
const a = latest.split('.').map(Number);
|
|
96
|
+
const b = current.split('.').map(Number);
|
|
97
|
+
for (let i = 0; i < 3; i++) if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function checkForUpdate() {
|
|
102
|
+
try {
|
|
103
|
+
const res = await axios.get('https://registry.npmjs.org/boilercraft/latest', { timeout: 1500 });
|
|
104
|
+
return res.data?.version && isNewer(res.data.version, pkg.version) ? res.data.version : null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Create project
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
function validateName(name, cwd) {
|
|
115
|
+
if (!name) return 'Please enter a name';
|
|
116
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(name)) return 'Use letters, numbers, - and _ (must start with a letter or number)';
|
|
117
|
+
if (name.length > 60) return 'Keep it under 60 characters';
|
|
118
|
+
const target = path.join(cwd, name.toLowerCase());
|
|
119
|
+
if (fs.existsSync(target) && fs.readdirSync(target).length) return `A non-empty folder "${name.toLowerCase()}" already exists here`;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function versionOption(v) {
|
|
124
|
+
const tags = [v.recommended && 'recommended', v.lts && 'LTS', !v.tested && 'preview support'].filter(Boolean);
|
|
125
|
+
return { value: v.major, label: `${v.major}.x`, short: `${v.major}.x`, hint: `latest ${v.latest}${tags.length ? ' · ' + tags.join(', ') : ''}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function askProject(given = {}) {
|
|
129
|
+
const cwd = path.resolve(given.out || process.cwd());
|
|
130
|
+
const ask = !given.yes;
|
|
131
|
+
|
|
132
|
+
console.log(ui.divider('New project'));
|
|
133
|
+
console.log('');
|
|
134
|
+
|
|
135
|
+
const projectName = given.name
|
|
136
|
+
? (() => {
|
|
137
|
+
const err = validateName(given.name, cwd);
|
|
138
|
+
if (err) throw new Error(err);
|
|
139
|
+
return given.name;
|
|
140
|
+
})()
|
|
141
|
+
: ask ? await ui.input('Project name', { initial: 'my-app', validate: v => validateName(v, cwd) }) : 'my-app';
|
|
142
|
+
|
|
143
|
+
const stacks = await ui.spinner('Loading frameworks and latest versions…', () => loadStacks());
|
|
144
|
+
|
|
145
|
+
let stack = given.stack && stacks.find(s => s.id === given.stack);
|
|
146
|
+
if (given.stack && !stack) throw new Error(`Unknown stack "${given.stack}". Options: ${stacks.map(s => s.id).join(', ')}`);
|
|
147
|
+
if (!stack) {
|
|
148
|
+
stack = ask
|
|
149
|
+
? await ui.select('Framework', stacks.map(s => ({
|
|
150
|
+
value: s,
|
|
151
|
+
label: s.name,
|
|
152
|
+
short: s.name,
|
|
153
|
+
hint: s.requirements.ok ? STACK_HINTS[s.id] : c.yellow(`needs ${s.requirements.missing.map(m => m.name).join(', ')} · can install`)
|
|
154
|
+
})))
|
|
155
|
+
: stacks[0];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let version;
|
|
159
|
+
if (stack.versions.length) {
|
|
160
|
+
const rec = Math.max(0, stack.versions.findIndex(v => v.recommended));
|
|
161
|
+
if (given.version) {
|
|
162
|
+
version = parseInt(given.version, 10);
|
|
163
|
+
if (!stack.versions.some(v => v.major === version)) {
|
|
164
|
+
throw new Error(`${stack.name} ${given.version} is not available. Options: ${stack.versions.map(v => v.major).join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
version = ask ? await ui.select(`${stack.name} version`, stack.versions.map(versionOption), { initial: rec }) : stack.versions[rec].major;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Stack-specific questions declared in the manifest (e.g. Next.js router).
|
|
172
|
+
const options = {};
|
|
173
|
+
for (const opt of stack.options || []) {
|
|
174
|
+
const preset = given[opt.id];
|
|
175
|
+
if (preset !== undefined && !opt.choices.some(ch => ch.value === preset)) {
|
|
176
|
+
throw new Error(`Invalid ${opt.label.toLowerCase()} "${preset}". Options: ${opt.choices.map(ch => ch.value).join(', ')}`);
|
|
177
|
+
}
|
|
178
|
+
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);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const auth = given.auth !== undefined ? given.auth : ask
|
|
185
|
+
? await ui.select('Authentication', [
|
|
186
|
+
{ value: true, label: 'Include authentication', short: 'Included', hint: 'register · login · token' },
|
|
187
|
+
{ value: false, label: 'No authentication', short: 'None' }
|
|
188
|
+
])
|
|
189
|
+
: true;
|
|
190
|
+
|
|
191
|
+
const themeIds = Object.keys(THEMES);
|
|
192
|
+
if (given.theme && !THEMES[given.theme]) throw new Error(`Unknown theme "${given.theme}". Options: ${themeIds.join(', ')}`);
|
|
193
|
+
const theme = given.theme || (ask
|
|
194
|
+
? await ui.select('Theme', themeIds.map(id => ({
|
|
195
|
+
value: id,
|
|
196
|
+
label: `${ui.swatch(THEMES[id].primary)}${ui.swatch(THEMES[id].secondary)}${ui.swatch(THEMES[id].surface)} ${THEMES[id].name}`,
|
|
197
|
+
short: THEMES[id].name
|
|
198
|
+
})))
|
|
199
|
+
: themeIds[0]);
|
|
200
|
+
const colorMode = given.mode || (ask ? await ui.select('Color mode', COLOR_MODES.map(m => ({ ...m, short: m.label }))) : 'all');
|
|
201
|
+
|
|
202
|
+
if (given.db && !stack.databases.includes(given.db)) {
|
|
203
|
+
throw new Error(`${stack.name} supports: ${stack.databases.join(', ')} (got "${given.db}")`);
|
|
204
|
+
}
|
|
205
|
+
const database = given.db || (ask
|
|
206
|
+
? await ui.select('Database', stack.databases.map(d => ({ value: d, label: DB_LABELS[d] || d })))
|
|
207
|
+
: stack.databases[0]);
|
|
208
|
+
const styling = given.styling || (ask
|
|
209
|
+
? await ui.select('Styling', stack.stylings.map(s => ({ value: s, label: STYLE_LABELS[s] || s })))
|
|
210
|
+
: stack.stylings[0]);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
stack,
|
|
214
|
+
config: {
|
|
215
|
+
projectName,
|
|
216
|
+
backend: stack.id,
|
|
217
|
+
versions: version ? { [stack.id]: version } : {},
|
|
218
|
+
options,
|
|
219
|
+
auth: { enabled: auth },
|
|
220
|
+
theme,
|
|
221
|
+
colorMode,
|
|
222
|
+
database,
|
|
223
|
+
styling,
|
|
224
|
+
port: given.port ? parseInt(given.port, 10) : undefined,
|
|
225
|
+
appTitle: given.title,
|
|
226
|
+
author: given.author,
|
|
227
|
+
outputDir: cwd,
|
|
228
|
+
allowLegacyFallback: !given.noFallback
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function displayPath(p) {
|
|
234
|
+
const rel = path.relative(process.cwd(), p);
|
|
235
|
+
return rel && !rel.startsWith('..') && !path.isAbsolute(rel) ? `.${path.sep}${rel}` : p;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function summaryBox(stack, config) {
|
|
239
|
+
const version = config.versions[stack.id];
|
|
240
|
+
const rows = [
|
|
241
|
+
['Project', c.bold(config.projectName)],
|
|
242
|
+
['Framework', `${stack.name}${version ? ` ${version}.x` : ''}`],
|
|
243
|
+
...(stack.options || []).map(opt => [opt.label, opt.choices.find(ch => ch.value === config.options?.[opt.id])?.label || '']),
|
|
244
|
+
['Auth', config.auth.enabled ? 'Included' : 'None'],
|
|
245
|
+
['Theme', `${ui.swatch(THEMES[config.theme].primary)} ${THEMES[config.theme].name} · ${COLOR_MODES.find(m => m.value === config.colorMode)?.label}`],
|
|
246
|
+
['Database', DB_LABELS[config.database] || config.database],
|
|
247
|
+
['Styling', STYLE_LABELS[config.styling] || config.styling],
|
|
248
|
+
['Location', c.gray(displayPath(path.join(config.outputDir, config.projectName.toLowerCase())))]
|
|
249
|
+
];
|
|
250
|
+
return ui.box(rows.map(([k, v]) => `${c.gray(k.padEnd(10))} ${v}`), { title: 'Summary' });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function ensureTools(stack, config, given = {}) {
|
|
254
|
+
const manifest = getStack(stack.id);
|
|
255
|
+
if (!manifest) return true;
|
|
256
|
+
const version = config.versions[stack.id];
|
|
257
|
+
const missing = await checkRequirements(manifest, version);
|
|
258
|
+
if (!missing.length) return true;
|
|
259
|
+
|
|
260
|
+
const installable = missing.filter(m => canInstall(m.tool));
|
|
261
|
+
const manual = missing.filter(m => !canInstall(m.tool));
|
|
262
|
+
console.log('');
|
|
263
|
+
log.warn(`${stack.name}${version ? ` ${version}` : ''} needs ${missing.map(m => m.name).join(', ')}, which ${missing.length > 1 ? 'are' : 'is'} not installed.`);
|
|
264
|
+
for (const m of manual) log.info(`Install ${m.name} manually: ${m.install}`);
|
|
265
|
+
|
|
266
|
+
let choice;
|
|
267
|
+
if (given.installTools) choice = 'install';
|
|
268
|
+
else if (given.yes) choice = 'fallback';
|
|
269
|
+
else {
|
|
270
|
+
const options = [];
|
|
271
|
+
if (installable.length && !manual.length) {
|
|
272
|
+
options.push({ value: 'install', label: `Install ${installable.map(m => m.name).join(' + ')} for me`, short: 'Install tools', hint: 'recommended · no admin needed for .NET / Composer' });
|
|
273
|
+
}
|
|
274
|
+
options.push({ value: 'fallback', label: 'Continue with offline templates', short: 'Offline templates', hint: 'works now, version may not match exactly' });
|
|
275
|
+
options.push({ value: 'back', label: 'Go back', short: 'Back' });
|
|
276
|
+
choice = await ui.select('How do you want to continue?', options);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (choice === 'back') return false;
|
|
280
|
+
if (choice === 'install') {
|
|
281
|
+
console.log('');
|
|
282
|
+
await installMissing(installable, msg => log.step(msg));
|
|
283
|
+
const still = await checkRequirements(manifest, version);
|
|
284
|
+
if (still.length) throw new Error(`Still missing after install: ${still.map(m => m.name).join(', ')}. Open a new terminal and run npx boilercraft again.`);
|
|
285
|
+
log.ok('Tools installed');
|
|
286
|
+
loadStacks({ refresh: true });
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function runGeneration(config) {
|
|
292
|
+
console.log('');
|
|
293
|
+
const progress = ui.tasks();
|
|
294
|
+
let result;
|
|
295
|
+
try {
|
|
296
|
+
result = await generate(config, msg => {
|
|
297
|
+
if (msg.startsWith('$ ')) progress.detail(msg.slice(2));
|
|
298
|
+
else if (msg.startsWith('WARNING')) progress.warn(msg.replace(/^WARNING:\s*/, ''));
|
|
299
|
+
else if (msg === 'Done.' || msg.startsWith('Scaffolding ')) return;
|
|
300
|
+
else progress.update(msg);
|
|
301
|
+
});
|
|
302
|
+
} catch (err) {
|
|
303
|
+
progress.fail();
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
if (!result.success) {
|
|
307
|
+
progress.fail();
|
|
308
|
+
throw new Error(result.error || 'Generation failed');
|
|
309
|
+
}
|
|
310
|
+
if (result.engine !== 'official') progress.update('Generated from offline templates');
|
|
311
|
+
const seconds = progress.done();
|
|
312
|
+
return { ...result, seconds };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function afterCreate(stack, result) {
|
|
316
|
+
const rel = path.relative(process.cwd(), result.path) || '.';
|
|
317
|
+
const steps = result.nextSteps || (stack.nextSteps || []);
|
|
318
|
+
console.log('');
|
|
319
|
+
console.log(ui.box([
|
|
320
|
+
`${c.green('✔')} ${c.bold(result.projectName)} is ready ${c.gray(`in ${result.seconds}s`)}`,
|
|
321
|
+
'',
|
|
322
|
+
`${c.gray('Folder'.padEnd(8))} ${rel}`,
|
|
323
|
+
`${c.gray('Stack'.padEnd(8))} ${result.combination || stack.name}`,
|
|
324
|
+
`${c.gray('Docs'.padEnd(8))} ${result.engine === 'official' ? 'BOILERCRAFT.md' : 'README.md'}`
|
|
325
|
+
], { title: 'Done', color: c.green }));
|
|
326
|
+
console.log('');
|
|
327
|
+
|
|
328
|
+
const hasCode = !!(await probe('code', ['--version']));
|
|
329
|
+
for (;;) {
|
|
330
|
+
const options = [
|
|
331
|
+
{ value: 'run', label: 'Start the dev server', hint: steps.join(' → ') },
|
|
332
|
+
hasCode && { value: 'code', label: 'Open in VS Code' },
|
|
333
|
+
{ value: 'folder', label: 'Open the project folder' },
|
|
334
|
+
{ value: 'menu', label: 'Back to main menu' },
|
|
335
|
+
{ value: 'exit', label: 'Exit' }
|
|
336
|
+
].filter(Boolean);
|
|
337
|
+
let action;
|
|
338
|
+
try {
|
|
339
|
+
action = await ui.select('What next?', options);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (err instanceof ui.CancelError) return 'exit';
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (action === 'run') {
|
|
346
|
+
console.log('');
|
|
347
|
+
log.info(`Starting in ${rel} · press ${c.bold('Ctrl+C')} to stop and come back here`);
|
|
348
|
+
for (let i = 0; i < steps.length; i++) {
|
|
349
|
+
console.log(ui.divider(steps[i]));
|
|
350
|
+
const code = await runAttached(steps[i], result.path);
|
|
351
|
+
if (code !== 0 && i < steps.length - 1) {
|
|
352
|
+
log.warn(`"${steps[i]}" failed (exit ${code}). Is your database running? Continuing…`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
console.log('');
|
|
356
|
+
} else if (action === 'code') {
|
|
357
|
+
openInBackground(`code "${result.path}"`);
|
|
358
|
+
log.ok('Opened in VS Code');
|
|
359
|
+
} else if (action === 'folder') {
|
|
360
|
+
openInBackground(openerFor(result.path));
|
|
361
|
+
log.ok('Opened folder');
|
|
362
|
+
} else {
|
|
363
|
+
return action;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function createFlow(given = {}) {
|
|
369
|
+
for (;;) {
|
|
370
|
+
const { stack, config } = await askProject(given);
|
|
371
|
+
console.log('');
|
|
372
|
+
console.log(summaryBox(stack, config));
|
|
373
|
+
console.log('');
|
|
374
|
+
|
|
375
|
+
if (!(await ensureTools(stack, config, given))) {
|
|
376
|
+
if (given.yes) return 'exit';
|
|
377
|
+
continue; // start over
|
|
378
|
+
}
|
|
379
|
+
if (!given.yes) {
|
|
380
|
+
const go = await ui.select('Create this project?', [
|
|
381
|
+
{ value: 'yes', label: 'Create project', short: 'Yes' },
|
|
382
|
+
{ value: 'edit', label: 'Start over', short: 'Start over' },
|
|
383
|
+
{ value: 'cancel', label: 'Cancel', short: 'Cancel' }
|
|
384
|
+
]);
|
|
385
|
+
if (go === 'edit') {
|
|
386
|
+
console.log('');
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (go === 'cancel') return 'menu';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const result = await runGeneration(config);
|
|
393
|
+
if (given.yes || !ui.isTTY) {
|
|
394
|
+
printPlainNextSteps(stack, result);
|
|
395
|
+
return 'exit';
|
|
396
|
+
}
|
|
397
|
+
return afterCreate(stack, result);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function printPlainNextSteps(stack, result) {
|
|
402
|
+
const rel = path.relative(process.cwd(), result.path) || '.';
|
|
403
|
+
console.log('');
|
|
404
|
+
log.ok(`${c.bold(result.projectName)} is ready ${c.gray(`(${result.combination || result.engine}, ${result.seconds}s)`)}`);
|
|
405
|
+
console.log(`\n ${c.bold('Next steps')}`);
|
|
406
|
+
console.log(` cd ${rel.includes(' ') ? `"${rel}"` : rel}`);
|
|
407
|
+
for (const s of result.nextSteps || stack.nextSteps || []) console.log(` ${s}`);
|
|
408
|
+
console.log('');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// Tools
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
async function toolStatus() {
|
|
416
|
+
const [npm, php, composer, sdks] = await Promise.all([
|
|
417
|
+
probe('npm', ['--version']),
|
|
418
|
+
probe('php', ['-r', '"echo PHP_VERSION;"']),
|
|
419
|
+
probe('composer', ['--version', '--no-ansi']),
|
|
420
|
+
probe('dotnet', ['--list-sdks'])
|
|
421
|
+
]);
|
|
422
|
+
const sdkList = sdks ? [...new Set(sdks.split(/\r?\n/).map(l => l.split(' ')[0]).filter(Boolean))] : [];
|
|
423
|
+
return [
|
|
424
|
+
{ id: 'node', name: 'Node.js', version: process.version.slice(1), usedBy: 'Next.js · Express' },
|
|
425
|
+
{ id: 'npm', name: 'npm', version: npm, usedBy: 'Next.js · Express' },
|
|
426
|
+
{ id: 'php', name: 'PHP', version: php, usedBy: 'Laravel · Raw PHP' },
|
|
427
|
+
{ 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' }
|
|
429
|
+
];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function toolsFlow() {
|
|
433
|
+
for (;;) {
|
|
434
|
+
console.log(ui.divider('Tools'));
|
|
435
|
+
console.log('');
|
|
436
|
+
const tools = await ui.spinner('Checking installed tools…', toolStatus);
|
|
437
|
+
console.log(ui.box(tools.map(t => {
|
|
438
|
+
const mark = t.version ? c.green('●') : c.gray('○');
|
|
439
|
+
const ver = t.version ? t.version : c.yellow('not installed');
|
|
440
|
+
return `${mark} ${t.name.padEnd(10)} ${ver} ${c.gray(`· ${t.usedBy}`)}`;
|
|
441
|
+
}), { title: 'Your toolchain' }));
|
|
442
|
+
console.log(` ${c.gray(`Tools installed by BoilerCraft live in ${TOOLS_HOME}`)}`);
|
|
443
|
+
console.log('');
|
|
444
|
+
|
|
445
|
+
const byId = Object.fromEntries(tools.map(t => [t.id, t]));
|
|
446
|
+
const options = [];
|
|
447
|
+
if (!byId.php.version) options.push({ value: 'php', label: 'Install PHP', hint: process.platform === 'win32' ? 'via winget' : process.platform === 'darwin' ? 'via Homebrew' : 'via your package manager' });
|
|
448
|
+
if (!byId.composer.version) options.push({ value: 'composer', label: 'Install Composer', hint: byId.php.version ? 'official installer' : 'needs PHP first', disabled: !byId.php.version });
|
|
449
|
+
options.push({ value: 'dotnet', label: byId.dotnet.version ? 'Install another .NET SDK version' : 'Install .NET SDK', hint: 'official script · no admin' });
|
|
450
|
+
options.push({ value: 'back', label: 'Back to main menu' });
|
|
451
|
+
|
|
452
|
+
const choice = await ui.select('Install something?', options);
|
|
453
|
+
if (choice === 'back') return;
|
|
454
|
+
|
|
455
|
+
let major;
|
|
456
|
+
if (choice === 'dotnet') {
|
|
457
|
+
const stacks = await ui.spinner('Fetching .NET releases…', () => loadStacks());
|
|
458
|
+
const dotnet = stacks.find(s => s.id === 'dotnet-core');
|
|
459
|
+
major = await ui.select('.NET SDK version', dotnet.versions.map(versionOption), { initial: Math.max(0, dotnet.versions.findIndex(v => v.recommended)) });
|
|
460
|
+
}
|
|
461
|
+
console.log('');
|
|
462
|
+
try {
|
|
463
|
+
await installMissing([{ tool: choice, name: RECIPES[choice].name, major }], msg => log.step(msg));
|
|
464
|
+
log.ok(`${RECIPES[choice].name} installed`);
|
|
465
|
+
loadStacks({ refresh: true });
|
|
466
|
+
} catch (err) {
|
|
467
|
+
log.error(err.message);
|
|
468
|
+
}
|
|
469
|
+
console.log('');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// Browse / About / Web studio
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
async function browseFlow() {
|
|
478
|
+
console.log(ui.divider('Frameworks & versions'));
|
|
479
|
+
console.log('');
|
|
480
|
+
const stacks = await ui.spinner('Loading frameworks and latest versions…', () => loadStacks());
|
|
481
|
+
for (const s of stacks) {
|
|
482
|
+
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(STACK_HINTS[s.id] || '')}`];
|
|
484
|
+
if (s.versions.length) {
|
|
485
|
+
lines.push('');
|
|
486
|
+
for (const v of s.versions) {
|
|
487
|
+
const tags = [v.recommended && c.cyan('recommended'), v.lts && c.green('LTS'), !v.tested && c.yellow('preview support')].filter(Boolean);
|
|
488
|
+
lines.push(`${`${v.major}.x`.padEnd(6)} ${c.gray(`latest ${v.latest}`.padEnd(18))} ${tags.join(c.gray(' · '))}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
lines.push('', `${c.gray('Databases')} ${s.databases.map(d => DB_LABELS[d] || d).join(', ')}`);
|
|
492
|
+
console.log(ui.box(lines, { title: s.name }));
|
|
493
|
+
}
|
|
494
|
+
console.log(`\n ${c.gray('Versions are fetched live from npm, Packagist and the .NET release index,')}`);
|
|
495
|
+
console.log(` ${c.gray('so new framework releases appear here automatically.')}\n`);
|
|
496
|
+
await ui.select('Done?', [{ value: 'back', label: 'Back to main menu' }]);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function aboutFlow() {
|
|
500
|
+
console.log('');
|
|
501
|
+
console.log(ui.box([
|
|
502
|
+
`${c.bold('BoilerCraft')} ${c.gray(`v${pkg.version}`)}`,
|
|
503
|
+
c.gray(TAGLINE),
|
|
504
|
+
'',
|
|
505
|
+
`${c.gray('Author'.padEnd(9))} ${c.bold(AUTHOR)}`,
|
|
506
|
+
`${c.gray('GitHub'.padEnd(9))} ${REPO_URL}`,
|
|
507
|
+
`${c.gray('License'.padEnd(9))} ${pkg.license}`,
|
|
508
|
+
'',
|
|
509
|
+
'Every project starts from the framework\'s own official creator',
|
|
510
|
+
'(composer create-project, create-next-app, dotnet new, npm), then',
|
|
511
|
+
'BoilerCraft adds auth, database wiring and your theme on top.'
|
|
512
|
+
], { title: 'About' }));
|
|
513
|
+
console.log('');
|
|
514
|
+
await ui.select('Done?', [
|
|
515
|
+
{ value: 'back', label: 'Back to main menu' },
|
|
516
|
+
{ value: 'repo', label: 'Open GitHub page' }
|
|
517
|
+
]).then(v => v === 'repo' && openInBackground(openerFor(REPO_URL)));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function webFlow() {
|
|
521
|
+
const { startServer } = require('../server');
|
|
522
|
+
await new Promise(resolve => {
|
|
523
|
+
const server = startServer(port => {
|
|
524
|
+
const url = `http://localhost:${port}`;
|
|
525
|
+
console.log('');
|
|
526
|
+
log.ok(`Web studio running at ${c.cyan(url)}`);
|
|
527
|
+
log.info(`Press ${c.bold('Ctrl+C')} to stop and return to the menu`);
|
|
528
|
+
openInBackground(openerFor(url));
|
|
529
|
+
});
|
|
530
|
+
const stop = () => {
|
|
531
|
+
process.removeListener('SIGINT', stop);
|
|
532
|
+
server.close(() => resolve());
|
|
533
|
+
server.closeAllConnections?.(); // drop browser keep-alive sockets so Ctrl+C returns promptly
|
|
534
|
+
console.log('');
|
|
535
|
+
};
|
|
536
|
+
process.on('SIGINT', stop);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
// Home
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
function goodbye() {
|
|
545
|
+
console.log(`\n ${c.gray('Thanks for using')} ${ui.gradient('BoilerCraft')}${c.gray(' · made with')} ${c.red('♥')} ${c.gray('by')} ${c.bold(AUTHOR)}\n`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function printBanner() {
|
|
549
|
+
console.log(ui.banner({ version: pkg.version, tagline: TAGLINE, author: AUTHOR }));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function home() {
|
|
553
|
+
printBanner();
|
|
554
|
+
const updatePromise = checkForUpdate();
|
|
555
|
+
loadStacks(); // warm up in the background while the user reads the menu
|
|
556
|
+
|
|
557
|
+
const latest = await Promise.race([updatePromise, new Promise(r => setTimeout(() => r(null), 800))]);
|
|
558
|
+
if (latest) {
|
|
559
|
+
console.log(ui.box([`Update available ${c.gray(pkg.version)} → ${c.green(latest)}`, `Run ${c.cyan('npx boilercraft@latest')}`], { color: c.yellow }));
|
|
560
|
+
console.log('');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
for (;;) {
|
|
564
|
+
let action;
|
|
565
|
+
try {
|
|
566
|
+
action = await ui.select('What would you like to do?', [
|
|
567
|
+
{ value: 'create', label: 'Create a new project', hint: 'Laravel · Next.js · Express · .NET · PHP' },
|
|
568
|
+
{ value: 'tools', label: 'Check & install tools', hint: 'PHP · Composer · .NET SDK' },
|
|
569
|
+
{ value: 'browse', label: 'Browse frameworks & versions' },
|
|
570
|
+
{ value: 'web', label: 'Open the web studio', hint: 'same features in your browser' },
|
|
571
|
+
{ value: 'about', label: 'About BoilerCraft' },
|
|
572
|
+
{ value: 'exit', label: 'Exit' }
|
|
573
|
+
]);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
if (err instanceof ui.CancelError) action = 'exit';
|
|
576
|
+
else throw err;
|
|
577
|
+
}
|
|
578
|
+
if (action === 'exit') return goodbye();
|
|
579
|
+
|
|
580
|
+
console.log('');
|
|
581
|
+
try {
|
|
582
|
+
if (action === 'create') {
|
|
583
|
+
if ((await createFlow()) === 'exit') return goodbye();
|
|
584
|
+
} else if (action === 'tools') await toolsFlow();
|
|
585
|
+
else if (action === 'browse') await browseFlow();
|
|
586
|
+
else if (action === 'web') await webFlow();
|
|
587
|
+
else if (action === 'about') await aboutFlow();
|
|
588
|
+
} catch (err) {
|
|
589
|
+
if (err instanceof ui.CancelError) log.info('Cancelled — back to the main menu');
|
|
590
|
+
else log.error(err.message);
|
|
591
|
+
}
|
|
592
|
+
console.log('');
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
module.exports = { home, createFlow, toolsFlow, browseFlow, printBanner, goodbye, AUTHOR };
|