lsh-framework 1.1.0 → 1.2.1

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 (73) hide show
  1. package/README.md +70 -4
  2. package/dist/cli.js +104 -486
  3. package/dist/commands/doctor.js +427 -0
  4. package/dist/commands/init.js +371 -0
  5. package/dist/constants/api.js +94 -0
  6. package/dist/constants/commands.js +64 -0
  7. package/dist/constants/config.js +56 -0
  8. package/dist/constants/database.js +21 -0
  9. package/dist/constants/errors.js +79 -0
  10. package/dist/constants/index.js +28 -0
  11. package/dist/constants/paths.js +28 -0
  12. package/dist/constants/ui.js +73 -0
  13. package/dist/constants/validation.js +124 -0
  14. package/dist/daemon/lshd.js +11 -32
  15. package/dist/lib/daemon-client-helper.js +7 -4
  16. package/dist/lib/daemon-client.js +9 -2
  17. package/dist/lib/format-utils.js +163 -0
  18. package/dist/lib/job-manager.js +2 -1
  19. package/dist/lib/platform-utils.js +211 -0
  20. package/dist/lib/secrets-manager.js +11 -1
  21. package/dist/lib/string-utils.js +128 -0
  22. package/dist/services/daemon/daemon-registrar.js +3 -2
  23. package/dist/services/secrets/secrets.js +154 -30
  24. package/package.json +10 -74
  25. package/dist/app.js +0 -33
  26. package/dist/cicd/analytics.js +0 -261
  27. package/dist/cicd/auth.js +0 -269
  28. package/dist/cicd/cache-manager.js +0 -172
  29. package/dist/cicd/data-retention.js +0 -305
  30. package/dist/cicd/performance-monitor.js +0 -224
  31. package/dist/cicd/webhook-receiver.js +0 -640
  32. package/dist/commands/api.js +0 -346
  33. package/dist/commands/theme.js +0 -261
  34. package/dist/commands/zsh-import.js +0 -240
  35. package/dist/components/App.js +0 -1
  36. package/dist/components/Divider.js +0 -29
  37. package/dist/components/REPL.js +0 -43
  38. package/dist/components/Terminal.js +0 -232
  39. package/dist/components/UserInput.js +0 -30
  40. package/dist/daemon/api-server.js +0 -316
  41. package/dist/daemon/monitoring-api.js +0 -220
  42. package/dist/lib/api-error-handler.js +0 -185
  43. package/dist/lib/associative-arrays.js +0 -285
  44. package/dist/lib/base-api-server.js +0 -290
  45. package/dist/lib/brace-expansion.js +0 -160
  46. package/dist/lib/builtin-commands.js +0 -439
  47. package/dist/lib/executors/builtin-executor.js +0 -52
  48. package/dist/lib/extended-globbing.js +0 -411
  49. package/dist/lib/extended-parameter-expansion.js +0 -227
  50. package/dist/lib/interactive-shell.js +0 -460
  51. package/dist/lib/job-builtins.js +0 -582
  52. package/dist/lib/pathname-expansion.js +0 -216
  53. package/dist/lib/script-runner.js +0 -226
  54. package/dist/lib/shell-executor.js +0 -2504
  55. package/dist/lib/shell-parser.js +0 -958
  56. package/dist/lib/shell-types.js +0 -6
  57. package/dist/lib/shell.lib.js +0 -40
  58. package/dist/lib/theme-manager.js +0 -476
  59. package/dist/lib/variable-expansion.js +0 -385
  60. package/dist/lib/zsh-compatibility.js +0 -659
  61. package/dist/lib/zsh-import-manager.js +0 -707
  62. package/dist/lib/zsh-options.js +0 -328
  63. package/dist/pipeline/job-tracker.js +0 -491
  64. package/dist/pipeline/mcli-bridge.js +0 -309
  65. package/dist/pipeline/pipeline-service.js +0 -1119
  66. package/dist/pipeline/workflow-engine.js +0 -870
  67. package/dist/services/api/api.js +0 -58
  68. package/dist/services/api/auth.js +0 -35
  69. package/dist/services/api/config.js +0 -7
  70. package/dist/services/api/file.js +0 -22
  71. package/dist/services/shell/shell.js +0 -28
  72. package/dist/services/zapier.js +0 -16
  73. package/dist/simple-api-server.js +0 -148
