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.
Files changed (44) hide show
  1. package/README.md +162 -0
  2. package/package.json +36 -0
  3. package/src/assets/icon-generator.js +251 -0
  4. package/src/assets/resource-embedder.js +351 -0
  5. package/src/doctor/detectors/cmake.js +84 -0
  6. package/src/doctor/detectors/compiler.js +145 -0
  7. package/src/doctor/detectors/just.js +45 -0
  8. package/src/doctor/detectors/nodejs.js +57 -0
  9. package/src/doctor/detectors/webview2.js +66 -0
  10. package/src/doctor/index.js +184 -0
  11. package/src/doctor/installers/linux.js +121 -0
  12. package/src/doctor/installers/macos.js +123 -0
  13. package/src/doctor/installers/windows.js +117 -0
  14. package/src/doctor/reporter.js +219 -0
  15. package/src/index.js +904 -0
  16. package/templates/base/Justfile +115 -0
  17. package/templates/base/README.md.template +168 -0
  18. package/templates/base/assets/README.md +88 -0
  19. package/templates/base/assets/icon.png +0 -0
  20. package/templates/manager.js +217 -0
  21. package/templates/react/.vscode/c_cpp_properties.json +24 -0
  22. package/templates/react/CMakeLists.txt.template +151 -0
  23. package/templates/react/frontend/index.html +12 -0
  24. package/templates/react/frontend/package.json.template +24 -0
  25. package/templates/react/frontend/src/App.tsx +134 -0
  26. package/templates/react/frontend/src/main.tsx +10 -0
  27. package/templates/react/frontend/src/styles/app.css +140 -0
  28. package/templates/react/frontend/tsconfig.json +21 -0
  29. package/templates/react/frontend/tsconfig.node.json +11 -0
  30. package/templates/react/frontend/vite.config.ts +14 -0
  31. package/templates/react/main.cpp.template +201 -0
  32. package/templates/react/package.json.template +23 -0
  33. package/templates/solid/.vscode/c_cpp_properties.json +24 -0
  34. package/templates/solid/CMakeLists.txt.template +151 -0
  35. package/templates/solid/frontend/index.html +12 -0
  36. package/templates/solid/frontend/package.json.template +21 -0
  37. package/templates/solid/frontend/src/App.tsx +133 -0
  38. package/templates/solid/frontend/src/main.tsx +5 -0
  39. package/templates/solid/frontend/src/styles/app.css +140 -0
  40. package/templates/solid/frontend/tsconfig.json +22 -0
  41. package/templates/solid/frontend/tsconfig.node.json +11 -0
  42. package/templates/solid/frontend/vite.config.ts +14 -0
  43. package/templates/solid/main.cpp.template +192 -0
  44. 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
+ }