opencode-skills-collection 4.0.73 → 4.0.75

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 (63) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +16 -1
  2. package/bundled-skills/claude-api/SKILL.md +4 -4
  3. package/bundled-skills/claude-api/curl/examples.md +5 -5
  4. package/bundled-skills/claude-api/java/claude-api.md +1 -1
  5. package/bundled-skills/claude-api/php/claude-api.md +2 -2
  6. package/bundled-skills/claude-api/python/claude-api/README.md +13 -13
  7. package/bundled-skills/claude-api/python/claude-api/batches.md +3 -3
  8. package/bundled-skills/claude-api/python/claude-api/files-api.md +3 -3
  9. package/bundled-skills/claude-api/python/claude-api/streaming.md +6 -6
  10. package/bundled-skills/claude-api/python/claude-api/tool-use.md +19 -19
  11. package/bundled-skills/claude-api/ruby/claude-api.md +3 -3
  12. package/bundled-skills/claude-api/shared/error-codes.md +2 -2
  13. package/bundled-skills/claude-api/shared/live-sources.md +1 -1
  14. package/bundled-skills/claude-api/shared/models.md +4 -4
  15. package/bundled-skills/claude-api/shared/tool-use-concepts.md +2 -2
  16. package/bundled-skills/claude-api/typescript/claude-api/README.md +12 -12
  17. package/bundled-skills/claude-api/typescript/claude-api/batches.md +2 -2
  18. package/bundled-skills/claude-api/typescript/claude-api/files-api.md +1 -1
  19. package/bundled-skills/claude-api/typescript/claude-api/streaming.md +4 -4
  20. package/bundled-skills/claude-api/typescript/claude-api/tool-use.md +14 -14
  21. package/bundled-skills/electron-drive-skill/LICENSE +21 -0
  22. package/bundled-skills/electron-drive-skill/README.md +172 -0
  23. package/bundled-skills/electron-drive-skill/SKILL.md +237 -0
  24. package/bundled-skills/electron-drive-skill/drive.config.json.example +10 -0
  25. package/bundled-skills/electron-drive-skill/package.json +28 -0
  26. package/bundled-skills/electron-drive-skill/scripts/daemon.mjs +352 -0
  27. package/bundled-skills/electron-drive-skill/scripts/drive.mjs +250 -0
  28. package/bundled-skills/electron-drive-skill/scripts/lib.mjs +165 -0
  29. package/bundled-skills/electron-drive-skill/scripts/lib.test.mjs +56 -0
  30. package/bundled-skills/film-crew/SKILL.md +129 -0
  31. package/bundled-skills/film-crew/references/model-prompting.md +89 -0
  32. package/bundled-skills/film-crew/references/reroll-review.md +46 -0
  33. package/bundled-skills/film-crew/references/roles/director.md +38 -0
  34. package/bundled-skills/film-crew/references/roles/dp.md +28 -0
  35. package/bundled-skills/film-crew/references/roles/editor.md +34 -0
  36. package/bundled-skills/film-crew/references/roles/gaffer.md +30 -0
  37. package/bundled-skills/film-crew/references/roles/production-designer.md +43 -0
  38. package/bundled-skills/film-crew/references/roles/script-supervisor.md +35 -0
  39. package/bundled-skills/film-crew/references/roles/sound.md +23 -0
  40. package/bundled-skills/film-crew/references/templates/shot-list.md +38 -0
  41. package/bundled-skills/film-crew/references/unslop.md +29 -0
  42. package/bundled-skills/mcp-dependency-drift-audit/SKILL.md +169 -0
  43. package/bundled-skills/nexrad-mosaic-access/SKILL.md +297 -0
  44. package/bundled-skills/nexrad-mosaic-construction/SKILL.md +277 -0
  45. package/bundled-skills/nexrad-product-access/SKILL.md +258 -0
  46. package/bundled-skills/nexrad-radar-visualization/SKILL.md +318 -0
  47. package/bundled-skills/radar-satellite-analysis/SKILL.md +314 -0
  48. package/bundled-skills/testdriver-e2e-testing/SKILL.md +121 -0
  49. package/bundled-skills/traderspy-market-briefing/SKILL.md +132 -0
  50. package/bundled-skills/traderspy-market-screener/SKILL.md +182 -0
  51. package/bundled-skills/traderspy-market-screener/references/condition-cookbook.md +70 -0
  52. package/bundled-skills/traderspy-position-check/SKILL.md +130 -0
  53. package/bundled-skills/traderspy-position-check/references/risk-math.md +60 -0
  54. package/bundled-skills/traderspy-smart-money/SKILL.md +151 -0
  55. package/bundled-skills/traderspy-smart-money/references/exchange-notes.md +40 -0
  56. package/bundled-skills/traderspy-technical-analysis/SKILL.md +162 -0
  57. package/bundled-skills/traderspy-technical-analysis/references/derivatives-guide.md +57 -0
  58. package/bundled-skills/traderspy-technical-analysis/references/indicator-guide.md +47 -0
  59. package/bundled-skills/traderspy-trading-signals/SKILL.md +156 -0
  60. package/bundled-skills/traderspy-trading-signals/references/signal-fields.md +64 -0
  61. package/bundled-skills/unified-ai-gateway/SKILL.md +3 -3
  62. package/package.json +1 -1
  63. package/skills_index.json +499 -1
