@oh-my-pi/pi-utils 18.3.0 → 18.3.2

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.3.1] - 2026-09-25
6
+
7
+ ### Fixed
8
+
9
+ - Fixed log rotation near local-day boundaries so dated log files are consistently assigned to the correct local date.
10
+
5
11
  ## [18.2.7] - 2026-09-21
6
12
 
7
13
  ### Changed
@@ -78,8 +78,8 @@ below.
78
78
  uutils coreutils (https://github.com/uutils/coreutils), MIT
79
79
  --------------------------------------------------------------------------------
80
80
  Covers: base32, base64, basename, cat, cksum (shared checksum machinery),
81
- b2sum, md5sum, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, comm, cut,
82
- date, dirname, head, hostname, ln, ls, mkdir, mktemp, mv, nproc, paste,
81
+ b2sum, md5sum, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, comm, cp,
82
+ cut, date, dirname, head, hostname, ln, ls, mkdir, mktemp, mv, nproc, paste,
83
83
  printenv, readlink, realpath, rm, seq, sort, stat, tac, tail, tee, touch, tr,
84
84
  truncate, uname, uniq, wc, whoami, yes.
85
85
 
@@ -12,6 +12,8 @@
12
12
  */
13
13
  /** App name (e.g. "omp") */
14
14
  export declare const APP_NAME: string;
15
+ /** Public homepage that inference gateways (OpenRouter, Vercel AI Gateway) credit omp traffic to. */
16
+ export declare const APP_URL: string;
15
17
  /** Config directory name (e.g. ".omp") */
16
18
  export declare const CONFIG_DIR_NAME: string;
17
19
  /** Ordered main settings filenames: canonical write target first, legacy-compatible YAML fallback second. */
@@ -135,7 +137,16 @@ export declare function getProjectAgentDir(cwd?: string): string;
135
137
  export declare function getReportsDir(): string;
136
138
  /** Get the logs directory (~/.omp/logs). */
137
139
  export declare function getLogsDir(): string;
138
- /** Get this process's dated log path (~/.omp/logs/omp.YYYY-MM-DD.PID.log). */
140
+ /**
141
+ * Local-timezone `YYYY-MM-DD` day key (zero-padded), formatted exactly like
142
+ * the rotating log sink's file naming: log files are named `omp.<day>.<pid>.log`
143
+ * with the LOCAL day, not the UTC day `toISOString()` yields. Anything that
144
+ * computes "today's" log path or matches same-day log files by name must use
145
+ * this key, or between local midnight and UTC midnight it points at files that
146
+ * do not exist (e.g. 00:00–08:00 in UTC+8).
147
+ */
148
+ export declare function localDay(date: Date): string;
149
+ /** Get this process's dated log path (~/.omp/logs/omp.YYYY-MM-DD.PID.log, local-day named like the rotating sink). */
139
150
  export declare function getLogPath(date?: Date, pid?: number): string;
140
151
  /**
141
152
  * Get the plugins directory (~/.omp/plugins or its XDG equivalent).
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `fs.openSync` with close-on-exec, matching Node's default.
3
+ *
4
+ * libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
5
+ * dies at `exec`. Bun's `fs.open*` does not, and descriptors it returns survive
6
+ * into any child spawned through a path that does not close strays itself — the
7
+ * bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
8
+ * this for every descriptor kept open past such a spawn (session transcripts,
9
+ * logs, spools); a child must never hold a writable handle to them (#13224).
10
+ *
11
+ * Flags are numeric only: the bit cannot be OR-ed into `"a"`/`"w+"` strings.
12
+ *
13
+ * @example
14
+ * const fd = openCloexecSync(logPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
15
+ */
16
+ export declare function openCloexecSync(filePath: string, flags: number, mode?: number): number;
@@ -10,6 +10,7 @@ export * from "./file-lock.js";
10
10
  export * from "./format.js";
11
11
  export * from "./frontmatter.js";
12
12
  export * from "./fs-error.js";
13
+ export * from "./fs-open.js";
13
14
  export * from "./incoming-json.js";
14
15
  export * from "./json.js";
15
16
  export * from "./json-parse.js";
@@ -65,6 +65,9 @@ export declare function resolveWindowsShell(env?: Record<string, string | undefi
65
65
  * {@link resolveWindowsShell}) — never fails
66
66
  * 3. On Unix: $SHELL if bash/zsh, then fallback paths
67
67
  * 4. Fallback: sh
68
+ *
69
+ * Custom and auto-resolved configs are cached separately, so clearing
70
+ * `shellPath` mid-process immediately falls back to the platform default.
68
71
  */
69
72
  export declare function getShellConfig(customShellPath?: string, options?: ShellConfigOptions): ShellConfig;
70
73
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "18.3.0",
3
+ "version": "18.3.2",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "cli",
@@ -57,7 +57,7 @@
57
57
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
58
58
  },
