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.
- package/CHANGELOG.md +115 -0
- package/README.md +108 -283
- package/dist/args-Cnq0nwSM.mjs +272 -0
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.d.mts +28 -3
- package/dist/codegen/index.d.mts.map +1 -1
- package/dist/codegen/index.mjs +169 -19
- package/dist/codegen/index.mjs.map +1 -1
- package/dist/commands-B_gufyR9.mjs +514 -0
- package/dist/commands-B_gufyR9.mjs.map +1 -0
- package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +22 -2
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +92 -7
- package/dist/docs/index.mjs.map +1 -1
- package/dist/errors-CL63UOzt.mjs +137 -0
- package/dist/errors-CL63UOzt.mjs.map +1 -0
- package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/help-B5Kk83of.mjs +849 -0
- package/dist/help-B5Kk83of.mjs.map +1 -0
- package/dist/index-BaU3X6dY.d.mts +1178 -0
- package/dist/index-BaU3X6dY.d.mts.map +1 -0
- package/dist/index.d.mts +763 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3608 -1534
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-BM-d0nZi.mjs +377 -0
- package/dist/mcp-BM-d0nZi.mjs.map +1 -0
- package/dist/serve-Bk0JUlCj.mjs +402 -0
- package/dist/serve-Bk0JUlCj.mjs.map +1 -0
- package/dist/stream-DC4H8YTx.mjs +77 -0
- package/dist/stream-DC4H8YTx.mjs.map +1 -0
- package/dist/test.d.mts +5 -8
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +5 -27
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +32 -0
- package/dist/zod.d.mts.map +1 -0
- package/dist/zod.mjs +50 -0
- package/dist/zod.mjs.map +1 -0
- package/package.json +20 -9
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -16
- package/src/cli/doctor.ts +213 -24
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +12 -10
- package/src/cli/link.ts +22 -18
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/discovery.ts +80 -28
- package/src/codegen/index.ts +2 -1
- package/src/codegen/parsers/bash.ts +179 -0
- package/src/codegen/schema-to-code.ts +2 -1
- package/src/core/args.ts +296 -0
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +268 -0
- package/src/{runtime.ts → core/default-runtime.ts} +70 -135
- package/src/{errors.ts → core/errors.ts} +22 -0
- package/src/core/exec.ts +259 -0
- package/src/core/interceptors.ts +302 -0
- package/src/{parse.ts → core/parse.ts} +36 -89
- package/src/core/program-methods.ts +301 -0
- package/src/core/results.ts +229 -0
- package/src/core/runtime.ts +246 -0
- package/src/core/validate.ts +247 -0
- package/src/docs/index.ts +124 -11
- package/src/extension/auto-output.ts +95 -0
- package/src/extension/color.ts +38 -0
- package/src/extension/completion.ts +49 -0
- package/src/extension/config.ts +262 -0
- package/src/extension/env.ts +101 -0
- package/src/extension/help.ts +192 -0
- package/src/extension/index.ts +43 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +214 -0
- package/src/extension/man.ts +51 -0
- package/src/extension/mcp.ts +52 -0
- package/src/extension/progress-renderer.ts +338 -0
- package/src/extension/progress.ts +299 -0
- package/src/extension/repl.ts +94 -0
- package/src/extension/serve.ts +48 -0
- package/src/extension/signal.ts +87 -0
- package/src/extension/stdin.ts +62 -0
- package/src/extension/suggestions.ts +114 -0
- package/src/extension/timing.ts +81 -0
- package/src/extension/tracing.ts +175 -0
- package/src/extension/update-check.ts +77 -0
- package/src/extension/utils.ts +51 -0
- package/src/extension/version.ts +63 -0
- package/src/{completion.ts → feature/completion.ts} +130 -57
- package/src/{interactive.ts → feature/interactive.ts} +47 -6
- package/src/feature/mcp.ts +387 -0
- package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
- package/src/feature/serve.ts +438 -0
- package/src/feature/test.ts +262 -0
- package/src/{update-check.ts → feature/update-check.ts} +16 -16
- package/src/{wrap.ts → feature/wrap.ts} +27 -27
- package/src/index.ts +120 -11
- package/src/output/colorizer.ts +154 -0
- package/src/{formatter.ts → output/formatter.ts} +281 -135
- package/src/{help.ts → output/help.ts} +62 -15
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -285
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +697 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +59 -0
- package/src/types/interceptor.ts +296 -0
- package/src/types/preferences.ts +83 -0
- package/src/types/result.ts +71 -0
- package/src/types/schema.ts +19 -0
- package/src/util/dotenv.ts +244 -0
- package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
- package/src/util/stream.ts +101 -0
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +99 -37
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -0
- package/dist/args-CVDbyyzG.mjs +0 -199
- package/dist/args-CVDbyyzG.mjs.map +0 -1
- package/dist/chunk-y_GBKt04.mjs +0 -5
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
- package/dist/help-CcBe91bV.mjs +0 -1254
- package/dist/help-CcBe91bV.mjs.map +0 -1
- package/dist/types-DjIdJN5G.d.mts +0 -1059
- package/dist/types-DjIdJN5G.d.mts.map +0 -1
- package/dist/update-check-EbNDkzyV.mjs.map +0 -1
- package/src/args.ts +0 -461
- package/src/colorizer.ts +0 -41
- package/src/command-utils.ts +0 -532
- package/src/create.ts +0 -1477
- package/src/types.ts +0 -1109
- 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
|
-
*
|
|
49
|
-
*
|
|
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
|
|
52
|
-
const
|
|
53
|
+
function splitQuoteAware(input: string, delimiter: ' ' | ',', opts?: { brackets?: boolean; trim?: boolean }): string[] {
|
|
54
|
+
const results: string[] = [];
|
|
53
55
|
let current = '';
|
|
54
|
-
let inQuote:
|
|
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 === ' '
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
102
|
+
return results;
|
|
109
103
|
}
|
|
110
104
|
|
|
111
105
|
export function parseCliInputToParts(input: string): ParsePart[] {
|
|
112
|
-
const parts =
|
|
106
|
+
const parts = splitQuoteAware(input.trim(), ' ', { brackets: true });
|
|
113
107
|
const result: ParsePart[] = [];
|
|
114
108
|
|
|
115
|
-
|
|
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
|
-
|
|
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
|
|
136
|
-
|
|
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
|
|
141
|
-
|
|
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
|
-
|
|
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'
|
|
151
|
+
result.push({ type: 'alias', key: [keyStr[ci]!], value: undefined });
|
|
160
152
|
}
|
|
161
|
-
|
|
162
|
-
|
|
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'
|
|
158
|
+
result.push({ type: 'alias', key: [keyStr[ci]!], value: undefined });
|
|
168
159
|
}
|
|
169
|
-
result.push({ type: 'alias'
|
|
160
|
+
result.push({ type: 'alias', key: [keyStr[keyStr.length - 1]!], value });
|
|
170
161
|
} else {
|
|
171
162
|
// Single char: -v or -v=value
|
|
172
|
-
|
|
173
|
-
if (typeof value === 'undefined')
|
|
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 (
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
}
|