@kustodian/cli 1.0.0

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/src/runner.ts ADDED
@@ -0,0 +1,213 @@
1
+ import { type ResultType, failure, success } from '@kustodian/core';
2
+ import type { KustodianErrorType } from '@kustodian/core';
3
+
4
+ import type { CommandType, HandlerType } from './command.js';
5
+ import type { ContainerType } from './container.js';
6
+ import {
7
+ type ContextType,
8
+ type MiddlewareType,
9
+ create_context,
10
+ create_pipeline,
11
+ } from './middleware.js';
12
+
13
+ /**
14
+ * CLI configuration.
15
+ */
16
+ export interface CLIConfigType {
17
+ name: string;
18
+ version: string;
19
+ description?: string;
20
+ }
21
+
22
+ /**
23
+ * CLI builder interface.
24
+ */
25
+ export interface CLIType {
26
+ command(cmd: CommandType): CLIType;
27
+ use(middleware: MiddlewareType): CLIType;
28
+ run(args: string[], container: ContainerType): Promise<ResultType<void, KustodianErrorType>>;
29
+ }
30
+
31
+ /**
32
+ * Creates a new CLI instance.
33
+ */
34
+ export function create_cli(_config: CLIConfigType): CLIType {
35
+ const commands: CommandType[] = [];
36
+ const global_middleware: MiddlewareType[] = [];
37
+
38
+ const cli: CLIType = {
39
+ command(cmd: CommandType): CLIType {
40
+ commands.push(cmd);
41
+ return cli;
42
+ },
43
+
44
+ use(middleware: MiddlewareType): CLIType {
45
+ global_middleware.push(middleware);
46
+ return cli;
47
+ },
48
+
49
+ async run(
50
+ args: string[],
51
+ container: ContainerType,
52
+ ): Promise<ResultType<void, KustodianErrorType>> {
53
+ // Parse command from args
54
+ const [command_name, ...rest_args] = args;
55
+
56
+ if (!command_name) {
57
+ // Show help if no command specified
58
+ return success(undefined);
59
+ }
60
+
61
+ const command = commands.find((c) => c.name === command_name);
62
+ if (!command) {
63
+ return failure({
64
+ code: 'COMMAND_NOT_FOUND',
65
+ message: `Unknown command: ${command_name}`,
66
+ });
67
+ }
68
+
69
+ // Parse options and arguments
70
+ const { options, positional_args } = parse_args(rest_args, command);
71
+
72
+ // Create context
73
+ const ctx = create_context(positional_args, options);
74
+
75
+ // Combine middleware
76
+ const command_middleware = command.middleware ?? [];
77
+ const all_middleware = [...global_middleware, ...command_middleware];
78
+
79
+ // Create handler middleware
80
+ const handler_middleware = create_handler_middleware(command.handler, container);
81
+
82
+ // Execute pipeline
83
+ const pipeline = create_pipeline([...all_middleware, handler_middleware]);
84
+
85
+ return pipeline(ctx, async () => success(undefined));
86
+ },
87
+ };
88
+
89
+ return cli;
90
+ }
91
+
92
+ /**
93
+ * Parses command line arguments.
94
+ */
95
+ function parse_args(
96
+ args: string[],
97
+ command: CommandType,
98
+ ): { options: Record<string, unknown>; positional_args: string[] } {
99
+ const options: Record<string, unknown> = {};
100
+ const positional_args: string[] = [];
101
+
102
+ // Set default values
103
+ for (const opt of command.options ?? []) {
104
+ if (opt.default_value !== undefined) {
105
+ options[opt.name] = opt.default_value;
106
+ }
107
+ }
108
+
109
+ let i = 0;
110
+ while (i < args.length) {
111
+ const arg = args[i];
112
+
113
+ if (!arg) {
114
+ i++;
115
+ continue;
116
+ }
117
+
118
+ if (arg.startsWith('--')) {
119
+ const [key, value] = arg.slice(2).split('=');
120
+ if (!key) {
121
+ i++;
122
+ continue;
123
+ }
124
+
125
+ const opt = command.options?.find((o) => o.name === key);
126
+ if (opt) {
127
+ if (opt.type === 'boolean') {
128
+ options[key] = value !== 'false';
129
+ } else if (value !== undefined) {
130
+ options[key] = opt.type === 'number' ? Number(value) : value;
131
+ } else {
132
+ const next_arg = args[i + 1];
133
+ const next_value = next_arg;
134
+ if (next_value && !next_value.startsWith('-')) {
135
+ options[key] = opt.type === 'number' ? Number(next_value) : next_value;
136
+ i++;
137
+ } else {
138
+ options[key] = true;
139
+ }
140
+ }
141
+ }
142
+ } else if (arg.startsWith('-') && arg.length === 2) {
143
+ const short = arg.slice(1);
144
+ const opt = command.options?.find((o) => o.short === short);
145
+ if (opt) {
146
+ const next_arg = args[i + 1];
147
+ const next_value = next_arg;
148
+ if (opt.type === 'boolean') {
149
+ options[opt.name] = true;
150
+ } else if (next_value && !next_value.startsWith('-')) {
151
+ options[opt.name] = opt.type === 'number' ? Number(next_value) : next_value;
152
+ i++;
153
+ }
154
+ }
155
+ } else {
156
+ positional_args.push(arg);
157
+ }
158
+
159
+ i++;
160
+ }
161
+
162
+ return { options, positional_args };
163
+ }
164
+
165
+ /**
166
+ * Creates a middleware that executes the command handler.
167
+ */
168
+ function create_handler_middleware(
169
+ handler: HandlerType | undefined,
170
+ container: ContainerType,
171
+ ): MiddlewareType {
172
+ return async (ctx: ContextType, next) => {
173
+ if (handler) {
174
+ const result = await handler(ctx, container);
175
+ if (!result.success) {
176
+ return result;
177
+ }
178
+ }
179
+ return next();
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Gets CLI version info string.
185
+ */
186
+ export function format_version(config: CLIConfigType): string {
187
+ return `${config.name} v${config.version}`;
188
+ }
189
+
190
+ /**
191
+ * Gets CLI help text.
192
+ */
193
+ export function format_help(config: CLIConfigType, commands: CommandType[]): string {
194
+ const lines: string[] = [];
195
+
196
+ lines.push(config.description ?? config.name);
197
+ lines.push('');
198
+ lines.push('Usage:');
199
+ lines.push(` ${config.name} <command> [options]`);
200
+ lines.push('');
201
+ lines.push('Commands:');
202
+
203
+ for (const cmd of commands) {
204
+ lines.push(` ${cmd.name.padEnd(20)} ${cmd.description}`);
205
+ }
206
+
207
+ lines.push('');
208
+ lines.push('Options:');
209
+ lines.push(' --help, -h Show help');
210
+ lines.push(' --version, -v Show version');
211
+
212
+ return lines.join('\n');
213
+ }