padrone 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +2 -1
  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-D6-7dz0l.d.mts → index-C2n3k4e8.d.mts} +332 -116
  17. package/dist/index-C2n3k4e8.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 +78 -65
  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 +4 -3
  32. package/src/core/interceptors.ts +12 -6
  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.ts +2 -2
  42. package/src/extension/tracing.ts +1 -1
  43. package/src/index.ts +5 -1
  44. package/src/output/formatter.ts +6 -1
  45. package/src/output/help.ts +15 -3
  46. package/src/types/args-meta.ts +10 -0
  47. package/src/types/builder.ts +140 -38
  48. package/src/types/index.ts +2 -0
  49. package/src/types/interceptor.ts +12 -12
  50. package/src/util/type-utils.ts +22 -0
  51. package/dist/args-Cnq0nwSM.mjs.map +0 -1
  52. package/dist/formatter-DrvhDMrq.d.mts.map +0 -1
  53. package/dist/help-BtxLgrF_.mjs.map +0 -1
  54. package/dist/index-D6-7dz0l.d.mts.map +0 -1
@@ -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
  });
@@ -201,7 +201,7 @@ function progressInterceptor(config: string | PadroneProgressConfig) {
201
201
  return next();
202
202
  },
203
203
 
204
- execute(ctx, next) {
204
+ execute(_ctx, next) {
205
205
  // Transition from validation message to progress message
206
206
  if (indicator && msgs!.validation) indicator.update(msgs!.progress);
207
207
 
@@ -221,7 +221,7 @@ function progressInterceptor(config: string | PadroneProgressConfig) {
221
221
 
222
222
  let result: any;
223
223
  try {
224
- result = next({ context: { ...(ctx.context as any), progress: effectiveIndicator } });
224
+ result = next({ context: { progress: effectiveIndicator } });
225
225
  } catch (err) {
226
226
  onError(err);
227
227
  }
@@ -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) {
package/src/index.ts CHANGED
@@ -38,6 +38,7 @@ export type {
38
38
  PadroneTracer,
39
39
  PadroneTracingConfig,
40
40
  VersionCommand,
41
+ WithAsync,
41
42
  WithCompletion,
42
43
  WithHelp,
43
44
  WithLogger,
@@ -79,7 +80,7 @@ export type { UpdateCheckConfig } from './feature/update-check.ts';
79
80
  export type { WrapConfig, WrapResult } from './feature/wrap.ts';
80
81
  export type { AnsiStyle, ColorConfig, ColorTheme } from './output/colorizer.ts';
81
82
  export { colorThemes } from './output/colorizer.ts';
82
- export type { HelpInfo } from './output/formatter.ts';
83
+ export type { HelpDetail, HelpFormat, HelpInfo } from './output/formatter.ts';
83
84
  export type { PadroneOutputIndicator } from './output/output-indicator.ts';
84
85
  export type { KeyValueOptions, ListItem, ListOptions, TableOptions, TreeNode, TreeOptions } from './output/primitives.ts';
85
86
  export type { OutputContext, OutputFormat } from './output/styling.ts';
@@ -90,6 +91,8 @@ export type {
90
91
  AsyncPadroneSchema,
91
92
  CommandTypesBase,
92
93
  DefineCommand,
94
+ DefineCommandBuilder,
95
+ DefineCommandContext,
93
96
  ExtractInterceptorContext,
94
97
  ExtractInterceptorRequires,
95
98
  GetArgsMeta,
@@ -121,6 +124,7 @@ export type {
121
124
  PadroneProgram,
122
125
  PadroneProgramMeta,
123
126
  PadroneSchema,
127
+ RegisteredInterceptor,
124
128
  } from './types/index.ts';
125
129
  export type { AsyncStreamMeta } from './util/stream.ts';
126
130
  export { asyncStream } from './util/stream.ts';
@@ -58,6 +58,8 @@ export type HelpArgumentInfo = {
58
58
  variadic?: boolean;
59
59
  /** Whether this arg is a boolean (shown as --[no-]arg) */
60
60
  negatable?: boolean;
61
+ /** Custom negative keyword(s) that set this arg to false (e.g. `['remote']` for `--remote`) */
62
+ negative?: string[];
61
63
  /** Config file key that maps to this arg */
62
64
  configKey?: string;
63
65
  /** Group name for organizing this option under a labeled section in help output */
@@ -316,7 +318,10 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig, showAllBui
316
318
  const remainingAliases = arg.aliases?.filter((a) => a !== primaryName);
317
319
 
318
320
  const flagsPlain = arg.flags?.length ? arg.flags.map((f) => `-${f}`).join(', ') : '';
319
- const namesPlain = [`--${primaryName}`, ...(remainingAliases?.map((a) => `--${a}`) || [])].join(', ');
321
+ const negPlain = arg.negative?.length ? arg.negative.map((n) => `--${n}`).join(', ') : '';
322
+ const namesPlain = [`--${primaryName}`, ...(remainingAliases?.map((a) => `--${a}`) || []), ...(negPlain ? [negPlain] : [])].join(
323
+ ', ',
324
+ );
320
325
  const typePlain = arg.type && arg.type !== 'boolean' ? (arg.optional ? `[${arg.type}]` : `<${arg.type}>`) : '';
321
326
 
322
327
  const isDeprecated = !!arg.deprecated;
@@ -128,9 +128,20 @@ function extractArgsInfo(schema: StandardJSONSchemaV1, meta?: PadroneArgsSchemaM
128
128
  const optMeta = argsMeta?.[key];
129
129
  const propType = prop.type as string;
130
130
 
131
- // Booleans are negatable unless there's an explicit noArg property
132
- // or this arg is itself a negation of another arg
133
- const isNegatable = propType === 'boolean' && !hasExplicitNegation(key) && !isNegationOf(key);
131
+ // Resolve custom negative keywords from meta or schema
132
+ const rawNegative = optMeta?.negative ?? prop?.negative;
133
+ const hasCustomNegative = rawNegative !== undefined;
134
+ const negativeList = hasCustomNegative
135
+ ? typeof rawNegative === 'string'
136
+ ? rawNegative
137
+ ? [rawNegative]
138
+ : []
139
+ : Array.from(rawNegative as readonly string[]).filter(Boolean)
140
+ : undefined;
141
+
142
+ // Booleans are negatable unless there's an explicit noArg property,
143
+ // this arg is itself a negation of another arg, or custom negative keywords are set
144
+ const isNegatable = propType === 'boolean' && !hasCustomNegative && !hasExplicitNegation(key) && !isNegationOf(key);
134
145
 
135
146
  result.push({
136
147
  name: key,
@@ -144,6 +155,7 @@ function extractArgsInfo(schema: StandardJSONSchemaV1, meta?: PadroneArgsSchemaM
144
155
  examples: optMeta?.examples ?? prop?.examples,
145
156
  variadic: propType === 'array',
146
157
  negatable: isNegatable,
158
+ negative: negativeList?.length ? negativeList : undefined,
147
159
  group: optMeta?.group,
148
160
  });
149
161
  }
@@ -35,6 +35,16 @@ export interface PadroneFieldMeta {
35
35
  flags?: readonly SingleChar[] | SingleChar;
36
36
  /** Multi-character alternative long names. Used with double dash (e.g. `--dry-run` for `--dryRun`). */
37
37
  alias?: readonly string[] | string;
38
+ /**
39
+ * Custom negative keyword(s) for boolean options. When provided, `--<keyword>` sets this option to `false`.
40
+ * Disables the default `--no-<option>` negation prefix. Set to `''` or `[]` to only disable the prefix.
41
+ * @example
42
+ * ```ts
43
+ * local: z.boolean().default(true).meta({ negative: 'remote' })
44
+ * // --remote sets local to false, --no-local is disabled
45
+ * ```
46
+ */
47
+ negative?: readonly string[] | string;
38
48
  deprecated?: boolean | string;
39
49
  hidden?: boolean;
40
50
  examples?: readonly unknown[];
@@ -1,6 +1,8 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  import type { Tool } from 'ai';
3
- import type { PadroneRuntime } from '../core/runtime.ts';
3
+ import type { PadroneProgressIndicator, PadroneRuntime } from '../core/runtime.ts';
4
+ import type { PadroneLogger } from '../extension/logger.ts';
5
+ import type { PadroneTracer } from '../extension/tracing.ts';
4
6
  import type { PadroneMcpPreferences } from '../feature/mcp.ts';
5
7
  import type { PadroneServePreferences } from '../feature/serve.ts';
6
8
  import type { WrapConfig, WrapResult } from '../feature/wrap.ts';
@@ -340,21 +342,11 @@ export type PadroneBuilderMethods<
340
342
  >;
341
343
 
342
344
  /** Add or override a subcommand. Pass a builder function to define its schema, action, and nested commands. @category Builder */
343
- command: <
344
- TNameNested extends string,
345
- TAliases extends string[] = [],
346
- TBuilder extends CommandTypesBase = DefaultCommandBuilder<
347
- TProgramName,
348
- TNameNested,
349
- FullCommandName<TName, TParentName>,
350
- TArgs,
351
- TCommands,
352
- TContext & TContextProvided
353
- >,
354
- >(
355
- name: TNameNested | readonly [TNameNested, ...TAliases],
356
- builderFn?: (
357
- builder: InitialCommandBuilder<
345
+ command: {
346
+ <
347
+ TNameNested extends string,
348
+ TAliases extends string[] = [],
349
+ TBuilder extends CommandTypesBase = DefaultCommandBuilder<
358
350
  TProgramName,
359
351
  TNameNested,
360
352
  FullCommandName<TName, TParentName>,
@@ -362,28 +354,94 @@ export type PadroneBuilderMethods<
362
354
  TCommands,
363
355
  TContext & TContextProvided
364
356
  >,
365
- ) => TBuilder,
366
- ) => BuilderOrProgram<
367
- TReturn,
368
- TProgramName,
369
- TName,
370
- TParentName,
371
- TArgs,
372
- TRes,
373
- TCommands extends []
374
- ? [WithAliases<TBuilder['~types']['command'], TAliases>]
375
- : AnyPadroneCommand[] extends TCommands
357
+ >(
358
+ name: TNameNested | readonly [TNameNested, ...TAliases],
359
+ builderFn?: (
360
+ builder: InitialCommandBuilder<
361
+ TProgramName,
362
+ TNameNested,
363
+ FullCommandName<TName, TParentName>,
364
+ TArgs,
365
+ TCommands,
366
+ TContext & TContextProvided
367
+ >,
368
+ ) => TBuilder,
369
+ ): BuilderOrProgram<
370
+ TReturn,
371
+ TProgramName,
372
+ TName,
373
+ TParentName,
374
+ TArgs,
375
+ TRes,
376
+ TCommands extends []
376
377
  ? [WithAliases<TBuilder['~types']['command'], TAliases>]
377
- : ReplaceOrAppendCommand<
378
- TCommands,
379
- TNameNested,
380
- WithAliases<TBuilder['~types']['command'], ResolvedAliases<TCommands, TNameNested, TAliases>>
381
- >,
382
- TParentArgs,
383
- TAsync,
384
- TContext,
385
- TContextProvided
386
- >;
378
+ : AnyPadroneCommand[] extends TCommands
379
+ ? [WithAliases<TBuilder['~types']['command'], TAliases>]
380
+ : ReplaceOrAppendCommand<
381
+ TCommands,
382
+ TNameNested,
383
+ WithAliases<TBuilder['~types']['command'], ResolvedAliases<TCommands, TNameNested, TAliases>>
384
+ >,
385
+ TParentArgs,
386
+ TAsync,
387
+ TContext,
388
+ TContextProvided
389
+ >;
390
+ // Overload for defineCommand.requires() branded callbacks — validates context requirements
391
+ <TNameNested extends string, TAliases extends string[] = [], TBuilder extends CommandTypesBase = CommandTypesBase, TReq = unknown>(
392
+ name: TNameNested | readonly [TNameNested, ...TAliases],
393
+ builderFn: ((builder: any) => TBuilder) & { '~contextRequires': (ctx: TReq) => void },
394
+ ): TContext & TContextProvided extends TReq
395
+ ? BuilderOrProgram<
396
+ TReturn,
397
+ TProgramName,
398
+ TName,
399
+ TParentName,
400
+ TArgs,
401
+ TRes,
402
+ TCommands extends []
403
+ ? [WithAliases<TBuilder['~types']['command'], TAliases>]
404
+ : AnyPadroneCommand[] extends TCommands
405
+ ? [WithAliases<TBuilder['~types']['command'], TAliases>]
406
+ : ReplaceOrAppendCommand<
407
+ TCommands,
408
+ TNameNested,
409
+ WithAliases<TBuilder['~types']['command'], ResolvedAliases<TCommands, TNameNested, TAliases>>
410
+ >,
411
+ TParentArgs,
412
+ TAsync,
413
+ TContext,
414
+ TContextProvided
415
+ >
416
+ : DefineCommandRequiresError;
417
+ // Fallback overload: accepts DefineCommand-typed callbacks where the builder type is not structurally compatible
418
+ // (e.g., DefineCommand with unknown context used in a parent with specific context)
419
+ <TNameNested extends string, TAliases extends string[] = [], TBuilder extends CommandTypesBase = CommandTypesBase>(
420
+ name: TNameNested | readonly [TNameNested, ...TAliases],
421
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
422
+ builderFn?: (builder: any) => TBuilder,
423
+ ): BuilderOrProgram<
424
+ TReturn,
425
+ TProgramName,
426
+ TName,
427
+ TParentName,
428
+ TArgs,
429
+ TRes,
430
+ TCommands extends []
431
+ ? [WithAliases<TBuilder['~types']['command'], TAliases>]
432
+ : AnyPadroneCommand[] extends TCommands
433
+ ? [WithAliases<TBuilder['~types']['command'], TAliases>]
434
+ : ReplaceOrAppendCommand<
435
+ TCommands,
436
+ TNameNested,
437
+ WithAliases<TBuilder['~types']['command'], ResolvedAliases<TCommands, TNameNested, TAliases>>
438
+ >,
439
+ TParentArgs,
440
+ TAsync,
441
+ TContext,
442
+ TContextProvided
443
+ >;
444
+ };
387
445
 
388
446
  /** Mount an existing program as a subcommand, optionally transforming the context. @category Builder */
389
447
  mount: {
@@ -694,6 +752,30 @@ export type AnyPadroneProgram = PadroneProgram<string, string, string, any, any,
694
752
  */
695
753
  export type PadroneExtension<TIn extends CommandTypesBase = CommandTypesBase, TOut extends CommandTypesBase = TIn> = (builder: TIn) => TOut;
696
754
 
755
+ /**
756
+ * Default context type for commands defined with `defineCommand()`.
757
+ * Includes optional context properties provided by common extensions (logger, tracing, progress).
758
+ *
759
+ * Override globally via module augmentation to add your application's context:
760
+ * ```ts
761
+ * declare module 'padrone' {
762
+ * interface DefineCommandContext {
763
+ * db: Database;
764
+ * }
765
+ * }
766
+ * ```
767
+ */
768
+ export interface DefineCommandContext {
769
+ logger?: PadroneLogger;
770
+ tracing?: PadroneTracer;
771
+ progress?: PadroneProgressIndicator;
772
+ }
773
+
774
+ /** Error brand returned by `.command()` when a `defineCommand.requires()` context requirement is not satisfied. */
775
+ export type DefineCommandRequiresError = {
776
+ readonly '~error': 'Required context not satisfied. Ensure required interceptors are registered on the program.';
777
+ };
778
+
697
779
  /**
698
780
  * Type for a command builder callback used with `.command()`.
699
781
  * Use this when defining commands in separate files where full return type inference isn't needed.
@@ -712,7 +794,27 @@ export type PadroneExtension<TIn extends CommandTypesBase = CommandTypesBase, TO
712
794
  * ```
713
795
  */
714
796
  export type DefineCommand<TContext = unknown, TParentArgs extends PadroneSchema = PadroneSchema> = (
715
- builder: PadroneBuilder<string, string, string, PadroneSchema<void>, void, [], TParentArgs, false, TContext>,
797
+ builder: PadroneBuilder<string, string, string, PadroneSchema<void>, void, [], TParentArgs, false, TContext, DefineCommandContext>,
716
798
  ) => CommandTypesBase;
717
799
 
800
+ /**
801
+ * Builder returned by `defineCommand()` (no-arg form).
802
+ * Call `.requires<T>()` to declare context dependencies, then `.command()` to provide the builder callback.
803
+ *
804
+ * @example
805
+ * ```ts
806
+ * const adminCommand = defineCommand()
807
+ * .requires<{ adminDb: AdminDB }>()
808
+ * .define((c) => c.action((_args, ctx) => ctx.context.adminDb.query(...)));
809
+ * ```
810
+ */
811
+ export type DefineCommandBuilder<TContextProvided = DefineCommandContext, TBrand = unknown> = {
812
+ /** Declare context types this command requires. Purely type-level — no runtime effect. */
813
+ requires: <TRequires>() => DefineCommandBuilder<DefineCommandContext & TRequires, { '~contextRequires': (ctx: TRequires) => void }>;
814
+ /** Provide the command builder callback. */
815
+ define: <TContext = unknown, TOut extends CommandTypesBase = CommandTypesBase>(
816
+ fn: (builder: PadroneBuilder<string, string, string, PadroneSchema<void>, void, [], any, false, TContext, TContextProvided>) => TOut,
817
+ ) => typeof fn & TBrand;
818
+ };
819
+
718
820
  type DefaultArgs = Record<string, unknown> | void;
@@ -3,6 +3,8 @@ export type {
3
3
  AnyPadroneBuilder,
4
4
  AnyPadroneProgram,
5
5
  DefineCommand,
6
+ DefineCommandBuilder,
7
+ DefineCommandContext,
6
8
  PadroneBuilder,
7
9
  PadroneBuilderMethods,
8
10
  PadroneExtension,
@@ -8,7 +8,7 @@ import type { AnyPadroneCommand, PadroneActionContext } from './command.ts';
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
10
  /** Base context shared across all interceptor phases within a single execution. */
11
- export type InterceptorBaseContext<TContext = unknown> = {
11
+ export type InterceptorBaseContext<TContext = object> = {
12
12
  /** The resolved command for this execution. In the parse phase, this is the root program. */
13
13
  command: AnyPadroneCommand;
14
14
  /** The raw CLI input string (undefined when invoked without input). */
@@ -26,7 +26,7 @@ export type InterceptorBaseContext<TContext = unknown> = {
26
26
  };
27
27
 
28
28
  /** Context for the parse phase. */
29
- export type InterceptorParseContext<TContext = unknown> = InterceptorBaseContext<TContext>;
29
+ export type InterceptorParseContext<TContext = object> = InterceptorBaseContext<TContext>;
30
30
 
31
31
  /** Result returned by the parse phase's `next()`. */
32
32
  export type InterceptorParseResult = {
@@ -36,7 +36,7 @@ export type InterceptorParseResult = {
36
36
  };
37
37
 
38
38
  /** Context for the validate phase. */
39
- export type InterceptorValidateContext<TContext = unknown> = InterceptorBaseContext<TContext> & {
39
+ export type InterceptorValidateContext<TContext = object> = InterceptorBaseContext<TContext> & {
40
40
  /** Raw named arguments extracted by the parser. Mutable — modify before `next()` to inject/override values. */
41
41
  rawArgs: Record<string, unknown>;
42
42
  /** Positional argument strings extracted by the parser. */
@@ -54,7 +54,7 @@ export type InterceptorValidateResult<TArgs = unknown> = {
54
54
  };
55
55
 
56
56
  /** Context for the execute phase. Includes validate context fields (rawArgs, positionalArgs). */
57
- export type InterceptorExecuteContext<TArgs = unknown, TContext = unknown> = InterceptorValidateContext<TContext> & {
57
+ export type InterceptorExecuteContext<TArgs = unknown, TContext = object> = InterceptorValidateContext<TContext> & {
58
58
  /** Validated arguments that will be passed to the action. Mutable — modify before `next()` to override. */
59
59
  args: TArgs;
60
60
  };
@@ -65,10 +65,10 @@ export type InterceptorExecuteResult<TResult = unknown> = {
65
65
  };
66
66
 
67
67
  /** Context for the start phase. Runs before parsing, wraps the entire pipeline. */
68
- export type InterceptorStartContext<TContext = unknown> = InterceptorBaseContext<TContext>;
68
+ export type InterceptorStartContext<TContext = object> = InterceptorBaseContext<TContext>;
69
69
 
70
70
  /** Context for the error phase. Called when the pipeline throws. Includes pipeline state accumulated before the error. */
71
- export type InterceptorErrorContext<TContext = unknown> = InterceptorBaseContext<TContext> & {
71
+ export type InterceptorErrorContext<TContext = object> = InterceptorBaseContext<TContext> & {
72
72
  /** The error that was thrown. */
73
73
  error: unknown;
74
74
  /** Raw named arguments (available if parse completed). */
@@ -88,7 +88,7 @@ export type InterceptorErrorResult<TResult = unknown> = {
88
88
  };
89
89
 
90
90
  /** Context for the shutdown phase. Always runs after the pipeline (success or failure). Includes pipeline state accumulated before completion. */
91
- export type InterceptorShutdownContext<TResult = unknown, TContext = unknown> = InterceptorBaseContext<TContext> & {
91
+ export type InterceptorShutdownContext<TResult = unknown, TContext = object> = InterceptorBaseContext<TContext> & {
92
92
  /** The error, if the pipeline failed (after error phase processing). */
93
93
  error?: unknown;
94
94
  /** The pipeline result, if it succeeded. */
@@ -156,7 +156,7 @@ export type InterceptorMeta = {
156
156
  * - `TArgs` — the validated arguments type (output of the args schema).
157
157
  * - `TResult` — the command's return type.
158
158
  */
159
- export type InterceptorPhases<TArgs = unknown, TResult = unknown, TContext = unknown> = {
159
+ export type InterceptorPhases<TArgs = unknown, TResult = unknown, TContext = object> = {
160
160
  /**
161
161
  * Runs before the pipeline (parse → validate → execute). `next()` proceeds to the pipeline.
162
162
  * Root interceptors only. Use for startup tasks like telemetry, update checks, or global config loading.
@@ -188,7 +188,7 @@ export type InterceptorPhases<TArgs = unknown, TResult = unknown, TContext = unk
188
188
  * Factory function that creates phase handlers for an interceptor.
189
189
  * Called once per command execution — the closure provides typed, scoped cross-phase state across phases.
190
190
  */
191
- export type InterceptorFactory<TArgs = unknown, TResult = unknown, TContext = unknown> = () => InterceptorPhases<TArgs, TResult, TContext>;
191
+ export type InterceptorFactory<TArgs = unknown, TResult = unknown, TContext = object> = () => InterceptorPhases<TArgs, TResult, TContext>;
192
192
 
193
193
  /**
194
194
  * A self-contained interceptor value: a factory function with static metadata as own properties.
@@ -198,7 +198,7 @@ export type InterceptorFactory<TArgs = unknown, TResult = unknown, TContext = un
198
198
  * Also accepted directly by `.intercept()` as the single-argument form.
199
199
  * Call `.provides<T>()` to brand it as a context-providing interceptor.
200
200
  */
201
- export type PadroneInterceptorFn<TArgs = unknown, TResult = unknown, TContext = unknown> = InterceptorFactory<TArgs, TResult, TContext> &
201
+ export type PadroneInterceptorFn<TArgs = unknown, TResult = unknown, TContext = object> = InterceptorFactory<TArgs, TResult, TContext> &
202
202
  InterceptorMeta & {
203
203
  /** Brand this interceptor as providing additional context of type `TProvides`. No-op at runtime; purely a type-level cast. */
204
204
  provides: <TProvides>() => PadroneContextInterceptor<TProvides, TArgs, TResult, TContext>;
@@ -217,7 +217,7 @@ export type PadroneInterceptorFn<TArgs = unknown, TResult = unknown, TContext =
217
217
  *
218
218
  * Create with `defineInterceptor(meta, factory)` or pass `(meta, factory)` directly to `.intercept()`.
219
219
  */
220
- export type PadroneInterceptor<TArgs = unknown, TResult = unknown, TContext = unknown> = PadroneInterceptorFn<TArgs, TResult, TContext>;
220
+ export type PadroneInterceptor<TArgs = unknown, TResult = unknown, TContext = object> = PadroneInterceptorFn<TArgs, TResult, TContext>;
221
221
 
222
222
  /**
223
223
  * A context-providing interceptor. Carries a phantom `'~context'` brand declaring what it adds
@@ -227,7 +227,7 @@ export type PadroneInterceptor<TArgs = unknown, TResult = unknown, TContext = un
227
227
  * Created by calling `.provides<T>()` on a `PadroneInterceptorFn`.
228
228
  * Chain `.requires<T>()` to also declare context dependencies.
229
229
  */
230
- export type PadroneContextInterceptor<TProvides = unknown, TArgs = unknown, TResult = unknown, TContext = unknown> = Omit<
230
+ export type PadroneContextInterceptor<TProvides = unknown, TArgs = unknown, TResult = unknown, TContext = object> = Omit<
231
231
  PadroneInterceptorFn<TArgs, TResult, TContext>,
232
232
  'requires'
233
233
  > &