@rynx-ai/cli 0.1.11-beta.5 → 0.1.11-beta.50

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.
@@ -0,0 +1,5 @@
1
+ import type { AutostartRenderInput } from "./autostart.js";
2
+ export declare function renderAutostartEnvironmentFile(input: AutostartRenderInput): string;
3
+ export declare function serviceEnvironmentPath(input: AutostartRenderInput): string;
4
+ export declare function currentShell(): string;
5
+ export declare function autostartLauncherArguments(input: AutostartRenderInput, serviceFlag: string): string[];
@@ -0,0 +1,64 @@
1
+ import { readlinkSync } from "node:fs";
2
+ import { userInfo } from "node:os";
3
+ import path from "node:path";
4
+ // These describe a particular service invocation or IPC connection. The new
5
+ // service/Node process supplies its own values; all other variables are captured.
6
+ const BLOCKED_SERVICE_ENVIRONMENT = new Set([
7
+ "INVOCATION_ID",
8
+ "JOURNAL_STREAM",
9
+ "LISTEN_FDS",
10
+ "LISTEN_FDNAMES",
11
+ "LISTEN_PID",
12
+ "MAINPID",
13
+ "MANAGERPID",
14
+ "NOTIFY_SOCKET",
15
+ "ELECTRON_RUN_AS_NODE",
16
+ "NODE_CHANNEL_FD",
17
+ "NODE_UNIQUE_ID",
18
+ "PM2_HOME",
19
+ "RYNX_DAEMON_LIFECYCLE",
20
+ "RYNX_HOME",
21
+ "RYNX_REFRESH_LOGIN_SHELL_PATH",
22
+ "RYNX_SYSTEMD_SERVICE",
23
+ "RYNX_LAUNCHD_SERVICE",
24
+ "XPC_SERVICE_NAME",
25
+ "SYSTEMD_EXEC_PID",
26
+ "SYSTEMD_INVOCATION_ID",
27
+ "WATCHDOG_PID",
28
+ "WATCHDOG_USEC",
29
+ ]);
30
+ export function renderAutostartEnvironmentFile(input) {
31
+ const env = { ...input.environment, PATH: input.pathEnv };
32
+ return Object.entries(env)
33
+ .filter(([name, value]) => value !== undefined &&
34
+ /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
35
+ !BLOCKED_SERVICE_ENVIRONMENT.has(name))
36
+ .sort(([left], [right]) => left.localeCompare(right))
37
+ // Preserve literal newlines and quote metacharacters. The launcher parses
38
+ // this as data; it must not execute the assignments in a shell.
39
+ .map(([name, value]) => `${name}="${value.replace(/[\\"$`]/g, "\\$&")}"`)
40
+ .join("\n") + "\n";
41
+ }
42
+ export function serviceEnvironmentPath(input) {
43
+ return path.join(input.dataDir, "autostart.env");
44
+ }
45
+ export function currentShell() {
46
+ try {
47
+ const parent = readlinkSync(`/proc/${process.ppid}/exe`);
48
+ if (/^(?:ba|z|fi|da|k|mk|c|tc)?sh$/.test(path.basename(parent)))
49
+ return parent;
50
+ }
51
+ catch {
52
+ // The caller may be a launcher, or /proc may be unavailable.
53
+ }
54
+ return process.env.SHELL || userInfo().shell || "/bin/sh";
55
+ }
56
+ export function autostartLauncherArguments(input, serviceFlag) {
57
+ return [
58
+ path.join(path.dirname(input.cliPath), "autostart-launcher.js"),
59
+ input.shellPath ?? "/bin/sh",
60
+ input.cliPath,
61
+ input.dataDir,
62
+ serviceFlag,
63
+ ];
64
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ // Keep this entry independent of CLI initialization: restore the environment
5
+ // and initialize the user's shell before loading any Rynx runtime code.
6
+ const [shellPath, cliPath, dataDir, serviceFlag] = process.argv.slice(2);
7
+ if (!shellPath || !cliPath || !dataDir ||
8
+ (serviceFlag !== "--systemd-service" && serviceFlag !== "--launchd-service")) {
9
+ throw new Error("invalid autostart launcher arguments");
10
+ }
11
+ const contents = readFileSync(path.join(dataDir, "autostart.env"), "utf8");
12
+ // Parse only the quoted format emitted by renderAutostartEnvironmentFile.
13
+ // Never source assignments: a saved SHELLOPTS/UID may be readonly in a shell.
14
+ const assignments = [...contents.matchAll(/([A-Za-z_][A-Za-z0-9_]*)="((?:\\[\\"$`]|[^"\\])*)"\n/g)];
15
+ if (assignments.map(([assignment]) => assignment).join("") !== contents) {
16
+ throw new Error("invalid autostart environment snapshot");
17
+ }
18
+ const snapshot = Object.fromEntries(assignments.map(([, name = "", value = ""]) => [
19
+ name,
20
+ value.replace(/\\([\\"$`])/g, "$1"),
21
+ ]));
22
+ const isCShell = /^(?:t?csh)$/.test(path.basename(shellPath));
23
+ const command = "exec " + [
24
+ "/usr/bin/env",
25
+ `RYNX_HOME=${dataDir}`,
26
+ serviceFlag === "--systemd-service"
27
+ ? "RYNX_SYSTEMD_SERVICE=rynx.service"
28
+ : "RYNX_LAUNCHD_SERVICE=ai.rynx.daemon",
29
+ process.execPath,
30
+ cliPath,
31
+ "start",
32
+ serviceFlag,
33
+ ].map((value) => {
34
+ const quoted = `'${value.replaceAll("'", "'\\''")}'`;
35
+ return isCShell ? quoted.replaceAll("!", "\\!").replaceAll("\n", "\\\n") : quoted;
36
+ }).join(" ");
37
+ // csh/tcsh require -l on its own. Reading the command from stdin preserves
38
+ // their login AND interactive startup, including configuration guarded by prompt.
39
+ const result = spawnSync(shellPath, isCShell ? ["-l"] : ["-lic", command], {
40
+ env: { ...process.env, ...snapshot },
41
+ stdio: [isCShell ? "pipe" : "inherit", "inherit", "inherit"],
42
+ ...(isCShell ? { input: `${command}\n` } : {}),
43
+ });
44
+ // The shell passes its final environment directly to Rynx. Do not merge the
45
+ // snapshot again: that would undo configuration overrides and unset operations.
46
+ if (result.error)
47
+ throw result.error;
48
+ process.exitCode = result.status ?? 1;
@@ -0,0 +1,29 @@
1
+ export { renderSystemdUnit } from "./systemd-service.js";
2
+ export type AutostartManager = "launchd" | "systemd-user" | "unsupported";
3
+ export interface AutostartState {
4
+ supported: boolean;
5
+ registered: boolean;
6
+ active?: boolean;
7
+ lingerEnabled?: boolean;
8
+ lingerEnableCommand?: string;
9
+ manager: AutostartManager;
10
+ registrationPath?: string;
11
+ detail?: string;
12
+ }
13
+ export interface AutostartRenderInput {
14
+ cliPath: string;
15
+ dataDir: string;
16
+ environment?: NodeJS.ProcessEnv;
17
+ logDir: string;
18
+ nodePath: string;
19
+ pathEnv: string;
20
+ shellPath?: string;
21
+ }
22
+ export declare function autostartPlatformLabel(platform?: NodeJS.Platform): string | undefined;
23
+ export declare function inspectAutostart(): AutostartState;
24
+ export declare function enableAutostart(): AutostartState;
25
+ export declare function disableAutostart(): AutostartState;
26
+ /** Refresh an existing registration and its environment without enabling a new one. */
27
+ export declare function refreshAutostart(): boolean;
28
+ export declare function renderLaunchAgent(input: AutostartRenderInput): string;
29
+ export declare function assertMacLaunchdInvocation(): void;
@@ -0,0 +1,266 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { homedir, userInfo } from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { rynxHome } from "@rynx-ai/core";
7
+ import { autostartLauncherArguments, currentShell, renderAutostartEnvironmentFile, serviceEnvironmentPath, } from "./autostart-environment.js";
8
+ import { disableLinuxAutostart, enableLinuxAutostart, inspectLinuxAutostart, refreshLinuxServiceIfPresent, } from "./systemd-service.js";
9
+ export { renderSystemdUnit } from "./systemd-service.js";
10
+ const LAUNCHD_LABEL = "ai.rynx.daemon";
11
+ const LAUNCHD_SERVICE_ENV = "RYNX_LAUNCHD_SERVICE";
12
+ const COMMAND_TIMEOUT_MS = 5_000;
13
+ export function autostartPlatformLabel(platform = process.platform) {
14
+ if (platform === "darwin")
15
+ return "macOS LaunchAgent";
16
+ if (platform === "linux")
17
+ return "Linux user systemd";
18
+ return undefined;
19
+ }
20
+ export function inspectAutostart() {
21
+ if (process.platform !== "darwin" && process.platform !== "linux") {
22
+ return unsupportedState();
23
+ }
24
+ const context = currentContext();
25
+ return process.platform === "darwin" ? inspectMac(context) : inspectLinuxAutostart();
26
+ }
27
+ export function enableAutostart() {
28
+ if (process.platform !== "darwin" && process.platform !== "linux") {
29
+ throw new Error(`Rynx autostart is not supported on ${process.platform}`);
30
+ }
31
+ const context = currentContext();
32
+ if (process.platform === "darwin")
33
+ enableMac(context);
34
+ else
35
+ enableLinuxAutostart();
36
+ return inspectAutostart();
37
+ }
38
+ export function disableAutostart() {
39
+ if (process.platform !== "darwin" && process.platform !== "linux") {
40
+ throw new Error(`Rynx autostart is not supported on ${process.platform}`);
41
+ }
42
+ const context = currentContext();
43
+ if (process.platform === "darwin")
44
+ disableMac(context);
45
+ else
46
+ disableLinuxAutostart();
47
+ return inspectAutostart();
48
+ }
49
+ /** Refresh an existing registration and its environment without enabling a new one. */
50
+ export function refreshAutostart() {
51
+ if (process.platform === "darwin") {
52
+ const context = currentContext();
53
+ const registrationPath = launchAgentPath(context);
54
+ if (!existsSync(registrationPath))
55
+ return false;
56
+ return syncMacRegistration(context);
57
+ }
58
+ if (process.platform === "linux")
59
+ return refreshLinuxServiceIfPresent();
60
+ return false;
61
+ }
62
+ function unsupportedState() {
63
+ return {
64
+ supported: false,
65
+ registered: false,
66
+ manager: "unsupported",
67
+ detail: `Rynx autostart is not supported on ${process.platform}`,
68
+ };
69
+ }
70
+ export function renderLaunchAgent(input) {
71
+ const outLog = path.join(input.logDir, "autostart-out.log");
72
+ const errLog = path.join(input.logDir, "autostart-err.log");
73
+ const argv = [input.nodePath, ...autostartLauncherArguments(input, "--launchd-service")];
74
+ return `<?xml version="1.0" encoding="UTF-8"?>
75
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
76
+ <plist version="1.0">
77
+ <dict>
78
+ <key>Label</key>
79
+ <string>${escapeXml(LAUNCHD_LABEL)}</string>
80
+ <key>ProgramArguments</key>
81
+ <array>
82
+ ${argv.map((value) => ` <string>${escapeXml(value)}</string>`).join("\n")}
83
+ </array>
84
+ <key>RunAtLoad</key>
85
+ <true/>
86
+ <key>KeepAlive</key>
87
+ <false/>
88
+ <key>WorkingDirectory</key>
89
+ <string>${escapeXml(input.dataDir)}</string>
90
+ <key>EnvironmentVariables</key>
91
+ <dict>
92
+ <key>PATH</key>
93
+ <string>${escapeXml(input.pathEnv)}</string>
94
+ <key>RYNX_HOME</key>
95
+ <string>${escapeXml(input.dataDir)}</string>
96
+ <key>${LAUNCHD_SERVICE_ENV}</key>
97
+ <string>${LAUNCHD_LABEL}</string>
98
+ </dict>
99
+ <key>StandardOutPath</key>
100
+ <string>${escapeXml(outLog)}</string>
101
+ <key>StandardErrorPath</key>
102
+ <string>${escapeXml(errLog)}</string>
103
+ </dict>
104
+ </plist>
105
+ `;
106
+ }
107
+ export function assertMacLaunchdInvocation() {
108
+ if (process.platform !== "darwin") {
109
+ throw new Error("--launchd-service is only valid on macOS");
110
+ }
111
+ if (process.env[LAUNCHD_SERVICE_ENV] !== LAUNCHD_LABEL) {
112
+ throw new Error("--launchd-service requires the Rynx launchd environment");
113
+ }
114
+ }
115
+ function currentContext() {
116
+ const dataDir = rynxHome();
117
+ const homeDir = homedir();
118
+ return {
119
+ homeDir,
120
+ dataDir,
121
+ logDir: path.join(dataDir, "logs"),
122
+ cliPath: fileURLToPath(new URL("./cli.js", import.meta.url)),
123
+ environment: process.env,
124
+ nodePath: process.execPath,
125
+ pathEnv: process.env.PATH || defaultPath(),
126
+ shellPath: currentShell(),
127
+ };
128
+ }
129
+ function defaultPath() {
130
+ if (process.platform === "darwin") {
131
+ return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
132
+ }
133
+ return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
134
+ }
135
+ function launchAgentPath(context) {
136
+ return path.join(context.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
137
+ }
138
+ function inspectMac(context) {
139
+ const registrationPath = launchAgentPath(context);
140
+ return {
141
+ supported: true,
142
+ registered: existsSync(registrationPath),
143
+ active: launchctlLoaded(),
144
+ manager: "launchd",
145
+ registrationPath,
146
+ };
147
+ }
148
+ function enableMac(context) {
149
+ mkdirSync(context.logDir, { recursive: true, mode: 0o700 });
150
+ syncMacRegistration(context);
151
+ }
152
+ function syncMacRegistration(context) {
153
+ const registrationPath = launchAgentPath(context);
154
+ const previous = existsSync(registrationPath)
155
+ ? readFileSync(registrationPath, "utf8")
156
+ : undefined;
157
+ const content = renderLaunchAgent(context);
158
+ const changed = previous !== content;
159
+ const environmentPath = serviceEnvironmentPath(context);
160
+ const environment = renderAutostartEnvironmentFile(context);
161
+ const previousEnvironment = existsSync(environmentPath)
162
+ ? readFileSync(environmentPath, "utf8")
163
+ : undefined;
164
+ const environmentChanged = previousEnvironment !== environment;
165
+ if (!changed && !environmentChanged)
166
+ return false;
167
+ const wasLoaded = changed && launchctlLoaded();
168
+ const restore = () => {
169
+ if (environmentChanged)
170
+ restoreRegistrationFile(environmentPath, previousEnvironment, 0o600);
171
+ if (changed)
172
+ restoreRegistrationFile(registrationPath, previous);
173
+ };
174
+ try {
175
+ if (environmentChanged)
176
+ replaceFileAtomically(environmentPath, environment, 0o600);
177
+ if (changed)
178
+ replaceFileAtomically(registrationPath, content);
179
+ }
180
+ catch (error) {
181
+ restore();
182
+ throw error;
183
+ }
184
+ // An environment-only refresh needs no reload: the job reads it on every run.
185
+ if (!wasLoaded)
186
+ return true;
187
+ if (!launchctlBootout(registrationPath)) {
188
+ restore();
189
+ throw new Error(`launchctl could not unload ${LAUNCHD_LABEL}; the previous file was restored`);
190
+ }
191
+ if (launchctlBootstrap(registrationPath))
192
+ return true;
193
+ restore();
194
+ if (previous !== undefined && launchctlBootstrap(registrationPath)) {
195
+ throw new Error(`launchctl could not load the updated registration; the previous job was restored`);
196
+ }
197
+ throw new Error(`launchctl could not load the updated registration and could not restore the previous job`);
198
+ }
199
+ function disableMac(context) {
200
+ const registrationPath = launchAgentPath(context);
201
+ if (launchctlLoaded() && !launchctlBootout(registrationPath)) {
202
+ throw new Error(`launchctl could not unload ${LAUNCHD_LABEL}`);
203
+ }
204
+ rmSync(registrationPath, { force: true });
205
+ rmSync(serviceEnvironmentPath(context), { force: true });
206
+ }
207
+ function launchctlLoaded() {
208
+ const uid = userInfo().uid;
209
+ return run("launchctl", ["print", `gui/${uid}/${LAUNCHD_LABEL}`]).status === 0;
210
+ }
211
+ function launchctlBootstrap(registrationPath) {
212
+ const uid = userInfo().uid;
213
+ const modern = run("launchctl", [
214
+ "bootstrap",
215
+ `gui/${uid}`,
216
+ registrationPath,
217
+ ]);
218
+ if (modern.status === 0)
219
+ return true;
220
+ return run("launchctl", ["load", "-w", registrationPath]).status === 0;
221
+ }
222
+ function launchctlBootout(registrationPath) {
223
+ const uid = userInfo().uid;
224
+ const modern = run("launchctl", [
225
+ "bootout",
226
+ `gui/${uid}/${LAUNCHD_LABEL}`,
227
+ ]);
228
+ if (modern.status === 0)
229
+ return true;
230
+ return run("launchctl", ["unload", "-w", registrationPath]).status === 0;
231
+ }
232
+ function run(command, args) {
233
+ return spawnSync(command, [...args], {
234
+ encoding: "utf8",
235
+ stdio: "pipe",
236
+ timeout: COMMAND_TIMEOUT_MS,
237
+ killSignal: "SIGTERM",
238
+ });
239
+ }
240
+ function escapeXml(value) {
241
+ return value
242
+ .replaceAll("&", "&amp;")
243
+ .replaceAll("<", "&lt;")
244
+ .replaceAll(">", "&gt;");
245
+ }
246
+ function replaceFileAtomically(file, content, mode = 0o644) {
247
+ mkdirSync(path.dirname(file), { recursive: true });
248
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
249
+ try {
250
+ writeFileSync(temporary, content, {
251
+ encoding: "utf8",
252
+ flag: "wx",
253
+ mode,
254
+ });
255
+ renameSync(temporary, file);
256
+ }
257
+ finally {
258
+ rmSync(temporary, { force: true });
259
+ }
260
+ }
261
+ function restoreRegistrationFile(file, previous, mode = 0o644) {
262
+ if (previous === undefined)
263
+ rmSync(file, { force: true });
264
+ else
265
+ replaceFileAtomically(file, previous, mode);
266
+ }
@@ -1,22 +1,22 @@
1
1
  import type { RuntimeBrowserBootstrapCredential } from "@rynx-ai/protocol/runtime-browser-bootstrap";
2
- export type BrowserCliSubcommand = "install" | "update" | "version" | "clean" | "open" | "status" | "pages" | "endpoint" | "snapshot" | "navigate" | "click" | "type" | "screenshot" | "close";
2
+ export type BrowserCliSubcommand = "exec" | "install" | "update" | "version" | "clean" | "open" | "status" | "pages" | "endpoint" | "headers" | "close";
3
3
  export interface BrowserCliArgs {
4
4
  subcommand: BrowserCliSubcommand;
5
5
  json: boolean;
6
6
  ensure: boolean;
7
+ argv?: string[];
7
8
  channel?: string;
8
9
  version?: string;
9
10
  runtime?: string;
10
11
  session?: string;
12
+ page?: string;
11
13
  url?: string;
12
- ref?: string;
13
- selector?: string;
14
- text?: string;
15
- output?: string;
16
- format?: "png" | "jpeg" | "webp";
17
- quality?: number;
18
- x?: number;
19
- y?: number;
14
+ headersAction?: "get" | "set";
15
+ headers?: string[];
16
+ expectedRevision?: string;
17
+ headersEnabled?: boolean;
18
+ clearHeaders?: boolean;
19
+ showHeaderValues?: boolean;
20
20
  }
21
21
  export interface BrowserCliTarget {
22
22
  runtimeSelector: string;
@@ -1,4 +1,5 @@
1
1
  const SPECS = {
2
+ exec: { values: ["--session", "--page"], flags: ["--json"] },
2
3
  install: { values: ["--channel", "--version"], flags: ["--json"] },
3
4
  update: { values: ["--channel"], flags: ["--json"] },
4
5
  version: { values: [], flags: ["--json"] },
@@ -11,35 +12,31 @@ const SPECS = {
11
12
  status: { values: ["--session", "--runtime"], flags: ["--json"] },
12
13
  pages: { values: ["--session", "--runtime"], flags: ["--json"] },
13
14
  endpoint: { values: ["--session"], flags: ["--ensure", "--json"] },
14
- snapshot: { values: ["--session"], flags: ["--json"] },
15
- navigate: {
16
- values: ["--url", "--session"],
17
- flags: ["--json"],
18
- positionalUrl: true,
19
- },
20
- click: {
21
- values: ["--ref", "--selector", "--x", "--y", "--session"],
22
- flags: ["--json"],
23
- },
24
- type: {
25
- values: ["--ref", "--selector", "--text", "--session"],
26
- flags: ["--json"],
27
- },
28
- screenshot: {
29
- values: ["--output", "--format", "--quality", "--session"],
30
- flags: ["--json"],
15
+ headers: {
16
+ values: ["--session", "--runtime", "--header", "--expected-revision"],
17
+ flags: ["--json", "--enable", "--disable", "--clear", "--show-values"],
18
+ positionalAction: true,
31
19
  },
32
20
  close: { values: ["--session", "--runtime"], flags: ["--json"] },
33
21
  };
34
22
  export function parseBrowserCliArgs(args) {
23
+ let argv;
24
+ if (args[0] === "exec") {
25
+ const boundary = args.indexOf("--");
26
+ if (boundary < 1 || boundary === args.length - 1)
27
+ throw new Error("browser exec: use rynx browser exec [--page <id>] -- <agent-browser command and arguments>");
28
+ argv = args.slice(boundary + 1);
29
+ args = args.slice(0, boundary);
30
+ }
35
31
  const subcommand = args[0];
36
32
  if (!isBrowserSubcommand(subcommand)) {
37
33
  throw new Error("browser: expected install, update, version, clean, open, status, pages, endpoint, " +
38
- "snapshot, navigate, click, type, screenshot, or close");
34
+ "exec, headers, or close; page actions use browser exec -- <agent-browser command>");
39
35
  }
40
36
  const spec = SPECS[subcommand];
41
37
  const values = new Map();
42
38
  const flags = new Set();
39
+ const headerValues = [];
43
40
  const positionals = [];
44
41
  for (let index = 1; index < args.length; index += 1) {
45
42
  const arg = args[index];
@@ -51,13 +48,17 @@ export function parseBrowserCliArgs(args) {
51
48
  continue;
52
49
  }
53
50
  if (spec.values.includes(arg)) {
54
- if (values.has(arg))
51
+ if (arg !== "--header" && values.has(arg)) {
55
52
  throw new Error(`browser ${subcommand}: duplicate option ${arg}`);
53
+ }
56
54
  const value = args[index + 1];
57
55
  if (!value || value.startsWith("--")) {
58
56
  throw new Error(`browser ${subcommand}: ${arg} requires a value`);
59
57
  }
60
- values.set(arg, value);
58
+ if (arg === "--header")
59
+ headerValues.push(value);
60
+ else
61
+ values.set(arg, value);
61
62
  index += 1;
62
63
  continue;
63
64
  }
@@ -65,69 +66,63 @@ export function parseBrowserCliArgs(args) {
65
66
  }
66
67
  positionals.push(arg);
67
68
  }
68
- if (!spec.positionalUrl && positionals.length > 0) {
69
+ if (!spec.positionalUrl && !spec.positionalAction && positionals.length > 0) {
69
70
  throw new Error(`browser ${subcommand}: unexpected argument ${positionals[0]}`);
70
71
  }
71
72
  if (positionals.length > 1) {
72
73
  throw new Error(`browser ${subcommand}: unexpected extra argument ${positionals[1]}`);
73
74
  }
74
- if (positionals.length === 1 && values.has("--url")) {
75
+ if (spec.positionalUrl && positionals.length === 1 && values.has("--url")) {
75
76
  throw new Error("browser open: pass the URL once, either positionally or with --url");
76
77
  }
77
78
  const result = {
78
79
  subcommand,
79
80
  json: flags.has("--json"),
80
81
  ensure: flags.has("--ensure"),
82
+ ...(argv ? { argv } : {}),
81
83
  ...(values.has("--channel") ? { channel: values.get("--channel") } : {}),
82
84
  ...(values.has("--version") ? { version: values.get("--version") } : {}),
83
85
  ...(values.has("--runtime") ? { runtime: values.get("--runtime") } : {}),
84
86
  ...(values.has("--session") ? { session: values.get("--session") } : {}),
85
- ...(values.has("--url") || positionals[0]
87
+ ...(values.has("--page") ? { page: nonEmpty(values.get("--page"), "--page") } : {}),
88
+ ...(spec.positionalUrl && (values.has("--url") || positionals[0])
86
89
  ? { url: values.get("--url") ?? positionals[0] }
87
90
  : {}),
88
91
  };
89
- if (values.has("--ref"))
90
- result.ref = nonEmpty(values.get("--ref"), "--ref");
91
- if (values.has("--selector"))
92
- result.selector = nonEmpty(values.get("--selector"), "--selector");
93
- if (values.has("--text"))
94
- result.text = values.get("--text");
95
- if (values.has("--output"))
96
- result.output = nonEmpty(values.get("--output"), "--output");
97
- if (values.has("--format")) {
98
- const format = values.get("--format");
99
- if (format !== "png" && format !== "jpeg" && format !== "webp") {
100
- throw new Error(`browser screenshot: --format must be png, jpeg, or webp`);
92
+ if (subcommand === "headers") {
93
+ const action = positionals[0];
94
+ if (action !== "get" && action !== "set") {
95
+ throw new Error("browser headers: expected get or set");
101
96
  }
102
- result.format = format;
103
- }
104
- if (values.has("--quality")) {
105
- result.quality = integer(values.get("--quality"), "--quality", 0, 100);
106
- }
107
- if (values.has("--x"))
108
- result.x = finiteNumber(values.get("--x"), "--x");
109
- if (values.has("--y"))
110
- result.y = finiteNumber(values.get("--y"), "--y");
111
- if (subcommand === "navigate" && !result.url) {
112
- throw new Error("browser navigate: URL is required");
113
- }
114
- if (subcommand === "click") {
115
- const targets = Number(result.ref !== undefined)
116
- + Number(result.selector !== undefined)
117
- + Number(result.x !== undefined || result.y !== undefined);
118
- if (targets !== 1 || (result.x === undefined) !== (result.y === undefined)) {
119
- throw new Error("browser click: pass exactly one of --ref, --selector, or both --x and --y");
97
+ if (flags.has("--enable") && flags.has("--disable")) {
98
+ throw new Error("browser headers set: choose either --enable or --disable");
120
99
  }
121
- }
122
- if (subcommand === "type") {
123
- if ((result.ref === undefined) === (result.selector === undefined)) {
124
- throw new Error("browser type: pass exactly one of --ref or --selector");
100
+ result.headersAction = action;
101
+ if (headerValues.length > 0)
102
+ result.headers = headerValues;
103
+ if (values.has("--expected-revision")) {
104
+ result.expectedRevision = nonEmpty(values.get("--expected-revision"), "--expected-revision");
105
+ }
106
+ if (flags.has("--enable"))
107
+ result.headersEnabled = true;
108
+ if (flags.has("--disable"))
109
+ result.headersEnabled = false;
110
+ if (flags.has("--clear"))
111
+ result.clearHeaders = true;
112
+ if (flags.has("--show-values"))
113
+ result.showHeaderValues = true;
114
+ if (action === "get" && (headerValues.length > 0 ||
115
+ result.expectedRevision !== undefined ||
116
+ result.headersEnabled !== undefined ||
117
+ result.clearHeaders)) {
118
+ throw new Error("browser headers get: update options are not allowed");
119
+ }
120
+ if (action === "set" && result.showHeaderValues) {
121
+ throw new Error("browser headers set: --show-values is only valid with get");
122
+ }
123
+ if (action === "set" && headerValues.length > 0 && result.clearHeaders) {
124
+ throw new Error("browser headers set: --clear cannot be combined with --header");
125
125
  }
126
- if (result.text === undefined)
127
- throw new Error("browser type: --text is required");
128
- }
129
- if (subcommand === "screenshot" && !result.output) {
130
- throw new Error("browser screenshot: --output is required");
131
126
  }
132
127
  return result;
133
128
  }
@@ -166,16 +161,3 @@ function nonEmpty(value, option) {
166
161
  throw new Error(`${option} must not be empty`);
167
162
  return value;
168
163
  }
169
- function finiteNumber(value, option) {
170
- const parsed = Number(value);
171
- if (!Number.isFinite(parsed))
172
- throw new Error(`${option} must be a finite number`);
173
- return parsed;
174
- }
175
- function integer(value, option, minimum, maximum) {
176
- const parsed = Number(value);
177
- if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
178
- throw new Error(`${option} must be an integer from ${minimum} to ${maximum}`);
179
- }
180
- return parsed;
181
- }
@@ -0,0 +1 @@
1
+ export declare function runAutostartCommand(args: readonly string[]): Promise<number>;