veodl 1.12.1 → 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.
Files changed (60) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -21
  3. package/README.md +178 -97
  4. package/bin/veo.js +47 -2
  5. package/docs/AGENT_GUIDE.md +1 -1
  6. package/native/android-arm64/veo +0 -0
  7. package/native/darwin-arm64/veo +0 -0
  8. package/native/darwin-x64/veo +0 -0
  9. package/native/linux-arm64/veo +0 -0
  10. package/native/linux-x64/veo +0 -0
  11. package/native/win32-arm64/veo.exe +0 -0
  12. package/native/win32-x64/veo.exe +0 -0
  13. package/package.json +53 -57
  14. package/src/alias.js +0 -258
  15. package/src/backend-update.js +0 -191
  16. package/src/backend.js +0 -572
  17. package/src/changes.js +0 -278
  18. package/src/cli.js +0 -785
  19. package/src/compatibility.js +0 -39
  20. package/src/config-clipboard.js +0 -67
  21. package/src/config-diagnostics.js +0 -155
  22. package/src/config-editor.js +0 -404
  23. package/src/config-errors.js +0 -64
  24. package/src/config-reset.js +0 -39
  25. package/src/config-template.js +0 -227
  26. package/src/config.js +0 -307
  27. package/src/disk-space.js +0 -68
  28. package/src/doctor.js +0 -306
  29. package/src/download-cache.js +0 -31
  30. package/src/downloader.js +0 -704
  31. package/src/execution.js +0 -68
  32. package/src/experimental-fragments.js +0 -29
  33. package/src/flush.js +0 -66
  34. package/src/history.js +0 -173
  35. package/src/inline-region.js +0 -72
  36. package/src/inspect-media.js +0 -179
  37. package/src/interactive.js +0 -120
  38. package/src/jobs.js +0 -218
  39. package/src/legacy-config-comments.js +0 -106
  40. package/src/media-assets.js +0 -272
  41. package/src/naming.js +0 -54
  42. package/src/open-file.js +0 -15
  43. package/src/option-suggestions.js +0 -43
  44. package/src/output.js +0 -81
  45. package/src/path-links.js +0 -53
  46. package/src/paths.js +0 -26
  47. package/src/playlist.js +0 -31
  48. package/src/progress.js +0 -341
  49. package/src/run-archive.js +0 -148
  50. package/src/runs.js +0 -346
  51. package/src/source-discovery.js +0 -305
  52. package/src/state.js +0 -27
  53. package/src/stats.js +0 -63
  54. package/src/terminal-size.js +0 -8
  55. package/src/terminal-title.js +0 -19
  56. package/src/tool-setup.js +0 -289
  57. package/src/uninstall.js +0 -222
  58. package/src/updater.js +0 -229
  59. package/src/utils.js +0 -208
  60. package/src/version.js +0 -38
