@speedkit/cli 4.5.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -2
  3. package/dist/commands/onboarding.js +2 -2
  4. package/dist/helpers/site-analyzer.d.ts +1 -0
  5. package/dist/helpers/site-analyzer.js +2 -0
  6. package/dist/services/customer-config/customer-config-service-model.d.ts +1 -0
  7. package/dist/services/customer-config/customer-config-service.js +9 -0
  8. package/dist/services/customer-config/templates/config_documentHandler.js.hbs +3 -0
  9. package/dist/services/onboarding/browser/executable/executable-validator.js +4 -3
  10. package/dist/services/onboarding/browser/extension/devtools-extension-api.d.ts +21 -0
  11. package/dist/services/onboarding/browser/extension/devtools-extension-api.js +38 -0
  12. package/dist/services/onboarding/browser/extension/extension-bridge.d.ts +51 -0
  13. package/dist/services/onboarding/browser/extension/extension-bridge.js +165 -0
  14. package/dist/services/onboarding/onboarding-model.d.ts +43 -5
  15. package/dist/services/onboarding/onboarding-model.js +11 -3
  16. package/dist/services/onboarding/onboarding-model.spec.d.ts +1 -0
  17. package/dist/services/onboarding/onboarding-model.spec.js +28 -0
  18. package/dist/services/onboarding/onboarding-service-factory.js +16 -6
  19. package/dist/services/onboarding/onboarding-service.d.ts +11 -1
  20. package/dist/services/onboarding/onboarding-service.js +19 -10
  21. package/dist/services/onboarding/virtual-orestes-app/index.d.ts +8 -1
  22. package/dist/services/onboarding/virtual-orestes-app/index.js +37 -1
  23. package/oclif.manifest.json +9 -1
  24. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ # [4.6.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.5.0...v4.6.0) (2026-05-29)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **onboarding:** include product name in Windows browser version ([a058447](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/a058447e3a0e3f26a57bc9bf4581943132f2183b))
7
+
8
+
9
+ ### Features
10
+
11
+ * **customer-config:** add usercentrics handling ([c8b1da7](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/c8b1da76a1e3bd629e029aa79fa8e5252c9faf10))
12
+ * **extension-bridge:** write CLI state to devtools via chrome.storage.local ([0942178](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/09421783de1116c5cf6c346230ed1b4625d8a584))
13
+ * **local-cache-errors:** emit transform failures to devtools panel ([5989846](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/598984629c7dc4de2cc9c064457a410f0e433e54))
14
+ * **onboarding:** configurable mcp port ([ec562f4](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/ec562f42f53ad0144c25373eaca1998a411b3b33))
15
+ * **onboarding:** configurable mcp port ([6649b79](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/6649b798ff935e7714c28d3c046a25d950d18f82))
16
+ * **project:** claude ([0332a46](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/0332a46778e5a4711d7919e53f53b74064679c00))
17
+
1
18
  # [4.5.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.4.2...v4.5.0) (2026-05-15)
2
19
 
3
20
 
package/README.md CHANGED
@@ -21,7 +21,7 @@ $ npm install -g @speedkit/cli
21
21
  $ sk COMMAND
22
22
  running command...
23
23
  $ sk (--version)
24
- @speedkit/cli/4.5.0 linux-x64 node-v22.22.3
24
+ @speedkit/cli/4.6.0 linux-x64 node-v22.22.3
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -383,7 +383,8 @@ Start a local dev environment for building and testing the Speed Kit configurati
383
383
 
