@optique/discover 1.2.0-dev.2254 → 1.2.0-dev.2259

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.
@@ -0,0 +1,285 @@
1
+ import { CommandOptions } from "@optique/core/primitives";
2
+ import { Mode, Parser } from "@optique/core/parser";
3
+
4
+ //#region src/command.d.ts
5
+ declare const commandBrand: unique symbol;
6
+ /**
7
+ * Metadata shown for a discovered command.
8
+ *
9
+ * This uses the same shape as Optique's `command()` options so discovered
10
+ * commands can provide descriptions, usage overrides, visibility, and custom
11
+ * command-level errors.
12
+ *
13
+ * @since 1.1.0
14
+ */
15
+ type CommandMetadata = CommandOptions;
16
+ /**
17
+ * Command path used by static command registration.
18
+ *
19
+ * An empty path represents the root command.
20
+ *
21
+ * @since 1.1.0
22
+ */
23
+ type CommandPath = readonly string[];
24
+ /**
25
+ * Resource bundle threaded through {@link ProgramHooks} for a single command
26
+ * run.
27
+ *
28
+ * A {@link ProgramHooks.beforeEach} hook returns this object; the dispatcher
29
+ * forwards it to the command handler's second parameter and to the matching
30
+ * {@link ProgramHooks.afterEach} and {@link ProgramHooks.onError} hooks. This
31
+ * threads handler-time resources without global state.
32
+ *
33
+ * @since 1.2.0
34
+ */
35
+ interface ProgramHookContext {
36
+ /**
37
+ * Caller-defined resource. Common shapes include a database pool, a logger
38
+ * scope, or a tracing span.
39
+ */
40
+ readonly resource?: unknown;
41
+ }
42
+ /**
43
+ * The parsed command selected by a discovered command parser.
44
+ *
45
+ * Most applications receive this only indirectly through `runProgram()`, which
46
+ * calls the handler automatically.
47
+ *
48
+ * @since 1.1.0
49
+ */
50
+ interface ProgramInvocation {
51
+ /**
52
+ * The command definition that matched the input.
53
+ */
54
+ readonly command: AnyCommand;
55
+ /**
56
+ * The resolved command path that matched the input.
57
+ *
58
+ * Unlike {@link CommandDefinition.path}, this is always populated: for
59
+ * file-based discovery it is the path derived from the module's location even
60
+ * when the command definition omits an explicit `path`. The root command
61
+ * uses an empty array. Lifecycle hooks can use this to identify which
62
+ * command is running.
63
+ */
64
+ readonly path: CommandPath;
65
+ /**
66
+ * Parsed value produced by the command parser.
67
+ */
68
+ readonly value: unknown;
69
+ /**
70
+ * Handler to call with {@link ProgramInvocation.value} and, when a
71
+ * {@link ProgramHooks} `beforeEach` produced one, a {@link ProgramHookContext}.
72
+ *
73
+ * The context is optional: `runProgram()` supplies it only when a program-level
74
+ * or command-level `beforeEach` ran, and callers that dispatch invocations
75
+ * directly can keep passing only the value.
76
+ */
77
+ readonly handler: (value: unknown, context?: ProgramHookContext) => void | Promise<void>;
78
+ }
79
+ /**
80
+ * Lifecycle hooks invoked around a command handler.
81
+ *
82
+ * Hooks let cross-cutting concerns — log scopes, tracing spans, lazy resource
83
+ * setup, structured timing, error reporting — live in a single place instead
84
+ * of being duplicated inside every command handler. Pass them to
85
+ * `runProgram({ hooks })` to wrap every command, or to
86
+ * {@link CommandDefinition.hooks} to wrap a single command.
87
+ *
88
+ * When both program-level and command-level hooks are present, they nest:
89
+ *
90
+ * ```
91
+ * program.beforeEach → command.beforeEach → handler
92
+ * ↓
93
+ * program.afterEach ← command.afterEach ←─┘
94
+ * program.onError ← command.onError ← on failure
95
+ * ```
96
+ *
97
+ * @since 1.2.0
98
+ */
99
+ interface ProgramHooks {
100
+ /**
101
+ * Called before the command handler runs, receiving the matched command, its
102
+ * resolved {@link ProgramInvocation.path}, the parsed value, and the handler
103
+ * via {@link ProgramInvocation}.
104
+ *
105
+ * The returned {@link ProgramHookContext} is threaded forward as the second
106
+ * argument to the command handler (when this is the most specific hook scope)
107
+ * and to {@link afterEach} and {@link onError}. Returning a context is
108
+ * optional: omit the return or return `null`, and an empty context is
109
+ * threaded forward instead.
110
+ *
111
+ * Returning a promise is supported; the dispatcher awaits it. A rejected
112
+ * promise (or a thrown error) aborts the command before the handler runs and
113
+ * invokes {@link onError}.
114
+ */
115
+ readonly beforeEach?: (invocation: ProgramInvocation) => ProgramHookContext | null | void | Promise<ProgramHookContext | null | void>;
116
+ /**
117
+ * Called after the handler returns successfully, receiving the context from
118
+ * {@link beforeEach} (or an empty object when no `beforeEach` ran) and the
119
+ * handler's return value.
120
+ *
121
+ * Returning a promise is supported; the dispatcher awaits it. If this hook
122
+ * throws or rejects, the dispatcher treats it as a handler failure and
123
+ * invokes {@link onError} with the thrown error.
124
+ */
125
+ readonly afterEach?: (context: ProgramHookContext, result: unknown) => void | Promise<void>;
126
+ /**
127
+ * Called when the handler (or {@link beforeEach}/{@link afterEach}) throws or
128
+ * rejects, receiving the context from {@link beforeEach} (or an empty object)
129
+ * and the thrown error.
130
+ *
131
+ * The dispatcher re-throws the original error after this hook resolves, so
132
+ * process exit-code behavior is unchanged; the hook is for observation and
133
+ * cleanup, not for swallowing the error. An error thrown by this hook itself
134
+ * is suppressed so it cannot mask the original failure.
135
+ *
136
+ * Returning a promise is supported; the dispatcher awaits it.
137
+ */
138
+ readonly onError?: (context: ProgramHookContext, error: unknown) => void | Promise<void>;
139
+ }
140
+ /**
141
+ * Input accepted by {@link defineCommand}.
142
+ *
143
+ * @template M The mode of the command parser.
144
+ * @template T The parsed value passed to the command handler.
145
+ * @since 1.1.0
146
+ */
147
+ interface CommandDefinition<M extends Mode, T> {
148
+ /**
149
+ * Command path used when commands are passed directly to `runProgram()`.
150
+ * Use an empty path (`[]`) to register the root command.
151
+ *
152
+ * File-based discovery derives the command path from the file name and uses
153
+ * this field only to validate that the declared path matches.
154
+ */
155
+ readonly path?: CommandPath;
156
+ /**
157
+ * Parser for this command's command-specific arguments and options.
158
+ */
159
+ readonly parser: Parser<M, T, unknown>;
160
+ /**
161
+ * Metadata used in help output and shell completion.
162
+ */
163
+ readonly metadata?: CommandMetadata;
164
+ /**
165
+ * Lifecycle hooks scoped to this command.
166
+ *
167
+ * These run inside any program-level hooks passed to `runProgram({ hooks })`:
168
+ * the program-level `beforeEach` runs first, then this command's
169
+ * `beforeEach`, then the handler; teardown unwinds in reverse. Use this when
170
+ * a single command needs its own preflight, such as a `deploy` command that
171
+ * always refreshes an auth token, instead of program-wide logic.
172
+ *
173
+ * @since 1.2.0
174
+ */
175
+ readonly hooks?: ProgramHooks;
176
+ /**
177
+ * Handles the parsed command value.
178
+ *
179
+ * @param value Parsed command value.
180
+ * @param context Resource bundle from the most specific {@link ProgramHooks}
181
+ * `beforeEach` that ran. It is omitted when no program-level
182
+ * or command-level `beforeEach` ran, so a plain command
183
+ * without hooks receives only the value, exactly as before.
184
+ * Existing single-argument handlers can ignore it.
185
+ * @returns Nothing, or a promise that resolves when command handling
186
+ * completes.
187
+ */
188
+ readonly handler: (value: T, context?: ProgramHookContext) => void | Promise<void>;
189
+ }
190
+ /**
191
+ * A discovered command module definition.
192
+ *
193
+ * @template M The mode of the command parser.
194
+ * @template T The parsed value passed to the command handler.
195
+ * @since 1.1.0
196
+ */
197
+ interface Command<M extends Mode, T> extends CommandDefinition<M, T> {
198
+ /**
199
+ * Internal marker used to validate discovered modules.
200
+ *
201
+ * @internal
202
+ */
203
+ readonly [commandBrand]: true;
204
+ }
205
+ /**
206
+ * A command that declares its own command path.
207
+ *
208
+ * Static `runProgram({ commands })` registration accepts this shape.
209
+ *
210
+ * @template M The mode of the command parser.
211
+ * @template T The parsed value passed to the command handler.
212
+ * @since 1.1.0
213
+ */
214
+ interface StaticCommand<M extends Mode, T> extends Command<M, T> {
215
+ /**
216
+ * Command path used by static command registration.
217
+ */
218
+ readonly path: CommandPath;
219
+ }
220
+ /**
221
+ * A command with its handler value type erased.
222
+ *
223
+ * This type is used by discovery APIs that collect commands with different
224
+ * parsed value types. The handler cannot be called directly without first
225
+ * recovering the parser's value type.
226
+ *
227
+ * @since 1.1.0
228
+ */
229
+ type AnyCommand = Omit<Command<Mode, unknown>, "handler"> & {
230
+ /**
231
+ * Erased command handler.
232
+ */
233
+ readonly handler: (value: never, context?: ProgramHookContext) => void | Promise<void>;
234
+ };
235
+ /**
236
+ * A statically registered command with its handler value type erased.
237
+ *
238
+ * @since 1.1.0
239
+ */
240
+ type AnyStaticCommand = Omit<StaticCommand<Mode, unknown>, "handler"> & {
241
+ /**
242
+ * Erased command handler.
243
+ */
244
+ readonly handler: (value: never, context?: ProgramHookContext) => void | Promise<void>;
245
+ };
246
+ /**
247
+ * Defines a command module for `@optique/discover`.
248
+ *
249
+ * This helper returns its argument unchanged while preserving parser value
250
+ * inference for the handler callback.
251
+ *
252
+ * @template M The mode of the command parser.
253
+ * @template T The parsed value passed to the command handler.
254
+ * @param command The command definition.
255
+ * @returns The same command definition with inferred types.
256
+ * @throws {TypeError} If the parser, path, handler, or hooks are missing or
257
+ * malformed.
258
+ * @since 1.1.0
259
+ */
260
+ declare function defineCommand<M extends Mode, T>(command: CommandDefinition<M, T> & {
261
+ readonly path: CommandPath;
262
+ }): StaticCommand<M, T>;
263
+ declare function defineCommand<M extends Mode, T>(command: CommandDefinition<M, T>): Command<M, T>;
264
+ /**
265
+ * Returns whether a value is a command created by {@link defineCommand}.
266
+ *
267
+ * @param value The value to inspect.
268
+ * @returns `true` when the value is a discovered command definition.
269
+ * @since 1.1.0
270
+ */
271
+ declare function isCommand(value: unknown): value is AnyCommand;
272
+ /**
273
+ * Validates a {@link ProgramHooks} value, throwing a descriptive error when it
274
+ * is malformed.
275
+ *
276
+ * @param hooks The value to validate.
277
+ * @param scope Label used in error messages: `"Command"` for command-level
278
+ * hooks and `"Program"` for program-level hooks.
279
+ * @throws {TypeError} If `hooks` is not an object, or a hook is neither
280
+ * nullish nor a function.
281
+ * @internal
282
+ */
283
+ declare function validateHooks(hooks: unknown, scope: "Command" | "Program"): asserts hooks is ProgramHooks;
284
+ //#endregion
285
+ export { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, ProgramHookContext, ProgramHooks, ProgramInvocation, StaticCommand, defineCommand, isCommand, validateHooks };
package/dist/command.cjs CHANGED
@@ -1,4 +1,5 @@
1
- const require_command = require('./command-CUn2_NIA.cjs');
1
+ const require_command = require('./command-C-NgG0KJ.cjs');
2
2
 
