decorated-pi 0.7.2 → 0.8.0

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.
package/utils/which.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Locate a binary by name. Pure Node — no shell required.
3
+ *
4
+ * Why no shell: the previous implementation called `bash -c "command -v X"`,
5
+ * which depended on `command` being a builtin in the user's shell. That's
6
+ * true for bash/zsh/sh/ash/dash, but not for nushell/fish/pwsh — so a user
7
+ * with `SHELL=/usr/bin/nu` saw the whole extension fail to start with
8
+ * "External command failed" errors from the `command` builtin lookup.
9
+ *
10
+ * Walking $PATH with fs.accessSync(X_OK) is universal:
11
+ * - No shell dependency
12
+ * - No builtin dependency
13
+ * - Works on minimal Linux (alpine, busybox, distroless)
14
+ * - Works regardless of $SHELL
15
+ * - No execFileSync overhead per call
16
+ *
17
+ * `extendPath` lets callers inject extra search locations (user-configured
18
+ * override paths, module-specific fallback dirs like ~/.wakatime, project
19
+ * node_modules/.bin). Each entry can be a file path (checked directly) or
20
+ * a directory (searched for `name` inside). extendPath entries are tried
21
+ * before $PATH, so callers control priority by ordering.
22
+ */
23
+ import { accessSync, constants, statSync } from "node:fs";
24
+ import { delimiter, resolve } from "node:path";
25
+ import { execFileSync } from "node:child_process";
26
+ import { homedir } from "node:os";
27
+
28
+ export interface WhichOptions {
29
+ /** Extra locations to search before $PATH. Each entry can be a file
30
+ * path (e.g. "/custom/bin/rtk") or a directory (e.g. "~/.wakatime").
31
+ * Tried in array order; first executable match wins. */
32
+ extendPath?: string[];
33
+ }
34
+
35
+ export function which(name: string, opts?: WhichOptions): string | null {
36
+ if (!name) return null;
37
+
38
+ // Absolute or relative path in name: check the file directly.
39
+ if (name.includes("/") || name.includes("\\")) {
40
+ try {
41
+ accessSync(name, constants.X_OK);
42
+ return resolve(name);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ if (process.platform === "win32") {
49
+ // Windows: `where` is a built-in command that handles PATHEXT
50
+ // (the per-user list of executable extensions — typically
51
+ // .COM;.EXE;.BAT;.CMD;.VBS;.JS;.WSC;.MSC;.PS1) and PATH lookup.
52
+ // We don't replicate that in pure Node; we defer to it.
53
+ try {
54
+ const out = execFileSync("where", [name], { encoding: "utf-8" }).trim();
55
+ return out.split(/\r?\n/)[0] || null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ // Build the search list: extendPath entries first, then $PATH dirs.
62
+ // extendPath entries can be files (checked directly) or directories
63
+ // (searched for `name` inside). We stat each to tell them apart.
64
+ const home = homedir();
65
+ const expandHome = (d: string): string => {
66
+ if (d === "~") return home;
67
+ if (d.startsWith("~/") || d.startsWith("~\\")) return home + d.slice(1);
68
+ return d;
69
+ };
70
+
71
+ const tryCandidate = (candidate: string): string | null => {
72
+ try {
73
+ accessSync(candidate, constants.X_OK);
74
+ return resolve(candidate);
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+
80
+ // 1. extendPath (caller-injected): override paths + module fallbacks.
81
+ for (const entry of opts?.extendPath ?? []) {
82
+ const expanded = expandHome(entry);
83
+ let isDir = false;
84
+ try {
85
+ isDir = statSync(expanded).isDirectory();
86
+ } catch {
87
+ // Doesn't exist or not accessible — skip.
88
+ continue;
89
+ }
90
+ const candidate = isDir ? resolve(expanded, name) : expanded;
91
+ const found = tryCandidate(candidate);
92
+ if (found) return found;
93
+ }
94
+
95
+ // 2. $PATH walk — mirrors the standard `which(1)` command's behavior:
96
+ // exact match in each directory, no recursion.
97
+ //
98
+ // `~` expansion: bash expands `~/bin` when sourced from .bashrc via
99
+ // `export PATH=~/bin:$PATH`, but not when PATH is set in launchd,
100
+ // systemd Environment=, Docker ENV, or GUI app launchers. We expand
101
+ // it here so we don't regress on those paths.
102
+ const dirs = (process.env.PATH || "").split(delimiter).map(expandHome);
103
+ for (const dir of dirs) {
104
+ if (!dir) continue;
105
+ const found = tryCandidate(resolve(dir, name));
106
+ if (found) return found;
107
+ }
108
+ return null;
109
+ }