@reshotdev/screenshot 0.0.1-beta.12 → 0.0.1-beta.14

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 (63) hide show
  1. package/README.md +67 -22
  2. package/package.json +18 -14
  3. package/src/commands/auth.js +37 -7
  4. package/src/commands/capture-dom.js +50 -0
  5. package/src/commands/compose.js +220 -0
  6. package/src/commands/doctor-release.js +7 -0
  7. package/src/commands/doctor-target.js +36 -4
  8. package/src/commands/drifts.js +13 -1
  9. package/src/commands/publish.js +183 -21
  10. package/src/commands/pull.js +9 -4
  11. package/src/commands/refresh.js +166 -0
  12. package/src/commands/setup-wizard.js +57 -3
  13. package/src/commands/status.js +22 -2
  14. package/src/commands/variation.js +194 -0
  15. package/src/index.js +190 -10
  16. package/src/lib/api-client.js +61 -35
  17. package/src/lib/auto-update/refresh.js +598 -0
  18. package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
  19. package/src/lib/auto-update/spec.js +89 -0
  20. package/src/lib/capture-engine.js +76 -2
  21. package/src/lib/capture-script-runner.js +289 -138
  22. package/src/lib/certification.js +23 -1
  23. package/src/lib/compose-context.js +156 -0
  24. package/src/lib/compose-pack.js +42 -0
  25. package/src/lib/compose-runtime.js +34 -0
  26. package/src/lib/compose-upload.js +142 -0
  27. package/src/lib/config.js +2 -2
  28. package/src/lib/dom-capture.js +64 -0
  29. package/src/lib/ensure-browser.js +147 -0
  30. package/src/lib/record-clip.js +83 -3
  31. package/src/lib/record-config.js +0 -4
  32. package/src/lib/release-doctor.js +11 -3
  33. package/src/lib/resolve-targets.js +60 -0
  34. package/src/lib/run-manifest.js +45 -0
  35. package/src/lib/ui-api-helpers.js +118 -0
  36. package/src/lib/ui-api.js +28 -820
  37. package/src/lib/ui-asset-cleanup.js +62 -0
  38. package/src/lib/ui-output-versions.js +165 -0
  39. package/src/lib/ui-recorder-routes.js +341 -0
  40. package/src/lib/ui-scenario-metadata.js +161 -0
  41. package/vendor/compose/dist/auto-update.cjs +5544 -0
  42. package/vendor/compose/dist/auto-update.mjs +5518 -0
  43. package/vendor/compose/dist/capture.cjs +1450 -0
  44. package/vendor/compose/dist/capture.mjs +1416 -0
  45. package/vendor/compose/dist/eligibility.cjs +5331 -0
  46. package/vendor/compose/dist/eligibility.mjs +5313 -0
  47. package/vendor/compose/dist/index.cjs +2046 -0
  48. package/vendor/compose/dist/index.mjs +1997 -0
  49. package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
  50. package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
  51. package/vendor/compose/dist/jsx-runtime.cjs +58 -0
  52. package/vendor/compose/dist/jsx-runtime.mjs +31 -0
  53. package/vendor/compose/dist/render.cjs +558 -0
  54. package/vendor/compose/dist/render.mjs +515 -0
  55. package/vendor/compose/dist/verify-cli.cjs +3806 -0
  56. package/vendor/compose/dist/verify-cli.mjs +3812 -0
  57. package/vendor/compose/dist/verify.cjs +3880 -0
  58. package/vendor/compose/dist/verify.mjs +3858 -0
  59. package/web/manager/dist/assets/{index-CvleJUur.js → index-D0S2otug.js} +56 -56
  60. package/web/manager/dist/index.html +1 -1
  61. package/src/commands/ingest.js +0 -458
  62. package/src/commands/setup.js +0 -165
  63. package/src/lib/playwright-runner.js +0 -252
