padrone 1.4.0 → 1.6.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.
Files changed (141) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +108 -283
  3. package/dist/args-Cnq0nwSM.mjs +272 -0
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.d.mts +28 -3
  6. package/dist/codegen/index.d.mts.map +1 -1
  7. package/dist/codegen/index.mjs +169 -19
  8. package/dist/codegen/index.mjs.map +1 -1
  9. package/dist/commands-B_gufyR9.mjs +514 -0
  10. package/dist/commands-B_gufyR9.mjs.map +1 -0
  11. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
  12. package/dist/completion-BEuflbDO.mjs.map +1 -0
  13. package/dist/docs/index.d.mts +22 -2
  14. package/dist/docs/index.d.mts.map +1 -1
  15. package/dist/docs/index.mjs +92 -7
  16. package/dist/docs/index.mjs.map +1 -1
  17. package/dist/errors-CL63UOzt.mjs +137 -0
  18. package/dist/errors-CL63UOzt.mjs.map +1 -0
  19. package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
  20. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  21. package/dist/help-B5Kk83of.mjs +849 -0
  22. package/dist/help-B5Kk83of.mjs.map +1 -0
  23. package/dist/index-BaU3X6dY.d.mts +1178 -0
  24. package/dist/index-BaU3X6dY.d.mts.map +1 -0
  25. package/dist/index.d.mts +763 -36
  26. package/dist/index.d.mts.map +1 -1
  27. package/dist/index.mjs +3608 -1534
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mcp-BM-d0nZi.mjs +377 -0
  30. package/dist/mcp-BM-d0nZi.mjs.map +1 -0
  31. package/dist/serve-Bk0JUlCj.mjs +402 -0
  32. package/dist/serve-Bk0JUlCj.mjs.map +1 -0
  33. package/dist/stream-DC4H8YTx.mjs +77 -0
  34. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  35. package/dist/test.d.mts +5 -8
  36. package/dist/test.d.mts.map +1 -1
  37. package/dist/test.mjs +5 -27
  38. package/dist/test.mjs.map +1 -1
  39. package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  40. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  41. package/dist/zod.d.mts +32 -0
  42. package/dist/zod.d.mts.map +1 -0
  43. package/dist/zod.mjs +50 -0
  44. package/dist/zod.mjs.map +1 -0
  45. package/package.json +20 -9
  46. package/src/cli/completions.ts +14 -11
  47. package/src/cli/docs.ts +13 -16
  48. package/src/cli/doctor.ts +213 -24
  49. package/src/cli/index.ts +28 -82
  50. package/src/cli/init.ts +12 -10
  51. package/src/cli/link.ts +22 -18
  52. package/src/cli/wrap.ts +14 -11
  53. package/src/codegen/discovery.ts +80 -28
  54. package/src/codegen/index.ts +2 -1
  55. package/src/codegen/parsers/bash.ts +179 -0
  56. package/src/codegen/schema-to-code.ts +2 -1
  57. package/src/core/args.ts +296 -0
  58. package/src/core/commands.ts +373 -0
  59. package/src/core/create.ts +268 -0
  60. package/src/{runtime.ts → core/default-runtime.ts} +70 -135
  61. package/src/{errors.ts → core/errors.ts} +22 -0
  62. package/src/core/exec.ts +259 -0
  63. package/src/core/interceptors.ts +302 -0
  64. package/src/{parse.ts → core/parse.ts} +36 -89
  65. package/src/core/program-methods.ts +301 -0
  66. package/src/core/results.ts +229 -0
  67. package/src/core/runtime.ts +246 -0
  68. package/src/core/validate.ts +247 -0
  69. package/src/docs/index.ts +124 -11
  70. package/src/extension/auto-output.ts +95 -0
  71. package/src/extension/color.ts +38 -0
  72. package/src/extension/completion.ts +49 -0
  73. package/src/extension/config.ts +262 -0
  74. package/src/extension/env.ts +101 -0
  75. package/src/extension/help.ts +192 -0
  76. package/src/extension/index.ts +43 -0
  77. package/src/extension/ink.ts +93 -0
  78. package/src/extension/interactive.ts +106 -0
  79. package/src/extension/logger.ts +214 -0
  80. package/src/extension/man.ts +51 -0
  81. package/src/extension/mcp.ts +52 -0
  82. package/src/extension/progress-renderer.ts +338 -0
  83. package/src/extension/progress.ts +299 -0
  84. package/src/extension/repl.ts +94 -0
  85. package/src/extension/serve.ts +48 -0
  86. package/src/extension/signal.ts +87 -0
  87. package/src/extension/stdin.ts +62 -0
  88. package/src/extension/suggestions.ts +114 -0
  89. package/src/extension/timing.ts +81 -0
  90. package/src/extension/tracing.ts +175 -0
  91. package/src/extension/update-check.ts +77 -0
  92. package/src/extension/utils.ts +51 -0
  93. package/src/extension/version.ts +63 -0
  94. package/src/{completion.ts → feature/completion.ts} +130 -57
  95. package/src/{interactive.ts → feature/interactive.ts} +47 -6
  96. package/src/feature/mcp.ts +387 -0
  97. package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
  98. package/src/feature/serve.ts +438 -0
  99. package/src/feature/test.ts +262 -0
  100. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  101. package/src/{wrap.ts → feature/wrap.ts} +27 -27
  102. package/src/index.ts +120 -11
  103. package/src/output/colorizer.ts +154 -0
  104. package/src/{formatter.ts → output/formatter.ts} +281 -135
  105. package/src/{help.ts → output/help.ts} +62 -15
  106. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  107. package/src/schema/zod.ts +50 -0
  108. package/src/test.ts +2 -285
  109. package/src/types/args-meta.ts +151 -0
  110. package/src/types/builder.ts +697 -0
  111. package/src/types/command.ts +157 -0
  112. package/src/types/index.ts +59 -0
  113. package/src/types/interceptor.ts +296 -0
  114. package/src/types/preferences.ts +83 -0
  115. package/src/types/result.ts +71 -0
  116. package/src/types/schema.ts +19 -0
  117. package/src/util/dotenv.ts +244 -0
  118. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  119. package/src/util/stream.ts +101 -0
  120. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  121. package/src/{type-utils.ts → util/type-utils.ts} +99 -37
  122. package/src/util/utils.ts +51 -0
  123. package/src/zod.ts +1 -0
  124. package/dist/args-CVDbyyzG.mjs +0 -199
  125. package/dist/args-CVDbyyzG.mjs.map +0 -1
  126. package/dist/chunk-y_GBKt04.mjs +0 -5
  127. package/dist/completion.d.mts +0 -64
  128. package/dist/completion.d.mts.map +0 -1
  129. package/dist/completion.mjs.map +0 -1
  130. package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
  131. package/dist/help-CcBe91bV.mjs +0 -1254
  132. package/dist/help-CcBe91bV.mjs.map +0 -1
  133. package/dist/types-DjIdJN5G.d.mts +0 -1059
  134. package/dist/types-DjIdJN5G.d.mts.map +0 -1
  135. package/dist/update-check-EbNDkzyV.mjs.map +0 -1
  136. package/src/args.ts +0 -461
  137. package/src/colorizer.ts +0 -41
  138. package/src/command-utils.ts +0 -532
  139. package/src/create.ts +0 -1477
  140. package/src/types.ts +0 -1109
  141. package/src/utils.ts +0 -140