@@ -1,39 +0,0 @@
1
- import path from 'node:path';
2
- import { rename, rm } from 'node:fs/promises';
3
-
4
- export async function ensureCompatibility(file, { backend, runner, signal, convert = false, reporter }) {
5
- const executable = name => path.join(backend.ffmpegLocation, name + (process.platform === 'win32' ? '.exe' : ''));
6
- let streams;
7
- try {
8
- const raw = await runner(executable('ffprobe'), ['-v', 'error', '-show_streams', '-of', 'json', file], { signal });
9
- streams = JSON.parse(raw).streams;
10
- if (!Array.isArray(streams)) throw new Error('No stream metadata');
11
- } catch (error) {
12
- if (signal?.aborted || convert) throw error;
13
- reporter?.status('Playback compatibility could not be checked; original media retained.');
14
- return file;
15
- }
16
- const video = streams.find(stream => stream.codec_type === 'video' && !stream.disposition?.attached_pic);
17
- const audio = streams.filter(stream => stream.codec_type === 'audio');
18
- if (!video) return file;
19
- const videoOK = video.codec_name === 'h264' && video.pix_fmt === 'yuv420p';
20
- const audioOK = audio.every(stream => stream.codec_name === 'aac');
21
- if (!convert) {
22
- if (!videoOK || !audioOK) reporter?.status(`Playback note: ${video.codec_name}/${audio.map(stream => stream.codec_name).join(',') || 'no audio'} may require additional player codecs. Original quality retained; use --compatible for H.264/AAC conversion.`);
23
- return file;
24
- }
25
- if (videoOK && audioOK && path.extname(file) === '.mp4') return file;
26
- if (!videoOK && (video.color_transfer === 'smpte2084' || video.color_transfer === 'arib-std-b67')) throw new Error('HDR conversion needs tone mapping; original download retained. Use an HDR-capable player or disable --compatible.');
27
- reporter?.status(videoOK && audioOK ? 'Preparing compatible MP4 without re-encoding…' : 'Converting to compatible H.264/AAC; this takes time and may lose quality…');
28
- const temporary = path.join(path.dirname(file), 'compatible-output.mp4');
29
- const target = path.join(path.dirname(file), 'media.mp4');
30
- try {
31
- await runner(executable('ffmpeg'), ['-hide_banner', '-loglevel', 'error', '-nostdin', '-y', '-i', file,
32
- '-map', '0:v:0', '-map', '0:a?', '-map_metadata', '0',
33
- '-c:v', videoOK ? 'copy' : 'libx264', ...(!videoOK ? ['-preset', 'fast', '-crf', '18', '-pix_fmt', 'yuv420p'] : []),
34
- '-c:a', audioOK ? 'copy' : 'aac', ...(!audioOK ? ['-b:a', '192k'] : []), '-movflags', '+faststart', temporary], { signal });
35
- await rename(temporary, target);
36
- if (file !== target) await rm(file);
37
- return target;
38
- } finally { await rm(temporary, { force: true }); }
39
- }
@@ -1,67 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
-
3
- const MAX_TEXT = 2 * 1024 * 1024;
4
-
5
- // Prefer the clipboard for the active display server, then try the other common
6
- // Linux tools. A terminal reached over SSH may have neither display available.
7
- export function clipboardCommands(operation, { platform = process.platform, env = process.env } = {}) {
8
- if (platform === 'win32') {
9
- const setup = 'Add-Type -AssemblyName System.Windows.Forms; [Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false); [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); ';
10
- return [['powershell.exe', ['-NoProfile', '-NonInteractive', '-Sta', '-Command', setup + (operation === 'copy'
11
- ? '[System.Windows.Forms.Clipboard]::SetText([Console]::In.ReadToEnd())'
12
- : '[Console]::Out.Write([System.Windows.Forms.Clipboard]::GetText())')]]];
13
- }
14
- if (platform === 'darwin') return [[operation === 'copy' ? 'pbcopy' : 'pbpaste', []]];
15
- if (platform === 'android') return [[operation === 'copy' ? 'termux-clipboard-set' : 'termux-clipboard-get', []]];
16
- const wayland = [operation === 'copy' ? 'wl-copy' : 'wl-paste', operation === 'copy' ? [] : ['--no-newline']];
17
- const xclip = ['xclip', ['-selection', 'clipboard', operation === 'copy' ? '-in' : '-out']];
18
- const xsel = ['xsel', ['--clipboard', operation === 'copy' ? '--input' : '--output']];
19
- const x11 = [xclip, xsel];
20
- return env.WAYLAND_DISPLAY ? [wayland, ...x11] : env.DISPLAY ? [...x11, wayland] : [wayland, ...x11];
21
- }
22
-
23
- function runCommand([program, args], operation, value) {
24
- return new Promise((resolve, reject) => {
25
- // Clipboard owners such as xclip and wl-copy may continue in the background.
26
- // Do not keep their inherited output pipes open after the command exits.
27
- const child = spawn(program, args, {
28
- shell: false, windowsHide: true,
29
- stdio: operation === 'copy' ? ['pipe', 'ignore', 'ignore'] : ['ignore', 'pipe', 'ignore'],
30
- });
31
- const timer = setTimeout(() => child.kill(), 5000);
32
- const chunks = [];
33
- let size = 0, failed = false;
34
- child.stdout?.on('data', chunk => {
35
- size += chunk.length;
36
- if (size > MAX_TEXT) child.kill();
37
- else chunks.push(chunk);
38
- });
39
- child.stdin?.on('error', () => {});
40
- child.on('error', () => { failed = true; });
41
- child.on('close', code => {
42
- clearTimeout(timer);
43
- if (failed || code !== 0 || size > MAX_TEXT) reject(new Error('Clipboard command failed.'));
44
- else resolve(operation === 'paste' ? Buffer.concat(chunks).toString('utf8') : undefined);
45
- });
46
- child.stdin?.end(value);
47
- });
48
- }
49
-
50
- export async function clipboardText(operation, value = '', options = {}) {
51
- if (Buffer.byteLength(value, 'utf8') > MAX_TEXT) throw new Error('Selection is too large for the editor clipboard.');
52
- const candidates = clipboardCommands(operation, options);
53
- for (const candidate of candidates) {
54
- try { return await (options.run || runCommand)(candidate, operation, value); }
55
- catch { /* Try the next available display-server clipboard. */ }
56
- }
57
- const platform = options.platform || process.platform;
58
- const hint = platform === 'linux'
59
- ? ' Install wl-clipboard, xclip or xsel and use a graphical session.'
60
- : platform === 'android' ? ' Install the Termux:API app and pkg install termux-api.' : '';
61
- throw new Error(`System clipboard is unavailable.${hint}`);
62
- }
63
-
64
- export const configClipboard = {
65
- copy: value => clipboardText('copy', value),
66
- paste: () => clipboardText('paste'),
67
- };
@@ -1,155 +0,0 @@
1
- import { CONFIG_KEYS, RESERVED_PROFILE_NAMES, parseConfigText, effectiveConfig, applyProfile } from './config.js';
2
- import { stripConfigComments } from './config-template.js';
3
- import { QUALITIES, AUDIO_FORMATS, VIDEO_FORMATS } from './utils.js';
4
-
5
- // Parse only after JSON.parse succeeds. Keep paths and source spans, including
6
- // escaped property names and repeated settings in different profiles.
7
- export function configSource(text) {
8
- const clean = stripConfigComments(text);
9
- const data = JSON.parse(clean), nodes = new Map();
10
- const tokens = [...clean.matchAll(/"(?:\\.|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}\[\]:,]/g)];
11
- let index = 0;
12
- const bom = text.startsWith('\uFEFF') ? 1 : 0;
13
- const visit = (path, keyToken) => {
14
- const first = tokens[index++];
15
- if (first[0] === '{') {
16
- while (tokens[index][0] !== '}') {
17
- const key = tokens[index++]; index++; // colon
18
- visit([...path, JSON.parse(key[0])], key);
19
- if (tokens[index][0] === ',') index++;
20
- }
21
- index++;
22
- } else if (first[0] === '[') {
23
- let item = 0;
24
- while (tokens[index][0] !== ']') {
25
- visit([...path, item++]);
26
- if (tokens[index][0] === ',') index++;
27
- }
28
- index++;
29
- }
30
- const last = tokens[index - 1];
31
- nodes.set(JSON.stringify(path), { path, start: first.index + bom, end: last.index + last[0].length + bom,
32
- keyStart: keyToken?.index + bom, keyEnd: keyToken ? keyToken.index + keyToken[0].length + bom : undefined });
33
- };
34
- visit([]);
35
- return { data, nodes };
36
- }
37
-
38
- function distance(a, b) {
39
- let row = Array.from({ length: b.length + 1 }, (_, i) => i);
40
- for (let i = 0; i < a.length; i++) {
41
- const next = [i + 1];
42
- for (let j = 0; j < b.length; j++) next.push(Math.min(next[j] + 1, row[j + 1] + 1, row[j] + (a[i] !== b[j])));
43
- row = next;
44
- }
45
- return row[b.length];
46
- }
47
-
48
- const keys = Object.keys(CONFIG_KEYS);
49
- function suggestion(key, choices) {
50
- const ranked = choices.map(value => [value, distance(key.toLowerCase(), value.toLowerCase())]).sort((a, b) => a[1] - b[1]);
51
- return ranked[0]?.[1] <= 2 ? ranked[0][0] : null;
52
- }
53
-
54
- function located(text, node, message, property = false, choices = []) {
55
- const start = (property ? node?.keyStart : node?.start) ?? 0;
56
- const end = (property ? node?.keyEnd : node?.end) ?? start + 1;
57
- return { message, start, end, line: text.slice(0, start).split('\n').length,
58
- column: start - text.lastIndexOf('\n', start - 1), choices };
59
- }
60
-
61
- function valueChoices(key, config = {}) {
62
- if (CONFIG_KEYS[key] === 'boolean') return [true, false];
63
- if (key === 'quality') return QUALITIES;
64
- if (key === 'format') return config.audio ? AUDIO_FORMATS : VIDEO_FORMATS;
65
- if (key === 'timeout') return ['30s', '45s', '1m', '2m', '5m'];
66
- if (['concurrentDownloads', 'playlistConcurrency'].includes(key)) return [1, 2, 3, 4];
67
- if (key === 'concurrentFragments') return Array.from({ length: 64 }, (_, i) => i + 1);
68
- return [];
69
- }
70
-
71
- export async function semanticIssue(text) {
72
- const { data, nodes } = configSource(text);
73
- const node = path => nodes.get(JSON.stringify(path));
74
- const object = value => value && typeof value === 'object' && !Array.isArray(value);
75
- if (!object(data)) return located(text, node([]), 'The config must contain a JSON object.');
76
- const check = (settings, path) => {
77
- for (const [key, value] of Object.entries(settings)) {
78
- if (!path.length && ['$schema', 'profiles', 'activeProfile'].includes(key)) continue;
79
- const target = node([...path, key]);
80
- if (!Object.hasOwn(CONFIG_KEYS, key)) {
81
- const proposed = suggestion(key, path.length ? keys : [...keys, 'profiles', '$schema']);
82
- return located(text, target, `Unknown setting "${key}".${proposed ? ` Did you mean "${proposed}"?` : ''}`, true, proposed ? [proposed] : []);
83
- }
84
- if (typeof value !== CONFIG_KEYS[key]) {
85
- const choices = valueChoices(key, settings);
86
- return located(text, target, `"${key}" must be a ${CONFIG_KEYS[key]}.${choices.length ? ` Choose: ${choices.join(', ')}.` : ''}`, false, choices);
87
- }
88
- }
89
- return null;
90
- };
91
- let issue = check(data, []);
92
- if (issue) return issue;
93
- if (Object.hasOwn(data, 'profiles')) {
94
- if (!object(data.profiles)) return located(text, node(['profiles']), 'Profiles must be an object.');
95
- for (const [name, profile] of Object.entries(data.profiles)) {
96
- if (RESERVED_PROFILE_NAMES.includes(name)) {
97
- return located(text, node(['profiles', name]), `Profile name "${name}" is reserved for veo profile ${name}. Rename this profile.`, true);
98
- }
99
- if (!object(profile)) return located(text, node(['profiles', name]), `Profile "${name}" must be an object.`);
100
- issue = check(profile, ['profiles', name]); if (issue) return issue;
101
- }
102
- }
103
- if (Object.hasOwn(data, 'activeProfile')) {
104
- if (typeof data.activeProfile !== 'string' || !data.activeProfile) return located(text, node(['activeProfile']), '"activeProfile" must be a non-empty profile name.');
105
- if (!object(data.profiles) || !Object.hasOwn(data.profiles, data.activeProfile)) {
106
- return located(text, node(['activeProfile']), `Unknown active profile "${data.activeProfile}". Choose: ${Object.keys(data.profiles || {}).join(', ') || 'none'}.`, false, Object.keys(data.profiles || {}));
107
- }
108
- }
109
- const { config } = parseConfigText(text);
110
- for (const profile of [undefined, ...Object.keys(config.profiles || {})]) {
111
- try { await effectiveConfig(config, profile); }
112
- catch (error) {
113
- const message = error.message;
114
- let key = /Invalid quality/.test(message) ? 'quality' : /Invalid (?:audio|video) format/.test(message) ? 'format' :
115
- /output directory/.test(message) ? 'output' : /custom filename/.test(message) ? 'rename' :
116
- /--([a-z-]+)/.exec(message)?.[1]?.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
117
- const name = profile ?? config.activeProfile ?? (config.profiles?.default ? 'default' : undefined);
118
- const selected = applyProfile(config, profile);
119
- // Template errors do not name the CLI option. Locate the failing template.
120
- if (!key) {
121
- const { validateTemplate } = await import('./naming.js');
122
- for (const candidate of ['filenameTemplate', 'folderTemplate']) {
123
- if (selected[candidate] !== undefined) {
124
- try { validateTemplate(selected[candidate], { folders: candidate === 'folderTemplate' }); }
125
- catch { key = candidate; break; }
126
- }
127
- }
128
- }
129
- const target = name !== undefined && Object.hasOwn(config.profiles[name], key) ? node(['profiles', name, key]) : node([key]);
130
- const value = ['quality', 'format'].includes(key) ? `"${key}": unsupported value ${JSON.stringify(selected[key])}. ` : '';
131
- return located(text, target || node([]), `${name === undefined ? '' : `Profile "${name}": `}${value}${message}`, false, valueChoices(key, selected));
132
- }
133
- }
134
- return null;
135
- }
136
-
137
- export function editorCompletions(text, cursor) {
138
- try {
139
- const { data, nodes } = configSource(text);
140
- for (const item of nodes.values()) {
141
- const path = item.path;
142
- if (!(path.length === 1 || (path.length === 3 && path[0] === 'profiles'))) continue;
143
- const key = path.at(-1), property = cursor >= item.keyStart && cursor <= item.keyEnd;
144
- if (!property && !(cursor >= item.start && cursor <= item.end)) continue;
145
- const profile = path.length === 3 ? path[1] : undefined;
146
- const choices = property ? (path.length === 1 ? [...keys, 'profiles', 'activeProfile', '$schema'] : keys) :
147
- key === 'activeProfile' ? Object.keys(data.profiles || {}) : valueChoices(key, applyProfile(data, profile));
148
- if (!choices.length) return null;
149
- const proposed = property ? suggestion(key, choices) : null;
150
- return { start: property ? item.keyStart : item.start, end: property ? item.keyEnd : item.end,
151
- choices: proposed ? [proposed, ...choices.filter(value => value !== proposed)] : choices };
152
- }
153
- } catch { /* Syntax must be valid before offering safe token replacements. */ }
154
- return null;
155
- }