@lovrabet/cli-framework 0.1.1-beta.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.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # @lovrabet/cli-framework
2
+
3
+ [![English](https://img.shields.io/badge/-English-blue?style=flat-square)](README.md)
4
+ [![中文](https://img.shields.io/badge/-中文-red?style=flat-square)](README_zh-CN.md)
5
+ [![Bahasa Indonesia](https://img.shields.io/badge/-Bahasa%20Indonesia-green?style=flat-square)](README_id-ID.md)
6
+ [![日本語](https://img.shields.io/badge/-日本語-orange?style=flat-square)](README_ja-JP.md)
7
+ [![Türkçe](https://img.shields.io/badge/-Türkçe-purple?style=flat-square)](README_tr-TR.md)
8
+
9
+ A lightweight, type-safe CLI framework for building structured command-line tools with built-in flag parsing, output formatting, risk control, and dry-run support.
10
+
11
+ ## Features
12
+
13
+ - **Type-Safe Commands** — Define commands with TypeScript interfaces for flags, arguments, and results
14
+ - **Flexible Flag Parsing** — Supports string, boolean, number types with validation, enums, and regex patterns
15
+ - **Multiple Output Formats** — JSON, pretty-printed, and compress modes
16
+ - **Risk Control** — Built-in risk levels (read/write/high-risk-write) with configurable policies
17
+ - **Dry-Run Mode** — Preview commands without executing them
18
+ - **Context-Aware Runtime** — Unified runtime context with helper methods for flags and output
19
+ - **Help Generation** — Automatic help text generation from command definitions
20
+ - **Schema Export** — Export command schemas for tooling and documentation
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ bun add @lovrabet/cli-framework
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ `runCommandWithAdapter(def, env, adapter)` needs:
31
+ - `def`: declarative command definition
32
+ - `env`: raw CLI input/environment values
33
+ - `adapter`: CLI-specific bridge implementation (errors, output, prepare, confirmation, risk policy)
34
+
35
+ ```typescript
36
+ import {
37
+ createCliErrors,
38
+ formatOutput,
39
+ runCommandWithAdapter,
40
+ type CommandDefinition,
41
+ type RunnerAdapter,
42
+ type RunnerEnvBase,
43
+ } from "@lovrabet/cli-framework";
44
+
45
+ const myCommand: CommandDefinition = {
46
+ service: "app",
47
+ command: "list",
48
+ description: "List all applications",
49
+ risk: "read",
50
+ requiresAuth: true,
51
+ flags: [
52
+ { name: "format", type: "string", description: "Output format", default: "compress" },
53
+ { name: "page", type: "number", description: "Page number", default: 1 },
54
+ ],
55
+ async execute(ctx) {
56
+ const apps = await fetchApps({ page: ctx.num("page") });
57
+ return { ok: true, data: apps };
58
+ },
59
+ };
60
+
61
+ const env: RunnerEnvBase = {
62
+ rawFlags: { format: "compress", page: 1 },
63
+ isNonInteractive: true,
64
+ args: [],
65
+ };
66
+
67
+ const CliErrors = createCliErrors({ programName: "my-cli" });
68
+
69
+ const adapter: RunnerAdapter<RunnerEnvBase> = {
70
+ cliErrors: CliErrors,
71
+ formatOutput,
72
+ getCommandLabel: (def) => `${def.service} ${def.command}`,
73
+ prepare: async () => ({ extras: {} }),
74
+ confirmHighRisk: async () => {},
75
+ riskPolicy: {
76
+ createError: (message) => CliErrors.validation(message),
77
+ },
78
+ };
79
+
80
+ await runCommandWithAdapter(myCommand, env, adapter);
81
+ ```
82
+
83
+ ## Core Concepts
84
+
85
+ ### Command Definition
86
+
87
+ A command consists of:
88
+
89
+ | Field | Type | Description |
90
+ |-------|------|-------------|
91
+ | `service` | `string` | Service/namespace grouping |
92
+ | `command` | `string` | Command name |
93
+ | `description` | `string` | Help text description |
94
+ | `risk` | `Risk` | Risk level: `read`, `write`, `high-risk-write` |
95
+ | `flags` | `FlagDef[]` | Flag definitions |
96
+ | `args` | `ArgDef[]` | Positional argument definitions |
97
+ | `validate` | `(ctx) => Promise<void>` | Pre-execution validation |
98
+ | `dryRun` | `(ctx) => Promise<DryRunResult>` | Dry-run implementation |
99
+ | `execute` | `(ctx) => Promise<CommandResult>` | Main execution logic |
100
+
101
+ ### Flag Types
102
+
103
+ ```typescript
104
+ // String flag with enum
105
+ { name: "format", type: "string", enum: ["json", "pretty", "compress"], default: "compress" }
106
+
107
+ // Boolean flag
108
+ { name: "dry-run", type: "boolean", description: "Preview without executing" }
109
+
110
+ // Number flag with validation
111
+ { name: "page", type: "number", default: 1 }
112
+
113
+ // String with regex pattern
114
+ { name: "appcode", type: "string", pattern: { regex: /^[a-z0-9-]+$/, description: "lowercase alphanumeric with dashes" } }
115
+ ```
116
+
117
+ ### Output Formats
118
+
119
+ | Format | Description |
120
+ |--------|-------------|
121
+ | `json` | Raw JSON output |
122
+ | `pretty` | Formatted JSON with colors |
123
+ | `compress` | Minified single-line JSON (default) |
124
+
125
+ ### Risk Levels
126
+
127
+ | Level | Order | Description |
128
+ |-------|-------|-------------|
129
+ | `read` | 0 | Safe read operations |
130
+ | `write` | 1 | Data modification |
131
+ | `high-risk-write` | 2 | Destructive operations |
132
+
133
+ ## API Reference
134
+
135
+ ### Types
136
+
137
+ ```typescript
138
+ // Core types
139
+ export type { CommandDefinition, CommandResult, RuntimeContext, FlagDef, Risk }
140
+
141
+ // Runner types
142
+ export type { RunnerAdapter, RunnerEnvBase, RiskPolicy }
143
+
144
+ // Help types
145
+ export type { HelpGeneratorOptions, HelpServiceEntry }
146
+ ```
147
+
148
+ ### Functions
149
+
150
+ ```typescript
151
+ // Command execution
152
+ runCommandWithAdapter(def, env, adapter)
153
+
154
+ // Flag helpers
155
+ createFlagHelpers(cliErrors) → { parseFlags, validateFlags }
156
+
157
+ // Output formatting
158
+ formatOutput(result, options)
159
+
160
+ // Runtime context builders
161
+ buildRuntimeContext(options)
162
+ resolveOutputConfig(options)
163
+ assertRiskWithinLevel(options)
164
+ runDryRunIfNeeded(options)
165
+
166
+ // Help generation
167
+ createHelpGenerators(options)
168
+
169
+ // Schema export
170
+ buildSchemaPayload(commands, options)
171
+
172
+ // Utilities
173
+ extractList(data) → ListResponse
174
+ extractPaging(data) → Paging
175
+ resolveJqFilter(filter, data)
176
+ ```
177
+
178
+ ### Error Handling
179
+
180
+ ```typescript
181
+ import { CliError, createCliErrors } from "@lovrabet/cli-framework";
182
+
183
+ const CliErrors = createCliErrors({ programName: "my-cli" });
184
+
185
+ throw CliErrors.flagMissing("appcode", "App code is required");
186
+ throw CliErrors.validation("Invalid format value");
187
+ throw CliErrors.authRequired("Login first with: my-cli auth login");
188
+ ```
189
+
190
+ ## Example: Complete CLI Setup
191
+
192
+ ```typescript
193
+ import {
194
+ runCommandWithAdapter,
195
+ createFlagHelpers,
196
+ formatOutput,
197
+ CliError,
198
+ } from "@lovrabet/cli-framework";
199
+ import type { CommandDefinition, RunnerAdapter } from "@lovrabet/cli-framework";
200
+
201
+ const adapter: RunnerAdapter<any> = {
202
+ cliErrors: createCliErrors({ programName: "myapp" }),
203
+ formatOutput,
204
+ getCommandLabel: (def) => `${def.service} ${def.command}`,
205
+ prepare: async (def, env, flags) => ({
206
+ appCode: flags["appcode"] as string,
207
+ cookie: env.cookie,
208
+ apiDomain: "https://api.example.com",
209
+ apiDir: "/v1",
210
+ }),
211
+ confirmHighRisk: async ({ commandLabel }) => {
212
+ const confirmed = await confirm(`${commandLabel}?`);
213
+ if (!confirmed) throw new CliError("cancelled");
214
+ },
215
+ riskPolicy: {
216
+ createError: (msg) => new Error(msg),
217
+ },
218
+ };
219
+
220
+ // Define and run commands
221
+ const commands: CommandDefinition[] = [listCommand, createCommand, deleteCommand];
222
+
223
+ for (const def of commands) {
224
+ await runCommandWithAdapter(def, env, adapter);
225
+ }
226
+ ```
227
+
228
+ ## Development
229
+
230
+ ```bash
231
+ # Install dependencies
232
+ bun install
233
+
234
+ # Type check
235
+ bun run typecheck
236
+
237
+ # Run tests
238
+ bun test
239
+
240
+ # Build
241
+ bun run build
242
+ ```
243
+
244
+ ## License
245
+
246
+ MIT
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @fileoverview Error types, factory interfaces, and creator for CLI-specific errors.
3
+ * All CLI errors are expressed as {@link CliError} instances with a machine-readable
4
+ * `code`, numeric `exitCode`, human-readable `message`, and optional `hint`.
5
+ */
6
+ /**
7
+ * Unified CLI error class thrown by command handlers and caught by the top-level
8
+ * error handler in the runner/adapter.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * throw new CliError("flag_missing", 1, "Missing required flag: --appcode", "Use --appcode=<code>");
13
+ * ```
14
+ */
15
+ export declare class CliError extends Error {
16
+ /** Machine-readable error code (e.g. "auth_required", "flag_missing"). */
17
+ readonly code: string;
18
+ /**
19
+ * Numeric exit code to emit on process exit.
20
+ * - `0` = success or user-cancelled
21
+ * - `1` = validation / user error
22
+ * - `2` = network / API / system error
23
+ */
24
+ readonly exitCode: number;
25
+ /** Optional hint shown after the error message to guide the user. */
26
+ readonly hint?: string;
27
+ /**
28
+ * @param code - Machine-readable error identifier.
29
+ * @param exitCode - Process exit code.
30
+ * @param message - Human-readable error message.
31
+ * @param hint - Optional actionable hint displayed below the message.
32
+ */
33
+ constructor(code: string, exitCode: number, message: string, hint?: string);
34
+ }
35
+ /**
36
+ * Shape of the error factory returned by {@link createCliErrors}.
37
+ * Each method produces a typed {@link CliError} with a known `code` and `exitCode`.
38
+ */
39
+ export interface CliErrorsShape {
40
+ /**
41
+ * Creates an error when the command requires authentication but none is present.
42
+ * @param hint - Optional override of the default auth hint.
43
+ */
44
+ authRequired(hint?: string): CliError;
45
+ /**
46
+ * Creates an error when the configuration file cannot be found.
47
+ * @param hint - Optional override of the default config hint.
48
+ */
49
+ configMissing(hint?: string): CliError;
50
+ /** Creates an error when the current directory is not inside a project. */
51
+ notInProject(): CliError;
52
+ /**
53
+ * Creates an error for a failed API call.
54
+ * @param message - Raw API error message.
55
+ * @param hint - Optional hint for recovery.
56
+ */
57
+ apiError(message: string, hint?: string): CliError;
58
+ /**
59
+ * Creates an error for a network-level failure.
60
+ * @param message - Brief description of the network problem.
61
+ */
62
+ networkError(message: string): CliError;
63
+ /**
64
+ * Creates an error for an unrecognized command or subcommand.
65
+ * @param cmd - The command string that was not recognized.
66
+ */
67
+ unknownCommand(cmd: string): CliError;
68
+ /**
69
+ * Creates an error when a required flag is missing.
70
+ * @param flagName - Name of the missing flag (without leading `--`).
71
+ * @param hint - Optional hint describing how to provide the flag.
72
+ */
73
+ flagMissing(flagName: string, hint?: string): CliError;
74
+ /**
75
+ * Creates an error for general business-logic validation failures.
76
+ * @param message - Description of the validation failure.
77
+ * @param hint - Optional hint for correcting the input.
78
+ */
79
+ validation(message: string, hint?: string): CliError;
80
+ /**
81
+ * Creates an error signalling the user cancelled an interactive prompt (Ctrl+C).
82
+ * Exit code is `0`; the top-level handler treats this as a silent success.
83
+ * @param message - Optional override of the default cancellation message.
84
+ */
85
+ cancelled(message?: string): CliError;
86
+ }
87
+ /**
88
+ * Configuration options passed to {@link createCliErrors}.
89
+ * These supply the CLI binary name and the default hint messages used when
90
+ * callers do not provide explicit hints.
91
+ */
92
+ export interface CliErrorFactoryOptions {
93
+ /** CLI binary name used in auto-generated hint messages (e.g. "rabetbase"). */
94
+ cliBinName: string;
95
+ /** Default hint shown when `authRequired()` is called without an explicit hint. */
96
+ authRequiredHint: string;
97
+ /** Default hint shown when `configMissing()` is called without an explicit hint. */
98
+ configMissingHint: string;
99
+ /** Default hint shown when `notInProject()` is called without an explicit hint. */
100
+ notInProjectHint: string;
101
+ }
102
+ /**
103
+ * Creates a pre-configured {@link CliErrorsShape} instance.
104
+ * The returned factory uses `options.cliBinName` and the default hints from
105
+ * `options` for any method call that does not receive an explicit hint.
106
+ *
107
+ * @param options - Bin name and default hint messages.
108
+ * @returns A fully-populated error factory.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * const cliErrors = createCliErrors({
113
+ * cliBinName: "mycli",
114
+ * authRequiredHint: "Run `mycli auth login` first.",
115
+ * configMissingHint: "Run `mycli init` to create a project.",
116
+ * notInProjectHint: " cd into your project directory and try again.",
117
+ * });
118
+ * throw cliErrors.flagMissing("appcode");
119
+ * ```
120
+ */
121
+ export declare function createCliErrors(options: CliErrorFactoryOptions): CliErrorsShape;
package/lib/errors.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @fileoverview Error types, factory interfaces, and creator for CLI-specific errors.
3
+ * All CLI errors are expressed as {@link CliError} instances with a machine-readable
4
+ * `code`, numeric `exitCode`, human-readable `message`, and optional `hint`.
5
+ */
6
+ /**
7
+ * Unified CLI error class thrown by command handlers and caught by the top-level
8
+ * error handler in the runner/adapter.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * throw new CliError("flag_missing", 1, "Missing required flag: --appcode", "Use --appcode=<code>");
13
+ * ```
14
+ */
15
+ export class CliError extends Error {
16
+ /** Machine-readable error code (e.g. "auth_required", "flag_missing"). */
17
+ code;
18
+ /**
19
+ * Numeric exit code to emit on process exit.
20
+ * - `0` = success or user-cancelled
21
+ * - `1` = validation / user error
22
+ * - `2` = network / API / system error
23
+ */
24
+ exitCode;
25
+ /** Optional hint shown after the error message to guide the user. */
26
+ hint;
27
+ /**
28
+ * @param code - Machine-readable error identifier.
29
+ * @param exitCode - Process exit code.
30
+ * @param message - Human-readable error message.
31
+ * @param hint - Optional actionable hint displayed below the message.
32
+ */
33
+ constructor(code, exitCode, message, hint) {
34
+ super(message);
35
+ this.name = "CliError";
36
+ this.code = code;
37
+ this.exitCode = exitCode;
38
+ this.hint = hint;
39
+ }
40
+ }
41
+ /**
42
+ * Creates a pre-configured {@link CliErrorsShape} instance.
43
+ * The returned factory uses `options.cliBinName` and the default hints from
44
+ * `options` for any method call that does not receive an explicit hint.
45
+ *
46
+ * @param options - Bin name and default hint messages.
47
+ * @returns A fully-populated error factory.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const cliErrors = createCliErrors({
52
+ * cliBinName: "mycli",
53
+ * authRequiredHint: "Run `mycli auth login` first.",
54
+ * configMissingHint: "Run `mycli init` to create a project.",
55
+ * notInProjectHint: " cd into your project directory and try again.",
56
+ * });
57
+ * throw cliErrors.flagMissing("appcode");
58
+ * ```
59
+ */
60
+ export function createCliErrors(options) {
61
+ const { cliBinName, authRequiredHint, configMissingHint, notInProjectHint } = options;
62
+ return {
63
+ authRequired: (hint) => new CliError("auth_required", 1, "Authentication required", hint ?? authRequiredHint),
64
+ configMissing: (hint) => new CliError("config_missing", 1, "Configuration file not found", hint ?? configMissingHint),
65
+ notInProject: () => new CliError("not_in_project", 1, `Not in a ${cliBinName} project directory`, notInProjectHint),
66
+ apiError: (message, hint) => new CliError("api_error", 2, message, hint),
67
+ networkError: (message) => new CliError("network_error", 2, `Network error: ${message}`, "Check your internet connection and try again."),
68
+ unknownCommand: (cmd) => new CliError("unknown_command", 1, `Unknown command: ${cmd}`, `Run \`${cliBinName} --help\` to see available commands.`),
69
+ flagMissing: (flagName, hint) => new CliError("flag_missing", 1, `Missing required flag: --${flagName}`, hint),
70
+ validation: (message, hint) => new CliError("validation_error", 1, message, hint),
71
+ cancelled: (message) => new CliError("cancelled", 0, message ?? "Operation cancelled.", undefined),
72
+ };
73
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @fileoverview Builds the complete effective flag list for a command.
3
+ *
4
+ * Injects framework-managed built-in flags (`--dry-run`, `--format`, `--yes`)
5
+ * alongside the command's own flag definitions, so the runner only needs to
6
+ * pass a single merged list to {@link createFlagHelpers}.
7
+ */
8
+ import type { CommandDefinition, FlagDef } from "./types.js";
9
+ /**
10
+ * Options for {@link buildAllFlags}.
11
+ */
12
+ export interface BuildAllFlagsOptions {
13
+ /**
14
+ * The global default output format, used as the fallback for `--format`
15
+ * when the command does not define its own `defaultOutputFormat`.
16
+ */
17
+ defaultOutputFormat: "json" | "pretty" | "compress";
18
+ }
19
+ /**
20
+ * Builds the full effective flag list for a command by appending
21
+ * framework-injected flags when appropriate:
22
+ *
23
+ * - `--dry-run` (boolean) — added only when the command defines a `dryRun` hook.
24
+ * - `--format` (string) — added unless `hasFormat === false`; defaults to
25
+ * `def.defaultOutputFormat ?? options.defaultOutputFormat`.
26
+ * - `--yes` (boolean) — added only for `high-risk-write` commands.
27
+ *
28
+ * @param def - Command definition to extend.
29
+ * @param options - Global options supplying the default format.
30
+ * @returns A new array containing command flags plus any applicable built-in flags.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const allFlags = buildAllFlags(myCommandDef, { defaultOutputFormat: "compress" });
35
+ * const { parseFlags } = createFlagHelpers(cliErrors);
36
+ * const flags = parseFlags(allFlags, rawFlags);
37
+ * ```
38
+ */
39
+ export declare function buildAllFlags(def: CommandDefinition, options: BuildAllFlagsOptions): FlagDef[];
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @fileoverview Builds the complete effective flag list for a command.
3
+ *
4
+ * Injects framework-managed built-in flags (`--dry-run`, `--format`, `--yes`)
5
+ * alongside the command's own flag definitions, so the runner only needs to
6
+ * pass a single merged list to {@link createFlagHelpers}.
7
+ */
8
+ /**
9
+ * Builds the full effective flag list for a command by appending
10
+ * framework-injected flags when appropriate:
11
+ *
12
+ * - `--dry-run` (boolean) — added only when the command defines a `dryRun` hook.
13
+ * - `--format` (string) — added unless `hasFormat === false`; defaults to
14
+ * `def.defaultOutputFormat ?? options.defaultOutputFormat`.
15
+ * - `--yes` (boolean) — added only for `high-risk-write` commands.
16
+ *
17
+ * @param def - Command definition to extend.
18
+ * @param options - Global options supplying the default format.
19
+ * @returns A new array containing command flags plus any applicable built-in flags.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const allFlags = buildAllFlags(myCommandDef, { defaultOutputFormat: "compress" });
24
+ * const { parseFlags } = createFlagHelpers(cliErrors);
25
+ * const flags = parseFlags(allFlags, rawFlags);
26
+ * ```
27
+ */
28
+ export function buildAllFlags(def, options) {
29
+ const flags = [...def.flags];
30
+ if (def.dryRun) {
31
+ flags.push({
32
+ name: "dry-run",
33
+ type: "boolean",
34
+ description: "Preview the operation without executing",
35
+ });
36
+ }
37
+ if (def.hasFormat !== false) {
38
+ flags.push({
39
+ name: "format",
40
+ type: "string",
41
+ default: def.defaultOutputFormat ?? options.defaultOutputFormat,
42
+ enum: ["json", "pretty", "compress"],
43
+ description: "Output format",
44
+ });
45
+ }
46
+ if (def.risk === "high-risk-write") {
47
+ flags.push({
48
+ name: "yes",
49
+ type: "boolean",
50
+ description: "Skip confirmation prompt",
51
+ });
52
+ }
53
+ return flags;
54
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @fileoverview Flag parsing and validation helpers.
3
+ *
4
+ * Provides the {@link createFlagHelpers} factory which returns `parseFlags`
5
+ * and `validateFlags` — the two-phase pipeline used by the runner adapter.
6
+ *
7
+ * Phase 1 (`parseFlags`): coerce raw CLI inputs into typed values using
8
+ * {@link FlagDef} metadata.
9
+ *
10
+ * Phase 2 (`validateFlags`): enforce `required`, `enum`, and `pattern`
11
+ * constraints and throw typed {@link CliError} on violation.
12
+ */
13
+ import type { CliErrorsShape } from "../errors.js";
14
+ import type { FlagDef } from "./types.js";
15
+ /**
16
+ * Result of {@link parseFlags}: a flat map of flag names to their coerced
17
+ * typed values. Missing flags with defaults are populated; missing flags
18
+ * without defaults are `undefined`.
19
+ */
20
+ export type ParsedFlags = Record<string, string | boolean | number | undefined>;
21
+ /**
22
+ * Creates a paired set of flag helpers used by the framework runner.
23
+ *
24
+ * @param cliErrors - Subset of the error factory providing `flagMissing` and `validation`.
25
+ * @returns An object with `parseFlags` and `validateFlags` functions.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const { parseFlags, validateFlags } = createFlagHelpers(cliErrors);
30
+ * const flags = parseFlags(flagDefs, rawInput);
31
+ * validateFlags(flagDefs, flags, "api list");
32
+ * ```
33
+ */
34
+ export declare function createFlagHelpers(cliErrors: Pick<CliErrorsShape, "flagMissing" | "validation">): {
35
+ parseFlags: (defs: FlagDef[], rawFlags: Record<string, any>) => ParsedFlags;
36
+ validateFlags: (defs: FlagDef[], parsed: ParsedFlags, commandLabel: string) => void;
37
+ };