fln 1.1.2 → 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 (84) hide show
  1. package/README.md +335 -179
  2. package/dist/api/fln.d.ts +1 -1
  3. package/dist/api/fln.js +69 -32
  4. package/dist/api/index.d.ts +2 -2
  5. package/dist/api/index.js +2 -2
  6. package/dist/api/types.d.ts +12 -1
  7. package/dist/cli/commandLine.js +73 -23
  8. package/dist/cli/help.d.ts +1 -1
  9. package/dist/cli/help.js +21 -9
  10. package/dist/cli/index.js +1 -1
  11. package/dist/cli/output/components/breakdown.js +2 -2
  12. package/dist/cli/output/components/errors.js +1 -1
  13. package/dist/cli/output/components/index.d.ts +4 -5
  14. package/dist/cli/output/components/index.js +4 -5
  15. package/dist/cli/output/components/summary.js +2 -2
  16. package/dist/cli/output/components/warnings.js +1 -1
  17. package/dist/cli/output/index.d.ts +4 -4
  18. package/dist/cli/output/index.js +4 -4
  19. package/dist/cli/output/renderer.d.ts +3 -5
  20. package/dist/cli/output/renderer.js +4 -7
  21. package/dist/cli/output/styles.d.ts +7 -7
  22. package/dist/config/defaults.d.ts +1 -1
  23. package/dist/config/defaults.js +1 -1
  24. package/dist/config/index.d.ts +6 -5
  25. package/dist/config/index.js +6 -5
  26. package/dist/config/initTemplate.d.ts +1 -0
  27. package/dist/config/initTemplate.js +32 -0
  28. package/dist/config/loader.d.ts +7 -2
  29. package/dist/config/loader.js +10 -4
  30. package/dist/config/resolver.d.ts +6 -3
  31. package/dist/config/resolver.js +32 -22
  32. package/dist/config/types.d.ts +19 -8
  33. package/dist/config/utils.d.ts +5 -2
  34. package/dist/config/utils.js +54 -48
  35. package/dist/core/ignoreMatcher.d.ts +4 -3
  36. package/dist/core/ignoreMatcher.js +32 -34
  37. package/dist/core/index.d.ts +7 -7
  38. package/dist/core/index.js +7 -7
  39. package/dist/core/renderOutput.d.ts +2 -2
  40. package/dist/core/renderOutput.js +50 -49
  41. package/dist/core/renderTree.d.ts +1 -1
  42. package/dist/core/scanTree.d.ts +2 -2
  43. package/dist/core/scanTree.js +48 -71
  44. package/dist/core/size.js +6 -20
  45. package/dist/core/statsCollector.d.ts +1 -1
  46. package/dist/core/types.d.ts +6 -6
  47. package/dist/index.d.ts +1 -3
  48. package/dist/index.js +1 -3
  49. package/dist/infra/datetime.js +8 -6
  50. package/dist/infra/deprecate.d.ts +2 -0
  51. package/dist/infra/deprecate.js +14 -0
  52. package/dist/infra/gitDiff.d.ts +2 -0
  53. package/dist/infra/gitDiff.js +33 -0
  54. package/dist/infra/index.d.ts +8 -6
  55. package/dist/infra/index.js +8 -6
  56. package/dist/infra/logger.d.ts +2 -2
  57. package/dist/infra/logger.js +15 -18
  58. package/dist/infra/outputWriter.d.ts +1 -1
  59. package/dist/infra/outputWriter.js +22 -4
  60. package/dist/infra/terminal.d.ts +4 -32
  61. package/dist/infra/terminal.js +26 -54
  62. package/dist/path/canonical.d.ts +1 -0
  63. package/dist/path/canonical.js +13 -0
  64. package/dist/path/ignoreSafe.d.ts +1 -0
  65. package/dist/path/ignoreSafe.js +15 -0
  66. package/dist/path/index.d.ts +6 -0
  67. package/dist/path/index.js +6 -0
  68. package/dist/path/normalize.d.ts +2 -0
  69. package/dist/path/normalize.js +9 -0
  70. package/dist/path/output.d.ts +4 -0
  71. package/dist/path/output.js +12 -0
  72. package/dist/path/posix.d.ts +1 -0
  73. package/dist/path/posix.js +4 -0
  74. package/dist/path/resolve.d.ts +1 -0
  75. package/dist/path/resolve.js +6 -0
  76. package/dist/pattern/index.d.ts +1 -0
  77. package/dist/pattern/index.js +1 -0
  78. package/dist/pattern/normalize.d.ts +2 -0
  79. package/dist/pattern/normalize.js +43 -0
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/package.json +7 -3
  83. package/dist/cli/output/components/progressBar.d.ts +0 -6
  84. package/dist/cli/output/components/progressBar.js +0 -38
