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

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
@@ -2,6 +2,9 @@
2
2
  const { chromium } = require("playwright");
3
3
  const chalk = require("chalk");
4
4
  const http = require("http");
5
+ const fs = require("fs-extra");
6
+ const path = require("path");
7
+ const os = require("os");
5
8
 
6
9
  /**
7
10
  * Check if Chrome CDP endpoint is available
@@ -418,15 +421,14 @@ async function saveSessionState(outputPath) {
418
421
  console.log(chalk.gray(` Sanitized cookies: ${stats.fixed} fixed, ${stats.removed} removed, ${stats.stripped} stripped`));
419
422
  }
420
423
 
421
- // Ensure directory exists
422
- const fs = require("fs-extra");
423
- const path = require("path");
424
- fs.ensureDirSync(path.dirname(outputPath));
425
-
426
- // Write storage state to file
427
- fs.writeJsonSync(outputPath, storageState, { spaces: 2 });
424
+ const activePageUrl = page?.url?.() || null;
425
+ const artifactInfo = writeSessionArtifacts(outputPath, storageState, {
426
+ pageUrl: activePageUrl,
427
+ baseUrl: activePageUrl,
428
+ });
428
429
 
429
430
  console.log(chalk.green(` ✔ Session saved to: ${outputPath}`));
431
+ console.log(chalk.gray(` Metadata: ${artifactInfo.metadataPath}`));
430
432
  console.log(chalk.gray(` Cookies: ${storageState.cookies?.length || 0}`));
431
433
  console.log(chalk.gray(` Origins with localStorage: ${storageState.origins?.length || 0}`));
432
434
 
@@ -445,11 +447,249 @@ async function saveSessionState(outputPath) {
445
447
  * @returns {string}
446
448
  */
447
449
  function getDefaultSessionPath() {
448
- const path = require("path");
449
- const os = require("os");
450
450
  return path.join(os.homedir(), ".reshot", "session-state.json");
451
451
  }
452
452
 
