@davstack/peek 0.1.1 → 0.1.2

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/cli-spec.js CHANGED
@@ -17,7 +17,7 @@ __export(src_exports, {
17
17
  peekFolder: () => peekFolder,
18
18
  scanFolderPeek: () => scanFolderPeek
19
19
  });
20
- import { execFile } from "child_process";
20
+ import { execFile, spawn } from "child_process";
21
21
  import { mkdir, readdir, readFile, writeFile } from "fs/promises";
22
22
  import path from "path";
23
23
  import { promisify } from "util";
@@ -25,7 +25,15 @@ async function scanFolderPeek(folder, options = {}) {
25
25
  const root = path.resolve(folder);
26
26
  const repoRoot = await findRepoRoot(root);
27
27
  const outputConfig = resolveOutputConfig(options);
28
- const summary = await collectFolderSummary(root, repoRoot, root, outputConfig.deep, outputConfig);
28
+ const fileLimit = createLimiter(64);
29
+ const summary = await collectFolderSummary(
30
+ root,
31
+ repoRoot,
32
+ root,
33
+ outputConfig.deep,
34
+ outputConfig,
35
+ fileLimit
36
+ );
29
37
  return `${renderFolderSummary(summary, outputConfig)}
30
38
  `;
31
39
  }
@@ -47,27 +55,34 @@ async function peekFolder(folder, options = {}) {
47
55
  }
48
56
  return (await generateFolderPeek(root, options)).content;
49
57
  }
50
- async function collectFolderSummary(root, repoRoot, dir, deep, outputConfig) {
58
+ async function collectFolderSummary(root, repoRoot, dir, deep, outputConfig, fileLimit) {
51
59
  const entries = await readdir(dir, { withFileTypes: true });
52
- const files = [];
53
- const folders = [];
60
+ const fileTasks = [];
61
+ const folderTasks = [];
54
62
  const omittedFiles = [];
63
+ const ignoredPaths = await checkIgnoredPaths(root, entries.map((entry) => path.join(dir, entry.name)));
55
64
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
56
65
  const fullPath = path.join(dir, entry.name);
57
- if (await isGitIgnored(root, fullPath)) continue;
66
+ if (ignoredPaths.has(toPosix(path.relative(root, fullPath)))) continue;
58
67
  if (entry.isDirectory()) {
59
68
  if (!deep || shouldSkipDirectory(entry.name)) continue;
60
- folders.push(await collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig));
69
+ folderTasks.push(collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig, fileLimit));
61
70
  continue;
62
71
  }
63
72
  if (!entry.isFile() || shouldSkipFile(entry.name)) continue;
64
- const summary = await summarizeFile(root, repoRoot, fullPath, outputConfig);
65
- if (summary) {
66
- files.push(summary);
67
- } else {
68
- omittedFiles.push(entry.name);
69
- }
73
+ fileTasks.push(
74
+ fileLimit(async () => ({
75
+ name: entry.name,
76
+ summary: await summarizeFile(root, repoRoot, fullPath, outputConfig)
77
+ }))
78
+ );
70
79
  }