@@ -1,10 +1,10 @@
1
1
  import { createReadStream } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { extname, join } from "node:path";
4
- import { createOutputWriter, formatDateTime } from "../infra";
5
- import { VERSION } from "../version";
6
- import { renderTree } from "./renderTree";
7
- import { formatByteSize } from "./size";
4
+ import { createOutputWriter, formatDateTime } from "../infra/index.js";
5
+ import { VERSION } from "../version.js";
6
+ import { renderTree } from "./renderTree.js";
7
+ import { formatByteSize } from "./size.js";
8
8
  function getLanguageFromFilename(fileName) {
9
9
  const extension = extname(fileName).replace(".", "");
10
10
  return extension === "" ? "txt" : extension;
@@ -48,34 +48,38 @@ async function writeMarkdownContent(writer, filePath) {
48
48
  if (lastCharacter !== "" && lastCharacter !== "\n")
49
49
  await writer.write("\n");
50
50
  }
51
- function* iterateFileNodes(node) {
52
- if (node.type === "file")
53
- yield node;
54
- if (node.children)
55
- for (const child of node.children)
56
- yield* iterateFileNodes(child);
57
- }
58
- function filterSkippedNodes(node) {
51
+ function filterAndCollectFileNodes(node) {
59
52
  if (node.skipReason)
60
- return undefined;
61
- if (!node.children || node.children.length === 0)
62
- return { ...node };
63
- const children = node.children
64
- .map(child => filterSkippedNodes(child))
65
- .filter((child) => child !== undefined);
66
- return { ...node, children };
53
+ return { filtered: undefined, fileNodes: [] };
54
+ if (node.type === "file")
55
+ return { filtered: { ...node }, fileNodes: [node] };
56
+ const allFileNodes = [];
57
+ const filteredChildren = [];
58
+ for (const child of node.children ?? []) {
59
+ const { filtered, fileNodes } = filterAndCollectFileNodes(child);
60
+ allFileNodes.push(...fileNodes);
61
+ if (filtered)
62
+ filteredChildren.push(filtered);
63
+ }
64
+ if (filteredChildren.length === 0)
65
+ return { filtered: undefined, fileNodes: allFileNodes };
66
+ return {
67
+ filtered: { ...node, children: filteredChildren },
68
+ fileNodes: allFileNodes
69
+ };
67
70
  }
68
71
  async function writeMarkdown(result, config) {
69
- const writer = await createOutputWriter(config.outputFile, config.maximumTotalSizeBytes);
70
- const outputRoot = filterSkippedNodes(result.root);
71
- if (!outputRoot)
72
- throw new Error("Root directory was skipped.");
72
+ const writer = await createOutputWriter(config.output, config.maxTotalSize);
73
+ const { filtered: outputRoot, fileNodes } = filterAndCollectFileNodes(result.root);
74
+ const effectiveRoot = outputRoot ?? { ...result.root, children: [] };
73
75
  try {
74
- await writer.writeLine(`<!-- 🥞 fln ${VERSION} -->`);
75
- await writer.writeLine("");
76
+ if (config.output !== "-") {
77
+ await writer.writeLine(`<!-- 🥞 fln ${VERSION} -->`);
78
+ await writer.writeLine("");
79
+ }
76
80
  await writer.writeLine(`# Codebase Snapshot: ${result.projectName}`);
77
81
  await writer.writeLine("");
78
- await writer.writeLine(`Generated: ${config.generatedDate ?? formatDateTime()} `);
82
+ await writer.writeLine(`Generated: ${config.date ?? formatDateTime()} `);
79
83
  await writer.writeLine(`Files: ${result.stats.files} | Directories: ${result.stats.directories}`);
80
84
  await writer.writeLine("");
81
85
  await writer.writeLine("---");
@@ -87,14 +91,14 @@ async function writeMarkdown(result, config) {
87
91
  if (config.includeTree) {
88
92
  await writer.writeLine("## Directory Tree");
89
93
  await writer.writeLine("```text");
90
- await writer.write(renderTree(outputRoot));
94
+ await writer.write(renderTree(effectiveRoot));
91
95
  await writer.writeLine("```");
92
96
  await writer.writeLine("");
93
97
  await writer.writeLine("---");
94
98
  await writer.writeLine("");
95
99
  }
96
- if (config.includeContents)
97
- await writeMarkdownFiles(outputRoot, writer, config);
100
+ if (config.includeContents && fileNodes.length > 0)
101
+ await writeMarkdownFiles(fileNodes, writer, config);
98
102
  if (config.footer) {
99
103
  await writer.writeLine("");
100
104
  await writer.writeLine(config.footer);
@@ -109,15 +113,14 @@ async function writeMarkdown(result, config) {
109
113
  throw error;
110
114
  }
111
115
  }
112
- async function writeMarkdownFiles(rootNode, outputWriter, renderConfig) {
116
+ async function writeMarkdownFiles(fileNodes, outputWriter, renderConfig) {
113
117
  await outputWriter.writeLine("## Source Files");
114
118
  await outputWriter.writeLine("");
115
- const fileNodes = Array.from(iterateFileNodes(rootNode));
116
119
  for (let i = 0; i < fileNodes.length; i++) {
117
120
  const node = fileNodes[i];
118
121
  const language = getLanguageFromFilename(node.name);
119
122
  const isLastFile = i === fileNodes.length - 1;
120
- const filePath = join(renderConfig.rootDirectory, node.path);
123
+ const filePath = join(renderConfig.input, node.path);
121
124
  let fenceLength = 3;
122
125
  if (!node.isBinary)
123
126
  try {
@@ -144,37 +147,37 @@ async function writeMarkdownFiles(rootNode, outputWriter, renderConfig) {
144
147
  }
145
148
  }
146
149
  async function writeJson(result, config) {
147
- const writer = await createOutputWriter(config.outputFile, config.maximumTotalSizeBytes);
148
- const outputRoot = filterSkippedNodes(result.root);
149
- if (!outputRoot)
150
- throw new Error("Root directory was skipped.");
150
+ const writer = await createOutputWriter(config.output, config.maxTotalSize);
151
+ const { filtered: outputRoot, fileNodes } = filterAndCollectFileNodes(result.root);
152
+ const effectiveRoot = outputRoot ?? { ...result.root, children: [] };
151
153
  try {
152
154
  await writer.write("{");
153
155
  await writer.write(`"version":${JSON.stringify(VERSION)}`);
154
- await writer.write(`,"generated":${JSON.stringify(config.generatedDate ?? formatDateTime())}`);
156
+ await writer.write(`,"generated":${JSON.stringify(config.date ?? formatDateTime())}`);
155
157
  await writer.write(`,"projectName":${JSON.stringify(result.projectName)}`);
156
- await writer.write(`,"rootDirectory":${JSON.stringify(config.rootDirectory)}`);
157
- await writer.write(`,"stats":${JSON.stringify(result.stats)}`);
158
+ await writer.write(`,"input":${JSON.stringify(config.input)}`);
159
+ await writer.write(`,"rootDirectory":${JSON.stringify(config.input)}`);
160
+ const { outputSizeBytes: _, outputTokenCount: __, ...statsForJson } = result.stats;
161
+ await writer.write(`,"stats":${JSON.stringify(statsForJson)}`);
158
162
  await writer.write(`,"options":${JSON.stringify({
159
163
  includeTree: config.includeTree,
160
164
  includeContents: config.includeContents,
161
165
  format: config.format,
162
- maximumFileSizeBytes: config.maximumFileSizeBytes,
163
- maximumTotalSizeBytes: config.maximumTotalSizeBytes,
166
+ maxFileSize: config.maxFileSize,
167
+ maxTotalSize: config.maxTotalSize,
164
168
  includeHidden: config.includeHidden,
165
- useGitignore: config.useGitignore,
169
+ gitignore: config.gitignore,
166
170
  excludePatterns: config.excludePatterns,
167
171
  includePatterns: config.includePatterns,
168
172
  followSymlinks: config.followSymlinks,
169
173
  banner: config.banner,
170
174
  footer: config.footer
171
175
  })}`);
172
- await writer.write(`,"tree":${JSON.stringify(outputRoot)}`);
173
- await writer.write(`,"stats":${JSON.stringify(result.stats)}`);
176
+ await writer.write(`,"tree":${JSON.stringify(effectiveRoot)}`);
174
177
  if (config.includeContents) {
175
178
  await writer.write(",\"files\":[");
176
179
  let isFirst = true;
177
- for (const node of iterateFileNodes(outputRoot)) {
180
+ for (const node of fileNodes) {
178
181
  if (!isFirst)
179
182
  await writer.write(",");
180
183
  isFirst = false;
@@ -182,13 +185,11 @@ async function writeJson(result, config) {
182
185
  await writer.write(`"path":${JSON.stringify(node.path)}`);
183
186
  await writer.write(`,"language":${JSON.stringify(getLanguageFromFilename(node.name))}`);
184
187
  await writer.write(`,"isBinary":${JSON.stringify(Boolean(node.isBinary))}`);
185
- if (node.skipReason)
186
- await writer.write(`,"skipReason":${JSON.stringify(node.skipReason)}`);
187
- if (node.isBinary || node.skipReason)
188
+ if (node.isBinary)
188
189
  await writer.write(",\"content\":null");
189
190
  else
190
191
  try {
191
- const filePath = join(config.rootDirectory, node.path);
192
+ const filePath = join(config.input, node.path);
192
193
  const content = await readFile(filePath, "utf8");
193
194
  await writer.write(`,"content":${JSON.stringify(content)}`);
194
195
  }
@@ -1,2 +1,2 @@
1
- import type { FileNode } from "./types";
1
+ import type { FileNode } from "./types.js";
2
2
  export declare function renderTree(root: FileNode): string;
@@ -1,3 +1,3 @@
1
- import type { Logger } from "../infra";
2
- import type { ScanOptions, ScanResult } from "./types";
1
+ import type { Logger } from "../infra/index.js";
2
+ import type { ScanOptions, ScanResult } from "./types.js";
3
3
  export declare function scanTree(options: ScanOptions, logger: Logger): Promise<ScanResult>;
@@ -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 { IgnoreMatcher } from "./ignoreMatcher";
6
- function normalizePathSegment(pathSegment) {
7
- if (sep === "/")
8
- return pathSegment;
9
- return pathSegment.split(sep).join("/");
10
- }
5
+ import pLimit from "p-limit";
6
+ import { toCanonicalRelative, toIgnoreSafePath, toPosixPath } from "../path/index.js";
7
+ import { normalizeIncludePattern } from "../pattern/index.js";
8
+ import { IgnoreMatcher } from "./ignoreMatcher.js";
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)
@@ -1,3 +1,3 @@
1
- import type { FileNode } from "./types";
1
+ import type { FileNode } from "./types.js";
2
2
  export declare function collectExtensionStats(root: FileNode): Map<string, number>;
3
3
  export declare function collectProcessedFiles(root: FileNode): string[];
@@ -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
- export * from "./api";
2
- export * from "./core";
3
- export * from "./version";
1
+ export * from "./api/index.js";
package/dist/index.js CHANGED
@@ -1,3 +1 @@
1
- export * from "./api";
2
- export * from "./core";
3
- export * from "./version";
1
+ export * from "./api/index.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,6 +1,8 @@
1
- export * from "./countTokens";
2
- export * from "./datetime";
3
- export * from "./logger";
4
- export * from "./outputWriter";
5
- export * from "./terminal";
6
- export * from "./usageTracker";
1
+ export * from "./countTokens.js";
2
+ export * from "./datetime.js";
3
+ export * from "./deprecate.js";
4
+ export * from "./gitDiff.js";
5
+ export * from "./logger.js";
6
+ export * from "./outputWriter.js";
7
+ export * from "./terminal.js";
8
+ export * from "./usageTracker.js";
@@ -1,6 +1,8 @@
1
- export * from "./countTokens";
2
- export * from "./datetime";
3
- export * from "./logger";
4
- export * from "./outputWriter";
5
- export * from "./terminal";
6
- export * from "./usageTracker";
1
+ export * from "./countTokens.js";
2
+ export * from "./datetime.js";
3
+ export * from "./deprecate.js";
4
+ export * from "./gitDiff.js";
5
+ export * from "./logger.js";
6
+ export * from "./outputWriter.js";
7
+ export * from "./terminal.js";
8
+ export * from "./usageTracker.js";
@@ -1,6 +1,6 @@
1
- import type { LogLevel } from "../core";
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 = {