@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.
- package/LICENSE +190 -0
- package/README.md +388 -0
- package/package.json +64 -0
- package/src/commands/auth.js +259 -0
- package/src/commands/chrome.js +140 -0
- package/src/commands/ci-run.js +123 -0
- package/src/commands/ci-setup.js +288 -0
- package/src/commands/drifts.js +423 -0
- package/src/commands/import-tests.js +309 -0
- package/src/commands/ingest.js +458 -0
- package/src/commands/init.js +633 -0
- package/src/commands/publish.js +1721 -0
- package/src/commands/pull.js +303 -0
- package/src/commands/record.js +94 -0
- package/src/commands/run.js +476 -0
- package/src/commands/setup-wizard.js +740 -0
- package/src/commands/setup.js +137 -0
- package/src/commands/status.js +275 -0
- package/src/commands/sync.js +621 -0
- package/src/commands/ui.js +248 -0
- package/src/commands/validate-docs.js +529 -0
- package/src/index.js +462 -0
- package/src/lib/api-client.js +815 -0
- package/src/lib/capture-engine.js +1623 -0
- package/src/lib/capture-script-runner.js +3120 -0
- package/src/lib/ci-detect.js +137 -0
- package/src/lib/config.js +1240 -0
- package/src/lib/diff-engine.js +642 -0
- package/src/lib/hash.js +74 -0
- package/src/lib/image-crop.js +396 -0
- package/src/lib/matrix.js +89 -0
- package/src/lib/output-path-template.js +318 -0
- package/src/lib/playwright-runner.js +252 -0
- package/src/lib/polished-clip.js +553 -0
- package/src/lib/privacy-engine.js +408 -0
- package/src/lib/progress-tracker.js +142 -0
- package/src/lib/record-browser-injection.js +654 -0
- package/src/lib/record-cdp.js +612 -0
- package/src/lib/record-clip.js +343 -0
- package/src/lib/record-config.js +623 -0
- package/src/lib/record-screenshot.js +360 -0
- package/src/lib/record-terminal.js +123 -0
- package/src/lib/recorder-service.js +781 -0
- package/src/lib/secrets.js +51 -0
- package/src/lib/selector-strategies.js +859 -0
- package/src/lib/standalone-mode.js +400 -0
- package/src/lib/storage-providers.js +569 -0
- package/src/lib/style-engine.js +684 -0
- package/src/lib/ui-api.js +4677 -0
- package/src/lib/ui-assets.js +373 -0
- package/src/lib/ui-executor.js +587 -0
- package/src/lib/variant-injector.js +591 -0
- package/src/lib/viewport-presets.js +454 -0
- package/src/lib/worker-pool.js +118 -0
- package/web/cropper/index.html +436 -0
- package/web/manager/dist/assets/index--ZgioErz.js +507 -0
- package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
- package/web/manager/dist/index.html +27 -0
- package/web/subtitle-editor/index.html +295 -0
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
// config.js - Configuration file helpers
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
// Import new modules for enhanced functionality
|
|
6
|
+
const {
|
|
7
|
+
validateTemplate,
|
|
8
|
+
getTemplatePresets,
|
|
9
|
+
TEMPLATE_PRESETS,
|
|
10
|
+
} = require("./output-path-template");
|
|
11
|
+
const {
|
|
12
|
+
validateViewport,
|
|
13
|
+
resolveViewport,
|
|
14
|
+
getAllViewportPresets,
|
|
15
|
+
getAllCropPresets,
|
|
16
|
+
VIEWPORT_PRESETS,
|
|
17
|
+
} = require("./viewport-presets");
|
|
18
|
+
const {
|
|
19
|
+
isStandaloneMode,
|
|
20
|
+
getAvailableFeatures,
|
|
21
|
+
getConfigDefaults,
|
|
22
|
+
validateCaptureRequirements,
|
|
23
|
+
} = require("./standalone-mode");
|
|
24
|
+
|
|
25
|
+
const SETTINGS_DIR = ".reshot";
|
|
26
|
+
const SETTINGS_PATH = path.join(process.cwd(), SETTINGS_DIR, "settings.json");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if an error indicates the API key is invalid and re-auth is needed
|
|
30
|
+
* @param {Error|Object} error - The error from API call
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function isAuthError(error) {
|
|
34
|
+
if (!error) return false;
|
|
35
|
+
|
|
36
|
+
// Check for axios response errors
|
|
37
|
+
const status = error.response?.status;
|
|
38
|
+
if (status === 401 || status === 403) return true;
|
|
39
|
+
|
|
40
|
+
// Check error message for auth-related keywords
|
|
41
|
+
const message = (
|
|
42
|
+
error.message ||
|
|
43
|
+
error.response?.data?.error ||
|
|
44
|
+
""
|
|
45
|
+
).toLowerCase();
|
|
46
|
+
return (
|
|
47
|
+
message.includes("invalid api key") ||
|
|
48
|
+
message.includes("api key required") ||
|
|
49
|
+
message.includes("unauthorized") ||
|
|
50
|
+
message.includes("authentication") ||
|
|
51
|
+
message.includes("not authenticated")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create an auth error response object for the UI
|
|
57
|
+
* @param {string} message - Error message
|
|
58
|
+
* @returns {Object}
|
|
59
|
+
*/
|
|
60
|
+
function createAuthErrorResponse(message) {
|
|
61
|
+
return {
|
|
62
|
+
error: message,
|
|
63
|
+
authRequired: true,
|
|
64
|
+
code: "AUTH_REQUIRED",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const CONFIG_PATH = path.join(process.cwd(), "docsync.config.json");
|
|
68
|
+
const WORKSPACE_PATH = path.join(process.cwd(), SETTINGS_DIR, "workspace.json");
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read settings file
|
|
72
|
+
* @returns {Object} Settings object with projectId
|
|
73
|
+
*/
|
|
74
|
+
function readSettings() {
|
|
75
|
+
if (!fs.existsSync(SETTINGS_PATH)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"Reshot is not initialized in this directory. Run `reshot init` after authenticating."
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return fs.readJSONSync(SETTINGS_PATH);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Write settings file
|
|
85
|
+
* @param {Object} settings - Settings object to write
|
|
86
|
+
*/
|
|
87
|
+
function writeSettings(settings) {
|
|
88
|
+
const settingsDir = path.dirname(SETTINGS_PATH);
|
|
89
|
+
fs.ensureDirSync(settingsDir);
|
|
90
|
+
fs.writeJSONSync(SETTINGS_PATH, settings, { spaces: 2 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ===== WORKSPACE MANAGEMENT =====
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Default workspace structure
|
|
97
|
+
* A workspace groups multiple scenarios with shared variant dimensions
|
|
98
|
+
*/
|
|
99
|
+
const DEFAULT_WORKSPACE = {
|
|
100
|
+
name: "Default Workspace",
|
|
101
|
+
description: "",
|
|
102
|
+
// Common variant dimensions that apply to all scenarios in this workspace
|
|
103
|
+
variants: {
|
|
104
|
+
dimensions: {
|
|
105
|
+
// Example: locale, role, theme dimensions
|
|
106
|
+
},
|
|
107
|
+
presets: {
|
|
108
|
+
// Example: commonly used variant combinations
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
// Scenarios included in this workspace (by key)
|
|
112
|
+
scenarios: [],
|
|
113
|
+
// Metadata
|
|
114
|
+
createdAt: null,
|
|
115
|
+
updatedAt: null,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if workspace file exists
|
|
120
|
+
* @returns {boolean}
|
|
121
|
+
*/
|
|
122
|
+
function workspaceExists() {
|
|
123
|
+
return fs.existsSync(WORKSPACE_PATH);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Read workspace file
|
|
128
|
+
* @returns {Object} Workspace configuration
|
|
129
|
+
*/
|
|
130
|
+
function readWorkspace() {
|
|
131
|
+
if (!fs.existsSync(WORKSPACE_PATH)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return fs.readJSONSync(WORKSPACE_PATH);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Write workspace file
|
|
139
|
+
* @param {Object} workspace - Workspace object to write
|
|
140
|
+
*/
|
|
141
|
+
function writeWorkspace(workspace) {
|
|
142
|
+
const settingsDir = path.dirname(WORKSPACE_PATH);
|
|
143
|
+
fs.ensureDirSync(settingsDir);
|
|
144
|
+
workspace.updatedAt = new Date().toISOString();
|
|
145
|
+
fs.writeJSONSync(WORKSPACE_PATH, workspace, { spaces: 2 });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a new workspace
|
|
150
|
+
* @param {Object} options - Workspace options
|
|
151
|
+
* @param {string} options.name - Workspace name
|
|
152
|
+
* @param {string} [options.description] - Workspace description
|
|
153
|
+
* @param {Object} [options.variants] - Variant configuration
|
|
154
|
+
* @returns {Object} Created workspace
|
|
155
|
+
*/
|
|
156
|
+
function createWorkspace(options = {}) {
|
|
157
|
+
const workspace = {
|
|
158
|
+
...DEFAULT_WORKSPACE,
|
|
159
|
+
name: options.name || "Default Workspace",
|
|
160
|
+
description: options.description || "",
|
|
161
|
+
variants: options.variants || DEFAULT_WORKSPACE.variants,
|
|
162
|
+
scenarios: [],
|
|
163
|
+
createdAt: new Date().toISOString(),
|
|
164
|
+
updatedAt: new Date().toISOString(),
|
|
165
|
+
};
|
|
166
|
+
writeWorkspace(workspace);
|
|
167
|
+
return workspace;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Add a scenario to the workspace
|
|
172
|
+
* @param {string} scenarioKey - Scenario key to add
|
|
173
|
+
* @returns {Object} Updated workspace
|
|
174
|
+
*/
|
|
175
|
+
function addScenarioToWorkspace(scenarioKey) {
|
|
176
|
+
let workspace = readWorkspace();
|
|
177
|
+
if (!workspace) {
|
|
178
|
+
workspace = createWorkspace();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Ensure scenarios is an array
|
|
182
|
+
if (!Array.isArray(workspace.scenarios)) {
|
|
183
|
+
workspace.scenarios = [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!workspace.scenarios.includes(scenarioKey)) {
|
|
187
|
+
workspace.scenarios.push(scenarioKey);
|
|
188
|
+
writeWorkspace(workspace);
|
|
189
|
+
}
|
|
190
|
+
return workspace;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Remove a scenario from the workspace
|
|
195
|
+
* @param {string} scenarioKey - Scenario key to remove
|
|
196
|
+
* @returns {Object} Updated workspace
|
|
197
|
+
*/
|
|
198
|
+
function removeScenarioFromWorkspace(scenarioKey) {
|
|
199
|
+
const workspace = readWorkspace();
|
|
200
|
+
if (!workspace) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Ensure scenarios is an array
|
|
205
|
+
if (!Array.isArray(workspace.scenarios)) {
|
|
206
|
+
workspace.scenarios = [];
|
|
207
|
+
writeWorkspace(workspace);
|
|
208
|
+
return workspace;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const index = workspace.scenarios.indexOf(scenarioKey);
|
|
212
|
+
if (index !== -1) {
|
|
213
|
+
workspace.scenarios.splice(index, 1);
|
|
214
|
+
writeWorkspace(workspace);
|
|
215
|
+
}
|
|
216
|
+
return workspace;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Update workspace variants configuration
|
|
221
|
+
* @param {Object} variants - New variants configuration
|
|
222
|
+
* @returns {Object} Updated workspace
|
|
223
|
+
*/
|
|
224
|
+
function updateWorkspaceVariants(variants) {
|
|
225
|
+
let workspace = readWorkspace();
|
|
226
|
+
if (!workspace) {
|
|
227
|
+
workspace = createWorkspace();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
workspace.variants = variants;
|
|
231
|
+
writeWorkspace(workspace);
|
|
232
|
+
return workspace;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get workspace with resolved scenarios
|
|
237
|
+
* @returns {Object|null} Workspace with scenario details from config
|
|
238
|
+
*/
|
|
239
|
+
function getWorkspaceWithScenarios() {
|
|
240
|
+
const workspace = readWorkspace();
|
|
241
|
+
if (!workspace) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let docSyncConfig = null;
|
|
246
|
+
try {
|
|
247
|
+
docSyncConfig = readConfig();
|
|
248
|
+
} catch (e) {
|
|
249
|
+
// Config doesn't exist
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const allScenarios = docSyncConfig?.scenarios || [];
|
|
253
|
+
|
|
254
|
+
// Ensure workspace.scenarios is always an array to prevent .map errors
|
|
255
|
+
const workspaceScenarioKeys = Array.isArray(workspace.scenarios)
|
|
256
|
+
? workspace.scenarios
|
|
257
|
+
: [];
|
|
258
|
+
|
|
259
|
+
const workspaceScenarios = workspaceScenarioKeys
|
|
260
|
+
.map((key) => allScenarios.find((s) => s.key === key))
|
|
261
|
+
.filter(Boolean);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
...workspace,
|
|
265
|
+
scenarios: workspaceScenarioKeys, // Ensure scenarios is always an array
|
|
266
|
+
resolvedScenarios: workspaceScenarios,
|
|
267
|
+
allScenarios: allScenarios,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Read config file
|
|
273
|
+
* @returns {Object} Reshot configuration
|
|
274
|
+
*/
|
|
275
|
+
function readConfig() {
|
|
276
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`Config file not found at ${CONFIG_PATH}. Run \`reshot init\` to create one.`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const config = fs.readJSONSync(CONFIG_PATH);
|
|
283
|
+
|
|
284
|
+
// Validate required fields
|
|
285
|
+
if (!config.scenarios || !Array.isArray(config.scenarios)) {
|
|
286
|
+
throw new Error('Config must have a "scenarios" array');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const validActions = [
|
|
290
|
+
"click",
|
|
291
|
+
"type",
|
|
292
|
+
"input",
|
|
293
|
+
"hover",
|
|
294
|
+
"wait",
|
|
295
|
+
"waitForSelector",
|
|
296
|
+
"screenshot",
|
|
297
|
+
"goto",
|
|
298
|
+
"scroll",
|
|
299
|
+
"select",
|
|
300
|
+
"keyboard",
|
|
301
|
+
"clip",
|
|
302
|
+
"gif", // Looping GIF capture
|
|
303
|
+
"video", // Video clip capture
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
// Valid output formats
|
|
307
|
+
const validOutputFormats = [
|
|
308
|
+
"png", // Static screenshot (default)
|
|
309
|
+
"gif", // Looping GIF (primary for animations)
|
|
310
|
+
"mp4", // Video clip
|
|
311
|
+
"step-by-step-images", // Legacy: individual step screenshots
|
|
312
|
+
"summary-video", // Legacy: combined video of all steps
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
for (const scenario of config.scenarios) {
|
|
316
|
+
if (!scenario.name) {
|
|
317
|
+
throw new Error('Each scenario must have a "name" field');
|
|
318
|
+
}
|
|
319
|
+
if (!scenario.key) {
|
|
320
|
+
throw new Error(`Scenario "${scenario.name}" must have a "key" field`);
|
|
321
|
+
}
|
|
322
|
+
if (!/^[a-z0-9-]+$/i.test(scenario.key)) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`Scenario "${scenario.name}" has invalid key "${scenario.key}". Keys must be alphanumeric with hyphens only.`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (!scenario.url) {
|
|
328
|
+
throw new Error(`Scenario "${scenario.name}" must have a "url" field`);
|
|
329
|
+
}
|
|
330
|
+
if (!scenario.steps || !Array.isArray(scenario.steps)) {
|
|
331
|
+
throw new Error(`Scenario "${scenario.name}" must have a "steps" array`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Validate each step
|
|
335
|
+
for (let i = 0; i < scenario.steps.length; i++) {
|
|
336
|
+
const step = scenario.steps[i];
|
|
337
|
+
const stepNum = i + 1;
|
|
338
|
+
|
|
339
|
+
if (!step.action) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`Scenario "${scenario.name}" step ${stepNum} must have an "action" field`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!validActions.includes(step.action)) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`Scenario "${scenario.name}" step ${stepNum} has invalid action "${
|
|
348
|
+
step.action
|
|
349
|
+
}". Valid actions: ${validActions.join(", ")}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Validate selector for actions that need it
|
|
354
|
+
const needsSelector = [
|
|
355
|
+
"click",
|
|
356
|
+
"type",
|
|
357
|
+
"input",
|
|
358
|
+
"hover",
|
|
359
|
+
"waitForSelector",
|
|
360
|
+
"scroll",
|
|
361
|
+
"select",
|
|
362
|
+
];
|
|
363
|
+
if (needsSelector.includes(step.action) && !step.selector) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Scenario "${scenario.name}" step ${stepNum} (${step.action}) requires a "selector" field`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Validate text for type/input actions
|
|
370
|
+
if (
|
|
371
|
+
(step.action === "type" || step.action === "input") &&
|
|
372
|
+
step.text === undefined
|
|
373
|
+
) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Scenario "${scenario.name}" step ${stepNum} (${step.action}) requires a "text" field`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Validate step-level privacy override (must be object or undefined)
|
|
380
|
+
if (step.privacy !== undefined) {
|
|
381
|
+
if (typeof step.privacy !== "object" || step.privacy === null || Array.isArray(step.privacy)) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`Scenario "${scenario.name}" step ${stepNum}: privacy must be an object`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Validate step-level style override (must be object or undefined)
|
|
389
|
+
if (step.style !== undefined) {
|
|
390
|
+
if (typeof step.style !== "object" || step.style === null || Array.isArray(step.style)) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Scenario "${scenario.name}" step ${stepNum}: style must be an object`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Validate optional privacy block
|
|
400
|
+
if (config.privacy !== undefined) {
|
|
401
|
+
if (typeof config.privacy !== "object" || config.privacy === null || Array.isArray(config.privacy)) {
|
|
402
|
+
throw new Error("privacy must be an object");
|
|
403
|
+
}
|
|
404
|
+
if (config.privacy.method !== undefined && !["redact", "blur", "hide", "remove"].includes(config.privacy.method)) {
|
|
405
|
+
throw new Error('privacy.method must be one of: redact, blur, hide, remove');
|
|
406
|
+
}
|
|
407
|
+
if (config.privacy.blurRadius !== undefined) {
|
|
408
|
+
if (typeof config.privacy.blurRadius !== "number" || config.privacy.blurRadius < 1 || config.privacy.blurRadius > 100) {
|
|
409
|
+
throw new Error("privacy.blurRadius must be a number between 1 and 100");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (config.privacy.selectors !== undefined && !Array.isArray(config.privacy.selectors)) {
|
|
413
|
+
throw new Error("privacy.selectors must be an array");
|
|
414
|
+
}
|
|
415
|
+
// Validate individual selector entries
|
|
416
|
+
if (Array.isArray(config.privacy.selectors)) {
|
|
417
|
+
for (let i = 0; i < config.privacy.selectors.length; i++) {
|
|
418
|
+
const entry = config.privacy.selectors[i];
|
|
419
|
+
if (typeof entry === "string") {
|
|
420
|
+
if (!entry.trim()) {
|
|
421
|
+
throw new Error(`privacy.selectors[${i}] is empty`);
|
|
422
|
+
}
|
|
423
|
+
} else if (entry && typeof entry === "object") {
|
|
424
|
+
if (!entry.selector || typeof entry.selector !== "string" || !entry.selector.trim()) {
|
|
425
|
+
throw new Error(`privacy.selectors[${i}].selector must be a non-empty string`);
|
|
426
|
+
}
|
|
427
|
+
if (entry.method !== undefined && !["redact", "blur", "hide", "remove"].includes(entry.method)) {
|
|
428
|
+
throw new Error(`privacy.selectors[${i}].method must be one of: redact, blur, hide, remove`);
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
throw new Error(`privacy.selectors[${i}] must be a string or { selector, method?, blurRadius? }`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Validate optional style block
|
|
438
|
+
if (config.style !== undefined) {
|
|
439
|
+
if (typeof config.style !== "object" || config.style === null || Array.isArray(config.style)) {
|
|
440
|
+
throw new Error("style must be an object");
|
|
441
|
+
}
|
|
442
|
+
if (config.style.frame !== undefined && !["none", "macos", "windows"].includes(config.style.frame)) {
|
|
443
|
+
throw new Error('style.frame must be one of: none, macos, windows');
|
|
444
|
+
}
|
|
445
|
+
if (config.style.shadow !== undefined && !["none", "small", "medium", "large"].includes(config.style.shadow)) {
|
|
446
|
+
throw new Error('style.shadow must be one of: none, small, medium, large');
|
|
447
|
+
}
|
|
448
|
+
if (config.style.padding !== undefined) {
|
|
449
|
+
if (typeof config.style.padding !== "number" || config.style.padding < 0 || config.style.padding > 200) {
|
|
450
|
+
throw new Error("style.padding must be a number between 0 and 200");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (config.style.borderRadius !== undefined) {
|
|
454
|
+
if (typeof config.style.borderRadius !== "number" || config.style.borderRadius < 0 || config.style.borderRadius > 100) {
|
|
455
|
+
throw new Error("style.borderRadius must be a number between 0 and 100");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (config.style.background !== undefined) {
|
|
459
|
+
if (typeof config.style.background !== "string") {
|
|
460
|
+
throw new Error("style.background must be a string");
|
|
461
|
+
}
|
|
462
|
+
const bg = config.style.background;
|
|
463
|
+
if (bg !== "transparent") {
|
|
464
|
+
const isHex = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(bg);
|
|
465
|
+
const isGradient = bg.startsWith("linear-gradient(");
|
|
466
|
+
if (!isHex && !isGradient) {
|
|
467
|
+
throw new Error('style.background must be "transparent", a hex color, or a linear-gradient()');
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Validate optional docs block
|
|
474
|
+
if (config.docs !== undefined) {
|
|
475
|
+
if (
|
|
476
|
+
typeof config.docs !== "object" ||
|
|
477
|
+
config.docs === null ||
|
|
478
|
+
Array.isArray(config.docs)
|
|
479
|
+
) {
|
|
480
|
+
throw new Error("docs must be an object");
|
|
481
|
+
}
|
|
482
|
+
if (
|
|
483
|
+
config.docs.root !== undefined &&
|
|
484
|
+
typeof config.docs.root !== "string"
|
|
485
|
+
) {
|
|
486
|
+
throw new Error("docs.root must be a string");
|
|
487
|
+
}
|
|
488
|
+
if (
|
|
489
|
+
config.docs.include !== undefined &&
|
|
490
|
+
!Array.isArray(config.docs.include)
|
|
491
|
+
) {
|
|
492
|
+
throw new Error("docs.include must be an array of strings");
|
|
493
|
+
}
|
|
494
|
+
if (
|
|
495
|
+
config.docs.exclude !== undefined &&
|
|
496
|
+
!Array.isArray(config.docs.exclude)
|
|
497
|
+
) {
|
|
498
|
+
throw new Error("docs.exclude must be an array of strings");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return config;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Read docsync.config.json with DocSync-specific configuration
|
|
507
|
+
* Returns the full config including the documentation block for ingestion
|
|
508
|
+
* @returns {Object} DocSync configuration
|
|
509
|
+
*/
|
|
510
|
+
function readDocSyncConfig() {
|
|
511
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
`Config file not found at ${CONFIG_PATH}. Run \`reshot init\` to create one.`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const config = fs.readJSONSync(CONFIG_PATH);
|
|
518
|
+
|
|
519
|
+
// Validate documentation block if present
|
|
520
|
+
if (config.documentation) {
|
|
521
|
+
const doc = config.documentation;
|
|
522
|
+
|
|
523
|
+
// Validate required fields
|
|
524
|
+
if (!doc.strategy) {
|
|
525
|
+
throw new Error('documentation.strategy is required (git_pr or external_host)');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!['git_pr', 'external_host'].includes(doc.strategy)) {
|
|
529
|
+
throw new Error('documentation.strategy must be "git_pr" or "external_host"');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Validate optional fields
|
|
533
|
+
if (doc.assetFormat && !['cdn_link', 'markdown'].includes(doc.assetFormat)) {
|
|
534
|
+
throw new Error('documentation.assetFormat must be "cdn_link" or "markdown"');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (doc.include && !Array.isArray(doc.include)) {
|
|
538
|
+
throw new Error('documentation.include must be an array of glob patterns');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (doc.exclude && !Array.isArray(doc.exclude)) {
|
|
542
|
+
throw new Error('documentation.exclude must be an array of glob patterns');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (doc.mappings && typeof doc.mappings !== 'object') {
|
|
546
|
+
throw new Error('documentation.mappings must be an object');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return config;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Write config file
|
|
555
|
+
* @param {Object} config - Config object to write
|
|
556
|
+
*/
|
|
557
|
+
function writeConfig(config) {
|
|
558
|
+
fs.writeJSONSync(CONFIG_PATH, config, { spaces: 2 });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Check if config file exists
|
|
563
|
+
* @returns {boolean}
|
|
564
|
+
*/
|
|
565
|
+
function configExists() {
|
|
566
|
+
return fs.existsSync(CONFIG_PATH);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ===== CAPTURE CONFIGURATION =====
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Default capture configuration
|
|
573
|
+
*/
|
|
574
|
+
const DEFAULT_CAPTURE_CONFIG = {
|
|
575
|
+
retryOnError: 2,
|
|
576
|
+
retryDelay: 1000,
|
|
577
|
+
readyTimeout: 15000,
|
|
578
|
+
scenarioTimeout: 60000,
|
|
579
|
+
errorSelectors: ["[data-testid='page-error']", "[data-error-type]"],
|
|
580
|
+
errorHeuristics: true,
|
|
581
|
+
contentVerification: false,
|
|
582
|
+
preflightCheck: true,
|
|
583
|
+
authPatterns: [],
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get capture configuration with sensible defaults
|
|
588
|
+
* Merges global capture config with per-scenario overrides
|
|
589
|
+
* @param {Object} [scenarioOverrides] - Per-scenario capture config overrides
|
|
590
|
+
* @returns {Object} Merged capture config
|
|
591
|
+
*/
|
|
592
|
+
function getCaptureConfig(scenarioOverrides = {}) {
|
|
593
|
+
let globalConfig = {};
|
|
594
|
+
try {
|
|
595
|
+
const config = readConfig();
|
|
596
|
+
globalConfig = config.capture || {};
|
|
597
|
+
} catch (e) {
|
|
598
|
+
// Config doesn't exist, use defaults
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Filter out undefined values so they don't overwrite defaults
|
|
602
|
+
const cleanOverrides = Object.fromEntries(
|
|
603
|
+
Object.entries(scenarioOverrides).filter(([, v]) => v !== undefined)
|
|
604
|
+
);
|
|
605
|
+
const cleanGlobal = Object.fromEntries(
|
|
606
|
+
Object.entries(globalConfig).filter(([, v]) => v !== undefined)
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const merged = {
|
|
610
|
+
...DEFAULT_CAPTURE_CONFIG,
|
|
611
|
+
...cleanGlobal,
|
|
612
|
+
...cleanOverrides,
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Validate bounds
|
|
616
|
+
if (typeof merged.retryOnError === "number") {
|
|
617
|
+
merged.retryOnError = Math.max(0, Math.min(merged.retryOnError, 5));
|
|
618
|
+
}
|
|
619
|
+
if (typeof merged.retryDelay === "number") {
|
|
620
|
+
merged.retryDelay = Math.max(500, Math.min(merged.retryDelay, 30000));
|
|
621
|
+
}
|
|
622
|
+
if (typeof merged.readyTimeout === "number") {
|
|
623
|
+
merged.readyTimeout = Math.max(1000, Math.min(merged.readyTimeout, 60000));
|
|
624
|
+
}
|
|
625
|
+
if (typeof merged.scenarioTimeout === "number") {
|
|
626
|
+
merged.scenarioTimeout = Math.max(
|
|
627
|
+
5000,
|
|
628
|
+
Math.min(merged.scenarioTimeout, 300000)
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Ensure errorSelectors is always an array
|
|
633
|
+
if (!Array.isArray(merged.errorSelectors)) {
|
|
634
|
+
merged.errorSelectors = DEFAULT_CAPTURE_CONFIG.errorSelectors;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return merged;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ===== PRIVACY CONFIGURATION =====
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Default privacy configuration
|
|
644
|
+
*/
|
|
645
|
+
const DEFAULT_PRIVACY_CONFIG = {
|
|
646
|
+
enabled: true,
|
|
647
|
+
method: "redact",
|
|
648
|
+
blurRadius: 8,
|
|
649
|
+
selectors: [],
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const VALID_PRIVACY_METHODS = ["redact", "blur", "hide", "remove"];
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Get privacy configuration with sensible defaults
|
|
656
|
+
* Merges global privacy config with per-scenario overrides.
|
|
657
|
+
* Selectors are ADDITIVE (union). method/blurRadius are overridden.
|
|
658
|
+
* @param {Object} [scenarioOverrides] - Per-scenario privacy config overrides
|
|
659
|
+
* @returns {Object} Merged privacy config
|
|
660
|
+
*/
|
|
661
|
+
function getPrivacyConfig(scenarioOverrides = {}) {
|
|
662
|
+
let globalConfig = {};
|
|
663
|
+
try {
|
|
664
|
+
const config = readConfig();
|
|
665
|
+
globalConfig = config.privacy || {};
|
|
666
|
+
} catch (e) {
|
|
667
|
+
// Config doesn't exist, use defaults
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const merged = {
|
|
671
|
+
enabled: scenarioOverrides.enabled !== undefined
|
|
672
|
+
? scenarioOverrides.enabled
|
|
673
|
+
: globalConfig.enabled !== undefined
|
|
674
|
+
? globalConfig.enabled
|
|
675
|
+
: DEFAULT_PRIVACY_CONFIG.enabled,
|
|
676
|
+
method: scenarioOverrides.method || globalConfig.method || DEFAULT_PRIVACY_CONFIG.method,
|
|
677
|
+
blurRadius: scenarioOverrides.blurRadius || globalConfig.blurRadius || DEFAULT_PRIVACY_CONFIG.blurRadius,
|
|
678
|
+
// Selectors are additive (union of global + scenario)
|
|
679
|
+
selectors: [
|
|
680
|
+
...(globalConfig.selectors || []),
|
|
681
|
+
...(scenarioOverrides.selectors || []),
|
|
682
|
+
],
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// Validate method
|
|
686
|
+
if (!VALID_PRIVACY_METHODS.includes(merged.method)) {
|
|
687
|
+
merged.method = DEFAULT_PRIVACY_CONFIG.method;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Validate blurRadius bounds
|
|
691
|
+
if (typeof merged.blurRadius === "number") {
|
|
692
|
+
merged.blurRadius = Math.max(1, Math.min(merged.blurRadius, 100));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return merged;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ===== STYLE CONFIGURATION =====
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Default style configuration
|
|
702
|
+
*/
|
|
703
|
+
const DEFAULT_STYLE_CONFIG = {
|
|
704
|
+
enabled: true,
|
|
705
|
+
frame: "none",
|
|
706
|
+
shadow: "medium",
|
|
707
|
+
padding: 40,
|
|
708
|
+
background: "transparent",
|
|
709
|
+
borderRadius: 0,
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const VALID_FRAMES = ["none", "macos", "windows"];
|
|
713
|
+
const VALID_SHADOWS = ["none", "small", "medium", "large"];
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Get style configuration with sensible defaults
|
|
717
|
+
* Merges global style config with per-scenario overrides (flat replace, not additive).
|
|
718
|
+
* @param {Object} [scenarioOverrides] - Per-scenario style config overrides
|
|
719
|
+
* @returns {Object} Merged style config
|
|
720
|
+
*/
|
|
721
|
+
function getStyleConfig(scenarioOverrides = {}) {
|
|
722
|
+
let globalConfig = {};
|
|
723
|
+
try {
|
|
724
|
+
const config = readConfig();
|
|
725
|
+
globalConfig = config.style || {};
|
|
726
|
+
} catch (e) {
|
|
727
|
+
// Config doesn't exist, use defaults
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Filter out undefined values
|
|
731
|
+
const cleanOverrides = Object.fromEntries(
|
|
732
|
+
Object.entries(scenarioOverrides).filter(([, v]) => v !== undefined)
|
|
733
|
+
);
|
|
734
|
+
const cleanGlobal = Object.fromEntries(
|
|
735
|
+
Object.entries(globalConfig).filter(([, v]) => v !== undefined)
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
const merged = {
|
|
739
|
+
...DEFAULT_STYLE_CONFIG,
|
|
740
|
+
...cleanGlobal,
|
|
741
|
+
...cleanOverrides,
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// Validate frame
|
|
745
|
+
if (!VALID_FRAMES.includes(merged.frame)) {
|
|
746
|
+
merged.frame = DEFAULT_STYLE_CONFIG.frame;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Validate shadow
|
|
750
|
+
if (!VALID_SHADOWS.includes(merged.shadow)) {
|
|
751
|
+
merged.shadow = DEFAULT_STYLE_CONFIG.shadow;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Validate bounds
|
|
755
|
+
if (typeof merged.padding === "number") {
|
|
756
|
+
merged.padding = Math.max(0, Math.min(merged.padding, 200));
|
|
757
|
+
}
|
|
758
|
+
if (typeof merged.borderRadius === "number") {
|
|
759
|
+
merged.borderRadius = Math.max(0, Math.min(merged.borderRadius, 100));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return merged;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ===== DIFFING CONFIGURATION =====
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Get diffing configuration with defaults
|
|
769
|
+
* Diffing is ENABLED by default for local version-to-version comparison
|
|
770
|
+
* @returns {Object} Diffing config { enabled, threshold, includeAA }
|
|
771
|
+
*/
|
|
772
|
+
function getDiffingConfig() {
|
|
773
|
+
try {
|
|
774
|
+
const config = readConfig();
|
|
775
|
+
return {
|
|
776
|
+
// Default to TRUE - always diff unless explicitly disabled
|
|
777
|
+
enabled: config.diffing?.enabled ?? true,
|
|
778
|
+
threshold: config.diffing?.threshold ?? 0.1,
|
|
779
|
+
includeAA: config.diffing?.includeAA ?? false,
|
|
780
|
+
};
|
|
781
|
+
} catch (e) {
|
|
782
|
+
// Return defaults if config doesn't exist - diffing ON by default
|
|
783
|
+
return {
|
|
784
|
+
enabled: true,
|
|
785
|
+
threshold: 0.1,
|
|
786
|
+
includeAA: false,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Update diffing configuration
|
|
793
|
+
* @param {Object} diffingConfig - New diffing config (partial updates supported)
|
|
794
|
+
*/
|
|
795
|
+
function updateDiffingConfig(diffingConfig) {
|
|
796
|
+
const config = readConfig();
|
|
797
|
+
config.diffing = {
|
|
798
|
+
enabled: config.diffing?.enabled ?? false,
|
|
799
|
+
threshold: config.diffing?.threshold ?? 0.1,
|
|
800
|
+
includeAA: config.diffing?.includeAA ?? false,
|
|
801
|
+
...diffingConfig,
|
|
802
|
+
};
|
|
803
|
+
writeConfig(config);
|
|
804
|
+
return config.diffing;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Initialize project by fetching config from platform
|
|
809
|
+
* This is the core logic shared between CLI init command and UI init endpoint
|
|
810
|
+
* @param {string} projectId - Project ID to initialize
|
|
811
|
+
* @param {string} apiKey - API key for authentication
|
|
812
|
+
* @param {Object} options - Options
|
|
813
|
+
* @param {boolean} options.overwrite - Whether to overwrite existing config (default: false)
|
|
814
|
+
* @returns {Promise<Object>} The initialized config
|
|
815
|
+
*/
|
|
816
|
+
async function initializeProject(projectId, apiKey, options = {}) {
|
|
817
|
+
const apiClient = require("./api-client");
|
|
818
|
+
const { overwrite = false } = options;
|
|
819
|
+
|
|
820
|
+
if (!projectId || !apiKey) {
|
|
821
|
+
throw new Error("projectId and apiKey are required");
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Check if config exists and overwrite is false
|
|
825
|
+
if (configExists() && !overwrite) {
|
|
826
|
+
throw new Error("Config already exists. Set overwrite=true to replace it.");
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
let blueprint = null;
|
|
830
|
+
try {
|
|
831
|
+
blueprint = await apiClient.getProjectConfig(projectId, apiKey);
|
|
832
|
+
} catch (error) {
|
|
833
|
+
// If fetch fails, use boilerplate
|
|
834
|
+
const BOILERPLATE_CONFIG = {
|
|
835
|
+
baseUrl: "https://example.com",
|
|
836
|
+
assetDir: ".reshot/output",
|
|
837
|
+
concurrency: 2,
|
|
838
|
+
defaultWaitUntil: "networkidle",
|
|
839
|
+
viewport: { width: 1280, height: 720 },
|
|
840
|
+
timeout: 45000,
|
|
841
|
+
headless: true,
|
|
842
|
+
contexts: {
|
|
843
|
+
default: { name: "default", data: {} },
|
|
844
|
+
},
|
|
845
|
+
scenarios: [], // Start with empty scenarios - user will record their own
|
|
846
|
+
_metadata: {
|
|
847
|
+
projectId,
|
|
848
|
+
projectName: "Unknown Project",
|
|
849
|
+
generatedAt: new Date().toISOString(),
|
|
850
|
+
visualCount: 1,
|
|
851
|
+
contextCount: 1,
|
|
852
|
+
features: {
|
|
853
|
+
visuals: true,
|
|
854
|
+
docs: false,
|
|
855
|
+
changelog: true,
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
blueprint = BOILERPLATE_CONFIG;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Write config
|
|
863
|
+
writeConfig(blueprint);
|
|
864
|
+
|
|
865
|
+
// Update settings
|
|
866
|
+
let settings;
|
|
867
|
+
try {
|
|
868
|
+
settings = readSettings();
|
|
869
|
+
} catch (error) {
|
|
870
|
+
// Create new settings if they don't exist
|
|
871
|
+
settings = { projectId, apiKey };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const updatedSettings = {
|
|
875
|
+
...settings,
|
|
876
|
+
projectId,
|
|
877
|
+
apiKey,
|
|
878
|
+
projectName:
|
|
879
|
+
blueprint._metadata?.projectName || settings.projectName || null,
|
|
880
|
+
lastSyncedAt: new Date().toISOString(),
|
|
881
|
+
};
|
|
882
|
+
writeSettings(updatedSettings);
|
|
883
|
+
|
|
884
|
+
return blueprint;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Get output configuration with enhanced features
|
|
889
|
+
* @returns {Object} Output config with template, viewports, and crop settings
|
|
890
|
+
*/
|
|
891
|
+
function getOutputConfig() {
|
|
892
|
+
try {
|
|
893
|
+
const config = readConfig();
|
|
894
|
+
return {
|
|
895
|
+
template: config.output?.template || TEMPLATE_PRESETS.default,
|
|
896
|
+
templatePresets: getTemplatePresets(),
|
|
897
|
+
viewport: config.viewport || { width: 1280, height: 720 },
|
|
898
|
+
viewportPresets: config.viewportPresets || {},
|
|
899
|
+
crop: config.output?.crop || null,
|
|
900
|
+
cropPresets: getAllCropPresets(),
|
|
901
|
+
};
|
|
902
|
+
} catch (e) {
|
|
903
|
+
return {
|
|
904
|
+
template: TEMPLATE_PRESETS.default,
|
|
905
|
+
templatePresets: getTemplatePresets(),
|
|
906
|
+
viewport: { width: 1280, height: 720 },
|
|
907
|
+
viewportPresets: {},
|
|
908
|
+
crop: null,
|
|
909
|
+
cropPresets: getAllCropPresets(),
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Update output configuration
|
|
916
|
+
* @param {Object} outputConfig - New output configuration (partial update)
|
|
917
|
+
* @returns {Object} Updated configuration
|
|
918
|
+
*/
|
|
919
|
+
function updateOutputConfig(outputConfig) {
|
|
920
|
+
const config = readConfig();
|
|
921
|
+
|
|
922
|
+
// Validate template if provided
|
|
923
|
+
if (outputConfig.template) {
|
|
924
|
+
const validation = validateTemplate(outputConfig.template);
|
|
925
|
+
if (!validation.valid) {
|
|
926
|
+
throw new Error(`Invalid output template: ${validation.error}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Validate viewport if provided
|
|
931
|
+
if (outputConfig.viewport) {
|
|
932
|
+
const resolved = resolveViewport(outputConfig.viewport);
|
|
933
|
+
const validation = validateViewport(resolved);
|
|
934
|
+
if (!validation.valid) {
|
|
935
|
+
throw new Error(`Invalid viewport: ${validation.error}`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Merge output config
|
|
940
|
+
config.output = {
|
|
941
|
+
...config.output,
|
|
942
|
+
...outputConfig,
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
// Update viewport at top level if provided
|
|
946
|
+
if (outputConfig.viewport) {
|
|
947
|
+
config.viewport = resolveViewport(outputConfig.viewport);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
writeConfig(config);
|
|
951
|
+
return config;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Get available viewport presets (built-in + custom)
|
|
956
|
+
* @returns {Object} Viewport presets
|
|
957
|
+
*/
|
|
958
|
+
function getViewportPresetsConfig() {
|
|
959
|
+
try {
|
|
960
|
+
const config = readConfig();
|
|
961
|
+
const builtIn = getAllViewportPresets();
|
|
962
|
+
const custom = config.viewportPresets || {};
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
builtIn,
|
|
966
|
+
custom,
|
|
967
|
+
all: { ...builtIn, ...custom },
|
|
968
|
+
};
|
|
969
|
+
} catch (e) {
|
|
970
|
+
return {
|
|
971
|
+
builtIn: getAllViewportPresets(),
|
|
972
|
+
custom: {},
|
|
973
|
+
all: getAllViewportPresets(),
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Add or update a custom viewport preset
|
|
980
|
+
* @param {string} key - Preset key
|
|
981
|
+
* @param {Object} preset - Preset configuration
|
|
982
|
+
* @returns {Object} Updated config
|
|
983
|
+
*/
|
|
984
|
+
function saveViewportPreset(key, preset) {
|
|
985
|
+
const resolved = resolveViewport(preset);
|
|
986
|
+
const validation = validateViewport(resolved);
|
|
987
|
+
if (!validation.valid) {
|
|
988
|
+
throw new Error(`Invalid viewport preset: ${validation.error}`);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const config = readConfig();
|
|
992
|
+
config.viewportPresets = config.viewportPresets || {};
|
|
993
|
+
config.viewportPresets[key] = {
|
|
994
|
+
...preset,
|
|
995
|
+
...resolved,
|
|
996
|
+
category: "custom",
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
writeConfig(config);
|
|
1000
|
+
return config;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Delete a custom viewport preset
|
|
1005
|
+
* @param {string} key - Preset key to delete
|
|
1006
|
+
* @returns {Object} Updated config
|
|
1007
|
+
*/
|
|
1008
|
+
function deleteViewportPreset(key) {
|
|
1009
|
+
const config = readConfig();
|
|
1010
|
+
if (config.viewportPresets && config.viewportPresets[key]) {
|
|
1011
|
+
delete config.viewportPresets[key];
|
|
1012
|
+
writeConfig(config);
|
|
1013
|
+
}
|
|
1014
|
+
return config;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Check CLI mode and features
|
|
1019
|
+
* @returns {Object} Mode info and available features
|
|
1020
|
+
*/
|
|
1021
|
+
function getModeInfo() {
|
|
1022
|
+
let settings = null;
|
|
1023
|
+
try {
|
|
1024
|
+
settings = readSettings();
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
// No settings file
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return {
|
|
1030
|
+
isStandalone: isStandaloneMode(settings),
|
|
1031
|
+
features: getAvailableFeatures(settings),
|
|
1032
|
+
settings: settings
|
|
1033
|
+
? {
|
|
1034
|
+
mode: settings.mode || (settings.apiKey ? "connected" : "standalone"),
|
|
1035
|
+
projectName: settings.projectName,
|
|
1036
|
+
projectId: settings.projectId,
|
|
1037
|
+
hasApiKey: !!settings.apiKey,
|
|
1038
|
+
}
|
|
1039
|
+
: null,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Validate full configuration for capture readiness
|
|
1045
|
+
* @returns {Object} Validation result
|
|
1046
|
+
*/
|
|
1047
|
+
function validateConfig() {
|
|
1048
|
+
try {
|
|
1049
|
+
const config = readConfig();
|
|
1050
|
+
return validateCaptureRequirements(config);
|
|
1051
|
+
} catch (e) {
|
|
1052
|
+
return {
|
|
1053
|
+
valid: false,
|
|
1054
|
+
errors: [e.message],
|
|
1055
|
+
warnings: [],
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ===== VERSIONING CONFIGURATION =====
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Get versioning configuration with defaults
|
|
1064
|
+
* Supports pinned versions vs live head URLs
|
|
1065
|
+
* @returns {Object} Versioning config
|
|
1066
|
+
*/
|
|
1067
|
+
function getVersioningConfig() {
|
|
1068
|
+
try {
|
|
1069
|
+
const config = readConfig();
|
|
1070
|
+
return {
|
|
1071
|
+
// Default URL type: "live" (always latest) or "pinned" (specific version)
|
|
1072
|
+
defaultUrlType: config.versioning?.defaultUrlType ?? "live",
|
|
1073
|
+
// Current pinned tag (if any)
|
|
1074
|
+
pinnedTag: config.versioning?.pinnedTag ?? null,
|
|
1075
|
+
// All available tags
|
|
1076
|
+
tags: config.versioning?.tags ?? [],
|
|
1077
|
+
};
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
return {
|
|
1080
|
+
defaultUrlType: "live",
|
|
1081
|
+
pinnedTag: null,
|
|
1082
|
+
tags: [],
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Update versioning configuration
|
|
1089
|
+
* @param {Object} versioningConfig - New versioning config
|
|
1090
|
+
*/
|
|
1091
|
+
function updateVersioningConfig(versioningConfig) {
|
|
1092
|
+
const config = readConfig();
|
|
1093
|
+
config.versioning = {
|
|
1094
|
+
...config.versioning,
|
|
1095
|
+
...versioningConfig,
|
|
1096
|
+
};
|
|
1097
|
+
writeConfig(config);
|
|
1098
|
+
return config.versioning;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Add a new tag to versioning history
|
|
1103
|
+
* @param {string} tag - Tag name (e.g., "v1.2", "release-2024-01")
|
|
1104
|
+
* @param {Object} metadata - Tag metadata
|
|
1105
|
+
*/
|
|
1106
|
+
function addVersionTag(tag, metadata = {}) {
|
|
1107
|
+
const config = readConfig();
|
|
1108
|
+
config.versioning = config.versioning || { tags: [] };
|
|
1109
|
+
config.versioning.tags = config.versioning.tags || [];
|
|
1110
|
+
|
|
1111
|
+
// Ensure no duplicate tags
|
|
1112
|
+
const existingIndex = config.versioning.tags.findIndex((t) => t.name === tag);
|
|
1113
|
+
const tagData = {
|
|
1114
|
+
name: tag,
|
|
1115
|
+
createdAt: new Date().toISOString(),
|
|
1116
|
+
commitHash: metadata.commitHash || null,
|
|
1117
|
+
...metadata,
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
if (existingIndex >= 0) {
|
|
1121
|
+
config.versioning.tags[existingIndex] = tagData;
|
|
1122
|
+
} else {
|
|
1123
|
+
config.versioning.tags.push(tagData);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
writeConfig(config);
|
|
1127
|
+
return tagData;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// ===== OUTPUT FORMAT CONFIGURATION =====
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Get the preferred output format configuration
|
|
1134
|
+
* Prioritizes GIF/video for animations, PNG for static captures
|
|
1135
|
+
* @returns {Object} Output format preferences
|
|
1136
|
+
*/
|
|
1137
|
+
function getOutputFormatConfig() {
|
|
1138
|
+
try {
|
|
1139
|
+
const config = readConfig();
|
|
1140
|
+
return {
|
|
1141
|
+
// Primary format for multi-step scenarios (prefer GIF for animations)
|
|
1142
|
+
primaryFormat: config.output?.primaryFormat ?? "gif",
|
|
1143
|
+
// Fallback format for static single-step captures
|
|
1144
|
+
staticFormat: config.output?.staticFormat ?? "png",
|
|
1145
|
+
// Video format for full recordings
|
|
1146
|
+
videoFormat: config.output?.videoFormat ?? "mp4",
|
|
1147
|
+
// GIF settings
|
|
1148
|
+
gif: {
|
|
1149
|
+
loop: config.output?.gif?.loop ?? true, // Loop infinitely by default
|
|
1150
|
+
fps: config.output?.gif?.fps ?? 15, // Frames per second
|
|
1151
|
+
quality: config.output?.gif?.quality ?? "high", // high, medium, low
|
|
1152
|
+
maxDuration: config.output?.gif?.maxDuration ?? 10000, // Max 10 seconds
|
|
1153
|
+
},
|
|
1154
|
+
// Video settings
|
|
1155
|
+
video: {
|
|
1156
|
+
codec: config.output?.video?.codec ?? "h264",
|
|
1157
|
+
fps: config.output?.video?.fps ?? 30,
|
|
1158
|
+
quality: config.output?.video?.quality ?? "high",
|
|
1159
|
+
},
|
|
1160
|
+
};
|
|
1161
|
+
} catch (e) {
|
|
1162
|
+
return {
|
|
1163
|
+
primaryFormat: "gif",
|
|
1164
|
+
staticFormat: "png",
|
|
1165
|
+
videoFormat: "mp4",
|
|
1166
|
+
gif: { loop: true, fps: 15, quality: "high", maxDuration: 10000 },
|
|
1167
|
+
video: { codec: "h264", fps: 30, quality: "high" },
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Update output format configuration
|
|
1174
|
+
* @param {Object} formatConfig - New format config
|
|
1175
|
+
*/
|
|
1176
|
+
function updateOutputFormatConfig(formatConfig) {
|
|
1177
|
+
const config = readConfig();
|
|
1178
|
+
config.output = {
|
|
1179
|
+
...config.output,
|
|
1180
|
+
...formatConfig,
|
|
1181
|
+
};
|
|
1182
|
+
writeConfig(config);
|
|
1183
|
+
return config.output;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
module.exports = {
|
|
1187
|
+
readSettings,
|
|
1188
|
+
writeSettings,
|
|
1189
|
+
readConfig,
|
|
1190
|
+
writeConfig,
|
|
1191
|
+
configExists,
|
|
1192
|
+
initializeProject,
|
|
1193
|
+
// Capture configuration
|
|
1194
|
+
getCaptureConfig,
|
|
1195
|
+
DEFAULT_CAPTURE_CONFIG,
|
|
1196
|
+
// Privacy configuration
|
|
1197
|
+
getPrivacyConfig,
|
|
1198
|
+
DEFAULT_PRIVACY_CONFIG,
|
|
1199
|
+
// Style configuration
|
|
1200
|
+
getStyleConfig,
|
|
1201
|
+
DEFAULT_STYLE_CONFIG,
|
|
1202
|
+
// Diffing configuration
|
|
1203
|
+
getDiffingConfig,
|
|
1204
|
+
updateDiffingConfig,
|
|
1205
|
+
// Versioning configuration
|
|
1206
|
+
getVersioningConfig,
|
|
1207
|
+
updateVersioningConfig,
|
|
1208
|
+
addVersionTag,
|
|
1209
|
+
// Output format configuration
|
|
1210
|
+
getOutputFormatConfig,
|
|
1211
|
+
updateOutputFormatConfig,
|
|
1212
|
+
// Workspace management
|
|
1213
|
+
workspaceExists,
|
|
1214
|
+
readWorkspace,
|
|
1215
|
+
writeWorkspace,
|
|
1216
|
+
createWorkspace,
|
|
1217
|
+
addScenarioToWorkspace,
|
|
1218
|
+
removeScenarioFromWorkspace,
|
|
1219
|
+
updateWorkspaceVariants,
|
|
1220
|
+
getWorkspaceWithScenarios,
|
|
1221
|
+
// Auth helpers
|
|
1222
|
+
isAuthError,
|
|
1223
|
+
createAuthErrorResponse,
|
|
1224
|
+
// Output & viewport configuration
|
|
1225
|
+
getOutputConfig,
|
|
1226
|
+
updateOutputConfig,
|
|
1227
|
+
getViewportPresetsConfig,
|
|
1228
|
+
saveViewportPreset,
|
|
1229
|
+
deleteViewportPreset,
|
|
1230
|
+
// Mode & validation
|
|
1231
|
+
getModeInfo,
|
|
1232
|
+
validateConfig,
|
|
1233
|
+
// DocSync configuration
|
|
1234
|
+
readDocSyncConfig,
|
|
1235
|
+
// Paths
|
|
1236
|
+
SETTINGS_PATH,
|
|
1237
|
+
SETTINGS_DIR,
|
|
1238
|
+
CONFIG_PATH,
|
|
1239
|
+
WORKSPACE_PATH,
|
|
1240
|
+
};
|