@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.
@@ -55,47 +55,116 @@ async function build(target, options) {
55
55
  }
56
56
  }
57
57
 
58
- // ==================== PROJECT TYPE DETECTION ====================
59
- async function detectProjectType(projectPath) {
60
- const hasPackageJson = await fs.pathExists(path.join(projectPath, 'package.json'));
61
- const hasViteConfig = await fs.pathExists(path.join(projectPath, 'vite.config.js')) ||
62
- await fs.pathExists(path.join(projectPath, 'vite.config.ts'));
63
- const hasIndexHtml = await fs.pathExists(path.join(projectPath, 'index.html'));
64
- const hasSrcMain = await fs.pathExists(path.join(projectPath, 'src', 'main.js')) ||
65
- await fs.pathExists(path.join(projectPath, 'src', 'main.jsx')) ||
66
- await fs.pathExists(path.join(projectPath, 'src', 'main.ts')) ||
67
- await fs.pathExists(path.join(projectPath, 'src', 'main.tsx'));
68
-
69
- // Check if package.json has a build script
70
- let hasBuildScript = false;
71
- if (hasPackageJson) {
72
- try {
73
- const pkg = await fs.readJson(path.join(projectPath, 'package.json'));
74
- hasBuildScript = !!(pkg.scripts && pkg.scripts.build);
75
- } catch { }
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
- // Static site: has index.html but no package.json or no build script
79
- if (hasIndexHtml && (!hasPackageJson || !hasBuildScript)) {
80
- return 'static';
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
- // Vite project
84
- if (hasViteConfig) {
85
- return 'vite';
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
- // Has package.json with build script
89
- if (hasPackageJson && hasBuildScript) {
90
- return 'bundled';
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
- // Default to static if just has HTML
145
+ // 3. Static site detection
146
+ const hasIndexHtml = await fs.pathExists(path.join(projectPath, 'index.html'));
94
147
  if (hasIndexHtml) {
95
- return 'static';
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 'unknown';
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 project type
246
- const projectType = await detectProjectType(projectPath);
247
- console.log(chalk.gray(` Detected: ${projectType} project`));
345
+ // Auto-detect framework (Vercel-style)
346
+ const framework = await detectFramework(projectPath);
248
347
 
249
- if (projectType === 'static') {
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
- const spinner = ora('Running build command...').start();
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('Web build complete!');
388
+ spinner.succeed(`${framework.icon} ${framework.name} build complete!`);
274
389
 
275
390
  // Validate output
276
- await validateBuildOutput(outputDir);
391
+ await validateBuildOutput(finalOutputDir);
277
392
 
278
- console.log(chalk.gray(` Output: ${outputDir}/\n`));
393
+ console.log(chalk.gray(` Output: ${finalOutputDir}/\n`));
279
394
  } catch (error) {
280
395
  spinner.fail('Web build failed');
281
396
  throw error;
@@ -121,27 +121,107 @@ async function deploy(options) {
121
121
 
122
122
  // Web App
123
123
  if (config.platforms.includes('web')) {
124
- const buildDir = path.resolve(process.cwd(), config.build?.outDir || 'dist');
125
- if (await fs.pathExists(buildDir)) {
126
- const zipPath = path.join(os.tmpdir(), `web-build-${Date.now()}.zip`);
127
- const output = fs.createWriteStream(zipPath);
128
- const archive = archiver('zip', { zlib: { level: 9 } });
129
-
130
- spinner.text = 'Zipping web build...';
131
-
132
- await new Promise((resolve, reject) => {
133
- output.on('close', resolve);
134
- archive.on('error', reject);
135
- archive.pipe(output);
136
- archive.directory(buildDir, false);
137
- archive.finalize();
138
- });
139
-
140
- form.append('web', fs.createReadStream(zipPath));
141
- manifest.platforms.push('web');
142
- foundArtifacts++;
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
- spinner.warn('Web build directory not found (skipping web upload)');
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!');
@@ -5,125 +5,218 @@ const inquirer = require('inquirer');
5
5
  const ora = require('ora');
6
6
 
7
7
  async function init(name, options) {
8
- console.log(chalk.bold('\n📦 Initialize Select Project\n'));
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
- // 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
10
+ // If no name provided, ask for it
11
+ if (!name) {
36
12
  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 JavaScript', value: 'vanilla' }
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
- const spinner = ora('Creating project...').start();
72
-
73
- try {
74
- // Create project directory
75
- await fs.ensureDir(projectDir);
76
-
77
- // Create select.json config
78
- const selectConfig = {
79
- name: name,
80
- version: '1.0.0',
81
- description: 'A Select app',
82
- platforms: answers.platforms,
83
- desktop: {
84
- framework: answers.desktopFramework || 'tauri'
85
- },
86
- build: {
87
- command: 'npm run build',
88
- devCommand: 'npm run dev',
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
- await fs.writeJson(path.join(projectDir, 'package.json'), packageJson, { spaces: 2 });
209
+ button.addEventListener('click', () => {
210
+ count++;
211
+ button.textContent = 'Count: ' + count;
212
+ });
213
+
214
+ console.log('${name} loaded!');
215
+ `);
124
216
 
125
- // Create basic index.html
126
- const indexHtml = `<!DOCTYPE html>
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 === 'vanilla' ? 'js' : answers.template === 'react' ? 'jsx' : 'js'}"></script>
228
+ <script type="module" src="/src/main.${answers.template === 'react' ? 'jsx' : 'js'}"></script>
136
229
  </body>
137
230
  </html>`;
138
231
 
139
- await fs.writeFile(path.join(projectDir, 'index.html'), indexHtml);
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
- // Create GitHub Actions workflow
145
- await createGithubWorkflow(projectDir, answers.desktopFramework || 'tauri', name);
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 style={{ padding: '2rem', fontFamily: 'system-ui' }}>
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
- } else {
163
- await fs.writeFile(path.join(projectDir, 'src', 'main.js'), `document.getElementById('app').innerHTML = \`
164
- <div style="padding: 2rem; font-family: system-ui;">
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
- spinner.succeed('Project created successfully!');
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
- console.log(chalk.green(`
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
- // Check for required dependencies
185
- await checkDependencies(answers);
329
+ // Check for required dependencies
330
+ await checkDependencies(answers);
186
331
 
187
- } catch (error) {
188
- spinner.fail('Failed to create project');
189
- console.error(chalk.red(error.message));
190
- process.exit(1);
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
- const hasDesktop = answers.platforms?.some(p => ['windows', 'macos', 'linux'].includes(p));
340
+ const hasDesktop = answers.platforms?.some(p => ['windows', 'macos', 'linux'].includes(p));
196
341
 
197
- if (hasDesktop && answers.desktopFramework === 'tauri') {
198
- console.log(chalk.yellow('\n⚠️ Tauri Requirements:'));
199
- console.log(chalk.gray(' Run "slct build desktop" for detailed setup instructions.\n'));
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
- const workflowDir = path.join(projectDir, '.github', 'workflows');
205
- await fs.ensureDir(workflowDir);
349
+ const workflowDir = path.join(projectDir, '.github', 'workflows');
350
+ await fs.ensureDir(workflowDir);
206
351
 
207
- let workflowContent = '';
352
+ let workflowContent = '';
208
353
 
209
- if (framework === 'tauri') {
210
- workflowContent = `name: Build Apps
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
- } else {
257
- // Electron Workflow
258
- workflowContent = `name: Build Apps
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
- await fs.writeFile(path.join(workflowDir, 'build.yml'), workflowContent);
437
+ await fs.writeFile(path.join(workflowDir, 'build.yml'), workflowContent);
293
438
  }
294
439
 
295
440
  module.exports = init;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iselect/select",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Build and deploy apps for web, desktop, and mobile from a single codebase",
5
5
  "main": "lib/index.js",
6
6
  "bin": {