cdx-chores 0.0.7

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.mjs ADDED
@@ -0,0 +1,787 @@
1
+ import { Command } from "commander";
2
+ import { mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, extname, join, resolve } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { confirm, input, select } from "@inquirer/prompts";
6
+
7
+ //#region src/cli/csv.ts
8
+ function normalizeLineBreaks(value) {
9
+ return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
10
+ }
11
+ function quoteCsvField(value) {
12
+ const text = value === null || value === void 0 ? "" : typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" ? String(value) : JSON.stringify(value);
13
+ if (/["\n,]/.test(text)) return `"${text.replaceAll("\"", "\"\"")}"`;
14
+ return text;
15
+ }
16
+ function stringifyCsv(rows) {
17
+ if (rows.length === 0) return "";
18
+ const headers = [];
19
+ const seen = /* @__PURE__ */ new Set();
20
+ for (const row of rows) for (const key of Object.keys(row)) if (!seen.has(key)) {
21
+ seen.add(key);
22
+ headers.push(key);
23
+ }
24
+ return `${[headers.map((header) => quoteCsvField(header)).join(","), ...rows.map((row) => headers.map((header) => quoteCsvField(row[header])).join(","))].join("\n")}\n`;
25
+ }
26
+ function parseCsv(text) {
27
+ const source = normalizeLineBreaks(text);
28
+ if (source.length === 0) return [];
29
+ const rows = [];
30
+ let row = [];
31
+ let field = "";
32
+ let inQuotes = false;
33
+ for (let index = 0; index < source.length; index += 1) {
34
+ const char = source[index];
35
+ if (inQuotes) {
36
+ if (char === "\"") if (source[index + 1] === "\"") {
37
+ field += "\"";
38
+ index += 1;
39
+ } else inQuotes = false;
40
+ else field += char;
41
+ continue;
42
+ }
43
+ if (char === "\"") {
44
+ inQuotes = true;
45
+ continue;
46
+ }
47
+ if (char === ",") {
48
+ row.push(field);
49
+ field = "";
50
+ continue;
51
+ }
52
+ if (char === "\n") {
53
+ row.push(field);
54
+ rows.push(row);
55
+ row = [];
56
+ field = "";
57
+ continue;
58
+ }
59
+ field += char;
60
+ }
61
+ row.push(field);
62
+ if (row.length > 1 || row[0] !== "" || rows.length === 0) rows.push(row);
63
+ return rows;
64
+ }
65
+ function csvRowsToObjects(rows) {
66
+ if (rows.length === 0) return [];
67
+ const [headerRow, ...dataRows] = rows;
68
+ const headers = headerRow.map((header, index) => (index === 0 ? header.replace(/^\uFEFF/, "") : header).trim());
69
+ return dataRows.filter((row) => row.some((value) => value.length > 0)).map((row) => {
70
+ const record = {};
71
+ headers.forEach((header, index) => {
72
+ if (header.length === 0) return;
73
+ record[header] = row[index] ?? "";
74
+ });
75
+ return record;
76
+ });
77
+ }
78
+
79
+ //#endregion
80
+ //#region src/cli/errors.ts
81
+ var CliError = class extends Error {
82
+ exitCode;
83
+ code;
84
+ constructor(message, options = {}) {
85
+ super(message);
86
+ this.name = "CliError";
87
+ this.exitCode = options.exitCode ?? 1;
88
+ this.code = options.code ?? "CLI_ERROR";
89
+ }
90
+ };
91
+ function toCliError(error) {
92
+ if (error instanceof CliError) return error;
93
+ return new CliError(error instanceof Error ? error.message : String(error));
94
+ }
95
+
96
+ //#endregion
97
+ //#region src/cli/process.ts
98
+ async function execCommand(command, args, options = {}) {
99
+ return await new Promise((resolve, reject) => {
100
+ const child = spawn(command, args, {
101
+ cwd: options.cwd,
102
+ stdio: [
103
+ "ignore",
104
+ "pipe",
105
+ "pipe"
106
+ ]
107
+ });
108
+ let stdout = "";
109
+ let stderr = "";
110
+ child.stdout.setEncoding("utf8");
111
+ child.stderr.setEncoding("utf8");
112
+ child.stdout.on("data", (chunk) => {
113
+ stdout += chunk;
114
+ });
115
+ child.stderr.on("data", (chunk) => {
116
+ stderr += chunk;
117
+ });
118
+ child.on("error", (error) => {
119
+ reject(error);
120
+ });
121
+ child.on("close", (code, signal) => {
122
+ resolve({
123
+ ok: code === 0,
124
+ code,
125
+ signal,
126
+ stdout,
127
+ stderr
128
+ });
129
+ });
130
+ });
131
+ }
132
+
133
+ //#endregion
134
+ //#region src/cli/deps.ts
135
+ function installHintFor(command, platform) {
136
+ if (platform === "darwin") return command === "pandoc" ? "brew install pandoc" : "brew install ffmpeg";
137
+ if (platform === "win32") return command === "pandoc" ? "Install via winget/choco (example: winget install --id JohnMacFarlane.Pandoc)" : "Install via winget/choco (example: winget install Gyan.FFmpeg)";
138
+ return command === "pandoc" ? "Install via your package manager (examples: apt/dnf/pacman) for pandoc" : "Install via your package manager (examples: apt/dnf/pacman) for ffmpeg";
139
+ }
140
+ function parseVersion(command, output) {
141
+ const firstLine = output.split(/\r?\n/, 1)[0]?.trim() ?? "";
142
+ if (!firstLine) return null;
143
+ if (command === "pandoc") return firstLine.match(/^pandoc\s+([^\s]+)/i)?.[1] ?? firstLine;
144
+ if (command === "ffmpeg") return firstLine.match(/^ffmpeg version\s+([^\s]+)/i)?.[1] ?? firstLine;
145
+ return firstLine;
146
+ }
147
+ async function inspectCommand(command, platform) {
148
+ try {
149
+ const result = await execCommand(command, command === "pandoc" ? ["--version"] : ["-version"]);
150
+ return {
151
+ name: command,
152
+ available: result.ok,
153
+ version: result.ok ? parseVersion(command, result.stdout || result.stderr) : null,
154
+ installHint: installHintFor(command, platform)
155
+ };
156
+ } catch (error) {
157
+ const message = error instanceof Error ? error.message : String(error);
158
+ if (/ENOENT/i.test(message)) return {
159
+ name: command,
160
+ available: false,
161
+ version: null,
162
+ installHint: installHintFor(command, platform)
163
+ };
164
+ throw new CliError(`Failed to inspect dependency '${command}': ${message}`, {
165
+ code: "DEPENDENCY_CHECK_FAILED",
166
+ exitCode: 2
167
+ });
168
+ }
169
+ }
170
+ async function requireCommandAvailable(command, platform) {
171
+ const status = await inspectCommand(command, platform);
172
+ if (status.available) return;
173
+ throw new CliError(`Missing required dependency: ${command}. Install suggestion: ${status.installHint}`, {
174
+ code: "DEPENDENCY_MISSING",
175
+ exitCode: 2
176
+ });
177
+ }
178
+
179
+ //#endregion
180
+ //#region src/cli/fs-utils.ts
181
+ function resolveFromCwd(runtime, filePath) {
182
+ return resolve(runtime.cwd, filePath);
183
+ }
184
+ async function readTextFileRequired(path) {
185
+ try {
186
+ return await readFile(path, "utf8");
187
+ } catch (error) {
188
+ throw new CliError(`Failed to read file: ${path} (${error instanceof Error ? error.message : String(error)})`, {
189
+ code: "FILE_READ_ERROR",
190
+ exitCode: 2
191
+ });
192
+ }
193
+ }
194
+ async function ensureParentDir(path) {
195
+ await mkdir(dirname(path), { recursive: true });
196
+ }
197
+ async function writeTextFileSafe(path, content, options = {}) {
198
+ const overwrite = options.overwrite ?? false;
199
+ try {
200
+ if (!overwrite) {
201
+ await stat(path);
202
+ throw new CliError(`Output file already exists: ${path}. Use --overwrite to replace it.`, {
203
+ code: "OUTPUT_EXISTS",
204
+ exitCode: 2
205
+ });
206
+ }
207
+ } catch (error) {
208
+ if (error instanceof CliError) throw error;
209
+ }
210
+ await ensureParentDir(path);
211
+ try {
212
+ await writeFile(path, content, "utf8");
213
+ } catch (error) {
214
+ throw new CliError(`Failed to write file: ${path} (${error instanceof Error ? error.message : String(error)})`, {
215
+ code: "FILE_WRITE_ERROR",
216
+ exitCode: 2
217
+ });
218
+ }
219
+ }
220
+ function defaultOutputPath(inputPath, nextExtension) {
221
+ const extension = nextExtension.startsWith(".") ? nextExtension : `.${nextExtension}`;
222
+ const currentExt = extname(inputPath);
223
+ if (currentExt.length === 0) return `${inputPath}${extension}`;
224
+ return `${inputPath.slice(0, -currentExt.length)}${extension}`;
225
+ }
226
+ function slugifyName(value) {
227
+ return value.normalize("NFKD").replace(/[^\x00-\x7F]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "file";
228
+ }
229
+ function formatFileDateTime(date) {
230
+ return `${date.getUTCFullYear()}${String(date.getUTCMonth() + 1).padStart(2, "0")}${String(date.getUTCDate()).padStart(2, "0")}-${String(date.getUTCHours()).padStart(2, "0")}${String(date.getUTCMinutes()).padStart(2, "0")}${String(date.getUTCSeconds()).padStart(2, "0")}`;
231
+ }
232
+ async function planBatchRename(runtime, directoryInput, options = {}) {
233
+ const directoryPath = resolveFromCwd(runtime, directoryInput);
234
+ const files = (await readdir(directoryPath, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
235
+ const prefix = slugifyName(options.prefix?.trim() || "file");
236
+ const plannedTargets = /* @__PURE__ */ new Set();
237
+ const sourcePaths = /* @__PURE__ */ new Set();
238
+ const plans = [];
239
+ for (const name of files) {
240
+ const sourcePath = join(directoryPath, name);
241
+ sourcePaths.add(sourcePath);
242
+ let fileStats;
243
+ try {
244
+ fileStats = await stat(sourcePath);
245
+ } catch {
246
+ continue;
247
+ }
248
+ const ext = extname(name).toLowerCase();
249
+ const slug = slugifyName(basename(name, extname(name))).slice(0, 48);
250
+ const dt = formatFileDateTime(fileStats.mtime ?? options.now ?? runtime.now());
251
+ let counter = 0;
252
+ let candidatePath = "";
253
+ while (true) {
254
+ candidatePath = join(directoryPath, `${prefix}-${dt}-${slug}${counter === 0 ? "" : `-${String(counter).padStart(2, "0")}`}${ext}`);
255
+ if (!plannedTargets.has(candidatePath)) break;
256
+ counter += 1;
257
+ }
258
+ plannedTargets.add(candidatePath);
259
+ plans.push({
260
+ fromPath: sourcePath,
261
+ toPath: candidatePath,
262
+ changed: sourcePath !== candidatePath
263
+ });
264
+ }
265
+ return {
266
+ directoryPath,
267
+ plans
268
+ };
269
+ }
270
+ async function applyPlannedRenames(plans) {
271
+ const changes = plans.filter((plan) => plan.changed);
272
+ if (changes.length === 0) return;
273
+ const tempMoves = [];
274
+ for (let index = 0; index < changes.length; index += 1) {
275
+ const plan = changes[index];
276
+ const tempPath = `${plan.fromPath}.cdx-chores-tmp-${process.pid}-${index}`;
277
+ await rename(plan.fromPath, tempPath);
278
+ tempMoves.push({
279
+ tempPath,
280
+ finalPath: plan.toPath
281
+ });
282
+ }
283
+ for (const move of tempMoves) await rename(move.tempPath, move.finalPath);
284
+ }
285
+
286
+ //#endregion
287
+ //#region src/cli/actions.ts
288
+ function printLine(stream, line = "") {
289
+ stream.write(`${line}\n`);
290
+ }
291
+ function assertNonEmpty(value, label) {
292
+ const next = value?.trim() ?? "";
293
+ if (!next) throw new CliError(`${label} is required.`, {
294
+ code: "INVALID_INPUT",
295
+ exitCode: 2
296
+ });
297
+ return next;
298
+ }
299
+ async function ensureFileExists(path, label) {
300
+ try {
301
+ if (!(await stat(path)).isFile()) throw new Error("not a file");
302
+ } catch {
303
+ throw new CliError(`${label} file not found: ${path}`, {
304
+ code: "FILE_NOT_FOUND",
305
+ exitCode: 2
306
+ });
307
+ }
308
+ }
309
+ async function actionDoctor(runtime, options = {}) {
310
+ const [pandoc, ffmpeg] = await Promise.all([inspectCommand("pandoc", runtime.platform), inspectCommand("ffmpeg", runtime.platform)]);
311
+ const capabilities = {
312
+ "md.to-docx": pandoc.available,
313
+ "video.convert": ffmpeg.available,
314
+ "video.resize": ffmpeg.available,
315
+ "video.gif": ffmpeg.available
316
+ };
317
+ if (options.json) {
318
+ const payload = {
319
+ generatedAt: runtime.now().toISOString(),
320
+ platform: runtime.platform,
321
+ nodeVersion: process.version,
322
+ tools: {
323
+ pandoc,
324
+ ffmpeg
325
+ },
326
+ capabilities
327
+ };
328
+ printLine(runtime.stdout, JSON.stringify(payload, null, 2));
329
+ return;
330
+ }
331
+ printLine(runtime.stdout, "cdx-chores doctor");
332
+ printLine(runtime.stdout, `Platform: ${runtime.platform}`);
333
+ printLine(runtime.stdout, `Node.js: ${process.version}`);
334
+ printLine(runtime.stdout);
335
+ for (const item of [pandoc, ffmpeg]) {
336
+ printLine(runtime.stdout, `- ${item.name}: ${item.available ? `available (${item.version ?? "unknown version"})` : "missing"}`);
337
+ if (!item.available) printLine(runtime.stdout, ` Install suggestion: ${item.installHint}`);
338
+ }
339
+ printLine(runtime.stdout);
340
+ printLine(runtime.stdout, "Capabilities:");
341
+ for (const [capability, available] of Object.entries(capabilities)) printLine(runtime.stdout, `- ${capability}: ${available ? "available" : "unavailable"}`);
342
+ }
343
+ function normalizeRowsFromJson(input) {
344
+ if (Array.isArray(input)) {
345
+ if (input.length === 0) return [];
346
+ if (input.every((item) => item !== null && typeof item === "object" && !Array.isArray(item))) return input;
347
+ return input.map((item) => ({ value: item }));
348
+ }
349
+ if (input !== null && typeof input === "object") return [input];
350
+ return [{ value: input }];
351
+ }
352
+ async function actionJsonToCsv(runtime, options) {
353
+ const inputPath = resolveFromCwd(runtime, assertNonEmpty(options.input, "Input path"));
354
+ const outputPath = resolveFromCwd(runtime, options.output?.trim() || defaultOutputPath(inputPath, ".csv"));
355
+ await ensureFileExists(inputPath, "Input");
356
+ const raw = await readTextFileRequired(inputPath);
357
+ let parsed;
358
+ try {
359
+ parsed = JSON.parse(raw);
360
+ } catch (error) {
361
+ throw new CliError(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`, {
362
+ code: "INVALID_JSON",
363
+ exitCode: 2
364
+ });
365
+ }
366
+ const rows = normalizeRowsFromJson(parsed);
367
+ await writeTextFileSafe(outputPath, stringifyCsv(rows), { overwrite: options.overwrite });
368
+ printLine(runtime.stdout, `Wrote CSV: ${outputPath}`);
369
+ printLine(runtime.stdout, `Rows: ${rows.length}`);
370
+ }
371
+ async function actionCsvToJson(runtime, options) {
372
+ const inputPath = resolveFromCwd(runtime, assertNonEmpty(options.input, "Input path"));
373
+ const outputPath = resolveFromCwd(runtime, options.output?.trim() || defaultOutputPath(inputPath, ".json"));
374
+ await ensureFileExists(inputPath, "Input");
375
+ const records = csvRowsToObjects(parseCsv(await readTextFileRequired(inputPath)));
376
+ await writeTextFileSafe(outputPath, `${JSON.stringify(records, null, options.pretty ? 2 : 0)}\n`, { overwrite: options.overwrite });
377
+ printLine(runtime.stdout, `Wrote JSON: ${outputPath}`);
378
+ printLine(runtime.stdout, `Rows: ${records.length}`);
379
+ }
380
+ async function actionMdToDocx(runtime, options) {
381
+ const inputPath = resolveFromCwd(runtime, assertNonEmpty(options.input, "Input path"));
382
+ const outputPath = resolveFromCwd(runtime, options.output?.trim() || defaultOutputPath(inputPath, ".docx"));
383
+ await ensureFileExists(inputPath, "Input");
384
+ await requireCommandAvailable("pandoc", runtime.platform);
385
+ if (!options.overwrite) try {
386
+ await stat(outputPath);
387
+ throw new CliError(`Output file already exists: ${outputPath}. Use --overwrite to replace it.`, {
388
+ code: "OUTPUT_EXISTS",
389
+ exitCode: 2
390
+ });
391
+ } catch (error) {
392
+ if (error instanceof CliError) throw error;
393
+ }
394
+ const result = await execCommand("pandoc", [
395
+ inputPath,
396
+ "-o",
397
+ outputPath
398
+ ], { cwd: runtime.cwd });
399
+ if (!result.ok) throw new CliError(`pandoc failed (${result.code ?? "unknown"}): ${result.stderr || result.stdout}`.trim(), {
400
+ code: "PROCESS_FAILED",
401
+ exitCode: 1
402
+ });
403
+ printLine(runtime.stdout, `Wrote DOCX: ${outputPath}`);
404
+ }
405
+ function formatRenamePreviewLine(plan) {
406
+ const fromName = basename(plan.fromPath);
407
+ const toName = basename(plan.toPath);
408
+ return plan.changed ? `- ${fromName} -> ${toName}` : `- ${fromName} (unchanged)`;
409
+ }
410
+ async function actionRenameBatch(runtime, options) {
411
+ const { directoryPath, plans } = await planBatchRename(runtime, assertNonEmpty(options.directory, "Directory path"), {
412
+ prefix: options.prefix,
413
+ now: runtime.now()
414
+ });
415
+ const totalCount = plans.length;
416
+ const changedCount = plans.filter((plan) => plan.changed).length;
417
+ printLine(runtime.stdout, `Directory: ${directoryPath}`);
418
+ printLine(runtime.stdout, `Files found: ${totalCount}`);
419
+ printLine(runtime.stdout, `Files to rename: ${changedCount}`);
420
+ printLine(runtime.stdout);
421
+ for (const plan of plans) printLine(runtime.stdout, formatRenamePreviewLine(plan));
422
+ if (options.dryRun ?? false) {
423
+ printLine(runtime.stdout);
424
+ printLine(runtime.stdout, "Dry run only. No files were renamed.");
425
+ return {
426
+ changedCount,
427
+ totalCount,
428
+ directoryPath
429
+ };
430
+ }
431
+ await applyPlannedRenames(plans);
432
+ printLine(runtime.stdout);
433
+ printLine(runtime.stdout, `Renamed ${changedCount} file(s).`);
434
+ return {
435
+ changedCount,
436
+ totalCount,
437
+ directoryPath
438
+ };
439
+ }
440
+ async function runFfmpeg(runtime, args) {
441
+ await requireCommandAvailable("ffmpeg", runtime.platform);
442
+ const result = await execCommand("ffmpeg", args, { cwd: runtime.cwd });
443
+ if (!result.ok) throw new CliError(`ffmpeg failed (${result.code ?? "unknown"}): ${result.stderr || result.stdout}`.trim(), {
444
+ code: "PROCESS_FAILED",
445
+ exitCode: 1
446
+ });
447
+ }
448
+ async function actionVideoConvert(runtime, options) {
449
+ const inputPath = resolve(runtime.cwd, assertNonEmpty(options.input, "Input path"));
450
+ const outputPath = resolve(runtime.cwd, assertNonEmpty(options.output, "Output path"));
451
+ await ensureFileExists(inputPath, "Input");
452
+ await runFfmpeg(runtime, [
453
+ options.overwrite ? "-y" : "-n",
454
+ "-i",
455
+ inputPath,
456
+ outputPath
457
+ ]);
458
+ printLine(runtime.stdout, `Wrote video: ${outputPath}`);
459
+ }
460
+ async function actionVideoResize(runtime, options) {
461
+ const inputPath = resolve(runtime.cwd, assertNonEmpty(options.input, "Input path"));
462
+ const outputPath = resolve(runtime.cwd, assertNonEmpty(options.output, "Output path"));
463
+ if (!Number.isFinite(options.width) || options.width <= 0) throw new CliError("Width must be a positive number.", {
464
+ code: "INVALID_INPUT",
465
+ exitCode: 2
466
+ });
467
+ if (!Number.isFinite(options.height) || options.height <= 0) throw new CliError("Height must be a positive number.", {
468
+ code: "INVALID_INPUT",
469
+ exitCode: 2
470
+ });
471
+ await ensureFileExists(inputPath, "Input");
472
+ await runFfmpeg(runtime, [
473
+ options.overwrite ? "-y" : "-n",
474
+ "-i",
475
+ inputPath,
476
+ "-vf",
477
+ `scale=${Math.trunc(options.width)}:${Math.trunc(options.height)}`,
478
+ outputPath
479
+ ]);
480
+ printLine(runtime.stdout, `Wrote resized video: ${outputPath}`);
481
+ }
482
+ async function actionVideoGif(runtime, options) {
483
+ const inputPath = resolve(runtime.cwd, assertNonEmpty(options.input, "Input path"));
484
+ const outputPath = resolve(runtime.cwd, options.output?.trim() || defaultOutputPath(inputPath, ".gif"));
485
+ await ensureFileExists(inputPath, "Input");
486
+ const fps = Number.isFinite(options.fps) && (options.fps ?? 0) > 0 ? Math.trunc(options.fps) : 10;
487
+ const width = Number.isFinite(options.width) && (options.width ?? 0) > 0 ? Math.trunc(options.width) : 480;
488
+ await runFfmpeg(runtime, [
489
+ options.overwrite ? "-y" : "-n",
490
+ "-i",
491
+ inputPath,
492
+ "-vf",
493
+ `fps=${fps},scale=${width}:-1:flags=lanczos`,
494
+ outputPath
495
+ ]);
496
+ printLine(runtime.stdout, `Wrote GIF: ${outputPath}`);
497
+ }
498
+ async function actionDeferred(runtime, label) {
499
+ throw new CliError(`${label} is not implemented in the initial launch phase yet.`, {
500
+ code: "DEFERRED_FEATURE",
501
+ exitCode: 2
502
+ });
503
+ }
504
+
505
+ //#endregion
506
+ //#region src/cli/interactive.ts
507
+ async function promptPath(message) {
508
+ return await input({
509
+ message,
510
+ validate: (value) => value.trim().length > 0 ? true : "Required"
511
+ });
512
+ }
513
+ async function runInteractiveMode(runtime) {
514
+ const action = await select({
515
+ message: "Choose a command",
516
+ choices: [
517
+ {
518
+ name: "doctor",
519
+ value: "doctor",
520
+ description: "Check dependencies and capabilities"
521
+ },
522
+ {
523
+ name: "data json-to-csv",
524
+ value: "data:json-to-csv"
525
+ },
526
+ {
527
+ name: "data csv-to-json",
528
+ value: "data:csv-to-json"
529
+ },
530
+ {
531
+ name: "md to-docx",
532
+ value: "md:to-docx"
533
+ },
534
+ {
535
+ name: "rename batch",
536
+ value: "rename:batch"
537
+ },
538
+ {
539
+ name: "video convert",
540
+ value: "video:convert"
541
+ },
542
+ {
543
+ name: "video resize",
544
+ value: "video:resize"
545
+ },
546
+ {
547
+ name: "video gif",
548
+ value: "video:gif"
549
+ },
550
+ {
551
+ name: "cancel",
552
+ value: "cancel"
553
+ }
554
+ ]
555
+ });
556
+ if (action === "cancel") {
557
+ runtime.stdout.write("Cancelled.\n");
558
+ return;
559
+ }
560
+ if (action === "doctor") {
561
+ await actionDoctor(runtime, { json: await confirm({
562
+ message: "Output as JSON?",
563
+ default: false
564
+ }) });
565
+ return;
566
+ }
567
+ if (action === "data:json-to-csv") {
568
+ const inputPath = await promptPath("Input JSON file");
569
+ const outputPath = await input({ message: "Output CSV file (optional)" });
570
+ const overwrite = await confirm({
571
+ message: "Overwrite if exists?",
572
+ default: false
573
+ });
574
+ await actionJsonToCsv(runtime, {
575
+ input: inputPath,
576
+ output: outputPath || void 0,
577
+ overwrite
578
+ });
579
+ return;
580
+ }
581
+ if (action === "data:csv-to-json") {
582
+ const inputPath = await promptPath("Input CSV file");
583
+ const outputPath = await input({ message: "Output JSON file (optional)" });
584
+ const pretty = await confirm({
585
+ message: "Pretty-print JSON?",
586
+ default: true
587
+ });
588
+ const overwrite = await confirm({
589
+ message: "Overwrite if exists?",
590
+ default: false
591
+ });
592
+ await actionCsvToJson(runtime, {
593
+ input: inputPath,
594
+ output: outputPath || void 0,
595
+ pretty,
596
+ overwrite
597
+ });
598
+ return;
599
+ }
600
+ if (action === "md:to-docx") {
601
+ const inputPath = await promptPath("Input Markdown file");
602
+ const outputPath = await input({ message: "Output DOCX file (optional)" });
603
+ const overwrite = await confirm({
604
+ message: "Overwrite if exists?",
605
+ default: false
606
+ });
607
+ await actionMdToDocx(runtime, {
608
+ input: inputPath,
609
+ output: outputPath || void 0,
610
+ overwrite
611
+ });
612
+ return;
613
+ }
614
+ if (action === "rename:batch") {
615
+ const directory = await promptPath("Target directory");
616
+ const prefix = await input({
617
+ message: "Filename prefix",
618
+ default: "file"
619
+ });
620
+ const dryRun = await confirm({
621
+ message: "Dry run only?",
622
+ default: true
623
+ });
624
+ const result = await actionRenameBatch(runtime, {
625
+ directory,
626
+ prefix,
627
+ dryRun
628
+ });
629
+ if (!dryRun && result.changedCount > 0) return;
630
+ if (dryRun && result.changedCount > 0) {
631
+ if (await confirm({
632
+ message: "Apply these renames now?",
633
+ default: false
634
+ })) await actionRenameBatch(runtime, {
635
+ directory,
636
+ prefix,
637
+ dryRun: false
638
+ });
639
+ }
640
+ return;
641
+ }
642
+ if (action === "video:convert") {
643
+ await actionVideoConvert(runtime, {
644
+ input: await promptPath("Input video file"),
645
+ output: await promptPath("Output video file"),
646
+ overwrite: await confirm({
647
+ message: "Overwrite if exists?",
648
+ default: false
649
+ })
650
+ });
651
+ return;
652
+ }
653
+ if (action === "video:resize") {
654
+ await actionVideoResize(runtime, {
655
+ input: await promptPath("Input video file"),
656
+ output: await promptPath("Output video file"),
657
+ width: Number(await promptPath("Width (px)")),
658
+ height: Number(await promptPath("Height (px)")),
659
+ overwrite: await confirm({
660
+ message: "Overwrite if exists?",
661
+ default: false
662
+ })
663
+ });
664
+ return;
665
+ }
666
+ if (action === "video:gif") {
667
+ const inputPath = await promptPath("Input video file");
668
+ const outputPath = await input({ message: "Output GIF file (optional)" });
669
+ const widthInput = await input({
670
+ message: "Width in px (optional)",
671
+ default: "480"
672
+ });
673
+ const fpsInput = await input({
674
+ message: "FPS (optional)",
675
+ default: "10"
676
+ });
677
+ const overwrite = await confirm({
678
+ message: "Overwrite if exists?",
679
+ default: false
680
+ });
681
+ await actionVideoGif(runtime, {
682
+ input: inputPath,
683
+ output: outputPath || void 0,
684
+ width: widthInput.trim() ? Number(widthInput) : void 0,
685
+ fps: fpsInput.trim() ? Number(fpsInput) : void 0,
686
+ overwrite
687
+ });
688
+ }
689
+ }
690
+
691
+ //#endregion
692
+ //#region src/command.ts
693
+ function createRuntime(options) {
694
+ return {
695
+ cwd: options.cwd ?? process.cwd(),
696
+ now: options.now ?? (() => /* @__PURE__ */ new Date()),
697
+ platform: options.platform ?? process.platform,
698
+ stdout: options.stdout ?? process.stdout,
699
+ stderr: options.stderr ?? process.stderr,
700
+ stdin: options.stdin ?? process.stdin
701
+ };
702
+ }
703
+ function applyCommonFileOptions(command) {
704
+ command.option("-o, --output <path>", "Output file path").option("--overwrite", "Overwrite output file if it already exists", false);
705
+ }
706
+ async function runCli(argv = process.argv, runtime = {}) {
707
+ const cliRuntime = createRuntime(runtime);
708
+ if (argv.slice(2).length === 0) {
709
+ if (!process.stdin.isTTY) {
710
+ cliRuntime.stderr.write("No arguments provided and interactive mode requires a TTY. Use --help for commands.\n");
711
+ process.exitCode = 2;
712
+ return;
713
+ }
714
+ try {
715
+ await runInteractiveMode(cliRuntime);
716
+ } catch (error) {
717
+ const cliError = toCliError(error);
718
+ cliRuntime.stderr.write(`${cliError.message}\n`);
719
+ process.exitCode = cliError.exitCode;
720
+ }
721
+ return;
722
+ }
723
+ const program = new Command();
724
+ program.name("cdx-chores").description("CLI chores toolkit for file/media/document workflow helpers").showHelpAfterError().version("0.0.1");
725
+ program.command("interactive").description("Start interactive mode").action(async () => {
726
+ await runInteractiveMode(cliRuntime);
727
+ });
728
+ program.command("doctor").description("Check tool availability and current feature capabilities").option("--json", "Output machine-readable JSON", false).action(async (options) => {
729
+ await actionDoctor(cliRuntime, { json: options.json });
730
+ });
731
+ const dataCommand = program.command("data").description("Data conversion utilities");
732
+ applyCommonFileOptions(dataCommand.command("json-to-csv").description("Convert JSON file to CSV").requiredOption("-i, --input <path>", "Input JSON file").action(async (options) => {
733
+ await actionJsonToCsv(cliRuntime, options);
734
+ }));
735
+ dataCommand.command("csv-to-json").description("Convert CSV file to JSON").requiredOption("-i, --input <path>", "Input CSV file").option("-o, --output <path>", "Output JSON file path").option("--overwrite", "Overwrite output file if it already exists", false).option("--pretty", "Pretty-print JSON output", false).action(async (options) => {
736
+ await actionCsvToJson(cliRuntime, options);
737
+ });
738
+ applyCommonFileOptions(program.command("md").description("Markdown utilities").command("to-docx").description("Convert Markdown to DOCX using pandoc").requiredOption("-i, --input <path>", "Input Markdown file").action(async (options) => {
739
+ await actionMdToDocx(cliRuntime, options);
740
+ }));
741
+ program.command("docx").description("DOCX utilities").command("to-pdf").description("Convert DOCX to PDF (deferred)").action(async () => {
742
+ await actionDeferred(cliRuntime, "docx to-pdf");
743
+ });
744
+ const pdfCommand = program.command("pdf").description("PDF utilities (deferred in initial phase)");
745
+ for (const [name, description] of [
746
+ ["to-images", "Convert PDF pages to images (deferred)"],
747
+ ["from-images", "Build PDF from image sequence (deferred)"],
748
+ ["merge", "Merge PDF files (deferred)"],
749
+ ["split", "Split PDF file (deferred)"]
750
+ ]) pdfCommand.command(name).description(description).action(async () => {
751
+ await actionDeferred(cliRuntime, `pdf ${name}`);
752
+ });
753
+ program.command("rename").description("Rename helpers").command("batch").description("Batch rename files in a directory").argument("<directory>", "Target directory").option("--prefix <value>", "Filename prefix", "file").option("--dry-run", "Preview rename plan only", false).action(async (directory, options) => {
754
+ await actionRenameBatch(cliRuntime, {
755
+ directory,
756
+ prefix: options.prefix,
757
+ dryRun: options.dryRun
758
+ });
759
+ });
760
+ program.command("batch-rename").description("Alias for `rename batch`").argument("<directory>", "Target directory").option("--prefix <value>", "Filename prefix", "file").option("--dry-run", "Preview rename plan only", false).action(async (directory, options) => {
761
+ await actionRenameBatch(cliRuntime, {
762
+ directory,
763
+ prefix: options.prefix,
764
+ dryRun: options.dryRun
765
+ });
766
+ });
767
+ const videoCommand = program.command("video").description("Video utilities (ffmpeg-backed)");
768
+ videoCommand.command("convert").description("Convert a video file to another format via ffmpeg").requiredOption("-i, --input <path>", "Input video file").requiredOption("-o, --output <path>", "Output video file").option("--overwrite", "Overwrite output file if it already exists", false).action(async (options) => {
769
+ await actionVideoConvert(cliRuntime, options);
770
+ });
771
+ videoCommand.command("resize").description("Resize video dimensions via ffmpeg").requiredOption("-i, --input <path>", "Input video file").requiredOption("-o, --output <path>", "Output video file").requiredOption("--width <px>", "Output width", (value) => Number(value)).requiredOption("--height <px>", "Output height", (value) => Number(value)).option("--overwrite", "Overwrite output file if it already exists", false).action(async (options) => {
772
+ await actionVideoResize(cliRuntime, options);
773
+ });
774
+ videoCommand.command("gif").description("Convert video to GIF via ffmpeg").requiredOption("-i, --input <path>", "Input video file").option("-o, --output <path>", "Output GIF file path").option("--width <px>", "GIF width", (value) => Number(value)).option("--fps <value>", "GIF frames per second", (value) => Number(value)).option("--overwrite", "Overwrite output file if it already exists", false).action(async (options) => {
775
+ await actionVideoGif(cliRuntime, options);
776
+ });
777
+ try {
778
+ await program.parseAsync(argv);
779
+ } catch (error) {
780
+ const cliError = toCliError(error);
781
+ cliRuntime.stderr.write(`${cliError.message}\n`);
782
+ process.exitCode = cliError.exitCode;
783
+ }
784
+ }
785
+
786
+ //#endregion
787
+ export { runCli };