@oh-my-pi/pi-utils 17.1.6 → 17.1.8
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/CHANGELOG.md +10 -0
- package/dist/types/env.d.ts +4 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/process-name.d.ts +7 -0
- package/package.json +2 -2
- package/src/env.ts +110 -27
- package/src/index.ts +1 -0
- package/src/process-name.ts +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.1.8] - 2026-07-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `setProcessName` utility to set the OS-visible process name on Linux via `bun:ffi`, bypassing Bun's `process.title` limitations.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed child shell environment filtering to drop launch-directory `.env` values in addition to Bun-autoloaded `.env.local` values.
|
|
14
|
+
|
|
5
15
|
## [17.1.5] - 2026-07-27
|
|
6
16
|
|
|
7
17
|
### Fixed
|
package/dist/types/env.d.ts
CHANGED
|
@@ -17,13 +17,12 @@ export declare function isSafeEnvName(name: string): boolean;
|
|
|
17
17
|
export declare function isSafeEnvValue(value: string): boolean;
|
|
18
18
|
export declare function isMacosMallocStackLoggingEnvName(name: string): boolean;
|
|
19
19
|
export declare function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string>;
|
|
20
|
-
/** Filters process env for child shells without launch-cwd
|
|
20
|
+
/** Filters process env for child shells without launch-cwd dotenv values. */
|
|
21
21
|
export declare function filterChildShellEnv(env: Record<string, string | undefined>, cwd?: string): Record<string, string>;
|
|
22
22
|
/**
|
|
23
|
-
* Parses a .env file synchronously
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Returns an object of key-value pairs.
|
|
23
|
+
* Parses a .env file synchronously into key-value string pairs using
|
|
24
|
+
* {@link parseEnvLine} for Bun-compatible line semantics, then mirrors valid
|
|
25
|
+
* `OMP_` variables to their `PI_` aliases.
|
|
27
26
|
*/
|
|
28
27
|
export declare function parseEnvFile(filePath: string): Record<string, string>;
|
|
29
28
|
/**
|
package/dist/types/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from "./path.js";
|
|
|
19
19
|
export * from "./path-tree.js";
|
|
20
20
|
export * from "./peek-file.js";
|
|
21
21
|
export * as postmortem from "./postmortem.js";
|
|
22
|
+
export * from "./process-name.js";
|
|
22
23
|
export * as procmgr from "./procmgr.js";
|
|
23
24
|
export * as prompt from "./prompt.js";
|
|
24
25
|
export * as ptree from "./ptree.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set both the JS `process.title` and — on Linux — the kernel `comm` name.
|
|
3
|
+
*
|
|
4
|
+
* Never throws: `bun:ffi` unavailability or a failed syscall degrades silently
|
|
5
|
+
* to the `process.title`-only behavior, so it is safe to call at startup.
|
|
6
|
+
*/
|
|
7
|
+
export declare function setProcessName(name: string): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "17.1.
|
|
4
|
+
"version": "17.1.8",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "17.1.
|
|
34
|
+
"@oh-my-pi/pi-natives": "17.1.8",
|
|
35
35
|
"handlebars": "^4.7.9",
|
|
36
36
|
"winston": "^3.19.0",
|
|
37
37
|
"winston-daily-rotate-file": "^5.0.0"
|
package/src/env.ts
CHANGED
|
@@ -52,50 +52,132 @@ export function filterProcessEnv(env: Record<string, string | undefined>): Recor
|
|
|
52
52
|
}
|
|
53
53
|
return result;
|
|
54
54
|
}
|
|
55
|
+
// Bun autoloads the project's dotenv files into `process.env` before user code
|
|
56
|
+
// runs — including inside `bun build --compile` binaries — so a snapshot of
|
|
57
|
+
// `Bun.env` is only pre-dotenv when autoloading was explicitly disabled. Linux
|
|
58
|
+
// keeps the original exec environment in procfs, which is authoritative.
|
|
59
|
+
function readLaunchEnv(): ReadonlyMap<string, string> | undefined {
|
|
60
|
+
if (process.platform === "linux") {
|
|
61
|
+
try {
|
|
62
|
+
const values = new Map<string, string>();
|
|
63
|
+
for (const entry of fs.readFileSync("/proc/self/environ", "utf8").split("\0")) {
|
|
64
|
+
const separator = entry.indexOf("=");
|
|
65
|
+
if (separator > 0) values.set(entry.slice(0, separator), entry.slice(separator + 1));
|
|
66
|
+
}
|
|
67
|
+
return values;
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
if (!process.execArgv.includes("--no-env-file")) return undefined;
|
|
71
|
+
const values = new Map<string, string>();
|
|
72
|
+
for (const key in Bun.env) {
|
|
73
|
+
const value = Bun.env[key];
|
|
74
|
+
if (value !== undefined) values.set(key, value);
|
|
75
|
+
}
|
|
76
|
+
return values;
|
|
77
|
+
}
|
|
55
78
|
|
|
56
|
-
|
|
79
|
+
const launchEnvValues = readLaunchEnv();
|
|
80
|
+
const projectEnvNamesLoadedByOmp = new Set<string>();
|
|
81
|
+
|
|
82
|
+
function expandDotenvValues(values: Record<string, string>, env: Record<string, string>): Record<string, string> {
|
|
83
|
+
const expanded: Record<string, string> = {};
|
|
84
|
+
for (const key in values) {
|
|
85
|
+
expanded[key] = values[key].replace(
|
|
86
|
+
/(\\)?\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g,
|
|
87
|
+
(match, escaped: string | undefined, braced: string | undefined, bare: string | undefined) => {
|
|
88
|
+
if (escaped) return match.slice(1);
|
|
89
|
+
const name = braced ?? bare;
|
|
90
|
+
if (!name) return match;
|
|
91
|
+
return env[name] ?? expanded[name] ?? "";
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return expanded;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Filters process env for child shells without launch-cwd dotenv values. */
|
|
57
99
|
export function filterChildShellEnv(
|
|
58
100
|
env: Record<string, string | undefined>,
|
|
59
101
|
cwd: string = process.cwd(),
|
|
60
102
|
): Record<string, string> {
|
|
61
103
|
const result = filterProcessEnv(env);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
104
|
+
const projectEnv = parseEnvFile(path.join(cwd, ".env"));
|
|
105
|
+
const nodeEnvName = `.env.${env.NODE_ENV || "development"}`;
|
|
106
|
+
const modeEnv = parseEnvFile(path.join(cwd, nodeEnvName));
|
|
107
|
+
const localEnv = parseEnvFile(path.join(cwd, ".env.local"));
|
|
108
|
+
const launchEnv = { ...projectEnv, ...modeEnv, ...localEnv };
|
|
109
|
+
const expandedLaunchEnv = {
|
|
110
|
+
...expandDotenvValues(projectEnv, result),
|
|
111
|
+
...expandDotenvValues(modeEnv, result),
|
|
112
|
+
...expandDotenvValues(localEnv, result),
|
|
113
|
+
};
|
|
114
|
+
for (const key in launchEnv) {
|
|
115
|
+
const launchValue = launchEnvValues?.get(key);
|
|
116
|
+
if (launchValue !== undefined) {
|
|
117
|
+
// Launcher-owned name: it keeps the launcher's own value. Bun overwrites
|
|
118
|
+
// an empty launcher value with the dotenv one, so restore the launcher
|
|
119
|
+
// value whenever what survived is exactly what the dotenv file defines.
|
|
120
|
+
if (
|
|
121
|
+
result[key] !== launchValue &&
|
|
122
|
+
(result[key] === launchEnv[key] || result[key] === expandedLaunchEnv[key])
|
|
123
|
+
) {
|
|
124
|
+
result[key] = launchValue;
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (launchEnvValues || projectEnvNamesLoadedByOmp.has(key)) {
|
|
129
|
+
// Strong provenance: the launch environment is known and this name is
|
|
130
|
+
// absent from it, or OMP itself injected the value — either way it came
|
|
131
|
+
// from a project dotenv file, not the parent shell.
|
|
132
|
+
delete result[key];
|
|
133
|
+
} else if (result[key] === launchEnv[key] || result[key] === expandedLaunchEnv[key]) {
|
|
134
|
+
// No launch-env snapshot (dotenv autoloaded without procfs): best-effort
|
|
135
|
+
// value match against the Bun-parsed dotenv.
|
|
136
|
+
delete result[key];
|
|
137
|
+
}
|
|
65
138
|
}
|
|
66
139
|
return result;
|
|
67
140
|
}
|
|
68
141
|
|
|
69
142
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* Returns
|
|
143
|
+
* Parse one dotenv line with Bun-compatible semantics: an optional `export`
|
|
144
|
+
* prefix, full-line `#` comments, inline `#` comments after whitespace on
|
|
145
|
+
* unquoted values, and single/double/backtick quoting (a `#` inside quotes
|
|
146
|
+
* stays literal). Returns undefined for blank lines, comments, and malformed
|
|
147
|
+
* names.
|
|
148
|
+
*/
|
|
149
|
+
function parseEnvLine(line: string): { key: string; value: string } | undefined {
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
if (!trimmed || trimmed.startsWith("#")) return undefined;
|
|
152
|
+
const eqIndex = trimmed.indexOf("=");
|
|
153
|
+
if (eqIndex === -1) return undefined;
|
|
154
|
+
let key = trimmed.slice(0, eqIndex).trim();
|
|
155
|
+
const exported = key.match(/^export[ \t]+(.*)$/);
|
|
156
|
+
if (exported) key = exported[1].trim();
|
|
157
|
+
if (!isValidEnvName(key)) return undefined;
|
|
158
|
+
const raw = trimmed.slice(eqIndex + 1).replace(/^[ \t]+/, "");
|
|
159
|
+
const quote = raw[0];
|
|
160
|
+
if (quote === '"' || quote === "'" || quote === "`") {
|
|
161
|
+
let close = raw.indexOf(quote, 1);
|
|
162
|
+
while (close !== -1 && raw[close - 1] === "\\") close = raw.indexOf(quote, close + 1);
|
|
163
|
+
return { key, value: close === -1 ? raw.slice(1) : raw.slice(1, close) };
|
|
164
|
+
}
|
|
165
|
+
const commentIndex = raw.search(/[ \t]#/);
|
|
166
|
+
return { key, value: (commentIndex === -1 ? raw : raw.slice(0, commentIndex)).trimEnd() };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parses a .env file synchronously into key-value string pairs using
|
|
171
|
+
* {@link parseEnvLine} for Bun-compatible line semantics, then mirrors valid
|
|
172
|
+
* `OMP_` variables to their `PI_` aliases.
|
|
74
173
|
*/
|
|
75
174
|
export function parseEnvFile(filePath: string): Record<string, string> {
|
|
76
175
|
const result: Record<string, string> = {};
|
|
77
176
|
try {
|
|
78
177
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
79
178
|
for (const line of content.split("\n")) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
83
|
-
|
|
84
|
-
const eqIndex = trimmed.indexOf("=");
|
|
85
|
-
if (eqIndex === -1) continue;
|
|
86
|
-
|
|
87
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
88
|
-
if (!isValidEnvName(key)) continue;
|
|
89
|
-
|
|
90
|
-
let value = trimmed.slice(eqIndex + 1).trim();
|
|
91
|
-
|
|
92
|
-
// Remove surrounding quotes (" or ')
|
|
93
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
94
|
-
value = value.slice(1, -1);
|
|
95
|
-
}
|
|
96
|
-
if (!isSafeEnvValue(value)) continue;
|
|
97
|
-
|
|
98
|
-
result[key] = value;
|
|
179
|
+
const parsed = parseEnvLine(line);
|
|
180
|
+
if (parsed && isSafeEnvValue(parsed.value)) result[parsed.key] = parsed.value;
|
|
99
181
|
}
|
|
100
182
|
} catch {
|
|
101
183
|
// File doesn't exist or can't be read - return empty result
|
|
@@ -128,6 +210,7 @@ for (const file of [projectEnv, agentEnv, piEnv, homeEnv]) {
|
|
|
128
210
|
for (const key in file) {
|
|
129
211
|
if (!isMacosMallocStackLoggingEnvName(key) && !Bun.env[key]) {
|
|
130
212
|
Bun.env[key] = file[key];
|
|
213
|
+
if (file === projectEnv) projectEnvNamesLoadedByOmp.add(key);
|
|
131
214
|
}
|
|
132
215
|
}
|
|
133
216
|
}
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from "./path";
|
|
|
19
19
|
export * from "./path-tree";
|
|
20
20
|
export * from "./peek-file";
|
|
21
21
|
export * as postmortem from "./postmortem";
|
|
22
|
+
export * from "./process-name";
|
|
22
23
|
export * as procmgr from "./procmgr";
|
|
23
24
|
export * as prompt from "./prompt";
|
|
24
25
|
export * as ptree from "./ptree";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set the OS-visible process name (`/proc/self/comm`) so `omp` shows up as
|
|
3
|
+
* `omp` — not `bun` — in `ps`, `pgrep`, `killall`, `top`, `htop`, and systemd.
|
|
4
|
+
*
|
|
5
|
+
* Bun's `process.title` setter only stores the value on the JS side; unlike
|
|
6
|
+
* Node/libuv it never calls `prctl(PR_SET_NAME)`, so the kernel's `comm` stays
|
|
7
|
+
* `bun` and process-name-based tooling can't target omp (and `pkill bun` becomes
|
|
8
|
+
* a footgun that kills every Bun process on the machine). We keep the
|
|
9
|
+
* `process.title` assignment (correct getter, future-proof if Bun ever fixes the
|
|
10
|
+
* setter) and additionally drive `prctl` via `bun:ffi` on Linux, mirroring the
|
|
11
|
+
* libc-FFI pattern in `ttyid.ts` / `stderr-guard.ts`.
|
|
12
|
+
*
|
|
13
|
+
* macOS has no clean userspace equivalent for the shebang-run path, and on
|
|
14
|
+
* Windows / compiled binaries the kernel derives the name from the exec'd file,
|
|
15
|
+
* so those paths already report correctly; there we only set `process.title`.
|
|
16
|
+
*/
|
|
17
|
+
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
18
|
+
import * as os from "node:os";
|
|
19
|
+
|
|
20
|
+
/** `prctl(2)` option that sets the calling thread's `comm` name. */
|
|
21
|
+
const PR_SET_NAME = 15;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set both the JS `process.title` and — on Linux — the kernel `comm` name.
|
|
25
|
+
*
|
|
26
|
+
* Never throws: `bun:ffi` unavailability or a failed syscall degrades silently
|
|
27
|
+
* to the `process.title`-only behavior, so it is safe to call at startup.
|
|
28
|
+
*/
|
|
29
|
+
export function setProcessName(name: string): void {
|
|
30
|
+
try {
|
|
31
|
+
process.title = name;
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
if (os.platform() !== "linux") return;
|
|
35
|
+
|
|
36
|
+
// glibc first, then the generic soname for musl-style layouts (see stderr-guard.ts).
|
|
37
|
+
for (const soname of ["libc.so.6", "libc.so"]) {
|
|
38
|
+
try {
|
|
39
|
+
const libc = dlopen(soname, {
|
|
40
|
+
prctl: {
|
|
41
|
+
args: [FFIType.i32, FFIType.ptr, FFIType.u64, FFIType.u64, FFIType.u64],
|
|
42
|
+
returns: FFIType.i32,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
// TASK_COMM_LEN is 16 (name + NUL); the kernel truncates the rest.
|
|
47
|
+
const buf = Buffer.from(`${name}\0`, "utf8");
|
|
48
|
+
libc.symbols.prctl(PR_SET_NAME, ptr(buf), 0n, 0n, 0n);
|
|
49
|
+
} finally {
|
|
50
|
+
libc.close();
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
} catch {
|
|
54
|
+
// bun:ffi unavailable or this soname missing; try the next candidate.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|