@reshotdev/screenshot 0.0.1-beta.2 → 0.0.1-beta.21

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 (81) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +138 -47
  3. package/package.json +27 -16
  4. package/src/commands/auth.js +159 -30
  5. package/src/commands/capture-dom.js +50 -0
  6. package/src/commands/certify.js +62 -0
  7. package/src/commands/compose.js +220 -0
  8. package/src/commands/doctor-release.js +74 -0
  9. package/src/commands/doctor-target.js +108 -0
  10. package/src/commands/drifts.js +16 -69
  11. package/src/commands/import-tests.js +13 -13
  12. package/src/commands/init.js +16 -277
  13. package/src/commands/publish.js +484 -257
  14. package/src/commands/pull.js +302 -35
  15. package/src/commands/refresh.js +166 -0
  16. package/src/commands/run.js +292 -12
  17. package/src/commands/setup-wizard.js +348 -496
  18. package/src/commands/status.js +334 -126
  19. package/src/commands/sync.js +28 -236
  20. package/src/commands/ui.js +1 -1
  21. package/src/commands/variation.js +194 -0
  22. package/src/commands/verify-publish.js +46 -0
  23. package/src/index.js +383 -118
  24. package/src/lib/api-client.js +172 -60
  25. package/src/lib/auto-update/refresh.js +598 -0
  26. package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
  27. package/src/lib/auto-update/spec.js +89 -0
  28. package/src/lib/capture-engine.js +179 -9
  29. package/src/lib/capture-script-runner.js +639 -214
  30. package/src/lib/certification.js +887 -0
  31. package/src/lib/compose-context.js +156 -0
  32. package/src/lib/compose-pack.js +42 -0
  33. package/src/lib/compose-runtime.js +34 -0
  34. package/src/lib/compose-upload.js +142 -0
  35. package/src/lib/config.js +186 -81
  36. package/src/lib/dom-capture.js +64 -0
  37. package/src/lib/ensure-browser.js +147 -0
  38. package/src/lib/output-path-template.js +3 -3
  39. package/src/lib/record-cdp.js +288 -16
  40. package/src/lib/record-clip.js +83 -3
  41. package/src/lib/record-config.js +1 -5
  42. package/src/lib/release-doctor.js +321 -0
  43. package/src/lib/resolve-targets.js +60 -0
  44. package/src/lib/run-manifest.js +148 -0
  45. package/src/lib/standalone-mode.js +1 -1
  46. package/src/lib/storage-providers.js +5 -5
  47. package/src/lib/style-engine.js +5 -5
  48. package/src/lib/target-contract.js +292 -0
  49. package/src/lib/ui-api-helpers.js +118 -0
  50. package/src/lib/ui-api.js +31 -824
  51. package/src/lib/ui-asset-cleanup.js +62 -0
  52. package/src/lib/ui-output-versions.js +165 -0
  53. package/src/lib/ui-recorder-routes.js +341 -0
  54. package/src/lib/ui-scenario-metadata.js +161 -0
  55. package/vendor/compose/dist/auto-update.cjs +5544 -0
  56. package/vendor/compose/dist/auto-update.mjs +5518 -0
  57. package/vendor/compose/dist/capture.cjs +1450 -0
  58. package/vendor/compose/dist/capture.mjs +1416 -0
  59. package/vendor/compose/dist/eligibility.cjs +5331 -0
  60. package/vendor/compose/dist/eligibility.mjs +5313 -0
  61. package/vendor/compose/dist/index.cjs +2046 -0
  62. package/vendor/compose/dist/index.mjs +1997 -0
  63. package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
  64. package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
  65. package/vendor/compose/dist/jsx-runtime.cjs +58 -0
  66. package/vendor/compose/dist/jsx-runtime.mjs +31 -0
  67. package/vendor/compose/dist/render.cjs +558 -0
  68. package/vendor/compose/dist/render.mjs +515 -0
  69. package/vendor/compose/dist/verify-cli.cjs +3806 -0
  70. package/vendor/compose/dist/verify-cli.mjs +3812 -0
  71. package/vendor/compose/dist/verify.cjs +3880 -0
  72. package/vendor/compose/dist/verify.mjs +3858 -0
  73. package/web/manager/dist/assets/index-D0S2otug.js +507 -0
  74. package/web/manager/dist/index.html +1 -1
  75. package/src/commands/ci-run.js +0 -123
  76. package/src/commands/ci-setup.js +0 -288
  77. package/src/commands/ingest.js +0 -458
  78. package/src/commands/setup.js +0 -137
  79. package/src/commands/validate-docs.js +0 -529
  80. package/src/lib/playwright-runner.js +0 -252
  81. package/web/manager/dist/assets/index--ZgioErz.js +0 -507
@@ -1,41 +1,21 @@
1
- // setup-wizard.js - Comprehensive interactive setup wizard
2
- // Consolidates the initial setup flow: auth + init + feature configuration
3
- // Individual commands (auth, init) still exist for power users
1
+ // setup-wizard.js - Interactive setup wizard
2
+ // Streamlined flow: choose lane, create config, optional Studio launch
4
3
 
5
4
  const inquirer = require("inquirer");
6
5
  const chalk = require("chalk");
7
6
  const fs = require("fs-extra");
8
7
  const path = require("path");
9
8
  const config = require("../lib/config");
