@jackwener/opencli 0.7.11 → 0.8.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/src/engine.ts CHANGED
@@ -164,15 +164,72 @@ function registerYamlCli(filePath: string, defaultSite: string): void {
164
164
  }
165
165
  }
166
166
 
167
+ /**
168
+ * Validates and coerces arguments based on the command's Arg definitions.
169
+ */
170
+ function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: Record<string, any>): Record<string, any> {
171
+ const result: Record<string, any> = { ...kwargs };
172
+
173
+ for (const argDef of cmdArgs) {
174
+ const val = result[argDef.name];
175
+
176
+ // 1. Check required
177
+ if (argDef.required && (val === undefined || val === null || val === '')) {
178
+ throw new Error(`Argument "${argDef.name}" is required.\n${argDef.help ? `Hint: ${argDef.help}` : ''}`);
179
+ }
180
+
181
+ if (val !== undefined && val !== null) {
182
+ // 2. Type coercion
183
+ if (argDef.type === 'int' || argDef.type === 'number') {
184
+ const num = Number(val);
185
+ if (Number.isNaN(num)) {
186
+ throw new Error(`Argument "${argDef.name}" must be a valid number. Received: "${val}"`);
187
+ }
188
+ result[argDef.name] = num;
189
+ } else if (argDef.type === 'boolean' || argDef.type === 'bool') {
190
+ if (typeof val === 'string') {
191
+ const lower = val.toLowerCase();
192
+ if (lower === 'true' || lower === '1') result[argDef.name] = true;
193
+ else if (lower === 'false' || lower === '0') result[argDef.name] = false;
194
+ else throw new Error(`Argument "${argDef.name}" must be a boolean (true/false). Received: "${val}"`);
195
+ } else {
196
+ result[argDef.name] = Boolean(val);
197
+ }
198
+ }
199
+
200
+ // 3. Choices validation
201
+ const coercedVal = result[argDef.name];
202
+ if (argDef.choices && argDef.choices.length > 0) {
203
+ // Only stringent check for string/number types against choices array
204
+ if (!argDef.choices.map(String).includes(String(coercedVal))) {
205
+ throw new Error(`Argument "${argDef.name}" must be one of: ${argDef.choices.join(', ')}. Received: "${coercedVal}"`);
206
+ }
207
+ }
208
+ } else if (argDef.default !== undefined) {
209
+ // Set default if value is missing
210
+ result[argDef.name] = argDef.default;
211
+ }
212
+ }
213
+ return result;
214
+ }
215
+
167
216
  /**
168
217
  * Execute a CLI command. Handles lazy-loading of TS modules.
169
218
  */
