craftdriver 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -6
- package/README.md +17 -6
- package/dist/cli/agentSession.d.ts +62 -0
- package/dist/cli/agentSession.d.ts.map +1 -0
- package/dist/cli/agentSession.js +100 -0
- package/dist/cli/agentSession.js.map +1 -0
- package/dist/cli/artifactPaths.d.ts +42 -0
- package/dist/cli/artifactPaths.d.ts.map +1 -0
- package/dist/cli/artifactPaths.js +144 -0
- package/dist/cli/artifactPaths.js.map +1 -0
- package/dist/cli/bounds.d.ts +88 -0
- package/dist/cli/bounds.d.ts.map +1 -0
- package/dist/cli/bounds.js +128 -0
- package/dist/cli/bounds.js.map +1 -0
- package/dist/cli/client.d.ts +3 -0
- package/dist/cli/client.d.ts.map +1 -1
- package/dist/cli/client.js +24 -8
- package/dist/cli/client.js.map +1 -1
- package/dist/cli/daemon.d.ts +3 -0
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +152 -39
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/defaults.d.ts +23 -3
- package/dist/cli/defaults.d.ts.map +1 -1
- package/dist/cli/defaults.js +50 -5
- package/dist/cli/defaults.js.map +1 -1
- package/dist/cli/dispatcher.d.ts +54 -11
- package/dist/cli/dispatcher.d.ts.map +1 -1
- package/dist/cli/dispatcher.js +851 -54
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +305 -56
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +14 -14
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +328 -180
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/journal.d.ts +153 -0
- package/dist/cli/journal.d.ts.map +1 -0
- package/dist/cli/journal.js +252 -0
- package/dist/cli/journal.js.map +1 -0
- package/dist/cli/lineReader.d.ts +33 -0
- package/dist/cli/lineReader.d.ts.map +1 -0
- package/dist/cli/lineReader.js +66 -0
- package/dist/cli/lineReader.js.map +1 -0
- package/dist/cli/locatorCandidates.d.ts +44 -0
- package/dist/cli/locatorCandidates.d.ts.map +1 -0
- package/dist/cli/locatorCandidates.js +190 -0
- package/dist/cli/locatorCandidates.js.map +1 -0
- package/dist/cli/mcp/artifacts.d.ts +39 -1
- package/dist/cli/mcp/artifacts.d.ts.map +1 -1
- package/dist/cli/mcp/artifacts.js +104 -9
- package/dist/cli/mcp/artifacts.js.map +1 -1
- package/dist/cli/mcp/bounds.d.ts +65 -0
- package/dist/cli/mcp/bounds.d.ts.map +1 -0
- package/dist/cli/mcp/bounds.js +124 -0
- package/dist/cli/mcp/bounds.js.map +1 -0
- package/dist/cli/mcp/params.d.ts +38 -0
- package/dist/cli/mcp/params.d.ts.map +1 -0
- package/dist/cli/mcp/params.js +174 -0
- package/dist/cli/mcp/params.js.map +1 -0
- package/dist/cli/mcp/server.d.ts +94 -16
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/server.js +304 -150
- package/dist/cli/mcp/server.js.map +1 -1
- package/dist/cli/mcp/tools.d.ts +62 -17
- package/dist/cli/mcp/tools.d.ts.map +1 -1
- package/dist/cli/mcp/tools.js +373 -158
- package/dist/cli/mcp/tools.js.map +1 -1
- package/dist/cli/pageSemantics.d.ts +42 -0
- package/dist/cli/pageSemantics.d.ts.map +1 -0
- package/dist/cli/pageSemantics.js +194 -0
- package/dist/cli/pageSemantics.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +6 -18
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +689 -45
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/cli/protocol.d.ts +8 -0
- package/dist/cli/protocol.d.ts.map +1 -1
- package/dist/cli/sessionRegistry.d.ts +42 -0
- package/dist/cli/sessionRegistry.d.ts.map +1 -0
- package/dist/cli/sessionRegistry.js +99 -0
- package/dist/cli/sessionRegistry.js.map +1 -0
- package/dist/cli/snapshot.d.ts +100 -18
- package/dist/cli/snapshot.d.ts.map +1 -1
- package/dist/cli/snapshot.js +248 -102
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/cli/stateStore.d.ts +50 -0
- package/dist/cli/stateStore.d.ts.map +1 -0
- package/dist/cli/stateStore.js +122 -0
- package/dist/cli/stateStore.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/bidi/index.d.ts +1 -1
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +2 -2
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +3 -0
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +7 -0
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/bidi/network.d.ts +14 -1
- package/dist/lib/bidi/network.d.ts.map +1 -1
- package/dist/lib/bidi/network.js +45 -7
- package/dist/lib/bidi/network.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +30 -6
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +190 -45
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/bidi/types.d.ts +7 -2
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +38 -3
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +196 -30
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +54 -18
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +295 -92
- package/dist/lib/browserContext.js.map +1 -1
- package/dist/lib/by.d.ts +3 -3
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +7 -3
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/driver.d.ts +7 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +12 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/errors.d.ts +15 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +15 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/initScript.d.ts +9 -0
- package/dist/lib/initScript.d.ts.map +1 -0
- package/dist/lib/initScript.js +14 -0
- package/dist/lib/initScript.js.map +1 -0
- package/dist/lib/page.d.ts +22 -0
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +48 -0
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/secureFile.d.ts +8 -0
- package/dist/lib/secureFile.d.ts.map +1 -0
- package/dist/lib/secureFile.js +67 -0
- package/dist/lib/secureFile.js.map +1 -0
- package/dist/lib/sessionStateValidation.d.ts +35 -0
- package/dist/lib/sessionStateValidation.d.ts.map +1 -0
- package/dist/lib/sessionStateValidation.js +223 -0
- package/dist/lib/sessionStateValidation.js.map +1 -0
- package/dist/lib/timing.d.ts +8 -0
- package/dist/lib/timing.d.ts.map +1 -1
- package/dist/lib/timing.js +8 -0
- package/dist/lib/timing.js.map +1 -1
- package/dist/lib/tracing.d.ts +2 -1
- package/dist/lib/tracing.d.ts.map +1 -1
- package/dist/lib/tracing.js +12 -1
- package/dist/lib/tracing.js.map +1 -1
- package/dist/lib/visual/antialias.d.ts +41 -0
- package/dist/lib/visual/antialias.d.ts.map +1 -0
- package/dist/lib/visual/antialias.js +126 -0
- package/dist/lib/visual/antialias.js.map +1 -0
- package/dist/lib/visual/codec.d.ts +24 -0
- package/dist/lib/visual/codec.d.ts.map +1 -0
- package/dist/lib/visual/codec.js +11 -0
- package/dist/lib/visual/codec.js.map +1 -0
- package/dist/lib/visual/compare.d.ts +38 -0
- package/dist/lib/visual/compare.d.ts.map +1 -0
- package/dist/lib/visual/compare.js +209 -0
- package/dist/lib/visual/compare.js.map +1 -0
- package/dist/lib/visual/diff.d.ts +18 -0
- package/dist/lib/visual/diff.d.ts.map +1 -0
- package/dist/lib/visual/diff.js +23 -0
- package/dist/lib/visual/diff.js.map +1 -0
- package/dist/lib/visual/expectScreenshot.d.ts +65 -0
- package/dist/lib/visual/expectScreenshot.d.ts.map +1 -0
- package/dist/lib/visual/expectScreenshot.js +287 -0
- package/dist/lib/visual/expectScreenshot.js.map +1 -0
- package/dist/lib/visual/index.d.ts +10 -0
- package/dist/lib/visual/index.d.ts.map +1 -0
- package/dist/lib/visual/index.js +9 -0
- package/dist/lib/visual/index.js.map +1 -0
- package/dist/lib/visual/pixelMath.d.ts +32 -0
- package/dist/lib/visual/pixelMath.d.ts.map +1 -0
- package/dist/lib/visual/pixelMath.js +70 -0
- package/dist/lib/visual/pixelMath.js.map +1 -0
- package/dist/lib/visual/pngHeader.d.ts +27 -0
- package/dist/lib/visual/pngHeader.d.ts.map +1 -0
- package/dist/lib/visual/pngHeader.js +63 -0
- package/dist/lib/visual/pngHeader.js.map +1 -0
- package/dist/lib/visual/pngjsCodec.d.ts +22 -0
- package/dist/lib/visual/pngjsCodec.d.ts.map +1 -0
- package/dist/lib/visual/pngjsCodec.js +70 -0
- package/dist/lib/visual/pngjsCodec.js.map +1 -0
- package/dist/lib/visual/types.d.ts +89 -0
- package/dist/lib/visual/types.d.ts.map +1 -0
- package/dist/lib/visual/types.js +20 -0
- package/dist/lib/visual/types.js.map +1 -0
- package/dist/lib/visual/visualError.d.ts +32 -0
- package/dist/lib/visual/visualError.d.ts.map +1 -0
- package/dist/lib/visual/visualError.js +59 -0
- package/dist/lib/visual/visualError.js.map +1 -0
- package/dist/lib/wait.d.ts.map +1 -1
- package/dist/lib/wait.js +7 -1
- package/dist/lib/wait.js.map +1 -1
- package/docs/agents.md +56 -61
- package/docs/api-reference.md +16 -1
- package/docs/browser-api.md +24 -4
- package/docs/browser-context.md +18 -12
- package/docs/cli.md +346 -26
- package/docs/error-codes.md +6 -4
- package/docs/getting-started.md +6 -1
- package/docs/index.md +1 -1
- package/docs/mcp.md +129 -62
- package/docs/public/examples/agent-actions.html +48 -0
- package/docs/public/examples/agent-debug.html +86 -0
- package/docs/public/examples/agent-late-mutation.html +18 -0
- package/docs/public/examples/agent-ref-shift.html +33 -0
- package/docs/recipes/login-once-reuse-session.md +32 -4
- package/docs/screenshots.md +15 -6
- package/docs/session-management.md +64 -3
- package/docs/visual-testing.md +254 -0
- package/docs/why-craftdriver.md +4 -3
- package/package.json +5 -1
- package/skills/craftdriver/SKILL.md +59 -75
- package/skills/craftdriver/agents/openai.yaml +4 -0
- package/skills/craftdriver/cheatsheet.md +18 -4
- package/skills/craftdriver/cli.md +194 -68
- package/skills/craftdriver/patterns.md +14 -8
- package/skills/craftdriver/workflow.md +149 -0
package/dist/cli/parseArgs.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate raw `argv` into a `(cmd, args)` pair that the dispatcher
|
|
3
|
+
* understands. Hand-rolled because v1 has ~20 commands; pulling in
|
|
4
|
+
* `commander`/`yargs` would dwarf the library footprint.
|
|
5
|
+
*
|
|
6
|
+
* Recognised global flags (anywhere on the command line):
|
|
7
|
+
* --timeout <ms> per-call timeout override
|
|
8
|
+
* --json force JSON output even on a TTY
|
|
9
|
+
* --pretty force pretty output even when piped
|
|
10
|
+
* --headless / --headed launch option (only honoured for ephemeral / first daemon start)
|
|
11
|
+
* --browser <name> chrome|chromium|firefox|safari
|
|
12
|
+
* --ephemeral run from stdin, don't talk to daemon
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` when the user only asked for help / version.
|
|
15
|
+
*/
|
|
16
|
+
import { resolve } from 'node:path';
|
|
1
17
|
export const HELP_TEXT = `craftdriver — agent-first WebDriver CLI
|
|
2
18
|
|
|
3
19
|
USAGE
|
|
@@ -7,7 +23,14 @@ COMMON COMMANDS
|
|
|
7
23
|
go <url> navigate the active page
|
|
8
24
|
find <selector> [--all] [--limit N] [--offset M]
|
|
9
25
|
click <selector>
|
|
10
|
-
|
|
26
|
+
dblclick <selector>
|
|
27
|
+
fill <selector> <value> clear, then enter the value
|
|
28
|
+
type <text> type into the focused element
|
|
29
|
+
clear <selector>
|
|
30
|
+
check <selector> | uncheck <selector>
|
|
31
|
+
select <selector> <value> pick an <option> by its value attribute
|
|
32
|
+
focus <selector> focus a field (caret to end), then type
|
|
33
|
+
scroll <selector>
|
|
11
34
|
press <key> [selector]
|
|
12
35
|
hover <selector>
|
|
13
36
|
text [selector] [--limit N]
|
|
@@ -18,35 +41,81 @@ COMMON COMMANDS
|
|
|
18
41
|
wait load [--state load|domcontentloaded|networkidle]
|
|
19
42
|
exists <selector> 0-wait probe; exit 0 if any match
|
|
20
43
|
pages list open pages
|
|
44
|
+
page list open pages, marking the active one
|
|
45
|
+
page open [url] open a tab and select it
|
|
46
|
+
page select <index|id> send later commands to that tab
|
|
47
|
+
page close <index|id> close a tab
|
|
21
48
|
snapshot sanitized DOM summary with refs
|
|
22
49
|
(use ref=eN as a selector)
|
|
50
|
+
locators <selector> durable selectors for an element,
|
|
51
|
+
each re-checked against the live page
|
|
52
|
+
logs [list] [--kind console,error,request,response] [--level error]
|
|
53
|
+
[--contains text] [--since N] [--limit N]
|
|
54
|
+
what the page logged and requested
|
|
55
|
+
logs wait --contains <text> [--kind k] [--timeout ms]
|
|
56
|
+
logs clear
|
|
57
|
+
mock add <url-pattern> [--status N] [--body S] [--content-type T]
|
|
58
|
+
reply with a fixed response
|
|
59
|
+
mock block <url-pattern> fail matching requests
|
|
60
|
+
mock list | mock remove <id> | mock clear
|
|
61
|
+
trace start [name] [--no-screenshots]
|
|
62
|
+
record actions, console and network
|
|
63
|
+
trace stop [--zip] stop and report where it landed
|
|
64
|
+
trace status
|
|
65
|
+
state save <name> [--session-storage]
|
|
66
|
+
save cookies + local storage for reuse
|
|
67
|
+
state load <name> restore saved login state (pre-navigation on BiDi)
|
|
68
|
+
state list saved state names
|
|
23
69
|
screenshot [-o file.png] [--full-page] [--selector S]
|
|
24
70
|
eval <js> advanced; evaluate JS on active page
|
|
25
71
|
back | forward | reload | status | quit
|
|
26
72
|
|
|
73
|
+
KEYBOARD / MOUSE / DIALOGS / UPLOAD
|
|
74
|
+
key press|down|up <key> [--selector S]
|
|
75
|
+
mouse move|click [selector] [--x N --y N] [--button left|middle|right]
|
|
76
|
+
mouse down|up [--button left|middle|right]
|
|
77
|
+
mouse wheel [selector] [--delta-x N] [--delta-y N]
|
|
78
|
+
dialog inspect | dialog accept [text] | dialog dismiss
|
|
79
|
+
upload <selector> <file> [file...]
|
|
80
|
+
|
|
81
|
+
REFS VS. DURABLE LOCATORS
|
|
82
|
+
Refs (e1, e2, …) come from "snapshot" and are for exploration only. They
|
|
83
|
+
bind to one element in one document: if it is removed, duplicated, or the
|
|
84
|
+
page navigates, the ref fails with STALE_REF instead of quietly acting on
|
|
85
|
+
a different element. Never put a ref in a committed test — run
|
|
86
|
+
"locators <ref>" and use a candidate it reports as unique.
|
|
87
|
+
|
|
27
88
|
DAEMON
|
|
28
89
|
daemon start launch background browser (Unix socket)
|
|
29
90
|
daemon status PID, browser, active page URL
|
|
30
91
|
daemon stop graceful shutdown
|
|
92
|
+
Not available on Windows; use --ephemeral or MCP there.
|
|
93
|
+
|
|
94
|
+
NAMED SESSIONS
|
|
95
|
+
--session <name> route a command to its own browser
|
|
96
|
+
session list open sessions and the limit
|
|
97
|
+
session close [name] quit one session's browser, free the slot
|
|
98
|
+
|
|
99
|
+
Each session owns a browser, page selection, snapshot baseline and refs.
|
|
100
|
+
Sessions number their refs independently, so "e4" exists in more than one
|
|
101
|
+
session and always acts on the session you addressed — never carry a ref
|
|
102
|
+
between sessions. Sessions are created on first use and run independently,
|
|
103
|
+
so a slow command in one does not block another. Not available with
|
|
104
|
+
--ephemeral, which is a single browser that exits with the command.
|
|
31
105
|
|
|
32
|
-
INIT (
|
|
33
|
-
init
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
copilot → .github/copilot-instructions.md
|
|
38
|
-
claude → CLAUDE.md
|
|
39
|
-
cursor → .cursor/rules/craftdriver.mdc
|
|
40
|
-
gemini → GEMINI.md
|
|
41
|
-
all → every file above
|
|
106
|
+
INIT (safe project-local skill)
|
|
107
|
+
init codex [--dry-run] [--mcp]
|
|
108
|
+
installs .agents/skills/craftdriver without reading or changing
|
|
109
|
+
AGENTS.md, CLAUDE.md, other repository instructions, or Codex config
|
|
110
|
+
--mcp prints a project-pinned config snippet for manual installation
|
|
42
111
|
|
|
43
112
|
MCP SERVER (for hosted / sandboxed AI agents)
|
|
44
113
|
mcp speak Model Context Protocol on stdio
|
|
45
114
|
install snippet (Claude Code):
|
|
46
|
-
claude mcp add craftdriver -- npx -
|
|
115
|
+
claude mcp add craftdriver -- npx --no-install craftdriver mcp
|
|
47
116
|
install snippet (Cursor / Windsurf / Zed):
|
|
48
117
|
{ "mcpServers": { "craftdriver": {
|
|
49
|
-
"command": "npx", "args": ["-
|
|
118
|
+
"command": "npx", "args": ["--no-install", "craftdriver", "mcp"] } } }
|
|
50
119
|
|
|
51
120
|
EPHEMERAL MODE (sandboxed agents)
|
|
52
121
|
craftdriver --ephemeral < script.txt
|
|
@@ -62,15 +131,305 @@ FLAGS
|
|
|
62
131
|
--json | --pretty force output format
|
|
63
132
|
--headless / --headed override headless mode (first launch only)
|
|
64
133
|
--browser <chrome|chromium|firefox|safari>
|
|
134
|
+
--session <name> named daemon session (default: default)
|
|
65
135
|
--ephemeral no daemon; read commands from stdin
|
|
66
136
|
|
|
67
137
|
EXIT CODES
|
|
68
138
|
0 success 1 assertion/timeout/no-match 2 usage error
|
|
69
139
|
`;
|
|
140
|
+
/**
|
|
141
|
+
* Every flag the CLI accepts. The parser branches below are the real
|
|
142
|
+
* definition; this list exists to turn a typo into a suggestion, so keep the
|
|
143
|
+
* two in step — `parse-args` covers the drift.
|
|
144
|
+
*/
|
|
145
|
+
const KNOWN_FLAGS = [
|
|
146
|
+
'--help', '--version', '--json', '--pretty', '--ephemeral', '--headless',
|
|
147
|
+
'--headed', '--browser', '--session', '--timeout', '--limit', '--offset',
|
|
148
|
+
'--all', '--dry-run', '--mcp', '--force', '--full-page', '--session-storage',
|
|
149
|
+
'--kind', '--level', '--contains', '--since', '--no-screenshots', '--zip',
|
|
150
|
+
'--status', '--body', '--content-type', '--state', '--selector', '--output',
|
|
151
|
+
'--name', '--text', '--button', '--files', '--delta-x', '--delta-y',
|
|
152
|
+
'--x', '--y',
|
|
153
|
+
];
|
|
154
|
+
/**
|
|
155
|
+
* Positional and command-specific option contracts.
|
|
156
|
+
*
|
|
157
|
+
* The dispatcher still validates untrusted socket/MCP input, but the CLI must
|
|
158
|
+
* reject its own mistakes before it starts a daemon or browser. Without this
|
|
159
|
+
* table, `click #pay -s admin` ignored two extra arguments and `click #pay
|
|
160
|
+
* --all` accepted a real-but-irrelevant flag, then reported success.
|
|
161
|
+
*/
|
|
162
|
+
const COMMAND_SYNTAX = {
|
|
163
|
+
go: { min: 1, max: 1, usage: 'go <url>' },
|
|
164
|
+
click: { min: 1, max: 1, usage: 'click <selector>' },
|
|
165
|
+
dblclick: { min: 1, max: 1, usage: 'dblclick <selector>' },
|
|
166
|
+
fill: { min: 2, max: Infinity, usage: 'fill <selector> <value>' },
|
|
167
|
+
type: { min: 1, max: Infinity, usage: 'type <text>' },
|
|
168
|
+
press: { min: 1, max: 2, usage: 'press <key> [selector]' },
|
|
169
|
+
hover: { min: 1, max: 1, usage: 'hover <selector>' },
|
|
170
|
+
clear: { min: 1, max: 1, usage: 'clear <selector>' },
|
|
171
|
+
check: { min: 1, max: 1, usage: 'check <selector>' },
|
|
172
|
+
uncheck: { min: 1, max: 1, usage: 'uncheck <selector>' },
|
|
173
|
+
select: { min: 2, max: Infinity, usage: 'select <selector> <value>' },
|
|
174
|
+
focus: { min: 1, max: 1, usage: 'focus <selector>' },
|
|
175
|
+
scroll: { min: 1, max: 1, usage: 'scroll <selector>' },
|
|
176
|
+
locators: { min: 1, max: 1, usage: 'locators <selector>', options: ['limit'] },
|
|
177
|
+
upload: { min: 1, max: Infinity, usage: 'upload <selector> <file> [file...]', options: ['files'] },
|
|
178
|
+
key: { min: 2, max: Infinity, usage: 'key press|down|up <key>', options: ['selector'] },
|
|
179
|
+
mouse: {
|
|
180
|
+
min: 0,
|
|
181
|
+
max: 2,
|
|
182
|
+
usage: 'mouse move|click [selector] | mouse down|up | mouse wheel [selector]',
|
|
183
|
+
options: ['selector', 'x', 'y', 'button', 'deltaX', 'deltaY'],
|
|
184
|
+
},
|
|
185
|
+
dialog: { min: 0, max: Infinity, usage: 'dialog inspect | accept [text] | dismiss', options: ['text'] },
|
|
186
|
+
find: { min: 1, max: 1, usage: 'find <selector>', options: ['all', 'limit', 'offset'] },
|
|
187
|
+
exists: { min: 1, max: 1, usage: 'exists <selector>' },
|
|
188
|
+
text: { min: 0, max: 1, usage: 'text [selector]', options: ['limit'] },
|
|
189
|
+
attr: { min: 2, max: 2, usage: 'attr <selector> <name>' },
|
|
190
|
+
value: { min: 1, max: 1, usage: 'value <selector>' },
|
|
191
|
+
is: { min: 2, max: 2, usage: 'is visible|enabled|checked <selector>' },
|
|
192
|
+
wait: { min: 1, max: 1, usage: 'wait <selector>|load', options: ['state'] },
|
|
193
|
+
pages: { min: 0, max: 0, usage: 'pages' },
|
|
194
|
+
page: { min: 0, max: 2, usage: 'page list|open|select|close [target]' },
|
|
195
|
+
logs: { min: 0, max: 1, usage: 'logs [list|wait|clear]', options: ['kind', 'level', 'contains', 'since', 'limit'] },
|
|
196
|
+
mock: { min: 0, max: 2, usage: 'mock add|block|list|remove|clear [target]', options: ['status', 'body', 'contentType'] },
|
|
197
|
+
trace: { min: 0, max: 2, usage: 'trace start|stop|status [name]', options: ['name', 'noScreenshots', 'zip'] },
|
|
198
|
+
state: { min: 0, max: 2, usage: 'state save|load|list [name]', options: ['sessionStorage'] },
|
|
199
|
+
screenshot: { min: 0, max: 0, usage: 'screenshot [-o file.png]', options: ['path', 'fullPage', 'selector'] },
|
|
200
|
+
snapshot: { min: 0, max: 0, usage: 'snapshot' },
|
|
201
|
+
eval: { min: 1, max: Infinity, usage: 'eval <javascript>' },
|
|
202
|
+
back: { min: 0, max: 0, usage: 'back' },
|
|
203
|
+
forward: { min: 0, max: 0, usage: 'forward' },
|
|
204
|
+
reload: { min: 0, max: 0, usage: 'reload' },
|
|
205
|
+
status: { min: 0, max: 0, usage: 'status' },
|
|
206
|
+
quit: { min: 0, max: 0, usage: 'quit' },
|
|
207
|
+
daemon: { min: 0, max: 1, usage: 'daemon start|status|stop' },
|
|
208
|
+
session: { min: 0, max: 2, usage: 'session list|close [name]' },
|
|
209
|
+
init: { min: 1, max: 1, usage: 'init codex', options: ['dry-run', 'mcp', 'force'] },
|
|
210
|
+
mcp: { min: 0, max: 0, usage: 'mcp' },
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* Contracts for commands whose first positional selects an action.
|
|
214
|
+
*
|
|
215
|
+
* Keeping these beside the top-level table prevents a known-but-irrelevant
|
|
216
|
+
* flag from disappearing: `trace start --zip` used to start a trace and
|
|
217
|
+
* quietly ignore the requested archive, while `page list extra` listed pages
|
|
218
|
+
* and reported success. Counts include the action token itself; the default
|
|
219
|
+
* action covers the zero-token form.
|
|
220
|
+
*/
|
|
221
|
+
const ACTION_COMMAND_SYNTAX = {
|
|
222
|
+
key: {
|
|
223
|
+
defaultAction: '',
|
|
224
|
+
actions: {
|
|
225
|
+
press: { min: 2, max: Infinity, usage: 'key press <key>', options: ['selector'] },
|
|
226
|
+
down: { min: 2, max: Infinity, usage: 'key down <key>', options: ['selector'] },
|
|
227
|
+
up: { min: 2, max: Infinity, usage: 'key up <key>', options: ['selector'] },
|
|
228
|
+
// Backward-compatible spelling; the dispatcher maps it to top-level type.
|
|
229
|
+
type: { min: 2, max: Infinity, usage: 'key type <text>' },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
mouse: {
|
|
233
|
+
defaultAction: 'move',
|
|
234
|
+
actions: {
|
|
235
|
+
move: { min: 0, max: 2, usage: 'mouse move [selector]', options: ['selector', 'x', 'y'] },
|
|
236
|
+
click: { min: 1, max: 2, usage: 'mouse click [selector]', options: ['selector', 'x', 'y', 'button'] },
|
|
237
|
+
down: { min: 1, max: 1, usage: 'mouse down', options: ['button'] },
|
|
238
|
+
up: { min: 1, max: 1, usage: 'mouse up', options: ['button'] },
|
|
239
|
+
wheel: { min: 1, max: 2, usage: 'mouse wheel [selector]', options: ['selector', 'deltaX', 'deltaY'] },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
dialog: {
|
|
243
|
+
defaultAction: 'inspect',
|
|
244
|
+
actions: {
|
|
245
|
+
inspect: { min: 0, max: 1, usage: 'dialog inspect' },
|
|
246
|
+
accept: { min: 1, max: Infinity, usage: 'dialog accept [text]', options: ['text'] },
|
|
247
|
+
dismiss: { min: 1, max: 1, usage: 'dialog dismiss' },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
page: {
|
|
251
|
+
defaultAction: 'list',
|
|
252
|
+
actions: {
|
|
253
|
+
list: { min: 0, max: 1, usage: 'page list' },
|
|
254
|
+
open: { min: 1, max: 2, usage: 'page open [url]' },
|
|
255
|
+
select: { min: 2, max: 2, usage: 'page select <index|id>' },
|
|
256
|
+
close: { min: 2, max: 2, usage: 'page close <index|id>' },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
logs: {
|
|
260
|
+
defaultAction: 'list',
|
|
261
|
+
actions: {
|
|
262
|
+
list: { min: 0, max: 1, usage: 'logs [list]', options: ['kind', 'level', 'contains', 'since', 'limit'] },
|
|
263
|
+
wait: { min: 1, max: 1, usage: 'logs wait', options: ['kind', 'level', 'contains', 'since', 'limit'] },
|
|
264
|
+
clear: { min: 1, max: 1, usage: 'logs clear' },
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
mock: {
|
|
268
|
+
defaultAction: 'list',
|
|
269
|
+
actions: {
|
|
270
|
+
list: { min: 0, max: 1, usage: 'mock list' },
|
|
271
|
+
add: { min: 2, max: 2, usage: 'mock add <url-pattern>', options: ['status', 'body', 'contentType'] },
|
|
272
|
+
block: { min: 2, max: 2, usage: 'mock block <url-pattern>' },
|
|
273
|
+
remove: { min: 2, max: 2, usage: 'mock remove <id>' },
|
|
274
|
+
clear: { min: 1, max: 1, usage: 'mock clear' },
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
trace: {
|
|
278
|
+
defaultAction: 'status',
|
|
279
|
+
actions: {
|
|
280
|
+
start: { min: 1, max: 2, usage: 'trace start [name]', options: ['name', 'noScreenshots'] },
|
|
281
|
+
stop: { min: 1, max: 1, usage: 'trace stop', options: ['zip'] },
|
|
282
|
+
status: { min: 0, max: 1, usage: 'trace status' },
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
state: {
|
|
286
|
+
defaultAction: 'list',
|
|
287
|
+
actions: {
|
|
288
|
+
save: { min: 2, max: 2, usage: 'state save <name>', options: ['sessionStorage'] },
|
|
289
|
+
load: { min: 2, max: 2, usage: 'state load <name>' },
|
|
290
|
+
list: { min: 0, max: 1, usage: 'state list' },
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
daemon: {
|
|
294
|
+
defaultAction: 'status',
|
|
295
|
+
actions: {
|
|
296
|
+
start: { min: 1, max: 1, usage: 'daemon start' },
|
|
297
|
+
status: { min: 0, max: 1, usage: 'daemon status' },
|
|
298
|
+
stop: { min: 1, max: 1, usage: 'daemon stop' },
|
|
299
|
+
// Private child-process entry point used by autoStartDaemon.
|
|
300
|
+
__run__: { min: 1, max: 1, usage: 'daemon __run__' },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
session: {
|
|
304
|
+
defaultAction: 'list',
|
|
305
|
+
actions: {
|
|
306
|
+
list: { min: 0, max: 1, usage: 'session list' },
|
|
307
|
+
close: { min: 1, max: 2, usage: 'session close [name]' },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
const COMMAND_ALIASES = {
|
|
312
|
+
goto: 'go',
|
|
313
|
+
navigate: 'go',
|
|
314
|
+
doubleclick: 'dblclick',
|
|
315
|
+
attribute: 'attr',
|
|
316
|
+
shot: 'screenshot',
|
|
317
|
+
log: 'logs',
|
|
318
|
+
};
|
|
319
|
+
const OPTION_FLAG = {
|
|
320
|
+
'dry-run': '--dry-run',
|
|
321
|
+
all: '--all',
|
|
322
|
+
body: '--body',
|
|
323
|
+
button: '--button',
|
|
324
|
+
contains: '--contains',
|
|
325
|
+
contentType: '--content-type',
|
|
326
|
+
deltaX: '--delta-x',
|
|
327
|
+
deltaY: '--delta-y',
|
|
328
|
+
files: '--files',
|
|
329
|
+
force: '--force',
|
|
330
|
+
fullPage: '--full-page',
|
|
331
|
+
kind: '--kind',
|
|
332
|
+
level: '--level',
|
|
333
|
+
limit: '--limit',
|
|
334
|
+
mcp: '--mcp',
|
|
335
|
+
name: '--name',
|
|
336
|
+
noScreenshots: '--no-screenshots',
|
|
337
|
+
offset: '--offset',
|
|
338
|
+
path: '--output',
|
|
339
|
+
selector: '--selector',
|
|
340
|
+
sessionStorage: '--session-storage',
|
|
341
|
+
since: '--since',
|
|
342
|
+
state: '--state',
|
|
343
|
+
status: '--status',
|
|
344
|
+
text: '--text',
|
|
345
|
+
x: '--x',
|
|
346
|
+
y: '--y',
|
|
347
|
+
zip: '--zip',
|
|
348
|
+
};
|
|
349
|
+
function usageError(flags, message, usage) {
|
|
350
|
+
return {
|
|
351
|
+
cmd: '__usage_error__',
|
|
352
|
+
args: { message, ...(usage ? { usage: `craftdriver ${usage}` } : {}) },
|
|
353
|
+
flags,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function validateActionCommand(canonical, rest, opts, flags) {
|
|
357
|
+
const command = ACTION_COMMAND_SYNTAX[canonical];
|
|
358
|
+
if (!command)
|
|
359
|
+
return null;
|
|
360
|
+
const action = (rest[0] ?? command.defaultAction).toLowerCase();
|
|
361
|
+
const syntax = command.actions[action];
|
|
362
|
+
if (!syntax) {
|
|
363
|
+
return usageError(flags, `${canonical}: unknown action ${JSON.stringify(action)}`, COMMAND_SYNTAX[canonical].usage);
|
|
364
|
+
}
|
|
365
|
+
const allowed = new Set(['timeout', ...(syntax.options ?? [])]);
|
|
366
|
+
const invalidOption = Object.keys(opts).find((key) => !allowed.has(key));
|
|
367
|
+
if (invalidOption) {
|
|
368
|
+
return usageError(flags, `${OPTION_FLAG[invalidOption] ?? `--${invalidOption}`} is not valid for ${canonical} ${action}`, syntax.usage);
|
|
369
|
+
}
|
|
370
|
+
if (rest.length < syntax.min) {
|
|
371
|
+
return usageError(flags, `${canonical} ${action}: missing required argument`, syntax.usage);
|
|
372
|
+
}
|
|
373
|
+
if (rest.length > syntax.max) {
|
|
374
|
+
return usageError(flags, `${canonical} ${action}: unexpected argument ${JSON.stringify(rest[syntax.max])}`, syntax.usage);
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
/** Levenshtein distance, capped by the shorter input — inputs are tiny. */
|
|
379
|
+
function editDistance(a, b) {
|
|
380
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
381
|
+
const cur = new Array(b.length + 1).fill(0);
|
|
382
|
+
for (let i = 1; i <= a.length; i++) {
|
|
383
|
+
cur[0] = i;
|
|
384
|
+
for (let j = 1; j <= b.length; j++) {
|
|
385
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
386
|
+
}
|
|
387
|
+
prev.splice(0, prev.length, ...cur);
|
|
388
|
+
}
|
|
389
|
+
return prev[b.length];
|
|
390
|
+
}
|
|
391
|
+
/** Nearest known flag, when one is close enough to be worth naming. */
|
|
392
|
+
function suggestFlag(flag) {
|
|
393
|
+
let best = null;
|
|
394
|
+
let bestDistance = Infinity;
|
|
395
|
+
for (const known of KNOWN_FLAGS) {
|
|
396
|
+
const d = editDistance(flag, known);
|
|
397
|
+
if (d < bestDistance) {
|
|
398
|
+
bestDistance = d;
|
|
399
|
+
best = known;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Beyond a couple of edits the "did you mean" is noise, not help.
|
|
403
|
+
return best && bestDistance <= 3 ? { suggestion: best } : {};
|
|
404
|
+
}
|
|
70
405
|
export function parseArgv(argv) {
|
|
71
406
|
const flags = { launch: {} };
|
|
72
407
|
const positional = [];
|
|
73
408
|
const opts = {};
|
|
409
|
+
const takeValue = (flag, index) => {
|
|
410
|
+
const value = argv[index + 1];
|
|
411
|
+
if (value === undefined || value.startsWith('--')) {
|
|
412
|
+
return usageError(flags, `${flag} requires a value`);
|
|
413
|
+
}
|
|
414
|
+
return value;
|
|
415
|
+
};
|
|
416
|
+
const takeNumber = (flag, index, limits = {}) => {
|
|
417
|
+
const raw = takeValue(flag, index);
|
|
418
|
+
if (typeof raw !== 'string')
|
|
419
|
+
return raw;
|
|
420
|
+
const value = Number(raw);
|
|
421
|
+
const invalid = !Number.isFinite(value) ||
|
|
422
|
+
(limits.integer === true && !Number.isInteger(value)) ||
|
|
423
|
+
(limits.min !== undefined && value < limits.min) ||
|
|
424
|
+
(limits.max !== undefined && value > limits.max);
|
|
425
|
+
if (invalid) {
|
|
426
|
+
const range = limits.min !== undefined || limits.max !== undefined
|
|
427
|
+
? ` (${limits.min ?? '-∞'}..${limits.max ?? '∞'})`
|
|
428
|
+
: '';
|
|
429
|
+
return usageError(flags, `${flag} requires ${limits.integer ? 'an integer' : 'a number'}${range}; got ${JSON.stringify(raw)}`);
|
|
430
|
+
}
|
|
431
|
+
return value;
|
|
432
|
+
};
|
|
74
433
|
for (let i = 0; i < argv.length; i++) {
|
|
75
434
|
const a = argv[i];
|
|
76
435
|
if (a === '--help' || a === '-h')
|
|
@@ -81,49 +440,153 @@ export function parseArgv(argv) {
|
|
|
81
440
|
flags.json = true;
|
|
82
441
|
else if (a === '--pretty')
|
|
83
442
|
flags.pretty = true;
|
|
84
|
-
else if (a === '--explain')
|
|
85
|
-
flags.explain = true;
|
|
86
443
|
else if (a === '--ephemeral')
|
|
87
444
|
flags.ephemeral = true;
|
|
88
445
|
else if (a === '--headless')
|
|
89
446
|
flags.headless = true;
|
|
90
447
|
else if (a === '--headed')
|
|
91
448
|
flags.headless = false;
|
|
92
|
-
else if (a === '--browser')
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
flags
|
|
98
|
-
opts.timeout = n;
|
|
449
|
+
else if (a === '--browser') {
|
|
450
|
+
const value = takeValue(a, i);
|
|
451
|
+
if (typeof value !== 'string')
|
|
452
|
+
return value;
|
|
453
|
+
if (!['chrome', 'chromium', 'firefox', 'safari'].includes(value)) {
|
|
454
|
+
return usageError(flags, `--browser: unknown browser ${JSON.stringify(value)}`);
|
|
99
455
|
}
|
|
456
|
+
flags.launch.browserName = value;
|
|
457
|
+
i += 1;
|
|
458
|
+
}
|
|
459
|
+
// A global flag, not a command arg: it addresses a session rather than
|
|
460
|
+
// parameterising a command, and the dispatcher never sees it.
|
|
461
|
+
else if (a === '--session') {
|
|
462
|
+
const value = takeValue(a, i);
|
|
463
|
+
if (typeof value !== 'string')
|
|
464
|
+
return value;
|
|
465
|
+
flags.session = value;
|
|
466
|
+
i += 1;
|
|
467
|
+
}
|
|
468
|
+
else if (a === '--timeout') {
|
|
469
|
+
const value = takeNumber(a, i, { integer: true, min: 0, max: 300_000 });
|
|
470
|
+
if (typeof value !== 'number')
|
|
471
|
+
return value;
|
|
472
|
+
flags.timeout = value;
|
|
473
|
+
opts.timeout = value;
|
|
474
|
+
i += 1;
|
|
475
|
+
}
|
|
476
|
+
else if (a === '--limit') {
|
|
477
|
+
const value = takeNumber(a, i, { integer: true });
|
|
478
|
+
if (typeof value !== 'number')
|
|
479
|
+
return value;
|
|
480
|
+
opts.limit = value;
|
|
481
|
+
i += 1;
|
|
482
|
+
}
|
|
483
|
+
else if (a === '--offset') {
|
|
484
|
+
const value = takeNumber(a, i, { integer: true, min: 0 });
|
|
485
|
+
if (typeof value !== 'number')
|
|
486
|
+
return value;
|
|
487
|
+
opts.offset = value;
|
|
488
|
+
i += 1;
|
|
100
489
|
}
|
|
101
|
-
else if (a === '--limit')
|
|
102
|
-
opts.limit = Number(argv[++i]);
|
|
103
|
-
else if (a === '--offset')
|
|
104
|
-
opts.offset = Number(argv[++i]);
|
|
105
490
|
else if (a === '--all')
|
|
106
491
|
opts.all = true;
|
|
492
|
+
else if (a === '--dry-run')
|
|
493
|
+
opts['dry-run'] = true;
|
|
494
|
+
else if (a === '--mcp')
|
|
495
|
+
opts.mcp = true;
|
|
496
|
+
else if (a === '--force')
|
|
497
|
+
opts.force = true;
|
|
107
498
|
else if (a === '--full-page' || a === '--fullPage')
|
|
108
499
|
opts.fullPage = true;
|
|
109
|
-
else if (a === '--
|
|
110
|
-
opts.
|
|
111
|
-
else if (a === '--
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
500
|
+
else if (a === '--session-storage' || a === '--sessionStorage')
|
|
501
|
+
opts.sessionStorage = true;
|
|
502
|
+
else if (a === '--kind' || a === '--level' || a === '--contains') {
|
|
503
|
+
const value = takeValue(a, i);
|
|
504
|
+
if (typeof value !== 'string')
|
|
505
|
+
return value;
|
|
506
|
+
opts[a.slice(2)] = value;
|
|
507
|
+
i += 1;
|
|
508
|
+
}
|
|
509
|
+
else if (a === '--since') {
|
|
510
|
+
const value = takeNumber(a, i, { integer: true, min: 0 });
|
|
511
|
+
if (typeof value !== 'number')
|
|
512
|
+
return value;
|
|
513
|
+
opts.since = value;
|
|
514
|
+
i += 1;
|
|
515
|
+
}
|
|
516
|
+
else if (a === '--no-screenshots' || a === '--noScreenshots')
|
|
517
|
+
opts.noScreenshots = true;
|
|
518
|
+
else if (a === '--zip')
|
|
519
|
+
opts.zip = true;
|
|
520
|
+
else if (a === '--status') {
|
|
521
|
+
const value = takeNumber(a, i, { integer: true, min: 100, max: 599 });
|
|
522
|
+
if (typeof value !== 'number')
|
|
523
|
+
return value;
|
|
524
|
+
opts.status = value;
|
|
525
|
+
i += 1;
|
|
526
|
+
}
|
|
527
|
+
else if (a === '--body' || a === '--content-type' || a === '--contentType' ||
|
|
528
|
+
a === '--state' || a === '--selector' || a === '--name' ||
|
|
529
|
+
a === '--text' || a === '--button') {
|
|
530
|
+
const value = takeValue(a, i);
|
|
531
|
+
if (typeof value !== 'string')
|
|
532
|
+
return value;
|
|
533
|
+
const key = a === '--content-type' || a === '--contentType'
|
|
534
|
+
? 'contentType'
|
|
535
|
+
: a.slice(2);
|
|
536
|
+
opts[key] = value;
|
|
537
|
+
i += 1;
|
|
538
|
+
}
|
|
539
|
+
else if (a === '-o' || a === '--output') {
|
|
540
|
+
const value = takeValue(a, i);
|
|
541
|
+
if (typeof value !== 'string')
|
|
542
|
+
return value;
|
|
543
|
+
// Resolve in the short-lived caller process. The daemon has a different,
|
|
544
|
+
// long-lived cwd, so sending a relative output path there writes to the
|
|
545
|
+
// wrong directory.
|
|
546
|
+
opts.path = resolve(value);
|
|
547
|
+
i += 1;
|
|
548
|
+
}
|
|
549
|
+
// Resolved here, in the caller's process, for the same reason the
|
|
550
|
+
// positional form is: the daemon has its own long-lived cwd.
|
|
551
|
+
else if (a === '--files') {
|
|
552
|
+
const value = takeValue(a, i);
|
|
553
|
+
if (typeof value !== 'string')
|
|
554
|
+
return value;
|
|
555
|
+
opts.files = value
|
|
556
|
+
.split(',')
|
|
557
|
+
.map((f) => f.trim())
|
|
558
|
+
.filter((f) => f.length > 0)
|
|
559
|
+
.map((f) => resolve(f));
|
|
560
|
+
i += 1;
|
|
561
|
+
}
|
|
562
|
+
else if (a === '--delta-x' || a === '--deltaX' || a === '--delta-y' || a === '--deltaY' || a === '--x' || a === '--y') {
|
|
563
|
+
const value = takeNumber(a, i, { integer: true });
|
|
564
|
+
if (typeof value !== 'number')
|
|
565
|
+
return value;
|
|
566
|
+
const key = a === '--delta-x' || a === '--deltaX'
|
|
567
|
+
? 'deltaX'
|
|
568
|
+
: a === '--delta-y' || a === '--deltaY'
|
|
569
|
+
? 'deltaY'
|
|
570
|
+
: a.slice(2);
|
|
571
|
+
opts[key] = value;
|
|
572
|
+
i += 1;
|
|
573
|
+
}
|
|
574
|
+
// Everything after a bare `--` is data, so text that looks like a flag
|
|
575
|
+
// can still be typed: `type -- --draft`.
|
|
576
|
+
else if (a === '--') {
|
|
577
|
+
positional.push(...argv.slice(i + 1));
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
// Unknown long flags are a usage error, never silently absorbed. Accepting
|
|
581
|
+
// them meant `--sesion admin` ran against the default session: the typo
|
|
582
|
+
// vanished and a destructive command hit the wrong browser and the wrong
|
|
583
|
+
// login. Failing closed is the only safe reading of an unrecognised flag.
|
|
584
|
+
//
|
|
585
|
+
// Single-dash tokens are left alone: `type -bob` types the text "-bob",
|
|
586
|
+
// and `--delta-y -100` passes a negative number. Only `-h`, `-V` and `-o`
|
|
587
|
+
// are short flags, so there is little to typo and much to break.
|
|
117
588
|
else if (a.startsWith('--')) {
|
|
118
|
-
|
|
119
|
-
const key = a.slice(2);
|
|
120
|
-
const next = argv[i + 1];
|
|
121
|
-
if (next !== undefined && !next.startsWith('--')) {
|
|
122
|
-
opts[key] = next;
|
|
123
|
-
i++;
|
|
124
|
-
}
|
|
125
|
-
else
|
|
126
|
-
opts[key] = true;
|
|
589
|
+
return { cmd: '__unknown_flag__', args: { flag: a, ...suggestFlag(a) }, flags };
|
|
127
590
|
}
|
|
128
591
|
else
|
|
129
592
|
positional.push(a);
|
|
@@ -144,6 +607,44 @@ export function parseArgv(argv) {
|
|
|
144
607
|
}
|
|
145
608
|
const [cmd0, ...rest] = positional;
|
|
146
609
|
const lower = cmd0.toLowerCase();
|
|
610
|
+
const canonical = COMMAND_ALIASES[lower] ?? lower;
|
|
611
|
+
const syntax = COMMAND_SYNTAX[canonical];
|
|
612
|
+
if (syntax) {
|
|
613
|
+
const allowed = new Set(['timeout', ...(syntax.options ?? [])]);
|
|
614
|
+
const invalidOption = Object.keys(opts).find((key) => !allowed.has(key));
|
|
615
|
+
if (invalidOption) {
|
|
616
|
+
return usageError(flags, `${OPTION_FLAG[invalidOption] ?? `--${invalidOption}`} is not valid for ${cmd0}`, syntax.usage);
|
|
617
|
+
}
|
|
618
|
+
if (rest.length < syntax.min) {
|
|
619
|
+
return usageError(flags, `${cmd0}: missing required argument`, syntax.usage);
|
|
620
|
+
}
|
|
621
|
+
if (rest.length > syntax.max) {
|
|
622
|
+
return usageError(flags, `${cmd0}: unexpected argument ${JSON.stringify(rest[syntax.max])}`, syntax.usage);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const actionError = validateActionCommand(canonical, rest, opts, flags);
|
|
626
|
+
if (actionError)
|
|
627
|
+
return actionError;
|
|
628
|
+
if (canonical === 'upload') {
|
|
629
|
+
const optionFiles = Array.isArray(opts.files) ? opts.files : [];
|
|
630
|
+
if (rest.length < 2 && optionFiles.length === 0) {
|
|
631
|
+
return usageError(flags, 'upload: missing required argument "file"', COMMAND_SYNTAX.upload.usage);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (canonical === 'is' && !['visible', 'enabled', 'checked'].includes(rest[0].toLowerCase())) {
|
|
635
|
+
return usageError(flags, `is: unknown state ${JSON.stringify(rest[0])}`, COMMAND_SYNTAX.is.usage);
|
|
636
|
+
}
|
|
637
|
+
if (canonical === 'wait' && typeof opts.state === 'string') {
|
|
638
|
+
const states = rest[0] === 'load'
|
|
639
|
+
? ['load', 'domcontentloaded', 'networkidle']
|
|
640
|
+
: ['visible', 'hidden', 'attached', 'detached'];
|
|
641
|
+
if (!states.includes(opts.state)) {
|
|
642
|
+
return usageError(flags, `wait: unknown state ${JSON.stringify(opts.state)}`, COMMAND_SYNTAX.wait.usage);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (canonical === 'init' && rest[0].toLowerCase() !== 'codex') {
|
|
646
|
+
return usageError(flags, `init: unknown target ${JSON.stringify(rest[0])}`, COMMAND_SYNTAX.init.usage);
|
|
647
|
+
}
|
|
147
648
|
// Map surface command → dispatcher command + collect args.
|
|
148
649
|
switch (lower) {
|
|
149
650
|
case 'go':
|
|
@@ -153,13 +654,80 @@ export function parseArgv(argv) {
|
|
|
153
654
|
case 'click':
|
|
154
655
|
return { cmd: 'click', args: { selector: rest[0], ...opts }, flags };
|
|
155
656
|
case 'fill':
|
|
156
|
-
case 'type':
|
|
157
657
|
return { cmd: 'fill', args: { selector: rest[0], value: rest.slice(1).join(' '), ...opts }, flags };
|
|
658
|
+
case 'type':
|
|
659
|
+
// BREAKING (was an alias for `fill`): `type` now types into the
|
|
660
|
+
// focused element and takes no selector, matching `playwright-cli
|
|
661
|
+
// type <text>`. Use `fill <selector> <value>` to target a field.
|
|
662
|
+
return { cmd: 'type', args: { text: rest.join(' '), ...opts }, flags };
|
|
158
663
|
case 'press':
|
|
159
664
|
// craftdriver press <key> [selector]
|
|
160
665
|
return { cmd: 'press', args: { key: rest[0], ...(rest[1] ? { selector: rest[1] } : {}), ...opts }, flags };
|
|
161
666
|
case 'hover':
|
|
162
667
|
return { cmd: 'hover', args: { selector: rest[0], ...opts }, flags };
|
|
668
|
+
case 'dblclick':
|
|
669
|
+
case 'doubleclick':
|
|
670
|
+
return { cmd: 'dblclick', args: { selector: rest[0], ...opts }, flags };
|
|
671
|
+
case 'clear':
|
|
672
|
+
return { cmd: 'clear', args: { selector: rest[0], ...opts }, flags };
|
|
673
|
+
case 'check':
|
|
674
|
+
case 'uncheck':
|
|
675
|
+
return { cmd: lower, args: { selector: rest[0], ...opts }, flags };
|
|
676
|
+
case 'select':
|
|
677
|
+
// craftdriver select <selector> <value>
|
|
678
|
+
return { cmd: 'select', args: { selector: rest[0], value: rest.slice(1).join(' '), ...opts }, flags };
|
|
679
|
+
case 'focus':
|
|
680
|
+
case 'scroll':
|
|
681
|
+
return { cmd: lower, args: { selector: rest[0], ...opts }, flags };
|
|
682
|
+
case 'locators':
|
|
683
|
+
// craftdriver locators <selector> — durable selectors for a ref
|
|
684
|
+
return { cmd: 'locators', args: { selector: rest[0], ...opts }, flags };
|
|
685
|
+
case 'upload': {
|
|
686
|
+
// craftdriver upload <selector> <file> [file...]
|
|
687
|
+
//
|
|
688
|
+
// Resolve paths here, in the caller's process. The daemon is a
|
|
689
|
+
// long-lived process with its own cwd, so a relative path sent over
|
|
690
|
+
// the socket would resolve against wherever the daemon was started.
|
|
691
|
+
const files = rest.slice(1).map((f) => resolve(f));
|
|
692
|
+
return {
|
|
693
|
+
cmd: 'upload',
|
|
694
|
+
args: {
|
|
695
|
+
selector: rest[0],
|
|
696
|
+
...(files.length > 0 ? { files } : {}),
|
|
697
|
+
...opts,
|
|
698
|
+
},
|
|
699
|
+
flags,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
case 'key': {
|
|
703
|
+
// craftdriver key press|down|up <key> [--selector S]
|
|
704
|
+
const action = (rest[0] ?? 'press').toLowerCase();
|
|
705
|
+
const value = rest.slice(1).join(' ');
|
|
706
|
+
// `key type` was folded into the top-level `type` command. Route it
|
|
707
|
+
// there rather than emitting args the dispatcher cannot service.
|
|
708
|
+
if (action === 'type') {
|
|
709
|
+
return { cmd: 'type', args: { text: value, ...opts }, flags };
|
|
710
|
+
}
|
|
711
|
+
return { cmd: 'key', args: { action, key: value, ...opts }, flags };
|
|
712
|
+
}
|
|
713
|
+
case 'mouse': {
|
|
714
|
+
// craftdriver mouse move|click|wheel [selector] | down|up
|
|
715
|
+
const action = (rest[0] ?? 'move').toLowerCase();
|
|
716
|
+
return {
|
|
717
|
+
cmd: 'mouse',
|
|
718
|
+
args: { action, ...(rest[1] ? { selector: rest[1] } : {}), ...opts },
|
|
719
|
+
flags,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
case 'dialog': {
|
|
723
|
+
// craftdriver dialog inspect | accept [text] | dismiss
|
|
724
|
+
const action = (rest[0] ?? 'inspect').toLowerCase();
|
|
725
|
+
return {
|
|
726
|
+
cmd: 'dialog',
|
|
727
|
+
args: { action, ...(rest[1] ? { text: rest.slice(1).join(' ') } : {}), ...opts },
|
|
728
|
+
flags,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
163
731
|
case 'find':
|
|
164
732
|
return { cmd: 'find', args: { selector: rest[0], ...opts }, flags };
|
|
165
733
|
case 'exists':
|
|
@@ -183,6 +751,70 @@ export function parseArgv(argv) {
|
|
|
183
751
|
}
|
|
184
752
|
case 'pages':
|
|
185
753
|
return { cmd: 'pages', args: { ...opts }, flags };
|
|
754
|
+
case 'page': {
|
|
755
|
+
// craftdriver page list|open [url]|select <index|id>|close <index|id>
|
|
756
|
+
const action = (rest[0] ?? 'list').toLowerCase();
|
|
757
|
+
const target = rest[1];
|
|
758
|
+
return {
|
|
759
|
+
cmd: 'page',
|
|
760
|
+
args: {
|
|
761
|
+
action,
|
|
762
|
+
...(action === 'open' && target ? { url: target } : {}),
|
|
763
|
+
...(action !== 'open' && target ? { target } : {}),
|
|
764
|
+
...opts,
|
|
765
|
+
},
|
|
766
|
+
flags,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
case 'logs':
|
|
770
|
+
case 'log': {
|
|
771
|
+
// craftdriver logs [list|wait|clear] [--kind k] [--level l]
|
|
772
|
+
// [--contains s] [--since N] [--limit N]
|
|
773
|
+
const action = (rest[0] ?? 'list').toLowerCase();
|
|
774
|
+
return { cmd: 'logs', args: { action, ...opts }, flags };
|
|
775
|
+
}
|
|
776
|
+
case 'mock': {
|
|
777
|
+
// craftdriver mock add <pattern> [--status N] [--body S] [--content-type T]
|
|
778
|
+
// | block <pattern> | list | remove <id> | clear
|
|
779
|
+
const action = (rest[0] ?? 'list').toLowerCase();
|
|
780
|
+
const target = rest[1];
|
|
781
|
+
return {
|
|
782
|
+
cmd: 'mock',
|
|
783
|
+
args: {
|
|
784
|
+
action,
|
|
785
|
+
...(action === 'remove' && target ? { id: target } : {}),
|
|
786
|
+
...((action === 'add' || action === 'block') && target ? { pattern: target } : {}),
|
|
787
|
+
...opts,
|
|
788
|
+
},
|
|
789
|
+
flags,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
case 'trace': {
|
|
793
|
+
// craftdriver trace start [name] [--no-screenshots] | stop [--zip] | status
|
|
794
|
+
const action = (rest[0] ?? 'status').toLowerCase();
|
|
795
|
+
return {
|
|
796
|
+
cmd: 'trace',
|
|
797
|
+
args: {
|
|
798
|
+
action,
|
|
799
|
+
...(action === 'start' && rest[1] ? { name: rest[1] } : {}),
|
|
800
|
+
...opts,
|
|
801
|
+
},
|
|
802
|
+
flags,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
case 'state': {
|
|
806
|
+
// craftdriver state save <name> [--session-storage] | load <name> | list
|
|
807
|
+
const action = (rest[0] ?? 'list').toLowerCase();
|
|
808
|
+
return {
|
|
809
|
+
cmd: 'state',
|
|
810
|
+
args: {
|
|
811
|
+
action,
|
|
812
|
+
...(rest[1] ? { name: rest[1] } : {}),
|
|
813
|
+
...opts,
|
|
814
|
+
},
|
|
815
|
+
flags,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
186
818
|
case 'screenshot':
|
|
187
819
|
case 'shot':
|
|
188
820
|
return { cmd: 'screenshot', args: { ...opts }, flags };
|
|
@@ -201,8 +833,20 @@ export function parseArgv(argv) {
|
|
|
201
833
|
const sub = (rest[0] ?? 'status').toLowerCase();
|
|
202
834
|
return { cmd: `daemon:${sub}`, args: { ...opts }, flags };
|
|
203
835
|
}
|
|
836
|
+
case 'session': {
|
|
837
|
+
// craftdriver session list | close [name]
|
|
838
|
+
//
|
|
839
|
+
// `close` takes the name positionally or from `--session`, so both
|
|
840
|
+
// `session close checkout` and `session close --session checkout` work.
|
|
841
|
+
const sub = (rest[0] ?? 'list').toLowerCase();
|
|
842
|
+
return {
|
|
843
|
+
cmd: `session:${sub}`,
|
|
844
|
+
args: { ...(rest[1] ? { target: rest[1] } : {}), ...opts },
|
|
845
|
+
flags,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
204
848
|
case 'init': {
|
|
205
|
-
// craftdriver init
|
|
849
|
+
// craftdriver init codex [--dry-run] [--mcp]
|
|
206
850
|
const flavor = (rest[0] ?? '').toLowerCase();
|
|
207
851
|
return { cmd: 'init', args: { flavor, ...opts }, flags };
|
|
208
852
|
}
|