Skip to content

Commit

Permalink
add support for inline ignore patters in templates
Browse files Browse the repository at this point in the history
  • Loading branch information
yielder committed Oct 22, 2024
1 parent df91035 commit d02e329
Show file tree
Hide file tree
Showing 12 changed files with 412 additions and 192 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ It offers two main functionalities:
- Support for Git repositories and respect for `.gitignore`
- Built-in token counting
- Easy-to-use CLI utility
- Inline ignore patterns for fine-grained control over included files

## Usage

Expand Down Expand Up @@ -67,6 +68,16 @@ Review the entire 'utils' directory:
{{@utils}}
```
Review the 'src' directory, excluding .test.js files:
```
{{@src:-*.test.js}}
```
Review all files in the current directory, excluding markdown files and the 'subdir' directory:
```
{{@.:-*.md,-**/subdir/**}}
```
[new feature description / instructions for the LLM]
````

Expand All @@ -78,6 +89,26 @@ copa template prompt.txt
copa t prompt.txt
```

## Inline Ignore Patterns

You can use inline ignore patterns to exclude specific files or patterns within a directory reference:

```
{{@directory:-pattern1,-pattern2,...}}
```

Examples:
- `{{@src:-*.test.js}}` includes all files in the 'src' directory except for files ending with '.test.js'
- `{{@.:-*.md,-**/subdir/**}}` includes all files in the current directory, excluding markdown files and the 'subdir' directory
- `{{@.:-**/*dir/**,-*.y*}}` excludes all files in any directory ending with 'dir' and all files with extensions starting with 'y'

Ignore patterns support:
- File extensions: `-*.js`
- Specific files: `-file.txt`
- Directories: `-**/dirname/**`
- Glob patterns: `-**/*.test.js`
- Hidden files and directories: `-.*`

## Commands

- `t, template <file>`: Process a template file
Expand Down Expand Up @@ -118,6 +149,7 @@ And its test:
1. Use relative paths in templates for better portability
2. Create a "prompts" directory in project root
3. Create a library of templates for common tasks
4. Use inline ignore patterns for fine-grained control over included files

## Global Configuration

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"clipboardy": "^4.0.0",
"commander": "^12.1.0",
"glob": "^11.0.0",
"minimatch": "^10.0.1",
"simple-git": "^3.25.0"
}
}
68 changes: 68 additions & 0 deletions prompts/failed_test.copa
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
I'm working on an OSS project

This is the source of it:

```
{{@../src}}
```

And tests:

```
{{@../tests}}
```

Some tests are failing, can you investigate?



● CoPa Functionality › filterFiles › excludes files based on command line options

expect(received).toBe(expected) // Object.is equality

Expected: 2
Received: 6

39 | test('excludes files based on command line options', async () => {
40 | const files = await filterFiles({exclude: 'js,md'}, testDir);
> 41 | expect(files.length).toBe(2);
| ^
42 | expect(files.sort()).toEqual([
43 | 'file3.yml',
44 | path.join('subdir', 'file6.yml')

at Object.<anonymous> (tests/sanity.test.ts:41:34)

● CoPa Functionality › filterFiles › excludes files based on single extension

expect(received).toBe(expected) // Object.is equality

Expected: 4
Received: 6

48 | test('excludes files based on single extension', async () => {
49 | const files = await filterFiles({exclude: 'yml'}, testDir);
> 50 | expect(files.length).toBe(4);
| ^
51 | expect(files.sort()).toEqual([
52 | 'file1.js',
53 | 'file2.md',

at Object.<anonymous> (tests/sanity.test.ts:50:34)

● hidden folders › filterFiles › excludes hidden folder and its files with glob pattern

expect(received).toBe(expected) // Object.is equality

Expected: 6
Received: 7

131 | test('excludes hidden folder and its files with glob pattern', async () => {
132 | const files = await filterFiles({ exclude: '.*' }, testDir);
> 133 | expect(files.length).toBe(6);
| ^
134 | expect(files.sort()).toEqual([
135 | 'file1.js',
136 | 'file2.md',

at Object.<anonymous> (tests/sanity.test.ts:133:34)
19 changes: 19 additions & 0 deletions prompts/new_feature.copa
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
I'm working on an OSS project

```
{{@../README.md}}
```

This is the source of it:

```
{{@../src}}
```

And tests

```
{{@../tests}}
```

<<feature description>>
19 changes: 19 additions & 0 deletions prompts/update_readme.copa
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
I'm working on an OSS project

````
{{@../README.md}}
````

This is the source code:

```
{{@../src}}
```

And tests:

```
{{@../tests}}
```

Can you help me update the README to be inline with the latest code and the examples seen in tests?
13 changes: 7 additions & 6 deletions src/copa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async function copyFilesToClipboard(source: {
const tokensPerFile: { [_: string]: number } = {};
let content = '';

for (const file of files) {
for (const file of files ?? []) {
try {
const fileContent = await fs.readFile(file, 'utf-8');
const fileSection = `===== ${file} =====\n${fileContent}\n\n`;
Expand All @@ -51,12 +51,12 @@ async function copyFilesToClipboard(source: {
}

await copyToClipboard(content);
console.log(`${files.length} files from ${source.directory ? source.directory : 'files list'} have been copied to the clipboard.`);
console.log(`${files?.length} files from ${source.directory ? source.directory : 'files list'} have been copied to the clipboard.`);
console.log(`Total tokens: ${totalTokens}`);

if (options.verbose) {
console.log('Copied files:');
files.forEach(file => console.log(`${file} [${tokensPerFile[file]}]`));
files?.forEach(file => console.log(`${file} [${tokensPerFile[file]}]`));
}
} catch (error) {
console.error('Error copying files to clipboard:', error);
Expand All @@ -66,19 +66,20 @@ async function copyFilesToClipboard(source: {

async function handleTemplateCommand(file: string, options: { verbose?: boolean }) {
try {
const globalExclude = await readGlobalConfig();
const {
content,
warnings,
includedFiles,
totalTokens
} = await processPromptFile(file);
} = await processPromptFile(file, globalExclude);

await copyToClipboard(content);
await copyToClipboard(content);
console.log(`Processed template from ${file} has been copied to the clipboard.`);
console.log(`Total tokens: ${totalTokens}`);

if (warnings.length > 0) {
console.warn('Warnings:', warnings.join('\n'));
console.warn(warnings.join('\n'));
}

if (options.verbose && includedFiles) {
Expand Down
77 changes: 44 additions & 33 deletions src/filterFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,73 @@ import {Options} from "./options";
import {simpleGit} from "simple-git";
import {glob} from "glob";
import path from "path";
import {minimatch} from "minimatch";
import fs from "fs/promises";

const printDebug = false;

function debug(...args: any[]) {
if (printDebug) {
console.debug(...args);
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath); // Check if file exists
return true;
} catch (error) {
return false;
}
}

export async function filterFiles(options: Options, directory: string, globalExclude?: string) {
export async function filterFiles(options: Options, pathToProcess: string, globalExclude?: string): Promise<string[] | undefined> {
const userExclude = options.exclude || '';
const combinedExclude = [globalExclude ?? '', userExclude].filter(Boolean).join(',');
const excludePatterns = combinedExclude.split(',').filter(Boolean);
const excludePatterns = combinedExclude.split(',')
.filter(Boolean)
.map(exPath => exPath.startsWith('-') ? exPath.slice(1) : exPath);

debug('Exclude patterns:', excludePatterns);

let allFiles: string[];

try {
const git = simpleGit(directory);
const isGitRepo = await git.checkIsRepo();
const foundFile = await fileExists(pathToProcess);
if (!foundFile) {
console.warn(`The specified path does not exist: ${pathToProcess}`);
return undefined;
}

const stats = await fs.stat(pathToProcess);

if (isGitRepo) {
debug('Using Git to list files');
const gitFiles = await git.raw(['ls-files', '-co', '--exclude-standard', directory]);
allFiles = gitFiles.split('\n').filter(Boolean);
if (stats.isDirectory()) {
const git = simpleGit(pathToProcess);
const isGitRepo = await git.checkIsRepo();

if (isGitRepo) {
const gitFiles = await git.raw(['ls-files', '-co', '--exclude-standard', pathToProcess]);
allFiles = gitFiles.split('\n').filter(Boolean);
} else {
const globPattern = path.join(pathToProcess, '**/*');
allFiles = await glob(globPattern, {dot: true, nodir: true});
}
} else {
debug('Using glob to list files');
const globPattern = path.join(directory, '**/*');
allFiles = await glob(globPattern, {dot: true, nodir: true});
allFiles = [pathToProcess];
}

// Convert to relative paths
allFiles = allFiles.map(file => path.relative(directory, file));

debug('Total files found:', allFiles.length);
allFiles = allFiles.map(file => {
return path.resolve(pathToProcess, file)
});

// Filter files
const filteredFiles = allFiles.filter(file => {
return !excludePatterns.some(pattern => {
if (glob.hasMagic(pattern)) {
const matchers = glob.sync(pattern, {cwd: directory})
debug(`Magic pattern matching [${pattern}] [${file}] match=[${glob.sync(pattern, {cwd: directory}).includes(file)}] matchers=[${matchers.join(',')}]`)
return matchers.includes(file) || file.split(path.sep).some(_ => matchers.includes(_));
return allFiles.filter(file => {
const isExcluded = excludePatterns.some(pattern => {
if (pattern === '.*') {
return file.split(path.sep).some(part => part.startsWith('.'));
} else if (pattern.includes('*') || pattern.includes('/')) {
return minimatch(file, pattern, {dot: true, matchBase: true});
} else {
debug(`Normal pattern matching [${pattern}] [${file}] match=[${file.endsWith(pattern) || file.split(path.sep).includes(pattern)}]`)
return file.endsWith(pattern) || file.split(path.sep).includes(pattern);
return pattern.endsWith(path.extname(file)) || file.startsWith(pattern);
}
});
if (isExcluded) {
}
return !isExcluded;
});

debug('Files after filtering:', filteredFiles.length);

return filteredFiles;
} catch (error: any) {
console.error('Error in filterFiles:', error.message);
throw new Error(`Error listing or filtering files: ${error.message}`);
}
}
1 change: 0 additions & 1 deletion src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ export interface Options {
exclude?: string;
verbose?: boolean;
file?: string[];
read?: string;
}
Loading

0 comments on commit d02e329

Please sign in to comment.