sweetlink 0.1.0 → 0.2.1
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 +20 -0
- package/LICENSE +1 -1
- package/README.md +22 -11
- package/dist/daemon/src/codename.d.ts.map +1 -1
- package/dist/daemon/src/codename.js +11 -11
- package/dist/daemon/src/codename.js.map +1 -1
- package/dist/daemon/src/index.d.ts +1 -1
- package/dist/daemon/src/index.js +131 -122
- package/dist/daemon/src/index.js.map +1 -1
- package/dist/shared/src/env.d.ts.map +1 -1
- package/dist/shared/src/env.js +12 -12
- package/dist/shared/src/env.js.map +1 -1
- package/dist/shared/src/index.d.ts +30 -30
- package/dist/shared/src/index.d.ts.map +1 -1
- package/dist/shared/src/index.js +17 -17
- package/dist/shared/src/index.js.map +1 -1
- package/dist/shared/src/node.d.ts +1 -1
- package/dist/shared/src/node.d.ts.map +1 -1
- package/dist/shared/src/node.js +18 -18
- package/dist/shared/src/node.js.map +1 -1
- package/dist/src/codex.d.ts +1 -1
- package/dist/src/codex.js +19 -19
- package/dist/src/codex.js.map +1 -1
- package/dist/src/commands/click.d.ts +1 -1
- package/dist/src/commands/click.d.ts.map +1 -1
- package/dist/src/commands/click.js +32 -24
- package/dist/src/commands/click.js.map +1 -1
- package/dist/src/commands/run-js.d.ts +1 -1
- package/dist/src/commands/run-js.d.ts.map +1 -1
- package/dist/src/commands/run-js.js +10 -10
- package/dist/src/commands/run-js.js.map +1 -1
- package/dist/src/commands/trust-ca.d.ts +1 -1
- package/dist/src/commands/trust-ca.js +15 -15
- package/dist/src/core/config-file.d.ts +6 -0
- package/dist/src/core/config-file.d.ts.map +1 -1
- package/dist/src/core/config-file.js +73 -38
- package/dist/src/core/config-file.js.map +1 -1
- package/dist/src/core/config.d.ts +3 -2
- package/dist/src/core/config.d.ts.map +1 -1
- package/dist/src/core/config.js +31 -18
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/dev-bootstrap.d.ts +10 -0
- package/dist/src/core/dev-bootstrap.d.ts.map +1 -0
- package/dist/src/core/dev-bootstrap.js +112 -0
- package/dist/src/core/dev-bootstrap.js.map +1 -0
- package/dist/src/core/env.d.ts +1 -1
- package/dist/src/core/env.d.ts.map +1 -1
- package/dist/src/core/env.js +3 -3
- package/dist/src/core/env.js.map +1 -1
- package/dist/src/devtools-registry.d.ts.map +1 -1
- package/dist/src/devtools-registry.js +48 -46
- package/dist/src/devtools-registry.js.map +1 -1
- package/dist/src/env.d.ts +1 -1
- package/dist/src/env.js +1 -1
- package/dist/src/http.d.ts.map +1 -1
- package/dist/src/http.js +7 -6
- package/dist/src/http.js.map +1 -1
- package/dist/src/index.d.ts +7 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +450 -363
- package/dist/src/index.js.map +1 -1
- package/dist/src/runtime/browser/client.d.ts +3 -3
- package/dist/src/runtime/browser/client.d.ts.map +1 -1
- package/dist/src/runtime/browser/client.js +79 -72
- package/dist/src/runtime/browser/client.js.map +1 -1
- package/dist/src/runtime/browser/commands/index.d.ts +2 -2
- package/dist/src/runtime/browser/commands/index.js +17 -17
- package/dist/src/runtime/browser/commands/index.js.map +1 -1
- package/dist/src/runtime/browser/dom-to-image-loader.d.ts.map +1 -1
- package/dist/src/runtime/browser/dom-to-image-loader.js +7 -7
- package/dist/src/runtime/browser/dom-to-image-loader.js.map +1 -1
- package/dist/src/runtime/browser/index.d.ts +3 -3
- package/dist/src/runtime/browser/index.js +2 -2
- package/dist/src/runtime/browser/module-loader.js +2 -2
- package/dist/src/runtime/browser/screenshot/hooks.d.ts +25 -53
- package/dist/src/runtime/browser/screenshot/hooks.d.ts.map +1 -1
- package/dist/src/runtime/browser/screenshot/hooks.js +32 -29
- package/dist/src/runtime/browser/screenshot/hooks.js.map +1 -1
- package/dist/src/runtime/browser/screenshot/index.d.ts +3 -3
- package/dist/src/runtime/browser/screenshot/index.d.ts.map +1 -1
- package/dist/src/runtime/browser/screenshot/index.js +17 -15
- package/dist/src/runtime/browser/screenshot/index.js.map +1 -1
- package/dist/src/runtime/browser/screenshot/renderers/dom-to-image.d.ts +1 -1
- package/dist/src/runtime/browser/screenshot/renderers/dom-to-image.js +13 -13
- package/dist/src/runtime/browser/screenshot/renderers/dom-to-image.js.map +1 -1
- package/dist/src/runtime/browser/screenshot/renderers/html2canvas.d.ts +1 -1
- package/dist/src/runtime/browser/screenshot/renderers/html2canvas.d.ts.map +1 -1
- package/dist/src/runtime/browser/screenshot/renderers/html2canvas.js +24 -24
- package/dist/src/runtime/browser/screenshot/renderers/html2canvas.js.map +1 -1
- package/dist/src/runtime/browser/screenshot/targets.d.ts +2 -2
- package/dist/src/runtime/browser/screenshot/targets.js +9 -9
- package/dist/src/runtime/browser/screenshot/utils.d.ts +1 -1
- package/dist/src/runtime/browser/screenshot/utils.d.ts.map +1 -1
- package/dist/src/runtime/browser/screenshot/utils.js +73 -71
- package/dist/src/runtime/browser/screenshot/utils.js.map +1 -1
- package/dist/src/runtime/browser/selector-discovery.d.ts +1 -1
- package/dist/src/runtime/browser/selector-discovery.d.ts.map +1 -1
- package/dist/src/runtime/browser/selector-discovery.js +24 -22
- package/dist/src/runtime/browser/selector-discovery.js.map +1 -1
- package/dist/src/runtime/browser/storage/session-storage.d.ts +1 -1
- package/dist/src/runtime/browser/storage/session-storage.d.ts.map +1 -1
- package/dist/src/runtime/browser/storage/session-storage.js +12 -10
- package/dist/src/runtime/browser/storage/session-storage.js.map +1 -1
- package/dist/src/runtime/browser/types.d.ts +7 -7
- package/dist/src/runtime/browser/types.js +1 -1
- package/dist/src/runtime/browser/utils/console.d.ts +1 -1
- package/dist/src/runtime/browser/utils/console.js +16 -16
- package/dist/src/runtime/browser/utils/console.js.map +1 -1
- package/dist/src/runtime/browser/utils/environment.js +2 -2
- package/dist/src/runtime/browser/utils/errors.d.ts.map +1 -1
- package/dist/src/runtime/browser/utils/errors.js +11 -11
- package/dist/src/runtime/browser/utils/number.d.ts.map +1 -1
- package/dist/src/runtime/browser/utils/number.js.map +1 -1
- package/dist/src/runtime/browser/utils/object.d.ts.map +1 -1
- package/dist/src/runtime/browser/utils/object.js +2 -2
- package/dist/src/runtime/browser/utils/object.js.map +1 -1
- package/dist/src/runtime/browser/utils/sanitize.js +2 -2
- package/dist/src/runtime/browser/utils/time.d.ts.map +1 -1
- package/dist/src/runtime/browser/utils/time.js +1 -1
- package/dist/src/runtime/chrome/cookies.d.ts +4 -4
- package/dist/src/runtime/chrome/cookies.d.ts.map +1 -1
- package/dist/src/runtime/chrome/cookies.js +31 -31
- package/dist/src/runtime/chrome/cookies.js.map +1 -1
- package/dist/src/runtime/chrome/deep-link.d.ts +13 -0
- package/dist/src/runtime/chrome/deep-link.d.ts.map +1 -0
- package/dist/src/runtime/chrome/deep-link.js +292 -0
- package/dist/src/runtime/chrome/deep-link.js.map +1 -0
- package/dist/src/runtime/chrome/diagnostics.d.ts.map +1 -1
- package/dist/src/runtime/chrome/diagnostics.js +13 -11
- package/dist/src/runtime/chrome/diagnostics.js.map +1 -1
- package/dist/src/runtime/chrome/focus.d.ts.map +1 -1
- package/dist/src/runtime/chrome/focus.js +5 -5
- package/dist/src/runtime/chrome/focus.js.map +1 -1
- package/dist/src/runtime/chrome/launch.d.ts.map +1 -1
- package/dist/src/runtime/chrome/launch.js +30 -30
- package/dist/src/runtime/chrome/launch.js.map +1 -1
- package/dist/src/runtime/chrome/puppeteer.d.ts +2 -2
- package/dist/src/runtime/chrome/puppeteer.d.ts.map +1 -1
- package/dist/src/runtime/chrome/puppeteer.js +16 -11
- package/dist/src/runtime/chrome/puppeteer.js.map +1 -1
- package/dist/src/runtime/chrome/reuse.d.ts.map +1 -1
- package/dist/src/runtime/chrome/reuse.js +38 -35
- package/dist/src/runtime/chrome/reuse.js.map +1 -1
- package/dist/src/runtime/chrome/session.d.ts +1 -1
- package/dist/src/runtime/chrome/session.d.ts.map +1 -1
- package/dist/src/runtime/chrome/session.js +12 -10
- package/dist/src/runtime/chrome/session.js.map +1 -1
- package/dist/src/runtime/chrome.d.ts +8 -8
- package/dist/src/runtime/chrome.js +8 -8
- package/dist/src/runtime/cookies.d.ts +1 -16
- package/dist/src/runtime/cookies.d.ts.map +1 -1
- package/dist/src/runtime/cookies.js +177 -281
- package/dist/src/runtime/cookies.js.map +1 -1
- package/dist/src/runtime/devstack.d.ts +1 -1
- package/dist/src/runtime/devstack.d.ts.map +1 -1
- package/dist/src/runtime/devstack.js +31 -28
- package/dist/src/runtime/devstack.js.map +1 -1
- package/dist/src/runtime/devtools/background.js +22 -22
- package/dist/src/runtime/devtools/cdp.d.ts +3 -2
- package/dist/src/runtime/devtools/cdp.d.ts.map +1 -1
- package/dist/src/runtime/devtools/cdp.js +61 -56
- package/dist/src/runtime/devtools/cdp.js.map +1 -1
- package/dist/src/runtime/devtools/config.d.ts +44 -1
- package/dist/src/runtime/devtools/config.d.ts.map +1 -1
- package/dist/src/runtime/devtools/config.js +15 -15
- package/dist/src/runtime/devtools/config.js.map +1 -1
- package/dist/src/runtime/devtools/constants.d.ts.map +1 -1
- package/dist/src/runtime/devtools/constants.js +18 -10
- package/dist/src/runtime/devtools/constants.js.map +1 -1
- package/dist/src/runtime/devtools/diagnostics.d.ts +6 -3
- package/dist/src/runtime/devtools/diagnostics.d.ts.map +1 -1
- package/dist/src/runtime/devtools/diagnostics.js +40 -38
- package/dist/src/runtime/devtools/diagnostics.js.map +1 -1
- package/dist/src/runtime/devtools/oauth.d.ts +1 -1
- package/dist/src/runtime/devtools/oauth.d.ts.map +1 -1
- package/dist/src/runtime/devtools/oauth.js +35 -28
- package/dist/src/runtime/devtools/oauth.js.map +1 -1
- package/dist/src/runtime/devtools/types.d.ts +3 -48
- package/dist/src/runtime/devtools/types.d.ts.map +1 -1
- package/dist/src/runtime/devtools.d.ts +7 -7
- package/dist/src/runtime/devtools.js +7 -7
- package/dist/src/runtime/next-devtools.d.ts.map +1 -1
- package/dist/src/runtime/next-devtools.js +12 -12
- package/dist/src/runtime/next-devtools.js.map +1 -1
- package/dist/src/runtime/screenshot.d.ts +9 -9
- package/dist/src/runtime/screenshot.d.ts.map +1 -1
- package/dist/src/runtime/screenshot.js +65 -46
- package/dist/src/runtime/screenshot.js.map +1 -1
- package/dist/src/runtime/scripts.d.ts +1 -1
- package/dist/src/runtime/scripts.d.ts.map +1 -1
- package/dist/src/runtime/scripts.js +28 -28
- package/dist/src/runtime/scripts.js.map +1 -1
- package/dist/src/runtime/session.d.ts +6 -6
- package/dist/src/runtime/session.d.ts.map +1 -1
- package/dist/src/runtime/session.js +35 -35
- package/dist/src/runtime/session.js.map +1 -1
- package/dist/src/runtime/smoke.d.ts +3 -3
- package/dist/src/runtime/smoke.d.ts.map +1 -1
- package/dist/src/runtime/smoke.js +85 -79
- package/dist/src/runtime/smoke.js.map +1 -1
- package/dist/src/runtime/url.d.ts +2 -2
- package/dist/src/runtime/url.d.ts.map +1 -1
- package/dist/src/runtime/url.js +20 -20
- package/dist/src/runtime/url.js.map +1 -1
- package/dist/src/screenshot-hooks.d.ts +1 -1
- package/dist/src/screenshot-hooks.d.ts.map +1 -1
- package/dist/src/screenshot-hooks.js +12 -12
- package/dist/src/screenshot-hooks.js.map +1 -1
- package/dist/src/shared/env.d.ts.map +1 -1
- package/dist/src/shared/env.js +11 -11
- package/dist/src/shared/env.js.map +1 -1
- package/dist/src/shared/index.d.ts +30 -30
- package/dist/src/shared/index.d.ts.map +1 -1
- package/dist/src/shared/index.js +17 -17
- package/dist/src/shared/index.js.map +1 -1
- package/dist/src/shared/node.d.ts +1 -1
- package/dist/src/shared/node.d.ts.map +1 -1
- package/dist/src/shared/node.js +18 -18
- package/dist/src/shared/node.js.map +1 -1
- package/dist/src/token.d.ts +1 -1
- package/dist/src/token.d.ts.map +1 -1
- package/dist/src/token.js +21 -18
- package/dist/src/token.js.map +1 -1
- package/dist/src/types.d.ts +7 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/util/app-label.js +4 -4
- package/dist/src/util/errors.d.ts.map +1 -1
- package/dist/src/util/errors.js +12 -10
- package/dist/src/util/errors.js.map +1 -1
- package/dist/src/util/path.js +2 -2
- package/dist/src/util/regex.js +4 -4
- package/package.json +48 -40
package/dist/src/index.js
CHANGED
|
@@ -1,42 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync } from
|
|
3
|
-
import { mkdir, readFile, rm } from
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { Command, CommanderError, Option } from "commander";
|
|
9
|
+
import { compact, uniq } from "es-toolkit";
|
|
10
|
+
import { createSweetLinkCommandId, } from "../shared/src/index.js";
|
|
11
|
+
import { registerClickCommand } from "./commands/click.js";
|
|
12
|
+
import { registerRunJsCommand } from "./commands/run-js.js";
|
|
13
|
+
import { registerTrustCaCommand } from "./commands/trust-ca.js";
|
|
14
|
+
import { readRootProgramOptions, resolveConfig } from "./core/config.js";
|
|
15
|
+
import { loadSweetLinkFileConfig } from "./core/config-file.js";
|
|
16
|
+
import { cloneProcessEnv, readCommandOptions } from "./core/env.js";
|
|
17
|
+
import { cleanupControlledChromeRegistry, registerControlledChromeInstance, } from "./devtools-registry.js";
|
|
18
|
+
import { sweetLinkCliTestMode, sweetLinkDebug, sweetLinkEnv } from "./env.js";
|
|
19
|
+
import { fetchJson } from "./http.js";
|
|
20
|
+
import { collectPuppeteerDiagnostics, focusControlledChromePage, launchChrome, launchControlledChrome, prepareChromeLaunch, reuseExistingControlledChrome, signalSweetLinkBootstrap, waitForSweetLinkSession, } from "./runtime/chrome.js";
|
|
21
|
+
import { ensureDeepLinkAuthFlow } from "./runtime/chrome/deep-link.js";
|
|
22
|
+
import { buildCookieOrigins, collectChromeCookies, collectChromeCookiesForDomains, normalizePuppeteerCookie, } from "./runtime/cookies.js";
|
|
23
|
+
import { ensureDevStackRunning as ensureDevStackRunningRuntime, isAppReachable as isAppReachableRuntime, maybeInstallMkcertDispatcher, } from "./runtime/devstack.js";
|
|
24
|
+
import { attemptTwitterOauthAutoAccept, collectBootstrapDiagnostics, connectToDevTools, createEmptyDevToolsState, createNetworkEntryFromRequest, DEVTOOLS_CONSOLE_LIMIT, DEVTOOLS_NETWORK_LIMIT, DEVTOOLS_STATE_PATH, deriveDevtoolsLinkInfo, diagnosticsContainBlockingIssues, ensureBackgroundDevtoolsListener, fetchDevToolsTabs, formatConsoleArg, loadDevToolsConfig, loadDevToolsState, logBootstrapDiagnostics, saveDevToolsConfig, saveDevToolsState, serializeConsoleMessage, trimBuffer, } from "./runtime/devtools.js";
|
|
25
|
+
import { fetchNextDevtoolsErrors } from "./runtime/next-devtools.js";
|
|
26
|
+
import { attemptDevToolsCapture, maybeDescribeScreenshot, persistScreenshotResult, tryDevToolsRecovery, tryHtmlToImageFallback, } from "./runtime/screenshot.js";
|
|
27
|
+
import { renderCommandResult } from "./runtime/scripts.js";
|
|
28
|
+
import { buildClickScript, fetchConsoleEvents, fetchSessionSummaries, formatSessionHeadline, getSessionSummaryById, isSweetLinkSelectorCandidate, isSweetLinkSelectorDiscoveryResult, resolvePromptOption, resolveSessionIdFromHint, } from "./runtime/session.js";
|
|
29
|
+
import { buildSmokeRouteUrl, clearSmokeProgress, consoleEventIndicatesAuthIssue, consoleEventIndicatesRuntimeError, DEFAULT_SMOKE_ROUTES, deriveSmokeRoutes, ensureSweetLinkSessionConnected, formatConsoleEventSummary, loadSmokeProgressIndex, navigateSweetLinkSession, saveSmokeProgressIndex, triggerSweetLinkCliAuto, waitForSmokeRouteReady, } from "./runtime/smoke.js";
|
|
30
|
+
import { buildWaitCandidateUrls, configurePathRedirects, normalizeUrlForMatch, trimTrailingSlash, } from "./runtime/url.js";
|
|
31
|
+
import { buildScreenshotHooks } from "./screenshot-hooks.js";
|
|
32
|
+
import { fetchCliToken } from "./token.js";
|
|
33
|
+
import { describeAppForPrompt, formatAppLabel } from "./util/app-label.js";
|
|
34
|
+
import { extractEventMessage, isErrnoException, logDebugError } from "./util/errors.js";
|
|
33
35
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
36
|
const __dirname = path.dirname(__filename);
|
|
35
37
|
function resolveRepoRoot(startDir) {
|
|
36
38
|
let currentDir = startDir;
|
|
37
39
|
// Walk up until a package.json exists or we reach the filesystem root.
|
|
38
40
|
// This keeps the CLI working whether we run from `src` (tsx) or `dist/src`.
|
|
39
|
-
while (!existsSync(path.join(currentDir,
|
|
41
|
+
while (!existsSync(path.join(currentDir, "package.json"))) {
|
|
40
42
|
const parent = path.dirname(currentDir);
|
|
41
43
|
if (parent === currentDir) {
|
|
42
44
|
return startDir;
|
|
@@ -46,11 +48,11 @@ function resolveRepoRoot(startDir) {
|
|
|
46
48
|
return currentDir;
|
|
47
49
|
}
|
|
48
50
|
const repoRoot = resolveRepoRoot(__dirname);
|
|
49
|
-
const packageJsonPath = path.join(repoRoot,
|
|
50
|
-
let packageVersion =
|
|
51
|
+
const packageJsonPath = path.join(repoRoot, "package.json");
|
|
52
|
+
let packageVersion = "0.0.0";
|
|
51
53
|
try {
|
|
52
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath,
|
|
53
|
-
if (typeof packageJson.version ===
|
|
54
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
55
|
+
if (typeof packageJson.version === "string") {
|
|
54
56
|
packageVersion = packageJson.version;
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -63,7 +65,7 @@ const OAUTH_URL_PATTERN = /oauth|authorize/i;
|
|
|
63
65
|
const TRAILING_SLASH_PATTERN = /\/?$/;
|
|
64
66
|
function formatDuration(ms) {
|
|
65
67
|
if (!Number.isFinite(ms)) {
|
|
66
|
-
return
|
|
68
|
+
return "unknown";
|
|
67
69
|
}
|
|
68
70
|
const abs = Math.max(0, ms);
|
|
69
71
|
if (abs < 1000) {
|
|
@@ -90,11 +92,11 @@ function formatDuration(ms) {
|
|
|
90
92
|
}
|
|
91
93
|
const program = new Command();
|
|
92
94
|
program
|
|
93
|
-
.name(
|
|
94
|
-
.description(
|
|
95
|
-
.version(packageVersion,
|
|
95
|
+
.name("sweetlink")
|
|
96
|
+
.description("Interact with SweetLink daemon sessions")
|
|
97
|
+
.version(packageVersion, "-v, --version", "Show SweetLink CLI version");
|
|
96
98
|
maybeInstallMkcertDispatcher();
|
|
97
|
-
const LOOSE_PATH_SUFFIXES = new Set([
|
|
99
|
+
const LOOSE_PATH_SUFFIXES = new Set(["home", "index", "overview"]);
|
|
98
100
|
const { config: fileConfig } = loadSweetLinkFileConfig();
|
|
99
101
|
configurePathRedirects(fileConfig.redirects);
|
|
100
102
|
const { appUrl: envAppUrl, daemonUrl: envDaemonUrl, localAdminApiKey, adminApiKey: sharedAdminApiKey, prodAppUrl: envProdAppUrl, } = sweetLinkEnv;
|
|
@@ -102,15 +104,15 @@ const defaultAppLabel = formatAppLabel(fileConfig.appLabel ?? sweetLinkEnv.appLa
|
|
|
102
104
|
const defaultAppUrl = deriveDefaultAppUrl(envAppUrl, fileConfig);
|
|
103
105
|
const defaultProdAppUrl = fileConfig.prodUrl ?? envProdAppUrl;
|
|
104
106
|
const defaultDaemonUrl = fileConfig.daemonUrl ?? envDaemonUrl;
|
|
105
|
-
const defaultAdminKey = fileConfig.adminKey ?? localAdminApiKey ?? sharedAdminApiKey ??
|
|
107
|
+
const defaultAdminKey = fileConfig.adminKey ?? localAdminApiKey ?? sharedAdminApiKey ?? "";
|
|
106
108
|
const defaultHealthPaths = fileConfig.healthChecks?.paths ?? null;
|
|
107
|
-
const LOCAL_DEFAULT_APP_URL =
|
|
109
|
+
const LOCAL_DEFAULT_APP_URL = "http://localhost:3000";
|
|
108
110
|
function deriveDefaultAppUrl(envUrl, config) {
|
|
109
|
-
const configuredAppUrl = typeof config.appUrl ===
|
|
111
|
+
const configuredAppUrl = typeof config.appUrl === "string" ? config.appUrl.trim() : "";
|
|
110
112
|
if (configuredAppUrl.length > 0) {
|
|
111
113
|
return configuredAppUrl;
|
|
112
114
|
}
|
|
113
|
-
if (typeof config.port ===
|
|
115
|
+
if (typeof config.port === "number" && Number.isFinite(config.port) && config.port > 0) {
|
|
114
116
|
const baseUrl = envUrl ?? LOCAL_DEFAULT_APP_URL;
|
|
115
117
|
return applyPortToUrl(baseUrl, config.port);
|
|
116
118
|
}
|
|
@@ -129,22 +131,22 @@ function applyPortToUrl(base, port) {
|
|
|
129
131
|
function parseCliPort(value) {
|
|
130
132
|
const parsed = Number.parseInt(value, 10);
|
|
131
133
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
132
|
-
throw new CommanderError(1,
|
|
134
|
+
throw new CommanderError(1, "InvalidPort", `--port expects a positive integer, received "${value}".`);
|
|
133
135
|
}
|
|
134
136
|
return parsed;
|
|
135
137
|
}
|
|
136
138
|
program
|
|
137
|
-
.option(
|
|
138
|
-
.option(
|
|
139
|
-
.option(
|
|
140
|
-
.addOption(new Option(
|
|
141
|
-
.option(
|
|
142
|
-
.option(
|
|
143
|
-
.option(
|
|
139
|
+
.option("-a, --app-url <url>", "Application base URL for SweetLink commands", defaultAppUrl)
|
|
140
|
+
.option("--app-label <label>", "Friendly name for your application (used in help output)", defaultAppLabel)
|
|
141
|
+
.option("--url <url>", "Alias for --app-url")
|
|
142
|
+
.addOption(new Option("--port <number>", "Override local app port (defaults to config or 3000)").argParser(parseCliPort))
|
|
143
|
+
.option("-d, --daemon-url <url>", "SweetLink daemon base URL", defaultDaemonUrl)
|
|
144
|
+
.option("-k, --admin-key <key>", "Optional admin API key (defaults to SWEETLINK_LOCAL_ADMIN_API_KEY or SWEETLINK_ADMIN_API_KEY env; falls back to legacy SWEETISTICS_* keys)", defaultAdminKey)
|
|
145
|
+
.option("--oauth-script <path>", "Absolute or relative path to an OAuth automation script (ESM module). Overrides config/env defaults.");
|
|
144
146
|
program
|
|
145
|
-
.command(
|
|
146
|
-
.description(
|
|
147
|
-
.option(
|
|
147
|
+
.command("sessions")
|
|
148
|
+
.description("List active SweetLink sessions")
|
|
149
|
+
.option("--json", "Output JSON instead of a table", false)
|
|
148
150
|
.action(async (options, command) => {
|
|
149
151
|
const config = resolveConfig(command);
|
|
150
152
|
const token = await fetchCliToken(config);
|
|
@@ -167,15 +169,17 @@ program
|
|
|
167
169
|
return;
|
|
168
170
|
}
|
|
169
171
|
if (sessions.length === 0) {
|
|
170
|
-
console.log(
|
|
171
|
-
console.log(
|
|
172
|
+
console.log("No active SweetLink sessions.");
|
|
173
|
+
console.log("Hint: run `pnpm sweetlink open --controlled --path /` to launch an authenticated tab automatically.");
|
|
172
174
|
return;
|
|
173
175
|
}
|
|
174
176
|
const now = Date.now();
|
|
175
|
-
console.log(
|
|
177
|
+
console.log("Active SweetLink sessions:\n");
|
|
176
178
|
for (const session of sessions) {
|
|
177
|
-
const heartbeatMsAgo = typeof session.heartbeatMsAgo ===
|
|
178
|
-
|
|
179
|
+
const heartbeatMsAgo = typeof session.heartbeatMsAgo === "number"
|
|
180
|
+
? session.heartbeatMsAgo
|
|
181
|
+
: Math.max(0, now - session.lastSeenAt);
|
|
182
|
+
const socketState = session.socketState ?? "unknown";
|
|
179
183
|
const consoleEventsBuffered = session.consoleEventsBuffered ?? 0;
|
|
180
184
|
const consoleErrorsBuffered = session.consoleErrorsBuffered ?? 0;
|
|
181
185
|
const pendingCommandCount = session.pendingCommandCount ?? 0;
|
|
@@ -186,30 +190,30 @@ program
|
|
|
186
190
|
const openedLabel = `${formatDuration(openedMsAgo)} ago`;
|
|
187
191
|
const socketLabel = `socket ${socketState}`;
|
|
188
192
|
console.log(` - last: ${lastHeartbeatLabel} • opened ${openedLabel} • ${socketLabel}`);
|
|
189
|
-
const pendingLabel = pendingCommandCount === 1 ?
|
|
193
|
+
const pendingLabel = pendingCommandCount === 1 ? "command" : "commands";
|
|
190
194
|
const consoleLabel = consoleEventsBuffered > 0
|
|
191
|
-
? `${consoleEventsBuffered} event${consoleEventsBuffered === 1 ?
|
|
192
|
-
:
|
|
195
|
+
? `${consoleEventsBuffered} event${consoleEventsBuffered === 1 ? "" : "s"}${consoleErrorsBuffered ? ` (${consoleErrorsBuffered} error${consoleErrorsBuffered === 1 ? "" : "s"})` : ""}`
|
|
196
|
+
: "none";
|
|
193
197
|
console.log(` - queues: ${pendingCommandCount} ${pendingLabel} • console ${consoleLabel}`);
|
|
194
198
|
if (devtoolsSessionIds.has(session.sessionId)) {
|
|
195
|
-
console.log(` - devtools: linked${devtoolsEndpoint ? ` (${devtoolsEndpoint})` :
|
|
199
|
+
console.log(` - devtools: linked${devtoolsEndpoint ? ` (${devtoolsEndpoint})` : ""}`);
|
|
196
200
|
}
|
|
197
201
|
if (session.userAgent) {
|
|
198
202
|
console.log(` - ua: ${session.userAgent}`);
|
|
199
203
|
}
|
|
200
|
-
console.log(
|
|
204
|
+
console.log("");
|
|
201
205
|
}
|
|
202
|
-
console.log(
|
|
206
|
+
console.log("Tip: run `pnpm sweetlink console <sessionId> -n 50` to inspect the most recent console events for a session.");
|
|
203
207
|
});
|
|
204
208
|
program
|
|
205
|
-
.command(
|
|
206
|
-
.description(
|
|
207
|
-
.argument(
|
|
208
|
-
.option(
|
|
209
|
+
.command("cookies")
|
|
210
|
+
.description("Dump Chrome cookies for one or more domains or origins")
|
|
211
|
+
.argument("<domains...>", "Domains or fully-qualified origins (e.g. localhost, https://example.com)")
|
|
212
|
+
.option("--json", "Output JSON instead of a human-readable list", false)
|
|
209
213
|
.action(async (domains, options) => {
|
|
210
214
|
const uniqueDomains = uniq(compact(domains.map((domain) => domain.trim())));
|
|
211
215
|
if (uniqueDomains.length === 0) {
|
|
212
|
-
console.log(
|
|
216
|
+
console.log("No domains provided; nothing to collect.");
|
|
213
217
|
return;
|
|
214
218
|
}
|
|
215
219
|
const cookiesByDomain = await collectChromeCookiesForDomains(uniqueDomains);
|
|
@@ -220,29 +224,29 @@ program
|
|
|
220
224
|
for (const domain of uniqueDomains) {
|
|
221
225
|
const domainCookies = cookiesByDomain[domain];
|
|
222
226
|
const cookies = Array.isArray(domainCookies) ? domainCookies : [];
|
|
223
|
-
console.log(`\n${domain} — ${cookies.length} cookie${cookies.length === 1 ?
|
|
227
|
+
console.log(`\n${domain} — ${cookies.length} cookie${cookies.length === 1 ? "" : "s"}`);
|
|
224
228
|
if (cookies.length === 0) {
|
|
225
229
|
continue;
|
|
226
230
|
}
|
|
227
231
|
for (const cookie of cookies) {
|
|
228
|
-
const scope = cookie.domain ? `domain=${cookie.domain}` : `url=${cookie.url ??
|
|
229
|
-
const cookiePath = cookie.path ??
|
|
230
|
-
const secureFlag = cookie.secure ?
|
|
231
|
-
const httpOnlyFlag = cookie.httpOnly ?
|
|
232
|
+
const scope = cookie.domain ? `domain=${cookie.domain}` : `url=${cookie.url ?? "unknown"}`;
|
|
233
|
+
const cookiePath = cookie.path ?? "/";
|
|
234
|
+
const secureFlag = cookie.secure ? "; Secure" : "";
|
|
235
|
+
const httpOnlyFlag = cookie.httpOnly ? "; HttpOnly" : "";
|
|
232
236
|
console.log(` • ${cookie.name} (${scope} ${cookiePath}${secureFlag}${httpOnlyFlag})`);
|
|
233
237
|
console.log(` ${cookie.value}`);
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
|
-
console.log(
|
|
240
|
+
console.log("");
|
|
237
241
|
});
|
|
238
242
|
registerRunJsCommand(program);
|
|
239
243
|
registerTrustCaCommand(program);
|
|
240
244
|
registerClickCommand(program);
|
|
241
245
|
program
|
|
242
|
-
.command(
|
|
243
|
-
.description(
|
|
244
|
-
.option(
|
|
245
|
-
.option(
|
|
246
|
+
.command("console <sessionId>")
|
|
247
|
+
.description("Fetch buffered console events for a session")
|
|
248
|
+
.option("-n, --limit <count>", "Show only the last <count> console events", Number)
|
|
249
|
+
.option("--json", "Output JSON", false)
|
|
246
250
|
.action(async (sessionId, options, command) => {
|
|
247
251
|
const config = resolveConfig(command);
|
|
248
252
|
const resolvedSessionId = await resolveSessionIdFromHint(sessionId, config);
|
|
@@ -255,10 +259,10 @@ program
|
|
|
255
259
|
return;
|
|
256
260
|
}
|
|
257
261
|
if (response.events.length === 0) {
|
|
258
|
-
console.log(
|
|
262
|
+
console.log("No buffered console output.");
|
|
259
263
|
return;
|
|
260
264
|
}
|
|
261
|
-
const limit = typeof options.limit ===
|
|
265
|
+
const limit = typeof options.limit === "number" && Number.isFinite(options.limit) && options.limit > 0
|
|
262
266
|
? Math.floor(options.limit)
|
|
263
267
|
: null;
|
|
264
268
|
const events = limit ? response.events.slice(-limit) : response.events;
|
|
@@ -270,35 +274,43 @@ program
|
|
|
270
274
|
}
|
|
271
275
|
});
|
|
272
276
|
program
|
|
273
|
-
.command(
|
|
277
|
+
.command("open")
|
|
274
278
|
.description(`Open ${defaultAppLabel} in Chrome with SweetLink auto-enabled`)
|
|
275
|
-
.option(
|
|
276
|
-
.option(
|
|
277
|
-
.option(
|
|
278
|
-
.option(
|
|
279
|
-
.option(
|
|
280
|
-
.option(
|
|
281
|
-
.option(
|
|
282
|
-
.option(
|
|
283
|
-
.option(
|
|
284
|
-
.option(
|
|
279
|
+
.option("-e, --env <env>", "Environment to open (dev or prod)", "dev")
|
|
280
|
+
.option("-p, --path <path>", 'Optional path to append (default "")', "")
|
|
281
|
+
.option("--url <url>", "Explicit URL to open (overrides --path and --env)")
|
|
282
|
+
.option("--controlled", "Launch Chrome in controlled mode with DevTools enabled", false)
|
|
283
|
+
.option("--devtools-port <port>", "Specify DevTools port to use with --controlled", Number)
|
|
284
|
+
.option("--no-cookie-sync", "Disable copying cookies from your main Chrome profile into the controlled window", false)
|
|
285
|
+
.option("--timeout <seconds>", "Seconds to wait for a SweetLink session (default 15)", Number)
|
|
286
|
+
.option("--no-devtools", "Skip DevTools automation when opening in controlled mode")
|
|
287
|
+
.option("--headless", "Launch the controlled browser headlessly", false)
|
|
288
|
+
.option("--foreground", "Bring the Chrome window to the foreground after opening (macOS only)", false)
|
|
285
289
|
.action(async (options, command) => {
|
|
286
290
|
await runOpenCommand(options, command, program);
|
|
287
291
|
});
|
|
292
|
+
program
|
|
293
|
+
.command("daemon")
|
|
294
|
+
.description("Launch the SweetLink daemon process")
|
|
295
|
+
.argument("[args...]", "Arguments forwarded to sweetlinkd")
|
|
296
|
+
.allowUnknownOption(true)
|
|
297
|
+
.action((_args, command) => {
|
|
298
|
+
runDaemonCommand(command);
|
|
299
|
+
});
|
|
288
300
|
async function runOpenCommand(options, command, rootProgram) {
|
|
289
301
|
const context = buildOpenCommandContext(options, command, rootProgram);
|
|
290
302
|
if (!context.controlled) {
|
|
291
303
|
if (options.devtools === false) {
|
|
292
|
-
console.log(
|
|
304
|
+
console.log("--no-devtools is ignored when launching an uncontrolled browser window.");
|
|
293
305
|
}
|
|
294
306
|
if (options.headless) {
|
|
295
|
-
console.log(
|
|
307
|
+
console.log("--headless requires --controlled; launching a regular Chrome window instead.");
|
|
296
308
|
}
|
|
297
309
|
}
|
|
298
310
|
if (context.headless && context.foreground) {
|
|
299
|
-
console.log(
|
|
311
|
+
console.log("--foreground is ignored in headless mode.");
|
|
300
312
|
}
|
|
301
|
-
if (context.env ===
|
|
313
|
+
if (context.env === "dev") {
|
|
302
314
|
await ensureDevStackRunningRuntime(context.targetUrl, {
|
|
303
315
|
repoRoot,
|
|
304
316
|
healthPaths: context.healthCheckPaths ?? undefined,
|
|
@@ -318,6 +330,29 @@ async function runOpenCommand(options, command, rootProgram) {
|
|
|
318
330
|
}
|
|
319
331
|
await handleUncontrolledOpen(context, waitToken);
|
|
320
332
|
}
|
|
333
|
+
function runDaemonCommand(command) {
|
|
334
|
+
const parent = command.parent;
|
|
335
|
+
const rawArgs = parent?.rawArgs ?? process.argv;
|
|
336
|
+
const daemonIndex = rawArgs.findIndex((arg) => arg === "daemon");
|
|
337
|
+
const afterDaemon = daemonIndex === -1 ? [] : rawArgs.slice(daemonIndex + 1);
|
|
338
|
+
const passthroughIndex = afterDaemon.indexOf("--");
|
|
339
|
+
const forwarded = passthroughIndex >= 0
|
|
340
|
+
? afterDaemon.slice(passthroughIndex + 1)
|
|
341
|
+
: (command.args ?? []).filter((value) => typeof value === "string");
|
|
342
|
+
const child = spawn("sweetlinkd", forwarded, { stdio: "inherit", env: cloneProcessEnv() });
|
|
343
|
+
child.on("error", (error) => {
|
|
344
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
345
|
+
console.error('Unable to find "sweetlinkd" on PATH. Try `pnpm exec sweetlinkd` instead.');
|
|
346
|
+
process.exitCode = 1;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
console.error("Failed to launch sweetlinkd:", extractEventMessage(error));
|
|
350
|
+
process.exitCode = 1;
|
|
351
|
+
});
|
|
352
|
+
child.on("exit", (code) => {
|
|
353
|
+
process.exitCode = code ?? 0;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
321
356
|
function buildOpenCommandContext(options, command, rootProgram) {
|
|
322
357
|
const config = resolveConfig(command);
|
|
323
358
|
const env = normalizeOpenCommandEnvironment(options.env);
|
|
@@ -325,11 +360,11 @@ function buildOpenCommandContext(options, command, rootProgram) {
|
|
|
325
360
|
const parentOptions = readRootProgramOptions(parent);
|
|
326
361
|
const developmentBaseUrl = parentOptions.appUrl;
|
|
327
362
|
const productionBaseUrl = defaultProdAppUrl;
|
|
328
|
-
const baseUrl = env ===
|
|
363
|
+
const baseUrl = env === "prod" ? productionBaseUrl : developmentBaseUrl;
|
|
329
364
|
const explicitTarget = resolveExplicitTargetUrl(options.url);
|
|
330
365
|
const targetUrl = explicitTarget ?? buildOpenCommandTargetUrl(baseUrl, options.path);
|
|
331
366
|
const serverConfig = config.servers[env] ?? null;
|
|
332
|
-
const preferredPort = typeof options.devtoolsPort ===
|
|
367
|
+
const preferredPort = typeof options.devtoolsPort === "number" && Number.isFinite(options.devtoolsPort)
|
|
333
368
|
? options.devtoolsPort
|
|
334
369
|
: undefined;
|
|
335
370
|
const controlled = Boolean(options.controlled);
|
|
@@ -355,8 +390,8 @@ function buildOpenCommandContext(options, command, rootProgram) {
|
|
|
355
390
|
};
|
|
356
391
|
}
|
|
357
392
|
function normalizeOpenCommandEnvironment(value) {
|
|
358
|
-
const normalized = (value ??
|
|
359
|
-
if (normalized ===
|
|
393
|
+
const normalized = (value ?? "dev").trim().toLowerCase();
|
|
394
|
+
if (normalized === "dev" || normalized === "prod") {
|
|
360
395
|
return normalized;
|
|
361
396
|
}
|
|
362
397
|
throw new Error('Invalid environment. Use "dev" or "prod".');
|
|
@@ -370,13 +405,13 @@ function buildOpenCommandTargetUrl(baseUrl, rawPath) {
|
|
|
370
405
|
const message = error instanceof Error ? error.message : String(error);
|
|
371
406
|
throw new Error(`Failed to parse base URL "${baseUrl}": ${message}`);
|
|
372
407
|
}
|
|
373
|
-
targetUrl.searchParams.set(
|
|
374
|
-
const fallbackPath =
|
|
408
|
+
targetUrl.searchParams.set("sweetlink", "auto");
|
|
409
|
+
const fallbackPath = "/timeline";
|
|
375
410
|
const trimmedPath = rawPath?.trim();
|
|
376
411
|
const pathSource = trimmedPath && trimmedPath.length > 0 ? trimmedPath : fallbackPath;
|
|
377
|
-
const [pathPartRaw, queryPart] = pathSource.split(
|
|
412
|
+
const [pathPartRaw, queryPart] = pathSource.split("?", 2);
|
|
378
413
|
const pathPart = pathPartRaw && pathPartRaw.length > 0 ? pathPartRaw : fallbackPath;
|
|
379
|
-
targetUrl.pathname = pathPart.startsWith(
|
|
414
|
+
targetUrl.pathname = pathPart.startsWith("/") ? pathPart : `/${pathPart}`;
|
|
380
415
|
if (queryPart) {
|
|
381
416
|
const extraSearch = new URLSearchParams(queryPart);
|
|
382
417
|
for (const [key, value] of extraSearch.entries()) {
|
|
@@ -395,7 +430,7 @@ function resolveExplicitTargetUrl(raw) {
|
|
|
395
430
|
}
|
|
396
431
|
try {
|
|
397
432
|
const target = new URL(trimmed);
|
|
398
|
-
target.searchParams.set(
|
|
433
|
+
target.searchParams.set("sweetlink", target.searchParams.get("sweetlink") ?? "auto");
|
|
399
434
|
return target;
|
|
400
435
|
}
|
|
401
436
|
catch (error) {
|
|
@@ -404,14 +439,14 @@ function resolveExplicitTargetUrl(raw) {
|
|
|
404
439
|
}
|
|
405
440
|
}
|
|
406
441
|
function resolveCookieSyncPreference(command, cookieSyncOption) {
|
|
407
|
-
const source = command.getOptionValueSource?.(
|
|
408
|
-
if (source ===
|
|
442
|
+
const source = command.getOptionValueSource?.("cookieSync");
|
|
443
|
+
if (source === "default") {
|
|
409
444
|
return true;
|
|
410
445
|
}
|
|
411
446
|
return cookieSyncOption !== false;
|
|
412
447
|
}
|
|
413
448
|
function resolveOpenCommandTimeoutSeconds(value) {
|
|
414
|
-
if (typeof value ===
|
|
449
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
415
450
|
return Math.max(0, value);
|
|
416
451
|
}
|
|
417
452
|
return 30;
|
|
@@ -424,7 +459,7 @@ async function fetchWaitTokenIfNeeded(context) {
|
|
|
424
459
|
return await fetchCliToken(context.config);
|
|
425
460
|
}
|
|
426
461
|
catch (error) {
|
|
427
|
-
console.warn(
|
|
462
|
+
console.warn("Unable to fetch CLI token for session wait:", error);
|
|
428
463
|
return null;
|
|
429
464
|
}
|
|
430
465
|
}
|
|
@@ -446,60 +481,76 @@ async function handleControlledOpen(context, waitToken) {
|
|
|
446
481
|
await handleControlledReuse(context, waitToken, reuseResult);
|
|
447
482
|
return;
|
|
448
483
|
}
|
|
449
|
-
console.warn(
|
|
484
|
+
console.warn("No reusable controlled Chrome session matched the target; launching a fresh controlled window.");
|
|
450
485
|
await handleControlledLaunch(context, waitToken);
|
|
451
486
|
}
|
|
452
487
|
async function handleControlledReuse(context, waitToken, reuseResult) {
|
|
453
488
|
const shouldFocus = context.foreground && !context.headless;
|
|
489
|
+
let deepLinkResult = null;
|
|
454
490
|
if (context.enableDevtools) {
|
|
455
491
|
await registerControlledChromeInstance(reuseResult.devtoolsUrl, reuseResult.userDataDir);
|
|
456
492
|
await cleanupControlledChromeRegistry(reuseResult.devtoolsUrl);
|
|
457
493
|
await signalSweetLinkBootstrap(reuseResult.devtoolsUrl, context.targetUrlString);
|
|
458
494
|
try {
|
|
459
|
-
|
|
495
|
+
deepLinkResult = await ensureDeepLinkAuthFlow({
|
|
460
496
|
devtoolsUrl: reuseResult.devtoolsUrl,
|
|
461
|
-
|
|
462
|
-
|
|
497
|
+
targetUrl: context.targetUrlString,
|
|
498
|
+
oauthScriptPath: context.oauthScriptPath,
|
|
463
499
|
});
|
|
464
|
-
if (
|
|
465
|
-
console.log(
|
|
500
|
+
if (deepLinkResult.signInClicked) {
|
|
501
|
+
console.log("Triggered Sweetistics sign-in to reach the deep link.");
|
|
466
502
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
:
|
|
471
|
-
|
|
503
|
+
const oauthAttempt = deepLinkResult.oauthAttempt;
|
|
504
|
+
if (oauthAttempt) {
|
|
505
|
+
if (oauthAttempt.handled) {
|
|
506
|
+
console.log(`Automatically approved the OAuth prompt via ${oauthAttempt.action ?? "click"}${oauthAttempt.clickedText ? ` (${oauthAttempt.clickedText})` : ""}.`);
|
|
507
|
+
}
|
|
508
|
+
else if (oauthAttempt.reason && oauthAttempt.reason !== "button-not-found") {
|
|
509
|
+
const locationHint = oauthAttempt.url || oauthAttempt.title || oauthAttempt.host
|
|
510
|
+
? ` (at ${oauthAttempt.title ?? oauthAttempt.host ?? "unknown page"} ${oauthAttempt.url ?? ""})`
|
|
511
|
+
: "";
|
|
512
|
+
console.log(`OAuth auto-accept skipped: ${oauthAttempt.reason}${locationHint}.`);
|
|
513
|
+
}
|
|
472
514
|
}
|
|
473
515
|
}
|
|
474
516
|
catch (error) {
|
|
475
517
|
if (sweetLinkDebug) {
|
|
476
|
-
console.warn(
|
|
518
|
+
console.warn("Deep-link auth flow failed:", error);
|
|
477
519
|
}
|
|
478
520
|
}
|
|
479
521
|
}
|
|
480
522
|
else {
|
|
481
|
-
console.log(
|
|
523
|
+
console.log("DevTools automation disabled (--no-devtools); skipping telemetry bootstrap and OAuth auto-click.");
|
|
524
|
+
}
|
|
525
|
+
if (context.enableDevtools && !deepLinkResult?.navigatedToTarget) {
|
|
526
|
+
console.warn("Deep link did not resolve in reused controlled Chrome; launching a fresh controlled window.");
|
|
527
|
+
const freshContext = {
|
|
528
|
+
...context,
|
|
529
|
+
preferredPort: undefined,
|
|
530
|
+
};
|
|
531
|
+
await handleControlledLaunch(freshContext, waitToken);
|
|
532
|
+
return;
|
|
482
533
|
}
|
|
483
534
|
console.log(`Reused controlled Chrome at ${reuseResult.devtoolsUrl} (env: ${context.env}).`);
|
|
484
535
|
if (reuseResult.targetAlreadyOpen) {
|
|
485
|
-
console.log(
|
|
536
|
+
console.log("Target tab was already open; activated existing page.");
|
|
486
537
|
}
|
|
487
538
|
else {
|
|
488
|
-
console.log(
|
|
539
|
+
console.log("Opened a new tab in the existing controlled Chrome window.");
|
|
489
540
|
}
|
|
490
541
|
if (context.enableDevtools) {
|
|
491
|
-
console.log(
|
|
542
|
+
console.log("The screenshot command will continue to use this DevTools instance.");
|
|
492
543
|
}
|
|
493
544
|
if (shouldFocus && context.enableDevtools) {
|
|
494
545
|
const focused = await focusControlledChromePage(reuseResult.devtoolsUrl, context.targetUrlString);
|
|
495
546
|
if (!focused && sweetLinkDebug) {
|
|
496
|
-
console.warn(
|
|
547
|
+
console.warn("Unable to focus controlled Chrome window automatically.");
|
|
497
548
|
}
|
|
498
549
|
}
|
|
499
550
|
else if (context.foreground && !context.enableDevtools) {
|
|
500
|
-
console.log(
|
|
551
|
+
console.log("--foreground requires DevTools automation; skipping automatic focus.");
|
|
501
552
|
}
|
|
502
|
-
console.log(
|
|
553
|
+
console.log("Remember this session codename. Run `pnpm sweetlink sessions`, copy the session id or codename, and use that handle for every follow-up command instead of rerunning `pnpm sweetlink open`.");
|
|
503
554
|
await waitForSessionAfterOpen(context, waitToken, {
|
|
504
555
|
devtoolsUrl: context.enableDevtools ? reuseResult.devtoolsUrl : undefined,
|
|
505
556
|
trigger: context.enableDevtools
|
|
@@ -509,10 +560,10 @@ async function handleControlledReuse(context, waitToken, reuseResult) {
|
|
|
509
560
|
? () => triggerSweetLinkCliAuto(reuseResult.devtoolsUrl, context.targetUrlString)
|
|
510
561
|
: undefined,
|
|
511
562
|
failureMessage: context.enableDevtools
|
|
512
|
-
?
|
|
513
|
-
:
|
|
563
|
+
? "Controlled Chrome reused but SweetLink did not register automatically."
|
|
564
|
+
: "Controlled Chrome reused. DevTools automation disabled; verify the session from the UI if SweetLink does not appear.",
|
|
514
565
|
});
|
|
515
|
-
await surfaceBlockingDiagnosticsAfterNavigation(
|
|
566
|
+
await surfaceBlockingDiagnosticsAfterNavigation("SweetLink open", context.enableDevtools ? reuseResult.devtoolsUrl : undefined, context.targetUrlString);
|
|
516
567
|
}
|
|
517
568
|
async function handleControlledLaunch(context, waitToken) {
|
|
518
569
|
const info = await launchControlledChrome(context.targetUrlString, {
|
|
@@ -521,62 +572,68 @@ async function handleControlledLaunch(context, waitToken) {
|
|
|
521
572
|
headless: context.headless,
|
|
522
573
|
foreground: context.foreground,
|
|
523
574
|
});
|
|
524
|
-
const userDataDirectoryDisplay = info.userDataDir.replace(os.homedir(),
|
|
575
|
+
const userDataDirectoryDisplay = info.userDataDir.replace(os.homedir(), "~");
|
|
525
576
|
if (context.enableDevtools) {
|
|
526
577
|
await registerControlledChromeInstance(info.devtoolsUrl, info.userDataDir);
|
|
527
578
|
await cleanupControlledChromeRegistry(info.devtoolsUrl);
|
|
528
579
|
await signalSweetLinkBootstrap(info.devtoolsUrl, context.targetUrlString);
|
|
529
580
|
try {
|
|
530
|
-
const
|
|
581
|
+
const deepLinkResult = await ensureDeepLinkAuthFlow({
|
|
531
582
|
devtoolsUrl: info.devtoolsUrl,
|
|
532
|
-
|
|
533
|
-
|
|
583
|
+
targetUrl: context.targetUrlString,
|
|
584
|
+
oauthScriptPath: context.oauthScriptPath,
|
|
534
585
|
});
|
|
535
|
-
if (
|
|
536
|
-
console.log(
|
|
586
|
+
if (deepLinkResult.signInClicked) {
|
|
587
|
+
console.log("Triggered Sweetistics sign-in to reach the deep link.");
|
|
537
588
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
:
|
|
542
|
-
|
|
589
|
+
const oauthAttempt = deepLinkResult.oauthAttempt;
|
|
590
|
+
if (oauthAttempt) {
|
|
591
|
+
if (oauthAttempt.handled) {
|
|
592
|
+
console.log(`Automatically approved the OAuth prompt via ${oauthAttempt.action ?? "click"}${oauthAttempt.clickedText ? ` (${oauthAttempt.clickedText})` : ""}.`);
|
|
593
|
+
}
|
|
594
|
+
else if (oauthAttempt.reason && oauthAttempt.reason !== "button-not-found") {
|
|
595
|
+
const locationHint = oauthAttempt.url || oauthAttempt.title || oauthAttempt.host
|
|
596
|
+
? ` (at ${oauthAttempt.title ?? oauthAttempt.host ?? "unknown page"} ${oauthAttempt.url ?? ""})`
|
|
597
|
+
: "";
|
|
598
|
+
console.log(`OAuth auto-accept skipped: ${oauthAttempt.reason}${locationHint}.`);
|
|
599
|
+
}
|
|
543
600
|
}
|
|
544
601
|
}
|
|
545
602
|
catch (error) {
|
|
546
603
|
if (sweetLinkDebug) {
|
|
547
|
-
console.warn(
|
|
604
|
+
console.warn("Deep-link auth flow failed:", error);
|
|
548
605
|
}
|
|
549
606
|
}
|
|
550
607
|
}
|
|
551
608
|
else {
|
|
552
|
-
console.log(
|
|
609
|
+
console.log("DevTools automation disabled (--no-devtools); launched without OAuth auto-click.");
|
|
553
610
|
}
|
|
554
611
|
console.log(`Opened controlled Chrome window to ${context.targetUrlString} (env: ${context.env}).`);
|
|
555
612
|
console.log(`DevTools endpoint: ${info.devtoolsUrl}`);
|
|
556
613
|
console.log(`User data dir : ${userDataDirectoryDisplay}`);
|
|
557
614
|
if (context.enableDevtools) {
|
|
558
|
-
console.log(
|
|
615
|
+
console.log("The screenshot command will auto-detect this DevTools instance.");
|
|
559
616
|
}
|
|
560
617
|
if (context.headless) {
|
|
561
|
-
console.log(
|
|
618
|
+
console.log("Running in headless mode (--headless).");
|
|
562
619
|
}
|
|
563
620
|
const shouldFocus = context.foreground && !context.headless;
|
|
564
621
|
if (shouldFocus && context.enableDevtools) {
|
|
565
622
|
const focused = await focusControlledChromePage(info.devtoolsUrl, context.targetUrlString);
|
|
566
623
|
if (!focused && sweetLinkDebug) {
|
|
567
|
-
console.warn(
|
|
624
|
+
console.warn("Unable to focus controlled Chrome window automatically.");
|
|
568
625
|
}
|
|
569
626
|
}
|
|
570
627
|
else if (context.foreground && !context.enableDevtools) {
|
|
571
|
-
console.log(
|
|
628
|
+
console.log("--foreground requires DevTools automation; skipping automatic focus.");
|
|
572
629
|
}
|
|
573
630
|
await waitForSessionAfterOpen(context, waitToken, {
|
|
574
631
|
devtoolsUrl: context.enableDevtools ? info.devtoolsUrl : undefined,
|
|
575
632
|
failureMessage: context.enableDevtools
|
|
576
|
-
?
|
|
577
|
-
:
|
|
633
|
+
? "Controlled Chrome launched but SweetLink did not register automatically; keep the window open and retry from the UI if needed."
|
|
634
|
+
: "Controlled Chrome launched without DevTools automation; complete the login manually if SweetLink does not register.",
|
|
578
635
|
});
|
|
579
|
-
await surfaceBlockingDiagnosticsAfterNavigation(
|
|
636
|
+
await surfaceBlockingDiagnosticsAfterNavigation("SweetLink open", context.enableDevtools ? info.devtoolsUrl : undefined, context.targetUrlString);
|
|
580
637
|
}
|
|
581
638
|
async function waitForSessionAfterOpen(context, waitToken, options) {
|
|
582
639
|
if (!waitToken || context.timeoutSeconds <= 0) {
|
|
@@ -621,14 +678,14 @@ async function waitForSessionAfterOpen(context, waitToken, options) {
|
|
|
621
678
|
try {
|
|
622
679
|
const diagnostics = await collectBootstrapDiagnostics(options.devtoolsUrl, candidates);
|
|
623
680
|
if (diagnostics) {
|
|
624
|
-
console.warn(
|
|
625
|
-
logBootstrapDiagnostics(
|
|
681
|
+
console.warn("SweetLink bootstrap diagnostics (DevTools snapshot):");
|
|
682
|
+
logBootstrapDiagnostics("SweetLink", diagnostics);
|
|
626
683
|
}
|
|
627
684
|
}
|
|
628
685
|
catch (error) {
|
|
629
|
-
console.warn(
|
|
686
|
+
console.warn("Failed to collect DevTools diagnostics:", error);
|
|
630
687
|
}
|
|
631
|
-
await logPuppeteerPageSnapshot(
|
|
688
|
+
await logPuppeteerPageSnapshot("SweetLink", options.devtoolsUrl, context.targetUrlString);
|
|
632
689
|
}
|
|
633
690
|
}
|
|
634
691
|
// Runs after every navigation so CLI users see the same crash text the browser shows.
|
|
@@ -644,7 +701,8 @@ async function surfaceBlockingDiagnosticsAfterNavigation(label, devtoolsUrl, tar
|
|
|
644
701
|
const diagnostics = await collectBootstrapDiagnostics(devtoolsUrl, candidates);
|
|
645
702
|
if (diagnostics) {
|
|
646
703
|
const bootstrapIncomplete = diagnostics.autoFlag !== true ||
|
|
647
|
-
(typeof diagnostics.sessionStorageAuto ===
|
|
704
|
+
(typeof diagnostics.sessionStorageAuto === "string" &&
|
|
705
|
+
diagnostics.sessionStorageAuto.length > 0);
|
|
648
706
|
const hasBlocking = diagnosticsContainBlockingIssues(diagnostics) || bootstrapIncomplete;
|
|
649
707
|
if (hasBlocking) {
|
|
650
708
|
console.warn(`${label}: detected runtime anomalies after navigation.`);
|
|
@@ -661,7 +719,7 @@ async function surfaceBlockingDiagnosticsAfterNavigation(label, devtoolsUrl, tar
|
|
|
661
719
|
}
|
|
662
720
|
}
|
|
663
721
|
catch (error) {
|
|
664
|
-
console.warn(
|
|
722
|
+
console.warn("Failed to collect DevTools diagnostics after navigation:", error);
|
|
665
723
|
}
|
|
666
724
|
if (!loggedBlocking) {
|
|
667
725
|
const nextErrors = await fetchNextDevtoolsErrors(targetUrl);
|
|
@@ -687,7 +745,7 @@ async function logPuppeteerPageSnapshot(label, devtoolsUrl, targetUrl) {
|
|
|
687
745
|
}
|
|
688
746
|
if (snapshot.overlayText) {
|
|
689
747
|
console.warn(`${label} overlay (via Puppeteer):`);
|
|
690
|
-
for (const line of snapshot.overlayText.split(
|
|
748
|
+
for (const line of snapshot.overlayText.split("\n")) {
|
|
691
749
|
const trimmed = line.trim();
|
|
692
750
|
if (trimmed.length > 0) {
|
|
693
751
|
console.warn(` ${trimmed}`);
|
|
@@ -696,7 +754,7 @@ async function logPuppeteerPageSnapshot(label, devtoolsUrl, targetUrl) {
|
|
|
696
754
|
return;
|
|
697
755
|
}
|
|
698
756
|
if (snapshot.bodyText && snapshot.bodyText.trim().length > 0) {
|
|
699
|
-
const condensed = snapshot.bodyText.replaceAll(/\s+/g,
|
|
757
|
+
const condensed = snapshot.bodyText.replaceAll(/\s+/g, " ").trim();
|
|
700
758
|
const snippet = condensed.length > 600 ? `${condensed.slice(0, 600)}…` : condensed;
|
|
701
759
|
console.warn(`${label} body text (via Puppeteer): ${snippet}`);
|
|
702
760
|
}
|
|
@@ -712,50 +770,52 @@ async function handleUncontrolledOpen(context, waitToken) {
|
|
|
712
770
|
await launchChrome(context.targetUrlString, { foreground: context.foreground });
|
|
713
771
|
console.log(`Opened Chrome to ${context.targetUrlString} (env: ${context.env}).`);
|
|
714
772
|
await waitForSessionAfterOpen(context, waitToken, {
|
|
715
|
-
failureMessage:
|
|
773
|
+
failureMessage: "Chrome tab opened but SweetLink did not register within the timeout window. Use `pnpm sweetlink sessions` to inspect manually.",
|
|
716
774
|
});
|
|
717
775
|
}
|
|
718
776
|
program
|
|
719
|
-
.command(
|
|
720
|
-
.description(
|
|
721
|
-
.option(
|
|
722
|
-
.option(
|
|
723
|
-
.option(
|
|
724
|
-
.option(
|
|
777
|
+
.command("smoke")
|
|
778
|
+
.description("Run the SweetLink authenticated smoke test across non-admin routes")
|
|
779
|
+
.option("--session <sessionId>", "Existing SweetLink session id or codename to reuse")
|
|
780
|
+
.option("--routes <routes>", `Comma-separated list of routes (default ${DEFAULT_SMOKE_ROUTES.join(", ")})`)
|
|
781
|
+
.option("--timeout <seconds>", "Per-route timeout in seconds (default 45)", Number)
|
|
782
|
+
.option("--resume", "Resume from the last completed route", false)
|
|
725
783
|
.action(async function () {
|
|
726
784
|
const options = readCommandOptions(this);
|
|
727
785
|
const routes = deriveSmokeRoutes(options.routes, DEFAULT_SMOKE_ROUTES);
|
|
728
786
|
if (routes.length === 0) {
|
|
729
|
-
console.log(
|
|
787
|
+
console.log("No routes specified for the smoke test. Provide --routes or keep the default set.");
|
|
730
788
|
return;
|
|
731
789
|
}
|
|
732
|
-
const timeoutSeconds = typeof options.timeout ===
|
|
790
|
+
const timeoutSeconds = typeof options.timeout === "number" && Number.isFinite(options.timeout)
|
|
791
|
+
? Math.max(5, options.timeout)
|
|
792
|
+
: 45;
|
|
733
793
|
await runSweetLinkSmoke({ sessionHint: options.session, routes, timeoutSeconds, resume: options.resume === true }, this);
|
|
734
794
|
});
|
|
735
795
|
program
|
|
736
|
-
.command(
|
|
737
|
-
.description(
|
|
738
|
-
.option(
|
|
739
|
-
.option(
|
|
740
|
-
.option(
|
|
741
|
-
.option(
|
|
742
|
-
.option(
|
|
743
|
-
.option(
|
|
744
|
-
.option(
|
|
745
|
-
.option(
|
|
746
|
-
.option(
|
|
747
|
-
.option(
|
|
748
|
-
.option(
|
|
749
|
-
.option(
|
|
750
|
-
.option(
|
|
751
|
-
.option(
|
|
752
|
-
.option(
|
|
753
|
-
.addOption(program.createOption(
|
|
796
|
+
.command("screenshot <sessionId>")
|
|
797
|
+
.description("Capture a JPEG screenshot from a SweetLink session")
|
|
798
|
+
.option("-s, --selector <selector>", "CSS selector to capture (defaults to full page)")
|
|
799
|
+
.option("-q, --quality <0-1>", "JPEG quality (0-1, default 0.92)", Number)
|
|
800
|
+
.option("-o, --output <path>", "Output path (defaults to /tmp/sweetlink-<timestamp>.jpg)")
|
|
801
|
+
.option("-t, --timeout <ms>", "Command timeout in milliseconds (default 30_000)", Number, 30_000)
|
|
802
|
+
.option("--devtools-url <url>", "DevTools HTTP endpoint (default http://127.0.0.1:9222)")
|
|
803
|
+
.option("--method <method>", "Capture method: auto, puppeteer, html2canvas, html-to-image", "auto")
|
|
804
|
+
.option("--scroll-into-view", "Scroll the target into view before capturing", false)
|
|
805
|
+
.option("--scroll-selector <selector>", "Selector to scroll into view (defaults to capture selector)")
|
|
806
|
+
.option("--wait-for-selector <selector>", "Wait for a selector to appear before capturing")
|
|
807
|
+
.option("--wait-visible", "Require the wait selector to be visible", false)
|
|
808
|
+
.option("--wait-timeout <ms>", "Timeout for --wait-for-selector (default 10_000)", Number, 10_000)
|
|
809
|
+
.option("--delay <ms>", "Delay in milliseconds after hooks run (default 0)", Number)
|
|
810
|
+
.option("--before-script <codeOrPath>", "Inline JS (or @path) to run before capture")
|
|
811
|
+
.option("--preset <name>", "Hook preset compatibility alias (card-ready is applied automatically and no longer requires this flag)")
|
|
812
|
+
.option("--prompt <prompt>", "Send the saved screenshot to Codex for analysis")
|
|
813
|
+
.addOption(program.createOption("--question <prompt>").hideHelp())
|
|
754
814
|
.action(async (sessionId, options, command) => {
|
|
755
815
|
const config = resolveConfig(command);
|
|
756
816
|
const resolvedSessionId = await resolveSessionIdFromHint(sessionId, config);
|
|
757
817
|
const token = await fetchCliToken(config);
|
|
758
|
-
const mode = options.selector ?
|
|
818
|
+
const mode = options.selector ? "element" : "full";
|
|
759
819
|
const prompt = resolvePromptOption(options);
|
|
760
820
|
const suppressOutput = Boolean(prompt);
|
|
761
821
|
const logInfo = (...args) => {
|
|
@@ -764,15 +824,17 @@ program
|
|
|
764
824
|
}
|
|
765
825
|
};
|
|
766
826
|
const now = new Date();
|
|
767
|
-
const defaultOutput = path.join(os.tmpdir(), `sweetlink-${now.toISOString().replaceAll(
|
|
827
|
+
const defaultOutput = path.join(os.tmpdir(), `sweetlink-${now.toISOString().replaceAll(":", "-").replaceAll(".", "-")}.jpg`);
|
|
768
828
|
const outputPath = options.output ? path.resolve(options.output) : defaultOutput;
|
|
769
|
-
const quality = typeof options.quality ===
|
|
829
|
+
const quality = typeof options.quality === "number" && !Number.isNaN(options.quality)
|
|
830
|
+
? options.quality
|
|
831
|
+
: 0.92;
|
|
770
832
|
const method = normalizeScreenshotMethod(options.method);
|
|
771
833
|
await mkdir(path.dirname(outputPath), { recursive: true }).catch(() => {
|
|
772
834
|
/* ignore directory creation failures; writeFile will surface if it truly fails */
|
|
773
835
|
});
|
|
774
836
|
const devtoolsConfig = await loadDevToolsConfig();
|
|
775
|
-
const devtoolsUrl = options.devtoolsUrl ?? devtoolsConfig?.devtoolsUrl ??
|
|
837
|
+
const devtoolsUrl = options.devtoolsUrl ?? devtoolsConfig?.devtoolsUrl ?? "http://127.0.0.1:9222";
|
|
776
838
|
let sessionSummary;
|
|
777
839
|
try {
|
|
778
840
|
sessionSummary = await getSessionSummaryById(config, token, resolvedSessionId);
|
|
@@ -780,7 +842,7 @@ program
|
|
|
780
842
|
catch {
|
|
781
843
|
sessionSummary = undefined;
|
|
782
844
|
}
|
|
783
|
-
const wantsPuppeteer = method ===
|
|
845
|
+
const wantsPuppeteer = method === "puppeteer" || method === "auto";
|
|
784
846
|
if (wantsPuppeteer && sessionSummary?.url) {
|
|
785
847
|
const devtoolsCaptureResult = await attemptDevToolsCapture({
|
|
786
848
|
devtoolsUrl,
|
|
@@ -792,21 +854,24 @@ program
|
|
|
792
854
|
});
|
|
793
855
|
if (devtoolsCaptureResult) {
|
|
794
856
|
logInfo(`Saved screenshot to ${outputPath} (${devtoolsCaptureResult.width}x${devtoolsCaptureResult.height}, ${devtoolsCaptureResult.sizeKb.toFixed(1)} KB, method: ${devtoolsCaptureResult.renderer}).`);
|
|
795
|
-
await maybeDescribeScreenshot(prompt, outputPath, {
|
|
857
|
+
await maybeDescribeScreenshot(prompt, outputPath, {
|
|
858
|
+
silent: suppressOutput,
|
|
859
|
+
appLabel: config.appLabel,
|
|
860
|
+
});
|
|
796
861
|
return;
|
|
797
862
|
}
|
|
798
|
-
if (method ===
|
|
799
|
-
throw new Error(
|
|
863
|
+
if (method === "puppeteer") {
|
|
864
|
+
throw new Error("Failed to capture via Puppeteer. Ensure Chrome is running with a DevTools port (try `pnpm sweetlink open --controlled`).");
|
|
800
865
|
}
|
|
801
866
|
}
|
|
802
|
-
const rendererOverride = method ===
|
|
867
|
+
const rendererOverride = method === "html2canvas" || method === "html-to-image" ? method : undefined;
|
|
803
868
|
const beforeScriptCode = await resolveHookSnippet(options.beforeScript);
|
|
804
869
|
const commandOptions = readCommandOptions(command);
|
|
805
870
|
const presetCandidate = commandOptions.preset ?? options.preset;
|
|
806
|
-
const presetRaw = typeof presetCandidate ===
|
|
871
|
+
const presetRaw = typeof presetCandidate === "string" ? presetCandidate.trim() : "";
|
|
807
872
|
const presetName = presetRaw.toLowerCase();
|
|
808
873
|
if (presetName.length > 0) {
|
|
809
|
-
if (presetName ===
|
|
874
|
+
if (presetName === "card-ready") {
|
|
810
875
|
logInfo('Preset "card-ready" is now the default hook stack; the flag is optional.');
|
|
811
876
|
}
|
|
812
877
|
else {
|
|
@@ -819,31 +884,35 @@ program
|
|
|
819
884
|
scrollSelector: options.scrollSelector,
|
|
820
885
|
waitSelector: options.waitForSelector,
|
|
821
886
|
waitVisible: options.waitVisible === undefined ? undefined : Boolean(options.waitVisible),
|
|
822
|
-
waitTimeout: typeof options.waitTimeout ===
|
|
887
|
+
waitTimeout: typeof options.waitTimeout === "number" && Number.isFinite(options.waitTimeout)
|
|
823
888
|
? options.waitTimeout
|
|
824
889
|
: undefined,
|
|
825
|
-
delayMs: typeof options.delay ===
|
|
890
|
+
delayMs: typeof options.delay === "number" && Number.isFinite(options.delay)
|
|
891
|
+
? options.delay
|
|
892
|
+
: undefined,
|
|
826
893
|
beforeScript: beforeScriptCode ?? undefined,
|
|
827
894
|
};
|
|
828
895
|
const hooks = buildScreenshotHooks(hookOptions);
|
|
829
896
|
if (hooks.length > 0) {
|
|
830
|
-
logInfo(`Applying ${hooks.length} pre-capture hook${hooks.length === 1 ?
|
|
897
|
+
logInfo(`Applying ${hooks.length} pre-capture hook${hooks.length === 1 ? "" : "s"} before screenshot.`);
|
|
831
898
|
}
|
|
832
899
|
const payload = {
|
|
833
|
-
type:
|
|
900
|
+
type: "screenshot",
|
|
834
901
|
id: createSweetLinkCommandId(),
|
|
835
902
|
mode,
|
|
836
903
|
selector: options.selector,
|
|
837
904
|
quality,
|
|
838
|
-
timeoutMs: typeof options.timeout ===
|
|
905
|
+
timeoutMs: typeof options.timeout === "number" && !Number.isNaN(options.timeout)
|
|
906
|
+
? options.timeout
|
|
907
|
+
: 30_000,
|
|
839
908
|
renderer: rendererOverride,
|
|
840
909
|
hooks: hooks.length > 0 ? hooks : undefined,
|
|
841
910
|
};
|
|
842
911
|
const response = await fetchJson(`${config.daemonBaseUrl}/sessions/${encodeURIComponent(resolvedSessionId)}/command`, {
|
|
843
|
-
method:
|
|
912
|
+
method: "POST",
|
|
844
913
|
headers: {
|
|
845
914
|
Authorization: `Bearer ${token}`,
|
|
846
|
-
|
|
915
|
+
"Content-Type": "application/json",
|
|
847
916
|
},
|
|
848
917
|
body: JSON.stringify(payload),
|
|
849
918
|
});
|
|
@@ -864,7 +933,9 @@ program
|
|
|
864
933
|
if (fallbackOutcome.handled) {
|
|
865
934
|
return;
|
|
866
935
|
}
|
|
867
|
-
const fallbackError = fallbackOutcome.fallbackResult.ok
|
|
936
|
+
const fallbackError = fallbackOutcome.fallbackResult.ok
|
|
937
|
+
? null
|
|
938
|
+
: fallbackOutcome.fallbackResult.error;
|
|
868
939
|
const recovered = await tryDevToolsRecovery({
|
|
869
940
|
sessionUrl: sessionSummary?.url,
|
|
870
941
|
devtoolsUrl,
|
|
@@ -906,41 +977,44 @@ program
|
|
|
906
977
|
return;
|
|
907
978
|
}
|
|
908
979
|
await persistScreenshotResult(outputPath, result, { silent: suppressOutput });
|
|
909
|
-
await maybeDescribeScreenshot(prompt, outputPath, {
|
|
980
|
+
await maybeDescribeScreenshot(prompt, outputPath, {
|
|
981
|
+
silent: suppressOutput,
|
|
982
|
+
appLabel: config.appLabel,
|
|
983
|
+
});
|
|
910
984
|
});
|
|
911
985
|
program
|
|
912
|
-
.command(
|
|
913
|
-
.description(
|
|
914
|
-
.option(
|
|
915
|
-
.option(
|
|
916
|
-
.option(
|
|
917
|
-
.option(
|
|
918
|
-
.option(
|
|
986
|
+
.command("selectors <sessionId>")
|
|
987
|
+
.description("Discover candidate selectors within a SweetLink session")
|
|
988
|
+
.option("-l, --limit <count>", "Maximum number of candidates to return (default 20)", Number)
|
|
989
|
+
.option("-m, --max <count>", "Alias for --limit", Number)
|
|
990
|
+
.option("--scope <selector>", "Restrict discovery to elements inside this selector")
|
|
991
|
+
.option("--include-hidden", "Include off-screen or hidden elements", false)
|
|
992
|
+
.option("--json", "Output JSON payload", false)
|
|
919
993
|
.action(async (sessionId, options, command) => {
|
|
920
994
|
const config = resolveConfig(command);
|
|
921
995
|
const resolvedSessionId = await resolveSessionIdFromHint(sessionId, config);
|
|
922
996
|
const token = await fetchCliToken(config);
|
|
923
997
|
const limit = (() => {
|
|
924
|
-
if (typeof options.max ===
|
|
998
|
+
if (typeof options.max === "number" && Number.isFinite(options.max)) {
|
|
925
999
|
return Math.max(1, Math.floor(options.max));
|
|
926
1000
|
}
|
|
927
|
-
if (typeof options.limit ===
|
|
1001
|
+
if (typeof options.limit === "number" && Number.isFinite(options.limit)) {
|
|
928
1002
|
return Math.max(1, Math.floor(options.limit));
|
|
929
1003
|
}
|
|
930
1004
|
return 20;
|
|
931
1005
|
})();
|
|
932
1006
|
const payload = {
|
|
933
|
-
type:
|
|
1007
|
+
type: "discoverSelectors",
|
|
934
1008
|
id: createSweetLinkCommandId(),
|
|
935
1009
|
scopeSelector: options.scope ?? null,
|
|
936
1010
|
limit,
|
|
937
1011
|
includeHidden: Boolean(options.includeHidden),
|
|
938
1012
|
};
|
|
939
1013
|
const response = await fetchJson(`${config.daemonBaseUrl}/sessions/${encodeURIComponent(resolvedSessionId)}/command`, {
|
|
940
|
-
method:
|
|
1014
|
+
method: "POST",
|
|
941
1015
|
headers: {
|
|
942
1016
|
Authorization: `Bearer ${token}`,
|
|
943
|
-
|
|
1017
|
+
"Content-Type": "application/json",
|
|
944
1018
|
},
|
|
945
1019
|
body: JSON.stringify(payload),
|
|
946
1020
|
});
|
|
@@ -963,15 +1037,15 @@ program
|
|
|
963
1037
|
return;
|
|
964
1038
|
}
|
|
965
1039
|
if (candidates.length === 0) {
|
|
966
|
-
console.log(
|
|
967
|
-
console.log(
|
|
1040
|
+
console.log("No candidates were discovered. Ensure the target UI is mounted and try again.");
|
|
1041
|
+
console.log("Tip: use --scope to limit discovery or --include-hidden to inspect collapsed panels.");
|
|
968
1042
|
return;
|
|
969
1043
|
}
|
|
970
|
-
console.log(`Discovered ${candidates.length} selector candidate${candidates.length === 1 ?
|
|
1044
|
+
console.log(`Discovered ${candidates.length} selector candidate${candidates.length === 1 ? "" : "s"} (limit ${limit}):\n`);
|
|
971
1045
|
for (const candidate of candidates.slice(0, limit)) {
|
|
972
1046
|
console.log(`• ${candidate.selector}`);
|
|
973
1047
|
console.log(` Hook : ${candidate.hook} (score ${candidate.score})`);
|
|
974
|
-
console.log(` Visible : ${candidate.visible ?
|
|
1048
|
+
console.log(` Visible : ${candidate.visible ? "yes" : "no"} (${candidate.size.width}x${candidate.size.height})`);
|
|
975
1049
|
console.log(` Text : ${candidate.textSnippet}`);
|
|
976
1050
|
if (candidate.dataTarget) {
|
|
977
1051
|
console.log(` Target : data-sweetlink-target="${candidate.dataTarget}"`);
|
|
@@ -984,64 +1058,66 @@ program
|
|
|
984
1058
|
}
|
|
985
1059
|
console.log(` Position : x=${candidate.position.left}, y=${candidate.position.top}`);
|
|
986
1060
|
console.log(` Path : ${candidate.path}`);
|
|
987
|
-
console.log(
|
|
1061
|
+
console.log("");
|
|
988
1062
|
}
|
|
989
1063
|
});
|
|
990
|
-
const devtools = program
|
|
1064
|
+
const devtools = program
|
|
1065
|
+
.command("devtools")
|
|
1066
|
+
.description("Inspect DevTools-enabled Chrome sessions");
|
|
991
1067
|
devtools
|
|
992
|
-
.command(
|
|
993
|
-
.description(
|
|
994
|
-
.option(
|
|
1068
|
+
.command("status")
|
|
1069
|
+
.description("Show DevTools connection status and telemetry summary")
|
|
1070
|
+
.option("--json", "Output JSON payload", false)
|
|
995
1071
|
.action(async (options, command) => {
|
|
996
1072
|
await devtoolsStatus(options, command);
|
|
997
1073
|
});
|
|
998
1074
|
devtools
|
|
999
|
-
.command(
|
|
1000
|
-
.description(
|
|
1001
|
-
.option(
|
|
1075
|
+
.command("tabs")
|
|
1076
|
+
.description("List open tabs in the controlled Chrome window")
|
|
1077
|
+
.option("--json", "Output JSON payload", false)
|
|
1002
1078
|
.action(async (options) => {
|
|
1003
1079
|
await devtoolsTabs(options);
|
|
1004
1080
|
});
|
|
1005
1081
|
devtools
|
|
1006
|
-
.command(
|
|
1007
|
-
.description(
|
|
1008
|
-
.option(
|
|
1009
|
-
.option(
|
|
1082
|
+
.command("console")
|
|
1083
|
+
.description("Print buffered console events from the controlled Chrome window")
|
|
1084
|
+
.option("--tail <count>", "Number of entries to show (default 50)", Number, 50)
|
|
1085
|
+
.option("--json", "Output JSON payload", false)
|
|
1010
1086
|
.action(async (options) => {
|
|
1011
1087
|
await devtoolsShowConsole(options);
|
|
1012
1088
|
});
|
|
1013
1089
|
devtools
|
|
1014
|
-
.command(
|
|
1015
|
-
.description(
|
|
1016
|
-
.option(
|
|
1017
|
-
.option(
|
|
1090
|
+
.command("network")
|
|
1091
|
+
.description("Print buffered network requests from the controlled Chrome window")
|
|
1092
|
+
.option("--tail <count>", "Number of entries to show (default 50)", Number, 50)
|
|
1093
|
+
.option("--json", "Output JSON payload", false)
|
|
1018
1094
|
.action(async (options) => {
|
|
1019
1095
|
await devtoolsShowNetwork(options);
|
|
1020
1096
|
});
|
|
1021
1097
|
devtools
|
|
1022
|
-
.command(
|
|
1023
|
-
.description(
|
|
1024
|
-
.option(
|
|
1025
|
-
.option(
|
|
1026
|
-
.option(
|
|
1098
|
+
.command("listen")
|
|
1099
|
+
.description("Attach to DevTools and buffer console/network telemetry locally")
|
|
1100
|
+
.option("--session <sessionId>", "Associate telemetry with a SweetLink session id")
|
|
1101
|
+
.option("--reset", "Clear existing telemetry buffers before listening", false)
|
|
1102
|
+
.option("--background", "Run without interactive prompts (for automation)", false)
|
|
1027
1103
|
.action(async (options, command) => {
|
|
1028
1104
|
await devtoolsListen(options, command);
|
|
1029
1105
|
});
|
|
1030
1106
|
devtools
|
|
1031
|
-
.command(
|
|
1032
|
-
.description(
|
|
1033
|
-
.option(
|
|
1107
|
+
.command("authorize")
|
|
1108
|
+
.description("Attempt to auto-click the OAuth authorize prompt in the controlled browser")
|
|
1109
|
+
.option("--url <url>", "Override the candidate OAuth URL (defaults to the tracked session)")
|
|
1034
1110
|
.action(async (options, command) => {
|
|
1035
1111
|
await devtoolsAuthorize(options, command);
|
|
1036
1112
|
});
|
|
1037
|
-
program.hook(
|
|
1113
|
+
program.hook("preAction", () => {
|
|
1038
1114
|
// Ensure commander does not swallow promise rejections so we can log helpful messages.
|
|
1039
|
-
process.on(
|
|
1115
|
+
process.on("unhandledRejection", (error) => {
|
|
1040
1116
|
reportError(error);
|
|
1041
1117
|
process.exitCode = 1;
|
|
1042
1118
|
});
|
|
1043
1119
|
});
|
|
1044
|
-
program.addHelpText(
|
|
1120
|
+
program.addHelpText("afterAll", `
|
|
1045
1121
|
DevTools commands:
|
|
1046
1122
|
devtools status Show endpoint reachability, viewport, and buffer sizes
|
|
1047
1123
|
devtools tabs List open tabs in the controlled Chrome window
|
|
@@ -1073,19 +1149,19 @@ async function resolveHookSnippet(value) {
|
|
|
1073
1149
|
if (trimmed.length === 0) {
|
|
1074
1150
|
return null;
|
|
1075
1151
|
}
|
|
1076
|
-
if (trimmed.startsWith(
|
|
1152
|
+
if (trimmed.startsWith("@")) {
|
|
1077
1153
|
const candidatePath = trimmed.slice(1).trim();
|
|
1078
1154
|
if (!candidatePath) {
|
|
1079
|
-
throw new Error(
|
|
1155
|
+
throw new Error("Expected a file path after @ for --before-script.");
|
|
1080
1156
|
}
|
|
1081
1157
|
const absolute = path.isAbsolute(candidatePath) ? candidatePath : path.resolve(candidatePath);
|
|
1082
|
-
const hookContents = await readFile(absolute,
|
|
1158
|
+
const hookContents = await readFile(absolute, "utf8");
|
|
1083
1159
|
return hookContents.toString();
|
|
1084
1160
|
}
|
|
1085
1161
|
return trimmed;
|
|
1086
1162
|
}
|
|
1087
1163
|
export function formatPathForDisplay(value) {
|
|
1088
|
-
return value.replace(os.homedir(),
|
|
1164
|
+
return value.replace(os.homedir(), "~");
|
|
1089
1165
|
}
|
|
1090
1166
|
function reportError(error) {
|
|
1091
1167
|
if (error instanceof CommanderError) {
|
|
@@ -1096,32 +1172,32 @@ function reportError(error) {
|
|
|
1096
1172
|
console.error(error.message);
|
|
1097
1173
|
return;
|
|
1098
1174
|
}
|
|
1099
|
-
console.error(
|
|
1175
|
+
console.error("Unexpected error", error);
|
|
1100
1176
|
}
|
|
1101
1177
|
function normalizeScreenshotMethod(input) {
|
|
1102
1178
|
if (!input) {
|
|
1103
|
-
return
|
|
1179
|
+
return "auto";
|
|
1104
1180
|
}
|
|
1105
1181
|
const normalized = input.toLowerCase().trim();
|
|
1106
1182
|
switch (normalized) {
|
|
1107
|
-
case
|
|
1108
|
-
return
|
|
1183
|
+
case "auto": {
|
|
1184
|
+
return "auto";
|
|
1109
1185
|
}
|
|
1110
|
-
case
|
|
1111
|
-
return
|
|
1186
|
+
case "puppeteer": {
|
|
1187
|
+
return "puppeteer";
|
|
1112
1188
|
}
|
|
1113
|
-
case
|
|
1114
|
-
case
|
|
1115
|
-
return
|
|
1189
|
+
case "html2canvas":
|
|
1190
|
+
case "html-2-canvas": {
|
|
1191
|
+
return "html2canvas";
|
|
1116
1192
|
}
|
|
1117
|
-
case
|
|
1118
|
-
case
|
|
1119
|
-
case
|
|
1120
|
-
case
|
|
1121
|
-
return
|
|
1193
|
+
case "html-to-image":
|
|
1194
|
+
case "htmltoimage":
|
|
1195
|
+
case "dom-to-image":
|
|
1196
|
+
case "domtoimage": {
|
|
1197
|
+
return "html-to-image";
|
|
1122
1198
|
}
|
|
1123
1199
|
default: {
|
|
1124
|
-
throw new Error(
|
|
1200
|
+
throw new Error("Invalid screenshot method. Use auto, puppeteer, html2canvas, or html-to-image.");
|
|
1125
1201
|
}
|
|
1126
1202
|
}
|
|
1127
1203
|
}
|
|
@@ -1129,7 +1205,7 @@ async function resolveSmokePrerequisites(params, command) {
|
|
|
1129
1205
|
const config = resolveConfig(command);
|
|
1130
1206
|
const devtoolsConfig = await loadDevToolsConfig();
|
|
1131
1207
|
if (!devtoolsConfig?.devtoolsUrl) {
|
|
1132
|
-
console.error(
|
|
1208
|
+
console.error("DevTools endpoint not found. Launch a controlled Chrome window with `pnpm sweetlink open --controlled` first.");
|
|
1133
1209
|
process.exitCode = 1;
|
|
1134
1210
|
return null;
|
|
1135
1211
|
}
|
|
@@ -1137,7 +1213,7 @@ async function resolveSmokePrerequisites(params, command) {
|
|
|
1137
1213
|
? await resolveSessionIdFromHint(params.sessionHint, config)
|
|
1138
1214
|
: (devtoolsConfig.sessionId ?? null);
|
|
1139
1215
|
if (!sessionId) {
|
|
1140
|
-
console.error(
|
|
1216
|
+
console.error("Unable to determine a SweetLink session. Pass --session or rerun `pnpm sweetlink open --controlled`.");
|
|
1141
1217
|
process.exitCode = 1;
|
|
1142
1218
|
return null;
|
|
1143
1219
|
}
|
|
@@ -1146,7 +1222,7 @@ async function resolveSmokePrerequisites(params, command) {
|
|
|
1146
1222
|
token = await fetchCliToken(config);
|
|
1147
1223
|
}
|
|
1148
1224
|
catch (error) {
|
|
1149
|
-
console.error(
|
|
1225
|
+
console.error("Unable to fetch SweetLink CLI token:", extractEventMessage(error));
|
|
1150
1226
|
process.exitCode = 1;
|
|
1151
1227
|
return null;
|
|
1152
1228
|
}
|
|
@@ -1173,18 +1249,18 @@ async function determineSmokeStartIndex(options) {
|
|
|
1173
1249
|
}
|
|
1174
1250
|
catch (error) {
|
|
1175
1251
|
if (sweetLinkDebug) {
|
|
1176
|
-
console.warn(
|
|
1252
|
+
console.warn("Failed to clear stored smoke progress before run:", error);
|
|
1177
1253
|
}
|
|
1178
1254
|
}
|
|
1179
1255
|
return 0;
|
|
1180
1256
|
}
|
|
1181
1257
|
const savedIndex = await loadSmokeProgressIndex(options.baseOrigin, options.routes);
|
|
1182
1258
|
if (savedIndex === null) {
|
|
1183
|
-
console.log(
|
|
1259
|
+
console.log("No prior smoke progress found. Starting from the beginning.");
|
|
1184
1260
|
return 0;
|
|
1185
1261
|
}
|
|
1186
1262
|
if (savedIndex >= options.routes.length) {
|
|
1187
|
-
console.log(
|
|
1263
|
+
console.log("Previous smoke run completed every route. Starting from the beginning.");
|
|
1188
1264
|
return 0;
|
|
1189
1265
|
}
|
|
1190
1266
|
if (savedIndex > 0) {
|
|
@@ -1210,12 +1286,12 @@ async function executeSmokeRoute(context, state, route, routeIndex) {
|
|
|
1210
1286
|
lastKnownUrl,
|
|
1211
1287
|
failure: {
|
|
1212
1288
|
route: `#${routeIndex + 1}`,
|
|
1213
|
-
reason:
|
|
1289
|
+
reason: "Missing route entry in smoke route list.",
|
|
1214
1290
|
},
|
|
1215
1291
|
};
|
|
1216
1292
|
}
|
|
1217
1293
|
const targetUrl = buildSmokeRouteUrl(context.baseUrl, route);
|
|
1218
|
-
const displayPath = `${targetUrl.pathname}${targetUrl.search ||
|
|
1294
|
+
const displayPath = `${targetUrl.pathname}${targetUrl.search || ""}`;
|
|
1219
1295
|
console.log(`\n→ ${displayPath}`);
|
|
1220
1296
|
const sessionConnected = await ensureSweetLinkSessionConnected({
|
|
1221
1297
|
config: context.config,
|
|
@@ -1228,23 +1304,27 @@ async function executeSmokeRoute(context, state, route, routeIndex) {
|
|
|
1228
1304
|
candidateUrls: [targetUrl.toString()],
|
|
1229
1305
|
});
|
|
1230
1306
|
if (!sessionConnected) {
|
|
1231
|
-
console.warn(
|
|
1307
|
+
console.warn(" Unable to verify active SweetLink session before navigation.");
|
|
1232
1308
|
return {
|
|
1233
1309
|
session,
|
|
1234
1310
|
lastKnownUrl,
|
|
1235
|
-
failure: { route: displayPath, reason:
|
|
1311
|
+
failure: { route: displayPath, reason: "session unavailable before navigation" },
|
|
1236
1312
|
};
|
|
1237
1313
|
}
|
|
1238
1314
|
const attemptNavigation = async () => {
|
|
1239
|
-
await navigateSweetLinkSession({
|
|
1315
|
+
await navigateSweetLinkSession({
|
|
1316
|
+
sessionId: activeSessionId,
|
|
1317
|
+
targetUrl,
|
|
1318
|
+
config: context.config,
|
|
1319
|
+
});
|
|
1240
1320
|
};
|
|
1241
1321
|
try {
|
|
1242
1322
|
await attemptNavigation();
|
|
1243
1323
|
}
|
|
1244
1324
|
catch (error) {
|
|
1245
|
-
const reason = extractEventMessage(error,
|
|
1325
|
+
const reason = extractEventMessage(error, "navigation failed");
|
|
1246
1326
|
if (SESSION_RECOVERY_PATTERN.test(reason)) {
|
|
1247
|
-
console.warn(
|
|
1327
|
+
console.warn(" Session went offline during navigation command. Attempting recovery…");
|
|
1248
1328
|
const recovered = await ensureSweetLinkSessionConnected({
|
|
1249
1329
|
config: context.config,
|
|
1250
1330
|
token: context.token,
|
|
@@ -1260,7 +1340,7 @@ async function executeSmokeRoute(context, state, route, routeIndex) {
|
|
|
1260
1340
|
await attemptNavigation();
|
|
1261
1341
|
}
|
|
1262
1342
|
catch (retryError) {
|
|
1263
|
-
const retryReason = extractEventMessage(retryError,
|
|
1343
|
+
const retryReason = extractEventMessage(retryError, "navigation failed");
|
|
1264
1344
|
console.warn(` Navigation failed after recovery: ${retryReason}`);
|
|
1265
1345
|
return { session, lastKnownUrl, failure: { route: displayPath, reason: retryReason } };
|
|
1266
1346
|
}
|
|
@@ -1283,7 +1363,7 @@ async function executeSmokeRoute(context, state, route, routeIndex) {
|
|
|
1283
1363
|
devtoolsUrl: context.devtoolsUrl,
|
|
1284
1364
|
});
|
|
1285
1365
|
if (!handshake && context.devtoolsUrl) {
|
|
1286
|
-
console.warn(
|
|
1366
|
+
console.warn(" SweetLink session did not reconnect after navigation. Retrying bootstrap…");
|
|
1287
1367
|
await triggerSweetLinkCliAuto(context.devtoolsUrl, targetUrl.toString());
|
|
1288
1368
|
handshake = await waitForSweetLinkSession({
|
|
1289
1369
|
config: context.config,
|
|
@@ -1305,7 +1385,9 @@ async function executeSmokeRoute(context, state, route, routeIndex) {
|
|
|
1305
1385
|
});
|
|
1306
1386
|
if (recovered) {
|
|
1307
1387
|
const refreshed = await getSessionSummaryById(context.config, context.token, activeSessionId);
|
|
1308
|
-
const recoveredUrl = typeof refreshed?.url ===
|
|
1388
|
+
const recoveredUrl = typeof refreshed?.url === "string" && refreshed.url.length > 0
|
|
1389
|
+
? refreshed.url
|
|
1390
|
+
: targetUrl.toString();
|
|
1309
1391
|
handshake = {
|
|
1310
1392
|
sessionId: activeSessionId,
|
|
1311
1393
|
url: recoveredUrl,
|
|
@@ -1314,43 +1396,48 @@ async function executeSmokeRoute(context, state, route, routeIndex) {
|
|
|
1314
1396
|
}
|
|
1315
1397
|
}
|
|
1316
1398
|
if (!handshake) {
|
|
1317
|
-
console.warn(
|
|
1399
|
+
console.warn(" SweetLink session did not come back online after navigation.");
|
|
1318
1400
|
return {
|
|
1319
1401
|
session,
|
|
1320
1402
|
lastKnownUrl,
|
|
1321
|
-
failure: { route: displayPath, reason:
|
|
1403
|
+
failure: { route: displayPath, reason: "session did not reconnect" },
|
|
1322
1404
|
};
|
|
1323
1405
|
}
|
|
1324
1406
|
if (handshake.sessionId && handshake.sessionId !== activeSessionId) {
|
|
1325
1407
|
setActiveSessionId(handshake.sessionId);
|
|
1326
1408
|
}
|
|
1327
1409
|
lastKnownUrl = handshake.url ?? targetUrl.toString();
|
|
1328
|
-
session =
|
|
1410
|
+
session =
|
|
1411
|
+
(await getSessionSummaryById(context.config, context.token, activeSessionId)) ?? session;
|
|
1329
1412
|
const diagnostics = await waitForSmokeRouteReady({
|
|
1330
1413
|
devtoolsUrl: context.devtoolsUrl,
|
|
1331
1414
|
targetUrl,
|
|
1332
1415
|
timeoutMs: context.timeoutSeconds * 1000,
|
|
1333
1416
|
});
|
|
1334
1417
|
if (!diagnostics) {
|
|
1335
|
-
console.warn(
|
|
1336
|
-
return {
|
|
1418
|
+
console.warn(" Timed out waiting for the route to reach a stable state.");
|
|
1419
|
+
return {
|
|
1420
|
+
session,
|
|
1421
|
+
lastKnownUrl,
|
|
1422
|
+
failure: { route: displayPath, reason: "timeout awaiting route readiness" },
|
|
1423
|
+
};
|
|
1337
1424
|
}
|
|
1338
|
-
const finalHref = diagnostics.locationHref ??
|
|
1425
|
+
const finalHref = diagnostics.locationHref ?? "unknown";
|
|
1339
1426
|
if (!urlsRoughlyMatch(finalHref, targetUrl.toString())) {
|
|
1340
1427
|
console.warn(` Expected ${targetUrl.toString()} but browser reported ${finalHref}.`);
|
|
1341
1428
|
return {
|
|
1342
1429
|
session,
|
|
1343
1430
|
lastKnownUrl,
|
|
1344
|
-
failure: { route: displayPath, reason:
|
|
1431
|
+
failure: { route: displayPath, reason: "unexpected location after navigation" },
|
|
1345
1432
|
};
|
|
1346
1433
|
}
|
|
1347
1434
|
if (diagnosticsContainBlockingIssues(diagnostics)) {
|
|
1348
|
-
console.warn(
|
|
1349
|
-
logBootstrapDiagnostics(
|
|
1435
|
+
console.warn(" Blocking diagnostics detected while loading the route:");
|
|
1436
|
+
logBootstrapDiagnostics("Smoke", diagnostics);
|
|
1350
1437
|
return {
|
|
1351
1438
|
session,
|
|
1352
1439
|
lastKnownUrl,
|
|
1353
|
-
failure: { route: displayPath, reason:
|
|
1440
|
+
failure: { route: displayPath, reason: "runtime diagnostics reported blocking issues" },
|
|
1354
1441
|
};
|
|
1355
1442
|
}
|
|
1356
1443
|
const consoleEvents = await fetchConsoleEvents(context.config, activeSessionId).catch(() => []);
|
|
@@ -1360,26 +1447,26 @@ async function executeSmokeRoute(context, state, route, routeIndex) {
|
|
|
1360
1447
|
}
|
|
1361
1448
|
const authEvents = newEvents.filter((event) => consoleEventIndicatesAuthIssue(event));
|
|
1362
1449
|
if (authEvents.length > 0) {
|
|
1363
|
-
console.warn(
|
|
1450
|
+
console.warn(" Detected authentication failures in the console log:");
|
|
1364
1451
|
for (const event of authEvents.slice(-5)) {
|
|
1365
1452
|
console.warn(` ${formatConsoleEventSummary(event)}`);
|
|
1366
1453
|
}
|
|
1367
1454
|
return {
|
|
1368
1455
|
session,
|
|
1369
1456
|
lastKnownUrl,
|
|
1370
|
-
failure: { route: displayPath, reason:
|
|
1457
|
+
failure: { route: displayPath, reason: "authentication failures detected in console output" },
|
|
1371
1458
|
};
|
|
1372
1459
|
}
|
|
1373
1460
|
const runtimeErrorEvents = newEvents.filter((event) => consoleEventIndicatesRuntimeError(event));
|
|
1374
1461
|
if (runtimeErrorEvents.length > 0) {
|
|
1375
|
-
console.warn(
|
|
1462
|
+
console.warn(" Detected console errors after the route finished loading:");
|
|
1376
1463
|
for (const event of runtimeErrorEvents.slice(-5)) {
|
|
1377
1464
|
console.warn(` ${formatConsoleEventSummary(event)}`);
|
|
1378
1465
|
}
|
|
1379
1466
|
return {
|
|
1380
1467
|
session,
|
|
1381
1468
|
lastKnownUrl,
|
|
1382
|
-
failure: { route: displayPath, reason:
|
|
1469
|
+
failure: { route: displayPath, reason: "console errors detected after load" },
|
|
1383
1470
|
};
|
|
1384
1471
|
}
|
|
1385
1472
|
try {
|
|
@@ -1387,10 +1474,10 @@ async function executeSmokeRoute(context, state, route, routeIndex) {
|
|
|
1387
1474
|
}
|
|
1388
1475
|
catch (error) {
|
|
1389
1476
|
if (sweetLinkDebug) {
|
|
1390
|
-
console.warn(
|
|
1477
|
+
console.warn("Failed to persist smoke progress after route completion:", error);
|
|
1391
1478
|
}
|
|
1392
1479
|
}
|
|
1393
|
-
console.log(
|
|
1480
|
+
console.log(" ✅ Route passed without authentication or runtime errors.");
|
|
1394
1481
|
return {
|
|
1395
1482
|
session,
|
|
1396
1483
|
lastKnownUrl,
|
|
@@ -1413,9 +1500,9 @@ async function runSweetLinkSmoke(params, command) {
|
|
|
1413
1500
|
const initialConsoleEvents = await fetchConsoleEvents(config, sessionId).catch(() => []);
|
|
1414
1501
|
const seenConsoleIds = new Set(initialConsoleEvents.map((event) => event.id));
|
|
1415
1502
|
const remainingRouteCount = params.routes.length - startIndex;
|
|
1416
|
-
console.log(`Running SweetLink smoke test across ${remainingRouteCount} route${remainingRouteCount === 1 ?
|
|
1503
|
+
console.log(`Running SweetLink smoke test across ${remainingRouteCount} route${remainingRouteCount === 1 ? "" : "s"} using session ${formatSessionHeadline(initialSession)}.`);
|
|
1417
1504
|
if (startIndex > 0) {
|
|
1418
|
-
console.log(`Skipping ${startIndex} route${startIndex === 1 ?
|
|
1505
|
+
console.log(`Skipping ${startIndex} route${startIndex === 1 ? "" : "s"} that already passed in a previous run.`);
|
|
1419
1506
|
}
|
|
1420
1507
|
let lastKnownUrl = initialSession.url ?? baseUrl.toString();
|
|
1421
1508
|
const context = {
|
|
@@ -1441,11 +1528,11 @@ async function runSweetLinkSmoke(params, command) {
|
|
|
1441
1528
|
}
|
|
1442
1529
|
}
|
|
1443
1530
|
if (failures.length > 0) {
|
|
1444
|
-
console.error(`\nSweetLink smoke test detected issues on ${failures.length} route${failures.length === 1 ?
|
|
1531
|
+
console.error(`\nSweetLink smoke test detected issues on ${failures.length} route${failures.length === 1 ? "" : "s"}:`);
|
|
1445
1532
|
for (const failure of failures) {
|
|
1446
1533
|
console.error(` - ${failure.route}: ${failure.reason}`);
|
|
1447
1534
|
}
|
|
1448
|
-
console.error(
|
|
1535
|
+
console.error("Review the diagnostics above, fix the auth flow, and rerun `pnpm sweetlink smoke`.");
|
|
1449
1536
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
1450
1537
|
process.exitCode = 1;
|
|
1451
1538
|
}
|
|
@@ -1456,18 +1543,18 @@ async function runSweetLinkSmoke(params, command) {
|
|
|
1456
1543
|
}
|
|
1457
1544
|
catch (error) {
|
|
1458
1545
|
if (sweetLinkDebug) {
|
|
1459
|
-
console.warn(
|
|
1546
|
+
console.warn("Failed to clear smoke progress after successful run:", error);
|
|
1460
1547
|
}
|
|
1461
1548
|
}
|
|
1462
|
-
console.log(
|
|
1549
|
+
console.log("\nSweetLink smoke test passed for all routes.");
|
|
1463
1550
|
}
|
|
1464
1551
|
}
|
|
1465
1552
|
function extractPathSegments(routePath) {
|
|
1466
1553
|
const normalized = trimTrailingSlash(routePath);
|
|
1467
|
-
if (normalized ===
|
|
1554
|
+
if (normalized === "/" || normalized.length === 0) {
|
|
1468
1555
|
return [];
|
|
1469
1556
|
}
|
|
1470
|
-
return normalized.replace(LEADING_SLASH_PATTERN,
|
|
1557
|
+
return normalized.replace(LEADING_SLASH_PATTERN, "").split("/");
|
|
1471
1558
|
}
|
|
1472
1559
|
function suffixSegmentsAllowed(segments) {
|
|
1473
1560
|
if (segments.length === 0) {
|
|
@@ -1485,8 +1572,8 @@ export const __sweetlinkCliTestHelpers = {
|
|
|
1485
1572
|
buildClickScript,
|
|
1486
1573
|
};
|
|
1487
1574
|
/* biome-ignore lint/performance/noBarrelFile: CLI entrypoint intentionally re-exports runtime helpers for compatibility. */
|
|
1488
|
-
export { diagnosticsContainBlockingIssues, logBootstrapDiagnostics } from
|
|
1489
|
-
export { buildClickScript, fetchConsoleEvents, fetchSessionSummaries, formatSessionHeadline, resolvePromptOption, resolveSessionIdFromHint, } from
|
|
1575
|
+
export { diagnosticsContainBlockingIssues, logBootstrapDiagnostics } from "./runtime/devtools.js";
|
|
1576
|
+
export { buildClickScript, fetchConsoleEvents, fetchSessionSummaries, formatSessionHeadline, resolvePromptOption, resolveSessionIdFromHint, } from "./runtime/session.js";
|
|
1490
1577
|
function urlsRoughlyMatch(a, b) {
|
|
1491
1578
|
const urlA = normalizeUrlForMatch(a);
|
|
1492
1579
|
const urlB = normalizeUrlForMatch(b);
|
|
@@ -1516,7 +1603,7 @@ function urlsRoughlyMatch(a, b) {
|
|
|
1516
1603
|
async function devtoolsAuthorize(options, command) {
|
|
1517
1604
|
const devtoolsConfig = await loadDevToolsConfig();
|
|
1518
1605
|
if (!devtoolsConfig?.devtoolsUrl) {
|
|
1519
|
-
console.log(
|
|
1606
|
+
console.log("No DevTools session detected. Launch Chrome with `pnpm sweetlink open --controlled` first.");
|
|
1520
1607
|
return;
|
|
1521
1608
|
}
|
|
1522
1609
|
const cliConfig = resolveConfig(command);
|
|
@@ -1549,12 +1636,12 @@ async function devtoolsAuthorize(options, command) {
|
|
|
1549
1636
|
}
|
|
1550
1637
|
catch (error) {
|
|
1551
1638
|
if (sweetLinkDebug) {
|
|
1552
|
-
console.warn(
|
|
1639
|
+
console.warn("Failed to inspect DevTools tabs for authorize command:", error);
|
|
1553
1640
|
}
|
|
1554
1641
|
}
|
|
1555
1642
|
}
|
|
1556
1643
|
if (!sessionUrl) {
|
|
1557
|
-
console.error(
|
|
1644
|
+
console.error("Unable to determine which tab contains the OAuth consent screen. Pass --url <authorizeUrl> to override.");
|
|
1558
1645
|
if (process.exitCode === undefined) {
|
|
1559
1646
|
process.exitCode = 1;
|
|
1560
1647
|
}
|
|
@@ -1567,18 +1654,18 @@ async function devtoolsAuthorize(options, command) {
|
|
|
1567
1654
|
scriptPath: cliConfig.oauthScriptPath,
|
|
1568
1655
|
});
|
|
1569
1656
|
if (result.handled) {
|
|
1570
|
-
console.log(`Authorize prompt handled via ${result.action ??
|
|
1657
|
+
console.log(`Authorize prompt handled via ${result.action ?? "click"}${result.clickedText ? ` (${result.clickedText})` : ""}.`);
|
|
1571
1658
|
}
|
|
1572
1659
|
else {
|
|
1573
|
-
const reason = result.reason ??
|
|
1660
|
+
const reason = result.reason ?? "no authorize button detected";
|
|
1574
1661
|
console.log(`Authorize prompt not handled automatically (${reason}).`);
|
|
1575
|
-
if (reason ===
|
|
1576
|
-
console.log(
|
|
1662
|
+
if (reason === "requires-login") {
|
|
1663
|
+
console.log("Twitter login inputs detected. Complete login manually and rerun the command.");
|
|
1577
1664
|
}
|
|
1578
1665
|
}
|
|
1579
1666
|
}
|
|
1580
1667
|
catch (error) {
|
|
1581
|
-
console.error(
|
|
1668
|
+
console.error("Failed to trigger OAuth authorize automation:", extractEventMessage(error));
|
|
1582
1669
|
if (process.exitCode === undefined) {
|
|
1583
1670
|
process.exitCode = 1;
|
|
1584
1671
|
}
|
|
@@ -1587,13 +1674,13 @@ async function devtoolsAuthorize(options, command) {
|
|
|
1587
1674
|
async function devtoolsStatus(options, command) {
|
|
1588
1675
|
const config = await loadDevToolsConfig();
|
|
1589
1676
|
if (!config) {
|
|
1590
|
-
console.log(
|
|
1677
|
+
console.log("No DevTools session detected. Launch Chrome with `pnpm sweetlink open --controlled` first.");
|
|
1591
1678
|
return;
|
|
1592
1679
|
}
|
|
1593
1680
|
let reachable;
|
|
1594
1681
|
let tabs = [];
|
|
1595
1682
|
try {
|
|
1596
|
-
const response = await fetch(`${config.devtoolsUrl.replace(TRAILING_SLASH_PATTERN,
|
|
1683
|
+
const response = await fetch(`${config.devtoolsUrl.replace(TRAILING_SLASH_PATTERN, "")}/json/version`, { method: "GET" });
|
|
1597
1684
|
if (response.ok) {
|
|
1598
1685
|
reachable = true;
|
|
1599
1686
|
tabs = await fetchDevToolsTabs(config.devtoolsUrl);
|
|
@@ -1637,7 +1724,7 @@ async function devtoolsStatus(options, command) {
|
|
|
1637
1724
|
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
1638
1725
|
return;
|
|
1639
1726
|
}
|
|
1640
|
-
const reachabilityLabel = isReachable ?
|
|
1727
|
+
const reachabilityLabel = isReachable ? "reachable" : "offline";
|
|
1641
1728
|
console.log(`DevTools endpoint : ${config.devtoolsUrl} (${reachabilityLabel})`);
|
|
1642
1729
|
console.log(`Last updated : ${new Date(config.updatedAt).toISOString()}`);
|
|
1643
1730
|
if (config.viewport) {
|
|
@@ -1646,7 +1733,7 @@ async function devtoolsStatus(options, command) {
|
|
|
1646
1733
|
}
|
|
1647
1734
|
console.log(`Tabs : ${tabs.length}`);
|
|
1648
1735
|
if (matchedSessionId) {
|
|
1649
|
-
console.log(`Linked session : ${matchedSessionId}${matchedSessionTitle ? ` (${matchedSessionTitle})` :
|
|
1736
|
+
console.log(`Linked session : ${matchedSessionId}${matchedSessionTitle ? ` (${matchedSessionTitle})` : ""}`);
|
|
1650
1737
|
}
|
|
1651
1738
|
console.log(`Console buffer : ${state?.console.length ?? 0} entries`);
|
|
1652
1739
|
console.log(`Network buffer : ${state?.network.length ?? 0} entries`);
|
|
@@ -1657,7 +1744,7 @@ async function devtoolsStatus(options, command) {
|
|
|
1657
1744
|
async function devtoolsTabs(options) {
|
|
1658
1745
|
const config = await loadDevToolsConfig();
|
|
1659
1746
|
if (!config) {
|
|
1660
|
-
console.log(
|
|
1747
|
+
console.log("No DevTools session detected. Launch Chrome with `pnpm sweetlink open --controlled` first.");
|
|
1661
1748
|
return;
|
|
1662
1749
|
}
|
|
1663
1750
|
const tabs = await fetchDevToolsTabs(config.devtoolsUrl);
|
|
@@ -1666,23 +1753,23 @@ async function devtoolsTabs(options) {
|
|
|
1666
1753
|
return;
|
|
1667
1754
|
}
|
|
1668
1755
|
if (tabs.length === 0) {
|
|
1669
|
-
console.log(
|
|
1756
|
+
console.log("No tabs reported by DevTools.");
|
|
1670
1757
|
return;
|
|
1671
1758
|
}
|
|
1672
1759
|
for (const tab of tabs) {
|
|
1673
|
-
console.log(`• ${tab.title ||
|
|
1760
|
+
console.log(`• ${tab.title || "(untitled)"}`);
|
|
1674
1761
|
console.log(` URL : ${tab.url}`);
|
|
1675
1762
|
console.log(` ID : ${tab.id}`);
|
|
1676
1763
|
if (tab.type) {
|
|
1677
1764
|
console.log(` Type: ${tab.type}`);
|
|
1678
1765
|
}
|
|
1679
|
-
console.log(
|
|
1766
|
+
console.log("");
|
|
1680
1767
|
}
|
|
1681
1768
|
}
|
|
1682
1769
|
async function devtoolsShowConsole(options) {
|
|
1683
1770
|
const state = await loadDevToolsState();
|
|
1684
1771
|
if (!state || state.console.length === 0) {
|
|
1685
|
-
console.log(
|
|
1772
|
+
console.log("No console events recorded. Run `pnpm sweetlink devtools listen` to start capturing telemetry.");
|
|
1686
1773
|
return;
|
|
1687
1774
|
}
|
|
1688
1775
|
const tail = Number.isFinite(options.tail) && options.tail > 0 ? Math.floor(options.tail) : 50;
|
|
@@ -1693,19 +1780,19 @@ async function devtoolsShowConsole(options) {
|
|
|
1693
1780
|
}
|
|
1694
1781
|
for (const entry of entries) {
|
|
1695
1782
|
const timestamp = new Date(entry.ts).toISOString();
|
|
1696
|
-
let location =
|
|
1783
|
+
let location = "";
|
|
1697
1784
|
if (entry.location?.url) {
|
|
1698
|
-
const lineSuffix = entry.location.lineNumber === undefined ?
|
|
1699
|
-
const columnSuffix = entry.location.columnNumber === undefined ?
|
|
1785
|
+
const lineSuffix = entry.location.lineNumber === undefined ? "" : `:${entry.location.lineNumber}`;
|
|
1786
|
+
const columnSuffix = entry.location.columnNumber === undefined ? "" : `:${entry.location.columnNumber}`;
|
|
1700
1787
|
location = ` (${entry.location.url}${lineSuffix}${columnSuffix})`;
|
|
1701
1788
|
}
|
|
1702
|
-
let argsSuffix =
|
|
1789
|
+
let argsSuffix = "";
|
|
1703
1790
|
if (entry.args.length > 0) {
|
|
1704
1791
|
const formattedArgs = [];
|
|
1705
1792
|
for (const value of entry.args) {
|
|
1706
1793
|
formattedArgs.push(formatConsoleArg(value));
|
|
1707
1794
|
}
|
|
1708
|
-
argsSuffix = ` ${formattedArgs.join(
|
|
1795
|
+
argsSuffix = ` ${formattedArgs.join(" ")}`;
|
|
1709
1796
|
}
|
|
1710
1797
|
console.log(`[${timestamp}] ${entry.type}: ${entry.text}${argsSuffix}${location}`);
|
|
1711
1798
|
}
|
|
@@ -1713,7 +1800,7 @@ async function devtoolsShowConsole(options) {
|
|
|
1713
1800
|
async function devtoolsShowNetwork(options) {
|
|
1714
1801
|
const state = await loadDevToolsState();
|
|
1715
1802
|
if (!state || state.network.length === 0) {
|
|
1716
|
-
console.log(
|
|
1803
|
+
console.log("No network entries recorded. Run `pnpm sweetlink devtools listen` to start capturing telemetry.");
|
|
1717
1804
|
return;
|
|
1718
1805
|
}
|
|
1719
1806
|
const tail = Number.isFinite(options.tail) && options.tail > 0 ? Math.floor(options.tail) : 50;
|
|
@@ -1724,8 +1811,8 @@ async function devtoolsShowNetwork(options) {
|
|
|
1724
1811
|
}
|
|
1725
1812
|
for (const entry of entries) {
|
|
1726
1813
|
const timestamp = new Date(entry.ts).toISOString();
|
|
1727
|
-
const status = entry.status === undefined ? (entry.failureText ??
|
|
1728
|
-
const type = entry.resourceType ? ` [${entry.resourceType}]` :
|
|
1814
|
+
const status = entry.status === undefined ? (entry.failureText ?? "failed") : entry.status;
|
|
1815
|
+
const type = entry.resourceType ? ` [${entry.resourceType}]` : "";
|
|
1729
1816
|
console.log(`[${timestamp}] ${entry.method} ${status} ${entry.url}${type}`);
|
|
1730
1817
|
}
|
|
1731
1818
|
}
|
|
@@ -1734,7 +1821,7 @@ async function devtoolsListen(options, command) {
|
|
|
1734
1821
|
const config = await loadDevToolsConfig();
|
|
1735
1822
|
if (!config) {
|
|
1736
1823
|
if (!background) {
|
|
1737
|
-
console.log(
|
|
1824
|
+
console.log("No DevTools session detected. Launch Chrome with `pnpm sweetlink open --controlled` first.");
|
|
1738
1825
|
}
|
|
1739
1826
|
return;
|
|
1740
1827
|
}
|
|
@@ -1742,12 +1829,12 @@ async function devtoolsListen(options, command) {
|
|
|
1742
1829
|
try {
|
|
1743
1830
|
await rm(DEVTOOLS_STATE_PATH, { force: true });
|
|
1744
1831
|
if (!background) {
|
|
1745
|
-
console.log(
|
|
1832
|
+
console.log("Cleared cached DevTools telemetry state.");
|
|
1746
1833
|
}
|
|
1747
1834
|
}
|
|
1748
1835
|
catch (error) {
|
|
1749
|
-
if (!isErrnoException(error) || error.code !==
|
|
1750
|
-
console.warn(
|
|
1836
|
+
if (!isErrnoException(error) || error.code !== "ENOENT") {
|
|
1837
|
+
console.warn("Failed to reset DevTools state:", error);
|
|
1751
1838
|
}
|
|
1752
1839
|
}
|
|
1753
1840
|
}
|
|
@@ -1763,10 +1850,10 @@ async function devtoolsListen(options, command) {
|
|
|
1763
1850
|
await saveDevToolsState(state);
|
|
1764
1851
|
}
|
|
1765
1852
|
catch (error) {
|
|
1766
|
-
console.warn(
|
|
1853
|
+
console.warn("Failed to persist reset DevTools state:", error);
|
|
1767
1854
|
}
|
|
1768
1855
|
if (!background) {
|
|
1769
|
-
console.log(
|
|
1856
|
+
console.log("Reset complete. Re-run without --reset to resume live capture.");
|
|
1770
1857
|
}
|
|
1771
1858
|
return;
|
|
1772
1859
|
}
|
|
@@ -1782,7 +1869,7 @@ async function devtoolsListen(options, command) {
|
|
|
1782
1869
|
await saveDevToolsConfig({ devtoolsUrl: config.devtoolsUrl, sessionId });
|
|
1783
1870
|
}
|
|
1784
1871
|
catch (error) {
|
|
1785
|
-
logDebugError(
|
|
1872
|
+
logDebugError("Failed to persist DevTools session binding", error);
|
|
1786
1873
|
}
|
|
1787
1874
|
}
|
|
1788
1875
|
if (!background) {
|
|
@@ -1794,8 +1881,8 @@ async function devtoolsListen(options, command) {
|
|
|
1794
1881
|
({ browser, page } = await connectToDevTools(config));
|
|
1795
1882
|
}
|
|
1796
1883
|
catch (error) {
|
|
1797
|
-
console.error(
|
|
1798
|
-
console.error(
|
|
1884
|
+
console.error("Failed to connect to the DevTools endpoint:", extractEventMessage(error));
|
|
1885
|
+
console.error("Hint: ensure a controlled Chrome window is running via `pnpm sweetlink open --controlled`.");
|
|
1799
1886
|
process.exitCode = 1;
|
|
1800
1887
|
return;
|
|
1801
1888
|
}
|
|
@@ -1809,7 +1896,7 @@ async function devtoolsListen(options, command) {
|
|
|
1809
1896
|
await saveDevToolsConfig({ devtoolsUrl: config.devtoolsUrl, viewport });
|
|
1810
1897
|
}
|
|
1811
1898
|
catch (error) {
|
|
1812
|
-
logDebugError(
|
|
1899
|
+
logDebugError("Failed to persist DevTools viewport configuration", error);
|
|
1813
1900
|
}
|
|
1814
1901
|
const flush = async () => {
|
|
1815
1902
|
await saveDevToolsState(devtoolsState);
|
|
@@ -1824,14 +1911,14 @@ async function devtoolsListen(options, command) {
|
|
|
1824
1911
|
timer = setTimeout(() => {
|
|
1825
1912
|
timer = null;
|
|
1826
1913
|
flush().catch((error) => {
|
|
1827
|
-
console.warn(
|
|
1914
|
+
console.warn("Failed to persist DevTools telemetry:", error);
|
|
1828
1915
|
});
|
|
1829
1916
|
}, 300);
|
|
1830
1917
|
};
|
|
1831
1918
|
})();
|
|
1832
1919
|
const attachListeners = (p) => {
|
|
1833
1920
|
// Capture console and network events for the page and trim buffers as they grow.
|
|
1834
|
-
p.on(
|
|
1921
|
+
p.on("console", (message) => {
|
|
1835
1922
|
const persistConsole = async () => {
|
|
1836
1923
|
const entry = await serializeConsoleMessage(message);
|
|
1837
1924
|
devtoolsState.console.push(entry);
|
|
@@ -1839,10 +1926,10 @@ async function devtoolsListen(options, command) {
|
|
|
1839
1926
|
scheduleFlush();
|
|
1840
1927
|
};
|
|
1841
1928
|
persistConsole().catch((error) => {
|
|
1842
|
-
console.warn(
|
|
1929
|
+
console.warn("Failed to serialize console message:", error);
|
|
1843
1930
|
});
|
|
1844
1931
|
});
|
|
1845
|
-
p.on(
|
|
1932
|
+
p.on("requestfinished", (request) => {
|
|
1846
1933
|
const persistNetworkEntry = async () => {
|
|
1847
1934
|
const response = await request.response();
|
|
1848
1935
|
const entry = createNetworkEntryFromRequest(request, response?.status());
|
|
@@ -1851,12 +1938,12 @@ async function devtoolsListen(options, command) {
|
|
|
1851
1938
|
scheduleFlush();
|
|
1852
1939
|
};
|
|
1853
1940
|
persistNetworkEntry().catch((error) => {
|
|
1854
|
-
console.warn(
|
|
1941
|
+
console.warn("Failed to serialize network request:", error);
|
|
1855
1942
|
});
|
|
1856
1943
|
});
|
|
1857
|
-
p.on(
|
|
1944
|
+
p.on("requestfailed", (request) => {
|
|
1858
1945
|
const failure = request.failure();
|
|
1859
|
-
const entry = createNetworkEntryFromRequest(request, undefined, failure?.errorText ??
|
|
1946
|
+
const entry = createNetworkEntryFromRequest(request, undefined, failure?.errorText ?? "failed");
|
|
1860
1947
|
devtoolsState.network.push(entry);
|
|
1861
1948
|
trimBuffer(devtoolsState.network, DEVTOOLS_NETWORK_LIMIT);
|
|
1862
1949
|
scheduleFlush();
|
|
@@ -1864,11 +1951,11 @@ async function devtoolsListen(options, command) {
|
|
|
1864
1951
|
};
|
|
1865
1952
|
attachListeners(page);
|
|
1866
1953
|
const context = page.context();
|
|
1867
|
-
context.on(
|
|
1954
|
+
context.on("page", (newPage) => {
|
|
1868
1955
|
attachListeners(newPage);
|
|
1869
1956
|
});
|
|
1870
1957
|
if (!background) {
|
|
1871
|
-
console.log(
|
|
1958
|
+
console.log("Listening for console and network events… Press Ctrl+C to stop.");
|
|
1872
1959
|
}
|
|
1873
1960
|
const shutdown = async () => {
|
|
1874
1961
|
try {
|
|
@@ -1876,7 +1963,7 @@ async function devtoolsListen(options, command) {
|
|
|
1876
1963
|
}
|
|
1877
1964
|
catch (error) {
|
|
1878
1965
|
if (sweetLinkDebug) {
|
|
1879
|
-
console.warn(
|
|
1966
|
+
console.warn("Failed to flush DevTools state during shutdown.", error);
|
|
1880
1967
|
}
|
|
1881
1968
|
}
|
|
1882
1969
|
try {
|
|
@@ -1884,22 +1971,22 @@ async function devtoolsListen(options, command) {
|
|
|
1884
1971
|
}
|
|
1885
1972
|
catch (error) {
|
|
1886
1973
|
if (sweetLinkDebug) {
|
|
1887
|
-
console.warn(
|
|
1974
|
+
console.warn("Failed to close DevTools browser during shutdown.", error);
|
|
1888
1975
|
}
|
|
1889
1976
|
}
|
|
1890
1977
|
process.exit(0);
|
|
1891
1978
|
};
|
|
1892
|
-
process.once(
|
|
1979
|
+
process.once("SIGINT", () => {
|
|
1893
1980
|
shutdown().catch((error) => {
|
|
1894
1981
|
if (sweetLinkDebug) {
|
|
1895
|
-
console.warn(
|
|
1982
|
+
console.warn("Graceful shutdown after SIGINT failed:", error);
|
|
1896
1983
|
}
|
|
1897
1984
|
});
|
|
1898
1985
|
});
|
|
1899
|
-
process.once(
|
|
1986
|
+
process.once("SIGTERM", () => {
|
|
1900
1987
|
shutdown().catch((error) => {
|
|
1901
1988
|
if (sweetLinkDebug) {
|
|
1902
|
-
console.warn(
|
|
1989
|
+
console.warn("Graceful shutdown after SIGTERM failed:", error);
|
|
1903
1990
|
}
|
|
1904
1991
|
});
|
|
1905
1992
|
});
|