@linxiraos/pi-utils 1.4.13 → 1.5.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/CHANGELOG.md +4 -0
- package/dist/types/dirs.d.ts +18 -10
- package/dist/types/env.d.ts +28 -0
- package/dist/types/postmortem.d.ts +11 -1
- package/package.json +3 -3
- package/src/dirs.ts +30 -16
- package/src/env.ts +85 -5
- package/src/json-parse.ts +8 -1
- package/src/postmortem.ts +45 -9
package/CHANGELOG.md
CHANGED
package/dist/types/dirs.d.ts
CHANGED
|
@@ -68,7 +68,8 @@ export declare function normalizePathForComparison(inputPath: string): string;
|
|
|
68
68
|
* work.
|
|
69
69
|
*/
|
|
70
70
|
export declare function relativePathWithinNormalizedRoot(normalizedRoot: string, normalizedCandidate: string): string | null;
|
|
71
|
-
|
|
71
|
+
/** `options` apply to `candidate` only. */
|
|
72
|
+
export declare function pathIsWithin(root: string, candidate: string, options?: EquivalentPathOptions): boolean;
|
|
72
73
|
export declare function relativePathWithinRoot(root: string, candidate: string): string | null;
|
|
73
74
|
/** Get the project directory. */
|
|
74
75
|
export declare function getProjectDir(): string;
|
|
@@ -76,15 +77,14 @@ export declare function getProjectDir(): string;
|
|
|
76
77
|
export declare function setProjectDir(dir: string): void;
|
|
77
78
|
/** Reset the cached project directory (test seam). */
|
|
78
79
|
export declare function __resetProjectDirCacheForTests(): void;
|
|
79
|
-
/** Whether a path is absent or not a directory
|
|
80
|
-
export declare function directoryIsMissing(dir: string
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
export declare function directoryExists(dir: string): Promise<boolean>;
|
|
80
|
+
/** Whether a path is absent or not a directory; `strict` propagates non-missing stat failures. */
|
|
81
|
+
export declare function directoryIsMissing(dir: string, options?: {
|
|
82
|
+
strict?: boolean;
|
|
83
|
+
}): Promise<boolean>;
|
|
84
|
+
/** Whether a path is an existing directory; `strict` propagates non-missing stat failures. */
|
|
85
|
+
export declare function directoryExists(dir: string, options?: {
|
|
86
|
+
strict?: boolean;
|
|
87
|
+
}): Promise<boolean>;
|
|
88
88
|
/**
|
|
89
89
|
* Whether `dir` both exists and can be entered. POSIX `stat` succeeds for a
|
|
90
90
|
* directory whose own search/execute permission is denied (it only needs
|
|
@@ -326,6 +326,14 @@ export declare function getNativeGrammarsDir(): string;
|
|
|
326
326
|
export declare function getNativesDir(): string;
|
|
327
327
|
/** Get the stats database path (~/.zeta/stats.db). */
|
|
328
328
|
export declare function getStatsDbPath(): string;
|
|
329
|
+
/**
|
|
330
|
+
* Get the global team database path (~/.zeta/data/zeta.db).
|
|
331
|
+
*
|
|
332
|
+
* One database for every team, PM, repo binding, engineer and worktree. It
|
|
333
|
+
* must never live inside a team root, a repository, or a workspace: markers
|
|
334
|
+
* on disk are discovery hints, this file is the authority.
|
|
335
|
+
*/
|
|
336
|
+
export declare function getZetaDbPath(): string;
|
|
329
337
|
/** Get the autoresearch state directory (~/.zeta/autoresearch). */
|
|
330
338
|
export declare function getAutoresearchDir(): string;
|
|
331
339
|
/** Get the per-project autoresearch state directory (~/.zeta/autoresearch/<encoded-project>). */
|
package/dist/types/env.d.ts
CHANGED
|
@@ -44,6 +44,34 @@ export declare function getDotenvEnvValues(cwd?: string, env?: Record<string, st
|
|
|
44
44
|
* only shell-identifier names and spawn-safe values.
|
|
45
45
|
*/
|
|
46
46
|
export declare function parseEnvFile(filePath: string): Record<string, string>;
|
|
47
|
+
/**
|
|
48
|
+
* @internal Loader seam, exported for tests only. Claims an `OTEL_*` entry from one of OMP's own dotenv files: the value is
|
|
49
|
+
* kept in a private map, never in the process environment. Returns `false` for
|
|
50
|
+
* any other name, which the caller handles as usual.
|
|
51
|
+
*
|
|
52
|
+
* Precedence matches the process-environment loader: a non-empty live value
|
|
53
|
+
* (shell export, project `.env`) wins, and an earlier non-empty file value wins
|
|
54
|
+
* over a later file.
|
|
55
|
+
*
|
|
56
|
+
* @param platform - Name-folding semantics; injectable for tests.
|
|
57
|
+
*/
|
|
58
|
+
export declare function claimOmpOtelEnvFileEntry(name: string, value: string, platform?: NodeJS.Platform): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* `OTEL_*` lookup for OMP's own telemetry: the live environment wins (a shell
|
|
61
|
+
* export or project `.env`), then OMP's own `.env` files. Use this instead of
|
|
62
|
+
* `process.env` for any `OTEL_*` setting OMP reads itself.
|
|
63
|
+
*/
|
|
64
|
+
export declare function $otelEnv(name: string): string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Runs `fn` with OMP's `.env`-sourced `OTEL_*` values temporarily present in
|
|
67
|
+
* `process.env` (only for names with no live value), then removes them again in
|
|
68
|
+
* a `finally`. For code that reads the environment itself, such as the
|
|
69
|
+
* OpenTelemetry SDK constructors.
|
|
70
|
+
*
|
|
71
|
+
* Synchronous only: awaiting inside would let other code, including child
|
|
72
|
+
* process spawns, run while the values are set.
|
|
73
|
+
*/
|
|
74
|
+
export declare function withOmpOtelEnv<T>(fn: () => T): T;
|
|
47
75
|
/**
|
|
48
76
|
* Intentional re-export of Bun.env.
|
|
49
77
|
*
|
|
@@ -168,12 +168,22 @@ export declare function isExpectedCleanupError(reason: unknown): boolean;
|
|
|
168
168
|
export declare function interceptUnhandledRejections(interceptor: (reason: unknown) => boolean): () => void;
|
|
169
169
|
/**
|
|
170
170
|
* Register a synchronous recovery command to print when the process exits
|
|
171
|
-
* through an uncaught exception, an unhandled rejection, or
|
|
171
|
+
* through an uncaught exception, an unhandled rejection, SIGTERM, or SIGHUP.
|
|
172
172
|
*
|
|
173
173
|
* Providers are read once cleanup finishes (or hits its deadline), so they
|
|
174
174
|
* report what teardown left behind: a session it saved or one it dropped.
|
|
175
175
|
*/
|
|
176
176
|
export declare function registerFatalRecoveryHint(provider: FatalRecoveryHintProvider): () => void;
|
|
177
|
+
/**
|
|
178
|
+
* Record that the terminal is gone, so signal exits stop writing to fd 2.
|
|
179
|
+
*
|
|
180
|
+
* The terminal owner calls this before raising its own disconnect signal. The
|
|
181
|
+
* `EIO` try/catch in {@link writeTerminalReport} covers a descriptor revoked by
|
|
182
|
+
* an outside hangup; it cannot cover a write that blocks on a full pty.
|
|
183
|
+
*/
|
|
184
|
+
export declare function declareTerminalDead(): void;
|
|
185
|
+
/** Whether a terminal owner has already declared the terminal gone. */
|
|
186
|
+
export declare function isTerminalDeclaredDead(): boolean;
|
|
177
187
|
/**
|
|
178
188
|
* Reports a caught top-level failure after terminal owners restore their display, then exits.
|
|
179
189
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxiraos/pi-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"utilities",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@linxiraos/pi-natives": "1.
|
|
58
|
-
"@linxiraos/zeta-schema": "1.
|
|
57
|
+
"@linxiraos/pi-natives": "1.5.0",
|
|
58
|
+
"@linxiraos/zeta-schema": "1.5.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/bun": "1.4.4"
|
package/src/dirs.ts
CHANGED
|
@@ -204,11 +204,15 @@ export function resolveEquivalentPath(inputPath: string, options?: EquivalentPat
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
const resolvedPath = resolveEquivalentPath(inputPath);
|
|
207
|
+
function comparablePath(inputPath: string, options?: EquivalentPathOptions): string {
|
|
208
|
+
const resolvedPath = resolveEquivalentPath(inputPath, options);
|
|
209
209
|
return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
export function normalizePathForComparison(inputPath: string): string {
|
|
213
|
+
return comparablePath(inputPath);
|
|
214
|
+
}
|
|
215
|
+
|
|
212
216
|
/**
|
|
213
217
|
* Compare paths already normalized by {@link normalizePathForComparison}.
|
|
214
218
|
*
|
|
@@ -224,9 +228,10 @@ export function relativePathWithinNormalizedRoot(normalizedRoot: string, normali
|
|
|
224
228
|
return relative;
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
|
|
231
|
+
/** `options` apply to `candidate` only. */
|
|
232
|
+
export function pathIsWithin(root: string, candidate: string, options?: EquivalentPathOptions): boolean {
|
|
228
233
|
const normalizedRoot = normalizePathForComparison(root);
|
|
229
|
-
const normalizedCandidate =
|
|
234
|
+
const normalizedCandidate = comparablePath(candidate, options);
|
|
230
235
|
return relativePathWithinNormalizedRoot(normalizedRoot, normalizedCandidate) !== null;
|
|
231
236
|
}
|
|
232
237
|
|
|
@@ -277,25 +282,23 @@ export function __resetProjectDirCacheForTests(): void {
|
|
|
277
282
|
projectDir = undefined;
|
|
278
283
|
}
|
|
279
284
|
|
|
280
|
-
/** Whether a path is absent or not a directory
|
|
281
|
-
export async function directoryIsMissing(dir: string): Promise<boolean> {
|
|
285
|
+
/** Whether a path is absent or not a directory; `strict` propagates non-missing stat failures. */
|
|
286
|
+
export async function directoryIsMissing(dir: string, options?: { strict?: boolean }): Promise<boolean> {
|
|
282
287
|
try {
|
|
283
288
|
return !(await fs.promises.stat(dir)).isDirectory();
|
|
284
289
|
} catch (error) {
|
|
285
|
-
|
|
290
|
+
if (isEnoent(error) || isEnotdir(error)) return true;
|
|
291
|
+
if (options?.strict) throw error;
|
|
292
|
+
return false;
|
|
286
293
|
}
|
|
287
294
|
}
|
|
288
295
|
|
|
289
|
-
/**
|
|
290
|
-
|
|
291
|
-
* path (ENOENT), permission error, or a non-directory — returns `false`, so
|
|
292
|
-
* callers can decide whether a directory is safe to `chdir` into or adopt as a
|
|
293
|
-
* working directory before {@link setProjectDir} throws on it.
|
|
294
|
-
*/
|
|
295
|
-
export async function directoryExists(dir: string): Promise<boolean> {
|
|
296
|
+
/** Whether a path is an existing directory; `strict` propagates non-missing stat failures. */
|
|
297
|
+
export async function directoryExists(dir: string, options?: { strict?: boolean }): Promise<boolean> {
|
|
296
298
|
try {
|
|
297
299
|
return (await fs.promises.stat(dir)).isDirectory();
|
|
298
|
-
} catch {
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (options?.strict && !isEnoent(error) && !isEnotdir(error)) throw error;
|
|
299
302
|
return false;
|
|
300
303
|
}
|
|
301
304
|
}
|
|
@@ -652,7 +655,7 @@ export function setProfile(profile: string | undefined): void {
|
|
|
652
655
|
dirs = new DirResolver({ profile: activeProfile });
|
|
653
656
|
process.env.ZETA_PROFILE = activeProfile;
|
|
654
657
|
process.env.ZETA_PROFILE = activeProfile;
|
|
655
|
-
process.env.
|
|
658
|
+
process.env.ZETA_PROFILE = activeProfile;
|
|
656
659
|
process.env.ZETA_CODING_AGENT_DIR = dirs.agentDir;
|
|
657
660
|
} else {
|
|
658
661
|
for (const key of PROFILE_ENV_KEYS) {
|
|
@@ -1060,6 +1063,17 @@ export function getStatsDbPath(): string {
|
|
|
1060
1063
|
return dirs.rootSubdir("stats.db", "data");
|
|
1061
1064
|
}
|
|
1062
1065
|
|
|
1066
|
+
/**
|
|
1067
|
+
* Get the global team database path (~/.zeta/data/zeta.db).
|
|
1068
|
+
*
|
|
1069
|
+
* One database for every team, PM, repo binding, engineer and worktree. It
|
|
1070
|
+
* must never live inside a team root, a repository, or a workspace: markers
|
|
1071
|
+
* on disk are discovery hints, this file is the authority.
|
|
1072
|
+
*/
|
|
1073
|
+
export function getZetaDbPath(): string {
|
|
1074
|
+
return dirs.rootSubdir(path.join("data", "zeta.db"), "data");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1063
1077
|
/** Get the autoresearch state directory (~/.zeta/autoresearch). */
|
|
1064
1078
|
export function getAutoresearchDir(): string {
|
|
1065
1079
|
return dirs.rootSubdir("autoresearch", "state");
|
package/src/env.ts
CHANGED
|
@@ -291,18 +291,98 @@ for (const key of Object.keys(Bun.env)) {
|
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
/**
|
|
295
|
-
|
|
294
|
+
/**
|
|
295
|
+
* `OTEL_*` values from OMP's own (non-project) dotenv files: the agent dir,
|
|
296
|
+
* config root, and home `.env`. Those files configure OMP's exporter (endpoint,
|
|
297
|
+
* auth headers, GenAI content capture), so the values are kept here instead of
|
|
298
|
+
* in `Bun.env`/`process.env`: no child process or in-process extension
|
|
299
|
+
* inherits them. Read through {@link $otelEnv}; {@link withOmpOtelEnv} exposes
|
|
300
|
+
* them to the OpenTelemetry SDK for the synchronous span of its construction.
|
|
301
|
+
*
|
|
302
|
+
* Keys are folded with {@link foldOtelEnvName}.
|
|
303
|
+
*/
|
|
304
|
+
const ompOtelEnvFileValues = new Map<string, string>();
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Environment names compare case-insensitively on Windows, so a file that spells
|
|
308
|
+
* `otel_exporter_otlp_headers` configures the same variable as the upper-case
|
|
309
|
+
* name; match and key them folded there, exactly on POSIX (names are
|
|
310
|
+
* case-sensitive).
|
|
311
|
+
*/
|
|
312
|
+
function foldOtelEnvName(name: string, platform: NodeJS.Platform): string {
|
|
313
|
+
return platform === "win32" ? name.toUpperCase() : name;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @internal Loader seam, exported for tests only. Claims an `OTEL_*` entry from one of OMP's own dotenv files: the value is
|
|
318
|
+
* kept in a private map, never in the process environment. Returns `false` for
|
|
319
|
+
* any other name, which the caller handles as usual.
|
|
320
|
+
*
|
|
321
|
+
* Precedence matches the process-environment loader: a non-empty live value
|
|
322
|
+
* (shell export, project `.env`) wins, and an earlier non-empty file value wins
|
|
323
|
+
* over a later file.
|
|
324
|
+
*
|
|
325
|
+
* @param platform - Name-folding semantics; injectable for tests.
|
|
326
|
+
*/
|
|
327
|
+
export function claimOmpOtelEnvFileEntry(
|
|
328
|
+
name: string,
|
|
329
|
+
value: string,
|
|
330
|
+
platform: NodeJS.Platform = process.platform,
|
|
331
|
+
): boolean {
|
|
332
|
+
const key = foldOtelEnvName(name, platform);
|
|
333
|
+
if (!key.startsWith("OTEL_")) return false;
|
|
334
|
+
if (!Bun.env[name] && !ompOtelEnvFileValues.get(key)) ompOtelEnvFileValues.set(key, value);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* `OTEL_*` lookup for OMP's own telemetry: the live environment wins (a shell
|
|
340
|
+
* export or project `.env`), then OMP's own `.env` files. Use this instead of
|
|
341
|
+
* `process.env` for any `OTEL_*` setting OMP reads itself.
|
|
342
|
+
*/
|
|
343
|
+
export function $otelEnv(name: string): string | undefined {
|
|
344
|
+
const live = Bun.env[name];
|
|
345
|
+
if (live) return live;
|
|
346
|
+
return ompOtelEnvFileValues.get(foldOtelEnvName(name, process.platform)) ?? live;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Runs `fn` with OMP's `.env`-sourced `OTEL_*` values temporarily present in
|
|
351
|
+
* `process.env` (only for names with no live value), then removes them again in
|
|
352
|
+
* a `finally`. For code that reads the environment itself, such as the
|
|
353
|
+
* OpenTelemetry SDK constructors.
|
|
354
|
+
*
|
|
355
|
+
* Synchronous only: awaiting inside would let other code, including child
|
|
356
|
+
* process spawns, run while the values are set.
|
|
357
|
+
*/
|
|
358
|
+
export function withOmpOtelEnv<T>(fn: () => T): T {
|
|
359
|
+
const previous = new Map<string, string | undefined>();
|
|
360
|
+
for (const [name, value] of ompOtelEnvFileValues) {
|
|
361
|
+
if (process.env[name]) continue;
|
|
362
|
+
previous.set(name, process.env[name]);
|
|
363
|
+
process.env[name] = value;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
return fn();
|
|
367
|
+
} finally {
|
|
368
|
+
for (const [name, prior] of previous) {
|
|
369
|
+
if (prior === undefined) delete process.env[name];
|
|
370
|
+
else process.env[name] = prior;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const file of [projectEnv, agentEnv, piEnv, homeEnv]) {
|
|
296
376
|
for (const key in file) {
|
|
297
|
-
if (
|
|
377
|
+
if (isMacosMallocStackLoggingEnvName(key)) continue;
|
|
378
|
+
if (file !== projectEnv && claimOmpOtelEnvFileEntry(key, file[key])) continue;
|
|
379
|
+
if (!Bun.env[key]) {
|
|
298
380
|
Bun.env[key] = file[key];
|
|
299
381
|
if (file === projectEnv) projectEnvNamesLoadedFromDotenv.add(key);
|
|
300
382
|
}
|
|
301
383
|
}
|
|
302
384
|
}
|
|
303
385
|
|
|
304
|
-
for (const file of [projectEnv, agentEnv, piEnv, homeEnv]) mergeDotenvIntoProcess(file);
|
|
305
|
-
|
|
306
386
|
// Directory-affecting keys (XDG_*_HOME, and in default mode ZETA_CODING_AGENT_DIR)
|
|
307
387
|
// may have just arrived from the profile/agent `.env` applied above. The dirs
|
|
308
388
|
// resolver cached its paths at module load — before this file ran — so rebuild
|
package/src/json-parse.ts
CHANGED
|
@@ -223,7 +223,14 @@ class RelaxedJson {
|
|
|
223
223
|
}
|
|
224
224
|
const value = this.#value(true);
|
|
225
225
|
if (value === INCOMPLETE) return out;
|
|
226
|
-
|
|
226
|
+
// A decoded __proto__ key must become an own property, not invoke
|
|
227
|
+
// the legacy prototype setter on the result object (same guard as
|
|
228
|
+
// schema/normalize.ts and validation.ts).
|
|
229
|
+
if (key === "__proto__") {
|
|
230
|
+
Object.defineProperty(out, key, { value, enumerable: true, writable: true, configurable: true });
|
|
231
|
+
} else {
|
|
232
|
+
out[key] = value;
|
|
233
|
+
}
|
|
227
234
|
lex.ws();
|
|
228
235
|
const d = lex.peek();
|
|
229
236
|
if (d === COMMA) {
|
package/src/postmortem.ts
CHANGED
|
@@ -602,7 +602,7 @@ function removeEmbeddedRejectionListener(): void {
|
|
|
602
602
|
|
|
603
603
|
/**
|
|
604
604
|
* Register a synchronous recovery command to print when the process exits
|
|
605
|
-
* through an uncaught exception, an unhandled rejection, or
|
|
605
|
+
* through an uncaught exception, an unhandled rejection, SIGTERM, or SIGHUP.
|
|
606
606
|
*
|
|
607
607
|
* Providers are read once cleanup finishes (or hits its deadline), so they
|
|
608
608
|
* report what teardown left behind: a session it saved or one it dropped.
|
|
@@ -644,6 +644,30 @@ function formatFatalError(label: string, err: Error): string {
|
|
|
644
644
|
return `\n[${label}] ${name}: ${message}${formattedStack}\n`;
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Whether a terminal owner has declared the terminal gone, e.g. the TUI's
|
|
649
|
+
* `#markTerminalDisconnected`. The owner sets this before raising SIGHUP itself,
|
|
650
|
+
* so the signal handlers must not write afterwards: with stdout stalled, fd 2 is
|
|
651
|
+
* a blocking, full pty, and a write there hangs in the kernel before the exit.
|
|
652
|
+
*/
|
|
653
|
+
let terminalDeclaredDead = false;
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Record that the terminal is gone, so signal exits stop writing to fd 2.
|
|
657
|
+
*
|
|
658
|
+
* The terminal owner calls this before raising its own disconnect signal. The
|
|
659
|
+
* `EIO` try/catch in {@link writeTerminalReport} covers a descriptor revoked by
|
|
660
|
+
* an outside hangup; it cannot cover a write that blocks on a full pty.
|
|
661
|
+
*/
|
|
662
|
+
export function declareTerminalDead(): void {
|
|
663
|
+
terminalDeclaredDead = true;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Whether a terminal owner has already declared the terminal gone. */
|
|
667
|
+
export function isTerminalDeclaredDead(): boolean {
|
|
668
|
+
return terminalDeclaredDead;
|
|
669
|
+
}
|
|
670
|
+
|
|
647
671
|
/** Write a final report to stderr once terminal owners have handed the terminal back. */
|
|
648
672
|
function writeTerminalReport(output: string): void {
|
|
649
673
|
restoreTerminalStderr();
|
|
@@ -728,6 +752,21 @@ process.on("exit", () => {
|
|
|
728
752
|
|
|
729
753
|
let processHandlersInstalled = false;
|
|
730
754
|
|
|
755
|
+
/**
|
|
756
|
+
* Exit through a termination signal: clean up, print the recovery hints, exit.
|
|
757
|
+
*
|
|
758
|
+
* The hint write is skipped once a terminal owner declared the terminal dead
|
|
759
|
+
* ({@link declareTerminalDead}): a stalled stdout leaves fd 2 a full, blocking
|
|
760
|
+
* pty, so the write would hang the process before the exit below. A repeated
|
|
761
|
+
* signal joins the same cleanup and the hints still print once.
|
|
762
|
+
*/
|
|
763
|
+
async function exitAfterSignal(reason: Reason, heading: string, exitCode: number): Promise<never> {
|
|
764
|
+
const hintProviders = [...fatalRecoveryHintProviders];
|
|
765
|
+
await runCleanup(reason);
|
|
766
|
+
if (!terminalDeclaredDead) writeRecoveryHints(hintProviders, heading);
|
|
767
|
+
exitProcess(exitCode);
|
|
768
|
+
}
|
|
769
|
+
|
|
731
770
|
/** Installs OMP's process-wide signal and fatal handlers for a CLI-owned process. */
|
|
732
771
|
export function installProcessHandlers(): void {
|
|
733
772
|
if (processHandlersInstalled) return;
|
|
@@ -805,16 +844,13 @@ export function installProcessHandlers(): void {
|
|
|
805
844
|
})
|
|
806
845
|
.on("SIGTERM", async () => {
|
|
807
846
|
// A stopped session otherwise leaves its last frame above the shell prompt,
|
|
808
|
-
// which reads as a crash.
|
|
809
|
-
//
|
|
810
|
-
const hintProviders = [...fatalRecoveryHintProviders];
|
|
811
|
-
await runCleanup(Reason.SIGTERM);
|
|
812
|
-
writeRecoveryHints(hintProviders, "\n[Stopped by SIGTERM]");
|
|
813
|
-
exitProcess(143); // 128 + SIGTERM (15)
|
|
847
|
+
// which reads as a crash.
|
|
848
|
+
await exitAfterSignal(Reason.SIGTERM, "\n[Stopped by SIGTERM]", 143); // 128 + SIGTERM (15)
|
|
814
849
|
})
|
|
815
850
|
.on("SIGHUP", async () => {
|
|
816
|
-
|
|
817
|
-
|
|
851
|
+
// Same report as SIGTERM: a runtime that stops the program with a hangup
|
|
852
|
+
// keeps its screen, so the resume command survives the death.
|
|
853
|
+
await exitAfterSignal(Reason.SIGHUP, "\n[Stopped by SIGHUP]", 129); // 128 + SIGHUP (1)
|
|
818
854
|
});
|
|
819
855
|
}
|
|
820
856
|
|