@rhythmjs/cli 0.0.1 → 0.0.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 CHANGED
@@ -1,18 +1,23 @@
1
1
  # @rhythmjs/cli
2
2
 
3
- CLI command routing on top of `@rhythmjs/rhythm`. `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 via `.use(cli.commands())`, so an unmatched command correctly falls through to whatever's registered after it.
3
+ CLI command routing on top of `@rhythmjs/rhythm`. `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
+
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 and the adapters.
4
6
 
5
7
  ## Example
6
8
 
7
9
  ```ts
10
+ import { Rhythm } from "@rhythmjs/rhythm";
8
11
  import { RhythmCli } from "@rhythmjs/cli";
9
12
  import { toCliHandler } from "@rhythmjs/cli/adapters/bun";
13
+ import type { RhythmCliContext } from "@rhythmjs/cli/adapters/context";
10
14
 
11
15
  const cli = new RhythmCli().command("deploy :environment", (ctx) => {
12
16
  ctx.response.print(`deploying to ${ctx.args.environment}`);
13
17
  });
14
18
 
15
- process.exitCode = await toCliHandler(cli)(process.argv.slice(2));
19
+ const app = new Rhythm<RhythmCliContext>().use(cli.commands());
20
+ process.exitCode = await toCliHandler(app)(process.argv.slice(2));
16
21
  ```
17
22
 
18
23
  A fuller runnable version, including nested command groups and interactive prompts, is at [`examples/cli`](../../examples/cli).
@@ -23,17 +28,19 @@ A fuller runnable version, including nested command groups and interactive promp
23
28
  - **`ctx.args`** — captured `:name` command tokens, added once a command matches.
24
29
  - **`ctx.flags`** — parsed `--foo` / `--foo=bar` / `-f` options. Schema-less: a bare `--foo` is boolean `true`, `--foo bar` takes the next token as its value unless the token itself looks like a flag — same default behavior as `minimist`.
25
30
  - **`ctx.stdin`** — a `ReadableStream`, or `null` when stdin is a TTY (nothing piped in).
26
- - **Interactive prompts** — `createPrompt()` gives `text()`/`confirm()`/`select()`/`multiSelect()` (the latter two support an `allowCustom` option that adds a "type your own" choice). Wire it in via a normal `.provide()` — see the example.
27
- - **`register()` is disabled** on `RhythmCli`, same reasoning as `RhythmRouter`.
31
+ - **Interactive prompts** — `createPrompt()` gives `text()`/`confirm()`/`select()`/`multiSelect()` (the latter two support an `allowCustom` option that adds a "type your own" choice). Wire it in via a `.provide()` on the host `Rhythm` app — see the example.
32
+ - **Prefixes compose across nesting** — a child cli mounted into a prefixed parent via `.use(child)` gets the parent's prefix segments joined onto every one of its commands, at any nesting depth. Mounting copies the child's commands and middleware at that moment; commands added to the child afterwards don't appear in the parent, and the child keeps working standalone.
33
+ - **Registration order is execution order** — a `.use()` middleware wraps only the commands registered after it; commands registered before it are untouched, and a matched command that doesn't call `next()` returns without reaching anything registered later. An unmatched command falls through, entry by entry, to the outer `next()`.
34
+ - **A cli is a controller, not a module** — it has no `provide()` or `register()`, and it cannot be `register()`ed into a `Rhythm` app either; `register()` composes `Rhythm` modules only. A cli mounts into an app exactly one way: koa-style, via `.use(cli.commands())`.
28
35
 
29
36
  ## API
30
37
 
31
- - `new RhythmCli(options?)` — `options.name`, `options.prefix` (space-separated, e.g. `"remote"`).
38
+ - `new RhythmCli(options?)` — `options.prefix` (space-separated, e.g. `"remote"`).
32
39
  - `.command(path, ...handlers)` — register a command; `path` is space-separated and may contain `:param` tokens (e.g. `"deploy :environment"`).
