veodl 1.8.2 → 1.8.3

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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to veo. This project follows [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## 1.8.3
6
+
7
+ ### Added
8
+
9
+ - Added `veo alias` commands (`list`, `add`, `remove`) to manage custom command names and shortcuts that forward to `veo`.
10
+ - Added `veo uninstall` to remove individual aliases with `-p <name>`, or perform a full interactive/planned purge with `--yes`.
11
+ - Added `veo up` as a short alias for `veo update`.
12
+
13
+ ### Changed
14
+
15
+ - Streamlined package binary commands to `veo` and `veodl`.
16
+
5
17
  ## 1.8.2
6
18
 
7
19
  ### Changed
package/README.md CHANGED
@@ -10,7 +10,7 @@ Automating with an agent? Start with the [agent guide](docs/AGENT_GUIDE.md) for
10
10
 
11
11
  Requires **Node.js 22+** and npm. Internet access is needed for installation and first-use backend setup.
12
12
 
13
- The npm package is `veodl`; the installed commands are `veo`, `veod`, and `veodl`.
13
+ The npm package is `veodl`; the installed commands are `veo` and `veodl`.
14
14
 
15
15
  ```bash
16
16
  npx veodl "https://example.com/video.mp4"
@@ -21,7 +21,7 @@ Or install globally:
21
21
  ```bash
22
22
  npm install -g veodl
23
23
  veo "https://example.com/video.mp4"
24
- # or: veod / veodl "https://example.com/video.mp4"
24
+ # or: veodl "https://example.com/video.mp4"
25
25
  ```
26
26
 
27
27
  The first download automatically prepares missing yt-dlp, FFmpeg and FFprobe.
@@ -452,9 +452,11 @@ paths and custom comments are preserved.
452
452
  xdg-utils and a file association). The CLI does not wait for the player to close. If the
453
453
  opener cannot be launched, a warning is printed and the successful download still exits
454
454
  with `0`; later errors inside the detached opener/player are not monitored.
455
- - After each successful download, veo checks the npm registry at most once per day for a
456
- newer version and prints a one-line notice on stderr (never on failure). Disable it with
457
- `VEO_NO_UPDATE_CHECK=1`; `VEO_REGISTRY`/`npm_config_registry` are respected.
455
+ - After each successful command, veo checks the npm registry at most once per day for a
456
+ newer version and prints a one-line notice on stderr (`Update available: veo X
457
+ (you have Y). Run: veo up`). Help, version and the update commands themselves
458
+ stay quiet. Disable it with `VEO_NO_UPDATE_CHECK=1`;
459
+ `VEO_REGISTRY`/`npm_config_registry` are respected.
458
460
  - Exit status is `0` on success, `1` on errors, and `130` on cancellation. With several
459
461
  URLs, `1` means at least one URL failed.
460
462
 
@@ -483,10 +485,17 @@ veo flush # stop runs, clear temporary downloads and retry jobs
483
485
  veo flush --stats # the same, and reset the statistics
484
486
  veo update # install the latest veo with npm
485
487
  veo update --check # only check for a newer veo
488
+ veo up # short alias for veo update (also: veo up --check)
486
489
  veo upgrade # alias for veo update
487
490
  veo check update # alias for veo update --check
488
491
  veo backend update # install the newest yt-dlp release
489
492
  veo backend reset # back to the release pinned in this veo version
493
+ veo alias list # shipped (veo, veodl) plus custom wrappers
494
+ veo alias add veo-dl # create veo-dl as a wrapper that calls veo
495
+ veo alias remove veo-dl # delete a custom wrapper
496
+ veo uninstall -p veodl # delete one alias (veo itself stays installed)
497
+ veo uninstall # plan the full removal (aliases, package, cache, config)
498
+ veo uninstall --yes # remove everything
490
499
  ```
491
500
 
492
501
  `veo update` runs `npm install -g veodl@latest` and then removes yt-dlp backend
@@ -495,6 +504,33 @@ never uses a shell on Linux/macOS, passes fixed arguments only, and prints the m
495
504
  command on any failure. The registry can be overridden with `VEO_REGISTRY` (or npm's
496
505
  `npm_config_registry`) for mirrors and proxies.
497
506
 
507
+ ### `veo alias` and `veo uninstall`
508
+
509
+ The npm package is `veodl`; a global install ships the commands `veo`
510
+ and `veodl`. `veo alias` manages extra command names without reinstalling:
511
+
512
+ ```bash
513
+ veo alias list
514
+ veo alias add veo-dl
515
+ veo alias remove veo-dl
516
+ veo uninstall -p veodl
517
+ ```
518
+
519
+ `add` creates a small wrapper next to the `veo` command found on `PATH` that
520
+ forwards to `veo`, so it survives `veo update`. Names use 2-31 lowercase
521
+ letters, digits or hyphens. `remove` (also `veo alias rm`) deletes the wrapper
522
+ files; `veo` itself can never be removed this way. Removing a shipped name such
523
+ as `veodl` only deletes that shortcut — the next `veo update` recreates it.
524
+
525
+ `veo uninstall -p <name>` is a shortcut for `veo alias remove <name>`. Bare
526
+ `veo uninstall` removes everything: without flags it only prints the plan
527
+ (package `veodl` with commands veo, veodl, plus custom wrappers, cache
528
+ and config paths); `veo uninstall --yes` deletes all of it — custom wrappers,
529
+ the package with `npm uninstall -g veodl`, veo's cache directory (downloads,
530
+ jobs, backend tools, history, statistics) and the config file. Use
531
+ `--keep-aliases`, `--keep-cache` or `--keep-config` to preserve one part.
532
+ Add `--json` for scripting.
533
+
498
534
  ### `veo flush`
