@houwert/conductor 0.20.0 → 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 +13 -1
- package/dist/commands/init.js +240 -0
- package/dist/commands/options.js +44 -0
- package/dist/commands/press-key.js +5 -5
- package/dist/commands/set-viewport.js +4 -4
- package/dist/enum-options.js +130 -0
- package/dist/index.js +29 -1
- package/dist/utils.js +2 -0
- package/package.json +3 -2
- package/skills/conductor-create-flow/SKILL.md +56 -0
- package/skills/conductor-device-interact/SKILL.md +96 -0
- package/skills/conductor-device-setup/SKILL.md +73 -0
- package/skills/conductor-inspect/SKILL.md +48 -0
- package/skills/conductor-metro-debugger/SKILL.md +51 -0
- package/skills/conductor-profiler/SKILL.md +42 -0
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
|
|
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
|
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
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) => {
|
|
@@ -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
|
-
|
|
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);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Central registry of commands/parameters that only accept a fixed set of
|
|
3
|
+
// enumerated values. The `list-options` command and the global `--options`
|
|
4
|
+
// flag read from here so agents (and humans) can discover valid values without
|
|
5
|
+
// trial-and-error.
|
|
6
|
+
//
|
|
7
|
+
// Where a value list already exists as the canonical source elsewhere, we
|
|
8
|
+
// import it so this registry can never drift from what the command validates.
|
|
9
|
+
// Small, stable 2–4 value lists are inlined with a pointer to their source.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.ENUM_PARAMS = void 0;
|
|
12
|
+
exports.commandsWithEnums = commandsWithEnums;
|
|
13
|
+
exports.findEnumParams = findEnumParams;
|
|
14
|
+
const press_key_js_1 = require("./commands/press-key.js");
|
|
15
|
+
const set_viewport_js_1 = require("./commands/set-viewport.js");
|
|
16
|
+
const utils_js_1 = require("./utils.js");
|
|
17
|
+
const types_js_1 = require("./drivers/log-sources/types.js");
|
|
18
|
+
const DIRECTION_VALUES = utils_js_1.DIRECTIONS.map((d) => ({ value: d }));
|
|
19
|
+
// `--level` accepts every key of LEVEL_SEVERITY (includes the `warn` alias).
|
|
20
|
+
const LOG_LEVELS = Object.keys(types_js_1.LEVEL_SEVERITY).map((value) => ({ value }));
|
|
21
|
+
exports.ENUM_PARAMS = [
|
|
22
|
+
{
|
|
23
|
+
command: 'press-key',
|
|
24
|
+
param: '<key>',
|
|
25
|
+
description: 'Key, hardware button, or remote button to press',
|
|
26
|
+
values: press_key_js_1.VALID_KEYS.map((value) => ({ value })),
|
|
27
|
+
note: 'Matched case-insensitively. Availability varies by platform: "Remote …" / "TV …" keys target tvOS and Android TV; hardware buttons (Home, Lock, Power, Volume…) target iOS/Android.',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
command: 'scroll',
|
|
31
|
+
param: '--direction',
|
|
32
|
+
description: 'Scroll direction (default: down)',
|
|
33
|
+
values: DIRECTION_VALUES,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
command: 'swipe',
|
|
37
|
+
param: '--direction',
|
|
38
|
+
description: 'Swipe direction (required unless --start/--end are given)',
|
|
39
|
+
values: DIRECTION_VALUES,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
command: 'scroll-until-visible',
|
|
43
|
+
param: '--direction',
|
|
44
|
+
description: 'Scroll direction while searching (default: down)',
|
|
45
|
+
values: DIRECTION_VALUES,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
command: 'set-orientation',
|
|
49
|
+
param: '<orientation>',
|
|
50
|
+
description: 'Device orientation',
|
|
51
|
+
// Source: VALID in commands/set-orientation.ts
|
|
52
|
+
values: [{ value: 'portrait' }, { value: 'landscape' }],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
command: 'start-device',
|
|
56
|
+
param: '--platform',
|
|
57
|
+
description: 'Platform of the device to start',
|
|
58
|
+
// Source: switch in commands/start-device.ts
|
|
59
|
+
values: [{ value: 'ios' }, { value: 'android' }, { value: 'tvos' }, { value: 'web' }],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
command: 'install-web',
|
|
63
|
+
param: '[browser]',
|
|
64
|
+
description: 'Playwright browser to install (default: chromium)',
|
|
65
|
+
// Source: validBrowsers in commands/install.ts
|
|
66
|
+
values: [{ value: 'chromium' }, { value: 'firefox' }, { value: 'webkit' }],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
command: 'set-viewport',
|
|
70
|
+
param: '--preset',
|
|
71
|
+
description: 'Device size preset instead of explicit width/height (web only)',
|
|
72
|
+
values: Object.entries(set_viewport_js_1.PRESETS).map(([value, p]) => ({
|
|
73
|
+
value,
|
|
74
|
+
description: `${p.width}x${p.height} @${p.deviceScaleFactor}x${p.isMobile ? ', mobile' : ''}`,
|
|
75
|
+
})),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
command: 'set-viewport',
|
|
79
|
+
param: '--color-scheme',
|
|
80
|
+
description: 'Emulate prefers-color-scheme (web only)',
|
|
81
|
+
values: [{ value: 'dark' }, { value: 'light' }],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
command: 'logs',
|
|
85
|
+
param: '--source',
|
|
86
|
+
description: 'Filter logs by source (default: both)',
|
|
87
|
+
// Source: sourceFilter in commands/logs.ts
|
|
88
|
+
values: [{ value: 'metro' }, { value: 'device' }],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
command: 'logs',
|
|
92
|
+
param: '--level',
|
|
93
|
+
description: 'Minimum log level to show',
|
|
94
|
+
values: LOG_LEVELS,
|
|
95
|
+
note: '"warn" is an alias for "warning".',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
command: 'list-devices',
|
|
99
|
+
param: '--platform',
|
|
100
|
+
description: 'Filter listed devices by platform (also a global filter on most commands)',
|
|
101
|
+
values: [{ value: 'ios' }, { value: 'android' }, { value: 'tvos' }, { value: 'web' }],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
/** All distinct command names that have at least one enumerated parameter. */
|
|
105
|
+
function commandsWithEnums() {
|
|
106
|
+
return [...new Set(exports.ENUM_PARAMS.map((p) => p.command))];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Find enumerated parameters matching a query. Matches a command name (e.g.
|
|
110
|
+
* "press-key"), a bare parameter name (e.g. "direction", "--level"), or a
|
|
111
|
+
* value (e.g. "tvos"). Returns all params when query is empty.
|
|
112
|
+
*/
|
|
113
|
+
function findEnumParams(query) {
|
|
114
|
+
if (!query)
|
|
115
|
+
return exports.ENUM_PARAMS;
|
|
116
|
+
const norm = (s) => s
|
|
117
|
+
.toLowerCase()
|
|
118
|
+
.replace(/^-+/, '')
|
|
119
|
+
.replace(/[<>[\]]/g, '');
|
|
120
|
+
const q = norm(query);
|
|
121
|
+
// Exact matches (command, param, or value) take precedence so that an exact
|
|
122
|
+
// command name like "scroll" doesn't also drag in "scroll-until-visible".
|
|
123
|
+
const exact = exports.ENUM_PARAMS.filter((p) => p.command.toLowerCase() === q ||
|
|
124
|
+
norm(p.param) === q ||
|
|
125
|
+
p.values.some((v) => v.value.toLowerCase() === q));
|
|
126
|
+
if (exact.length > 0)
|
|
127
|
+
return exact;
|
|
128
|
+
// Otherwise fall back to substring matching to forgive partial queries.
|
|
129
|
+
return exports.ENUM_PARAMS.filter((p) => p.command.toLowerCase().includes(q) || norm(p.param).includes(q));
|
|
130
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ const press_key_js_1 = require("./commands/press-key.js");
|
|
|
28
28
|
const session_js_1 = require("./commands/session.js");
|
|
29
29
|
const daemon_js_1 = require("./commands/daemon.js");
|
|
30
30
|
const install_js_1 = require("./commands/install.js");
|
|
31
|
+
const init_js_1 = require("./commands/init.js");
|
|
31
32
|
const device_pool_js_1 = require("./commands/device-pool.js");
|
|
32
33
|
const run_parallel_js_1 = require("./commands/run-parallel.js");
|
|
33
34
|
const run_sequence_js_1 = require("./commands/run-sequence.js");
|
|
@@ -59,6 +60,7 @@ const logs_js_1 = require("./commands/logs.js");
|
|
|
59
60
|
const memory_js_1 = require("./commands/memory.js");
|
|
60
61
|
const metro_js_1 = require("./commands/metro.js");
|
|
61
62
|
const clipboard_js_1 = require("./commands/clipboard.js");
|
|
63
|
+
const options_js_1 = require("./commands/options.js");
|
|
62
64
|
const device_picker_js_1 = require("./device-picker.js");
|
|
63
65
|
const update_check_js_1 = require("./update-check.js");
|
|
64
66
|
const pkg_root_js_1 = require("./pkg-root.js");
|
|
@@ -101,6 +103,7 @@ const COMMAND_HELP = {
|
|
|
101
103
|
'run-flow-inline': run_flow_inline_js_1.HELP,
|
|
102
104
|
session: session_js_1.HELP,
|
|
103
105
|
'install-web': install_js_1.HELP_INSTALL_WEB,
|
|
106
|
+
init: init_js_1.HELP,
|
|
104
107
|
'daemon-start': daemon_js_1.HELP_DAEMON_START,
|
|
105
108
|
'daemon-stop': daemon_js_1.HELP_DAEMON_STOP,
|
|
106
109
|
'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
|
|
@@ -119,12 +122,14 @@ const COMMAND_HELP = {
|
|
|
119
122
|
metro: metro_js_1.HELP,
|
|
120
123
|
clipboard: clipboard_js_1.HELP,
|
|
121
124
|
paste: ' paste Trigger OS-level paste (or type clipboard on iOS)',
|
|
125
|
+
'list-options': options_js_1.HELP,
|
|
122
126
|
};
|
|
123
127
|
const OPTIONS_HELP = `Options:
|
|
124
128
|
--device <id> Target device ID (also keys the session and daemon)
|
|
125
129
|
--device-name <n> Target a booted device by name (resolved to ID from booted devices)
|
|
126
130
|
--platform <p> Filter to devices of this platform (ios, android, tvos, web)
|
|
127
131
|
--json Output as machine-readable JSON
|
|
132
|
+
--options List valid values for a command's enumerated parameters and exit
|
|
128
133
|
--verbose, -v Log daemon calls, fallbacks, and raw output
|
|
129
134
|
--version, -V Print version number
|
|
130
135
|
--help, -h Show this help`;
|
|
@@ -141,6 +146,7 @@ async function main() {
|
|
|
141
146
|
boolean: [
|
|
142
147
|
'json',
|
|
143
148
|
'help',
|
|
149
|
+
'options',
|
|
144
150
|
'version',
|
|
145
151
|
'clear',
|
|
146
152
|
'list',
|
|
@@ -163,6 +169,9 @@ async function main() {
|
|
|
163
169
|
'leaks',
|
|
164
170
|
'snapshots',
|
|
165
171
|
'growth-only',
|
|
172
|
+
'global',
|
|
173
|
+
'force',
|
|
174
|
+
'yes',
|
|
166
175
|
],
|
|
167
176
|
string: [
|
|
168
177
|
'device',
|
|
@@ -222,7 +231,7 @@ async function main() {
|
|
|
222
231
|
'user-agent',
|
|
223
232
|
'color-scheme',
|
|
224
233
|
],
|
|
225
|
-
alias: { h: 'help', v: 'verbose', V: 'version', o: 'output' },
|
|
234
|
+
alias: { h: 'help', v: 'verbose', V: 'version', o: 'output', y: 'yes' },
|
|
226
235
|
});
|
|
227
236
|
if (argv['verbose'])
|
|
228
237
|
(0, verbose_js_1.setVerbose)(true);
|
|
@@ -234,6 +243,12 @@ async function main() {
|
|
|
234
243
|
console.log(pkg.version);
|
|
235
244
|
process.exit(0);
|
|
236
245
|
}
|
|
246
|
+
// `<command> --options` lists the valid values for that command's enumerated
|
|
247
|
+
// parameters and exits — no device resolution needed. With no command it
|
|
248
|
+
// lists every enumerated parameter. `--help` still wins if both are passed.
|
|
249
|
+
if (argv['options'] && !argv['help'] && command !== 'list-options') {
|
|
250
|
+
process.exit((0, options_js_1.listOptions)(command, opts));
|
|
251
|
+
}
|
|
237
252
|
// Handle help and unknown commands before device resolution —
|
|
238
253
|
// no point prompting for a device if we're just printing help or erroring out.
|
|
239
254
|
if (!command || argv['help']) {
|
|
@@ -252,11 +267,13 @@ async function main() {
|
|
|
252
267
|
'stop-device',
|
|
253
268
|
'delete-device',
|
|
254
269
|
'install-web',
|
|
270
|
+
'init',
|
|
255
271
|
'copy-app',
|
|
256
272
|
'device-pool',
|
|
257
273
|
'run-parallel',
|
|
258
274
|
'metro',
|
|
259
275
|
'workspace',
|
|
276
|
+
'list-options',
|
|
260
277
|
// `logs --list` and `logs --source metro` only query Metro on localhost — no device needed
|
|
261
278
|
// `logs` always needs a device session — Metro discovery is device-scoped.
|
|
262
279
|
// `daemon-stop --all` stops every daemon — no device needed
|
|
@@ -418,6 +435,10 @@ async function main() {
|
|
|
418
435
|
exitCode = await (0, press_key_js_1.pressKey)(key, opts, sessionName);
|
|
419
436
|
break;
|
|
420
437
|
}
|
|
438
|
+
case 'list-options': {
|
|
439
|
+
exitCode = (0, options_js_1.listOptions)(rest[0], opts);
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
421
442
|
case 'scroll': {
|
|
422
443
|
const dir = (argv['direction'] || 'down').toLowerCase();
|
|
423
444
|
exitCode = await (0, scroll_js_1.scroll)(dir, opts, sessionName);
|
|
@@ -610,6 +631,13 @@ async function main() {
|
|
|
610
631
|
case 'session':
|
|
611
632
|
exitCode = await (0, session_js_1.sessionCmd)(argv['clear'], argv['list'], opts, sessionName);
|
|
612
633
|
break;
|
|
634
|
+
case 'init':
|
|
635
|
+
exitCode = await (0, init_js_1.init)(opts, rest[0], {
|
|
636
|
+
global: argv['global'],
|
|
637
|
+
force: argv['force'],
|
|
638
|
+
yes: argv['yes'],
|
|
639
|
+
});
|
|
640
|
+
break;
|
|
613
641
|
case 'install-web':
|
|
614
642
|
exitCode = await (0, install_js_1.installWebCli)(opts, argv['check'], rest[0]);
|
|
615
643
|
break;
|
package/dist/utils.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DIRECTIONS = void 0;
|
|
3
4
|
exports.sleep = sleep;
|
|
4
5
|
exports.swipeCoords = swipeCoords;
|
|
5
6
|
function sleep(ms) {
|
|
6
7
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
8
|
}
|
|
9
|
+
exports.DIRECTIONS = ['down', 'up', 'left', 'right'];
|
|
8
10
|
function swipeCoords(dir) {
|
|
9
11
|
switch (dir) {
|
|
10
12
|
case 'down':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@houwert/conductor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "CLI tool for mobile app interactions — optimized for AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"main": "./dist/index.js",
|
|
18
18
|
"files": [
|
|
19
19
|
"dist/",
|
|
20
|
-
"proto/"
|
|
20
|
+
"proto/",
|
|
21
|
+
"skills/"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "tsc",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conductor-create-flow
|
|
3
|
+
description: Author and run Maestro-compatible YAML flows and command sequences with the conductor CLI, including recording flows from live interactions and sharding them across devices. Use when scripting a repeatable multi-step app journey, running an existing Maestro flow, recording a flow by interacting with the app, or running flows in parallel across booted devices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Conductor — flows
|
|
7
|
+
|
|
8
|
+
A **flow** is a YAML file describing a sequence of conductor commands.
|
|
9
|
+
Conductor's format is a subset of [Maestro](https://maestro.mobile.dev)'s — most
|
|
10
|
+
existing Maestro flows run unchanged. Use flows for repeatable journeys; use
|
|
11
|
+
`conductor-device-interact` for one-off ad-hoc steps.
|
|
12
|
+
|
|
13
|
+
## Run flows
|
|
14
|
+
|
|
15
|
+
| Command | Purpose |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `conductor run-flow <file> [--env K=V] [--benchmark]` | Run a Maestro YAML flow file |
|
|
18
|
+
| `conductor run-flow-inline '<yaml>' [--benchmark]` | Run inline YAML from the command line |
|
|
19
|
+
| `conductor run-sequence [--file path.json]` | Run a JSON sequence of conductor commands serially; reads stdin if no `--file` |
|
|
20
|
+
| `conductor run-parallel --flows-dir <path>` | Shard a directory of flows across all booted devices |
|
|
21
|
+
|
|
22
|
+
`--benchmark` prints elapsed time per command and total flow time.
|
|
23
|
+
|
|
24
|
+
## Flow YAML
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
appId: com.example.myapp
|
|
28
|
+
---
|
|
29
|
+
- launchApp
|
|
30
|
+
- tapOn: "Sign In"
|
|
31
|
+
- inputText: "user@example.com"
|
|
32
|
+
- assertVisible: "Dashboard"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`run-sequence` JSON shape (stops on first non-zero exit):
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{ "steps": [ { "cmd": "tap-on", "args": ["Login"] }, { "cmd": "input-text", "args": ["user@example.com"] } ] }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Record a flow from your interactions
|
|
42
|
+
|
|
43
|
+
| Command | Purpose |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `conductor flow record start [--out path]` | Start recording this session's interactions to a YAML file |
|
|
46
|
+
| `conductor flow record echo <text>` | Insert a `console.log` step |
|
|
47
|
+
| `conductor flow record status` | Show the active recording path |
|
|
48
|
+
| `conductor flow record finish` | Close the recording, print the file path |
|
|
49
|
+
|
|
50
|
+
Record, then interact via `conductor-device-interact`; each action is appended
|
|
51
|
+
to the flow. `finish` gives you a runnable `.yaml`.
|
|
52
|
+
|
|
53
|
+
## Tips
|
|
54
|
+
|
|
55
|
+
- Add `--json` for machine-readable output; a failed step exits non-zero.
|
|
56
|
+
- `conductor run-flow --help` (and friends) for exact flags.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conductor-device-interact
|
|
3
|
+
description: Drive a running iOS simulator, Android emulator, tvOS simulator, or Playwright web app with the conductor CLI. Use when launching apps, tapping UI elements, typing text, scrolling/swiping, performing gestures, pressing hardware/keyboard keys, opening URLs or deep links, navigating back, or verifying an app change in the real running app.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Conductor — device interaction
|
|
7
|
+
|
|
8
|
+
`conductor` drives a real running app the way a user would: launch it, tap,
|
|
9
|
+
type, scroll, and assert. It bundles its own native drivers — no second CLI to
|
|
10
|
+
install. Use it to verify a change in the actual app, not just in tests.
|
|
11
|
+
|
|
12
|
+
To **observe** the screen (inspect the hierarchy, screenshot, read element
|
|
13
|
+
state), use the `conductor-inspect` skill — it pairs with this one.
|
|
14
|
+
|
|
15
|
+
## The core loop: act → observe → act
|
|
16
|
+
|
|
17
|
+
Never tap blind, never assume the result. After every action, observe before
|
|
18
|
+
the next one.
|
|
19
|
+
|
|
20
|
+
1. **Observe** with `conductor capture-ui` (see `conductor-inspect`) to see the
|
|
21
|
+
screen and get short element refs (`@e1`, `@e2`, …).
|
|
22
|
+
2. **Act** — `tap-on`, `input-text`, `scroll`, etc.
|
|
23
|
+
3. **Confirm** with `assert-visible` / another `capture-ui`.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
conductor launch-app com.example.myapp
|
|
27
|
+
conductor capture-ui # observe; get @eN refs
|
|
28
|
+
conductor tap-on "Sign In" # or: conductor tap-on @e3
|
|
29
|
+
conductor input-text "user@example.com"
|
|
30
|
+
conductor assert-visible "Dashboard"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Interaction commands
|
|
34
|
+
|
|
35
|
+
| Command | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `conductor launch-app <appId>` | Launch app (saved to session). `--no-stop-app` resumes; `--argument key=value` passes launch args |
|
|
38
|
+
| `conductor stop-app [<appId>]` | Stop the app |
|
|
39
|
+
| `conductor tap-on <element>` | Tap by text, id, or `@eN`. `--long-press`, `--double-tap`, `--optional`, `--index <n>` |
|
|
40
|
+
| `conductor input-text <text>` | Type into the focused field |
|
|
41
|
+
| `conductor erase-text [n]` | Erase n characters (default 50) |
|
|
42
|
+
| `conductor press-key <key>` | Press a key (Enter, Backspace, Home, …) |
|
|
43
|
+
| `conductor hide-keyboard` | Dismiss the on-screen keyboard |
|
|
44
|
+
| `conductor back` | Press back |
|
|
45
|
+
| `conductor scroll [--direction down\|up\|left\|right]` | Scroll |
|
|
46
|
+
| `conductor scroll-until-visible <element> [--direction] [--timeout ms]` | Scroll until element appears |
|
|
47
|
+
| `conductor swipe --direction <dir>` / `--start <x,y> --end <x,y> [--duration ms]` | Swipe |
|
|
48
|
+
| `conductor open-link <url>` | Open a URL / deep link |
|
|
49
|
+
| `conductor pinch [--scale N] [--center x,y]` | Two-finger pinch (scale<1 out, >1 in) |
|
|
50
|
+
| `conductor rotate-gesture [--degrees N] [--center x,y]` | Two-finger rotate |
|
|
51
|
+
| `conductor gesture <json\|--file path>` | Play a multi-touch path |
|
|
52
|
+
| `conductor clipboard read` / `clipboard write <text>` / `paste` | Clipboard (iOS) |
|
|
53
|
+
| `conductor list-options [command]` | List valid values for enumerated params |
|
|
54
|
+
|
|
55
|
+
## Discovering valid values
|
|
56
|
+
|
|
57
|
+
Several commands only accept a fixed set of values (`press-key <key>`,
|
|
58
|
+
`--direction`, `set-orientation`, `set-viewport --preset`/`--color-scheme`,
|
|
59
|
+
`logs --level`/`--source`, `--platform`). Don't guess — list them:
|
|
60
|
+
|
|
61
|
+
- `conductor <command> --options` — valid values for that command, e.g.
|
|
62
|
+
`conductor press-key --options`, `conductor swipe --options`.
|
|
63
|
+
- `conductor list-options [command|param]` — same data; with no argument it
|
|
64
|
+
lists every enumerated parameter, or filter by name (`list-options direction`).
|
|
65
|
+
- Add `--json` for machine-readable output.
|
|
66
|
+
|
|
67
|
+
## Selecting elements
|
|
68
|
+
|
|
69
|
+
Positional `<element>` matches **accessibility id first, then visible text**.
|
|
70
|
+
Disambiguate when multiple match:
|
|
71
|
+
|
|
72
|
+
- `--id <id>` / `--text <text>` — id-only / text-only matching
|
|
73
|
+
- `--index <n>` — nth match (0-based)
|
|
74
|
+
- `--below` / `--above` / `--left-of` / `--right-of <text>` — relative position
|
|
75
|
+
- `--focused`, `--enabled`, `--checked`, `--selected` — state filters
|
|
76
|
+
- `--timeout <ms>` — wait for the element to appear
|
|
77
|
+
- `--optional` — missing element is a no-op, not an error
|
|
78
|
+
- `@eN` — exact element from the **last `capture-ui`** (cached coords, ephemeral
|
|
79
|
+
~60s; re-capture after navigating)
|
|
80
|
+
|
|
81
|
+
If you can't find an element, run `conductor inspect` or `capture-ui` to see the
|
|
82
|
+
real ids and texts rather than guessing.
|
|
83
|
+
|
|
84
|
+
## ⚠️ Don't reset state to "fix" navigation
|
|
85
|
+
|
|
86
|
+
Never use `launch-app --clear-state`, `clear-state`, or `--clear-keychain` to
|
|
87
|
+
clear focus or navigation — they **wipe user data and sign the user out**, and
|
|
88
|
+
can't be undone without their credentials. Navigate out with `back` / Menu, or
|
|
89
|
+
relaunch without the flag. (See `conductor-device-setup`.)
|
|
90
|
+
|
|
91
|
+
## Tips
|
|
92
|
+
|
|
93
|
+
- `--device <id>` / `--device-name <name>` targets a device; `--platform <ios|android|tvos|web>` scopes by platform.
|
|
94
|
+
- Add `--json` for machine-readable output; failed assertions exit non-zero.
|
|
95
|
+
- Run a per-session daemon for many commands (see `conductor-device-setup`).
|
|
96
|
+
- `conductor <command> --help` for exact flags.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conductor-device-setup
|
|
3
|
+
description: Boot, list, and manage devices and app installs for the conductor CLI — iOS simulators, Android emulators, tvOS simulators, and Playwright web browsers — plus sessions, the warm-driver daemon, and the parallel device pool. Use when starting or stopping a simulator/emulator/browser, installing or launching an app, setting up the web driver, keeping the driver warm, or coordinating multiple devices for parallel agents.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Conductor — device & app setup
|
|
7
|
+
|
|
8
|
+
Get a device running and an app installed before you drive it. Start here when
|
|
9
|
+
nothing is booted yet.
|
|
10
|
+
|
|
11
|
+
## Orient first
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
conductor workspace info # detected project type, bundle IDs, devices, Metro port — best first call
|
|
15
|
+
conductor list-devices # booted + available devices
|
|
16
|
+
conductor foreground-app # bundle id of the app currently in front
|
|
17
|
+
conductor list-apps # installed app ids / package names
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Devices
|
|
21
|
+
|
|
22
|
+
| Command | Purpose |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `conductor start-device --platform <ios\|android\|tvos\|web>` | Boot a simulator/emulator or start the web driver |
|
|
25
|
+
| `conductor start-device --os-version <n> --device-type <name>` | Pick OS version + device type (creates if needed) |
|
|
26
|
+
| `conductor stop-device [<name-or-id>] [--all]` | Shut down device(s) |
|
|
27
|
+
| `conductor delete-device <name-or-id> [--all]` | Delete simulator(s)/AVD(s)/web session(s) |
|
|
28
|
+
| `conductor set-location --lat <n> --lng <n>` | Set GPS coordinates |
|
|
29
|
+
| `conductor set-orientation <portrait\|landscape>` | Set orientation |
|
|
30
|
+
| `conductor set-viewport [<w> <h>] [--preset mobile\|tablet\|desktop]` | Resize web viewport (web only) |
|
|
31
|
+
| `conductor install-web [--check] [browser]` | Install a Playwright browser (chromium/firefox/webkit); `--check` = status |
|
|
32
|
+
|
|
33
|
+
## App lifecycle
|
|
34
|
+
|
|
35
|
+
| Command | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `conductor install-app <path>` | Install .app / .ipa / .apk |
|
|
38
|
+
| `conductor launch-app <appId>` | Launch app (saved to session); `--no-stop-app`, `--argument key=value` |
|
|
39
|
+
| `conductor stop-app [<appId>]` | Stop app |
|
|
40
|
+
| `conductor uninstall-app <appId>` | Uninstall app |
|
|
41
|
+
| `conductor copy-app <bundleId> --from <id> --to <id>` | Copy an installed app between iOS simulators |
|
|
42
|
+
| `conductor download-app <appId> --output <path>` | Download installed app binary |
|
|
43
|
+
|
|
44
|
+
### ⚠️ Destructive flags — ask the user first
|
|
45
|
+
|
|
46
|
+
`conductor clear-state [<appId>]`, `launch-app --clear-state`, and
|
|
47
|
+
`launch-app --clear-keychain` **wipe app data and sign the user out**, and can't
|
|
48
|
+
be undone without their credentials. Never use them to "reset focus" or clear
|
|
49
|
+
navigation — relaunch without the flag, or navigate out with `back` / Menu. If
|
|
50
|
+
you genuinely need one, ask the human first.
|
|
51
|
+
|
|
52
|
+
## Sessions, daemon & device pool
|
|
53
|
+
|
|
54
|
+
A **session** remembers the last device + app so you don't re-specify them.
|
|
55
|
+
Parallel agents each get their own `--session <name>` so they don't collide.
|
|
56
|
+
|
|
57
|
+
| Command | Purpose |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `conductor session [--clear] [--list]` | Show, clear, or list sessions |
|
|
60
|
+
| `conductor daemon-start` | Start the per-session background daemon (keeps the driver warm — do this for any multi-step session) |
|
|
61
|
+
| `conductor daemon-status` | Show daemon status |
|
|
62
|
+
| `conductor daemon-stop [--all]` | Stop this session's daemon (`--all` = every session) |
|
|
63
|
+
| `conductor device-pool --list` | List devices + pool status |
|
|
64
|
+
| `conductor device-pool --acquire` | Claim a free device (prints id) |
|
|
65
|
+
| `conductor device-pool --release <id>` | Release a device back to the pool |
|
|
66
|
+
|
|
67
|
+
Don't leave a daemon running when you're done — `daemon-stop` it.
|
|
68
|
+
|
|
69
|
+
## Tips
|
|
70
|
+
|
|
71
|
+
- `--device <id>` / `--device-name <name>` targets a device; `--platform` scopes by platform.
|
|
72
|
+
- Add `--json` for machine-readable output.
|
|
73
|
+
- `conductor <command> --help` for exact flags.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conductor-inspect
|
|
3
|
+
description: Read the live UI state of a running app with the conductor CLI — view hierarchy, accessibility snapshot, screenshots, focused element, and element refs. Use when you need to see what's on screen, find an element's id/text/coordinates, take a screenshot, check focus, or assert that something is (or isn't) visible before or after acting.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Conductor — inspection & assertions
|
|
7
|
+
|
|
8
|
+
These commands let you **observe** a running app's screen so you know what to do
|
|
9
|
+
next. Pair them with `conductor-device-interact`, which acts on what you find
|
|
10
|
+
here. Always observe before you act, and confirm after.
|
|
11
|
+
|
|
12
|
+
## Observe the screen
|
|
13
|
+
|
|
14
|
+
| Command | Purpose |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `conductor capture-ui [--output <path.json>]` | Screenshot + hierarchy + a11y snapshot in one JSON bundle; assigns short `@eN` refs. **Preferred way to observe.** |
|
|
17
|
+
| `conductor inspect [--dump]` | Print the UI hierarchy (`--dump` = raw driver output) |
|
|
18
|
+
| `conductor inspect --at <x,y> [--tappable]` | Topmost view at a screen point |
|
|
19
|
+
| `conductor focused [--poll [ms]]` | Metadata of the focused element. `--poll` watches changes — only with a bounded use, then stop it |
|
|
20
|
+
| `conductor take-screenshot [<element>] [--output <path>] [--full-page]` | Screenshot; crop to a matched element; `--full-page` (web) |
|
|
21
|
+
|
|
22
|
+
`capture-ui` is the workhorse: it returns the screen as structured data **and**
|
|
23
|
+
gives each element a ref like `@e3` that `conductor tap-on @e3` taps by cached
|
|
24
|
+
coordinates. Refs are ephemeral (~60s) — re-capture after navigating or waiting.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
conductor capture-ui --output /tmp/screen.json
|
|
28
|
+
# read it: element texts, ids, frames, and @eN refs
|
|
29
|
+
conductor tap-on @e5
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Assertions
|
|
33
|
+
|
|
34
|
+
| Command | Purpose |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `conductor assert-visible <element> [--timeout ms]` | Assert element is visible (non-zero exit on failure) |
|
|
37
|
+
| `conductor assert-not-visible <element> [--timeout ms]` | Assert element is absent |
|
|
38
|
+
|
|
39
|
+
Both take the same selectors as `tap-on`: `--id`, `--text`, `--index`,
|
|
40
|
+
`--below` / `--above` / `--left-of` / `--right-of`, `--focused`, `--enabled`,
|
|
41
|
+
`--checked`, `--selected`, `--optional`.
|
|
42
|
+
|
|
43
|
+
## Tips
|
|
44
|
+
|
|
45
|
+
- Add `--json` to parse output programmatically (pipe through `jq`).
|
|
46
|
+
- When an interaction can't find an element, `inspect` / `capture-ui` shows the
|
|
47
|
+
real ids and texts on screen — don't guess selectors.
|
|
48
|
+
- `conductor <command> --help` for exact flags.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conductor-metro-debugger
|
|
3
|
+
description: Inspect a running app's JS runtime, logs, and network with the conductor CLI — evaluate JS in React Native (Hermes) or the web page, dump the React component tree, read console/Metro/device logs, and inspect or issue HTTP requests. Use when debugging app behavior, reading logs, evaluating expressions in the live runtime, inspecting React components, or examining network traffic.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Conductor — runtime debugging, logs & network
|
|
7
|
+
|
|
8
|
+
Inspect what a running app is doing under the UI — its JS runtime, console
|
|
9
|
+
output, and HTTP traffic. Works against React Native (Hermes/Fusebox) and
|
|
10
|
+
Playwright web.
|
|
11
|
+
|
|
12
|
+
## Runtime (React Native / web)
|
|
13
|
+
|
|
14
|
+
| Command | Purpose |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `conductor debug status [--port N]` | RN debugger connection info |
|
|
17
|
+
| `conductor debug evaluate <expr> [--port N]` | Run JS in the app runtime (Hermes or web page) |
|
|
18
|
+
| `conductor debug component-tree [--port N]` | On-screen React component tree |
|
|
19
|
+
| `conductor debug inspect-element <x,y>` | React component at a screen point |
|
|
20
|
+
| `conductor debug log-registry [--source metro]` | Summarize recent Metro/Hermes console logs |
|
|
21
|
+
|
|
22
|
+
## Logs
|
|
23
|
+
|
|
24
|
+
| Command | Purpose |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `conductor logs --recent <n>` | Last N buffered log lines — **agent-friendly, exits immediately** |
|
|
27
|
+
| `conductor logs [--source metro\|device] [--level …] [--json] [--duration s]` | Stream logs (bound it with `--duration`) |
|
|
28
|
+
| `conductor logs --list` | List Metro debugger targets for this device |
|
|
29
|
+
|
|
30
|
+
Prefer `logs --recent N` over a bare `logs` stream — don't leave a stream
|
|
31
|
+
running indefinitely.
|
|
32
|
+
|
|
33
|
+
## Network
|
|
34
|
+
|
|
35
|
+
| Command | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `conductor network logs [--limit N]` | Recent HTTP traffic (RN fetch/XHR shim; web via Playwright) |
|
|
38
|
+
| `conductor network request <url> [--method M] [--body STR] [--header K=V]` | Issue an HTTP request from the app's context |
|
|
39
|
+
|
|
40
|
+
## Metro bundler
|
|
41
|
+
|
|
42
|
+
| Command | Purpose |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `conductor metro reload [--port N] [--target N]` | Reload the JS bundle without restarting native |
|
|
45
|
+
| `conductor metro stop [--port N]` | Stop the Metro bundler on a port (default 8081) |
|
|
46
|
+
|
|
47
|
+
## Tips
|
|
48
|
+
|
|
49
|
+
- `--port N` targets a specific Metro/debugger port when auto-detection isn't enough.
|
|
50
|
+
- Add `--json` for machine-readable output.
|
|
51
|
+
- For crashes and performance, see `conductor-profiler`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conductor-profiler
|
|
3
|
+
description: Profile a running app's CPU, memory, and React render performance with the conductor CLI, and read crash reports. Use when investigating slowness, jank, memory growth or leaks, excessive React re-renders, or when an app has crashed and you need the crash report.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Conductor — profiling & crashes
|
|
7
|
+
|
|
8
|
+
Measure a running app's performance and inspect crashes.
|
|
9
|
+
|
|
10
|
+
## Profiling
|
|
11
|
+
|
|
12
|
+
| Command | Purpose |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `conductor profile cpu --duration <s> [--out <path>]` | Record a CPU trace (iOS: xctrace, Android: simpleperf) |
|
|
15
|
+
| `conductor profile memory --track <s> [--interval ms] [<appId>]` | Sample memory for N seconds, report deltas |
|
|
16
|
+
| `conductor profile react start` / `profile react stop [--top N]` | Install a React commit-profiler hook, then summarize captured commits |
|
|
17
|
+
|
|
18
|
+
## Memory
|
|
19
|
+
|
|
20
|
+
| Command | Purpose |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `conductor memory [<appId>]` | Device + app memory usage |
|
|
23
|
+
| `conductor memory --objects` | Include per-class object counts (iOS heap; slower) |
|
|
24
|
+
| `conductor memory --leaks` | Run leak detection (iOS only; slow, can pause the app) |
|
|
25
|
+
| `conductor memory --save <name>` / `--diff <name>` / `--diff <name> --vs <other>` | Snapshot and diff memory reports |
|
|
26
|
+
| `conductor memory --filter <regex>` / `--growth-only` / `--top <n>` | Narrow object/class tables (great for leak-hunting) |
|
|
27
|
+
|
|
28
|
+
Typical leak hunt: `memory --save before`, exercise the screen, then
|
|
29
|
+
`memory --diff before --growth-only`.
|
|
30
|
+
|
|
31
|
+
## Crashes
|
|
32
|
+
|
|
33
|
+
| Command | Purpose |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `conductor crashes list [--app <bundleId>] [--since <duration>]` | List recent crash reports (iOS host + Android logcat) |
|
|
36
|
+
| `conductor crashes show <id>` | Print a specific crash report |
|
|
37
|
+
| `conductor crashes tail` | Stream new crash reports as they appear |
|
|
38
|
+
|
|
39
|
+
## Tips
|
|
40
|
+
|
|
41
|
+
- Add `--json` to parse reports programmatically.
|
|
42
|
+
- These commands can be slow or pause the app — scope them with `--duration` / `--track` and avoid leaving `crashes tail` running.
|