@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 +246 -0
- package/lib/errors.d.ts +121 -0
- package/lib/errors.js +73 -0
- package/lib/framework/build-all-flags.d.ts +39 -0
- package/lib/framework/build-all-flags.js +54 -0
- package/lib/framework/flags.d.ts +37 -0
- package/lib/framework/flags.js +152 -0
- package/lib/framework/help.d.ts +136 -0
- package/lib/framework/help.js +244 -0
- package/lib/framework/output.d.ts +45 -0
- package/lib/framework/output.js +354 -0
- package/lib/framework/response.d.ts +60 -0
- package/lib/framework/response.js +47 -0
- package/lib/framework/runner-shared.d.ts +244 -0
- package/lib/framework/runner-shared.js +240 -0
- package/lib/framework/runner.d.ts +246 -0
- package/lib/framework/runner.js +184 -0
- package/lib/framework/schema-export.d.ts +176 -0
- package/lib/framework/schema-export.js +148 -0
- package/lib/framework/types.d.ts +338 -0
- package/lib/framework/types.js +53 -0
- package/lib/index.d.ts +209 -0
- package/lib/index.js +52 -0
- package/lib/utils/apply-jq-filter.d.ts +33 -0
- package/lib/utils/apply-jq-filter.js +99 -0
- package/package.json +35 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared utilities used by the framework runner.
|
|
3
|
+
*
|
|
4
|
+
* Provides pure functions for:
|
|
5
|
+
* - Resolving the effective output format and jq filter from flags/config.
|
|
6
|
+
* - Building the runtime context object passed to command implementations.
|
|
7
|
+
* - Enforcing risk-level policy.
|
|
8
|
+
* - Running dry-run previews and confirmation prompts.
|
|
9
|
+
*
|
|
10
|
+
* These functions are called from both the runner adapter and help/schema
|
|
11
|
+
* generators, so they carry no side-effects beyond argument validation.
|
|
12
|
+
*/
|
|
13
|
+
import { normalizeLegacyOutputFormat, riskLevelOrder } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Extracts the `--jq` flag value from parsed flags.
|
|
16
|
+
*
|
|
17
|
+
* @param flags - Parsed flag map.
|
|
18
|
+
* @returns Trimmed jq expression, or `undefined` if absent / blank.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const jq = resolveJqFilter(flags); // ".[].name" | undefined
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function resolveJqFilter(flags) {
|
|
26
|
+
const v = flags.jq;
|
|
27
|
+
if (v === undefined || v === null)
|
|
28
|
+
return undefined;
|
|
29
|
+
const s = String(v).trim();
|
|
30
|
+
return s === "" ? undefined : s;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolves the effective output format from the cascade:
|
|
34
|
+
* CLI flag → global config default → command default → `"compress"`.
|
|
35
|
+
*
|
|
36
|
+
* @param options - Resolution options.
|
|
37
|
+
* @returns The effective output format.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveFormat({ flags, def, configDefault, }) {
|
|
40
|
+
if (def.hasFormat === false)
|
|
41
|
+
return "pretty";
|
|
42
|
+
const fromFlag = normalizeLegacyOutputFormat(flags.format);
|
|
43
|
+
if (fromFlag)
|
|
44
|
+
return fromFlag;
|
|
45
|
+
const fromConfig = normalizeLegacyOutputFormat(configDefault);
|
|
46
|
+
if (fromConfig)
|
|
47
|
+
return fromConfig;
|
|
48
|
+
const fromDef = normalizeLegacyOutputFormat(def.defaultOutputFormat);
|
|
49
|
+
if (fromDef)
|
|
50
|
+
return fromDef;
|
|
51
|
+
return "compress";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolves both the output format and jq filter, and validates that `--jq`
|
|
55
|
+
* is only used with `json` or `compress` formats.
|
|
56
|
+
*
|
|
57
|
+
* @param options - Resolution options.
|
|
58
|
+
* @returns Resolved format and jq filter.
|
|
59
|
+
* @throws Error with code `"validation_error"` if `--jq` is used with an incompatible format.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveOutputConfig(options) {
|
|
62
|
+
let format = resolveFormat(options);
|
|
63
|
+
const jqFilter = resolveJqFilter(options.flags);
|
|
64
|
+
if (jqFilter && format === "pretty") {
|
|
65
|
+
format = "json";
|
|
66
|
+
}
|
|
67
|
+
if (jqFilter && format !== "json" && format !== "compress") {
|
|
68
|
+
throw new Error(`--jq only applies with --format json or --format compress (current: ${format}).`);
|
|
69
|
+
}
|
|
70
|
+
return { format, jqFilter };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Asserts that the command's risk level does not exceed the configured ceiling.
|
|
74
|
+
*
|
|
75
|
+
* The comparison uses {@link riskLevelOrder}: `high-risk-write (2) > write (1) > read (0)`.
|
|
76
|
+
*
|
|
77
|
+
* @param options - Assertion options.
|
|
78
|
+
* @throws The error returned by `options.createError` when the ceiling is exceeded.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* assertRiskWithinLevel({
|
|
83
|
+
* commandLabel: "app delete",
|
|
84
|
+
* commandRisk: "high-risk-write",
|
|
85
|
+
* configuredRiskLevel: "write",
|
|
86
|
+
* nonInteractive: true,
|
|
87
|
+
* createError: (msg) => cliErrors.validation(msg),
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function assertRiskWithinLevel(options) {
|
|
92
|
+
if (!options.configuredRiskLevel)
|
|
93
|
+
return;
|
|
94
|
+
if (options.skipWhenDryRun && options.dryRun)
|
|
95
|
+
return;
|
|
96
|
+
if (riskLevelOrder(options.commandRisk) <= riskLevelOrder(options.configuredRiskLevel)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const detail = options.nonInteractive
|
|
100
|
+
? `Command \`${options.commandLabel}\` has risk level "${options.commandRisk}", which exceeds the configured riskLevel "${options.configuredRiskLevel}".`
|
|
101
|
+
: `Command \`${options.commandLabel}\` has risk level "${options.commandRisk}", which exceeds the configured riskLevel "${options.configuredRiskLevel}".\n` +
|
|
102
|
+
` Edit the config file manually and set riskLevel to "${options.commandRisk}". Visit https://qizhiyuntu.feishu.cn/docx/JTiOdxQlXo2dQLxXVu6ctutcnme for more information.`;
|
|
103
|
+
options.onViolation?.(detail);
|
|
104
|
+
throw options.createError(detail);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Constructs the {@link RuntimeContext} object passed to every command's
|
|
108
|
+
* `validate`, `dryRun`, and `execute` function.
|
|
109
|
+
*
|
|
110
|
+
* The context provides:
|
|
111
|
+
* - Typed accessors for flags (`str`, `bool`, `num`, `flag`).
|
|
112
|
+
* - Positional argument accessors (`arg`).
|
|
113
|
+
* - The `output()` method for rendering results through the formatter.
|
|
114
|
+
*
|
|
115
|
+
* @template Extras - Shape of the adapter-supplied extras object.
|
|
116
|
+
* @param opts - Construction options.
|
|
117
|
+
* @returns A fully populated runtime context object.
|
|
118
|
+
*/
|
|
119
|
+
export function buildRuntimeContext(opts) {
|
|
120
|
+
const { flags, def } = opts;
|
|
121
|
+
const argIndexByName = new Map((def.args ?? []).map((arg, index) => [arg.name, index]));
|
|
122
|
+
return {
|
|
123
|
+
rawFlags: flags,
|
|
124
|
+
format: opts.format,
|
|
125
|
+
nonInteractive: opts.nonInteractive,
|
|
126
|
+
args: opts.args,
|
|
127
|
+
...(opts.extras ?? {}),
|
|
128
|
+
arg(nameOrIndex, defaultVal) {
|
|
129
|
+
const index = typeof nameOrIndex === "number"
|
|
130
|
+
? nameOrIndex
|
|
131
|
+
: argIndexByName.get(nameOrIndex) ?? -1;
|
|
132
|
+
const value = index >= 0 ? opts.args[index] : undefined;
|
|
133
|
+
return value ?? defaultVal ?? "";
|
|
134
|
+
},
|
|
135
|
+
str(name) {
|
|
136
|
+
return String(flags[name] ?? "");
|
|
137
|
+
},
|
|
138
|
+
bool(name) {
|
|
139
|
+
if (flags[name] === true)
|
|
140
|
+
return true;
|
|
141
|
+
return opts.defaults?.booleans?.[name] === true;
|
|
142
|
+
},
|
|
143
|
+
num(name, defaultVal) {
|
|
144
|
+
const v = flags[name];
|
|
145
|
+
if (typeof v === "number")
|
|
146
|
+
return v;
|
|
147
|
+
const configuredDefault = opts.defaults?.numbers?.[name];
|
|
148
|
+
if (configuredDefault != null)
|
|
149
|
+
return configuredDefault;
|
|
150
|
+
return defaultVal ?? 0;
|
|
151
|
+
},
|
|
152
|
+
flag(name) {
|
|
153
|
+
return flags[name];
|
|
154
|
+
},
|
|
155
|
+
output(result) {
|
|
156
|
+
opts.formatOutput(result, {
|
|
157
|
+
command: opts.commandLabel,
|
|
158
|
+
risk: def.risk,
|
|
159
|
+
format: opts.format,
|
|
160
|
+
jqFilter: opts.jqFilter,
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Checks whether `--dry-run` was supplied and, if so, calls the command's
|
|
167
|
+
* `dryRun` hook and outputs the preview.
|
|
168
|
+
*
|
|
169
|
+
* @param options - Dry-run options.
|
|
170
|
+
* @returns `true` if dry-run was handled (caller should return early);
|
|
171
|
+
* `false` if `--dry-run` was not present.
|
|
172
|
+
* @throws The error from `createUnsupportedError` when `--dry-run` is used
|
|
173
|
+
* on a command without a `dryRun` hook.
|
|
174
|
+
*/
|
|
175
|
+
export async function runDryRunIfNeeded(options) {
|
|
176
|
+
if (!options.flags["dry-run"])
|
|
177
|
+
return false;
|
|
178
|
+
if (!options.def.dryRun) {
|
|
179
|
+
throw options.createUnsupportedError(`--dry-run is not supported for \`${options.commandLabel}\`.`);
|
|
180
|
+
}
|
|
181
|
+
let preview;
|
|
182
|
+
try {
|
|
183
|
+
preview = await options.def.dryRun(options.ctx);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (options.mapError) {
|
|
187
|
+
throw options.mapError(error);
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
options.formatOutput({ ok: true, data: preview }, {
|
|
192
|
+
command: options.commandLabel,
|
|
193
|
+
risk: options.def.risk,
|
|
194
|
+
format: options.format,
|
|
195
|
+
dryRun: true,
|
|
196
|
+
jqFilter: options.jqFilter,
|
|
197
|
+
});
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Displays an interactive confirmation prompt on stderr and resolves
|
|
202
|
+
* when the user types `y` or `yes` (case-insensitive).
|
|
203
|
+
*
|
|
204
|
+
* - Any other answer throws a `"cancelled"` error.
|
|
205
|
+
* - Ctrl+C (SIGINT) also throws a `"cancelled"` error without a stack trace.
|
|
206
|
+
*
|
|
207
|
+
* @param options - Prompt options.
|
|
208
|
+
* @returns Resolves normally on confirmation.
|
|
209
|
+
* @throws The error from `createCancelledError` on decline or Ctrl+C.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* await requireConfirmationPrompt({
|
|
214
|
+
* lines: ["This will permanently delete the dataset.", "Are you sure? (y/N)"],
|
|
215
|
+
* createCancelledError: (msg) => cliErrors.cancelled(msg),
|
|
216
|
+
* });
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export async function requireConfirmationPrompt(options) {
|
|
220
|
+
const readline = await import("node:readline");
|
|
221
|
+
const rl = readline.createInterface({
|
|
222
|
+
input: process.stdin,
|
|
223
|
+
output: process.stderr,
|
|
224
|
+
});
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
rl.on("SIGINT", () => {
|
|
227
|
+
rl.close();
|
|
228
|
+
reject(options.createCancelledError("Operation cancelled by user."));
|
|
229
|
+
});
|
|
230
|
+
rl.question(options.lines.join("\n"), (answer) => {
|
|
231
|
+
rl.close();
|
|
232
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
233
|
+
resolve();
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
reject(options.createCancelledError("Operation cancelled by user."));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Command execution engine — the core runner.
|
|
3
|
+
*
|
|
4
|
+
* {@link runCommandWithAdapter} is the central orchestration function that
|
|
5
|
+
* drives every declarative command through the following pipeline:
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* parseFlags → validateFlags → validateArgs → assertRiskWithinLevel
|
|
9
|
+
* → prepare (adapter) → resolveOutputConfig → buildRuntimeContext
|
|
10
|
+
* → validate (command hook) → runDryRunIfNeeded → confirmHighRisk (adapter)
|
|
11
|
+
* → execute (command) → formatOutput (adapter)
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The adapter encapsulates CLI-specific concerns (auth, config, confirmation UI)
|
|
15
|
+
* so the runner itself remains framework-only and testable.
|
|
16
|
+
*/
|
|
17
|
+
import type { CliErrorsShape } from "../errors.js";
|
|
18
|
+
import { type ParsedFlags } from "./flags.js";
|
|
19
|
+
import type { CommandDefinition, CommandResult, FlagDef, OutputFormat, Risk, RuntimeContext } from "./types.js";
|
|
20
|
+
/**
|
|
21
|
+
* Raw environment values supplied by the CLI entry point before any
|
|
22
|
+
* processing occurs. These come directly from the meow/yargs/commander
|
|
23
|
+
* parsed input and the loaded config file.
|
|
24
|
+
*/
|
|
25
|
+
export interface RunnerEnvBase {
|
|
26
|
+
/** Raw parsed flags (string-keyed, untyped). */
|
|
27
|
+
rawFlags: Record<string, any>;
|
|
28
|
+
/**
|
|
29
|
+
* `true` when stdin is not a TTY, or when a confirmation flag
|
|
30
|
+
* (`--yes` / `-y`) was explicitly supplied.
|
|
31
|
+
*/
|
|
32
|
+
isNonInteractive: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Default output format sourced from the config file.
|
|
35
|
+
* Falls through when neither the CLI flag nor command default is set.
|
|
36
|
+
*/
|
|
37
|
+
defaultFormat?: OutputFormat;
|
|
38
|
+
/**
|
|
39
|
+
* User-configured risk ceiling from the config file.
|
|
40
|
+
* Commands whose risk exceeds this are blocked in non-interactive mode.
|
|
41
|
+
*/
|
|
42
|
+
riskLevel?: Risk;
|
|
43
|
+
/**
|
|
44
|
+
* Positional arguments after the subcommand name.
|
|
45
|
+
*/
|
|
46
|
+
args?: string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Values produced by the adapter's `prepare` hook and merged into the
|
|
50
|
+
* runtime context. Allows the adapter to inject dependencies
|
|
51
|
+
* (e.g. `appCode`, `session`, `config`) without the runner knowing
|
|
52
|
+
* about them.
|
|
53
|
+
*
|
|
54
|
+
* @template CtxExtras - Shape of the extras object merged into the context.
|
|
55
|
+
*/
|
|
56
|
+
export interface PreparedRuntimeValues<CtxExtras extends object = {}> {
|
|
57
|
+
/**
|
|
58
|
+
* Static defaults for boolean and number flags that should take effect
|
|
59
|
+
* even when the flag was not explicitly provided.
|
|
60
|
+
*/
|
|
61
|
+
defaults?: {
|
|
62
|
+
booleans?: Record<string, boolean | undefined>;
|
|
63
|
+
numbers?: Record<string, number | undefined>;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Additional context properties merged into {@link RuntimeContext}.
|
|
67
|
+
*/
|
|
68
|
+
extras?: CtxExtras;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Risk policy that governs how the runner handles the configured risk ceiling.
|
|
72
|
+
*
|
|
73
|
+
* @template Env - Concrete runner environment type.
|
|
74
|
+
*/
|
|
75
|
+
export interface RiskPolicy<Env extends RunnerEnvBase> {
|
|
76
|
+
/**
|
|
77
|
+
* When `true`, the risk ceiling check is skipped during dry-run.
|
|
78
|
+
* @default false
|
|
79
|
+
*/
|
|
80
|
+
skipWhenDryRun?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Side-effect callback invoked with the violation message before throwing.
|
|
83
|
+
* Use for logging, telemetry, or user-facing warnings.
|
|
84
|
+
*/
|
|
85
|
+
onViolation?: (message: string, params: {
|
|
86
|
+
def: CommandDefinition;
|
|
87
|
+
env: Env;
|
|
88
|
+
flags: ParsedFlags;
|
|
89
|
+
}) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Factory that creates the error to throw when a risk violation occurs.
|
|
92
|
+
* Typically produces a {@link CliError} with code `"validation_error"`.
|
|
93
|
+
*/
|
|
94
|
+
createError: (message: string, params: {
|
|
95
|
+
def: CommandDefinition;
|
|
96
|
+
env: Env;
|
|
97
|
+
flags: ParsedFlags;
|
|
98
|
+
}) => unknown;
|
|
99
|
+
}
|
|
100
|
+
/** Options for the adapter's `confirmHighRisk` method. */
|
|
101
|
+
export interface ConfirmHighRiskOptions<Env extends RunnerEnvBase> {
|
|
102
|
+
/** Command definition being executed. */
|
|
103
|
+
def: CommandDefinition;
|
|
104
|
+
/** Runner environment. */
|
|
105
|
+
env: Env;
|
|
106
|
+
/** Parsed flags. */
|
|
107
|
+
flags: ParsedFlags;
|
|
108
|
+
/** Human-readable command label. */
|
|
109
|
+
commandLabel: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Context object passed to every hook on the runner adapter.
|
|
113
|
+
* Contains the command definition, environment, parsed flags,
|
|
114
|
+
* and the already-constructed runtime context.
|
|
115
|
+
*
|
|
116
|
+
* @template Env - Concrete runner environment type.
|
|
117
|
+
* @template CtxExtras - Shape of the adapter extras.
|
|
118
|
+
*/
|
|
119
|
+
export interface ExecuteHookOptions<Env extends RunnerEnvBase, CtxExtras extends object = {}> {
|
|
120
|
+
/** Command definition. */
|
|
121
|
+
def: CommandDefinition<any>;
|
|
122
|
+
/** Runner environment. */
|
|
123
|
+
env: Env;
|
|
124
|
+
/** Parsed flags. */
|
|
125
|
+
flags: ParsedFlags;
|
|
126
|
+
/** Fully constructed runtime context. */
|
|
127
|
+
ctx: RuntimeContext<CtxExtras>;
|
|
128
|
+
/** Human-readable command label. */
|
|
129
|
+
commandLabel: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Extended hook options passed to the `finalize` hook, which additionally
|
|
133
|
+
* receives the command result and any thrown error.
|
|
134
|
+
*
|
|
135
|
+
* @template Env - Concrete runner environment type.
|
|
136
|
+
* @template CtxExtras - Shape of the adapter extras.
|
|
137
|
+
*/
|
|
138
|
+
export interface FinalizeHookOptions<Env extends RunnerEnvBase, CtxExtras extends object = {}> extends ExecuteHookOptions<Env, CtxExtras> {
|
|
139
|
+
/** Command result if `execute` completed successfully. */
|
|
140
|
+
result?: CommandResult;
|
|
141
|
+
/** Error thrown by `execute`, or `undefined` on success. */
|
|
142
|
+
error?: unknown;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Adapter interface that bridges the framework runner with a concrete CLI.
|
|
146
|
+
*
|
|
147
|
+
* Implementations (one per CLI binary) provide:
|
|
148
|
+
* - Error factory with CLI-specific error codes.
|
|
149
|
+
* - Global/pipeline flag definitions.
|
|
150
|
+
* - `prepare` hook for dependency injection.
|
|
151
|
+
* - `confirmHighRisk` for interactive prompts.
|
|
152
|
+
* - `formatOutput` for rendering results.
|
|
153
|
+
*
|
|
154
|
+
* @template Env - Concrete environment type supplied by the CLI entry point.
|
|
155
|
+
* @template CtxExtras - Shape of the extras object injected by `prepare`.
|
|
156
|
+
*/
|
|
157
|
+
export interface RunnerAdapter<Env extends RunnerEnvBase, CtxExtras extends object = {}> {
|
|
158
|
+
/** Error factory for runner-issued errors. */
|
|
159
|
+
cliErrors: Pick<CliErrorsShape, "flagMissing" | "validation" | "cancelled">;
|
|
160
|
+
/**
|
|
161
|
+
* Global flag definitions prepended to every command's flag list.
|
|
162
|
+
* Examples: `--env`, `--appcode`, `--global`.
|
|
163
|
+
*/
|
|
164
|
+
pipelineFlags?: FlagDef[];
|
|
165
|
+
/**
|
|
166
|
+
* Renders a {@link CommandResult} to stdout/stderr.
|
|
167
|
+
* Receives the resolved format and jq filter so it does not need to
|
|
168
|
+
* resolve them again.
|
|
169
|
+
*/
|
|
170
|
+
formatOutput: <T>(result: CommandResult<T>, options: {
|
|
171
|
+
command: string;
|
|
172
|
+
risk: CommandDefinition["risk"];
|
|
173
|
+
format: OutputFormat;
|
|
174
|
+
dryRun?: boolean;
|
|
175
|
+
jqFilter?: string;
|
|
176
|
+
}) => void;
|
|
177
|
+
/**
|
|
178
|
+
* Produces the full command label string for error messages and envelopes.
|
|
179
|
+
* Example: `"dataset list"` for `rabetbase dataset list`.
|
|
180
|
+
*/
|
|
181
|
+
getCommandLabel: (def: CommandDefinition, env: Env) => string;
|
|
182
|
+
/**
|
|
183
|
+
* Prepares the runtime extras and defaults by inspecting the environment
|
|
184
|
+
* and parsed flags. Called after flag validation but before context building.
|
|
185
|
+
*
|
|
186
|
+
* Typical responsibilities:
|
|
187
|
+
* - Resolve and validate `appCode`.
|
|
188
|
+
* - Load or refresh session cookie.
|
|
189
|
+
* - Merge config file with flags.
|
|
190
|
+
*/
|
|
191
|
+
prepare: (def: CommandDefinition<any>, env: Env, flags: ParsedFlags, commandLabel: string) => Promise<PreparedRuntimeValues<CtxExtras>>;
|
|
192
|
+
/**
|
|
193
|
+
* Prompts the user for confirmation when executing a `high-risk-write`
|
|
194
|
+
* command without `--yes`. The adapter decides how to prompt
|
|
195
|
+
* (readline, inquirer, etc.) and throws a `cancelled` error on decline.
|
|
196
|
+
*/
|
|
197
|
+
confirmHighRisk: (options: ConfirmHighRiskOptions<Env>) => Promise<void>;
|
|
198
|
+
/** Risk policy governing how the runner handles risk ceiling violations. */
|
|
199
|
+
riskPolicy: RiskPolicy<Env>;
|
|
200
|
+
/**
|
|
201
|
+
* Optional error mapper applied to errors thrown by the `dryRun` hook.
|
|
202
|
+
* Allows the adapter to normalize framework errors to its own types.
|
|
203
|
+
*/
|
|
204
|
+
mapDryRunError?: (error: unknown, options: ExecuteHookOptions<Env, CtxExtras>) => unknown;
|
|
205
|
+
/**
|
|
206
|
+
* Optional error mapper applied to errors thrown by `execute`.
|
|
207
|
+
* Allows the adapter to catch and re-throw with additional context.
|
|
208
|
+
*/
|
|
209
|
+
mapExecuteError?: (error: unknown, options: ExecuteHookOptions<Env, CtxExtras>) => unknown;
|
|
210
|
+
/**
|
|
211
|
+
* Optional cleanup hook called in a `finally` block after execution
|
|
212
|
+
* (whether `execute` succeeded or threw). Suitable for:
|
|
213
|
+
* - Revoking / refreshing session cookies.
|
|
214
|
+
* - Flushing telemetry spans.
|
|
215
|
+
* - Closing database connections.
|
|
216
|
+
*/
|
|
217
|
+
finalize?: (options: FinalizeHookOptions<Env, CtxExtras>) => Promise<void>;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Orchestrates the full command lifecycle using an adapter for CLI-specific concerns.
|
|
221
|
+
*
|
|
222
|
+
* Pipeline order:
|
|
223
|
+
* 1. Merge pipeline flags with command flags via {@link mergeFlagDefs}.
|
|
224
|
+
* 2. {@link parseFlags} and {@link validateFlags}.
|
|
225
|
+
* 3. {@link validateArgs} for required positional arguments.
|
|
226
|
+
* 4. {@link assertRiskWithinLevel} using the adapter's risk policy.
|
|
227
|
+
* 5. `adapter.prepare(...)` to resolve dependencies.
|
|
228
|
+
* 6. {@link resolveOutputConfig} for format and jq.
|
|
229
|
+
* 7. {@link buildRuntimeContext} to construct the context object.
|
|
230
|
+
* 8. `def.validate(ctx)` — business-level validation.
|
|
231
|
+
* 9. {@link runDryRunIfNeeded} — dry-run preview if `--dry-run`.
|
|
232
|
+
* 10. `adapter.confirmHighRisk(...)` — high-risk-write confirmation if needed.
|
|
233
|
+
* 11. `def.execute(ctx)` — command implementation.
|
|
234
|
+
* 12. `adapter.formatOutput(result, ...)` — render to stdout.
|
|
235
|
+
*
|
|
236
|
+
* On error, the thrown error (after optional mapping) propagates to the
|
|
237
|
+
* caller's `handleErrorAsync` / try-catch wrapper.
|
|
238
|
+
*
|
|
239
|
+
* @template Env - Concrete environment type.
|
|
240
|
+
* @template CtxExtras - Shape of the adapter extras.
|
|
241
|
+
* @param def - Command definition to execute.
|
|
242
|
+
* @param env - Runner environment from the CLI entry point.
|
|
243
|
+
* @param adapter - Adapter bridging to the concrete CLI implementation.
|
|
244
|
+
* @returns Resolves on success; throws on any failure.
|
|
245
|
+
*/
|
|
246
|
+
export declare function runCommandWithAdapter<Env extends RunnerEnvBase, CtxExtras extends object = {}>(def: CommandDefinition<any>, env: Env, adapter: RunnerAdapter<Env, CtxExtras>): Promise<void>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Command execution engine — the core runner.
|
|
3
|
+
*
|
|
4
|
+
* {@link runCommandWithAdapter} is the central orchestration function that
|
|
5
|
+
* drives every declarative command through the following pipeline:
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* parseFlags → validateFlags → validateArgs → assertRiskWithinLevel
|
|
9
|
+
* → prepare (adapter) → resolveOutputConfig → buildRuntimeContext
|
|
10
|
+
* → validate (command hook) → runDryRunIfNeeded → confirmHighRisk (adapter)
|
|
11
|
+
* → execute (command) → formatOutput (adapter)
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The adapter encapsulates CLI-specific concerns (auth, config, confirmation UI)
|
|
15
|
+
* so the runner itself remains framework-only and testable.
|
|
16
|
+
*/
|
|
17
|
+
import { createFlagHelpers } from "./flags.js";
|
|
18
|
+
import { assertRiskWithinLevel, buildRuntimeContext, resolveOutputConfig, runDryRunIfNeeded, } from "./runner-shared.js";
|
|
19
|
+
/**
|
|
20
|
+
* Orchestrates the full command lifecycle using an adapter for CLI-specific concerns.
|
|
21
|
+
*
|
|
22
|
+
* Pipeline order:
|
|
23
|
+
* 1. Merge pipeline flags with command flags via {@link mergeFlagDefs}.
|
|
24
|
+
* 2. {@link parseFlags} and {@link validateFlags}.
|
|
25
|
+
* 3. {@link validateArgs} for required positional arguments.
|
|
26
|
+
* 4. {@link assertRiskWithinLevel} using the adapter's risk policy.
|
|
27
|
+
* 5. `adapter.prepare(...)` to resolve dependencies.
|
|
28
|
+
* 6. {@link resolveOutputConfig} for format and jq.
|
|
29
|
+
* 7. {@link buildRuntimeContext} to construct the context object.
|
|
30
|
+
* 8. `def.validate(ctx)` — business-level validation.
|
|
31
|
+
* 9. {@link runDryRunIfNeeded} — dry-run preview if `--dry-run`.
|
|
32
|
+
* 10. `adapter.confirmHighRisk(...)` — high-risk-write confirmation if needed.
|
|
33
|
+
* 11. `def.execute(ctx)` — command implementation.
|
|
34
|
+
* 12. `adapter.formatOutput(result, ...)` — render to stdout.
|
|
35
|
+
*
|
|
36
|
+
* On error, the thrown error (after optional mapping) propagates to the
|
|
37
|
+
* caller's `handleErrorAsync` / try-catch wrapper.
|
|
38
|
+
*
|
|
39
|
+
* @template Env - Concrete environment type.
|
|
40
|
+
* @template CtxExtras - Shape of the adapter extras.
|
|
41
|
+
* @param def - Command definition to execute.
|
|
42
|
+
* @param env - Runner environment from the CLI entry point.
|
|
43
|
+
* @param adapter - Adapter bridging to the concrete CLI implementation.
|
|
44
|
+
* @returns Resolves on success; throws on any failure.
|
|
45
|
+
*/
|
|
46
|
+
export async function runCommandWithAdapter(def, env, adapter) {
|
|
47
|
+
const commandLabel = adapter.getCommandLabel(def, env);
|
|
48
|
+
const { parseFlags, validateFlags } = createFlagHelpers(adapter.cliErrors);
|
|
49
|
+
const allFlagDefs = mergeFlagDefs(adapter.pipelineFlags ?? [], def.flags);
|
|
50
|
+
const flags = parseFlags(allFlagDefs, env.rawFlags);
|
|
51
|
+
validateFlags(allFlagDefs, flags, commandLabel);
|
|
52
|
+
validateArgs(def, env.args ?? [], adapter.cliErrors, commandLabel);
|
|
53
|
+
assertRiskWithinLevel({
|
|
54
|
+
commandLabel,
|
|
55
|
+
commandRisk: def.risk,
|
|
56
|
+
configuredRiskLevel: env.riskLevel,
|
|
57
|
+
nonInteractive: env.isNonInteractive,
|
|
58
|
+
dryRun: flags["dry-run"] === true,
|
|
59
|
+
skipWhenDryRun: adapter.riskPolicy.skipWhenDryRun,
|
|
60
|
+
onViolation: (message) => adapter.riskPolicy.onViolation?.(message, { def, env, flags }),
|
|
61
|
+
createError: (message) => adapter.riskPolicy.createError(message, { def, env, flags }),
|
|
62
|
+
});
|
|
63
|
+
const prepared = await adapter.prepare(def, env, flags, commandLabel);
|
|
64
|
+
let format;
|
|
65
|
+
let jqFilter;
|
|
66
|
+
try {
|
|
67
|
+
({ format, jqFilter } = resolveOutputConfig({
|
|
68
|
+
flags,
|
|
69
|
+
def,
|
|
70
|
+
configDefault: env.defaultFormat,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw adapter.cliErrors.validation(error instanceof Error ? error.message : String(error));
|
|
75
|
+
}
|
|
76
|
+
const ctx = buildRuntimeContext({
|
|
77
|
+
commandLabel,
|
|
78
|
+
format,
|
|
79
|
+
jqFilter,
|
|
80
|
+
flags,
|
|
81
|
+
def,
|
|
82
|
+
nonInteractive: env.isNonInteractive,
|
|
83
|
+
args: env.args ?? [],
|
|
84
|
+
defaults: prepared.defaults,
|
|
85
|
+
extras: prepared.extras,
|
|
86
|
+
formatOutput: adapter.formatOutput,
|
|
87
|
+
});
|
|
88
|
+
if (def.validate) {
|
|
89
|
+
await def.validate(ctx);
|
|
90
|
+
}
|
|
91
|
+
const executeOptions = {
|
|
92
|
+
def,
|
|
93
|
+
env,
|
|
94
|
+
flags,
|
|
95
|
+
ctx,
|
|
96
|
+
commandLabel,
|
|
97
|
+
};
|
|
98
|
+
if (await runDryRunIfNeeded({
|
|
99
|
+
flags,
|
|
100
|
+
def,
|
|
101
|
+
ctx,
|
|
102
|
+
commandLabel,
|
|
103
|
+
format,
|
|
104
|
+
jqFilter,
|
|
105
|
+
formatOutput: adapter.formatOutput,
|
|
106
|
+
createUnsupportedError: (message) => adapter.cliErrors.validation(message),
|
|
107
|
+
mapError: adapter.mapDryRunError
|
|
108
|
+
? (error) => adapter.mapDryRunError(error, executeOptions)
|
|
109
|
+
: undefined,
|
|
110
|
+
})) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (def.risk === "high-risk-write" && !flags.yes) {
|
|
114
|
+
await adapter.confirmHighRisk({
|
|
115
|
+
def,
|
|
116
|
+
env,
|
|
117
|
+
flags,
|
|
118
|
+
commandLabel,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
let result;
|
|
122
|
+
let thrown;
|
|
123
|
+
try {
|
|
124
|
+
result = await def.execute(ctx);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
thrown = adapter.mapExecuteError
|
|
128
|
+
? adapter.mapExecuteError(error, executeOptions)
|
|
129
|
+
: error;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
if (adapter.finalize) {
|
|
133
|
+
await adapter.finalize({
|
|
134
|
+
...executeOptions,
|
|
135
|
+
result,
|
|
136
|
+
error: thrown,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (thrown) {
|
|
141
|
+
throw thrown;
|
|
142
|
+
}
|
|
143
|
+
adapter.formatOutput(result, {
|
|
144
|
+
command: commandLabel,
|
|
145
|
+
risk: def.risk,
|
|
146
|
+
format,
|
|
147
|
+
jqFilter,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Merges pipeline (global) flags with command-specific flags.
|
|
152
|
+
* Command flags take precedence over pipeline flags of the same name.
|
|
153
|
+
*
|
|
154
|
+
* @param base - Pipeline/global flag definitions.
|
|
155
|
+
* @param command - Command-specific flag definitions.
|
|
156
|
+
* @returns Merged flag list (command overrides pipeline).
|
|
157
|
+
*/
|
|
158
|
+
function mergeFlagDefs(base, command) {
|
|
159
|
+
const merged = new Map();
|
|
160
|
+
for (const def of [...base, ...command]) {
|
|
161
|
+
merged.set(def.name, def);
|
|
162
|
+
}
|
|
163
|
+
return [...merged.values()];
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Validates that all required positional arguments are present.
|
|
167
|
+
*
|
|
168
|
+
* @param def - Command definition whose args are checked.
|
|
169
|
+
* @param args - Actual positional arguments from the CLI.
|
|
170
|
+
* @param cliErrors - Error factory.
|
|
171
|
+
* @param commandLabel - Human-readable command label for error messages.
|
|
172
|
+
* @throws {@link CliError} with code `"validation_error"` for the first missing required arg.
|
|
173
|
+
*/
|
|
174
|
+
function validateArgs(def, args, cliErrors, commandLabel) {
|
|
175
|
+
for (const [index, argDef] of (def.args ?? []).entries()) {
|
|
176
|
+
if (argDef.required === false) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const value = args[index];
|
|
180
|
+
if (value === undefined || value === "") {
|
|
181
|
+
throw cliErrors.validation(`Validation error: Missing required argument: <${argDef.name}> for \`${commandLabel}\`.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|