@@ -0,0 +1,237 @@
1
+ ---
2
+ name: electron-drive-skill
3
+ description: "Launch the project's Electron app on a scratch profile and drive it: click, type, screenshot, run renderer or main-process code, read logs. Use to verify UI changes end to end."
4
+ category: testing
5
+ risk: critical
6
+ source: community
7
+ source_repo: Search-3D/electron-drive-skill
8
+ source_type: community
9
+ date_added: "2026-09-23"
10
+ author: Search-3D
11
+ tags: [electron, playwright, desktop, e2e, testing, automation]
12
+ tools: [claude]
13
+ license: "MIT"
14
+ license_source: "https://github.com/Search-3D/electron-drive-skill/blob/main/LICENSE"
15
+ ---
16
+
17
+ # Driving the Electron app
18
+
19
+ ## Overview
20
+
21
+ `scripts/drive.mjs` launches the project's Electron app under Playwright in a
22
+ background daemon and keeps it running between commands, so each action
23
+ (click, type, snapshot, screenshot, eval) is one fast shell call. Every launch
24
+ uses a scratch profile, so the agent can check a UI change or reproduce a bug
25
+ in the real app, not only in unit tests, without touching the user's data.
26
+
27
+ ## When to Use This Skill
28
+
29
+ - Use when you need to verify a UI change in the running Electron app.
30
+ - Use when reproducing a renderer or startup bug, or a failure that only
31
+ shows up in the production build (for example a stricter CSP).
32
+ - Use when checking first-run, onboarding or settings screens.
33
+ - Use when exercising a feature end to end, including IPC through the preload
34
+ bridge or code in the main process.
35
+ - Do not use it for web apps without Electron, or for multi-window flows (see
36
+ Limitations).
37
+
38
+ ## How It Works
39
+
40
+ ### Step 1: Check prerequisites
41
+
42
+ `scripts/drive.mjs`, in this skill's directory, finds the project by the
43
+ nearest `package.json`, so run it from anywhere inside the project. Set `DR`
44
+ to its absolute path.
45
+
46
+ It needs `electron` and `playwright-core` (1.49 or later) installed in the
47
+ project. If `start` says either is missing, tell the user rather than
48
+ installing it yourself. Launch settings come from `drive.config.json` at the
49
+ project root (see Step 4); with no file, it runs `electron .`.
50
+
51
+ ### Step 2: Start, look, act, stop
52
+
53
+ ```bash
54
+ DR=<this skill's directory>/scripts/drive.mjs
55
+
56
+ $DR start # last build, profile 'default'; prints status
57
+ $DR snapshot # accessibility tree: find what to click, by role and name
58
+ $DR click 'role=button[name="Get started"]'
59
+ $DR fill 'role=textbox[name="Email"]' 'test@example.com'
60
+ $DR wait 'text=Welcome'
61
+ $DR screenshot # prints a PNG path; Read it to see the window
62
+ $DR logs --lines 80 # main and renderer console, plus the config's logFile
63
+ $DR stop # ALWAYS, before finishing
64
+ ```
65
+
66
+ - **Start options:**
67
+ - `--build`: run the config's `build` command first. **Needed after any
68
+ source change** if the app runs from a build, because the build is
69
+ otherwise stale; `start` prints its age.
70
+ - `--fresh`: wipe the scratch profile, which gives you the app's first-run
71
+ state.
72
+ - `--profile <name>`: a separate scratch profile.
73
+ - `--dev`: see Step 5.
74
+ - **Targets** are Playwright selectors. Prefer `role=button[name="…"]` from
75
+ `snapshot` output, then `text=…`, then CSS.
76
+ - **`--`** ends the flags. Put it before text that starts with dashes:
77
+ `$DR fill 'role=textbox[name="Args"]' -- --verbose`.
78
+ - Other commands: `select <target> <value|label>`, `press <key>`,
79
+ `status`, `screenshot --selector <css>`. `$DR help` lists everything.
80
+
81
+ ### Step 3: Run code in the app
82
+
83
+ **`eval <js>`** runs in the renderer, **`main <js>`** in the main process
84
+ (`electron` and `process` are in scope, `require` is not). Use a bare
85
+ expression, or a body with `return`. `-` reads the code from stdin, which
86
+ avoids quoting entirely. The result comes back as JSON, so return plain data:
87
+ a DOM node or a function comes back as `undefined` or `{}`.
88
+
89
+ To exercise IPC, call whatever the app's preload exposes through `eval`. That
90
+ goes through the real preload bridge, as the app's own renderer code does.
91
+
92
+ ### Step 4: Configure the launch (optional)
93
+
94
+ `drive.config.json` at the project root; every field is optional:
95
+
96
+ ```json
97
+ {
98
+ "build": "npm run build",
99
+ "args": ["."],
100
+ "env": { "APP_DATA_DIR": "{profile}/data" },
101
+ "logFile": "logs/main.log",
102
+ "dev": {
103
+ "args": ["."],
104
+ "env": { "ELECTRON_RENDERER_URL": "http://localhost:5173" },
105
+ "url": "http://localhost:5173"
106
+ }
107
+ }
108
+ ```
109
+
110
+ - `args`: what Electron is launched with: an app directory whose
111
+ `package.json` `main` is the built entry point, or the entry file itself.
112
+ - `env`: added to the app's environment. `{profile}` becomes the scratch
113
+ profile directory (in `env` values only, not in `args`). This is how data
114
+ kept outside `userData` is redirected: `APP_DATA_DIR` is only an example
115
+ name, and it has an effect only if the app reads it. Check the app's source
116
+ for the variable it actually uses.
117
+ - `logFile`: a log file relative to the profile directory, shown by `logs`.
118
+
119
+ If a project has no config and `start` fails or launches the wrong thing,
120
+ work out these values from the project's `package.json` and build setup,
121
+ then suggest a `drive.config.json` to the user.
122
+
123
+ ### Step 5: Dev mode (optional)
124
+
125
+ For a fast loop on renderer code, with hot reload and source maps:
126
+
127
+ 1. The user (or a background Bash call) starts the renderer dev server
128
+ **without** its own Electron. Many templates' `start`/`dev` scripts launch
129
+ Electron too, and two instances would share state. The config's
130
+ `dev.url` is checked before launch.
131
+ 2. `$DR start --dev` launches Electron with `dev.args` and `dev.env`, and
132
+ `NODE_ENV=development`.
133
+
134
+ ## Examples
135
+
136
+ ### Example 1: Verify a change to the first-run screen
137
+
138
+ ```bash
139
+ $DR start --build --fresh
140
+ $DR snapshot
141
+ $DR click 'role=button[name=/Next/]'
142
+ $DR screenshot
143
+ $DR logs --lines 40
144
+ $DR stop
145
+ ```
146
+
147
+ ### Example 2: Inspect renderer state and call IPC
148
+
149
+ ```bash
150
+ echo "return document.querySelectorAll('button').length" | $DR eval -
151
+ $DR eval "window.api.getSettings()"
152
+ ```
153
+
154
+ ### Example 3: Query the main process
155
+
156
+ ```bash
157
+ $DR main "electron.app.getVersion()"
158
+ $DR main "electron.BrowserWindow.getAllWindows().length"
159
+ ```
160
+
161
+ ## Best Practices
162
+
163
+ - ✅ **Look before acting.** Take a `snapshot` or `screenshot` first. The same
164
+ profile can open differently on a second launch (past the first-run
165
+ screens), and a click that times out usually means the screen is not what
166
+ you assumed.
167
+ - ✅ **Check `logs`** for `[renderer:error]` and `[renderer:pageerror]` after
168
+ each step.
169
+ - ✅ **Prefer the production build.** It catches failures that only show up
170
+ there, such as a stricter Content-Security-Policy, which the dev server
171
+ would miss.
172
+ - ✅ **Always `stop`**, including after a failure. `stop` reports how the app
173
+ went down; anything but "quit via app.quit()" (a hung quit, a SIGKILL) is
174
+ worth a line in your report.
175
+ - ❌ **Don't use the npm/yarn script wrappers** (e.g. `yarn drive`). Run the
176
+ script directly. yarn 1 re-splits arguments and drops inner quotes, which
177
+ silently breaks most JavaScript passed to `eval` and `main`.
178
+ - ❌ **Don't install `electron` or `playwright-core`** yourself; ask the user.
179
+
180
+ ## Limitations
181
+
182
+ - **One window.** Commands act on the first window that is not DevTools, and
183
+ there is no way to pick another. `status` shows which window that is; a
184
+ splash screen is current until it closes.
185
+ - **One app at a time** per project. `start` refuses while one is running.
186
+ - **Windows:** run it as `node scripts/drive.mjs`. The process-tree kill that
187
+ `stop` falls back to is POSIX only.
188
+ - Requires `electron` and `playwright-core` 1.49+ in the project, and Node.js
189
+ 18+.
190
+ - It does not replace the project's own test suite or a human check of
191
+ visual design. Stop and ask if the launch settings or data locations are
192
+ unclear.
193
+
194
+ ## Security & Safety Notes
195
+
196
+ - **Local-only.** The daemon listens on `127.0.0.1` with a random per-session
197
+ token. Nothing is sent to third-party services.
198
+ - **Never point it at the user's real data.** Every launch gets
199
+ `--user-data-dir` set to a scratch profile under
200
+ `$TMPDIR/electron-drive/<project>/profiles/<name>`, which moves
201
+ `app.getPath('userData')`. Data the app keeps elsewhere (a documents
202
+ folder, a path from an env var) is only redirected if `drive.config.json`
203
+ sets it through `env`. Check that before doing anything that writes data.
204
+ If the app would touch real user files, stop and tell the user.
205
+ - **`eval` and `main` run arbitrary code** with the app's privileges; the
206
+ main process has full Node.js access. Only run code needed for the task,
207
+ and never code that deletes files or makes network calls outside the app's
208
+ normal behavior without the user's confirmation.
209
+ - **`start --build` runs the `build` command from `drive.config.json` in a
210
+ shell.** Read the config before the first `--build` in a project you did
211
+ not set up.
212
+ - **`--fresh` deletes the scratch profile directory** only, never the app's
213
+ real `userData`.
214
+
215
+ ## Common Pitfalls
216
+
217
+ - **Problem:** `start` fails.
218
+ **Solution:** Read the daemon log it prints. Common causes are a missing
219
+ build (`--build`), `electron` or `playwright-core` not installed, or wrong
220
+ `args`.
221
+ - **Problem:** "The app is not running".
222
+ **Solution:** It crashed or quit. Run `logs`, then `stop`, then `start`.
223
+ - **Problem:** The app shows old behavior after a source change.
224
+ **Solution:** The build is stale. Restart with `start --build`.
225
+ - **Problem:** A click can't find a button whose name includes icon-font text
226
+ (such as `arrow_forward`).
227
+ **Solution:** Use a regex name: `role=button[name=/Next/]`.
228
+ - **Problem:** A stuck state after an interrupted session.
229
+ **Solution:** `$DR stop` cleans up the daemon, the app and the state file,
230
+ even when the daemon is already gone.
231
+
232
+ ## Related Skills
233
+
234
+ - `@electron-development` - For building and packaging the Electron app
235
+ itself; use this skill to verify the result in the running app.
236
+ - `@systematic-debugging` - Pair with this skill to reproduce and narrow down
237
+ a bug in the real app.
@@ -0,0 +1,10 @@
1
+ {
2
+ "build": "pnpm build",
3
+ "args": ["release/app"],
4
+ "env": { "APP_DATA_DIR": "{profile}/data" },
5
+ "logFile": "data/app.log",
6
+ "dev": {
7
+ "args": ["."],
8
+ "url": "http://localhost:1212"
9
+ }
10
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "electron-drive-skill",
3
+ "version": "1.0.0",
4
+ "description": "Enable your agents to interact with your Electron application automatically. Start/Stop, screenshot, clicking, filling in text, etc.",
5
+ "keywords": [
6
+ "agent",
7
+ "skill",
8
+ "electron",
9
+ "control"
10
+ ],
11
+ "homepage": "https://github.com/Search-3D/electron-drive-skill#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Search-3D/electron-drive-skill/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Search-3D/electron-drive-skill.git"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Search3D <github.com/Search-3D>",
21
+ "type": "module",
22
+ "scripts": {
23
+ "test": "node scripts/lib.test.mjs"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ }
28
+ }
@@ -0,0 +1,352 @@
1
+ /**
2
+ * The long-lived half of the driver: launches the app through Playwright,
3
+ * keeps it up between commands, and answers them over loopback HTTP.
4
+ *
5
+ * Started detached by `drive start`; never run by hand.
6
+ */
7
+ import { execFileSync } from 'node:child_process'
8
+ import { randomBytes } from 'node:crypto'
9
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
10
+ import http from 'node:http'
11
+ import { createRequire } from 'node:module'
12
+ import path from 'node:path'
13
+
14
+ import {
15
+ asFunctionBody,
16
+ childEnv,
17
+ daemonLogPath,
18
+ loadConfig,
19
+ PROJECT_ROOT,
20
+ screenshotDir,
21
+ statePath
22
+ } from './lib.mjs'
23
+
24
+ const [, , mode, profile, dataDir] = process.argv
25
+ const config = loadConfig()
26
+
27
+ const log = (message) => {
28
+ // stdout is daemon.log (drive.mjs redirects it).
29
+ console.log(`${new Date().toISOString()} ${message}`)
30
+ }
31
+
32
+ /** Recent console output from both processes, for `logs`. */
33
+ const output = []
34
+ const remember = (line) => {
35
+ output.push(line)
36
+ if (output.length > 500) output.shift()
37
+ }
38
+
39
+ let app
40
+ let electronPid
41
+
42
+ /**
43
+ * The app's window, not DevTools. A dev build may open DevTools as a window of
44
+ * its own, and it can be the first one to appear.
45
+ */
46
+ const getPage = async (timeoutMs = 30_000) => {
47
+ const deadline = Date.now() + timeoutMs
48
+ while (Date.now() < deadline) {
49
+ const page = app.windows().find((candidate) => !candidate.url().startsWith('devtools://'))
50
+ if (page) return page
51
+ await new Promise((resolve) => setTimeout(resolve, 250))
52
+ }
53
+ throw new Error(`No app window appeared within ${timeoutMs}ms.`)
54
+ }
55
+
56
+ const watchPage = (page) => {
57
+ page.on('console', (message) => remember(`[renderer:${message.type()}] ${message.text()}`))
58
+ page.on('pageerror', (error) => remember(`[renderer:pageerror] ${error.message}`))
59
+ }
60
+
61
+ const launch = async () => {
62
+ // Both come from the project, not from this skill, so the skill can live
63
+ // anywhere. Outside Electron, `require('electron')` is the binary's path.
64
+ const projectRequire = createRequire(path.join(PROJECT_ROOT, 'package.json'))
65
+ const load = (name) => {
66
+ try {
67
+ return projectRequire(name)
68
+ } catch {
69
+ return null
70
+ }
71
+ }
72
+ const playwright = load('playwright-core') ?? load('playwright')
73
+ if (!playwright) throw new Error(`playwright-core is not installed in ${PROJECT_ROOT}. Run: npm i -D playwright-core`)
74
+ const electronPath = load('electron')
75
+ if (!electronPath) throw new Error(`electron is not installed in ${PROJECT_ROOT}.`)
76
+
77
+ const launchConfig = mode === 'dev' ? config.dev : config
78
+ // --user-data-dir moves app.getPath('userData'), and with it localStorage,
79
+ // IndexedDB, cookies and most apps' settings, into the scratch profile.
80
+ const args = [...launchConfig.args, `--user-data-dir=${dataDir}`]
81
+ log(`Launching ${mode}: electron ${args.join(' ')} (profile ${profile})`)
82
+ app = await playwright._electron.launch({
83
+ executablePath: electronPath,
84
+ args,
85
+ cwd: PROJECT_ROOT,
86
+ env: childEnv(process.env, dataDir, mode, { ...config.env, ...(mode === 'dev' ? config.dev.env : {}) }),
87
+ timeout: 90_000
88
+ })
89
+
90
+ const child = app.process()
91
+ electronPid = child.pid
92
+ const lines = (tag) => (chunk) =>
93
+ String(chunk).split('\n').filter(Boolean).forEach((line) => remember(`[${tag}] ${line}`))
94
+ child.stdout?.on('data', lines('main'))
95
+ child.stderr?.on('data', lines('main:err'))
96
+ app.on('window', watchPage)
97
+ app.on('close', () => {
98
+ log('The app exited.')
99
+ remember('[driver] the app exited')
100
+ })
101
+
102
+ const page = await getPage(90_000)
103
+ watchPage(page)
104
+ await page.waitForLoadState('domcontentloaded')
105
+ log(`Window up: ${await page.title()} (${page.url()})`)
106
+ }
107
+
108
+ const isRunning = (pid) => {
109
+ try {
110
+ process.kill(pid, 0)
111
+ return true
112
+ } catch {
113
+ return false
114
+ }
115
+ }
116
+
117
+ const appExited = () => electronPid !== undefined && !isRunning(electronPid)
118
+
119
+ const waitForExit = async (pid, ms) => {
120
+ const deadline = Date.now() + ms
121
+ while (Date.now() < deadline) {
122
+ if (!isRunning(pid)) return true
123
+ await new Promise((resolve) => setTimeout(resolve, 100))
124
+ }
125
+ return !isRunning(pid)
126
+ }
127
+
128
+ /** Every descendant of `pid`, deepest first, via `pgrep -P`. POSIX only. */
129
+ const descendants = (pid) => {
130
+ if (process.platform === 'win32') return []
131
+ let children = []
132
+ try {
133
+ children = execFileSync('pgrep', ['-P', String(pid)], { encoding: 'utf8' })
134
+ .split('\n')
135
+ .filter(Boolean)
136
+ .map(Number)
137
+ } catch {
138
+ return []
139
+ }
140
+ return children.flatMap((child) => [...descendants(child), child])
141
+ }
142
+
143
+ const within = (ms, promise) =>
144
+ Promise.race([promise, new Promise((resolve) => setTimeout(resolve, ms))])
145
+
146
+ /**
147
+ * End the app, and say how. `app.quit()` first, as a user quitting would; then
148
+ * Playwright's close; then SIGKILL on the whole tree, for apps whose quit
149
+ * handlers hang or that ignore SIGTERM.
150
+ */
151
+ const stop = async () => {
152
+ if (!electronPid || !isRunning(electronPid)) return 'already exited'
153
+ const pid = electronPid
154
+ const tree = descendants(pid)
155
+
156
+ // The quit can tear down the connection before the evaluate returns.
157
+ await within(1000, app.evaluate(({ app: electronApp }) => electronApp.quit())).catch(() => {})
158
+ if (await waitForExit(pid, 5000)) return 'quit via app.quit()'
159
+
160
+ await within(3000, app.close()).catch(() => {})
161
+ if (await waitForExit(pid, 2000)) return 'closed by Playwright after app.quit() did not exit within 5s'
162
+
163
+ for (const victim of [...tree, pid]) {
164
+ try {
165
+ process.kill(victim, 'SIGKILL')
166
+ } catch {
167
+ // Already gone.
168
+ }
169
+ }
170
+ await waitForExit(pid, 2000)
171
+ return 'SIGKILLed (process tree) after app.quit() and Playwright close both failed'
172
+ }
173
+
174
+ const handlers = {
175
+ async status() {
176
+ const page = appExited() ? null : await getPage(2000).catch(() => null)
177
+ return {
178
+ mode,
179
+ profile,
180
+ dataDir,
181
+ electronPid,
182
+ running: !appExited(),
183
+ window: page ? { title: await page.title(), url: page.url() } : null
184
+ }
185
+ },
186
+
187
+ async snapshot() {
188
+ const page = await getPage()
189
+ return page.locator('body').ariaSnapshot()
190
+ },
191
+
192
+ async screenshot({ out, selector }) {
193
+ const page = await getPage()
194
+ mkdirSync(screenshotDir(), { recursive: true })
195
+ const file = out ?? path.join(screenshotDir(), `${new Date().toISOString().replace(/[:.]/g, '-')}.png`)
196
+ if (selector) await page.locator(selector).first().screenshot({ path: file })
197
+ else await page.screenshot({ path: file })
198
+ return file
199
+ },
200
+
201
+ async click({ target }) {
202
+ const page = await getPage()
203
+ await page.locator(target).first().click({ timeout: 10_000 })
204
+ return `clicked ${target}`
205
+ },
206
+
207
+ async fill({ target, text }) {
208
+ const page = await getPage()
209
+ await page.locator(target).first().fill(text, { timeout: 10_000 })
210
+ return `filled ${target}`
211
+ },
212
+
213
+ async select({ target, value }) {
214
+ const page = await getPage()
215
+ // A string matches an option's value or its visible label.
216
+ const chosen = await page.locator(target).first().selectOption(value, { timeout: 10_000 })
217
+ return `selected ${chosen.join(', ')} in ${target}`
218
+ },
219
+
220
+ async press({ key }) {
221
+ const page = await getPage()
222
+ await page.keyboard.press(key)
223
+ return `pressed ${key}`
224
+ },
225
+
226
+ async wait({ target, timeout }) {
227
+ const page = await getPage()
228
+ await page.locator(target).first().waitFor({ timeout: timeout ?? 10_000 })
229
+ return `found ${target}`
230
+ },
231
+
232
+ async eval({ code }) {
233
+ const page = await getPage()
234
+ // CDP evaluation, so the page's CSP (no 'unsafe-eval') does not apply to it.
235
+ return page.evaluate(`(async () => { ${asFunctionBody(code)} })()`)
236
+ },
237
+
238
+ async main({ code }) {
239
+ // Playwright takes a string as an *expression*, so a function written as a
240
+ // string comes back as the function, not its result. The body is passed as
241
+ // an argument instead and compiled in main, where there is no CSP. In scope:
242
+ // `electron` (the main process's module) and Node's globals (`process`,
243
+ // `Buffer`); `require` is not.
244
+ return app.evaluate(async (electron, body) => {
245
+ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor
246
+ return new AsyncFunction('electron', body)(electron)
247
+ }, asFunctionBody(code))
248
+ },
249
+
250
+ async logs({ lines }) {
251
+ const count = lines ?? 60
252
+ let fileTail = ''
253
+ if (config.logFile) {
254
+ const file = path.resolve(dataDir, config.logFile)
255
+ const tail = existsSync(file)
256
+ ? readFileSync(file, 'utf8').split('\n').slice(-count).join('\n')
257
+ : '(not written yet)'
258
+ fileTail = `--- ${file} (last ${count}) ---\n${tail}\n`
259
+ }
260
+ return `${fileTail}--- console (last ${count}) ---\n${output.slice(-count).join('\n')}`
261
+ }
262
+ }
263
+
264
+ const main = async () => {
265
+ const token = randomBytes(24).toString('hex')
266
+
267
+ try {
268
+ await launch()
269
+ } catch (e) {
270
+ log(`Launch failed: ${e?.stack ?? e}`)
271
+ if (electronPid) await stop().catch(() => {})
272
+ process.exit(1)
273
+ }
274
+
275
+ const server = http.createServer((request, response) => {
276
+ let body = ''
277
+ request.on('data', (chunk) => {
278
+ body += chunk
279
+ })
280
+ request.on('end', async () => {
281
+ const reply = (status, payload) => {
282
+ response.writeHead(status, { 'content-type': 'application/json' })
283
+ response.end(JSON.stringify(payload))
284
+ }
285
+ let parsed
286
+ try {
287
+ parsed = JSON.parse(body)
288
+ } catch {
289
+ reply(400, { ok: false, error: 'Bad request.' })
290
+ return
291
+ }
292
+ if (parsed.token !== token) {
293
+ reply(403, { ok: false, error: 'Bad token.' })
294
+ return
295
+ }
296
+
297
+ if (parsed.command === 'stop') {
298
+ const how = await stop()
299
+ log(`Stopped: ${how}`)
300
+ reply(200, { ok: true, result: how })
301
+ rmSync(statePath(), { force: true })
302
+ server.close()
303
+ setTimeout(() => process.exit(0), 100)
304
+ return
305
+ }
306
+
307
+ const handler = handlers[parsed.command ?? '']
308
+ if (!handler) {
309
+ reply(404, { ok: false, error: `Unknown command '${parsed.command}'.` })
310
+ return
311
+ }
312
+ if (parsed.command !== 'status' && parsed.command !== 'logs' && appExited()) {
313
+ reply(409, {
314
+ ok: false,
315
+ error: "The app is not running (it exited or crashed). Run 'drive logs', then 'drive stop' and start again."
316
+ })
317
+ return
318
+ }
319
+ try {
320
+ reply(200, { ok: true, result: await handler(parsed.args ?? {}) })
321
+ } catch (e) {
322
+ reply(200, { ok: false, error: e?.message ?? String(e) })
323
+ }
324
+ })
325
+ })
326
+
327
+ server.listen(0, '127.0.0.1', () => {
328
+ const state = {
329
+ pid: process.pid,
330
+ port: server.address().port,
331
+ token,
332
+ mode,
333
+ profile,
334
+ dataDir,
335
+ electronPid,
336
+ startedAt: new Date().toISOString()
337
+ }
338
+ writeFileSync(statePath(), JSON.stringify(state, null, 2), { mode: 0o600 })
339
+ log(`Listening on 127.0.0.1:${state.port}`)
340
+ })
341
+
342
+ const shutdown = async () => {
343
+ await stop().catch(() => {})
344
+ rmSync(statePath(), { force: true })
345
+ process.exit(0)
346
+ }
347
+ process.on('SIGTERM', shutdown)
348
+ process.on('SIGINT', shutdown)
349
+ }
350
+
351
+ log(`Daemon ${process.pid} starting; log at ${daemonLogPath()}`)
352
+ main()