@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.
- package/README.md +65 -7
- package/package.json +9 -2
- package/src/commands/auth.js +108 -26
- package/src/commands/certify.js +62 -0
- package/src/commands/ci-run.js +57 -2
- package/src/commands/ci-setup.js +5 -5
- package/src/commands/doctor-release.js +67 -0
- package/src/commands/doctor-target.js +49 -0
- package/src/commands/drifts.js +5 -70
- package/src/commands/import-tests.js +13 -13
- package/src/commands/ingest.js +10 -10
- package/src/commands/init.js +16 -277
- package/src/commands/publish.js +204 -237
- package/src/commands/pull.js +253 -23
- package/src/commands/run.js +292 -12
- package/src/commands/setup-wizard.js +277 -499
- package/src/commands/setup.js +41 -13
- package/src/commands/status.js +313 -125
- package/src/commands/sync.js +28 -236
- package/src/commands/ui.js +1 -1
- package/src/commands/verify-publish.js +46 -0
- package/src/index.js +194 -94
- package/src/lib/api-client.js +121 -35
- package/src/lib/capture-engine.js +103 -7
- package/src/lib/capture-script-runner.js +359 -58
- package/src/lib/certification.js +865 -0
- package/src/lib/config.js +181 -76
- package/src/lib/record-cdp.js +288 -16
- package/src/lib/record-config.js +1 -1
- package/src/lib/release-doctor.js +313 -0
- package/src/lib/run-manifest.js +103 -0
- package/src/lib/standalone-mode.js +1 -1
- package/src/lib/storage-providers.js +4 -4
- package/src/lib/target-contract.js +292 -0
- package/src/lib/ui-api.js +6 -7
- package/web/manager/dist/assets/{index--ZgioErz.js → index-D2qqcFNN.js} +1 -1
- package/web/manager/dist/index.html +1 -1
- package/src/commands/validate-docs.js +0 -529
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");
|
|
@@ -64,7 +69,7 @@ function createAuthErrorResponse(message) {
|
|
|
64
69
|
code: "AUTH_REQUIRED",
|
|
65
70
|
};
|
|
66
71
|
}
|
|
67
|
-
const CONFIG_PATH = path.join(process.cwd(), "
|
|
72
|
+
const CONFIG_PATH = path.join(process.cwd(), "reshot.config.json");
|
|
68
73
|
const WORKSPACE_PATH = path.join(process.cwd(), SETTINGS_DIR, "workspace.json");
|
|
69
74
|
|
|
70
75
|
/**
|
|
@@ -279,13 +284,19 @@ function readConfig() {
|
|
|
279
284
|
);
|
|
280
285
|
}
|
|
281
286
|
|
|
282
|
-
const
|
|
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",
|
|
@@ -470,84 +481,22 @@ function readConfig() {
|
|
|
470
481
|
}
|
|
471
482
|
}
|
|
472
483
|
|
|
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
484
|
return config;
|
|
503
485
|
}
|
|
504
486
|
|
|
505
487
|
/**
|
|
506
|
-
* Read
|
|
507
|
-
*
|
|
508
|
-
* @returns {Object}
|
|
488
|
+
* Read reshot.config.json without requiring scenarios array
|
|
489
|
+
* Used by sync and other commands that don't need scenario validation
|
|
490
|
+
* @returns {Object} Configuration
|
|
509
491
|
*/
|
|
510
|
-
function
|
|
492
|
+
function readConfigLenient() {
|
|
511
493
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
512
494
|
throw new Error(
|
|
513
495
|
`Config file not found at ${CONFIG_PATH}. Run \`reshot init\` to create one.`
|
|
514
496
|
);
|
|
515
497
|
}
|
|
516
498
|
|
|
517
|
-
|
|
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;
|
|
499
|
+
return normalizeConfigContract(fs.readJSONSync(CONFIG_PATH));
|
|
551
500
|
}
|
|
552
501
|
|
|
553
502
|
/**
|
|
@@ -555,7 +504,7 @@ function readDocSyncConfig() {
|
|
|
555
504
|
* @param {Object} config - Config object to write
|
|
556
505
|
*/
|
|
557
506
|
function writeConfig(config) {
|
|
558
|
-
fs.writeJSONSync(CONFIG_PATH, config, { spaces: 2 });
|
|
507
|
+
fs.writeJSONSync(CONFIG_PATH, normalizeConfigContract(config), { spaces: 2 });
|
|
559
508
|
}
|
|
560
509
|
|
|
561
510
|
/**
|
|
@@ -851,8 +800,6 @@ async function initializeProject(projectId, apiKey, options = {}) {
|
|
|
851
800
|
contextCount: 1,
|
|
852
801
|
features: {
|
|
853
802
|
visuals: true,
|
|
854
|
-
docs: false,
|
|
855
|
-
changelog: true,
|
|
856
803
|
},
|
|
857
804
|
},
|
|
858
805
|
};
|
|
@@ -1040,19 +987,175 @@ function getModeInfo() {
|
|
|
1040
987
|
};
|
|
1041
988
|
}
|
|
1042
989
|
|
|
990
|
+
function readSettingsSafe() {
|
|
991
|
+
try {
|
|
992
|
+
return readSettings();
|
|
993
|
+
} catch (error) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function collectTemplateVariables(value) {
|
|
999
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1000
|
+
return [];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const variables = new Set();
|
|
1004
|
+
const regex = /\{\{(\w+)\}\}/g;
|
|
1005
|
+
let match = regex.exec(value);
|
|
1006
|
+
|
|
1007
|
+
while (match) {
|
|
1008
|
+
variables.add(match[1]);
|
|
1009
|
+
match = regex.exec(value);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return [...variables];
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function hasDeterministicReadyContract(scenario) {
|
|
1016
|
+
if (!scenario || typeof scenario !== "object") {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (scenario.readySelector) {
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (scenario.ready?.selector || scenario.ready?.expression) {
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return Array.isArray(scenario.steps)
|
|
1029
|
+
? scenario.steps.some(
|
|
1030
|
+
(step) => step?.action === "waitForSelector" && step?.selector,
|
|
1031
|
+
)
|
|
1032
|
+
: false;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1043
1035
|
/**
|
|
1044
1036
|
* Validate full configuration for capture readiness
|
|
1037
|
+
* @param {Object} [options] - Validation options
|
|
1038
|
+
* @param {string[]} [options.scenarioKeys] - Restrict validation to scenario keys
|
|
1039
|
+
* @param {boolean} [options.requireReadyContract] - Enforce deterministic readiness
|
|
1045
1040
|
* @returns {Object} Validation result
|
|
1046
1041
|
*/
|
|
1047
|
-
function validateConfig() {
|
|
1042
|
+
function validateConfig(options = {}) {
|
|
1043
|
+
const { scenarioKeys = null, requireReadyContract = false } = options;
|
|
1044
|
+
|
|
1048
1045
|
try {
|
|
1049
1046
|
const config = readConfig();
|
|
1050
|
-
|
|
1047
|
+
const baseResult = validateCaptureRequirements(config);
|
|
1048
|
+
const errors = [...baseResult.errors];
|
|
1049
|
+
const warnings = baseResult.warnings.filter(
|
|
1050
|
+
(warning) => warning !== "baseUrl should start with http:// or https://",
|
|
1051
|
+
);
|
|
1052
|
+
const scenarios = Array.isArray(config.scenarios) ? config.scenarios : [];
|
|
1053
|
+
const requestedScenarioKeys = Array.isArray(scenarioKeys)
|
|
1054
|
+
? scenarioKeys.filter(Boolean)
|
|
1055
|
+
: [];
|
|
1056
|
+
const selectedScenarios = requestedScenarioKeys.length
|
|
1057
|
+
? scenarios.filter((scenario) => requestedScenarioKeys.includes(scenario.key))
|
|
1058
|
+
: scenarios;
|
|
1059
|
+
const missingScenarioKeys = requestedScenarioKeys.filter(
|
|
1060
|
+
(key) => !scenarios.some((scenario) => scenario.key === key),
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
if (missingScenarioKeys.length > 0) {
|
|
1064
|
+
errors.push(
|
|
1065
|
+
`Unknown scenario key(s): ${missingScenarioKeys.join(", ")}. ` +
|
|
1066
|
+
`Available scenarios: ${scenarios.map((scenario) => scenario.key).join(", ") || "none"}`,
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (config.baseUrl) {
|
|
1071
|
+
try {
|
|
1072
|
+
const parsedUrl = new URL(config.baseUrl);
|
|
1073
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
1074
|
+
errors.push(
|
|
1075
|
+
`baseUrl must use http:// or https:// (received ${parsedUrl.protocol})`,
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
errors.push(
|
|
1080
|
+
`baseUrl must be a valid absolute URL. Received: ${config.baseUrl}`,
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const settings = readSettingsSafe();
|
|
1086
|
+
const configuredVariables = {
|
|
1087
|
+
...(settings?.urlVariables || {}),
|
|
1088
|
+
};
|
|
1089
|
+
if (!configuredVariables.PROJECT_ID && settings?.projectId) {
|
|
1090
|
+
configuredVariables.PROJECT_ID = settings.projectId;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const unresolvedVariables = [];
|
|
1094
|
+
for (const scenario of selectedScenarios) {
|
|
1095
|
+
for (const variableName of collectTemplateVariables(scenario.url)) {
|
|
1096
|
+
if (!configuredVariables[variableName] && !process.env[variableName]) {
|
|
1097
|
+
unresolvedVariables.push(`${scenario.key}:${variableName}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (unresolvedVariables.length > 0) {
|
|
1103
|
+
errors.push(
|
|
1104
|
+
`Unresolved URL variable(s): ${unresolvedVariables.join(", ")}. ` +
|
|
1105
|
+
`Set them in .reshot/settings.json under urlVariables or as environment variables.`,
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const shouldRequireReadyContract =
|
|
1110
|
+
requireReadyContract || config.target?.tier === "certified";
|
|
1111
|
+
if (shouldRequireReadyContract) {
|
|
1112
|
+
const missingReadyContract = selectedScenarios
|
|
1113
|
+
.filter((scenario) => !hasDeterministicReadyContract(scenario))
|
|
1114
|
+
.map((scenario) => scenario.key);
|
|
1115
|
+
|
|
1116
|
+
if (missingReadyContract.length > 0) {
|
|
1117
|
+
errors.push(
|
|
1118
|
+
`Deterministic readiness is required for: ${missingReadyContract.join(", ")}. ` +
|
|
1119
|
+
`Add ready.selector, ready.expression, readySelector, or a waitForSelector step.`,
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const authScenarioCount = selectedScenarios.filter(
|
|
1125
|
+
(scenario) => scenario.captureClass === "live-auth" || scenario.requiresAuth,
|
|
1126
|
+
).length;
|
|
1127
|
+
const liveAuthScenarioCount = selectedScenarios.filter(
|
|
1128
|
+
(scenario) => scenario.captureClass === "live-auth",
|
|
1129
|
+
).length;
|
|
1130
|
+
|
|
1131
|
+
return {
|
|
1132
|
+
valid: errors.length === 0,
|
|
1133
|
+
errors,
|
|
1134
|
+
warnings,
|
|
1135
|
+
details: {
|
|
1136
|
+
baseUrl: config.baseUrl || null,
|
|
1137
|
+
selectedScenarioKeys: selectedScenarios.map((scenario) => scenario.key),
|
|
1138
|
+
missingScenarioKeys,
|
|
1139
|
+
unresolvedVariables,
|
|
1140
|
+
requiresReadyContract: shouldRequireReadyContract,
|
|
1141
|
+
authScenarioCount,
|
|
1142
|
+
liveAuthScenarioCount,
|
|
1143
|
+
},
|
|
1144
|
+
};
|
|
1051
1145
|
} catch (e) {
|
|
1052
1146
|
return {
|
|
1053
1147
|
valid: false,
|
|
1054
1148
|
errors: [e.message],
|
|
1055
1149
|
warnings: [],
|
|
1150
|
+
details: {
|
|
1151
|
+
baseUrl: null,
|
|
1152
|
+
selectedScenarioKeys: [],
|
|
1153
|
+
missingScenarioKeys: [],
|
|
1154
|
+
unresolvedVariables: [],
|
|
1155
|
+
requiresReadyContract: requireReadyContract,
|
|
1156
|
+
authScenarioCount: 0,
|
|
1157
|
+
liveAuthScenarioCount: 0,
|
|
1158
|
+
},
|
|
1056
1159
|
};
|
|
1057
1160
|
}
|
|
1058
1161
|
}
|
|
@@ -1230,8 +1333,10 @@ module.exports = {
|
|
|
1230
1333
|
// Mode & validation
|
|
1231
1334
|
getModeInfo,
|
|
1232
1335
|
validateConfig,
|
|
1233
|
-
//
|
|
1234
|
-
|
|
1336
|
+
// Lenient config read (no scenario validation)
|
|
1337
|
+
readConfigLenient,
|
|
1338
|
+
// Certified target contract helpers
|
|
1339
|
+
getCertifiedScenarioKeys,
|
|
1235
1340
|
// Paths
|
|
1236
1341
|
SETTINGS_PATH,
|
|
1237
1342
|
SETTINGS_DIR,
|
package/src/lib/record-cdp.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
const { chromium } = require("playwright");
|
|
3
3
|
const chalk = require("chalk");
|
|
4
4
|
const http = require("http");
|
|
5
|
+
const fs = require("fs-extra");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const os = require("os");
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Check if Chrome CDP endpoint is available
|
|
@@ -418,15 +421,14 @@ async function saveSessionState(outputPath) {
|
|
|
418
421
|
console.log(chalk.gray(` Sanitized cookies: ${stats.fixed} fixed, ${stats.removed} removed, ${stats.stripped} stripped`));
|
|
419
422
|
}
|
|
420
423
|
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
// Write storage state to file
|
|
427
|
-
fs.writeJsonSync(outputPath, storageState, { spaces: 2 });
|
|
424
|
+
const activePageUrl = page?.url?.() || null;
|
|
425
|
+
const artifactInfo = writeSessionArtifacts(outputPath, storageState, {
|
|
426
|
+
pageUrl: activePageUrl,
|
|
427
|
+
baseUrl: activePageUrl,
|
|
428
|
+
});
|
|
428
429
|
|
|
429
430
|
console.log(chalk.green(` ✔ Session saved to: ${outputPath}`));
|
|
431
|
+
console.log(chalk.gray(` Metadata: ${artifactInfo.metadataPath}`));
|
|
430
432
|
console.log(chalk.gray(` Cookies: ${storageState.cookies?.length || 0}`));
|
|
431
433
|
console.log(chalk.gray(` Origins with localStorage: ${storageState.origins?.length || 0}`));
|
|
432
434
|
|
|
@@ -445,11 +447,249 @@ async function saveSessionState(outputPath) {
|
|
|
445
447
|
* @returns {string}
|
|
446
448
|
*/
|
|
447
449
|
function getDefaultSessionPath() {
|
|
448
|
-
const path = require("path");
|
|
449
|
-
const os = require("os");
|
|
450
450
|
return path.join(os.homedir(), ".reshot", "session-state.json");
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
function getSessionMetadataPath(sessionPath = getDefaultSessionPath()) {
|
|
454
|
+
if (sessionPath.endsWith(".json")) {
|
|
455
|
+
return sessionPath.replace(/\.json$/i, ".meta.json");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return `${sessionPath}.meta.json`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function normalizeOrigin(value) {
|
|
462
|
+
if (!value || typeof value !== "string") {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
return new URL(value).origin.toLowerCase();
|
|
468
|
+
} catch {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function normalizeCookieDomain(domain) {
|
|
474
|
+
if (!domain || typeof domain !== "string") {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return domain.replace(/^\./, "").toLowerCase();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function extractSessionEvidence(storageState) {
|
|
482
|
+
const storageOrigins = Array.from(
|
|
483
|
+
new Set(
|
|
484
|
+
(storageState?.origins || [])
|
|
485
|
+
.map((origin) => normalizeOrigin(origin?.origin))
|
|
486
|
+
.filter(Boolean),
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const cookieDomains = Array.from(
|
|
491
|
+
new Set(
|
|
492
|
+
(storageState?.cookies || [])
|
|
493
|
+
.map((cookie) => normalizeCookieDomain(cookie?.domain))
|
|
494
|
+
.filter(Boolean),
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
return { storageOrigins, cookieDomains };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function buildSessionMetadata(options = {}) {
|
|
502
|
+
const {
|
|
503
|
+
baseUrl = null,
|
|
504
|
+
pageUrl = null,
|
|
505
|
+
storageState = null,
|
|
506
|
+
capturedAt = new Date().toISOString(),
|
|
507
|
+
} = options;
|
|
508
|
+
const evidence = extractSessionEvidence(storageState);
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
version: 1,
|
|
512
|
+
capturedAt,
|
|
513
|
+
sourceUrl: pageUrl || baseUrl || null,
|
|
514
|
+
sourceOrigin:
|
|
515
|
+
normalizeOrigin(pageUrl) ||
|
|
516
|
+
normalizeOrigin(baseUrl) ||
|
|
517
|
+
evidence.storageOrigins[0] ||
|
|
518
|
+
null,
|
|
519
|
+
storageOrigins: evidence.storageOrigins,
|
|
520
|
+
cookieDomains: evidence.cookieDomains,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function writeSessionArtifacts(sessionPath, storageState, options = {}) {
|
|
525
|
+
const metadataPath = getSessionMetadataPath(sessionPath);
|
|
526
|
+
const metadata = buildSessionMetadata({ ...options, storageState });
|
|
527
|
+
|
|
528
|
+
fs.ensureDirSync(path.dirname(sessionPath));
|
|
529
|
+
fs.writeJsonSync(sessionPath, storageState, { spaces: 2 });
|
|
530
|
+
fs.writeJsonSync(metadataPath, metadata, { spaces: 2 });
|
|
531
|
+
|
|
532
|
+
return { sessionPath, metadataPath, metadata };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function readSessionMetadata(sessionPath = getDefaultSessionPath()) {
|
|
536
|
+
const metadataPath = getSessionMetadataPath(sessionPath);
|
|
537
|
+
|
|
538
|
+
if (!fs.existsSync(metadataPath)) {
|
|
539
|
+
return {
|
|
540
|
+
exists: false,
|
|
541
|
+
metadataPath,
|
|
542
|
+
metadata: null,
|
|
543
|
+
error: null,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
return {
|
|
549
|
+
exists: true,
|
|
550
|
+
metadataPath,
|
|
551
|
+
metadata: fs.readJsonSync(metadataPath),
|
|
552
|
+
error: null,
|
|
553
|
+
};
|
|
554
|
+
} catch (error) {
|
|
555
|
+
return {
|
|
556
|
+
exists: true,
|
|
557
|
+
metadataPath,
|
|
558
|
+
metadata: null,
|
|
559
|
+
error: error.message,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function sessionHostMatchesCookieDomains(hostname, cookieDomains) {
|
|
565
|
+
if (!hostname) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return cookieDomains.some(
|
|
570
|
+
(domain) => hostname === domain || hostname.endsWith(`.${domain}`),
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function assessSessionHealth(
|
|
575
|
+
sessionPath = getDefaultSessionPath(),
|
|
576
|
+
baseUrl = null,
|
|
577
|
+
options = {},
|
|
578
|
+
) {
|
|
579
|
+
const maxAgeMinutes = Number.isFinite(options.maxAgeMinutes)
|
|
580
|
+
? options.maxAgeMinutes
|
|
581
|
+
: 360;
|
|
582
|
+
const expectedOrigin = normalizeOrigin(baseUrl);
|
|
583
|
+
const result = {
|
|
584
|
+
ok: false,
|
|
585
|
+
exists: fs.existsSync(sessionPath),
|
|
586
|
+
sessionPath,
|
|
587
|
+
metadataPath: getSessionMetadataPath(sessionPath),
|
|
588
|
+
expectedOrigin,
|
|
589
|
+
compatible: true,
|
|
590
|
+
stale: false,
|
|
591
|
+
ageMinutes: null,
|
|
592
|
+
metadata: null,
|
|
593
|
+
warnings: [],
|
|
594
|
+
issues: [],
|
|
595
|
+
evidence: {
|
|
596
|
+
sourceOrigin: null,
|
|
597
|
+
storageOrigins: [],
|
|
598
|
+
cookieDomains: [],
|
|
599
|
+
matchSource: null,
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
if (!result.exists) {
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
let storageState = null;
|
|
608
|
+
try {
|
|
609
|
+
storageState = fs.readJsonSync(sessionPath);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
result.compatible = false;
|
|
612
|
+
result.issues.push(`Cached session file is unreadable: ${error.message}`);
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const metadataInfo = readSessionMetadata(sessionPath);
|
|
617
|
+
if (metadataInfo.error) {
|
|
618
|
+
result.warnings.push(`Session metadata is unreadable: ${metadataInfo.error}`);
|
|
619
|
+
} else if (!metadataInfo.exists) {
|
|
620
|
+
result.warnings.push(
|
|
621
|
+
"Session metadata is missing; compatibility was inferred from stored cookies and origins.",
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const inferredEvidence = extractSessionEvidence(storageState);
|
|
626
|
+
const metadata = metadataInfo.metadata || null;
|
|
627
|
+
const storageOrigins =
|
|
628
|
+
metadata?.storageOrigins
|
|
629
|
+
?.map((origin) => normalizeOrigin(origin))
|
|
630
|
+
.filter(Boolean) || inferredEvidence.storageOrigins;
|
|
631
|
+
const cookieDomains =
|
|
632
|
+
metadata?.cookieDomains
|
|
633
|
+
?.map((domain) => normalizeCookieDomain(domain))
|
|
634
|
+
.filter(Boolean) || inferredEvidence.cookieDomains;
|
|
635
|
+
const sourceOrigin =
|
|
636
|
+
normalizeOrigin(metadata?.sourceOrigin) ||
|
|
637
|
+
normalizeOrigin(metadata?.sourceUrl) ||
|
|
638
|
+
inferredEvidence.storageOrigins[0] ||
|
|
639
|
+
null;
|
|
640
|
+
|
|
641
|
+
result.metadata = metadata;
|
|
642
|
+
result.evidence = {
|
|
643
|
+
sourceOrigin,
|
|
644
|
+
storageOrigins,
|
|
645
|
+
cookieDomains,
|
|
646
|
+
matchSource: null,
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const capturedAtMs = Date.parse(metadata?.capturedAt || "");
|
|
650
|
+
const sessionStat = fs.statSync(sessionPath);
|
|
651
|
+
const referenceTime = Number.isFinite(capturedAtMs)
|
|
652
|
+
? capturedAtMs
|
|
653
|
+
: sessionStat.mtimeMs;
|
|
654
|
+
result.ageMinutes = Math.max(
|
|
655
|
+
0,
|
|
656
|
+
Math.round((Date.now() - referenceTime) / 60000),
|
|
657
|
+
);
|
|
658
|
+
result.stale = result.ageMinutes >= maxAgeMinutes;
|
|
659
|
+
|
|
660
|
+
if (expectedOrigin) {
|
|
661
|
+
const expectedHost = new URL(baseUrl).hostname.toLowerCase();
|
|
662
|
+
const matchesSourceOrigin = sourceOrigin === expectedOrigin;
|
|
663
|
+
const matchesStorageOrigin = storageOrigins.includes(expectedOrigin);
|
|
664
|
+
const matchesCookieDomain = sessionHostMatchesCookieDomains(
|
|
665
|
+
expectedHost,
|
|
666
|
+
cookieDomains,
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
result.evidence.matchSource = matchesSourceOrigin
|
|
670
|
+
? "sourceOrigin"
|
|
671
|
+
: matchesStorageOrigin
|
|
672
|
+
? "storageOrigins"
|
|
673
|
+
: matchesCookieDomain
|
|
674
|
+
? "cookieDomains"
|
|
675
|
+
: null;
|
|
676
|
+
|
|
677
|
+
const hasEvidence =
|
|
678
|
+
Boolean(sourceOrigin) ||
|
|
679
|
+
storageOrigins.length > 0 ||
|
|
680
|
+
cookieDomains.length > 0;
|
|
681
|
+
if (hasEvidence && !result.evidence.matchSource) {
|
|
682
|
+
result.compatible = false;
|
|
683
|
+
result.issues.push(
|
|
684
|
+
`Cached session targets ${sourceOrigin || storageOrigins[0] || cookieDomains[0]}, not ${expectedOrigin}.`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
result.ok = result.compatible && result.issues.length === 0;
|
|
690
|
+
return result;
|
|
691
|
+
}
|
|
692
|
+
|
|
453
693
|
/**
|
|
454
694
|
* Quietly check if CDP is available and sync the session if so.
|
|
455
695
|
* This is called automatically before captures to use the live browser session.
|
|
@@ -459,13 +699,24 @@ function getDefaultSessionPath() {
|
|
|
459
699
|
*/
|
|
460
700
|
async function autoSyncSessionFromCDP(outputPath = null, logger = null) {
|
|
461
701
|
const log = logger || (() => {}); // Quiet by default
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
702
|
+
|
|
703
|
+
// Allow disabling CDP sync via env var (useful when session was created programmatically)
|
|
704
|
+
if (process.env.RESHOT_SKIP_CDP_SYNC === "1") {
|
|
705
|
+
return { synced: false, reason: "disabled" };
|
|
706
|
+
}
|
|
707
|
+
|
|
466
708
|
const sessionPath = outputPath || path.join(os.homedir(), ".reshot", "session-state.json");
|
|
467
|
-
|
|
709
|
+
|
|
468
710
|
try {
|
|
711
|
+
// Skip sync if cached session is very recent (likely just generated programmatically)
|
|
712
|
+
if (fs.existsSync(sessionPath)) {
|
|
713
|
+
const cachedAge = Date.now() - fs.statSync(sessionPath).mtimeMs;
|
|
714
|
+
if (cachedAge < 5 * 60 * 1000) {
|
|
715
|
+
log(chalk.gray(" → Cached session is fresh (<5min), skipping CDP sync"));
|
|
716
|
+
return { synced: false, reason: "cached_fresh" };
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
469
720
|
// Step 1: Check if CDP endpoint is available (quietly)
|
|
470
721
|
const endpointCheck = await checkCdpEndpoint("localhost", 9222);
|
|
471
722
|
|
|
@@ -513,9 +764,25 @@ async function autoSyncSessionFromCDP(outputPath = null, logger = null) {
|
|
|
513
764
|
return { synced: false, reason: "empty_session" };
|
|
514
765
|
}
|
|
515
766
|
|
|
767
|
+
const activePage =
|
|
768
|
+
context
|
|
769
|
+
.pages()
|
|
770
|
+
.find((candidate) => {
|
|
771
|
+
const currentUrl = candidate.url();
|
|
772
|
+
return (
|
|
773
|
+
currentUrl &&
|
|
774
|
+
!currentUrl.startsWith("chrome://") &&
|
|
775
|
+
!currentUrl.startsWith("devtools://") &&
|
|
776
|
+
!currentUrl.startsWith("chrome-extension://")
|
|
777
|
+
);
|
|
778
|
+
}) || context.pages()[0] || null;
|
|
779
|
+
const activePageUrl = activePage?.url?.() || null;
|
|
780
|
+
|
|
516
781
|
// Step 4: Save to file
|
|
517
|
-
|
|
518
|
-
|
|
782
|
+
writeSessionArtifacts(sessionPath, storageState, {
|
|
783
|
+
pageUrl: activePageUrl,
|
|
784
|
+
baseUrl: activePageUrl,
|
|
785
|
+
});
|
|
519
786
|
|
|
520
787
|
log(chalk.green(` ✔ Auto-synced session from CDP browser`));
|
|
521
788
|
log(chalk.gray(` Cookies: ${storageState.cookies?.length || 0}, localStorage origins: ${storageState.origins?.length || 0}`));
|
|
@@ -607,6 +874,11 @@ module.exports = {
|
|
|
607
874
|
printChromeInstructions,
|
|
608
875
|
saveSessionState,
|
|
609
876
|
getDefaultSessionPath,
|
|
877
|
+
getSessionMetadataPath,
|
|
610
878
|
autoSyncSessionFromCDP,
|
|
611
879
|
sanitizeStorageState,
|
|
880
|
+
buildSessionMetadata,
|
|
881
|
+
writeSessionArtifacts,
|
|
882
|
+
readSessionMetadata,
|
|
883
|
+
assessSessionHealth,
|
|
612
884
|
};
|
package/src/lib/record-config.js
CHANGED
|
@@ -600,7 +600,7 @@ async function finalizeScenarioAndWriteConfig(
|
|
|
600
600
|
|
|
601
601
|
console.log(
|
|
602
602
|
chalk.green(
|
|
603
|
-
"\n✔
|
|
603
|
+
"\n✔ reshot.config.json has been updated. Please review and commit the changes to your repository.\n"
|
|
604
604
|
)
|
|
605
605
|
);
|
|
606
606
|
console.log(chalk.gray(`Scenario: ${result.scenarioName}`));
|