@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
package/src/lib/ui-api.js CHANGED
@@ -36,118 +36,20 @@ const {
36
36
  applyStyle,
37
37
  isStyleAvailable,
38
38
  } = require("./style-engine");
39
-
40
- /**
41
- * Get the platform URL from settings, falling back to localhost for development
42
- * @param {Object} settings - CLI settings object
43
- * @returns {string} Platform URL
44
- */
45
- function getPlatformUrl(settings) {
46
- // Priority: settings.platformUrl > env var > localhost default
47
- if (settings?.platformUrl) {
48
- return settings.platformUrl;
49
- }
50
- const envUrl =
51
- process.env.RESHOT_API_BASE_URL || process.env.DOCSYNC_API_BASE_URL;
52
- if (envUrl) {
53
- // Remove /api suffix if present to get platform URL
54
- return envUrl.replace(/\/api\/?$/, "");
55
- }
56
- return "http://localhost:3000";
57
- }
58
-
59
- /**
60
- * Handle API errors and detect if re-auth is needed
61
- * @param {Error} error - The error from API call
62
- * @param {Object} res - Express response object
63
- * @returns {Object|null} Response if error was handled, null otherwise
64
- */
65
- function handleApiError(error, res) {
66
- if (config.isAuthError(error)) {
67
- const errorMsg =
68
- error.response?.data?.error ||
69
- error.message ||
70
- "API key is invalid or expired";
71
- return res.status(401).json(config.createAuthErrorResponse(errorMsg));
72
- }
73
- return null; // Error not handled, let caller handle it
74
- }
75
-
76
- /**
77
- * Generate all possible variant combinations from dimensions
78
- * @param {Object} dimensions - Variant dimensions config
79
- * @param {string[]} dimensionKeys - Which dimensions to include
80
- * @returns {Array<Object>} Array of variant objects
81
- */
82
- function generateVariantCombinations(dimensions, dimensionKeys = []) {
83
- if (!dimensions || dimensionKeys.length === 0) {
84
- return [];
85
- }
86
-
87
- // Get options for each dimension
88
- const dimensionOptions = dimensionKeys
89
- .map((key) => {
90
- const dim = dimensions[key];
91
- if (!dim?.options) return [];
92
- return Object.keys(dim.options).map((optKey) => ({
93
- dimension: key,
94
- option: optKey,
95
- }));
96
- })
97
- .filter((opts) => opts.length > 0);
98
-
99
- if (dimensionOptions.length === 0) {
100
- return [];
101
- }
102
-
103
- // Generate cartesian product of all dimension options
104
- const cartesian = (...arrays) => {
105
- return arrays.reduce(
106
- (acc, arr) => acc.flatMap((combo) => arr.map((item) => [...combo, item])),
107
- [[]],
108
- );
109
- };
110
-
111
- const combinations = cartesian(...dimensionOptions);
112
-
113
- // Convert to variant objects
114
- return combinations.map((combo) => {
115
- const variant = {};
116
- for (const { dimension, option } of combo) {
117
- variant[dimension] = option;
118
- }
119
- return variant;
120
- });
121
- }
122
-
123
- /**
124
- * Validate a path segment to prevent directory traversal attacks
125
- * @param {string} segment - Path segment to validate
126
- * @returns {boolean} True if safe, false if potentially malicious
127
- */
128
- function isValidPathSegment(segment) {
129
- if (!segment || typeof segment !== "string") return false;
130
- // Reject empty, dots-only, or segments with path separators
131
- if (segment === "." || segment === "..") return false;
132
- if (segment.includes("/") || segment.includes("\\")) return false;
133
- if (segment.includes("\0")) return false; // Null byte injection
134
- return true;
135
- }
136
-
137
- /**
138
- * Validate that a resolved path stays within the expected base directory
139
- * @param {string} resolvedPath - Fully resolved path
140
- * @param {string} baseDir - Expected base directory
141
- * @returns {boolean} True if path is within base, false otherwise
142
- */
143
- function isPathWithinBase(resolvedPath, baseDir) {
144
- const normalizedBase = path.resolve(baseDir);
145
- const normalizedPath = path.resolve(resolvedPath);
146
- return (
147
- normalizedPath.startsWith(normalizedBase + path.sep) ||
148
- normalizedPath === normalizedBase
149
- );
150
- }
39
+ const {
40
+ generateVariantCombinations,
41
+ getPlatformUrl,
42
+ handleApiError,
43
+ isPathWithinBase,
44
+ isValidPathSegment,
45
+ } = require("./ui-api-helpers");
46
+ const {
47
+ deleteAllOutputAssets,
48
+ deleteScenarioAssetDirectories,
49
+ } = require("./ui-asset-cleanup");
50
+ const { listScenarioVersions } = require("./ui-output-versions");
51
+ const { addScenarioMetadata } = require("./ui-scenario-metadata");
52
+ const { attachRecorderRoutes } = require("./ui-recorder-routes");
151
53
 
