create-thunderous 0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan DeWitt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bin/index.js ADDED
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { Command } from 'commander';
7
+ import { input, confirm, select } from '@inquirer/prompts';
8
+ import stringWidth from 'string-width';
9
+ import chalk from 'chalk';
10
+ import { emojify } from 'node-emoji';
11
+ import { shimConsoleLog } from 'emoji-space-shim';
12
+
13
+ const shim = shimConsoleLog();
14
+ process.on('exit', () => {
15
+ shim.restore();
16
+ });
17
+
18
+ const DEFAULT_NAME = 'Thunderous Project';
19
+ const DEFAULT_PACKAGE_MANAGER = 'pnpm';
20
+ const PACKAGE_MANAGERS = ['pnpm', 'npm', 'yarn'];
21
+ const PLACEHOLDER = '{{ APP_NAME }}';
22
+
23
+ const program = new Command();
24
+
25
+ program
26
+ .name('create-thunderous')
27
+ .description('Scaffold a new Thunderous project')
28
+ .version('0.0.0')
29
+ .argument('[project-name]', 'project name, or "." to scaffold in the current directory')
30
+ .option('--current-dir', 'scaffold in the current directory')
31
+ .option('-p, --package-manager <name>', `package manager to use (${PACKAGE_MANAGERS.join(', ')})`)
32
+ .showHelpAfterError('(add --help for additional information)')
33
+ .configureOutput({
34
+ outputError: (str, write) => write(`\x1b[31m${str}\x1b[0m`),
35
+ });
36
+
37
+ program.parse();
38
+
39
+ const options = program.opts();
40
+ const rawNameArg = program.args[0];
41
+
42
+ await main({
43
+ rawNameArg,
44
+ currentDir: Boolean(options.currentDir),
45
+ packageManager: options.packageManager,
46
+ });
47
+
48
+ async function main({ rawNameArg, currentDir, packageManager }) {
49
+ const cwd = process.cwd();
50
+ const currentDirName = path.basename(cwd);
51
+ const dotMeansCurrentDir = rawNameArg === '.';
52
+
53
+ let projectName = dotMeansCurrentDir ? currentDirName : rawNameArg;
54
+
55
+ if (!projectName) {
56
+ projectName = await input({
57
+ message: 'Project name',
58
+ default: DEFAULT_NAME,
59
+ });
60
+ }
61
+
62
+ projectName = projectName.trim() || DEFAULT_NAME;
63
+
64
+ const namesMatch = looselyMatches(projectName, currentDirName);
65
+
66
+ let targetDir = cwd;
67
+ let createdFolder = false;
68
+
69
+ if (currentDir || dotMeansCurrentDir) {
70
+ targetDir = cwd;
71
+ } else if (!namesMatch) {
72
+ targetDir = path.join(cwd, toKebabCase(projectName));
73
+ createdFolder = true;
74
+ }
75
+
76
+ ensureTargetIsUsable(targetDir, createdFolder);
77
+
78
+ const chosenPackageManager = await choosePackageManager(packageManager);
79
+
80
+ const templateDir = path.resolve(getScriptDir(), '../reference-project');
81
+ if (!fs.existsSync(templateDir)) {
82
+ fail(`Could not find reference-project at: ${templateDir}`);
83
+ }
84
+
85
+ logStep('Scaffolding project files');
86
+ copyDirectoryContents(templateDir, targetDir);
87
+ replacePlaceholderInDirectory(targetDir, PLACEHOLDER, projectName);
88
+
89
+ logStep(`Preparing ${chosenPackageManager}`);
90
+ ensurePackageManagerInstalled(chosenPackageManager);
91
+
92
+ logStep(`Installing dependencies with ${chosenPackageManager}`);
93
+ runInstall(chosenPackageManager, targetDir);
94
+
95
+ const shouldInitializeGit = await confirm({
96
+ message: 'Initialize git repository?',
97
+ default: true,
98
+ });
99
+
100
+ if (shouldInitializeGit) {
101
+ logStep('Initializing git repository');
102
+ runCommand('git', ['init'], { cwd: targetDir });
103
+ runCommand('git', ['add', '.'], { cwd: targetDir });
104
+ runCommand('git', ['commit', '-m', 'Initial commit'], { cwd: targetDir });
105
+ }
106
+
107
+ printSuccess(projectName, targetDir, cwd, chosenPackageManager);
108
+ }
109
+
110
+ async function choosePackageManager(explicitValue) {
111
+ if (explicitValue) {
112
+ const normalized = explicitValue.toLowerCase();
113
+ if (!PACKAGE_MANAGERS.includes(normalized)) {
114
+ fail(`Unsupported package manager "${explicitValue}". Use one of: ${PACKAGE_MANAGERS.join(', ')}`);
115
+ }
116
+ return normalized;
117
+ }
118
+
119
+ return await select({
120
+ message: 'Which package manager would you like to use?',
121
+ default: DEFAULT_PACKAGE_MANAGER,
122
+ choices: [
123
+ { name: 'pnpm', value: 'pnpm' },
124
+ { name: 'npm', value: 'npm' },
125
+ { name: 'yarn', value: 'yarn' },
126
+ ],
127
+ });
128
+ }
129
+
130
+ function normalizeForComparison(value) {
131
+ return value
132
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
133
+ .replace(/[_-]+/g, ' ')
134
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
135
+ .toLowerCase()
136
+ .trim()
137
+ .replace(/\s+/g, ' ');
138
+ }
139
+
140
+ function looselyMatches(a, b) {
141
+ return normalizeForComparison(a) === normalizeForComparison(b);
142
+ }
143
+
144
+ function toKebabCase(value) {
145
+ return normalizeForComparison(value).replace(/\s+/g, '-');
146
+ }
147
+
148
+ function ensureTargetIsUsable(targetDir, createdFolder) {
149
+ if (!fs.existsSync(targetDir)) {
150
+ fs.mkdirSync(targetDir, { recursive: true });
151
+ return;
152
+ }
153
+
154
+ const entries = fs.readdirSync(targetDir);
155
+ if (entries.length > 0 && createdFolder) {
156
+ fail(`Target directory already exists and is not empty: ${targetDir}`);
157
+ }
158
+ }
159
+
160
+ function copyDirectoryContents(sourceDir, targetDir) {
161
+ fs.mkdirSync(targetDir, { recursive: true });
162
+
163
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
164
+ const sourcePath = path.join(sourceDir, entry.name);
165
+ const targetPath = path.join(targetDir, entry.name);
166
+
167
+ if (entry.isDirectory()) {
168
+ fs.cpSync(sourcePath, targetPath, { recursive: true });
169
+ } else {
170
+ fs.copyFileSync(sourcePath, targetPath);
171
+ }
172
+ }
173
+ }
174
+
175
+ function replacePlaceholderInDirectory(dir, searchValue, replacementValue) {
176
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
177
+ const fullPath = path.join(dir, entry.name);
178
+
179
+ if (entry.isDirectory()) {
180
+ replacePlaceholderInDirectory(fullPath, searchValue, replacementValue);
181
+ continue;
182
+ }
183
+
184
+ if (!entry.isFile()) continue;
185
+ if (looksBinary(fullPath)) continue;
186
+
187
+ const original = fs.readFileSync(fullPath, 'utf8');
188
+ const updated = original.split(searchValue).join(replacementValue);
189
+
190
+ if (updated !== original) {
191
+ fs.writeFileSync(fullPath, updated, 'utf8');
192
+ }
193
+ }
194
+ }
195
+
196
+ function looksBinary(filePath) {
197
+ const buffer = fs.readFileSync(filePath);
198
+ const sampleSize = Math.min(buffer.length, 8000);
199
+
200
+ for (let i = 0; i < sampleSize; i++) {
201
+ if (buffer[i] === 0) return true;
202
+ }
203
+
204
+ return false;
205
+ }
206
+
207
+ function ensurePackageManagerInstalled(packageManager) {
208
+ if (hasCommand(packageManager)) return;
209
+
210
+ if (packageManager === 'npm') {
211
+ fail('npm is not available on this system. Install Node.js first, since npm is bundled with Node.');
212
+ }
213
+
214
+ if (!hasCommand('corepack')) {
215
+ if (!hasCommand('npm')) {
216
+ fail(
217
+ `${packageManager} is not installed, and Corepack is unavailable. Install Node.js/npm or install ${packageManager} manually.`,
218
+ );
219
+ }
220
+
221
+ logSubstep('Installing Corepack');
222
+ runCommand('npm', ['install', '-g', 'corepack@latest']);
223
+ }
224
+
225
+ logSubstep(`Enabling Corepack shim for ${packageManager}`);
226
+ runCommand('corepack', ['enable', packageManager]);
227
+
228
+ logSubstep(`Installing ${packageManager} via Corepack`);
229
+ runCommand('corepack', ['install', '-g', `${packageManager}@latest`]);
230
+
231
+ if (!hasCommand(packageManager)) {
232
+ fail(`Failed to make ${packageManager} available on PATH.`);
233
+ }
234
+ }
235
+
236
+ function runInstall(packageManager, cwd) {
237
+ if (packageManager === 'npm') {
238
+ runCommand('npm', ['install'], { cwd });
239
+ return;
240
+ }
241
+
242
+ if (packageManager === 'pnpm') {
243
+ runCommand('pnpm', ['install'], { cwd });
244
+ return;
245
+ }
246
+
247
+ if (packageManager === 'yarn') {
248
+ runCommand('yarn', ['install'], { cwd });
249
+ return;
250
+ }
251
+
252
+ fail(`Unsupported package manager: ${packageManager}`);
253
+ }
254
+
255
+ function hasCommand(command) {
256
+ const result = spawnSync(command, ['--version'], {
257
+ stdio: 'ignore',
258
+ shell: process.platform === 'win32',
259
+ });
260
+
261
+ return result.status === 0;
262
+ }
263
+
264
+ function runCommand(command, args, options = {}) {
265
+ const result = spawnSync(command, args, {
266
+ stdio: 'inherit',
267
+ cwd: options.cwd,
268
+ shell: process.platform === 'win32',
269
+ });
270
+
271
+ if (result.error) {
272
+ fail(`Failed to run "${command} ${args.join(' ')}": ${result.error.message}`);
273
+ }
274
+
275
+ if (result.status !== 0) {
276
+ fail(`Command failed: ${command} ${args.join(' ')}`);
277
+ }
278
+ }
279
+
280
+ function getScriptDir() {
281
+ return path.dirname(fileURLToPath(import.meta.url));
282
+ }
283
+
284
+ function stripAnsi(value) {
285
+ return value.replace(/\x1b\[[0-9;]*m/g, '');
286
+ }
287
+
288
+ function visibleWidth(value) {
289
+ return stringWidth(stripAnsi(value));
290
+ }
291
+
292
+ function printSuccess(projectName, targetDir, originalCwd, packageManager) {
293
+ const lines = [
294
+ `${chalk.blue(emojify(':cloud_with_lightning:'))} ${chalk.magenta('Thunderous project created successfully!')} ${chalk.blue(emojify(':cloud_with_lightning:'))}`,
295
+ '',
296
+ ` ${chalk.bold('Name:')} ${chalk.blue(projectName)}`,
297
+ ` ${chalk.bold('Package manager:')} ${chalk.blue(packageManager)}`,
298
+ ` ${chalk.bold('Location:')} ${chalk.underline(chalk.blue(targetDir))}`,
299
+ '',
300
+ ` ${chalk.bold(targetDir !== originalCwd ? 'Next steps:' : 'Next step:')}`,
301
+ ...(targetDir !== originalCwd ? [chalk.blue(` cd ${path.relative(originalCwd, targetDir) || '.'}`)] : []),
302
+ chalk.blue(` ${packageManager} dev`),
303
+ ];
304
+
305
+ printBox(lines, {
306
+ borderColor: 'magenta',
307
+ textColor: 'white',
308
+ padding: 2,
309
+ marginBottom: 1,
310
+ });
311
+ }
312
+
313
+ function printBox(lines, options = {}) {
314
+ const { borderColor = '', textColor = '', padding = 0, marginTop = 1, marginBottom = 0 } = options;
315
+
316
+ const contentWidth = Math.max(...lines.map((line) => visibleWidth(line)), 0);
317
+ const innerWidth = contentWidth + padding * 2;
318
+ const horizontalWidth = innerWidth + 2;
319
+ const bc = chalk[borderColor];
320
+ const tc = chalk[textColor];
321
+
322
+ const top = bc(`╭${'─'.repeat(horizontalWidth)}──╮`);
323
+ const bottom = bc(`╰${'─'.repeat(horizontalWidth)}──╯`);
324
+ const emptyLine = `${bc('│')} ${' '.repeat(innerWidth)} ${bc('│')}`;
325
+
326
+ if (marginTop > 0) {
327
+ process.stdout.write('\n'.repeat(marginTop));
328
+ }
329
+
330
+ console.log(top);
331
+ console.log(emptyLine);
332
+
333
+ for (const line of lines) {
334
+ const width = visibleWidth(line);
335
+ const rightPad = innerWidth - width - padding;
336
+ const paddedLine = ' '.repeat(padding) + line + ' '.repeat(Math.max(0, rightPad));
337
+
338
+ console.log(`${bc('│')} ${tc(paddedLine)} ${bc('│')}`);
339
+ }
340
+
341
+ console.log(emptyLine);
342
+ console.log(bottom);
343
+
344
+ if (marginBottom > 0) {
345
+ process.stdout.write('\n'.repeat(marginBottom));
346
+ }
347
+ }
348
+
349
+ function logStep(message) {
350
+ console.log('');
351
+ console.log(`\x1b[36m▶\x1b[0m ${message}`);
352
+ }
353
+
354
+ function logSubstep(message) {
355
+ console.log(` \x1b[90m•\x1b[0m ${message}`);
356
+ }
357
+
358
+ function fail(message) {
359
+ console.error('');
360
+ console.error(`\x1b[31m✖ ${message}\x1b[0m`);
361
+ process.exit(1);
362
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "create-thunderous",
3
+ "version": "0.0.0",
4
+ "description": "A CLI tool to scaffold a new project using the Thunderous stack.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "create-thunderous": "./bin/index.js"
8
+ },
9
+ "keywords": [
10
+ "thunderous",
11
+ "create",
12
+ "starter",
13
+ "template"
14
+ ],
15
+ "author": "Jonathan DeWitt <jon.dewitt@thunder.solutions>",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "@inquirer/prompts": "^8.3.2",
19
+ "chalk": "^5.6.2",
20
+ "commander": "^14.0.3",
21
+ "emoji-space-shim": "^0.1.7",
22
+ "node-emoji": "^2.2.0",
23
+ "string-width": "^8.2.0"
24
+ }
25
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "useTabs": true,
3
+ "printWidth": 120,
4
+ "trailingComma": "all",
5
+ "quoteProps": "consistent",
6
+ "singleQuote": true,
7
+ "endOfLine": "auto",
8
+ "overrides": [
9
+ {
10
+ "files": "*.md",
11
+ "options": {
12
+ "tabWidth": 2,
13
+ "useTabs": false
14
+ }
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,7 @@
1
+ # Thunderous Project
2
+
3
+ This project was generated using `create-thunderous`. By default, it includes a simple structure with examples and comments to help you get started.
4
+
5
+ For more information, see the [Thunderous documentation](https://thunderous.dev/docs).
6
+
7
+ > **Note**: The documentation may not be up to date yet, and thus may not include information about the full Thunderous stack. Please refer to the [source code](https://github.com/thunder-solutions/thunderous/tree/trunk) for the latest information.
@@ -0,0 +1,16 @@
1
+ import js from '@eslint/js';
2
+ import ts from 'typescript-eslint';
3
+ import { defineConfig } from 'eslint/config';
4
+
5
+ export default defineConfig([
6
+ js.configs.recommended,
7
+ ...ts.configs.recommended,
8
+ {
9
+ rules: {
10
+ '@typescript-eslint/no-explicit-any': 'error',
11
+ },
12
+ },
13
+ {
14
+ ignores: ['package-lock.json', 'node_modules/', 'dist/', '**/*.tmp.*'],
15
+ },
16
+ ]);
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "{{ APP_NAME }}",
3
+ "version": "0.0.0",
4
+ "description": "A demo to showcase Thunderous server capabilities.",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "main": "index.js",
9
+ "scripts": {
10
+ "start": "serve dist",
11
+ "dev": "thunderous dev",
12
+ "build": "thunderous build",
13
+ "lint": "eslint .",
14
+ "lint:fix": "eslint . --fix",
15
+ "format": "prettier --check .",
16
+ "format:fix": "prettier --write . --list-different",
17
+ "typecheck": "tsc --noEmit",
18
+ "test": "echo \"Error: no test specified\" && exit 1"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/css": "^0.13.0",
22
+ "@eslint/js": "^9.38.0",
23
+ "@eslint/json": "^0.13.2",
24
+ "@total-typescript/ts-reset": "^0.6.1",
25
+ "@types/express": "^5.0.3",
26
+ "@types/node": "^24.9.1",
27
+ "@typescript-eslint/eslint-plugin": "^8.46.2",
28
+ "eslint": "^9.38.0",
29
+ "prettier": "^3.6.2",
30
+ "serve": "^14.2.5",
31
+ "thunderous-server": "^0.0.0",
32
+ "tsx": "^4.20.6",
33
+ "typescript": "^5.9.3",
34
+ "typescript-eslint": "^8.46.2",
35
+ "undici-types": "^7.16.0"
36
+ },
37
+ "dependencies": {
38
+ "thunderous": "^2.4.1",
39
+ "thunderous-csr": "^0.0.0"
40
+ }
41
+ }