browser4-cli 0.1.3
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/bin/browser4-cli-linux-x64 +0 -0
- package/bin/browser4-cli-win32-x64.exe +0 -0
- package/bin/browser4-cli.js +37 -0
- package/package.json +49 -0
- package/scripts/build-all-platforms.ps1 +104 -0
- package/scripts/build-all-platforms.sh +74 -0
- package/scripts/check-version-sync.js +51 -0
- package/scripts/copy-native.js +36 -0
- package/scripts/postinstall.js +314 -0
- package/scripts/sync-version.js +69 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const scriptDir = path.dirname(__filename);
|
|
10
|
+
const exeName = process.platform === "win32" ? "browser4-cli.exe" : "browser4-cli";
|
|
11
|
+
const exePath = path.join(scriptDir, "target", "release", exeName);
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(exePath)) {
|
|
14
|
+
const scriptName = path.basename(__filename);
|
|
15
|
+
console.error(`[${scriptName}] ERROR: executable not found: "${exePath}"`);
|
|
16
|
+
console.error(`[${scriptName}] Run: cargo build --release (in sdks/browser4-cli)`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const child = spawn(exePath, process.argv.slice(2), {
|
|
21
|
+
stdio: "inherit",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
child.on("error", (error) => {
|
|
25
|
+
console.error(`[${path.basename(__filename)}] ERROR: failed to launch executable: ${error.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
child.on("exit", (code, signal) => {
|
|
30
|
+
if (signal) {
|
|
31
|
+
process.kill(process.pid, signal);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process.exit(code === null ? 1 : code);
|
|
36
|
+
});
|
|
37
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "browser4-cli",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Browser automation CLI for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin",
|
|
8
|
+
"scripts",
|
|
9
|
+
"skill-data",
|
|
10
|
+
"skills"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"browser4": "./bin/browser4-cli.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"version:sync": "node scripts/sync-version.js",
|
|
17
|
+
"version": "npm run version:sync && git add browser4-cli/Cargo.toml",
|
|
18
|
+
"build:native": "npm run version:sync && cargo build --release --manifest-path browser4-cli/Cargo.toml && node scripts/copy-native.js",
|
|
19
|
+
"build:linux": "npm run version:sync && docker compose -f docker/docker-compose.yml run --rm build-linux",
|
|
20
|
+
"build:macos": "npm run version:sync && (cargo build --release --manifest-path browser4-cli/Cargo.toml --target aarch64-apple-darwin & cargo build --release --manifest-path browser4-cli/Cargo.toml --target x86_64-apple-darwin & wait) && cp cli/target/aarch64-apple-darwin/release/browser4 bin/browser4-darwin-arm64 && cp cli/target/x86_64-apple-darwin/release/browser4 bin/browser4-darwin-x64",
|
|
21
|
+
"build:windows": "npm run version:sync && docker compose -f docker/docker-compose.yml run --rm build-windows",
|
|
22
|
+
"build:all-platforms": "npm run version:sync && (npm run build:linux & npm run build:windows & wait) && npm run build:macos",
|
|
23
|
+
"build:docker": "docker build -t browser4-builder -f docker/Dockerfile.build .",
|
|
24
|
+
"release": "npm run version:sync && npm run build:all-platforms && npm publish",
|
|
25
|
+
"postinstall": "node scripts/postinstall.js"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"browser",
|
|
29
|
+
"browser4",
|
|
30
|
+
"automation",
|
|
31
|
+
"headless",
|
|
32
|
+
"chrome",
|
|
33
|
+
"cdp",
|
|
34
|
+
"cli",
|
|
35
|
+
"browser4-cli",
|
|
36
|
+
"agent"
|
|
37
|
+
],
|
|
38
|
+
"license": "Apache-2.0",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/platonai/Browser4.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/platonai/Browser4/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://browser4.io",
|
|
47
|
+
"devDependencies": {},
|
|
48
|
+
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
|
|
49
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path,
|
|
4
|
+
[string]$ImageName = "browser4-builder",
|
|
5
|
+
[switch]$SkipDockerBuild,
|
|
6
|
+
[switch]$DryRun
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
Set-StrictMode -Version Latest
|
|
10
|
+
$ErrorActionPreference = "Stop"
|
|
11
|
+
|
|
12
|
+
$OutputDir = Join-Path $ProjectRoot "bin"
|
|
13
|
+
$CliDir = Join-Path $ProjectRoot "browser4-cli"
|
|
14
|
+
$DockerfilePath = Join-Path $ProjectRoot "docker/Dockerfile.build"
|
|
15
|
+
|
|
16
|
+
$Targets = @(
|
|
17
|
+
@{ Target = "x86_64-unknown-linux-gnu"; Output = "browser4-cli-linux-x64" },
|
|
18
|
+
@{ Target = "aarch64-unknown-linux-gnu"; Output = "browser4-cli-linux-arm64" },
|
|
19
|
+
@{ Target = "x86_64-pc-windows-gnu"; Output = "browser4-cli-win32-x64.exe" },
|
|
20
|
+
@{ Target = "x86_64-apple-darwin"; Output = "browser4-cli-darwin-x64" },
|
|
21
|
+
@{ Target = "aarch64-apple-darwin"; Output = "browser4-cli-darwin-arm64" },
|
|
22
|
+
@{ Target = "x86_64-unknown-linux-musl"; Output = "browser4-cli-linux-musl-x64" },
|
|
23
|
+
@{ Target = "aarch64-unknown-linux-musl"; Output = "browser4-cli-linux-musl-arm64" }
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
function Invoke-DockerCommand {
|
|
27
|
+
param([string[]]$Args)
|
|
28
|
+
|
|
29
|
+
if ($DryRun) {
|
|
30
|
+
Write-Host ("[DRY-RUN] docker " + ($Args -join " ")) -ForegroundColor DarkYellow
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
& docker @Args
|
|
35
|
+
if ($LASTEXITCODE -ne 0) {
|
|
36
|
+
throw "Docker command failed with exit code $LASTEXITCODE"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Build-Target {
|
|
41
|
+
param(
|
|
42
|
+
[string]$Target,
|
|
43
|
+
[string]$OutputName
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
Write-Host "Building for $Target..." -ForegroundColor Yellow
|
|
47
|
+
|
|
48
|
+
$containerCmd = "cargo zigbuild --release --target $Target && cp /build/target/$Target/release/browser4-cli* /output/$OutputName && chmod +x /output/$OutputName 2>/dev/null || true"
|
|
49
|
+
|
|
50
|
+
Invoke-DockerCommand -Args @(
|
|
51
|
+
"run", "--rm",
|
|
52
|
+
"-v", "${CliDir}:/build",
|
|
53
|
+
"-v", "${OutputDir}:/output",
|
|
54
|
+
$ImageName,
|
|
55
|
+
"-c", $containerCmd
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
$artifactPath = Join-Path $OutputDir $OutputName
|
|
59
|
+
if (-not (Test-Path -Path $artifactPath -PathType Leaf)) {
|
|
60
|
+
throw "Failed to build $OutputName"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$artifactSize = (Get-Item -Path $artifactPath).Length
|
|
64
|
+
if ($artifactSize -le 0) {
|
|
65
|
+
throw "Built artifact is empty: $OutputName"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Write-Host "Built $OutputName" -ForegroundColor Green
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
|
72
|
+
throw "Docker CLI not found in PATH. Install Docker Desktop and retry."
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Write-Host "Building browser4-cli for all platforms..." -ForegroundColor Yellow
|
|
76
|
+
Write-Host ""
|
|
77
|
+
|
|
78
|
+
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
|
|
79
|
+
|
|
80
|
+
if (-not $SkipDockerBuild) {
|
|
81
|
+
Write-Host "Building Docker cross-compilation image..." -ForegroundColor Yellow
|
|
82
|
+
Invoke-DockerCommand -Args @(
|
|
83
|
+
"build",
|
|
84
|
+
"-t", $ImageName,
|
|
85
|
+
"-f", $DockerfilePath,
|
|
86
|
+
$ProjectRoot
|
|
87
|
+
)
|
|
88
|
+
Write-Host ""
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
foreach ($entry in $Targets) {
|
|
92
|
+
Build-Target -Target $entry.Target -OutputName $entry.Output
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Write-Host ""
|
|
96
|
+
Write-Host "Build complete" -ForegroundColor Green
|
|
97
|
+
Write-Host "Binaries are in: $OutputDir"
|
|
98
|
+
|
|
99
|
+
if ($DryRun) {
|
|
100
|
+
Write-Host "[DRY-RUN] Skipping artifact listing." -ForegroundColor DarkYellow
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
Get-ChildItem -Path $OutputDir -Filter "browser4-cli-*" | Select-Object Name, Length, LastWriteTime
|
|
104
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Build browser4 for all platforms using Docker
|
|
5
|
+
# Usage: ./scripts/build-all-platforms.sh
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
9
|
+
OUTPUT_DIR="$PROJECT_ROOT/bin"
|
|
10
|
+
|
|
11
|
+
# Colors
|
|
12
|
+
RED='\033[0;31m'
|
|
13
|
+
GREEN='\033[0;32m'
|
|
14
|
+
YELLOW='\033[1;33m'
|
|
15
|
+
NC='\033[0m' # No Color
|
|
16
|
+
|
|
17
|
+
echo -e "${YELLOW}Building browser4 for all platforms...${NC}"
|
|
18
|
+
echo ""
|
|
19
|
+
|
|
20
|
+
# Ensure output directory exists
|
|
21
|
+
mkdir -p "$OUTPUT_DIR"
|
|
22
|
+
|
|
23
|
+
# Build the Docker image if needed
|
|
24
|
+
echo -e "${YELLOW}Building Docker cross-compilation image...${NC}"
|
|
25
|
+
docker build -t browser4-builder -f "$PROJECT_ROOT/docker/Dockerfile.build" "$PROJECT_ROOT"
|
|
26
|
+
|
|
27
|
+
# Function to build for a target
|
|
28
|
+
build_target() {
|
|
29
|
+
local target=$1
|
|
30
|
+
local output_name=$2
|
|
31
|
+
|
|
32
|
+
echo -e "${YELLOW}Building for ${target}...${NC}"
|
|
33
|
+
|
|
34
|
+
docker run --rm \
|
|
35
|
+
-v "$PROJECT_ROOT/cli:/build" \
|
|
36
|
+
-v "$OUTPUT_DIR:/output" \
|
|
37
|
+
browser4-builder \
|
|
38
|
+
-c "cargo zigbuild --release --target ${target} && cp /build/target/${target}/release/browser4-cli* /output/${output_name} && chmod +x /output/${output_name} 2>/dev/null || true"
|
|
39
|
+
|
|
40
|
+
if [ -f "$OUTPUT_DIR/$output_name" ]; then
|
|
41
|
+
echo -e "${GREEN}✓ Built ${output_name}${NC}"
|
|
42
|
+
else
|
|
43
|
+
echo -e "${RED}✗ Failed to build ${output_name}${NC}"
|
|
44
|
+
return 1
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Build for each platform
|
|
49
|
+
# Linux x64
|
|
50
|
+
build_target "x86_64-unknown-linux-gnu" "browser4-cli-linux-x64"
|
|
51
|
+
|
|
52
|
+
# Linux ARM64
|
|
53
|
+
build_target "aarch64-unknown-linux-gnu" "browser4-cli-linux-arm64"
|
|
54
|
+
|
|
55
|
+
# Windows x64
|
|
56
|
+
build_target "x86_64-pc-windows-gnu" "browser4-cli-win32-x64.exe"
|
|
57
|
+
|
|
58
|
+
# macOS x64 (via zig for cross-compilation)
|
|
59
|
+
build_target "x86_64-apple-darwin" "browser4-cli-darwin-x64"
|
|
60
|
+
|
|
61
|
+
# macOS ARM64 (via zig for cross-compilation)
|
|
62
|
+
build_target "aarch64-apple-darwin" "browser4-cli-darwin-arm64"
|
|
63
|
+
|
|
64
|
+
# Linux musl x64 (Alpine)
|
|
65
|
+
build_target "x86_64-unknown-linux-musl" "browser4-cli-linux-musl-x64"
|
|
66
|
+
|
|
67
|
+
# Linux musl ARM64 (Alpine)
|
|
68
|
+
build_target "aarch64-unknown-linux-musl" "browser4-cli-linux-musl-arm64"
|
|
69
|
+
|
|
70
|
+
echo ""
|
|
71
|
+
echo -e "${GREEN}Build complete!${NC}"
|
|
72
|
+
echo ""
|
|
73
|
+
echo "Binaries are in: $OUTPUT_DIR"
|
|
74
|
+
ls -la "$OUTPUT_DIR"/browser4-cli-*
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies that package.json and browser4-cli/Cargo.toml have the same version.
|
|
5
|
+
* Used in CI to catch version drift.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const rootDir = join(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
// Read package.json version
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'));
|
|
17
|
+
const packageVersion = packageJson.version;
|
|
18
|
+
|
|
19
|
+
// Read Cargo.toml version
|
|
20
|
+
const cargoToml = readFileSync(join(rootDir, 'browser4-cli/Cargo.toml'), 'utf-8');
|
|
21
|
+
const cargoVersionMatch = cargoToml.match(/^version\s*=\s*"([^"]*)"/m);
|
|
22
|
+
|
|
23
|
+
if (!cargoVersionMatch) {
|
|
24
|
+
console.error('Could not find version in browser4-cli/Cargo.toml');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cargoVersion = cargoVersionMatch[1];
|
|
29
|
+
|
|
30
|
+
// Read dashboard package.json version
|
|
31
|
+
const dashboardPkg = JSON.parse(readFileSync(join(rootDir, 'packages/dashboard/package.json'), 'utf-8'));
|
|
32
|
+
const dashboardVersion = dashboardPkg.version;
|
|
33
|
+
|
|
34
|
+
const mismatches = [];
|
|
35
|
+
if (packageVersion !== cargoVersion) {
|
|
36
|
+
mismatches.push(` browser4-cli/Cargo.toml: ${cargoVersion}`);
|
|
37
|
+
}
|
|
38
|
+
if (packageVersion !== dashboardVersion) {
|
|
39
|
+
mismatches.push(` packages/dashboard: ${dashboardVersion}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (mismatches.length > 0) {
|
|
43
|
+
console.error('Version mismatch detected!');
|
|
44
|
+
console.error(` package.json: ${packageVersion}`);
|
|
45
|
+
for (const m of mismatches) console.error(m);
|
|
46
|
+
console.error('');
|
|
47
|
+
console.error("Run 'pnpm run version:sync' to fix this.");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(`Versions are in sync: ${packageVersion}`);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copies the compiled Rust binary to bin/ with platform-specific naming
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { platform, arch } from 'os';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const projectRoot = join(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
const sourceExt = platform() === 'win32' ? '.exe' : '';
|
|
16
|
+
const sourcePath = join(projectRoot, `browser4-cli/target/release/browser4-cli${sourceExt}`);
|
|
17
|
+
const binDir = join(projectRoot, 'bin');
|
|
18
|
+
|
|
19
|
+
// Determine platform suffix
|
|
20
|
+
const platformKey = `${platform()}-${arch()}`;
|
|
21
|
+
const ext = platform() === 'win32' ? '.exe' : '';
|
|
22
|
+
const targetName = `browser4-cli-${platformKey}${ext}`;
|
|
23
|
+
const targetPath = join(binDir, targetName);
|
|
24
|
+
|
|
25
|
+
if (!existsSync(sourcePath)) {
|
|
26
|
+
console.error(`Error: Native binary not found at ${sourcePath}`);
|
|
27
|
+
console.error('Run "cargo build --release --manifest-path browser4-cli/Cargo.toml" first');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!existsSync(binDir)) {
|
|
32
|
+
mkdirSync(binDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
copyFileSync(sourcePath, targetPath);
|
|
36
|
+
console.log(`✓ Copied native binary to ${targetPath}`);
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script for browser4
|
|
5
|
+
*
|
|
6
|
+
* Downloads the platform-specific native binary if not present.
|
|
7
|
+
* On global installs, patches npm's bin entry to use the native binary directly:
|
|
8
|
+
* - Windows: Overwrites .cmd/.ps1 shims
|
|
9
|
+
* - Mac/Linux: Replaces symlink to point to native binary
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, writeFileSync, symlinkSync, lstatSync } from 'fs';
|
|
13
|
+
import { dirname, join } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { platform, arch } from 'os';
|
|
16
|
+
import { get } from 'https';
|
|
17
|
+
import { execSync } from 'child_process';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const projectRoot = join(__dirname, '..');
|
|
21
|
+
const binDir = join(projectRoot, 'bin');
|
|
22
|
+
|
|
23
|
+
// Detect if the system uses musl libc (e.g. Alpine Linux)
|
|
24
|
+
function isMusl() {
|
|
25
|
+
if (platform() !== 'linux') return false;
|
|
26
|
+
try {
|
|
27
|
+
const result = execSync('ldd --version 2>&1 || true', { encoding: 'utf8' });
|
|
28
|
+
return result.toLowerCase().includes('musl');
|
|
29
|
+
} catch {
|
|
30
|
+
return existsSync('/lib/ld-musl-x86_64.so.1') || existsSync('/lib/ld-musl-aarch64.so.1');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Platform detection
|
|
35
|
+
const osKey = platform() === 'linux' && isMusl() ? 'linux-musl' : platform();
|
|
36
|
+
const platformKey = `${osKey}-${arch()}`;
|
|
37
|
+
const ext = platform() === 'win32' ? '.exe' : '';
|
|
38
|
+
const binaryName = `browser4-cli-${platformKey}${ext}`;
|
|
39
|
+
const binaryPath = join(binDir, binaryName);
|
|
40
|
+
|
|
41
|
+
// Package info
|
|
42
|
+
const packageJson = JSON.parse(
|
|
43
|
+
(await import('fs')).readFileSync(join(projectRoot, 'package.json'), 'utf8')
|
|
44
|
+
);
|
|
45
|
+
const version = packageJson.version;
|
|
46
|
+
|
|
47
|
+
// GitHub release URL
|
|
48
|
+
const GITHUB_REPO = 'platonai/browser4';
|
|
49
|
+
const DOWNLOAD_URL = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${binaryName}`;
|
|
50
|
+
|
|
51
|
+
async function downloadFile(url, dest) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const file = createWriteStream(dest);
|
|
54
|
+
|
|
55
|
+
const request = (url) => {
|
|
56
|
+
get(url, (response) => {
|
|
57
|
+
// Handle redirects
|
|
58
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
59
|
+
request(response.headers.location);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (response.statusCode !== 200) {
|
|
64
|
+
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
response.pipe(file);
|
|
69
|
+
file.on('finish', () => {
|
|
70
|
+
file.close();
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
}).on('error', (err) => {
|
|
74
|
+
unlinkSync(dest);
|
|
75
|
+
reject(err);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
request(url);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detect which package manager ran this postinstall and write a marker file
|
|
85
|
+
* next to the binary so `browser4 upgrade` can use the correct one
|
|
86
|
+
* without fragile path heuristics or slow subprocess probing.
|
|
87
|
+
*
|
|
88
|
+
* npm_config_user_agent is set by npm/pnpm/yarn/bun during lifecycle scripts,
|
|
89
|
+
* e.g. "pnpm/8.10.0 node/v20.10.0 linux x64"
|
|
90
|
+
*/
|
|
91
|
+
function writeInstallMethod() {
|
|
92
|
+
const ua = process.env.npm_config_user_agent || '';
|
|
93
|
+
let method = '';
|
|
94
|
+
if (ua.startsWith('pnpm/')) method = 'pnpm';
|
|
95
|
+
else if (ua.startsWith('yarn/')) method = 'yarn';
|
|
96
|
+
else if (ua.startsWith('bun/')) method = 'bun';
|
|
97
|
+
else if (ua.startsWith('npm/')) method = 'npm';
|
|
98
|
+
|
|
99
|
+
if (method) {
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(join(binDir, '.install-method'), method);
|
|
102
|
+
} catch {
|
|
103
|
+
// Non-critical — upgrade will fall back to heuristics
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function main() {
|
|
109
|
+
// Check if binary already exists
|
|
110
|
+
if (existsSync(binaryPath)) {
|
|
111
|
+
// Ensure binary is executable (npm doesn't preserve execute bit)
|
|
112
|
+
if (platform() !== 'win32') {
|
|
113
|
+
chmodSync(binaryPath, 0o755);
|
|
114
|
+
}
|
|
115
|
+
console.log(`✓ Native binary ready: ${binaryName}`);
|
|
116
|
+
|
|
117
|
+
writeInstallMethod();
|
|
118
|
+
|
|
119
|
+
// On global installs, fix npm's bin entry to use native binary directly
|
|
120
|
+
await fixGlobalInstallBin();
|
|
121
|
+
|
|
122
|
+
showInstallReminder();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Ensure bin directory exists
|
|
127
|
+
if (!existsSync(binDir)) {
|
|
128
|
+
mkdirSync(binDir, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`Downloading native binary for ${platformKey}...`);
|
|
132
|
+
console.log(`URL: ${DOWNLOAD_URL}`);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await downloadFile(DOWNLOAD_URL, binaryPath);
|
|
136
|
+
|
|
137
|
+
// Make executable on Unix
|
|
138
|
+
if (platform() !== 'win32') {
|
|
139
|
+
chmodSync(binaryPath, 0o755);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(`✓ Downloaded native binary: ${binaryName}`);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.log(`Could not download native binary: ${err.message}`);
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log('To build the native binary locally:');
|
|
147
|
+
console.log(' 1. Install Rust: https://rustup.rs');
|
|
148
|
+
console.log(' 2. Run: npm run build:native');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
writeInstallMethod();
|
|
152
|
+
|
|
153
|
+
// On global installs, fix npm's bin entry to use native binary directly
|
|
154
|
+
// This avoids the /bin/sh error on Windows and provides zero-overhead execution
|
|
155
|
+
await fixGlobalInstallBin();
|
|
156
|
+
|
|
157
|
+
showInstallReminder();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function findSystemChrome() {
|
|
161
|
+
const os = platform();
|
|
162
|
+
if (os === 'darwin') {
|
|
163
|
+
const candidates = [
|
|
164
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
165
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
166
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
167
|
+
];
|
|
168
|
+
return candidates.find(p => existsSync(p)) || null;
|
|
169
|
+
}
|
|
170
|
+
if (os === 'linux') {
|
|
171
|
+
const names = ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium'];
|
|
172
|
+
for (const name of names) {
|
|
173
|
+
try {
|
|
174
|
+
const result = execSync(`which ${name} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
175
|
+
if (result) return result;
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (os === 'win32') {
|
|
181
|
+
const candidates = [
|
|
182
|
+
`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
183
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
184
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
185
|
+
];
|
|
186
|
+
return candidates.find(p => p && existsSync(p)) || null;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function showInstallReminder() {
|
|
192
|
+
const systemChrome = findSystemChrome();
|
|
193
|
+
if (systemChrome) {
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log(` ✓ System Chrome found: ${systemChrome}`);
|
|
196
|
+
console.log(' browser4 will use it automatically.');
|
|
197
|
+
console.log('');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(' ⚠ No Chrome installation detected.');
|
|
203
|
+
console.log(' If you plan to use a local browser, run:');
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(' browser4 install');
|
|
206
|
+
if (platform() === 'linux') {
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(' On Linux, include system dependencies with:');
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log(' browser4 install --with-deps');
|
|
211
|
+
}
|
|
212
|
+
console.log('');
|
|
213
|
+
console.log(' You can skip this if you use --cdp, --provider, --engine, or --executable-path.');
|
|
214
|
+
console.log('');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Fix npm's bin entry on global installs to use the native binary directly.
|
|
219
|
+
* This provides zero-overhead CLI execution for global installs.
|
|
220
|
+
*/
|
|
221
|
+
async function fixGlobalInstallBin() {
|
|
222
|
+
if (platform() === 'win32') {
|
|
223
|
+
await fixWindowsShims();
|
|
224
|
+
} else {
|
|
225
|
+
await fixUnixSymlink();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Fix npm symlink on Mac/Linux global installs.
|
|
231
|
+
* Replace the symlink to the JS wrapper with a symlink to the native binary.
|
|
232
|
+
*/
|
|
233
|
+
async function fixUnixSymlink() {
|
|
234
|
+
// Get npm's global bin directory (npm prefix -g + /bin)
|
|
235
|
+
let npmBinDir;
|
|
236
|
+
try {
|
|
237
|
+
const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
|
238
|
+
npmBinDir = join(prefix, 'bin');
|
|
239
|
+
} catch {
|
|
240
|
+
return; // npm not available
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const symlinkPath = join(npmBinDir, 'browser4');
|
|
244
|
+
|
|
245
|
+
// Check if symlink exists (indicates global install)
|
|
246
|
+
try {
|
|
247
|
+
const stat = lstatSync(symlinkPath);
|
|
248
|
+
if (!stat.isSymbolicLink()) {
|
|
249
|
+
return; // Not a symlink, don't touch it
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
return; // Symlink doesn't exist, not a global install
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Replace symlink to point directly to native binary
|
|
256
|
+
try {
|
|
257
|
+
unlinkSync(symlinkPath);
|
|
258
|
+
symlinkSync(binaryPath, symlinkPath);
|
|
259
|
+
console.log('✓ Optimized: symlink points to native binary (zero overhead)');
|
|
260
|
+
} catch (err) {
|
|
261
|
+
// Permission error or other issue - not critical, JS wrapper still works
|
|
262
|
+
console.log(`⚠ Could not optimize symlink: ${err.message}`);
|
|
263
|
+
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Fix npm-generated shims on Windows global installs.
|
|
269
|
+
* npm generates shims that try to run /bin/sh, which doesn't exist on Windows.
|
|
270
|
+
* We overwrite them to invoke the native .exe directly.
|
|
271
|
+
*/
|
|
272
|
+
async function fixWindowsShims() {
|
|
273
|
+
let npmBinDir;
|
|
274
|
+
try {
|
|
275
|
+
npmBinDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
|
276
|
+
} catch {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const cmdShim = join(npmBinDir, 'browser4.cmd');
|
|
281
|
+
const ps1Shim = join(npmBinDir, 'browser4.ps1');
|
|
282
|
+
|
|
283
|
+
// Shims may not exist yet during postinstall (npm creates them after
|
|
284
|
+
// lifecycle scripts). If missing, fall back: the JS wrapper at
|
|
285
|
+
// bin/browser4.js handles Windows correctly via child_process.spawn.
|
|
286
|
+
if (!existsSync(cmdShim)) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Detect architecture so ARM64 Windows is handled correctly
|
|
291
|
+
const cpuArch = arch() === 'arm64' ? 'arm64' : 'x64';
|
|
292
|
+
const relativeBinaryPath = `node_modules\\browser4\\bin\\browser4-win32-${cpuArch}.exe`;
|
|
293
|
+
const absoluteBinaryPath = join(npmBinDir, relativeBinaryPath);
|
|
294
|
+
|
|
295
|
+
// Only rewrite shims if the native binary actually exists
|
|
296
|
+
if (!existsSync(absoluteBinaryPath)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`;
|
|
302
|
+
writeFileSync(cmdShim, cmdContent);
|
|
303
|
+
|
|
304
|
+
const ps1Content = `#!/usr/bin/env pwsh\r\n$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent\r\n& "$basedir\\${relativeBinaryPath}" $args\r\nexit $LASTEXITCODE\r\n`;
|
|
305
|
+
writeFileSync(ps1Shim, ps1Content);
|
|
306
|
+
|
|
307
|
+
console.log('✓ Optimized: shims point to native binary (zero overhead)');
|
|
308
|
+
} catch (err) {
|
|
309
|
+
console.log(`⚠ Could not optimize shims: ${err.message}`);
|
|
310
|
+
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Syncs the version from package.json to all other config files.
|
|
5
|
+
* Run this script before building or releasing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const rootDir = join(__dirname, "..");
|
|
15
|
+
const cliDir = join(rootDir, "browser4-cli");
|
|
16
|
+
|
|
17
|
+
// Read version from package.json (single source of truth)
|
|
18
|
+
const packageJson = JSON.parse(
|
|
19
|
+
readFileSync(join(rootDir, "package.json"), "utf-8")
|
|
20
|
+
);
|
|
21
|
+
const version = packageJson.version;
|
|
22
|
+
|
|
23
|
+
console.log(`Syncing version ${version} to all config files...`);
|
|
24
|
+
|
|
25
|
+
// Update Cargo.toml
|
|
26
|
+
const cargoTomlPath = join(cliDir, "Cargo.toml");
|
|
27
|
+
let cargoToml = readFileSync(cargoTomlPath, "utf-8");
|
|
28
|
+
const cargoVersionRegex = /^version\s*=\s*"[^"]*"/m;
|
|
29
|
+
const newCargoVersion = `version = "${version}"`;
|
|
30
|
+
|
|
31
|
+
let cargoTomlUpdated = false;
|
|
32
|
+
if (cargoVersionRegex.test(cargoToml)) {
|
|
33
|
+
const oldMatch = cargoToml.match(cargoVersionRegex)?.[0];
|
|
34
|
+
if (oldMatch !== newCargoVersion) {
|
|
35
|
+
cargoToml = cargoToml.replace(cargoVersionRegex, newCargoVersion);
|
|
36
|
+
writeFileSync(cargoTomlPath, cargoToml);
|
|
37
|
+
console.log(` Updated browser4-cli/Cargo.toml: ${oldMatch} -> ${newCargoVersion}`);
|
|
38
|
+
cargoTomlUpdated = true;
|
|
39
|
+
} else {
|
|
40
|
+
console.log(` cli/Cargo.toml already up to date`);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.error(" Could not find version field in browser4-cli/Cargo.toml");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Update Cargo.lock to match Cargo.toml
|
|
48
|
+
if (cargoTomlUpdated) {
|
|
49
|
+
try {
|
|
50
|
+
execSync("cargo update -p browser4 --offline", {
|
|
51
|
+
cwd: cliDir,
|
|
52
|
+
stdio: "pipe",
|
|
53
|
+
});
|
|
54
|
+
console.log(` Updated cli/Cargo.lock`);
|
|
55
|
+
} catch {
|
|
56
|
+
// --offline may fail if package not in cache, try without it
|
|
57
|
+
try {
|
|
58
|
+
execSync("cargo update -p browser4", {
|
|
59
|
+
cwd: cliDir,
|
|
60
|
+
stdio: "pipe",
|
|
61
|
+
});
|
|
62
|
+
console.log(` Updated cli/Cargo.lock`);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(` Warning: Could not update Cargo.lock: ${e.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log("Version sync complete.");
|