453
+ function getSessionMetadataPath(sessionPath = getDefaultSessionPath()) {
454
+ if (sessionPath.endsWith(".json")) {
455
+ return sessionPath.replace(/\.json$/i, ".meta.json");
456
+ }
457
+
458
+ return `${sessionPath}.meta.json`;
459
+ }
460
+
461
+ function normalizeOrigin(value) {
462
+ if (!value || typeof value !== "string") {
463
+ return null;
464
+ }
465
+
466
+ try {
467
+ return new URL(value).origin.toLowerCase();
468
+ } catch {
469
+ return null;
470
+ }
471
+ }
472
+
473
+ function normalizeCookieDomain(domain) {
474
+ if (!domain || typeof domain !== "string") {
475
+ return null;
476
+ }
477
+
478
+ return domain.replace(/^\./, "").toLowerCase();
479
+ }
480
+
481
+ function extractSessionEvidence(storageState) {
482
+ const storageOrigins = Array.from(
483
+ new Set(
484
+ (storageState?.origins || [])
485
+ .map((origin) => normalizeOrigin(origin?.origin))
486
+ .filter(Boolean),
487
+ ),
488
+ );
489
+
490
+ const cookieDomains = Array.from(
491
+ new Set(
492
+ (storageState?.cookies || [])
493
+ .map((cookie) => normalizeCookieDomain(cookie?.domain))
494
+ .filter(Boolean),
495
+ ),
496
+ );
497
+
498
+ return { storageOrigins, cookieDomains };
499
+ }
500
+
501
+ function buildSessionMetadata(options = {}) {
502
+ const {
503
+ baseUrl = null,
504
+ pageUrl = null,
505
+ storageState = null,
506
+ capturedAt = new Date().toISOString(),
507
+ } = options;
508
+ const evidence = extractSessionEvidence(storageState);
509
+
510
+ return {
511
+ version: 1,
512
+ capturedAt,
513
+ sourceUrl: pageUrl || baseUrl || null,
514
+ sourceOrigin:
515
+ normalizeOrigin(pageUrl) ||
516
+ normalizeOrigin(baseUrl) ||
517
+ evidence.storageOrigins[0] ||
518
+ null,
519
+ storageOrigins: evidence.storageOrigins,
520
+ cookieDomains: evidence.cookieDomains,
521
+ };
522
+ }
523
+
524
+ function writeSessionArtifacts(sessionPath, storageState, options = {}) {
525
+ const metadataPath = getSessionMetadataPath(sessionPath);
526
+ const metadata = buildSessionMetadata({ ...options, storageState });
527
+
528
+ fs.ensureDirSync(path.dirname(sessionPath));
529
+ fs.writeJsonSync(sessionPath, storageState, { spaces: 2 });
530
+ fs.writeJsonSync(metadataPath, metadata, { spaces: 2 });
531
+
532
+ return { sessionPath, metadataPath, metadata };
533
+ }
534
+
535
+ function readSessionMetadata(sessionPath = getDefaultSessionPath()) {
536
+ const metadataPath = getSessionMetadataPath(sessionPath);
537
+
538
+ if (!fs.existsSync(metadataPath)) {
539
+ return {
540
+ exists: false,
541
+ metadataPath,
542
+ metadata: null,
543
+ error: null,
544
+ };
545
+ }
546
+
547
+ try {
548
+ return {
549
+ exists: true,
550
+ metadataPath,
551
+ metadata: fs.readJsonSync(metadataPath),
552
+ error: null,
553
+ };
554
+ } catch (error) {
555
+ return {
556
+ exists: true,
557
+ metadataPath,
558
+ metadata: null,
559
+ error: error.message,
560
+ };
561
+ }
562
+ }
563
+
564
+ function sessionHostMatchesCookieDomains(hostname, cookieDomains) {
565
+ if (!hostname) {
566
+ return false;
567
+ }
568
+
569
+ return cookieDomains.some(
570
+ (domain) => hostname === domain || hostname.endsWith(`.${domain}`),
571
+ );
572
+ }
573
+
574
+ function assessSessionHealth(
575
+ sessionPath = getDefaultSessionPath(),
576
+ baseUrl = null,
577
+ options = {},
578
+ ) {
579
+ const maxAgeMinutes = Number.isFinite(options.maxAgeMinutes)
580
+ ? options.maxAgeMinutes
581
+ : 360;
582
+ const expectedOrigin = normalizeOrigin(baseUrl);
583
+ const result = {
584
+ ok: false,
585
+ exists: fs.existsSync(sessionPath),
586
+ sessionPath,
587
+ metadataPath: getSessionMetadataPath(sessionPath),
588
+ expectedOrigin,
589
+ compatible: true,
590
+ stale: false,
591
+ ageMinutes: null,
592
+ metadata: null,
593
+ warnings: [],
594
+ issues: [],
595
+ evidence: {
596
+ sourceOrigin: null,
597
+ storageOrigins: [],
598
+ cookieDomains: [],
599
+ matchSource: null,
600
+ },
601
+ };
602
+
603
+ if (!result.exists) {
604
+ return result;
605
+ }
606
+
607
+ let storageState = null;
608
+ try {
609
+ storageState = fs.readJsonSync(sessionPath);
610
+ } catch (error) {
611
+ result.compatible = false;
612
+ result.issues.push(`Cached session file is unreadable: ${error.message}`);
613
+ return result;
614
+ }
615
+
616
+ const metadataInfo = readSessionMetadata(sessionPath);
617
+ if (metadataInfo.error) {
618
+ result.warnings.push(`Session metadata is unreadable: ${metadataInfo.error}`);
619
+ } else if (!metadataInfo.exists) {
620
+ result.warnings.push(
621
+ "Session metadata is missing; compatibility was inferred from stored cookies and origins.",
622
+ );
623
+ }
624
+
625
+ const inferredEvidence = extractSessionEvidence(storageState);
626
+ const metadata = metadataInfo.metadata || null;
627
+ const storageOrigins =
628
+ metadata?.storageOrigins
629
+ ?.map((origin) => normalizeOrigin(origin))
630
+ .filter(Boolean) || inferredEvidence.storageOrigins;
631
+ const cookieDomains =
632
+ metadata?.cookieDomains
633
+ ?.map((domain) => normalizeCookieDomain(domain))
634
+ .filter(Boolean) || inferredEvidence.cookieDomains;
635
+ const sourceOrigin =
636
+ normalizeOrigin(metadata?.sourceOrigin) ||
637
+ normalizeOrigin(metadata?.sourceUrl) ||
638
+ inferredEvidence.storageOrigins[0] ||
639
+ null;
640
+
641
+ result.metadata = metadata;
642
+ result.evidence = {
643
+ sourceOrigin,
644
+ storageOrigins,
645
+ cookieDomains,
646
+ matchSource: null,
647
+ };
648
+
649
+ const capturedAtMs = Date.parse(metadata?.capturedAt || "");
650
+ const sessionStat = fs.statSync(sessionPath);
651
+ const referenceTime = Number.isFinite(capturedAtMs)
652
+ ? capturedAtMs
653
+ : sessionStat.mtimeMs;
654
+ result.ageMinutes = Math.max(
655
+ 0,
656
+ Math.round((Date.now() - referenceTime) / 60000),
657
+ );
658
+ result.stale = result.ageMinutes >= maxAgeMinutes;
659
+
660
+ if (expectedOrigin) {
661
+ const expectedHost = new URL(baseUrl).hostname.toLowerCase();
662
+ const matchesSourceOrigin = sourceOrigin === expectedOrigin;
663
+ const matchesStorageOrigin = storageOrigins.includes(expectedOrigin);
664
+ const matchesCookieDomain = sessionHostMatchesCookieDomains(
665
+ expectedHost,
666
+ cookieDomains,
667
+ );
668
+
669
+ result.evidence.matchSource = matchesSourceOrigin
670
+ ? "sourceOrigin"
671
+ : matchesStorageOrigin
672
+ ? "storageOrigins"
673
+ : matchesCookieDomain
674
+ ? "cookieDomains"
675
+ : null;
676
+
677
+ const hasEvidence =
678
+ Boolean(sourceOrigin) ||
679
+ storageOrigins.length > 0 ||
680
+ cookieDomains.length > 0;
681
+ if (hasEvidence && !result.evidence.matchSource) {
682
+ result.compatible = false;
683
+ result.issues.push(
684
+ `Cached session targets ${sourceOrigin || storageOrigins[0] || cookieDomains[0]}, not ${expectedOrigin}.`,
685
+ );
686
+ }
687
+ }
688
+
689
+ result.ok = result.compatible && result.issues.length === 0;
690
+ return result;
691
+ }
692
+
453
693
  /**
454
694
  * Quietly check if CDP is available and sync the session if so.
455
695
  * This is called automatically before captures to use the live browser session.
@@ -459,13 +699,24 @@ function getDefaultSessionPath() {
459
699
  */
