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.
@@ -0,0 +1,251 @@
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
+
11
+ function abortable(promise, signal) {
12
+ if (!signal) return promise;
13
+ if (signal.aborted) {
14
+ Promise.resolve(promise).catch(() => {});
15
+ throw signal.reason;
16
+ }
17
+ return new Promise((resolve, reject) => {
18
+ const onAbort = () => reject(signal.reason);
19
+ signal.addEventListener('abort', onAbort, { once: true });
20
+ Promise.resolve(promise).then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
21
+ });
22
+ }
23
+
24
+ export function parseSourceTimeout(value) {
25
+ const match = /^(\d+)(ms|s|m)?$/i.exec(String(value || '').trim());
26
+ if (!match) throw new Error('--timeout expects seconds, e.g. 30, 30s or 2m.');
27
+ const milliseconds = Number(match[1]) * ({ ms: 1, s: 1000, m: 60000 }[match[2]?.toLowerCase() || 's']);
28
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 5000 || milliseconds > 600000) {
29
+ throw new Error('--timeout must be between 5 seconds and 10 minutes.');
30
+ }
31
+ return milliseconds;
32
+ }
33
+
34
+ export function shouldOfferSourceDiscovery(error) {
35
+ if (error?.name === 'AbortError') return false;
36
+ 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
37
+ .test(String(error?.message || error));
38
+ }
39
+
40
+ export function browserCandidates() {
41
+ const custom = process.env.VEO_BROWSER_PATH;
42
+ if (custom) return [custom];
43
+ if (process.platform === 'win32') {
44
+ const roots = [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(Boolean);
45
+ return roots.flatMap(root => [
46
+ path.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'),
47
+ path.join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
48
+ path.join(root, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
49
+ ]);
50
+ }
51
+ if (process.platform === 'darwin') return [
52
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
53
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
54
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
55
+ ];
56
+ return ['chromium', 'chromium-browser', 'google-chrome', 'microsoft-edge', 'brave-browser'];
57
+ }
58
+
59
+ export function mediaUrlsFromText(body) {
60
+ if (typeof body !== 'string' || body.length > 524288) return [];
61
+ const normalized = body.replaceAll('\\/', '/').replaceAll('\\u0026', '&').replaceAll('&amp;', '&');
62
+ const urls = normalized.match(/https?:\/\/[^\s"'<>\\]+/gi) || [];
63
+ return [...new Set(urls.map(value => value.replace(/[),;]+$/, '')).filter(value => MEDIA_URL.test(value)).map(value => {
64
+ try { return validateUrl(value); } catch { return null; }
65
+ }).filter(Boolean))];
66
+ }
67
+
68
+ export function summarizeSource(metadata, url, index, pageUrl) {
69
+ const formats = (metadata.formats || []).filter(format => !format.has_drm);
70
+ const heights = [...new Set(formats.map(format => format.height).filter(Number.isFinite))].sort((a, b) => b - a);
71
+ const bytes = metadata.filesize || metadata.filesize_approx || null;
72
+ const bitrate = Number.isFinite(metadata.tbr) ? metadata.tbr : Math.max(0, ...formats.map(format => format.tbr).filter(Number.isFinite));
73
+ return {
74
+ index, title: cleanText(metadata.title || metadata.id || 'Video'),
75
+ source: new URL(url).hostname, type: /\.m3u8(?:[?#]|$)/i.test(url) ? 'HLS' : /\.mpd(?:[?#]|$)/i.test(url) ? 'DASH' : 'Video',
76
+ quality: heights.length ? heights.map(height => `${height}p`) : [],
77
+ estimatedMiB: bytes ? Math.round(bytes / 1048576 * 10) / 10 : null,
78
+ bitrateMbps: bitrate > 0 ? Math.round(bitrate / 100) / 10 : null,
79
+ pageUrl,
80
+ };
81
+ }
82
+
83
+ function createCdp(socket) {
84
+ let nextId = 0;
85
+ const pending = new Map();
86
+ const listeners = new Set();
87
+ socket.addEventListener('message', event => {
88
+ let message;
89
+ try { message = JSON.parse(event.data); } catch { return; }
90
+ if (message.id) {
91
+ const entry = pending.get(message.id);
92
+ if (!entry) return;
93
+ pending.delete(message.id);
94
+ if (message.error) entry.reject(new Error(message.error.message));
95
+ else entry.resolve(message.result || {});
96
+ } else for (const listener of listeners) listener(message);
97
+ });
98
+ socket.addEventListener('close', () => {
99
+ for (const entry of pending.values()) entry.reject(new Error('Browser connection closed.'));
100
+ pending.clear();
101
+ });
102
+ return {
103
+ on(listener) { listeners.add(listener); return () => listeners.delete(listener); },
104
+ send(method, params = {}, sessionId) {
105
+ const id = ++nextId;
106
+ const promise = new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
107
+ socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
108
+ return promise;
109
+ },
110
+ };
111
+ }
112
+
113
+ async function findDebuggerPort(directory, child, signal) {
114
+ for (let attempt = 0; attempt < 80; attempt++) {
115
+ signal?.throwIfAborted();
116
+ if (child.exitCode !== null) throw new Error('The browser exited before source discovery started.');
117
+ const text = await readFile(path.join(directory, 'DevToolsActivePort'), 'utf8').catch(() => null);
118
+ if (text) return Number(text.split(/\r?\n/)[0]);
119
+ await delay(100, undefined, { signal });
120
+ }
121
+ throw new Error('The browser did not start its debugging connection.');
122
+ }
123
+
124
+ /** Observe media requests in a temporary browser profile; no browser cookies are copied. */
125
+ export async function captureBrowserMedia(pageUrl, { signal, browserPaths = browserCandidates(), observeMs = 8000, deepScan = false } = {}) {
126
+ const directory = await mkdtemp(path.join(tmpdir(), 'veo-discover-'));
127
+ let child, socket;
128
+ const urls = new Set();
129
+ try {
130
+ let started = false;
131
+ for (const executable of browserPaths) {
132
+ try {
133
+ child = spawn(executable, ['--headless=new', '--remote-debugging-port=0', `--user-data-dir=${directory}`,
134
+ '--no-first-run', '--no-default-browser-check', '--autoplay-policy=no-user-gesture-required', 'about:blank'],
135
+ { stdio: 'ignore', windowsHide: true });
136
+ await new Promise((resolve, reject) => {
137
+ child.once('spawn', resolve);
138
+ child.once('error', reject);
139
+ });
140
+ started = true;
141
+ break;
142
+ } catch { child = null; }
143
+ }
144
+ if (!started) throw new Error('No Chromium-based browser found. Set VEO_BROWSER_PATH to Chrome, Edge or Chromium.');
145
+ const port = await findDebuggerPort(directory, child, signal);
146
+ const endpoint = await fetch(`http://127.0.0.1:${port}/json/version`, { signal }).then(response => response.json());
147
+ socket = new WebSocket(endpoint.webSocketDebuggerUrl);
148
+ await abortable(new Promise((resolve, reject) => {
149
+ socket.addEventListener('open', resolve, { once: true });
150
+ socket.addEventListener('error', reject, { once: true });
151
+ }), signal);
152
+ const cdp = createCdp(socket);
153
+ const responses = new Map();
154
+ const bodyReads = [];
155
+ const collect = (value, mime = '') => {
156
+ if (!MEDIA_URL.test(value) && !MEDIA_TYPE.test(mime)) return;
157
+ try { urls.add(validateUrl(value)); } catch {}
158
+ };
159
+ cdp.on(message => {
160
+ if (message.method === 'Target.attachedToTarget') {
161
+ const attached = message.params.sessionId;
162
+ cdp.send('Network.enable', {}, attached).catch(() => {});
163
+ return;
164
+ }
165
+ if (message.method === 'Network.requestWillBeSent') collect(message.params.request?.url);
166
+ if (message.method === 'Network.responseReceived') {
167
+ const response = message.params.response;
168
+ collect(response.url, response.mimeType);
169
+ if (/(?:json|javascript|text\/plain)/i.test(response.mimeType || '')) {
170
+ responses.set(`${message.sessionId || ''}:${message.params.requestId}`, { sessionId: message.sessionId, requestId: message.params.requestId });
171
+ }
172
+ }
173
+ if (message.method === 'Network.loadingFinished') {
174
+ const key = `${message.sessionId || ''}:${message.params.requestId}`;
175
+ const response = responses.get(key);
176
+ if (!response || message.params.encodedDataLength > 524288) return;
177
+ responses.delete(key);
178
+ if (!deepScan && bodyReads.length >= 100) return;
179
+ bodyReads.push(cdp.send('Network.getResponseBody', { requestId: response.requestId }, response.sessionId)
180
+ .then(result => { if (!result.base64Encoded) for (const url of mediaUrlsFromText(result.body)) urls.add(url); })
181
+ .catch(() => {}));
182
+ }
183
+ });
184
+ await abortable(cdp.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true }), signal);
185
+ const target = await abortable(cdp.send('Target.createTarget', { url: 'about:blank' }), signal);
186
+ const attached = await abortable(cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true }), signal);
187
+ await abortable(cdp.send('Network.enable', {}, attached.sessionId), signal);
188
+ await abortable(cdp.send('Page.enable', {}, attached.sessionId), signal);
189
+ await abortable(cdp.send('Page.navigate', { url: pageUrl }, attached.sessionId), signal);
190
+ await delay(Math.min(observeMs / 2, 4000), undefined, { signal });
191
+ await abortable(cdp.send('Runtime.evaluate', { expression: `(() => {
192
+ for (const video of document.querySelectorAll('video')) video.play().catch(() => {});
193
+ const buttons = [...document.querySelectorAll('button,[role="button"]')];
194
+ const play = buttons.find(node => /^(play|watch|abspielen|ansehen|start)(?:\\s+(?:video|movie|film))?$/i.test((node.getAttribute('aria-label') || node.textContent || '').trim()));
195
+ if (play) play.click();
196
+ })()`, returnByValue: true }, attached.sessionId).catch(() => {}), signal);
197
+ await delay(Math.max(1000, observeMs / 2), undefined, { signal });
198
+ await Promise.race([Promise.allSettled(bodyReads), delay(1000, undefined, { signal }).catch(() => {})]);
199
+ return [...urls];
200
+ } catch (error) {
201
+ if (signal?.aborted && ['AbortError', 'TimeoutError'].includes(error?.name)) return [...urls];
202
+ throw error;
203
+ } finally {
204
+ socket?.close();
205
+ child?.kill();
206
+ await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => {});
207
+ }
208
+ }
209
+
210
+ /** Verify candidates with yt-dlp before offering them to the user. */
211
+ export async function discoverSources(pageUrl, { signal, capture = captureBrowserMedia, inspect, limit = 30,
212
+ deepScan = false, timeoutMs = deepScan ? 120000 : 45000 } = {}) {
213
+ signal?.throwIfAborted();
214
+ const timeout = new AbortController();
215
+ const timer = setTimeout(() => timeout.abort(new DOMException('Source scan timed out.', 'TimeoutError')), timeoutMs);
216
+ const scanSignal = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
217
+ const sources = [];
218
+ const seen = new Set();
219
+ try {
220
+ const observeMs = deepScan ? Math.min(30000, Math.floor(timeoutMs * 0.6)) : Math.min(8000, Math.floor(timeoutMs * 0.5));
221
+ let urls;
222
+ try { urls = await capture(pageUrl, { signal: scanSignal, observeMs, deepScan }); }
223
+ catch (error) { if (!timeout.signal.aborted) throw error; urls = []; }
224
+ signal?.throwIfAborted();
225
+ for (const url of deepScan ? urls : urls.slice(0, limit)) {
226
+ if (timeout.signal.aborted) break;
227
+ signal?.throwIfAborted();
228
+ try {
229
+ const metadata = await abortable(inspect(url, pageUrl, { signal: scanSignal }), scanSignal);
230
+ if (metadata.has_drm || metadata.is_live || metadata.formats?.length && metadata.formats.every(format => format.has_drm)
231
+ || !metadata.formats?.length && !metadata.url) continue;
232
+ const address = new URL(url);
233
+ const key = `${address.origin}${address.pathname}:${metadata.id || ''}`;
234
+ if (seen.has(key)) continue;
235
+ seen.add(key);
236
+ sources.push({ ...summarizeSource(metadata, url, sources.length + 1, pageUrl), url });
237
+ } catch { /* A request alone is not proof of downloadable media. */ }
238
+ }
239
+ signal?.throwIfAborted();
240
+ Object.defineProperty(sources, 'timedOut', { value: timeout.signal.aborted });
241
+ return sources;
242
+ } finally {
243
+ clearTimeout(timer);
244
+ }
245
+ }
246
+
247
+ export function formatSource(source) {
248
+ const size = source.estimatedMiB === null ? 'Size unknown' : `about ${source.estimatedMiB} MiB`;
249
+ const bitrate = source.bitrateMbps === null ? '' : ` | ${source.bitrateMbps} Mbit/s`;
250
+ return `${source.index}. ${source.title} | ${source.source} | ${source.type} | ${source.quality.join(', ') || 'Quality unknown'} | ${size}${bitrate}`;
251
+ }
package/src/state.js CHANGED
@@ -1,27 +1,27 @@
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, ...safe } = options;
26
- return { ...safe, output: path.resolve(options.output) };
27
- }
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
+ }
@@ -0,0 +1,222 @@
1
+ import path from 'node:path';
2
+ import { spawn } from 'node:child_process';
3
+ import { rm } from 'node:fs/promises';
4
+ import { commandOutput } from './output.js';
5
+ import { cacheBase } from './paths.js';
6
+ import { configFile as resolveConfigFile } from './config.js';
7
+ import { cleanText, readableError } from './utils.js';
8
+ import { removeAlias, validateAliasName, listAliases } from './alias.js';
9
+
10
+ const PACKAGE = 'veodl';
11
+
12
+ export const UNINSTALL_HELP = `veo uninstall - remove veo completely or one command alias
13
+
14
+ Usage:
15
+ veo uninstall [-p|--prefix <name>] [--json]
16
+ Remove one alias (e.g. veo uninstall -p veodl)
17
+ veo uninstall [--yes] [--keep-cache] [--keep-config] [--keep-aliases] [--json]
18
+ Remove everything: aliases, package, cache, config
19
+ veo uninstall --help Show this help
20
+
21
+ With -p/--prefix, only that alias is deleted; veo itself stays installed.
22
+ Without -p, veo prints what would be removed. Add --yes to delete everything:
23
+ custom wrappers created with veo alias add, the ${PACKAGE} package itself
24
+ (commands veo, veodl) with npm, veo's cache directory (downloads, jobs,
25
+ backend tools, history, statistics) and the config file. Use --keep-cache,
26
+ --keep-config or --keep-aliases to preserve one part. The config file is
27
+ deleted by default; its path is shown before and after.
28
+ `;
29
+
30
+ export function npmUninstallCommand({ platform = process.platform } = {}) {
31
+ const args = ['uninstall', '-g', PACKAGE];
32
+ if (platform === 'win32') {
33
+ return {
34
+ command: 'cmd.exe',
35
+ args: ['/d', '/s', '/c', `npm ${args.join(' ')}`],
36
+ shell: false,
37
+ windowsVerbatimArguments: true,
38
+ };
39
+ }
40
+ return { command: 'npm', args, shell: false };
41
+ }
42
+
43
+ export function runNpmUninstall({ spawnImpl = spawn, platform = process.platform, signal, timeoutMs = 600_000 } = {}) {
44
+ const { command, args, ...spawnOptions } = npmUninstallCommand({ platform });
45
+ return new Promise((resolve, reject) => {
46
+ const child = spawnImpl(command, args, { ...spawnOptions, stdio: 'inherit', windowsHide: true, signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs) });
47
+ child.once('error', reject);
48
+ child.once('close', resolve);
49
+ });
50
+ }
51
+
52
+ function parseUninstallArgs(args) {
53
+ let prefix;
54
+ let yes = false;
55
+ let keepCache = false;
56
+ let keepConfig = false;
57
+ let keepAliases = false;
58
+ let json = false;
59
+ let binDir;
60
+ const rest = [];
61
+ for (let index = 0; index < args.length; index++) {
62
+ const token = args[index];
63
+ if (token === '-p' || token === '--prefix') {
64
+ prefix = args[++index];
65
+ if (!prefix) throw new Error('--prefix requires an alias name. Use: veo uninstall -p veodl');
66
+ } else if (token.startsWith('-p=')) {
67
+ prefix = token.slice(3) || undefined;
68
+ if (!prefix) throw new Error('--prefix requires an alias name. Use: veo uninstall -p veodl');
69
+ } else if (token.startsWith('--prefix=')) {
70
+ prefix = token.slice(9) || undefined;
71
+ if (!prefix) throw new Error('--prefix requires an alias name. Use: veo uninstall -p veodl');
72
+ } else if (token === '--yes') yes = true;
73
+ else if (token === '--purge') {
74
+ // Deprecated: full cleanup is now the default. Kept so older commands keep working.
75
+ } else if (token === '--keep-cache') keepCache = true;
76
+ else if (token === '--keep-config') keepConfig = true;
77
+ else if (token === '--keep-aliases') keepAliases = true;
78
+ else if (token === '--json') json = true;
79
+ else if (token === '--bin-dir') {
80
+ binDir = args[++index];
81
+ if (!binDir) throw new Error('--bin-dir requires a directory.');
82
+ } else if (token.startsWith('--bin-dir=')) {
83
+ binDir = token.slice(10) || undefined;
84
+ if (!binDir) throw new Error('--bin-dir requires a directory.');
85
+ } else rest.push(token);
86
+ }
87
+ return { prefix, yes, keepCache, keepConfig, keepAliases, json, binDir, rest };
88
+ }
89
+
90
+ export async function uninstallMain(args = [], {
91
+ stdout = process.stdout,
92
+ stderr = process.stderr,
93
+ platform = process.platform,
94
+ env = process.env,
95
+ spawnImpl = spawn,
96
+ rmImpl = rm,
97
+ cacheRoot,
98
+ configFile,
99
+ listImpl = listAliases,
100
+ removeImpl = removeAlias,
101
+ } = {}) {
102
+ [args, stdout, stderr] = commandOutput(args, stdout, stderr);
103
+ const { prefix, yes, keepCache, keepConfig, keepAliases, json, binDir, rest } = parseUninstallArgs(args);
104
+ if (rest.includes('-h') || rest.includes('--help')) {
105
+ stdout.write(UNINSTALL_HELP);
106
+ return 0;
107
+ }
108
+ if (rest.length) throw new Error(`Unknown option for veo uninstall: ${rest.join(' ')}. Use: veo uninstall --help`);
109
+
110
+ if (prefix) {
111
+ if (keepCache || keepConfig || keepAliases) throw new Error('--keep-* cannot be combined with --prefix. Remove one alias or uninstall veo itself.');
112
+ const valid = validateAliasName(prefix);
113
+ const removed = await removeImpl(valid, { binDir, platform, env });
114
+ if (json) {
115
+ stdout.write(`${JSON.stringify({ status: 'removed', ...removed })}\n`);
116
+ return 0;
117
+ }
118
+ stdout.write(`Alias removed: ${removed.name} (${removed.removed.length} file(s) in ${removed.binDir})\n`);
119
+ if (removed.builtin) stdout.write('Note: veo update recreates the shipped veodl name.\n');
120
+ return 0;
121
+ }
122
+
123
+ const cache = cacheRoot || cacheBase({ platform, env });
124
+ const config = configFile || resolveConfigFile({ platform, env });
125
+ const manual = `npm uninstall -g ${PACKAGE}`;
126
+
127
+ // Custom wrappers first so the plan names them; npm removes veo/veodl itself.
128
+ let customAliases = [];
129
+ let aliasDir = binDir ? path.resolve(binDir) : null;
130
+ try {
131
+ const listed = await listImpl({ binDir: binDir || undefined, platform, env });
132
+ aliasDir = listed.binDir;
133
+ customAliases = listed.aliases.filter(entry => entry.present && entry.managed && !entry.builtin).map(entry => entry.name);
134
+ } catch {
135
+ // No bin directory (e.g. running from source without a global install):
136
+ // npm uninstall still works, alias cleanup is reported as skipped.
137
+ }
138
+
139
+ if (!yes) {
140
+ if (json) {
141
+ stdout.write(`${JSON.stringify({ status: 'planned', package: PACKAGE, manual, aliases: customAliases, binDir: aliasDir, cache, config })}\n`);
142
+ return 0;
143
+ }
144
+ stdout.write(`This removes everything: the ${PACKAGE} package with npm (commands veo, veodl).\n`);
145
+ if (!keepAliases) {
146
+ stdout.write(customAliases.length
147
+ ? `Custom wrappers to delete: ${customAliases.join(', ')} (${aliasDir})\n`
148
+ : `Custom wrappers: none found${aliasDir ? ` (${aliasDir})` : ''}.\n`);
149
+ }
150
+ if (!keepCache) stdout.write(`Cache to delete: ${cache}\n`);
151
+ if (!keepConfig) stdout.write(`Config to delete: ${config}\n`);
152
+ if (keepCache || keepConfig || keepAliases) {
153
+ const kept = [
154
+ keepAliases && 'aliases',
155
+ keepCache && 'cache',
156
+ keepConfig && 'config',
157
+ ].filter(Boolean).join(', ');
158
+ stdout.write(`Kept: ${kept}.\n`);
159
+ }
160
+ const keepFlags = `${keepAliases ? ' --keep-aliases' : ''}${keepCache ? ' --keep-cache' : ''}${keepConfig ? ' --keep-config' : ''}`;
161
+ stdout.write(`Run with --yes to proceed: veo uninstall --yes${keepFlags}\n`);
162
+ stdout.write(`Manual command: ${manual}\n`);
163
+ return 0;
164
+ }
165
+
166
+ let code;
167
+ try {
168
+ stdout.write(`Uninstalling ${PACKAGE} with npm…\n`);
169
+ code = await runNpmUninstall({ spawnImpl, platform, signal: AbortSignal.timeout(600_000) });
170
+ } catch (error) {
171
+ const reason = error.code === 'ENOENT' ? 'npm was not found. Install Node.js/npm, or uninstall manually.' : `Could not run npm: ${readableError(error)}`;
172
+ stderr.write(`veo: ${reason}\nManual command: ${manual}\n`);
173
+ return 1;
174
+ }
175
+ if (code !== 0) {
176
+ stderr.write(`veo: npm exited with code ${code}. Uninstall manually with: ${manual}\n`);
177
+ return 1;
178
+ }
179
+
180
+ const removedAliases = [];
181
+ if (!keepAliases) {
182
+ for (const name of customAliases) {
183
+ try {
184
+ await removeImpl(name, { binDir: aliasDir, platform, env });
185
+ removedAliases.push(name);
186
+ } catch (error) {
187
+ stderr.write(`veo: removed the package but could not delete alias ${name}: ${readableError(error)}\n`);
188
+ return 1;
189
+ }
190
+ }
191
+ }
192
+ let cacheDeleted = false;
193
+ if (!keepCache) {
194
+ try {
195
+ await rmImpl(cache, { recursive: true, force: true });
196
+ cacheDeleted = true;
197
+ } catch (error) {
198
+ stderr.write(`veo: removed the package but could not delete the cache ${cache}: ${readableError(error)}\n`);
199
+ return 1;
200
+ }
201
+ }
202
+ let configDeleted = false;
203
+ if (!keepConfig) {
204
+ try {
205
+ await rmImpl(config, { force: true });
206
+ configDeleted = true;
207
+ } catch (error) {
208
+ stderr.write(`veo: removed the package but could not delete the config ${config}: ${readableError(error)}\n`);
209
+ return 1;
210
+ }
211
+ }
212
+ if (json) {
213
+ stdout.write(`${JSON.stringify({ status: 'uninstalled', package: PACKAGE, aliases: removedAliases, binDir: aliasDir, cacheDeleted, cache, configDeleted, config })}\n`);
214
+ return 0;
215
+ }
216
+ stdout.write(`veo uninstalled (${PACKAGE}).`);
217
+ if (!keepAliases) stdout.write(removedAliases.length ? ` Aliases deleted: ${removedAliases.join(', ')}.` : ' No custom wrappers found.');
218
+ stdout.write(cacheDeleted ? ` Cache deleted: ${cache}.` : ` Cache kept: ${cache}.`);
219
+ stdout.write(configDeleted ? ` Config deleted: ${config}.` : ` Config kept: ${config}.`);
220
+ stdout.write('\n');
221
+ return 0;
222
+ }
package/src/updater.js CHANGED
@@ -17,6 +17,7 @@ export const UPDATE_HELP = `veo update - keep veo current
17
17
  Usage:
18
18
  veo update Install the latest version with npm
19
19
  veo update --check Only check whether a newer version exists
20
+ veo up Short alias for veo update (also: veo up --check)
20
21
  veo upgrade Alias for veo update
21
22
  veo check update Alias for veo update --check
22
23
 
@@ -95,7 +96,7 @@ export async function maybeUpdateNotice({
95
96
  try {
96
97
  latest = await fetchLatestVersion({ fetchImpl, registry, timeoutMs });
97
98
  if (compareVersions(latest, currentVersion) > 0) {
98
- message = `Update available: veo ${latest} (you have ${currentVersion}). Run: veo update`;
99
+ message = `Update available: veo ${latest} (you have ${currentVersion}). Run: veo up`;
99
100
  }
100
101
  } catch {
101
102
  latest = null; // Unreachable registry still consumes the throttle interval.
@@ -158,7 +159,7 @@ export async function packageVersion() {
158
159
  }
159
160
 
160
161
  /**
161
- * Implements `veo update [--check]`, `veo upgrade`, and `veo check update`.
162
+ * Implements `veo update [--check]`, `veo up`, `veo upgrade`, and `veo check update`.
162
163
  * Returns the process exit code. Deps are injectable for tests.
163
164
  */
164
165
  export async function updateMain(args, {
@@ -174,6 +175,7 @@ export async function updateMain(args, {
174
175
  activeBackend,
175
176
  } = {}) {
176
177
  [args, stdout, stderr] = commandOutput(args, stdout, stderr);
178
+ if (args[0] === 'up') args = ['update', ...args.slice(1)];
177
179
  current = current ?? await packageVersion();
178
180
  const isCheckCommand = args[0] === 'check';
179
181
  const rest = isCheckCommand ? args.slice(2) : args.slice(1);
@@ -194,7 +196,7 @@ export async function updateMain(args, {
194
196
  }
195
197
  const newer = compareVersions(latest, current) > 0;
196
198
  if (checkOnly) {
197
- stdout.write(newer ? `Update available: veo ${latest} (you have ${current}). Run: veo update\n` : `${currentText} is up to date.\n`);
199
+ stdout.write(newer ? `Update available: veo ${latest} (you have ${current}). Run: veo up\n` : `${currentText} is up to date.\n`);
198
200
  return 0;
199
201
  }
200
202
  if (newer) {