@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 +2 -48
- package/dist/types/dirs.d.ts +4 -0
- package/dist/types/file-lock.d.ts +6 -0
- package/package.json +2 -2
- package/src/browsers.ts +39 -25
- package/src/dirs.ts +7 -1
- package/src/env.ts +1 -1
- package/src/file-lock.ts +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,53 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [1.1.
|
|
5
|
+
## [1.1.15] - 2026-09-16
|
|
6
6
|
|
|
7
|
-
- 上游 v18.1.
|
|
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.
|
package/dist/types/dirs.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
60
|
+
const lock = await acquireFileLock(filePath, options);
|
|
55
61
|
try {
|
|
56
62
|
return await fn();
|
|
57
63
|
} finally {
|