@houwert/conductor 0.19.1 → 0.21.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/README.md CHANGED
@@ -38,7 +38,18 @@ One agent writes the feature. Another taps through the app. They talk. It works.
38
38
  npm install -g @houwert/conductor
39
39
  ```
40
40
 
41
- That's it. Conductor is a pure CLI — no Claude Code plugin or skill is registered. Wire it into your agent however you like (a custom `CLAUDE.md`, a project skill, a slash command — it's up to you). Run `conductor --help` for the full command reference, or `conductor <command> --help` for per-command flags.
41
+ That's it. Conductor is a pure CLI. To teach an AI agent how to use it, set up the bundled skills in your repo:
42
+
43
+ ```bash
44
+ conductor init # interactive: pick scope + skills, writes them into .claude/skills/
45
+ conductor init --yes # non-interactive: install all skills into ./.claude/skills/
46
+ conductor init --global # install into ~/.claude/skills/ for all repos
47
+ conductor init --force # re-sync skills you've already installed
48
+ ```
49
+
50
+ `init` is the one manual setup step — run it once per repo. In a terminal it walks you through which skills and where; piped or headless (CI/agent) it installs everything non-interactively. It drops a set of capability-scoped Claude Code skills — `conductor-device-interact`, `conductor-inspect`, `conductor-create-flow`, `conductor-metro-debugger`, `conductor-profiler`, and `conductor-device-setup` — that document every command and the act → observe → act workflow.
51
+
52
+ When you upgrade conductor, re-run `conductor init --force` to re-sync the installed skills (it stamps the installed version, so `init` tells you when they're out of date, and prunes any skills no longer shipped). Or wire it in however you like (a custom `CLAUDE.md`, a slash command — it's up to you). Run `conductor --help` for the full command reference, or `conductor <command> --help` for per-command flags.
42
53
 
43
54
  ### 📱 What the CLI can do
44
55
 
@@ -52,6 +63,7 @@ That's it. Conductor is a pure CLI — no Claude Code plugin or skill is registe
52
63
  | Flows | `run-flow`, `run-flow-inline`, `run-parallel` |
53
64
  | Devices | `start-device`, `list-devices`, `set-location`, `set-orientation` |
54
65
  | Web setup | `install-web [browser]` (installs a Playwright browser; `--check` prints status) |
66
+ | Discovery | `list-options [command]` / `<command> --options` (valid values for enumerated params) |
55
67
 
56
68
  ## 🔨 Building locally
57
69
 
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.HELP = void 0;
7
7
  exports.captureUI = captureUI;
8
- exports.HELP = ` capture-ui [--output <path>] Capture screenshot + hierarchy + a11y snapshot (for Argus UI panel)`;
8
+ exports.HELP = ` capture-ui [--output <path.json>] Capture screenshot + hierarchy + a11y snapshot as a JSON bundle (for Argus UI panel)`;
9
9
  const promises_1 = __importDefault(require("fs/promises"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const runner_js_1 = require("../runner.js");
@@ -17,6 +17,19 @@ const a11y_js_1 = require("../drivers/a11y.js");
17
17
  const snapshot_store_js_1 = require("../snapshot-store.js");
18
18
  async function captureUI(outputPath, opts = {}, sessionName = 'default') {
19
19
  try {
20
+ // capture-ui always emits a JSON bundle (screenshot is embedded as base64).
21
+ // Reject non-JSON output paths up front so a `.png`/`.jpg` mistake doesn't
22
+ // silently produce an image-named file full of JSON. Use take-screenshot
23
+ // for an actual image.
24
+ if (outputPath) {
25
+ const ext = path_1.default.extname(outputPath).toLowerCase();
26
+ if (ext && ext !== '.json') {
27
+ (0, output_js_1.printError)(`capture-ui — \`--output\` must be a .json path (got "${ext}"). ` +
28
+ `capture-ui writes a JSON bundle (screenshot + hierarchy + a11y snapshot), not an image. ` +
29
+ `Use \`take-screenshot\` to save an image file.`, opts);
30
+ return 1;
31
+ }
32
+ }
20
33
  const driver = await (0, runner_js_1.getDriver)(sessionName);
