@tkeron/commands 0.3.1 → 0.4.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.
@@ -4,21 +4,27 @@ on:
4
4
  branches:
5
5
  - main
6
6
 
7
+ permissions:
8
+ id-token: write
9
+ contents: read
10
+
7
11
  jobs:
8
12
  build-test-publish:
9
13
  runs-on: ubuntu-latest
14
+
10
15
  steps:
11
16
  - uses: actions/checkout@v4
12
- - uses: actions/setup-node@v4.0.3
17
+
18
+ - uses: actions/setup-node@v4
13
19
  with:
14
- node-version: 20.x
15
- registry-url: "https://registry.npmjs.org/"
20
+ node-version: 22.x
21
+ registry-url: "https://registry.npmjs.org"
16
22
 
17
23
  - uses: oven-sh/setup-bun@v2
18
24
 
19
- - run: |
20
- bun i
21
- bun test
22
- npm publish --access public
25
+ - run: bun i
26
+ - run: bun test
27
+
28
+ - run: npm publish --provenance
23
29
  env:
24
30
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/bun.lockb CHANGED
Binary file
package/changelog.md CHANGED
@@ -1,3 +1,19 @@
1
+ # v0.4.0
2
+
3
+ - add standard CLI syntax support (`--flag`, `-f`, `--opt value`, `--opt=value`)
4
+ - add typed option definitions (`OptionDefinition`) with `boolean`, `string`, `number` types
5
+ - add `addFlag()`, `addStringOption()`, `addNumberOption()` builder methods
6
+ - add argument parser (`parseArgs`) with auto-detection of legacy/standard/mixed syntax
7
+ - add default values via `applyDefaults()`
8
+ - add option validation (`validateOptions`) with required, type, and allowed values checks
9
+ - add combined short flags support (`-vw`)
10
+ - add negated flags (`--no-verbose`)
11
+ - add double-dash separator (`--`) to stop parsing
12
+ - add option aliases support
13
+ - maintain full backward compatibility with legacy `key=value` syntax
14
+ - refactor `getStart` to use new parsing pipeline
15
+ - update help text generation for typed options
16
+
1
17
  # v0.3.0
2
18
 
3
19
  - migrate project to bun
