scenescout 3.15.0 → 3.17.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 +87 -0
- package/README.md +70 -18
- package/dist/browsers.js +28 -0
- package/dist/check-run.js +191 -14
- package/dist/ci-run.js +268 -52
- package/dist/cli.js +107 -47
- package/dist/commands.js +3 -2
- package/dist/engine/baseline.js +377 -0
- package/dist/engine/brief.js +16 -7
- package/dist/engine/browser.js +1147 -286
- package/dist/engine/calibration.js +61 -30
- package/dist/engine/capture.js +164 -0
- package/dist/engine/check.js +244 -42
- package/dist/engine/ci-lanes.js +215 -0
- package/dist/engine/ci.js +136 -18
- package/dist/engine/claims.js +159 -3
- package/dist/engine/collector.js +561 -30
- package/dist/engine/crawl.js +49 -0
- package/dist/engine/design.js +281 -38
- package/dist/engine/export.js +877 -0
- package/dist/engine/fingerprint.js +92 -4
- package/dist/engine/flow.js +18 -6
- package/dist/engine/forms.js +181 -18
- package/dist/engine/journey.js +29 -1
- package/dist/engine/lane.js +13 -3
- package/dist/engine/launch.js +45 -6
- package/dist/engine/limits.js +7 -0
- package/dist/engine/live-page.js +49 -2
- package/dist/engine/live.js +4 -1
- package/dist/engine/memory.js +501 -47
- package/dist/engine/open.js +118 -0
- package/dist/engine/oracles.js +41 -1
- package/dist/engine/plain.js +268 -0
- package/dist/engine/png.js +127 -0
- package/dist/engine/policy.js +379 -9
- package/dist/engine/probes.js +3 -2
- package/dist/engine/profiles.js +45 -9
- package/dist/engine/project-folder.js +191 -0
- package/dist/engine/refresh.js +68 -3
- package/dist/engine/replay.js +63 -10
- package/dist/engine/report.js +241 -40
- package/dist/engine/request.js +317 -23
- package/dist/engine/sarif.js +120 -0
- package/dist/engine/settle.js +67 -0
- package/dist/engine/signed-in.js +256 -0
- package/dist/engine/status-pane-page.js +441 -0
- package/dist/engine/status-pane.js +128 -0
- package/dist/engine/tickets.js +671 -0
- package/dist/engine/unload.js +3 -2
- package/dist/export-run.js +633 -0
- package/dist/first-run.js +5 -0
- package/dist/installer.js +378 -8
- package/dist/intake.js +104 -0
- package/dist/login-run.js +250 -36
- package/dist/mcp-server.js +660 -65
- package/dist/playbook.js +5 -0
- package/dist/prompts.js +106 -0
- package/package.json +8 -5
- package/skills/scenescout/SKILL.md +49 -16
package/dist/cli.js
CHANGED
|
@@ -10,25 +10,30 @@
|
|
|
10
10
|
* scenescout check <url> Visit every route, measure it, and pass or fail (no model involved)
|
|
11
11
|
* scenescout ci <url> An exploratory run driven by a model's API, unattended, that reports
|
|
12
12
|
* scenescout login <url> --role r Sign in once in a visible browser and save it as a named role
|
|
13
|
+
* scenescout export --to github File the project's findings as GitHub or Jira issues, each once
|
|
13
14
|
*/
|
|
14
15
|
import { spawnSync } from "node:child_process";
|
|
15
16
|
import fs from "node:fs";
|
|
16
|
-
import { createRequire } from "node:module";
|
|
17
17
|
import os from "node:os";
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { APPROX_DISK_MB,
|
|
20
|
+
import { APPROX_DISK_MB, defaultAttachNote, defaultEngine, launchTarget, parseBrowserSelection } from "./browsers.js";
|
|
21
21
|
import { CLIENT_LABELS, firstMessageHint, manualFor, parseClients, registerWithClient, vscodeBinary } from "./clients.js";
|
|
22
|
-
import { CLI_NAME, diagnose, ensureCommand, findOnUserPath, installSkill, isEphemeralRoot, launchCommand, manualRegisterCommand, planCommand, registerMcp, resolveClaudeDir, spawnRunner, } from "./installer.js";
|
|
23
|
-
import {
|
|
22
|
+
import { CLAUDE_CODE_NOT_NEEDED, CLI_NAME, desktopExtensionRoots, diagnose, doctorAllGood, findDesktopExtension, installClosing, ensureCommand, findOnUserPath, installSkill, isEphemeralRoot, launchCommand, manualRegisterCommand, planCommand, registerMcp, resolveClaudeDir, spawnRunner, } from "./installer.js";
|
|
23
|
+
import { downloadBrowsers, presentBrowsers } from "./installer.js";
|
|
24
|
+
import { baselinesDirOf, defaultCheckDir, readCheckInputs, runCheck } from "./check-run.js";
|
|
24
25
|
import { httpClient, httpJudgeAsk, runCi } from "./ci-run.js";
|
|
26
|
+
import { runExport } from "./export-run.js";
|
|
25
27
|
import { runLogin, runScriptedLogin, savedLine } from "./login-run.js";
|
|
26
28
|
import { credentialRedactor, LOGIN_ENV, readScriptedLogin } from "./engine/scripted-login.js";
|
|
27
29
|
import { parseLoginArgs } from "./engine/profiles.js";
|
|
28
30
|
import { detectProvider, EXIT_CI, judgeEffort, KEY_ENV, parseCiArgs, redactKeys, secretValues } from "./engine/ci.js";
|
|
31
|
+
import { VISUAL_DIRNAME } from "./engine/baseline.js";
|
|
29
32
|
import { EXIT, exitCodeOf, formatCheck, parseCheckArgs, refusedFlowReason, toSarif, toSummaryJson, unmeasuredReason, } from "./engine/check.js";
|
|
30
33
|
import { downloadLine, EXIT_FIRST_RUN, FIRST_RUN_DIRNAME, firstRunCheckOptions, firstRunDownloads, firstRunSummary, formatFirstRun, modeSentence, parseFirstRunArgs, reportFolderProblem, unreachableReason, writeFirstRunReport, } from "./first-run.js";
|
|
34
|
+
import { credentialSecrets, EXIT_EXPORT, parseExportArgs } from "./engine/export.js";
|
|
31
35
|
import { LEGACY_MEMORY_DIRNAME, MEMORY_DIRNAME, writeSelfIgnore } from "./engine/memory.js";
|
|
36
|
+
import { sarifFilesFor } from "./engine/sarif.js";
|
|
32
37
|
import { formatStatus, liveEngines, liveTokenFileName, localClock, LIVE_TOKEN_FILE, pidAlive, watchTarget, wholeSessions, } from "./engine/live.js";
|
|
33
38
|
import { formatScan, scanProject } from "./scan.js";
|
|
34
39
|
import { dispatch } from "./commands.js";
|
|
@@ -73,7 +78,8 @@ Usage:
|
|
|
73
78
|
so it can gate a pull request. Writes report.md, check.sarif and check.json.
|
|
74
79
|
(--fail-on high|medium|low|never (default high); --mode observe|read-only;
|
|
75
80
|
--max-routes N (default 50); --paths /a,/b to check only those;
|
|
76
|
-
--ignore rule,rule; --
|
|
81
|
+
--ignore rule,rule; --ignore-path /a,rule:/b to exempt a path, or one rule on it;
|
|
82
|
+
--storage-state file to check signed in;
|
|
77
83
|
--project dir (default: here); --out dir (default: .scenescout/check);
|
|
78
84
|
--browser chromium|firefox|webkit;
|
|
79
85
|
--action-timeout-ms N (default 5000), --nav-timeout-ms N (default 20000;
|
|
@@ -87,7 +93,19 @@ Usage:
|
|
|
87
93
|
--on-refused-step report|stop: report (default) marks a flow whose step was
|
|
88
94
|
refused "could not run", keeps every other verdict and exits 2; stop exits 2 there;
|
|
89
95
|
--gate-retests never|high|all: which still-reproducing findings fail the gate
|
|
90
|
-
(default high: those filed high)
|
|
96
|
+
(default high: those filed high);
|
|
97
|
+
--baseline off|compare|update: compare each page or element listed in the
|
|
98
|
+
baselines' targets.json with its baseline picture, or write new baselines
|
|
99
|
+
(update; only when asked) (default off);
|
|
100
|
+
--baselines dir: where targets.json and the baselines are (default
|
|
101
|
+
.scenescout/baselines, which git ignores; name a folder you commit to share
|
|
102
|
+
them); --baseline-threshold N: the % of a picture's pixels that may change
|
|
103
|
+
before its baseline is not met; update rewrites those past it, and any taken
|
|
104
|
+
on another OS (default 0.1, so small anti-aliasing noise between machines
|
|
105
|
+
passes; 0 counts every changed pixel);
|
|
106
|
+
--sarif-file-anchor path: the repository file a SARIF result points at when
|
|
107
|
+
no saved flow raised it (default: the running workflow's file on GitHub
|
|
108
|
+
Actions, else package.json, else README.md))
|
|
91
109
|
Exit code: 0 passed, 1 failed the gate, 2 could not run.
|
|
92
110
|
scenescout ci <url> An exploratory run with no person present: a model reached through its API
|
|
93
111
|
drives the tools by the SceneScout method and the run ends in the report.
|
|
@@ -100,6 +118,8 @@ Usage:
|
|
|
100
118
|
--max-turns N (default 40); --max-tokens N (default 1500000);
|
|
101
119
|
--max-minutes N (default 20): the run stops at the first cap reached and still
|
|
102
120
|
writes the report;
|
|
121
|
+
--lanes N (default 1, at most 8): split the app between N model loops that
|
|
122
|
+
explore at once, each in its own browser, sharing those caps;
|
|
103
123
|
--price-in, --price-cached-in, --price-out: US dollars per million tokens,
|
|
104
124
|
over the built-in prices, for the cost estimate of any model;
|
|
105
125
|
--mode observe|read-only|safe-write|destructive (default read-only;
|
|
@@ -114,15 +134,22 @@ Usage:
|
|
|
114
134
|
--dedup judge|rule: judge (default) also asks the run's model, at its lowest
|
|
115
135
|
effort, whether a filed finding the rule keeps apart is one already on its
|
|
116
136
|
page (titles, categories, evidence and the page's path are sent);
|
|
117
|
-
rule asks nothing
|
|
137
|
+
rule asks nothing;
|
|
138
|
+
--sarif-file-anchor path: the repository file each SARIF result points at,
|
|
139
|
+
as for check)
|
|
118
140
|
Exit code: 0 the run ran (findings never change it), 2 could not run.
|
|
119
141
|
scenescout login <url> --role <name>
|
|
120
|
-
Open a visible browser at the URL
|
|
121
|
-
|
|
122
|
-
.scenescout/auth/<name>.json (owner-only; never printed, never
|
|
123
|
-
|
|
124
|
-
scout_attach { role: "<name>" },
|
|
125
|
-
|
|
142
|
+
Open a visible browser at the URL and sign in there (SSO, MFA, anything). Once
|
|
143
|
+
you are back on the app with a new session, the window saves it as that role's
|
|
144
|
+
profile, in .scenescout/auth/<name>.json (owner-only; never printed, never
|
|
145
|
+
committed), and closes. Enter in this terminal saves at once. Closing the window
|
|
146
|
+
or Ctrl+C saves nothing. Agents then attach with scout_attach { role: "<name>" },
|
|
147
|
+
as many sessions as they like from one login. From a conversation, scout_login
|
|
148
|
+
opens the same window.
|
|
149
|
+
(--project dir (default: here); --browser chromium|firefox|webkit;
|
|
150
|
+
--save auto|enter (default auto; enter: save on Enter only, as before);
|
|
151
|
+
--success-url text|url: signed in once the URL's path contains this, or the URL
|
|
152
|
+
starts with it, instead of when a new session appears)
|
|
126
153
|
scenescout login <url> --role <name> --script
|
|
127
154
|
For CI: sign in headless from SCENESCOUT_LOGIN_USERNAME, SCENESCOUT_LOGIN_PASSWORD
|
|
128
155
|
and, if the form asks for a code, SCENESCOUT_LOGIN_TOTP_SECRET (base32 or an
|
|
@@ -134,6 +161,30 @@ Usage:
|
|
|
134
161
|
--password-selector, --otp-selector, --submit-selector css; each of these also
|
|
135
162
|
from SCENESCOUT_LOGIN_<FLAG>, e.g. SCENESCOUT_LOGIN_SUCCESS_URL;
|
|
136
163
|
--timeout seconds (default 60))
|
|
164
|
+
scenescout export --to github|jira
|
|
165
|
+
File the project's open findings (from .scenescout/memory.json) as issues,
|
|
166
|
+
each once: a finding whose marker is already on an issue is skipped. A dry run
|
|
167
|
+
that lists what it would file unless --yes is given. Credentials come from the
|
|
168
|
+
environment only: GH_TOKEN or GITHUB_TOKEN; JIRA_EMAIL and JIRA_API_TOKEN.
|
|
169
|
+
(--repo owner/name for GitHub (GITHUB_API_URL for GitHub Enterprise Server);
|
|
170
|
+
--jira-url https://…, --jira-project KEY, --jira-issue-type name (default Bug),
|
|
171
|
+
--jira-link-type name|none (default Relates), or JIRA_BASE_URL,
|
|
172
|
+
JIRA_PROJECT_KEY, JIRA_ISSUE_TYPE, JIRA_LINK_TYPE, for Jira Cloud: an issue
|
|
173
|
+
is linked to each ticket whose criterion its finding fails;
|
|
174
|
+
--jira-update on|off (default on): update an open Jira issue filed earlier,
|
|
175
|
+
leaving a summary or description edited in Jira as it is;
|
|
176
|
+
--min-severity high|medium|low (default low); --only id,id;
|
|
177
|
+
--max-issues N (default 20, at most 100): the most one export files;
|
|
178
|
+
--refile-closed: file a finding again when its issue was closed (by default
|
|
179
|
+
an issue open or closed counts as filed); --include-worth-a-look;
|
|
180
|
+
--severity-map high=…,medium=…,low=… or none: a label on GitHub, a priority
|
|
181
|
+
in Jira (default severity: high… / High, Medium, Low); --labels a,b;
|
|
182
|
+
--screenshots on|off (default on: the finding's picture and the run's frames,
|
|
183
|
+
attached in Jira, named on GitHub);
|
|
184
|
+
--project dir (default: here); --dry-run; --yes)
|
|
185
|
+
Exit code: 0 done (findings over the cap wait for the next export), 2 could not
|
|
186
|
+
export (it lists what it filed before it stopped), or a screenshot was not
|
|
187
|
+
attached or a ticket not linked.
|
|
137
188
|
scenescout status [projectPath] What is the engine doing right now? (every session + recent actions)
|
|
138
189
|
scenescout watch [projectPath] Open the live view in a browser: what each session is doing, a thumbnail
|
|
139
190
|
of its page, and a live stream you can switch on per session
|
|
@@ -281,26 +332,6 @@ function browserOpener(url) {
|
|
|
281
332
|
return { command: "xdg-open", args: [url] };
|
|
282
333
|
}
|
|
283
334
|
}
|
|
284
|
-
/** Which browser builds are on disk, going by the paths Playwright reports for the version we depend on. */
|
|
285
|
-
async function presentBrowsers() {
|
|
286
|
-
const executables = { chromium: null, firefox: null, webkit: null };
|
|
287
|
-
try {
|
|
288
|
-
const playwright = await import("playwright");
|
|
289
|
-
for (const name of BROWSER_ENGINES)
|
|
290
|
-
executables[name] = playwright[name].executablePath() || null;
|
|
291
|
-
}
|
|
292
|
-
catch {
|
|
293
|
-
// Playwright cannot be loaded: every build reads as absent, which is what doctor should say.
|
|
294
|
-
}
|
|
295
|
-
return browserPresence(executables);
|
|
296
|
-
}
|
|
297
|
-
/** Download browser builds through the playwright CLI that ships with our own dependency. */
|
|
298
|
-
function downloadBrowsers(targets) {
|
|
299
|
-
const require = createRequire(import.meta.url);
|
|
300
|
-
const cli = path.join(path.dirname(require.resolve("playwright/package.json")), "cli.js");
|
|
301
|
-
const r = spawnSync(process.execPath, [cli, ...playwrightInstallArgs(targets)], { stdio: "inherit" });
|
|
302
|
-
return r.status === 0;
|
|
303
|
-
}
|
|
304
335
|
/**
|
|
305
336
|
* The value of a flag written as `--name x` or `--name=x`. Undefined when the
|
|
306
337
|
* flag is absent; empty when it was given no value, which includes being
|
|
@@ -392,7 +423,7 @@ async function install(flags) {
|
|
|
392
423
|
if (missing.length > 0) {
|
|
393
424
|
const size = missing.reduce((sum, t) => sum + APPROX_DISK_MB[t], 0);
|
|
394
425
|
console.log(`· Downloading ${missing.join(", ")} (one-time, about ${size} MB on disk)…`);
|
|
395
|
-
if (downloadBrowsers(missing))
|
|
426
|
+
if ((await downloadBrowsers(missing, "inherit")).ok)
|
|
396
427
|
console.log(`✓ Downloaded: ${missing.join(", ")}.`);
|
|
397
428
|
else {
|
|
398
429
|
failed = true;
|
|
@@ -452,10 +483,8 @@ async function install(flags) {
|
|
|
452
483
|
}
|
|
453
484
|
else {
|
|
454
485
|
failed = true;
|
|
455
|
-
// On Windows a client installed through npm is a .cmd shim, which node cannot start directly.
|
|
456
|
-
const windowsNote = process.platform === "win32" ? " (or it is installed as a .cmd shim, which cannot be started from here)" : "";
|
|
457
486
|
console.log(reg.status === "client-missing"
|
|
458
|
-
? `· ${label} was not found on this machine
|
|
487
|
+
? `· ${label} was not found on this machine, so nothing was registered with it.`
|
|
459
488
|
: `✗ Registering with ${label} failed: ${reg.detail}`);
|
|
460
489
|
console.log(` To do it by hand, ${reg.manual}\n`);
|
|
461
490
|
}
|
|
@@ -496,12 +525,11 @@ async function install(flags) {
|
|
|
496
525
|
process.exitCode = 1;
|
|
497
526
|
return;
|
|
498
527
|
}
|
|
499
|
-
|
|
500
|
-
|
|
528
|
+
const closing = installClosing({ browserOnly, forClaude });
|
|
529
|
+
if (closing)
|
|
530
|
+
console.log(`\n${closing}`);
|
|
531
|
+
if (browserOnly)
|
|
501
532
|
return;
|
|
502
|
-
}
|
|
503
|
-
if (forClaude)
|
|
504
|
-
console.log("\nStart a FRESH Claude Code session, then in any project run: /scenescout");
|
|
505
533
|
// Telling someone to restart a client nothing was registered with sends them looking for a server that is not there.
|
|
506
534
|
if (others.length > 0 && !flags.includes("--no-register"))
|
|
507
535
|
console.log(`\n${firstMessageHint(others)}`);
|
|
@@ -513,12 +541,15 @@ async function doctor(flags) {
|
|
|
513
541
|
packageRoot,
|
|
514
542
|
claudeDir: resolveClaudeDir(process.env, os.homedir()),
|
|
515
543
|
nodeVersion: process.version,
|
|
544
|
+
version: packageVersion(),
|
|
516
545
|
// What a default attach launches: the headless build of the default browser.
|
|
517
546
|
defaultBrowser: await (async () => {
|
|
518
547
|
const target = launchTarget(defaultEngine(process.env), false);
|
|
519
548
|
const found = (await presentBrowsers())[target];
|
|
520
549
|
return { target, path: found.installed ? found.path : null, expected: found.path };
|
|
521
550
|
})(),
|
|
551
|
+
desktopExtension: findDesktopExtension(desktopExtensionRoots({ platform: process.platform, home: os.homedir(), env: process.env })),
|
|
552
|
+
headlessShellDir: (await presentBrowsers())["chromium-headless-shell"].path,
|
|
522
553
|
run: spawnRunner,
|
|
523
554
|
});
|
|
524
555
|
for (const c of checks) {
|
|
@@ -528,9 +559,7 @@ async function doctor(flags) {
|
|
|
528
559
|
}
|
|
529
560
|
if (checks.some((c) => !c.ok))
|
|
530
561
|
process.exit(1);
|
|
531
|
-
console.log(flags.includes("--engine")
|
|
532
|
-
? "\nAll good. Ask your agent: Use SceneScout to test http://localhost:3000"
|
|
533
|
-
: "\nAll good. In any project, run: /scenescout (or ask: Use SceneScout to test http://localhost:3000)");
|
|
562
|
+
console.log(`\n${doctorAllGood({ engineOnly: flags.includes("--engine"), desktopOnly: checks.some((c) => c.name === CLAUDE_CODE_NOT_NEEDED) })}`);
|
|
534
563
|
}
|
|
535
564
|
/** `scenescout check`: exit 0 passed, 1 failed the gate, 2 could not run. */
|
|
536
565
|
async function check(args) {
|
|
@@ -576,7 +605,16 @@ async function check(args) {
|
|
|
576
605
|
writeSelfIgnore(path.dirname(outDir));
|
|
577
606
|
fs.mkdirSync(outDir, { recursive: true });
|
|
578
607
|
fs.writeFileSync(path.join(outDir, "report.md"), markdown);
|
|
579
|
-
|
|
608
|
+
const sarifFiles = sarifFilesFor({
|
|
609
|
+
option: options.sarifFileAnchor,
|
|
610
|
+
env: process.env,
|
|
611
|
+
projectDir: options.projectDir,
|
|
612
|
+
flowsDir: inputs.flowsDir,
|
|
613
|
+
exists: (p) => fs.existsSync(p),
|
|
614
|
+
});
|
|
615
|
+
if (sarifFiles.warning)
|
|
616
|
+
console.error(`scenescout check: ${sarifFiles.warning}`);
|
|
617
|
+
fs.writeFileSync(path.join(outDir, "check.sarif"), JSON.stringify(toSarif(result, version, sarifFiles), null, 2) + "\n");
|
|
580
618
|
fs.writeFileSync(path.join(outDir, "check.json"), JSON.stringify(toSummaryJson(result, version), null, 2) + "\n");
|
|
581
619
|
// On GitHub Actions the verdict also goes on the run's summary page.
|
|
582
620
|
if (process.env.GITHUB_STEP_SUMMARY)
|
|
@@ -589,6 +627,12 @@ async function check(args) {
|
|
|
589
627
|
}
|
|
590
628
|
console.log("\n" + markdown);
|
|
591
629
|
console.log(`Wrote report.md, check.sarif and check.json to ${outDir}`);
|
|
630
|
+
const pictured = result.baselines?.results.filter((r) => r.files).length ?? 0;
|
|
631
|
+
if (pictured > 0)
|
|
632
|
+
console.log(`Wrote the pictures of ${pictured} changed baseline(s) under ${path.join(outDir, VISUAL_DIRNAME)}`);
|
|
633
|
+
const written = result.baselines?.results.filter((r) => r.status === "updated").length ?? 0;
|
|
634
|
+
if (written > 0)
|
|
635
|
+
console.log(`Wrote ${written} baseline(s) to ${baselinesDirOf(options)}`);
|
|
592
636
|
// --on-refused-step report: everything else has its verdict in the files, and the exit code still says the run was incomplete.
|
|
593
637
|
if (refused)
|
|
594
638
|
console.error(`scenescout check: could not run a saved flow: ${refused}`);
|
|
@@ -616,7 +660,7 @@ async function firstRun(args) {
|
|
|
616
660
|
if (downloads.length > 0) {
|
|
617
661
|
console.log(downloadLine(downloads));
|
|
618
662
|
const began = Date.now();
|
|
619
|
-
if (!downloadBrowsers(downloads)) {
|
|
663
|
+
if (!(await downloadBrowsers(downloads, "inherit")).ok) {
|
|
620
664
|
return fail(`Chromium could not be downloaded. Check the network or proxy and run this again, or download it by hand: npx playwright install ${downloads.join(" ")}`);
|
|
621
665
|
}
|
|
622
666
|
// The installer's exit code is not the build: look again before saying it is there.
|
|
@@ -761,6 +805,21 @@ async function login(args) {
|
|
|
761
805
|
}
|
|
762
806
|
process.exit(0);
|
|
763
807
|
}
|
|
808
|
+
/** `scenescout export`: exits as EXIT_EXPORT says. */
|
|
809
|
+
async function exportFindings(args) {
|
|
810
|
+
const parsed = parseExportArgs(args, process.cwd(), process.env);
|
|
811
|
+
if (!parsed.ok) {
|
|
812
|
+
// A refusal can quote the value it refused, and a credential pasted into an option is still a credential.
|
|
813
|
+
console.error(redactKeys(`scenescout export: ${parsed.error}`, credentialSecrets(process.env)));
|
|
814
|
+
process.exit(EXIT_EXPORT.couldNotExport);
|
|
815
|
+
}
|
|
816
|
+
const outcome = await runExport(parsed.options, {
|
|
817
|
+
env: process.env,
|
|
818
|
+
log: (line) => console.log(line),
|
|
819
|
+
error: (line) => console.error(line),
|
|
820
|
+
});
|
|
821
|
+
process.exit(outcome.exitCode);
|
|
822
|
+
}
|
|
764
823
|
const [, , command, ...args] = process.argv;
|
|
765
824
|
// A CLI's failure mode should be a sentence, not a stack trace. `scan` on a
|
|
766
825
|
// path that does not exist and `status` on a half-written status.json both
|
|
@@ -790,6 +849,7 @@ try {
|
|
|
790
849
|
check,
|
|
791
850
|
ci,
|
|
792
851
|
login,
|
|
852
|
+
export: exportFindings,
|
|
793
853
|
status: (a) => status(path.resolve(a[0] ?? process.cwd())),
|
|
794
854
|
watch: (a) => {
|
|
795
855
|
const positional = a.filter((x) => !x.startsWith("--"));
|
package/dist/commands.js
CHANGED
|
@@ -18,13 +18,14 @@ const HANDLERS_OF = {
|
|
|
18
18
|
check: true,
|
|
19
19
|
ci: true,
|
|
20
20
|
login: true,
|
|
21
|
+
export: true,
|
|
21
22
|
status: true,
|
|
22
23
|
watch: true,
|
|
23
24
|
};
|
|
24
25
|
export const SUBCOMMANDS = Object.keys(HANDLERS_OF);
|
|
25
26
|
/**
|
|
26
|
-
* Commands that read their arguments by hand. `check`, `ci
|
|
27
|
-
* absent: their own parsers refuse unknown options. `serve` is absent on
|
|
27
|
+
* Commands that read their arguments by hand. `check`, `ci`, `login` and
|
|
28
|
+
* `export` are absent: their own parsers refuse unknown options. `serve` is absent on
|
|
28
29
|
* purpose: it is the line an MCP client launches, and a stray argument there
|
|
29
30
|
* should not stop the server from starting.
|
|
30
31
|
*/
|