plusui-native 0.2.108 → 0.2.109

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/src/index.js CHANGED
@@ -1,1358 +1,1358 @@
1
1
  #!/usr/bin/env node
2
-
3
- import { mkdir, readFile, stat, rm, readdir, writeFile, copyFile } from 'fs/promises';
4
- import { existsSync, watch, statSync, mkdirSync } from 'fs';
5
- import { exec, spawn, execSync } from 'child_process';
6
- import { join, dirname, basename, resolve } from 'path';
7
- import { fileURLToPath } from 'url';
8
- import { createInterface } from 'readline';
9
- import { createServer as createViteServer } from 'vite';
10
- import { runDoctor } from './doctor/index.js';
11
- import { TemplateManager } from '../templates/manager.js';
12
-
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = dirname(__filename);
15
-
16
- // Load package.json for version info
17
- const cliPackageJson = JSON.parse(
18
- await readFile(join(__dirname, '..', 'package.json'), 'utf8')
19
- );
20
-
21
- const COLORS = {
22
- reset: '\x1b[0m',
23
- bright: '\x1b[1m',
24
- dim: '\x1b[2m',
25
- green: '\x1b[32m',
26
- blue: '\x1b[34m',
27
- yellow: '\x1b[33m',
28
- red: '\x1b[31m',
29
- cyan: '\x1b[36m',
30
- magenta: '\x1b[35m',
31
- };
32
-
33
- function log(msg, color = 'reset') {
34
- console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
35
- }
36
-
37
- function logSection(title) {
38
- console.log(`\n${COLORS.bright}${COLORS.blue}=== ${title} ===${COLORS.reset}\n`);
39
- }
40
-
41
- function error(msg) {
42
- console.error(`${COLORS.red}Error: ${msg}${COLORS.reset}`);
43
- process.exit(1);
44
- }
45
-
46
- function checkTools() {
47
- const platform = process.platform;
48
- const required = [];
49
-
50
- const cmakePaths = [
51
- 'cmake',
52
- 'C:\\Program Files\\CMake\\bin\\cmake.exe',
53
- 'C:\\Program Files (x86)\\CMake\\bin\\cmake.exe',
54
- '/usr/local/bin/cmake',
55
- '/usr/bin/cmake'
56
- ];
57
-
58
- let cmakeFound = false;
59
- for (const p of cmakePaths) {
60
- try {
61
- execSync(`"${p}" --version`, { stdio: 'ignore' });
62
- cmakeFound = true;
63
- break;
64
- } catch { }
65
- }
66
-
67
- if (!cmakeFound) {
68
- if (platform === 'win32') {
69
- required.push({ name: 'CMake', install: 'winget install Kitware.CMake', auto: 'winget install -e --id Kitware.CMake' });
70
- } else if (platform === 'darwin') {
71
- required.push({ name: 'CMake', install: 'brew install cmake', auto: 'brew install cmake' });
72
- } else {
73
- required.push({ name: 'CMake', install: 'sudo apt install cmake', auto: 'sudo apt install cmake' });
74
- }
75
- }
76
-
77
- // Compiler / build tools check
78
- if (platform === 'win32') {
79
- // Use vswhere.exe first (same detection as `plusui doctor`)
80
- let vsFound = false;
81
- const vswherePath = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe';
82
- if (existsSync(vswherePath)) {
83
- try {
84
- const output = execSync(
85
- `"${vswherePath}" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`,
86
- { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8', timeout: 10000 }
87
- ).trim();
88
- if (output) vsFound = true;
89
- } catch { }
90
- }
91
-
92
- // Fallback: check common VS paths for years 2019–2026
93
- if (!vsFound) {
94
- const vsYears = ['2026', '2025', '2024', '2023', '2022', '2019'];
95
- const vsEditions = ['Community', 'Professional', 'Enterprise', 'BuildTools'];
96
- for (const year of vsYears) {
97
- for (const edition of vsEditions) {
98
- if (existsSync(`C:\\Program Files\\Microsoft Visual Studio\\${year}\\${edition}\\VC\\Tools\\MSVC`)) {
99
- vsFound = true;
100
- break;
101
- }
102
- }
103
- if (vsFound) break;
104
- }
105
- }
106
-
107
- if (!vsFound) {
108
- required.push({ name: 'Visual Studio (C++ workload)', install: 'Download from visualstudio.microsoft.com with C++ workload', auto: null });
109
- }
110
- } else if (platform === 'darwin') {
111
- try {
112
- execSync('clang++ --version', { stdio: 'ignore' });
113
- } catch {
114
- required.push({ name: 'Xcode', install: 'xcode-select --install', auto: 'xcode-select --install' });
115
- }
116
- } else {
117
- try {
118
- execSync('g++ --version', { stdio: 'ignore' });
119
- } catch {
120
- required.push({ name: 'GCC/Clang', install: 'sudo apt install build-essential', auto: 'sudo apt install build-essential' });
121
- }
122
- }
123
-
124
- if (required.length > 0) {
125
- log('\n=== Missing Required Tools ===', 'yellow');
126
-
127
- for (const tool of required) {
128
- log(`\n ${tool.name}`, 'bright');
129
- log(` Install: ${tool.install}`, 'reset');
130
- if (tool.auto) {
131
- log(` Run: ${tool.auto}`, 'green');
132
- }
133
- }
134
-
135
- log('\n');
136
- return { missing: required };
137
- }
138
- return null;
139
- }
140
-
141
- const USAGE = `
142
- ${COLORS.bright}${cliPackageJson.name}${COLORS.reset} v${cliPackageJson.version} - Build C++ desktop apps with web tech
143
-
144
- ${COLORS.bright}Usage:${COLORS.reset}
145
- plusui doctor Check development environment
146
- plusui doctor --fix Check and auto-install missing tools
147
- plusui create <name> Create a new PlusUI project
148
- plusui dev Run in development mode (Vite HMR + C++ app)
149
- plusui build Build for current platform (production)
150
- plusui build:frontend Build frontend only
151
- plusui build:backend Build C++ backend only
152
- plusui build:all Build for all platforms
153
- plusui run Run the built application
154
- plusui clean Clean build artifacts
155
- plusui connect Generate connection bindings for current app (aliases: bind, bindgen)
156
- plusui update Update all PlusUI packages to latest versions
157
- plusui help Show this help message
158
-
159
- ${COLORS.bright}Platform Builds:${COLORS.reset}
160
- plusui build:windows Build for Windows
161
- plusui build:macos Build for macOS
162
- plusui build:linux Build for Linux
163
-
164
- ${COLORS.bright}Asset Commands:${COLORS.reset}
165
- plusui icons [input] Generate platform icons from source icon
166
- plusui embed [platform] Embed resources for platform (win32/darwin/linux/all)
167
-
168
- ${COLORS.bright}Options:${COLORS.reset}
169
- -h, --help Show this help message
170
- -v, --version Show version number
171
- -t, --template Specify template (solid, react)
172
-
173
- ${COLORS.bright}Assets:${COLORS.reset}
174
- Place assets/icon.png (512x512+ recommended) for automatic icon generation.
175
- All assets in "assets/" are embedded into the binary for single-exe distribution.
176
- Embedded resources are accessible via plusui::resources::getResource("path").
177
- `;
178
-
179
- // Platform configuration
180
- const PLATFORMS = {
181
- win32: { name: 'Windows', folder: 'Windows', ext: '.exe', generator: null },
182
- darwin: { name: 'macOS', folder: 'MacOS', ext: '', generator: 'Xcode' },
183
- linux: { name: 'Linux', folder: 'Linux', ext: '', generator: 'Ninja' },
184
- android: { name: 'Android', folder: 'Android', ext: '.apk', generator: 'Ninja' },
185
- ios: { name: 'iOS', folder: 'iOS', ext: '.app', generator: 'Xcode' },
186
- };
187
-
188
- function getProjectName() {
189
- try {
190
- const pkg = JSON.parse(execSync('npm pkg get name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }));
191
- return typeof pkg === 'string' ? pkg.replace(/"/g, '') : basename(process.cwd());
192
- } catch {
193
- return basename(process.cwd());
194
- }
195
- }
196
-
197
- function getCMakePath() {
198
- const paths = [
199
- 'cmake',
200
- 'C:\\Program Files\\CMake\\bin\\cmake.exe',
201
- 'C:\\Program Files (x86)\\CMake\\bin\\cmake.exe',
202
- '/usr/local/bin/cmake',
203
- '/usr/bin/cmake'
204
- ];
205
- for (const p of paths) {
206
- try {
207
- execSync(`"${p}" --version`, { stdio: 'ignore' });
208
- return p;
209
- } catch { }
210
- }
211
- return 'cmake';
212
- }
213
-
214
- // Find vcvarsall.bat for Windows builds (needed for Ninja generator)
215
- let _vcvarsallCache = undefined;
216
- function findVcvarsall() {
217
- if (_vcvarsallCache !== undefined) return _vcvarsallCache;
218
-
219
- const vswherePath = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe';
220
- if (existsSync(vswherePath)) {
221
- try {
222
- const installPath = execSync(
223
- `"${vswherePath}" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`,
224
- { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8', timeout: 10000 }
225
- ).trim();
226
- if (installPath) {
227
- const vcvars = join(installPath, 'VC', 'Auxiliary', 'Build', 'vcvarsall.bat');
228
- if (existsSync(vcvars)) {
229
- _vcvarsallCache = vcvars;
230
- return vcvars;
231
- }
232
- }
233
- } catch { }
234
- }
235
-
236
- // Fallback: scan known paths
237
- const vsYears = ['2026', '2025', '2024', '2023', '2022', '2019'];
238
- const vsEditions = ['Community', 'Professional', 'Enterprise', 'BuildTools'];
239
- for (const year of vsYears) {
240
- for (const edition of vsEditions) {
241
- const vcvars = `C:\\Program Files\\Microsoft Visual Studio\\${year}\\${edition}\\VC\\Auxiliary\\Build\\vcvarsall.bat`;
242
- if (existsSync(vcvars)) {
243
- _vcvarsallCache = vcvars;
244
- return vcvars;
245
- }
246
- }
247
- }
248
-
249
- _vcvarsallCache = null;
250
- return null;
251
- }
252
-
253
- // Capture the full environment from vcvarsall.bat (cached per session)
254
- let _vcEnvCache = undefined;
255
- function getVcEnvironment() {
256
- if (_vcEnvCache !== undefined) return _vcEnvCache;
257
-
258
- if (process.platform !== 'win32') {
259
- _vcEnvCache = null;
260
- return null;
261
- }
262
-
263
- const vcvarsall = findVcvarsall();
264
- if (!vcvarsall) {
265
- _vcEnvCache = null;
266
- return null;
267
- }
268
-
269
- try {
270
- // Run vcvarsall and dump all environment variables
271
- const output = execSync(
272
- `cmd /c ""${vcvarsall}" x64 >nul 2>&1 && set"`,
273
- { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30000, shell: true }
274
- );
275
-
276
- // Parse "KEY=VALUE" lines into an env object
277
- const env = {};
278
- for (const line of output.split(/\r?\n/)) {
279
- const idx = line.indexOf('=');
280
- if (idx > 0) {
281
- env[line.substring(0, idx)] = line.substring(idx + 1);
282
- }
283
- }
284
-
285
- // Sanity check: we should have PATH and INCLUDE set
286
- if (env.PATH && env.INCLUDE) {
287
- _vcEnvCache = env;
288
- return env;
289
- }
290
- } catch { }
291
-
292
- _vcEnvCache = null;
293
- return null;
294
- }
295
-
296
- function runCMake(args, options = {}) {
297
- const cmake = getCMakePath();
298
-
299
- // On Windows, use the captured vcvarsall environment so cl.exe + ninja are in PATH
300
- if (process.platform === 'win32') {
301
- const vcEnv = getVcEnvironment();
302
- if (vcEnv) {
303
- return execSync(`"${cmake}" ${args}`, { stdio: 'inherit', env: vcEnv, ...options });
304
- }
305
- }
306
-
307
- return execSync(`"${cmake}" ${args}`, { stdio: 'inherit', ...options });
308
- }
309
-
310
- function getInstalledPackageVersion(packageName) {
311
- try {
312
- const result = execSync(`npm list ${packageName} --depth=0 --json`, {
313
- encoding: 'utf8',
314
- stdio: ['pipe', 'pipe', 'ignore']
315
- });
316
- const json = JSON.parse(result);
317
- if (json.dependencies && json.dependencies[packageName]) {
318
- return json.dependencies[packageName].version;
319
- }
320
- } catch {
321
- // Package not installed locally
322
- }
323
-
324
- // Try global installation
325
- try {
326
- const result = execSync(`npm list -g ${packageName} --depth=0 --json`, {
327
- encoding: 'utf8',
328
- stdio: ['pipe', 'pipe', 'ignore']
329
- });
330
- const json = JSON.parse(result);
331
- if (json.dependencies && json.dependencies[packageName]) {
332
- return json.dependencies[packageName].version;
333
- }
334
- } catch {
335
- return null;
336
- }
337
-
338
- return null;
339
- }
340
-
341
- function getLatestPackageVersion(packageName) {
342
- try {
343
- const result = execSync(`npm view ${packageName} version`, {
344
- encoding: 'utf8',
345
- stdio: ['pipe', 'pipe', 'ignore']
346
- });
347
- return result.trim();
348
- } catch {
349
- return null;
350
- }
351
- }
352
-
353
- function compareVersions(v1, v2) {
354
- const parts1 = v1.split('.').map(Number);
355
- const parts2 = v2.split('.').map(Number);
356
-
357
- for (let i = 0; i < 3; i++) {
358
- if (parts1[i] > parts2[i]) return 1;
359
- if (parts1[i] < parts2[i]) return -1;
360
- }
361
- return 0;
362
- }
363
-
364
- function showVersionInfo() {
365
- const packages = [
366
- cliPackageJson.name,
367
- 'plusui-native-core',
368
- 'plusui-native-builder',
369
- 'plusui-native-connect'
370
- ];
371
-
372
- logSection('PlusUI Package Versions');
373
-
374
- packages.forEach(pkg => {
375
- let version;
376
- if (pkg === cliPackageJson.name) {
377
- version = cliPackageJson.version;
378
- log(`${pkg}: ${COLORS.green}v${version}${COLORS.reset} ${COLORS.dim}(current)${COLORS.reset}`, 'reset');
379
- } else {
380
- version = getInstalledPackageVersion(pkg);
381
- if (version) {
382
- log(`${pkg}: ${COLORS.green}v${version}${COLORS.reset}`, 'reset');
383
- } else {
384
- log(`${pkg}: ${COLORS.dim}not installed${COLORS.reset}`, 'reset');
385
- }
386
- }
387
- });
388
-
389
- console.log('');
390
- }
391
-
392
- async function updatePlusUIPackages() {
393
- logSection('Updating PlusUI Packages');
394
-
395
- const packages = [
396
- cliPackageJson.name,
397
- 'plusui-native-core',
398
- 'plusui-native-builder',
399
- 'plusui-native-connect'
400
- ];
401
-
402
- log('Checking for updates...\n', 'blue');
403
-
404
- // Check if packages are installed locally or globally
405
- const isInProject = existsSync(join(process.cwd(), 'package.json'));
406
-
407
- if (isInProject) {
408
- let updatedCount = 0;
409
- let upToDateCount = 0;
410
- let installedCount = 0;
411
-
412
- for (const pkg of packages) {
413
- const currentVersion = getInstalledPackageVersion(pkg);
414
-
415
- if (!currentVersion) {
416
- const latestVersion = getLatestPackageVersion(pkg);
417
-
418
- if (!latestVersion) {
419
- log(`${COLORS.yellow}${pkg}: not installed (couldn't resolve latest version)${COLORS.reset}`);
420
- continue;
421
- }
422
-
423
- try {
424
- log(`${COLORS.blue}${pkg}: not installed → ${latestVersion}${COLORS.reset}`);
425
- execSync(`npm install ${pkg}@${latestVersion}`, {
426
- stdio: ['ignore', 'ignore', 'pipe'],
427
- encoding: 'utf8'
428
- });
429
- log(`${COLORS.green}✓ ${pkg} installed${COLORS.reset}`);
430
- installedCount++;
431
- } catch (e) {
432
- log(`${COLORS.red}✗ ${pkg} install failed${COLORS.reset}`);
433
- }
434
- continue;
435
- }
436
-
437
- // Get latest version from npm
438
- const latestVersion = getLatestPackageVersion(pkg);
439
-
440
- if (!latestVersion) {
441
- log(`${COLORS.yellow}${pkg}: couldn't check for updates${COLORS.reset}`);
442
- continue;
443
- }
444
-
445
- const comparison = compareVersions(latestVersion, currentVersion);
446
-
447
- if (comparison > 0) {
448
- // Newer version available
449
- try {
450
- log(`${COLORS.blue}${pkg}: ${currentVersion} → ${latestVersion}${COLORS.reset}`);
451
- execSync(`npm install ${pkg}@${latestVersion}`, {
452
- stdio: ['ignore', 'ignore', 'pipe'],
453
- encoding: 'utf8'
454
- });
455
- log(`${COLORS.green}✓ ${pkg} updated${COLORS.reset}`);
456
- updatedCount++;
457
- } catch (e) {
458
- log(`${COLORS.red}✗ ${pkg} update failed${COLORS.reset}`);
459
- }
460
- } else {
461
- // Already up to date
462
- log(`${COLORS.green}✓ ${pkg} v${currentVersion} (up to date)${COLORS.reset}`);
463
- upToDateCount++;
464
- }
465
- }
466
-
467
- console.log('');
468
- if (updatedCount > 0) {
469
- log(`Updated ${updatedCount} package${updatedCount !== 1 ? 's' : ''}`, 'green');
470
- }
471
- if (installedCount > 0) {
472
- log(`Installed ${installedCount} missing package${installedCount !== 1 ? 's' : ''}`, 'green');
473
- }
474
- if (upToDateCount > 0) {
475
- log(`${upToDateCount} package${upToDateCount !== 1 ? 's' : ''} already up to date`, 'dim');
476
- }
477
- } else {
478
- log('Updating global CLI package...', 'cyan');
479
-
480
- const currentVersion = cliPackageJson.version;
481
- const latestVersion = getLatestPackageVersion(cliPackageJson.name);
482
-
483
- if (!latestVersion) {
484
- log('Couldn\'t check for updates', 'yellow');
485
- return;
486
- }
487
-
488
- const comparison = compareVersions(latestVersion, currentVersion);
489
-
490
- if (comparison > 0) {
491
- try {
492
- log(`${COLORS.blue}${cliPackageJson.name}: ${currentVersion} → ${latestVersion}${COLORS.reset}`);
493
- execSync(`npm install -g ${cliPackageJson.name}@${latestVersion}`, { stdio: 'inherit' });
494
- log(`✓ ${cliPackageJson.name} updated successfully`, 'green');
495
- } catch (e) {
496
- log(`Failed to update ${cliPackageJson.name}`, 'red');
497
- }
498
- } else {
499
- log(`✓ ${cliPackageJson.name} v${currentVersion} (already up to date)`, 'green');
500
- }
501
- }
502
-
503
- console.log('');
504
- }
505
-
506
- function getAppBindgenPaths() {
507
- return {
508
- featuresDir: join(process.cwd(), 'src', 'features'),
509
- // Connections/ is now at the project root — shared by C++ and TS
510
- outputDir: join(process.cwd(), 'Connections'),
511
- };
512
- }
513
-
514
- function findLikelyProjectDirs(baseDir) {
515
- try {
516
- const entries = execSync('npm pkg get name', { cwd: baseDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
517
- if (entries) {
518
- // noop; just to ensure cwd is a Node project when possible
519
- }
520
- } catch { }
521
-
522
- const candidates = [];
523
- try {
524
- const dirs = execSync(process.platform === 'win32' ? 'dir /b /ad' : 'ls -1 -d */', {
525
- cwd: baseDir,
526
- encoding: 'utf8',
527
- stdio: ['ignore', 'pipe', 'ignore'],
528
- shell: true,
529
- })
530
- .split(/\r?\n/)
531
- .map(s => s.trim().replace(/[\\/]$/, ''))
532
- .filter(Boolean);
533
-
534
- for (const dirName of dirs) {
535
- const fullDir = join(baseDir, dirName);
536
- if (existsSync(join(fullDir, 'CMakeLists.txt')) && existsSync(join(fullDir, 'package.json'))) {
537
- candidates.push(dirName);
538
- }
539
- }
540
- } catch { }
541
-
542
- return candidates;
543
- }
544
-
545
- function ensureProjectRoot(commandName) {
546
- const hasCMake = existsSync(join(process.cwd(), 'CMakeLists.txt'));
547
- const hasPackage = existsSync(join(process.cwd(), 'package.json'));
548
-
549
- if (hasCMake && hasPackage) {
550
- return;
551
- }
552
-
553
- const likelyDirs = findLikelyProjectDirs(process.cwd());
554
- let hint = '';
555
-
556
- if (likelyDirs.length === 1) {
557
- hint = `\n\nHint: you may be in a parent folder. Try:\n cd ${likelyDirs[0]}\n plusui ${commandName}`;
558
- } else if (likelyDirs.length > 1) {
559
- hint = `\n\nHint: run this command from your app folder (one containing CMakeLists.txt and package.json).`;
560
- }
561
-
562
- error(`This command must be run from a PlusUI project root (missing CMakeLists.txt and/or package.json in ${process.cwd()}).${hint}`);
563
- }
564
-
565
- function ensureBuildLayout() {
566
- const buildRoot = join(process.cwd(), 'build');
567
- for (const platform of Object.values(PLATFORMS)) {
568
- mkdirSync(join(buildRoot, platform.folder), { recursive: true });
569
- }
570
- }
571
-
572
- function getDevBuildDir() {
573
- const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
574
- return join('.plusui', 'dev', platformFolder);
575
- }
576
-
577
- function resolveBindgenScriptPath() {
578
- const candidates = [
579
- resolve(__dirname, '../../plusui-connect/src/connect.js'),
580
- resolve(__dirname, '../../plusui-connect/src/index.js'),
581
- resolve(__dirname, '../../plusui-native-connect/src/connect.js'),
582
- resolve(__dirname, '../../plusui-native-connect/src/index.js'),
583
- resolve(__dirname, '../../plusui-native-bindgen/src/index.js'),
584
- resolve(__dirname, '../../../plusui-native-connect/src/connect.js'),
585
- resolve(__dirname, '../../../plusui-native-connect/src/index.js'),
586
- resolve(__dirname, '../../../plusui-native-bindgen/src/index.js'),
587
- resolve(process.cwd(), 'node_modules', 'plusui-native-connect', 'src', 'connect.js'),
588
- resolve(process.cwd(), 'node_modules', 'plusui-native-connect', 'src', 'index.js'),
589
- resolve(process.cwd(), 'node_modules', 'plusui-native-bindgen', 'src', 'index.js'),
590
- ];
591
-
592
- for (const candidate of candidates) {
593
- if (existsSync(candidate)) {
594
- return candidate;
595
- }
596
- }
597
-
598
- return null;
599
- }
600
-
601
- async function syncGeneratedTsBindings(backendOutputDir, frontendOutputDir) {
602
- const generatedTsPath = join(backendOutputDir, 'bindings.gen.ts');
603
- if (!existsSync(generatedTsPath)) {
604
- return;
605
- }
606
-
607
- await mkdir(frontendOutputDir, { recursive: true });
608
- const frontendTsPath = join(frontendOutputDir, 'bindings.gen.ts');
609
- await copyFile(generatedTsPath, frontendTsPath);
610
- log(`Synced TS bindings: ${frontendTsPath}`, 'dim');
611
- }
612
-
613
- function promptTemplateSelection() {
614
- return new Promise((resolve) => {
615
- const rl = createInterface({
616
- input: process.stdin,
617
- output: process.stdout
618
- });
619
-
620
- console.log(`\n${COLORS.bright}Select a template:${COLORS.reset}`);
621
- console.log(` ${COLORS.cyan}1)${COLORS.reset} solid - SolidJS + TypeScript (lightweight, reactive)`);
622
- console.log(` ${COLORS.cyan}2)${COLORS.reset} react - React + TypeScript (popular, familiar)`);
623
-
624
- rl.question(`\n${COLORS.yellow}Enter choice [1-2] (default: solid):${COLORS.reset} `, (answer) => {
625
- rl.close();
626
- const choice = answer.trim() || '1';
627
- const templates = { '1': 'solid', '2': 'react' };
628
- const template = templates[choice] || 'solid';
629
- console.log(`${COLORS.green}✓${COLORS.reset} Selected: ${COLORS.cyan}${template}${COLORS.reset}\n`);
630
- resolve(template);
631
- });
632
- });
633
- }
634
-
635
- async function createProject(name, options = {}) {
636
- try {
637
- const templateManager = new TemplateManager();
638
- await templateManager.create(name, options);
639
- } catch (e) {
640
- error(e.message);
641
- }
642
- }
643
-
644
- // ============================================================
645
- // BUILD FUNCTIONS
646
- // ============================================================
647
-
648
- function buildFrontend() {
649
- ensureProjectRoot('build:frontend');
650
- logSection('Building Frontend');
651
-
652
- if (existsSync('frontend')) {
653
- log('Running Vite build...', 'blue');
654
- execSync('cd frontend && npm run build', { stdio: 'inherit', shell: true });
655
- log('Frontend built successfully!', 'green');
656
- } else {
657
- log('No frontend directory found, skipping...', 'yellow');
658
- }
659
- }
660
-
661
- function buildBackend(platform = null, devMode = false) {
662
- ensureProjectRoot(devMode ? 'dev:backend' : 'build:backend');
663
- const targetPlatform = platform || process.platform;
664
- const platformConfig = PLATFORMS[targetPlatform] || PLATFORMS[Object.keys(PLATFORMS).find(k => PLATFORMS[k].folder.toLowerCase() === targetPlatform?.toLowerCase())];
665
-
666
- if (!platformConfig) {
667
- error(`Unsupported platform: ${targetPlatform}`);
668
- }
669
-
670
- logSection(`Building Backend (${platformConfig.name})`);
671
-
672
- const buildDir = `build/${platformConfig.folder}`;
673
-
674
- ensureBuildLayout();
675
-
676
- // Create build directory
677
- if (!existsSync(buildDir)) {
678
- execSync(`mkdir -p "${buildDir}"`, { stdio: 'ignore', shell: true });
679
- }
680
-
681
- // CMake configure
682
- let cmakeArgs = `-S . -B "${buildDir}" -DCMAKE_BUILD_TYPE=Release`;
683
-
684
- if (devMode) {
685
- cmakeArgs += ' -DPLUSUI_DEV_MODE=ON';
686
- } else {
687
- cmakeArgs += ' -DPLUSUI_DEV_MODE=OFF';
688
- }
689
-
690
- if (platformConfig.generator) {
691
- cmakeArgs += ` -G "${platformConfig.generator}"`;
692
- } else if (process.platform === 'win32') {
693
- // Use embedded debug info (/Z7) to avoid VS 2026 PDB creation bug (C1041)
694
- cmakeArgs += ' -DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=Embedded';
695
- }
696
-
697
- log(`Configuring CMake...`, 'blue');
698
- runCMake(cmakeArgs);
699
-
700
- // CMake build
701
- log(`Building...`, 'blue');
702
- runCMake(`--build "${buildDir}" --config Release`);
703
-
704
- log(`Backend built: ${buildDir}`, 'green');
705
-
706
- return buildDir;
707
- }
708
-
709
- async function generateIcons(inputPath = null) {
710
- ensureProjectRoot('icons');
711
- logSection('Generating Platform Icons');
712
-
713
- const { IconGenerator } = await import('./assets/icon-generator.js');
714
- const generator = new IconGenerator();
715
-
716
- const srcIcon = inputPath || join(process.cwd(), 'assets', 'icon.png');
717
- const outputBase = join(process.cwd(), 'assets', 'icons');
718
-
719
- if (!existsSync(srcIcon)) {
720
- log(`Icon not found: ${srcIcon}`, 'yellow');
721
- log('Place a 512x512+ PNG icon at assets/icon.png', 'dim');
722
- return;
723
- }
724
-
725
- await generator.generate(srcIcon, outputBase);
726
- }
727
-
728
- async function embedResources(platform = null) {
729
- ensureProjectRoot('embed');
730
- const targetPlatform = platform || process.platform;
731
- logSection(`Embedding Resources (${targetPlatform})`);
732
-
733
- const { ResourceEmbedder } = await import('./assets/resource-embedder.js');
734
- const embedder = new ResourceEmbedder({ verbose: true });
735
-
736
- const frontendDist = join(process.cwd(), 'frontend', 'dist');
737
- const outputDir = join(process.cwd(), 'generated', 'resources');
738
-
739
- if (!existsSync(frontendDist)) {
740
- log('Frontend not built yet. Run: plusui build:frontend', 'yellow');
741
- return;
742
- }
743
-
744
- if (platform === 'all') {
745
- await embedder.embedAll(frontendDist, outputDir);
746
- } else {
747
- await embedder.embed(frontendDist, outputDir, targetPlatform);
748
- }
749
- }
750
-
751
- async function embedAssets() {
752
- ensureProjectRoot('build');
753
- const assetsDir = join(process.cwd(), 'assets');
754
- if (!existsSync(assetsDir)) {
755
- try {
756
- await mkdir(assetsDir, { recursive: true });
757
- } catch (e) { }
758
- }
759
-
760
- logSection('Embedding Assets');
761
-
762
- // Always generate the header file, even if empty
763
- let headerContent = '#pragma once\n\n';
764
- headerContent += '// THIS FILE IS AUTO-GENERATED BY PLUSUI CLI\n';
765
- headerContent += '// DO NOT MODIFY MANUALLY\n\n';
766
-
767
- const files = existsSync(assetsDir)
768
- ? (await readdir(assetsDir)).filter(f => !statSync(join(assetsDir, f)).isDirectory())
769
- : [];
770
-
771
- if (files.length === 0) {
772
- log('No assets found in assets/ folder', 'dim');
773
- } else {
774
- for (const file of files) {
775
- const filePath = join(assetsDir, file);
776
- log(`Processing ${file}...`, 'dim');
777
-
778
- const varName = file.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
779
- const data = await readFile(filePath);
780
-
781
- headerContent += `static const unsigned char ASSET_${varName}[] = {`;
782
- for (let i = 0; i < data.length; i++) {
783
- if (i % 16 === 0) headerContent += '\n ';
784
- headerContent += `0x${data[i].toString(16).padStart(2, '0')}, `;
785
- }
786
- headerContent += '\n};\n';
787
- headerContent += `static const unsigned int ASSET_${varName}_LEN = ${data.length};\n\n`;
788
- }
789
- }
790
-
791
- const genDir = join(process.cwd(), 'generated');
792
- if (!existsSync(genDir)) await mkdir(genDir, { recursive: true });
793
-
794
- await writeFile(join(genDir, 'assets.h'), headerContent);
795
- log(`✓ Assets header generated: generated/assets.h`, 'green');
796
- }
797
-
798
-
799
-
800
- async function build(production = true) {
801
- ensureProjectRoot('build');
802
- logSection('Building PlusUI Application');
803
-
804
- // Embed assets
805
- await embedAssets();
806
-
807
- // Build frontend first
808
- if (production) {
809
- buildFrontend();
810
- }
811
-
812
- // Build backend
813
- const buildDir = buildBackend(null, !production);
814
-
815
- log('\nBuild complete!', 'green');
816
- return buildDir;
817
- }
818
-
819
- function buildAll() {
820
- ensureProjectRoot('build:all');
821
- logSection('Building for All Platforms');
822
-
823
- ensureBuildLayout();
824
-
825
- buildFrontend();
826
-
827
- const supportedPlatforms = ['win32', 'darwin', 'linux'];
828
-
829
- for (const platform of supportedPlatforms) {
830
- try {
831
- buildBackend(platform, false);
832
- } catch (e) {
833
- log(`Failed to build for ${PLATFORMS[platform].name}: ${e.message}`, 'yellow');
834
- }
835
- }
836
-
837
- log('\nAll platform builds complete!', 'green');
838
- log('\nOutput directories:', 'bright');
839
- log(' build/Windows/', 'cyan');
840
- log(' build/MacOS/', 'cyan');
841
- log(' build/Linux/', 'cyan');
842
- log(' build/Android/', 'cyan');
843
- log(' build/iOS/', 'cyan');
844
- }
845
-
846
- function buildPlatform(platform) {
847
- logSection(`Building for ${PLATFORMS[platform]?.name || platform}`);
848
- buildFrontend();
849
- buildBackend(platform, false);
850
- }
851
-
852
- // ============================================================
853
- // DEVELOPMENT FUNCTIONS
854
- // ============================================================
855
-
856
- let viteServer = null;
857
- let cppProcess = null;
858
-
859
- async function startViteServer() {
860
- ensureProjectRoot('dev:frontend');
861
- log('Starting Vite dev server...', 'blue');
862
-
863
- viteServer = await createViteServer({
864
- root: 'frontend',
865
- server: {
866
- port: 5173,
867
- strictPort: false,
868
- },
869
- });
870
-
871
- await viteServer.listen();
872
-
873
- const actualPort = viteServer.httpServer?.address()?.port || 5173;
874
- log(`Vite server: http://localhost:${actualPort}`, 'green');
875
- return viteServer;
876
- }
877
-
878
- async function startBackend() {
879
- ensureProjectRoot('dev');
880
- logSection('Building C++ Backend (Dev Mode)');
881
-
882
- const projectName = getProjectName();
883
- killProcessByName(projectName);
884
-
885
- const buildDir = getDevBuildDir();
886
-
887
- // On Windows, use embedded debug info (/Z7) to avoid VS 2026 PDB creation bug (C1041)
888
- let generatorArgs = '';
889
- if (process.platform === 'win32') {
890
- generatorArgs = ' -DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=Embedded';
891
- }
892
-
893
- // Auto-clean build dir if generator changed (e.g. VS → Ninja)
894
- const cacheFile = join(buildDir, 'CMakeCache.txt');
895
- if (existsSync(cacheFile)) {
896
- try {
897
- const cacheContent = execSync(`type "${cacheFile}"`, { encoding: 'utf8', shell: true, stdio: ['pipe', 'pipe', 'ignore'] });
898
- const generatorMatch = cacheContent.match(/CMAKE_GENERATOR:INTERNAL=(.*)/);
899
- const cachedGenerator = generatorMatch ? generatorMatch[1].trim() : '';
900
- const wantNinja = generatorArgs.includes('Ninja');
901
- if ((wantNinja && cachedGenerator !== 'Ninja') || (!wantNinja && cachedGenerator === 'Ninja')) {
902
- log(`Generator changed (${cachedGenerator} → ${wantNinja ? 'Ninja' : 'default'}), cleaning build dir...`, 'yellow');
903
- execSync(process.platform === 'win32' ? `rmdir /s /q "${buildDir}"` : `rm -rf "${buildDir}"`, { stdio: 'ignore', shell: true });
904
- }
905
- } catch { }
906
- }
907
-
908
- // Always configure with dev mode to ensure PLUSUI_DEV_MODE is set correctly
909
- log('Configuring CMake...', 'blue');
910
- runCMake(`-S . -B "${buildDir}" -DPLUSUI_DEV_MODE=ON${generatorArgs}`);
911
-
912
- log('Compiling...', 'blue');
913
- runCMake(`--build "${buildDir}"`);
914
-
915
- // Find executable
916
- let exePath;
917
- if (process.platform === 'win32') {
918
- // Ninja puts exe directly in build dir
919
- exePath = join(buildDir, `${projectName}.exe`);
920
- if (!existsSync(exePath)) {
921
- exePath = join(buildDir, 'bin', `${projectName}.exe`);
922
- }
923
- // Fallback for VS generator (Debug subfolder)
924
- if (!existsSync(exePath)) {
925
- exePath = join(buildDir, projectName, 'Debug', `${projectName}.exe`);
926
- }
927
- if (!existsSync(exePath)) {
928
- exePath = join(buildDir, 'Debug', `${projectName}.exe`);
929
- }
930
- } else {
931
- exePath = join(buildDir, projectName);
932
- if (!existsSync(exePath)) {
933
- exePath = join(buildDir, 'bin', projectName);
934
- }
935
- }
936
-
937
- if (!existsSync(exePath)) {
938
- error(`Executable not found. Expected: ${exePath}`);
939
- }
940
-
941
- log('Starting C++ app...', 'blue');
942
-
943
- cppProcess = spawn(exePath, [], {
944
- shell: true,
945
- stdio: 'inherit',
946
- env: { ...process.env, PLUSUI_DEV: '1' }
947
- });
948
-
949
- cppProcess.on('error', (err) => {
950
- log(`Failed to start app: ${err.message}`, 'red');
951
- });
952
-
953
- return cppProcess;
954
- }
955
-
956
- async function killPort(port) {
957
- log(`Checking port ${port}...`, 'dim');
958
- try {
959
- if (process.platform === 'win32') {
960
- try {
961
- const output = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
962
- const lines = output.split('\n').filter(line => line.includes(`:${port}`) && line.includes('LISTENING'));
963
-
964
- for (const line of lines) {
965
- const parts = line.trim().split(/\s+/);
966
- const pid = parts[parts.length - 1];
967
- if (pid && /^\d+$/.test(pid)) {
968
- log(`Killing process ${pid} on port ${port}...`, 'yellow');
969
- try {
970
- execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
971
- } catch (e) {
972
- // Ignore if already dead
973
- }
974
- }
975
- }
976
- } catch (e) {
977
- // findstr returns exit code 1 if no match found
978
- }
979
- } else {
980
- try {
981
- execSync(`lsof -i :${port} -t | xargs kill -9`, { stdio: 'ignore' });
982
- } catch {
983
- // Ignore if no process found
984
- }
985
- }
986
- } catch (e) {
987
- // Ignore general errors
988
- }
989
- }
990
-
991
- function killProcessByName(name) {
992
- const processName = process.platform === 'win32' ? `${name}.exe` : name;
993
- log(`Cleaning up previous instance (${processName})...`, 'dim');
994
- try {
995
- if (process.platform === 'win32') {
996
- execSync(`taskkill /IM "${processName}" /F`, { stdio: 'ignore' });
997
- } else {
998
- execSync(`pkill -f "${processName}"`, { stdio: 'ignore' });
999
- }
1000
- } catch (e) {
1001
- // Ignore if process not found
1002
- }
1003
- }
1004
-
1005
- async function dev() {
1006
- ensureProjectRoot('dev');
1007
- logSection('PlusUI Development Mode');
1008
-
1009
- const toolCheck = checkTools();
1010
- if (toolCheck && toolCheck.missing) {
1011
- error('Missing required build tools. Run: plusui doctor --fix');
1012
- }
1013
-
1014
- // Embed assets
1015
- await embedAssets();
1016
-
1017
- await runBindgen([], { skipIfNoInput: true, source: 'dev' });
1018
-
1019
- // specific port cleaning
1020
- await killPort(5173);
1021
-
1022
- // Start Vite first
1023
- try {
1024
- viteServer = await startViteServer();
1025
- } catch (e) {
1026
- log(`Vite failed to start: ${e.message}`, 'red');
1027
- error('Could not start development server');
1028
- }
1029
-
1030
- const actualPort = viteServer.httpServer?.address()?.port || 5173;
1031
-
1032
- // Build and start C++ backend
1033
- await startBackend();
1034
-
1035
- log('\n' + '='.repeat(50), 'dim');
1036
- log('Development mode active', 'green');
1037
- log('='.repeat(50), 'dim');
1038
- log(`\nFrontend: http://localhost:${actualPort} (HMR enabled)`, 'cyan');
1039
- log('Backend: C++ app running with webview', 'cyan');
1040
- log('\nEdit frontend/src/* for live reload', 'dim');
1041
- log('Edit main.cpp and restart for C++ changes', 'dim');
1042
- log('\nPress Ctrl+C to stop\n', 'yellow');
1043
-
1044
- // Handle shutdown
1045
- process.on('SIGINT', async () => {
1046
- log('\nShutting down...', 'yellow');
1047
- if (viteServer) await viteServer.close();
1048
- if (cppProcess) cppProcess.kill();
1049
- process.exit(0);
1050
- });
1051
- }
1052
-
1053
- async function devFrontend() {
1054
- ensureProjectRoot('dev:frontend');
1055
- logSection('Frontend Development Mode');
1056
-
1057
- // specific port cleaning
1058
- await killPort(5173);
1059
-
1060
- viteServer = await createViteServer({
1061
- root: 'frontend',
1062
- server: { port: 5173, strictPort: false },
1063
- });
1064
-
1065
- await viteServer.listen();
1066
-
1067
- const actualPort = viteServer.httpServer?.address()?.port || 5173;
1068
- log(`Frontend: http://localhost:${actualPort}`, 'green');
1069
- log('HMR enabled - changes will reflect instantly!\n', 'green');
1070
-
1071
- process.on('SIGINT', async () => {
1072
- if (viteServer) await viteServer.close();
1073
- process.exit(0);
1074
- });
1075
- }
1076
-
1077
- function devBackend() {
1078
- ensureProjectRoot('dev:backend');
1079
- logSection('Backend Development Mode');
1080
-
1081
- const projectName = getProjectName();
1082
- killProcessByName(projectName);
1083
-
1084
- const buildDir = getDevBuildDir();
1085
-
1086
- // Always configure with dev mode to ensure PLUSUI_DEV_MODE is set correctly
1087
- log('Configuring CMake...', 'blue');
1088
- runCMake(`-S . -B "${buildDir}" -DPLUSUI_DEV_MODE=ON`);
1089
-
1090
- runCMake(`--build "${buildDir}"`);
1091
-
1092
- let exePath;
1093
- if (process.platform === 'win32') {
1094
- exePath = join(buildDir, projectName, 'Debug', `${projectName}.exe`);
1095
- if (!existsSync(exePath)) exePath = join(buildDir, 'Debug', `${projectName}.exe`);
1096
- if (!existsSync(exePath)) exePath = join(buildDir, `${projectName}.exe`);
1097
- } else {
1098
- exePath = join(buildDir, projectName);
1099
- }
1100
-
1101
- function runApp() {
1102
- if (cppProcess) cppProcess.kill();
1103
-
1104
- cppProcess = spawn(exePath, [], { shell: true, stdio: 'inherit' });
1105
-
1106
- cppProcess.on('close', (code) => {
1107
- if (code !== null && code !== 0) {
1108
- log(`App exited with code ${code}`, 'yellow');
1109
- }
1110
- });
1111
- }
1112
-
1113
- runApp();
1114
-
1115
- log('\nWatching C++ files for changes...', 'yellow');
1116
- log('Press Ctrl+C to stop\n', 'dim');
1117
-
1118
- // Watch main.cpp and features/ folder (if exists)
1119
- const watchItems = ['main.cpp'];
1120
- if (existsSync('features')) watchItems.push('features');
1121
-
1122
- const watchers = watchItems.map(item => {
1123
- const isDir = existsSync(item) && statSync(item).isDirectory();
1124
- const w = isDir ? watch(item, { recursive: true }) : watch(item);
1125
- w.on('change', (eventType, filename) => {
1126
- const file = item === 'main.cpp' ? 'main.cpp' : filename;
1127
- if (file && (file.endsWith('.cpp') || file.endsWith('.hpp'))) {
1128
- log(`Changed: ${file}`, 'yellow');
1129
- log('Rebuilding...', 'blue');
1130
- try {
1131
- killProcessByName(projectName);
1132
- runCMake(`--build "${buildDir}"`);
1133
- runApp();
1134
- } catch (e) {
1135
- log('Build failed, waiting for fixes...', 'red');
1136
- }
1137
- }
1138
- });
1139
- return w;
1140
- });
1141
-
1142
- process.on('SIGINT', () => {
1143
- if (cppProcess) cppProcess.kill();
1144
- watchers.forEach(w => w.close());
1145
- process.exit(0);
1146
- });
1147
- }
1148
-
1149
- // ============================================================
1150
- // RUN FUNCTION
1151
- // ============================================================
1152
-
1153
- function run() {
1154
- ensureProjectRoot('run');
1155
- const projectName = getProjectName();
1156
- const platform = PLATFORMS[process.platform];
1157
- const buildDir = `build/${platform?.folder || 'Windows'}`;
1158
-
1159
- let exePath;
1160
- if (process.platform === 'win32') {
1161
- exePath = join(buildDir, 'Release', `${projectName}.exe`);
1162
- if (!existsSync(exePath)) exePath = join(buildDir, 'bin', `${projectName}.exe`);
1163
- if (!existsSync(exePath)) exePath = join(buildDir, `${projectName}.exe`);
1164
- } else {
1165
- exePath = join(buildDir, projectName);
1166
- if (!existsSync(exePath)) exePath = join(buildDir, 'bin', projectName);
1167
- }
1168
-
1169
- if (!existsSync(exePath)) {
1170
- error(`Build not found at ${exePath}. Run "plusui build" first.`);
1171
- }
1172
-
1173
- log(`Running: ${exePath}`, 'blue');
1174
-
1175
- const proc = spawn(exePath, [], { shell: true, stdio: 'inherit' });
1176
-
1177
- proc.on('close', (code) => {
1178
- process.exit(code);
1179
- });
1180
- }
1181
-
1182
- // ============================================================
1183
- // CLEAN FUNCTION
1184
- // ============================================================
1185
-
1186
- async function clean() {
1187
- ensureProjectRoot('clean');
1188
- logSection('Cleaning Build Artifacts');
1189
-
1190
- const dirs = ['build', '.plusui', 'frontend/dist'];
1191
-
1192
- for (const dir of dirs) {
1193
- if (existsSync(dir)) {
1194
- log(`Removing: ${dir}`, 'yellow');
1195
- await rm(dir, { recursive: true, force: true });
1196
- }
1197
- }
1198
-
1199
- log('\nClean complete!', 'green');
1200
- }
1201
-
1202
- // ============================================================
1203
- // CONNECT GENERATOR FUNCTION
1204
- // ============================================================
1205
-
1206
- async function runBindgen(providedArgs = null, options = {}) {
1207
- ensureProjectRoot('bindgen');
1208
- logSection('Running Connection Generator');
1209
-
1210
- const { skipIfNoInput = false, source = 'manual' } = options;
1211
-
1212
- const scriptPath = resolveBindgenScriptPath();
1213
-
1214
- if (!scriptPath) {
1215
- error(`Connection generator script not found. Please ensure plusui-native-connect is installed.`);
1216
- }
1217
-
1218
- log(`Using connect generator: ${scriptPath}`, 'dim');
1219
-
1220
- // plusui connect [projectRoot] [outputDir]
1221
- // Defaults to app-local paths when available.
1222
- const args = providedArgs ?? process.argv.slice(3);
1223
- let bindgenArgs = [...args];
1224
-
1225
- let usedDefaultAppMode = false;
1226
- let defaultOutputDir = null;
1227
-
1228
- if (bindgenArgs.length === 0) {
1229
- const { outputDir: appOutputDir } = getAppBindgenPaths();
1230
- bindgenArgs = [process.cwd(), appOutputDir];
1231
- usedDefaultAppMode = true;
1232
- defaultOutputDir = appOutputDir;
1233
- log(`Project mode: ${process.cwd()} -> ${appOutputDir}`, 'dim');
1234
- }
1235
-
1236
- // Spawn node process
1237
- const proc = spawn(process.execPath, [scriptPath, ...bindgenArgs], {
1238
- stdio: 'inherit',
1239
- env: process.env
1240
- });
1241
-
1242
- return new Promise((resolve, reject) => {
1243
- proc.on('close', async (code) => {
1244
- if (code === 0) {
1245
- try {
1246
- log('\nBindgen complete!', 'green');
1247
- resolve();
1248
- } catch (syncErr) {
1249
- reject(syncErr);
1250
- }
1251
- } else {
1252
- reject(new Error(`Connection generator failed with code ${code}`));
1253
- }
1254
- });
1255
- });
1256
- }
1257
-
1258
- // ============================================================
1259
- // MAIN
1260
- // ============================================================
1261
-
1262
- async function main() {
1263
- const args = process.argv.slice(2);
1264
- const cmd = args[0];
1265
-
1266
- switch (cmd) {
1267
- case 'doctor':
1268
- await runDoctor({
1269
- fix: args.includes('--fix'),
1270
- json: args.includes('--json'),
1271
- quick: args.includes('--quick')
1272
- });
1273
- break;
1274
- case 'create':
1275
- if (!args[1] || args[1].startsWith('-')) error('Project name required');
1276
- const template = await promptTemplateSelection();
1277
- await createProject(args[1], { template });
1278
- break;
1279
- case 'dev':
1280
- await dev();
1281
- break;
1282
- case 'dev:frontend':
1283
- await devFrontend();
1284
- break;
1285
- case 'dev:backend':
1286
- await runBindgen([], { skipIfNoInput: true, source: 'dev:backend' });
1287
- devBackend();
1288
- break;
1289
- case 'build':
1290
- await runBindgen([], { skipIfNoInput: true, source: 'build' });
1291
- await build(true);
1292
- break;
1293
- case 'build:frontend':
1294
- buildFrontend();
1295
- break;
1296
- case 'build:backend':
1297
- await runBindgen([], { skipIfNoInput: true, source: 'build:backend' });
1298
- buildBackend(null, false);
1299
- break;
1300
- case 'build:all':
1301
- await runBindgen([], { skipIfNoInput: true, source: 'build:all' });
1302
- buildAll();
1303
- break;
1304
- case 'build:windows':
1305
- await runBindgen([], { skipIfNoInput: true, source: 'build:windows' });
1306
- buildPlatform('win32');
1307
- break;
1308
- case 'build:macos':
1309
- await runBindgen([], { skipIfNoInput: true, source: 'build:macos' });
1310
- buildPlatform('darwin');
1311
- break;
1312
- case 'build:linux':
1313
- await runBindgen([], { skipIfNoInput: true, source: 'build:linux' });
1314
- buildPlatform('linux');
1315
- break;
1316
- case 'build:android':
1317
- await runBindgen([], { skipIfNoInput: true, source: 'build:android' });
1318
- buildPlatform('android');
1319
- break;
1320
- case 'build:ios':
1321
- await runBindgen([], { skipIfNoInput: true, source: 'build:ios' });
1322
- buildPlatform('ios');
1323
- break;
1324
- case 'run':
1325
- run();
1326
- break;
1327
- case 'clean':
1328
- await clean();
1329
- break;
1330
- case 'connect':
1331
- case 'bind':
1332
- case 'bindgen':
1333
- await runBindgen();
1334
- break;
1335
- case 'update':
1336
- await updatePlusUIPackages();
1337
- break;
1338
- case 'icons':
1339
- await generateIcons(args[1]);
1340
- break;
1341
- case 'embed':
1342
- await embedResources(args[1]);
1343
- break;
1344
- case 'help':
1345
- case '-h':
1346
- case '--help':
1347
- console.log(USAGE);
1348
- break;
1349
- case '-v':
1350
- case '--version':
1351
- showVersionInfo();
1352
- break;
1353
- default:
1354
- console.log(USAGE);
1355
- }
1356
- }
1357
-
1358
- main().catch(e => error(e.message));
2
+
3
+ import { mkdir, readFile, stat, rm, readdir, writeFile, copyFile } from 'fs/promises';
4
+ import { existsSync, watch, statSync, mkdirSync } from 'fs';
5
+ import { exec, spawn, execSync } from 'child_process';
6
+ import { join, dirname, basename, resolve } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ import { createInterface } from 'readline';
9
+ import { createServer as createViteServer } from 'vite';
10
+ import { runDoctor } from './doctor/index.js';
11
+ import { TemplateManager } from '../templates/manager.js';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ // Load package.json for version info
17
+ const cliPackageJson = JSON.parse(
18
+ await readFile(join(__dirname, '..', 'package.json'), 'utf8')
19
+ );
20
+
21
+ const COLORS = {
22
+ reset: '\x1b[0m',
23
+ bright: '\x1b[1m',
24
+ dim: '\x1b[2m',
25
+ green: '\x1b[32m',
26
+ blue: '\x1b[34m',
27
+ yellow: '\x1b[33m',
28
+ red: '\x1b[31m',
29
+ cyan: '\x1b[36m',
30
+ magenta: '\x1b[35m',
31
+ };
32
+
33
+ function log(msg, color = 'reset') {
34
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
35
+ }
36
+
37
+ function logSection(title) {
38
+ console.log(`\n${COLORS.bright}${COLORS.blue}=== ${title} ===${COLORS.reset}\n`);
39
+ }
40
+
41
+ function error(msg) {
42
+ console.error(`${COLORS.red}Error: ${msg}${COLORS.reset}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ function checkTools() {
47
+ const platform = process.platform;
48
+ const required = [];
49
+
50
+ const cmakePaths = [
51
+ 'cmake',
52
+ 'C:\\Program Files\\CMake\\bin\\cmake.exe',
53
+ 'C:\\Program Files (x86)\\CMake\\bin\\cmake.exe',
54
+ '/usr/local/bin/cmake',
55
+ '/usr/bin/cmake'
56
+ ];
57
+
58
+ let cmakeFound = false;
59
+ for (const p of cmakePaths) {
60
+ try {
61
+ execSync(`"${p}" --version`, { stdio: 'ignore' });
62
+ cmakeFound = true;
63
+ break;
64
+ } catch { }
65
+ }
66
+
67
+ if (!cmakeFound) {
68
+ if (platform === 'win32') {
69
+ required.push({ name: 'CMake', install: 'winget install Kitware.CMake', auto: 'winget install -e --id Kitware.CMake' });
70
+ } else if (platform === 'darwin') {
71
+ required.push({ name: 'CMake', install: 'brew install cmake', auto: 'brew install cmake' });
72
+ } else {
73
+ required.push({ name: 'CMake', install: 'sudo apt install cmake', auto: 'sudo apt install cmake' });
74
+ }
75
+ }
76
+
77
+ // Compiler / build tools check
78
+ if (platform === 'win32') {
79
+ // Use vswhere.exe first (same detection as `plusui doctor`)
80
+ let vsFound = false;
81
+ const vswherePath = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe';
82
+ if (existsSync(vswherePath)) {
83
+ try {
84
+ const output = execSync(
85
+ `"${vswherePath}" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`,
86
+ { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8', timeout: 10000 }
87
+ ).trim();
88
+ if (output) vsFound = true;
89
+ } catch { }
90
+ }
91
+
92
+ // Fallback: check common VS paths for years 2019–2026
93
+ if (!vsFound) {
94
+ const vsYears = ['2026', '2025', '2024', '2023', '2022', '2019'];
95
+ const vsEditions = ['Community', 'Professional', 'Enterprise', 'BuildTools'];
96
+ for (const year of vsYears) {
97
+ for (const edition of vsEditions) {
98
+ if (existsSync(`C:\\Program Files\\Microsoft Visual Studio\\${year}\\${edition}\\VC\\Tools\\MSVC`)) {
99
+ vsFound = true;
100
+ break;
101
+ }
102
+ }
103
+ if (vsFound) break;
104
+ }
105
+ }
106
+
107
+ if (!vsFound) {
108
+ required.push({ name: 'Visual Studio (C++ workload)', install: 'Download from visualstudio.microsoft.com with C++ workload', auto: null });
109
+ }
110
+ } else if (platform === 'darwin') {
111
+ try {
112
+ execSync('clang++ --version', { stdio: 'ignore' });
113
+ } catch {
114
+ required.push({ name: 'Xcode', install: 'xcode-select --install', auto: 'xcode-select --install' });
115
+ }
116
+ } else {
117
+ try {
118
+ execSync('g++ --version', { stdio: 'ignore' });
119
+ } catch {
120
+ required.push({ name: 'GCC/Clang', install: 'sudo apt install build-essential', auto: 'sudo apt install build-essential' });
121
+ }
122
+ }
123
+
124
+ if (required.length > 0) {
125
+ log('\n=== Missing Required Tools ===', 'yellow');
126
+
127
+ for (const tool of required) {
128
+ log(`\n ${tool.name}`, 'bright');
129
+ log(` Install: ${tool.install}`, 'reset');
130
+ if (tool.auto) {
131
+ log(` Run: ${tool.auto}`, 'green');
132
+ }
133
+ }
134
+
135
+ log('\n');
136
+ return { missing: required };
137
+ }
138
+ return null;
139
+ }
140
+
141
+ const USAGE = `
142
+ ${COLORS.bright}${cliPackageJson.name}${COLORS.reset} v${cliPackageJson.version} - Build C++ desktop apps with web tech
143
+
144
+ ${COLORS.bright}Usage:${COLORS.reset}
145
+ plusui doctor Check development environment
146
+ plusui doctor --fix Check and auto-install missing tools
147
+ plusui create <name> Create a new PlusUI project
148
+ plusui dev Run in development mode (Vite HMR + C++ app)
149
+ plusui build Build for current platform (production)
150
+ plusui build:frontend Build frontend only
151
+ plusui build:backend Build C++ backend only
152
+ plusui build:all Build for all platforms
153
+ plusui run Run the built application
154
+ plusui clean Clean build artifacts
155
+ plusui connect Generate connection bindings for current app (aliases: bind, bindgen)
156
+ plusui update Update all PlusUI packages to latest versions
157
+ plusui help Show this help message
158
+
159
+ ${COLORS.bright}Platform Builds:${COLORS.reset}
160
+ plusui build:windows Build for Windows
161
+ plusui build:macos Build for macOS
162
+ plusui build:linux Build for Linux
163
+
164
+ ${COLORS.bright}Asset Commands:${COLORS.reset}
165
+ plusui icons [input] Generate platform icons from source icon
166
+ plusui embed [platform] Embed resources for platform (win32/darwin/linux/all)
167
+
168
+ ${COLORS.bright}Options:${COLORS.reset}
169
+ -h, --help Show this help message
170
+ -v, --version Show version number
171
+ -t, --template Specify template (solid, react)
172
+
173
+ ${COLORS.bright}Assets:${COLORS.reset}
174
+ Place assets/icon.png (512x512+ recommended) for automatic icon generation.
175
+ All assets in "assets/" are embedded into the binary for single-exe distribution.
176
+ Embedded resources are accessible via plusui::resources::getResource("path").
177
+ `;
178
+
179
+ // Platform configuration
180
+ const PLATFORMS = {
181
+ win32: { name: 'Windows', folder: 'Windows', ext: '.exe', generator: null },
182
+ darwin: { name: 'macOS', folder: 'MacOS', ext: '', generator: 'Xcode' },
183
+ linux: { name: 'Linux', folder: 'Linux', ext: '', generator: 'Ninja' },
184
+ android: { name: 'Android', folder: 'Android', ext: '.apk', generator: 'Ninja' },
185
+ ios: { name: 'iOS', folder: 'iOS', ext: '.app', generator: 'Xcode' },
186
+ };
187
+
188
+ function getProjectName() {
189
+ try {
190
+ const pkg = JSON.parse(execSync('npm pkg get name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }));
191
+ return typeof pkg === 'string' ? pkg.replace(/"/g, '') : basename(process.cwd());
192
+ } catch {
193
+ return basename(process.cwd());
194
+ }
195
+ }
196
+
197
+ function getCMakePath() {
198
+ const paths = [
199
+ 'cmake',
200
+ 'C:\\Program Files\\CMake\\bin\\cmake.exe',
201
+ 'C:\\Program Files (x86)\\CMake\\bin\\cmake.exe',
202
+ '/usr/local/bin/cmake',
203
+ '/usr/bin/cmake'
204
+ ];
205
+ for (const p of paths) {
206
+ try {
207
+ execSync(`"${p}" --version`, { stdio: 'ignore' });
208
+ return p;
209
+ } catch { }
210
+ }
211
+ return 'cmake';
212
+ }
213
+
214
+ // Find vcvarsall.bat for Windows builds (needed for Ninja generator)
215
+ let _vcvarsallCache = undefined;
216
+ function findVcvarsall() {
217
+ if (_vcvarsallCache !== undefined) return _vcvarsallCache;
218
+
219
+ const vswherePath = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe';
220
+ if (existsSync(vswherePath)) {
221
+ try {
222
+ const installPath = execSync(
223
+ `"${vswherePath}" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`,
224
+ { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8', timeout: 10000 }
225
+ ).trim();
226
+ if (installPath) {
227
+ const vcvars = join(installPath, 'VC', 'Auxiliary', 'Build', 'vcvarsall.bat');
228
+ if (existsSync(vcvars)) {
229
+ _vcvarsallCache = vcvars;
230
+ return vcvars;
231
+ }
232
+ }
233
+ } catch { }
234
+ }
235
+
236
+ // Fallback: scan known paths
237
+ const vsYears = ['2026', '2025', '2024', '2023', '2022', '2019'];
238
+ const vsEditions = ['Community', 'Professional', 'Enterprise', 'BuildTools'];
239
+ for (const year of vsYears) {
240
+ for (const edition of vsEditions) {
241
+ const vcvars = `C:\\Program Files\\Microsoft Visual Studio\\${year}\\${edition}\\VC\\Auxiliary\\Build\\vcvarsall.bat`;
242
+ if (existsSync(vcvars)) {
243
+ _vcvarsallCache = vcvars;
244
+ return vcvars;
245
+ }
246
+ }
247
+ }
248
+
249
+ _vcvarsallCache = null;
250
+ return null;
251
+ }
252
+
253
+ // Capture the full environment from vcvarsall.bat (cached per session)
254
+ let _vcEnvCache = undefined;
255
+ function getVcEnvironment() {
256
+ if (_vcEnvCache !== undefined) return _vcEnvCache;
257
+
258
+ if (process.platform !== 'win32') {
259
+ _vcEnvCache = null;
260
+ return null;
261
+ }
262
+
263
+ const vcvarsall = findVcvarsall();
264
+ if (!vcvarsall) {
265
+ _vcEnvCache = null;
266
+ return null;
267
+ }
268
+
269
+ try {
270
+ // Run vcvarsall and dump all environment variables
271
+ const output = execSync(
272
+ `cmd /c ""${vcvarsall}" x64 >nul 2>&1 && set"`,
273
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30000, shell: true }
274
+ );
275
+
276
+ // Parse "KEY=VALUE" lines into an env object
277
+ const env = {};
278
+ for (const line of output.split(/\r?\n/)) {
279
+ const idx = line.indexOf('=');
280
+ if (idx > 0) {
281
+ env[line.substring(0, idx)] = line.substring(idx + 1);
282
+ }
283
+ }
284
+
285
+ // Sanity check: we should have PATH and INCLUDE set
286
+ if (env.PATH && env.INCLUDE) {
287
+ _vcEnvCache = env;
288
+ return env;
289
+ }
290
+ } catch { }
291
+
292
+ _vcEnvCache = null;
293
+ return null;
294
+ }
295
+
296
+ function runCMake(args, options = {}) {
297
+ const cmake = getCMakePath();
298
+
299
+ // On Windows, use the captured vcvarsall environment so cl.exe + ninja are in PATH
300
+ if (process.platform === 'win32') {
301
+ const vcEnv = getVcEnvironment();
302
+ if (vcEnv) {
303
+ return execSync(`"${cmake}" ${args}`, { stdio: 'inherit', env: vcEnv, ...options });
304
+ }
305
+ }
306
+
307
+ return execSync(`"${cmake}" ${args}`, { stdio: 'inherit', ...options });
308
+ }
309
+
310
+ function getInstalledPackageVersion(packageName) {
311
+ try {
312
+ const result = execSync(`npm list ${packageName} --depth=0 --json`, {
313
+ encoding: 'utf8',
314
+ stdio: ['pipe', 'pipe', 'ignore']
315
+ });
316
+ const json = JSON.parse(result);
317
+ if (json.dependencies && json.dependencies[packageName]) {
318
+ return json.dependencies[packageName].version;
319
+ }
320
+ } catch {
321
+ // Package not installed locally
322
+ }
323
+
324
+ // Try global installation
325
+ try {
326
+ const result = execSync(`npm list -g ${packageName} --depth=0 --json`, {
327
+ encoding: 'utf8',
328
+ stdio: ['pipe', 'pipe', 'ignore']
329
+ });
330
+ const json = JSON.parse(result);
331
+ if (json.dependencies && json.dependencies[packageName]) {
332
+ return json.dependencies[packageName].version;
333
+ }
334
+ } catch {
335
+ return null;
336
+ }
337
+
338
+ return null;
339
+ }
340
+
341
+ function getLatestPackageVersion(packageName) {
342
+ try {
343
+ const result = execSync(`npm view ${packageName} version`, {
344
+ encoding: 'utf8',
345
+ stdio: ['pipe', 'pipe', 'ignore']
346
+ });
347
+ return result.trim();
348
+ } catch {
349
+ return null;
350
+ }
351
+ }
352
+
353
+ function compareVersions(v1, v2) {
354
+ const parts1 = v1.split('.').map(Number);
355
+ const parts2 = v2.split('.').map(Number);
356
+
357
+ for (let i = 0; i < 3; i++) {
358
+ if (parts1[i] > parts2[i]) return 1;
359
+ if (parts1[i] < parts2[i]) return -1;
360
+ }
361
+ return 0;
362
+ }
363
+
364
+ function showVersionInfo() {
365
+ const packages = [
366
+ cliPackageJson.name,
367
+ 'plusui-native-core',
368
+ 'plusui-native-builder',
369
+ 'plusui-native-connect'
370
+ ];
371
+
372
+ logSection('PlusUI Package Versions');
373
+
374
+ packages.forEach(pkg => {
375
+ let version;
376
+ if (pkg === cliPackageJson.name) {
377
+ version = cliPackageJson.version;
378
+ log(`${pkg}: ${COLORS.green}v${version}${COLORS.reset} ${COLORS.dim}(current)${COLORS.reset}`, 'reset');
379
+ } else {
380
+ version = getInstalledPackageVersion(pkg);
381
+ if (version) {
382
+ log(`${pkg}: ${COLORS.green}v${version}${COLORS.reset}`, 'reset');
383
+ } else {
384
+ log(`${pkg}: ${COLORS.dim}not installed${COLORS.reset}`, 'reset');
385
+ }
386
+ }
387
+ });
388
+
389
+ console.log('');
390
+ }
391
+
392
+ async function updatePlusUIPackages() {
393
+ logSection('Updating PlusUI Packages');
394
+
395
+ const packages = [
396
+ cliPackageJson.name,
397
+ 'plusui-native-core',
398
+ 'plusui-native-builder',
399
+ 'plusui-native-connect'
400
+ ];
401
+
402
+ log('Checking for updates...\n', 'blue');
403
+
404
+ // Check if packages are installed locally or globally
405
+ const isInProject = existsSync(join(process.cwd(), 'package.json'));
406
+
407
+ if (isInProject) {
408
+ let updatedCount = 0;
409
+ let upToDateCount = 0;
410
+ let installedCount = 0;
411
+
412
+ for (const pkg of packages) {
413
+ const currentVersion = getInstalledPackageVersion(pkg);
414
+
415
+ if (!currentVersion) {
416
+ const latestVersion = getLatestPackageVersion(pkg);
417
+
418
+ if (!latestVersion) {
419
+ log(`${COLORS.yellow}${pkg}: not installed (couldn't resolve latest version)${COLORS.reset}`);
420
+ continue;
421
+ }
422
+
423
+ try {
424
+ log(`${COLORS.blue}${pkg}: not installed → ${latestVersion}${COLORS.reset}`);
425
+ execSync(`npm install ${pkg}@${latestVersion}`, {
426
+ stdio: ['ignore', 'ignore', 'pipe'],
427
+ encoding: 'utf8'
428
+ });
429
+ log(`${COLORS.green}✓ ${pkg} installed${COLORS.reset}`);
430
+ installedCount++;
431
+ } catch (e) {
432
+ log(`${COLORS.red}✗ ${pkg} install failed${COLORS.reset}`);
433
+ }
434
+ continue;
435
+ }
436
+
437
+ // Get latest version from npm
438
+ const latestVersion = getLatestPackageVersion(pkg);
439
+
440
+ if (!latestVersion) {
441
+ log(`${COLORS.yellow}${pkg}: couldn't check for updates${COLORS.reset}`);
442
+ continue;
443
+ }
444
+
445
+ const comparison = compareVersions(latestVersion, currentVersion);
446
+
447
+ if (comparison > 0) {
448
+ // Newer version available
449
+ try {
450
+ log(`${COLORS.blue}${pkg}: ${currentVersion} → ${latestVersion}${COLORS.reset}`);
451
+ execSync(`npm install ${pkg}@${latestVersion}`, {
452
+ stdio: ['ignore', 'ignore', 'pipe'],
453
+ encoding: 'utf8'
454
+ });
455
+ log(`${COLORS.green}✓ ${pkg} updated${COLORS.reset}`);
456
+ updatedCount++;
457
+ } catch (e) {
458
+ log(`${COLORS.red}✗ ${pkg} update failed${COLORS.reset}`);
459
+ }
460
+ } else {
461
+ // Already up to date
462
+ log(`${COLORS.green}✓ ${pkg} v${currentVersion} (up to date)${COLORS.reset}`);
463
+ upToDateCount++;
464
+ }
465
+ }
466
+
467
+ console.log('');
468
+ if (updatedCount > 0) {
469
+ log(`Updated ${updatedCount} package${updatedCount !== 1 ? 's' : ''}`, 'green');
470
+ }
471
+ if (installedCount > 0) {
472
+ log(`Installed ${installedCount} missing package${installedCount !== 1 ? 's' : ''}`, 'green');
473
+ }
474
+ if (upToDateCount > 0) {
475
+ log(`${upToDateCount} package${upToDateCount !== 1 ? 's' : ''} already up to date`, 'dim');
476
+ }
477
+ } else {
478
+ log('Updating global CLI package...', 'cyan');
479
+
480
+ const currentVersion = cliPackageJson.version;
481
+ const latestVersion = getLatestPackageVersion(cliPackageJson.name);
482
+
483
+ if (!latestVersion) {
484
+ log('Couldn\'t check for updates', 'yellow');
485
+ return;
486
+ }
487
+
488
+ const comparison = compareVersions(latestVersion, currentVersion);
489
+
490
+ if (comparison > 0) {
491
+ try {
492
+ log(`${COLORS.blue}${cliPackageJson.name}: ${currentVersion} → ${latestVersion}${COLORS.reset}`);
493
+ execSync(`npm install -g ${cliPackageJson.name}@${latestVersion}`, { stdio: 'inherit' });
494
+ log(`✓ ${cliPackageJson.name} updated successfully`, 'green');
495
+ } catch (e) {
496
+ log(`Failed to update ${cliPackageJson.name}`, 'red');
497
+ }
498
+ } else {
499
+ log(`✓ ${cliPackageJson.name} v${currentVersion} (already up to date)`, 'green');
500
+ }
501
+ }
502
+
503
+ console.log('');
504
+ }
505
+
506
+ function getAppBindgenPaths() {
507
+ return {
508
+ featuresDir: join(process.cwd(), 'src', 'features'),
509
+ // Connections/ is now at the project root — shared by C++ and TS
510
+ outputDir: join(process.cwd(), 'Connections'),
511
+ };
512
+ }
513
+
514
+ function findLikelyProjectDirs(baseDir) {
515
+ try {
516
+ const entries = execSync('npm pkg get name', { cwd: baseDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
517
+ if (entries) {
518
+ // noop; just to ensure cwd is a Node project when possible
519
+ }
520
+ } catch { }
521
+
522
+ const candidates = [];
523
+ try {
524
+ const dirs = execSync(process.platform === 'win32' ? 'dir /b /ad' : 'ls -1 -d */', {
525
+ cwd: baseDir,
526
+ encoding: 'utf8',
527
+ stdio: ['ignore', 'pipe', 'ignore'],
528
+ shell: true,
529
+ })
530
+ .split(/\r?\n/)
531
+ .map(s => s.trim().replace(/[\\/]$/, ''))
532
+ .filter(Boolean);
533
+
534
+ for (const dirName of dirs) {
535
+ const fullDir = join(baseDir, dirName);
536
+ if (existsSync(join(fullDir, 'CMakeLists.txt')) && existsSync(join(fullDir, 'package.json'))) {
537
+ candidates.push(dirName);
538
+ }
539
+ }
540
+ } catch { }
541
+
542
+ return candidates;
543
+ }
544
+
545
+ function ensureProjectRoot(commandName) {
546
+ const hasCMake = existsSync(join(process.cwd(), 'CMakeLists.txt'));
547
+ const hasPackage = existsSync(join(process.cwd(), 'package.json'));
548
+
549
+ if (hasCMake && hasPackage) {
550
+ return;
551
+ }
552
+
553
+ const likelyDirs = findLikelyProjectDirs(process.cwd());
554
+ let hint = '';
555
+
556
+ if (likelyDirs.length === 1) {
557
+ hint = `\n\nHint: you may be in a parent folder. Try:\n cd ${likelyDirs[0]}\n plusui ${commandName}`;
558
+ } else if (likelyDirs.length > 1) {
559
+ hint = `\n\nHint: run this command from your app folder (one containing CMakeLists.txt and package.json).`;
560
+ }
561
+
562
+ error(`This command must be run from a PlusUI project root (missing CMakeLists.txt and/or package.json in ${process.cwd()}).${hint}`);
563
+ }
564
+
565
+ function ensureBuildLayout() {
566
+ const buildRoot = join(process.cwd(), 'Build');
567
+ for (const platform of Object.values(PLATFORMS)) {
568
+ mkdirSync(join(buildRoot, platform.folder), { recursive: true });
569
+ }
570
+ }
571
+
572
+ function getDevBuildDir() {
573
+ const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
574
+ return join('Build', platformFolder);
575
+ }
576
+
577
+ function resolveBindgenScriptPath() {
578
+ const candidates = [
579
+ resolve(__dirname, '../../plusui-connect/src/connect.js'),
580
+ resolve(__dirname, '../../plusui-connect/src/index.js'),
581
+ resolve(__dirname, '../../plusui-native-connect/src/connect.js'),
582
+ resolve(__dirname, '../../plusui-native-connect/src/index.js'),
583
+ resolve(__dirname, '../../plusui-native-bindgen/src/index.js'),
584
+ resolve(__dirname, '../../../plusui-native-connect/src/connect.js'),
585
+ resolve(__dirname, '../../../plusui-native-connect/src/index.js'),
586
+ resolve(__dirname, '../../../plusui-native-bindgen/src/index.js'),
587
+ resolve(process.cwd(), 'node_modules', 'plusui-native-connect', 'src', 'connect.js'),
588
+ resolve(process.cwd(), 'node_modules', 'plusui-native-connect', 'src', 'index.js'),
589
+ resolve(process.cwd(), 'node_modules', 'plusui-native-bindgen', 'src', 'index.js'),
590
+ ];
591
+
592
+ for (const candidate of candidates) {
593
+ if (existsSync(candidate)) {
594
+ return candidate;
595
+ }
596
+ }
597
+
598
+ return null;
599
+ }
600
+
601
+ async function syncGeneratedTsBindings(backendOutputDir, frontendOutputDir) {
602
+ const generatedTsPath = join(backendOutputDir, 'bindings.gen.ts');
603
+ if (!existsSync(generatedTsPath)) {
604
+ return;
605
+ }
606
+
607
+ await mkdir(frontendOutputDir, { recursive: true });
608
+ const frontendTsPath = join(frontendOutputDir, 'bindings.gen.ts');
609
+ await copyFile(generatedTsPath, frontendTsPath);
610
+ log(`Synced TS bindings: ${frontendTsPath}`, 'dim');
611
+ }
612
+
613
+ function promptTemplateSelection() {
614
+ return new Promise((resolve) => {
615
+ const rl = createInterface({
616
+ input: process.stdin,
617
+ output: process.stdout
618
+ });
619
+
620
+ console.log(`\n${COLORS.bright}Select a template:${COLORS.reset}`);
621
+ console.log(` ${COLORS.cyan}1)${COLORS.reset} solid - SolidJS + TypeScript (lightweight, reactive)`);
622
+ console.log(` ${COLORS.cyan}2)${COLORS.reset} react - React + TypeScript (popular, familiar)`);
623
+
624
+ rl.question(`\n${COLORS.yellow}Enter choice [1-2] (default: solid):${COLORS.reset} `, (answer) => {
625
+ rl.close();
626
+ const choice = answer.trim() || '1';
627
+ const templates = { '1': 'solid', '2': 'react' };
628
+ const template = templates[choice] || 'solid';
629
+ console.log(`${COLORS.green}✓${COLORS.reset} Selected: ${COLORS.cyan}${template}${COLORS.reset}\n`);
630
+ resolve(template);
631
+ });
632
+ });
633
+ }
634
+
635
+ async function createProject(name, options = {}) {
636
+ try {
637
+ const templateManager = new TemplateManager();
638
+ await templateManager.create(name, options);
639
+ } catch (e) {
640
+ error(e.message);
641
+ }
642
+ }
643
+
644
+ // ============================================================
645
+ // BUILD FUNCTIONS
646
+ // ============================================================
647
+
648
+ function buildFrontend() {
649
+ ensureProjectRoot('build:frontend');
650
+ logSection('Building Frontend');
651
+
652
+ if (existsSync('frontend')) {
653
+ log('Running Vite build...', 'blue');
654
+ execSync('cd frontend && npm run build', { stdio: 'inherit', shell: true });
655
+ log('Frontend built successfully!', 'green');
656
+ } else {
657
+ log('No frontend directory found, skipping...', 'yellow');
658
+ }
659
+ }
660
+
661
+ function buildBackend(platform = null, devMode = false) {
662
+ ensureProjectRoot(devMode ? 'dev:backend' : 'build:backend');
663
+ const targetPlatform = platform || process.platform;
664
+ const platformConfig = PLATFORMS[targetPlatform] || PLATFORMS[Object.keys(PLATFORMS).find(k => PLATFORMS[k].folder.toLowerCase() === targetPlatform?.toLowerCase())];
665
+
666
+ if (!platformConfig) {
667
+ error(`Unsupported platform: ${targetPlatform}`);
668
+ }
669
+
670
+ logSection(`Building Backend (${platformConfig.name})`);
671
+
672
+ const buildDir = `Build/${platformConfig.folder}`;
673
+
674
+ ensureBuildLayout();
675
+
676
+ // Create build directory
677
+ if (!existsSync(buildDir)) {
678
+ execSync(`mkdir -p "${buildDir}"`, { stdio: 'ignore', shell: true });
679
+ }
680
+
681
+ // CMake configure
682
+ let cmakeArgs = `-S . -B "${buildDir}" -DCMAKE_BUILD_TYPE=Release`;
683
+
684
+ if (devMode) {
685
+ cmakeArgs += ' -DPLUSUI_DEV_MODE=ON';
686
+ } else {
687
+ cmakeArgs += ' -DPLUSUI_DEV_MODE=OFF';
688
+ }
689
+
690
+ if (platformConfig.generator) {
691
+ cmakeArgs += ` -G "${platformConfig.generator}"`;
692
+ } else if (process.platform === 'win32') {
693
+ // Use embedded debug info (/Z7) to avoid VS 2026 PDB creation bug (C1041)
694
+ cmakeArgs += ' -DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=Embedded';
695
+ }
696
+
697
+ log(`Configuring CMake...`, 'blue');
698
+ runCMake(cmakeArgs);
699
+
700
+ // CMake build
701
+ log(`Building...`, 'blue');
702
+ runCMake(`--build "${buildDir}" --config Release`);
703
+
704
+ log(`Backend built: ${buildDir}`, 'green');
705
+
706
+ return buildDir;
707
+ }
708
+
709
+ async function generateIcons(inputPath = null) {
710
+ ensureProjectRoot('icons');
711
+ logSection('Generating Platform Icons');
712
+
713
+ const { IconGenerator } = await import('./assets/icon-generator.js');
714
+ const generator = new IconGenerator();
715
+
716
+ const srcIcon = inputPath || join(process.cwd(), 'assets', 'icon.png');
717
+ const outputBase = join(process.cwd(), 'assets', 'icons');
718
+
719
+ if (!existsSync(srcIcon)) {
720
+ log(`Icon not found: ${srcIcon}`, 'yellow');
721
+ log('Place a 512x512+ PNG icon at assets/icon.png', 'dim');
722
+ return;
723
+ }
724
+
725
+ await generator.generate(srcIcon, outputBase);
726
+ }
727
+
728
+ async function embedResources(platform = null) {
729
+ ensureProjectRoot('embed');
730
+ const targetPlatform = platform || process.platform;
731
+ logSection(`Embedding Resources (${targetPlatform})`);
732
+
733
+ const { ResourceEmbedder } = await import('./assets/resource-embedder.js');
734
+ const embedder = new ResourceEmbedder({ verbose: true });
735
+
736
+ const frontendDist = join(process.cwd(), 'frontend', 'dist');
737
+ const outputDir = join(process.cwd(), 'generated', 'resources');
738
+
739
+ if (!existsSync(frontendDist)) {
740
+ log('Frontend not built yet. Run: plusui build:frontend', 'yellow');
741
+ return;
742
+ }
743
+
744
+ if (platform === 'all') {
745
+ await embedder.embedAll(frontendDist, outputDir);
746
+ } else {
747
+ await embedder.embed(frontendDist, outputDir, targetPlatform);
748
+ }
749
+ }
750
+
751
+ async function embedAssets() {
752
+ ensureProjectRoot('build');
753
+ const assetsDir = join(process.cwd(), 'assets');
754
+ if (!existsSync(assetsDir)) {
755
+ try {
756
+ await mkdir(assetsDir, { recursive: true });
757
+ } catch (e) { }
758
+ }
759
+
760
+ logSection('Embedding Assets');
761
+
762
+ // Always generate the header file, even if empty
763
+ let headerContent = '#pragma once\n\n';
764
+ headerContent += '// THIS FILE IS AUTO-GENERATED BY PLUSUI CLI\n';
765
+ headerContent += '// DO NOT MODIFY MANUALLY\n\n';
766
+
767
+ const files = existsSync(assetsDir)
768
+ ? (await readdir(assetsDir)).filter(f => !statSync(join(assetsDir, f)).isDirectory())
769
+ : [];
770
+
771
+ if (files.length === 0) {
772
+ log('No assets found in assets/ folder', 'dim');
773
+ } else {
774
+ for (const file of files) {
775
+ const filePath = join(assetsDir, file);
776
+ log(`Processing ${file}...`, 'dim');
777
+
778
+ const varName = file.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
779
+ const data = await readFile(filePath);
780
+
781
+ headerContent += `static const unsigned char ASSET_${varName}[] = {`;
782
+ for (let i = 0; i < data.length; i++) {
783
+ if (i % 16 === 0) headerContent += '\n ';
784
+ headerContent += `0x${data[i].toString(16).padStart(2, '0')}, `;
785
+ }
786
+ headerContent += '\n};\n';
787
+ headerContent += `static const unsigned int ASSET_${varName}_LEN = ${data.length};\n\n`;
788
+ }
789
+ }
790
+
791
+ const genDir = join(process.cwd(), 'generated');
792
+ if (!existsSync(genDir)) await mkdir(genDir, { recursive: true });
793
+
794
+ await writeFile(join(genDir, 'assets.h'), headerContent);
795
+ log(`✓ Assets header generated: generated/assets.h`, 'green');
796
+ }
797
+
798
+
799
+
800
+ async function build(production = true) {
801
+ ensureProjectRoot('build');
802
+ logSection('Building PlusUI Application');
803
+
804
+ // Embed assets
805
+ await embedAssets();
806
+
807
+ // Build frontend first
808
+ if (production) {
809
+ buildFrontend();
810
+ }
811
+
812
+ // Build backend
813
+ const buildDir = buildBackend(null, !production);
814
+
815
+ log('\nBuild complete!', 'green');
816
+ return buildDir;
817
+ }
818
+
819
+ function buildAll() {
820
+ ensureProjectRoot('build:all');
821
+ logSection('Building for All Platforms');
822
+
823
+ ensureBuildLayout();
824
+
825
+ buildFrontend();
826
+
827
+ const supportedPlatforms = ['win32', 'darwin', 'linux'];
828
+
829
+ for (const platform of supportedPlatforms) {
830
+ try {
831
+ buildBackend(platform, false);
832
+ } catch (e) {
833
+ log(`Failed to build for ${PLATFORMS[platform].name}: ${e.message}`, 'yellow');
834
+ }
835
+ }
836
+
837
+ log('\nAll platform builds complete!', 'green');
838
+ log('\nOutput directories:', 'bright');
839
+ log(' Build/Windows/', 'cyan');
840
+ log(' Build/MacOS/', 'cyan');
841
+ log(' Build/Linux/', 'cyan');
842
+ log(' Build/Android/', 'cyan');
843
+ log(' Build/iOS/', 'cyan');
844
+ }
845
+
846
+ function buildPlatform(platform) {
847
+ logSection(`Building for ${PLATFORMS[platform]?.name || platform}`);
848
+ buildFrontend();
849
+ buildBackend(platform, false);
850
+ }
851
+
852
+ // ============================================================
853
+ // DEVELOPMENT FUNCTIONS
854
+ // ============================================================
855
+
856
+ let viteServer = null;
857
+ let cppProcess = null;
858
+
859
+ async function startViteServer() {
860
+ ensureProjectRoot('dev:frontend');
861
+ log('Starting Vite dev server...', 'blue');
862
+
863
+ viteServer = await createViteServer({
864
+ root: 'frontend',
865
+ server: {
866
+ port: 5173,
867
+ strictPort: false,
868
+ },
869
+ });
870
+
871
+ await viteServer.listen();
872
+
873
+ const actualPort = viteServer.httpServer?.address()?.port || 5173;
874
+ log(`Vite server: http://localhost:${actualPort}`, 'green');
875
+ return viteServer;
876
+ }
877
+
878
+ async function startBackend() {
879
+ ensureProjectRoot('dev');
880
+ logSection('Building C++ Backend (Dev Mode)');
881
+
882
+ const projectName = getProjectName();
883
+ killProcessByName(projectName);
884
+
885
+ const buildDir = getDevBuildDir();
886
+
887
+ // On Windows, use embedded debug info (/Z7) to avoid VS 2026 PDB creation bug (C1041)
888
+ let generatorArgs = '';
889
+ if (process.platform === 'win32') {
890
+ generatorArgs = ' -DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=Embedded';
891
+ }
892
+
893
+ // Auto-clean build dir if generator changed (e.g. VS → Ninja)
894
+ const cacheFile = join(buildDir, 'CMakeCache.txt');
895
+ if (existsSync(cacheFile)) {
896
+ try {
897
+ const cacheContent = execSync(`type "${cacheFile}"`, { encoding: 'utf8', shell: true, stdio: ['pipe', 'pipe', 'ignore'] });
898
+ const generatorMatch = cacheContent.match(/CMAKE_GENERATOR:INTERNAL=(.*)/);
899
+ const cachedGenerator = generatorMatch ? generatorMatch[1].trim() : '';
900
+ const wantNinja = generatorArgs.includes('Ninja');
901
+ if ((wantNinja && cachedGenerator !== 'Ninja') || (!wantNinja && cachedGenerator === 'Ninja')) {
902
+ log(`Generator changed (${cachedGenerator} → ${wantNinja ? 'Ninja' : 'default'}), cleaning build dir...`, 'yellow');
903
+ execSync(process.platform === 'win32' ? `rmdir /s /q "${buildDir}"` : `rm -rf "${buildDir}"`, { stdio: 'ignore', shell: true });
904
+ }
905
+ } catch { }
906
+ }
907
+
908
+ // Always configure with dev mode to ensure PLUSUI_DEV_MODE is set correctly
909
+ log('Configuring CMake...', 'blue');
910
+ runCMake(`-S . -B "${buildDir}" -DPLUSUI_DEV_MODE=ON${generatorArgs}`);
911
+
912
+ log('Compiling...', 'blue');
913
+ runCMake(`--build "${buildDir}"`);
914
+
915
+ // Find executable
916
+ let exePath;
917
+ if (process.platform === 'win32') {
918
+ // Ninja puts exe directly in build dir
919
+ exePath = join(buildDir, `${projectName}.exe`);
920
+ if (!existsSync(exePath)) {
921
+ exePath = join(buildDir, 'bin', `${projectName}.exe`);
922
+ }
923
+ // Fallback for VS generator (Debug subfolder)
924
+ if (!existsSync(exePath)) {
925
+ exePath = join(buildDir, projectName, 'Debug', `${projectName}.exe`);
926
+ }
927
+ if (!existsSync(exePath)) {
928
+ exePath = join(buildDir, 'Debug', `${projectName}.exe`);
929
+ }
930
+ } else {
931
+ exePath = join(buildDir, projectName);
932
+ if (!existsSync(exePath)) {
933
+ exePath = join(buildDir, 'bin', projectName);
934
+ }
935
+ }
936
+
937
+ if (!existsSync(exePath)) {
938
+ error(`Executable not found. Expected: ${exePath}`);
939
+ }
940
+
941
+ log('Starting C++ app...', 'blue');
942
+
943
+ cppProcess = spawn(exePath, [], {
944
+ shell: true,
945
+ stdio: 'inherit',
946
+ env: { ...process.env, PLUSUI_DEV: '1' }
947
+ });
948
+
949
+ cppProcess.on('error', (err) => {
950
+ log(`Failed to start app: ${err.message}`, 'red');
951
+ });
952
+
953
+ return cppProcess;
954
+ }
955
+
956
+ async function killPort(port) {
957
+ log(`Checking port ${port}...`, 'dim');
958
+ try {
959
+ if (process.platform === 'win32') {
960
+ try {
961
+ const output = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
962
+ const lines = output.split('\n').filter(line => line.includes(`:${port}`) && line.includes('LISTENING'));
963
+
964
+ for (const line of lines) {
965
+ const parts = line.trim().split(/\s+/);
966
+ const pid = parts[parts.length - 1];
967
+ if (pid && /^\d+$/.test(pid)) {
968
+ log(`Killing process ${pid} on port ${port}...`, 'yellow');
969
+ try {
970
+ execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
971
+ } catch (e) {
972
+ // Ignore if already dead
973
+ }
974
+ }
975
+ }
976
+ } catch (e) {
977
+ // findstr returns exit code 1 if no match found
978
+ }
979
+ } else {
980
+ try {
981
+ execSync(`lsof -i :${port} -t | xargs kill -9`, { stdio: 'ignore' });
982
+ } catch {
983
+ // Ignore if no process found
984
+ }
985
+ }
986
+ } catch (e) {
987
+ // Ignore general errors
988
+ }
989
+ }
990
+
991
+ function killProcessByName(name) {
992
+ const processName = process.platform === 'win32' ? `${name}.exe` : name;
993
+ log(`Cleaning up previous instance (${processName})...`, 'dim');
994
+ try {
995
+ if (process.platform === 'win32') {
996
+ execSync(`taskkill /IM "${processName}" /F`, { stdio: 'ignore' });
997
+ } else {
998
+ execSync(`pkill -f "${processName}"`, { stdio: 'ignore' });
999
+ }
1000
+ } catch (e) {
1001
+ // Ignore if process not found
1002
+ }
1003
+ }
1004
+
1005
+ async function dev() {
1006
+ ensureProjectRoot('dev');
1007
+ logSection('PlusUI Development Mode');
1008
+
1009
+ const toolCheck = checkTools();
1010
+ if (toolCheck && toolCheck.missing) {
1011
+ error('Missing required build tools. Run: plusui doctor --fix');
1012
+ }
1013
+
1014
+ // Embed assets
1015
+ await embedAssets();
1016
+
1017
+ await runBindgen([], { skipIfNoInput: true, source: 'dev' });
1018
+
1019
+ // specific port cleaning
1020
+ await killPort(5173);
1021
+
1022
+ // Start Vite first
1023
+ try {
1024
+ viteServer = await startViteServer();
1025
+ } catch (e) {
1026
+ log(`Vite failed to start: ${e.message}`, 'red');
1027
+ error('Could not start development server');
1028
+ }
1029
+
1030
+ const actualPort = viteServer.httpServer?.address()?.port || 5173;
1031
+
1032
+ // Build and start C++ backend
1033
+ await startBackend();
1034
+
1035
+ log('\n' + '='.repeat(50), 'dim');
1036
+ log('Development mode active', 'green');
1037
+ log('='.repeat(50), 'dim');
1038
+ log(`\nFrontend: http://localhost:${actualPort} (HMR enabled)`, 'cyan');
1039
+ log('Backend: C++ app running with webview', 'cyan');
1040
+ log('\nEdit frontend/src/* for live reload', 'dim');
1041
+ log('Edit main.cpp and restart for C++ changes', 'dim');
1042
+ log('\nPress Ctrl+C to stop\n', 'yellow');
1043
+
1044
+ // Handle shutdown
1045
+ process.on('SIGINT', async () => {
1046
+ log('\nShutting down...', 'yellow');
1047
+ if (viteServer) await viteServer.close();
1048
+ if (cppProcess) cppProcess.kill();
1049
+ process.exit(0);
1050
+ });
1051
+ }
1052
+
1053
+ async function devFrontend() {
1054
+ ensureProjectRoot('dev:frontend');
1055
+ logSection('Frontend Development Mode');
1056
+
1057
+ // specific port cleaning
1058
+ await killPort(5173);
1059
+
1060
+ viteServer = await createViteServer({
1061
+ root: 'frontend',
1062
+ server: { port: 5173, strictPort: false },
1063
+ });
1064
+
1065
+ await viteServer.listen();
1066
+
1067
+ const actualPort = viteServer.httpServer?.address()?.port || 5173;
1068
+ log(`Frontend: http://localhost:${actualPort}`, 'green');
1069
+ log('HMR enabled - changes will reflect instantly!\n', 'green');
1070
+
1071
+ process.on('SIGINT', async () => {
1072
+ if (viteServer) await viteServer.close();
1073
+ process.exit(0);
1074
+ });
1075
+ }
1076
+
1077
+ function devBackend() {
1078
+ ensureProjectRoot('dev:backend');
1079
+ logSection('Backend Development Mode');
1080
+
1081
+ const projectName = getProjectName();
1082
+ killProcessByName(projectName);
1083
+
1084
+ const buildDir = getDevBuildDir();
1085
+
1086
+ // Always configure with dev mode to ensure PLUSUI_DEV_MODE is set correctly
1087
+ log('Configuring CMake...', 'blue');
1088
+ runCMake(`-S . -B "${buildDir}" -DPLUSUI_DEV_MODE=ON`);
1089
+
1090
+ runCMake(`--build "${buildDir}"`);
1091
+
1092
+ let exePath;
1093
+ if (process.platform === 'win32') {
1094
+ exePath = join(buildDir, projectName, 'Debug', `${projectName}.exe`);
1095
+ if (!existsSync(exePath)) exePath = join(buildDir, 'Debug', `${projectName}.exe`);
1096
+ if (!existsSync(exePath)) exePath = join(buildDir, `${projectName}.exe`);
1097
+ } else {
1098
+ exePath = join(buildDir, projectName);
1099
+ }
1100
+
1101
+ function runApp() {
1102
+ if (cppProcess) cppProcess.kill();
1103
+
1104
+ cppProcess = spawn(exePath, [], { shell: true, stdio: 'inherit' });
1105
+
1106
+ cppProcess.on('close', (code) => {
1107
+ if (code !== null && code !== 0) {
1108
+ log(`App exited with code ${code}`, 'yellow');
1109
+ }
1110
+ });
1111
+ }
1112
+
1113
+ runApp();
1114
+
1115
+ log('\nWatching C++ files for changes...', 'yellow');
1116
+ log('Press Ctrl+C to stop\n', 'dim');
1117
+
1118
+ // Watch main.cpp and features/ folder (if exists)
1119
+ const watchItems = ['main.cpp'];
1120
+ if (existsSync('features')) watchItems.push('features');
1121
+
1122
+ const watchers = watchItems.map(item => {
1123
+ const isDir = existsSync(item) && statSync(item).isDirectory();
1124
+ const w = isDir ? watch(item, { recursive: true }) : watch(item);
1125
+ w.on('change', (eventType, filename) => {
1126
+ const file = item === 'main.cpp' ? 'main.cpp' : filename;
1127
+ if (file && (file.endsWith('.cpp') || file.endsWith('.hpp'))) {
1128
+ log(`Changed: ${file}`, 'yellow');
1129
+ log('Rebuilding...', 'blue');
1130
+ try {
1131
+ killProcessByName(projectName);
1132
+ runCMake(`--build "${buildDir}"`);
1133
+ runApp();
1134
+ } catch (e) {
1135
+ log('Build failed, waiting for fixes...', 'red');
1136
+ }
1137
+ }
1138
+ });
1139
+ return w;
1140
+ });
1141
+
1142
+ process.on('SIGINT', () => {
1143
+ if (cppProcess) cppProcess.kill();
1144
+ watchers.forEach(w => w.close());
1145
+ process.exit(0);
1146
+ });
1147
+ }
1148
+
1149
+ // ============================================================
1150
+ // RUN FUNCTION
1151
+ // ============================================================
1152
+
1153
+ function run() {
1154
+ ensureProjectRoot('run');
1155
+ const projectName = getProjectName();
1156
+ const platform = PLATFORMS[process.platform];
1157
+ const buildDir = `Build/${platform?.folder || 'Windows'}`;
1158
+
1159
+ let exePath;
1160
+ if (process.platform === 'win32') {
1161
+ exePath = join(buildDir, 'Release', `${projectName}.exe`);
1162
+ if (!existsSync(exePath)) exePath = join(buildDir, 'bin', `${projectName}.exe`);
1163
+ if (!existsSync(exePath)) exePath = join(buildDir, `${projectName}.exe`);
1164
+ } else {
1165
+ exePath = join(buildDir, projectName);
1166
+ if (!existsSync(exePath)) exePath = join(buildDir, 'bin', projectName);
1167
+ }
1168
+
1169
+ if (!existsSync(exePath)) {
1170
+ error(`Build not found at ${exePath}. Run "plusui build" first.`);
1171
+ }
1172
+
1173
+ log(`Running: ${exePath}`, 'blue');
1174
+
1175
+ const proc = spawn(exePath, [], { shell: true, stdio: 'inherit' });
1176
+
1177
+ proc.on('close', (code) => {
1178
+ process.exit(code);
1179
+ });
1180
+ }
1181
+
1182
+ // ============================================================
1183
+ // CLEAN FUNCTION
1184
+ // ============================================================
1185
+
1186
+ async function clean() {
1187
+ ensureProjectRoot('clean');
1188
+ logSection('Cleaning Build Artifacts');
1189
+
1190
+ const dirs = ['Build', 'frontend/dist'];
1191
+
1192
+ for (const dir of dirs) {
1193
+ if (existsSync(dir)) {
1194
+ log(`Removing: ${dir}`, 'yellow');
1195
+ await rm(dir, { recursive: true, force: true });
1196
+ }
1197
+ }
1198
+
1199
+ log('\nClean complete!', 'green');
1200
+ }
1201
+
1202
+ // ============================================================
1203
+ // CONNECT GENERATOR FUNCTION
1204
+ // ============================================================
1205
+
1206
+ async function runBindgen(providedArgs = null, options = {}) {
1207
+ ensureProjectRoot('bindgen');
1208
+ logSection('Running Connection Generator');
1209
+
1210
+ const { skipIfNoInput = false, source = 'manual' } = options;
1211
+
1212
+ const scriptPath = resolveBindgenScriptPath();
1213
+
1214
+ if (!scriptPath) {
1215
+ error(`Connection generator script not found. Please ensure plusui-native-connect is installed.`);
1216
+ }
1217
+
1218
+ log(`Using connect generator: ${scriptPath}`, 'dim');
1219
+
1220
+ // plusui connect [projectRoot] [outputDir]
1221
+ // Defaults to app-local paths when available.
1222
+ const args = providedArgs ?? process.argv.slice(3);
1223
+ let bindgenArgs = [...args];
1224
+
1225
+ let usedDefaultAppMode = false;
1226
+ let defaultOutputDir = null;
1227
+
1228
+ if (bindgenArgs.length === 0) {
1229
+ const { outputDir: appOutputDir } = getAppBindgenPaths();
1230
+ bindgenArgs = [process.cwd(), appOutputDir];
1231
+ usedDefaultAppMode = true;
1232
+ defaultOutputDir = appOutputDir;
1233
+ log(`Project mode: ${process.cwd()} -> ${appOutputDir}`, 'dim');
1234
+ }
1235
+
1236
+ // Spawn node process
1237
+ const proc = spawn(process.execPath, [scriptPath, ...bindgenArgs], {
1238
+ stdio: 'inherit',
1239
+ env: process.env
1240
+ });
1241
+
1242
+ return new Promise((resolve, reject) => {
1243
+ proc.on('close', async (code) => {
1244
+ if (code === 0) {
1245
+ try {
1246
+ log('\nBindgen complete!', 'green');
1247
+ resolve();
1248
+ } catch (syncErr) {
1249
+ reject(syncErr);
1250
+ }
1251
+ } else {
1252
+ reject(new Error(`Connection generator failed with code ${code}`));
1253
+ }
1254
+ });
1255
+ });
1256
+ }
1257
+
1258
+ // ============================================================
1259
+ // MAIN
1260
+ // ============================================================
1261
+
1262
+ async function main() {
1263
+ const args = process.argv.slice(2);
1264
+ const cmd = args[0];
1265
+
1266
+ switch (cmd) {
1267
+ case 'doctor':
1268
+ await runDoctor({
1269
+ fix: args.includes('--fix'),
1270
+ json: args.includes('--json'),
1271
+ quick: args.includes('--quick')
1272
+ });
1273
+ break;
1274
+ case 'create':
1275
+ if (!args[1] || args[1].startsWith('-')) error('Project name required');
1276
+ const template = await promptTemplateSelection();
1277
+ await createProject(args[1], { template });
1278
+ break;
1279
+ case 'dev':
1280
+ await dev();
1281
+ break;
1282
+ case 'dev:frontend':
1283
+ await devFrontend();
1284
+ break;
1285
+ case 'dev:backend':
1286
+ await runBindgen([], { skipIfNoInput: true, source: 'dev:backend' });
1287
+ devBackend();
1288
+ break;
1289
+ case 'build':
1290
+ await runBindgen([], { skipIfNoInput: true, source: 'build' });
1291
+ await build(true);
1292
+ break;
1293
+ case 'build:frontend':
1294
+ buildFrontend();
1295
+ break;
1296
+ case 'build:backend':
1297
+ await runBindgen([], { skipIfNoInput: true, source: 'build:backend' });
1298
+ buildBackend(null, false);
1299
+ break;
1300
+ case 'build:all':
1301
+ await runBindgen([], { skipIfNoInput: true, source: 'build:all' });
1302
+ buildAll();
1303
+ break;
1304
+ case 'build:windows':
1305
+ await runBindgen([], { skipIfNoInput: true, source: 'build:windows' });
1306
+ buildPlatform('win32');
1307
+ break;
1308
+ case 'build:macos':
1309
+ await runBindgen([], { skipIfNoInput: true, source: 'build:macos' });
1310
+ buildPlatform('darwin');
1311
+ break;
1312
+ case 'build:linux':
1313
+ await runBindgen([], { skipIfNoInput: true, source: 'build:linux' });
1314
+ buildPlatform('linux');
1315
+ break;
1316
+ case 'build:android':
1317
+ await runBindgen([], { skipIfNoInput: true, source: 'build:android' });
1318
+ buildPlatform('android');
1319
+ break;
1320
+ case 'build:ios':
1321
+ await runBindgen([], { skipIfNoInput: true, source: 'build:ios' });
1322
+ buildPlatform('ios');
1323
+ break;
1324
+ case 'run':
1325
+ run();
1326
+ break;
1327
+ case 'clean':
1328
+ await clean();
1329
+ break;
1330
+ case 'connect':
1331
+ case 'bind':
1332
+ case 'bindgen':
1333
+ await runBindgen();
1334
+ break;
1335
+ case 'update':
1336
+ await updatePlusUIPackages();
1337
+ break;
1338
+ case 'icons':
1339
+ await generateIcons(args[1]);
1340
+ break;
1341
+ case 'embed':
1342
+ await embedResources(args[1]);
1343
+ break;
1344
+ case 'help':
1345
+ case '-h':
1346
+ case '--help':
1347
+ console.log(USAGE);
1348
+ break;
1349
+ case '-v':
1350
+ case '--version':
1351
+ showVersionInfo();
1352
+ break;
1353
+ default:
1354
+ console.log(USAGE);
1355
+ }
1356
+ }
1357
+
1358
+ main().catch(e => error(e.message));