@linxiraos/pi-utils 1.1.13 → 1.1.15

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,53 +2,7 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [1.1.13] - 2026-09-10
5
+ ## [1.1.15] - 2026-09-16
6
6
 
7
- - 上游 v18.1.16 同步:retry 提示解析升级为 longest-wins 合并(账号重置 + 追加 retry-after 同时出现时取更长窗口);`retry-after-ms` 支持 `:`/` =` 分隔形式;provider 明确要求立即重试(=0 或已过期时间戳)时返回 0 而非 undefined,避免误用启发式退避。
7
+ - 上游 v18.1.17–v18.1.21 同步带过(无独立用户可见变化)。
8
8
 
9
- ## [1.1.12] - 2026-09-10
10
-
11
- - worker 宿主选择器族统一为 `__zeta_worker_*` 前缀(与主 CLI 对齐)。
12
-
13
- ## [1.1.10] - 2026-09-07
14
-
15
- ### Fixed
16
-
17
- - Fixed `extractRetryHint` dropping the longer timing signal when an error body carries both an account reset and an appended retry hint: competing signals now merge by longest window instead of first match, so retries honor the provider's full backoff.
18
-
19
- ## [1.1.9] - 2026-09-05
20
-
21
- - USER_AGENT constant restored to zeta/<version> (the v18.1.10 merge re-introduced the upstream omp/ UA on every provider request); brand wording in path-resolver docs corrected; profile validation error message Zeta'd.
22
-
23
- ## [1.1.6] - 2026-08-30
24
-
25
- - 同步上游 OMP v18.0.10(`33cc6b9a043a`)。
26
- - 同步上游 OMP v18.0.9(`cc14e04f075d`)。
27
-
28
- ## [1.1.5] - 2026-08-26
29
-
30
- - 同步上游 OMP v18.0.5 / v18.0.6:新增 browsers / json 工具,SHA-2/SHA-3 在 ARM64 上加速。
31
-
32
- ## [1.1.2] - 2026-08-25
33
-
34
- ### Fixed
35
-
36
- - Republished as 1.1.2 to reset the `latest` tag after the broken 1.1.0 (no functional change over 1.1.1).
37
-
38
- ## [1.1.1] - 2026-08-25
39
-
40
- ### Fixed
41
-
42
- - Published tarballs now carry real dependency versions instead of Bun's `catalog:` protocol (1.1.0 installs failed with "Unsupported URL Type catalog:").
43
-
44
- ## [1.1.0] - 2026-08-25
45
-
46
- ### Changed
47
-
48
- - 同步上游 OMP v18.0.3 / v18.0.4(内部运行时与构建改进,无独立用户可见变更)。
49
-
50
- ## [1.0.0] - 2026-08-13
51
-
52
- ### Changed
53
-
54
- - Reset the version to 1.0.0 and republished under the `@linxiraos/*` scope, breaking from the `@linxiraos` version lineage.
@@ -37,6 +37,8 @@ export declare function normalizeProfileName(profile: string | undefined): strin
37
37
  * syntactically invalid value).
38
38
  */
39
39
  export declare function resolveProfileEnv(profile: string | undefined): string | undefined;
40
+ /** Profile-independent config root (~/.zeta), shared by every omp profile. */
41
+ export declare function getBaseConfigRoot(): string;
40
42
  export declare function resolveEquivalentPath(inputPath: string): string;
41
43
  export declare function normalizePathForComparison(inputPath: string): string;
42
44
  export declare function pathIsWithin(root: string, candidate: string): boolean;
@@ -172,6 +174,8 @@ export declare function getPythonGatewayDir(): string;
172
174
  export declare function getPuppeteerDir(): string;
173
175
  /** Get the browser relay extension install directory (~/.zeta/browser-relay). */
174
176
  export declare function getBrowserRelayDir(): string;
177
+ /** Get the profile root for Chromium browsers the browser tool spawns via `app.path` (~/.zeta/browser-profiles). */
178
+ export declare function getBrowserProfilesDir(): string;
175
179
  /** Get DOCS_RS cache directory () */
176
180
  export declare function getDocsRsCacheDir(): string;
177
181
  /** Get the auto-QA grievances SQLite database path (~/.zeta/autoqa.db; XDG: $XDG_DATA_HOME/zeta/autoqa.db). */
@@ -8,8 +8,14 @@ export interface FileLockOptions {
8
8
  /** Maximum age of the lock before it is considered stale and can be broken. */
9
9
  staleMs?: number;
10
10
  }
11
+ /** An exclusive OS-backed lease. Releasing an already released handle is safe. */
12
+ export interface FileLockHandle {
13
+ release(): void;
14
+ }
11
15
  declare function getLockPath(filePath: string): string;
