@iselect/select 1.0.1 → 1.0.4
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/lib/commands/build.js +164 -49
- package/lib/commands/deploy.js +100 -20
- package/lib/commands/init.js +295 -150
- package/package.json +1 -1
package/lib/commands/build.js
CHANGED
|
@@ -55,47 +55,116 @@ async function build(target, options) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// ====================
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
58
|
+
// ==================== FRAMEWORK SIGNATURES (Vercel-style) ====================
|
|
59
|
+
const FRAMEWORK_SIGNATURES = {
|
|
60
|
+
// Next.js
|
|
61
|
+
'next.config.js': { name: 'Next.js', build: 'npm run build', output: '.next', icon: '▲' },
|
|
62
|
+
'next.config.mjs': { name: 'Next.js', build: 'npm run build', output: '.next', icon: '▲' },
|
|
63
|
+
'next.config.ts': { name: 'Next.js', build: 'npm run build', output: '.next', icon: '▲' },
|
|
64
|
+
|
|
65
|
+
// Astro
|
|
66
|
+
'astro.config.mjs': { name: 'Astro', build: 'npm run build', output: 'dist', icon: '🚀' },
|
|
67
|
+
'astro.config.js': { name: 'Astro', build: 'npm run build', output: 'dist', icon: '🚀' },
|
|
68
|
+
'astro.config.ts': { name: 'Astro', build: 'npm run build', output: 'dist', icon: '🚀' },
|
|
69
|
+
|
|
70
|
+
// SvelteKit
|
|
71
|
+
'svelte.config.js': { name: 'SvelteKit', build: 'npm run build', output: 'build', icon: '🔶' },
|
|
72
|
+
'svelte.config.ts': { name: 'SvelteKit', build: 'npm run build', output: 'build', icon: '🔶' },
|
|
73
|
+
|
|
74
|
+
// Nuxt
|
|
75
|
+
'nuxt.config.js': { name: 'Nuxt', build: 'npm run build', output: '.nuxt', icon: '💚' },
|
|
76
|
+
'nuxt.config.ts': { name: 'Nuxt', build: 'npm run build', output: '.nuxt', icon: '💚' },
|
|
77
|
+
|
|
78
|
+
// Remix
|
|
79
|
+
'remix.config.js': { name: 'Remix', build: 'npm run build', output: 'build', icon: '💿' },
|
|
80
|
+
|
|
81
|
+
// Gatsby
|
|
82
|
+
'gatsby-config.js': { name: 'Gatsby', build: 'npm run build', output: 'public', icon: '💜' },
|
|
83
|
+
'gatsby-config.ts': { name: 'Gatsby', build: 'npm run build', output: 'public', icon: '💜' },
|
|
84
|
+
|
|
85
|
+
// Vite (generic)
|
|
86
|
+
'vite.config.js': { name: 'Vite', build: 'npm run build', output: 'dist', icon: '⚡' },
|
|
87
|
+
'vite.config.ts': { name: 'Vite', build: 'npm run build', output: 'dist', icon: '⚡' },
|
|
88
|
+
'vite.config.mjs': { name: 'Vite', build: 'npm run build', output: 'dist', icon: '⚡' },
|
|
89
|
+
|
|
90
|
+
// Angular
|
|
91
|
+
'angular.json': { name: 'Angular', build: 'npm run build', output: 'dist', icon: '🅰️' },
|
|
92
|
+
|
|
93
|
+
// Vue CLI
|
|
94
|
+
'vue.config.js': { name: 'Vue CLI', build: 'npm run build', output: 'dist', icon: '💚' },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Package.json dependency-based detection
|
|
98
|
+
const DEPENDENCY_SIGNATURES = {
|
|
99
|
+
'next': { name: 'Next.js', build: 'npm run build', output: '.next', icon: '▲' },
|
|
100
|
+
'@remix-run/react': { name: 'Remix', build: 'npm run build', output: 'build', icon: '💿' },
|
|
101
|
+
'gatsby': { name: 'Gatsby', build: 'npm run build', output: 'public', icon: '💜' },
|
|
102
|
+
'@angular/core': { name: 'Angular', build: 'npm run build', output: 'dist', icon: '🅰️' },
|
|
103
|
+
'react-scripts': { name: 'Create React App', build: 'npm run build', output: 'build', icon: '⚛️' },
|
|
104
|
+
'vite': { name: 'Vite', build: 'npm run build', output: 'dist', icon: '⚡' },
|
|
105
|
+
'astro': { name: 'Astro', build: 'npm run build', output: 'dist', icon: '🚀' },
|
|
106
|
+
'svelte': { name: 'Svelte', build: 'npm run build', output: 'build', icon: '🔶' },
|
|
107
|
+
'nuxt': { name: 'Nuxt', build: 'npm run build', output: '.nuxt', icon: '💚' },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ==================== FRAMEWORK DETECTION ====================
|
|
111
|
+
async function detectFramework(projectPath) {
|
|
112
|
+
// 1. Check for framework config files
|
|
113
|
+
for (const [configFile, framework] of Object.entries(FRAMEWORK_SIGNATURES)) {
|
|
114
|
+
if (await fs.pathExists(path.join(projectPath, configFile))) {
|
|
115
|
+
return { ...framework, detected: 'config', configFile };
|
|
116
|
+
}
|
|
76
117
|
}
|
|
77
118
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
119
|
+
// 2. Check package.json dependencies
|
|
120
|
+
const pkgPath = path.join(projectPath, 'package.json');
|
|
121
|
+
if (await fs.pathExists(pkgPath)) {
|
|
122
|
+
try {
|
|
123
|
+
const pkg = await fs.readJson(pkgPath);
|
|
124
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
82
125
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
126
|
+
for (const [dep, framework] of Object.entries(DEPENDENCY_SIGNATURES)) {
|
|
127
|
+
if (allDeps[dep]) {
|
|
128
|
+
return { ...framework, detected: 'dependency', dependency: dep };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
87
131
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
132
|
+
// Check for generic build script
|
|
133
|
+
if (pkg.scripts?.build) {
|
|
134
|
+
return {
|
|
135
|
+
name: 'Custom',
|
|
136
|
+
build: 'npm run build',
|
|
137
|
+
output: 'dist',
|
|
138
|
+
icon: '📦',
|
|
139
|
+
detected: 'build-script'
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
} catch { }
|
|
91
143
|
}
|
|
92
144
|
|
|
93
|
-
//
|
|
145
|
+
// 3. Static site detection
|
|
146
|
+
const hasIndexHtml = await fs.pathExists(path.join(projectPath, 'index.html'));
|
|
94
147
|
if (hasIndexHtml) {
|
|
95
|
-
return
|
|
148
|
+
return {
|
|
149
|
+
name: 'Static',
|
|
150
|
+
build: null, // No build needed
|
|
151
|
+
output: '.',
|
|
152
|
+
icon: '📄',
|
|
153
|
+
detected: 'static'
|
|
154
|
+
};
|
|
96
155
|
}
|
|
97
156
|
|
|
98
|
-
return
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ==================== PROJECT TYPE DETECTION (legacy wrapper) ====================
|
|
161
|
+
async function detectProjectType(projectPath) {
|
|
162
|
+
const framework = await detectFramework(projectPath);
|
|
163
|
+
|
|
164
|
+
if (!framework) return 'unknown';
|
|
165
|
+
if (framework.detected === 'static') return 'static';
|
|
166
|
+
if (framework.name === 'Vite') return 'vite';
|
|
167
|
+
return 'bundled';
|
|
99
168
|
}
|
|
100
169
|
|
|
101
170
|
// ==================== INLINE BUILD ====================
|
|
@@ -224,6 +293,37 @@ async function buildWeb(config, options) {
|
|
|
224
293
|
const projectPath = process.cwd();
|
|
225
294
|
const outputDir = path.resolve(projectPath, config.build?.outDir || 'dist');
|
|
226
295
|
|
|
296
|
+
// Check if npm install has been run (for projects with package.json)
|
|
297
|
+
const hasPackageJson = await fs.pathExists(path.join(projectPath, 'package.json'));
|
|
298
|
+
const hasNodeModules = await fs.pathExists(path.join(projectPath, 'node_modules'));
|
|
299
|
+
|
|
300
|
+
if (hasPackageJson && !hasNodeModules) {
|
|
301
|
+
console.log(chalk.yellow('⚠️ Dependencies not installed'));
|
|
302
|
+
console.log(chalk.gray(' Run: npm install\n'));
|
|
303
|
+
|
|
304
|
+
const answers = await inquirer.prompt([{
|
|
305
|
+
type: 'confirm',
|
|
306
|
+
name: 'install',
|
|
307
|
+
message: 'Run npm install now?',
|
|
308
|
+
default: true
|
|
309
|
+
}]);
|
|
310
|
+
|
|
311
|
+
if (answers.install) {
|
|
312
|
+
const spinner = ora('Installing dependencies...').start();
|
|
313
|
+
try {
|
|
314
|
+
execSync('npm install', { cwd: projectPath, stdio: 'pipe' });
|
|
315
|
+
spinner.succeed('Dependencies installed!');
|
|
316
|
+
console.log('');
|
|
317
|
+
} catch (error) {
|
|
318
|
+
spinner.fail('Failed to install dependencies');
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
console.log(chalk.gray(' Skipping build.\n'));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
227
327
|
// Check for --static flag
|
|
228
328
|
if (options.static) {
|
|
229
329
|
console.log(chalk.gray(' Mode: Static (--static flag)'));
|
|
@@ -242,40 +342,55 @@ async function buildWeb(config, options) {
|
|
|
242
342
|
return;
|
|
243
343
|
}
|
|
244
344
|
|
|
245
|
-
// Auto-detect
|
|
246
|
-
const
|
|
247
|
-
console.log(chalk.gray(` Detected: ${projectType} project`));
|
|
345
|
+
// Auto-detect framework (Vercel-style)
|
|
346
|
+
const framework = await detectFramework(projectPath);
|
|
248
347
|
|
|
249
|
-
if (
|
|
250
|
-
console.log(chalk.gray(' Mode: Static site (copying files directly)\n'));
|
|
251
|
-
await copyStaticFiles(projectPath, outputDir);
|
|
252
|
-
console.log(chalk.green('\n✅ Static web build complete!'));
|
|
253
|
-
console.log(chalk.gray(` Output: ${outputDir}/\n`));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (projectType === 'unknown') {
|
|
348
|
+
if (!framework) {
|
|
258
349
|
console.log(chalk.yellow('\n⚠️ Could not detect project type'));
|
|
259
350
|
console.log(chalk.gray(' No index.html or package.json found'));
|
|
260
351
|
console.log(chalk.gray(' Try: slct build web --static\n'));
|
|
261
352
|
process.exit(1);
|
|
262
353
|
}
|
|
263
354
|
|
|
355
|
+
// Display detected framework
|
|
356
|
+
console.log(chalk.cyan(` ${framework.icon} Detected: ${chalk.bold(framework.name)}`));
|
|
357
|
+
if (framework.detected === 'config') {
|
|
358
|
+
console.log(chalk.gray(` via ${framework.configFile}`));
|
|
359
|
+
} else if (framework.detected === 'dependency') {
|
|
360
|
+
console.log(chalk.gray(` via ${framework.dependency} dependency`));
|
|
361
|
+
} else if (framework.detected === 'static') {
|
|
362
|
+
console.log(chalk.gray(' (static HTML/CSS/JS site)'));
|
|
363
|
+
}
|
|
364
|
+
console.log('');
|
|
365
|
+
|
|
366
|
+
// Handle static sites
|
|
367
|
+
if (framework.detected === 'static') {
|
|
368
|
+
await copyStaticFiles(projectPath, outputDir);
|
|
369
|
+
console.log(chalk.green('\n✅ Static web build complete!'));
|
|
370
|
+
console.log(chalk.gray(` Output: ${outputDir}/\n`));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
264
374
|
// Run build command for bundled projects
|
|
265
|
-
|
|
375
|
+
// Priority: select.json config > detected framework > default
|
|
376
|
+
const buildCommand = config.build?.command || framework.build || 'npm run build';
|
|
377
|
+
const finalOutputDir = config.build?.outDir
|
|
378
|
+
? path.resolve(projectPath, config.build.outDir)
|
|
379
|
+
: path.resolve(projectPath, framework.output || 'dist');
|
|
380
|
+
|
|
381
|
+
const spinner = ora(`Running: ${chalk.cyan(buildCommand)}`).start();
|
|
266
382
|
|
|
267
383
|
try {
|
|
268
|
-
const buildCommand = config.build?.command || 'npm run build';
|
|
269
384
|
execSync(buildCommand, {
|
|
270
385
|
stdio: 'inherit',
|
|
271
386
|
cwd: projectPath
|
|
272
387
|
});
|
|
273
|
-
spinner.succeed(
|
|
388
|
+
spinner.succeed(`${framework.icon} ${framework.name} build complete!`);
|
|
274
389
|
|
|
275
390
|
// Validate output
|
|
276
|
-
await validateBuildOutput(
|
|
391
|
+
await validateBuildOutput(finalOutputDir);
|
|
277
392
|
|
|
278
|
-
console.log(chalk.gray(` Output: ${
|
|
393
|
+
console.log(chalk.gray(` Output: ${finalOutputDir}/\n`));
|
|
279
394
|
} catch (error) {
|
|
280
395
|
spinner.fail('Web build failed');
|
|
281
396
|
throw error;
|
package/lib/commands/deploy.js
CHANGED
|
@@ -121,27 +121,107 @@ async function deploy(options) {
|
|
|
121
121
|
|
|
122
122
|
// Web App
|
|
123
123
|
if (config.platforms.includes('web')) {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
124
|
+
const isStaticSite = !config.build?.command;
|
|
125
|
+
|
|
126
|
+
if (isStaticSite) {
|
|
127
|
+
// Static site - upload files directly (no zipping)
|
|
128
|
+
spinner.text = 'Preparing static files for upload...';
|
|
129
|
+
|
|
130
|
+
// For static sites, the source IS the dist
|
|
131
|
+
const sourceDir = process.cwd();
|
|
132
|
+
const staticFiles = ['index.html', 'style.css', 'app.js', 'script.js', 'main.js'];
|
|
133
|
+
const staticFolders = ['assets', 'css', 'js', 'images', 'fonts'];
|
|
134
|
+
|
|
135
|
+
const filesToUpload = [];
|
|
136
|
+
|
|
137
|
+
// Collect individual files
|
|
138
|
+
for (const file of staticFiles) {
|
|
139
|
+
const filePath = path.join(sourceDir, file);
|
|
140
|
+
if (await fs.pathExists(filePath)) {
|
|
141
|
+
const content = await fs.readFile(filePath);
|
|
142
|
+
const hash = require('crypto').createHash('md5').update(content).digest('hex').slice(0, 8);
|
|
143
|
+
filesToUpload.push({ path: file, content, hash });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Collect files from folders
|
|
148
|
+
for (const folder of staticFolders) {
|
|
149
|
+
const folderPath = path.join(sourceDir, folder);
|
|
150
|
+
if (await fs.pathExists(folderPath)) {
|
|
151
|
+
const files = await fs.readdir(folderPath, { recursive: true });
|
|
152
|
+
for (const file of files) {
|
|
153
|
+
const fullPath = path.join(folderPath, file);
|
|
154
|
+
const stat = await fs.stat(fullPath);
|
|
155
|
+
if (stat.isFile()) {
|
|
156
|
+
const content = await fs.readFile(fullPath);
|
|
157
|
+
const hash = require('crypto').createHash('md5').update(content).digest('hex').slice(0, 8);
|
|
158
|
+
filesToUpload.push({ path: `${folder}/${file}`, content, hash });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (filesToUpload.length === 0) {
|
|
165
|
+
spinner.warn('No static files found to deploy');
|
|
166
|
+
} else {
|
|
167
|
+
spinner.text = `Found ${filesToUpload.length} files to upload...`;
|
|
168
|
+
|
|
169
|
+
// Create a manifest of files with hashes
|
|
170
|
+
manifest.staticFiles = filesToUpload.map(f => ({ path: f.path, hash: f.hash }));
|
|
171
|
+
|
|
172
|
+
// For now, still use zip but with the static files only
|
|
173
|
+
// TODO: Switch to direct R2 upload when backend supports it
|
|
174
|
+
const zipPath = path.join(os.tmpdir(), `static-build-${Date.now()}.zip`);
|
|
175
|
+
const output = fs.createWriteStream(zipPath);
|
|
176
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
177
|
+
|
|
178
|
+
spinner.text = 'Creating deployment package...';
|
|
179
|
+
|
|
180
|
+
await new Promise((resolve, reject) => {
|
|
181
|
+
output.on('close', resolve);
|
|
182
|
+
archive.on('error', reject);
|
|
183
|
+
archive.pipe(output);
|
|
184
|
+
for (const file of filesToUpload) {
|
|
185
|
+
archive.append(file.content, { name: file.path });
|
|
186
|
+
}
|
|
187
|
+
archive.finalize();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
form.append('web', fs.createReadStream(zipPath));
|
|
191
|
+
manifest.platforms.push('web');
|
|
192
|
+
manifest.deployMode = 'static';
|
|
193
|
+
foundArtifacts++;
|
|
194
|
+
|
|
195
|
+
// Log file hashes for potential future deduplication
|
|
196
|
+
console.log(chalk.gray(`\n Files to deploy:`));
|
|
197
|
+
filesToUpload.forEach(f => {
|
|
198
|
+
console.log(chalk.gray(` ${f.hash} ${f.path}`));
|
|
199
|
+
});
|
|
200
|
+
}
|
|
143
201
|
} else {
|
|
144
|
-
|
|
202
|
+
// Bundled project - use dist directory
|
|
203
|
+
const buildDir = path.resolve(process.cwd(), config.build?.outDir || 'dist');
|
|
204
|
+
if (await fs.pathExists(buildDir)) {
|
|
205
|
+
const zipPath = path.join(os.tmpdir(), `web-build-${Date.now()}.zip`);
|
|
206
|
+
const output = fs.createWriteStream(zipPath);
|
|
207
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
208
|
+
|
|
209
|
+
spinner.text = 'Zipping web build...';
|
|
210
|
+
|
|
211
|
+
await new Promise((resolve, reject) => {
|
|
212
|
+
output.on('close', resolve);
|
|
213
|
+
archive.on('error', reject);
|
|
214
|
+
archive.pipe(output);
|
|
215
|
+
archive.directory(buildDir, false);
|
|
216
|
+
archive.finalize();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
form.append('web', fs.createReadStream(zipPath));
|
|
220
|
+
manifest.platforms.push('web');
|
|
221
|
+
foundArtifacts++;
|
|
222
|
+
} else {
|
|
223
|
+
spinner.warn('Web build directory not found (skipping web upload)');
|
|
224
|
+
}
|
|
145
225
|
}
|
|
146
226
|
} else if (foundArtifacts === 0) { // Only error if NO artifacts found and web not requested
|
|
147
227
|
spinner.fail('No build artifacts found!');
|
package/lib/commands/init.js
CHANGED
|
@@ -5,125 +5,218 @@ const inquirer = require('inquirer');
|
|
|
5
5
|
const ora = require('ora');
|
|
6
6
|
|
|
7
7
|
async function init(name, options) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// If no name provided, ask for it
|
|
11
|
-
if (!name) {
|
|
12
|
-
const answers = await inquirer.prompt([
|
|
13
|
-
{
|
|
14
|
-
type: 'input',
|
|
15
|
-
name: 'name',
|
|
16
|
-
message: 'Project name:',
|
|
17
|
-
default: 'my-select-app',
|
|
18
|
-
validate: (input) => {
|
|
19
|
-
if (/^[a-z0-9-]+$/.test(input)) return true;
|
|
20
|
-
return 'Project name must be lowercase with hyphens only';
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
]);
|
|
24
|
-
name = answers.name;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const projectDir = path.resolve(process.cwd(), name);
|
|
8
|
+
console.log(chalk.bold('\n📦 Initialize Select Project\n'));
|
|
28
9
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.log(chalk.red(`\n❌ Directory "${name}" already exists.\n`));
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Ask for project details
|
|
10
|
+
// If no name provided, ask for it
|
|
11
|
+
if (!name) {
|
|
36
12
|
const answers = await inquirer.prompt([
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
],
|
|
46
|
-
default: options.template
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
type: 'checkbox',
|
|
50
|
-
name: 'platforms',
|
|
51
|
-
message: 'Target platforms:',
|
|
52
|
-
choices: [
|
|
53
|
-
{ name: 'Web', value: 'web', checked: true },
|
|
54
|
-
{ name: 'Windows', value: 'windows' },
|
|
55
|
-
{ name: 'macOS', value: 'macos' },
|
|
56
|
-
{ name: 'Linux', value: 'linux' }
|
|
57
|
-
]
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
type: 'list',
|
|
61
|
-
name: 'desktopFramework',
|
|
62
|
-
message: 'Desktop framework:',
|
|
63
|
-
choices: [
|
|
64
|
-
{ name: 'Tauri (Recommended - smaller, faster)', value: 'tauri' },
|
|
65
|
-
{ name: 'Electron (Larger, more compatible)', value: 'electron' }
|
|
66
|
-
],
|
|
67
|
-
when: (ans) => ans.platforms.some(p => ['windows', 'macos', 'linux'].includes(p))
|
|
13
|
+
{
|
|
14
|
+
type: 'input',
|
|
15
|
+
name: 'name',
|
|
16
|
+
message: 'Project name:',
|
|
17
|
+
default: 'my-select-app',
|
|
18
|
+
validate: (input) => {
|
|
19
|
+
if (/^[a-z0-9-]+$/.test(input)) return true;
|
|
20
|
+
return 'Project name must be lowercase with hyphens only';
|
|
68
21
|
}
|
|
22
|
+
}
|
|
69
23
|
]);
|
|
24
|
+
name = answers.name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const projectDir = path.resolve(process.cwd(), name);
|
|
28
|
+
|
|
29
|
+
// Check if directory exists
|
|
30
|
+
if (await fs.pathExists(projectDir)) {
|
|
31
|
+
console.log(chalk.red(`\n❌ Directory "${name}" already exists.\n`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Ask for project details
|
|
36
|
+
const answers = await inquirer.prompt([
|
|
37
|
+
{
|
|
38
|
+
type: 'list',
|
|
39
|
+
name: 'template',
|
|
40
|
+
message: 'Select a template:',
|
|
41
|
+
choices: [
|
|
42
|
+
{ name: 'React + Vite', value: 'react' },
|
|
43
|
+
{ name: 'Vue + Vite', value: 'vue' },
|
|
44
|
+
{ name: 'Vanilla + Vite', value: 'vanilla' },
|
|
45
|
+
{ name: 'Pure Static (no bundler)', value: 'static' }
|
|
46
|
+
],
|
|
47
|
+
default: options.template
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'checkbox',
|
|
51
|
+
name: 'platforms',
|
|
52
|
+
message: 'Target platforms:',
|
|
53
|
+
choices: [
|
|
54
|
+
{ name: 'Web', value: 'web', checked: true },
|
|
55
|
+
{ name: 'Windows', value: 'windows' },
|
|
56
|
+
{ name: 'macOS', value: 'macos' },
|
|
57
|
+
{ name: 'Linux', value: 'linux' }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'list',
|
|
62
|
+
name: 'desktopFramework',
|
|
63
|
+
message: 'Desktop framework:',
|
|
64
|
+
choices: [
|
|
65
|
+
{ name: 'Tauri (Recommended - smaller, faster)', value: 'tauri' },
|
|
66
|
+
{ name: 'Electron (Larger, more compatible)', value: 'electron' }
|
|
67
|
+
],
|
|
68
|
+
when: (ans) => ans.platforms.some(p => ['windows', 'macos', 'linux'].includes(p))
|
|
69
|
+
}
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const spinner = ora('Creating project...').start();
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Create project directory
|
|
76
|
+
await fs.ensureDir(projectDir);
|
|
77
|
+
|
|
78
|
+
// Create select.json config
|
|
79
|
+
const selectConfig = {
|
|
80
|
+
name: name,
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
description: 'A Select app',
|
|
83
|
+
platforms: answers.platforms,
|
|
84
|
+
desktop: {
|
|
85
|
+
framework: answers.desktopFramework || 'tauri'
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Add build config only for non-static templates
|
|
90
|
+
if (answers.template !== 'static') {
|
|
91
|
+
selectConfig.build = {
|
|
92
|
+
command: 'npm run build',
|
|
93
|
+
devCommand: 'npm run dev',
|
|
94
|
+
outDir: 'dist'
|
|
95
|
+
};
|
|
96
|
+
}
|
|
70
97
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
outDir: 'dist'
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
await fs.writeJson(path.join(projectDir, 'select.json'), selectConfig, { spaces: 2 });
|
|
94
|
-
|
|
95
|
-
// Create basic package.json
|
|
96
|
-
const packageJson = {
|
|
97
|
-
name: name,
|
|
98
|
-
version: '1.0.0',
|
|
99
|
-
private: true,
|
|
100
|
-
scripts: {
|
|
101
|
-
dev: 'vite',
|
|
102
|
-
build: 'vite build',
|
|
103
|
-
preview: 'vite preview',
|
|
104
|
-
'slct:build': 'slct build all',
|
|
105
|
-
'slct:deploy': 'slct deploy'
|
|
106
|
-
},
|
|
107
|
-
dependencies: {},
|
|
108
|
-
devDependencies: {
|
|
109
|
-
'vite': '^5.0.0'
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// Add template-specific dependencies
|
|
114
|
-
if (answers.template === 'react') {
|
|
115
|
-
packageJson.dependencies['react'] = '^18.2.0';
|
|
116
|
-
packageJson.dependencies['react-dom'] = '^18.2.0';
|
|
117
|
-
packageJson.devDependencies['@vitejs/plugin-react'] = '^4.2.0';
|
|
118
|
-
} else if (answers.template === 'vue') {
|
|
119
|
-
packageJson.dependencies['vue'] = '^3.4.0';
|
|
120
|
-
packageJson.devDependencies['@vitejs/plugin-vue'] = '^4.5.0';
|
|
98
|
+
await fs.writeJson(path.join(projectDir, 'select.json'), selectConfig, { spaces: 2 });
|
|
99
|
+
|
|
100
|
+
// Only create package.json for bundled templates (not static)
|
|
101
|
+
if (answers.template !== 'static') {
|
|
102
|
+
const packageJson = {
|
|
103
|
+
name: name,
|
|
104
|
+
version: '1.0.0',
|
|
105
|
+
private: true,
|
|
106
|
+
scripts: {
|
|
107
|
+
dev: 'vite',
|
|
108
|
+
build: 'vite build',
|
|
109
|
+
preview: 'vite preview',
|
|
110
|
+
'slct:build': 'slct build all',
|
|
111
|
+
'slct:deploy': 'slct deploy'
|
|
112
|
+
},
|
|
113
|
+
dependencies: {},
|
|
114
|
+
devDependencies: {
|
|
115
|
+
'vite': '^5.0.0'
|
|
121
116
|
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Add template-specific dependencies
|
|
120
|
+
if (answers.template === 'react') {
|
|
121
|
+
packageJson.dependencies['react'] = '^18.2.0';
|
|
122
|
+
packageJson.dependencies['react-dom'] = '^18.2.0';
|
|
123
|
+
packageJson.devDependencies['@vitejs/plugin-react'] = '^4.2.0';
|
|
124
|
+
} else if (answers.template === 'vue') {
|
|
125
|
+
packageJson.dependencies['vue'] = '^3.4.0';
|
|
126
|
+
packageJson.devDependencies['@vitejs/plugin-vue'] = '^4.5.0';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await fs.writeJson(path.join(projectDir, 'package.json'), packageJson, { spaces: 2 });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle template-specific file generation
|
|
133
|
+
if (answers.template === 'static') {
|
|
134
|
+
// Pure static template - no bundler, files in root
|
|
135
|
+
await fs.writeFile(path.join(projectDir, 'index.html'), `<!DOCTYPE html>
|
|
136
|
+
<html lang="en">
|
|
137
|
+
<head>
|
|
138
|
+
<meta charset="UTF-8">
|
|
139
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
140
|
+
<title>${name}</title>
|
|
141
|
+
<link rel="stylesheet" href="style.css">
|
|
142
|
+
</head>
|
|
143
|
+
<body>
|
|
144
|
+
<div id="app">
|
|
145
|
+
<div class="container">
|
|
146
|
+
<h1>Welcome to ${name}</h1>
|
|
147
|
+
<p>Built with Select CLI</p>
|
|
148
|
+
<button id="counter-btn">Count: 0</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<script src="app.js"></script>
|
|
152
|
+
</body>
|
|
153
|
+
</html>`);
|
|
154
|
+
|
|
155
|
+
await fs.writeFile(path.join(projectDir, 'style.css'), `* {
|
|
156
|
+
margin: 0;
|
|
157
|
+
padding: 0;
|
|
158
|
+
box-sizing: border-box;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
body {
|
|
162
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
163
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
164
|
+
min-height: 100vh;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
justify-content: center;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.container {
|
|
171
|
+
background: white;
|
|
172
|
+
padding: 3rem 4rem;
|
|
173
|
+
border-radius: 1rem;
|
|
174
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
175
|
+
text-align: center;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
h1 {
|
|
179
|
+
color: #1a202c;
|
|
180
|
+
margin-bottom: 0.5rem;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
p {
|
|
184
|
+
color: #718096;
|
|
185
|
+
margin-bottom: 1.5rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
button {
|
|
189
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
190
|
+
color: white;
|
|
191
|
+
border: none;
|
|
192
|
+
padding: 0.75rem 2rem;
|
|
193
|
+
font-size: 1rem;
|
|
194
|
+
border-radius: 0.5rem;
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
button:hover {
|
|
200
|
+
transform: translateY(-2px);
|
|
201
|
+
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
|
|
202
|
+
}
|
|
203
|
+
`);
|
|
204
|
+
|
|
205
|
+
await fs.writeFile(path.join(projectDir, 'app.js'), `// Simple counter example
|
|
206
|
+
let count = 0;
|
|
207
|
+
const button = document.getElementById('counter-btn');
|
|
122
208
|
|
|
123
|
-
|
|
209
|
+
button.addEventListener('click', () => {
|
|
210
|
+
count++;
|
|
211
|
+
button.textContent = 'Count: ' + count;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
console.log('${name} loaded!');
|
|
215
|
+
`);
|
|
124
216
|
|
|
125
|
-
|
|
126
|
-
|
|
217
|
+
} else {
|
|
218
|
+
// Vite-based templates (react, vue, vanilla)
|
|
219
|
+
const indexHtml = `<!DOCTYPE html>
|
|
127
220
|
<html lang="en">
|
|
128
221
|
<head>
|
|
129
222
|
<meta charset="UTF-8">
|
|
@@ -132,25 +225,21 @@ async function init(name, options) {
|
|
|
132
225
|
</head>
|
|
133
226
|
<body>
|
|
134
227
|
<div id="app"></div>
|
|
135
|
-
<script type="module" src="/src/main.${answers.template === '
|
|
228
|
+
<script type="module" src="/src/main.${answers.template === 'react' ? 'jsx' : 'js'}"></script>
|
|
136
229
|
</body>
|
|
137
230
|
</html>`;
|
|
138
231
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Create src directory with basic file
|
|
142
|
-
await fs.ensureDir(path.join(projectDir, 'src'));
|
|
232
|
+
await fs.writeFile(path.join(projectDir, 'index.html'), indexHtml);
|
|
233
|
+
await fs.ensureDir(path.join(projectDir, 'src'));
|
|
143
234
|
|
|
144
|
-
|
|
145
|
-
await
|
|
146
|
-
|
|
147
|
-
if (answers.template === 'react') {
|
|
148
|
-
await fs.writeFile(path.join(projectDir, 'src', 'main.jsx'), `import React from 'react'
|
|
235
|
+
if (answers.template === 'react') {
|
|
236
|
+
await fs.writeFile(path.join(projectDir, 'src', 'main.jsx'), `import React from 'react'
|
|
149
237
|
import ReactDOM from 'react-dom/client'
|
|
238
|
+
import './style.css'
|
|
150
239
|
|
|
151
240
|
function App() {
|
|
152
241
|
return (
|
|
153
|
-
<div
|
|
242
|
+
<div className="app">
|
|
154
243
|
<h1>Welcome to ${name}</h1>
|
|
155
244
|
<p>Built with Select CLI</p>
|
|
156
245
|
</div>
|
|
@@ -159,19 +248,74 @@ function App() {
|
|
|
159
248
|
|
|
160
249
|
ReactDOM.createRoot(document.getElementById('app')).render(<App />)
|
|
161
250
|
`);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
251
|
+
} else {
|
|
252
|
+
// Vanilla JS template
|
|
253
|
+
await fs.writeFile(path.join(projectDir, 'src', 'main.js'), `import './style.css'
|
|
254
|
+
|
|
255
|
+
document.getElementById('app').innerHTML = \`
|
|
256
|
+
<div class="app">
|
|
165
257
|
<h1>Welcome to ${name}</h1>
|
|
166
258
|
<p>Built with Select CLI</p>
|
|
167
259
|
</div>
|
|
168
260
|
\`
|
|
169
261
|
`);
|
|
170
|
-
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Create style.css for Vite templates
|
|
265
|
+
await fs.writeFile(path.join(projectDir, 'src', 'style.css'), `* {
|
|
266
|
+
margin: 0;
|
|
267
|
+
padding: 0;
|
|
268
|
+
box-sizing: border-box;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
body {
|
|
272
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
273
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
274
|
+
min-height: 100vh;
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
justify-content: center;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.app {
|
|
281
|
+
background: white;
|
|
282
|
+
padding: 3rem 4rem;
|
|
283
|
+
border-radius: 1rem;
|
|
284
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
285
|
+
text-align: center;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
h1 {
|
|
289
|
+
color: #1a202c;
|
|
290
|
+
margin-bottom: 0.5rem;
|
|
291
|
+
}
|
|
171
292
|
|
|
172
|
-
|
|
293
|
+
p {
|
|
294
|
+
color: #718096;
|
|
295
|
+
}
|
|
296
|
+
`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Create GitHub Actions workflow (only for non-static with desktop targets)
|
|
300
|
+
if (answers.template !== 'static' && answers.platforms.some(p => ['windows', 'macos', 'linux'].includes(p))) {
|
|
301
|
+
await createGithubWorkflow(projectDir, answers.desktopFramework || 'tauri', name);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
spinner.succeed('Project created successfully!');
|
|
305
|
+
|
|
306
|
+
// Different next steps for static vs bundled
|
|
307
|
+
if (answers.template === 'static') {
|
|
308
|
+
console.log(chalk.green(`
|
|
309
|
+
✅ Project "${name}" created!
|
|
310
|
+
|
|
311
|
+
Next steps:
|
|
312
|
+
${chalk.cyan(`cd ${name}`)}
|
|
313
|
+
${chalk.cyan('slct deploy')}
|
|
173
314
|
|
|
174
|
-
|
|
315
|
+
No npm install needed - it's pure HTML/CSS/JS!
|
|
316
|
+
`));
|
|
317
|
+
} else {
|
|
318
|
+
console.log(chalk.green(`
|
|
175
319
|
✅ Project "${name}" created!
|
|
176
320
|
|
|
177
321
|
Next steps:
|
|
@@ -180,34 +324,35 @@ Next steps:
|
|
|
180
324
|
${chalk.cyan('slct build')}
|
|
181
325
|
|
|
182
326
|
`));
|
|
327
|
+
}
|
|
183
328
|
|
|
184
|
-
|
|
185
|
-
|
|
329
|
+
// Check for required dependencies
|
|
330
|
+
await checkDependencies(answers);
|
|
186
331
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
332
|
+
} catch (error) {
|
|
333
|
+
spinner.fail('Failed to create project');
|
|
334
|
+
console.error(chalk.red(error.message));
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
192
337
|
}
|
|
193
338
|
|
|
194
339
|
async function checkDependencies(answers) {
|
|
195
|
-
|
|
340
|
+
const hasDesktop = answers.platforms?.some(p => ['windows', 'macos', 'linux'].includes(p));
|
|
196
341
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
342
|
+
if (hasDesktop && answers.desktopFramework === 'tauri') {
|
|
343
|
+
console.log(chalk.yellow('\n⚠️ Tauri Requirements:'));
|
|
344
|
+
console.log(chalk.gray(' Run "slct build desktop" for detailed setup instructions.\n'));
|
|
345
|
+
}
|
|
201
346
|
}
|
|
202
347
|
|
|
203
348
|
async function createGithubWorkflow(projectDir, framework, name) {
|
|
204
|
-
|
|
205
|
-
|
|
349
|
+
const workflowDir = path.join(projectDir, '.github', 'workflows');
|
|
350
|
+
await fs.ensureDir(workflowDir);
|
|
206
351
|
|
|
207
|
-
|
|
352
|
+
let workflowContent = '';
|
|
208
353
|
|
|
209
|
-
|
|
210
|
-
|
|
354
|
+
if (framework === 'tauri') {
|
|
355
|
+
workflowContent = `name: Build Apps
|
|
211
356
|
on:
|
|
212
357
|
push:
|
|
213
358
|
branches: [ main ]
|
|
@@ -253,9 +398,9 @@ jobs:
|
|
|
253
398
|
releaseDraft: true
|
|
254
399
|
prerelease: false
|
|
255
400
|
`;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
401
|
+
} else {
|
|
402
|
+
// Electron Workflow
|
|
403
|
+
workflowContent = `name: Build Apps
|
|
259
404
|
on:
|
|
260
405
|
push:
|
|
261
406
|
branches: [ main ]
|
|
@@ -287,9 +432,9 @@ jobs:
|
|
|
287
432
|
env:
|
|
288
433
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
289
434
|
`;
|
|
290
|
-
|
|
435
|
+
}
|
|
291
436
|
|
|
292
|
-
|
|
437
|
+
await fs.writeFile(path.join(workflowDir, 'build.yml'), workflowContent);
|
|
293
438
|
}
|
|
294
439
|
|
|
295
440
|
module.exports = init;
|