fconvert 0.1.4 → 0.1.5

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/dist/convert CHANGED
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fconvert",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Fast local CLI conversion tool with graph-based anything-to-anything routing",
5
5
  "type": "module",
6
6
  "main": "src/cli/main.ts",
@@ -7,10 +7,13 @@ import { ConsoleLogger } from "../../core/logger.ts";
7
7
  import type { ConvertSummary, FileFormat } from "../../core/types.ts";
8
8
  import { buildDefaultOutputPath, pickOutputFormatInteractive } from "../output-picker.ts";
9
9
  import { ConversionEngine } from "../../executor/executor.ts";
10
+ import { Workspace } from "../../executor/workspace.ts";
10
11
  import { detectInputFormat, resolveOutputFormat } from "../../formats/detect.ts";
11
12
  import { FormatRegistry } from "../../formats/registry.ts";
12
13
  import { HandlerRegistry } from "../../handlers/registry.ts";
14
+ import { ConversionGraph } from "../../planner/graph.ts";
13
15
  import { explainRoute } from "../../planner/explain.ts";
16
+ import { findReachableFormats } from "../../planner/search.ts";
14
17
 
15
18
  async function outputExists(path: string): Promise<boolean> {
16
19
  try {
@@ -21,6 +24,37 @@ async function outputExists(path: string): Promise<boolean> {
21
24
  }
22
25
  }
23
26
 
27
+ async function findSelectableOutputFormats(input: {
28
+ formats: FormatRegistry;
29
+ inputFormat: FileFormat;
30
+ bundle: BundleResolver;
31
+ logger: ConsoleLogger;
32
+ strict: boolean;
33
+ maxSteps: number;
34
+ timeoutMs?: number;
35
+ }): Promise<FileFormat[]> {
36
+ const workspace = await Workspace.create("convert-probe-");
37
+ const probeHandlers = new HandlerRegistry();
38
+ try {
39
+ await probeHandlers.init({
40
+ workspace,
41
+ bundle: input.bundle,
42
+ logger: input.logger,
43
+ timeoutMs: input.timeoutMs,
44
+ });
45
+
46
+ const graph = new ConversionGraph(input.formats, probeHandlers.availableHandlers(), input.strict);
47
+ const reachable = findReachableFormats(graph, input.inputFormat.id, Math.max(1, input.maxSteps));
48
+ const candidates = input.formats
49
+ .all()
50
+ .filter((format) => reachable.has(format.id))
51
+ .sort((a, b) => a.id.localeCompare(b.id));
52
+ return candidates;
53
+ } finally {
54
+ await workspace.cleanup(false);
55
+ }
56
+ }
57
+
24
58
  export async function runConvertCommand(input: {
25
59
  positionals: string[];
26
60
  options: {
@@ -54,11 +88,21 @@ export async function runConvertCommand(input: {
54
88
 
55
89
  const formats = new FormatRegistry();
56
90
  const inputFormat = detectInputFormat(inputPath, input.options.from, formats);
91
+ const bundle = new BundleResolver();
57
92
  let outputFormat: FileFormat;
58
93
  if (explicitOutputPath || input.options.to) {
59
94
  outputFormat = resolveOutputFormat(explicitOutputPath, input.options.to, formats);
60
95
  } else {
61
- outputFormat = await pickOutputFormatInteractive(formats, inputPath);
96
+ const selectableFormats = await findSelectableOutputFormats({
97
+ formats,
98
+ inputFormat,
99
+ bundle,
100
+ logger,
101
+ strict: input.options.strict,
102
+ maxSteps: input.options.maxSteps,
103
+ timeoutMs: input.options.timeoutMs,
104
+ });
105
+ outputFormat = await pickOutputFormatInteractive(selectableFormats, inputPath);
62
106
  }
63
107
 
64
108
  const outputPath = explicitOutputPath ?? buildDefaultOutputPath(inputPath, outputFormat);
@@ -67,7 +111,6 @@ export async function runConvertCommand(input: {
67
111
  throw new CliError(`Output exists: ${outputPath}. Use --force to overwrite.`, ExitCode.InvalidArgs);
68
112
  }
69
113
 
70
- const bundle = new BundleResolver();
71
114
  const handlers = new HandlerRegistry();
72
115
  const engine = new ConversionEngine(formats, handlers, bundle, logger);
73
116
 
@@ -4,7 +4,6 @@ import { tmpdir } from "node:os";
4
4
  import { basename, dirname, extname, join } from "node:path";
5
5
  import { CliError, ExitCode } from "../core/errors.ts";
6
6
  import type { FileFormat } from "../core/types.ts";
7
- import { FormatRegistry } from "../formats/registry.ts";
8
7
 
9
8
  interface PickerPayload {
10
9
  prompt: string;
@@ -87,7 +86,7 @@ export function buildDefaultOutputPath(inputPath: string, outputFormat: FileForm
87
86
  }
88
87
 
89
88
  export async function pickOutputFormatInteractive(
90
- registry: FormatRegistry,
89
+ formats: FileFormat[],
91
90
  inputPath: string,
92
91
  ): Promise<FileFormat> {
93
92
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -97,6 +96,13 @@ export async function pickOutputFormatInteractive(
97
96
  );
98
97
  }
99
98
 
99
+ if (formats.length === 0) {
100
+ throw new CliError(
101
+ "No reachable output formats detected for this input with current handlers.",
102
+ ExitCode.UnsupportedRoute,
103
+ );
104
+ }
105
+
100
106
  const pickerBinary = await resolvePickerBinary();
101
107
  if (!pickerBinary) {
102
108
  throw new CliError(
@@ -113,7 +119,7 @@ export async function pickOutputFormatInteractive(
113
119
  prompt: "output format",
114
120
  query: "",
115
121
  preferred: extname(inputPath).replace(/^\./, ""),
116
- options: registry.all().map((format) => ({
122
+ options: formats.map((format) => ({
117
123
  id: format.id,
118
124
  name: format.name,
119
125
  extension: format.extension,
@@ -141,7 +147,7 @@ export async function pickOutputFormatInteractive(
141
147
  }
142
148
 
143
149
  const chosenId = (await readFile(resultPath, "utf8")).trim();
144
- const format = registry.getById(chosenId);
150
+ const format = formats.find((item) => item.id === chosenId);
145
151
  if (!format) {
146
152
  throw new CliError(`Picker returned unknown format: ${chosenId}`, ExitCode.InternalError);
147
153
  }
@@ -75,3 +75,45 @@ export function findRoutes(
75
75
 
76
76
  return routes.sort((a, b) => a.totalCost - b.totalCost);
77
77
  }
78
+
79
+ interface ReachableState {
80
+ node: string;
81
+ steps: number;
82
+ }
83
+
84
+ export function findReachableFormats(
85
+ graph: ConversionGraph,
86
+ fromId: string,
87
+ maxSteps: number,
88
+ ): Set<string> {
89
+ const reached = new Set<string>();
90
+ const bestSteps = new Map<string, number>();
91
+ const queue: ReachableState[] = [{ node: fromId, steps: 0 }];
92
+
93
+ bestSteps.set(fromId, 0);
94
+
95
+ while (queue.length > 0) {
96
+ const current = queue.shift();
97
+ if (!current) {
98
+ break;
99
+ }
100
+
101
+ if (current.steps >= maxSteps) {
102
+ continue;
103
+ }
104
+
105
+ for (const edge of graph.outgoing(current.node)) {
106
+ const nextSteps = current.steps + 1;
107
+ const known = bestSteps.get(edge.to.id);
108
+ if (typeof known === "number" && known <= nextSteps) {
109
+ continue;
110
+ }
111
+
112
+ bestSteps.set(edge.to.id, nextSteps);
113
+ reached.add(edge.to.id);
114
+ queue.push({ node: edge.to.id, steps: nextSteps });
115
+ }
116
+ }
117
+
118
+ return reached;
119
+ }