fln 1.1.3 → 1.2.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.
Files changed (67) hide show
  1. package/README.md +335 -179
  2. package/dist/api/fln.js +67 -30
  3. package/dist/api/types.d.ts +12 -1
  4. package/dist/cli/commandLine.js +68 -18
  5. package/dist/cli/help.d.ts +1 -1
  6. package/dist/cli/help.js +20 -8
  7. package/dist/cli/output/components/index.d.ts +0 -1
  8. package/dist/cli/output/components/index.js +0 -1
  9. package/dist/cli/output/renderer.d.ts +1 -3
  10. package/dist/cli/output/renderer.js +2 -5
  11. package/dist/config/defaults.d.ts +1 -1
  12. package/dist/config/defaults.js +1 -1
  13. package/dist/config/index.d.ts +1 -0
  14. package/dist/config/index.js +1 -0
  15. package/dist/config/initTemplate.d.ts +1 -0
  16. package/dist/config/initTemplate.js +32 -0
  17. package/dist/config/loader.d.ts +6 -1
  18. package/dist/config/loader.js +10 -4
  19. package/dist/config/resolver.d.ts +5 -2
  20. package/dist/config/resolver.js +31 -21
  21. package/dist/config/types.d.ts +18 -7
  22. package/dist/config/utils.d.ts +5 -2
  23. package/dist/config/utils.js +54 -48
  24. package/dist/core/ignoreMatcher.d.ts +3 -2
  25. package/dist/core/ignoreMatcher.js +32 -34
  26. package/dist/core/renderOutput.js +46 -45
  27. package/dist/core/scanTree.js +47 -70
  28. package/dist/core/size.js +6 -20
  29. package/dist/core/types.d.ts +6 -6
  30. package/dist/index.d.ts +0 -2
  31. package/dist/index.js +0 -2
  32. package/dist/infra/datetime.js +8 -6
  33. package/dist/infra/deprecate.d.ts +2 -0
  34. package/dist/infra/deprecate.js +14 -0
  35. package/dist/infra/gitDiff.d.ts +2 -0
  36. package/dist/infra/gitDiff.js +33 -0
  37. package/dist/infra/index.d.ts +2 -0
  38. package/dist/infra/index.js +2 -0
  39. package/dist/infra/logger.d.ts +1 -1
  40. package/dist/infra/logger.js +15 -18
  41. package/dist/infra/outputWriter.d.ts +1 -1
  42. package/dist/infra/outputWriter.js +21 -3
  43. package/dist/infra/terminal.d.ts +4 -32
  44. package/dist/infra/terminal.js +26 -54
  45. package/dist/path/canonical.d.ts +1 -0
  46. package/dist/path/canonical.js +13 -0
  47. package/dist/path/ignoreSafe.d.ts +1 -0
  48. package/dist/path/ignoreSafe.js +15 -0
  49. package/dist/path/index.d.ts +6 -0
  50. package/dist/path/index.js +6 -0
  51. package/dist/path/normalize.d.ts +2 -0
  52. package/dist/path/normalize.js +9 -0
  53. package/dist/path/output.d.ts +4 -0
  54. package/dist/path/output.js +12 -0
  55. package/dist/path/posix.d.ts +1 -0
  56. package/dist/path/posix.js +4 -0
  57. package/dist/path/resolve.d.ts +1 -0
  58. package/dist/path/resolve.js +6 -0
  59. package/dist/pattern/index.d.ts +1 -0
  60. package/dist/pattern/index.js +1 -0
  61. package/dist/pattern/normalize.d.ts +2 -0
  62. package/dist/pattern/normalize.js +43 -0
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/package.json +7 -3
  66. package/dist/cli/output/components/progressBar.d.ts +0 -6
  67. package/dist/cli/output/components/progressBar.js +0 -38
@@ -2,12 +2,10 @@ import { lstat, open, readdir, readlink, realpath, stat } from "node:fs/promises
2
2
  import { cpus } from "node:os";
3
3
  import { relative, sep } from "node:path";
