@brunosps00/dev-workflow 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +22 -15
  2. package/bin/dev-workflow.js +1 -1
  3. package/lib/constants.js +6 -2
  4. package/lib/doctor.js +3 -1
  5. package/lib/install-deps.js +3 -3
  6. package/lib/setup-wsl-browser.js +10 -50
  7. package/lib/skill-registry.js +41 -0
  8. package/package.json +1 -1
  9. package/scaffold/en/agent-instructions.md +5 -2
  10. package/scaffold/en/commands/dw-analyze-project.md +1 -1
  11. package/scaffold/en/commands/dw-brainstorm.md +4 -1
  12. package/scaffold/en/commands/dw-functional-doc.md +3 -3
  13. package/scaffold/en/commands/dw-harness-audit.md +4 -2
  14. package/scaffold/en/commands/dw-help.md +12 -7
  15. package/scaffold/en/commands/dw-opportunities.md +131 -0
  16. package/scaffold/en/commands/dw-refactor.md +54 -0
  17. package/scaffold/en/commands/dw-review.md +1 -1
  18. package/scaffold/en/commands/dw-skill-health.md +11 -3
  19. package/scaffold/en/references/refactoring-catalog.md +1 -1
  20. package/scaffold/en/templates/constitution-template.md +1 -1
  21. package/scaffold/pt-br/agent-instructions.md +5 -2
  22. package/scaffold/pt-br/commands/dw-analyze-project.md +1 -1
  23. package/scaffold/pt-br/commands/dw-brainstorm.md +4 -1
  24. package/scaffold/pt-br/commands/dw-functional-doc.md +3 -3
  25. package/scaffold/pt-br/commands/dw-harness-audit.md +4 -2
  26. package/scaffold/pt-br/commands/dw-help.md +14 -7
  27. package/scaffold/pt-br/commands/dw-opportunities.md +131 -0
  28. package/scaffold/pt-br/commands/dw-refactor.md +54 -0
  29. package/scaffold/pt-br/commands/dw-review.md +1 -1
  30. package/scaffold/pt-br/commands/dw-skill-health.md +11 -3
  31. package/scaffold/pt-br/references/refactoring-catalog.md +1 -1
  32. package/scaffold/pt-br/templates/constitution-template.md +1 -1
  33. package/scaffold/scripts/cdp-relay/Cargo.toml +1 -1
  34. package/scaffold/scripts/cdp-relay/bin/cdp-relay.exe +0 -0
  35. package/scaffold/scripts/cdp-relay/src/main.rs +120 -23
  36. package/scaffold/scripts/functional-doc/run-playwright-flow.mjs +55 -24
  37. package/scaffold/scripts/lib/capture-screenshots.mjs +56 -17
  38. package/scaffold/scripts/lib/resolve-browser.mjs +314 -82
  39. package/scaffold/skill-registry.json +1 -1
  40. package/scaffold/skills/api-testing-recipes/SKILL.md +12 -0
  41. package/scaffold/skills/docker-compose-recipes/SKILL.md +12 -0
  42. package/scaffold/skills/dw-codebase-intel/SKILL.md +13 -1
  43. package/scaffold/skills/dw-context-budget/SKILL.md +12 -0
  44. package/scaffold/skills/dw-council/SKILL.md +12 -0
  45. package/scaffold/skills/dw-debug-protocol/SKILL.md +12 -0
  46. package/scaffold/skills/dw-execute-phase/SKILL.md +12 -0
  47. package/scaffold/skills/dw-git-discipline/SKILL.md +12 -0
  48. package/scaffold/skills/dw-incident-response/SKILL.md +12 -0
  49. package/scaffold/skills/dw-llm-eval/SKILL.md +12 -0
  50. package/scaffold/skills/dw-memory/SKILL.md +12 -0
  51. package/scaffold/skills/dw-review-rigor/SKILL.md +14 -2
  52. package/scaffold/skills/dw-search-first/SKILL.md +12 -0
  53. package/scaffold/skills/dw-silent-failure/SKILL.md +12 -0
  54. package/scaffold/skills/dw-simplification/SKILL.md +18 -6
  55. package/scaffold/skills/dw-simplification/references/deep-modules.md +1 -1
  56. package/scaffold/skills/dw-source-grounding/SKILL.md +12 -0
  57. package/scaffold/skills/dw-testing-discipline/SKILL.md +12 -0
  58. package/scaffold/skills/dw-testing-discipline/references/playwright-recipes.md +18 -19
  59. package/scaffold/skills/dw-ui-discipline/SKILL.md +12 -0
  60. package/scaffold/skills/dw-verify/SKILL.md +10 -0
  61. package/scaffold/skills/humanizer/SKILL.md +12 -0
  62. package/scaffold/skills/remotion-best-practices/SKILL.md +12 -0
  63. package/scaffold/skills/security-review/SKILL.md +12 -0
  64. package/scaffold/skills/vercel-react-best-practices/SKILL.md +12 -0
  65. package/scaffold/skills/vercel-react-best-practices/references/perf-discipline.md +1 -1
