padrone 1.7.1 → 1.8.1
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 +26 -0
- package/README.md +3 -2
- package/dist/{args-Cnq0nwSM.mjs → args-DrCXxXeP.mjs} +20 -4
- package/dist/args-DrCXxXeP.mjs.map +1 -0
- package/dist/codegen/index.mjs +1 -1
- package/dist/{commands-B_gufyR9.mjs → commands-DLR0rFgq.mjs} +2 -2
- package/dist/{commands-B_gufyR9.mjs.map → commands-DLR0rFgq.mjs.map} +1 -1
- package/dist/{completion-BEuflbDO.mjs → completion-UnBKfGuk.mjs} +2 -2
- package/dist/{completion-BEuflbDO.mjs.map → completion-UnBKfGuk.mjs.map} +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{formatter-DrvhDMrq.d.mts → formatter-CY3KrOEd.d.mts} +3 -2
- package/dist/formatter-CY3KrOEd.d.mts.map +1 -0
- package/dist/{help-BtxLgrF_.mjs → help-B-ZMYyn-.mjs} +16 -6
- package/dist/help-B-ZMYyn-.mjs.map +1 -0
- package/dist/{index-C0Tab27T.d.mts → index-Guyz-CBm.d.mts} +340 -116
- package/dist/index-Guyz-CBm.d.mts.map +1 -0
- package/dist/index.d.mts +17 -160
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +227 -102
- package/dist/index.mjs.map +1 -1
- package/dist/{mcp-6-Jw4Bpq.mjs → mcp-D6PdtjIs.mjs} +4 -4
- package/dist/{mcp-6-Jw4Bpq.mjs.map → mcp-D6PdtjIs.mjs.map} +1 -1
- package/dist/{serve-YVTPzBCl.mjs → serve-PaCLsNoD.mjs} +4 -4
- package/dist/{serve-YVTPzBCl.mjs.map → serve-PaCLsNoD.mjs.map} +1 -1
- package/dist/test.d.mts +1 -1
- package/dist/zod.d.mts +1 -1
- package/package.json +3 -3
- package/src/core/args.ts +24 -1
- package/src/core/create.ts +21 -14
- package/src/core/exec.ts +87 -46
- package/src/core/interceptors.ts +107 -7
- package/src/core/program-methods.ts +12 -2
- package/src/core/validate.ts +26 -7
- package/src/extension/auto-output.ts +1 -1
- package/src/extension/config.ts +2 -1
- package/src/extension/env.ts +5 -4
- package/src/extension/index.ts +1 -0
- package/src/extension/interactive.ts +2 -1
- package/src/extension/logger.ts +1 -1
- package/src/extension/progress-renderer.ts +3 -0
- package/src/extension/progress.ts +37 -3
- package/src/extension/tracing.ts +1 -1
- package/src/index.ts +6 -1
- package/src/output/formatter.ts +6 -1
- package/src/output/help.ts +15 -3
- package/src/types/args-meta.ts +10 -0
- package/src/types/builder.ts +75 -2
- package/src/types/index.ts +3 -0
- package/src/types/interceptor.ts +26 -12
- package/src/util/type-utils.ts +22 -0
- package/dist/args-Cnq0nwSM.mjs.map +0 -1
- package/dist/formatter-DrvhDMrq.d.mts.map +0 -1
- package/dist/help-BtxLgrF_.mjs.map +0 -1
- package/dist/index-C0Tab27T.d.mts.map +0 -1
package/src/core/exec.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
InterceptorExecuteResult,
|
|
7
7
|
InterceptorParseContext,
|
|
8
8
|
InterceptorParseResult,
|
|
9
|
+
InterceptorRouteContext,
|
|
9
10
|
InterceptorValidateContext,
|
|
10
11
|
InterceptorValidateResult,
|
|
11
12
|
PadroneActionContext,
|
|
@@ -15,7 +16,7 @@ import type {
|
|
|
15
16
|
} from '../types/index.ts';
|
|
16
17
|
import { getCommandRuntime } from './commands.ts';
|
|
17
18
|
import { RoutingError, SignalError, ValidationError } from './errors.ts';
|
|
18
|
-
import { resolveRegisteredInterceptors, runInterceptorChain, wrapWithLifecycle } from './interceptors.ts';
|
|
19
|
+
import { resolveRegisteredInterceptors, runInterceptorChain, wrapWithCommandLifecycle, wrapWithLifecycle } from './interceptors.ts';
|
|
19
20
|
import { errorResult, noop, thenMaybe, warnIfUnexpectedAsync, withDrain } from './results.ts';
|
|
20
21
|
import { buildCommandArgs, formatIssueMessages, validateCommandArgs } from './validate.ts';
|
|
21
22
|
|
|
@@ -152,6 +153,7 @@ export function execCommand(
|
|
|
152
153
|
const factoryCache = new Map<RegisteredInterceptor, ResolvedInterceptor>();
|
|
153
154
|
const rootRegistered = rootCommand.interceptors ?? [];
|
|
154
155
|
const rootInterceptors = resolveRegisteredInterceptors(rootRegistered, factoryCache);
|
|
156
|
+
const rootInterceptorSet = new Set(rootInterceptors);
|
|
155
157
|
|
|
156
158
|
const runPipeline = (signal: AbortSignal, pipelineContext: unknown) => {
|
|
157
159
|
// ── Phase 1: Parse ──────────────────────────────────────────────────
|
|
@@ -159,7 +161,7 @@ export function execCommand(
|
|
|
159
161
|
input: resolvedInput,
|
|
160
162
|
command: rootCommand,
|
|
161
163
|
signal,
|
|
162
|
-
context: pipelineContext,
|
|
164
|
+
context: pipelineContext as object,
|
|
163
165
|
runtime,
|
|
164
166
|
program: ctx.builder,
|
|
165
167
|
caller,
|
|
@@ -176,68 +178,107 @@ export function execCommand(
|
|
|
176
178
|
pipelineState.rawArgs = parsed.rawArgs;
|
|
177
179
|
pipelineState.positionalArgs = parsed.positionalArgs;
|
|
178
180
|
const commandInterceptors = resolveRegisteredInterceptors(collectInterceptorsFn(command), factoryCache);
|
|
181
|
+
const commandOnlyInterceptors = commandInterceptors.filter((i) => !rootInterceptorSet.has(i));
|
|
179
182
|
const context = resolveContext(command, pipelineContext);
|
|
180
183
|
|
|
181
|
-
// ── Phase 2:
|
|
182
|
-
const
|
|
184
|
+
// ── Phase 2: Route ──────────────────────────────────────────────
|
|
185
|
+
const routeCtx: InterceptorRouteContext = {
|
|
183
186
|
...parseCtx,
|
|
184
187
|
command,
|
|
185
188
|
rawArgs: parsed.rawArgs,
|
|
186
189
|
positionalArgs: parsed.positionalArgs,
|
|
187
|
-
context,
|
|
188
|
-
evalInteractive: evalOptions?.interactive,
|
|
190
|
+
context: context as object,
|
|
189
191
|
};
|
|
190
192
|
|
|
191
|
-
const
|
|
192
|
-
const preprocessedArgs = buildCommandArgs(validateCtx.command, validateCtx.rawArgs, validateCtx.positionalArgs);
|
|
193
|
-
const validated = validateCommandArgs(validateCtx.command, preprocessedArgs);
|
|
194
|
-
return thenMaybe(validated, (v) => v as InterceptorValidateResult);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const validatedOrPromise = runInterceptorChain('validate', commandInterceptors, validateCtx, coreValidate);
|
|
193
|
+
const routedOrPromise = runInterceptorChain('route', commandInterceptors, routeCtx, () => {});
|
|
198
194
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
195
|
+
const continueAfterRoute = () => {
|
|
196
|
+
const runValidateAndExecute = () => {
|
|
197
|
+
// ── Phase 3: Validate ───────────────────────────────────────────
|
|
198
|
+
const validateCtx: InterceptorValidateContext = {
|
|
199
|
+
...parseCtx,
|
|
200
|
+
command,
|
|
201
|
+
rawArgs: parsed.rawArgs,
|
|
202
|
+
positionalArgs: parsed.positionalArgs,
|
|
203
|
+
context: context as object,
|
|
204
|
+
evalInteractive: evalOptions?.interactive,
|
|
205
|
+
};
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
207
|
+
const coreValidate = (
|
|
208
|
+
validateCtx: InterceptorValidateContext,
|
|
209
|
+
): InterceptorValidateResult | Promise<InterceptorValidateResult> => {
|
|
210
|
+
const { args: preprocessedArgs, issues } = buildCommandArgs(
|
|
211
|
+
validateCtx.command,
|
|
212
|
+
validateCtx.rawArgs,
|
|
213
|
+
validateCtx.positionalArgs,
|
|
214
|
+
);
|
|
215
|
+
if (issues) return { args: undefined, argsResult: { issues } as any };
|
|
216
|
+
const validated = validateCommandArgs(validateCtx.command, preprocessedArgs);
|
|
217
|
+
return thenMaybe(validated, (v) => v as InterceptorValidateResult);
|
|
219
218
|
};
|
|
220
|
-
const result = handler(executeCtx.args as any, actionCtx);
|
|
221
|
-
return { result };
|
|
222
|
-
};
|
|
223
219
|
|
|
224
|
-
|
|
220
|
+
const validatedOrPromise = runInterceptorChain('validate', commandInterceptors, validateCtx, coreValidate);
|
|
221
|
+
|
|
222
|
+
// ── Phase 3: Execute (or handle validation errors) ──────────────
|
|
223
|
+
const continueAfterValidate = (v: InterceptorValidateResult) => {
|
|
224
|
+
pipelineState.args = v.args;
|
|
225
|
+
if (v.argsResult?.issues) return handleValidationIssues(v.argsResult as StandardSchemaV1.FailureResult, command, errorMode);
|
|
225
226
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
withDrain({
|
|
229
|
-
command: command as any,
|
|
227
|
+
const executeCtx: InterceptorExecuteContext = {
|
|
228
|
+
...validateCtx,
|
|
230
229
|
args: v.args,
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const coreExecute = (executeCtx: InterceptorExecuteContext): InterceptorExecuteResult => {
|
|
233
|
+
const handler = command.action ?? noop;
|
|
234
|
+
const effectiveRuntime = executeCtx.runtime;
|
|
235
|
+
const actionCtx: PadroneActionContext = {
|
|
236
|
+
runtime: effectiveRuntime,
|
|
237
|
+
command: executeCtx.command,
|
|
238
|
+
program: ctx.builder as any,
|
|
239
|
+
signal: executeCtx.signal,
|
|
240
|
+
context: executeCtx.context,
|
|
241
|
+
caller,
|
|
242
|
+
};
|
|
243
|
+
const result = handler(executeCtx.args as any, actionCtx);
|
|
244
|
+
return { result };
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const executedOrPromise = runInterceptorChain('execute', commandInterceptors, executeCtx, coreExecute);
|
|
248
|
+
|
|
249
|
+
return thenMaybe(executedOrPromise, (e) => {
|
|
250
|
+
const finalize = (result: unknown) =>
|
|
251
|
+
withDrain({
|
|
252
|
+
command: command as any,
|
|
253
|
+
args: v.args,
|
|
254
|
+
argsResult: v.argsResult,
|
|
255
|
+
result,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (e.result instanceof Promise) return e.result.then(finalize);
|
|
259
|
+
return finalize(e.result);
|
|
233
260
|
});
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return thenMaybe(warnIfUnexpectedAsync(validatedOrPromise, command), continueAfterValidate) as any;
|
|
264
|
+
};
|
|
234
265
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
266
|
+
return wrapWithCommandLifecycle(
|
|
267
|
+
commandOnlyInterceptors,
|
|
268
|
+
command,
|
|
269
|
+
resolvedInput,
|
|
270
|
+
runValidateAndExecute,
|
|
271
|
+
(result) => withDrain({ command: command as any, args: undefined, argsResult: undefined, result }),
|
|
272
|
+
signal,
|
|
273
|
+
context,
|
|
274
|
+
runtime,
|
|
275
|
+
ctx.builder,
|
|
276
|
+
caller,
|
|
277
|
+
pipelineState,
|
|
278
|
+
);
|
|
238
279
|
};
|
|
239
280
|
|
|
240
|
-
return thenMaybe(
|
|
281
|
+
return thenMaybe(routedOrPromise, continueAfterRoute) as any;
|
|
241
282
|
};
|
|
242
283
|
|
|
243
284
|
return thenMaybe(parsedOrPromise, continueAfterParse) as any;
|
package/src/core/interceptors.ts
CHANGED
|
@@ -151,7 +151,7 @@ function deduplicateInterceptors(interceptors: ResolvedInterceptor[]): ResolvedI
|
|
|
151
151
|
* into the context before passing to the next interceptor or core function.
|
|
152
152
|
*/
|
|
153
153
|
export function runInterceptorChain<TCtx extends object, TResult>(
|
|
154
|
-
phase: 'start' | 'parse' | 'validate' | 'execute' | 'error' | 'shutdown',
|
|
154
|
+
phase: 'start' | 'parse' | 'route' | 'validate' | 'execute' | 'error' | 'shutdown',
|
|
155
155
|
interceptors: ResolvedInterceptor[],
|
|
156
156
|
ctx: TCtx,
|
|
157
157
|
core: (ctx: TCtx) => TResult | Promise<TResult>,
|
|
@@ -173,9 +173,15 @@ export function runInterceptorChain<TCtx extends object, TResult>(
|
|
|
173
173
|
) => TResult | Promise<TResult>;
|
|
174
174
|
const prevNext = next;
|
|
175
175
|
next = (currentCtx: TCtx) =>
|
|
176
|
-
handler(currentCtx, (overrides?: Record<string, unknown>) =>
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
handler(currentCtx, (overrides?: Record<string, unknown>) => {
|
|
177
|
+
if (!overrides) return prevNext(currentCtx);
|
|
178
|
+
// Auto-merge context: `next({ context: { user } })` merges into existing context
|
|
179
|
+
// instead of replacing it, so interceptors can't accidentally drop context.
|
|
180
|
+
if (overrides.context != null && typeof overrides.context === 'object') {
|
|
181
|
+
overrides = { ...overrides, context: Object.assign({}, (currentCtx as Record<string, unknown>).context, overrides.context) };
|
|
182
|
+
}
|
|
183
|
+
return prevNext(Object.assign({}, currentCtx, overrides) as TCtx);
|
|
184
|
+
});
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
return next(ctx);
|
|
@@ -220,7 +226,7 @@ export function wrapWithLifecycle<T>(
|
|
|
220
226
|
error,
|
|
221
227
|
result,
|
|
222
228
|
signal: effectiveSignal,
|
|
223
|
-
context: effectiveContext,
|
|
229
|
+
context: effectiveContext as object,
|
|
224
230
|
runtime: runtime!,
|
|
225
231
|
program: program!,
|
|
226
232
|
caller,
|
|
@@ -243,7 +249,7 @@ export function wrapWithLifecycle<T>(
|
|
|
243
249
|
input,
|
|
244
250
|
error,
|
|
245
251
|
signal: effectiveSignal,
|
|
246
|
-
context: effectiveContext,
|
|
252
|
+
context: effectiveContext as object,
|
|
247
253
|
runtime: runtime!,
|
|
248
254
|
program: program!,
|
|
249
255
|
caller,
|
|
@@ -272,7 +278,7 @@ export function wrapWithLifecycle<T>(
|
|
|
272
278
|
const startCtx: InterceptorStartContext = {
|
|
273
279
|
command,
|
|
274
280
|
signal: effectiveSignal,
|
|
275
|
-
context: effectiveContext,
|
|
281
|
+
context: effectiveContext as object,
|
|
276
282
|
runtime: runtime!,
|
|
277
283
|
program: program!,
|
|
278
284
|
input,
|
|
@@ -300,3 +306,97 @@ export function wrapWithLifecycle<T>(
|
|
|
300
306
|
|
|
301
307
|
return handleSuccess(result);
|
|
302
308
|
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Wraps a command-level pipeline (validate + execute) with error → shutdown lifecycle hooks.
|
|
312
|
+
* Unlike `wrapWithLifecycle`, this has no `start` phase and uses the resolved command context.
|
|
313
|
+
* Only interceptors exclusive to the command chain (not in root) should be passed here.
|
|
314
|
+
*/
|
|
315
|
+
export function wrapWithCommandLifecycle<T>(
|
|
316
|
+
interceptors: ResolvedInterceptor[],
|
|
317
|
+
command: AnyPadroneCommand,
|
|
318
|
+
input: string | undefined,
|
|
319
|
+
pipeline: () => T | Promise<T>,
|
|
320
|
+
wrapErrorResult: ((result: unknown) => T) | undefined,
|
|
321
|
+
signal: AbortSignal,
|
|
322
|
+
context: unknown,
|
|
323
|
+
runtime: ResolvedPadroneRuntime,
|
|
324
|
+
program: AnyPadroneProgram,
|
|
325
|
+
caller: 'cli' | 'eval' | 'run' | 'repl' | 'serve' | 'mcp' | 'tool',
|
|
326
|
+
pipelineState: { rawArgs?: Record<string, unknown>; positionalArgs?: string[]; args?: unknown },
|
|
327
|
+
): T | Promise<T> {
|
|
328
|
+
const hasError = interceptors.some((p) => p.error);
|
|
329
|
+
const hasShutdown = interceptors.some((p) => p.shutdown);
|
|
330
|
+
|
|
331
|
+
if (!hasError && !hasShutdown) return pipeline();
|
|
332
|
+
|
|
333
|
+
const runShutdown = (error?: unknown, result?: unknown) => {
|
|
334
|
+
if (!hasShutdown) return;
|
|
335
|
+
const ctx: InterceptorShutdownContext = {
|
|
336
|
+
command,
|
|
337
|
+
input,
|
|
338
|
+
error,
|
|
339
|
+
result,
|
|
340
|
+
signal,
|
|
341
|
+
context: context as object,
|
|
342
|
+
runtime,
|
|
343
|
+
program,
|
|
344
|
+
caller,
|
|
345
|
+
...pipelineState,
|
|
346
|
+
};
|
|
347
|
+
return runInterceptorChain('shutdown', interceptors, ctx, () => {});
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const runError = (error: unknown): T | Promise<T> => {
|
|
351
|
+
if (!hasError) {
|
|
352
|
+
const s = runShutdown(error);
|
|
353
|
+
if (s instanceof Promise)
|
|
354
|
+
return s.then(() => {
|
|
355
|
+
throw error;
|
|
356
|
+
});
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
const ctx: InterceptorErrorContext = {
|
|
360
|
+
command,
|
|
361
|
+
input,
|
|
362
|
+
error,
|
|
363
|
+
signal,
|
|
364
|
+
context: context as object,
|
|
365
|
+
runtime,
|
|
366
|
+
program,
|
|
367
|
+
caller,
|
|
368
|
+
...pipelineState,
|
|
369
|
+
};
|
|
370
|
+
const errorResult = runInterceptorChain('error', interceptors, ctx, (): InterceptorErrorResult => ({ error }));
|
|
371
|
+
return thenMaybe(errorResult, (er) => {
|
|
372
|
+
if (er.error !== undefined) {
|
|
373
|
+
const s = runShutdown(er.error);
|
|
374
|
+
return thenMaybe(s as void | Promise<void>, () => {
|
|
375
|
+
throw er.error;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
const wrapped = wrapErrorResult ? wrapErrorResult(er.result) : (er.result as T);
|
|
379
|
+
const s = runShutdown(undefined, wrapped);
|
|
380
|
+
return thenMaybe(s as void | Promise<void>, () => wrapped);
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const handleSuccess = (result: T): T | Promise<T> => {
|
|
385
|
+
const s = runShutdown(undefined, result);
|
|
386
|
+
if (s instanceof Promise) return s.then(() => result);
|
|
387
|
+
return result;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
let result: T | Promise<T>;
|
|
391
|
+
try {
|
|
392
|
+
result = pipeline();
|
|
393
|
+
} catch (e) {
|
|
394
|
+
return runError(e);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (result instanceof Promise) {
|
|
398
|
+
return result.then(handleSuccess, runError);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return handleSuccess(result);
|
|
402
|
+
}
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
PadroneAPI,
|
|
12
12
|
PadroneReplPreferences,
|
|
13
13
|
} from '../types/index.ts';
|
|
14
|
-
import { parsePositionalConfig } from './args.ts';
|
|
14
|
+
import { extractSchemaMetadata, parsePositionalConfig } from './args.ts';
|
|
15
15
|
import { findCommandByName, getCommandRuntime, resolveAllCommands } from './commands.ts';
|
|
16
16
|
import { RoutingError } from './errors.ts';
|
|
17
17
|
import type { ExecContext } from './exec.ts';
|
|
@@ -37,6 +37,15 @@ export function createProgramMethods(ctx: ExecContext, evalCommand: AnyPadronePr
|
|
|
37
37
|
const positionalConfig = commandObj.meta?.positional ? parsePositionalConfig(commandObj.meta.positional) : [];
|
|
38
38
|
const positionalNames = new Set(positionalConfig.map((p) => p.name));
|
|
39
39
|
|
|
40
|
+
// Build reverse map: arg name → first negative keyword (for stringify)
|
|
41
|
+
const negativeKeyword: Record<string, string> = {};
|
|
42
|
+
if (commandObj.argsSchema) {
|
|
43
|
+
const { negatives } = extractSchemaMetadata(commandObj.argsSchema, commandObj.meta?.fields, commandObj.meta?.autoAlias);
|
|
44
|
+
for (const [keyword, argName] of Object.entries(negatives)) {
|
|
45
|
+
if (!(argName in negativeKeyword)) negativeKeyword[argName] = keyword;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
if (args && typeof args === 'object') {
|
|
41
50
|
for (const { name, variadic } of positionalConfig) {
|
|
42
51
|
const value = (args as Record<string, unknown>)[name];
|
|
@@ -60,6 +69,7 @@ export function createProgramMethods(ctx: ExecContext, evalCommand: AnyPadronePr
|
|
|
60
69
|
|
|
61
70
|
if (typeof value === 'boolean') {
|
|
62
71
|
if (value) parts.push(`--${key}`);
|
|
72
|
+
else if (negativeKeyword[key]) parts.push(`--${negativeKeyword[key]}`);
|
|
63
73
|
else parts.push(`--no-${key}`);
|
|
64
74
|
} else if (Array.isArray(value)) {
|
|
65
75
|
for (const v of value) {
|
|
@@ -117,7 +127,7 @@ export function createProgramMethods(ctx: ExecContext, evalCommand: AnyPadronePr
|
|
|
117
127
|
positionalArgs: [],
|
|
118
128
|
args,
|
|
119
129
|
signal: inertSignal,
|
|
120
|
-
context: resolvedCtx,
|
|
130
|
+
context: resolvedCtx as object,
|
|
121
131
|
runtime: commandRuntime,
|
|
122
132
|
program: ctx.builder as any,
|
|
123
133
|
caller: 'run',
|
package/src/core/validate.ts
CHANGED
|
@@ -51,8 +51,8 @@ export function parseCommand(input: string | undefined, rootCommand: AnyPadroneC
|
|
|
51
51
|
const argsMeta = curCommand.meta?.fields;
|
|
52
52
|
const schemaMetadata = curCommand.argsSchema
|
|
53
53
|
? extractSchemaMetadata(curCommand.argsSchema, argsMeta, curCommand.meta?.autoAlias)
|
|
54
|
-
: { flags: {}, aliases: {} };
|
|
55
|
-
const { flags, aliases } = schemaMetadata;
|
|
54
|
+
: { flags: {}, aliases: {}, negatives: {}, customNegation: new Set<string>() };
|
|
55
|
+
const { flags, aliases, negatives, customNegation } = schemaMetadata;
|
|
56
56
|
|
|
57
57
|
const arrayArguments = new Set<string>();
|
|
58
58
|
if (curCommand.argsSchema) {
|
|
@@ -77,6 +77,10 @@ export function parseCommand(input: string | undefined, rootCommand: AnyPadroneC
|
|
|
77
77
|
key = [flags[arg.key[0]!]!];
|
|
78
78
|
} else if (arg.type === 'named' && arg.key.length === 1 && aliases[arg.key[0]!]) {
|
|
79
79
|
key = [aliases[arg.key[0]!]!];
|
|
80
|
+
} else if (arg.type === 'named' && arg.key.length === 1 && negatives[arg.key[0]!]) {
|
|
81
|
+
// Negative keyword: --remote sets local to false
|
|
82
|
+
setNestedValue(rawArgs, [negatives[arg.key[0]!]!], false);
|
|
83
|
+
continue;
|
|
80
84
|
} else {
|
|
81
85
|
key = arg.key;
|
|
82
86
|
}
|
|
@@ -84,6 +88,12 @@ export function parseCommand(input: string | undefined, rootCommand: AnyPadroneC
|
|
|
84
88
|
const rootKey = key[0]!;
|
|
85
89
|
|
|
86
90
|
if (arg.type === 'named' && arg.negated) {
|
|
91
|
+
// Skip --no- prefix negation for args with custom negation
|
|
92
|
+
if (customNegation.has(rootKey)) {
|
|
93
|
+
// Treat as unknown: put it back as `no-<key>` so detectUnknownArgs catches it
|
|
94
|
+
setNestedValue(rawArgs, [`no-${key.join('.')}`], false);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
87
97
|
setNestedValue(rawArgs, key, false);
|
|
88
98
|
continue;
|
|
89
99
|
}
|
|
@@ -132,8 +142,9 @@ export function buildCommandArgs(
|
|
|
132
142
|
command: AnyPadroneCommand,
|
|
133
143
|
rawArgs: Record<string, unknown>,
|
|
134
144
|
positionalArgs: string[],
|
|
135
|
-
): Record<string, unknown
|
|
145
|
+
): { args: Record<string, unknown>; issues?: StandardSchemaV1.Issue[] } {
|
|
136
146
|
let preprocessedArgs = preprocessArgs(rawArgs, { flags: {}, aliases: {} });
|
|
147
|
+
let issues: StandardSchemaV1.Issue[] | undefined;
|
|
137
148
|
|
|
138
149
|
const positionalConfig = command.meta?.positional ? parsePositionalConfig(command.meta.positional) : [];
|
|
139
150
|
|
|
@@ -143,6 +154,13 @@ export function buildCommandArgs(
|
|
|
143
154
|
const { name, variadic } = positionalConfig[i]!;
|
|
144
155
|
if (argIndex >= positionalArgs.length) break;
|
|
145
156
|
|
|
157
|
+
// Detect ambiguity: same arg provided both positionally and as a named option
|
|
158
|
+
if (name in preprocessedArgs) {
|
|
159
|
+
issues ??= [];
|
|
160
|
+
issues.push({ path: [name], message: `Ambiguous argument "${name}": provided both positionally and as a named option` });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
146
164
|
if (variadic) {
|
|
147
165
|
const remainingPositionals = positionalConfig.slice(i + 1);
|
|
148
166
|
const nonVariadicAfter = remainingPositionals.filter((p) => !p.variadic).length;
|
|
@@ -163,7 +181,7 @@ export function buildCommandArgs(
|
|
|
163
181
|
preprocessedArgs = coerceArgs(preprocessedArgs, command.argsSchema);
|
|
164
182
|
}
|
|
165
183
|
|
|
166
|
-
return preprocessedArgs;
|
|
184
|
+
return { args: preprocessedArgs, issues };
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
/**
|
|
@@ -180,9 +198,9 @@ export function checkUnknownArgs(command: AnyPadroneCommand, preprocessedArgs: R
|
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
const argsMeta = command.meta?.fields;
|
|
183
|
-
const { flags, aliases } = extractSchemaMetadata(command.argsSchema, argsMeta, command.meta?.autoAlias);
|
|
201
|
+
const { flags, aliases, negatives } = extractSchemaMetadata(command.argsSchema, argsMeta, command.meta?.autoAlias);
|
|
184
202
|
|
|
185
|
-
return detectUnknownArgs(preprocessedArgs, command.argsSchema, flags, aliases);
|
|
203
|
+
return detectUnknownArgs(preprocessedArgs, command.argsSchema, flags, aliases, negatives);
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
/**
|
|
@@ -241,7 +259,8 @@ export function coreValidateForParse(
|
|
|
241
259
|
rawArgs: Record<string, unknown>,
|
|
242
260
|
positionalArgs: string[],
|
|
243
261
|
): InterceptorValidateResult | Promise<InterceptorValidateResult> {
|
|
244
|
-
const preprocessedArgs = buildCommandArgs(command, rawArgs, positionalArgs);
|
|
262
|
+
const { args: preprocessedArgs, issues } = buildCommandArgs(command, rawArgs, positionalArgs);
|
|
263
|
+
if (issues) return { args: undefined, argsResult: { issues } as any };
|
|
245
264
|
const validated = validateCommandArgs(command, preprocessedArgs);
|
|
246
265
|
return thenMaybe(validated, (v) => v as InterceptorValidateResult);
|
|
247
266
|
}
|
|
@@ -88,7 +88,7 @@ function createAutoOutputInterceptor(outputConfig?: OutputConfig) {
|
|
|
88
88
|
return { result: collected };
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
-
const executedOrPromise = next({ context: {
|
|
91
|
+
const executedOrPromise = next({ context: { output: indicator } });
|
|
92
92
|
if (executedOrPromise instanceof Promise) return executedOrPromise.then(handleResult);
|
|
93
93
|
return handleResult(executedOrPromise);
|
|
94
94
|
},
|
package/src/extension/config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { ConfigError } from '../core/errors.ts';
|
|
|
4
4
|
import { defineInterceptor } from '../core/interceptors.ts';
|
|
5
5
|
import { thenMaybe } from '../core/results.ts';
|
|
6
6
|
import type { AnyPadroneBuilder, CommandTypesBase, InterceptorValidateContext } from '../types/index.ts';
|
|
7
|
+
import type { WithAsync } from '../util/type-utils.ts';
|
|
7
8
|
import { getRootCommand } from '../util/utils.ts';
|
|
8
9
|
|
|
9
10
|
// ── Types ────────────────────────────────────────────────────────────────
|
|
@@ -189,7 +190,7 @@ function loadConfig(
|
|
|
189
190
|
* }))
|
|
190
191
|
* ```
|
|
191
192
|
*/
|
|
192
|
-
export function padroneConfig(options?: PadroneConfigOptions): <T extends CommandTypesBase>(builder: T) => T {
|
|
193
|
+
export function padroneConfig(options?: PadroneConfigOptions): <T extends CommandTypesBase>(builder: T) => WithAsync<T> {
|
|
193
194
|
if (options?.disabled) {
|
|
194
195
|
const disabled = defineInterceptor({ id: 'padrone:config', name: 'padrone:config', order: -999, disabled: true }, () => ({}));
|
|
195
196
|
return ((builder: AnyPadroneBuilder) => builder.intercept(disabled)) as any;
|
package/src/extension/env.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { thenMaybe } from '../core/results.ts';
|
|
|
5
5
|
import type { AnyPadroneBuilder, CommandTypesBase, InterceptorValidateContext } from '../types/index.ts';
|
|
6
6
|
import type { LoadEnvFilesOptions } from '../util/dotenv.ts';
|
|
7
7
|
import { loadEnvFiles } from '../util/dotenv.ts';
|
|
8
|
+
import type { WithAsync } from '../util/type-utils.ts';
|
|
8
9
|
|
|
9
10
|
// ── Types ────────────────────────────────────────────────────────────────
|
|
10
11
|
|
|
@@ -53,13 +54,13 @@ function isSchema(value: unknown): value is StandardSchemaV1 {
|
|
|
53
54
|
*
|
|
54
55
|
* Env values have lower precedence than CLI args and stdin, but higher than config files.
|
|
55
56
|
*/
|
|
56
|
-
export function padroneEnv(schema: StandardSchemaV1): <T extends CommandTypesBase>(builder: T) => T
|
|
57
|
-
export function padroneEnv(schema: StandardSchemaV1, options: PadroneEnvOptions): <T extends CommandTypesBase>(builder: T) => T
|
|
58
|
-
export function padroneEnv(options: PadroneEnvOptions): <T extends CommandTypesBase>(builder: T) => T
|
|
57
|
+
export function padroneEnv(schema: StandardSchemaV1): <T extends CommandTypesBase>(builder: T) => WithAsync<T>;
|
|
58
|
+
export function padroneEnv(schema: StandardSchemaV1, options: PadroneEnvOptions): <T extends CommandTypesBase>(builder: T) => WithAsync<T>;
|
|
59
|
+
export function padroneEnv(options: PadroneEnvOptions): <T extends CommandTypesBase>(builder: T) => WithAsync<T>;
|
|
59
60
|
export function padroneEnv(
|
|
60
61
|
schemaOrOptions: StandardSchemaV1 | PadroneEnvOptions,
|
|
61
62
|
maybeOptions?: PadroneEnvOptions,
|
|
62
|
-
): <T extends CommandTypesBase>(builder: T) => T {
|
|
63
|
+
): <T extends CommandTypesBase>(builder: T) => WithAsync<T> {
|
|
63
64
|
const schema = isSchema(schemaOrOptions) ? schemaOrOptions : undefined;
|
|
64
65
|
const options = isSchema(schemaOrOptions) ? maybeOptions : schemaOrOptions;
|
|
65
66
|
const hasFiles = options?.modes !== undefined;
|
package/src/extension/index.ts
CHANGED
|
@@ -37,7 +37,8 @@ const interactiveInterceptor = defineInterceptor({ id: 'padrone:interactive', na
|
|
|
37
37
|
if (!willPrompt) return next();
|
|
38
38
|
|
|
39
39
|
// Preprocess args to determine what's missing
|
|
40
|
-
const preprocessedArgs = buildCommandArgs(command, ctx.rawArgs, ctx.positionalArgs);
|
|
40
|
+
const { args: preprocessedArgs, issues: positionalIssues } = buildCommandArgs(command, ctx.rawArgs, ctx.positionalArgs);
|
|
41
|
+
if (positionalIssues) return { args: undefined, argsResult: { issues: positionalIssues } } as any;
|
|
41
42
|
|
|
42
43
|
// Check for unknown args before prompting
|
|
43
44
|
const unknowns = checkUnknownArgs(command, preprocessedArgs);
|
package/src/extension/logger.ts
CHANGED
|
@@ -209,7 +209,7 @@ function loggerInterceptor(rawConfig?: PadroneLoggerConfig) {
|
|
|
209
209
|
timestamps: rawConfig?.timestamps ?? ctxCfg?.timestamps ?? false,
|
|
210
210
|
};
|
|
211
211
|
const logger = createLogger(ctx.runtime, resolved.level, resolved, ctx.context?.tracing);
|
|
212
|
-
return next({ context: {
|
|
212
|
+
return next({ context: { logger } });
|
|
213
213
|
},
|
|
214
214
|
};
|
|
215
215
|
});
|
|
@@ -265,6 +265,9 @@ export function createTerminalProgress(message: string, options?: PadroneProgres
|
|
|
265
265
|
}, tickInterval)
|
|
266
266
|
: undefined;
|
|
267
267
|
|
|
268
|
+
// Prevent the spinner timer from keeping the process alive on uncaught errors
|
|
269
|
+
if (timer && typeof timer === 'object' && 'unref' in timer) (timer as NodeJS.Timeout).unref();
|
|
270
|
+
|
|
268
271
|
render();
|
|
269
272
|
|
|
270
273
|
const clear = () => {
|
|
@@ -198,10 +198,35 @@ function progressInterceptor(config: string | PadroneProgressConfig) {
|
|
|
198
198
|
ctx.runtime.error = originalError;
|
|
199
199
|
};
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
const onValidationFailure = (error: unknown) => {
|
|
202
|
+
if (indicator) {
|
|
203
|
+
cleanup(indicator, msgs!.success, msgs!.error, error, undefined, true);
|
|
204
|
+
teardown();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const checkResult = (result: any) => {
|
|
209
|
+
if (result.argsResult?.issues) onValidationFailure(new Error('Validation failed'));
|
|
210
|
+
return result;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
let result: any;
|
|
214
|
+
try {
|
|
215
|
+
result = next();
|
|
216
|
+
} catch (err) {
|
|
217
|
+
onValidationFailure(err);
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
if (result instanceof Promise) {
|
|
221
|
+
return result.then(checkResult, (err: unknown) => {
|
|
222
|
+
onValidationFailure(err);
|
|
223
|
+
throw err;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return checkResult(result);
|
|
202
227
|
},
|
|
203
228
|
|
|
204
|
-
execute(
|
|
229
|
+
execute(_ctx, next) {
|
|
205
230
|
// Transition from validation message to progress message
|
|
206
231
|
if (indicator && msgs!.validation) indicator.update(msgs!.progress);
|
|
207
232
|
|
|
@@ -221,7 +246,7 @@ function progressInterceptor(config: string | PadroneProgressConfig) {
|
|
|
221
246
|
|
|
222
247
|
let result: any;
|
|
223
248
|
try {
|
|
224
|
-
result = next({ context: {
|
|
249
|
+
result = next({ context: { progress: effectiveIndicator } });
|
|
225
250
|
} catch (err) {
|
|
226
251
|
onError(err);
|
|
227
252
|
}
|
|
@@ -259,6 +284,15 @@ function progressInterceptor(config: string | PadroneProgressConfig) {
|
|
|
259
284
|
onSuccess(result!.result);
|
|
260
285
|
return result;
|
|
261
286
|
},
|
|
287
|
+
|
|
288
|
+
shutdown(ctx) {
|
|
289
|
+
// Safety net: if validate/execute cleanup paths were bypassed (e.g., outer interceptor
|
|
290
|
+
// threw during execute before reaching this interceptor's execute handler), stop the indicator.
|
|
291
|
+
if (indicator) {
|
|
292
|
+
cleanup(indicator, msgs!.success, msgs!.error, ctx.error, ctx.result, !!ctx.error);
|
|
293
|
+
teardown();
|
|
294
|
+
}
|
|
295
|
+
},
|
|
262
296
|
};
|
|
263
297
|
})
|
|
264
298
|
.provides<{ progress: PadroneProgressIndicator }>();
|
package/src/extension/tracing.ts
CHANGED
|
@@ -109,7 +109,7 @@ function tracingInterceptor(config: ResolvedTracingConfig) {
|
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
return next({ context: {
|
|
112
|
+
return next({ context: { tracing: padroneTracer } });
|
|
113
113
|
},
|
|
114
114
|
|
|
115
115
|
error(ctx, next) {
|