optionkit 0.0.1 → 0.0.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.
@@ -0,0 +1,663 @@
1
+ import { constants } from 'node:fs';
2
+ import { access, mkdir, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { detectPackageManager, installPackage } from '@antfu/install-pkg';
6
+ import { catalog } from './_generated/catalog.js';
7
+ import { importPackageIfAvailable, resolvePackage } from './resolve.js';
8
+ const REQUIRED_CLI_DEPENDENCY_SPECIFIERS = [
9
+ `fireargs@${catalog.fireargs}`,
10
+ `zod@${catalog.zod}`,
11
+ ];
12
+ const CLEAN_STACK_SPECIFIER = `clean-stack@${catalog['clean-stack']}`;
13
+ const installPromises = new Map();
14
+ const cleanStackPromises = new Map();
15
+ /**
16
+ * Load the CLI adapter on demand. Every command input is transparently augmented
17
+ * with environment bindings and conflict-free short flags.
18
+ */
19
+ export async function createOptionKit(settings = {}) {
20
+ const packageInfo = await consumingPackage(settings);
21
+ const loaded = await loadDependencies(packageInfo.root, settings);
22
+ const environmentPrefix = resolveEnvironmentPrefix(settings.environmentPrefix, settings.projectName ?? packageInfo.name);
23
+ return {
24
+ f: createAdapter(loaded.f, loaded.z, environmentPrefix, settings),
25
+ z: loaded.z,
26
+ environmentPrefix,
27
+ packageRoot: packageInfo.root,
28
+ dependencySource: loaded.source,
29
+ formatError: error => formatError(error, loaded.cleanStack),
30
+ };
31
+ }
32
+ async function consumingPackage(settings) {
33
+ const root = settings.packageRoot
34
+ ? path.resolve(settings.packageRoot)
35
+ : await findPackageRoot(await discoveryStart(settings.moduleUrl));
36
+ const manifest = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
37
+ const name = settings.projectName ?? manifest.name;
38
+ if (typeof name !== 'string' || name.trim() === '') {
39
+ throw new TypeError('The consuming package must have a name or set projectName.');
40
+ }
41
+ return { name, root };
42
+ }
43
+ async function discoveryStart(moduleUrl) {
44
+ const candidate = moduleUrl === undefined
45
+ ? process.argv[1] ?? process.cwd()
46
+ : moduleUrl instanceof URL || moduleUrl.startsWith('file:')
47
+ ? fileURLToPath(moduleUrl)
48
+ : moduleUrl;
49
+ const absolute = path.resolve(candidate);
50
+ try {
51
+ return await realpath(absolute);
52
+ }
53
+ catch {
54
+ return absolute;
55
+ }
56
+ }
57
+ async function findPackageRoot(start) {
58
+ let current;
59
+ try {
60
+ current = (await stat(start)).isDirectory() ? start : path.dirname(start);
61
+ }
62
+ catch {
63
+ current = path.extname(start) === '' ? start : path.dirname(start);
64
+ }
65
+ while (true) {
66
+ const manifest = path.join(current, 'package.json');
67
+ try {
68
+ await access(manifest, constants.R_OK);
69
+ return current;
70
+ }
71
+ catch {
72
+ const parent = path.dirname(current);
73
+ if (parent === current) {
74
+ throw new Error(`Could not find a package.json above ${JSON.stringify(start)}.`);
75
+ }
76
+ current = parent;
77
+ }
78
+ }
79
+ }
80
+ async function loadDependencies(packageRoot, settings) {
81
+ const project = await importDependencies(packageRoot);
82
+ const invocationDirectory = path.resolve(settings.invocationDirectory ?? process.cwd());
83
+ const detected = await detectPackageManager(invocationDirectory);
84
+ const packageManager = detected ?? 'npm';
85
+ const installRoot = path.resolve(settings.cacheDirectory ?? packageRoot);
86
+ const installSource = settings.cacheDirectory ? 'cache' : 'package-root';
87
+ if (settings.cacheDirectory)
88
+ await ensureCachePackage(installRoot);
89
+ if (project) {
90
+ const projectCleanStack = await importCleanStack(packageRoot, settings.log);
91
+ const cleanStack = projectCleanStack ?? await cachedCleanStack(installRoot, packageManager, settings.log);
92
+ return { ...project, cleanStack, source: 'project' };
93
+ }
94
+ let installation = installPromises.get(installRoot);
95
+ if (!installation) {
96
+ installation = installAndImport(installRoot, packageManager, installSource, settings.log);
97
+ installPromises.set(installRoot, installation);
98
+ }
99
+ try {
100
+ return await installation;
101
+ }
102
+ catch (error) {
103
+ installPromises.delete(installRoot);
104
+ throw error;
105
+ }
106
+ }
107
+ async function installAndImport(installRoot, packageManager, source, log) {
108
+ const cleanStack = await cachedCleanStack(installRoot, packageManager, log);
109
+ const existing = await importDependencies(installRoot);
110
+ if (existing)
111
+ return { ...existing, cleanStack, source };
112
+ log?.('Installing required CLI dependencies.', {
113
+ installDirectory: installRoot,
114
+ packages: REQUIRED_CLI_DEPENDENCY_SPECIFIERS,
115
+ packageManager,
116
+ });
117
+ try {
118
+ await installWithoutSaving(REQUIRED_CLI_DEPENDENCY_SPECIFIERS, installRoot, packageManager);
119
+ }
120
+ catch (error) {
121
+ log?.('Could not install required CLI dependencies.', {
122
+ error: formatError(error, cleanStack),
123
+ });
124
+ throw error;
125
+ }
126
+ const installed = await importDependencies(installRoot);
127
+ if (!installed) {
128
+ const unresolved = ['fireargs', 'zod'].filter(name => !resolvePackage(name, installRoot));
129
+ throw new Error(`CLI dependencies were installed but ${unresolved.join(' and ') || 'their exports'} could not be resolved from ${installRoot}.`);
130
+ }
131
+ log?.('Required CLI dependencies are ready.', { installDirectory: installRoot });
132
+ return { ...installed, cleanStack, source };
133
+ }
134
+ async function importDependencies(root) {
135
+ const [adapter, zod] = await Promise.all([
136
+ importPackageIfAvailable('fireargs', root),
137
+ importPackageIfAvailable('zod', root),
138
+ ]);
139
+ if (!adapter || !zod)
140
+ return undefined;
141
+ const requiredBuilders = ['argument', 'command', 'option', 'program'];
142
+ if (!adapter.f || requiredBuilders.some(name => typeof adapter.f[name] !== 'function')) {
143
+ throw new TypeError('The resolved CLI adapter does not expose its required builder functions.');
144
+ }
145
+ if (!zod.z || typeof zod.z.object !== 'function') {
146
+ throw new TypeError('The resolved Zod package does not expose z.object().');
147
+ }
148
+ return { f: adapter.f, z: zod.z };
149
+ }
150
+ async function cachedCleanStack(cacheRoot, packageManager, log) {
151
+ let installation = cleanStackPromises.get(cacheRoot);
152
+ if (!installation) {
153
+ installation = installCleanStack(cacheRoot, packageManager, log);
154
+ cleanStackPromises.set(cacheRoot, installation);
155
+ }
156
+ return installation;
157
+ }
158
+ async function installCleanStack(installRoot, packageManager, log) {
159
+ const existing = await importCleanStack(installRoot, log);
160
+ if (existing)
161
+ return existing;
162
+ log?.('Installing optional stack cleaner.', {
163
+ installDirectory: installRoot,
164
+ package: CLEAN_STACK_SPECIFIER,
165
+ packageManager,
166
+ });
167
+ try {
168
+ await installWithoutSaving(CLEAN_STACK_SPECIFIER, installRoot, packageManager);
169
+ const installed = await importCleanStack(installRoot, log);
170
+ if (installed)
171
+ log?.('Optional stack cleaner is ready.', { installDirectory: installRoot });
172
+ return installed;
173
+ }
174
+ catch (error) {
175
+ log?.('Optional stack cleaner is unavailable.', { error: formatError(error) });
176
+ return undefined;
177
+ }
178
+ }
179
+ async function importCleanStack(root, log) {
180
+ try {
181
+ return (await importPackageIfAvailable('clean-stack', root))
182
+ ?.default;
183
+ }
184
+ catch (error) {
185
+ log?.('Optional stack cleaner could not be loaded.', { error: formatError(error) });
186
+ return undefined;
187
+ }
188
+ }
189
+ function formatError(error, cleanStack) {
190
+ const stack = error instanceof Error
191
+ ? error.stack ?? `${error.name}: ${error.message}`
192
+ : String(error);
193
+ if (!cleanStack)
194
+ return stack;
195
+ try {
196
+ return cleanStack(stack, { pretty: true });
197
+ }
198
+ catch {
199
+ return stack;
200
+ }
201
+ }
202
+ async function ensureCachePackage(cacheRoot) {
203
+ await mkdir(cacheRoot, { recursive: true });
204
+ const manifest = path.join(cacheRoot, 'package.json');
205
+ try {
206
+ await writeFile(manifest, `${JSON.stringify({ name: 'optionkit-cli-cache', private: true, type: 'module' }, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
207
+ }
208
+ catch (error) {
209
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST'))
210
+ throw error;
211
+ }
212
+ }
213
+ const INSTALL_STATE_FILES = [
214
+ 'package.json',
215
+ 'bun.lock',
216
+ 'bun.lockb',
217
+ 'package-lock.json',
218
+ 'pnpm-lock.yaml',
219
+ 'yarn.lock',
220
+ ];
221
+ async function installWithoutSaving(packages, installRoot, packageManager) {
222
+ const snapshots = new Map();
223
+ for (const name of INSTALL_STATE_FILES) {
224
+ const filename = path.join(installRoot, name);
225
+ try {
226
+ snapshots.set(filename, await readFile(filename));
227
+ }
228
+ catch (error) {
229
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
230
+ throw error;
231
+ snapshots.set(filename, undefined);
232
+ }
233
+ }
234
+ try {
235
+ await installPackage(packages, {
236
+ additionalArgs: agent => noSaveArguments(agent),
237
+ cwd: installRoot,
238
+ packageManager,
239
+ preferOffline: true,
240
+ silent: true,
241
+ });
242
+ }
243
+ finally {
244
+ for (const [filename, content] of snapshots) {
245
+ if (content)
246
+ await writeFile(filename, content);
247
+ else
248
+ await rm(filename, { force: true });
249
+ }
250
+ }
251
+ }
252
+ function noSaveArguments(agent) {
253
+ if (agent === 'bun')
254
+ return ['--no-save'];
255
+ if (agent === 'npm')
256
+ return ['--no-save', '--package-lock=false'];
257
+ if (agent === 'pnpm')
258
+ return ['--lockfile=false'];
259
+ if (agent === 'yarn')
260
+ return ['--no-lockfile'];
261
+ return [];
262
+ }
263
+ function createAdapter(adapter, zod, environmentPrefix, settings) {
264
+ const environmentBindings = new WeakMap();
265
+ return {
266
+ ...adapter,
267
+ command(configuration = {}) {
268
+ const { passThroughArguments, ...fireargsConfiguration } = configuration;
269
+ const passThrough = passThroughArguments === true ||
270
+ (typeof passThroughArguments === 'object' && passThroughArguments !== null);
271
+ const builder = adapter.command(passThrough
272
+ ? {
273
+ allowExcessArguments: true,
274
+ allowUnknownOption: true,
275
+ enablePositionalOptions: true,
276
+ jsonOption: false,
277
+ llmsOption: false,
278
+ passThroughOptions: true,
279
+ ...fireargsConfiguration,
280
+ }
281
+ : fireargsConfiguration);
282
+ return {
283
+ input(schema) {
284
+ const commandInput = passThrough
285
+ ? addPassThroughArgument(schema, adapter, zod, passThroughArguments)
286
+ : schema;
287
+ const { adapted, bindings } = adaptInputSchema(commandInput, adapter, zod, environmentPrefix, settings);
288
+ const inputBuilder = builder.input(adapted);
289
+ return {
290
+ output(output) {
291
+ const outputBuilder = inputBuilder.output(output);
292
+ const attach = (route) => {
293
+ environmentBindings.set(route, bindings);
294
+ wrapStructuredEnvironment(route, () => bindings);
295
+ return route;
296
+ };
297
+ return {
298
+ handler(handler) {
299
+ return attach(outputBuilder.handler(handler));
300
+ },
301
+ render(render) {
302
+ const handlerBuilder = outputBuilder.render(render);
303
+ return {
304
+ handler(handler) {
305
+ return attach(handlerBuilder.handler(handler));
306
+ },
307
+ };
308
+ },
309
+ };
310
+ },
311
+ };
312
+ },
313
+ };
314
+ },
315
+ program(configuration = {}) {
316
+ const builder = adapter.program(configuration);
317
+ return {
318
+ router(routes) {
319
+ const program = builder.router(routes);
320
+ wrapStructuredEnvironment(program, (argv, options) => {
321
+ const selected = selectedLeaf(program, argv, options);
322
+ return selected ? environmentBindings.get(selected) ?? [] : [];
323
+ });
324
+ return program;
325
+ },
326
+ };
327
+ },
328
+ };
329
+ }
330
+ function addPassThroughArgument(schema, adapter, zod, configuration) {
331
+ const object = objectSchema(schema);
332
+ if ('args' in object.shape) {
333
+ throw new TypeError('passThroughArguments reserves the input field "args".');
334
+ }
335
+ const description = typeof configuration === 'object'
336
+ ? configuration.description
337
+ : undefined;
338
+ const args = adapter.argument().schema(zod.array(zod.string()).default([]).describe(description ?? 'Arguments passed to the executable.'));
339
+ return object.safeExtend({ args });
340
+ }
341
+ function adaptInputSchema(schema, adapter, zod, environmentPrefix, settings) {
342
+ const object = objectSchema(schema);
343
+ const shape = object.shape;
344
+ const registered = Object.fromEntries(Object.entries(shape).flatMap(([key, field]) => {
345
+ const configuration = registeredFieldConfiguration(field, zod);
346
+ return configuration ? [[key, configuration]] : [];
347
+ }));
348
+ const configurations = optionConfigurations(shape, environmentPrefix, zod, settings, registered);
349
+ const fields = Object.fromEntries(Object.entries(shape).map(([key, field]) => {
350
+ if (registered[key]?.kind === 'argument')
351
+ return [key, field];
352
+ return [key, adapter.option(configurations[key]).schema(field)];
353
+ }));
354
+ const bindings = Object.entries(configurations).flatMap(([key, configuration]) => {
355
+ if (configuration.env === undefined ||
356
+ configuration.parse !== undefined ||
357
+ structuredValueKind(shape[key], zod) !== 'array')
358
+ return [];
359
+ return [{
360
+ env: configuration.env,
361
+ name: configuration.name ?? toCliKey(key),
362
+ short: configuration.short,
363
+ }];
364
+ });
365
+ return {
366
+ adapted: object.safeExtend(fields),
367
+ bindings,
368
+ };
369
+ }
370
+ function objectSchema(schema) {
371
+ let candidate = schema;
372
+ while (candidate && typeof candidate === 'object') {
373
+ if ('shape' in candidate) {
374
+ const shape = candidate.shape;
375
+ if (shape && typeof shape === 'object')
376
+ return candidate;
377
+ }
378
+ if (!('unwrap' in candidate) || typeof candidate.unwrap !== 'function')
379
+ break;
380
+ candidate = candidate.unwrap();
381
+ }
382
+ throw new TypeError('createOptionKit requires a Zod object schema, optionally wrapped as readonly.');
383
+ }
384
+ function resolveEnvironmentPrefix(configured, projectName) {
385
+ if (configured === false)
386
+ return false;
387
+ const translated = toEnvironmentKey(configured ?? projectName);
388
+ if (!translated)
389
+ throw new TypeError('The environment prefix must contain a letter or number.');
390
+ return `${translated}_`;
391
+ }
392
+ function toCliKey(value) {
393
+ return value
394
+ .replace(/([a-z0-9])([A-Z])/gu, '$1-$2')
395
+ .replace(/([A-Z])([A-Z][a-z])/gu, '$1-$2')
396
+ .toLowerCase();
397
+ }
398
+ function optionConfigurations(shape, environmentPrefix, zod, settings, registered = {}) {
399
+ const keys = Object.keys(shape).filter(key => registered[key]?.kind !== 'argument');
400
+ const overrides = Object.fromEntries(keys.map(key => {
401
+ const existing = registered[key];
402
+ const existingOption = existing?.kind === 'option'
403
+ ? Object.fromEntries(Object.entries(existing).filter(([name]) => name !== 'kind'))
404
+ : {};
405
+ return [key, { ...settings.fields?.[key], ...existingOption }];
406
+ }));
407
+ const shorts = generatedShortFlags(keys, overrides, settings);
408
+ return Object.fromEntries(keys.map(key => {
409
+ const override = overrides?.[key];
410
+ const generated = {};
411
+ if (environmentPrefix !== false && override?.env !== false) {
412
+ generated.env = override?.env ?? `${environmentPrefix}${toEnvironmentKey(key)}`;
413
+ const kind = structuredValueKind(shape[key], zod);
414
+ if (kind === 'object' && !override?.parse)
415
+ generated.parse = structuredValueParser(kind);
416
+ }
417
+ if (override?.short !== false && (override?.short ?? shorts[key])) {
418
+ generated.short = typeof override?.short === 'string'
419
+ ? normalizeShort(override.short, key)
420
+ : shorts[key];
421
+ }
422
+ if (override) {
423
+ const { env: _env, short: _short, parse, ...rest } = override;
424
+ Object.assign(generated, rest);
425
+ if (parse)
426
+ generated.parse = parse;
427
+ }
428
+ return [key, generated];
429
+ }));
430
+ }
431
+ function registeredFieldConfiguration(schema, zod, seen = new Set()) {
432
+ if (seen.has(schema))
433
+ return undefined;
434
+ seen.add(schema);
435
+ const metadata = zod.globalRegistry.get(schema);
436
+ const registered = metadata?.fireargs;
437
+ if (registered && typeof registered === 'object' && 'kind' in registered) {
438
+ const kind = registered.kind;
439
+ if (kind === 'argument' || kind === 'option') {
440
+ return registered;
441
+ }
442
+ }
443
+ if (schema instanceof zod.ZodPipe) {
444
+ return registeredFieldConfiguration(schema.in, zod, seen);
445
+ }
446
+ const wrapper = schema;
447
+ return typeof wrapper.unwrap === 'function'
448
+ ? registeredFieldConfiguration(wrapper.unwrap(), zod, seen)
449
+ : undefined;
450
+ }
451
+ /**
452
+ * Fireargs handles structured argv values, but an environment variable reaches
453
+ * Commander as one string. Detect only unambiguous JSON containers so scalar
454
+ * coercion and Fireargs' native parsing remain untouched.
455
+ */
456
+ function structuredValueKind(schema, zod, seen = new Set()) {
457
+ if (!schema || seen.has(schema))
458
+ return undefined;
459
+ seen.add(schema);
460
+ if (schema instanceof zod.ZodArray || schema instanceof zod.ZodTuple)
461
+ return 'array';
462
+ if (schema instanceof zod.ZodObject || schema instanceof zod.ZodRecord)
463
+ return 'object';
464
+ if (schema instanceof zod.ZodUnion) {
465
+ const kinds = schema.options.map(option => (structuredValueKind(option, zod, new Set(seen)))).filter((kind) => kind !== undefined);
466
+ const unique = new Set(kinds);
467
+ return unique.size === 1 ? kinds[0] : undefined;
468
+ }
469
+ if (schema instanceof zod.ZodPipe) {
470
+ return structuredValueKind(schema.in, zod, seen);
471
+ }
472
+ const wrapper = schema;
473
+ return typeof wrapper.unwrap === 'function'
474
+ ? structuredValueKind(wrapper.unwrap(), zod, seen)
475
+ : undefined;
476
+ }
477
+ function structuredValueParser(kind) {
478
+ return ((value) => decodeStructuredValue(value, kind));
479
+ }
480
+ function decodeStructuredValue(value, kind) {
481
+ try {
482
+ return JSON.parse(value);
483
+ }
484
+ catch {
485
+ // A non-JSON string may still be useful to an array schema or a union.
486
+ }
487
+ return kind === 'array' ? splitArrayValue(value) ?? value : value;
488
+ }
489
+ function wrapStructuredEnvironment(route, bindings) {
490
+ const parse = route.parseAsync.bind(route);
491
+ route.parseAsync = async (argv, options) => {
492
+ const active = bindings(argv, options);
493
+ if (!active.length)
494
+ return parse(argv, options);
495
+ const effectiveArgv = [...(argv ?? process.argv)];
496
+ const userOffset = argvOffset(options);
497
+ const userArguments = effectiveArgv.slice(userOffset);
498
+ const insertion = effectiveArgv.indexOf('--', userOffset);
499
+ const injected = [];
500
+ const previous = new Map();
501
+ for (const binding of active) {
502
+ const value = process.env[binding.env];
503
+ if (value === undefined)
504
+ continue;
505
+ previous.set(binding.env, value);
506
+ delete process.env[binding.env];
507
+ if (hasOption(userArguments, binding))
508
+ continue;
509
+ const decoded = decodeStructuredValue(value, 'array');
510
+ const values = Array.isArray(decoded) ? decoded : [decoded];
511
+ for (const item of values)
512
+ injected.push(`--${binding.name}=${stringifyCliValue(item)}`);
513
+ }
514
+ effectiveArgv.splice(insertion === -1 ? effectiveArgv.length : insertion, 0, ...injected);
515
+ try {
516
+ return await parse(effectiveArgv, options);
517
+ }
518
+ finally {
519
+ for (const [name, value] of previous)
520
+ process.env[name] = value;
521
+ }
522
+ };
523
+ }
524
+ function selectedLeaf(program, argv, options) {
525
+ const args = (argv ?? process.argv).slice(argvOffset(options));
526
+ let current = program;
527
+ let offset = 0;
528
+ while (current.commands?.length) {
529
+ let selected;
530
+ let selectedAt = -1;
531
+ for (let index = offset; index < args.length; index += 1) {
532
+ const token = args[index];
533
+ if (!token || token.startsWith('-'))
534
+ continue;
535
+ selected = current.commands.find(command => (command.name?.() === token || command.aliases?.().includes(token)));
536
+ if (selected) {
537
+ selectedAt = index;
538
+ break;
539
+ }
540
+ }
541
+ if (!selected)
542
+ return undefined;
543
+ current = selected;
544
+ offset = selectedAt + 1;
545
+ }
546
+ return current;
547
+ }
548
+ function argvOffset(options) {
549
+ if (options?.from === 'user')
550
+ return 0;
551
+ if (options?.from === 'electron')
552
+ return 1;
553
+ return 2;
554
+ }
555
+ function hasOption(args, binding) {
556
+ const long = `--${binding.name}`;
557
+ const short = binding.short ? `-${binding.short}` : undefined;
558
+ return args.some(argument => (argument === long ||
559
+ argument.startsWith(`${long}=`) ||
560
+ (short !== undefined && (argument === short || argument.startsWith(`${short}=`)))));
561
+ }
562
+ function stringifyCliValue(value) {
563
+ if (typeof value === 'string')
564
+ return value;
565
+ if (value === null || typeof value !== 'object')
566
+ return String(value);
567
+ return JSON.stringify(value);
568
+ }
569
+ /** Split comma- or whitespace-delimited shell words without interpreting operators. */
570
+ function splitArrayValue(value) {
571
+ const result = [];
572
+ let token = '';
573
+ let quote;
574
+ let escaped = false;
575
+ let started = false;
576
+ for (const character of value) {
577
+ if (escaped) {
578
+ token += character;
579
+ escaped = false;
580
+ started = true;
581
+ }
582
+ else if (character === '\\') {
583
+ escaped = true;
584
+ started = true;
585
+ }
586
+ else if (quote) {
587
+ if (character === quote)
588
+ quote = undefined;
589
+ else
590
+ token += character;
591
+ started = true;
592
+ }
593
+ else if (character === '"' || character === "'") {
594
+ quote = character;
595
+ started = true;
596
+ }
597
+ else if (character === ',' || /\s/u.test(character)) {
598
+ if (started)
599
+ result.push(token);
600
+ token = '';
601
+ started = false;
602
+ }
603
+ else {
604
+ token += character;
605
+ started = true;
606
+ }
607
+ }
608
+ if (quote || escaped)
609
+ return undefined;
610
+ if (started)
611
+ result.push(token);
612
+ return result;
613
+ }
614
+ function generatedShortFlags(keys, overrides, settings) {
615
+ if (settings.shortFlags === false)
616
+ return {};
617
+ const reserved = new Set(['h', ...(settings.reservedShortFlags ?? [])].map(value => value.toLowerCase()));
618
+ const explicit = new Map();
619
+ for (const key of keys) {
620
+ const value = overrides?.[key]?.short;
621
+ if (typeof value !== 'string')
622
+ continue;
623
+ const normalized = normalizeShort(value, key);
624
+ const owner = explicit.get(normalized);
625
+ if (owner)
626
+ throw new TypeError(`Short flag -${normalized} is configured for both ${owner} and ${key}.`);
627
+ if (reserved.has(normalized))
628
+ throw new TypeError(`Short flag -${normalized} is reserved.`);
629
+ explicit.set(normalized, key);
630
+ }
631
+ const candidates = new Map();
632
+ for (const key of keys) {
633
+ if (overrides?.[key]?.short !== undefined)
634
+ continue;
635
+ const longName = overrides?.[key]?.name ?? key;
636
+ const candidate = toEnvironmentKey(longName).charAt(0).toLowerCase();
637
+ if (!candidate || reserved.has(candidate) || explicit.has(candidate))
638
+ continue;
639
+ candidates.set(candidate, [...(candidates.get(candidate) ?? []), key]);
640
+ }
641
+ const result = {};
642
+ for (const [candidate, owners] of candidates) {
643
+ if (owners.length === 1)
644
+ result[owners[0]] = candidate;
645
+ }
646
+ return result;
647
+ }
648
+ function normalizeShort(value, key) {
649
+ const normalized = value.replace(/^-+/u, '').toLowerCase();
650
+ if (!/^[a-z0-9]$/u.test(normalized)) {
651
+ throw new TypeError(`Short flag for ${key} must be one letter or digit.`);
652
+ }
653
+ return normalized;
654
+ }
655
+ function toEnvironmentKey(value) {
656
+ return value
657
+ .replace(/([a-z0-9])([A-Z])/gu, '$1_$2')
658
+ .replace(/^@/u, '')
659
+ .replace(/[^a-zA-Z0-9]+/gu, '_')
660
+ .replace(/^_+|_+$/gu, '')
661
+ .toUpperCase();
662
+ }
663
+ //# sourceMappingURL=optionkit.js.map