499
535
 
500
536
  Run `veo flush` to stop active veo runs started with this version, then remove
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "veodl",
3
- "version": "1.8.2",
3
+ "version": "1.8.3",
4
4
  "description": "A clean, simple video downloader powered by yt-dlp.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "veo": "bin/veo.js",
8
- "veod": "bin/veo.js",
9
8
  "veodl": "bin/veo.js"
10
9
  },
11
10
  "files": [
package/src/alias.js ADDED
@@ -0,0 +1,258 @@
1
+ import path from 'node:path';
2
+ import { chmod, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { commandOutput } from './output.js';
4
+ import { findOnPath } from './backend.js';
5
+ import { cleanText, readableError } from './utils.js';
6
+
7
+ export const BUILTIN_ALIASES = ['veo', 'veodl'];
8
+ export const ALIAS_MARKER = 'veo-alias';
9
+
10
+ export const ALIAS_HELP = `veo alias - manage extra command names for veo
11
+
12
+ Usage:
13
+ veo alias List command names (same as: veo alias list)
14
+ veo alias list [--json] Show veo, veodl plus custom wrappers
15
+ veo alias add <name> [--force] [--json]
16
+ Create <name> as a wrapper that calls veo
17
+ veo alias remove <name> [--json] Delete a wrapper (also: veo alias rm)
18
+ veo alias --help Show this help
19
+
20
+ Names use 2-31 lowercase letters, digits or hyphens and start with a letter,
21
+ e.g. veo-dl. The wrapper is created next to the veo command found on PATH and
22
+ simply forwards to veo, so it survives veo updates. Removing veodl only
23
+ removes that shortcut; veo update recreates the shipped names. veo itself can
24
+ never be removed. Use --bin-dir <path> to override the target directory.
25
+ `;
26
+
27
+ export function validateAliasName(input) {
28
+ const raw = String(input ?? '').trim();
29
+ if (!raw) throw new Error('An alias name is required. Use: veo alias add <name>');
30
+ if (cleanText(raw) !== raw) throw new Error(`Invalid alias name: ${raw}. Use plain text without escapes.`);
31
+ if (!/^[a-z][a-z0-9-]{1,30}$/.test(raw)) {
32
+ throw new Error(`Invalid alias name "${raw}". Use 2-31 lowercase letters, digits or hyphens, starting with a letter (e.g. veo-dl).`);
33
+ }
34
+ return raw;
35
+ }
36
+
37
+ export function aliasFiles(binDir, name, platform = process.platform) {
38
+ if (platform === 'win32') {
39
+ return [path.join(binDir, name), path.join(binDir, `${name}.cmd`), path.join(binDir, `${name}.ps1`)];
40
+ }
41
+ return [path.join(binDir, name)];
42
+ }
43
+
44
+ function posixWrapper(name) {
45
+ return `#!/bin/sh\n# ${ALIAS_MARKER}: ${name}\nexec veo "$@"\n`;
46
+ }
47
+
48
+ function windowsCmdWrapper(name) {
49
+ return `@echo off\r\nREM ${ALIAS_MARKER}: ${name}\r\nveo %*\r\n`;
50
+ }
51
+
52
+ function windowsShWrapper(name) {
53
+ return `#!/bin/sh\n# ${ALIAS_MARKER}: ${name}\nexec veo "$@"\n`;
54
+ }
55
+
56
+ function windowsPsWrapper(name) {
57
+ return `# ${ALIAS_MARKER}: ${name}\nveo @args\n`;
58
+ }
59
+
60
+ async function exists(file, { statImpl = stat } = {}) {
61
+ try {
62
+ const info = await statImpl(file);
63
+ return info.isFile();
64
+ } catch (error) {
65
+ if (error?.code === 'ENOENT') return false;
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ async function isManaged(file, { readFileImpl = readFile } = {}) {
71
+ try {
72
+ const text = await readFileImpl(file, 'utf8');
73
+ return text.slice(0, 4096).includes(ALIAS_MARKER);
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ export async function resolveBinDir({ binDir, platform = process.platform, env = process.env, find = findOnPath } = {}) {
80
+ if (binDir) return path.resolve(binDir);
81
+ const found = await find(['veo'], { platform, env });
82
+ if (!found) {
83
+ throw new Error('Could not locate the veo command on PATH. Install veo globally (npm install -g veodl) or pass --bin-dir <path>.');
84
+ }
85
+ return path.dirname(found);
86
+ }
87
+
88
+ export async function aliasStatus(name, { binDir, platform = process.platform, ...fs } = {}) {
89
+ const files = aliasFiles(binDir, name, platform);
90
+ const present = [];
91
+ let managed = false;
92
+ for (const file of files) {
93
+ if (await exists(file, fs)) {
94
+ present.push(file);
95
+ if (await isManaged(file, fs)) managed = true;
96
+ }
97
+ }
98
+ return { name, builtin: BUILTIN_ALIASES.includes(name), present: present.length > 0, managed, paths: present };
99
+ }
100
+
101
+ function baseNameOf(file, platform) {
102
+ const base = path.basename(file);
103
+ if (platform === 'win32') {
104
+ if (base.toLowerCase().endsWith('.cmd')) return base.slice(0, -4);
105
+ if (base.toLowerCase().endsWith('.ps1')) return base.slice(0, -4);
106
+ }
107
+ return base;
108
+ }
109
+
110
+ export async function listAliases({ binDir, platform = process.platform, builtins = BUILTIN_ALIASES, readdirImpl = readdir, readFileImpl = readFile, statImpl = stat } = {}) {
111
+ const resolved = await resolveBinDir({ binDir, platform });
112
+ const entries = [];
113
+ for (const name of builtins) {
114
+ entries.push(await aliasStatus(name, { binDir: resolved, platform, readFileImpl, statImpl }));
115
+ }
116
+ const known = new Set(entries.map(entry => entry.name));
117
+ let files = [];
118
+ try {
119
+ files = await readdirImpl(resolved, { withFileTypes: true });
120
+ } catch (error) {
121
+ throw new Error(`Cannot list the command directory ${resolved}: ${readableError(error)}`);
122
+ }
123
+ for (const entry of files) {
124
+ if (!entry.isFile()) continue;
125
+ const full = path.join(resolved, entry.name);
126
+ let text = '';
127
+ try {
128
+ text = String(await readFileImpl(full, 'utf8')).slice(0, 4096);
129
+ } catch {
130
+ continue;
131
+ }
132
+ if (!text.includes(ALIAS_MARKER)) continue;
133
+ const base = baseNameOf(entry.name, platform);
134
+ try {
135
+ validateAliasName(base);
136
+ } catch {
137
+ continue;
138
+ }
139
+ if (known.has(base)) {
140
+ const existing = entries.find(item => item.name === base);
141
+ if (existing) existing.managed = true;
142
+ continue;
143
+ }
144
+ known.add(base);
145
+ entries.push({ name: base, builtin: false, present: true, managed: true, paths: [full] });
146
+ }
147
+ return { binDir: resolved, aliases: entries.sort((a, b) => a.name.localeCompare(b.name)) };
148
+ }
149
+
150
+ export async function addAlias(name, { binDir, platform = process.platform, force = false, writeFileImpl = writeFile, statImpl = stat, chmodImpl = chmod, mkdirImpl = mkdir } = {}) {
151
+ const valid = validateAliasName(name);
152
+ if (valid === 'veo') throw new Error('veo is already the main command.');
153
+ const resolved = await resolveBinDir({ binDir, platform });
154
+ await mkdirImpl(resolved, { recursive: true });
155
+ const files = aliasFiles(resolved, valid, platform);
156
+ const present = [];
157
+ for (const file of files) {
158
+ if (await exists(file, { statImpl })) present.push(file);
159
+ }
160
+ if (present.length && !force) {
161
+ throw new Error(`${valid} already exists (${present.map(file => path.basename(file)).join(', ')}). Use --force to replace it.`);
162
+ }
163
+ for (const file of present) await rm(file, { force: true });
164
+ if (platform === 'win32') {
165
+ await writeFileImpl(files[0], windowsShWrapper(valid), { mode: 0o755 });
166
+ await writeFileImpl(files[1], windowsCmdWrapper(valid));
167
+ await writeFileImpl(files[2], windowsPsWrapper(valid));
168
+ } else {
169
+ await writeFileImpl(files[0], posixWrapper(valid), { mode: 0o755 });
170
+ try {
171
+ await chmodImpl(files[0], 0o755);
172
+ } catch {
173
+ // Non-POSIX filesystems may ignore modes; the shebang still works.
174
+ }
175
+ }
176
+ return { name: valid, binDir: resolved, paths: aliasFiles(resolved, valid, platform) };
177
+ }
178
+
179
+ export async function removeAlias(name, { binDir, platform = process.platform, statImpl = stat, rmImpl = rm } = {}) {
180
+ const valid = validateAliasName(name);
181
+ if (valid === 'veo') throw new Error('Refusing to remove veo itself. Remove veodl or a custom wrapper instead.');
182
+ const resolved = await resolveBinDir({ binDir, platform });
183
+ const status = await aliasStatus(valid, { binDir: resolved, platform, statImpl });
184
+ if (!status.present) throw new Error(`${valid} is not installed in ${resolved}.`);
185
+ for (const file of status.paths) await rmImpl(file, { force: true });
186
+ return { name: valid, binDir: resolved, removed: status.paths, builtin: status.builtin };
187
+ }
188
+
189
+ function parseAliasArgs(args) {
190
+ let binDir;
191
+ let force = false;
192
+ let json = false;
193
+ const rest = [];
194
+ for (let index = 0; index < args.length; index++) {
195
+ const token = args[index];
196
+ if (token === '--force') force = true;
197
+ else if (token === '--json') json = true;
198
+ else if (token === '--bin-dir') {
199
+ binDir = args[++index];
200
+ if (!binDir) throw new Error('--bin-dir requires a directory. Run veo alias --help for usage.');
201
+ } else if (token.startsWith('--bin-dir=')) {
202
+ binDir = token.slice(10) || undefined;
203
+ if (!binDir) throw new Error('--bin-dir requires a directory. Run veo alias --help for usage.');
204
+ } else rest.push(token);
205
+ }
206
+ return { binDir, force, json, rest };
207
+ }
208
+
209
+ export async function aliasMain(args = [], { stdout = process.stdout, stderr = process.stderr, ...options } = {}) {
210
+ [args, stdout, stderr] = commandOutput(args, stdout, stderr);
211
+ const { binDir, force, json, rest } = parseAliasArgs(args);
212
+ const [command, name, ...extra] = rest;
213
+ if (!command || command === 'list') {
214
+ if (name || extra.length) throw new Error('Usage: veo alias list [--json] (or veo alias --help)');
215
+ if (command === 'list' && extra.length) throw new Error('Usage: veo alias list [--json] (or veo alias --help)');
216
+ const result = await listAliases({ ...options, binDir });
217
+ if (json) {
218
+ stdout.write(`${JSON.stringify(result)}\n`);
219
+ return 0;
220
+ }
221
+ stdout.write(`Command directory: ${result.binDir}\n`);
222
+ for (const entry of result.aliases) {
223
+ const kind = entry.builtin ? 'shipped' : 'custom';
224
+ const state = entry.present ? (entry.managed ? 'wrapper -> veo' : 'installed') : 'missing';
225
+ stdout.write(` ${entry.name} (${kind}, ${state})\n`);
226
+ }
227
+ const missing = result.aliases.filter(entry => !entry.present).map(entry => entry.name);
228
+ if (missing.length) stdout.write(`Missing: ${missing.join(', ')} (veo update recreates shipped names).\n`);
229
+ return 0;
230
+ }
231
+ if (command === '-h' || command === '--help') {
232
+ stdout.write(ALIAS_HELP);
233
+ return 0;
234
+ }
235
+ if (command === 'add') {
236
+ if (!name || extra.length) throw new Error('Usage: veo alias add <name> [--force] (or veo alias --help)');
237
+ const created = await addAlias(name, { ...options, binDir, force });
238
+ if (json) {
239
+ stdout.write(`${JSON.stringify({ status: 'added', ...created })}\n`);
240
+ return 0;
241
+ }
242
+ stdout.write(`Alias installed: ${created.name} -> veo (${created.binDir})\n`);
243
+ stdout.write(`Run "${created.name} --help" to use it. Remove it with: veo alias remove ${created.name}\n`);
244
+ return 0;
245
+ }
246
+ if (command === 'remove' || command === 'rm' || command === 'uninstall') {
247
+ if (!name || extra.length) throw new Error('Usage: veo alias remove <name> (or veo alias --help)');
248
+ const removed = await removeAlias(name, { ...options, binDir });
249
+ if (json) {
250
+ stdout.write(`${JSON.stringify({ status: 'removed', ...removed })}\n`);
251
+ return 0;
252
+ }
253
+ stdout.write(`Alias removed: ${removed.name} (${removed.removed.length} file(s) in ${removed.binDir})\n`);
254
+ if (removed.builtin) stdout.write('Note: veo update recreates the shipped veodl name.\n');
255
+ return 0;
256
+ }
257
+ throw new Error(`Unknown alias command: ${cleanText(command)}. Use: veo alias list | veo alias add <name> | veo alias remove <name>`);
258
+ }
package/src/cli.js CHANGED
@@ -66,7 +66,7 @@ Options:
66
66
  -h, --help Show help (also: veo help)
67
67
 
68
68
  Commands:
69
- veo update [--check] Update veo itself with npm
69
+ veo update|up [--check] Update veo itself with npm (up is a short alias)
70
70
  veo backend update Install a newer yt-dlp release (see veo backend --help)
71
71
  veo doctor Diagnose the local setup
72
72
  veo flush Stop veo runs and clear temporary downloads and jobs
@@ -78,6 +78,8 @@ Commands:
78
78
  veo inspect <file> Read media metadata; --check-audio measures audio signal
79
79
  veo inspect run <id> Inspect saved files from a finished run by its id
80
80
  veo stop [id] Stop one run, or every active run
81
+ veo alias list|add|remove Manage extra command names (wrappers calling veo)
82
+ veo uninstall [-p <name>] Remove one alias, or everything with --yes
81
83
  veo version Show the installed version
82
84
  veo config edit|path|profiles|check|show|reset Manage defaults and named profiles
83
85
  veo config edit [--external|--terminal] Choose the configuration editor
@@ -291,8 +293,28 @@ export async function main(args = process.argv.slice(2), { config } = {}) {
291
293
  }
292
294
  // The command's own validation reports configuration errors.
293
295
  }
294
- if (['stats', 'history', 'flush', 'runs', 'inspect', 'stop', 'update', 'upgrade', 'check', 'doctor', 'backend'].includes(args[0])) args = display.remaining;
295
- return withOutputSettings(color, () => runMain(args, { config }));
296
+ if (['stats', 'history', 'flush', 'runs', 'inspect', 'stop', 'update', 'up', 'upgrade', 'check', 'doctor', 'backend', 'alias', 'uninstall'].includes(args[0])) args = display.remaining;
297
+ const code = await withOutputSettings(color, () => runMain(args, { config }));
298
+ // One throttled (once per day) update hint after every successful command.
299
+ // Failures, cancellations, help/version output and the update commands
300
+ // themselves never trigger it; VEO_NO_UPDATE_CHECK=1 disables it for scripts.
301
+ if (code === 0 && shouldUpdateNotice(args)) {
302
+ try {
303
+ const notice = await maybeUpdateNotice({ currentVersion: await packageVersion() });
304
+ if (notice) process.stderr.write(`${notice}\n`);
305
+ } catch {
306
+ // Informational only: a broken notice must never fail the command.
307
+ }
308
+ }
309
+ return code;
310
+ }
311
+
312
+ // Help, version and the update commands report versions themselves;
313
+ // anything else earns the daily update hint on success.
314
+ export function shouldUpdateNotice(args = []) {
315
+ if (args.includes('-h') || args.includes('--help') || args.includes('-v') || args.includes('--version')) return false;
316
+ if (['update', 'up', 'upgrade', 'check', 'version', 'help'].includes(args[0])) return false;
317
+ return true;
296
318
  }
297
319
 
298
320
  async function runMain(args, { config }) {
@@ -334,8 +356,9 @@ async function runMain(args, { config }) {
334
356
  }
335
357
  const { cleanupDownloadCache } = await import('./download-cache.js');
336
358
  await cleanupDownloadCache().catch(error => stderr.write(`veo: Could not clean expired local downloads: ${readableError(error)}\n`));
337
- // update/upgrade/check subcommands are handled before URL validation.
338
- if (['update', 'upgrade', 'check'].includes(args[0])) {
359
+ // update/upgrade/up/check subcommands are handled before URL validation.
360
+ if (['update', 'up', 'upgrade', 'check'].includes(args[0])) {
361
+ if (args[0] === 'up') args = ['update', ...args.slice(1)];
339
362
  if (args[0] === 'check' && args[1] !== 'update') {
340
363
  stdout.write(UPDATE_HELP);
341
364
  return 0;
@@ -364,6 +387,24 @@ async function runMain(args, { config }) {
364
387
  return 1;
365
388
  }
366
389
  }
390
+ if (args[0] === 'alias') {
391
+ try {
392
+ const { aliasMain } = await import('./alias.js');
393
+ return await aliasMain(args.slice(1));
394
+ } catch (error) {
395
+ stderr.write(`veo: ${readableError(error)}\n`);
396
+ return 1;
397
+ }
398
+ }
399
+ if (args[0] === 'uninstall') {
400
+ try {
401
+ const { uninstallMain } = await import('./uninstall.js');
402
+ return await uninstallMain(args.slice(1));
403
+ } catch (error) {
404
+ stderr.write(`veo: ${readableError(error)}\n`);
405
+ return 1;
406
+ }
407
+ }
367
408
  const reporter = createReporter();
368
409
  const controller = new AbortController();
369
410
  const cancel = () => controller.abort();
@@ -438,10 +479,7 @@ async function runMain(args, { config }) {
438
479
  const { createStatsRecorder } = await import('./stats.js');
439
480
  const { createHistoryRecorder } = await import('./history.js');
440
481
  const result = await runJob(options, { download, reporter, signal: controller.signal, openFile, items: retryItems, jobFile: jobFile || undefined, runId: run?.id, recordStats: createStatsRecorder(), recordHistory: createHistoryRecorder() });
441
- if (result !== 0) return result;
442
- const notice = await maybeUpdateNotice({ currentVersion: await packageVersion() });
443
- if (notice) stderr.write(`${notice}\n`);
444
- return 0;
482
+ return result;
445
483
  } catch (error) {
446
484
  reporter.fail(controller.signal.aborted);
447
485
  stderr.write(`veo: ${readableError(error)}\n`);
package/src/doctor.js CHANGED
@@ -249,7 +249,7 @@ export async function collectChecks({
249
249
  }
250
250
  if (latest) {
251
251
  const newer = compareVersions(latest, version) > 0;
252
- push(newer ? 'warn' : 'ok', 'veo version', newer ? `${version} — ${latest} is available (run: veo update)` : `${version} is the latest release`);
252
+ push(newer ? 'warn' : 'ok', 'veo version', newer ? `${version} — ${latest} is available (run: veo up)` : `${version} is the latest release`);
253
253
  }
254
254
  }
255
255
  return checks;
package/src/output.js CHANGED
@@ -27,7 +27,7 @@ export function formatOutput(text, stream, enabled = settings.getStore()?.enable
27
27
  if (!line.trim() || /\x1b/.test(line)) return line;
28
28
  const value = line.trim();
29
29
  let role = 'muted';
30
- if (/^(?:veo (?:stats|history|runs|stop|doctor|flush|update|backend)\b|veo \d[^ ]* doctor|ID\s+PID\s+STATE|Usage:|Options:|Commands:|Examples:|\d+\. )/.test(value)) role = 'title';
30
+ if (/^(?:veo (?:stats|history|runs|stop|doctor|flush|update|backend|alias|uninstall)\b|veo \d[^ ]* doctor|ID\s+PID\s+STATE|Usage:|Options:|Commands:|Examples:|\d+\. )/.test(value)) role = 'title';
31
31
  if (/^(?:\[?OK\]?\s|Saved:|Flushed:|Stopped\b|Removed\b|Config OK:|Statistics reset|Managed tools are ready)|\bis up to date\b|\bupdated to\b|\binstalled and will be used\b/i.test(value)) role = 'success';
32
32
  if (/^(?:\[?FAIL\]?\s|Error:|veo: (?!note:))|Status:\s*(?:failed|cancelled)/i.test(value)) role = 'error';
33
33
  if (/Status:\s*saved/.test(value)) role = 'success';
@@ -0,0 +1,221 @@
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, configBase } from './paths.js';
6
+ import { cleanText, readableError } from './utils.js';
7
+ import { removeAlias, validateAliasName, listAliases } from './alias.js';
8
+
9
+ const PACKAGE = 'veodl';
10
+
11
+ export const UNINSTALL_HELP = `veo uninstall - remove veo completely or one command alias
12
+
13
+ Usage:
14
+ veo uninstall [-p|--prefix <name>] [--json]
15
+ Remove one alias (e.g. veo uninstall -p veodl)
16
+ veo uninstall [--yes] [--keep-cache] [--keep-config] [--keep-aliases] [--json]
17
+ Remove everything: aliases, package, cache, config
18
+ veo uninstall --help Show this help
19
+
20
+ With -p/--prefix, only that alias is deleted; veo itself stays installed.
21
+ Without -p, veo prints what would be removed. Add --yes to delete everything:
22
+ custom wrappers created with veo alias add, the ${PACKAGE} package itself
23
+ (commands veo, veodl) with npm, veo's cache directory (downloads, jobs,
24
+ backend tools, history, statistics) and the config file. Use --keep-cache,
25
+ --keep-config or --keep-aliases to preserve one part. The config file is
26
+ deleted by default; its path is shown before and after.
27
+ `;
28
+
29
+ export function npmUninstallCommand({ platform = process.platform } = {}) {
30
+ const args = ['uninstall', '-g', PACKAGE];
31
+ if (platform === 'win32') {
32
+ return {
33
+ command: 'cmd.exe',
34
+ args: ['/d', '/s', '/c', `npm ${args.join(' ')}`],
35
+ shell: false,
36
+ windowsVerbatimArguments: true,
37
+ };
38
+ }
39
+ return { command: 'npm', args, shell: false };
40
+ }
41
+
42
+ export function runNpmUninstall({ spawnImpl = spawn, platform = process.platform, signal, timeoutMs = 600_000 } = {}) {
43
+ const { command, args, ...spawnOptions } = npmUninstallCommand({ platform });
44
+ return new Promise((resolve, reject) => {
45
+ const child = spawnImpl(command, args, { ...spawnOptions, stdio: 'inherit', windowsHide: true, signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs) });
46
+ child.once('error', reject);
47
+ child.once('close', resolve);
48
+ });
49
+ }
50
+
51
+ function parseUninstallArgs(args) {
52
+ let prefix;
53
+ let yes = false;
54
+ let keepCache = false;
55
+ let keepConfig = false;
56
+ let keepAliases = false;
57
+ let json = false;
58
+ let binDir;
59
+ const rest = [];
60
+ for (let index = 0; index < args.length; index++) {
61
+ const token = args[index];
62
+ if (token === '-p' || token === '--prefix') {
63
+ prefix = args[++index];
64
+ if (!prefix) throw new Error('--prefix requires an alias name. Use: veo uninstall -p veodl');
65
+ } else if (token.startsWith('-p=')) {
66
+ prefix = token.slice(3) || undefined;
67
+ if (!prefix) throw new Error('--prefix requires an alias name. Use: veo uninstall -p veodl');
68
+ } else if (token.startsWith('--prefix=')) {
69
+ prefix = token.slice(9) || undefined;
70
+ if (!prefix) throw new Error('--prefix requires an alias name. Use: veo uninstall -p veodl');
71
+ } else if (token === '--yes') yes = true;
72
+ else if (token === '--purge') {
73
+ // Deprecated: full cleanup is now the default. Kept so older commands keep working.
74
+ } else if (token === '--keep-cache') keepCache = true;
75
+ else if (token === '--keep-config') keepConfig = true;
76
+ else if (token === '--keep-aliases') keepAliases = true;
77
+ else if (token === '--json') json = true;
78
+ else if (token === '--bin-dir') {
79
+ binDir = args[++index];
80
+ if (!binDir) throw new Error('--bin-dir requires a directory.');
81
+ } else if (token.startsWith('--bin-dir=')) {
82
+ binDir = token.slice(10) || undefined;
83
+ if (!binDir) throw new Error('--bin-dir requires a directory.');
84
+ } else rest.push(token);
85
+ }
86
+ return { prefix, yes, keepCache, keepConfig, keepAliases, json, binDir, rest };
87
+ }
88
+
89
+ export async function uninstallMain(args = [], {
90
+ stdout = process.stdout,
91
+ stderr = process.stderr,
92
+ platform = process.platform,
93
+ env = process.env,
94
+ spawnImpl = spawn,
95
+ rmImpl = rm,
96
+ cacheRoot,
97
+ configFile,
98
+ listImpl = listAliases,
99
+ removeImpl = removeAlias,
100
+ } = {}) {
101
+ [args, stdout, stderr] = commandOutput(args, stdout, stderr);
102
+ const { prefix, yes, keepCache, keepConfig, keepAliases, json, binDir, rest } = parseUninstallArgs(args);
103
+ if (rest.includes('-h') || rest.includes('--help')) {
104
+ stdout.write(UNINSTALL_HELP);
105
+ return 0;
106
+ }
107
+ if (rest.length) throw new Error(`Unknown option for veo uninstall: ${rest.join(' ')}. Use: veo uninstall --help`);
108
+
109
+ if (prefix) {
110
+ if (keepCache || keepConfig || keepAliases) throw new Error('--keep-* cannot be combined with --prefix. Remove one alias or uninstall veo itself.');
111
+ const valid = validateAliasName(prefix);
112
+ const removed = await removeImpl(valid, { binDir, platform, env });
113
+ if (json) {
114
+ stdout.write(`${JSON.stringify({ status: 'removed', ...removed })}\n`);
115
+ return 0;
116
+ }
117
+ stdout.write(`Alias removed: ${removed.name} (${removed.removed.length} file(s) in ${removed.binDir})\n`);
118
+ if (removed.builtin) stdout.write('Note: veo update recreates the shipped veodl name.\n');
119
+ return 0;
120
+ }
121
+
122
+ const cache = cacheRoot || cacheBase({ platform, env });
123
+ const config = configFile || path.join(configBase({ platform, env }), 'config.json');
124
+ const manual = `npm uninstall -g ${PACKAGE}`;
125
+
126
+ // Custom wrappers first so the plan names them; npm removes veo/veodl itself.
127
+ let customAliases = [];
128
+ let aliasDir = binDir ? path.resolve(binDir) : null;
129
+ try {
130
+ const listed = await listImpl({ binDir: binDir || undefined, platform, env });
131
+ aliasDir = listed.binDir;
132
+ customAliases = listed.aliases.filter(entry => entry.present && entry.managed && !entry.builtin).map(entry => entry.name);
133
+ } catch {
134
+ // No bin directory (e.g. running from source without a global install):
135
+ // npm uninstall still works, alias cleanup is reported as skipped.
136
+ }
137
+
138
+ if (!yes) {
139
+ if (json) {
140
+ stdout.write(`${JSON.stringify({ status: 'planned', package: PACKAGE, manual, aliases: customAliases, binDir: aliasDir, cache, config })}\n`);
141
+ return 0;
142
+ }
143
+ stdout.write(`This removes everything: the ${PACKAGE} package with npm (commands veo, veodl).\n`);
144
+ if (!keepAliases) {
145
+ stdout.write(customAliases.length
146
+ ? `Custom wrappers to delete: ${customAliases.join(', ')} (${aliasDir})\n`
147
+ : `Custom wrappers: none found${aliasDir ? ` (${aliasDir})` : ''}.\n`);
148
+ }
149
+ if (!keepCache) stdout.write(`Cache to delete: ${cache}\n`);
150
+ if (!keepConfig) stdout.write(`Config to delete: ${config}\n`);
151
+ if (keepCache || keepConfig || keepAliases) {
152
+ const kept = [
153
+ keepAliases && 'aliases',
154
+ keepCache && 'cache',
155
+ keepConfig && 'config',
156
+ ].filter(Boolean).join(', ');
157
+ stdout.write(`Kept: ${kept}.\n`);
158
+ }
159
+ const keepFlags = `${keepAliases ? ' --keep-aliases' : ''}${keepCache ? ' --keep-cache' : ''}${keepConfig ? ' --keep-config' : ''}`;
160
+ stdout.write(`Run with --yes to proceed: veo uninstall --yes${keepFlags}\n`);
161
+ stdout.write(`Manual command: ${manual}\n`);
162
+ return 0;
163
+ }
164
+
165
+ let code;
166
+ try {
167
+ stdout.write(`Uninstalling ${PACKAGE} with npm…\n`);
168
+ code = await runNpmUninstall({ spawnImpl, platform, signal: AbortSignal.timeout(600_000) });
169
+ } catch (error) {
170
+ const reason = error.code === 'ENOENT' ? 'npm was not found. Install Node.js/npm, or uninstall manually.' : `Could not run npm: ${readableError(error)}`;
171
+ stderr.write(`veo: ${reason}\nManual command: ${manual}\n`);
172
+ return 1;
173
+ }
174
+ if (code !== 0) {
175
+ stderr.write(`veo: npm exited with code ${code}. Uninstall manually with: ${manual}\n`);
176
+ return 1;
177
+ }
178
+
179
+ const removedAliases = [];
180
+ if (!keepAliases) {
181
+ for (const name of customAliases) {
182
+ try {
183
+ await removeImpl(name, { binDir: aliasDir, platform, env });
184
+ removedAliases.push(name);
185
+ } catch (error) {
186
+ stderr.write(`veo: removed the package but could not delete alias ${name}: ${readableError(error)}\n`);
187
+ return 1;
188
+ }
189
+ }
190
+ }
191
+ let cacheDeleted = false;
192
+ if (!keepCache) {
193
+ try {
194
+ await rmImpl(cache, { recursive: true, force: true });
195
+ cacheDeleted = true;
196
+ } catch (error) {
197
+ stderr.write(`veo: removed the package but could not delete the cache ${cache}: ${readableError(error)}\n`);
198
+ return 1;
199
+ }
200
+ }
201
+ let configDeleted = false;
202
+ if (!keepConfig) {
203
+ try {
204
+ await rmImpl(config, { force: true });
205
+ configDeleted = true;
206
+ } catch (error) {
207
+ stderr.write(`veo: removed the package but could not delete the config ${config}: ${readableError(error)}\n`);
208
+ return 1;
209
+ }
210
+ }
211
+ if (json) {
212
+ stdout.write(`${JSON.stringify({ status: 'uninstalled', package: PACKAGE, aliases: removedAliases, binDir: aliasDir, cacheDeleted, cache, configDeleted, config })}\n`);
213
+ return 0;
214
+ }
215
+ stdout.write(`veo uninstalled (${PACKAGE}).`);
216
+ if (!keepAliases) stdout.write(removedAliases.length ? ` Aliases deleted: ${removedAliases.join(', ')}.` : ' No custom wrappers found.');
217
+ stdout.write(cacheDeleted ? ` Cache deleted: ${cache}.` : ` Cache kept: ${cache}.`);
218
+ stdout.write(configDeleted ? ` Config deleted: ${config}.` : ` Config kept: ${config}.`);
219
+ stdout.write('\n');
220
+ return 0;
221
+ }
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) {