@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,259 @@
1
+ const axios = require("axios");
2
+ const chalk = require("chalk");
3
+ const http = require("http");
4
+ // `open` and `ora` are ESM-only in the versions we use; when required from CommonJS
5
+ // their callable export is exposed on the `default` property.
6
+ const openModule = require("open");
7
+ const open = openModule.default || openModule;
8
+ const oraModule = require("ora");
9
+ const ora = oraModule.default || oraModule;
10
+
11
+ const { writeSettings, SETTINGS_PATH, SETTINGS_DIR } = require("../lib/config");
12
+ const { getApiBaseUrl } = require("../lib/api-client");
13
+ const pkg = require("../../package.json");
14
+
15
+ const DEFAULT_CALLBACK_PORT = 3721;
16
+ const POLL_INTERVAL_MS = 2000;
17
+
18
+ const unwrapResponse = (payload) => {
19
+ if (!payload) {
20
+ return {};
21
+ }
22
+ if (typeof payload === "object" && "data" in payload) {
23
+ return payload.data;
24
+ }
25
+ return payload;
26
+ };
27
+
28
+ function wait(ms) {
29
+ return new Promise((resolve) => setTimeout(resolve, ms));
30
+ }
31
+
32
+ function startLocalStatusServer(requestedPort, options = {}) {
33
+ const { explicit = false } = options;
34
+
35
+ return new Promise((resolve, reject) => {
36
+ const server = http.createServer((req, res) => {
37
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
38
+ res.end(
39
+ `<!doctype html>
40
+ <html lang="en">
41
+ <head>
42
+ <meta charset="UTF-8" />
43
+ <title>Reshot CLI Authentication</title>
44
+ <style>
45
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; padding: 2rem; background: #0b1727; color: #f4f6fb; }
46
+ .card { max-width: 640px; margin: 0 auto; background: #111f33; border-radius: 16px; padding: 2rem; box-shadow: 0 20px 45px rgba(0,0,0,0.45); }
47
+ h1 { font-size: 1.5rem; margin-top: 0; }
48
+ p { line-height: 1.5; color: #d3d9e6; }
49
+ code { background: rgba(255,255,255,0.08); padding: 0.15rem 0.35rem; border-radius: 6px; }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <div class="card">
54
+ <h1>Waiting for CLI confirmation…</h1>
55
+ <p>Your browser session is connected to the Reshot CLI. You can close this tab once the CLI confirms that authentication is complete.</p>
56
+ <p>If you closed the terminal prompt, run <code>reshot auth</code> again to start a new session.</p>
57
+ </div>
58
+ </body>
59
+ </html>`
60
+ );
61
+ });
62
+
63
+ server.on("error", (error) => {
64
+ if (error.code === "EADDRINUSE") {
65
+ // If the user explicitly requested this port via env, surface a clear,
66
+ // actionable error. Otherwise, just bubble up the code so the caller
67
+ // can fall back to another port.
68
+ if (explicit) {
69
+ const err = new Error(
70
+ `Port ${requestedPort} is already in use. Set RESHOT_CLI_CALLBACK_PORT to a free port and retry.`
71
+ );
72
+ err.code = error.code;
73
+ return reject(err);
74
+ }
75
+
76
+ const err = new Error(error.message);
77
+ err.code = error.code;
78
+ return reject(err);
79
+ }
80
+
81
+ reject(error);
82
+ });
83
+
84
+ server.listen(requestedPort, () => {
85
+ const address = server.address();
86
+ const actualPort =
87
+ typeof address === "object" &&
88
+ address &&
89
+ typeof address.port === "number"
90
+ ? address.port
91
+ : requestedPort;
92
+
93
+ resolve({ server, port: actualPort });
94
+ });
95
+ });
96
+ }
97
+
98
+ async function waitForCompletion(apiBaseUrl, authToken, expiresAtIso) {
99
+ const expiresAt = expiresAtIso
100
+ ? Date.parse(expiresAtIso)
101
+ : Date.now() + 5 * 60 * 1000;
102
+ const statusSpinner = ora("Waiting for browser authentication…").start();
103
+
104
+ try {
105
+ while (Date.now() < expiresAt) {
106
+ const statusResponse = await axios.get(`${apiBaseUrl}/auth/cli/status`, {
107
+ params: { token: authToken },
108
+ });
109
+ const payload = unwrapResponse(statusResponse.data);
110
+ const status = payload.status;
111
+
112
+ if (status === "completed" && payload.project?.apiKey) {
113
+ statusSpinner.succeed("Browser authentication confirmed");
114
+ return payload;
115
+ }
116
+
117
+ if (status === "expired") {
118
+ throw new Error(
119
+ "Authentication token expired. Run `reshot auth` again."
120
+ );
121
+ }
122
+
123
+ if (status === "invalid") {
124
+ throw new Error("Authentication session invalid. Start a new session.");
125
+ }
126
+
127
+ await wait(POLL_INTERVAL_MS);
128
+ }
129
+
130
+ throw new Error("Authentication timed out before completion.");
131
+ } catch (error) {
132
+ statusSpinner.fail("Browser authentication failed");
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ async function verifyApiKey(apiBaseUrl, apiKey) {
138
+ await axios.get(`${apiBaseUrl}/auth/cli/verify`, {
139
+ headers: {
140
+ Authorization: `Bearer ${apiKey}`,
141
+ },
142
+ });
143
+ }
144
+
145
+ async function authCommand() {
146
+ const apiBaseUrl = getApiBaseUrl();
147
+ const explicitPortEnv =
148
+ process.env.RESHOT_CLI_CALLBACK_PORT ||
149
+ process.env.DOCSYNC_CLI_CALLBACK_PORT ||
150
+ "";
151
+ const basePort = parseInt(explicitPortEnv || `${DEFAULT_CALLBACK_PORT}`, 10);
152
+ const hasExplicitPort = Boolean(explicitPortEnv);
153
+
154
+ let localServer;
155
+ let callbackPort;
156
+ const spinner = ora("Requesting authentication session…").start();
157
+
158
+ try {
159
+ if (hasExplicitPort) {
160
+ // Respect an explicitly configured port and fail fast with a clear error
161
+ // if it is not available.
162
+ const { server, port } = await startLocalStatusServer(basePort, {
163
+ explicit: true,
164
+ });
165
+ localServer = server;
166
+ callbackPort = port;
167
+ } else {
168
+ // Default behaviour: try the default port first for a stable experience,
169
+ // but automatically fall back to any available port so users never have
170
+ // to think about port conflicts.
171
+ try {
172
+ const { server, port } = await startLocalStatusServer(basePort);
173
+ localServer = server;
174
+ callbackPort = port;
175
+ } catch (error) {
176
+ if (error && error.code === "EADDRINUSE") {
177
+ const { server, port } = await startLocalStatusServer(0);
178
+ localServer = server;
179
+ callbackPort = port;
180
+ console.log(
181
+ chalk.gray(
182
+ `Callback port ${basePort} is in use; using available port ${callbackPort} instead.`
183
+ )
184
+ );
185
+ } else {
186
+ throw error;
187
+ }
188
+ }
189
+ }
190
+
191
+ const initiateResponse = await axios.post(
192
+ `${apiBaseUrl}/auth/cli/initiate`,
193
+ {
194
+ callbackPort,
195
+ clientVersion: pkg.version,
196
+ },
197
+ { headers: { "Content-Type": "application/json" } }
198
+ );
199
+
200
+ const payload = unwrapResponse(initiateResponse.data);
201
+ const { authUrl, authToken, expiresAt } = payload;
202
+
203
+ if (!authUrl || !authToken) {
204
+ throw new Error("Authentication session did not return a URL or token.");
205
+ }
206
+
207
+ spinner.succeed("Authentication session created");
208
+ console.log(chalk.gray(`Token expires at ${expiresAt || "unknown time"}`));
209
+ console.log(chalk.gray(`Settings will be stored in ${SETTINGS_PATH}`));
210
+
211
+ await open(authUrl, { wait: false });
212
+ console.log(
213
+ chalk.blue(
214
+ "A browser window has been opened. Complete the flow to continue."
215
+ )
216
+ );
217
+
218
+ const status = await waitForCompletion(apiBaseUrl, authToken, expiresAt);
219
+ await verifyApiKey(apiBaseUrl, status.project.apiKey);
220
+
221
+ // Derive platformUrl from apiBaseUrl (remove /api suffix)
222
+ const platformUrl = apiBaseUrl.replace(/\/api\/?$/, '') || 'http://localhost:3000';
223
+
224
+ writeSettings({
225
+ projectId: status.project.id,
226
+ projectName: status.project.name,
227
+ apiKey: status.project.apiKey,
228
+ platformUrl: platformUrl,
229
+ workspace: status.project.workspace || null,
230
+ workspaceName: status.project.workspace?.name || null,
231
+ linkedAt: new Date().toISOString(),
232
+ cliVersion: pkg.version,
233
+ user: status.user
234
+ ? {
235
+ id: status.user.id,
236
+ email: status.user.email,
237
+ fullName: status.user.fullName,
238
+ }
239
+ : null,
240
+ settingsDir: SETTINGS_DIR,
241
+ });
242
+
243
+ console.log();
244
+ console.log(
245
+ chalk.green(
246
+ `āœ” Reshot CLI is now linked to ${
247
+ status.project.workspace?.name || "your workspace"
248
+ } / ${status.project.name}`
249
+ )
250
+ );
251
+ console.log(chalk.gray(`Settings saved to ${SETTINGS_PATH}`));
252
+ } finally {
253
+ if (localServer) {
254
+ localServer.close();
255
+ }
256
+ }
257
+ }
258
+
259
+ module.exports = authCommand;
@@ -0,0 +1,140 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+
7
+ const DEFAULT_PORT = process.env.RESHOT_CHROME_PORT || '9222';
8
+ const DEFAULT_URL = 'about:blank';
9
+
10
+ const MAC_CANDIDATES = [
11
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
12
+ '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome',
13
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
14
+ ];
15
+
16
+ const WIN_CANDIDATES = [
17
+ path.join(process.env['PROGRAMFILES'] || 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
18
+ path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe')
19
+ ];
20
+
21
+ const LINUX_CANDIDATES = [
22
+ 'google-chrome-stable',
23
+ 'google-chrome',
24
+ 'chromium-browser',
25
+ 'chromium'
26
+ ];
27
+
28
+ function getCandidates() {
29
+ const platform = process.platform;
30
+
31
+ if (platform === 'darwin') {
32
+ return MAC_CANDIDATES;
33
+ }
34
+
35
+ if (platform === 'win32') {
36
+ return WIN_CANDIDATES;
37
+ }
38
+
39
+ return LINUX_CANDIDATES;
40
+ }
41
+
42
+ function buildManualCommand(port) {
43
+ if (process.platform === 'darwin') {
44
+ return '/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=' + port;
45
+ }
46
+
47
+ if (process.platform === 'win32') {
48
+ return '"C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe" --remote-debugging-port=' + port;
49
+ }
50
+
51
+ return `google-chrome --remote-debugging-port=${port}`;
52
+ }
53
+
54
+ function spawnChrome(executable, args) {
55
+ return new Promise((resolve) => {
56
+ let done = false;
57
+ try {
58
+ const child = spawn(executable, args, {
59
+ detached: true,
60
+ stdio: 'ignore',
61
+ windowsHide: true
62
+ });
63
+
64
+ child.once('error', (error) => {
65
+ if (done) return;
66
+ done = true;
67
+ resolve({ success: false, error });
68
+ });
69
+
70
+ child.unref();
71
+ setTimeout(() => {
72
+ if (!done) {
73
+ done = true;
74
+ resolve({ success: true });
75
+ }
76
+ }, 300);
77
+ } catch (error) {
78
+ resolve({ success: false, error });
79
+ }
80
+ });
81
+ }
82
+
83
+ async function chromeCommand(options = {}) {
84
+ const port = options.port || DEFAULT_PORT;
85
+ const targetUrl = options.url || DEFAULT_URL;
86
+ const homeDir = os.homedir();
87
+ const profileDir = path.join(homeDir, '.reshot', 'chrome-profile');
88
+
89
+ await fs.ensureDir(profileDir);
90
+
91
+ const chromeArgs = [
92
+ `--remote-debugging-port=${port}`,
93
+ '--remote-debugging-address=127.0.0.1',
94
+ '--no-first-run',
95
+ '--no-default-browser-check',
96
+ '--disable-popup-blocking',
97
+ '--disable-background-networking',
98
+ '--disable-component-extensions-with-background-pages',
99
+ `--user-data-dir=${profileDir}`
100
+ ];
101
+
102
+ if (targetUrl) {
103
+ chromeArgs.push(targetUrl);
104
+ }
105
+
106
+ const candidates = getCandidates();
107
+ const errors = [];
108
+
109
+ for (const executable of candidates) {
110
+ const isAbsolutePath = executable.includes('/') || executable.includes('\\');
111
+ if (isAbsolutePath && !fs.existsSync(executable)) {
112
+ continue;
113
+ }
114
+
115
+ // eslint-disable-next-line no-await-in-loop
116
+ const result = await spawnChrome(executable, chromeArgs);
117
+ if (result.success) {
118
+ console.log(chalk.green(`āœ” Chrome launched with remote debugging on port ${port}`));
119
+ console.log(chalk.gray('Keep this Chrome window open while you run `reshot record`.'));
120
+ return;
121
+ }
122
+
123
+ errors.push({ executable, error: result.error });
124
+ }
125
+
126
+ console.error(chalk.red('āœ– Unable to launch Chrome automatically.'));
127
+ if (errors.length > 0) {
128
+ errors.forEach(({ executable, error }) => {
129
+ console.error(chalk.gray(` • ${executable}: ${error?.message || 'not found'}`));
130
+ });
131
+ }
132
+
133
+ console.log('');
134
+ console.log(chalk.yellow('You can launch Chrome manually with:'));
135
+ console.log(chalk.cyan(buildManualCommand(port)));
136
+ console.log('');
137
+ console.log(chalk.gray('After Chrome is running, continue with `reshot record`.'));
138
+ }
139
+
140
+ module.exports = chromeCommand;
@@ -0,0 +1,123 @@
1
+ // ci-run.js - Composite command: run + publish in one step (CI-optimized)
2
+ const chalk = require("chalk");
3
+ const fs = require("fs-extra");
4
+ const path = require("path");
5
+ const { detectCI, getCIMetadata } = require("../lib/ci-detect");
6
+
7
+ async function ciRunCommand(options = {}) {
8
+ const { config: configPath, tag, message, dryRun, publish: shouldPublish = true } = options;
9
+
10
+ // Disable colors in CI for cleaner logs
11
+ const { isCI, provider } = detectCI();
12
+ if (isCI) {
13
+ process.env.NO_COLOR = "1";
14
+ }
15
+
16
+ const ciMeta = getCIMetadata();
17
+
18
+ console.log(chalk.cyan("šŸ¤– Reshot CI Run\n"));
19
+ if (isCI) {
20
+ console.log(chalk.gray(` Provider: ${provider || "unknown"}`));
21
+ if (ciMeta.commitSha) {
22
+ console.log(chalk.gray(` Commit: ${ciMeta.commitSha.slice(0, 8)}`));
23
+ }
24
+ if (ciMeta.branch) {
25
+ console.log(chalk.gray(` Branch: ${ciMeta.branch}`));
26
+ }
27
+ console.log();
28
+ }
29
+
30
+ // Override config path if specified
31
+ if (configPath) {
32
+ process.env.RESHOT_CONFIG_PATH = configPath;
33
+ }
34
+
35
+ const ciResult = {
36
+ run: { success: false, scenariosRun: 0, scenariosFailed: 0 },
37
+ publish: null,
38
+ ci: ciMeta,
39
+ timestamp: new Date().toISOString(),
40
+ };
41
+
42
+ // Step 1: Run capture scenarios
43
+ console.log(chalk.cyan("━━━ Step 1: Capture ━━━\n"));
44
+ try {
45
+ const runCommand = require("./run");
46
+ const runResult = await runCommand({
47
+ headless: true,
48
+ noExit: true,
49
+ });
50
+
51
+ ciResult.run.success = runResult?.success ?? false;
52
+ if (runResult?.results) {
53
+ ciResult.run.scenariosRun = runResult.results.length;
54
+ ciResult.run.scenariosFailed = runResult.results.filter((r) => !r.success).length;
55
+ }
56
+ } catch (error) {
57
+ console.error(chalk.red(`Capture failed: ${error.message}`));
58
+ ciResult.run.success = false;
59
+ ciResult.run.error = error.message;
60
+ }
61
+
62
+ // Step 2: Publish (unless --no-publish or capture failed)
63
+ if (shouldPublish && ciResult.run.success) {
64
+ console.log(chalk.cyan("\n━━━ Step 2: Publish ━━━\n"));
65
+ try {
66
+ const publishCommand = require("./publish");
67
+ const publishResult = await publishCommand({
68
+ tag,
69
+ message: message || (ciMeta.commitSha ? `CI publish (${ciMeta.commitSha.slice(0, 8)})` : "CI publish"),
70
+ dryRun,
71
+ force: true, // Skip prompts in CI
72
+ outputJson: true,
73
+ });
74
+
75
+ ciResult.publish = {
76
+ success: (publishResult?.assetsFailed || 0) === 0,
77
+ assetsProcessed: publishResult?.assetsProcessed || 0,
78
+ assetsFailed: publishResult?.assetsFailed || 0,
79
+ assetsSkipped: publishResult?.assetsSkipped || 0,
80
+ viewUrl: publishResult?.viewUrl || null,
81
+ };
82
+ } catch (error) {
83
+ console.error(chalk.red(`Publish failed: ${error.message}`));
84
+ ciResult.publish = { success: false, error: error.message };
85
+ }
86
+ } else if (!shouldPublish) {
87
+ console.log(chalk.gray("\n Skipping publish (--no-publish)"));
88
+ } else if (!ciResult.run.success) {
89
+ console.log(chalk.yellow("\n Skipping publish (capture failed)"));
90
+ }
91
+
92
+ // Write composite result
93
+ const outputDir = path.join(process.cwd(), ".reshot", "output");
94
+ fs.ensureDirSync(outputDir);
95
+ const resultPath = path.join(outputDir, "ci-result.json");
96
+ fs.writeJsonSync(resultPath, ciResult, { spaces: 2 });
97
+ console.log(chalk.gray(`\nšŸ“„ CI result written to: ${resultPath}`));
98
+
99
+ // Summary
100
+ console.log(chalk.cyan("\n━━━ CI Summary ━━━\n"));
101
+ const runIcon = ciResult.run.success ? chalk.green("āœ”") : chalk.red("āœ–");
102
+ console.log(` ${runIcon} Capture: ${ciResult.run.scenariosRun} scenario(s), ${ciResult.run.scenariosFailed} failed`);
103
+
104
+ if (ciResult.publish) {
105
+ const pubIcon = ciResult.publish.success ? chalk.green("āœ”") : chalk.red("āœ–");
106
+ console.log(` ${pubIcon} Publish: ${ciResult.publish.assetsProcessed || 0} asset(s)`);
107
+ if (ciResult.publish.viewUrl) {
108
+ console.log(chalk.cyan(` šŸ”— ${ciResult.publish.viewUrl}`));
109
+ }
110
+ }
111
+
112
+ console.log();
113
+
114
+ // Exit with appropriate code
115
+ const overallSuccess = ciResult.run.success && (!ciResult.publish || ciResult.publish.success);
116
+ if (!overallSuccess) {
117
+ process.exitCode = 1;
118
+ }
119
+
120
+ return ciResult;
121
+ }
122
+
123
+ module.exports = ciRunCommand;