3
3
  exports.defineCommand = require_command.defineCommand;
4
- exports.isCommand = require_command.isCommand;
4
+ exports.isCommand = require_command.isCommand;
5
+ exports.validateHooks = require_command.validateHooks;
@@ -1,2 +1,2 @@
1
- import { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, StaticCommand, defineCommand, isCommand } from "./command-DSHBTa5c.cjs";
2
- export { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, StaticCommand, defineCommand, isCommand };
1
+ import { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, ProgramHookContext, ProgramHooks, ProgramInvocation, StaticCommand, defineCommand, isCommand, validateHooks } from "./command-Dvg452tU.cjs";
2
+ export { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, ProgramHookContext, ProgramHooks, ProgramInvocation, StaticCommand, defineCommand, isCommand, validateHooks };
package/dist/command.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, StaticCommand, defineCommand, isCommand } from "./command-DrmNW0HO.js";
2
- export { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, StaticCommand, defineCommand, isCommand };
1
+ import { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, ProgramHookContext, ProgramHooks, ProgramInvocation, StaticCommand, defineCommand, isCommand, validateHooks } from "./command-egqHCvDL.js";
2
+ export { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, ProgramHookContext, ProgramHooks, ProgramInvocation, StaticCommand, defineCommand, isCommand, validateHooks };
package/dist/command.js CHANGED
@@ -1,3 +1,3 @@
1
- import { defineCommand, isCommand } from "./command-DO5zgkvS.js";
1
+ import { defineCommand, isCommand, validateHooks } from "./command-9sgrJtwh.js";
2
2
 