170
219
  export async function executeCommand(
171
220
  cmd: CliCommand,
172
221
  page: IPage | null,
173
- kwargs: Record<string, any>,
222
+ rawKwargs: Record<string, any>,
174
223
  debug: boolean = false,
175
224
  ): Promise<any> {
225
+ let kwargs: Record<string, any>;
226
+ try {
227
+ kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
228
+ } catch (err: any) {
229
+ // Re-throw validation errors clearly
230
+ throw new Error(`[Argument Validation Error]\n${err.message}`);
231
+ }
232
+
176
233
  // Lazy-load TS module on first execution
177
234
  const internal = cmd as InternalCliCommand;
178
235
  if (internal._lazy && internal._modulePath) {
package/src/main.ts CHANGED
@@ -189,19 +189,21 @@ for (const [, cmd] of registry) {
189
189
  const actionOpts = actionArgs[positionalArgs.length] ?? {};
190
190
  const startTime = Date.now();
191
191
  const kwargs: Record<string, any> = {};
192
+
192
193
  // Collect positional args
193
194
  for (let i = 0; i < positionalArgs.length; i++) {
194
195
  const arg = positionalArgs[i];
195
196
  const v = actionArgs[i];
196
- if (v !== undefined) kwargs[arg.name] = coerce(v, arg.type ?? 'str');
197
- else if (arg.default != null) kwargs[arg.name] = arg.default;
197
+ if (v !== undefined) kwargs[arg.name] = v;
198
198
  }
199
+
199
200
  // Collect named options
200
201
  for (const arg of cmd.args) {
201
202
  if (arg.positional) continue;
202
- const v = actionOpts[arg.name]; if (v !== undefined) kwargs[arg.name] = coerce(v, arg.type ?? 'str');
203
- else if (arg.default != null) kwargs[arg.name] = arg.default;
203
+ const v = actionOpts[arg.name];
204
+ if (v !== undefined) kwargs[arg.name] = v;
204
205
  }
206
+
205
207
  try {
206
208
  if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
207
209
  let result: any;
@@ -226,11 +228,4 @@ for (const [, cmd] of registry) {
226
228
  });
227
229
  }
228
230
 
229
- function coerce(v: any, t: string): any {
230
- if (t === 'bool') return ['1', 'true', 'yes', 'on'].includes(String(v).toLowerCase());
231
- if (t === 'int') return parseInt(String(v), 10);
232
- if (t === 'float') return parseFloat(String(v));
233
- return String(v);
234
- }
235
-
236
231
  program.parse();
@@ -4,11 +4,7 @@
4
4
 
5
5
  import chalk from 'chalk';
6
6
  import type { IPage } from '../types.js';
7
- import { stepNavigate, stepClick, stepType, stepWait, stepPress, stepSnapshot, stepEvaluate } from './steps/browser.js';
8
- import { stepFetch } from './steps/fetch.js';
9
- import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
10
- import { stepIntercept } from './steps/intercept.js';
11
- import { stepTap } from './steps/tap.js';
7
+ import { getStep, type StepHandler } from './registry.js';
12
8
  import { log } from '../logger.js';
13
9
 
14
10
  export interface PipelineContext {
@@ -16,28 +12,6 @@ export interface PipelineContext {
16
12
  debug?: boolean;
17
13
  }
18
14
 
19
- /** Step handler: all steps conform to (page, params, data, args) => Promise<any> */
20
- type StepHandler = (page: IPage | null, params: any, data: any, args: Record<string, any>) => Promise<any>;
21
-
22
- /** Registry of all available step handlers */
23
- const STEP_HANDLERS: Record<string, StepHandler> = {
24
- navigate: stepNavigate,
25
- fetch: stepFetch,
26
- select: stepSelect,
27
- evaluate: stepEvaluate,
28
- snapshot: stepSnapshot,
29
- click: stepClick,
30
- type: stepType,
31
- wait: stepWait,
32
- press: stepPress,
33
- map: stepMap,
34
- filter: stepFilter,
35
- sort: stepSort,
36
- limit: stepLimit,
37
- intercept: stepIntercept,
38
- tap: stepTap,
39
- };
40
-
41
15
  export async function executePipeline(
42
16
  page: IPage | null,
43
17
  pipeline: any[],
@@ -54,7 +28,7 @@ export async function executePipeline(
54
28
  for (const [op, params] of Object.entries(step)) {
55
29
  if (debug) debugStepStart(i + 1, total, op, params);
56
30
 
57
- const handler = STEP_HANDLERS[op];
31
+ const handler = getStep(op);
58
32
  if (handler) {
59
33
  data = await handler(page, params, data, args);
60
34
  } else {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Dynamic registry for pipeline steps.
3
+ * Allows core and third-party plugins to register custom YAML operations.
4
+ */
5
+
6
+ import type { IPage } from '../types.js';
7
+
8
+ // Import core steps
9
+ import { stepNavigate, stepClick, stepType, stepWait, stepPress, stepSnapshot, stepEvaluate } from './steps/browser.js';
10
+ import { stepFetch } from './steps/fetch.js';
11
+ import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
12
+ import { stepIntercept } from './steps/intercept.js';
13
+ import { stepTap } from './steps/tap.js';
14
+
15
+ /**
16
+ * Step handler: all pipeline steps conform to this generic interface.
17
+ * TData is the type of the `data` state flowing into the step.
18
+ * TResult is the expected return type.
19
+ */
20
+ export type StepHandler<TData = any, TResult = any> = (
21
+ page: IPage | null,
22
+ params: any,
23
+ data: TData,
24
+ args: Record<string, any>
25
+ ) => Promise<TResult>;
26
+
27
+ const _stepRegistry = new Map<string, StepHandler>();
28
+
29
+ /**
30
+ * Get a registered step handler by name.
31
+ */
32
+ export function getStep(name: string): StepHandler | undefined {
33
+ return _stepRegistry.get(name);
34
+ }
35
+
36
+ /**
37
+ * Register a new custom step handler for the YAML pipeline.
38
+ */
39
+ export function registerStep(name: string, handler: StepHandler): void {
40
+ _stepRegistry.set(name, handler);
41
+ }
42
+
43
+ // -------------------------------------------------------------
44
+ // Auto-Register Core Steps
45
+ // -------------------------------------------------------------
46
+ registerStep('navigate', stepNavigate);
47
+ registerStep('fetch', stepFetch);
48
+ registerStep('select', stepSelect);
49
+ registerStep('evaluate', stepEvaluate);
50
+ registerStep('snapshot', stepSnapshot);
51
+ registerStep('click', stepClick);
52
+ registerStep('type', stepType);
53
+ registerStep('wait', stepWait);
54
+ registerStep('press', stepPress);
55
+ registerStep('map', stepMap);
56
+ registerStep('filter', stepFilter);
57
+ registerStep('sort', stepSort);
58
+ registerStep('limit', stepLimit);
59
+ registerStep('intercept', stepIntercept);
60
+ registerStep('tap', stepTap);