@reshotdev/screenshot 0.0.1-beta.8 → 0.0.1-beta.9

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/src/lib/config.js CHANGED
@@ -21,6 +21,11 @@ const {
21
21
  getConfigDefaults,
22
22
  validateCaptureRequirements,
23
23
  } = require("./standalone-mode");
24
+ const {
25
+ normalizeConfigContract,
26
+ validateNormalizedConfig,
27
+ getCertifiedScenarioKeys,
28
+ } = require("./target-contract");
24
29
 
25
30
  const SETTINGS_DIR = ".reshot";
26
31
  const SETTINGS_PATH = path.join(process.cwd(), SETTINGS_DIR, "settings.json");
@@ -279,13 +284,19 @@ function readConfig() {
279
284
  );
280
285
  }
281
286
 
282
- const config = fs.readJSONSync(CONFIG_PATH);
287
+ const rawConfig = fs.readJSONSync(CONFIG_PATH);
288
+ const config = normalizeConfigContract(rawConfig);
283
289
 
284
290
  // Validate required fields
285
291
  if (!config.scenarios || !Array.isArray(config.scenarios)) {
286
292
  throw new Error('Config must have a "scenarios" array');
287
293
  }
288
294
 
295
+ const targetValidation = validateNormalizedConfig(config);
296
+ if (!targetValidation.valid) {
297
+ throw new Error(targetValidation.errors.join("\n"));
298
+ }
299
+
289
300
  const validActions = [
290
301
  "click",
291
302
  "type",
@@ -485,7 +496,7 @@ function readConfigLenient() {
485
496
  );
486
497
  }
487
498
 
488
- return fs.readJSONSync(CONFIG_PATH);
499
+ return normalizeConfigContract(fs.readJSONSync(CONFIG_PATH));
489
500
  }
490
501
 
491
502
  /**
@@ -493,7 +504,7 @@ function readConfigLenient() {
493
504
  * @param {Object} config - Config object to write
494
505
  */
495
506
  function writeConfig(config) {
496
- fs.writeJSONSync(CONFIG_PATH, config, { spaces: 2 });
507
+ fs.writeJSONSync(CONFIG_PATH, normalizeConfigContract(config), { spaces: 2 });
497
508
  }
498
509
 
