craftdriver 1.7.0 → 1.9.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.
Files changed (234) hide show
  1. package/CHANGELOG.md +25 -12
  2. package/README.md +16 -6
  3. package/dist/cli/agentSession.d.ts +62 -0
  4. package/dist/cli/agentSession.d.ts.map +1 -0
  5. package/dist/cli/agentSession.js +100 -0
  6. package/dist/cli/agentSession.js.map +1 -0
  7. package/dist/cli/artifactPaths.d.ts +42 -0
  8. package/dist/cli/artifactPaths.d.ts.map +1 -0
  9. package/dist/cli/artifactPaths.js +144 -0
  10. package/dist/cli/artifactPaths.js.map +1 -0
  11. package/dist/cli/bounds.d.ts +88 -0
  12. package/dist/cli/bounds.d.ts.map +1 -0
  13. package/dist/cli/bounds.js +128 -0
  14. package/dist/cli/bounds.js.map +1 -0
  15. package/dist/cli/client.d.ts +3 -0
  16. package/dist/cli/client.d.ts.map +1 -1
  17. package/dist/cli/client.js +24 -8
  18. package/dist/cli/client.js.map +1 -1
  19. package/dist/cli/daemon.d.ts +3 -0
  20. package/dist/cli/daemon.d.ts.map +1 -1
  21. package/dist/cli/daemon.js +152 -39
  22. package/dist/cli/daemon.js.map +1 -1
  23. package/dist/cli/defaults.d.ts +23 -3
  24. package/dist/cli/defaults.d.ts.map +1 -1
  25. package/dist/cli/defaults.js +50 -5
  26. package/dist/cli/defaults.js.map +1 -1
  27. package/dist/cli/dispatcher.d.ts +54 -11
  28. package/dist/cli/dispatcher.d.ts.map +1 -1
  29. package/dist/cli/dispatcher.js +851 -54
  30. package/dist/cli/dispatcher.js.map +1 -1
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +305 -56
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/init.d.ts +14 -14
  35. package/dist/cli/init.d.ts.map +1 -1
  36. package/dist/cli/init.js +328 -180
  37. package/dist/cli/init.js.map +1 -1
  38. package/dist/cli/journal.d.ts +153 -0
  39. package/dist/cli/journal.d.ts.map +1 -0
  40. package/dist/cli/journal.js +252 -0
  41. package/dist/cli/journal.js.map +1 -0
  42. package/dist/cli/lineReader.d.ts +33 -0
  43. package/dist/cli/lineReader.d.ts.map +1 -0
  44. package/dist/cli/lineReader.js +66 -0
  45. package/dist/cli/lineReader.js.map +1 -0
  46. package/dist/cli/locatorCandidates.d.ts +44 -0
  47. package/dist/cli/locatorCandidates.d.ts.map +1 -0
  48. package/dist/cli/locatorCandidates.js +190 -0
  49. package/dist/cli/locatorCandidates.js.map +1 -0
  50. package/dist/cli/mcp/artifacts.d.ts +39 -1
  51. package/dist/cli/mcp/artifacts.d.ts.map +1 -1
  52. package/dist/cli/mcp/artifacts.js +104 -9
  53. package/dist/cli/mcp/artifacts.js.map +1 -1
  54. package/dist/cli/mcp/bounds.d.ts +65 -0
  55. package/dist/cli/mcp/bounds.d.ts.map +1 -0
  56. package/dist/cli/mcp/bounds.js +124 -0
  57. package/dist/cli/mcp/bounds.js.map +1 -0
  58. package/dist/cli/mcp/params.d.ts +38 -0
  59. package/dist/cli/mcp/params.d.ts.map +1 -0
  60. package/dist/cli/mcp/params.js +174 -0
  61. package/dist/cli/mcp/params.js.map +1 -0
  62. package/dist/cli/mcp/server.d.ts +94 -16
  63. package/dist/cli/mcp/server.d.ts.map +1 -1
  64. package/dist/cli/mcp/server.js +304 -150
  65. package/dist/cli/mcp/server.js.map +1 -1
  66. package/dist/cli/mcp/tools.d.ts +62 -17
  67. package/dist/cli/mcp/tools.d.ts.map +1 -1
  68. package/dist/cli/mcp/tools.js +373 -158
  69. package/dist/cli/mcp/tools.js.map +1 -1
  70. package/dist/cli/pageSemantics.d.ts +6 -0
  71. package/dist/cli/pageSemantics.d.ts.map +1 -0
  72. package/dist/cli/pageSemantics.js +6 -0
  73. package/dist/cli/pageSemantics.js.map +1 -0
  74. package/dist/cli/parseArgs.d.ts +6 -18
  75. package/dist/cli/parseArgs.d.ts.map +1 -1
  76. package/dist/cli/parseArgs.js +689 -45
  77. package/dist/cli/parseArgs.js.map +1 -1
  78. package/dist/cli/protocol.d.ts +8 -0
  79. package/dist/cli/protocol.d.ts.map +1 -1
  80. package/dist/cli/selector.d.ts +3 -3
  81. package/dist/cli/selector.d.ts.map +1 -1
  82. package/dist/cli/selector.js +6 -9
  83. package/dist/cli/selector.js.map +1 -1
  84. package/dist/cli/sessionRegistry.d.ts +42 -0
  85. package/dist/cli/sessionRegistry.d.ts.map +1 -0
  86. package/dist/cli/sessionRegistry.js +99 -0
  87. package/dist/cli/sessionRegistry.js.map +1 -0
  88. package/dist/cli/snapshot.d.ts +100 -19
  89. package/dist/cli/snapshot.d.ts.map +1 -1
  90. package/dist/cli/snapshot.js +295 -108
  91. package/dist/cli/snapshot.js.map +1 -1
  92. package/dist/cli/stateStore.d.ts +50 -0
  93. package/dist/cli/stateStore.d.ts.map +1 -0
  94. package/dist/cli/stateStore.js +122 -0
  95. package/dist/cli/stateStore.js.map +1 -0
  96. package/dist/index.d.ts +3 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +1 -0
  99. package/dist/index.js.map +1 -1
  100. package/dist/lib/a11y.d.ts +6 -2
  101. package/dist/lib/a11y.d.ts.map +1 -1
  102. package/dist/lib/a11y.js +3 -1
  103. package/dist/lib/a11y.js.map +1 -1
  104. package/dist/lib/bidi/evaluate.d.ts +11 -0
  105. package/dist/lib/bidi/evaluate.d.ts.map +1 -0
  106. package/dist/lib/bidi/evaluate.js +25 -0
  107. package/dist/lib/bidi/evaluate.js.map +1 -0
  108. package/dist/lib/bidi/index.d.ts +1 -1
  109. package/dist/lib/bidi/index.d.ts.map +1 -1
  110. package/dist/lib/bidi/index.js +2 -2
  111. package/dist/lib/bidi/index.js.map +1 -1
  112. package/dist/lib/bidi/logs.d.ts +3 -0
  113. package/dist/lib/bidi/logs.d.ts.map +1 -1
  114. package/dist/lib/bidi/logs.js +7 -0
  115. package/dist/lib/bidi/logs.js.map +1 -1
  116. package/dist/lib/bidi/network.d.ts +14 -1
  117. package/dist/lib/bidi/network.d.ts.map +1 -1
  118. package/dist/lib/bidi/network.js +45 -7
  119. package/dist/lib/bidi/network.js.map +1 -1
  120. package/dist/lib/bidi/storage.d.ts +30 -6
  121. package/dist/lib/bidi/storage.d.ts.map +1 -1
  122. package/dist/lib/bidi/storage.js +190 -45
  123. package/dist/lib/bidi/storage.js.map +1 -1
  124. package/dist/lib/bidi/types.d.ts +9 -2
  125. package/dist/lib/bidi/types.d.ts.map +1 -1
  126. package/dist/lib/browser.d.ts +7 -4
  127. package/dist/lib/browser.d.ts.map +1 -1
  128. package/dist/lib/browser.js +186 -65
  129. package/dist/lib/browser.js.map +1 -1
  130. package/dist/lib/browserContext.d.ts +58 -24
  131. package/dist/lib/browserContext.d.ts.map +1 -1
  132. package/dist/lib/browserContext.js +300 -99
  133. package/dist/lib/browserContext.js.map +1 -1
  134. package/dist/lib/by.d.ts +68 -30
  135. package/dist/lib/by.d.ts.map +1 -1
  136. package/dist/lib/by.js +54 -23
  137. package/dist/lib/by.js.map +1 -1
  138. package/dist/lib/clearFastPath.d.ts.map +1 -1
  139. package/dist/lib/clearFastPath.js +2 -0
  140. package/dist/lib/clearFastPath.js.map +1 -1
  141. package/dist/lib/clickFastPath.d.ts.map +1 -1
  142. package/dist/lib/clickFastPath.js +2 -0
  143. package/dist/lib/clickFastPath.js.map +1 -1
  144. package/dist/lib/driver.d.ts +26 -0
  145. package/dist/lib/driver.d.ts.map +1 -1
  146. package/dist/lib/driver.js +123 -4
  147. package/dist/lib/driver.js.map +1 -1
  148. package/dist/lib/elementHandle.d.ts +13 -10
  149. package/dist/lib/elementHandle.d.ts.map +1 -1
  150. package/dist/lib/elementHandle.js +119 -50
  151. package/dist/lib/elementHandle.js.map +1 -1
  152. package/dist/lib/errors.d.ts +12 -0
  153. package/dist/lib/errors.d.ts.map +1 -1
  154. package/dist/lib/errors.js +12 -0
  155. package/dist/lib/errors.js.map +1 -1
  156. package/dist/lib/expect.d.ts +10 -0
  157. package/dist/lib/expect.d.ts.map +1 -1
  158. package/dist/lib/expect.js +76 -49
  159. package/dist/lib/expect.js.map +1 -1
  160. package/dist/lib/fillFastPath.d.ts.map +1 -1
  161. package/dist/lib/fillFastPath.js +2 -0
  162. package/dist/lib/fillFastPath.js.map +1 -1
  163. package/dist/lib/frame.d.ts.map +1 -1
  164. package/dist/lib/frame.js +29 -17
  165. package/dist/lib/frame.js.map +1 -1
  166. package/dist/lib/initScript.d.ts +9 -0
  167. package/dist/lib/initScript.d.ts.map +1 -0
  168. package/dist/lib/initScript.js +14 -0
  169. package/dist/lib/initScript.js.map +1 -0
  170. package/dist/lib/locator.d.ts +21 -54
  171. package/dist/lib/locator.d.ts.map +1 -1
  172. package/dist/lib/locator.js +165 -208
  173. package/dist/lib/locator.js.map +1 -1
  174. package/dist/lib/page.d.ts +22 -0
  175. package/dist/lib/page.d.ts.map +1 -1
  176. package/dist/lib/page.js +71 -13
  177. package/dist/lib/page.js.map +1 -1
  178. package/dist/lib/pageSemantics.d.ts +42 -0
  179. package/dist/lib/pageSemantics.d.ts.map +1 -0
  180. package/dist/lib/pageSemantics.js +216 -0
  181. package/dist/lib/pageSemantics.js.map +1 -0
  182. package/dist/lib/query.d.ts +68 -0
  183. package/dist/lib/query.d.ts.map +1 -0
  184. package/dist/lib/query.js +590 -0
  185. package/dist/lib/query.js.map +1 -0
  186. package/dist/lib/secureFile.d.ts +8 -0
  187. package/dist/lib/secureFile.d.ts.map +1 -0
  188. package/dist/lib/secureFile.js +67 -0
  189. package/dist/lib/secureFile.js.map +1 -0
  190. package/dist/lib/sessionStateValidation.d.ts +35 -0
  191. package/dist/lib/sessionStateValidation.d.ts.map +1 -0
  192. package/dist/lib/sessionStateValidation.js +223 -0
  193. package/dist/lib/sessionStateValidation.js.map +1 -0
  194. package/dist/lib/shadowRoot.d.ts +21 -0
  195. package/dist/lib/shadowRoot.d.ts.map +1 -0
  196. package/dist/lib/shadowRoot.js +29 -0
  197. package/dist/lib/shadowRoot.js.map +1 -0
  198. package/dist/lib/shadowRootLocator.d.ts +31 -0
  199. package/dist/lib/shadowRootLocator.d.ts.map +1 -0
  200. package/dist/lib/shadowRootLocator.js +55 -0
  201. package/dist/lib/shadowRootLocator.js.map +1 -0
  202. package/dist/lib/timing.d.ts +2 -2
  203. package/dist/lib/timing.js +2 -2
  204. package/dist/lib/tracing.d.ts +2 -1
  205. package/dist/lib/tracing.d.ts.map +1 -1
  206. package/dist/lib/tracing.js +12 -1
  207. package/dist/lib/tracing.js.map +1 -1
  208. package/docs/accessibility.md +9 -1
  209. package/docs/agents.md +56 -61
  210. package/docs/api-reference.md +5 -1
  211. package/docs/browser-api.md +27 -7
  212. package/docs/browser-context.md +21 -15
  213. package/docs/cli.md +353 -30
  214. package/docs/error-codes.md +7 -4
  215. package/docs/getting-started.md +6 -1
  216. package/docs/index.md +1 -1
  217. package/docs/mcp.md +136 -65
  218. package/docs/public/examples/agent-actions.html +48 -0
  219. package/docs/public/examples/agent-debug.html +86 -0
  220. package/docs/public/examples/agent-late-mutation.html +18 -0
  221. package/docs/public/examples/agent-ref-shift.html +33 -0
  222. package/docs/public/examples/iframe-child.html +12 -1
  223. package/docs/public/examples/shadow-dom.html +137 -0
  224. package/docs/recipes/login-once-reuse-session.md +33 -5
  225. package/docs/selectors.md +61 -0
  226. package/docs/session-management.md +80 -12
  227. package/docs/why-craftdriver.md +1 -1
  228. package/package.json +5 -3
  229. package/skills/craftdriver/SKILL.md +59 -75
  230. package/skills/craftdriver/agents/openai.yaml +4 -0
  231. package/skills/craftdriver/cheatsheet.md +18 -4
  232. package/skills/craftdriver/cli.md +194 -68
  233. package/skills/craftdriver/patterns.md +17 -8
  234. package/skills/craftdriver/workflow.md +149 -0
@@ -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
- fill <selector> <value>
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 (agent guide files for the current project)
33
- init <flavor> [--force] [--dry-run]
34
- flavors: agents | copilot | claude | cursor | gemini | all
35
- writes a short rules file under the path each tool expects:
36
- agents → AGENTS.md
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 -y craftdriver mcp
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": ["-y", "craftdriver", "mcp"] } } }
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
- flags.launch.browserName = argv[++i];
94
- else if (a === '--timeout') {
95
- const n = Number(argv[++i]);
96
- if (Number.isFinite(n)) {
97
- flags.timeout = n;
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 === '--state')
110
- opts.state = argv[++i];
111
- else if (a === '--selector')
112
- opts.selector = argv[++i];
113
- else if (a === '-o' || a === '--output')
114
- opts.path = argv[++i];
115
- else if (a === '--name')
116
- opts.name = argv[++i];
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
- // Unknown long flag: take a value if next token is not another flag.
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 <flavor> [--force] [--dry-run]
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
  }