@xynogen/pix-runtime 0.10.1 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * user-shell.ts — pick the shell for user `!` commands, per OS.
3
+ *
4
+ * - Windows: PowerShell (Pi resolves pwsh 7, else powershell.exe 5.1). Pi's
5
+ * default is Git Bash, which is not what a Windows user types into `!`.
6
+ * - POSIX: $SHELL when it is not bash. zsh also needs rc + aliases: `zsh -c`
7
+ * is non-interactive and resolves aliases at parse time, so we source
8
+ * .zshrc and then `eval` the command to get a fresh parse boundary.
9
+ */
10
+
11
+ import { homeDir } from "./paths.ts";
12
+ import { currentPlatform, type HostPlatform } from "./platform.ts";
13
+
14
+ export type UserShell =
15
+ | { kind: "powershell"; wrap(command: string): string }
16
+ | { kind: "posix"; shellPath: string; wrap(command: string): string };
17
+
18
+ export interface UserShellOptions {
19
+ host?: HostPlatform;
20
+ shell?: string;
21
+ home?: string;
22
+ }
23
+
24
+ /** Guarded: `$PSStyle` exists only in PowerShell 7.2+. Strips ANSI color from output. */
25
+ // ponytail: duplicated from pix-powershell (Package Independence). One line, keep both in sync.
26
+ export const PLAIN_TEXT_PREAMBLE = "if ($PSStyle) { $PSStyle.OutputRendering = 'PlainText' }\n";
27
+
28
+ /** Single-quote a string for POSIX shell embedding. */
29
+ function quote(s: string): string {
30
+ return `'${s.replace(/'/g, "'\\''")}'`;
31
+ }
32
+
33
+ export function userShell(opts: UserShellOptions = {}): UserShell | undefined {
34
+ const host = opts.host ?? currentPlatform();
35
+ // ponytail: no opt-out back to Git Bash on Windows. Add a pix.json knob on request.
36
+ if (host.os === "win32") {
37
+ return { kind: "powershell", wrap: (command) => `${PLAIN_TEXT_PREAMBLE}${command}` };
38
+ }
39
+ const shell = "shell" in opts ? opts.shell : process.env.SHELL;
40
+ if (!shell) return undefined;
41
+ const name = shell.split("/").pop() ?? "";
42
+ if (name === "bash") return undefined;
43
+ if (name !== "zsh") return { kind: "posix", shellPath: shell, wrap: (command) => command };
44
+ // ponytail: only zsh gets rc wrapping. fish/nu run with shellPath alone. Add their rc logic on request.
45
+ const rc = quote(`${opts.home ?? homeDir()}/.zshrc`);
46
+ return {
47
+ kind: "posix",
48
+ shellPath: shell,
49
+ wrap: (command) => `. ${rc} 2>/dev/null; eval ${quote(command)}`,
50
+ };
51
+ }
package/src/which.ts CHANGED
@@ -16,15 +16,24 @@
16
16
  import { accessSync, constants, statSync } from "node:fs";
17
17
  import { access, stat } from "node:fs/promises";
18
18
  import { delimiter, isAbsolute, join, sep } from "node:path";
19
+ import { currentPlatform } from "./platform.ts";
19
20
 
20
- const isWindows = process.platform === "win32";
21
+ const isWindows = currentPlatform().os === "win32";
21
22
 
22
23
  /** Windows executable extensions, from `PATHEXT` with a sane fallback. */
23
24
  function pathExtensions(env: NodeJS.ProcessEnv): string[] {
24
25
  if (!isWindows) return [""];
25
26
  const raw = env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
26
- // Leading "" lets an already-suffixed name (foo.exe) match as-is.
27
- return ["", ...raw.split(";").filter(Boolean)];
27
+ // Leading "" lets an already-suffixed name (foo.exe) match as-is. Lowercased:
28
+ // NTFS matches either case, and the returned path should read `rtk.exe`, not
29
+ // `rtk.EXE`, since that is how the files are conventionally named.
30
+ return [
31
+ "",
32
+ ...raw
33
+ .split(";")
34
+ .filter(Boolean)
35
+ .map((ext) => ext.toLowerCase()),
36
+ ];
28
37
  }
29
38
 
30
39
  /** Directories to scan, from `PATH`. Windows also probes the current directory first. */
@@ -40,7 +49,15 @@ export interface FindExecutableOptions {
40
49
  }
41
50
 
42
51
  function candidatesFor(name: string, env: NodeJS.ProcessEnv): string[] {
43
- const exts = pathExtensions(env);
52
+ let exts = pathExtensions(env);
53
+ // Windows cannot spawn an extensionless file (npm ships a `npm` sh script
54
+ // beside `npm.cmd`), so only accept the bare name when it already carries a
55
+ // PATHEXT extension.
56
+ if (isWindows) {
57
+ const lower = name.toLowerCase();
58
+ const suffixed = exts.some((ext) => ext && lower.endsWith(ext));
59
+ exts = suffixed ? [""] : exts.filter(Boolean);
60
+ }
44
61
  const withExts = (base: string): string[] => exts.map((ext) => base + ext);
45
62
  // A name with a separator (or absolute) is a direct path — do not scan PATH.
46
63
  if (isAbsolute(name) || name.includes(sep) || (isWindows && name.includes("/"))) {