stdin-glob 1.4.0 → 1.8.1
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 +139 -1
- package/bin/stdin-glob.cjs +3 -0
- package/dist/apply.d.ts +26 -0
- package/dist/apply.js +103 -0
- package/dist/apply.test.d.ts +2 -0
- package/dist/apply.test.js +317 -0
- package/dist/index.js +2 -161
- package/dist/main.d.ts +18 -0
- package/dist/main.js +249 -0
- package/dist/main.test.d.ts +2 -0
- package/dist/main.test.js +133 -0
- package/dist/utils/binary.test.d.ts +2 -0
- package/dist/utils/binary.test.js +33 -0
- package/dist/utils/gitignore.test.d.ts +2 -0
- package/dist/utils/gitignore.test.js +62 -0
- package/dist/utils/pipes.test.d.ts +2 -0
- package/dist/utils/pipes.test.js +40 -0
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ This pipes all relevant TypeScript/TSX files directly into my clipboard, ready t
|
|
|
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
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)
|
|
35
36
|
- Written in TypeScript
|
|
36
37
|
|
|
37
38
|
## Installation
|
|
@@ -65,6 +66,44 @@ stdin-glob [options] [patterns...]
|
|
|
65
66
|
| ---------- | ------------------------------------------ |
|
|
66
67
|
| `patterns` | Glob patterns to match files (one or more) |
|
|
67
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
|
+
|
|
68
107
|
## Pattern Syntax
|
|
69
108
|
|
|
70
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).
|
|
@@ -254,7 +293,6 @@ stdin-glob "src/**/*.ts" --content
|
|
|
254
293
|
stdin-glob "src/**/*.ts" --copy
|
|
255
294
|
```
|
|
256
295
|
|
|
257
|
-
|
|
258
296
|
### .gitignore Support
|
|
259
297
|
|
|
260
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.
|
|
@@ -270,3 +308,103 @@ stdin-glob "dist/**/*.js" --no-gitignore
|
|
|
270
308
|
```
|
|
271
309
|
|
|
272
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
|
+
```
|
package/dist/apply.d.ts
ADDED
|
@@ -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,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
|