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.
Files changed (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +3 -2
  3. package/dist/{args-Cnq0nwSM.mjs → args-DrCXxXeP.mjs} +20 -4
  4. package/dist/args-DrCXxXeP.mjs.map +1 -0
  5. package/dist/codegen/index.mjs +1 -1
  6. package/dist/{commands-B_gufyR9.mjs → commands-DLR0rFgq.mjs} +2 -2
  7. package/dist/{commands-B_gufyR9.mjs.map → commands-DLR0rFgq.mjs.map} +1 -1
  8. package/dist/{completion-BEuflbDO.mjs → completion-UnBKfGuk.mjs} +2 -2
  9. package/dist/{completion-BEuflbDO.mjs.map → completion-UnBKfGuk.mjs.map} +1 -1
  10. package/dist/docs/index.d.mts +1 -1
  11. package/dist/docs/index.mjs +2 -2
  12. package/dist/{formatter-DrvhDMrq.d.mts → formatter-CY3KrOEd.d.mts} +3 -2
  13. package/dist/formatter-CY3KrOEd.d.mts.map +1 -0
  14. package/dist/{help-BtxLgrF_.mjs → help-B-ZMYyn-.mjs} +16 -6
  15. package/dist/help-B-ZMYyn-.mjs.map +1 -0
  16. package/dist/{index-C0Tab27T.d.mts → index-Guyz-CBm.d.mts} +340 -116
  17. package/dist/index-Guyz-CBm.d.mts.map +1 -0
  18. package/dist/index.d.mts +17 -160
  19. package/dist/index.d.mts.map +1 -1
  20. package/dist/index.mjs +227 -102
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{mcp-6-Jw4Bpq.mjs → mcp-D6PdtjIs.mjs} +4 -4
  23. package/dist/{mcp-6-Jw4Bpq.mjs.map → mcp-D6PdtjIs.mjs.map} +1 -1
  24. package/dist/{serve-YVTPzBCl.mjs → serve-PaCLsNoD.mjs} +4 -4
  25. package/dist/{serve-YVTPzBCl.mjs.map → serve-PaCLsNoD.mjs.map} +1 -1
  26. package/dist/test.d.mts +1 -1
  27. package/dist/zod.d.mts +1 -1
  28. package/package.json +3 -3
  29. package/src/core/args.ts +24 -1
  30. package/src/core/create.ts +21 -14
  31. package/src/core/exec.ts +87 -46
  32. package/src/core/interceptors.ts +107 -7
  33. package/src/core/program-methods.ts +12 -2
  34. package/src/core/validate.ts +26 -7
  35. package/src/extension/auto-output.ts +1 -1
  36. package/src/extension/config.ts +2 -1
  37. package/src/extension/env.ts +5 -4
  38. package/src/extension/index.ts +1 -0
  39. package/src/extension/interactive.ts +2 -1
  40. package/src/extension/logger.ts +1 -1
  41. package/src/extension/progress-renderer.ts +3 -0
  42. package/src/extension/progress.ts +37 -3
  43. package/src/extension/tracing.ts +1 -1
  44. package/src/index.ts +6 -1
  45. package/src/output/formatter.ts +6 -1
  46. package/src/output/help.ts +15 -3
  47. package/src/types/args-meta.ts +10 -0
  48. package/src/types/builder.ts +75 -2
  49. package/src/types/index.ts +3 -0
  50. package/src/types/interceptor.ts +26 -12
  51. package/src/util/type-utils.ts +22 -0
  52. package/dist/args-Cnq0nwSM.mjs.map +0 -1
  53. package/dist/formatter-DrvhDMrq.d.mts.map +0 -1
  54. package/dist/help-BtxLgrF_.mjs.map +0 -1
  55. 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: Validate ───────────────────────────────────────────
182
- const validateCtx: InterceptorValidateContext = {
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 coreValidate = (validateCtx: InterceptorValidateContext): InterceptorValidateResult | Promise<InterceptorValidateResult> => {
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
- // ── Phase 3: Execute (or handle validation errors) ──────────────
200
- const continueAfterValidate = (v: InterceptorValidateResult) => {
201
- pipelineState.args = v.args;
202
- if (v.argsResult?.issues) return handleValidationIssues(v.argsResult as StandardSchemaV1.FailureResult, command, errorMode);
203
-
204
- const executeCtx: InterceptorExecuteContext = {
205
- ...validateCtx,
206
- args: v.args,
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
- const coreExecute = (executeCtx: InterceptorExecuteContext): InterceptorExecuteResult => {
210
- const handler = command.action ?? noop;
211
- const effectiveRuntime = executeCtx.runtime;
212
- const actionCtx: PadroneActionContext = {
213
- runtime: effectiveRuntime,
214
- command: executeCtx.command,
215
- program: ctx.builder as any,
216
- signal: executeCtx.signal,
217
- context: executeCtx.context,
218
- caller,
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
- const executedOrPromise = runInterceptorChain('execute', commandInterceptors, executeCtx, coreExecute);
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
- return thenMaybe(executedOrPromise, (e) => {
227
- const finalize = (result: unknown) =>
228
- withDrain({
229
- command: command as any,
227
+ const executeCtx: InterceptorExecuteContext = {
228
+ ...validateCtx,
230
229
  args: v.args,
231
- argsResult: v.argsResult,
232
- result,
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
- if (e.result instanceof Promise) return e.result.then(finalize);
236
- return finalize(e.result);
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(warnIfUnexpectedAsync(validatedOrPromise, command), continueAfterValidate) as any;
281
+ return thenMaybe(routedOrPromise, continueAfterRoute) as any;
241
282
  };
242
283
 
243
284
  return thenMaybe(parsedOrPromise, continueAfterParse) as any;
@@ -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
- prevNext(overrides ? (Object.assign({}, currentCtx, overrides) as TCtx) : currentCtx),
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',
@@ -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: { ...(ctx.context as any), output: indicator } });
91
+ const executedOrPromise = next({ context: { output: indicator } });
92
92
  if (executedOrPromise instanceof Promise) return executedOrPromise.then(handleResult);
93
93
  return handleResult(executedOrPromise);
94
94
  },
@@ -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;
@@ -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;
@@ -1,3 +1,4 @@
1
+ export type { WithAsync } from '../util/type-utils.ts';
1
2
  export type { PadroneAutoOutputOptions } from './auto-output.ts';
2
3
  export { padroneAutoOutput } from './auto-output.ts';
3
4
  export { padroneColor } from './color.ts';
@@ -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);
@@ -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: { ...ctx.context, logger } });
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
- return next();
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(ctx, next) {
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: { ...(ctx.context as any), progress: effectiveIndicator } });
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 }>();
@@ -109,7 +109,7 @@ function tracingInterceptor(config: ResolvedTracingConfig) {
109
109
  },
110
110
  };
111
111
 
112
- return next({ context: { ...(ctx.context as any), tracing: padroneTracer } });
112
+ return next({ context: { tracing: padroneTracer } });
113
113
  },
114
114
 
115
115
  error(ctx, next) {