@@ -0,0 +1,116 @@
1
+ import { getCommands } from "../src/index.js";
2
+ import type { Commands } from "../src/types.js";
3
+
4
+ const commands = getCommands("test program", "0.0.14")
5
+ .addCommand("com1")
6
+ .addAlias("c1")
7
+ .addOption("opt1")
8
+ .addOption("opt2")
9
+ .addDescription("command 001 test...")
10
+ .addPositionedArgument("pos0")
11
+ .addPositionedArgument("pos1")
12
+ .setCallback(console.log)
13
+
14
+ .commands()
15
+
16
+ .addCommand("com2")
17
+ .addAlias("c2")
18
+ .addAlias("a2")
19
+ .addOption("opt1")
20
+ .addDescription("command 002 test...")
21
+ .setCallback(console.log)
22
+
23
+ .commands()
24
+
25
+ .addCommand("com3")
26
+ .addAlias("c3")
27
+ .addOption("opt1")
28
+ .addOption("opt2", "exOpt2...")
29
+ .addOption("opt3")
30
+ .addDescription("command 003 test...")
31
+ .addPositionedArgument("pos0")
32
+ .addPositionedArgument("pos1")
33
+ .addPositionedArgument("pos3")
34
+ .setCallback(console.log)
35
+
36
+ .commands()
37
+
38
+ .addHeaderText("header...\n\n")
39
+ .addFooterText("\n\nFooter...");
40
+
41
+ commands.start([
42
+ "",
43
+ "",
44
+ ..."com1 pos0value opt1=qw111erty pos1value opt2=as222d asdasd"
45
+ .replace(/\s+/g, " ")
46
+ .split(" "),
47
+ ]);
48
+
49
+ commands.start([
50
+ "",
51
+ "",
52
+ ..."a2 pos0value opt1=qw111erty pos1value opt2=as222d asdasd"
53
+ .replace(/\s+/g, " ")
54
+ .split(" "),
55
+ ]);
56
+
57
+ commands.start([
58
+ "",
59
+ "",
60
+ ..."c3 pos0value opt1=qw111erty pos1value opt2=as222d asdasd"
61
+ .replace(/\s+/g, " ")
62
+ .split(" "),
63
+ ]);
64
+
65
+ const standardCommands = getCommands("myapp", "1.0.0")
66
+ .addCommand("build")
67
+ .addAlias("b")
68
+ .addDescription("Build project")
69
+ .addFlag("verbose", "v", "Verbose output")
70
+ .addFlag("watch", "w", "Watch mode")
71
+ .addStringOption("output", "o", "Output directory", { default: "./dist" })
72
+ .addPositionedArgument("source")
73
+ .setCallback(console.log)
74
+
75
+ .commands()
76
+
77
+ .addCommand("serve")
78
+ .addAlias("s")
79
+ .addDescription("Start dev server")
80
+ .addNumberOption("port", "p", "Port number", { default: 3000 })
81
+ .addStringOption("host", "h", "Host", { default: "localhost" })
82
+ .addFlag("open", "o", "Open browser")
83
+ .setCallback(console.log)
84
+
85
+ .commands()
86
+
87
+ .addCommand("test")
88
+ .addAlias("t")
89
+ .addDescription("Run tests")
90
+ .addStringOption("filter", "f", "Test filter")
91
+ .addFlag("coverage", "c", "Coverage report")
92
+ .addNumberOption("timeout", undefined, "Timeout in ms", { default: 5000 })
93
+ .setCallback(console.log)
94
+
95
+ .commands()
96
+
97
+ .addHeaderText("MyApp CLI v1.0.0\n")
98
+ .addFooterText("\nFor more info: https://example.com");
99
+
100
+ standardCommands.start(["", "", "build", "src", "--verbose", "-o", "build"]);
101
+
102
+ standardCommands.start(["", "", "serve", "-p", "8080", "--open"]);
103
+
104
+ standardCommands.start(["", "", "test", "--filter", "unit", "--coverage"]);
105
+
106
+ standardCommands.start(["", "", "b", "-vw"]);
107
+
108
+ standardCommands.start(["", "", "build", "src", "--verbose", "extra=legacy"]);
109
+
110
+ declare global {
111
+ var commands: Commands;
112
+ var standardCommands: Commands;
113
+ }
114
+
115
+ globalThis.commands = commands;
116
+ globalThis.standardCommands = standardCommands;
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "@tkeron/commands",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "library for handling command line arguments",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
7
7
  "type": "module",
8
8
  "author": "tkeron",
9
9
  "license": "MIT",
10
+ "scripts": {
11
+ "test": "bun test --concurrent"
12
+ },
10
13
  "devDependencies": {
11
- "@types/bun": "^1.3.1"
14
+ "@types/bun": "^1.3.9"
12
15
  },