4
4
  import ignore from "ignore";
5
+ import pLimit from "p-limit";
6
+ import { toCanonicalRelative, toIgnoreSafePath, toPosixPath } from "../path/index.js";
7
+ import { normalizeIncludePattern } from "../pattern/index.js";
5
8
  import { IgnoreMatcher } from "./ignoreMatcher.js";
6
- function normalizePathSegment(pathSegment) {
7
- if (sep === "/")
8
- return pathSegment;
9
- return pathSegment.split(sep).join("/");
10
- }
11
9
  function getFileScore(fileName) {
12
10
  const lowerName = fileName.toLowerCase();
13
11
  if (lowerName.startsWith("readme"))
@@ -21,6 +19,7 @@ function getFileScore(fileName) {
21
19
  lowerName === "makefile" ||
22
20
  lowerName === "dockerfile" ||
23
21
  lowerName === "vcpkg.json" ||
22
+ lowerName === "pom.xml" ||
24
23
  lowerName.startsWith(".env") ||
25
24
  lowerName.includes(".config.") ||
26
25
  lowerName.startsWith(".prettier") ||
@@ -55,34 +54,17 @@ function getFileScore(fileName) {
55
54
  return 20;
56
55
  return 10;
57
56
  }
58
- async function isBinaryFile(filePath, fileSize) {
57
+ async function inspectFile(filePath, fileSize) {
59
58
  if (fileSize === 0)
60
- return false;
59
+ return { isGenerated: false, isBinary: false };
61
60
  const handle = await open(filePath, "r");
62
61
  try {
63
62
  const buffer = Buffer.alloc(Math.min(512, fileSize));
64
63
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
65
- for (let index = 0; index < bytesRead; index++)
66
- if (buffer[index] === 0)
67
- return true;
68
- return false;
69
- }
70
- finally {
71
- await handle.close();
72
- }
73
- }
74
- async function isGeneratedFile(filePath, fileSize) {
75
- if (fileSize === 0)
76
- return false;
77
- const handle = await open(filePath, "r");
78
- try {
79
- const buffer = Buffer.alloc(Math.min(100, fileSize));
80
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
81
- const content = buffer.toString("utf8", 0, bytesRead);
82
- return content.includes("<!-- 🥞 fln");
83
- }
84
- catch {
85
- return false;
64
+ const header = buffer.toString("utf8", 0, Math.min(100, bytesRead));
65
+ const isGenerated = header.includes("<!-- 🥞 fln");
66
+ const isBinary = !isGenerated && buffer.slice(0, bytesRead).includes(0);
67
+ return { isGenerated, isBinary };
86
68
  }
87
69
  finally {
88
70
  await handle.close();
@@ -100,37 +82,52 @@ export async function scanTree(options, logger) {
100
82
  outputTokenCount: 0
101
83
  };
102
84
  const ignoreMatcher = new IgnoreMatcher({
103
- rootDirectory: options.rootDirectory,
85
+ input: options.input,
104
86
  excludePatterns: options.excludePatterns,
105
- useGitignore: options.useGitignore,
87
+ gitignore: options.gitignore,
106
88
  logger
107
89
  });
108
- const includeMatcher = ignore().add(options.includePatterns);
90
+ const normalizedIncludePatterns = options.includePatterns
91
+ .map(pattern => normalizeIncludePattern(pattern, options.input))
92
+ .filter((p) => p !== null);
93
+ const includeMatcher = ignore().add(normalizedIncludePatterns);
109
94
  const concurrencyLimit = Math.max(8, Math.min(64, cpus().length * 4));
110
- const excludedPathSet = new Set(options.excludedPaths.map(pathItem => normalizePathSegment(pathItem)));
95
+ const limit = pLimit(concurrencyLimit);
96
+ const excludedPathSet = new Set(options.excludedPaths
97
+ .map(path => toCanonicalRelative(path, options.input))
98
+ .filter((p) => p !== null && p !== ""));
111
99
  const visitedRealPaths = new Set();
112
100
  let processedItems = 0;
113
101
  let totalEstimate = 0;
114
102
  if (options.followSymlinks)
115
103
  try {
116
- const rootRealPath = await realpath(options.rootDirectory);
104
+ const rootRealPath = await realpath(options.input);
117
105
  visitedRealPaths.add(rootRealPath);
118
106
  }
119
107
  catch {
120
108
  logger.debug("Failed to resolve root real path.");
121
109
  }
122
- const rootNode = await scanEntry(options.rootDirectory, "");
110
+ const rootNode = await scanEntry(options.input, "");
123
111
  if (!rootNode || rootNode.type !== "directory")
124
112
  throw new Error("Root directory is empty or all files were excluded.");
125
113
  return { projectName: options.projectName, root: rootNode, stats };
126
114
  async function scanEntry(currentPath, relativePath, dirent) {
127
- const normalizedRelativePath = normalizePathSegment(relativePath);
115
+ const normalizedRelativePath = toPosixPath(relativePath);
128
116
  const name = dirent?.name ?? currentPath.split(sep).pop() ?? "";
129
117
  if (normalizedRelativePath !== "" && excludedPathSet.has(normalizedRelativePath))
130
118
  return undefined;
131
- const isExplicitlyIncluded = normalizedRelativePath !== "" && includeMatcher.ignores(normalizedRelativePath);
132
- const pathForIgnoreCheck = normalizedRelativePath === "" ? "" : (dirent?.isDirectory() ? `${normalizedRelativePath}/` : normalizedRelativePath);
133
- if (!isExplicitlyIncluded && pathForIgnoreCheck !== "" && ignoreMatcher.ignores(pathForIgnoreCheck))
119
+ const pathForCheck = normalizedRelativePath === "" ? "" : (dirent?.isDirectory() ? `${normalizedRelativePath}/` : normalizedRelativePath);
120
+ const safePath = toIgnoreSafePath(pathForCheck, options.input);
121
+ const isExplicitlyIncluded = safePath !== null &&
122
+ safePath !== "" &&
123
+ includeMatcher.ignores(safePath);
124
+ const isDirectory = pathForCheck.endsWith("/");
125
+ if (normalizedIncludePatterns.length > 0 &&
126
+ !isExplicitlyIncluded &&
127
+ pathForCheck !== "" &&
128
+ !isDirectory)
129
+ return undefined;
130
+ if (!isExplicitlyIncluded && pathForCheck !== "" && ignoreMatcher.ignoresSafePath(safePath))
134
131
  return undefined;
135
132
  if (!options.includeHidden && name.startsWith(".") && name !== ".")
136
133
  return undefined;
@@ -255,20 +252,23 @@ export async function scanTree(options, logger) {
255
252
  if (options.onProgress)
256
253
  options.onProgress(processedItems, Math.max(totalEstimate, processedItems));
257
254
  let skipReason;
258
- if (!input.isExplicitlyIncluded && await isGeneratedFile(input.currentPath, input.fileSize))
259
- skipReason = "generated";
260
- if (!skipReason && input.fileSize > options.maximumFileSizeBytes)
261
- skipReason = "tooLarge";
262
255
  let isBinary = false;
263
- if (!skipReason)
256
+ const needsRead = input.fileSize > 0 &&
257
+ (input.fileSize <= options.maxFileSize || !input.isExplicitlyIncluded);
258
+ if (needsRead)
264
259
  try {
265
- isBinary = await isBinaryFile(input.currentPath, input.fileSize);
260
+ const { isGenerated, isBinary: binary } = await inspectFile(input.currentPath, input.fileSize);
261
+ if (!input.isExplicitlyIncluded && isGenerated)
262
+ skipReason = "generated";
263
+ isBinary = binary;
266
264
  }
267
265
  catch (error) {
268
266
  stats.errors++;
269
267
  skipReason = "readError";
270
268
  logger.warn(`Failed to read ${input.normalizedRelativePath || "."}: ${String(error)}`);
271
269
  }
270
+ if (!skipReason && input.fileSize > options.maxFileSize)
271
+ skipReason = "tooLarge";
272
272
  if (isBinary)
273
273
  stats.binary++;
274
274
  if (skipReason) {
@@ -297,11 +297,11 @@ export async function scanTree(options, logger) {
297
297
  await ignoreMatcher.addGitignoreForDirectory(input.currentPath);
298
298
  const entries = await readdir(input.currentPath, { withFileTypes: true });
299
299
  totalEstimate = Math.max(totalEstimate, processedItems + entries.length);
300
- const children = (await mapWithConcurrency(entries, concurrencyLimit, async (entry) => {
300
+ const children = (await Promise.all(entries.map(entry => limit(() => {
301
301
  const childPath = `${input.currentPath}${sep}${entry.name}`;
302
- const childRelativePath = relative(options.rootDirectory, childPath);
302
+ const childRelativePath = relative(options.input, childPath);
303
303
  return scanEntry(childPath, childRelativePath, entry);
304
- }))
304
+ }))))
305
305
  .filter((node) => node !== undefined)
306
306
  .sort((left, right) => {
307
307
  if (left.type !== right.type)
@@ -324,26 +324,3 @@ export async function scanTree(options, logger) {
324
324
  };
325
325
  }
326
326
  }
327
- async function mapWithConcurrency(items, concurrency, mapper) {
328
- if (items.length === 0)
329
- return [];
330
- const limit = Math.max(1, Math.min(concurrency, items.length));
331
- if (limit === 1) {
332
- const results = [];
333
- for (const item of items)
334
- results.push(await mapper(item));
335
- return results;
336
- }
337
- const results = new Array(items.length);
338
- let nextIndex = 0;
339
- const workers = Array.from({ length: limit }, async () => {
340
- while (true) {
341
- const currentIndex = nextIndex++;
342
- if (currentIndex >= items.length)
343
- return;
344
- results[currentIndex] = await mapper(items[currentIndex]);
345
- }
346
- });
347
- await Promise.all(workers);
348
- return results;
349
- }
package/dist/core/size.js CHANGED
@@ -1,27 +1,13 @@
1
- const kibibyte = 1024;
2
- const mebibyte = kibibyte * 1024;
3
- const gibibyte = mebibyte * 1024;
1
+ import bytes from "bytes";
4
2
  export function parseByteSize(input) {
5
- const normalizedInput = input.trim().toLowerCase();
6
- const match = normalizedInput.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
7
- if (!match)
3
+ const result = bytes.parse(input.trim());
4
+ if (result === null || result < 0)
8
5
  throw new Error(`Invalid size: "${input}"`);
9
- const value = Number(match[1]);
10
- const unit = match[2] ?? "b";
11
- const multiplier = unit === "kb" ? kibibyte :
12
- unit === "mb" ? mebibyte :
13
- unit === "gb" ? gibibyte :
14
- 1;
15
- return Math.floor(value * multiplier);
6
+ return Math.floor(result);
16
7
  }
17
8
  export function formatByteSize(sizeBytes) {
18
- if (sizeBytes >= gibibyte)
19
- return `${(sizeBytes / gibibyte).toFixed(2)} GB`;
20
- if (sizeBytes >= mebibyte)
21
- return `${(sizeBytes / mebibyte).toFixed(2)} MB`;
22
- if (sizeBytes >= kibibyte)
23
- return `${(sizeBytes / kibibyte).toFixed(2)} KB`;
24
- return `${sizeBytes} B`;
9
+ const result = bytes(sizeBytes, { unitSeparator: " " });
10
+ return result ?? `${sizeBytes} B`;
25
11
  }
26
12
  export function formatTokenCount(count) {
27
13
  if (count >= 1_000_000)
@@ -30,23 +30,23 @@ export type ScanResult = {
30
30
  export type ProgressCallback = (current: number, total: number) => void;
31
31
  export type ScanOptions = {
32
32
  projectName: string;
33
- rootDirectory: string;
33
+ input: string;
34
34
  excludePatterns: string[];
35
35
  includePatterns: string[];
36
36
  excludedPaths: string[];
37
37
  includeHidden: boolean;
38
- useGitignore: boolean;
39
- maximumFileSizeBytes: number;
40
- maximumTotalSizeBytes: number;
38
+ gitignore: boolean;
39
+ maxFileSize: number;
40
+ maxTotalSize: number;
41
41
  followSymlinks: boolean;
42
42
  onProgress?: ProgressCallback;
43
43
  };
44
44
  export type RenderOptions = {
45
- outputFile: string;
45
+ output: string;
46
46
  format: OutputFormat;
47
47
  includeTree: boolean;
48
48
  includeContents: boolean;
49
- useAnsi: boolean;
49
+ ansi: boolean;
50
50
  banner?: string;
51
51
  footer?: string;
52
52
  };
package/dist/index.d.ts CHANGED
@@ -1,3 +1 @@
1
1
  export * from "./api/index.js";
2
- export * from "./core/index.js";
3
- export * from "./version.js";
package/dist/index.js CHANGED
@@ -1,3 +1 @@
1
1
  export * from "./api/index.js";
2
- export * from "./core/index.js";
3
- export * from "./version.js";
@@ -1,12 +1,14 @@
1
1
  const generatedDateRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/;
2
2
  export function formatDateTime() {
3
3
  const now = new Date();
4
- const year = now.getFullYear();
5
- const month = String(now.getMonth() + 1).padStart(2, "0");
6
- const day = String(now.getDate()).padStart(2, "0");
7
- const hours = String(now.getHours()).padStart(2, "0");
8
- const minutes = String(now.getMinutes()).padStart(2, "0");
9
- return `${year}-${month}-${day} ${hours}:${minutes}`;
4
+ return `${[
5
+ now.getFullYear(),
6
+ String(now.getMonth() + 1).padStart(2, "0"),
7
+ String(now.getDate()).padStart(2, "0")
8
+ ].join("-")} ${[
9
+ String(now.getHours()).padStart(2, "0"),
10
+ String(now.getMinutes()).padStart(2, "0")
11
+ ].join(":")}`;
10
12
  }
11
13
  export function parseGeneratedDate(value) {
12
14
  if (!generatedDateRegex.test(value.trim()))
@@ -0,0 +1,2 @@
1
+ export declare function warnDeprecated(oldName: string, newName: string, context?: string): void;
2
+ export declare function resolveOption<T, O extends Record<string, unknown> = Record<string, unknown>>(options: O, newKey: keyof O, deprecatedKey: keyof O, context: string, parse?: (value: unknown) => T | undefined): T | undefined;
@@ -0,0 +1,14 @@
1
+ export function warnDeprecated(oldName, newName, context) {
2
+ const message = context ?
3
+ `fln: "${oldName}" is deprecated, use "${newName}" instead (${context})` :
4
+ `fln: "${oldName}" is deprecated, use "${newName}" instead`;
5
+ console.warn(message);
6
+ }
7
+ export function resolveOption(options, newKey, deprecatedKey, context, parse) {
8
+ const newVal = options[newKey];
9
+ const deprecatedVal = options[deprecatedKey];
10
+ if (deprecatedVal !== undefined && newVal === undefined)
11
+ warnDeprecated(String(deprecatedKey), String(newKey), context);
12
+ const raw = newVal ?? deprecatedVal;
13
+ return raw === undefined ? undefined : (parse ? parse(raw) : raw);
14
+ }
@@ -0,0 +1,2 @@
1
+ export declare function getChangedFilesSince(ref: string, cwd: string): string[];
2
+ export declare function filterPathsUnderBase(gitPaths: string[], cwd: string, inputBase: string): string[];
@@ -0,0 +1,33 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { join, relative } from "node:path";
3
+ import { toPosixPath } from "../path/index.js";
4
+ export function getChangedFilesSince(ref, cwd) {
5
+ const result = spawnSync("git", ["diff", "--name-only", ref], {
6
+ cwd,
7
+ encoding: "utf8",
8
+ stdio: ["pipe", "pipe", "pipe"]
9
+ });
10
+ if (result.error)
11
+ throw new Error(`fln: git not found (${result.error.message}). Install git and ensure it is in PATH.`);
12
+ if (result.status !== 0) {
13
+ const stderr = (result.stderr ?? "").trim();
14
+ throw new Error(stderr ?
15
+ `fln: git diff failed: ${stderr}` :
16
+ `fln: git diff failed (exit ${result.status}). Not a git repository or invalid ref: ${ref}`);
17
+ }
18
+ const output = (result.stdout ?? "").trim();
19
+ if (!output)
20
+ return [];
21
+ return output.split("\n").filter(Boolean);
22
+ }
23
+ export function filterPathsUnderBase(gitPaths, cwd, inputBase) {
24
+ return gitPaths
25
+ .map(gitPath => {
26
+ const absolutePath = join(cwd, gitPath);
27
+ const relativeToInput = relative(inputBase, absolutePath);
28
+ if (relativeToInput.startsWith("..") || relativeToInput === "")
29
+ return null;
30
+ return toPosixPath(relativeToInput);
31
+ })
32
+ .filter((path) => path !== null && path !== "");
33
+ }
@@ -1,5 +1,7 @@
1
1
  export * from "./countTokens.js";
2
2
  export * from "./datetime.js";
3
+ export * from "./deprecate.js";
4
+ export * from "./gitDiff.js";
3
5
  export * from "./logger.js";
4
6
  export * from "./outputWriter.js";
5
7
  export * from "./terminal.js";
@@ -1,5 +1,7 @@
1
1
  export * from "./countTokens.js";
2
2
  export * from "./datetime.js";
3
+ export * from "./deprecate.js";
4
+ export * from "./gitDiff.js";
3
5
  export * from "./logger.js";
4
6
  export * from "./outputWriter.js";
5
7
  export * from "./terminal.js";
@@ -1,6 +1,6 @@
1
1
  import type { LogLevel } from "../core/index.js";
2
2
  type LoggerOptions = {
3
- useAnsi: boolean;
3
+ ansi: boolean;
4
4
  logLevel: LogLevel;
5
5
  };
6
6
  export type Logger = {
@@ -1,14 +1,11 @@
1
- import { ansi, getTerminalInfo, renderBox, symbols } from "./terminal.js";
1
+ import pc from "picocolors";
2
+ import { getTerminalInfo, renderBox, symbols } from "./terminal.js";
2
3
  export function createLogger(options) {
3
- const { useAnsi, logLevel } = options;
4
+ const { ansi: useAnsi, logLevel } = options;
4
5
  const { width } = getTerminalInfo();
5
6
  const isSilent = logLevel === "silent";
6
7
  const isVerbose = logLevel === "verbose" || logLevel === "debug";
7
- const formatMessage = (symbol, color, message) => {
8
- if (!useAnsi)
9
- return `${symbol} ${message}`;
10
- return ` ${color}${symbol}${ansi.reset} ${message}`;
11
- };
8
+ const formatMessage = (symbolOrColored, message) => `${useAnsi ? " " : ""}${symbolOrColored} ${message}`;
12
9
  const writeInfo = (formatted) => {
13
10
  if (!isSilent)
14
11
  console.info(formatted);
@@ -17,24 +14,24 @@ export function createLogger(options) {
17
14
  info: (message) => {
18
15
  if (!isSilent)
19
16
  if (useAnsi)
20
- console.info(` ${ansi.dim}${message}${ansi.reset}`);
17
+ console.info(` ${pc.dim(message)}`);
21
18
  else
22
19
  console.info(message);
23
20
  },
24
21
  success: (message) => {
25
- writeInfo(formatMessage(symbols.check, ansi.green, message));
22
+ writeInfo(formatMessage(useAnsi ? pc.green(symbols.check) : symbols.check, message));
26
23
  },
27
24
  warn: (message) => {
28
25
  if (!isSilent)
29
- console.warn(formatMessage(symbols.warning, ansi.yellow, message));
26
+ console.warn(formatMessage(useAnsi ? pc.yellow(symbols.warning) : symbols.warning, message));
30
27
  },
31
28
  error: (message) => {
32
- console.error(formatMessage(symbols.cross, ansi.red, message));
29
+ console.error(formatMessage(useAnsi ? pc.red(symbols.cross) : symbols.cross, message));
33
30
  },
34
31
  debug: (message) => {
35
32
  if (!isSilent && isVerbose)
36
33
  if (useAnsi)
37
- console.info(` ${ansi.dim}${symbols.info} ${message}${ansi.reset}`);
34
+ console.info(` ${pc.dim(`${symbols.info} ${message}`)}`);
38
35
  else
39
36
  console.info(`${symbols.info} ${message}`);
40
37
  },
@@ -45,9 +42,9 @@ export function createLogger(options) {
45
42
  const boxWidth = Math.min(width - 4, 60);
46
43
  const paddedText = text.padEnd(boxWidth - 4);
47
44
  console.info("");
48
- console.info(`${ansi.dim}${symbols.boxTopLeft}${symbols.boxHorizontal.repeat(boxWidth - 2)}${symbols.boxTopRight}${ansi.reset}`);
49
- console.info(`${ansi.dim}${symbols.boxVertical}${ansi.reset}${ansi.bold}${paddedText}${ansi.reset} ${ansi.dim}${symbols.boxVertical}${ansi.reset}`);
50
- console.info(`${ansi.dim}${symbols.boxBottomLeft}${symbols.boxHorizontal.repeat(boxWidth - 2)}${symbols.boxBottomRight}${ansi.reset}`);
45
+ console.info(pc.dim(`${symbols.boxTopLeft}${symbols.boxHorizontal.repeat(boxWidth - 2)}${symbols.boxTopRight}`));
46
+ console.info(`${pc.dim(symbols.boxVertical)}${pc.bold(paddedText)} ${pc.dim(symbols.boxVertical)}`);
47
+ console.info(pc.dim(`${symbols.boxBottomLeft}${symbols.boxHorizontal.repeat(boxWidth - 2)}${symbols.boxBottomRight}`));
51
48
  console.info("");
52
49
  }
53
50
  else {
@@ -61,7 +58,7 @@ export function createLogger(options) {
61
58
  return;
62
59
  if (useAnsi) {
63
60
  console.info("");
64
- console.info(`${ansi.bold}${title}${ansi.reset}`);
61
+ console.info(pc.bold(title));
65
62
  console.info("");
66
63
  }
67
64
  else {
@@ -72,7 +69,7 @@ export function createLogger(options) {
72
69
  for (const [key, value] of Object.entries(items)) {
73
70
  const paddedKey = key.padEnd(maxKeyLength);
74
71
  if (useAnsi)
75
- console.info(` ${ansi.dim}${paddedKey}${ansi.reset} ${value}`);
72
+ console.info(` ${pc.dim(paddedKey)} ${value}`);
76
73
  else
77
74
  console.info(` ${paddedKey} ${value}`);
78
75
  }
@@ -85,7 +82,7 @@ export function createLogger(options) {
85
82
  title,
86
83
  content,
87
84
  width: boxWidth,
88
- useAnsi,
85
+ ansi: useAnsi,
89
86
  showDivider
90
87
  });
91
88
  console.info("");
@@ -10,5 +10,5 @@ type OutputWriter = {
10
10
  tokenCount: number;
11
11
  }>;
12
12
  };
13
- export declare function createOutputWriter(outputFile: string, maxSizeBytes?: number): Promise<OutputWriter>;
13
+ export declare function createOutputWriter(output: string, maxSizeBytes?: number): Promise<OutputWriter>;
14
14
  export {};
@@ -3,11 +3,29 @@ import { createWriteStream } from "node:fs";
3
3
  import { mkdir } from "node:fs/promises";
4
4
  import { dirname } from "node:path";
5
5
  import { countTokens } from "./countTokens.js";
6
- export async function createOutputWriter(outputFile, maxSizeBytes = 0) {
7
- const outputDirectory = dirname(outputFile);
6
+ export async function createOutputWriter(output, maxSizeBytes = 0) {
7
+ if (output === "-") {
8
+ let bytesWritten = 0;
9
+ let totalTokenCount = 0;
10
+ const write = async (text) => {
11
+ const textBytes = Buffer.byteLength(text);
12
+ if (maxSizeBytes > 0 && bytesWritten + textBytes > maxSizeBytes)
13
+ throw new Error(`Output size would exceed maximum of ${maxSizeBytes} bytes`);
14
+ bytesWritten += textBytes;
15
+ totalTokenCount += countTokens(text);
16
+ process.stdout.write(text);
17
+ };
18
+ return {
19
+ write,
20
+ writeLine: (text) => write(`${text}\n`),
21
+ getStats: () => ({ sizeBytes: bytesWritten, tokenCount: totalTokenCount }),
22
+ close: async () => ({ sizeBytes: bytesWritten, tokenCount: totalTokenCount })
23
+ };
24
+ }
25
+ const outputDirectory = dirname(output);
8
26
  if (outputDirectory !== ".")
9
27
  await mkdir(outputDirectory, { recursive: true });
10
- const stream = createWriteStream(outputFile, { encoding: "utf8" });
28
+ const stream = createWriteStream(output, { encoding: "utf8" });
11
29
  let bytesWritten = 0;
12
30
  let totalTokenCount = 0;
13
31
  const write = async (text) => {
@@ -5,7 +5,7 @@ type TerminalInfo = {
5
5
  export declare function getTerminalInfo(): TerminalInfo;
6
6
  export declare function isTTY(): boolean;
7
7
  export declare function shouldUseColors(): boolean;
8
- export declare const ansi: {
8
+ export declare const cursor: {
9
9
  cursorHide: string;
10
10
  cursorShow: string;
11
11
  cursorUp: (lines: number) => string;
@@ -13,34 +13,6 @@ export declare const ansi: {
13
13
  cursorTo: (column: number) => string;
14
14
  clearLine: string;
15
15
  clearLineRight: string;
16
- reset: string;
17
- bold: string;
18
- dim: string;
19
- black: string;
20
- red: string;
21
- green: string;
22
- yellow: string;
23
- blue: string;
24
- magenta: string;
25
- cyan: string;
26
- white: string;
27
- gray: string;
28
- bgBlack: string;
29
- bgRed: string;
30
- bgGreen: string;
31
- bgYellow: string;
32
- bgBlue: string;
33
- bgMagenta: string;
34
- bgCyan: string;
35
- bgWhite: string;
36
- brightBlack: string;
37
- brightRed: string;
38
- brightGreen: string;
39
- brightYellow: string;
40
- brightBlue: string;
41
- brightMagenta: string;
42
- brightCyan: string;
43
- brightWhite: string;
44
16
  };
45
17
  export declare const symbols: {
46
18
  dot: string;
@@ -67,7 +39,7 @@ type ProgressBarOptions = {
67
39
  total: number;
68
40
  current: number;
69
41
  width: number;
70
- useAnsi: boolean;
42
+ ansi: boolean;
71
43
  label?: string;
72
44
  suffix?: string;
73
45
  };
@@ -76,7 +48,7 @@ type BoxOptions = {
76
48
  title?: string;
77
49
  content: string[];
78
50
  width?: number;
79
- useAnsi: boolean;
51
+ ansi: boolean;
80
52
  showDivider?: boolean;
81
53
  };
82
54
  export declare function renderBox(options: BoxOptions): string;
@@ -86,5 +58,5 @@ export type ProgressRenderer = {
86
58
  finish: (message?: string) => void;
87
59
  cleanup: () => void;
88
60
  };
89
- export declare function createProgressRenderer(label: string, useAnsi: boolean, isQuiet: boolean): ProgressRenderer;
61
+ export declare function createProgressRenderer(label: string, ansi: boolean, isQuiet: boolean): ProgressRenderer;
90
62
  export {};