lsh-framework 0.5.4

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 (90) hide show
  1. package/.env.example +51 -0
  2. package/README.md +399 -0
  3. package/dist/app.js +33 -0
  4. package/dist/cicd/analytics.js +261 -0
  5. package/dist/cicd/auth.js +269 -0
  6. package/dist/cicd/cache-manager.js +172 -0
  7. package/dist/cicd/data-retention.js +305 -0
  8. package/dist/cicd/performance-monitor.js +224 -0
  9. package/dist/cicd/webhook-receiver.js +634 -0
  10. package/dist/cli.js +500 -0
  11. package/dist/commands/api.js +343 -0
  12. package/dist/commands/self.js +318 -0
  13. package/dist/commands/theme.js +257 -0
  14. package/dist/commands/zsh-import.js +240 -0
  15. package/dist/components/App.js +1 -0
  16. package/dist/components/Divider.js +29 -0
  17. package/dist/components/REPL.js +43 -0
  18. package/dist/components/Terminal.js +232 -0
  19. package/dist/components/UserInput.js +30 -0
  20. package/dist/daemon/api-server.js +315 -0
  21. package/dist/daemon/job-registry.js +554 -0
  22. package/dist/daemon/lshd.js +822 -0
  23. package/dist/daemon/monitoring-api.js +220 -0
  24. package/dist/examples/supabase-integration.js +106 -0
  25. package/dist/lib/api-error-handler.js +183 -0
  26. package/dist/lib/associative-arrays.js +285 -0
  27. package/dist/lib/base-api-server.js +290 -0
  28. package/dist/lib/base-command-registrar.js +286 -0
  29. package/dist/lib/base-job-manager.js +293 -0
  30. package/dist/lib/brace-expansion.js +160 -0
  31. package/dist/lib/builtin-commands.js +439 -0
  32. package/dist/lib/cloud-config-manager.js +347 -0
  33. package/dist/lib/command-validator.js +190 -0
  34. package/dist/lib/completion-system.js +344 -0
  35. package/dist/lib/cron-job-manager.js +364 -0
  36. package/dist/lib/daemon-client-helper.js +141 -0
  37. package/dist/lib/daemon-client.js +501 -0
  38. package/dist/lib/database-persistence.js +638 -0
  39. package/dist/lib/database-schema.js +259 -0
  40. package/dist/lib/enhanced-history-system.js +246 -0
  41. package/dist/lib/env-validator.js +265 -0
  42. package/dist/lib/executors/builtin-executor.js +52 -0
  43. package/dist/lib/extended-globbing.js +411 -0
  44. package/dist/lib/extended-parameter-expansion.js +227 -0
  45. package/dist/lib/floating-point-arithmetic.js +256 -0
  46. package/dist/lib/history-system.js +245 -0
  47. package/dist/lib/interactive-shell.js +460 -0
  48. package/dist/lib/job-builtins.js +580 -0
  49. package/dist/lib/job-manager.js +386 -0
  50. package/dist/lib/job-storage-database.js +156 -0
  51. package/dist/lib/job-storage-memory.js +73 -0
  52. package/dist/lib/logger.js +274 -0
  53. package/dist/lib/lshrc-init.js +177 -0
  54. package/dist/lib/pathname-expansion.js +216 -0
  55. package/dist/lib/prompt-system.js +328 -0
  56. package/dist/lib/script-runner.js +226 -0
  57. package/dist/lib/secrets-manager.js +193 -0
  58. package/dist/lib/shell-executor.js +2504 -0
  59. package/dist/lib/shell-parser.js +958 -0
  60. package/dist/lib/shell-types.js +6 -0
  61. package/dist/lib/shell.lib.js +40 -0
  62. package/dist/lib/supabase-client.js +58 -0
  63. package/dist/lib/theme-manager.js +476 -0
  64. package/dist/lib/variable-expansion.js +385 -0
  65. package/dist/lib/zsh-compatibility.js +658 -0
  66. package/dist/lib/zsh-import-manager.js +699 -0
  67. package/dist/lib/zsh-options.js +328 -0
  68. package/dist/pipeline/job-tracker.js +491 -0
  69. package/dist/pipeline/mcli-bridge.js +302 -0
  70. package/dist/pipeline/pipeline-service.js +1116 -0
  71. package/dist/pipeline/workflow-engine.js +867 -0
  72. package/dist/services/api/api.js +58 -0
  73. package/dist/services/api/auth.js +35 -0
  74. package/dist/services/api/config.js +7 -0
  75. package/dist/services/api/file.js +22 -0
  76. package/dist/services/cron/cron-registrar.js +235 -0
  77. package/dist/services/cron/cron.js +9 -0
  78. package/dist/services/daemon/daemon-registrar.js +565 -0
  79. package/dist/services/daemon/daemon.js +9 -0
  80. package/dist/services/lib/lib.js +86 -0
  81. package/dist/services/log-file-extractor.js +170 -0
  82. package/dist/services/secrets/secrets.js +94 -0
  83. package/dist/services/shell/shell.js +28 -0
  84. package/dist/services/supabase/supabase-registrar.js +367 -0
  85. package/dist/services/supabase/supabase.js +9 -0
  86. package/dist/services/zapier.js +16 -0
  87. package/dist/simple-api-server.js +148 -0
  88. package/dist/store/store.js +31 -0
  89. package/dist/util/lib.util.js +11 -0
  90. package/package.json +144 -0
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Advanced Prompt System Implementation
3
+ * Provides ZSH-compatible prompt customization
4
+ */
5
+ import * as os from 'os';
6
+ import * as path from 'path';
7
+ export class PromptSystem {
8
+ themes = new Map();
9
+ currentTheme = 'default';
10
+ context;
11
+ constructor() {
12
+ this.context = this.createDefaultContext();
13
+ this.setupDefaultThemes();
14
+ }
15
+ /**
16
+ * Expand prompt string with ZSH-style sequences
17
+ */
18
+ expandPrompt(prompt, context) {
19
+ const ctx = { ...this.context, ...context };
20
+ let result = prompt;
21
+ // Basic sequences
22
+ result = result.replace(/%n/g, ctx.user); // Username
23
+ result = result.replace(/%m/g, ctx.host); // Hostname
24
+ result = result.replace(/%M/g, ctx.host.split('.')[0]); // Hostname without domain
25
+ result = result.replace(/%~/g, this.formatPath(ctx.cwd, ctx.home)); // Current directory
26
+ result = result.replace(/%d/g, ctx.cwd); // Full path
27
+ result = result.replace(/%c/g, path.basename(ctx.cwd)); // Basename of current directory
28
+ result = result.replace(/%C/g, path.basename(ctx.cwd)); // Same as %c
29
+ result = result.replace(/%h/g, ctx.jobCount.toString()); // Number of jobs
30
+ result = result.replace(/%j/g, ctx.jobCount.toString()); // Number of jobs
31
+ result = result.replace(/%L/g, ctx.exitCode.toString()); // Exit code of last command
32
+ result = result.replace(/%\?/g, ctx.exitCode.toString()); // Exit code of last command
33
+ result = result.replace(/%#/g, ctx.user === 'root' ? '#' : '$'); // Prompt character
34
+ result = result.replace(/%\$/g, ctx.user === 'root' ? '#' : '$'); // Prompt character
35
+ result = result.replace(/%%/g, '%'); // Literal %
36
+ // Time sequences
37
+ result = result.replace(/%T/g, this.formatTime(ctx.time, 'HH:MM')); // 24-hour time
38
+ result = result.replace(/%t/g, this.formatTime(ctx.time, 'h:mm')); // 12-hour time
39
+ result = result.replace(/%D/g, this.formatTime(ctx.time, 'yyyy-MM-dd')); // Date
40
+ result = result.replace(/%w/g, this.formatTime(ctx.time, 'EEE')); // Day of week
41
+ result = result.replace(/%W/g, this.formatTime(ctx.time, 'MM/dd')); // Date
42
+ // Git sequences
43
+ if (ctx.git) {
44
+ result = result.replace(/%b/g, ctx.git.branch); // Git branch
45
+ result = result.replace(/%B/g, ctx.git.branch); // Git branch
46
+ result = result.replace(/%r/g, this.formatGitStatus(ctx.git.status)); // Git status
47
+ }
48
+ // Conditional sequences
49
+ result = this.expandConditionalSequences(result, ctx);
50
+ // Color sequences
51
+ result = this.expandColorSequences(result);
52
+ return result;
53
+ }
54
+ /**
55
+ * Set current theme
56
+ */
57
+ setTheme(themeName) {
58
+ if (this.themes.has(themeName)) {
59
+ this.currentTheme = themeName;
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Get current theme
66
+ */
67
+ getCurrentTheme() {
68
+ return this.currentTheme;
69
+ }
70
+ /**
71
+ * Get current prompt
72
+ */
73
+ getCurrentPrompt(context) {
74
+ const theme = this.themes.get(this.currentTheme);
75
+ if (!theme)
76
+ return '$ ';
77
+ return this.expandPrompt(theme.prompt, context);
78
+ }
79
+ /**
80
+ * Get current right prompt
81
+ */
82
+ getCurrentRPrompt(context) {
83
+ const theme = this.themes.get(this.currentTheme);
84
+ if (!theme || !theme.rprompt)
85
+ return '';
86
+ return this.expandPrompt(theme.rprompt, context);
87
+ }
88
+ /**
89
+ * Add a custom theme
90
+ */
91
+ addTheme(theme) {
92
+ this.themes.set(theme.name, theme);
93
+ }
94
+ /**
95
+ * Get all available themes
96
+ */
97
+ getAvailableThemes() {
98
+ return Array.from(this.themes.keys());
99
+ }
100
+ /**
101
+ * Get theme information
102
+ */
103
+ getThemeInfo(themeName) {
104
+ return this.themes.get(themeName);
105
+ }
106
+ /**
107
+ * Update prompt context
108
+ */
109
+ updateContext(updates) {
110
+ this.context = { ...this.context, ...updates };
111
+ }
112
+ /**
113
+ * Create default context
114
+ */
115
+ createDefaultContext() {
116
+ return {
117
+ user: os.userInfo().username,
118
+ host: os.hostname(),
119
+ cwd: process.cwd(),
120
+ home: os.homedir(),
121
+ exitCode: 0,
122
+ jobCount: 0,
123
+ time: new Date(),
124
+ };
125
+ }
126
+ /**
127
+ * Setup default themes
128
+ */
129
+ setupDefaultThemes() {
130
+ // Default theme
131
+ this.addTheme({
132
+ name: 'default',
133
+ description: 'Simple default prompt',
134
+ prompt: '%n@%m:%~$ ',
135
+ });
136
+ // Minimal theme
137
+ this.addTheme({
138
+ name: 'minimal',
139
+ description: 'Minimal prompt',
140
+ prompt: '$ ',
141
+ });
142
+ // Detailed theme
143
+ this.addTheme({
144
+ name: 'detailed',
145
+ description: 'Detailed prompt with time and exit code',
146
+ prompt: '[%T] %n@%m:%~ %?$ ',
147
+ rprompt: '%h jobs',
148
+ });
149
+ // Git theme
150
+ this.addTheme({
151
+ name: 'git',
152
+ description: 'Git-aware prompt',
153
+ prompt: '%n@%m:%~ %b$ ',
154
+ rprompt: '%r',
155
+ });
156
+ // Powerline-style theme
157
+ this.addTheme({
158
+ name: 'powerline',
159
+ description: 'Powerline-style prompt',
160
+ prompt: '%n@%m %~ %# ',
161
+ colors: {
162
+ user: '32', // Green
163
+ host: '34', // Blue
164
+ path: '35', // Magenta
165
+ git: '33', // Yellow
166
+ error: '31', // Red
167
+ success: '32', // Green
168
+ },
169
+ });
170
+ // Oh My Zsh-style theme
171
+ this.addTheme({
172
+ name: 'ohmyzsh',
173
+ description: 'Oh My Zsh-style prompt',
174
+ prompt: '%n@%m %~ %# ',
175
+ rprompt: '%T',
176
+ });
177
+ // Fish-style theme
178
+ this.addTheme({
179
+ name: 'fish',
180
+ description: 'Fish shell-style prompt',
181
+ prompt: '%n@%m %~ > ',
182
+ });
183
+ // Bash-style theme
184
+ this.addTheme({
185
+ name: 'bash',
186
+ description: 'Bash-style prompt',
187
+ prompt: '\\u@\\h:\\w\\$ ',
188
+ });
189
+ }
190
+ /**
191
+ * Format path with tilde expansion
192
+ */
193
+ formatPath(cwd, home) {
194
+ if (cwd.startsWith(home)) {
195
+ return '~' + cwd.substring(home.length);
196
+ }
197
+ return cwd;
198
+ }
199
+ /**
200
+ * Format time according to pattern
201
+ */
202
+ formatTime(time, pattern) {
203
+ const hours = time.getHours();
204
+ const minutes = time.getMinutes();
205
+ const seconds = time.getSeconds();
206
+ const day = time.getDate();
207
+ const month = time.getMonth() + 1;
208
+ const year = time.getFullYear();
209
+ const dayOfWeek = time.getDay();
210
+ const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
211
+ return pattern
212
+ .replace(/HH/g, hours.toString().padStart(2, '0'))
213
+ .replace(/H/g, hours.toString())
214
+ .replace(/h/g, (hours % 12 || 12).toString())
215
+ .replace(/mm/g, minutes.toString().padStart(2, '0'))
216
+ .replace(/m/g, minutes.toString())
217
+ .replace(/ss/g, seconds.toString().padStart(2, '0'))
218
+ .replace(/s/g, seconds.toString())
219
+ .replace(/dd/g, day.toString().padStart(2, '0'))
220
+ .replace(/d/g, day.toString())
221
+ .replace(/MM/g, month.toString().padStart(2, '0'))
222
+ .replace(/M/g, month.toString())
223
+ .replace(/yyyy/g, year.toString())
224
+ .replace(/yy/g, year.toString().slice(-2))
225
+ .replace(/EEE/g, dayNames[dayOfWeek]);
226
+ }
227
+ /**
228
+ * Format git status
229
+ */
230
+ formatGitStatus(status) {
231
+ switch (status) {
232
+ case 'clean': return '✓';
233
+ case 'dirty': return '●';
234
+ case 'ahead': return '↑';
235
+ case 'behind': return '↓';
236
+ case 'diverged': return '↕';
237
+ default: return '';
238
+ }
239
+ }
240
+ /**
241
+ * Expand conditional sequences
242
+ */
243
+ expandConditionalSequences(prompt, context) {
244
+ // Handle %(condition.true-text.false-text)
245
+ const conditionalRegex = /%\(([^)]+)\)/g;
246
+ return prompt.replace(conditionalRegex, (match, condition) => {
247
+ const parts = condition.split('.');
248
+ if (parts.length < 2)
249
+ return match;
250
+ const [cond, trueText, falseText = ''] = parts;
251
+ const isTrue = this.evaluateCondition(cond, context);
252
+ return isTrue ? trueText : falseText;
253
+ });
254
+ }
255
+ /**
256
+ * Evaluate condition for conditional sequences
257
+ */
258
+ evaluateCondition(condition, context) {
259
+ switch (condition) {
260
+ case '?': return context.exitCode === 0;
261
+ case '!': return context.exitCode !== 0;
262
+ case 'j': return context.jobCount > 0;
263
+ case 'n': return context.user !== 'root';
264
+ case 'root': return context.user === 'root';
265
+ case 'git': return context.git !== undefined;
266
+ default: return false;
267
+ }
268
+ }
269
+ /**
270
+ * Expand color sequences
271
+ */
272
+ expandColorSequences(prompt) {
273
+ // Handle %F{color} and %f
274
+ const colorRegex = /%F\{([^}]+)\}/g;
275
+ prompt = prompt.replace(colorRegex, (match, color) => {
276
+ return this.getColorCode(color);
277
+ });
278
+ prompt = prompt.replace(/%f/g, '\x1b[0m'); // Reset color
279
+ // Handle %K{color} and %k (background colors)
280
+ const bgColorRegex = /%K\{([^}]+)\}/g;
281
+ prompt = prompt.replace(bgColorRegex, (match, color) => {
282
+ return this.getBackgroundColorCode(color);
283
+ });
284
+ prompt = prompt.replace(/%k/g, '\x1b[49m'); // Reset background
285
+ // Handle %B and %b (bold)
286
+ prompt = prompt.replace(/%B/g, '\x1b[1m');
287
+ prompt = prompt.replace(/%b/g, '\x1b[22m');
288
+ // Handle %U and %u (underline)
289
+ prompt = prompt.replace(/%U/g, '\x1b[4m');
290
+ prompt = prompt.replace(/%u/g, '\x1b[24m');
291
+ return prompt;
292
+ }
293
+ /**
294
+ * Get color code for color name
295
+ */
296
+ getColorCode(color) {
297
+ const colors = {
298
+ black: '\x1b[30m',
299
+ red: '\x1b[31m',
300
+ green: '\x1b[32m',
301
+ yellow: '\x1b[33m',
302
+ blue: '\x1b[34m',
303
+ magenta: '\x1b[35m',
304
+ cyan: '\x1b[36m',
305
+ white: '\x1b[37m',
306
+ default: '\x1b[39m',
307
+ };
308
+ return colors[color.toLowerCase()] || '\x1b[39m';
309
+ }
310
+ /**
311
+ * Get background color code for color name
312
+ */
313
+ getBackgroundColorCode(color) {
314
+ const colors = {
315
+ black: '\x1b[40m',
316
+ red: '\x1b[41m',
317
+ green: '\x1b[42m',
318
+ yellow: '\x1b[43m',
319
+ blue: '\x1b[44m',
320
+ magenta: '\x1b[45m',
321
+ cyan: '\x1b[46m',
322
+ white: '\x1b[47m',
323
+ default: '\x1b[49m',
324
+ };
325
+ return colors[color.toLowerCase()] || '\x1b[49m';
326
+ }
327
+ }
328
+ export default PromptSystem;
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Shell Script Runner
3
+ * Executes shell scripts with LSH
4
+ */
5
+ import { ShellExecutor } from './shell-executor.js';
6
+ import { parseShellCommand } from './shell-parser.js';
7
+ import * as fs from 'fs';
8
+ export class ScriptRunner {
9
+ executor;
10
+ constructor(options) {
11
+ this.executor = new ShellExecutor({
12
+ cwd: options?.cwd || process.cwd(),
13
+ env: {
14
+ ...Object.fromEntries(Object.entries(process.env).filter(([_, v]) => v !== undefined)),
15
+ ...options?.env
16
+ },
17
+ });
18
+ }
19
+ /**
20
+ * Execute a shell script file
21
+ */
22
+ async executeScript(scriptPath, options = {}) {
23
+ try {
24
+ // Read script file
25
+ const scriptContent = fs.readFileSync(scriptPath, 'utf8');
26
+ // Set up script context
27
+ if (options.args) {
28
+ this.executor.setPositionalParams(options.args);
29
+ }
30
+ // Parse and execute script
31
+ const ast = parseShellCommand(scriptContent);
32
+ const result = await this.executor.execute(ast);
33
+ return {
34
+ success: result.success,
35
+ exitCode: result.exitCode,
36
+ output: result.stdout,
37
+ errors: result.stderr,
38
+ };
39
+ }
40
+ catch (error) {
41
+ return {
42
+ success: false,
43
+ exitCode: 1,
44
+ output: '',
45
+ errors: `Script execution failed: ${error.message}`,
46
+ };
47
+ }
48
+ }
49
+ /**
50
+ * Execute shell commands from string
51
+ */
52
+ async executeCommands(commands, options = {}) {
53
+ try {
54
+ // Set up context
55
+ if (options.args) {
56
+ this.executor.setPositionalParams(options.args);
57
+ }
58
+ // Parse and execute commands
59
+ const ast = parseShellCommand(commands);
60
+ const result = await this.executor.execute(ast);
61
+ return {
62
+ success: result.success,
63
+ exitCode: result.exitCode,
64
+ output: result.stdout,
65
+ errors: result.stderr,
66
+ };
67
+ }
68
+ catch (error) {
69
+ return {
70
+ success: false,
71
+ exitCode: 1,
72
+ output: '',
73
+ errors: `Command execution failed: ${error.message}`,
74
+ };
75
+ }
76
+ }
77
+ /**
78
+ * Execute script with shebang detection
79
+ */
80
+ async executeWithShebang(scriptPath, options = {}) {
81
+ try {
82
+ const scriptContent = fs.readFileSync(scriptPath, 'utf8');
83
+ // Check for shebang
84
+ const lines = scriptContent.split('\n');
85
+ const firstLine = lines[0];
86
+ if (firstLine.startsWith('#!')) {
87
+ const interpreter = firstLine.substring(2).trim();
88
+ // Handle different interpreters
89
+ if (interpreter.includes('sh') || interpreter.includes('bash') || interpreter.includes('zsh')) {
90
+ // Execute as shell script
91
+ return await this.executeScript(scriptPath, options);
92
+ }
93
+ else {
94
+ // For other interpreters, delegate to system
95
+ return await this.executeSystemScript(scriptPath, options);
96
+ }
97
+ }
98
+ else {
99
+ // No shebang, execute as shell script
100
+ return await this.executeScript(scriptPath, options);
101
+ }
102
+ }
103
+ catch (error) {
104
+ return {
105
+ success: false,
106
+ exitCode: 1,
107
+ output: '',
108
+ errors: `Script execution failed: ${error.message}`,
109
+ };
110
+ }
111
+ }
112
+ /**
113
+ * Execute system script (fallback)
114
+ */
115
+ async executeSystemScript(scriptPath, options) {
116
+ const { spawn } = await import('child_process');
117
+ return new Promise((resolve) => {
118
+ const child = spawn('sh', [scriptPath], {
119
+ cwd: options.cwd || process.cwd(),
120
+ env: { ...process.env, ...options.env },
121
+ stdio: ['pipe', 'pipe', 'pipe'],
122
+ });
123
+ let stdout = '';
124
+ let stderr = '';
125
+ child.stdout?.on('data', (data) => {
126
+ stdout += data.toString();
127
+ });
128
+ child.stderr?.on('data', (data) => {
129
+ stderr += data.toString();
130
+ });
131
+ child.on('close', (code) => {
132
+ resolve({
133
+ success: code === 0,
134
+ exitCode: code || 0,
135
+ output: stdout,
136
+ errors: stderr,
137
+ });
138
+ });
139
+ child.on('error', (error) => {
140
+ resolve({
141
+ success: false,
142
+ exitCode: 1,
143
+ output: '',
144
+ errors: error.message,
145
+ });
146
+ });
147
+ });
148
+ }
149
+ /**
150
+ * Validate shell script syntax
151
+ */
152
+ validateScript(scriptPath) {
153
+ try {
154
+ const scriptContent = fs.readFileSync(scriptPath, 'utf8');
155
+ const _ast = parseShellCommand(scriptContent);
156
+ // Basic validation - if parsing succeeds, syntax is valid
157
+ return { valid: true, errors: [] };
158
+ }
159
+ catch (error) {
160
+ return { valid: false, errors: [error.message] };
161
+ }
162
+ }
163
+ /**
164
+ * Get script information
165
+ */
166
+ getScriptInfo(scriptPath) {
167
+ try {
168
+ const stats = fs.statSync(scriptPath);
169
+ const content = fs.readFileSync(scriptPath, 'utf8');
170
+ const firstLine = content.split('\n')[0];
171
+ let shebang;
172
+ let interpreter;
173
+ if (firstLine.startsWith('#!')) {
174
+ shebang = firstLine;
175
+ interpreter = firstLine.substring(2).trim();
176
+ }
177
+ return {
178
+ exists: true,
179
+ executable: stats.mode & 0o111 ? true : false,
180
+ size: stats.size,
181
+ shebang,
182
+ interpreter,
183
+ };
184
+ }
185
+ catch {
186
+ return {
187
+ exists: false,
188
+ executable: false,
189
+ size: 0,
190
+ };
191
+ }
192
+ }
193
+ /**
194
+ * Make script executable
195
+ */
196
+ makeExecutable(scriptPath) {
197
+ try {
198
+ const stats = fs.statSync(scriptPath);
199
+ fs.chmodSync(scriptPath, stats.mode | 0o111);
200
+ return true;
201
+ }
202
+ catch {
203
+ return false;
204
+ }
205
+ }
206
+ /**
207
+ * Create a simple shell script
208
+ */
209
+ createScript(scriptPath, content, makeExecutable = true) {
210
+ try {
211
+ // Add shebang if not present
212
+ if (!content.startsWith('#!')) {
213
+ content = '#!/bin/sh\n' + content;
214
+ }
215
+ fs.writeFileSync(scriptPath, content, 'utf8');
216
+ if (makeExecutable) {
217
+ this.makeExecutable(scriptPath);
218
+ }
219
+ return true;
220
+ }
221
+ catch {
222
+ return false;
223
+ }
224
+ }
225
+ }
226
+ export default ScriptRunner;