veodl 1.8.2 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +796 -725
- package/docs/AGENT_GUIDE.md +82 -66
- package/package.json +1 -2
- package/src/alias.js +258 -0
- package/src/cli.js +613 -454
- package/src/config-diagnostics.js +145 -144
- package/src/config-editor.js +285 -281
- package/src/config-template.js +224 -171
- package/src/config.js +217 -213
- package/src/doctor.js +1 -1
- package/src/downloader.js +601 -600
- package/src/interactive.js +115 -82
- package/src/output.js +1 -1
- package/src/runs.js +343 -333
- package/src/source-discovery.js +251 -0
- package/src/state.js +27 -27
- package/src/uninstall.js +222 -0
- package/src/updater.js +5 -3
package/docs/AGENT_GUIDE.md
CHANGED
|
@@ -1,66 +1,82 @@
|
|
|
1
|
-
# veo for agents
|
|
2
|
-
|
|
3
|
-
veo is a command-line video downloader. Use it only for content the user is authorized to download. This guide covers the stable command flow and the difference between download state, media tracks and measured audio signal.
|
|
4
|
-
|
|
5
|
-
## Fast path
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
veo "https://example.com/video" --dry-run --json
|
|
9
|
-
veo "https://example.com/video" --json --output ./downloads
|
|
10
|
-
veo inspect "./downloads/video.mp4" --check-audio --json
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
# veo for agents
|
|
2
|
+
|
|
3
|
+
veo is a command-line video downloader. Use it only for content the user is authorized to download. This guide covers the stable command flow and the difference between download state, media tracks and measured audio signal.
|
|
4
|
+
|
|
5
|
+
## Fast path
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
veo "https://example.com/video" --dry-run --json
|
|
9
|
+
veo "https://example.com/video" --json --output ./downloads
|
|
10
|
+
veo inspect "./downloads/video.mp4" --check-audio --json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For a page with an embedded player that the normal extractor cannot identify,
|
|
14
|
+
run `veo "<page-url>" --list-sources --json` and choose an `index` from the
|
|
15
|
+
returned `sources`. Then use `veo "<page-url>" --source <index> --dry-run --json`
|
|
16
|
+
before the download. Source discovery opens a temporary headless Chromium-based
|
|
17
|
+
browser and observes media requests after Play. It needs Chrome, Edge or Chromium,
|
|
18
|
+
or `VEO_BROWSER_PATH`. A found source is verified by the backend; size may be
|
|
19
|
+
unknown, and DRM-protected media is excluded. Source numbers are tied to the
|
|
20
|
+
page's current discovery run and may change later.
|
|
21
|
+
|
|
22
|
+
Use `--deep-scan --timeout 2m` with `--list-sources` to check every observed
|
|
23
|
+
media candidate up to a two-minute source-search deadline. The JSON result
|
|
24
|
+
contains `timedOut: true` if the scan stopped at the deadline; any returned
|
|
25
|
+
sources were verified before it stopped. A timed-out listing exits nonzero,
|
|
26
|
+
even if it contains partial results. The timeout applies to discovery,
|
|
27
|
+
not to a later download.
|
|
28
|
+
|
|
29
|
+
Set `VEO_NO_UPDATE_CHECK=1` when a background update notice would distract a script. Use an explicit output directory to avoid depending on the agent's current working directory. `--dry-run` plans the download without saving media. It can still inspect the source and prepare backend tools on first use.
|
|
30
|
+
|
|
31
|
+
## Output and failures
|
|
32
|
+
|
|
33
|
+
Download commands write **one JSON object per URL** to stdout with `--json`. Several URLs produce newline-delimited JSON, not a JSON array. Progress, summaries and errors go to stderr. A failed item can still contain saved files, especially within a playlist.
|
|
34
|
+
|
|
35
|
+
Example success:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{"url":"https://example.com/video","status":"saved","title":"Example","files":["/downloads/Example.mp4"],"runId":"abc123"}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Example failure:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{"url":"https://example.com/video","status":"failed","error":"HTTP Error 429","files":[]}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
These are abbreviated examples; fields such as timing and playlist counts may also be present. Exit code `0` means the command succeeded, `1` means at least one item failed, and `130` means cancellation. Validate both the exit code and each result's `status`; a command can fail before producing any stdout JSON. Never infer success from the presence of a file path alone.
|
|
48
|
+
|
|
49
|
+
## Inspect runs and results
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
veo runs --json
|
|
53
|
+
veo runs k3f9qa --json
|
|
54
|
+
veo history --failed --limit 20 --json
|
|
55
|
+
veo retry --last --json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`runs` reports **active** runs, including their ID, state, options, job path and per-item progress. Completed runs disappear from this list. Download JSON and `history` retain the `runId`. `history` reports finished attempts with their outcome and saved file paths. A failed history entry may contain a `job` path, which can be passed to `veo --retry-failed "<job-path>"`; `veo retry --last` finds the newest retryable job automatically. Running jobs are excluded from `retry --last`.
|
|
59
|
+
|
|
60
|
+
## Inspect saved media and audio
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
veo inspect "/downloads/Example.mp4" --json
|
|
64
|
+
veo inspect "/downloads/Example.mp4" --check-audio --json
|
|
65
|
+
veo inspect run abc123 --check-audio --json
|
|
66
|
+
veo inspect run abc123 --search "/moved/videos" --json
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`inspect` uses local FFprobe/FFmpeg and never downloads a URL. Its JSON contains `format`, `durationSeconds`, `sizeBytes`, `streams`, `hasAudioTrack` and `hasVideoTrack`. Without `--check-audio`, `audioCheck` is `null`.
|
|
70
|
+
|
|
71
|
+
With `--check-audio`, veo decodes every audio track from start to end. `audioCheck.hasSignal` is true when at least one track's peak exceeds **−60 dBFS**. `audioCheck.tracks` lists each track's `streamIndex`, `maxDbfs` and `hasSignal`; digital silence may have `maxDbfs: null` or a very low level after conversion. A present audio track does not prove audible content. This is a signal-level check, not a listening or speech-quality test. Decoding long media can take time; if FFmpeg fails, the command exits nonzero instead of reporting silence.
|
|
72
|
+
|
|
73
|
+
`inspect run` reads the saved run record even after the download finishes. It checks whether each file still exists **before** probing it. If a media file was renamed, veo searches the original output tree using file identity, size and a three-part SHA-256 fingerprint. `--search` adds one more directory after a move. Missing or ambiguous matches are reported without probing a different file; missing files make the command exit with code `1`. The fingerprint lives in veo's run history, so this works across media formats without rewriting the media. Runs created before this feature do not have a persistent run record.
|
|
74
|
+
|
|
75
|
+
The run record is stored in veo's per-user cache on the device that ran the download. `inspect` works on Windows, macOS, Linux and Termux with locally available FFmpeg/FFprobe; set `VEO_FFMPEG_PATH` to their directory when they are not on the normal search path. Moving only the media to another device does not transfer the run record.
|
|
76
|
+
|
|
77
|
+
## Operational notes
|
|
78
|
+
|
|
79
|
+
- Run `veo doctor --offline` to check the local setup without network repair. `veo doctor fix` may install missing tools.
|
|
80
|
+
- Use `--resume` for interrupted downloads and `--skip-existing` when repeated saves should be avoided.
|
|
81
|
+
- `veo flush` removes temporary downloads and retry jobs; do not run it as routine maintenance in an agent workflow.
|
|
82
|
+
- Do not call bare `veo` in an interactive terminal from automation: it opens a wizard. Use explicit URLs and flags.
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "veodl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
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
|
+
}
|