fconvert 0.1.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.
Files changed (46) hide show
  1. package/README.md +54 -0
  2. package/assets/manifest.json +15 -0
  3. package/bun.lock +26 -0
  4. package/dist/convert +0 -0
  5. package/package.json +25 -0
  6. package/src/artifacts/artifact.ts +21 -0
  7. package/src/artifacts/file.ts +11 -0
  8. package/src/bundle/manifest.ts +10 -0
  9. package/src/bundle/platform.ts +10 -0
  10. package/src/bundle/resolve.ts +88 -0
  11. package/src/cli/commands/convert.ts +105 -0
  12. package/src/cli/commands/doctor.ts +16 -0
  13. package/src/cli/commands/formats.ts +20 -0
  14. package/src/cli/commands/handlers.ts +37 -0
  15. package/src/cli/commands/route.ts +91 -0
  16. package/src/cli/main.ts +69 -0
  17. package/src/cli/parse.ts +127 -0
  18. package/src/core/config.ts +16 -0
  19. package/src/core/errors.ts +18 -0
  20. package/src/core/logger.ts +38 -0
  21. package/src/core/types.ts +77 -0
  22. package/src/diagnostics/doctor.ts +43 -0
  23. package/src/executor/executor.ts +177 -0
  24. package/src/executor/workspace.ts +47 -0
  25. package/src/formats/aliases.ts +8 -0
  26. package/src/formats/common.ts +69 -0
  27. package/src/formats/detect.ts +62 -0
  28. package/src/formats/registry.ts +64 -0
  29. package/src/handlers/base.ts +61 -0
  30. package/src/handlers/bridges/binary.ts +22 -0
  31. package/src/handlers/bridges/text.ts +127 -0
  32. package/src/handlers/exec.ts +47 -0
  33. package/src/handlers/native/ffmpeg.ts +77 -0
  34. package/src/handlers/native/imagemagick.ts +65 -0
  35. package/src/handlers/native/pandoc.ts +66 -0
  36. package/src/handlers/native/sevenzip.ts +115 -0
  37. package/src/handlers/registry.ts +68 -0
  38. package/src/planner/costs.ts +111 -0
  39. package/src/planner/deadends.ts +31 -0
  40. package/src/planner/explain.ts +12 -0
  41. package/src/planner/graph.ts +68 -0
  42. package/src/planner/search.ts +77 -0
  43. package/test/e2e/engine-bridge.test.ts +52 -0
  44. package/test/unit/detect.test.ts +15 -0
  45. package/test/unit/planner.test.ts +46 -0
  46. package/tsconfig.json +31 -0
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # convert
2
+
3
+ Fast local CLI conversion tool with graph-based anything-to-anything routing.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ bun install
9
+ bun run convert formats
10
+ ```
11
+
12
+ ## Commands
13
+
14
+ ```bash
15
+ bun run convert <input> <output>
16
+ bun run convert route <input> --to <format>
17
+ bun run convert formats
18
+ bun run convert handlers
19
+ bun run convert doctor
20
+ ```
21
+
22
+ ## Useful flags
23
+
24
+ ```bash
25
+ --from <format>
26
+ --to <format>
27
+ --output <path>
28
+ --force
29
+ --strict
30
+ --show-route
31
+ --json
32
+ --verbose
33
+ --keep-temp
34
+ --max-steps <n>
35
+ --max-candidates <n>
36
+ ```
37
+
38
+ ## Build
39
+
40
+ ```bash
41
+ bun run build
42
+ ```
43
+
44
+ Compiled binary is written to `dist/convert`.
45
+
46
+ ## Project structure
47
+
48
+ - `src/cli`: command parsing and command entrypoints
49
+ - `src/formats`: format registry and detection
50
+ - `src/handlers`: native and bridge handlers
51
+ - `src/planner`: weighted route graph and search
52
+ - `src/executor`: workspace and route execution
53
+ - `src/bundle`: bundled binary resolution
54
+ - `src/diagnostics`: doctor checks
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "platforms": {
4
+ "linux-x64": {
5
+ "bins": {
6
+ "ffmpeg": "bin/ffmpeg",
7
+ "pandoc": "bin/pandoc",
8
+ "magick": "bin/magick",
9
+ "7zz": "bin/7zz"
10
+ },
11
+ "wasm": [],
12
+ "libs": []
13
+ }
14
+ }
15
+ }
package/bun.lock ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "convert-cli",
7
+ "devDependencies": {
8
+ "@types/bun": "latest",
9
+ },
10
+ "peerDependencies": {
11
+ "typescript": "^5",
12
+ },
13
+ },
14
+ },
15
+ "packages": {
16
+ "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
17
+
18
+ "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
19
+
20
+ "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
21
+
22
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
23
+
24
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
25
+ }
26
+ }
package/dist/convert ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "fconvert",
3
+ "version": "0.1.0",
4
+ "description": "Fast local CLI conversion tool with graph-based anything-to-anything routing",
5
+ "type": "module",
6
+ "main": "src/cli/main.ts",
7
+ "scripts": {
8
+ "start": "bun run src/cli/main.ts",
9
+ "convert": "bun run src/cli/main.ts",
10
+ "test": "bun test",
11
+ "typecheck": "bunx tsc --noEmit",
12
+ "build": "bun build src/cli/main.ts --compile --outfile dist/convert"
13
+ },
14
+ "bin": {
15
+ "convert": "dist/convert"
16
+ },
17
+ "devDependencies": {
18
+ "@types/bun": "latest"
19
+ },
20
+ "peerDependencies": {
21
+ "typescript": "^5"
22
+ },
23
+ "keywords": ["convert", "converter", "ffmpeg", "pandoc", "cli"],
24
+ "license": "MIT"
25
+ }
@@ -0,0 +1,21 @@
1
+ import type { FileFormat } from "../core/types.ts";
2
+
3
+ export type ArtifactKind = "file" | "directory" | "sequence";
4
+
5
+ export interface ArtifactRef {
6
+ kind: ArtifactKind;
7
+ path: string;
8
+ format?: FileFormat;
9
+ size?: number;
10
+ metadata?: Record<string, unknown>;
11
+ }
12
+
13
+ export interface ConversionInput {
14
+ artifact: ArtifactRef;
15
+ format: FileFormat;
16
+ }
17
+
18
+ export interface ConversionOutput {
19
+ artifacts: ArtifactRef[];
20
+ format: FileFormat;
21
+ }
@@ -0,0 +1,11 @@
1
+ import { stat } from "node:fs/promises";
2
+ import type { ArtifactRef } from "./artifact.ts";
3
+
4
+ export async function toFileArtifact(path: string): Promise<ArtifactRef> {
5
+ const fileStat = await stat(path);
6
+ return {
7
+ kind: "file",
8
+ path,
9
+ size: fileStat.size,
10
+ };
11
+ }
@@ -0,0 +1,10 @@
1
+ export interface PlatformManifest {
2
+ bins: Record<string, string>;
3
+ wasm?: string[];
4
+ libs?: string[];
5
+ }
6
+
7
+ export interface BundleManifest {
8
+ version: string;
9
+ platforms: Record<string, PlatformManifest>;
10
+ }
@@ -0,0 +1,10 @@
1
+ import { arch, platform } from "node:process";
2
+
3
+ export function platformKey(): string {
4
+ const os = platform;
5
+ const cpu = arch;
6
+
7
+ const osPart = os === "win32" ? "windows" : os === "darwin" ? "macos" : os;
8
+ const cpuPart = cpu === "x64" ? "x64" : cpu === "arm64" ? "arm64" : cpu;
9
+ return `${osPart}-${cpuPart}`;
10
+ }
@@ -0,0 +1,88 @@
1
+ import { constants as fsConstants } from "node:fs";
2
+ import { access, readFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { CliError, ExitCode } from "../core/errors.ts";
6
+ import type { BundleManifest, PlatformManifest } from "./manifest.ts";
7
+ import { platformKey } from "./platform.ts";
8
+
9
+ function isExecutable(path: string): Promise<boolean> {
10
+ return access(path, fsConstants.X_OK)
11
+ .then(() => true)
12
+ .catch(() => false);
13
+ }
14
+
15
+ export class BundleResolver {
16
+ readonly assetsDir: string;
17
+ private manifestCache?: BundleManifest;
18
+
19
+ constructor(assetsDir?: string) {
20
+ if (assetsDir) {
21
+ this.assetsDir = assetsDir;
22
+ return;
23
+ }
24
+
25
+ const fromEnv = process.env.CONVERT_ASSETS_DIR;
26
+ if (fromEnv) {
27
+ this.assetsDir = fromEnv;
28
+ return;
29
+ }
30
+
31
+ const currentDir = dirname(fileURLToPath(import.meta.url));
32
+ this.assetsDir = join(currentDir, "../../assets");
33
+ }
34
+
35
+ private async manifest(): Promise<BundleManifest | undefined> {
36
+ if (this.manifestCache) {
37
+ return this.manifestCache;
38
+ }
39
+
40
+ const manifestPath = join(this.assetsDir, "manifest.json");
41
+ try {
42
+ const contents = await readFile(manifestPath, "utf8");
43
+ this.manifestCache = JSON.parse(contents) as BundleManifest;
44
+ return this.manifestCache;
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ async resolveBinary(name: string, allowSystem = true): Promise<string | undefined> {
51
+ const envKey = `CONVERT_BIN_${name.toUpperCase()}`;
52
+ const fromEnv = process.env[envKey];
53
+ if (fromEnv && (await isExecutable(fromEnv))) {
54
+ return fromEnv;
55
+ }
56
+
57
+ const manifest = await this.manifest();
58
+ const platform = platformKey();
59
+ const platformManifest: PlatformManifest | undefined = manifest?.platforms[platform];
60
+ const relative = platformManifest?.bins[name];
61
+ if (relative) {
62
+ const candidate = join(this.assetsDir, platform, relative);
63
+ if (await isExecutable(candidate)) {
64
+ return candidate;
65
+ }
66
+ }
67
+
68
+ if (allowSystem) {
69
+ const fromSystem = Bun.which(name);
70
+ if (fromSystem) {
71
+ return fromSystem;
72
+ }
73
+ }
74
+
75
+ return undefined;
76
+ }
77
+
78
+ async mustResolveBinary(name: string): Promise<string> {
79
+ const path = await this.resolveBinary(name);
80
+ if (!path) {
81
+ throw new CliError(
82
+ `Required binary not found: ${name}. Run 'convert doctor' for details.`,
83
+ ExitCode.EnvironmentError,
84
+ );
85
+ }
86
+ return path;
87
+ }
88
+ }
@@ -0,0 +1,105 @@
1
+ import { access } from "node:fs/promises";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { BundleResolver } from "../../bundle/resolve.ts";
4
+ import { buildPlanOptions } from "../../core/config.ts";
5
+ import { CliError, ExitCode } from "../../core/errors.ts";
6
+ import { ConsoleLogger } from "../../core/logger.ts";
7
+ import type { ConvertSummary } from "../../core/types.ts";
8
+ import { ConversionEngine } from "../../executor/executor.ts";
9
+ import { detectInputFormat, resolveOutputFormat } from "../../formats/detect.ts";
10
+ import { FormatRegistry } from "../../formats/registry.ts";
11
+ import { HandlerRegistry } from "../../handlers/registry.ts";
12
+ import { explainRoute } from "../../planner/explain.ts";
13
+
14
+ async function outputExists(path: string): Promise<boolean> {
15
+ try {
16
+ await access(path, fsConstants.F_OK);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ export async function runConvertCommand(input: {
24
+ positionals: string[];
25
+ options: {
26
+ from?: string;
27
+ to?: string;
28
+ output?: string;
29
+ force: boolean;
30
+ strict: boolean;
31
+ json: boolean;
32
+ verbose: boolean;
33
+ quiet: boolean;
34
+ showRoute: boolean;
35
+ keepTemp: boolean;
36
+ timeoutMs?: number;
37
+ maxSteps: number;
38
+ maxCandidates: number;
39
+ };
40
+ }): Promise<void> {
41
+ const logger = new ConsoleLogger(input.options.verbose, input.options.quiet);
42
+
43
+ const inputPath = input.positionals[0];
44
+ const positionalOutput = input.positionals[1];
45
+ const outputPath = input.options.output ?? positionalOutput;
46
+
47
+ if (!inputPath) {
48
+ throw new CliError("Usage: convert <input> <output> [--from fmt] [--to fmt]", ExitCode.InvalidArgs);
49
+ }
50
+
51
+ if (!outputPath) {
52
+ throw new CliError("Missing output path. Provide <output> or --output", ExitCode.InvalidArgs);
53
+ }
54
+
55
+ if ((await outputExists(outputPath)) && !input.options.force) {
56
+ throw new CliError(`Output exists: ${outputPath}. Use --force to overwrite.`, ExitCode.InvalidArgs);
57
+ }
58
+
59
+ const formats = new FormatRegistry();
60
+ const inputFormat = detectInputFormat(inputPath, input.options.from, formats);
61
+ const outputFormat = resolveOutputFormat(outputPath, input.options.to, formats);
62
+
63
+ const bundle = new BundleResolver();
64
+ const handlers = new HandlerRegistry();
65
+ const engine = new ConversionEngine(formats, handlers, bundle, logger);
66
+
67
+ const result = await engine.execute({
68
+ inputPath,
69
+ outputPath,
70
+ inputFormat,
71
+ outputFormat,
72
+ strict: input.options.strict,
73
+ keepTemp: input.options.keepTemp,
74
+ timeoutMs: input.options.timeoutMs,
75
+ plan: buildPlanOptions({
76
+ strict: input.options.strict,
77
+ maxSteps: input.options.maxSteps,
78
+ maxCandidates: input.options.maxCandidates,
79
+ }),
80
+ });
81
+
82
+ const route = explainRoute(result.route);
83
+ const summary: ConvertSummary = {
84
+ ok: true,
85
+ input: inputPath,
86
+ output: result.outputPath,
87
+ route,
88
+ durationMs: result.durationMs,
89
+ warnings: result.warnings,
90
+ };
91
+
92
+ if (input.options.json) {
93
+ console.log(JSON.stringify(summary, null, 2));
94
+ return;
95
+ }
96
+
97
+ logger.info(`converted: ${inputPath} -> ${result.outputPath}`);
98
+ logger.info(`duration: ${result.durationMs}ms`);
99
+ if (input.options.showRoute || input.options.verbose) {
100
+ logger.info(`route: ${route.join(" | ")}`);
101
+ }
102
+ for (const warning of result.warnings) {
103
+ logger.warn(`warning: ${warning}`);
104
+ }
105
+ }
@@ -0,0 +1,16 @@
1
+ import { BundleResolver } from "../../bundle/resolve.ts";
2
+ import { runDoctor } from "../../diagnostics/doctor.ts";
3
+
4
+ export async function runDoctorCommand(json: boolean): Promise<void> {
5
+ const bundle = new BundleResolver();
6
+ const checks = await runDoctor(bundle);
7
+
8
+ if (json) {
9
+ console.log(JSON.stringify({ ok: checks.every((item) => item.ok), checks }, null, 2));
10
+ return;
11
+ }
12
+
13
+ for (const check of checks) {
14
+ console.log(`${check.ok ? "ok " : "err"} ${check.name.padEnd(16)} ${check.detail}`);
15
+ }
16
+ }
@@ -0,0 +1,20 @@
1
+ import { FormatRegistry } from "../../formats/registry.ts";
2
+
3
+ export async function runFormatsCommand(json: boolean): Promise<void> {
4
+ const registry = new FormatRegistry();
5
+ const formats = registry.all().map((format) => ({
6
+ id: format.id,
7
+ extension: format.extension,
8
+ category: format.category,
9
+ name: format.name,
10
+ }));
11
+
12
+ if (json) {
13
+ console.log(JSON.stringify({ ok: true, formats }, null, 2));
14
+ return;
15
+ }
16
+
17
+ for (const format of formats) {
18
+ console.log(`${format.id.padEnd(8)} .${format.extension.padEnd(6)} ${format.category.join(",")}`);
19
+ }
20
+ }
@@ -0,0 +1,37 @@
1
+ import { BundleResolver } from "../../bundle/resolve.ts";
2
+ import { ConsoleLogger } from "../../core/logger.ts";
3
+ import { Workspace } from "../../executor/workspace.ts";
4
+ import { HandlerRegistry } from "../../handlers/registry.ts";
5
+
6
+ export async function runHandlersCommand(input: {
7
+ json: boolean;
8
+ verbose: boolean;
9
+ quiet: boolean;
10
+ timeoutMs?: number;
11
+ }): Promise<void> {
12
+ const logger = new ConsoleLogger(input.verbose, input.quiet);
13
+ const workspace = await Workspace.create();
14
+ const bundle = new BundleResolver();
15
+ const handlers = new HandlerRegistry();
16
+
17
+ try {
18
+ await handlers.init({
19
+ workspace,
20
+ bundle,
21
+ logger,
22
+ timeoutMs: input.timeoutMs,
23
+ });
24
+
25
+ const status = handlers.status();
26
+ if (input.json) {
27
+ console.log(JSON.stringify({ ok: true, handlers: status }, null, 2));
28
+ return;
29
+ }
30
+
31
+ for (const item of status) {
32
+ console.log(`${item.name.padEnd(14)} ${item.available ? "available" : "missing"}`);
33
+ }
34
+ } finally {
35
+ await workspace.cleanup(false);
36
+ }
37
+ }
@@ -0,0 +1,91 @@
1
+ import { BundleResolver } from "../../bundle/resolve.ts";
2
+ import { buildPlanOptions } from "../../core/config.ts";
3
+ import { CliError, ExitCode } from "../../core/errors.ts";
4
+ import { ConsoleLogger } from "../../core/logger.ts";
5
+ import { detectInputFormat, resolveOutputFormat } from "../../formats/detect.ts";
6
+ import { FormatRegistry } from "../../formats/registry.ts";
7
+ import { HandlerRegistry } from "../../handlers/registry.ts";
8
+ import { describeRoutes } from "../../planner/explain.ts";
9
+ import { ConversionEngine } from "../../executor/executor.ts";
10
+
11
+ export async function runRouteCommand(input: {
12
+ positionals: string[];
13
+ options: {
14
+ from?: string;
15
+ to?: string;
16
+ output?: string;
17
+ strict: boolean;
18
+ json: boolean;
19
+ verbose: boolean;
20
+ quiet: boolean;
21
+ timeoutMs?: number;
22
+ maxSteps: number;
23
+ maxCandidates: number;
24
+ };
25
+ }): Promise<void> {
26
+ const logger = new ConsoleLogger(input.options.verbose, input.options.quiet);
27
+
28
+ const inputPath = input.positionals[0];
29
+ const positionalOutput = input.positionals[1];
30
+ const outputPath = input.options.output ?? positionalOutput;
31
+
32
+ if (!inputPath) {
33
+ throw new CliError("Usage: convert route <input> --to <format>", ExitCode.InvalidArgs);
34
+ }
35
+ if (!input.options.to && !outputPath) {
36
+ throw new CliError("Missing target format: provide --to or output path", ExitCode.InvalidArgs);
37
+ }
38
+
39
+ const formats = new FormatRegistry();
40
+ const inputFormat = detectInputFormat(inputPath, input.options.from, formats);
41
+ const outputFormat = resolveOutputFormat(outputPath, input.options.to, formats);
42
+
43
+ const bundle = new BundleResolver();
44
+ const handlers = new HandlerRegistry();
45
+ const engine = new ConversionEngine(formats, handlers, bundle, logger);
46
+
47
+ const planned = await engine.planRoutes(
48
+ inputFormat,
49
+ outputFormat,
50
+ input.options.strict,
51
+ buildPlanOptions({
52
+ strict: input.options.strict,
53
+ maxSteps: input.options.maxSteps,
54
+ maxCandidates: input.options.maxCandidates,
55
+ }),
56
+ input.options.timeoutMs,
57
+ );
58
+
59
+ try {
60
+ if (input.options.json) {
61
+ console.log(
62
+ JSON.stringify(
63
+ {
64
+ ok: true,
65
+ from: inputFormat.id,
66
+ to: outputFormat.id,
67
+ routes: planned.routes.map((route) => ({
68
+ totalCost: route.totalCost,
69
+ steps: route.edges.map((edge) => `${edge.handler.name}:${edge.from.id}->${edge.to.id}`),
70
+ })),
71
+ },
72
+ null,
73
+ 2,
74
+ ),
75
+ );
76
+ return;
77
+ }
78
+
79
+ if (planned.routes.length === 0) {
80
+ logger.info(`no route found for ${inputFormat.id} -> ${outputFormat.id}`);
81
+ return;
82
+ }
83
+
84
+ logger.info(`routes for ${inputFormat.id} -> ${outputFormat.id}`);
85
+ for (const line of describeRoutes(planned.routes, 10)) {
86
+ logger.info(line);
87
+ }
88
+ } finally {
89
+ await planned.workspace.cleanup(false);
90
+ }
91
+ }
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { runConvertCommand } from "./commands/convert.ts";
4
+ import { runDoctorCommand } from "./commands/doctor.ts";
5
+ import { runFormatsCommand } from "./commands/formats.ts";
6
+ import { runHandlersCommand } from "./commands/handlers.ts";
7
+ import { runRouteCommand } from "./commands/route.ts";
8
+ import { CliError, ExitCode } from "../core/errors.ts";
9
+ import { parseArgs } from "./parse.ts";
10
+
11
+ function printUsage(): void {
12
+ console.log("convert <input> <output> [--from fmt] [--to fmt]");
13
+ console.log("convert route <input> --to <format>");
14
+ console.log("convert formats");
15
+ console.log("convert handlers");
16
+ console.log("convert doctor");
17
+ }
18
+
19
+ async function main(): Promise<void> {
20
+ const parsed = parseArgs(Bun.argv.slice(2));
21
+
22
+ switch (parsed.command) {
23
+ case "convert":
24
+ await runConvertCommand({
25
+ positionals: parsed.positionals,
26
+ options: parsed.options,
27
+ });
28
+ return;
29
+ case "route":
30
+ await runRouteCommand({
31
+ positionals: parsed.positionals,
32
+ options: parsed.options,
33
+ });
34
+ return;
35
+ case "formats":
36
+ await runFormatsCommand(parsed.options.json);
37
+ return;
38
+ case "handlers":
39
+ await runHandlersCommand({
40
+ json: parsed.options.json,
41
+ verbose: parsed.options.verbose,
42
+ quiet: parsed.options.quiet,
43
+ timeoutMs: parsed.options.timeoutMs,
44
+ });
45
+ return;
46
+ case "doctor":
47
+ await runDoctorCommand(parsed.options.json);
48
+ return;
49
+ default:
50
+ throw new CliError(`Unknown command: ${parsed.command}`, ExitCode.InvalidArgs);
51
+ }
52
+ }
53
+
54
+ main().catch((error: unknown) => {
55
+ if (error instanceof CliError) {
56
+ console.error(error.message);
57
+ if (error.exitCode === ExitCode.InvalidArgs) {
58
+ printUsage();
59
+ }
60
+ process.exit(error.exitCode);
61
+ }
62
+
63
+ if (error instanceof Error) {
64
+ console.error(error.message);
65
+ } else {
66
+ console.error(String(error));
67
+ }
68
+ process.exit(ExitCode.InternalError);
69
+ });