@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,248 @@
1
+ // ui.js - Launch Reshot management web UI
2
+ const express = require("express");
3
+ const cors = require("cors");
4
+ const path = require("path");
5
+ const chalk = require("chalk");
6
+ const { spawn } = require("child_process");
7
+ const fs = require("fs-extra");
8
+ const http = require("http");
9
+ const { Server: SocketIOServer } = require("socket.io");
10
+ // `open` is ESM-only in the version we use; when required from CommonJS
11
+ // its callable export is exposed on the `default` property.
12
+ const openModule = require("open");
13
+ const open = openModule.default || openModule;
14
+ const getPortModule = require("get-port");
15
+ const getPort = getPortModule.default || getPortModule;
16
+ const { readSettings, readConfig, configExists } = require("../lib/config");
17
+ const { attachApiRoutes } = require("../lib/ui-api");
18
+ const RecorderService = require("../lib/recorder-service");
19
+
20
+ /**
21
+ * Build the frontend if it doesn't exist
22
+ */
23
+ async function ensureFrontendBuilt(managerDir, staticDir) {
24
+ const indexHtmlPath = path.join(staticDir, "index.html");
25
+
26
+ if (fs.existsSync(indexHtmlPath)) {
27
+ return true;
28
+ }
29
+
30
+ console.log(chalk.cyan("\n📦 Building Reshot Studio UI (web/manager)...\n"));
31
+
32
+ // Check if node_modules exists, if not run npm install
33
+ const nodeModulesPath = path.join(managerDir, "node_modules");
34
+ if (!fs.existsSync(nodeModulesPath)) {
35
+ console.log(chalk.gray("Installing dependencies..."));
36
+ await runCommand("npm", ["install"], { cwd: managerDir, stdio: "inherit" });
37
+ }
38
+
39
+ // Run build
40
+ console.log(chalk.gray("Building frontend..."));
41
+ const buildSuccess = await runCommand("npm", ["run", "build"], {
42
+ cwd: managerDir,
43
+ stdio: "inherit",
44
+ });
45
+
46
+ if (!buildSuccess || !fs.existsSync(indexHtmlPath)) {
47
+ console.error(
48
+ chalk.red("\n❌ Failed to build frontend."),
49
+ "\nYou can manually build it by running:",
50
+ chalk.cyan(" npm run ui:build"),
51
+ "\nOr from the web/manager directory:",
52
+ chalk.cyan(" cd web/manager && npm install && npm run build\n")
53
+ );
54
+ process.exit(1);
55
+ }
56
+
57
+ console.log(chalk.green("✔ Frontend built successfully\n"));
58
+ return true;
59
+ }
60
+
61
+ /**
62
+ * Run a command and wait for it to complete
63
+ */
64
+ function runCommand(command, args, options = {}) {
65
+ return new Promise((resolve) => {
66
+ const child = spawn(command, args, {
67
+ ...options,
68
+ shell: process.platform === "win32",
69
+ });
70
+
71
+ child.on("close", (code) => {
72
+ resolve(code === 0);
73
+ });
74
+
75
+ child.on("error", (error) => {
76
+ console.error(chalk.red(`Failed to run ${command}:`), error.message);
77
+ resolve(false);
78
+ });
79
+ });
80
+ }
81
+
82
+ module.exports = async function uiCommand(options = {}) {
83
+ const requestedPort = parseInt(options.port || "4300", 10);
84
+ const host = options.host || "127.0.0.1";
85
+ const shouldOpen = options.open !== false; // Default to true, allow --no-open flag
86
+
87
+ // Find an available port, preferring the requested port
88
+ const port = await getPort({ port: requestedPort });
89
+
90
+ if (port !== requestedPort) {
91
+ console.log(
92
+ chalk.yellow(
93
+ `⚠ Port ${requestedPort} is in use. Using port ${port} instead.`
94
+ )
95
+ );
96
+ }
97
+
98
+ // 1) Try to read settings, but allow degraded mode
99
+ let settings = null;
100
+ let isAuthenticated = false;
101
+ try {
102
+ settings = readSettings();
103
+ isAuthenticated = !!(settings?.apiKey && settings?.projectId);
104
+ } catch (error) {
105
+ console.warn(
106
+ chalk.yellow("⚠ Warning:"),
107
+ "No CLI settings found. Some features may be limited."
108
+ );
109
+ console.warn(
110
+ chalk.gray(
111
+ " Run `reshot auth` to authenticate and unlock full functionality.\n"
112
+ )
113
+ );
114
+ }
115
+
116
+ // 2) Check if config exists, but allow UI to start anyway (it can pull from platform)
117
+ let config = null;
118
+ if (configExists()) {
119
+ try {
120
+ config = readConfig();
121
+ } catch (error) {
122
+ console.warn(
123
+ chalk.yellow("Warning:"),
124
+ "Config file exists but is invalid. UI will allow you to fix it or pull from platform."
125
+ );
126
+ }
127
+ } else {
128
+ console.log(
129
+ chalk.yellow("Info:"),
130
+ "No docsync.config.json found. Use the UI to pull config from platform or create a new one."
131
+ );
132
+ }
133
+
134
+ // 3) Ensure frontend is built
135
+ const managerDir = path.join(__dirname, "..", "..", "web", "manager");
136
+ const staticDir = path.join(managerDir, "dist");
137
+
138
+ try {
139
+ await ensureFrontendBuilt(managerDir, staticDir);
140
+ } catch (error) {
141
+ console.error(chalk.red("Failed to build frontend:"), error.message);
142
+ process.exit(1);
143
+ }
144
+
145
+ const app = express();
146
+ app.use(cors());
147
+ app.use(express.json({ limit: "2mb" }));
148
+
149
+ // Create HTTP server and Socket.io instance
150
+ const httpServer = http.createServer(app);
151
+ const io = new SocketIOServer(httpServer, {
152
+ cors: {
153
+ origin: "*",
154
+ methods: ["GET", "POST"],
155
+ },
156
+ });
157
+
158
+ // Create RecorderService instance with Socket.io
159
+ const recorderService = new RecorderService({
160
+ io,
161
+ logger: console.log,
162
+ });
163
+
164
+ // 4) Attach JSON API routes (pass settings, io, and recorderService)
165
+ attachApiRoutes(app, { settings, io, recorderService });
166
+
167
+ // Socket.io connection handling
168
+ io.on("connection", (socket) => {
169
+ console.log(chalk.gray("[Socket.io] Client connected"));
170
+
171
+ // Send current recorder status on connect
172
+ socket.emit("recorder:status", recorderService.getStatus());
173
+ socket.emit("recorder:steps", { steps: recorderService.getSteps() });
174
+
175
+ socket.on("disconnect", () => {
176
+ console.log(chalk.gray("[Socket.io] Client disconnected"));
177
+ });
178
+ });
179
+
180
+ // 5) Serve static frontend
181
+ app.use(express.static(staticDir));
182
+ app.get("*", (req, res) => {
183
+ res.sendFile(path.join(staticDir, "index.html"));
184
+ });
185
+
186
+ httpServer.listen(port, host, () => {
187
+ const url = `http://${host}:${port}`;
188
+ console.log(chalk.cyan("\n✨ Reshot Studio is running"));
189
+ console.log(chalk.gray(` Local studio website: ${chalk.underline(url)}`));
190
+ console.log(chalk.gray(` Recording controls: Available via Socket.io`));
191
+ console.log(chalk.gray(` Press Ctrl+C to stop the server\n`));
192
+
193
+ // Auto-open browser unless --no-open flag is set
194
+ if (shouldOpen) {
195
+ setTimeout(() => {
196
+ open(url).catch((err) => {
197
+ // Silently fail if browser can't be opened
198
+ console.warn(
199
+ chalk.yellow("Could not auto-open browser. Please open manually:"),
200
+ url
201
+ );
202
+ });
203
+ }, 500);
204
+ }
205
+ });
206
+
207
+ // Graceful shutdown helper
208
+ let isShuttingDown = false;
209
+ async function shutdown(signal) {
210
+ if (isShuttingDown) return;
211
+ isShuttingDown = true;
212
+
213
+ console.log(chalk.gray(`\n\nShutting down Reshot Studio (${signal})...`));
214
+
215
+ // Force exit after 3 seconds if graceful shutdown hangs
216
+ const forceExitTimer = setTimeout(() => {
217
+ console.log(chalk.yellow("Force exiting..."));
218
+ process.exit(0);
219
+ }, 3000);
220
+
221
+ // Clean up recorder session if active
222
+ try {
223
+ await recorderService.forceCleanup();
224
+ } catch (e) {
225
+ // Ignore cleanup errors
226
+ }
227
+
228
+ // Close all socket.io connections first (this is key!)
229
+ try {
230
+ io.close();
231
+ } catch (e) {
232
+ // Ignore
233
+ }
234
+
235
+ // Now close the HTTP server
236
+ httpServer.close(() => {
237
+ clearTimeout(forceExitTimer);
238
+ console.log(chalk.green("✔ Server closed"));
239
+ process.exit(0);
240
+ });
241
+ }
242
+
243
+ process.on("SIGINT", () => shutdown("SIGINT"));
244
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
245
+
246
+ // Keep process alive
247
+ return new Promise(() => {});
248
+ };