3
- export { defineCommand, isCommand };
3
+ export { defineCommand, isCommand, validateHooks };
@@ -1,4 +1,4 @@
1
- import { getDefaultExtensions } from "./src-kBDpUW2H.js";
1
+ import { getDefaultExtensions } from "./src-wHLRsXWK.js";
2
2
  import { mkdir, readdir, realpath, stat, writeFile } from "node:fs/promises";
3
3
  import { dirname, posix, relative, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require('./chunk-CUT6urMc.cjs');
2
- const require_src = require('./src-BDObn4mj.cjs');
2
+ const require_src = require('./src-C3_a7SFM.cjs');
3
3
  const node_fs_promises = require_chunk.__toESM(require("node:fs/promises"));
4
4
  const node_path = require_chunk.__toESM(require("node:path"));
5
5
  const node_url = require_chunk.__toESM(require("node:url"));
@@ -1,6 +1,6 @@
1
- require('./command-CUn2_NIA.cjs');
2
- require('./src-BDObn4mj.cjs');
3
- const require_generator = require('./generator-BV1QDOot.cjs');
1
+ require('./command-C-NgG0KJ.cjs');
2
+ require('./src-C3_a7SFM.cjs');
3
+ const require_generator = require('./generator-CkGpilfc.cjs');
4
4
 
5
5
  exports.generateCommandsModule = require_generator.generateCommandsModule;
6
6
  exports.watchCommandsModule = require_generator.watchCommandsModule;
package/dist/generator.js CHANGED
@@ -1,5 +1,5 @@
1
- import "./command-DO5zgkvS.js";
2
- import "./src-kBDpUW2H.js";
3
- import { generateCommandsModule, watchCommandsModule, writeCommandsModule } from "./generator-B81thoIS.js";
1
+ import "./command-9sgrJtwh.js";
2
+ import "./src-wHLRsXWK.js";
3
+ import { generateCommandsModule, watchCommandsModule, writeCommandsModule } from "./generator-B8UY7n7b.js";
4
4
 
5
5
  export { generateCommandsModule, watchCommandsModule, writeCommandsModule };
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
- const require_command = require('./command-CUn2_NIA.cjs');
2
- const require_src = require('./src-BDObn4mj.cjs');
1
+ const require_command = require('./command-C-NgG0KJ.cjs');
2
+ const require_src = require('./src-C3_a7SFM.cjs');
3
3
 
4
4
  exports.commandsFromModules = require_src.commandsFromModules;
5
5
  exports.createProgramParser = require_src.createProgramParser;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, StaticCommand, defineCommand, isCommand } from "./command-DSHBTa5c.cjs";
