@oh-my-pi/pi-utils 17.2.3 → 17.2.5
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 +25 -0
- package/dist/types/cli.d.ts +14 -9
- package/dist/types/dirs.d.ts +12 -0
- package/dist/types/env.d.ts +36 -1
- package/dist/types/postmortem.d.ts +12 -0
- package/package.json +2 -2
- package/src/cli.ts +41 -27
- package/src/dirs.ts +57 -0
- package/src/env.ts +58 -4
- package/src/postmortem.ts +35 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.2.5] - 2026-08-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added utility functions `parseFlag()`, `getBrowserRelayDir()`, and `getGlobalDaemonRuntimeDir()` to support browser relay mode and global daemon runtime directory resolution.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Updated the lightweight CLI runner to support static command metadata, allowing root help to render without importing full command implementations.
|
|
14
|
+
|
|
15
|
+
## [17.2.4] - 2026-08-01
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Added `getSecretPlaceholderKeyPath()`, `getDaemonRuntimeDir()`, `getProviderInFlightRoot()`, and `getMarketplacesRegistryPath()` to resolve secret key, daemon runtime, provider in-flight, and marketplace registry paths under their respective XDG categories (state, data) instead of the config root.
|
|
20
|
+
- Existing installs enabling XDG keep their data: a legacy `~/.omp/agent/secret-placeholder.key` or `~/.omp/marketplaces.json` is copied to its XDG location on first resolution, so persisted transcripts still deobfuscate and added marketplaces survive the move.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Headless hosts (print/RPC/ACP/eval/SDK) now use a 1s SQLite `busy_timeout` for the session-critical databases (agent.db, history.db, stats.db) via `getDbBusyTimeoutMs()`, so lock contention no longer freezes the protocol loop for the full interactive 5s timeout; interactive hosts keep the 5s timeout.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Fixed Bun test-runtime detection treating application-owned `NODE_ENV=test` and `BUN_ENV=test` values as test-runner signals ([#7261](https://github.com/can1357/oh-my-pi/issues/7261)).
|
|
29
|
+
|
|
5
30
|
## [17.2.1] - 2026-07-30
|
|
6
31
|
|
|
7
32
|
### Added
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -70,22 +70,24 @@ export interface ParseOutput<F extends Record<string, FlagDescriptor> = Record<s
|
|
|
70
70
|
args: ArgValues<A>;
|
|
71
71
|
argv: string[];
|
|
72
72
|
}
|
|
73
|
-
export interface
|
|
74
|
-
new (argv: string[], config: CliConfig): Command;
|
|
73
|
+
export interface CommandMetadata {
|
|
75
74
|
description?: string;
|
|
76
75
|
hidden?: boolean;
|
|
77
|
-
strict?: boolean;
|
|
78
|
-
aliases?: string[];
|
|
79
|
-
examples?: string[];
|
|
80
76
|
flags?: Record<string, FlagDescriptor>;
|
|
81
77
|
args?: Record<string, ArgDescriptor>;
|
|
78
|
+
examples?: string[];
|
|
79
|
+
}
|
|
80
|
+
export interface CommandCtor extends CommandMetadata {
|
|
81
|
+
new (argv: string[], config: CliConfig): Command;
|
|
82
|
+
strict?: boolean;
|
|
83
|
+
aliases?: string[];
|
|
82
84
|
}
|
|
83
85
|
/** Configuration passed to every command instance and help renderers. */
|
|
84
|
-
export interface CliConfig {
|
|
86
|
+
export interface CliConfig<TCommand extends CommandMetadata = CommandCtor> {
|
|
85
87
|
bin: string;
|
|
86
88
|
version: string;
|
|
87
89
|
/** All registered commands keyed by their canonical name. */
|
|
88
|
-
commands: Map<string,
|
|
90
|
+
commands: Map<string, TCommand>;
|
|
89
91
|
}
|
|
90
92
|
/** Minimal Command base matching the oclif surface we use. */
|
|
91
93
|
export declare abstract class Command {
|
|
@@ -100,7 +102,7 @@ export declare abstract class Command {
|
|
|
100
102
|
parse<C extends CommandCtor>(_Cmd: C): Promise<ParseOutput<NonNullable<C["flags"]> extends Record<string, FlagDescriptor> ? NonNullable<C["flags"]> : Record<string, FlagDescriptor>, NonNullable<C["args"]> extends Record<string, ArgDescriptor> ? NonNullable<C["args"]> : Record<string, ArgDescriptor>>>;
|
|
101
103
|
}
|
|
102
104
|
/** Render full root help: header, default command details, subcommand list. */
|
|
103
|
-
export declare function renderRootHelp(config: CliConfig): void;
|
|
105
|
+
export declare function renderRootHelp(config: CliConfig<CommandMetadata>): void;
|
|
104
106
|
/** Build the single USAGE line for a command (without the leading label). */
|
|
105
107
|
export declare function commandUsageLine(bin: string, id: string, Cmd: CommandCtor): string;
|
|
106
108
|
/** Render help for a single command. */
|
|
@@ -109,6 +111,7 @@ export declare function renderCommandHelp(bin: string, id: string, Cmd: CommandC
|
|
|
109
111
|
export interface CommandEntry {
|
|
110
112
|
name: string;
|
|
111
113
|
load: () => Promise<CommandCtor>;
|
|
114
|
+
help?: CommandMetadata;
|
|
112
115
|
aliases?: string[];
|
|
113
116
|
}
|
|
114
117
|
export interface RunOptions {
|
|
@@ -116,8 +119,10 @@ export interface RunOptions {
|
|
|
116
119
|
version: string;
|
|
117
120
|
argv: string[];
|
|
118
121
|
commands: CommandEntry[];
|
|
119
|
-
/** Custom help renderer
|
|
122
|
+
/** Custom help renderer with the fully loaded command constructors. */
|
|
120
123
|
help?: (config: CliConfig) => Promise<void> | void;
|
|
124
|
+
/** Lightweight help renderer backed by static command metadata. */
|
|
125
|
+
metadataHelp?: (config: CliConfig<CommandMetadata>) => Promise<void> | void;
|
|
121
126
|
}
|
|
122
127
|
/**
|
|
123
128
|
* Main entry point — replaces `run()` from @oclif/core.
|
package/dist/types/dirs.d.ts
CHANGED
|
@@ -153,6 +153,8 @@ export declare function getPythonEnvDir(): string;
|
|
|
153
153
|
export declare function getPythonGatewayDir(): string;
|
|
154
154
|
/** Get the puppeteer sandbox directory (~/.omp/puppeteer). */
|
|
155
155
|
export declare function getPuppeteerDir(): string;
|
|
156
|
+
/** Get the browser relay extension install directory (~/.omp/browser-relay). */
|
|
157
|
+
export declare function getBrowserRelayDir(): string;
|
|
156
158
|
/** Get DOCS_RS cache directory () */
|
|
157
159
|
export declare function getDocsRsCacheDir(): string;
|
|
158
160
|
/** Get the auto-QA grievances SQLite database path (~/.omp/autoqa.db; XDG: $XDG_DATA_HOME/omp/autoqa.db). */
|
|
@@ -237,6 +239,16 @@ export declare function getTerminalSessionsDir(agentDir?: string): string;
|
|
|
237
239
|
export declare function getCrashLogPath(agentDir?: string): string;
|
|
238
240
|
/** Get the debug log path (~/.omp/agent/omp-debug.log). */
|
|
239
241
|
export declare function getDebugLogPath(agentDir?: string): string;
|
|
242
|
+
/** Get the secret placeholder key path (~/.omp/agent/secret-placeholder.key; XDG default: $XDG_STATE_HOME/omp/secret-placeholder.key). Adopts a legacy key on first XDG resolution. */
|
|
243
|
+
export declare function getSecretPlaceholderKeyPath(): string;
|
|
244
|
+
/** Get the daemon runtime directory for a project (~/.omp/run/daemons/<hash>; XDG default: $XDG_STATE_HOME/omp/run/daemons/<hash>). */
|
|
245
|
+
export declare function getDaemonRuntimeDir(projectDir: string): string;
|
|
246
|
+
/** Get a profile-independent runtime directory for a machine-global daemon service. */
|
|
247
|
+
export declare function getGlobalDaemonRuntimeDir(service: string): string;
|
|
248
|
+
/** Get the provider in-flight root directory (~/.omp/run/provider-inflight; XDG default: $XDG_STATE_HOME/omp/run/provider-inflight). */
|
|
249
|
+
export declare function getProviderInFlightRoot(): string;
|
|
250
|
+
/** Get the marketplaces registry path (~/.omp/marketplaces.json; XDG default: $XDG_DATA_HOME/omp/marketplaces.json). Adopts a legacy registry on first XDG resolution. */
|
|
251
|
+
export declare function getMarketplacesRegistryPath(): string;
|
|
240
252
|
/** Get the project-level Python modules directory (.omp/modules). */
|
|
241
253
|
export declare function getProjectModulesDir(cwd?: string): string;
|
|
242
254
|
/** Get the project-level prompts directory (.omp/prompts). */
|
package/dist/types/env.d.ts
CHANGED
|
@@ -39,12 +39,31 @@ export declare const $env: Record<string, string>;
|
|
|
39
39
|
* @returns The first environment variable value, or undefined if no value is found.
|
|
40
40
|
*/
|
|
41
41
|
export declare function $pickenv(...keys: string[]): string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Read an environment variable by its EXACT, case-sensitive name.
|
|
44
|
+
*
|
|
45
|
+
* `process.env` / `Bun.env` lookups are case-insensitive on Windows (Node backs
|
|
46
|
+
* them with `uv_os_getenv`, Bun with a `CaseInsensitiveASCIIStringArrayHashMap`),
|
|
47
|
+
* so a lowercase literal like `public` silently resolves to a differently-cased
|
|
48
|
+
* system variable — Windows ships `PUBLIC=C:\Users\Public`. Enumerated keys are
|
|
49
|
+
* the only signal that preserves the real casing, so this trusts the lookup only
|
|
50
|
+
* when a key with identical casing is actually present. On POSIX (case-sensitive
|
|
51
|
+
* env) it is equivalent to a direct lookup.
|
|
52
|
+
*
|
|
53
|
+
* Use this instead of `process.env[name] ?? literal` wherever `name` may be a
|
|
54
|
+
* user-supplied literal (e.g. a stored API key) rather than a genuine env-var
|
|
55
|
+
* reference — otherwise the literal gets hijacked by a same-named system var.
|
|
56
|
+
*
|
|
57
|
+
* @param name - Environment variable name to look up.
|
|
58
|
+
* @param env - Environment source; defaults to `process.env`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function $envExact(name: string, env?: Record<string, string | undefined>): string | undefined;
|
|
42
61
|
/**
|
|
43
62
|
* Parses a positive decimal integer from `$env[name]`.
|
|
44
63
|
* Empty, invalid, NaN, zero, or negative values return `defaultValue`.
|
|
45
64
|
*/
|
|
46
65
|
export declare function $envpos(name: string, defaultValue: number): number;
|
|
47
|
-
/** True when
|
|
66
|
+
/** True when the process is an explicitly marked test child or Bun is running a test entrypoint. */
|
|
48
67
|
export declare function isBunTestRuntime(): boolean;
|
|
49
68
|
/**
|
|
50
69
|
* True when real-terminal side effects must be suppressed: stdout escape/frame
|
|
@@ -77,6 +96,20 @@ export declare function isInteractiveHost(): boolean;
|
|
|
77
96
|
* restore exact prior state. See {@link isInteractiveHost}.
|
|
78
97
|
*/
|
|
79
98
|
export declare function setInteractiveHost(interactive: boolean): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* SQLite `busy_timeout` for the session-critical databases (agent.db,
|
|
101
|
+
* history.db, stats.db).
|
|
102
|
+
*
|
|
103
|
+
* Interactive hosts tolerate a longer synchronous wait on lock contention
|
|
104
|
+
* (SQLITE_BUSY during WAL recovery/checkpoint — see oh-my-pi#2421): the
|
|
105
|
+
* operator sees a brief freeze and the statement eventually completes.
|
|
106
|
+
* Headless hosts (print/RPC/ACP/eval/SDK) run a protocol on the same thread —
|
|
107
|
+
* a multi-second synchronous busy-wait freezes their event loop and stalls
|
|
108
|
+
* every in-flight frame with no liveness signal, so they use a short timeout
|
|
109
|
+
* and rely on the existing asynchronous open/retry paths to recover from
|
|
110
|
+
* contention instead of blocking.
|
|
111
|
+
*/
|
|
112
|
+
export declare function getDbBusyTimeoutMs(): number;
|
|
80
113
|
/**
|
|
81
114
|
* True when this code is running inside a `bun build --compile` standalone
|
|
82
115
|
* binary. Detects via the embedded virtual-filesystem path markers
|
|
@@ -86,4 +119,6 @@ export declare function setInteractiveHost(interactive: boolean): boolean;
|
|
|
86
119
|
* first for cheap fast-path detection.
|
|
87
120
|
*/
|
|
88
121
|
export declare function isCompiledBinary(): boolean;
|
|
122
|
+
/** Parse a boolean-ish env value ("1", "yes", "on", …); `def` when unset/empty. */
|
|
123
|
+
export declare function parseFlag(value: string | undefined, def?: boolean): boolean;
|
|
89
124
|
export declare function $flag(name: string, def?: boolean): boolean;
|
|
@@ -15,6 +15,18 @@ export declare enum Reason {
|
|
|
15
15
|
UNHANDLED_REJECTION = "unhandled_rejection",// Unhandled promise rejection
|
|
16
16
|
MANUAL = "manual"
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Symbol stamped by the extension-load guard onto the throwing replacement it
|
|
20
|
+
* installs over `process.exit` / `process.reallyExit`, carrying the native
|
|
21
|
+
* primitive that replacement shadows.
|
|
22
|
+
*
|
|
23
|
+
* Host-owned shutdown ({@link exitProcess}) reads through it so a signal that
|
|
24
|
+
* lands while the guard is active still terminates the process (#6488), while
|
|
25
|
+
* a signal that lands after the guard has restored the native exit also
|
|
26
|
+
* terminates cleanly (#7393). `Symbol.for` so it survives duplicate module
|
|
27
|
+
* instances across bundles/realms.
|
|
28
|
+
*/
|
|
29
|
+
export declare const NATIVE_PROCESS_EXIT: unique symbol;
|
|
18
30
|
/** Origin of an EPIPE raised by a process communication channel. */
|
|
19
31
|
export type BrokenPipeSource = "ipc-send" | "stdio-write";
|
|
20
32
|
/**
|
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.2.
|
|
4
|
+
"version": "17.2.5",
|
|
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.2.
|
|
34
|
+
"@oh-my-pi/pi-natives": "17.2.5",
|
|
35
35
|
"handlebars": "^4.7.9",
|
|
36
36
|
"winston": "^3.19.0",
|
|
37
37
|
"winston-daily-rotate-file": "5.0.0"
|
package/src/cli.ts
CHANGED
|
@@ -133,23 +133,26 @@ export interface ParseOutput<
|
|
|
133
133
|
// Command base class
|
|
134
134
|
// ---------------------------------------------------------------------------
|
|
135
135
|
|
|
136
|
-
export interface
|
|
137
|
-
new (argv: string[], config: CliConfig): Command;
|
|
136
|
+
export interface CommandMetadata {
|
|
138
137
|
description?: string;
|
|
139
138
|
hidden?: boolean;
|
|
140
|
-
strict?: boolean;
|
|
141
|
-
aliases?: string[];
|
|
142
|
-
examples?: string[];
|
|
143
139
|
flags?: Record<string, FlagDescriptor>;
|
|
144
140
|
args?: Record<string, ArgDescriptor>;
|
|
141
|
+
examples?: string[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface CommandCtor extends CommandMetadata {
|
|
145
|
+
new (argv: string[], config: CliConfig): Command;
|
|
146
|
+
strict?: boolean;
|
|
147
|
+
aliases?: string[];
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
/** Configuration passed to every command instance and help renderers. */
|
|
148
|
-
export interface CliConfig {
|
|
151
|
+
export interface CliConfig<TCommand extends CommandMetadata = CommandCtor> {
|
|
149
152
|
bin: string;
|
|
150
153
|
version: string;
|
|
151
154
|
/** All registered commands keyed by their canonical name. */
|
|
152
|
-
commands: Map<string,
|
|
155
|
+
commands: Map<string, TCommand>;
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
/** Minimal Command base matching the oclif surface we use. */
|
|
@@ -290,7 +293,7 @@ export abstract class Command {
|
|
|
290
293
|
// ---------------------------------------------------------------------------
|
|
291
294
|
|
|
292
295
|
/** Render full root help: header, default command details, subcommand list. */
|
|
293
|
-
export function renderRootHelp(config: CliConfig): void {
|
|
296
|
+
export function renderRootHelp(config: CliConfig<CommandMetadata>): void {
|
|
294
297
|
const { bin, version, commands } = config;
|
|
295
298
|
const lines: string[] = [];
|
|
296
299
|
lines.push(`${bin} v${version}\n`);
|
|
@@ -299,7 +302,7 @@ export function renderRootHelp(config: CliConfig): void {
|
|
|
299
302
|
|
|
300
303
|
// Show the default command's flags/args/examples inline.
|
|
301
304
|
// The default command is the one marked hidden (it's the implicit entry point).
|
|
302
|
-
const defaultCmd = [...commands.values()].find(
|
|
305
|
+
const defaultCmd = [...commands.values()].find(command => command.hidden);
|
|
303
306
|
if (defaultCmd) {
|
|
304
307
|
renderCommandBody(lines, defaultCmd);
|
|
305
308
|
}
|
|
@@ -309,8 +312,8 @@ export function renderRootHelp(config: CliConfig): void {
|
|
|
309
312
|
if (visible.length > 0) {
|
|
310
313
|
lines.push("COMMANDS");
|
|
311
314
|
const maxLen = Math.max(...visible.map(([n]) => n.length));
|
|
312
|
-
for (const [name,
|
|
313
|
-
lines.push(` ${name.padEnd(maxLen + 2)}${
|
|
315
|
+
for (const [name, command] of visible.sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
316
|
+
lines.push(` ${name.padEnd(maxLen + 2)}${command.description ?? ""}`);
|
|
314
317
|
}
|
|
315
318
|
lines.push("");
|
|
316
319
|
}
|
|
@@ -350,9 +353,9 @@ export function renderCommandHelp(bin: string, id: string, Cmd: CommandCtor): vo
|
|
|
350
353
|
process.stdout.write(lines.join("\n"));
|
|
351
354
|
}
|
|
352
355
|
|
|
353
|
-
function renderCommandBody(lines: string[],
|
|
354
|
-
const argDefs =
|
|
355
|
-
const flagDefs =
|
|
356
|
+
function renderCommandBody(lines: string[], command: CommandMetadata): void {
|
|
357
|
+
const argDefs = command.args ?? {};
|
|
358
|
+
const flagDefs = command.flags ?? {};
|
|
356
359
|
|
|
357
360
|
// Arguments
|
|
358
361
|
const argEntries = Object.entries(argDefs);
|
|
@@ -387,9 +390,9 @@ function renderCommandBody(lines: string[], Cmd: CommandCtor): void {
|
|
|
387
390
|
}
|
|
388
391
|
|
|
389
392
|
// Examples
|
|
390
|
-
if (
|
|
393
|
+
if (command.examples && command.examples.length > 0) {
|
|
391
394
|
lines.push("EXAMPLES");
|
|
392
|
-
for (const ex of
|
|
395
|
+
for (const ex of command.examples) {
|
|
393
396
|
for (const line of ex.split("\n")) {
|
|
394
397
|
lines.push(` ${line}`);
|
|
395
398
|
}
|
|
@@ -406,6 +409,7 @@ function renderCommandBody(lines: string[], Cmd: CommandCtor): void {
|
|
|
406
409
|
export interface CommandEntry {
|
|
407
410
|
name: string;
|
|
408
411
|
load: () => Promise<CommandCtor>;
|
|
412
|
+
help?: CommandMetadata;
|
|
409
413
|
aliases?: string[];
|
|
410
414
|
}
|
|
411
415
|
|
|
@@ -414,8 +418,10 @@ export interface RunOptions {
|
|
|
414
418
|
version: string;
|
|
415
419
|
argv: string[];
|
|
416
420
|
commands: CommandEntry[];
|
|
417
|
-
/** Custom help renderer
|
|
421
|
+
/** Custom help renderer with the fully loaded command constructors. */
|
|
418
422
|
help?: (config: CliConfig) => Promise<void> | void;
|
|
423
|
+
/** Lightweight help renderer backed by static command metadata. */
|
|
424
|
+
metadataHelp?: (config: CliConfig<CommandMetadata>) => Promise<void> | void;
|
|
419
425
|
}
|
|
420
426
|
|
|
421
427
|
/** Find a command entry by exact name or alias. */
|
|
@@ -437,11 +443,15 @@ export async function run(opts: RunOptions): Promise<void> {
|
|
|
437
443
|
|
|
438
444
|
// Top-level help
|
|
439
445
|
if (commandId === "--help" || commandId === "-h" || commandId === "help" || commandId === "") {
|
|
440
|
-
const config = await loadAllCommands(opts);
|
|
441
446
|
if (opts.help) {
|
|
442
|
-
await opts.help(
|
|
447
|
+
await opts.help(await loadAllCommands(opts));
|
|
443
448
|
} else {
|
|
444
|
-
|
|
449
|
+
const config = await loadAllCommandMetadata(opts);
|
|
450
|
+
if (opts.metadataHelp) {
|
|
451
|
+
await opts.metadataHelp(config);
|
|
452
|
+
} else {
|
|
453
|
+
renderRootHelp(config);
|
|
454
|
+
}
|
|
445
455
|
}
|
|
446
456
|
return;
|
|
447
457
|
}
|
|
@@ -504,12 +514,16 @@ async function loadEntry(entry: CommandEntry): Promise<CommandCtor> {
|
|
|
504
514
|
return Cmd;
|
|
505
515
|
}
|
|
506
516
|
|
|
507
|
-
/**
|
|
517
|
+
/** Load every command constructor for backward-compatible custom help callbacks. */
|
|
508
518
|
async function loadAllCommands(opts: RunOptions): Promise<CliConfig> {
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
519
|
+
const loaded = await Promise.all(opts.commands.map(async entry => [entry.name, await loadEntry(entry)] as const));
|
|
520
|
+
return { bin: opts.bin, version: opts.version, commands: new Map(loaded) };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Resolve static command metadata for lightweight root help. */
|
|
524
|
+
async function loadAllCommandMetadata(opts: RunOptions): Promise<CliConfig<CommandMetadata>> {
|
|
525
|
+
const loaded = await Promise.all(
|
|
526
|
+
opts.commands.map(async entry => [entry.name, entry.help ?? (await loadEntry(entry))] as const),
|
|
527
|
+
);
|
|
528
|
+
return { bin: opts.bin, version: opts.version, commands: new Map(loaded) };
|
|
515
529
|
}
|
package/src/dirs.ts
CHANGED
|
@@ -627,6 +627,11 @@ export function getPuppeteerDir(): string {
|
|
|
627
627
|
return dirs.rootSubdir("puppeteer", "cache");
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
/** Get the browser relay extension install directory (~/.omp/browser-relay). */
|
|
631
|
+
export function getBrowserRelayDir(): string {
|
|
632
|
+
return dirs.rootSubdir("browser-relay", "data");
|
|
633
|
+
}
|
|
634
|
+
|
|
630
635
|
/** Get DOCS_RS cache directory () */
|
|
631
636
|
export function getDocsRsCacheDir(): string {
|
|
632
637
|
return dirs.rootSubdir("webcache", "cache");
|
|
@@ -820,6 +825,58 @@ export function getDebugLogPath(agentDir?: string): string {
|
|
|
820
825
|
return dirs.agentSubdir(agentDir, `${APP_NAME}-debug.log`, "state");
|
|
821
826
|
}
|
|
822
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Best-effort one-time copy of a legacy config-root file to its redirected XDG
|
|
830
|
+
* location. Existing installs that enable XDG after the file was created keep
|
|
831
|
+
* their data (e.g. a placeholder key whose loss would break deobfuscation of
|
|
832
|
+
* persisted transcripts). The legacy file is left in place for older omp
|
|
833
|
+
* versions sharing the profile.
|
|
834
|
+
*/
|
|
835
|
+
function adoptLegacyFile(legacyPath: string, targetPath: string): void {
|
|
836
|
+
if (targetPath === legacyPath) return;
|
|
837
|
+
try {
|
|
838
|
+
if (fs.existsSync(targetPath) || !fs.existsSync(legacyPath)) return;
|
|
839
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
840
|
+
fs.copyFileSync(legacyPath, targetPath, fs.constants.COPYFILE_EXCL);
|
|
841
|
+
} catch {
|
|
842
|
+
// Opportunistic: a copy race or unwritable XDG dir falls back to a fresh
|
|
843
|
+
// file at the new path — the pre-adoption behavior.
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Get the secret placeholder key path (~/.omp/agent/secret-placeholder.key; XDG default: $XDG_STATE_HOME/omp/secret-placeholder.key). Adopts a legacy key on first XDG resolution. */
|
|
848
|
+
export function getSecretPlaceholderKeyPath(): string {
|
|
849
|
+
const keyPath = dirs.agentSubdir(undefined, "secret-placeholder.key", "state");
|
|
850
|
+
adoptLegacyFile(path.join(dirs.agentDir, "secret-placeholder.key"), keyPath);
|
|
851
|
+
return keyPath;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** Get the daemon runtime directory for a project (~/.omp/run/daemons/<hash>; XDG default: $XDG_STATE_HOME/omp/run/daemons/<hash>). */
|
|
855
|
+
export function getDaemonRuntimeDir(projectDir: string): string {
|
|
856
|
+
const key = Bun.hash.wyhash(path.resolve(projectDir)).toString(16).padStart(16, "0");
|
|
857
|
+
return dirs.rootSubdir(path.join("run", "daemons", key), "state");
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Get a profile-independent runtime directory for a machine-global daemon service. */
|
|
861
|
+
export function getGlobalDaemonRuntimeDir(service: string): string {
|
|
862
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(service)) {
|
|
863
|
+
throw new Error(`Invalid global daemon service name: ${JSON.stringify(service)}`);
|
|
864
|
+
}
|
|
865
|
+
return path.join(getBaseConfigRoot(), "run", "daemons", "global", service);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** Get the provider in-flight root directory (~/.omp/run/provider-inflight; XDG default: $XDG_STATE_HOME/omp/run/provider-inflight). */
|
|
869
|
+
export function getProviderInFlightRoot(): string {
|
|
870
|
+
return dirs.rootSubdir(path.join("run", "provider-inflight"), "state");
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Get the marketplaces registry path (~/.omp/marketplaces.json; XDG default: $XDG_DATA_HOME/omp/marketplaces.json). Adopts a legacy registry on first XDG resolution. */
|
|
874
|
+
export function getMarketplacesRegistryPath(): string {
|
|
875
|
+
const registryPath = dirs.rootSubdir("marketplaces.json", "data");
|
|
876
|
+
adoptLegacyFile(path.join(dirs.configRoot, "marketplaces.json"), registryPath);
|
|
877
|
+
return registryPath;
|
|
878
|
+
}
|
|
879
|
+
|
|
823
880
|
// =============================================================================
|
|
824
881
|
// Project subdirectories (.omp/*)
|
|
825
882
|
// =============================================================================
|
package/src/env.ts
CHANGED
|
@@ -246,6 +246,35 @@ export function $pickenv(...keys: string[]): string | undefined {
|
|
|
246
246
|
return undefined;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Read an environment variable by its EXACT, case-sensitive name.
|
|
251
|
+
*
|
|
252
|
+
* `process.env` / `Bun.env` lookups are case-insensitive on Windows (Node backs
|
|
253
|
+
* them with `uv_os_getenv`, Bun with a `CaseInsensitiveASCIIStringArrayHashMap`),
|
|
254
|
+
* so a lowercase literal like `public` silently resolves to a differently-cased
|
|
255
|
+
* system variable — Windows ships `PUBLIC=C:\Users\Public`. Enumerated keys are
|
|
256
|
+
* the only signal that preserves the real casing, so this trusts the lookup only
|
|
257
|
+
* when a key with identical casing is actually present. On POSIX (case-sensitive
|
|
258
|
+
* env) it is equivalent to a direct lookup.
|
|
259
|
+
*
|
|
260
|
+
* Use this instead of `process.env[name] ?? literal` wherever `name` may be a
|
|
261
|
+
* user-supplied literal (e.g. a stored API key) rather than a genuine env-var
|
|
262
|
+
* reference — otherwise the literal gets hijacked by a same-named system var.
|
|
263
|
+
*
|
|
264
|
+
* @param name - Environment variable name to look up.
|
|
265
|
+
* @param env - Environment source; defaults to `process.env`.
|
|
266
|
+
*/
|
|
267
|
+
export function $envExact(name: string, env: Record<string, string | undefined> = process.env): string | undefined {
|
|
268
|
+
const value = env[name];
|
|
269
|
+
if (value === undefined) return undefined;
|
|
270
|
+
// Enumeration preserves real key casing on Windows, unlike the getter; the
|
|
271
|
+
// value is trusted only when an exact-case entry actually exists.
|
|
272
|
+
for (const key in env) {
|
|
273
|
+
if (key === name) return value;
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
249
278
|
/**
|
|
250
279
|
* Parses a positive decimal integer from `$env[name]`.
|
|
251
280
|
* Empty, invalid, NaN, zero, or negative values return `defaultValue`.
|
|
@@ -258,9 +287,13 @@ export function $envpos(name: string, defaultValue: number): number {
|
|
|
258
287
|
return parsed;
|
|
259
288
|
}
|
|
260
289
|
|
|
261
|
-
|
|
290
|
+
const BUN_TEST_ENTRY_PATTERN = /[._](?:test|spec)\.[cm]?[jt]sx?$/;
|
|
291
|
+
|
|
292
|
+
/** True when the process is an explicitly marked test child or Bun is running a test entrypoint. */
|
|
262
293
|
export function isBunTestRuntime(): boolean {
|
|
263
|
-
|
|
294
|
+
if (Bun.env.PI_TEST_RUNTIME === "1") return true;
|
|
295
|
+
const hasTestEnvironment = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
|
|
296
|
+
return hasTestEnvironment && BUN_TEST_ENTRY_PATTERN.test(Bun.main);
|
|
264
297
|
}
|
|
265
298
|
|
|
266
299
|
let terminalHeadless = isBunTestRuntime();
|
|
@@ -314,6 +347,23 @@ export function setInteractiveHost(interactive: boolean): boolean {
|
|
|
314
347
|
return previous;
|
|
315
348
|
}
|
|
316
349
|
|
|
350
|
+
/**
|
|
351
|
+
* SQLite `busy_timeout` for the session-critical databases (agent.db,
|
|
352
|
+
* history.db, stats.db).
|
|
353
|
+
*
|
|
354
|
+
* Interactive hosts tolerate a longer synchronous wait on lock contention
|
|
355
|
+
* (SQLITE_BUSY during WAL recovery/checkpoint — see oh-my-pi#2421): the
|
|
356
|
+
* operator sees a brief freeze and the statement eventually completes.
|
|
357
|
+
* Headless hosts (print/RPC/ACP/eval/SDK) run a protocol on the same thread —
|
|
358
|
+
* a multi-second synchronous busy-wait freezes their event loop and stalls
|
|
359
|
+
* every in-flight frame with no liveness signal, so they use a short timeout
|
|
360
|
+
* and rely on the existing asynchronous open/retry paths to recover from
|
|
361
|
+
* contention instead of blocking.
|
|
362
|
+
*/
|
|
363
|
+
export function getDbBusyTimeoutMs(): number {
|
|
364
|
+
return isInteractiveHost() ? 5000 : 1000;
|
|
365
|
+
}
|
|
366
|
+
|
|
317
367
|
/**
|
|
318
368
|
* True when this code is running inside a `bun build --compile` standalone
|
|
319
369
|
* binary. Detects via the embedded virtual-filesystem path markers
|
|
@@ -339,8 +389,12 @@ const TRUTHY: Dict<boolean> = {
|
|
|
339
389
|
ON: true,
|
|
340
390
|
on: true,
|
|
341
391
|
};
|
|
342
|
-
|
|
343
|
-
|
|
392
|
+
/** Parse a boolean-ish env value ("1", "yes", "on", …); `def` when unset/empty. */
|
|
393
|
+
export function parseFlag(value: string | undefined, def = false): boolean {
|
|
344
394
|
if (!value) return def;
|
|
345
395
|
return TRUTHY[value] === true;
|
|
346
396
|
}
|
|
397
|
+
|
|
398
|
+
export function $flag(name: string, def: boolean = false): boolean {
|
|
399
|
+
return parseFlag($env[name], def);
|
|
400
|
+
}
|
package/src/postmortem.ts
CHANGED
|
@@ -29,8 +29,41 @@ const callbackList: ((reason: Reason) => Promise<void> | void)[] = [];
|
|
|
29
29
|
// Tracks cleanup run state (to prevent recursion/reentry issues)
|
|
30
30
|
let cleanupStage: "idle" | "running" | "complete" = "idle";
|
|
31
31
|
const CLEANUP_DEADLINE_MS = 10_000;
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Symbol stamped by the extension-load guard onto the throwing replacement it
|
|
34
|
+
* installs over `process.exit` / `process.reallyExit`, carrying the native
|
|
35
|
+
* primitive that replacement shadows.
|
|
36
|
+
*
|
|
37
|
+
* Host-owned shutdown ({@link exitProcess}) reads through it so a signal that
|
|
38
|
+
* lands while the guard is active still terminates the process (#6488), while
|
|
39
|
+
* a signal that lands after the guard has restored the native exit also
|
|
40
|
+
* terminates cleanly (#7393). `Symbol.for` so it survives duplicate module
|
|
41
|
+
* instances across bundles/realms.
|
|
42
|
+
*/
|
|
43
|
+
export const NATIVE_PROCESS_EXIT = Symbol.for("omp.postmortem.nativeProcessExit");
|
|
44
|
+
|
|
45
|
+
type HardExitFn = (code?: number) => never;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hard-exit the process through the native primitive, resolved on every call.
|
|
49
|
+
*
|
|
50
|
+
* The native exit is deliberately re-resolved here rather than bound at module
|
|
51
|
+
* load: the extension/hook loader's `withHostGuard` transiently swaps
|
|
52
|
+
* `process.reallyExit`/`process.exit` for a stub that throws
|
|
53
|
+
* `ExtensionExitError`, and the shipped bundle defers this module's evaluation
|
|
54
|
+
* until first access — which can land inside that guard window, so binding at
|
|
55
|
+
* init could freeze the throwing stub forever and turn every later shutdown
|
|
56
|
+
* (SIGHUP/SIGINT/fatal) into an unhandled-rejection loop (#7393). When the
|
|
57
|
+
* guard is active the stub carries the native exit under
|
|
58
|
+
* {@link NATIVE_PROCESS_EXIT}; unwrapping it lets a mid-guard signal still exit
|
|
59
|
+
* (#6488). Otherwise the current `process.reallyExit`/`process.exit` is native.
|
|
60
|
+
*/
|
|
61
|
+
function exitProcess(code: number): never {
|
|
62
|
+
const current: HardExitFn = typeof process.reallyExit === "function" ? process.reallyExit : process.exit;
|
|
63
|
+
const behind = Reflect.get(current, NATIVE_PROCESS_EXIT);
|
|
64
|
+
const nativeExit = typeof behind === "function" ? (behind as HardExitFn) : current;
|
|
65
|
+
return nativeExit.call(process, code) as never;
|
|
66
|
+
}
|
|
34
67
|
let cleanupPromise: Promise<void> | undefined;
|
|
35
68
|
let stdioDisconnectRegistrations = 0;
|
|
36
69
|
|