decorated-pi 0.7.2 → 0.7.3
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/README.md +18 -2
- package/commands/dp-settings.ts +3 -3
- package/hooks/compaction.ts +115 -85
- package/hooks/externalize.ts +48 -35
- package/hooks/rtk.ts +24 -97
- package/hooks/skeleton.ts +53 -18
- package/hooks/smart-at.ts +51 -7
- package/hooks/wakatime.ts +17 -30
- package/index.ts +15 -11
- package/package.json +1 -1
- package/settings.ts +159 -1
- package/tools/ask/index.ts +0 -3
- package/tools/lsp/servers.ts +31 -15
- package/tools/mcp/config.ts +34 -15
- package/tsconfig.json +1 -0
- package/ui/ask.ts +72 -22
- package/ui/module-settings.ts +166 -16
- package/utils/which.ts +109 -0
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
|
+
}
|