80
+ const fileResults = await Promise.all(fileTasks);
81
+ const files = fileResults.map((result) => result.summary).filter((summary) => summary !== null);
82
+ omittedFiles.push(
83
+ ...fileResults.filter((result) => result.summary === null).map((result) => result.name)
84
+ );
85
+ const folders = await Promise.all(folderTasks);
71
86
  return {
72
87
  path: formatFolderPath(root, repoRoot, dir),
73
88
  files: files.sort((a, b) => a.path.localeCompare(b.path)),
@@ -213,6 +228,22 @@ function stripBlockComments(text) {
213
228
  function unique(items) {
214
229
  return Array.from(new Set(items));
215
230
  }
231
+ function createLimiter(maxConcurrent) {
232
+ let active = 0;
233
+ const queue = [];
234
+ return async function limit(task) {
235
+ if (active >= maxConcurrent) {
236
+ await new Promise((resolve) => queue.push(resolve));
237
+ }
238
+ active += 1;
239
+ try {
240
+ return await task();
241
+ } finally {
242
+ active -= 1;
243
+ queue.shift()?.();
244
+ }
245
+ };
246
+ }
216
247
  function lineNumberAt(text, index) {
217
248
  let lineNumber = 1;
218
249
  for (let offset = 0; offset < index; offset += 1) {
@@ -342,17 +373,28 @@ function escapeAttribute(value) {
342
373
  function escapeText(value) {
343
374
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;");
344
375
  }
345
- async function isGitIgnored(root, fullPath) {
346
- const relativePath = toPosix(path.relative(root, fullPath));
347
- if (!relativePath || relativePath.startsWith("..")) return false;
348
- try {
349
- await execFileAsync("git", ["-C", root, "check-ignore", "--quiet", "--", relativePath]);
350
- return true;
351
- } catch (error) {
352
- const code = error.code;
353
- if (code === 1 || code === 128 || code === "ENOENT") return false;
354
- return false;
355
- }
376
+ async function checkIgnoredPaths(root, fullPaths) {
377
+ const relativePaths = fullPaths.map((fullPath) => toPosix(path.relative(root, fullPath))).filter((relativePath) => relativePath && !relativePath.startsWith(".."));
378
+ if (relativePaths.length === 0) return /* @__PURE__ */ new Set();
379
+ return await new Promise((resolve) => {
380
+ const child = spawn("git", ["-C", root, "check-ignore", "--stdin"], {
381
+ stdio: ["pipe", "pipe", "ignore"]
382
+ });
383
+ let stdout = "";
384
+ child.stdout.setEncoding("utf8");
385
+ child.stdout.on("data", (chunk) => {
386
+ stdout += chunk;
387
+ });
388
+ child.on("error", () => resolve(/* @__PURE__ */ new Set()));
389
+ child.on("close", (code) => {
390
+ if (code !== 0 && code !== 1) {
391
+ resolve(/* @__PURE__ */ new Set());
392
+ return;
393
+ }
394
+ resolve(new Set(stdout.split(/\r?\n/).filter(Boolean)));
395
+ });
396
+ child.stdin.end(relativePaths.join("\n"));
397
+ });
356
398
  }
357
399
  async function findRepoRoot(root) {
358
400
  try {
@@ -412,6 +454,7 @@ var outputFlags = {
412
454
  },
413
455
  file_paths: {
414
456
  type: "string",
457
+ values: ["concise", "full"],
415
458
  description: "File path style: concise or full"
416
459
  },
417
460
  "include-lines-count": {
@@ -439,6 +482,16 @@ function resolveCliScanOptions(flags) {
439
482
  var cliSpec = {
440
483
  name: "peek",
441
484
  description: "Print concise folder summaries for agents.",
485
+ examples: [
486
+ "peek .",
487
+ "peek packages/context-compactor --agent",
488
+ "peek . --human",
489
+ "peek . --file_paths=full --no-include-lines-count"
490
+ ],
491
+ defaults: [
492
+ "--human behavior: deep scan, indented output, full file paths, line counts",
493
+ "--agent behavior: deep scan, no indentation, concise file paths, line counts"
494
+ ],
442
495
  positionals: [{ name: "path", required: true, description: "Folder to peek at" }],
443
496
  flags: {
444
497
  deep: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/cli-spec.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nexport const GENERATED_PEEK_FILE = '.folder-peek.generated.md';\nconst execFileAsync = promisify(execFile);\n\nexport type ScanOptions = {\n deep?: boolean;\n preset?: PeekOutputPresetName;\n indent?: boolean;\n filePaths?: PeekFilePathMode;\n file_paths?: PeekFilePathMode;\n includeLinesCount?: boolean;\n include_lines_count?: boolean;\n};\n\ntype FileSummary = {\n path: string;\n kind: 'markdown' | 'typescript' | 'python';\n lines: number;\n items: string[];\n};\n\nexport type PeekFilePathMode = 'concise' | 'full';\nexport type PeekOutputPresetName = 'agent' | 'human';\n\nexport type PeekOutputConfig = {\n deep: boolean;\n indent: boolean;\n filePaths: PeekFilePathMode;\n includeLinesCount: boolean;\n};\n\nexport const PEEK_OUTPUT_PRESETS: Record<PeekOutputPresetName, PeekOutputConfig> = {\n human: {\n deep: true,\n indent: true,\n filePaths: 'full',\n includeLinesCount: true,\n },\n agent: {\n deep: true,\n indent: false,\n filePaths: 'concise',\n includeLinesCount: true,\n },\n};\n\ntype FolderSummary = {\n path: string;\n files: FileSummary[];\n folders: FolderSummary[];\n omittedFiles: string[];\n};\n\nconst SKIP_DIRS = new Set([\n '.git',\n '.next',\n 'coverage',\n 'dist',\n 'dist-ssr',\n 'node_modules',\n]);\n\nconst SKIP_FILES = new Set([GENERATED_PEEK_FILE]);\n\nexport async function scanFolderPeek(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const repoRoot = await findRepoRoot(root);\n const outputConfig = resolveOutputConfig(options);\n const summary = await collectFolderSummary(root, repoRoot, root, outputConfig.deep, outputConfig);\n\n return `${renderFolderSummary(summary, outputConfig)}\\n`;\n}\n\nexport async function generateFolderPeek(\n folder: string,\n options: ScanOptions = {},\n): Promise<{ path: string; content: string }> {\n const root = path.resolve(folder);\n const content = await scanFolderPeek(root, options);\n const outPath = path.join(root, GENERATED_PEEK_FILE);\n await mkdir(root, { recursive: true });\n await writeFile(outPath, content, 'utf8');\n return { path: outPath, content };\n}\n\nexport async function peekFolder(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const generatedPath = path.join(root, GENERATED_PEEK_FILE);\n try {\n if (!options.deep && !hasOutputOverrides(options)) return await readFile(generatedPath, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n return (await generateFolderPeek(root, options)).content;\n}\n\nasync function collectFolderSummary(\n root: string,\n repoRoot: string,\n dir: string,\n deep: boolean,\n outputConfig: PeekOutputConfig,\n): Promise<FolderSummary> {\n const entries = await readdir(dir, { withFileTypes: true });\n const files: FileSummary[] = [];\n const folders: FolderSummary[] = [];\n const omittedFiles: string[] = [];\n\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const fullPath = path.join(dir, entry.name);\n if (await isGitIgnored(root, fullPath)) continue;\n\n if (entry.isDirectory()) {\n if (!deep || shouldSkipDirectory(entry.name)) continue;\n folders.push(await collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig));\n continue;\n }\n if (!entry.isFile() || shouldSkipFile(entry.name)) continue;\n\n const summary = await summarizeFile(root, repoRoot, fullPath, outputConfig);\n if (summary) {\n files.push(summary);\n } else {\n omittedFiles.push(entry.name);\n }\n }\n\n return {\n path: formatFolderPath(root, repoRoot, dir),\n files: files.sort((a, b) => a.path.localeCompare(b.path)),\n folders: folders.sort((a, b) => a.path.localeCompare(b.path)),\n omittedFiles: omittedFiles.sort((a, b) => a.localeCompare(b)),\n };\n}\n\nfunction formatFolderPath(root: string, repoRoot: string, dir: string): string {\n const base = isInsidePath(repoRoot, dir) ? repoRoot : root;\n const relativePath = toPosix(path.relative(base, dir));\n return relativePath || '.';\n}\n\nfunction renderFolderSummary(\n summary: FolderSummary,\n outputConfig: PeekOutputConfig,\n depth = 0,\n): string {\n const indent = outputConfig.indent ? '\\t'.repeat(Math.max(0, depth - 1)) : '';\n const childIndent = outputConfig.indent ? '\\t'.repeat(depth) : '';\n const lines = [`${indent}<folder path=\"${escapeAttribute(summary.path)}\">`];\n\n for (const file of summary.files) {\n const lineCount = outputConfig.includeLinesCount ? ` lines=\"${file.lines}\"` : '';\n lines.push(`${childIndent}<file path=\"${escapeAttribute(file.path)}\"${lineCount}>`);\n for (const item of file.items) {\n const prefix = item.startsWith('[ln ') ? '' : '- ';\n lines.push(`${childIndent}${prefix}${item}`);\n }\n lines.push(`${childIndent}</file>`);\n }\n\n for (const folder of summary.folders) {\n lines.push(renderFolderSummary(folder, outputConfig, depth + 1));\n }\n\n if (summary.omittedFiles.length > 0) {\n lines.push(`${childIndent}<omitted_files>`);\n for (const omittedFile of summary.omittedFiles) {\n lines.push(`${childIndent}- ${escapeText(omittedFile)}`);\n }\n lines.push(`${childIndent}</omitted_files>`);\n }\n\n lines.push(`${indent}</folder>`);\n return lines.join('\\n');\n}\n\nasync function summarizeFile(\n root: string,\n repoRoot: string,\n fullPath: string,\n outputConfig: PeekOutputConfig,\n): Promise<FileSummary | null> {\n const extension = path.extname(fullPath);\n const relativePath = formatFilePath(root, repoRoot, fullPath, outputConfig.filePaths);\n\n if (extension === '.md' || extension === '.mdx') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'markdown',\n lines: countLines(text),\n items: extractMarkdownHeadings(text),\n };\n }\n if (isTypeScriptLikeFile(extension, fullPath)) {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'typescript',\n lines: countLines(text),\n items: extractTypeScriptSymbols(text),\n };\n }\n if (extension === '.py') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'python',\n lines: countLines(text),\n items: extractPythonSymbols(text),\n };\n }\n return null;\n}\n\nfunction isTypeScriptLikeFile(extension: string, fullPath: string): boolean {\n return ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs'].includes(extension) && !fullPath.endsWith('.d.ts');\n}\n\nfunction extractMarkdownHeadings(text: string): string[] {\n return text\n .split(/\\r?\\n/)\n .map((line) => /^(#{1,6})\\s+(.+?)\\s*$/.exec(line))\n .filter((match): match is RegExpExecArray => match !== null)\n .map((match) => `h${match[1].length} ${match[2].replace(/\\s+#+$/, '').trim()}`);\n}\n\nfunction extractTypeScriptSymbols(text: string): string[] {\n const items: string[] = [];\n const source = stripBlockComments(text);\n const patterns: Array<[RegExp, string]> = [\n [/^(?:export\\s+)?(?:abstract\\s+)?class\\s+([A-Za-z_$][\\w$]*)/gm, 'class'],\n [/^(?:export\\s+)?interface\\s+([A-Za-z_$][\\w$]*)/gm, 'interface'],\n [/^(?:export\\s+)?type\\s+([A-Za-z_$][\\w$]*)\\s*=/gm, 'type'],\n [/^(?:export\\s+)?(?:declare\\s+)?(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)/gm, 'const'],\n [\n /^(?:export\\s+)?(?:async\\s+)?function\\s+([A-Za-z_$][\\w$]*)\\s*\\(/gm,\n 'function',\n ],\n ];\n\n for (const [pattern, label] of patterns) {\n for (const match of source.matchAll(pattern)) {\n const range = lineRangeForTypeScriptSymbol(source, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${label} ${match[1]}`);\n }\n }\n items.push(...extractTypeScriptTestCalls(source));\n return unique(items);\n}\n\nfunction extractTypeScriptTestCalls(text: string): string[] {\n const items: string[] = [];\n const pattern = /^\\s*(describe|test|it)\\s*\\(\\s*(['\"`])((?:\\\\.|(?!\\2)[\\s\\S])*?)\\2/gm;\n\n for (const match of text.matchAll(pattern)) {\n const callName = match[1];\n const quote = match[2];\n const title = match[3];\n const range = lineRangeForCallExpression(text, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${callName}(${quote}${title}${quote})`);\n }\n\n return items;\n}\n\nfunction extractPythonSymbols(text: string): string[] {\n const items: string[] = [];\n const lines = text.split(/\\r?\\n/);\n for (const [index, line] of lines.entries()) {\n const lineNumber = index + 1;\n let match = /^class\\s+([A-Za-z_]\\w*)\\s*[:(]/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} class ${match[1]}`);\n continue;\n }\n match = /^(?:async\\s+)?def\\s+([A-Za-z_]\\w*)\\s*\\(/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} function ${match[1]}`);\n continue;\n }\n match = /^([A-Z][A-Z0-9_]*)\\s*[:=]/.exec(line);\n if (match) items.push(`${formatLineRange({ start: lineNumber, end: lineNumber })} const ${match[1]}`);\n }\n return unique(items);\n}\n\nfunction shouldSkipDirectory(name: string): boolean {\n return SKIP_DIRS.has(name);\n}\n\nfunction shouldSkipFile(name: string): boolean {\n return SKIP_FILES.has(name) || name.endsWith('.map');\n}\n\nfunction stripBlockComments(text: string): string {\n return text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, (comment) =>\n comment.replace(/[^\\r\\n]/g, ' '),\n );\n}\n\nfunction unique(items: string[]): string[] {\n return Array.from(new Set(items));\n}\n\nfunction lineNumberAt(text: string, index: number): number {\n let lineNumber = 1;\n for (let offset = 0; offset < index; offset += 1) {\n if (text.charCodeAt(offset) === 10) lineNumber += 1;\n }\n return lineNumber;\n}\n\nfunction lineRangeForTypeScriptSymbol(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const lineEnd = text.indexOf('\\n', index);\n const declarationEnd = lineEnd === -1 ? text.length : lineEnd;\n const openingBrace = text.indexOf('{', index);\n\n if (openingBrace !== -1 && openingBrace < declarationEnd) {\n const closingBrace = findMatchingBrace(text, openingBrace);\n if (closingBrace !== -1) return { start, end: lineNumberAt(text, closingBrace) };\n }\n\n const semicolon = text.indexOf(';', index);\n if (semicolon !== -1 && semicolon < declarationEnd) {\n return { start, end: lineNumberAt(text, semicolon) };\n }\n\n return { start, end: start };\n}\n\nfunction findMatchingBrace(text: string, openingBrace: number): number {\n let depth = 0;\n for (let index = openingBrace; index < text.length; index += 1) {\n const char = text[index];\n if (char === '{') depth += 1;\n if (char === '}') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForCallExpression(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const openingParen = text.indexOf('(', index);\n if (openingParen === -1) return { start, end: start };\n\n const closingParen = findMatchingParen(text, openingParen);\n if (closingParen === -1) return { start, end: start };\n return { start, end: lineNumberAt(text, closingParen) };\n}\n\nfunction findMatchingParen(text: string, openingParen: number): number {\n let depth = 0;\n let quote: string | null = null;\n\n for (let index = openingParen; index < text.length; index += 1) {\n const char = text[index];\n if (quote) {\n if (char === '\\\\') {\n index += 1;\n continue;\n }\n if (char === quote) quote = null;\n continue;\n }\n if (char === '\"' || char === \"'\" || char === '`') {\n quote = char;\n continue;\n }\n if (char === '(') depth += 1;\n if (char === ')') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForPythonBlock(\n lines: string[],\n startIndex: number,\n): { start: number; end: number } {\n const startIndent = indentationLength(lines[startIndex] ?? '');\n let endIndex = startIndex;\n\n for (let index = startIndex + 1; index < lines.length; index += 1) {\n const line = lines[index] ?? '';\n if (line.trim() === '') {\n endIndex = index;\n continue;\n }\n if (indentationLength(line) <= startIndent) break;\n endIndex = index;\n }\n\n while (endIndex > startIndex && (lines[endIndex] ?? '').trim() === '') endIndex -= 1;\n return { start: startIndex + 1, end: endIndex + 1 };\n}\n\nfunction indentationLength(line: string): number {\n return line.match(/^\\s*/)?.[0].length ?? 0;\n}\n\nfunction formatLineRange(range: { start: number; end: number }): string {\n return range.start === range.end ? `[ln ${range.start}]` : `[ln ${range.start}-${range.end}]`;\n}\n\nfunction formatFilePath(\n root: string,\n repoRoot: string,\n fullPath: string,\n filePaths: PeekFilePathMode,\n): string {\n if (filePaths === 'concise') return `/${path.basename(fullPath)}`;\n const base = isInsidePath(repoRoot, fullPath) ? repoRoot : root;\n return toPosix(path.relative(base, fullPath));\n}\n\nfunction resolveOutputConfig(options: ScanOptions): PeekOutputConfig {\n const presetName = options.preset ?? 'human';\n const preset = PEEK_OUTPUT_PRESETS[presetName];\n if (!preset) throw new Error(`Unknown peek output preset: ${presetName}`);\n\n return {\n deep: options.deep ?? preset.deep,\n indent: options.indent ?? preset.indent,\n filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths),\n includeLinesCount:\n options.includeLinesCount ?? options.include_lines_count ?? preset.includeLinesCount,\n };\n}\n\nfunction normalizeFilePathMode(value: PeekFilePathMode): PeekFilePathMode {\n if (value === 'concise' || value === 'full') return value;\n throw new Error(`Unknown peek file path mode: ${String(value)}`);\n}\n\nfunction countLines(text: string): number {\n if (text.length === 0) return 0;\n const newlineCount = text.match(/\\r\\n|\\r|\\n/g)?.length ?? 0;\n return newlineCount + (/(?:\\r\\n|\\r|\\n)$/.test(text) ? 0 : 1);\n}\n\nfunction hasOutputOverrides(options: ScanOptions): boolean {\n return (\n options.preset !== undefined ||\n options.indent !== undefined ||\n options.filePaths !== undefined ||\n options.file_paths !== undefined ||\n options.includeLinesCount !== undefined ||\n options.include_lines_count !== undefined\n );\n}\n\nfunction isInsidePath(parent: string, child: string): boolean {\n const relativePath = path.relative(parent, child);\n return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join('/');\n}\n\nfunction escapeAttribute(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;');\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/</g, '&lt;');\n}\n\nasync function isGitIgnored(root: string, fullPath: string): Promise<boolean> {\n const relativePath = toPosix(path.relative(root, fullPath));\n if (!relativePath || relativePath.startsWith('..')) return false;\n\n try {\n await execFileAsync('git', ['-C', root, 'check-ignore', '--quiet', '--', relativePath]);\n return true;\n } catch (error) {\n const code = (error as { code?: number | string }).code;\n if (code === 1 || code === 128 || code === 'ENOENT') return false;\n return false;\n }\n}\n\nasync function findRepoRoot(root: string): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', ['-C', root, 'rev-parse', '--show-toplevel']);\n return path.resolve(stdout.trim());\n } catch {\n return root;\n }\n}\n","import type { CliSpec } from '@davstack/cli-utils';\nimport type { PeekFilePathMode, PeekOutputPresetName, ScanOptions } from './index.js';\n\nconst outputFlags = {\n human: {\n type: 'boolean',\n default: false,\n description: 'Use human-readable output defaults: indented XML and full repo-relative file paths',\n },\n agent: {\n type: 'boolean',\n default: false,\n description: 'Use token-optimized output defaults: no indentation and concise file paths',\n },\n indent: {\n type: 'boolean',\n description: 'Indent nested folder output',\n },\n file_paths: {\n type: 'string',\n description: 'File path style: concise or full',\n },\n 'include-lines-count': {\n type: 'boolean',\n description: 'Include total line counts on file tags',\n },\n} as const;\n\nfunction resolveCliScanOptions(flags: Record<string, unknown>): ScanOptions {\n const human = flags.human === true;\n const agent = flags.agent === true;\n if (human && agent) throw new Error('Use only one output preset: --human or --agent');\n\n const preset: PeekOutputPresetName | undefined = agent ? 'agent' : human ? 'human' : undefined;\n const filePaths = flags.file_paths;\n if (filePaths !== undefined && filePaths !== 'concise' && filePaths !== 'full') {\n throw new Error('--file_paths must be \"concise\" or \"full\"');\n }\n\n return {\n deep: flags.deep as boolean,\n preset,\n indent: flags.indent as boolean | undefined,\n filePaths: filePaths as PeekFilePathMode | undefined,\n includeLinesCount: flags['include-lines-count'] as boolean | undefined,\n };\n}\n\nexport const cliSpec: CliSpec = {\n name: 'peek',\n description: 'Print concise folder summaries for agents.',\n positionals: [{ name: 'path', required: true, description: 'Folder to peek at' }],\n flags: {\n deep: {\n type: 'boolean',\n description: 'Recursively include child folders',\n },\n ...outputFlags,\n },\n run: async (ctx) => {\n const { peekFolder } = await import('./index.js');\n console.log(await peekFolder(ctx.positionals[0], resolveCliScanOptions(ctx.flags)));\n },\n};\n"],"mappings":"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","names":["peekFolder"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/cli-spec.ts"],"sourcesContent":["import { execFile, spawn } from 'node:child_process';\nimport { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nexport const GENERATED_PEEK_FILE = '.folder-peek.generated.md';\nconst execFileAsync = promisify(execFile);\n\nexport type ScanOptions = {\n deep?: boolean;\n preset?: PeekOutputPresetName;\n indent?: boolean;\n filePaths?: PeekFilePathMode;\n file_paths?: PeekFilePathMode;\n includeLinesCount?: boolean;\n include_lines_count?: boolean;\n};\n\ntype FileSummary = {\n path: string;\n kind: 'markdown' | 'typescript' | 'python';\n lines: number;\n items: string[];\n};\n\nexport type PeekFilePathMode = 'concise' | 'full';\nexport type PeekOutputPresetName = 'agent' | 'human';\n\nexport type PeekOutputConfig = {\n deep: boolean;\n indent: boolean;\n filePaths: PeekFilePathMode;\n includeLinesCount: boolean;\n};\n\nexport const PEEK_OUTPUT_PRESETS: Record<PeekOutputPresetName, PeekOutputConfig> = {\n human: {\n deep: true,\n indent: true,\n filePaths: 'full',\n includeLinesCount: true,\n },\n agent: {\n deep: true,\n indent: false,\n filePaths: 'concise',\n includeLinesCount: true,\n },\n};\n\ntype FolderSummary = {\n path: string;\n files: FileSummary[];\n folders: FolderSummary[];\n omittedFiles: string[];\n};\n\nconst SKIP_DIRS = new Set([\n '.git',\n '.next',\n 'coverage',\n 'dist',\n 'dist-ssr',\n 'node_modules',\n]);\n\nconst SKIP_FILES = new Set([GENERATED_PEEK_FILE]);\n\nexport async function scanFolderPeek(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const repoRoot = await findRepoRoot(root);\n const outputConfig = resolveOutputConfig(options);\n const fileLimit = createLimiter(64);\n const summary = await collectFolderSummary(\n root,\n repoRoot,\n root,\n outputConfig.deep,\n outputConfig,\n fileLimit,\n );\n\n return `${renderFolderSummary(summary, outputConfig)}\\n`;\n}\n\nexport async function generateFolderPeek(\n folder: string,\n options: ScanOptions = {},\n): Promise<{ path: string; content: string }> {\n const root = path.resolve(folder);\n const content = await scanFolderPeek(root, options);\n const outPath = path.join(root, GENERATED_PEEK_FILE);\n await mkdir(root, { recursive: true });\n await writeFile(outPath, content, 'utf8');\n return { path: outPath, content };\n}\n\nexport async function peekFolder(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const generatedPath = path.join(root, GENERATED_PEEK_FILE);\n try {\n if (!options.deep && !hasOutputOverrides(options)) return await readFile(generatedPath, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n return (await generateFolderPeek(root, options)).content;\n}\n\nasync function collectFolderSummary(\n root: string,\n repoRoot: string,\n dir: string,\n deep: boolean,\n outputConfig: PeekOutputConfig,\n fileLimit: <T>(task: () => Promise<T>) => Promise<T>,\n): Promise<FolderSummary> {\n const entries = await readdir(dir, { withFileTypes: true });\n const fileTasks: Array<Promise<{ name: string; summary: FileSummary | null }>> = [];\n const folderTasks: Array<Promise<FolderSummary>> = [];\n const omittedFiles: string[] = [];\n const ignoredPaths = await checkIgnoredPaths(root, entries.map((entry) => path.join(dir, entry.name)));\n\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const fullPath = path.join(dir, entry.name);\n if (ignoredPaths.has(toPosix(path.relative(root, fullPath)))) continue;\n\n if (entry.isDirectory()) {\n if (!deep || shouldSkipDirectory(entry.name)) continue;\n folderTasks.push(collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig, fileLimit));\n continue;\n }\n if (!entry.isFile() || shouldSkipFile(entry.name)) continue;\n\n fileTasks.push(\n fileLimit(async () => ({\n name: entry.name,\n summary: await summarizeFile(root, repoRoot, fullPath, outputConfig),\n })),\n );\n }\n\n const fileResults = await Promise.all(fileTasks);\n const files = fileResults\n .map((result) => result.summary)\n .filter((summary): summary is FileSummary => summary !== null);\n omittedFiles.push(\n ...fileResults\n .filter((result) => result.summary === null)\n .map((result) => result.name),\n );\n const folders = await Promise.all(folderTasks);\n\n return {\n path: formatFolderPath(root, repoRoot, dir),\n files: files.sort((a, b) => a.path.localeCompare(b.path)),\n folders: folders.sort((a, b) => a.path.localeCompare(b.path)),\n omittedFiles: omittedFiles.sort((a, b) => a.localeCompare(b)),\n };\n}\n\nfunction formatFolderPath(root: string, repoRoot: string, dir: string): string {\n const base = isInsidePath(repoRoot, dir) ? repoRoot : root;\n const relativePath = toPosix(path.relative(base, dir));\n return relativePath || '.';\n}\n\nfunction renderFolderSummary(\n summary: FolderSummary,\n outputConfig: PeekOutputConfig,\n depth = 0,\n): string {\n const indent = outputConfig.indent ? '\\t'.repeat(Math.max(0, depth - 1)) : '';\n const childIndent = outputConfig.indent ? '\\t'.repeat(depth) : '';\n const lines = [`${indent}<folder path=\"${escapeAttribute(summary.path)}\">`];\n\n for (const file of summary.files) {\n const lineCount = outputConfig.includeLinesCount ? ` lines=\"${file.lines}\"` : '';\n lines.push(`${childIndent}<file path=\"${escapeAttribute(file.path)}\"${lineCount}>`);\n for (const item of file.items) {\n const prefix = item.startsWith('[ln ') ? '' : '- ';\n lines.push(`${childIndent}${prefix}${item}`);\n }\n lines.push(`${childIndent}</file>`);\n }\n\n for (const folder of summary.folders) {\n lines.push(renderFolderSummary(folder, outputConfig, depth + 1));\n }\n\n if (summary.omittedFiles.length > 0) {\n lines.push(`${childIndent}<omitted_files>`);\n for (const omittedFile of summary.omittedFiles) {\n lines.push(`${childIndent}- ${escapeText(omittedFile)}`);\n }\n lines.push(`${childIndent}</omitted_files>`);\n }\n\n lines.push(`${indent}</folder>`);\n return lines.join('\\n');\n}\n\nasync function summarizeFile(\n root: string,\n repoRoot: string,\n fullPath: string,\n outputConfig: PeekOutputConfig,\n): Promise<FileSummary | null> {\n const extension = path.extname(fullPath);\n const relativePath = formatFilePath(root, repoRoot, fullPath, outputConfig.filePaths);\n\n if (extension === '.md' || extension === '.mdx') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'markdown',\n lines: countLines(text),\n items: extractMarkdownHeadings(text),\n };\n }\n if (isTypeScriptLikeFile(extension, fullPath)) {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'typescript',\n lines: countLines(text),\n items: extractTypeScriptSymbols(text),\n };\n }\n if (extension === '.py') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'python',\n lines: countLines(text),\n items: extractPythonSymbols(text),\n };\n }\n return null;\n}\n\nfunction isTypeScriptLikeFile(extension: string, fullPath: string): boolean {\n return ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs'].includes(extension) && !fullPath.endsWith('.d.ts');\n}\n\nfunction extractMarkdownHeadings(text: string): string[] {\n return text\n .split(/\\r?\\n/)\n .map((line) => /^(#{1,6})\\s+(.+?)\\s*$/.exec(line))\n .filter((match): match is RegExpExecArray => match !== null)\n .map((match) => `h${match[1].length} ${match[2].replace(/\\s+#+$/, '').trim()}`);\n}\n\nfunction extractTypeScriptSymbols(text: string): string[] {\n const items: string[] = [];\n const source = stripBlockComments(text);\n const patterns: Array<[RegExp, string]> = [\n [/^(?:export\\s+)?(?:abstract\\s+)?class\\s+([A-Za-z_$][\\w$]*)/gm, 'class'],\n [/^(?:export\\s+)?interface\\s+([A-Za-z_$][\\w$]*)/gm, 'interface'],\n [/^(?:export\\s+)?type\\s+([A-Za-z_$][\\w$]*)\\s*=/gm, 'type'],\n [/^(?:export\\s+)?(?:declare\\s+)?(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)/gm, 'const'],\n [\n /^(?:export\\s+)?(?:async\\s+)?function\\s+([A-Za-z_$][\\w$]*)\\s*\\(/gm,\n 'function',\n ],\n ];\n\n for (const [pattern, label] of patterns) {\n for (const match of source.matchAll(pattern)) {\n const range = lineRangeForTypeScriptSymbol(source, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${label} ${match[1]}`);\n }\n }\n items.push(...extractTypeScriptTestCalls(source));\n return unique(items);\n}\n\nfunction extractTypeScriptTestCalls(text: string): string[] {\n const items: string[] = [];\n const pattern = /^\\s*(describe|test|it)\\s*\\(\\s*(['\"`])((?:\\\\.|(?!\\2)[\\s\\S])*?)\\2/gm;\n\n for (const match of text.matchAll(pattern)) {\n const callName = match[1];\n const quote = match[2];\n const title = match[3];\n const range = lineRangeForCallExpression(text, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${callName}(${quote}${title}${quote})`);\n }\n\n return items;\n}\n\nfunction extractPythonSymbols(text: string): string[] {\n const items: string[] = [];\n const lines = text.split(/\\r?\\n/);\n for (const [index, line] of lines.entries()) {\n const lineNumber = index + 1;\n let match = /^class\\s+([A-Za-z_]\\w*)\\s*[:(]/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} class ${match[1]}`);\n continue;\n }\n match = /^(?:async\\s+)?def\\s+([A-Za-z_]\\w*)\\s*\\(/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} function ${match[1]}`);\n continue;\n }\n match = /^([A-Z][A-Z0-9_]*)\\s*[:=]/.exec(line);\n if (match) items.push(`${formatLineRange({ start: lineNumber, end: lineNumber })} const ${match[1]}`);\n }\n return unique(items);\n}\n\nfunction shouldSkipDirectory(name: string): boolean {\n return SKIP_DIRS.has(name);\n}\n\nfunction shouldSkipFile(name: string): boolean {\n return SKIP_FILES.has(name) || name.endsWith('.map');\n}\n\nfunction stripBlockComments(text: string): string {\n return text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, (comment) =>\n comment.replace(/[^\\r\\n]/g, ' '),\n );\n}\n\nfunction unique(items: string[]): string[] {\n return Array.from(new Set(items));\n}\n\nfunction createLimiter(maxConcurrent: number): <T>(task: () => Promise<T>) => Promise<T> {\n let active = 0;\n const queue: Array<() => void> = [];\n\n return async function limit<T>(task: () => Promise<T>): Promise<T> {\n if (active >= maxConcurrent) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n\n active += 1;\n try {\n return await task();\n } finally {\n active -= 1;\n queue.shift()?.();\n }\n };\n}\n\nfunction lineNumberAt(text: string, index: number): number {\n let lineNumber = 1;\n for (let offset = 0; offset < index; offset += 1) {\n if (text.charCodeAt(offset) === 10) lineNumber += 1;\n }\n return lineNumber;\n}\n\nfunction lineRangeForTypeScriptSymbol(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const lineEnd = text.indexOf('\\n', index);\n const declarationEnd = lineEnd === -1 ? text.length : lineEnd;\n const openingBrace = text.indexOf('{', index);\n\n if (openingBrace !== -1 && openingBrace < declarationEnd) {\n const closingBrace = findMatchingBrace(text, openingBrace);\n if (closingBrace !== -1) return { start, end: lineNumberAt(text, closingBrace) };\n }\n\n const semicolon = text.indexOf(';', index);\n if (semicolon !== -1 && semicolon < declarationEnd) {\n return { start, end: lineNumberAt(text, semicolon) };\n }\n\n return { start, end: start };\n}\n\nfunction findMatchingBrace(text: string, openingBrace: number): number {\n let depth = 0;\n for (let index = openingBrace; index < text.length; index += 1) {\n const char = text[index];\n if (char === '{') depth += 1;\n if (char === '}') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForCallExpression(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const openingParen = text.indexOf('(', index);\n if (openingParen === -1) return { start, end: start };\n\n const closingParen = findMatchingParen(text, openingParen);\n if (closingParen === -1) return { start, end: start };\n return { start, end: lineNumberAt(text, closingParen) };\n}\n\nfunction findMatchingParen(text: string, openingParen: number): number {\n let depth = 0;\n let quote: string | null = null;\n\n for (let index = openingParen; index < text.length; index += 1) {\n const char = text[index];\n if (quote) {\n if (char === '\\\\') {\n index += 1;\n continue;\n }\n if (char === quote) quote = null;\n continue;\n }\n if (char === '\"' || char === \"'\" || char === '`') {\n quote = char;\n continue;\n }\n if (char === '(') depth += 1;\n if (char === ')') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForPythonBlock(\n lines: string[],\n startIndex: number,\n): { start: number; end: number } {\n const startIndent = indentationLength(lines[startIndex] ?? '');\n let endIndex = startIndex;\n\n for (let index = startIndex + 1; index < lines.length; index += 1) {\n const line = lines[index] ?? '';\n if (line.trim() === '') {\n endIndex = index;\n continue;\n }\n if (indentationLength(line) <= startIndent) break;\n endIndex = index;\n }\n\n while (endIndex > startIndex && (lines[endIndex] ?? '').trim() === '') endIndex -= 1;\n return { start: startIndex + 1, end: endIndex + 1 };\n}\n\nfunction indentationLength(line: string): number {\n return line.match(/^\\s*/)?.[0].length ?? 0;\n}\n\nfunction formatLineRange(range: { start: number; end: number }): string {\n return range.start === range.end ? `[ln ${range.start}]` : `[ln ${range.start}-${range.end}]`;\n}\n\nfunction formatFilePath(\n root: string,\n repoRoot: string,\n fullPath: string,\n filePaths: PeekFilePathMode,\n): string {\n if (filePaths === 'concise') return `/${path.basename(fullPath)}`;\n const base = isInsidePath(repoRoot, fullPath) ? repoRoot : root;\n return toPosix(path.relative(base, fullPath));\n}\n\nfunction resolveOutputConfig(options: ScanOptions): PeekOutputConfig {\n const presetName = options.preset ?? 'human';\n const preset = PEEK_OUTPUT_PRESETS[presetName];\n if (!preset) throw new Error(`Unknown peek output preset: ${presetName}`);\n\n return {\n deep: options.deep ?? preset.deep,\n indent: options.indent ?? preset.indent,\n filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths),\n includeLinesCount:\n options.includeLinesCount ?? options.include_lines_count ?? preset.includeLinesCount,\n };\n}\n\nfunction normalizeFilePathMode(value: PeekFilePathMode): PeekFilePathMode {\n if (value === 'concise' || value === 'full') return value;\n throw new Error(`Unknown peek file path mode: ${String(value)}`);\n}\n\nfunction countLines(text: string): number {\n if (text.length === 0) return 0;\n const newlineCount = text.match(/\\r\\n|\\r|\\n/g)?.length ?? 0;\n return newlineCount + (/(?:\\r\\n|\\r|\\n)$/.test(text) ? 0 : 1);\n}\n\nfunction hasOutputOverrides(options: ScanOptions): boolean {\n return (\n options.preset !== undefined ||\n options.indent !== undefined ||\n options.filePaths !== undefined ||\n options.file_paths !== undefined ||\n options.includeLinesCount !== undefined ||\n options.include_lines_count !== undefined\n );\n}\n\nfunction isInsidePath(parent: string, child: string): boolean {\n const relativePath = path.relative(parent, child);\n return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join('/');\n}\n\nfunction escapeAttribute(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;');\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/</g, '&lt;');\n}\n\nasync function checkIgnoredPaths(root: string, fullPaths: string[]): Promise<Set<string>> {\n const relativePaths = fullPaths\n .map((fullPath) => toPosix(path.relative(root, fullPath)))\n .filter((relativePath) => relativePath && !relativePath.startsWith('..'));\n\n if (relativePaths.length === 0) return new Set();\n\n return await new Promise((resolve) => {\n const child = spawn('git', ['-C', root, 'check-ignore', '--stdin'], {\n stdio: ['pipe', 'pipe', 'ignore'],\n });\n let stdout = '';\n\n child.stdout.setEncoding('utf8');\n child.stdout.on('data', (chunk) => {\n stdout += chunk;\n });\n child.on('error', () => resolve(new Set()));\n child.on('close', (code) => {\n if (code !== 0 && code !== 1) {\n resolve(new Set());\n return;\n }\n resolve(new Set(stdout.split(/\\r?\\n/).filter(Boolean)));\n });\n\n child.stdin.end(relativePaths.join('\\n'));\n });\n}\n\nasync function findRepoRoot(root: string): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', ['-C', root, 'rev-parse', '--show-toplevel']);\n return path.resolve(stdout.trim());\n } catch {\n return root;\n }\n}\n","import type { CliSpec } from '@davstack/cli-utils';\nimport type { PeekFilePathMode, PeekOutputPresetName, ScanOptions } from './index.js';\n\nconst outputFlags = {\n human: {\n type: 'boolean',\n default: false,\n description: 'Use human-readable output defaults: indented XML and full repo-relative file paths',\n },\n agent: {\n type: 'boolean',\n default: false,\n description: 'Use token-optimized output defaults: no indentation and concise file paths',\n },\n indent: {\n type: 'boolean',\n description: 'Indent nested folder output',\n },\n file_paths: {\n type: 'string',\n values: ['concise', 'full'],\n description: 'File path style: concise or full',\n },\n 'include-lines-count': {\n type: 'boolean',\n description: 'Include total line counts on file tags',\n },\n} as const;\n\nfunction resolveCliScanOptions(flags: Record<string, unknown>): ScanOptions {\n const human = flags.human === true;\n const agent = flags.agent === true;\n if (human && agent) throw new Error('Use only one output preset: --human or --agent');\n\n const preset: PeekOutputPresetName | undefined = agent ? 'agent' : human ? 'human' : undefined;\n const filePaths = flags.file_paths;\n if (filePaths !== undefined && filePaths !== 'concise' && filePaths !== 'full') {\n throw new Error('--file_paths must be \"concise\" or \"full\"');\n }\n\n return {\n deep: flags.deep as boolean,\n preset,\n indent: flags.indent as boolean | undefined,\n filePaths: filePaths as PeekFilePathMode | undefined,\n includeLinesCount: flags['include-lines-count'] as boolean | undefined,\n };\n}\n\nexport const cliSpec: CliSpec = {\n name: 'peek',\n description: 'Print concise folder summaries for agents.',\n examples: [\n 'peek .',\n 'peek packages/context-compactor --agent',\n 'peek . --human',\n 'peek . --file_paths=full --no-include-lines-count',\n ],\n defaults: [\n '--human behavior: deep scan, indented output, full file paths, line counts',\n '--agent behavior: deep scan, no indentation, concise file paths, line counts',\n ],\n positionals: [{ name: 'path', required: true, description: 'Folder to peek at' }],\n flags: {\n deep: {\n type: 'boolean',\n description: 'Recursively include child folders',\n },\n ...outputFlags,\n },\n run: async (ctx) => {\n const { peekFolder } = await import('./index.js');\n console.log(await peekFolder(ctx.positionals[0], resolveCliScanOptions(ctx.flags)));\n },\n};\n"],"mappings":"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","names":["peekFolder"]}
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ __export(src_exports, {
17
17
  peekFolder: () => peekFolder,
18
18
  scanFolderPeek: () => scanFolderPeek
19
19
  });
20
- import { execFile } from "child_process";
20
+ import { execFile, spawn } from "child_process";
21
21
  import { mkdir, readdir, readFile, writeFile } from "fs/promises";
22
22
  import path from "path";
23
23
  import { promisify } from "util";
@@ -25,7 +25,15 @@ async function scanFolderPeek(folder, options = {}) {
25
25
  const root = path.resolve(folder);
26
26
  const repoRoot = await findRepoRoot(root);
27
27
  const outputConfig = resolveOutputConfig(options);
28
- const summary = await collectFolderSummary(root, repoRoot, root, outputConfig.deep, outputConfig);
28
+ const fileLimit = createLimiter(64);
29
+ const summary = await collectFolderSummary(
30
+ root,
31
+ repoRoot,
32
+ root,
33
+ outputConfig.deep,
34
+ outputConfig,
35
+ fileLimit
36
+ );
29
37
  return `${renderFolderSummary(summary, outputConfig)}
30
38
  `;
31
39
  }
@@ -47,27 +55,34 @@ async function peekFolder(folder, options = {}) {
47
55
  }
48
56
  return (await generateFolderPeek(root, options)).content;
49
57
  }
50
- async function collectFolderSummary(root, repoRoot, dir, deep, outputConfig) {
58
+ async function collectFolderSummary(root, repoRoot, dir, deep, outputConfig, fileLimit) {
51
59
  const entries = await readdir(dir, { withFileTypes: true });
52
- const files = [];
53
- const folders = [];
60
+ const fileTasks = [];
61
+ const folderTasks = [];
54
62
  const omittedFiles = [];
63
+ const ignoredPaths = await checkIgnoredPaths(root, entries.map((entry) => path.join(dir, entry.name)));
55
64
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
56
65
  const fullPath = path.join(dir, entry.name);
57
- if (await isGitIgnored(root, fullPath)) continue;
66
+ if (ignoredPaths.has(toPosix(path.relative(root, fullPath)))) continue;
58
67
  if (entry.isDirectory()) {
59
68
  if (!deep || shouldSkipDirectory(entry.name)) continue;
60
- folders.push(await collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig));
69
+ folderTasks.push(collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig, fileLimit));
61
70
  continue;
62
71
  }
63
72
  if (!entry.isFile() || shouldSkipFile(entry.name)) continue;
64
- const summary = await summarizeFile(root, repoRoot, fullPath, outputConfig);
65
- if (summary) {
66
- files.push(summary);
67
- } else {
68
- omittedFiles.push(entry.name);
69
- }
73
+ fileTasks.push(
74
+ fileLimit(async () => ({
75
+ name: entry.name,
76
+ summary: await summarizeFile(root, repoRoot, fullPath, outputConfig)
77
+ }))
78
+ );
70
79
  }
80
+ const fileResults = await Promise.all(fileTasks);
81
+ const files = fileResults.map((result) => result.summary).filter((summary) => summary !== null);
82
+ omittedFiles.push(
83
+ ...fileResults.filter((result) => result.summary === null).map((result) => result.name)
84
+ );
85
+ const folders = await Promise.all(folderTasks);
71
86
  return {
72
87
  path: formatFolderPath(root, repoRoot, dir),
73
88
  files: files.sort((a, b) => a.path.localeCompare(b.path)),
@@ -213,6 +228,22 @@ function stripBlockComments(text) {
213
228
  function unique(items) {
214
229
  return Array.from(new Set(items));
215
230
  }
231
+ function createLimiter(maxConcurrent) {
232
+ let active = 0;
233
+ const queue = [];
234
+ return async function limit(task) {
235
+ if (active >= maxConcurrent) {
236
+ await new Promise((resolve) => queue.push(resolve));
237
+ }
238
+ active += 1;
239
+ try {
240
+ return await task();
241
+ } finally {
242
+ active -= 1;
243
+ queue.shift()?.();
244
+ }
245
+ };
246
+ }
216
247
  function lineNumberAt(text, index) {
217
248
  let lineNumber = 1;
218
249
  for (let offset = 0; offset < index; offset += 1) {
@@ -342,17 +373,28 @@ function escapeAttribute(value) {
342
373
  function escapeText(value) {
343
374
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;");
344
375
  }
345
- async function isGitIgnored(root, fullPath) {
346
- const relativePath = toPosix(path.relative(root, fullPath));
347
- if (!relativePath || relativePath.startsWith("..")) return false;
348
- try {
349
- await execFileAsync("git", ["-C", root, "check-ignore", "--quiet", "--", relativePath]);
350
- return true;
351
- } catch (error) {
352
- const code2 = error.code;
353
- if (code2 === 1 || code2 === 128 || code2 === "ENOENT") return false;
354
- return false;
355
- }
376
+ async function checkIgnoredPaths(root, fullPaths) {
377
+ const relativePaths = fullPaths.map((fullPath) => toPosix(path.relative(root, fullPath))).filter((relativePath) => relativePath && !relativePath.startsWith(".."));
378
+ if (relativePaths.length === 0) return /* @__PURE__ */ new Set();
379
+ return await new Promise((resolve) => {
380
+ const child = spawn("git", ["-C", root, "check-ignore", "--stdin"], {
381
+ stdio: ["pipe", "pipe", "ignore"]
382
+ });
383
+ let stdout = "";
384
+ child.stdout.setEncoding("utf8");
385
+ child.stdout.on("data", (chunk) => {
386
+ stdout += chunk;
387
+ });
388
+ child.on("error", () => resolve(/* @__PURE__ */ new Set()));
389
+ child.on("close", (code2) => {
390
+ if (code2 !== 0 && code2 !== 1) {
391
+ resolve(/* @__PURE__ */ new Set());
392
+ return;
393
+ }
394
+ resolve(new Set(stdout.split(/\r?\n/).filter(Boolean)));
395
+ });
396
+ child.stdin.end(relativePaths.join("\n"));
397
+ });
356
398
  }
357
399
  async function findRepoRoot(root) {
358
400
  try {
@@ -415,6 +457,7 @@ var outputFlags = {
415
457
  },
416
458
  file_paths: {
417
459
  type: "string",
460
+ values: ["concise", "full"],
418
461
  description: "File path style: concise or full"
419
462
  },
420
463
  "include-lines-count": {
@@ -442,6 +485,16 @@ function resolveCliScanOptions(flags) {
442
485
  var cliSpec = {
443
486
  name: "peek",
444
487
  description: "Print concise folder summaries for agents.",
488
+ examples: [
489
+ "peek .",
490
+ "peek packages/context-compactor --agent",
491
+ "peek . --human",
492
+ "peek . --file_paths=full --no-include-lines-count"
493
+ ],
494
+ defaults: [
495
+ "--human behavior: deep scan, indented output, full file paths, line counts",
496
+ "--agent behavior: deep scan, no indentation, concise file paths, line counts"
497
+ ],
445
498
  positionals: [{ name: "path", required: true, description: "Folder to peek at" }],
446
499
  flags: {
447
500
  deep: {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/cli.ts","../src/cli-spec.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nexport const GENERATED_PEEK_FILE = '.folder-peek.generated.md';\nconst execFileAsync = promisify(execFile);\n\nexport type ScanOptions = {\n deep?: boolean;\n preset?: PeekOutputPresetName;\n indent?: boolean;\n filePaths?: PeekFilePathMode;\n file_paths?: PeekFilePathMode;\n includeLinesCount?: boolean;\n include_lines_count?: boolean;\n};\n\ntype FileSummary = {\n path: string;\n kind: 'markdown' | 'typescript' | 'python';\n lines: number;\n items: string[];\n};\n\nexport type PeekFilePathMode = 'concise' | 'full';\nexport type PeekOutputPresetName = 'agent' | 'human';\n\nexport type PeekOutputConfig = {\n deep: boolean;\n indent: boolean;\n filePaths: PeekFilePathMode;\n includeLinesCount: boolean;\n};\n\nexport const PEEK_OUTPUT_PRESETS: Record<PeekOutputPresetName, PeekOutputConfig> = {\n human: {\n deep: true,\n indent: true,\n filePaths: 'full',\n includeLinesCount: true,\n },\n agent: {\n deep: true,\n indent: false,\n filePaths: 'concise',\n includeLinesCount: true,\n },\n};\n\ntype FolderSummary = {\n path: string;\n files: FileSummary[];\n folders: FolderSummary[];\n omittedFiles: string[];\n};\n\nconst SKIP_DIRS = new Set([\n '.git',\n '.next',\n 'coverage',\n 'dist',\n 'dist-ssr',\n 'node_modules',\n]);\n\nconst SKIP_FILES = new Set([GENERATED_PEEK_FILE]);\n\nexport async function scanFolderPeek(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const repoRoot = await findRepoRoot(root);\n const outputConfig = resolveOutputConfig(options);\n const summary = await collectFolderSummary(root, repoRoot, root, outputConfig.deep, outputConfig);\n\n return `${renderFolderSummary(summary, outputConfig)}\\n`;\n}\n\nexport async function generateFolderPeek(\n folder: string,\n options: ScanOptions = {},\n): Promise<{ path: string; content: string }> {\n const root = path.resolve(folder);\n const content = await scanFolderPeek(root, options);\n const outPath = path.join(root, GENERATED_PEEK_FILE);\n await mkdir(root, { recursive: true });\n await writeFile(outPath, content, 'utf8');\n return { path: outPath, content };\n}\n\nexport async function peekFolder(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const generatedPath = path.join(root, GENERATED_PEEK_FILE);\n try {\n if (!options.deep && !hasOutputOverrides(options)) return await readFile(generatedPath, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n return (await generateFolderPeek(root, options)).content;\n}\n\nasync function collectFolderSummary(\n root: string,\n repoRoot: string,\n dir: string,\n deep: boolean,\n outputConfig: PeekOutputConfig,\n): Promise<FolderSummary> {\n const entries = await readdir(dir, { withFileTypes: true });\n const files: FileSummary[] = [];\n const folders: FolderSummary[] = [];\n const omittedFiles: string[] = [];\n\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const fullPath = path.join(dir, entry.name);\n if (await isGitIgnored(root, fullPath)) continue;\n\n if (entry.isDirectory()) {\n if (!deep || shouldSkipDirectory(entry.name)) continue;\n folders.push(await collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig));\n continue;\n }\n if (!entry.isFile() || shouldSkipFile(entry.name)) continue;\n\n const summary = await summarizeFile(root, repoRoot, fullPath, outputConfig);\n if (summary) {\n files.push(summary);\n } else {\n omittedFiles.push(entry.name);\n }\n }\n\n return {\n path: formatFolderPath(root, repoRoot, dir),\n files: files.sort((a, b) => a.path.localeCompare(b.path)),\n folders: folders.sort((a, b) => a.path.localeCompare(b.path)),\n omittedFiles: omittedFiles.sort((a, b) => a.localeCompare(b)),\n };\n}\n\nfunction formatFolderPath(root: string, repoRoot: string, dir: string): string {\n const base = isInsidePath(repoRoot, dir) ? repoRoot : root;\n const relativePath = toPosix(path.relative(base, dir));\n return relativePath || '.';\n}\n\nfunction renderFolderSummary(\n summary: FolderSummary,\n outputConfig: PeekOutputConfig,\n depth = 0,\n): string {\n const indent = outputConfig.indent ? '\\t'.repeat(Math.max(0, depth - 1)) : '';\n const childIndent = outputConfig.indent ? '\\t'.repeat(depth) : '';\n const lines = [`${indent}<folder path=\"${escapeAttribute(summary.path)}\">`];\n\n for (const file of summary.files) {\n const lineCount = outputConfig.includeLinesCount ? ` lines=\"${file.lines}\"` : '';\n lines.push(`${childIndent}<file path=\"${escapeAttribute(file.path)}\"${lineCount}>`);\n for (const item of file.items) {\n const prefix = item.startsWith('[ln ') ? '' : '- ';\n lines.push(`${childIndent}${prefix}${item}`);\n }\n lines.push(`${childIndent}</file>`);\n }\n\n for (const folder of summary.folders) {\n lines.push(renderFolderSummary(folder, outputConfig, depth + 1));\n }\n\n if (summary.omittedFiles.length > 0) {\n lines.push(`${childIndent}<omitted_files>`);\n for (const omittedFile of summary.omittedFiles) {\n lines.push(`${childIndent}- ${escapeText(omittedFile)}`);\n }\n lines.push(`${childIndent}</omitted_files>`);\n }\n\n lines.push(`${indent}</folder>`);\n return lines.join('\\n');\n}\n\nasync function summarizeFile(\n root: string,\n repoRoot: string,\n fullPath: string,\n outputConfig: PeekOutputConfig,\n): Promise<FileSummary | null> {\n const extension = path.extname(fullPath);\n const relativePath = formatFilePath(root, repoRoot, fullPath, outputConfig.filePaths);\n\n if (extension === '.md' || extension === '.mdx') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'markdown',\n lines: countLines(text),\n items: extractMarkdownHeadings(text),\n };\n }\n if (isTypeScriptLikeFile(extension, fullPath)) {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'typescript',\n lines: countLines(text),\n items: extractTypeScriptSymbols(text),\n };\n }\n if (extension === '.py') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'python',\n lines: countLines(text),\n items: extractPythonSymbols(text),\n };\n }\n return null;\n}\n\nfunction isTypeScriptLikeFile(extension: string, fullPath: string): boolean {\n return ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs'].includes(extension) && !fullPath.endsWith('.d.ts');\n}\n\nfunction extractMarkdownHeadings(text: string): string[] {\n return text\n .split(/\\r?\\n/)\n .map((line) => /^(#{1,6})\\s+(.+?)\\s*$/.exec(line))\n .filter((match): match is RegExpExecArray => match !== null)\n .map((match) => `h${match[1].length} ${match[2].replace(/\\s+#+$/, '').trim()}`);\n}\n\nfunction extractTypeScriptSymbols(text: string): string[] {\n const items: string[] = [];\n const source = stripBlockComments(text);\n const patterns: Array<[RegExp, string]> = [\n [/^(?:export\\s+)?(?:abstract\\s+)?class\\s+([A-Za-z_$][\\w$]*)/gm, 'class'],\n [/^(?:export\\s+)?interface\\s+([A-Za-z_$][\\w$]*)/gm, 'interface'],\n [/^(?:export\\s+)?type\\s+([A-Za-z_$][\\w$]*)\\s*=/gm, 'type'],\n [/^(?:export\\s+)?(?:declare\\s+)?(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)/gm, 'const'],\n [\n /^(?:export\\s+)?(?:async\\s+)?function\\s+([A-Za-z_$][\\w$]*)\\s*\\(/gm,\n 'function',\n ],\n ];\n\n for (const [pattern, label] of patterns) {\n for (const match of source.matchAll(pattern)) {\n const range = lineRangeForTypeScriptSymbol(source, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${label} ${match[1]}`);\n }\n }\n items.push(...extractTypeScriptTestCalls(source));\n return unique(items);\n}\n\nfunction extractTypeScriptTestCalls(text: string): string[] {\n const items: string[] = [];\n const pattern = /^\\s*(describe|test|it)\\s*\\(\\s*(['\"`])((?:\\\\.|(?!\\2)[\\s\\S])*?)\\2/gm;\n\n for (const match of text.matchAll(pattern)) {\n const callName = match[1];\n const quote = match[2];\n const title = match[3];\n const range = lineRangeForCallExpression(text, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${callName}(${quote}${title}${quote})`);\n }\n\n return items;\n}\n\nfunction extractPythonSymbols(text: string): string[] {\n const items: string[] = [];\n const lines = text.split(/\\r?\\n/);\n for (const [index, line] of lines.entries()) {\n const lineNumber = index + 1;\n let match = /^class\\s+([A-Za-z_]\\w*)\\s*[:(]/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} class ${match[1]}`);\n continue;\n }\n match = /^(?:async\\s+)?def\\s+([A-Za-z_]\\w*)\\s*\\(/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} function ${match[1]}`);\n continue;\n }\n match = /^([A-Z][A-Z0-9_]*)\\s*[:=]/.exec(line);\n if (match) items.push(`${formatLineRange({ start: lineNumber, end: lineNumber })} const ${match[1]}`);\n }\n return unique(items);\n}\n\nfunction shouldSkipDirectory(name: string): boolean {\n return SKIP_DIRS.has(name);\n}\n\nfunction shouldSkipFile(name: string): boolean {\n return SKIP_FILES.has(name) || name.endsWith('.map');\n}\n\nfunction stripBlockComments(text: string): string {\n return text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, (comment) =>\n comment.replace(/[^\\r\\n]/g, ' '),\n );\n}\n\nfunction unique(items: string[]): string[] {\n return Array.from(new Set(items));\n}\n\nfunction lineNumberAt(text: string, index: number): number {\n let lineNumber = 1;\n for (let offset = 0; offset < index; offset += 1) {\n if (text.charCodeAt(offset) === 10) lineNumber += 1;\n }\n return lineNumber;\n}\n\nfunction lineRangeForTypeScriptSymbol(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const lineEnd = text.indexOf('\\n', index);\n const declarationEnd = lineEnd === -1 ? text.length : lineEnd;\n const openingBrace = text.indexOf('{', index);\n\n if (openingBrace !== -1 && openingBrace < declarationEnd) {\n const closingBrace = findMatchingBrace(text, openingBrace);\n if (closingBrace !== -1) return { start, end: lineNumberAt(text, closingBrace) };\n }\n\n const semicolon = text.indexOf(';', index);\n if (semicolon !== -1 && semicolon < declarationEnd) {\n return { start, end: lineNumberAt(text, semicolon) };\n }\n\n return { start, end: start };\n}\n\nfunction findMatchingBrace(text: string, openingBrace: number): number {\n let depth = 0;\n for (let index = openingBrace; index < text.length; index += 1) {\n const char = text[index];\n if (char === '{') depth += 1;\n if (char === '}') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForCallExpression(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const openingParen = text.indexOf('(', index);\n if (openingParen === -1) return { start, end: start };\n\n const closingParen = findMatchingParen(text, openingParen);\n if (closingParen === -1) return { start, end: start };\n return { start, end: lineNumberAt(text, closingParen) };\n}\n\nfunction findMatchingParen(text: string, openingParen: number): number {\n let depth = 0;\n let quote: string | null = null;\n\n for (let index = openingParen; index < text.length; index += 1) {\n const char = text[index];\n if (quote) {\n if (char === '\\\\') {\n index += 1;\n continue;\n }\n if (char === quote) quote = null;\n continue;\n }\n if (char === '\"' || char === \"'\" || char === '`') {\n quote = char;\n continue;\n }\n if (char === '(') depth += 1;\n if (char === ')') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForPythonBlock(\n lines: string[],\n startIndex: number,\n): { start: number; end: number } {\n const startIndent = indentationLength(lines[startIndex] ?? '');\n let endIndex = startIndex;\n\n for (let index = startIndex + 1; index < lines.length; index += 1) {\n const line = lines[index] ?? '';\n if (line.trim() === '') {\n endIndex = index;\n continue;\n }\n if (indentationLength(line) <= startIndent) break;\n endIndex = index;\n }\n\n while (endIndex > startIndex && (lines[endIndex] ?? '').trim() === '') endIndex -= 1;\n return { start: startIndex + 1, end: endIndex + 1 };\n}\n\nfunction indentationLength(line: string): number {\n return line.match(/^\\s*/)?.[0].length ?? 0;\n}\n\nfunction formatLineRange(range: { start: number; end: number }): string {\n return range.start === range.end ? `[ln ${range.start}]` : `[ln ${range.start}-${range.end}]`;\n}\n\nfunction formatFilePath(\n root: string,\n repoRoot: string,\n fullPath: string,\n filePaths: PeekFilePathMode,\n): string {\n if (filePaths === 'concise') return `/${path.basename(fullPath)}`;\n const base = isInsidePath(repoRoot, fullPath) ? repoRoot : root;\n return toPosix(path.relative(base, fullPath));\n}\n\nfunction resolveOutputConfig(options: ScanOptions): PeekOutputConfig {\n const presetName = options.preset ?? 'human';\n const preset = PEEK_OUTPUT_PRESETS[presetName];\n if (!preset) throw new Error(`Unknown peek output preset: ${presetName}`);\n\n return {\n deep: options.deep ?? preset.deep,\n indent: options.indent ?? preset.indent,\n filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths),\n includeLinesCount:\n options.includeLinesCount ?? options.include_lines_count ?? preset.includeLinesCount,\n };\n}\n\nfunction normalizeFilePathMode(value: PeekFilePathMode): PeekFilePathMode {\n if (value === 'concise' || value === 'full') return value;\n throw new Error(`Unknown peek file path mode: ${String(value)}`);\n}\n\nfunction countLines(text: string): number {\n if (text.length === 0) return 0;\n const newlineCount = text.match(/\\r\\n|\\r|\\n/g)?.length ?? 0;\n return newlineCount + (/(?:\\r\\n|\\r|\\n)$/.test(text) ? 0 : 1);\n}\n\nfunction hasOutputOverrides(options: ScanOptions): boolean {\n return (\n options.preset !== undefined ||\n options.indent !== undefined ||\n options.filePaths !== undefined ||\n options.file_paths !== undefined ||\n options.includeLinesCount !== undefined ||\n options.include_lines_count !== undefined\n );\n}\n\nfunction isInsidePath(parent: string, child: string): boolean {\n const relativePath = path.relative(parent, child);\n return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join('/');\n}\n\nfunction escapeAttribute(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;');\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/</g, '&lt;');\n}\n\nasync function isGitIgnored(root: string, fullPath: string): Promise<boolean> {\n const relativePath = toPosix(path.relative(root, fullPath));\n if (!relativePath || relativePath.startsWith('..')) return false;\n\n try {\n await execFileAsync('git', ['-C', root, 'check-ignore', '--quiet', '--', relativePath]);\n return true;\n } catch (error) {\n const code = (error as { code?: number | string }).code;\n if (code === 1 || code === 128 || code === 'ENOENT') return false;\n return false;\n }\n}\n\nasync function findRepoRoot(root: string): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', ['-C', root, 'rev-parse', '--show-toplevel']);\n return path.resolve(stdout.trim());\n } catch {\n return root;\n }\n}\n","import { defineCli } from '@davstack/cli-utils';\nimport { cliSpec } from './cli-spec.js';\n\nconst code = await defineCli(cliSpec).run(process.argv.slice(2));\nprocess.exit(code);\n","import type { CliSpec } from '@davstack/cli-utils';\nimport type { PeekFilePathMode, PeekOutputPresetName, ScanOptions } from './index.js';\n\nconst outputFlags = {\n human: {\n type: 'boolean',\n default: false,\n description: 'Use human-readable output defaults: indented XML and full repo-relative file paths',\n },\n agent: {\n type: 'boolean',\n default: false,\n description: 'Use token-optimized output defaults: no indentation and concise file paths',\n },\n indent: {\n type: 'boolean',\n description: 'Indent nested folder output',\n },\n file_paths: {\n type: 'string',\n description: 'File path style: concise or full',\n },\n 'include-lines-count': {\n type: 'boolean',\n description: 'Include total line counts on file tags',\n },\n} as const;\n\nfunction resolveCliScanOptions(flags: Record<string, unknown>): ScanOptions {\n const human = flags.human === true;\n const agent = flags.agent === true;\n if (human && agent) throw new Error('Use only one output preset: --human or --agent');\n\n const preset: PeekOutputPresetName | undefined = agent ? 'agent' : human ? 'human' : undefined;\n const filePaths = flags.file_paths;\n if (filePaths !== undefined && filePaths !== 'concise' && filePaths !== 'full') {\n throw new Error('--file_paths must be \"concise\" or \"full\"');\n }\n\n return {\n deep: flags.deep as boolean,\n preset,\n indent: flags.indent as boolean | undefined,\n filePaths: filePaths as PeekFilePathMode | undefined,\n includeLinesCount: flags['include-lines-count'] as boolean | undefined,\n };\n}\n\nexport const cliSpec: CliSpec = {\n name: 'peek',\n description: 'Print concise folder summaries for agents.',\n positionals: [{ name: 'path', required: true, description: 'Folder to peek at' }],\n flags: {\n deep: {\n type: 'boolean',\n description: 'Recursively include child folders',\n },\n ...outputFlags,\n },\n run: async (ctx) => {\n const { peekFolder } = await import('./index.js');\n console.log(await peekFolder(ctx.positionals[0], resolveCliScanOptions(ctx.flags)));\n },\n};\n"],"mappings":"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","names":["code","peekFolder"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/cli.ts","../src/cli-spec.ts"],"sourcesContent":["import { execFile, spawn } from 'node:child_process';\nimport { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nexport const GENERATED_PEEK_FILE = '.folder-peek.generated.md';\nconst execFileAsync = promisify(execFile);\n\nexport type ScanOptions = {\n deep?: boolean;\n preset?: PeekOutputPresetName;\n indent?: boolean;\n filePaths?: PeekFilePathMode;\n file_paths?: PeekFilePathMode;\n includeLinesCount?: boolean;\n include_lines_count?: boolean;\n};\n\ntype FileSummary = {\n path: string;\n kind: 'markdown' | 'typescript' | 'python';\n lines: number;\n items: string[];\n};\n\nexport type PeekFilePathMode = 'concise' | 'full';\nexport type PeekOutputPresetName = 'agent' | 'human';\n\nexport type PeekOutputConfig = {\n deep: boolean;\n indent: boolean;\n filePaths: PeekFilePathMode;\n includeLinesCount: boolean;\n};\n\nexport const PEEK_OUTPUT_PRESETS: Record<PeekOutputPresetName, PeekOutputConfig> = {\n human: {\n deep: true,\n indent: true,\n filePaths: 'full',\n includeLinesCount: true,\n },\n agent: {\n deep: true,\n indent: false,\n filePaths: 'concise',\n includeLinesCount: true,\n },\n};\n\ntype FolderSummary = {\n path: string;\n files: FileSummary[];\n folders: FolderSummary[];\n omittedFiles: string[];\n};\n\nconst SKIP_DIRS = new Set([\n '.git',\n '.next',\n 'coverage',\n 'dist',\n 'dist-ssr',\n 'node_modules',\n]);\n\nconst SKIP_FILES = new Set([GENERATED_PEEK_FILE]);\n\nexport async function scanFolderPeek(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const repoRoot = await findRepoRoot(root);\n const outputConfig = resolveOutputConfig(options);\n const fileLimit = createLimiter(64);\n const summary = await collectFolderSummary(\n root,\n repoRoot,\n root,\n outputConfig.deep,\n outputConfig,\n fileLimit,\n );\n\n return `${renderFolderSummary(summary, outputConfig)}\\n`;\n}\n\nexport async function generateFolderPeek(\n folder: string,\n options: ScanOptions = {},\n): Promise<{ path: string; content: string }> {\n const root = path.resolve(folder);\n const content = await scanFolderPeek(root, options);\n const outPath = path.join(root, GENERATED_PEEK_FILE);\n await mkdir(root, { recursive: true });\n await writeFile(outPath, content, 'utf8');\n return { path: outPath, content };\n}\n\nexport async function peekFolder(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const generatedPath = path.join(root, GENERATED_PEEK_FILE);\n try {\n if (!options.deep && !hasOutputOverrides(options)) return await readFile(generatedPath, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n return (await generateFolderPeek(root, options)).content;\n}\n\nasync function collectFolderSummary(\n root: string,\n repoRoot: string,\n dir: string,\n deep: boolean,\n outputConfig: PeekOutputConfig,\n fileLimit: <T>(task: () => Promise<T>) => Promise<T>,\n): Promise<FolderSummary> {\n const entries = await readdir(dir, { withFileTypes: true });\n const fileTasks: Array<Promise<{ name: string; summary: FileSummary | null }>> = [];\n const folderTasks: Array<Promise<FolderSummary>> = [];\n const omittedFiles: string[] = [];\n const ignoredPaths = await checkIgnoredPaths(root, entries.map((entry) => path.join(dir, entry.name)));\n\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const fullPath = path.join(dir, entry.name);\n if (ignoredPaths.has(toPosix(path.relative(root, fullPath)))) continue;\n\n if (entry.isDirectory()) {\n if (!deep || shouldSkipDirectory(entry.name)) continue;\n folderTasks.push(collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig, fileLimit));\n continue;\n }\n if (!entry.isFile() || shouldSkipFile(entry.name)) continue;\n\n fileTasks.push(\n fileLimit(async () => ({\n name: entry.name,\n summary: await summarizeFile(root, repoRoot, fullPath, outputConfig),\n })),\n );\n }\n\n const fileResults = await Promise.all(fileTasks);\n const files = fileResults\n .map((result) => result.summary)\n .filter((summary): summary is FileSummary => summary !== null);\n omittedFiles.push(\n ...fileResults\n .filter((result) => result.summary === null)\n .map((result) => result.name),\n );\n const folders = await Promise.all(folderTasks);\n\n return {\n path: formatFolderPath(root, repoRoot, dir),\n files: files.sort((a, b) => a.path.localeCompare(b.path)),\n folders: folders.sort((a, b) => a.path.localeCompare(b.path)),\n omittedFiles: omittedFiles.sort((a, b) => a.localeCompare(b)),\n };\n}\n\nfunction formatFolderPath(root: string, repoRoot: string, dir: string): string {\n const base = isInsidePath(repoRoot, dir) ? repoRoot : root;\n const relativePath = toPosix(path.relative(base, dir));\n return relativePath || '.';\n}\n\nfunction renderFolderSummary(\n summary: FolderSummary,\n outputConfig: PeekOutputConfig,\n depth = 0,\n): string {\n const indent = outputConfig.indent ? '\\t'.repeat(Math.max(0, depth - 1)) : '';\n const childIndent = outputConfig.indent ? '\\t'.repeat(depth) : '';\n const lines = [`${indent}<folder path=\"${escapeAttribute(summary.path)}\">`];\n\n for (const file of summary.files) {\n const lineCount = outputConfig.includeLinesCount ? ` lines=\"${file.lines}\"` : '';\n lines.push(`${childIndent}<file path=\"${escapeAttribute(file.path)}\"${lineCount}>`);\n for (const item of file.items) {\n const prefix = item.startsWith('[ln ') ? '' : '- ';\n lines.push(`${childIndent}${prefix}${item}`);\n }\n lines.push(`${childIndent}</file>`);\n }\n\n for (const folder of summary.folders) {\n lines.push(renderFolderSummary(folder, outputConfig, depth + 1));\n }\n\n if (summary.omittedFiles.length > 0) {\n lines.push(`${childIndent}<omitted_files>`);\n for (const omittedFile of summary.omittedFiles) {\n lines.push(`${childIndent}- ${escapeText(omittedFile)}`);\n }\n lines.push(`${childIndent}</omitted_files>`);\n }\n\n lines.push(`${indent}</folder>`);\n return lines.join('\\n');\n}\n\nasync function summarizeFile(\n root: string,\n repoRoot: string,\n fullPath: string,\n outputConfig: PeekOutputConfig,\n): Promise<FileSummary | null> {\n const extension = path.extname(fullPath);\n const relativePath = formatFilePath(root, repoRoot, fullPath, outputConfig.filePaths);\n\n if (extension === '.md' || extension === '.mdx') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'markdown',\n lines: countLines(text),\n items: extractMarkdownHeadings(text),\n };\n }\n if (isTypeScriptLikeFile(extension, fullPath)) {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'typescript',\n lines: countLines(text),\n items: extractTypeScriptSymbols(text),\n };\n }\n if (extension === '.py') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'python',\n lines: countLines(text),\n items: extractPythonSymbols(text),\n };\n }\n return null;\n}\n\nfunction isTypeScriptLikeFile(extension: string, fullPath: string): boolean {\n return ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs'].includes(extension) && !fullPath.endsWith('.d.ts');\n}\n\nfunction extractMarkdownHeadings(text: string): string[] {\n return text\n .split(/\\r?\\n/)\n .map((line) => /^(#{1,6})\\s+(.+?)\\s*$/.exec(line))\n .filter((match): match is RegExpExecArray => match !== null)\n .map((match) => `h${match[1].length} ${match[2].replace(/\\s+#+$/, '').trim()}`);\n}\n\nfunction extractTypeScriptSymbols(text: string): string[] {\n const items: string[] = [];\n const source = stripBlockComments(text);\n const patterns: Array<[RegExp, string]> = [\n [/^(?:export\\s+)?(?:abstract\\s+)?class\\s+([A-Za-z_$][\\w$]*)/gm, 'class'],\n [/^(?:export\\s+)?interface\\s+([A-Za-z_$][\\w$]*)/gm, 'interface'],\n [/^(?:export\\s+)?type\\s+([A-Za-z_$][\\w$]*)\\s*=/gm, 'type'],\n [/^(?:export\\s+)?(?:declare\\s+)?(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)/gm, 'const'],\n [\n /^(?:export\\s+)?(?:async\\s+)?function\\s+([A-Za-z_$][\\w$]*)\\s*\\(/gm,\n 'function',\n ],\n ];\n\n for (const [pattern, label] of patterns) {\n for (const match of source.matchAll(pattern)) {\n const range = lineRangeForTypeScriptSymbol(source, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${label} ${match[1]}`);\n }\n }\n items.push(...extractTypeScriptTestCalls(source));\n return unique(items);\n}\n\nfunction extractTypeScriptTestCalls(text: string): string[] {\n const items: string[] = [];\n const pattern = /^\\s*(describe|test|it)\\s*\\(\\s*(['\"`])((?:\\\\.|(?!\\2)[\\s\\S])*?)\\2/gm;\n\n for (const match of text.matchAll(pattern)) {\n const callName = match[1];\n const quote = match[2];\n const title = match[3];\n const range = lineRangeForCallExpression(text, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${callName}(${quote}${title}${quote})`);\n }\n\n return items;\n}\n\nfunction extractPythonSymbols(text: string): string[] {\n const items: string[] = [];\n const lines = text.split(/\\r?\\n/);\n for (const [index, line] of lines.entries()) {\n const lineNumber = index + 1;\n let match = /^class\\s+([A-Za-z_]\\w*)\\s*[:(]/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} class ${match[1]}`);\n continue;\n }\n match = /^(?:async\\s+)?def\\s+([A-Za-z_]\\w*)\\s*\\(/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} function ${match[1]}`);\n continue;\n }\n match = /^([A-Z][A-Z0-9_]*)\\s*[:=]/.exec(line);\n if (match) items.push(`${formatLineRange({ start: lineNumber, end: lineNumber })} const ${match[1]}`);\n }\n return unique(items);\n}\n\nfunction shouldSkipDirectory(name: string): boolean {\n return SKIP_DIRS.has(name);\n}\n\nfunction shouldSkipFile(name: string): boolean {\n return SKIP_FILES.has(name) || name.endsWith('.map');\n}\n\nfunction stripBlockComments(text: string): string {\n return text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, (comment) =>\n comment.replace(/[^\\r\\n]/g, ' '),\n );\n}\n\nfunction unique(items: string[]): string[] {\n return Array.from(new Set(items));\n}\n\nfunction createLimiter(maxConcurrent: number): <T>(task: () => Promise<T>) => Promise<T> {\n let active = 0;\n const queue: Array<() => void> = [];\n\n return async function limit<T>(task: () => Promise<T>): Promise<T> {\n if (active >= maxConcurrent) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n\n active += 1;\n try {\n return await task();\n } finally {\n active -= 1;\n queue.shift()?.();\n }\n };\n}\n\nfunction lineNumberAt(text: string, index: number): number {\n let lineNumber = 1;\n for (let offset = 0; offset < index; offset += 1) {\n if (text.charCodeAt(offset) === 10) lineNumber += 1;\n }\n return lineNumber;\n}\n\nfunction lineRangeForTypeScriptSymbol(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const lineEnd = text.indexOf('\\n', index);\n const declarationEnd = lineEnd === -1 ? text.length : lineEnd;\n const openingBrace = text.indexOf('{', index);\n\n if (openingBrace !== -1 && openingBrace < declarationEnd) {\n const closingBrace = findMatchingBrace(text, openingBrace);\n if (closingBrace !== -1) return { start, end: lineNumberAt(text, closingBrace) };\n }\n\n const semicolon = text.indexOf(';', index);\n if (semicolon !== -1 && semicolon < declarationEnd) {\n return { start, end: lineNumberAt(text, semicolon) };\n }\n\n return { start, end: start };\n}\n\nfunction findMatchingBrace(text: string, openingBrace: number): number {\n let depth = 0;\n for (let index = openingBrace; index < text.length; index += 1) {\n const char = text[index];\n if (char === '{') depth += 1;\n if (char === '}') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForCallExpression(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const openingParen = text.indexOf('(', index);\n if (openingParen === -1) return { start, end: start };\n\n const closingParen = findMatchingParen(text, openingParen);\n if (closingParen === -1) return { start, end: start };\n return { start, end: lineNumberAt(text, closingParen) };\n}\n\nfunction findMatchingParen(text: string, openingParen: number): number {\n let depth = 0;\n let quote: string | null = null;\n\n for (let index = openingParen; index < text.length; index += 1) {\n const char = text[index];\n if (quote) {\n if (char === '\\\\') {\n index += 1;\n continue;\n }\n if (char === quote) quote = null;\n continue;\n }\n if (char === '\"' || char === \"'\" || char === '`') {\n quote = char;\n continue;\n }\n if (char === '(') depth += 1;\n if (char === ')') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForPythonBlock(\n lines: string[],\n startIndex: number,\n): { start: number; end: number } {\n const startIndent = indentationLength(lines[startIndex] ?? '');\n let endIndex = startIndex;\n\n for (let index = startIndex + 1; index < lines.length; index += 1) {\n const line = lines[index] ?? '';\n if (line.trim() === '') {\n endIndex = index;\n continue;\n }\n if (indentationLength(line) <= startIndent) break;\n endIndex = index;\n }\n\n while (endIndex > startIndex && (lines[endIndex] ?? '').trim() === '') endIndex -= 1;\n return { start: startIndex + 1, end: endIndex + 1 };\n}\n\nfunction indentationLength(line: string): number {\n return line.match(/^\\s*/)?.[0].length ?? 0;\n}\n\nfunction formatLineRange(range: { start: number; end: number }): string {\n return range.start === range.end ? `[ln ${range.start}]` : `[ln ${range.start}-${range.end}]`;\n}\n\nfunction formatFilePath(\n root: string,\n repoRoot: string,\n fullPath: string,\n filePaths: PeekFilePathMode,\n): string {\n if (filePaths === 'concise') return `/${path.basename(fullPath)}`;\n const base = isInsidePath(repoRoot, fullPath) ? repoRoot : root;\n return toPosix(path.relative(base, fullPath));\n}\n\nfunction resolveOutputConfig(options: ScanOptions): PeekOutputConfig {\n const presetName = options.preset ?? 'human';\n const preset = PEEK_OUTPUT_PRESETS[presetName];\n if (!preset) throw new Error(`Unknown peek output preset: ${presetName}`);\n\n return {\n deep: options.deep ?? preset.deep,\n indent: options.indent ?? preset.indent,\n filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths),\n includeLinesCount:\n options.includeLinesCount ?? options.include_lines_count ?? preset.includeLinesCount,\n };\n}\n\nfunction normalizeFilePathMode(value: PeekFilePathMode): PeekFilePathMode {\n if (value === 'concise' || value === 'full') return value;\n throw new Error(`Unknown peek file path mode: ${String(value)}`);\n}\n\nfunction countLines(text: string): number {\n if (text.length === 0) return 0;\n const newlineCount = text.match(/\\r\\n|\\r|\\n/g)?.length ?? 0;\n return newlineCount + (/(?:\\r\\n|\\r|\\n)$/.test(text) ? 0 : 1);\n}\n\nfunction hasOutputOverrides(options: ScanOptions): boolean {\n return (\n options.preset !== undefined ||\n options.indent !== undefined ||\n options.filePaths !== undefined ||\n options.file_paths !== undefined ||\n options.includeLinesCount !== undefined ||\n options.include_lines_count !== undefined\n );\n}\n\nfunction isInsidePath(parent: string, child: string): boolean {\n const relativePath = path.relative(parent, child);\n return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join('/');\n}\n\nfunction escapeAttribute(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;');\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/</g, '&lt;');\n}\n\nasync function checkIgnoredPaths(root: string, fullPaths: string[]): Promise<Set<string>> {\n const relativePaths = fullPaths\n .map((fullPath) => toPosix(path.relative(root, fullPath)))\n .filter((relativePath) => relativePath && !relativePath.startsWith('..'));\n\n if (relativePaths.length === 0) return new Set();\n\n return await new Promise((resolve) => {\n const child = spawn('git', ['-C', root, 'check-ignore', '--stdin'], {\n stdio: ['pipe', 'pipe', 'ignore'],\n });\n let stdout = '';\n\n child.stdout.setEncoding('utf8');\n child.stdout.on('data', (chunk) => {\n stdout += chunk;\n });\n child.on('error', () => resolve(new Set()));\n child.on('close', (code) => {\n if (code !== 0 && code !== 1) {\n resolve(new Set());\n return;\n }\n resolve(new Set(stdout.split(/\\r?\\n/).filter(Boolean)));\n });\n\n child.stdin.end(relativePaths.join('\\n'));\n });\n}\n\nasync function findRepoRoot(root: string): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', ['-C', root, 'rev-parse', '--show-toplevel']);\n return path.resolve(stdout.trim());\n } catch {\n return root;\n }\n}\n","import { defineCli } from '@davstack/cli-utils';\nimport { cliSpec } from './cli-spec.js';\n\nconst code = await defineCli(cliSpec).run(process.argv.slice(2));\nprocess.exit(code);\n","import type { CliSpec } from '@davstack/cli-utils';\nimport type { PeekFilePathMode, PeekOutputPresetName, ScanOptions } from './index.js';\n\nconst outputFlags = {\n human: {\n type: 'boolean',\n default: false,\n description: 'Use human-readable output defaults: indented XML and full repo-relative file paths',\n },\n agent: {\n type: 'boolean',\n default: false,\n description: 'Use token-optimized output defaults: no indentation and concise file paths',\n },\n indent: {\n type: 'boolean',\n description: 'Indent nested folder output',\n },\n file_paths: {\n type: 'string',\n values: ['concise', 'full'],\n description: 'File path style: concise or full',\n },\n 'include-lines-count': {\n type: 'boolean',\n description: 'Include total line counts on file tags',\n },\n} as const;\n\nfunction resolveCliScanOptions(flags: Record<string, unknown>): ScanOptions {\n const human = flags.human === true;\n const agent = flags.agent === true;\n if (human && agent) throw new Error('Use only one output preset: --human or --agent');\n\n const preset: PeekOutputPresetName | undefined = agent ? 'agent' : human ? 'human' : undefined;\n const filePaths = flags.file_paths;\n if (filePaths !== undefined && filePaths !== 'concise' && filePaths !== 'full') {\n throw new Error('--file_paths must be \"concise\" or \"full\"');\n }\n\n return {\n deep: flags.deep as boolean,\n preset,\n indent: flags.indent as boolean | undefined,\n filePaths: filePaths as PeekFilePathMode | undefined,\n includeLinesCount: flags['include-lines-count'] as boolean | undefined,\n };\n}\n\nexport const cliSpec: CliSpec = {\n name: 'peek',\n description: 'Print concise folder summaries for agents.',\n examples: [\n 'peek .',\n 'peek packages/context-compactor --agent',\n 'peek . --human',\n 'peek . --file_paths=full --no-include-lines-count',\n ],\n defaults: [\n '--human behavior: deep scan, indented output, full file paths, line counts',\n '--agent behavior: deep scan, no indentation, concise file paths, line counts',\n ],\n positionals: [{ name: 'path', required: true, description: 'Folder to peek at' }],\n flags: {\n deep: {\n type: 'boolean',\n description: 'Recursively include child folders',\n },\n ...outputFlags,\n },\n run: async (ctx) => {\n const { peekFolder } = await import('./index.js');\n console.log(await peekFolder(ctx.positionals[0], resolveCliScanOptions(ctx.flags)));\n },\n};\n"],"mappings":"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","names":["code","peekFolder"]}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { execFile } from "child_process";
2
+ import { execFile, spawn } from "child_process";
3
3
  import { mkdir, readdir, readFile, writeFile } from "fs/promises";
4
4
  import path from "path";
5
5
  import { promisify } from "util";
@@ -32,7 +32,15 @@ async function scanFolderPeek(folder, options = {}) {
32
32
  const root = path.resolve(folder);
33
33
  const repoRoot = await findRepoRoot(root);
34
34
  const outputConfig = resolveOutputConfig(options);
35
- const summary = await collectFolderSummary(root, repoRoot, root, outputConfig.deep, outputConfig);
35
+ const fileLimit = createLimiter(64);
36
+ const summary = await collectFolderSummary(
37
+ root,
38
+ repoRoot,
39
+ root,
40
+ outputConfig.deep,
41
+ outputConfig,
42
+ fileLimit
43
+ );
36
44
  return `${renderFolderSummary(summary, outputConfig)}
37
45
  `;
38
46
  }
@@ -54,27 +62,34 @@ async function peekFolder(folder, options = {}) {
54
62
  }
55
63
  return (await generateFolderPeek(root, options)).content;
56
64
  }
57
- async function collectFolderSummary(root, repoRoot, dir, deep, outputConfig) {
65
+ async function collectFolderSummary(root, repoRoot, dir, deep, outputConfig, fileLimit) {
58
66
  const entries = await readdir(dir, { withFileTypes: true });
59
- const files = [];
60
- const folders = [];
67
+ const fileTasks = [];
68
+ const folderTasks = [];
61
69
  const omittedFiles = [];
70
+ const ignoredPaths = await checkIgnoredPaths(root, entries.map((entry) => path.join(dir, entry.name)));
62
71
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
63
72
  const fullPath = path.join(dir, entry.name);
64
- if (await isGitIgnored(root, fullPath)) continue;
73
+ if (ignoredPaths.has(toPosix(path.relative(root, fullPath)))) continue;
65
74
  if (entry.isDirectory()) {
66
75
  if (!deep || shouldSkipDirectory(entry.name)) continue;
67
- folders.push(await collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig));
76
+ folderTasks.push(collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig, fileLimit));
68
77
  continue;
69
78
  }
70
79
  if (!entry.isFile() || shouldSkipFile(entry.name)) continue;
71
- const summary = await summarizeFile(root, repoRoot, fullPath, outputConfig);
72
- if (summary) {
73
- files.push(summary);
74
- } else {
75
- omittedFiles.push(entry.name);
76
- }
80
+ fileTasks.push(
81
+ fileLimit(async () => ({
82
+ name: entry.name,
83
+ summary: await summarizeFile(root, repoRoot, fullPath, outputConfig)
84
+ }))
85
+ );
77
86
  }
87
+ const fileResults = await Promise.all(fileTasks);
88
+ const files = fileResults.map((result) => result.summary).filter((summary) => summary !== null);
89
+ omittedFiles.push(
90
+ ...fileResults.filter((result) => result.summary === null).map((result) => result.name)
91
+ );
92
+ const folders = await Promise.all(folderTasks);
78
93
  return {
79
94
  path: formatFolderPath(root, repoRoot, dir),
80
95
  files: files.sort((a, b) => a.path.localeCompare(b.path)),
@@ -220,6 +235,22 @@ function stripBlockComments(text) {
220
235
  function unique(items) {
221
236
  return Array.from(new Set(items));
222
237
  }
238
+ function createLimiter(maxConcurrent) {
239
+ let active = 0;
240
+ const queue = [];
241
+ return async function limit(task) {
242
+ if (active >= maxConcurrent) {
243
+ await new Promise((resolve) => queue.push(resolve));
244
+ }
245
+ active += 1;
246
+ try {
247
+ return await task();
248
+ } finally {
249
+ active -= 1;
250
+ queue.shift()?.();
251
+ }
252
+ };
253
+ }
223
254
  function lineNumberAt(text, index) {
224
255
  let lineNumber = 1;
225
256
  for (let offset = 0; offset < index; offset += 1) {
@@ -349,17 +380,28 @@ function escapeAttribute(value) {
349
380
  function escapeText(value) {
350
381
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;");
351
382
  }
352
- async function isGitIgnored(root, fullPath) {
353
- const relativePath = toPosix(path.relative(root, fullPath));
354
- if (!relativePath || relativePath.startsWith("..")) return false;
355
- try {
356
- await execFileAsync("git", ["-C", root, "check-ignore", "--quiet", "--", relativePath]);
357
- return true;
358
- } catch (error) {
359
- const code = error.code;
360
- if (code === 1 || code === 128 || code === "ENOENT") return false;
361
- return false;
362
- }
383
+ async function checkIgnoredPaths(root, fullPaths) {
384
+ const relativePaths = fullPaths.map((fullPath) => toPosix(path.relative(root, fullPath))).filter((relativePath) => relativePath && !relativePath.startsWith(".."));
385
+ if (relativePaths.length === 0) return /* @__PURE__ */ new Set();
386
+ return await new Promise((resolve) => {
387
+ const child = spawn("git", ["-C", root, "check-ignore", "--stdin"], {
388
+ stdio: ["pipe", "pipe", "ignore"]
389
+ });
390
+ let stdout = "";
391
+ child.stdout.setEncoding("utf8");
392
+ child.stdout.on("data", (chunk) => {
393
+ stdout += chunk;
394
+ });
395
+ child.on("error", () => resolve(/* @__PURE__ */ new Set()));
396
+ child.on("close", (code) => {
397
+ if (code !== 0 && code !== 1) {
398
+ resolve(/* @__PURE__ */ new Set());
399
+ return;
400
+ }
401
+ resolve(new Set(stdout.split(/\r?\n/).filter(Boolean)));
402
+ });
403
+ child.stdin.end(relativePaths.join("\n"));
404
+ });
363
405
  }
364
406
  async function findRepoRoot(root) {
365
407
  try {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nexport const GENERATED_PEEK_FILE = '.folder-peek.generated.md';\nconst execFileAsync = promisify(execFile);\n\nexport type ScanOptions = {\n deep?: boolean;\n preset?: PeekOutputPresetName;\n indent?: boolean;\n filePaths?: PeekFilePathMode;\n file_paths?: PeekFilePathMode;\n includeLinesCount?: boolean;\n include_lines_count?: boolean;\n};\n\ntype FileSummary = {\n path: string;\n kind: 'markdown' | 'typescript' | 'python';\n lines: number;\n items: string[];\n};\n\nexport type PeekFilePathMode = 'concise' | 'full';\nexport type PeekOutputPresetName = 'agent' | 'human';\n\nexport type PeekOutputConfig = {\n deep: boolean;\n indent: boolean;\n filePaths: PeekFilePathMode;\n includeLinesCount: boolean;\n};\n\nexport const PEEK_OUTPUT_PRESETS: Record<PeekOutputPresetName, PeekOutputConfig> = {\n human: {\n deep: true,\n indent: true,\n filePaths: 'full',\n includeLinesCount: true,\n },\n agent: {\n deep: true,\n indent: false,\n filePaths: 'concise',\n includeLinesCount: true,\n },\n};\n\ntype FolderSummary = {\n path: string;\n files: FileSummary[];\n folders: FolderSummary[];\n omittedFiles: string[];\n};\n\nconst SKIP_DIRS = new Set([\n '.git',\n '.next',\n 'coverage',\n 'dist',\n 'dist-ssr',\n 'node_modules',\n]);\n\nconst SKIP_FILES = new Set([GENERATED_PEEK_FILE]);\n\nexport async function scanFolderPeek(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const repoRoot = await findRepoRoot(root);\n const outputConfig = resolveOutputConfig(options);\n const summary = await collectFolderSummary(root, repoRoot, root, outputConfig.deep, outputConfig);\n\n return `${renderFolderSummary(summary, outputConfig)}\\n`;\n}\n\nexport async function generateFolderPeek(\n folder: string,\n options: ScanOptions = {},\n): Promise<{ path: string; content: string }> {\n const root = path.resolve(folder);\n const content = await scanFolderPeek(root, options);\n const outPath = path.join(root, GENERATED_PEEK_FILE);\n await mkdir(root, { recursive: true });\n await writeFile(outPath, content, 'utf8');\n return { path: outPath, content };\n}\n\nexport async function peekFolder(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const generatedPath = path.join(root, GENERATED_PEEK_FILE);\n try {\n if (!options.deep && !hasOutputOverrides(options)) return await readFile(generatedPath, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n return (await generateFolderPeek(root, options)).content;\n}\n\nasync function collectFolderSummary(\n root: string,\n repoRoot: string,\n dir: string,\n deep: boolean,\n outputConfig: PeekOutputConfig,\n): Promise<FolderSummary> {\n const entries = await readdir(dir, { withFileTypes: true });\n const files: FileSummary[] = [];\n const folders: FolderSummary[] = [];\n const omittedFiles: string[] = [];\n\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const fullPath = path.join(dir, entry.name);\n if (await isGitIgnored(root, fullPath)) continue;\n\n if (entry.isDirectory()) {\n if (!deep || shouldSkipDirectory(entry.name)) continue;\n folders.push(await collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig));\n continue;\n }\n if (!entry.isFile() || shouldSkipFile(entry.name)) continue;\n\n const summary = await summarizeFile(root, repoRoot, fullPath, outputConfig);\n if (summary) {\n files.push(summary);\n } else {\n omittedFiles.push(entry.name);\n }\n }\n\n return {\n path: formatFolderPath(root, repoRoot, dir),\n files: files.sort((a, b) => a.path.localeCompare(b.path)),\n folders: folders.sort((a, b) => a.path.localeCompare(b.path)),\n omittedFiles: omittedFiles.sort((a, b) => a.localeCompare(b)),\n };\n}\n\nfunction formatFolderPath(root: string, repoRoot: string, dir: string): string {\n const base = isInsidePath(repoRoot, dir) ? repoRoot : root;\n const relativePath = toPosix(path.relative(base, dir));\n return relativePath || '.';\n}\n\nfunction renderFolderSummary(\n summary: FolderSummary,\n outputConfig: PeekOutputConfig,\n depth = 0,\n): string {\n const indent = outputConfig.indent ? '\\t'.repeat(Math.max(0, depth - 1)) : '';\n const childIndent = outputConfig.indent ? '\\t'.repeat(depth) : '';\n const lines = [`${indent}<folder path=\"${escapeAttribute(summary.path)}\">`];\n\n for (const file of summary.files) {\n const lineCount = outputConfig.includeLinesCount ? ` lines=\"${file.lines}\"` : '';\n lines.push(`${childIndent}<file path=\"${escapeAttribute(file.path)}\"${lineCount}>`);\n for (const item of file.items) {\n const prefix = item.startsWith('[ln ') ? '' : '- ';\n lines.push(`${childIndent}${prefix}${item}`);\n }\n lines.push(`${childIndent}</file>`);\n }\n\n for (const folder of summary.folders) {\n lines.push(renderFolderSummary(folder, outputConfig, depth + 1));\n }\n\n if (summary.omittedFiles.length > 0) {\n lines.push(`${childIndent}<omitted_files>`);\n for (const omittedFile of summary.omittedFiles) {\n lines.push(`${childIndent}- ${escapeText(omittedFile)}`);\n }\n lines.push(`${childIndent}</omitted_files>`);\n }\n\n lines.push(`${indent}</folder>`);\n return lines.join('\\n');\n}\n\nasync function summarizeFile(\n root: string,\n repoRoot: string,\n fullPath: string,\n outputConfig: PeekOutputConfig,\n): Promise<FileSummary | null> {\n const extension = path.extname(fullPath);\n const relativePath = formatFilePath(root, repoRoot, fullPath, outputConfig.filePaths);\n\n if (extension === '.md' || extension === '.mdx') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'markdown',\n lines: countLines(text),\n items: extractMarkdownHeadings(text),\n };\n }\n if (isTypeScriptLikeFile(extension, fullPath)) {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'typescript',\n lines: countLines(text),\n items: extractTypeScriptSymbols(text),\n };\n }\n if (extension === '.py') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'python',\n lines: countLines(text),\n items: extractPythonSymbols(text),\n };\n }\n return null;\n}\n\nfunction isTypeScriptLikeFile(extension: string, fullPath: string): boolean {\n return ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs'].includes(extension) && !fullPath.endsWith('.d.ts');\n}\n\nfunction extractMarkdownHeadings(text: string): string[] {\n return text\n .split(/\\r?\\n/)\n .map((line) => /^(#{1,6})\\s+(.+?)\\s*$/.exec(line))\n .filter((match): match is RegExpExecArray => match !== null)\n .map((match) => `h${match[1].length} ${match[2].replace(/\\s+#+$/, '').trim()}`);\n}\n\nfunction extractTypeScriptSymbols(text: string): string[] {\n const items: string[] = [];\n const source = stripBlockComments(text);\n const patterns: Array<[RegExp, string]> = [\n [/^(?:export\\s+)?(?:abstract\\s+)?class\\s+([A-Za-z_$][\\w$]*)/gm, 'class'],\n [/^(?:export\\s+)?interface\\s+([A-Za-z_$][\\w$]*)/gm, 'interface'],\n [/^(?:export\\s+)?type\\s+([A-Za-z_$][\\w$]*)\\s*=/gm, 'type'],\n [/^(?:export\\s+)?(?:declare\\s+)?(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)/gm, 'const'],\n [\n /^(?:export\\s+)?(?:async\\s+)?function\\s+([A-Za-z_$][\\w$]*)\\s*\\(/gm,\n 'function',\n ],\n ];\n\n for (const [pattern, label] of patterns) {\n for (const match of source.matchAll(pattern)) {\n const range = lineRangeForTypeScriptSymbol(source, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${label} ${match[1]}`);\n }\n }\n items.push(...extractTypeScriptTestCalls(source));\n return unique(items);\n}\n\nfunction extractTypeScriptTestCalls(text: string): string[] {\n const items: string[] = [];\n const pattern = /^\\s*(describe|test|it)\\s*\\(\\s*(['\"`])((?:\\\\.|(?!\\2)[\\s\\S])*?)\\2/gm;\n\n for (const match of text.matchAll(pattern)) {\n const callName = match[1];\n const quote = match[2];\n const title = match[3];\n const range = lineRangeForCallExpression(text, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${callName}(${quote}${title}${quote})`);\n }\n\n return items;\n}\n\nfunction extractPythonSymbols(text: string): string[] {\n const items: string[] = [];\n const lines = text.split(/\\r?\\n/);\n for (const [index, line] of lines.entries()) {\n const lineNumber = index + 1;\n let match = /^class\\s+([A-Za-z_]\\w*)\\s*[:(]/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} class ${match[1]}`);\n continue;\n }\n match = /^(?:async\\s+)?def\\s+([A-Za-z_]\\w*)\\s*\\(/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} function ${match[1]}`);\n continue;\n }\n match = /^([A-Z][A-Z0-9_]*)\\s*[:=]/.exec(line);\n if (match) items.push(`${formatLineRange({ start: lineNumber, end: lineNumber })} const ${match[1]}`);\n }\n return unique(items);\n}\n\nfunction shouldSkipDirectory(name: string): boolean {\n return SKIP_DIRS.has(name);\n}\n\nfunction shouldSkipFile(name: string): boolean {\n return SKIP_FILES.has(name) || name.endsWith('.map');\n}\n\nfunction stripBlockComments(text: string): string {\n return text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, (comment) =>\n comment.replace(/[^\\r\\n]/g, ' '),\n );\n}\n\nfunction unique(items: string[]): string[] {\n return Array.from(new Set(items));\n}\n\nfunction lineNumberAt(text: string, index: number): number {\n let lineNumber = 1;\n for (let offset = 0; offset < index; offset += 1) {\n if (text.charCodeAt(offset) === 10) lineNumber += 1;\n }\n return lineNumber;\n}\n\nfunction lineRangeForTypeScriptSymbol(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const lineEnd = text.indexOf('\\n', index);\n const declarationEnd = lineEnd === -1 ? text.length : lineEnd;\n const openingBrace = text.indexOf('{', index);\n\n if (openingBrace !== -1 && openingBrace < declarationEnd) {\n const closingBrace = findMatchingBrace(text, openingBrace);\n if (closingBrace !== -1) return { start, end: lineNumberAt(text, closingBrace) };\n }\n\n const semicolon = text.indexOf(';', index);\n if (semicolon !== -1 && semicolon < declarationEnd) {\n return { start, end: lineNumberAt(text, semicolon) };\n }\n\n return { start, end: start };\n}\n\nfunction findMatchingBrace(text: string, openingBrace: number): number {\n let depth = 0;\n for (let index = openingBrace; index < text.length; index += 1) {\n const char = text[index];\n if (char === '{') depth += 1;\n if (char === '}') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForCallExpression(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const openingParen = text.indexOf('(', index);\n if (openingParen === -1) return { start, end: start };\n\n const closingParen = findMatchingParen(text, openingParen);\n if (closingParen === -1) return { start, end: start };\n return { start, end: lineNumberAt(text, closingParen) };\n}\n\nfunction findMatchingParen(text: string, openingParen: number): number {\n let depth = 0;\n let quote: string | null = null;\n\n for (let index = openingParen; index < text.length; index += 1) {\n const char = text[index];\n if (quote) {\n if (char === '\\\\') {\n index += 1;\n continue;\n }\n if (char === quote) quote = null;\n continue;\n }\n if (char === '\"' || char === \"'\" || char === '`') {\n quote = char;\n continue;\n }\n if (char === '(') depth += 1;\n if (char === ')') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForPythonBlock(\n lines: string[],\n startIndex: number,\n): { start: number; end: number } {\n const startIndent = indentationLength(lines[startIndex] ?? '');\n let endIndex = startIndex;\n\n for (let index = startIndex + 1; index < lines.length; index += 1) {\n const line = lines[index] ?? '';\n if (line.trim() === '') {\n endIndex = index;\n continue;\n }\n if (indentationLength(line) <= startIndent) break;\n endIndex = index;\n }\n\n while (endIndex > startIndex && (lines[endIndex] ?? '').trim() === '') endIndex -= 1;\n return { start: startIndex + 1, end: endIndex + 1 };\n}\n\nfunction indentationLength(line: string): number {\n return line.match(/^\\s*/)?.[0].length ?? 0;\n}\n\nfunction formatLineRange(range: { start: number; end: number }): string {\n return range.start === range.end ? `[ln ${range.start}]` : `[ln ${range.start}-${range.end}]`;\n}\n\nfunction formatFilePath(\n root: string,\n repoRoot: string,\n fullPath: string,\n filePaths: PeekFilePathMode,\n): string {\n if (filePaths === 'concise') return `/${path.basename(fullPath)}`;\n const base = isInsidePath(repoRoot, fullPath) ? repoRoot : root;\n return toPosix(path.relative(base, fullPath));\n}\n\nfunction resolveOutputConfig(options: ScanOptions): PeekOutputConfig {\n const presetName = options.preset ?? 'human';\n const preset = PEEK_OUTPUT_PRESETS[presetName];\n if (!preset) throw new Error(`Unknown peek output preset: ${presetName}`);\n\n return {\n deep: options.deep ?? preset.deep,\n indent: options.indent ?? preset.indent,\n filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths),\n includeLinesCount:\n options.includeLinesCount ?? options.include_lines_count ?? preset.includeLinesCount,\n };\n}\n\nfunction normalizeFilePathMode(value: PeekFilePathMode): PeekFilePathMode {\n if (value === 'concise' || value === 'full') return value;\n throw new Error(`Unknown peek file path mode: ${String(value)}`);\n}\n\nfunction countLines(text: string): number {\n if (text.length === 0) return 0;\n const newlineCount = text.match(/\\r\\n|\\r|\\n/g)?.length ?? 0;\n return newlineCount + (/(?:\\r\\n|\\r|\\n)$/.test(text) ? 0 : 1);\n}\n\nfunction hasOutputOverrides(options: ScanOptions): boolean {\n return (\n options.preset !== undefined ||\n options.indent !== undefined ||\n options.filePaths !== undefined ||\n options.file_paths !== undefined ||\n options.includeLinesCount !== undefined ||\n options.include_lines_count !== undefined\n );\n}\n\nfunction isInsidePath(parent: string, child: string): boolean {\n const relativePath = path.relative(parent, child);\n return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join('/');\n}\n\nfunction escapeAttribute(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;');\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/</g, '&lt;');\n}\n\nasync function isGitIgnored(root: string, fullPath: string): Promise<boolean> {\n const relativePath = toPosix(path.relative(root, fullPath));\n if (!relativePath || relativePath.startsWith('..')) return false;\n\n try {\n await execFileAsync('git', ['-C', root, 'check-ignore', '--quiet', '--', relativePath]);\n return true;\n } catch (error) {\n const code = (error as { code?: number | string }).code;\n if (code === 1 || code === 128 || code === 'ENOENT') return false;\n return false;\n }\n}\n\nasync function findRepoRoot(root: string): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', ['-C', root, 'rev-parse', '--show-toplevel']);\n return path.resolve(stdout.trim());\n } catch {\n return root;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { execFile, spawn } from 'node:child_process';\nimport { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nexport const GENERATED_PEEK_FILE = '.folder-peek.generated.md';\nconst execFileAsync = promisify(execFile);\n\nexport type ScanOptions = {\n deep?: boolean;\n preset?: PeekOutputPresetName;\n indent?: boolean;\n filePaths?: PeekFilePathMode;\n file_paths?: PeekFilePathMode;\n includeLinesCount?: boolean;\n include_lines_count?: boolean;\n};\n\ntype FileSummary = {\n path: string;\n kind: 'markdown' | 'typescript' | 'python';\n lines: number;\n items: string[];\n};\n\nexport type PeekFilePathMode = 'concise' | 'full';\nexport type PeekOutputPresetName = 'agent' | 'human';\n\nexport type PeekOutputConfig = {\n deep: boolean;\n indent: boolean;\n filePaths: PeekFilePathMode;\n includeLinesCount: boolean;\n};\n\nexport const PEEK_OUTPUT_PRESETS: Record<PeekOutputPresetName, PeekOutputConfig> = {\n human: {\n deep: true,\n indent: true,\n filePaths: 'full',\n includeLinesCount: true,\n },\n agent: {\n deep: true,\n indent: false,\n filePaths: 'concise',\n includeLinesCount: true,\n },\n};\n\ntype FolderSummary = {\n path: string;\n files: FileSummary[];\n folders: FolderSummary[];\n omittedFiles: string[];\n};\n\nconst SKIP_DIRS = new Set([\n '.git',\n '.next',\n 'coverage',\n 'dist',\n 'dist-ssr',\n 'node_modules',\n]);\n\nconst SKIP_FILES = new Set([GENERATED_PEEK_FILE]);\n\nexport async function scanFolderPeek(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const repoRoot = await findRepoRoot(root);\n const outputConfig = resolveOutputConfig(options);\n const fileLimit = createLimiter(64);\n const summary = await collectFolderSummary(\n root,\n repoRoot,\n root,\n outputConfig.deep,\n outputConfig,\n fileLimit,\n );\n\n return `${renderFolderSummary(summary, outputConfig)}\\n`;\n}\n\nexport async function generateFolderPeek(\n folder: string,\n options: ScanOptions = {},\n): Promise<{ path: string; content: string }> {\n const root = path.resolve(folder);\n const content = await scanFolderPeek(root, options);\n const outPath = path.join(root, GENERATED_PEEK_FILE);\n await mkdir(root, { recursive: true });\n await writeFile(outPath, content, 'utf8');\n return { path: outPath, content };\n}\n\nexport async function peekFolder(folder: string, options: ScanOptions = {}): Promise<string> {\n const root = path.resolve(folder);\n const generatedPath = path.join(root, GENERATED_PEEK_FILE);\n try {\n if (!options.deep && !hasOutputOverrides(options)) return await readFile(generatedPath, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n return (await generateFolderPeek(root, options)).content;\n}\n\nasync function collectFolderSummary(\n root: string,\n repoRoot: string,\n dir: string,\n deep: boolean,\n outputConfig: PeekOutputConfig,\n fileLimit: <T>(task: () => Promise<T>) => Promise<T>,\n): Promise<FolderSummary> {\n const entries = await readdir(dir, { withFileTypes: true });\n const fileTasks: Array<Promise<{ name: string; summary: FileSummary | null }>> = [];\n const folderTasks: Array<Promise<FolderSummary>> = [];\n const omittedFiles: string[] = [];\n const ignoredPaths = await checkIgnoredPaths(root, entries.map((entry) => path.join(dir, entry.name)));\n\n for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n const fullPath = path.join(dir, entry.name);\n if (ignoredPaths.has(toPosix(path.relative(root, fullPath)))) continue;\n\n if (entry.isDirectory()) {\n if (!deep || shouldSkipDirectory(entry.name)) continue;\n folderTasks.push(collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig, fileLimit));\n continue;\n }\n if (!entry.isFile() || shouldSkipFile(entry.name)) continue;\n\n fileTasks.push(\n fileLimit(async () => ({\n name: entry.name,\n summary: await summarizeFile(root, repoRoot, fullPath, outputConfig),\n })),\n );\n }\n\n const fileResults = await Promise.all(fileTasks);\n const files = fileResults\n .map((result) => result.summary)\n .filter((summary): summary is FileSummary => summary !== null);\n omittedFiles.push(\n ...fileResults\n .filter((result) => result.summary === null)\n .map((result) => result.name),\n );\n const folders = await Promise.all(folderTasks);\n\n return {\n path: formatFolderPath(root, repoRoot, dir),\n files: files.sort((a, b) => a.path.localeCompare(b.path)),\n folders: folders.sort((a, b) => a.path.localeCompare(b.path)),\n omittedFiles: omittedFiles.sort((a, b) => a.localeCompare(b)),\n };\n}\n\nfunction formatFolderPath(root: string, repoRoot: string, dir: string): string {\n const base = isInsidePath(repoRoot, dir) ? repoRoot : root;\n const relativePath = toPosix(path.relative(base, dir));\n return relativePath || '.';\n}\n\nfunction renderFolderSummary(\n summary: FolderSummary,\n outputConfig: PeekOutputConfig,\n depth = 0,\n): string {\n const indent = outputConfig.indent ? '\\t'.repeat(Math.max(0, depth - 1)) : '';\n const childIndent = outputConfig.indent ? '\\t'.repeat(depth) : '';\n const lines = [`${indent}<folder path=\"${escapeAttribute(summary.path)}\">`];\n\n for (const file of summary.files) {\n const lineCount = outputConfig.includeLinesCount ? ` lines=\"${file.lines}\"` : '';\n lines.push(`${childIndent}<file path=\"${escapeAttribute(file.path)}\"${lineCount}>`);\n for (const item of file.items) {\n const prefix = item.startsWith('[ln ') ? '' : '- ';\n lines.push(`${childIndent}${prefix}${item}`);\n }\n lines.push(`${childIndent}</file>`);\n }\n\n for (const folder of summary.folders) {\n lines.push(renderFolderSummary(folder, outputConfig, depth + 1));\n }\n\n if (summary.omittedFiles.length > 0) {\n lines.push(`${childIndent}<omitted_files>`);\n for (const omittedFile of summary.omittedFiles) {\n lines.push(`${childIndent}- ${escapeText(omittedFile)}`);\n }\n lines.push(`${childIndent}</omitted_files>`);\n }\n\n lines.push(`${indent}</folder>`);\n return lines.join('\\n');\n}\n\nasync function summarizeFile(\n root: string,\n repoRoot: string,\n fullPath: string,\n outputConfig: PeekOutputConfig,\n): Promise<FileSummary | null> {\n const extension = path.extname(fullPath);\n const relativePath = formatFilePath(root, repoRoot, fullPath, outputConfig.filePaths);\n\n if (extension === '.md' || extension === '.mdx') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'markdown',\n lines: countLines(text),\n items: extractMarkdownHeadings(text),\n };\n }\n if (isTypeScriptLikeFile(extension, fullPath)) {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'typescript',\n lines: countLines(text),\n items: extractTypeScriptSymbols(text),\n };\n }\n if (extension === '.py') {\n const text = await readFile(fullPath, 'utf8');\n return {\n path: relativePath,\n kind: 'python',\n lines: countLines(text),\n items: extractPythonSymbols(text),\n };\n }\n return null;\n}\n\nfunction isTypeScriptLikeFile(extension: string, fullPath: string): boolean {\n return ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs'].includes(extension) && !fullPath.endsWith('.d.ts');\n}\n\nfunction extractMarkdownHeadings(text: string): string[] {\n return text\n .split(/\\r?\\n/)\n .map((line) => /^(#{1,6})\\s+(.+?)\\s*$/.exec(line))\n .filter((match): match is RegExpExecArray => match !== null)\n .map((match) => `h${match[1].length} ${match[2].replace(/\\s+#+$/, '').trim()}`);\n}\n\nfunction extractTypeScriptSymbols(text: string): string[] {\n const items: string[] = [];\n const source = stripBlockComments(text);\n const patterns: Array<[RegExp, string]> = [\n [/^(?:export\\s+)?(?:abstract\\s+)?class\\s+([A-Za-z_$][\\w$]*)/gm, 'class'],\n [/^(?:export\\s+)?interface\\s+([A-Za-z_$][\\w$]*)/gm, 'interface'],\n [/^(?:export\\s+)?type\\s+([A-Za-z_$][\\w$]*)\\s*=/gm, 'type'],\n [/^(?:export\\s+)?(?:declare\\s+)?(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)/gm, 'const'],\n [\n /^(?:export\\s+)?(?:async\\s+)?function\\s+([A-Za-z_$][\\w$]*)\\s*\\(/gm,\n 'function',\n ],\n ];\n\n for (const [pattern, label] of patterns) {\n for (const match of source.matchAll(pattern)) {\n const range = lineRangeForTypeScriptSymbol(source, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${label} ${match[1]}`);\n }\n }\n items.push(...extractTypeScriptTestCalls(source));\n return unique(items);\n}\n\nfunction extractTypeScriptTestCalls(text: string): string[] {\n const items: string[] = [];\n const pattern = /^\\s*(describe|test|it)\\s*\\(\\s*(['\"`])((?:\\\\.|(?!\\2)[\\s\\S])*?)\\2/gm;\n\n for (const match of text.matchAll(pattern)) {\n const callName = match[1];\n const quote = match[2];\n const title = match[3];\n const range = lineRangeForCallExpression(text, match.index ?? 0);\n items.push(`${formatLineRange(range)} ${callName}(${quote}${title}${quote})`);\n }\n\n return items;\n}\n\nfunction extractPythonSymbols(text: string): string[] {\n const items: string[] = [];\n const lines = text.split(/\\r?\\n/);\n for (const [index, line] of lines.entries()) {\n const lineNumber = index + 1;\n let match = /^class\\s+([A-Za-z_]\\w*)\\s*[:(]/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} class ${match[1]}`);\n continue;\n }\n match = /^(?:async\\s+)?def\\s+([A-Za-z_]\\w*)\\s*\\(/.exec(line);\n if (match) {\n items.push(`${formatLineRange(lineRangeForPythonBlock(lines, index))} function ${match[1]}`);\n continue;\n }\n match = /^([A-Z][A-Z0-9_]*)\\s*[:=]/.exec(line);\n if (match) items.push(`${formatLineRange({ start: lineNumber, end: lineNumber })} const ${match[1]}`);\n }\n return unique(items);\n}\n\nfunction shouldSkipDirectory(name: string): boolean {\n return SKIP_DIRS.has(name);\n}\n\nfunction shouldSkipFile(name: string): boolean {\n return SKIP_FILES.has(name) || name.endsWith('.map');\n}\n\nfunction stripBlockComments(text: string): string {\n return text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, (comment) =>\n comment.replace(/[^\\r\\n]/g, ' '),\n );\n}\n\nfunction unique(items: string[]): string[] {\n return Array.from(new Set(items));\n}\n\nfunction createLimiter(maxConcurrent: number): <T>(task: () => Promise<T>) => Promise<T> {\n let active = 0;\n const queue: Array<() => void> = [];\n\n return async function limit<T>(task: () => Promise<T>): Promise<T> {\n if (active >= maxConcurrent) {\n await new Promise<void>((resolve) => queue.push(resolve));\n }\n\n active += 1;\n try {\n return await task();\n } finally {\n active -= 1;\n queue.shift()?.();\n }\n };\n}\n\nfunction lineNumberAt(text: string, index: number): number {\n let lineNumber = 1;\n for (let offset = 0; offset < index; offset += 1) {\n if (text.charCodeAt(offset) === 10) lineNumber += 1;\n }\n return lineNumber;\n}\n\nfunction lineRangeForTypeScriptSymbol(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const lineEnd = text.indexOf('\\n', index);\n const declarationEnd = lineEnd === -1 ? text.length : lineEnd;\n const openingBrace = text.indexOf('{', index);\n\n if (openingBrace !== -1 && openingBrace < declarationEnd) {\n const closingBrace = findMatchingBrace(text, openingBrace);\n if (closingBrace !== -1) return { start, end: lineNumberAt(text, closingBrace) };\n }\n\n const semicolon = text.indexOf(';', index);\n if (semicolon !== -1 && semicolon < declarationEnd) {\n return { start, end: lineNumberAt(text, semicolon) };\n }\n\n return { start, end: start };\n}\n\nfunction findMatchingBrace(text: string, openingBrace: number): number {\n let depth = 0;\n for (let index = openingBrace; index < text.length; index += 1) {\n const char = text[index];\n if (char === '{') depth += 1;\n if (char === '}') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForCallExpression(text: string, index: number): { start: number; end: number } {\n const start = lineNumberAt(text, index);\n const openingParen = text.indexOf('(', index);\n if (openingParen === -1) return { start, end: start };\n\n const closingParen = findMatchingParen(text, openingParen);\n if (closingParen === -1) return { start, end: start };\n return { start, end: lineNumberAt(text, closingParen) };\n}\n\nfunction findMatchingParen(text: string, openingParen: number): number {\n let depth = 0;\n let quote: string | null = null;\n\n for (let index = openingParen; index < text.length; index += 1) {\n const char = text[index];\n if (quote) {\n if (char === '\\\\') {\n index += 1;\n continue;\n }\n if (char === quote) quote = null;\n continue;\n }\n if (char === '\"' || char === \"'\" || char === '`') {\n quote = char;\n continue;\n }\n if (char === '(') depth += 1;\n if (char === ')') {\n depth -= 1;\n if (depth === 0) return index;\n }\n }\n return -1;\n}\n\nfunction lineRangeForPythonBlock(\n lines: string[],\n startIndex: number,\n): { start: number; end: number } {\n const startIndent = indentationLength(lines[startIndex] ?? '');\n let endIndex = startIndex;\n\n for (let index = startIndex + 1; index < lines.length; index += 1) {\n const line = lines[index] ?? '';\n if (line.trim() === '') {\n endIndex = index;\n continue;\n }\n if (indentationLength(line) <= startIndent) break;\n endIndex = index;\n }\n\n while (endIndex > startIndex && (lines[endIndex] ?? '').trim() === '') endIndex -= 1;\n return { start: startIndex + 1, end: endIndex + 1 };\n}\n\nfunction indentationLength(line: string): number {\n return line.match(/^\\s*/)?.[0].length ?? 0;\n}\n\nfunction formatLineRange(range: { start: number; end: number }): string {\n return range.start === range.end ? `[ln ${range.start}]` : `[ln ${range.start}-${range.end}]`;\n}\n\nfunction formatFilePath(\n root: string,\n repoRoot: string,\n fullPath: string,\n filePaths: PeekFilePathMode,\n): string {\n if (filePaths === 'concise') return `/${path.basename(fullPath)}`;\n const base = isInsidePath(repoRoot, fullPath) ? repoRoot : root;\n return toPosix(path.relative(base, fullPath));\n}\n\nfunction resolveOutputConfig(options: ScanOptions): PeekOutputConfig {\n const presetName = options.preset ?? 'human';\n const preset = PEEK_OUTPUT_PRESETS[presetName];\n if (!preset) throw new Error(`Unknown peek output preset: ${presetName}`);\n\n return {\n deep: options.deep ?? preset.deep,\n indent: options.indent ?? preset.indent,\n filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths),\n includeLinesCount:\n options.includeLinesCount ?? options.include_lines_count ?? preset.includeLinesCount,\n };\n}\n\nfunction normalizeFilePathMode(value: PeekFilePathMode): PeekFilePathMode {\n if (value === 'concise' || value === 'full') return value;\n throw new Error(`Unknown peek file path mode: ${String(value)}`);\n}\n\nfunction countLines(text: string): number {\n if (text.length === 0) return 0;\n const newlineCount = text.match(/\\r\\n|\\r|\\n/g)?.length ?? 0;\n return newlineCount + (/(?:\\r\\n|\\r|\\n)$/.test(text) ? 0 : 1);\n}\n\nfunction hasOutputOverrides(options: ScanOptions): boolean {\n return (\n options.preset !== undefined ||\n options.indent !== undefined ||\n options.filePaths !== undefined ||\n options.file_paths !== undefined ||\n options.includeLinesCount !== undefined ||\n options.include_lines_count !== undefined\n );\n}\n\nfunction isInsidePath(parent: string, child: string): boolean {\n const relativePath = path.relative(parent, child);\n return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join('/');\n}\n\nfunction escapeAttribute(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;');\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/</g, '&lt;');\n}\n\nasync function checkIgnoredPaths(root: string, fullPaths: string[]): Promise<Set<string>> {\n const relativePaths = fullPaths\n .map((fullPath) => toPosix(path.relative(root, fullPath)))\n .filter((relativePath) => relativePath && !relativePath.startsWith('..'));\n\n if (relativePaths.length === 0) return new Set();\n\n return await new Promise((resolve) => {\n const child = spawn('git', ['-C', root, 'check-ignore', '--stdin'], {\n stdio: ['pipe', 'pipe', 'ignore'],\n });\n let stdout = '';\n\n child.stdout.setEncoding('utf8');\n child.stdout.on('data', (chunk) => {\n stdout += chunk;\n });\n child.on('error', () => resolve(new Set()));\n child.on('close', (code) => {\n if (code !== 0 && code !== 1) {\n resolve(new Set());\n return;\n }\n resolve(new Set(stdout.split(/\\r?\\n/).filter(Boolean)));\n });\n\n child.stdin.end(relativePaths.join('\\n'));\n });\n}\n\nasync function findRepoRoot(root: string): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', ['-C', root, 'rev-parse', '--show-toplevel']);\n return path.resolve(stdout.trim());\n } catch {\n return root;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davstack/peek",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Peek at concise, agent-friendly folder summaries.",