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