@dev-pi2pie/word-counter 0.1.0-canary.4 → 0.1.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/esm/bin.mjs CHANGED
@@ -1,37 +1,759 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command, Option } from "commander";
4
- import { readFileSync } from "node:fs";
5
- import { parseDocument } from "yaml";
6
- import { extname, relative, resolve } from "node:path";
4
+ import { closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, statSync } from "node:fs";
5
+ import { basename, dirname, extname, join, relative, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
7
  import { readFile, readdir, stat } from "node:fs/promises";
8
+ import { parseDocument } from "yaml";
9
+
10
+ //#region \0rolldown/runtime.js
11
+ var __create = Object.create;
12
+ var __defProp = Object.defineProperty;
13
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
14
+ var __getOwnPropNames = Object.getOwnPropertyNames;
15
+ var __getProtoOf = Object.getPrototypeOf;
16
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
17
+ var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
21
+ key = keys[i];
22
+ if (!__hasOwnProp.call(to, key) && key !== except) {
23
+ __defProp(to, key, {
24
+ get: ((k) => from[k]).bind(null, key),
25
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
26
+ });
27
+ }
28
+ }
29
+ }
30
+ return to;
31
+ };
32
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
33
+ value: mod,
34
+ enumerable: true
35
+ }) : target, mod));
36
+
37
+ //#endregion
38
+ //#region src/cli/debug/channel.ts
39
+ const NOOP_CLOSE = async () => {};
40
+ function shouldEmitAtVerbosity(channelVerbosity, eventVerbosity) {
41
+ return channelVerbosity === "verbose" || eventVerbosity === "compact";
42
+ }
43
+ function formatTimestampPart(value) {
44
+ return String(value).padStart(2, "0");
45
+ }
46
+ function formatDebugReportTimestamp(now) {
47
+ return [`${now.getFullYear()}${formatTimestampPart(now.getMonth() + 1)}${formatTimestampPart(now.getDate())}`, `${formatTimestampPart(now.getHours())}${formatTimestampPart(now.getMinutes())}${formatTimestampPart(now.getSeconds())}`].join("-");
48
+ }
49
+ function withCollisionSuffix(pathValue, sequence) {
50
+ if (sequence <= 0) return pathValue;
51
+ const extension = extname(pathValue);
52
+ const baseName = basename(pathValue, extension);
53
+ return join(dirname(pathValue), `${baseName}-${sequence}${extension}`);
54
+ }
55
+ function resolveReportPath(report, now, pid) {
56
+ const cwd = report.cwd ?? process.cwd();
57
+ const defaultName = `wc-debug-${formatDebugReportTimestamp(now)}-${pid}.jsonl`;
58
+ const explicitPath = typeof report.path === "string";
59
+ const basePath = explicitPath ? resolve(cwd, report.path) : resolve(cwd, defaultName);
60
+ mkdirSync(dirname(basePath), { recursive: true });
61
+ if (explicitPath) {
62
+ if (existsSync(basePath) && statSync(basePath).isDirectory()) throw new Error(`debug report path must be a file: ${basePath}`);
63
+ return basePath;
64
+ }
65
+ let candidate = basePath;
66
+ let sequence = 0;
67
+ while (existsSync(candidate)) {
68
+ sequence += 1;
69
+ candidate = withCollisionSuffix(basePath, sequence);
70
+ }
71
+ return candidate;
72
+ }
73
+ function createTerminalSink() {
74
+ return {
75
+ write(line) {
76
+ console.error(`[debug] ${line}`);
77
+ },
78
+ close: NOOP_CLOSE
79
+ };
80
+ }
81
+ function createFileSink(pathValue) {
82
+ try {
83
+ closeSync(openSync(pathValue, "a"));
84
+ } catch (error) {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ throw new Error(`debug report path is not writable: ${pathValue} (${message})`);
87
+ }
88
+ const stream = createWriteStream(pathValue, {
89
+ flags: "a",
90
+ encoding: "utf8"
91
+ });
92
+ let streamError;
93
+ stream.on("error", (error) => {
94
+ streamError = error;
95
+ });
96
+ return {
97
+ write(line) {
98
+ if (streamError || stream.destroyed) return;
99
+ stream.write(`${line}\n`);
100
+ },
101
+ close() {
102
+ if (streamError || stream.destroyed || stream.writableEnded) return Promise.resolve();
103
+ return new Promise((resolve) => {
104
+ stream.end(() => {
105
+ resolve();
106
+ });
107
+ });
108
+ }
109
+ };
110
+ }
111
+ function createDebugChannel(options) {
112
+ if (!options.enabled) return {
113
+ enabled: false,
114
+ verbosity: options.verbosity ?? "compact",
115
+ emit() {},
116
+ close: NOOP_CLOSE
117
+ };
118
+ const verbosity = options.verbosity ?? "compact";
119
+ const sinks = [];
120
+ let reportPath;
121
+ if (options.report) {
122
+ const now = options.now?.() ?? /* @__PURE__ */ new Date();
123
+ const pid = options.pid ?? process.pid;
124
+ reportPath = resolveReportPath(options.report, now, pid);
125
+ sinks.push(createFileSink(reportPath));
126
+ if (options.report.tee) sinks.push(createTerminalSink());
127
+ } else sinks.push(createTerminalSink());
128
+ return {
129
+ enabled: true,
130
+ verbosity,
131
+ reportPath,
132
+ emit(event, details = {}, eventOptions = {}) {
133
+ if (!shouldEmitAtVerbosity(verbosity, eventOptions.verbosity ?? "compact")) return;
134
+ const payload = JSON.stringify({
135
+ event,
136
+ ...details
137
+ });
138
+ for (const sink of sinks) sink.write(payload);
139
+ },
140
+ async close() {
141
+ for (const sink of sinks) await sink.close();
142
+ }
143
+ };
144
+ }
145
+
146
+ //#endregion
147
+ //#region src/cli/path/filter.ts
148
+ const DEFAULT_INCLUDE_EXTENSIONS = new Set([
149
+ ".md",
150
+ ".markdown",
151
+ ".mdx",
152
+ ".mdc",
153
+ ".txt"
154
+ ]);
155
+ function collectExtensionOption(value, previous = []) {
156
+ return [...previous, value];
157
+ }
158
+ function normalizeExtensionToken(value) {
159
+ const trimmed = value.trim().toLowerCase();
160
+ if (!trimmed) return null;
161
+ const normalized = trimmed.startsWith(".") ? trimmed : `.${trimmed}`;
162
+ if (normalized === ".") return null;
163
+ return normalized;
164
+ }
165
+ function parseExtensionValues(values) {
166
+ const parsed = /* @__PURE__ */ new Set();
167
+ if (!values || values.length === 0) return parsed;
168
+ for (const value of values) for (const token of value.split(",")) {
169
+ const normalized = normalizeExtensionToken(token);
170
+ if (!normalized) continue;
171
+ parsed.add(normalized);
172
+ }
173
+ return parsed;
174
+ }
175
+ function buildDirectoryExtensionFilter(includeValues, excludeValues) {
176
+ const includeFromFlags = parseExtensionValues(includeValues);
177
+ const excludeExtensions = parseExtensionValues(excludeValues);
178
+ const includeExtensions = includeFromFlags.size > 0 ? includeFromFlags : new Set(DEFAULT_INCLUDE_EXTENSIONS);
179
+ const effectiveIncludeExtensions = /* @__PURE__ */ new Set();
180
+ for (const extension of includeExtensions) {
181
+ if (excludeExtensions.has(extension)) continue;
182
+ effectiveIncludeExtensions.add(extension);
183
+ }
184
+ return {
185
+ includeExtensions,
186
+ excludeExtensions,
187
+ effectiveIncludeExtensions
188
+ };
189
+ }
190
+ function shouldIncludeFromDirectory(filePath, filter) {
191
+ const extension = extname(filePath).toLowerCase();
192
+ return filter.effectiveIncludeExtensions.has(extension);
193
+ }
194
+
195
+ //#endregion
196
+ //#region src/cli/total-of.ts
197
+ const TOTAL_OF_PARTS = [
198
+ "words",
199
+ "emoji",
200
+ "symbols",
201
+ "punctuation",
202
+ "whitespace"
203
+ ];
204
+ const TOTAL_OF_PART_ALIASES = {
205
+ word: "words",
206
+ words: "words",
207
+ emoji: "emoji",
208
+ emojis: "emoji",
209
+ symbol: "symbols",
210
+ symbols: "symbols",
211
+ punction: "punctuation",
212
+ punctuation: "punctuation",
213
+ whitespace: "whitespace"
214
+ };
215
+ function createTotalOfCounts() {
216
+ return {
217
+ words: 0,
218
+ emoji: 0,
219
+ symbols: 0,
220
+ punctuation: 0,
221
+ whitespace: 0
222
+ };
223
+ }
224
+ function collectNonWordCounts(target, nonWords) {
225
+ if (!nonWords) return;
226
+ target.emoji += nonWords.counts.emoji;
227
+ target.symbols += nonWords.counts.symbols;
228
+ target.punctuation += nonWords.counts.punctuation;
229
+ target.whitespace += nonWords.counts.whitespace ?? 0;
230
+ }
231
+ function collectFromWordCounterResult(result) {
232
+ const counts = createTotalOfCounts();
233
+ counts.words += result.counts?.words ?? result.total;
234
+ if (result.breakdown.mode === "collector") {
235
+ collectNonWordCounts(counts, result.breakdown.nonWords);
236
+ return counts;
237
+ }
238
+ for (const item of result.breakdown.items) collectNonWordCounts(counts, item.nonWords);
239
+ return counts;
240
+ }
241
+ function collectTotalOfCounts(result) {
242
+ if (!("section" in result)) return collectFromWordCounterResult(result);
243
+ const counts = createTotalOfCounts();
244
+ for (const item of result.items) {
245
+ const itemCounts = collectFromWordCounterResult(item.result);
246
+ for (const part of TOTAL_OF_PARTS) counts[part] += itemCounts[part];
247
+ }
248
+ return counts;
249
+ }
250
+ function parseTotalOfToken(token) {
251
+ const canonical = TOTAL_OF_PART_ALIASES[token.trim().toLowerCase()];
252
+ if (canonical) return canonical;
253
+ throw new Error(`Invalid --total-of part: ${token}. Allowed: ${TOTAL_OF_PARTS.join(", ")}.`);
254
+ }
255
+ function parseTotalOfOption(value) {
256
+ const rawTokens = value.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
257
+ if (rawTokens.length === 0) throw new Error(`Invalid --total-of value: "${value}". Use comma-separated parts from: ${TOTAL_OF_PARTS.join(", ")}.`);
258
+ const parts = [];
259
+ const seen = /* @__PURE__ */ new Set();
260
+ for (const token of rawTokens) {
261
+ const parsed = parseTotalOfToken(token);
262
+ if (seen.has(parsed)) continue;
263
+ seen.add(parsed);
264
+ parts.push(parsed);
265
+ }
266
+ return parts;
267
+ }
268
+ function requiresNonWordCollection(parts) {
269
+ if (!parts || parts.length === 0) return false;
270
+ return parts.some((part) => part !== "words");
271
+ }
272
+ function requiresWhitespaceCollection(parts) {
273
+ if (!parts || parts.length === 0) return false;
274
+ return parts.includes("whitespace");
275
+ }
276
+ function resolveTotalOfOverride(result, parts) {
277
+ if (!parts || parts.length === 0) return;
278
+ const counts = collectTotalOfCounts(result);
279
+ let total = 0;
280
+ for (const part of parts) total += counts[part];
281
+ return {
282
+ parts: [...parts],
283
+ total
284
+ };
285
+ }
286
+ function formatTotalOfParts(parts) {
287
+ return parts.join(", ");
288
+ }
289
+
290
+ //#endregion
291
+ //#region src/cli/program/options.ts
292
+ const MODE_CHOICES = [
293
+ "chunk",
294
+ "segments",
295
+ "collector",
296
+ "char",
297
+ "char-collector"
298
+ ];
299
+ const FORMAT_CHOICES = [
300
+ "standard",
301
+ "raw",
302
+ "json"
303
+ ];
304
+ const SECTION_CHOICES = [
305
+ "all",
306
+ "split",
307
+ "frontmatter",
308
+ "content",
309
+ "per-key",
310
+ "split-per-key"
311
+ ];
312
+ const PATH_MODE_CHOICES = ["auto", "manual"];
313
+ function collectPathValue(value, previous = []) {
314
+ return [...previous, value];
315
+ }
316
+ function configureProgramOptions(program, parseMode) {
317
+ program.addOption(new Option("-m, --mode <mode>", "breakdown mode").choices(MODE_CHOICES).argParser(parseMode).default("chunk")).addOption(new Option("-f, --format <format>", "output format").choices(FORMAT_CHOICES).default("standard")).addOption(new Option("--section <section>", "document section mode").choices(SECTION_CHOICES).default("all")).addOption(new Option("--path-mode <mode>", "path resolution mode").choices(PATH_MODE_CHOICES).default("auto")).option("--latin-language <language>", "hint a language tag for Latin script text").option("--latin-tag <tag>", "hint a BCP 47 tag for Latin script text").option("--latin-locale <locale>", "legacy alias of --latin-language").option("--han-language <language>", "hint a language tag for Han script text").option("--han-tag <tag>", "hint a BCP 47 tag for Han script text").option("--non-words", "collect emoji, symbols, and punctuation (excludes whitespace)").option("--include-whitespace", "include whitespace counts (implies with --non-words; same as --misc)").option("--misc", "collect non-words plus whitespace (alias for --include-whitespace)").option("--total-of <parts>", "override total composition (comma-separated): words,emoji,symbols,punctuation,whitespace", parseTotalOfOption).option("--pretty", "pretty print JSON output", false).option("--debug", "enable debug diagnostics on stderr").option("--verbose", "emit verbose per-file debug diagnostics (requires --debug)").option("--debug-report [path]", "write debug diagnostics to a report file").option("--debug-report-tee", "mirror debug diagnostics to both report file and stderr").option("--debug-tee", "alias of --debug-report-tee").option("--merged", "show merged aggregate output (default)").option("--per-file", "show per-file output plus merged summary").option("--no-progress", "disable batch progress indicator").option("--keep-progress", "keep final batch progress line visible in standard mode").option("--no-recursive", "disable recursive directory traversal").option("--quiet-skips", "hide skip diagnostics (applies when --debug is enabled)").option("--include-ext <exts>", "comma-separated extensions to include during directory scanning", collectExtensionOption, []).option("--exclude-ext <exts>", "comma-separated extensions to exclude during directory scanning", collectExtensionOption, []).option("-p, --path <path>", "read input from file or directory", collectPathValue, []).argument("[text...]", "text to count").showHelpAfterError();
318
+ }
319
+
320
+ //#endregion
321
+ //#region node_modules/picocolors/picocolors.js
322
+ var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
323
+ let p = process || {}, argv = p.argv || [], env = p.env || {};
324
+ let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
325
+ let formatter = (open, close, replace = open) => (input) => {
326
+ let string = "" + input, index = string.indexOf(close, open.length);
327
+ return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
328
+ };
329
+ let replaceClose = (string, close, replace, index) => {
330
+ let result = "", cursor = 0;
331
+ do {
332
+ result += string.substring(cursor, index) + replace;
333
+ cursor = index + close.length;
334
+ index = string.indexOf(close, cursor);
335
+ } while (~index);
336
+ return result + string.substring(cursor);
337
+ };
338
+ let createColors = (enabled = isColorSupported) => {
339
+ let f = enabled ? formatter : () => String;
340
+ return {
341
+ isColorSupported: enabled,
342
+ reset: f("\x1B[0m", "\x1B[0m"),
343
+ bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
344
+ dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
345
+ italic: f("\x1B[3m", "\x1B[23m"),
346
+ underline: f("\x1B[4m", "\x1B[24m"),
347
+ inverse: f("\x1B[7m", "\x1B[27m"),
348
+ hidden: f("\x1B[8m", "\x1B[28m"),
349
+ strikethrough: f("\x1B[9m", "\x1B[29m"),
350
+ black: f("\x1B[30m", "\x1B[39m"),
351
+ red: f("\x1B[31m", "\x1B[39m"),
352
+ green: f("\x1B[32m", "\x1B[39m"),
353
+ yellow: f("\x1B[33m", "\x1B[39m"),
354
+ blue: f("\x1B[34m", "\x1B[39m"),
355
+ magenta: f("\x1B[35m", "\x1B[39m"),
356
+ cyan: f("\x1B[36m", "\x1B[39m"),
357
+ white: f("\x1B[37m", "\x1B[39m"),
358
+ gray: f("\x1B[90m", "\x1B[39m"),
359
+ bgBlack: f("\x1B[40m", "\x1B[49m"),
360
+ bgRed: f("\x1B[41m", "\x1B[49m"),
361
+ bgGreen: f("\x1B[42m", "\x1B[49m"),
362
+ bgYellow: f("\x1B[43m", "\x1B[49m"),
363
+ bgBlue: f("\x1B[44m", "\x1B[49m"),
364
+ bgMagenta: f("\x1B[45m", "\x1B[49m"),
365
+ bgCyan: f("\x1B[46m", "\x1B[49m"),
366
+ bgWhite: f("\x1B[47m", "\x1B[49m"),
367
+ blackBright: f("\x1B[90m", "\x1B[39m"),
368
+ redBright: f("\x1B[91m", "\x1B[39m"),
369
+ greenBright: f("\x1B[92m", "\x1B[39m"),
370
+ yellowBright: f("\x1B[93m", "\x1B[39m"),
371
+ blueBright: f("\x1B[94m", "\x1B[39m"),
372
+ magentaBright: f("\x1B[95m", "\x1B[39m"),
373
+ cyanBright: f("\x1B[96m", "\x1B[39m"),
374
+ whiteBright: f("\x1B[97m", "\x1B[39m"),
375
+ bgBlackBright: f("\x1B[100m", "\x1B[49m"),
376
+ bgRedBright: f("\x1B[101m", "\x1B[49m"),
377
+ bgGreenBright: f("\x1B[102m", "\x1B[49m"),
378
+ bgYellowBright: f("\x1B[103m", "\x1B[49m"),
379
+ bgBlueBright: f("\x1B[104m", "\x1B[49m"),
380
+ bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
381
+ bgCyanBright: f("\x1B[106m", "\x1B[49m"),
382
+ bgWhiteBright: f("\x1B[107m", "\x1B[49m")
383
+ };
384
+ };
385
+ module.exports = createColors();
386
+ module.exports.createColors = createColors;
387
+ }));
388
+
389
+ //#endregion
390
+ //#region src/cli/program/version.ts
391
+ var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
392
+ function* candidateSearchRoots() {
393
+ yield dirname(fileURLToPath(import.meta.url));
394
+ const argvPath = process.argv[1];
395
+ if (typeof argvPath === "string" && argvPath.length > 0) yield dirname(resolve(argvPath));
396
+ yield process.cwd();
397
+ }
398
+ function* walkUpDirectories(start, maxLevels) {
399
+ let current = start;
400
+ for (let depth = 0; depth < maxLevels; depth += 1) {
401
+ yield current;
402
+ const parent = dirname(current);
403
+ if (parent === current) break;
404
+ current = parent;
405
+ }
406
+ }
407
+ function resolveVersionFromPath(start, maxLevels) {
408
+ for (const directory of walkUpDirectories(start, maxLevels)) try {
409
+ const raw = readFileSync(join(directory, "package.json"), "utf8");
410
+ const data = JSON.parse(raw);
411
+ if (data.version) return data.version;
412
+ } catch {}
413
+ return null;
414
+ }
415
+ function resolvePackageVersion() {
416
+ const maxLevels = 8;
417
+ const seen = /* @__PURE__ */ new Set();
418
+ for (const root of candidateSearchRoots()) {
419
+ if (seen.has(root)) continue;
420
+ seen.add(root);
421
+ const version = resolveVersionFromPath(root, maxLevels);
422
+ if (version) return version;
423
+ }
424
+ return "0.0.0";
425
+ }
426
+ function getFormattedVersionLabel() {
427
+ const version = resolvePackageVersion();
428
+ return import_picocolors.default.bgBlack(import_picocolors.default.bold(import_picocolors.default.italic(` word-counter ${import_picocolors.default.cyanBright(`ver.${version}`)} `)));
429
+ }
430
+
431
+ //#endregion
432
+ //#region src/utils/append-all.ts
433
+ function appendAll(target, source) {
434
+ for (const item of source) target.push(item);
435
+ }
436
+
437
+ //#endregion
438
+ //#region src/cli/path/load.ts
439
+ function isProbablyBinary(buffer) {
440
+ if (buffer.length === 0) return false;
441
+ const sampleSize = Math.min(buffer.length, 1024);
442
+ let suspicious = 0;
443
+ for (let index = 0; index < sampleSize; index += 1) {
444
+ const byte = buffer[index] ?? 0;
445
+ if (byte === 0) return true;
446
+ if (byte === 9 || byte === 10 || byte === 13) continue;
447
+ if (byte >= 32 && byte <= 126) continue;
448
+ if (byte >= 128) continue;
449
+ suspicious += 1;
450
+ }
451
+ return suspicious / sampleSize > .3;
452
+ }
453
+ async function loadBatchInputs(filePaths) {
454
+ const files = [];
455
+ const skipped = [];
456
+ for (const filePath of filePaths) {
457
+ let buffer;
458
+ try {
459
+ buffer = await readFile(filePath);
460
+ } catch (error) {
461
+ const message = error instanceof Error ? error.message : String(error);
462
+ skipped.push({
463
+ path: filePath,
464
+ reason: `not readable: ${message}`
465
+ });
466
+ continue;
467
+ }
468
+ if (isProbablyBinary(buffer)) {
469
+ skipped.push({
470
+ path: filePath,
471
+ reason: "binary file"
472
+ });
473
+ continue;
474
+ }
475
+ files.push({
476
+ path: filePath,
477
+ content: buffer.toString("utf8")
478
+ });
479
+ }
480
+ return {
481
+ files,
482
+ skipped
483
+ };
484
+ }
8
485
 