1
+ import { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, ProgramHookContext, ProgramHooks, ProgramInvocation, StaticCommand, defineCommand, isCommand } from "./command-Dvg452tU.cjs";
2
2
  import { Mode } from "@optique/core/parser";
3
3
  import { FluentParser } from "@optique/core/fluent";
4
4
  import { Message } from "@optique/core/message";
@@ -7,28 +7,6 @@ import { RunOptions } from "@optique/run";
7
7
 
8
8
  //#region src/index.d.ts
9
9
 
10
- /**
11
- * The parsed command selected by a discovered command parser.
12
- *
13
- * Most applications receive this only indirectly through {@link runProgram},
14
- * which calls the handler automatically.
15
- *
16
- * @since 1.1.0
17
- */
18
- interface ProgramInvocation {
19
- /**
20
- * The command definition that matched the input.
21
- */
22
- readonly command: AnyCommand;
23
- /**
24
- * Parsed value produced by the command parser.
25
- */
26
- readonly value: unknown;
27
- /**
28
- * Handler to call with {@link ProgramInvocation.value}.
29
- */
30
- readonly handler: (value: unknown) => void | Promise<void>;
31
- }
32
10
  /**
33
11
  * A command paired with its command path.
34
12
  *
@@ -198,6 +176,18 @@ interface RunProgramBaseOptions extends Omit<RunOptions, "help" | "version" | "c
198
176
  * @default `"both"`
199
177
  */
