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.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/bin/boilercraft.js +110 -0
  4. package/cli/app.js +596 -0
  5. package/cli/ui.js +356 -0
  6. package/docs/STACK_ENGINE.md +93 -0
  7. package/docs/assets/cli-create.svg +52 -0
  8. package/docs/assets/cli-menu.svg +47 -0
  9. package/docs/stacks/dotnet-core.md +69 -0
  10. package/docs/stacks/laravel-vue.md +46 -0
  11. package/docs/stacks/nextjs.md +58 -0
  12. package/docs/stacks/node-express.md +58 -0
  13. package/docs/stacks/raw-php.md +130 -0
  14. package/main.js +82 -0
  15. package/package.json +86 -0
  16. package/public/index.html +721 -0
  17. package/public/logo.png +17 -0
  18. package/public/logo.svg +17 -0
  19. package/server.js +84 -0
  20. package/services/componentGenerator.js +195 -0
  21. package/services/engine/scaffoldEngine.js +517 -0
  22. package/services/engine/toolInstaller.js +232 -0
  23. package/services/engine/versionCatalog.js +115 -0
  24. package/services/generate.js +46 -0
  25. package/services/projectGenerator.js +686 -0
  26. package/services/readmeGenerator.js +221 -0
  27. package/services/themeGenerator.js +150 -0
  28. package/services/versionResolver.js +176 -0
  29. package/stacks/dotnet-core/overlays/api/Controllers/ProductsController.cs +33 -0
  30. package/stacks/dotnet-core/overlays/api/Program.cs +33 -0
  31. package/stacks/dotnet-core/overlays/api/wwwroot/index.html +38 -0
  32. package/stacks/dotnet-core/overlays/api-auth/Controllers/AuthController.cs +87 -0
  33. package/stacks/dotnet-core/overlays/common/Data/ApplicationDbContext.cs +25 -0
  34. package/stacks/dotnet-core/overlays/common/Models/Product.cs +19 -0
  35. package/stacks/dotnet-core/overlays/mvc/Controllers/ProductsController.cs +84 -0
  36. package/stacks/dotnet-core/overlays/mvc/Views/Home/Index.cshtml +23 -0
  37. package/stacks/dotnet-core/overlays/mvc/Views/Products/Create.cshtml +16 -0
  38. package/stacks/dotnet-core/overlays/mvc/Views/Products/Delete.cshtml +16 -0
  39. package/stacks/dotnet-core/overlays/mvc/Views/Products/Edit.cshtml +17 -0
  40. package/stacks/dotnet-core/overlays/mvc/Views/Products/Index.cshtml +46 -0
  41. package/stacks/dotnet-core/overlays/mvc/Views/Products/_Form.cshtml +19 -0
  42. package/stacks/dotnet-core/overlays/mvc-auth/Controllers/AccountController.cs +98 -0
  43. package/stacks/dotnet-core/overlays/mvc-auth/Models/AppUser.cs +48 -0
  44. package/stacks/dotnet-core/overlays/mvc-auth/Views/Account/Login.cshtml +35 -0
  45. package/stacks/dotnet-core/overlays/mvc-auth/Views/Account/Register.cshtml +41 -0
  46. package/stacks/dotnet-core/overlays/mvc-auth/Views/Shared/_AuthNav.cshtml +22 -0
  47. package/stacks/dotnet-core/snippets/packages-auth.xml +4 -0
  48. package/stacks/dotnet-core/snippets/packages-common.xml +5 -0
  49. package/stacks/dotnet-core/snippets/packages-ef.xml +4 -0
  50. package/stacks/dotnet-core/snippets/packages-mssql.xml +4 -0
  51. package/stacks/dotnet-core/snippets/packages-mysql.xml +4 -0
  52. package/stacks/dotnet-core/snippets/packages-swagger.xml +4 -0
  53. package/stacks/dotnet-core/stack.json +426 -0
  54. package/stacks/laravel/overlays/auth/app/Http/Controllers/Api/AuthController.php +67 -0
  55. package/stacks/laravel/overlays/common/app/Http/Controllers/BoilerCraftController.php +14 -0
  56. package/stacks/laravel/overlays/common/resources/views/boilercraft.blade.php +36 -0
  57. package/stacks/laravel/overlays/inertia-common/resources/views/app.blade.php +15 -0
  58. package/stacks/laravel/overlays/inertia-react/resources/js/Pages/Welcome.jsx +18 -0
  59. package/stacks/laravel/overlays/inertia-react/resources/js/app.jsx +13 -0
  60. package/stacks/laravel/overlays/inertia-vue/resources/js/Pages/Welcome.vue +23 -0
  61. package/stacks/laravel/overlays/inertia-vue/resources/js/app.js +13 -0
  62. package/stacks/laravel/snippets/api-routes.php +9 -0
  63. package/stacks/laravel/snippets/web-routes-inertia.php +7 -0
  64. package/stacks/laravel/snippets/web-routes.php +4 -0
  65. package/stacks/laravel/stack.json +477 -0
  66. package/stacks/nextjs/overlays/auth/src/lib/auth.js +36 -0
  67. package/stacks/nextjs/overlays/auth-app/src/app/api/auth/login/route.js +11 -0
  68. package/stacks/nextjs/overlays/auth-app/src/app/api/auth/register/route.js +14 -0
  69. package/stacks/nextjs/overlays/auth-pages/src/pages/api/auth/login.js +14 -0
  70. package/stacks/nextjs/overlays/auth-pages/src/pages/api/auth/register.js +17 -0
  71. package/stacks/nextjs/overlays/common/.env.example +3 -0
  72. package/stacks/nextjs/overlays/common/.env.local +3 -0
  73. package/stacks/nextjs/overlays/db/mongodb/src/lib/db.js +12 -0
  74. package/stacks/nextjs/overlays/db/mssql/src/lib/db.js +17 -0
  75. package/stacks/nextjs/overlays/db/mysql/src/lib/db.js +13 -0
  76. package/stacks/nextjs/overlays/router-app/src/app/api/health/route.js +10 -0
  77. package/stacks/nextjs/overlays/router-app/src/app/layout.js +16 -0
  78. package/stacks/nextjs/overlays/router-app/src/app/page.js +19 -0
  79. package/stacks/nextjs/overlays/router-pages/src/pages/_app.js +7 -0
  80. package/stacks/nextjs/overlays/router-pages/src/pages/_document.js +13 -0
  81. package/stacks/nextjs/overlays/router-pages/src/pages/api/health.js +8 -0
  82. package/stacks/nextjs/overlays/router-pages/src/pages/index.js +27 -0
  83. package/stacks/nextjs/stack.json +257 -0
  84. package/stacks/node-express/overlays/auth/src/controllers/authController.js +53 -0
  85. package/stacks/node-express/overlays/auth/src/middlewares/auth.js +36 -0
  86. package/stacks/node-express/overlays/auth/src/routes/authRoutes.js +16 -0
  87. package/stacks/node-express/overlays/common/.env.example +5 -0
  88. package/stacks/node-express/overlays/common/public/index.html +31 -0
  89. package/stacks/node-express/overlays/common/src/server.js +32 -0
  90. package/stacks/node-express/overlays/db/mongodb/src/db.js +13 -0
  91. package/stacks/node-express/overlays/db/mssql/src/db.js +22 -0
  92. package/stacks/node-express/overlays/db/mysql/src/db.js +22 -0
  93. package/stacks/node-express/stack.json +165 -0
  94. package/templates/dotnet-bootstrap-mssql/Controllers/AuthController.cs +74 -0
  95. package/templates/dotnet-bootstrap-mssql/Controllers/ProductsController.cs +33 -0
  96. package/templates/dotnet-bootstrap-mssql/Data/ApplicationDbContext.cs +24 -0
  97. package/templates/dotnet-bootstrap-mssql/Models/Product.cs +10 -0
  98. package/templates/dotnet-core/Controllers/AuthController.cs +74 -0
  99. package/templates/laravel-full/app/Http/Controllers/AuthController.php +79 -0
  100. package/templates/laravel-full/app/Http/Controllers/HomeController.php +22 -0
  101. package/templates/laravel-full/app/Models/User.php +49 -0
  102. package/templates/laravel-full/artisan +22 -0
  103. package/templates/laravel-full/bootstrap/app.php +21 -0
  104. package/templates/laravel-full/config/database.php +54 -0
  105. package/templates/laravel-full/database/migrations/2024_01_01_000001_create_users_table.php +27 -0
  106. package/templates/laravel-full/public/index.php +17 -0
  107. package/templates/laravel-full/resources/css/app.css +3 -0
  108. package/templates/laravel-full/resources/js/Pages/Home.vue +76 -0
  109. package/templates/laravel-full/resources/js/app.js +21 -0
  110. package/templates/laravel-full/resources/js/bootstrap.js +3 -0
  111. package/templates/laravel-full/resources/views/app.blade.php +15 -0
  112. package/templates/laravel-full/routes/api.php +8 -0
  113. package/templates/laravel-full/routes/console.php +7 -0
  114. package/templates/laravel-full/routes/web.php +24 -0
  115. package/templates/laravel-full/tailwind.config.js +12 -0
  116. package/templates/laravel-full/vite.config.js +20 -0
  117. package/templates/laravel-vue/app/Http/Controllers/Auth/AuthenticatedSessionController.php +43 -0
  118. package/templates/laravel-vue-mysql/app/Http/Controllers/HomeController.php +25 -0
  119. package/templates/laravel-vue-mysql/resources/js/Pages/Home.vue +70 -0
  120. package/templates/nextjs/src/app/api/auth/login/route.js +28 -0
  121. package/templates/nextjs-mongodb/src/app/api/health/route.js +17 -0
  122. package/templates/nextjs-mongodb/src/lib/mongodb.js +18 -0
  123. package/templates/nextjs-mssql/src/app/api/health/route.js +24 -0
  124. package/templates/nextjs-mssql/src/lib/mssql.js +38 -0
  125. package/templates/node-express/src/controllers/authController.js +53 -0
  126. package/templates/node-express/src/middlewares/auth.js +36 -0
  127. package/templates/node-express/src/routes/authRoutes.js +16 -0
  128. package/templates/node-mongodb/src/controllers/userController.js +30 -0
  129. package/templates/node-mongodb/src/middlewares/errorHandler.js +8 -0
  130. package/templates/node-mongodb/src/models/User.js +23 -0
  131. package/templates/node-mongodb/src/routes/api.js +9 -0
  132. package/templates/raw-php-mysql/app/Controllers/AuthController.php +73 -0
  133. package/templates/raw-php-mysql/app/Controllers/HomeController.php +65 -0
  134. package/templates/raw-php-mysql/app/Models/User.php +60 -0
  135. package/templates/raw-php-mysql/app/Views/404.php +15 -0
  136. package/templates/raw-php-mysql/app/Views/auth/login.php +66 -0
  137. package/templates/raw-php-mysql/app/Views/auth/register.php +71 -0
  138. package/templates/raw-php-mysql/app/Views/dashboard.php +50 -0
  139. package/templates/raw-php-mysql/app/Views/home.php +83 -0
  140. package/templates/raw-php-mysql/config/Auth.php +62 -0
  141. package/templates/raw-php-mysql/config/Database.php +100 -0
  142. package/templates/raw-php-mysql/config/Env.php +30 -0
  143. package/templates/raw-php-mysql/config/Middleware.php +53 -0
  144. package/templates/raw-php-mysql/config/QueryBuilder.php +109 -0
  145. package/templates/raw-php-mysql/config/Router.php +33 -0
  146. package/templates/raw-php-mysql/public/index.php +57 -0