21
34
  const capturedAt = new Date().toISOString();
22
35
  let platform;
@@ -11,7 +11,7 @@ exports.debugInspectElement = debugInspectElement;
11
11
  exports.debugLogRegistry = debugLogRegistry;
12
12
  exports.debugReload = debugReload;
13
13
  exports.HELP = ` debug status [--port N] Show RN debugger connection info
14
- debug evaluate <expr> [--port N] Run JS in the app runtime (Hermes/Fusebox)
14
+ debug evaluate <expr> [--port N] Run JS in the app runtime (RN: Hermes/Fusebox; web: page context)
15
15
  debug component-tree [--port N] Print the React component tree (on-screen)
16
16
  debug inspect-element <x,y> Print the React component at a screen point
17
17
  debug log-registry [--source metro] Summarize recent Metro/Hermes console logs`;
@@ -22,6 +22,18 @@ const bootstrap_js_1 = require("../drivers/bootstrap.js");
22
22
  const metro_js_1 = require("../drivers/log-sources/metro.js");
23
23
  const metro_scripts_js_1 = require("../drivers/metro-scripts.js");
24
24
  const logs_js_1 = require("./logs.js");
25
+ const runner_js_1 = require("../runner.js");
26
+ const web_js_1 = require("../drivers/web.js");
27
+ /** Resolve the web driver when this session targets a web device, else null (→ Metro path). */
28
+ async function webDriverFor(sessionName) {
29
+ if (!sessionName || sessionName === 'default')
30
+ return null;
31
+ const platform = await (0, bootstrap_js_1.detectPlatform)(sessionName).catch(() => undefined);
32
+ if (platform !== 'web')
33
+ return null;
34
+ const driver = await (0, runner_js_1.getDriver)(sessionName);
35
+ return driver instanceof web_js_1.WebDriver ? driver : null;
36
+ }
25
37
  function newRequestId() {
26
38
  return crypto_1.default.randomBytes(6).toString('hex');
27
39
  }
@@ -88,6 +100,26 @@ async function debugEvaluate(expr, opts, sessionName, debugOpts) {
88
100
  (0, output_js_1.printError)('debug evaluate requires a JS expression', opts);
89
101
  return 1;
90
102
  }
103
+ // Web: evaluate in the page runtime via Playwright (no Metro/Hermes).
104
+ const web = await webDriverFor(sessionName);
105
+ if (web) {
106
+ try {
107
+ const { result, error } = await web.evaluate(expr);
108
+ if (error) {
109
+ (0, output_js_1.printError)(`debug evaluate — ${error}`, opts);
110
+ return 1;
111
+ }
112
+ if (opts.json)
113
+ (0, output_js_1.printData)({ result }, opts);
114
+ else
115
+ console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
116
+ return 0;
117
+ }
118
+ catch (err) {
119
+ (0, output_js_1.printError)(`debug evaluate — ${err instanceof Error ? err.message : String(err)}`, opts);
120
+ return 1;
121
+ }
122
+ }
91
123
  const port = debugOpts.port ?? 8081;
92
124
  const { deviceId, platformPromise } = resolveSession(sessionName);
