@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,50 @@
1
+ import { type } from "arktype";
2
+ import type { CLIOption } from "./types";
3
+
4
+ /**
5
+ * Built-in global flags available to all commands
6
+ * Enhanced with better descriptions and validation
7
+ */
8
+ export const GLOBAL_FLAGS = {
9
+ interactive: {
10
+ schema: type("boolean | undefined").configure({
11
+ description: "enable interactive terminal user interface mode",
12
+ }),
13
+ short: "i",
14
+ description: "Run in interactive TUI mode",
15
+ },
16
+ tui: {
17
+ schema: type("boolean | undefined").configure({
18
+ description: "force terminal user interface mode",
19
+ }),
20
+ description: "Force TUI mode (same as --interactive)",
21
+ },
22
+ "no-tui": {
23
+ schema: type("boolean | undefined").configure({
24
+ description: "disable terminal user interface mode",
25
+ }),
26
+ description: "Disable TUI mode, use CLI handler instead",
27
+ },
28
+ help: {
29
+ schema: type("boolean | undefined").configure({
30
+ description: "display help information",
31
+ }),
32
+ short: "h",
33
+ description: "Show help",
34
+ },
35
+ version: {
36
+ schema: type("boolean | undefined").configure({
37
+ description: "display version information",
38
+ }),
39
+ short: "v",
40
+ description: "Show version",
41
+ },
42
+ } satisfies Record<string, CLIOption>;
43
+
44
+ export interface GlobalFlags {
45
+ interactive?: boolean;
46
+ tui?: boolean;
47
+ "no-tui"?: boolean;
48
+ help?: boolean;
49
+ version?: boolean;
50
+ }
package/src/mod.ts ADDED
@@ -0,0 +1,74 @@
1
+ // Note: createCLI is now async and returns Promise<CLI>
2
+
3
+ export { SchemaError } from "@standard-schema/utils";
4
+ export { createApp, createCLI } from "./cli";
5
+ export { defineConfig, type RemptsConfig, remptsConfigSchema } from "./config";
6
+ export { type LoadedConfig, loadConfig } from "./config-loader";
7
+ export type {
8
+ CommandConflict,
9
+ CommandFileInfo,
10
+ CommandFileTree,
11
+ } from "./file-loader";
12
+ export { createFileCommandLoader, loadCommandsFromDirectory } from "./file-loader";
13
+ export type { GlobalFlags } from "./global-flags";
14
+ // Export global flags
15
+ export { GLOBAL_FLAGS } from "./global-flags";
16
+ // Export TUI registry
17
+ export {
18
+ clearTuiRenderer,
19
+ getTuiRenderer,
20
+ registerTuiRenderer,
21
+ } from "./tui/registry";
22
+ export type {
23
+ CLI,
24
+ CLIOption,
25
+ Command,
26
+ CommandOptions,
27
+ Handler,
28
+ HandlerArgs,
29
+ Options,
30
+ PluginConfig,
31
+ RegisteredCommands,
32
+ RenderArgs,
33
+ RenderFunction,
34
+ RenderResult,
35
+ ResolvedConfig,
36
+ RuntimeInfo,
37
+ TerminalInfo,
38
+ } from "./types";
39
+ export { defineCommand, option } from "./types";
40
+
41
+ // Note: Plugin system is exported via subpath export
42
+ // Usage: import { PluginManager, createPlugin } from '@reliverse/rempts-core/plugin'
43
+
44
+ // Export type utilities
45
+ export type {
46
+ Assign,
47
+ Constrain,
48
+ DeepPartial,
49
+ Expand,
50
+ ExtractObjects,
51
+ ExtractPrimitives,
52
+ IntersectAssign,
53
+ IsAny,
54
+ IsNonEmptyObject,
55
+ IsUnion,
56
+ MakeDifferenceOptional,
57
+ MergeAll,
58
+ MergeAllObjects,
59
+ NoInfer,
60
+ PartialMergeAll,
61
+ PickAsRequired,
62
+ PickOptional,
63
+ PickRequired,
64
+ UnionToIntersection,
65
+ WithoutEmpty,
66
+ } from "./utils/type-helpers";
67
+ // Export validation utilities
68
+ export {
69
+ createBatchValidator,
70
+ createValidator,
71
+ isValueOfType,
72
+ validateValue,
73
+ validateValues,
74
+ } from "./validation";
package/src/parser.ts ADDED
@@ -0,0 +1,212 @@
1
+ import type { InferOptions, Options, StandardSchemaV1 } from "./types";
2
+ import { RemptsValidationError } from "./types";
3
+
4
+ export interface ParsedArgs<TOptions extends Options = Options> {
5
+ flags: InferOptions<TOptions>;
6
+ positional: string[];
7
+ }
8
+
9
+ export async function parseArgs<TOptions extends Options = Options>(
10
+ args: string[],
11
+ options: TOptions,
12
+ commandName = "unknown"
13
+ ): Promise<ParsedArgs<TOptions>> {
14
+ const flags: Record<string, unknown> = {};
15
+ const positional: string[] = [];
16
+
17
+ // Build lookup maps for short aliases
18
+ const shortToName = new Map<string, string>();
19
+ for (const [name, opt] of Object.entries(options)) {
20
+ if (opt.short) {
21
+ shortToName.set(opt.short, name);
22
+ }
23
+ }
24
+
25
+ // Parse arguments
26
+ let stopParsingFlags = false;
27
+ for (let i = 0; i < args.length; i++) {
28
+ const arg = args[i];
29
+ if (!arg) {
30
+ continue;
31
+ }
32
+
33
+ // Handle -- separator: everything after is positional
34
+ if (arg === "--") {
35
+ stopParsingFlags = true;
36
+ continue;
37
+ }
38
+
39
+ // After -- separator, treat everything as positional
40
+ if (stopParsingFlags) {
41
+ positional.push(arg);
42
+ continue;
43
+ }
44
+
45
+ if (arg.startsWith("--")) {
46
+ // Long flag: --name or --name=value
47
+ const eqIndex = arg.indexOf("=");
48
+ const name = eqIndex > 0 ? arg.slice(2, eqIndex) : arg.slice(2);
49
+ const inlineValue = eqIndex > 0 ? arg.slice(eqIndex + 1) : undefined;
50
+
51
+ if (!(name && options[name])) {
52
+ continue;
53
+ }
54
+
55
+ // Get the value (inline, next arg, or 'true' for boolean-like flags)
56
+ let value: string | undefined = inlineValue;
57
+ if (value === undefined && i + 1 < args.length && !args[i + 1]?.startsWith("-")) {
58
+ value = args[++i];
59
+ }
60
+
61
+ // Pass the value to the schema for validation
62
+ flags[name] = await validateOption(name, value ?? "true", options[name]?.schema, commandName);
63
+ } else if (arg.startsWith("-") && arg.length > 1) {
64
+ // Short flag: -n or -n value
65
+ const short = arg.slice(1);
66
+ const name = shortToName.get(short);
67
+
68
+ if (name && options[name]) {
69
+ // Get the next argument as value if available
70
+ let value: string | undefined;
71
+ if (i + 1 < args.length && !args[i + 1]?.startsWith("-")) {
72
+ value = args[++i];
73
+ }
74
+
75
+ flags[name] = await validateOption(
76
+ name,
77
+ value ?? "true",
78
+ options[name]?.schema,
79
+ commandName
80
+ );
81
+ }
82
+ } else {
83
+ // Positional argument
84
+ positional.push(arg);
85
+ }
86
+ }
87
+
88
+ // Validate all options were provided (schemas handle their own defaults/required logic)
89
+ // We run validation with undefined for options not provided on command line
90
+ // If a schema has a default value, it will be used during validation
91
+ for (const [name, opt] of Object.entries(options)) {
92
+ if (!(name in flags)) {
93
+ // Check if the option has an explicit default value
94
+ if (opt.default !== undefined) {
95
+ // Validate the default value against the schema
96
+ const defaultValidated = await validateOption(name, opt.default, opt.schema, commandName);
97
+ flags[name] = defaultValidated;
98
+ } else {
99
+ // No explicit default, validate undefined
100
+ const validatedValue = await validateOption(name, undefined, opt.schema, commandName);
101
+
102
+ // For boolean flags that weren't provided, default to false instead of undefined
103
+ // This is a common CLI pattern and makes boolean flags more intuitive to use
104
+ if (validatedValue === undefined) {
105
+ // Check if the schema accepts boolean values
106
+ const booleanTest = await opt.schema["~standard"].validate(false);
107
+ if (booleanTest.issues) {
108
+ // Schema doesn't accept boolean, keep undefined
109
+ flags[name] = undefined;
110
+ } else {
111
+ // Schema accepts boolean, so default to false
112
+ flags[name] = false;
113
+ }
114
+ } else {
115
+ // Validation returned a value (could be a default from the schema)
116
+ flags[name] = validatedValue;
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ // Type assertion: flags are validated at runtime, so we can safely assert the type
123
+ return { flags: flags as InferOptions<TOptions>, positional };
124
+ }
125
+
126
+ async function validateOption(
127
+ name: string,
128
+ value: unknown,
129
+ schema: StandardSchemaV1,
130
+ commandName = "unknown"
131
+ ): Promise<unknown> {
132
+ // Convert string 'true'/'false' to boolean for boolean schemas
133
+ let processedValue = value;
134
+ if (typeof value === "string" && (value === "true" || value === "false")) {
135
+ // Check if the schema expects a boolean by trying to validate true
136
+ const testResult = await schema["~standard"].validate(true);
137
+ if (!testResult.issues) {
138
+ // Schema accepts boolean, convert the string
139
+ processedValue = value === "true";
140
+ }
141
+ }
142
+
143
+ // Use Standard Schema validation
144
+ const result = await schema["~standard"].validate(processedValue);
145
+
146
+ if (result.issues && result.issues.length > 0) {
147
+ const issue = result.issues[0];
148
+ if (!issue) {
149
+ return processedValue; // Fallback if no issues
150
+ }
151
+
152
+ const expectedType = extractSchemaType(schema);
153
+ const hint = generateHint(schema, value);
154
+
155
+ throw new RemptsValidationError(`Invalid option '${name}': ${issue.message}`, {
156
+ option: name,
157
+ value,
158
+ command: commandName,
159
+ expectedType,
160
+ hint,
161
+ });
162
+ }
163
+
164
+ return "value" in result ? result.value : processedValue;
165
+ }
166
+
167
+ /**
168
+ * Extract a human-readable type description from a schema
169
+ */
170
+ function extractSchemaType(schema: StandardSchemaV1): string {
171
+ // Try to infer type from the schema structure
172
+ if ("type" in schema && typeof schema.type === "string") {
173
+ return schema.type;
174
+ }
175
+
176
+ // Fallback to checking common patterns
177
+ if ("enum" in schema) {
178
+ return "enum";
179
+ }
180
+ if ("items" in schema) {
181
+ return "array";
182
+ }
183
+ if ("properties" in schema) {
184
+ return "object";
185
+ }
186
+ if ("format" in schema) {
187
+ return "string";
188
+ }
189
+
190
+ return "unknown";
191
+ }
192
+
193
+ /**
194
+ * Generate a helpful hint based on the schema and value
195
+ */
196
+ function generateHint(schema: StandardSchemaV1, value: unknown): string {
197
+ const type = extractSchemaType(schema);
198
+
199
+ if (type === "boolean" && typeof value === "string") {
200
+ return "Use --flag or --no-flag for boolean options";
201
+ }
202
+ if (type === "number" && typeof value === "string") {
203
+ return "Provide a numeric value";
204
+ }
205
+ if (type === "array" && !Array.isArray(value)) {
206
+ return "Provide a comma-separated list of values";
207
+ }
208
+ if (type === "enum" && typeof value === "string") {
209
+ return "Choose from the available options";
210
+ }
211
+ return "";
212
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Plugin context implementations - functional approach
3
+ */
4
+
5
+ import type { PluginStore } from "./store.js";
6
+ import type { EnvironmentInfo, CommandContext as ICommandContext } from "./types";
7
+
8
+ /**
9
+ * Create environment info for contexts
10
+ */
11
+ export function createEnvironmentInfo(): EnvironmentInfo {
12
+ const isCI = !!(
13
+ process.env.CI ||
14
+ process.env.CONTINUOUS_INTEGRATION ||
15
+ process.env.GITHUB_ACTIONS ||
16
+ process.env.GITLAB_CI ||
17
+ process.env.CIRCLECI ||
18
+ process.env.TRAVIS
19
+ );
20
+
21
+ return {
22
+ cwd: process.cwd(),
23
+ home: require("node:os").homedir(),
24
+ temp: require("node:os").tmpdir(),
25
+ platform: process.platform,
26
+ arch: process.arch,
27
+ nodeVersion: process.version,
28
+ isCI,
29
+ // Initialize plugin-extended properties with defaults
30
+ isAIAgent: false,
31
+ aiAgents: [],
32
+ };
33
+ }
34
+
35
+ // Plugin context is now created internally in the plugin manager
36
+ // to avoid interface mismatches
37
+
38
+ /**
39
+ * Create command context for command execution
40
+ */
41
+ export function createCommandContext<TStore = {}>(
42
+ command: string,
43
+ commandDef: any,
44
+ args: string[],
45
+ flags: Record<string, any>,
46
+ env: EnvironmentInfo,
47
+ store?: PluginStore<TStore>
48
+ ): ICommandContext<TStore> {
49
+ return {
50
+ command,
51
+ commandDef,
52
+ args,
53
+ flags,
54
+ env,
55
+ store,
56
+
57
+ /**
58
+ * Type-safe store value access
59
+ * Provides compile-time type checking for store properties
60
+ */
61
+ getStoreValue(key: keyof TStore | string | number | symbol): any {
62
+ if (!store) return undefined;
63
+ const state = store.getState();
64
+ return (state as any)[key];
65
+ },
66
+
67
+ /**
68
+ * Type-safe store value update
69
+ * Provides compile-time type checking for store property updates
70
+ */
71
+ setStoreValue(key: keyof TStore | string | number | symbol, value: any): void {
72
+ if (!store) return;
73
+ store.setState((prevState: TStore) => ({
74
+ ...prevState,
75
+ [key]: value,
76
+ }));
77
+ },
78
+
79
+ /**
80
+ * Check if a store property exists
81
+ */
82
+ hasStoreValue(key: keyof TStore | string | number | symbol): boolean {
83
+ if (!store) return false;
84
+ const state = store.getState();
85
+ return key in (state as object);
86
+ },
87
+ };
88
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Plugin development utilities
3
+ */
4
+
5
+ import { createPluginStore, type PluginStore } from "./store.js";
6
+ import type { Plugin, PluginFactory, PluginHooks } from "./types";
7
+
8
+ /**
9
+ * Create a plugin - supports both direct plugins and plugin factories
10
+ *
11
+ * @example Direct plugin with explicit store type:
12
+ * ```typescript
13
+ * interface MyStore {
14
+ * count: number
15
+ * message: string
16
+ * }
17
+ *
18
+ * const myPlugin = createPlugin<MyStore>({
19
+ * name: 'my-plugin',
20
+ * store: {
21
+ * count: 0,
22
+ * message: ''
23
+ * },
24
+ * beforeCommand(context) {
25
+ * context.store.count++ // TypeScript knows the type!
26
+ * }
27
+ * })
28
+ * ```
29
+ *
30
+ * @example Plugin factory with options:
31
+ * ```typescript
32
+ * const myPlugin = createPlugin((options: { prefix: string }) => ({
33
+ * name: 'my-plugin',
34
+ * store: {
35
+ * count: 0
36
+ * },
37
+ * beforeCommand(context) {
38
+ * console.log(`${options.prefix}: ${context.store.count}`)
39
+ * }
40
+ * } satisfies PluginHooks<{ count: number }>))
41
+ *
42
+ * // Use it:
43
+ * myPlugin({ prefix: 'Hello' })
44
+ * ```
45
+ */
46
+ // Overload for direct plugin
47
+ export function createPlugin<TStore = {}>(plugin: Plugin<TStore>): Plugin<TStore>;
48
+
49
+ export function createPlugin<TOptions, TStore = {}>(
50
+ factory: (options: TOptions) => Plugin<TStore>
51
+ ): (options: TOptions) => Plugin<TStore>;
52
+
53
+ export function createPlugin<T>(input: T): T {
54
+ return input;
55
+ }
56
+
57
+ /**
58
+ * Infer plugin options type from a plugin factory
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * type Options = InferPluginOptions<typeof myPlugin>
63
+ * ```
64
+ */
65
+ export type InferPluginOptions<T> = T extends PluginFactory<infer O, any> ? O : never;
66
+
67
+ /**
68
+ * Infer plugin store type
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * type Store = InferPluginStore<typeof myPlugin>
73
+ * ```
74
+ */
75
+ export type InferPluginStore<T> =
76
+ T extends Plugin<infer S> ? S : T extends PluginFactory<any, infer S> ? S : {};
77
+
78
+ /**
79
+ * Create a test plugin for development and testing
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * const testPlugin = createTestPlugin(
84
+ * { count: 0, message: '' },
85
+ * {
86
+ * beforeCommand(context) {
87
+ * context.store.count++
88
+ * console.log(`Count: ${context.store.count}`)
89
+ * }
90
+ * }
91
+ * )
92
+ * ```
93
+ */
94
+ export function createTestPlugin<TStore = {}>(
95
+ initialState: TStore,
96
+ hooks: Partial<PluginHooks<TStore>>
97
+ ): Plugin<TStore> {
98
+ return () => ({
99
+ store: createPluginStore(initialState),
100
+ ...hooks,
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Compose multiple plugins into a single plugin
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * const composedPlugin = composePlugins(
110
+ * authPlugin({ provider: 'github' }),
111
+ * loggingPlugin({ level: 'debug' }),
112
+ * metricsPlugin({ enabled: true })
113
+ * )
114
+ * ```
115
+ */
116
+ export function composePlugins<T extends Plugin[]>(...plugins: T): Plugin {
117
+ return () => {
118
+ const hooksArray = plugins.map((plugin) => plugin());
119
+
120
+ // Collect all stores from plugins
121
+ const stores: Record<string, PluginStore<any>> = {};
122
+ hooksArray.forEach((hooks, index) => {
123
+ if (hooks.store) {
124
+ stores[`plugin_${index}`] = hooks.store;
125
+ }
126
+ });
127
+
128
+ // Create combined store if there are any stores
129
+ const composedStore =
130
+ Object.keys(stores).length > 0
131
+ ? createPluginStore(
132
+ Object.keys(stores).reduce((acc, key) => {
133
+ const store = stores[key];
134
+ if (store) {
135
+ acc[key] = store.getState();
136
+ }
137
+ return acc;
138
+ }, {} as any)
139
+ )
140
+ : undefined;
141
+
142
+ return {
143
+ store: composedStore,
144
+ async setup(context) {
145
+ for (const hooks of hooksArray) {
146
+ if (hooks.setup) {
147
+ await hooks.setup(context);
148
+ }
149
+ }
150
+ },
151
+ async configResolved(config) {
152
+ for (const hooks of hooksArray) {
153
+ if (hooks.configResolved) {
154
+ await hooks.configResolved(config);
155
+ }
156
+ }
157
+ },
158
+ async beforeCommand(context) {
159
+ for (const hooks of hooksArray) {
160
+ if (hooks.beforeCommand) {
161
+ await hooks.beforeCommand(context);
162
+ }
163
+ }
164
+ },
165
+ async afterCommand(context) {
166
+ for (const hooks of hooksArray) {
167
+ if (hooks.afterCommand) {
168
+ await hooks.afterCommand(context);
169
+ }
170
+ }
171
+ },
172
+ };
173
+ };
174
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Plugin loader implementation - functional approach
3
+ */
4
+
5
+ import { join } from "node:path";
6
+ import type { Plugin, PluginConfig } from "./types";
7
+
8
+ export interface PluginLoader {
9
+ loadPlugin(config: PluginConfig): Promise<Plugin>;
10
+ validatePlugin(plugin: Plugin): void;
11
+ }
12
+
13
+ /**
14
+ * Check if a value is a valid plugin function
15
+ */
16
+ function isPluginFunction(obj: any): obj is Plugin {
17
+ return obj && typeof obj === "function";
18
+ }
19
+
20
+ /**
21
+ * Load plugin from file path
22
+ */
23
+ async function loadFromPath(path: string): Promise<Plugin> {
24
+ try {
25
+ // Handle both absolute and relative paths
26
+ const resolvedPath = path.startsWith(".") ? join(process.cwd(), path) : path;
27
+
28
+ // Dynamic import
29
+ const module = await import(resolvedPath);
30
+
31
+ // Handle various export styles
32
+ const plugin = module.default || module.plugin || module;
33
+
34
+ // If it's a factory function, call it without options
35
+ if (typeof plugin === "function" && !isPluginFunction(plugin)) {
36
+ return plugin();
37
+ }
38
+
39
+ // Validate it's a plugin function
40
+ if (!isPluginFunction(plugin)) {
41
+ throw new Error("Module does not export a valid plugin");
42
+ }
43
+
44
+ return plugin;
45
+ } catch (error: any) {
46
+ throw new Error(`Failed to load plugin from ${path}: ${error.message}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Create a plugin loader
52
+ */
53
+ export function createPluginLoader(): PluginLoader {
54
+ return {
55
+ /**
56
+ * Load a plugin from various configuration formats
57
+ */
58
+ async loadPlugin(config: PluginConfig): Promise<Plugin> {
59
+ // String path - dynamic import
60
+ if (typeof config === "string") {
61
+ return loadFromPath(config);
62
+ }
63
+
64
+ // Plugin function - use directly
65
+ if (isPluginFunction(config)) {
66
+ return config;
67
+ }
68
+
69
+ // Function - call it (legacy factory support)
70
+ if (typeof config === "function") {
71
+ return config();
72
+ }
73
+
74
+ // Array - function with options
75
+ if (Array.isArray(config) && config.length === 2) {
76
+ const [factory, options] = config;
77
+ if (typeof factory === "function") {
78
+ return factory(options);
79
+ }
80
+ }
81
+
82
+ throw new Error(`Invalid plugin configuration: ${JSON.stringify(config)}`);
83
+ },
84
+
85
+ /**
86
+ * Validate loaded plugin
87
+ */
88
+ validatePlugin(plugin: Plugin): void {
89
+ // Validate that it's a function
90
+ if (typeof plugin !== "function") {
91
+ throw new Error("Plugin must be a function");
92
+ }
93
+
94
+ // Call the plugin to get hooks and validate them
95
+ try {
96
+ const hooks = plugin();
97
+
98
+ // Check hook types
99
+ const hookNames = ["setup", "configResolved", "beforeCommand", "afterCommand"];
100
+ for (const hook of hookNames) {
101
+ const value = hooks[hook as keyof typeof hooks];
102
+ if (value !== undefined && typeof value !== "function") {
103
+ throw new Error(`Plugin hook ${hook} must be a function`);
104
+ }
105
+ }
106
+ } catch (error: any) {
107
+ throw new Error(`Plugin validation failed: ${error.message}`);
108
+ }
109
+ },
110
+ };
111
+ }