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/path-links.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { statSync } from 'node:fs';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
|
-
|
|
5
|
-
export function supportsPathLinks(stream, env = process.env) {
|
|
6
|
-
return Boolean(stream.isTTY && env.TERM !== 'dumb' &&
|
|
7
|
-
(env.WT_SESSION || env.TERM_PROGRAM === 'iTerm.app' || env.TERM_PROGRAM === 'WezTerm' ||
|
|
8
|
-
env.TERM_PROGRAM === 'vscode' || env.KITTY_WINDOW_ID || Number(env.VTE_VERSION) >= 5000));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// File links target their containing folder so clicking does not play the media.
|
|
12
|
-
export function folderLink(stream, target, label = target, env = process.env) {
|
|
13
|
-
const safeLabel = String(label).replace(/[\x00-\x1f\x7f-\x9f]/g, ' ');
|
|
14
|
-
if (!supportsPathLinks(stream, env)) return safeLabel;
|
|
15
|
-
try {
|
|
16
|
-
const absolute = path.resolve(target);
|
|
17
|
-
const folder = statSync(absolute).isDirectory() ? absolute : path.dirname(absolute);
|
|
18
|
-
const url = pathToFileURL(folder + path.sep).href;
|
|
19
|
-
return `\x1b]8;;${url}\x1b\\${safeLabel}\x1b]8;;\x1b\\`;
|
|
20
|
-
} catch { return safeLabel; }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Recognize absolute paths in application prose before display truncation.
|
|
24
|
-
// Resolve the longest existing candidate so spaces and parentheses in names survive.
|
|
25
|
-
export function linkOutputPaths(stream, original, displayed = original, env = process.env) {
|
|
26
|
-
if (!supportsPathLinks(stream, env) || /\x1b/.test(original) ||
|
|
27
|
-
/^\s*(?:["{[]|Retry:|Usage:|Manual command:|Run with|Stop:|Cleanup:)/.test(original)) return displayed;
|
|
28
|
-
const matches = [...original.matchAll(/(?:^|[\s("'])([A-Za-z]:[\\/]|\\\\[^\\\s]+\\|\/(?!\/))/g)];
|
|
29
|
-
for (const match of matches.reverse()) {
|
|
30
|
-
const start = match.index + match[0].length - match[1].length;
|
|
31
|
-
const rest = original.slice(start);
|
|
32
|
-
const ends = [rest.length];
|
|
33
|
-
for (let i = rest.length - 1; i > 0; i--) {
|
|
34
|
-
if (/[\s)"',;:.]/.test(rest[i])) ends.push(i);
|
|
35
|
-
}
|
|
36
|
-
let target;
|
|
37
|
-
for (const end of ends) {
|
|
38
|
-
const candidate = rest.slice(0, end);
|
|
39
|
-
try { statSync(candidate); target = candidate; break; } catch { /* try a prose boundary */ }
|
|
40
|
-
}
|
|
41
|
-
if (!target) continue;
|
|
42
|
-
// Truncation has already happened; keep the complete destination in the URL.
|
|
43
|
-
if (displayed.slice(start, start + target.length) === target) {
|
|
44
|
-
displayed = displayed.slice(0, start) + folderLink(stream, target, target, env) + displayed.slice(start + target.length);
|
|
45
|
-
} else {
|
|
46
|
-
const visible = displayed.slice(start);
|
|
47
|
-
if (visible.endsWith('…') && target.startsWith(visible.slice(0, -1))) {
|
|
48
|
-
displayed = displayed.slice(0, start) + folderLink(stream, target, visible, env);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return displayed;
|
|
53
|
-
}
|
package/src/paths.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Single source of truth for veo's per-user cache root. Used for the yt-dlp
|
|
6
|
-
* backend, the staged FFmpeg binaries, and small state files.
|
|
7
|
-
* An invalid relative OS cache setting must never write into the output directory.
|
|
8
|
-
*/
|
|
9
|
-
export function cacheBase({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
10
|
-
let base;
|
|
11
|
-
if (platform === 'win32') base = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
12
|
-
else if (platform === 'darwin') base = path.join(home, 'Library', 'Caches');
|
|
13
|
-
else base = env.XDG_CACHE_HOME || path.join(home, '.cache');
|
|
14
|
-
if (!path.isAbsolute(base)) base = path.join(home, '.cache');
|
|
15
|
-
return path.join(base, 'veo');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Per-user configuration directory, following each platform's own convention. */
|
|
19
|
-
export function configBase({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
20
|
-
let base;
|
|
21
|
-
if (platform === 'win32') base = env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
22
|
-
else if (platform === 'darwin') base = path.join(home, 'Library', 'Application Support');
|
|
23
|
-
else base = env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
24
|
-
if (!path.isAbsolute(base)) base = path.join(home, '.config');
|
|
25
|
-
return path.join(base, 'veo');
|
|
26
|
-
}
|
package/src/playlist.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export function validateItems(value) {
|
|
2
|
-
if (typeof value !== 'string' || !/^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/.test(value)) {
|
|
3
|
-
throw new Error('--playlist-items expects positive indices or ranges, e.g. 1,3-5.');
|
|
4
|
-
}
|
|
5
|
-
for (const part of value.split(',')) {
|
|
6
|
-
const [start, end = start] = part.split('-').map(Number);
|
|
7
|
-
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) {
|
|
8
|
-
throw new Error('--playlist-items requires ascending, positive ranges.');
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
return value;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function selectedEntries(metadata, selection) {
|
|
15
|
-
const ranges = selection ? validateItems(selection).split(',').map(part => part.split('-').map(Number)) : null;
|
|
16
|
-
const entries = (metadata.entries || []).map((entry, offset) => ({ entry, index: entry?.playlist_index || offset + 1 }));
|
|
17
|
-
const selected = entries.filter(({ index }) => !ranges || ranges.some(([start, end = start]) => index >= start && index <= end));
|
|
18
|
-
if (!selected.length) throw new Error('No playlist entries match the selection.');
|
|
19
|
-
return selected;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function sizeEstimate(entries) {
|
|
23
|
-
const sizes = entries.map(({ entry }) => entry?.filesize || entry?.filesize_approx);
|
|
24
|
-
const known = sizes.filter(value => Number.isFinite(value) && value > 0);
|
|
25
|
-
return { estimatedBytes: known.reduce((sum, value) => sum + value, 0), knownSizes: known.length, totalEntries: entries.length };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function describeEstimate(estimate) {
|
|
29
|
-
if (!estimate.knownSizes) return `${estimate.totalEntries} entries; total size unknown`;
|
|
30
|
-
return `${estimate.totalEntries} entries; approximately ${(estimate.estimatedBytes / 1048576).toFixed(1)} MiB${estimate.knownSizes < estimate.totalEntries ? ` for ${estimate.knownSizes} entries; remaining sizes unknown` : ''}`;
|
|
31
|
-
}
|
package/src/progress.js
DELETED
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
import { cleanText } from './utils.js';
|
|
2
|
-
import { createTerminalTitle } from './terminal-title.js';
|
|
3
|
-
import { terminalColumns } from './terminal-size.js';
|
|
4
|
-
import { createInlineRegion } from './inline-region.js';
|
|
5
|
-
|
|
6
|
-
const PROCESSING_LABELS = { Merger: 'Merging audio/video', VideoRemuxer: 'Changing video container', VideoConvertor: 'Converting video', ExtractAudio: 'Converting audio', EmbedSubtitle: 'Embedding subtitles', Metadata: 'Writing metadata', EmbedThumbnail: 'Embedding thumbnail', MoveFiles: 'Preparing saved file' };
|
|
7
|
-
|
|
8
|
-
export function styleText(stream, text, role = 'muted', enabled = true, env = process.env) {
|
|
9
|
-
if (!enabled || !stream.isTTY || Object.hasOwn(env, 'NO_COLOR') || env.TERM === 'dumb') return text;
|
|
10
|
-
const codes = { muted: 90, title: 1, profile: 97, success: 32, error: 31 };
|
|
11
|
-
return '\x1b[' + (codes[role] || 90) + 'm' + text + '\x1b[0m';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function bytes(value) {
|
|
15
|
-
if (!Number.isFinite(value) || value < 0) return '?';
|
|
16
|
-
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
|
17
|
-
let i = 0;
|
|
18
|
-
while (value >= 1024 && i < units.length - 1) { value /= 1024; i++; }
|
|
19
|
-
return `${value.toFixed(i ? 1 : 0)} ${units[i]}`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Count terminal cells conservatively, including wide titles and emoji.
|
|
23
|
-
function cells(text) {
|
|
24
|
-
return [...text].reduce((width, char) => width + (/\p{Mark}|\u200d/u.test(char) ? 0 : /[\u1100-\u115f\u2329\u232a\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff01-\uff60\uffe0-\uffe6]|\p{Extended_Pictographic}/u.test(char) ? 2 : 1), 0);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function fit(text, width) {
|
|
28
|
-
if (cells(text) <= width) return text;
|
|
29
|
-
let result = '';
|
|
30
|
-
for (const char of text) {
|
|
31
|
-
if (cells(result + char) > width - 1) break;
|
|
32
|
-
result += char;
|
|
33
|
-
}
|
|
34
|
-
return width > 0 ? result + '…' : '';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function wrap(text, width) {
|
|
38
|
-
if (!Number.isFinite(width)) return text;
|
|
39
|
-
const lines = [];
|
|
40
|
-
let line = '';
|
|
41
|
-
for (const char of text) {
|
|
42
|
-
if (line && cells(line + char) > width) { lines.push(line); line = ''; }
|
|
43
|
-
line += char;
|
|
44
|
-
}
|
|
45
|
-
return [...lines, line].join('\n');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function terminalText(stream, text) {
|
|
49
|
-
return fit(cleanText(text), stream.isTTY ? Math.max(1, terminalColumns(stream) - 1) : Infinity);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function terminalTitle(stream, text) {
|
|
53
|
-
const safe = cleanText(text);
|
|
54
|
-
if (!stream.isTTY) return safe;
|
|
55
|
-
const width = Math.max(1, terminalColumns(stream) - 1);
|
|
56
|
-
const lines = [];
|
|
57
|
-
let line = '';
|
|
58
|
-
for (const char of safe) {
|
|
59
|
-
if (cells(line + char) > width) {
|
|
60
|
-
if (lines.length === 2) return [...lines, fit(line + char + '…', width)].join('\n');
|
|
61
|
-
lines.push(line);
|
|
62
|
-
line = '';
|
|
63
|
-
}
|
|
64
|
-
line += char;
|
|
65
|
-
}
|
|
66
|
-
return [...lines, line].join('\n');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function formatProgress(data, { columns = Infinity, prefix = '' } = {}) {
|
|
70
|
-
const done = Number.isFinite(data.downloaded_bytes) ? data.downloaded_bytes : 0;
|
|
71
|
-
const total = data.total_bytes || data.total_bytes_estimate;
|
|
72
|
-
const estimated = !data.total_bytes && Boolean(data.total_bytes_estimate);
|
|
73
|
-
const percent = total > 0 ? Math.max(0, Math.min(estimated && data.status !== 'finished' ? 99 : 100, done / total * 100)) : null;
|
|
74
|
-
const eta = Number.isFinite(data.eta) ? `${Math.floor(data.eta / 60)}:${String(Math.floor(data.eta % 60)).padStart(2, '0')}` : '?';
|
|
75
|
-
const pct = `${estimated && data.status !== 'finished' ? '~' : ''}${percent === null ? '?' : percent.toFixed(0)}%`;
|
|
76
|
-
const size = `${estimated ? '~' : ''}${bytes(total)}`;
|
|
77
|
-
const label = prefix ? cleanText(prefix) + ' ' : '';
|
|
78
|
-
if (data.status === 'finished') return wrap(`${label}${bytes(done)} received; processing…`, columns);
|
|
79
|
-
const stats = [`${bytes(data.speed)}/s`, `${bytes(done)} / ${size}`, `ETA ${eta}`];
|
|
80
|
-
const barSize = Math.max(1, Math.min(20, Number.isFinite(columns) ? columns - cells(pct) - 3 : 20));
|
|
81
|
-
const filled = percent === null ? 0 : Math.round(percent / 100 * barSize);
|
|
82
|
-
const bar = `[${'='.repeat(filled)}${'-'.repeat(barSize - filled)}] ${pct}`;
|
|
83
|
-
const full = `${label}${bar} ${stats.join(' ')}`;
|
|
84
|
-
if (cells(full) <= columns) return full;
|
|
85
|
-
const lines = [];
|
|
86
|
-
if (label) lines.push(fit(label.trimEnd(), columns));
|
|
87
|
-
lines.push(wrap(bar, columns));
|
|
88
|
-
let statLine = '';
|
|
89
|
-
for (const stat of stats) {
|
|
90
|
-
if (statLine && cells(`${statLine} ${stat}`) > columns) { lines.push(wrap(statLine, columns)); statLine = ''; }
|
|
91
|
-
statLine += (statLine ? ' ' : '') + stat;
|
|
92
|
-
}
|
|
93
|
-
if (statLine) lines.push(wrap(statLine, columns));
|
|
94
|
-
return lines.join('\n');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function createReporter(stream = process.stderr, { setTitle = createTerminalTitle(stream), env = process.env } = {}) {
|
|
98
|
-
let color = true;
|
|
99
|
-
let region;
|
|
100
|
-
const muted = text => styleText(stream, text, 'muted', color, env);
|
|
101
|
-
const heading = text => styleText(stream, text, 'title', color, env);
|
|
102
|
-
let active = false;
|
|
103
|
-
let lastLog = 0;
|
|
104
|
-
let name = '';
|
|
105
|
-
let phase = 'Starting…';
|
|
106
|
-
let started = false;
|
|
107
|
-
let position = '';
|
|
108
|
-
let hasItem = false;
|
|
109
|
-
let streamName = '';
|
|
110
|
-
let animation;
|
|
111
|
-
let animationLabel = '';
|
|
112
|
-
let animationOwner;
|
|
113
|
-
let animationFrame = 0;
|
|
114
|
-
let redraw;
|
|
115
|
-
let listening = false;
|
|
116
|
-
let sizeTimer;
|
|
117
|
-
let lastWidth = terminalColumns(stream);
|
|
118
|
-
const append = render => {
|
|
119
|
-
if (region) region.log(render); else stream.write(render());
|
|
120
|
-
};
|
|
121
|
-
const eraseLive = () => {
|
|
122
|
-
if (!active) return;
|
|
123
|
-
stream.write('\r\x1b[2K\x1b[J');
|
|
124
|
-
};
|
|
125
|
-
const renderLive = text => {
|
|
126
|
-
eraseLive();
|
|
127
|
-
const rows = text.split('\n').length;
|
|
128
|
-
// Park the real cursor at the block's first row. The terminal relocates it
|
|
129
|
-
// during reflow; no saved cursor or guessed old line count is needed on resize.
|
|
130
|
-
stream.write(`\r\x1b[2K${text.replace(/\n/g, '\r\n')}${rows > 1 ? `\x1b[${rows - 1}A` : ''}\r`);
|
|
131
|
-
active = true;
|
|
132
|
-
};
|
|
133
|
-
const onResize = () => {
|
|
134
|
-
const width = terminalColumns(stream);
|
|
135
|
-
if (width === lastWidth) return;
|
|
136
|
-
lastWidth = width;
|
|
137
|
-
// Terminal scrollback reflows differently across hosts. Never infer cursor
|
|
138
|
-
// positions for past log/title lines; redraw only the owned live block.
|
|
139
|
-
if (active) redraw?.();
|
|
140
|
-
};
|
|
141
|
-
const watchResize = render => {
|
|
142
|
-
if (region) return;
|
|
143
|
-
if (stream.isTTY && terminalColumns(stream) !== lastWidth) onResize();
|
|
144
|
-
redraw = render;
|
|
145
|
-
if (!sizeTimer && stream.isTTY) {
|
|
146
|
-
lastWidth = terminalColumns(stream);
|
|
147
|
-
sizeTimer = setInterval(() => {
|
|
148
|
-
const width = terminalColumns(stream);
|
|
149
|
-
if (width !== lastWidth) onResize();
|
|
150
|
-
}, 150);
|
|
151
|
-
sizeTimer.unref?.();
|
|
152
|
-
}
|
|
153
|
-
if (!listening && stream.isTTY && stream.on) {
|
|
154
|
-
stream.on('resize', onResize);
|
|
155
|
-
listening = true;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
const stopAnimation = () => { if (animation) clearInterval(animation); animation = undefined; };
|
|
159
|
-
const display = value => terminalText(stream, value);
|
|
160
|
-
const animate = (label, owner) => {
|
|
161
|
-
clear();
|
|
162
|
-
if (!stream.isTTY) { stream.write(muted(label + '…') + '\n'); return; }
|
|
163
|
-
animationLabel = label;
|
|
164
|
-
animationOwner = owner;
|
|
165
|
-
animationFrame = 0;
|
|
166
|
-
const tick = () => {
|
|
167
|
-
const render = () => muted(display(animationLabel + '.'.repeat(animationFrame % 3 + 1)));
|
|
168
|
-
if (region) region.live(render); else renderLive(render());
|
|
169
|
-
animationFrame++;
|
|
170
|
-
};
|
|
171
|
-
watchResize(tick);
|
|
172
|
-
tick();
|
|
173
|
-
animation = setInterval(tick, 350);
|
|
174
|
-
animation.unref?.();
|
|
175
|
-
};
|
|
176
|
-
const processingResult = (label, status) => {
|
|
177
|
-
clear();
|
|
178
|
-
const failed = status === 'failed' || status === 'error';
|
|
179
|
-
const suffix = failed ? 'failed' : 'done';
|
|
180
|
-
append(() => muted(`${fit(label, Math.max(0, (stream.isTTY ? terminalColumns(stream) - 1 : Infinity) - suffix.length - 2))}: `) + styleText(stream, suffix, failed ? 'error' : 'success', color, env) + '\n');
|
|
181
|
-
watchResize(undefined);
|
|
182
|
-
};
|
|
183
|
-
const finishStatus = (message, owner, prefix = '', outcome) => {
|
|
184
|
-
const label = prefix + cleanText(message);
|
|
185
|
-
if (!animation || animationOwner !== owner || animationLabel !== label.slice(0, -1)) return;
|
|
186
|
-
if (outcome) { processingResult(label.slice(0, -1), outcome); return; }
|
|
187
|
-
clear();
|
|
188
|
-
append(() => muted(display(label)) + '\n');
|
|
189
|
-
watchResize(undefined);
|
|
190
|
-
};
|
|
191
|
-
const line = (data, prefix) => formatProgress(data, { prefix, columns: stream.isTTY ? Math.max(1, terminalColumns(stream) - 1) : Infinity });
|
|
192
|
-
const draw = (data, prefix) => {
|
|
193
|
-
if (region) { region.live(() => line(data, prefix)); return; }
|
|
194
|
-
const render = () => renderLive(line(data, prefix));
|
|
195
|
-
watchResize(render);
|
|
196
|
-
render();
|
|
197
|
-
};
|
|
198
|
-
const updateTitle = () => setTitle(`veo | ${phase}${name ? ` | ${name}` : ''}`);
|
|
199
|
-
function clear() {
|
|
200
|
-
stopAnimation();
|
|
201
|
-
region?.live(undefined);
|
|
202
|
-
if (sizeTimer) clearInterval(sizeTimer);
|
|
203
|
-
sizeTimer = undefined;
|
|
204
|
-
if (listening) stream.removeListener?.('resize', onResize);
|
|
205
|
-
listening = false;
|
|
206
|
-
redraw = undefined;
|
|
207
|
-
if (active && stream.isTTY) { eraseLive(); stream.write('\r\x1b[2K'); }
|
|
208
|
-
active = false;
|
|
209
|
-
animationLabel = '';
|
|
210
|
-
animationOwner = undefined;
|
|
211
|
-
}
|
|
212
|
-
const scoped = (index, total, title, parent = '') => {
|
|
213
|
-
let childName = cleanText(title), lastProgress = 0;
|
|
214
|
-
const owner = Symbol('scoped reporter');
|
|
215
|
-
const prefix = parent + '[' + index + '/' + total + '] ';
|
|
216
|
-
const log = (message, quiet = true) => {
|
|
217
|
-
phase = cleanText(message); name = childName;
|
|
218
|
-
const line = prefix + childName + ': ' + phase;
|
|
219
|
-
if (phase.endsWith('…')) animate(line.slice(0, -1), owner);
|
|
220
|
-
else { clear(); append(() => (quiet ? muted(display(line)) : display(line)) + '\n'); }
|
|
221
|
-
if (started) updateTitle();
|
|
222
|
-
};
|
|
223
|
-
return {
|
|
224
|
-
scoped: (index, total, title) => scoped(index, total, title, prefix),
|
|
225
|
-
name(value) { childName = cleanText(value); },
|
|
226
|
-
status: log,
|
|
227
|
-
finishStatus(message, outcome) { finishStatus(message, owner, prefix + childName + ': ', outcome); },
|
|
228
|
-
progress(data) {
|
|
229
|
-
if (stream.isTTY) {
|
|
230
|
-
draw(data, prefix + childName + ': ' + (data.stream || 'Media'));
|
|
231
|
-
} else if (Date.now() - lastProgress >= 5000 || data.status === 'finished') {
|
|
232
|
-
log((data.stream || 'Media') + ' ' + formatProgress(data), false);
|
|
233
|
-
lastProgress = Date.now();
|
|
234
|
-
}
|
|
235
|
-
},
|
|
236
|
-
processing(data) {
|
|
237
|
-
const label = prefix + childName + ': ' + (PROCESSING_LABELS[data.postprocessor] || 'Processing media');
|
|
238
|
-
if (['finished', 'failed', 'error'].includes(data.status)) processingResult(label, data.status);
|
|
239
|
-
else animate(label, owner);
|
|
240
|
-
phase = `${PROCESSING_LABELS[data.postprocessor] || 'Processing media'}${data.status === 'finished' ? ': done' : '…'}`;
|
|
241
|
-
name = childName; if (started) updateTitle();
|
|
242
|
-
},
|
|
243
|
-
finish() {},
|
|
244
|
-
failStep() { if (animation && animationOwner === owner) processingResult(animationLabel, 'failed'); },
|
|
245
|
-
};
|
|
246
|
-
};
|
|
247
|
-
return {
|
|
248
|
-
enableInline() {
|
|
249
|
-
if (!region && stream.isTTY && env.TERM !== 'dumb') { clear(); region = createInlineRegion(stream); }
|
|
250
|
-
},
|
|
251
|
-
output(destination) {
|
|
252
|
-
if (!stream.isTTY || !destination.isTTY) return destination;
|
|
253
|
-
return {
|
|
254
|
-
isTTY: true,
|
|
255
|
-
get columns() { return terminalColumns(destination); },
|
|
256
|
-
write(chunk, ...args) {
|
|
257
|
-
clear();
|
|
258
|
-
region?.commit();
|
|
259
|
-
return destination.write(chunk, ...args);
|
|
260
|
-
},
|
|
261
|
-
};
|
|
262
|
-
},
|
|
263
|
-
dispose() { clear(); region?.close(); region = undefined; },
|
|
264
|
-
configure(options) { color = options.color !== false; },
|
|
265
|
-
scoped,
|
|
266
|
-
item(index, total, title) {
|
|
267
|
-
clear(); position = total > 1 ? `[${index}/${total}] ` : ''; hasItem = true; name = cleanText(title); streamName = ''; lastLog = 0;
|
|
268
|
-
const titleText = `${position}${name}`;
|
|
269
|
-
const render = () => heading(terminalTitle(stream, titleText)) + '\n';
|
|
270
|
-
if (region) region.source(render); else append(render);
|
|
271
|
-
watchResize(undefined);
|
|
272
|
-
phase = 'Starting…'; if (started) updateTitle();
|
|
273
|
-
},
|
|
274
|
-
processing(data) {
|
|
275
|
-
const processor = cleanText(data.postprocessor || 'Processing');
|
|
276
|
-
const label = PROCESSING_LABELS[processor] || 'Processing media';
|
|
277
|
-
phase = `${label}${data.status === 'finished' ? ': done' : '…'}`;
|
|
278
|
-
if (started) updateTitle();
|
|
279
|
-
if (['finished', 'failed', 'error'].includes(data.status)) processingResult(`${position}${label}`, data.status);
|
|
280
|
-
else animate(`${position}${label}`);
|
|
281
|
-
},
|
|
282
|
-
start(title = '') { started = true; name = cleanText(title); phase = 'Starting…'; updateTitle(); },
|
|
283
|
-
name(title) {
|
|
284
|
-
const next = cleanText(title);
|
|
285
|
-
if (hasItem && next !== name) {
|
|
286
|
-
clear();
|
|
287
|
-
const titleText = `${position}${next}`;
|
|
288
|
-
const render = () => heading(terminalTitle(stream, titleText)) + '\n';
|
|
289
|
-
// Titles join the log stream at their chronological position, like
|
|
290
|
-
// without the inline region. Stored as a render function, they still
|
|
291
|
-
// re-render on resize (max three rows, current width).
|
|
292
|
-
if (region) region.log(render); else append(render);
|
|
293
|
-
watchResize(undefined);
|
|
294
|
-
}
|
|
295
|
-
name = next; if (started) updateTitle();
|
|
296
|
-
},
|
|
297
|
-
status(message) {
|
|
298
|
-
phase = cleanText(message);
|
|
299
|
-
if (started) updateTitle();
|
|
300
|
-
if (phase.endsWith('…')) animate(phase.slice(0, -1));
|
|
301
|
-
else {
|
|
302
|
-
clear();
|
|
303
|
-
const status = phase;
|
|
304
|
-
append(() => muted(display(status)) + '\n');
|
|
305
|
-
watchResize(undefined);
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
profile(value) {
|
|
309
|
-
const selected = value ? cleanText(value) : 'global (no profile)';
|
|
310
|
-
phase = `Profile: ${selected}`;
|
|
311
|
-
if (started) updateTitle();
|
|
312
|
-
clear();
|
|
313
|
-
append(() => value && value !== 'default'
|
|
314
|
-
? muted('Profile: ') + styleText(stream, fit(selected, stream.isTTY ? Math.max(0, terminalColumns(stream) - 10) : Infinity), 'profile', color, env) + '\n'
|
|
315
|
-
: muted(display(`Profile: ${selected}`)) + '\n');
|
|
316
|
-
},
|
|
317
|
-
finishStatus(message, outcome) { finishStatus(message, undefined, '', outcome); },
|
|
318
|
-
failStep() { if (animation && animationOwner === undefined) processingResult(animationLabel, 'failed'); },
|
|
319
|
-
complete() { clear(); phase = 'Done'; if (started) updateTitle(); },
|
|
320
|
-
fail(cancelled = false) { clear(); phase = cancelled ? 'Cancelled' : 'Failed'; if (started) updateTitle(); },
|
|
321
|
-
progress(data) {
|
|
322
|
-
if (data.stream && data.stream !== streamName) {
|
|
323
|
-
clear(); streamName = data.stream; lastLog = 0;
|
|
324
|
-
const label = `${position}${streamName} download`;
|
|
325
|
-
append(() => muted(display(label)) + '\n');
|
|
326
|
-
}
|
|
327
|
-
const total = data.total_bytes;
|
|
328
|
-
const percent = Number.isFinite(total) && total > 0 && Number.isFinite(data.downloaded_bytes)
|
|
329
|
-
? Math.max(0, Math.min(100, Math.round(data.downloaded_bytes / total * 100))) : null;
|
|
330
|
-
phase = data.status === 'finished' ? 'Processing…' : percent === null ? 'Downloading…' : `${percent}%`;
|
|
331
|
-
if (started) updateTitle();
|
|
332
|
-
if (stream.isTTY) {
|
|
333
|
-
draw(data, `${position}${streamName}`);
|
|
334
|
-
} else if (Date.now() - lastLog >= 5000 || data.status === 'finished') {
|
|
335
|
-
stream.write(`${position}${streamName ? `${streamName} ` : ''}${formatProgress(data)}\n`);
|
|
336
|
-
lastLog = Date.now();
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
finish() { clear(); region?.commit(); },
|
|
340
|
-
};
|
|
341
|
-
}
|
package/src/run-archive.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { lstat, open, readdir } from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { cacheBase } from './paths.js';
|
|
5
|
-
import { readJson, writeJson } from './state.js';
|
|
6
|
-
|
|
7
|
-
export const RUN_ARCHIVE_ID = /^[0-9a-z]{6}$/;
|
|
8
|
-
const SAMPLE_BYTES = 64 * 1024;
|
|
9
|
-
const MAX_SEARCH_FILES = 200_000;
|
|
10
|
-
const MEDIA_EXTENSIONS = new Set(['.mp4', '.mkv', '.webm', '.mov', '.m4v', '.mp3', '.m4a', '.aac', '.opus', '.ogg', '.flac', '.wav', '.mka', '.ts']);
|
|
11
|
-
|
|
12
|
-
export function runArchiveFile(id, root = cacheBase()) {
|
|
13
|
-
if (!RUN_ARCHIVE_ID.test(id)) throw new Error('Run ids have 6 letters or digits.');
|
|
14
|
-
return path.join(path.resolve(root), 'run-history', `${id}.json`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function isMediaFile(file) { return MEDIA_EXTENSIONS.has(path.extname(file).toLowerCase()); }
|
|
18
|
-
|
|
19
|
-
async function regularFile(file) {
|
|
20
|
-
const info = await lstat(file).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
|
|
21
|
-
return info?.isFile() && !info.isSymbolicLink() ? info : null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Sample three parts of a file so renames can be found without rereading a huge video. */
|
|
25
|
-
export async function fileFingerprint(file, knownInfo) {
|
|
26
|
-
const info = knownInfo || await regularFile(file);
|
|
27
|
-
if (!info) return null;
|
|
28
|
-
const digest = createHash('sha256').update(`veo-file-v1:${info.size}:`);
|
|
29
|
-
const positions = [...new Set([0, Math.floor(Math.max(0, info.size - SAMPLE_BYTES) / 2), Math.max(0, info.size - SAMPLE_BYTES)])];
|
|
30
|
-
const handle = await open(file, 'r');
|
|
31
|
-
try {
|
|
32
|
-
for (const position of positions) {
|
|
33
|
-
const bytes = Math.min(SAMPLE_BYTES, info.size - position);
|
|
34
|
-
const buffer = Buffer.alloc(bytes);
|
|
35
|
-
const { bytesRead } = await handle.read(buffer, 0, bytes, position);
|
|
36
|
-
if (bytesRead !== bytes) throw new Error(`File changed while recording its identity: ${file}`);
|
|
37
|
-
digest.update(String(position)).update(':').update(buffer);
|
|
38
|
-
}
|
|
39
|
-
} finally { await handle.close(); }
|
|
40
|
-
return { size: info.size, sampleSha256: digest.digest('hex'),
|
|
41
|
-
device: info.dev, inode: info.ino };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function archiveRun(job, { root = cacheBase(), now = new Date() } = {}) {
|
|
45
|
-
const id = job.runId;
|
|
46
|
-
if (!RUN_ARCHIVE_ID.test(id || '')) return null;
|
|
47
|
-
const items = [];
|
|
48
|
-
for (const item of job.items || []) {
|
|
49
|
-
const files = [];
|
|
50
|
-
for (const file of item.files || []) {
|
|
51
|
-
const media = isMediaFile(file);
|
|
52
|
-
files.push({ originalPath: path.resolve(file), media,
|
|
53
|
-
fingerprint: media ? item.fingerprints?.[file] || await fileFingerprint(file).catch(() => null) : null });
|
|
54
|
-
}
|
|
55
|
-
items.push({ url: item.url, status: item.status, error: item.error || null,
|
|
56
|
-
output: path.resolve(item.options?.output || job.options?.output || process.cwd()), files });
|
|
57
|
-
}
|
|
58
|
-
const archived = { version: 1, id, startedAt: job.startedAt || null, finishedAt: now.toISOString(),
|
|
59
|
-
status: items.every(item => ['saved', 'skipped'].includes(item.status)) ? 'completed' : 'incomplete', items };
|
|
60
|
-
if (await lstat(runArchiveFile(id, root)).then(() => true, error => { if (error.code === 'ENOENT') return false; throw error; })) {
|
|
61
|
-
throw new Error(`Run archive ${id} already exists; refusing to overwrite it.`);
|
|
62
|
-
}
|
|
63
|
-
await writeJson(runArchiveFile(id, root), archived);
|
|
64
|
-
return archived;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function readRunArchive(id, root = cacheBase()) {
|
|
68
|
-
const archive = await readJson(runArchiveFile(id, root), null);
|
|
69
|
-
if (!archive) throw new Error(`No finished run with id ${id} was found. Active runs appear in veo runs --json.`);
|
|
70
|
-
if (archive.version !== 1 || archive.id !== id || !Array.isArray(archive.items)) throw new Error(`Invalid archived run: ${id}`);
|
|
71
|
-
return archive;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function scan(root, wantedSizes, signal) {
|
|
75
|
-
const info = await regularFile(root);
|
|
76
|
-
if (info) throw new Error(`Search root is a file: ${root}`);
|
|
77
|
-
const directory = await lstat(root).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
|
|
78
|
-
if (!directory) return [];
|
|
79
|
-
if (!directory.isDirectory() || directory.isSymbolicLink()) throw new Error(`Search root is not a plain directory: ${root}`);
|
|
80
|
-
const candidates = [];
|
|
81
|
-
const pending = [root];
|
|
82
|
-
let seen = 0;
|
|
83
|
-
while (pending.length) {
|
|
84
|
-
signal?.throwIfAborted();
|
|
85
|
-
const current = pending.pop();
|
|
86
|
-
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
87
|
-
const file = path.join(current, entry.name);
|
|
88
|
-
if (entry.isDirectory()) pending.push(file);
|
|
89
|
-
else if (entry.isFile()) {
|
|
90
|
-
if (++seen > MAX_SEARCH_FILES) throw new Error(`Too many files under ${root}; use --search with a narrower directory.`);
|
|
91
|
-
const details = await regularFile(file);
|
|
92
|
-
if (details && wantedSizes.has(details.size)) candidates.push({ file, details });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return candidates;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Resolve original paths first, then use fingerprints to find renamed media. */
|
|
100
|
-
export async function locateRunFiles(archive, { searchRoot, signal } = {}) {
|
|
101
|
-
const items = [];
|
|
102
|
-
const missing = [];
|
|
103
|
-
for (const item of archive.items) {
|
|
104
|
-
signal?.throwIfAborted();
|
|
105
|
-
const files = [];
|
|
106
|
-
for (const entry of item.files || []) {
|
|
107
|
-
const details = await regularFile(entry.originalPath);
|
|
108
|
-
const fingerprint = details && entry.fingerprint ? await fileFingerprint(entry.originalPath, details).catch(error => { if (error.code === 'ENOENT') return null; throw error; }) : null;
|
|
109
|
-
const valid = details && (!entry.fingerprint || (fingerprint?.size === entry.fingerprint.size && fingerprint.sampleSha256 === entry.fingerprint.sampleSha256));
|
|
110
|
-
const resolved = { originalPath: entry.originalPath, path: valid ? entry.originalPath : null,
|
|
111
|
-
found: Boolean(valid), renamed: false, media: Boolean(entry.media) };
|
|
112
|
-
files.push(resolved);
|
|
113
|
-
if (!valid && entry.media && entry.fingerprint) missing.push({ resolved, entry, output: item.output });
|
|
114
|
-
}
|
|
115
|
-
items.push({ url: item.url, status: item.status, error: item.error || null, files });
|
|
116
|
-
}
|
|
117
|
-
if (missing.length) {
|
|
118
|
-
const roots = [...new Set(missing.map(item => item.output).concat(searchRoot ? [path.resolve(searchRoot)] : []))];
|
|
119
|
-
const wanted = new Set(missing.map(item => item.entry.fingerprint.size));
|
|
120
|
-
const matches = new Map();
|
|
121
|
-
for (const root of roots) for (const candidate of await scan(root, wanted, signal)) {
|
|
122
|
-
signal?.throwIfAborted();
|
|
123
|
-
const fingerprint = await fileFingerprint(candidate.file, candidate.details).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
|
|
124
|
-
if (!fingerprint) continue;
|
|
125
|
-
const key = `${fingerprint.size}:${fingerprint.sampleSha256}`;
|
|
126
|
-
const paths = matches.get(key) || new Set();
|
|
127
|
-
paths.add(candidate.file);
|
|
128
|
-
matches.set(key, paths);
|
|
129
|
-
}
|
|
130
|
-
for (const { resolved, entry } of missing) {
|
|
131
|
-
const key = `${entry.fingerprint.size}:${entry.fingerprint.sampleSha256}`;
|
|
132
|
-
const paths = [...(matches.get(key) || [])];
|
|
133
|
-
let selected = paths.length === 1 ? paths[0] : null;
|
|
134
|
-
if (!selected && paths.length > 1 && entry.fingerprint.inode > 0) {
|
|
135
|
-
const sameFile = [];
|
|
136
|
-
for (const file of paths) {
|
|
137
|
-
const details = await regularFile(file);
|
|
138
|
-
if (details?.dev === entry.fingerprint.device && details.ino === entry.fingerprint.inode) sameFile.push(file);
|
|
139
|
-
}
|
|
140
|
-
if (sameFile.length === 1) selected = sameFile[0];
|
|
141
|
-
}
|
|
142
|
-
if (selected) {
|
|
143
|
-
resolved.path = selected; resolved.found = true; resolved.renamed = selected !== entry.originalPath;
|
|
144
|
-
} else if (paths.length > 1) resolved.candidates = paths;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return items;
|
|
148
|
-
}
|