93
125
  try {
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HELP = void 0;
7
+ exports.init = init;
8
+ exports.HELP = ` init [target-dir] Set up conductor in a repo: install the agent skills into .claude/skills/
9
+ Interactive when run in a terminal; non-interactive otherwise.
10
+ --global Install into ~/.claude/skills/ instead of the current repo
11
+ --force Re-sync skills that are already installed (overwrite)
12
+ --yes, -y Skip prompts and accept defaults (install all skills)`;
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const os_1 = __importDefault(require("os"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const promises_1 = __importDefault(require("readline/promises"));
17
+ const output_js_1 = require("../output.js");
18
+ const pkg_root_js_1 = require("../pkg-root.js");
19
+ /** Skills conductor installs are namespaced with this prefix; prune is bounded to it. */
20
+ const SKILL_PREFIX = 'conductor-';
21
+ /** Records what conductor installed into a skills dir, so we can detect staleness and prune. */
22
+ const MANIFEST_FILE = '.conductor-skills.json';
23
+ /**
24
+ * The skill templates ship inside the published package under `skills/`
25
+ * (declared in package.json `files`), one directory per `conductor-<capability>`
26
+ * skill, each containing a SKILL.md. `findPkgRoot` resolves the package root for
27
+ * both the production build (dist/) and the test build (which has an extra `src/`
28
+ * path level), mirroring how the bundled drivers are located.
29
+ */
30
+ function bundledSkillsRoot() {
31
+ return path_1.default.join((0, pkg_root_js_1.findPkgRoot)(__dirname), 'skills');
32
+ }
33
+ function packageVersion() {
34
+ try {
35
+ const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join((0, pkg_root_js_1.findPkgRoot)(__dirname), 'package.json'), 'utf-8'));
36
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown';
37
+ }
38
+ catch {
39
+ return 'unknown';
40
+ }
41
+ }
42
+ /** Enumerate bundled skill directories (those containing a SKILL.md). */
43
+ function listBundledSkills(skillsRoot) {
44
+ if (!fs_1.default.existsSync(skillsRoot))
45
+ return [];
46
+ return fs_1.default
47
+ .readdirSync(skillsRoot)
48
+ .filter((name) => fs_1.default.existsSync(path_1.default.join(skillsRoot, name, 'SKILL.md')))
49
+ .sort();
50
+ }
51
+ function readManifest(destRoot) {
52
+ try {
53
+ const raw = JSON.parse(fs_1.default.readFileSync(path_1.default.join(destRoot, MANIFEST_FILE), 'utf-8'));
54
+ if (raw && Array.isArray(raw.skills)) {
55
+ return { version: String(raw.version ?? 'unknown'), skills: raw.skills.map(String) };
56
+ }
57
+ }
58
+ catch {
59
+ /* missing or corrupt → treat as no prior install */
60
+ }
61
+ return null;
62
+ }
63
+ function writeManifest(destRoot, manifest) {
64
+ fs_1.default.mkdirSync(destRoot, { recursive: true });
65
+ fs_1.default.writeFileSync(path_1.default.join(destRoot, MANIFEST_FILE), JSON.stringify(manifest, null, 2) + '\n');
66
+ }
67
+ function copySkill(srcDir, destDir) {
68
+ fs_1.default.mkdirSync(destDir, { recursive: true });
69
+ for (const entry of fs_1.default.readdirSync(srcDir)) {
70
+ const from = path_1.default.join(srcDir, entry);
71
+ if (!fs_1.default.statSync(from).isFile())
72
+ continue;
73
+ fs_1.default.copyFileSync(from, path_1.default.join(destDir, entry));
74
+ }
75
+ }
76
+ /**
77
+ * Remove skills conductor previously installed that are no longer bundled (renamed
78
+ * or dropped). Bounded to skills recorded in our manifest and the `conductor-`
79
+ * prefix, so it never touches user-authored or third-party skills.
80
+ */
81
+ function pruneOrphans(destRoot, prev, bundled) {
82
+ if (!prev)
83
+ return [];
84
+ const pruned = [];
85
+ for (const name of prev.skills) {
86
+ if (!name.startsWith(SKILL_PREFIX) || bundled.has(name))
87
+ continue;
88
+ const dir = path_1.default.join(destRoot, name);
89
+ if (fs_1.default.existsSync(dir)) {
90
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
91
+ pruned.push(name);
92
+ }
93
+ }
94
+ return pruned;
95
+ }
96
+ function resolveDestRoot(global, targetDir) {
97
+ return global
98
+ ? path_1.default.join(os_1.default.homedir(), '.claude', 'skills')
99
+ : path_1.default.join(path_1.default.resolve(targetDir ?? process.cwd()), '.claude', 'skills');
100
+ }
101
+ /**
102
+ * Setting up conductor is the one manual, human-driven step — so when `init` runs
103
+ * in a real terminal we walk the dev through scope and skill selection, the way
104
+ * argent's wizard does. Headless/agent/CI runs (no TTY, --json, or --yes) take the
105
+ * non-interactive path with sensible defaults: all skills, project scope.
106
+ */
107
+ async function promptPlan(skills, version, targetDir, flags) {
108
+ const rl = promises_1.default.createInterface({ input: process.stdin, output: process.stdout });
109
+ try {
110
+ // Scope — skip the prompt if the flags already decided it.
111
+ let global = flags.global;
112
+ if (!flags.global && targetDir === undefined) {
113
+ const ans = (await rl.question('Where should the skills be installed?\n' +
114
+ ' 1) This project (./.claude/skills) [default]\n' +
115
+ ' 2) Globally (~/.claude/skills)\n' +
116
+ '> ')).trim();
117
+ global = ans === '2';
118
+ }
119
+ const destRoot = resolveDestRoot(global, targetDir);
120
+ // Skill selection.
121
+ let selected = skills;
122
+ const sel = (await rl.question(`\nInstall all ${skills.length} skills, or choose a subset?\n` +
123
+ ' 1) All [default]\n' +
124
+ ' 2) Select\n' +
125
+ '> ')).trim();
126
+ if (sel === '2') {
127
+ skills.forEach((name, i) => console.log(` ${i + 1}) ${name}`));
128
+ const picks = (await rl.question('Enter numbers (comma-separated): ')).trim();
129
+ const chosen = picks
130
+ .split(',')
131
+ .map((s) => parseInt(s.trim(), 10) - 1)
132
+ .filter((i) => i >= 0 && i < skills.length)
133
+ .map((i) => skills[i]);
134
+ if (chosen.length > 0)
135
+ selected = [...new Set(chosen)];
136
+ }
137
+ // Offer to re-sync already-installed skills. If they're from an older
138
+ // conductor, say so and default to yes; otherwise default to no.
139
+ let force = flags.force;
140
+ if (!force) {
141
+ const existing = selected.filter((name) => fs_1.default.existsSync(path_1.default.join(destRoot, name)));
142
+ if (existing.length > 0) {
143
+ const prev = readManifest(destRoot);
144
+ const stale = prev !== null && prev.version !== version;
145
+ const prompt = stale
146
+ ? `\n${existing.length} installed skill(s) are from conductor v${prev?.version} (this is v${version}). Re-sync (overwrite) them? [Y/n] `
147
+ : `\n${existing.length} of these are already installed. Re-sync (overwrite) them? [y/N] `;
148
+ const ans = (await rl.question(prompt)).trim().toLowerCase();
149
+ force = stale ? ans !== 'n' && ans !== 'no' : ans === 'y' || ans === 'yes';
150
+ }
151
+ }
152
+ return { destRoot, selected, force };
153
+ }
154
+ finally {
155
+ rl.close();
156
+ }
157
+ }
158
+ async function init(opts, targetDir, flags) {
159
+ try {
160
+ if (targetDir !== undefined && flags.global) {
161
+ (0, output_js_1.printError)('init: pass a target directory or --global, not both.', opts);
162
+ return 1;
163
+ }
164
+ const srcRoot = bundledSkillsRoot();
165
+ const skills = listBundledSkills(srcRoot);
166
+ if (skills.length === 0) {
167
+ (0, output_js_1.printError)(`No bundled skill templates found at ${srcRoot}`, opts);
168
+ return 1;
169
+ }
170
+ const version = packageVersion();
171
+ const bundledSet = new Set(skills);
172
+ const interactive = !opts.json && !flags.yes && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
173
+ const plan = interactive
174
+ ? await promptPlan(skills, version, targetDir, flags)
175
+ : {
176
+ destRoot: resolveDestRoot(flags.global, targetDir),
177
+ selected: skills,
178
+ force: flags.force,
179
+ };
180
+ const prev = readManifest(plan.destRoot);
181
+ const installed = [];
182
+ const skipped = [];
183
+ for (const name of plan.selected) {
184
+ const destDir = path_1.default.join(plan.destRoot, name);
185
+ if (fs_1.default.existsSync(destDir) && !plan.force) {
186
+ skipped.push(name);
187
+ continue;
188
+ }
189
+ copySkill(path_1.default.join(srcRoot, name), destDir);
190
+ installed.push(name);
191
+ }
192
+ // Remove skills we previously installed that are no longer bundled.
193
+ const pruned = pruneOrphans(plan.destRoot, prev, bundledSet);
194
+ // Update the manifest. Only claim the current version when we fully re-synced
195
+ // (force); otherwise existing skills may still be stale, so keep the old stamp.
196
+ const present = skills.filter((name) => fs_1.default.existsSync(path_1.default.join(plan.destRoot, name)));
197
+ if (present.length > 0) {
198
+ const stampVersion = !prev || plan.force ? version : prev.version;
199
+ writeManifest(plan.destRoot, { version: stampVersion, skills: present });
200
+ }
201
+ const stale = !plan.force && prev !== null && prev.version !== version && skipped.length > 0;
202
+ if (opts.json) {
203
+ (0, output_js_1.printData)({ status: 'ok', dir: plan.destRoot, version, installed, skipped, pruned, stale }, opts);
204
+ return 0;
205
+ }
206
+ // argent-style messaging.
207
+ if (installed.length > 0) {
208
+ console.log(`\nInstalling skills…`);
209
+ for (const name of installed)
210
+ console.log(` + ${name}`);
211
+ console.log(`Skills installed → ${plan.destRoot}`);
212
+ }
213
+ if (pruned.length > 0) {
214
+ console.log(`Pruned skills no longer shipped: ${pruned.join(', ')}`);
215
+ }
216
+ if (skipped.length > 0) {
217
+ console.log(`Already installed (skipped): ${skipped.join(', ')}. Re-run with --force to re-sync.`);
218
+ }
219
+ if (stale) {
220
+ console.log(`Note: skipped skills are from conductor v${prev?.version} (this is v${version}). Re-run \`conductor init --force\` to update them.`);
221
+ }
222
+ if (installed.length === 0 && pruned.length === 0 && skipped.length > 0) {
223
+ console.log('Nothing to do — all selected skills already installed.');
224
+ }
225
+ else {
226
+ console.log('Conductor is ready. Restart your agent / Claude Code session to pick up the skills.');
227
+ }
228
+ return 0;
229
+ }
230
+ catch (err) {
231
+ const message = err instanceof Error ? err.message : String(err);
232
+ (0, output_js_1.printError)(`init failed: ${message}`, opts);
233
+ // Manual fallback, in the spirit of argent's "install manually" note.
234
+ if (!opts.json) {
235
+ console.error('To install manually, copy the bundled skills into your skills directory:');
236
+ console.error(` cp -r "${bundledSkillsRoot()}"/* ./.claude/skills/`);
237
+ }
238
+ return 1;
239
+ }
240
+ }
@@ -42,6 +42,21 @@ function formatEntry(entry, opts) {
42
42
  }
43
43
  return line;
44
44
  }
