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.
- package/README.md +54 -0
- package/assets/manifest.json +15 -0
- package/bun.lock +26 -0
- package/dist/convert +0 -0
- package/package.json +25 -0
- package/src/artifacts/artifact.ts +21 -0
- package/src/artifacts/file.ts +11 -0
- package/src/bundle/manifest.ts +10 -0
- package/src/bundle/platform.ts +10 -0
- package/src/bundle/resolve.ts +88 -0
- package/src/cli/commands/convert.ts +105 -0
- package/src/cli/commands/doctor.ts +16 -0
- package/src/cli/commands/formats.ts +20 -0
- package/src/cli/commands/handlers.ts +37 -0
- package/src/cli/commands/route.ts +91 -0
- package/src/cli/main.ts +69 -0
- package/src/cli/parse.ts +127 -0
- package/src/core/config.ts +16 -0
- package/src/core/errors.ts +18 -0
- package/src/core/logger.ts +38 -0
- package/src/core/types.ts +77 -0
- package/src/diagnostics/doctor.ts +43 -0
- package/src/executor/executor.ts +177 -0
- package/src/executor/workspace.ts +47 -0
- package/src/formats/aliases.ts +8 -0
- package/src/formats/common.ts +69 -0
- package/src/formats/detect.ts +62 -0
- package/src/formats/registry.ts +64 -0
- package/src/handlers/base.ts +61 -0
- package/src/handlers/bridges/binary.ts +22 -0
- package/src/handlers/bridges/text.ts +127 -0
- package/src/handlers/exec.ts +47 -0
- package/src/handlers/native/ffmpeg.ts +77 -0
- package/src/handlers/native/imagemagick.ts +65 -0
- package/src/handlers/native/pandoc.ts +66 -0
- package/src/handlers/native/sevenzip.ts +115 -0
- package/src/handlers/registry.ts +68 -0
- package/src/planner/costs.ts +111 -0
- package/src/planner/deadends.ts +31 -0
- package/src/planner/explain.ts +12 -0
- package/src/planner/graph.ts +68 -0
- package/src/planner/search.ts +77 -0
- package/test/e2e/engine-bridge.test.ts +52 -0
- package/test/unit/detect.test.ts +15 -0
- package/test/unit/planner.test.ts +46 -0
- 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
|
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
|
+
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
|
+
}
|
package/src/cli/main.ts
ADDED
|
@@ -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
|
+
});
|