create-skateboard-app 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/bin/cli.js +426 -0
  4. package/package.json +36 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 stevederico
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/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # Create Skateboard App
2
+
3
+ The fastest way to create a new [Skateboard](https://github.com/stevederico/skateboard) app.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx create-skateboard-app
9
+ cd my-app
10
+ npm run dev
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Interactive Mode (Default)
16
+ ```bash
17
+ npx create-skateboard-app
18
+ ```
19
+
20
+ ### With Project Name
21
+ ```bash
22
+ npx create-skateboard-app my-app
23
+ ```
24
+
25
+ ## What You Get
26
+
27
+ - âš›ī¸ React v19
28
+ - 🎨 TailwindCSS v4
29
+ - 🧩 Shadcn/ui components
30
+ - ⚡ Vite build tool
31
+ - 🔐 Authentication ready
32
+ - đŸ’ŗ Stripe integration
33
+ - 🌙 Dark mode support
34
+ - 📱 Mobile responsive
35
+ - đŸ›Ŗī¸ React Router
36
+ - đŸ“Ļ Modern JavaScript
37
+
38
+ ## Features Included
39
+
40
+ - Sign up/Sign in pages
41
+ - Landing page
42
+ - Settings page with Home and Other views
43
+ - Legal pages (Privacy, Terms, EULA)
44
+ - 404 error handling
45
+ - Protected routes
46
+ - Mobile tab bar
47
+ - Customizable constants
48
+
49
+ ## Requirements
50
+
51
+ - Node.js 22.5+
52
+ - npm or yarn
53
+ - git, curl, or npx (for template download)
54
+
55
+ ## Contributing
56
+
57
+ Contributions are welcome! Please check out the [Skateboard repository](https://github.com/stevederico/skateboard) for more information.
58
+
59
+ ## License
60
+
61
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from 'child_process';
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
5
+ import { join } from 'path';
6
+ import https from 'https';
7
+ import { createWriteStream } from 'fs';
8
+ import { createInterface } from 'readline';
9
+
10
+ // Simple colors using ANSI codes
11
+ const colors = {
12
+ green: '\x1b[32m',
13
+ red: '\x1b[31m',
14
+ yellow: '\x1b[33m',
15
+ blue: '\x1b[34m',
16
+ cyan: '\x1b[36m',
17
+ magenta: '\x1b[35m',
18
+ reset: '\x1b[0m',
19
+ bold: '\x1b[1m'
20
+ };
21
+
22
+ function log(message, color = 'reset') {
23
+ console.log(`${colors[color]}${message}${colors.reset}`);
24
+ }
25
+
26
+ function error(message) {
27
+ log(`❌ ${message}`, 'red');
28
+ }
29
+
30
+ function success(message) {
31
+ log(`✅ ${message}`, 'green');
32
+ }
33
+
34
+ function info(message) {
35
+ log(`â„šī¸ ${message}`, 'blue');
36
+ }
37
+
38
+ function checkCommand(command) {
39
+ try {
40
+ execSync(`which ${command}`, { stdio: 'pipe' });
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ async function downloadTemplate(projectName) {
48
+ // Try multiple methods in order of preference
49
+ const methods = [
50
+ {
51
+ name: 'degit',
52
+ check: () => checkCommand('npx'),
53
+ execute: () => execSync(`npx degit stevederico/skateboard ${projectName}`, {
54
+ stdio: 'pipe',
55
+ timeout: 30000
56
+ })
57
+ },
58
+ {
59
+ name: 'git clone',
60
+ check: () => checkCommand('git'),
61
+ execute: () => {
62
+ execSync(`git clone --depth 1 https://github.com/stevederico/skateboard.git ${projectName}`, {
63
+ stdio: 'pipe',
64
+ timeout: 30000
65
+ });
66
+ // Remove .git directory to avoid including git history
67
+ execSync(`rm -rf ${projectName}/.git`, { stdio: 'pipe' });
68
+ }
69
+ },
70
+ {
71
+ name: 'curl + unzip',
72
+ check: () => checkCommand('curl') && checkCommand('unzip'),
73
+ execute: () => {
74
+ execSync(`curl -L https://github.com/stevederico/skateboard/archive/refs/heads/master.zip -o temp.zip`, {
75
+ stdio: 'pipe',
76
+ timeout: 30000
77
+ });
78
+ execSync(`unzip -q temp.zip`, { stdio: 'pipe' });
79
+ execSync(`mv skateboard-master ${projectName}`, { stdio: 'pipe' });
80
+ execSync(`rm temp.zip`, { stdio: 'pipe' });
81
+ }
82
+ }
83
+ ];
84
+
85
+ for (const method of methods) {
86
+ if (!method.check()) {
87
+ log(`${method.name} not available, skipping...`, 'yellow');
88
+ continue;
89
+ }
90
+
91
+ try {
92
+ info(`Trying ${method.name}...`);
93
+ method.execute();
94
+ success(`Template downloaded via ${method.name}`);
95
+ return;
96
+ } catch (err) {
97
+ log(`${method.name} failed, trying next method...`, 'yellow');
98
+ }
99
+ }
100
+
101
+ throw new Error('All download methods failed. Please ensure you have git, curl, or npx available and check your internet connection.');
102
+ }
103
+
104
+ // Interactive prompt functions
105
+ function ask(question, defaultValue = '') {
106
+ const rl = createInterface({
107
+ input: process.stdin,
108
+ output: process.stdout
109
+ });
110
+
111
+ return new Promise((resolve) => {
112
+ const prompt = defaultValue
113
+ ? `${colors.cyan}${question}${colors.reset} ${colors.yellow}(${defaultValue})${colors.reset}: `
114
+ : `${colors.cyan}${question}${colors.reset}: `;
115
+
116
+ rl.question(prompt, (answer) => {
117
+ rl.close();
118
+ resolve(answer.trim() || defaultValue);
119
+ });
120
+ });
121
+ }
122
+
123
+ function askChoice(question, choices, defaultChoice = 0) {
124
+ return new Promise((resolve) => {
125
+ let currentChoice = defaultChoice;
126
+
127
+ const displayMenu = () => {
128
+ // Clear screen and show menu
129
+ console.clear();
130
+ log(`\n${colors.cyan}${question}${colors.reset}\n`);
131
+ choices.forEach((choice, index) => {
132
+ const marker = index === currentChoice ? '●' : '○';
133
+ const color = index === currentChoice ? 'green' : 'reset';
134
+ const highlight = index === currentChoice ? colors.bold : '';
135
+ log(` ${colors[color]}${highlight}${marker} ${choice.label}${colors.reset}`);
136
+ });
137
+ log(`\n${colors.yellow}Use ↑/↓ arrows to navigate, Enter to select${colors.reset}`);
138
+ };
139
+
140
+ displayMenu();
141
+
142
+ // Enable raw mode to capture arrow keys
143
+ process.stdin.setRawMode(true);
144
+ process.stdin.resume();
145
+ process.stdin.setEncoding('utf8');
146
+
147
+ const handleKeypress = (key) => {
148
+ switch (key) {
149
+ case '\u001b[A': // Up arrow
150
+ currentChoice = currentChoice > 0 ? currentChoice - 1 : choices.length - 1;
151
+ displayMenu();
152
+ break;
153
+ case '\u001b[B': // Down arrow
154
+ currentChoice = currentChoice < choices.length - 1 ? currentChoice + 1 : 0;
155
+ displayMenu();
156
+ break;
157
+ case '\r': // Enter
158
+ case '\n':
159
+ process.stdin.setRawMode(false);
160
+ process.stdin.pause();
161
+ process.stdin.removeListener('data', handleKeypress);
162
+ resolve(choices[currentChoice]);
163
+ break;
164
+ case '\u0003': // Ctrl+C
165
+ process.exit(0);
166
+ break;
167
+ }
168
+ };
169
+
170
+ process.stdin.on('data', handleKeypress);
171
+ });
172
+ }
173
+
174
+ async function askYesNo(question, defaultYes = true) {
175
+ const defaultText = defaultYes ? 'Y/n' : 'y/N';
176
+ const answer = await ask(`${question} (${defaultText})`, defaultYes ? 'y' : 'n');
177
+ return answer.toLowerCase().startsWith('y');
178
+ }
179
+
180
+ async function collectProjectConfig(projectName) {
181
+ log(`\n${colors.bold}Let's configure your Skateboard app!${colors.reset}\n`);
182
+
183
+ // App name
184
+ const appName = await ask('App name', projectName.split('-').map(word =>
185
+ word.charAt(0).toUpperCase() + word.slice(1)
186
+ ).join(' '));
187
+
188
+ // Tagline
189
+ const tagline = await ask('App tagline', 'Try Something New');
190
+
191
+ // App color selection
192
+ const colorChoices = [
193
+ { label: 'đŸ”ĩ Blue', value: 'blue' },
194
+ { label: '💚 Green', value: 'green' },
195
+ { label: 'đŸŸŖ Purple', value: 'purple' },
196
+ { label: '🔴 Red', value: 'red' },
197
+ { label: '🟠 Orange', value: 'orange' },
198
+ { label: '🟡 Yellow', value: 'yellow' },
199
+ { label: '🩷 Pink', value: 'pink' },
200
+ { label: 'đŸŠĩ Cyan', value: 'cyan' }
201
+ ];
202
+
203
+ const selectedColor = await askChoice('Choose your app color:', colorChoices);
204
+
205
+ // App icon
206
+ const iconChoices = [
207
+ { label: '⌘ Command', value: 'command' },
208
+ { label: '🏠 House', value: 'house' },
209
+ { label: '⚡ Zap', value: 'zap' },
210
+ { label: '🚀 Rocket', value: 'rocket' },
211
+ { label: '💎 Diamond', value: 'diamond' },
212
+ { label: 'đŸŽ¯ Target', value: 'target' },
213
+ { label: 'đŸ”Ĩ Flame', value: 'flame' },
214
+ { label: '⭐ Star', value: 'star' }
215
+ ];
216
+
217
+ const selectedIcon = await askChoice('Choose an app icon:', iconChoices);
218
+
219
+ // Backend URLs
220
+ const backendURL = await ask('Production backend URL', 'https://api.example.com');
221
+ const devBackendURL = await ask('Development backend URL', 'http://localhost:8000');
222
+
223
+ // App pages configuration
224
+ log(`\n${colors.cyan}Configure your app pages:${colors.reset}`);
225
+ const pages = [];
226
+
227
+ const addDefaultPages = await askYesNo('Add default pages (Home, Other)?', true);
228
+ if (addDefaultPages) {
229
+ pages.push(
230
+ { title: 'Home', url: 'home', icon: 'house' },
231
+ { title: 'Other', url: 'other', icon: 'inbox' }
232
+ );
233
+ }
234
+
235
+ const addMorePages = await askYesNo('Add more custom pages?', false);
236
+ if (addMorePages) {
237
+ let addAnother = true;
238
+ while (addAnother) {
239
+ const pageTitle = await ask('Page title');
240
+ const pageUrl = await ask('Page URL', pageTitle.toLowerCase().replace(/\s+/g, '-'));
241
+ const pageIcon = await ask('Page icon (lucide icon name)', 'circle');
242
+
243
+ pages.push({
244
+ title: pageTitle,
245
+ url: pageUrl,
246
+ icon: pageIcon
247
+ });
248
+
249
+ addAnother = await askYesNo('Add another page?', false);
250
+ }
251
+ }
252
+
253
+ // Company name (after pages configuration)
254
+ const companyName = await ask('Company name', 'Your Company');
255
+
256
+ // Installation preferences
257
+ const installDeps = await askYesNo('Install dependencies automatically?', true);
258
+ const initGit = await askYesNo('Initialize git repository?', true);
259
+
260
+ return {
261
+ companyName,
262
+ appName,
263
+ tagline,
264
+ appColor: selectedColor.value,
265
+ appIcon: selectedIcon.value,
266
+ backendURL,
267
+ devBackendURL,
268
+ pages,
269
+ installDeps,
270
+ initGit
271
+ };
272
+ }
273
+
274
+ function showHelp() {
275
+ log(`
276
+ ${colors.bold}🛹 Create Skateboard App${colors.reset}
277
+
278
+ ${colors.cyan}Usage:${colors.reset}
279
+ npx create-skateboard-app
280
+
281
+ ${colors.cyan}Arguments:${colors.reset}
282
+ project-name Optional project directory name (will prompt if not provided)
283
+
284
+ ${colors.cyan}Options:${colors.reset}
285
+ --help, -h Show this help message
286
+ --version, -v Show version number
287
+
288
+ ${colors.cyan}Examples:${colors.reset}
289
+ npx create-skateboard-app # Interactive mode
290
+ npx create-skateboard-app my-app # With project name
291
+ npx create-skateboard-app awesome-project # With custom name
292
+ `, 'reset');
293
+ }
294
+
295
+ function showVersion() {
296
+ const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
297
+ log(`v${packageJson.version}`, 'green');
298
+ }
299
+
300
+ async function main() {
301
+ // Get project name from command line
302
+ const args = process.argv.slice(2);
303
+ let projectName = args[0];
304
+
305
+ // Handle help and version flags
306
+ if (args.includes('--help') || args.includes('-h')) {
307
+ showHelp();
308
+ process.exit(0);
309
+ }
310
+
311
+ if (args.includes('--version') || args.includes('-v')) {
312
+ showVersion();
313
+ process.exit(0);
314
+ }
315
+
316
+ // If no project name provided, ask for it
317
+ if (!projectName) {
318
+ log(`\n${colors.bold}🛹 Welcome to Skateboard App Creator!${colors.reset}\n`);
319
+ projectName = await ask('What is the name of your project?', 'my-skateboard-app');
320
+ }
321
+
322
+ // Validate project name
323
+ if (!/^[a-zA-Z0-9-_]+$/.test(projectName)) {
324
+ error('Project name can only contain letters, numbers, hyphens, and underscores');
325
+ process.exit(1);
326
+ }
327
+
328
+ // Check if directory already exists
329
+ if (existsSync(projectName)) {
330
+ error(`Directory '${projectName}' already exists`);
331
+ process.exit(1);
332
+ }
333
+
334
+ try {
335
+ log(`\n${colors.bold}🛹 Creating Skateboard app: ${projectName}${colors.reset}\n`);
336
+
337
+ // Step 1: Download the template with fallback methods
338
+ info('Downloading template...');
339
+ await downloadTemplate(projectName);
340
+
341
+ // Step 2: Collect user configuration
342
+ const config = await collectProjectConfig(projectName);
343
+
344
+ // Step 3: Update package.json
345
+ info('Updating package.json...');
346
+ const packageJsonPath = join(projectName, 'package.json');
347
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
348
+ packageJson.name = projectName;
349
+ packageJson.version = '0.1.0';
350
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
351
+ success('Package.json updated');
352
+
353
+ // Step 4: Update constants.json with user configuration
354
+ info('Configuring app settings...');
355
+ const constantsPath = join(projectName, 'src', 'constants.json');
356
+ if (existsSync(constantsPath)) {
357
+ const constants = JSON.parse(readFileSync(constantsPath, 'utf8'));
358
+ constants.companyName = config.companyName;
359
+ constants.appName = config.appName;
360
+ constants.tagline = config.tagline;
361
+ constants.appIcon = config.appIcon;
362
+ constants.backendURL = config.backendURL;
363
+ constants.devBackendURL = config.devBackendURL;
364
+ constants.pages = config.pages;
365
+ writeFileSync(constantsPath, JSON.stringify(constants, null, 4));
366
+ success('App configuration updated');
367
+ }
368
+
369
+ // Step 5: Update app color in styles.css
370
+ info('Setting app color...');
371
+ const stylesPath = join(projectName, 'src', 'assets', 'styles.css');
372
+ if (existsSync(stylesPath)) {
373
+ let stylesContent = readFileSync(stylesPath, 'utf8');
374
+ // Replace the app color in the @theme block
375
+ stylesContent = stylesContent.replace(
376
+ /--color-app:\s*var\(--color-[^)]+\);/,
377
+ `--color-app: var(--color-${config.appColor}-500);`
378
+ );
379
+ writeFileSync(stylesPath, stylesContent);
380
+ success(`App color set to ${config.appColor}`);
381
+ }
382
+
383
+ // Step 6: Install dependencies (if requested)
384
+ if (config.installDeps) {
385
+ info('Installing dependencies...');
386
+ execSync(`cd ${projectName} && npm install`, { stdio: 'inherit' });
387
+ success('Dependencies installed');
388
+ }
389
+
390
+ // Step 7: Initialize git (if requested)
391
+ if (config.initGit) {
392
+ info('Initializing git repository...');
393
+ execSync(`cd ${projectName} && git init`, { stdio: 'pipe' });
394
+ success('Git repository initialized');
395
+ }
396
+
397
+ // Success message
398
+ log(`\n${colors.bold}${colors.green}🎉 Success! Created ${config.appName}${colors.reset}\n`);
399
+
400
+ // Change to the new project directory
401
+ process.chdir(projectName);
402
+ info(`Switched to ${projectName} directory`);
403
+
404
+ log('Next steps:', 'yellow');
405
+ if (!config.installDeps) {
406
+ log(` npm install`);
407
+ }
408
+ log(` npm run dev`);
409
+ log(`\n${colors.cyan}Your app is configured with:${colors.reset}`);
410
+ log(` đŸĸ Company: ${config.companyName}`);
411
+ log(` 📱 App: ${config.appName}`);
412
+ log(` đŸ’Ŧ Tagline: ${config.tagline}`);
413
+ log(` 🎨 Color: ${config.appColor}`);
414
+ log(` đŸŽ¯ Icon: ${config.appIcon}`);
415
+ log(` 📄 Pages: ${config.pages.map(p => p.title).join(', ')}`);
416
+ log(` 🌐 Backend: ${config.backendURL}`);
417
+ log(`\n${colors.magenta}You're now in the ${projectName} directory!${colors.reset}`);
418
+ log(`${colors.yellow}Run 'npm run dev' to begin development 🛹${colors.reset}\n`);
419
+
420
+ } catch (err) {
421
+ error(`Failed to create project: ${err.message}`);
422
+ process.exit(1);
423
+ }
424
+ }
425
+
426
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "create-skateboard-app",
3
+ "version": "1.0.0",
4
+ "description": "Create a new Skateboard app with React, TailwindCSS, and more",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-skateboard-app": "bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [
14
+ "react",
15
+ "boilerplate",
16
+ "template",
17
+ "tailwindcss",
18
+ "vite",
19
+ "skateboard",
20
+ "cli"
21
+ ],
22
+ "author": "stevederico",
23
+ "license": "MIT",
24
+ "dependencies": {},
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/stevederico/create-skateboard-app.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/stevederico/create-skateboard-app/issues"
34
+ },
35
+ "homepage": "https://github.com/stevederico/create-skateboard-app#readme"
36
+ }