veodl 1.9.0 → 1.10.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 +57 -37
- package/docs/AGENT_GUIDE.md +8 -5
- package/package.json +1 -1
- package/src/backend.js +64 -36
- package/src/cli.js +120 -56
- package/src/config-diagnostics.js +19 -9
- package/src/config-editor.js +181 -66
- package/src/config-template.js +63 -73
- package/src/config.js +110 -27
- package/src/downloader.js +28 -21
- package/src/interactive.js +19 -19
- package/src/jobs.js +13 -4
- package/src/option-suggestions.js +43 -0
- package/src/progress.js +68 -12
- package/src/runs.js +8 -5
- package/src/source-discovery.js +82 -30
package/src/cli.js
CHANGED
|
@@ -7,9 +7,10 @@ import { createInterface } from 'node:readline/promises';
|
|
|
7
7
|
import { createReporter } from './progress.js';
|
|
8
8
|
import { openFile } from './open-file.js';
|
|
9
9
|
import { maybeUpdateNotice, updateMain, UPDATE_HELP, defaultRegistry, packageVersion } from './updater.js';
|
|
10
|
-
import { applyProfile, configMain, loadConfig } from './config.js';
|
|
10
|
+
import { applyProfile, configMain, loadConfig, profileMain, selectedProfileName } from './config.js';
|
|
11
11
|
import { validateItems, describeEstimate } from './playlist.js';
|
|
12
|
-
import { parseSourceTimeout } from './source-discovery.js';
|
|
12
|
+
import { parseSourceTimeout } from './source-discovery.js';
|
|
13
|
+
import { explainUnknownOption, optionSpellings, suggestOption } from './option-suggestions.js';
|
|
13
14
|
import { retryOptions, runJob, jobFilePath } from './jobs.js';
|
|
14
15
|
import { QUALITIES, VIDEO_FORMATS, AUDIO_FORMATS, validateUrl, readableError, cleanText, validateCookieFile, validateBrowserSpec, cookieFileWarning } from './utils.js';
|
|
15
16
|
|
|
@@ -57,7 +58,7 @@ Options:
|
|
|
57
58
|
--resume Keep partial data and continue an interrupted download
|
|
58
59
|
--list-formats Show the available formats and exit
|
|
59
60
|
--list-sources Find video sources loaded by a web page and exit
|
|
60
|
-
--auto-list-sources Search
|
|
61
|
+
--auto-list-sources Search after no video is found (default: on)
|
|
61
62
|
--source <n> Select source number from that page (also for scripts)
|
|
62
63
|
--deep-scan Check every media candidate found during source search
|
|
63
64
|
--timeout <duration> Source search deadline, e.g. 30, 30s or 2m (5s-10m)
|
|
@@ -88,7 +89,8 @@ Commands:
|
|
|
88
89
|
veo alias list|add|remove Manage extra command names (wrappers calling veo)
|
|
89
90
|
veo uninstall [-p <name>] Remove one alias, or everything with --yes
|
|
90
91
|
veo version Show the installed version
|
|
91
|
-
veo config edit|path|profiles|check|show|reset Manage defaults and named profiles
|
|
92
|
+
veo config edit|path|profiles|guide|check|show|reset Manage defaults and named profiles
|
|
93
|
+
veo profile [list|reset|name] Show, list or change the default profile
|
|
92
94
|
veo config edit [--external|--terminal] Choose the configuration editor
|
|
93
95
|
|
|
94
96
|
Run veo without arguments in a terminal for interactive setup.
|
|
@@ -197,11 +199,11 @@ export function optionDefaults(config = {}) {
|
|
|
197
199
|
timeout: config.timeout,
|
|
198
200
|
'deep-scan': config.deepScan ?? false,
|
|
199
201
|
'list-sources': config.listSources ?? false,
|
|
200
|
-
'auto-list-sources': config.autoListSources ??
|
|
202
|
+
'auto-list-sources': config.autoListSources ?? true,
|
|
201
203
|
};
|
|
202
204
|
}
|
|
203
205
|
|
|
204
|
-
export function cliOptions(config = {}) {
|
|
206
|
+
export function cliOptions(config = {}) {
|
|
205
207
|
const defaults = optionDefaults(config);
|
|
206
208
|
const options = {};
|
|
207
209
|
for (const [name, extra] of Object.entries(STRING_OPTIONS)) {
|
|
@@ -213,7 +215,42 @@ export function cliOptions(config = {}) {
|
|
|
213
215
|
if (defaults[name] !== undefined) options[name].default = defaults[name];
|
|
214
216
|
}
|
|
215
217
|
return options;
|
|
216
|
-
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const COMMAND_FLAGS = {
|
|
221
|
+
stats: ['--json'], history: ['--json', '--limit', '--failed'],
|
|
222
|
+
runs: ['--json'], stop: [], flush: ['--stats'],
|
|
223
|
+
inspect: ['--json', '--check-audio', '--search'],
|
|
224
|
+
doctor: ['--offline', '--output', '-o'],
|
|
225
|
+
update: ['--check'], up: ['--check'], upgrade: ['--check'], check: ['--check'],
|
|
226
|
+
backend: ['--check', '--keep-files'],
|
|
227
|
+
alias: ['--json', '--force', '--bin-dir'],
|
|
228
|
+
uninstall: ['--prefix', '-p', '--yes', '--keep-cache', '--keep-config', '--keep-aliases', '--json', '--bin-dir'],
|
|
229
|
+
config: ['--profile', '--external', '--terminal'],
|
|
230
|
+
profile: [],
|
|
231
|
+
};
|
|
232
|
+
const COMMAND_NAMES = [...Object.keys(COMMAND_FLAGS), 'retry', 'help', 'version'];
|
|
233
|
+
|
|
234
|
+
function mistypedCommandOption(args) {
|
|
235
|
+
const command = args[0];
|
|
236
|
+
if (command && !command.startsWith('-') && !COMMAND_NAMES.includes(command)) {
|
|
237
|
+
const suggestion = suggestOption(command, COMMAND_NAMES);
|
|
238
|
+
if (suggestion) return `Unknown command "${command}". Did you mean "${suggestion}"?`;
|
|
239
|
+
}
|
|
240
|
+
const options = Object.hasOwn(COMMAND_FLAGS, command)
|
|
241
|
+
? [...COMMAND_FLAGS[command], '--help', '-h', '--no-color', '--color', '--profile']
|
|
242
|
+
: command === 'retry' ? [...optionSpellings(cliOptions()), '--last'] : null;
|
|
243
|
+
if (!options) return null; // Download flags are checked by parseCli itself.
|
|
244
|
+
const start = ['config', 'backend', 'alias'].includes(command) && args[1] && !args[1].startsWith('-') ? 2 : 1;
|
|
245
|
+
for (const arg of args.slice(start)) {
|
|
246
|
+
if (arg === '--') break;
|
|
247
|
+
const name = arg.split('=')[0];
|
|
248
|
+
if (!name.startsWith('-') || options.includes(name)) continue;
|
|
249
|
+
const suggestion = suggestOption(name, options);
|
|
250
|
+
if (suggestion) return `Unknown option "${name}". Did you mean "${suggestion}"?`;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
217
254
|
|
|
218
255
|
export function parseCli(args, { config = {} } = {}) {
|
|
219
256
|
if (args[0] === 'help') args = ['--help', ...args.slice(1)];
|
|
@@ -221,9 +258,15 @@ export function parseCli(args, { config = {} } = {}) {
|
|
|
221
258
|
if (args.length !== 1) throw new Error('Usage: veo version');
|
|
222
259
|
return { version: true };
|
|
223
260
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
261
|
+
let preliminary;
|
|
262
|
+
try { preliminary = parseArgs({ args, allowPositionals: true, strict: true, allowNegative: true, options: cliOptions() }); }
|
|
263
|
+
catch (error) { throw explainUnknownOption(error, cliOptions()); }
|
|
264
|
+
const profileName = selectedProfileName(config, preliminary.values.profile);
|
|
265
|
+
config = applyProfile(config, preliminary.values.profile);
|
|
266
|
+
let parsed;
|
|
267
|
+
try { parsed = parseArgs({ args, tokens: true, allowNegative: true, allowPositionals: true, strict: true, options: cliOptions(config) }); }
|
|
268
|
+
catch (error) { throw explainUnknownOption(error, cliOptions(config)); }
|
|
269
|
+
const { values, positionals, tokens } = parsed;
|
|
227
270
|
if (values.help || values.version) return values;
|
|
228
271
|
// A stored default conflicting with a flag typed right now is a user error;
|
|
229
272
|
// a stored default merely ignored by another flag is not.
|
|
@@ -300,15 +343,18 @@ export function parseCli(args, { config = {} } = {}) {
|
|
|
300
343
|
if (options.rename && options.filenameTemplate) throw new Error('--rename and --filename-template cannot be combined.');
|
|
301
344
|
const disableSubs = values.subs === false && tokens.some(token => token.name === 'subs');
|
|
302
345
|
if (options.subLangs && !disableSubs) options.subs = true;
|
|
303
|
-
if (disableSubs) {
|
|
304
|
-
options.subLangs = undefined;
|
|
305
|
-
options.embedSubs = false;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
346
|
+
if (disableSubs) {
|
|
347
|
+
options.subLangs = undefined;
|
|
348
|
+
options.embedSubs = false;
|
|
349
|
+
}
|
|
350
|
+
options.profile = profileName;
|
|
351
|
+
return options;
|
|
352
|
+
}
|
|
309
353
|
|
|
310
|
-
export async function main(args = process.argv.slice(2), { config } = {}) {
|
|
311
|
-
if (args[0] === 'help') args = ['--help', ...args.slice(1)];
|
|
354
|
+
export async function main(args = process.argv.slice(2), { config } = {}) {
|
|
355
|
+
if (args[0] === 'help') args = ['--help', ...args.slice(1)];
|
|
356
|
+
const mistyped = mistypedCommandOption(args);
|
|
357
|
+
if (mistyped) { process.stderr.write(`veo: ${mistyped}\n`); return 1; }
|
|
312
358
|
let color = !args.includes('--no-color') && !args.includes('--json');
|
|
313
359
|
let display;
|
|
314
360
|
try {
|
|
@@ -375,7 +421,7 @@ async function runMain(args, { config }) {
|
|
|
375
421
|
try { return await (await import('./runs.js')).stopMain(args.slice(1)); }
|
|
376
422
|
catch (error) { stderr.write(`veo: ${readableError(error)}\n`); return 1; }
|
|
377
423
|
}
|
|
378
|
-
if (args[0] === 'retry') {
|
|
424
|
+
if (args[0] === 'retry') {
|
|
379
425
|
if (args[1] !== '--last') {
|
|
380
426
|
stderr.write('veo: Usage: veo retry --last [download options]\n');
|
|
381
427
|
return 1;
|
|
@@ -384,8 +430,12 @@ async function runMain(args, { config }) {
|
|
|
384
430
|
const { latestFailedJob } = await import('./jobs.js');
|
|
385
431
|
args = ['--retry-failed', await latestFailedJob(), ...args.slice(2)];
|
|
386
432
|
} catch (error) { stderr.write(`veo: ${readableError(error)}\n`); return 1; }
|
|
387
|
-
}
|
|
388
|
-
|
|
433
|
+
}
|
|
434
|
+
if (args[0] === 'profile') {
|
|
435
|
+
try { return await profileMain(args.slice(1)); }
|
|
436
|
+
catch (error) { stderr.write(`veo: ${readableError(error)}\n`); return 1; }
|
|
437
|
+
}
|
|
438
|
+
const { cleanupDownloadCache } = await import('./download-cache.js');
|
|
389
439
|
await cleanupDownloadCache().catch(error => stderr.write(`veo: Could not clean expired local downloads: ${readableError(error)}\n`));
|
|
390
440
|
// update/upgrade/up/check subcommands are handled before URL validation.
|
|
391
441
|
if (['update', 'up', 'upgrade', 'check'].includes(args[0])) {
|
|
@@ -396,10 +446,10 @@ async function runMain(args, { config }) {
|
|
|
396
446
|
}
|
|
397
447
|
return updateMain(args, { registry: defaultRegistry() });
|
|
398
448
|
}
|
|
399
|
-
if (args[0] === 'config') {
|
|
400
|
-
try { return await configMain(args.slice(1)); }
|
|
401
|
-
catch (error) { stderr.write(`veo: ${readableError(error)}\n`); return 1; }
|
|
402
|
-
}
|
|
449
|
+
if (args[0] === 'config') {
|
|
450
|
+
try { return await configMain(args.slice(1)); }
|
|
451
|
+
catch (error) { stderr.write(`veo: ${readableError(error)}\n`); return 1; }
|
|
452
|
+
}
|
|
403
453
|
if (args[0] === 'doctor') {
|
|
404
454
|
try {
|
|
405
455
|
const { doctorMain } = await import('./doctor.js');
|
|
@@ -465,7 +515,14 @@ async function runMain(args, { config }) {
|
|
|
465
515
|
retryItems = await retryOptions(options.retryFailed);
|
|
466
516
|
// Revalidate stored options and apply only flags explicitly supplied now.
|
|
467
517
|
const retryArgs = args.filter((arg, index) => arg !== '--retry-failed' && args[index - 1] !== '--retry-failed' && !arg.startsWith('--retry-failed='));
|
|
468
|
-
|
|
518
|
+
const overrideProfile = retryArgs.some(arg => arg === '--profile' || arg.startsWith('--profile='));
|
|
519
|
+
retryItems = retryItems.map(item => {
|
|
520
|
+
const resumed = parseCli([item.url, ...retryArgs], { config: overrideProfile
|
|
521
|
+
? { ...item, profiles: loaded.config?.profiles }
|
|
522
|
+
: item });
|
|
523
|
+
if (!overrideProfile) resumed.profile = item.profile;
|
|
524
|
+
return resumed;
|
|
525
|
+
});
|
|
469
526
|
options = { ...retryItems[0], urls: retryItems.map(item => item.url) };
|
|
470
527
|
}
|
|
471
528
|
if (options.batchFile) {
|
|
@@ -479,17 +536,22 @@ async function runMain(args, { config }) {
|
|
|
479
536
|
// The job file is created before the first download, so another terminal can
|
|
480
537
|
// follow this run's per-item progress with `veo runs <id>`.
|
|
481
538
|
const jobFile = options.listFormats || options.listSources || options.dryRun ? null : jobFilePath();
|
|
482
|
-
await run?.describe({ urls: options.urls, output: options.output ? path.resolve(options.output) : null,
|
|
483
|
-
audio: options.audio, quality: options.quality, format: options.format,
|
|
484
|
-
|
|
485
|
-
reporter.
|
|
486
|
-
|
|
539
|
+
await run?.describe({ urls: options.urls, output: options.output ? path.resolve(options.output) : null,
|
|
540
|
+
audio: options.audio, quality: options.quality, format: options.format, profile: options.profile || null,
|
|
541
|
+
playlist: options.playlist, job: jobFile });
|
|
542
|
+
reporter.configure?.({ color: options.color && !options.json });
|
|
543
|
+
reporter.start(options.rename);
|
|
544
|
+
if (!options.dryRun && !options.listFormats && !options.listSources) reporter.profile(options.profile);
|
|
545
|
+
const preparedBackends = new Map();
|
|
546
|
+
const reuseBackend = request => preparedBackends.has(request.url) ? async () => preparedBackends.get(request.url) : undefined;
|
|
547
|
+
const cookieWarning = cookieFileWarning(options.cookies);
|
|
487
548
|
if (cookieWarning) stderr.write(`veo: ${cookieWarning}\n`);
|
|
488
549
|
|
|
489
550
|
// For a single page, fall back to media requests observed while its player loads.
|
|
490
551
|
// Explicit --source always rediscovers: signed media URLs can expire between runs.
|
|
491
552
|
if (!retryItems && options.urls.length === 1 && (!options.playlist || options.listSources)) {
|
|
492
|
-
const discovery = await resolvePageSource(options, { signal: controller.signal, stderr
|
|
553
|
+
const discovery = await resolvePageSource(options, { signal: controller.signal, stderr, reporter,
|
|
554
|
+
onBackend: backend => preparedBackends.set(options.url, backend) });
|
|
493
555
|
if (options.listSources) {
|
|
494
556
|
if (options.json) stdout.write(`${JSON.stringify({ url: options.url, status: 'sources', timedOut: Boolean(discovery.sources.timedOut), sources: discovery.sources.map(({ url, pageUrl, ...safe }) => safe) })}\n`);
|
|
495
557
|
else for (const source of discovery.sources) stdout.write(`${discovery.formatSource(source)}\n`);
|
|
@@ -512,20 +574,21 @@ async function runMain(args, { config }) {
|
|
|
512
574
|
}
|
|
513
575
|
if (retryItems) for (const item of retryItems) {
|
|
514
576
|
if (!item.source) continue;
|
|
515
|
-
const discovery = await resolvePageSource(item, { signal: controller.signal, stderr
|
|
577
|
+
const discovery = await resolvePageSource(item, { signal: controller.signal, stderr, reporter,
|
|
578
|
+
onBackend: backend => preparedBackends.set(item.url, backend) });
|
|
516
579
|
item.mediaUrl = discovery.mediaUrl;
|
|
517
580
|
}
|
|
518
581
|
|
|
519
582
|
if (options.listFormats) {
|
|
520
583
|
const { listFormats } = await import('./downloader.js');
|
|
521
|
-
stdout.write(await listFormats(options, { signal: controller.signal, reporter }));
|
|
584
|
+
stdout.write(await listFormats(options, { signal: controller.signal, reporter, backendResolver: reuseBackend(options) }));
|
|
522
585
|
return 0;
|
|
523
586
|
}
|
|
524
587
|
if (options.dryRun) {
|
|
525
588
|
const { planDownload } = await import('./downloader.js');
|
|
526
589
|
for (const request of retryItems || options.urls.map(url => ({ ...options, url }))) {
|
|
527
590
|
const { url } = request;
|
|
528
|
-
const plan = await planDownload(request, { signal: controller.signal, reporter });
|
|
591
|
+
const plan = await planDownload(request, { signal: controller.signal, reporter, backendResolver: reuseBackend(request) });
|
|
529
592
|
if (options.json) stdout.write(`${JSON.stringify({ url, status: 'planned', ...plan })}\n`);
|
|
530
593
|
else {
|
|
531
594
|
stdout.write(`URL: ${url}\n`);
|
|
@@ -539,7 +602,8 @@ async function runMain(args, { config }) {
|
|
|
539
602
|
const { download } = await import('./downloader.js');
|
|
540
603
|
const { createStatsRecorder } = await import('./stats.js');
|
|
541
604
|
const { createHistoryRecorder } = await import('./history.js');
|
|
542
|
-
const result = await runJob(options, { download
|
|
605
|
+
const result = await runJob(options, { download: (request, dependencies) => download(request, { ...dependencies, backendResolver: reuseBackend(request) }),
|
|
606
|
+
reporter, signal: controller.signal, openFile, items: retryItems, jobFile: jobFile || undefined, runId: run?.id, recordStats: createStatsRecorder(), recordHistory: createHistoryRecorder() });
|
|
543
607
|
return result;
|
|
544
608
|
} catch (error) {
|
|
545
609
|
reporter.fail(controller.signal.aborted);
|
|
@@ -558,11 +622,12 @@ async function askSourceQuestion(prompt, signal) {
|
|
|
558
622
|
finally { rl.close(); }
|
|
559
623
|
}
|
|
560
624
|
|
|
561
|
-
export async function resolvePageSource(options, { signal, stderr, inspect, discover, ask = askSourceQuestion,
|
|
562
|
-
backendResolver, interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !options.json) }) {
|
|
625
|
+
export async function resolvePageSource(options, { signal, stderr, reporter, onBackend, inspect, discover, ask = askSourceQuestion,
|
|
626
|
+
backendResolver, interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !options.json) }) {
|
|
563
627
|
const { fetchMetadata, prepareBackend, runBackend } = await import('./downloader.js');
|
|
564
628
|
const { discoverSources, formatSource, shouldOfferSourceDiscovery } = await import('./source-discovery.js');
|
|
565
|
-
const backend = inspect ? null : await (backendResolver || prepareBackend)(options, { signal });
|
|
629
|
+
const backend = inspect ? null : await (backendResolver || prepareBackend)(options, { signal, reporter });
|
|
630
|
+
if (backend) onBackend?.(backend);
|
|
566
631
|
const inspectMedia = (request, requestSignal = signal) => inspect ? inspect(request, requestSignal) : fetchMetadata(request, { signal: requestSignal, backend, runner: runBackend });
|
|
567
632
|
let originalError;
|
|
568
633
|
if (!options.source && !options.listSources) {
|
|
@@ -573,13 +638,10 @@ export async function resolvePageSource(options, { signal, stderr, inspect, disc
|
|
|
573
638
|
}
|
|
574
639
|
return {};
|
|
575
640
|
}
|
|
576
|
-
catch (error) { originalError = error; }
|
|
577
|
-
if (!shouldOfferSourceDiscovery(originalError)) throw originalError;
|
|
578
|
-
if (
|
|
579
|
-
|
|
580
|
-
const answer = (await ask('No downloadable video found. Search this page for other sources? (y/N): ', signal)).trim().toLowerCase();
|
|
581
|
-
if (!['y', 'yes'].includes(answer)) throw originalError;
|
|
582
|
-
}
|
|
641
|
+
catch (error) { originalError = error; }
|
|
642
|
+
if (!shouldOfferSourceDiscovery(originalError)) throw originalError;
|
|
643
|
+
if (options.autoListSources === false) throw new Error(`${originalError.message} Use --list-sources to search this page, then --source <number> to select one.`);
|
|
644
|
+
stderr.write('veo: No direct video found; searching the page for a playable source…\n');
|
|
583
645
|
}
|
|
584
646
|
let sources;
|
|
585
647
|
try {
|
|
@@ -594,16 +656,18 @@ export async function resolvePageSource(options, { signal, stderr, inspect, disc
|
|
|
594
656
|
}
|
|
595
657
|
if (options.listSources) return { sources, formatSource };
|
|
596
658
|
if (sources.timedOut) stderr.write(`veo: Source search timed out after ${options.timeout || (options.deepScan ? '120s' : '45s')}; showing verified sources found so far.\n`);
|
|
597
|
-
if (!sources.length) {
|
|
598
|
-
if (originalError) throw originalError;
|
|
599
|
-
throw new Error('No downloadable video sources were found on this page.');
|
|
600
|
-
}
|
|
601
|
-
let index = Number(options.source);
|
|
602
|
-
if (!index) {
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
659
|
+
if (!sources.length) {
|
|
660
|
+
if (originalError) throw originalError;
|
|
661
|
+
throw new Error('No downloadable video sources were found on this page.');
|
|
662
|
+
}
|
|
663
|
+
let index = Number(options.source);
|
|
664
|
+
if (!index) {
|
|
665
|
+
if (sources.length === 1) index = 1;
|
|
666
|
+
else if (!interactive) return { sources, formatSource, needsSelection: true };
|
|
667
|
+
}
|
|
668
|
+
if (!index) {
|
|
669
|
+
for (const source of sources) stderr.write(`${formatSource(source)}\n`);
|
|
670
|
+
const answer = await ask('Source number (Enter to cancel): ', signal);
|
|
607
671
|
if (!/^[1-9]\d*$/.test(answer.trim())) throw new Error('No source selected.');
|
|
608
672
|
index = Number(answer.trim());
|
|
609
673
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONFIG_KEYS, parseConfigText, effectiveConfig, applyProfile } from './config.js';
|
|
1
|
+
import { CONFIG_KEYS, RESERVED_PROFILE_NAMES, parseConfigText, effectiveConfig, applyProfile } from './config.js';
|
|
2
2
|
import { stripConfigComments } from './config-template.js';
|
|
3
3
|
import { QUALITIES, AUDIO_FORMATS, VIDEO_FORMATS } from './utils.js';
|
|
4
4
|
|
|
@@ -75,7 +75,7 @@ export async function semanticIssue(text) {
|
|
|
75
75
|
if (!object(data)) return located(text, node([]), 'The config must contain a JSON object.');
|
|
76
76
|
const check = (settings, path) => {
|
|
77
77
|
for (const [key, value] of Object.entries(settings)) {
|
|
78
|
-
if (!path.length && ['$schema', 'profiles'].includes(key)) continue;
|
|
78
|
+
if (!path.length && ['$schema', 'profiles', 'activeProfile'].includes(key)) continue;
|
|
79
79
|
const target = node([...path, key]);
|
|
80
80
|
if (!Object.hasOwn(CONFIG_KEYS, key)) {
|
|
81
81
|
const proposed = suggestion(key, path.length ? keys : [...keys, 'profiles', '$schema']);
|
|
@@ -90,13 +90,22 @@ export async function semanticIssue(text) {
|
|
|
90
90
|
};
|
|
91
91
|
let issue = check(data, []);
|
|
92
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 (
|
|
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.`);
|
|
97
100
|
issue = check(profile, ['profiles', name]); if (issue) return issue;
|
|
98
101
|
}
|
|
99
|
-
}
|
|
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
|
+
}
|
|
100
109
|
const { config } = parseConfigText(text);
|
|
101
110
|
for (const profile of [undefined, ...Object.keys(config.profiles || {})]) {
|
|
102
111
|
try { await effectiveConfig(config, profile); }
|
|
@@ -105,7 +114,7 @@ export async function semanticIssue(text) {
|
|
|
105
114
|
let key = /Invalid quality/.test(message) ? 'quality' : /Invalid (?:audio|video) format/.test(message) ? 'format' :
|
|
106
115
|
/output directory/.test(message) ? 'output' : /custom filename/.test(message) ? 'rename' :
|
|
107
116
|
/--([a-z-]+)/.exec(message)?.[1]?.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
108
|
-
const name = profile ?? (config.profiles?.default ? 'default' : undefined);
|
|
117
|
+
const name = profile ?? config.activeProfile ?? (config.profiles?.default ? 'default' : undefined);
|
|
109
118
|
const selected = applyProfile(config, profile);
|
|
110
119
|
// Template errors do not name the CLI option. Locate the failing template.
|
|
111
120
|
if (!key) {
|
|
@@ -134,7 +143,8 @@ export function editorCompletions(text, cursor) {
|
|
|
134
143
|
const key = path.at(-1), property = cursor >= item.keyStart && cursor <= item.keyEnd;
|
|
135
144
|
if (!property && !(cursor >= item.start && cursor <= item.end)) continue;
|
|
136
145
|
const profile = path.length === 3 ? path[1] : undefined;
|
|
137
|
-
const choices = property ? (path.length === 1 ? [...keys, 'profiles', '$schema'] : keys) :
|
|
146
|
+
const choices = property ? (path.length === 1 ? [...keys, 'profiles', 'activeProfile', '$schema'] : keys) :
|
|
147
|
+
key === 'activeProfile' ? Object.keys(data.profiles || {}) : valueChoices(key, applyProfile(data, profile));
|
|
138
148
|
if (!choices.length) return null;
|
|
139
149
|
const proposed = property ? suggestion(key, choices) : null;
|
|
140
150
|
return { start: property ? item.keyStart : item.start, end: property ? item.keyEnd : item.end,
|