plusui-native 0.2.4 → 0.2.7

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,904 +1,957 @@
1
- #!/usr/bin/env node
2
-
3
- import { mkdir, readFile, stat, rm, readdir, writeFile } from 'fs/promises';
4
- import { existsSync, watch, statSync } 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
- const COLORS = {
17
- reset: '\x1b[0m',
18
- bright: '\x1b[1m',
19
- dim: '\x1b[2m',
20
- green: '\x1b[32m',
21
- blue: '\x1b[34m',
22
- yellow: '\x1b[33m',
23
- red: '\x1b[31m',
24
- cyan: '\x1b[36m',
25
- magenta: '\x1b[35m',
26
- };
27
-
28
- function log(msg, color = 'reset') {
29
- console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
30
- }
31
-
32
- function logSection(title) {
33
- console.log(`\n${COLORS.bright}${COLORS.blue}=== ${title} ===${COLORS.reset}\n`);
34
- }
35
-
36
- function error(msg) {
37
- console.error(`${COLORS.red}Error: ${msg}${COLORS.reset}`);
38
- process.exit(1);
39
- }
40
-
41
- function checkTools() {
42
- const platform = process.platform;
43
- const required = [];
44
-
45
- const cmakePaths = [
46
- 'cmake',
47
- 'C:\\Program Files\\CMake\\bin\\cmake.exe',
48
- 'C:\\Program Files (x86)\\CMake\\bin\\cmake.exe',
49
- '/usr/local/bin/cmake',
50
- '/usr/bin/cmake'
51
- ];
52
-
53
- let cmakeFound = false;
54
- for (const p of cmakePaths) {
55
- try {
56
- execSync(`"${p}" --version`, { stdio: 'ignore' });
57
- cmakeFound = true;
58
- break;
59
- } catch { }
60
- }
61
-
62
- if (!cmakeFound) {
63
- if (platform === 'win32') {
64
- required.push({ name: 'CMake', install: 'winget install Kitware.CMake', auto: 'winget install -e --id Kitware.CMake' });
65
- } else if (platform === 'darwin') {
66
- required.push({ name: 'CMake', install: 'brew install cmake', auto: 'brew install cmake' });
67
- } else {
68
- required.push({ name: 'CMake', install: 'sudo apt install cmake', auto: 'sudo apt install cmake' });
69
- }
70
- }
71
-
72
- if (platform === 'win32') {
73
- const vsPaths = [
74
- 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC',
75
- 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional\\VC\\Tools\\MSVC',
76
- 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Tools\\MSVC'
77
- ];
78
-
79
- let vsFound = false;
80
- for (const p of vsPaths) {
81
- if (existsSync(p)) {
82
- vsFound = true;
83
- break;
84
- }
85
- }
86
-
87
- if (!vsFound) {
88
- required.push({ name: 'Visual Studio 2022', install: 'Download from visualstudio.microsoft.com with C++ workload', auto: null });
89
- }
90
- } else if (platform === 'darwin') {
91
- try {
92
- execSync('clang++ --version', { stdio: 'ignore' });
93
- } catch {
94
- required.push({ name: 'Xcode', install: 'xcode-select --install', auto: 'xcode-select --install' });
95
- }
96
- } else {
97
- try {
98
- execSync('g++ --version', { stdio: 'ignore' });
99
- } catch {
100
- required.push({ name: 'GCC/Clang', install: 'sudo apt install build-essential', auto: 'sudo apt install build-essential' });
101
- }
102
- }
103
-
104
- if (required.length > 0) {
105
- log('\n=== Missing Required Tools ===', 'yellow');
106
-
107
- for (const tool of required) {
108
- log(`\n ${tool.name}`, 'bright');
109
- log(` Install: ${tool.install}`, 'reset');
110
- if (tool.auto) {
111
- log(` Run: ${tool.auto}`, 'green');
112
- }
113
- }
114
-
115
- log('\n');
116
- return { missing: required };
117
- }
118
- return null;
119
- }
120
-
121
- const USAGE = `
122
- ${COLORS.bright}PlusUI CLI${COLORS.reset} - Build C++ desktop apps with web tech
123
-
124
- ${COLORS.bright}Usage:${COLORS.reset}
125
- plusui doctor Check development environment
126
- plusui doctor --fix Check and auto-install missing tools
127
- plusui create <name> Create a new PlusUI project
128
- plusui dev Run in development mode (Vite HMR + C++ app)
129
- plusui build Build for current platform (production)
130
- plusui build:frontend Build frontend only
131
- plusui build:backend Build C++ backend only
132
- plusui build:all Build for all platforms
133
- plusui run Run the built application
134
- plusui clean Clean build artifacts
135
- plusui bind Generate bindings for current app (alias: bindgen)
136
- plusui help Show this help message
137
-
138
- ${COLORS.bright}Platform Builds:${COLORS.reset}
139
- plusui build:windows Build for Windows
140
- plusui build:macos Build for macOS
141
- plusui build:linux Build for Linux
142
-
143
- ${COLORS.bright}Asset Commands:${COLORS.reset}
144
- plusui icons [input] Generate platform icons from source icon
145
- plusui embed [platform] Embed resources for platform (win32/darwin/linux/all)
146
-
147
- ${COLORS.bright}Options:${COLORS.reset}
148
- -h, --help Show this help message
149
- -v, --version Show version number
150
- -t, --template Specify template (solid, react)
151
-
152
- ${COLORS.bright}Assets:${COLORS.reset}
153
- Place assets/icon.png (512x512+ recommended) for automatic icon generation.
154
- All assets in "assets/" are embedded into the binary for single-exe distribution.
155
- Embedded resources are accessible via plusui::resources::getResource("path").
156
- `;
157
-
158
- // Platform configuration
159
- const PLATFORMS = {
160
- win32: { name: 'Windows', folder: 'Windows', ext: '.exe', generator: null },
161
- darwin: { name: 'macOS', folder: 'MacOS', ext: '', generator: 'Xcode' },
162
- linux: { name: 'Linux', folder: 'Linux', ext: '', generator: 'Ninja' },
163
- android: { name: 'Android', folder: 'Android', ext: '.apk', generator: 'Ninja' },
164
- ios: { name: 'iOS', folder: 'iOS', ext: '.app', generator: 'Xcode' },
165
- };
166
-
167
- function getProjectName() {
168
- try {
169
- const pkg = JSON.parse(execSync('npm pkg get name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }));
170
- return typeof pkg === 'string' ? pkg.replace(/"/g, '') : basename(process.cwd());
171
- } catch {
172
- return basename(process.cwd());
173
- }
174
- }
175
-
176
- function getCMakePath() {
177
- const paths = [
178
- 'cmake',
179
- 'C:\\Program Files\\CMake\\bin\\cmake.exe',
180
- 'C:\\Program Files (x86)\\CMake\\bin\\cmake.exe',
181
- '/usr/local/bin/cmake',
182
- '/usr/bin/cmake'
183
- ];
184
- for (const p of paths) {
185
- try {
186
- execSync(`"${p}" --version`, { stdio: 'ignore' });
187
- return p;
188
- } catch { }
189
- }
190
- return 'cmake';
191
- }
192
-
193
- function runCMake(args, options = {}) {
194
- const cmake = getCMakePath();
195
- return execSync(`"${cmake}" ${args}`, { stdio: 'inherit', ...options });
196
- }
197
-
198
- function getAppBindgenPaths() {
199
- return {
200
- featuresDir: join(process.cwd(), 'src', 'features'),
201
- outputDir: join(process.cwd(), 'src', 'Bindings_Generated'),
202
- };
203
- }
204
-
205
- function promptTemplateSelection() {
206
- return new Promise((resolve) => {
207
- const rl = createInterface({
208
- input: process.stdin,
209
- output: process.stdout
210
- });
211
-
212
- console.log(`\n${COLORS.bright}Select a template:${COLORS.reset}`);
213
- console.log(` ${COLORS.cyan}1)${COLORS.reset} solid - SolidJS + TypeScript (lightweight, reactive)`);
214
- console.log(` ${COLORS.cyan}2)${COLORS.reset} react - React + TypeScript (popular, familiar)`);
215
-
216
- rl.question(`\n${COLORS.yellow}Enter choice [1-2] (default: solid):${COLORS.reset} `, (answer) => {
217
- rl.close();
218
- const choice = answer.trim() || '1';
219
- const templates = { '1': 'solid', '2': 'react' };
220
- const template = templates[choice] || 'solid';
221
- console.log(`${COLORS.green}✓${COLORS.reset} Selected: ${COLORS.cyan}${template}${COLORS.reset}\n`);
222
- resolve(template);
223
- });
224
- });
225
- }
226
-
227
- async function createProject(name, options = {}) {
228
- try {
229
- const templateManager = new TemplateManager();
230
- await templateManager.create(name, options);
231
- } catch (e) {
232
- error(e.message);
233
- }
234
- }
235
-
236
- // ============================================================
237
- // BUILD FUNCTIONS
238
- // ============================================================
239
-
240
- function buildFrontend() {
241
- logSection('Building Frontend');
242
-
243
- if (existsSync('frontend')) {
244
- log('Running Vite build...', 'blue');
245
- execSync('cd frontend && npm run build', { stdio: 'inherit', shell: true });
246
- log('Frontend built successfully!', 'green');
247
- } else {
248
- log('No frontend directory found, skipping...', 'yellow');
249
- }
250
- }
251
-
252
- function buildBackend(platform = null, devMode = false) {
253
- const targetPlatform = platform || process.platform;
254
- const platformConfig = PLATFORMS[targetPlatform] || PLATFORMS[Object.keys(PLATFORMS).find(k => PLATFORMS[k].folder.toLowerCase() === targetPlatform?.toLowerCase())];
255
-
256
- if (!platformConfig) {
257
- error(`Unsupported platform: ${targetPlatform}`);
258
- }
259
-
260
- logSection(`Building Backend (${platformConfig.name})`);
261
-
262
- const projectName = getProjectName();
263
- const buildDir = `build/${platformConfig.folder}`;
264
-
265
- // Create build directory
266
- if (!existsSync(buildDir)) {
267
- execSync(`mkdir -p "${buildDir}"`, { stdio: 'ignore', shell: true });
268
- }
269
-
270
- // CMake configure
271
- let cmakeArgs = `-S . -B "${buildDir}" -DCMAKE_BUILD_TYPE=Release`;
272
-
273
- if (devMode) {
274
- cmakeArgs += ' -DPLUSUI_DEV_MODE=ON';
275
- }
276
-
277
- if (platformConfig.generator) {
278
- cmakeArgs += ` -G "${platformConfig.generator}"`;
279
- }
280
-
281
- log(`Configuring CMake...`, 'blue');
282
- runCMake(cmakeArgs);
283
-
284
- // CMake build
285
- log(`Building...`, 'blue');
286
- runCMake(`--build "${buildDir}" --config Release`);
287
-
288
- log(`Backend built: ${buildDir}`, 'green');
289
-
290
- return buildDir;
291
- }
292
-
293
- async function generateIcons(inputPath = null) {
294
- logSection('Generating Platform Icons');
295
-
296
- const { IconGenerator } = await import('./assets/icon-generator.js');
297
- const generator = new IconGenerator();
298
-
299
- const srcIcon = inputPath || join(process.cwd(), 'assets', 'icon.png');
300
- const outputBase = join(process.cwd(), 'assets', 'icons');
301
-
302
- if (!existsSync(srcIcon)) {
303
- log(`Icon not found: ${srcIcon}`, 'yellow');
304
- log('Place a 512x512+ PNG icon at assets/icon.png', 'dim');
305
- return;
306
- }
307
-
308
- await generator.generate(srcIcon, outputBase);
309
- }
310
-
311
- async function embedResources(platform = null) {
312
- const targetPlatform = platform || process.platform;
313
- logSection(`Embedding Resources (${targetPlatform})`);
314
-
315
- const { ResourceEmbedder } = await import('./assets/resource-embedder.js');
316
- const embedder = new ResourceEmbedder({ verbose: true });
317
-
318
- const frontendDist = join(process.cwd(), 'frontend', 'dist');
319
- const outputDir = join(process.cwd(), 'generated', 'resources');
320
-
321
- if (!existsSync(frontendDist)) {
322
- log('Frontend not built yet. Run: plusui build:frontend', 'yellow');
323
- return;
324
- }
325
-
326
- if (platform === 'all') {
327
- await embedder.embedAll(frontendDist, outputDir);
328
- } else {
329
- await embedder.embed(frontendDist, outputDir, targetPlatform);
330
- }
331
- }
332
-
333
- async function embedAssets() {
334
- const assetsDir = join(process.cwd(), 'assets');
335
- if (!existsSync(assetsDir)) {
336
- try {
337
- await mkdir(assetsDir, { recursive: true });
338
- } catch(e) {}
339
- }
340
-
341
- logSection('Embedding Assets');
342
-
343
- // Always generate the header file, even if empty
344
- let headerContent = '#pragma once\n\n';
345
- headerContent += '// THIS FILE IS AUTO-GENERATED BY PLUSUI CLI\n';
346
- headerContent += '// DO NOT MODIFY MANUALLY\n\n';
347
-
348
- const files = existsSync(assetsDir)
349
- ? (await readdir(assetsDir)).filter(f => !statSync(join(assetsDir, f)).isDirectory())
350
- : [];
351
-
352
- if (files.length === 0) {
353
- log('No assets found in assets/ folder', 'dim');
354
- } else {
355
- for (const file of files) {
356
- const filePath = join(assetsDir, file);
357
- log(`Processing ${file}...`, 'dim');
358
-
359
- const varName = file.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
360
- const data = await readFile(filePath);
361
-
362
- headerContent += `static const unsigned char ASSET_${varName}[] = {`;
363
- for (let i = 0; i < data.length; i++) {
364
- if (i % 16 === 0) headerContent += '\n ';
365
- headerContent += `0x${data[i].toString(16).padStart(2, '0')}, `;
366
- }
367
- headerContent += '\n};\n';
368
- headerContent += `static const unsigned int ASSET_${varName}_LEN = ${data.length};\n\n`;
369
- }
370
- }
371
-
372
- const genDir = join(process.cwd(), 'generated');
373
- if (!existsSync(genDir)) await mkdir(genDir, { recursive: true });
374
-
375
- await writeFile(join(genDir, 'assets.h'), headerContent);
376
- log(`✓ Assets header generated: generated/assets.h`, 'green');
377
- }
378
-
379
-
380
-
381
- async function build(production = true) {
382
- logSection('Building PlusUI Application');
383
-
384
- // Embed assets
385
- await embedAssets();
386
-
387
- // Build frontend first
388
- if (production) {
389
- buildFrontend();
390
- }
391
-
392
- // Build backend
393
- const buildDir = buildBackend(null, !production);
394
-
395
- log('\nBuild complete!', 'green');
396
- return buildDir;
397
- }
398
-
399
- function buildAll() {
400
- logSection('Building for All Platforms');
401
-
402
- buildFrontend();
403
-
404
- const supportedPlatforms = ['win32', 'darwin', 'linux'];
405
-
406
- for (const platform of supportedPlatforms) {
407
- try {
408
- buildBackend(platform, false);
409
- } catch (e) {
410
- log(`Failed to build for ${PLATFORMS[platform].name}: ${e.message}`, 'yellow');
411
- }
412
- }
413
-
414
- log('\nAll platform builds complete!', 'green');
415
- log('\nOutput directories:', 'bright');
416
- log(' build/Windows/', 'cyan');
417
- log(' build/MacOS/', 'cyan');
418
- log(' build/Linux/', 'cyan');
419
- }
420
-
421
- function buildPlatform(platform) {
422
- logSection(`Building for ${PLATFORMS[platform]?.name || platform}`);
423
- buildFrontend();
424
- buildBackend(platform, false);
425
- }
426
-
427
- // ============================================================
428
- // DEVELOPMENT FUNCTIONS
429
- // ============================================================
430
-
431
- let viteServer = null;
432
- let cppProcess = null;
433
-
434
- async function startViteServer() {
435
- log('Starting Vite dev server...', 'blue');
436
-
437
- viteServer = await createViteServer({
438
- root: 'frontend',
439
- server: {
440
- port: 5173,
441
- strictPort: true,
442
- },
443
- });
444
-
445
- await viteServer.listen();
446
-
447
- log('Vite server: http://localhost:5173', 'green');
448
- return viteServer;
449
- }
450
-
451
- async function startBackend() {
452
- logSection('Building C++ Backend (Dev Mode)');
453
-
454
- const projectName = getProjectName();
455
- killProcessByName(projectName);
456
-
457
- const buildDir = 'build/dev';
458
-
459
- // Configure with dev mode if not configured
460
- if (!existsSync(join(buildDir, 'CMakeCache.txt'))) {
461
- log('Configuring CMake...', 'blue');
462
- runCMake(`-S . -B "${buildDir}" -DPLUSUI_DEV_MODE=ON`);
463
- }
464
-
465
- log('Compiling...', 'blue');
466
- runCMake(`--build "${buildDir}"`);
467
-
468
- // Find executable
469
- let exePath;
470
- if (process.platform === 'win32') {
471
- // Visual Studio puts exe in build/dev/<projectname>/Debug/<projectname>.exe
472
- exePath = join(buildDir, projectName, 'Debug', `${projectName}.exe`);
473
- if (!existsSync(exePath)) {
474
- exePath = join(buildDir, 'Debug', `${projectName}.exe`);
475
- }
476
- if (!existsSync(exePath)) {
477
- exePath = join(buildDir, 'bin', `${projectName}.exe`);
478
- }
479
- if (!existsSync(exePath)) {
480
- exePath = join(buildDir, `${projectName}.exe`);
481
- }
482
- } else {
483
- exePath = join(buildDir, projectName);
484
- if (!existsSync(exePath)) {
485
- exePath = join(buildDir, 'bin', projectName);
486
- }
487
- }
488
-
489
- if (!existsSync(exePath)) {
490
- error(`Executable not found. Expected: ${exePath}`);
491
- }
492
-
493
- log('Starting C++ app...', 'blue');
494
-
495
- cppProcess = spawn(exePath, [], {
496
- shell: true,
497
- stdio: 'inherit',
498
- env: { ...process.env, PLUSUI_DEV: '1' }
499
- });
500
-
501
- cppProcess.on('error', (err) => {
502
- log(`Failed to start app: ${err.message}`, 'red');
503
- });
504
-
505
- return cppProcess;
506
- }
507
-
508
- async function killPort(port) {
509
- log(`Checking port ${port}...`, 'dim');
510
- try {
511
- if (process.platform === 'win32') {
512
- try {
513
- const output = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
514
- const lines = output.split('\n').filter(line => line.includes(`:${port}`) && line.includes('LISTENING'));
515
-
516
- for (const line of lines) {
517
- const parts = line.trim().split(/\s+/);
518
- const pid = parts[parts.length - 1];
519
- if (pid && /^\d+$/.test(pid)) {
520
- log(`Killing process ${pid} on port ${port}...`, 'yellow');
521
- try {
522
- execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
523
- } catch (e) {
524
- // Ignore if already dead
525
- }
526
- }
527
- }
528
- } catch (e) {
529
- // findstr returns exit code 1 if no match found
530
- }
531
- } else {
532
- try {
533
- execSync(`lsof -i :${port} -t | xargs kill -9`, { stdio: 'ignore' });
534
- } catch {
535
- // Ignore if no process found
536
- }
537
- }
538
- } catch (e) {
539
- // Ignore general errors
540
- }
541
- }
542
-
543
- function killProcessByName(name) {
544
- const processName = process.platform === 'win32' ? `${name}.exe` : name;
545
- log(`Cleaning up previous instance (${processName})...`, 'dim');
546
- try {
547
- if (process.platform === 'win32') {
548
- execSync(`taskkill /IM "${processName}" /F`, { stdio: 'ignore' });
549
- } else {
550
- execSync(`pkill -f "${processName}"`, { stdio: 'ignore' });
551
- }
552
- } catch (e) {
553
- // Ignore if process not found
554
- }
555
- }
556
-
557
- async function dev() {
558
- logSection('PlusUI Development Mode');
559
-
560
- const toolCheck = checkTools();
561
- if (toolCheck && toolCheck.missing) {
562
- error('Missing required build tools. Run: plusui doctor --fix');
563
- }
564
-
565
- // Embed assets
566
- await embedAssets();
567
-
568
- await runBindgen([], { skipIfNoInput: true, source: 'dev' });
569
-
570
- // specific port cleaning
571
- await killPort(5173);
572
-
573
- // Start Vite first
574
- try {
575
- viteServer = await startViteServer();
576
- } catch (e) {
577
- log(`Vite failed to start: ${e.message}`, 'red');
578
- error('Could not start development server');
579
- }
580
-
581
- // Build and start C++ backend
582
- await startBackend();
583
-
584
- log('\n' + '='.repeat(50), 'dim');
585
- log('Development mode active', 'green');
586
- log('='.repeat(50), 'dim');
587
- log('\nFrontend: http://localhost:5173 (HMR enabled)', 'cyan');
588
- log('Backend: C++ app running with webview', 'cyan');
589
- log('\nEdit frontend/src/* for live reload', 'dim');
590
- log('Edit main.cpp and restart for C++ changes', 'dim');
591
- log('\nPress Ctrl+C to stop\n', 'yellow');
592
-
593
- // Handle shutdown
594
- process.on('SIGINT', async () => {
595
- log('\nShutting down...', 'yellow');
596
- if (viteServer) await viteServer.close();
597
- if (cppProcess) cppProcess.kill();
598
- process.exit(0);
599
- });
600
- }
601
-
602
- async function devFrontend() {
603
- logSection('Frontend Development Mode');
604
-
605
- // specific port cleaning
606
- await killPort(5173);
607
-
608
- viteServer = await createViteServer({
609
- root: 'frontend',
610
- server: { port: 5173 },
611
- });
612
-
613
- await viteServer.listen();
614
-
615
- log('Frontend: http://localhost:5173', 'green');
616
- log('HMR enabled - changes will reflect instantly!\n', 'green');
617
-
618
- process.on('SIGINT', async () => {
619
- if (viteServer) await viteServer.close();
620
- process.exit(0);
621
- });
622
- }
623
-
624
- function devBackend() {
625
- logSection('Backend Development Mode');
626
-
627
- const projectName = getProjectName();
628
- killProcessByName(projectName);
629
-
630
- const buildDir = 'build/dev';
631
-
632
- if (!existsSync(join(buildDir, 'CMakeCache.txt'))) {
633
- log('Configuring CMake...', 'blue');
634
- runCMake(`-S . -B "${buildDir}" -DPLUSUI_DEV_MODE=ON`);
635
- }
636
-
637
- runCMake(`--build "${buildDir}"`);
638
-
639
- let exePath;
640
- if (process.platform === 'win32') {
641
- exePath = join(buildDir, projectName, 'Debug', `${projectName}.exe`);
642
- if (!existsSync(exePath)) exePath = join(buildDir, 'Debug', `${projectName}.exe`);
643
- if (!existsSync(exePath)) exePath = join(buildDir, `${projectName}.exe`);
644
- } else {
645
- exePath = join(buildDir, projectName);
646
- }
647
-
648
- function runApp() {
649
- if (cppProcess) cppProcess.kill();
650
-
651
- cppProcess = spawn(exePath, [], { shell: true, stdio: 'inherit' });
652
-
653
- cppProcess.on('close', (code) => {
654
- if (code !== null && code !== 0) {
655
- log(`App exited with code ${code}`, 'yellow');
656
- }
657
- });
658
- }
659
-
660
- runApp();
661
-
662
- log('\nWatching C++ files for changes...', 'yellow');
663
- log('Press Ctrl+C to stop\n', 'dim');
664
-
665
- // Watch main.cpp and features/ folder (if exists)
666
- const watchItems = ['main.cpp'];
667
- if (existsSync('features')) watchItems.push('features');
668
-
669
- const watchers = watchItems.map(item => {
670
- const isDir = existsSync(item) && statSync(item).isDirectory();
671
- const w = isDir ? watch(item, { recursive: true }) : watch(item);
672
- w.on('change', (eventType, filename) => {
673
- const file = item === 'main.cpp' ? 'main.cpp' : filename;
674
- if (file && (file.endsWith('.cpp') || file.endsWith('.hpp'))) {
675
- log(`Changed: ${file}`, 'yellow');
676
- log('Rebuilding...', 'blue');
677
- try {
678
- killProcessByName(projectName);
679
- runCMake(`--build "${buildDir}"`);
680
- runApp();
681
- } catch (e) {
682
- log('Build failed, waiting for fixes...', 'red');
683
- }
684
- }
685
- });
686
- return w;
687
- });
688
-
689
- process.on('SIGINT', () => {
690
- if (cppProcess) cppProcess.kill();
691
- watchers.forEach(w => w.close());
692
- process.exit(0);
693
- });
694
- }
695
-
696
- // ============================================================
697
- // RUN FUNCTION
698
- // ============================================================
699
-
700
- function run() {
701
- const projectName = getProjectName();
702
- const platform = PLATFORMS[process.platform];
703
- const buildDir = `build/${platform?.folder || 'dev'}`;
704
-
705
- let exePath;
706
- if (process.platform === 'win32') {
707
- exePath = join(buildDir, 'Release', `${projectName}.exe`);
708
- if (!existsSync(exePath)) exePath = join(buildDir, 'bin', `${projectName}.exe`);
709
- if (!existsSync(exePath)) exePath = join(buildDir, `${projectName}.exe`);
710
- } else {
711
- exePath = join(buildDir, projectName);
712
- if (!existsSync(exePath)) exePath = join(buildDir, 'bin', projectName);
713
- }
714
-
715
- if (!existsSync(exePath)) {
716
- error(`Build not found at ${exePath}. Run "plusui build" first.`);
717
- }
718
-
719
- log(`Running: ${exePath}`, 'blue');
720
-
721
- const proc = spawn(exePath, [], { shell: true, stdio: 'inherit' });
722
-
723
- proc.on('close', (code) => {
724
- process.exit(code);
725
- });
726
- }
727
-
728
- // ============================================================
729
- // CLEAN FUNCTION
730
- // ============================================================
731
-
732
- async function clean() {
733
- logSection('Cleaning Build Artifacts');
734
-
735
- const dirs = ['build', 'frontend/dist'];
736
-
737
- for (const dir of dirs) {
738
- if (existsSync(dir)) {
739
- log(`Removing: ${dir}`, 'yellow');
740
- await rm(dir, { recursive: true, force: true });
741
- }
742
- }
743
-
744
- log('\nClean complete!', 'green');
745
- }
746
-
747
- // ============================================================
748
- // BINDGEN FUNCTION
749
- // ============================================================
750
-
751
- async function runBindgen(providedArgs = null, options = {}) {
752
- logSection('Running Binding Generator');
753
-
754
- const { skipIfNoInput = false, source = 'manual' } = options;
755
-
756
- // Try to find the bindgen script
757
- let scriptPath = resolve(__dirname, '../../plusui-bindgen/src/index.js');
758
- if (!existsSync(scriptPath)) {
759
- // Try installed location (node_modules/plusui-native-bindgen)
760
- scriptPath = resolve(__dirname, '../../../plusui-native-bindgen/src/index.js');
761
- }
762
-
763
- if (!existsSync(scriptPath)) {
764
- error(`Bindgen script not found. Please ensure plusui-native-bindgen is installed.`);
765
- }
766
-
767
- log(`Using bindgen: ${scriptPath}`, 'dim');
768
-
769
- // plusui bindgen [featuresDir] [outputDir]
770
- // Defaults to app-local paths when available.
771
- const args = providedArgs ?? process.argv.slice(3);
772
- let bindgenArgs = [...args];
773
-
774
- if (bindgenArgs.length === 0) {
775
- const { featuresDir: appFeaturesDir, outputDir: appOutputDir } = getAppBindgenPaths();
776
-
777
- if (existsSync(appFeaturesDir)) {
778
- bindgenArgs = [appFeaturesDir, appOutputDir];
779
- log(`App mode: ${appFeaturesDir} -> ${appOutputDir}`, 'dim');
780
- } else {
781
- if (skipIfNoInput) {
782
- log(`No src/features folder found; skipping binding refresh for ${source}.`, 'dim');
783
- return;
784
- } else {
785
- error('No bindgen input found. Create src/features in your app or pass paths: plusui bindgen <featuresDir> <outputDir>');
786
- }
787
- }
788
- }
789
-
790
- // Spawn node process
791
- const proc = spawn(process.execPath, [scriptPath, ...bindgenArgs], {
792
- stdio: 'inherit',
793
- env: process.env
794
- });
795
-
796
- return new Promise((resolve, reject) => {
797
- proc.on('close', (code) => {
798
- if (code === 0) {
799
- log('\nBindgen complete!', 'green');
800
- resolve();
801
- } else {
802
- reject(new Error(`Bindgen failed with code ${code}`));
803
- }
804
- });
805
- });
806
- }
807
-
808
- // ============================================================
809
- // MAIN
810
- // ============================================================
811
-
812
- async function main() {
813
- const args = process.argv.slice(2);
814
- const cmd = args[0];
815
-
816
- switch (cmd) {
817
- case 'doctor':
818
- await runDoctor({
819
- fix: args.includes('--fix'),
820
- json: args.includes('--json'),
821
- quick: args.includes('--quick')
822
- });
823
- break;
824
- case 'create':
825
- if (!args[1] || args[1].startsWith('-')) error('Project name required');
826
- const template = await promptTemplateSelection();
827
- await createProject(args[1], { template });
828
- break;
829
- case 'dev':
830
- await dev();
831
- break;
832
- case 'dev:frontend':
833
- await devFrontend();
834
- break;
835
- case 'dev:backend':
836
- await runBindgen([], { skipIfNoInput: true, source: 'dev:backend' });
837
- devBackend();
838
- break;
839
- case 'build':
840
- await runBindgen([], { skipIfNoInput: true, source: 'build' });
841
- await build(true);
842
- break;
843
- case 'build:frontend':
844
- buildFrontend();
845
- break;
846
- case 'build:backend':
847
- await runBindgen([], { skipIfNoInput: true, source: 'build:backend' });
848
- buildBackend(null, false);
849
- break;
850
- case 'build:all':
851
- await runBindgen([], { skipIfNoInput: true, source: 'build:all' });
852
- buildAll();
853
- break;
854
- case 'build:windows':
855
- await runBindgen([], { skipIfNoInput: true, source: 'build:windows' });
856
- buildPlatform('win32');
857
- break;
858
- case 'build:macos':
859
- await runBindgen([], { skipIfNoInput: true, source: 'build:macos' });
860
- buildPlatform('darwin');
861
- break;
862
- case 'build:linux':
863
- await runBindgen([], { skipIfNoInput: true, source: 'build:linux' });
864
- buildPlatform('linux');
865
- break;
866
- case 'build:android':
867
- await runBindgen([], { skipIfNoInput: true, source: 'build:android' });
868
- buildPlatform('android');
869
- break;
870
- case 'build:ios':
871
- await runBindgen([], { skipIfNoInput: true, source: 'build:ios' });
872
- buildPlatform('ios');
873
- break;
874
- case 'run':
875
- run();
876
- break;
877
- case 'clean':
878
- await clean();
879
- break;
880
- case 'bind':
881
- case 'bindgen':
882
- await runBindgen();
883
- break;
884
- case 'icons':
885
- await generateIcons(args[1]);
886
- break;
887
- case 'embed':
888
- await embedResources(args[1]);
889
- break;
890
- case 'help':
891
- case '-h':
892
- case '--help':
893
- console.log(USAGE);
894
- break;
895
- case '-v':
896
- case '--version':
897
- console.log('plusui-cli v0.2.0');
898
- break;
899
- default:
900
- console.log(USAGE);
901
- }
902
- }
903
-
904
- main().catch(e => error(e.message));
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
+ const COLORS = {
17
+ reset: '\x1b[0m',
18
+ bright: '\x1b[1m',
19
+ dim: '\x1b[2m',
20
+ green: '\x1b[32m',
21
+ blue: '\x1b[34m',
22
+ yellow: '\x1b[33m',
23
+ red: '\x1b[31m',
24
+ cyan: '\x1b[36m',
25
+ magenta: '\x1b[35m',
26
+ };
27
+
28
+ function log(msg, color = 'reset') {
29
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
30
+ }
31
+
32
+ function logSection(title) {
33
+ console.log(`\n${COLORS.bright}${COLORS.blue}=== ${title} ===${COLORS.reset}\n`);
34
+ }
35
+
36
+ function error(msg) {
37
+ console.error(`${COLORS.red}Error: ${msg}${COLORS.reset}`);
38
+ process.exit(1);
39
+ }
40
+
41
+ function checkTools() {
42
+ const platform = process.platform;
43
+ const required = [];
44
+
45
+ const cmakePaths = [
46
+ 'cmake',
47
+ 'C:\\Program Files\\CMake\\bin\\cmake.exe',
48
+ 'C:\\Program Files (x86)\\CMake\\bin\\cmake.exe',
49
+ '/usr/local/bin/cmake',
50
+ '/usr/bin/cmake'
51
+ ];
52
+
53
+ let cmakeFound = false;
54
+ for (const p of cmakePaths) {
55
+ try {
56
+ execSync(`"${p}" --version`, { stdio: 'ignore' });
57
+ cmakeFound = true;
58
+ break;
59
+ } catch { }
60
+ }
61
+
62
+ if (!cmakeFound) {
63
+ if (platform === 'win32') {
64
+ required.push({ name: 'CMake', install: 'winget install Kitware.CMake', auto: 'winget install -e --id Kitware.CMake' });
65
+ } else if (platform === 'darwin') {
66
+ required.push({ name: 'CMake', install: 'brew install cmake', auto: 'brew install cmake' });
67
+ } else {
68
+ required.push({ name: 'CMake', install: 'sudo apt install cmake', auto: 'sudo apt install cmake' });
69
+ }
70
+ }
71
+
72
+ if (platform === 'win32') {
73
+ const vsPaths = [
74
+ 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC',
75
+ 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional\\VC\\Tools\\MSVC',
76
+ 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Tools\\MSVC'
77
+ ];
78
+
79
+ let vsFound = false;
80
+ for (const p of vsPaths) {
81
+ if (existsSync(p)) {
82
+ vsFound = true;
83
+ break;
84
+ }
85
+ }
86
+
87
+ if (!vsFound) {
88
+ required.push({ name: 'Visual Studio 2022', install: 'Download from visualstudio.microsoft.com with C++ workload', auto: null });
89
+ }
90
+ } else if (platform === 'darwin') {
91
+ try {
92
+ execSync('clang++ --version', { stdio: 'ignore' });
93
+ } catch {
94
+ required.push({ name: 'Xcode', install: 'xcode-select --install', auto: 'xcode-select --install' });
95
+ }
96
+ } else {
97
+ try {
98
+ execSync('g++ --version', { stdio: 'ignore' });
99
+ } catch {
100
+ required.push({ name: 'GCC/Clang', install: 'sudo apt install build-essential', auto: 'sudo apt install build-essential' });
101
+ }
102
+ }
103
+
104
+ if (required.length > 0) {
105
+ log('\n=== Missing Required Tools ===', 'yellow');
106
+
107
+ for (const tool of required) {
108
+ log(`\n ${tool.name}`, 'bright');
109
+ log(` Install: ${tool.install}`, 'reset');
110
+ if (tool.auto) {
111
+ log(` Run: ${tool.auto}`, 'green');
112
+ }
113
+ }
114
+
115
+ log('\n');
116
+ return { missing: required };
117
+ }
118
+ return null;
119
+ }
120
+
121
+ const USAGE = `
122
+ ${COLORS.bright}PlusUI CLI${COLORS.reset} - Build C++ desktop apps with web tech
123
+
124
+ ${COLORS.bright}Usage:${COLORS.reset}
125
+ plusui doctor Check development environment
126
+ plusui doctor --fix Check and auto-install missing tools
127
+ plusui create <name> Create a new PlusUI project
128
+ plusui dev Run in development mode (Vite HMR + C++ app)
129
+ plusui build Build for current platform (production)
130
+ plusui build:frontend Build frontend only
131
+ plusui build:backend Build C++ backend only
132
+ plusui build:all Build for all platforms
133
+ plusui run Run the built application
134
+ plusui clean Clean build artifacts
135
+ plusui bind Generate bindings for current app (alias: bindgen)
136
+ plusui help Show this help message
137
+
138
+ ${COLORS.bright}Platform Builds:${COLORS.reset}
139
+ plusui build:windows Build for Windows
140
+ plusui build:macos Build for macOS
141
+ plusui build:linux Build for Linux
142
+
143
+ ${COLORS.bright}Asset Commands:${COLORS.reset}
144
+ plusui icons [input] Generate platform icons from source icon
145
+ plusui embed [platform] Embed resources for platform (win32/darwin/linux/all)
146
+
147
+ ${COLORS.bright}Options:${COLORS.reset}
148
+ -h, --help Show this help message
149
+ -v, --version Show version number
150
+ -t, --template Specify template (solid, react)
151
+
152
+ ${COLORS.bright}Assets:${COLORS.reset}
153
+ Place assets/icon.png (512x512+ recommended) for automatic icon generation.
154
+ All assets in "assets/" are embedded into the binary for single-exe distribution.
155
+ Embedded resources are accessible via plusui::resources::getResource("path").
156
+ `;
157
+
158
+ // Platform configuration
159
+ const PLATFORMS = {
160
+ win32: { name: 'Windows', folder: 'Windows', ext: '.exe', generator: null },
161
+ darwin: { name: 'macOS', folder: 'MacOS', ext: '', generator: 'Xcode' },
162
+ linux: { name: 'Linux', folder: 'Linux', ext: '', generator: 'Ninja' },
163
+ android: { name: 'Android', folder: 'Android', ext: '.apk', generator: 'Ninja' },
164
+ ios: { name: 'iOS', folder: 'iOS', ext: '.app', generator: 'Xcode' },
165
+ };
166
+
167
+ function getProjectName() {
168
+ try {
169
+ const pkg = JSON.parse(execSync('npm pkg get name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }));
170
+ return typeof pkg === 'string' ? pkg.replace(/"/g, '') : basename(process.cwd());
171
+ } catch {
172
+ return basename(process.cwd());
173
+ }
174
+ }
175
+
176
+ function getCMakePath() {
177
+ const paths = [
178
+ 'cmake',
179
+ 'C:\\Program Files\\CMake\\bin\\cmake.exe',
180
+ 'C:\\Program Files (x86)\\CMake\\bin\\cmake.exe',
181
+ '/usr/local/bin/cmake',
182
+ '/usr/bin/cmake'
183
+ ];
184
+ for (const p of paths) {
185
+ try {
186
+ execSync(`"${p}" --version`, { stdio: 'ignore' });
187
+ return p;
188
+ } catch { }
189
+ }
190
+ return 'cmake';
191
+ }
192
+
193
+ function runCMake(args, options = {}) {
194
+ const cmake = getCMakePath();
195
+ return execSync(`"${cmake}" ${args}`, { stdio: 'inherit', ...options });
196
+ }
197
+
198
+ function getAppBindgenPaths() {
199
+ return {
200
+ featuresDir: join(process.cwd(), 'src', 'features'),
201
+ outputDir: join(process.cwd(), 'src', 'Bindings_Generated'),
202
+ frontendOutputDir: join(process.cwd(), 'frontend', 'src', 'Bindings_Generated'),
203
+ };
204
+ }
205
+
206
+ function ensureBuildLayout() {
207
+ const buildRoot = join(process.cwd(), 'build');
208
+ for (const platform of Object.values(PLATFORMS)) {
209
+ mkdirSync(join(buildRoot, platform.folder), { recursive: true });
210
+ }
211
+ }
212
+
213
+ function resolveBindgenScriptPath() {
214
+ const candidates = [
215
+ resolve(__dirname, '../../plusui-bindgen/src/index.js'),
216
+ resolve(__dirname, '../../plusui-native-bindgen/src/index.js'),
217
+ resolve(__dirname, '../../../plusui-native-bindgen/src/index.js'),
218
+ resolve(process.cwd(), 'node_modules', 'plusui-native-bindgen', 'src', 'index.js'),
219
+ ];
220
+
221
+ for (const candidate of candidates) {
222
+ if (existsSync(candidate)) {
223
+ return candidate;
224
+ }
225
+ }
226
+
227
+ return null;
228
+ }
229
+
230
+ async function syncGeneratedTsBindings(backendOutputDir, frontendOutputDir) {
231
+ const generatedTsPath = join(backendOutputDir, 'bindings.gen.ts');
232
+ if (!existsSync(generatedTsPath)) {
233
+ return;
234
+ }
235
+
236
+ await mkdir(frontendOutputDir, { recursive: true });
237
+ const frontendTsPath = join(frontendOutputDir, 'bindings.gen.ts');
238
+ await copyFile(generatedTsPath, frontendTsPath);
239
+ log(`Synced TS bindings: ${frontendTsPath}`, 'dim');
240
+ }
241
+
242
+ function promptTemplateSelection() {
243
+ return new Promise((resolve) => {
244
+ const rl = createInterface({
245
+ input: process.stdin,
246
+ output: process.stdout
247
+ });
248
+
249
+ console.log(`\n${COLORS.bright}Select a template:${COLORS.reset}`);
250
+ console.log(` ${COLORS.cyan}1)${COLORS.reset} solid - SolidJS + TypeScript (lightweight, reactive)`);
251
+ console.log(` ${COLORS.cyan}2)${COLORS.reset} react - React + TypeScript (popular, familiar)`);
252
+
253
+ rl.question(`\n${COLORS.yellow}Enter choice [1-2] (default: solid):${COLORS.reset} `, (answer) => {
254
+ rl.close();
255
+ const choice = answer.trim() || '1';
256
+ const templates = { '1': 'solid', '2': 'react' };
257
+ const template = templates[choice] || 'solid';
258
+ console.log(`${COLORS.green}✓${COLORS.reset} Selected: ${COLORS.cyan}${template}${COLORS.reset}\n`);
259
+ resolve(template);
260
+ });
261
+ });
262
+ }
263
+
264
+ async function createProject(name, options = {}) {
265
+ try {
266
+ const templateManager = new TemplateManager();
267
+ await templateManager.create(name, options);
268
+ } catch (e) {
269
+ error(e.message);
270
+ }
271
+ }
272
+
273
+ // ============================================================
274
+ // BUILD FUNCTIONS
275
+ // ============================================================
276
+
277
+ function buildFrontend() {
278
+ logSection('Building Frontend');
279
+
280
+ if (existsSync('frontend')) {
281
+ log('Running Vite build...', 'blue');
282
+ execSync('cd frontend && npm run build', { stdio: 'inherit', shell: true });
283
+ log('Frontend built successfully!', 'green');
284
+ } else {
285
+ log('No frontend directory found, skipping...', 'yellow');
286
+ }
287
+ }
288
+
289
+ function buildBackend(platform = null, devMode = false) {
290
+ const targetPlatform = platform || process.platform;
291
+ const platformConfig = PLATFORMS[targetPlatform] || PLATFORMS[Object.keys(PLATFORMS).find(k => PLATFORMS[k].folder.toLowerCase() === targetPlatform?.toLowerCase())];
292
+
293
+ if (!platformConfig) {
294
+ error(`Unsupported platform: ${targetPlatform}`);
295
+ }
296
+
297
+ logSection(`Building Backend (${platformConfig.name})`);
298
+
299
+ const buildDir = `build/${platformConfig.folder}`;
300
+
301
+ ensureBuildLayout();
302
+
303
+ // Create build directory
304
+ if (!existsSync(buildDir)) {
305
+ execSync(`mkdir -p "${buildDir}"`, { stdio: 'ignore', shell: true });
306
+ }
307
+
308
+ // CMake configure
309
+ let cmakeArgs = `-S . -B "${buildDir}" -DCMAKE_BUILD_TYPE=Release`;
310
+
311
+ if (devMode) {
312
+ cmakeArgs += ' -DPLUSUI_DEV_MODE=ON';
313
+ }
314
+
315
+ if (platformConfig.generator) {
316
+ cmakeArgs += ` -G "${platformConfig.generator}"`;
317
+ }
318
+
319
+ log(`Configuring CMake...`, 'blue');
320
+ runCMake(cmakeArgs);
321
+
322
+ // CMake build
323
+ log(`Building...`, 'blue');
324
+ runCMake(`--build "${buildDir}" --config Release`);
325
+
326
+ log(`Backend built: ${buildDir}`, 'green');
327
+
328
+ return buildDir;
329
+ }
330
+
331
+ async function generateIcons(inputPath = null) {
332
+ logSection('Generating Platform Icons');
333
+
334
+ const { IconGenerator } = await import('./assets/icon-generator.js');
335
+ const generator = new IconGenerator();
336
+
337
+ const srcIcon = inputPath || join(process.cwd(), 'assets', 'icon.png');
338
+ const outputBase = join(process.cwd(), 'assets', 'icons');
339
+
340
+ if (!existsSync(srcIcon)) {
341
+ log(`Icon not found: ${srcIcon}`, 'yellow');
342
+ log('Place a 512x512+ PNG icon at assets/icon.png', 'dim');
343
+ return;
344
+ }
345
+
346
+ await generator.generate(srcIcon, outputBase);
347
+ }
348
+
349
+ async function embedResources(platform = null) {
350
+ const targetPlatform = platform || process.platform;
351
+ logSection(`Embedding Resources (${targetPlatform})`);
352
+
353
+ const { ResourceEmbedder } = await import('./assets/resource-embedder.js');
354
+ const embedder = new ResourceEmbedder({ verbose: true });
355
+
356
+ const frontendDist = join(process.cwd(), 'frontend', 'dist');
357
+ const outputDir = join(process.cwd(), 'generated', 'resources');
358
+
359
+ if (!existsSync(frontendDist)) {
360
+ log('Frontend not built yet. Run: plusui build:frontend', 'yellow');
361
+ return;
362
+ }
363
+
364
+ if (platform === 'all') {
365
+ await embedder.embedAll(frontendDist, outputDir);
366
+ } else {
367
+ await embedder.embed(frontendDist, outputDir, targetPlatform);
368
+ }
369
+ }
370
+
371
+ async function embedAssets() {
372
+ const assetsDir = join(process.cwd(), 'assets');
373
+ if (!existsSync(assetsDir)) {
374
+ try {
375
+ await mkdir(assetsDir, { recursive: true });
376
+ } catch(e) {}
377
+ }
378
+
379
+ logSection('Embedding Assets');
380
+
381
+ // Always generate the header file, even if empty
382
+ let headerContent = '#pragma once\n\n';
383
+ headerContent += '// THIS FILE IS AUTO-GENERATED BY PLUSUI CLI\n';
384
+ headerContent += '// DO NOT MODIFY MANUALLY\n\n';
385
+
386
+ const files = existsSync(assetsDir)
387
+ ? (await readdir(assetsDir)).filter(f => !statSync(join(assetsDir, f)).isDirectory())
388
+ : [];
389
+
390
+ if (files.length === 0) {
391
+ log('No assets found in assets/ folder', 'dim');
392
+ } else {
393
+ for (const file of files) {
394
+ const filePath = join(assetsDir, file);
395
+ log(`Processing ${file}...`, 'dim');
396
+
397
+ const varName = file.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
398
+ const data = await readFile(filePath);
399
+
400
+ headerContent += `static const unsigned char ASSET_${varName}[] = {`;
401
+ for (let i = 0; i < data.length; i++) {
402
+ if (i % 16 === 0) headerContent += '\n ';
403
+ headerContent += `0x${data[i].toString(16).padStart(2, '0')}, `;
404
+ }
405
+ headerContent += '\n};\n';
406
+ headerContent += `static const unsigned int ASSET_${varName}_LEN = ${data.length};\n\n`;
407
+ }
408
+ }
409
+
410
+ const genDir = join(process.cwd(), 'generated');
411
+ if (!existsSync(genDir)) await mkdir(genDir, { recursive: true });
412
+
413
+ await writeFile(join(genDir, 'assets.h'), headerContent);
414
+ log(`✓ Assets header generated: generated/assets.h`, 'green');
415
+ }
416
+
417
+
418
+
419
+ async function build(production = true) {
420
+ logSection('Building PlusUI Application');
421
+
422
+ // Embed assets
423
+ await embedAssets();
424
+
425
+ // Build frontend first
426
+ if (production) {
427
+ buildFrontend();
428
+ }
429
+
430
+ // Build backend
431
+ const buildDir = buildBackend(null, !production);
432
+
433
+ log('\nBuild complete!', 'green');
434
+ return buildDir;
435
+ }
436
+
437
+ function buildAll() {
438
+ logSection('Building for All Platforms');
439
+
440
+ ensureBuildLayout();
441
+
442
+ buildFrontend();
443
+
444
+ const supportedPlatforms = ['win32', 'darwin', 'linux'];
445
+
446
+ for (const platform of supportedPlatforms) {
447
+ try {
448
+ buildBackend(platform, false);
449
+ } catch (e) {
450
+ log(`Failed to build for ${PLATFORMS[platform].name}: ${e.message}`, 'yellow');
451
+ }
452
+ }
453
+
454
+ log('\nAll platform builds complete!', 'green');
455
+ log('\nOutput directories:', 'bright');
456
+ log(' build/Windows/', 'cyan');
457
+ log(' build/MacOS/', 'cyan');
458
+ log(' build/Linux/', 'cyan');
459
+ log(' build/Android/', 'cyan');
460
+ log(' build/iOS/', 'cyan');
461
+ }
462
+
463
+ function buildPlatform(platform) {
464
+ logSection(`Building for ${PLATFORMS[platform]?.name || platform}`);
465
+ buildFrontend();
466
+ buildBackend(platform, false);
467
+ }
468
+
469
+ // ============================================================
470
+ // DEVELOPMENT FUNCTIONS
471
+ // ============================================================
472
+
473
+ let viteServer = null;
474
+ let cppProcess = null;
475
+
476
+ async function startViteServer() {
477
+ log('Starting Vite dev server...', 'blue');
478
+
479
+ viteServer = await createViteServer({
480
+ root: 'frontend',
481
+ server: {
482
+ port: 5173,
483
+ strictPort: true,
484
+ },
485
+ });
486
+
487
+ await viteServer.listen();
488
+
489
+ log('Vite server: http://localhost:5173', 'green');
490
+ return viteServer;
491
+ }
492
+
493
+ async function startBackend() {
494
+ logSection('Building C++ Backend (Dev Mode)');
495
+
496
+ const projectName = getProjectName();
497
+ killProcessByName(projectName);
498
+
499
+ const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
500
+ const buildDir = join('build', platformFolder, 'dev');
501
+
502
+ // Configure with dev mode if not configured
503
+ if (!existsSync(join(buildDir, 'CMakeCache.txt'))) {
504
+ log('Configuring CMake...', 'blue');
505
+ runCMake(`-S . -B "${buildDir}" -DPLUSUI_DEV_MODE=ON`);
506
+ }
507
+
508
+ log('Compiling...', 'blue');
509
+ runCMake(`--build "${buildDir}"`);
510
+
511
+ // Find executable
512
+ let exePath;
513
+ if (process.platform === 'win32') {
514
+ // Visual Studio puts exe in build/dev/<projectname>/Debug/<projectname>.exe
515
+ exePath = join(buildDir, projectName, 'Debug', `${projectName}.exe`);
516
+ if (!existsSync(exePath)) {
517
+ exePath = join(buildDir, 'Debug', `${projectName}.exe`);
518
+ }
519
+ if (!existsSync(exePath)) {
520
+ exePath = join(buildDir, 'bin', `${projectName}.exe`);
521
+ }
522
+ if (!existsSync(exePath)) {
523
+ exePath = join(buildDir, `${projectName}.exe`);
524
+ }
525
+ } else {
526
+ exePath = join(buildDir, projectName);
527
+ if (!existsSync(exePath)) {
528
+ exePath = join(buildDir, 'bin', projectName);
529
+ }
530
+ }
531
+
532
+ if (!existsSync(exePath)) {
533
+ error(`Executable not found. Expected: ${exePath}`);
534
+ }
535
+
536
+ log('Starting C++ app...', 'blue');
537
+
538
+ cppProcess = spawn(exePath, [], {
539
+ shell: true,
540
+ stdio: 'inherit',
541
+ env: { ...process.env, PLUSUI_DEV: '1' }
542
+ });
543
+
544
+ cppProcess.on('error', (err) => {
545
+ log(`Failed to start app: ${err.message}`, 'red');
546
+ });
547
+
548
+ return cppProcess;
549
+ }
550
+
551
+ async function killPort(port) {
552
+ log(`Checking port ${port}...`, 'dim');
553
+ try {
554
+ if (process.platform === 'win32') {
555
+ try {
556
+ const output = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
557
+ const lines = output.split('\n').filter(line => line.includes(`:${port}`) && line.includes('LISTENING'));
558
+
559
+ for (const line of lines) {
560
+ const parts = line.trim().split(/\s+/);
561
+ const pid = parts[parts.length - 1];
562
+ if (pid && /^\d+$/.test(pid)) {
563
+ log(`Killing process ${pid} on port ${port}...`, 'yellow');
564
+ try {
565
+ execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
566
+ } catch (e) {
567
+ // Ignore if already dead
568
+ }
569
+ }
570
+ }
571
+ } catch (e) {
572
+ // findstr returns exit code 1 if no match found
573
+ }
574
+ } else {
575
+ try {
576
+ execSync(`lsof -i :${port} -t | xargs kill -9`, { stdio: 'ignore' });
577
+ } catch {
578
+ // Ignore if no process found
579
+ }
580
+ }
581
+ } catch (e) {
582
+ // Ignore general errors
583
+ }
584
+ }
585
+
586
+ function killProcessByName(name) {
587
+ const processName = process.platform === 'win32' ? `${name}.exe` : name;
588
+ log(`Cleaning up previous instance (${processName})...`, 'dim');
589
+ try {
590
+ if (process.platform === 'win32') {
591
+ execSync(`taskkill /IM "${processName}" /F`, { stdio: 'ignore' });
592
+ } else {
593
+ execSync(`pkill -f "${processName}"`, { stdio: 'ignore' });
594
+ }
595
+ } catch (e) {
596
+ // Ignore if process not found
597
+ }
598
+ }
599
+
600
+ async function dev() {
601
+ logSection('PlusUI Development Mode');
602
+
603
+ const toolCheck = checkTools();
604
+ if (toolCheck && toolCheck.missing) {
605
+ error('Missing required build tools. Run: plusui doctor --fix');
606
+ }
607
+
608
+ // Embed assets
609
+ await embedAssets();
610
+
611
+ await runBindgen([], { skipIfNoInput: true, source: 'dev' });
612
+
613
+ // specific port cleaning
614
+ await killPort(5173);
615
+
616
+ // Start Vite first
617
+ try {
618
+ viteServer = await startViteServer();
619
+ } catch (e) {
620
+ log(`Vite failed to start: ${e.message}`, 'red');
621
+ error('Could not start development server');
622
+ }
623
+
624
+ // Build and start C++ backend
625
+ await startBackend();
626
+
627
+ log('\n' + '='.repeat(50), 'dim');
628
+ log('Development mode active', 'green');
629
+ log('='.repeat(50), 'dim');
630
+ log('\nFrontend: http://localhost:5173 (HMR enabled)', 'cyan');
631
+ log('Backend: C++ app running with webview', 'cyan');
632
+ log('\nEdit frontend/src/* for live reload', 'dim');
633
+ log('Edit main.cpp and restart for C++ changes', 'dim');
634
+ log('\nPress Ctrl+C to stop\n', 'yellow');
635
+
636
+ // Handle shutdown
637
+ process.on('SIGINT', async () => {
638
+ log('\nShutting down...', 'yellow');
639
+ if (viteServer) await viteServer.close();
640
+ if (cppProcess) cppProcess.kill();
641
+ process.exit(0);
642
+ });
643
+ }
644
+
645
+ async function devFrontend() {
646
+ logSection('Frontend Development Mode');
647
+
648
+ // specific port cleaning
649
+ await killPort(5173);
650
+
651
+ viteServer = await createViteServer({
652
+ root: 'frontend',
653
+ server: { port: 5173 },
654
+ });
655
+
656
+ await viteServer.listen();
657
+
658
+ log('Frontend: http://localhost:5173', 'green');
659
+ log('HMR enabled - changes will reflect instantly!\n', 'green');
660
+
661
+ process.on('SIGINT', async () => {
662
+ if (viteServer) await viteServer.close();
663
+ process.exit(0);
664
+ });
665
+ }
666
+
667
+ function devBackend() {
668
+ logSection('Backend Development Mode');
669
+
670
+ const projectName = getProjectName();
671
+ killProcessByName(projectName);
672
+
673
+ const platformFolder = PLATFORMS[process.platform]?.folder || 'Windows';
674
+ const buildDir = join('build', platformFolder, 'dev');
675
+
676
+ if (!existsSync(join(buildDir, 'CMakeCache.txt'))) {
677
+ log('Configuring CMake...', 'blue');
678
+ runCMake(`-S . -B "${buildDir}" -DPLUSUI_DEV_MODE=ON`);
679
+ }
680
+
681
+ runCMake(`--build "${buildDir}"`);
682
+
683
+ let exePath;
684
+ if (process.platform === 'win32') {
685
+ exePath = join(buildDir, projectName, 'Debug', `${projectName}.exe`);
686
+ if (!existsSync(exePath)) exePath = join(buildDir, 'Debug', `${projectName}.exe`);
687
+ if (!existsSync(exePath)) exePath = join(buildDir, `${projectName}.exe`);
688
+ } else {
689
+ exePath = join(buildDir, projectName);
690
+ }
691
+
692
+ function runApp() {
693
+ if (cppProcess) cppProcess.kill();
694
+
695
+ cppProcess = spawn(exePath, [], { shell: true, stdio: 'inherit' });
696
+
697
+ cppProcess.on('close', (code) => {
698
+ if (code !== null && code !== 0) {
699
+ log(`App exited with code ${code}`, 'yellow');
700
+ }
701
+ });
702
+ }
703
+
704
+ runApp();
705
+
706
+ log('\nWatching C++ files for changes...', 'yellow');
707
+ log('Press Ctrl+C to stop\n', 'dim');
708
+
709
+ // Watch main.cpp and features/ folder (if exists)
710
+ const watchItems = ['main.cpp'];
711
+ if (existsSync('features')) watchItems.push('features');
712
+
713
+ const watchers = watchItems.map(item => {
714
+ const isDir = existsSync(item) && statSync(item).isDirectory();
715
+ const w = isDir ? watch(item, { recursive: true }) : watch(item);
716
+ w.on('change', (eventType, filename) => {
717
+ const file = item === 'main.cpp' ? 'main.cpp' : filename;
718
+ if (file && (file.endsWith('.cpp') || file.endsWith('.hpp'))) {
719
+ log(`Changed: ${file}`, 'yellow');
720
+ log('Rebuilding...', 'blue');
721
+ try {
722
+ killProcessByName(projectName);
723
+ runCMake(`--build "${buildDir}"`);
724
+ runApp();
725
+ } catch (e) {
726
+ log('Build failed, waiting for fixes...', 'red');
727
+ }
728
+ }
729
+ });
730
+ return w;
731
+ });
732
+
733
+ process.on('SIGINT', () => {
734
+ if (cppProcess) cppProcess.kill();
735
+ watchers.forEach(w => w.close());
736
+ process.exit(0);
737
+ });
738
+ }
739
+
740
+ // ============================================================
741
+ // RUN FUNCTION
742
+ // ============================================================
743
+
744
+ function run() {
745
+ const projectName = getProjectName();
746
+ const platform = PLATFORMS[process.platform];
747
+ const buildDir = `build/${platform?.folder || 'Windows'}`;
748
+
749
+ let exePath;
750
+ if (process.platform === 'win32') {
751
+ exePath = join(buildDir, 'Release', `${projectName}.exe`);
752
+ if (!existsSync(exePath)) exePath = join(buildDir, 'bin', `${projectName}.exe`);
753
+ if (!existsSync(exePath)) exePath = join(buildDir, `${projectName}.exe`);
754
+ } else {
755
+ exePath = join(buildDir, projectName);
756
+ if (!existsSync(exePath)) exePath = join(buildDir, 'bin', projectName);
757
+ }
758
+
759
+ if (!existsSync(exePath)) {
760
+ error(`Build not found at ${exePath}. Run "plusui build" first.`);
761
+ }
762
+
763
+ log(`Running: ${exePath}`, 'blue');
764
+
765
+ const proc = spawn(exePath, [], { shell: true, stdio: 'inherit' });
766
+
767
+ proc.on('close', (code) => {
768
+ process.exit(code);
769
+ });
770
+ }
771
+
772
+ // ============================================================
773
+ // CLEAN FUNCTION
774
+ // ============================================================
775
+
776
+ async function clean() {
777
+ logSection('Cleaning Build Artifacts');
778
+
779
+ const dirs = ['build', 'frontend/dist'];
780
+
781
+ for (const dir of dirs) {
782
+ if (existsSync(dir)) {
783
+ log(`Removing: ${dir}`, 'yellow');
784
+ await rm(dir, { recursive: true, force: true });
785
+ }
786
+ }
787
+
788
+ log('\nClean complete!', 'green');
789
+ }
790
+
791
+ // ============================================================
792
+ // BINDGEN FUNCTION
793
+ // ============================================================
794
+
795
+ async function runBindgen(providedArgs = null, options = {}) {
796
+ logSection('Running Binding Generator');
797
+
798
+ const { skipIfNoInput = false, source = 'manual' } = options;
799
+
800
+ const scriptPath = resolveBindgenScriptPath();
801
+
802
+ if (!scriptPath) {
803
+ error(`Bindgen script not found. Please ensure plusui-native-bindgen is installed.`);
804
+ }
805
+
806
+ log(`Using bindgen: ${scriptPath}`, 'dim');
807
+
808
+ // plusui bindgen [featuresDir] [outputDir]
809
+ // Defaults to app-local paths when available.
810
+ const args = providedArgs ?? process.argv.slice(3);
811
+ let bindgenArgs = [...args];
812
+
813
+ let usedDefaultAppMode = false;
814
+ let defaultOutputDir = null;
815
+ let defaultFrontendOutputDir = null;
816
+
817
+ if (bindgenArgs.length === 0) {
818
+ const { featuresDir: appFeaturesDir, outputDir: appOutputDir, frontendOutputDir } = getAppBindgenPaths();
819
+
820
+ if (existsSync(appFeaturesDir)) {
821
+ bindgenArgs = [appFeaturesDir, appOutputDir];
822
+ usedDefaultAppMode = true;
823
+ defaultOutputDir = appOutputDir;
824
+ defaultFrontendOutputDir = frontendOutputDir;
825
+ log(`App mode: ${appFeaturesDir} -> ${appOutputDir}`, 'dim');
826
+ } else {
827
+ if (skipIfNoInput) {
828
+ log(`No src/features folder found; skipping binding refresh for ${source}.`, 'dim');
829
+ return;
830
+ } else {
831
+ error('No bindgen input found. Create src/features in your app or pass paths: plusui bindgen <featuresDir> <outputDir>');
832
+ }
833
+ }
834
+ }
835
+
836
+ // Spawn node process
837
+ const proc = spawn(process.execPath, [scriptPath, ...bindgenArgs], {
838
+ stdio: 'inherit',
839
+ env: process.env
840
+ });
841
+
842
+ return new Promise((resolve, reject) => {
843
+ proc.on('close', async (code) => {
844
+ if (code === 0) {
845
+ try {
846
+ if (usedDefaultAppMode && defaultOutputDir && defaultFrontendOutputDir) {
847
+ await syncGeneratedTsBindings(defaultOutputDir, defaultFrontendOutputDir);
848
+ }
849
+ log('\nBindgen complete!', 'green');
850
+ resolve();
851
+ } catch (syncErr) {
852
+ reject(syncErr);
853
+ }
854
+ } else {
855
+ reject(new Error(`Bindgen failed with code ${code}`));
856
+ }
857
+ });
858
+ });
859
+ }
860
+
861
+ // ============================================================
862
+ // MAIN
863
+ // ============================================================
864
+
865
+ async function main() {
866
+ const args = process.argv.slice(2);
867
+ const cmd = args[0];
868
+
869
+ switch (cmd) {
870
+ case 'doctor':
871
+ await runDoctor({
872
+ fix: args.includes('--fix'),
873
+ json: args.includes('--json'),
874
+ quick: args.includes('--quick')
875
+ });
876
+ break;
877
+ case 'create':
878
+ if (!args[1] || args[1].startsWith('-')) error('Project name required');
879
+ const template = await promptTemplateSelection();
880
+ await createProject(args[1], { template });
881
+ break;
882
+ case 'dev':
883
+ await dev();
884
+ break;
885
+ case 'dev:frontend':
886
+ await devFrontend();
887
+ break;
888
+ case 'dev:backend':
889
+ await runBindgen([], { skipIfNoInput: true, source: 'dev:backend' });
890
+ devBackend();
891
+ break;
892
+ case 'build':
893
+ await runBindgen([], { skipIfNoInput: true, source: 'build' });
894
+ await build(true);
895
+ break;
896
+ case 'build:frontend':
897
+ buildFrontend();
898
+ break;
899
+ case 'build:backend':
900
+ await runBindgen([], { skipIfNoInput: true, source: 'build:backend' });
901
+ buildBackend(null, false);
902
+ break;
903
+ case 'build:all':
904
+ await runBindgen([], { skipIfNoInput: true, source: 'build:all' });
905
+ buildAll();
906
+ break;
907
+ case 'build:windows':
908
+ await runBindgen([], { skipIfNoInput: true, source: 'build:windows' });
909
+ buildPlatform('win32');
910
+ break;
911
+ case 'build:macos':
912
+ await runBindgen([], { skipIfNoInput: true, source: 'build:macos' });
913
+ buildPlatform('darwin');
914
+ break;
915
+ case 'build:linux':
916
+ await runBindgen([], { skipIfNoInput: true, source: 'build:linux' });
917
+ buildPlatform('linux');
918
+ break;
919
+ case 'build:android':
920
+ await runBindgen([], { skipIfNoInput: true, source: 'build:android' });
921
+ buildPlatform('android');
922
+ break;
923
+ case 'build:ios':
924
+ await runBindgen([], { skipIfNoInput: true, source: 'build:ios' });
925
+ buildPlatform('ios');
926
+ break;
927
+ case 'run':
928
+ run();
929
+ break;
930
+ case 'clean':
931
+ await clean();
932
+ break;
933
+ case 'bind':
934
+ case 'bindgen':
935
+ await runBindgen();
936
+ break;
937
+ case 'icons':
938
+ await generateIcons(args[1]);
939
+ break;
940
+ case 'embed':
941
+ await embedResources(args[1]);
942
+ break;
943
+ case 'help':
944
+ case '-h':
945
+ case '--help':
946
+ console.log(USAGE);
947
+ break;
948
+ case '-v':
949
+ case '--version':
950
+ console.log('plusui-cli v0.2.0');
951
+ break;
952
+ default:
953
+ console.log(USAGE);
954
+ }
955
+ }
956
+
957
+ main().catch(e => error(e.message));