@shellui/cli 0.0.1 → 0.0.5

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.
@@ -0,0 +1,180 @@
1
+ import { createServer } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import pc from 'picocolors';
6
+ import {
7
+ loadConfig,
8
+ getCoreSrcPath,
9
+ createResolveAlias,
10
+ createPostCSSConfig,
11
+ createViteDefine,
12
+ resolvePackagePath,
13
+ } from '../utils/index.js';
14
+ import { serviceWorkerDevPlugin } from '../utils/service-worker-plugin.js';
15
+
16
+ let currentServer = null;
17
+ let configWatcher = null;
18
+ let restartTimeout = null;
19
+ let isFirstStart = true;
20
+
21
+ /**
22
+ * Start the Vite server with current configuration
23
+ * @param {string} root - Root directory
24
+ * @param {string} cwd - Current working directory
25
+ * @param {boolean} shouldOpen - Whether to open the browser (only on first start)
26
+ * @returns {Promise<import('vite').ViteDevServer>}
27
+ */
28
+ async function startServer(root, cwd, shouldOpen = false) {
29
+ // Load configuration
30
+ const config = await loadConfig(root);
31
+
32
+ // Get core package paths
33
+ const corePackagePath = resolvePackagePath('@shellui/core');
34
+ const coreSrcPath = getCoreSrcPath();
35
+
36
+ // Check if static folder exists in project root
37
+ const staticPath = path.resolve(cwd, root, 'static');
38
+ const publicDir = fs.existsSync(staticPath) ? staticPath : false;
39
+
40
+ const server = await createServer({
41
+ root: coreSrcPath,
42
+ plugins: [react(), serviceWorkerDevPlugin(corePackagePath, coreSrcPath)],
43
+ define: createViteDefine(config),
44
+ resolve: {
45
+ alias: createResolveAlias(),
46
+ },
47
+ css: {
48
+ postcss: createPostCSSConfig(),
49
+ },
50
+ publicDir: publicDir || false,
51
+ // Disable source maps in dev mode to avoid errors from missing source map files
52
+ // Source maps are enabled in build mode for production debugging
53
+ esbuild: {
54
+ sourcemap: false,
55
+ },
56
+ server: {
57
+ port: config.port || 3000,
58
+ open: shouldOpen,
59
+ fs: {
60
+ // Allow serving files from core package, SDK package, and user's project
61
+ allow: [corePackagePath, cwd],
62
+ },
63
+ },
64
+ });
65
+
66
+ await server.listen();
67
+ return server;
68
+ }
69
+
70
+ /**
71
+ * Restart the server when config changes
72
+ * @param {string} root - Root directory
73
+ * @param {string} cwd - Current working directory
74
+ */
75
+ async function restartServer(root, cwd) {
76
+ if (restartTimeout) {
77
+ clearTimeout(restartTimeout);
78
+ }
79
+
80
+ restartTimeout = setTimeout(async () => {
81
+ try {
82
+ console.log(pc.yellow('\nšŸ”„ Config file changed, restarting server...\n'));
83
+
84
+ // Close existing server
85
+ if (currentServer) {
86
+ await currentServer.close();
87
+ }
88
+
89
+ // Start new server with updated config (don't open browser on restart)
90
+ currentServer = await startServer(root, cwd, false);
91
+ currentServer.printUrls();
92
+ } catch (e) {
93
+ console.error(pc.red(`Error restarting server: ${e.message}`));
94
+ }
95
+ }, 300); // Debounce: wait 300ms before restarting
96
+ }
97
+
98
+ /**
99
+ * Watch config file for changes
100
+ * @param {string} root - Root directory
101
+ * @param {string} cwd - Current working directory
102
+ */
103
+ function watchConfig(root, cwd) {
104
+ const configDir = path.resolve(cwd, root);
105
+ const tsConfigPath = path.join(configDir, 'shellui.config.ts');
106
+ const jsonConfigPath = path.join(configDir, 'shellui.config.json');
107
+
108
+ // Determine which config file exists (prefer TypeScript)
109
+ let configPath = null;
110
+ if (fs.existsSync(tsConfigPath)) {
111
+ configPath = tsConfigPath;
112
+ } else if (fs.existsSync(jsonConfigPath)) {
113
+ configPath = jsonConfigPath;
114
+ }
115
+
116
+ // Only watch if config file exists
117
+ if (!configPath) {
118
+ console.log(
119
+ pc.yellow(`No shellui.config.ts or shellui.config.json found, config watching disabled.`),
120
+ );
121
+ return;
122
+ }
123
+
124
+ // Close existing watcher if any
125
+ if (configWatcher) {
126
+ configWatcher.close();
127
+ }
128
+
129
+ configWatcher = fs.watch(configPath, { persistent: true }, async (eventType) => {
130
+ if (eventType === 'change') {
131
+ await restartServer(root, cwd);
132
+ }
133
+ });
134
+
135
+ console.log(pc.green(`šŸ‘€ Watching config file: ${configPath}`));
136
+ }
137
+
138
+ /**
139
+ * Start command - Starts the ShellUI development server
140
+ * @param {string} root - Root directory (default: '.')
141
+ */
142
+ export async function startCommand(root = '.') {
143
+ const cwd = process.cwd();
144
+
145
+ console.log(pc.blue(`Starting ShellUI...`));
146
+
147
+ try {
148
+ // Start initial server (open browser only on first start)
149
+ currentServer = await startServer(root, cwd, isFirstStart);
150
+ isFirstStart = false;
151
+ currentServer.printUrls();
152
+
153
+ // Watch config file for changes
154
+ watchConfig(root, cwd);
155
+
156
+ // Handle graceful shutdown
157
+ process.on('SIGTERM', async () => {
158
+ if (configWatcher) {
159
+ configWatcher.close();
160
+ }
161
+ if (currentServer) {
162
+ await currentServer.close();
163
+ }
164
+ process.exit(0);
165
+ });
166
+
167
+ process.on('SIGINT', async () => {
168
+ if (configWatcher) {
169
+ configWatcher.close();
170
+ }
171
+ if (currentServer) {
172
+ await currentServer.close();
173
+ }
174
+ process.exit(0);
175
+ });
176
+ } catch (e) {
177
+ console.error(pc.red(`Error starting server: ${e.message}`));
178
+ process.exit(1);
179
+ }
180
+ }
@@ -0,0 +1,211 @@
1
+ import { test, describe, beforeEach, afterEach, expect } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+ import { loadJsonConfig, loadTypeScriptConfig } from '../config-loaders.js';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ // Create a temporary test directory
12
+ const testDir = path.join(__dirname, 'test-fixtures-loaders');
13
+ const originalConsoleLog = console.log;
14
+ const originalConsoleError = console.error;
15
+
16
+ describe('Config Loaders', () => {
17
+ beforeEach(() => {
18
+ // Create test directory
19
+ if (!fs.existsSync(testDir)) {
20
+ fs.mkdirSync(testDir, { recursive: true });
21
+ }
22
+ // Suppress console output during tests
23
+ console.log = () => {};
24
+ console.error = () => {};
25
+ });
26
+
27
+ afterEach(() => {
28
+ // Restore console first
29
+ console.log = originalConsoleLog;
30
+ console.error = originalConsoleError;
31
+
32
+ // Clean up test directory
33
+ if (fs.existsSync(testDir)) {
34
+ const files = fs.readdirSync(testDir);
35
+ for (const file of files) {
36
+ const filePath = path.join(testDir, file);
37
+ try {
38
+ const stat = fs.statSync(filePath);
39
+ if (stat.isDirectory()) {
40
+ fs.rmSync(filePath, { recursive: true, force: true });
41
+ } else {
42
+ fs.unlinkSync(filePath);
43
+ }
44
+ } catch (e) {
45
+ // Ignore cleanup errors
46
+ }
47
+ }
48
+ try {
49
+ fs.rmdirSync(testDir);
50
+ } catch (e) {
51
+ // Ignore cleanup errors
52
+ }
53
+ }
54
+ });
55
+
56
+ describe('loadJsonConfig', () => {
57
+ test('should load valid JSON config file', () => {
58
+ const configPath = path.join(testDir, 'test-config.json');
59
+ const expectedConfig = {
60
+ name: 'test-app',
61
+ version: '1.0.0',
62
+ routes: ['/home', '/about'],
63
+ };
64
+
65
+ fs.writeFileSync(configPath, JSON.stringify(expectedConfig, null, 2));
66
+
67
+ const config = loadJsonConfig(configPath);
68
+
69
+ expect(config).toStrictEqual(expectedConfig);
70
+ });
71
+
72
+ test('should throw error for invalid JSON', () => {
73
+ const configPath = path.join(testDir, 'invalid-config.json');
74
+ fs.writeFileSync(configPath, '{ invalid json }');
75
+
76
+ expect(() => {
77
+ loadJsonConfig(configPath);
78
+ }).toThrow(/JSON/);
79
+ });
80
+
81
+ test('should throw error for missing file', () => {
82
+ const configPath = path.join(testDir, 'non-existent.json');
83
+
84
+ expect(() => {
85
+ loadJsonConfig(configPath);
86
+ }).toThrow(/ENOENT/);
87
+ });
88
+
89
+ test('should handle empty JSON object', () => {
90
+ const configPath = path.join(testDir, 'empty-config.json');
91
+ fs.writeFileSync(configPath, '{}');
92
+
93
+ const config = loadJsonConfig(configPath);
94
+
95
+ expect(config).toStrictEqual({});
96
+ });
97
+
98
+ test('should handle complex nested JSON', () => {
99
+ const configPath = path.join(testDir, 'complex-config.json');
100
+ const expectedConfig = {
101
+ app: {
102
+ name: 'test',
103
+ settings: {
104
+ theme: 'dark',
105
+ language: 'en',
106
+ },
107
+ },
108
+ features: ['feature1', 'feature2'],
109
+ };
110
+
111
+ fs.writeFileSync(configPath, JSON.stringify(expectedConfig, null, 2));
112
+
113
+ const config = loadJsonConfig(configPath);
114
+
115
+ expect(config).toStrictEqual(expectedConfig);
116
+ });
117
+ });
118
+
119
+ describe('loadTypeScriptConfig', () => {
120
+ test('should load valid TypeScript config file with default export', async () => {
121
+ const configPath = path.join(testDir, 'test-config.ts');
122
+ const expectedConfig = {
123
+ name: 'test-app',
124
+ version: '2.0.0',
125
+ };
126
+
127
+ const tsConfigContent = `export default ${JSON.stringify(expectedConfig)};`;
128
+ fs.writeFileSync(configPath, tsConfigContent);
129
+
130
+ const config = await loadTypeScriptConfig(configPath, testDir);
131
+
132
+ expect(config).toStrictEqual(expectedConfig);
133
+ });
134
+
135
+ test('should load TypeScript config with named export', async () => {
136
+ const configPath = path.join(testDir, 'test-config-named.ts');
137
+ const expectedConfig = {
138
+ name: 'test-app',
139
+ routes: ['/home'],
140
+ };
141
+
142
+ const tsConfigContent = `export const config = ${JSON.stringify(expectedConfig)};`;
143
+ fs.writeFileSync(configPath, tsConfigContent);
144
+
145
+ const loadedConfig = await loadTypeScriptConfig(configPath, testDir);
146
+
147
+ expect(loadedConfig).toStrictEqual(expectedConfig);
148
+ });
149
+
150
+ test('should handle TypeScript config with both default and named exports (prefers default)', async () => {
151
+ const configPath = path.join(testDir, 'test-config-both.ts');
152
+ const defaultConfig = { name: 'default' };
153
+ const namedConfig = { name: 'named' };
154
+
155
+ const tsConfigContent = `export default ${JSON.stringify(defaultConfig)};\nexport const config = ${JSON.stringify(namedConfig)};`;
156
+ fs.writeFileSync(configPath, tsConfigContent);
157
+
158
+ const loadedConfig = await loadTypeScriptConfig(configPath, testDir);
159
+
160
+ expect(loadedConfig).toStrictEqual(defaultConfig);
161
+ });
162
+
163
+ test('should throw error for invalid TypeScript file', async () => {
164
+ const configPath = path.join(testDir, 'invalid-config.ts');
165
+ fs.writeFileSync(configPath, 'export const invalid = syntax error;');
166
+
167
+ await expect(loadTypeScriptConfig(configPath, testDir)).rejects.toThrow(
168
+ /Failed to load TypeScript config/,
169
+ );
170
+ });
171
+
172
+ test('should throw error for missing TypeScript file', async () => {
173
+ const configPath = path.join(testDir, 'non-existent.ts');
174
+
175
+ await expect(loadTypeScriptConfig(configPath, testDir)).rejects.toThrow(
176
+ /Failed to load TypeScript config/,
177
+ );
178
+ });
179
+
180
+ test('should clean up temporary loader script', async () => {
181
+ const configPath = path.join(testDir, 'cleanup-test.ts');
182
+ const expectedConfig = { name: 'cleanup-test' };
183
+
184
+ const tsConfigContent = `export default ${JSON.stringify(expectedConfig)};`;
185
+ fs.writeFileSync(configPath, tsConfigContent);
186
+
187
+ await loadTypeScriptConfig(configPath, testDir);
188
+
189
+ const tempScriptPath = path.join(testDir, '.shellui-config-loader.mjs');
190
+ expect(fs.existsSync(tempScriptPath)).toBe(false);
191
+ });
192
+
193
+ test('should handle complex TypeScript config with functions and types', async () => {
194
+ const configPath = path.join(testDir, 'complex-config.ts');
195
+ const expectedConfig = {
196
+ name: 'complex-app',
197
+ settings: {
198
+ port: 3000,
199
+ host: 'localhost',
200
+ },
201
+ };
202
+
203
+ const tsConfigContent = `interface Config { name: string; settings: { port: number; host: string; }; }\nconst config: Config = ${JSON.stringify(expectedConfig)};\nexport default config;`;
204
+ fs.writeFileSync(configPath, tsConfigContent);
205
+
206
+ const loadedConfig = await loadTypeScriptConfig(configPath, testDir);
207
+
208
+ expect(loadedConfig).toStrictEqual(expectedConfig);
209
+ });
210
+ });
211
+ });
@@ -0,0 +1,146 @@
1
+ import { test, describe, beforeEach, afterEach, expect } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+ import { loadConfig } from '../config.js';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ // Create a temporary test directory
12
+ const testDir = path.join(__dirname, 'test-fixtures-config');
13
+ const originalConsoleLog = console.log;
14
+ const originalConsoleError = console.error;
15
+ const originalCwd = process.cwd();
16
+
17
+ describe('loadConfig', () => {
18
+ beforeEach(() => {
19
+ // Create test directory
20
+ if (!fs.existsSync(testDir)) {
21
+ fs.mkdirSync(testDir, { recursive: true });
22
+ }
23
+ // Change to test directory
24
+ process.chdir(testDir);
25
+ // Suppress console output during tests
26
+ console.log = () => {};
27
+ console.error = () => {};
28
+ });
29
+
30
+ afterEach(() => {
31
+ // Restore console first
32
+ console.log = originalConsoleLog;
33
+ console.error = originalConsoleError;
34
+ // Restore original working directory
35
+ process.chdir(originalCwd);
36
+ // Clean up test directory
37
+ if (fs.existsSync(testDir)) {
38
+ const files = fs.readdirSync(testDir);
39
+ for (const file of files) {
40
+ const filePath = path.join(testDir, file);
41
+ try {
42
+ const stat = fs.statSync(filePath);
43
+ if (stat.isDirectory()) {
44
+ fs.rmSync(filePath, { recursive: true, force: true });
45
+ } else {
46
+ fs.unlinkSync(filePath);
47
+ }
48
+ } catch (e) {
49
+ // Ignore cleanup errors
50
+ }
51
+ }
52
+ try {
53
+ fs.rmdirSync(testDir);
54
+ } catch (e) {
55
+ // Ignore cleanup errors
56
+ }
57
+ }
58
+ });
59
+
60
+ test('should prefer TypeScript config over JSON config', async () => {
61
+ const tsConfig = { name: 'ts-config', version: '1.0.0' };
62
+ const jsonConfig = { name: 'json-config', version: '2.0.0' };
63
+
64
+ fs.writeFileSync('shellui.config.ts', `export default ${JSON.stringify(tsConfig)};`);
65
+ fs.writeFileSync('shellui.config.json', JSON.stringify(jsonConfig, null, 2));
66
+
67
+ const config = await loadConfig('.');
68
+
69
+ expect(config).toStrictEqual(tsConfig);
70
+ });
71
+
72
+ test('should load JSON config when TypeScript config does not exist', async () => {
73
+ const jsonConfig = { name: 'json-only', version: '1.0.0' };
74
+
75
+ fs.writeFileSync('shellui.config.json', JSON.stringify(jsonConfig, null, 2));
76
+
77
+ const config = await loadConfig('.');
78
+
79
+ expect(config).toStrictEqual(jsonConfig);
80
+ });
81
+
82
+ test('should return empty object when no config files exist', async () => {
83
+ const config = await loadConfig('.');
84
+
85
+ expect(config).toStrictEqual({});
86
+ });
87
+
88
+ test('should handle custom root directory', async () => {
89
+ const subDir = path.join(testDir, 'subdir');
90
+ fs.mkdirSync(subDir, { recursive: true });
91
+
92
+ const jsonConfig = { name: 'subdir-config', version: '1.0.0' };
93
+ fs.writeFileSync(path.join(subDir, 'shellui.config.json'), JSON.stringify(jsonConfig, null, 2));
94
+
95
+ const config = await loadConfig(subDir);
96
+
97
+ expect(config).toStrictEqual(jsonConfig);
98
+ });
99
+
100
+ test('should handle TypeScript config in custom root', async () => {
101
+ const subDir = path.join(testDir, 'subdir');
102
+ fs.mkdirSync(subDir, { recursive: true });
103
+
104
+ const tsConfig = { name: 'subdir-ts-config', version: '1.0.0' };
105
+ fs.writeFileSync(
106
+ path.join(subDir, 'shellui.config.ts'),
107
+ `export default ${JSON.stringify(tsConfig)};`,
108
+ );
109
+
110
+ const config = await loadConfig(subDir);
111
+
112
+ expect(config).toStrictEqual(tsConfig);
113
+ });
114
+
115
+ test('should handle errors gracefully and return empty object', async () => {
116
+ const invalidJsonPath = path.join(testDir, 'shellui.config.json');
117
+ fs.writeFileSync(invalidJsonPath, '{ invalid json }');
118
+
119
+ const config = await loadConfig('.');
120
+
121
+ // Should return empty object on error
122
+ expect(config).toStrictEqual({});
123
+ });
124
+
125
+ test('should handle invalid TypeScript config gracefully', async () => {
126
+ const invalidTsPath = path.join(testDir, 'shellui.config.ts');
127
+ fs.writeFileSync(invalidTsPath, 'export const invalid = syntax error;');
128
+
129
+ const config = await loadConfig('.');
130
+
131
+ // Should return empty object on error
132
+ expect(config).toStrictEqual({});
133
+ });
134
+
135
+ test('should work with relative path', async () => {
136
+ const jsonConfig = { name: 'relative-config', version: '1.0.0' };
137
+ fs.writeFileSync('shellui.config.json', JSON.stringify(jsonConfig, null, 2));
138
+
139
+ // Change to parent directory
140
+ process.chdir(path.dirname(testDir));
141
+
142
+ const config = await loadConfig(path.basename(testDir));
143
+
144
+ expect(config).toStrictEqual(jsonConfig);
145
+ });
146
+ });
@@ -0,0 +1,142 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { pathToFileURL } from 'url';
4
+ import { spawn } from 'child_process';
5
+ import pc from 'picocolors';
6
+
7
+ /**
8
+ * Load TypeScript configuration file using tsx
9
+ * @param {string} configPath - Path to the TypeScript config file
10
+ * @param {string} configDir - Directory containing the config file
11
+ * @returns {Promise<Object>} Configuration object
12
+ */
13
+ export async function loadTypeScriptConfig(configPath, configDir) {
14
+ // Load TypeScript config using tsx CLI via child process
15
+ // This avoids module cycle issues with dynamic registration
16
+ // Use file URL for the import to ensure proper resolution
17
+ const configFileUrl = pathToFileURL(configPath).href;
18
+ const loaderScript = `
19
+ import * as configModule from ${JSON.stringify(configFileUrl)};
20
+ // Extract the actual config, handling both default export and named exports
21
+ let result = configModule.default || configModule.config || configModule;
22
+ // If result itself has a default property, extract it
23
+ if (result && typeof result === 'object' && result.default && Object.keys(result).length === 1) {
24
+ result = result.default;
25
+ }
26
+ console.log(JSON.stringify(result));
27
+ `;
28
+
29
+ // Write temporary script to load the config
30
+ const tempScriptPath = path.join(configDir, '.shellui-config-loader.mjs');
31
+ fs.writeFileSync(tempScriptPath, loaderScript);
32
+
33
+ try {
34
+ // Find tsx - try to resolve it from node_modules
35
+ const { createRequire } = await import('module');
36
+ const require = createRequire(import.meta.url);
37
+
38
+ let tsxPath;
39
+ try {
40
+ // Try to resolve tsx package
41
+ const tsxPackagePath = require.resolve('tsx/package.json');
42
+ const tsxPackageDir = path.dirname(tsxPackagePath);
43
+ tsxPath = path.join(tsxPackageDir, 'dist/cli.mjs');
44
+
45
+ if (!fs.existsSync(tsxPath)) {
46
+ throw new Error('tsx CLI not found');
47
+ }
48
+ } catch (resolveError) {
49
+ // Fallback to npx if tsx not found
50
+ tsxPath = null;
51
+ }
52
+
53
+ // Execute tsx to run the loader script
54
+ const result = await new Promise((resolve, reject) => {
55
+ const useNpx = !tsxPath;
56
+ const command = useNpx ? 'npx' : 'node';
57
+ const args = useNpx ? ['-y', 'tsx', tempScriptPath] : [tsxPath, tempScriptPath];
58
+
59
+ // Pass environment variables to child process, including build detection vars
60
+ const childEnv = {
61
+ ...process.env,
62
+ // Ensure build detection variables are passed to the child process
63
+ NODE_ENV: process.env.NODE_ENV || 'development',
64
+ SHELLUI_BUILD: process.env.SHELLUI_BUILD || 'false',
65
+ };
66
+
67
+ const child = spawn(command, args, {
68
+ cwd: configDir,
69
+ stdio: ['ignore', 'pipe', 'pipe'],
70
+ env: childEnv,
71
+ });
72
+
73
+ let stdout = '';
74
+ let stderr = '';
75
+
76
+ child.stdout.on('data', (data) => {
77
+ stdout += data.toString();
78
+ });
79
+
80
+ child.stderr.on('data', (data) => {
81
+ stderr += data.toString();
82
+ });
83
+
84
+ child.on('close', (code) => {
85
+ if (code !== 0) {
86
+ reject(
87
+ new Error(
88
+ `Failed to load TypeScript config: ${stderr || `Process exited with code ${code}`}`,
89
+ ),
90
+ );
91
+ } else {
92
+ try {
93
+ const parsed = JSON.parse(stdout.trim());
94
+ // If the parsed result has a 'default' key, extract it
95
+ const config = parsed.default || parsed.config || parsed;
96
+ resolve(config);
97
+ } catch (parseError) {
98
+ reject(
99
+ new Error(
100
+ `Failed to parse config output: ${parseError.message}\nOutput: ${stdout}\nStderr: ${stderr}`,
101
+ ),
102
+ );
103
+ }
104
+ }
105
+ });
106
+
107
+ child.on('error', (error) => {
108
+ reject(new Error(`Failed to execute tsx: ${error.message}`));
109
+ });
110
+ });
111
+
112
+ // Clean up temp script
113
+ if (fs.existsSync(tempScriptPath)) {
114
+ fs.unlinkSync(tempScriptPath);
115
+ }
116
+
117
+ console.log(pc.green(`Loaded TypeScript config from ${configPath}`));
118
+ return result;
119
+ } catch (execError) {
120
+ // Clean up temp script on error
121
+ if (fs.existsSync(tempScriptPath)) {
122
+ try {
123
+ fs.unlinkSync(tempScriptPath);
124
+ } catch (e) {
125
+ // Ignore cleanup errors
126
+ }
127
+ }
128
+ throw execError;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Load JSON configuration file
134
+ * @param {string} configPath - Path to the JSON config file
135
+ * @returns {Object} Configuration object
136
+ */
137
+ export function loadJsonConfig(configPath) {
138
+ const configFile = fs.readFileSync(configPath, 'utf-8');
139
+ const config = JSON.parse(configFile);
140
+ console.log(pc.green(`Loaded JSON config from ${configPath}`));
141
+ return config;
142
+ }