12
16
  declare function tryAcquireLock(lockPath: string): NativeFileLock | null;
17
+ /** Acquire an exclusive lease; callers must release it when their operation ends. */
18
+ export declare function acquireFileLock(filePath: string, options?: FileLockOptions): Promise<FileLockHandle>;
13
19
  /** Run `fn` while holding an OS-backed exclusive lock for `filePath`. */
14
20
  export declare function withFileLock<T>(filePath: string, fn: () => Promise<T>, options?: FileLockOptions): Promise<T>;
15
21
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linxiraos/pi-utils",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "utilities",
@@ -54,7 +54,7 @@
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.1.13"
57
+ "@linxiraos/pi-natives": "1.1.15"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/bun": "^1.3.14"
package/src/browsers.ts CHANGED
@@ -5,9 +5,13 @@ import * as fsp from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import { type ArchiveLimits, extractArchive } from "./ar";
8
+ import { withFileLock } from "./file-lock";
8
9
 
9
10
  const CHROME_FOR_TESTING_BASE_URL = "https://storage.googleapis.com/chrome-for-testing-public";
10
11
  const CHROME_METADATA_BASE_URL = "https://googlechromelabs.github.io/chrome-for-testing";
12
+ // Installation is outside browser/Eval operation deadlines, but a stalled CDN
13
+ // must still release the cached install promise and permit a later attempt.
14
+ const DOWNLOAD_TIMEOUT_MS = 5 * 60_000;
11
15
 
12
16
  /**
13
17
  * Archive ceilings for the managed browser download. The default archive
@@ -251,29 +255,39 @@ export async function install(options: InstallOptions): Promise<InstalledBrowser
251
255
  return { browser: options.browser, buildId: options.buildId, platform, path: installPath, executablePath };
252
256
  }
253
257
 
254
- await fsp.mkdir(options.cacheDir, { recursive: true });
255
- const nonce = `${process.pid}-${crypto.randomUUID()}`;
256
- const archivePath = path.join(options.cacheDir, `.browser-${nonce}.zip`);
257
- const stagingPath = path.join(options.cacheDir, `.browser-${nonce}`);
258
- try {
259
- await downloadArchive(
260
- getDownloadUrl(options.browser, platform, options.buildId, options.baseUrl),
261
- archivePath,
262
- options.downloadProgressCallback,
263
- );
264
- await extractArchive(archivePath, stagingPath, { limits: BROWSER_ARCHIVE_LIMITS });
265
- await fsp.mkdir(path.dirname(installPath), { recursive: true });
266
- await fsp.rm(installPath, { recursive: true, force: true });
267
- await fsp.rename(stagingPath, installPath);
268
- } finally {
269
- await Promise.all([
270
- fsp.rm(archivePath, { force: true }).catch(() => {}),
271
- fsp.rm(stagingPath, { recursive: true, force: true }).catch(() => {}),
272
- ]);
273
- }
274
- if (!(await pathExists(executablePath)))
275
- throw new Error(`Browser archive did not contain its expected executable: ${executablePath}`);
276
- return { browser: options.browser, buildId: options.buildId, platform, path: installPath, executablePath };
258
+ await fsp.mkdir(path.dirname(installPath), { recursive: true });
259
+ return withFileLock(
260
+ `${installPath}.install`,
261
+ async () => {
262
+ // The in-process launch promise cannot serialize separate OMP sessions.
263
+ // Recheck under the OS lock: never replace a winner's running Chrome.
264
+ if (!(await pathExists(executablePath))) {
265
+ const nonce = `${process.pid}-${crypto.randomUUID()}`;
266
+ const archivePath = path.join(options.cacheDir, `.browser-${nonce}.zip`);
267
+ const stagingPath = path.join(options.cacheDir, `.browser-${nonce}`);
268
+ try {
269
+ await downloadArchive(
270
+ getDownloadUrl(options.browser, platform, options.buildId, options.baseUrl),
271
+ archivePath,
272
+ options.downloadProgressCallback,
273
+ );
274
+ await extractArchive(archivePath, stagingPath, { limits: BROWSER_ARCHIVE_LIMITS });
275
+ if (!(await pathExists(path.join(stagingPath, path.relative(installPath, executablePath))))) {
276
+ throw new Error(`Browser archive did not contain its expected executable: ${executablePath}`);
277
+ }
278
+ await fsp.rm(installPath, { recursive: true, force: true });
279
+ await fsp.rename(stagingPath, installPath);
280
+ } finally {
281
+ await Promise.all([
282
+ fsp.rm(archivePath, { force: true }).catch(() => {}),
283
+ fsp.rm(stagingPath, { recursive: true, force: true }).catch(() => {}),
284
+ ]);
285
+ }
286
+ }
287
+ return { browser: options.browser, buildId: options.buildId, platform, path: installPath, executablePath };
288
+ },
289
+ { retries: Math.ceil(DOWNLOAD_TIMEOUT_MS / 100) + 1, retryDelayMs: 100 },
290
+ );
277
291
  }
278
292
 
279
293
  function chromeChannelName(tag: string): string | undefined {
@@ -292,7 +306,7 @@ function chromeChannelName(tag: string): string | undefined {
292
306
  }
293
307
 
294
308
  async function fetchMetadata<T>(filename: string): Promise<T> {
295
- const response = await fetch(`${CHROME_METADATA_BASE_URL}/${filename}`);
309
+ const response = await fetch(`${CHROME_METADATA_BASE_URL}/${filename}`, { signal: AbortSignal.timeout(30_000) });
296
310
  if (!response.ok)
297
311
  throw new Error(`Failed to fetch Chrome-for-Testing metadata (${response.status} ${response.statusText})`);
298
312
  return (await response.json()) as T;
@@ -345,7 +359,7 @@ async function downloadArchive(
345
359
  destination: string,
346
360
  onProgress: ((progress: BrowserDownloadProgress) => void) | undefined,
347
361
  ): Promise<void> {
348
- const response = await fetch(url);
362
+ const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
349
363
  if (!response.ok || !response.body) {
350
364
  throw new Error(`Browser download failed (${response.status} ${response.statusText}) from ${url}`);
351
365
  }
package/src/dirs.ts CHANGED
@@ -104,7 +104,8 @@ function readProfileFromEnvSafe(): string | undefined {
104
104
  }
105
105
  }
106
106
 
107
- function getBaseConfigRoot(): string {
107
+ /** Profile-independent config root (~/.zeta), shared by every omp profile. */
108
+ export function getBaseConfigRoot(): string {
108
109
  return path.join(os.homedir(), getConfigDirName());
109
110
  }