384
384
  ```
385
385
  USAGE
386
- $ sk onboarding CUSTOMERPATH [-c <value>] [-t] [-d <value>] [-l] [-p] [-v] [-s] [--headless]
386
+ $ sk onboarding CUSTOMERPATH [-c <value>] [-t] [-d <value>] [-l] [-p] [--debugPort <value>] [-v] [-s]
387
+ [--headless]
387
388
 
388
389
  ARGUMENTS
389
390
  CUSTOMERPATH The customer config path
@@ -396,6 +397,8 @@ FLAGS
396
397
  -s, --ignoreContentSecurityPolicy Removes header/meta "content-security-policy" for origin-responses
397
398
  -t, --useTestConfig Override app with configured testApp
398
399
  -v, --verboseLevel Show all messages
400
+ --debugPort=<value> [default: 9222] Chrome remote debugging port (only used with -p/--headless). Use
401
+ distinct ports to run multiple onboarding processes at once.
399
402
  --headless Run Chrome in headless mode (for CI environments). Implicitly enables remote
400
403
  debugging port.
401
404
 
@@ -11,8 +11,8 @@ export default class Onboarding extends Command {
11
11
  `$ sk onboarding ${CLIParametersExample.CustomerPath} -${CLIParametersChar.ConfigName} ${CLIParametersExample.ConfigName} -${CLIParametersChar.Domain} ${CLIParametersExample.Domain}`,
12
12
  ];
13
13
  async run() {
14
- const { args: { customerPath }, flags: { configName, domain, local, verboseLevel, ignoreContentSecurityPolicy, useTestConfig, debuggingPort, headless, }, } = await this.parse(Onboarding);
15
- const service = await new OnboardingServiceFactory(new OnboardingContext(customerPath, configName, domain, local, ignoreContentSecurityPolicy, this.config.root, verboseLevel, useTestConfig, debuggingPort, headless), new CliConfig(this.config).load(customerPath)).getService();
14
+ const { args: { customerPath }, flags: { configName, domain, local, verboseLevel, ignoreContentSecurityPolicy, useTestConfig, debuggingPort, headless, debugPort, }, } = await this.parse(Onboarding);
15
+ const service = await new OnboardingServiceFactory(new OnboardingContext(customerPath, configName, domain, local, ignoreContentSecurityPolicy, this.config.root, verboseLevel, useTestConfig, debuggingPort, headless, debugPort), new CliConfig(this.config).load(customerPath)).getService();
16
16
  const shutdown = new AbortController();
17
17
  const onSignal = () => shutdown.abort();
18
18
  // `once`: a second Ctrl+C falls through to Node's default handler and
@@ -6,6 +6,7 @@ export interface SiteAnalysisResult {
6
6
  includeServiceWorker: boolean;
7
7
  removeLazyLoading: boolean;
8
8
  hasSoftNavigations: boolean;
9
+ handleUsercentrics: boolean;
9
10
  shopSystem: DetectedShopSystem;
10
11
  hasInstallJs: boolean;
11
12
  }
@@ -7,6 +7,7 @@ const EMPTY_FLAGS = {
7
7
  includeServiceWorker: false,
8
8
  removeLazyLoading: false,
9
9
  hasSoftNavigations: false,
10
+ handleUsercentrics: false,
10
11
  };
11
12
  function detectFeatures(html, headers) {
12
13
  return {
@@ -16,6 +17,7 @@ function detectFeatures(html, headers) {
16
17
  includeServiceWorker: /navigator\.serviceWorker\.register|serviceWorker\.register/.test(html),
17
18
  removeLazyLoading: /loading=["']lazy["']|data-src=|class=["'][^"']*lazyload|lazysizes/.test(html),
18
19
  hasSoftNavigations: /__NEXT_DATA__|__NUXT__|vue-router|react-router|history\.pushState/.test(html),
20
+ handleUsercentrics: /uc-block(\.[\w-]+)*\.js|usercentrics\.eu|Usercentrics|data-usercentrics|__ucCmp|window\.UC_UI/.test(html),
19
21
  };
20
22
  }
21
23
  function mergeFeatures(results) {
@@ -48,6 +48,7 @@ export interface CustomerConfigSettings {
48
48
  activateCfRocketLoaderPlugin?: boolean;
49
49
  hasSoftNavigations?: boolean;
50
50
  convertSoftToHardNavigations?: boolean;
51
+ handleUsercentrics?: boolean;
51
52
  isPOV?: boolean;
52
53
  useDisabledSitesApproach?: boolean;
53
54
  shopSystemName?: string;
@@ -251,6 +251,7 @@ export class CustomerConfigService {
251
251
  const gaTracking = resolve("useGATracking", true);
252
252
  const googleOptimize = resolve("withGoogleOptimize", false);
253
253
  const cfRocketLoader = resolve("activateCfRocketLoaderPlugin", false);
254
+ const usercentrics = resolve("handleUsercentrics", false);
254
255
  // Detectable features — sorted detected-first when analysis is available
255
256
  const detectableChoices = [
256
257
  {
@@ -293,6 +294,12 @@ export class CustomerConfigService {
293
294
  checked: cfRocketLoader.checked,
294
295
  description: "Handle Cloudflare Rocket Loader script deferral",
295
296
  },
297
+ {
298
+ name: `Usercentrics Handling${usercentrics.label}`,
299
+ value: "handleUsercentrics",
300
+ checked: usercentrics.checked,
301
+ description: "Set handleUsercentrics: true in DH Config — declares uc-block.bundle.js (Usercentrics CMP) as safe script",
302
+ },
296
303
  ];
297
304
  if (siteAnalysis) {
298
305
  const detectionRank = (name) => {
@@ -352,6 +359,7 @@ export class CustomerConfigService {
352
359
  const isPOV = has("isPOV");
353
360
  const withGoogleOptimize = has("withGoogleOptimize");
354
361
  const activateCfRocketLoaderPlugin = has("activateCfRocketLoaderPlugin");
362
+ const handleUsercentrics = has("handleUsercentrics");
355
363
  const useScrapingBee = has("useScrapingBee");
356
364
  const addDowntimeDetection = has("addDowntimeDetection");
357
365
  // Follow-up questions for features that need additional input
@@ -385,6 +393,7 @@ export class CustomerConfigService {
385
393
  useScrapingBee,
386
394
  withGoogleOptimize,
387
395
  activateCfRocketLoaderPlugin,
396
+ handleUsercentrics,
388
397
  hasSoftNavigations,
389
398
  convertSoftToHardNavigations,
390
399
  useDisabledSitesApproach,
@@ -106,6 +106,9 @@ const config = {
106
106
  {{#if activateCfRocketLoaderPlugin}}
107
107
  handleRocketLoader: true,
108
108
  {{/if}}
109
+ {{#if handleUsercentrics}}
110
+ handleUsercentrics: true,
111
+ {{/if}}
109
112
  {{#if isShopify}}
110
113
  delayScriptPath: "/apps/speed-kit/speed-kit-dom-ready.js", // Shopify specific
111
114
  {{else}}
@@ -51,10 +51,11 @@ export class ExecutableValidator {
51
51
  }
52
52
  getCurrentBrowserVersion() {
53
53
  // on windows executing chrome to gather the version will spawn a chromeProcess
54
- // so we use a powershell instead
54
+ // so we use a powershell instead. Emit `<ProductName> <FileVersion>` so the
55
+ // output is shape-compatible with `chrome --version` on macOS/Linux —
56
+ // downstream consumers (isChromeBrowser, log lines) can stay OS-agnostic.
55
57
  if (this.userConfig.runtimeConfig.windows) {
56
- return this.cli.execSync(`(Get-Item '${this.userConfig.chromePath}').VersionInfo.FileVersion`, { shell: "powershell.exe" });
57
- // return this.userConfig.chromePath;
58
+ return this.cli.execSync(`$i = (Get-Item '${this.userConfig.chromePath}').VersionInfo; "$($i.ProductName) $($i.FileVersion)"`, { shell: "powershell.exe" });
58
59
  }
59
60
  return this.cli.execSync(`"${this.userConfig.chromePath}" --version`);
60
61
  }
@@ -1,7 +1,28 @@
1
1
  import { DevelopmentToolsMessage } from "../../onboarding-model.js";
2
+ import { ExtensionBridge } from "./extension-bridge.js";
3
+ /**
4
+ * In-memory queue of `DevelopmentToolsMessage` snapshots delivered to the
5
+ * devtools extension. Producers (factory, onboarding-service CDP listeners)
6
+ * call `addMessage`. Once an `ExtensionBridge` is attached, every change
7
+ * triggers a fire-and-forget push that mirrors the full message array into
8
+ * the extension's `chrome.storage.local`.
9
+ *
10
+ * Messages added before the bridge is attached stay queued — `setBridge`
11
+ * flushes them.
12
+ */
2
13
  export declare class DevtoolsExtensionApi {
3
14
  messages: DevelopmentToolsMessage[];
15
+ private bridge;
4
16
  getMessages(): DevelopmentToolsMessage[];
5
17
  addMessage(message: DevelopmentToolsMessage): void;
18
+ /**
19
+ * Replace the existing message with the same `id`, or push if none yet.
20
+ * Use this for "current state" messages (e.g. `localCacheError` carries
21
+ * the live error set, not a history of events) so the devtools panel
22
+ * doesn't have to dedupe.
23
+ */
24
+ upsertMessage(message: DevelopmentToolsMessage): void;
6
25
  clear(): void;
26
+ setBridge(bridge: ExtensionBridge): void;
27
+ private flush;
7
28
  }
@@ -1,12 +1,50 @@
1
+ /**
2
+ * In-memory queue of `DevelopmentToolsMessage` snapshots delivered to the
3
+ * devtools extension. Producers (factory, onboarding-service CDP listeners)
4
+ * call `addMessage`. Once an `ExtensionBridge` is attached, every change
5
+ * triggers a fire-and-forget push that mirrors the full message array into
6
+ * the extension's `chrome.storage.local`.
7
+ *
8
+ * Messages added before the bridge is attached stay queued — `setBridge`
9
+ * flushes them.
10
+ */
1
11
  export class DevtoolsExtensionApi {
2
12
  messages = [];
13
+ bridge = null;
3
14
  getMessages() {
4
15
  return this.messages;
5
16
  }
6
17
  addMessage(message) {
7
18
  this.messages.push(message);
19
+ void this.flush();
20
+ }
21
+ /**
22
+ * Replace the existing message with the same `id`, or push if none yet.
23
+ * Use this for "current state" messages (e.g. `localCacheError` carries
24
+ * the live error set, not a history of events) so the devtools panel
25
+ * doesn't have to dedupe.
26
+ */
27
+ upsertMessage(message) {
28
+ const idx = this.messages.findIndex((m) => m.id === message.id);
29
+ if (idx >= 0) {
30
+ this.messages[idx] = message;
31
+ }
32
+ else {
33
+ this.messages.push(message);
34
+ }
35
+ void this.flush();
8
36
  }
9
37
  clear() {
10
38
  this.messages = [];
39
+ void this.flush();
40
+ }
41
+ setBridge(bridge) {
42
+ this.bridge = bridge;
43
+ void this.flush();
44
+ }
45
+ async flush() {
46
+ if (!this.bridge)
47
+ return;
48
+ await this.bridge.writeMessages(this.messages);
11
49
  }
12
50
  }
@@ -0,0 +1,51 @@
1
+ import { Browser } from "puppeteer";
2
+ import { DevelopmentToolsMessage } from "../../onboarding-model.js";
3
+ import { CliServiceInterface } from "../../../cli/index.js";
4
+ /**
5
+ * Writes onboarding state (messages, status) into the devtools extension's
6
+ * chrome.storage.local via a CDP session against the extension's MV3
7
+ * service-worker target. The devtools extension subscribes via
8
+ * chrome.storage.onChanged. Replaces the legacy `window.skCliMessages` page
9
+ * injection — no more customer-visible global state.
10
+ *
11
+ * Lifecycle: construct once per browser, call `init()` after the browser is
12
+ * launched and the devtools extension has loaded, then call `writeMessages`
13
+ * whenever the message list changes.
14
+ */
15
+ export declare class ExtensionBridge {
16
+ private readonly cli;
17
+ private browser;
18
+ private extensionId;
19
+ constructor(cli: CliServiceInterface);
20
+ /**
21
+ * Bind the bridge to a freshly-launched browser, discover the
22
+ * devtools-extension's service-worker target, and cache its extension ID.
23
+ * Polls for ~INIT_POLL_TIMEOUT_MS because MV3 service workers are
24
+ * sometimes not registered as targets until Chrome has finished loading
25
+ * the extension. Throws if no matching SW target is found in time.
26
+ */
27
+ init(browser: Browser): Promise<void>;
28
+ /**
29
+ * Write the current messages snapshot into chrome.storage.local. One retry
30
+ * on transient failures (MV3 service workers can briefly disappear during
31
+ * respawn). Failures are surfaced via `cli.writeError` but do not throw —
32
+ * a missed write is degraded UX, not a fatal onboarding failure.
33
+ */
34
+ writeMessages(messages: DevelopmentToolsMessage[]): Promise<void>;
35
+ private evaluateInServiceWorker;
36
+ /**
37
+ * Look up the service-worker target by cached extension ID. Returns null
38
+ * when the SW is dormant — callers handle that via retry.
39
+ */
40
+ private getServiceWorkerTarget;
41
+ /**
42
+ * Scan service-worker targets, evaluate `chrome.runtime.getManifest().name`
43
+ * on each, and return the first whose name matches the devtools-extension
44
+ * heuristic. We discover the extension by name (not by computing the ID
45
+ * from the unpacked path) because unpacked-extension IDs depend on the
46
+ * absolute path and OS path normalization.
47
+ */
48
+ private findDevtoolsServiceWorker;
49
+ private readManifestName;
50
+ private extractExtensionId;
51
+ }
@@ -0,0 +1,165 @@
1
+ import { safe } from "../../../../helpers/safe.js";
2
+ const STORAGE_KEY = "skCli";
3
+ const PROTOCOL_VERSION = 1;
4
+ const SW_LOOKUP_RETRY_MS = 250;
5
+ const INIT_POLL_INTERVAL_MS = 200;
6
+ const INIT_POLL_TIMEOUT_MS = 4000;
7
+ // Same name heuristic as ExtensionValidator.handleDevtoolsExtension.
8
+ const EXTENSION_NAME_MATCHERS = ["speed-kit devtools", "devtools extension"];
9
+ /**
10
+ * Writes onboarding state (messages, status) into the devtools extension's
11
+ * chrome.storage.local via a CDP session against the extension's MV3
12
+ * service-worker target. The devtools extension subscribes via
13
+ * chrome.storage.onChanged. Replaces the legacy `window.skCliMessages` page
14
+ * injection — no more customer-visible global state.
15
+ *
16
+ * Lifecycle: construct once per browser, call `init()` after the browser is
17
+ * launched and the devtools extension has loaded, then call `writeMessages`
18
+ * whenever the message list changes.
19
+ */
20
+ export class ExtensionBridge {
21
+ cli;
22
+ browser = null;
23
+ extensionId = null;
24
+ constructor(cli) {
25
+ this.cli = cli;
26
+ }
27
+ /**
28
+ * Bind the bridge to a freshly-launched browser, discover the
29
+ * devtools-extension's service-worker target, and cache its extension ID.
30
+ * Polls for ~INIT_POLL_TIMEOUT_MS because MV3 service workers are
31
+ * sometimes not registered as targets until Chrome has finished loading
32
+ * the extension. Throws if no matching SW target is found in time.
33
+ */
34
+ async init(browser) {
35
+ this.browser = browser;
36
+ const deadline = Date.now() + INIT_POLL_TIMEOUT_MS;
37
+ let target = await this.findDevtoolsServiceWorker();
38
+ while (!target && Date.now() < deadline) {
39
+ await sleep(INIT_POLL_INTERVAL_MS);
40
+ target = await this.findDevtoolsServiceWorker();
41
+ }
42
+ if (!target) {
43
+ throw new Error("ExtensionBridge: no devtools-extension service-worker target found " +
44
+ `within ${INIT_POLL_TIMEOUT_MS} ms. Confirm the extension is loaded ` +
45
+ "and its name matches one of: " +
46
+ EXTENSION_NAME_MATCHERS.join(", "));
47
+ }
48
+ this.extensionId = this.extractExtensionId(target.url());
49
+ }
50
+ /**
51
+ * Write the current messages snapshot into chrome.storage.local. One retry
52
+ * on transient failures (MV3 service workers can briefly disappear during
53
+ * respawn). Failures are surfaced via `cli.writeError` but do not throw —
54
+ * a missed write is degraded UX, not a fatal onboarding failure.
55
+ */
56
+ async writeMessages(messages) {
57
+ if (!this.browser || !this.extensionId) {
58
+ this.cli.writeWarning("ExtensionBridge.writeMessages called before init()");
59
+ return;
60
+ }
61
+ const payload = {
62
+ protocolVersion: PROTOCOL_VERSION,
63
+ writtenAt: new Date().toISOString(),
64
+ messages,
65
+ };
66
+ const first = await safe(this.evaluateInServiceWorker(payload));
67
+ if (first.success === true)
68
+ return;
69
+ await sleep(SW_LOOKUP_RETRY_MS);
70
+ const retry = await safe(this.evaluateInServiceWorker(payload));
71
+ if (retry.success !== true) {
72
+ this.cli.writeError(`ExtensionBridge.writeMessages failed: ${retry.error}`);
73
+ }
74
+ }
75
+ async evaluateInServiceWorker(payload) {
76
+ const target = await this.getServiceWorkerTarget();
77
+ if (!target) {
78
+ throw new Error("service-worker target not found");
79
+ }
80
+ const session = await target.createCDPSession();
81
+ try {
82
+ const expression = `chrome.storage.local.set(${JSON.stringify({
83
+ [STORAGE_KEY]: payload,
84
+ })})`;
85
+ const evalResult = await session.send("Runtime.evaluate", {
86
+ expression,
87
+ awaitPromise: true,
88
+ returnByValue: true,
89
+ });
90
+ if (evalResult.exceptionDetails) {
91
+ throw new Error(evalResult.exceptionDetails.text ||
92
+ "Runtime.evaluate threw an unknown exception");
93
+ }
94
+ }
95
+ finally {
96
+ await safe(session.detach());
97
+ }
98
+ }
99
+ /**
100
+ * Look up the service-worker target by cached extension ID. Returns null
101
+ * when the SW is dormant — callers handle that via retry.
102
+ */
103
+ async getServiceWorkerTarget() {
104
+ if (!this.browser || !this.extensionId)
105
+ return null;
106
+ const prefix = `chrome-extension://${this.extensionId}/`;
107
+ for (const target of this.browser.targets()) {
108
+ if (target.type() === "service_worker" &&
109
+ target.url().startsWith(prefix)) {
110
+ return target;
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ /**
116
+ * Scan service-worker targets, evaluate `chrome.runtime.getManifest().name`
117
+ * on each, and return the first whose name matches the devtools-extension
118
+ * heuristic. We discover the extension by name (not by computing the ID
119
+ * from the unpacked path) because unpacked-extension IDs depend on the
120
+ * absolute path and OS path normalization.
121
+ */
122
+ async findDevtoolsServiceWorker() {
123
+ if (!this.browser)
124
+ return null;
125
+ const swTargets = this.browser
126
+ .targets()
127
+ .filter((t) => t.type() === "service_worker");
128
+ for (const target of swTargets) {
129
+ const name = await safe(this.readManifestName(target));
130
+ if (!name.success || !name.data)
131
+ continue;
132
+ const lower = name.data.toLowerCase();
133
+ if (EXTENSION_NAME_MATCHERS.some((m) => lower.includes(m))) {
134
+ return target;
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ async readManifestName(target) {
140
+ const session = await target.createCDPSession();
141
+ try {
142
+ const evalResult = await session.send("Runtime.evaluate", {
143
+ expression: "chrome.runtime.getManifest().name",
144
+ returnByValue: true,
145
+ });
146
+ if (evalResult.exceptionDetails)
147
+ return null;
148
+ const value = evalResult.result.value;
149
+ return typeof value === "string" ? value : null;
150
+ }
151
+ finally {
152
+ await safe(session.detach());
153
+ }
154
+ }
155
+ extractExtensionId(url) {
156
+ const match = url.match(/^chrome-extension:\/\/([a-p]{32})\//);
157
+ if (!match) {
158
+ throw new Error(`Could not extract extension ID from URL: ${url}`);
159
+ }
160
+ return match[1];
161
+ }
162
+ }
163
+ function sleep(ms) {
164
+ return new Promise((resolve) => setTimeout(resolve, ms));
165
+ }
@@ -2,6 +2,7 @@ import { CDPSession } from "puppeteer";
2
2
  import { Protocol } from "puppeteer-core";
3
3
  import { UserCliConfig } from "../../helpers/cli-config.js";
4
4
  import { AbortResponse } from "./browser/abort-response.js";
5
+ import { task } from "./todo-collector.js";
5
6
  import { VanillaPuppeteer } from "puppeteer-extra";
6
7
  import { Command } from "@oclif/core/command";
7
8
  export declare const BROWSER_EVENTS: {
@@ -38,9 +39,10 @@ export declare class BrowserContext {
38
39
  userAgent?: string;
39
40
  readonly debuggingPort: boolean;
40
41
  readonly headless: boolean;
42
+ readonly debugPort: number;
41
43
  readonly browserArgs: string[];
42
44
  readonly chromePath: string;
43
- constructor(domain: string, browserVersionString?: string, chromeFlags?: string[], userConfig?: UserCliConfig, userAgent?: string, debuggingPort?: boolean, headless?: boolean);
45
+ constructor(domain: string, browserVersionString?: string, chromeFlags?: string[], userConfig?: UserCliConfig, userAgent?: string, debuggingPort?: boolean, headless?: boolean, debugPort?: number);
44
46
  private isChromeBrowser;
45
47
  private getBrowserExtensionPath;
46
48
  getPuppeteerLaunchOptions(): Parameters<VanillaPuppeteer["launch"]>[0];
@@ -60,12 +62,13 @@ export declare class OnboardingContext {
60
62
  readonly useTestConfig: boolean;
61
63
  readonly debuggingPort: boolean;
62
64
  readonly headless: boolean;
65
+ readonly debugPort: number;
63
66
  static flags: Record<string, Command.Flag>;
64
67
  readonly DEFAULT_DF_PATH = "https://www.baqend.com/speed-kit/latest/dynamic-fetcher.js";
65
68
  readonly DEFAULT_DOCUMENT_HANDLER_PATH = "https://www.baqend.com/speed-kit-handler/latest/DocumentHandler.js";
66
69
  readonly DEFAULT_SNIPPET_PATH = "https://www.baqend.com/speed-kit/latest/snippet.js";
67
70
  readonly DEFAULT_SW_PATH = "https://www.baqend.com/speed-kit/latest/sw.js";
68
- constructor(customerPath: string, configName: string, domain: string, local: boolean, ignoreContentSecurityPolicy: boolean, cliPath: string, verboseLevel?: boolean, useTestConfig?: boolean, debuggingPort?: boolean, headless?: boolean);
71
+ constructor(customerPath: string, configName: string, domain: string, local: boolean, ignoreContentSecurityPolicy: boolean, cliPath: string, verboseLevel?: boolean, useTestConfig?: boolean, debuggingPort?: boolean, headless?: boolean, debugPort?: number);
69
72
  }
70
73
  export interface SpeedKitServerConfigAuthentication {
71
74
  password: string;
@@ -105,10 +108,45 @@ export interface SpeedKitServerConfig {
105
108
  variations: SpeedKitServerConfigVariation;
106
109
  };
107
110
  }
108
- export interface DevelopmentToolsMessage {
109
- id: string;
110
- data: any;
111
+ export interface SkCliInfo {
112
+ version: string;
113
+ app: string;
114
+ customerConfigPath: string;
111
115
  }
116
+ export interface AthenaInfo {
117
+ client: boolean;
118
+ }
119
+ export interface AppTokenInfo {
120
+ app: string;
121
+ token: string;
122
+ }
123
+ export interface LocalCacheError {
124
+ url: string;
125
+ assetUrl: string;
126
+ variation: string;
127
+ error: string;
128
+ stack: string | null;
129
+ occurredAt: number;
130
+ }
131
+ export type DevelopmentToolsMessage = {
132
+ id: "skCli";
133
+ data: SkCliInfo;
134
+ } | {
135
+ id: "athena";
136
+ data: AthenaInfo;
137
+ } | {
138
+ id: "todo";
139
+ data: task[];
140
+ } | {
141
+ id: "appToken";
142
+ data: AppTokenInfo;
143
+ } | {
144
+ id: "serviceWorker";
145
+ data: string;
146
+ } | {
147
+ id: "localCacheError";
148
+ data: LocalCacheError[];
149
+ };
112
150
  export declare const USER_AGENT: {
113
151
  DESKTOP: string;
114
152
  MOBILE: string;
@@ -46,9 +46,10 @@ export class BrowserContext {
46
46
  userAgent;
47
47
  debuggingPort;
48
48
  headless;
49
+ debugPort;
49
50
  browserArgs;
50
51
  chromePath;
51
- constructor(domain, browserVersionString = "unknown", chromeFlags, userConfig, userAgent, debuggingPort = false, headless = false) {
52
+ constructor(domain, browserVersionString = "unknown", chromeFlags, userConfig, userAgent, debuggingPort = false, headless = false, debugPort = 9222) {
52
53
  this.domain = domain;
53
54
  this.browserVersionString = browserVersionString;
54
55
  this.chromeFlags = chromeFlags;
@@ -56,6 +57,7 @@ export class BrowserContext {
56
57
  this.userAgent = userAgent;
57
58
  this.debuggingPort = debuggingPort;
58
59
  this.headless = headless;
60
+ this.debugPort = debugPort;
59
61
  this.domain = domain;
60
62
  this.chromeFlags = chromeFlags || [];
61
63
  this.chromePath = resolve(userConfig.chromePath);
@@ -106,7 +108,7 @@ export class BrowserContext {
106
108
  }
107
109
  }
108
110
  if (this.debuggingPort || this.headless) {
109
- options.args.push("--remote-debugging-port=9222");
111
+ options.args.push(`--remote-debugging-port=${this.debugPort}`);
110
112
  }
111
113
  return options;
112
114
  }
@@ -122,6 +124,7 @@ export class OnboardingContext {
122
124
  useTestConfig;
123
125
  debuggingPort;
124
126
  headless;
127
+ debugPort;
125
128
  static flags = {
126
129
  ...CLI_CONFIG_NAME,
127
130
  ...CLI_CONFIG_IS_TEST,
@@ -139,6 +142,10 @@ export class OnboardingContext {
139
142
  default: false,
140
143
  description: "Enable remote debugging port (disables speculation rules/prerendering)",
141
144
  }),
145
+ debugPort: Flags.integer({
146
+ default: 9222,
147
+ description: "Chrome remote debugging port (only used with -p/--headless). Use distinct ports to run multiple onboarding processes at once.",
148
+ }),
142
149
  verboseLevel: Flags.boolean({
143
150
  char: "v",
144
151
  default: false,
@@ -158,7 +165,7 @@ export class OnboardingContext {
158
165
  DEFAULT_DOCUMENT_HANDLER_PATH = "https://www.baqend.com/speed-kit-handler/latest/DocumentHandler.js";
159
166
  DEFAULT_SNIPPET_PATH = "https://www.baqend.com/speed-kit/latest/snippet.js";
160
167
  DEFAULT_SW_PATH = "https://www.baqend.com/speed-kit/latest/sw.js";
161
- constructor(customerPath, configName, domain, local, ignoreContentSecurityPolicy = false, cliPath, verboseLevel, useTestConfig = false, debuggingPort = false, headless = false) {
168
+ constructor(customerPath, configName, domain, local, ignoreContentSecurityPolicy = false, cliPath, verboseLevel, useTestConfig = false, debuggingPort = false, headless = false, debugPort = 9222) {
162
169
  this.customerPath = customerPath;
163
170
  this.configName = configName;
164
171
  this.domain = domain;
@@ -169,6 +176,7 @@ export class OnboardingContext {
169
176
  this.useTestConfig = useTestConfig;
170
177
  this.debuggingPort = debuggingPort;
171
178
  this.headless = headless;
179
+ this.debugPort = debugPort;
172
180
  }
173
181
  }
174
182
  export const USER_AGENT = {
@@ -0,0 +1,28 @@
1
+ import { expect } from "chai";
2
+ import { describe, it } from "mocha";
3
+ import { BrowserContext } from "./onboarding-model.js";
4
+ // Providing chromeUserProfilePath keeps BrowserContext off the filesystem
5
+ // (it skips the temp-profile branch in the constructor).
6
+ const userConfig = {
7
+ chromePath: "/usr/bin/google-chrome",
8
+ chromeUserProfilePath: "/tmp/sk-test-profile",
9
+ };
10
+ function remoteDebuggingArg(debuggingPort, headless, debugPort) {
11
+ const context = new BrowserContext("example.com", "Chrome/120", [], userConfig, undefined, debuggingPort, headless, debugPort);
12
+ const args = context.getPuppeteerLaunchOptions().args;
13
+ return args.find((arg) => arg.startsWith("--remote-debugging-port="));
14
+ }
15
+ describe("BrowserContext.getPuppeteerLaunchOptions remote debugging port", () => {
16
+ it("defaults to port 9222 when debugging is enabled without a custom port", () => {
17
+ expect(remoteDebuggingArg(true, false)).to.equal("--remote-debugging-port=9222");
18
+ });
19
+ it("uses the provided debug port when debugging is enabled", () => {
20
+ expect(remoteDebuggingArg(true, false, 9223)).to.equal("--remote-debugging-port=9223");
21
+ });
22
+ it("uses the provided debug port in headless mode", () => {
23
+ expect(remoteDebuggingArg(false, true, 9224)).to.equal("--remote-debugging-port=9224");
24
+ });
25
+ it("omits the remote debugging port when debugging and headless are both off", () => {
26
+ expect(remoteDebuggingArg(false, false, 9223)).to.equal(undefined);
27
+ });
28
+ });
@@ -27,6 +27,7 @@ import { InstallSpeedKitHtmlTemplate } from "./templates/install-speed-kit-html-
27
27
  import { InstallSpeedKitJsTemplate } from "./templates/install-speed-kit-js-template.js";
28
28
  import { ExtensionValidator } from "./browser/extension/extension-validator.js";
29
29
  import { DevtoolsExtensionApi } from "./browser/extension/devtools-extension-api.js";
30
+ import { ExtensionBridge } from "./browser/extension/extension-bridge.js";
30
31
  import { TodoCollector } from "./todo-collector.js";
31
32
  import { Crawler } from "./virtual-orestes-app/crawler.js";
32
33
  import { VirtualOrestesApp } from "./virtual-orestes-app/index.js";
@@ -72,21 +73,30 @@ export class OnboardingServiceFactory {
72
73
  this.cliConfig.chromeExtensionPaths = extensionPaths.join(",");
73
74
  const browserValidator = new ExecutableValidator(this.cliConfig, cli);
74
75
  const browserVersionString = await browserValidator.validateOrInstall();
75
- const browserContext = new BrowserContext(domainToStart, browserVersionString, customerConfig.chromeFlags || [], this.cliConfig, undefined, this.context.debuggingPort, this.context.headless);
76
+ const browserContext = new BrowserContext(domainToStart, browserVersionString, customerConfig.chromeFlags || [], this.cliConfig, undefined, this.context.debuggingPort, this.context.headless, this.context.debugPort);
76
77
  if (this.context.debuggingPort || this.context.headless) {
77
- cli.writeWarning("Chrome remote debugging enabled on port 9222.");
78
+ cli.writeWarning(`Chrome remote debugging enabled on port ${this.context.debugPort}.`);
78
79
  }
79
80
  if (this.context.headless) {
80
81
  cli.writeWarning("Running in headless mode.");
81
82
  }
82
83
  const fileWatcher = new FileWatcher(cli, customerConfig, files, documentHandler, browserContext, cache);
83
84
  const athenaClient = (await this.getAthenaClient()) || null;
84
- const fetchHandler = await this.getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient);
85
85
  const messageHandler = await this.prepareMessagehandler(athenaClient, files, customerConfig);
86
- return new OnboardingService(browserContext, customerConfig, fetchHandler, fileWatcher, cli, messageHandler);
86
+ const fetchHandler = await this.getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient, messageHandler);
87
+ const extensionBridge = new ExtensionBridge(cli);
88
+ return new OnboardingService(browserContext, customerConfig, fetchHandler, fileWatcher, cli, messageHandler, extensionBridge);
87
89
  }
88
90
  async prepareMessagehandler(athenaClient, files, customerConfig) {
89
91
  const messageHandler = new DevtoolsExtensionApi();
92
+ messageHandler.addMessage({
93
+ id: "skCli",
94
+ data: {
95
+ version: this.cliConfig.runtimeConfig.version,
96
+ app: customerConfig.app,
97
+ customerConfigPath: path.resolve(this.context.customerPath, this.context.configName),
98
+ },
99
+ });
90
100
  messageHandler.addMessage({
91
101
  id: "athena",
92
102
  data: { client: !!athenaClient },
@@ -135,7 +145,7 @@ export class OnboardingServiceFactory {
135
145
  const configApiContext = new ConfigApiContext(app);
136
146
  return new ConfigApiServiceFactory(configApiContext).getService();
137
147
  }
138
- async getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient) {
148
+ async getFetchEventHandler(files, customerConfig, documentHandler, cache, cli, browserContext, athenaClient, messageHandler) {
139
149
  const configApi = this.getConfigApi(customerConfig.app);
140
150
  const serverConfig = await this.getSpeedKitServerConfig(configApi, cli);
141
151
  const agent = this.getCrawlerAgent(customerConfig, cli);
@@ -156,7 +166,7 @@ export class OnboardingServiceFactory {
156
166
  new DashboardRequest(new Dashboard(customerConfig, parameterQueryBuilder, athenaClient, requestDiffService, new DiffAgainstCurrentPage(DiffService, documentHandler, crawler, cli), cache)),
157
167
  ];
158
168
  if (this.context.local) {
159
- const orestesApp = new VirtualOrestesApp(customerConfig, crawler, documentHandler, cache, cli);
169
+ const orestesApp = new VirtualOrestesApp(customerConfig, crawler, documentHandler, cache, cli, messageHandler);
160
170
  handlers.push(new SpeedKitAssetRequest(customerConfig, orestesApp), new SpeedKitRumPiRequest(customerConfig, cache, cli));
161
171
  }
162
172
  return new FetchEventHandler(handlers, customerConfig, cli, configApi);
@@ -4,6 +4,7 @@ import { FetchEventHandler } from "./fetch-event-handler.js";
4
4
  import { FileWatcher } from "./file-events/file-watcher.js";
5
5
  import { CliService } from "../cli/index.js";
6
6
  import { DevtoolsExtensionApi } from "./browser/extension/devtools-extension-api.js";
7
+ import { ExtensionBridge } from "./browser/extension/extension-bridge.js";
7
8
  /**
8
9
  * Represents the OnboardingService which is responsible for setting up the browser to test a config using Puppeteer.
9
10
  */
@@ -14,8 +15,9 @@ export declare class OnboardingService {
14
15
  private fileWatcher;
15
16
  private cli;
16
17
  private developmentToolsMessages;
18
+ private extensionBridge;
17
19
  private reinstallOnNextNavigate;
18
- constructor(browserContext: BrowserContext, customerConfig: CustomerConfig, fetchEventHandler: FetchEventHandler, fileWatcher: FileWatcher, cli: CliService, developmentToolsMessages: DevtoolsExtensionApi);
20
+ constructor(browserContext: BrowserContext, customerConfig: CustomerConfig, fetchEventHandler: FetchEventHandler, fileWatcher: FileWatcher, cli: CliService, developmentToolsMessages: DevtoolsExtensionApi, extensionBridge: ExtensionBridge);
19
21
  /**
20
22
  * Prepare the puppeteer browser, wire event listeners, navigate to the
21
23
  * install page, then block until either the caller aborts the signal
@@ -25,6 +27,14 @@ export declare class OnboardingService {
25
27
  run(externalSignal: AbortSignal): Promise<void>;
26
28
  private prepareBrowser;
27
29
  private preparePage;
30
+ /**
31
+ * Bind the injected ExtensionBridge to the freshly-launched browser so
32
+ * message updates are mirrored into the devtools extension's
33
+ * `chrome.storage.local`. A failure here is non-fatal: onboarding still
34
+ * proceeds, but the devtools panel will not receive live updates from
35
+ * sk-cli.
36
+ */
37
+ private attachExtensionBridge;
28
38
  private findPage;
29
39
  private prepareCleanState;
30
40
  private reinstallSpeedKit;
@@ -11,14 +11,16 @@ export class OnboardingService {
11
11
  fileWatcher;
12
12
  cli;
13
13
  developmentToolsMessages;
14
+ extensionBridge;
14
15
  reinstallOnNextNavigate = false;
15
- constructor(browserContext, customerConfig, fetchEventHandler, fileWatcher, cli, developmentToolsMessages) {
16
+ constructor(browserContext, customerConfig, fetchEventHandler, fileWatcher, cli, developmentToolsMessages, extensionBridge) {
16
17
  this.browserContext = browserContext;
17
18
  this.customerConfig = customerConfig;
18
19
  this.fetchEventHandler = fetchEventHandler;
19
20
  this.fileWatcher = fileWatcher;
20
21
  this.cli = cli;
21
22
  this.developmentToolsMessages = developmentToolsMessages;
23
+ this.extensionBridge = extensionBridge;
22
24
  }
23
25
  /**
24
26
  * Prepare the puppeteer browser, wire event listeners, navigate to the
@@ -34,6 +36,7 @@ export class OnboardingService {
34
36
  localController.signal,
35
37
  ]);
36
38
  const browser = await this.prepareBrowser(localController);
39
+ await this.attachExtensionBridge(browser);
37
40
  const page = await this.preparePage(browser, shutdownSignal);
38
41
  this.cli.successAction("ONBOARDING:PREPARE:BROWSER");
39
42
  await this.navigate(page, `${this.customerConfig.scope}speed-kit-install.html?url=${this.customerConfig.scope}`);
@@ -58,17 +61,23 @@ export class OnboardingService {
58
61
  await safe(this.prepareCleanState(page));
59
62
  this.registerFileWatcher(page, shutdownSignal);
60
63
  this.registerReinstallSpeedKitOnNavigate(page);
61
- this.developmentToolsMessages.addMessage({
62
- id: "cli",
63
- data: `config loaded for app "${this.customerConfig.app}"`,
64
- });
65
- const messages = this.developmentToolsMessages.messages;
66
- await page.evaluateOnNewDocument((messages) => {
67
- // @ts-expect-error unknown method getSkCliMessages
68
- window.skCliMessages = messages;
69
- }, messages);
70
64
  return page;
71
65
  }
66
+ /**
67
+ * Bind the injected ExtensionBridge to the freshly-launched browser so
68
+ * message updates are mirrored into the devtools extension's
69
+ * `chrome.storage.local`. A failure here is non-fatal: onboarding still
70
+ * proceeds, but the devtools panel will not receive live updates from
71
+ * sk-cli.
72
+ */
73
+ async attachExtensionBridge(browser) {
74
+ const result = await safe(this.extensionBridge.init(browser));
75
+ if (result.success !== true) {
76
+ this.cli.writeWarning(`ExtensionBridge unavailable, devtools panel will not receive live updates: ${result.error}`);
77
+ return;
78
+ }
79
+ this.developmentToolsMessages.setBridge(this.extensionBridge);
80
+ }
72
81
  async findPage(browser) {
73
82
  if (!browser)
74
83
  throw new Error("Please start a browser first.");
@@ -5,15 +5,22 @@ import { Cache } from "../request-cache/cache.js";
5
5
  import { CliService } from "../../cli/index.js";
6
6
  import { Crawler } from "./crawler.js";
7
7
  import { CustomerConfig } from "../../integration-api/index.js";
8
+ import { DevtoolsExtensionApi } from "../browser/extension/devtools-extension-api.js";
8
9
  export declare class VirtualOrestesApp {
9
10
  private customerConfig;
10
11
  private crawler;
11
12
  private documentHandler;
12
13
  private cache;
13
14
  private cli;
14
- constructor(customerConfig: CustomerConfig, crawler: Crawler, documentHandler: DocumentHandlerServer, cache: Cache, cli: CliService);
15
+ private developmentToolsMessages;
16
+ private localCacheErrors;
17
+ constructor(customerConfig: CustomerConfig, crawler: Crawler, documentHandler: DocumentHandlerServer, cache: Cache, cli: CliService, developmentToolsMessages: DevtoolsExtensionApi);
15
18
  fetchAssetResponse(event: Protocol.Fetch.RequestPausedEvent): Promise<Partial<Protocol.Fetch.FulfillRequestRequest> | AbortResponse>;
16
19
  private getResponseText;
20
+ private localCacheErrorKey;
21
+ private recordLocalCacheError;
22
+ private clearLocalCacheError;
23
+ private emitLocalCacheErrors;
17
24
  private returnErrorResponse;
18
25
  private convertResponseHeadersToOrestesFormat;
19
26
  private returnRedirect;
@@ -13,12 +13,19 @@ export class VirtualOrestesApp {
13
13
  documentHandler;
14
14
  cache;
15
15
  cli;
16
- constructor(customerConfig, crawler, documentHandler, cache, cli) {
16
+ developmentToolsMessages;
17
+ // Local-mode transform errors keyed by `${url}|${variation}`. Emitted to
18
+ // the devtools panel via DevtoolsExtensionApi.upsertMessage so the panel
19
+ // can surface an "error for this URL" affordance in the Causes & sources
20
+ // card. Cleared per-URL on the next successful transform.
21
+ localCacheErrors = new Map();
22
+ constructor(customerConfig, crawler, documentHandler, cache, cli, developmentToolsMessages) {
17
23
  this.customerConfig = customerConfig;
18
24
  this.crawler = crawler;
19
25
  this.documentHandler = documentHandler;
20
26
  this.cache = cache;
21
27
  this.cli = cli;
28
+ this.developmentToolsMessages = developmentToolsMessages;
22
29
  }
23
30
  async fetchAssetResponse(event) {
24
31
  const originUrl = this.getAssetUrl(event.request.url);
@@ -56,8 +63,10 @@ export class VirtualOrestesApp {
56
63
  }
57
64
  const customResponse = await safe(this.documentHandler.transform(`text/html;charset=${textEncoding}`, responseContent, variation, originUrl, this.convertResponseHeadersToOrestesFormat(response)));
58
65
  if (customResponse.success === false) {
66
+ this.recordLocalCacheError(originUrl, event.request.url, variation, customResponse);
59
67
  return this.returnErrorResponse(customResponse, customHeaders);
60
68
  }
69
+ this.clearLocalCacheError(originUrl, variation);
61
70
  const skResponse = customResponse.data;
62
71
  if (!Buffer.isBuffer(skResponse.body) && this.customerConfig.forceInstall) {
63
72
  skResponse.body = this.rewriteInstallResource(skResponse.body);
@@ -108,6 +117,33 @@ export class VirtualOrestesApp {
108
117
  textEncoding: encodingFromBodyMeta ?? encodingFromHeader,
109
118
  };
110
119
  }
120
+ localCacheErrorKey(url, variation) {
121
+ return `${url}|${variation}`;
122
+ }
123
+ recordLocalCacheError(url, assetUrl, variation, customResponse) {
124
+ this.localCacheErrors.set(this.localCacheErrorKey(url, variation), {
125
+ url,
126
+ assetUrl,
127
+ variation,
128
+ error: customResponse.error,
129
+ stack: customResponse.errorObj?.stack ?? null,
130
+ occurredAt: Date.now(),
131
+ });
132
+ this.emitLocalCacheErrors();
133
+ }
134
+ clearLocalCacheError(url, variation) {
135
+ const key = this.localCacheErrorKey(url, variation);
136
+ if (!this.localCacheErrors.has(key))
137
+ return;
138
+ this.localCacheErrors.delete(key);
139
+ this.emitLocalCacheErrors();
140
+ }
141
+ emitLocalCacheErrors() {
142
+ this.developmentToolsMessages.upsertMessage({
143
+ id: "localCacheError",
144
+ data: [...this.localCacheErrors.values()],
145
+ });
146
+ }
111
147
  returnErrorResponse(customResponse, customHeaders) {
112
148
  const message = `Could not transform html: - ${customResponse.error}`;
113
149
  this.cli.writeError(`[VirtualOrestes]: ${message}`);
@@ -500,6 +500,14 @@
500
500
  "allowNo": false,
501
501
  "type": "boolean"
502
502
  },
503
+ "debugPort": {
504
+ "description": "Chrome remote debugging port (only used with -p/--headless). Use distinct ports to run multiple onboarding processes at once.",
505
+ "name": "debugPort",
506
+ "default": 9222,
507
+ "hasDynamicHelp": false,
508
+ "multiple": false,
509
+ "type": "option"
510
+ },
503
511
  "verboseLevel": {
504
512
  "char": "v",
505
513
  "description": "Show all messages",
@@ -751,5 +759,5 @@
751
759
  ]
752
760
  }
753
761
  },
754
- "version": "4.5.0"
762
+ "version": "4.6.0"
755
763
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@speedkit/cli",
3
3
  "description": "Speed Kit CLI",
4
- "version": "4.5.0",
4
+ "version": "4.6.0",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"