@@ -0,0 +1,62 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ function countFilesRecursive(dir) {
5
+ let count = 0;
6
+ const items = fs.readdirSync(dir);
7
+
8
+ for (const item of items) {
9
+ const fullPath = path.join(dir, item);
10
+ const stat = fs.statSync(fullPath);
11
+
12
+ if (stat.isDirectory()) {
13
+ count += countFilesRecursive(fullPath);
14
+ } else {
15
+ count++;
16
+ }
17
+ }
18
+
19
+ return count;
20
+ }
21
+
22
+ function deleteAllOutputAssets(outputDir) {
23
+ if (!fs.existsSync(outputDir)) {
24
+ return 0;
25
+ }
26
+
27
+ const deletedFiles = countFilesRecursive(outputDir);
28
+ fs.emptyDirSync(outputDir);
29
+
30
+ return deletedFiles;
31
+ }
32
+
33
+ function deleteScenarioAssetDirectories(outputDir, scenarioKeys, isPathWithinBase) {
34
+ if (!fs.existsSync(outputDir)) {
35
+ return { deletedScenarios: 0, deletedFiles: 0 };
36
+ }
37
+
38
+ let deletedScenarios = 0;
39
+ let deletedFiles = 0;
40
+
41
+ for (const scenarioKey of scenarioKeys) {
42
+ const scenarioDir = path.join(outputDir, scenarioKey);
43
+
44
+ if (!isPathWithinBase(scenarioDir, outputDir)) {
45
+ continue;
46
+ }
47
+
48
+ if (fs.existsSync(scenarioDir)) {
49
+ deletedFiles += countFilesRecursive(scenarioDir);
50
+ fs.removeSync(scenarioDir);
51
+ deletedScenarios++;
52
+ }
53
+ }
54
+
55
+ return { deletedScenarios, deletedFiles };
56
+ }
57
+
58
+ module.exports = {
59
+ countFilesRecursive,
60
+ deleteAllOutputAssets,
61
+ deleteScenarioAssetDirectories,
62
+ };
@@ -0,0 +1,165 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+
4
+ const TIMESTAMP_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/;
5
+ const ASSET_EXTENSIONS = new Set([".png", ".gif", ".mp4", ".jpg", ".jpeg", ".webm"]);
6
+
7
+ function isTimestampFolder(name) {
8
+ return TIMESTAMP_DIR_PATTERN.test(name);
9
+ }
10
+
11
+ function countAssets(dir) {
12
+ let count = 0;
13
+
14
+ function walk(currentDir) {
15
+ try {
16
+ const items = fs.readdirSync(currentDir);
17
+ for (const item of items) {
18
+ const fullPath = path.join(currentDir, item);
19
+ const stat = fs.statSync(fullPath);
20
+
21
+ if (stat.isDirectory()) {
22
+ walk(fullPath);
23
+ } else if (ASSET_EXTENSIONS.has(path.extname(item).toLowerCase())) {
24
+ count++;
25
+ }
26
+ }
27
+ } catch {
28
+ return;
29
+ }
30
+ }
31
+
32
+ walk(dir);
33
+ return count;
34
+ }
35
+
36
+ function detectVariants(dir) {
37
+ try {
38
+ return fs
39
+ .readdirSync(dir)
40
+ .map((item) => {
41
+ const fullPath = path.join(dir, item);
42
+ const stat = fs.statSync(fullPath);
43
+
44
+ if (!stat.isDirectory()) {
45
+ return null;
46
+ }
47
+
48
+ const assetCount = countAssets(fullPath);
49
+ return assetCount > 0 ? { name: item, assetCount } : null;
50
+ })
51
+ .filter(Boolean);
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+
57
+ function formatTimestampFolder(timestamp) {
58
+ const parts = timestamp.match(
59
+ /(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})/,
60
+ );
61
+
62
+ if (!parts) {
63
+ return { label: timestamp, date: timestamp };
64
+ }
65
+
66
+ const date = new Date(
67
+ `${parts[1]}-${parts[2]}-${parts[3]}T${parts[4]}:${parts[5]}:${parts[6]}`,
68
+ );
69
+
70
+ if (Number.isNaN(date.getTime())) {
71
+ return { label: timestamp, date: timestamp };
72
+ }
73
+
74
+ return {
75
+ label: date.toLocaleString(),
76
+ date: date.toISOString(),
77
+ };
78
+ }
79
+
80
+ function readVersionManifestMetadata(versionDir) {
81
+ try {
82
+ const manifestPath = path.join(versionDir, "manifest.json");
83
+ if (!fs.existsSync(manifestPath)) {
84
+ return {};
85
+ }
86
+
87
+ const manifest = fs.readJSONSync(manifestPath);
88
+ const metadata = {};
89
+ if (manifest.privacy) metadata.privacy = manifest.privacy;
90
+ if (manifest.style) metadata.style = manifest.style;
91
+
92
+ return metadata;
93
+ } catch {
94
+ return {};
95
+ }
96
+ }
97
+
98
+ function listScenarioVersions(scenarioDir, now = new Date()) {
99
+ if (!fs.existsSync(scenarioDir)) {
100
+ return [];
101
+ }
102
+
103
+ const subFolders = fs.readdirSync(scenarioDir).filter((item) => {
104
+ const fullPath = path.join(scenarioDir, item);
105
+ try {
106
+ return fs.statSync(fullPath).isDirectory();
107
+ } catch {
108
+ return false;
109
+ }
110
+ });
111
+
112
+ const versions = subFolders
113
+ .filter(isTimestampFolder)
114
+ .sort()
115
+ .reverse()
116
+ .map((timestamp, index) => {
117
+ const versionDir = path.join(scenarioDir, timestamp);
118
+ const variants = detectVariants(versionDir);
119
+ const formatted = formatTimestampFolder(timestamp);
120
+
121
+ return {
122
+ timestamp,
123
+ label: formatted.label,
124
+ date: formatted.date,
125
+ assetCount: countAssets(versionDir),
126
+ isLatest: index === 0,
127
+ variants,
128
+ hasVariants: variants.length > 0,
129
+ ...readVersionManifestMetadata(versionDir),
130
+ };
131
+ });
132
+
133
+ const specialVersions = subFolders
134
+ .filter((folder) => folder === "latest" || folder === "default")
135
+ .map((folder) => {
136
+ const folderPath = path.join(scenarioDir, folder);
137
+ const assetCount = countAssets(folderPath);
138
+ const variants = detectVariants(folderPath);
139
+
140
+ if (assetCount === 0) {
141
+ return null;
142
+ }
143
+
144
+ return {
145
+ timestamp: folder,
146
+ label: folder === "latest" ? "Latest" : "Default",
147
+ date: now.toISOString(),
148
+ assetCount,
149
+ isLatest: folder === "latest" && versions.length === 0,
150
+ variants,
151
+ hasVariants: variants.length > 0,
152
+ };
153
+ })
154
+ .filter(Boolean);
155
+
156
+ return [...versions, ...specialVersions];
157
+ }
158
+
159
+ module.exports = {
160
+ countAssets,
161
+ detectVariants,
162
+ formatTimestampFolder,
163
+ isTimestampFolder,
164
+ listScenarioVersions,
165
+ };
@@ -0,0 +1,341 @@
1
+ const fs = require("fs-extra");
2
+
3
+ function isRecordableTarget(target) {
4
+ return (
5
+ target.type === "page" &&
6
+ !target.url.startsWith("chrome://") &&
7
+ !target.url.startsWith("chrome-error://") &&
8
+ target.url !== "about:blank"
9
+ );
10
+ }
11
+
12
+ function toRecorderTab(target) {
13
+ const isOurUI =
14
+ target.url.includes("localhost:4300") ||
15
+ target.url.includes("127.0.0.1:4300");
16
+ const isChrome =
17
+ target.url.startsWith("chrome://") ||
18
+ target.url.startsWith("chrome-error://") ||
19
+ target.url === "about:blank";
20
+
21
+ return {
22
+ id: target.id,
23
+ url: target.url,
24
+ title: target.title || target.url,
25
+ isOurUI,
26
+ isChrome,
27
+ };
28
+ }
29
+
30
+ function sortRecorderTabs(a, b) {
31
+ if (a.isOurUI && !b.isOurUI) return 1;
32
+ if (!a.isOurUI && b.isOurUI) return -1;
33
+ if (a.isChrome && !b.isChrome) return 1;
34
+ if (!a.isChrome && b.isChrome) return -1;
35
+ return 0;
36
+ }
37
+
38
+ function getChromeInstructions() {
39
+ return {
40
+ darwin:
41
+ '/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.reshot/chrome-debug"',
42
+ win32:
43
+ '"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%USERPROFILE%\\.reshot\\chrome-debug"',
44
+ linux:
45
+ 'google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.reshot/chrome-debug"',
46
+ };
47
+ }
48
+
49
+ function getRecorderServiceUnavailableResponse() {
50
+ return {
51
+ ok: true,
52
+ status: { active: false, error: "Recorder service not available" },
53
+ };
54
+ }
55
+
56
+ function attachRecorderRoutes(app, context, deps = {}) {
57
+ const recordCdp = deps.recordCdp || require("./record-cdp");
58
+ const fileSystem = deps.fs || fs;
59
+
60
+ app.get("/api/recorder/check-chrome", async (req, res) => {
61
+ try {
62
+ const endpointCheck = await recordCdp.checkCdpEndpoint("localhost", 9222);
63
+
64
+ if (!endpointCheck.available) {
65
+ return res.json({
66
+ ok: false,
67
+ chromeAvailable: false,
68
+ error: endpointCheck.error,
69
+ instructions: getChromeInstructions(),
70
+ });
71
+ }
72
+
73
+ let targets = [];
74
+ try {
75
+ targets = await recordCdp.getCdpTargets("localhost", 9222);
76
+ } catch (e) {
77
+ // Keep this endpoint helpful even when target enumeration fails.
78
+ }
79
+
80
+ const pageTargets = targets.filter((target) => target.type === "page");
81
+ const validTargets = pageTargets.filter(isRecordableTarget);
82
+
83
+ res.json({
84
+ ok: true,
85
+ chromeAvailable: true,
86
+ browserInfo: endpointCheck.info,
87
+ tabs: pageTargets.map((target) => ({
88
+ title: target.title,
89
+ url: target.url,
90
+ isValid: isRecordableTarget(target),
91
+ })),
92
+ hasValidTab: validTargets.length > 0,
93
+ message:
94
+ validTargets.length > 0
95
+ ? `Chrome ready with ${validTargets.length} valid tab(s)`
96
+ : "Chrome is running but no valid tabs found. Please navigate to your application.",
97
+ });
98
+ } catch (error) {
99
+ res.json({
100
+ ok: false,
101
+ chromeAvailable: false,
102
+ error: error.message,
103
+ });
104
+ }
105
+ });
106
+
107
+ app.get("/api/recorder/status", async (req, res, next) => {
108
+ try {
109
+ const { recorderService } = context;
110
+ if (!recorderService) {
111
+ return res.json(getRecorderServiceUnavailableResponse());
112
+ }
113
+
114
+ const status = recorderService.getStatus();
115
+ res.json({ ok: true, status });
116
+ } catch (error) {
117
+ next(error);
118
+ }
119
+ });
120
+
121
+ app.get("/api/recorder/steps", async (req, res, next) => {
122
+ try {
123
+ const { recorderService } = context;
124
+ if (!recorderService) {
125
+ return res.json({ ok: true, steps: [] });
126
+ }
127
+
128
+ const steps = recorderService.getSteps();
129
+ res.json({ ok: true, steps });
130
+ } catch (error) {
131
+ next(error);
132
+ }
133
+ });
134
+
135
+ app.get("/api/recorder/tabs", async (req, res) => {
136
+ try {
137
+ const endpointCheck = await recordCdp.checkCdpEndpoint("localhost", 9222);
138
+ if (!endpointCheck.available) {
139
+ return res.json({
140
+ ok: false,
141
+ chromeAvailable: false,
142
+ error: "Chrome is not running with remote debugging enabled",
143
+ tabs: [],
144
+ });
145
+ }
146
+
147
+ const targets = await recordCdp.getCdpTargets("localhost", 9222);
148
+ const tabs = targets
149
+ .filter((target) => target.type === "page")
150
+ .map(toRecorderTab)
151
+ .sort(sortRecorderTabs);
152
+
153
+ res.json({ ok: true, chromeAvailable: true, tabs });
154
+ } catch (error) {
155
+ console.error("[Recorder API] Get tabs failed:", error);
156
+ res
157
+ .status(500)
158
+ .json({ error: error.message || "Failed to get Chrome tabs" });
159
+ }
160
+ });
161
+
162
+ app.post("/api/recorder/start", async (req, res) => {
163
+ try {
164
+ const { recorderService } = context;
165
+ if (!recorderService) {
166
+ return res
167
+ .status(503)
168
+ .json({ error: "Recorder service not available" });
169
+ }
170
+
171
+ const { visualKey, title, targetUrl, targetId, scenarioUrl } = req.body;
172
+
173
+ const result = await recorderService.start({
174
+ visualKey,
175
+ title,
176
+ targetUrl,
177
+ targetId,
178
+ scenarioUrl,
179
+ uiMode: true,
180
+ });
181
+
182
+ res.json({ ok: true, ...result });
183
+ } catch (error) {
184
+ console.error("[Recorder API] Start failed:", error);
185
+ res
186
+ .status(500)
187
+ .json({ error: error.message || "Failed to start recording" });
188
+ }
189
+ });
190
+
191
+ app.post("/api/recorder/stop", async (req, res) => {
192
+ try {
193
+ const { recorderService } = context;
194
+ if (!recorderService) {
195
+ return res
196
+ .status(503)
197
+ .json({ error: "Recorder service not available" });
198
+ }
199
+
200
+ const { save = true, mergeMode = "replace" } = req.body;
201
+
202
+ const result = await recorderService.stop(save, {
203
+ uiMode: true,
204
+ mergeMode,
205
+ });
206
+
207
+ res.json({ ok: true, ...result });
208
+ } catch (error) {
209
+ console.error("[Recorder API] Stop failed:", error);
210
+ res
211
+ .status(500)
212
+ .json({ error: error.message || "Failed to stop recording" });
213
+ }
214
+ });
215
+
216
+ app.post("/api/recorder/capture", async (req, res) => {
217
+ try {
218
+ const { recorderService } = context;
219
+ if (!recorderService) {
220
+ return res
221
+ .status(503)
222
+ .json({ error: "Recorder service not available" });
223
+ }
224
+
225
+ const { outputFilename, areaType, selector } = req.body;
226
+
227
+ const step = await recorderService.capture({
228
+ outputFilename,
229
+ areaType: areaType || "full",
230
+ selector,
231
+ uiMode: true,
232
+ });
233
+
234
+ res.json({ ok: true, step });
235
+ } catch (error) {
236
+ console.error("[Recorder API] Capture failed:", error);
237
+ res
238
+ .status(500)
239
+ .json({ error: error.message || "Failed to capture screenshot" });
240
+ }
241
+ });
242
+
243
+ app.delete("/api/recorder/steps/:index", async (req, res) => {
244
+ try {
245
+ const { recorderService } = context;
246
+ if (!recorderService) {
247
+ return res
248
+ .status(503)
249
+ .json({ error: "Recorder service not available" });
250
+ }
251
+
252
+ const index = parseInt(req.params.index, 10);
253
+ if (isNaN(index)) {
254
+ return res.status(400).json({ error: "Invalid step index" });
255
+ }
256
+
257
+ const result = recorderService.removeStep(index);
258
+ res.json({ ok: true, ...result });
259
+ } catch (error) {
260
+ console.error("[Recorder API] Remove step failed:", error);
261
+ res.status(500).json({ error: error.message || "Failed to remove step" });
262
+ }
263
+ });
264
+
265
+ app.post("/api/recorder/save-session", async (req, res) => {
266
+ try {
267
+ const sessionPath = recordCdp.getDefaultSessionPath();
268
+ const result = await recordCdp.saveSessionState(sessionPath);
269
+
270
+ if (result.success) {
271
+ res.json({
272
+ ok: true,
273
+ path: result.path,
274
+ message:
275
+ "Session saved successfully. Captures will now use your authenticated session.",
276
+ });
277
+ } else {
278
+ res.status(400).json({
279
+ ok: false,
280
+ error: result.error,
281
+ });
282
+ }
283
+ } catch (error) {
284
+ console.error("[Recorder API] Save session failed:", error);
285
+ res
286
+ .status(500)
287
+ .json({ error: error.message || "Failed to save session" });
288
+ }
289
+ });
290
+
291
+ app.get("/api/recorder/session-status", async (req, res) => {
292
+ try {
293
+ const sessionPath = recordCdp.getDefaultSessionPath();
294
+
295
+ if (fileSystem.existsSync(sessionPath)) {
296
+ const stat = fileSystem.statSync(sessionPath);
297
+ const ageHours = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60);
298
+
299
+ try {
300
+ const sessionData = fileSystem.readJsonSync(sessionPath);
301
+ res.json({
302
+ ok: true,
303
+ hasSession: true,
304
+ path: sessionPath,
305
+ savedAt: stat.mtime.toISOString(),
306
+ ageHours: Math.round(ageHours * 10) / 10,
307
+ cookieCount: sessionData.cookies?.length || 0,
308
+ originsCount: sessionData.origins?.length || 0,
309
+ isStale: ageHours > 24,
310
+ });
311
+ } catch (parseError) {
312
+ res.json({
313
+ ok: true,
314
+ hasSession: true,
315
+ path: sessionPath,
316
+ error: "Session file is corrupted",
317
+ });
318
+ }
319
+ } else {
320
+ res.json({
321
+ ok: true,
322
+ hasSession: false,
323
+ message:
324
+ "No saved session. Use 'Save Session' in Recorder to capture your authenticated state.",
325
+ });
326
+ }
327
+ } catch (error) {
328
+ console.error("[Recorder API] Session status failed:", error);
329
+ res
330
+ .status(500)
331
+ .json({ error: error.message || "Failed to check session status" });
332
+ }
333
+ });
334
+ }
335
+
336
+ module.exports = {
337
+ attachRecorderRoutes,
338
+ isRecordableTarget,
339
+ sortRecorderTabs,
340
+ toRecorderTab,
341
+ };