9
- //#region rolldown:runtime
10
- var __create = Object.create;
11
- var __defProp = Object.defineProperty;
12
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
- var __getOwnPropNames = Object.getOwnPropertyNames;
14
- var __getProtoOf = Object.getPrototypeOf;
15
- var __hasOwnProp = Object.prototype.hasOwnProperty;
16
- var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
17
- var __copyProps = (to, from, except, desc) => {
18
- if (from && typeof from === "object" || typeof from === "function") {
19
- for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
20
- key = keys[i];
21
- if (!__hasOwnProp.call(to, key) && key !== except) {
22
- __defProp(to, key, {
23
- get: ((k) => from[k]).bind(null, key),
24
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
486
+ //#endregion
487
+ //#region src/cli/path/resolve.ts
488
+ async function expandDirectory(directoryPath, recursive, filter, skipped, debug, stats) {
489
+ let entries;
490
+ try {
491
+ entries = await readdir(directoryPath, {
492
+ withFileTypes: true,
493
+ encoding: "utf8"
494
+ });
495
+ } catch (error) {
496
+ const message = error instanceof Error ? error.message : String(error);
497
+ skipped.push({
498
+ path: directoryPath,
499
+ reason: `directory read failed: ${message}`
500
+ });
501
+ debug.emit("path.resolve.expand.read_failed", {
502
+ directory: directoryPath,
503
+ reason: `directory read failed: ${message}`
504
+ });
505
+ return [];
506
+ }
507
+ const sortedEntries = entries.slice().sort((left, right) => left.name.localeCompare(right.name));
508
+ const files = [];
509
+ debug.emit("path.resolve.expand.start", {
510
+ directory: directoryPath,
511
+ entries: sortedEntries.length,
512
+ recursive
513
+ });
514
+ for (const entry of sortedEntries) {
515
+ const entryPath = resolve(directoryPath, entry.name);
516
+ if (entry.isFile()) {
517
+ if (!shouldIncludeFromDirectory(entryPath, filter)) {
518
+ skipped.push({
519
+ path: entryPath,
520
+ reason: "extension excluded"
25
521
  });
522
+ debug.emit("path.resolve.filter.excluded", {
523
+ path: entryPath,
524
+ reason: "extension excluded"
525
+ }, { verbosity: "verbose" });
526
+ stats.filterExcluded += 1;
527
+ continue;
26
528
  }
529
+ files.push(entryPath);
530
+ stats.directoryIncluded += 1;
531
+ debug.emit("path.resolve.expand.include", {
532
+ path: entryPath,
533
+ source: "directory"
534
+ }, { verbosity: "verbose" });
535
+ continue;
27
536
  }
537
+ if (!entry.isDirectory() || !recursive) continue;
538
+ appendAll(files, await expandDirectory(entryPath, recursive, filter, skipped, debug, stats));
28
539
  }
29
- return to;
30
- };
31
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
32
- value: mod,
33
- enumerable: true
34
- }) : target, mod));
540
+ debug.emit("path.resolve.expand.complete", {
541
+ directory: directoryPath,
542
+ files: files.length
543
+ });
544
+ return files;
545
+ }
546
+ async function resolveBatchFilePaths(pathInputs, options) {
547
+ const skipped = [];
548
+ const resolvedFiles = /* @__PURE__ */ new Set();
549
+ const stats = {
550
+ dedupeAccepted: 0,
551
+ dedupeDuplicates: 0,
552
+ filterExcluded: 0,
553
+ directoryIncluded: 0
554
+ };
555
+ const extensionFilter = options.extensionFilter ?? buildDirectoryExtensionFilter(void 0, void 0);
556
+ const debug = options.debug ?? {
557
+ enabled: false,
558
+ verbosity: "compact",
559
+ emit() {},
560
+ close: async () => {}
561
+ };
562
+ debug.emit("path.resolve.inputs", {
563
+ inputs: pathInputs.length,
564
+ pathMode: options.pathMode,
565
+ recursive: options.recursive
566
+ });
567
+ const addResolvedFile = (filePath, details) => {
568
+ if (resolvedFiles.has(filePath)) {
569
+ stats.dedupeDuplicates += 1;
570
+ debug.emit("path.resolve.dedupe.duplicate", {
571
+ path: filePath,
572
+ source: details.source,
573
+ input: details.input
574
+ }, { verbosity: "verbose" });
575
+ return;
576
+ }
577
+ resolvedFiles.add(filePath);
578
+ stats.dedupeAccepted += 1;
579
+ debug.emit("path.resolve.dedupe.accept", {
580
+ path: filePath,
581
+ source: details.source,
582
+ input: details.input
583
+ }, { verbosity: "verbose" });
584
+ };
585
+ for (const rawPath of pathInputs) {
586
+ const targetPath = resolve(rawPath);
587
+ debug.emit("path.resolve.input", {
588
+ rawPath,
589
+ resolvedPath: targetPath
590
+ });
591
+ let metadata;
592
+ try {
593
+ metadata = await stat(targetPath);
594
+ } catch (error) {
595
+ const message = error instanceof Error ? error.message : String(error);
596
+ skipped.push({
597
+ path: targetPath,
598
+ reason: `not readable: ${message}`
599
+ });
600
+ debug.emit("path.resolve.skip", {
601
+ path: targetPath,
602
+ reason: `not readable: ${message}`
603
+ });
604
+ continue;
605
+ }
606
+ if (metadata.isDirectory() && options.pathMode === "auto") {
607
+ debug.emit("path.resolve.root.expand", {
608
+ root: targetPath,
609
+ recursive: options.recursive
610
+ });
611
+ const files = await expandDirectory(targetPath, options.recursive, extensionFilter, skipped, debug, stats);
612
+ for (const file of files) addResolvedFile(file, {
613
+ source: "directory",
614
+ input: targetPath
615
+ });
616
+ continue;
617
+ }
618
+ if (!metadata.isFile()) {
619
+ skipped.push({
620
+ path: targetPath,
621
+ reason: "not a regular file"
622
+ });
623
+ debug.emit("path.resolve.skip", {
624
+ path: targetPath,
625
+ reason: "not a regular file"
626
+ });
627
+ continue;
628
+ }
629
+ addResolvedFile(targetPath, {
630
+ source: "direct",
631
+ input: targetPath
632
+ });
633
+ }
634
+ const files = [...resolvedFiles].sort((left, right) => left.localeCompare(right));
635
+ debug.emit("path.resolve.filter.summary", {
636
+ excluded: stats.filterExcluded,
637
+ included: stats.directoryIncluded
638
+ });
639
+ debug.emit("path.resolve.dedupe.summary", {
640
+ accepted: stats.dedupeAccepted,
641
+ duplicates: stats.dedupeDuplicates
642
+ });
643
+ debug.emit("path.resolve.complete", {
644
+ files: files.length,
645
+ skipped: skipped.length,
646
+ ordering: "absolute-path-ascending"
647
+ });
648
+ return {
649
+ files,
650
+ skipped
651
+ };
652
+ }
653
+
654
+ //#endregion
655
+ //#region src/cli/progress/reporter.ts
656
+ const PROGRESS_BAR_WIDTH = 20;
657
+ const FILLED_BAR_CHAR = "█";
658
+ const EMPTY_BAR_CHAR = "░";
659
+ function clamp(value, min, max) {
660
+ return Math.max(min, Math.min(max, value));
661
+ }
662
+ function buildProgressBar(completed, total) {
663
+ const safeTotal = Math.max(total, 1);
664
+ const ratio = clamp(completed / safeTotal, 0, 1);
665
+ const filled = completed >= safeTotal ? PROGRESS_BAR_WIDTH : Math.floor(ratio * PROGRESS_BAR_WIDTH);
666
+ const empty = PROGRESS_BAR_WIDTH - filled;
667
+ return `${FILLED_BAR_CHAR.repeat(filled)}${EMPTY_BAR_CHAR.repeat(empty)}`;
668
+ }
669
+ function formatElapsed(startedAtMs) {
670
+ const elapsedMs = Date.now() - startedAtMs;
671
+ const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
672
+ const minutes = Math.floor(totalSeconds / 60);
673
+ const seconds = totalSeconds % 60;
674
+ const tenths = Math.floor(Math.max(0, elapsedMs) % 1e3 / 100);
675
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${tenths}`;
676
+ }
677
+ function buildProgressLine(completed, total, startedAtMs) {
678
+ const safeTotal = Math.max(total, 1);
679
+ const percent = completed >= safeTotal ? 100 : Math.floor(completed / safeTotal * 100);
680
+ return `Counting files [${buildProgressBar(completed, safeTotal)}] ${`${String(percent).padStart(3, " ")}%`} ${String(completed).padStart(String(safeTotal).length, " ")}/${safeTotal} elapsed ${formatElapsed(startedAtMs)}`;
681
+ }
682
+ function buildFinalizingLine(startedAtMs) {
683
+ return `Finalizing aggregate... elapsed ${formatElapsed(startedAtMs)}`;
684
+ }
685
+ function createBatchProgressReporter(options) {
686
+ const enabled = options.enabled;
687
+ const isTTY = Boolean(options.stream.isTTY);
688
+ const clearOnFinish = options.clearOnFinish ?? true;
689
+ let active = false;
690
+ let total = 0;
691
+ let lastLineLength = 0;
692
+ let startedAtMs = 0;
693
+ let lastRenderedPercent = -1;
694
+ let finalizingStarted = false;
695
+ const writeTTYLine = (line) => {
696
+ const trailingPadding = lastLineLength > line.length ? " ".repeat(lastLineLength - line.length) : "";
697
+ options.stream.write(`\r${line}${trailingPadding}`);
698
+ lastLineLength = line.length;
699
+ };
700
+ const render = (completed) => {
701
+ const line = buildProgressLine(completed, total, startedAtMs);
702
+ const safeTotal = Math.max(total, 1);
703
+ const percent = completed >= safeTotal ? 100 : Math.floor(completed / safeTotal * 100);
704
+ if (!isTTY && percent === lastRenderedPercent && completed < safeTotal) return;
705
+ lastRenderedPercent = percent;
706
+ if (isTTY) {
707
+ writeTTYLine(line);
708
+ return;
709
+ }
710
+ lastLineLength = line.length;
711
+ options.stream.write(`${line}\n`);
712
+ };
713
+ const clearLine = () => {
714
+ if (lastLineLength === 0) return;
715
+ options.stream.write(`\r${" ".repeat(lastLineLength)}\r`);
716
+ lastLineLength = 0;
717
+ };
718
+ return {
719
+ enabled,
720
+ start(nextTotal, nextStartedAtMs) {
721
+ if (!enabled || nextTotal <= 1) return;
722
+ total = nextTotal;
723
+ active = true;
724
+ startedAtMs = nextStartedAtMs ?? Date.now();
725
+ lastRenderedPercent = -1;
726
+ finalizingStarted = false;
727
+ render(0);
728
+ },
729
+ advance(snapshot) {
730
+ if (!active) return;
731
+ render(snapshot.completed);
732
+ },
733
+ startFinalizing() {
734
+ if (!active || finalizingStarted) return;
735
+ finalizingStarted = true;
736
+ const line = buildFinalizingLine(startedAtMs);
737
+ if (isTTY) {
738
+ if (!clearOnFinish) {
739
+ options.stream.write(`\n${line}`);
740
+ lastLineLength = line.length;
741
+ return;
742
+ }
743
+ writeTTYLine(line);
744
+ return;
745
+ }
746
+ lastLineLength = line.length;
747
+ options.stream.write(`${line}\n`);
748
+ },
749
+ finish() {
750
+ if (!active) return;
751
+ if (isTTY) if (clearOnFinish) clearLine();
752
+ else options.stream.write("\n");
753
+ active = false;
754
+ }
755
+ };
756
+ }
35
757
 
36
758
  //#endregion
37
759
  //#region src/markdown/toml/arrays.ts
@@ -329,7 +1051,7 @@ function parseTomlFrontmatter(frontmatter) {
329
1051
  index += 1;
330
1052
  const nextLine = lines[index] ?? "";
331
1053
  combined += `\n${nextLine}`;
332
- if ((/* @__PURE__ */ new RegExp(`${delimiter}\\s*$`)).test(nextLine)) {
1054
+ if (new RegExp(`${delimiter}\\s*$`).test(nextLine)) {
333
1055
  closed = true;
334
1056
  break;
335
1057
  }
@@ -457,10 +1179,10 @@ function parseMarkdown(input) {
457
1179
  data: null,
458
1180
  frontmatterType: null
459
1181
  };
460
- const frontmatter$1 = jsonBlock.jsonText;
1182
+ const frontmatter = jsonBlock.jsonText;
461
1183
  let content = normalizedWithoutBom.slice(jsonBlock.endIndex + 1);
462
1184
  if (content.startsWith("\n")) content = content.slice(1);
463
- const data = parseFrontmatter(frontmatter$1, "json");
1185
+ const data = parseFrontmatter(frontmatter, "json");
464
1186
  if (!data) return {
465
1187
  frontmatter: null,
466
1188
  content: normalizedWithoutBom,
@@ -468,7 +1190,7 @@ function parseMarkdown(input) {
468
1190
  frontmatterType: null
469
1191
  };
470
1192
  return {
471
- frontmatter: frontmatter$1,
1193
+ frontmatter,
472
1194
  content,
473
1195
  data,
474
1196
  frontmatterType: "json"
@@ -523,12 +1245,6 @@ function countCharsForLocale(text, locale) {
523
1245
  return count;
524
1246
  }
525
1247
 
526
- //#endregion
527
- //#region src/utils/append-all.ts
528
- function appendAll(target, source) {
529
- for (const item of source) target.push(item);
530
- }
531
-
532
1248
  //#endregion
533
1249
  //#region src/wc/non-words.ts
534
1250
  const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation})/u;
@@ -697,6 +1413,32 @@ function analyzeCharChunk(chunk, collectNonWords, includeWhitespace) {
697
1413
  nonWords: nonWords ?? void 0
698
1414
  };
699
1415
  }
1416
+ function aggregateCharsByLocale(chunks) {
1417
+ const order = [];
1418
+ const map = /* @__PURE__ */ new Map();
1419
+ for (const chunk of chunks) {
1420
+ const existing = map.get(chunk.locale);
1421
+ if (existing) {
1422
+ existing.chars += chunk.chars;
1423
+ existing.wordChars += chunk.wordChars;
1424
+ existing.nonWordChars += chunk.nonWordChars;
1425
+ if (chunk.nonWords) {
1426
+ if (!existing.nonWords) existing.nonWords = createNonWordCollection();
1427
+ mergeNonWordCollections(existing.nonWords, chunk.nonWords);
1428
+ }
1429
+ continue;
1430
+ }
1431
+ order.push(chunk.locale);
1432
+ map.set(chunk.locale, {
1433
+ locale: chunk.locale,
1434
+ chars: chunk.chars,
1435
+ wordChars: chunk.wordChars,
1436
+ nonWordChars: chunk.nonWordChars,
1437
+ nonWords: chunk.nonWords ? mergeNonWordCollections(createNonWordCollection(), chunk.nonWords) : void 0
1438
+ });
1439
+ }
1440
+ return order.map((locale) => map.get(locale));
1441
+ }
700
1442
  function aggregateByLocale(chunks) {
701
1443
  const order = [];
702
1444
  const map = /* @__PURE__ */ new Map();
@@ -731,11 +1473,55 @@ const MODE_ALIASES = {
731
1473
  char: "char",
732
1474
  chars: "char",
733
1475
  character: "char",
734
- characters: "char"
1476
+ characters: "char",
1477
+ "char-collector": "char-collector"
735
1478
  };
1479
+ const CHAR_MODE_ALIASES = new Set([
1480
+ "char",
1481
+ "chars",
1482
+ "character",
1483
+ "characters"
1484
+ ]);
1485
+ const COLLECTOR_MODE_ALIASES = new Set([
1486
+ "collector",
1487
+ "collect",
1488
+ "colle",
1489
+ "col"
1490
+ ]);
1491
+ function collapseSeparators(value) {
1492
+ return value.replace(/[-_\s]+/g, "");
1493
+ }
1494
+ function isComposedCharCollectorFromTokens(value) {
1495
+ const tokens = value.split(/[-_\s]+/).map((token) => token.trim()).filter((token) => token.length > 0);
1496
+ if (tokens.length < 2) return false;
1497
+ let hasCharAlias = false;
1498
+ let hasCollectorAlias = false;
1499
+ for (const token of tokens) {
1500
+ if (CHAR_MODE_ALIASES.has(token)) {
1501
+ hasCharAlias = true;
1502
+ continue;
1503
+ }
1504
+ if (COLLECTOR_MODE_ALIASES.has(token)) {
1505
+ hasCollectorAlias = true;
1506
+ continue;
1507
+ }
1508
+ return false;
1509
+ }
1510
+ return hasCharAlias && hasCollectorAlias;
1511
+ }
1512
+ function isComposedCharCollectorCompact(value) {
1513
+ for (const charAlias of CHAR_MODE_ALIASES) for (const collectorAlias of COLLECTOR_MODE_ALIASES) if (value === `${charAlias}${collectorAlias}` || value === `${collectorAlias}${charAlias}`) return true;
1514
+ return false;
1515
+ }
736
1516
  function normalizeMode(input) {
737
1517
  if (!input) return null;
738
- return MODE_ALIASES[input.trim().toLowerCase()] ?? null;
1518
+ const normalized = input.trim().toLowerCase();
1519
+ const direct = MODE_ALIASES[normalized];
1520
+ if (direct) return direct;
1521
+ if (isComposedCharCollectorFromTokens(normalized)) return "char-collector";
1522
+ const compact = collapseSeparators(normalized);
1523
+ if (isComposedCharCollectorCompact(compact)) return "char-collector";
1524
+ return MODE_ALIASES[compact] ?? null;
739
1525
  }
740
1526
  function resolveMode(input, fallback = "chunk") {
741
1527
  return normalizeMode(input) ?? fallback;
@@ -897,25 +1683,37 @@ function wordCounter(text, options = {}) {
897
1683
  hanLanguageHint: options.hanLanguageHint,
898
1684
  hanTagHint: options.hanTagHint
899
1685
  });
900
- if (mode === "char") {
901
- const analyzed$1 = chunks.map((chunk) => analyzeCharChunk(chunk, collectNonWords, includeWhitespace));
902
- const total$1 = analyzed$1.reduce((sum, chunk) => sum + chunk.chars, 0);
903
- const items = analyzed$1.map((chunk) => ({
904
- locale: chunk.locale,
905
- text: chunk.text,
906
- chars: chunk.chars,
907
- nonWords: chunk.nonWords
908
- }));
1686
+ if (mode === "char" || mode === "char-collector") {
1687
+ const analyzed = chunks.map((chunk) => analyzeCharChunk(chunk, collectNonWords, includeWhitespace));
1688
+ const total = analyzed.reduce((sum, chunk) => sum + chunk.chars, 0);
1689
+ const counts = collectNonWords ? {
1690
+ words: analyzed.reduce((sum, chunk) => sum + chunk.wordChars, 0),
1691
+ nonWords: analyzed.reduce((sum, chunk) => sum + chunk.nonWordChars, 0),
1692
+ total
1693
+ } : void 0;
1694
+ if (mode === "char") return {
1695
+ total,
1696
+ counts,
1697
+ breakdown: {
1698
+ mode,
1699
+ items: analyzed.map((chunk) => ({
1700
+ locale: chunk.locale,
1701
+ text: chunk.text,
1702
+ chars: chunk.chars,
1703
+ nonWords: chunk.nonWords
1704
+ }))
1705
+ }
1706
+ };
909
1707
  return {
910
- total: total$1,
911
- counts: collectNonWords ? {
912
- words: analyzed$1.reduce((sum, chunk) => sum + chunk.wordChars, 0),
913
- nonWords: analyzed$1.reduce((sum, chunk) => sum + chunk.nonWordChars, 0),
914
- total: total$1
915
- } : void 0,
1708
+ total,
1709
+ counts,
916
1710
  breakdown: {
917
1711
  mode,
918
- items
1712
+ items: aggregateCharsByLocale(analyzed).map((chunk) => ({
1713
+ locale: chunk.locale,
1714
+ chars: chunk.chars,
1715
+ nonWords: chunk.nonWords
1716
+ }))
919
1717
  }
920
1718
  };
921
1719
  }
@@ -1006,327 +1804,51 @@ function buildPerKeyItems(data, mode, options) {
1006
1804
  return Object.entries(data).map(([key, value]) => {
1007
1805
  const valueText = normalizeText(value);
1008
1806
  return {
1009
- name: key,
1010
- source: "frontmatter",
1011
- result: wc_default(valueText ? `${key}: ${valueText}` : key, options)
1012
- };
1013
- });
1014
- }
1015
- function buildSingleItem(name, text, mode, options, source) {
1016
- return [{
1017
- name,
1018
- source,
1019
- result: wc_default(text, options)
1020
- }];
1021
- }
1022
- function sumTotals(items) {
1023
- return items.reduce((sum, item) => sum + item.result.total, 0);
1024
- }
1025
- function countSections(input, section, options = {}) {
1026
- const mode = options.mode ?? "chunk";
1027
- if (section === "all") {
1028
- const result = wc_default(input, options);
1029
- return {
1030
- section,
1031
- total: result.total,
1032
- frontmatterType: null,
1033
- items: [{
1034
- name: "all",
1035
- source: "content",
1036
- result
1037
- }]
1038
- };
1039
- }
1040
- const parsed = parseMarkdown(input);
1041
- const frontmatterText = parsed.frontmatter ?? "";
1042
- const contentText = parsed.content ?? "";
1043
- let items = [];
1044
- if (section === "frontmatter") items = buildSingleItem("frontmatter", frontmatterText, mode, options, "frontmatter");
1045
- else if (section === "content") items = buildSingleItem("content", contentText, mode, options, "content");
1046
- else if (section === "split") items = [...buildSingleItem("frontmatter", frontmatterText, mode, options, "frontmatter"), ...buildSingleItem("content", contentText, mode, options, "content")];
1047
- else if (section === "per-key") items = buildPerKeyItems(parsed.data, mode, options);
1048
- else if (section === "split-per-key") items = [...buildPerKeyItems(parsed.data, mode, options), ...buildSingleItem("content", contentText, mode, options, "content")];
1049
- return {
1050
- section,
1051
- total: sumTotals(items),
1052
- frontmatterType: parsed.frontmatterType,
1053
- items
1054
- };
1055
- }
1056
-
1057
- //#endregion
1058
- //#region src/cli/path/filter.ts
1059
- const DEFAULT_INCLUDE_EXTENSIONS = new Set([
1060
- ".md",
1061
- ".markdown",
1062
- ".mdx",
1063
- ".mdc",
1064
- ".txt"
1065
- ]);
1066
- function collectExtensionOption(value, previous = []) {
1067
- return [...previous, value];
1068
- }
1069
- function normalizeExtensionToken(value) {
1070
- const trimmed = value.trim().toLowerCase();
1071
- if (!trimmed) return null;
1072
- const normalized = trimmed.startsWith(".") ? trimmed : `.${trimmed}`;
1073
- if (normalized === ".") return null;
1074
- return normalized;
1075
- }
1076
- function parseExtensionValues(values) {
1077
- const parsed = /* @__PURE__ */ new Set();
1078
- if (!values || values.length === 0) return parsed;
1079
- for (const value of values) for (const token of value.split(",")) {
1080
- const normalized = normalizeExtensionToken(token);
1081
- if (!normalized) continue;
1082
- parsed.add(normalized);
1083
- }
1084
- return parsed;
1085
- }
1086
- function buildDirectoryExtensionFilter(includeValues, excludeValues) {
1087
- const includeFromFlags = parseExtensionValues(includeValues);
1088
- const excludeExtensions = parseExtensionValues(excludeValues);
1089
- const includeExtensions = includeFromFlags.size > 0 ? includeFromFlags : new Set(DEFAULT_INCLUDE_EXTENSIONS);
1090
- const effectiveIncludeExtensions = /* @__PURE__ */ new Set();
1091
- for (const extension of includeExtensions) {
1092
- if (excludeExtensions.has(extension)) continue;
1093
- effectiveIncludeExtensions.add(extension);
1094
- }
1095
- return {
1096
- includeExtensions,
1097
- excludeExtensions,
1098
- effectiveIncludeExtensions
1099
- };
1100
- }
1101
- function shouldIncludeFromDirectory(filePath, filter) {
1102
- const extension = extname(filePath).toLowerCase();
1103
- return filter.effectiveIncludeExtensions.has(extension);
1104
- }
1105
-
1106
- //#endregion
1107
- //#region src/cli/path/load.ts
1108
- function isProbablyBinary(buffer) {
1109
- if (buffer.length === 0) return false;
1110
- const sampleSize = Math.min(buffer.length, 1024);
1111
- let suspicious = 0;
1112
- for (let index = 0; index < sampleSize; index += 1) {
1113
- const byte = buffer[index] ?? 0;
1114
- if (byte === 0) return true;
1115
- if (byte === 9 || byte === 10 || byte === 13) continue;
1116
- if (byte >= 32 && byte <= 126) continue;
1117
- if (byte >= 128) continue;
1118
- suspicious += 1;
1119
- }
1120
- return suspicious / sampleSize > .3;
1121
- }
1122
- async function loadBatchInputs(filePaths) {
1123
- const files = [];
1124
- const skipped = [];
1125
- for (const filePath of filePaths) {
1126
- let buffer;
1127
- try {
1128
- buffer = await readFile(filePath);
1129
- } catch (error) {
1130
- const message = error instanceof Error ? error.message : String(error);
1131
- skipped.push({
1132
- path: filePath,
1133
- reason: `not readable: ${message}`
1134
- });
1135
- continue;
1136
- }
1137
- if (isProbablyBinary(buffer)) {
1138
- skipped.push({
1139
- path: filePath,
1140
- reason: "binary file"
1141
- });
1142
- continue;
1143
- }
1144
- files.push({
1145
- path: filePath,
1146
- content: buffer.toString("utf8")
1147
- });
1148
- }
1149
- return {
1150
- files,
1151
- skipped
1152
- };
1153
- }
1154
-
1155
- //#endregion
1156
- //#region src/cli/path/resolve.ts
1157
- async function expandDirectory(directoryPath, recursive, filter, skipped) {
1158
- let entries;
1159
- try {
1160
- entries = await readdir(directoryPath, {
1161
- withFileTypes: true,
1162
- encoding: "utf8"
1163
- });
1164
- } catch (error) {
1165
- const message = error instanceof Error ? error.message : String(error);
1166
- skipped.push({
1167
- path: directoryPath,
1168
- reason: `directory read failed: ${message}`
1169
- });
1170
- return [];
1171
- }
1172
- const sortedEntries = entries.slice().sort((left, right) => left.name.localeCompare(right.name));
1173
- const files = [];
1174
- for (const entry of sortedEntries) {
1175
- const entryPath = resolve(directoryPath, entry.name);
1176
- if (entry.isFile()) {
1177
- if (!shouldIncludeFromDirectory(entryPath, filter)) {
1178
- skipped.push({
1179
- path: entryPath,
1180
- reason: "extension excluded"
1181
- });
1182
- continue;
1183
- }
1184
- files.push(entryPath);
1185
- continue;
1186
- }
1187
- if (!entry.isDirectory() || !recursive) continue;
1188
- appendAll(files, await expandDirectory(entryPath, recursive, filter, skipped));
1189
- }
1190
- return files;
1191
- }
1192
- async function resolveBatchFilePaths(pathInputs, options) {
1193
- const skipped = [];
1194
- const resolvedFiles = /* @__PURE__ */ new Set();
1195
- const extensionFilter = options.extensionFilter ?? buildDirectoryExtensionFilter(void 0, void 0);
1196
- for (const rawPath of pathInputs) {
1197
- const targetPath = resolve(rawPath);
1198
- let metadata;
1199
- try {
1200
- metadata = await stat(targetPath);
1201
- } catch (error) {
1202
- const message = error instanceof Error ? error.message : String(error);
1203
- skipped.push({
1204
- path: targetPath,
1205
- reason: `not readable: ${message}`
1206
- });
1207
- continue;
1208
- }
1209
- if (metadata.isDirectory() && options.pathMode === "auto") {
1210
- const files = await expandDirectory(targetPath, options.recursive, extensionFilter, skipped);
1211
- for (const file of files) resolvedFiles.add(file);
1212
- continue;
1213
- }
1214
- if (!metadata.isFile()) {
1215
- skipped.push({
1216
- path: targetPath,
1217
- reason: "not a regular file"
1218
- });
1219
- continue;
1220
- }
1221
- resolvedFiles.add(targetPath);
1222
- }
1223
- return {
1224
- files: [...resolvedFiles].sort((left, right) => left.localeCompare(right)),
1225
- skipped
1226
- };
1227
- }
1228
-
1229
- //#endregion
1230
- //#region src/cli/progress/reporter.ts
1231
- const PROGRESS_BAR_WIDTH = 20;
1232
- const FILLED_BAR_CHAR = "█";
1233
- const EMPTY_BAR_CHAR = "░";
1234
- function clamp(value, min, max) {
1235
- return Math.max(min, Math.min(max, value));
1236
- }
1237
- function buildProgressBar(completed, total) {
1238
- const safeTotal = Math.max(total, 1);
1239
- const ratio = clamp(completed / safeTotal, 0, 1);
1240
- const filled = completed >= safeTotal ? PROGRESS_BAR_WIDTH : Math.floor(ratio * PROGRESS_BAR_WIDTH);
1241
- const empty = PROGRESS_BAR_WIDTH - filled;
1242
- return `${FILLED_BAR_CHAR.repeat(filled)}${EMPTY_BAR_CHAR.repeat(empty)}`;
1243
- }
1244
- function formatElapsed(startedAtMs) {
1245
- const elapsedMs = Date.now() - startedAtMs;
1246
- const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
1247
- const minutes = Math.floor(totalSeconds / 60);
1248
- const seconds = totalSeconds % 60;
1249
- const tenths = Math.floor(Math.max(0, elapsedMs) % 1e3 / 100);
1250
- return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${tenths}`;
1807
+ name: key,
1808
+ source: "frontmatter",
1809
+ result: wc_default(valueText ? `${key}: ${valueText}` : key, options)
1810
+ };
1811
+ });
1251
1812
  }
