@notedge/panduck 0.0.0 → 0.0.1
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 +29 -34
- package/readme.md +5 -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 +2 -2
- package/src/wasm/load.ts +23 -23
- package/src/wasm/panduck_wasm.d.ts +6 -6
package/src/cli/paths.ts
CHANGED
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
import { extname, resolve } from "node:path";
|
|
2
|
-
|
|
3
|
-
const EXTENSION_HINTS: Record<string, string[]> = {
|
|
4
|
-
".md": ["markdown"],
|
|
5
|
-
".markdown": ["markdown"],
|
|
6
|
-
".
|
|
7
|
-
".
|
|
8
|
-
".
|
|
9
|
-
".
|
|
10
|
-
".
|
|
11
|
-
".
|
|
12
|
-
".
|
|
13
|
-
".
|
|
14
|
-
".
|
|
15
|
-
".
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
import { extname, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
const EXTENSION_HINTS: Record<string, string[]> = {
|
|
4
|
+
".md": ["markdown"],
|
|
5
|
+
".markdown": ["markdown"],
|
|
6
|
+
".note": ["notedown"],
|
|
7
|
+
".notedown": ["notedown"],
|
|
8
|
+
".org": ["org"],
|
|
9
|
+
".rst": ["rst"],
|
|
10
|
+
".tex": ["tex"],
|
|
11
|
+
".html": ["html"],
|
|
12
|
+
".htm": ["html"],
|
|
13
|
+
".docx": ["docx"],
|
|
14
|
+
".epub": ["epub"],
|
|
15
|
+
".pdf": ["pdf"],
|
|
16
|
+
".doc": ["doc"],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function isStdinPath(path: string | undefined): boolean {
|
|
20
|
+
return path === "-";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isStdoutPath(path: string | undefined): boolean {
|
|
24
|
+
return path === "-";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function normalizeFormatId(value: string): string {
|
|
28
|
+
const lowered = value.trim().toLowerCase();
|
|
29
|
+
if (lowered === "ebook") {
|
|
30
|
+
return "epub";
|
|
31
|
+
}
|
|
32
|
+
return lowered;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function extensionHints(path: string): string[] {
|
|
36
|
+
const ext = extname(path).toLowerCase();
|
|
37
|
+
return EXTENSION_HINTS[ext] ? [...EXTENSION_HINTS[ext]] : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function inferTargetFromPath(path: string): string | undefined {
|
|
41
|
+
const hints = extensionHints(path);
|
|
42
|
+
return hints.length === 1 ? hints[0] : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveFilesystemPath(path: string, cwd = process.cwd()): string {
|
|
46
|
+
if (isStdinPath(path) || isStdoutPath(path)) {
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
return resolve(cwd, path);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function pathsOverlap(inputPath: string, outputPath: string, cwd = process.cwd()): boolean {
|
|
53
|
+
if (isStdinPath(inputPath) || isStdoutPath(outputPath)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return resolveFilesystemPath(inputPath, cwd) === resolveFilesystemPath(outputPath, cwd);
|
|
57
|
+
}
|
package/src/cli/pipeline.ts
CHANGED
|
@@ -1,225 +1,225 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rename, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { basename, dirname, join } from "node:path";
|
|
4
|
-
import { randomBytes } from "node:crypto";
|
|
5
|
-
|
|
6
|
-
import type { PanduckBindings } from "../types.js";
|
|
7
|
-
import { ExitCode } from "./exit-codes.js";
|
|
8
|
-
import type { SharedCliOptions } from "./options.js";
|
|
9
|
-
import { createReport, type PanduckReport } from "./report.js";
|
|
10
|
-
import { emitReport } from "./diagnostics.js";
|
|
11
|
-
import type { CliContext } from "./context.js";
|
|
12
|
-
import type { ResolvedFormats } from "./context.js";
|
|
13
|
-
import { isStdoutPath } from "./paths.js";
|
|
14
|
-
|
|
15
|
-
export type PipelineResult = {
|
|
16
|
-
exitCode: number;
|
|
17
|
-
report: PanduckReport;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function hasPipeline(
|
|
21
|
-
bindings: PanduckBindings | null,
|
|
22
|
-
from: string | undefined,
|
|
23
|
-
to: string,
|
|
24
|
-
): boolean {
|
|
25
|
-
if (!bindings?.supportsConversion || !from) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
return bindings.supportsConversion(from, to);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function inferCheckTarget(bindings: PanduckBindings, from: string): string | undefined {
|
|
32
|
-
const routes = bindings.supportedConversions?.() ?? [];
|
|
33
|
-
const targets = routes
|
|
34
|
-
.filter((route) => route.startsWith(`${from}:`))
|
|
35
|
-
.map((route) => route.slice(from.length + 1));
|
|
36
|
-
if (targets.length === 1) {
|
|
37
|
-
return targets[0];
|
|
38
|
-
}
|
|
39
|
-
if (targets.includes("markdown")) {
|
|
40
|
-
return "markdown";
|
|
41
|
-
}
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function runCheckPipeline(input: {
|
|
46
|
-
ctx: CliContext;
|
|
47
|
-
bindings: PanduckBindings;
|
|
48
|
-
inputPath: string;
|
|
49
|
-
resolved: ResolvedFormats;
|
|
50
|
-
shared: SharedCliOptions;
|
|
51
|
-
}): Promise<PipelineResult> {
|
|
52
|
-
const { bindings, inputPath, resolved, shared, ctx } = input;
|
|
53
|
-
const from = resolved.from;
|
|
54
|
-
const to = resolved.to;
|
|
55
|
-
if (!from || !to) {
|
|
56
|
-
throw new Error("check requires resolved from/to formats");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const workdir = await mkdtemp(join(tmpdir(), "panduck-check-"));
|
|
60
|
-
const extension = to === "markdown" ? ".md" : `.${to}`;
|
|
61
|
-
const outputPath = join(workdir, `check${extension}`);
|
|
62
|
-
|
|
63
|
-
const result = await runConversionPipeline({
|
|
64
|
-
ctx,
|
|
65
|
-
bindings,
|
|
66
|
-
inputPath,
|
|
67
|
-
outputPath,
|
|
68
|
-
resolved: { ...resolved, to },
|
|
69
|
-
shared,
|
|
70
|
-
toolVersion: ctx.toolVersion,
|
|
71
|
-
operation: "check",
|
|
72
|
-
suppressStdout: true,
|
|
73
|
-
publishOutput: false,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
exitCode: result.exitCode,
|
|
78
|
-
report: {
|
|
79
|
-
...result.report,
|
|
80
|
-
operation: "check",
|
|
81
|
-
outputs: [{ path: outputPath, published: false }],
|
|
82
|
-
pipeline: {
|
|
83
|
-
...result.report.pipeline,
|
|
84
|
-
stages: ["read", "ir", "write", "validate"],
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function runConversionPipeline(input: {
|
|
91
|
-
ctx: CliContext;
|
|
92
|
-
bindings: PanduckBindings;
|
|
93
|
-
inputPath: string;
|
|
94
|
-
outputPath: string | undefined;
|
|
95
|
-
resolved: ResolvedFormats;
|
|
96
|
-
shared: SharedCliOptions;
|
|
97
|
-
toolVersion: string;
|
|
98
|
-
operation?: "convert" | "check";
|
|
99
|
-
suppressStdout?: boolean;
|
|
100
|
-
publishOutput?: boolean;
|
|
101
|
-
}): Promise<PipelineResult> {
|
|
102
|
-
const {
|
|
103
|
-
bindings,
|
|
104
|
-
inputPath,
|
|
105
|
-
outputPath,
|
|
106
|
-
resolved,
|
|
107
|
-
shared,
|
|
108
|
-
ctx,
|
|
109
|
-
operation = "convert",
|
|
110
|
-
suppressStdout = false,
|
|
111
|
-
publishOutput = true,
|
|
112
|
-
} = input;
|
|
113
|
-
const from = resolved.from;
|
|
114
|
-
const to = resolved.to;
|
|
115
|
-
if (!from || !to) {
|
|
116
|
-
throw new Error("conversion requires resolved from/to formats");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (!bindings.convertDocument) {
|
|
120
|
-
return blocked(ctx, operation, inputPath, resolved, shared, "native convertDocument binding is missing");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const response = bindings.convertDocument(from, to, inputPath);
|
|
124
|
-
const markdown = response.markdown ?? "";
|
|
125
|
-
const binary = response.binary;
|
|
126
|
-
const parsedReport = safeParseReport(response.reportJson);
|
|
127
|
-
|
|
128
|
-
if (publishOutput && outputPath && !isStdoutPath(outputPath)) {
|
|
129
|
-
if (binary && binary.length > 0) {
|
|
130
|
-
await publishBinary(outputPath, binary);
|
|
131
|
-
} else {
|
|
132
|
-
await publishText(outputPath, markdown);
|
|
133
|
-
}
|
|
134
|
-
} else if (!suppressStdout && (isStdoutPath(outputPath) || !outputPath)) {
|
|
135
|
-
if (binary && binary.length > 0) {
|
|
136
|
-
process.stdout.write(binary);
|
|
137
|
-
} else {
|
|
138
|
-
process.stdout.write(markdown);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const lossCount = Array.isArray(parsedReport?.losses) ? parsedReport.losses.length : 0;
|
|
143
|
-
const status = lossCount > 0 ? "success_with_loss" : "success";
|
|
144
|
-
let exitCode: number = ExitCode.Success;
|
|
145
|
-
if (shared.strict || shared.loss === "deny") {
|
|
146
|
-
if (lossCount > 0 || status === "success_with_loss") {
|
|
147
|
-
exitCode = ExitCode.LossPolicyViolation;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const report = createReport({
|
|
152
|
-
operation,
|
|
153
|
-
toolVersion: ctx.toolVersion,
|
|
154
|
-
status,
|
|
155
|
-
inputs: [{ path: inputPath, format: from }],
|
|
156
|
-
detection: { format: from, outer: "zip", inner: "opc", confidence: "verified", hints: resolved.hints },
|
|
157
|
-
pipeline: {
|
|
158
|
-
reader: from,
|
|
159
|
-
writer: to,
|
|
160
|
-
stages: operation === "check" ? ["read", "ir", "write", "validate"] : ["read", "ir", "write", "publish"],
|
|
161
|
-
},
|
|
162
|
-
losses: parsedReport?.losses as PanduckReport["losses"],
|
|
163
|
-
outputs: outputPath
|
|
164
|
-
? [{ path: outputPath, published: publishOutput && !isStdoutPath(outputPath) }]
|
|
165
|
-
: [{ path: "-", published: !suppressStdout }],
|
|
166
|
-
budgets: shared.budgets,
|
|
167
|
-
determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
|
|
168
|
-
statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: exitCode },
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
return { exitCode, report };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function publishBinary(path: string, content: Uint8Array): Promise<void> {
|
|
175
|
-
const dir = dirname(path);
|
|
176
|
-
await mkdir(dir, { recursive: true });
|
|
177
|
-
const temp = join(dir, `.${basename(path)}.${randomBytes(4).toString("hex")}.tmp`);
|
|
178
|
-
await writeFile(temp, content);
|
|
179
|
-
await rename(temp, path);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function publishText(path: string, content: string): Promise<void> {
|
|
183
|
-
const dir = dirname(path);
|
|
184
|
-
await mkdir(dir, { recursive: true });
|
|
185
|
-
const temp = join(dir, `.${basename(path)}.${randomBytes(4).toString("hex")}.tmp`);
|
|
186
|
-
await writeFile(temp, content, "utf8");
|
|
187
|
-
await rename(temp, path);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function safeParseReport(raw: string): Record<string, unknown> | null {
|
|
191
|
-
try {
|
|
192
|
-
return JSON.parse(raw) as Record<string, unknown>;
|
|
193
|
-
} catch {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function blocked(
|
|
199
|
-
ctx: CliContext,
|
|
200
|
-
operation: "convert" | "check",
|
|
201
|
-
inputPath: string,
|
|
202
|
-
resolved: ResolvedFormats,
|
|
203
|
-
shared: SharedCliOptions,
|
|
204
|
-
reason: string,
|
|
205
|
-
): PipelineResult {
|
|
206
|
-
const report = createReport({
|
|
207
|
-
operation,
|
|
208
|
-
toolVersion: ctx.toolVersion,
|
|
209
|
-
status: "blocked",
|
|
210
|
-
inputs: [{ path: inputPath, format: resolved.from }],
|
|
211
|
-
pipeline: { blocked_reason: reason },
|
|
212
|
-
diagnostics: [{ owner: "panduck", severity: "error", message: reason }],
|
|
213
|
-
determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
|
|
214
|
-
statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.IrOrWriterFailure },
|
|
215
|
-
});
|
|
216
|
-
return { exitCode: ExitCode.IrOrWriterFailure, report };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export async function emitPipelineResult(
|
|
220
|
-
result: PipelineResult,
|
|
221
|
-
shared: SharedCliOptions,
|
|
222
|
-
): Promise<number> {
|
|
223
|
-
emitReport(result.report, shared.diagnostics, shared.json);
|
|
224
|
-
return result.exitCode;
|
|
225
|
-
}
|
|
1
|
+
import { mkdir, mkdtemp, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
import type { PanduckBindings } from "../types.js";
|
|
7
|
+
import { ExitCode } from "./exit-codes.js";
|
|
8
|
+
import type { SharedCliOptions } from "./options.js";
|
|
9
|
+
import { createReport, type PanduckReport } from "./report.js";
|
|
10
|
+
import { emitReport } from "./diagnostics.js";
|
|
11
|
+
import type { CliContext } from "./context.js";
|
|
12
|
+
import type { ResolvedFormats } from "./context.js";
|
|
13
|
+
import { isStdoutPath } from "./paths.js";
|
|
14
|
+
|
|
15
|
+
export type PipelineResult = {
|
|
16
|
+
exitCode: number;
|
|
17
|
+
report: PanduckReport;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function hasPipeline(
|
|
21
|
+
bindings: PanduckBindings | null,
|
|
22
|
+
from: string | undefined,
|
|
23
|
+
to: string,
|
|
24
|
+
): boolean {
|
|
25
|
+
if (!bindings?.supportsConversion || !from) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return bindings.supportsConversion(from, to);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function inferCheckTarget(bindings: PanduckBindings, from: string): string | undefined {
|
|
32
|
+
const routes = bindings.supportedConversions?.() ?? [];
|
|
33
|
+
const targets = routes
|
|
34
|
+
.filter((route) => route.startsWith(`${from}:`))
|
|
35
|
+
.map((route) => route.slice(from.length + 1));
|
|
36
|
+
if (targets.length === 1) {
|
|
37
|
+
return targets[0];
|
|
38
|
+
}
|
|
39
|
+
if (targets.includes("markdown")) {
|
|
40
|
+
return "markdown";
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runCheckPipeline(input: {
|
|
46
|
+
ctx: CliContext;
|
|
47
|
+
bindings: PanduckBindings;
|
|
48
|
+
inputPath: string;
|
|
49
|
+
resolved: ResolvedFormats;
|
|
50
|
+
shared: SharedCliOptions;
|
|
51
|
+
}): Promise<PipelineResult> {
|
|
52
|
+
const { bindings, inputPath, resolved, shared, ctx } = input;
|
|
53
|
+
const from = resolved.from;
|
|
54
|
+
const to = resolved.to;
|
|
55
|
+
if (!from || !to) {
|
|
56
|
+
throw new Error("check requires resolved from/to formats");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const workdir = await mkdtemp(join(tmpdir(), "panduck-check-"));
|
|
60
|
+
const extension = to === "markdown" ? ".md" : `.${to}`;
|
|
61
|
+
const outputPath = join(workdir, `check${extension}`);
|
|
62
|
+
|
|
63
|
+
const result = await runConversionPipeline({
|
|
64
|
+
ctx,
|
|
65
|
+
bindings,
|
|
66
|
+
inputPath,
|
|
67
|
+
outputPath,
|
|
68
|
+
resolved: { ...resolved, to },
|
|
69
|
+
shared,
|
|
70
|
+
toolVersion: ctx.toolVersion,
|
|
71
|
+
operation: "check",
|
|
72
|
+
suppressStdout: true,
|
|
73
|
+
publishOutput: false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
exitCode: result.exitCode,
|
|
78
|
+
report: {
|
|
79
|
+
...result.report,
|
|
80
|
+
operation: "check",
|
|
81
|
+
outputs: [{ path: outputPath, published: false }],
|
|
82
|
+
pipeline: {
|
|
83
|
+
...result.report.pipeline,
|
|
84
|
+
stages: ["read", "ir", "write", "validate"],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function runConversionPipeline(input: {
|
|
91
|
+
ctx: CliContext;
|
|
92
|
+
bindings: PanduckBindings;
|
|
93
|
+
inputPath: string;
|
|
94
|
+
outputPath: string | undefined;
|
|
95
|
+
resolved: ResolvedFormats;
|
|
96
|
+
shared: SharedCliOptions;
|
|
97
|
+
toolVersion: string;
|
|
98
|
+
operation?: "convert" | "check";
|
|
99
|
+
suppressStdout?: boolean;
|
|
100
|
+
publishOutput?: boolean;
|
|
101
|
+
}): Promise<PipelineResult> {
|
|
102
|
+
const {
|
|
103
|
+
bindings,
|
|
104
|
+
inputPath,
|
|
105
|
+
outputPath,
|
|
106
|
+
resolved,
|
|
107
|
+
shared,
|
|
108
|
+
ctx,
|
|
109
|
+
operation = "convert",
|
|
110
|
+
suppressStdout = false,
|
|
111
|
+
publishOutput = true,
|
|
112
|
+
} = input;
|
|
113
|
+
const from = resolved.from;
|
|
114
|
+
const to = resolved.to;
|
|
115
|
+
if (!from || !to) {
|
|
116
|
+
throw new Error("conversion requires resolved from/to formats");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!bindings.convertDocument) {
|
|
120
|
+
return blocked(ctx, operation, inputPath, resolved, shared, "native convertDocument binding is missing");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const response = bindings.convertDocument(from, to, inputPath);
|
|
124
|
+
const markdown = response.markdown ?? "";
|
|
125
|
+
const binary = response.binary;
|
|
126
|
+
const parsedReport = safeParseReport(response.reportJson);
|
|
127
|
+
|
|
128
|
+
if (publishOutput && outputPath && !isStdoutPath(outputPath)) {
|
|
129
|
+
if (binary && binary.length > 0) {
|
|
130
|
+
await publishBinary(outputPath, binary);
|
|
131
|
+
} else {
|
|
132
|
+
await publishText(outputPath, markdown);
|
|
133
|
+
}
|
|
134
|
+
} else if (!suppressStdout && (isStdoutPath(outputPath) || !outputPath)) {
|
|
135
|
+
if (binary && binary.length > 0) {
|
|
136
|
+
process.stdout.write(binary);
|
|
137
|
+
} else {
|
|
138
|
+
process.stdout.write(markdown);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const lossCount = Array.isArray(parsedReport?.losses) ? parsedReport.losses.length : 0;
|
|
143
|
+
const status = lossCount > 0 ? "success_with_loss" : "success";
|
|
144
|
+
let exitCode: number = ExitCode.Success;
|
|
145
|
+
if (shared.strict || shared.loss === "deny") {
|
|
146
|
+
if (lossCount > 0 || status === "success_with_loss") {
|
|
147
|
+
exitCode = ExitCode.LossPolicyViolation;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const report = createReport({
|
|
152
|
+
operation,
|
|
153
|
+
toolVersion: ctx.toolVersion,
|
|
154
|
+
status,
|
|
155
|
+
inputs: [{ path: inputPath, format: from }],
|
|
156
|
+
detection: { format: from, outer: "zip", inner: "opc", confidence: "verified", hints: resolved.hints },
|
|
157
|
+
pipeline: {
|
|
158
|
+
reader: from,
|
|
159
|
+
writer: to,
|
|
160
|
+
stages: operation === "check" ? ["read", "ir", "write", "validate"] : ["read", "ir", "write", "publish"],
|
|
161
|
+
},
|
|
162
|
+
losses: parsedReport?.losses as PanduckReport["losses"],
|
|
163
|
+
outputs: outputPath
|
|
164
|
+
? [{ path: outputPath, published: publishOutput && !isStdoutPath(outputPath) }]
|
|
165
|
+
: [{ path: "-", published: !suppressStdout }],
|
|
166
|
+
budgets: shared.budgets,
|
|
167
|
+
determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
|
|
168
|
+
statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: exitCode },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return { exitCode, report };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function publishBinary(path: string, content: Uint8Array): Promise<void> {
|
|
175
|
+
const dir = dirname(path);
|
|
176
|
+
await mkdir(dir, { recursive: true });
|
|
177
|
+
const temp = join(dir, `.${basename(path)}.${randomBytes(4).toString("hex")}.tmp`);
|
|
178
|
+
await writeFile(temp, content);
|
|
179
|
+
await rename(temp, path);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function publishText(path: string, content: string): Promise<void> {
|
|
183
|
+
const dir = dirname(path);
|
|
184
|
+
await mkdir(dir, { recursive: true });
|
|
185
|
+
const temp = join(dir, `.${basename(path)}.${randomBytes(4).toString("hex")}.tmp`);
|
|
186
|
+
await writeFile(temp, content, "utf8");
|
|
187
|
+
await rename(temp, path);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function safeParseReport(raw: string): Record<string, unknown> | null {
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function blocked(
|
|
199
|
+
ctx: CliContext,
|
|
200
|
+
operation: "convert" | "check",
|
|
201
|
+
inputPath: string,
|
|
202
|
+
resolved: ResolvedFormats,
|
|
203
|
+
shared: SharedCliOptions,
|
|
204
|
+
reason: string,
|
|
205
|
+
): PipelineResult {
|
|
206
|
+
const report = createReport({
|
|
207
|
+
operation,
|
|
208
|
+
toolVersion: ctx.toolVersion,
|
|
209
|
+
status: "blocked",
|
|
210
|
+
inputs: [{ path: inputPath, format: resolved.from }],
|
|
211
|
+
pipeline: { blocked_reason: reason },
|
|
212
|
+
diagnostics: [{ owner: "panduck", severity: "error", message: reason }],
|
|
213
|
+
determinism: { profile: shared.profile ?? "default", config: shared.config ?? null },
|
|
214
|
+
statusPolicy: { loss: shared.loss, strict: shared.strict, exit_code: ExitCode.IrOrWriterFailure },
|
|
215
|
+
});
|
|
216
|
+
return { exitCode: ExitCode.IrOrWriterFailure, report };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function emitPipelineResult(
|
|
220
|
+
result: PipelineResult,
|
|
221
|
+
shared: SharedCliOptions,
|
|
222
|
+
): Promise<number> {
|
|
223
|
+
emitReport(result.report, shared.diagnostics, shared.json);
|
|
224
|
+
return result.exitCode;
|
|
225
|
+
}
|