@northern/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/LICENSE +21 -0
- package/README.md +238 -0
- package/lib/cjs/index.js +192 -0
- package/lib/cjs/package.json +1 -0
- package/lib/esm/index.js +184 -0
- package/lib/types/index.d.ts +60 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luke Schreur
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# @northern/cli
|
|
2
|
+
|
|
3
|
+
A lightweight, typed CLI program framework for building command-driven tools with consistent help output and parameter parsing.
|
|
4
|
+
|
|
5
|
+
## Feature overview
|
|
6
|
+
|
|
7
|
+
- **Typed program model** with `Program`, `Command`, `Argument`, and `Option` definitions.
|
|
8
|
+
- **Built-in parsing** for required arguments and optional flags.
|
|
9
|
+
- **Three parameter modes**: `value`, `switch`, and `enum`.
|
|
10
|
+
- **Automatic help output** for global and command-specific options.
|
|
11
|
+
- **Validation and errors** via `ParameterError` when arguments are invalid or duplicated.
|
|
12
|
+
- **Pluggable console** powered by `@northern/console` for consistent styled output.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @northern/cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## How to use
|
|
21
|
+
|
|
22
|
+
### 1) Define your program
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Interpreter, ParameterType, Program } from '@northern/cli';
|
|
26
|
+
|
|
27
|
+
const program: Program = {
|
|
28
|
+
title: 'Acme Tool',
|
|
29
|
+
version: '1.2.3',
|
|
30
|
+
name: 'acme',
|
|
31
|
+
options: [
|
|
32
|
+
{
|
|
33
|
+
name: 'verbose',
|
|
34
|
+
description: 'Enable verbose output',
|
|
35
|
+
type: ParameterType.SWITCH,
|
|
36
|
+
directives: ['-v', '--verbose'],
|
|
37
|
+
example: '--verbose',
|
|
38
|
+
default: false,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
commands: [
|
|
42
|
+
{
|
|
43
|
+
name: 'build',
|
|
44
|
+
description: 'Build the project',
|
|
45
|
+
arguments: [
|
|
46
|
+
{
|
|
47
|
+
name: 'source',
|
|
48
|
+
description: 'Source file',
|
|
49
|
+
type: ParameterType.VALUE,
|
|
50
|
+
directives: ['-s', '--source'],
|
|
51
|
+
example: '--source ./app.yaml',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
options: [
|
|
55
|
+
{
|
|
56
|
+
name: 'mode',
|
|
57
|
+
description: 'Build mode',
|
|
58
|
+
type: ParameterType.ENUM,
|
|
59
|
+
directives: ['-m', '--mode'],
|
|
60
|
+
example: '--mode production',
|
|
61
|
+
values: ['development', 'production'],
|
|
62
|
+
default: 'development',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2) Parse arguments and execute
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const interpreter = new Interpreter(program, process.argv.slice(2));
|
|
74
|
+
|
|
75
|
+
if (!interpreter.init()) {
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const command = interpreter.getCommand();
|
|
80
|
+
if (!command) {
|
|
81
|
+
interpreter.displayHelp(process.argv[2]);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const params = interpreter.getParameters(command);
|
|
86
|
+
if (!params) {
|
|
87
|
+
interpreter.displayCommandHelp(command);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const globalOptions = interpreter.getGlobalOptions() ?? {};
|
|
92
|
+
|
|
93
|
+
// Execute your command using parsed values
|
|
94
|
+
// e.g. runBuild(params, globalOptions)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 3) What parsing does
|
|
98
|
+
|
|
99
|
+
- **Required arguments** must be present or `getParameters()` returns `null`.
|
|
100
|
+
- **Options** default to their `default` value when not specified.
|
|
101
|
+
- **Switch options** (`ParameterType.SWITCH`) resolve to `true` if present.
|
|
102
|
+
- **Enum options** (`ParameterType.ENUM`) must match one of the configured `values`.
|
|
103
|
+
- **Duplicate directives** (e.g. `--verbose --verbose`) trigger a parse error.
|
|
104
|
+
|
|
105
|
+
## Reference documentation
|
|
106
|
+
|
|
107
|
+
### Types
|
|
108
|
+
|
|
109
|
+
#### `ParameterType`
|
|
110
|
+
|
|
111
|
+
- `ParameterType.VALUE` — a directive that requires a value (e.g. `--config path`).
|
|
112
|
+
- `ParameterType.SWITCH` — a boolean flag (e.g. `--verbose`).
|
|
113
|
+
- `ParameterType.ENUM` — a directive that accepts one of a predefined set of values.
|
|
114
|
+
|
|
115
|
+
#### `Argument`
|
|
116
|
+
|
|
117
|
+
Required parameter definition:
|
|
118
|
+
|
|
119
|
+
| Field | Type | Description |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `name` | `string` | Internal key used in parsed output. |
|
|
122
|
+
| `description` | `string` | Human-readable help text. |
|
|
123
|
+
| `type` | `ParameterType` | Parameter type. |
|
|
124
|
+
| `directives` | `string[]` | Accepted flags (e.g. `['-s', '--source']`). |
|
|
125
|
+
| `example` | `string` | Example usage shown in help. |
|
|
126
|
+
| `values?` | `string[]` | Allowed values for `ENUM` parameters. |
|
|
127
|
+
|
|
128
|
+
#### `Option`
|
|
129
|
+
|
|
130
|
+
Optional parameter definition. Same as `Argument` plus:
|
|
131
|
+
|
|
132
|
+
| Field | Type | Description |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `default` | `unknown` | Default value when option is omitted. |
|
|
135
|
+
|
|
136
|
+
#### `Command`
|
|
137
|
+
|
|
138
|
+
| Field | Type | Description |
|
|
139
|
+
| --- | --- | --- |
|
|
140
|
+
| `name` | `string` | Command name (e.g. `build`). |
|
|
141
|
+
| `description` | `string` | Help summary. |
|
|
142
|
+
| `arguments?` | `Argument[]` | Required parameters. |
|
|
143
|
+
| `options?` | `Option[]` | Command-specific options. |
|
|
144
|
+
|
|
145
|
+
#### `Program`
|
|
146
|
+
|
|
147
|
+
| Field | Type | Description |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| `title` | `string` | Display title shown in the banner. |
|
|
150
|
+
| `version` | `string` | Version string. |
|
|
151
|
+
| `name` | `string` | Executable name for usage output. |
|
|
152
|
+
| `options` | `Option[]` | Global options. |
|
|
153
|
+
| `commands` | `Command[]` | Available commands. |
|
|
154
|
+
|
|
155
|
+
#### `Parameters`
|
|
156
|
+
|
|
157
|
+
`Record<string, unknown>` containing parsed values keyed by `Argument`/`Option` `name`.
|
|
158
|
+
|
|
159
|
+
#### `ICommand`
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
interface ICommand {
|
|
163
|
+
run(parameters: Parameters): Promise<unknown>
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use this to define an execution contract for command handlers.
|
|
168
|
+
|
|
169
|
+
### Errors
|
|
170
|
+
|
|
171
|
+
#### `ParameterError`
|
|
172
|
+
|
|
173
|
+
Thrown internally when parsing fails (missing values, invalid enums, duplicate directives).
|
|
174
|
+
The interpreter returns `null` from `getParameters()`/`getGlobalOptions()` to preserve backward compatibility.
|
|
175
|
+
|
|
176
|
+
### `Interpreter`
|
|
177
|
+
|
|
178
|
+
#### Constructor
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
new Interpreter(program: Program, args: string[], console?: Console)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Creates a parser instance. Provide `args` (typically `process.argv.slice(2)`).
|
|
185
|
+
|
|
186
|
+
#### `init()`
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
init(): boolean
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Displays general help when no arguments are provided and returns `false`.
|
|
193
|
+
|
|
194
|
+
#### `getCommand()`
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
getCommand(): Command | null
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Returns the command matching the first argument (case-insensitive), or `null`.
|
|
201
|
+
|
|
202
|
+
#### `getBanner()`
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
getBanner(): string
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Returns a formatted banner string with title and version.
|
|
209
|
+
|
|
210
|
+
#### `displayHelp(command?: string)`
|
|
211
|
+
|
|
212
|
+
Displays general help and optionally an “unknown command” message.
|
|
213
|
+
|
|
214
|
+
#### `displayCommandHelp(command: Command)`
|
|
215
|
+
|
|
216
|
+
Shows command-specific usage, required arguments, options, and global options.
|
|
217
|
+
|
|
218
|
+
#### `getParameters(command: Command)`
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
getParameters(command: Command): Parameters | null
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Parses required arguments and command options. Returns `null` on validation failure.
|
|
225
|
+
|
|
226
|
+
#### `getGlobalOptions()`
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
getGlobalOptions(): Parameters | null
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Parses global options only. Returns `null` on validation failure.
|
|
233
|
+
|
|
234
|
+
#### `setConsole(console: Console)`
|
|
235
|
+
|
|
236
|
+
Replaces the internal console instance (useful for testing).
|
|
237
|
+
|
|
238
|
+
|
package/lib/cjs/index.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Interpreter = exports.ParameterError = exports.ParameterType = void 0;
|
|
7
|
+
const console_1 = __importDefault(require("@northern/console"));
|
|
8
|
+
var ParameterType;
|
|
9
|
+
(function (ParameterType) {
|
|
10
|
+
ParameterType["VALUE"] = "value";
|
|
11
|
+
ParameterType["SWITCH"] = "switch";
|
|
12
|
+
ParameterType["ENUM"] = "enum";
|
|
13
|
+
})(ParameterType || (exports.ParameterType = ParameterType = {}));
|
|
14
|
+
class ParameterError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ParameterError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.ParameterError = ParameterError;
|
|
21
|
+
const c = console;
|
|
22
|
+
class Interpreter {
|
|
23
|
+
constructor(program, args, console) {
|
|
24
|
+
this.program = program;
|
|
25
|
+
this.args = args;
|
|
26
|
+
c.log(console_1.default);
|
|
27
|
+
this.console = console !== null && console !== void 0 ? console : (new console_1.default());
|
|
28
|
+
}
|
|
29
|
+
setConsole(console) {
|
|
30
|
+
this.console = console;
|
|
31
|
+
}
|
|
32
|
+
init() {
|
|
33
|
+
if (!this.args.length) {
|
|
34
|
+
this.displayHelp();
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
getCommand() {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
const commandName = (_a = this.args[0]) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
|
|
42
|
+
if (!commandName) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return (_b = this.program.commands.find((cmd) => cmd.name.trim().toLowerCase() === commandName)) !== null && _b !== void 0 ? _b : null;
|
|
46
|
+
}
|
|
47
|
+
getBanner() {
|
|
48
|
+
return `<command>${this.program.title}</command> version <highlight>${this.program.version}</highlight>\n`;
|
|
49
|
+
}
|
|
50
|
+
displayHelp(command) {
|
|
51
|
+
this.console.write(this.getBanner());
|
|
52
|
+
if (command) {
|
|
53
|
+
this.console.write(`<error>Unknown command: '${command}'</error>\n`);
|
|
54
|
+
}
|
|
55
|
+
this.console.write(`<highlight>Usage:</highlight>\n ${this.program.name} <command> [arguments] [options]\n`);
|
|
56
|
+
this.console.write('<highlight>>Available commands:</highlight>');
|
|
57
|
+
for (const cmd of this.program.commands) {
|
|
58
|
+
this.console.write(` <command>${cmd.name.padEnd(30)}</command> ${cmd.description}`);
|
|
59
|
+
}
|
|
60
|
+
this.console.write('');
|
|
61
|
+
this.displayOptions(this.program.options, 'Global options:');
|
|
62
|
+
}
|
|
63
|
+
displayCommandHelp(command) {
|
|
64
|
+
this.console.write(this.getBanner());
|
|
65
|
+
this.console.write(`<error>Missing or invalid arguments for command: ${command.name}</error>\n`);
|
|
66
|
+
if (command.arguments && command.arguments.length > 0) {
|
|
67
|
+
this.console.write('<highlight>Example:</highlight>');
|
|
68
|
+
const exampleArgs = command.arguments.map((arg) => arg.example).join(' ');
|
|
69
|
+
this.console.write(` ${command.name} ${exampleArgs}`);
|
|
70
|
+
this.console.write('');
|
|
71
|
+
}
|
|
72
|
+
this.console.write(`<highlight>Usage:</highlight>\n ${this.program.name} ${command.name} [arguments] [options]\n`);
|
|
73
|
+
if (command.arguments && command.arguments.length > 0) {
|
|
74
|
+
this.console.write('<highlight>Arguments (required):</highlight>');
|
|
75
|
+
for (const arg of command.arguments) {
|
|
76
|
+
this.displayParameter(arg);
|
|
77
|
+
}
|
|
78
|
+
this.console.write('');
|
|
79
|
+
}
|
|
80
|
+
if (command.options && command.options.length > 0) {
|
|
81
|
+
this.displayOptions(command.options, 'Options:');
|
|
82
|
+
}
|
|
83
|
+
this.displayOptions(this.program.options, 'Global options:');
|
|
84
|
+
}
|
|
85
|
+
displayParameter(param) {
|
|
86
|
+
const directives = param.directives.join(', ').padEnd(30);
|
|
87
|
+
const values = param.values ? ` Possible values: ${param.values.join(', ')}` : '';
|
|
88
|
+
this.console.write(` <command>${directives}</command> ${param.description}${values}`);
|
|
89
|
+
}
|
|
90
|
+
displayOptions(options, title) {
|
|
91
|
+
if (options.length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.console.write(`<highlight>${title}</highlight>`);
|
|
95
|
+
for (const option of options) {
|
|
96
|
+
this.displayParameter(option);
|
|
97
|
+
}
|
|
98
|
+
this.console.write('');
|
|
99
|
+
}
|
|
100
|
+
getGlobalOptions() {
|
|
101
|
+
try {
|
|
102
|
+
const parameters = {};
|
|
103
|
+
for (const option of this.program.options) {
|
|
104
|
+
this.parseParameter(option, parameters, false);
|
|
105
|
+
}
|
|
106
|
+
return parameters;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
getParameters(command) {
|
|
113
|
+
try {
|
|
114
|
+
const parameters = {};
|
|
115
|
+
if (command.arguments) {
|
|
116
|
+
for (const arg of command.arguments) {
|
|
117
|
+
this.parseParameter(arg, parameters, true);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (command.options) {
|
|
121
|
+
for (const option of command.options) {
|
|
122
|
+
this.parseParameter(option, parameters, false);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return parameters;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
parseParameter(param, parameters, required) {
|
|
132
|
+
const { directive, index } = this.findDirective(param.directives);
|
|
133
|
+
if (!directive) {
|
|
134
|
+
if (required) {
|
|
135
|
+
throw new ParameterError(`Missing required argument: ${param.name}`);
|
|
136
|
+
}
|
|
137
|
+
parameters[param.name] = param.default;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const value = this.parseParameterValue(param, directive, index);
|
|
141
|
+
parameters[param.name] = value;
|
|
142
|
+
}
|
|
143
|
+
findDirective(directives) {
|
|
144
|
+
for (const directive of directives) {
|
|
145
|
+
const index = this.args.indexOf(directive);
|
|
146
|
+
if (index !== -1) {
|
|
147
|
+
const lastIndex = this.args.lastIndexOf(directive);
|
|
148
|
+
if (lastIndex !== index) {
|
|
149
|
+
throw new ParameterError(`Directive '${directive}' specified multiple times`);
|
|
150
|
+
}
|
|
151
|
+
return { directive, index };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { directive: null, index: -1 };
|
|
155
|
+
}
|
|
156
|
+
parseParameterValue(param, directive, index) {
|
|
157
|
+
switch (param.type) {
|
|
158
|
+
case ParameterType.SWITCH:
|
|
159
|
+
return true;
|
|
160
|
+
case ParameterType.VALUE: {
|
|
161
|
+
if (index + 1 >= this.args.length) {
|
|
162
|
+
throw new ParameterError(`Directive '${directive}' requires a value`);
|
|
163
|
+
}
|
|
164
|
+
const value = this.args[index + 1];
|
|
165
|
+
if (value.startsWith('-')) {
|
|
166
|
+
throw new ParameterError(`Directive '${directive}' requires a value, got '${value}'`);
|
|
167
|
+
}
|
|
168
|
+
if (value === '') {
|
|
169
|
+
throw new ParameterError(`Directive '${directive}' requires a non-empty value`);
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
case ParameterType.ENUM: {
|
|
174
|
+
if (index + 1 >= this.args.length) {
|
|
175
|
+
throw new ParameterError(`Directive '${directive}' requires a value`);
|
|
176
|
+
}
|
|
177
|
+
const value = this.args[index + 1];
|
|
178
|
+
if (!param.values || param.values.length === 0) {
|
|
179
|
+
throw new ParameterError(`No valid values defined for '${param.name}'`);
|
|
180
|
+
}
|
|
181
|
+
if (!param.values.includes(value)) {
|
|
182
|
+
const validValues = param.values.join(', ');
|
|
183
|
+
throw new ParameterError(`Invalid value '${value}' for '${directive}'. Valid values: ${validValues}`);
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
default:
|
|
188
|
+
throw new ParameterError(`Unknown parameter type: ${param.type}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
exports.Interpreter = Interpreter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import Console from '@northern/console';
|
|
2
|
+
export var ParameterType;
|
|
3
|
+
(function (ParameterType) {
|
|
4
|
+
ParameterType["VALUE"] = "value";
|
|
5
|
+
ParameterType["SWITCH"] = "switch";
|
|
6
|
+
ParameterType["ENUM"] = "enum";
|
|
7
|
+
})(ParameterType || (ParameterType = {}));
|
|
8
|
+
export class ParameterError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'ParameterError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const c = console;
|
|
15
|
+
export class Interpreter {
|
|
16
|
+
constructor(program, args, console) {
|
|
17
|
+
this.program = program;
|
|
18
|
+
this.args = args;
|
|
19
|
+
c.log(Console);
|
|
20
|
+
this.console = console !== null && console !== void 0 ? console : (new Console());
|
|
21
|
+
}
|
|
22
|
+
setConsole(console) {
|
|
23
|
+
this.console = console;
|
|
24
|
+
}
|
|
25
|
+
init() {
|
|
26
|
+
if (!this.args.length) {
|
|
27
|
+
this.displayHelp();
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
getCommand() {
|
|
33
|
+
var _a, _b;
|
|
34
|
+
const commandName = (_a = this.args[0]) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
|
|
35
|
+
if (!commandName) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return (_b = this.program.commands.find((cmd) => cmd.name.trim().toLowerCase() === commandName)) !== null && _b !== void 0 ? _b : null;
|
|
39
|
+
}
|
|
40
|
+
getBanner() {
|
|
41
|
+
return `<command>${this.program.title}</command> version <highlight>${this.program.version}</highlight>\n`;
|
|
42
|
+
}
|
|
43
|
+
displayHelp(command) {
|
|
44
|
+
this.console.write(this.getBanner());
|
|
45
|
+
if (command) {
|
|
46
|
+
this.console.write(`<error>Unknown command: '${command}'</error>\n`);
|
|
47
|
+
}
|
|
48
|
+
this.console.write(`<highlight>Usage:</highlight>\n ${this.program.name} <command> [arguments] [options]\n`);
|
|
49
|
+
this.console.write('<highlight>>Available commands:</highlight>');
|
|
50
|
+
for (const cmd of this.program.commands) {
|
|
51
|
+
this.console.write(` <command>${cmd.name.padEnd(30)}</command> ${cmd.description}`);
|
|
52
|
+
}
|
|
53
|
+
this.console.write('');
|
|
54
|
+
this.displayOptions(this.program.options, 'Global options:');
|
|
55
|
+
}
|
|
56
|
+
displayCommandHelp(command) {
|
|
57
|
+
this.console.write(this.getBanner());
|
|
58
|
+
this.console.write(`<error>Missing or invalid arguments for command: ${command.name}</error>\n`);
|
|
59
|
+
if (command.arguments && command.arguments.length > 0) {
|
|
60
|
+
this.console.write('<highlight>Example:</highlight>');
|
|
61
|
+
const exampleArgs = command.arguments.map((arg) => arg.example).join(' ');
|
|
62
|
+
this.console.write(` ${command.name} ${exampleArgs}`);
|
|
63
|
+
this.console.write('');
|
|
64
|
+
}
|
|
65
|
+
this.console.write(`<highlight>Usage:</highlight>\n ${this.program.name} ${command.name} [arguments] [options]\n`);
|
|
66
|
+
if (command.arguments && command.arguments.length > 0) {
|
|
67
|
+
this.console.write('<highlight>Arguments (required):</highlight>');
|
|
68
|
+
for (const arg of command.arguments) {
|
|
69
|
+
this.displayParameter(arg);
|
|
70
|
+
}
|
|
71
|
+
this.console.write('');
|
|
72
|
+
}
|
|
73
|
+
if (command.options && command.options.length > 0) {
|
|
74
|
+
this.displayOptions(command.options, 'Options:');
|
|
75
|
+
}
|
|
76
|
+
this.displayOptions(this.program.options, 'Global options:');
|
|
77
|
+
}
|
|
78
|
+
displayParameter(param) {
|
|
79
|
+
const directives = param.directives.join(', ').padEnd(30);
|
|
80
|
+
const values = param.values ? ` Possible values: ${param.values.join(', ')}` : '';
|
|
81
|
+
this.console.write(` <command>${directives}</command> ${param.description}${values}`);
|
|
82
|
+
}
|
|
83
|
+
displayOptions(options, title) {
|
|
84
|
+
if (options.length === 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.console.write(`<highlight>${title}</highlight>`);
|
|
88
|
+
for (const option of options) {
|
|
89
|
+
this.displayParameter(option);
|
|
90
|
+
}
|
|
91
|
+
this.console.write('');
|
|
92
|
+
}
|
|
93
|
+
getGlobalOptions() {
|
|
94
|
+
try {
|
|
95
|
+
const parameters = {};
|
|
96
|
+
for (const option of this.program.options) {
|
|
97
|
+
this.parseParameter(option, parameters, false);
|
|
98
|
+
}
|
|
99
|
+
return parameters;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
getParameters(command) {
|
|
106
|
+
try {
|
|
107
|
+
const parameters = {};
|
|
108
|
+
if (command.arguments) {
|
|
109
|
+
for (const arg of command.arguments) {
|
|
110
|
+
this.parseParameter(arg, parameters, true);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (command.options) {
|
|
114
|
+
for (const option of command.options) {
|
|
115
|
+
this.parseParameter(option, parameters, false);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return parameters;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
parseParameter(param, parameters, required) {
|
|
125
|
+
const { directive, index } = this.findDirective(param.directives);
|
|
126
|
+
if (!directive) {
|
|
127
|
+
if (required) {
|
|
128
|
+
throw new ParameterError(`Missing required argument: ${param.name}`);
|
|
129
|
+
}
|
|
130
|
+
parameters[param.name] = param.default;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const value = this.parseParameterValue(param, directive, index);
|
|
134
|
+
parameters[param.name] = value;
|
|
135
|
+
}
|
|
136
|
+
findDirective(directives) {
|
|
137
|
+
for (const directive of directives) {
|
|
138
|
+
const index = this.args.indexOf(directive);
|
|
139
|
+
if (index !== -1) {
|
|
140
|
+
const lastIndex = this.args.lastIndexOf(directive);
|
|
141
|
+
if (lastIndex !== index) {
|
|
142
|
+
throw new ParameterError(`Directive '${directive}' specified multiple times`);
|
|
143
|
+
}
|
|
144
|
+
return { directive, index };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { directive: null, index: -1 };
|
|
148
|
+
}
|
|
149
|
+
parseParameterValue(param, directive, index) {
|
|
150
|
+
switch (param.type) {
|
|
151
|
+
case ParameterType.SWITCH:
|
|
152
|
+
return true;
|
|
153
|
+
case ParameterType.VALUE: {
|
|
154
|
+
if (index + 1 >= this.args.length) {
|
|
155
|
+
throw new ParameterError(`Directive '${directive}' requires a value`);
|
|
156
|
+
}
|
|
157
|
+
const value = this.args[index + 1];
|
|
158
|
+
if (value.startsWith('-')) {
|
|
159
|
+
throw new ParameterError(`Directive '${directive}' requires a value, got '${value}'`);
|
|
160
|
+
}
|
|
161
|
+
if (value === '') {
|
|
162
|
+
throw new ParameterError(`Directive '${directive}' requires a non-empty value`);
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
case ParameterType.ENUM: {
|
|
167
|
+
if (index + 1 >= this.args.length) {
|
|
168
|
+
throw new ParameterError(`Directive '${directive}' requires a value`);
|
|
169
|
+
}
|
|
170
|
+
const value = this.args[index + 1];
|
|
171
|
+
if (!param.values || param.values.length === 0) {
|
|
172
|
+
throw new ParameterError(`No valid values defined for '${param.name}'`);
|
|
173
|
+
}
|
|
174
|
+
if (!param.values.includes(value)) {
|
|
175
|
+
const validValues = param.values.join(', ');
|
|
176
|
+
throw new ParameterError(`Invalid value '${value}' for '${directive}'. Valid values: ${validValues}`);
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
default:
|
|
181
|
+
throw new ParameterError(`Unknown parameter type: ${param.type}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Console from '@northern/console';
|
|
2
|
+
export type OutputFunction = (...args: unknown[]) => void;
|
|
3
|
+
export declare enum ParameterType {
|
|
4
|
+
VALUE = "value",
|
|
5
|
+
SWITCH = "switch",
|
|
6
|
+
ENUM = "enum"
|
|
7
|
+
}
|
|
8
|
+
type BaseParameter = {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
type: ParameterType;
|
|
12
|
+
directives: string[];
|
|
13
|
+
example: string;
|
|
14
|
+
values?: string[];
|
|
15
|
+
value?: unknown;
|
|
16
|
+
};
|
|
17
|
+
export type Argument = BaseParameter;
|
|
18
|
+
export type Option = BaseParameter & {
|
|
19
|
+
default: unknown;
|
|
20
|
+
};
|
|
21
|
+
export type Command = {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
arguments?: Argument[];
|
|
25
|
+
options?: Option[];
|
|
26
|
+
};
|
|
27
|
+
export type Program = {
|
|
28
|
+
title: string;
|
|
29
|
+
version: string;
|
|
30
|
+
name: string;
|
|
31
|
+
options: Option[];
|
|
32
|
+
commands: Command[];
|
|
33
|
+
};
|
|
34
|
+
export type Parameters = Record<string, unknown>;
|
|
35
|
+
export interface ICommand {
|
|
36
|
+
run(parameters: Parameters): Promise<unknown>;
|
|
37
|
+
}
|
|
38
|
+
export declare class ParameterError extends Error {
|
|
39
|
+
constructor(message: string);
|
|
40
|
+
}
|
|
41
|
+
export declare class Interpreter {
|
|
42
|
+
private readonly program;
|
|
43
|
+
private readonly args;
|
|
44
|
+
private console;
|
|
45
|
+
constructor(program: Program, args: string[], console?: Console);
|
|
46
|
+
setConsole(console: Console): void;
|
|
47
|
+
init(): boolean;
|
|
48
|
+
getCommand(): Command | null;
|
|
49
|
+
getBanner(): string;
|
|
50
|
+
displayHelp(command?: string): void;
|
|
51
|
+
displayCommandHelp(command: Command): void;
|
|
52
|
+
private displayParameter;
|
|
53
|
+
private displayOptions;
|
|
54
|
+
getGlobalOptions(): Parameters | null;
|
|
55
|
+
getParameters(command: Command): Parameters | null;
|
|
56
|
+
private parseParameter;
|
|
57
|
+
private findDirective;
|
|
58
|
+
private parseParameterValue;
|
|
59
|
+
}
|
|
60
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@northern/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight CLI program framework.",
|
|
5
|
+
"main": "./lib/cjs/index.js",
|
|
6
|
+
"module": "./lib/esm/index.js",
|
|
7
|
+
"types": "./lib/types/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./lib/types/index.d.ts",
|
|
12
|
+
"import": "./lib/esm/index.js",
|
|
13
|
+
"require": "./lib/cjs/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib/**/*"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "npm run build:esm && npm run build:cjs && npm run build:types && npm run build:cjs-package",
|
|
22
|
+
"build:esm": "tsc --project tsconfig.esm.json --removeComments",
|
|
23
|
+
"build:cjs": "tsc --project tsconfig.cjs.json --removeComments",
|
|
24
|
+
"build:types": "tsc --project tsconfig.types.json --removeComments",
|
|
25
|
+
"build:cjs-package": "echo '{\"type\":\"commonjs\"}' > lib/cjs/package.json",
|
|
26
|
+
"test": "jest",
|
|
27
|
+
"test:watch": "jest --watch --no-coverage",
|
|
28
|
+
"test:coverage": "jest --coverage",
|
|
29
|
+
"prepare": "npm run build",
|
|
30
|
+
"prepublishOnly": "npm test"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/northern/cli.git"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"console",
|
|
38
|
+
"terminal",
|
|
39
|
+
"cli",
|
|
40
|
+
"framework"
|
|
41
|
+
],
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=14.0.0"
|
|
47
|
+
},
|
|
48
|
+
"author": "northern",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/northern/cli/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/northern/cli#readme",
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/jest": "^29",
|
|
56
|
+
"jest": "^29",
|
|
57
|
+
"ts-jest": "^29",
|
|
58
|
+
"typescript": "^5"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@northern/console": "^1"
|
|
62
|
+
}
|
|
63
|
+
}
|