10
- const {
11
- validateStorageConfig,
12
- getStorageSetupHelp,
13
- } = require("../lib/storage-providers");
9
+ const { normalizeConfigContract } = require("../lib/target-contract");
10
+ const { detectCI } = require("../lib/ci-detect");
14
11
 
15
12
  /**
16
- * Auto-detect documentation directories
13
+ * Whether the current process can prompt the user interactively.
14
+ * False in CI or when stdin is not a TTY (piped / redirected), where an
15
+ * inquirer prompt would throw `ERR_USE_AFTER_CLOSE: readline`.
17
16
  */
18
- function detectDocumentationRoot() {
19
- const commonPaths = [
20
- "docs",
21
- "documentation",
22
- "content",
23
- "doc",
24
- "guides",
25
- ".vitepress/content",
26
- ".docusaurus/docs",
27
- ];
28
-
29
- for (const dir of commonPaths) {
30
- const fullPath = path.join(process.cwd(), dir);
31
- if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
32
- const files = fs.readdirSync(fullPath);
33
- if (files.some((f) => f.endsWith(".md") || f.endsWith(".mdx"))) {
34
- return dir;
35
- }
36
- }
37
- }
38
- return null;
17
+ function isInteractive() {
18
+ return !!process.stdin.isTTY && !detectCI().isCI;
39
19
  }
40
20
 
41
21
  /**
@@ -91,184 +71,110 @@ function detectPlaywright() {
91
71
  return { hasPlaywright: false, configFile: null };
92
72
  }
93
73
 
94
- /**
95
- * Generate documentation block configuration
96
- */
97
- function generateDocumentationConfig(root = "./docs", strategy = "git_pr") {
98
- return {
99
- root,
100
- include: ["**/*.md", "**/*.mdx"],
101
- exclude: ["**/_*.mdx", "**/node_modules/**", "**/.next/**"],
102
- strategy,
103
- assetFormat: "markdown",
104
- mappings: {},
105
- };
106
- }
107
-
108
- /**
109
- * Scaffold a basic documentation structure
110
- */
111
- async function scaffoldDocumentation(template = "basic") {
112
- const docsDir = path.join(process.cwd(), "docs");
113
- fs.ensureDirSync(docsDir);
114
-
115
- const readmeContent = `# Documentation
116
-
117
- This directory contains documentation for your project.
118
-
119
- ## Linking Docs to Visual Journeys
120
-
121
- Add \`reshot_journey\` frontmatter to your markdown files to link them to Playwright test captures:
122
-
123
- \`\`\`markdown
124
- ---
125
- title: "Feature Name"
126
- reshot_journey: "feature/workflow-name"
127
- ---
128
-
129
- # Feature Documentation
130
-
131
- Your content here...
132
- \`\`\`
133
-
134
- The journey key should match your Playwright test structure. For example:
135
- - Test file: \`tests/01-auth-flows.spec.ts\` with "Login Flow" describe block
136
- - Journey key: \`auth-flows/login-flow\`
137
-
138
- ## Commands
139
-
140
- - \`reshot import-tests\` - Import existing Playwright tests
141
- - \`reshot sync\` - Sync visuals and documentation
142
- - \`reshot status\` - View sync status and drifts
143
- - \`reshot validate\` - Validate configuration
144
- - \`reshot studio\` - Open visual management UI
145
-
146
- For more information, visit: https://docs.reshot.dev
147
- `;
148
-
149
- fs.writeFileSync(path.join(docsDir, "README.md"), readmeContent);
150
-
151
- const exampleContent = `---
152
- title: "Getting Started"
153
- reshot_journey: "onboarding/first-steps"
154
- description: "Quick start guide for new users"
155
- ---
156
-
157
- # Getting Started
158
-
159
- Welcome! This is an example documentation file linked to the \`onboarding/first-steps\` journey.
160
-
161
- ## Overview
162
-
163
- When Reshot detects UI changes in your Playwright tests, it will automatically propose updates to this document with new screenshots and descriptions.
74
+ function detectSetupMode(projectConfig, useCloud) {
75
+ const normalizedConfig =
76
+ projectConfig && typeof projectConfig === "object"
77
+ ? normalizeConfigContract(projectConfig)
78
+ : null;
79
+ const targetTier = normalizedConfig?.target?.tier || null;
164
80
 
165
- ## Steps
166
-
167
- 1. **Step 1**: Description of first step
168
-
169
- <!-- Reshot will inject screenshots here when visual drift is detected -->
170
-
171
- 2. **Step 2**: Description of second step
172
-
173
- 3. **Step 3**: Description of third step
174
-
175
- ## Next Steps
81
+ if (targetTier === "certified") {
82
+ return "certified-target";
83
+ }
176
84
 
177
- - Link more documents to journeys
178
- - Run \`reshot sync\` to upload traces
179
- - Check \`reshot status\` for drift notifications
180
- `;
85
+ if (targetTier === "candidate") {
86
+ return "candidate-target";
87
+ }
181
88
 
182
- fs.writeFileSync(path.join(docsDir, "getting-started.md"), exampleContent);
183
- return "docs";
89
+ return useCloud ? "cloud-connected" : "local-only";
184
90
  }
185
91
 