@@ -0,0 +1,232 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const crypto = require('crypto');
5
+ const { spawn } = require('child_process');
6
+ const axios = require('axios');
7
+
8
+ /**
9
+ * Installs missing toolchains (PHP, Composer, .NET SDK) on demand.
10
+ *
11
+ * Where possible tools go into a per-user folder (~/.boilercraft/tools) with
12
+ * no admin rights needed; that folder is prepended to PATH for every command
13
+ * BoilerCraft runs, so freshly installed tools work immediately without
14
+ * restarting the terminal.
15
+ *
16
+ * .NET SDK → Microsoft's official dotnet-install script (exact major, user folder)
17
+ * Composer → official getcomposer.org installer, signature-verified (user folder)
18
+ * PHP → winget (Windows) / Homebrew (macOS) / apt · dnf · pacman (Linux)
19
+ */
20
+
21
+ const TOOLS_HOME = process.env.BOILERCRAFT_TOOLS_HOME || path.join(os.homedir(), '.boilercraft', 'tools');
22
+ const BIN_DIR = path.join(TOOLS_HOME, 'bin');
23
+ const DOTNET_DIR = path.join(TOOLS_HOME, 'dotnet');
24
+ const PHP_WINGET_ID = 'PHP.PHP.8.4';
25
+
26
+ // Extra folders searched for tools, ahead of the system PATH.
27
+ function toolPaths() {
28
+ const dirs = [BIN_DIR, DOTNET_DIR];
29
+ if (process.platform === 'win32' && process.env.LOCALAPPDATA) {
30
+ // winget puts command shims here; a running process doesn't see PATH updates.
31
+ dirs.push(path.join(process.env.LOCALAPPDATA, 'Microsoft', 'WinGet', 'Links'));
32
+ }
33
+ return dirs.filter(d => fs.existsSync(d));
34
+ }
35
+
36
+ function toolEnv(base = process.env) {
37
+ const env = { ...base };
38
+ const key = Object.keys(env).find(k => k.toUpperCase() === 'PATH') || 'PATH';
39
+ const extra = toolPaths();
40
+ if (extra.length) env[key] = [...extra, env[key] || ''].join(path.delimiter);
41
+ if (fs.existsSync(path.join(DOTNET_DIR, process.platform === 'win32' ? 'dotnet.exe' : 'dotnet'))) {
42
+ env.DOTNET_ROOT = DOTNET_DIR;
43
+ }
44
+ return env;
45
+ }
46
+
47
+ function exec(cmd, args, { shell = false } = {}) {
48
+ return new Promise((resolve, reject) => {
49
+ // stdio inherit: installers may show progress or ask for a sudo password.
50
+ const child = spawn(cmd, args, { stdio: 'inherit', env: toolEnv(), shell });
51
+ child.on('error', reject);
52
+ child.on('close', code => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${code}`))));
53
+ });
54
+ }
55
+
56
+ function which(cmd) {
57
+ return new Promise(resolve => {
58
+ const probe = process.platform === 'win32' ? 'where' : 'which';
59
+ const child = spawn(probe, [cmd], { env: toolEnv(), shell: process.platform === 'win32' });
60
+ let out = '';
61
+ child.stdout.on('data', d => (out += d));
62
+ child.on('error', () => resolve(null));
63
+ child.on('close', code => resolve(code === 0 ? out.split(/\r?\n/)[0].trim() : null));
64
+ });
65
+ }
66
+
67
+ async function download(url, dest) {
68
+ const res = await axios.get(url, { responseType: 'arraybuffer', timeout: 60000 });
69
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
70
+ fs.writeFileSync(dest, Buffer.from(res.data));
71
+ return dest;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Recipes
76
+ // ---------------------------------------------------------------------------
77
+
78
+ async function installDotnet({ major }, log) {
79
+ const channel = `${major || 'LTS'}${major ? '.0' : ''}`;
80
+ log(`Installing .NET SDK ${channel} into ${DOTNET_DIR} (official dotnet-install script)`);
81
+ if (process.platform === 'win32') {
82
+ const script = await download('https://dot.net/v1/dotnet-install.ps1', path.join(TOOLS_HOME, 'dotnet-install.ps1'));
83
+ await exec('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, '-Channel', channel, '-InstallDir', DOTNET_DIR]);
84
+ } else {
85
+ const script = await download('https://dot.net/v1/dotnet-install.sh', path.join(TOOLS_HOME, 'dotnet-install.sh'));
86
+ await exec('bash', [script, '--channel', channel, '--install-dir', DOTNET_DIR]);
87
+ }
88
+ }
89
+
90
+ async function installComposer(opts, log) {
91
+ if (!(await which('php'))) throw new Error('Composer needs PHP — install PHP first.');
92
+ log(`Installing Composer into ${BIN_DIR} (official installer, signature-verified)`);
93
+ const installer = await download('https://getcomposer.org/installer', path.join(TOOLS_HOME, 'composer-setup.php'));
94
+ const expected = (await axios.get('https://composer.github.io/installer.sig', { timeout: 15000 })).data.trim();
95
+ const actual = crypto.createHash('sha384').update(fs.readFileSync(installer)).digest('hex');
96
+ if (actual !== expected) {
97
+ fs.rmSync(installer, { force: true });
98
+ throw new Error('Composer installer signature mismatch — aborted.');
99
+ }
100
+ fs.mkdirSync(BIN_DIR, { recursive: true });
101
+ await exec('php', [installer, `--install-dir=${BIN_DIR}`, '--filename=composer.phar'], { shell: process.platform === 'win32' });
102
+ fs.rmSync(installer, { force: true });
103
+
104
+ // Shim so plain `composer` works.
105
+ if (process.platform === 'win32') {
106
+ fs.writeFileSync(path.join(BIN_DIR, 'composer.bat'), '@php "%~dp0composer.phar" %*\r\n');
107
+ } else {
108
+ const shim = path.join(BIN_DIR, 'composer');
109
+ fs.writeFileSync(shim, '#!/bin/sh\nexec php "$(dirname "$0")/composer.phar" "$@"\n');
110
+ fs.chmodSync(shim, 0o755);
111
+ }
112
+ }
113
+
114
+ // The Windows PHP zip ships without php.ini, so openssl/mbstring/etc. are off
115
+ // and Composer/Laravel fail. Create one from the bundled development template.
116
+ function ensureWindowsPhpIni(phpExe, log) {
117
+ const dir = path.dirname(fs.realpathSync(phpExe));
118
+ const ini = path.join(dir, 'php.ini');
119
+ if (fs.existsSync(ini)) return;
120
+ const template = path.join(dir, 'php.ini-development');
121
+ if (!fs.existsSync(template)) return log(`php.ini template not found in ${dir}; enable extensions manually.`);
122
+ let content = fs.readFileSync(template, 'utf8');
123
+ content = content.replace(/^;\s*extension_dir\s*=\s*"ext"/m, 'extension_dir = "ext"');
124
+ for (const ext of ['curl', 'fileinfo', 'mbstring', 'openssl', 'pdo_mysql', 'pdo_sqlite', 'sqlite3', 'zip', 'intl', 'sodium']) {
125
+ content = content.replace(new RegExp(`^;\\s*extension=${ext}\\s*$`, 'm'), `extension=${ext}`);
126
+ }
127
+ fs.writeFileSync(ini, content);
128
+ log(`Created ${ini} with the extensions Laravel needs.`);
129
+ }
130
+
131
+ async function installPhp(opts, log) {
132
+ if (process.platform === 'win32') {
133
+ if (!(await which('winget'))) throw new Error('winget not found. Install PHP manually: https://windows.php.net/download');
134
+ log(`Installing PHP via winget (${PHP_WINGET_ID})`);
135
+ await exec('winget', ['install', '--id', PHP_WINGET_ID, '-e', '--accept-source-agreements', '--accept-package-agreements'], { shell: true });
136
+ const php = await which('php');
137
+ if (!php) throw new Error('PHP installed, but not found on PATH yet. Open a new terminal and run the command again.');
138
+ ensureWindowsPhpIni(php, log);
139
+ } else if (process.platform === 'darwin') {
140
+ if (!(await which('brew'))) throw new Error('Homebrew not found. Install it from https://brew.sh, then re-run.');
141
+ log('Installing PHP via Homebrew');
142
+ await exec('brew', ['install', 'php']);
143
+ } else {
144
+ const managers = [
145
+ ['apt-get', ['sudo', 'apt-get', 'install', '-y', 'php-cli', 'php-mbstring', 'php-xml', 'php-curl', 'php-zip', 'php-sqlite3', 'php-mysql', 'php-intl', 'unzip']],
146
+ ['dnf', ['sudo', 'dnf', 'install', '-y', 'php-cli', 'php-mbstring', 'php-xml', 'php-pdo', 'php-mysqlnd', 'php-intl', 'unzip']],
147
+ ['pacman', ['sudo', 'pacman', '-S', '--noconfirm', 'php', 'unzip']]
148
+ ];
149
+ for (const [probe, [cmd, ...args]] of managers) {
150
+ if (await which(probe)) {
151
+ log(`Installing PHP via ${probe} (you may be asked for your password)`);
152
+ return exec(cmd, args);
153
+ }
154
+ }
155
+ throw new Error('No supported package manager found. Install PHP 8.3+ manually.');
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Makes tools installed into TOOLS_HOME available in new terminals too, by
161
+ * adding them to the *user* PATH (no admin): the user Path environment
162
+ * variable on Windows, the shell rc file elsewhere. Returns a description of
163
+ * what was changed, or null if nothing needed changing.
164
+ */
165
+ async function persistPath() {
166
+ const dirs = [BIN_DIR, DOTNET_DIR].filter(d => fs.existsSync(d));
167
+ if (!dirs.length || process.env.BOILERCRAFT_NO_PATH_UPDATE) return null;
168
+ const hasDotnet = dirs.includes(DOTNET_DIR);
169
+
170
+ if (process.platform === 'win32') {
171
+ // Read-modify-write of the User scope only, so the (long) machine PATH is
172
+ // never copied into it and nothing gets truncated (unlike setx).
173
+ const ps = [
174
+ '$p = [Environment]::GetEnvironmentVariable("Path", "User"); if (-not $p) { $p = "" }',
175
+ `$add = @(${dirs.map(d => `"${d}"`).join(',')}) | Where-Object { ($p -split ";") -notcontains $_ }`,
176
+ 'if ($add) { [Environment]::SetEnvironmentVariable("Path", (($add + ($p -split ";" | Where-Object { $_ })) -join ";"), "User"); Write-Output "changed" }',
177
+ hasDotnet ? `[Environment]::SetEnvironmentVariable("DOTNET_ROOT", "${DOTNET_DIR}", "User")` : ''
178
+ ].join('; ');
179
+ await new Promise((resolve, reject) => {
180
+ const child = spawn('powershell.exe', ['-NoProfile', '-Command', ps], { stdio: 'ignore' });
181
+ child.on('error', reject);
182
+ child.on('close', resolve);
183
+ });
184
+ return `Added ${dirs.join(', ')} to your user PATH. Open a new terminal to use them.`;
185
+ }
186
+
187
+ const shell = path.basename(process.env.SHELL || '');
188
+ const rc = path.join(os.homedir(), shell === 'zsh' ? '.zshrc' : shell === 'bash' ? '.bashrc' : '.profile');
189
+ const marker = '# Added by BoilerCraft';
190
+ const existing = fs.existsSync(rc) ? fs.readFileSync(rc, 'utf8') : '';
191
+ if (existing.includes(marker)) return null;
192
+ const lines = [
193
+ '',
194
+ marker,
195
+ `export PATH="${dirs.join(':')}:$PATH"`,
196
+ hasDotnet ? `export DOTNET_ROOT="${DOTNET_DIR}"` : ''
197
+ ].filter(l => l !== '').join('\n');
198
+ fs.appendFileSync(rc, `\n${lines}\n`);
199
+ return `Added BoilerCraft tools to PATH in ${rc}. Run "source ${rc}" or open a new terminal.`;
200
+ }
201
+
202
+ const RECIPES = {
203
+ php: { name: 'PHP', install: installPhp },
204
+ composer: { name: 'Composer', install: installComposer, after: ['php'] },
205
+ dotnet: { name: '.NET SDK', install: installDotnet }
206
+ };
207
+
208
+ function canInstall(tool) {
209
+ return Object.prototype.hasOwnProperty.call(RECIPES, tool);
210
+ }
211
+
212
+ /**
213
+ * Installs the given missing requirements (from checkRequirements) in
214
+ * dependency order. Returns the names that could not be auto-installed.
215
+ */
216
+ async function installMissing(missing, log = console.log) {
217
+ const order = ['php', 'composer', 'dotnet'];
218
+ const todo = [...missing].sort((a, b) => order.indexOf(a.tool) - order.indexOf(b.tool));
219
+ const manual = [];
220
+ for (const m of todo) {
221
+ if (!canInstall(m.tool)) {
222
+ manual.push(m);
223
+ continue;
224
+ }
225
+ await RECIPES[m.tool].install({ major: m.major }, log);
226
+ }
227
+ const pathNote = await persistPath();
228
+ if (pathNote) log(pathNote);
229
+ return manual;
230
+ }
231
+
232
+ module.exports = { toolEnv, installMissing, persistPath, canInstall, RECIPES, TOOLS_HOME };
@@ -0,0 +1,115 @@
1
+ const axios = require('axios');
2
+
3
+ // Lists the framework versions a stack can be scaffolded with, straight from
4
+ // the upstream registry. A new major release shows up here automatically —
5
+ // no code change needed. Majors newer than the stack's "testedUpTo" are still
6
+ // offered, just flagged as untested until someone verifies the overlays.
7
+
8
+ const CACHE_TTL_MS = 60 * 60 * 1000;
9
+ const cache = new Map();
10
+
11
+ function majorOf(v) {
12
+ const m = /^v?(\d+)\./.exec(String(v));
13
+ return m ? parseInt(m[1], 10) : null;
14
+ }
15
+
16
+ function isStable(v) {
17
+ return !/(-|dev|alpha|beta|rc|preview|canary)/i.test(String(v));
18
+ }
19
+
20
+ function compareVersions(a, b) {
21
+ const pa = String(a).replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
22
+ const pb = String(b).replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
23
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
24
+ const diff = (pa[i] || 0) - (pb[i] || 0);
25
+ if (diff !== 0) return diff;
26
+ }
27
+ return 0;
28
+ }
29
+
30
+ // Collapses a flat list of version strings into { major -> newest stable }.
31
+ function newestPerMajor(versions, minMajor) {
32
+ const byMajor = new Map();
33
+ for (const v of versions) {
34
+ if (!isStable(v)) continue;
35
+ const major = majorOf(v);
36
+ if (major === null || major < minMajor) continue;
37
+ const current = byMajor.get(major);
38
+ if (!current || compareVersions(v, current) > 0) byMajor.set(major, v.replace(/^v/, ''));
39
+ }
40
+ return [...byMajor.entries()].map(([major, latest]) => ({ major, latest }));
41
+ }
42
+
43
+ const sources = {
44
+ async npm({ package: pkg, minMajor = 0 }) {
45
+ const res = await axios.get(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`, {
46
+ timeout: 5000,
47
+ headers: { Accept: 'application/vnd.npm.install-v1+json' } // abbreviated metadata, much smaller
48
+ });
49
+ return newestPerMajor(Object.keys(res.data.versions || {}), minMajor);
50
+ },
51
+
52
+ async packagist({ package: pkg, minMajor = 0 }) {
53
+ const res = await axios.get(`https://repo.packagist.org/p2/${pkg}.json`, { timeout: 5000 });
54
+ const releases = (res.data.packages && res.data.packages[pkg]) || [];
55
+ return newestPerMajor(releases.map(r => r.version), minMajor);
56
+ },
57
+
58
+ async dotnet({ minMajor = 0 }) {
59
+ const res = await axios.get(
60
+ 'https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json',
61
+ { timeout: 5000 }
62
+ );
63
+ return (res.data['releases-index'] || [])
64
+ .filter(r => r.product === '.NET')
65
+ .filter(r => r['support-phase'] === 'active' || r['support-phase'] === 'maintenance')
66
+ .map(r => ({
67
+ major: majorOf(r['channel-version']),
68
+ latest: r['latest-release'],
69
+ lts: r['release-type'] === 'lts'
70
+ }))
71
+ .filter(r => r.major !== null && r.major >= minMajor);
72
+ }
73
+ };
74
+
75
+ /**
76
+ * Returns [{ major, latest, tested, lts?, recommended }] newest-first.
77
+ * Falls back to the manifest's offline list when the registry is unreachable.
78
+ */
79
+ async function listVersions(stack) {
80
+ const spec = stack.versions;
81
+ const key = `${stack.id}`;
82
+ const hit = cache.get(key);
83
+ if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.list;
84
+
85
+ let list;
86
+ let offline = false;
87
+ try {
88
+ const fetcher = sources[spec.source];
89
+ if (!fetcher) throw new Error(`Unknown version source '${spec.source}'`);
90
+ list = await fetcher(spec);
91
+ if (!list.length) throw new Error('registry returned no versions');
92
+ } catch (err) {
93
+ console.warn(`[VersionCatalog] ${stack.id}: ${err.message}. Using offline list.`);
94
+ list = (spec.offline || []).map(major => ({ major, latest: `${major}.x` }));
95
+ offline = true;
96
+ }
97
+
98
+ const lts = new Set(spec.lts || []);
99
+ list = list
100
+ .map(v => ({
101
+ ...v,
102
+ lts: v.lts || lts.has(v.major),
103
+ tested: v.major <= spec.testedUpTo
104
+ }))
105
+ .sort((a, b) => b.major - a.major);
106
+
107
+ // Recommended = newest tested major; if nothing is tested, the newest one.
108
+ const recommended = list.find(v => v.tested) || list[0];
109
+ if (recommended) recommended.recommended = true;
110
+
111
+ if (!offline) cache.set(key, { at: Date.now(), list });
112
+ return list;
113
+ }
114
+
115
+ module.exports = { listVersions, majorOf, compareVersions };
@@ -0,0 +1,46 @@
1
+ const { generateProject } = require('./projectGenerator');
2
+ const { scaffold, getStack, parseMajor, RequirementError } = require('./engine/scaffoldEngine');
3
+
4
+ // Shared entry point for the web/desktop server and the CLI.
5
+
6
+ // The legacy generator expects the old per-stack version formats.
7
+ function toLegacyVersions(config) {
8
+ const v = config.versions || {};
9
+ const major = key => parseMajor(v[key]);
10
+ return {
11
+ laravel: major('laravel') ? `^${major('laravel')}.0` : v.laravel,
12
+ nextjs: major('nextjs') ? `^${major('nextjs')}.0.0` : v.nextjs,
13
+ nodeExpress: major('node-express') ? `^${major('node-express')}.0.0` : v.nodeExpress,
14
+ dotnet: major('dotnet-core') ? `net${major('dotnet-core')}.0` : v.dotnet
15
+ };
16
+ }
17
+
18
+ async function runLegacy(config, warning) {
19
+ const result = await generateProject({ ...config, versions: toLegacyVersions(config) }, config.outputDir || null);
20
+ return { ...result, engine: 'legacy', warnings: warning ? [warning] : [] };
21
+ }
22
+
23
+ /**
24
+ * Stacks with a manifest in stacks/ are scaffolded by the official-CLI engine.
25
+ * Everything else (e.g. raw-php) still uses the template generator. If the
26
+ * required toolchain is missing, fall back to templates unless the caller
27
+ * opted out with allowLegacyFallback: false.
28
+ */
29
+ async function generate(config, onLog = () => {}) {
30
+ const backend = config.backend || config.stack;
31
+ if (config.engine === 'legacy' || !getStack(backend)) return runLegacy(config);
32
+
33
+ try {
34
+ return await scaffold(config, config.outputDir || null, onLog);
35
+ } catch (err) {
36
+ if (err instanceof RequirementError && config.allowLegacyFallback !== false) {
37
+ const hint = err.missing.map(m => `${m.name}${m.install ? ` (${m.install})` : ''}`).join(', ');
38
+ const warning = `${err.message}. Used offline templates instead — install ${hint} for an official, version-exact scaffold.`;
39
+ onLog(`WARNING: ${warning}`);
40
+ return runLegacy(config, warning);
41
+ }
42
+ throw err;
43
+ }
44
+ }
45
+
46
+ module.exports = { generate, toLegacyVersions };