13
16
  "peerDependencies": {
14
17
  "typescript": "^5.9.3"
@@ -19,6 +22,9 @@
19
22
  "command-line",
20
23
  "arguments"
21
24
  ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
22
28
  "repository": {
23
29
  "url": "git@github.com:tkeron/commands.git"
24
30
  }
package/readme.md CHANGED
@@ -1,3 +1,120 @@
1
- # Tkeron Commands
1
+ # @tkeron/commands
2
2
 
3
- Simple package to evaluate cli arguments, with minimun dependencies.
3
+ Zero-dependency CLI command parser and router for Bun/Node.js.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @tkeron/commands
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { getCommands } from "@tkeron/commands";
15
+
16
+ const cli = getCommands("myapp", "1.0.0")
17
+ .addCommand("build")
18
+ .addAlias("b")
19
+ .addDescription("Build the project")
20
+ .addFlag("verbose", "v", "Enable verbose output")
21
+ .addStringOption("output", "o", "Output directory", { default: "./dist" })
22
+ .addPositionedArgument("source")
23
+ .setCallback(console.log)
24
+
25
+ .commands()
26
+
27
+ .addCommand("serve")
28
+ .addAlias("s")
29
+ .addDescription("Start dev server")
30
+ .addNumberOption("port", "p", "Port number", { default: 3000 })
31
+ .addFlag("open", "o", "Open browser")
32
+ .setCallback(console.log)
33
+
34
+ .commands()
35
+
36
+ .addHeaderText("MyApp v1.0.0\n")
37
+ .addFooterText("\nDocs: https://example.com");
38
+
39
+ cli.start();
40
+ ```
41
+
42
+ Usage:
43
+
44
+ ```bash
45
+ myapp build src --verbose -o dist
46
+ myapp serve -p 8080 --open
47
+ myapp help
48
+ myapp version
49
+ ```
50
+
51
+ ## API
52
+
53
+ ### `getCommands(programName, version)`
54
+
55
+ Creates a new `Commands` instance.
56
+
57
+ ### Commands
58
+
59
+ | Method | Description |
60
+ | ---------------------- | ------------------------------------------------- |
61
+ | `.addCommand(name)` | Add a command, returns `CommandFactory` |
62
+ | `.addHeaderText(text)` | Set help header text |
63
+ | `.addFooterText(text)` | Set help footer text |
64
+ | `.start(argv?)` | Parse and execute. Uses `process.argv` by default |
65
+
66
+ ### CommandFactory (chaining)
67
+
68
+ | Method | Description |
69
+ | ----------------------------------------------------------- | ----------------------------------------- |
70
+ | `.addAlias(alias)` | Add command alias |
71
+ | `.addDescription(text)` | Set command description |
72
+ | `.addFlag(name, shortFlag?, description?)` | Add boolean flag (`--verbose`, `-v`) |
73
+ | `.addStringOption(name, shortFlag?, description?, config?)` | Add string option |
74
+ | `.addNumberOption(name, shortFlag?, description?, config?)` | Add number option |
75
+ | `.addOption(name, example?)` | Add legacy `key=value` option |
76
+ | `.addPositionedArgument(name)` | Add positional argument |
77
+ | `.setCallback(fn)` | Set command handler |
78
+ | `.commands()` | Return to `Commands` to add more commands |
79
+
80
+ ### Option Config
81
+
82
+ `addStringOption` and `addNumberOption` accept an optional config object:
83
+
84
+ ```typescript
85
+ {
86
+ default?: string | number | boolean;
87
+ required?: boolean;
88
+ aliases?: string[];
89
+ allowedValues?: (string | number)[];
90
+ valueName?: string;
91
+ }
92
+ ```
93
+
94
+ ## Syntax Support
95
+
96
+ Both syntaxes work and can be mixed:
97
+
98
+ **Standard** (new):
99
+
100
+ ```bash
101
+ myapp build --verbose --output dist
102
+ myapp build -v -o dist
103
+ myapp build -vw # combined short flags
104
+ myapp build --no-verbose # negated flags
105
+ myapp build -- --not-parsed # stop parsing after --
106
+ ```
107
+
108
+ **Legacy** (backward compatible):
109
+
110
+ ```bash
111
+ myapp build output=dist format=json
112
+ ```
113
+
114
+ ## Built-in Commands
115
+
116
+ `help` and `version` are added automatically.
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,20 @@
1
+ import type { Command, ParsedOptions } from "./types.js";
2
+
3
+ export const applyDefaults = (
4
+ parsed: ParsedOptions,
5
+ command: Command,
6
+ ): ParsedOptions => {
7
+ const result = { ...parsed };
8
+
9
+ if (!command.optionDefinitions || command.optionDefinitions.length === 0) {
10
+ return result;
11
+ }
12
+
13
+ for (const def of command.optionDefinitions) {
14
+ if (def.default !== undefined && result[def.name] === undefined) {
15
+ result[def.name] = def.default;
16
+ }
17
+ }
18
+
19
+ return result;
20
+ };
@@ -1,17 +1,18 @@
1
- import { getStart } from "./getStart";
2
- import { buildHelpText, getCommandText } from "./textFuncs";
1
+ import { getStart } from "./getStart.js";
2
+ import { buildHelpText, getCommandText } from "./textFuncs.js";
3
3
  import type {
4
4
  CommandFactory,
5
5
  Command,
6
6
  Commands,
7
7
  CommandsCollection,
8
8
  Callback,
9
- } from "./types";
10
- export * from "./types";
9
+ OptionDefinition,
10
+ } from "./types.js";
11
+ export * from "./types.js";
11
12
 
12
13
  export const getCommands = (
13
14
  programName: string = "program",
14
- version: string = "0.0.1"
15
+ version: string = "0.0.1",
15
16
  ): Commands => {
16
17
  const commandsCollection: CommandsCollection = {};
17
18
 
@@ -41,10 +42,13 @@ export const initCommands = (
41
42
  name: undefined,
42
43
  addAlias: undefined,
43
44
  addOption: undefined,
45
+ addFlag: undefined,
46
+ addStringOption: undefined,
47
+ addNumberOption: undefined,
44
48
  addPositionedArgument: undefined,
45
49
  addDescription: undefined,
46
50
  setCallback: undefined,
47
- })
51
+ }),
48
52
  ): Commands => {
49
53
  if (!commandFactory.commands) commandFactory.commands = () => commands;
50
54
  commands.addCommand = getAddCommand(commandsCollection, commandFactory);
@@ -57,15 +61,15 @@ export const initCommands = (
57
61
 
58
62
  export const initHelpAndVersion = (
59
63
  commands: Commands,
60
- commandsCollection: CommandsCollection
64
+ commandsCollection: CommandsCollection,
61
65
  ) => {
62
66
  const helpCallback = () =>
63
- console["log"](buildHelpText(commands, commandsCollection));
67
+ console.log(buildHelpText(commands, commandsCollection));
64
68
  const versionCallback = () =>
65
- console["log"](
69
+ console.log(
66
70
  `${commands.headerText || ""}\n${commands.version}\n${
67
71
  commands.footerText || ""
68
- }\n`
72
+ }\n`,
69
73
  );