@@ -0,0 +1,302 @@
1
+ import type {
2
+ AnyPadroneCommand,
3
+ AnyPadroneProgram,
4
+ InterceptorDefBuilder,
5
+ InterceptorErrorContext,
6
+ InterceptorErrorResult,
7
+ InterceptorFactory,
8
+ InterceptorMeta,
9
+ InterceptorShutdownContext,
10
+ InterceptorStartContext,
11
+ PadroneInterceptorFn,
12
+ RegisteredInterceptor,
13
+ ResolvedInterceptor,
14
+ } from '../types/index.ts';
15
+ import { thenMaybe } from './results.ts';
16
+ import type { ResolvedPadroneRuntime } from './runtime.ts';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // defineInterceptor — creates a single-value distributable interceptor
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function buildInterceptorFn(meta: InterceptorMeta, factory: InterceptorFactory<any, any, any>): PadroneInterceptorFn<any, any, any> {
23
+ Object.defineProperty(factory, 'name', { value: meta.name, configurable: true });
24
+ if (meta.id !== undefined) (factory as any).id = meta.id;
25
+ if (meta.order !== undefined) (factory as any).order = meta.order;
26
+ if (meta.disabled !== undefined) (factory as any).disabled = meta.disabled;
27
+ if (meta.inherit !== undefined) (factory as any).inherit = meta.inherit;
28
+ (factory as any).provides = () => factory;
29
+ (factory as any).requires = () => factory;
30
+ return factory as PadroneInterceptorFn<any, any, any>;
31
+ }
32
+
33
+ /**
34
+ * Creates a self-contained interceptor value by attaching static metadata to the factory function.
35
+ * The returned value can be passed directly to `.intercept()` or exported from a package.
36
+ *
37
+ * Two-arg form — define metadata and factory in one call:
38
+ * ```ts
39
+ * export const myInterceptor = defineInterceptor(
40
+ * { name: 'my-interceptor', order: 10 },
41
+ * () => ({
42
+ * execute(ctx, next) { return next(); },
43
+ * }),
44
+ * );
45
+ * ```
46
+ *
47
+ * Single-arg form — chain `.requires<T>()` for typed context, then `.factory()`:
48
+ * ```ts
49
+ * export const myInterceptor = defineInterceptor({ name: 'with-db' })
50
+ * .requires<{ db: DB }>()
51
+ * .factory(() => ({
52
+ * execute(ctx, next) {
53
+ * ctx.context.db; // typed!
54
+ * return next();
55
+ * },
56
+ * }));
57
+ * ```
58
+ */
59
+ export function defineInterceptor<TArgs = unknown, TResult = unknown>(
60
+ meta: InterceptorMeta,
61
+ factory: InterceptorFactory<TArgs, TResult>,
62
+ ): PadroneInterceptorFn<TArgs, TResult>;
63
+ export function defineInterceptor(meta: InterceptorMeta): InterceptorDefBuilder;
64
+ export function defineInterceptor(
65
+ meta: InterceptorMeta,
66
+ factory?: InterceptorFactory<any, any, any>,
67
+ ): PadroneInterceptorFn<any, any, any> | InterceptorDefBuilder {
68
+ if (factory) return buildInterceptorFn(meta, factory);
69
+ const builder: InterceptorDefBuilder = {
70
+ requires: () => builder as any,
71
+ factory: (f) => buildInterceptorFn(meta, f) as any,
72
+ };
73
+ return builder;
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Registration normalization
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /**
81
+ * Normalizes an interceptor input (single-value form or two-arg form) into the internal
82
+ * `RegisteredInterceptor` storage format.
83
+ */
84
+ export function toRegisteredInterceptor(
85
+ metaOrFn: InterceptorMeta | PadroneInterceptorFn<any, any, any>,
86
+ factory?: InterceptorFactory<any, any, any>,
87
+ ): RegisteredInterceptor {
88
+ if (typeof metaOrFn === 'function') {
89
+ // Single-value form: PadroneInterceptorFn (factory with meta as own properties)
90
+ return {
91
+ meta: { name: metaOrFn.name, id: metaOrFn.id, order: metaOrFn.order, disabled: metaOrFn.disabled, inherit: metaOrFn.inherit },
92
+ factory: metaOrFn,
93
+ };
94
+ }
95
+ // Two-arg form: (meta, factory)
96
+ return { meta: metaOrFn, factory: factory! };
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Factory resolution
101
+ // ---------------------------------------------------------------------------
102
+
103
+ /**
104
+ * Resolves registered interceptors by calling their factories and merging the resulting
105
+ * phase handlers with the static metadata. Uses a cache to ensure each factory is called
106
+ * at most once per execution (so root interceptor closures are shared across all phases).
107
+ */
108
+ export function resolveRegisteredInterceptors(
109
+ registered: RegisteredInterceptor[],
110
+ cache: Map<RegisteredInterceptor, ResolvedInterceptor>,
111
+ ): ResolvedInterceptor[] {
112
+ return registered.map((reg) => {
113
+ let resolved = cache.get(reg);
114
+ if (!resolved) {
115
+ resolved = { ...reg.meta, ...reg.factory() };
116
+ cache.set(reg, resolved);
117
+ }
118
+ return resolved;
119
+ });
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Interceptor chain runner
124
+ // ---------------------------------------------------------------------------
125
+
126
+ /**
127
+ * Deduplicates interceptors by `id`. When multiple interceptors share the same `id`,
128
+ * only the last one in the array is kept. Interceptors without an `id` are always kept.
129
+ */
130
+ function deduplicateInterceptors(interceptors: ResolvedInterceptor[]): ResolvedInterceptor[] {
131
+ // Fast path: no ids at all
132
+ if (!interceptors.some((p) => p.id)) return interceptors;
133
+
134
+ // Find the last index for each id
135
+ const lastIndex = new Map<string, number>();
136
+ for (let i = 0; i < interceptors.length; i++) {
137
+ const id = interceptors[i]!.id;
138
+ if (id) lastIndex.set(id, i);
139
+ }
140
+
141
+ return interceptors.filter((p, i) => !p.id || lastIndex.get(p.id) === i);
142
+ }
143
+
144
+ /**
145
+ * Runs an interceptor chain for a given phase using the onion/middleware pattern.
146
+ * Interceptors are sorted by `order` (ascending, stable), then composed so that
147
+ * the first interceptor in sorted order is the outermost wrapper.
148
+ * If no interceptors handle this phase, `core` is called directly.
149
+ *
150
+ * Each interceptor's `next()` accepts optional partial overrides that are merged
151
+ * into the context before passing to the next interceptor or core function.
152
+ */
153
+ export function runInterceptorChain<TCtx extends object, TResult>(
154
+ phase: 'start' | 'parse' | 'validate' | 'execute' | 'error' | 'shutdown',
155
+ interceptors: ResolvedInterceptor[],
156
+ ctx: TCtx,
157
+ core: (ctx: TCtx) => TResult | Promise<TResult>,
158
+ ): TResult | Promise<TResult> {
159
+ // Deduplicate by id (last wins), then filter to enabled interceptors that have a handler for this phase
160
+ const deduped = deduplicateInterceptors(interceptors);
161
+ const phaseInterceptors = deduped.filter((p) => p[phase] && !p.disabled);
162
+ if (phaseInterceptors.length === 0) return core(ctx);
163
+
164
+ // Stable sort by order (lower = outermost). Equal order preserves registration order.
165
+ phaseInterceptors.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
166
+
167
+ // Build chain from inside out: last interceptor wraps core, first interceptor is outermost
168
+ let next: (currentCtx: TCtx) => TResult | Promise<TResult> = core;
169
+ for (let i = phaseInterceptors.length - 1; i >= 0; i--) {
170
+ const handler = phaseInterceptors[i]![phase]! as unknown as (
171
+ ctx: TCtx,
172
+ next: (overrides?: Record<string, unknown>) => TResult | Promise<TResult>,
173
+ ) => TResult | Promise<TResult>;
174
+ const prevNext = next;
175
+ next = (currentCtx: TCtx) =>
176
+ handler(currentCtx, (overrides?: Record<string, unknown>) =>
177
+ prevNext(overrides ? (Object.assign({}, currentCtx, overrides) as TCtx) : currentCtx),
178
+ );
179
+ }
180
+
181
+ return next(ctx);
182
+ }
183
+
184
+ /**
185
+ * Wraps a pipeline with start → error → shutdown lifecycle hooks.
186
+ * - `start` interceptors wrap the pipeline (onion pattern, root interceptors only).
187
+ * - On error: `error` interceptors run (can transform/suppress the error).
188
+ * - Always: `shutdown` interceptors run (success or failure).
189
+ */
190
+ export function wrapWithLifecycle<T>(
191
+ interceptors: ResolvedInterceptor[],
192
+ command: AnyPadroneCommand,
193
+ input: string | undefined,
194
+ pipeline: (signal: AbortSignal, context: unknown) => T | Promise<T>,
195
+ wrapErrorResult?: (result: unknown) => T,
196
+ signal?: AbortSignal,
197
+ context?: unknown,
198
+ runtime?: ResolvedPadroneRuntime,
199
+ program?: AnyPadroneProgram,
200
+ caller: 'cli' | 'eval' | 'run' | 'repl' | 'serve' | 'mcp' | 'tool' = 'eval',
201
+ pipelineState?: { rawArgs?: Record<string, unknown>; positionalArgs?: string[]; args?: unknown },
202
+ ): T | Promise<T> {
203
+ const defaultSignal = typeof AbortSignal !== 'undefined' ? AbortSignal.abort() : (undefined as unknown as AbortSignal);
204
+ const hasStart = interceptors.some((p) => p.start);
205
+ const hasError = interceptors.some((p) => p.error);
206
+ const hasShutdown = interceptors.some((p) => p.shutdown);
207
+
208
+ // Fast path: no lifecycle interceptors
209
+ if (!hasStart && !hasError && !hasShutdown) return pipeline(signal ?? defaultSignal, context);
210
+ // Mutable refs: start-phase interceptors can override signal and context (e.g., signal extension, auth),
211
+ // and the overrides propagate to error/shutdown contexts.
212
+ let effectiveSignal = signal ?? defaultSignal;
213
+ let effectiveContext = context;
214
+
215
+ const runShutdown = (error?: unknown, result?: unknown) => {
216
+ if (!hasShutdown) return;
217
+ const ctx: InterceptorShutdownContext = {
218
+ command,
219
+ input,
220
+ error,
221
+ result,
222
+ signal: effectiveSignal,
223
+ context: effectiveContext,
224
+ runtime: runtime!,
225
+ program: program!,
226
+ caller,
227
+ ...pipelineState,
228
+ };
229
+ return runInterceptorChain('shutdown', interceptors, ctx, () => {});
230
+ };
231
+
232
+ const runError = (error: unknown): T | Promise<T> => {
233
+ if (!hasError) {
234
+ const s = runShutdown(error);
235
+ if (s instanceof Promise)
236
+ return s.then(() => {
237
+ throw error;
238
+ });
239
+ throw error;
240
+ }
241
+ const ctx: InterceptorErrorContext = {
242
+ command,
243
+ input,
244
+ error,
245
+ signal: effectiveSignal,
246
+ context: effectiveContext,
247
+ runtime: runtime!,
248
+ program: program!,
249
+ caller,
250
+ ...pipelineState,
251
+ };
252
+ const errorResult = runInterceptorChain('error', interceptors, ctx, (): InterceptorErrorResult => ({ error }));
253
+ return thenMaybe(errorResult, (er) => {
254
+ if (er.error !== undefined) {
255
+ const s = runShutdown(er.error);
256
+ return thenMaybe(s as void | Promise<void>, () => {
257
+ throw er.error;
258
+ });
259
+ }
260
+ const wrapped = wrapErrorResult ? wrapErrorResult(er.result) : (er.result as T);
261
+ const s = runShutdown(undefined, wrapped);
262
+ return thenMaybe(s as void | Promise<void>, () => wrapped);
263
+ });
264
+ };
265
+
266
+ const handleSuccess = (result: T): T | Promise<T> => {
267
+ const s = runShutdown(undefined, result);
268
+ if (s instanceof Promise) return s.then(() => result);
269
+ return result;
270
+ };
271
+
272
+ const startCtx: InterceptorStartContext = {
273
+ command,
274
+ signal: effectiveSignal,
275
+ context: effectiveContext,
276
+ runtime: runtime!,
277
+ program: program!,
278
+ input,
279
+ caller,
280
+ };
281
+ let result: T | Promise<T>;
282
+ try {
283
+ result = (
284
+ hasStart
285
+ ? runInterceptorChain('start', interceptors, startCtx, (ctx) => {
286
+ // Capture overrides from start-phase interceptors so downstream phases see them.
287
+ effectiveSignal = ctx.signal;
288
+ effectiveContext = ctx.context;
289
+ return pipeline(ctx.signal, ctx.context);
290
+ })
291
+ : pipeline(effectiveSignal, effectiveContext)
292
+ ) as T | Promise<T>;
293
+ } catch (e) {
294
+ return runError(e);
295
+ }
296
+
297
+ if (result instanceof Promise) {
298
+ return result.then(handleSuccess, runError);
299
+ }
300
+
301
+ return handleSuccess(result);
302
+ }
@@ -44,14 +44,16 @@ type ParseParts = {
44
44
 
45
45
  type ParsePart = ParseParts[keyof ParseParts];
46
46
 
47
+ type QuoteChar = '"' | "'" | '`';
48
+
47
49
  /**
48
- * Tokenizes input string respecting quoted strings and bracket arrays.
49
- * Supports single quotes, double quotes, backticks, and square brackets.
50
+ * Split a string by a delimiter, respecting quoted segments and optional bracket nesting.
51
+ * Handles escape sequences within quotes (\\" and \\\\).
50
52
  */
51
- function tokenizeInput(input: string): string[] {
52
- const tokens: string[] = [];
53
+ function splitQuoteAware(input: string, delimiter: ' ' | ',', opts?: { brackets?: boolean; trim?: boolean }): string[] {
54
+ const results: string[] = [];
53
55
  let current = '';
54
- let inQuote: '"' | "'" | '`' | null = null;
56
+ let inQuote: QuoteChar | null = null;
55
57
  let bracketDepth = 0;
56
58
  let i = 0;
57
59
 
@@ -59,39 +61,32 @@ function tokenizeInput(input: string): string[] {
59
61
  const char = input[i];
60
62
 
61
63
  if (inQuote) {
62
- // Check for escape sequences within quotes
63
64
  if (char === '\\' && i + 1 < input.length) {
64
65
  const nextChar = input[i + 1];
65
- // Handle escape sequences
66
66
  if (nextChar === inQuote || nextChar === '\\') {
67
67
  current += nextChar;
68
68
  i += 2;
69
69
  continue;
70
70
  }
71
71
  }
72
-
73
72
  if (char === inQuote) {
74
- // End of quoted string
75
73
  inQuote = null;
76
74
  } else {
77
75
  current += char;
78
76
  }
79
- } else if (char === '[') {
77
+ } else if (opts?.brackets && char === '[') {
80
78
  bracketDepth++;
81
79
  current += char;
82
- } else if (char === ']') {
80
+ } else if (opts?.brackets && char === ']') {
83
81
  bracketDepth = Math.max(0, bracketDepth - 1);
84
82
  current += char;
85
83
  } else if (bracketDepth > 0) {
86
- // Inside brackets - include everything including spaces
87
84
  current += char;
88
85
  } else if (char === '"' || char === "'" || char === '`') {
89
- // Start of quoted string
90
86
  inQuote = char;
91
- } else if (char === ' ' || char === '\t') {
92
- // Whitespace outside quotes and brackets - end current token
93
- if (current) {
94
- tokens.push(current);
87
+ } else if (char === delimiter || (delimiter === ' ' && char === '\t')) {
88
+ if (delimiter === ' ' ? current : true) {
89
+ results.push(opts?.trim ? current.trim() : current);
95
90
  current = '';
96
91
  }
97
92
  } else {
@@ -100,19 +95,20 @@ function tokenizeInput(input: string): string[] {
100
95
  i++;
101
96
  }
102
97
 
103
- // Add the last token if any
104
- if (current) {
105
- tokens.push(current);
98
+ if (delimiter === ' ' ? current : current || results.length > 0) {
99
+ results.push(opts?.trim ? current.trim() : current);
106
100
  }
107
101
 
108
- return tokens;
102
+ return results;
109
103
  }
110
104
 
111
105
  export function parseCliInputToParts(input: string): ParsePart[] {
112
- const parts = tokenizeInput(input.trim());
106
+ const parts = splitQuoteAware(input.trim(), ' ', { brackets: true });
113
107
  const result: ParsePart[] = [];
114
108
 
115
- let pendingValue: ParseParts['named'] | ParseParts['alias'] | undefined;
109
+ // Index into `result` of the last part that can accept a pending value (-1 = none)
110
+ let pendingIdx = -1;
111
+ // Once a non-term positional arg appears, all subsequent bare values become args
116
112
  let allowTerm = true;
117
113
  let afterDoubleDash = false;
118
114
 
@@ -121,7 +117,7 @@ export function parseCliInputToParts(input: string): ParsePart[] {
121
117
 
122
118
  // Bare `--` separator: everything after is a literal positional arg
123
119
  if (part === '--' && !afterDoubleDash) {
124
- if (pendingValue) pendingValue = undefined;
120
+ pendingIdx = -1;
125
121
  afterDoubleDash = true;
126
122
  allowTerm = false;
127
123
  continue;
@@ -132,22 +128,18 @@ export function parseCliInputToParts(input: string): ParsePart[] {
132
128
  continue;
133
129
  }
134
130
 
135
- const wasPending = pendingValue;
136
- pendingValue = undefined;
131
+ const hadPending = pendingIdx;
132
+ pendingIdx = -1;
137
133
 
138
134
  if (part.startsWith('--no-') && part.length > 5) {
139
135
  // Negated boolean arg (--no-verbose or --no-config.debug)
140
- const keyStr = part.slice(5);
141
- const key = keyStr.split('.');
142
- const p = { type: 'named' as const, key, value: undefined, negated: true };
143
- result.push(p);
136
+ const key = part.slice(5).split('.');
137
+ result.push({ type: 'named', key, value: undefined, negated: true });
144
138
  } else if (part.startsWith('--')) {
145
139
  const [keyStr = '', value] = splitNamedArgValue(part.slice(2));
146
140
  const key = keyStr.split('.');
147
-
148
- const p = { type: 'named' as const, key, value };
149
- if (typeof value === 'undefined') pendingValue = p;
150
- result.push(p);
141
+ result.push({ type: 'named', key, value });
142
+ if (typeof value === 'undefined') pendingIdx = result.length - 1;
151
143
  } else if (part.startsWith('-') && part.length > 1 && !/^-\d/.test(part)) {
152
144
  // Short flag(s) (but not negative numbers like -5)
153
145
  // Supports flag stacking: -abc → -a -b -c (last flag can take a value)
@@ -156,25 +148,23 @@ export function parseCliInputToParts(input: string): ParsePart[] {
156
148
  if (keyStr.length > 1 && typeof value === 'undefined') {
157
149
  // Flag stacking: -abc → -a, -b, -c (all set to true except last which can take next arg's value)
158
150
  for (let ci = 0; ci < keyStr.length - 1; ci++) {
159
- result.push({ type: 'alias' as const, key: [keyStr[ci]!], value: undefined });
151
+ result.push({ type: 'alias', key: [keyStr[ci]!], value: undefined });
160
152
  }
161
- const lastFlag = { type: 'alias' as const, key: [keyStr[keyStr.length - 1]!], value: undefined as string | string[] | undefined };
162
- pendingValue = lastFlag;
163
- result.push(lastFlag);
153
+ result.push({ type: 'alias', key: [keyStr[keyStr.length - 1]!], value: undefined });
154
+ pendingIdx = result.length - 1;
164
155
  } else if (keyStr.length > 1 && typeof value !== 'undefined') {
165
156
  // -abc=val → -a, -b, -c=val (stacked with value on last)
166
157
  for (let ci = 0; ci < keyStr.length - 1; ci++) {
167
- result.push({ type: 'alias' as const, key: [keyStr[ci]!], value: undefined });
158
+ result.push({ type: 'alias', key: [keyStr[ci]!], value: undefined });
168
159
  }
169
- result.push({ type: 'alias' as const, key: [keyStr[keyStr.length - 1]!], value });
160
+ result.push({ type: 'alias', key: [keyStr[keyStr.length - 1]!], value });
170
161
  } else {
171
162
  // Single char: -v or -v=value
172
- const p = { type: 'alias' as const, key: [keyStr], value };
173
- if (typeof value === 'undefined') pendingValue = p;
174
- result.push(p);
163
+ result.push({ type: 'alias', key: [keyStr], value });
164
+ if (typeof value === 'undefined') pendingIdx = result.length - 1;
175
165
  }
176
- } else if (wasPending) {
177
- wasPending.value = part;
166
+ } else if (hadPending >= 0) {
167
+ result[hadPending]!.value = part;
178
168
  } else if (/^[a-zA-Z0-9_-]+$/.test(part) && allowTerm) {
179
169
  result.push({ type: 'term', value: part });
180
170
  } else {
@@ -209,8 +199,7 @@ function splitNamedArgValue(str: string): [string, string | string[] | undefined
209
199
  if (value.startsWith('[') && value.endsWith(']')) {
210
200
  const inner = value.slice(1, -1);
211
201
  if (inner === '') return [key, []];
212
- const items = parseArrayItems(inner);
213
- return [key, items];
202
+ return [key, splitQuoteAware(inner, ',', { trim: true })];
214
203
  }
215
204
 
216
205
  return [key, value];
@@ -252,45 +241,3 @@ export function getNestedValue(obj: Record<string, unknown>, path: string[]): un
252
241
 
253
242
  return current;
254
243
  }
255
-
256
- /**
257
- * Parse comma-separated items, respecting quotes within items.
258
- */
259
- function parseArrayItems(input: string): string[] {
260
- const items: string[] = [];
261
- let current = '';
262
- let inQuote: '"' | "'" | '`' | null = null;
263
- let i = 0;
264
-
265
- while (i < input.length) {
266
- const char = input[i];
267
-
268
- if (inQuote) {
269
- if (char === '\\' && i + 1 < input.length && input[i + 1] === inQuote) {
270
- current += input[i + 1];
271
- i += 2;
272
- continue;
273
- }
274
- if (char === inQuote) {
275
- inQuote = null;
276
- } else {
277
- current += char;
278
- }
279
- } else if (char === '"' || char === "'" || char === '`') {
280
- inQuote = char;
281
- } else if (char === ',') {
282
- items.push(current.trim());
283
- current = '';
284
- } else {
285
- current += char;
286
- }
287
- i++;
288
- }
289
-
290
- // Add the last item
291
- if (current || items.length > 0) {
292
- items.push(current.trim());
293
- }
294
-
295
- return items;
296
- }