@reshotdev/screenshot 0.0.1-beta.8 → 0.0.1-beta.9

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.
@@ -16,6 +16,137 @@ function toCamelCase(str) {
16
16
  .replace(/^[A-Z]/, (chr) => chr.toLowerCase());
17
17
  }
18
18
 
19
+ function normalizeHostedAssetUrl(urlString) {
20
+ if (!urlString || typeof urlString !== "string") {
21
+ return urlString;
22
+ }
23
+
24
+ let normalized = urlString;
25
+ if (!normalized.includes("?") && normalized.includes("&")) {
26
+ const firstAmp = normalized.indexOf("&");
27
+ normalized = `${normalized.slice(0, firstAmp)}?${normalized.slice(firstAmp + 1)}`;
28
+ }
29
+
30
+ const parsed = new URL(normalized);
31
+ const capture = parsed.searchParams.get("capture");
32
+ if (capture && !parsed.searchParams.get("step")) {
33
+ parsed.searchParams.set("step", capture);
34
+ }
35
+ parsed.searchParams.delete("capture");
36
+
37
+ return parsed.toString();
38
+ }
39
+
40
+ function normalizeHostedAssetUrlWithMeta(urlString) {
41
+ const normalized = normalizeHostedAssetUrl(urlString);
42
+ return {
43
+ url: normalized,
44
+ repaired: normalized !== urlString,
45
+ };
46
+ }
47
+
48
+ function normalizeAssetEntry(entry) {
49
+ if (!entry || typeof entry !== "object") {
50
+ return { entry, repairs: 0 };
51
+ }
52
+
53
+ if (Array.isArray(entry.steps)) {
54
+ return {
55
+ entry: {
56
+ ...entry,
57
+ steps: entry.steps.map((step) => {
58
+ const src = normalizeHostedAssetUrlWithMeta(step.src);
59
+ const poster = step.poster
60
+ ? normalizeHostedAssetUrlWithMeta(step.poster)
61
+ : null;
62
+ return {
63
+ ...step,
64
+ src: src.url,
65
+ poster: poster ? poster.url : step.poster,
66
+ };
67
+ }),
68
+ },
69
+ repairs: entry.steps.reduce((count, step) => {
70
+ const src = normalizeHostedAssetUrlWithMeta(step.src);
71
+ const poster = step.poster
72
+ ? normalizeHostedAssetUrlWithMeta(step.poster)
73
+ : null;
74
+ return count + (src.repaired ? 1 : 0) + (poster?.repaired ? 1 : 0);
75
+ }, 0),
76
+ };
77
+ }
78
+
79
+ return {
80
+ entry: {
81
+ ...entry,
82
+ src: entry.src ? normalizeHostedAssetUrl(entry.src) : entry.src,
83
+ poster: entry.poster ? normalizeHostedAssetUrl(entry.poster) : entry.poster,
84
+ },
85
+ repairs:
86
+ (entry.src && normalizeHostedAssetUrlWithMeta(entry.src).repaired ? 1 : 0) +
87
+ (entry.poster && normalizeHostedAssetUrlWithMeta(entry.poster).repaired ? 1 : 0),
88
+ };
89
+ }
90
+
91
+ function normalizeAssetMap(assets) {
92
+ const normalized = {};
93
+ let repairs = 0;
94
+
95
+ for (const [group, visuals] of Object.entries(assets || {})) {
96
+ normalized[group] = {};
97
+
98
+ for (const [visualKey, variants] of Object.entries(visuals || {})) {
99
+ normalized[group][visualKey] = {};
100
+
101
+ for (const [variant, entry] of Object.entries(variants || {})) {
102
+ const normalizedEntry = normalizeAssetEntry(entry);
103
+ normalized[group][visualKey][variant] = normalizedEntry.entry;
104
+ repairs += normalizedEntry.repairs;
105
+ }
106
+ }
107
+ }
108
+
109
+ return { assets: normalized, repairs };
110
+ }
111
+
112
+ function validateHostedAssetUrl(urlString) {
113
+ if (!urlString || typeof urlString !== "string") {
114
+ return;
115
+ }
116
+
117
+ if (!urlString.includes("?") && (urlString.includes("&step=") || urlString.includes("&poster="))) {
118
+ throw new Error(`Malformed asset URL emitted without query delimiter: ${urlString}`);
119
+ }
120
+
121
+ const parsed = new URL(urlString);
122
+ if (parsed.searchParams.has("capture")) {
123
+ throw new Error(`Legacy capture param emitted in normalized output: ${urlString}`);
124
+ }
125
+ }
126
+
127
+ function validateAssetMap(assets) {
128
+ for (const visuals of Object.values(assets || {})) {
129
+ for (const variants of Object.values(visuals || {})) {
130
+ for (const entry of Object.values(variants || {})) {
131
+ if (entry && typeof entry === "object" && Array.isArray(entry.steps)) {
132
+ for (const step of entry.steps) {
133
+ validateHostedAssetUrl(step.src);
134
+ if (step.poster) {
135
+ validateHostedAssetUrl(step.poster);
136
+ }
137
+ }
138
+ continue;
139
+ }
140
+
141
+ validateHostedAssetUrl(entry?.src);
142
+ if (entry?.poster) {
143
+ validateHostedAssetUrl(entry.poster);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+
19
150
  /**
20
151
  * Generate TypeScript code from the assets object
21
152
  * This provides type safety - if a visual is deleted, the build will fail
@@ -39,12 +170,27 @@ function generateTypeScript(assets, includeMetadata = false) {
39
170
 
40
171
  for (const [variant, asset] of Object.entries(variants)) {
41
172
  lines.push(` ${variant}: {`);
42
- lines.push(` src: "${asset.src}",`);
43
173
  lines.push(` type: "${asset.type}",`);
44
- if (asset.width) lines.push(` width: ${asset.width},`);
45
- if (asset.height) lines.push(` height: ${asset.height},`);
46
174
  lines.push(` alt: "${asset.alt.replace(/"/g, '\\"')}",`);
47
- if (asset.poster) lines.push(` poster: "${asset.poster}",`);
175
+ if (Array.isArray(asset.steps)) {
176
+ lines.push(` steps: [`);
177
+ for (const step of asset.steps) {
178
+ lines.push(" {");
179
+ lines.push(` step: "${step.step}",`);
180
+ lines.push(` src: "${step.src}",`);
181
+ lines.push(` type: "${step.type}",`);
182
+ if (step.width) lines.push(` width: ${step.width},`);
183
+ if (step.height) lines.push(` height: ${step.height},`);
184
+ if (step.poster) lines.push(` poster: "${step.poster}",`);
185
+ lines.push(" },");
186
+ }
187
+ lines.push(" ],");
188
+ } else {
189
+ lines.push(` src: "${asset.src}",`);
190
+ if (asset.width) lines.push(` width: ${asset.width},`);
191
+ if (asset.height) lines.push(` height: ${asset.height},`);
192
+ if (asset.poster) lines.push(` poster: "${asset.poster}",`);
193
+ }
48
194
  lines.push(` },`);
49
195
  }
50
196
 
@@ -67,11 +213,19 @@ function generateTypeScript(assets, includeMetadata = false) {
67
213
  for (const [visualKey, variants] of Object.entries(visuals)) {
68
214
  // If there's only a default variant, flatten it
69
215
  if (Object.keys(variants).length === 1 && variants.default) {
70
- lines.push(` ${visualKey}: "${variants.default.src}",`);
216
+ if (Array.isArray(variants.default.steps)) {
217
+ lines.push(` ${visualKey}: ${JSON.stringify(variants.default.steps)},`);
218
+ } else {
219
+ lines.push(` ${visualKey}: "${variants.default.src}",`);
220
+ }
71
221
  } else {
72
222
  lines.push(` ${visualKey}: {`);
73
223
  for (const [variant, asset] of Object.entries(variants)) {
74
- lines.push(` ${variant}: "${asset.src}",`);
224
+ if (Array.isArray(asset.steps)) {
225
+ lines.push(` ${variant}: ${JSON.stringify(asset.steps)},`);
226
+ } else {
227
+ lines.push(` ${variant}: "${asset.src}",`);
228
+ }
75
229
  }
76
230
  lines.push(` },`);
77
231
  }
@@ -102,6 +256,7 @@ async function pullCommand(options = {}) {
102
256
  output = null,
103
257
  full = false,
104
258
  status = "approved",
259
+ noExit = false,
105
260
  } = options;
106
261
 
107
262
  console.log(chalk.blue("⬇ Pulling asset map from Reshot...\n"));
@@ -114,15 +269,28 @@ async function pullCommand(options = {}) {
114
269
  console.error(
115
270
  chalk.red("Error: No reshot.config.json found. Run 'reshot init' first.")
116
271
  );
117
- process.exit(1);
272
+ if (!noExit) process.exit(1);
273
+ return { success: false, error: "No reshot.config.json found" };
118
274
  }
119
275
 
120
- const projectId = projectConfig._metadata?.projectId || projectConfig.projectId;
276
+ let projectId = projectConfig._metadata?.projectId || projectConfig.projectId;
121
277
  if (!projectId) {
122
- console.error(
123
- chalk.red("Error: No projectId found in reshot.config.json")
124
- );
125
- process.exit(1);
278
+ // Fall back to reading projectId from .reshot/settings.json (written by `reshot auth`)
279
+ try {
280
+ const settings = config.readSettings();
281
+ if (settings?.projectId) {
282
+ projectId = settings.projectId;
283
+ }
284
+ } catch (e) {
285
+ // No settings file either
286
+ }
287
+ }
288
+ if (!projectId) {
289
+ console.error(
290
+ chalk.red("Error: No projectId found. Add it to reshot.config.json or run 'reshot auth' first.")
291
+ );
292
+ if (!noExit) process.exit(1);
293
+ return { success: false, error: "No projectId found" };
126
294
  }
127
295
 
128
296
  // Check authentication
@@ -133,13 +301,15 @@ async function pullCommand(options = {}) {
133
301
  console.error(
134
302
  chalk.red("Error: Not authenticated. Run 'reshot auth' first.")
135
303
  );
136
- process.exit(1);
304
+ if (!noExit) process.exit(1);
305
+ return { success: false, error: "Not authenticated" };
137
306
  }
138
307
  if (!settings?.apiKey) {
139
308
  console.error(
140
309
  chalk.red("Error: Not authenticated. Run 'reshot auth' first.")
141
310
  );
142
- process.exit(1);
311
+ if (!noExit) process.exit(1);
312
+ return { success: false, error: "Not authenticated" };
143
313
  }
144
314
 
145
315
  if (settings.projectId && projectId !== settings.projectId) {
@@ -167,11 +337,16 @@ async function pullCommand(options = {}) {
167
337
 
168
338
  if (!response || !response.assets) {
169
339
  console.error(chalk.red("Error: Invalid response from API"));
170
- process.exit(1);
340
+ if (!noExit) process.exit(1);
341
+ return { success: false, error: "Invalid response from API" };
171
342
  }
172
343
 
173
- const { meta, assets } = response;
174
- const assetCount = Object.values(assets).reduce(
344
+ const normalization = normalizeAssetMap(response.assets);
345
+ const normalizedAssets = normalization.assets;
346
+ validateAssetMap(normalizedAssets);
347
+
348
+ const { meta } = response;
349
+ const assetCount = Object.values(normalizedAssets).reduce(
175
350
  (count, group) => count + Object.keys(group).length,
176
351
  0
177
352
  );
@@ -211,7 +386,7 @@ async function pullCommand(options = {}) {
211
386
 
212
387
  switch (format) {
213
388
  case "ts":
214
- content = generateTypeScript(assets, full);
389
+ content = generateTypeScript(normalizedAssets, full);
215
390
  contentType = "TypeScript";
216
391
  break;
217
392
 
@@ -220,9 +395,28 @@ async function pullCommand(options = {}) {
220
395
  const headers = ["Key", "Group", "Name", "Context", "Type", "Unbreakable_URL", "Width", "Height"];
221
396
  const rows = [headers.join(",")];
222
397
 
223
- for (const [group, visuals] of Object.entries(assets)) {
398
+ for (const [group, visuals] of Object.entries(normalizedAssets)) {
224
399
  for (const [visualKey, variants] of Object.entries(visuals)) {
225
400
  for (const [variant, asset] of Object.entries(variants)) {
401
+ if (Array.isArray(asset.steps)) {
402
+ for (const step of asset.steps) {
403
+ const type = step.type.split("/")[0];
404
+ rows.push(
405
+ [
406
+ `${group}/${visualKey}/${step.step}`,
407
+ group,
408
+ visualKey,
409
+ variant,
410
+ type,
411
+ step.src,
412
+ step.width || "",
413
+ step.height || "",
414
+ ].join(",")
415
+ );
416
+ }
417
+ continue;
418
+ }
419
+
226
420
  const type = asset.type.split("/")[0]; // 'image' or 'video'
227
421
  rows.push(
228
422
  [
@@ -246,7 +440,7 @@ async function pullCommand(options = {}) {
246
440
 
247
441
  case "json":
248
442
  default:
249
- content = JSON.stringify({ meta, assets }, null, 2);
443
+ content = JSON.stringify({ meta, assets: normalizedAssets }, null, 2);
250
444
  contentType = "JSON";
251
445
  break;
252
446
  }
@@ -309,6 +503,15 @@ async function pullCommand(options = {}) {
309
503
  console.log(chalk.gray(" This ensures your build always uses the latest approved visuals."));
310
504
  console.log(chalk.gray(" If a visual is deleted, your build will fail (preventing broken images).\n"));
311
505
 
506
+ return {
507
+ success: true,
508
+ assetCount,
509
+ outputPath: path.resolve(outputPath),
510
+ normalizedAssets,
511
+ normalizationRepairs: normalization.repairs,
512
+ meta,
513
+ };
514
+
312
515
  } catch (error) {
313
516
  if (config.isAuthError(error)) {
314
517
  console.error(
@@ -320,7 +523,10 @@ async function pullCommand(options = {}) {
320
523
  console.error(chalk.gray(error.stack));
321
524
  }
322
525
  }
323
- process.exit(1);
526
+ if (!noExit) {
527
+ process.exit(1);
528
+ }
529
+ return { success: false, error: error.message };
324
530
  }
325
531
  }
326
532
 
@@ -6,6 +6,7 @@ const chalk = require("chalk");
6
6
  const fs = require("fs-extra");
7
7
  const path = require("path");
8
8
  const config = require("../lib/config");
9
+ const { normalizeConfigContract } = require("../lib/target-contract");
9
10
 
10
11
  /**
11
12
  * Detect if this is a Git repository and if it's GitHub
@@ -60,6 +61,94 @@ function detectPlaywright() {
60
61
  return { hasPlaywright: false, configFile: null };
61
62
  }
62
63
 
64
+ function detectSetupMode(projectConfig, useCloud) {
65
+ const normalizedConfig =
66
+ projectConfig && typeof projectConfig === "object"
67
+ ? normalizeConfigContract(projectConfig)
68
+ : null;
69
+ const targetTier = normalizedConfig?.target?.tier || null;
70
+
71
+ if (targetTier === "certified") {
72
+ return "certified-target";
73
+ }
74
+
75
+ if (targetTier === "candidate") {
76
+ return "candidate-target";
77
+ }
78
+
79
+ return useCloud ? "cloud-connected" : "local-only";
80
+ }
81
+
82
+ function getRecommendedLocalServerCommand(normalizedConfig) {
83
+ const explicitCommand = normalizedConfig?.target?.supportedLocalCommand;
84
+ if (explicitCommand) {
85
+ return explicitCommand;
86
+ }
87
+
88
+ if (normalizedConfig?.target?.tier === "certified") {
89
+ return "npm run build && npm run start";
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ function getNextRecommendedCommand(mode, useCloud) {
96
+ if (mode === "certified-target" || mode === "candidate-target") {
97
+ return "reshot doctor target";
98
+ }
99
+
100
+ return useCloud ? "reshot publish" : "reshot run";
101
+ }
102
+
103
+ function writeSetupReport({
104
+ mode,
105
+ useCloud,
106
+ projectId,
107
+ projectName,
108
+ configCreated,
109
+ playwrightDetected,
110
+ supportedLocalCommand,
111
+ }) {
112
+ const reportPath = path.join(
113
+ process.cwd(),
114
+ ".reshot",
115
+ "reports",
116
+ "self-serve-setup.json",
117
+ );
118
+
119
+ fs.ensureDirSync(path.dirname(reportPath));
120
+ fs.writeJSONSync(
121
+ reportPath,
122
+ {
123
+ generatedAt: new Date().toISOString(),
124
+ mode,
125
+ nextRecommendedCommand: getNextRecommendedCommand(mode, useCloud),
126
+ useCloud,
127
+ projectId: projectId || null,
128
+ projectName: projectName || null,
129
+ configCreated,
130
+ playwrightDetected,
131
+ supportedEnvironment: {
132
+ launchSupported: "production-like localhost",
133
+ launchUnsupported: ["next dev"],
134
+ supportedLocalCommand: supportedLocalCommand || null,
135
+ },
136
+ blockingIssues: [],
137
+ advisories: [
138
+ "Run your target app with a production-like local server for launch-grade captures.",
139
+ ],
140
+ nextMilestones: [
141
+ "reshot setup",
142
+ "reshot run",
143
+ ...(useCloud ? ["reshot publish"] : []),
144
+ ],
145
+ },
146
+ { spaces: 2 },
147
+ );
148
+
149
+ return reportPath;
150
+ }
151
+
63
152
  /**
64
153
  * Main setup wizard
65
154
  */
@@ -152,7 +241,7 @@ async function setupWizard(options = {}) {
152
241
  if (action === "reauth") {
153
242
  console.log(chalk.gray("\nOpening browser for authentication...\n"));
154
243
  const authCommand = require("./auth");
155
- await authCommand();
244
+ const authResult = await authCommand();
156
245
 
157
246
  // Re-read settings after auth
158
247
  try {
@@ -172,6 +261,9 @@ async function setupWizard(options = {}) {
172
261
  chalk.green("\n✔ Connected to"),
173
262
  chalk.cyan(existingSettings.projectName || existingSettings.projectId),
174
263
  );
264
+ if (authResult?.mode) {
265
+ console.log(chalk.gray(` Mode: ${authResult.mode}`));
266
+ }
175
267
  console.log(
176
268
  chalk.gray("\nRun"),
177
269
  chalk.cyan("reshot studio"),
@@ -187,7 +279,7 @@ async function setupWizard(options = {}) {
187
279
  // STEP 1: Platform Connection
188
280
  // ========================================
189
281
  if (!isAlreadyAuthed && !offline) {
190
- console.log(chalk.cyan("\n━━━ Step 1: Platform Connection ━━━\n"));
282
+ console.log(chalk.cyan("\n━━━ Step 1: Choose Your Lane ━━━\n"));
191
283
 
192
284
  const { connectToCloud } = await inquirer.prompt([
193
285
  {
@@ -196,11 +288,11 @@ async function setupWizard(options = {}) {
196
288
  message: "How would you like to use Reshot?",
197
289
  choices: [
198
290
  {
199
- name: `${chalk.cyan("Connect to Reshot Cloud")} - Governance, CDN hosting, team collaboration`,
291
+ name: `${chalk.cyan("Set up hosted pipeline")} - Connect to Reshot Cloud for hosted assets, review workflows, and team collaboration`,
200
292
  value: true,
201
293
  },
202
294
  {
203
- name: `${chalk.gray("Offline mode")} - Local-only visuals (no cloud sync)`,
295
+ name: `${chalk.gray("Start locally")} - Get a first capture working on your machine before you add hosted delivery`,
204
296
  value: false,
205
297
  },
206
298
  ],
@@ -211,7 +303,7 @@ async function setupWizard(options = {}) {
211
303
  console.log(chalk.gray("\nOpening browser for authentication...\n"));
212
304
 
213
305
  const authCommand = require("./auth");
214
- await authCommand();
306
+ const authResult = await authCommand();
215
307
 
216
308
  // Re-read settings after auth
217
309
  try {
@@ -231,6 +323,9 @@ async function setupWizard(options = {}) {
231
323
  chalk.green("\n✔ Connected to"),
232
324
  chalk.cyan(existingSettings.projectName || existingSettings.projectId),
233
325
  );
326
+ if (authResult?.mode) {
327
+ console.log(chalk.gray(` Mode: ${authResult.mode}`));
328
+ }
234
329
  } else {
235
330
  console.log(chalk.gray("Running in offline mode — no cloud sync."));
236
331
  }
@@ -240,11 +335,16 @@ async function setupWizard(options = {}) {
240
335
 
241
336
  const useCloud = isAlreadyAuthed;
242
337
  const projectId = existingSettings?.projectId;
338
+ const normalizedSetupConfig = normalizeConfigContract(
339
+ existingConfig && typeof existingConfig === "object" ? existingConfig : {},
340
+ );
341
+ const supportedLocalCommand =
342
+ getRecommendedLocalServerCommand(normalizedSetupConfig);
243
343
 
244
344
  // ========================================
245
- // STEP 2: Visual Capture Configuration
345
+ // STEP 2: Project Defaults
246
346
  // ========================================
247
- console.log(chalk.cyan("\n━━━ Visual Capture ━━━\n"));
347
+ console.log(chalk.cyan("\n━━━ Step 2: Project Defaults ━━━\n"));
248
348
 
249
349
  let traceDir = "./test-results";
250
350
 
@@ -253,24 +353,25 @@ async function setupWizard(options = {}) {
253
353
  if (playwrightInfo.configFile) {
254
354
  console.log(chalk.gray(` Config: ${playwrightInfo.configFile}`));
255
355
  }
356
+ const { customTraceDir } = await inquirer.prompt([
357
+ {
358
+ type: "input",
359
+ name: "customTraceDir",
360
+ message: "Playwright test-results directory:",
361
+ default: "./test-results",
362
+ },
363
+ ]);
364
+
365
+ traceDir = customTraceDir;
256
366
  } else {
257
- console.log(chalk.yellow(" Playwright not detected"));
367
+ console.log(chalk.green(" No Playwright setup detected"));
258
368
  console.log(
259
- chalk.gray(" Install with: npm install -D @playwright/test"),
369
+ chalk.gray(
370
+ " That is okay for the local-first workflow. You can define or record scenarios and run them directly.",
371
+ ),
260
372
  );
261
373
  }
262
374
 
263
- const { customTraceDir } = await inquirer.prompt([
264
- {
265
- type: "input",
266
- name: "customTraceDir",
267
- message: "Playwright test-results directory:",
268
- default: "./test-results",
269
- },
270
- ]);
271
-
272
- traceDir = customTraceDir;
273
-
274
375
  // ========================================
275
376
  // Generate Configuration
276
377
  // ========================================
@@ -311,6 +412,25 @@ async function setupWizard(options = {}) {
311
412
  config.writeConfig(newConfig);
312
413
  console.log(chalk.green("✔ Created reshot.config.json"));
313
414
 
415
+ const combinedConfig =
416
+ existingConfig && typeof existingConfig === "object"
417
+ ? { ...existingConfig, ...newConfig }
418
+ : newConfig;
419
+ const normalizedCombinedConfig = normalizeConfigContract(combinedConfig);
420
+ const setupMode = detectSetupMode(combinedConfig, useCloud);
421
+ const finalSupportedLocalCommand =
422
+ getRecommendedLocalServerCommand(normalizedCombinedConfig) ||
423
+ supportedLocalCommand;
424
+ const reportPath = writeSetupReport({
425
+ mode: setupMode,
426
+ useCloud,
427
+ projectId,
428
+ projectName: existingSettings?.projectName,
429
+ configCreated: true,
430
+ playwrightDetected: playwrightInfo.hasPlaywright,
431
+ supportedLocalCommand: finalSupportedLocalCommand,
432
+ });
433
+
314
434
  // ========================================
315
435
  // Success & Next Steps
316
436
  // ========================================
@@ -318,21 +438,59 @@ async function setupWizard(options = {}) {
318
438
  console.log(chalk.green.bold("✔ Reshot setup complete!"));
319
439
  console.log(chalk.green("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
320
440
 
321
- console.log(chalk.cyan("Next steps:\n"));
441
+ console.log(chalk.cyan("Supported launch path:\n"));
442
+ console.log(
443
+ ` ${chalk.gray("Run your target app with a production-like local server before capture.")}`,
444
+ );
445
+ console.log(
446
+ ` ${chalk.cyan(finalSupportedLocalCommand || "npm run build && npm run start")}`,
447
+ );
448
+ console.log(
449
+ ` ${chalk.gray("Unsupported for launch reliability:")} ${chalk.yellow("next dev")}\n`,
450
+ );
451
+
452
+ console.log(chalk.cyan("Setup mode:\n"));
453
+ console.log(` ${chalk.green(setupMode)}\n`);
322
454
 
455
+ console.log(chalk.cyan("Next steps:\n"));
456
+ console.log(
457
+ ` 1. ${chalk.gray("Review")} ${chalk.cyan("reshot.config.json")} ${chalk.gray("and add your first scenario or recording")}`,
458
+ );
323
459
  console.log(
324
- ` 1. ${chalk.gray("Review")} ${chalk.cyan("reshot.config.json")} ${chalk.gray("and commit to your repo")}`,
460
+ ` 2. ${chalk.gray("Start your app in the supported environment:")}`,
325
461
  );
326
462
  console.log(
327
- ` 2. ${chalk.gray("Run Playwright tests to generate traces:")}`,
463
+ ` ${chalk.cyan(finalSupportedLocalCommand || "npm run build && npm run start")}`,
328
464
  );
329
- console.log(` ${chalk.cyan("npx playwright test")}`);
465
+ console.log(` 3. ${chalk.gray("Generate your first local capture:")}`);
466
+ console.log(` ${chalk.cyan("reshot run")}`);
330
467
 
331
- console.log(`\n ${chalk.gray("Sync visuals to Reshot:")}`);
332
- console.log(` ${chalk.cyan("reshot sync")}`);
468
+ if (useCloud) {
469
+ console.log(
470
+ `\n 4. ${chalk.gray("Upgrade to hosted assets when you are ready:")}`,
471
+ );
472
+ console.log(` ${chalk.cyan("reshot publish")}`);
473
+ }
333
474
 
334
- console.log(`\n ${chalk.gray("Launch the visual management UI:")}`);
335
- console.log(` ${chalk.cyan("reshot studio")}\n`);
475
+ if (setupMode === "certified-target" || setupMode === "candidate-target") {
476
+ console.log(
477
+ `\n 5. ${chalk.gray("Use advanced target checks when you need them:")}`,
478
+ );
479
+ console.log(` ${chalk.cyan("reshot doctor target")}`);
480
+ console.log(` ${chalk.cyan("reshot verify publish")}`);
481
+ if (setupMode === "certified-target") {
482
+ console.log(` ${chalk.cyan("reshot certify")}`);
483
+ }
484
+ }
485
+
486
+ console.log(`\n ${chalk.gray("Open Studio to inspect output locally:")}`);
487
+ console.log(` ${chalk.cyan("reshot studio")}`);
488
+ console.log(
489
+ `\n ${chalk.gray("Supported environments guide:")} ${chalk.cyan("https://reshot.dev/docs/cli/getting-started/supported-environments")}`,
490
+ );
491
+ console.log(
492
+ `\n ${chalk.gray("Setup report written to:")} ${chalk.cyan(path.relative(process.cwd(), reportPath))}\n`,
493
+ );
336
494
 
337
495
  // Offer to launch studio
338
496
  const { launchStudio } = await inquirer.prompt([
@@ -340,7 +498,7 @@ async function setupWizard(options = {}) {
340
498
  type: "confirm",
341
499
  name: "launchStudio",
342
500
  message: "Launch Reshot Studio now?",
343
- default: true,
501
+ default: false,
344
502
  },
345
503
  ]);
346
504
 
@@ -156,9 +156,9 @@ async function setupCommand(options = {}) {
156
156
  await uiCommand({ open: true });
157
157
  } else {
158
158
  console.log(chalk.gray("Next steps:"));
159
- console.log(chalk.gray(" • Run"), chalk.cyan("reshot studio"), chalk.gray("to open the management UI"));
160
- console.log(chalk.gray(" • Run"), chalk.cyan("reshot record"), chalk.gray("to capture your first visual"));
161
- console.log(chalk.gray(" • Run"), chalk.cyan("reshot run"), chalk.gray("to execute scenarios\n"));
159
+ console.log(chalk.gray(" • Run"), chalk.cyan("reshot run"), chalk.gray("to generate your first local capture"));
160
+ console.log(chalk.gray(" • Run"), chalk.cyan("reshot publish"), chalk.gray("when you are ready for hosted assets and review workflows"));
161
+ console.log(chalk.gray(" • Run"), chalk.cyan("reshot studio"), chalk.gray("to inspect output locally\n"));
162
162
  }
163
163
  }
164
164