70
74
 
71
75
  commands
@@ -96,6 +100,7 @@ export const getAddCommand =
96
100
  options: [],
97
101
  optionsExamples: [],
98
102
  positionedArguments: [],
103
+ optionDefinitions: [],
99
104
  getHelpLine: undefined,
100
105
  });
101
106
  commandsCollection[commandName] = command;
@@ -103,17 +108,26 @@ export const getAddCommand =
103
108
 
104
109
  commandFactory.addAlias = getAddAlias(commandFactory, commandsCollection);
105
110
  commandFactory.addOption = getAddOption(commandFactory, commandsCollection);
111
+ commandFactory.addFlag = getAddFlag(commandFactory, commandsCollection);
112
+ commandFactory.addStringOption = getAddStringOption(
113
+ commandFactory,
114
+ commandsCollection,
115
+ );
116
+ commandFactory.addNumberOption = getAddNumberOption(
117
+ commandFactory,
118
+ commandsCollection,
119
+ );
106
120
  commandFactory.addDescription = getAddDescription(
107
121
  commandFactory,
108
- commandsCollection
122
+ commandsCollection,
109
123
  );
110
124
  commandFactory.setCallback = getSetCallback(
111
125
  commandFactory,
112
- commandsCollection
126
+ commandsCollection,
113
127
  );
114
128
  commandFactory.addPositionedArgument = getAddPositionedArgument(
115
129
  commandFactory,
116
- commandsCollection
130
+ commandsCollection,
117
131
  );
118
132
 
119
133
  return commandFactory;
