@reshotdev/screenshot 0.0.1-beta.0

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 (59) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +388 -0
  3. package/package.json +64 -0
  4. package/src/commands/auth.js +259 -0
  5. package/src/commands/chrome.js +140 -0
  6. package/src/commands/ci-run.js +123 -0
  7. package/src/commands/ci-setup.js +288 -0
  8. package/src/commands/drifts.js +423 -0
  9. package/src/commands/import-tests.js +309 -0
  10. package/src/commands/ingest.js +458 -0
  11. package/src/commands/init.js +633 -0
  12. package/src/commands/publish.js +1721 -0
  13. package/src/commands/pull.js +303 -0
  14. package/src/commands/record.js +94 -0
  15. package/src/commands/run.js +476 -0
  16. package/src/commands/setup-wizard.js +740 -0
  17. package/src/commands/setup.js +137 -0
  18. package/src/commands/status.js +275 -0
  19. package/src/commands/sync.js +621 -0
  20. package/src/commands/ui.js +248 -0
  21. package/src/commands/validate-docs.js +529 -0
  22. package/src/index.js +462 -0
  23. package/src/lib/api-client.js +815 -0
  24. package/src/lib/capture-engine.js +1623 -0
  25. package/src/lib/capture-script-runner.js +3120 -0
  26. package/src/lib/ci-detect.js +137 -0
  27. package/src/lib/config.js +1240 -0
  28. package/src/lib/diff-engine.js +642 -0
  29. package/src/lib/hash.js +74 -0
  30. package/src/lib/image-crop.js +396 -0
  31. package/src/lib/matrix.js +89 -0
  32. package/src/lib/output-path-template.js +318 -0
  33. package/src/lib/playwright-runner.js +252 -0
  34. package/src/lib/polished-clip.js +553 -0
  35. package/src/lib/privacy-engine.js +408 -0
  36. package/src/lib/progress-tracker.js +142 -0
  37. package/src/lib/record-browser-injection.js +654 -0
  38. package/src/lib/record-cdp.js +612 -0
  39. package/src/lib/record-clip.js +343 -0
  40. package/src/lib/record-config.js +623 -0
  41. package/src/lib/record-screenshot.js +360 -0
  42. package/src/lib/record-terminal.js +123 -0
  43. package/src/lib/recorder-service.js +781 -0
  44. package/src/lib/secrets.js +51 -0
  45. package/src/lib/selector-strategies.js +859 -0
  46. package/src/lib/standalone-mode.js +400 -0
  47. package/src/lib/storage-providers.js +569 -0
  48. package/src/lib/style-engine.js +684 -0
  49. package/src/lib/ui-api.js +4677 -0
  50. package/src/lib/ui-assets.js +373 -0
  51. package/src/lib/ui-executor.js +587 -0
  52. package/src/lib/variant-injector.js +591 -0
  53. package/src/lib/viewport-presets.js +454 -0
  54. package/src/lib/worker-pool.js +118 -0
  55. package/web/cropper/index.html +436 -0
  56. package/web/manager/dist/assets/index--ZgioErz.js +507 -0
  57. package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
  58. package/web/manager/dist/index.html +27 -0
  59. package/web/subtitle-editor/index.html +295 -0
