@reliverse/rempts-core 1.6.1 → 2.3.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 (155) hide show
  1. package/README.md +398 -102
  2. package/dist/cli.d.ts +32 -0
  3. package/dist/cli.js +731 -0
  4. package/dist/config-loader.d.ts +42 -0
  5. package/dist/config-loader.js +20 -0
  6. package/dist/config.d.ts +99 -0
  7. package/dist/config.js +188 -0
  8. package/dist/file-loader.d.ts +43 -0
  9. package/dist/file-loader.js +199 -0
  10. package/dist/global-flags.d.ts +36 -0
  11. package/dist/global-flags.js +36 -0
  12. package/dist/mod.d.ts +13 -0
  13. package/dist/mod.js +19 -0
  14. package/dist/parser.d.ts +6 -0
  15. package/dist/parser.js +137 -0
  16. package/dist/plugin/context.d.ts +13 -0
  17. package/dist/plugin/context.js +53 -0
  18. package/dist/plugin/create.d.ts +92 -0
  19. package/dist/plugin/create.js +61 -0
  20. package/dist/plugin/loader.d.ts +12 -0
  21. package/dist/plugin/loader.js +65 -0
  22. package/dist/plugin/manager.d.ts +53 -0
  23. package/dist/plugin/manager.js +135 -0
  24. package/dist/plugin/mod.d.ts +10 -0
  25. package/dist/plugin/mod.js +27 -0
  26. package/dist/plugin/store.d.ts +45 -0
  27. package/dist/plugin/store.js +60 -0
  28. package/dist/plugin/testing.d.ts +38 -0
  29. package/dist/plugin/testing.js +175 -0
  30. package/dist/plugin/types.d.ts +146 -0
  31. package/dist/tui/registry.d.ts +8 -0
  32. package/dist/tui/registry.js +10 -0
  33. package/dist/tui/types.d.ts +58 -0
  34. package/dist/tui/types.js +10 -0
  35. package/dist/types.d.ts +178 -0
  36. package/dist/types.js +25 -0
  37. package/dist/utils/logger.d.ts +10 -0
  38. package/dist/utils/logger.js +27 -0
  39. package/dist/utils/merge.d.ts +13 -0
  40. package/dist/utils/merge.js +25 -0
  41. package/dist/utils/mod.d.ts +6 -0
  42. package/dist/utils/mod.js +2 -0
  43. package/dist/utils/type-helpers.d.ts +41 -0
  44. package/dist/utils/type-helpers.js +0 -0
  45. package/dist/validation.d.ts +30 -0
  46. package/dist/validation.js +121 -0
  47. package/package.json +47 -44
  48. package/src/cli.ts +1049 -0
  49. package/src/config-loader.ts +71 -0
  50. package/src/config.ts +270 -0
  51. package/src/file-loader.ts +346 -0
  52. package/src/global-flags.ts +50 -0
  53. package/src/mod.ts +74 -0
  54. package/src/parser.ts +212 -0
  55. package/src/plugin/context.ts +88 -0
  56. package/src/plugin/create.ts +174 -0
  57. package/src/plugin/loader.ts +111 -0
  58. package/src/plugin/manager.ts +244 -0
  59. package/src/plugin/mod.ts +51 -0
  60. package/src/plugin/store.ts +124 -0
  61. package/src/plugin/testing.ts +236 -0
  62. package/src/plugin/types.ts +206 -0
  63. package/src/tui/registry.ts +22 -0
  64. package/src/tui/types.ts +79 -0
  65. package/src/types.ts +285 -0
  66. package/src/utils/logger.ts +43 -0
  67. package/src/utils/merge.ts +54 -0
  68. package/src/utils/mod.ts +7 -0
  69. package/src/utils/type-helpers.ts +151 -0
  70. package/src/validation.ts +177 -0
  71. package/LICENSE +0 -21
  72. package/bin/core-impl/anykey/anykey-mod.d.ts +0 -12
  73. package/bin/core-impl/anykey/anykey-mod.js +0 -125
  74. package/bin/core-impl/date/date.d.ts +0 -2
  75. package/bin/core-impl/date/date.js +0 -236
  76. package/bin/core-impl/editor/editor-mod.d.ts +0 -25
  77. package/bin/core-impl/editor/editor-mod.js +0 -896
  78. package/bin/core-impl/figures/figures-mod.d.ts +0 -233
  79. package/bin/core-impl/figures/figures-mod.js +0 -286
  80. package/bin/core-impl/figures/figures.test.d.ts +0 -1
  81. package/bin/core-impl/figures/figures.test.js +0 -474
  82. package/bin/core-impl/input/confirm-prompt.d.ts +0 -5
  83. package/bin/core-impl/input/confirm-prompt.js +0 -173
  84. package/bin/core-impl/input/input-prompt.d.ts +0 -16
  85. package/bin/core-impl/input/input-prompt.js +0 -370
  86. package/bin/core-impl/launcher/_parser.d.ts +0 -2
  87. package/bin/core-impl/launcher/_parser.js +0 -122
  88. package/bin/core-impl/launcher/_utils.d.ts +0 -8
  89. package/bin/core-impl/launcher/_utils.js +0 -29
  90. package/bin/core-impl/launcher/args.d.ts +0 -3
  91. package/bin/core-impl/launcher/args.js +0 -89
  92. package/bin/core-impl/launcher/command.d.ts +0 -8
  93. package/bin/core-impl/launcher/command.js +0 -68
  94. package/bin/core-impl/launcher/launcher-mod.d.ts +0 -8
  95. package/bin/core-impl/launcher/launcher-mod.js +0 -34
  96. package/bin/core-impl/launcher/usage.d.ts +0 -3
  97. package/bin/core-impl/launcher/usage.js +0 -104
  98. package/bin/core-impl/msg-fmt/colors.d.ts +0 -30
  99. package/bin/core-impl/msg-fmt/colors.js +0 -42
  100. package/bin/core-impl/msg-fmt/logger.d.ts +0 -17
  101. package/bin/core-impl/msg-fmt/logger.js +0 -106
  102. package/bin/core-impl/msg-fmt/mapping.d.ts +0 -3
  103. package/bin/core-impl/msg-fmt/mapping.js +0 -49
  104. package/bin/core-impl/msg-fmt/messages.d.ts +0 -35
  105. package/bin/core-impl/msg-fmt/messages.js +0 -314
  106. package/bin/core-impl/msg-fmt/terminal.d.ts +0 -15
  107. package/bin/core-impl/msg-fmt/terminal.js +0 -59
  108. package/bin/core-impl/msg-fmt/variants.d.ts +0 -11
  109. package/bin/core-impl/msg-fmt/variants.js +0 -52
  110. package/bin/core-impl/next-steps/next-steps.d.ts +0 -14
  111. package/bin/core-impl/next-steps/next-steps.js +0 -24
  112. package/bin/core-impl/number/number-mod.d.ts +0 -28
  113. package/bin/core-impl/number/number-mod.js +0 -197
  114. package/bin/core-impl/results/results.d.ts +0 -7
  115. package/bin/core-impl/results/results.js +0 -27
  116. package/bin/core-impl/select/multiselect-prompt.d.ts +0 -2
  117. package/bin/core-impl/select/multiselect-prompt.js +0 -341
  118. package/bin/core-impl/select/nummultiselect-prompt.d.ts +0 -6
  119. package/bin/core-impl/select/nummultiselect-prompt.js +0 -105
  120. package/bin/core-impl/select/numselect-prompt.d.ts +0 -7
  121. package/bin/core-impl/select/numselect-prompt.js +0 -115
  122. package/bin/core-impl/select/select-prompt.d.ts +0 -33
  123. package/bin/core-impl/select/select-prompt.js +0 -302
  124. package/bin/core-impl/select/toggle-prompt.d.ts +0 -5
  125. package/bin/core-impl/select/toggle-prompt.js +0 -208
  126. package/bin/core-impl/st-end/end.d.ts +0 -2
  127. package/bin/core-impl/st-end/end.js +0 -42
  128. package/bin/core-impl/st-end/start.d.ts +0 -17
  129. package/bin/core-impl/st-end/start.js +0 -66
  130. package/bin/core-impl/task/progress.d.ts +0 -2
  131. package/bin/core-impl/task/progress.js +0 -57
  132. package/bin/core-impl/task/spinner.d.ts +0 -15
  133. package/bin/core-impl/task/spinner.js +0 -110
  134. package/bin/core-impl/utils/colorize.d.ts +0 -2
  135. package/bin/core-impl/utils/colorize.js +0 -134
  136. package/bin/core-impl/utils/errors.d.ts +0 -1
  137. package/bin/core-impl/utils/errors.js +0 -15
  138. package/bin/core-impl/utils/prevent.d.ts +0 -10
  139. package/bin/core-impl/utils/prevent.js +0 -69
  140. package/bin/core-impl/utils/prompt-end.d.ts +0 -8
  141. package/bin/core-impl/utils/prompt-end.js +0 -33
  142. package/bin/core-impl/utils/stream-text.d.ts +0 -18
  143. package/bin/core-impl/utils/stream-text.js +0 -136
  144. package/bin/core-impl/utils/system.d.ts +0 -6
  145. package/bin/core-impl/utils/system.js +0 -7
  146. package/bin/core-impl/utils/validate.d.ts +0 -22
  147. package/bin/core-impl/utils/validate.js +0 -17
  148. package/bin/core-impl/visual/animate/animate.d.ts +0 -14
  149. package/bin/core-impl/visual/animate/animate.js +0 -64
  150. package/bin/core-impl/visual/ascii-art/ascii-art.d.ts +0 -6
  151. package/bin/core-impl/visual/ascii-art/ascii-art.js +0 -12
  152. package/bin/core-types.d.ts +0 -434
  153. package/bin/main.d.ts +0 -41
  154. package/bin/main.js +0 -96
  155. /package/{bin/core-types.js → dist/plugin/types.js} +0 -0