@@ -10,8 +10,10 @@ Voce e o auditor de saude de skills e agentes do dev-workflow.
10
10
  1. Leia `scaffold/skill-registry.json` ao auditar o repo dev-workflow, ou `.dw/skill-registry.json` + `.agents/skills/` em projetos consumidores.
11
11
  2. Agrupe skills por `kind`: `protocol`, `domain-pack`, `recipe-pack`, `asset-pack`.
12
12
  3. Valide que toda skill tem trigger, output esperado, owner, load policy e limite de contexto.
13
- 4. Aponte nomes duplicados, `SKILL.md` ausente, frontmatter ausente, `SKILL.md` grande demais, e references/assets lidos sem trigger especifico.
14
- 5. Recomende manter, compactar entrypoint, mesclar, reclassificar, ou remover da visibilidade core.
13
+ 4. Valide que todo `SKILL.md` bundled tem contrato `## Structured Return` com `Status`, `Scope`, `Evidence`, `Artifacts`, `Decisions`, `Risks` e `Next Step`.
14
+ 5. Confirme que o vocabulario de `Status` esta explicito: `PASS`, `FINDINGS`, `BLOCKED`, `NOT_APPLICABLE`.
15
+ 6. Aponte nomes duplicados, `SKILL.md` ausente, frontmatter ausente, `SKILL.md` grande demais, metadata de output esperado desatualizada, e references/assets lidos sem trigger especifico.
16
+ 7. Recomende manter, compactar entrypoint, mesclar, reclassificar, adicionar retorno estruturado, ou remover da visibilidade core.
15
17
 
16
18
  ## Taxonomia
17
19
 
@@ -23,7 +25,13 @@ Voce e o auditor de saude de skills e agentes do dev-workflow.
23
25
  `SKILL.md` deve ser um roteador/protocolo curto. Regras longas, exemplos, paletas, receitas, services e assets ficam em references/assets/rules/recipes e sao carregados sob demanda.
24
26
 
25
27
  ## Saida
26
- Retorne um relatorio conciso. Nao delete arquivos.
28
+ Retorne um relatorio conciso com uma secao de retorno estruturado. Nao delete arquivos.
29
+
30
+ Inclua:
31
+ - Status geral: `PASS`, `FINDINGS` ou `BLOCKED`.
32
+ - Issues de registry/schema.
33
+ - Cobertura de retorno estruturado, incluindo qualquer skill sem contrato ou vocabulario de status.
34
+ - Top 3 fixes.
27
35
 
28
36
  Marcador final: `## SKILL-HEALTH COMPLETE`
29
37
  </system_instructions>
@@ -1,6 +1,6 @@
1
1
  # Catálogo de Refatoração — Exemplos Antes/Depois
2
2
 
3
- Referência para `/dw-brainstorm --refactor`. Baseado no catálogo de Fowler.
3
+ Referência para `/dw-refactor` e `/dw-brainstorm --mode=refactor-audit`. Baseado no catálogo de Fowler.
4
4
 
5
5
  ## 1. Função Longa → Extract Function
6
6
 
@@ -71,7 +71,7 @@ mode: defaults | custom
71
71
  **P-008 — Queries N+1 são flagueadas em code review** (severity: info)
72
72
  **Regra:** Loops ou operações em lista que disparam chamadas DB/HTTP por item devem batchar (ex: `IN (...)`, `findMany`, DataLoader) ou ser explicitamente justificadas.
73
73
  **Why:** Padrões N+1 escalam linearmente com tamanho dos dados e silenciosamente degradam até que a carga de produção revele.
74
- **Enforcement:** `dw-code-review` e `dw-refactoring-analysis` detectam padrões await-em-loop contra módulos de repository / API client.
74
+ **Enforcement:** `/dw-review` e `/dw-refactor` detectam padrões await-em-loop contra módulos de repository / API client.
75
75
 
76
76
  ---
77
77
 
