@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,303 @@
|
|
|
1
|
+
// pull.js - Pull asset map from Reshot platform
|
|
2
|
+
// Generates JSON, TypeScript, or CSV files for consumption in build pipelines
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const fs = require("fs-extra");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const config = require("../lib/config");
|
|
7
|
+
const apiClient = require("../lib/api-client");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a string to camelCase for TypeScript keys
|
|
11
|
+
*/
|
|
12
|
+
function toCamelCase(str) {
|
|
13
|
+
return str
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
|
|
16
|
+
.replace(/^[A-Z]/, (chr) => chr.toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate TypeScript code from the assets object
|
|
21
|
+
* This provides type safety - if a visual is deleted, the build will fail
|
|
22
|
+
*/
|
|
23
|
+
function generateTypeScript(assets, includeMetadata = false) {
|
|
24
|
+
const lines = [
|
|
25
|
+
"// GENERATED BY RESHOT - DO NOT EDIT",
|
|
26
|
+
"// Run 'npx reshot pull' to regenerate this file",
|
|
27
|
+
"",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
if (includeMetadata) {
|
|
31
|
+
// Full export with all metadata (src, width, height, type, alt)
|
|
32
|
+
lines.push("export const ReshotVisuals = {");
|
|
33
|
+
|
|
34
|
+
for (const [group, visuals] of Object.entries(assets)) {
|
|
35
|
+
lines.push(` ${group}: {`);
|
|
36
|
+
|
|
37
|
+
for (const [visualKey, variants] of Object.entries(visuals)) {
|
|
38
|
+
lines.push(` ${visualKey}: {`);
|
|
39
|
+
|
|
40
|
+
for (const [variant, asset] of Object.entries(variants)) {
|
|
41
|
+
lines.push(` ${variant}: {`);
|
|
42
|
+
lines.push(` src: "${asset.src}",`);
|
|
43
|
+
lines.push(` type: "${asset.type}",`);
|
|
44
|
+
if (asset.width) lines.push(` width: ${asset.width},`);
|
|
45
|
+
if (asset.height) lines.push(` height: ${asset.height},`);
|
|
46
|
+
lines.push(` alt: "${asset.alt.replace(/"/g, '\\"')}",`);
|
|
47
|
+
if (asset.poster) lines.push(` poster: "${asset.poster}",`);
|
|
48
|
+
lines.push(` },`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
lines.push(` },`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
lines.push(` },`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
lines.push("} as const;");
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push("export type ReshotVisualKey = keyof typeof ReshotVisuals;");
|
|
60
|
+
} else {
|
|
61
|
+
// Simplified export - just URLs for easy access
|
|
62
|
+
lines.push("export const ReshotVisuals = {");
|
|
63
|
+
|
|
64
|
+
for (const [group, visuals] of Object.entries(assets)) {
|
|
65
|
+
lines.push(` ${group}: {`);
|
|
66
|
+
|
|
67
|
+
for (const [visualKey, variants] of Object.entries(visuals)) {
|
|
68
|
+
// If there's only a default variant, flatten it
|
|
69
|
+
if (Object.keys(variants).length === 1 && variants.default) {
|
|
70
|
+
lines.push(` ${visualKey}: "${variants.default.src}",`);
|
|
71
|
+
} else {
|
|
72
|
+
lines.push(` ${visualKey}: {`);
|
|
73
|
+
for (const [variant, asset] of Object.entries(variants)) {
|
|
74
|
+
lines.push(` ${variant}: "${asset.src}",`);
|
|
75
|
+
}
|
|
76
|
+
lines.push(` },`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
lines.push(` },`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
lines.push("} as const;");
|
|
84
|
+
lines.push("");
|
|
85
|
+
lines.push("export type ReshotVisualKey = keyof typeof ReshotVisuals;");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return lines.join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Main pull command
|
|
93
|
+
* @param {Object} options - Command options
|
|
94
|
+
* @param {string} options.format - Output format: 'json', 'ts', 'csv'
|
|
95
|
+
* @param {string} options.output - Output file path
|
|
96
|
+
* @param {boolean} options.full - Include full metadata (for ts format)
|
|
97
|
+
* @param {string} options.status - Filter by status: 'approved', 'pending', 'all'
|
|
98
|
+
*/
|
|
99
|
+
async function pullCommand(options = {}) {
|
|
100
|
+
const {
|
|
101
|
+
format = "json",
|
|
102
|
+
output = null,
|
|
103
|
+
full = false,
|
|
104
|
+
status = "approved",
|
|
105
|
+
} = options;
|
|
106
|
+
|
|
107
|
+
console.log(chalk.blue("⬇ Pulling asset map from Reshot...\n"));
|
|
108
|
+
|
|
109
|
+
// Load project configuration
|
|
110
|
+
let projectConfig;
|
|
111
|
+
try {
|
|
112
|
+
projectConfig = config.readConfig();
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error(
|
|
115
|
+
chalk.red("Error: No docsync.config.json found. Run 'reshot init' first.")
|
|
116
|
+
);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const projectId = projectConfig._metadata?.projectId || projectConfig.projectId;
|
|
121
|
+
if (!projectId) {
|
|
122
|
+
console.error(
|
|
123
|
+
chalk.red("Error: No projectId found in docsync.config.json")
|
|
124
|
+
);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check authentication
|
|
129
|
+
let settings;
|
|
130
|
+
try {
|
|
131
|
+
settings = config.readSettings();
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.error(
|
|
134
|
+
chalk.red("Error: Not authenticated. Run 'reshot auth' first.")
|
|
135
|
+
);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
if (!settings?.apiKey) {
|
|
139
|
+
console.error(
|
|
140
|
+
chalk.red("Error: Not authenticated. Run 'reshot auth' first.")
|
|
141
|
+
);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// Fetch the asset map from the API
|
|
147
|
+
console.log(chalk.gray(` Fetching assets for project: ${projectId}`));
|
|
148
|
+
console.log(chalk.gray(` Status filter: ${status}`));
|
|
149
|
+
|
|
150
|
+
const response = await apiClient.exportVisuals(projectId, {
|
|
151
|
+
format: "json", // Always fetch JSON, we'll transform locally
|
|
152
|
+
status,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!response || !response.assets) {
|
|
156
|
+
console.error(chalk.red("Error: Invalid response from API"));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { meta, assets } = response;
|
|
161
|
+
const assetCount = Object.values(assets).reduce(
|
|
162
|
+
(count, group) => count + Object.keys(group).length,
|
|
163
|
+
0
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
console.log(chalk.green(` ✓ Fetched ${assetCount} visuals\n`));
|
|
167
|
+
|
|
168
|
+
// Determine output path
|
|
169
|
+
let outputPath = output;
|
|
170
|
+
if (!outputPath) {
|
|
171
|
+
// Default paths based on format
|
|
172
|
+
const defaultPaths = {
|
|
173
|
+
json: "src/data/reshot-assets.json",
|
|
174
|
+
ts: "src/data/visuals.ts",
|
|
175
|
+
csv: "reshot-assets.csv",
|
|
176
|
+
};
|
|
177
|
+
outputPath = defaultPaths[format] || "reshot-assets.json";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Ensure the output directory exists
|
|
181
|
+
const outputDir = path.dirname(path.resolve(outputPath));
|
|
182
|
+
await fs.ensureDir(outputDir);
|
|
183
|
+
|
|
184
|
+
// Generate output based on format
|
|
185
|
+
let content;
|
|
186
|
+
let contentType;
|
|
187
|
+
|
|
188
|
+
switch (format) {
|
|
189
|
+
case "ts":
|
|
190
|
+
content = generateTypeScript(assets, full);
|
|
191
|
+
contentType = "TypeScript";
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case "csv":
|
|
195
|
+
// Generate CSV format
|
|
196
|
+
const headers = ["Key", "Group", "Name", "Context", "Type", "Unbreakable_URL", "Width", "Height"];
|
|
197
|
+
const rows = [headers.join(",")];
|
|
198
|
+
|
|
199
|
+
for (const [group, visuals] of Object.entries(assets)) {
|
|
200
|
+
for (const [visualKey, variants] of Object.entries(visuals)) {
|
|
201
|
+
for (const [variant, asset] of Object.entries(variants)) {
|
|
202
|
+
const type = asset.type.split("/")[0]; // 'image' or 'video'
|
|
203
|
+
rows.push(
|
|
204
|
+
[
|
|
205
|
+
`${group}/${visualKey}`,
|
|
206
|
+
group,
|
|
207
|
+
visualKey,
|
|
208
|
+
variant,
|
|
209
|
+
type,
|
|
210
|
+
asset.src,
|
|
211
|
+
asset.width || "",
|
|
212
|
+
asset.height || "",
|
|
213
|
+
].join(",")
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
content = rows.join("\n");
|
|
220
|
+
contentType = "CSV";
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case "json":
|
|
224
|
+
default:
|
|
225
|
+
content = JSON.stringify({ meta, assets }, null, 2);
|
|
226
|
+
contentType = "JSON";
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Write the output file
|
|
231
|
+
await fs.writeFile(path.resolve(outputPath), content, "utf-8");
|
|
232
|
+
|
|
233
|
+
console.log(chalk.green(`✓ Generated ${contentType} file: ${outputPath}`));
|
|
234
|
+
console.log(chalk.gray(` ${assetCount} visuals exported\n`));
|
|
235
|
+
|
|
236
|
+
// Show format-specific usage instructions
|
|
237
|
+
if (format === "ts") {
|
|
238
|
+
console.log(chalk.blue("━━━ TypeScript Usage ━━━\n"));
|
|
239
|
+
console.log(chalk.white("Import in your React/Next.js component:\n"));
|
|
240
|
+
console.log(chalk.cyan(' import { ReshotVisuals } from "./visuals";\n'));
|
|
241
|
+
console.log(chalk.white("Use with full autocomplete support:\n"));
|
|
242
|
+
console.log(chalk.cyan(" // Single context (flattened)"));
|
|
243
|
+
console.log(chalk.cyan(" <img src={ReshotVisuals.dashboard.mainView} />\n"));
|
|
244
|
+
console.log(chalk.cyan(" // Multiple contexts (dark mode, locales)"));
|
|
245
|
+
console.log(chalk.cyan(" <img src={ReshotVisuals.dashboard.mainView.default} />"));
|
|
246
|
+
console.log(chalk.cyan(" <img src={ReshotVisuals.dashboard.mainView.dark} />\n"));
|
|
247
|
+
if (!full) {
|
|
248
|
+
console.log(chalk.gray(" Tip: Use --full flag to include width, height, and alt text\n"));
|
|
249
|
+
}
|
|
250
|
+
} else if (format === "json") {
|
|
251
|
+
console.log(chalk.blue("━━━ JSON Usage ━━━\n"));
|
|
252
|
+
console.log(chalk.white("Import in JavaScript/TypeScript:\n"));
|
|
253
|
+
console.log(chalk.cyan(' import assets from "./reshot-assets.json";\n'));
|
|
254
|
+
console.log(chalk.white("Access assets with dot notation:\n"));
|
|
255
|
+
console.log(chalk.cyan(" <img"));
|
|
256
|
+
console.log(chalk.cyan(" src={assets.assets.dashboard.mainView.default.src}"));
|
|
257
|
+
console.log(chalk.cyan(" width={assets.assets.dashboard.mainView.default.width}"));
|
|
258
|
+
console.log(chalk.cyan(" height={assets.assets.dashboard.mainView.default.height}"));
|
|
259
|
+
console.log(chalk.cyan(" alt={assets.assets.dashboard.mainView.default.alt}"));
|
|
260
|
+
console.log(chalk.cyan(" />\n"));
|
|
261
|
+
} else if (format === "csv") {
|
|
262
|
+
console.log(chalk.blue("━━━ CSV Usage ━━━\n"));
|
|
263
|
+
console.log(chalk.white("Import into your CMS:\n"));
|
|
264
|
+
console.log(chalk.gray(" 1. Open Contentful / Webflow / WordPress"));
|
|
265
|
+
console.log(chalk.gray(" 2. Navigate to bulk import/media settings"));
|
|
266
|
+
console.log(chalk.gray(` 3. Upload ${outputPath}`));
|
|
267
|
+
console.log(chalk.gray(" 4. Map columns to your asset fields\n"));
|
|
268
|
+
console.log(chalk.white("CSV Columns:\n"));
|
|
269
|
+
console.log(chalk.gray(" • Key - Unique asset identifier"));
|
|
270
|
+
console.log(chalk.gray(" • Group - Scenario/folder grouping"));
|
|
271
|
+
console.log(chalk.gray(" • Name - Human-readable name"));
|
|
272
|
+
console.log(chalk.gray(" • Context - Variant (default, dark, fr-FR)"));
|
|
273
|
+
console.log(chalk.gray(" • Type - image or video"));
|
|
274
|
+
console.log(chalk.gray(" • Unbreakable_URL - CDN URL (never changes)"));
|
|
275
|
+
console.log(chalk.gray(" • Width/Height - Dimensions in pixels\n"));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// CI/CD integration tip
|
|
279
|
+
console.log(chalk.blue("━━━ CI/CD Integration ━━━\n"));
|
|
280
|
+
console.log(chalk.white("Add to your build pipeline:\n"));
|
|
281
|
+
console.log(chalk.cyan(" # package.json"));
|
|
282
|
+
console.log(chalk.cyan(' "scripts": {'));
|
|
283
|
+
console.log(chalk.cyan(` "prebuild": "npx reshot pull --format=${format}${output ? ` --output=${output}` : ''}"`));
|
|
284
|
+
console.log(chalk.cyan(' }\n'));
|
|
285
|
+
console.log(chalk.gray(" This ensures your build always uses the latest approved visuals."));
|
|
286
|
+
console.log(chalk.gray(" If a visual is deleted, your build will fail (preventing broken images).\n"));
|
|
287
|
+
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (config.isAuthError(error)) {
|
|
290
|
+
console.error(
|
|
291
|
+
chalk.red("Error: Authentication failed. Run 'reshot auth' to re-authenticate.")
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
295
|
+
if (process.env.RESHOT_DEBUG) {
|
|
296
|
+
console.error(chalk.gray(error.stack));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = pullCommand;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// record.js - Interactive scenario recording via CDP
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { connectToActivePage } = require('../lib/record-cdp');
|
|
4
|
+
const { setupBrowserActionListener, updateBrowserMode } = require('../lib/record-browser-injection');
|
|
5
|
+
const { setupTerminalHotkeys, runEventLoop } = require('../lib/record-terminal');
|
|
6
|
+
const { startCaptureFlow } = require('../lib/record-screenshot');
|
|
7
|
+
const { startClipRecording } = require('../lib/record-clip');
|
|
8
|
+
const { showVisualSelectionMenu, finalizeScenarioAndWriteConfig } = require('../lib/record-config');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Main record command
|
|
12
|
+
* @param {string|undefined} title - Optional title for the visual (e.g., "Admin Dashboard")
|
|
13
|
+
*/
|
|
14
|
+
async function recordCommand(title) {
|
|
15
|
+
console.log(chalk.cyan('🎬 Starting interactive recording session...\n'));
|
|
16
|
+
|
|
17
|
+
let cleanup = null;
|
|
18
|
+
let browser = null;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Step 1: Connect to browser via CDP
|
|
22
|
+
const { browser: connectedBrowser, page } = await connectToActivePage();
|
|
23
|
+
browser = connectedBrowser;
|
|
24
|
+
|
|
25
|
+
// Step 2: Visual selection and session initialization
|
|
26
|
+
const { visualKey, existingScenario } = await showVisualSelectionMenu(page, title);
|
|
27
|
+
|
|
28
|
+
const existingScenarioSnapshot = existingScenario
|
|
29
|
+
? JSON.parse(JSON.stringify(existingScenario))
|
|
30
|
+
: null;
|
|
31
|
+
|
|
32
|
+
const sessionState = {
|
|
33
|
+
visualKey,
|
|
34
|
+
capturedSteps: [],
|
|
35
|
+
existingScenario: existingScenarioSnapshot,
|
|
36
|
+
savedStepCount: 0,
|
|
37
|
+
mode: 'normal',
|
|
38
|
+
phase: 'idle',
|
|
39
|
+
pendingCapture: null,
|
|
40
|
+
quit: false,
|
|
41
|
+
saveOnQuit: true,
|
|
42
|
+
clipEvents: null,
|
|
43
|
+
recordingStart: null,
|
|
44
|
+
stopClipRecording: false,
|
|
45
|
+
onChange: null,
|
|
46
|
+
onElementSelected: null
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
console.log(chalk.green(`\n✔ Visual: ${visualKey}\n`));
|
|
50
|
+
|
|
51
|
+
// Step 3: Inject browser action listener
|
|
52
|
+
await setupBrowserActionListener(page, sessionState);
|
|
53
|
+
|
|
54
|
+
// Step 4: Set up terminal hotkeys
|
|
55
|
+
cleanup = setupTerminalHotkeys(sessionState, async () => {
|
|
56
|
+
// Capture flow callback
|
|
57
|
+
try {
|
|
58
|
+
if (sessionState.mode === 'normal') {
|
|
59
|
+
await startCaptureFlow(sessionState, page);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(chalk.red('Capture error:'), error.message);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Step 5: Run main event loop
|
|
67
|
+
await runEventLoop(sessionState);
|
|
68
|
+
|
|
69
|
+
// Step 6: Finalize and save config
|
|
70
|
+
if (cleanup) cleanup();
|
|
71
|
+
|
|
72
|
+
await finalizeScenarioAndWriteConfig(sessionState, page);
|
|
73
|
+
sessionState.phase = 'finished';
|
|
74
|
+
|
|
75
|
+
// Close browser connection
|
|
76
|
+
await browser.close();
|
|
77
|
+
|
|
78
|
+
console.log(chalk.cyan('👋 Recording session ended.\n'));
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (cleanup) cleanup();
|
|
82
|
+
if (browser) {
|
|
83
|
+
try {
|
|
84
|
+
await browser.close();
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// Ignore cleanup errors
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = recordCommand;
|
|
94
|
+
|