@rhythmjs/cli 0.0.1
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 +44 -0
- package/dist/adapters/bun.d.ts +10 -0
- package/dist/adapters/bun.js +39 -0
- package/dist/adapters/context.d.ts +2 -0
- package/dist/adapters/context.js +22 -0
- package/dist/adapters/deno.d.ts +2 -0
- package/dist/adapters/deno.js +2 -0
- package/dist/adapters/node.d.ts +10 -0
- package/dist/adapters/node.js +40 -0
- package/dist/argv.d.ts +7 -0
- package/dist/argv.js +47 -0
- package/dist/context-SCtZ6pOg.d.ts +19 -0
- package/dist/prompt-DXqXd-xC.d.ts +26 -0
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +62 -0
- package/dist/rhythm-cli.d.ts +20 -0
- package/dist/rhythm-cli.js +96 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @rhythmjs/cli
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { RhythmCli } from "@rhythmjs/cli";
|
|
9
|
+
import { toCliHandler } from "@rhythmjs/cli/adapters/bun";
|
|
10
|
+
|
|
11
|
+
const cli = new RhythmCli().command("deploy :environment", (ctx) => {
|
|
12
|
+
ctx.response.print(`deploying to ${ctx.args.environment}`);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
process.exitCode = await toCliHandler(cli)(process.argv.slice(2));
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
A fuller runnable version, including nested command groups and interactive prompts, is at [`examples/cli`](../../examples/cli).
|
|
19
|
+
|
|
20
|
+
## Concepts
|
|
21
|
+
|
|
22
|
+
- **`ctx.response`** — `print(line)`, `printError(line)`, `exit(code)`, all chainable. Flushed to the console and returned as the process exit code by the adapter.
|
|
23
|
+
- **`ctx.args`** — captured `:name` command tokens, added once a command matches.
|
|
24
|
+
- **`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
|
+
- **`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`.
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
- `new RhythmCli(options?)` — `options.name`, `options.prefix` (space-separated, e.g. `"remote"`).
|
|
32
|
+
- `.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()`.
|
|
37
|
+
|
|
38
|
+
## Runtime adapters
|
|
39
|
+
|
|
40
|
+
`RhythmCli` itself is runtime-agnostic; only stdin access and the interactive prompt's I/O touch a specific runtime.
|
|
41
|
+
|
|
42
|
+
- **`@rhythmjs/cli/adapters/bun`** — uses `Bun.stdin.stream()`, Bun's own native stdin API.
|
|
43
|
+
- **`@rhythmjs/cli/adapters/node`** — the portable version: `process.stdin` + `node:stream`'s `Readable.toWeb()`, and `node:readline` for `createPrompt()`. No Bun-specific API at all.
|
|
44
|
+
- **`@rhythmjs/cli/adapters/deno`** — re-exports the Node adapter unchanged. Deno's Node-compat layer implements `process.stdin`, `Readable.toWeb`/`fromWeb`, and `node:readline`, so the same code works correctly under Deno.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { t as RhythmPrompt } from "../prompt-DXqXd-xC.js";
|
|
2
|
+
import { t as RhythmCliContext } from "../context-SCtZ6pOg.js";
|
|
3
|
+
import { Rhythm } from "@rhythmjs/rhythm";
|
|
4
|
+
//#region src/adapters/bun.d.ts
|
|
5
|
+
export declare function toCliHandler<TContext extends RhythmCliContext, TProviders extends object = {}>(app: Rhythm<RhythmCliContext, TContext, TProviders>): (argv: string[]) => Promise<number>;
|
|
6
|
+
export declare function createPrompt(): {
|
|
7
|
+
prompt: RhythmPrompt;
|
|
8
|
+
close: () => void;
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { parseArgv } from "../argv.js";
|
|
2
|
+
import { createPrompt as createPrompt$1 } from "../prompt.js";
|
|
3
|
+
import { RhythmCliResponse } from "./context.js";
|
|
4
|
+
import * as readline from "node:readline";
|
|
5
|
+
//#region src/adapters/bun.ts
|
|
6
|
+
function toCliHandler(app) {
|
|
7
|
+
const run = app.callback();
|
|
8
|
+
return async (argv) => {
|
|
9
|
+
const { flags } = parseArgv(argv);
|
|
10
|
+
const stdin = process.stdin.isTTY ? null : Bun.stdin.stream();
|
|
11
|
+
const ctx = await run({
|
|
12
|
+
argv,
|
|
13
|
+
flags,
|
|
14
|
+
stdin,
|
|
15
|
+
response: new RhythmCliResponse()
|
|
16
|
+
});
|
|
17
|
+
for (const line of ctx.response.stdout) console.log(line);
|
|
18
|
+
for (const line of ctx.response.stderr) console.error(line);
|
|
19
|
+
return ctx.response.exitCode;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function defaultPromptIO(input = process.stdin, output = process.stdout) {
|
|
23
|
+
const rl = readline.createInterface({
|
|
24
|
+
input,
|
|
25
|
+
output
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
ask: (query) => new Promise((resolve) => rl.question(query, resolve)),
|
|
29
|
+
write: (text) => {
|
|
30
|
+
output.write(text);
|
|
31
|
+
},
|
|
32
|
+
close: () => rl.close()
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function createPrompt() {
|
|
36
|
+
return createPrompt$1(defaultPromptIO());
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { createPrompt, toCliHandler };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { RhythmMutable } from "@rhythmjs/rhythm";
|
|
2
|
+
//#region src/adapters/context.ts
|
|
3
|
+
var RhythmCliResponse = class {
|
|
4
|
+
[RhythmMutable] = true;
|
|
5
|
+
exitCode = 0;
|
|
6
|
+
stdout = [];
|
|
7
|
+
stderr = [];
|
|
8
|
+
print(line) {
|
|
9
|
+
this.stdout.push(line);
|
|
10
|
+
return this;
|
|
11
|
+
}
|
|
12
|
+
printError(line) {
|
|
13
|
+
this.stderr.push(line);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
exit(code) {
|
|
17
|
+
this.exitCode = code;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { RhythmCliResponse };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { t as RhythmPrompt } from "../prompt-DXqXd-xC.js";
|
|
2
|
+
import { t as RhythmCliContext } from "../context-SCtZ6pOg.js";
|
|
3
|
+
import { Rhythm } from "@rhythmjs/rhythm";
|
|
4
|
+
//#region src/adapters/node.d.ts
|
|
5
|
+
export declare function toCliHandler<TContext extends RhythmCliContext, TProviders extends object = {}>(app: Rhythm<RhythmCliContext, TContext, TProviders>): (argv: string[]) => Promise<number>;
|
|
6
|
+
export declare function createPrompt(): {
|
|
7
|
+
prompt: RhythmPrompt;
|
|
8
|
+
close: () => void;
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parseArgv } from "../argv.js";
|
|
2
|
+
import { createPrompt as createPrompt$1 } from "../prompt.js";
|
|
3
|
+
import { RhythmCliResponse } from "./context.js";
|
|
4
|
+
import * as readline from "node:readline";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
//#region src/adapters/node.ts
|
|
7
|
+
function toCliHandler(app) {
|
|
8
|
+
const run = app.callback();
|
|
9
|
+
return async (argv) => {
|
|
10
|
+
const { flags } = parseArgv(argv);
|
|
11
|
+
const stdin = process.stdin.isTTY ? null : Readable.toWeb(process.stdin);
|
|
12
|
+
const ctx = await run({
|
|
13
|
+
argv,
|
|
14
|
+
flags,
|
|
15
|
+
stdin,
|
|
16
|
+
response: new RhythmCliResponse()
|
|
17
|
+
});
|
|
18
|
+
for (const line of ctx.response.stdout) console.log(line);
|
|
19
|
+
for (const line of ctx.response.stderr) console.error(line);
|
|
20
|
+
return ctx.response.exitCode;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function defaultPromptIO(input = process.stdin, output = process.stdout) {
|
|
24
|
+
const rl = readline.createInterface({
|
|
25
|
+
input,
|
|
26
|
+
output
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
ask: (query) => new Promise((resolve) => rl.question(query, resolve)),
|
|
30
|
+
write: (text) => {
|
|
31
|
+
output.write(text);
|
|
32
|
+
},
|
|
33
|
+
close: () => rl.close()
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createPrompt() {
|
|
37
|
+
return createPrompt$1(defaultPromptIO());
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { createPrompt, toCliHandler };
|
package/dist/argv.d.ts
ADDED
package/dist/argv.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/argv.ts
|
|
2
|
+
function parseArgv(argv) {
|
|
3
|
+
const positionals = [];
|
|
4
|
+
const flags = {};
|
|
5
|
+
let rawMode = false;
|
|
6
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7
|
+
const token = argv[i];
|
|
8
|
+
if (rawMode) {
|
|
9
|
+
positionals.push(token);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (token === "--") {
|
|
13
|
+
rawMode = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (token.startsWith("--")) {
|
|
17
|
+
const eqIndex = token.indexOf("=");
|
|
18
|
+
if (eqIndex !== -1) {
|
|
19
|
+
flags[token.slice(2, eqIndex)] = token.slice(eqIndex + 1);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const name = token.slice(2);
|
|
23
|
+
const next = argv[i + 1];
|
|
24
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
25
|
+
flags[name] = next;
|
|
26
|
+
i++;
|
|
27
|
+
} else flags[name] = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
31
|
+
const name = token.slice(1);
|
|
32
|
+
const next = argv[i + 1];
|
|
33
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
34
|
+
flags[name] = next;
|
|
35
|
+
i++;
|
|
36
|
+
} else flags[name] = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
positionals.push(token);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
positionals,
|
|
43
|
+
flags
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { parseArgv };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { RhythmMutable } from "@rhythmjs/rhythm";
|
|
2
|
+
//#region src/adapters/context.d.ts
|
|
3
|
+
declare class RhythmCliResponse {
|
|
4
|
+
readonly [RhythmMutable] = true;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
stdout: string[];
|
|
7
|
+
stderr: string[];
|
|
8
|
+
print(line: string): this;
|
|
9
|
+
printError(line: string): this;
|
|
10
|
+
exit(code: number): this;
|
|
11
|
+
}
|
|
12
|
+
interface RhythmCliContext {
|
|
13
|
+
argv: string[];
|
|
14
|
+
flags: Record<string, string | boolean>;
|
|
15
|
+
stdin: ReadableStream<Uint8Array> | null;
|
|
16
|
+
response: RhythmCliResponse;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { RhythmCliResponse as n, RhythmCliContext as t };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/prompt.d.ts
|
|
2
|
+
interface RhythmPrompt {
|
|
3
|
+
text(message: string, options?: {
|
|
4
|
+
default?: string;
|
|
5
|
+
}): Promise<string>;
|
|
6
|
+
confirm(message: string, options?: {
|
|
7
|
+
default?: boolean;
|
|
8
|
+
}): Promise<boolean>;
|
|
9
|
+
select<T extends string>(message: string, choices: readonly T[], options?: {
|
|
10
|
+
allowCustom?: boolean;
|
|
11
|
+
}): Promise<T | string>;
|
|
12
|
+
multiSelect<T extends string>(message: string, choices: readonly T[], options?: {
|
|
13
|
+
allowCustom?: boolean;
|
|
14
|
+
}): Promise<(T | string)[]>;
|
|
15
|
+
}
|
|
16
|
+
interface RhythmPromptIO {
|
|
17
|
+
ask(this: void, query: string): Promise<string>;
|
|
18
|
+
write(this: void, text: string): void;
|
|
19
|
+
close?(this: void): void;
|
|
20
|
+
}
|
|
21
|
+
declare function createPrompt(io: RhythmPromptIO): {
|
|
22
|
+
prompt: RhythmPrompt;
|
|
23
|
+
close: () => void;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { RhythmPromptIO as n, createPrompt as r, RhythmPrompt as t };
|
package/dist/prompt.d.ts
ADDED
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/prompt.ts
|
|
2
|
+
const CUSTOM_LABEL = "Other (type your own)";
|
|
3
|
+
function createPrompt(io) {
|
|
4
|
+
const { ask, write } = io;
|
|
5
|
+
const text = async (message, options) => {
|
|
6
|
+
const suffix = options?.default ? ` (${options.default})` : "";
|
|
7
|
+
return (await ask(`${message}${suffix} `)).trim() || options?.default || "";
|
|
8
|
+
};
|
|
9
|
+
const confirm = async (message, options) => {
|
|
10
|
+
const suffix = options?.default === void 0 ? "(y/n)" : options.default ? "(Y/n)" : "(y/N)";
|
|
11
|
+
while (true) {
|
|
12
|
+
const answer = (await ask(`${message} ${suffix} `)).trim().toLowerCase();
|
|
13
|
+
if (!answer && options?.default !== void 0) return options.default;
|
|
14
|
+
if (answer === "y" || answer === "yes") return true;
|
|
15
|
+
if (answer === "n" || answer === "no") return false;
|
|
16
|
+
write("Please answer y or n.\n");
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
function listChoices(message, choices, allowCustom) {
|
|
20
|
+
write(`${message}\n`);
|
|
21
|
+
choices.forEach((choice, i) => write(` ${i + 1}) ${choice}\n`));
|
|
22
|
+
const customIndex = allowCustom ? choices.length + 1 : -1;
|
|
23
|
+
if (allowCustom) write(` ${customIndex}) ${CUSTOM_LABEL}\n`);
|
|
24
|
+
return customIndex;
|
|
25
|
+
}
|
|
26
|
+
const select = async (message, choices, options) => {
|
|
27
|
+
const customIndex = listChoices(message, choices, options?.allowCustom);
|
|
28
|
+
while (true) {
|
|
29
|
+
const index = Number(await ask("Enter number: "));
|
|
30
|
+
if (Number.isInteger(index) && index >= 1 && index <= choices.length) return choices[index - 1];
|
|
31
|
+
if (index === customIndex) return text("Enter value:");
|
|
32
|
+
write("Invalid choice, try again.\n");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const multiSelect = async (message, choices, options) => {
|
|
36
|
+
const customIndex = listChoices(message, choices, options?.allowCustom);
|
|
37
|
+
const maxIndex = options?.allowCustom ? customIndex : choices.length;
|
|
38
|
+
write("(comma-separated numbers, e.g. \"1,3\")\n");
|
|
39
|
+
while (true) {
|
|
40
|
+
const raw = await ask("Enter numbers: ");
|
|
41
|
+
const indices = raw.split(",").map((part) => Number(part.trim())).filter((n) => n !== 0 || raw.trim() === "0");
|
|
42
|
+
if (!(indices.length > 0 && indices.every((n) => Number.isInteger(n) && n >= 1 && n <= maxIndex))) {
|
|
43
|
+
write("Invalid choices, try again.\n");
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const results = [];
|
|
47
|
+
for (const n of indices) results.push(n === customIndex ? await text("Enter value:") : choices[n - 1]);
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
prompt: {
|
|
53
|
+
text,
|
|
54
|
+
confirm,
|
|
55
|
+
select,
|
|
56
|
+
multiSelect
|
|
57
|
+
},
|
|
58
|
+
close: () => io.close?.()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { createPrompt };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { t as RhythmCliContext } from "./context-SCtZ6pOg.js";
|
|
2
|
+
import { DeepReadonly, Middleware, OmitHashKeys, Rhythm } from "@rhythmjs/rhythm";
|
|
3
|
+
//#region src/rhythm-cli.d.ts
|
|
4
|
+
export interface RhythmCliCommandContext {
|
|
5
|
+
args: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export interface RhythmCliOptions {
|
|
8
|
+
name?: string;
|
|
9
|
+
prefix?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class RhythmCli<TContext extends RhythmCliContext = RhythmCliContext, TProviders extends object = {}> extends Rhythm<RhythmCliContext, TContext, TProviders> {
|
|
12
|
+
#private;
|
|
13
|
+
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;
|
|
17
|
+
command(path: string, ...handlers: Middleware<TContext & RhythmCliCommandContext>[]): this;
|
|
18
|
+
commands(): Middleware<TContext>;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { parseArgv } from "./argv.js";
|
|
2
|
+
import { Rhythm, compose } from "@rhythmjs/rhythm";
|
|
3
|
+
//#region src/rhythm-cli.ts
|
|
4
|
+
function toSegments(command) {
|
|
5
|
+
return command.trim().split(/\s+/).filter(Boolean);
|
|
6
|
+
}
|
|
7
|
+
function matchCommand(pattern, positionals) {
|
|
8
|
+
if (pattern.length !== positionals.length) return null;
|
|
9
|
+
const params = {};
|
|
10
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
11
|
+
const segment = pattern[i];
|
|
12
|
+
const token = positionals[i];
|
|
13
|
+
if (segment.startsWith(":")) params[segment.slice(1)] = token;
|
|
14
|
+
else if (segment !== token) return null;
|
|
15
|
+
}
|
|
16
|
+
return params;
|
|
17
|
+
}
|
|
18
|
+
const RhythmCliTag = Symbol("RhythmCliTag");
|
|
19
|
+
var RhythmCli = class extends Rhythm {
|
|
20
|
+
#prefixSegments;
|
|
21
|
+
#entries = [];
|
|
22
|
+
#commands = [];
|
|
23
|
+
#dispatchInstalled = false;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
super({
|
|
26
|
+
name: options.name ?? "cli",
|
|
27
|
+
type: "controller"
|
|
28
|
+
});
|
|
29
|
+
this.#prefixSegments = options.prefix ? toSegments(options.prefix) : [];
|
|
30
|
+
}
|
|
31
|
+
use(fn) {
|
|
32
|
+
const nested = fn[RhythmCliTag];
|
|
33
|
+
if (nested) for (const entry of nested.#entries) this.#mount(entry);
|
|
34
|
+
else {
|
|
35
|
+
this.#entries.push({
|
|
36
|
+
kind: "middleware",
|
|
37
|
+
fn
|
|
38
|
+
});
|
|
39
|
+
super.use(fn);
|
|
40
|
+
}
|
|
41
|
+
return this;
|
|
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) {
|
|
59
|
+
this.#entries.push({
|
|
60
|
+
kind: "command",
|
|
61
|
+
segments,
|
|
62
|
+
handlers
|
|
63
|
+
});
|
|
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);
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
commands() {
|
|
90
|
+
const mw = this.middleware();
|
|
91
|
+
mw[RhythmCliTag] = this;
|
|
92
|
+
return mw;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
//#endregion
|
|
96
|
+
export { RhythmCli };
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rhythmjs/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Command-line argument parsing, prompts, and command routing for the Rhythm middleware kernel.",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/rhythmjs/rhythm.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/rhythm-cli.d.ts",
|
|
19
|
+
"default": "./dist/rhythm-cli.js"
|
|
20
|
+
},
|
|
21
|
+
"./argv": {
|
|
22
|
+
"types": "./dist/argv.d.ts",
|
|
23
|
+
"default": "./dist/argv.js"
|
|
24
|
+
},
|
|
25
|
+
"./prompt": {
|
|
26
|
+
"types": "./dist/prompt.d.ts",
|
|
27
|
+
"default": "./dist/prompt.js"
|
|
28
|
+
},
|
|
29
|
+
"./adapters/bun": {
|
|
30
|
+
"types": "./dist/adapters/bun.d.ts",
|
|
31
|
+
"default": "./dist/adapters/bun.js"
|
|
32
|
+
},
|
|
33
|
+
"./adapters/deno": {
|
|
34
|
+
"types": "./dist/adapters/deno.d.ts",
|
|
35
|
+
"default": "./dist/adapters/deno.js"
|
|
36
|
+
},
|
|
37
|
+
"./adapters/node": {
|
|
38
|
+
"types": "./dist/adapters/node.d.ts",
|
|
39
|
+
"default": "./dist/adapters/node.js"
|
|
40
|
+
},
|
|
41
|
+
"./adapters/context": {
|
|
42
|
+
"types": "./dist/adapters/context.d.ts",
|
|
43
|
+
"default": "./dist/adapters/context.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@rhythmjs/rhythm": "0.0.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bun": "^1.4.2",
|
|
55
|
+
"@types/node": "^26.6.3",
|
|
56
|
+
"typescript": "^7.0.2",
|
|
57
|
+
"vite-plus": "^1.0.0"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=20.19.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "vp pack",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"test": "vp test"
|
|
66
|
+
}
|
|
67
|
+
}
|