lsallcp 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # lsallcp
2
2
 
3
- > Recursively dump every file in a directory as colorized, fenced-code blocks -- with smart skip patterns, a live progress bar, and one-flag clipboard export. Built on [`cli-atelier`](https://www.npmjs.com/package/cli-atelier), runs on [Bun](https://bun.sh).
3
+ > Recursively dump every file in a directory as colorized, fenced-code blocks -- with smart skip patterns, a live progress bar, and one-flag clipboard export. Built on [`cli-atelier`](https://www.npmjs.com/package/cli-atelier), runs on Node.js.
4
4
 
5
5
  ```bash
6
- bun add -g lsallcp
6
+ npm install -g lsallcp
7
7
  ```
8
8
 
9
9
  ---
@@ -34,39 +34,36 @@ It's built for quickly dumping a project's source into a chat window, a doc, or
34
34
  | Live progress bar | Real-time progress while files are read, powered by `cli-atelier` |
35
35
  | Colorized output | Paths, fences, and log messages are colored for readability |
36
36
  | Clipboard export | `-c` / `--copy` copies plain-text results directly to your system clipboard |
37
- | Zero runtime dependencies (besides `cli-atelier`) | No bundler, no build step -- pure Bun + TypeScript |
37
+ | Zero runtime dependencies (besides `cli-atelier`) | Ships precompiled JavaScript -- no separate build step at install time |
38
38
  | Cross-platform | Works in Windows Terminal, Git Bash, macOS Terminal, and Linux shells |
39
39
 
40
40
  ---
41
41
 
42
42
  ## Requirements
43
43
 
44
- | Dependency | Minimum version | Notes |
45
- | ---------- | --------------- | ------------------------------------------------------------ |
46
- | Bun | `>= 1.1.0` | Required at runtime -- the CLI executes as raw `.ts` via Bun |
47
- | Node.js | `>= 18.0.0` | Only needed if installing globally via `npm`/`pnpm`/`yarn` |
44
+ | Dependency | Minimum version | Notes |
45
+ | ---------- | --------------- | ------------------------------------------------------- |
46
+ | Node.js | `>= 18.0.0` | Runs the compiled CLI directly, no other runtime needed |
48
47
 
49
- > `lsallcp` always executes through Bun. When installed via `npm install -g`, the generated shim (`bin/lsallcp.js`) runs under Node just long enough to spawn Bun as a subprocess -- this is what makes the same package work identically in Windows Terminal, Git Bash, macOS, and Linux.
48
+ `lsallcp` is a pure Node.js package. TypeScript sources are compiled to plain JavaScript with `tsc` and published under `dist/`, so no Bun, `ts-node`, or extra runtime is required to use it.
50
49
 
51
50
  ---
52
51
 
53
52
  ## Installation
54
53
 
55
54
  ```bash
56
- # Bun (recommended -- installs and runs natively)
57
- bun add -g lsallcp
58
-
59
- # npm / pnpm / yarn (cross-platform shim, still requires Bun on PATH)
60
55
  npm install -g lsallcp
56
+ # or
61
57
  pnpm add -g lsallcp
58
+ # or
62
59
  yarn global add lsallcp
63
60
  ```
64
61
 
65
62
  To use it inside a single project instead of globally:
66
63
 
67
64
  ```bash
68
- bun add -d lsallcp
69
- bunx lsallcp .
65
+ npm install -D lsallcp
66
+ npx lsallcp .
70
67
  ```
71
68
 
72
69
  ---
@@ -167,10 +164,10 @@ If none of these tools are available on Linux, `lsallcp` prints a warning and le
167
164
 
168
165
  Each file is rendered as:
169
166
 
170
- <relativePath>:
171
- <code><extension>
172
- <file content>
173
- </code>
167
+ \<relativePath>:<br>
168
+ \<code>\<extension><br>
169
+ \<file content><br>
170
+ \</code>
174
171
 
175
172
  - `<relativePath>` is colored cyan and bold, relative to the scanned root, using forward slashes on all platforms.
176
173
  - `<extension>` is the file's extension without the leading dot (`ts`, `json`, `md`); extensionless files fall back to `text`.
@@ -189,13 +186,13 @@ Each file is rendered as:
189
186
 
190
187
  ## How it works
191
188
 
192
- `lsallcp` is a thin, purpose-built CLI in three stages:
189
+ `lsallcp` is a thin, purpose-built CLI in three stages, written in TypeScript and compiled to JavaScript via `tsc` (`npm run build`, wired to `prepare` so it's built automatically on install from source):
193
190
 
194
191
  1. **Scan** -- recursively walks the target directory (`src/scanner.ts`), pruning any path matched by a compiled skip matcher (`src/matcher.ts`) before ever entering it.
195
192
  2. **Read** -- reads each remaining file, detects binary content (`src/binary.ts`), and formats it into a colorized fenced block (`src/render.ts`), updating a live progress bar as it goes.
196
193
  3. **Output** -- either prints the assembled result to stdout, or strips ANSI codes and sends it to the system clipboard (`src/clipboard.ts`) when `-c`/`--copy` is set.
197
194
 
198
- All terminal UI -- headers, spinners, progress bars, logs, boxes -- comes from [`cli-atelier`](https://www.npmjs.com/package/cli-atelier), a zero-dependency, pure-ANSI terminal framework for Bun.
195
+ All terminal UI -- headers, spinners, progress bars, logs, boxes -- comes from [`cli-atelier`](https://www.npmjs.com/package/cli-atelier), a zero-dependency, pure-ANSI terminal framework.
199
196
 
200
197
  ---
201
198
 
package/dist/binary.js ADDED
@@ -0,0 +1,7 @@
1
+ export function isBinaryBuffer(buffer, sampleSize = 8000) {
2
+ const length = Math.min(buffer.length, sampleSize);
3
+ for (let index = 0; index < length; index++)
4
+ if (buffer[index] === 0)
5
+ return true;
6
+ return false;
7
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { promises as fs } from 'node:fs';
4
+ import { Display, Log, Header, Spinner, Progress, Box } from 'cli-atelier';
5
+ import { buildSkipMatchers } from './matcher.js';
6
+ import { scanDirectory } from './scanner.js';
7
+ import { renderFile } from './render.js';
8
+ import { copyToClipboard } from './clipboard.js';
9
+ const COPY_FLAGS = new Set(['--copy', '-c']);
10
+ function printHelp() {
11
+ Box.render([
12
+ Display.paint('lsallcp <path> [skipPattern...] [--copy|-c]', Display.color.white, Display.color.bold),
13
+ '',
14
+ ' lsallcp . List every file under the current dir',
15
+ ' lsallcp . node_modules Skip any file/folder named node_modules',
16
+ ' lsallcp . *.png *.lock Skip by glob pattern',
17
+ ' lsallcp . /\\.test\\.ts$/i Skip by regex literal',
18
+ " lsallcp . --copy List, copy to clipboard, don't print content",
19
+ " lsallcp . node_modules -c Skip + copy, flag order doesn't matter",
20
+ '',
21
+ Display.paint('Note:', Display.color.yellow) + ' .git is always skipped.',
22
+ ], { style: 'round', color: Display.color.cyan, title: 'lsallcp' });
23
+ }
24
+ async function resolveRoot(targetArgument) {
25
+ const root = path.resolve(process.cwd(), targetArgument);
26
+ const stat = await fs.stat(root);
27
+ if (!stat.isDirectory())
28
+ throw new Error(`Target is not a directory: ${root}`);
29
+ return root;
30
+ }
31
+ function parseArgs(argv) {
32
+ let copy = false;
33
+ const rest = [];
34
+ for (const arg of argv) {
35
+ if (COPY_FLAGS.has(arg))
36
+ copy = true;
37
+ else
38
+ rest.push(arg);
39
+ }
40
+ const [targetArgument, ...skipArguments] = rest;
41
+ return { targetArgument, skipArguments, copy };
42
+ }
43
+ async function main() {
44
+ const argv = process.argv.slice(2);
45
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
46
+ printHelp();
47
+ return;
48
+ }
49
+ const { targetArgument, skipArguments, copy } = parseArgs(argv);
50
+ if (!targetArgument) {
51
+ printHelp();
52
+ return;
53
+ }
54
+ Header.render('lsallcp', { style: 'banner', color: Display.color.magenta });
55
+ const root = await resolveRoot(targetArgument);
56
+ Log.info(`Target: ${Display.paint(root, Display.color.yellow)}`);
57
+ if (skipArguments.length > 0)
58
+ Log.info(`Skip patterns: ${Display.paint(skipArguments.join(', '), Display.color.gray)}`);
59
+ if (copy)
60
+ Log.info(`Clipboard: ${Display.paint('enabled', Display.color.green)} (content will not be printed)`);
61
+ const matchers = buildSkipMatchers(skipArguments);
62
+ const spinner = new Spinner('Walking directory tree...');
63
+ spinner.start();
64
+ const files = await scanDirectory(root, matchers);
65
+ spinner.stop(`Found ${files.length} file(s) after skipping`);
66
+ if (files.length === 0) {
67
+ Log.warn('No files matched. Nothing to list.');
68
+ return;
69
+ }
70
+ const bar = new Progress({ label: 'Reading files', width: 40, color: Display.color.cyan });
71
+ const blocks = [];
72
+ for (let index = 0; index < files.length; index++) {
73
+ blocks.push(await renderFile(files[index]));
74
+ bar.update(Math.round(((index + 1) / files.length) * 100));
75
+ }
76
+ bar.finish('All files read');
77
+ const output = blocks.join('\n\n');
78
+ if (!copy) {
79
+ Header.divider();
80
+ console.log(output);
81
+ Header.divider();
82
+ }
83
+ Log.success(`Listed ${files.length} file(s) from ${Display.paint(root, Display.color.yellow)}`);
84
+ if (copy) {
85
+ const plainOutput = Display.strip(output);
86
+ const copied = await copyToClipboard(plainOutput);
87
+ if (copied)
88
+ Log.success('Result copied to clipboard');
89
+ else
90
+ Log.warn('Could not copy to clipboard (no clip/pbcopy/wl-copy/xclip/xsel found)');
91
+ }
92
+ }
93
+ main().catch((error) => {
94
+ Log.error(error instanceof Error ? error.stack ?? error.message : String(error));
95
+ process.exit(1);
96
+ });
@@ -0,0 +1,30 @@
1
+ import { spawn } from 'node:child_process';
2
+ function candidateCommands() {
3
+ switch (process.platform) {
4
+ case 'win32': return [{ command: 'clip', args: [] }];
5
+ case 'darwin': return [{ command: 'pbcopy', args: [] }];
6
+ default: return [
7
+ { command: 'wl-copy', args: [] },
8
+ { command: 'xclip', args: ['-selection', 'clipboard'] },
9
+ { command: 'xsel', args: ['--clipboard', '--input'] },
10
+ ];
11
+ }
12
+ }
13
+ function tryCommand(command, text) {
14
+ return new Promise((resolve) => {
15
+ const child = spawn(command.command, command.args, { stdio: ['pipe', 'ignore', 'ignore'] });
16
+ child.once('error', () => resolve(false));
17
+ child.once('close', (code) => resolve(code === 0));
18
+ child.stdin.on('error', () => { });
19
+ child.stdin.write(text);
20
+ child.stdin.end();
21
+ });
22
+ }
23
+ export async function copyToClipboard(text) {
24
+ for (const command of candidateCommands()) {
25
+ const ok = await tryCommand(command, text);
26
+ if (ok)
27
+ return true;
28
+ }
29
+ return false;
30
+ }
@@ -0,0 +1,51 @@
1
+ function defaultGitMatcher(baseName) { return baseName === '.git'; }
2
+ function parseRegexLiteral(raw) {
3
+ const match = raw.match(/^\/(.+)\/([a-z]*)$/i);
4
+ if (!match)
5
+ return null;
6
+ try {
7
+ return new RegExp(match[1], match[2]);
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ }
13
+ function globToRegExp(glob) {
14
+ let out = '^';
15
+ for (let index = 0; index < glob.length; index++) {
16
+ const char = glob[index];
17
+ if (char === '*') {
18
+ if (glob[index + 1] === '*') {
19
+ out += '.*';
20
+ index++;
21
+ }
22
+ else
23
+ out += '[^/]*';
24
+ }
25
+ else if (char === '?')
26
+ out += '[^/]';
27
+ else if ('.+^$()[]{}|\\'.includes(char))
28
+ out += '\\' + char;
29
+ else
30
+ out += char;
31
+ }
32
+ out += '$';
33
+ return new RegExp(out, 'i');
34
+ }
35
+ function compilePattern(raw) {
36
+ const regexLiteral = parseRegexLiteral(raw);
37
+ if (regexLiteral)
38
+ return (relativePath, baseName) => regexLiteral.test(baseName) || regexLiteral.test(relativePath);
39
+ if (/[*?]/.test(raw)) {
40
+ const compiled = globToRegExp(raw);
41
+ return (relativePath, baseName) => compiled.test(baseName) || compiled.test(relativePath);
42
+ }
43
+ const lowered = raw.toLowerCase();
44
+ return (_relativePath, baseName) => baseName.toLowerCase() === lowered;
45
+ }
46
+ export function buildSkipMatchers(patterns) {
47
+ return [defaultGitMatcher, ...patterns.map(compilePattern)];
48
+ }
49
+ export function shouldSkip(matchers, relativePath, baseName, isDirectory) {
50
+ return matchers.some((matcher) => matcher(relativePath, baseName, isDirectory));
51
+ }
package/dist/render.js ADDED
@@ -0,0 +1,26 @@
1
+ import path from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
+ import { Display } from 'cli-atelier';
4
+ import { isBinaryBuffer } from './binary.js';
5
+ function resolveExtension(relativePath) {
6
+ const extension = path.extname(relativePath).slice(1);
7
+ return extension.length > 0 ? extension : 'text';
8
+ }
9
+ export async function renderFile(file) {
10
+ const extension = resolveExtension(file.relativePath);
11
+ let body;
12
+ try {
13
+ const buffer = await fs.readFile(file.absolutePath);
14
+ if (isBinaryBuffer(buffer))
15
+ body = Display.paint('[binary file - content skipped]', Display.color.gray, Display.color.italic);
16
+ else
17
+ body = buffer.toString('utf8');
18
+ }
19
+ catch (error) {
20
+ body = Display.paint(`[unreadable: ${error.message}]`, Display.color.red);
21
+ }
22
+ const header = Display.paint(file.relativePath, Display.color.cyan, Display.color.bold) + ':';
23
+ const openFence = Display.paint('```' + extension, Display.color.gray);
24
+ const closeFence = Display.paint('```', Display.color.gray);
25
+ return `${header}\n${openFence}\n${body}\n${closeFence}`;
26
+ }
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
+ import { Log } from 'cli-atelier';
4
+ import { shouldSkip } from './matcher.js';
5
+ export async function scanDirectory(root, matchers) {
6
+ const results = [];
7
+ await walk(root, '', matchers, results);
8
+ results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
9
+ return results;
10
+ }
11
+ async function walk(directory, relativeBase, matchers, results) {
12
+ let entries;
13
+ try {
14
+ entries = await fs.readdir(directory, { withFileTypes: true });
15
+ }
16
+ catch (error) {
17
+ Log.warn(`Skipping unreadable directory: ${directory} (${error.message})`);
18
+ return;
19
+ }
20
+ for (const entry of entries) {
21
+ const relativePath = relativeBase ? `${relativeBase}/${entry.name}` : entry.name;
22
+ const isDirectory = entry.isDirectory();
23
+ if (shouldSkip(matchers, relativePath, entry.name, isDirectory))
24
+ continue;
25
+ if (isDirectory) {
26
+ await walk(path.join(directory, entry.name), relativePath, matchers, results);
27
+ }
28
+ else if (entry.isFile()) {
29
+ results.push({ absolutePath: path.join(directory, entry.name), relativePath });
30
+ }
31
+ }
32
+ }
package/package.json CHANGED
@@ -1,21 +1,24 @@
1
1
  {
2
2
  "name": "lsallcp",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Recursively list every file's content as relativePath: ```ext code fences```, with glob/regex/name skip patterns, progress bar and colored output",
5
5
  "type": "module",
6
6
  "bin": {
7
- "lsallcp": "./bin/lsallcp.js"
7
+ "lsallcp": "./dist/cli.js"
8
8
  },
9
- "files": ["src", "bin", "package.json", "README.md", "LICENSE"],
9
+ "main": "./dist/cli.js",
10
+ "files": ["dist", "package.json", "README.md", "LICENSE"],
10
11
  "engines": {
11
- "bun": ">=1.1.0"
12
+ "node": ">=18.0.0"
12
13
  },
13
14
  "scripts": {
14
- "start": "bun src/cli.ts"
15
+ "build": "tsc",
16
+ "prepare": "bun run build",
17
+ "start": "bun run dist/cli.js"
15
18
  },
16
- "keywords": ["cli", "bun", "list", "files", "dump", "tree"],
19
+ "keywords": ["cli", "node", "list", "files", "dump", "tree"],
17
20
  "devDependencies": {
18
- "@types/bun": "latest",
21
+ "@types/node": "latest",
19
22
  "typescript": "latest"
20
23
  },
21
24
  "dependencies": {
package/bin/lsallcp.js DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const { spawnSync } = require('node:child_process')
4
- const path = require('node:path')
5
-
6
- const cliEntry = path.join(__dirname, '..', 'src', 'cli.ts')
7
- const args = process.argv.slice(2)
8
-
9
- const result = spawnSync('bun', [cliEntry, ...args], { stdio: 'inherit' })
10
-
11
- if (result.error) {
12
- console.error('lsallcp requires Bun to be installed and available on your PATH.')
13
- console.error('Install it from https://bun.sh/ then try again.')
14
- process.exit(1)
15
- }
16
-
17
- process.exit(result.status === null ? 1 : result.status)
package/src/binary.ts DELETED
@@ -1,5 +0,0 @@
1
- export function isBinaryBuffer(buffer: Buffer, sampleSize = 8000): boolean {
2
- const length = Math.min(buffer.length, sampleSize)
3
- for (let index = 0; index < length; index++) if (buffer[index] === 0) return true
4
- return false
5
- }
package/src/cli.ts DELETED
@@ -1,99 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import path from 'node:path'
4
- import { promises as fs } from 'node:fs'
5
- import { Display, Log, Header, Spinner, Progress, Box } from 'cli-atelier'
6
- import { buildSkipMatchers } from './matcher'
7
- import { scanDirectory } from './scanner'
8
- import { renderFile } from './render'
9
- import { copyToClipboard } from './clipboard'
10
-
11
- const COPY_FLAGS = new Set(['--copy', '-c'])
12
-
13
- function printHelp(): void {
14
- Box.render(
15
- [
16
- Display.paint('lsallcp <path> [skipPattern...] [--copy|-c]', Display.color.white, Display.color.bold),
17
- '',
18
- ' lsallcp . List every file under the current dir',
19
- ' lsallcp . node_modules Skip any file/folder named node_modules',
20
- ' lsallcp . *.png *.lock Skip by glob pattern',
21
- ' lsallcp . /\\.test\\.ts$/i Skip by regex literal',
22
- " lsallcp . --copy List, copy to clipboard, don't print content",
23
- " lsallcp . node_modules -c Skip + copy, flag order doesn't matter",
24
- '',
25
- Display.paint('Note:', Display.color.yellow) + ' .git is always skipped.',
26
- ],
27
- { style: 'round', color: Display.color.cyan, title: 'lsallcp' }
28
- )
29
- }
30
-
31
- async function resolveRoot(targetArgument: string): Promise<string> {
32
- const root = path.resolve(process.cwd(), targetArgument)
33
- const stat = await fs.stat(root)
34
- if (!stat.isDirectory()) throw new Error(`Target is not a directory: ${root}`)
35
- return root
36
- }
37
-
38
- function parseArgs(argv: string[]): { targetArgument: string, skipArguments: string[], copy: boolean } {
39
- let copy = false
40
- const rest: string[] = []
41
- for (const arg of argv) {
42
- if (COPY_FLAGS.has(arg)) copy = true
43
- else rest.push(arg)
44
- }
45
- const [targetArgument, ...skipArguments] = rest
46
- return { targetArgument, skipArguments, copy }
47
- }
48
-
49
- async function main(): Promise<void> {
50
- const argv = process.argv.slice(2)
51
- if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
52
- printHelp()
53
- return
54
- }
55
- const { targetArgument, skipArguments, copy } = parseArgs(argv)
56
- if (!targetArgument) {
57
- printHelp()
58
- return
59
- }
60
- Header.render('lsallcp', { style: 'banner', color: Display.color.magenta })
61
- const root = await resolveRoot(targetArgument)
62
- Log.info(`Target: ${Display.paint(root, Display.color.yellow)}`)
63
- if (skipArguments.length > 0) Log.info(`Skip patterns: ${Display.paint(skipArguments.join(', '), Display.color.gray)}`)
64
- if (copy) Log.info(`Clipboard: ${Display.paint('enabled', Display.color.green)} (content will not be printed)`)
65
- const matchers = buildSkipMatchers(skipArguments)
66
- const spinner = new Spinner('Walking directory tree...')
67
- spinner.start()
68
- const files = await scanDirectory(root, matchers)
69
- spinner.stop(`Found ${files.length} file(s) after skipping`)
70
- if (files.length === 0) {
71
- Log.warn('No files matched. Nothing to list.')
72
- return
73
- }
74
- const bar = new Progress({ label: 'Reading files', width: 40, color: Display.color.cyan })
75
- const blocks: string[] = []
76
- for (let index = 0; index < files.length; index++) {
77
- blocks.push(await renderFile(files[index]))
78
- bar.update(Math.round(((index + 1) / files.length) * 100))
79
- }
80
- bar.finish('All files read')
81
- const output = blocks.join('\n\n')
82
- if (!copy) {
83
- Header.divider()
84
- console.log(output)
85
- Header.divider()
86
- }
87
- Log.success(`Listed ${files.length} file(s) from ${Display.paint(root, Display.color.yellow)}`)
88
- if (copy) {
89
- const plainOutput = Display.strip(output)
90
- const copied = await copyToClipboard(plainOutput)
91
- if (copied) Log.success('Result copied to clipboard')
92
- else Log.warn('Could not copy to clipboard (no clip/pbcopy/wl-copy/xclip/xsel found)')
93
- }
94
- }
95
-
96
- main().catch((error) => {
97
- Log.error(error instanceof Error ? error.stack ?? error.message : String(error))
98
- process.exit(1)
99
- })
package/src/clipboard.ts DELETED
@@ -1,37 +0,0 @@
1
- import { spawn } from 'node:child_process'
2
-
3
- interface ClipboardCommand {
4
- command: string
5
- args: string[]
6
- }
7
-
8
- function candidateCommands(): ClipboardCommand[] {
9
- switch (process.platform) {
10
- case 'win32': return [{ command: 'clip', args: [] }]
11
- case 'darwin': return [{ command: 'pbcopy', args: [] }]
12
- default: return [
13
- { command: 'wl-copy', args: [] },
14
- { command: 'xclip', args: ['-selection', 'clipboard'] },
15
- { command: 'xsel', args: ['--clipboard', '--input'] },
16
- ]
17
- }
18
- }
19
-
20
- function tryCommand(command: ClipboardCommand, text: string): Promise<boolean> {
21
- return new Promise((resolve) => {
22
- const child = spawn(command.command, command.args, { stdio: ['pipe', 'ignore', 'ignore'] })
23
- child.once('error', () => resolve(false))
24
- child.once('close', (code) => resolve(code === 0))
25
- child.stdin.on('error', () => { })
26
- child.stdin.write(text)
27
- child.stdin.end()
28
- })
29
- }
30
-
31
- export async function copyToClipboard(text: string): Promise<boolean> {
32
- for (const command of candidateCommands()) {
33
- const ok = await tryCommand(command, text)
34
- if (ok) return true
35
- }
36
- return false
37
- }
package/src/matcher.ts DELETED
@@ -1,45 +0,0 @@
1
- export type SkipMatcher = (relativePath: string, baseName: string, isDirectory: boolean) => boolean
2
-
3
- function defaultGitMatcher(baseName: string): boolean { return baseName === '.git' }
4
-
5
- function parseRegexLiteral(raw: string): RegExp | null {
6
- const match = raw.match(/^\/(.+)\/([a-z]*)$/i)
7
- if (!match) return null
8
- try { return new RegExp(match[1], match[2]) } catch { return null }
9
- }
10
-
11
- function globToRegExp(glob: string): RegExp {
12
- let out = '^'
13
- for (let index = 0; index < glob.length; index++) {
14
- const char = glob[index]
15
- if (char === '*') {
16
- if (glob[index + 1] === '*') {
17
- out += '.*'
18
- index++
19
- } else out += '[^/]*'
20
- } else if (char === '?') out += '[^/]'
21
- else if ('.+^$()[]{}|\\'.includes(char)) out += '\\' + char
22
- else out += char
23
- }
24
- out += '$'
25
- return new RegExp(out, 'i')
26
- }
27
-
28
- function compilePattern(raw: string): SkipMatcher {
29
- const regexLiteral = parseRegexLiteral(raw)
30
- if (regexLiteral) return (relativePath, baseName) => regexLiteral.test(baseName) || regexLiteral.test(relativePath)
31
- if (/[*?]/.test(raw)) {
32
- const compiled = globToRegExp(raw)
33
- return (relativePath, baseName) => compiled.test(baseName) || compiled.test(relativePath)
34
- }
35
- const lowered = raw.toLowerCase()
36
- return (_relativePath, baseName) => baseName.toLowerCase() === lowered
37
- }
38
-
39
- export function buildSkipMatchers(patterns: string[]): SkipMatcher[] {
40
- return [defaultGitMatcher, ...patterns.map(compilePattern)]
41
- }
42
-
43
- export function shouldSkip(matchers: SkipMatcher[], relativePath: string, baseName: string, isDirectory: boolean): boolean {
44
- return matchers.some((matcher) => matcher(relativePath, baseName, isDirectory))
45
- }
package/src/render.ts DELETED
@@ -1,26 +0,0 @@
1
- import path from 'node:path'
2
- import { promises as fs } from 'node:fs'
3
- import { Display } from 'cli-atelier'
4
- import type { ScannedFile } from './scanner'
5
- import { isBinaryBuffer } from './binary'
6
-
7
- function resolveExtension(relativePath: string): string {
8
- const extension = path.extname(relativePath).slice(1)
9
- return extension.length > 0 ? extension : 'text'
10
- }
11
-
12
- export async function renderFile(file: ScannedFile): Promise<string> {
13
- const extension = resolveExtension(file.relativePath)
14
- let body: string
15
- try {
16
- const buffer = await fs.readFile(file.absolutePath)
17
- if (isBinaryBuffer(buffer)) body = Display.paint('[binary file - content skipped]', Display.color.gray, Display.color.italic)
18
- else body = buffer.toString('utf8')
19
- } catch (error) {
20
- body = Display.paint(`[unreadable: ${(error as Error).message}]`, Display.color.red)
21
- }
22
- const header = Display.paint(file.relativePath, Display.color.cyan, Display.color.bold) + ':'
23
- const openFence = Display.paint('```' + extension, Display.color.gray)
24
- const closeFence = Display.paint('```', Display.color.gray)
25
- return `${header}\n${openFence}\n${body}\n${closeFence}`
26
- }
package/src/scanner.ts DELETED
@@ -1,37 +0,0 @@
1
- import path from 'node:path'
2
- import { promises as fs } from 'node:fs'
3
- import { Log } from 'cli-atelier'
4
- import type { SkipMatcher } from './matcher'
5
- import { shouldSkip } from './matcher'
6
-
7
- export interface ScannedFile {
8
- absolutePath: string
9
- relativePath: string
10
- }
11
-
12
- export async function scanDirectory(root: string, matchers: SkipMatcher[]): Promise<ScannedFile[]> {
13
- const results: ScannedFile[] = []
14
- await walk(root, '', matchers, results)
15
- results.sort((a, b) => a.relativePath.localeCompare(b.relativePath))
16
- return results
17
- }
18
-
19
- async function walk(directory: string, relativeBase: string, matchers: SkipMatcher[], results: ScannedFile[]): Promise<void> {
20
- let entries: import('node:fs').Dirent[]
21
- try {
22
- entries = await fs.readdir(directory, { withFileTypes: true })
23
- } catch (error) {
24
- Log.warn(`Skipping unreadable directory: ${directory} (${(error as Error).message})`)
25
- return
26
- }
27
- for (const entry of entries) {
28
- const relativePath = relativeBase ? `${relativeBase}/${entry.name}` : entry.name
29
- const isDirectory = entry.isDirectory()
30
- if (shouldSkip(matchers, relativePath, entry.name, isDirectory)) continue
31
- if (isDirectory) {
32
- await walk(path.join(directory, entry.name), relativePath, matchers, results)
33
- } else if (entry.isFile()) {
34
- results.push({ absolutePath: path.join(directory, entry.name), relativePath })
35
- }
36
- }
37
- }