460
700
  async function autoSyncSessionFromCDP(outputPath = null, logger = null) {
461
701
  const log = logger || (() => {}); // Quiet by default
462
- const fs = require("fs-extra");
463
- const path = require("path");
464
- const os = require("os");
465
-
702
+
703
+ // Allow disabling CDP sync via env var (useful when session was created programmatically)
704
+ if (process.env.RESHOT_SKIP_CDP_SYNC === "1") {
705
+ return { synced: false, reason: "disabled" };
706
+ }
707
+
466
708
  const sessionPath = outputPath || path.join(os.homedir(), ".reshot", "session-state.json");
467
-
709
+
468
710
  try {
711
+ // Skip sync if cached session is very recent (likely just generated programmatically)
712
+ if (fs.existsSync(sessionPath)) {
713
+ const cachedAge = Date.now() - fs.statSync(sessionPath).mtimeMs;
714
+ if (cachedAge < 5 * 60 * 1000) {
715
+ log(chalk.gray(" → Cached session is fresh (<5min), skipping CDP sync"));
716
+ return { synced: false, reason: "cached_fresh" };
717
+ }
718
+ }
719
+
469
720
  // Step 1: Check if CDP endpoint is available (quietly)
470
721
  const endpointCheck = await checkCdpEndpoint("localhost", 9222);
471
722
 
@@ -513,9 +764,25 @@ async function autoSyncSessionFromCDP(outputPath = null, logger = null) {
513
764
  return { synced: false, reason: "empty_session" };
514
765
  }
515
766
 
767
+ const activePage =
768
+ context
769
+ .pages()
770
+ .find((candidate) => {
771
+ const currentUrl = candidate.url();
772
+ return (
773
+ currentUrl &&
774
+ !currentUrl.startsWith("chrome://") &&
775
+ !currentUrl.startsWith("devtools://") &&
776
+ !currentUrl.startsWith("chrome-extension://")
777
+ );
778
+ }) || context.pages()[0] || null;
779
+ const activePageUrl = activePage?.url?.() || null;
780
+
516
781
  // Step 4: Save to file
517
- fs.ensureDirSync(path.dirname(sessionPath));
518
- fs.writeJsonSync(sessionPath, storageState, { spaces: 2 });
782
+ writeSessionArtifacts(sessionPath, storageState, {
783
+ pageUrl: activePageUrl,
784
+ baseUrl: activePageUrl,
785
+ });
519
786
 
520
787
  log(chalk.green(` ✔ Auto-synced session from CDP browser`));
521
788
  log(chalk.gray(` Cookies: ${storageState.cookies?.length || 0}, localStorage origins: ${storageState.origins?.length || 0}`));
@@ -607,6 +874,11 @@ module.exports = {
607
874
  printChromeInstructions,
608
875
  saveSessionState,
609
876
  getDefaultSessionPath,
877
+ getSessionMetadataPath,
610
878
  autoSyncSessionFromCDP,
611
879
  sanitizeStorageState,
880
+ buildSessionMetadata,
881
+ writeSessionArtifacts,
882
+ readSessionMetadata,
883
+ assessSessionHealth,
612
884
  };
@@ -8,13 +8,27 @@ const { chromium } = require('playwright');
8
8
  const { updateBrowserMode } = require('./record-browser-injection');
9
9
  const { runPolishedClip } = require('./polished-clip');
10
10
  const { saveScenarioProgress } = require('./record-config');
11
+ const { resolveTargets } = require('./resolve-targets');
12
+ const { captureDomArtifact } = require('./dom-capture');
11
13
 
12
14
  /**
13
15
  * Start clip recording flow
14
16
  * @param {Object} sessionState - Recording session state
15
17
  * @param {Page} page - Playwright page object
16
18
  */
17
- async function startClipRecording(sessionState, page) {
19
+ async function startClipRecording(sessionState, page, config = {}) {
20
+ const metadataRecorder = config.emitMetadata
21
+ ? createClipMetadataRecorder({
22
+ slug: config.slug || sessionState.visualKey,
23
+ captureSize: config.captureSize || { width: 1280, height: 720 },
24
+ })
25
+ : null;
26
+
27
+ if (metadataRecorder) {
28
+ sessionState.logEvent = metadataRecorder.logEvent;
29
+ metadataRecorder.logEvent('workflow_start');
30
+ }
31
+
18
32
  // Ask about container element
19
33
  const { useContainer } = await inquirer.prompt([
20
34
  {
@@ -127,6 +141,10 @@ async function startClipRecording(sessionState, page) {
127
141
 
128
142
  // Set up action replay handler to sync video with real user actions
129
143
  sessionState.replayActionToRecording = async (action, selector, text) => {
144
+ if (metadataRecorder) {
145
+ metadataRecorder.logEvent(action, { selector, text });
146
+ }
147
+
130
148
  try {
131
149
  if (action === 'click') {
132
150
  await recordingPage.click(selector);
@@ -233,6 +251,17 @@ async function startClipRecording(sessionState, page) {
233
251
 
234
252
  sessionState.capturedSteps.push(clipStep);
235
253
  await saveScenarioProgress(sessionState, page, { finalize: false });
254
+
255
+ if (metadataRecorder) {
256
+ await writeClipMetadata({
257
+ page,
258
+ outputDir: config.outputDir || sessionState.outputDir || process.cwd(),
259
+ slug: metadataRecorder.slug,
260
+ captureSize: metadataRecorder.captureSize,
261
+ timeline: metadataRecorder.timeline,
262
+ targets: config.targets,
263
+ });
264
+ }
236
265
 
237
266
  // Clean up temp video
238
267
  fs.removeSync(rawVideoPath);
@@ -242,6 +271,9 @@ async function startClipRecording(sessionState, page) {
242
271
  sessionState.clipEvents = null;
243
272
  sessionState.recordingStart = null;
244
273
  sessionState.stopClipRecording = false;
274
+ if (metadataRecorder) {
275
+ delete sessionState.logEvent;
276
+ }
245
277
 
246
278
  console.log(
247
279
  chalk.green(
@@ -250,6 +282,53 @@ async function startClipRecording(sessionState, page) {
250
282
  );
251
283
  }
252
284
 
285
+ function createClipMetadataRecorder({ slug, captureSize }) {
286
+ const startedAt = Date.now();
287
+ const timeline = [];
288
+
289
+ return {
290
+ slug,
291
+ captureSize,
292
+ timeline,
293
+ logEvent(type, payload = {}) {
294
+ timeline.push({
295
+ tMs: Date.now() - startedAt,
296
+ type,
297
+ payload,
298
+ });
299
+ },
300
+ };
301
+ }
302
+
303
+ async function writeClipMetadata({
304
+ page,
305
+ outputDir,
306
+ slug,
307
+ captureSize,
308
+ timeline,
309
+ targets,
310
+ }) {
311
+ const resolvedTargets = targets ? await resolveTargets(page, targets) : {};
312
+
313
+ // Tier-3: alongside the video, emit a self-contained DOM reconstruction
314
+ // artifact (<slug>.dom.html + sidecars) for eligible screens. Additive and
315
+ // best-effort — it must never break the video path, so failures are swallowed.
316
+ await fs.ensureDir(outputDir);
317
+ const domArtifact = await captureDomArtifact({ page, outputDir, slug });
318
+
319
+ const metadata = {
320
+ slug,
321
+ version: 1,
322
+ captureSize,
323
+ timeline,
324
+ targets: resolvedTargets,
325
+ ...(domArtifact ? { domArtifact } : {}),
326
+ };
327
+ const metadataPath = path.join(outputDir, `${slug}.metadata.json`);
328
+ await fs.writeJson(metadataPath, metadata, { spaces: 2 });
329
+ return metadataPath;
330
+ }
331
+
253
332
  /**
254
333
  * Run subtitle editor mini-server
255
334
  * @param {Array} events - Clip events with timestamps
@@ -338,6 +417,7 @@ function buildClipKey(visualKey, filename) {
338
417
  }
339
418
 
340
419
  module.exports = {
341
- startClipRecording
420
+ createClipMetadataRecorder,
421
+ startClipRecording,
422
+ writeClipMetadata
342
423
  };
343
-
@@ -492,10 +492,6 @@ async function persistFinalScenario(sessionState, page, options = {}) {
492
492
  // Default output configuration for automatic step-by-step image generation
493
493
  const defaultOutput = {
494
494
  format: "step-by-step-images",
495
- highlight: {
496
- color: "rgba(255, 255, 0, 0.5)",
497
- style: "box",
498
- },
499
495
  };
500
496
 
501
497
  // Parse groupPath from session state or existing scenario
@@ -600,7 +596,7 @@ async function finalizeScenarioAndWriteConfig(
600
596
 
601
597
  console.log(
602
598
  chalk.green(
603
- "\n✔ docsync.config.json has been updated. Please review and commit the changes to your repository.\n"
599
+ "\n✔ reshot.config.json has been updated. Please review and commit the changes to your repository.\n"
604
600
  )
605
601
  );
606
602
  console.log(chalk.gray(`Scenario: ${result.scenarioName}`));