200
178
  readonly completion?: RunOptions["completion"] | false;
179
+ /**
180
+ * Lifecycle hooks invoked around each command handler.
181
+ *
182
+ * Use these for cross-cutting concerns such as opening a log scope, starting
183
+ * a tracing span, or reporting handler failures, without duplicating the
184
+ * logic in every command. Hooks are opt-in: omitting this field keeps the
185
+ * exact behavior of a plain `runProgram()` call. Command-level hooks defined
186
+ * on {@link CommandDefinition.hooks} nest inside these.
187
+ *
188
+ * @since 1.2.0
189
+ */
190
+ readonly hooks?: ProgramHooks;
201
191
  }
202
192
  /**
203
193
  * Options for {@link runProgram} when discovering commands from files.
@@ -311,7 +301,8 @@ declare function createProgramParser(commands: readonly CommandEntry[], metadata
311
301
  * @param options Program options.
312
302
  * @returns A promise that resolves after the selected command handler
313
303
  * completes.
314
- * @throws {TypeError} If discovery or command loading fails.
304
+ * @throws {TypeError} If discovery or command loading fails, or `hooks` is
305
+ * malformed.
315
306
  * @since 1.1.0
316
307
  */
317
308
  declare function runProgram(options: RunProgramOptions): Promise<void>;
@@ -335,4 +326,4 @@ interface ProgramHelpMetadata {
335
326
  readonly footer?: Message;
336
327
  }
337
328
  //#endregion
338
- export { type AnyCommand, type AnyStaticCommand, type Command, type CommandDefinition, CommandEntry, type CommandMetadata, type CommandPath, CommandsFromModulesOptions, DiscoverCommandsOptions, DiscoveredCommand, ModuleCommand, ModuleMap, ProgramHelpMetadata, ProgramInvocation, RunProgramDiscoveryOptions, RunProgramOptions, RunProgramStaticOptions, RuntimeExtensionOptions, type StaticCommand, commandsFromModules, createProgramParser, defineCommand, discoverCommands, getDefaultExtensions, isCommand, runProgram };
329
+ export { type AnyCommand, type AnyStaticCommand, type Command, type CommandDefinition, CommandEntry, type CommandMetadata, type CommandPath, CommandsFromModulesOptions, DiscoverCommandsOptions, DiscoveredCommand, ModuleCommand, ModuleMap, ProgramHelpMetadata, type ProgramHookContext, type ProgramHooks, type ProgramInvocation, RunProgramDiscoveryOptions, RunProgramOptions, RunProgramStaticOptions, RuntimeExtensionOptions, type StaticCommand, commandsFromModules, createProgramParser, defineCommand, discoverCommands, getDefaultExtensions, isCommand, runProgram };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, StaticCommand, defineCommand, isCommand } from "./command-DrmNW0HO.js";
1
+ import { AnyCommand, AnyStaticCommand, Command, CommandDefinition, CommandMetadata, CommandPath, ProgramHookContext, ProgramHooks, ProgramInvocation, StaticCommand, defineCommand, isCommand } from "./command-egqHCvDL.js";
2
2
  import { Message } from "@optique/core/message";
3
3
  import { ProgramMetadata } from "@optique/core/program";
