@notedge/panduck 0.0.0 → 0.0.2
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/bin/{panduck.mjs → panduck} +11 -11
- package/locales/en-US/cli.json +46 -46
- package/locales/locales.json +7 -7
- package/locales/zh-CN/cli.json +46 -46
- package/package.json +30 -34
- package/readme.md +89 -5
- package/src/cli/cli.ts +28 -28
- package/src/cli/commands/check-cmd.ts +174 -174
- package/src/cli/commands/convert-cmd.ts +300 -300
- package/src/cli/commands/doctor-cmd.ts +84 -84
- package/src/cli/commands/formats-cmd.ts +50 -50
- package/src/cli/commands/inspect-cmd.ts +231 -231
- package/src/cli/commands/plan-cmd.ts +188 -188
- package/src/cli/console-backend.ts +16 -16
- package/src/cli/context.ts +98 -98
- package/src/cli/diagnostics.ts +24 -24
- package/src/cli/exit-codes.ts +14 -14
- package/src/cli/format-registry.ts +170 -170
- package/src/cli/index.ts +7 -7
- package/src/cli/main.ts +10 -10
- package/src/cli/options.ts +127 -127
- package/src/cli/paths.ts +57 -56
- package/src/cli/pipeline.ts +225 -225
- package/src/cli/report.ts +110 -110
- package/src/index.ts +8 -8
- package/src/node/index.ts +14 -14
- package/src/node/load.ts +120 -120
- package/src/types.ts +49 -49
- package/src/wasm/index.ts +14 -2
- package/src/wasm/load.ts +25 -23
- package/src/wasm/panduck_wasm.d.ts +0 -6
|
@@ -1,300 +1,300 @@
|
|
|
1
|
-
import { access } from "node:fs/promises";
|
|
2
|
-
import { constants } from "node:fs";
|
|
3
|
-
|
|
4
|
-
import type { Cli, ParsedOptions } from "@vmz/commander";
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
assertReadableInput,
|
|
8
|
-
buildBlockedReport,
|
|
9
|
-
createContext,
|
|
10
|
-
resolveFormats,
|
|
11
|
-
validateIoPaths,
|
|
12
|
-
} from "../context.js";
|
|
13
|
-
import { emitReport } from "../diagnostics.js";
|
|
14
|
-
import { ExitCode } from "../exit-codes.js";
|
|
15
|
-
import { findFormat, isOperationReady } from "../format-registry.js";
|
|
16
|
-
import {
|
|
17
|
-
CliOptionError,
|
|
18
|
-
parseAssetPolicy,
|
|
19
|
-
parseLinkPolicy,
|
|
20
|
-
readSharedOptions,
|
|
21
|
-
readString,
|
|
22
|
-
} from "../options.js";
|
|
23
|
-
import { createReport } from "../report.js";
|
|
24
|
-
import { isStdoutPath } from "../paths.js";
|
|
25
|
-
import { configureConsoleBackend } from "../console-backend.js";
|
|
26
|
-
import { emitPipelineResult, hasPipeline, runConversionPipeline } from "../pipeline.js";
|
|
27
|
-
|
|
28
|
-
export function registerConvertCommand(cli: Cli): void {
|
|
29
|
-
cli.command("convert", "cli.cmd.convert")
|
|
30
|
-
.option("--to <format>", "cli.opt.to")
|
|
31
|
-
.option("-o, --output <file>", "cli.opt.output")
|
|
32
|
-
.option("--from <format>", "cli.opt.from")
|
|
33
|
-
.option("--profile <name>", "cli.opt.profile")
|
|
34
|
-
.option("--config <file>", "cli.opt.config")
|
|
35
|
-
.option("--assets-dir <dir>", "cli.opt.assets-dir")
|
|
36
|
-
.option("--asset-policy <policy>", "cli.opt.asset-policy")
|
|
37
|
-
.option("--link-policy <policy>", "cli.opt.link-policy")
|
|
38
|
-
.option("--loss <policy>", "cli.opt.loss")
|
|
39
|
-
.option("--strict", "cli.opt.strict")
|
|
40
|
-
.option("--allow-partial", "cli.opt.allow-partial")
|
|
41
|
-
.option("--diagnostics <mode>", "cli.opt.diagnostics")
|
|
42
|
-
.option("--batch", "cli.opt.batch")
|
|
43
|
-
.option("--output-dir <dir>", "cli.opt.output-dir")
|
|
44
|
-
.option("--report <file>", "cli.opt.report")
|
|
45
|
-
.option("--overwrite", "cli.opt.overwrite")
|
|
46
|
-
.option("--budget <limit>", "cli.opt.budget")
|
|
47
|
-
.option("--dry-run", "cli.opt.dry-run")
|
|
48
|
-
.action((options: ParsedOptions) => runConvert(options));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function runConvert(options: ParsedOptions): Promise<number> {
|
|
52
|
-
try {
|
|
53
|
-
const shared = readSharedOptions(options);
|
|
54
|
-
const inputPath = options._[0] ?? readString(options, "input");
|
|
55
|
-
const outputPath = readString(options, "output");
|
|
56
|
-
const reportPath = readString(options, "report");
|
|
57
|
-
const batch = options.batch === true;
|
|
58
|
-
const dryRun = options["dry-run"] === true;
|
|
59
|
-
|
|
60
|
-
if (!inputPath) {
|
|
61
|
-
return ExitCode.InvalidArgs;
|
|
62
|
-
}
|
|
63
|
-
if (batch && inputPath === "-") {
|
|
64
|
-
console.error("standard input cannot be used with --batch");
|
|
65
|
-
return ExitCode.InvalidArgs;
|
|
66
|
-
}
|
|
67
|
-
if (batch && !readString(options, "output-dir")) {
|
|
68
|
-
console.error("--batch requires --output-dir");
|
|
69
|
-
return ExitCode.InvalidArgs;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const overlapCode = validateIoPaths(inputPath, outputPath);
|
|
73
|
-
if (overlapCode !== null) {
|
|
74
|
-
console.error(`input and output paths must not be the same: ${inputPath}`);
|
|
75
|
-
return overlapCode;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (isStdoutPath(outputPath) && !shared.to) {
|
|
79
|
-
console.error("writing to stdout requires an explicit --to format");
|
|
80
|
-
return ExitCode.InvalidArgs;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const ctx = createContext();
|
|
84
|
-
configureConsoleBackend(ctx.bindings, shared.logFile);
|
|
85
|
-
const resolved = resolveFormats(inputPath, outputPath, shared);
|
|
86
|
-
|
|
87
|
-
if (resolved.ambiguous) {
|
|
88
|
-
const report = buildBlockedReport(
|
|
89
|
-
ctx,
|
|
90
|
-
"convert",
|
|
91
|
-
inputPath,
|
|
92
|
-
resolved,
|
|
93
|
-
"ambiguous input format",
|
|
94
|
-
ExitCode.InputUnsupported,
|
|
95
|
-
shared,
|
|
96
|
-
);
|
|
97
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
98
|
-
return ExitCode.InputUnsupported;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!resolved.to) {
|
|
102
|
-
const report = buildBlockedReport(
|
|
103
|
-
ctx,
|
|
104
|
-
"convert",
|
|
105
|
-
inputPath,
|
|
106
|
-
resolved,
|
|
107
|
-
"target format is required",
|
|
108
|
-
ExitCode.InvalidArgs,
|
|
109
|
-
shared,
|
|
110
|
-
);
|
|
111
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
112
|
-
return ExitCode.InvalidArgs;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
await assertReadableInput(inputPath);
|
|
117
|
-
} catch {
|
|
118
|
-
const report = buildBlockedReport(
|
|
119
|
-
ctx,
|
|
120
|
-
"convert",
|
|
121
|
-
inputPath,
|
|
122
|
-
resolved,
|
|
123
|
-
"input is not readable",
|
|
124
|
-
ExitCode.InputUnsupported,
|
|
125
|
-
shared,
|
|
126
|
-
);
|
|
127
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
128
|
-
return ExitCode.InputUnsupported;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (outputPath && outputPath !== "-" && !shared.overwrite) {
|
|
132
|
-
try {
|
|
133
|
-
await access(outputPath, constants.F_OK);
|
|
134
|
-
const report = buildBlockedReport(
|
|
135
|
-
ctx,
|
|
136
|
-
"convert",
|
|
137
|
-
inputPath,
|
|
138
|
-
resolved,
|
|
139
|
-
"output already exists",
|
|
140
|
-
ExitCode.OutputFailure,
|
|
141
|
-
shared,
|
|
142
|
-
);
|
|
143
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
144
|
-
return ExitCode.OutputFailure;
|
|
145
|
-
} catch {
|
|
146
|
-
// target does not exist yet
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
parseAssetPolicy(readString(options, "asset-policy"));
|
|
151
|
-
parseLinkPolicy(readString(options, "link-policy"));
|
|
152
|
-
|
|
153
|
-
const source = resolved.from ? findFormat(ctx.registry, resolved.from) : undefined;
|
|
154
|
-
const target = findFormat(ctx.registry, resolved.to);
|
|
155
|
-
if (!target) {
|
|
156
|
-
const report = buildBlockedReport(
|
|
157
|
-
ctx,
|
|
158
|
-
"convert",
|
|
159
|
-
inputPath,
|
|
160
|
-
resolved,
|
|
161
|
-
`unknown target format ${resolved.to}`,
|
|
162
|
-
ExitCode.InputUnsupported,
|
|
163
|
-
shared,
|
|
164
|
-
);
|
|
165
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
166
|
-
return ExitCode.InputUnsupported;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const routeReady = hasPipeline(ctx.bindings, resolved.from, resolved.to);
|
|
170
|
-
|
|
171
|
-
if (resolved.from === "doc") {
|
|
172
|
-
const report = buildBlockedReport(
|
|
173
|
-
ctx,
|
|
174
|
-
"convert",
|
|
175
|
-
inputPath,
|
|
176
|
-
resolved,
|
|
177
|
-
"legacy .doc import requires OLE reader support; convert to .docx first",
|
|
178
|
-
ExitCode.InputUnsupported,
|
|
179
|
-
shared,
|
|
180
|
-
);
|
|
181
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
182
|
-
return ExitCode.InputUnsupported;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (!routeReady) {
|
|
186
|
-
if (source && !isOperationReady(source, "read")) {
|
|
187
|
-
const report = buildBlockedReport(
|
|
188
|
-
ctx,
|
|
189
|
-
"convert",
|
|
190
|
-
inputPath,
|
|
191
|
-
resolved,
|
|
192
|
-
`source format ${source.format} is not ready for read`,
|
|
193
|
-
ExitCode.InputUnsupported,
|
|
194
|
-
shared,
|
|
195
|
-
);
|
|
196
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
197
|
-
return ExitCode.InputUnsupported;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (!isOperationReady(target, "write")) {
|
|
201
|
-
const report = buildBlockedReport(
|
|
202
|
-
ctx,
|
|
203
|
-
"convert",
|
|
204
|
-
inputPath,
|
|
205
|
-
resolved,
|
|
206
|
-
`target format ${target.format} is not ready for write`,
|
|
207
|
-
ExitCode.InputUnsupported,
|
|
208
|
-
shared,
|
|
209
|
-
);
|
|
210
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
211
|
-
return ExitCode.InputUnsupported;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (dryRun) {
|
|
216
|
-
const report = createReport({
|
|
217
|
-
operation: "convert",
|
|
218
|
-
toolVersion: ctx.toolVersion,
|
|
219
|
-
status: "success",
|
|
220
|
-
inputs: [{ path: inputPath, format: resolved.from }],
|
|
221
|
-
detection: { format: resolved.from, confidence: "hint", hints: resolved.hints },
|
|
222
|
-
pipeline: {
|
|
223
|
-
reader: source?.format ?? resolved.from,
|
|
224
|
-
writer: target.format,
|
|
225
|
-
stages: ["detect", "read", "ir", "write", "publish"],
|
|
226
|
-
},
|
|
227
|
-
outputs: outputPath ? [{ path: outputPath, published: false }] : [],
|
|
228
|
-
budgets: shared.budgets,
|
|
229
|
-
determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
|
|
230
|
-
statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.Success },
|
|
231
|
-
});
|
|
232
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
233
|
-
return ExitCode.Success;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (routeReady && ctx.bindings) {
|
|
237
|
-
try {
|
|
238
|
-
const result = await runConversionPipeline({
|
|
239
|
-
ctx,
|
|
240
|
-
bindings: ctx.bindings,
|
|
241
|
-
inputPath,
|
|
242
|
-
outputPath,
|
|
243
|
-
resolved,
|
|
244
|
-
shared,
|
|
245
|
-
toolVersion: ctx.toolVersion,
|
|
246
|
-
});
|
|
247
|
-
return emitPipelineResult(result, shared);
|
|
248
|
-
} catch (error) {
|
|
249
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
250
|
-
const report = buildBlockedReport(
|
|
251
|
-
ctx,
|
|
252
|
-
"convert",
|
|
253
|
-
inputPath,
|
|
254
|
-
resolved,
|
|
255
|
-
message,
|
|
256
|
-
ExitCode.IrOrWriterFailure,
|
|
257
|
-
shared,
|
|
258
|
-
);
|
|
259
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
260
|
-
return ExitCode.IrOrWriterFailure;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const report = createReport({
|
|
265
|
-
operation: "convert",
|
|
266
|
-
toolVersion: ctx.toolVersion,
|
|
267
|
-
status: "blocked",
|
|
268
|
-
inputs: [{ path: inputPath, format: resolved.from }],
|
|
269
|
-
detection: { format: resolved.from, confidence: "hint", hints: resolved.hints },
|
|
270
|
-
pipeline: {
|
|
271
|
-
reader: source?.format ?? resolved.from,
|
|
272
|
-
writer: target.format,
|
|
273
|
-
stages: ["detect", "read", "ir", "write", "publish"],
|
|
274
|
-
blocked_reason: "conversion pipeline is not wired yet",
|
|
275
|
-
},
|
|
276
|
-
diagnostics: [
|
|
277
|
-
{
|
|
278
|
-
owner: "panduck",
|
|
279
|
-
severity: "error",
|
|
280
|
-
message: "conversion pipeline is not wired yet",
|
|
281
|
-
code: "panduck.convert.not_implemented",
|
|
282
|
-
},
|
|
283
|
-
],
|
|
284
|
-
outputs: outputPath ? [{ path: outputPath, published: false }] : [],
|
|
285
|
-
budgets: shared.budgets,
|
|
286
|
-
determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
|
|
287
|
-
statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.IrOrWriterFailure },
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
emitReport(report, shared.diagnostics, shared.json);
|
|
291
|
-
return ExitCode.IrOrWriterFailure;
|
|
292
|
-
} catch (error) {
|
|
293
|
-
if (error instanceof CliOptionError) {
|
|
294
|
-
console.error(error.message);
|
|
295
|
-
return error.exitCode;
|
|
296
|
-
}
|
|
297
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
298
|
-
return ExitCode.InvalidArgs;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
|
|
4
|
+
import type { Cli, ParsedOptions } from "@vmz/commander";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
assertReadableInput,
|
|
8
|
+
buildBlockedReport,
|
|
9
|
+
createContext,
|
|
10
|
+
resolveFormats,
|
|
11
|
+
validateIoPaths,
|
|
12
|
+
} from "../context.js";
|
|
13
|
+
import { emitReport } from "../diagnostics.js";
|
|
14
|
+
import { ExitCode } from "../exit-codes.js";
|
|
15
|
+
import { findFormat, isOperationReady } from "../format-registry.js";
|
|
16
|
+
import {
|
|
17
|
+
CliOptionError,
|
|
18
|
+
parseAssetPolicy,
|
|
19
|
+
parseLinkPolicy,
|
|
20
|
+
readSharedOptions,
|
|
21
|
+
readString,
|
|
22
|
+
} from "../options.js";
|
|
23
|
+
import { createReport } from "../report.js";
|
|
24
|
+
import { isStdoutPath } from "../paths.js";
|
|
25
|
+
import { configureConsoleBackend } from "../console-backend.js";
|
|
26
|
+
import { emitPipelineResult, hasPipeline, runConversionPipeline } from "../pipeline.js";
|
|
27
|
+
|
|
28
|
+
export function registerConvertCommand(cli: Cli): void {
|
|
29
|
+
cli.command("convert", "cli.cmd.convert")
|
|
30
|
+
.option("--to <format>", "cli.opt.to")
|
|
31
|
+
.option("-o, --output <file>", "cli.opt.output")
|
|
32
|
+
.option("--from <format>", "cli.opt.from")
|
|
33
|
+
.option("--profile <name>", "cli.opt.profile")
|
|
34
|
+
.option("--config <file>", "cli.opt.config")
|
|
35
|
+
.option("--assets-dir <dir>", "cli.opt.assets-dir")
|
|
36
|
+
.option("--asset-policy <policy>", "cli.opt.asset-policy")
|
|
37
|
+
.option("--link-policy <policy>", "cli.opt.link-policy")
|
|
38
|
+
.option("--loss <policy>", "cli.opt.loss")
|
|
39
|
+
.option("--strict", "cli.opt.strict")
|
|
40
|
+
.option("--allow-partial", "cli.opt.allow-partial")
|
|
41
|
+
.option("--diagnostics <mode>", "cli.opt.diagnostics")
|
|
42
|
+
.option("--batch", "cli.opt.batch")
|
|
43
|
+
.option("--output-dir <dir>", "cli.opt.output-dir")
|
|
44
|
+
.option("--report <file>", "cli.opt.report")
|
|
45
|
+
.option("--overwrite", "cli.opt.overwrite")
|
|
46
|
+
.option("--budget <limit>", "cli.opt.budget")
|
|
47
|
+
.option("--dry-run", "cli.opt.dry-run")
|
|
48
|
+
.action((options: ParsedOptions) => runConvert(options));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function runConvert(options: ParsedOptions): Promise<number> {
|
|
52
|
+
try {
|
|
53
|
+
const shared = readSharedOptions(options);
|
|
54
|
+
const inputPath = options._[0] ?? readString(options, "input");
|
|
55
|
+
const outputPath = readString(options, "output");
|
|
56
|
+
const reportPath = readString(options, "report");
|
|
57
|
+
const batch = options.batch === true;
|
|
58
|
+
const dryRun = options["dry-run"] === true;
|
|
59
|
+
|
|
60
|
+
if (!inputPath) {
|
|
61
|
+
return ExitCode.InvalidArgs;
|
|
62
|
+
}
|
|
63
|
+
if (batch && inputPath === "-") {
|
|
64
|
+
console.error("standard input cannot be used with --batch");
|
|
65
|
+
return ExitCode.InvalidArgs;
|
|
66
|
+
}
|
|
67
|
+
if (batch && !readString(options, "output-dir")) {
|
|
68
|
+
console.error("--batch requires --output-dir");
|
|
69
|
+
return ExitCode.InvalidArgs;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const overlapCode = validateIoPaths(inputPath, outputPath);
|
|
73
|
+
if (overlapCode !== null) {
|
|
74
|
+
console.error(`input and output paths must not be the same: ${inputPath}`);
|
|
75
|
+
return overlapCode;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isStdoutPath(outputPath) && !shared.to) {
|
|
79
|
+
console.error("writing to stdout requires an explicit --to format");
|
|
80
|
+
return ExitCode.InvalidArgs;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ctx = createContext();
|
|
84
|
+
configureConsoleBackend(ctx.bindings, shared.logFile);
|
|
85
|
+
const resolved = resolveFormats(inputPath, outputPath, shared);
|
|
86
|
+
|
|
87
|
+
if (resolved.ambiguous) {
|
|
88
|
+
const report = buildBlockedReport(
|
|
89
|
+
ctx,
|
|
90
|
+
"convert",
|
|
91
|
+
inputPath,
|
|
92
|
+
resolved,
|
|
93
|
+
"ambiguous input format",
|
|
94
|
+
ExitCode.InputUnsupported,
|
|
95
|
+
shared,
|
|
96
|
+
);
|
|
97
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
98
|
+
return ExitCode.InputUnsupported;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!resolved.to) {
|
|
102
|
+
const report = buildBlockedReport(
|
|
103
|
+
ctx,
|
|
104
|
+
"convert",
|
|
105
|
+
inputPath,
|
|
106
|
+
resolved,
|
|
107
|
+
"target format is required",
|
|
108
|
+
ExitCode.InvalidArgs,
|
|
109
|
+
shared,
|
|
110
|
+
);
|
|
111
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
112
|
+
return ExitCode.InvalidArgs;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await assertReadableInput(inputPath);
|
|
117
|
+
} catch {
|
|
118
|
+
const report = buildBlockedReport(
|
|
119
|
+
ctx,
|
|
120
|
+
"convert",
|
|
121
|
+
inputPath,
|
|
122
|
+
resolved,
|
|
123
|
+
"input is not readable",
|
|
124
|
+
ExitCode.InputUnsupported,
|
|
125
|
+
shared,
|
|
126
|
+
);
|
|
127
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
128
|
+
return ExitCode.InputUnsupported;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (outputPath && outputPath !== "-" && !shared.overwrite) {
|
|
132
|
+
try {
|
|
133
|
+
await access(outputPath, constants.F_OK);
|
|
134
|
+
const report = buildBlockedReport(
|
|
135
|
+
ctx,
|
|
136
|
+
"convert",
|
|
137
|
+
inputPath,
|
|
138
|
+
resolved,
|
|
139
|
+
"output already exists",
|
|
140
|
+
ExitCode.OutputFailure,
|
|
141
|
+
shared,
|
|
142
|
+
);
|
|
143
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
144
|
+
return ExitCode.OutputFailure;
|
|
145
|
+
} catch {
|
|
146
|
+
// target does not exist yet
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
parseAssetPolicy(readString(options, "asset-policy"));
|
|
151
|
+
parseLinkPolicy(readString(options, "link-policy"));
|
|
152
|
+
|
|
153
|
+
const source = resolved.from ? findFormat(ctx.registry, resolved.from) : undefined;
|
|
154
|
+
const target = findFormat(ctx.registry, resolved.to);
|
|
155
|
+
if (!target) {
|
|
156
|
+
const report = buildBlockedReport(
|
|
157
|
+
ctx,
|
|
158
|
+
"convert",
|
|
159
|
+
inputPath,
|
|
160
|
+
resolved,
|
|
161
|
+
`unknown target format ${resolved.to}`,
|
|
162
|
+
ExitCode.InputUnsupported,
|
|
163
|
+
shared,
|
|
164
|
+
);
|
|
165
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
166
|
+
return ExitCode.InputUnsupported;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const routeReady = hasPipeline(ctx.bindings, resolved.from, resolved.to);
|
|
170
|
+
|
|
171
|
+
if (resolved.from === "doc") {
|
|
172
|
+
const report = buildBlockedReport(
|
|
173
|
+
ctx,
|
|
174
|
+
"convert",
|
|
175
|
+
inputPath,
|
|
176
|
+
resolved,
|
|
177
|
+
"legacy .doc import requires OLE reader support; convert to .docx first",
|
|
178
|
+
ExitCode.InputUnsupported,
|
|
179
|
+
shared,
|
|
180
|
+
);
|
|
181
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
182
|
+
return ExitCode.InputUnsupported;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!routeReady) {
|
|
186
|
+
if (source && !isOperationReady(source, "read")) {
|
|
187
|
+
const report = buildBlockedReport(
|
|
188
|
+
ctx,
|
|
189
|
+
"convert",
|
|
190
|
+
inputPath,
|
|
191
|
+
resolved,
|
|
192
|
+
`source format ${source.format} is not ready for read`,
|
|
193
|
+
ExitCode.InputUnsupported,
|
|
194
|
+
shared,
|
|
195
|
+
);
|
|
196
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
197
|
+
return ExitCode.InputUnsupported;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!isOperationReady(target, "write")) {
|
|
201
|
+
const report = buildBlockedReport(
|
|
202
|
+
ctx,
|
|
203
|
+
"convert",
|
|
204
|
+
inputPath,
|
|
205
|
+
resolved,
|
|
206
|
+
`target format ${target.format} is not ready for write`,
|
|
207
|
+
ExitCode.InputUnsupported,
|
|
208
|
+
shared,
|
|
209
|
+
);
|
|
210
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
211
|
+
return ExitCode.InputUnsupported;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (dryRun) {
|
|
216
|
+
const report = createReport({
|
|
217
|
+
operation: "convert",
|
|
218
|
+
toolVersion: ctx.toolVersion,
|
|
219
|
+
status: "success",
|
|
220
|
+
inputs: [{ path: inputPath, format: resolved.from }],
|
|
221
|
+
detection: { format: resolved.from, confidence: "hint", hints: resolved.hints },
|
|
222
|
+
pipeline: {
|
|
223
|
+
reader: source?.format ?? resolved.from,
|
|
224
|
+
writer: target.format,
|
|
225
|
+
stages: ["detect", "read", "ir", "write", "publish"],
|
|
226
|
+
},
|
|
227
|
+
outputs: outputPath ? [{ path: outputPath, published: false }] : [],
|
|
228
|
+
budgets: shared.budgets,
|
|
229
|
+
determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
|
|
230
|
+
statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.Success },
|
|
231
|
+
});
|
|
232
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
233
|
+
return ExitCode.Success;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (routeReady && ctx.bindings) {
|
|
237
|
+
try {
|
|
238
|
+
const result = await runConversionPipeline({
|
|
239
|
+
ctx,
|
|
240
|
+
bindings: ctx.bindings,
|
|
241
|
+
inputPath,
|
|
242
|
+
outputPath,
|
|
243
|
+
resolved,
|
|
244
|
+
shared,
|
|
245
|
+
toolVersion: ctx.toolVersion,
|
|
246
|
+
});
|
|
247
|
+
return emitPipelineResult(result, shared);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
250
|
+
const report = buildBlockedReport(
|
|
251
|
+
ctx,
|
|
252
|
+
"convert",
|
|
253
|
+
inputPath,
|
|
254
|
+
resolved,
|
|
255
|
+
message,
|
|
256
|
+
ExitCode.IrOrWriterFailure,
|
|
257
|
+
shared,
|
|
258
|
+
);
|
|
259
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
260
|
+
return ExitCode.IrOrWriterFailure;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const report = createReport({
|
|
265
|
+
operation: "convert",
|
|
266
|
+
toolVersion: ctx.toolVersion,
|
|
267
|
+
status: "blocked",
|
|
268
|
+
inputs: [{ path: inputPath, format: resolved.from }],
|
|
269
|
+
detection: { format: resolved.from, confidence: "hint", hints: resolved.hints },
|
|
270
|
+
pipeline: {
|
|
271
|
+
reader: source?.format ?? resolved.from,
|
|
272
|
+
writer: target.format,
|
|
273
|
+
stages: ["detect", "read", "ir", "write", "publish"],
|
|
274
|
+
blocked_reason: "conversion pipeline is not wired yet",
|
|
275
|
+
},
|
|
276
|
+
diagnostics: [
|
|
277
|
+
{
|
|
278
|
+
owner: "panduck",
|
|
279
|
+
severity: "error",
|
|
280
|
+
message: "conversion pipeline is not wired yet",
|
|
281
|
+
code: "panduck.convert.not_implemented",
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
outputs: outputPath ? [{ path: outputPath, published: false }] : [],
|
|
285
|
+
budgets: shared.budgets,
|
|
286
|
+
determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
|
|
287
|
+
statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.IrOrWriterFailure },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
emitReport(report, shared.diagnostics, shared.json);
|
|
291
|
+
return ExitCode.IrOrWriterFailure;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error instanceof CliOptionError) {
|
|
294
|
+
console.error(error.message);
|
|
295
|
+
return error.exitCode;
|
|
296
|
+
}
|
|
297
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
298
|
+
return ExitCode.InvalidArgs;
|
|
299
|
+
}
|
|
300
|
+
}
|