veodl 1.8.2 → 1.9.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 +28 -0
- package/README.md +796 -725
- package/docs/AGENT_GUIDE.md +82 -66
- package/package.json +1 -2
- package/src/alias.js +258 -0
- package/src/cli.js +613 -454
- package/src/config-diagnostics.js +145 -144
- package/src/config-editor.js +285 -281
- package/src/config-template.js +224 -171
- package/src/config.js +217 -213
- package/src/doctor.js +1 -1
- package/src/downloader.js +601 -600
- package/src/interactive.js +115 -82
- package/src/output.js +1 -1
- package/src/runs.js +343 -333
- package/src/source-discovery.js +251 -0
- package/src/state.js +27 -27
- package/src/uninstall.js +222 -0
- package/src/updater.js +5 -3
package/src/runs.js
CHANGED
|
@@ -1,333 +1,343 @@
|
|
|
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, 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
|
-
|
|
130
|
-
timer
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
async
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return `${
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
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, 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
|
+
playlist: Boolean(details.playlist),
|
|
152
|
+
job: text(details.job) || null,
|
|
153
|
+
});
|
|
154
|
+
await writeJson(file, record);
|
|
155
|
+
},
|
|
156
|
+
async unregister() {
|
|
157
|
+
cancelling = true;
|
|
158
|
+
clearInterval(timer);
|
|
159
|
+
await rm(file, { force: true });
|
|
160
|
+
await rm(request, { force: true });
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Per-item progress of a run, read from the job file it writes as it works. */
|
|
166
|
+
async function readProgress(jobFile) {
|
|
167
|
+
if (!jobFile) return null;
|
|
168
|
+
const job = await readRecordFile(jobFile);
|
|
169
|
+
if (job?.version !== 1 || !Array.isArray(job.items)) return null;
|
|
170
|
+
const items = job.items.map(item => ({
|
|
171
|
+
url: text(item?.url),
|
|
172
|
+
status: ITEM_STATUSES.has(item?.status) ? item.status : 'pending',
|
|
173
|
+
files: (Array.isArray(item?.files) ? item.files : []).map(text).filter(Boolean),
|
|
174
|
+
error: text(item?.error) || null,
|
|
175
|
+
}));
|
|
176
|
+
const counts = { total: items.length, pending: 0, running: 0, saved: 0, skipped: 0, failed: 0, cancelled: 0 };
|
|
177
|
+
for (const item of items) counts[item.status]++;
|
|
178
|
+
return { items, counts };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function describeRuns(root) {
|
|
182
|
+
const runs = await listRuns(root);
|
|
183
|
+
return Promise.all(runs.map(async run => ({ ...run, progress: await readProgress(run.job) })));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function summarizeProgress(progress) {
|
|
187
|
+
const counts = progress?.counts;
|
|
188
|
+
if (!counts?.total) return 'starting';
|
|
189
|
+
const parts = [];
|
|
190
|
+
const done = counts.saved + counts.skipped;
|
|
191
|
+
if (done) parts.push(`${done}/${counts.total} done`);
|
|
192
|
+
if (counts.running) parts.push(`${counts.running} running`);
|
|
193
|
+
if (counts.failed) parts.push(`${counts.failed} failed`);
|
|
194
|
+
if (counts.cancelled) parts.push(`${counts.cancelled} cancelled`);
|
|
195
|
+
if (counts.pending) parts.push(parts.length ? `${counts.pending} pending` : `${counts.total} pending`);
|
|
196
|
+
return parts.join(', ');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function shortDuration(ms) {
|
|
200
|
+
if (!Number.isFinite(ms) || ms < 0) return '0s';
|
|
201
|
+
const seconds = Math.floor(ms / 1000);
|
|
202
|
+
if (seconds < 60) return `${seconds}s`;
|
|
203
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
204
|
+
return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const shorten = (value, width = OUTPUT_WIDTH) => value.length <= width ? value : `…${value.slice(-(width - 1))}`;
|
|
208
|
+
const label = run => run.id || `PID ${run.pid}`;
|
|
209
|
+
// Records of older versions have no start time, so no uptime can be shown.
|
|
210
|
+
const uptime = run => Number.isFinite(Date.parse(run.startedAt)) ? shortDuration(Date.now() - Date.parse(run.startedAt)) : null;
|
|
211
|
+
|
|
212
|
+
function describeMedia(run) {
|
|
213
|
+
const detail = run.media === 'audio' ? run.format : run.quality;
|
|
214
|
+
return `${run.media || 'not resolved yet'}${detail ? `, ${detail}` : ''}${run.playlist ? ', playlist' : ''}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function formatRuns(runs) {
|
|
218
|
+
if (!runs.length) return 'veo runs\n\nNo veo runs are active. Start a download to see it here.\n';
|
|
219
|
+
const rows = runs.map(run => {
|
|
220
|
+
const running = uptime(run);
|
|
221
|
+
return [run.id || '-', String(run.pid), run.alive ? `running${running ? ` ${running}` : ''}` : 'stale',
|
|
222
|
+
localStamp(run.startedAt), summarizeProgress(run.progress), run.output ? shorten(text(run.output)) : '-'];
|
|
223
|
+
});
|
|
224
|
+
const header = ['ID', 'PID', 'STATE', 'STARTED', 'ITEMS', 'OUTPUT'];
|
|
225
|
+
const widths = header.map((title, index) => Math.max(title.length, ...rows.map(row => row[index].length)));
|
|
226
|
+
const line = cells => cells.map((cell, index) => cell.padEnd(widths[index])).join(' ').trimEnd();
|
|
227
|
+
return `${['veo runs', '', line(header), ...rows.map(line), '', 'Details: veo runs <id> Stop: veo stop [id]'].join('\n')}\n`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function formatRunDetails(run) {
|
|
231
|
+
const lines = [`veo runs ${run.id || '-'}`, ''];
|
|
232
|
+
const running = uptime(run);
|
|
233
|
+
lines.push(`State: ${run.alive ? `running (PID ${run.pid})` : `stale (PID ${run.pid} is gone)`}`);
|
|
234
|
+
lines.push(`Started: ${localStamp(run.startedAt)}${run.alive && running ? ` (${running} ago)` : ''}`);
|
|
235
|
+
lines.push(`Media: ${describeMedia(run)}`);
|
|
236
|
+
lines.push(`Output: ${text(run.output) || 'not resolved yet'}`);
|
|
237
|
+
lines.push(`Job: ${text(run.job) || 'not created yet'}`);
|
|
238
|
+
const urls = (Array.isArray(run.urls) ? run.urls : []).map(text).filter(Boolean);
|
|
239
|
+
lines.push(`URLs: ${urls.length ? `${urls.length}` : 'not resolved yet'}`);
|
|
240
|
+
urls.slice(0, SHOWN_ITEMS).forEach((url, index) => lines.push(` ${index + 1}. ${url}`));
|
|
241
|
+
if (urls.length > SHOWN_ITEMS) lines.push(` … and ${urls.length - SHOWN_ITEMS} more`);
|
|
242
|
+
const items = run.progress?.items || [];
|
|
243
|
+
if (items.length) {
|
|
244
|
+
lines.push(`Items: ${summarizeProgress(run.progress)}`);
|
|
245
|
+
items.slice(0, SHOWN_ITEMS).forEach((item, index) => {
|
|
246
|
+
const files = item.files.length ? ` ${item.files[0]}${item.files.length > 1 ? ` (+${item.files.length - 1} file(s))` : ''}` : '';
|
|
247
|
+
lines.push(` ${String(index + 1).padStart(2)}. ${item.status}${item.status === 'saved' ? files : ` ${item.url}`}`);
|
|
248
|
+
if (item.error) lines.push(` ${item.error}`);
|
|
249
|
+
});
|
|
250
|
+
if (items.length > SHOWN_ITEMS) lines.push(` … and ${items.length - SHOWN_ITEMS} more item(s)`);
|
|
251
|
+
}
|
|
252
|
+
lines.push('');
|
|
253
|
+
lines.push(run.alive && run.id ? `Stop: veo stop ${run.id}` : 'Cleanup: veo stop (removes stale records)');
|
|
254
|
+
return `${lines.join('\n')}\n`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeId(value) {
|
|
258
|
+
const id = cleanText(value).toLowerCase();
|
|
259
|
+
if (!RUN_ID.test(id)) throw new Error(`Invalid run id "${cleanText(value)}". Run ids have 6 characters and are listed by veo runs.`);
|
|
260
|
+
return id;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function runsMain(args = [], { stdout = process.stdout, root = cacheBase() } = {}) {
|
|
264
|
+
[args, stdout] = commandOutput(args, stdout);
|
|
265
|
+
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
266
|
+
stdout.write(RUNS_HELP);
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
const json = args.includes('--json');
|
|
270
|
+
args = args.filter(arg => arg !== '--json');
|
|
271
|
+
if (args.length > 1) throw new Error('Usage: veo runs [id] [--json]');
|
|
272
|
+
const runs = await describeRuns(root);
|
|
273
|
+
if (!args.length) {
|
|
274
|
+
if (json) stdout.write(`${JSON.stringify({ count: runs.length, runs: runs.map(publicRun) })}\n`);
|
|
275
|
+
else stdout.write(formatRuns(runs));
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
const id = normalizeId(args[0]);
|
|
279
|
+
const run = runs.find(item => item.id === id);
|
|
280
|
+
if (!run) throw new Error(`No veo run with id ${id} is active. List runs with veo runs.`);
|
|
281
|
+
if (json) stdout.write(`${JSON.stringify(publicRun(run))}\n`);
|
|
282
|
+
else stdout.write(formatRunDetails(run));
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function publicRun(run) {
|
|
287
|
+
return { id: run.id, pid: run.pid, state: run.alive ? 'running' : 'stale', startedAt: run.startedAt || null,
|
|
288
|
+
urls: Array.isArray(run.urls) ? run.urls : [], output: run.output || null, media: run.media || null,
|
|
289
|
+
quality: run.quality || null, format: run.format || null, playlist: Boolean(run.playlist), job: run.job || null,
|
|
290
|
+
progress: run.progress || null };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* A run answers a stop request by cleaning up its own record, so a run counts as
|
|
295
|
+
* gone as soon as its record disappears — a live-looking process is not enough
|
|
296
|
+
* (the pid may already belong to something else).
|
|
297
|
+
*/
|
|
298
|
+
export async function waitForExit(runs, timeoutMs) {
|
|
299
|
+
const pending = async () => {
|
|
300
|
+
for (const run of runs) if (alive(run.pid) && await exists(run.file)) return true;
|
|
301
|
+
return false;
|
|
302
|
+
};
|
|
303
|
+
const deadline = Date.now() + timeoutMs;
|
|
304
|
+
while (await pending()) {
|
|
305
|
+
if (Date.now() >= deadline) return false;
|
|
306
|
+
await delay(100);
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function stopMain(args = [], { stdout = process.stdout, root = cacheBase(), timeoutMs = STOP_TIMEOUT_MS } = {}) {
|
|
312
|
+
[args, stdout] = commandOutput(args, stdout);
|
|
313
|
+
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
314
|
+
stdout.write(STOP_HELP);
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
if (args.length > 1) throw new Error('Usage: veo stop [id]');
|
|
318
|
+
let targets = await listRuns(root);
|
|
319
|
+
if (args.length) {
|
|
320
|
+
const id = normalizeId(args[0]);
|
|
321
|
+
targets = targets.filter(run => run.id === id);
|
|
322
|
+
if (!targets.length) throw new Error(`No veo run with id ${id} is active. List runs with veo runs.`);
|
|
323
|
+
}
|
|
324
|
+
const stale = targets.filter(run => !run.alive);
|
|
325
|
+
const live = targets.filter(run => run.alive);
|
|
326
|
+
for (const run of live) await writeFile(`${run.file}.cancel`, 'stop');
|
|
327
|
+
if (live.length) await waitForExit(live, timeoutMs);
|
|
328
|
+
const remaining = [];
|
|
329
|
+
for (const run of live) if (await exists(run.file) && alive(run.pid)) remaining.push(run);
|
|
330
|
+
const stopped = live.filter(run => !remaining.includes(run));
|
|
331
|
+
for (const run of [...stale, ...stopped]) {
|
|
332
|
+
await rm(run.file, { force: true });
|
|
333
|
+
await rm(`${run.file}.cancel`, { force: true });
|
|
334
|
+
}
|
|
335
|
+
const lines = ['veo stop', ''];
|
|
336
|
+
if (stopped.length) lines.push(`Stopped: ${stopped.map(label).join(', ')}`);
|
|
337
|
+
if (stale.length) lines.push(`Removed ${stale.length} stale record(s): ${stale.map(label).join(', ')}`);
|
|
338
|
+
if (remaining.length) lines.push(`Still running: ${remaining.map(label).join(', ')}`);
|
|
339
|
+
if (!stopped.length && !stale.length && !remaining.length) lines.push('No veo runs were active.');
|
|
340
|
+
if (stopped.length) lines.push('', 'Partial data and retry jobs are kept; veo flush removes them.');
|
|
341
|
+
stdout.write(`${lines.join('\n')}\n`);
|
|
342
|
+
return remaining.length ? 1 : 0;
|
|
343
|
+
}
|