59
59
  "dependencies": {
60
- "@oh-my-pi/pi-natives": "18.3.0"
60
+ "@oh-my-pi/pi-natives": "18.3.2"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/bun": "^1.3.14"
package/src/dirs.ts CHANGED
@@ -20,6 +20,9 @@ import { isEnoent, isEnotdir } from "./fs-error";
20
20
  /** App name (e.g. "omp") */
21
21
  export const APP_NAME: string = "omp";
22
22
 
23
+ /** Public homepage that inference gateways (OpenRouter, Vercel AI Gateway) credit omp traffic to. */
24
+ export const APP_URL: string = "https://omp.sh/";
25
+
23
26
  /** Config directory name (e.g. ".omp") */
24
27
  export const CONFIG_DIR_NAME: string = ".omp";
25
28
 
@@ -601,9 +604,21 @@ export function getLogsDir(): string {
601
604
  return dirs.rootSubdir("logs", "state");
602
605
  }
603
606
 
604
- /** Get this process's dated log path (~/.omp/logs/omp.YYYY-MM-DD.PID.log). */
607
+ /**
608
+ * Local-timezone `YYYY-MM-DD` day key (zero-padded), formatted exactly like
609
+ * the rotating log sink's file naming: log files are named `omp.<day>.<pid>.log`
610
+ * with the LOCAL day, not the UTC day `toISOString()` yields. Anything that
611
+ * computes "today's" log path or matches same-day log files by name must use
612
+ * this key, or between local midnight and UTC midnight it points at files that
613
+ * do not exist (e.g. 00:00–08:00 in UTC+8).
614
+ */
615
+ export function localDay(date: Date): string {
616
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
617
+ }
618
+
619
+ /** Get this process's dated log path (~/.omp/logs/omp.YYYY-MM-DD.PID.log, local-day named like the rotating sink). */
605
620
  export function getLogPath(date = new Date(), pid = process.pid): string {
606
- return path.join(getLogsDir(), `${APP_NAME}.${date.toISOString().slice(0, 10)}.${pid}.log`);
621
+ return path.join(getLogsDir(), `${APP_NAME}.${localDay(date)}.${pid}.log`);
607
622
  }
608
623
 
609
624
  /**
package/src/fs-open.ts ADDED
@@ -0,0 +1,31 @@
1
+ /** Close-on-exec file opens for descriptors held past a child spawn. */
2
+ import * as fs from "node:fs";
3
+
4
+ /**
5
+ * `O_CLOEXEC`, which neither Node nor Bun exposes in `fs.constants`.
6
+ *
7
+ * Kernel ABI values: `0o2000000` on Linux, `0x0100_0000` on Darwin. Windows has
8
+ * no `exec` and its CRT handles are inherited only when a spawn explicitly asks
9
+ * for it, so the bit is 0 there — as it is on any other platform, where adding
10
+ * an unknown flag bit would be worse than leaving the descriptor inheritable.
11
+ */
12
+ const O_CLOEXEC = process.platform === "linux" ? 0o2000000 : process.platform === "darwin" ? 0x0100_0000 : 0;
13
+
14
+ /**
15
+ * `fs.openSync` with close-on-exec, matching Node's default.
16
+ *
17
+ * libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
18
+ * dies at `exec`. Bun's `fs.open*` does not, and descriptors it returns survive
19
+ * into any child spawned through a path that does not close strays itself — the
20
+ * bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
21
+ * this for every descriptor kept open past such a spawn (session transcripts,
22
+ * logs, spools); a child must never hold a writable handle to them (#13224).
23
+ *
24
+ * Flags are numeric only: the bit cannot be OR-ed into `"a"`/`"w+"` strings.
25
+ *
26
+ * @example
27
+ * const fd = openCloexecSync(logPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
28
+ */
29
+ export function openCloexecSync(filePath: string, flags: number, mode?: number): number {
30
+ return fs.openSync(filePath, flags | O_CLOEXEC, mode);
31
+ }
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from "./file-lock";
10
10
  export * from "./format";
11
11
  export * from "./frontmatter";
12
12
  export * from "./fs-error";
13
+ export * from "./fs-open";
13
14
  export * from "./incoming-json";
14
15
  export * from "./json";
15
16
  export * from "./json-parse";
@@ -2,6 +2,8 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
+ import { localDay } from "../dirs";
6
+ import { openCloexecSync } from "../fs-open";
5
7
 
6
8
  interface AuditEntry {
7
9
  readonly date: number;
@@ -62,7 +64,7 @@ export class RotatingFileSink {
62
64
  this.#maxFiles = options.maxFiles;
63
65
  this.#files = this.#readAudit();
64
66
  const now = new Date();
65
- this.#selectFile(this.#localDay(now));
67
+ this.#selectFile(localDay(now));
66
68
  const activePath = this.#activePath;
67
69
  if (activePath) {
68
70
  this.#registerFile(activePath, now.getTime());
@@ -72,7 +74,7 @@ export class RotatingFileSink {
72
74
 
73
75
  #openFd(filePath: string): void {
74
76
  this.#closeFd();
75
- this.#fd = fs.openSync(filePath, "a");
77
+ this.#fd = openCloexecSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
76
78
  }
77
79
 
78
80
  #closeFd(): void {
@@ -91,7 +93,7 @@ export class RotatingFileSink {
91
93
  if (this.#closed) return;
92
94
  const prevPath = this.#activePath;
93
95
  const now = new Date();
94
- this.#selectFile(this.#localDay(now));
96
+ this.#selectFile(localDay(now));
95
97
  const activePath = this.#activePath;
96
98
  if (!activePath) return;
97
99
  // Rotation moved the active path: close the old descriptor BEFORE
@@ -119,10 +121,6 @@ export class RotatingFileSink {
119
121
  this.#closeFd();
120
122
  }
121
123
 
122
- #localDay(date: Date): string {
123
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
124
- }
125
-
126
124
  #selectFile(day: string): void {
127
125
  if (day !== this.#activeDay) {
128
126
  this.#activeDay = day;
package/src/procmgr.ts CHANGED
@@ -20,7 +20,10 @@ export interface ShellConfigOptions {
20
20
  /** File path or runtime layer that supplied the active shell setting. */
21
21
  configSource?: string;
22
22
  }
23
- let cachedShellConfig: ShellConfig | null = null;
23
+ /** Auto-resolved shell (no custom path); stable for the process lifetime. */
24
+ let cachedDefaultShellConfig: ShellConfig | null = null;
25
+ /** Config for the most recent custom `shellPath`; never served once the path is cleared. */
26
+ let cachedCustomShellConfig: ShellConfig | null = null;
24
27
 
25
28
  /**
26
29
  * Build the spawn environment (cached).
@@ -177,6 +180,9 @@ export function resolveWindowsShell(env: Record<string, string | undefined> = Bu
177
180
  * {@link resolveWindowsShell}) — never fails
178
181
  * 3. On Unix: $SHELL if bash/zsh, then fallback paths
179
182
  * 4. Fallback: sh
183
+ *
184
+ * Custom and auto-resolved configs are cached separately, so clearing
185
+ * `shellPath` mid-process immediately falls back to the platform default.
180
186
  */
181
187
  export function getShellConfig(customShellPath?: string, options: ShellConfigOptions = {}): ShellConfig {
182
188
  const configSource = options.configSource ?? path.join(getAgentDir(), MAIN_CONFIG_FILENAMES[0]);
@@ -187,36 +193,30 @@ export function getShellConfig(customShellPath?: string, options: ShellConfigOpt
187
193
  if (!fs.existsSync(customShellPath)) {
188
194
  throw new Error(`Custom shell path not found: ${customShellPath}\nPlease update shellPath in ${configSource}`);
189
195
  }
190
- if (cachedShellConfig?.shell !== customShellPath) {
191
- cachedShellConfig = buildConfig(customShellPath);
196
+ if (cachedCustomShellConfig?.shell !== customShellPath) {
197
+ cachedCustomShellConfig = buildConfig(customShellPath);
192
198
  }
193
- return cachedShellConfig;
194
- }
195
- if (cachedShellConfig) {
196
- return cachedShellConfig;
199
+ return cachedCustomShellConfig;
197
200
  }
201
+ cachedDefaultShellConfig ??= buildConfig(resolveDefaultShell());
202
+ return cachedDefaultShellConfig;
203
+ }
198
204
 
205
+ /** Platform shell discovery used when no custom `shellPath` is configured. */
206
+ function resolveDefaultShell(): string {
199
207
  if (process.platform === "win32") {
200
- cachedShellConfig = buildConfig(resolveWindowsShell());
201
- return cachedShellConfig;
208
+ return resolveWindowsShell();
202
209
  }
203
210
 
204
211
  // Unix: prefer user's shell from $SHELL if it's bash/zsh and executable
205
212
  const userShell = Bun.env.SHELL;
206
213
  const isValidShell = userShell && (userShell.includes("bash") || userShell.includes("zsh"));
207
214
  if (isValidShell && isExecutable(userShell)) {
208
- cachedShellConfig = buildConfig(userShell);
209
- return cachedShellConfig;
215
+ return userShell;
210
216
  }
211
217
 
212
218
  // 4. Fallback: use basic shell
213
- const basicShell = resolveBasicShell();
214
- if (basicShell) {
215
- cachedShellConfig = buildConfig(basicShell);
216
- return cachedShellConfig;
217
- }
218
- cachedShellConfig = buildConfig("sh");
219
- return cachedShellConfig;
219
+ return resolveBasicShell() ?? "sh";
220
220
  }
221
221
 
222
222
  /**