@davstack/peek 0.1.0 → 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.
File without changes
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)),
@@ -85,7 +100,8 @@ function renderFolderSummary(summary, outputConfig, depth = 0) {
85
100
  const childIndent = outputConfig.indent ? " ".repeat(depth) : "";
86
101
  const lines = [`${indent}<folder path="${escapeAttribute(summary.path)}">`];
87
102
  for (const file of summary.files) {
88
- lines.push(`${childIndent}<file path="${escapeAttribute(file.path)}">`);
103
+ const lineCount = outputConfig.includeLinesCount ? ` lines="${file.lines}"` : "";
104
+ lines.push(`${childIndent}<file path="${escapeAttribute(file.path)}"${lineCount}>`);
89
105
  for (const item of file.items) {
90
106
  const prefix = item.startsWith("[ln ") ? "" : "- ";
91
107
  lines.push(`${childIndent}${prefix}${item}`);
@@ -110,15 +126,30 @@ async function summarizeFile(root, repoRoot, fullPath, outputConfig) {
110
126
  const relativePath = formatFilePath(root, repoRoot, fullPath, outputConfig.filePaths);
111
127
  if (extension === ".md" || extension === ".mdx") {
112
128
  const text = await readFile(fullPath, "utf8");
113
- return { path: relativePath, kind: "markdown", items: extractMarkdownHeadings(text) };
129
+ return {
130
+ path: relativePath,
131
+ kind: "markdown",
132
+ lines: countLines(text),
133
+ items: extractMarkdownHeadings(text)
134
+ };
114
135
  }
115
136
  if (isTypeScriptLikeFile(extension, fullPath)) {
116
137
  const text = await readFile(fullPath, "utf8");
117
- return { path: relativePath, kind: "typescript", items: extractTypeScriptSymbols(text) };
138
+ return {
139
+ path: relativePath,
140
+ kind: "typescript",
141
+ lines: countLines(text),
142
+ items: extractTypeScriptSymbols(text)
143
+ };
118
144
  }
119
145
  if (extension === ".py") {
120
146
  const text = await readFile(fullPath, "utf8");
121
- return { path: relativePath, kind: "python", items: extractPythonSymbols(text) };
147
+ return {
148
+ path: relativePath,
149
+ kind: "python",
150
+ lines: countLines(text),
151
+ items: extractPythonSymbols(text)
152
+ };
122
153
  }
123
154
  return null;
124
155
  }
@@ -197,6 +228,22 @@ function stripBlockComments(text) {
197
228
  function unique(items) {
198
229
  return Array.from(new Set(items));
199
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
+ }
200
247
  function lineNumberAt(text, index) {
201
248
  let lineNumber = 1;
202
249
  for (let offset = 0; offset < index; offset += 1) {
@@ -297,15 +344,21 @@ function resolveOutputConfig(options) {
297
344
  return {
298
345
  deep: options.deep ?? preset.deep,
299
346
  indent: options.indent ?? preset.indent,
300
- filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths)
347
+ filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths),
348
+ includeLinesCount: options.includeLinesCount ?? options.include_lines_count ?? preset.includeLinesCount
301
349
  };
302
350
  }
303
351
  function normalizeFilePathMode(value) {
304
352
  if (value === "concise" || value === "full") return value;
305
353
  throw new Error(`Unknown peek file path mode: ${String(value)}`);
306
354
  }
355
+ function countLines(text) {
356
+ if (text.length === 0) return 0;
357
+ const newlineCount = text.match(/\r\n|\r|\n/g)?.length ?? 0;
358
+ return newlineCount + (/(?:\r\n|\r|\n)$/.test(text) ? 0 : 1);
359
+ }
307
360
  function hasOutputOverrides(options) {
308
- return options.preset !== void 0 || options.indent !== void 0 || options.filePaths !== void 0 || options.file_paths !== void 0;
361
+ return options.preset !== void 0 || options.indent !== void 0 || options.filePaths !== void 0 || options.file_paths !== void 0 || options.includeLinesCount !== void 0 || options.include_lines_count !== void 0;
309
362
  }
310
363
  function isInsidePath(parent, child) {
311
364
  const relativePath = path.relative(parent, child);
@@ -320,17 +373,28 @@ function escapeAttribute(value) {
320
373
  function escapeText(value) {
321
374
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;");
322
375
  }
323
- async function isGitIgnored(root, fullPath) {
324
- const relativePath = toPosix(path.relative(root, fullPath));
325
- if (!relativePath || relativePath.startsWith("..")) return false;
326
- try {
327
- await execFileAsync("git", ["-C", root, "check-ignore", "--quiet", "--", relativePath]);
328
- return true;
329
- } catch (error) {
330
- const code = error.code;
331
- if (code === 1 || code === 128 || code === "ENOENT") return false;
332
- return false;
333
- }
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
+ });
334
398
  }
335
399
  async function findRepoRoot(root) {
336
400
  try {
@@ -350,12 +414,14 @@ var init_src = __esm({
350
414
  human: {
351
415
  deep: true,
352
416
  indent: true,
353
- filePaths: "full"
417
+ filePaths: "full",
418
+ includeLinesCount: true
354
419
  },
355
420
  agent: {
356
421
  deep: true,
357
422
  indent: false,
358
- filePaths: "concise"
423
+ filePaths: "concise",
424
+ includeLinesCount: true
359
425
  }
360
426
  };
361
427
  SKIP_DIRS = /* @__PURE__ */ new Set([
@@ -388,7 +454,12 @@ var outputFlags = {
388
454
  },
389
455
  file_paths: {
390
456
  type: "string",
457
+ values: ["concise", "full"],
391
458
  description: "File path style: concise or full"
459
+ },
460
+ "include-lines-count": {
461
+ type: "boolean",
462
+ description: "Include total line counts on file tags"
392
463
  }
393
464
  };
394
465
  function resolveCliScanOptions(flags) {
@@ -404,12 +475,23 @@ function resolveCliScanOptions(flags) {
404
475
  deep: flags.deep,
405
476
  preset,
406
477
  indent: flags.indent,
407
- filePaths
478
+ filePaths,
479
+ includeLinesCount: flags["include-lines-count"]
408
480
  };
409
481
  }
410
482
  var cliSpec = {
411
483
  name: "peek",
412
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
+ ],
413
495
  positionals: [{ name: "path", required: true, description: "Folder to peek at" }],
414
496
  flags: {
415
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};\n\ntype FileSummary = {\n path: string;\n kind: 'markdown' | 'typescript' | 'python';\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};\n\nexport const PEEK_OUTPUT_PRESETS: Record<PeekOutputPresetName, PeekOutputConfig> = {\n human: {\n deep: true,\n indent: true,\n filePaths: 'full',\n },\n agent: {\n deep: true,\n indent: false,\n filePaths: 'concise',\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 lines.push(`${childIndent}<file path=\"${escapeAttribute(file.path)}\">`);\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 { path: relativePath, kind: 'markdown', items: extractMarkdownHeadings(text) };\n }\n if (isTypeScriptLikeFile(extension, fullPath)) {\n const text = await readFile(fullPath, 'utf8');\n return { path: relativePath, kind: 'typescript', items: extractTypeScriptSymbols(text) };\n }\n if (extension === '.py') {\n const text = await readFile(fullPath, 'utf8');\n return { path: relativePath, kind: 'python', items: extractPythonSymbols(text) };\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 };\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 hasOutputOverrides(options: ScanOptions): boolean {\n return (\n options.preset !== undefined ||\n options.indent !== undefined ||\n options.filePaths !== undefined ||\n options.file_paths !== 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} 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 };\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)),
@@ -85,7 +100,8 @@ function renderFolderSummary(summary, outputConfig, depth = 0) {
85
100
  const childIndent = outputConfig.indent ? " ".repeat(depth) : "";
86
101
  const lines = [`${indent}<folder path="${escapeAttribute(summary.path)}">`];
87
102
  for (const file of summary.files) {
88
- lines.push(`${childIndent}<file path="${escapeAttribute(file.path)}">`);
103
+ const lineCount = outputConfig.includeLinesCount ? ` lines="${file.lines}"` : "";
104
+ lines.push(`${childIndent}<file path="${escapeAttribute(file.path)}"${lineCount}>`);
89
105
  for (const item of file.items) {
90
106
  const prefix = item.startsWith("[ln ") ? "" : "- ";
91
107
  lines.push(`${childIndent}${prefix}${item}`);
@@ -110,15 +126,30 @@ async function summarizeFile(root, repoRoot, fullPath, outputConfig) {
110
126
  const relativePath = formatFilePath(root, repoRoot, fullPath, outputConfig.filePaths);
111
127
  if (extension === ".md" || extension === ".mdx") {
112
128
  const text = await readFile(fullPath, "utf8");
113
- return { path: relativePath, kind: "markdown", items: extractMarkdownHeadings(text) };
129
+ return {
130
+ path: relativePath,
131
+ kind: "markdown",
132
+ lines: countLines(text),
133
+ items: extractMarkdownHeadings(text)
134
+ };
114
135
  }
115
136
  if (isTypeScriptLikeFile(extension, fullPath)) {
116
137
  const text = await readFile(fullPath, "utf8");
117
- return { path: relativePath, kind: "typescript", items: extractTypeScriptSymbols(text) };
138
+ return {
139
+ path: relativePath,
140
+ kind: "typescript",
141
+ lines: countLines(text),
142
+ items: extractTypeScriptSymbols(text)
143
+ };
118
144
  }
119
145
  if (extension === ".py") {
120
146
  const text = await readFile(fullPath, "utf8");
121
- return { path: relativePath, kind: "python", items: extractPythonSymbols(text) };
147
+ return {
148
+ path: relativePath,
149
+ kind: "python",
150
+ lines: countLines(text),
151
+ items: extractPythonSymbols(text)
152
+ };
122
153
  }
123
154
  return null;
124
155
  }
@@ -197,6 +228,22 @@ function stripBlockComments(text) {
197
228
  function unique(items) {
198
229
  return Array.from(new Set(items));
199
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
+ }
200
247
  function lineNumberAt(text, index) {
201
248
  let lineNumber = 1;
202
249
  for (let offset = 0; offset < index; offset += 1) {
@@ -297,15 +344,21 @@ function resolveOutputConfig(options) {
297
344
  return {
298
345
  deep: options.deep ?? preset.deep,
299
346
  indent: options.indent ?? preset.indent,
300
- filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths)
347
+ filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths),
348
+ includeLinesCount: options.includeLinesCount ?? options.include_lines_count ?? preset.includeLinesCount
301
349
  };
302
350
  }
303
351
  function normalizeFilePathMode(value) {
304
352
  if (value === "concise" || value === "full") return value;
305
353
  throw new Error(`Unknown peek file path mode: ${String(value)}`);
306
354
  }
355
+ function countLines(text) {
356
+ if (text.length === 0) return 0;
357
+ const newlineCount = text.match(/\r\n|\r|\n/g)?.length ?? 0;
358
+ return newlineCount + (/(?:\r\n|\r|\n)$/.test(text) ? 0 : 1);
359
+ }
307
360
  function hasOutputOverrides(options) {
308
- return options.preset !== void 0 || options.indent !== void 0 || options.filePaths !== void 0 || options.file_paths !== void 0;
361
+ return options.preset !== void 0 || options.indent !== void 0 || options.filePaths !== void 0 || options.file_paths !== void 0 || options.includeLinesCount !== void 0 || options.include_lines_count !== void 0;
309
362
  }
310
363
  function isInsidePath(parent, child) {
311
364
  const relativePath = path.relative(parent, child);
@@ -320,17 +373,28 @@ function escapeAttribute(value) {
320
373
  function escapeText(value) {
321
374
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;");
322
375
  }
323
- async function isGitIgnored(root, fullPath) {
324
- const relativePath = toPosix(path.relative(root, fullPath));
325
- if (!relativePath || relativePath.startsWith("..")) return false;
326
- try {
327
- await execFileAsync("git", ["-C", root, "check-ignore", "--quiet", "--", relativePath]);
328
- return true;
329
- } catch (error) {
330
- const code2 = error.code;
331
- if (code2 === 1 || code2 === 128 || code2 === "ENOENT") return false;
332
- return false;
333
- }
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
+ });
334
398
  }
335
399
  async function findRepoRoot(root) {
336
400
  try {
@@ -350,12 +414,14 @@ var init_src = __esm({
350
414
  human: {
351
415
  deep: true,
352
416
  indent: true,
353
- filePaths: "full"
417
+ filePaths: "full",
418
+ includeLinesCount: true
354
419
  },
355
420
  agent: {
356
421
  deep: true,
357
422
  indent: false,
358
- filePaths: "concise"
423
+ filePaths: "concise",
424
+ includeLinesCount: true
359
425
  }
360
426
  };
361
427
  SKIP_DIRS = /* @__PURE__ */ new Set([
@@ -391,7 +457,12 @@ var outputFlags = {
391
457
  },
392
458
  file_paths: {
393
459
  type: "string",
460
+ values: ["concise", "full"],
394
461
  description: "File path style: concise or full"
462
+ },
463
+ "include-lines-count": {
464
+ type: "boolean",
465
+ description: "Include total line counts on file tags"
395
466
  }
396
467
  };
397
468
  function resolveCliScanOptions(flags) {
@@ -407,12 +478,23 @@ function resolveCliScanOptions(flags) {
407
478
  deep: flags.deep,
408
479
  preset,
409
480
  indent: flags.indent,
410
- filePaths
481
+ filePaths,
482
+ includeLinesCount: flags["include-lines-count"]
411
483
  };
412
484
  }
413
485
  var cliSpec = {
414
486
  name: "peek",
415
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
+ ],
416
498
  positionals: [{ name: "path", required: true, description: "Folder to peek at" }],
417
499
  flags: {
418
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};\n\ntype FileSummary = {\n path: string;\n kind: 'markdown' | 'typescript' | 'python';\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};\n\nexport const PEEK_OUTPUT_PRESETS: Record<PeekOutputPresetName, PeekOutputConfig> = {\n human: {\n deep: true,\n indent: true,\n filePaths: 'full',\n },\n agent: {\n deep: true,\n indent: false,\n filePaths: 'concise',\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 lines.push(`${childIndent}<file path=\"${escapeAttribute(file.path)}\">`);\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 { path: relativePath, kind: 'markdown', items: extractMarkdownHeadings(text) };\n }\n if (isTypeScriptLikeFile(extension, fullPath)) {\n const text = await readFile(fullPath, 'utf8');\n return { path: relativePath, kind: 'typescript', items: extractTypeScriptSymbols(text) };\n }\n if (extension === '.py') {\n const text = await readFile(fullPath, 'utf8');\n return { path: relativePath, kind: 'python', items: extractPythonSymbols(text) };\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 };\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 hasOutputOverrides(options: ScanOptions): boolean {\n return (\n options.preset !== undefined ||\n options.indent !== undefined ||\n options.filePaths !== undefined ||\n options.file_paths !== 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} 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 };\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.d.ts CHANGED
@@ -5,6 +5,8 @@ type ScanOptions = {
5
5
  indent?: boolean;
6
6
  filePaths?: PeekFilePathMode;
7
7
  file_paths?: PeekFilePathMode;
8
+ includeLinesCount?: boolean;
9
+ include_lines_count?: boolean;
8
10
  };
9
11
  type PeekFilePathMode = 'concise' | 'full';
10
12
  type PeekOutputPresetName = 'agent' | 'human';
@@ -12,6 +14,7 @@ type PeekOutputConfig = {
12
14
  deep: boolean;
13
15
  indent: boolean;
14
16
  filePaths: PeekFilePathMode;
17
+ includeLinesCount: boolean;
15
18
  };
16
19
  declare const PEEK_OUTPUT_PRESETS: Record<PeekOutputPresetName, PeekOutputConfig>;
17
20
  declare function scanFolderPeek(folder: string, options?: ScanOptions): Promise<string>;
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";
@@ -9,12 +9,14 @@ var PEEK_OUTPUT_PRESETS = {
9
9
  human: {
10
10
  deep: true,
11
11
  indent: true,
12
- filePaths: "full"
12
+ filePaths: "full",
13
+ includeLinesCount: true
13
14
  },
14
15
  agent: {
15
16
  deep: true,
16
17
  indent: false,
17
- filePaths: "concise"
18
+ filePaths: "concise",
19
+ includeLinesCount: true
18
20
  }
19
21
  };
20
22
  var SKIP_DIRS = /* @__PURE__ */ new Set([
@@ -30,7 +32,15 @@ async function scanFolderPeek(folder, options = {}) {
30
32
  const root = path.resolve(folder);
31
33
  const repoRoot = await findRepoRoot(root);
32
34
  const outputConfig = resolveOutputConfig(options);
33
- 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
+ );
34
44
  return `${renderFolderSummary(summary, outputConfig)}
35
45
  `;
36
46
  }
@@ -52,27 +62,34 @@ async function peekFolder(folder, options = {}) {
52
62
  }
53
63
  return (await generateFolderPeek(root, options)).content;
54
64
  }
55
- async function collectFolderSummary(root, repoRoot, dir, deep, outputConfig) {
65
+ async function collectFolderSummary(root, repoRoot, dir, deep, outputConfig, fileLimit) {
56
66
  const entries = await readdir(dir, { withFileTypes: true });
57
- const files = [];
58
- const folders = [];
67
+ const fileTasks = [];
68
+ const folderTasks = [];
59
69
  const omittedFiles = [];
70
+ const ignoredPaths = await checkIgnoredPaths(root, entries.map((entry) => path.join(dir, entry.name)));
60
71
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
61
72
  const fullPath = path.join(dir, entry.name);
62
- if (await isGitIgnored(root, fullPath)) continue;
73
+ if (ignoredPaths.has(toPosix(path.relative(root, fullPath)))) continue;
63
74
  if (entry.isDirectory()) {
64
75
  if (!deep || shouldSkipDirectory(entry.name)) continue;
65
- folders.push(await collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig));
76
+ folderTasks.push(collectFolderSummary(root, repoRoot, fullPath, deep, outputConfig, fileLimit));
66
77
  continue;
67
78
  }
68
79
  if (!entry.isFile() || shouldSkipFile(entry.name)) continue;
69
- const summary = await summarizeFile(root, repoRoot, fullPath, outputConfig);
70
- if (summary) {
71
- files.push(summary);
72
- } else {
73
- omittedFiles.push(entry.name);
74
- }
80
+ fileTasks.push(
81
+ fileLimit(async () => ({
82
+ name: entry.name,
83
+ summary: await summarizeFile(root, repoRoot, fullPath, outputConfig)
84
+ }))
85
+ );
75
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);
76
93
  return {
77
94
  path: formatFolderPath(root, repoRoot, dir),
78
95
  files: files.sort((a, b) => a.path.localeCompare(b.path)),
@@ -90,7 +107,8 @@ function renderFolderSummary(summary, outputConfig, depth = 0) {
90
107
  const childIndent = outputConfig.indent ? " ".repeat(depth) : "";
91
108
  const lines = [`${indent}<folder path="${escapeAttribute(summary.path)}">`];
92
109
  for (const file of summary.files) {
93
- lines.push(`${childIndent}<file path="${escapeAttribute(file.path)}">`);
110
+ const lineCount = outputConfig.includeLinesCount ? ` lines="${file.lines}"` : "";
111
+ lines.push(`${childIndent}<file path="${escapeAttribute(file.path)}"${lineCount}>`);
94
112
  for (const item of file.items) {
95
113
  const prefix = item.startsWith("[ln ") ? "" : "- ";
96
114
  lines.push(`${childIndent}${prefix}${item}`);
@@ -115,15 +133,30 @@ async function summarizeFile(root, repoRoot, fullPath, outputConfig) {
115
133
  const relativePath = formatFilePath(root, repoRoot, fullPath, outputConfig.filePaths);
116
134
  if (extension === ".md" || extension === ".mdx") {
117
135
  const text = await readFile(fullPath, "utf8");
118
- return { path: relativePath, kind: "markdown", items: extractMarkdownHeadings(text) };
136
+ return {
137
+ path: relativePath,
138
+ kind: "markdown",
139
+ lines: countLines(text),
140
+ items: extractMarkdownHeadings(text)
141
+ };
119
142
  }
120
143
  if (isTypeScriptLikeFile(extension, fullPath)) {
121
144
  const text = await readFile(fullPath, "utf8");
122
- return { path: relativePath, kind: "typescript", items: extractTypeScriptSymbols(text) };
145
+ return {
146
+ path: relativePath,
147
+ kind: "typescript",
148
+ lines: countLines(text),
149
+ items: extractTypeScriptSymbols(text)
150
+ };
123
151
  }
124
152
  if (extension === ".py") {
125
153
  const text = await readFile(fullPath, "utf8");
126
- return { path: relativePath, kind: "python", items: extractPythonSymbols(text) };
154
+ return {
155
+ path: relativePath,
156
+ kind: "python",
157
+ lines: countLines(text),
158
+ items: extractPythonSymbols(text)
159
+ };
127
160
  }
128
161
  return null;
129
162
  }
@@ -202,6 +235,22 @@ function stripBlockComments(text) {
202
235
  function unique(items) {
203
236
  return Array.from(new Set(items));
204
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
+ }
205
254
  function lineNumberAt(text, index) {
206
255
  let lineNumber = 1;
207
256
  for (let offset = 0; offset < index; offset += 1) {
@@ -302,15 +351,21 @@ function resolveOutputConfig(options) {
302
351
  return {
303
352
  deep: options.deep ?? preset.deep,
304
353
  indent: options.indent ?? preset.indent,
305
- filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths)
354
+ filePaths: normalizeFilePathMode(options.filePaths ?? options.file_paths ?? preset.filePaths),
355
+ includeLinesCount: options.includeLinesCount ?? options.include_lines_count ?? preset.includeLinesCount
306
356
  };
307
357
  }
308
358
  function normalizeFilePathMode(value) {
309
359
  if (value === "concise" || value === "full") return value;
310
360
  throw new Error(`Unknown peek file path mode: ${String(value)}`);
311
361
  }
362
+ function countLines(text) {
363
+ if (text.length === 0) return 0;
364
+ const newlineCount = text.match(/\r\n|\r|\n/g)?.length ?? 0;
365
+ return newlineCount + (/(?:\r\n|\r|\n)$/.test(text) ? 0 : 1);
366
+ }
312
367
  function hasOutputOverrides(options) {
313
- return options.preset !== void 0 || options.indent !== void 0 || options.filePaths !== void 0 || options.file_paths !== void 0;
368
+ return options.preset !== void 0 || options.indent !== void 0 || options.filePaths !== void 0 || options.file_paths !== void 0 || options.includeLinesCount !== void 0 || options.include_lines_count !== void 0;
314
369
  }
315
370
  function isInsidePath(parent, child) {
316
371
  const relativePath = path.relative(parent, child);
@@ -325,17 +380,28 @@ function escapeAttribute(value) {
325
380
  function escapeText(value) {
326
381
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;");
327
382
  }
328
- async function isGitIgnored(root, fullPath) {
329
- const relativePath = toPosix(path.relative(root, fullPath));
330
- if (!relativePath || relativePath.startsWith("..")) return false;
331
- try {
332
- await execFileAsync("git", ["-C", root, "check-ignore", "--quiet", "--", relativePath]);
333
- return true;
334
- } catch (error) {
335
- const code = error.code;
336
- if (code === 1 || code === 128 || code === "ENOENT") return false;
337
- return false;
338
- }
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
+ });
339
405
  }
340
406
  async function findRepoRoot(root) {
341
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};\n\ntype FileSummary = {\n path: string;\n kind: 'markdown' | 'typescript' | 'python';\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};\n\nexport const PEEK_OUTPUT_PRESETS: Record<PeekOutputPresetName, PeekOutputConfig> = {\n human: {\n deep: true,\n indent: true,\n filePaths: 'full',\n },\n agent: {\n deep: true,\n indent: false,\n filePaths: 'concise',\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 lines.push(`${childIndent}<file path=\"${escapeAttribute(file.path)}\">`);\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 { path: relativePath, kind: 'markdown', items: extractMarkdownHeadings(text) };\n }\n if (isTypeScriptLikeFile(extension, fullPath)) {\n const text = await readFile(fullPath, 'utf8');\n return { path: relativePath, kind: 'typescript', items: extractTypeScriptSymbols(text) };\n }\n if (extension === '.py') {\n const text = await readFile(fullPath, 'utf8');\n return { path: relativePath, kind: 'python', items: extractPythonSymbols(text) };\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 };\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 hasOutputOverrides(options: ScanOptions): boolean {\n return (\n options.preset !== undefined ||\n options.indent !== undefined ||\n options.filePaths !== undefined ||\n options.file_paths !== 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,11 +1,16 @@
1
1
  {
2
2
  "name": "@davstack/peek",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Peek at concise, agent-friendly folder summaries.",
7
+ "scripts": {
8
+ "build": "tsup",
9
+ "prepublishOnly": "tsup",
10
+ "test": "pnpm -w exec vitest run --project peek"
11
+ },
7
12
  "bin": {
8
- "peek": "./bin/davstack-peek.mjs"
13
+ "peek": "bin/davstack-peek.mjs"
9
14
  },
10
15
  "exports": {
11
16
  ".": {
@@ -26,7 +31,7 @@
26
31
  "node": ">=20"
27
32
  },
28
33
  "dependencies": {
29
- "@davstack/cli-utils": "1.3.1"
34
+ "@davstack/cli-utils": "workspace:*"
30
35
  },
31
36
  "devDependencies": {
32
37
  "@types/node": "^25.9.1",
@@ -35,9 +40,5 @@
35
40
  },
36
41
  "publishConfig": {
37
42
  "access": "public"
38
- },
39
- "scripts": {
40
- "build": "tsup",
41
- "test": "pnpm -w exec vitest run --project peek"
42
43
  }
43
- }
44
+ }