@@ -2,7 +2,7 @@
2
2
  name = "cdp-relay"
3
3
  version = "0.1.0"
4
4
  edition = "2021"
5
- description = "Tiny TCP forwarder so WSL can reach a Windows browser's CDP debug port in NAT mode. Source for the prebuilt dev-workflow relay; no runtime dependency."
5
+ description = "Tiny forward/reverse TCP relay so WSL can reach a Windows browser's CDP debug port in NAT mode without admin setup."
6
6
 
7
7
  [[bin]]
8
8
  name = "cdp-relay"
@@ -1,15 +1,20 @@
1
1
  //! cdp-relay — a tiny TCP forwarder.
2
2
  //!
3
- //! Listens on 0.0.0.0:<listen> and forwards every connection to 127.0.0.1:<target>.
4
- //! dev-workflow runs this on the Windows side so WSL (NAT networking) can reach a Windows
5
- //! browser's CDP remote-debugging port, which Chromium binds to loopback only. std-only:
6
- //! no crates, no runtime. dev-workflow ships a prebuilt Windows x64 .exe; this source is
7
- //! kept for maintainers who need to rebuild it.
3
+ //! Default mode listens on 0.0.0.0:<listen> and forwards every connection to
4
+ //! 127.0.0.1:<target>. Reverse mode connects outbound to a WSL broker, then forwards
5
+ //! each broker-approved session to 127.0.0.1:<target>. dev-workflow uses reverse mode
6
+ //! on WSL NAT so no Windows admin/firewall rule is required. std-only: no crates, no
7
+ //! runtime. dev-workflow ships a prebuilt Windows x64 .exe; this source is kept for
8
+ //! maintainers who need to rebuild it.
8
9
 
9
10
  use std::env;
10
- use std::io;
11
+ use std::io::{self, Read};
11
12
  use std::net::{Shutdown, TcpListener, TcpStream};
12
13
  use std::thread;
14
+ use std::time::Duration;
15
+
16
+ const DEFAULT_REVERSE_POOL: usize = 8;
17
+ const RETRY_DELAY: Duration = Duration::from_millis(200);
13
18
 