152
54
  /**
153
55
  * Attach all API routes to an Express app
@@ -219,134 +121,13 @@ function attachApiRoutes(app, context) {
219
121
  : { scenarios: [] };
220
122
  const scenarios = docSyncConfig?.scenarios || [];
221
123
  const outputBaseDir = path.join(process.cwd(), ".reshot", "output");
222
-
223
- // Get all jobs to find last run times
124
+ const uiExecutor = require("./ui-executor");
224
125
  const allJobs = uiExecutor.getAllJobs(500);
225
-
226
- // Build metadata for each scenario
227
- const scenariosWithMetadata = scenarios.map((scenario) => {
228
- let createdAt = null;
229
- let lastRunAt = null;
230
- let lastRunStatus = null;
231
- let assetCount = 0;
232
-
233
- // Try to find creation date from earliest output folder
234
- const scenarioOutputDir = path.join(outputBaseDir, scenario.key);
235
- if (fs.existsSync(scenarioOutputDir)) {
236
- try {
237
- const subFolders = fs
238
- .readdirSync(scenarioOutputDir)
239
- .filter((item) => {
240
- const fullPath = path.join(scenarioOutputDir, item);
241
- try {
242
- return (
243
- fs.statSync(fullPath).isDirectory() && item !== "latest"
244
- );
245
- } catch {
246
- return false;
247
- }
248
- });
249
-
250
- // Parse timestamps from folder names (format: YYYY-MM-DD_HH-MM-SS)
251
- const timestamps = subFolders
252
- .filter((f) => /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/.test(f))
253
- .map((f) => {
254
- const [date, time] = f.split("_");
255
- const [year, month, day] = date.split("-");
256
- const [hour, min, sec] = time.split("-");
257
- return new Date(
258
- `${year}-${month}-${day}T${hour}:${min}:${sec}`,
259
- );
260
- })
261
- .filter((d) => !isNaN(d.getTime()))
262
- .sort((a, b) => a.getTime() - b.getTime());
263
-
264
- if (timestamps.length > 0) {
265
- createdAt = timestamps[0].toISOString();
266
- lastRunAt = timestamps[timestamps.length - 1].toISOString();
267
- }
268
-
269
- // Count assets in latest folder
270
- const latestDir = path.join(scenarioOutputDir, "latest");
271
- if (fs.existsSync(latestDir)) {
272
- try {
273
- const files = fs.readdirSync(latestDir);
274
- assetCount = files.filter(
275
- (f) =>
276
- f.endsWith(".png") ||
277
- f.endsWith(".jpg") ||
278
- f.endsWith(".mp4") ||
279
- f.endsWith(".webm"),
280
- ).length;
281
- } catch {}
282
- }
283
- } catch (err) {
284
- // Ignore errors reading output directories
285
- }
286
- }
287
-
288
- // Also check jobs for more accurate last run info
289
- const scenarioJobs = allJobs.filter((job) => {
290
- if (job.type !== "run") return false;
291
- const keys = job.params?.scenarioKeys || [];
292
- return keys.includes(scenario.key);
293
- });
294
-
295
- if (scenarioJobs.length > 0) {
296
- // Sort by createdAt desc
297
- scenarioJobs.sort(
298
- (a, b) =>
299
- new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
300
- );
301
- const latestJob = scenarioJobs[0];
302
-
303
- // Use job completion time if available
304
- if (latestJob.completedAt) {
305
- const jobTime = new Date(latestJob.completedAt).toISOString();
306
- if (!lastRunAt || jobTime > lastRunAt) {
307
- lastRunAt = jobTime;
308
- lastRunStatus = latestJob.status;
309
- }
310
- } else if (latestJob.createdAt) {
311
- const jobTime = new Date(latestJob.createdAt).toISOString();
312
- if (!lastRunAt || jobTime > lastRunAt) {
313
- lastRunAt = jobTime;
314
- lastRunStatus = latestJob.status;
315
- }
316
- }
317
-
318
- // Get earliest job for createdAt
319
- const earliestJob = scenarioJobs[scenarioJobs.length - 1];
320
- if (
321
- earliestJob.createdAt &&
322
- (!createdAt || earliestJob.createdAt < createdAt)
323
- ) {
324
- createdAt = earliestJob.createdAt;
325
- }
326
- }
327
-
328
- return {
329
- ...scenario,
330
- _metadata: {
331
- createdAt,
332
- lastRunAt,
333
- lastRunStatus,
334
- assetCount,
335
- },
336
- };
337
- });
338
-
339
- // Sort by lastRunAt descending (most recent first), fallback to name
340
- scenariosWithMetadata.sort((a, b) => {
341
- const aTime = a._metadata?.lastRunAt
342
- ? new Date(a._metadata.lastRunAt).getTime()
343
- : 0;
344
- const bTime = b._metadata?.lastRunAt
345
- ? new Date(b._metadata.lastRunAt).getTime()
346
- : 0;
347
- if (aTime !== bTime) return bTime - aTime;
348
- return a.name.localeCompare(b.name);
349
- });
126
+ const scenariosWithMetadata = addScenarioMetadata(
127
+ scenarios,
128
+ allJobs,
129
+ outputBaseDir,
130
+ );
350
131
 
351
132
  res.json({ scenarios: scenariosWithMetadata });
352
133
  } catch (error) {
@@ -431,7 +212,7 @@ function attachApiRoutes(app, context) {
431
212
 
432
213
  /**
433
214
  * PUT /api/privacy
434
- * Update privacy configuration in docsync.config.json
215
+ * Update privacy configuration in reshot.config.json
435
216
  */
