Skip to content

Commit bd274ce

Browse files
authored
refactor: type discovery core (#219)
1 parent 28c393e commit bd274ce

File tree

4 files changed

+163
-68
lines changed

4 files changed

+163
-68
lines changed

src/build-manifest.ts

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1818
const CLIS_DIR = path.resolve(__dirname, 'clis');
1919
const OUTPUT = path.resolve(__dirname, '..', 'dist', 'cli-manifest.json');
2020

21-
interface ManifestEntry {
21+
export interface ManifestEntry {
2222
site: string;
2323
name: string;
2424
description: string;
@@ -28,14 +28,14 @@ interface ManifestEntry {
2828
args: Array<{
2929
name: string;
3030
type?: string;
31-
default?: any;
31+
default?: unknown;
3232
required?: boolean;
3333
positional?: boolean;
3434
help?: string;
3535
choices?: string[];
3636
}>;
3737
columns?: string[];
38-
pipeline?: any[];
38+
pipeline?: Record<string, unknown>[];
3939
timeout?: number;
4040
/** 'yaml' or 'ts' — determines how executeCommand loads the handler */
4141
type: 'yaml' | 'ts';
@@ -45,6 +45,38 @@ interface ManifestEntry {
4545
navigateBefore?: boolean | string;
4646
}
4747

48+
interface YamlArgDefinition {
49+
type?: string;
50+
default?: unknown;
51+
required?: boolean;
52+
positional?: boolean;
53+
description?: string;
54+
help?: string;
55+
choices?: string[];
56+
}
57+
58+
interface YamlCliDefinition {
59+
site?: string;
60+
name?: string;
61+
description?: string;
62+
domain?: string;
63+
strategy?: string;
64+
browser?: boolean;
65+
args?: Record<string, YamlArgDefinition>;
66+
columns?: string[];
67+
pipeline?: Record<string, unknown>[];
68+
timeout?: number;
69+
navigateBefore?: boolean | string;
70+
}
71+
72+
function isRecord(value: unknown): value is Record<string, unknown> {
73+
return typeof value === 'object' && value !== null && !Array.isArray(value);
74+
}
75+
76+
function getErrorMessage(error: unknown): string {
77+
return error instanceof Error ? error.message : String(error);
78+
}
79+
4880
function extractBalancedBlock(
4981
source: string,
5082
startIndex: number,
@@ -129,7 +161,7 @@ export function parseTsArgsBlock(argsBlock: string): ManifestEntry['args'] {
129161
const helpMatch = body.match(/help\s*:\s*['"`]([^'"`]*)['"`]/);
130162
const positionalMatch = body.match(/positional\s*:\s*(true|false)/);
131163

132-
let defaultVal: any = undefined;
164+
let defaultVal: unknown = undefined;
133165
if (defaultMatch) {
134166
const raw = defaultMatch[1].trim();
135167
if (raw === 'true') defaultVal = true;
@@ -158,16 +190,17 @@ export function parseTsArgsBlock(argsBlock: string): ManifestEntry['args'] {
158190
function scanYaml(filePath: string, site: string): ManifestEntry | null {
159191
try {
160192
const raw = fs.readFileSync(filePath, 'utf-8');
161-
const def = yaml.load(raw) as any;
162-
if (!def || typeof def !== 'object') return null;
193+
const def = yaml.load(raw) as YamlCliDefinition | null;
194+
if (!isRecord(def)) return null;
195+
const cliDef = def as YamlCliDefinition;
163196

164-
const strategyStr = def.strategy ?? (def.browser === false ? 'public' : 'cookie');
197+
const strategyStr = cliDef.strategy ?? (cliDef.browser === false ? 'public' : 'cookie');
165198
const strategy = strategyStr.toUpperCase();
166-
const browser = def.browser ?? (strategy !== 'PUBLIC');
199+
const browser = cliDef.browser ?? (strategy !== 'PUBLIC');
167200

168201
const args: ManifestEntry['args'] = [];
169-
if (def.args && typeof def.args === 'object') {
170-
for (const [argName, argDef] of Object.entries(def.args as Record<string, any>)) {
202+
if (cliDef.args && typeof cliDef.args === 'object') {
203+
for (const [argName, argDef] of Object.entries(cliDef.args)) {
171204
args.push({
172205
name: argName,
173206
type: argDef?.type ?? 'str',
@@ -180,21 +213,21 @@ function scanYaml(filePath: string, site: string): ManifestEntry | null {
180213
}
181214

182215
return {
183-
site: def.site ?? site,
184-
name: def.name ?? path.basename(filePath, path.extname(filePath)),
185-
description: def.description ?? '',
186-
domain: def.domain,
216+
site: cliDef.site ?? site,
217+
name: cliDef.name ?? path.basename(filePath, path.extname(filePath)),
218+
description: cliDef.description ?? '',
219+
domain: cliDef.domain,
187220
strategy: strategy.toLowerCase(),
188221
browser,
189222
args,
190-
columns: def.columns,
191-
pipeline: def.pipeline,
192-
timeout: def.timeout,
223+
columns: cliDef.columns,
224+
pipeline: cliDef.pipeline,
225+
timeout: cliDef.timeout,
193226
type: 'yaml',
194-
navigateBefore: def.navigateBefore,
227+
navigateBefore: cliDef.navigateBefore,
195228
};
196-
} catch (err: any) {
197-
process.stderr.write(`Warning: failed to parse ${filePath}: ${err.message}\n`);
229+
} catch (err) {
230+
process.stderr.write(`Warning: failed to parse ${filePath}: ${getErrorMessage(err)}\n`);
198231
return null;
199232
}
200233
}
@@ -256,9 +289,9 @@ export function scanTs(filePath: string, site: string): ManifestEntry | null {
256289
if (navMatch) entry.navigateBefore = navMatch[1] === 'true' ? true : false;
257290

258291
return entry;
259-
} catch (err: any) {
292+
} catch (err) {
260293
// If parsing fails, log a warning (matching scanYaml behaviour) and skip the entry.
261-
process.stderr.write(`Warning: failed to scan ${filePath}: ${err.message}\n`);
294+
process.stderr.write(`Warning: failed to scan ${filePath}: ${getErrorMessage(err)}\n`);
262295
return null;
263296
}
264297
}

src/commanderAdapter.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import { render as renderOutput } from './output.js';
1818
import { executeCommand } from './execution.js';
1919
import { CliError } from './errors.js';
2020

21+
function getErrorMessage(error: unknown): string {
22+
return error instanceof Error ? error.message : String(error);
23+
}
24+
2125
/**
2226
* Register a single CliCommand as a Commander subcommand.
2327
*/
@@ -46,49 +50,52 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
4650

4751
subCmd.addHelpText('after', formatRegistryHelpText(cmd));
4852

49-
subCmd.action(async (...actionArgs: any[]) => {
53+
subCmd.action(async (...actionArgs: unknown[]) => {
5054
const actionOpts = actionArgs[positionalArgs.length] ?? {};
55+
const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts as Record<string, unknown> : {};
5156
const startTime = Date.now();
5257

5358
// ── Collect kwargs ──────────────────────────────────────────────────
54-
const kwargs: Record<string, any> = {};
59+
const kwargs: Record<string, unknown> = {};
5560
for (let i = 0; i < positionalArgs.length; i++) {
5661
const v = actionArgs[i];
5762
if (v !== undefined) kwargs[positionalArgs[i].name] = v;
5863
}
5964
for (const arg of cmd.args) {
6065
if (arg.positional) continue;
6166
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
62-
const v = actionOpts[arg.name] ?? actionOpts[camelName];
67+
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
6368
if (v !== undefined) kwargs[arg.name] = v;
6469
}
6570

6671
// ── Execute + render ────────────────────────────────────────────────
6772
try {
68-
if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
73+
const verbose = optionsRecord.verbose === true;
74+
const format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
75+
if (verbose) process.env.OPENCLI_VERBOSE = '1';
6976

70-
const result = await executeCommand(cmd, kwargs, actionOpts.verbose);
77+
const result = await executeCommand(cmd, kwargs, verbose);
7178

72-
if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
79+
if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
7380
console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
7481
}
7582
const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
7683
renderOutput(result, {
77-
fmt: actionOpts.format,
84+
fmt: format,
7885
columns: resolved.columns,
7986
title: `${resolved.site}/${resolved.name}`,
8087
elapsed: (Date.now() - startTime) / 1000,
8188
source: fullName(resolved),
8289
footerExtra: resolved.footerExtra?.(kwargs),
8390
});
84-
} catch (err: any) {
91+
} catch (err) {
8592
if (err instanceof CliError) {
8693
console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
8794
if (err.hint) console.error(chalk.yellow(`Hint: ${err.hint}`));
88-
} else if (actionOpts.verbose && err.stack) {
95+
} else if (optionsRecord.verbose === true && err instanceof Error && err.stack) {
8996
console.error(chalk.red(err.stack));
9097
} else {
91-
console.error(chalk.red(`Error: ${err.message ?? err}`));
98+
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
9299
}
93100
process.exitCode = 1;
94101
}

0 commit comments

Comments
 (0)