@rhythmjs/cli 0.0.12 → 0.0.13

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The command-line layer of Rhythm, the Bun-native backend framework: CLI command routing on top of the `@rhythmjs/rhythm` kernel. `RhythmCli` matches commands against argv positional tokens with a simple linear scan (appropriate for the handful-to-dozens of commands a real CLI has), supports prefixes and nested command groups, and mounts flat into a parent `Rhythm` app via `.use(cli.commands())`, so an unmatched command correctly falls through to whatever's registered after it.
4
4
 
5
- `RhythmCli` is not an app and does not extend `Rhythm`; it is a controller that compiles commands and middleware down to a single middleware (`.commands()`). It shares the core middleware contract (`compose`, `Middleware`, `next(extra)`), but has no `provide()` or `register()`, and it can't be served on its own: a `Rhythm` app is always the host that owns the lifecycle.
5
+ `RhythmCli` is not an app and does not extend `Rhythm`; it is a controller that compiles commands and middleware down to a single middleware (`.commands()`). It shares the core middleware contract (`compose`, `Middleware`, `derive`; `next()` takes no arguments, extend the context with `derive()`), but has no `provide()` or `register()`, and it can't be served on its own: a `Rhythm` app is always the host that owns the lifecycle.
6
6
 
7
7
  ## Example
8
8
 
@@ -36,7 +36,7 @@ A fuller runnable version, including nested command groups and interactive promp
36
36
  ## API
37
37
 
38
38
  - `new RhythmCli(options?)`: `options.prefix` (space-separated, e.g. `"remote"`).
39
- - `.command(path, ...handlers)`: register a command; `path` is space-separated and may contain `:param` tokens (e.g. `"deploy :environment"`).
39
+ - `.command(path, ...handlers)`: register a command; `path` is space-separated and may contain `:param` tokens (e.g. `"deploy :environment"`). A trailing `:param?` is optional (e.g. `"new :name?"`): `ctx.args.param` is left out when the token is absent. Optional params must come last. A trailing `**` is a catch-all, like the router: it captures the remaining positionals into `ctx.args._`, joined by spaces, and is absent when there are none (e.g. `"run :script **"`). Order is required, then optional, then at most one catch-all.
40
40
  - `.use(fn)`: plain middleware. `.use(child)`: mount a nested `RhythmCli` (prefixes compose).
41
41
  - `.commands()`: this CLI as a plain middleware, for mounting into a `Rhythm` app via `.use()`; the cli's only way onto a runtime. Note: mounting a _cli_ into a _cli_ must use `.use(child)`, not `.use(child.commands())`, because an opaque middleware can't have the parent's prefix applied to its commands.
42
42
  - `toCliHandler(app)`: bridges a `Rhythm` app to `(argv: string[]) => Promise<number>`.
@@ -8,21 +8,41 @@ import { compose } from "@rhythmjs/rhythm/compose";
8
8
  function toSegments(command) {
9
9
  return command.trim().split(/\s+/).filter(Boolean);
10
10
  }
11
+ var isOptional = (segment) => segment.startsWith(":") && segment.endsWith("?");
12
+ var isCatchAll = (segment) => segment === "**";
11
13
  function matchCommand(pattern, positionals) {
12
- if (pattern.length !== positionals.length)
14
+ const catchAll = isCatchAll(pattern.at(-1));
15
+ const fixed = catchAll ? pattern.slice(0, -1) : pattern;
16
+ const required = fixed.filter((segment) => !isOptional(segment)).length;
17
+ if (positionals.length < required || !catchAll && positionals.length > fixed.length)
13
18
  return null;
14
19
  const params = {};
15
- for (let i = 0;i < pattern.length; i++) {
16
- const segment = pattern[i];
20
+ for (let i = 0;i < fixed.length; i++) {
21
+ const segment = fixed[i];
17
22
  const token = positionals[i];
18
23
  if (segment.startsWith(":")) {
19
- params[segment.slice(1)] = token;
24
+ if (token !== undefined)
25
+ params[segment.slice(1, isOptional(segment) ? -1 : undefined)] = token;
20
26
  } else if (segment !== token) {
21
27
  return null;
22
28
  }
23
29
  }
30
+ if (catchAll && positionals.length > fixed.length) {
31
+ params._ = positionals.slice(fixed.length).join(" ");
32
+ }
24
33
  return params;
25
34
  }
35
+ function assertValidPattern(path, segments) {
36
+ const rank = (segment) => isCatchAll(segment) ? 2 : isOptional(segment) ? 1 : 0;
37
+ for (let i = 1;i < segments.length; i++) {
38
+ if (rank(segments[i]) < rank(segments[i - 1])) {
39
+ throw new TypeError(`optional params and a catch-all must come last, in that order, in command "${path}"`);
40
+ }
41
+ }
42
+ if (segments.filter(isCatchAll).length > 1) {
43
+ throw new TypeError(`command "${path}" has more than one catch-all`);
44
+ }
45
+ }
26
46
 
27
47
  class RhythmCli {
28
48
  #options;
@@ -40,7 +60,9 @@ class RhythmCli {
40
60
  return this;
41
61
  }
42
62
  command(path, ...handlers) {
43
- this.#entries.push({ kind: "command", segments: [...this.#prefixSegments, ...toSegments(path)], handlers });
63
+ const segments = toSegments(path);
64
+ assertValidPattern(path, segments);
65
+ this.#entries.push({ kind: "command", segments: [...this.#prefixSegments, ...segments], handlers });
44
66
  return this;
45
67
  }
46
68
  #compile() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhythmjs/cli",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Command-line argument parsing, prompts, and command routing for the Rhythm middleware kernel, running on Bun.",
5
5
  "homepage": "https://rhythm.js.org/cli",
6
6
  "license": "ISC",
@@ -49,7 +49,7 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
- "@rhythmjs/rhythm": "0.0.12"
52
+ "@rhythmjs/rhythm": "0.0.13"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/bun": "^1.4.2",