@@ -1,2504 +0,0 @@
1
- /**
2
- * POSIX Shell Command Executor
3
- * Executes parsed AST nodes following POSIX semantics
4
- */
5
- import { spawn, exec } from 'child_process';
6
- import { promisify } from 'util';
7
- import * as fs from 'fs';
8
- import * as path from 'path';
9
- import { parseShellCommand } from './shell-parser.js';
10
- import { VariableExpander } from './variable-expansion.js';
11
- import { PathnameExpander } from './pathname-expansion.js';
12
- import { BraceExpander } from './brace-expansion.js';
13
- import CompletionSystem from './completion-system.js';
14
- import JobManager from './job-manager.js';
15
- import JobBuiltins from './job-builtins.js';
16
- import HistorySystem from './history-system.js';
17
- import AssociativeArrayManager from './associative-arrays.js';
18
- import ExtendedParameterExpander from './extended-parameter-expansion.js';
19
- import ExtendedGlobber from './extended-globbing.js';
20
- import ZshOptionsManager from './zsh-options.js';
21
- import PromptSystem from './prompt-system.js';
22
- import FloatingPointArithmetic from './floating-point-arithmetic.js';
23
- const execAsync = promisify(exec);
24
- export class ShellExecutor {
25
- context;
26
- expander;
27
- pathExpander;
28
- braceExpander;
29
- jobManager;
30
- jobBuiltins;
31
- extendedExpander;
32
- extendedGlobber;
33
- constructor(initialContext) {
34
- this.context = {
35
- env: Object.fromEntries(Object.entries(process.env).filter(([_, v]) => v !== undefined)),
36
- cwd: process.cwd(),
37
- variables: {},
38
- lastExitCode: 0,
39
- lastReturnCode: 0,
40
- positionalParams: [],
41
- ifs: ' \t\n',
42
- functions: {},
43
- jobControl: {
44
- jobs: new Map(),
45
- nextJobId: 1,
46
- lastBackgroundPid: 0,
47
- },
48
- options: {
49
- errexit: false,
50
- nounset: false,
51
- xtrace: false,
52
- verbose: false,
53
- noglob: false,
54
- monitor: false,
55
- noclobber: false,
56
- allexport: false,
57
- },
58
- history: new HistorySystem(),
59
- completion: new CompletionSystem(),
60
- arrays: new AssociativeArrayManager(),
61
- zshOptions: new ZshOptionsManager(),
62
- prompt: new PromptSystem(),
63
- floatingPoint: new FloatingPointArithmetic(),
64
- zshCompatibility: null, // Will be initialized after constructor
65
- ...initialContext,
66
- };
67
- this.expander = new VariableExpander(this.createVariableContext());
68
- this.pathExpander = new PathnameExpander(this.context.cwd);
69
- this.braceExpander = new BraceExpander();
70
- this.jobManager = new JobManager();
71
- this.jobBuiltins = new JobBuiltins(this.jobManager);
72
- this.extendedExpander = new ExtendedParameterExpander({
73
- variables: this.context.variables,
74
- env: this.context.env,
75
- arrays: this.context.arrays,
76
- });
77
- this.extendedGlobber = new ExtendedGlobber(this.context.cwd);
78
- }
79
- createVariableContext() {
80
- return {
81
- variables: this.context.variables,
82
- env: this.context.env,
83
- positionalParams: this.context.positionalParams,
84
- specialParams: {
85
- '$': process.pid.toString(),
86
- '?': this.context.lastExitCode.toString(),
87
- '#': this.context.positionalParams.length.toString(),
88
- '*': this.context.positionalParams.join(' '),
89
- '@': this.context.positionalParams,
90
- '!': this.context.jobControl.lastBackgroundPid.toString(),
91
- '0': 'lsh',
92
- '-': this.formatShellOptions(),
93
- },
94
- options: this.context.options, // Pass shell options for set -u behavior
95
- };
96
- }
97
- updateExpander() {
98
- this.expander.updateContext(this.createVariableContext());
99
- }
100
- formatShellOptions() {
101
- const options = this.context.options;
102
- let optionString = '';
103
- if (options.errexit)
104
- optionString += 'e';
105
- if (options.nounset)
106
- optionString += 'u';
107
- if (options.xtrace)
108
- optionString += 'x';
109
- if (options.verbose)
110
- optionString += 'v';
111
- if (options.noglob)
112
- optionString += 'f';
113
- if (options.monitor)
114
- optionString += 'm';
115
- if (options.noclobber)
116
- optionString += 'C';
117
- if (options.allexport)
118
- optionString += 'a';
119
- return optionString;
120
- }
121
- adaptJobBuiltinResult(result) {
122
- return {
123
- ...result,
124
- success: result.exitCode === 0
125
- };
126
- }
127
- getContext() {
128
- return { ...this.context };
129
- }
130
- async execute(node) {
131
- try {
132
- return await this.executeNode(node);
133
- }
134
- catch (error) {
135
- return {
136
- stdout: '',
137
- stderr: error.message,
138
- exitCode: 1,
139
- success: false,
140
- };
141
- }
142
- }
143
- async executeNode(node) {
144
- // Implement set -x (xtrace) - print commands before execution
145
- if (this.context.options.xtrace && (node.type === 'SimpleCommand' || node.type === 'Pipeline')) {
146
- console.error(`+ ${this.nodeToString(node)}`);
147
- }
148
- let result;
149
- switch (node.type) {
150
- case 'SimpleCommand':
151
- result = await this.executeSimpleCommand(node);
152
- break;
153
- case 'Pipeline':
154
- result = await this.executePipeline(node);
155
- break;
156
- case 'CommandList':
157
- result = await this.executeCommandList(node);
158
- break;
159
- case 'Subshell':
160
- result = await this.executeSubshell(node);
161
- break;
162
- case 'CommandGroup':
163
- result = await this.executeCommandGroup(node);
164
- break;
165
- case 'IfStatement':
166
- result = await this.executeIfStatement(node);
167
- break;
168
- case 'ForStatement':
169
- result = await this.executeForStatement(node);
170
- break;
171
- case 'WhileStatement':
172
- result = await this.executeWhileStatement(node);
173
- break;
174
- case 'CaseStatement':
175
- result = await this.executeCaseStatement(node);
176
- break;
177
- case 'FunctionDefinition':
178
- result = await this.executeFunctionDefinition(node);
179
- break;
180
- default:
181
- throw new Error(`Unknown AST node type: ${node.type}`);
182
- }
183
- // Implement set -e (errexit) - exit immediately on command failure
184
- if (this.context.options.errexit && !result.success && result.exitCode !== 0) {
185
- // Only exit on actual command failures, not control structures or built-ins that expect failures
186
- if (node.type === 'SimpleCommand' || node.type === 'Pipeline') {
187
- throw new Error(`Command failed with exit code ${result.exitCode} (set -e)`);
188
- }
189
- }
190
- return result;
191
- }
192
- async executeSimpleCommand(cmd) {
193
- if (!cmd.name) {
194
- return { stdout: '', stderr: '', exitCode: 0, success: true };
195
- }
196
- this.updateExpander();
197
- // Expand command name and arguments
198
- const expandedName = await this.expander.expandString(cmd.name);
199
- const expandedArgs = [];
200
- for (const arg of cmd.args) {
201
- // Check for process substitution first
202
- if (arg.startsWith('<(') || arg.startsWith('>(')) {
203
- const fifoPath = await this.handleProcessSubstitution(arg);
204
- expandedArgs.push(fifoPath);
205
- continue;
206
- }
207
- // Step 1: Variable and command substitution expansion
208
- const variableExpanded = await this.expander.expandString(arg);
209
- // Step 2: Brace expansion
210
- const braceExpanded = this.braceExpander.expandBraces(variableExpanded);
211
- // Step 3: Process each brace-expanded result
212
- for (const braceResult of braceExpanded) {
213
- // Step 4: Field splitting (only if variable expansion occurred)
214
- let fields;
215
- if (variableExpanded !== arg && (arg.includes('$') || arg.includes('`'))) {
216
- // Only split if variable expansion happened
217
- fields = this.expander.splitFields(braceResult, this.context.ifs);
218
- }
219
- else {
220
- // No variable expansion, keep as single field
221
- fields = [braceResult];
222
- }
223
- // Step 5: Pathname expansion for each field
224
- for (const field of fields) {
225
- const pathExpanded = await this.pathExpander.expandPathnames(field, {
226
- cwd: this.context.cwd,
227
- includeHidden: false,
228
- });
229
- // If pathname expansion found matches, use them; otherwise use original field
230
- if (pathExpanded.length > 0 && pathExpanded[0] !== field) {
231
- expandedArgs.push(...pathExpanded);
232
- }
233
- else {
234
- expandedArgs.push(field);
235
- }
236
- }
237
- }
238
- }
239
- // Create expanded command
240
- const expandedCmd = {
241
- type: 'SimpleCommand',
242
- name: expandedName,
243
- args: expandedArgs,
244
- redirections: cmd.redirections,
245
- };
246
- // Handle redirections and execute command
247
- return this.executeWithRedirections(expandedCmd);
248
- }
249
- async executeBuiltin(name, args) {
250
- switch (name) {
251
- case 'cd':
252
- return this.builtin_cd(args);
253
- case 'pwd':
254
- return this.builtin_pwd(args);
255
- case 'echo':
256
- return this.builtin_echo(args);
257
- case 'true':
258
- return { stdout: '', stderr: '', exitCode: 0, success: true };
259
- case 'false':
260
- return { stdout: '', stderr: '', exitCode: 1, success: false };
261
- case 'exit':
262
- return this.builtin_exit(args);
263
- case 'export':
264
- return this.builtin_export(args);
265
- case 'unset':
266
- return this.builtin_unset(args);
267
- case 'set':
268
- return this.builtin_set(args);
269
- case 'test':
270
- case '[':
271
- return this.builtin_test(args);
272
- case 'printf':
273
- return this.builtin_printf(args);
274
- case 'eval':
275
- return this.builtin_eval(args);
276
- case 'exec':
277
- return this.builtin_exec(args);
278
- case 'return':
279
- return this.builtin_return(args);
280
- case 'shift':
281
- return this.builtin_shift(args);
282
- case 'local':
283
- return this.builtin_local(args);
284
- case 'jobs':
285
- return this.builtin_jobs(args);
286
- case 'fg':
287
- return this.builtin_fg(args);
288
- case 'bg':
289
- return this.builtin_bg(args);
290
- case 'wait':
291
- return this.builtin_wait(args);
292
- case 'read':
293
- return this.builtin_read(args);
294
- case 'getopts':
295
- return this.builtin_getopts(args);
296
- case 'trap':
297
- return this.builtin_trap(args);
298
- case 'typeset':
299
- return this.builtin_typeset(args);
300
- case 'setopt':
301
- return this.builtin_setopt(args);
302
- case 'unsetopt':
303
- return this.builtin_unsetopt(args);
304
- case 'history':
305
- return this.builtin_history(args);
306
- case 'fc':
307
- return this.builtin_fc(args);
308
- case 'r':
309
- return this.builtin_r(args);
310
- case 'alias':
311
- return this.builtin_alias(args);
312
- case 'unalias':
313
- return this.builtin_unalias(args);
314
- case 'readonly':
315
- return this.builtin_readonly(args);
316
- case 'type':
317
- return this.builtin_type(args);
318
- case 'hash':
319
- return this.builtin_hash(args);
320
- case 'kill':
321
- return this.builtin_kill(args);
322
- case 'source':
323
- return this.builtin_source(args);
324
- case 'install':
325
- return this.builtin_install(args);
326
- case 'uninstall':
327
- return this.builtin_uninstall(args);
328
- case 'zsh-migrate':
329
- return this.builtin_zsh_migrate(args);
330
- case 'zsh-source':
331
- return this.builtin_zsh_source(args);
332
- // Job Management Built-ins
333
- case 'job-create':
334
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobCreate(args));
335
- case 'job-list':
336
- case 'jlist':
337
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobList(args));
338
- case 'job-show':
339
- case 'jshow':
340
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobShow(args));
341
- case 'job-start':
342
- case 'jstart':
343
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobStart(args));
344
- case 'job-stop':
345
- case 'jstop':
346
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobStop(args));
347
- case 'job-pause':
348
- case 'jpause':
349
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobPause(args));
350
- case 'job-resume':
351
- case 'jresume':
352
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobResume(args));
353
- case 'job-remove':
354
- case 'jremove':
355
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobRemove(args));
356
- case 'job-update':
357
- case 'jupdate':
358
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobUpdate(args));
359
- case 'job-run':
360
- case 'jrun':
361
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobRun(args));
362
- case 'job-monitor':
363
- case 'jmonitor':
364
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobMonitor(args));
365
- case 'job-stats':
366
- case 'jstats':
367
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobStats(args));
368
- case 'job-cleanup':
369
- case 'jcleanup':
370
- return this.adaptJobBuiltinResult(await this.jobBuiltins.jobCleanup(args));
371
- case 'ps-list':
372
- case 'pslist':
373
- return this.adaptJobBuiltinResult(await this.jobBuiltins.psList(args));
374
- case 'ps-kill':
375
- case 'pskill':
376
- return this.adaptJobBuiltinResult(await this.jobBuiltins.psKill(args));
377
- default:
378
- return null; // Not a built-in
379
- }
380
- }
381
- async builtin_cd(args) {
382
- const target = args[0] || this.context.env.HOME || '/';
383
- try {
384
- const resolvedPath = path.resolve(this.context.cwd, target);
385
- // Check if directory exists
386
- if (!fs.existsSync(resolvedPath)) {
387
- return {
388
- stdout: '',
389
- stderr: `cd: ${target}: No such file or directory`,
390
- exitCode: 1,
391
- success: false,
392
- };
393
- }
394
- const stats = fs.statSync(resolvedPath);
395
- if (!stats.isDirectory()) {
396
- return {
397
- stdout: '',
398
- stderr: `cd: ${target}: Not a directory`,
399
- exitCode: 1,
400
- success: false,
401
- };
402
- }
403
- // Update context
404
- this.context.cwd = resolvedPath;
405
- process.chdir(resolvedPath);
406
- // Update path expander with new working directory
407
- this.pathExpander = new PathnameExpander(this.context.cwd);
408
- return { stdout: '', stderr: '', exitCode: 0, success: true };
409
- }
410
- catch (error) {
411
- return {
412
- stdout: '',
413
- stderr: `cd: ${target}: ${error.message}`,
414
- exitCode: 1,
415
- success: false,
416
- };
417
- }
418
- }
419
- async builtin_pwd(_args) {
420
- return {
421
- stdout: this.context.cwd,
422
- stderr: '',
423
- exitCode: 0,
424
- success: true,
425
- };
426
- }
427
- async builtin_echo(args) {
428
- let output = args.join(' ');
429
- // Handle -n flag (no trailing newline)
430
- if (args[0] === '-n') {
431
- output = args.slice(1).join(' ');
432
- }
433
- else {
434
- output += '\n';
435
- }
436
- return {
437
- stdout: output,
438
- stderr: '',
439
- exitCode: 0,
440
- success: true,
441
- };
442
- }
443
- async builtin_exit(args) {
444
- const code = args[0] ? parseInt(args[0], 10) : this.context.lastExitCode;
445
- // In a real shell, this would exit the process
446
- // For now, we'll just return the exit code
447
- return {
448
- stdout: '',
449
- stderr: '',
450
- exitCode: code,
451
- success: code === 0,
452
- };
453
- }
454
- async builtin_export(args) {
455
- for (const arg of args) {
456
- if (arg.includes('=')) {
457
- const [name, value] = arg.split('=', 2);
458
- this.context.env[name] = value;
459
- this.context.variables[name] = value;
460
- }
461
- else {
462
- // Export existing variable
463
- if (arg in this.context.variables) {
464
- this.context.env[arg] = this.context.variables[arg];
465
- }
466
- }
467
- }
468
- this.updateExpander();
469
- return { stdout: '', stderr: '', exitCode: 0, success: true };
470
- }
471
- async builtin_unset(args) {
472
- for (const name of args) {
473
- delete this.context.variables[name];
474
- delete this.context.env[name];
475
- }
476
- this.updateExpander();
477
- return { stdout: '', stderr: '', exitCode: 0, success: true };
478
- }
479
- async builtin_set(args) {
480
- if (args.length === 0) {
481
- // Show all variables and functions
482
- const output = Object.entries(this.context.variables)
483
- .map(([key, value]) => `${key}=${value}`)
484
- .join('\n');
485
- return {
486
- stdout: output,
487
- stderr: '',
488
- exitCode: 0,
489
- success: true,
490
- };
491
- }
492
- // Handle set options
493
- for (const arg of args) {
494
- if (arg.startsWith('-')) {
495
- // Enable options
496
- for (let i = 1; i < arg.length; i++) {
497
- const option = arg.charAt(i);
498
- switch (option) {
499
- case 'e':
500
- this.context.options.errexit = true;
501
- break;
502
- case 'u':
503
- this.context.options.nounset = true;
504
- break;
505
- case 'x':
506
- this.context.options.xtrace = true;
507
- break;
508
- case 'v':
509
- this.context.options.verbose = true;
510
- break;
511
- case 'f':
512
- this.context.options.noglob = true;
513
- break;
514
- case 'm':
515
- this.context.options.monitor = true;
516
- break;
517
- case 'C':
518
- this.context.options.noclobber = true;
519
- break;
520
- case 'a':
521
- this.context.options.allexport = true;
522
- break;
523
- default:
524
- return {
525
- stdout: '',
526
- stderr: `set: illegal option -- ${option}`,
527
- exitCode: 1,
528
- success: false,
529
- };
530
- }
531
- }
532
- }
533
- else if (arg.startsWith('+')) {
534
- // Disable options
535
- for (let i = 1; i < arg.length; i++) {
536
- const option = arg.charAt(i);
537
- switch (option) {
538
- case 'e':
539
- this.context.options.errexit = false;
540
- break;
541
- case 'u':
542
- this.context.options.nounset = false;
543
- break;
544
- case 'x':
545
- this.context.options.xtrace = false;
546
- break;
547
- case 'v':
548
- this.context.options.verbose = false;
549
- break;
550
- case 'f':
551
- this.context.options.noglob = false;
552
- break;
553
- case 'm':
554
- this.context.options.monitor = false;
555
- break;
556
- case 'C':
557
- this.context.options.noclobber = false;
558
- break;
559
- case 'a':
560
- this.context.options.allexport = false;
561
- break;
562
- default:
563
- return {
564
- stdout: '',
565
- stderr: `set: illegal option -- ${option}`,
566
- exitCode: 1,
567
- success: false,
568
- };
569
- }
570
- }
571
- }
572
- else {
573
- // Set positional parameters
574
- const index = args.indexOf(arg);
575
- this.context.positionalParams = args.slice(index);
576
- break;
577
- }
578
- }
579
- return {
580
- stdout: '',
581
- stderr: '',
582
- exitCode: 0,
583
- success: true,
584
- };
585
- }
586
- async builtin_test(args) {
587
- // POSIX test command implementation
588
- try {
589
- const result = this.evaluateTestExpression(args);
590
- return {
591
- stdout: '',
592
- stderr: '',
593
- exitCode: result ? 0 : 1,
594
- success: result,
595
- };
596
- }
597
- catch (error) {
598
- return {
599
- stdout: '',
600
- stderr: `test: ${error.message}`,
601
- exitCode: 2,
602
- success: false,
603
- };
604
- }
605
- }
606
- evaluateTestExpression(args) {
607
- if (args.length === 0) {
608
- return false; // No arguments means false
609
- }
610
- // Handle [ command - remove trailing ]
611
- if (args[args.length - 1] === ']') {
612
- args = args.slice(0, -1);
613
- }
614
- if (args.length === 1) {
615
- // Single argument: test if string is non-empty
616
- return args[0] !== '';
617
- }
618
- if (args.length === 2) {
619
- const [operator, operand] = args;
620
- return this.evaluateUnaryTest(operator, operand);
621
- }
622
- if (args.length === 3) {
623
- const [left, operator, right] = args;
624
- return this.evaluateBinaryTest(left, operator, right);
625
- }
626
- if (args.length >= 4) {
627
- // Complex expressions with logical operators
628
- return this.evaluateComplexTest(args);
629
- }
630
- return false;
631
- }
632
- evaluateUnaryTest(operator, operand) {
633
- switch (operator) {
634
- case '-z': return operand === ''; // String is empty
635
- case '-n': return operand !== ''; // String is non-empty
636
- case '-f': return this.isRegularFile(operand);
637
- case '-d': return this.isDirectory(operand);
638
- case '-e': return this.fileExists(operand);
639
- case '-r': return this.isReadable(operand);
640
- case '-w': return this.isWritable(operand);
641
- case '-x': return this.isExecutable(operand);
642
- case '-s': return this.hasSize(operand);
643
- case '!': return !this.evaluateTestExpression([operand]);
644
- default:
645
- throw new Error(`unknown unary operator: ${operator}`);
646
- }
647
- }
648
- evaluateBinaryTest(left, operator, right) {
649
- switch (operator) {
650
- // String comparisons
651
- case '=':
652
- case '==':
653
- return left === right;
654
- case '!=':
655
- return left !== right;
656
- // Numeric comparisons
657
- case '-eq': return parseInt(left, 10) === parseInt(right, 10);
658
- case '-ne': return parseInt(left, 10) !== parseInt(right, 10);
659
- case '-lt': return parseInt(left, 10) < parseInt(right, 10);
660
- case '-le': return parseInt(left, 10) <= parseInt(right, 10);
661
- case '-gt': return parseInt(left, 10) > parseInt(right, 10);
662
- case '-ge': return parseInt(left, 10) >= parseInt(right, 10);
663
- default:
664
- throw new Error(`unknown binary operator: ${operator}`);
665
- }
666
- }
667
- evaluateComplexTest(args) {
668
- // Handle logical operators -a (AND) and -o (OR)
669
- for (let i = 1; i < args.length - 1; i++) {
670
- if (args[i] === '-a') {
671
- const leftArgs = args.slice(0, i);
672
- const rightArgs = args.slice(i + 1);
673
- return this.evaluateTestExpression(leftArgs) && this.evaluateTestExpression(rightArgs);
674
- }
675
- if (args[i] === '-o') {
676
- const leftArgs = args.slice(0, i);
677
- const rightArgs = args.slice(i + 1);
678
- return this.evaluateTestExpression(leftArgs) || this.evaluateTestExpression(rightArgs);
679
- }
680
- }
681
- // If no logical operators, try to evaluate as a complex expression
682
- return false;
683
- }
684
- // File test helper methods
685
- fileExists(path) {
686
- try {
687
- fs.accessSync(path);
688
- return true;
689
- }
690
- catch {
691
- return false;
692
- }
693
- }
694
- isRegularFile(path) {
695
- try {
696
- return fs.statSync(path).isFile();
697
- }
698
- catch {
699
- return false;
700
- }
701
- }
702
- isDirectory(path) {
703
- try {
704
- return fs.statSync(path).isDirectory();
705
- }
706
- catch {
707
- return false;
708
- }
709
- }
710
- isReadable(path) {
711
- try {
712
- fs.accessSync(path, fs.constants.R_OK);
713
- return true;
714
- }
715
- catch {
716
- return false;
717
- }
718
- }
719
- isWritable(path) {
720
- try {
721
- fs.accessSync(path, fs.constants.W_OK);
722
- return true;
723
- }
724
- catch {
725
- return false;
726
- }
727
- }
728
- isExecutable(path) {
729
- try {
730
- fs.accessSync(path, fs.constants.X_OK);
731
- return true;
732
- }
733
- catch {
734
- return false;
735
- }
736
- }
737
- hasSize(path) {
738
- try {
739
- return fs.statSync(path).size > 0;
740
- }
741
- catch {
742
- return false;
743
- }
744
- }
745
- async builtin_printf(args) {
746
- if (args.length === 0) {
747
- return {
748
- stdout: '',
749
- stderr: 'printf: missing format string',
750
- exitCode: 1,
751
- success: false,
752
- };
753
- }
754
- const format = args[0];
755
- const values = args.slice(1);
756
- try {
757
- // Basic printf implementation - handle most common format specifiers
758
- let result = format;
759
- let valueIndex = 0;
760
- // Simple substitution for %s, %d, %c, etc.
761
- result = result.replace(/%[sdcxo%]/g, (match) => {
762
- if (match === '%%')
763
- return '%';
764
- if (valueIndex >= values.length)
765
- return match; // Keep placeholder if no value
766
- const value = values[valueIndex++];
767
- switch (match) {
768
- case '%s': return value;
769
- case '%d': return parseInt(value, 10).toString() || '0';
770
- case '%c': return value.charAt(0);
771
- case '%x': return parseInt(value, 10).toString(16) || '0';
772
- case '%o': return parseInt(value, 10).toString(8) || '0';
773
- default: return value;
774
- }
775
- });
776
- // Handle escape sequences
777
- result = result.replace(/\\n/g, '\n')
778
- .replace(/\\t/g, '\t')
779
- .replace(/\\r/g, '\r')
780
- .replace(/\\b/g, '\b')
781
- .replace(/\\f/g, '\f')
782
- .replace(/\\v/g, '\v')
783
- .replace(/\\\\/g, '\\');
784
- return {
785
- stdout: result,
786
- stderr: '',
787
- exitCode: 0,
788
- success: true,
789
- };
790
- }
791
- catch (error) {
792
- return {
793
- stdout: '',
794
- stderr: `printf: ${error.message}`,
795
- exitCode: 1,
796
- success: false,
797
- };
798
- }
799
- }
800
- async builtin_eval(args) {
801
- if (args.length === 0) {
802
- return { stdout: '', stderr: '', exitCode: 0, success: true };
803
- }
804
- // Join all arguments into a single command string
805
- const command = args.join(' ');
806
- try {
807
- // Parse and execute the command
808
- const { parseShellCommand } = await import('./shell-parser.js');
809
- const ast = parseShellCommand(command);
810
- return await this.execute(ast);
811
- }
812
- catch (error) {
813
- return {
814
- stdout: '',
815
- stderr: `eval: ${error.message}`,
816
- exitCode: 1,
817
- success: false,
818
- };
819
- }
820
- }
821
- async builtin_exec(args) {
822
- if (args.length === 0) {
823
- return {
824
- stdout: '',
825
- stderr: 'exec: usage: exec [-cl] [-a name] [command [arguments]]',
826
- exitCode: 2,
827
- success: false,
828
- };
829
- }
830
- // In a real shell, exec would replace the current process
831
- // For our implementation, we'll just execute the command
832
- try {
833
- const cmd = {
834
- type: 'SimpleCommand',
835
- name: args[0],
836
- args: args.slice(1),
837
- redirections: [],
838
- };
839
- return await this.executeSimpleCommand(cmd);
840
- }
841
- catch (error) {
842
- return {
843
- stdout: '',
844
- stderr: `exec: ${error.message}`,
845
- exitCode: 126,
846
- success: false,
847
- };
848
- }
849
- }
850
- async builtin_return(args) {
851
- // Parse exit code argument (defaults to 0)
852
- let exitCode = 0;
853
- if (args.length > 0) {
854
- const code = parseInt(args[0], 10);
855
- if (isNaN(code)) {
856
- return {
857
- stdout: '',
858
- stderr: `return: ${args[0]}: numeric argument required`,
859
- exitCode: 2,
860
- success: false,
861
- };
862
- }
863
- exitCode = code;
864
- }
865
- // In a real shell, return would exit from a function or sourced script
866
- // For our implementation, we'll store the return value and indicate completion
867
- this.context.lastReturnCode = exitCode;
868
- return {
869
- stdout: '',
870
- stderr: '',
871
- exitCode: exitCode,
872
- success: exitCode === 0,
873
- };
874
- }
875
- async builtin_shift(args) {
876
- // Parse shift count (defaults to 1)
877
- let count = 1;
878
- if (args.length > 0) {
879
- count = parseInt(args[0], 10);
880
- if (isNaN(count) || count < 0) {
881
- return {
882
- stdout: '',
883
- stderr: `shift: ${args[0]}: numeric argument required`,
884
- exitCode: 1,
885
- success: false,
886
- };
887
- }
888
- }
889
- // Check if we have enough positional parameters to shift
890
- if (!this.context.positionalParams) {
891
- this.context.positionalParams = [];
892
- }
893
- const available = this.context.positionalParams.length;
894
- if (count > available) {
895
- return {
896
- stdout: '',
897
- stderr: `shift: shift count (${count}) exceeds number of positional parameters (${available})`,
898
- exitCode: 1,
899
- success: false,
900
- };
901
- }
902
- // Perform the shift
903
- this.context.positionalParams = this.context.positionalParams.slice(count);
904
- return {
905
- stdout: '',
906
- stderr: '',
907
- exitCode: 0,
908
- success: true,
909
- };
910
- }
911
- async builtin_local(args) {
912
- // The local command is only meaningful within a function
913
- // For now, we'll treat it the same as variable assignment
914
- // In a full implementation, this would set function-local variables
915
- if (args.length === 0) {
916
- // List all local variables (in a full implementation)
917
- return {
918
- stdout: '',
919
- stderr: '',
920
- exitCode: 0,
921
- success: true,
922
- };
923
- }
924
- // Process variable assignments
925
- for (const arg of args) {
926
- const equalIndex = arg.indexOf('=');
927
- if (equalIndex > 0) {
928
- const name = arg.substring(0, equalIndex);
929
- const value = arg.substring(equalIndex + 1);
930
- this.context.variables[name] = value;
931
- }
932
- else {
933
- // Just declare the variable as local (set to empty in our implementation)
934
- this.context.variables[arg] = '';
935
- }
936
- }
937
- // Update expander with new variables
938
- this.updateExpander();
939
- return {
940
- stdout: '',
941
- stderr: '',
942
- exitCode: 0,
943
- success: true,
944
- };
945
- }
946
- // Job Control Built-ins
947
- async builtin_jobs(_args) {
948
- const jobs = Array.from(this.context.jobControl.jobs.values());
949
- if (jobs.length === 0) {
950
- return {
951
- stdout: '',
952
- stderr: '',
953
- exitCode: 0,
954
- success: true,
955
- };
956
- }
957
- let output = '';
958
- for (const job of jobs) {
959
- const status = job.status === 'running' ? 'Running' : 'Done';
960
- output += `[${job.id}]${job.status === 'running' ? '+' : '-'} ${status} ${job.command}\n`;
961
- }
962
- return {
963
- stdout: output,
964
- stderr: '',
965
- exitCode: 0,
966
- success: true,
967
- };
968
- }
969
- async builtin_fg(_args) {
970
- // In a real implementation, this would bring a background job to foreground
971
- // For now, just return a message
972
- return {
973
- stdout: '',
974
- stderr: 'fg: job control not fully implemented',
975
- exitCode: 1,
976
- success: false,
977
- };
978
- }
979
- async builtin_bg(_args) {
980
- // In a real implementation, this would resume a stopped job in background
981
- // For now, just return a message
982
- return {
983
- stdout: '',
984
- stderr: 'bg: job control not fully implemented',
985
- exitCode: 1,
986
- success: false,
987
- };
988
- }
989
- async builtin_wait(args) {
990
- // Wait for background jobs to complete
991
- if (args.length === 0) {
992
- // Wait for all background jobs
993
- const runningJobs = Array.from(this.context.jobControl.jobs.values())
994
- .filter(job => job.status === 'running');
995
- if (runningJobs.length === 0) {
996
- return {
997
- stdout: '',
998
- stderr: '',
999
- exitCode: 0,
1000
- success: true,
1001
- };
1002
- }
1003
- // In a real implementation, this would actually wait for process completion
1004
- // For now, just simulate waiting briefly
1005
- await new Promise(resolve => setTimeout(resolve, 100));
1006
- return {
1007
- stdout: '',
1008
- stderr: '',
1009
- exitCode: 0,
1010
- success: true,
1011
- };
1012
- }
1013
- // Wait for specific job (simplified implementation)
1014
- const pid = parseInt(args[0], 10);
1015
- const job = Array.from(this.context.jobControl.jobs.values())
1016
- .find(j => j.pid === pid);
1017
- if (!job) {
1018
- return {
1019
- stdout: '',
1020
- stderr: `wait: ${pid}: no such job`,
1021
- exitCode: 1,
1022
- success: false,
1023
- };
1024
- }
1025
- return {
1026
- stdout: '',
1027
- stderr: '',
1028
- exitCode: 0,
1029
- success: true,
1030
- };
1031
- }
1032
- // Essential Built-ins for POSIX Compliance
1033
- async builtin_read(args) {
1034
- // Basic read implementation - reads from stdin
1035
- // In a full implementation, this would handle options like -p, -t, -n, etc.
1036
- if (args.length === 0) {
1037
- return {
1038
- stdout: '',
1039
- stderr: 'read: missing variable name',
1040
- exitCode: 1,
1041
- success: false,
1042
- };
1043
- }
1044
- const varName = args[0];
1045
- try {
1046
- // For now, simulate reading input (in a real implementation, this would read from stdin)
1047
- // Since we can't easily read from stdin in this context, we'll set a default value
1048
- const inputValue = 'simulated_input';
1049
- // Set the variable
1050
- this.context.variables[varName] = inputValue;
1051
- this.updateExpander();
1052
- return {
1053
- stdout: '',
1054
- stderr: '',
1055
- exitCode: 0,
1056
- success: true,
1057
- };
1058
- }
1059
- catch (error) {
1060
- return {
1061
- stdout: '',
1062
- stderr: `read: ${error.message}`,
1063
- exitCode: 1,
1064
- success: false,
1065
- };
1066
- }
1067
- }
1068
- async builtin_getopts(args) {
1069
- // Basic getopts implementation for option parsing
1070
- // Usage: getopts optstring name [args...]
1071
- if (args.length < 2) {
1072
- return {
1073
- stdout: '',
1074
- stderr: 'getopts: usage: getopts optstring name [args...]',
1075
- exitCode: 1,
1076
- success: false,
1077
- };
1078
- }
1079
- const _optstring = args[0]; // TODO: Use this to validate options
1080
- const varName = args[1];
1081
- const optargs = args.length > 2 ? args.slice(2) : this.context.positionalParams;
1082
- // Initialize OPTIND if not set
1083
- if (!this.context.variables.OPTIND) {
1084
- this.context.variables.OPTIND = '1';
1085
- }
1086
- const optind = parseInt(this.context.variables.OPTIND, 10);
1087
- // If we've processed all arguments
1088
- if (optind > optargs.length) {
1089
- this.context.variables[varName] = '?';
1090
- return {
1091
- stdout: '',
1092
- stderr: '',
1093
- exitCode: 1,
1094
- success: false,
1095
- };
1096
- }
1097
- const currentArg = optargs[optind - 1];
1098
- // Check if current argument is an option
1099
- if (!currentArg || !currentArg.startsWith('-') || currentArg.length < 2) {
1100
- this.context.variables[varName] = '?';
1101
- return {
1102
- stdout: '',
1103
- stderr: '',
1104
- exitCode: 1,
1105
- success: false,
1106
- };
1107
- }
1108
- // Simple implementation - just return the first character after -
1109
- const option = currentArg.charAt(1);
1110
- this.context.variables[varName] = option;
1111
- this.context.variables.OPTIND = String(optind + 1);
1112
- this.updateExpander();
1113
- return {
1114
- stdout: '',
1115
- stderr: '',
1116
- exitCode: 0,
1117
- success: true,
1118
- };
1119
- }
1120
- async builtin_trap(args) {
1121
- // Basic trap implementation for signal handling
1122
- // Usage: trap [-lp] [command] [signal...]
1123
- if (args.length === 0) {
1124
- // List current traps (simplified)
1125
- let output = '';
1126
- if (this.context.variables.TRAP_INT) {
1127
- output += `trap -- '${this.context.variables.TRAP_INT}' INT\n`;
1128
- }
1129
- if (this.context.variables.TRAP_TERM) {
1130
- output += `trap -- '${this.context.variables.TRAP_TERM}' TERM\n`;
1131
- }
1132
- return {
1133
- stdout: output,
1134
- stderr: '',
1135
- exitCode: 0,
1136
- success: true,
1137
- };
1138
- }
1139
- if (args[0] === '-l') {
1140
- // List signal names
1141
- return {
1142
- stdout: ' 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP\n' +
1143
- ' 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1\n' +
1144
- '11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM\n',
1145
- stderr: '',
1146
- exitCode: 0,
1147
- success: true,
1148
- };
1149
- }
1150
- if (args.length >= 2) {
1151
- const command = args[0];
1152
- const signals = args.slice(1);
1153
- // Set trap for each signal (simplified implementation)
1154
- for (const signal of signals) {
1155
- const signalUpper = signal.toUpperCase();
1156
- if (signalUpper === 'INT' || signalUpper === 'SIGINT') {
1157
- this.context.variables.TRAP_INT = command;
1158
- }
1159
- else if (signalUpper === 'TERM' || signalUpper === 'SIGTERM') {
1160
- this.context.variables.TRAP_TERM = command;
1161
- }
1162
- }
1163
- this.updateExpander();
1164
- return {
1165
- stdout: '',
1166
- stderr: '',
1167
- exitCode: 0,
1168
- success: true,
1169
- };
1170
- }
1171
- return {
1172
- stdout: '',
1173
- stderr: 'trap: usage: trap [-lp] [command] [signal...]',
1174
- exitCode: 1,
1175
- success: false,
1176
- };
1177
- }
1178
- async executeExternalCommand(cmd) {
1179
- return new Promise((resolve) => {
1180
- const child = spawn(cmd.name, cmd.args, {
1181
- cwd: this.context.cwd,
1182
- env: this.context.env,
1183
- stdio: ['pipe', 'pipe', 'pipe'],
1184
- });
1185
- let stdout = '';
1186
- let stderr = '';
1187
- child.stdout?.on('data', (data) => {
1188
- stdout += data.toString();
1189
- });
1190
- child.stderr?.on('data', (data) => {
1191
- stderr += data.toString();
1192
- });
1193
- child.on('close', (code) => {
1194
- const exitCode = code || 0;
1195
- this.context.lastExitCode = exitCode;
1196
- resolve({
1197
- stdout: stdout.trim(),
1198
- stderr: stderr.trim(),
1199
- exitCode,
1200
- success: exitCode === 0,
1201
- });
1202
- });
1203
- child.on('error', (_error) => {
1204
- resolve({
1205
- stdout: '',
1206
- stderr: `${cmd.name}: command not found`,
1207
- exitCode: 127,
1208
- success: false,
1209
- });
1210
- });
1211
- });
1212
- }
1213
- async executePipeline(pipeline) {
1214
- if (pipeline.commands.length === 1) {
1215
- return this.executeNode(pipeline.commands[0]);
1216
- }
1217
- // For now, use simple shell delegation for pipelines
1218
- // TODO: Implement proper pipeline with stdout/stdin chaining
1219
- const commandStrings = pipeline.commands.map(cmd => this.nodeToString(cmd));
1220
- const pipelineCommand = commandStrings.join(' | ');
1221
- try {
1222
- const { stdout, stderr } = await execAsync(pipelineCommand, {
1223
- cwd: this.context.cwd,
1224
- env: this.context.env,
1225
- });
1226
- return {
1227
- stdout: stdout.trim(),
1228
- stderr: stderr.trim(),
1229
- exitCode: 0,
1230
- success: true,
1231
- };
1232
- }
1233
- catch (error) {
1234
- return {
1235
- stdout: '',
1236
- stderr: error.message,
1237
- exitCode: error.code || 1,
1238
- success: false,
1239
- };
1240
- }
1241
- }
1242
- // Helper method to convert AST node back to string (for fallback cases)
1243
- nodeToString(node) {
1244
- switch (node.type) {
1245
- case 'SimpleCommand': {
1246
- const cmd = node;
1247
- return [cmd.name, ...cmd.args].join(' ');
1248
- }
1249
- case 'Pipeline': {
1250
- const pipeline = node;
1251
- return pipeline.commands.map(c => this.nodeToString(c)).join(' | ');
1252
- }
1253
- case 'CommandList': {
1254
- const cmdList = node;
1255
- const left = this.nodeToString(cmdList.left);
1256
- const right = cmdList.right ? this.nodeToString(cmdList.right) : '';
1257
- return `${left} ${cmdList.operator} ${right}`.trim();
1258
- }
1259
- case 'IfStatement':
1260
- case 'ForStatement':
1261
- case 'WhileStatement':
1262
- case 'CaseStatement':
1263
- return `[${node.type}]`;
1264
- default:
1265
- return '';
1266
- }
1267
- }
1268
- // Control Structure Execution Methods
1269
- async executeIfStatement(ifStmt) {
1270
- // Execute condition
1271
- const conditionResult = await this.executeNode(ifStmt.condition);
1272
- if (conditionResult.success) {
1273
- // Condition succeeded, execute then clause
1274
- return this.executeNode(ifStmt.thenClause);
1275
- }
1276
- else if (ifStmt.elseClause) {
1277
- // Condition failed, execute else clause if present
1278
- return this.executeNode(ifStmt.elseClause);
1279
- }
1280
- else {
1281
- // No else clause, return success with empty output
1282
- return {
1283
- stdout: '',
1284
- stderr: '',
1285
- exitCode: 0,
1286
- success: true,
1287
- };
1288
- }
1289
- }
1290
- async executeForStatement(forStmt) {
1291
- let lastResult = {
1292
- stdout: '',
1293
- stderr: '',
1294
- exitCode: 0,
1295
- success: true,
1296
- };
1297
- this.updateExpander();
1298
- // Determine the word list to iterate over
1299
- let words = forStmt.words;
1300
- if (words.length === 0) {
1301
- // If no words specified, use positional parameters
1302
- words = this.context.positionalParams;
1303
- }
1304
- // Expand each word before iteration
1305
- const expandedWords = [];
1306
- for (const word of words) {
1307
- const expanded = await this.expander.expandString(word);
1308
- // Apply field splitting to expanded words
1309
- const fields = this.expander.splitFields(expanded, this.context.ifs);
1310
- expandedWords.push(...fields);
1311
- }
1312
- // Iterate over each word
1313
- for (const word of expandedWords) {
1314
- // Set the loop variable
1315
- this.context.variables[forStmt.variable] = word;
1316
- this.updateExpander();
1317
- // Execute the body
1318
- lastResult = await this.executeNode(forStmt.body);
1319
- // Update context with result
1320
- this.context.lastExitCode = lastResult.exitCode;
1321
- // TODO: Handle break/continue statements
1322
- // For now, continue execution unless there's an error
1323
- if (!lastResult.success) {
1324
- break;
1325
- }
1326
- }
1327
- return lastResult;
1328
- }
1329
- async executeWhileStatement(whileStmt) {
1330
- let lastResult = {
1331
- stdout: '',
1332
- stderr: '',
1333
- exitCode: 0,
1334
- success: true,
1335
- };
1336
- // Continue looping while condition succeeds
1337
- while (true) {
1338
- // Execute condition
1339
- const conditionResult = await this.executeNode(whileStmt.condition);
1340
- if (!conditionResult.success) {
1341
- // Condition failed, exit loop
1342
- break;
1343
- }
1344
- // Execute body
1345
- lastResult = await this.executeNode(whileStmt.body);
1346
- // Update context with result
1347
- this.context.lastExitCode = lastResult.exitCode;
1348
- // TODO: Handle break/continue statements
1349
- // For now, continue execution
1350
- }
1351
- return lastResult;
1352
- }
1353
- async executeCaseStatement(caseStmt) {
1354
- this.updateExpander();
1355
- // Expand the case word
1356
- const expandedWord = await this.expander.expandString(caseStmt.word);
1357
- // Try to match against each case item
1358
- for (const item of caseStmt.items) {
1359
- for (const pattern of item.patterns) {
1360
- // Expand the pattern
1361
- const expandedPattern = await this.expander.expandString(pattern);
1362
- // Simple pattern matching (would need full glob pattern matching)
1363
- if (this.matchesPattern(expandedWord, expandedPattern)) {
1364
- if (item.command) {
1365
- return this.executeNode(item.command);
1366
- }
1367
- else {
1368
- return {
1369
- stdout: '',
1370
- stderr: '',
1371
- exitCode: 0,
1372
- success: true,
1373
- };
1374
- }
1375
- }
1376
- }
1377
- }
1378
- // No pattern matched, return success
1379
- return {
1380
- stdout: '',
1381
- stderr: '',
1382
- exitCode: 0,
1383
- success: true,
1384
- };
1385
- }
1386
- matchesPattern(text, pattern) {
1387
- // Simple pattern matching - exact match or '*' wildcard
1388
- if (pattern === '*') {
1389
- return true;
1390
- }
1391
- // Convert simple glob patterns to regex
1392
- const regexPattern = pattern
1393
- .replace(/\*/g, '.*') // * matches any string
1394
- .replace(/\?/g, '.') // ? matches any single character
1395
- .replace(/\[([^\]]+)\]/g, '[$1]'); // [abc] character class
1396
- const regex = new RegExp(`^${regexPattern}$`);
1397
- return regex.test(text);
1398
- }
1399
- // Subshell and Command Grouping Implementation
1400
- async executeSubshell(subshell) {
1401
- // Create a new executor with isolated environment
1402
- const subshellContext = {
1403
- // Copy current context but isolate variables and working directory
1404
- env: { ...this.context.env },
1405
- cwd: this.context.cwd, // Subshells inherit CWD but changes don't affect parent
1406
- variables: { ...this.context.variables }, // Copy variables but changes are isolated
1407
- lastExitCode: this.context.lastExitCode,
1408
- lastReturnCode: this.context.lastReturnCode,
1409
- positionalParams: [...this.context.positionalParams],
1410
- ifs: this.context.ifs,
1411
- functions: this.context.functions, // Functions are shared (not isolated)
1412
- };
1413
- // Create isolated executor
1414
- const subshellExecutor = new ShellExecutor(subshellContext);
1415
- try {
1416
- // Execute command in subshell
1417
- const result = await subshellExecutor.execute(subshell.command);
1418
- // Update parent's exit code but not other context
1419
- this.context.lastExitCode = result.exitCode;
1420
- return result;
1421
- }
1422
- catch (error) {
1423
- return {
1424
- stdout: '',
1425
- stderr: `subshell: ${error.message}`,
1426
- exitCode: 1,
1427
- success: false,
1428
- };
1429
- }
1430
- }
1431
- async executeCommandGroup(group) {
1432
- // Command groups run in the current context (no isolation like subshells)
1433
- try {
1434
- return await this.executeNode(group.command);
1435
- }
1436
- catch (error) {
1437
- return {
1438
- stdout: '',
1439
- stderr: `command group: ${error.message}`,
1440
- exitCode: 1,
1441
- success: false,
1442
- };
1443
- }
1444
- }
1445
- async executeBackgroundCommand(backgroundCommand, foregroundCommand) {
1446
- // For now, use setImmediate for background execution
1447
- // TODO: Implement proper process spawning for real background jobs
1448
- const commandString = this.nodeToString(backgroundCommand);
1449
- // Create job entry
1450
- const jobId = this.context.jobControl.nextJobId++;
1451
- // For now, use a fake PID (in real implementation, this would be the actual process PID)
1452
- const fakePid = Date.now() % 100000;
1453
- const job = {
1454
- id: jobId,
1455
- pid: fakePid,
1456
- command: commandString,
1457
- status: 'running',
1458
- startTime: Date.now(),
1459
- };
1460
- // Add job to tracking
1461
- this.context.jobControl.jobs.set(jobId, job);
1462
- this.context.jobControl.lastBackgroundPid = fakePid;
1463
- // Update expander context to reflect new background PID
1464
- this.updateExpander();
1465
- // Execute background command asynchronously
1466
- setImmediate(async () => {
1467
- try {
1468
- await this.executeNode(backgroundCommand);
1469
- // Mark job as done
1470
- if (this.context.jobControl.jobs.has(jobId)) {
1471
- this.context.jobControl.jobs.get(jobId).status = 'done';
1472
- }
1473
- }
1474
- catch (_error) {
1475
- // Mark job as done with error
1476
- if (this.context.jobControl.jobs.has(jobId)) {
1477
- this.context.jobControl.jobs.get(jobId).status = 'done';
1478
- }
1479
- // Background errors are typically not displayed immediately
1480
- }
1481
- });
1482
- // Execute foreground command if present
1483
- if (foregroundCommand) {
1484
- return this.executeNode(foregroundCommand);
1485
- }
1486
- // Return success for background job started
1487
- return {
1488
- stdout: `[${jobId}] ${fakePid}`,
1489
- stderr: '',
1490
- exitCode: 0,
1491
- success: true,
1492
- };
1493
- }
1494
- async executeCommandList(cmdList) {
1495
- // Execute left side first
1496
- const leftResult = await this.executeNode(cmdList.left);
1497
- // Update context with left result
1498
- this.context.lastExitCode = leftResult.exitCode;
1499
- this.updateExpander();
1500
- // Handle different operators
1501
- switch (cmdList.operator) {
1502
- case '&&':
1503
- // Execute right only if left succeeded
1504
- if (leftResult.success) {
1505
- if (cmdList.right) {
1506
- return this.executeNode(cmdList.right);
1507
- }
1508
- }
1509
- return leftResult;
1510
- case '||':
1511
- // Execute right only if left failed
1512
- if (!leftResult.success) {
1513
- if (cmdList.right) {
1514
- return this.executeNode(cmdList.right);
1515
- }
1516
- }
1517
- return leftResult;
1518
- case ';':
1519
- // Execute right regardless of left result
1520
- if (cmdList.right) {
1521
- const rightResult = await this.executeNode(cmdList.right);
1522
- // Return the right result but preserve left output
1523
- return {
1524
- stdout: leftResult.stdout + rightResult.stdout,
1525
- stderr: leftResult.stderr + rightResult.stderr,
1526
- exitCode: rightResult.exitCode,
1527
- success: rightResult.success,
1528
- };
1529
- }
1530
- return leftResult;
1531
- case '&':
1532
- // Execute left in background, then execute right immediately
1533
- return this.executeBackgroundCommand(cmdList.left, cmdList.right);
1534
- default:
1535
- throw new Error(`Unknown command list operator: ${cmdList.operator}`);
1536
- }
1537
- }
1538
- // Function Implementation
1539
- async executeFunctionDefinition(funcDef) {
1540
- // Store the function definition in the context
1541
- this.context.functions[funcDef.name] = funcDef;
1542
- return {
1543
- stdout: '',
1544
- stderr: '',
1545
- exitCode: 0,
1546
- success: true,
1547
- };
1548
- }
1549
- async executeFunctionCall(name, args) {
1550
- const funcDef = this.context.functions[name];
1551
- if (!funcDef) {
1552
- return {
1553
- stdout: '',
1554
- stderr: `${name}: function not found`,
1555
- exitCode: 127,
1556
- success: false,
1557
- };
1558
- }
1559
- // Save current context for restoration
1560
- const savedPositionalParams = this.context.positionalParams;
1561
- const savedVariables = { ...this.context.variables };
1562
- try {
1563
- // Set up function parameters
1564
- this.context.positionalParams = args;
1565
- // Update expander with new context
1566
- this.updateExpander();
1567
- // Execute function body
1568
- const result = await this.executeNode(funcDef.body);
1569
- return result;
1570
- }
1571
- finally {
1572
- // Restore original context
1573
- this.context.positionalParams = savedPositionalParams;
1574
- // Restore non-local variables
1575
- for (const key in savedVariables) {
1576
- this.context.variables[key] = savedVariables[key];
1577
- }
1578
- // Remove any variables that were created during function execution
1579
- // (this is a simple implementation - real shells have more complex scoping)
1580
- this.updateExpander();
1581
- }
1582
- }
1583
- // I/O Redirection Implementation
1584
- async executeWithRedirections(cmd) {
1585
- if (!cmd.redirections || cmd.redirections.length === 0) {
1586
- // No redirections, execute normally
1587
- // Check for function call first
1588
- if (this.context.functions[cmd.name]) {
1589
- const result = await this.executeFunctionCall(cmd.name, cmd.args);
1590
- this.context.lastExitCode = result.exitCode;
1591
- this.updateExpander();
1592
- return result;
1593
- }
1594
- const builtinResult = await this.executeBuiltin(cmd.name, cmd.args);
1595
- if (builtinResult !== null) {
1596
- this.context.lastExitCode = builtinResult.exitCode;
1597
- this.updateExpander();
1598
- return builtinResult;
1599
- }
1600
- return this.executeExternalCommand(cmd);
1601
- }
1602
- // Process redirections and execute command
1603
- return this.executeWithRedirectionHandling(cmd);
1604
- }
1605
- async executeWithRedirectionHandling(cmd) {
1606
- const fs = await import('fs');
1607
- const originalFiles = {};
1608
- const redirectionFiles = {};
1609
- try {
1610
- // Process each redirection
1611
- for (const redir of cmd.redirections) {
1612
- await this.processRedirection(redir, originalFiles, redirectionFiles, fs);
1613
- }
1614
- // Execute the command with redirections in place
1615
- let result;
1616
- // Check for function call first
1617
- if (this.context.functions[cmd.name]) {
1618
- // Functions don't directly support redirections in our implementation
1619
- // We'll execute the function and handle output redirection manually
1620
- const functionResult = await this.executeFunctionCall(cmd.name, cmd.args);
1621
- result = await this.executeBuiltinWithRedirection(cmd.name, cmd.args, functionResult, redirectionFiles, fs);
1622
- this.context.lastExitCode = result.exitCode;
1623
- }
1624
- else {
1625
- const builtinResult = await this.executeBuiltin(cmd.name, cmd.args);
1626
- if (builtinResult !== null) {
1627
- // For built-ins, handle output redirection manually
1628
- result = await this.executeBuiltinWithRedirection(cmd.name, cmd.args, builtinResult, redirectionFiles, fs);
1629
- this.context.lastExitCode = result.exitCode;
1630
- }
1631
- else {
1632
- // For external commands, redirections are handled by spawn
1633
- result = await this.executeExternalCommandWithRedirection(cmd, redirectionFiles);
1634
- this.context.lastExitCode = result.exitCode;
1635
- }
1636
- }
1637
- return result;
1638
- }
1639
- catch (error) {
1640
- return {
1641
- stdout: '',
1642
- stderr: `Redirection error: ${error.message}`,
1643
- exitCode: 1,
1644
- success: false,
1645
- };
1646
- }
1647
- finally {
1648
- // Restore original file descriptors and clean up
1649
- this.cleanupRedirections(originalFiles, redirectionFiles, fs);
1650
- }
1651
- }
1652
- async processRedirection(redir, originalFiles, redirectionFiles, fs) {
1653
- const target = await this.expander.expandString(redir.target);
1654
- switch (redir.type) {
1655
- case 'output': // >
1656
- try {
1657
- const fd = fs.openSync(target, 'w');
1658
- redirectionFiles[1] = fd; // stdout
1659
- break;
1660
- }
1661
- catch (error) {
1662
- throw new Error(`cannot create ${target}: ${error.message}`);
1663
- }
1664
- case 'append': // >>
1665
- try {
1666
- const fd = fs.openSync(target, 'a');
1667
- redirectionFiles[1] = fd; // stdout
1668
- break;
1669
- }
1670
- catch (error) {
1671
- throw new Error(`cannot create ${target}: ${error.message}`);
1672
- }
1673
- case 'input': // <
1674
- try {
1675
- if (!fs.existsSync(target)) {
1676
- throw new Error(`${target}: No such file or directory`);
1677
- }
1678
- const fd = fs.openSync(target, 'r');
1679
- redirectionFiles[0] = fd; // stdin
1680
- break;
1681
- }
1682
- catch (error) {
1683
- throw new Error(`cannot open ${target}: ${error.message}`);
1684
- }
1685
- case 'heredoc': { // <<
1686
- // Create a temporary file with the here document content
1687
- const tmpFile = `/tmp/lsh-heredoc-${Date.now()}`;
1688
- fs.writeFileSync(tmpFile, target); // target contains the here-doc content
1689
- const fd = fs.openSync(tmpFile, 'r');
1690
- redirectionFiles[0] = fd; // stdin
1691
- // Schedule cleanup of temp file
1692
- setTimeout(() => {
1693
- try {
1694
- fs.unlinkSync(tmpFile);
1695
- }
1696
- catch (_) {
1697
- // Ignore cleanup errors
1698
- }
1699
- }, 1000);
1700
- break;
1701
- }
1702
- default:
1703
- throw new Error(`unsupported redirection type: ${redir.type}`);
1704
- }
1705
- }
1706
- async executeBuiltinWithRedirection(name, args, result, redirectionFiles, fs) {
1707
- // For built-ins, we need to manually handle output redirection
1708
- if (redirectionFiles[1] !== undefined) {
1709
- // Redirect stdout to file
1710
- if (result.stdout) {
1711
- fs.writeSync(redirectionFiles[1], result.stdout);
1712
- }
1713
- // Return result with empty stdout since it was redirected
1714
- return {
1715
- stdout: '',
1716
- stderr: result.stderr,
1717
- exitCode: result.exitCode,
1718
- success: result.success,
1719
- };
1720
- }
1721
- return result;
1722
- }
1723
- async executeExternalCommandWithRedirection(cmd, redirectionFiles) {
1724
- return new Promise((resolve) => {
1725
- const stdio = ['inherit', 'pipe', 'pipe'];
1726
- // Configure stdio based on redirections
1727
- if (redirectionFiles[0] !== undefined) {
1728
- stdio[0] = redirectionFiles[0]; // stdin
1729
- }
1730
- if (redirectionFiles[1] !== undefined) {
1731
- stdio[1] = redirectionFiles[1]; // stdout
1732
- }
1733
- if (redirectionFiles[2] !== undefined) {
1734
- stdio[2] = redirectionFiles[2]; // stderr
1735
- }
1736
- const child = spawn(cmd.name, cmd.args, {
1737
- stdio,
1738
- cwd: this.context.cwd,
1739
- env: { ...this.context.env, ...this.context.variables },
1740
- });
1741
- let stdout = '';
1742
- let stderr = '';
1743
- if (child.stdout && stdio[1] === 'pipe') {
1744
- child.stdout.on('data', (data) => {
1745
- stdout += data.toString();
1746
- });
1747
- }
1748
- if (child.stderr && stdio[2] === 'pipe') {
1749
- child.stderr.on('data', (data) => {
1750
- stderr += data.toString();
1751
- });
1752
- }
1753
- child.on('close', (code) => {
1754
- resolve({
1755
- stdout: stdio[1] === 'pipe' ? stdout : '', // Empty if redirected
1756
- stderr: stdio[2] === 'pipe' ? stderr : '',
1757
- exitCode: code || 0,
1758
- success: (code || 0) === 0,
1759
- });
1760
- });
1761
- child.on('error', (error) => {
1762
- resolve({
1763
- stdout: '',
1764
- stderr: `Command failed: ${error.message}`,
1765
- exitCode: 127,
1766
- success: false,
1767
- });
1768
- });
1769
- });
1770
- }
1771
- cleanupRedirections(originalFiles, redirectionFiles, fs) {
1772
- // Close redirection files
1773
- for (const fd of Object.values(redirectionFiles)) {
1774
- try {
1775
- if (typeof fd === 'number') {
1776
- fs.closeSync(fd);
1777
- }
1778
- }
1779
- catch (_error) {
1780
- // Ignore cleanup errors
1781
- }
1782
- }
1783
- }
1784
- async handleProcessSubstitution(procSubArg) {
1785
- const os = await import('os');
1786
- const path = await import('path');
1787
- const fs = await import('fs');
1788
- const { parseShellCommand } = await import('./shell-parser.js');
1789
- // Extract direction and command from the process substitution
1790
- const isInput = procSubArg.startsWith('<(');
1791
- const command = procSubArg.slice(2, -1); // Remove <( or >( and )
1792
- // Create a temporary file to simulate named pipe behavior
1793
- const tmpDir = await fs.promises.mkdtemp(path.default.join(os.default.tmpdir(), 'lsh-procsub-'));
1794
- const fifoPath = path.default.join(tmpDir, isInput ? 'input' : 'output');
1795
- try {
1796
- if (isInput) {
1797
- // For <(command), execute command and write output to temp file
1798
- const ast = parseShellCommand(command);
1799
- const executor = new ShellExecutor(this.context);
1800
- const result = await executor.execute(ast);
1801
- // Write command output to temporary file
1802
- await fs.promises.writeFile(fifoPath, result.stdout || '');
1803
- return fifoPath;
1804
- }
1805
- else {
1806
- // For >(command), create a temporary file that can be written to
1807
- // The command will process the file content after the main command finishes
1808
- // This is a simplified implementation - full POSIX would use actual named pipes
1809
- await fs.promises.writeFile(fifoPath, '');
1810
- // Store the command to execute later when the file has content
1811
- // For now, return the file path - the command would process it post-execution
1812
- return fifoPath;
1813
- }
1814
- }
1815
- catch (error) {
1816
- // Clean up on error
1817
- try {
1818
- await fs.promises.unlink(fifoPath);
1819
- }
1820
- catch (_) {
1821
- // Ignore cleanup errors
1822
- }
1823
- throw new Error(`Process substitution failed: ${error.message}`);
1824
- }
1825
- }
1826
- // ZSH-Style Built-in Commands
1827
- async builtin_typeset(args) {
1828
- const result = this.context.arrays.parseTypesetCommand(args);
1829
- return {
1830
- stdout: '',
1831
- stderr: result.message,
1832
- exitCode: result.success ? 0 : 1,
1833
- success: result.success,
1834
- };
1835
- }
1836
- async builtin_setopt(args) {
1837
- const result = this.context.zshOptions.parseSetoptCommand(args);
1838
- return {
1839
- stdout: '',
1840
- stderr: result.message,
1841
- exitCode: result.success ? 0 : 1,
1842
- success: result.success,
1843
- };
1844
- }
1845
- async builtin_unsetopt(args) {
1846
- const result = this.context.zshOptions.parseUnsetoptCommand(args);
1847
- return {
1848
- stdout: '',
1849
- stderr: result.message,
1850
- exitCode: result.success ? 0 : 1,
1851
- success: result.success,
1852
- };
1853
- }
1854
- async builtin_history(args) {
1855
- if (args.length === 0) {
1856
- // Show history
1857
- const entries = this.context.history.getAllEntries();
1858
- const output = entries
1859
- .map(entry => `${entry.lineNumber.toString().padStart(4)} ${entry.command}`)
1860
- .join('\n');
1861
- return {
1862
- stdout: output,
1863
- stderr: '',
1864
- exitCode: 0,
1865
- success: true,
1866
- };
1867
- }
1868
- // Handle history options
1869
- if (args[0] === '-c') {
1870
- this.context.history.clearHistory();
1871
- return {
1872
- stdout: '',
1873
- stderr: '',
1874
- exitCode: 0,
1875
- success: true,
1876
- };
1877
- }
1878
- if (args[0] === '-d') {
1879
- // Delete specific history entry
1880
- const lineNumber = parseInt(args[1], 10);
1881
- if (isNaN(lineNumber)) {
1882
- return {
1883
- stdout: '',
1884
- stderr: 'history: invalid line number',
1885
- exitCode: 1,
1886
- success: false,
1887
- };
1888
- }
1889
- // Implementation would delete specific entry
1890
- return {
1891
- stdout: '',
1892
- stderr: '',
1893
- exitCode: 0,
1894
- success: true,
1895
- };
1896
- }
1897
- return {
1898
- stdout: '',
1899
- stderr: 'history: unknown option',
1900
- exitCode: 1,
1901
- success: false,
1902
- };
1903
- }
1904
- async builtin_fc(args) {
1905
- // Fix command - edit and re-execute last command
1906
- if (args.length === 0) {
1907
- const entries = this.context.history.getAllEntries();
1908
- if (entries.length === 0) {
1909
- return {
1910
- stdout: '',
1911
- stderr: 'fc: no history',
1912
- exitCode: 1,
1913
- success: false,
1914
- };
1915
- }
1916
- const lastCommand = entries[entries.length - 1].command;
1917
- // In a real implementation, this would open an editor
1918
- return {
1919
- stdout: `Would edit: ${lastCommand}`,
1920
- stderr: '',
1921
- exitCode: 0,
1922
- success: true,
1923
- };
1924
- }
1925
- return {
1926
- stdout: '',
1927
- stderr: 'fc: not fully implemented',
1928
- exitCode: 1,
1929
- success: false,
1930
- };
1931
- }
1932
- async builtin_r(_args) {
1933
- // Repeat last command
1934
- const entries = this.context.history.getAllEntries();
1935
- if (entries.length === 0) {
1936
- return {
1937
- stdout: '',
1938
- stderr: 'r: no history',
1939
- exitCode: 1,
1940
- success: false,
1941
- };
1942
- }
1943
- const lastCommand = entries[entries.length - 1].command;
1944
- try {
1945
- const { parseShellCommand } = await import('./shell-parser.js');
1946
- const ast = parseShellCommand(lastCommand);
1947
- return await this.execute(ast);
1948
- }
1949
- catch (error) {
1950
- return {
1951
- stdout: '',
1952
- stderr: `r: ${error.message}`,
1953
- exitCode: 1,
1954
- success: false,
1955
- };
1956
- }
1957
- }
1958
- async builtin_alias(args) {
1959
- if (args.length === 0) {
1960
- // List all aliases
1961
- const aliases = Object.entries(this.context.variables)
1962
- .filter(([key, _value]) => key.startsWith('alias_'))
1963
- .map(([key, value]) => `${key.substring(6)}='${value}'`)
1964
- .join('\n');
1965
- return {
1966
- stdout: aliases,
1967
- stderr: '',
1968
- exitCode: 0,
1969
- success: true,
1970
- };
1971
- }
1972
- // Set alias
1973
- const aliasStr = args.join(' ');
1974
- const equalIndex = aliasStr.indexOf('=');
1975
- if (equalIndex === -1) {
1976
- return {
1977
- stdout: '',
1978
- stderr: 'alias: invalid syntax',
1979
- exitCode: 1,
1980
- success: false,
1981
- };
1982
- }
1983
- const name = aliasStr.substring(0, equalIndex);
1984
- const value = aliasStr.substring(equalIndex + 1);
1985
- this.context.variables[`alias_${name}`] = value;
1986
- this.updateExpander();
1987
- return {
1988
- stdout: '',
1989
- stderr: '',
1990
- exitCode: 0,
1991
- success: true,
1992
- };
1993
- }
1994
- async builtin_unalias(args) {
1995
- if (args.length === 0) {
1996
- return {
1997
- stdout: '',
1998
- stderr: 'unalias: missing argument',
1999
- exitCode: 1,
2000
- success: false,
2001
- };
2002
- }
2003
- for (const aliasName of args) {
2004
- delete this.context.variables[`alias_${aliasName}`];
2005
- }
2006
- this.updateExpander();
2007
- return {
2008
- stdout: '',
2009
- stderr: '',
2010
- exitCode: 0,
2011
- success: true,
2012
- };
2013
- }
2014
- // Enhanced parameter expansion with ZSH features
2015
- async expandParameterWithZshFeatures(paramExpr) {
2016
- // Try extended parameter expansion first
2017
- try {
2018
- return this.extendedExpander.expandParameter(paramExpr);
2019
- }
2020
- catch (_error) {
2021
- // Fall back to regular parameter expansion
2022
- return this.expander.expandParameterExpression(paramExpr);
2023
- }
2024
- }
2025
- // Enhanced globbing with ZSH features
2026
- async expandPathnamesWithZshFeatures(pattern) {
2027
- // Check if extended globbing is enabled
2028
- if (this.context.zshOptions.isOptionSet('EXTENDED_GLOB')) {
2029
- try {
2030
- return await this.extendedGlobber.expandPattern(pattern, {
2031
- cwd: this.context.cwd,
2032
- includeHidden: this.context.zshOptions.isOptionSet('GLOB_DOTS'),
2033
- extendedGlob: true,
2034
- });
2035
- }
2036
- catch (_error) {
2037
- // Fall back to regular globbing
2038
- }
2039
- }
2040
- // Use regular pathname expansion
2041
- return await this.pathExpander.expandPathnames(pattern, {
2042
- cwd: this.context.cwd,
2043
- includeHidden: this.context.zshOptions.isOptionSet('GLOB_DOTS'),
2044
- });
2045
- }
2046
- // Enhanced arithmetic expansion with floating point
2047
- evaluateArithmeticWithFloatingPoint(expression) {
2048
- try {
2049
- const result = this.context.floatingPoint.evaluate(expression);
2050
- return result.toString();
2051
- }
2052
- catch (_error) {
2053
- // Fall back to integer arithmetic
2054
- return this.expander.evaluateArithmeticExpression(expression).toString();
2055
- }
2056
- }
2057
- // Get completions for current context
2058
- async getCompletions(command, args, currentWord, wordIndex) {
2059
- const context = {
2060
- command,
2061
- args,
2062
- currentWord,
2063
- wordIndex,
2064
- cwd: this.context.cwd,
2065
- env: this.context.env,
2066
- };
2067
- const candidates = await this.context.completion.getCompletions(context);
2068
- return candidates.map(c => c.word);
2069
- }
2070
- // Register completion function
2071
- registerCompletion(command, func) {
2072
- this.context.completion.registerCompletion(command, func);
2073
- }
2074
- // Add command to history
2075
- addToHistory(command, exitCode) {
2076
- this.context.history.addCommand(command, exitCode);
2077
- }
2078
- // Get all history entries
2079
- getHistoryEntries() {
2080
- return this.context.history.getAllEntries();
2081
- }
2082
- // Set positional parameters
2083
- setPositionalParams(params) {
2084
- this.context.positionalParams = params;
2085
- }
2086
- // Get ZSH compatibility instance
2087
- getZshCompatibility() {
2088
- return this.context.zshCompatibility;
2089
- }
2090
- // Get current prompt
2091
- getPrompt() {
2092
- return this.context.prompt.getCurrentPrompt({
2093
- user: process.env.USER || 'user',
2094
- host: process.env.HOSTNAME || 'localhost',
2095
- cwd: this.context.cwd,
2096
- home: process.env.HOME || '/',
2097
- exitCode: this.context.lastExitCode,
2098
- jobCount: this.context.jobControl.jobs.size,
2099
- time: new Date(),
2100
- });
2101
- }
2102
- // Get current right prompt
2103
- getRPrompt() {
2104
- return this.context.prompt.getCurrentRPrompt({
2105
- user: process.env.USER || 'user',
2106
- host: process.env.HOSTNAME || 'localhost',
2107
- cwd: this.context.cwd,
2108
- home: process.env.HOME || '/',
2109
- exitCode: this.context.lastExitCode,
2110
- jobCount: this.context.jobControl.jobs.size,
2111
- time: new Date(),
2112
- });
2113
- }
2114
- // ZSH Compatibility Built-in Commands
2115
- async builtin_source(args) {
2116
- if (args.length === 0) {
2117
- return {
2118
- stdout: '',
2119
- stderr: 'source: missing filename',
2120
- exitCode: 1,
2121
- success: false,
2122
- };
2123
- }
2124
- const filename = args[0];
2125
- try {
2126
- const fs = await import('fs');
2127
- const content = fs.readFileSync(filename, 'utf8');
2128
- const lines = content.split('\n');
2129
- for (const line of lines) {
2130
- const trimmed = line.trim();
2131
- if (trimmed.startsWith('#') || trimmed === '') {
2132
- continue;
2133
- }
2134
- try {
2135
- const ast = parseShellCommand(trimmed);
2136
- await this.execute(ast);
2137
- }
2138
- catch (error) {
2139
- console.error(`Error in ${filename}: ${error.message}`);
2140
- }
2141
- }
2142
- return {
2143
- stdout: '',
2144
- stderr: '',
2145
- exitCode: 0,
2146
- success: true,
2147
- };
2148
- }
2149
- catch (error) {
2150
- return {
2151
- stdout: '',
2152
- stderr: `source: ${filename}: ${error.message}`,
2153
- exitCode: 1,
2154
- success: false,
2155
- };
2156
- }
2157
- }
2158
- async builtin_install(args) {
2159
- if (args.length === 0) {
2160
- return {
2161
- stdout: '',
2162
- stderr: 'install: missing package name',
2163
- exitCode: 1,
2164
- success: false,
2165
- };
2166
- }
2167
- const packageName = args[0];
2168
- const result = await this.context.zshCompatibility.installPackage(packageName);
2169
- return {
2170
- stdout: result.message,
2171
- stderr: '',
2172
- exitCode: result.success ? 0 : 1,
2173
- success: result.success,
2174
- };
2175
- }
2176
- async builtin_uninstall(args) {
2177
- if (args.length === 0) {
2178
- return {
2179
- stdout: '',
2180
- stderr: 'uninstall: missing package name',
2181
- exitCode: 1,
2182
- success: false,
2183
- };
2184
- }
2185
- const packageName = args[0];
2186
- const result = await this.context.zshCompatibility.uninstallPackage(packageName);
2187
- return {
2188
- stdout: result.message,
2189
- stderr: '',
2190
- exitCode: result.success ? 0 : 1,
2191
- success: result.success,
2192
- };
2193
- }
2194
- async builtin_zsh_migrate(_args) {
2195
- const result = await this.context.zshCompatibility.migrateZshConfig();
2196
- return {
2197
- stdout: result.message,
2198
- stderr: '',
2199
- exitCode: result.success ? 0 : 1,
2200
- success: result.success,
2201
- };
2202
- }
2203
- async builtin_zsh_source(_args) {
2204
- const result = await this.context.zshCompatibility.sourceZshConfig();
2205
- return {
2206
- stdout: result.message,
2207
- stderr: '',
2208
- exitCode: result.success ? 0 : 1,
2209
- success: result.success,
2210
- };
2211
- }
2212
- /**
2213
- * readonly - Make variables read-only
2214
- * Usage: readonly [name[=value]]...
2215
- */
2216
- async builtin_readonly(args) {
2217
- // Initialize readonly set if not exists
2218
- if (!this.context.readonlyVariables) {
2219
- this.context.readonlyVariables = new Set();
2220
- }
2221
- const readonlyVars = this.context.readonlyVariables;
2222
- if (args.length === 0) {
2223
- // List all readonly variables
2224
- let output = '';
2225
- for (const name of readonlyVars) {
2226
- const value = this.context.variables[name] ?? this.context.env[name] ?? '';
2227
- output += `readonly ${name}=${value}\n`;
2228
- }
2229
- return { stdout: output, stderr: '', exitCode: 0, success: true };
2230
- }
2231
- // Make variables readonly
2232
- for (const arg of args) {
2233
- if (arg.includes('=')) {
2234
- const [name, value] = arg.split('=', 2);
2235
- if (readonlyVars.has(name)) {
2236
- return {
2237
- stdout: '',
2238
- stderr: `readonly: ${name}: readonly variable\n`,
2239
- exitCode: 1,
2240
- success: false,
2241
- };
2242
- }
2243
- this.context.variables[name] = value;
2244
- readonlyVars.add(name);
2245
- }
2246
- else {
2247
- if (readonlyVars.has(arg)) {
2248
- return {
2249
- stdout: '',
2250
- stderr: `readonly: ${arg}: readonly variable\n`,
2251
- exitCode: 1,
2252
- success: false,
2253
- };
2254
- }
2255
- readonlyVars.add(arg);
2256
- }
2257
- }
2258
- return { stdout: '', stderr: '', exitCode: 0, success: true };
2259
- }
2260
- /**
2261
- * type - Display command type
2262
- * Usage: type name...
2263
- */
2264
- async builtin_type(args) {
2265
- if (args.length === 0) {
2266
- return {
2267
- stdout: '',
2268
- stderr: 'type: usage: type name [name ...]\n',
2269
- exitCode: 1,
2270
- success: false,
2271
- };
2272
- }
2273
- let output = '';
2274
- let hasError = false;
2275
- for (const name of args) {
2276
- // Check if it's a built-in
2277
- const builtinResult = await this.executeBuiltin(name, []);
2278
- if (builtinResult !== null) {
2279
- output += `${name} is a shell builtin\n`;
2280
- continue;
2281
- }
2282
- // Check if it's a function
2283
- if (this.context.functions[name]) {
2284
- output += `${name} is a function\n`;
2285
- continue;
2286
- }
2287
- // Check if it's an alias
2288
- const aliasKey = `alias_${name}`;
2289
- if (this.context.variables[aliasKey]) {
2290
- const aliasValue = this.context.variables[aliasKey];
2291
- output += `${name} is aliased to \`${aliasValue}'\n`;
2292
- continue;
2293
- }
2294
- // Check if it's an external command
2295
- try {
2296
- const which = await import('child_process');
2297
- const result = await new Promise((resolve) => {
2298
- which.exec(`which ${name}`, (error, stdout) => {
2299
- resolve(error ? '' : stdout.trim());
2300
- });
2301
- });
2302
- if (result) {
2303
- output += `${name} is ${result}\n`;
2304
- }
2305
- else {
2306
- output += `${name}: not found\n`;
2307
- hasError = true;
2308
- }
2309
- }
2310
- catch {
2311
- output += `${name}: not found\n`;
2312
- hasError = true;
2313
- }
2314
- }
2315
- return {
2316
- stdout: output,
2317
- stderr: '',
2318
- exitCode: hasError ? 1 : 0,
2319
- success: !hasError,
2320
- };
2321
- }
2322
- /**
2323
- * hash - Remember command locations
2324
- * Usage: hash [-lr] [name...]
2325
- */
2326
- async builtin_hash(args) {
2327
- // Initialize command hash table if not exists
2328
- if (!this.context.commandHash) {
2329
- this.context.commandHash = new Map();
2330
- }
2331
- const commandHash = this.context.commandHash;
2332
- // Parse options
2333
- let listAll = false;
2334
- let remove = false;
2335
- const names = [];
2336
- for (const arg of args) {
2337
- if (arg === '-l') {
2338
- listAll = true;
2339
- }
2340
- else if (arg === '-r') {
2341
- remove = true;
2342
- }
2343
- else if (!arg.startsWith('-')) {
2344
- names.push(arg);
2345
- }
2346
- }
2347
- // Remove all entries
2348
- if (remove && names.length === 0) {
2349
- commandHash.clear();
2350
- return { stdout: '', stderr: '', exitCode: 0, success: true };
2351
- }
2352
- // Remove specific entries
2353
- if (remove && names.length > 0) {
2354
- for (const name of names) {
2355
- commandHash.delete(name);
2356
- }
2357
- return { stdout: '', stderr: '', exitCode: 0, success: true };
2358
- }
2359
- // List all commands
2360
- if (listAll || names.length === 0) {
2361
- let output = '';
2362
- for (const [name, path] of commandHash.entries()) {
2363
- output += `${name}\t${path}\n`;
2364
- }
2365
- return { stdout: output, stderr: '', exitCode: 0, success: true };
2366
- }
2367
- // Hash specific commands
2368
- for (const name of names) {
2369
- try {
2370
- const which = await import('child_process');
2371
- const result = await new Promise((resolve) => {
2372
- which.exec(`which ${name}`, (error, stdout) => {
2373
- resolve(error ? '' : stdout.trim());
2374
- });
2375
- });
2376
- if (result) {
2377
- commandHash.set(name, result);
2378
- }
2379
- else {
2380
- return {
2381
- stdout: '',
2382
- stderr: `hash: ${name}: not found\n`,
2383
- exitCode: 1,
2384
- success: false,
2385
- };
2386
- }
2387
- }
2388
- catch (_error) {
2389
- return {
2390
- stdout: '',
2391
- stderr: `hash: ${name}: not found\n`,
2392
- exitCode: 1,
2393
- success: false,
2394
- };
2395
- }
2396
- }
2397
- return { stdout: '', stderr: '', exitCode: 0, success: true };
2398
- }
2399
- /**
2400
- * kill - Send signal to process
2401
- * Usage: kill [-s sigspec | -sigspec] pid...
2402
- */
2403
- async builtin_kill(args) {
2404
- if (args.length === 0) {
2405
- return {
2406
- stdout: '',
2407
- stderr: 'kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ...\n',
2408
- exitCode: 1,
2409
- success: false,
2410
- };
2411
- }
2412
- let signal = 'SIGTERM';
2413
- const pids = [];
2414
- for (let i = 0; i < args.length; i++) {
2415
- const arg = args[i];
2416
- if (arg === '-s') {
2417
- // -s SIGNAL
2418
- if (i + 1 < args.length) {
2419
- signal = args[++i];
2420
- }
2421
- }
2422
- else if (arg === '-l') {
2423
- // List signals
2424
- return {
2425
- stdout: 'HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM\n',
2426
- stderr: '',
2427
- exitCode: 0,
2428
- success: true,
2429
- };
2430
- }
2431
- else if (arg.startsWith('-') && arg.length > 1) {
2432
- // -SIGNAL format
2433
- signal = arg.substring(1);
2434
- if (signal.match(/^\d+$/)) {
2435
- // Numeric signal, convert to name (simplified)
2436
- const sigNum = parseInt(signal);
2437
- if (sigNum === 9)
2438
- signal = 'SIGKILL';
2439
- else if (sigNum === 15)
2440
- signal = 'SIGTERM';
2441
- else
2442
- signal = `SIG${sigNum}`;
2443
- }
2444
- else if (!signal.startsWith('SIG')) {
2445
- signal = `SIG${signal}`;
2446
- }
2447
- }
2448
- else {
2449
- // PID or job spec
2450
- if (arg.startsWith('%')) {
2451
- // Job spec - look up in job control
2452
- const jobId = arg.substring(1);
2453
- const job = this.context.jobControl.jobs.get(parseInt(jobId));
2454
- if (job && job.pid) {
2455
- pids.push(job.pid);
2456
- }
2457
- else {
2458
- return {
2459
- stdout: '',
2460
- stderr: `kill: ${arg}: no such job\n`,
2461
- exitCode: 1,
2462
- success: false,
2463
- };
2464
- }
2465
- }
2466
- else {
2467
- const pid = parseInt(arg);
2468
- if (isNaN(pid)) {
2469
- return {
2470
- stdout: '',
2471
- stderr: `kill: ${arg}: arguments must be process or job IDs\n`,
2472
- exitCode: 1,
2473
- success: false,
2474
- };
2475
- }
2476
- pids.push(pid);
2477
- }
2478
- }
2479
- }
2480
- if (pids.length === 0) {
2481
- return {
2482
- stdout: '',
2483
- stderr: 'kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ...\n',
2484
- exitCode: 1,
2485
- success: false,
2486
- };
2487
- }
2488
- // Send signal to all PIDs
2489
- for (const pid of pids) {
2490
- try {
2491
- process.kill(pid, signal);
2492
- }
2493
- catch (error) {
2494
- return {
2495
- stdout: '',
2496
- stderr: `kill: (${pid}) - ${error.message}\n`,
2497
- exitCode: 1,
2498
- success: false,
2499
- };
2500
- }
2501
- }
2502
- return { stdout: '', stderr: '', exitCode: 0, success: true };
2503
- }
2504
- }