@rhythmjs/cli 0.0.13 → 0.0.15
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 +9 -8
- package/dist/rhythm-cli.d.ts +12 -3
- package/dist/rhythm-cli.js +43 -3
- package/dist/run.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @rhythmjs/cli
|
|
2
2
|
|
|
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.
|
|
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.middleware())`, 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 (`.
|
|
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 (`.middleware()`). It shares the core middleware contract (`compose`, `Middleware`, `derive`; `next()` takes no arguments, extend the context with `derive()`), but has no a startup `context` 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
|
|
|
@@ -16,7 +16,7 @@ const cli = new RhythmCli().command("deploy :environment", (ctx) => {
|
|
|
16
16
|
ctx.response.print(`deploying to ${ctx.args.environment}`);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const app = new Rhythm<RhythmCliContext>().use(cli.
|
|
19
|
+
const app = new Rhythm<RhythmCliContext>().use(cli.middleware());
|
|
20
20
|
process.exitCode = await toCliHandler(app)(process.argv.slice(2));
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -28,19 +28,20 @@ A fuller runnable version, including nested command groups and interactive promp
|
|
|
28
28
|
- **`ctx.args`**: captured `:name` command tokens, added once a command matches.
|
|
29
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, the same default behavior as `minimist`.
|
|
30
30
|
- **`ctx.stdin`**: a `ReadableStream`, or `null` when stdin is a TTY (nothing piped in).
|
|
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
|
|
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 `app.context.prompt = ...` on the host `Rhythm` app; see the example.
|
|
32
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 `
|
|
33
|
+
- **Registration order is execution order**: a `.use()` middleware wraps only the commands registered after it, and runs only when one of them matches the argv (so a guard never answers `help` or another cli's commands); a mounted cli (`.use(child.middleware())`) always runs; 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 a startup `context` 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.middleware())`.
|
|
35
35
|
|
|
36
36
|
## API
|
|
37
37
|
|
|
38
38
|
- `new RhythmCli(options?)`: `options.prefix` (space-separated, e.g. `"remote"`).
|
|
39
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
|
+
- `.middleware()`: 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
|
+
- `.entries`: a read-only snapshot of registered middlewares and commands, in order. `.middleware()` is tagged with the cli as its source, so a parent module lists it in `sources`.
|
|
42
43
|
- `toCliHandler(app)`: bridges a `Rhythm` app to `(argv: string[]) => Promise<number>`.
|
|
43
|
-
- `createPrompt()`: a `{ text, confirm, select, multiSelect }` prompt reading lines through Bun's async-iterable `console`, for use
|
|
44
|
+
- `createPrompt()`: a `{ text, confirm, select, multiSelect }` prompt reading lines through Bun's async-iterable `console`, for use with `app.context` on the host app.
|
|
44
45
|
|
|
45
46
|
## Running on Bun
|
|
46
47
|
|
package/dist/rhythm-cli.d.ts
CHANGED
|
@@ -6,12 +6,21 @@ export interface RhythmCliCommandContext {
|
|
|
6
6
|
export interface RhythmCliOptions {
|
|
7
7
|
prefix?: string;
|
|
8
8
|
}
|
|
9
|
-
export
|
|
9
|
+
export type CliEntry = {
|
|
10
|
+
readonly kind: "middleware";
|
|
11
|
+
readonly fn: Middleware<any>;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "command";
|
|
14
|
+
readonly segments: readonly string[];
|
|
15
|
+
readonly handlers: readonly Middleware<any>[];
|
|
16
|
+
};
|
|
17
|
+
export declare class RhythmCli<TContext extends RhythmCliContext = RhythmCliContext, TInput extends RhythmCliContext = TContext> {
|
|
10
18
|
#private;
|
|
11
19
|
constructor(options?: RhythmCliOptions);
|
|
12
|
-
|
|
20
|
+
get entries(): readonly CliEntry[];
|
|
21
|
+
use<TExtra extends object>(fn: DeriveMiddleware<TContext, TExtra>): RhythmCli<TContext & TExtra, TInput>;
|
|
13
22
|
use(fn: Middleware<TContext>): this;
|
|
14
23
|
command<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmCliCommandContext, TExtra>, ...handlers: Middleware<TContext & RhythmCliCommandContext & TExtra>[]): this;
|
|
15
24
|
command(path: string, ...handlers: Middleware<TContext & RhythmCliCommandContext>[]): this;
|
|
16
|
-
middleware(): Middleware<
|
|
25
|
+
middleware(): Middleware<TInput>;
|
|
17
26
|
}
|
package/dist/rhythm-cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// src/rhythm-cli.ts
|
|
7
7
|
import { compose } from "@rhythmjs/rhythm/compose";
|
|
8
|
+
import { sourceOf, withSource } from "@rhythmjs/rhythm/source";
|
|
8
9
|
function toSegments(command) {
|
|
9
10
|
return command.trim().split(/\s+/).filter(Boolean);
|
|
10
11
|
}
|
|
@@ -43,6 +44,20 @@ function assertValidPattern(path, segments) {
|
|
|
43
44
|
throw new TypeError(`command "${path}" has more than one catch-all`);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
47
|
+
function mountedCommands(cli, out, seen = new Set) {
|
|
48
|
+
if (seen.has(cli))
|
|
49
|
+
return;
|
|
50
|
+
seen.add(cli);
|
|
51
|
+
for (const entry of cli.entries) {
|
|
52
|
+
if (entry.kind === "command")
|
|
53
|
+
out.push({ segments: [...entry.segments] });
|
|
54
|
+
else {
|
|
55
|
+
const child = sourceOf(entry.fn);
|
|
56
|
+
if (child instanceof RhythmCli)
|
|
57
|
+
mountedCommands(child, out, seen);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
46
61
|
|
|
47
62
|
class RhythmCli {
|
|
48
63
|
#options;
|
|
@@ -50,6 +65,9 @@ class RhythmCli {
|
|
|
50
65
|
constructor(options = {}) {
|
|
51
66
|
this.#options = options;
|
|
52
67
|
}
|
|
68
|
+
get entries() {
|
|
69
|
+
return [...this.#entries];
|
|
70
|
+
}
|
|
53
71
|
get #prefixSegments() {
|
|
54
72
|
return this.#options.prefix ? toSegments(this.#options.prefix) : [];
|
|
55
73
|
}
|
|
@@ -66,6 +84,15 @@ class RhythmCli {
|
|
|
66
84
|
return this;
|
|
67
85
|
}
|
|
68
86
|
#compile() {
|
|
87
|
+
const groups = [];
|
|
88
|
+
const reaches = (ctx, from) => {
|
|
89
|
+
const { positionals } = parseArgv2(ctx.argv);
|
|
90
|
+
for (let g = from;g < groups.length; g++) {
|
|
91
|
+
if (groups[g].some((command) => matchCommand(command.segments, positionals)))
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
};
|
|
69
96
|
const dispatchFor = (compiled) => {
|
|
70
97
|
return async (ctx, next) => {
|
|
71
98
|
const { positionals } = parseArgv2(ctx.argv);
|
|
@@ -84,7 +111,19 @@ class RhythmCli {
|
|
|
84
111
|
while (i < this.#entries.length) {
|
|
85
112
|
const entry = this.#entries[i];
|
|
86
113
|
if (entry.kind === "middleware") {
|
|
87
|
-
|
|
114
|
+
const { fn } = entry;
|
|
115
|
+
const source = sourceOf(fn);
|
|
116
|
+
if (source) {
|
|
117
|
+
if (source instanceof RhythmCli) {
|
|
118
|
+
const commands = [];
|
|
119
|
+
mountedCommands(source, commands);
|
|
120
|
+
groups.push(commands);
|
|
121
|
+
}
|
|
122
|
+
stack.push(fn);
|
|
123
|
+
} else {
|
|
124
|
+
const from = groups.length;
|
|
125
|
+
stack.push((ctx, next) => reaches(ctx, from) ? fn(ctx, next) : next());
|
|
126
|
+
}
|
|
88
127
|
i++;
|
|
89
128
|
continue;
|
|
90
129
|
}
|
|
@@ -96,15 +135,16 @@ class RhythmCli {
|
|
|
96
135
|
compiled.push({ segments: command.segments, dispatch: compose(command.handlers) });
|
|
97
136
|
i++;
|
|
98
137
|
}
|
|
138
|
+
groups.push(compiled);
|
|
99
139
|
stack.push(dispatchFor(compiled));
|
|
100
140
|
}
|
|
101
141
|
return compose(stack);
|
|
102
142
|
}
|
|
103
143
|
middleware() {
|
|
104
144
|
const fn = this.#compile();
|
|
105
|
-
return async (ctx, next) => {
|
|
145
|
+
return withSource(async (ctx, next) => {
|
|
106
146
|
await fn(ctx, next);
|
|
107
|
-
};
|
|
147
|
+
}, this);
|
|
108
148
|
}
|
|
109
149
|
}
|
|
110
150
|
export {
|
package/dist/run.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Rhythm } from "@rhythmjs/rhythm";
|
|
2
2
|
import { type RhythmPrompt } from "./prompt";
|
|
3
3
|
import { type RhythmCliContext } from "./context";
|
|
4
|
-
export declare function toCliHandler<TContext extends RhythmCliContext
|
|
4
|
+
export declare function toCliHandler<TContext extends RhythmCliContext>(app: Rhythm<RhythmCliContext, any, TContext>): (argv: string[]) => Promise<number>;
|
|
5
5
|
export declare function createPrompt(): {
|
|
6
6
|
prompt: RhythmPrompt;
|
|
7
7
|
close: () => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhythmjs/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
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.
|
|
52
|
+
"@rhythmjs/rhythm": "0.0.15"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/bun": "^1.4.2",
|