4
4
  import { RunOptions } from "@optique/run";
@@ -7,28 +7,6 @@ import { Mode } from "@optique/core/parser";
7
7
 
8
8
  //#region src/index.d.ts
9
9
 
10
- /**
11
- * The parsed command selected by a discovered command parser.
12
- *
13
- * Most applications receive this only indirectly through {@link runProgram},
14
- * which calls the handler automatically.
15
- *
16
- * @since 1.1.0
17
- */
18
- interface ProgramInvocation {
19
- /**
20
- * The command definition that matched the input.
21
- */
22
- readonly command: AnyCommand;
23
- /**
24
- * Parsed value produced by the command parser.
25
- */
26
- readonly value: unknown;
27
- /**
28
- * Handler to call with {@link ProgramInvocation.value}.
29
- */
30
- readonly handler: (value: unknown) => void | Promise<void>;
31
- }
32
10
  /**
33
11
  * A command paired with its command path.
34
12
  *
@@ -198,6 +176,18 @@ interface RunProgramBaseOptions extends Omit<RunOptions, "help" | "version" | "c
198
176
  * @default `"both"`
199
177
  */
200
178
  readonly completion?: RunOptions["completion"] | false;
179
+ /**
180
+ * Lifecycle hooks invoked around each command handler.
181
+ *
182
+ * Use these for cross-cutting concerns such as opening a log scope, starting
183
+ * a tracing span, or reporting handler failures, without duplicating the
184
+ * logic in every command. Hooks are opt-in: omitting this field keeps the
185
+ * exact behavior of a plain `runProgram()` call. Command-level hooks defined
186
+ * on {@link CommandDefinition.hooks} nest inside these.
187
+ *
188
+ * @since 1.2.0
189
+ */
190
+ readonly hooks?: ProgramHooks;
201
191
  }
202
192
  /**
203
193
  * Options for {@link runProgram} when discovering commands from files.
@@ -311,7 +301,8 @@ declare function createProgramParser(commands: readonly CommandEntry[], metadata
311
301
  * @param options Program options.
312
302
  * @returns A promise that resolves after the selected command handler
313
303
  * completes.
314
- * @throws {TypeError} If discovery or command loading fails.
304
+ * @throws {TypeError} If discovery or command loading fails, or `hooks` is
305
+ * malformed.
315
306
  * @since 1.1.0
316
307
  */
317
308
  declare function runProgram(options: RunProgramOptions): Promise<void>;
@@ -335,4 +326,4 @@ interface ProgramHelpMetadata {
335
326
  readonly footer?: Message;
336
327
  }
337
328
  //#endregion
338
- export { type AnyCommand, type AnyStaticCommand, type Command, type CommandDefinition, CommandEntry, type CommandMetadata, type CommandPath, CommandsFromModulesOptions, DiscoverCommandsOptions, DiscoveredCommand, ModuleCommand, ModuleMap, ProgramHelpMetadata, ProgramInvocation, RunProgramDiscoveryOptions, RunProgramOptions, RunProgramStaticOptions, RuntimeExtensionOptions, type StaticCommand, commandsFromModules, createProgramParser, defineCommand, discoverCommands, getDefaultExtensions, isCommand, runProgram };
329
+ export { type AnyCommand, type AnyStaticCommand, type Command, type CommandDefinition, CommandEntry, type CommandMetadata, type CommandPath, CommandsFromModulesOptions, DiscoverCommandsOptions, DiscoveredCommand, ModuleCommand, ModuleMap, ProgramHelpMetadata, type ProgramHookContext, type ProgramHooks, type ProgramInvocation, RunProgramDiscoveryOptions, RunProgramOptions, RunProgramStaticOptions, RuntimeExtensionOptions, type StaticCommand, commandsFromModules, createProgramParser, defineCommand, discoverCommands, getDefaultExtensions, isCommand, runProgram };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { defineCommand, isCommand } from "./command-DO5zgkvS.js";
2
- import { commandsFromModules, createProgramParser, discoverCommands, getDefaultExtensions, runProgram } from "./src-kBDpUW2H.js";
1
+ import { defineCommand, isCommand } from "./command-9sgrJtwh.js";
2
+ import { commandsFromModules, createProgramParser, discoverCommands, getDefaultExtensions, runProgram } from "./src-wHLRsXWK.js";
3
3
 
