scenescout 1.0.0 → 1.2.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/CHANGELOG.md +32 -0
- package/README.md +186 -65
- package/dist/browsers.js +185 -0
- package/dist/cli.js +167 -39
- package/dist/clients.js +213 -0
- package/dist/code-routes.js +462 -0
- package/dist/engine/browser.js +88 -38
- package/dist/engine/collector.js +148 -7
- package/dist/engine/hover.js +42 -0
- package/dist/engine/launch.js +12 -5
- package/dist/engine/policy.js +55 -3
- package/dist/engine/probes.js +4 -1
- package/dist/engine/report.js +6 -1
- package/dist/installer.js +40 -12
- package/dist/mcp-server.js +61 -6
- package/dist/playbook.js +83 -0
- package/dist/scan.js +26 -2
- package/package.json +19 -7
- package/skills/scenescout/SKILL.md +11 -8
package/dist/browsers.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which browsers SceneScout can drive, which builds `scenescout install`
|
|
3
|
+
* downloads for them, and which build a given launch needs.
|
|
4
|
+
*
|
|
5
|
+
* Kept free of Playwright so the rules can be table-tested: the CLI and the
|
|
6
|
+
* engine pass in the paths Playwright reports and get decisions back.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
/** A browser the engine can launch. */
|
|
11
|
+
export const BROWSER_ENGINES = ["chromium", "firefox", "webkit"];
|
|
12
|
+
/**
|
|
13
|
+
* A build Playwright downloads. Chromium comes as two: the full browser, which
|
|
14
|
+
* a headed run opens, and the headless shell, which every headless run uses.
|
|
15
|
+
* Installing "chromium" brings both, which is what install has always done.
|
|
16
|
+
*/
|
|
17
|
+
export const INSTALL_TARGETS = ["chromium", "chromium-headless-shell", "firefox", "webkit"];
|
|
18
|
+
export const DEFAULT_ENGINE = "chromium";
|
|
19
|
+
/** Environment variable naming the browser an attach uses when it does not name one. */
|
|
20
|
+
export const DEFAULT_ENGINE_ENV = "SCENESCOUT_BROWSER";
|
|
21
|
+
/** Approximate size on disk, so the install output can say what it is about to fetch. */
|
|
22
|
+
export const APPROX_DISK_MB = {
|
|
23
|
+
chromium: 550,
|
|
24
|
+
"chromium-headless-shell": 200,
|
|
25
|
+
firefox: 270,
|
|
26
|
+
webkit: 290,
|
|
27
|
+
};
|
|
28
|
+
export function isBrowserEngine(value) {
|
|
29
|
+
return BROWSER_ENGINES.includes(value);
|
|
30
|
+
}
|
|
31
|
+
/** The engine an attach uses when the caller names none: the environment's choice, else Chromium. */
|
|
32
|
+
export function defaultEngine(env) {
|
|
33
|
+
const raw = env[DEFAULT_ENGINE_ENV]?.trim().toLowerCase();
|
|
34
|
+
if (!raw)
|
|
35
|
+
return DEFAULT_ENGINE;
|
|
36
|
+
if (!isBrowserEngine(raw)) {
|
|
37
|
+
throw new Error(`${DEFAULT_ENGINE_ENV}="${env[DEFAULT_ENGINE_ENV]}" is not a browser SceneScout can drive. Use one of: ${BROWSER_ENGINES.join(", ")}.`);
|
|
38
|
+
}
|
|
39
|
+
return raw;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read the value of `--browsers`. Absent means what install has always done.
|
|
43
|
+
* `all` is every engine; a comma-separated list picks several. "chromium"
|
|
44
|
+
* already includes the headless shell, so naming both is the same as naming it.
|
|
45
|
+
*/
|
|
46
|
+
export function parseBrowserSelection(value) {
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return { targets: ["chromium"] };
|
|
49
|
+
const names = value
|
|
50
|
+
.split(",")
|
|
51
|
+
.map((s) => s.trim().toLowerCase())
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
const choices = `${INSTALL_TARGETS.join(", ")}, all`;
|
|
54
|
+
if (names.length === 0)
|
|
55
|
+
return { error: `--browsers needs a value. Choose from: ${choices}.` };
|
|
56
|
+
const picked = new Set();
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
if (name === "all") {
|
|
59
|
+
for (const t of ["chromium", "firefox", "webkit"])
|
|
60
|
+
picked.add(t);
|
|
61
|
+
}
|
|
62
|
+
else if (INSTALL_TARGETS.includes(name)) {
|
|
63
|
+
picked.add(name);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
return { error: `"${name}" is not a browser SceneScout can install. Choose from: ${choices}.` };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (picked.has("chromium"))
|
|
70
|
+
picked.delete("chromium-headless-shell");
|
|
71
|
+
return { targets: INSTALL_TARGETS.filter((t) => picked.has(t)) };
|
|
72
|
+
}
|
|
73
|
+
/** The arguments for Playwright's own installer. Target names are Playwright's names. */
|
|
74
|
+
export function playwrightInstallArgs(targets) {
|
|
75
|
+
return ["install", ...targets];
|
|
76
|
+
}
|
|
77
|
+
/** The build a launch needs on disk. */
|
|
78
|
+
export function launchTarget(engine, headed) {
|
|
79
|
+
if (engine !== "chromium")
|
|
80
|
+
return engine;
|
|
81
|
+
return headed ? "chromium" : "chromium-headless-shell";
|
|
82
|
+
}
|
|
83
|
+
/** The engine each build belongs to. */
|
|
84
|
+
export function engineOf(target) {
|
|
85
|
+
return target === "chromium-headless-shell" ? "chromium" : target;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Where the headless shell lives, worked out from the full browser's path.
|
|
89
|
+
* Playwright exposes no path for the shell, but it keeps the two side by side
|
|
90
|
+
* under one revision: `chromium-1234/…` next to `chromium_headless_shell-1234/`.
|
|
91
|
+
* Null when the path does not have that shape.
|
|
92
|
+
*/
|
|
93
|
+
export function headlessShellDir(chromiumExecutable) {
|
|
94
|
+
if (!chromiumExecutable)
|
|
95
|
+
return null;
|
|
96
|
+
const parts = chromiumExecutable.split(/[\\/]/);
|
|
97
|
+
// The LAST such segment: a cache kept under a directory that happens to be
|
|
98
|
+
// named the same way must not be mistaken for the build directory.
|
|
99
|
+
let at = -1;
|
|
100
|
+
for (let i = parts.length - 1; i >= 0 && at < 0; i--)
|
|
101
|
+
if (/^chromium-\d+$/.test(parts[i]))
|
|
102
|
+
at = i;
|
|
103
|
+
if (at < 0)
|
|
104
|
+
return null;
|
|
105
|
+
const sep = chromiumExecutable.includes("\\") && !chromiumExecutable.includes("/") ? "\\" : "/";
|
|
106
|
+
return [...parts.slice(0, at), parts[at].replace(/^chromium-/, "chromium_headless_shell-")].join(sep);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Which builds are on disk. `executables` is what Playwright reports for each
|
|
110
|
+
* engine; a path Playwright names is not proof the file is there. The shell is
|
|
111
|
+
* counted only once Playwright has marked its download complete.
|
|
112
|
+
*/
|
|
113
|
+
export function browserPresence(executables, exists = fs.existsSync) {
|
|
114
|
+
const at = (p) => ({ installed: !!p && exists(p), path: p });
|
|
115
|
+
const shellDir = headlessShellDir(executables.chromium);
|
|
116
|
+
const shellMarker = shellDir ? path.join(shellDir, "INSTALLATION_COMPLETE") : null;
|
|
117
|
+
const chromium = at(executables.chromium);
|
|
118
|
+
return {
|
|
119
|
+
chromium: { installed: chromium.installed && !!shellMarker && exists(shellMarker), path: chromium.path },
|
|
120
|
+
"chromium-headless-shell": { installed: !!shellMarker && exists(shellMarker), path: shellDir },
|
|
121
|
+
firefox: at(executables.firefox),
|
|
122
|
+
webkit: at(executables.webkit),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** The command that downloads one build, for the way this copy of SceneScout was installed. */
|
|
126
|
+
export function installCommandFor(target, fromCheckout) {
|
|
127
|
+
const flags = `--browser-only --browsers ${target}`;
|
|
128
|
+
return fromCheckout ? `node dist/cli.js install ${flags}` : `npx -y scenescout install ${flags}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Whether pages may register service workers in this browser.
|
|
132
|
+
*
|
|
133
|
+
* The write policy works by intercepting requests. Only Chromium lets the
|
|
134
|
+
* driver intercept a request a service worker issues; in Firefox and WebKit
|
|
135
|
+
* such a request goes straight to the network, so an app that syncs its writes
|
|
136
|
+
* from a worker would send a DELETE through read-only mode with nothing
|
|
137
|
+
* logged. Blocking registration there makes the page send those requests
|
|
138
|
+
* itself, where the policy sees them.
|
|
139
|
+
*/
|
|
140
|
+
export function serviceWorkerPolicy(engine) {
|
|
141
|
+
return engine === "chromium" ? "allow" : "block";
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The key that moves keyboard focus to the next control, links and buttons
|
|
145
|
+
* included. WebKit on macOS follows Safari: plain Tab stops only at text
|
|
146
|
+
* fields, and Option+Tab stops everywhere. A focus audit that pressed Tab
|
|
147
|
+
* there would walk past every button and report nothing.
|
|
148
|
+
*/
|
|
149
|
+
export function focusAdvanceKey(engine, platform) {
|
|
150
|
+
return engine === "webkit" && platform === "darwin" ? "Alt+Tab" : "Tab";
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Run in every page before its own scripts: takes shared workers away.
|
|
154
|
+
*
|
|
155
|
+
* A request issued by a shared worker cannot be intercepted in any browser, so
|
|
156
|
+
* a write sent from one passes the policy unseen and unlogged. Removing the
|
|
157
|
+
* constructor makes feature detection fail, and an app then does that work on
|
|
158
|
+
* the page, where the policy sees it. Not applied in `destructive` mode, where
|
|
159
|
+
* the policy blocks nothing and the person has opted in to everything.
|
|
160
|
+
*/
|
|
161
|
+
export const REMOVE_SHARED_WORKER_SCRIPT = `(() => {
|
|
162
|
+
try { delete globalThis.SharedWorker; } catch {}
|
|
163
|
+
if ("SharedWorker" in globalThis) {
|
|
164
|
+
try { Object.defineProperty(globalThis, "SharedWorker", { value: undefined, configurable: true, writable: true }); } catch {}
|
|
165
|
+
}
|
|
166
|
+
})()`;
|
|
167
|
+
/** Whether pages may use shared workers in this write mode. */
|
|
168
|
+
export function sharedWorkersAllowed(mode) {
|
|
169
|
+
return mode === "destructive";
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* What install says when the browsers it was asked for do not include the one
|
|
173
|
+
* a default attach launches, and that one is not on disk either. Without it,
|
|
174
|
+
* `install --browsers firefox` on a fresh machine ends in "ready" and the first
|
|
175
|
+
* attach fails. Null when there is nothing to say.
|
|
176
|
+
*/
|
|
177
|
+
export function defaultAttachNote(opts) {
|
|
178
|
+
if (opts.defaultInstalled || opts.selected.length === 0)
|
|
179
|
+
return null;
|
|
180
|
+
if (opts.selected.some((t) => engineOf(t) === opts.defaultEngine))
|
|
181
|
+
return null;
|
|
182
|
+
const engine = engineOf(opts.selected[0]);
|
|
183
|
+
return (`an attach drives ${opts.defaultEngine} unless told otherwise, and that build is not installed. ` +
|
|
184
|
+
`Pass browser: "${engine}" when attaching, or set ${DEFAULT_ENGINE_ENV}=${engine} in the MCP server's environment.`);
|
|
185
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* scenescout scan <projectPath> Print project discovery results
|
|
6
6
|
* scenescout serve Run the MCP server on stdio
|
|
7
|
-
* scenescout install Install the skill, download
|
|
7
|
+
* scenescout install Install the skill, download the browser, register the MCP server
|
|
8
8
|
* scenescout doctor Check every piece of the setup and say how to fix what is missing
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
@@ -13,6 +13,8 @@ import { createRequire } from "node:module";
|
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { APPROX_DISK_MB, BROWSER_ENGINES, browserPresence, defaultAttachNote, defaultEngine, launchTarget, parseBrowserSelection, playwrightInstallArgs, } from "./browsers.js";
|
|
17
|
+
import { CLIENT_LABELS, firstMessageHint, manualFor, parseClients, registerWithClient, vscodeBinary } from "./clients.js";
|
|
16
18
|
import { diagnose, installSkill, launchCommand, manualRegisterCommand, registerMcp, resolveClaudeDir, spawnRunner } from "./installer.js";
|
|
17
19
|
import { LEGACY_MEMORY_DIRNAME, MEMORY_DIRNAME } from "./engine/memory.js";
|
|
18
20
|
import { formatScan, scanProject } from "./scan.js";
|
|
@@ -26,7 +28,12 @@ Usage:
|
|
|
26
28
|
scenescout serve Run the MCP server (stdio)
|
|
27
29
|
scenescout install One-step setup: skill + Chromium + MCP registration
|
|
28
30
|
(--skip-browser, --no-register to opt out of a step;
|
|
29
|
-
--browser-only when the skill and server came from a plugin
|
|
31
|
+
--browser-only when the skill and server came from a plugin;
|
|
32
|
+
--browsers <list> to choose what to download: chromium (default),
|
|
33
|
+
chromium-headless-shell, firefox, webkit, all — comma-separated)
|
|
34
|
+
(--client <list> to set up another MCP client instead of, or as well as,
|
|
35
|
+
Claude Code: claude-code (default), cursor, vscode, codex, gemini,
|
|
36
|
+
copilot, windsurf — comma-separated)
|
|
30
37
|
scenescout doctor Check the setup and print the fix for anything missing
|
|
31
38
|
(--engine: only node, the build and the browser — for plugin
|
|
32
39
|
installs and other MCP clients)
|
|
@@ -109,23 +116,83 @@ function status(projectPath) {
|
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
}
|
|
112
|
-
/**
|
|
113
|
-
async function
|
|
119
|
+
/** Which browser builds are on disk, going by the paths Playwright reports for the version we depend on. */
|
|
120
|
+
async function presentBrowsers() {
|
|
121
|
+
const executables = { chromium: null, firefox: null, webkit: null };
|
|
114
122
|
try {
|
|
115
|
-
const
|
|
116
|
-
|
|
123
|
+
const playwright = await import("playwright");
|
|
124
|
+
for (const name of BROWSER_ENGINES)
|
|
125
|
+
executables[name] = playwright[name].executablePath() || null;
|
|
117
126
|
}
|
|
118
127
|
catch {
|
|
119
|
-
|
|
128
|
+
// Playwright cannot be loaded: every build reads as absent, which is what doctor should say.
|
|
120
129
|
}
|
|
130
|
+
return browserPresence(executables);
|
|
121
131
|
}
|
|
122
|
-
/** Download
|
|
123
|
-
function
|
|
132
|
+
/** Download browser builds through the playwright CLI that ships with our own dependency. */
|
|
133
|
+
function downloadBrowsers(targets) {
|
|
124
134
|
const require = createRequire(import.meta.url);
|
|
125
135
|
const cli = path.join(path.dirname(require.resolve("playwright/package.json")), "cli.js");
|
|
126
|
-
const r = spawnSync(process.execPath, [cli,
|
|
136
|
+
const r = spawnSync(process.execPath, [cli, ...playwrightInstallArgs(targets)], { stdio: "inherit" });
|
|
127
137
|
return r.status === 0;
|
|
128
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* The value of a flag written as `--name x` or `--name=x`. Undefined when the
|
|
141
|
+
* flag is absent; empty when it was given no value, which includes being
|
|
142
|
+
* followed by another flag.
|
|
143
|
+
*/
|
|
144
|
+
function flagValue(flags, name) {
|
|
145
|
+
const inline = flags.find((f) => f.startsWith(`${name}=`));
|
|
146
|
+
if (inline)
|
|
147
|
+
return inline.slice(name.length + 1);
|
|
148
|
+
const at = flags.indexOf(name);
|
|
149
|
+
if (at < 0)
|
|
150
|
+
return undefined;
|
|
151
|
+
const next = flags[at + 1];
|
|
152
|
+
return next === undefined || next.startsWith("--") ? "" : next;
|
|
153
|
+
}
|
|
154
|
+
function browsersFlag(flags) {
|
|
155
|
+
// `--browser-only` is a different flag; a bare `--browser` is a slip that would otherwise be ignored and download Chromium.
|
|
156
|
+
const slip = flags.find((f) => f === "--browser" || f.startsWith("--browser="));
|
|
157
|
+
if (slip)
|
|
158
|
+
throw new Error(`unknown flag ${slip.split("=")[0]} — did you mean --browsers?`);
|
|
159
|
+
return flagValue(flags, "--browsers");
|
|
160
|
+
}
|
|
161
|
+
/** The `code` command on PATH, with its real path: the real path is what tells VS Code from a fork. */
|
|
162
|
+
function codeOnPath() {
|
|
163
|
+
const names = process.platform === "win32" ? ["code.cmd", "code.exe"] : ["code"];
|
|
164
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
|
|
165
|
+
for (const name of names) {
|
|
166
|
+
const candidate = path.join(dir, name);
|
|
167
|
+
try {
|
|
168
|
+
if (fs.existsSync(candidate))
|
|
169
|
+
return { command: candidate, realPath: fs.realpathSync(candidate) };
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// An unreadable PATH entry is not a VS Code install.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
/** Every value given for a flag, across repeats and both spellings, joined the way one comma-separated value would be. */
|
|
179
|
+
function flagValues(flags, names) {
|
|
180
|
+
const values = [];
|
|
181
|
+
flags.forEach((f, i) => {
|
|
182
|
+
for (const name of names) {
|
|
183
|
+
if (f.startsWith(`${name}=`))
|
|
184
|
+
values.push(f.slice(name.length + 1));
|
|
185
|
+
else if (f === name) {
|
|
186
|
+
const next = flags[i + 1];
|
|
187
|
+
values.push(next === undefined || next.startsWith("--") ? "" : next);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
if (values.length === 0)
|
|
192
|
+
return undefined;
|
|
193
|
+
// One occurrence without a value is a mistake even when another has one.
|
|
194
|
+
return values.some((v) => v.trim() === "") ? "" : values.join(",");
|
|
195
|
+
}
|
|
129
196
|
async function install(flags) {
|
|
130
197
|
const serverPath = path.join(packageRoot, "dist", "mcp-server.js");
|
|
131
198
|
if (!fs.existsSync(serverPath)) {
|
|
@@ -137,7 +204,17 @@ async function install(flags) {
|
|
|
137
204
|
// A plugin install already brings the skill and the server registration; the
|
|
138
205
|
// only thing it cannot bring is the browser download.
|
|
139
206
|
const browserOnly = flags.includes("--browser-only");
|
|
140
|
-
|
|
207
|
+
// Read the choice before doing anything, so a typo costs nothing.
|
|
208
|
+
const selection = parseBrowserSelection(browsersFlag(flags));
|
|
209
|
+
if ("error" in selection)
|
|
210
|
+
throw new Error(selection.error);
|
|
211
|
+
const chosen = parseClients(flagValues(flags, ["--client", "--clients"]));
|
|
212
|
+
if ("error" in chosen)
|
|
213
|
+
throw new Error(chosen.error);
|
|
214
|
+
const forClaude = chosen.clients.includes("claude-code");
|
|
215
|
+
const others = chosen.clients.filter((c) => c !== "claude-code");
|
|
216
|
+
// The skill is Claude Code's way of receiving the method; every other client gets it from the server.
|
|
217
|
+
if (!browserOnly && forClaude) {
|
|
141
218
|
const skill = installSkill({ packageRoot, claudeDir: resolveClaudeDir(process.env, os.homedir()) });
|
|
142
219
|
for (const note of skill.notes)
|
|
143
220
|
console.log(`· ${note}`);
|
|
@@ -149,45 +226,85 @@ async function install(flags) {
|
|
|
149
226
|
console.log("· Browser download skipped (--skip-browser).");
|
|
150
227
|
}
|
|
151
228
|
else {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
229
|
+
const present = await presentBrowsers();
|
|
230
|
+
const missing = selection.targets.filter((t) => !present[t].installed);
|
|
231
|
+
for (const t of selection.targets)
|
|
232
|
+
if (present[t].installed)
|
|
233
|
+
console.log(`✓ ${t} already present: ${present[t].path}`);
|
|
234
|
+
if (missing.length > 0) {
|
|
235
|
+
const size = missing.reduce((sum, t) => sum + APPROX_DISK_MB[t], 0);
|
|
236
|
+
console.log(`· Downloading ${missing.join(", ")} (one-time, about ${size} MB on disk)…`);
|
|
237
|
+
if (downloadBrowsers(missing))
|
|
238
|
+
console.log(`✓ Downloaded: ${missing.join(", ")}.`);
|
|
160
239
|
else {
|
|
161
240
|
failed = true;
|
|
162
|
-
console.log(
|
|
241
|
+
console.log(`✗ Browser download failed — run \`npx playwright install ${missing.join(" ")}\` and check your network/proxy.`);
|
|
163
242
|
}
|
|
164
243
|
}
|
|
165
244
|
}
|
|
245
|
+
// Downloading a browser the server will not launch leaves the first attach failing with no hint why.
|
|
246
|
+
const engine = defaultEngine(process.env);
|
|
247
|
+
const note = defaultAttachNote({
|
|
248
|
+
selected: flags.includes("--skip-browser") ? [] : selection.targets,
|
|
249
|
+
defaultEngine: engine,
|
|
250
|
+
defaultInstalled: (await presentBrowsers())[launchTarget(engine, false)].installed,
|
|
251
|
+
});
|
|
252
|
+
if (note)
|
|
253
|
+
console.log(`· Note: ${note}`);
|
|
254
|
+
const launch = launchCommand({ packageRoot, nodePath: process.execPath, serverPath });
|
|
166
255
|
if (browserOnly) {
|
|
167
256
|
// nothing to register
|
|
168
257
|
}
|
|
169
258
|
else if (flags.includes("--no-register")) {
|
|
170
|
-
console.log(
|
|
259
|
+
console.log("· MCP registration skipped (--no-register). To do it by hand:\n");
|
|
260
|
+
if (forClaude)
|
|
261
|
+
console.log(` ${manualRegisterCommand(launch)}`);
|
|
262
|
+
for (const client of others)
|
|
263
|
+
console.log(` ${CLIENT_LABELS[client]}: ${manualFor(client, launch, os.homedir())}`);
|
|
264
|
+
console.log("");
|
|
171
265
|
}
|
|
172
266
|
else {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
267
|
+
if (forClaude) {
|
|
268
|
+
const reg = registerMcp({ launch, serverPath, run: spawnRunner });
|
|
269
|
+
if (reg.status === "registered") {
|
|
270
|
+
console.log(`✓ MCP server ${reg.replaced ? "re-registered (paths refreshed)" : "registered"} with Claude Code at user scope.`);
|
|
271
|
+
for (const name of reg.removedLegacy)
|
|
272
|
+
console.log(`· removed the pre-rename MCP registration "${name}" (it pointed at this same server).`);
|
|
273
|
+
for (const note of reg.notes)
|
|
274
|
+
console.log(`· ${note}`);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
failed = true;
|
|
278
|
+
console.log(reg.status === "claude-missing"
|
|
279
|
+
? "· `claude` is not on this shell's PATH, so the MCP server was not registered."
|
|
280
|
+
: `✗ \`claude mcp add\` failed: ${reg.detail}`);
|
|
281
|
+
console.log(` Run this once from a terminal where \`claude\` works:\n\n ${reg.manual}\n`);
|
|
282
|
+
}
|
|
180
283
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
284
|
+
const vscode = others.includes("vscode")
|
|
285
|
+
? vscodeBinary({ platform: process.platform, home: os.homedir(), exists: fs.existsSync, codeOnPath: codeOnPath() })
|
|
286
|
+
: null;
|
|
287
|
+
for (const client of others) {
|
|
288
|
+
const reg = registerWithClient(client, { launch, home: os.homedir(), run: spawnRunner, vscode });
|
|
289
|
+
const label = CLIENT_LABELS[client];
|
|
290
|
+
if (reg.status === "registered") {
|
|
291
|
+
console.log(`✓ MCP server ${reg.replaced ? "re-registered" : "registered"} with ${label} (${reg.where}).`);
|
|
292
|
+
for (const note of reg.notes)
|
|
293
|
+
console.log(`· ${note}`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
failed = true;
|
|
297
|
+
// On Windows a client installed through npm is a .cmd shim, which node cannot start directly.
|
|
298
|
+
const windowsNote = process.platform === "win32" ? " (or it is installed as a .cmd shim, which cannot be started from here)" : "";
|
|
299
|
+
console.log(reg.status === "client-missing"
|
|
300
|
+
? `· ${label} was not found on this machine${windowsNote}, so nothing was registered with it.`
|
|
301
|
+
: `✗ Registering with ${label} failed: ${reg.detail}`);
|
|
302
|
+
console.log(` To do it by hand, ${reg.manual}\n`);
|
|
303
|
+
}
|
|
187
304
|
}
|
|
188
305
|
}
|
|
189
306
|
if (failed) {
|
|
190
|
-
console.log(
|
|
307
|
+
console.log(`\nSetup is incomplete — fix the lines marked ✗ or · above, then run: scenescout doctor${forClaude ? "" : " --engine"}`);
|
|
191
308
|
process.exitCode = 1;
|
|
192
309
|
return;
|
|
193
310
|
}
|
|
@@ -195,8 +312,12 @@ async function install(flags) {
|
|
|
195
312
|
console.log("\nThe browser is ready — attach again.");
|
|
196
313
|
return;
|
|
197
314
|
}
|
|
198
|
-
|
|
199
|
-
|
|
315
|
+
if (forClaude)
|
|
316
|
+
console.log("\nStart a FRESH Claude Code session, then in any project run: /scenescout");
|
|
317
|
+
// Telling someone to restart a client nothing was registered with sends them looking for a server that is not there.
|
|
318
|
+
if (others.length > 0 && !flags.includes("--no-register"))
|
|
319
|
+
console.log(`\n${firstMessageHint(others)}`);
|
|
320
|
+
console.log(`Something off? Run: scenescout doctor${forClaude ? "" : " --engine"}`);
|
|
200
321
|
}
|
|
201
322
|
async function doctor(flags) {
|
|
202
323
|
const checks = diagnose({
|
|
@@ -204,7 +325,12 @@ async function doctor(flags) {
|
|
|
204
325
|
packageRoot,
|
|
205
326
|
claudeDir: resolveClaudeDir(process.env, os.homedir()),
|
|
206
327
|
nodeVersion: process.version,
|
|
207
|
-
|
|
328
|
+
// What a default attach launches: the headless build of the default browser.
|
|
329
|
+
defaultBrowser: await (async () => {
|
|
330
|
+
const target = launchTarget(defaultEngine(process.env), false);
|
|
331
|
+
const found = (await presentBrowsers())[target];
|
|
332
|
+
return { target, path: found.installed ? found.path : null, expected: found.path };
|
|
333
|
+
})(),
|
|
208
334
|
run: spawnRunner,
|
|
209
335
|
});
|
|
210
336
|
for (const c of checks) {
|
|
@@ -214,7 +340,9 @@ async function doctor(flags) {
|
|
|
214
340
|
}
|
|
215
341
|
if (checks.some((c) => !c.ok))
|
|
216
342
|
process.exit(1);
|
|
217
|
-
console.log(
|
|
343
|
+
console.log(flags.includes("--engine")
|
|
344
|
+
? "\nAll good. Ask your agent: Use SceneScout to test http://localhost:3000"
|
|
345
|
+
: "\nAll good. In any project, run: /scenescout (or ask: Use SceneScout to test http://localhost:3000)");
|
|
218
346
|
}
|
|
219
347
|
const [, , command, ...args] = process.argv;
|
|
220
348
|
// A CLI's failure mode should be a sentence, not a stack trace. `scan` on a
|