1252
- function buildProgressLine(completed, total, startedAtMs) {
1253
- const safeTotal = Math.max(total, 1);
1254
- const percent = completed >= safeTotal ? 100 : Math.floor(completed / safeTotal * 100);
1255
- return `Counting files [${buildProgressBar(completed, safeTotal)}] ${`${String(percent).padStart(3, " ")}%`} ${String(completed).padStart(String(safeTotal).length, " ")}/${safeTotal} elapsed ${formatElapsed(startedAtMs)}`;
1813
+ function buildSingleItem(name, text, mode, options, source) {
1814
+ return [{
1815
+ name,
1816
+ source,
1817
+ result: wc_default(text, options)
1818
+ }];
1256
1819
  }
1257
- function buildFinalizingLine(startedAtMs) {
1258
- return `Finalizing aggregate... elapsed ${formatElapsed(startedAtMs)}`;
1820
+ function sumTotals(items) {
1821
+ return items.reduce((sum, item) => sum + item.result.total, 0);
1259
1822
  }
1260
- function createBatchProgressReporter(options) {
1261
- const enabled = options.enabled;
1262
- const isTTY = Boolean(options.stream.isTTY);
1263
- const clearOnFinish = options.clearOnFinish ?? true;
1264
- let active = false;
1265
- let total = 0;
1266
- let lastLineLength = 0;
1267
- let startedAtMs = 0;
1268
- let lastRenderedPercent = -1;
1269
- let finalizingStarted = false;
1270
- const writeTTYLine = (line) => {
1271
- const trailingPadding = lastLineLength > line.length ? " ".repeat(lastLineLength - line.length) : "";
1272
- options.stream.write(`\r${line}${trailingPadding}`);
1273
- lastLineLength = line.length;
1274
- };
1275
- const render = (completed) => {
1276
- const line = buildProgressLine(completed, total, startedAtMs);
1277
- const safeTotal = Math.max(total, 1);
1278
- const percent = completed >= safeTotal ? 100 : Math.floor(completed / safeTotal * 100);
1279
- if (!isTTY && percent === lastRenderedPercent && completed < safeTotal) return;
1280
- lastRenderedPercent = percent;
1281
- if (isTTY) {
1282
- writeTTYLine(line);
1283
- return;
1284
- }
1285
- lastLineLength = line.length;
1286
- options.stream.write(`${line}\n`);
1287
- };
1288
- const clearLine = () => {
1289
- if (lastLineLength === 0) return;
1290
- options.stream.write(`\r${" ".repeat(lastLineLength)}\r`);
1291
- lastLineLength = 0;
1292
- };
1823
+ function countSections(input, section, options = {}) {
1824
+ const mode = options.mode ?? "chunk";
1825
+ if (section === "all") {
1826
+ const result = wc_default(input, options);
1827
+ return {
1828
+ section,
1829
+ total: result.total,
1830
+ frontmatterType: null,
1831
+ items: [{
1832
+ name: "all",
1833
+ source: "content",
1834
+ result
1835
+ }]
1836
+ };
1837
+ }
1838
+ const parsed = parseMarkdown(input);
1839
+ const frontmatterText = parsed.frontmatter ?? "";
1840
+ const contentText = parsed.content ?? "";
1841
+ let items = [];
1842
+ if (section === "frontmatter") items = buildSingleItem("frontmatter", frontmatterText, mode, options, "frontmatter");
1843
+ else if (section === "content") items = buildSingleItem("content", contentText, mode, options, "content");
1844
+ else if (section === "split") items = [...buildSingleItem("frontmatter", frontmatterText, mode, options, "frontmatter"), ...buildSingleItem("content", contentText, mode, options, "content")];
1845
+ else if (section === "per-key") items = buildPerKeyItems(parsed.data, mode, options);
1846
+ else if (section === "split-per-key") items = [...buildPerKeyItems(parsed.data, mode, options), ...buildSingleItem("content", contentText, mode, options, "content")];
1293
1847
  return {
1294
- enabled,
1295
- start(nextTotal, nextStartedAtMs) {
1296
- if (!enabled || nextTotal <= 1) return;
1297
- total = nextTotal;
1298
- active = true;
1299
- startedAtMs = nextStartedAtMs ?? Date.now();
1300
- lastRenderedPercent = -1;
1301
- finalizingStarted = false;
1302
- render(0);
1303
- },
1304
- advance(snapshot) {
1305
- if (!active) return;
1306
- render(snapshot.completed);
1307
- },
1308
- startFinalizing() {
1309
- if (!active || finalizingStarted) return;
1310
- finalizingStarted = true;
1311
- const line = buildFinalizingLine(startedAtMs);
1312
- if (isTTY) {
1313
- if (!clearOnFinish) {
1314
- options.stream.write(`\n${line}`);
1315
- lastLineLength = line.length;
1316
- return;
1317
- }
1318
- writeTTYLine(line);
1319
- return;
1320
- }
1321
- lastLineLength = line.length;
1322
- options.stream.write(`${line}\n`);
1323
- },
1324
- finish() {
1325
- if (!active) return;
1326
- if (isTTY) if (clearOnFinish) clearLine();
1327
- else options.stream.write("\n");
1328
- active = false;
1329
- }
1848
+ section,
1849
+ total: sumTotals(items),
1850
+ frontmatterType: parsed.frontmatterType,
1851
+ items
1330
1852
  };
1331
1853
  }
1332
1854
 
@@ -1364,6 +1886,43 @@ function mergeWordCounterResult(left, right, preserveCollectorSegments) {
1364
1886
  items: [...left.breakdown.items, ...right.breakdown.items]
1365
1887
  }
1366
1888
  };
1889
+ if (left.breakdown.mode === "char-collector" && right.breakdown.mode === "char-collector") {
1890
+ const localeOrder = [];
1891
+ const mergedByLocale = /* @__PURE__ */ new Map();
1892
+ const addItems = (items) => {
1893
+ for (const item of items) {
1894
+ const existing = mergedByLocale.get(item.locale);
1895
+ if (existing) {
1896
+ existing.chars += item.chars;
1897
+ if (item.nonWords) {
1898
+ if (!existing.nonWords) existing.nonWords = createNonWordCollection();
1899
+ mergeNonWordCollections(existing.nonWords, item.nonWords);
1900
+ }
1901
+ continue;
1902
+ }
1903
+ localeOrder.push(item.locale);
1904
+ mergedByLocale.set(item.locale, {
1905
+ locale: item.locale,
1906
+ chars: item.chars,
1907
+ nonWords: item.nonWords ? mergeNonWordCollections(createNonWordCollection(), item.nonWords) : void 0
1908
+ });
1909
+ }
1910
+ };
1911
+ addItems(left.breakdown.items);
1912
+ addItems(right.breakdown.items);
1913
+ return {
1914
+ total,
1915
+ counts,
1916
+ breakdown: {
1917
+ mode: "char-collector",
1918
+ items: localeOrder.map((locale) => {
1919
+ const value = mergedByLocale.get(locale);
1920
+ if (!value) throw new Error(`Missing char-collector entry for locale: ${locale}`);
1921
+ return value;
1922
+ })
1923
+ }
1924
+ };
1925
+ }
1367
1926
  if (left.breakdown.mode === "collector" && right.breakdown.mode === "collector") {
1368
1927
  const localeOrder = [];
1369
1928
  const mergedByLocale = /* @__PURE__ */ new Map();
@@ -1526,7 +2085,8 @@ async function runBatchCount(options) {
1526
2085
  const resolved = await resolveBatchFilePaths(options.pathInputs, {
1527
2086
  pathMode: options.batchOptions.pathMode,
1528
2087
  recursive: options.batchOptions.recursive,
1529
- extensionFilter: options.extensionFilter
2088
+ extensionFilter: options.extensionFilter,
2089
+ debug: options.debug
1530
2090
  });
1531
2091
  const resolveElapsedMs = Date.now() - resolveStartedAtMs;
1532
2092
  options.debug.emit("batch.resolve.complete", {
@@ -1608,199 +2168,18 @@ async function runBatchCount(options) {
1608
2168
  }
1609
2169
 
1610
2170
  //#endregion
1611
- //#region src/cli/debug/channel.ts
1612
- function createDebugChannel(enabled) {
1613
- return {
1614
- enabled,
1615
- emit(event, details = {}) {
1616
- if (!enabled) return;
1617
- const payload = {
1618
- event,
1619
- ...details
1620
- };
1621
- console.error(`[debug] ${JSON.stringify(payload)}`);
1622
- }
1623
- };
1624
- }
1625
-
1626
- //#endregion
1627
- //#region src/utils/show-singular-or-plural-word.ts
1628
- function showSingularOrPluralWord(count, word) {
1629
- return `${count} ${word}${count === 1 ? "" : "s"}`;
1630
- }
1631
-
1632
- //#endregion
1633
- //#region src/cli/total-of.ts
1634
- const TOTAL_OF_PARTS = [
1635
- "words",
1636
- "emoji",
1637
- "symbols",
1638
- "punctuation",
1639
- "whitespace"
1640
- ];
1641
- const TOTAL_OF_PART_ALIASES = {
1642
- word: "words",
1643
- words: "words",
1644
- emoji: "emoji",
1645
- emojis: "emoji",
1646
- symbol: "symbols",
1647
- symbols: "symbols",
1648
- punction: "punctuation",
1649
- punctuation: "punctuation",
1650
- whitespace: "whitespace"
1651
- };
1652
- function createTotalOfCounts() {
1653
- return {
1654
- words: 0,
1655
- emoji: 0,
1656
- symbols: 0,
1657
- punctuation: 0,
1658
- whitespace: 0
1659
- };
1660
- }
1661
- function collectNonWordCounts(target, nonWords) {
1662
- if (!nonWords) return;
1663
- target.emoji += nonWords.counts.emoji;
1664
- target.symbols += nonWords.counts.symbols;
1665
- target.punctuation += nonWords.counts.punctuation;
1666
- target.whitespace += nonWords.counts.whitespace ?? 0;
1667
- }
1668
- function collectFromWordCounterResult(result) {
1669
- const counts = createTotalOfCounts();
1670
- counts.words += result.counts?.words ?? result.total;
1671
- if (result.breakdown.mode === "collector") {
1672
- collectNonWordCounts(counts, result.breakdown.nonWords);
1673
- return counts;
1674
- }
1675
- for (const item of result.breakdown.items) collectNonWordCounts(counts, item.nonWords);
1676
- return counts;
1677
- }
1678
- function collectTotalOfCounts(result) {
1679
- if (!("section" in result)) return collectFromWordCounterResult(result);
1680
- const counts = createTotalOfCounts();
1681
- for (const item of result.items) {
1682
- const itemCounts = collectFromWordCounterResult(item.result);
1683
- for (const part of TOTAL_OF_PARTS) counts[part] += itemCounts[part];
1684
- }
1685
- return counts;
1686
- }
1687
- function parseTotalOfToken(token) {
1688
- const canonical = TOTAL_OF_PART_ALIASES[token.trim().toLowerCase()];
1689
- if (canonical) return canonical;
1690
- throw new Error(`Invalid --total-of part: ${token}. Allowed: ${TOTAL_OF_PARTS.join(", ")}.`);
1691
- }
1692
- function parseTotalOfOption(value) {
1693
- const rawTokens = value.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
1694
- if (rawTokens.length === 0) throw new Error(`Invalid --total-of value: "${value}". Use comma-separated parts from: ${TOTAL_OF_PARTS.join(", ")}.`);
1695
- const parts = [];
1696
- const seen = /* @__PURE__ */ new Set();
1697
- for (const token of rawTokens) {
1698
- const parsed = parseTotalOfToken(token);
1699
- if (seen.has(parsed)) continue;
1700
- seen.add(parsed);
1701
- parts.push(parsed);
1702
- }
1703
- return parts;
1704
- }
1705
- function requiresNonWordCollection(parts) {
1706
- if (!parts || parts.length === 0) return false;
1707
- return parts.some((part) => part !== "words");
1708
- }
1709
- function requiresWhitespaceCollection(parts) {
1710
- if (!parts || parts.length === 0) return false;
1711
- return parts.includes("whitespace");
1712
- }
1713
- function resolveTotalOfOverride(result, parts) {
1714
- if (!parts || parts.length === 0) return;
1715
- const counts = collectTotalOfCounts(result);
1716
- let total = 0;
1717
- for (const part of parts) total += counts[part];
1718
- return {
1719
- parts: [...parts],
1720
- total
1721
- };
1722
- }
1723
- function formatTotalOfParts(parts) {
1724
- return parts.join(", ");
1725
- }
1726
-
1727
- //#endregion
1728
- //#region node_modules/picocolors/picocolors.js
1729
- var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
1730
- let p = process || {}, argv = p.argv || [], env = p.env || {};
1731
- let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
1732
- let formatter = (open, close, replace = open) => (input) => {
1733
- let string = "" + input, index = string.indexOf(close, open.length);
1734
- return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
1735
- };
1736
- let replaceClose = (string, close, replace, index) => {
1737
- let result = "", cursor = 0;
1738
- do {
1739
- result += string.substring(cursor, index) + replace;
1740
- cursor = index + close.length;
1741
- index = string.indexOf(close, cursor);
1742
- } while (~index);
1743
- return result + string.substring(cursor);
1744
- };
1745
- let createColors = (enabled = isColorSupported) => {
1746
- let f = enabled ? formatter : () => String;
1747
- return {
1748
- isColorSupported: enabled,
1749
- reset: f("\x1B[0m", "\x1B[0m"),
1750
- bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
1751
- dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
1752
- italic: f("\x1B[3m", "\x1B[23m"),
1753
- underline: f("\x1B[4m", "\x1B[24m"),
1754
- inverse: f("\x1B[7m", "\x1B[27m"),
1755
- hidden: f("\x1B[8m", "\x1B[28m"),
1756
- strikethrough: f("\x1B[9m", "\x1B[29m"),
1757
- black: f("\x1B[30m", "\x1B[39m"),
1758
- red: f("\x1B[31m", "\x1B[39m"),
1759
- green: f("\x1B[32m", "\x1B[39m"),
1760
- yellow: f("\x1B[33m", "\x1B[39m"),
1761
- blue: f("\x1B[34m", "\x1B[39m"),
1762
- magenta: f("\x1B[35m", "\x1B[39m"),
1763
- cyan: f("\x1B[36m", "\x1B[39m"),
1764
- white: f("\x1B[37m", "\x1B[39m"),
1765
- gray: f("\x1B[90m", "\x1B[39m"),
1766
- bgBlack: f("\x1B[40m", "\x1B[49m"),
1767
- bgRed: f("\x1B[41m", "\x1B[49m"),
1768
- bgGreen: f("\x1B[42m", "\x1B[49m"),
1769
- bgYellow: f("\x1B[43m", "\x1B[49m"),
1770
- bgBlue: f("\x1B[44m", "\x1B[49m"),
1771
- bgMagenta: f("\x1B[45m", "\x1B[49m"),
1772
- bgCyan: f("\x1B[46m", "\x1B[49m"),
1773
- bgWhite: f("\x1B[47m", "\x1B[49m"),
1774
- blackBright: f("\x1B[90m", "\x1B[39m"),
1775
- redBright: f("\x1B[91m", "\x1B[39m"),
1776
- greenBright: f("\x1B[92m", "\x1B[39m"),
1777
- yellowBright: f("\x1B[93m", "\x1B[39m"),
1778
- blueBright: f("\x1B[94m", "\x1B[39m"),
1779
- magentaBright: f("\x1B[95m", "\x1B[39m"),
1780
- cyanBright: f("\x1B[96m", "\x1B[39m"),
1781
- whiteBright: f("\x1B[97m", "\x1B[39m"),
1782
- bgBlackBright: f("\x1B[100m", "\x1B[49m"),
1783
- bgRedBright: f("\x1B[101m", "\x1B[49m"),
1784
- bgGreenBright: f("\x1B[102m", "\x1B[49m"),
1785
- bgYellowBright: f("\x1B[103m", "\x1B[49m"),
1786
- bgBlueBright: f("\x1B[104m", "\x1B[49m"),
1787
- bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
1788
- bgCyanBright: f("\x1B[106m", "\x1B[49m"),
1789
- bgWhiteBright: f("\x1B[107m", "\x1B[49m")
1790
- };
1791
- };
1792
- module.exports = createColors();
1793
- module.exports.createColors = createColors;
1794
- }));
2171
+ //#region src/utils/show-singular-or-plural-word.ts
2172
+ function showSingularOrPluralWord(count, word) {
2173
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
2174
+ }
1795
2175
 
1796
2176
  //#endregion
1797
2177
  //#region src/cli/output/render.ts
1798
- var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
1799
2178
  function getCountUnit(mode) {
1800
- return mode === "char" ? "character" : "word";
2179
+ return mode === "char" || mode === "char-collector" ? "character" : "word";
1801
2180
  }
1802
2181
  function getTotalLabels(mode, includeNonWords) {
1803
- const unit = mode === "char" ? "characters" : "words";
2182
+ const unit = mode === "char" || mode === "char-collector" ? "characters" : "words";
1804
2183
  if (includeNonWords) return {
1805
2184
  overall: "Total count",
1806
2185
  section: "total count"
@@ -1856,7 +2235,7 @@ function renderStandardResult(result, totalLabel, totalOfOverride) {
1856
2235
  renderNonWords(result.breakdown.nonWords, false);
1857
2236
  return;
1858
2237
  }
1859
- if (result.breakdown.mode === "char") {
2238
+ if (result.breakdown.mode === "char" || result.breakdown.mode === "char-collector") {
1860
2239
  renderCountBreakdown(result.breakdown.items.map((item) => ({
1861
2240
  locale: item.locale,
1862
2241
  count: item.chars,
@@ -1894,7 +2273,7 @@ function renderStandardSectionedResult(result, labels, totalOfOverride) {
1894
2273
  renderNonWords(item.result.breakdown.nonWords, false);
1895
2274
  continue;
1896
2275
  }
1897
- if (item.result.breakdown.mode === "char") {
2276
+ if (item.result.breakdown.mode === "char" || item.result.breakdown.mode === "char-collector") {
1898
2277
  renderCountBreakdown(item.result.breakdown.items.map((chunk) => ({
1899
2278
  locale: chunk.locale,
1900
2279
  count: chunk.chars,
@@ -1922,92 +2301,25 @@ function reportSkipped(skipped) {
1922
2301
  console.error(import_picocolors.default.yellow(`Skipped ${skipped.length} path(s):`));
1923
2302
  for (const item of skipped) console.error(import_picocolors.default.yellow(`- ${toDisplayPath(item.path)} (${item.reason})`));
1924
2303
  }
1925
- function renderPerFileStandard(summary, labels, resolveTotalOfOverride$1) {
2304
+ function renderPerFileStandard(summary, labels, resolveTotalOfOverride) {
1926
2305
  for (const file of summary.files) {
1927
2306
  console.log(import_picocolors.default.bold(`[File] ${toDisplayPath(file.path)}`));
1928
2307
  if (isSectionedResult(file.result)) {
1929
- renderStandardSectionedResult(file.result, labels, resolveTotalOfOverride$1?.(file.result));
2308
+ renderStandardSectionedResult(file.result, labels, resolveTotalOfOverride?.(file.result));
1930
2309
  continue;
1931
2310
  }
1932
- renderStandardResult(file.result, labels.overall, resolveTotalOfOverride$1?.(file.result));
2311
+ renderStandardResult(file.result, labels.overall, resolveTotalOfOverride?.(file.result));
1933
2312
  }
1934
2313
  console.log(import_picocolors.default.bold(`[Merged] ${summary.files.length} file(s)`));
1935
2314
  if (isSectionedResult(summary.aggregate)) {
1936
- renderStandardSectionedResult(summary.aggregate, labels, resolveTotalOfOverride$1?.(summary.aggregate));
2315
+ renderStandardSectionedResult(summary.aggregate, labels, resolveTotalOfOverride?.(summary.aggregate));
1937
2316
  return;
1938
2317
  }
1939
- renderStandardResult(summary.aggregate, labels.overall, resolveTotalOfOverride$1?.(summary.aggregate));
2318
+ renderStandardResult(summary.aggregate, labels.overall, resolveTotalOfOverride?.(summary.aggregate));
1940
2319
  }
1941
2320
 
1942
2321
  //#endregion
1943
- //#region src/command.ts
1944
- const MODE_CHOICES = [
1945
- "chunk",
1946
- "segments",
1947
- "collector",
1948
- "char"
1949
- ];
1950
- const FORMAT_CHOICES = [
1951
- "standard",
1952
- "raw",
1953
- "json"
1954
- ];
1955
- const SECTION_CHOICES = [
1956
- "all",
1957
- "split",
1958
- "frontmatter",
1959
- "content",
1960
- "per-key",
1961
- "split-per-key"
1962
- ];
1963
- const PATH_MODE_CHOICES = ["auto", "manual"];
1964
- function getPackageVersion() {
1965
- const packageCandidates = [new URL("../package.json", import.meta.url), new URL("../../package.json", import.meta.url)];
1966
- let version = "0.0.0";
1967
- for (const packageUrl of packageCandidates) try {
1968
- const raw = readFileSync(packageUrl, "utf8");
1969
- const data = JSON.parse(raw);
1970
- if (data.version) {
1971
- version = data.version;
1972
- break;
1973
- }
1974
- } catch {
1975
- continue;
1976
- }
1977
- return import_picocolors.default.bgBlack(import_picocolors.default.bold(import_picocolors.default.italic(` word-counter ${import_picocolors.default.cyanBright(`ver.${version}`)} `)));
1978
- }
1979
- async function readStdin() {
1980
- if (process.stdin.isTTY) return "";
1981
- return new Promise((resolve$1, reject) => {
1982
- const chunks = [];
1983
- process.stdin.setEncoding("utf8");
1984
- process.stdin.on("data", (chunk) => chunks.push(String(chunk)));
1985
- process.stdin.on("end", () => resolve$1(chunks.join("")));
1986
- process.stdin.on("error", (error) => reject(error));
1987
- process.stdin.resume();
1988
- });
1989
- }
1990
- async function resolveInput(textTokens) {
1991
- if (textTokens.length > 0) return textTokens.join(" ");
1992
- return readStdin();
1993
- }
1994
- function collectPathValue(value, previous = []) {
1995
- return [...previous, value];
1996
- }
1997
- function resolveBatchScope(argv) {
1998
- let scope = "merged";
1999
- for (const token of argv) {
2000
- if (token === "--merged") {
2001
- scope = "merged";
2002
- continue;
2003
- }
2004
- if (token === "--per-file") scope = "per-file";
2005
- }
2006
- return scope;
2007
- }
2008
- function hasPathInput(pathValues) {
2009
- return Array.isArray(pathValues) && pathValues.length > 0;
2010
- }
2322
+ //#region src/cli/output/normalize-base.ts
2011
2323
  function normalizeWordCounterResultBase(result) {
2012
2324
  result.total = result.counts?.words ?? result.total;
2013
2325
  delete result.counts;
@@ -2015,7 +2327,7 @@ function normalizeWordCounterResultBase(result) {
2015
2327
  delete result.breakdown.nonWords;
2016
2328
  return result;
2017
2329
  }
2018
- if (result.breakdown.mode === "char") {
2330
+ if (result.breakdown.mode === "char" || result.breakdown.mode === "char-collector") {
2019
2331
  for (const item of result.breakdown.items) {
2020
2332
  const nonWordCount = (item.nonWords?.counts.emoji ?? 0) + (item.nonWords?.counts.symbols ?? 0) + (item.nonWords?.counts.punctuation ?? 0) + (item.nonWords?.counts.whitespace ?? 0);
2021
2333
  item.chars = Math.max(0, item.chars - nonWordCount);
@@ -2044,24 +2356,41 @@ function normalizeBatchSummaryBase(summary) {
2044
2356
  normalizeResultBase(summary.aggregate);
2045
2357
  return summary;
2046
2358
  }
2047
- async function runCli(argv = process.argv, runtime = {}) {
2048
- const program = new Command();
2049
- const parseMode = (value) => {
2050
- const normalized = normalizeMode(value);
2051
- if (!normalized) throw new Error(`Invalid mode: ${value}`);
2052
- return normalized;
2053
- };
2054
- program.name("word-counter").description("Locale-aware word counting powered by Intl.Segmenter.").version(getPackageVersion(), "-v, --version", "output the version number").addOption(new Option("-m, --mode <mode>", "breakdown mode").choices(MODE_CHOICES).argParser(parseMode).default("chunk")).addOption(new Option("-f, --format <format>", "output format").choices(FORMAT_CHOICES).default("standard")).addOption(new Option("--section <section>", "document section mode").choices(SECTION_CHOICES).default("all")).addOption(new Option("--path-mode <mode>", "path resolution mode").choices(PATH_MODE_CHOICES).default("auto")).option("--latin-language <language>", "hint a language tag for Latin script text").option("--latin-tag <tag>", "hint a BCP 47 tag for Latin script text").option("--latin-locale <locale>", "legacy alias of --latin-language").option("--han-language <language>", "hint a language tag for Han script text").option("--han-tag <tag>", "hint a BCP 47 tag for Han script text").option("--non-words", "collect emoji, symbols, and punctuation (excludes whitespace)").option("--include-whitespace", "include whitespace counts (implies with --non-words; same as --misc)").option("--misc", "collect non-words plus whitespace (alias for --include-whitespace)").option("--total-of <parts>", "override total composition (comma-separated): words,emoji,symbols,punctuation,whitespace", parseTotalOfOption).option("--pretty", "pretty print JSON output", false).option("--debug", "enable debug diagnostics on stderr").option("--merged", "show merged aggregate output (default)").option("--per-file", "show per-file output plus merged summary").option("--no-progress", "disable batch progress indicator").option("--keep-progress", "keep final batch progress line visible in standard mode").option("--no-recursive", "disable recursive directory traversal").option("--quiet-skips", "hide skip diagnostics (applies when --debug is enabled)").option("--include-ext <exts>", "comma-separated extensions to include during directory scanning", collectExtensionOption, []).option("--exclude-ext <exts>", "comma-separated extensions to exclude during directory scanning", collectExtensionOption, []).option("-p, --path <path>", "read input from file or directory", collectPathValue, []).argument("[text...]", "text to count").showHelpAfterError();
2055
- program.action(async (textTokens, options) => {
2056
- const useSection = options.section !== "all";
2057
- const totalOfParts = options.totalOf;
2058
- const requestedNonWords = Boolean(options.nonWords || options.includeWhitespace || options.misc);
2059
- const collectNonWordsForOverride = requiresNonWordCollection(totalOfParts);
2060
- const collectWhitespaceForOverride = requiresWhitespaceCollection(totalOfParts);
2061
- const enableNonWords = Boolean(options.nonWords || options.includeWhitespace || options.misc || collectNonWordsForOverride);
2062
- const enableWhitespace = Boolean(options.includeWhitespace || options.misc || collectWhitespaceForOverride);
2063
- const shouldNormalizeBaseOutput = !requestedNonWords && enableNonWords;
2064
- const wcOptions = {
2359
+
2360
+ //#endregion
2361
+ //#region src/cli/runtime/options.ts
2362
+ function hasPathInput(pathValues) {
2363
+ return Array.isArray(pathValues) && pathValues.length > 0;
2364
+ }
2365
+ function resolveBatchScope(argv) {
2366
+ let scope = "merged";
2367
+ for (const token of argv) {
2368
+ if (token === "--merged") {
2369
+ scope = "merged";
2370
+ continue;
2371
+ }
2372
+ if (token === "--per-file") scope = "per-file";
2373
+ }
2374
+ return scope;
2375
+ }
2376
+ function resolveDebugReportPathOption(rawValue) {
2377
+ if (rawValue === void 0 || rawValue === false) return;
2378
+ if (typeof rawValue === "string") return rawValue;
2379
+ }
2380
+ function resolveCountRunOptions(options) {
2381
+ const useSection = options.section !== "all";
2382
+ const totalOfParts = options.totalOf;
2383
+ const requestedNonWords = Boolean(options.nonWords || options.includeWhitespace || options.misc);
2384
+ const collectNonWordsForOverride = requiresNonWordCollection(totalOfParts);
2385
+ const collectWhitespaceForOverride = requiresWhitespaceCollection(totalOfParts);
2386
+ const enableNonWords = Boolean(options.nonWords || options.includeWhitespace || options.misc || collectNonWordsForOverride);
2387
+ const enableWhitespace = Boolean(options.includeWhitespace || options.misc || collectWhitespaceForOverride);
2388
+ return {
2389
+ useSection,
2390
+ totalOfParts,
2391
+ requestedNonWords,
2392
+ shouldNormalizeBaseOutput: !requestedNonWords && enableNonWords,
2393
+ wcOptions: {
2065
2394
  mode: options.mode,
2066
2395
  latinLanguageHint: options.latinLanguage,
2067
2396
  latinTagHint: options.latinTag,
@@ -2070,152 +2399,255 @@ async function runCli(argv = process.argv, runtime = {}) {
2070
2399
  hanTagHint: options.hanTag,
2071
2400
  nonWords: enableNonWords,
2072
2401
  includeWhitespace: enableWhitespace
2073
- };
2074
- if (!hasPathInput(options.path)) {
2075
- let input;
2076
- try {
2077
- input = await resolveInput(textTokens);
2078
- } catch (error) {
2079
- const message = error instanceof Error ? error.message : String(error);
2080
- program.error(`Failed to read input: ${message}`);
2081
- return;
2082
- }
2083
- const trimmed = input.trim();
2084
- if (!trimmed) {
2085
- program.error(import_picocolors.default.red("No input provided. Pass text, pipe stdin, or use --path."));
2086
- return;
2087
- }
2088
- const result = useSection ? countSections(trimmed, options.section, wcOptions) : wc_default(trimmed, wcOptions);
2089
- const totalOfOverride = resolveTotalOfOverride(result, totalOfParts);
2090
- const displayResult = shouldNormalizeBaseOutput ? normalizeResultBase(result) : result;
2091
- if (options.format === "raw") {
2092
- console.log(totalOfOverride?.total ?? displayResult.total);
2093
- return;
2094
- }
2095
- if (options.format === "json") {
2096
- const spacing = options.pretty ? 2 : 0;
2097
- if (!totalOfOverride) {
2098
- console.log(JSON.stringify(displayResult, null, spacing));
2099
- return;
2100
- }
2101
- console.log(JSON.stringify({
2102
- ...displayResult,
2103
- meta: {
2104
- totalOf: totalOfOverride.parts,
2105
- totalOfOverride: totalOfOverride.total
2106
- }
2107
- }, null, spacing));
2108
- return;
2109
- }
2110
- const labels$1 = getTotalLabels(options.mode, requestedNonWords);
2111
- if (isSectionedResult(displayResult)) {
2112
- renderStandardSectionedResult(displayResult, labels$1, totalOfOverride);
2113
- return;
2114
- }
2115
- renderStandardResult(displayResult, labels$1.overall, totalOfOverride);
2116
- return;
2117
2402
  }
2118
- const batchOptions = {
2119
- scope: resolveBatchScope(argv),
2120
- pathMode: options.pathMode,
2121
- recursive: options.recursive,
2122
- quietSkips: Boolean(options.quietSkips)
2123
- };
2124
- const debug = createDebugChannel(Boolean(options.debug));
2125
- const extensionFilter = buildDirectoryExtensionFilter(options.includeExt, options.excludeExt);
2126
- const summary = await runBatchCount({
2127
- pathInputs: options.path,
2128
- batchOptions,
2129
- extensionFilter,
2130
- section: options.section,
2131
- wcOptions,
2132
- preserveCollectorSegments: options.format === "json",
2133
- debug,
2134
- progressReporter: createBatchProgressReporter({
2135
- enabled: options.format === "standard" && options.progress,
2136
- stream: runtime.stderr ?? process.stderr,
2137
- clearOnFinish: !Boolean(options.debug || options.keepProgress)
2138
- })
2139
- });
2140
- const showSkipDiagnostics = Boolean(options.debug) && !batchOptions.quietSkips;
2141
- debug.emit("batch.skips.policy", {
2142
- enabled: showSkipDiagnostics,
2143
- quietSkips: batchOptions.quietSkips
2144
- });
2145
- if (showSkipDiagnostics) {
2146
- debug.emit("batch.skips.report", { count: summary.skipped.length });
2147
- reportSkipped(summary.skipped);
2403
+ };
2404
+ }
2405
+ function formatInputReadError(error) {
2406
+ return `Failed to read input: ${error instanceof Error ? error.message : String(error)}`;
2407
+ }
2408
+
2409
+ //#endregion
2410
+ //#region src/cli/runtime/batch.ts
2411
+ async function executeBatchCount({ argv, options, runtime, resolved, debug, teeEnabled }) {
2412
+ const batchOptions = {
2413
+ scope: resolveBatchScope(argv),
2414
+ pathMode: options.pathMode,
2415
+ recursive: options.recursive,
2416
+ quietSkips: Boolean(options.quietSkips)
2417
+ };
2418
+ const extensionFilter = buildDirectoryExtensionFilter(options.includeExt, options.excludeExt);
2419
+ const debugEnabled = Boolean(options.debug);
2420
+ const mirrorDebugToTerminal = debugEnabled && (!debug.reportPath || teeEnabled);
2421
+ const summary = await runBatchCount({
2422
+ pathInputs: options.path ?? [],
2423
+ batchOptions,
2424
+ extensionFilter,
2425
+ section: options.section,
2426
+ wcOptions: resolved.wcOptions,
2427
+ preserveCollectorSegments: options.format === "json",
2428
+ debug,
2429
+ progressReporter: createBatchProgressReporter({
2430
+ enabled: options.format === "standard" && options.progress,
2431
+ stream: runtime.stderr ?? process.stderr,
2432
+ clearOnFinish: !(mirrorDebugToTerminal || options.keepProgress)
2433
+ })
2434
+ });
2435
+ const showSkipDiagnostics = debugEnabled && !batchOptions.quietSkips;
2436
+ debug.emit("batch.skips.policy", {
2437
+ enabled: showSkipDiagnostics,
2438
+ quietSkips: batchOptions.quietSkips
2439
+ });
2440
+ if (showSkipDiagnostics) {
2441
+ debug.emit("batch.skips.report", { count: summary.skipped.length });
2442
+ if (options.verbose) for (const skip of summary.skipped) debug.emit("batch.skips.item", {
2443
+ path: skip.path,
2444
+ reason: skip.reason
2445
+ }, { verbosity: "verbose" });
2446
+ if (mirrorDebugToTerminal) reportSkipped(summary.skipped);
2447
+ }
2448
+ if (summary.files.length === 0) throw new Error("No readable text-like inputs were found from --path.");
2449
+ let aggregateTotalOfOverride;
2450
+ let totalOfOverridesByResult;
2451
+ if (resolved.totalOfParts && resolved.totalOfParts.length > 0) {
2452
+ totalOfOverridesByResult = /* @__PURE__ */ new WeakMap();
2453
+ const aggregateOverride = resolveTotalOfOverride(summary.aggregate, resolved.totalOfParts);
2454
+ if (aggregateOverride) {
2455
+ totalOfOverridesByResult.set(summary.aggregate, aggregateOverride);
2456
+ aggregateTotalOfOverride = aggregateOverride;
2457
+ }
2458
+ for (const file of summary.files) {
2459
+ const fileOverride = resolveTotalOfOverride(file.result, resolved.totalOfParts);
2460
+ if (!fileOverride) continue;
2461
+ totalOfOverridesByResult.set(file.result, fileOverride);
2462
+ }
2463
+ } else aggregateTotalOfOverride = resolveTotalOfOverride(summary.aggregate, resolved.totalOfParts);
2464
+ if (resolved.shouldNormalizeBaseOutput) normalizeBatchSummaryBase(summary);
2465
+ if (!aggregateTotalOfOverride && totalOfOverridesByResult) aggregateTotalOfOverride = totalOfOverridesByResult.get(summary.aggregate);
2466
+ if (options.format === "raw") {
2467
+ console.log(aggregateTotalOfOverride?.total ?? summary.aggregate.total);
2468
+ return;
2469
+ }
2470
+ if (options.format === "json") {
2471
+ const spacing = options.pretty ? 2 : 0;
2472
+ if (batchOptions.scope === "per-file") {
2473
+ const skipped = showSkipDiagnostics ? summary.skipped : void 0;
2474
+ const meta = resolved.totalOfParts && resolved.totalOfParts.length > 0 ? {
2475
+ totalOf: resolved.totalOfParts,
2476
+ aggregateTotalOfOverride: aggregateTotalOfOverride?.total ?? summary.aggregate.total
2477
+ } : void 0;
2478
+ const payload = {
2479
+ scope: "per-file",
2480
+ files: summary.files.map((file) => ({
2481
+ path: file.path,
2482
+ result: file.result
2483
+ })),
2484
+ ...skipped ? { skipped } : {},
2485
+ aggregate: summary.aggregate,
2486
+ ...meta ? { meta } : {}
2487
+ };
2488
+ console.log(JSON.stringify(payload, null, spacing));
2489
+ return;
2148
2490
  }
2149
- if (summary.files.length === 0) {
2150
- program.error(import_picocolors.default.red("No readable text-like inputs were found from --path."));
2491
+ if (!aggregateTotalOfOverride) {
2492
+ console.log(JSON.stringify(summary.aggregate, null, spacing));
2151
2493
  return;
2152
2494
  }
2153
- let aggregateTotalOfOverride;
2154
- let totalOfOverridesByResult;
2155
- if (totalOfParts && totalOfParts.length > 0) {
2156
- totalOfOverridesByResult = /* @__PURE__ */ new WeakMap();
2157
- const aggregateOverride = resolveTotalOfOverride(summary.aggregate, totalOfParts);
2158
- if (aggregateOverride) {
2159
- totalOfOverridesByResult.set(summary.aggregate, aggregateOverride);
2160
- aggregateTotalOfOverride = aggregateOverride;
2495
+ console.log(JSON.stringify({
2496
+ ...summary.aggregate,
2497
+ meta: {
2498
+ totalOf: aggregateTotalOfOverride.parts,
2499
+ totalOfOverride: aggregateTotalOfOverride.total
2161
2500
  }
2162
- for (const file of summary.files) {
2163
- const fileOverride = resolveTotalOfOverride(file.result, totalOfParts);
2164
- if (!fileOverride) continue;
2165
- totalOfOverridesByResult.set(file.result, fileOverride);
2166
- }
2167
- } else aggregateTotalOfOverride = resolveTotalOfOverride(summary.aggregate, totalOfParts);
2168
- if (shouldNormalizeBaseOutput) normalizeBatchSummaryBase(summary);
2169
- if (!aggregateTotalOfOverride && totalOfOverridesByResult) aggregateTotalOfOverride = totalOfOverridesByResult.get(summary.aggregate);
2170
- if (options.format === "raw") {
2171
- console.log(aggregateTotalOfOverride?.total ?? summary.aggregate.total);
2501
+ }, null, spacing));
2502
+ return;
2503
+ }
2504
+ const labels = getTotalLabels(options.mode, resolved.requestedNonWords);
2505
+ const totalOfResolver = resolved.totalOfParts && resolved.totalOfParts.length > 0 ? (result) => totalOfOverridesByResult?.get(result) ?? resolveTotalOfOverride(result, resolved.totalOfParts) : void 0;
2506
+ if (batchOptions.scope === "per-file") {
2507
+ renderPerFileStandard(summary, labels, totalOfResolver);
2508
+ return;
2509
+ }
2510
+ if (isSectionedResult(summary.aggregate)) {
2511
+ renderStandardSectionedResult(summary.aggregate, labels, aggregateTotalOfOverride);
2512
+ return;
2513
+ }
2514
+ renderStandardResult(summary.aggregate, labels.overall, aggregateTotalOfOverride);
2515
+ }
2516
+
2517
+ //#endregion
2518
+ //#region src/cli/runtime/input.ts
2519
+ async function readStdin() {
2520
+ if (process.stdin.isTTY) return "";
2521
+ return new Promise((resolve, reject) => {
2522
+ const chunks = [];
2523
+ process.stdin.setEncoding("utf8");
2524
+ process.stdin.on("data", (chunk) => chunks.push(String(chunk)));
2525
+ process.stdin.on("end", () => resolve(chunks.join("")));
2526
+ process.stdin.on("error", (error) => reject(error));
2527
+ process.stdin.resume();
2528
+ });
2529
+ }
2530
+ async function resolveInput(textTokens) {
2531
+ if (textTokens.length > 0) return textTokens.join(" ");
2532
+ return readStdin();
2533
+ }
2534
+
2535
+ //#endregion
2536
+ //#region src/cli/runtime/single.ts
2537
+ async function executeSingleCount({ textTokens, options, resolved }) {
2538
+ let input;
2539
+ try {
2540
+ input = await resolveInput(textTokens);
2541
+ } catch (error) {
2542
+ throw new Error(formatInputReadError(error));
2543
+ }
2544
+ const trimmed = input.trim();
2545
+ if (!trimmed) throw new Error("No input provided. Pass text, pipe stdin, or use --path.");
2546
+ const result = resolved.useSection ? countSections(trimmed, options.section, resolved.wcOptions) : wc_default(trimmed, resolved.wcOptions);
2547
+ const totalOfOverride = resolveTotalOfOverride(result, resolved.totalOfParts);
2548
+ const displayResult = resolved.shouldNormalizeBaseOutput ? normalizeResultBase(result) : result;
2549
+ if (options.format === "raw") {
2550
+ console.log(totalOfOverride?.total ?? displayResult.total);
2551
+ return;
2552
+ }
2553
+ if (options.format === "json") {
2554
+ const spacing = options.pretty ? 2 : 0;
2555
+ if (!totalOfOverride) {
2556
+ console.log(JSON.stringify(displayResult, null, spacing));
2172
2557
  return;
2173
2558
  }
2174
- if (options.format === "json") {
2175
- const spacing = options.pretty ? 2 : 0;
2176
- if (batchOptions.scope === "per-file") {
2177
- const skipped = showSkipDiagnostics ? summary.skipped : void 0;
2178
- const meta = totalOfParts && totalOfParts.length > 0 ? {
2179
- totalOf: totalOfParts,
2180
- aggregateTotalOfOverride: aggregateTotalOfOverride?.total ?? summary.aggregate.total
2181
- } : void 0;
2182
- const payload = {
2183
- scope: "per-file",
2184
- files: summary.files.map((file) => ({
2185
- path: file.path,
2186
- result: file.result
2187
- })),
2188
- ...skipped ? { skipped } : {},
2189
- aggregate: summary.aggregate,
2190
- ...meta ? { meta } : {}
2191
- };
2192
- console.log(JSON.stringify(payload, null, spacing));
2193
- return;
2194
- }
2195
- if (!aggregateTotalOfOverride) {
2196
- console.log(JSON.stringify(summary.aggregate, null, spacing));
2197
- return;
2559
+ console.log(JSON.stringify({
2560
+ ...displayResult,
2561
+ meta: {
2562
+ totalOf: totalOfOverride.parts,
2563
+ totalOfOverride: totalOfOverride.total
2198
2564
  }
2199
- console.log(JSON.stringify({
2200
- ...summary.aggregate,
2201
- meta: {
2202
- totalOf: aggregateTotalOfOverride.parts,
2203
- totalOfOverride: aggregateTotalOfOverride.total
2204
- }
2205
- }, null, spacing));
2565
+ }, null, spacing));
2566
+ return;
2567
+ }
2568
+ const labels = getTotalLabels(options.mode, resolved.requestedNonWords);
2569
+ if (isSectionedResult(displayResult)) {
2570
+ renderStandardSectionedResult(displayResult, labels, totalOfOverride);
2571
+ return;
2572
+ }
2573
+ renderStandardResult(displayResult, labels.overall, totalOfOverride);
2574
+ }
2575
+
2576
+ //#endregion
2577
+ //#region src/command.ts
2578
+ async function runCli(argv = process.argv, runtime = {}) {
2579
+ const program = new Command();
2580
+ const parseMode = (value) => {
2581
+ const normalized = normalizeMode(value);
2582
+ if (!normalized) throw new Error(`Invalid mode: ${value}`);
2583
+ return normalized;
2584
+ };
2585
+ program.name("word-counter").description("Locale-aware word counting powered by Intl.Segmenter.").version(getFormattedVersionLabel(), "-v, --version", "output the version number");
2586
+ configureProgramOptions(program, parseMode);
2587
+ program.action(async (textTokens, options) => {
2588
+ const debugEnabled = Boolean(options.debug);
2589
+ const debugReportPath = resolveDebugReportPathOption(options.debugReport);
2590
+ const debugReportEnabled = options.debugReport !== void 0 && options.debugReport !== false;
2591
+ if (options.verbose && !debugEnabled) {
2592
+ program.error(import_picocolors.default.red("`--verbose` requires `--debug`."));
2206
2593
  return;
2207
2594
  }
2208
- const labels = getTotalLabels(options.mode, requestedNonWords);
2209
- const totalOfResolver = totalOfParts && totalOfParts.length > 0 ? (result) => totalOfOverridesByResult?.get(result) ?? resolveTotalOfOverride(result, totalOfParts) : void 0;
2210
- if (batchOptions.scope === "per-file") {
2211
- renderPerFileStandard(summary, labels, totalOfResolver);
2595
+ if (debugReportEnabled && !debugEnabled) {
2596
+ program.error(import_picocolors.default.red("`--debug-report` requires `--debug`."));
2597
+ return;
2598
+ }
2599
+ const teeEnabled = Boolean(options.debugReportTee || options.debugTee);
2600
+ if (teeEnabled && !debugReportEnabled) {
2601
+ program.error(import_picocolors.default.red("`--debug-report-tee` (alias: `--debug-tee`) requires `--debug-report`."));
2212
2602
  return;
2213
2603
  }
2214
- if (isSectionedResult(summary.aggregate)) {
2215
- renderStandardSectionedResult(summary.aggregate, labels, aggregateTotalOfOverride);
2604
+ let debug;
2605
+ try {
2606
+ debug = createDebugChannel({
2607
+ enabled: debugEnabled,
2608
+ verbosity: options.verbose ? "verbose" : "compact",
2609
+ report: debugReportEnabled ? {
2610
+ path: debugReportPath,
2611
+ tee: teeEnabled
2612
+ } : void 0
2613
+ });
2614
+ } catch (error) {
2615
+ const message = error instanceof Error ? error.message : String(error);
2616
+ program.error(import_picocolors.default.red(`Failed to initialize debug diagnostics: ${message}`));
2216
2617
  return;
2217
2618
  }
2218
- renderStandardResult(summary.aggregate, labels.overall, aggregateTotalOfOverride);
2619
+ try {
2620
+ const resolved = resolveCountRunOptions(options);
2621
+ if (hasPathInput(options.path)) {
2622
+ await executeBatchCount({
2623
+ argv,
2624
+ options,
2625
+ runtime,
2626
+ resolved,
2627
+ debug,
2628
+ teeEnabled
2629
+ });
2630
+ return;
2631
+ }
2632
+ await executeSingleCount({
2633
+ textTokens,
2634
+ options,
2635
+ resolved
2636
+ });
2637
+ } catch (error) {
2638
+ const message = error instanceof Error ? error.message : String(error);
2639
+ if (message === "No input provided. Pass text, pipe stdin, or use --path.") {
2640
+ program.error(import_picocolors.default.red(message));
2641
+ return;
2642
+ }
2643
+ if (message === "No readable text-like inputs were found from --path.") {
2644
+ program.error(import_picocolors.default.red(message));
2645
+ return;
2646
+ }
2647
+ program.error(message);
2648
+ } finally {
2649
+ await debug.close();
2650
+ }
2219
2651
  });
2220
2652
  await program.parseAsync(argv);
2221
2653
  }