436
217
  app.put("/api/privacy", async (req, res, next) => {
437
218
  try {
@@ -472,7 +253,7 @@ function attachApiRoutes(app, context) {
472
253
 
473
254
  /**
474
255
  * PUT /api/style
475
- * Update style configuration in docsync.config.json
256
+ * Update style configuration in reshot.config.json
476
257
  */
477
258
  app.put("/api/style", async (req, res, next) => {
478
259
  try {
@@ -727,29 +508,7 @@ function attachApiRoutes(app, context) {
727
508
  app.delete("/api/assets", async (req, res, next) => {
728
509
  try {
729
510
  const outputDir = path.join(process.cwd(), ".reshot", "output");
730
-
731
- if (!fs.existsSync(outputDir)) {
732
- return res.json({ ok: true, deleted: 0 });
733
- }
734
-
735
- // Count files before deletion
736
- let fileCount = 0;
737
- function countFiles(dir) {
738
- const items = fs.readdirSync(dir);
739
- for (const item of items) {
740
- const fullPath = path.join(dir, item);
741
- const stat = fs.statSync(fullPath);
742
- if (stat.isDirectory()) {
743
- countFiles(fullPath);
744
- } else {
745
- fileCount++;
746
- }
747
- }
748
- }
749
- countFiles(outputDir);
750
-
751
- // Remove all contents of output directory
752
- fs.emptyDirSync(outputDir);
511
+ const fileCount = deleteAllOutputAssets(outputDir);
753
512
 
754
513
  res.json({ ok: true, deleted: fileCount });
755
514
  } catch (error) {
@@ -785,46 +544,11 @@ function attachApiRoutes(app, context) {
785
544
  }
786
545
 
787
546
  const outputDir = path.join(process.cwd(), ".reshot", "output");
788
-
789
- if (!fs.existsSync(outputDir)) {
790
- return res.json({ ok: true, deletedScenarios: 0, deletedFiles: 0 });
791
- }
792
-
793
- let deletedScenarios = 0;
794
- let deletedFiles = 0;
795
-
796
- for (const scenarioKey of scenarioKeys) {
797
- const scenarioDir = path.join(outputDir, scenarioKey);
798
-
799
- // Verify the path is within the output directory
800
- if (!isPathWithinBase(scenarioDir, outputDir)) {
801
- continue; // Skip paths that escape the output directory
802
- }
803
-
804
- if (fs.existsSync(scenarioDir)) {
805
- // Count files in this scenario directory
806
- function countFilesInDir(dir) {
807
- let count = 0;
808
- const items = fs.readdirSync(dir);
809
- for (const item of items) {
810
- const fullPath = path.join(dir, item);
811
- const stat = fs.statSync(fullPath);
812
- if (stat.isDirectory()) {
813
- count += countFilesInDir(fullPath);
814
- } else {
815
- count++;
816
- }
817
- }
818
- return count;
819
- }
820
-
821
- deletedFiles += countFilesInDir(scenarioDir);
822
-
823
- // Remove the scenario directory
824
- fs.removeSync(scenarioDir);
825
- deletedScenarios++;
826
- }
827
- }
547
+ const { deletedScenarios, deletedFiles } = deleteScenarioAssetDirectories(
548
+ outputDir,
549
+ scenarioKeys,
550
+ isPathWithinBase,
551
+ );
828
552
 
829
553
  res.json({ ok: true, deletedScenarios, deletedFiles });
830
554
  } catch (error) {
@@ -1943,156 +1667,7 @@ function attachApiRoutes(app, context) {
1943
1667
  return res.json({ versions: [] });
1944
1668
  }
1945
1669
 
1946
- const isTimestamp = (name) =>
1947
- /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/.test(name);
1948
- const extensions = [".png", ".gif", ".mp4", ".jpg", ".jpeg", ".webm"];
1949
-
1950
- // Helper to count assets in a directory
1951
- function countAssets(dir) {
1952
- let count = 0;
1953
- function walk(d) {
1954
- try {
1955
- const items = fs.readdirSync(d);
1956
- for (const item of items) {
1957
- const fullPath = path.join(d, item);
1958
- const stat = fs.statSync(fullPath);
1959
- if (stat.isDirectory()) {
1960
- walk(fullPath);
1961
- } else if (
1962
- extensions.includes(path.extname(item).toLowerCase())
1963
- ) {
1964
- count++;
1965
- }
1966
- }
1967
- } catch (e) {
1968
- /* ignore */
1969
- }
1970
- }
1971
- walk(dir);
1972
- return count;
1973
- }
1974
-
1975
- // Helper to detect variant subfolders in a directory
1976
- function detectVariants(dir) {
1977
- try {
1978
- const items = fs.readdirSync(dir);
1979
- const variants = [];
1980
- for (const item of items) {
1981
- const fullPath = path.join(dir, item);
1982
- const stat = fs.statSync(fullPath);
1983
- if (stat.isDirectory()) {
1984
- // Check if this folder contains assets (is a variant folder)
1985
- const assetCount = countAssets(fullPath);
1986
- if (assetCount > 0) {
1987
- variants.push({
1988
- name: item,
1989
- assetCount,
1990
- path: fullPath,
1991
- });
1992
- }
1993
- }
1994
- }
1995
- return variants;
1996
- } catch (e) {
1997
- return [];
1998
- }
1999
- }
2000
-
2001
- const subFolders = fs.readdirSync(scenarioDir).filter((item) => {
2002
- const fullPath = path.join(scenarioDir, item);
2003
- try {
2004
- return fs.statSync(fullPath).isDirectory();
2005
- } catch {
2006
- return false;
2007
- }
2008
- });
2009
-
2010
- // Get timestamp folders sorted newest first
2011
- const timestampFolders = subFolders
2012
- .filter((f) => isTimestamp(f))
2013
- .sort()
2014
- .reverse();
2015
-
2016
- // Also include 'latest' and 'default' folders if they exist and have assets
2017
- const specialFolders = subFolders.filter(
2018
- (f) => f === "latest" || f === "default",
2019
- );
2020
-
2021
- const versions = [];
2022
-
2023
- // Add timestamp versions first (newest first)
2024
- timestampFolders.forEach((ts, index) => {
2025
- const tsPath = path.join(scenarioDir, ts);
2026
- const totalAssetCount = countAssets(tsPath);
2027
-
2028
- // Detect variants in this timestamp folder
2029
- const variants = detectVariants(tsPath);
2030
-
2031
- // Parse timestamp to human-readable format
2032
- const parts = ts.match(
2033
- /(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})/,
2034
- );
2035
- let label = ts;
2036
- let isoDate = ts;
2037
- if (parts) {
2038
- const date = new Date(
2039
- `${parts[1]}-${parts[2]}-${parts[3]}T${parts[4]}:${parts[5]}:${parts[6]}`,
2040
- );
2041
- // Validate the date is valid before using
2042
- if (!isNaN(date.getTime())) {
2043
- label = date.toLocaleString();
2044
- isoDate = date.toISOString();
2045
- }
2046
- }
2047
-
2048
- // Read manifest for privacy/style metadata
2049
- let manifestMeta = {};
2050
- try {
2051
- const manifestPath = path.join(tsPath, "manifest.json");
2052
- if (fs.existsSync(manifestPath)) {
2053
- const manifest = fs.readJSONSync(manifestPath);
2054
- if (manifest.privacy) manifestMeta.privacy = manifest.privacy;
2055
- if (manifest.style) manifestMeta.style = manifest.style;
2056
- }
2057
- } catch (_e) { /* ignore */ }
2058
-
2059
- versions.push({
2060
- timestamp: ts,
2061
- label,
2062
- date: isoDate,
2063
- assetCount: totalAssetCount,
2064
- isLatest: index === 0,
2065
- variants: variants.map((v) => ({
2066
- name: v.name,
2067
- assetCount: v.assetCount,
2068
- })),
2069
- hasVariants: variants.length > 0,
2070
- ...manifestMeta,
2071
- });
2072
- });
2073
-
2074
- // Add special folders at the end (but only if they have assets and not duplicating timestamp versions)
2075
- specialFolders.forEach((folder) => {
2076
- const folderPath = path.join(scenarioDir, folder);
2077
- const assetCount = countAssets(folderPath);
2078
- const variants = detectVariants(folderPath);
2079
- if (assetCount > 0) {
2080
- // Only add if we don't already have timestamp versions (to avoid redundancy)
2081
- const label = folder === "latest" ? "Latest" : "Default";
2082
- versions.push({
2083
- timestamp: folder,
2084
- label,
2085
- date: new Date().toISOString(),
2086
- assetCount,
2087
- isLatest: folder === "latest" && versions.length === 0,
2088
- variants: variants.map((v) => ({
2089
- name: v.name,
2090
- assetCount: v.assetCount,
2091
- })),
2092
- hasVariants: variants.length > 0,
2093
- });
2094
- }
2095
- });
1670
+ const versions = listScenarioVersions(scenarioDir);
2096
1671
 
2097
1672
  res.json({ versions });
2098
1673
  } catch (error) {
@@ -2852,7 +2427,7 @@ function attachApiRoutes(app, context) {
2852
2427
  const apiBaseUrl = getApiBaseUrl();
2853
2428
  // Derive platformUrl from apiBaseUrl (remove /api suffix)
2854
2429
  const platformUrl =
2855
- apiBaseUrl.replace(/\/api\/?$/, "") || "http://localhost:3000";
2430
+ apiBaseUrl.replace(/\/api\/?$/, "") || "https://reshot.dev";
2856
2431
 
2857
2432
  config.writeSettings({
2858
2433
  projectId: project.id,
@@ -4298,375 +3873,7 @@ function attachApiRoutes(app, context) {
4298
3873
  }
4299
3874
  });
4300
3875
 
4301
- // ===== RECORDER ENDPOINTS =====
4302
- // These endpoints require the recorderService to be passed in context
4303
-
4304
- const { checkCdpEndpoint, getCdpTargets } = require("./record-cdp");
4305
-
4306
- /**
4307
- * GET /api/recorder/check-chrome
4308
- * Check if Chrome is running with remote debugging and get available tabs
4309
- */
4310
- app.get("/api/recorder/check-chrome", async (req, res, next) => {
4311
- try {
4312
- const endpointCheck = await checkCdpEndpoint("localhost", 9222);
4313
-
4314
- if (!endpointCheck.available) {
4315
- return res.json({
4316
- ok: false,
4317
- chromeAvailable: false,
4318
- error: endpointCheck.error,
4319
- instructions: {
4320
- darwin:
4321
- '/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.reshot/chrome-debug"',
4322
- win32:
4323
- '"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%USERPROFILE%\\.reshot\\chrome-debug"',
4324
- linux:
4325
- 'google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.reshot/chrome-debug"',
4326
- },
4327
- });
4328
- }
4329
-
4330
- // Get list of available tabs
4331
- let targets = [];
4332
- try {
4333
- targets = await getCdpTargets("localhost", 9222);
4334
- } catch (e) {
4335
- // Ignore, we'll just have empty targets
4336
- }
4337
-
4338
- const pageTargets = targets.filter((t) => t.type === "page");
4339
- const validTargets = pageTargets.filter(
4340
- (t) =>
4341
- !t.url.startsWith("chrome://") &&
4342
- !t.url.startsWith("chrome-error://") &&
4343
- t.url !== "about:blank",
4344
- );
4345
-
4346
- res.json({
4347
- ok: true,
4348
- chromeAvailable: true,
4349
- browserInfo: endpointCheck.info,
4350
- tabs: pageTargets.map((t) => ({
4351
- title: t.title,
4352
- url: t.url,
4353
- isValid:
4354
- !t.url.startsWith("chrome://") &&
4355
- !t.url.startsWith("chrome-error://") &&
4356
- t.url !== "about:blank",
4357
- })),
4358
- hasValidTab: validTargets.length > 0,
4359
- message:
4360
- validTargets.length > 0
4361
- ? `Chrome ready with ${validTargets.length} valid tab(s)`
4362
- : "Chrome is running but no valid tabs found. Please navigate to your application.",
4363
- });
4364
- } catch (error) {
4365
- res.json({
4366
- ok: false,
4367
- chromeAvailable: false,
4368
- error: error.message,
4369
- });
4370
- }
4371
- });
4372
-
4373
- /**
4374
- * GET /api/recorder/status
4375
- * Get current recorder session status
4376
- */
4377
- app.get("/api/recorder/status", async (req, res, next) => {
4378
- try {
4379
- const { recorderService } = context;
4380
- if (!recorderService) {
4381
- return res.json({
4382
- ok: true,
4383
- status: { active: false, error: "Recorder service not available" },
4384
- });
4385
- }
4386
-
4387
- const status = recorderService.getStatus();
4388
- res.json({ ok: true, status });
4389
- } catch (error) {
4390
- next(error);
4391
- }
4392
- });
4393
-
4394
- /**
4395
- * GET /api/recorder/steps
4396
- * Get captured steps from current or last session
4397
- */
4398
- app.get("/api/recorder/steps", async (req, res, next) => {
4399
- try {
4400
- const { recorderService } = context;
4401
- if (!recorderService) {
4402
- return res.json({ ok: true, steps: [] });
4403
- }
4404
-
4405
- const steps = recorderService.getSteps();
4406
- res.json({ ok: true, steps });
4407
- } catch (error) {
4408
- next(error);
4409
- }
4410
- });
4411
-
4412
- /**
4413
- * GET /api/recorder/tabs
4414
- * List available Chrome tabs for recording
4415
- */
4416
- app.get("/api/recorder/tabs", async (req, res, next) => {
4417
- try {
4418
- const { getCdpTargets, checkCdpEndpoint } = require("./record-cdp");
4419
-
4420
- // Check if Chrome is available
4421
- const endpointCheck = await checkCdpEndpoint("localhost", 9222);
4422
- if (!endpointCheck.available) {
4423
- return res.json({
4424
- ok: false,
4425
- chromeAvailable: false,
4426
- error: "Chrome is not running with remote debugging enabled",
4427
- tabs: [],
4428
- });
4429
- }
4430
-
4431
- // Get available tabs
4432
- const targets = await getCdpTargets("localhost", 9222);
4433
- const tabs = targets
4434
- .filter((t) => t.type === "page")
4435
- .map((t) => ({
4436
- id: t.id,
4437
- url: t.url,
4438
- title: t.title || t.url,
4439
- // Flag tabs that shouldn't be recorded
4440
- isOurUI:
4441
- t.url.includes("localhost:4300") ||
4442
- t.url.includes("127.0.0.1:4300"),
4443
- isChrome:
4444
- t.url.startsWith("chrome://") ||
4445
- t.url.startsWith("chrome-error://") ||
4446
- t.url === "about:blank",
4447
- }))
4448
- // Sort: real pages first, our UI last
4449
- .sort((a, b) => {
4450
- if (a.isOurUI && !b.isOurUI) return 1;
4451
- if (!a.isOurUI && b.isOurUI) return -1;
4452
- if (a.isChrome && !b.isChrome) return 1;
4453
- if (!a.isChrome && b.isChrome) return -1;
4454
- return 0;
4455
- });
4456
-
4457
- res.json({ ok: true, chromeAvailable: true, tabs });
4458
- } catch (error) {
4459
- console.error("[Recorder API] Get tabs failed:", error);
4460
- res
4461
- .status(500)
4462
- .json({ error: error.message || "Failed to get Chrome tabs" });
4463
- }
4464
- });
4465
-
4466
- /**
4467
- * POST /api/recorder/start
4468
- * Start a new recording session
4469
- */
4470
- app.post("/api/recorder/start", async (req, res, next) => {
4471
- try {
4472
- const { recorderService } = context;
4473
- if (!recorderService) {
4474
- return res
4475
- .status(503)
4476
- .json({ error: "Recorder service not available" });
4477
- }
4478
-
4479
- const { visualKey, title, targetUrl, targetId, scenarioUrl } = req.body;
4480
-
4481
- const result = await recorderService.start({
4482
- visualKey,
4483
- title,
4484
- targetUrl, // Specific URL to record
4485
- targetId, // Specific tab ID to record
4486
- scenarioUrl, // Custom URL to save with the scenario (defaults to targetUrl if not provided)
4487
- uiMode: true, // Important: Skip terminal prompts
4488
- });
4489
-
4490
- res.json({ ok: true, ...result });
4491
- } catch (error) {
4492
- console.error("[Recorder API] Start failed:", error);
4493
- res
4494
- .status(500)
4495
- .json({ error: error.message || "Failed to start recording" });
4496
- }
4497
- });
4498
-
4499
- /**
4500
- * POST /api/recorder/stop
4501
- * Stop the current recording session
4502
- */
4503
- app.post("/api/recorder/stop", async (req, res, next) => {
4504
- try {
4505
- const { recorderService } = context;
4506
- if (!recorderService) {
4507
- return res
4508
- .status(503)
4509
- .json({ error: "Recorder service not available" });
4510
- }
4511
-
4512
- const { save = true, mergeMode = "replace" } = req.body;
4513
-
4514
- const result = await recorderService.stop(save, {
4515
- uiMode: true,
4516
- mergeMode,
4517
- });
4518
-
4519
- res.json({ ok: true, ...result });
4520
- } catch (error) {
4521
- console.error("[Recorder API] Stop failed:", error);
4522
- res
4523
- .status(500)
4524
- .json({ error: error.message || "Failed to stop recording" });
4525
- }
4526
- });
4527
-
4528
- /**
4529
- * POST /api/recorder/capture
4530
- * Capture a screenshot during recording
4531
- */
4532
- app.post("/api/recorder/capture", async (req, res, next) => {
4533
- try {
4534
- const { recorderService } = context;
4535
- if (!recorderService) {
4536
- return res
4537
- .status(503)
4538
- .json({ error: "Recorder service not available" });
4539
- }
4540
-
4541
- const { outputFilename, areaType, selector } = req.body;
4542
-
4543
- const step = await recorderService.capture({
4544
- outputFilename,
4545
- areaType: areaType || "full",
4546
- selector,
4547
- uiMode: true,
4548
- });
4549
-
4550
- res.json({ ok: true, step });
4551
- } catch (error) {
4552
- console.error("[Recorder API] Capture failed:", error);
4553
- res
4554
- .status(500)
4555
- .json({ error: error.message || "Failed to capture screenshot" });
4556
- }
4557
- });
4558
-
4559
- /**
4560
- * DELETE /api/recorder/steps/:index
4561
- * Remove a step at a specific index during recording
4562
- */
4563
- app.delete("/api/recorder/steps/:index", async (req, res, next) => {
4564
- try {
4565
- const { recorderService } = context;
4566
- if (!recorderService) {
4567
- return res
4568
- .status(503)
4569
- .json({ error: "Recorder service not available" });
4570
- }
4571
-
4572
- const index = parseInt(req.params.index, 10);
4573
- if (isNaN(index)) {
4574
- return res.status(400).json({ error: "Invalid step index" });
4575
- }
4576
-
4577
- const result = recorderService.removeStep(index);
4578
- res.json({ ok: true, ...result });
4579
- } catch (error) {
4580
- console.error("[Recorder API] Remove step failed:", error);
4581
- res.status(500).json({ error: error.message || "Failed to remove step" });
4582
- }
4583
- });
4584
-
4585
- /**
4586
- * POST /api/recorder/save-session
4587
- * Save the current Chrome session state (cookies, localStorage) for use in captures
4588
- * This allows captures to run with authenticated sessions without manual login
4589
- */
4590
- app.post("/api/recorder/save-session", async (req, res, next) => {
4591
- try {
4592
- const {
4593
- saveSessionState,
4594
- getDefaultSessionPath,
4595
- } = require("./record-cdp");
4596
-
4597
- const sessionPath = getDefaultSessionPath();
4598
- const result = await saveSessionState(sessionPath);
4599
-
4600
- if (result.success) {
4601
- res.json({
4602
- ok: true,
4603
- path: result.path,
4604
- message:
4605
- "Session saved successfully. Captures will now use your authenticated session.",
4606
- });
4607
- } else {
4608
- res.status(400).json({
4609
- ok: false,
4610
- error: result.error,
4611
- });
4612
- }
4613
- } catch (error) {
4614
- console.error("[Recorder API] Save session failed:", error);
4615
- res
4616
- .status(500)
4617
- .json({ error: error.message || "Failed to save session" });
4618
- }
4619
- });
4620
-
4621
- /**
4622
- * GET /api/recorder/session-status
4623
- * Check if a saved session exists and is valid
4624
- */
4625
- app.get("/api/recorder/session-status", async (req, res, next) => {
4626
- try {
4627
- const { getDefaultSessionPath } = require("./record-cdp");
4628
- const sessionPath = getDefaultSessionPath();
4629
-
4630
- if (fs.existsSync(sessionPath)) {
4631
- const stat = fs.statSync(sessionPath);
4632
- const ageHours = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60);
4633
-
4634
- // Try to parse and get some info
4635
- try {
4636
- const sessionData = fs.readJsonSync(sessionPath);
4637
- res.json({
4638
- ok: true,
4639
- hasSession: true,
4640
- path: sessionPath,
4641
- savedAt: stat.mtime.toISOString(),
4642
- ageHours: Math.round(ageHours * 10) / 10,
4643
- cookieCount: sessionData.cookies?.length || 0,
4644
- originsCount: sessionData.origins?.length || 0,
4645
- isStale: ageHours > 24, // Consider stale after 24 hours
4646
- });
4647
- } catch (parseError) {
4648
- res.json({
4649
- ok: true,
4650
- hasSession: true,
4651
- path: sessionPath,
4652
- error: "Session file is corrupted",
4653
- });
4654
- }
4655
- } else {
4656
- res.json({
4657
- ok: true,
4658
- hasSession: false,
4659
- message:
4660
- "No saved session. Use 'Save Session' in Recorder to capture your authenticated state.",
4661
- });
4662
- }
4663
- } catch (error) {
4664
- console.error("[Recorder API] Session status failed:", error);
4665
- res
4666
- .status(500)
4667
- .json({ error: error.message || "Failed to check session status" });
4668
- }
4669
- });
3876
+ attachRecorderRoutes(app, context);
4670
3877
 
4671
3878
  // Error handler
4672
3879
  app.use(handleError);