camoufox-cli 0.4.0__tar.gz → 0.6.0__tar.gz
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.
Potentially problematic release.
This version of camoufox-cli might be problematic. Click here for more details.
- camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6/.claude-plugin/marketplace.json +20 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/README.md +31 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/package-lock.json +2 -2
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/package.json +1 -1
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/browser.ts +12 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/cli.ts +27 -12
- camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6/js/src/config.ts +140 -0
- camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6/js/src/install.ts +126 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/tests/cli.test.ts +156 -1
- camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6/js/tests/install.test.ts +153 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/pyproject.toml +1 -1
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/skills/camoufox-cli/SKILL.md +9 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/skills/camoufox-cli/references/commands.md +11 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/src/camoufox_cli/browser.py +13 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/src/camoufox_cli/cli.py +32 -15
- camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6/src/camoufox_cli/config.py +130 -0
- camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6/src/camoufox_cli/install.py +127 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/tests/test_cli.py +135 -0
- camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6/tests/test_install.py +150 -0
- camoufox_cli-0.6.0/.gitignore +30 -0
- camoufox_cli-0.6.0/LICENSE +21 -0
- camoufox_cli-0.6.0/Makefile +30 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0}/PKG-INFO +32 -1
- camoufox_cli-0.6.0/README.md +257 -0
- camoufox_cli-0.6.0/js/.gitignore +2 -0
- camoufox_cli-0.6.0/js/LICENSE +21 -0
- camoufox_cli-0.6.0/js/package-lock.json +2719 -0
- camoufox_cli-0.6.0/js/package.json +47 -0
- camoufox_cli-0.6.0/js/pnpm-lock.yaml +1599 -0
- camoufox_cli-0.6.0/js/scripts/postinstall.js +11 -0
- camoufox_cli-0.6.0/js/src/browser.ts +199 -0
- camoufox_cli-0.6.0/js/src/cli.ts +506 -0
- camoufox_cli-0.6.0/js/src/commands.ts +352 -0
- camoufox_cli-0.6.0/js/src/config.ts +140 -0
- camoufox_cli-0.6.0/js/src/daemon.ts +27 -0
- camoufox_cli-0.6.0/js/src/identity.ts +206 -0
- camoufox_cli-0.6.0/js/src/install.ts +126 -0
- camoufox_cli-0.6.0/js/src/protocol.ts +32 -0
- camoufox_cli-0.6.0/js/src/proxy.ts +40 -0
- camoufox_cli-0.6.0/js/src/refs.ts +65 -0
- camoufox_cli-0.6.0/js/src/server.ts +131 -0
- camoufox_cli-0.6.0/js/tests/browser.test.ts +61 -0
- camoufox_cli-0.6.0/js/tests/cli.test.ts +455 -0
- camoufox_cli-0.6.0/js/tests/commands.test.ts +194 -0
- camoufox_cli-0.6.0/js/tests/e2e.test.ts +216 -0
- camoufox_cli-0.6.0/js/tests/install.test.ts +153 -0
- camoufox_cli-0.6.0/js/tests/protocol.test.ts +98 -0
- camoufox_cli-0.6.0/js/tests/proxy.test.ts +55 -0
- camoufox_cli-0.6.0/js/tests/refs.test.ts +150 -0
- camoufox_cli-0.6.0/js/tests/server.test.ts +193 -0
- camoufox_cli-0.6.0/js/tsconfig.json +14 -0
- camoufox_cli-0.6.0/js/vitest.config.ts +7 -0
- camoufox_cli-0.6.0/pyproject.toml +46 -0
- camoufox_cli-0.6.0/skills/camoufox-cli/SKILL.md +351 -0
- camoufox_cli-0.6.0/skills/camoufox-cli/references/commands.md +143 -0
- camoufox_cli-0.6.0/skills/camoufox-cli/references/snapshot-refs.md +221 -0
- camoufox_cli-0.6.0/src/camoufox_cli/__init__.py +0 -0
- camoufox_cli-0.6.0/src/camoufox_cli/__main__.py +38 -0
- camoufox_cli-0.6.0/src/camoufox_cli/browser.py +197 -0
- camoufox_cli-0.6.0/src/camoufox_cli/cli.py +517 -0
- camoufox_cli-0.6.0/src/camoufox_cli/commands.py +390 -0
- camoufox_cli-0.6.0/src/camoufox_cli/config.py +130 -0
- camoufox_cli-0.6.0/src/camoufox_cli/identity.py +215 -0
- camoufox_cli-0.6.0/src/camoufox_cli/install.py +127 -0
- camoufox_cli-0.6.0/src/camoufox_cli/protocol.py +24 -0
- camoufox_cli-0.6.0/src/camoufox_cli/proxy.py +30 -0
- camoufox_cli-0.6.0/src/camoufox_cli/refs.py +83 -0
- camoufox_cli-0.6.0/src/camoufox_cli/server.py +145 -0
- camoufox_cli-0.6.0/tests/__init__.py +0 -0
- camoufox_cli-0.6.0/tests/fixture.html +22 -0
- camoufox_cli-0.6.0/tests/test_cli.py +392 -0
- camoufox_cli-0.6.0/tests/test_commands.py +556 -0
- camoufox_cli-0.6.0/tests/test_e2e.py +217 -0
- camoufox_cli-0.6.0/tests/test_install.py +150 -0
- camoufox_cli-0.6.0/tests/test_protocol.py +63 -0
- camoufox_cli-0.6.0/tests/test_proxy.py +37 -0
- camoufox_cli-0.6.0/tests/test_refs.py +112 -0
- camoufox_cli-0.4.0/.claude/settings.local.json +0 -20
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/.gitignore +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/LICENSE +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/Makefile +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/.gitignore +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/LICENSE +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/pnpm-lock.yaml +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/scripts/postinstall.js +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/commands.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/daemon.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/identity.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/protocol.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/proxy.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/refs.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/server.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/tests/browser.test.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/tests/commands.test.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/tests/e2e.test.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/tests/protocol.test.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/tests/proxy.test.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/tests/refs.test.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/tests/server.test.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/tsconfig.json +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/vitest.config.ts +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/skills/camoufox-cli/references/snapshot-refs.md +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/src/camoufox_cli/__init__.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/src/camoufox_cli/__main__.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/src/camoufox_cli/commands.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/src/camoufox_cli/identity.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/src/camoufox_cli/protocol.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/src/camoufox_cli/proxy.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/src/camoufox_cli/refs.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/src/camoufox_cli/server.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/tests/__init__.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/tests/fixture.html +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/tests/test_commands.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/tests/test_e2e.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/tests/test_protocol.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/tests/test_proxy.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/tests/test_refs.py +0 -0
- {camoufox_cli-0.4.0 → camoufox_cli-0.6.0}/.claude-plugin/marketplace.json +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "camoufox-cli",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Benn Huang"
|
|
5
|
+
},
|
|
6
|
+
"metadata": {
|
|
7
|
+
"description": "Anti-detect browser automation CLI & Skills for AI agents, powered by Camoufox",
|
|
8
|
+
"version": "0.1.0"
|
|
9
|
+
},
|
|
10
|
+
"plugins": [
|
|
11
|
+
{
|
|
12
|
+
"name": "camoufox-cli",
|
|
13
|
+
"description": "Anti-detect browser automation skill with fingerprint spoofing for AI agents",
|
|
14
|
+
"source": "./",
|
|
15
|
+
"skills": [
|
|
16
|
+
"./skills/camoufox-cli"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -40,6 +40,10 @@ On Linux, install system dependencies with:
|
|
|
40
40
|
camoufox-cli install --with-deps
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
The browser is downloaded from GitHub releases. If the anonymous GitHub API
|
|
44
|
+
rate limit is exhausted (common on shared server/CI IPs), the installer
|
|
45
|
+
automatically falls back to scraping github.com release pages.
|
|
46
|
+
|
|
43
47
|
### Agent Skill
|
|
44
48
|
|
|
45
49
|
```bash
|
|
@@ -158,6 +162,33 @@ camoufox-cli cookies export file.json # Export cookies
|
|
|
158
162
|
--locale <tag> Force browser locale (e.g. "en-US" or "en-US,zh-CN")
|
|
159
163
|
```
|
|
160
164
|
|
|
165
|
+
## Config File
|
|
166
|
+
|
|
167
|
+
Tired of repeating the same flags on every call? Put defaults in `~/.camoufox-cli/config.json` (override the path with `$CAMOUFOX_CLI_CONFIG`):
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"default": {
|
|
172
|
+
"persistent": true,
|
|
173
|
+
"timeout": 3600,
|
|
174
|
+
"proxy": "http://user:pass@host:8080"
|
|
175
|
+
},
|
|
176
|
+
"sessions": {
|
|
177
|
+
"<your-session-name>": {
|
|
178
|
+
"proxy": "socks5://127.0.0.1:1080",
|
|
179
|
+
"locale": "zh-CN"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- **`default`** applies to every session.
|
|
186
|
+
- **`sessions` is optional.** You never pre-register sessions — the name is just whatever you pass to `--session`. A `sessions.<name>` block only kicks in when you happen to run `--session <name>`, layering its keys on top of `default`; any other session (or no `--session` at all) just uses `default`. Above, `camoufox-cli --session <your-session-name> open <url>` swaps in that block's proxy + locale, while every other session keeps the `default` proxy.
|
|
187
|
+
- **Command-line flags always win** over the file, which in turn wins over built-in defaults.
|
|
188
|
+
- Settable keys: `proxy`, `locale`, `geoip`, `persistent` (`true` / `false` / path string), `headed`, `timeout`, `json`. `session` is command-line only (it selects which block to apply), and per-command options like `--full` or `-i` are never read from config.
|
|
189
|
+
- A broken config never blocks a command — it's ignored with a warning on stderr.
|
|
190
|
+
- Config is read when a session's daemon **first launches**. If one is already running for that session, run `camoufox-cli --session <name> close` first for changes to take effect.
|
|
191
|
+
|
|
161
192
|
## Persistent Identity
|
|
162
193
|
|
|
163
194
|
Without `--persistent`, every launch gets a fresh random fingerprint. With `--persistent`, a `camoufox-cli.json` file is written next to the browser's user data. Fingerprint, OS, and canvas/font seeds are generated on the first launch and frozen afterwards; locale and proxy-derived timezone/geolocation are also saved but track whatever you pass on the command line. Every subsequent launch with the same path reloads the identity, so sites see the same device on every visit.
|
{camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/package-lock.json
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "camoufox-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "camoufox-cli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.5.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
{camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/browser.ts
RENAMED
|
@@ -4,10 +4,13 @@ import { execFileSync } from "node:child_process";
|
|
|
4
4
|
import { mkdirSync } from "node:fs";
|
|
5
5
|
import { Camoufox, launchOptions } from "camoufox-js";
|
|
6
6
|
import { firefox, type Browser, type BrowserContext, type Page } from "playwright-core";
|
|
7
|
+
import { ensureMmdb } from "./install.js";
|
|
7
8
|
import { loadOrCreate, toLaunchOptions } from "./identity.js";
|
|
8
9
|
import { parseProxySettings } from "./proxy.js";
|
|
9
10
|
import { RefRegistry } from "./refs.js";
|
|
10
11
|
|
|
12
|
+
const MAX_HISTORY = 200;
|
|
13
|
+
|
|
11
14
|
function ensureBrowserInstalled(): void {
|
|
12
15
|
try {
|
|
13
16
|
execFileSync("npx", ["camoufox-js", "path"], { stdio: "pipe" });
|
|
@@ -42,6 +45,12 @@ export class BrowserManager {
|
|
|
42
45
|
|
|
43
46
|
ensureBrowserInstalled();
|
|
44
47
|
|
|
48
|
+
if (this.proxy && this.geoip) {
|
|
49
|
+
// Geoip resolution lazily downloads the GeoIP db via the rate-limited
|
|
50
|
+
// GitHub API; fetch it through the resilient path first.
|
|
51
|
+
await ensureMmdb();
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
const launchOpts: Record<string, unknown> = { headless };
|
|
46
55
|
let proxySettings: { server: string; username?: string; password?: string } | null = null;
|
|
47
56
|
if (this.proxy) {
|
|
@@ -147,6 +156,9 @@ export class BrowserManager {
|
|
|
147
156
|
pushHistory(url: string): void {
|
|
148
157
|
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
149
158
|
this.history.push(url);
|
|
159
|
+
if (this.history.length > MAX_HISTORY) {
|
|
160
|
+
this.history = this.history.slice(-MAX_HISTORY);
|
|
161
|
+
}
|
|
150
162
|
this.historyIndex = this.history.length - 1;
|
|
151
163
|
}
|
|
152
164
|
|
{camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/src/cli.ts
RENAMED
|
@@ -7,6 +7,7 @@ import * as os from "node:os";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { execFileSync, spawn } from "node:child_process";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { loadDefaults } from "./config.js";
|
|
10
11
|
|
|
11
12
|
const SOCKET_PREFIX = "/tmp/camoufox-cli-";
|
|
12
13
|
|
|
@@ -105,42 +106,46 @@ export interface Flags {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export function parseArgs(argv: string[]): { flags: Flags; command: Record<string, unknown> } {
|
|
108
|
-
|
|
109
|
+
// Flag precedence: command line > config file (per-session block, then the
|
|
110
|
+
// `default` block) > built-in defaults. Only flags explicitly passed on the
|
|
111
|
+
// command line are collected here, so they always win over config.
|
|
112
|
+
const builtin: Flags = { session: "default", headed: false, timeout: 1800, json: false, persistent: null, proxy: null, geoip: true, locale: null };
|
|
113
|
+
const cli: Partial<Flags> = {};
|
|
109
114
|
const rest: string[] = [];
|
|
110
115
|
|
|
111
116
|
let i = 0;
|
|
112
117
|
while (i < argv.length) {
|
|
113
118
|
switch (argv[i]) {
|
|
114
119
|
case "--session":
|
|
115
|
-
|
|
120
|
+
cli.session = argv[++i] ?? (process.stderr.write("Error: --session requires a value\n"), process.exit(1), "");
|
|
116
121
|
break;
|
|
117
122
|
case "--headed":
|
|
118
|
-
|
|
123
|
+
cli.headed = true;
|
|
119
124
|
break;
|
|
120
125
|
case "--timeout":
|
|
121
|
-
|
|
126
|
+
cli.timeout = parseInt(argv[++i] ?? "1800", 10);
|
|
122
127
|
break;
|
|
123
128
|
case "--json":
|
|
124
|
-
|
|
129
|
+
cli.json = true;
|
|
125
130
|
break;
|
|
126
131
|
case "--persistent": {
|
|
127
132
|
// Optional value: if next arg looks like a path, use it; otherwise use default
|
|
128
133
|
const next = argv[i + 1];
|
|
129
134
|
if (next && (next.includes("/") || next.startsWith(".") || next.startsWith("~"))) {
|
|
130
|
-
|
|
135
|
+
cli.persistent = argv[++i];
|
|
131
136
|
} else {
|
|
132
|
-
|
|
137
|
+
cli.persistent = "";
|
|
133
138
|
}
|
|
134
139
|
break;
|
|
135
140
|
}
|
|
136
141
|
case "--proxy":
|
|
137
|
-
|
|
142
|
+
cli.proxy = argv[++i] ?? null;
|
|
138
143
|
break;
|
|
139
144
|
case "--no-geoip":
|
|
140
|
-
|
|
145
|
+
cli.geoip = false;
|
|
141
146
|
break;
|
|
142
147
|
case "--locale":
|
|
143
|
-
|
|
148
|
+
cli.locale = argv[++i] ?? (process.stderr.write("Error: --locale requires a value\n"), process.exit(1), "");
|
|
144
149
|
break;
|
|
145
150
|
default:
|
|
146
151
|
rest.push(argv[i]);
|
|
@@ -153,6 +158,10 @@ export function parseArgs(argv: string[]): { flags: Flags; command: Record<strin
|
|
|
153
158
|
process.exit(1);
|
|
154
159
|
}
|
|
155
160
|
|
|
161
|
+
// session selects which config block applies, so it comes only from the CLI.
|
|
162
|
+
const session = cli.session ?? builtin.session;
|
|
163
|
+
const flags: Flags = { ...builtin, ...loadDefaults(session), ...cli };
|
|
164
|
+
|
|
156
165
|
const command = buildCommand(rest[0], rest);
|
|
157
166
|
return { flags, command };
|
|
158
167
|
}
|
|
@@ -372,7 +381,8 @@ async function main() {
|
|
|
372
381
|
// Client-side: install
|
|
373
382
|
if (action === "install") {
|
|
374
383
|
process.stderr.write("[camoufox-cli] Downloading browser...\n");
|
|
375
|
-
|
|
384
|
+
const { installBrowser } = await import("./install.js");
|
|
385
|
+
await installBrowser();
|
|
376
386
|
process.stderr.write("[camoufox-cli] Browser installed.\n");
|
|
377
387
|
if ((command.params as any)?.with_deps) {
|
|
378
388
|
installSystemDeps();
|
|
@@ -480,7 +490,12 @@ Flags:
|
|
|
480
490
|
--persistent [path] Persistent identity — freeze fingerprint/OS/locale + store cookies/state (default: ~/.camoufox-cli/profiles/<session>)
|
|
481
491
|
--proxy <url> Proxy server (e.g. http://host:port or https://host:443)
|
|
482
492
|
--no-geoip Disable automatic GeoIP spoofing (auto-enabled with --proxy)
|
|
483
|
-
--locale <tag> Force browser locale (e.g. "en-US" or "en-US,zh-CN")
|
|
493
|
+
--locale <tag> Force browser locale (e.g. "en-US" or "en-US,zh-CN")
|
|
494
|
+
|
|
495
|
+
Config file:
|
|
496
|
+
~/.camoufox-cli/config.json sets defaults for the flags above (override the
|
|
497
|
+
path with $CAMOUFOX_CLI_CONFIG). Command-line flags always take precedence.
|
|
498
|
+
Use a "default" block plus optional per-session blocks under "sessions".`;
|
|
484
499
|
|
|
485
500
|
const isDirectRun = (() => {
|
|
486
501
|
try {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional config file support.
|
|
3
|
+
*
|
|
4
|
+
* A JSON file at ~/.camoufox-cli/config.json (override with the
|
|
5
|
+
* CAMOUFOX_CLI_CONFIG env var) supplies default values for daemon-launch
|
|
6
|
+
* flags, so common settings (proxy, locale, persistent…) don't have to be
|
|
7
|
+
* repeated on every invocation.
|
|
8
|
+
*
|
|
9
|
+
* Precedence: command-line flag > config `sessions.<name>` block >
|
|
10
|
+
* config `default` block > built-in default.
|
|
11
|
+
*
|
|
12
|
+
* Only flags that affect daemon launch are honored. `session` itself is never
|
|
13
|
+
* read from config — it selects which block to apply, so it can only come from
|
|
14
|
+
* the command line. Per-command flags (--full, -i, …) are never read.
|
|
15
|
+
*
|
|
16
|
+
* The config only takes effect when a session's daemon first launches; an
|
|
17
|
+
* already-running daemon is reused as-is (see ensureDaemon).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as os from "node:os";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
|
|
24
|
+
export interface ConfigDefaults {
|
|
25
|
+
headed?: boolean;
|
|
26
|
+
timeout?: number;
|
|
27
|
+
json?: boolean;
|
|
28
|
+
persistent?: string | null;
|
|
29
|
+
proxy?: string | null;
|
|
30
|
+
geoip?: boolean;
|
|
31
|
+
locale?: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Flags a config file may set, grouped by the value type each one expects.
|
|
35
|
+
// "session" is intentionally absent — it selects which block to apply, so it
|
|
36
|
+
// must come from the command line.
|
|
37
|
+
const BOOL_KEYS = new Set(["headed", "geoip", "json"]);
|
|
38
|
+
const STR_KEYS = new Set(["proxy", "locale"]);
|
|
39
|
+
const ALLOWED_KEYS = new Set([...BOOL_KEYS, ...STR_KEYS, "timeout", "persistent"]);
|
|
40
|
+
const INVALID = Symbol("invalid");
|
|
41
|
+
|
|
42
|
+
/** Resolved config path, read at call time so the env var can be overridden (incl. by tests). */
|
|
43
|
+
export function configPath(): string {
|
|
44
|
+
return process.env.CAMOUFOX_CLI_CONFIG || path.join(os.homedir(), ".camoufox-cli", "config.json");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Return config-derived flag defaults for `session`.
|
|
49
|
+
*
|
|
50
|
+
* Merges the top-level `default` block with the `sessions.<session>` block
|
|
51
|
+
* (the latter wins). Returns `{}` when the file is absent or malformed — a
|
|
52
|
+
* broken config never blocks a command, it is only ignored with a warning on
|
|
53
|
+
* stderr.
|
|
54
|
+
*/
|
|
55
|
+
export function loadDefaults(session: string): ConfigDefaults {
|
|
56
|
+
const p = configPath();
|
|
57
|
+
if (!fs.existsSync(p)) return {};
|
|
58
|
+
|
|
59
|
+
let raw: unknown;
|
|
60
|
+
try {
|
|
61
|
+
raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
62
|
+
} catch (e: any) {
|
|
63
|
+
process.stderr.write(`[camoufox-cli] Ignoring config ${p}: ${e.message}\n`);
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
68
|
+
process.stderr.write(`[camoufox-cli] Ignoring config ${p}: top level must be a JSON object\n`);
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const obj = raw as Record<string, unknown>;
|
|
73
|
+
const merged: Record<string, unknown> = {};
|
|
74
|
+
Object.assign(merged, clean(obj.default, p));
|
|
75
|
+
|
|
76
|
+
const sessions = obj.sessions;
|
|
77
|
+
if (typeof sessions === "object" && sessions !== null && !Array.isArray(sessions)) {
|
|
78
|
+
Object.assign(merged, clean((sessions as Record<string, unknown>)[session], p));
|
|
79
|
+
} else if (sessions !== undefined) {
|
|
80
|
+
process.stderr.write(`[camoufox-cli] Ignoring config ${p}: "sessions" must be an object\n`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return normalize(merged);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Keep only allowed keys from a block, warning on anything else. */
|
|
87
|
+
function clean(block: unknown, p: string): Record<string, unknown> {
|
|
88
|
+
if (block === undefined || block === null) return {};
|
|
89
|
+
if (typeof block !== "object" || Array.isArray(block)) {
|
|
90
|
+
process.stderr.write(`[camoufox-cli] Ignoring config ${p}: blocks must be objects\n`);
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
const out: Record<string, unknown> = {};
|
|
94
|
+
for (const [key, value] of Object.entries(block as Record<string, unknown>)) {
|
|
95
|
+
if (ALLOWED_KEYS.has(key)) out[key] = value;
|
|
96
|
+
else if (key === "session") process.stderr.write(`[camoufox-cli] Ignoring "session" in config — set it with --session on the command line\n`);
|
|
97
|
+
else process.stderr.write(`[camoufox-cli] Ignoring unknown config key: ${key}\n`);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate/coerce each config value to the type its flag expects.
|
|
104
|
+
*
|
|
105
|
+
* A value of the wrong type is dropped with a warning rather than passed
|
|
106
|
+
* through, so a malformed config is always ignored, never fatal: a non-string
|
|
107
|
+
* proxy/locale/persistent would crash daemon launch, a non-numeric timeout
|
|
108
|
+
* would fail silently, and a non-bool toggle (e.g. the string "false") would
|
|
109
|
+
* be silently truthy.
|
|
110
|
+
*/
|
|
111
|
+
function normalize(flags: Record<string, unknown>): ConfigDefaults {
|
|
112
|
+
const out: Record<string, unknown> = {};
|
|
113
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
114
|
+
const coerced = coerce(key, value);
|
|
115
|
+
if (coerced === INVALID) {
|
|
116
|
+
process.stderr.write(`[camoufox-cli] Ignoring invalid ${key} in config: ${JSON.stringify(value)}\n`);
|
|
117
|
+
} else {
|
|
118
|
+
out[key] = coerced;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out as ConfigDefaults;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Return the normalized value if it fits the flag's type, else the INVALID sentinel. */
|
|
125
|
+
function coerce(key: string, value: unknown): unknown {
|
|
126
|
+
if (BOOL_KEYS.has(key)) return typeof value === "boolean" ? value : INVALID;
|
|
127
|
+
// null is accepted as "unset" (falls back to the built-in default).
|
|
128
|
+
if (STR_KEYS.has(key)) return value === null || typeof value === "string" ? value : INVALID;
|
|
129
|
+
if (key === "timeout") {
|
|
130
|
+
// a finite number only — excludes bool, string and null
|
|
131
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : INVALID;
|
|
132
|
+
}
|
|
133
|
+
if (key === "persistent") {
|
|
134
|
+
// true -> "" (default profile path, resolved in main); false -> null
|
|
135
|
+
// (disabled); null/string kept as-is; anything else invalid.
|
|
136
|
+
if (typeof value === "boolean") return value ? "" : null;
|
|
137
|
+
return value === null || typeof value === "string" ? value : INVALID;
|
|
138
|
+
}
|
|
139
|
+
return value; // unreachable: clean already filtered to known keys
|
|
140
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser installation with resilient GitHub release discovery.
|
|
3
|
+
*
|
|
4
|
+
* Anonymous GitHub API requests are limited to 60/hour per IP, which makes
|
|
5
|
+
* installs flaky on servers behind shared egress IPs (CI runners, cloud
|
|
6
|
+
* hosts). Mitigations:
|
|
7
|
+
*
|
|
8
|
+
* - If the API fails, release assets are discovered by paging through
|
|
9
|
+
* github.com release pages, which are not behind the API rate limit.
|
|
10
|
+
* - The GeoIP database is downloaded from a fixed releases/latest/download
|
|
11
|
+
* URL (no API involved) at install time and before geoip launches, so
|
|
12
|
+
* upstream's lazy API-based download never triggers.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
import { DefaultAddons, maybeDownloadAddons } from "camoufox-js/dist/addons.js";
|
|
19
|
+
import { downloadMMDB } from "camoufox-js/dist/locale.js";
|
|
20
|
+
import { CamoufoxFetcher, INSTALL_DIR, installedVerStr, webdl } from "camoufox-js/dist/pkgman.js";
|
|
21
|
+
|
|
22
|
+
const HEADERS = { "User-Agent": "camoufox-cli" };
|
|
23
|
+
const MMDB_URL = "https://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/GeoLite2-City.mmdb";
|
|
24
|
+
|
|
25
|
+
async function assetsViaApi(repo: string): Promise<any[]> {
|
|
26
|
+
const resp = await fetch(`https://api.github.com/repos/${repo}/releases`, { headers: HEADERS });
|
|
27
|
+
if (!resp.ok) throw new Error(`GitHub API responded with ${resp.status}`);
|
|
28
|
+
const releases = (await resp.json()) as any[];
|
|
29
|
+
return releases.flatMap((release) => release.assets ?? []);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Discover release assets by paging through github.com release pages,
|
|
34
|
+
* newest release first. Lazy: stops requesting once the caller stops.
|
|
35
|
+
*/
|
|
36
|
+
export async function* assetsViaWeb(repo: string): AsyncGenerator<any> {
|
|
37
|
+
const escaped = repo.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
38
|
+
const tagRe = new RegExp(`/${escaped}/releases/tag/([^"<]+)`, "g");
|
|
39
|
+
const linkRe = new RegExp(`href="(/${escaped}/releases/download/[^"]+)"`, "g");
|
|
40
|
+
|
|
41
|
+
const seen = new Set<string>();
|
|
42
|
+
for (let pageNum = 1; ; pageNum++) {
|
|
43
|
+
const listing = await fetch(`https://github.com/${repo}/releases?page=${pageNum}`, { headers: HEADERS });
|
|
44
|
+
if (!listing.ok) throw new Error(`github.com responded with ${listing.status}`);
|
|
45
|
+
const tags = [...new Set([...(await listing.text()).matchAll(tagRe)].map((m) => m[1]))]
|
|
46
|
+
.filter((tag) => !seen.has(tag));
|
|
47
|
+
if (tags.length === 0) return;
|
|
48
|
+
for (const tag of tags) seen.add(tag);
|
|
49
|
+
for (const tag of tags) {
|
|
50
|
+
const page = await fetch(`https://github.com/${repo}/releases/expanded_assets/${tag}`, { headers: HEADERS });
|
|
51
|
+
if (!page.ok) continue;
|
|
52
|
+
for (const m of (await page.text()).matchAll(linkRe)) {
|
|
53
|
+
yield {
|
|
54
|
+
name: decodeURIComponent(m[1].split("/").pop()!),
|
|
55
|
+
browser_download_url: `https://github.com${m[1]}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Yield all release assets of a repo, newest release first. */
|
|
63
|
+
export async function* iterReleaseAssets(repo: string): AsyncGenerator<any> {
|
|
64
|
+
let assets: any[];
|
|
65
|
+
try {
|
|
66
|
+
assets = await assetsViaApi(repo);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
process.stderr.write(`[camoufox-cli] GitHub API failed (${e}), falling back to github.com pages...\n`);
|
|
69
|
+
yield* assetsViaWeb(repo);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
yield* assets;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class ResilientFetcher extends CamoufoxFetcher {
|
|
76
|
+
async getAsset(): Promise<any> {
|
|
77
|
+
for await (const asset of iterReleaseAssets(this.githubRepo)) {
|
|
78
|
+
const data = this.checkAsset(asset);
|
|
79
|
+
if (data) return data;
|
|
80
|
+
}
|
|
81
|
+
this.missingAssetError();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Download the GeoIP database if missing, without hitting the GitHub API.
|
|
87
|
+
* Failures are non-fatal: upstream still lazily downloads on first use.
|
|
88
|
+
*/
|
|
89
|
+
export async function ensureMmdb(): Promise<void> {
|
|
90
|
+
const mmdbFile = join(INSTALL_DIR.toString(), "GeoLite2-City.mmdb");
|
|
91
|
+
if (existsSync(mmdbFile)) return;
|
|
92
|
+
try {
|
|
93
|
+
const buf = await webdl(MMDB_URL, "Downloading GeoIP database", true);
|
|
94
|
+
writeFileSync(mmdbFile, buf);
|
|
95
|
+
return;
|
|
96
|
+
} catch {
|
|
97
|
+
rmSync(mmdbFile, { force: true });
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await downloadMMDB(); // upstream API-based path, as a last resort
|
|
101
|
+
} catch (e) {
|
|
102
|
+
rmSync(mmdbFile, { force: true });
|
|
103
|
+
process.stderr.write(`[camoufox-cli] GeoIP database download failed (${e}).\n`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Download and install the Camoufox browser, GeoIP database and addons. */
|
|
108
|
+
export async function installBrowser(): Promise<void> {
|
|
109
|
+
const fetcher = new ResilientFetcher();
|
|
110
|
+
await fetcher.init();
|
|
111
|
+
|
|
112
|
+
let installed: string | null = null;
|
|
113
|
+
try {
|
|
114
|
+
installed = installedVerStr();
|
|
115
|
+
} catch {
|
|
116
|
+
installed = null;
|
|
117
|
+
}
|
|
118
|
+
if (installed === fetcher.verstr) {
|
|
119
|
+
process.stderr.write(`[camoufox-cli] Camoufox v${installed} is already up to date.\n`);
|
|
120
|
+
} else {
|
|
121
|
+
await fetcher.install();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await ensureMmdb();
|
|
125
|
+
maybeDownloadAddons(DefaultAddons);
|
|
126
|
+
}
|
{camoufox_cli-0.4.0 → camoufox_cli-0.6.0/.claude/worktrees/busy-darwin-6b67a6}/js/tests/cli.test.ts
RENAMED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
2
5
|
import { buildCommand, getSocketPath, parseArgs } from "../src/cli.js";
|
|
6
|
+
import { loadDefaults } from "../src/config.js";
|
|
3
7
|
|
|
4
8
|
// buildCommand calls process.exit on error; mock it to throw instead
|
|
5
9
|
beforeEach(() => {
|
|
@@ -7,6 +11,8 @@ beforeEach(() => {
|
|
|
7
11
|
throw new Error(`process.exit(${code})`);
|
|
8
12
|
});
|
|
9
13
|
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
14
|
+
// Isolate from any real ~/.camoufox-cli/config.json on the dev's machine.
|
|
15
|
+
process.env.CAMOUFOX_CLI_CONFIG = path.join(os.tmpdir(), "camoufox-cli-absent-dir", "config.json");
|
|
10
16
|
});
|
|
11
17
|
|
|
12
18
|
describe("buildCommand", () => {
|
|
@@ -298,3 +304,152 @@ describe("getSocketPath", () => {
|
|
|
298
304
|
expect(getSocketPath("my-session")).toBe("/tmp/camoufox-cli-my-session.sock");
|
|
299
305
|
});
|
|
300
306
|
});
|
|
307
|
+
|
|
308
|
+
describe("config file", () => {
|
|
309
|
+
let dir: string;
|
|
310
|
+
let cfg: string;
|
|
311
|
+
|
|
312
|
+
beforeEach(() => {
|
|
313
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "camoufox-cli-test-"));
|
|
314
|
+
cfg = path.join(dir, "config.json");
|
|
315
|
+
process.env.CAMOUFOX_CLI_CONFIG = cfg;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
afterEach(() => {
|
|
319
|
+
delete process.env.CAMOUFOX_CLI_CONFIG;
|
|
320
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const write = (obj: unknown) => fs.writeFileSync(cfg, JSON.stringify(obj));
|
|
324
|
+
|
|
325
|
+
// --- loadDefaults ---
|
|
326
|
+
it("absent file -> {}", () => {
|
|
327
|
+
expect(loadDefaults("default")).toEqual({});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("default block", () => {
|
|
331
|
+
write({ default: { timeout: 60, headed: true } });
|
|
332
|
+
const out = loadDefaults("default");
|
|
333
|
+
expect(out.timeout).toBe(60);
|
|
334
|
+
expect(out.headed).toBe(true);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("session overrides default", () => {
|
|
338
|
+
write({ default: { locale: "en-US", timeout: 60 }, sessions: { work: { locale: "zh-CN" } } });
|
|
339
|
+
const out = loadDefaults("work");
|
|
340
|
+
expect(out.locale).toBe("zh-CN"); // session block wins
|
|
341
|
+
expect(out.timeout).toBe(60); // inherited from default block
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("unknown session uses default block", () => {
|
|
345
|
+
write({ default: { locale: "en-US" }, sessions: { work: { locale: "zh-CN" } } });
|
|
346
|
+
expect(loadDefaults("other").locale).toBe("en-US");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("session key excluded", () => {
|
|
350
|
+
write({ default: { session: "ignored", proxy: "http://h:1" } });
|
|
351
|
+
const out = loadDefaults("default") as any;
|
|
352
|
+
expect(out.session).toBeUndefined();
|
|
353
|
+
expect(out.proxy).toBe("http://h:1");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("unknown key ignored", () => {
|
|
357
|
+
write({ default: { bogus: 1, headed: true } });
|
|
358
|
+
const out = loadDefaults("default") as any;
|
|
359
|
+
expect(out.bogus).toBeUndefined();
|
|
360
|
+
expect(out.headed).toBe(true);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("persistent true -> empty-string sentinel", () => {
|
|
364
|
+
write({ default: { persistent: true } });
|
|
365
|
+
expect(loadDefaults("default").persistent).toBe("");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("persistent false -> null", () => {
|
|
369
|
+
write({ default: { persistent: false } });
|
|
370
|
+
expect(loadDefaults("default").persistent).toBeNull();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("persistent path kept", () => {
|
|
374
|
+
write({ default: { persistent: "/tmp/p" } });
|
|
375
|
+
expect(loadDefaults("default").persistent).toBe("/tmp/p");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("invalid persistent dropped", () => {
|
|
379
|
+
// A number would crash daemon launch; it must be dropped, not passed through.
|
|
380
|
+
write({ default: { persistent: 123, headed: true } });
|
|
381
|
+
const out = loadDefaults("default") as any;
|
|
382
|
+
expect(out.persistent).toBeUndefined();
|
|
383
|
+
expect(out.headed).toBe(true);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("persistent null disables", () => {
|
|
387
|
+
write({ default: { persistent: null } });
|
|
388
|
+
expect(loadDefaults("default").persistent).toBeNull();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("invalid timeout dropped", () => {
|
|
392
|
+
write({ default: { timeout: "abc", headed: true } });
|
|
393
|
+
const out = loadDefaults("default") as any;
|
|
394
|
+
expect(out.timeout).toBeUndefined();
|
|
395
|
+
expect(out.headed).toBe(true);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("timeout bool dropped", () => {
|
|
399
|
+
write({ default: { timeout: true } });
|
|
400
|
+
expect("timeout" in loadDefaults("default")).toBe(false);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("timeout float truncated", () => {
|
|
404
|
+
write({ default: { timeout: 3600.9 } });
|
|
405
|
+
expect(loadDefaults("default").timeout).toBe(3600);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("invalid proxy dropped", () => {
|
|
409
|
+
// A non-string proxy would crash daemon launch; it must be dropped.
|
|
410
|
+
write({ default: { proxy: 123, locale: "en-US" } });
|
|
411
|
+
const out = loadDefaults("default") as any;
|
|
412
|
+
expect(out.proxy).toBeUndefined();
|
|
413
|
+
expect(out.locale).toBe("en-US");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("bool field wrong type dropped", () => {
|
|
417
|
+
write({ default: { headed: "false", geoip: "no" } });
|
|
418
|
+
const out = loadDefaults("default") as any;
|
|
419
|
+
expect(out.headed).toBeUndefined();
|
|
420
|
+
expect(out.geoip).toBeUndefined();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("malformed json ignored", () => {
|
|
424
|
+
fs.writeFileSync(cfg, "{not valid json");
|
|
425
|
+
expect(loadDefaults("default")).toEqual({});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("non-object top level ignored", () => {
|
|
429
|
+
write([1, 2, 3]);
|
|
430
|
+
expect(loadDefaults("default")).toEqual({});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// --- parseArgs integration (precedence: CLI > config > built-in) ---
|
|
434
|
+
it("config supplies defaults", () => {
|
|
435
|
+
write({ default: { proxy: "http://h:1", headed: true } });
|
|
436
|
+
const { flags } = parseArgs(["open", "https://example.com"]);
|
|
437
|
+
expect(flags.proxy).toBe("http://h:1");
|
|
438
|
+
expect(flags.headed).toBe(true);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("CLI overrides config", () => {
|
|
442
|
+
write({ default: { timeout: 60 } });
|
|
443
|
+
const { flags } = parseArgs(["--timeout", "999", "open", "x"]);
|
|
444
|
+
expect(flags.timeout).toBe(999);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("session block applies", () => {
|
|
448
|
+
write({ sessions: { work: { locale: "zh-CN" } } });
|
|
449
|
+
const { flags } = parseArgs(["--session", "work", "open", "x"]);
|
|
450
|
+
expect(flags.locale).toBe("zh-CN");
|
|
451
|
+
// A different session doesn't pick up the "work" block.
|
|
452
|
+
const { flags: f2 } = parseArgs(["open", "x"]);
|
|
453
|
+
expect(f2.locale).toBeNull();
|
|
454
|
+
});
|
|
455
|
+
});
|