plusui-native 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -0
- package/package.json +36 -0
- package/src/assets/icon-generator.js +251 -0
- package/src/assets/resource-embedder.js +351 -0
- package/src/doctor/detectors/cmake.js +84 -0
- package/src/doctor/detectors/compiler.js +145 -0
- package/src/doctor/detectors/just.js +45 -0
- package/src/doctor/detectors/nodejs.js +57 -0
- package/src/doctor/detectors/webview2.js +66 -0
- package/src/doctor/index.js +184 -0
- package/src/doctor/installers/linux.js +121 -0
- package/src/doctor/installers/macos.js +123 -0
- package/src/doctor/installers/windows.js +117 -0
- package/src/doctor/reporter.js +219 -0
- package/src/index.js +904 -0
- package/templates/base/Justfile +115 -0
- package/templates/base/README.md.template +168 -0
- package/templates/base/assets/README.md +88 -0
- package/templates/base/assets/icon.png +0 -0
- package/templates/manager.js +217 -0
- package/templates/react/.vscode/c_cpp_properties.json +24 -0
- package/templates/react/CMakeLists.txt.template +151 -0
- package/templates/react/frontend/index.html +12 -0
- package/templates/react/frontend/package.json.template +24 -0
- package/templates/react/frontend/src/App.tsx +134 -0
- package/templates/react/frontend/src/main.tsx +10 -0
- package/templates/react/frontend/src/styles/app.css +140 -0
- package/templates/react/frontend/tsconfig.json +21 -0
- package/templates/react/frontend/tsconfig.node.json +11 -0
- package/templates/react/frontend/vite.config.ts +14 -0
- package/templates/react/main.cpp.template +201 -0
- package/templates/react/package.json.template +23 -0
- package/templates/solid/.vscode/c_cpp_properties.json +24 -0
- package/templates/solid/CMakeLists.txt.template +151 -0
- package/templates/solid/frontend/index.html +12 -0
- package/templates/solid/frontend/package.json.template +21 -0
- package/templates/solid/frontend/src/App.tsx +133 -0
- package/templates/solid/frontend/src/main.tsx +5 -0
- package/templates/solid/frontend/src/styles/app.css +140 -0
- package/templates/solid/frontend/tsconfig.json +22 -0
- package/templates/solid/frontend/tsconfig.node.json +11 -0
- package/templates/solid/frontend/vite.config.ts +14 -0
- package/templates/solid/main.cpp.template +192 -0
- package/templates/solid/package.json.template +23 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cross-Platform Resource Embedder for PlusUI
|
|
5
|
+
*
|
|
6
|
+
* Embeds frontend assets and icons into the binary for single-executable distribution:
|
|
7
|
+
* - Windows: Generates .rc file with RCDATA resources
|
|
8
|
+
* - macOS: Copies resources to app bundle
|
|
9
|
+
* - Linux: Generates C++ header with binary data
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFile, writeFile, readdir, mkdir, stat, copyFile } from 'fs/promises';
|
|
13
|
+
import { existsSync, statSync } from 'fs';
|
|
14
|
+
import { join, relative, basename, extname, dirname } from 'path';
|
|
15
|
+
import { createHash } from 'crypto';
|
|
16
|
+
|
|
17
|
+
class ResourceEmbedder {
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.verbose = options.verbose || false;
|
|
20
|
+
this.compression = options.compression !== false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
log(msg) {
|
|
24
|
+
if (this.verbose) console.log(msg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get all files recursively from a directory
|
|
29
|
+
*/
|
|
30
|
+
async getAllFiles(dirPath, arrayOfFiles = []) {
|
|
31
|
+
const files = await readdir(dirPath);
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const filePath = join(dirPath, file);
|
|
35
|
+
const s = await stat(filePath);
|
|
36
|
+
|
|
37
|
+
if (s.isDirectory()) {
|
|
38
|
+
arrayOfFiles = await this.getAllFiles(filePath, arrayOfFiles);
|
|
39
|
+
} else {
|
|
40
|
+
arrayOfFiles.push(filePath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return arrayOfFiles;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate Windows .rc resource file
|
|
49
|
+
*/
|
|
50
|
+
async generateWindowsRC(assetsDir, outputPath) {
|
|
51
|
+
console.log(' 🪟 Generating Windows resource file (.rc)...');
|
|
52
|
+
|
|
53
|
+
if (!existsSync(assetsDir)) {
|
|
54
|
+
console.log(' ⚠️ Assets directory not found, skipping');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const files = await this.getAllFiles(assetsDir);
|
|
59
|
+
let rcContent = `// Auto-generated by PlusUI - DO NOT EDIT
|
|
60
|
+
// Embedded resources for single-executable distribution
|
|
61
|
+
|
|
62
|
+
#include <windows.h>
|
|
63
|
+
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
let count = 0;
|
|
67
|
+
const resources = [];
|
|
68
|
+
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const relPath = relative(assetsDir, file).replace(/\\/g, '/');
|
|
71
|
+
const resourceId = `IDR_${relPath.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`;
|
|
72
|
+
const winPath = file.replace(/\//g, '\\\\');
|
|
73
|
+
|
|
74
|
+
// Store for manifest
|
|
75
|
+
resources.push({ id: resourceId, path: relPath, file });
|
|
76
|
+
|
|
77
|
+
rcContent += `${resourceId} RCDATA "${winPath}"\n`;
|
|
78
|
+
count++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add icon if exists
|
|
82
|
+
const iconPaths = [
|
|
83
|
+
join(assetsDir, 'icons', 'windows', 'app.ico'),
|
|
84
|
+
join(assetsDir, 'icon.ico'),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const iconPath of iconPaths) {
|
|
88
|
+
if (existsSync(iconPath)) {
|
|
89
|
+
rcContent += `\n// Application icon\nIDI_APP_ICON ICON "${iconPath.replace(/\//g, '\\\\')}"\n`;
|
|
90
|
+
console.log(` ✓ Added app icon: ${basename(iconPath)}`);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
96
|
+
await writeFile(outputPath, rcContent);
|
|
97
|
+
|
|
98
|
+
// Generate resource manifest (C++ header)
|
|
99
|
+
await this.generateResourceManifest(resources, outputPath.replace('.rc', '_manifest.hpp'));
|
|
100
|
+
|
|
101
|
+
console.log(` ✓ Created ${basename(outputPath)} (${count} resources)`);
|
|
102
|
+
return outputPath;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate C++ header with resource manifest
|
|
107
|
+
*/
|
|
108
|
+
async generateResourceManifest(resources, outputPath) {
|
|
109
|
+
let content = `// Auto-generated by PlusUI - DO NOT EDIT
|
|
110
|
+
#pragma once
|
|
111
|
+
|
|
112
|
+
#include <string>
|
|
113
|
+
#include <unordered_map>
|
|
114
|
+
|
|
115
|
+
namespace plusui {
|
|
116
|
+
namespace resources {
|
|
117
|
+
|
|
118
|
+
// Resource ID to path mapping
|
|
119
|
+
static const std::unordered_map<std::string, int> RESOURCE_MAP = {
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
let idCounter = 100;
|
|
123
|
+
for (const res of resources) {
|
|
124
|
+
content += ` {"${res.path}", ${idCounter}},\n`;
|
|
125
|
+
idCounter++;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
content += `};
|
|
129
|
+
|
|
130
|
+
// Get resource data from embedded resources
|
|
131
|
+
#ifdef _WIN32
|
|
132
|
+
#include <windows.h>
|
|
133
|
+
inline std::string getResource(const std::string& path) {
|
|
134
|
+
auto it = RESOURCE_MAP.find(path);
|
|
135
|
+
if (it == RESOURCE_MAP.end()) return "";
|
|
136
|
+
|
|
137
|
+
HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(it->second), RT_RCDATA);
|
|
138
|
+
if (!hRes) return "";
|
|
139
|
+
|
|
140
|
+
HGLOBAL hData = LoadResource(NULL, hRes);
|
|
141
|
+
if (!hData) return "";
|
|
142
|
+
|
|
143
|
+
DWORD size = SizeofResource(NULL, hRes);
|
|
144
|
+
const char* data = static_cast<const char*>(LockResource(hData));
|
|
145
|
+
|
|
146
|
+
return std::string(data, size);
|
|
147
|
+
}
|
|
148
|
+
#else
|
|
149
|
+
inline std::string getResource(const std::string& path) {
|
|
150
|
+
// Use embedded data from binary (see embedded_resources.hpp)
|
|
151
|
+
extern const std::unordered_map<std::string, std::pair<const unsigned char*, size_t>>& EMBEDDED_DATA;
|
|
152
|
+
auto it = EMBEDDED_DATA.find(path);
|
|
153
|
+
if (it == EMBEDDED_DATA.end()) return "";
|
|
154
|
+
return std::string(reinterpret_cast<const char*>(it->second.first), it->second.second);
|
|
155
|
+
}
|
|
156
|
+
#endif
|
|
157
|
+
|
|
158
|
+
} // namespace resources
|
|
159
|
+
} // namespace plusui
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
await writeFile(outputPath, content);
|
|
163
|
+
console.log(` ✓ Created resource manifest`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Generate Linux/macOS binary embedding header
|
|
168
|
+
*/
|
|
169
|
+
async generateBinaryHeader(assetsDir, outputPath) {
|
|
170
|
+
console.log(' 🐧 Generating binary embedded resources...');
|
|
171
|
+
|
|
172
|
+
if (!existsSync(assetsDir)) {
|
|
173
|
+
console.log(' ⚠️ Assets directory not found, skipping');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const files = await this.getAllFiles(assetsDir);
|
|
178
|
+
|
|
179
|
+
let content = `// Auto-generated by PlusUI - DO NOT EDIT
|
|
180
|
+
#pragma once
|
|
181
|
+
|
|
182
|
+
#include <unordered_map>
|
|
183
|
+
#include <utility>
|
|
184
|
+
#include <string>
|
|
185
|
+
|
|
186
|
+
namespace plusui {
|
|
187
|
+
namespace resources {
|
|
188
|
+
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
let count = 0;
|
|
192
|
+
const dataMap = [];
|
|
193
|
+
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
const relPath = relative(assetsDir, file).replace(/\\/g, '/');
|
|
196
|
+
const varName = `ASSET_${relPath.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`;
|
|
197
|
+
const data = await readFile(file);
|
|
198
|
+
|
|
199
|
+
content += `// ${relPath}\nstatic const unsigned char ${varName}[] = {\n`;
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < data.length; i++) {
|
|
202
|
+
if (i % 16 === 0) content += ' ';
|
|
203
|
+
content += `0x${data[i].toString(16).padStart(2, '0')}`;
|
|
204
|
+
if (i < data.length - 1) content += ',';
|
|
205
|
+
if ((i + 1) % 16 === 0 || i === data.length - 1) content += '\n';
|
|
206
|
+
else content += ' ';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
content += `};\nstatic const size_t ${varName}_LEN = ${data.length};\n\n`;
|
|
210
|
+
|
|
211
|
+
dataMap.push({ path: relPath, varName, size: data.length });
|
|
212
|
+
count++;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Generate lookup map
|
|
216
|
+
content += `// Resource lookup map\nstatic const std::unordered_map<std::string, std::pair<const unsigned char*, size_t>> EMBEDDED_DATA = {\n`;
|
|
217
|
+
|
|
218
|
+
for (const item of dataMap) {
|
|
219
|
+
content += ` {"${item.path}", {${item.varName}, ${item.varName}_LEN}},\n`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
content += `};\n\n`;
|
|
223
|
+
|
|
224
|
+
// Add convenience function
|
|
225
|
+
content += `inline std::string getResource(const std::string& path) {
|
|
226
|
+
auto it = EMBEDDED_DATA.find(path);
|
|
227
|
+
if (it == EMBEDDED_DATA.end()) return "";
|
|
228
|
+
return std::string(reinterpret_cast<const char*>(it->second.first), it->second.second);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
} // namespace resources
|
|
232
|
+
} // namespace plusui
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
236
|
+
await writeFile(outputPath, content);
|
|
237
|
+
|
|
238
|
+
console.log(` ✓ Created ${basename(outputPath)} (${count} resources)`);
|
|
239
|
+
return outputPath;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Generate macOS app bundle resources
|
|
244
|
+
*/
|
|
245
|
+
async generateMacOSBundle(assetsDir, bundlePath) {
|
|
246
|
+
console.log(' 🍎 Copying resources to macOS bundle...');
|
|
247
|
+
|
|
248
|
+
if (!existsSync(assetsDir)) {
|
|
249
|
+
console.log(' ⚠️ Assets directory not found, skipping');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const resourcesDir = join(bundlePath, 'Contents', 'Resources');
|
|
254
|
+
await mkdir(resourcesDir, { recursive: true });
|
|
255
|
+
|
|
256
|
+
const files = await this.getAllFiles(assetsDir);
|
|
257
|
+
let count = 0;
|
|
258
|
+
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
const relPath = relative(assetsDir, file);
|
|
261
|
+
const dest = join(resourcesDir, relPath);
|
|
262
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
263
|
+
await copyFile(file, dest);
|
|
264
|
+
count++;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Copy icon if exists
|
|
268
|
+
const iconPath = join(assetsDir, 'icons', 'macos', 'app.icns');
|
|
269
|
+
if (existsSync(iconPath)) {
|
|
270
|
+
await copyFile(iconPath, join(resourcesDir, 'app.icns'));
|
|
271
|
+
console.log(` ✓ Added app icon`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log(` ✓ Copied ${count} resources to bundle`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Main embed function - chooses platform
|
|
279
|
+
*/
|
|
280
|
+
async embed(assetsDir, outputDir, platform = process.platform) {
|
|
281
|
+
console.log(`\n📦 Embedding resources for ${platform}...\n`);
|
|
282
|
+
|
|
283
|
+
await mkdir(outputDir, { recursive: true });
|
|
284
|
+
|
|
285
|
+
switch (platform) {
|
|
286
|
+
case 'win32':
|
|
287
|
+
return await this.generateWindowsRC(
|
|
288
|
+
assetsDir,
|
|
289
|
+
join(outputDir, 'resources.rc')
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
case 'darwin':
|
|
293
|
+
// macOS uses both binary embedding and bundle resources
|
|
294
|
+
await this.generateBinaryHeader(
|
|
295
|
+
assetsDir,
|
|
296
|
+
join(outputDir, 'embedded_resources.hpp')
|
|
297
|
+
);
|
|
298
|
+
// Bundle copying happens at app bundle creation time
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case 'linux':
|
|
302
|
+
return await this.generateBinaryHeader(
|
|
303
|
+
assetsDir,
|
|
304
|
+
join(outputDir, 'embedded_resources.hpp')
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
default:
|
|
308
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log('\n✨ Resource embedding complete!\n');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Embed for all platforms
|
|
316
|
+
*/
|
|
317
|
+
async embedAll(assetsDir, outputDir) {
|
|
318
|
+
console.log('\n📦 Embedding resources for all platforms...\n');
|
|
319
|
+
|
|
320
|
+
await this.embed(assetsDir, join(outputDir, 'windows'), 'win32');
|
|
321
|
+
await this.embed(assetsDir, join(outputDir, 'linux'), 'linux');
|
|
322
|
+
await this.embed(assetsDir, join(outputDir, 'darwin'), 'darwin');
|
|
323
|
+
|
|
324
|
+
console.log('\n✨ All platforms complete!\n');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// CLI
|
|
329
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
330
|
+
const args = process.argv.slice(2);
|
|
331
|
+
const command = args[0];
|
|
332
|
+
const assetsDir = args[1] || join(process.cwd(), 'frontend', 'dist');
|
|
333
|
+
const outputDir = args[2] || join(process.cwd(), 'generated', 'resources');
|
|
334
|
+
|
|
335
|
+
const embedder = new ResourceEmbedder({ verbose: true });
|
|
336
|
+
|
|
337
|
+
if (command === 'all') {
|
|
338
|
+
embedder.embedAll(assetsDir, outputDir).catch(err => {
|
|
339
|
+
console.error('❌ Error:', err.message);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
});
|
|
342
|
+
} else {
|
|
343
|
+
const platform = command || process.platform;
|
|
344
|
+
embedder.embed(assetsDir, outputDir, platform).catch(err => {
|
|
345
|
+
console.error('❌ Error:', err.message);
|
|
346
|
+
process.exit(1);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export { ResourceEmbedder };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
|
|
5
|
+
const REQUIRED_VERSION = '3.16.0';
|
|
6
|
+
|
|
7
|
+
function getPlatformCMakePaths() {
|
|
8
|
+
const platform = process.platform;
|
|
9
|
+
const paths = ['cmake']; // Always check PATH first
|
|
10
|
+
|
|
11
|
+
if (platform === 'win32') {
|
|
12
|
+
paths.push(
|
|
13
|
+
'C:\\Program Files\\CMake\\bin\\cmake.exe',
|
|
14
|
+
'C:\\Program Files (x86)\\CMake\\bin\\cmake.exe'
|
|
15
|
+
);
|
|
16
|
+
} else if (platform === 'darwin') {
|
|
17
|
+
paths.push(
|
|
18
|
+
'/usr/local/bin/cmake',
|
|
19
|
+
'/opt/homebrew/bin/cmake',
|
|
20
|
+
'/Applications/CMake.app/Contents/bin/cmake'
|
|
21
|
+
);
|
|
22
|
+
} else {
|
|
23
|
+
paths.push(
|
|
24
|
+
'/usr/bin/cmake',
|
|
25
|
+
'/usr/local/bin/cmake'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return paths;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseVersion(output) {
|
|
33
|
+
// Parse version from "cmake version 3.28.1"
|
|
34
|
+
const match = output.match(/cmake version (\d+\.\d+\.\d+)/i);
|
|
35
|
+
return match ? match[1] : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function tryCommand(command) {
|
|
39
|
+
try {
|
|
40
|
+
const output = execSync(command, {
|
|
41
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
timeout: 5000
|
|
44
|
+
});
|
|
45
|
+
return { success: true, output };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return { success: false, error };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function detectCMake() {
|
|
52
|
+
const paths = getPlatformCMakePaths();
|
|
53
|
+
|
|
54
|
+
for (const cmakePath of paths) {
|
|
55
|
+
// Check if path exists for file paths
|
|
56
|
+
if (cmakePath !== 'cmake' && !existsSync(cmakePath)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = await tryCommand(`"${cmakePath}" --version`);
|
|
61
|
+
if (result.success) {
|
|
62
|
+
const version = parseVersion(result.output);
|
|
63
|
+
if (!version) continue;
|
|
64
|
+
|
|
65
|
+
const versionValid = semver.gte(version, REQUIRED_VERSION);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
found: true,
|
|
69
|
+
path: cmakePath,
|
|
70
|
+
version,
|
|
71
|
+
valid: versionValid,
|
|
72
|
+
requiredVersion: REQUIRED_VERSION
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
found: false,
|
|
79
|
+
path: null,
|
|
80
|
+
version: null,
|
|
81
|
+
valid: false,
|
|
82
|
+
requiredVersion: REQUIRED_VERSION
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
async function tryCommand(command) {
|
|
5
|
+
try {
|
|
6
|
+
const output = execSync(command, {
|
|
7
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
timeout: 10000
|
|
10
|
+
}).trim();
|
|
11
|
+
return { success: true, output };
|
|
12
|
+
} catch (error) {
|
|
13
|
+
return { success: false, error };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function detectMSVC() {
|
|
18
|
+
// Use vswhere.exe to detect Visual Studio
|
|
19
|
+
const vswherePath = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe';
|
|
20
|
+
|
|
21
|
+
if (!existsSync(vswherePath)) {
|
|
22
|
+
// Fallback: check common VS paths manually
|
|
23
|
+
const vsPaths = [
|
|
24
|
+
'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC',
|
|
25
|
+
'C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional\\VC\\Tools\\MSVC',
|
|
26
|
+
'C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Tools\\MSVC',
|
|
27
|
+
'C:\\Program Files\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\MSVC',
|
|
28
|
+
'C:\\Program Files\\Microsoft Visual Studio\\2019\\Professional\\VC\\Tools\\MSVC',
|
|
29
|
+
'C:\\Program Files\\Microsoft Visual Studio\\2019\\Enterprise\\VC\\Tools\\MSVC'
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const vsPath of vsPaths) {
|
|
33
|
+
if (existsSync(vsPath)) {
|
|
34
|
+
const version = vsPath.includes('2022') ? 'Visual Studio 2022' : 'Visual Studio 2019';
|
|
35
|
+
return {
|
|
36
|
+
found: true,
|
|
37
|
+
name: version,
|
|
38
|
+
path: vsPath,
|
|
39
|
+
valid: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { found: false, name: 'Visual Studio', valid: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Use vswhere to find VS with C++ workload
|
|
48
|
+
const command = `"${vswherePath}" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`;
|
|
49
|
+
const result = await tryCommand(command);
|
|
50
|
+
|
|
51
|
+
if (!result.success || !result.output) {
|
|
52
|
+
return { found: false, name: 'Visual Studio', valid: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const installPath = result.output.trim();
|
|
56
|
+
|
|
57
|
+
// Get version
|
|
58
|
+
const versionCommand = `"${vswherePath}" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property displayName`;
|
|
59
|
+
const versionResult = await tryCommand(versionCommand);
|
|
60
|
+
|
|
61
|
+
const name = versionResult.success ? versionResult.output.trim() : 'Visual Studio';
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
found: true,
|
|
65
|
+
name,
|
|
66
|
+
path: installPath,
|
|
67
|
+
valid: true
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function detectXcode() {
|
|
72
|
+
// Check for Xcode Command Line Tools
|
|
73
|
+
const result = await tryCommand('xcode-select -p');
|
|
74
|
+
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
return {
|
|
77
|
+
found: false,
|
|
78
|
+
name: 'Xcode Command Line Tools',
|
|
79
|
+
valid: false
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Verify clang++ is available
|
|
84
|
+
const clangResult = await tryCommand('clang++ --version');
|
|
85
|
+
|
|
86
|
+
if (!clangResult.success) {
|
|
87
|
+
return {
|
|
88
|
+
found: true,
|
|
89
|
+
name: 'Xcode Command Line Tools',
|
|
90
|
+
path: result.output.trim(),
|
|
91
|
+
valid: false,
|
|
92
|
+
error: 'clang++ not available'
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Parse clang version
|
|
97
|
+
const versionMatch = clangResult.output.match(/clang version (\d+\.\d+\.\d+)/i) ||
|
|
98
|
+
clangResult.output.match(/Apple clang version (\d+\.\d+\.\d+)/i);
|
|
99
|
+
const version = versionMatch ? versionMatch[1] : 'unknown';
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
found: true,
|
|
103
|
+
name: 'Xcode Command Line Tools',
|
|
104
|
+
path: result.output.trim(),
|
|
105
|
+
version,
|
|
106
|
+
valid: true
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function detectGCC() {
|
|
111
|
+
// Check for g++
|
|
112
|
+
const result = await tryCommand('g++ --version');
|
|
113
|
+
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
return {
|
|
116
|
+
found: false,
|
|
117
|
+
name: 'GCC/G++',
|
|
118
|
+
valid: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Parse GCC version
|
|
123
|
+
const versionMatch = result.output.match(/g\+\+ .* (\d+\.\d+\.\d+)/i);
|
|
124
|
+
const version = versionMatch ? versionMatch[1] : 'unknown';
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
found: true,
|
|
128
|
+
name: 'GCC/G++',
|
|
129
|
+
version,
|
|
130
|
+
valid: true
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function detectCompiler() {
|
|
135
|
+
const platform = process.platform;
|
|
136
|
+
|
|
137
|
+
switch (platform) {
|
|
138
|
+
case 'win32':
|
|
139
|
+
return await detectMSVC();
|
|
140
|
+
case 'darwin':
|
|
141
|
+
return await detectXcode();
|
|
142
|
+
default:
|
|
143
|
+
return await detectGCC();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
|
|
4
|
+
const REQUIRED_JUST_VERSION = '1.0.0';
|
|
5
|
+
|
|
6
|
+
async function tryCommand(command) {
|
|
7
|
+
try {
|
|
8
|
+
const output = execSync(command, {
|
|
9
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
timeout: 5000
|
|
12
|
+
}).trim();
|
|
13
|
+
return { success: true, output };
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return { success: false, error };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function detectJust() {
|
|
20
|
+
const result = await tryCommand('just --version');
|
|
21
|
+
|
|
22
|
+
if (!result.success) {
|
|
23
|
+
return {
|
|
24
|
+
found: false,
|
|
25
|
+
version: null,
|
|
26
|
+
valid: false,
|
|
27
|
+
requiredVersion: REQUIRED_JUST_VERSION
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Parse version (output format: "just 1.34.0")
|
|
32
|
+
const versionMatch = result.output.match(/just (\d+\.\d+\.\d+)/);
|
|
33
|
+
const version = versionMatch ? versionMatch[1] : null;
|
|
34
|
+
|
|
35
|
+
// If we can't parse the version but the command succeeded, assume it's valid enough
|
|
36
|
+
// but ideally we check version. Just is usually pretty standard.
|
|
37
|
+
const valid = version ? semver.gte(version, REQUIRED_JUST_VERSION) : true;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
found: true,
|
|
41
|
+
version: version || 'unknown',
|
|
42
|
+
valid,
|
|
43
|
+
requiredVersion: REQUIRED_JUST_VERSION
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
|
|
4
|
+
const REQUIRED_NODE_VERSION = '18.0.0';
|
|
5
|
+
const REQUIRED_NPM_VERSION = '9.0.0';
|
|
6
|
+
|
|
7
|
+
async function tryCommand(command) {
|
|
8
|
+
try {
|
|
9
|
+
const output = execSync(command, {
|
|
10
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
timeout: 5000
|
|
13
|
+
}).trim();
|
|
14
|
+
return { success: true, output };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return { success: false, error };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function detectNodeJS() {
|
|
21
|
+
const nodeResult = await tryCommand('node --version');
|
|
22
|
+
const npmResult = await tryCommand('npm --version');
|
|
23
|
+
|
|
24
|
+
if (!nodeResult.success) {
|
|
25
|
+
return {
|
|
26
|
+
found: false,
|
|
27
|
+
version: null,
|
|
28
|
+
valid: false,
|
|
29
|
+
requiredVersion: REQUIRED_NODE_VERSION,
|
|
30
|
+
npm: {
|
|
31
|
+
found: false,
|
|
32
|
+
version: null,
|
|
33
|
+
valid: false
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Parse versions (remove 'v' prefix from node version)
|
|
39
|
+
const nodeVersion = nodeResult.output.replace(/^v/, '');
|
|
40
|
+
const npmVersion = npmResult.success ? npmResult.output : null;
|
|
41
|
+
|
|
42
|
+
const nodeValid = semver.gte(nodeVersion, REQUIRED_NODE_VERSION);
|
|
43
|
+
const npmValid = npmVersion ? semver.gte(npmVersion, REQUIRED_NPM_VERSION) : false;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
found: true,
|
|
47
|
+
version: nodeVersion,
|
|
48
|
+
valid: nodeValid,
|
|
49
|
+
requiredVersion: REQUIRED_NODE_VERSION,
|
|
50
|
+
npm: {
|
|
51
|
+
found: npmResult.success,
|
|
52
|
+
version: npmVersion,
|
|
53
|
+
valid: npmValid,
|
|
54
|
+
requiredVersion: REQUIRED_NPM_VERSION
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|