package/src/cli.ts ADDED
@@ -0,0 +1,1049 @@
1
+ import { dirname, join, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { relico } from "@reliverse/relico";
4
+ import { getDotPath, SchemaError } from "@standard-schema/utils";
5
+ import { type RemptsConfigStrict, remptsConfigSchema, remptsConfigStrictSchema } from "./config";
6
+ import { type LoadedConfig, loadConfig } from "./config-loader";
7
+ import { createFileCommandLoader } from "./file-loader";
8
+ import { GLOBAL_FLAGS, type GlobalFlags } from "./global-flags";
9
+ import { parseArgs } from "./parser";
10
+ import {
11
+ createPluginManager,
12
+ loadPlugins,
13
+ runAfterCommand,
14
+ runBeforeCommand,
15
+ runConfigResolved,
16
+ runSetup,
17
+ } from "./plugin/manager";
18
+ import type { CommandContext, MergePluginStores, Plugin, PluginConfig } from "./plugin/types";
19
+ import { getTuiRenderer } from "./tui/registry";
20
+ import type {
21
+ AfterHook,
22
+ BeforeHook,
23
+ CLI,
24
+ CLIOption,
25
+ Command,
26
+ HookContext,
27
+ InferMergedOptions,
28
+ MergedOptions,
29
+ Options,
30
+ RemptsConfig,
31
+ ResolvedConfig,
32
+ RuntimeInfo,
33
+ TerminalInfo,
34
+ } from "./types";
35
+
36
+ export async function createApp<
37
+ TPlugins extends readonly Plugin[] = [],
38
+ TDefaultCommand extends string | undefined = undefined,
39
+ >(
40
+ options: {
41
+ /**
42
+ * CLI configuration override
43
+ */
44
+ config?: Partial<RemptsConfig> & {
45
+ plugins?: TPlugins;
46
+ };
47
+ /**
48
+ * Default command to run when no arguments are provided
49
+ */
50
+ defaultCommand?: TDefaultCommand;
51
+ /**
52
+ * Whether to auto-initialize commands from config
53
+ * @default true
54
+ */
55
+ autoInit?: boolean;
56
+ /**
57
+ * Custom config directory (overrides --cwd detection)
58
+ */
59
+ configDir?: string;
60
+ /**
61
+ * Entry file path (e.g., import.meta.path or __filename)
62
+ * If not provided, will be auto-detected from call stack
63
+ * Commands directory will be <entry-file-dir>/cmds
64
+ */
65
+ entryFile?: string;
66
+ } = {}
67
+ ): Promise<CLI<MergePluginStores<TPlugins>>> {
68
+ const {
69
+ config: configOverride,
70
+ defaultCommand,
71
+ autoInit = true,
72
+ configDir: customConfigDir,
73
+ entryFile,
74
+ } = options;
75
+
76
+ // Auto-detect config directory from --cwd flag or current directory
77
+ let configDir = customConfigDir || process.cwd();
78
+
79
+ if (!customConfigDir) {
80
+ // Parse --cwd flag before loading config
81
+ const args = process.argv.slice(2);
82
+ const cwdIndex = args.indexOf("--cwd");
83
+ if (cwdIndex !== -1 && cwdIndex + 1 < args.length && args[cwdIndex + 1]) {
84
+ const cwdArg = args[cwdIndex + 1] as string;
85
+ configDir = cwdArg.startsWith("/") ? cwdArg : resolve(process.cwd(), cwdArg);
86
+ // Remove --cwd and its value from args
87
+ args.splice(cwdIndex, 2);
88
+ // Update process.argv to reflect the changes
89
+ process.argv = [process.argv[0] || "", process.argv[1] || "", ...args];
90
+ } else {
91
+ // No --cwd provided, try to auto-discover config location
92
+ try {
93
+ // First try current directory
94
+ await loadConfig(process.cwd());
95
+ configDir = process.cwd();
96
+ } catch {
97
+ // If not found, try apps/dler relative to current directory (monorepo support)
98
+ const monorepoConfigDir = resolve(process.cwd(), "apps/dler");
99
+ try {
100
+ await loadConfig(monorepoConfigDir);
101
+ configDir = monorepoConfigDir;
102
+ } catch {
103
+ // If still not found, keep original cwd (will fail with proper error message)
104
+ configDir = process.cwd();
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ // Load config from the detected directory
111
+ let loadedConfigData: LoadedConfig | null = null;
112
+ try {
113
+ loadedConfigData = await loadConfig(configDir);
114
+ } catch {
115
+ // Config not found, will use override only
116
+ }
117
+
118
+ // Create final config by merging loaded config with override
119
+ const finalConfigOverride = (() => {
120
+ // Start with loaded config or override
121
+ let baseConfig = loadedConfigData || configOverride;
122
+
123
+ if (loadedConfigData && configOverride) {
124
+ // Merge loaded config with override, ensuring plugins type compatibility
125
+ const { plugins: overridePlugins, ...overrideRest } = configOverride;
126
+ baseConfig = {
127
+ ...loadedConfigData,
128
+ ...overrideRest,
129
+ // Override plugins only if explicitly provided in configOverride
130
+ ...(overridePlugins !== undefined ? { plugins: overridePlugins as any } : {}),
131
+ } as any;
132
+ }
133
+
134
+ // Resolve relative paths in config to absolute paths based on config directory
135
+ function resolveConfigPaths(obj: any, baseDir: string): any {
136
+ if (typeof obj === "string" && obj.startsWith("./")) {
137
+ return resolve(baseDir, obj);
138
+ }
139
+ if (Array.isArray(obj)) {
140
+ return obj.map((item) => resolveConfigPaths(item, baseDir));
141
+ }
142
+ if (obj && typeof obj === "object") {
143
+ const result: any = {};
144
+ for (const [key, value] of Object.entries(obj)) {
145
+ result[key] = resolveConfigPaths(value, baseDir);
146
+ }
147
+ return result;
148
+ }
149
+ return obj;
150
+ }
151
+
152
+ if (baseConfig) {
153
+ baseConfig = resolveConfigPaths(baseConfig, configDir);
154
+ }
155
+
156
+ // Ensure commands directory is resolved relative to configDir if it exists
157
+ if (baseConfig?.commands?.directory && typeof baseConfig.commands.directory === "string") {
158
+ (baseConfig as any).commands.directory = baseConfig.commands.directory.startsWith(".")
159
+ ? resolve(configDir, baseConfig.commands.directory)
160
+ : baseConfig.commands.directory;
161
+ }
162
+
163
+ return baseConfig as
164
+ | (Partial<RemptsConfig> & {
165
+ plugins?: TPlugins;
166
+ })
167
+ | undefined;
168
+ })();
169
+
170
+ const cli = await createCLI(finalConfigOverride || {}, entryFile);
171
+
172
+ // Load commands from directory (if autoInit)
173
+ if (autoInit) {
174
+ await cli.init();
175
+ }
176
+
177
+ // Handle default command injection
178
+ if (defaultCommand) {
179
+ const originalRun = cli.run.bind(cli);
180
+ cli.run = async (argv = process.argv.slice(2)) => {
181
+ // If no arguments or only flags, inject default command
182
+ if (argv.length === 0 || argv[0]?.startsWith("-")) {
183
+ process.argv.splice(2, 0, defaultCommand);
184
+ argv = [defaultCommand, ...argv];
185
+ }
186
+ // If first arg is not a flag and not the default command, it's a project name - inject default command
187
+ else if (argv[0] && !argv[0].startsWith("-") && argv[0] !== defaultCommand) {
188
+ process.argv.splice(2, 0, defaultCommand);
189
+ argv = [defaultCommand, ...argv];
190
+ }
191
+
192
+ return originalRun(argv);
193
+ };
194
+ }
195
+
196
+ return cli;
197
+ }
198
+
199
+ /**
200
+ * Get entry file path from call stack
201
+ * Tries to find the first file that's not in rempts-core
202
+ */
203
+ function getEntryFileFromStack(): string | undefined {
204
+ try {
205
+ const stack = new Error("Stack trace").stack;
206
+ if (!stack) return undefined;
207
+
208
+ const lines = stack.split("\n");
209
+ // Skip first line (Error message) and second line (this function)
210
+ for (let i = 2; i < lines.length; i++) {
211
+ const line = lines[i];
212
+ if (!line) continue;
213
+
214
+ // Match file paths in stack traces
215
+ // Bun format: at function (file:///path/to/file.ts:line:col)
216
+ // Node format: at function (/path/to/file.ts:line:col)
217
+ const match = line.match(/\(?(file:\/\/\/?|)([^:]+\.(ts|js|mjs)):\d+:\d+\)?/);
218
+ if (match) {
219
+ const filePath = match[2];
220
+ if (filePath && !filePath.includes("rempts-core") && !filePath.includes("node_modules")) {
221
+ // Convert file:// URL to path if needed
222
+ if (filePath.startsWith("file://")) {
223
+ return fileURLToPath(filePath);
224
+ }
225
+ return filePath;
226
+ }
227
+ }
228
+ }
229
+ } catch (_error) {
230
+ // Ignore errors in stack parsing
231
+ }
232
+ return undefined;
233
+ }
234
+
235
+ export async function createCLI<TPlugins extends readonly Plugin[] = []>(
236
+ configOverride?: Partial<RemptsConfig> & {
237
+ plugins?: TPlugins;
238
+ },
239
+ entryFile?: string
240
+ ): Promise<CLI<MergePluginStores<TPlugins>>> {
241
+ type TStore = MergePluginStores<TPlugins>;
242
+
243
+ // Auto-load config from dler.config.ts (optional)
244
+ let loadedConfigData: LoadedConfig | null = null;
245
+ try {
246
+ loadedConfigData = await loadConfig();
247
+ } catch {
248
+ // Config file is optional - if not found, use override or defaults
249
+ loadedConfigData = null;
250
+ }
251
+
252
+ // Use loaded config or create from override
253
+ let baseConfig =
254
+ loadedConfigData || (remptsConfigSchema.assert(configOverride || {}) as LoadedConfig);
255
+
256
+ // Determine commands directory from entry file
257
+ // Commands directory is always <entry-file-dir>/cmds
258
+ let cmdsDir: string;
259
+ const detectedEntryFile = entryFile || getEntryFileFromStack();
260
+
261
+ if (detectedEntryFile) {
262
+ // Resolve entry file to absolute path
263
+ const entryFilePath = detectedEntryFile.startsWith("file://")
264
+ ? fileURLToPath(detectedEntryFile)
265
+ : resolve(detectedEntryFile);
266
+ const entryDir = dirname(entryFilePath);
267
+ cmdsDir = join(entryDir, "cmds");
268
+ } else {
269
+ // Fallback: use config if provided, otherwise use process.cwd()/cmds
270
+ if (baseConfig.commands?.directory) {
271
+ cmdsDir = resolve(baseConfig.commands.directory);
272
+ } else {
273
+ cmdsDir = join(process.cwd(), "cmds");
274
+ }
275
+ }
276
+
277
+ // Override commands directory in config (entry file takes precedence)
278
+ baseConfig = {
279
+ ...baseConfig,
280
+ commands: {
281
+ ...baseConfig.commands,
282
+ directory: cmdsDir,
283
+ },
284
+ };
285
+
286
+ const loadedConfig: RemptsConfig = baseConfig;
287
+
288
+ // Merge override config on top of loaded config
289
+ const mergedConfig = {
290
+ ...loadedConfig,
291
+ ...configOverride,
292
+ // Deep merge plugins arrays
293
+ plugins: configOverride?.plugins || loadedConfig.plugins || [],
294
+ };
295
+
296
+ // Validate and coerce config - only require name/version if they are explicitly provided
297
+ let fullConfig: RemptsConfigStrict;
298
+ try {
299
+ // If name and version are not provided, create a minimal config with defaults
300
+ if (mergedConfig.name && mergedConfig.version) {
301
+ fullConfig = remptsConfigStrictSchema.assert(mergedConfig);
302
+ } else {
303
+ const minimalConfig = {
304
+ name: mergedConfig.name || "cli",
305
+ version: mergedConfig.version || "1.0.0",
306
+ ...mergedConfig,
307
+ };
308
+ fullConfig = remptsConfigStrictSchema.assert(minimalConfig);
309
+ }
310
+ } catch (error) {
311
+ throw new Error(
312
+ "[rempts] Invalid config: " + (error instanceof Error ? error.message : String(error))
313
+ );
314
+ }
315
+
316
+ const commands = new Map<string, Command<any, any>>();
317
+ const commandSources = new Map<string, "directory">(); // Track where commands come from
318
+ // Prefix tree for fast subcommand discovery: parentName -> Set<subcommandFullName>
319
+ const subcommandIndex = new Map<string, Set<string>>();
320
+
321
+ // Global before/after hooks
322
+ const beforeHooks: BeforeHook<TStore>[] = [];
323
+ const afterHooks: AfterHook<TStore>[] = [];
324
+
325
+ // Global hook context storage (shared across commands)
326
+ let globalHookContext: Record<string, any> = {};
327
+
328
+ // Helper to get terminal information
329
+ function getTerminalInfo(): TerminalInfo {
330
+ const isInteractive = process.stdout.isTTY;
331
+ const isCI = !!(
332
+ process.env.CI ||
333
+ process.env.CONTINUOUS_INTEGRATION ||
334
+ process.env.GITHUB_ACTIONS ||
335
+ process.env.GITLAB_CI ||
336
+ process.env.CIRCLECI ||
337
+ process.env.TRAVIS
338
+ );
339
+
340
+ return {
341
+ width: process.stdout.columns || 80,
342
+ height: process.stdout.rows || 24,
343
+ isInteractive,
344
+ isCI,
345
+ supportsColor: isInteractive && !isCI && process.env.TERM !== "dumb",
346
+ supportsMouse: isInteractive && !isCI && process.env.TERM_PROGRAM !== "Apple_Terminal",
347
+ };
348
+ }
349
+ const pluginManagerState = createPluginManager<TStore>();
350
+
351
+ // Load plugins if configured
352
+ if (mergedConfig.plugins && mergedConfig.plugins.length > 0) {
353
+ await loadPlugins(pluginManagerState, mergedConfig.plugins as any as PluginConfig[]);
354
+
355
+ // Run setup hooks - this may modify config
356
+ const { config: updatedConfig, commands: pluginCommands } = await runSetup(
357
+ pluginManagerState,
358
+ fullConfig
359
+ );
360
+ // Re-validate after plugins potentially modified config
361
+ fullConfig = remptsConfigStrictSchema.assert(updatedConfig);
362
+
363
+ // Register plugin commands (if any)
364
+ // Note: Since @reliverse/rempts is file-based only, plugins should register commands from files
365
+ // Plugin commands are deprecated - use file-based commands instead
366
+ if (pluginCommands.length > 0) {
367
+ console.warn(
368
+ "Warning: Plugin command registration is deprecated. " +
369
+ "Rempts is file-based only - register commands via file structure: <cmds-dir>/<cmd-name>/cmd.{ts,js,mjs}"
370
+ );
371
+ // For backward compatibility, try to extract name from command metadata
372
+ // But this won't work reliably since commands don't have names anymore
373
+ pluginCommands.forEach((cmd) => {
374
+ // Try to get name from command metadata or use a fallback
375
+ const cmdName = (cmd as any).name || "unknown";
376
+ if (cmdName === "unknown") {
377
+ console.warn("Skipping plugin command without name - use file-based commands instead");
378
+ return;
379
+ }
380
+ registerCommand(cmdName, cmd, [], "directory");
381
+ });
382
+ }
383
+ }
384
+
385
+ // Create resolved config with defaults
386
+ const resolvedConfig: ResolvedConfig = {
387
+ name: fullConfig.name,
388
+ version: fullConfig.version,
389
+ description: fullConfig.description || "",
390
+ commands: fullConfig.commands || {},
391
+ build: fullConfig.build || {
392
+ targets: ["native"],
393
+ compress: false,
394
+ minify: false,
395
+ sourcemap: true,
396
+ },
397
+ dev: fullConfig.dev || {
398
+ watch: true,
399
+ inspect: false,
400
+ },
401
+ test: fullConfig.test || {
402
+ pattern: ["**/*.test.ts", "**/*.spec.ts"],
403
+ coverage: false,
404
+ watch: false,
405
+ },
406
+ workspace: fullConfig.workspace || {
407
+ versionStrategy: "fixed",
408
+ },
409
+ release: fullConfig.release || {
410
+ npm: true,
411
+ github: false,
412
+ tagFormat: "v{{version}}",
413
+ conventionalCommits: true,
414
+ },
415
+ plugins: fullConfig.plugins || [],
416
+ };
417
+
418
+ // Run configResolved hooks
419
+ if (mergedConfig.plugins && mergedConfig.plugins.length > 0) {
420
+ await runConfigResolved(pluginManagerState, resolvedConfig);
421
+ }
422
+
423
+ // Helper to register a command and its aliases
424
+ // Commands are file-based only - name is always inferred from file path
425
+ function registerCommand(
426
+ name: string,
427
+ cmd: Command<any, any>,
428
+ path: string[] = [],
429
+ source: "directory" = "directory"
430
+ ) {
431
+ const fullName = [...path, name].join(" ");
432
+
433
+ // Skip if command already exists
434
+ // This prevents conflicts when the same command is registered from multiple sources
435
+ // (e.g., file loading or duplicate files)
436
+ // File-loaded commands take precedence
437
+ if (commands.has(fullName)) {
438
+ return;
439
+ }
440
+
441
+ commands.set(fullName, cmd);
442
+ commandSources.set(fullName, source);
443
+
444
+ // Update subcommand index for fast subcommand discovery
445
+ // Performance: O(depth) where depth is command nesting level
446
+ // For "a b c", add it to indices for "a" and "a b"
447
+ // This enables O(1) lookup of parent -> Set<subcommands> instead of O(n) scan
448
+ const nameParts = fullName.split(" ");
449
+ // Skip empty parts (shouldn't happen, but be defensive)
450
+ const validParts = nameParts.filter((part) => part.length > 0);
451
+ for (let i = 0; i < validParts.length - 1; i++) {
452
+ const parentName = validParts.slice(0, i + 1).join(" ");
453
+ if (!subcommandIndex.has(parentName)) {
454
+ subcommandIndex.set(parentName, new Set());
455
+ }
456
+ subcommandIndex.get(parentName)!.add(fullName);
457
+ }
458
+
459
+ // Register aliases
460
+ if (cmd.alias) {
461
+ const aliases = Array.isArray(cmd.alias) ? cmd.alias : [cmd.alias];
462
+ aliases.forEach((alias) => {
463
+ const aliasPath = [...path, alias].join(" ");
464
+ // Skip if alias already exists (prevents duplicate registration)
465
+ if (commands.has(aliasPath)) {
466
+ return;
467
+ }
468
+ commands.set(aliasPath, cmd);
469
+ commandSources.set(aliasPath, source);
470
+
471
+ // Update subcommand index for aliases so they can discover subcommands too
472
+ // If this command has subcommands, make them discoverable via alias
473
+ const aliasParts = aliasPath.split(" ").filter((part) => part.length > 0);
474
+ for (let i = 0; i < aliasParts.length - 1; i++) {
475
+ const parentName = aliasParts.slice(0, i + 1).join(" ");
476
+ if (!subcommandIndex.has(parentName)) {
477
+ subcommandIndex.set(parentName, new Set());
478
+ }
479
+ // Add the original command name to the alias parent's index
480
+ subcommandIndex.get(parentName)!.add(fullName);
481
+ }
482
+ // If this command itself has subcommands, add them to alias index
483
+ const subcommandNames = subcommandIndex.get(fullName);
484
+ if (subcommandNames) {
485
+ if (!subcommandIndex.has(aliasPath)) {
486
+ subcommandIndex.set(aliasPath, new Set());
487
+ }
488
+ // Copy all subcommands to the alias index
489
+ for (const subCmdName of subcommandNames) {
490
+ subcommandIndex.get(aliasPath)!.add(subCmdName);
491
+ }
492
+ }
493
+ });
494
+ }
495
+
496
+ // Note: Nested commands are already registered individually from files
497
+ // Parent commands just group them together - no need to re-register
498
+ }
499
+
500
+ // Helper to find command by path
501
+ // Returns command name (from map key) and command object
502
+ // Performance: O(depth) where depth is the number of command parts
503
+ // Uses Map.get() which is O(1), so total is O(depth) - optimal for arbitrary nesting
504
+ function findCommand(args: string[]): {
505
+ command: Command<any, any> | undefined;
506
+ commandName: string | undefined;
507
+ remainingArgs: string[];
508
+ } {
509
+ // Try to find the deepest matching command
510
+ // Start from longest path (most specific) and work backwards
511
+ // This ensures we match "a b c" before "a b" when args = ["a", "b", "c"]
512
+ for (let i = args.length; i > 0; i--) {
513
+ const cmdPath = args.slice(0, i).join(" ");
514
+ const command = commands.get(cmdPath);
515
+ if (command) {
516
+ return { command, commandName: cmdPath, remainingArgs: args.slice(i) };
517
+ }
518
+ }
519
+ return { command: undefined, commandName: undefined, remainingArgs: args };
520
+ }
521
+
522
+ // Helper to show help for a command
523
+ // Name comes from the commands map key (inferred from file path)
524
+ function showHelp(cmdName?: string, cmd?: Command<any, TStore>, path: string[] = []) {
525
+ if (cmd && cmdName) {
526
+ // Show command-specific help
527
+ const fullPath = [...path, cmdName].join(" ");
528
+ console.log(relico.bold(`Usage: ${fullConfig.name} ${fullPath} [options]`));
529
+ console.log(`\n${relico.dim(cmd.description)}`);
530
+
531
+ if (cmd.options && Object.keys(cmd.options).length > 0) {
532
+ console.log(`\n${relico.bold("Options:")}`);
533
+ for (const [name, opt] of Object.entries(cmd.options)) {
534
+ const option = opt as CLIOption<any>;
535
+ const flag = `--${name}${option.short ? `, -${option.short}` : ""}`;
536
+ const description = option.description || "";
537
+ console.log(` ${relico.yellow(flag.padEnd(20))} ${relico.dim(description)}`);
538
+ }
539
+ }
540
+
541
+ // Discover subcommands using prefix tree index
542
+ // Performance: O(k) where k is the number of direct subcommands (not all commands)
543
+ // Much faster than scanning all commands O(n) where n is total command count
544
+ const subcommandNames = subcommandIndex.get(fullPath);
545
+ const subCommands: Array<{ name: string; command: Command<any, any> }> = [];
546
+ if (subcommandNames) {
547
+ const parentDepth = fullPath.split(" ").length;
548
+ for (const subCmdFullName of subcommandNames) {
549
+ // Only include direct children (depth = parentDepth + 1)
550
+ // This filters out grandchildren like "a b c d" when parent is "a b"
551
+ if (subCmdFullName.split(" ").length === parentDepth + 1) {
552
+ const subCmdName = subCmdFullName.slice(fullPath.length + 1);
553
+ const command = commands.get(subCmdFullName);
554
+ if (command) {
555
+ subCommands.push({ name: subCmdName, command });
556
+ }
557
+ }
558
+ }
559
+ }
560
+
561
+ if (subCommands.length > 0) {
562
+ console.log(`\n${relico.bold("Subcommands:")}`);
563
+ for (const { name: subCmdName, command: subCmd } of subCommands) {
564
+ console.log(` ${relico.green(subCmdName.padEnd(20))} ${relico.dim(subCmd.description)}`);
565
+ }
566
+ }
567
+ } else {
568
+ // Show root help
569
+ console.log(relico.bold(relico.cyan(`${fullConfig.name} v${fullConfig.version}`)));
570
+ if (fullConfig.description) {
571
+ console.log(relico.dim(fullConfig.description));
572
+ }
573
+ console.log(`\n${relico.bold("Commands:")}`);
574
+
575
+ // Show only top-level commands (names that don't contain spaces)
576
+ for (const [name, command] of commands) {
577
+ if (!(name.includes(" ") || command.alias?.includes(name))) {
578
+ console.log(` ${relico.green(name.padEnd(20))} ${relico.dim(command.description)}`);
579
+ }
580
+ }
581
+ }
582
+ }
583
+
584
+ function ensureRenderAvailable(commandName: string, command: Command<any, any>) {
585
+ if (!command.render) {
586
+ throw new Error(`Command ${commandName} does not support TUI rendering.`);
587
+ }
588
+ if (!getTuiRenderer()) {
589
+ throw new Error(
590
+ `TUI renderer not registered. Import '@reliverse/rempts-tui/register' or call registerTuiRenderer before running commands with render.`
591
+ );
592
+ }
593
+ }
594
+
595
+ // Auto-load commands from config if specified
596
+ async function loadFromConfig() {
597
+ // Load from directory if specified
598
+ if (fullConfig.commands?.directory) {
599
+ try {
600
+ // Use the already resolved commands directory
601
+ const cmdsDir = fullConfig.commands.directory;
602
+
603
+ const fileLoader = createFileCommandLoader();
604
+ const commandTree = await fileLoader.loadFromDirectory(cmdsDir);
605
+ const fileCommands = await fileLoader.loadCommandsFromTree(commandTree);
606
+
607
+ // Register all commands from the directory structure
608
+ // Name is inferred from file path: <cmds-dir>/<cmd-name>/cmd.{ts,js,mjs}
609
+ fileCommands.forEach(({ name, command }) => {
610
+ registerCommand(name, command, [], "directory");
611
+ });
612
+ } catch (error) {
613
+ console.error(
614
+ `Failed to load commands from directory ${fullConfig.commands.directory}:`,
615
+ error
616
+ );
617
+ throw error; // Re-throw to prevent CLI from starting with invalid config
618
+ }
619
+ }
620
+ }
621
+
622
+ async function runCommandInternal(
623
+ commandName: string,
624
+ command: Command<any, any>,
625
+ argv: string[],
626
+ providedFlags?: Record<string, unknown>
627
+ ) {
628
+ let context: CommandContext<any> | undefined;
629
+ let resultParsed: { flags: unknown; positional: string[] } | undefined;
630
+
631
+ try {
632
+ const mergedOptions = {
633
+ ...GLOBAL_FLAGS,
634
+ ...(command.options || {}),
635
+ } as MergedOptions<(typeof command.options & Options) | Options>;
636
+ const parsed = providedFlags
637
+ ? (() => {
638
+ // Parse with empty args for defaults, then overlay provided flags
639
+ // This keeps behavior consistent with execute(options)
640
+ return parseArgs([], mergedOptions, commandName).then(
641
+ (p) => (Object.assign(p.flags, providedFlags), p)
642
+ );
643
+ })()
644
+ : parseArgs(argv, mergedOptions, commandName);
645
+ resultParsed = await parsed;
646
+ const { prompt, spinner } = await import("@reliverse/rempts-utils");
647
+
648
+ if (mergedConfig.plugins && mergedConfig.plugins.length > 0) {
649
+ context = await runBeforeCommand(
650
+ pluginManagerState,
651
+ commandName,
652
+ command,
653
+ providedFlags ? [] : resultParsed.positional,
654
+ resultParsed.flags as Record<string, any>
655
+ );
656
+ }
657
+
658
+ // Run global before hooks
659
+ if (beforeHooks.length > 0) {
660
+ // Reset global hook context for this command
661
+ globalHookContext = {};
662
+
663
+ const hookContext: HookContext<TStore> = {
664
+ flags: resultParsed.flags as Record<string, unknown>,
665
+ store: context?.store?.getState() || ({} as TStore),
666
+ env: process.env,
667
+ cwd: process.cwd(),
668
+ set: (key: string, value: any) => {
669
+ globalHookContext[key] = value;
670
+ },
671
+ get: (key: string) => {
672
+ return globalHookContext[key];
673
+ },
674
+ };
675
+
676
+ for (const hook of beforeHooks) {
677
+ await hook(hookContext);
678
+ }
679
+ }
680
+
681
+ const terminalInfo = getTerminalInfo();
682
+ const globalFlags = resultParsed.flags as GlobalFlags & Record<string, unknown>;
683
+ const runtimeInfo: RuntimeInfo = {
684
+ startTime: Date.now(),
685
+ args: providedFlags ? [] : argv,
686
+ command: commandName,
687
+ };
688
+
689
+ let render = false;
690
+ if (command.render) {
691
+ if ((globalFlags as Record<string, unknown>)["no-tui"]) {
692
+ render = false;
693
+ } else if (
694
+ (globalFlags as Record<string, unknown>).tui ||
695
+ (globalFlags as Record<string, unknown>).interactive
696
+ ) {
697
+ render = true;
698
+ } else {
699
+ render = terminalInfo.isInteractive && !terminalInfo.isCI;
700
+ }
701
+ }
702
+
703
+ if (render) {
704
+ ensureRenderAvailable(commandName, command);
705
+ await getTuiRenderer<Record<string, unknown>, TStore>()?.({
706
+ command,
707
+ flags: resultParsed.flags as Record<string, unknown>,
708
+ positional: resultParsed.positional,
709
+ shell: Bun.$,
710
+ env: process.env,
711
+ cwd: process.cwd(),
712
+ prompt,
713
+ spinner,
714
+ colors: relico,
715
+ terminal: terminalInfo,
716
+ runtime: runtimeInfo,
717
+ ...(context ? { context } : {}),
718
+ ...(Object.keys(globalHookContext).length > 0 ? { hooks: globalHookContext } : {}),
719
+ });
720
+ } else {
721
+ if (!command.handler) {
722
+ throw new Error("Command does not provide a handler for non-TUI execution");
723
+ }
724
+ // Type assertion: flags are validated and typed by parseArgs
725
+ const typedFlags = resultParsed.flags as InferMergedOptions<Options>;
726
+ await command.handler({
727
+ flags: typedFlags,
728
+ positional: resultParsed.positional,
729
+ shell: Bun.$,
730
+ env: process.env,
731
+ cwd: process.cwd(),
732
+ prompt,
733
+ spinner,
734
+ colors: relico,
735
+ terminal: terminalInfo,
736
+ runtime: runtimeInfo,
737
+ ...(context ? { context } : {}),
738
+ ...(Object.keys(globalHookContext).length > 0 ? { hooks: globalHookContext } : {}),
739
+ });
740
+ }
741
+
742
+ if (mergedConfig.plugins && mergedConfig.plugins.length > 0 && context) {
743
+ await runAfterCommand(pluginManagerState, context, { exitCode: 0 });
744
+ }
745
+
746
+ // Run global after hooks
747
+ if (afterHooks.length > 0) {
748
+ const hookContext: HookContext<TStore> & { exitCode: number } = {
749
+ flags: resultParsed.flags as Record<string, unknown>,
750
+ store: context?.store?.getState() || ({} as TStore),
751
+ env: process.env,
752
+ cwd: process.cwd(),
753
+ set: () => {}, // Not used in after hooks
754
+ get: () => undefined, // Not used in after hooks
755
+ exitCode: 0,
756
+ };
757
+
758
+ for (const hook of afterHooks) {
759
+ await hook(hookContext);
760
+ }
761
+ }
762
+ } catch (error) {
763
+ if (mergedConfig.plugins && mergedConfig.plugins.length > 0 && context) {
764
+ await runAfterCommand(pluginManagerState, context, { exitCode: 1 });
765
+ }
766
+
767
+ // Run global after hooks on error
768
+ if (afterHooks.length > 0) {
769
+ const hookContext: HookContext<TStore> & {
770
+ exitCode: number;
771
+ error?: Error;
772
+ } = {
773
+ flags: (resultParsed?.flags as Record<string, unknown> | undefined) || {},
774
+ store: context?.store?.getState() || ({} as TStore),
775
+ env: process.env,
776
+ cwd: process.cwd(),
777
+ set: () => {}, // Not used in after hooks
778
+ get: () => undefined, // Not used in after hooks
779
+ exitCode: 1,
780
+ error: error instanceof Error ? error : new Error(String(error)),
781
+ };
782
+
783
+ for (const hook of afterHooks) {
784
+ await hook(hookContext);
785
+ }
786
+ }
787
+
788
+ if (error instanceof SchemaError) {
789
+ console.error(relico.red("Validation Error:"));
790
+ const generalErrors: string[] = [];
791
+ const fieldErrors: Record<string, string[]> = {};
792
+ for (const issue of error.issues) {
793
+ const path = getDotPath(issue);
794
+ if (path) {
795
+ if (!fieldErrors[path]) {
796
+ fieldErrors[path] = [];
797
+ }
798
+ fieldErrors[path].push(issue.message);
799
+ } else {
800
+ generalErrors.push(issue.message);
801
+ }
802
+ }
803
+ for (const [field, messages] of Object.entries(fieldErrors)) {
804
+ console.error(relico.dim(` ${field}:`));
805
+ for (const message of messages) {
806
+ console.error(relico.dim(` • ${message}`));
807
+ }
808
+ }
809
+ for (const message of generalErrors) {
810
+ console.error(relico.dim(` • ${message}`));
811
+ }
812
+ process.exit(1);
813
+ } else if (error instanceof Error) {
814
+ console.error(relico.red(`Error: ${error.message}`));
815
+ process.exit(1);
816
+ }
817
+ throw error;
818
+ }
819
+ }
820
+
821
+ const api = {
822
+ // Internal method for command registration (not part of public API)
823
+ command<TCommandStore = any>(name: string, cmd: Command<any, TCommandStore>) {
824
+ registerCommand(name, cmd, [], "directory");
825
+ },
826
+
827
+ async init() {
828
+ await loadFromConfig();
829
+ },
830
+
831
+ async run(argv = process.argv.slice(2)) {
832
+ if (argv.length === 0) {
833
+ showHelp(undefined, undefined, []);
834
+ return;
835
+ }
836
+
837
+ // Handle -- separator: split args before and after --
838
+ const separatorIndex = argv.indexOf("--");
839
+ const commandArgs = separatorIndex >= 0 ? argv.slice(0, separatorIndex) : argv;
840
+ const passthroughArgs = separatorIndex >= 0 ? argv.slice(separatorIndex + 1) : [];
841
+
842
+ // Handle version flag (only check before -- separator)
843
+ if (commandArgs.includes("--version") || commandArgs.includes("-v")) {
844
+ console.log(relico.bold(relico.cyan(`${fullConfig.name} v${fullConfig.version}`)));
845
+ return;
846
+ }
847
+
848
+ // Handle help flags (only check before -- separator)
849
+ if (commandArgs.includes("--help") || commandArgs.includes("-h")) {
850
+ const helpIndex = Math.max(commandArgs.indexOf("--help"), commandArgs.indexOf("-h"));
851
+ const cmdArgs = commandArgs.slice(0, helpIndex);
852
+
853
+ if (cmdArgs.length === 0) {
854
+ showHelp(undefined, undefined, []);
855
+ } else {
856
+ const { command, commandName, remainingArgs: _remainingArgs } = findCommand(cmdArgs);
857
+ if (command && commandName) {
858
+ const pathParts = commandName.split(" ").slice(0, -1);
859
+ showHelp(commandName, command, pathParts);
860
+ } else {
861
+ console.error(`Unknown command: ${cmdArgs.join(" ")}`);
862
+ process.exit(1);
863
+ }
864
+ }
865
+ return;
866
+ }
867
+
868
+ // Find and execute command
869
+ const { command, commandName, remainingArgs } = findCommand(commandArgs);
870
+
871
+ if (!(command && commandName)) {
872
+ console.error(`Unknown command: ${commandArgs[0]}`);
873
+ process.exit(1);
874
+ }
875
+
876
+ // If command has subcommands but no handler, show help
877
+ // Use prefix tree index for O(1) lookup instead of scanning all commands
878
+ const hasSubcommands =
879
+ subcommandIndex.has(commandName) &&
880
+ Array.from(subcommandIndex.get(commandName)!).some(
881
+ (name) => name.split(" ").length === commandName.split(" ").length + 1
882
+ );
883
+ if (!(command.handler || command.render) && hasSubcommands) {
884
+ const pathParts = commandName.split(" ").slice(0, -1);
885
+ showHelp(commandName, command, pathParts);
886
+ return;
887
+ }
888
+
889
+ if (command.handler || command.render) {
890
+ // Combine remaining args from command parsing with passthrough args
891
+ const allArgs = [...remainingArgs, ...passthroughArgs];
892
+ await runCommandInternal(commandName, command, allArgs);
893
+ }
894
+ },
895
+
896
+ async execute(
897
+ commandName: string,
898
+ argsOrOptions?: string[] | Record<string, any>,
899
+ options?: Record<string, any>
900
+ ) {
901
+ // Parse command name to handle nested commands (git/sync -> git sync)
902
+ const commandPath = commandName.replace(/\//g, " ").split(" ");
903
+ const { command, commandName: foundCommandName, remainingArgs } = findCommand(commandPath);
904
+ if (!(command && foundCommandName)) {
905
+ throw new Error(`Command '${commandName}' not found`);
906
+ }
907
+
908
+ // Handle different overload patterns
909
+ let finalArgs: string[] = [];
910
+ let finalOptions: Record<string, any> = {};
911
+
912
+ if (argsOrOptions && !Array.isArray(argsOrOptions)) {
913
+ // Pattern: execute(commandName, options)
914
+ finalOptions = argsOrOptions as Record<string, any>;
915
+ } else if (Array.isArray(argsOrOptions) && options) {
916
+ // Pattern: execute(commandName, args, options)
917
+ finalArgs = argsOrOptions;
918
+ finalOptions = options;
919
+ } else if (Array.isArray(argsOrOptions)) {
920
+ // Pattern: execute(commandName, args)
921
+ finalArgs = argsOrOptions;
922
+ }
923
+
924
+ // If options object provided, use directly as flags
925
+ if (Object.keys(finalOptions).length > 0) {
926
+ // Merge global flags with command options
927
+ const mergedOptions = {
928
+ ...GLOBAL_FLAGS,
929
+ ...(command.options || {}),
930
+ } as MergedOptions<(typeof command.options & Options) | Options>;
931
+
932
+ // Parse with empty args to get defaults, then merge options
933
+ const parsed = await parseArgs([], mergedOptions, foundCommandName);
934
+ Object.assign(parsed.flags, finalOptions);
935
+
936
+ const { prompt, spinner } = await import("@reliverse/rempts-utils");
937
+
938
+ // Run beforeCommand hooks if plugins are loaded
939
+ let context: CommandContext<TStore> | undefined;
940
+ if (mergedConfig.plugins && mergedConfig.plugins.length > 0) {
941
+ context = await runBeforeCommand(
942
+ pluginManagerState,
943
+ foundCommandName,
944
+ command,
945
+ [],
946
+ parsed.flags
947
+ );
948
+ }
949
+
950
+ // Run global before hooks
951
+ if (beforeHooks.length > 0) {
952
+ // Reset global hook context for this command
953
+ globalHookContext = {};
954
+
955
+ const hookContext: HookContext<TStore> = {
956
+ flags: parsed.flags,
957
+ store: context?.store?.getState() || ({} as TStore),
958
+ env: process.env,
959
+ cwd: process.cwd(),
960
+ set: (key: string, value: any) => {
961
+ globalHookContext[key] = value;
962
+ },
963
+ get: (key: string) => {
964
+ return globalHookContext[key];
965
+ },
966
+ };
967
+
968
+ for (const hook of beforeHooks) {
969
+ await hook(hookContext);
970
+ }
971
+ }
972
+
973
+ // Create runtime info
974
+ const runtimeInfo: RuntimeInfo = {
975
+ startTime: Date.now(),
976
+ args: [],
977
+ command: foundCommandName,
978
+ };
979
+
980
+ const terminalInfo = getTerminalInfo();
981
+
982
+ if (command.handler) {
983
+ // Type assertion: flags are validated and typed by parseArgs
984
+ const typedFlags = parsed.flags as InferMergedOptions<
985
+ (typeof command.options & Options) | Options
986
+ >;
987
+ await command.handler({
988
+ flags: typedFlags,
989
+ positional: [],
990
+ shell: Bun.$,
991
+ env: process.env,
992
+ cwd: process.cwd(),
993
+ prompt,
994
+ spinner,
995
+ colors: relico,
996
+ terminal: terminalInfo,
997
+ runtime: runtimeInfo,
998
+ ...(context ? { context } : {}),
999
+ ...(Object.keys(globalHookContext).length > 0 ? { hooks: globalHookContext } : {}),
1000
+ });
1001
+ }
1002
+
1003
+ // Run afterCommand hooks if plugins are loaded
1004
+ if (mergedConfig.plugins && mergedConfig.plugins.length > 0 && context) {
1005
+ await runAfterCommand(pluginManagerState, context, { exitCode: 0 });
1006
+ }
1007
+
1008
+ // Run global after hooks
1009
+ if (afterHooks.length > 0) {
1010
+ const hookContext: HookContext<TStore> & { exitCode: number } = {
1011
+ flags: parsed.flags,
1012
+ store: context?.store?.getState() || ({} as TStore),
1013
+ env: process.env,
1014
+ cwd: process.cwd(),
1015
+ set: () => {}, // Not used in after hooks
1016
+ get: () => undefined, // Not used in after hooks
1017
+ exitCode: 0,
1018
+ };
1019
+
1020
+ for (const hook of afterHooks) {
1021
+ await hook(hookContext);
1022
+ }
1023
+ }
1024
+ return;
1025
+ }
1026
+
1027
+ // Parse string args normally
1028
+ const args = finalArgs.length > 0 ? finalArgs : (argsOrOptions as string[] | undefined) || [];
1029
+ // Use the already found command and remaining args
1030
+ const foundCommand = command;
1031
+ const finalArgsToUse = [...remainingArgs, ...args];
1032
+
1033
+ // Execute the command using the same logic as the run method
1034
+ if (foundCommand.handler || foundCommand.render) {
1035
+ await runCommandInternal(foundCommandName, foundCommand, finalArgsToUse);
1036
+ }
1037
+ },
1038
+
1039
+ before(hook: BeforeHook<TStore>) {
1040
+ beforeHooks.push(hook);
1041
+ },
1042
+
1043
+ after(hook: AfterHook<TStore>) {
1044
+ afterHooks.push(hook);
1045
+ },
1046
+ };
1047
+
1048
+ return api;
1049
+ }