@houwert/conductor 0.12.3 → 0.13.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 +3 -28
- package/dist/commands/install.js +1 -139
- package/dist/commands/logs.js +81 -85
- package/dist/daemon/client.js +2 -9
- package/dist/daemon/log-collector.js +91 -198
- package/dist/daemon/server.js +1 -11
- package/dist/drivers/log-sources/daemon.js +2 -9
- package/dist/drivers/log-sources/metro-discovery.js +208 -0
- package/dist/index.js +1 -21
- package/package.json +3 -6
- package/.claude-plugin/plugin.json +0 -9
- package/dist/commands/cheat-sheet.js +0 -109
- package/dist/postinstall.js +0 -12
- package/skills/conductor/SKILL.md +0 -825
- package/skills/conductor/references/flow-syntax.md +0 -182
- package/skills/skills.yaml +0 -8
package/README.md
CHANGED
|
@@ -38,34 +38,9 @@ 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.
|
|
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.
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
The plugin registers itself globally in `~/.claude/plugins/` and ships two skill files:
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
skills/conductor/
|
|
49
|
-
├── SKILL.md # Full command reference and agent workflow guide
|
|
50
|
-
└── references/
|
|
51
|
-
└── flow-syntax.md # Maestro YAML flow syntax reference
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Claude learns every available command, how to coordinate across devices, and how to write and run Maestro YAML flows. See [`skills/conductor/SKILL.md`](./skills/conductor/SKILL.md) for the full reference.
|
|
55
|
-
|
|
56
|
-
### Install modes
|
|
57
|
-
|
|
58
|
-
| Command | What it does |
|
|
59
|
-
|---|---|
|
|
60
|
-
| `npm install -g @houwert/conductor` | Registers or updates the global Claude Code plugin (via package postinstall) |
|
|
61
|
-
| `conductor install-plugin` | Re-register or update the global Claude Code plugin (same as postinstall) |
|
|
62
|
-
| `conductor install-plugin --check` | Print whether the global plugin is registered (no changes) |
|
|
63
|
-
| `conductor install-skills` | Copy skills into `.claude/skills/conductor/` in the current project |
|
|
64
|
-
| `conductor install-skills --check` | Print whether local skills are installed (no changes) |
|
|
65
|
-
| `conductor install-web` | Install a Playwright browser for web automation (default: chromium) |
|
|
66
|
-
| `conductor install-web --check` | Print which Playwright browsers are installed (no changes) |
|
|
67
|
-
|
|
68
|
-
### 📱 What Claude can do
|
|
43
|
+
### 📱 What the CLI can do
|
|
69
44
|
|
|
70
45
|
| Capability | Commands |
|
|
71
46
|
|---|---|
|
|
@@ -76,6 +51,7 @@ Claude learns every available command, how to coordinate across devices, and how
|
|
|
76
51
|
| Navigation | `open-link`, `back` |
|
|
77
52
|
| Flows | `run-flow`, `run-flow-inline`, `run-parallel` |
|
|
78
53
|
| Devices | `start-device`, `list-devices`, `set-location`, `set-orientation` |
|
|
54
|
+
| Web setup | `install-web [browser]` (installs a Playwright browser; `--check` prints status) |
|
|
79
55
|
|
|
80
56
|
## 🔨 Building locally
|
|
81
57
|
|
|
@@ -113,7 +89,6 @@ make build-cli # CLI TypeScript only
|
|
|
113
89
|
make build-ios-driver # iOS XCTest driver
|
|
114
90
|
make build-android-driver # Android instrumentation driver
|
|
115
91
|
make package-cli # Bundle drivers into CLI package
|
|
116
|
-
make copy-skills # Copy skills/ into packages/cli/skills/ (build artifact)
|
|
117
92
|
```
|
|
118
93
|
|
|
119
94
|
## 🗂️ Repository structure
|
package/dist/commands/install.js
CHANGED
|
@@ -1,78 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.HELP_INSTALL_WEB =
|
|
7
|
-
exports.installPluginCli = installPluginCli;
|
|
8
|
-
exports.installSkillsCli = installSkillsCli;
|
|
3
|
+
exports.HELP_INSTALL_WEB = void 0;
|
|
9
4
|
exports.installWebCli = installWebCli;
|
|
10
|
-
exports.installLocalSkills = installLocalSkills;
|
|
11
|
-
exports.installPlugin = installPlugin;
|
|
12
|
-
exports.HELP_INSTALL_PLUGIN = ` install-plugin [--check] Register/update the global Claude Code plugin (status only with --check)`;
|
|
13
|
-
exports.HELP_INSTALL_SKILLS = ` install-skills [--check] Copy skills into local .claude/skills/ (status only with --check)`;
|
|
14
5
|
exports.HELP_INSTALL_WEB = ` install-web [--check] [browser] Install Playwright browser (chromium, firefox, webkit) (status only with --check)`;
|
|
15
|
-
const fs_1 = __importDefault(require("fs"));
|
|
16
|
-
const os_1 = __importDefault(require("os"));
|
|
17
|
-
const path_1 = __importDefault(require("path"));
|
|
18
6
|
const output_js_1 = require("../output.js");
|
|
19
|
-
const pkg_root_js_1 = require("../pkg-root.js");
|
|
20
7
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
21
|
-
async function installPluginCli(opts, check) {
|
|
22
|
-
try {
|
|
23
|
-
if (check) {
|
|
24
|
-
return checkPluginInstallStatus(opts);
|
|
25
|
-
}
|
|
26
|
-
const version = installPlugin();
|
|
27
|
-
const pluginCacheDir = path_1.default.join(os_1.default.homedir(), '.claude', 'plugins', 'cache', 'conductor', 'conductor', version);
|
|
28
|
-
(0, output_js_1.printSuccess)(`Conductor plugin installed (v${version}) → ${pluginCacheDir}`, opts);
|
|
29
|
-
return 0;
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
-
(0, output_js_1.printError)(`Install failed: ${message}`, opts);
|
|
34
|
-
return 1;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function checkPluginInstallStatus(opts) {
|
|
38
|
-
const installedPluginsPath = path_1.default.join(os_1.default.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
39
|
-
let pluginVersion = null;
|
|
40
|
-
if (fs_1.default.existsSync(installedPluginsPath)) {
|
|
41
|
-
const installed = JSON.parse(fs_1.default.readFileSync(installedPluginsPath, 'utf8'));
|
|
42
|
-
const plugins = Array.isArray(installed.plugins) ? installed.plugins : [];
|
|
43
|
-
const entry = plugins.find((p) => p.name === 'conductor');
|
|
44
|
-
if (entry)
|
|
45
|
-
pluginVersion = entry.version;
|
|
46
|
-
}
|
|
47
|
-
if (opts.json) {
|
|
48
|
-
(0, output_js_1.printData)({ globalPlugin: { installed: pluginVersion !== null, version: pluginVersion } }, opts);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
if (pluginVersion) {
|
|
52
|
-
console.log(`Global plugin: installed (v${pluginVersion})`);
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
console.log('Global plugin: not installed');
|
|
56
|
-
console.log('Run `npm install -g @houwert/conductor` or `conductor install-plugin` to register it.');
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return 0;
|
|
60
|
-
}
|
|
61
|
-
async function installSkillsCli(opts, check) {
|
|
62
|
-
try {
|
|
63
|
-
if (check) {
|
|
64
|
-
return checkSkillsInstallStatus(opts);
|
|
65
|
-
}
|
|
66
|
-
installLocalSkills();
|
|
67
|
-
(0, output_js_1.printSuccess)('Conductor skills installed → .claude/skills/conductor/', opts);
|
|
68
|
-
return 0;
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
-
(0, output_js_1.printError)(`Install failed: ${message}`, opts);
|
|
73
|
-
return 1;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
8
|
async function installWebCli(opts, check, browserArg) {
|
|
77
9
|
try {
|
|
78
10
|
if (check) {
|
|
@@ -86,23 +18,6 @@ async function installWebCli(opts, check, browserArg) {
|
|
|
86
18
|
return 1;
|
|
87
19
|
}
|
|
88
20
|
}
|
|
89
|
-
function checkSkillsInstallStatus(opts) {
|
|
90
|
-
const localSkillsPath = path_1.default.join(process.cwd(), '.claude', 'skills', 'conductor', 'SKILL.md');
|
|
91
|
-
const hasLocalSkills = fs_1.default.existsSync(localSkillsPath);
|
|
92
|
-
if (opts.json) {
|
|
93
|
-
(0, output_js_1.printData)({ localSkills: { installed: hasLocalSkills } }, opts);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
if (hasLocalSkills) {
|
|
97
|
-
console.log('Local skills: installed → .claude/skills/conductor/');
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
console.log('Local skills: not installed');
|
|
101
|
-
console.log('Run `conductor install-skills` to copy skills into this project.');
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return 0;
|
|
105
|
-
}
|
|
106
21
|
function checkWebInstallStatus(opts) {
|
|
107
22
|
const webBrowsers = {
|
|
108
23
|
chromium: (0, bootstrap_js_1.isPlaywrightBrowserInstalled)('chromium'),
|
|
@@ -126,46 +41,6 @@ function checkWebInstallStatus(opts) {
|
|
|
126
41
|
}
|
|
127
42
|
return 0;
|
|
128
43
|
}
|
|
129
|
-
function installLocalSkills() {
|
|
130
|
-
const pkgRoot = (0, pkg_root_js_1.findPkgRoot)(__dirname);
|
|
131
|
-
const skillsSrc = path_1.default.join(pkgRoot, 'skills', 'conductor');
|
|
132
|
-
if (!fs_1.default.existsSync(skillsSrc)) {
|
|
133
|
-
throw new Error(`No skills found at ${skillsSrc}`);
|
|
134
|
-
}
|
|
135
|
-
const skillsDest = path_1.default.join(process.cwd(), '.claude', 'skills', 'conductor');
|
|
136
|
-
copyDir(skillsSrc, skillsDest);
|
|
137
|
-
}
|
|
138
|
-
function installPlugin() {
|
|
139
|
-
const pkgRoot = (0, pkg_root_js_1.findPkgRoot)(__dirname);
|
|
140
|
-
const pkgJsonPath = path_1.default.join(pkgRoot, 'package.json');
|
|
141
|
-
const pkg = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, 'utf8'));
|
|
142
|
-
const version = pkg.version;
|
|
143
|
-
const pluginCacheDir = path_1.default.join(os_1.default.homedir(), '.claude', 'plugins', 'cache', 'conductor', 'conductor', version);
|
|
144
|
-
fs_1.default.mkdirSync(pluginCacheDir, { recursive: true });
|
|
145
|
-
const skillsSrc = path_1.default.join(pkgRoot, 'skills', 'conductor');
|
|
146
|
-
if (fs_1.default.existsSync(skillsSrc)) {
|
|
147
|
-
copyDir(skillsSrc, path_1.default.join(pluginCacheDir, 'skills', 'conductor'));
|
|
148
|
-
}
|
|
149
|
-
const pluginJsonSrc = path_1.default.join(pkgRoot, '.claude-plugin', 'plugin.json');
|
|
150
|
-
if (fs_1.default.existsSync(pluginJsonSrc)) {
|
|
151
|
-
const pluginMetaDir = path_1.default.join(pluginCacheDir, '.claude-plugin');
|
|
152
|
-
fs_1.default.mkdirSync(pluginMetaDir, { recursive: true });
|
|
153
|
-
fs_1.default.copyFileSync(pluginJsonSrc, path_1.default.join(pluginMetaDir, 'plugin.json'));
|
|
154
|
-
}
|
|
155
|
-
const installedPluginsPath = path_1.default.join(os_1.default.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
156
|
-
let installed = { plugins: [] };
|
|
157
|
-
if (fs_1.default.existsSync(installedPluginsPath)) {
|
|
158
|
-
installed = JSON.parse(fs_1.default.readFileSync(installedPluginsPath, 'utf8'));
|
|
159
|
-
if (!Array.isArray(installed.plugins)) {
|
|
160
|
-
installed.plugins = [];
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
installed.plugins = installed.plugins.filter((p) => p.name !== 'conductor');
|
|
164
|
-
installed.plugins.push({ name: 'conductor', version, path: pluginCacheDir });
|
|
165
|
-
fs_1.default.mkdirSync(path_1.default.dirname(installedPluginsPath), { recursive: true });
|
|
166
|
-
fs_1.default.writeFileSync(installedPluginsPath, JSON.stringify(installed, null, 2));
|
|
167
|
-
return version;
|
|
168
|
-
}
|
|
169
44
|
async function installWebBrowser(browserArg, opts) {
|
|
170
45
|
const validBrowsers = ['chromium', 'firefox', 'webkit'];
|
|
171
46
|
let browserName = 'chromium';
|
|
@@ -190,16 +65,3 @@ async function installWebBrowser(browserArg, opts) {
|
|
|
190
65
|
return 1;
|
|
191
66
|
}
|
|
192
67
|
}
|
|
193
|
-
function copyDir(src, dest) {
|
|
194
|
-
fs_1.default.mkdirSync(dest, { recursive: true });
|
|
195
|
-
for (const entry of fs_1.default.readdirSync(src, { withFileTypes: true })) {
|
|
196
|
-
const srcPath = path_1.default.join(src, entry.name);
|
|
197
|
-
const destPath = path_1.default.join(dest, entry.name);
|
|
198
|
-
if (entry.isDirectory()) {
|
|
199
|
-
copyDir(srcPath, destPath);
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
fs_1.default.copyFileSync(srcPath, destPath);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
package/dist/commands/logs.js
CHANGED
|
@@ -3,12 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.HELP = void 0;
|
|
4
4
|
exports.logs = logs;
|
|
5
5
|
exports.HELP = ` logs [--source <source>] [--level <level>] Stream app logs (console, Metro, or device)
|
|
6
|
-
--source <source>
|
|
6
|
+
--source <source> Filter by source: metro, device (default: both)
|
|
7
7
|
--level <level> Minimum level: verbose, debug, log, info, warn, error
|
|
8
|
-
--
|
|
9
|
-
--metro-port <port> Override Metro dev server port (skips auto-discovery)
|
|
10
|
-
--target <n> Metro debugger target index (when multiple devices share one Metro)
|
|
11
|
-
--list List available Metro debugger targets and exit
|
|
8
|
+
--list List Metro debugger targets for this device and exit
|
|
12
9
|
--recent <n> Return the last N buffered log entries and exit (agent-friendly)
|
|
13
10
|
--duration <seconds> Stream logs for N seconds, then exit
|
|
14
11
|
--json Output as NDJSON (one JSON object per line)`;
|
|
@@ -18,6 +15,7 @@ const android_js_1 = require("../drivers/android.js");
|
|
|
18
15
|
const web_js_1 = require("../drivers/web.js");
|
|
19
16
|
const types_js_1 = require("../drivers/log-sources/types.js");
|
|
20
17
|
const metro_js_1 = require("../drivers/log-sources/metro.js");
|
|
18
|
+
const metro_discovery_js_1 = require("../drivers/log-sources/metro-discovery.js");
|
|
21
19
|
const daemon_js_1 = require("../drivers/log-sources/daemon.js");
|
|
22
20
|
const client_js_1 = require("../daemon/client.js");
|
|
23
21
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
@@ -44,71 +42,100 @@ function formatEntry(entry, opts) {
|
|
|
44
42
|
}
|
|
45
43
|
return line;
|
|
46
44
|
}
|
|
47
|
-
async function
|
|
48
|
-
|
|
45
|
+
async function resolvePlatformAndDevice(sessionName) {
|
|
46
|
+
try {
|
|
47
|
+
const driver = await (0, runner_js_1.getDriver)(sessionName);
|
|
48
|
+
let platform = 'unknown';
|
|
49
|
+
if (driver instanceof ios_js_1.IOSDriver)
|
|
50
|
+
platform = driver.platform;
|
|
51
|
+
else if (driver instanceof android_js_1.AndroidDriver)
|
|
52
|
+
platform = 'android';
|
|
53
|
+
else if (driver instanceof web_js_1.WebDriver)
|
|
54
|
+
platform = 'web';
|
|
55
|
+
else
|
|
56
|
+
platform = await (0, bootstrap_js_1.detectPlatform)(sessionName);
|
|
57
|
+
return { platform, deviceId: sessionName };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function logs(opts = {}, sessionName = 'default', { source, level, list, recent, duration } = {}) {
|
|
64
|
+
const sourceFilter = source === 'metro' || source === 'device' ? source : undefined;
|
|
65
|
+
// --list: resolve the device's Metro port deterministically, then print its targets.
|
|
49
66
|
if (list) {
|
|
50
67
|
try {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (opts.json)
|
|
55
|
-
console.log(JSON.stringify({ status: '
|
|
56
|
-
|
|
68
|
+
const ctx = await resolvePlatformAndDevice(sessionName);
|
|
69
|
+
if (!ctx) {
|
|
70
|
+
const msg = 'Could not resolve device session for --list';
|
|
71
|
+
if (opts.json)
|
|
72
|
+
console.log(JSON.stringify({ status: 'error', message: msg }));
|
|
73
|
+
else
|
|
74
|
+
console.error(`✗ logs --list — ${msg}`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
const port = await (0, metro_discovery_js_1.discoverMetroPortForDevice)(ctx.platform, ctx.deviceId);
|
|
78
|
+
if (port === null) {
|
|
79
|
+
if (opts.json)
|
|
80
|
+
console.log(JSON.stringify({ status: 'ok', port: null, targets: [] }));
|
|
57
81
|
else {
|
|
58
|
-
console.log('No Metro
|
|
82
|
+
console.log('No Metro connection detected for this device. The daemon will keep trying — ' +
|
|
83
|
+
'launch the React Native app on this device and retry, or confirm the app is not RN.');
|
|
59
84
|
}
|
|
60
85
|
return 0;
|
|
61
86
|
}
|
|
87
|
+
const allTargets = await (0, metro_js_1.fetchTargets)(port, 'localhost');
|
|
88
|
+
const displayName = await (0, metro_discovery_js_1.getDeviceDisplayName)(ctx.platform, ctx.deviceId);
|
|
89
|
+
const deviceTargets = displayName ? (0, metro_discovery_js_1.targetsForDevice)(allTargets, displayName) : [];
|
|
62
90
|
if (opts.json) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
console.log(JSON.stringify({
|
|
92
|
+
status: 'ok',
|
|
93
|
+
port,
|
|
94
|
+
deviceName: displayName,
|
|
95
|
+
targets: deviceTargets.map((t, i) => ({
|
|
96
|
+
index: i,
|
|
97
|
+
title: t.title ?? null,
|
|
98
|
+
description: t.description ?? null,
|
|
99
|
+
deviceName: t.deviceName ?? null,
|
|
100
|
+
appId: t.appId ?? null,
|
|
101
|
+
logicalDeviceId: t.reactNative?.logicalDeviceId ?? null,
|
|
102
|
+
webSocketDebuggerUrl: t.webSocketDebuggerUrl ?? null,
|
|
103
|
+
})),
|
|
71
104
|
}));
|
|
72
|
-
console.log(JSON.stringify({ status: 'ok', targets: items }));
|
|
73
105
|
}
|
|
74
106
|
else {
|
|
75
|
-
console.log(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
console.log(`Metro on port ${port} (device: ${displayName ?? 'unknown'})`);
|
|
108
|
+
if (deviceTargets.length === 0) {
|
|
109
|
+
console.log(' No targets for this device.');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
for (let i = 0; i < deviceTargets.length; i++) {
|
|
113
|
+
const t = deviceTargets[i];
|
|
114
|
+
const desc = t.description ? ` — ${t.description}` : '';
|
|
115
|
+
console.log(` ${i}: ${t.title ?? '(unnamed)'}${desc}`);
|
|
116
|
+
}
|
|
81
117
|
}
|
|
82
118
|
}
|
|
83
119
|
return 0;
|
|
84
120
|
}
|
|
85
121
|
catch (err) {
|
|
86
122
|
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
-
if (opts.json)
|
|
123
|
+
if (opts.json)
|
|
88
124
|
console.log(JSON.stringify({ status: 'error', message: msg }));
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
console.error(`\u2717 logs --list \u2014 ${msg}`);
|
|
92
|
-
}
|
|
125
|
+
else
|
|
126
|
+
console.error(`✗ logs --list — ${msg}`);
|
|
93
127
|
return 1;
|
|
94
128
|
}
|
|
95
129
|
}
|
|
96
130
|
try {
|
|
97
131
|
// ── Snapshot mode (--recent N) ──────────────────────────────────────────
|
|
98
|
-
// Single fetch from the daemon's log buffer, print, and exit immediately.
|
|
99
|
-
// This is the primary agent-friendly mode.
|
|
100
132
|
if (recent !== undefined) {
|
|
101
|
-
// Ensure daemon is running (starts it if needed)
|
|
102
133
|
await (0, runner_js_1.getDriver)(sessionName);
|
|
103
134
|
const minSeverity = level ? (types_js_1.LEVEL_SEVERITY[level] ?? 0) : 0;
|
|
104
|
-
|
|
105
|
-
const metroOpt = metro ? (metroPort ?? 'auto') : undefined;
|
|
106
|
-
const entries = await (0, client_js_1.fetchDaemonLogs)(sessionName, {
|
|
107
|
-
limit: recent,
|
|
108
|
-
level,
|
|
109
|
-
metro: metroOpt,
|
|
110
|
-
});
|
|
135
|
+
const entries = await (0, client_js_1.fetchDaemonLogs)(sessionName, { limit: recent, level });
|
|
111
136
|
for (const entry of entries) {
|
|
137
|
+
if (sourceFilter && entry.source !== sourceFilter)
|
|
138
|
+
continue;
|
|
112
139
|
const entrySeverity = types_js_1.LEVEL_SEVERITY[entry.level] ?? 0;
|
|
113
140
|
if (entrySeverity < minSeverity)
|
|
114
141
|
continue;
|
|
@@ -116,41 +143,12 @@ async function logs(opts = {}, sessionName = 'default', { source = 'auto', level
|
|
|
116
143
|
}
|
|
117
144
|
return 0;
|
|
118
145
|
}
|
|
119
|
-
// ──
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
let _platform = 'unknown';
|
|
123
|
-
if (source !== 'metro') {
|
|
124
|
-
const driver = await (0, runner_js_1.getDriver)(sessionName);
|
|
125
|
-
if (driver instanceof ios_js_1.IOSDriver) {
|
|
126
|
-
_platform = driver.platform;
|
|
127
|
-
}
|
|
128
|
-
else if (driver instanceof android_js_1.AndroidDriver) {
|
|
129
|
-
_platform = 'android';
|
|
130
|
-
}
|
|
131
|
-
else if (driver instanceof web_js_1.WebDriver) {
|
|
132
|
-
_platform = 'web';
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
_platform = await (0, bootstrap_js_1.detectPlatform)(sessionName);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
146
|
+
// ── Streaming modes ─────────────────────────────────────────────────────
|
|
147
|
+
// Ensure daemon is running; its log collector auto-discovers Metro.
|
|
148
|
+
await (0, runner_js_1.getDriver)(sessionName);
|
|
138
149
|
const minSeverity = level ? (types_js_1.LEVEL_SEVERITY[level] ?? 0) : 0;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (source === 'metro') {
|
|
142
|
-
// Explicit --source metro: connect directly to Metro via CLI
|
|
143
|
-
logSource = new metro_js_1.MetroLogSource(metroPort ?? 8081, 'localhost', target);
|
|
144
|
-
await logSource.connect();
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
// Device logs via daemon. When --metro is set, pass the metro port
|
|
148
|
-
// (or 'auto' for auto-discovery) so the daemon finds Metro for this device.
|
|
149
|
-
const metroOpt = metro ? (metroPort ?? 'auto') : undefined;
|
|
150
|
-
logSource = new daemon_js_1.DaemonLogSource(sessionName, metroOpt);
|
|
151
|
-
await logSource.connect();
|
|
152
|
-
}
|
|
153
|
-
// Set up graceful shutdown
|
|
150
|
+
const logSource = new daemon_js_1.DaemonLogSource(sessionName);
|
|
151
|
+
await logSource.connect();
|
|
154
152
|
const cleanup = () => {
|
|
155
153
|
logSource.disconnect();
|
|
156
154
|
process.exit(0);
|
|
@@ -158,12 +156,13 @@ async function logs(opts = {}, sessionName = 'default', { source = 'auto', level
|
|
|
158
156
|
process.on('SIGINT', cleanup);
|
|
159
157
|
process.on('SIGTERM', cleanup);
|
|
160
158
|
logSource.onEntry((entry) => {
|
|
159
|
+
if (sourceFilter && entry.source !== sourceFilter)
|
|
160
|
+
return;
|
|
161
161
|
const entrySeverity = types_js_1.LEVEL_SEVERITY[entry.level] ?? 0;
|
|
162
162
|
if (entrySeverity < minSeverity)
|
|
163
163
|
return;
|
|
164
164
|
console.log(formatEntry(entry, opts));
|
|
165
165
|
});
|
|
166
|
-
// ── Duration mode (--duration N) ───────────────────────────────────────
|
|
167
166
|
if (duration !== undefined) {
|
|
168
167
|
await new Promise((resolve) => {
|
|
169
168
|
setTimeout(() => {
|
|
@@ -173,11 +172,8 @@ async function logs(opts = {}, sessionName = 'default', { source = 'auto', level
|
|
|
173
172
|
});
|
|
174
173
|
return 0;
|
|
175
174
|
}
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
await new Promise(() => {
|
|
179
|
-
// Never resolves — exits via SIGINT/SIGTERM
|
|
180
|
-
});
|
|
175
|
+
// Streaming — never resolves; exits via SIGINT/SIGTERM
|
|
176
|
+
await new Promise(() => { });
|
|
181
177
|
return 0;
|
|
182
178
|
}
|
|
183
179
|
catch (err) {
|
|
@@ -186,7 +182,7 @@ async function logs(opts = {}, sessionName = 'default', { source = 'auto', level
|
|
|
186
182
|
console.log(JSON.stringify({ status: 'error', message: msg }));
|
|
187
183
|
}
|
|
188
184
|
else {
|
|
189
|
-
console.error(
|
|
185
|
+
console.error(`✗ logs — ${msg}`);
|
|
190
186
|
}
|
|
191
187
|
return 1;
|
|
192
188
|
}
|
package/dist/daemon/client.js
CHANGED
|
@@ -205,9 +205,8 @@ async function findRunningWebSession(browserName) {
|
|
|
205
205
|
* Fetch buffered log entries from the daemon's /logs HTTP endpoint.
|
|
206
206
|
* Used by `conductor logs --recent` for snapshot access.
|
|
207
207
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* matching this device and merge JS console entries into the log buffer.
|
|
208
|
+
* The daemon always auto-discovers Metro for the device in this session and
|
|
209
|
+
* merges JS console entries (source='metro') alongside platform logs.
|
|
211
210
|
*/
|
|
212
211
|
async function fetchDaemonLogs(sessionName, opts = {}) {
|
|
213
212
|
const params = new URLSearchParams();
|
|
@@ -217,12 +216,6 @@ async function fetchDaemonLogs(sessionName, opts = {}) {
|
|
|
217
216
|
params.set('level', opts.level);
|
|
218
217
|
if (opts.limit)
|
|
219
218
|
params.set('limit', String(opts.limit));
|
|
220
|
-
if (opts.metro === 'auto') {
|
|
221
|
-
params.set('metro', '');
|
|
222
|
-
}
|
|
223
|
-
else if (opts.metro) {
|
|
224
|
-
params.set('metro', String(opts.metro));
|
|
225
|
-
}
|
|
226
219
|
const qs = params.toString();
|
|
227
220
|
const reqPath = qs ? `/logs?${qs}` : '/logs';
|
|
228
221
|
return new Promise((resolve, reject) => {
|