@@ -121,7 +135,7 @@ export const getAddCommand =
121
135
 
122
136
  export const getAddAlias = (
123
137
  commandFactory: CommandFactory,
124
- commandsCollection: CommandsCollection
138
+ commandsCollection: CommandsCollection,
125
139
  ) => {
126
140
  return (alias: string) => {
127
141
  commandsCollection[commandFactory.name].aliases.push(alias);
@@ -133,22 +147,28 @@ export const getAddAlias = (
133
147
 
134
148
  export const getAddOption =
135
149
  (commandFactory: CommandFactory, commandsCollection: CommandsCollection) =>
136
- (option: string, example?: string) => {
137
- commandsCollection[commandFactory.name].options.push(option);
138
- commandsCollection[commandFactory.name].optionsExamples.push(example || "");
150
+ (option: string | OptionDefinition, example?: string) => {
151
+ if (typeof option === "string") {
152
+ commandsCollection[commandFactory.name].options.push(option);
153
+ commandsCollection[commandFactory.name].optionsExamples.push(
154
+ example || "",
155
+ );
156
+ } else {
157
+ commandsCollection[commandFactory.name].optionDefinitions.push(option);
158
+ }
139
159
 
140
160
  return commandFactory;
141
161
  };
142
162
 
143
163
  export const getAddPositionedArgument = (
144
164
  commandFactory: CommandFactory,
145
- commandsCollection: CommandsCollection
165
+ commandsCollection: CommandsCollection,
146
166
  ) => {
147
167
  return (arg: string): CommandFactory => {
148
168
  const positionedArguments =
149
169
  commandsCollection[commandFactory.name].positionedArguments;
150
170
 
151
- if (!positionedArguments.includes(commandFactory.name)) {
171
+ if (!positionedArguments.includes(arg)) {
152
172
  positionedArguments.push(arg);
153
173
  }
154
174
 
@@ -158,7 +178,7 @@ export const getAddPositionedArgument = (
158
178
 
159
179
  export const getSetCallback = (
160
180
  commandFactory: CommandFactory,
161
- commandsCollection: CommandsCollection
181
+ commandsCollection: CommandsCollection,
162
182
  ) => {
163
183
  return (fn: Callback): CommandFactory => {
164
184
  commandsCollection[commandFactory.name].callback = fn;
@@ -169,7 +189,7 @@ export const getSetCallback = (
169
189
 
170
190
  export const getAddDescription = (
171
191
  commandFactory: CommandFactory,
172
- commandsCollection: CommandsCollection
192
+ commandsCollection: CommandsCollection,
173
193
  ) => {
174
194
  return (description: string) => {
175
195
  commandsCollection[commandFactory.name].description = description;
@@ -191,6 +211,57 @@ export const getAddFooterText = (commands: Commands) => {
191
211
  };
192
212
  };
193
213
 
214
+ export const getAddFlag =
215
+ (commandFactory: CommandFactory, commandsCollection: CommandsCollection) =>
216
+ (name: string, shortFlag?: string, description?: string): CommandFactory => {
217
+ const def: OptionDefinition = {
218
+ name,
219
+ shortFlag,
220
+ type: "boolean",
221
+ description: description || "",
222
+ };
223
+ commandsCollection[commandFactory.name].optionDefinitions.push(def);
224
+ return commandFactory;
225
+ };
226
+
227
+ export const getAddStringOption =
228
+ (commandFactory: CommandFactory, commandsCollection: CommandsCollection) =>
229
+ (
230
+ name: string,
231
+ shortFlag?: string,
232
+ description?: string,
233
+ config?: Partial<OptionDefinition>,
234
+ ): CommandFactory => {
235
+ const def: OptionDefinition = {
236
+ name,
237
+ shortFlag,
238
+ type: "string",
239
+ description: description || "",
240
+ ...config,
241
+ };
242
+ commandsCollection[commandFactory.name].optionDefinitions.push(def);
243
+ return commandFactory;
244
+ };
245
+
246
+ export const getAddNumberOption =
247
+ (commandFactory: CommandFactory, commandsCollection: CommandsCollection) =>
248
+ (
249
+ name: string,
250
+ shortFlag?: string,
251
+ description?: string,
252
+ config?: Partial<OptionDefinition>,
253
+ ): CommandFactory => {
254
+ const def: OptionDefinition = {
255
+ name,
256
+ shortFlag,
257
+ type: "number",
258
+ description: description || "",
259
+ ...config,
260
+ };
261
+ commandsCollection[commandFactory.name].optionDefinitions.push(def);
262
+ return commandFactory;
263
+ };
264
+
194
265
  export const getGetHelpLine =
195
266
  (command: Command) =>
196
267
  (width: number = 50) => {
package/src/getStart.ts CHANGED
@@ -1,10 +1,13 @@
1
- import type { CommandsCollection } from "./types";
1
+ import type { CommandsCollection } from "./types.js";
2
+ import { parseArgs } from "./parseArgs.js";
3
+ import { applyDefaults } from "./applyDefaults.js";
4
+ import { validateOptions } from "./validateOptions.js";
2
5
 
3
6
  export const getStart =
4
7
  (commandsCollection: CommandsCollection) => (argv?: string[]) => {
5
8
  if (!argv) argv = process.argv;
6
9
  if (!Array.isArray(argv)) throw new Error("no arguments passed");
7
- if (argv.length < 2) throw Error("arguments out of range");
10
+ if (argv.length < 2) throw new Error("arguments out of range");
8
11
  argv = argv.slice(2);
9
12
  if (argv.length === 0) {
10
13
  commandsCollection.help.callback();
@@ -15,39 +18,31 @@ export const getStart =
15
18
 
16
19
  const command = commandsCollection[commandName];
17
20
  if (!command) {
18
- console["log"](`command '${commandName}' not found`);
21
+ console.log(`command '${commandName}' not found`);
19
22
  return;
20
23
  }
21
24
 
22
- const options = argv
23
- .slice(1)
24
- .filter((arg) => /\=/g.test(arg))
25
- .map((arg) => arg.split("="))
26
- .reduce((p: any, c) => {
27
- p[c[0]] = c[1];
25
+ const parsedResult = parseArgs(argv.slice(1), command);
26
+ let options = parsedResult.options;
28
27
 
29
- return p;
30
- }, {});
28
+ options = applyDefaults(options, command);
31
29
 
32
- const positionedArgsValues = argv
33
- .slice(1)
34
- .filter((arg) => !/\=/g.test(arg));
35
-
36
- if (positionedArgsValues.length <= command.positionedArguments.length) {
37
- positionedArgsValues.forEach(
38
- (arg, n) => (options[command.positionedArguments[n]] = arg)
39
- );
30
+ const validation = validateOptions(options, command);
31
+ if (!validation.valid) {
32
+ for (const error of validation.errors) {
33
+ console.log(error);
34
+ }
35
+ return;
40
36
  }
41
37
 
42
- if (positionedArgsValues.length > command.positionedArguments.length) {
43
- console["log"](
44
- `argument${
45
- positionedArgsValues.length - command.positionedArguments.length === 1
46
- ? ""
47
- : "s"
48
- } '${positionedArgsValues
49
- .slice(command.positionedArguments.length)
50
- .join(", ")}' not defined`
38
+ const positionedOverflow = parsedResult.positional.filter(
39
+ (arg) =>
40
+ !command.positionedArguments.some((name) => options[name] === arg),
41
+ );
42
+
43
+ if (positionedOverflow.length > 0) {
44
+ console.log(
45
+ `argument${positionedOverflow.length === 1 ? "" : "s"} '${positionedOverflow.join(", ")}' not defined`,
51
46
  );
52
47
  return;
53
48
  }
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export { getCommands } from "./getCommandsFuncs";
1
+ export { getCommands } from "./getCommandsFuncs.js";