@reshotdev/screenshot 0.0.1-beta.1 → 0.0.1-beta.10

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 (38) hide show
  1. package/README.md +65 -7
  2. package/package.json +9 -2
  3. package/src/commands/auth.js +108 -26
  4. package/src/commands/certify.js +62 -0
  5. package/src/commands/ci-run.js +57 -2
  6. package/src/commands/ci-setup.js +5 -5
  7. package/src/commands/doctor-release.js +67 -0
  8. package/src/commands/doctor-target.js +49 -0
  9. package/src/commands/drifts.js +5 -70
  10. package/src/commands/import-tests.js +13 -13
  11. package/src/commands/ingest.js +10 -10
  12. package/src/commands/init.js +16 -277
  13. package/src/commands/publish.js +204 -237
  14. package/src/commands/pull.js +253 -23
  15. package/src/commands/run.js +292 -12
  16. package/src/commands/setup-wizard.js +277 -499
  17. package/src/commands/setup.js +41 -13
  18. package/src/commands/status.js +313 -125
  19. package/src/commands/sync.js +28 -236
  20. package/src/commands/ui.js +1 -1
  21. package/src/commands/verify-publish.js +46 -0
  22. package/src/index.js +194 -94
  23. package/src/lib/api-client.js +121 -35
  24. package/src/lib/capture-engine.js +103 -7
  25. package/src/lib/capture-script-runner.js +305 -58
  26. package/src/lib/certification.js +865 -0
  27. package/src/lib/config.js +181 -76
  28. package/src/lib/record-cdp.js +288 -16
  29. package/src/lib/record-config.js +1 -1
  30. package/src/lib/release-doctor.js +313 -0
  31. package/src/lib/run-manifest.js +103 -0
  32. package/src/lib/standalone-mode.js +1 -1
  33. package/src/lib/storage-providers.js +4 -4
  34. package/src/lib/target-contract.js +292 -0
  35. package/src/lib/ui-api.js +6 -7
  36. package/web/manager/dist/assets/{index--ZgioErz.js → index-D2qqcFNN.js} +1 -1
  37. package/web/manager/dist/index.html +1 -1
  38. package/src/commands/validate-docs.js +0 -529
@@ -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"));
@@ -112,17 +267,30 @@ async function pullCommand(options = {}) {
112
267
  projectConfig = config.readConfig();
113
268
  } catch (e) {
114
269
  console.error(
115
- chalk.red("Error: No docsync.config.json found. Run 'reshot init' first.")
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 docsync.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,28 @@ 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" };
313
+ }
314
+
315
+ if (settings.projectId && projectId !== settings.projectId) {
316
+ console.warn(
317
+ chalk.yellow(
318
+ ` ⚠ Project ID mismatch: config uses ${projectId}, but authenticated project is ${settings.projectId}.`
319
+ )
320
+ );
321
+ console.warn(
322
+ chalk.yellow(
323
+ ` This will cause auth failures. Run 'reshot setup --force' to re-link.\n`
324
+ )
325
+ );
143
326
  }
144
327
 
145
328
  try {
@@ -154,16 +337,32 @@ async function pullCommand(options = {}) {
154
337
 
155
338
  if (!response || !response.assets) {
156
339
  console.error(chalk.red("Error: Invalid response from API"));
157
- process.exit(1);
340
+ if (!noExit) process.exit(1);
341
+ return { success: false, error: "Invalid response from API" };
158
342
  }
159
343
 
160
- const { meta, assets } = response;
161
- 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(
162
350
  (count, group) => count + Object.keys(group).length,
163
351
  0
164
352
  );
165
353
 
166
- console.log(chalk.green(` ✓ Fetched ${assetCount} visuals\n`));
354
+ if (assetCount === 0) {
355
+ console.log(chalk.yellow(` ⚠ Fetched 0 visuals.\n`));
356
+ console.log(chalk.gray(` Possible reasons:`));
357
+ if (status === "approved") {
358
+ console.log(chalk.gray(` • No visuals approved yet. Try: reshot pull --status all`));
359
+ }
360
+ console.log(chalk.gray(` • No visuals published. Run: reshot publish`));
361
+ console.log(chalk.gray(` • Wrong project. Config uses: ${projectId}`));
362
+ console.log(chalk.gray(` • Check: ${settings.platformUrl || "https://reshot.dev"}\n`));
363
+ } else {
364
+ console.log(chalk.green(` ✓ Fetched ${assetCount} visuals\n`));
365
+ }
167
366
 
168
367
  // Determine output path
169
368
  let outputPath = output;
@@ -187,7 +386,7 @@ async function pullCommand(options = {}) {
187
386
 
188
387
  switch (format) {
189
388
  case "ts":
190
- content = generateTypeScript(assets, full);
389
+ content = generateTypeScript(normalizedAssets, full);
191
390
  contentType = "TypeScript";
192
391
  break;
193
392
 
@@ -196,9 +395,28 @@ async function pullCommand(options = {}) {
196
395
  const headers = ["Key", "Group", "Name", "Context", "Type", "Unbreakable_URL", "Width", "Height"];
197
396
  const rows = [headers.join(",")];
198
397
 
199
- for (const [group, visuals] of Object.entries(assets)) {
398
+ for (const [group, visuals] of Object.entries(normalizedAssets)) {
200
399
  for (const [visualKey, variants] of Object.entries(visuals)) {
201
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
+
202
420
  const type = asset.type.split("/")[0]; // 'image' or 'video'
203
421
  rows.push(
204
422
  [
@@ -222,7 +440,7 @@ async function pullCommand(options = {}) {
222
440
 
223
441
  case "json":
224
442
  default:
225
- content = JSON.stringify({ meta, assets }, null, 2);
443
+ content = JSON.stringify({ meta, assets: normalizedAssets }, null, 2);
226
444
  contentType = "JSON";
227
445
  break;
228
446
  }
@@ -285,6 +503,15 @@ async function pullCommand(options = {}) {
285
503
  console.log(chalk.gray(" This ensures your build always uses the latest approved visuals."));
286
504
  console.log(chalk.gray(" If a visual is deleted, your build will fail (preventing broken images).\n"));
287
505
 
506
+ return {
507
+ success: true,
508
+ assetCount,
509
+ outputPath: path.resolve(outputPath),
510
+ normalizedAssets,
511
+ normalizationRepairs: normalization.repairs,
512
+ meta,
513
+ };
514
+
288
515
  } catch (error) {
289
516
  if (config.isAuthError(error)) {
290
517
  console.error(
@@ -296,7 +523,10 @@ async function pullCommand(options = {}) {
296
523
  console.error(chalk.gray(error.stack));
297
524
  }
298
525
  }
299
- process.exit(1);
526
+ if (!noExit) {
527
+ process.exit(1);
528
+ }
529
+ return { success: false, error: error.message };
300
530
  }
301
531
  }
302
532