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/package.json +5 -6
- package/src/index.js +957 -904
- package/templates/base/README.md.template +13 -13
- package/templates/manager.js +173 -217
- package/templates/react/CMakeLists.txt.template +25 -15
- package/templates/react/frontend/package.json.template +3 -4
- package/templates/react/frontend/src/App.tsx +3 -3
- package/templates/react/frontend/src/plusui.ts +117 -0
- package/templates/react/main.cpp.template +3 -3
- package/templates/react/package.json.template +2 -1
- package/templates/solid/CMakeLists.txt.template +25 -15
- package/templates/solid/frontend/package.json.template +3 -4
- package/templates/solid/frontend/src/App.tsx +4 -5
- package/templates/solid/frontend/src/plusui.ts +117 -0
- package/templates/solid/main.cpp.template +3 -3
- package/templates/solid/package.json.template +2 -1
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
if (!existsSync(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
log(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
log(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
case '
|
|
871
|
-
await
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
break;
|
|
877
|
-
case '
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
case '
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
case '
|
|
897
|
-
|
|
898
|
-
break;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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));
|