110
111
 
@@ -687,6 +688,11 @@ export function getBrowserRelayDir(): string {
687
688
  return dirs.rootSubdir("browser-relay", "data");
688
689
  }
689
690
 
691
+ /** Get the profile root for Chromium browsers the browser tool spawns via `app.path` (~/.zeta/browser-profiles). */
692
+ export function getBrowserProfilesDir(): string {
693
+ return dirs.rootSubdir("browser-profiles", "state");
694
+ }
695
+
690
696
  /** Get DOCS_RS cache directory () */
691
697
  export function getDocsRsCacheDir(): string {
692
698
  return dirs.rootSubdir("webcache", "cache");
package/src/env.ts CHANGED
@@ -110,7 +110,7 @@ function expandDotenvValues(values: Record<string, string>, env: Record<string,
110
110
  /** Filters process env for child shells without launch-cwd dotenv values. */
111
111
  export function filterChildShellEnv(
112
112
  env: Record<string, string | undefined>,
113
- cwd: string = process.cwd(),
113
+ cwd: string = getProjectDir(),
114
114
  ): Record<string, string> {
115
115
  const runtimeLaunchEnvValues = env === Bun.env || env === process.env ? launchEnvValues : undefined;
116
116
  const result = filterProcessEnv(env);
package/src/file-lock.ts CHANGED
@@ -17,6 +17,11 @@ export interface FileLockOptions {
17
17
  staleMs?: number;
18
18
  }
19
19
 
20
+ /** An exclusive OS-backed lease. Releasing an already released handle is safe. */
21
+ export interface FileLockHandle {
22
+ release(): void;
23
+ }
24
+
20
25
  const DEFAULT_OPTIONS: Required<FileLockOptions> = {
21
26
  retries: 50,
22
27
  retryDelayMs: 100,
@@ -32,7 +37,8 @@ function tryAcquireLock(lockPath: string): NativeFileLock | null {
32
37
  return lock.acquired ? lock : null;
33
38
  }
34
39
 
35
- async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<NativeFileLock> {
40
+ /** Acquire an exclusive lease; callers must release it when their operation ends. */
41
+ export async function acquireFileLock(filePath: string, options: FileLockOptions = {}): Promise<FileLockHandle> {
36
42
  const opts = { ...DEFAULT_OPTIONS, ...options };
37
43
  const lockPath = getLockPath(filePath);
38
44
 
@@ -51,7 +57,7 @@ export async function withFileLock<T>(
51
57
  fn: () => Promise<T>,
52
58
  options: FileLockOptions = {},
53
59
  ): Promise<T> {
54
- const lock = await acquireLock(filePath, options);
60
+ const lock = await acquireFileLock(filePath, options);
55
61
  try {
56
62
  return await fn();
57
63
  } finally {