veodl 1.12.0 → 2.0.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/CHANGELOG.md +27 -0
- package/README.md +183 -100
- package/bin/veo.js +47 -2
- package/docs/AGENT_GUIDE.md +1 -0
- package/native/android-arm64/veo +0 -0
- package/native/darwin-arm64/veo +0 -0
- package/native/darwin-x64/veo +0 -0
- package/native/linux-arm64/veo +0 -0
- package/native/linux-x64/veo +0 -0
- package/native/win32-arm64/veo.exe +0 -0
- package/native/win32-x64/veo.exe +0 -0
- package/package.json +53 -61
- package/src/alias.js +0 -258
- package/src/backend-update.js +0 -191
- package/src/backend.js +0 -548
- package/src/changes.js +0 -278
- package/src/cli.js +0 -781
- package/src/compatibility.js +0 -39
- package/src/config-clipboard.js +0 -67
- package/src/config-diagnostics.js +0 -155
- package/src/config-editor.js +0 -404
- package/src/config-errors.js +0 -64
- package/src/config-reset.js +0 -39
- package/src/config-template.js +0 -227
- package/src/config.js +0 -307
- package/src/disk-space.js +0 -68
- package/src/doctor.js +0 -302
- package/src/download-cache.js +0 -31
- package/src/downloader.js +0 -669
- package/src/execution.js +0 -68
- package/src/experimental-fragments.js +0 -29
- package/src/flush.js +0 -66
- package/src/history.js +0 -173
- package/src/inline-region.js +0 -72
- package/src/inspect-media.js +0 -179
- package/src/interactive.js +0 -120
- package/src/jobs.js +0 -218
- package/src/legacy-config-comments.js +0 -106
- package/src/naming.js +0 -54
- package/src/open-file.js +0 -15
- package/src/option-suggestions.js +0 -43
- package/src/output.js +0 -81
- package/src/path-links.js +0 -53
- package/src/paths.js +0 -26
- package/src/playlist.js +0 -31
- package/src/progress.js +0 -341
- package/src/run-archive.js +0 -148
- package/src/runs.js +0 -346
- package/src/source-discovery.js +0 -303
- package/src/state.js +0 -27
- package/src/stats.js +0 -63
- package/src/terminal-size.js +0 -8
- package/src/terminal-title.js +0 -19
- package/src/tool-setup.js +0 -151
- package/src/uninstall.js +0 -222
- package/src/updater.js +0 -229
- package/src/utils.js +0 -208
- package/src/version.js +0 -38
package/src/runs.js
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import { commandOutput } from './output.js';
|
|
2
|
-
import { randomInt, randomUUID } from 'node:crypto';
|
|
3
|
-
import { lstat, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
-
import { cacheBase } from './paths.js';
|
|
7
|
-
import { writeJson } from './state.js';
|
|
8
|
-
import { runArchiveFile } from './run-archive.js';
|
|
9
|
-
import { cleanText, localStamp } from './utils.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Every veo run registers itself under the per-user cache's `runs` directory
|
|
13
|
-
* while it works, so another terminal can list it (`veo runs`), inspect it
|
|
14
|
-
* (`veo runs <id>`) or cancel it (`veo stop <id>`) without guessing at PIDs.
|
|
15
|
-
* Records hold an id, times, URLs, paths and settings — never credentials or
|
|
16
|
-
* cookie settings, and the record itself is always removed when the run ends.
|
|
17
|
-
*/
|
|
18
|
-
export const RUN_ID = /^[0-9a-z]{6}$/;
|
|
19
|
-
export const STOP_TIMEOUT_MS = 15000;
|
|
20
|
-
const RECORD = /^[a-f0-9-]{36}\.json$/;
|
|
21
|
-
const ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
|
22
|
-
const ITEM_STATUSES = new Set(['pending', 'running', 'saved', 'skipped', 'failed', 'cancelled']);
|
|
23
|
-
const SHOWN_ITEMS = 10;
|
|
24
|
-
const OUTPUT_WIDTH = 42;
|
|
25
|
-
|
|
26
|
-
export const RUNS_HELP = `veo runs [id] [--json]
|
|
27
|
-
|
|
28
|
-
List active veo runs with their 6-character id, process id, start time and progress.
|
|
29
|
-
With an id, show one run in detail: URLs, media settings, output directory, job file
|
|
30
|
-
and the state of every item. A run appears while it works and disappears when it
|
|
31
|
-
finishes. Records of crashed runs are kept but marked stale.
|
|
32
|
-
Use --json for machine-readable run metadata and per-item progress.
|
|
33
|
-
Use veo stop <id> to stop one run.
|
|
34
|
-
`;
|
|
35
|
-
|
|
36
|
-
export const STOP_HELP = `veo stop [id]
|
|
37
|
-
|
|
38
|
-
Ask one run, or every run when no id is given, to stop, and wait until it exits.
|
|
39
|
-
A stopped run keeps its partial data and its retry job, so the printed
|
|
40
|
-
veo --retry-failed command still works; veo flush removes those later.
|
|
41
|
-
Stale records of crashed runs are removed. Exit status is 1 when a run does not
|
|
42
|
-
stop in time.
|
|
43
|
-
`;
|
|
44
|
-
|
|
45
|
-
export function runsDirectory(root = cacheBase()) {
|
|
46
|
-
return path.join(path.resolve(root), 'runs');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* A run record is only trusted when it names a plausible process. Older veo
|
|
51
|
-
* versions wrote nothing but a pid, so a missing version is still accepted.
|
|
52
|
-
*/
|
|
53
|
-
function validRecord(run) {
|
|
54
|
-
if (!run || typeof run !== 'object' || Array.isArray(run)) return false;
|
|
55
|
-
return (run.version === undefined || run.version === 1) && Number.isInteger(run.pid) && run.pid > 0;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const exists = file => lstat(file).then(() => true, error => { if (error.code === 'ENOENT') return false; throw error; });
|
|
59
|
-
const text = value => typeof value === 'string' ? cleanText(value) : '';
|
|
60
|
-
const newRunId = () => Array.from({ length: 6 }, () => ID_ALPHABET[randomInt(ID_ALPHABET.length)]).join('');
|
|
61
|
-
|
|
62
|
-
export function alive(pid) {
|
|
63
|
-
try { process.kill(pid, 0); return true; }
|
|
64
|
-
catch (error) { return error.code !== 'ESRCH'; }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function runFiles(root) {
|
|
68
|
-
const directory = runsDirectory(root);
|
|
69
|
-
const info = await lstat(directory).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
|
|
70
|
-
if (!info) return [];
|
|
71
|
-
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`Invalid active run directory: ${directory}`);
|
|
72
|
-
return (await readdir(directory, { withFileTypes: true }))
|
|
73
|
-
.filter(entry => entry.isFile() && RECORD.test(entry.name)).map(entry => path.join(directory, entry.name));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Cache files are machine-written: a missing, truncated or foreign file must
|
|
77
|
-
// never take a veo command down, so unreadable JSON simply counts as unknown.
|
|
78
|
-
async function readRecordFile(file) {
|
|
79
|
-
try { return JSON.parse(await readFile(file, 'utf8')); } catch { return null; }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function readRecords(root) {
|
|
83
|
-
const records = [];
|
|
84
|
-
for (const file of await runFiles(root)) {
|
|
85
|
-
// Damaged or foreign files are ignored, never fatal: a single stray file
|
|
86
|
-
// must not block every later veo run. Downloads of a run that could not be
|
|
87
|
-
// recognized stay protected by the staging lock that run holds.
|
|
88
|
-
const run = await readRecordFile(file);
|
|
89
|
-
if (!validRecord(run)) continue;
|
|
90
|
-
records.push({ ...run, id: RUN_ID.test(run.id || '') ? run.id : null, file });
|
|
91
|
-
}
|
|
92
|
-
return records;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Every registered run, live ones first and then the most recently started. */
|
|
96
|
-
export async function listRuns(root = cacheBase()) {
|
|
97
|
-
const runs = await readRecords(root);
|
|
98
|
-
for (const run of runs) run.alive = alive(run.pid);
|
|
99
|
-
return runs.sort((a, b) => Number(b.alive) - Number(a.alive) || String(b.startedAt || '').localeCompare(String(a.startedAt || '')));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Register this process as a run and poll for a stop request. The returned
|
|
104
|
-
* handle exposes the run's id, its resolved settings for `veo runs <id>`, and
|
|
105
|
-
* the cleanup that removes the record when the run ends.
|
|
106
|
-
*/
|
|
107
|
-
export async function registerRun(cancel, root = cacheBase()) {
|
|
108
|
-
const directory = runsDirectory(root);
|
|
109
|
-
await mkdir(directory, { recursive: true });
|
|
110
|
-
const gate = path.join(path.resolve(root), '.flush');
|
|
111
|
-
if (await exists(gate)) throw new Error('veo flush is in progress. Try again after cleanup finishes.');
|
|
112
|
-
const taken = new Set((await readRecords(root)).map(run => run.id).filter(Boolean));
|
|
113
|
-
let id = null;
|
|
114
|
-
for (let attempt = 0; attempt < 20 && !id; attempt++) {
|
|
115
|
-
const candidate = newRunId();
|
|
116
|
-
if (!taken.has(candidate) && !await exists(runArchiveFile(candidate, root))) id = candidate;
|
|
117
|
-
}
|
|
118
|
-
if (!id) throw new Error('Could not allocate a free run id. Remove stale run records with veo stop or veo flush.');
|
|
119
|
-
const file = path.join(directory, `${randomUUID()}.json`);
|
|
120
|
-
const request = `${file}.cancel`;
|
|
121
|
-
const record = { version: 1, id, pid: process.pid, startedAt: new Date().toISOString(),
|
|
122
|
-
urls: [], output: null, media: null, quality: null, format: null, profile: null, playlist: false, job: null };
|
|
123
|
-
await writeJson(file, record);
|
|
124
|
-
// A flush that started while this run registered must still win.
|
|
125
|
-
if (await exists(gate)) {
|
|
126
|
-
await rm(file, { force: true });
|
|
127
|
-
throw new Error('veo flush is in progress. Try again after cleanup finishes.');
|
|
128
|
-
}
|
|
129
|
-
let cancelling = false;
|
|
130
|
-
const timer = setInterval(() => {
|
|
131
|
-
if (cancelling) return;
|
|
132
|
-
exists(request).then(found => {
|
|
133
|
-
if (found && !cancelling) {
|
|
134
|
-
cancelling = true;
|
|
135
|
-
cancel();
|
|
136
|
-
}
|
|
137
|
-
}).catch(() => {});
|
|
138
|
-
}, 200);
|
|
139
|
-
timer.unref();
|
|
140
|
-
return {
|
|
141
|
-
id,
|
|
142
|
-
file,
|
|
143
|
-
/** Merge what this run resolved, so other terminals can inspect it. */
|
|
144
|
-
async describe(details = {}) {
|
|
145
|
-
Object.assign(record, {
|
|
146
|
-
urls: (Array.isArray(details.urls) ? details.urls : []).filter(url => typeof url === 'string').slice(0, 200).map(cleanText),
|
|
147
|
-
output: text(details.output) || null,
|
|
148
|
-
media: details.audio ? 'audio' : 'video',
|
|
149
|
-
quality: details.audio ? null : text(details.quality) || null,
|
|
150
|
-
format: text(details.format) || null,
|
|
151
|
-
profile: text(details.profile) || null,
|
|
152
|
-
playlist: Boolean(details.playlist),
|
|
153
|
-
job: text(details.job) || null,
|
|
154
|
-
});
|
|
155
|
-
await writeJson(file, record);
|
|
156
|
-
},
|
|
157
|
-
async unregister() {
|
|
158
|
-
cancelling = true;
|
|
159
|
-
clearInterval(timer);
|
|
160
|
-
await rm(file, { force: true });
|
|
161
|
-
await rm(request, { force: true });
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Per-item progress of a run, read from the job file it writes as it works. */
|
|
167
|
-
async function readProgress(jobFile) {
|
|
168
|
-
if (!jobFile) return null;
|
|
169
|
-
const job = await readRecordFile(jobFile);
|
|
170
|
-
if (job?.version !== 1 || !Array.isArray(job.items)) return null;
|
|
171
|
-
const items = job.items.map(item => ({
|
|
172
|
-
url: text(item?.url),
|
|
173
|
-
status: ITEM_STATUSES.has(item?.status) ? item.status : 'pending',
|
|
174
|
-
files: (Array.isArray(item?.files) ? item.files : []).map(text).filter(Boolean),
|
|
175
|
-
error: text(item?.error) || null,
|
|
176
|
-
}));
|
|
177
|
-
const counts = { total: items.length, pending: 0, running: 0, saved: 0, skipped: 0, failed: 0, cancelled: 0 };
|
|
178
|
-
for (const item of items) counts[item.status]++;
|
|
179
|
-
return { items, counts };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function describeRuns(root) {
|
|
183
|
-
const runs = await listRuns(root);
|
|
184
|
-
return Promise.all(runs.map(async run => ({ ...run, progress: await readProgress(run.job) })));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function summarizeProgress(progress) {
|
|
188
|
-
const counts = progress?.counts;
|
|
189
|
-
if (!counts?.total) return 'starting';
|
|
190
|
-
const parts = [];
|
|
191
|
-
const done = counts.saved + counts.skipped;
|
|
192
|
-
if (done) parts.push(`${done}/${counts.total} done`);
|
|
193
|
-
if (counts.running) parts.push(`${counts.running} running`);
|
|
194
|
-
if (counts.failed) parts.push(`${counts.failed} failed`);
|
|
195
|
-
if (counts.cancelled) parts.push(`${counts.cancelled} cancelled`);
|
|
196
|
-
if (counts.pending) parts.push(parts.length ? `${counts.pending} pending` : `${counts.total} pending`);
|
|
197
|
-
return parts.join(', ');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function shortDuration(ms) {
|
|
201
|
-
if (!Number.isFinite(ms) || ms < 0) return '0s';
|
|
202
|
-
const seconds = Math.floor(ms / 1000);
|
|
203
|
-
if (seconds < 60) return `${seconds}s`;
|
|
204
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
205
|
-
return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const shorten = (value, width = OUTPUT_WIDTH) => value.length <= width ? value : `…${value.slice(-(width - 1))}`;
|
|
209
|
-
const label = run => run.id || `PID ${run.pid}`;
|
|
210
|
-
// Records of older versions have no start time, so no uptime can be shown.
|
|
211
|
-
const uptime = run => Number.isFinite(Date.parse(run.startedAt)) ? shortDuration(Date.now() - Date.parse(run.startedAt)) : null;
|
|
212
|
-
|
|
213
|
-
function describeMedia(run) {
|
|
214
|
-
const detail = run.media === 'audio' ? run.format : run.quality;
|
|
215
|
-
return `${run.media || 'not resolved yet'}${detail ? `, ${detail}` : ''}${run.playlist ? ', playlist' : ''}`;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function formatRuns(runs) {
|
|
219
|
-
if (!runs.length) return 'veo runs\n\nNo veo runs are active. Start a download to see it here.\n';
|
|
220
|
-
const rows = runs.map(run => {
|
|
221
|
-
const running = uptime(run);
|
|
222
|
-
return [run.id || '-', String(run.pid), run.alive ? `running${running ? ` ${running}` : ''}` : 'stale',
|
|
223
|
-
localStamp(run.startedAt), summarizeProgress(run.progress), run.output ? shorten(text(run.output)) : '-'];
|
|
224
|
-
});
|
|
225
|
-
const header = ['ID', 'PID', 'STATE', 'STARTED', 'ITEMS', 'OUTPUT'];
|
|
226
|
-
const widths = header.map((title, index) => Math.max(title.length, ...rows.map(row => row[index].length)));
|
|
227
|
-
const line = cells => cells.map((cell, index) => cell.padEnd(widths[index])).join(' ').trimEnd();
|
|
228
|
-
return `${['veo runs', '', line(header), ...rows.map(line), '', 'Details: veo runs <id> Stop: veo stop [id]'].join('\n')}\n`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export function formatRunDetails(run) {
|
|
232
|
-
const lines = [`veo runs ${run.id || '-'}`, ''];
|
|
233
|
-
const running = uptime(run);
|
|
234
|
-
lines.push(`State: ${run.alive ? `running (PID ${run.pid})` : `stale (PID ${run.pid} is gone)`}`);
|
|
235
|
-
lines.push(`Started: ${localStamp(run.startedAt)}${run.alive && running ? ` (${running} ago)` : ''}`);
|
|
236
|
-
lines.push(`Media: ${describeMedia(run)}`);
|
|
237
|
-
lines.push(`Profile: ${text(run.profile) || 'global (no profile)'}`);
|
|
238
|
-
lines.push(`Output: ${text(run.output) || 'not resolved yet'}`);
|
|
239
|
-
lines.push(`Job: ${text(run.job) || 'not created yet'}`);
|
|
240
|
-
const urls = (Array.isArray(run.urls) ? run.urls : []).map(text).filter(Boolean);
|
|
241
|
-
lines.push(`URLs: ${urls.length ? `${urls.length}` : 'not resolved yet'}`);
|
|
242
|
-
urls.slice(0, SHOWN_ITEMS).forEach((url, index) => lines.push(` ${index + 1}. ${url}`));
|
|
243
|
-
if (urls.length > SHOWN_ITEMS) lines.push(` … and ${urls.length - SHOWN_ITEMS} more`);
|
|
244
|
-
const items = run.progress?.items || [];
|
|
245
|
-
if (items.length) {
|
|
246
|
-
lines.push(`Items: ${summarizeProgress(run.progress)}`);
|
|
247
|
-
items.slice(0, SHOWN_ITEMS).forEach((item, index) => {
|
|
248
|
-
const files = item.files.length ? ` ${item.files[0]}${item.files.length > 1 ? ` (+${item.files.length - 1} file(s))` : ''}` : '';
|
|
249
|
-
lines.push(` ${String(index + 1).padStart(2)}. ${item.status}${item.status === 'saved' ? files : ` ${item.url}`}`);
|
|
250
|
-
if (item.error) lines.push(` ${item.error}`);
|
|
251
|
-
});
|
|
252
|
-
if (items.length > SHOWN_ITEMS) lines.push(` … and ${items.length - SHOWN_ITEMS} more item(s)`);
|
|
253
|
-
}
|
|
254
|
-
lines.push('');
|
|
255
|
-
lines.push(run.alive && run.id ? `Stop: veo stop ${run.id}` : 'Cleanup: veo stop (removes stale records)');
|
|
256
|
-
return `${lines.join('\n')}\n`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function normalizeId(value) {
|
|
260
|
-
const id = cleanText(value).toLowerCase();
|
|
261
|
-
if (!RUN_ID.test(id)) throw new Error(`Invalid run id "${cleanText(value)}". Run ids have 6 characters and are listed by veo runs.`);
|
|
262
|
-
return id;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export async function runsMain(args = [], { stdout = process.stdout, root = cacheBase() } = {}) {
|
|
266
|
-
[args, stdout] = commandOutput(args, stdout);
|
|
267
|
-
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
268
|
-
stdout.write(RUNS_HELP);
|
|
269
|
-
return 0;
|
|
270
|
-
}
|
|
271
|
-
const json = args.includes('--json');
|
|
272
|
-
args = args.filter(arg => arg !== '--json');
|
|
273
|
-
if (args.length > 1) throw new Error('Usage: veo runs [id] [--json]');
|
|
274
|
-
const runs = await describeRuns(root);
|
|
275
|
-
if (!args.length) {
|
|
276
|
-
if (json) stdout.write(`${JSON.stringify({ count: runs.length, runs: runs.map(publicRun) })}\n`);
|
|
277
|
-
else stdout.write(formatRuns(runs));
|
|
278
|
-
return 0;
|
|
279
|
-
}
|
|
280
|
-
const id = normalizeId(args[0]);
|
|
281
|
-
const run = runs.find(item => item.id === id);
|
|
282
|
-
if (!run) throw new Error(`No veo run with id ${id} is active. List runs with veo runs.`);
|
|
283
|
-
if (json) stdout.write(`${JSON.stringify(publicRun(run))}\n`);
|
|
284
|
-
else stdout.write(formatRunDetails(run));
|
|
285
|
-
return 0;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function publicRun(run) {
|
|
289
|
-
return { id: run.id, pid: run.pid, state: run.alive ? 'running' : 'stale', startedAt: run.startedAt || null,
|
|
290
|
-
urls: Array.isArray(run.urls) ? run.urls : [], output: run.output || null, media: run.media || null,
|
|
291
|
-
quality: run.quality || null, format: run.format || null, profile: run.profile || null,
|
|
292
|
-
playlist: Boolean(run.playlist), job: run.job || null,
|
|
293
|
-
progress: run.progress || null };
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* A run answers a stop request by cleaning up its own record, so a run counts as
|
|
298
|
-
* gone as soon as its record disappears — a live-looking process is not enough
|
|
299
|
-
* (the pid may already belong to something else).
|
|
300
|
-
*/
|
|
301
|
-
export async function waitForExit(runs, timeoutMs) {
|
|
302
|
-
const pending = async () => {
|
|
303
|
-
for (const run of runs) if (alive(run.pid) && await exists(run.file)) return true;
|
|
304
|
-
return false;
|
|
305
|
-
};
|
|
306
|
-
const deadline = Date.now() + timeoutMs;
|
|
307
|
-
while (await pending()) {
|
|
308
|
-
if (Date.now() >= deadline) return false;
|
|
309
|
-
await delay(100);
|
|
310
|
-
}
|
|
311
|
-
return true;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export async function stopMain(args = [], { stdout = process.stdout, root = cacheBase(), timeoutMs = STOP_TIMEOUT_MS } = {}) {
|
|
315
|
-
[args, stdout] = commandOutput(args, stdout);
|
|
316
|
-
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
317
|
-
stdout.write(STOP_HELP);
|
|
318
|
-
return 0;
|
|
319
|
-
}
|
|
320
|
-
if (args.length > 1) throw new Error('Usage: veo stop [id]');
|
|
321
|
-
let targets = await listRuns(root);
|
|
322
|
-
if (args.length) {
|
|
323
|
-
const id = normalizeId(args[0]);
|
|
324
|
-
targets = targets.filter(run => run.id === id);
|
|
325
|
-
if (!targets.length) throw new Error(`No veo run with id ${id} is active. List runs with veo runs.`);
|
|
326
|
-
}
|
|
327
|
-
const stale = targets.filter(run => !run.alive);
|
|
328
|
-
const live = targets.filter(run => run.alive);
|
|
329
|
-
for (const run of live) await writeFile(`${run.file}.cancel`, 'stop');
|
|
330
|
-
if (live.length) await waitForExit(live, timeoutMs);
|
|
331
|
-
const remaining = [];
|
|
332
|
-
for (const run of live) if (await exists(run.file) && alive(run.pid)) remaining.push(run);
|
|
333
|
-
const stopped = live.filter(run => !remaining.includes(run));
|
|
334
|
-
for (const run of [...stale, ...stopped]) {
|
|
335
|
-
await rm(run.file, { force: true });
|
|
336
|
-
await rm(`${run.file}.cancel`, { force: true });
|
|
337
|
-
}
|
|
338
|
-
const lines = ['veo stop', ''];
|
|
339
|
-
if (stopped.length) lines.push(`Stopped: ${stopped.map(label).join(', ')}`);
|
|
340
|
-
if (stale.length) lines.push(`Removed ${stale.length} stale record(s): ${stale.map(label).join(', ')}`);
|
|
341
|
-
if (remaining.length) lines.push(`Still running: ${remaining.map(label).join(', ')}`);
|
|
342
|
-
if (!stopped.length && !stale.length && !remaining.length) lines.push('No veo runs were active.');
|
|
343
|
-
if (stopped.length) lines.push('', 'Partial data and retry jobs are kept; veo flush removes them.');
|
|
344
|
-
stdout.write(`${lines.join('\n')}\n`);
|
|
345
|
-
return remaining.length ? 1 : 0;
|
|
346
|
-
}
|
package/src/source-discovery.js
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
-
import { cleanText, validateUrl } from './utils.js';
|
|
7
|
-
|
|
8
|
-
const MEDIA_URL = /\.(?:m3u8|mpd|mp4|webm|mov)(?:[?#]|$)/i;
|
|
9
|
-
const MEDIA_TYPE = /(?:mpegurl|dash\+xml|video\/(?:mp4|webm|quicktime))/i;
|
|
10
|
-
const PLAY_CONTROL = String.raw`(() => {
|
|
11
|
-
const visited = new WeakSet();
|
|
12
|
-
const state = globalThis[Symbol.for('veo.playControls')] ||= { visited, clicks: 0 };
|
|
13
|
-
const labelMatches = node => {
|
|
14
|
-
const label = [node.getAttribute('aria-label'), node.getAttribute('title'), node.getAttribute('data-testid'),
|
|
15
|
-
node.getAttribute('value'), node.textContent].find(value => value && value.trim())?.trim() || '';
|
|
16
|
-
return /^(?:play|watch|start|replay|abspielen|ansehen|wiedergabe)(?:\s+(?:video|movie|film|now))?$/i.test(label)
|
|
17
|
-
|| /(?:^|[-_\s])(?:big[-_])?play(?:[-_\s]|$)/i.test(node.className?.baseVal || node.className || '');
|
|
18
|
-
};
|
|
19
|
-
const documents = [document];
|
|
20
|
-
for (let index = 0; index < documents.length && index < 16; index++) {
|
|
21
|
-
for (const frame of documents[index].querySelectorAll('iframe')) {
|
|
22
|
-
try { if (frame.contentDocument && !documents.includes(frame.contentDocument)) documents.push(frame.contentDocument); } catch {}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
for (const doc of documents) {
|
|
26
|
-
const controls = doc.querySelectorAll('button,[role="button"],[aria-label],[title],[data-testid],input[type="button"],input[type="image"],[class*="play"],[class*="Play"]');
|
|
27
|
-
for (const node of controls) {
|
|
28
|
-
if (state.clicks >= 8) return false;
|
|
29
|
-
if (state.visited.has(node) || node.disabled || node.getAttribute('aria-disabled') === 'true' || !labelMatches(node)) continue;
|
|
30
|
-
const style = doc.defaultView.getComputedStyle(node);
|
|
31
|
-
if (style.display === 'none' || style.visibility === 'hidden' || style.pointerEvents === 'none' || !node.getClientRects().length) continue;
|
|
32
|
-
state.visited.add(node);
|
|
33
|
-
state.clicks++;
|
|
34
|
-
node.click();
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
for (const video of doc.querySelectorAll('video')) {
|
|
38
|
-
if (state.visited.has(video) || !video.paused) continue;
|
|
39
|
-
state.visited.add(video);
|
|
40
|
-
video.play().catch(() => {});
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return false;
|
|
45
|
-
})()`;
|
|
46
|
-
|
|
47
|
-
function abortable(promise, signal) {
|
|
48
|
-
if (!signal) return promise;
|
|
49
|
-
if (signal.aborted) {
|
|
50
|
-
Promise.resolve(promise).catch(() => {});
|
|
51
|
-
throw signal.reason;
|
|
52
|
-
}
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
const onAbort = () => reject(signal.reason);
|
|
55
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
56
|
-
Promise.resolve(promise).then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function parseSourceTimeout(value) {
|
|
61
|
-
const match = /^(\d+)(ms|s|m)?$/i.exec(String(value || '').trim());
|
|
62
|
-
if (!match) throw new Error('--timeout expects seconds, e.g. 30, 30s or 2m.');
|
|
63
|
-
const milliseconds = Number(match[1]) * ({ ms: 1, s: 1000, m: 60000 }[match[2]?.toLowerCase() || 's']);
|
|
64
|
-
if (!Number.isSafeInteger(milliseconds) || milliseconds < 5000 || milliseconds > 600000) {
|
|
65
|
-
throw new Error('--timeout must be between 5 seconds and 10 minutes.');
|
|
66
|
-
}
|
|
67
|
-
return milliseconds;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function shouldOfferSourceDiscovery(error) {
|
|
71
|
-
if (error?.name === 'AbortError') return false;
|
|
72
|
-
return !/(?:HTTP (?:Error )?(?:401|403|404|429|5\d\d)|\b(?:DRM|live stream|rate limit|timed out|timeout|connection refused|login required|sign in|private video|geo.?block)\b|This URL is a collection)/i
|
|
73
|
-
.test(String(error?.message || error));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function browserCandidates() {
|
|
77
|
-
const custom = process.env.VEO_BROWSER_PATH;
|
|
78
|
-
if (custom) return [custom];
|
|
79
|
-
if (process.platform === 'win32') {
|
|
80
|
-
const roots = [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(Boolean);
|
|
81
|
-
return roots.flatMap(root => [
|
|
82
|
-
path.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
83
|
-
path.join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
84
|
-
path.join(root, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
|
|
85
|
-
]);
|
|
86
|
-
}
|
|
87
|
-
if (process.platform === 'darwin') return [
|
|
88
|
-
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
89
|
-
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
90
|
-
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
91
|
-
];
|
|
92
|
-
return ['chromium', 'chromium-browser', 'google-chrome', 'microsoft-edge', 'brave-browser'];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function mediaUrlsFromText(body) {
|
|
96
|
-
if (typeof body !== 'string' || body.length > 524288) return [];
|
|
97
|
-
const normalized = body.replaceAll('\\/', '/').replaceAll('\\u0026', '&').replaceAll('&', '&');
|
|
98
|
-
const urls = normalized.match(/https?:\/\/[^\s"'<>\\]+/gi) || [];
|
|
99
|
-
return [...new Set(urls.map(value => value.replace(/[),;]+$/, '')).filter(value => MEDIA_URL.test(value)).map(value => {
|
|
100
|
-
try { return validateUrl(value); } catch { return null; }
|
|
101
|
-
}).filter(Boolean))];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function summarizeSource(metadata, url, index, pageUrl) {
|
|
105
|
-
const formats = (metadata.formats || []).filter(format => !format.has_drm);
|
|
106
|
-
const heights = [...new Set(formats.map(format => format.height).filter(Number.isFinite))].sort((a, b) => b - a);
|
|
107
|
-
const bytes = metadata.filesize || metadata.filesize_approx || null;
|
|
108
|
-
const bitrate = Number.isFinite(metadata.tbr) ? metadata.tbr : Math.max(0, ...formats.map(format => format.tbr).filter(Number.isFinite));
|
|
109
|
-
return {
|
|
110
|
-
index, title: cleanText(metadata.title || metadata.id || 'Video'),
|
|
111
|
-
source: new URL(url).hostname, type: /\.m3u8(?:[?#]|$)/i.test(url) ? 'HLS' : /\.mpd(?:[?#]|$)/i.test(url) ? 'DASH' : 'Video',
|
|
112
|
-
quality: heights.length ? heights.map(height => `${height}p`) : [],
|
|
113
|
-
estimatedMiB: bytes ? Math.round(bytes / 1048576 * 10) / 10 : null,
|
|
114
|
-
bitrateMbps: bitrate > 0 ? Math.round(bitrate / 100) / 10 : null,
|
|
115
|
-
pageUrl,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function createCdp(socket) {
|
|
120
|
-
let nextId = 0;
|
|
121
|
-
const pending = new Map();
|
|
122
|
-
const listeners = new Set();
|
|
123
|
-
socket.addEventListener('message', event => {
|
|
124
|
-
let message;
|
|
125
|
-
try { message = JSON.parse(event.data); } catch { return; }
|
|
126
|
-
if (message.id) {
|
|
127
|
-
const entry = pending.get(message.id);
|
|
128
|
-
if (!entry) return;
|
|
129
|
-
pending.delete(message.id);
|
|
130
|
-
if (message.error) entry.reject(new Error(message.error.message));
|
|
131
|
-
else entry.resolve(message.result || {});
|
|
132
|
-
} else for (const listener of listeners) listener(message);
|
|
133
|
-
});
|
|
134
|
-
socket.addEventListener('close', () => {
|
|
135
|
-
for (const entry of pending.values()) entry.reject(new Error('Browser connection closed.'));
|
|
136
|
-
pending.clear();
|
|
137
|
-
});
|
|
138
|
-
return {
|
|
139
|
-
on(listener) { listeners.add(listener); return () => listeners.delete(listener); },
|
|
140
|
-
send(method, params = {}, sessionId) {
|
|
141
|
-
const id = ++nextId;
|
|
142
|
-
const promise = new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
|
|
143
|
-
socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
|
|
144
|
-
return promise;
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function findDebuggerPort(directory, child, signal) {
|
|
150
|
-
for (let attempt = 0; attempt < 80; attempt++) {
|
|
151
|
-
signal?.throwIfAborted();
|
|
152
|
-
if (child.exitCode !== null) throw new Error('The browser exited before source discovery started.');
|
|
153
|
-
const text = await readFile(path.join(directory, 'DevToolsActivePort'), 'utf8').catch(() => null);
|
|
154
|
-
if (text) return Number(text.split(/\r?\n/)[0]);
|
|
155
|
-
await delay(100, undefined, { signal });
|
|
156
|
-
}
|
|
157
|
-
throw new Error('The browser did not start its debugging connection.');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Observe media requests in a temporary browser profile; no browser cookies are copied. */
|
|
161
|
-
export async function captureBrowserMedia(pageUrl, { signal, browserPaths = browserCandidates(), observeMs = 8000, deepScan = false } = {}) {
|
|
162
|
-
const directory = await mkdtemp(path.join(tmpdir(), 'veo-discover-'));
|
|
163
|
-
let child, socket;
|
|
164
|
-
const urls = new Set();
|
|
165
|
-
try {
|
|
166
|
-
let started = false;
|
|
167
|
-
for (const executable of browserPaths) {
|
|
168
|
-
try {
|
|
169
|
-
child = spawn(executable, ['--headless=new', '--remote-debugging-port=0', `--user-data-dir=${directory}`,
|
|
170
|
-
'--no-first-run', '--no-default-browser-check', '--autoplay-policy=no-user-gesture-required', 'about:blank'],
|
|
171
|
-
{ stdio: 'ignore', windowsHide: true });
|
|
172
|
-
await new Promise((resolve, reject) => {
|
|
173
|
-
child.once('spawn', resolve);
|
|
174
|
-
child.once('error', reject);
|
|
175
|
-
});
|
|
176
|
-
started = true;
|
|
177
|
-
break;
|
|
178
|
-
} catch { child = null; }
|
|
179
|
-
}
|
|
180
|
-
if (!started) throw new Error('No Chromium-based browser found. Set VEO_BROWSER_PATH to Chrome, Edge or Chromium.');
|
|
181
|
-
const port = await findDebuggerPort(directory, child, signal);
|
|
182
|
-
const endpoint = await fetch(`http://127.0.0.1:${port}/json/version`, { signal }).then(response => response.json());
|
|
183
|
-
socket = new WebSocket(endpoint.webSocketDebuggerUrl);
|
|
184
|
-
await abortable(new Promise((resolve, reject) => {
|
|
185
|
-
socket.addEventListener('open', resolve, { once: true });
|
|
186
|
-
socket.addEventListener('error', reject, { once: true });
|
|
187
|
-
}), signal);
|
|
188
|
-
const cdp = createCdp(socket);
|
|
189
|
-
const responses = new Map();
|
|
190
|
-
const bodyReads = [];
|
|
191
|
-
const sessions = new Set();
|
|
192
|
-
const collect = (value, mime = '') => {
|
|
193
|
-
if (!MEDIA_URL.test(value) && !MEDIA_TYPE.test(mime)) return;
|
|
194
|
-
try { urls.add(validateUrl(value)); } catch {}
|
|
195
|
-
};
|
|
196
|
-
cdp.on(message => {
|
|
197
|
-
if (message.method === 'Target.attachedToTarget') {
|
|
198
|
-
const attached = message.params.sessionId;
|
|
199
|
-
sessions.add(attached);
|
|
200
|
-
cdp.send('Network.enable', {}, attached).catch(() => {});
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (message.method === 'Target.detachedFromTarget') sessions.delete(message.params.sessionId);
|
|
204
|
-
if (message.method === 'Network.requestWillBeSent') collect(message.params.request?.url);
|
|
205
|
-
if (message.method === 'Network.responseReceived') {
|
|
206
|
-
const response = message.params.response;
|
|
207
|
-
collect(response.url, response.mimeType);
|
|
208
|
-
if (/(?:json|javascript|text\/plain)/i.test(response.mimeType || '')) {
|
|
209
|
-
responses.set(`${message.sessionId || ''}:${message.params.requestId}`, { sessionId: message.sessionId, requestId: message.params.requestId });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (message.method === 'Network.loadingFinished') {
|
|
213
|
-
const key = `${message.sessionId || ''}:${message.params.requestId}`;
|
|
214
|
-
const response = responses.get(key);
|
|
215
|
-
if (!response || message.params.encodedDataLength > 524288) return;
|
|
216
|
-
responses.delete(key);
|
|
217
|
-
if (!deepScan && bodyReads.length >= 100) return;
|
|
218
|
-
bodyReads.push(cdp.send('Network.getResponseBody', { requestId: response.requestId }, response.sessionId)
|
|
219
|
-
.then(result => { if (!result.base64Encoded) for (const url of mediaUrlsFromText(result.body)) urls.add(url); })
|
|
220
|
-
.catch(() => {}));
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
await abortable(cdp.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true }), signal);
|
|
224
|
-
const target = await abortable(cdp.send('Target.createTarget', { url: 'about:blank' }), signal);
|
|
225
|
-
const attached = await abortable(cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true }), signal);
|
|
226
|
-
sessions.add(attached.sessionId);
|
|
227
|
-
await abortable(cdp.send('Network.enable', {}, attached.sessionId), signal);
|
|
228
|
-
await abortable(cdp.send('Page.enable', {}, attached.sessionId), signal);
|
|
229
|
-
await abortable(cdp.send('Page.navigate', { url: pageUrl }, attached.sessionId), signal);
|
|
230
|
-
const observeUntil = Date.now() + observeMs;
|
|
231
|
-
while (Date.now() < observeUntil) {
|
|
232
|
-
signal?.throwIfAborted();
|
|
233
|
-
await Promise.allSettled([...sessions].map(sessionId => abortable(cdp.send('Runtime.evaluate', {
|
|
234
|
-
expression: PLAY_CONTROL, returnByValue: true,
|
|
235
|
-
}, sessionId), signal)));
|
|
236
|
-
if (!deepScan && urls.size > 0) break;
|
|
237
|
-
const remaining = observeUntil - Date.now();
|
|
238
|
-
if (remaining > 0) await delay(Math.min(300, remaining), undefined, { signal });
|
|
239
|
-
}
|
|
240
|
-
await Promise.race([Promise.allSettled(bodyReads), delay(1000, undefined, { signal }).catch(() => {})]);
|
|
241
|
-
return [...urls];
|
|
242
|
-
} catch (error) {
|
|
243
|
-
if (signal?.aborted && ['AbortError', 'TimeoutError'].includes(error?.name)) return [...urls];
|
|
244
|
-
throw error;
|
|
245
|
-
} finally {
|
|
246
|
-
socket?.close();
|
|
247
|
-
child?.kill();
|
|
248
|
-
await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => {});
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Verify candidates with yt-dlp before offering them to the user. */
|
|
253
|
-
export async function discoverSources(pageUrl, { signal, capture = captureBrowserMedia, inspect, limit = 30,
|
|
254
|
-
deepScan = false, timeoutMs = deepScan ? 120000 : 45000 } = {}) {
|
|
255
|
-
signal?.throwIfAborted();
|
|
256
|
-
const timeout = new AbortController();
|
|
257
|
-
const timer = setTimeout(() => timeout.abort(new DOMException('Source scan timed out.', 'TimeoutError')), timeoutMs);
|
|
258
|
-
const scanSignal = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
|
|
259
|
-
const sources = [];
|
|
260
|
-
const seen = new Set();
|
|
261
|
-
const formatReferences = new Map();
|
|
262
|
-
const mediaKey = value => {
|
|
263
|
-
try { const address = new URL(value); return `${address.origin}${address.pathname}`; }
|
|
264
|
-
catch { return null; }
|
|
265
|
-
};
|
|
266
|
-
try {
|
|
267
|
-
const observeMs = deepScan ? Math.min(30000, Math.floor(timeoutMs * 0.6)) : Math.min(8000, Math.floor(timeoutMs * 0.5));
|
|
268
|
-
let urls;
|
|
269
|
-
try { urls = await capture(pageUrl, { signal: scanSignal, observeMs, deepScan }); }
|
|
270
|
-
catch (error) { if (!timeout.signal.aborted) throw error; urls = []; }
|
|
271
|
-
signal?.throwIfAborted();
|
|
272
|
-
for (const url of deepScan ? urls : urls.slice(0, limit)) {
|
|
273
|
-
if (timeout.signal.aborted) break;
|
|
274
|
-
signal?.throwIfAborted();
|
|
275
|
-
try {
|
|
276
|
-
const metadata = await abortable(inspect(url, pageUrl, { signal: scanSignal }), scanSignal);
|
|
277
|
-
if (metadata.has_drm || metadata.is_live || metadata.formats?.length && metadata.formats.every(format => format.has_drm)
|
|
278
|
-
|| !metadata.formats?.length && !metadata.url) continue;
|
|
279
|
-
const address = new URL(url);
|
|
280
|
-
const key = `${address.origin}${address.pathname}:${metadata.id || ''}`;
|
|
281
|
-
if (seen.has(key)) continue;
|
|
282
|
-
seen.add(key);
|
|
283
|
-
sources.push({ ...summarizeSource(metadata, url, sources.length + 1, pageUrl), url });
|
|
284
|
-
formatReferences.set(url, new Set((metadata.formats || []).map(format => mediaKey(format.url)).filter(Boolean)));
|
|
285
|
-
} catch { /* A request alone is not proof of downloadable media. */ }
|
|
286
|
-
}
|
|
287
|
-
signal?.throwIfAborted();
|
|
288
|
-
// A master playlist and its variant streams describe the same video.
|
|
289
|
-
const distinct = sources.filter(source => !sources.some(other => other !== source
|
|
290
|
-
&& formatReferences.get(other.url)?.has(mediaKey(source.url))));
|
|
291
|
-
distinct.forEach((source, index) => { source.index = index + 1; });
|
|
292
|
-
Object.defineProperty(distinct, 'timedOut', { value: timeout.signal.aborted });
|
|
293
|
-
return distinct;
|
|
294
|
-
} finally {
|
|
295
|
-
clearTimeout(timer);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export function formatSource(source) {
|
|
300
|
-
const size = source.estimatedMiB === null ? 'Size unknown' : `about ${source.estimatedMiB} MiB`;
|
|
301
|
-
const bitrate = source.bitrateMbps === null ? '' : ` | ${source.bitrateMbps} Mbit/s`;
|
|
302
|
-
return `${source.index}. ${source.title} | ${source.source} | ${source.type} | ${source.quality.join(', ') || 'Quality unknown'} | ${size}${bitrate}`;
|
|
303
|
-
}
|
package/src/state.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
export function digest(value) {
|
|
6
|
-
return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 24);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export async function readJson(file, fallback) {
|
|
10
|
-
try { return JSON.parse(await readFile(file, 'utf8')); }
|
|
11
|
-
catch (error) { if (error.code === 'ENOENT') return fallback; throw error; }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function writeJson(file, value) {
|
|
15
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
16
|
-
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
17
|
-
try {
|
|
18
|
-
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
19
|
-
await rename(temporary, file);
|
|
20
|
-
} finally { await rm(temporary, { force: true }); }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Do not persist browser sessions or cookie paths in retry jobs.
|
|
24
|
-
export function publicOptions(options) {
|
|
25
|
-
const { cookies, cookiesFromBrowser, profile, urls, url, retryFailed, batchFile, mediaUrl, ...safe } = options;
|
|
26
|
-
return { ...safe, output: path.resolve(options.output) };
|
|
27
|
-
}
|