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/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 automatically after no downloadable video is found
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 ?? false,
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
- const preliminary = parseArgs({ args, allowPositionals: true, strict: true, allowNegative: true, options: cliOptions() });
225
- config = applyProfile(config, preliminary.values.profile);
226
- const { values, positionals, tokens } = parseArgs({ args, tokens: true, allowNegative: true, allowPositionals: true, strict: true, options: cliOptions(config) });
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
- return options;
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
- const { cleanupDownloadCache } = await import('./download-cache.js');
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
- retryItems = retryItems.map(item => parseCli([item.url, ...retryArgs], { config: { ...item, profiles: loaded.config?.profiles } }));
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, playlist: options.playlist, job: jobFile });
484
- reporter.configure?.({ color: options.color && !options.json });
485
- reporter.start(options.rename);
486
- const cookieWarning = cookieFileWarning(options.cookies);
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, reporter, signal: controller.signal, openFile, items: retryItems, jobFile: jobFile || undefined, runId: run?.id, recordStats: createStatsRecorder(), recordHistory: createHistoryRecorder() });
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 (!interactive && !options.autoListSources) throw new Error(`${originalError.message} Use --list-sources to search this page, then --source <number> to select one.`);
579
- if (interactive && !options.autoListSources) {
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 (!interactive && options.autoListSources) return { sources, formatSource, needsSelection: true };
604
- for (const source of sources) stderr.write(`${formatSource(source)}\n`);
605
- if (!interactive) throw new Error('Select a source with --source <number>.');
606
- const answer = await ask('Source number (Enter to cancel): ', signal);
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 (!object(profile)) return located(text, node(['profiles', name]), `Profile "${name}" must be an object.`);
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) : valueChoices(key, applyProfile(data, profile));
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,