@codeyam-editor/codeyam-editor 0.1.0-staging.0f231be

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.
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require("path");
4
+ /**
5
+ * Start the container monitoring UI.
6
+ *
7
+ * Starts the Rust backend (if not already running) and opens the
8
+ * inline monitoring UI at http://localhost:14199.
9
+ */
10
+
11
+ const {
12
+ ensureServer,
13
+ rootDir,
14
+ killChildProcess,
15
+ readMergedEditorConfig,
16
+ resolveLauncherPort,
17
+ } = require("./utils");
18
+ const { open } = require("./open");
19
+
20
+ async function main() {
21
+ // Same resolution order as the editor launcher: env var
22
+ // (CODEYAM_EDITOR_PORT) → merged editor.json + editor.local.json
23
+ // (proxy.controlPort) → hard fallback (14199).
24
+ const merged = readMergedEditorConfig(rootDir);
25
+ const PORT = resolveLauncherPort(
26
+ process.env.CODEYAM_EDITOR_PORT,
27
+ merged.proxy && merged.proxy.controlPort,
28
+ 14199,
29
+ );
30
+
31
+ const serverChild = await ensureServer(PORT);
32
+ const url = `http://localhost:${PORT}`;
33
+ console.error(`Container UI: ${url}`);
34
+ console.error(`Logs: ${path.join(rootDir, ".codeyam", "logs")}`);
35
+ open(url);
36
+ const keepAlive = setInterval(() => {}, 60_000);
37
+
38
+ process.on("SIGINT", () => {
39
+ clearInterval(keepAlive);
40
+ killChildProcess(serverChild);
41
+ process.exit(0);
42
+ });
43
+ }
44
+
45
+ module.exports = { main };
46
+
47
+ if (require.main === module) {
48
+ main();
49
+ }
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Development CLI for running the codeyam-editor against any project.
5
+ *
6
+ * Two modes:
7
+ *
8
+ * Launcher — no subcommand (or --restart): builds the Rust binary and UI
9
+ * from the editor repo, starts the server, waits for readiness, opens browser.
10
+ * codeyam-editor-dev
11
+ * codeyam-editor-dev --restart
12
+ *
13
+ * Passthrough — any subcommand (start, init, editor, ...): rebuilds the
14
+ * Rust binary, then forwards args to it.
15
+ * codeyam-editor-dev editor step 1
16
+ * codeyam-editor-dev editor advance
17
+ */
18
+
19
+ const { execSync, spawn } = require("child_process");
20
+ const path = require("path");
21
+ const {
22
+ ensureServer,
23
+ restartServer,
24
+ waitForPort,
25
+ ensureBinary,
26
+ rootDir,
27
+ killChildProcess,
28
+ readMergedEditorConfig,
29
+ resolveLauncherPort,
30
+ } = require("./utils");
31
+ const { open } = require("./open");
32
+
33
+ // Tell the Rust binary to emit "codeyam-editor-dev" in all instructions,
34
+ // permissions, and installed files instead of "codeyam-editor".
35
+ process.env.CODEYAM_CLI = "codeyam-editor-dev";
36
+
37
+ const args = process.argv.slice(2);
38
+ const KNOWN_SUBCOMMANDS = ["start", "init", "editor"];
39
+
40
+ /**
41
+ * Check if the first non-flag argument is a known subcommand.
42
+ * Accepts an explicit argv array so the helper is testable without mutating
43
+ * process.argv (the module-level `args` const is the production default).
44
+ */
45
+ function hasSubcommand(argv = args) {
46
+ for (const arg of argv) {
47
+ if (arg.startsWith("-")) continue;
48
+ return KNOWN_SUBCOMMANDS.includes(arg);
49
+ }
50
+ return false;
51
+ }
52
+
53
+ /** Build the Rust binary from the editor repo. */
54
+ function buildBinary() {
55
+ console.error("Building Rust binary...");
56
+ try {
57
+ execSync("cargo build", { stdio: "inherit", cwd: rootDir });
58
+ } catch {
59
+ console.error("Failed to build Rust binary. Continuing with existing build (if any)...");
60
+ }
61
+ }
62
+
63
+ module.exports = { hasSubcommand, buildBinary };
64
+
65
+ if (require.main === module) {
66
+ // --- Passthrough mode: build binary, then forward to it ---
67
+ if (hasSubcommand()) {
68
+ buildBinary();
69
+ const binary = ensureBinary();
70
+ const child = spawn(binary, args, {
71
+ stdio: "inherit",
72
+ cwd: process.cwd(),
73
+ env: process.env,
74
+ });
75
+ child.on("exit", (code) => {
76
+ process.exit(code ?? 1);
77
+ });
78
+ } else {
79
+ // --- Launcher mode: build everything, start server, open browser ---
80
+ const shouldRestart = args.includes("--restart");
81
+
82
+ const launcherMain = async () => {
83
+ const projectDir = process.cwd();
84
+ console.error(`Editor repo: ${rootDir}`);
85
+ console.error(`Target project: ${projectDir}`);
86
+
87
+ // Editor server's control port is owned by the editor repo's
88
+ // merged config (rootDir), not the target project's. The target
89
+ // project's own `editor.local.json` only contributes to the
90
+ // app/dev-server port below.
91
+ const editorMerged = readMergedEditorConfig(rootDir);
92
+ const SERVER_PORT = resolveLauncherPort(
93
+ process.env.CODEYAM_EDITOR_PORT,
94
+ editorMerged.proxy && editorMerged.proxy.controlPort,
95
+ 14199,
96
+ );
97
+
98
+ // Target project's dev-server port comes from its own
99
+ // editor.json + editor.local.json (developers iterating on the
100
+ // editor against a real client project rely on the override
101
+ // file here too).
102
+ const projectMerged = readMergedEditorConfig(projectDir);
103
+ const appPort = resolveLauncherPort(undefined, projectMerged.port, 3000);
104
+
105
+ buildBinary();
106
+
107
+ // Build the UI from the editor repo
108
+ console.error("Building UI...");
109
+ try {
110
+ execSync("npx vite build", { stdio: "inherit", cwd: path.join(rootDir, "ui") });
111
+ } catch {
112
+ console.error("Failed to build UI. Continuing with existing build (if any)...");
113
+ }
114
+
115
+ const serverChild = shouldRestart
116
+ ? await restartServer(SERVER_PORT)
117
+ : await ensureServer(SERVER_PORT);
118
+
119
+ const viteInternalPort = appPort + 1;
120
+ console.error(`Waiting for dev server on port ${viteInternalPort}...`);
121
+ const viteReady = await waitForPort(viteInternalPort, 15000, 300);
122
+ if (!viteReady) {
123
+ console.error(`Warning: Dev server not ready on port ${viteInternalPort}. Live Preview may not work initially.`);
124
+ }
125
+
126
+ const url = `http://localhost:${SERVER_PORT}`;
127
+ console.error(`Editor UI: ${url}`);
128
+ console.error(`Live Preview: http://localhost:${appPort}`);
129
+ console.error(`Project: ${projectDir}`);
130
+ open(url);
131
+ const keepAlive = setInterval(() => {}, 60_000);
132
+
133
+ process.on("SIGINT", () => {
134
+ clearInterval(keepAlive);
135
+ killChildProcess(serverChild);
136
+ process.exit(0);
137
+ });
138
+ };
139
+
140
+ launcherMain();
141
+ }
142
+ }
package/npm/editor.js ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Start the editor.
5
+ *
6
+ * 1. Builds the React UI into ui/dist/ (stable outer shell, no HMR)
7
+ * 2. Starts the Rust backend which:
8
+ * - Serves ui/dist/ as static files on port 14199
9
+ * - Launches the Vite dev server (startCommand) on port 5173 for Live Preview HMR
10
+ * - Provides the API, WebSocket, and screenshot endpoints
11
+ * 3. Waits for the Vite dev server to be ready (so the Live Preview iframe works)
12
+ * 4. Opens http://localhost:14199 in the browser
13
+ */
14
+
15
+ const { execSync } = require("child_process");
16
+ const fs = require("fs");
17
+ const os = require("os");
18
+ const path = require("path");
19
+ const {
20
+ ensureServer,
21
+ stopServer,
22
+ waitForPort,
23
+ rootDir,
24
+ killChildProcess,
25
+ readMergedEditorConfig,
26
+ resolveLauncherPort,
27
+ classifyPortHolder,
28
+ formatForeignListenerError,
29
+ formatCrossProjectError,
30
+ } = require("./utils");
31
+ const { open } = require("./open");
32
+
33
+ const shouldRestart = process.argv.includes("--restart");
34
+
35
+ function buildBinary() {
36
+ console.error("Building Rust binary...");
37
+ try {
38
+ execSync("cargo build", { stdio: "inherit", cwd: rootDir });
39
+ return true;
40
+ } catch {
41
+ console.error("Failed to build Rust binary. Continuing with existing build (if any)...");
42
+ return false;
43
+ }
44
+ }
45
+
46
+ // Resolve <CARGO_HOME>/bin (falling back to $HOME/.cargo/bin). Mirrors
47
+ // `installed_binary_path()` in crates/control-api/src/pty_broker_startup.rs
48
+ // — the Rust broker auto-spawn execs the binary at this path, so we MUST
49
+ // keep it in sync with the freshly-built debug binary or `npm run editor`
50
+ // will start a server whose PTY-broker auto-spawn execs a stale install.
51
+ function cargoBinDir() {
52
+ if (process.env.CARGO_HOME) return path.join(process.env.CARGO_HOME, "bin");
53
+ return path.join(os.homedir(), ".cargo", "bin");
54
+ }
55
+
56
+ // Install target/debug/codeyam-editor to <cargo_bin>/codeyam-editor
57
+ // atomically (write to tmp in same dir, then rename), and refresh the
58
+ // codeyam-editor-pty-broker hardlink so it points at the new inode.
59
+ //
60
+ // The PTY broker auto-spawn (control-api/src/pty_broker_startup.rs) always
61
+ // resolves the *installed* path, NOT current_exe() — that was a deliberate
62
+ // fix to prevent a fork-bomb where test binaries spawned themselves. So a
63
+ // fresh `target/debug/codeyam-editor` is not enough; the install path must
64
+ // be refreshed too. Without this, build tabs hang on "Reconnecting..."
65
+ // because the broker daemon errors out (`unrecognized subcommand`) before
66
+ // the UDS socket opens.
67
+ function installBinary() {
68
+ const srcPath = path.join(rootDir, "target", "debug", "codeyam-editor");
69
+ if (!fs.existsSync(srcPath)) {
70
+ console.error(`Skipping install: ${srcPath} not found.`);
71
+ return;
72
+ }
73
+ const binDir = cargoBinDir();
74
+ const dstPath = path.join(binDir, "codeyam-editor");
75
+ const linkPath = path.join(binDir, "codeyam-editor-pty-broker");
76
+ const tmpPath = path.join(binDir, `.codeyam-editor.tmp.${process.pid}`);
77
+ console.error(`Installing fresh binary to ${dstPath}...`);
78
+ try {
79
+ fs.mkdirSync(binDir, { recursive: true });
80
+ fs.copyFileSync(srcPath, tmpPath);
81
+ fs.chmodSync(tmpPath, 0o755);
82
+ fs.renameSync(tmpPath, dstPath);
83
+ try {
84
+ fs.unlinkSync(linkPath);
85
+ } catch (e) {
86
+ if (e.code !== "ENOENT") throw e;
87
+ }
88
+ fs.linkSync(dstPath, linkPath);
89
+ } catch (err) {
90
+ console.error(`Failed to install binary: ${err.message}`);
91
+ try { fs.unlinkSync(tmpPath); } catch {}
92
+ }
93
+ }
94
+
95
+ // Evict Rust build artifacts not touched in the last day so target/ doesn't
96
+ // grow unboundedly. Must run after the old server is stopped (it holds files
97
+ // in target/debug/ open) and before the new server starts. Non-fatal.
98
+ function sweepStaleArtifacts() {
99
+ console.error("Sweeping stale Rust build artifacts (idle >1 day)...");
100
+ try {
101
+ execSync("cargo sweep --time 1", { stdio: "inherit", cwd: rootDir });
102
+ } catch {
103
+ console.error("cargo-sweep failed (install with `cargo install cargo-sweep`). Continuing.");
104
+ }
105
+ }
106
+
107
+ async function main() {
108
+ // Read ports from the merged editor config (editor.json + per-developer
109
+ // editor.local.json). The Rust backend reads the same merged config on
110
+ // its own — we resolve here so the launcher's log lines, `waitForPort`
111
+ // calls, and `--port` arg agree with what the binary will actually
112
+ // bind. Env vars still win for scripted/CI invocations
113
+ // (CODEYAM_EDITOR_PORT, PORT), then the merged config, then a hard
114
+ // fallback for fresh checkouts with no override file.
115
+ const merged = readMergedEditorConfig(rootDir);
116
+ const SERVER_PORT = resolveLauncherPort(
117
+ process.env.CODEYAM_EDITOR_PORT,
118
+ merged.proxy && merged.proxy.controlPort,
119
+ 14199,
120
+ );
121
+ const VITE_PORT = resolveLauncherPort(process.env.PORT, merged.port, 5173);
122
+ // The reverse proxy sits on VITE_PORT; the actual Vite dev server runs on
123
+ // VITE_PORT + 1. We need to wait for the real Vite server, not the proxy.
124
+ const VITE_INTERNAL_PORT = VITE_PORT + 1;
125
+
126
+ // Rebuild the Rust binary so code changes are picked up on restart.
127
+ // This script is only invoked from a source checkout (via `npm run editor`),
128
+ // so Cargo.toml and the crates tree are always present.
129
+ const buildOk = buildBinary();
130
+ // Refresh <cargo_bin>/codeyam-editor (and the pty-broker hardlink) so
131
+ // the broker auto-spawn execs a binary that knows the `pty-broker
132
+ // daemon` subcommand. Skip if the build failed — installing a stale
133
+ // debug binary over a working install is worse than leaving it.
134
+ if (buildOk) {
135
+ installBinary();
136
+ }
137
+
138
+ // Build the UI so the backend can serve it as static files
139
+ console.error("Building UI...");
140
+ try {
141
+ execSync("npx vite build", {
142
+ stdio: "inherit",
143
+ cwd: path.join(rootDir, "ui"),
144
+ });
145
+ } catch {
146
+ console.error("Failed to build UI. Continuing with existing build (if any)...");
147
+ }
148
+
149
+ // Start the Rust backend — it serves ui/dist/ and launches the Vite dev
150
+ // server (configured via startCommand in editor.json) for Live Preview HMR.
151
+ // On --restart: identity-check first, then stop the existing server,
152
+ // sweep stale artifacts while target/ is idle, then start fresh.
153
+ //
154
+ // Stopping someone else's editor server on `--restart` is the worst
155
+ // possible outcome of a port collision — only stop the listener if
156
+ // the identity probe confirms it's this project's server.
157
+ if (shouldRestart) {
158
+ const holder = await classifyPortHolder(SERVER_PORT);
159
+ if (holder.kind === "foreign") {
160
+ console.error(formatForeignListenerError(SERVER_PORT));
161
+ process.exit(1);
162
+ }
163
+ if (holder.kind === "cross-project") {
164
+ console.error(formatCrossProjectError(SERVER_PORT, holder, "restart"));
165
+ process.exit(1);
166
+ }
167
+ if (holder.kind === "same-project") {
168
+ console.error(`Stopping existing server on port ${SERVER_PORT}...`);
169
+ await stopServer(SERVER_PORT);
170
+ }
171
+ // holder.kind === "free": nothing to stop, fall through to ensureServer.
172
+ }
173
+ if (shouldRestart) {
174
+ sweepStaleArtifacts();
175
+ }
176
+ const serverChild = await ensureServer(SERVER_PORT);
177
+
178
+ // Wait for the actual Vite dev server on the internal port (not the reverse
179
+ // proxy on VITE_PORT, which starts immediately with the Rust binary).
180
+ console.error("Waiting for Vite dev server...");
181
+ const viteReady = await waitForPort(VITE_INTERNAL_PORT, 15000, 300);
182
+ if (!viteReady) {
183
+ console.error(`Warning: Vite dev server not ready on port ${VITE_INTERNAL_PORT}. Live Preview may not work initially.`);
184
+ }
185
+
186
+ const url = `http://localhost:${SERVER_PORT}`;
187
+ console.error(`Editor UI: ${url}`);
188
+ console.error(`Live Preview: http://localhost:${VITE_PORT}`);
189
+ console.error(`Logs: ${path.join(rootDir, ".codeyam", "logs")}`);
190
+ open(url);
191
+ const keepAlive = setInterval(() => {}, 60_000);
192
+
193
+ process.on("SIGINT", () => {
194
+ clearInterval(keepAlive);
195
+ killChildProcess(serverChild);
196
+ process.exit(0);
197
+ });
198
+ }
199
+
200
+ module.exports = {
201
+ buildBinary,
202
+ cargoBinDir,
203
+ installBinary,
204
+ main,
205
+ sweepStaleArtifacts,
206
+ };
207
+
208
+ if (require.main === module) {
209
+ main();
210
+ }
package/npm/open.js ADDED
@@ -0,0 +1,15 @@
1
+ const { spawn } = require("child_process");
2
+
3
+ /** Open a URL in the default browser. */
4
+ function open(url) {
5
+ const cmd =
6
+ process.platform === "darwin"
7
+ ? "open"
8
+ : process.platform === "win32"
9
+ ? "start"
10
+ : "xdg-open";
11
+
12
+ spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
13
+ }
14
+
15
+ module.exports = { open };
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Render environment (colorScheme, deviceScaleFactor, userAgent, locale,
4
+ // timezoneId, reduceMotion, forcedColors) is read from config when present
5
+ // and passed to browser.newContext(). This is what makes screenshots match
6
+ // the Live Preview iframe's host browser — see docs/rendering.md.
7
+ //
8
+ // iframeBackground is forwarded to buildIframeHarness so the capture paints
9
+ // the user's editor-shell background (or whatever the UI detected) behind
10
+ // the iframe instead of a hardcoded white.
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const { chromium } = require("playwright");
15
+
16
+ const {
17
+ findErrorPattern,
18
+ buildErrorContextSnippet,
19
+ hasRenderableContent,
20
+ describeBlankReason,
21
+ } = require("./scenario-metrics");
22
+
23
+ const {
24
+ createIssue,
25
+ pushIssue,
26
+ buildResult,
27
+ } = require("./scenario-issues");
28
+
29
+ const {
30
+ attachHttpMocks,
31
+ } = require("./scenario-mocks");
32
+
33
+ const {
34
+ loadScenarioInIframe,
35
+ waitForStablePage,
36
+ collectContentState,
37
+ } = require("./scenario-playwright");
38
+
39
+ const {
40
+ getInitScript,
41
+ handleConsoleMessage,
42
+ handlePageError,
43
+ handleRequestFailed,
44
+ } = require("./scenario-handlers");
45
+
46
+ const BLANK_RETRY_DELAY_MS = 500;
47
+
48
+ async function runScenarioCheck(config) {
49
+ const { url, outputPath, width, height, httpMocks = {} } = config;
50
+ const issues = [];
51
+ const browser = await chromium.launch();
52
+ const contextOptions = {
53
+ viewport: { width: width || 1440, height: height || 900 },
54
+ };
55
+ if (config.colorScheme) contextOptions.colorScheme = config.colorScheme;
56
+ if (config.deviceScaleFactor)
57
+ contextOptions.deviceScaleFactor = config.deviceScaleFactor;
58
+ if (config.userAgent) contextOptions.userAgent = config.userAgent;
59
+ if (config.locale) contextOptions.locale = config.locale;
60
+ if (config.timezoneId) contextOptions.timezoneId = config.timezoneId;
61
+ if (config.reduceMotion) contextOptions.reducedMotion = config.reduceMotion;
62
+ if (config.forcedColors) contextOptions.forcedColors = config.forcedColors;
63
+ const context = await browser.newContext(contextOptions);
64
+
65
+ // Context-level init script runs in ALL frames (including cross-origin iframes)
66
+ await context.addInitScript(getInitScript());
67
+
68
+ const page = await context.newPage();
69
+ await attachHttpMocks(page, httpMocks);
70
+
71
+ page.on("pageerror", (error) => {
72
+ pushIssue(issues, handlePageError(error));
73
+ });
74
+
75
+ page.on("console", (message) => {
76
+ const issue = handleConsoleMessage(message);
77
+ if (issue) {
78
+ pushIssue(issues, issue);
79
+ }
80
+ });
81
+
82
+ page.on("requestfailed", (request) => {
83
+ const issue = handleRequestFailed(request);
84
+ if (issue) {
85
+ pushIssue(issues, issue);
86
+ }
87
+ });
88
+
89
+ let loaded = false;
90
+
91
+ try {
92
+ const { frame, response } = await loadScenarioInIframe(page, url, {
93
+ background: config.iframeBackground,
94
+ });
95
+ loaded = true;
96
+
97
+ if (response && response.status() >= 400) {
98
+ pushIssue(
99
+ issues,
100
+ createIssue("navigation", `Navigation returned HTTP ${response.status()}`, {
101
+ url: response.url(),
102
+ status: response.status(),
103
+ }),
104
+ );
105
+ }
106
+
107
+ await waitForStablePage(page, frame);
108
+
109
+ const rejectionMessages = await frame.evaluate(
110
+ () => window.__codeyamUnhandledRejections || [],
111
+ );
112
+ for (const message of rejectionMessages) {
113
+ pushIssue(
114
+ issues,
115
+ createIssue("unhandledrejection", message, {
116
+ url: page.url() || url,
117
+ }),
118
+ );
119
+ }
120
+
121
+ // Cold-start retry: a single re-collect after a short pause covers
122
+ // the React.lazy / Suspense-fallback race that flagged 4-of-59
123
+ // scenarios as blank against a cold Vite dev server on 2026-04-30.
124
+ // One retry is enough — the Suspense window is sub-second in
125
+ // practice, longer waits just slow down genuine blank-render bugs.
126
+ let contentState = await collectContentState(frame);
127
+ let hasContent = hasRenderableContent(contentState);
128
+ if (!hasContent) {
129
+ await new Promise((r) => setTimeout(r, BLANK_RETRY_DELAY_MS));
130
+ contentState = await collectContentState(frame);
131
+ hasContent = hasRenderableContent(contentState);
132
+ }
133
+
134
+ if (!hasContent) {
135
+ pushIssue(
136
+ issues,
137
+ createIssue(
138
+ "blank",
139
+ `Page rendered no visible content (${describeBlankReason(contentState)})`,
140
+ { url: page.url() || url },
141
+ ),
142
+ );
143
+ }
144
+
145
+ // Check for known error states in the rendered content
146
+ const bodyText = await frame.evaluate(() => document.body.innerText || "");
147
+ const matchedPattern = findErrorPattern(bodyText);
148
+ if (matchedPattern) {
149
+ const contextSnippet = buildErrorContextSnippet(bodyText, matchedPattern);
150
+ pushIssue(
151
+ issues,
152
+ createIssue(
153
+ "error-state",
154
+ `Page contains error content (matched "${matchedPattern}"): ${contextSnippet ?? bodyText.slice(0, 200)}`,
155
+ {
156
+ url: page.url() || url,
157
+ matchedPattern,
158
+ contextSnippet,
159
+ },
160
+ ),
161
+ );
162
+ }
163
+
164
+ if (outputPath && loaded) {
165
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
166
+ await page.screenshot({ path: outputPath, fullPage: false });
167
+ }
168
+
169
+ return buildResult({
170
+ loaded,
171
+ hasContent,
172
+ issues,
173
+ outputPath,
174
+ url: frame.url() || url,
175
+ });
176
+ } catch (error) {
177
+ pushIssue(
178
+ issues,
179
+ createIssue("navigation", error.message || String(error), { url }),
180
+ );
181
+
182
+ return buildResult({
183
+ loaded,
184
+ hasContent: false,
185
+ issues,
186
+ outputPath,
187
+ url,
188
+ });
189
+ } finally {
190
+ await browser.close();
191
+ }
192
+ }
193
+
194
+ async function main() {
195
+ const config = JSON.parse(process.argv[2] || "{}");
196
+
197
+ if (!config.url) {
198
+ console.error(
199
+ "Usage: node scenario-check.js '{\"url\":\"...\",\"outputPath\":\"...\",\"width\":1440,\"height\":900}'",
200
+ );
201
+ process.exit(1);
202
+ }
203
+
204
+ const result = await runScenarioCheck(config);
205
+ console.log(JSON.stringify(result));
206
+ }
207
+
208
+ module.exports = {
209
+ runScenarioCheck,
210
+ main,
211
+ };
212
+
213
+ if (require.main === module) {
214
+ main().catch((error) => {
215
+ console.error(error.message || String(error));
216
+ process.exit(1);
217
+ });
218
+ }