stdin-glob 1.3.0 → 1.8.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
@@ -31,6 +31,8 @@ This pipes all relevant TypeScript/TSX files directly into my clipboard, ready t
31
31
  - Support for absolute or relative paths
32
32
  - Option to show only file paths without content
33
33
  - **Intelligent handling of binary files** - shows metadata instead of attempting to display unreadable content
34
+ - **Automatic .gitignore filtering** - respects your project's ignore rules by default
35
+ - **Reverse apply** - recreate files from a markdown document containing code blocks (inverse operation)
34
36
  - Written in TypeScript
35
37
 
36
38
  ## Installation
@@ -47,15 +49,16 @@ stdin-glob [options] [patterns...]
47
49
 
48
50
  ### Options
49
51
 
50
- | Option | Description |
51
- | --------------------- | --------------------------------------------------------------------- |
52
- | `--no-content` | Do not show file contents, only list matching paths |
53
- | `--absolute` | Show absolute paths for entries |
54
- | `-c, --copy` | Copy the output to clipboard instead of printing to console |
55
- | `-m, --max-lines <n>` | Show only the first N lines of each file (shows full file if omitted) |
56
- | `-n, --line-numbers` | Display line numbers next to each line, like in IDE sidebars |
57
- | `-V, --version` | Output the version number |
58
- | `-h, --help` | Display help information |
52
+ | Option | Description |
53
+ | --------------------- | --------------------------------------------------------------------------- |
54
+ | `--no-content` | Do not show file contents, only list matching paths |
55
+ | `--absolute` | Show absolute paths for entries |
56
+ | `-c, --copy` | Copy the output to clipboard instead of printing to console |
57
+ | `-m, --max-lines <n>` | Show only the first N lines of each file (shows full file if omitted) |
58
+ | `-n, --line-numbers` | Display line numbers next to each line, like in IDE sidebars |
59
+ | `--no-gitignore` | Disable .gitignore filtering (include files that would normally be ignored) |
60
+ | `-V, --version` | Output the version number |
61
+ | `-h, --help` | Display help information |
59
62
 
60
63
  ### Arguments
61
64
 
@@ -63,6 +66,44 @@ stdin-glob [options] [patterns...]
63
66
  | ---------- | ------------------------------------------ |
64
67
  | `patterns` | Glob patterns to match files (one or more) |
65
68
 
69
+ ### Apply Command
70
+
71
+ The `apply` subcommand is the inverse operation: it reads a file containing code blocks in the format produced by `stdin-glob` and creates or updates the corresponding files on disk.
72
+
73
+ ```bash
74
+ stdin-glob apply <input-file> [options]
75
+ ```
76
+
77
+ #### Apply Options
78
+
79
+ | Option | Description |
80
+ | ------------------ | ---------------------------------------------------------- |
81
+ | `<input-file>` | File containing code blocks in markdown format |
82
+ | `-d, --dir <path>` | Base directory to apply files (default: current directory) |
83
+ | `--dry-run` | Show what would be done without making any changes |
84
+
85
+ #### How It Works
86
+
87
+ The `apply` command parses a document looking for code blocks that follow this structure:
88
+
89
+ ````
90
+ ```ext
91
+ // path/to/file.ext
92
+ file content here
93
+ ```
94
+ ````
95
+
96
+ It then creates or updates each file based on the extracted content. This is particularly useful when an LLM generates modified code—you can simply apply the output directly to your project.
97
+
98
+ Key behaviors:
99
+
100
+ - **Noise tolerance**: Ignores any text outside of code blocks (explanations, comments, etc.)
101
+ - **Matching backticks**: Only recognizes code blocks where the opening and closing have the exact same number of backticks
102
+ - **Binary file detection**: Files marked with `[BINARY FILE]` are automatically skipped
103
+ - **Truncation warnings**: Warns when the source content was truncated
104
+ - **Directory creation**: Automatically creates any necessary parent directories
105
+ - **Create vs. update**: Reports which files were created new and which were modified
106
+
66
107
  ## Pattern Syntax
