@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,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
|
+
}
|