33
- - `.use(fn)` — plain middleware, or mount a nested `RhythmCli` via `.use(child.commands())`.
34
- - `.commands()` — returns this CLI as a plain middleware, for mounting into a parent via `.use()`.
35
- - `toCliHandler(app)` — bridges a `Rhythm`/`RhythmCli` app to `(argv: string[]) => Promise<number>`.
36
- - `createPrompt()` — a readline-backed `{ text, confirm, select, multiSelect }` prompt, for use inside a `.provide()`.
40
+ - `.use(fn)` — plain middleware. `.use(child)` — mount a nested `RhythmCli` (prefixes compose).
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())` — an opaque middleware can't have the parent's prefix applied to its commands.
42
+ - `toCliHandler(app)` — bridges a `Rhythm` app to `(argv: string[]) => Promise<number>`.
43
+ - `createPrompt()` — a readline-backed `{ text, confirm, select, multiSelect }` prompt, for use inside a `.provide()` on the host app.
37
44
 
38
45
  ## Runtime adapters
39
46
 
@@ -1,19 +1,17 @@
1
1
  import { t as RhythmCliContext } from "./context-SCtZ6pOg.js";
2
- import { DeepReadonly, Middleware, OmitHashKeys, Rhythm } from "@rhythmjs/rhythm";
2
+ import { Middleware } from "@rhythmjs/rhythm";
3
3
  //#region src/rhythm-cli.d.ts
4
4
  export interface RhythmCliCommandContext {
5
5
  args: Record<string, string>;
6
6
  }
7
7
  export interface RhythmCliOptions {
8
- name?: string;
9
8
  prefix?: string;
10
9
  }
11
- export declare class RhythmCli<TContext extends RhythmCliContext = RhythmCliContext, TProviders extends object = {}> extends Rhythm<RhythmCliContext, TContext, TProviders> {
10
+ export declare class RhythmCli<TContext extends RhythmCliContext = RhythmCliContext> {
12
11
  #private;
13
12
  constructor(options?: RhythmCliOptions);
14
- use<TExtra extends object = {}>(fn: Middleware<TContext>): RhythmCli<TContext & TExtra, TProviders>;
15
- provide<TValue extends object>(factory: (deps: DeepReadonly<TProviders>) => TValue | Promise<TValue>, dispose?: (value: TValue) => void | Promise<void>): RhythmCli<TContext & OmitHashKeys<TValue>, TProviders & OmitHashKeys<TValue>>;
16
- register(): never;
13
+ use(child: RhythmCli<any>): this;
14
+ use<TExtra extends object = {}>(fn: Middleware<TContext>): RhythmCli<TContext & TExtra>;
17
15
  command(path: string, ...handlers: Middleware<TContext & RhythmCliCommandContext>[]): this;
18
16
  commands(): Middleware<TContext>;
19
17
  }
@@ -1,5 +1,5 @@
1
1
  import { parseArgv } from "./argv.js";
2
- import { Rhythm, compose } from "@rhythmjs/rhythm";
2
+ import { compose } from "@rhythmjs/rhythm";
3
3
  //#region src/rhythm-cli.ts
4
4
  function toSegments(command) {
5
5
  return command.trim().split(/\s+/).filter(Boolean);
@@ -15,81 +15,85 @@ function matchCommand(pattern, positionals) {
15
15
  }
16
16
  return params;
17
17
  }
18
- const RhythmCliTag = Symbol("RhythmCliTag");
19
- var RhythmCli = class extends Rhythm {
20
- #prefixSegments;
18
+ var RhythmCli = class RhythmCli {
19
+ #options;
21
20
  #entries = [];
22
- #commands = [];
23
- #dispatchInstalled = false;
21
+ #composed = null;
24
22
  constructor(options = {}) {
25
- super({
26
- name: options.name ?? "cli",
27
- type: "controller"
28
- });
29
- this.#prefixSegments = options.prefix ? toSegments(options.prefix) : [];
23
+ this.#options = options;
24
+ }
25
+ get #prefixSegments() {
26
+ return this.#options.prefix ? toSegments(this.#options.prefix) : [];
30
27
  }
31
- use(fn) {
32
- const nested = fn[RhythmCliTag];
33
- if (nested) for (const entry of nested.#entries) this.#mount(entry);
28
+ use(arg) {
29
+ if (arg instanceof RhythmCli) for (const entry of arg.#entries) this.#entries.push(entry.kind === "command" ? {
30
+ ...entry,
31
+ segments: [...this.#prefixSegments, ...entry.segments]
32
+ } : entry);
34
33
  else {
34
+ if (typeof arg !== "function") throw new TypeError("middleware must be a function!");
35
35
  this.#entries.push({
36
36
  kind: "middleware",
37
- fn
37
+ fn: arg
38
38
  });
39
- super.use(fn);
40
39
  }
40
+ this.#composed = null;
41
41
  return this;
42
42
  }
43
- provide(factory, dispose) {
44
- super.provide(factory, dispose);
45
- return this;
46
- }
47
- register() {
48
- throw new Error("RhythmCli is a controller and cannot register() other modules or controllers");
49
- }
50
- #mount(entry) {
51
- if (entry.kind === "middleware") {
52
- this.#entries.push(entry);
53
- super.use(entry.fn);
54
- return;
55
- }
56
- this.#registerCommand([...this.#prefixSegments, ...entry.segments], entry.handlers);
57
- }
58
- #registerCommand(segments, handlers) {
43
+ command(path, ...handlers) {
59
44
  this.#entries.push({
60
45
  kind: "command",
61
- segments,
46
+ segments: [...this.#prefixSegments, ...toSegments(path)],
62
47
  handlers
63
48
  });
64
- this.#commands.push({
65
- segments,
66
- dispatch: compose(handlers)
67
- });
68
- if (this.#dispatchInstalled) return;
69
- this.#dispatchInstalled = true;
70
- const commands = this.#commands;
71
- super.use(async (ctx, next) => {
72
- const { positionals } = parseArgv(ctx.argv);
73
- for (const cmd of commands) {
74
- const params = matchCommand(cmd.segments, positionals);
75
- if (!params) continue;
76
- await cmd.dispatch({
77
- ...ctx,
78
- args: params
79
- }, next);
80
- return;
81
- }
82
- await next();
83
- });
84
- }
85
- command(path, ...handlers) {
86
- this.#registerCommand([...this.#prefixSegments, ...toSegments(path)], handlers);
49
+ this.#composed = null;
87
50
  return this;
88
51
  }
52
+ #compile() {
53
+ if (this.#composed) return this.#composed;
54
+ const dispatchFor = (compiled) => {
55
+ return async (ctx, next) => {
56
+ const { positionals } = parseArgv(ctx.argv);
57
+ for (const command of compiled) {
58
+ const args = matchCommand(command.segments, positionals);
59
+ if (!args) continue;
60
+ await command.dispatch({
61
+ ...ctx,
62
+ args
63
+ }, next);
64
+ return;
65
+ }
66
+ await next();
67
+ };
68
+ };
69
+ const stack = [];
70
+ let i = 0;
71
+ while (i < this.#entries.length) {
72
+ const entry = this.#entries[i];
73
+ if (entry.kind === "middleware") {
74
+ stack.push(entry.fn);
75
+ i++;
76
+ continue;
77
+ }
78
+ const compiled = [];
79
+ while (i < this.#entries.length) {
80
+ const command = this.#entries[i];
81
+ if (command.kind !== "command") break;
82
+ compiled.push({
83
+ segments: command.segments,
84
+ dispatch: compose(command.handlers)
85
+ });
86
+ i++;
87
+ }
88
+ stack.push(dispatchFor(compiled));
89
+ }
90
+ this.#composed = compose(stack);
91
+ return this.#composed;
92
+ }
89
93
  commands() {
90
- const mw = this.middleware();
91
- mw[RhythmCliTag] = this;
92
- return mw;
94
+ return async (ctx, next) => {
95
+ await this.#compile()(ctx, next);
96
+ };
93
97
  }
94
98
  };
95
99
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@rhythmjs/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Command-line argument parsing, prompts, and command routing for the Rhythm middleware kernel.",
5
+ "homepage": "https://rhythm.js.org/cli",
5
6
  "license": "ISC",
6
7
  "repository": {
7
8
  "type": "git",
@@ -48,7 +49,7 @@
48
49
  "access": "public"
49
50
  },
50
51
  "dependencies": {
51
- "@rhythmjs/rhythm": "0.0.1"
52
+ "@rhythmjs/rhythm": "0.0.2"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/bun": "^1.4.2",
@@ -58,10 +59,5 @@
58
59
  },
59
60
  "engines": {
60
61
  "node": ">=20.19.0"
61
- },
62
- "scripts": {
63
- "build": "vp pack",
64
- "typecheck": "tsc --noEmit",
65
- "test": "vp test"
66
62
  }
67
63
  }