@reshotdev/screenshot 0.0.1-beta.12 → 0.0.1-beta.14
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 +67 -22
- package/package.json +18 -14
- package/src/commands/auth.js +37 -7
- package/src/commands/capture-dom.js +50 -0
- package/src/commands/compose.js +220 -0
- package/src/commands/doctor-release.js +7 -0
- package/src/commands/doctor-target.js +36 -4
- package/src/commands/drifts.js +13 -1
- package/src/commands/publish.js +183 -21
- package/src/commands/pull.js +9 -4
- package/src/commands/refresh.js +166 -0
- package/src/commands/setup-wizard.js +57 -3
- package/src/commands/status.js +22 -2
- package/src/commands/variation.js +194 -0
- package/src/index.js +190 -10
- package/src/lib/api-client.js +61 -35
- package/src/lib/auto-update/refresh.js +598 -0
- package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
- package/src/lib/auto-update/spec.js +89 -0
- package/src/lib/capture-engine.js +76 -2
- package/src/lib/capture-script-runner.js +289 -138
- package/src/lib/certification.js +23 -1
- package/src/lib/compose-context.js +156 -0
- package/src/lib/compose-pack.js +42 -0
- package/src/lib/compose-runtime.js +34 -0
- package/src/lib/compose-upload.js +142 -0
- package/src/lib/config.js +2 -2
- package/src/lib/dom-capture.js +64 -0
- package/src/lib/ensure-browser.js +147 -0
- package/src/lib/record-clip.js +83 -3
- package/src/lib/record-config.js +0 -4
- package/src/lib/release-doctor.js +11 -3
- package/src/lib/resolve-targets.js +60 -0
- package/src/lib/run-manifest.js +45 -0
- package/src/lib/ui-api-helpers.js +118 -0
- package/src/lib/ui-api.js +28 -820
- package/src/lib/ui-asset-cleanup.js +62 -0
- package/src/lib/ui-output-versions.js +165 -0
- package/src/lib/ui-recorder-routes.js +341 -0
- package/src/lib/ui-scenario-metadata.js +161 -0
- package/vendor/compose/dist/auto-update.cjs +5544 -0
- package/vendor/compose/dist/auto-update.mjs +5518 -0
- package/vendor/compose/dist/capture.cjs +1450 -0
- package/vendor/compose/dist/capture.mjs +1416 -0
- package/vendor/compose/dist/eligibility.cjs +5331 -0
- package/vendor/compose/dist/eligibility.mjs +5313 -0
- package/vendor/compose/dist/index.cjs +2046 -0
- package/vendor/compose/dist/index.mjs +1997 -0
- package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
- package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
- package/vendor/compose/dist/jsx-runtime.cjs +58 -0
- package/vendor/compose/dist/jsx-runtime.mjs +31 -0
- package/vendor/compose/dist/render.cjs +558 -0
- package/vendor/compose/dist/render.mjs +515 -0
- package/vendor/compose/dist/verify-cli.cjs +3806 -0
- package/vendor/compose/dist/verify-cli.mjs +3812 -0
- package/vendor/compose/dist/verify.cjs +3880 -0
- package/vendor/compose/dist/verify.mjs +3858 -0
- package/web/manager/dist/assets/{index-CvleJUur.js → index-D0S2otug.js} +56 -56
- package/web/manager/dist/index.html +1 -1
- package/src/commands/ingest.js +0 -458
- package/src/commands/setup.js +0 -165
- package/src/lib/playwright-runner.js +0 -252
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function countFilesRecursive(dir) {
|
|
5
|
+
let count = 0;
|
|
6
|
+
const items = fs.readdirSync(dir);
|
|
7
|
+
|
|
8
|
+
for (const item of items) {
|
|
9
|
+
const fullPath = path.join(dir, item);
|
|
10
|
+
const stat = fs.statSync(fullPath);
|
|
11
|
+
|
|
12
|
+
if (stat.isDirectory()) {
|
|
13
|
+
count += countFilesRecursive(fullPath);
|
|
14
|
+
} else {
|
|
15
|
+
count++;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return count;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function deleteAllOutputAssets(outputDir) {
|
|
23
|
+
if (!fs.existsSync(outputDir)) {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const deletedFiles = countFilesRecursive(outputDir);
|
|
28
|
+
fs.emptyDirSync(outputDir);
|
|
29
|
+
|
|
30
|
+
return deletedFiles;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deleteScenarioAssetDirectories(outputDir, scenarioKeys, isPathWithinBase) {
|
|
34
|
+
if (!fs.existsSync(outputDir)) {
|
|
35
|
+
return { deletedScenarios: 0, deletedFiles: 0 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let deletedScenarios = 0;
|
|
39
|
+
let deletedFiles = 0;
|
|
40
|
+
|
|
41
|
+
for (const scenarioKey of scenarioKeys) {
|
|
42
|
+
const scenarioDir = path.join(outputDir, scenarioKey);
|
|
43
|
+
|
|
44
|
+
if (!isPathWithinBase(scenarioDir, outputDir)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (fs.existsSync(scenarioDir)) {
|
|
49
|
+
deletedFiles += countFilesRecursive(scenarioDir);
|
|
50
|
+
fs.removeSync(scenarioDir);
|
|
51
|
+
deletedScenarios++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { deletedScenarios, deletedFiles };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
countFilesRecursive,
|
|
60
|
+
deleteAllOutputAssets,
|
|
61
|
+
deleteScenarioAssetDirectories,
|
|
62
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const TIMESTAMP_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/;
|
|
5
|
+
const ASSET_EXTENSIONS = new Set([".png", ".gif", ".mp4", ".jpg", ".jpeg", ".webm"]);
|
|
6
|
+
|
|
7
|
+
function isTimestampFolder(name) {
|
|
8
|
+
return TIMESTAMP_DIR_PATTERN.test(name);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function countAssets(dir) {
|
|
12
|
+
let count = 0;
|
|
13
|
+
|
|
14
|
+
function walk(currentDir) {
|
|
15
|
+
try {
|
|
16
|
+
const items = fs.readdirSync(currentDir);
|
|
17
|
+
for (const item of items) {
|
|
18
|
+
const fullPath = path.join(currentDir, item);
|
|
19
|
+
const stat = fs.statSync(fullPath);
|
|
20
|
+
|
|
21
|
+
if (stat.isDirectory()) {
|
|
22
|
+
walk(fullPath);
|
|
23
|
+
} else if (ASSET_EXTENSIONS.has(path.extname(item).toLowerCase())) {
|
|
24
|
+
count++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
walk(dir);
|
|
33
|
+
return count;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function detectVariants(dir) {
|
|
37
|
+
try {
|
|
38
|
+
return fs
|
|
39
|
+
.readdirSync(dir)
|
|
40
|
+
.map((item) => {
|
|
41
|
+
const fullPath = path.join(dir, item);
|
|
42
|
+
const stat = fs.statSync(fullPath);
|
|
43
|
+
|
|
44
|
+
if (!stat.isDirectory()) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const assetCount = countAssets(fullPath);
|
|
49
|
+
return assetCount > 0 ? { name: item, assetCount } : null;
|
|
50
|
+
})
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatTimestampFolder(timestamp) {
|
|
58
|
+
const parts = timestamp.match(
|
|
59
|
+
/(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})/,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!parts) {
|
|
63
|
+
return { label: timestamp, date: timestamp };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const date = new Date(
|
|
67
|
+
`${parts[1]}-${parts[2]}-${parts[3]}T${parts[4]}:${parts[5]}:${parts[6]}`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (Number.isNaN(date.getTime())) {
|
|
71
|
+
return { label: timestamp, date: timestamp };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
label: date.toLocaleString(),
|
|
76
|
+
date: date.toISOString(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readVersionManifestMetadata(versionDir) {
|
|
81
|
+
try {
|
|
82
|
+
const manifestPath = path.join(versionDir, "manifest.json");
|
|
83
|
+
if (!fs.existsSync(manifestPath)) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const manifest = fs.readJSONSync(manifestPath);
|
|
88
|
+
const metadata = {};
|
|
89
|
+
if (manifest.privacy) metadata.privacy = manifest.privacy;
|
|
90
|
+
if (manifest.style) metadata.style = manifest.style;
|
|
91
|
+
|
|
92
|
+
return metadata;
|
|
93
|
+
} catch {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function listScenarioVersions(scenarioDir, now = new Date()) {
|
|
99
|
+
if (!fs.existsSync(scenarioDir)) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const subFolders = fs.readdirSync(scenarioDir).filter((item) => {
|
|
104
|
+
const fullPath = path.join(scenarioDir, item);
|
|
105
|
+
try {
|
|
106
|
+
return fs.statSync(fullPath).isDirectory();
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const versions = subFolders
|
|
113
|
+
.filter(isTimestampFolder)
|
|
114
|
+
.sort()
|
|
115
|
+
.reverse()
|
|
116
|
+
.map((timestamp, index) => {
|
|
117
|
+
const versionDir = path.join(scenarioDir, timestamp);
|
|
118
|
+
const variants = detectVariants(versionDir);
|
|
119
|
+
const formatted = formatTimestampFolder(timestamp);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
timestamp,
|
|
123
|
+
label: formatted.label,
|
|
124
|
+
date: formatted.date,
|
|
125
|
+
assetCount: countAssets(versionDir),
|
|
126
|
+
isLatest: index === 0,
|
|
127
|
+
variants,
|
|
128
|
+
hasVariants: variants.length > 0,
|
|
129
|
+
...readVersionManifestMetadata(versionDir),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const specialVersions = subFolders
|
|
134
|
+
.filter((folder) => folder === "latest" || folder === "default")
|
|
135
|
+
.map((folder) => {
|
|
136
|
+
const folderPath = path.join(scenarioDir, folder);
|
|
137
|
+
const assetCount = countAssets(folderPath);
|
|
138
|
+
const variants = detectVariants(folderPath);
|
|
139
|
+
|
|
140
|
+
if (assetCount === 0) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
timestamp: folder,
|
|
146
|
+
label: folder === "latest" ? "Latest" : "Default",
|
|
147
|
+
date: now.toISOString(),
|
|
148
|
+
assetCount,
|
|
149
|
+
isLatest: folder === "latest" && versions.length === 0,
|
|
150
|
+
variants,
|
|
151
|
+
hasVariants: variants.length > 0,
|
|
152
|
+
};
|
|
153
|
+
})
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
|
|
156
|
+
return [...versions, ...specialVersions];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
countAssets,
|
|
161
|
+
detectVariants,
|
|
162
|
+
formatTimestampFolder,
|
|
163
|
+
isTimestampFolder,
|
|
164
|
+
listScenarioVersions,
|
|
165
|
+
};
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
|
|
3
|
+
function isRecordableTarget(target) {
|
|
4
|
+
return (
|
|
5
|
+
target.type === "page" &&
|
|
6
|
+
!target.url.startsWith("chrome://") &&
|
|
7
|
+
!target.url.startsWith("chrome-error://") &&
|
|
8
|
+
target.url !== "about:blank"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toRecorderTab(target) {
|
|
13
|
+
const isOurUI =
|
|
14
|
+
target.url.includes("localhost:4300") ||
|
|
15
|
+
target.url.includes("127.0.0.1:4300");
|
|
16
|
+
const isChrome =
|
|
17
|
+
target.url.startsWith("chrome://") ||
|
|
18
|
+
target.url.startsWith("chrome-error://") ||
|
|
19
|
+
target.url === "about:blank";
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
id: target.id,
|
|
23
|
+
url: target.url,
|
|
24
|
+
title: target.title || target.url,
|
|
25
|
+
isOurUI,
|
|
26
|
+
isChrome,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sortRecorderTabs(a, b) {
|
|
31
|
+
if (a.isOurUI && !b.isOurUI) return 1;
|
|
32
|
+
if (!a.isOurUI && b.isOurUI) return -1;
|
|
33
|
+
if (a.isChrome && !b.isChrome) return 1;
|
|
34
|
+
if (!a.isChrome && b.isChrome) return -1;
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getChromeInstructions() {
|
|
39
|
+
return {
|
|
40
|
+
darwin:
|
|
41
|
+
'/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.reshot/chrome-debug"',
|
|
42
|
+
win32:
|
|
43
|
+
'"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%USERPROFILE%\\.reshot\\chrome-debug"',
|
|
44
|
+
linux:
|
|
45
|
+
'google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.reshot/chrome-debug"',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getRecorderServiceUnavailableResponse() {
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
status: { active: false, error: "Recorder service not available" },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function attachRecorderRoutes(app, context, deps = {}) {
|
|
57
|
+
const recordCdp = deps.recordCdp || require("./record-cdp");
|
|
58
|
+
const fileSystem = deps.fs || fs;
|
|
59
|
+
|
|
60
|
+
app.get("/api/recorder/check-chrome", async (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const endpointCheck = await recordCdp.checkCdpEndpoint("localhost", 9222);
|
|
63
|
+
|
|
64
|
+
if (!endpointCheck.available) {
|
|
65
|
+
return res.json({
|
|
66
|
+
ok: false,
|
|
67
|
+
chromeAvailable: false,
|
|
68
|
+
error: endpointCheck.error,
|
|
69
|
+
instructions: getChromeInstructions(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let targets = [];
|
|
74
|
+
try {
|
|
75
|
+
targets = await recordCdp.getCdpTargets("localhost", 9222);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// Keep this endpoint helpful even when target enumeration fails.
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const pageTargets = targets.filter((target) => target.type === "page");
|
|
81
|
+
const validTargets = pageTargets.filter(isRecordableTarget);
|
|
82
|
+
|
|
83
|
+
res.json({
|
|
84
|
+
ok: true,
|
|
85
|
+
chromeAvailable: true,
|
|
86
|
+
browserInfo: endpointCheck.info,
|
|
87
|
+
tabs: pageTargets.map((target) => ({
|
|
88
|
+
title: target.title,
|
|
89
|
+
url: target.url,
|
|
90
|
+
isValid: isRecordableTarget(target),
|
|
91
|
+
})),
|
|
92
|
+
hasValidTab: validTargets.length > 0,
|
|
93
|
+
message:
|
|
94
|
+
validTargets.length > 0
|
|
95
|
+
? `Chrome ready with ${validTargets.length} valid tab(s)`
|
|
96
|
+
: "Chrome is running but no valid tabs found. Please navigate to your application.",
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
res.json({
|
|
100
|
+
ok: false,
|
|
101
|
+
chromeAvailable: false,
|
|
102
|
+
error: error.message,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
app.get("/api/recorder/status", async (req, res, next) => {
|
|
108
|
+
try {
|
|
109
|
+
const { recorderService } = context;
|
|
110
|
+
if (!recorderService) {
|
|
111
|
+
return res.json(getRecorderServiceUnavailableResponse());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const status = recorderService.getStatus();
|
|
115
|
+
res.json({ ok: true, status });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
next(error);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
app.get("/api/recorder/steps", async (req, res, next) => {
|
|
122
|
+
try {
|
|
123
|
+
const { recorderService } = context;
|
|
124
|
+
if (!recorderService) {
|
|
125
|
+
return res.json({ ok: true, steps: [] });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const steps = recorderService.getSteps();
|
|
129
|
+
res.json({ ok: true, steps });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
next(error);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
app.get("/api/recorder/tabs", async (req, res) => {
|
|
136
|
+
try {
|
|
137
|
+
const endpointCheck = await recordCdp.checkCdpEndpoint("localhost", 9222);
|
|
138
|
+
if (!endpointCheck.available) {
|
|
139
|
+
return res.json({
|
|
140
|
+
ok: false,
|
|
141
|
+
chromeAvailable: false,
|
|
142
|
+
error: "Chrome is not running with remote debugging enabled",
|
|
143
|
+
tabs: [],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const targets = await recordCdp.getCdpTargets("localhost", 9222);
|
|
148
|
+
const tabs = targets
|
|
149
|
+
.filter((target) => target.type === "page")
|
|
150
|
+
.map(toRecorderTab)
|
|
151
|
+
.sort(sortRecorderTabs);
|
|
152
|
+
|
|
153
|
+
res.json({ ok: true, chromeAvailable: true, tabs });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("[Recorder API] Get tabs failed:", error);
|
|
156
|
+
res
|
|
157
|
+
.status(500)
|
|
158
|
+
.json({ error: error.message || "Failed to get Chrome tabs" });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
app.post("/api/recorder/start", async (req, res) => {
|
|
163
|
+
try {
|
|
164
|
+
const { recorderService } = context;
|
|
165
|
+
if (!recorderService) {
|
|
166
|
+
return res
|
|
167
|
+
.status(503)
|
|
168
|
+
.json({ error: "Recorder service not available" });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { visualKey, title, targetUrl, targetId, scenarioUrl } = req.body;
|
|
172
|
+
|
|
173
|
+
const result = await recorderService.start({
|
|
174
|
+
visualKey,
|
|
175
|
+
title,
|
|
176
|
+
targetUrl,
|
|
177
|
+
targetId,
|
|
178
|
+
scenarioUrl,
|
|
179
|
+
uiMode: true,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
res.json({ ok: true, ...result });
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("[Recorder API] Start failed:", error);
|
|
185
|
+
res
|
|
186
|
+
.status(500)
|
|
187
|
+
.json({ error: error.message || "Failed to start recording" });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
app.post("/api/recorder/stop", async (req, res) => {
|
|
192
|
+
try {
|
|
193
|
+
const { recorderService } = context;
|
|
194
|
+
if (!recorderService) {
|
|
195
|
+
return res
|
|
196
|
+
.status(503)
|
|
197
|
+
.json({ error: "Recorder service not available" });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { save = true, mergeMode = "replace" } = req.body;
|
|
201
|
+
|
|
202
|
+
const result = await recorderService.stop(save, {
|
|
203
|
+
uiMode: true,
|
|
204
|
+
mergeMode,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
res.json({ ok: true, ...result });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error("[Recorder API] Stop failed:", error);
|
|
210
|
+
res
|
|
211
|
+
.status(500)
|
|
212
|
+
.json({ error: error.message || "Failed to stop recording" });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
app.post("/api/recorder/capture", async (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
const { recorderService } = context;
|
|
219
|
+
if (!recorderService) {
|
|
220
|
+
return res
|
|
221
|
+
.status(503)
|
|
222
|
+
.json({ error: "Recorder service not available" });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const { outputFilename, areaType, selector } = req.body;
|
|
226
|
+
|
|
227
|
+
const step = await recorderService.capture({
|
|
228
|
+
outputFilename,
|
|
229
|
+
areaType: areaType || "full",
|
|
230
|
+
selector,
|
|
231
|
+
uiMode: true,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
res.json({ ok: true, step });
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error("[Recorder API] Capture failed:", error);
|
|
237
|
+
res
|
|
238
|
+
.status(500)
|
|
239
|
+
.json({ error: error.message || "Failed to capture screenshot" });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
app.delete("/api/recorder/steps/:index", async (req, res) => {
|
|
244
|
+
try {
|
|
245
|
+
const { recorderService } = context;
|
|
246
|
+
if (!recorderService) {
|
|
247
|
+
return res
|
|
248
|
+
.status(503)
|
|
249
|
+
.json({ error: "Recorder service not available" });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const index = parseInt(req.params.index, 10);
|
|
253
|
+
if (isNaN(index)) {
|
|
254
|
+
return res.status(400).json({ error: "Invalid step index" });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const result = recorderService.removeStep(index);
|
|
258
|
+
res.json({ ok: true, ...result });
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error("[Recorder API] Remove step failed:", error);
|
|
261
|
+
res.status(500).json({ error: error.message || "Failed to remove step" });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
app.post("/api/recorder/save-session", async (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const sessionPath = recordCdp.getDefaultSessionPath();
|
|
268
|
+
const result = await recordCdp.saveSessionState(sessionPath);
|
|
269
|
+
|
|
270
|
+
if (result.success) {
|
|
271
|
+
res.json({
|
|
272
|
+
ok: true,
|
|
273
|
+
path: result.path,
|
|
274
|
+
message:
|
|
275
|
+
"Session saved successfully. Captures will now use your authenticated session.",
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
res.status(400).json({
|
|
279
|
+
ok: false,
|
|
280
|
+
error: result.error,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error("[Recorder API] Save session failed:", error);
|
|
285
|
+
res
|
|
286
|
+
.status(500)
|
|
287
|
+
.json({ error: error.message || "Failed to save session" });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
app.get("/api/recorder/session-status", async (req, res) => {
|
|
292
|
+
try {
|
|
293
|
+
const sessionPath = recordCdp.getDefaultSessionPath();
|
|
294
|
+
|
|
295
|
+
if (fileSystem.existsSync(sessionPath)) {
|
|
296
|
+
const stat = fileSystem.statSync(sessionPath);
|
|
297
|
+
const ageHours = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60);
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const sessionData = fileSystem.readJsonSync(sessionPath);
|
|
301
|
+
res.json({
|
|
302
|
+
ok: true,
|
|
303
|
+
hasSession: true,
|
|
304
|
+
path: sessionPath,
|
|
305
|
+
savedAt: stat.mtime.toISOString(),
|
|
306
|
+
ageHours: Math.round(ageHours * 10) / 10,
|
|
307
|
+
cookieCount: sessionData.cookies?.length || 0,
|
|
308
|
+
originsCount: sessionData.origins?.length || 0,
|
|
309
|
+
isStale: ageHours > 24,
|
|
310
|
+
});
|
|
311
|
+
} catch (parseError) {
|
|
312
|
+
res.json({
|
|
313
|
+
ok: true,
|
|
314
|
+
hasSession: true,
|
|
315
|
+
path: sessionPath,
|
|
316
|
+
error: "Session file is corrupted",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
res.json({
|
|
321
|
+
ok: true,
|
|
322
|
+
hasSession: false,
|
|
323
|
+
message:
|
|
324
|
+
"No saved session. Use 'Save Session' in Recorder to capture your authenticated state.",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error("[Recorder API] Session status failed:", error);
|
|
329
|
+
res
|
|
330
|
+
.status(500)
|
|
331
|
+
.json({ error: error.message || "Failed to check session status" });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = {
|
|
337
|
+
attachRecorderRoutes,
|
|
338
|
+
isRecordableTarget,
|
|
339
|
+
sortRecorderTabs,
|
|
340
|
+
toRecorderTab,
|
|
341
|
+
};
|