4
4
  export { commandsFromModules, createProgramParser, defineCommand, discoverCommands, getDefaultExtensions, isCommand, runProgram };
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require('./chunk-CUT6urMc.cjs');
2
- const require_command = require('./command-CUn2_NIA.cjs');
2
+ const require_command = require('./command-C-NgG0KJ.cjs');
3
3
  const __optique_core_constructs = require_chunk.__toESM(require("@optique/core/constructs"));
4
4
  const __optique_core_modifiers = require_chunk.__toESM(require("@optique/core/modifiers"));
5
5
  const __optique_core_primitives = require_chunk.__toESM(require("@optique/core/primitives"));
@@ -146,10 +146,12 @@ function createProgramParser(commands, metadata = {}) {
146
146
  * @param options Program options.
147
147
  * @returns A promise that resolves after the selected command handler
148
148
  * completes.
149
- * @throws {TypeError} If discovery or command loading fails.
149
+ * @throws {TypeError} If discovery or command loading fails, or `hooks` is
150
+ * malformed.
150
151
  * @since 1.1.0
151
152
  */
152
153
  async function runProgram(options) {
154
+ if (options.hooks != null) require_command.validateHooks(options.hooks, "Program");
153
155
  let commands;
154
156
  if (isStaticRunProgramOptions(options)) commands = staticCommandsToEntries(options.commands);
155
157
  else commands = await discoverCommands({
@@ -159,7 +161,55 @@ async function runProgram(options) {
159
161
  });
160
162
  const parser = createProgramParser(commands, options.metadata);
161
163
  const invocation = await (0, __optique_run.runAsync)(parser, buildRunOptions(options));
162
- await invocation.handler(invocation.value);
164
+ await dispatchInvocation(invocation, options.hooks);
165
+ }
166
+ /**
167
+ * Runs a command handler wrapped in the program-level and command-level
168
+ * lifecycle hooks.
169
+ *
170
+ * The hooks nest: the program-level `beforeEach` runs first, then the command's
171
+ * `beforeEach`, then the handler; `afterEach` and `onError` unwind in reverse,
172
+ * with the command-level hook running before the program-level one.
173
+ *
174
+ * @param invocation The selected command invocation.
175
+ * @param programHooks Program-level hooks, if any.
176
+ * @returns A promise that resolves after the handler and matching hooks
177
+ * complete.
178
+ * @throws The original error thrown by `beforeEach`, the handler, or
179
+ * `afterEach`, re-thrown after the `onError` hooks run.
180
+ */
181
+ async function dispatchInvocation(invocation, programHooks) {
182
+ const commandHooks = invocation.command.hooks;
183
+ await runHookScope(programHooks, invocation, (programContext) => runHookScope(commandHooks, invocation, (commandContext) => {
184
+ if (commandHooks?.beforeEach != null) return invocation.handler(invocation.value, commandContext);
185
+ if (programHooks?.beforeEach != null) return invocation.handler(invocation.value, programContext);
186
+ return invocation.handler(invocation.value);
187
+ }));
188
+ }
189
+ /**
190
+ * Runs an inner step wrapped in a single set of lifecycle hooks.
191
+ *
192
+ * @param hooks The hooks for this scope, if any.
193
+ * @param invocation The selected command invocation passed to `beforeEach`.
194
+ * @param inner The step to wrap; receives the context from `beforeEach`.
195
+ * @returns The value returned by `inner`.
196
+ * @throws The original error thrown by `beforeEach`, `inner`, or `afterEach`,
197
+ * re-thrown after `onError` runs. An error thrown by `onError` itself
198
+ * is suppressed so it cannot mask the original failure.
199
+ */
200
+ async function runHookScope(hooks, invocation, inner) {
201
+ let context = {};
202
+ try {
203
+ if (hooks?.beforeEach != null) context = await hooks.beforeEach(invocation) ?? {};
204
+ const result = await inner(context);
205
+ if (hooks?.afterEach != null) await hooks.afterEach(context, result);
206
+ return result;
207
+ } catch (error) {
208
+ if (hooks?.onError != null) try {
209
+ await hooks.onError(context, error);
210
+ } catch {}
211
+ throw error;
212
+ }
163
213
  }
164
214
  function getRuntime() {
165
215
  if ("Deno" in globalThis) return "deno";
@@ -337,18 +387,19 @@ function buildCommandTree(commands) {
337
387
  current = child;
338
388
  }
339
389
  current.command = entry.command;
390
+ current.path = entry.path;
340
391
  }
341
392
  return root;
342
393
  }
343
394
  function buildNodeParser(node, inheritedHidden) {
344
395
  const childParser = buildChildrenParser(node, inheritedHidden);
345
- if (childParser != null && node.command != null) return createExecutableNodeParser(childParser, node.command);
396
+ if (childParser != null && node.command != null) return createExecutableNodeParser(childParser, node.command, node.path ?? []);
346
397
  if (childParser != null) return childParser;
347
- if (node.command != null) return createLeafParser(node.command);
398
+ if (node.command != null) return createLeafParser(node.command, node.path ?? []);
348
399
  throw new TypeError("Command tree node must contain a command.");
349
400
  }
350
- function createExecutableNodeParser(childParser, commandDefinition) {
351
- const leafParser = createLeafParser(commandDefinition, true);
401
+ function createExecutableNodeParser(childParser, commandDefinition, path) {
402
+ const leafParser = createLeafParser(commandDefinition, path, true);
352
403
  const branchParsers = [childParser, leafParser];
353
404
  const parser = (0, __optique_core_constructs.longestMatch)(childParser, leafParser);
354
405
  const phase2SeedHook = findPhase2SeedHook(parser);
@@ -387,16 +438,16 @@ function createExecutableNodeParser(childParser, commandDefinition) {
387
438
  state: toExclusiveState(activeState, context.state)
388
439
  }, prefix);
389
440
  },
390
- getSuggestRuntimeNodes(state, path) {
441
+ getSuggestRuntimeNodes(state, path$1) {
391
442
  const activeState = normalizeExecutableNodeState(state);
392
443
  if (activeState == null) {
393
- const branchPath$1 = [...path, 1];
444
+ const branchPath$1 = [...path$1, 1];
394
445
  const branchState$1 = (0, __optique_core_extension.inheritAnnotations)(state, leafParser.initialState);
395
446
  return getExecutableNodeBranchSuggestRuntimeNodes(leafParser, branchState$1, branchPath$1);
396
447
  }
397
- if (activeState?.result.success !== true) return parser.getSuggestRuntimeNodes?.(toExclusiveState(activeState, state), path) ?? [];
448
+ if (activeState?.result.success !== true) return parser.getSuggestRuntimeNodes?.(toExclusiveState(activeState, state), path$1) ?? [];
398
449
  const branchParser = branchParsers[activeState.branch];
399
- const branchPath = [...path, activeState.branch];
450
+ const branchPath = [...path$1, activeState.branch];
400
451
  const branchState = (0, __optique_core_extension.inheritAnnotations)(state, activeState.result.next.state);
401
452
  return getExecutableNodeBranchSuggestRuntimeNodes(branchParser, branchState, branchPath);
402
453
  },
@@ -648,9 +699,10 @@ function commandMetadataWithInheritedHidden(metadata, inheritedHidden) {
648
699
  ...hidden != null && { hidden }
649
700
  };
650
701
  }
651
- function createLeafParser(commandDefinition, includeMetadata = false) {
702
+ function createLeafParser(commandDefinition, path, includeMetadata = false) {
652
703
  const parser = (0, __optique_core_modifiers.map)(commandDefinition.parser, (value) => ({
653
704
  command: commandDefinition,
705
+ path,
654
706
  value,
655
707
  handler: commandDefinition.handler
656
708
  }));
@@ -703,7 +755,7 @@ function commandPathHidden(path, commandsByPath) {
703
755
  }
704
756
  function buildRunOptions(options) {
705
757
  const metadata = options.metadata;
706
- const { dir: _dir, commands: _commands, extensions: _extensions, entryFileName: _entryFileName, metadata: _metadata, help, version, completion,...rest } = options;
758
+ const { dir: _dir, commands: _commands, extensions: _extensions, entryFileName: _entryFileName, metadata: _metadata, hooks: _hooks, help, version, completion,...rest } = options;
707
759
  const runOptions = {
708
760
  ...rest,
709
761
  contexts: unwrapProgramContexts(rest.contexts),