@reliverse/rempts-core 1.6.1 → 2.3.2

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
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Plugin lifecycle manager - functional implementation
3
+ */
4
+
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
+ import type { RemptsConfig, ResolvedConfig } from "../types";
8
+ import { createLogger } from "../utils/logger";
9
+ import { deepMerge } from "../utils/merge";
10
+ import { createCommandContext, createEnvironmentInfo } from "./context";
11
+ import { createPluginLoader } from "./loader";
12
+ import { combinePluginStores, type PluginStore } from "./store.js";
13
+ import type {
14
+ CommandDefinition,
15
+ CommandResult,
16
+ Middleware,
17
+ Plugin,
18
+ PluginConfig,
19
+ PluginHooks,
20
+ } from "./types";
21
+
22
+ export interface PluginSetupResult {
23
+ config: Partial<RemptsConfig>;
24
+ commands: CommandDefinition[];
25
+ middlewares: Middleware[];
26
+ }
27
+
28
+ export interface PluginManagerState<TStore = {}> {
29
+ plugins: Plugin[];
30
+ pluginHooks: PluginHooks[];
31
+ combinedStore?: PluginStore<TStore>;
32
+ loader: ReturnType<typeof createPluginLoader>;
33
+ logger: ReturnType<typeof createLogger>;
34
+ }
35
+
36
+ /**
37
+ * Create a new plugin manager state
38
+ */
39
+ export function createPluginManager<TStore = {}>(): PluginManagerState<TStore> {
40
+ return {
41
+ plugins: [],
42
+ pluginHooks: [],
43
+ combinedStore: undefined,
44
+ loader: createPluginLoader(),
45
+ logger: createLogger("plugin-manager"),
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Load and validate plugins
51
+ */
52
+ export async function loadPlugins<TStore>(
53
+ state: PluginManagerState<TStore>,
54
+ configs: PluginConfig[]
55
+ ): Promise<void> {
56
+ // Load all plugins
57
+ const loadPromises = configs.map(async (config) => {
58
+ try {
59
+ const plugin = await state.loader.loadPlugin(config);
60
+ state.loader.validatePlugin(plugin);
61
+ return plugin;
62
+ } catch (error: any) {
63
+ state.logger.error(`Failed to load plugin: ${error.message}`);
64
+ throw error;
65
+ }
66
+ });
67
+
68
+ state.plugins = await Promise.all(loadPromises);
69
+
70
+ // Instantiate hooks for all plugins
71
+ state.pluginHooks = state.plugins.map((plugin) => plugin());
72
+
73
+ // Combine all plugin stores using Zustand
74
+ const stores: Record<string, PluginStore<any>> = {};
75
+ state.pluginHooks.forEach((hooks, index) => {
76
+ if (hooks.store) {
77
+ stores[`plugin_${index}`] = hooks.store;
78
+ }
79
+ });
80
+
81
+ if (Object.keys(stores).length > 0) {
82
+ state.combinedStore = combinePluginStores(stores) as any;
83
+ }
84
+
85
+ state.logger.info(`Loaded ${state.plugins.length} plugins`);
86
+ }
87
+
88
+ /**
89
+ * Run setup hooks for all plugins
90
+ */
91
+ export async function runSetup<TStore>(
92
+ state: PluginManagerState<TStore>,
93
+ config: Partial<RemptsConfig>
94
+ ): Promise<PluginSetupResult> {
95
+ // Create a stateful context that can collect results
96
+ const setupState = {
97
+ configUpdates: [] as Partial<RemptsConfig>[],
98
+ commands: [] as CommandDefinition[],
99
+ middlewares: [] as Middleware[],
100
+ };
101
+
102
+ const context = {
103
+ config,
104
+ store: state.combinedStore,
105
+ logger: createLogger("plugins"),
106
+ paths: {
107
+ cwd: process.cwd(),
108
+ home: homedir(),
109
+ config: join(homedir(), ".config", config.name || "rempts"),
110
+ },
111
+
112
+ updateConfig(partial: Partial<RemptsConfig>): void {
113
+ setupState.configUpdates.push(partial);
114
+ },
115
+
116
+ registerCommand(command: CommandDefinition): void {
117
+ setupState.commands.push(command);
118
+ },
119
+
120
+ use(middleware: Middleware): void {
121
+ setupState.middlewares.push(middleware);
122
+ },
123
+ };
124
+
125
+ // Run all setup hooks
126
+ for (const hooks of state.pluginHooks) {
127
+ if (hooks.setup) {
128
+ state.logger.debug(`Running setup for plugin`);
129
+ try {
130
+ await hooks.setup(context as any);
131
+ } catch (error: any) {
132
+ throw new Error(`Plugin setup failed: ${error.message}`);
133
+ }
134
+ }
135
+ }
136
+
137
+ // Merge all config updates
138
+ const mergedConfig =
139
+ setupState.configUpdates.length > 0 ? deepMerge(config, ...setupState.configUpdates) : config;
140
+
141
+ return {
142
+ config: mergedConfig,
143
+ commands: setupState.commands,
144
+ middlewares: setupState.middlewares,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Run configResolved hooks
150
+ */
151
+ export async function runConfigResolved<TStore>(
152
+ state: PluginManagerState<TStore>,
153
+ config: ResolvedConfig
154
+ ): Promise<void> {
155
+ for (const hooks of state.pluginHooks) {
156
+ if (hooks.configResolved) {
157
+ state.logger.debug(`Running configResolved for plugin`);
158
+ try {
159
+ await hooks.configResolved(config);
160
+ } catch (error: any) {
161
+ // Log but don't fail - config is already resolved
162
+ state.logger.error(`Plugin configResolved error: ${error.message}`);
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Run beforeCommand hooks
170
+ */
171
+ export async function runBeforeCommand<TStore>(
172
+ state: PluginManagerState<TStore>,
173
+ command: string,
174
+ commandDef: any,
175
+ args: string[],
176
+ flags: Record<string, any>
177
+ ): Promise<ReturnType<typeof createCommandContext<TStore>>> {
178
+ // Use the combined store directly (Zustand handles immutability)
179
+ const commandStore = state.combinedStore;
180
+
181
+ const context = createCommandContext<TStore>(
182
+ command,
183
+ commandDef,
184
+ args,
185
+ flags,
186
+ createEnvironmentInfo(),
187
+ commandStore as any
188
+ );
189
+
190
+ // Run all beforeCommand hooks
191
+ for (const hooks of state.pluginHooks) {
192
+ if (hooks.beforeCommand) {
193
+ state.logger.debug(`Running beforeCommand for plugin`);
194
+ try {
195
+ await hooks.beforeCommand(context as any);
196
+ } catch (error: any) {
197
+ throw new Error(`Plugin beforeCommand failed: ${error.message}`);
198
+ }
199
+ }
200
+ }
201
+
202
+ return context;
203
+ }
204
+
205
+ /**
206
+ * Run afterCommand hooks
207
+ */
208
+ export async function runAfterCommand<TStore>(
209
+ state: PluginManagerState<TStore>,
210
+ context: ReturnType<typeof createCommandContext<TStore>>,
211
+ result: CommandResult
212
+ ): Promise<void> {
213
+ const fullContext = Object.assign(context, result);
214
+
215
+ // Run all afterCommand hooks
216
+ for (const hooks of state.pluginHooks) {
217
+ if (hooks.afterCommand) {
218
+ state.logger.debug(`Running afterCommand for plugin`);
219
+ try {
220
+ await hooks.afterCommand(fullContext as any);
221
+ } catch (error: any) {
222
+ // Log error but don't fail - command already executed
223
+ state.logger.error(`Plugin afterCommand error: ${error.message}`);
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Get loaded plugins (for debugging/listing)
231
+ */
232
+ export function getPlugins<TStore>(state: PluginManagerState<TStore>): readonly Plugin[] {
233
+ return state.plugins;
234
+ }
235
+
236
+ /**
237
+ * Get plugin hooks by index
238
+ */
239
+ export function getPlugin<TStore>(
240
+ state: PluginManagerState<TStore>,
241
+ index: number
242
+ ): PluginHooks | undefined {
243
+ return state.pluginHooks[index];
244
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Plugin system public API
3
+ */
4
+
5
+ export {
6
+ createCommandContext,
7
+ createEnvironmentInfo,
8
+ } from "./context";
9
+ // Plugin development utilities
10
+ export { composePlugins, createPlugin, createTestPlugin } from "./create";
11
+ export {
12
+ createPluginManager,
13
+ getPlugin,
14
+ getPlugins,
15
+ loadPlugins,
16
+ runAfterCommand,
17
+ runBeforeCommand,
18
+ runConfigResolved,
19
+ runSetup,
20
+ } from "./manager";
21
+ // Store utilities
22
+ export {
23
+ combinePluginStores,
24
+ createPluginStore,
25
+ createPluginStoreWithLogging,
26
+ type PluginStore,
27
+ type PluginStoreState,
28
+ } from "./store";
29
+ // Plugin testing utilities
30
+ export {
31
+ assertPluginBehavior,
32
+ createMockCommandContext,
33
+ createMockPluginContext,
34
+ testPluginHooks,
35
+ } from "./testing";
36
+ // Re-export for convenience
37
+ export type {
38
+ CommandContext as ICommandContext,
39
+ CommandResult,
40
+ EnvironmentInfo,
41
+ MergePluginStores,
42
+ MergeStores,
43
+ Middleware,
44
+ PathInfo,
45
+ Plugin,
46
+ PluginConfig,
47
+ PluginContext as IPluginContext,
48
+ PluginFactory,
49
+ PluginHooks,
50
+ } from "./types";
51
+ export * from "./types";
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Zustand store utilities for Rempts plugin system
3
+ */
4
+
5
+ import { createStore, type StoreApi } from "zustand";
6
+
7
+ /**
8
+ * Generic Zustand store interface for plugins
9
+ */
10
+ export interface PluginStore<TState = any> extends StoreApi<TState> {
11
+ /**
12
+ * Get the current state
13
+ */
14
+ getState(): TState;
15
+
16
+ /**
17
+ * Subscribe to state changes
18
+ */
19
+ subscribe(listener: (state: TState, prevState: TState) => void): () => void;
20
+
21
+ /**
22
+ * Set new state
23
+ */
24
+ setState(updater: TState | ((prevState: TState) => TState)): void;
25
+
26
+ /**
27
+ * Destroy the store
28
+ */
29
+ destroy(): void;
30
+ }
31
+
32
+ /**
33
+ * Create a Zustand store for a plugin
34
+ */
35
+ export function createPluginStore<TState>(initialState: TState): PluginStore<TState> {
36
+ const store = createStore<TState>()(() => initialState);
37
+
38
+ return {
39
+ ...store,
40
+ destroy: () => {
41
+ // Zustand stores don't have a built-in destroy method
42
+ // but we provide this for consistency
43
+ },
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Combine multiple plugin stores into a single store
49
+ */
50
+ export function combinePluginStores<TCombined>(
51
+ stores: Record<string, PluginStore<any>>
52
+ ): PluginStore<TCombined> {
53
+ // Create initial combined state
54
+ const initialState = Object.keys(stores).reduce((acc, key) => {
55
+ const store = stores[key];
56
+ if (store) {
57
+ acc[key] = store.getState();
58
+ }
59
+ return acc;
60
+ }, {} as any);
61
+
62
+ const combinedStore = createStore<TCombined>()(() => initialState);
63
+
64
+ // Subscribe to all individual stores and update combined store
65
+ const unsubscribers = Object.keys(stores).map((key) => {
66
+ const store = stores[key];
67
+ if (store) {
68
+ return store.subscribe((state) => {
69
+ combinedStore.setState((prevState: TCombined) => ({
70
+ ...prevState,
71
+ [key]: state,
72
+ }));
73
+ });
74
+ }
75
+ return () => {}; // Return empty unsubscribe function if store is undefined
76
+ });
77
+
78
+ return {
79
+ ...combinedStore,
80
+ destroy: () => {
81
+ unsubscribers.forEach((unsubscribe) => unsubscribe());
82
+ },
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Type helper for plugin store state
88
+ */
89
+ export type PluginStoreState<TStore extends PluginStore<any>> = ReturnType<TStore["getState"]>;
90
+
91
+ /**
92
+ * Middleware for logging store changes (useful for debugging)
93
+ */
94
+ export function createLoggingMiddleware<TState>() {
95
+ return (config: any) => (set: any, get: any, api: any) =>
96
+ config(
97
+ (...args: any[]) => {
98
+ console.log("Store before:", get());
99
+ set(...args);
100
+ console.log("Store after:", get());
101
+ },
102
+ get,
103
+ api
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Create a store with logging enabled
109
+ */
110
+ export function createPluginStoreWithLogging<TState>(
111
+ initialState: TState,
112
+ enableLogging = false
113
+ ): PluginStore<TState> {
114
+ if (enableLogging) {
115
+ const store = createStore<TState>()(createLoggingMiddleware<TState>()(() => initialState));
116
+
117
+ return {
118
+ ...store,
119
+ destroy: () => {},
120
+ };
121
+ }
122
+
123
+ return createPluginStore(initialState);
124
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Plugin testing utilities for Rempts
3
+ */
4
+
5
+ import type { RemptsConfig, ResolvedConfig } from "../types";
6
+ import { createLogger } from "../utils/logger";
7
+ import { createPluginStore, type PluginStore } from "./store.js";
8
+ import type { CommandContext, Plugin, PluginContext } from "./types";
9
+
10
+ /**
11
+ * Mock plugin context for testing
12
+ */
13
+ export function createMockPluginContext(
14
+ config: Partial<RemptsConfig> = {},
15
+ store: PluginStore<any> = createPluginStore({})
16
+ ): PluginContext {
17
+ return {
18
+ config,
19
+ updateConfig: () => {},
20
+ registerCommand: () => {},
21
+ use: () => {},
22
+ store,
23
+ logger: createLogger("test"),
24
+ paths: {
25
+ cwd: process.cwd(),
26
+ home: process.env.HOME || "/tmp",
27
+ config: "/tmp/.config/rempts",
28
+ },
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Mock command context for testing
34
+ */
35
+ export function createMockCommandContext<TStore = {}>(
36
+ command = "test",
37
+ args: string[] = [],
38
+ flags: Record<string, any> = {},
39
+ initialStore: TStore = {} as TStore
40
+ ): CommandContext<TStore> {
41
+ // Create a mock store that behaves like a Zustand store
42
+ let storeState = { ...initialStore };
43
+ const mockStore = {
44
+ getState: () => storeState,
45
+ setState: (updater: any) => {
46
+ if (typeof updater === "function") {
47
+ storeState = { ...storeState, ...updater(storeState) };
48
+ } else {
49
+ storeState = { ...storeState, ...updater };
50
+ }
51
+ },
52
+ subscribe: () => () => {}, // Mock subscription
53
+ destroy: () => {},
54
+ };
55
+
56
+ return {
57
+ command,
58
+ commandDef: {} as any,
59
+ args,
60
+ flags,
61
+ env: {
62
+ isCI: false,
63
+ isAIAgent: false,
64
+ aiAgents: [],
65
+ },
66
+ store: mockStore as any,
67
+ getStoreValue: (key: keyof TStore | string | number | symbol) => (storeState as any)[key],
68
+ setStoreValue: (key: keyof TStore | string | number | symbol, value: any) => {
69
+ mockStore.setState({ [key]: value });
70
+ },
71
+ hasStoreValue: (key: keyof TStore | string | number | symbol) => key in (storeState as object),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Test plugin lifecycle hooks
77
+ */
78
+ export async function testPluginHooks<TStore = {}>(
79
+ plugin: Plugin<TStore>,
80
+ options: {
81
+ config?: Partial<RemptsConfig>;
82
+ store?: TStore;
83
+ command?: string;
84
+ args?: string[];
85
+ flags?: Record<string, any>;
86
+ } = {}
87
+ ) {
88
+ const hooks = plugin();
89
+ const results: {
90
+ setup?: any;
91
+ configResolved?: any;
92
+ beforeCommand?: any;
93
+ afterCommand?: any;
94
+ } = {};
95
+
96
+ // Test setup hook
97
+ if (hooks.setup) {
98
+ const context = createMockPluginContext(options.config);
99
+ try {
100
+ await hooks.setup(context);
101
+ results.setup = { success: true, context };
102
+ } catch (error) {
103
+ results.setup = { success: false, error };
104
+ }
105
+ }
106
+
107
+ // Test configResolved hook
108
+ if (hooks.configResolved) {
109
+ const config: ResolvedConfig = {
110
+ name: "test-cli",
111
+ version: "1.0.0",
112
+ description: "Test CLI",
113
+ commands: {},
114
+ build: {
115
+ targets: ["native"],
116
+ compress: false,
117
+ minify: false,
118
+ sourcemap: true,
119
+ },
120
+ dev: {
121
+ watch: true,
122
+ inspect: false,
123
+ },
124
+ test: {
125
+ pattern: ["**/*.test.ts", "**/*.spec.ts"],
126
+ coverage: false,
127
+ watch: false,
128
+ },
129
+ workspace: {
130
+ versionStrategy: "fixed",
131
+ },
132
+ release: {
133
+ npm: true,
134
+ github: false,
135
+ tagFormat: "v{{version}}",
136
+ conventionalCommits: true,
137
+ },
138
+ plugins: [],
139
+ };
140
+ try {
141
+ await hooks.configResolved(config);
142
+ results.configResolved = { success: true, config };
143
+ } catch (error) {
144
+ results.configResolved = { success: false, error };
145
+ }
146
+ }
147
+
148
+ // Test beforeCommand hook
149
+ if (hooks.beforeCommand) {
150
+ const context = createMockCommandContext(
151
+ options.command || "test",
152
+ options.args || [],
153
+ options.flags || {},
154
+ options.store || ({} as TStore)
155
+ );
156
+ try {
157
+ await hooks.beforeCommand(context);
158
+ results.beforeCommand = { success: true, context };
159
+ } catch (error) {
160
+ results.beforeCommand = { success: false, error };
161
+ }
162
+ }
163
+
164
+ // Test afterCommand hook
165
+ if (hooks.afterCommand) {
166
+ const context = createMockCommandContext(
167
+ options.command || "test",
168
+ options.args || [],
169
+ options.flags || {},
170
+ options.store || ({} as TStore)
171
+ );
172
+ try {
173
+ await hooks.afterCommand(context as any);
174
+ results.afterCommand = { success: true, context };
175
+ } catch (error) {
176
+ results.afterCommand = { success: false, error };
177
+ }
178
+ }
179
+
180
+ return results;
181
+ }
182
+
183
+ /**
184
+ * Assert plugin behavior in tests
185
+ */
186
+ export function assertPluginBehavior(
187
+ results: Awaited<ReturnType<typeof testPluginHooks>>,
188
+ expectations: {
189
+ setupShouldSucceed?: boolean;
190
+ configResolvedShouldSucceed?: boolean;
191
+ beforeCommandShouldSucceed?: boolean;
192
+ afterCommandShouldSucceed?: boolean;
193
+ }
194
+ ) {
195
+ const assertions: string[] = [];
196
+
197
+ if (expectations.setupShouldSucceed !== undefined) {
198
+ const actual = results.setup?.success ?? false;
199
+ if (actual !== expectations.setupShouldSucceed) {
200
+ assertions.push(
201
+ `Setup hook ${actual ? "succeeded" : "failed"} but expected ${expectations.setupShouldSucceed ? "success" : "failure"}`
202
+ );
203
+ }
204
+ }
205
+
206
+ if (expectations.configResolvedShouldSucceed !== undefined) {
207
+ const actual = results.configResolved?.success ?? false;
208
+ if (actual !== expectations.configResolvedShouldSucceed) {
209
+ assertions.push(
210
+ `ConfigResolved hook ${actual ? "succeeded" : "failed"} but expected ${expectations.configResolvedShouldSucceed ? "success" : "failure"}`
211
+ );
212
+ }
213
+ }
214
+
215
+ if (expectations.beforeCommandShouldSucceed !== undefined) {
216
+ const actual = results.beforeCommand?.success ?? false;
217
+ if (actual !== expectations.beforeCommandShouldSucceed) {
218
+ assertions.push(
219
+ `BeforeCommand hook ${actual ? "succeeded" : "failed"} but expected ${expectations.beforeCommandShouldSucceed ? "success" : "failure"}`
220
+ );
221
+ }
222
+ }
223
+
224
+ if (expectations.afterCommandShouldSucceed !== undefined) {
225
+ const actual = results.afterCommand?.success ?? false;
226
+ if (actual !== expectations.afterCommandShouldSucceed) {
227
+ assertions.push(
228
+ `AfterCommand hook ${actual ? "succeeded" : "failed"} but expected ${expectations.afterCommandShouldSucceed ? "success" : "failure"}`
229
+ );
230
+ }
231
+ }
232
+
233
+ if (assertions.length > 0) {
234
+ throw new Error(`Plugin behavior assertions failed:\n${assertions.join("\n")}`);
235
+ }
236
+ }