|
5 | 5 | "bytes" |
6 | 6 | "context" |
7 | 7 | "encoding/json" |
| 8 | + "errors" |
8 | 9 | "fmt" |
9 | 10 | "io" |
10 | 11 | "os" |
@@ -527,6 +528,70 @@ func spawnStdio(ctx context.Context, spec *CommandSpec, pluginDir string, req ma |
527 | 528 | } |
528 | 529 | } |
529 | 530 |
|
| 531 | + // --- Windows fallback: if shebang not detected/usable, run by extension --- |
| 532 | + // On Windows, shebang is not a reliable execution mechanism. |
| 533 | + // If entry is a script file, wrap it with an interpreter from PATH. |
| 534 | + if runtime.GOOS == "windows" && !useInterpreter { |
| 535 | + low := strings.ToLower(entry) |
| 536 | + |
| 537 | + switch { |
| 538 | + case strings.HasSuffix(low, ".py"): |
| 539 | + // Prefer python3, fallback python. |
| 540 | + if p, e := exec.LookPath("python3"); e == nil { |
| 541 | + interp = p |
| 542 | + } else if p, e := exec.LookPath("python"); e == nil { |
| 543 | + interp = p |
| 544 | + } else { |
| 545 | + writeLogLine(w, map[string]interface{}{ |
| 546 | + "level": "error", |
| 547 | + "message": "python not found in PATH (required for .py stdio scripts on Windows)", |
| 548 | + "entry": entry, |
| 549 | + }) |
| 550 | + return map[string]interface{}{ |
| 551 | + "status": "error", |
| 552 | + "message": "python not found in PATH (required for .py stdio scripts on Windows)", |
| 553 | + }, 1 |
| 554 | + } |
| 555 | + useInterpreter = true |
| 556 | + scriptAbs = entry |
| 557 | + |
| 558 | + case strings.HasSuffix(low, ".js"): |
| 559 | + if p, e := exec.LookPath("node"); e == nil { |
| 560 | + interp = p |
| 561 | + useInterpreter = true |
| 562 | + scriptAbs = entry |
| 563 | + } else { |
| 564 | + writeLogLine(w, map[string]interface{}{ |
| 565 | + "level": "error", |
| 566 | + "message": "node not found in PATH (required for .js stdio scripts on Windows)", |
| 567 | + "entry": entry, |
| 568 | + }) |
| 569 | + return map[string]interface{}{ |
| 570 | + "status": "error", |
| 571 | + "message": "node not found in PATH (required for .js stdio scripts on Windows)", |
| 572 | + }, 1 |
| 573 | + } |
| 574 | + |
| 575 | + case strings.HasSuffix(low, ".sh"): |
| 576 | + // Optional: if user has Git-Bash/WSL bash in PATH. |
| 577 | + if p, e := exec.LookPath("bash"); e == nil { |
| 578 | + interp = p |
| 579 | + useInterpreter = true |
| 580 | + scriptAbs = entry |
| 581 | + } else { |
| 582 | + writeLogLine(w, map[string]interface{}{ |
| 583 | + "level": "error", |
| 584 | + "message": "bash not found in PATH (required for .sh scripts on Windows)", |
| 585 | + "entry": entry, |
| 586 | + }) |
| 587 | + return map[string]interface{}{ |
| 588 | + "status": "error", |
| 589 | + "message": "bash not found in PATH (required for .sh scripts on Windows)", |
| 590 | + }, 1 |
| 591 | + } |
| 592 | + } |
| 593 | + } |
| 594 | + |
530 | 595 | // If no interpreter was resolved from shebang, pick one by file extension. |
531 | 596 | // This keeps stdio scripts runnable even without a shebang line. |
532 | 597 | if !useInterpreter { |
@@ -792,3 +857,50 @@ func copyFile(src, dst string) error { |
792 | 857 | _, err = io.Copy(out, in) |
793 | 858 | return err |
794 | 859 | } |
| 860 | + |
| 861 | +func normalizeProgramForOS(spec *CommandSpec) (string, []string, error) { |
| 862 | + prog := spec.Program |
| 863 | + args := append([]string{}, spec.Args...) |
| 864 | + |
| 865 | + // Only apply on Windows. Unix can execute shebang scripts directly. |
| 866 | + if runtime.GOOS != "windows" { |
| 867 | + return prog, args, nil |
| 868 | + } |
| 869 | + |
| 870 | + low := strings.ToLower(strings.TrimSpace(prog)) |
| 871 | + |
| 872 | + // If program is plugin-relative like ./scripts/a.py, keep it as-is, exec.Command can handle it |
| 873 | + // but Windows can't execute .py directly, so we wrap it with python. |
| 874 | + switch { |
| 875 | + case strings.HasSuffix(low, ".py"): |
| 876 | + // Prefer python3, fallback python. |
| 877 | + py := "" |
| 878 | + if p, err := exec.LookPath("python3"); err == nil { |
| 879 | + py = p |
| 880 | + } else if p, err := exec.LookPath("python"); err == nil { |
| 881 | + py = p |
| 882 | + } else { |
| 883 | + return "", nil, errors.New("python not found in PATH (required to run .py stdio scripts on Windows)") |
| 884 | + } |
| 885 | + // python <script> <args...> |
| 886 | + return py, append([]string{prog}, args...), nil |
| 887 | + |
| 888 | + case strings.HasSuffix(low, ".js"): |
| 889 | + node, err := exec.LookPath("node") |
| 890 | + if err != nil { |
| 891 | + return "", nil, errors.New("node not found in PATH (required to run .js stdio scripts on Windows)") |
| 892 | + } |
| 893 | + return node, append([]string{prog}, args...), nil |
| 894 | + |
| 895 | + case strings.HasSuffix(low, ".sh"): |
| 896 | + // If user has Git-Bash or WSL bash in PATH, allow it. |
| 897 | + bash, err := exec.LookPath("bash") |
| 898 | + if err != nil { |
| 899 | + return "", nil, errors.New("bash not found in PATH (required to run .sh scripts on Windows)") |
| 900 | + } |
| 901 | + return bash, append([]string{prog}, args...), nil |
| 902 | + } |
| 903 | + |
| 904 | + // For exe/cmd/ps1, do nothing |
| 905 | + return prog, args, nil |
| 906 | +} |
0 commit comments