@@ -0,0 +1,303 @@
1
+ // pull.js - Pull asset map from Reshot platform
2
+ // Generates JSON, TypeScript, or CSV files for consumption in build pipelines
3
+ const chalk = require("chalk");
4
+ const fs = require("fs-extra");
5
+ const path = require("path");
6
+ const config = require("../lib/config");
7
+ const apiClient = require("../lib/api-client");
8
+
9
+ /**
10
+ * Convert a string to camelCase for TypeScript keys
11
+ */
12
+ function toCamelCase(str) {
13
+ return str
14
+ .toLowerCase()
15
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
16
+ .replace(/^[A-Z]/, (chr) => chr.toLowerCase());
17
+ }
18
+
19
+ /**
20
+ * Generate TypeScript code from the assets object
21
+ * This provides type safety - if a visual is deleted, the build will fail
22
+ */
23
+ function generateTypeScript(assets, includeMetadata = false) {
24
+ const lines = [
25
+ "// GENERATED BY RESHOT - DO NOT EDIT",
26
+ "// Run 'npx reshot pull' to regenerate this file",
27
+ "",
28
+ ];
29
+
30
+ if (includeMetadata) {
31
+ // Full export with all metadata (src, width, height, type, alt)
32
+ lines.push("export const ReshotVisuals = {");
33
+
34
+ for (const [group, visuals] of Object.entries(assets)) {
35
+ lines.push(` ${group}: {`);
36
+
37
+ for (const [visualKey, variants] of Object.entries(visuals)) {
38
+ lines.push(` ${visualKey}: {`);
39
+
40
+ for (const [variant, asset] of Object.entries(variants)) {
41
+ lines.push(` ${variant}: {`);
42
+ lines.push(` src: "${asset.src}",`);
43
+ lines.push(` type: "${asset.type}",`);
44
+ if (asset.width) lines.push(` width: ${asset.width},`);
45
+ if (asset.height) lines.push(` height: ${asset.height},`);
46
+ lines.push(` alt: "${asset.alt.replace(/"/g, '\\"')}",`);
47
+ if (asset.poster) lines.push(` poster: "${asset.poster}",`);
48
+ lines.push(` },`);
49
+ }
50
+
51
+ lines.push(` },`);
52
+ }
53
+
54
+ lines.push(` },`);
55
+ }
56
+
57
+ lines.push("} as const;");
58
+ lines.push("");
59
+ lines.push("export type ReshotVisualKey = keyof typeof ReshotVisuals;");
60
+ } else {
61
+ // Simplified export - just URLs for easy access
62
+ lines.push("export const ReshotVisuals = {");
63
+
64
+ for (const [group, visuals] of Object.entries(assets)) {
65
+ lines.push(` ${group}: {`);
66
+
67
+ for (const [visualKey, variants] of Object.entries(visuals)) {
68
+ // If there's only a default variant, flatten it
69
+ if (Object.keys(variants).length === 1 && variants.default) {
70
+ lines.push(` ${visualKey}: "${variants.default.src}",`);
71
+ } else {
72
+ lines.push(` ${visualKey}: {`);
73
+ for (const [variant, asset] of Object.entries(variants)) {
74
+ lines.push(` ${variant}: "${asset.src}",`);
75
+ }
76
+ lines.push(` },`);
77
+ }
78
+ }
79
+
80
+ lines.push(` },`);
81
+ }
82
+
83
+ lines.push("} as const;");
84
+ lines.push("");
85
+ lines.push("export type ReshotVisualKey = keyof typeof ReshotVisuals;");
86
+ }
87
+
88
+ return lines.join("\n");
89
+ }
90
+
91
+ /**
92
+ * Main pull command
93
+ * @param {Object} options - Command options
94
+ * @param {string} options.format - Output format: 'json', 'ts', 'csv'
95
+ * @param {string} options.output - Output file path
96
+ * @param {boolean} options.full - Include full metadata (for ts format)
97
+ * @param {string} options.status - Filter by status: 'approved', 'pending', 'all'
98
+ */
99
+ async function pullCommand(options = {}) {
100
+ const {
101
+ format = "json",
102
+ output = null,
103
+ full = false,
104
+ status = "approved",
105
+ } = options;
106
+
107
+ console.log(chalk.blue("⬇ Pulling asset map from Reshot...\n"));
108
+
109
+ // Load project configuration
110
+ let projectConfig;
111
+ try {
112
+ projectConfig = config.readConfig();
113
+ } catch (e) {
114
+ console.error(
115
+ chalk.red("Error: No docsync.config.json found. Run 'reshot init' first.")
116
+ );
117
+ process.exit(1);
118
+ }
119
+
120
+ const projectId = projectConfig._metadata?.projectId || projectConfig.projectId;
121
+ if (!projectId) {
122
+ console.error(
123
+ chalk.red("Error: No projectId found in docsync.config.json")
124
+ );
125
+ process.exit(1);
126
+ }
127
+
128
+ // Check authentication
129
+ let settings;
130
+ try {
131
+ settings = config.readSettings();
132
+ } catch (e) {
133
+ console.error(
134
+ chalk.red("Error: Not authenticated. Run 'reshot auth' first.")
135
+ );
136
+ process.exit(1);
137
+ }
138
+ if (!settings?.apiKey) {
139
+ console.error(
140
+ chalk.red("Error: Not authenticated. Run 'reshot auth' first.")
141
+ );
142
+ process.exit(1);
143
+ }
144
+
145
+ try {
146
+ // Fetch the asset map from the API
147
+ console.log(chalk.gray(` Fetching assets for project: ${projectId}`));
148
+ console.log(chalk.gray(` Status filter: ${status}`));
149
+
150
+ const response = await apiClient.exportVisuals(projectId, {
151
+ format: "json", // Always fetch JSON, we'll transform locally
152
+ status,
153
+ });
154
+
155
+ if (!response || !response.assets) {
156
+ console.error(chalk.red("Error: Invalid response from API"));
157
+ process.exit(1);
158
+ }
159
+
160
+ const { meta, assets } = response;
161
+ const assetCount = Object.values(assets).reduce(
162
+ (count, group) => count + Object.keys(group).length,
163
+ 0
164
+ );
165
+
166
+ console.log(chalk.green(` ✓ Fetched ${assetCount} visuals\n`));
167
+
168
+ // Determine output path
169
+ let outputPath = output;
170
+ if (!outputPath) {
171
+ // Default paths based on format
172
+ const defaultPaths = {
173
+ json: "src/data/reshot-assets.json",
174
+ ts: "src/data/visuals.ts",
175
+ csv: "reshot-assets.csv",
176
+ };
177
+ outputPath = defaultPaths[format] || "reshot-assets.json";
178
+ }
179
+
180
+ // Ensure the output directory exists
181
+ const outputDir = path.dirname(path.resolve(outputPath));
182
+ await fs.ensureDir(outputDir);
183
+
184
+ // Generate output based on format
185
+ let content;
186
+ let contentType;
187
+
188
+ switch (format) {
189
+ case "ts":
190
+ content = generateTypeScript(assets, full);
191
+ contentType = "TypeScript";
192
+ break;
193
+
194
+ case "csv":
195
+ // Generate CSV format
196
+ const headers = ["Key", "Group", "Name", "Context", "Type", "Unbreakable_URL", "Width", "Height"];
197
+ const rows = [headers.join(",")];
198
+
199
+ for (const [group, visuals] of Object.entries(assets)) {
200
+ for (const [visualKey, variants] of Object.entries(visuals)) {
201
+ for (const [variant, asset] of Object.entries(variants)) {
202
+ const type = asset.type.split("/")[0]; // 'image' or 'video'
203
+ rows.push(
204
+ [
205
+ `${group}/${visualKey}`,
206
+ group,
207
+ visualKey,
208
+ variant,
209
+ type,
210
+ asset.src,
211
+ asset.width || "",
212
+ asset.height || "",
213
+ ].join(",")
214
+ );
215
+ }
216
+ }
217
+ }
218
+
219
+ content = rows.join("\n");
220
+ contentType = "CSV";
221
+ break;
222
+
223
+ case "json":
224
+ default:
225
+ content = JSON.stringify({ meta, assets }, null, 2);
226
+ contentType = "JSON";
227
+ break;
228
+ }
229
+
230
+ // Write the output file
231
+ await fs.writeFile(path.resolve(outputPath), content, "utf-8");
232
+
233
+ console.log(chalk.green(`✓ Generated ${contentType} file: ${outputPath}`));
234
+ console.log(chalk.gray(` ${assetCount} visuals exported\n`));
235
+
236
+ // Show format-specific usage instructions
237
+ if (format === "ts") {
238
+ console.log(chalk.blue("━━━ TypeScript Usage ━━━\n"));
239
+ console.log(chalk.white("Import in your React/Next.js component:\n"));
240
+ console.log(chalk.cyan(' import { ReshotVisuals } from "./visuals";\n'));
241
+ console.log(chalk.white("Use with full autocomplete support:\n"));
242
+ console.log(chalk.cyan(" // Single context (flattened)"));
243
+ console.log(chalk.cyan(" <img src={ReshotVisuals.dashboard.mainView} />\n"));
244
+ console.log(chalk.cyan(" // Multiple contexts (dark mode, locales)"));
245
+ console.log(chalk.cyan(" <img src={ReshotVisuals.dashboard.mainView.default} />"));
246
+ console.log(chalk.cyan(" <img src={ReshotVisuals.dashboard.mainView.dark} />\n"));
247
+ if (!full) {
248
+ console.log(chalk.gray(" Tip: Use --full flag to include width, height, and alt text\n"));
249
+ }
250
+ } else if (format === "json") {
251
+ console.log(chalk.blue("━━━ JSON Usage ━━━\n"));
252
+ console.log(chalk.white("Import in JavaScript/TypeScript:\n"));
253
+ console.log(chalk.cyan(' import assets from "./reshot-assets.json";\n'));
254
+ console.log(chalk.white("Access assets with dot notation:\n"));
255
+ console.log(chalk.cyan(" <img"));
256
+ console.log(chalk.cyan(" src={assets.assets.dashboard.mainView.default.src}"));
257
+ console.log(chalk.cyan(" width={assets.assets.dashboard.mainView.default.width}"));
258
+ console.log(chalk.cyan(" height={assets.assets.dashboard.mainView.default.height}"));
259
+ console.log(chalk.cyan(" alt={assets.assets.dashboard.mainView.default.alt}"));
260
+ console.log(chalk.cyan(" />\n"));
261
+ } else if (format === "csv") {
262
+ console.log(chalk.blue("━━━ CSV Usage ━━━\n"));
263
+ console.log(chalk.white("Import into your CMS:\n"));
264
+ console.log(chalk.gray(" 1. Open Contentful / Webflow / WordPress"));
265
+ console.log(chalk.gray(" 2. Navigate to bulk import/media settings"));
266
+ console.log(chalk.gray(` 3. Upload ${outputPath}`));
267
+ console.log(chalk.gray(" 4. Map columns to your asset fields\n"));
268
+ console.log(chalk.white("CSV Columns:\n"));
269
+ console.log(chalk.gray(" • Key - Unique asset identifier"));
270
+ console.log(chalk.gray(" • Group - Scenario/folder grouping"));
271
+ console.log(chalk.gray(" • Name - Human-readable name"));
272
+ console.log(chalk.gray(" • Context - Variant (default, dark, fr-FR)"));
273
+ console.log(chalk.gray(" • Type - image or video"));
274
+ console.log(chalk.gray(" • Unbreakable_URL - CDN URL (never changes)"));
275
+ console.log(chalk.gray(" • Width/Height - Dimensions in pixels\n"));
276
+ }
277
+
278
+ // CI/CD integration tip
279
+ console.log(chalk.blue("━━━ CI/CD Integration ━━━\n"));
280
+ console.log(chalk.white("Add to your build pipeline:\n"));
281
+ console.log(chalk.cyan(" # package.json"));
282
+ console.log(chalk.cyan(' "scripts": {'));
283
+ console.log(chalk.cyan(` "prebuild": "npx reshot pull --format=${format}${output ? ` --output=${output}` : ''}"`));
284
+ console.log(chalk.cyan(' }\n'));
285
+ console.log(chalk.gray(" This ensures your build always uses the latest approved visuals."));
286
+ console.log(chalk.gray(" If a visual is deleted, your build will fail (preventing broken images).\n"));
287
+
288
+ } catch (error) {
289
+ if (config.isAuthError(error)) {
290
+ console.error(
291
+ chalk.red("Error: Authentication failed. Run 'reshot auth' to re-authenticate.")
292
+ );
293
+ } else {
294
+ console.error(chalk.red(`Error: ${error.message}`));
295
+ if (process.env.RESHOT_DEBUG) {
296
+ console.error(chalk.gray(error.stack));
297
+ }
298
+ }
299
+ process.exit(1);
300
+ }
301
+ }
302
+
303
+ module.exports = pullCommand;
@@ -0,0 +1,94 @@
1
+ // record.js - Interactive scenario recording via CDP
2
+ const chalk = require('chalk');
3
+ const { connectToActivePage } = require('../lib/record-cdp');
4
+ const { setupBrowserActionListener, updateBrowserMode } = require('../lib/record-browser-injection');
5
+ const { setupTerminalHotkeys, runEventLoop } = require('../lib/record-terminal');
6
+ const { startCaptureFlow } = require('../lib/record-screenshot');
7
+ const { startClipRecording } = require('../lib/record-clip');
8
+ const { showVisualSelectionMenu, finalizeScenarioAndWriteConfig } = require('../lib/record-config');
9
+
10
+ /**
11
+ * Main record command
12
+ * @param {string|undefined} title - Optional title for the visual (e.g., "Admin Dashboard")
13
+ */
14
+ async function recordCommand(title) {
15
+ console.log(chalk.cyan('🎬 Starting interactive recording session...\n'));
16
+
17
+ let cleanup = null;
18
+ let browser = null;
19
+
20
+ try {
21
+ // Step 1: Connect to browser via CDP
22
+ const { browser: connectedBrowser, page } = await connectToActivePage();
23
+ browser = connectedBrowser;
24
+
25
+ // Step 2: Visual selection and session initialization
26
+ const { visualKey, existingScenario } = await showVisualSelectionMenu(page, title);
27
+
28
+ const existingScenarioSnapshot = existingScenario
29
+ ? JSON.parse(JSON.stringify(existingScenario))
30
+ : null;
31
+
32
+ const sessionState = {
33
+ visualKey,
34
+ capturedSteps: [],
35
+ existingScenario: existingScenarioSnapshot,
36
+ savedStepCount: 0,
37
+ mode: 'normal',
38
+ phase: 'idle',
39
+ pendingCapture: null,
40
+ quit: false,
41
+ saveOnQuit: true,
42
+ clipEvents: null,
43
+ recordingStart: null,
44
+ stopClipRecording: false,
45
+ onChange: null,
46
+ onElementSelected: null
47
+ };
48
+
49
+ console.log(chalk.green(`\n✔ Visual: ${visualKey}\n`));
50
+
51
+ // Step 3: Inject browser action listener
52
+ await setupBrowserActionListener(page, sessionState);
53
+
54
+ // Step 4: Set up terminal hotkeys
55
+ cleanup = setupTerminalHotkeys(sessionState, async () => {
56
+ // Capture flow callback
57
+ try {
58
+ if (sessionState.mode === 'normal') {
59
+ await startCaptureFlow(sessionState, page);
60
+ }
61
+ } catch (error) {
62
+ console.error(chalk.red('Capture error:'), error.message);
63
+ }
64
+ });
65
+
66
+ // Step 5: Run main event loop
67
+ await runEventLoop(sessionState);
68
+
69
+ // Step 6: Finalize and save config
70
+ if (cleanup) cleanup();
71
+
72
+ await finalizeScenarioAndWriteConfig(sessionState, page);
73
+ sessionState.phase = 'finished';
74
+
75
+ // Close browser connection
76
+ await browser.close();
77
+
78
+ console.log(chalk.cyan('👋 Recording session ended.\n'));
79
+
80
+ } catch (error) {
81
+ if (cleanup) cleanup();
82
+ if (browser) {
83
+ try {
84
+ await browser.close();
85
+ } catch (e) {
86
+ // Ignore cleanup errors
87
+ }
88
+ }
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ module.exports = recordCommand;
94
+