45
+ /**
46
+ * Ensure both the device driver and its daemon are up before reading logs.
47
+ *
48
+ * getDriver() starts the daemon when the driver *port* is closed, but skips it
49
+ * when the port is already open — which happens after the daemon idle-times-out
50
+ * while leaving the driver alive (e.g. tvOS deliberately keeps its runner up
51
+ * across daemon restarts). The log collector lives inside the daemon, so a live
52
+ * driver port is not enough: we must guarantee the daemon socket itself is up,
53
+ * otherwise the log source connects to a dead socket. startDaemon() is
54
+ * idempotent — it returns immediately when the daemon already answers /status.
55
+ */
56
+ async function ensureLogDaemon(sessionName) {
57
+ await (0, runner_js_1.getDriver)(sessionName);
58
+ await (0, client_js_1.startDaemon)(sessionName);
59
+ }
45
60
  async function resolvePlatformAndDevice(sessionName) {
46
61
  try {
47
62
  const driver = await (0, runner_js_1.getDriver)(sessionName);
@@ -130,7 +145,7 @@ async function logs(opts = {}, sessionName = 'default', { source, level, list, r
130
145
  try {
131
146
  // ── Snapshot mode (--recent N) ──────────────────────────────────────────
132
147
  if (recent !== undefined) {
133
- await (0, runner_js_1.getDriver)(sessionName);
148
+ await ensureLogDaemon(sessionName);
134
149
  const minSeverity = level ? (types_js_1.LEVEL_SEVERITY[level] ?? 0) : 0;
135
150
  const entries = await (0, client_js_1.fetchDaemonLogs)(sessionName, { limit: recent, level });
136
151
  for (const entry of entries) {
@@ -145,7 +160,7 @@ async function logs(opts = {}, sessionName = 'default', { source, level, list, r
145
160
  }
146
161
  // ── Streaming modes ─────────────────────────────────────────────────────
147
162
  // Ensure daemon is running; its log collector auto-discovers Metro.
148
- await (0, runner_js_1.getDriver)(sessionName);
163
+ await ensureLogDaemon(sessionName);
149
164
  const minSeverity = level ? (types_js_1.LEVEL_SEVERITY[level] ?? 0) : 0;
150
165
  const logSource = new daemon_js_1.DaemonLogSource(sessionName);
151
166
  await logSource.connect();
@@ -3,12 +3,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HELP = void 0;
4
4
  exports.networkLogs = networkLogs;
5
5
  exports.networkRequest = networkRequest;
6
- exports.HELP = ` network logs [--port N] [--limit N] Read recent HTTP traffic (installs a fetch/XHR shim once)
6
+ exports.HELP = ` network logs [--port N] [--limit N] Read recent HTTP traffic (RN: fetch/XHR shim; web: all traffic via Playwright)
7
7
  network request <url> [--method M] [--body STR] [--header K=V] [--port N]
8
8
  Issue an HTTP request from the app's context`;
9
9
  const output_js_1 = require("../output.js");
10
10
  const metro_cdp_js_1 = require("../drivers/metro-cdp.js");
11
11
  const bootstrap_js_1 = require("../drivers/bootstrap.js");
12
+ const runner_js_1 = require("../runner.js");
13
+ const web_js_1 = require("../drivers/web.js");
14
+ /** Resolve the web driver when this session targets a web device, else null (→ Metro path). */
15
+ async function webDriverFor(sessionName) {
16
+ if (!sessionName || sessionName === 'default')
17
+ return null;
18
+ const platform = await (0, bootstrap_js_1.detectPlatform)(sessionName).catch(() => undefined);
19
+ if (platform !== 'web')
20
+ return null;
21
+ const driver = await (0, runner_js_1.getDriver)(sessionName);
22
+ return driver instanceof web_js_1.WebDriver ? driver : null;
23
+ }
24
+ function formatNetEntry(e) {
25
+ const ts = e.timestamp.slice(11, 23);
26
+ const status = e.error ? `ERR ${e.error}` : e.status !== null ? String(e.status) : '...';
27
+ const dur = e.durationMs !== null ? `${e.durationMs}ms` : '-';
28
+ return `${ts} ${status.padEnd(6)} ${e.method.padEnd(6)} ${e.url} (${dur}, ${e.resourceType ?? e.kind ?? ''})`;
29
+ }
12
30
  const INSTALL_SHIM_SCRIPT = `
13
31
  (() => {
14
32
  if (globalThis.__CONDUCTOR_NET__ && globalThis.__CONDUCTOR_NET__.installed) {
@@ -99,8 +117,30 @@ function resolveSession(sessionName) {
99
117
  };
100
118
  }
101
119
  async function networkLogs(opts, sessionName, netOpts) {
102
- const port = netOpts.port ?? 8081;
103
120
  const limit = netOpts.limit ?? 50;
121
+ // Web: Playwright captures all traffic natively — no Metro, no injected shim.
122
+ const web = await webDriverFor(sessionName);
123
+ if (web) {
124
+ try {
125
+ const { entries } = await web.networkLogs({ limit });
126
+ if (opts.json) {
127
+ (0, output_js_1.printData)({ installed: true, count: entries.length, entries }, opts);
128
+ }
129
+ else if (entries.length === 0) {
130
+ console.log('No network entries captured yet. Reload the app and try again.');
131
+ }
132
+ else {
133
+ for (const e of entries)
134
+ console.log(formatNetEntry(e));
135
+ }
136
+ return 0;
137
+ }
138
+ catch (err) {
139
+ (0, output_js_1.printError)(`network logs — ${err instanceof Error ? err.message : String(err)}`, opts);
140
+ return 1;
141
+ }
142
+ }
143
+ const port = netOpts.port ?? 8081;
104
144
  const { deviceId, platformPromise } = resolveSession(sessionName);
105
145
  try {
106
146
  const platform = await platformPromise;
@@ -144,6 +184,31 @@ async function networkRequest(url, opts, sessionName, reqOpts) {
144
184
  headers[h.slice(0, idx)] = h.slice(idx + 1);
145
185
  }
146
186
  const body = reqOpts.body;
187
+ // Web: issue via the browser context (shares the page's cookies/session).
188
+ const web = await webDriverFor(sessionName);
189
+ if (web) {
190
+ try {
191
+ const result = await web.networkRequest(url, { method, headers, body });
192
+ if (opts.json)
193
+ (0, output_js_1.printData)(result, opts);
194
+ else {
195
+ if (result.error)
196
+ console.error(`error: ${result.error}`);
197
+ console.log(`status: ${result.status ?? 'n/a'}`);
198
+ for (const [k, v] of Object.entries(result.headers ?? {}))
199
+ console.log(`${k}: ${v}`);
200
+ if (result.body !== undefined) {
201
+ console.log('');
202
+ console.log(result.body);
203
+ }
204
+ }
205
+ return result.ok ? 0 : 1;
206
+ }
207
+ catch (err) {
208
+ (0, output_js_1.printError)(`network request — ${err instanceof Error ? err.message : String(err)}`, opts);
209
+ return 1;
210
+ }
211
+ }
147
212
  const init = JSON.stringify({
148
213
  method,
149
214
  headers,
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.listOptions = listOptions;
5
+ exports.HELP = ` list-options [command|param] List valid values for enumerated parameters
6
+ (e.g. \`list-options press-key\`, \`list-options direction\`, or no arg for all)`;
7
+ const output_js_1 = require("../output.js");
8
+ const enum_options_js_1 = require("../enum-options.js");
9
+ function renderParam(p) {
10
+ const lines = [];
11
+ lines.push(`${p.command} ${p.param}`);
12
+ lines.push(` ${p.description}`);
13
+ for (const v of p.values) {
14
+ lines.push(v.description ? ` ${v.value} — ${v.description}` : ` ${v.value}`);
15
+ }
16
+ if (p.note)
17
+ lines.push(` note: ${p.note}`);
18
+ return lines.join('\n');
19
+ }
20
+ /**
21
+ * List valid values for enumerated parameters. With no query, lists every
22
+ * enumerated parameter. With a query, filters by command name, parameter name,
23
+ * or value.
24
+ */
25
+ function listOptions(query, opts = {}) {
26
+ const matches = (0, enum_options_js_1.findEnumParams)(query);
27
+ if (matches.length === 0) {
28
+ const available = (0, enum_options_js_1.commandsWithEnums)().join(', ');
29
+ (0, output_js_1.printError)(`No enumerated parameters match "${query}". Commands with options: ${available}`, opts);
30
+ return 1;
31
+ }
32
+ if (opts.json) {
33
+ (0, output_js_1.printData)(matches.map((p) => ({
34
+ command: p.command,
35
+ param: p.param,
36
+ description: p.description,
37
+ values: p.values.map((v) => v.value),
38
+ note: p.note ?? null,
39
+ })), opts);
40
+ return 0;
41
+ }
42
+ console.log(matches.map(renderParam).join('\n\n'));
43
+ return 0;
44
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HELP = void 0;
3
+ exports.VALID_KEYS = exports.HELP = void 0;
4
4
  exports.pressKey = pressKey;
5
5
  exports.HELP = ` press-key <key> Press a key (Enter, Backspace, Home, ...)`;
6
6
  const runner_js_1 = require("../runner.js");
@@ -8,7 +8,7 @@ const output_js_1 = require("../output.js");
8
8
  const ios_js_1 = require("../drivers/ios.js");
9
9
  const android_js_1 = require("../drivers/android.js");
10
10
  const web_js_1 = require("../drivers/web.js");
11
- const VALID_KEYS = [
11
+ exports.VALID_KEYS = [
12
12
  'Enter',
13
13
  'Backspace',
14
14
  'Home',
@@ -107,12 +107,12 @@ const ANDROID_KEYCODE = {
107
107
  };
108
108
  async function pressKey(key, opts = {}, sessionName = 'default') {
109
109
  if (!key) {
110
- (0, output_js_1.printError)(`press-key requires <key>. Valid keys: ${VALID_KEYS.join(', ')}`, opts);
110
+ (0, output_js_1.printError)(`press-key requires <key>. Valid keys: ${exports.VALID_KEYS.join(', ')}`, opts);
111
111
  return 1;
112
112
  }
113
- const matched = VALID_KEYS.find((k) => k.toLowerCase() === key.toLowerCase());
113
+ const matched = exports.VALID_KEYS.find((k) => k.toLowerCase() === key.toLowerCase());
114
114
  if (!matched) {
115
- (0, output_js_1.printError)(`Unknown key "${key}". Valid keys: ${VALID_KEYS.join(', ')}`, opts);
115
+ (0, output_js_1.printError)(`Unknown key "${key}". Valid keys: ${exports.VALID_KEYS.join(', ')}`, opts);
116
116
  return 1;
117
117
  }
118
118
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
@@ -149,6 +149,13 @@ async function pressKey(key, opts = {}, sessionName = 'default') {
149
149
  Escape: 'Escape',
150
150
  Home: 'Home',
151
151
  End: 'End',
152
+ // Canvas webtv apps (Lightning/WPE) navigate focus via the D-pad, which they listen
153
+ // for as arrow keys (and Enter for select). Maps the TV remote onto web keyboard.
154
+ 'Remote Dpad Up': 'ArrowUp',
155
+ 'Remote Dpad Down': 'ArrowDown',
156
+ 'Remote Dpad Left': 'ArrowLeft',
157
+ 'Remote Dpad Right': 'ArrowRight',
158
+ 'Remote Dpad Center': 'Enter',
152
159
  };
153
160
  const webKey = WEB_KEY_MAP[matched];
154
161
  if (webKey) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HELP = void 0;
3
+ exports.PRESETS = exports.HELP = void 0;
4
4
  exports.setViewport = setViewport;
5
5
  exports.HELP = ` set-viewport [<width> <height>] Resize the web browser viewport (web only)
6
6
  --preset <mobile|tablet|desktop> Use a device preset instead of explicit width/height
@@ -12,7 +12,7 @@ exports.HELP = ` set-viewport [<width> <height>] Resize the web browser vi
12
12
  const runner_js_1 = require("../runner.js");
13
13
  const web_js_1 = require("../drivers/web.js");
14
14
  const output_js_1 = require("../output.js");
15
- const PRESETS = {
15
+ exports.PRESETS = {
16
16
  mobile: { width: 390, height: 844, deviceScaleFactor: 3, isMobile: true },
17
17
  tablet: { width: 820, height: 1180, deviceScaleFactor: 2, isMobile: true },
18
18
  desktop: { width: 1280, height: 800, deviceScaleFactor: 1, isMobile: false },
@@ -23,9 +23,9 @@ async function setViewport(flags, opts = {}, sessionName = 'default') {
23
23
  let isMobile = flags.mobile;
24
24
  let scale = flags.scale;
25
25
  if (flags.preset !== undefined) {
26
- const preset = PRESETS[flags.preset.toLowerCase()];
26
+ const preset = exports.PRESETS[flags.preset.toLowerCase()];
27
27
  if (!preset) {
28
- (0, output_js_1.printError)(`--preset must be one of: ${Object.keys(PRESETS).join(', ')}`, opts);
28
+ (0, output_js_1.printError)(`--preset must be one of: ${Object.keys(exports.PRESETS).join(', ')}`, opts);
29
29
  return 1;
30
30
  }
31
31
  width ?? (width = preset.width);