@reshotdev/screenshot 0.0.1-beta.1 → 0.0.1-beta.11

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 (38) hide show
  1. package/README.md +65 -7
  2. package/package.json +9 -2
  3. package/src/commands/auth.js +108 -26
  4. package/src/commands/certify.js +62 -0
  5. package/src/commands/ci-run.js +57 -2
  6. package/src/commands/ci-setup.js +5 -5
  7. package/src/commands/doctor-release.js +67 -0
  8. package/src/commands/doctor-target.js +49 -0
  9. package/src/commands/drifts.js +5 -70
  10. package/src/commands/import-tests.js +13 -13
  11. package/src/commands/ingest.js +10 -10
  12. package/src/commands/init.js +16 -277
  13. package/src/commands/publish.js +204 -237
  14. package/src/commands/pull.js +253 -23
  15. package/src/commands/run.js +292 -12
  16. package/src/commands/setup-wizard.js +277 -499
  17. package/src/commands/setup.js +41 -13
  18. package/src/commands/status.js +313 -125
  19. package/src/commands/sync.js +28 -236
  20. package/src/commands/ui.js +1 -1
  21. package/src/commands/verify-publish.js +46 -0
  22. package/src/index.js +194 -94
  23. package/src/lib/api-client.js +121 -35
  24. package/src/lib/capture-engine.js +103 -7
  25. package/src/lib/capture-script-runner.js +359 -58
  26. package/src/lib/certification.js +865 -0
  27. package/src/lib/config.js +181 -76
  28. package/src/lib/record-cdp.js +288 -16
  29. package/src/lib/record-config.js +1 -1
  30. package/src/lib/release-doctor.js +313 -0
  31. package/src/lib/run-manifest.js +103 -0
  32. package/src/lib/standalone-mode.js +1 -1
  33. package/src/lib/storage-providers.js +4 -4
  34. package/src/lib/target-contract.js +292 -0
  35. package/src/lib/ui-api.js +6 -7
  36. package/web/manager/dist/assets/{index--ZgioErz.js → index-D2qqcFNN.js} +1 -1
  37. package/web/manager/dist/index.html +1 -1
  38. package/src/commands/validate-docs.js +0 -529
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs-extra");
4
+ const path = require("path");
5
+ const config = require("./config");
6
+ const { buildRunPreflightReport } = require("../commands/run");
7
+ const { runDoctorTarget } = require("./certification");
8
+
9
+ const REPORT_DIR = path.join(process.cwd(), ".reshot", "reports");
10
+ const RELEASE_DOCTOR_REPORT_PATH = path.join(REPORT_DIR, "release-doctor.json");
11
+ const DEFAULT_DOCS_ASSET_MAP_MAX_AGE_DAYS = 30;
12
+ const RESHOT_CDN_ORIGIN = "https://cdn.reshot.dev/";
13
+
14
+ function ensureReportDir() {
15
+ fs.ensureDirSync(REPORT_DIR);
16
+ return REPORT_DIR;
17
+ }
18
+
19
+ function parseScenarioKeys(value) {
20
+ if (!value) {
21
+ return null;
22
+ }
23
+
24
+ if (Array.isArray(value)) {
25
+ return value.map((entry) => String(entry || "").trim()).filter(Boolean);
26
+ }
27
+
28
+ return String(value)
29
+ .split(",")
30
+ .map((entry) => entry.trim())
31
+ .filter(Boolean);
32
+ }
33
+
34
+ function resolveDocsAssetMapMaxAgeDays() {
35
+ const parsed = Number.parseInt(
36
+ process.env.RESHOT_DOCS_ASSET_MAP_MAX_AGE_DAYS || String(DEFAULT_DOCS_ASSET_MAP_MAX_AGE_DAYS),
37
+ 10,
38
+ );
39
+
40
+ return Number.isFinite(parsed) && parsed >= 0
41
+ ? parsed
42
+ : DEFAULT_DOCS_ASSET_MAP_MAX_AGE_DAYS;
43
+ }
44
+
45
+ function createDocsHealth(options = {}) {
46
+ return {
47
+ checked: false,
48
+ skipped: false,
49
+ ok: false,
50
+ path: null,
51
+ exportedAt: null,
52
+ ageDays: null,
53
+ maxAgeDays: resolveDocsAssetMapMaxAgeDays(),
54
+ summary: {
55
+ visuals: 0,
56
+ assets: 0,
57
+ steps: 0,
58
+ },
59
+ issues: [],
60
+ ...options,
61
+ };
62
+ }
63
+
64
+ function createDocsIssueCollector(summary, issues, groupKey, visualKey, contextKey) {
65
+ return {
66
+ checkEntry(entry) {
67
+ summary.assets += 1;
68
+
69
+ if (!entry || typeof entry !== "object") {
70
+ issues.push(`Asset map entry \"${groupKey}/${visualKey}/${contextKey}\" is invalid.`);
71
+ return;
72
+ }
73
+
74
+ if (!entry.type) {
75
+ issues.push(`Asset map entry \"${groupKey}/${visualKey}/${contextKey}\" is missing type.`);
76
+ }
77
+
78
+ if (!entry.alt) {
79
+ issues.push(`Asset map entry \"${groupKey}/${visualKey}/${contextKey}\" is missing alt text.`);
80
+ }
81
+
82
+ if (entry.src && !String(entry.src).startsWith(RESHOT_CDN_ORIGIN)) {
83
+ issues.push(
84
+ `Asset map entry \"${groupKey}/${visualKey}/${contextKey}\" does not use a direct cdn.reshot.dev URL.`,
85
+ );
86
+ }
87
+
88
+ if (entry.poster && !String(entry.poster).startsWith(RESHOT_CDN_ORIGIN)) {
89
+ issues.push(
90
+ `Asset map poster \"${groupKey}/${visualKey}/${contextKey}\" does not use a direct cdn.reshot.dev URL.`,
91
+ );
92
+ }
93
+
94
+ for (const step of entry.steps || []) {
95
+ summary.steps += 1;
96
+ if (!step?.src || !String(step.src).startsWith(RESHOT_CDN_ORIGIN)) {
97
+ issues.push(
98
+ `Asset map step \"${groupKey}/${visualKey}/${contextKey}/${step?.step || "unknown"}\" does not use a direct cdn.reshot.dev URL.`,
99
+ );
100
+ }
101
+ }
102
+ },
103
+ };
104
+ }
105
+
106
+ function inspectDocsAssetMap(assetMap, options = {}) {
107
+ const maxAgeDays = Number.isFinite(options.maxAgeDays)
108
+ ? options.maxAgeDays
109
+ : resolveDocsAssetMapMaxAgeDays();
110
+ const now = options.now || new Date();
111
+ const issues = [];
112
+ const summary = {
113
+ visuals: 0,
114
+ assets: 0,
115
+ steps: 0,
116
+ };
117
+
118
+ if (!assetMap || typeof assetMap !== "object") {
119
+ return createDocsHealth({
120
+ checked: true,
121
+ ok: false,
122
+ maxAgeDays,
123
+ issues: ["Asset map is missing or invalid."],
124
+ });
125
+ }
126
+
127
+ const exportedAt = typeof assetMap.meta?.exportedAt === "string"
128
+ ? assetMap.meta.exportedAt
129
+ : null;
130
+ const exportedAtMs = exportedAt ? Date.parse(exportedAt) : Number.NaN;
131
+ let ageDays = null;
132
+
133
+ if (!assetMap.meta?.projectId) {
134
+ issues.push("Asset map meta.projectId is missing.");
135
+ }
136
+
137
+ if (!exportedAt || Number.isNaN(exportedAtMs)) {
138
+ issues.push("Asset map meta.exportedAt is missing or invalid.");
139
+ } else {
140
+ ageDays = Math.max(0, Math.floor((now.getTime() - exportedAtMs) / 86_400_000));
141
+ if (ageDays > maxAgeDays) {
142
+ issues.push(`Asset map is stale: exported ${ageDays} day(s) ago (max ${maxAgeDays}).`);
143
+ }
144
+ }
145
+
146
+ const groups = assetMap.assets && typeof assetMap.assets === "object"
147
+ ? Object.entries(assetMap.assets)
148
+ : [];
149
+ if (groups.length === 0) {
150
+ issues.push("Asset map has no assets.");
151
+ }
152
+
153
+ for (const [groupKey, visuals] of groups) {
154
+ if (!visuals || typeof visuals !== "object") {
155
+ issues.push(`Asset map group \"${groupKey}\" is invalid.`);
156
+ continue;
157
+ }
158
+
159
+ for (const [visualKey, contexts] of Object.entries(visuals)) {
160
+ summary.visuals += 1;
161
+
162
+ if (!contexts || typeof contexts !== "object") {
163
+ issues.push(`Asset map visual \"${groupKey}/${visualKey}\" is invalid.`);
164
+ continue;
165
+ }
166
+
167
+ for (const [contextKey, entry] of Object.entries(contexts)) {
168
+ createDocsIssueCollector(summary, issues, groupKey, visualKey, contextKey)
169
+ .checkEntry(entry);
170
+ }
171
+ }
172
+ }
173
+
174
+ if (
175
+ typeof assetMap.meta?.totalVisuals === "number" &&
176
+ assetMap.meta.totalVisuals !== summary.visuals
177
+ ) {
178
+ issues.push(`Asset map meta.totalVisuals=${assetMap.meta.totalVisuals} but counted ${summary.visuals}.`);
179
+ }
180
+
181
+ if (
182
+ typeof assetMap.meta?.totalAssets === "number" &&
183
+ assetMap.meta.totalAssets !== summary.assets
184
+ ) {
185
+ issues.push(`Asset map meta.totalAssets=${assetMap.meta.totalAssets} but counted ${summary.assets}.`);
186
+ }
187
+
188
+ if (
189
+ typeof assetMap.meta?.totalSteps === "number" &&
190
+ assetMap.meta.totalSteps !== summary.steps
191
+ ) {
192
+ issues.push(`Asset map meta.totalSteps=${assetMap.meta.totalSteps} but counted ${summary.steps}.`);
193
+ }
194
+
195
+ return createDocsHealth({
196
+ checked: true,
197
+ ok: issues.length === 0,
198
+ exportedAt,
199
+ ageDays,
200
+ maxAgeDays,
201
+ summary,
202
+ issues,
203
+ });
204
+ }
205
+
206
+ function resolveDocsAssetMapCandidates(cwd = process.cwd()) {
207
+ return [
208
+ path.join(cwd, "src", "data", "reshot-assets.json"),
209
+ path.join(cwd, "app", "src", "data", "reshot-assets.json"),
210
+ ];
211
+ }
212
+
213
+ function inspectDocsAssetMapFile(options = {}) {
214
+ const cwd = options.cwd || process.cwd();
215
+ const candidates = resolveDocsAssetMapCandidates(cwd);
216
+ const assetMapPath = candidates.find((candidate) => fs.existsSync(candidate));
217
+
218
+ if (!assetMapPath) {
219
+ return createDocsHealth({
220
+ checked: false,
221
+ skipped: true,
222
+ ok: true,
223
+ issues: ["No docs asset map found; skipping docs asset verification."],
224
+ });
225
+ }
226
+
227
+ try {
228
+ const assetMap = fs.readJsonSync(assetMapPath);
229
+ return {
230
+ ...inspectDocsAssetMap(assetMap, options),
231
+ path: assetMapPath,
232
+ };
233
+ } catch (error) {
234
+ return createDocsHealth({
235
+ checked: true,
236
+ ok: false,
237
+ path: assetMapPath,
238
+ issues: [`Failed to read docs asset map: ${error.message}`],
239
+ });
240
+ }
241
+ }
242
+
243
+ async function runReleaseDoctor(options = {}) {
244
+ ensureReportDir();
245
+ const scenarioKeys = parseScenarioKeys(options.scenarioKeys || options.scenarios);
246
+ const docSyncConfig = config.readConfig();
247
+ const preflight = await buildRunPreflightReport(docSyncConfig, { scenarioKeys });
248
+ const targetDoctor = docSyncConfig.target?.tier === "certified"
249
+ ? await runDoctorTarget({ scenarioKeys })
250
+ : {
251
+ skipped: true,
252
+ ok: true,
253
+ target: docSyncConfig.target,
254
+ summary: {
255
+ overallSeverity: "info",
256
+ blockingIssues: [],
257
+ advisories: [],
258
+ info: [{ message: "Target doctor skipped for non-certified target." }],
259
+ },
260
+ };
261
+ const docsAssetMap = inspectDocsAssetMapFile({ cwd: process.cwd() });
262
+
263
+ const blockingIssues = [];
264
+ const advisories = [];
265
+
266
+ for (const error of preflight.errors || []) {
267
+ blockingIssues.push({ scope: "run-preflight", message: error });
268
+ }
269
+ for (const warning of preflight.warnings || []) {
270
+ advisories.push({ scope: "run-preflight", message: warning });
271
+ }
272
+ for (const issue of targetDoctor.summary?.blockingIssues || []) {
273
+ blockingIssues.push({ scope: "target-doctor", ...issue });
274
+ }
275
+ for (const issue of targetDoctor.summary?.advisories || []) {
276
+ advisories.push({ scope: "target-doctor", ...issue });
277
+ }
278
+ if (!docsAssetMap.skipped) {
279
+ for (const issue of docsAssetMap.issues) {
280
+ blockingIssues.push({ scope: "docs-asset-map", message: issue });
281
+ }
282
+ }
283
+
284
+ const ok = preflight.ok && targetDoctor.ok && (docsAssetMap.skipped || docsAssetMap.ok);
285
+ const report = {
286
+ type: "ReleaseDoctorReport",
287
+ stage: "doctor-release",
288
+ generatedAt: new Date().toISOString(),
289
+ ok,
290
+ scenarioKeys: scenarioKeys || null,
291
+ target: docSyncConfig.target || null,
292
+ runPreflight: preflight,
293
+ targetDoctor,
294
+ docsAssetMap,
295
+ summary: {
296
+ blockingIssues,
297
+ advisories,
298
+ },
299
+ };
300
+
301
+ await fs.writeJson(RELEASE_DOCTOR_REPORT_PATH, report, { spaces: 2 });
302
+ report.reportPath = RELEASE_DOCTOR_REPORT_PATH;
303
+ return report;
304
+ }
305
+
306
+ module.exports = {
307
+ RELEASE_DOCTOR_REPORT_PATH,
308
+ parseScenarioKeys,
309
+ resolveDocsAssetMapCandidates,
310
+ inspectDocsAssetMap,
311
+ inspectDocsAssetMapFile,
312
+ runReleaseDoctor,
313
+ };
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs-extra");
4
+ const path = require("path");
5
+
6
+ const RUN_MANIFEST_DIR = path.join(
7
+ process.cwd(),
8
+ ".reshot",
9
+ "manifests",
10
+ "runs",
11
+ );
12
+ const LATEST_RUN_MANIFEST_PATH = path.join(RUN_MANIFEST_DIR, "run-latest.json");
13
+
14
+ function ensureRunManifestDir() {
15
+ fs.ensureDirSync(RUN_MANIFEST_DIR);
16
+ return RUN_MANIFEST_DIR;
17
+ }
18
+
19
+ function normalizeScenarioResults(results = []) {
20
+ return results.map((result) => ({
21
+ key: result.key || result.scenario || null,
22
+ scenario: result.scenario || result.key || null,
23
+ success: result.success !== false,
24
+ timestamp: result.timestamp || null,
25
+ outputDir: result.outputDir || null,
26
+ variant: result.variant || null,
27
+ assetCount: Array.isArray(result.assets) ? result.assets.length : 0,
28
+ }));
29
+ }
30
+
31
+ function buildRunManifest(payload = {}) {
32
+ const generatedAt = payload.generatedAt || new Date().toISOString();
33
+ return {
34
+ type: "ReshotRunManifest",
35
+ runId: payload.runId || generatedAt.replace(/[:.]/g, "-"),
36
+ generatedAt,
37
+ success: payload.success !== false,
38
+ outputBaseDir: payload.outputBaseDir || null,
39
+ selectedScenarioKeys: payload.selectedScenarioKeys || [],
40
+ diffEnabled: Boolean(payload.diffEnabled),
41
+ scenarios: normalizeScenarioResults(payload.scenarios || []),
42
+ preflight: payload.preflight || null,
43
+ };
44
+ }
45
+
46
+ function writeRunManifest(payload = {}) {
47
+ const manifest = buildRunManifest(payload);
48
+ ensureRunManifestDir();
49
+ const manifestPath = path.join(RUN_MANIFEST_DIR, `run-${manifest.runId}.json`);
50
+ fs.writeJsonSync(manifestPath, manifest, { spaces: 2 });
51
+ fs.writeJsonSync(LATEST_RUN_MANIFEST_PATH, manifest, { spaces: 2 });
52
+ return { manifest, manifestPath, latestPath: LATEST_RUN_MANIFEST_PATH };
53
+ }
54
+
55
+ function readRunManifest(manifestPath) {
56
+ if (!manifestPath || !fs.existsSync(manifestPath)) {
57
+ return null;
58
+ }
59
+
60
+ return fs.readJsonSync(manifestPath);
61
+ }
62
+
63
+ function listRunManifestPaths() {
64
+ if (!fs.existsSync(RUN_MANIFEST_DIR)) {
65
+ return [];
66
+ }
67
+
68
+ return fs
69
+ .readdirSync(RUN_MANIFEST_DIR)
70
+ .filter(
71
+ (entry) =>
72
+ /^run-.*\.json$/.test(entry) && entry !== path.basename(LATEST_RUN_MANIFEST_PATH),
73
+ )
74
+ .map((entry) => path.join(RUN_MANIFEST_DIR, entry))
75
+ .sort((left, right) => right.localeCompare(left));
76
+ }
77
+
78
+ function getLatestSuccessfulRunManifest() {
79
+ const latest = readRunManifest(LATEST_RUN_MANIFEST_PATH);
80
+ if (latest?.success) {
81
+ return latest;
82
+ }
83
+
84
+ for (const manifestPath of listRunManifestPaths()) {
85
+ const manifest = readRunManifest(manifestPath);
86
+ if (manifest?.success) {
87
+ return manifest;
88
+ }
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ module.exports = {
95
+ RUN_MANIFEST_DIR,
96
+ LATEST_RUN_MANIFEST_PATH,
97
+ buildRunManifest,
98
+ writeRunManifest,
99
+ readRunManifest,
100
+ listRunManifestPaths,
101
+ getLatestSuccessfulRunManifest,
102
+ normalizeScenarioResults,
103
+ };
@@ -103,7 +103,7 @@ const DEFAULT_STANDALONE_SETTINGS = {
103
103
  };
104
104
 
105
105
  const SETTINGS_DIR = ".reshot";
106
- const CONFIG_PATH = "docsync.config.json";
106
+ const CONFIG_PATH = "reshot.config.json";
107
107
  const SETTINGS_PATH = path.join(SETTINGS_DIR, "settings.json");
108
108
 
109
109
  /**
@@ -114,7 +114,7 @@ ${chalk.cyan('AWS S3 Setup:')}
114
114
  ${chalk.gray('export AWS_SECRET_ACCESS_KEY="your-secret-access-key"')}
115
115
  ${chalk.gray('export AWS_REGION="us-east-1" # optional, defaults to us-east-1')}
116
116
 
117
- 3. ${chalk.yellow('Update docsync.config.json:')}
117
+ 3. ${chalk.yellow('Update reshot.config.json:')}
118
118
  ${chalk.gray(JSON.stringify({
119
119
  storage: {
120
120
  type: 's3',
@@ -143,7 +143,7 @@ ${chalk.cyan('Cloudflare R2 Setup:')}
143
143
  ${chalk.gray('export R2_ACCESS_KEY_ID="your-r2-access-key"')}
144
144
  ${chalk.gray('export R2_SECRET_ACCESS_KEY="your-r2-secret-key"')}
145
145
 
146
- 3. ${chalk.yellow('Update docsync.config.json:')}
146
+ 3. ${chalk.yellow('Update reshot.config.json:')}
147
147
  ${chalk.gray(JSON.stringify({
148
148
  storage: {
149
149
  type: 'r2',
@@ -165,7 +165,7 @@ ${chalk.cyan('Local Storage Setup:')}
165
165
 
166
166
  For local testing or self-hosted scenarios:
167
167
 
168
- 1. ${chalk.yellow('Update docsync.config.json:')}
168
+ 1. ${chalk.yellow('Update reshot.config.json:')}
169
169
  ${chalk.gray(JSON.stringify({
170
170
  storage: {
171
171
  type: 'local',
@@ -536,7 +536,7 @@ function createStorageProvider(config) {
536
536
 
537
537
  /**
538
538
  * Determine storage mode from config
539
- * @param {object} docSyncConfig - The docsync.config.json content
539
+ * @param {object} docSyncConfig - The reshot.config.json content
540
540
  * @returns {'platform'|'byos'}
541
541
  */
542
542
  function getStorageMode(docSyncConfig) {