67
108
 
68
109
  This tool uses [fast-glob](https://github.com/mrmlnc/fast-glob) for pattern matching, which supports the feature set of [picomatch](https://github.com/micromatch/picomatch). For detailed information about available globbing features and syntax options, refer to the [picomatch globbing features documentation](https://github.com/micromatch/picomatch?tab=readme-ov-file#globbing-features).
@@ -251,3 +292,119 @@ stdin-glob "src/**/*.ts" --no-content | pbcopy
251
292
  stdin-glob "src/**/*.ts" --content
252
293
  stdin-glob "src/**/*.ts" --copy
253
294
  ```
295
+
296
+ ### .gitignore Support
297
+
298
+ By default, `stdin-glob` automatically respects your project's `.gitignore` rules. This means files and directories listed in `.gitignore` won't appear in the output. This is especially useful when you want to avoid including build artifacts, dependencies, or environment files in your context.
299
+
300
+ The gitignore pattern matching implementation is based on the official [gitignore pattern format documentation](https://git-scm.com/docs/gitignore#_pattern_format), ensuring compatibility with how git itself handles ignore rules.
301
+
302
+ #### Including ignored files
303
+
304
+ If you need to include files that would normally be ignored, use the `--no-gitignore` flag:
305
+
306
+ ```bash
307
+ stdin-glob "dist/**/*.js" --no-gitignore
308
+ ```
309
+
310
+ This disables all `.gitignore` filtering and includes every file matching your patterns.
311
+
312
+ ### Apply files from markdown output
313
+
314
+ Apply code blocks from a file directly to your project:
315
+
316
+ ```bash
317
+ stdin-glob apply output.md
318
+ ```
319
+
320
+ This reads `output.md`, finds all code blocks with file paths, and creates or updates the corresponding files.
321
+
322
+ #### Apply to a specific directory
323
+
324
+ Target a different directory than the current one:
325
+
326
+ ```bash
327
+ stdin-glob apply output.md --dir ./my-project
328
+ ```
329
+
330
+ #### Dry run
331
+
332
+ Preview what would happen without making any changes:
333
+
334
+ ```bash
335
+ stdin-glob apply output.md --dry-run
336
+ ```
337
+
338
+ Output:
339
+
340
+ ```
341
+ Found 3 file(s) to process:
342
+
343
+ [OK] src/index.ts
344
+ [OK] src/utils/helpers.ts
345
+ [WARN - truncated] src/types/index.ts
346
+
347
+ [Dry run] No files were modified.
348
+ ```
349
+
350
+ #### Handling noisy input
351
+
352
+ The `apply` command is designed to work with real LLM output, which often includes explanations between code blocks:
353
+
354
+ ````
355
+ Here are the updated files:
356
+
357
+ The main index file has been modified to add error handling:
358
+
359
+ ```ts
360
+ // src/index.ts
361
+ console.log('Hello!');
362
+ ```
363
+
364
+ I also created a new utility:
365
+
366
+ ```js
367
+ // src/utils/new.js
368
+ export const helper = () => true;
369
+ ```
370
+
371
+ Let me know if you need anything else!
372
+ ````
373
+
374
+ Running `stdin-glob apply response.md` on the above will correctly extract and apply only the two code blocks, ignoring all the surrounding text.
375
+
376
+ #### Full workflow example
377
+
378
+ A typical workflow when working with LLMs:
379
+
380
+ ```bash
381
+ # 1. Gather context from your project
382
+ stdin-glob "src/**/*.ts" --copy
383
+
384
+ # 2. Paste into your LLM and ask for modifications
385
+
386
+ # 3. Save the LLM response to a file
387
+ # (paste from clipboard)
388
+ # pbpaste > response.md
389
+
390
+ # 4. Preview what will change
391
+ stdin-glob apply response.md --dry-run
392
+
393
+ # 5. Apply the changes
394
+ stdin-glob apply response.md
395
+ ```
396
+
397
+ Output after applying:
398
+
399
+ ```
400
+ Found 2 file(s) to process:
401
+
402
+ [OK] src/index.ts
403
+ [OK] src/utils/new.js
404
+
405
+ Results:
406
+ Created: 1
407
+ + src/utils/new.js
408
+ Updated: 1
409
+ ~ src/index.ts
410
+ ```
@@ -0,0 +1,26 @@
1
+ export interface ParsedFile {
2
+ filePath: string;
3
+ content: string;
4
+ isBinary: boolean;
5
+ }
6
+ /**
7
+ * Parse a document containing code blocks and extract file paths and contents
8
+ * Handles varying numbers of backticks and ignores "noise" between blocks
9
+ */
10
+ export declare const parseCodeBlocks: (input: string) => ParsedFile[];
11
+ export interface ApplyResult {
12
+ created: string[];
13
+ updated: string[];
14
+ skipped: string[];
15
+ }
16
+ /**
17
+ * Apply parsed files
18
+ */
19
+ export declare const applyFiles: (files: ParsedFile[], baseDir?: string) => Promise<ApplyResult>;
20
+ /**
21
+ *
22
+ */
23
+ export declare const applyFromFile: (inputPath: string, baseDir?: string) => Promise<ApplyResult & {
24
+ parsed: ParsedFile[];
25
+ }>;
26
+ //# sourceMappingURL=apply.d.ts.map
package/dist/apply.js ADDED
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.applyFromFile = exports.applyFiles = exports.parseCodeBlocks = void 0;
7
+ const promises_1 = require("fs/promises");
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * Parse a document containing code blocks and extract file paths and contents
11
+ * Handles varying numbers of backticks and ignores "noise" between blocks
12
+ */
13
+ const parseCodeBlocks = (input) => {
14
+ const files = [];
15
+ const lines = input.split('\n');
16
+ let i = 0;
17
+ while (i < lines.length) {
18
+ const line = lines[i];
19
+ // Check if this line is a code block opening (3+ backticks followed by optional extension)
20
+ const backtickMatch = line.match(/^(`{3,})(\S*)\s*$/);
21
+ if (backtickMatch) {
22
+ const numBackticks = backtickMatch[1].length;
23
+ //
24
+ const closingLine = '`'.repeat(numBackticks);
25
+ //
26
+ let j = i + 1;
27
+ while (j < lines.length && lines[j].trim() !== closingLine) {
28
+ j++;
29
+ }
30
+ if (j < lines.length) {
31
+ // found closing!! - extract content between opening and closing
32
+ const contentLines = lines.slice(i + 1, j);
33
+ // First line should be file path comment
34
+ // For example: `// src/some/thinghs.java`
35
+ if (contentLines.length > 0) {
36
+ const firstLine = contentLines[0].trim();
37
+ if (firstLine.startsWith('// ')) {
38
+ const filePath = firstLine.slice(3).trim();
39
+ // Content is everything after the first line
40
+ const content = contentLines.slice(1).join('\n');
41
+ // it's a binary file marker? I known't
42
+ const isBinary = content.includes('[BINARY FILE]');
43
+ files.push({ filePath, content, isBinary });
44
+ }
45
+ }
46
+ i = j + 1;
47
+ continue;
48
+ }
49
+ }
50
+ i++;
51
+ }
52
+ return files;
53
+ };
54
+ exports.parseCodeBlocks = parseCodeBlocks;
55
+ /**
56
+ * Apply parsed files
57
+ */
58
+ const applyFiles = async (files, baseDir) => {
59
+ const created = [];
60
+ const updated = [];
61
+ const skipped = [];
62
+ for (const file of files) {
63
+ const fullPath = baseDir
64
+ ? path_1.default.join(baseDir, file.filePath)
65
+ : file.filePath;
66
+ const dir = path_1.default.dirname(fullPath);
67
+ if (file.isBinary) {
68
+ // is binary file, skip then
69
+ skipped.push(fullPath);
70
+ continue;
71
+ }
72
+ // if it doesn't exist:
73
+ await (0, promises_1.mkdir)(dir, { recursive: true });
74
+ let fileExists = false;
75
+ try {
76
+ await (0, promises_1.stat)(fullPath);
77
+ fileExists = true;
78
+ }
79
+ catch {
80
+ // File doesn't exist
81
+ }
82
+ await (0, promises_1.writeFile)(fullPath, file.content, 'utf-8');
83
+ if (fileExists) {
84
+ updated.push(fullPath);
85
+ }
86
+ else {
87
+ created.push(fullPath);
88
+ }
89
+ }
90
+ return { created, updated, skipped };
91
+ };
92
+ exports.applyFiles = applyFiles;
93
+ /**
94
+ *
95
+ */
96
+ const applyFromFile = async (inputPath, baseDir) => {
97
+ const content = await (0, promises_1.readFile)(inputPath, 'utf-8');
98
+ const parsed = (0, exports.parseCodeBlocks)(content);
99
+ const result = await (0, exports.applyFiles)(parsed, baseDir);
100
+ return { ...result, parsed };
101
+ };
102
+ exports.applyFromFile = applyFromFile;
103
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=apply.test.d.ts.map
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const promises_1 = require("fs/promises");
5
+ const apply_1 = require("./apply");
6
+ vitest_1.vi.mock('fs/promises');
7
+ (0, vitest_1.describe)('parseCodeBlocks', () => {
8
+ const bt = (n) => '`'.repeat(n);
9
+ (0, vitest_1.it)('parses basic code blocks', () => {
10
+ const input = `${bt(3)}ts\n// src/index.ts\nconsole.log('hello');\n${bt(3)}`;
11
+ const files = (0, apply_1.parseCodeBlocks)(input);
12
+ (0, vitest_1.expect)(files).toHaveLength(1);
13
+ (0, vitest_1.expect)(files[0].filePath).toBe('src/index.ts');
14
+ (0, vitest_1.expect)(files[0].content).toBe("console.log('hello');");
15
+ (0, vitest_1.expect)(files[0].isBinary).toBe(false);
16
+ });
17
+ (0, vitest_1.it)('handles noise between blocks', () => {
18
+ const input = `Some noise here bla bla bla
19
+ ${bt(3)}txt
20
+ // file1.txt
21
+ content1
22
+ ${bt(3)}
23
+ More noise and random text, jojojo
24
+ ${bt(3)}js
25
+ // file2.js
26
+ content2
27
+ ${bt(3)}
28
+ Even more noise and webos`;
29
+ const files = (0, apply_1.parseCodeBlocks)(input);
30
+ (0, vitest_1.expect)(files).toHaveLength(2);
31
+ (0, vitest_1.expect)(files[0].filePath).toBe('file1.txt');
32
+ (0, vitest_1.expect)(files[0].content).toBe('content1');
33
+ (0, vitest_1.expect)(files[1].filePath).toBe('file2.js');
34
+ (0, vitest_1.expect)(files[1].content).toBe('content2');
35
+ });
36
+ (0, vitest_1.it)('handles different numbers of backticks', () => {
37
+ const input = `${bt(3)}ts\n// file1.ts\ncontent1\n${bt(3)}
38
+ ${bt(4)}js\n// file2.js\ncontent2 with ${bt(3)}backticks${bt(3)} inside\n${bt(4)}
39
+ ${bt(5)}py\n// file3.py\ncontent3\n${bt(5)}`;
40
+ const files = (0, apply_1.parseCodeBlocks)(input);
41
+ (0, vitest_1.expect)(files).toHaveLength(3);
42
+ (0, vitest_1.expect)(files[0].filePath).toBe('file1.ts');
43
+ (0, vitest_1.expect)(files[1].filePath).toBe('file2.js');
44
+ (0, vitest_1.expect)(files[1].content).toBe('content2 with ```backticks``` inside');
45
+ (0, vitest_1.expect)(files[2].filePath).toBe('file3.py');
46
+ });
47
+ (0, vitest_1.it)('requires matching backtick counts for opening and closing', () => {
48
+ const input = `${bt(4)}ts\n// file.ts\ncontent\n${bt(3)}`;
49
+ const files = (0, apply_1.parseCodeBlocks)(input);
50
+ (0, vitest_1.expect)(files).toHaveLength(0);
51
+ });
52
+ (0, vitest_1.it)('does not confuse shorter backtick sequence as closing for longer opening', () => {
53
+ const input = `${bt(5)}md
54
+ // readme.md
55
+ Here is some ${bt(3)}code${bt(3)}
56
+ And more ${bt(4)}more code${bt(4)}
57
+ ${bt(5)}`;
58
+ const files = (0, apply_1.parseCodeBlocks)(input);
59
+ (0, vitest_1.expect)(files).toHaveLength(1);
60
+ (0, vitest_1.expect)(files[0].content).toBe('Here is some ```code```\nAnd more ````more code````');
61
+ });
62
+ (0, vitest_1.it)('identifies binary files', () => {
63
+ const input = `${bt(3)}png\n// image.png\n// [BINARY FILE] - Size: 1.000 MB\n${bt(3)}`;
64
+ const files = (0, apply_1.parseCodeBlocks)(input);
65
+ (0, vitest_1.expect)(files).toHaveLength(1);
66
+ (0, vitest_1.expect)(files[0].isBinary).toBe(true);
67
+ });
68
+ (0, vitest_1.it)('identifies truncated files', () => {
69
+ const input = `${bt(3)}ts\n// file.ts\ncontent\n// ... (100 more lines truncated)\n${bt(3)}`;
70
+ const files = (0, apply_1.parseCodeBlocks)(input);
71
+ (0, vitest_1.expect)(files).toHaveLength(1);
72
+ });
73
+ (0, vitest_1.it)('handles empty content after file path', () => {
74
+ const input = `${bt(3)}ts\n// empty.ts\n${bt(3)}`;
75
+ const files = (0, apply_1.parseCodeBlocks)(input);
76
+ (0, vitest_1.expect)(files).toHaveLength(1);
77
+ (0, vitest_1.expect)(files[0].content).toBe('');
78
+ });
79
+ (0, vitest_1.it)('ignores blocks without file path comment', () => {
80
+ const input = `${bt(3)}ts\njust code without path\n${bt(3)}`;
81
+ const files = (0, apply_1.parseCodeBlocks)(input);
82
+ (0, vitest_1.expect)(files).toHaveLength(0);
83
+ });
84
+ (0, vitest_1.it)('ignores blocks where first line does not start with //', () => {
85
+ const input = `${bt(3)}ts\nsrc/index.ts\nconsole.log('hello');\n${bt(3)}`;
86
+ const files = (0, apply_1.parseCodeBlocks)(input);
87
+ (0, vitest_1.expect)(files).toHaveLength(0);
88
+ });
89
+ (0, vitest_1.it)('handles unclosed code blocks gracefully', () => {
90
+ const input = `${bt(3)}ts\n// file.ts\ncontent without closing`;
91
+ const files = (0, apply_1.parseCodeBlocks)(input);
92
+ (0, vitest_1.expect)(files).toHaveLength(0);
93
+ });
94
+ (0, vitest_1.it)('handles multiline content correctly', () => {
95
+ const input = `${bt(3)}ts
96
+ // src/main.ts
97
+ import { foo } from './bar';
98
+
99
+ const x = 1;
100
+ const y = 2;
101
+
102
+ export function add(): number {
103
+ return x + y;
104
+ }
105
+ ${bt(3)}`;
106
+ const files = (0, apply_1.parseCodeBlocks)(input);
107
+ (0, vitest_1.expect)(files).toHaveLength(1);
108
+ const expectedContent = `import { foo } from './bar';
109
+
110
+ const x = 1;
111
+ const y = 2;
112
+
113
+ export function add(): number {
114
+ return x + y;
115
+ }`;
116
+ (0, vitest_1.expect)(files[0].content).toBe(expectedContent);
117
+ });
118
+ (0, vitest_1.it)('handles file paths with spaces', () => {
119
+ const input = `${bt(3)}txt\n// path with spaces/file name.txt\ncontent\n${bt(3)}`;
120
+ const files = (0, apply_1.parseCodeBlocks)(input);
121
+ (0, vitest_1.expect)(files[0].filePath).toBe('path with spaces/file name.txt');
122
+ });
123
+ (0, vitest_1.it)('handles extension without content on same line', () => {
124
+ const input = `${bt(3)}js\n// app.js\nconsole.log('test');\n${bt(3)}`;
125
+ const files = (0, apply_1.parseCodeBlocks)(input);
126
+ (0, vitest_1.expect)(files).toHaveLength(1);
127
+ });
128
+ (0, vitest_1.it)('handles no extension (just backticks)', () => {
129
+ const input = `${bt(3)}\n// Makefile\nall:\n\techo hello\n${bt(3)}`;
130
+ const files = (0, apply_1.parseCodeBlocks)(input);
131
+ (0, vitest_1.expect)(files).toHaveLength(1);
132
+ (0, vitest_1.expect)(files[0].filePath).toBe('Makefile');
133
+ (0, vitest_1.expect)(files[0].content).toBe('all:\n\techo hello');
134
+ });
135
+ (0, vitest_1.it)('handles consecutive code blocks without noise', () => {
136
+ const input = `${bt(3)}a\n// file1.a\ncontent1\n${bt(3)}
137
+ ${bt(3)}b\n// file2.b\ncontent2\n${bt(3)}
138
+ ${bt(3)}c\n// file3.c\ncontent3\n${bt(3)}`;
139
+ const files = (0, apply_1.parseCodeBlocks)(input);
140
+ (0, vitest_1.expect)(files).toHaveLength(3);
141
+ });
142
+ (0, vitest_1.it)('example from the requirements', () => {
143
+ const input = `Aqui hay texto de ruido entre cada bloque de contenido
144
+ ${bt(3)}perl
145
+ // src/webo.perl
146
+ aqui hay contenido
147
+ ${bt(3)}
148
+ Aqui hay texto de ruido entre cada bloque de contenido
149
+ ${bt(3)}txt
150
+ // src/ejemplo.txt
151
+ Aqui nuevamente contenido correcto
152
+ ${bt(3)}`;
153
+ const files = (0, apply_1.parseCodeBlocks)(input);
154
+ (0, vitest_1.expect)(files).toHaveLength(2);
155
+ (0, vitest_1.expect)(files[0].filePath).toBe('src/webo.perl');
156
+ (0, vitest_1.expect)(files[0].content).toBe('aqui hay contenido');
157
+ (0, vitest_1.expect)(files[1].filePath).toBe('src/ejemplo.txt');
158
+ (0, vitest_1.expect)(files[1].content).toBe('Aqui nuevamente contenido correcto');
159
+ });
160
+ });
161
+ (0, vitest_1.describe)('applyFiles', () => {
162
+ (0, vitest_1.beforeEach)(() => {
163
+ vitest_1.vi.resetAllMocks();
164
+ });
165
+ (0, vitest_1.it)('creates new files', async () => {
166
+ vitest_1.vi.mocked(promises_1.stat).mockRejectedValue(new Error('Not found'));
167
+ vitest_1.vi.mocked(promises_1.mkdir).mockResolvedValue(undefined);
168
+ vitest_1.vi.mocked(promises_1.writeFile).mockResolvedValue(undefined);
169
+ const files = [
170
+ {
171
+ filePath: 'src/index.ts',
172
+ content: 'console.log("hello");',
173
+ isBinary: false,
174
+ isTruncated: false,
175
+ },
176
+ ];
177
+ const result = await (0, apply_1.applyFiles)(files);
178
+ (0, vitest_1.expect)(result.created).toHaveLength(1);
179
+ (0, vitest_1.expect)(result.created[0]).toContain('src/index.ts');
180
+ (0, vitest_1.expect)(result.updated).toHaveLength(0);
181
+ (0, vitest_1.expect)(promises_1.writeFile).toHaveBeenCalled();
182
+ });
183
+ (0, vitest_1.it)('updates existing files', async () => {
184
+ vitest_1.vi.mocked(promises_1.stat).mockResolvedValue({});
185
+ vitest_1.vi.mocked(promises_1.mkdir).mockResolvedValue(undefined);
186
+ vitest_1.vi.mocked(promises_1.writeFile).mockResolvedValue(undefined);
187
+ const files = [
188
+ {
189
+ filePath: 'src/index.ts',
190
+ content: 'console.log("updated");',
191
+ isBinary: false,
192
+ isTruncated: false,
193
+ },
194
+ ];
195
+ const result = await (0, apply_1.applyFiles)(files);
196
+ (0, vitest_1.expect)(result.updated).toHaveLength(1);
197
+ (0, vitest_1.expect)(result.created).toHaveLength(0);
198
+ });
199
+ (0, vitest_1.it)('skips binary files', async () => {
200
+ const files = [
201
+ {
202
+ filePath: 'image.png',
203
+ content: '// [BINARY FILE]',
204
+ isBinary: true,
205
+ isTruncated: false,
206
+ },
207
+ ];
208
+ const result = await (0, apply_1.applyFiles)(files);
209
+ (0, vitest_1.expect)(result.skipped).toHaveLength(1);
210
+ (0, vitest_1.expect)(result.skipped[0]).toContain('image.png');
211
+ (0, vitest_1.expect)(promises_1.writeFile).not.toHaveBeenCalled();
212
+ });
213
+ (0, vitest_1.it)('tracks truncated files', async () => {
214
+ vitest_1.vi.mocked(promises_1.stat).mockRejectedValue(new Error('Not found'));
215
+ vitest_1.vi.mocked(promises_1.mkdir).mockResolvedValue(undefined);
216
+ vitest_1.vi.mocked(promises_1.writeFile).mockResolvedValue(undefined);
217
+ const files = [
218
+ {
219
+ filePath: 'large.ts',
220
+ content: 'line1\n// ... (100 more lines truncated)',
221
+ isBinary: false,
222
+ isTruncated: true,
223
+ },
224
+ ];
225
+ const result = await (0, apply_1.applyFiles)(files);
226
+ (0, vitest_1.expect)(result.created).toHaveLength(1);
227
+ });
228
+ (0, vitest_1.it)('respects baseDir option', async () => {
229
+ vitest_1.vi.mocked(promises_1.stat).mockRejectedValue(new Error('Not found'));
230
+ vitest_1.vi.mocked(promises_1.mkdir).mockResolvedValue(undefined);
231
+ vitest_1.vi.mocked(promises_1.writeFile).mockResolvedValue(undefined);
232
+ const files = [
233
+ {
234
+ filePath: 'src/index.ts',
235
+ content: 'content',
236
+ isBinary: false,
237
+ isTruncated: false,
238
+ },
239
+ ];
240
+ await (0, apply_1.applyFiles)(files, '/custom/path');
241
+ (0, vitest_1.expect)(promises_1.mkdir).toHaveBeenCalledWith('/custom/path/src', { recursive: true });
242
+ (0, vitest_1.expect)(promises_1.writeFile).toHaveBeenCalledWith('/custom/path/src/index.ts', 'content', 'utf-8');
243
+ });
244
+ (0, vitest_1.it)('creates nested directories', async () => {
245
+ vitest_1.vi.mocked(promises_1.stat).mockRejectedValue(new Error('Not found'));
246
+ vitest_1.vi.mocked(promises_1.mkdir).mockResolvedValue(undefined);
247
+ vitest_1.vi.mocked(promises_1.writeFile).mockResolvedValue(undefined);
248
+ const files = [
249
+ {
250
+ filePath: 'src/deep/nested/file.ts',
251
+ content: 'content',
252
+ isBinary: false,
253
+ isTruncated: false,
254
+ },
255
+ ];
256
+ await (0, apply_1.applyFiles)(files);
257
+ (0, vitest_1.expect)(promises_1.mkdir).toHaveBeenCalledWith('src/deep/nested', { recursive: true });
258
+ });
259
+ (0, vitest_1.it)('handles multiple files with mixed states', async () => {
260
+ vitest_1.vi.mocked(promises_1.stat)
261
+ .mockRejectedValueOnce(new Error('Not found'))
262
+ .mockResolvedValueOnce({})
263
+ .mockRejectedValueOnce(new Error('Not found'));
264
+ vitest_1.vi.mocked(promises_1.mkdir).mockResolvedValue(undefined);
265
+ vitest_1.vi.mocked(promises_1.writeFile).mockResolvedValue(undefined);
266
+ const files = [
267
+ {
268
+ filePath: 'new.ts',
269
+ content: 'new',
270
+ isBinary: false,
271
+ isTruncated: false,
272
+ },
273
+ {
274
+ filePath: 'existing.ts',
275
+ content: 'updated',
276
+ isBinary: false,
277
+ isTruncated: false,
278
+ },
279
+ {
280
+ filePath: 'image.png',
281
+ content: '// [BINARY FILE]',
282
+ isBinary: true,
283
+ isTruncated: false,
284
+ },
285
+ ];
286
+ const result = await (0, apply_1.applyFiles)(files);
287
+ (0, vitest_1.expect)(result.created).toHaveLength(1);
288
+ (0, vitest_1.expect)(result.updated).toHaveLength(1);
289
+ (0, vitest_1.expect)(result.skipped).toHaveLength(1);
290
+ });
291
+ });
292
+ (0, vitest_1.describe)('applyFromFile', () => {
293
+ (0, vitest_1.beforeEach)(() => {
294
+ vitest_1.vi.resetAllMocks();
295
+ });
296
+ (0, vitest_1.it)('reads file and applies parsed content', async () => {
297
+ const inputContent = '```ts\n// test.ts\nconsole.log("test");\n```';
298
+ vitest_1.vi.mocked(promises_1.readFile).mockResolvedValue(inputContent);
299
+ vitest_1.vi.mocked(promises_1.stat).mockRejectedValue(new Error('Not found'));
300
+ vitest_1.vi.mocked(promises_1.mkdir).mockResolvedValue(undefined);
301
+ vitest_1.vi.mocked(promises_1.writeFile).mockResolvedValue(undefined);
302
+ const result = await (0, apply_1.applyFromFile)('input.md');
303
+ (0, vitest_1.expect)(result.parsed).toHaveLength(1);
304
+ (0, vitest_1.expect)(result.created).toHaveLength(1);
305
+ (0, vitest_1.expect)(promises_1.readFile).toHaveBeenCalledWith('input.md', 'utf-8');
306
+ });
307
+ (0, vitest_1.it)('passes baseDir to applyFiles', async () => {
308
+ const inputContent = '```ts\n// test.ts\ncontent\n```';
309
+ vitest_1.vi.mocked(promises_1.readFile).mockResolvedValue(inputContent);
310
+ vitest_1.vi.mocked(promises_1.stat).mockRejectedValue(new Error('Not found'));
311
+ vitest_1.vi.mocked(promises_1.mkdir).mockResolvedValue(undefined);
312
+ vitest_1.vi.mocked(promises_1.writeFile).mockResolvedValue(undefined);
313
+ await (0, apply_1.applyFromFile)('input.md', '/output');
314
+ (0, vitest_1.expect)(promises_1.mkdir).toHaveBeenCalledWith('/output', { recursive: true });
315
+ });
316
+ });
317
+ //# sourceMappingURL=apply.test.js.map