@rk0429/morphe-cli 0.1.0-dev.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/dist/index.js +1725 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1725 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import {
|
|
5
|
+
AuthError,
|
|
6
|
+
AssetError,
|
|
7
|
+
FontValidationError,
|
|
8
|
+
GoogleSlidesApiError,
|
|
9
|
+
LayoutIntegrityError,
|
|
10
|
+
MORPHE_CORE_VERSION,
|
|
11
|
+
MarkdownAuthoringError,
|
|
12
|
+
ReauthRequiredError,
|
|
13
|
+
MORPHE_MCP_PATH,
|
|
14
|
+
PptxExtractionError,
|
|
15
|
+
ThemeResolutionError,
|
|
16
|
+
createAuthService,
|
|
17
|
+
createAssetRepository,
|
|
18
|
+
createComposePresentationFn,
|
|
19
|
+
createMermaidCliRenderer,
|
|
20
|
+
createNodeHttpRemoteFetcher,
|
|
21
|
+
createRenderPptxFn,
|
|
22
|
+
createResvgAssetBinaryProcessor,
|
|
23
|
+
createShikiCodeHighlighter,
|
|
24
|
+
extractGslides as extractGslidesFromCore,
|
|
25
|
+
extractPptx as extractPptxFromCore,
|
|
26
|
+
formatStyleYamlDiagnostic,
|
|
27
|
+
formatWorkspaceBaseline,
|
|
28
|
+
generateGslides as generateGslidesFromCore,
|
|
29
|
+
generatePptx as generatePptxFromCore,
|
|
30
|
+
listThemes,
|
|
31
|
+
packPptxEntries,
|
|
32
|
+
serveMcpOverStdio,
|
|
33
|
+
startMcpHttpServer,
|
|
34
|
+
validateYaml as validateYamlFromCore
|
|
35
|
+
} from "@rk0429/morphe-core";
|
|
36
|
+
import { lstat, mkdir, opendir, readFile, writeFile } from "node:fs/promises";
|
|
37
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
38
|
+
import { pathToFileURL } from "node:url";
|
|
39
|
+
var CLI_VERSION = MORPHE_CORE_VERSION;
|
|
40
|
+
var CLI_EXIT_CODES = {
|
|
41
|
+
success: 0,
|
|
42
|
+
input: 1,
|
|
43
|
+
layout: 2,
|
|
44
|
+
auth: 3,
|
|
45
|
+
api: 4,
|
|
46
|
+
internal: 70
|
|
47
|
+
};
|
|
48
|
+
var MARKDOWN_INPUT_MAX_BYTES = 10 * 1024 * 1024;
|
|
49
|
+
var STYLE_YAML_INPUT_MAX_BYTES = 2 * 1024 * 1024;
|
|
50
|
+
var CliUsageError = class extends Error {
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "CliUsageError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var CliParseDiagnosticError = class extends CliUsageError {
|
|
57
|
+
diagnostic;
|
|
58
|
+
constructor(diagnostic) {
|
|
59
|
+
super(diagnostic.actual ?? "Input parse error.");
|
|
60
|
+
this.name = "CliParseDiagnosticError";
|
|
61
|
+
this.diagnostic = diagnostic;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function renderCliOutput() {
|
|
65
|
+
return formatWorkspaceBaseline();
|
|
66
|
+
}
|
|
67
|
+
var SERVICE_ACCOUNT_AUTH_GUIDANCE = "Use --service-account <path> or set GOOGLE_APPLICATION_CREDENTIALS to a service account JSON key, then retry. If OAuth was completed ahead of time, pass --token-path <path> or set MORPHE_TOKEN_PATH to the prepared token cache.";
|
|
68
|
+
function writeLine(io, line) {
|
|
69
|
+
io.write(`${line}
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
function parseGlobalFlags(argv) {
|
|
73
|
+
const remaining = [];
|
|
74
|
+
const flags = {
|
|
75
|
+
verbose: false,
|
|
76
|
+
quiet: false,
|
|
77
|
+
nonInteractive: false
|
|
78
|
+
};
|
|
79
|
+
for (const arg of argv) {
|
|
80
|
+
if (arg === "--verbose") {
|
|
81
|
+
flags.verbose = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg === "--quiet") {
|
|
85
|
+
flags.quiet = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg === "--non-interactive") {
|
|
89
|
+
flags.nonInteractive = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
remaining.push(arg);
|
|
93
|
+
}
|
|
94
|
+
if (flags.verbose && flags.quiet) {
|
|
95
|
+
throw new CliUsageError("`--verbose` and `--quiet` cannot be used together.");
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
argv: remaining,
|
|
99
|
+
flags
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function resolveNonInteractiveMode(dependencies, globalFlags) {
|
|
103
|
+
return globalFlags.nonInteractive || !isTtyInput(dependencies.stdin);
|
|
104
|
+
}
|
|
105
|
+
function isTtyInput(stdin) {
|
|
106
|
+
return stdin.isTTY === true;
|
|
107
|
+
}
|
|
108
|
+
function hasServiceAccountCredential(flags, env) {
|
|
109
|
+
return Boolean(flags.serviceAccountPath || env.GOOGLE_APPLICATION_CREDENTIALS);
|
|
110
|
+
}
|
|
111
|
+
function createNonInteractiveAuthError(action) {
|
|
112
|
+
return new AuthError(
|
|
113
|
+
`${action} cannot start the OAuth browser flow in non-interactive mode. ${SERVICE_ACCOUNT_AUTH_GUIDANCE}`,
|
|
114
|
+
"non_interactive_auth_required"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
function formatNonInteractiveAuthFailure(error) {
|
|
118
|
+
return `${toErrorMessage(error)}
|
|
119
|
+
${SERVICE_ACCOUNT_AUTH_GUIDANCE}`;
|
|
120
|
+
}
|
|
121
|
+
function parseLogFormat(value, source) {
|
|
122
|
+
if (value === "text" || value === "json") {
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
throw new CliUsageError(`${source} must be \`text\` or \`json\`.`);
|
|
126
|
+
}
|
|
127
|
+
var CONFIG_FILE_NAMES = [".morpherc", "morphe.config.yaml"];
|
|
128
|
+
var FONT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".otf", ".ttf", ".woff", ".woff2"]);
|
|
129
|
+
var RASTERIZATION_FONT_DIR_LIMITS = Object.freeze({
|
|
130
|
+
maxDirectories: 64,
|
|
131
|
+
maxFonts: 256,
|
|
132
|
+
maxSingleFontBytes: 32 * 1024 * 1024,
|
|
133
|
+
maxTotalFontBytes: 128 * 1024 * 1024
|
|
134
|
+
});
|
|
135
|
+
async function loadCliConfig(baseDir = process.cwd()) {
|
|
136
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
137
|
+
const configPath = resolve(baseDir, fileName);
|
|
138
|
+
try {
|
|
139
|
+
const content = await readFile(configPath, "utf8");
|
|
140
|
+
return parseCliConfig(content, configPath);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (isErrnoError(error) && error.code === "ENOENT") {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
path: null,
|
|
150
|
+
fontDirs: []
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function parseCliConfig(content, configPath) {
|
|
154
|
+
const rawConfig = parseConfigDocument(content, configPath);
|
|
155
|
+
const theme = optionalConfigString(rawConfig, ["theme"], configPath);
|
|
156
|
+
const rawLogFormat = optionalConfigString(rawConfig, ["logFormat", "log_format"], configPath) ?? void 0;
|
|
157
|
+
const rawFontDirs = rawConfig.fontDirs ?? rawConfig.font_dirs;
|
|
158
|
+
const fontDirs = normalizeConfigStringArray(rawFontDirs, "fontDirs", configPath).map(
|
|
159
|
+
(fontDir) => resolve(dirname(configPath), fontDir)
|
|
160
|
+
);
|
|
161
|
+
return {
|
|
162
|
+
path: configPath,
|
|
163
|
+
...theme ? { theme } : {},
|
|
164
|
+
...rawLogFormat ? { logFormat: parseLogFormat(rawLogFormat, `${configPath} logFormat`) } : {},
|
|
165
|
+
fontDirs
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function parseConfigDocument(content, configPath) {
|
|
169
|
+
const trimmed = content.trim();
|
|
170
|
+
if (trimmed.length === 0) {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
if (trimmed.startsWith("{")) {
|
|
174
|
+
let parsed;
|
|
175
|
+
try {
|
|
176
|
+
parsed = JSON.parse(trimmed);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw new CliUsageError(`${configPath} is not valid JSON: ${toErrorMessage(error)}`);
|
|
179
|
+
}
|
|
180
|
+
if (!isRecord(parsed)) {
|
|
181
|
+
throw new CliUsageError(`${configPath} must contain a JSON object.`);
|
|
182
|
+
}
|
|
183
|
+
return parsed;
|
|
184
|
+
}
|
|
185
|
+
return parseSimpleYamlConfig(content, configPath);
|
|
186
|
+
}
|
|
187
|
+
function parseSimpleYamlConfig(content, configPath) {
|
|
188
|
+
const result = {};
|
|
189
|
+
let listKey = null;
|
|
190
|
+
for (const [index, rawLine] of content.split(/\r?\n/u).entries()) {
|
|
191
|
+
const line = rawLine.replace(/\s+#.*$/u, "");
|
|
192
|
+
if (line.trim().length === 0) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (/^\s/u.test(line)) {
|
|
196
|
+
const item = line.trim().match(/^-\s+(.+)$/u);
|
|
197
|
+
if (!listKey || !item?.[1]) {
|
|
198
|
+
throw new CliUsageError(`${configPath}:L${index + 1} uses unsupported config YAML syntax.`);
|
|
199
|
+
}
|
|
200
|
+
const current = result[listKey];
|
|
201
|
+
if (!Array.isArray(current)) {
|
|
202
|
+
throw new CliUsageError(`${configPath}:L${index + 1} appends to a non-list config key.`);
|
|
203
|
+
}
|
|
204
|
+
current.push(parseYamlScalar(item[1]));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
listKey = null;
|
|
208
|
+
const entry = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):(?:\s*(.*))?$/u);
|
|
209
|
+
if (!entry?.[1]) {
|
|
210
|
+
throw new CliUsageError(`${configPath}:L${index + 1} uses unsupported config YAML syntax.`);
|
|
211
|
+
}
|
|
212
|
+
const key = entry[1];
|
|
213
|
+
const value = entry[2] ?? "";
|
|
214
|
+
if (value.length === 0) {
|
|
215
|
+
result[key] = [];
|
|
216
|
+
listKey = key;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
result[key] = value.trim().startsWith("[") ? parseInlineYamlList(value, configPath, index + 1) : parseYamlScalar(value);
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
function parseInlineYamlList(value, configPath, line) {
|
|
224
|
+
if (!value.endsWith("]")) {
|
|
225
|
+
throw new CliUsageError(`${configPath}:L${line} has an unterminated inline list.`);
|
|
226
|
+
}
|
|
227
|
+
const body = value.slice(1, -1).trim();
|
|
228
|
+
if (body.length === 0) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
return body.split(",").map((item) => parseYamlScalar(item));
|
|
232
|
+
}
|
|
233
|
+
function parseYamlScalar(value) {
|
|
234
|
+
const trimmed = value.trim();
|
|
235
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
236
|
+
return trimmed.slice(1, -1);
|
|
237
|
+
}
|
|
238
|
+
return trimmed;
|
|
239
|
+
}
|
|
240
|
+
function optionalConfigString(config, keys, configPath) {
|
|
241
|
+
for (const key of keys) {
|
|
242
|
+
const value = config[key];
|
|
243
|
+
if (value === void 0 || value === null) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (typeof value !== "string") {
|
|
247
|
+
throw new CliUsageError(`${configPath} ${key} must be a string.`);
|
|
248
|
+
}
|
|
249
|
+
const trimmed = value.trim();
|
|
250
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
251
|
+
}
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
function normalizeConfigStringArray(value, key, configPath) {
|
|
255
|
+
if (value === void 0 || value === null) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
if (typeof value === "string") {
|
|
259
|
+
const trimmed = value.trim();
|
|
260
|
+
return trimmed.length > 0 ? [trimmed] : [];
|
|
261
|
+
}
|
|
262
|
+
if (!Array.isArray(value)) {
|
|
263
|
+
throw new CliUsageError(`${configPath} ${key} must be a string or string list.`);
|
|
264
|
+
}
|
|
265
|
+
return value.map((item) => {
|
|
266
|
+
if (typeof item !== "string") {
|
|
267
|
+
throw new CliUsageError(`${configPath} ${key} entries must be strings.`);
|
|
268
|
+
}
|
|
269
|
+
return item.trim();
|
|
270
|
+
}).filter((item) => item.length > 0);
|
|
271
|
+
}
|
|
272
|
+
function isRecord(value) {
|
|
273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
274
|
+
}
|
|
275
|
+
function resolveLogOptions(dependencies, globalFlags, flagLogFormat, config = { path: null, fontDirs: [] }) {
|
|
276
|
+
const envLogFormat = dependencies.env.MORPHE_LOG_FORMAT?.trim();
|
|
277
|
+
const format = flagLogFormat ?? (envLogFormat ? parseLogFormat(envLogFormat, "MORPHE_LOG_FORMAT") : config.logFormat ?? "text");
|
|
278
|
+
return {
|
|
279
|
+
format,
|
|
280
|
+
minimumLevel: globalFlags.quiet ? "error" : globalFlags.verbose ? "debug" : "info"
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function shouldWriteLog(options, level) {
|
|
284
|
+
const levels = {
|
|
285
|
+
debug: 10,
|
|
286
|
+
info: 20,
|
|
287
|
+
warn: 30,
|
|
288
|
+
error: 40
|
|
289
|
+
};
|
|
290
|
+
return levels[level] >= levels[options.minimumLevel];
|
|
291
|
+
}
|
|
292
|
+
function writeLog(io, options, level, eventType, payload, text) {
|
|
293
|
+
if (!shouldWriteLog(options, level)) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (options.format === "json") {
|
|
297
|
+
writeLine(
|
|
298
|
+
io,
|
|
299
|
+
JSON.stringify({
|
|
300
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
301
|
+
level,
|
|
302
|
+
event_type: eventType,
|
|
303
|
+
payload
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
writeLine(io, text);
|
|
309
|
+
}
|
|
310
|
+
function resolveLogOptionsForError(dependencies, globalFlags, logOptions, args) {
|
|
311
|
+
if (logOptions) {
|
|
312
|
+
return logOptions;
|
|
313
|
+
}
|
|
314
|
+
return resolveLogOptions(dependencies, globalFlags, findRawLogFormat(args));
|
|
315
|
+
}
|
|
316
|
+
function findRawLogFormat(args) {
|
|
317
|
+
const logFormatIndex = args.indexOf("--log-format");
|
|
318
|
+
if (logFormatIndex === -1) {
|
|
319
|
+
return void 0;
|
|
320
|
+
}
|
|
321
|
+
const value = args[logFormatIndex + 1];
|
|
322
|
+
return value === "text" || value === "json" ? value : void 0;
|
|
323
|
+
}
|
|
324
|
+
function writeErrorDiagnostic(io, options, diagnostic, text) {
|
|
325
|
+
writeLog(io, options, "error", "diagnostic", diagnostic, text);
|
|
326
|
+
}
|
|
327
|
+
function createCliErrorDiagnostic(error, message, suggestion) {
|
|
328
|
+
if (isAuthError(error)) {
|
|
329
|
+
return {
|
|
330
|
+
error_type: "auth_error",
|
|
331
|
+
actual: message,
|
|
332
|
+
suggestion: SERVICE_ACCOUNT_AUTH_GUIDANCE
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const pptxExtractionError = findFirstErrorInChain(error, PptxExtractionError);
|
|
336
|
+
if (pptxExtractionError) {
|
|
337
|
+
return {
|
|
338
|
+
error_type: pptxExtractionError.errorType,
|
|
339
|
+
actual: message,
|
|
340
|
+
suggestion
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
error_type: isInputError(error) ? "parse_error" : "internal_error",
|
|
345
|
+
actual: message,
|
|
346
|
+
suggestion
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function parseOptionalEnvBoolean(value, source) {
|
|
350
|
+
if (value === void 0 || value.trim().length === 0) {
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
const normalized = value.trim().toLowerCase();
|
|
354
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
throw new CliUsageError(`${source} must be a boolean value.`);
|
|
361
|
+
}
|
|
362
|
+
function resolveRemoteFetchPolicy(flags, env) {
|
|
363
|
+
const policy = {};
|
|
364
|
+
const envAllowRemote = parseOptionalEnvBoolean(
|
|
365
|
+
env.MORPHE_ALLOW_REMOTE_IMAGES,
|
|
366
|
+
"MORPHE_ALLOW_REMOTE_IMAGES"
|
|
367
|
+
);
|
|
368
|
+
if (flags.allowRemoteImages) {
|
|
369
|
+
policy.allowRemote = true;
|
|
370
|
+
} else if (envAllowRemote !== void 0) {
|
|
371
|
+
policy.allowRemote = envAllowRemote;
|
|
372
|
+
}
|
|
373
|
+
if (flags.allowPrivateIp) {
|
|
374
|
+
policy.allowPrivateIp = true;
|
|
375
|
+
}
|
|
376
|
+
return Object.keys(policy).length > 0 ? policy : void 0;
|
|
377
|
+
}
|
|
378
|
+
async function resolveRasterizationFontSources(flags, config) {
|
|
379
|
+
const collectionState = {
|
|
380
|
+
directoryCount: 0,
|
|
381
|
+
fontCount: 0,
|
|
382
|
+
seenDirectories: /* @__PURE__ */ new Set(),
|
|
383
|
+
totalBytes: 0
|
|
384
|
+
};
|
|
385
|
+
return [
|
|
386
|
+
...await collectRasterizationFontSources(
|
|
387
|
+
flags.fontDirs,
|
|
388
|
+
"CliSpecifiedFont",
|
|
389
|
+
process.cwd(),
|
|
390
|
+
collectionState
|
|
391
|
+
),
|
|
392
|
+
...await collectRasterizationFontSources(
|
|
393
|
+
config.fontDirs,
|
|
394
|
+
"ProjectLocalFont",
|
|
395
|
+
process.cwd(),
|
|
396
|
+
collectionState
|
|
397
|
+
)
|
|
398
|
+
];
|
|
399
|
+
}
|
|
400
|
+
async function collectRasterizationFontSources(directories, source, baseDir, collectionState) {
|
|
401
|
+
const sources = [];
|
|
402
|
+
for (const directory of directories) {
|
|
403
|
+
const absoluteDirectory = resolve(baseDir, directory);
|
|
404
|
+
if (collectionState.seenDirectories.has(absoluteDirectory)) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
collectionState.seenDirectories.add(absoluteDirectory);
|
|
408
|
+
enforceFontDirectoryCountLimit(absoluteDirectory, collectionState);
|
|
409
|
+
await validateFontDirectory(absoluteDirectory);
|
|
410
|
+
let directoryHandle;
|
|
411
|
+
try {
|
|
412
|
+
directoryHandle = await opendir(absoluteDirectory);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (isErrnoError(error) && ["ENOENT", "ENOTDIR"].includes(error.code ?? "")) {
|
|
415
|
+
throw new CliUsageError(`Font directory does not exist: ${absoluteDirectory}`);
|
|
416
|
+
}
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
for await (const entry of directoryHandle) {
|
|
420
|
+
const extension = extname(entry.name).toLowerCase();
|
|
421
|
+
if (!FONT_FILE_EXTENSIONS.has(extension)) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const fontPath = resolve(absoluteDirectory, entry.name);
|
|
425
|
+
const stats = await validateFontDirectoryEntry(fontPath, entry);
|
|
426
|
+
enforceFontDirectoryLimits(fontPath, stats.size, collectionState);
|
|
427
|
+
sources.push({
|
|
428
|
+
family: basename(entry.name, extension).replaceAll(/[_-]+/gu, " ").trim(),
|
|
429
|
+
source,
|
|
430
|
+
resolvedPath: fontPath,
|
|
431
|
+
fallbackFor: null
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return sources.filter((fontSource) => fontSource.family.length > 0);
|
|
436
|
+
}
|
|
437
|
+
function enforceFontDirectoryCountLimit(directory, collectionState) {
|
|
438
|
+
const nextDirectoryCount = collectionState.directoryCount + 1;
|
|
439
|
+
if (nextDirectoryCount > RASTERIZATION_FONT_DIR_LIMITS.maxDirectories) {
|
|
440
|
+
throw createFontDirsParseError(
|
|
441
|
+
`fontDirs contain more than ${RASTERIZATION_FONT_DIR_LIMITS.maxDirectories} directories: ${directory}.`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
collectionState.directoryCount = nextDirectoryCount;
|
|
445
|
+
}
|
|
446
|
+
async function validateFontDirectory(directory) {
|
|
447
|
+
let stats;
|
|
448
|
+
try {
|
|
449
|
+
stats = await lstat(directory);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (isErrnoError(error) && ["ENOENT", "ENOTDIR"].includes(error.code ?? "")) {
|
|
452
|
+
throw new CliUsageError(`Font directory does not exist: ${directory}`);
|
|
453
|
+
}
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
457
|
+
throw createFontDirsParseError(`Font directory is not a regular directory: ${directory}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async function validateFontDirectoryEntry(fontPath, entry) {
|
|
461
|
+
let stats;
|
|
462
|
+
try {
|
|
463
|
+
stats = await lstat(fontPath);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
throw createFontDirsParseError(
|
|
466
|
+
`Font file could not be inspected before rasterization: ${fontPath}`,
|
|
467
|
+
toErrorMessage(error)
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (!entry.isFile() || stats.isSymbolicLink() || !stats.isFile()) {
|
|
471
|
+
throw createFontDirsParseError(`Font path is not a regular file: ${fontPath}`);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
size: stats.size
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function enforceFontDirectoryLimits(fontPath, size, collectionState) {
|
|
478
|
+
if (size > RASTERIZATION_FONT_DIR_LIMITS.maxSingleFontBytes) {
|
|
479
|
+
throw createFontDirsParseError(
|
|
480
|
+
`Font file exceeds the ${formatByteCount(RASTERIZATION_FONT_DIR_LIMITS.maxSingleFontBytes)} per-file limit: ${fontPath} (${formatByteCount(size)}).`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
const nextFontCount = collectionState.fontCount + 1;
|
|
484
|
+
if (nextFontCount > RASTERIZATION_FONT_DIR_LIMITS.maxFonts) {
|
|
485
|
+
throw createFontDirsParseError(
|
|
486
|
+
`fontDirs contain more than ${RASTERIZATION_FONT_DIR_LIMITS.maxFonts} font files.`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const nextTotalBytes = collectionState.totalBytes + size;
|
|
490
|
+
if (nextTotalBytes > RASTERIZATION_FONT_DIR_LIMITS.maxTotalFontBytes) {
|
|
491
|
+
throw createFontDirsParseError(
|
|
492
|
+
`fontDirs exceed the ${formatByteCount(RASTERIZATION_FONT_DIR_LIMITS.maxTotalFontBytes)} total font size limit (${formatByteCount(nextTotalBytes)}).`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
collectionState.fontCount = nextFontCount;
|
|
496
|
+
collectionState.totalBytes = nextTotalBytes;
|
|
497
|
+
}
|
|
498
|
+
function createFontDirsParseError(actual, suggestion) {
|
|
499
|
+
return new CliParseDiagnosticError({
|
|
500
|
+
error_type: "parse_error",
|
|
501
|
+
actual,
|
|
502
|
+
suggestion: suggestion ?? "Reduce --font-dir inputs or remove oversized font files."
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async function readTextFileWithLimit(filePath, fieldName, maxBytes) {
|
|
506
|
+
const stats = await lstat(filePath);
|
|
507
|
+
if (stats.size > maxBytes) {
|
|
508
|
+
throw new CliParseDiagnosticError({
|
|
509
|
+
error_type: "parse_error",
|
|
510
|
+
actual: `${fieldName} file exceeds the ${formatByteCount(maxBytes)} size limit: ${filePath} (${formatByteCount(stats.size)}).`,
|
|
511
|
+
expected: `${fieldName} file must be at most ${formatByteCount(maxBytes)}.`,
|
|
512
|
+
suggestion: `Reduce ${fieldName} to ${formatByteCount(maxBytes)} or less before running the command.`
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return await readFile(filePath, "utf8");
|
|
516
|
+
}
|
|
517
|
+
function formatByteCount(bytes) {
|
|
518
|
+
const mib = bytes / (1024 * 1024);
|
|
519
|
+
return Number.isInteger(mib) ? `${mib} MiB` : `${bytes} bytes`;
|
|
520
|
+
}
|
|
521
|
+
function basenameWithoutExtension(path) {
|
|
522
|
+
const extension = extname(path);
|
|
523
|
+
return basename(path, extension);
|
|
524
|
+
}
|
|
525
|
+
function deriveOutputPath(outputDir, sourcePath, extension) {
|
|
526
|
+
return join(outputDir, `${basenameWithoutExtension(sourcePath)}${extension}`);
|
|
527
|
+
}
|
|
528
|
+
async function ensureDirectoryForOutputDir(outputDir) {
|
|
529
|
+
if (!outputDir) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
await mkdir(outputDir, { recursive: true });
|
|
533
|
+
}
|
|
534
|
+
function deriveExtractOutputPath(flags) {
|
|
535
|
+
if (!flags.outputDir) {
|
|
536
|
+
return void 0;
|
|
537
|
+
}
|
|
538
|
+
if (flags.inputPath) {
|
|
539
|
+
return deriveOutputPath(flags.outputDir, flags.inputPath, ".yaml");
|
|
540
|
+
}
|
|
541
|
+
if (flags.presentationId) {
|
|
542
|
+
return join(flags.outputDir, `${flags.presentationId}.yaml`);
|
|
543
|
+
}
|
|
544
|
+
const presentationId = flags.presentationUrl?.match(/\/presentation\/d\/(?<id>[^/]+)/u)?.groups?.id;
|
|
545
|
+
return join(flags.outputDir, `${presentationId ?? "style"}.yaml`);
|
|
546
|
+
}
|
|
547
|
+
function resolveGenerateTheme(flags, env, config) {
|
|
548
|
+
const theme = flags.theme ?? (env.MORPHE_THEME && env.MORPHE_THEME.trim().length > 0 ? env.MORPHE_THEME : config.theme);
|
|
549
|
+
if (theme === void 0) {
|
|
550
|
+
return void 0;
|
|
551
|
+
}
|
|
552
|
+
const builtinThemeNames = new Set(listThemes().map((summary) => summary.name));
|
|
553
|
+
return builtinThemeNames.has(theme) ? `builtin:${theme}` : theme;
|
|
554
|
+
}
|
|
555
|
+
function resolveMcpPublicBaseUrl(value, bindHost) {
|
|
556
|
+
if (value === void 0) {
|
|
557
|
+
if (!isLoopbackHost(bindHost)) {
|
|
558
|
+
throw new CliUsageError(
|
|
559
|
+
"Non-loopback HTTP MCP bindings require an explicit HTTPS publicBaseUrl."
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
return void 0;
|
|
563
|
+
}
|
|
564
|
+
let url;
|
|
565
|
+
try {
|
|
566
|
+
url = new URL(value);
|
|
567
|
+
} catch {
|
|
568
|
+
throw new CliUsageError("publicBaseUrl must be a valid absolute HTTP(S) URL.");
|
|
569
|
+
}
|
|
570
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
571
|
+
throw new CliUsageError("publicBaseUrl must be a valid absolute HTTP(S) URL.");
|
|
572
|
+
}
|
|
573
|
+
if ((!isLoopbackHost(bindHost) || !isLoopbackHost(url.hostname)) && url.protocol !== "https:") {
|
|
574
|
+
throw new CliUsageError(
|
|
575
|
+
"Non-loopback HTTP MCP bindings require an explicit HTTPS publicBaseUrl."
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
return serializeBaseUrl(url);
|
|
579
|
+
}
|
|
580
|
+
function isLoopbackHost(host) {
|
|
581
|
+
const normalized = normalizeHostForComparison(host);
|
|
582
|
+
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
|
|
583
|
+
}
|
|
584
|
+
function normalizeHostForComparison(host) {
|
|
585
|
+
const normalized = host.trim().toLowerCase();
|
|
586
|
+
return normalized.startsWith("[") && normalized.endsWith("]") ? normalized.slice(1, -1) : normalized;
|
|
587
|
+
}
|
|
588
|
+
function serializeBaseUrl(url) {
|
|
589
|
+
url.search = "";
|
|
590
|
+
url.hash = "";
|
|
591
|
+
url.pathname = url.pathname.replace(/\/+$/u, "") || "/";
|
|
592
|
+
return url.pathname === "/" ? url.origin : `${url.origin}${url.pathname}`;
|
|
593
|
+
}
|
|
594
|
+
function detectCliExitCode(error) {
|
|
595
|
+
if (isInputError(error)) {
|
|
596
|
+
return CLI_EXIT_CODES.input;
|
|
597
|
+
}
|
|
598
|
+
if (isLayoutError(error)) {
|
|
599
|
+
return CLI_EXIT_CODES.layout;
|
|
600
|
+
}
|
|
601
|
+
if (isAuthError(error)) {
|
|
602
|
+
return CLI_EXIT_CODES.auth;
|
|
603
|
+
}
|
|
604
|
+
if (isApiError(error)) {
|
|
605
|
+
return CLI_EXIT_CODES.api;
|
|
606
|
+
}
|
|
607
|
+
return CLI_EXIT_CODES.internal;
|
|
608
|
+
}
|
|
609
|
+
function isInputError(error) {
|
|
610
|
+
if (error instanceof CliUsageError) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
if (errorChainContains(
|
|
614
|
+
error,
|
|
615
|
+
(candidate) => candidate instanceof AssetError && (candidate.diagnostic.errorType === "parse_error" || candidate.diagnostic.errorType === "schema_error")
|
|
616
|
+
)) {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
if (error instanceof PptxExtractionError) {
|
|
620
|
+
return error.errorType === "parse_error";
|
|
621
|
+
}
|
|
622
|
+
if (error instanceof MarkdownAuthoringError) {
|
|
623
|
+
return error.errorType === "parse_error";
|
|
624
|
+
}
|
|
625
|
+
if (error instanceof ThemeResolutionError) {
|
|
626
|
+
return error.errorType === "parse_error" || error.errorType === "schema_error";
|
|
627
|
+
}
|
|
628
|
+
if (errorChainContains(error, (candidate) => candidate instanceof FontValidationError)) {
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
if (isErrnoError(error) && ["ENOENT", "EISDIR", "ENOTDIR"].includes(error.code ?? "")) {
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
function isLayoutError(error) {
|
|
637
|
+
if (errorChainContains(error, (candidate) => candidate instanceof LayoutIntegrityError)) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
const layoutIntegrityError = [...iterateErrorChain(error)].some(
|
|
641
|
+
(candidate) => /layout_integrity_error|strict extraction failed/iu.test(candidate.message)
|
|
642
|
+
);
|
|
643
|
+
if (layoutIntegrityError) {
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
return errorChainContains(
|
|
647
|
+
error,
|
|
648
|
+
(candidate) => candidate instanceof AssetError && candidate.diagnostic.errorType === "rasterization_warning" && candidate.diagnostic.fatal
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
function isAuthError(error) {
|
|
652
|
+
return errorChainContains(
|
|
653
|
+
error,
|
|
654
|
+
(candidate) => candidate instanceof ReauthRequiredError || candidate instanceof AuthError
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
function isApiError(error) {
|
|
658
|
+
return errorChainContains(
|
|
659
|
+
error,
|
|
660
|
+
(candidate) => candidate instanceof GoogleSlidesApiError || candidate instanceof AssetError && candidate.diagnostic.errorType === "api_error"
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
function errorChainContains(error, predicate) {
|
|
664
|
+
for (const candidate of iterateErrorChain(error)) {
|
|
665
|
+
if (predicate(candidate)) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
function* iterateErrorChain(error) {
|
|
672
|
+
let current = error;
|
|
673
|
+
const seen = /* @__PURE__ */ new Set();
|
|
674
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
675
|
+
seen.add(current);
|
|
676
|
+
yield current;
|
|
677
|
+
current = current.cause;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function findFirstErrorInChain(error, constructor) {
|
|
681
|
+
for (const candidate of iterateErrorChain(error)) {
|
|
682
|
+
if (candidate instanceof constructor) {
|
|
683
|
+
return candidate;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
function isErrnoError(error) {
|
|
689
|
+
return error instanceof Error;
|
|
690
|
+
}
|
|
691
|
+
function parseAuthFlags(args) {
|
|
692
|
+
const flags = {};
|
|
693
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
694
|
+
const arg = args[index];
|
|
695
|
+
if (arg === "--token-path") {
|
|
696
|
+
const value = args[index + 1];
|
|
697
|
+
if (!value) {
|
|
698
|
+
throw new CliUsageError("Missing value for --token-path");
|
|
699
|
+
}
|
|
700
|
+
flags.tokenPath = value;
|
|
701
|
+
index += 1;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (arg === "--service-account") {
|
|
705
|
+
const value = args[index + 1];
|
|
706
|
+
if (!value) {
|
|
707
|
+
throw new CliUsageError("Missing value for --service-account");
|
|
708
|
+
}
|
|
709
|
+
flags.serviceAccountPath = value;
|
|
710
|
+
index += 1;
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
throw new CliUsageError(`Unknown auth flag: ${arg}`);
|
|
714
|
+
}
|
|
715
|
+
return flags;
|
|
716
|
+
}
|
|
717
|
+
function parseExtractFlags(args) {
|
|
718
|
+
const flags = {
|
|
719
|
+
strict: false
|
|
720
|
+
};
|
|
721
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
722
|
+
const arg = args[index];
|
|
723
|
+
if (arg === "--input") {
|
|
724
|
+
const value = args[index + 1];
|
|
725
|
+
if (!value) {
|
|
726
|
+
throw new CliUsageError("Missing value for --input");
|
|
727
|
+
}
|
|
728
|
+
flags.inputPath = value;
|
|
729
|
+
index += 1;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (arg === "--output") {
|
|
733
|
+
const value = args[index + 1];
|
|
734
|
+
if (!value) {
|
|
735
|
+
throw new CliUsageError("Missing value for --output");
|
|
736
|
+
}
|
|
737
|
+
flags.outputPath = value;
|
|
738
|
+
index += 1;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (arg === "--output-dir") {
|
|
742
|
+
const value = args[index + 1];
|
|
743
|
+
if (!value) {
|
|
744
|
+
throw new CliUsageError("Missing value for --output-dir");
|
|
745
|
+
}
|
|
746
|
+
flags.outputDir = value;
|
|
747
|
+
index += 1;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (arg === "--log-format") {
|
|
751
|
+
const value = args[index + 1];
|
|
752
|
+
if (!value) {
|
|
753
|
+
throw new CliUsageError("Missing value for --log-format");
|
|
754
|
+
}
|
|
755
|
+
flags.logFormat = parseLogFormat(value, "`--log-format`");
|
|
756
|
+
index += 1;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (arg === "--presentation-id") {
|
|
760
|
+
const value = args[index + 1];
|
|
761
|
+
if (!value) {
|
|
762
|
+
throw new CliUsageError("Missing value for --presentation-id");
|
|
763
|
+
}
|
|
764
|
+
flags.presentationId = value;
|
|
765
|
+
index += 1;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (arg === "--presentation-url") {
|
|
769
|
+
const value = args[index + 1];
|
|
770
|
+
if (!value) {
|
|
771
|
+
throw new CliUsageError("Missing value for --presentation-url");
|
|
772
|
+
}
|
|
773
|
+
flags.presentationUrl = value;
|
|
774
|
+
index += 1;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (arg === "--token-path") {
|
|
778
|
+
const value = args[index + 1];
|
|
779
|
+
if (!value) {
|
|
780
|
+
throw new CliUsageError("Missing value for --token-path");
|
|
781
|
+
}
|
|
782
|
+
flags.tokenPath = value;
|
|
783
|
+
index += 1;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (arg === "--service-account") {
|
|
787
|
+
const value = args[index + 1];
|
|
788
|
+
if (!value) {
|
|
789
|
+
throw new CliUsageError("Missing value for --service-account");
|
|
790
|
+
}
|
|
791
|
+
flags.serviceAccountPath = value;
|
|
792
|
+
index += 1;
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (arg === "--strict") {
|
|
796
|
+
flags.strict = true;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
throw new CliUsageError(`Unknown extract flag: ${arg}`);
|
|
800
|
+
}
|
|
801
|
+
if (!flags.inputPath && !flags.presentationId && !flags.presentationUrl) {
|
|
802
|
+
throw new CliUsageError("Either `--input`, `--presentation-id`, or `--presentation-url` is required.");
|
|
803
|
+
}
|
|
804
|
+
return flags;
|
|
805
|
+
}
|
|
806
|
+
function parseGenerateFlags(args) {
|
|
807
|
+
const flags = {
|
|
808
|
+
mode: "new",
|
|
809
|
+
fontDirs: [],
|
|
810
|
+
strictRasterization: false,
|
|
811
|
+
allowRemoteImages: false,
|
|
812
|
+
allowPrivateIp: false
|
|
813
|
+
};
|
|
814
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
815
|
+
const arg = args[index];
|
|
816
|
+
if (arg === "--input") {
|
|
817
|
+
const value = args[index + 1];
|
|
818
|
+
if (!value) {
|
|
819
|
+
throw new CliUsageError("Missing value for --input");
|
|
820
|
+
}
|
|
821
|
+
flags.inputPath = value;
|
|
822
|
+
index += 1;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (arg === "--style") {
|
|
826
|
+
const value = args[index + 1];
|
|
827
|
+
if (!value) {
|
|
828
|
+
throw new CliUsageError("Missing value for --style");
|
|
829
|
+
}
|
|
830
|
+
flags.stylePath = value;
|
|
831
|
+
index += 1;
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (arg === "--output") {
|
|
835
|
+
const value = args[index + 1];
|
|
836
|
+
if (!value) {
|
|
837
|
+
throw new CliUsageError("Missing value for --output");
|
|
838
|
+
}
|
|
839
|
+
flags.outputPath = value;
|
|
840
|
+
index += 1;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (arg === "--output-dir") {
|
|
844
|
+
const value = args[index + 1];
|
|
845
|
+
if (!value) {
|
|
846
|
+
throw new CliUsageError("Missing value for --output-dir");
|
|
847
|
+
}
|
|
848
|
+
flags.outputDir = value;
|
|
849
|
+
index += 1;
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (arg === "--format") {
|
|
853
|
+
const value = args[index + 1];
|
|
854
|
+
if (value !== "pptx" && value !== "gslides") {
|
|
855
|
+
throw new CliUsageError("`--format` must be one of `pptx` or `gslides`.");
|
|
856
|
+
}
|
|
857
|
+
flags.format = value;
|
|
858
|
+
index += 1;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (arg === "--log-format") {
|
|
862
|
+
const value = args[index + 1];
|
|
863
|
+
if (!value) {
|
|
864
|
+
throw new CliUsageError("Missing value for --log-format");
|
|
865
|
+
}
|
|
866
|
+
flags.logFormat = parseLogFormat(value, "`--log-format`");
|
|
867
|
+
index += 1;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (arg === "--strict-rasterization") {
|
|
871
|
+
flags.strictRasterization = true;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (arg === "--allow-remote-images") {
|
|
875
|
+
flags.allowRemoteImages = true;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (arg === "--allow-private-ip") {
|
|
879
|
+
flags.allowPrivateIp = true;
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (arg === "--mode") {
|
|
883
|
+
const value = args[index + 1];
|
|
884
|
+
if (value !== "new" && value !== "overwrite" && value !== "diff") {
|
|
885
|
+
throw new CliUsageError("`--mode` must be one of `new`, `overwrite`, or `diff`.");
|
|
886
|
+
}
|
|
887
|
+
flags.mode = value;
|
|
888
|
+
index += 1;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (arg === "--presentation-id") {
|
|
892
|
+
const value = args[index + 1];
|
|
893
|
+
if (!value) {
|
|
894
|
+
throw new CliUsageError("Missing value for --presentation-id");
|
|
895
|
+
}
|
|
896
|
+
flags.presentationId = value;
|
|
897
|
+
index += 1;
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (arg === "--drive-folder") {
|
|
901
|
+
const value = args[index + 1];
|
|
902
|
+
if (!value) {
|
|
903
|
+
throw new CliUsageError("Missing value for --drive-folder");
|
|
904
|
+
}
|
|
905
|
+
flags.driveFolder = value;
|
|
906
|
+
index += 1;
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (arg === "--theme") {
|
|
910
|
+
const value = args[index + 1];
|
|
911
|
+
if (!value) {
|
|
912
|
+
throw new CliUsageError("Missing value for --theme");
|
|
913
|
+
}
|
|
914
|
+
flags.theme = value;
|
|
915
|
+
index += 1;
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
if (arg === "--font-dir") {
|
|
919
|
+
const value = args[index + 1];
|
|
920
|
+
if (!value) {
|
|
921
|
+
throw new CliUsageError("Missing value for --font-dir");
|
|
922
|
+
}
|
|
923
|
+
flags.fontDirs.push(value);
|
|
924
|
+
index += 1;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (arg === "--token-path") {
|
|
928
|
+
const value = args[index + 1];
|
|
929
|
+
if (!value) {
|
|
930
|
+
throw new CliUsageError("Missing value for --token-path");
|
|
931
|
+
}
|
|
932
|
+
flags.tokenPath = value;
|
|
933
|
+
index += 1;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
if (arg === "--service-account") {
|
|
937
|
+
const value = args[index + 1];
|
|
938
|
+
if (!value) {
|
|
939
|
+
throw new CliUsageError("Missing value for --service-account");
|
|
940
|
+
}
|
|
941
|
+
flags.serviceAccountPath = value;
|
|
942
|
+
index += 1;
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
throw new CliUsageError(`Unknown generate flag: ${arg}`);
|
|
946
|
+
}
|
|
947
|
+
if (!flags.inputPath) {
|
|
948
|
+
throw new CliUsageError("`--input` is required.");
|
|
949
|
+
}
|
|
950
|
+
if (!flags.outputPath && flags.outputDir && flags.format !== "gslides") {
|
|
951
|
+
flags.outputPath = deriveOutputPath(flags.outputDir, flags.inputPath, ".pptx");
|
|
952
|
+
}
|
|
953
|
+
if (!flags.format) {
|
|
954
|
+
if (flags.outputPath?.toLowerCase().endsWith(".pptx")) {
|
|
955
|
+
flags.format = "pptx";
|
|
956
|
+
} else {
|
|
957
|
+
throw new CliUsageError("`--format` is required unless `--output` ends with `.pptx`.");
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (flags.format === "pptx" && !flags.outputPath) {
|
|
961
|
+
throw new CliUsageError("`--output` is required when `--format pptx` is used.");
|
|
962
|
+
}
|
|
963
|
+
if (flags.format === "gslides" && (flags.mode === "overwrite" || flags.mode === "diff") && !flags.presentationId) {
|
|
964
|
+
throw new CliUsageError("`--presentation-id` is required when `--mode overwrite` or `--mode diff` is used.");
|
|
965
|
+
}
|
|
966
|
+
return flags;
|
|
967
|
+
}
|
|
968
|
+
function parseValidateFlags(args) {
|
|
969
|
+
const flags = {};
|
|
970
|
+
const positionals = [];
|
|
971
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
972
|
+
const arg = args[index];
|
|
973
|
+
if (arg === "--input") {
|
|
974
|
+
const value = args[index + 1];
|
|
975
|
+
if (!value) {
|
|
976
|
+
throw new CliUsageError("Missing value for --input");
|
|
977
|
+
}
|
|
978
|
+
flags.inputPath = value;
|
|
979
|
+
index += 1;
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (arg === "--log-format") {
|
|
983
|
+
const value = args[index + 1];
|
|
984
|
+
if (!value) {
|
|
985
|
+
throw new CliUsageError("Missing value for --log-format");
|
|
986
|
+
}
|
|
987
|
+
flags.logFormat = parseLogFormat(value, "`--log-format`");
|
|
988
|
+
index += 1;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (arg.startsWith("--")) {
|
|
992
|
+
throw new CliUsageError(`Unknown validate flag: ${arg}`);
|
|
993
|
+
}
|
|
994
|
+
positionals.push(arg);
|
|
995
|
+
}
|
|
996
|
+
if (!flags.inputPath) {
|
|
997
|
+
flags.inputPath = positionals[0];
|
|
998
|
+
}
|
|
999
|
+
if (!flags.inputPath) {
|
|
1000
|
+
throw new CliUsageError("A YAML file path is required.");
|
|
1001
|
+
}
|
|
1002
|
+
return flags;
|
|
1003
|
+
}
|
|
1004
|
+
function parseMcpServeFlags(args) {
|
|
1005
|
+
const flags = {
|
|
1006
|
+
transport: "stdio",
|
|
1007
|
+
host: "127.0.0.1",
|
|
1008
|
+
port: 8080
|
|
1009
|
+
};
|
|
1010
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1011
|
+
const arg = args[index];
|
|
1012
|
+
if (arg === "--transport") {
|
|
1013
|
+
const value = args[index + 1];
|
|
1014
|
+
if (value !== "stdio" && value !== "http") {
|
|
1015
|
+
throw new CliUsageError("`--transport` must be `stdio` or `http`.");
|
|
1016
|
+
}
|
|
1017
|
+
flags.transport = value;
|
|
1018
|
+
index += 1;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (arg === "--host") {
|
|
1022
|
+
const value = args[index + 1];
|
|
1023
|
+
if (!value) {
|
|
1024
|
+
throw new CliUsageError("Missing value for --host");
|
|
1025
|
+
}
|
|
1026
|
+
flags.host = value;
|
|
1027
|
+
index += 1;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (arg === "--port") {
|
|
1031
|
+
const value = args[index + 1];
|
|
1032
|
+
if (!value) {
|
|
1033
|
+
throw new CliUsageError("Missing value for --port");
|
|
1034
|
+
}
|
|
1035
|
+
const parsed = Number(value);
|
|
1036
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
1037
|
+
throw new CliUsageError("`--port` must be an integer between 0 and 65535.");
|
|
1038
|
+
}
|
|
1039
|
+
flags.port = parsed;
|
|
1040
|
+
index += 1;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
if (arg === "--public-base-url" || arg === "--publicBaseUrl") {
|
|
1044
|
+
const value = args[index + 1];
|
|
1045
|
+
if (!value) {
|
|
1046
|
+
throw new CliUsageError(`Missing value for ${arg}`);
|
|
1047
|
+
}
|
|
1048
|
+
flags.publicBaseUrl = value;
|
|
1049
|
+
index += 1;
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
throw new CliUsageError(`Unknown mcp flag: ${arg}`);
|
|
1053
|
+
}
|
|
1054
|
+
if (flags.transport === "http") {
|
|
1055
|
+
flags.publicBaseUrl = resolveMcpPublicBaseUrl(flags.publicBaseUrl, flags.host);
|
|
1056
|
+
}
|
|
1057
|
+
return flags;
|
|
1058
|
+
}
|
|
1059
|
+
function formatDiagnosticForCli(diagnostic, label = "warning") {
|
|
1060
|
+
const details = [
|
|
1061
|
+
diagnostic.location ? formatCliSourceLocation(diagnostic.location) : void 0,
|
|
1062
|
+
diagnostic.actual,
|
|
1063
|
+
diagnostic.expected ? `expected ${diagnostic.expected}` : void 0,
|
|
1064
|
+
diagnostic.suggestion
|
|
1065
|
+
].filter((value) => Boolean(value && value.length > 0)).join(" | ");
|
|
1066
|
+
return details.length > 0 ? `${label} [${diagnostic.error_type}] ${details}` : `${label} [${diagnostic.error_type}]`;
|
|
1067
|
+
}
|
|
1068
|
+
function formatCliSourceLocation(location) {
|
|
1069
|
+
return `${location.file}:L${location.start.line}:C${location.start.column}`;
|
|
1070
|
+
}
|
|
1071
|
+
function formatAssetErrorDiagnosticForCli(error) {
|
|
1072
|
+
return {
|
|
1073
|
+
error_type: error.diagnostic.errorType,
|
|
1074
|
+
actual: error.diagnostic.message,
|
|
1075
|
+
location: error.diagnostic.location,
|
|
1076
|
+
suggestion: error.diagnostic.suggestion
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function formatFontValidationDiagnosticsForCli(error) {
|
|
1080
|
+
return error.diagnostics.map((diagnostic) => ({
|
|
1081
|
+
error_type: diagnostic.errorType,
|
|
1082
|
+
actual: diagnostic.message,
|
|
1083
|
+
location: diagnostic.location,
|
|
1084
|
+
suggestion: diagnostic.suggestion
|
|
1085
|
+
}));
|
|
1086
|
+
}
|
|
1087
|
+
function formatLayoutIntegrityDiagnosticsForCli(error) {
|
|
1088
|
+
return error.diagnostics.map((diagnostic) => ({
|
|
1089
|
+
error_type: error.errorType,
|
|
1090
|
+
actual: diagnostic.message,
|
|
1091
|
+
suggestion: `${diagnostic.yamlSuggestion} ${diagnostic.markdownSuggestion}`
|
|
1092
|
+
}));
|
|
1093
|
+
}
|
|
1094
|
+
function renderGeneralHelp() {
|
|
1095
|
+
return [
|
|
1096
|
+
"Usage: morphe <command> [options]",
|
|
1097
|
+
"",
|
|
1098
|
+
"Commands:",
|
|
1099
|
+
" generate Generate a PPTX or Google Slides presentation",
|
|
1100
|
+
" extract Extract Morphe style YAML from PPTX or Google Slides",
|
|
1101
|
+
" validate Validate Morphe style YAML",
|
|
1102
|
+
" themes list List bundled theme presets",
|
|
1103
|
+
" auth login Authenticate with Google APIs",
|
|
1104
|
+
" auth logout Remove cached OAuth tokens",
|
|
1105
|
+
" mcp serve Start the Morphe MCP server",
|
|
1106
|
+
"",
|
|
1107
|
+
"Global options:",
|
|
1108
|
+
" --help Show help",
|
|
1109
|
+
" --version Print the Morphe CLI version",
|
|
1110
|
+
" --verbose Emit DEBUG and above logs",
|
|
1111
|
+
" --quiet Emit ERROR logs only",
|
|
1112
|
+
" --non-interactive Disable OAuth/browser prompts (also enabled when stdin is not a TTY)"
|
|
1113
|
+
].join("\n");
|
|
1114
|
+
}
|
|
1115
|
+
function renderAuthHelp() {
|
|
1116
|
+
return "Usage: morphe auth <login|logout> [--token-path <path>] [--service-account <path>] [--non-interactive]";
|
|
1117
|
+
}
|
|
1118
|
+
function renderExtractHelp() {
|
|
1119
|
+
return [
|
|
1120
|
+
"Usage:",
|
|
1121
|
+
" morphe extract --input <existing.pptx> [--output <style.yaml>] [--output-dir <dir>] [--log-format <text|json>]",
|
|
1122
|
+
" morphe extract (--presentation-id <id> | --presentation-url <url>) [--output <style.yaml>] [--output-dir <dir>] [--log-format <text|json>] [--token-path <path>] [--service-account <path>] [--non-interactive] [--strict]"
|
|
1123
|
+
].join("\n");
|
|
1124
|
+
}
|
|
1125
|
+
function renderGenerateHelp() {
|
|
1126
|
+
return [
|
|
1127
|
+
"Usage:",
|
|
1128
|
+
" morphe generate --input <slides.md> --format pptx (--output <out.pptx> | --output-dir <dir>) [--style <theme.yaml>] [--theme <theme>] [--font-dir <dir>]... [--log-format <text|json>] [--strict-rasterization] [--allow-remote-images] [--allow-private-ip]",
|
|
1129
|
+
" morphe generate --input <slides.md> --format gslides [--style <theme.yaml>] [--mode <new|overwrite|diff>] [--presentation-id <id>] [--drive-folder <id>] [--theme <theme>] [--font-dir <dir>]... [--log-format <text|json>] [--strict-rasterization] [--allow-remote-images] [--allow-private-ip] [--token-path <path>] [--service-account <path>] [--non-interactive]",
|
|
1130
|
+
"",
|
|
1131
|
+
"Config files: .morpherc or morphe.config.yaml may set theme, logFormat, and fontDirs."
|
|
1132
|
+
].join("\n");
|
|
1133
|
+
}
|
|
1134
|
+
function renderValidateHelp() {
|
|
1135
|
+
return "Usage: morphe validate <style.yaml> [--log-format <text|json>]";
|
|
1136
|
+
}
|
|
1137
|
+
function renderThemesHelp() {
|
|
1138
|
+
return "Usage: morphe themes list";
|
|
1139
|
+
}
|
|
1140
|
+
function renderMcpHelp() {
|
|
1141
|
+
return "Usage: morphe mcp serve [--transport <stdio|http>] [--host <host>] [--port <port>] [--public-base-url <url>]";
|
|
1142
|
+
}
|
|
1143
|
+
async function handleAuthCommand(args, dependencies, nonInteractive) {
|
|
1144
|
+
const [subcommand, ...flagArgs] = args;
|
|
1145
|
+
if (!subcommand || subcommand === "--help") {
|
|
1146
|
+
writeLine(dependencies.stdout, renderAuthHelp());
|
|
1147
|
+
return subcommand ? 0 : 1;
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
if ((subcommand === "login" || subcommand === "logout") && flagArgs.includes("--help")) {
|
|
1151
|
+
writeLine(dependencies.stdout, renderAuthHelp());
|
|
1152
|
+
return CLI_EXIT_CODES.success;
|
|
1153
|
+
}
|
|
1154
|
+
const flags = parseAuthFlags(flagArgs);
|
|
1155
|
+
if (subcommand === "login") {
|
|
1156
|
+
if (nonInteractive && !hasServiceAccountCredential(flags, dependencies.env)) {
|
|
1157
|
+
throw createNonInteractiveAuthError("morphe auth login");
|
|
1158
|
+
}
|
|
1159
|
+
const result = await dependencies.authService.login({
|
|
1160
|
+
env: dependencies.env,
|
|
1161
|
+
tokenPath: flags.tokenPath,
|
|
1162
|
+
serviceAccountPath: flags.serviceAccountPath,
|
|
1163
|
+
allowInteractive: !nonInteractive
|
|
1164
|
+
});
|
|
1165
|
+
if (result.session.source === "service-account") {
|
|
1166
|
+
const jsonKeyPath = result.session.principal.kind === "service-account" ? result.session.principal.jsonKeyPath : "unknown";
|
|
1167
|
+
writeLine(dependencies.stdout, `Loaded service account credentials from ${jsonKeyPath}.`);
|
|
1168
|
+
return 0;
|
|
1169
|
+
}
|
|
1170
|
+
const authUrlSuffix = result.authorizationUrl ? ` ${result.authorizationUrl}` : "";
|
|
1171
|
+
writeLine(dependencies.stdout, `Started OAuth login flow.${authUrlSuffix}`.trimEnd());
|
|
1172
|
+
return 0;
|
|
1173
|
+
}
|
|
1174
|
+
if (subcommand === "logout") {
|
|
1175
|
+
if (flags.serviceAccountPath) {
|
|
1176
|
+
throw new CliUsageError("`--service-account` is only supported with `morphe auth login`.");
|
|
1177
|
+
}
|
|
1178
|
+
await dependencies.authService.logout({
|
|
1179
|
+
env: dependencies.env,
|
|
1180
|
+
tokenPath: flags.tokenPath
|
|
1181
|
+
});
|
|
1182
|
+
writeLine(dependencies.stdout, "Removed cached Morphe OAuth tokens.");
|
|
1183
|
+
return 0;
|
|
1184
|
+
}
|
|
1185
|
+
writeLine(dependencies.stderr, renderAuthHelp());
|
|
1186
|
+
return 1;
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
writeLine(dependencies.stderr, toErrorMessage(error));
|
|
1189
|
+
return detectCliExitCode(error);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
function handleThemesCommand(args, dependencies) {
|
|
1193
|
+
const [subcommand, ...rest] = args;
|
|
1194
|
+
if (!subcommand || subcommand === "--help") {
|
|
1195
|
+
writeLine(dependencies.stdout, renderThemesHelp());
|
|
1196
|
+
return subcommand ? 0 : 1;
|
|
1197
|
+
}
|
|
1198
|
+
if (subcommand !== "list") {
|
|
1199
|
+
writeLine(dependencies.stderr, `Unknown themes command: ${subcommand}`);
|
|
1200
|
+
return CLI_EXIT_CODES.input;
|
|
1201
|
+
}
|
|
1202
|
+
if (rest.includes("--help")) {
|
|
1203
|
+
writeLine(dependencies.stdout, renderThemesHelp());
|
|
1204
|
+
return CLI_EXIT_CODES.success;
|
|
1205
|
+
}
|
|
1206
|
+
if (rest.length > 0) {
|
|
1207
|
+
writeLine(dependencies.stderr, `Unknown themes flag: ${rest[0]}`);
|
|
1208
|
+
return CLI_EXIT_CODES.input;
|
|
1209
|
+
}
|
|
1210
|
+
for (const theme of listThemes()) {
|
|
1211
|
+
writeLine(dependencies.stdout, `${theme.name} ${theme.display_name} ${theme.description}`);
|
|
1212
|
+
}
|
|
1213
|
+
return CLI_EXIT_CODES.success;
|
|
1214
|
+
}
|
|
1215
|
+
async function handleExtractCommand(args, dependencies, globalFlags, nonInteractive) {
|
|
1216
|
+
if (args.includes("--help")) {
|
|
1217
|
+
writeLine(dependencies.stdout, renderExtractHelp());
|
|
1218
|
+
return 0;
|
|
1219
|
+
}
|
|
1220
|
+
let logOptions = null;
|
|
1221
|
+
try {
|
|
1222
|
+
const flags = parseExtractFlags(args);
|
|
1223
|
+
const config = await loadCliConfig();
|
|
1224
|
+
logOptions = resolveLogOptions(dependencies, globalFlags, flags.logFormat, config);
|
|
1225
|
+
const outputPath = flags.outputPath ?? deriveExtractOutputPath(flags);
|
|
1226
|
+
await ensureDirectoryForOutputDir(flags.outputDir);
|
|
1227
|
+
if (flags.inputPath) {
|
|
1228
|
+
const response2 = await dependencies.extractPptx(
|
|
1229
|
+
{
|
|
1230
|
+
input: {
|
|
1231
|
+
kind: "path",
|
|
1232
|
+
path: flags.inputPath
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
{}
|
|
1236
|
+
);
|
|
1237
|
+
const yaml2 = response2.style_yaml.content;
|
|
1238
|
+
if (outputPath) {
|
|
1239
|
+
await writeFile(outputPath, yaml2, "utf8");
|
|
1240
|
+
} else {
|
|
1241
|
+
writeLine(dependencies.stdout, yaml2.trimEnd());
|
|
1242
|
+
}
|
|
1243
|
+
if (response2.report.headerComment.length > 0) {
|
|
1244
|
+
writeLog(
|
|
1245
|
+
dependencies.stderr,
|
|
1246
|
+
logOptions,
|
|
1247
|
+
"warn",
|
|
1248
|
+
"extraction_warning_report",
|
|
1249
|
+
{
|
|
1250
|
+
header_comment: response2.report.headerComment,
|
|
1251
|
+
items: response2.report.items
|
|
1252
|
+
},
|
|
1253
|
+
response2.report.headerComment
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
return 0;
|
|
1257
|
+
}
|
|
1258
|
+
const response = await dependencies.extractGslides(
|
|
1259
|
+
{
|
|
1260
|
+
env: dependencies.env,
|
|
1261
|
+
presentationId: flags.presentationId,
|
|
1262
|
+
presentationUrl: flags.presentationUrl,
|
|
1263
|
+
serviceAccountPath: flags.serviceAccountPath,
|
|
1264
|
+
allowInteractive: !nonInteractive,
|
|
1265
|
+
strict: flags.strict,
|
|
1266
|
+
tokenPath: flags.tokenPath
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
authService: dependencies.authService
|
|
1270
|
+
}
|
|
1271
|
+
);
|
|
1272
|
+
const yaml = response.style_yaml;
|
|
1273
|
+
if (outputPath) {
|
|
1274
|
+
await writeFile(outputPath, yaml, "utf8");
|
|
1275
|
+
} else {
|
|
1276
|
+
writeLine(dependencies.stdout, yaml.trimEnd());
|
|
1277
|
+
}
|
|
1278
|
+
if (response.report.headerComment.length > 0) {
|
|
1279
|
+
writeLog(
|
|
1280
|
+
dependencies.stderr,
|
|
1281
|
+
logOptions,
|
|
1282
|
+
"warn",
|
|
1283
|
+
"extraction_warning_report",
|
|
1284
|
+
{
|
|
1285
|
+
header_comment: response.report.headerComment,
|
|
1286
|
+
items: response.report.items
|
|
1287
|
+
},
|
|
1288
|
+
response.report.headerComment
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
return 0;
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
const message = nonInteractive && isAuthError(error) ? formatNonInteractiveAuthFailure(error) : toErrorMessage(error);
|
|
1294
|
+
const errorLogOptions = resolveLogOptionsForError(dependencies, globalFlags, logOptions, args);
|
|
1295
|
+
if (errorLogOptions.format === "json") {
|
|
1296
|
+
const diagnostic = createCliErrorDiagnostic(error, message, renderExtractHelp());
|
|
1297
|
+
writeErrorDiagnostic(
|
|
1298
|
+
dependencies.stderr,
|
|
1299
|
+
errorLogOptions,
|
|
1300
|
+
diagnostic,
|
|
1301
|
+
formatDiagnosticForCli(diagnostic, "error")
|
|
1302
|
+
);
|
|
1303
|
+
return detectCliExitCode(error);
|
|
1304
|
+
}
|
|
1305
|
+
writeLine(dependencies.stderr, `${message}
|
|
1306
|
+
${renderExtractHelp()}`);
|
|
1307
|
+
return detectCliExitCode(error);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async function handleGenerateCommand(args, dependencies, globalFlags, nonInteractive) {
|
|
1311
|
+
if (args.includes("--help")) {
|
|
1312
|
+
writeLine(dependencies.stdout, renderGenerateHelp());
|
|
1313
|
+
return 0;
|
|
1314
|
+
}
|
|
1315
|
+
let logOptions = null;
|
|
1316
|
+
try {
|
|
1317
|
+
const flags = parseGenerateFlags(args);
|
|
1318
|
+
const config = await loadCliConfig();
|
|
1319
|
+
logOptions = resolveLogOptions(dependencies, globalFlags, flags.logFormat, config);
|
|
1320
|
+
const theme = resolveGenerateTheme(flags, dependencies.env, config);
|
|
1321
|
+
const rasterizationFontSources = await resolveRasterizationFontSources(flags, config);
|
|
1322
|
+
const markdown = await readTextFileWithLimit(
|
|
1323
|
+
flags.inputPath,
|
|
1324
|
+
"markdown",
|
|
1325
|
+
MARKDOWN_INPUT_MAX_BYTES
|
|
1326
|
+
);
|
|
1327
|
+
const styleYaml = flags.stylePath ? await readTextFileWithLimit(flags.stylePath, "style_yaml", STYLE_YAML_INPUT_MAX_BYTES) : void 0;
|
|
1328
|
+
if (flags.format === "pptx") {
|
|
1329
|
+
await ensureDirectoryForOutputDir(flags.outputDir);
|
|
1330
|
+
const remoteFetchPolicy2 = resolveRemoteFetchPolicy(flags, dependencies.env);
|
|
1331
|
+
const request2 = {
|
|
1332
|
+
markdown,
|
|
1333
|
+
markdownPath: flags.inputPath,
|
|
1334
|
+
styleYaml,
|
|
1335
|
+
stylePath: flags.stylePath,
|
|
1336
|
+
theme,
|
|
1337
|
+
...rasterizationFontSources.length > 0 ? { rasterizationFontSources } : {},
|
|
1338
|
+
outputPath: flags.outputPath,
|
|
1339
|
+
...flags.strictRasterization ? { strictRasterization: true } : {},
|
|
1340
|
+
...remoteFetchPolicy2 ? { remoteFetchPolicy: remoteFetchPolicy2 } : {}
|
|
1341
|
+
};
|
|
1342
|
+
const result = await dependencies.generatePptx(
|
|
1343
|
+
request2,
|
|
1344
|
+
{
|
|
1345
|
+
assetRepository: createAssetRepository({
|
|
1346
|
+
binaryProcessor: createResvgAssetBinaryProcessor(),
|
|
1347
|
+
codeBlockHighlighter: createShikiCodeHighlighter(),
|
|
1348
|
+
mermaidRenderer: createMermaidCliRenderer(),
|
|
1349
|
+
remoteAssetFetcher: createNodeHttpRemoteFetcher()
|
|
1350
|
+
}),
|
|
1351
|
+
compose: createComposePresentationFn(),
|
|
1352
|
+
render: createRenderPptxFn(),
|
|
1353
|
+
packZip: {
|
|
1354
|
+
pack: packPptxEntries
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
);
|
|
1358
|
+
writeLine(dependencies.stdout, flags.outputPath);
|
|
1359
|
+
for (const warning of result.warnings) {
|
|
1360
|
+
writeLog(
|
|
1361
|
+
dependencies.stderr,
|
|
1362
|
+
logOptions,
|
|
1363
|
+
"warn",
|
|
1364
|
+
"diagnostic",
|
|
1365
|
+
warning,
|
|
1366
|
+
formatDiagnosticForCli(warning)
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
return 0;
|
|
1370
|
+
}
|
|
1371
|
+
const remoteFetchPolicy = resolveRemoteFetchPolicy(flags, dependencies.env);
|
|
1372
|
+
const request = {
|
|
1373
|
+
env: dependencies.env,
|
|
1374
|
+
markdown,
|
|
1375
|
+
markdownPath: flags.inputPath,
|
|
1376
|
+
styleYaml,
|
|
1377
|
+
stylePath: flags.stylePath,
|
|
1378
|
+
mode: flags.mode,
|
|
1379
|
+
presentationId: flags.presentationId,
|
|
1380
|
+
driveFolder: flags.driveFolder,
|
|
1381
|
+
serviceAccountPath: flags.serviceAccountPath,
|
|
1382
|
+
allowInteractive: !nonInteractive,
|
|
1383
|
+
tokenPath: flags.tokenPath,
|
|
1384
|
+
theme,
|
|
1385
|
+
...rasterizationFontSources.length > 0 ? { rasterizationFontSources } : {},
|
|
1386
|
+
...flags.strictRasterization ? { strictRasterization: true } : {},
|
|
1387
|
+
...remoteFetchPolicy ? { remoteFetchPolicy } : {}
|
|
1388
|
+
};
|
|
1389
|
+
const response = await dependencies.generateGslides(
|
|
1390
|
+
request,
|
|
1391
|
+
{
|
|
1392
|
+
authService: dependencies.authService,
|
|
1393
|
+
assetRepository: createAssetRepository({
|
|
1394
|
+
binaryProcessor: createResvgAssetBinaryProcessor(),
|
|
1395
|
+
codeBlockHighlighter: createShikiCodeHighlighter(),
|
|
1396
|
+
mermaidRenderer: createMermaidCliRenderer(),
|
|
1397
|
+
remoteAssetFetcher: createNodeHttpRemoteFetcher()
|
|
1398
|
+
})
|
|
1399
|
+
}
|
|
1400
|
+
);
|
|
1401
|
+
writeLine(dependencies.stdout, response.presentation_url);
|
|
1402
|
+
writeLine(dependencies.stdout, response.presentation_id);
|
|
1403
|
+
const hasStrictRasterizationWarning = flags.strictRasterization && response.warnings.some((warning) => warning.error_type === "rasterization_warning");
|
|
1404
|
+
for (const warning of response.warnings) {
|
|
1405
|
+
const level = hasStrictRasterizationWarning && warning.error_type === "rasterization_warning" ? "error" : "warn";
|
|
1406
|
+
writeLog(
|
|
1407
|
+
dependencies.stderr,
|
|
1408
|
+
logOptions,
|
|
1409
|
+
level,
|
|
1410
|
+
"diagnostic",
|
|
1411
|
+
warning,
|
|
1412
|
+
formatDiagnosticForCli(warning, level === "error" ? "error" : "warning")
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
return hasStrictRasterizationWarning ? CLI_EXIT_CODES.layout : 0;
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
const cliParseDiagnosticError = findFirstErrorInChain(error, CliParseDiagnosticError);
|
|
1418
|
+
if (cliParseDiagnosticError) {
|
|
1419
|
+
writeErrorDiagnostic(
|
|
1420
|
+
dependencies.stderr,
|
|
1421
|
+
resolveLogOptionsForError(dependencies, globalFlags, logOptions, args),
|
|
1422
|
+
cliParseDiagnosticError.diagnostic,
|
|
1423
|
+
formatDiagnosticForCli(cliParseDiagnosticError.diagnostic, "error")
|
|
1424
|
+
);
|
|
1425
|
+
return detectCliExitCode(error);
|
|
1426
|
+
}
|
|
1427
|
+
const assetError = findFirstErrorInChain(error, AssetError);
|
|
1428
|
+
if (assetError) {
|
|
1429
|
+
const diagnostic = formatAssetErrorDiagnosticForCli(assetError);
|
|
1430
|
+
writeErrorDiagnostic(
|
|
1431
|
+
dependencies.stderr,
|
|
1432
|
+
resolveLogOptionsForError(dependencies, globalFlags, logOptions, args),
|
|
1433
|
+
diagnostic,
|
|
1434
|
+
formatDiagnosticForCli(diagnostic, "error")
|
|
1435
|
+
);
|
|
1436
|
+
return detectCliExitCode(error);
|
|
1437
|
+
}
|
|
1438
|
+
const fontValidationError = findFirstErrorInChain(error, FontValidationError);
|
|
1439
|
+
if (fontValidationError) {
|
|
1440
|
+
for (const diagnostic of formatFontValidationDiagnosticsForCli(fontValidationError)) {
|
|
1441
|
+
writeErrorDiagnostic(
|
|
1442
|
+
dependencies.stderr,
|
|
1443
|
+
resolveLogOptionsForError(dependencies, globalFlags, logOptions, args),
|
|
1444
|
+
diagnostic,
|
|
1445
|
+
formatDiagnosticForCli(diagnostic, "error")
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
return detectCliExitCode(error);
|
|
1449
|
+
}
|
|
1450
|
+
const layoutIntegrityError = findFirstErrorInChain(error, LayoutIntegrityError);
|
|
1451
|
+
if (layoutIntegrityError) {
|
|
1452
|
+
for (const diagnostic of formatLayoutIntegrityDiagnosticsForCli(layoutIntegrityError)) {
|
|
1453
|
+
writeErrorDiagnostic(
|
|
1454
|
+
dependencies.stderr,
|
|
1455
|
+
resolveLogOptionsForError(dependencies, globalFlags, logOptions, args),
|
|
1456
|
+
diagnostic,
|
|
1457
|
+
formatDiagnosticForCli(diagnostic, "error")
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
return detectCliExitCode(error);
|
|
1461
|
+
}
|
|
1462
|
+
const message = nonInteractive && isAuthError(error) ? formatNonInteractiveAuthFailure(error) : toErrorMessage(error);
|
|
1463
|
+
const errorLogOptions = resolveLogOptionsForError(dependencies, globalFlags, logOptions, args);
|
|
1464
|
+
if (errorLogOptions.format === "json") {
|
|
1465
|
+
const diagnostic = createCliErrorDiagnostic(error, message, renderGenerateHelp());
|
|
1466
|
+
writeErrorDiagnostic(
|
|
1467
|
+
dependencies.stderr,
|
|
1468
|
+
errorLogOptions,
|
|
1469
|
+
diagnostic,
|
|
1470
|
+
formatDiagnosticForCli(diagnostic, "error")
|
|
1471
|
+
);
|
|
1472
|
+
return detectCliExitCode(error);
|
|
1473
|
+
}
|
|
1474
|
+
writeLine(dependencies.stderr, `${message}
|
|
1475
|
+
${renderGenerateHelp()}`);
|
|
1476
|
+
return detectCliExitCode(error);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
async function handleValidateCommand(args, dependencies, globalFlags) {
|
|
1480
|
+
if (args.includes("--help")) {
|
|
1481
|
+
writeLine(dependencies.stdout, renderValidateHelp());
|
|
1482
|
+
return 0;
|
|
1483
|
+
}
|
|
1484
|
+
try {
|
|
1485
|
+
const flags = parseValidateFlags(args);
|
|
1486
|
+
const config = await loadCliConfig();
|
|
1487
|
+
const logOptions = resolveLogOptions(dependencies, globalFlags, flags.logFormat, config);
|
|
1488
|
+
const styleYaml = await readTextFileWithLimit(
|
|
1489
|
+
flags.inputPath,
|
|
1490
|
+
"style_yaml",
|
|
1491
|
+
STYLE_YAML_INPUT_MAX_BYTES
|
|
1492
|
+
);
|
|
1493
|
+
const result = dependencies.validateYaml({
|
|
1494
|
+
styleYaml,
|
|
1495
|
+
filePath: flags.inputPath
|
|
1496
|
+
});
|
|
1497
|
+
if (logOptions.format === "json") {
|
|
1498
|
+
const jsonPayload = !result.valid ? {
|
|
1499
|
+
valid: false,
|
|
1500
|
+
diagnostics: result.diagnostics.map(formatValidationResultDiagnostic)
|
|
1501
|
+
} : result.diagnostics && result.diagnostics.length > 0 ? {
|
|
1502
|
+
valid: true,
|
|
1503
|
+
diagnostics: result.diagnostics.map(formatValidationResultDiagnostic)
|
|
1504
|
+
} : {
|
|
1505
|
+
valid: true
|
|
1506
|
+
};
|
|
1507
|
+
writeLine(dependencies.stdout, JSON.stringify(jsonPayload));
|
|
1508
|
+
return result.valid ? 0 : 1;
|
|
1509
|
+
}
|
|
1510
|
+
if (!result.valid) {
|
|
1511
|
+
writeLine(dependencies.stdout, `invalid: ${result.diagnostics.length} violation(s)`);
|
|
1512
|
+
for (const diagnostic of result.diagnostics) {
|
|
1513
|
+
writeLine(dependencies.stdout, formatStyleYamlDiagnostic(diagnostic));
|
|
1514
|
+
}
|
|
1515
|
+
return 1;
|
|
1516
|
+
}
|
|
1517
|
+
if (!result.diagnostics || result.diagnostics.length === 0) {
|
|
1518
|
+
writeLine(dependencies.stdout, "valid");
|
|
1519
|
+
return 0;
|
|
1520
|
+
}
|
|
1521
|
+
writeLine(dependencies.stdout, `valid (${result.diagnostics.length} warning(s))`);
|
|
1522
|
+
for (const diagnostic of result.diagnostics) {
|
|
1523
|
+
writeLine(dependencies.stdout, formatStyleYamlDiagnostic(diagnostic));
|
|
1524
|
+
}
|
|
1525
|
+
return 0;
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
const cliParseDiagnosticError = findFirstErrorInChain(error, CliParseDiagnosticError);
|
|
1528
|
+
if (cliParseDiagnosticError) {
|
|
1529
|
+
writeLog(
|
|
1530
|
+
dependencies.stderr,
|
|
1531
|
+
resolveLogOptionsForError(dependencies, globalFlags, null, args),
|
|
1532
|
+
"error",
|
|
1533
|
+
"diagnostic",
|
|
1534
|
+
cliParseDiagnosticError.diagnostic,
|
|
1535
|
+
formatDiagnosticForCli(cliParseDiagnosticError.diagnostic, "error")
|
|
1536
|
+
);
|
|
1537
|
+
return detectCliExitCode(error);
|
|
1538
|
+
}
|
|
1539
|
+
const errorLogOptions = resolveLogOptionsForError(dependencies, globalFlags, null, args);
|
|
1540
|
+
if (errorLogOptions.format === "json") {
|
|
1541
|
+
const message = toErrorMessage(error);
|
|
1542
|
+
const diagnostic = createCliErrorDiagnostic(error, message, renderValidateHelp());
|
|
1543
|
+
writeErrorDiagnostic(
|
|
1544
|
+
dependencies.stderr,
|
|
1545
|
+
errorLogOptions,
|
|
1546
|
+
diagnostic,
|
|
1547
|
+
formatDiagnosticForCli(diagnostic, "error")
|
|
1548
|
+
);
|
|
1549
|
+
return detectCliExitCode(error);
|
|
1550
|
+
}
|
|
1551
|
+
writeLine(dependencies.stderr, `${toErrorMessage(error)}
|
|
1552
|
+
${renderValidateHelp()}`);
|
|
1553
|
+
return detectCliExitCode(error);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
async function handleMcpCommand(args, dependencies, globalFlags) {
|
|
1557
|
+
const [subcommand, ...flagArgs] = args;
|
|
1558
|
+
if (!subcommand || subcommand === "--help") {
|
|
1559
|
+
writeLine(dependencies.stdout, renderMcpHelp());
|
|
1560
|
+
return subcommand ? 0 : 1;
|
|
1561
|
+
}
|
|
1562
|
+
if (subcommand !== "serve") {
|
|
1563
|
+
writeLine(dependencies.stderr, renderMcpHelp());
|
|
1564
|
+
return 1;
|
|
1565
|
+
}
|
|
1566
|
+
if (flagArgs.includes("--help")) {
|
|
1567
|
+
writeLine(dependencies.stdout, renderMcpHelp());
|
|
1568
|
+
return 0;
|
|
1569
|
+
}
|
|
1570
|
+
try {
|
|
1571
|
+
const flags = parseMcpServeFlags(flagArgs);
|
|
1572
|
+
if (flags.transport === "stdio") {
|
|
1573
|
+
await dependencies.serveMcpOverStdio({
|
|
1574
|
+
authService: dependencies.authService
|
|
1575
|
+
});
|
|
1576
|
+
return 0;
|
|
1577
|
+
}
|
|
1578
|
+
const token = dependencies.env.MORPHE_MCP_TOKEN;
|
|
1579
|
+
if (!token) {
|
|
1580
|
+
throw new CliUsageError("`MORPHE_MCP_TOKEN` is required for `morphe mcp serve --transport http`.");
|
|
1581
|
+
}
|
|
1582
|
+
const handle = await dependencies.startMcpHttpServer({
|
|
1583
|
+
host: flags.host,
|
|
1584
|
+
port: flags.port,
|
|
1585
|
+
publicBaseUrl: flags.publicBaseUrl,
|
|
1586
|
+
token,
|
|
1587
|
+
authService: dependencies.authService
|
|
1588
|
+
});
|
|
1589
|
+
const logOptions = resolveLogOptions(dependencies, globalFlags);
|
|
1590
|
+
writeLog(
|
|
1591
|
+
dependencies.stderr,
|
|
1592
|
+
logOptions,
|
|
1593
|
+
"info",
|
|
1594
|
+
"mcp_http_listening",
|
|
1595
|
+
{
|
|
1596
|
+
url: `${handle.baseUrl}${MORPHE_MCP_PATH}`
|
|
1597
|
+
},
|
|
1598
|
+
`MCP HTTP server listening on ${handle.baseUrl}${MORPHE_MCP_PATH}`
|
|
1599
|
+
);
|
|
1600
|
+
await dependencies.waitForTermination(handle);
|
|
1601
|
+
return 0;
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
writeLine(dependencies.stderr, `${toErrorMessage(error)}
|
|
1604
|
+
${renderMcpHelp()}`);
|
|
1605
|
+
return detectCliExitCode(error);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
async function waitForTermination(handle) {
|
|
1609
|
+
await new Promise((resolve2, reject) => {
|
|
1610
|
+
let shuttingDown = false;
|
|
1611
|
+
const shutdown = () => {
|
|
1612
|
+
if (shuttingDown) {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
shuttingDown = true;
|
|
1616
|
+
void handle.close().then(resolve2).catch(reject).finally(() => {
|
|
1617
|
+
process.off("SIGINT", shutdown);
|
|
1618
|
+
process.off("SIGTERM", shutdown);
|
|
1619
|
+
});
|
|
1620
|
+
};
|
|
1621
|
+
process.once("SIGINT", shutdown);
|
|
1622
|
+
process.once("SIGTERM", shutdown);
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
function formatValidationResultDiagnostic(diagnostic) {
|
|
1626
|
+
return {
|
|
1627
|
+
error_type: diagnostic.errorType,
|
|
1628
|
+
location: {
|
|
1629
|
+
file: diagnostic.location.filePath,
|
|
1630
|
+
start: {
|
|
1631
|
+
line: diagnostic.location.line,
|
|
1632
|
+
column: diagnostic.location.column,
|
|
1633
|
+
offset: null
|
|
1634
|
+
},
|
|
1635
|
+
end: {
|
|
1636
|
+
line: diagnostic.location.line,
|
|
1637
|
+
column: diagnostic.location.column,
|
|
1638
|
+
offset: null
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
actual: diagnostic.message
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
function toErrorMessage(error) {
|
|
1645
|
+
if (error instanceof Error) {
|
|
1646
|
+
return error.message;
|
|
1647
|
+
}
|
|
1648
|
+
return String(error);
|
|
1649
|
+
}
|
|
1650
|
+
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
1651
|
+
const resolvedDependencies = {
|
|
1652
|
+
authService: dependencies.authService ?? createAuthService({ env: dependencies.env ?? process.env }),
|
|
1653
|
+
extractGslides: dependencies.extractGslides ?? extractGslidesFromCore,
|
|
1654
|
+
extractPptx: dependencies.extractPptx ?? extractPptxFromCore,
|
|
1655
|
+
generateGslides: dependencies.generateGslides ?? generateGslidesFromCore,
|
|
1656
|
+
generatePptx: dependencies.generatePptx ?? generatePptxFromCore,
|
|
1657
|
+
validateYaml: dependencies.validateYaml ?? validateYamlFromCore,
|
|
1658
|
+
serveMcpOverStdio: dependencies.serveMcpOverStdio ?? serveMcpOverStdio,
|
|
1659
|
+
startMcpHttpServer: dependencies.startMcpHttpServer ?? startMcpHttpServer,
|
|
1660
|
+
waitForTermination: dependencies.waitForTermination ?? waitForTermination,
|
|
1661
|
+
env: dependencies.env ?? process.env,
|
|
1662
|
+
stdin: dependencies.stdin ?? process.stdin,
|
|
1663
|
+
stdout: dependencies.stdout ?? process.stdout,
|
|
1664
|
+
stderr: dependencies.stderr ?? process.stderr
|
|
1665
|
+
};
|
|
1666
|
+
if (argv.length === 0) {
|
|
1667
|
+
writeLine(resolvedDependencies.stdout, renderCliOutput());
|
|
1668
|
+
return 0;
|
|
1669
|
+
}
|
|
1670
|
+
let parsedGlobal;
|
|
1671
|
+
try {
|
|
1672
|
+
parsedGlobal = parseGlobalFlags(argv);
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
writeLine(resolvedDependencies.stderr, toErrorMessage(error));
|
|
1675
|
+
return detectCliExitCode(error);
|
|
1676
|
+
}
|
|
1677
|
+
const parsedArgv = parsedGlobal.argv;
|
|
1678
|
+
const globalFlags = parsedGlobal.flags;
|
|
1679
|
+
const nonInteractive = resolveNonInteractiveMode(resolvedDependencies, globalFlags);
|
|
1680
|
+
if (parsedArgv.length === 0) {
|
|
1681
|
+
writeLine(resolvedDependencies.stdout, renderCliOutput());
|
|
1682
|
+
return 0;
|
|
1683
|
+
}
|
|
1684
|
+
if (parsedArgv.length === 1 && parsedArgv[0] === "--version") {
|
|
1685
|
+
writeLine(resolvedDependencies.stdout, `morphe ${CLI_VERSION}`);
|
|
1686
|
+
return 0;
|
|
1687
|
+
}
|
|
1688
|
+
if (parsedArgv.length === 1 && parsedArgv[0] === "--help") {
|
|
1689
|
+
writeLine(resolvedDependencies.stdout, renderGeneralHelp());
|
|
1690
|
+
return 0;
|
|
1691
|
+
}
|
|
1692
|
+
const [command, ...args] = parsedArgv;
|
|
1693
|
+
if (command === "auth") {
|
|
1694
|
+
return handleAuthCommand(args, resolvedDependencies, nonInteractive);
|
|
1695
|
+
}
|
|
1696
|
+
if (command === "extract") {
|
|
1697
|
+
return handleExtractCommand(args, resolvedDependencies, globalFlags, nonInteractive);
|
|
1698
|
+
}
|
|
1699
|
+
if (command === "generate") {
|
|
1700
|
+
return handleGenerateCommand(args, resolvedDependencies, globalFlags, nonInteractive);
|
|
1701
|
+
}
|
|
1702
|
+
if (command === "validate") {
|
|
1703
|
+
return handleValidateCommand(args, resolvedDependencies, globalFlags);
|
|
1704
|
+
}
|
|
1705
|
+
if (command === "themes") {
|
|
1706
|
+
return handleThemesCommand(args, resolvedDependencies);
|
|
1707
|
+
}
|
|
1708
|
+
if (command === "mcp") {
|
|
1709
|
+
return handleMcpCommand(args, resolvedDependencies, globalFlags);
|
|
1710
|
+
}
|
|
1711
|
+
writeLine(resolvedDependencies.stderr, `Unknown command: ${command}`);
|
|
1712
|
+
return 1;
|
|
1713
|
+
}
|
|
1714
|
+
async function run() {
|
|
1715
|
+
process.exitCode = await runCli();
|
|
1716
|
+
}
|
|
1717
|
+
var entryUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : void 0;
|
|
1718
|
+
if (entryUrl === import.meta.url) {
|
|
1719
|
+
void run();
|
|
1720
|
+
}
|
|
1721
|
+
export {
|
|
1722
|
+
renderCliOutput,
|
|
1723
|
+
run,
|
|
1724
|
+
runCli
|
|
1725
|
+
};
|