@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.
- package/README.md +398 -102
- package/dist/cli.d.ts +32 -0
- package/dist/cli.js +731 -0
- package/dist/config-loader.d.ts +42 -0
- package/dist/config-loader.js +20 -0
- package/dist/config.d.ts +99 -0
- package/dist/config.js +188 -0
- package/dist/file-loader.d.ts +43 -0
- package/dist/file-loader.js +199 -0
- package/dist/global-flags.d.ts +36 -0
- package/dist/global-flags.js +36 -0
- package/dist/mod.d.ts +13 -0
- package/dist/mod.js +19 -0
- package/dist/parser.d.ts +6 -0
- package/dist/parser.js +137 -0
- package/dist/plugin/context.d.ts +13 -0
- package/dist/plugin/context.js +53 -0
- package/dist/plugin/create.d.ts +92 -0
- package/dist/plugin/create.js +61 -0
- package/dist/plugin/loader.d.ts +12 -0
- package/dist/plugin/loader.js +65 -0
- package/dist/plugin/manager.d.ts +53 -0
- package/dist/plugin/manager.js +135 -0
- package/dist/plugin/mod.d.ts +10 -0
- package/dist/plugin/mod.js +27 -0
- package/dist/plugin/store.d.ts +45 -0
- package/dist/plugin/store.js +60 -0
- package/dist/plugin/testing.d.ts +38 -0
- package/dist/plugin/testing.js +175 -0
- package/dist/plugin/types.d.ts +146 -0
- package/dist/tui/registry.d.ts +8 -0
- package/dist/tui/registry.js +10 -0
- package/dist/tui/types.d.ts +58 -0
- package/dist/tui/types.js +10 -0
- package/dist/types.d.ts +178 -0
- package/dist/types.js +25 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/merge.d.ts +13 -0
- package/dist/utils/merge.js +25 -0
- package/dist/utils/mod.d.ts +6 -0
- package/dist/utils/mod.js +2 -0
- package/dist/utils/type-helpers.d.ts +41 -0
- package/dist/utils/type-helpers.js +0 -0
- package/dist/validation.d.ts +30 -0
- package/dist/validation.js +121 -0
- package/package.json +47 -44
- package/src/cli.ts +1049 -0
- package/src/config-loader.ts +71 -0
- package/src/config.ts +270 -0
- package/src/file-loader.ts +346 -0
- package/src/global-flags.ts +50 -0
- package/src/mod.ts +74 -0
- package/src/parser.ts +212 -0
- package/src/plugin/context.ts +88 -0
- package/src/plugin/create.ts +174 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +244 -0
- package/src/plugin/mod.ts +51 -0
- package/src/plugin/store.ts +124 -0
- package/src/plugin/testing.ts +236 -0
- package/src/plugin/types.ts +206 -0
- package/src/tui/registry.ts +22 -0
- package/src/tui/types.ts +79 -0
- package/src/types.ts +285 -0
- package/src/utils/logger.ts +43 -0
- package/src/utils/merge.ts +54 -0
- package/src/utils/mod.ts +7 -0
- package/src/utils/type-helpers.ts +151 -0
- package/src/validation.ts +177 -0
- package/LICENSE +0 -21
- package/bin/core-impl/anykey/anykey-mod.d.ts +0 -12
- package/bin/core-impl/anykey/anykey-mod.js +0 -125
- package/bin/core-impl/date/date.d.ts +0 -2
- package/bin/core-impl/date/date.js +0 -236
- package/bin/core-impl/editor/editor-mod.d.ts +0 -25
- package/bin/core-impl/editor/editor-mod.js +0 -896
- package/bin/core-impl/figures/figures-mod.d.ts +0 -233
- package/bin/core-impl/figures/figures-mod.js +0 -286
- package/bin/core-impl/figures/figures.test.d.ts +0 -1
- package/bin/core-impl/figures/figures.test.js +0 -474
- package/bin/core-impl/input/confirm-prompt.d.ts +0 -5
- package/bin/core-impl/input/confirm-prompt.js +0 -173
- package/bin/core-impl/input/input-prompt.d.ts +0 -16
- package/bin/core-impl/input/input-prompt.js +0 -370
- package/bin/core-impl/launcher/_parser.d.ts +0 -2
- package/bin/core-impl/launcher/_parser.js +0 -122
- package/bin/core-impl/launcher/_utils.d.ts +0 -8
- package/bin/core-impl/launcher/_utils.js +0 -29
- package/bin/core-impl/launcher/args.d.ts +0 -3
- package/bin/core-impl/launcher/args.js +0 -89
- package/bin/core-impl/launcher/command.d.ts +0 -8
- package/bin/core-impl/launcher/command.js +0 -68
- package/bin/core-impl/launcher/launcher-mod.d.ts +0 -8
- package/bin/core-impl/launcher/launcher-mod.js +0 -34
- package/bin/core-impl/launcher/usage.d.ts +0 -3
- package/bin/core-impl/launcher/usage.js +0 -104
- package/bin/core-impl/msg-fmt/colors.d.ts +0 -30
- package/bin/core-impl/msg-fmt/colors.js +0 -42
- package/bin/core-impl/msg-fmt/logger.d.ts +0 -17
- package/bin/core-impl/msg-fmt/logger.js +0 -106
- package/bin/core-impl/msg-fmt/mapping.d.ts +0 -3
- package/bin/core-impl/msg-fmt/mapping.js +0 -49
- package/bin/core-impl/msg-fmt/messages.d.ts +0 -35
- package/bin/core-impl/msg-fmt/messages.js +0 -314
- package/bin/core-impl/msg-fmt/terminal.d.ts +0 -15
- package/bin/core-impl/msg-fmt/terminal.js +0 -59
- package/bin/core-impl/msg-fmt/variants.d.ts +0 -11
- package/bin/core-impl/msg-fmt/variants.js +0 -52
- package/bin/core-impl/next-steps/next-steps.d.ts +0 -14
- package/bin/core-impl/next-steps/next-steps.js +0 -24
- package/bin/core-impl/number/number-mod.d.ts +0 -28
- package/bin/core-impl/number/number-mod.js +0 -197
- package/bin/core-impl/results/results.d.ts +0 -7
- package/bin/core-impl/results/results.js +0 -27
- package/bin/core-impl/select/multiselect-prompt.d.ts +0 -2
- package/bin/core-impl/select/multiselect-prompt.js +0 -341
- package/bin/core-impl/select/nummultiselect-prompt.d.ts +0 -6
- package/bin/core-impl/select/nummultiselect-prompt.js +0 -105
- package/bin/core-impl/select/numselect-prompt.d.ts +0 -7
- package/bin/core-impl/select/numselect-prompt.js +0 -115
- package/bin/core-impl/select/select-prompt.d.ts +0 -33
- package/bin/core-impl/select/select-prompt.js +0 -302
- package/bin/core-impl/select/toggle-prompt.d.ts +0 -5
- package/bin/core-impl/select/toggle-prompt.js +0 -208
- package/bin/core-impl/st-end/end.d.ts +0 -2
- package/bin/core-impl/st-end/end.js +0 -42
- package/bin/core-impl/st-end/start.d.ts +0 -17
- package/bin/core-impl/st-end/start.js +0 -66
- package/bin/core-impl/task/progress.d.ts +0 -2
- package/bin/core-impl/task/progress.js +0 -57
- package/bin/core-impl/task/spinner.d.ts +0 -15
- package/bin/core-impl/task/spinner.js +0 -110
- package/bin/core-impl/utils/colorize.d.ts +0 -2
- package/bin/core-impl/utils/colorize.js +0 -134
- package/bin/core-impl/utils/errors.d.ts +0 -1
- package/bin/core-impl/utils/errors.js +0 -15
- package/bin/core-impl/utils/prevent.d.ts +0 -10
- package/bin/core-impl/utils/prevent.js +0 -69
- package/bin/core-impl/utils/prompt-end.d.ts +0 -8
- package/bin/core-impl/utils/prompt-end.js +0 -33
- package/bin/core-impl/utils/stream-text.d.ts +0 -18
- package/bin/core-impl/utils/stream-text.js +0 -136
- package/bin/core-impl/utils/system.d.ts +0 -6
- package/bin/core-impl/utils/system.js +0 -7
- package/bin/core-impl/utils/validate.d.ts +0 -22
- package/bin/core-impl/utils/validate.js +0 -17
- package/bin/core-impl/visual/animate/animate.d.ts +0 -14
- package/bin/core-impl/visual/animate/animate.js +0 -64
- package/bin/core-impl/visual/ascii-art/ascii-art.d.ts +0 -6
- package/bin/core-impl/visual/ascii-art/ascii-art.js +0 -12
- package/bin/core-types.d.ts +0 -434
- package/bin/main.d.ts +0 -41
- package/bin/main.js +0 -96
- /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
|
+
}
|