186
- /**
187
- * Check if a documentation framework is already set up
188
- */
189
- function detectDocumentationFramework() {
190
- const frameworks = [
191
- {
192
- name: "Astro/Starlight",
193
- files: ["astro.config.mjs", "astro.config.ts"],
194
- deps: ["@astrojs/starlight"],
195
- },
196
- {
197
- name: "Docusaurus",
198
- files: ["docusaurus.config.js", "docusaurus.config.ts"],
199
- deps: ["@docusaurus/core"],
200
- },
201
- {
202
- name: "VitePress",
203
- files: [".vitepress/config.js", ".vitepress/config.ts"],
204
- deps: ["vitepress"],
205
- },
206
- { name: "Mintlify", files: ["mint.json"], deps: [] },
207
- { name: "GitBook", files: [".gitbook.yaml"], deps: [] },
208
- ];
92
+ function getRecommendedLocalServerCommand(normalizedConfig) {
93
+ const explicitCommand = normalizedConfig?.target?.supportedLocalCommand;
94
+ if (explicitCommand) {
95
+ return explicitCommand;
96
+ }
209
97
 
210
- // Check for config files
211
- for (const fw of frameworks) {
212
- for (const file of fw.files) {
213
- if (fs.existsSync(path.join(process.cwd(), file))) {
214
- return { framework: fw.name, detected: true };
215
- }
216
- }
98
+ if (normalizedConfig?.target?.tier === "certified") {
99
+ return "npm run build && npm run start";
217
100
  }
218
101
 
219
- // Check package.json for deps
220
- try {
221
- const pkg = fs.readJsonSync(path.join(process.cwd(), "package.json"));
222
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
102
+ return null;
103
+ }
223
104
 
224
- for (const fw of frameworks) {
225
- for (const dep of fw.deps) {
226
- if (allDeps[dep]) {
227
- return { framework: fw.name, detected: true };
228
- }
229
- }
230
- }
231
- } catch {
232
- // No package.json
105
+ function getNextRecommendedCommand(mode, useCloud) {
106
+ if (mode === "certified-target" || mode === "candidate-target") {
107
+ return "reshot doctor target";
233
108
  }
234
109
 
235
- return { framework: null, detected: false };
110
+ return useCloud ? "reshot publish" : "reshot run";
236
111
  }
237
112
 
238
- /**
239
- * Count documentation files in a directory
240
- */
241
- function countDocFiles(dir) {
242
- if (!fs.existsSync(dir)) return 0;
243
-
244
- let count = 0;
245
- function walk(d) {
246
- for (const item of fs.readdirSync(d)) {
247
- const full = path.join(d, item);
248
- if (fs.statSync(full).isDirectory()) {
249
- if (!item.startsWith(".") && item !== "node_modules") walk(full);
250
- } else if (item.endsWith(".md") || item.endsWith(".mdx")) {
251
- count++;
252
- }
253
- }
254
- }
255
- walk(dir);
256
- return count;
113
+ function writeSetupReport({
114
+ mode,
115
+ useCloud,
116
+ projectId,
117
+ projectName,
118
+ configCreated,
119
+ playwrightDetected,
120
+ supportedLocalCommand,
121
+ }) {
122
+ const reportPath = path.join(
123
+ process.cwd(),
124
+ ".reshot",
125
+ "reports",
126
+ "self-serve-setup.json",
127
+ );
128
+
129
+ fs.ensureDirSync(path.dirname(reportPath));
130
+ fs.writeJSONSync(
131
+ reportPath,
132
+ {
133
+ generatedAt: new Date().toISOString(),
134
+ mode,
135
+ nextRecommendedCommand: getNextRecommendedCommand(mode, useCloud),
136
+ useCloud,
137
+ projectId: projectId || null,
138
+ projectName: projectName || null,
139
+ configCreated,
140
+ playwrightDetected,
141
+ supportedEnvironment: {
142
+ launchSupported: "production-like localhost",
143
+ launchUnsupported: ["next dev"],
144
+ supportedLocalCommand: supportedLocalCommand || null,
145
+ },
146
+ blockingIssues: [],
147
+ advisories: [
148
+ "Run your target app with a production-like local server for launch-grade captures.",
149
+ ],
150
+ nextMilestones: [
151
+ "reshot setup",
152
+ "reshot run",
153
+ ...(useCloud ? ["reshot publish"] : []),
154
+ ],
155
+ },
156
+ { spaces: 2 },
157
+ );
158
+
159
+ return reportPath;
257
160
  }
258
161
 
259
162
  /**
260
163
  * Main setup wizard
261
164
  */
262
165
  async function setupWizard(options = {}) {
263
- const { offline = false, force = false } = options;
166
+ const {
167
+ offline = false,
168
+ force = false,
169
+ noStudio = false,
170
+ project: linkedProjectId,
171
+ token: linkedToken,
172
+ } = options;
264
173
 
265
- console.log(chalk.cyan.bold("\n🚀 Reshot Setup Wizard\n"));
266
- console.log(chalk.gray("Let's configure Reshot for your project.\n"));
174
+ console.log(chalk.cyan.bold("\n🚀 Reshot Setup\n"));
267
175
 
268
176
  // Detect project context
269
- const gitInfo = detectGitInfo();
270
177
  const playwrightInfo = detectPlaywright();
271
- const existingDocsRoot = detectDocumentationRoot();
272
178
 
273
179
  // Check existing setup
274
180
  let existingSettings = null;
@@ -292,8 +198,55 @@ async function setupWizard(options = {}) {
292
198
  // No existing config
293
199
  }
294
200
 
295
- // If already set up and not forcing, confirm continue
296
- if ((isAlreadyAuthed || existingConfig) && !force) {
201
+ let didLinkFromOptions = false;
202
+ if (linkedProjectId && linkedToken && !offline) {
203
+ const authCommand = require("./auth");
204
+ const authResult = await authCommand({
205
+ projectId: linkedProjectId,
206
+ apiKey: linkedToken,
207
+ });
208
+ existingSettings = config.readSettings();
209
+ isAlreadyAuthed = !!(
210
+ existingSettings?.apiKey && existingSettings?.projectId
211
+ );
212
+ didLinkFromOptions = true;
213
+ console.log(
214
+ chalk.green("\n✔ Connected to"),
215
+ chalk.cyan(existingSettings.projectName || existingSettings.projectId),
216
+ );
217
+ if (authResult?.mode) {
218
+ console.log(chalk.gray(` Mode: ${authResult.mode}`));
219
+ }
220
+ } else if (linkedProjectId && !linkedToken && !offline) {
221
+ console.log(
222
+ chalk.gray(
223
+ `Setup will target project ${linkedProjectId}. Complete browser authentication when prompted.`,
224
+ ),
225
+ );
226
+ }
227
+
228
+ // The interactive wizard below needs a TTY. In CI / piped-stdin contexts
229
+ // (and when no `--project/--token` were supplied to drive it non-
230
+ // interactively), bail out with clear guidance instead of letting inquirer
231
+ // throw an unhandled `ERR_USE_AFTER_CLOSE: readline` (audit run-11 F4).
232
+ if (!isInteractive() && !didLinkFromOptions) {
233
+ console.log(
234
+ chalk.yellow("\n⚠ Non-interactive environment detected (no TTY)."),
235
+ );
236
+ console.log(
237
+ chalk.gray(" Run setup with your project credentials instead:"),
238
+ );
239
+ console.log(
240
+ chalk.cyan(" npx reshot setup --project <projectId> --token <token>"),
241
+ );
242
+ console.log(
243
+ chalk.gray(" or set RESHOT_PROJECT_ID and RESHOT_API_KEY, then re-run.\n"),
244
+ );
245
+ return;
246
+ }
247
+
248
+ // If already set up and not forcing, show status and offer options
249
+ if ((isAlreadyAuthed || existingConfig) && !force && !didLinkFromOptions) {
297
250
  console.log(
298
251
  chalk.yellow("⚠ Reshot is already configured in this project.\n"),
299
252
  );
@@ -307,20 +260,39 @@ async function setupWizard(options = {}) {
307
260
  if (existingConfig) {
308
261
  console.log(
309
262
  chalk.green(" ✔ Config found:"),
310
- chalk.cyan("docsync.config.json"),
263
+ chalk.cyan("reshot.config.json"),
311
264
  );
312
265
  }
313
266
 
314
- const { continueSetup } = await inquirer.prompt([
267
+ const choices = [];
268
+
269
+ if (isAlreadyAuthed) {
270
+ choices.push({
271
+ name: "Re-authenticate (connect to a different project)",
272
+ value: "reauth",
273
+ });
274
+ }
275
+
276
+ choices.push({
277
+ name: "Reconfigure (regenerate reshot.config.json)",
278
+ value: "reconfig",
279
+ });
280
+
281
+ choices.push({
282
+ name: "Exit",
283
+ value: "exit",
284
+ });
285
+
286
+ const { action } = await inquirer.prompt([
315
287
  {
316
- type: "confirm",
317
- name: "continueSetup",
318
- message: "Would you like to reconfigure?",
319
- default: false,
288
+ type: "list",
289
+ name: "action",
290
+ message: "What would you like to do?",
291
+ choices,
320
292
  },
321
293
  ]);
322
294
 
323
- if (!continueSetup) {
295
+ if (action === "exit") {
324
296
  console.log(
325
297
  chalk.gray("\nRun"),
326
298
  chalk.cyan("reshot studio"),
@@ -328,19 +300,50 @@ async function setupWizard(options = {}) {
328
300
  );
329
301
  return;
330
302
  }
303
+
304
+ if (action === "reauth") {
305
+ console.log(chalk.gray("\nOpening browser for authentication...\n"));
306
+ const authCommand = require("./auth");
307
+ const authResult = await authCommand();
308
+
309
+ // Re-read settings after auth
310
+ try {
311
+ existingSettings = config.readSettings();
312
+ isAlreadyAuthed = !!(
313
+ existingSettings?.apiKey && existingSettings?.projectId
314
+ );
315
+ } catch {
316
+ throw new Error("Authentication failed. Please try again.");
317
+ }
318
+
319
+ if (!isAlreadyAuthed) {
320
+ throw new Error("Authentication was not completed.");
321
+ }
322
+
323
+ console.log(
324
+ chalk.green("\n✔ Connected to"),
325
+ chalk.cyan(existingSettings.projectName || existingSettings.projectId),
326
+ );
327
+ if (authResult?.mode) {
328
+ console.log(chalk.gray(` Mode: ${authResult.mode}`));
329
+ }
330
+ console.log(
331
+ chalk.gray("\nRun"),
332
+ chalk.cyan("reshot studio"),
333
+ chalk.gray("to manage your visuals.\n"),
334
+ );
335
+ return;
336
+ }
337
+
338
+ // action === "reconfig" — fall through to config generation below
331
339
  }
332
340
 
333
341
  // ========================================
334
342
  // STEP 1: Platform Connection
335
343
  // ========================================
336
- console.log(chalk.cyan("\n━━━ Step 1: Platform Connection ━━━\n"));
344
+ if (!isAlreadyAuthed && !offline) {
345
+ console.log(chalk.cyan("\n━━━ Step 1: Choose Your Lane ━━━\n"));
337
346
 
338
- let useCloud = false;
339
- let projectId = existingSettings?.projectId;
340
- let apiKey = existingSettings?.apiKey;
341
- let projectName = existingSettings?.projectName;
342
-
343
- if (!offline) {
344
347
  const { connectToCloud } = await inquirer.prompt([
345
348
  {
346
349
  type: "list",
@@ -348,33 +351,29 @@ async function setupWizard(options = {}) {
348
351
  message: "How would you like to use Reshot?",
349
352
  choices: [
350
353
  {
351
- name: `${chalk.cyan("Connect to Reshot Cloud")} - Governance, CDN hosting, team collaboration`,
354
+ name: `${chalk.cyan("Set up hosted pipeline")} - Connect to Reshot Cloud for hosted assets, review workflows, and team collaboration`,
352
355
  value: true,
353
356
  },
354
357
  {
355
- name: `${chalk.gray("Offline mode")} - Local-only visuals (no cloud sync)`,
358
+ name: `${chalk.gray("Start locally")} - Get a first capture working on your machine before you add hosted delivery`,
356
359
  value: false,
357
360
  },
358
361
  ],
359
- default: isAlreadyAuthed ? 0 : 1,
360
362
  },
361
363
  ]);
362
364
 
363
- useCloud = connectToCloud;
364
-
365
- if (useCloud && !isAlreadyAuthed) {
365
+ if (connectToCloud) {
366
366
  console.log(chalk.gray("\nOpening browser for authentication...\n"));
367
367
 
368
368
  const authCommand = require("./auth");
369
- await authCommand();
369
+ const authResult = await authCommand();
370
370
 
371
371
  // Re-read settings after auth
372
372
  try {
373
373
  existingSettings = config.readSettings();
374
- projectId = existingSettings?.projectId;
375
- apiKey = existingSettings?.apiKey;
376
- projectName = existingSettings?.projectName;
377
- isAlreadyAuthed = !!(apiKey && projectId);
374
+ isAlreadyAuthed = !!(
375
+ existingSettings?.apiKey && existingSettings?.projectId
376
+ );
378
377
  } catch {
379
378
  throw new Error("Authentication failed. Please try again.");
380
379
  }
@@ -385,105 +384,46 @@ async function setupWizard(options = {}) {
385
384
 
386
385
  console.log(
387
386
  chalk.green("\n✔ Connected to"),
388
- chalk.cyan(projectName || projectId),
387
+ chalk.cyan(existingSettings.projectName || existingSettings.projectId),
389
388
  );
390
- } else if (useCloud && isAlreadyAuthed) {
389
+ if (authResult?.mode) {
390
+ console.log(chalk.gray(` Mode: ${authResult.mode}`));
391
+ }
392
+ } else {
391
393
  console.log(
392
- chalk.green("✔ Already connected to"),
393
- chalk.cyan(projectName || projectId),
394
+ chalk.gray(
395
+ "Running in local-only mode — hosted publish and pull stay unavailable until you run `reshot auth`.",
396
+ ),
394
397
  );
395
398
  }
396
- } else {
397
- console.log(chalk.gray("Running in offline mode - no cloud sync."));
399
+ } else if (offline) {
400
+ console.log(
401
+ chalk.gray(
402
+ "Running in local-only mode — hosted publish and pull stay unavailable until you run `reshot auth`.",
403
+ ),
404
+ );
398
405
  }
399
406
 
400
- // ========================================
401
- // STEP 2: Feature Selection
402
- // ========================================
403
- console.log(chalk.cyan("\n━━━ Step 2: Features ━━━\n"));
404
-
405
- const { features } = await inquirer.prompt([
406
- {
407
- type: "checkbox",
408
- name: "features",
409
- message: "Which features would you like to enable?",
410
- choices: [
411
- {
412
- name: "Visual capture from Playwright tests",
413
- value: "visuals",
414
- checked: playwrightInfo.hasPlaywright,
415
- },
416
- {
417
- name: "Documentation sync (detect drift, auto-update docs)",
418
- value: "docsync",
419
- checked: !!existingDocsRoot,
420
- },
421
- ],
422
- validate: (answers) => {
423
- if (answers.length === 0) {
424
- return "Please select at least one feature";
425
- }
426
- return true;
427
- },
428
- },
429
- ]);
430
-
431
- const enableVisuals = features.includes("visuals");
432
- const enableDocSync = features.includes("docsync");
407
+ const useCloud = isAlreadyAuthed;
408
+ const projectId = existingSettings?.projectId;
409
+ const normalizedSetupConfig = normalizeConfigContract(
410
+ existingConfig && typeof existingConfig === "object" ? existingConfig : {},
411
+ );
412
+ const supportedLocalCommand =
413
+ getRecommendedLocalServerCommand(normalizedSetupConfig);
433
414
 
434
415
  // ========================================
435
- // STEP 3: Visuals Configuration (if enabled)
416
+ // STEP 2: Project Defaults
436
417
  // ========================================
437
- let traceDir = "./test-results";
438
- let journeyMappings = {};
439
-
440
- if (enableVisuals) {
441
- console.log(chalk.cyan("\n━━━ Step 3: Visual Capture ━━━\n"));
418
+ console.log(chalk.cyan("\n━━━ Step 2: Project Defaults ━━━\n"));
442
419
 
443
- if (playwrightInfo.hasPlaywright) {
444
- console.log(chalk.green("✔ Playwright detected"));
445
- if (playwrightInfo.configFile) {
446
- console.log(chalk.gray(` Config: ${playwrightInfo.configFile}`));
447
- }
420
+ let traceDir = "./test-results";
448
421
 
449
- // Offer to import existing tests
450
- const { importTests } = await inquirer.prompt([
451
- {
452
- type: "confirm",
453
- name: "importTests",
454
- message:
455
- "Import existing Playwright tests to create journey mappings?",
456
- default: true,
457
- },
458
- ]);
459
-
460
- if (importTests) {
461
- console.log(chalk.gray("\nScanning Playwright tests..."));
462
- try {
463
- const importTestsCommand = require("./import-tests");
464
- const result = await importTestsCommand({
465
- interactive: false,
466
- dryRun: false,
467
- });
468
- if (result?.journeyMappings) {
469
- journeyMappings = result.journeyMappings;
470
- console.log(
471
- chalk.green(
472
- `✔ Imported ${Object.keys(journeyMappings).length} journey mapping(s)`,
473
- ),
474
- );
475
- }
476
- } catch (err) {
477
- console.log(chalk.yellow(`⚠ Could not import tests: ${err.message}`));
478
- }
479
- }
480
- } else {
481
- console.log(chalk.yellow("⚠ Playwright not detected"));
482
- console.log(
483
- chalk.gray(" Install with: npm install -D @playwright/test"),
484
- );
422
+ if (playwrightInfo.hasPlaywright) {
423
+ console.log(chalk.green("✔ Playwright detected"));
424
+ if (playwrightInfo.configFile) {
425
+ console.log(chalk.gray(` Config: ${playwrightInfo.configFile}`));
485
426
  }
486
-
487
427
  const { customTraceDir } = await inquirer.prompt([
488
428
  {
489
429
  type: "input",
@@ -494,253 +434,165 @@ async function setupWizard(options = {}) {
494
434
  ]);
495
435
 
496
436
  traceDir = customTraceDir;
437
+ } else {
438
+ console.log(chalk.green("✔ No Playwright setup detected"));
439
+ console.log(
440
+ chalk.gray(
441
+ " That is okay for the local-first workflow. You can define or record scenarios and run them directly.",
442
+ ),
443
+ );
497
444
  }
498
445
 
499
446
  // ========================================
500
- // STEP 4: Documentation Configuration (if enabled)
501
- // ========================================
502
- let docConfig = null;
503
-
504
- if (enableDocSync) {
505
- console.log(chalk.cyan("\n━━━ Step 4: Documentation Sync ━━━\n"));
506
-
507
- // Check for existing documentation framework
508
- const docFramework = detectDocumentationFramework();
509
- let docsRoot = existingDocsRoot;
510
-
511
- if (docFramework.detected) {
512
- console.log(chalk.green(`✔ ${docFramework.framework} detected`));
513
- }
514
-
515
- if (docsRoot) {
516
- const docCount = countDocFiles(path.join(process.cwd(), docsRoot));
517
- console.log(
518
- chalk.green(
519
- `✔ Found documentation at: ${docsRoot}/ (${docCount} files)`,
520
- ),
521
- );
522
-
523
- const { useDetected } = await inquirer.prompt([
524
- {
525
- type: "confirm",
526
- name: "useDetected",
527
- message: `Use ${docsRoot}/ for documentation sync?`,
528
- default: true,
529
- },
530
- ]);
531
-
532
- if (!useDetected) {
533
- docsRoot = null;
534
- }
535
- }
536
-
537
- if (!docsRoot) {
538
- const choices = [
539
- { name: "Create docs/ directory with examples", value: "scaffold" },
540
- { name: "Specify existing directory path", value: "specify" },
541
- ];
542
-
543
- // Add Astro Starlight option if not already a docs framework
544
- if (!docFramework.detected) {
545
- choices.push({
546
- name:
547
- chalk.cyan("Create Astro Starlight project") +
548
- chalk.gray(" (recommended for new docs)"),
549
- value: "starlight",
550
- });
551
- }
552
-
553
- const { docAction } = await inquirer.prompt([
554
- {
555
- type: "list",
556
- name: "docAction",
557
- message: "Documentation setup:",
558
- choices,
559
- },
560
- ]);
561
-
562
- if (docAction === "scaffold") {
563
- console.log(chalk.gray("\nCreating docs/ directory..."));
564
- docsRoot = await scaffoldDocumentation();
565
- console.log(chalk.green("✔ Created docs/ with example files"));
566
- } else if (docAction === "starlight") {
567
- console.log(chalk.cyan("\nTo create an Astro Starlight project:\n"));
568
- console.log(
569
- chalk.white(" npm create astro@latest -- --template starlight"),
570
- );
571
- console.log(
572
- chalk.gray(
573
- "\nRun that command, then re-run reshot setup to link it.\n",
574
- ),
575
- );
576
-
577
- // Still create a basic docs folder as fallback
578
- const { createBasic } = await inquirer.prompt([
579
- {
580
- type: "confirm",
581
- name: "createBasic",
582
- message: "Create a basic docs/ folder for now?",
583
- default: true,
584
- },
585
- ]);
586
-
587
- if (createBasic) {
588
- docsRoot = await scaffoldDocumentation();
589
- console.log(chalk.green("✔ Created docs/ with example files"));
590
- }
591
- } else {
592
- const { customPath } = await inquirer.prompt([
593
- {
594
- type: "input",
595
- name: "customPath",
596
- message: "Documentation directory path:",
597
- default: "./docs",
598
- validate: (input) => {
599
- const fullPath = path.join(process.cwd(), input);
600
- if (!fs.existsSync(fullPath)) {
601
- return `Directory ${input} does not exist. Create it first or use scaffolding.`;
602
- }
603
- return true;
604
- },
605
- },
606
- ]);
607
- docsRoot = customPath;
608
- }
609
- }
610
-
611
- // Determine strategy
612
- let strategy = "git_pr";
613
-
614
- if (useCloud && docsRoot) {
615
- if (gitInfo.isGitHub) {
616
- console.log(chalk.green("\n✔ GitHub repository detected"));
617
- const { useGitPR } = await inquirer.prompt([
618
- {
619
- type: "confirm",
620
- name: "useGitPR",
621
- message: "Auto-create Pull Requests for documentation updates?",
622
- default: true,
623
- },
624
- ]);
625
- strategy = useGitPR ? "git_pr" : "external_host";
626
- } else {
627
- const { strategyChoice } = await inquirer.prompt([
628
- {
629
- type: "list",
630
- name: "strategyChoice",
631
- message: "How should documentation updates be delivered?",
632
- choices: [
633
- { name: "Git Pull Requests", value: "git_pr" },
634
- {
635
- name: "Notifications only (external CMS)",
636
- value: "external_host",
637
- },
638
- ],
639
- },
640
- ]);
641
- strategy = strategyChoice;
642
- }
643
- }
644
-
645
- docConfig = generateDocumentationConfig(docsRoot, strategy);
646
- }
647
-
648
- // ========================================
649
- // STEP 5: Generate Configuration
447
+ // Generate Configuration
650
448
  // ========================================
651
449
  console.log(chalk.cyan("\n━━━ Generating Configuration ━━━\n"));
652
450
 
653
451
  const newConfig = {
654
- $schema: "https://reshot.dev/schemas/docsync-config.json",
452
+ $schema: "https://reshot.dev/schemas/reshot-config.json",
655
453
  version: "2.0",
656
454
  baseUrl: existingConfig?.baseUrl || "http://localhost:3000",
657
455
  viewport: existingConfig?.viewport || { width: 1280, height: 720 },
658
- _metadata: {
659
- features: {
660
- visuals: enableVisuals,
661
- docsync: enableDocSync,
662
- },
663
- },
664
456
  };
665
457
 
666
458
  if (useCloud && projectId) {
667
459
  newConfig.projectId = projectId;
668
460
  }
669
461
 
670
- if (enableVisuals) {
671
- newConfig.visuals = {
672
- traceDir,
673
- journeyMappings:
674
- Object.keys(journeyMappings).length > 0 ? journeyMappings : undefined,
675
- };
676
-
677
- if (!useCloud) {
678
- const { customAssetDir } = await inquirer.prompt([
679
- {
680
- type: "input",
681
- name: "customAssetDir",
682
- message: "Where should screenshots be saved?",
683
- default: ".reshot/output",
684
- },
685
- ]);
686
- newConfig.assetDir = customAssetDir;
687
- } else {
688
- newConfig.assetDir = ".reshot/output";
689
- }
462
+ newConfig.visuals = {
463
+ traceDir,
464
+ };
690
465
 
691
- newConfig.scenarios = existingConfig?.scenarios || [];
466
+ if (!useCloud) {
467
+ const { customAssetDir } = await inquirer.prompt([
468
+ {
469
+ type: "input",
470
+ name: "customAssetDir",
471
+ message: "Where should screenshots be saved?",
472
+ default: ".reshot/output",
473
+ },
474
+ ]);
475
+ newConfig.assetDir = customAssetDir;
476
+ } else {
477
+ newConfig.assetDir = ".reshot/output";
692
478
  }
693
479
 
694
- if (enableDocSync && docConfig) {
695
- newConfig.documentation = docConfig;
696
- }
480
+ newConfig.scenarios = existingConfig?.scenarios || [];
697
481
 
698
482
  // Write configuration
699
483
  config.writeConfig(newConfig);
700
- console.log(chalk.green("✔ Created docsync.config.json"));
484
+ console.log(chalk.green("✔ Created reshot.config.json"));
485
+
486
+ const combinedConfig =
487
+ existingConfig && typeof existingConfig === "object"
488
+ ? { ...existingConfig, ...newConfig }
489
+ : newConfig;
490
+ const normalizedCombinedConfig = normalizeConfigContract(combinedConfig);
491
+ const setupMode = detectSetupMode(combinedConfig, useCloud);
492
+ const finalSupportedLocalCommand =
493
+ getRecommendedLocalServerCommand(normalizedCombinedConfig) ||
494
+ supportedLocalCommand;
495
+ const reportPath = writeSetupReport({
496
+ mode: setupMode,
497
+ useCloud,
498
+ projectId,
499
+ projectName: existingSettings?.projectName,
500
+ configCreated: true,
501
+ playwrightDetected: playwrightInfo.hasPlaywright,
502
+ supportedLocalCommand: finalSupportedLocalCommand,
503
+ });
701
504
 
702
505
  // ========================================
703
- // STEP 6: Success & Next Steps
506
+ // Success & Next Steps
704
507
  // ========================================
705
508
  console.log(chalk.green("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
706
509
  console.log(chalk.green.bold("✔ Reshot setup complete!"));
707
510
  console.log(chalk.green("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
708
511
 
709
- console.log(chalk.cyan("Next steps:\n"));
512
+ console.log(chalk.cyan("Supported launch path:\n"));
513
+ console.log(
514
+ ` ${chalk.gray("Run your target app with a production-like local server before capture.")}`,
515
+ );
516
+ console.log(
517
+ ` ${chalk.cyan(finalSupportedLocalCommand || "npm run build && npm run start")}`,
518
+ );
519
+ console.log(
520
+ ` ${chalk.gray("Unsupported for launch reliability:")} ${chalk.yellow("next dev")}\n`,
521
+ );
710
522
 
523
+ console.log(chalk.cyan("Setup mode:\n"));
524
+ console.log(` ${chalk.green(setupMode)}\n`);
525
+
526
+ console.log(chalk.cyan("Next steps:\n"));
527
+ console.log(
528
+ ` 1. ${chalk.gray("Review")} ${chalk.cyan("reshot.config.json")} ${chalk.gray("and add your first scenario or recording")}`,
529
+ );
711
530
  console.log(
712
- ` 1. ${chalk.gray("Review")} ${chalk.cyan("docsync.config.json")} ${chalk.gray("and commit to your repo")}`,
531
+ ` 2. ${chalk.gray("Start your app in the supported environment:")}`,
713
532
  );
533
+ console.log(
534
+ ` ${chalk.cyan(finalSupportedLocalCommand || "npm run build && npm run start")}`,
535
+ );
536
+ console.log(` 3. ${chalk.gray("Generate your first local capture:")}`);
537
+ console.log(` ${chalk.cyan("reshot run")}`);
714
538
 
715
- if (enableVisuals) {
539
+ if (useCloud) {
716
540
  console.log(
717
- ` 2. ${chalk.gray("Run Playwright tests to generate traces:")}`,
541
+ `\n 4. ${chalk.gray("Upgrade to hosted assets when you are ready:")}`,
718
542
  );
719
- console.log(` ${chalk.cyan("npx playwright test")}`);
543
+ console.log(` ${chalk.cyan("reshot publish")}`);
544
+ } else {
545
+ console.log(
546
+ `\n 4. ${chalk.gray("Connect hosted delivery later when you are ready:")}`,
547
+ );
548
+ console.log(` ${chalk.cyan("reshot auth")}`);
720
549
  }
721
550
 
722
- if (enableDocSync) {
551
+ if (setupMode === "certified-target" || setupMode === "candidate-target") {
723
552
  console.log(
724
- ` ${enableVisuals ? "3" : "2"}. ${chalk.gray("Add")} ${chalk.cyan("reshot_journey")} ${chalk.gray("frontmatter to your markdown files")}`,
553
+ `\n 5. ${chalk.gray("Use advanced target checks when you need them:")}`,
725
554
  );
555
+ console.log(` ${chalk.cyan("reshot doctor target")}`);
556
+ console.log(` ${chalk.cyan("reshot verify publish")}`);
557
+ if (setupMode === "certified-target") {
558
+ console.log(` ${chalk.cyan("reshot certify")}`);
559
+ }
560
+ }
561
+
562
+ console.log(`\n ${chalk.gray("Open Studio to inspect output locally:")}`);
563
+ console.log(` ${chalk.cyan("reshot studio")}`);
564
+ console.log(
565
+ `\n ${chalk.gray("Supported environments guide:")} ${chalk.cyan("https://reshot.dev/docs/cli/getting-started/supported-environments")}`,
566
+ );
567
+ console.log(
568
+ `\n ${chalk.gray("Setup report written to:")} ${chalk.cyan(path.relative(process.cwd(), reportPath))}\n`,
569
+ );
570
+
571
+ if (noStudio) {
726
572
  console.log(
727
- ` ${chalk.gray('Example: reshot_journey: "auth/login-flow"')}`,
573
+ chalk.gray("Studio launch skipped. Run `reshot studio` when you want the local UI.\n"),
728
574
  );
575
+ return;
729
576
  }
730
577
 
731
- console.log(`\n ${chalk.gray("Sync visuals and docs to Reshot:")}`);
732
- console.log(` ${chalk.cyan("reshot sync")}`);
733
-
734
- console.log(`\n ${chalk.gray("Launch the visual management UI:")}`);
735
- console.log(` ${chalk.cyan("reshot studio")}\n`);
578
+ // Offer to launch studio. In non-interactive environments (CI, piped stdin)
579
+ // an inquirer prompt throws `ERR_USE_AFTER_CLOSE: readline`, so skip the
580
+ // prompt and auto-answer the safe default (do NOT launch Studio).
581
+ if (!isInteractive()) {
582
+ console.log(
583
+ chalk.gray(
584
+ "Non-interactive environment detected — skipping Studio launch. Run `reshot studio` when you want the local UI.\n",
585
+ ),
586
+ );
587
+ return;
588
+ }
736
589
 
737
- // Offer to launch studio
738
590
  const { launchStudio } = await inquirer.prompt([
739
591
  {
740
592
  type: "confirm",
741
593
  name: "launchStudio",
742
594
  message: "Launch Reshot Studio now?",
743
- default: true,
595
+ default: false,
744
596
  },
745
597
  ]);
746
598