499
510
  /**
@@ -1168,6 +1179,8 @@ module.exports = {
1168
1179
  validateConfig,
1169
1180
  // Lenient config read (no scenario validation)
1170
1181
  readConfigLenient,
1182
+ // Certified target contract helpers
1183
+ getCertifiedScenarioKeys,
1171
1184
  // Paths
1172
1185
  SETTINGS_PATH,
1173
1186
  SETTINGS_DIR,
@@ -462,10 +462,24 @@ async function autoSyncSessionFromCDP(outputPath = null, logger = null) {
462
462
  const fs = require("fs-extra");
463
463
  const path = require("path");
464
464
  const os = require("os");
465
-
465
+
466
+ // Allow disabling CDP sync via env var (useful when session was created programmatically)
467
+ if (process.env.RESHOT_SKIP_CDP_SYNC === "1") {
468
+ return { synced: false, reason: "disabled" };
469
+ }
470
+
466
471
  const sessionPath = outputPath || path.join(os.homedir(), ".reshot", "session-state.json");
467
-
472
+
468
473
  try {
474
+ // Skip sync if cached session is very recent (likely just generated programmatically)
475
+ if (fs.existsSync(sessionPath)) {
476
+ const cachedAge = Date.now() - fs.statSync(sessionPath).mtimeMs;
477
+ if (cachedAge < 5 * 60 * 1000) {
478
+ log(chalk.gray(" → Cached session is fresh (<5min), skipping CDP sync"));
479
+ return { synced: false, reason: "cached_fresh" };
480
+ }
481
+ }
482
+
469
483
  // Step 1: Check if CDP endpoint is available (quietly)
470
484
  const endpointCheck = await checkCdpEndpoint("localhost", 9222);
471
485
 
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+
3
+ const TARGET_TIERS = new Set(["certified", "candidate", "custom"]);
4
+ const TARGET_AUTH_MODES = new Set(["public", "fixture", "live-auth"]);
5
+ const SCENARIO_CAPTURE_CLASSES = new Set([
6
+ "public",
7
+ "fixture-auth",
8
+ "live-auth",
9
+ ]);
10
+ const PUBLISH_POLICIES = new Set(["required", "optional"]);
11
+
12
+ function normalizeTargetTier(value) {
13
+ const normalized = String(value || "custom").trim().toLowerCase();
14
+ return TARGET_TIERS.has(normalized) ? normalized : "custom";
15
+ }
16
+
17
+ function normalizeTargetAuthMode(value) {
18
+ const normalized = String(value || "public").trim().toLowerCase();
19
+ if (normalized === "fixture-auth") return "fixture";
20
+ if (normalized === "authenticated" || normalized === "auth") {
21
+ return "live-auth";
22
+ }
23
+ return TARGET_AUTH_MODES.has(normalized) ? normalized : "public";
24
+ }
25
+
26
+ function normalizeScenarioCaptureClass(value, requiresAuth, defaultAuthMode) {
27
+ const normalized = String(value || "").trim().toLowerCase();
28
+
29
+ if (!normalized) {
30
+ if (requiresAuth) {
31
+ return defaultAuthMode === "fixture" ? "fixture-auth" : "live-auth";
32
+ }
33
+ return "public";
34
+ }
35
+
36
+ if (
37
+ normalized === "fixture" ||
38
+ normalized === "docs-fixture" ||
39
+ normalized === "fixture-auth"
40
+ ) {
41
+ return "fixture-auth";
42
+ }
43
+
44
+ if (
45
+ normalized === "live-auth" ||
46
+ normalized === "auth" ||
47
+ normalized === "authenticated"
48
+ ) {
49
+ return "live-auth";
50
+ }
51
+
52
+ if (
53
+ normalized === "public" ||
54
+ normalized === "public-docs" ||
55
+ normalized === "public-explorer"
56
+ ) {
57
+ return "public";
58
+ }
59
+
60
+ return requiresAuth
61
+ ? defaultAuthMode === "fixture"
62
+ ? "fixture-auth"
63
+ : "live-auth"
64
+ : "public";
65
+ }
66
+
67
+ function normalizeReadyContract(scenario) {
68
+ const ready = scenario.ready && typeof scenario.ready === "object"
69
+ ? scenario.ready
70
+ : {};
71
+ const waitForReady =
72
+ scenario.waitForReady && typeof scenario.waitForReady === "object"
73
+ ? scenario.waitForReady
74
+ : {};
75
+
76
+ const selector =
77
+ ready.selector ||
78
+ scenario.readySelector ||
79
+ waitForReady.selector ||
80
+ null;
81
+ const expression =
82
+ ready.expression ||
83
+ scenario.readyExpression ||
84
+ waitForReady.expression ||
85
+ null;
86
+ const timeout =
87
+ ready.timeout ||
88
+ waitForReady.timeout ||
89
+ scenario.readyTimeout ||
90
+ null;
91
+
92
+ if (!selector && !expression && !timeout) {
93
+ return null;
94
+ }
95
+
96
+ return {
97
+ ...(selector ? { selector } : {}),
98
+ ...(expression ? { expression } : {}),
99
+ ...(timeout ? { timeout } : {}),
100
+ };
101
+ }
102
+
103
+ function normalizeStringArray(value) {
104
+ if (!Array.isArray(value)) {
105
+ return [];
106
+ }
107
+
108
+ return value
109
+ .map((item) => String(item || "").trim())
110
+ .filter(Boolean);
111
+ }
112
+
113
+ function inferExpectedArtifacts(scenario) {
114
+ return (scenario.steps || [])
115
+ .filter((step) => step.action === "screenshot" && step.key)
116
+ .map((step) => String(step.key));
117
+ }
118
+
119
+ function normalizePublishPolicy(value) {
120
+ const normalized = String(value || "required").trim().toLowerCase();
121
+ return PUBLISH_POLICIES.has(normalized) ? normalized : "required";
122
+ }
123
+
124
+ function normalizeScenarioContract(scenario, target) {
125
+ const defaultAuthMode = target?.defaultAuthMode || "public";
126
+ const captureClass = normalizeScenarioCaptureClass(
127
+ scenario.captureClass,
128
+ Boolean(scenario.requiresAuth),
129
+ defaultAuthMode,
130
+ );
131
+ const ready = normalizeReadyContract(scenario);
132
+ const requiredSelectors = normalizeStringArray(scenario.requiredSelectors);
133
+ const readySelector = ready?.selector || null;
134
+ const expectedArtifacts = normalizeStringArray(scenario.expectedArtifacts);
135
+
136
+ return {
137
+ ...scenario,
138
+ captureClass,
139
+ requiresAuth: captureClass !== "public",
140
+ ready,
141
+ readySelector: readySelector || undefined,
142
+ readyExpression: ready?.expression || undefined,
143
+ readyTimeout: ready?.timeout || scenario.readyTimeout,
144
+ waitForReady: ready || scenario.waitForReady || null,
145
+ requiredRoutes: normalizeStringArray(
146
+ scenario.requiredRoutes && scenario.requiredRoutes.length > 0
147
+ ? scenario.requiredRoutes
148
+ : scenario.url
149
+ ? [scenario.url]
150
+ : [],
151
+ ),
152
+ requiredSelectors:
153
+ requiredSelectors.length > 0
154
+ ? requiredSelectors
155
+ : readySelector
156
+ ? [readySelector]
157
+ : [],
158
+ needsWorkspaceInjection:
159
+ scenario.needsWorkspaceInjection !== undefined
160
+ ? Boolean(scenario.needsWorkspaceInjection)
161
+ : captureClass !== "public",
162
+ expectedArtifacts:
163
+ expectedArtifacts.length > 0 ? expectedArtifacts : inferExpectedArtifacts(scenario),
164
+ publishPolicy: normalizePublishPolicy(scenario.publishPolicy),
165
+ };
166
+ }
167
+
168
+ function normalizeTargetBlock(config) {
169
+ const raw = config.target && typeof config.target === "object"
170
+ ? config.target
171
+ : {};
172
+
173
+ const fixture =
174
+ raw.fixture && typeof raw.fixture === "object" ? raw.fixture : {};
175
+
176
+ return {
177
+ key: String(raw.key || config.name || config.projectId || "local-target"),
178
+ displayName: String(raw.displayName || raw.key || config.name || "Local Target"),
179
+ tier: normalizeTargetTier(raw.tier),
180
+ owner: String(raw.owner || "local"),
181
+ baseUrl: String(raw.baseUrl || config.baseUrl || "").replace(/\/+$/, ""),
182
+ captureSafe: Boolean(raw.captureSafe),
183
+ defaultAuthMode: normalizeTargetAuthMode(raw.defaultAuthMode),
184
+ supportedLocalCommand: String(
185
+ raw.supportedLocalCommand ||
186
+ raw.localServerCommand ||
187
+ raw.startCommand ||
188
+ "",
189
+ ).trim() || null,
190
+ fixture:
191
+ fixture.command || fixture.script || fixture.healthUrl
192
+ ? {
193
+ ...(fixture.command ? { command: String(fixture.command) } : {}),
194
+ ...(fixture.script ? { script: String(fixture.script) } : {}),
195
+ ...(fixture.healthUrl ? { healthUrl: String(fixture.healthUrl) } : {}),
196
+ }
197
+ : null,
198
+ requiredEnv: normalizeStringArray(raw.requiredEnv),
199
+ certificationScenarioKeys: normalizeStringArray(
200
+ raw.certificationScenarioKeys || raw.certifiedScenarios || [],
201
+ ),
202
+ };
203
+ }
204
+
205
+ function normalizeConfigContract(config) {
206
+ const target = normalizeTargetBlock(config);
207
+ const scenarios = Array.isArray(config.scenarios)
208
+ ? config.scenarios.map((scenario) => normalizeScenarioContract(scenario, target))
209
+ : [];
210
+
211
+ return {
212
+ ...config,
213
+ target,
214
+ scenarios,
215
+ };
216
+ }
217
+
218
+ function validateNormalizedConfig(config) {
219
+ const errors = [];
220
+
221
+ if (!config.target || typeof config.target !== "object") {
222
+ return {
223
+ valid: true,
224
+ errors,
225
+ };
226
+ }
227
+
228
+ if (!TARGET_TIERS.has(config.target.tier)) {
229
+ errors.push(`Invalid target tier "${config.target.tier}"`);
230
+ }
231
+
232
+ if (!TARGET_AUTH_MODES.has(config.target.defaultAuthMode)) {
233
+ errors.push(`Invalid target.defaultAuthMode "${config.target.defaultAuthMode}"`);
234
+ }
235
+
236
+ for (const scenario of config.scenarios || []) {
237
+ if (!SCENARIO_CAPTURE_CLASSES.has(scenario.captureClass)) {
238
+ errors.push(
239
+ `Scenario "${scenario.key}" has invalid captureClass "${scenario.captureClass}"`,
240
+ );
241
+ }
242
+ if (!PUBLISH_POLICIES.has(scenario.publishPolicy)) {
243
+ errors.push(
244
+ `Scenario "${scenario.key}" has invalid publishPolicy "${scenario.publishPolicy}"`,
245
+ );
246
+ }
247
+ }
248
+
249
+ return {
250
+ valid: errors.length === 0,
251
+ errors,
252
+ };
253
+ }
254
+
255
+ function getCertifiedScenarioKeys(config, explicitScenarioKeys = null) {
256
+ if (Array.isArray(explicitScenarioKeys) && explicitScenarioKeys.length > 0) {
257
+ return explicitScenarioKeys;
258
+ }
259
+
260
+ const configured = normalizeStringArray(config?.target?.certificationScenarioKeys);
261
+ if (configured.length > 0) {
262
+ return configured;
263
+ }
264
+
265
+ return (config?.scenarios || []).map((scenario) => scenario.key);
266
+ }
267
+
268
+ module.exports = {
269
+ TARGET_TIERS,
270
+ TARGET_AUTH_MODES,
271
+ SCENARIO_CAPTURE_CLASSES,
272
+ PUBLISH_POLICIES,
273
+ normalizeConfigContract,
274
+ normalizeTargetBlock,
275
+ normalizeScenarioContract,
276
+ validateNormalizedConfig,
277
+ getCertifiedScenarioKeys,
278
+ };