@iselect/select 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.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # Select CLI
2
+
3
+ Build and deploy apps for web, desktop, and mobile from a single codebase.
4
+
5
+ ```
6
+ _____ ________ _______________
7
+ / ___// ____/ / / ____/ ____/_ /
8
+ \__ \/ __/ / / / __/ / / / /
9
+ ___/ / /___/ /___/ /___/ /___ / /
10
+ /____/_____/_____/_____/\____/ /_/
11
+ ```
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # Option 1: From Source (Recommended for Contributors)
17
+ git clone https://github.com/yourusername/select-cli.git
18
+ cd select-cli
19
+ npm link
20
+
21
+ # Option 2: From GitHub (If public)
22
+ npm install -g github:yourusername/select-cli
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ # Create a new project
29
+ slct init my-app
30
+
31
+ # Navigate to project
32
+ cd my-app
33
+
34
+ ```bash
35
+ # Select target platforms
36
+ slct
37
+
38
+ # Build for all desktop platforms & web
39
+ slct build all
40
+
41
+ # Deploy to Select marketplace
42
+ slct deploy
43
+ ```
44
+
45
+ ## Commands
46
+
47
+ ### `slct init [name]`
48
+
49
+ Create a new Select project with interactive prompts.
50
+
51
+ ```bash
52
+ slct init my-app
53
+ slct init --template vue
54
+ ```
55
+
56
+ **Options:**
57
+ - `-t, --template <template>` - Project template (react, vue, vanilla)
58
+
59
+ ### `slct` (or `slct s`)
60
+
61
+ Interactively select target platforms for your project.
62
+
63
+ ### `slct build [target]`
64
+
65
+ Build your app for specified platforms.
66
+
67
+ ```bash
68
+ slct build # Interactive selection
69
+ slct build web # Build for web only
70
+ slct build desktop # Build for desktop (Tauri/Electron)
71
+ slct build all # Build for all platforms (Web + Desktop)
72
+ ```
73
+
74
+ **Options:**
75
+ - `--tauri` - Use Tauri for desktop (default)
76
+ - `--electron` - Use Electron for desktop
77
+ - `--release` - Build for production release
78
+
79
+ ### `slct deploy`
80
+
81
+ Deploy your app to the Select marketplace.
82
+
83
+ ```bash
84
+ slct deploy
85
+ slct deploy --prod # Deploy to production
86
+ ```
87
+
88
+ ### `slct dev`
89
+
90
+ Start the development server.
91
+
92
+ ```bash
93
+ slct dev
94
+ slct dev --port 3000
95
+ ```
96
+
97
+ ## select.json
98
+
99
+ Configuration file generated by `slct init`:
100
+
101
+ ```json
102
+ {
103
+ "name": "my-app",
104
+ "version": "1.0.0",
105
+ "description": "A Select app",
106
+ "platforms": ["web", "windows", "linux"],
107
+ "desktop": {
108
+ "framework": "tauri"
109
+ },
110
+ "build": {
111
+ "command": "npm run build",
112
+ "devCommand": "npm run dev",
113
+ "outDir": "dist"
114
+ }
115
+ }
116
+ ```
117
+
118
+ ## Platform Requirements
119
+
120
+ ### Tauri (Desktop)
121
+
122
+ Tauri requires Rust and platform-specific build tools:
123
+
124
+ 1. **Install Rust**: https://rustup.rs/
125
+ 2. **Windows**: Install Visual Studio Build Tools
126
+ - MSVC v143 - VS 2022 C++ x64/x86 build tools
127
+ - Windows 10/11 SDK
128
+ - C++ CMake tools
129
+ 3. **macOS**: Install Xcode Command Line Tools
130
+ 4. **Linux**: Install build-essential and webkit2gtk
131
+
132
+ ### Electron (Desktop)
133
+
134
+ Electron is automatically installed when selected.
135
+
136
+ ## License
137
+
138
+ MIT
package/bin/select.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const chalk = require('chalk');
5
+ const pkg = require('../package.json');
6
+
7
+ // Import commands
8
+ const init = require('../lib/commands/init');
9
+ const select = require('../lib/commands/select');
10
+ const build = require('../lib/commands/build');
11
+ const deploy = require('../lib/commands/deploy');
12
+ const dev = require('../lib/commands/dev');
13
+
14
+ // ASCII Banner
15
+ console.log(chalk.cyan(`
16
+ _____ ________ _______________
17
+ / ___// ____/ / / ____/ ____/_ /
18
+ \\__ \\/ __/ / / / __/ / / / /
19
+ ___/ / /___/ /___/ /___/ /___ / /
20
+ /____/_____/_____/_____/\\____/ /_/
21
+ `));
22
+ console.log(chalk.gray(` v${pkg.version} - Build for web & desktop\n`));
23
+
24
+ program
25
+ .name('slct')
26
+ .description('Build and deploy apps for multiple platforms')
27
+ .version(pkg.version);
28
+
29
+ // slct init - Initialize a new project
30
+ program
31
+ .command('init [name]')
32
+ .description('Initialize a new Select project')
33
+ .option('-t, --template <template>', 'Project template (react, vue, vanilla)', 'react')
34
+ .action(init);
35
+
36
+ // select - Interactive platform selection
37
+ program
38
+ .command('select')
39
+ .alias('s')
40
+ .description('Interactively select target platforms')
41
+ .action(select);
42
+
43
+ // slct build - Build for platforms
44
+ program
45
+ .command('build [target]')
46
+ .description('Build for target platform (web, desktop, all)')
47
+ .option('--tauri', 'Use Tauri for desktop (default)')
48
+ .option('--electron', 'Use Electron for desktop')
49
+ .option('--release', 'Build for production release')
50
+ .action(build);
51
+
52
+ // slct deploy - Deploy to Select marketplace
53
+ program
54
+ .command('deploy')
55
+ .description('Deploy your app to Select marketplace')
56
+ .option('--prod', 'Deploy to production')
57
+ .action(deploy);
58
+
59
+ // slct dev - Start development server
60
+ program
61
+ .command('dev')
62
+ .description('Start development server')
63
+ .option('-p, --port <port>', 'Port number', '5173')
64
+ .action(dev);
65
+
66
+ // Parse arguments
67
+ program.parse();
@@ -0,0 +1,405 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const { execSync, spawn } = require('child_process');
5
+ const ora = require('ora');
6
+ const inquirer = require('inquirer');
7
+
8
+ async function build(target, options) {
9
+ console.log(chalk.bold('\n🔨 Select Build\n'));
10
+
11
+ // Load config
12
+ const configPath = path.resolve(process.cwd(), 'select.json');
13
+
14
+ if (!await fs.pathExists(configPath)) {
15
+ console.log(chalk.red('❌ No select.json found. Run "slct init" first.\n'));
16
+ process.exit(1);
17
+ }
18
+
19
+ const config = await fs.readJson(configPath);
20
+
21
+ // If no target specified, ask
22
+ if (!target) {
23
+ const answers = await inquirer.prompt([
24
+ {
25
+ type: 'list',
26
+ name: 'target',
27
+ message: 'Build target:',
28
+ choices: [
29
+ { name: 'Web', value: 'web' },
30
+ { name: 'Desktop (Windows/macOS/Linux)', value: 'desktop' },
31
+ { name: 'All platforms', value: 'all' }
32
+ ]
33
+ }
34
+ ]);
35
+ target = answers.target;
36
+ }
37
+
38
+ // Build based on target
39
+ try {
40
+ if (target === 'all') {
41
+ await buildWeb(config, options);
42
+ await buildDesktop(config, options);
43
+ } else if (target === 'web') {
44
+ await buildWeb(config, options);
45
+ } else if (target === 'desktop') {
46
+ await buildDesktop(config, options);
47
+ } else {
48
+ console.log(chalk.red(`❌ Unknown target: ${target}`));
49
+ console.log(chalk.gray(' Valid targets: web, desktop, all\n'));
50
+ process.exit(1);
51
+ }
52
+ } catch (error) {
53
+ console.error(chalk.red(`\n❌ Build failed: ${error.message}\n`));
54
+ process.exit(1);
55
+ }
56
+ }
57
+
58
+ // ==================== WEB BUILD ====================
59
+ async function buildWeb(config, options) {
60
+ console.log(chalk.blue('\n📦 Building for Web...\n'));
61
+
62
+ const spinner = ora('Running build command...').start();
63
+
64
+ try {
65
+ execSync(config.build?.command || 'npm run build', {
66
+ stdio: 'inherit',
67
+ cwd: process.cwd()
68
+ });
69
+ spinner.succeed('Web build complete!');
70
+ console.log(chalk.gray(` Output: ${config.build?.outDir || 'dist'}/\n`));
71
+ } catch (error) {
72
+ spinner.fail('Web build failed');
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ // ==================== DESKTOP BUILD ====================
78
+ async function buildDesktop(config, options) {
79
+ const framework = options.electron ? 'electron' : (config.desktop?.framework || 'tauri');
80
+
81
+ console.log(chalk.blue(`\n🖥️ Building for Desktop (${framework})...\n`));
82
+
83
+ if (framework === 'tauri') {
84
+ await buildTauri(config, options);
85
+ } else {
86
+ await buildElectron(config, options);
87
+ }
88
+ }
89
+
90
+ async function buildTauri(config, options) {
91
+ // Check if Tauri is installed
92
+ const tauriInstalled = await checkTauriInstalled();
93
+
94
+ if (!tauriInstalled) {
95
+ console.log(chalk.yellow('\n⚠️ Tauri is not installed. Setting up...\n'));
96
+ await setupTauri();
97
+ }
98
+
99
+ // Check if Tauri is initialized in project
100
+ const tauriConfigPath = path.join(process.cwd(), 'src-tauri', 'tauri.conf.json');
101
+ if (!await fs.pathExists(tauriConfigPath)) {
102
+ console.log(chalk.yellow('⚠️ Tauri not initialized in project. Initializing...\n'));
103
+
104
+ try {
105
+ execSync('npx tauri init', { stdio: 'inherit' });
106
+ } catch (error) {
107
+ console.log(chalk.red('\n❌ Failed to initialize Tauri.'));
108
+ await showTauriSetupInstructions();
109
+ process.exit(1);
110
+ }
111
+ }
112
+
113
+ // Build
114
+ const spinner = ora('Building with Tauri...').start();
115
+ try {
116
+ const buildCmd = options.release ? 'npx tauri build' : 'npx tauri build --debug';
117
+ execSync(buildCmd, { stdio: 'inherit' });
118
+ spinner.succeed('Tauri build complete!');
119
+ console.log(chalk.gray(' Output: src-tauri/target/release/\n'));
120
+ } catch (error) {
121
+ spinner.fail('Tauri build failed');
122
+ await showTauriSetupInstructions();
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ async function buildElectron(config, options) {
128
+ // Check if Electron builder is installed
129
+ const electronInstalled = await checkPackageInstalled('electron');
130
+
131
+ if (!electronInstalled) {
132
+ console.log(chalk.yellow('\n⚠️ Electron is not installed. Setting up...\n'));
133
+
134
+ const spinner = ora('Installing Electron dependencies...').start();
135
+ try {
136
+ execSync('npm install --save-dev electron electron-builder', { stdio: 'pipe' });
137
+ spinner.succeed('Electron installed!');
138
+ } catch (error) {
139
+ spinner.fail('Failed to install Electron');
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ // Check if electron main file exists
145
+ const mainPath = path.join(process.cwd(), 'electron', 'main.js');
146
+ if (!await fs.pathExists(mainPath)) {
147
+ console.log(chalk.yellow('⚠️ Creating Electron configuration...\n'));
148
+ await createElectronConfig(config);
149
+ }
150
+
151
+ // Build
152
+ const spinner = ora('Building with Electron...').start();
153
+ try {
154
+ execSync('npx electron-builder', { stdio: 'inherit' });
155
+ spinner.succeed('Electron build complete!');
156
+ console.log(chalk.gray(' Output: dist-electron/\n'));
157
+ } catch (error) {
158
+ spinner.fail('Electron build failed');
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ // ==================== MOBILE BUILD ====================
164
+ async function buildMobile(config, options) {
165
+ console.log(chalk.blue('\n📱 Building for Mobile (Capacitor)...\n'));
166
+
167
+ // Check if Capacitor is installed
168
+ const capInstalled = await checkPackageInstalled('@capacitor/core');
169
+
170
+ if (!capInstalled) {
171
+ console.log(chalk.yellow('\n⚠️ Capacitor is not installed. Setting up...\n'));
172
+ await setupCapacitor(config);
173
+ }
174
+
175
+ // Build web first
176
+ await buildWeb(config, options);
177
+
178
+ // Sync Capacitor
179
+ const spinner = ora('Syncing Capacitor...').start();
180
+ try {
181
+ execSync('npx cap sync', { stdio: 'pipe' });
182
+ spinner.succeed('Capacitor synced!');
183
+ } catch (error) {
184
+ spinner.fail('Capacitor sync failed');
185
+ throw error;
186
+ }
187
+
188
+ console.log(chalk.green('\n✅ Mobile build ready!'));
189
+ console.log(chalk.gray(`
190
+ To build native apps:
191
+
192
+ Android:
193
+ ${chalk.cyan('npx cap open android')}
194
+ Build in Android Studio
195
+
196
+ iOS:
197
+ ${chalk.cyan('npx cap open ios')}
198
+ Build in Xcode
199
+ `));
200
+ }
201
+
202
+ // ==================== SETUP FUNCTIONS ====================
203
+ async function checkTauriInstalled() {
204
+ try {
205
+ execSync('cargo --version', { stdio: 'pipe' });
206
+ return true;
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ async function checkPackageInstalled(packageName) {
213
+ try {
214
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
215
+ const pkg = await fs.readJson(packageJsonPath);
216
+ return !!(pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName]);
217
+ } catch {
218
+ return false;
219
+ }
220
+ }
221
+
222
+ async function showTauriSetupInstructions() {
223
+ console.log(chalk.yellow(`
224
+ ╔════════════════════════════════════════════════════════════════╗
225
+ ║ TAURI SETUP REQUIRED ║
226
+ ╚════════════════════════════════════════════════════════════════╝
227
+
228
+ Tauri requires Rust and platform-specific build tools.
229
+
230
+ ${chalk.bold('Step 1: Install Rust')}
231
+ ${chalk.cyan(' https://rustup.rs/')}
232
+ Run: ${chalk.white('curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh')}
233
+ Or on Windows: Download and run rustup-init.exe
234
+
235
+ ${chalk.bold('Step 2: Install Visual Studio Build Tools (Windows Only)')}
236
+ ${chalk.cyan(' https://visualstudio.microsoft.com/visual-cpp-build-tools/')}
237
+
238
+ Required components:
239
+ ${chalk.white('• MSVC v143 - VS 2022 C++ x64/x86 build tools')}
240
+ ${chalk.white('• Windows 10/11 SDK')}
241
+ ${chalk.white('• C++ CMake tools for Windows')}
242
+
243
+ ${chalk.bold('Step 3: Install Tauri CLI')}
244
+ ${chalk.cyan('npm install -g @tauri-apps/cli')}
245
+
246
+ ${chalk.bold('Step 4: Verify Installation')}
247
+ ${chalk.cyan('rustc --version')}
248
+ ${chalk.cyan('cargo --version')}
249
+
250
+ ${chalk.bold('Step 5: Re-run Select Build')}
251
+ ${chalk.cyan('slct build desktop')}
252
+
253
+ ${chalk.gray('For more info: https://tauri.app/v1/guides/getting-started/prerequisites')}
254
+ `));
255
+ }
256
+
257
+ async function setupTauri() {
258
+ await showTauriSetupInstructions();
259
+
260
+ const answers = await inquirer.prompt([
261
+ {
262
+ type: 'confirm',
263
+ name: 'continue',
264
+ message: 'Have you installed Rust and build tools?',
265
+ default: false
266
+ }
267
+ ]);
268
+
269
+ if (!answers.continue) {
270
+ console.log(chalk.yellow('\n⚠️ Please install the requirements and try again.\n'));
271
+ process.exit(1);
272
+ }
273
+
274
+ // Install Tauri CLI
275
+ const spinner = ora('Installing Tauri CLI...').start();
276
+ try {
277
+ execSync('npm install --save-dev @tauri-apps/cli @tauri-apps/api', { stdio: 'pipe' });
278
+ spinner.succeed('Tauri CLI installed!');
279
+ } catch (error) {
280
+ spinner.fail('Failed to install Tauri CLI');
281
+ throw error;
282
+ }
283
+ }
284
+
285
+ async function setupCapacitor(config) {
286
+ console.log(chalk.blue(`
287
+ ╔════════════════════════════════════════════════════════════════╗
288
+ ║ CAPACITOR SETUP ║
289
+ ╚════════════════════════════════════════════════════════════════╝
290
+
291
+ Setting up Capacitor for mobile builds...
292
+ `));
293
+
294
+ const spinner = ora('Installing Capacitor...').start();
295
+ try {
296
+ execSync('npm install @capacitor/core @capacitor/cli', { stdio: 'pipe' });
297
+ spinner.succeed('Capacitor core installed!');
298
+ } catch (error) {
299
+ spinner.fail('Failed to install Capacitor');
300
+ throw error;
301
+ }
302
+
303
+ // Initialize Capacitor
304
+ const spinner2 = ora('Initializing Capacitor...').start();
305
+ try {
306
+ execSync(`npx cap init "${config.name}" com.select.${config.name.replace(/-/g, '')} --web-dir dist`, {
307
+ stdio: 'pipe'
308
+ });
309
+ spinner2.succeed('Capacitor initialized!');
310
+ } catch (error) {
311
+ spinner2.fail('Failed to initialize Capacitor');
312
+ throw error;
313
+ }
314
+
315
+ // Ask which platforms to add
316
+ const answers = await inquirer.prompt([
317
+ {
318
+ type: 'checkbox',
319
+ name: 'platforms',
320
+ message: 'Add mobile platforms:',
321
+ choices: [
322
+ { name: 'Android', value: 'android', checked: true },
323
+ { name: 'iOS', value: 'ios', checked: true }
324
+ ]
325
+ }
326
+ ]);
327
+
328
+ for (const platform of answers.platforms) {
329
+ const spinner3 = ora(`Adding ${platform}...`).start();
330
+ try {
331
+ execSync(`npm install @capacitor/${platform}`, { stdio: 'pipe' });
332
+ execSync(`npx cap add ${platform}`, { stdio: 'pipe' });
333
+ spinner3.succeed(`${platform} added!`);
334
+ } catch (error) {
335
+ spinner3.fail(`Failed to add ${platform}`);
336
+ }
337
+ }
338
+
339
+ console.log(chalk.green('\n✅ Capacitor setup complete!\n'));
340
+
341
+ if (answers.platforms.includes('android')) {
342
+ console.log(chalk.gray(` Android: Install Android Studio from ${chalk.cyan('https://developer.android.com/studio')}`));
343
+ }
344
+ if (answers.platforms.includes('ios')) {
345
+ console.log(chalk.gray(` iOS: Requires macOS with Xcode installed`));
346
+ }
347
+ console.log('');
348
+ }
349
+
350
+ async function createElectronConfig(config) {
351
+ const electronDir = path.join(process.cwd(), 'electron');
352
+ await fs.ensureDir(electronDir);
353
+
354
+ // Create main.js
355
+ const mainJs = `const { app, BrowserWindow } = require('electron');
356
+ const path = require('path');
357
+
358
+ function createWindow() {
359
+ const win = new BrowserWindow({
360
+ width: 1200,
361
+ height: 800,
362
+ webPreferences: {
363
+ nodeIntegration: false,
364
+ contextIsolation: true
365
+ }
366
+ });
367
+
368
+ // Load the built app
369
+ win.loadFile(path.join(__dirname, '../dist/index.html'));
370
+ }
371
+
372
+ app.whenReady().then(createWindow);
373
+
374
+ app.on('window-all-closed', () => {
375
+ if (process.platform !== 'darwin') app.quit();
376
+ });
377
+
378
+ app.on('activate', () => {
379
+ if (BrowserWindow.getAllWindows().length === 0) createWindow();
380
+ });
381
+ `;
382
+
383
+ await fs.writeFile(path.join(electronDir, 'main.js'), mainJs);
384
+
385
+ // Update package.json
386
+ const pkgPath = path.join(process.cwd(), 'package.json');
387
+ const pkg = await fs.readJson(pkgPath);
388
+ pkg.main = 'electron/main.js';
389
+ pkg.build = {
390
+ appId: `com.select.${config.name.replace(/-/g, '')}`,
391
+ productName: config.name,
392
+ directories: {
393
+ output: 'dist-electron'
394
+ },
395
+ files: [
396
+ 'dist/**/*',
397
+ 'electron/**/*'
398
+ ]
399
+ };
400
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
401
+
402
+ console.log(chalk.green(' Electron configuration created!\n'));
403
+ }
404
+
405
+ module.exports = build;
@@ -0,0 +1,187 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const inquirer = require('inquirer');
5
+ const ora = require('ora');
6
+ const axios = require('axios');
7
+ const FormData = require('form-data');
8
+ const archiver = require('archiver');
9
+ const os = require('os');
10
+ const { glob } = require('glob'); // Using glob pattern matching if available, or manual find
11
+
12
+ async function deploy(options) {
13
+ console.log(chalk.bold('\n🚀 Deploy to Select\n'));
14
+
15
+ // Load config
16
+ const configPath = path.resolve(process.cwd(), 'select.json');
17
+ if (!await fs.pathExists(configPath)) {
18
+ console.log(chalk.red('❌ No select.json found. Run "slct init" first.\n'));
19
+ process.exit(1);
20
+ }
21
+ const config = await fs.readJson(configPath);
22
+ const API_URL = process.env.SELECT_API_URL || 'http://localhost:3001';
23
+
24
+ // Collect deploy information
25
+ const answers = await inquirer.prompt([
26
+ {
27
+ type: 'input',
28
+ name: 'name',
29
+ message: 'App name:',
30
+ default: config.name
31
+ },
32
+ {
33
+ type: 'input',
34
+ name: 'tagline',
35
+ message: 'Tagline:',
36
+ default: config.description || 'An awesome app'
37
+ },
38
+ {
39
+ type: 'list',
40
+ name: 'category',
41
+ message: 'Category:',
42
+ choices: ['Productivity', 'Finance', 'Developer Tools', 'Education', 'Entertainment', 'Utilities', 'Other'],
43
+ default: config.category || 'Productivity'
44
+ },
45
+ {
46
+ type: 'password',
47
+ name: 'deployKey',
48
+ message: 'Deploy Key:',
49
+ mask: '*'
50
+ },
51
+ {
52
+ type: 'confirm',
53
+ name: 'confirm',
54
+ message: `Deploy to ${API_URL}?`,
55
+ default: true
56
+ }
57
+ ]);
58
+
59
+ if (!answers.confirm) return;
60
+
61
+ const spinner = ora('Looking for build artifacts...').start();
62
+
63
+ // Prepare FormData
64
+ const form = new FormData();
65
+ const manifest = {
66
+ ...config,
67
+ name: answers.name,
68
+ tagline: answers.tagline,
69
+ category: answers.category.toLowerCase().replace(' ', '-'),
70
+ platforms: []
71
+ };
72
+
73
+ // Find artifacts (Tauri logic)
74
+ const tauriTarget = path.join(process.cwd(), 'src-tauri', 'target', 'release', 'bundle');
75
+ let foundArtifacts = 0;
76
+
77
+ // Windows
78
+ if (config.platforms.includes('windows')) {
79
+ const winPath = path.join(tauriTarget, 'nsis');
80
+ if (await fs.pathExists(winPath)) {
81
+ const files = await fs.readdir(winPath);
82
+ const exe = files.find(f => f.endsWith('.exe'));
83
+ if (exe) {
84
+ form.append('windows', fs.createReadStream(path.join(winPath, exe)));
85
+ manifest.platforms.push('windows');
86
+ foundArtifacts++;
87
+ spinner.text = `Found Windows binary: ${exe}`;
88
+ }
89
+ }
90
+ }
91
+
92
+ // macOS
93
+ if (config.platforms.includes('macos')) {
94
+ const macPath = path.join(tauriTarget, 'dmg');
95
+ if (await fs.pathExists(macPath)) {
96
+ const files = await fs.readdir(macPath);
97
+ const dmg = files.find(f => f.endsWith('.dmg'));
98
+ if (dmg) {
99
+ form.append('macos', fs.createReadStream(path.join(macPath, dmg)));
100
+ manifest.platforms.push('macos');
101
+ foundArtifacts++;
102
+ spinner.text = `Found macOS binary: ${dmg}`;
103
+ }
104
+ }
105
+ }
106
+
107
+ // Linux
108
+ if (config.platforms.includes('linux')) {
109
+ const linuxPath = path.join(tauriTarget, 'appimage');
110
+ if (await fs.pathExists(linuxPath)) {
111
+ const files = await fs.readdir(linuxPath);
112
+ const appImg = files.find(f => f.endsWith('.AppImage'));
113
+ if (appImg) {
114
+ form.append('linux', fs.createReadStream(path.join(linuxPath, appImg)));
115
+ manifest.platforms.push('linux');
116
+ foundArtifacts++;
117
+ spinner.text = `Found Linux binary: ${appImg}`;
118
+ }
119
+ }
120
+ }
121
+
122
+ // Web App
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++;
143
+ } else {
144
+ spinner.warn('Web build directory not found (skipping web upload)');
145
+ }
146
+ } else if (foundArtifacts === 0) { // Only error if NO artifacts found and web not requested
147
+ spinner.fail('No build artifacts found!');
148
+ console.log(chalk.yellow('\nTip: Run "slct build desktop" or "slct build web" first.\n'));
149
+ return;
150
+ }
151
+
152
+ form.append('manifest', JSON.stringify(manifest));
153
+
154
+ spinner.text = 'Uploading to Select...';
155
+
156
+ try {
157
+ const response = await axios.post(`${API_URL}/api/deploy`, form, {
158
+ headers: {
159
+ ...form.getHeaders(),
160
+ 'Authorization': `Bearer ${answers.deployKey}`
161
+ },
162
+ maxContentLength: Infinity,
163
+ maxBodyLength: Infinity
164
+ });
165
+
166
+ spinner.succeed('Deployment successful!');
167
+ console.log(chalk.green(`
168
+ ╔════════════════════════════════════════════════════════════════╗
169
+ ║ ✅ DEPLOYED SUCCESSFULLY ║
170
+ ╚════════════════════════════════════════════════════════════════╝
171
+
172
+ App: ${response.data.slug}
173
+ Version: ${response.data.version}
174
+ URL: ${API_URL}/apps/${response.data.slug}
175
+ `));
176
+
177
+ } catch (error) {
178
+ spinner.fail('Deployment failed');
179
+ if (error.response) {
180
+ console.error(chalk.red(`Server Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`));
181
+ } else {
182
+ console.error(chalk.red(`Error: ${error.message}`));
183
+ }
184
+ }
185
+ }
186
+
187
+ module.exports = deploy;
@@ -0,0 +1,55 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const { spawn } = require('child_process');
5
+
6
+ async function dev(options) {
7
+ console.log(chalk.bold('\n🔧 Starting Development Server\n'));
8
+
9
+ // Load config
10
+ const configPath = path.resolve(process.cwd(), 'select.json');
11
+ let devCommand = 'npm run dev';
12
+
13
+ if (await fs.pathExists(configPath)) {
14
+ const config = await fs.readJson(configPath);
15
+ devCommand = config.build?.devCommand || devCommand;
16
+ }
17
+
18
+ console.log(chalk.gray(` Running: ${devCommand}\n`));
19
+
20
+ // Parse command
21
+ const parts = devCommand.split(' ');
22
+ const cmd = parts[0];
23
+ const args = parts.slice(1);
24
+
25
+ // Add port if specified
26
+ if (options.port && options.port !== '5173') {
27
+ args.push('--port', options.port);
28
+ }
29
+
30
+ // Spawn dev server
31
+ const child = spawn(cmd, args, {
32
+ stdio: 'inherit',
33
+ shell: true,
34
+ cwd: process.cwd()
35
+ });
36
+
37
+ child.on('error', (error) => {
38
+ console.error(chalk.red(`\n❌ Failed to start dev server: ${error.message}\n`));
39
+ process.exit(1);
40
+ });
41
+
42
+ child.on('close', (code) => {
43
+ if (code !== 0) {
44
+ console.log(chalk.yellow(`\n⚠️ Dev server exited with code ${code}\n`));
45
+ }
46
+ });
47
+
48
+ // Handle Ctrl+C
49
+ process.on('SIGINT', () => {
50
+ child.kill('SIGINT');
51
+ process.exit(0);
52
+ });
53
+ }
54
+
55
+ module.exports = dev;
@@ -0,0 +1,295 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const inquirer = require('inquirer');
5
+ const ora = require('ora');
6
+
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);
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 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))
68
+ }
69
+ ]);
70
+
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';
121
+ }
122
+
123
+ await fs.writeJson(path.join(projectDir, 'package.json'), packageJson, { spaces: 2 });
124
+
125
+ // Create basic index.html
126
+ const indexHtml = `<!DOCTYPE html>
127
+ <html lang="en">
128
+ <head>
129
+ <meta charset="UTF-8">
130
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
131
+ <title>${name}</title>
132
+ </head>
133
+ <body>
134
+ <div id="app"></div>
135
+ <script type="module" src="/src/main.${answers.template === 'vanilla' ? 'js' : answers.template === 'react' ? 'jsx' : 'js'}"></script>
136
+ </body>
137
+ </html>`;
138
+
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'));
143
+
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'
149
+ import ReactDOM from 'react-dom/client'
150
+
151
+ function App() {
152
+ return (
153
+ <div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
154
+ <h1>Welcome to ${name}</h1>
155
+ <p>Built with Select CLI</p>
156
+ </div>
157
+ )
158
+ }
159
+
160
+ ReactDOM.createRoot(document.getElementById('app')).render(<App />)
161
+ `);
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;">
165
+ <h1>Welcome to ${name}</h1>
166
+ <p>Built with Select CLI</p>
167
+ </div>
168
+ \`
169
+ `);
170
+ }
171
+
172
+ spinner.succeed('Project created successfully!');
173
+
174
+ console.log(chalk.green(`
175
+ ✅ Project "${name}" created!
176
+
177
+ Next steps:
178
+ ${chalk.cyan(`cd ${name}`)}
179
+ ${chalk.cyan('npm install')}
180
+ ${chalk.cyan('slct build')}
181
+
182
+ `));
183
+
184
+ // Check for required dependencies
185
+ await checkDependencies(answers);
186
+
187
+ } catch (error) {
188
+ spinner.fail('Failed to create project');
189
+ console.error(chalk.red(error.message));
190
+ process.exit(1);
191
+ }
192
+ }
193
+
194
+ async function checkDependencies(answers) {
195
+ const hasDesktop = answers.platforms?.some(p => ['windows', 'macos', 'linux'].includes(p));
196
+
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
+ }
201
+ }
202
+
203
+ async function createGithubWorkflow(projectDir, framework, name) {
204
+ const workflowDir = path.join(projectDir, '.github', 'workflows');
205
+ await fs.ensureDir(workflowDir);
206
+
207
+ let workflowContent = '';
208
+
209
+ if (framework === 'tauri') {
210
+ workflowContent = `name: Build Apps
211
+ on:
212
+ push:
213
+ branches: [ main ]
214
+ pull_request:
215
+ branches: [ main ]
216
+
217
+ jobs:
218
+ build-tauri:
219
+ strategy:
220
+ fail-fast: false
221
+ matrix:
222
+ platform: [macos-latest, ubuntu-22.04, windows-latest]
223
+
224
+ runs-on: \${{ matrix.platform }}
225
+ steps:
226
+ - uses: actions/checkout@v4
227
+
228
+ - name: setup node
229
+ uses: actions/setup-node@v4
230
+ with:
231
+ node-version: lts/*
232
+
233
+ - name: install Rust stable
234
+ uses: dtolnay/rust-toolchain@stable
235
+
236
+ - name: install dependencies (ubuntu only)
237
+ if: matrix.platform == 'ubuntu-22.04'
238
+ run: |
239
+ sudo apt-get update
240
+ sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
241
+
242
+ - name: install frontend dependencies
243
+ run: npm install
244
+
245
+ - name: build tauri
246
+ uses: tauri-apps/tauri-action@v0
247
+ env:
248
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
249
+ with:
250
+ tagName: app-v__VERSION__ # the action automatically replaces \`__VERSION__\` with the app version
251
+ releaseName: 'App v__VERSION__'
252
+ releaseBody: 'See the assets to download this version and install.'
253
+ releaseDraft: true
254
+ prerelease: false
255
+ `;
256
+ } else {
257
+ // Electron Workflow
258
+ workflowContent = `name: Build Apps
259
+ on:
260
+ push:
261
+ branches: [ main ]
262
+ pull_request:
263
+ branches: [ main ]
264
+
265
+ jobs:
266
+ build-electron:
267
+ runs-on: \${{ matrix.os }}
268
+
269
+ strategy:
270
+ matrix:
271
+ os: [macos-latest, ubuntu-latest, windows-latest]
272
+
273
+ steps:
274
+ - name: Check out Git repository
275
+ uses: actions/checkout@v4
276
+
277
+ - name: Install Node.js
278
+ uses: actions/setup-node@v4
279
+ with:
280
+ node-version: lts/*
281
+
282
+ - name: Install Dependencies
283
+ run: npm install
284
+
285
+ - name: Build Electron App
286
+ run: npx electron-builder --publish always
287
+ env:
288
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
289
+ `;
290
+ }
291
+
292
+ await fs.writeFile(path.join(workflowDir, 'build.yml'), workflowContent);
293
+ }
294
+
295
+ module.exports = init;
@@ -0,0 +1,64 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const inquirer = require('inquirer');
5
+
6
+ async function select() {
7
+ console.log(chalk.bold('\n🎯 Select Target Platforms\n'));
8
+
9
+ // Check if select.json exists
10
+ const configPath = path.resolve(process.cwd(), 'select.json');
11
+
12
+ if (!await fs.pathExists(configPath)) {
13
+ console.log(chalk.red('❌ No select.json found. Run "slct init" first.\n'));
14
+ process.exit(1);
15
+ }
16
+
17
+ const config = await fs.readJson(configPath);
18
+
19
+ const answers = await inquirer.prompt([
20
+ {
21
+ type: 'checkbox',
22
+ name: 'platforms',
23
+ message: 'Select platforms to build for:',
24
+ choices: [
25
+ { name: 'Web', value: 'web', checked: config.platforms?.includes('web') },
26
+ { name: 'Windows', value: 'windows', checked: config.platforms?.includes('windows') },
27
+ { name: 'macOS', value: 'macos', checked: config.platforms?.includes('macos') },
28
+ { name: 'Linux', value: 'linux', checked: config.platforms?.includes('linux') }
29
+ ],
30
+ validate: (input) => input.length > 0 || 'Select at least one platform'
31
+ }
32
+ ]);
33
+
34
+ const hasDesktop = answers.platforms.some(p => ['windows', 'macos', 'linux'].includes(p));
35
+
36
+ if (hasDesktop) {
37
+ const desktopAnswer = await inquirer.prompt([
38
+ {
39
+ type: 'list',
40
+ name: 'desktopFramework',
41
+ message: 'Desktop framework:',
42
+ choices: [
43
+ { name: 'Tauri (Recommended - smaller, faster)', value: 'tauri' },
44
+ { name: 'Electron (Larger, more compatible)', value: 'electron' }
45
+ ],
46
+ default: config.desktop?.framework || 'tauri'
47
+ }
48
+ ]);
49
+ config.desktop = { framework: desktopAnswer.desktopFramework };
50
+ }
51
+
52
+ // Update config
53
+ config.platforms = answers.platforms;
54
+ await fs.writeJson(configPath, config, { spaces: 2 });
55
+
56
+ console.log(chalk.green('\n✅ Configuration saved!'));
57
+ console.log(chalk.gray(` Platforms: ${answers.platforms.join(', ')}`));
58
+ if (hasDesktop) {
59
+ console.log(chalk.gray(` Desktop: ${config.desktop.framework}`));
60
+ }
61
+ console.log(chalk.cyan('\n Run "slct build" to build your app.\n'));
62
+ }
63
+
64
+ module.exports = select;
package/lib/index.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ init: require('./commands/init'),
3
+ select: require('./commands/select'),
4
+ build: require('./commands/build'),
5
+ deploy: require('./commands/deploy'),
6
+ dev: require('./commands/dev')
7
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@iselect/select",
3
+ "version": "1.0.0",
4
+ "description": "Build and deploy apps for web, desktop, and mobile from a single codebase",
5
+ "main": "lib/index.js",
6
+ "bin": {
7
+ "slct": "bin/select.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Tests coming soon\""
11
+ },
12
+ "keywords": [
13
+ "select",
14
+ "cli",
15
+ "tauri",
16
+ "electron",
17
+ "capacitor",
18
+ "cross-platform",
19
+ "build",
20
+ "deploy"
21
+ ],
22
+ "author": "Select",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "archiver": "^7.0.1",
26
+ "axios": "^1.13.2",
27
+ "chalk": "^4.1.2",
28
+ "commander": "^11.1.0",
29
+ "form-data": "^4.0.5",
30
+ "fs-extra": "^11.2.0",
31
+ "inquirer": "^8.2.6",
32
+ "ora": "^5.4.1",
33
+ "semver": "^7.5.4"
34
+ },
35
+ "engines": {
36
+ "node": ">=16.0.0"
37
+ }
38
+ }