14
19
  fn pump(mut from: TcpStream, mut to: TcpStream) {
15
20
  let _ = io::copy(&mut from, &mut to);
@@ -17,29 +22,36 @@ fn pump(mut from: TcpStream, mut to: TcpStream) {
17
22
  let _ = to.shutdown(Shutdown::Write);
18
23
  }
19
24
 
25
+ fn bridge(left: TcpStream, right: TcpStream) -> io::Result<()> {
26
+ left.set_nodelay(true).ok();
27
+ right.set_nodelay(true).ok();
28
+
29
+ let left_rx = left.try_clone()?;
30
+ let right_rx = right.try_clone()?;
31
+ thread::spawn(move || pump(left, right_rx));
32
+ thread::spawn(move || pump(right, left_rx));
33
+ Ok(())
34
+ }
35
+
20
36
  fn handle(client: TcpStream, target_port: u16) -> io::Result<()> {
21
- client.set_nodelay(true).ok();
22
37
  let upstream = TcpStream::connect(("127.0.0.1", target_port))?;
23
- upstream.set_nodelay(true).ok();
24
-
25
- let client_rx = client.try_clone()?;
26
- let upstream_rx = upstream.try_clone()?;
27
- // client -> upstream
28
- thread::spawn(move || pump(client, upstream));
29
- // upstream -> client
30
- thread::spawn(move || pump(upstream_rx, client_rx));
31
- Ok(())
38
+ bridge(client, upstream)
32
39
  }
33
40
 
34
- fn main() {
35
- let args: Vec<String> = env::args().collect();
36
- if args.len() < 3 {
37
- eprintln!("usage: cdp-relay <listen_port> <target_port>");
38
- std::process::exit(2);
41
+ fn wait_for_go(stream: &mut TcpStream) -> io::Result<()> {
42
+ let mut seen = Vec::with_capacity(3);
43
+ let mut byte = [0u8; 1];
44
+ while seen.len() < 3 {
45
+ stream.read_exact(&mut byte)?;
46
+ seen.push(byte[0]);
47
+ if seen == b"GO\n" {
48
+ return Ok(());
49
+ }
39
50
  }
40
- let listen_port: u16 = args[1].parse().expect("invalid listen port");
41
- let target_port: u16 = args[2].parse().expect("invalid target port");
51
+ Err(io::Error::new(io::ErrorKind::InvalidData, "expected GO"))
52
+ }
42
53
 
54
+ fn run_listen(listen_port: u16, target_port: u16) -> io::Result<()> {
43
55
  let listener = TcpListener::bind(("0.0.0.0", listen_port)).expect("bind failed");
44
56
  for stream in listener.incoming() {
45
57
  if let Ok(client) = stream {
@@ -47,4 +59,89 @@ fn main() {
47
59
  let _ = handle(client, target_port);
48
60
  }
49
61
  }
62
+ Ok(())
63
+ }
64
+
65
+ fn handle_reverse_session(mut broker: TcpStream, target_port: u16) -> io::Result<()> {
66
+ broker.set_nodelay(true).ok();
67
+ wait_for_go(&mut broker)?;
68
+ let upstream = TcpStream::connect(("127.0.0.1", target_port))?;
69
+ bridge(broker, upstream)
70
+ }
71
+
72
+ fn reverse_worker(broker_host: String, broker_port: u16, target_port: u16) {
73
+ loop {
74
+ match TcpStream::connect((broker_host.as_str(), broker_port)) {
75
+ Ok(broker) => {
76
+ if handle_reverse_session(broker, target_port).is_err() {
77
+ thread::sleep(RETRY_DELAY);
78
+ }
79
+ }
80
+ Err(_) => thread::sleep(RETRY_DELAY),
81
+ }
82
+ }
83
+ }
84
+
85
+ fn run_reverse(broker_host: String, broker_port: u16, target_port: u16, pool_size: usize) {
86
+ let pool_size = pool_size.max(1);
87
+ for _ in 0..pool_size {
88
+ let host = broker_host.clone();
89
+ thread::spawn(move || reverse_worker(host, broker_port, target_port));
90
+ }
91
+ loop {
92
+ thread::sleep(Duration::from_secs(3600));
93
+ }
94
+ }
95
+
96
+ fn usage() -> ! {
97
+ eprintln!("usage:");
98
+ eprintln!(" cdp-relay <listen_port> <target_port>");
99
+ eprintln!(" cdp-relay --reverse <broker_host> <broker_port> <target_port> [pool_size]");
100
+ std::process::exit(2);
101
+ }
102
+
103
+ fn parse_port(value: &str, name: &str) -> u16 {
104
+ value.parse().unwrap_or_else(|_| {
105
+ eprintln!("invalid {name}");
106
+ std::process::exit(2);
107
+ })
108
+ }
109
+
110
+ fn parse_pool(value: Option<&String>) -> usize {
111
+ value
112
+ .map(|s| {
113
+ s.parse().unwrap_or_else(|_| {
114
+ eprintln!("invalid pool_size");
115
+ std::process::exit(2);
116
+ })
117
+ })
118
+ .unwrap_or(DEFAULT_REVERSE_POOL)
119
+ }
120
+
121
+ fn main() {
122
+ let args: Vec<String> = env::args().collect();
123
+ if args.get(1).map(String::as_str) == Some("--reverse") {
124
+ if args.len() < 5 {
125
+ usage();
126
+ }
127
+ let broker_host = args[2].clone();
128
+ let broker_port = parse_port(&args[3], "broker_port");
129
+ let target_port = parse_port(&args[4], "target_port");
130
+ run_reverse(
131
+ broker_host,
132
+ broker_port,
133
+ target_port,
134
+ parse_pool(args.get(5)),
135
+ );
136
+ }
137
+
138
+ if args.len() < 3 {
139
+ usage();
140
+ }
141
+ let listen_port = parse_port(&args[1], "listen_port");
142
+ let target_port = parse_port(&args[2], "target_port");
143
+ if let Err(err) = run_listen(listen_port, target_port) {
144
+ eprintln!("cdp-relay: {err}");
145
+ std::process::exit(1);
146
+ }
50
147
  }
@@ -6,10 +6,9 @@
6
6
  // which is the only mechanism that works over connectOverCDP (recordVideo does not).
7
7
  // Driving directly also removes the dependency on the project's playwright.config.*.
8
8
  //
9
- // Browser selection is delegated to ../lib/resolve-browser.mjs (WSL-resilient): a Windows
10
- // browser over CDP when reachable, otherwise the full headless Chromium (desktop-faithful,
11
- // no WSLg). The flow module is plain .mjs exporting `async function flow({ page, context,
12
- // expect, baseURL, step, shot })`.
9
+ // Browser selection is delegated to ../lib/resolve-browser.mjs (WSL-resilient): Playwright
10
+ // Chromium is primary, with a configured Windows/CDP target only as fallback. The flow module
11
+ // is plain .mjs exporting `async function flow({ page, context, expect, baseURL, step, shot })`.
13
12
 
14
13
  import fs from "node:fs";
15
14
  import path from "node:path";
@@ -79,22 +78,51 @@ async function importPlaywright() {
79
78
  }
80
79
  }
81
80
 
82
- async function openBrowser(chromium, descriptor) {
81
+ async function openBrowser(chromium, descriptor, log) {
83
82
  if (descriptor.mode === "cdp") {
84
- const browser = await chromium.connectOverCDP(descriptor.endpoint);
85
- return { browser, cleanup: descriptor.cleanup ?? (async () => {}) };
83
+ let browser;
84
+ try {
85
+ browser = await chromium.connectOverCDP(descriptor.endpoint);
86
+ } catch (error) {
87
+ await (descriptor.cleanup ?? (async () => {}))();
88
+ throw error;
89
+ }
90
+ return { browser, descriptor, cleanup: descriptor.cleanup ?? (async () => {}) };
91
+ }
92
+ try {
93
+ const browser = await chromium.launch(descriptor.launchOptions);
94
+ return {
95
+ browser,
96
+ descriptor,
97
+ cleanup: async () => {
98
+ try {
99
+ await browser.close();
100
+ } catch {
101
+ // ignore
102
+ }
103
+ },
104
+ };
105
+ } catch (primaryError) {
106
+ if (typeof descriptor.openFallback !== "function") {
107
+ throw primaryError;
108
+ }
109
+ log(`browser launch failed (${primaryError.message}); trying CDP fallback from ${descriptor.fallback?.source ?? "config"}.`);
110
+ let fallbackDescriptor;
111
+ try {
112
+ fallbackDescriptor = await descriptor.openFallback();
113
+ } catch (fallbackError) {
114
+ throw new Error(`Playwright Chromium launch failed: ${primaryError.message}; CDP fallback failed: ${fallbackError.message}`);
115
+ }
116
+ let browser;
117
+ try {
118
+ browser = await chromium.connectOverCDP(fallbackDescriptor.endpoint);
119
+ } catch (error) {
120
+ await (fallbackDescriptor.cleanup ?? (async () => {}))();
121
+ throw new Error(`Playwright Chromium launch failed: ${primaryError.message}; CDP fallback failed: ${error.message}`);
122
+ }
123
+ log(`browser fallback: mode=${fallbackDescriptor.mode} source=${fallbackDescriptor.source}`);
124
+ return { browser, descriptor: fallbackDescriptor, cleanup: fallbackDescriptor.cleanup ?? (async () => {}) };
86
125
  }
87
- const browser = await chromium.launch(descriptor.launchOptions);
88
- return {
89
- browser,
90
- cleanup: async () => {
91
- try {
92
- await browser.close();
93
- } catch {
94
- // ignore
95
- }
96
- },
97
- };
98
126
  }
99
127
 
100
128
  async function openContext(browser, descriptor, { resolution, baseURL }) {
@@ -168,15 +196,18 @@ async function main() {
168
196
  };
169
197
 
170
198
  const descriptor = await resolveBrowser({ projectRoot, headless: true });
171
- log(`browser: mode=${descriptor.mode} source=${descriptor.source}`);
199
+ log(
200
+ `browser: mode=${descriptor.mode} source=${descriptor.source}` +
201
+ (descriptor.fallback ? ` fallback=${descriptor.fallback.source}` : ""),
202
+ );
172
203
  if (args.browserName && args.browserName !== "chromium") {
173
204
  log(`note: --browser-name=${args.browserName} ignored; screencast/CDP require Chromium.`);
174
205
  }
175
206
 
176
- const { browser, cleanup: cleanupBrowser } = await openBrowser(chromium, descriptor);
177
- const { context, owns: ownsContext } = await openContext(browser, descriptor, { resolution, baseURL });
207
+ const { browser, descriptor: activeDescriptor, cleanup: cleanupBrowser } = await openBrowser(chromium, descriptor, log);
208
+ const { context, owns: ownsContext } = await openContext(browser, activeDescriptor, { resolution, baseURL });
178
209
  const page = await context.newPage();
179
- if (descriptor.mode === "cdp") {
210
+ if (activeDescriptor.mode === "cdp") {
180
211
  try {
181
212
  await page.setViewportSize({ width: resolution.width, height: resolution.height });
182
213
  } catch {
@@ -263,8 +294,8 @@ async function main() {
263
294
 
264
295
  updateManifest(manifestPath, {
265
296
  status,
266
- browserMode: descriptor.mode,
267
- browserSource: descriptor.source,
297
+ browserMode: activeDescriptor.mode,
298
+ browserSource: activeDescriptor.source,
268
299
  recording,
269
300
  videoResolution: resolution.label,
270
301
  video: videoExists ? videoPath : null,
@@ -62,6 +62,55 @@ function slugify(value) {
62
62
  );
63
63
  }
64
64
 
65
+ async function openBrowser(chromium, descriptor) {
66
+ if (descriptor.mode === "cdp") {
67
+ let browser;
68
+ try {
69
+ browser = await chromium.connectOverCDP(descriptor.endpoint);
70
+ } catch (error) {
71
+ await (descriptor.cleanup ?? (async () => {}))();
72
+ throw error;
73
+ }
74
+ return { browser, descriptor, cleanup: descriptor.cleanup ?? (async () => {}) };
75
+ }
76
+ try {
77
+ const browser = await chromium.launch(descriptor.launchOptions);
78
+ return {
79
+ browser,
80
+ descriptor,
81
+ cleanup: async () => {
82
+ try {
83
+ await browser.close();
84
+ } catch {
85
+ // ignore
86
+ }
87
+ },
88
+ };
89
+ } catch (primaryError) {
90
+ if (typeof descriptor.openFallback !== "function") {
91
+ throw primaryError;
92
+ }
93
+ process.stderr.write(
94
+ `browser launch failed (${primaryError.message}); trying CDP fallback from ${descriptor.fallback?.source ?? "config"}.\n`,
95
+ );
96
+ let fallbackDescriptor;
97
+ try {
98
+ fallbackDescriptor = await descriptor.openFallback();
99
+ } catch (fallbackError) {
100
+ throw new Error(`Playwright Chromium launch failed: ${primaryError.message}; CDP fallback failed: ${fallbackError.message}`);
101
+ }
102
+ let browser;
103
+ try {
104
+ browser = await chromium.connectOverCDP(fallbackDescriptor.endpoint);
105
+ } catch (error) {
106
+ await (fallbackDescriptor.cleanup ?? (async () => {}))();
107
+ throw new Error(`Playwright Chromium launch failed: ${primaryError.message}; CDP fallback failed: ${error.message}`);
108
+ }
109
+ process.stderr.write(`browser fallback: mode=${fallbackDescriptor.mode} source=${fallbackDescriptor.source}\n`);
110
+ return { browser, descriptor: fallbackDescriptor, cleanup: fallbackDescriptor.cleanup ?? (async () => {}) };
111
+ }
112
+ }
113
+
65
114
  async function main() {
66
115
  const args = parseArgs(process.argv.slice(2));
67
116
  if (!args.url) {
@@ -85,27 +134,17 @@ async function main() {
85
134
 
86
135
  fs.mkdirSync(outDir, { recursive: true });
87
136
  const descriptor = await resolveBrowser({ projectRoot: process.cwd(), headless: true });
88
- process.stderr.write(`browser: mode=${descriptor.mode} source=${descriptor.source}\n`);
137
+ process.stderr.write(
138
+ `browser: mode=${descriptor.mode} source=${descriptor.source}` +
139
+ (descriptor.fallback ? ` fallback=${descriptor.fallback.source}` : "") +
140
+ "\n",
141
+ );
89
142
 
90
- let browser;
91
- let cleanup = async () => {};
92
- if (descriptor.mode === "cdp") {
93
- browser = await chromium.connectOverCDP(descriptor.endpoint);
94
- cleanup = descriptor.cleanup ?? (async () => {});
95
- } else {
96
- browser = await chromium.launch(descriptor.launchOptions);
97
- cleanup = async () => {
98
- try {
99
- await browser.close();
100
- } catch {
101
- // ignore
102
- }
103
- };
104
- }
143
+ const { browser, descriptor: activeDescriptor, cleanup } = await openBrowser(chromium, descriptor);
105
144
 
106
145
  const shots = [];
107
146
  try {
108
- const existing = descriptor.mode === "cdp" ? browser.contexts()[0] : null;
147
+ const existing = activeDescriptor.mode === "cdp" ? browser.contexts()[0] : null;
109
148
  const context = existing ?? (await browser.newContext());
110
149
  const ownsContext = !existing;
111
150
  const page = await context.newPage();