Skip to content

Commit

Permalink
add tests + fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
yielder committed Jul 31, 2024
1 parent 1952d66 commit 32764a4
Show file tree
Hide file tree
Showing 17 changed files with 3,542 additions and 638 deletions.
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node"
};
15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,25 @@
"bin": "./dist/copa.js",
"scripts": {
"build": "tsc",
"start": "node dist/copa.js"
"start": "node dist/copa.js",
"test": "jest"
},
"author": "Roman Landenband",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.0.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.3",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
},
"dependencies": {
"@dqbd/tiktoken": "^1.0.15",
"@types/jsdom": "^21.1.7",
"axios": "^1.7.2",
"clipboardy": "^4.0.0",
"commander": "^12.1.0",
"dom-to-semantic-markdown": "1.1.2",
"glob": "^11.0.0",
"jsdom": "^24.1.1",
"simple-git": "^3.25.0",
"ts-node": "^10.9.2"
"simple-git": "^3.25.0"
}
}
86 changes: 17 additions & 69 deletions src/copa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,10 @@

import {program} from 'commander';
import * as fs from 'fs/promises';
import * as path from 'path';
import {simpleGit} from 'simple-git';
import {glob} from 'glob';
import * as os from 'os';
import {encoding_for_model} from '@dqbd/tiktoken';

interface Options {
exclude?: string;
verbose?: boolean;
file?: string;
}

async function readGlobalConfig(): Promise<string> {
const configPath = path.join(os.homedir(), '.copa');
try {
const configContent = await fs.readFile(configPath, 'utf-8');
const ignoreLine = configContent.split('\n').find(line => line.startsWith('ignore:'));
if (ignoreLine) {
return ignoreLine.split(':')[1].trim();
}
} catch (error) {
// If the file doesn't exist or can't be read, return an empty string
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
console.warn('Warning: Unable to read global config file:', error);
}
}
return '';
}
import {Options} from "./options";
import {readGlobalConfig} from "./readGlobalConfig";
import {filterFiles} from "./filterFiles";

function countTokens(input: string): number {
const tokenize = encoding_for_model('gpt-4');
Expand Down Expand Up @@ -63,53 +39,24 @@ async function readSingleFile(filePath: string): Promise<void> {
async function copyFilesToClipboard(directory: string, options: Options): Promise<void> {
const clipboardy = await import('clipboardy');

const globalExclude = await readGlobalConfig();
const userExclude = options.exclude || '';
const combinedExclude = [globalExclude, userExclude].filter(Boolean).join(',');

const excludePatterns = combinedExclude ? combinedExclude.split(',').map(ext => `**/*.${ext}`) : [];

let files: string[];

try {
const git = simpleGit(directory);
const isGitRepo = await git.checkIsRepo();

if (isGitRepo) {
const gitFiles = await git.raw(['ls-files', directory]);
files = gitFiles.split('\n').filter(Boolean);

if (excludePatterns.length > 0) {
files = files.filter(file => !excludePatterns.some(pattern =>
file.endsWith(pattern) ||
glob.hasMagic(pattern)
? glob.sync(pattern, {cwd: directory}).includes(file)
: false));
const globalExclude = await readGlobalConfig();
let files = await filterFiles(options, directory, globalExclude);
let totalTokens = 0;
let content = '';

for (const file of files) {
try {
const fileContent = await fs.readFile(file, 'utf-8');
const fileSection = `===== ${file} =====\n${fileContent}\n\n`;
content += fileSection;
totalTokens += countTokens(fileSection);
} catch (error) {
console.error(`Error reading file ${file}:`, error);
}
} else {
const globPattern = path.join(directory, '**/*');
files = await glob(globPattern, {nodir: true, ignore: excludePatterns});
}
} catch (error) {
console.error('Error listing files:', error);
process.exit(1);
}

let content = '';
let totalTokens = 0;

for (const file of files) {
try {
const fileContent = await fs.readFile(file, 'utf-8');
const fileSection = `===== ${file} =====\n${fileContent}\n\n`;
content += fileSection;
totalTokens += countTokens(fileSection);
} catch (error) {
console.error(`Error reading file ${file}:`, error);
}
}

try {
await clipboardy.default.write(content);
console.log(`${files.length} files from ${directory} have been copied to the clipboard.`);
console.log(`Total tokens: ${totalTokens}`);
Expand Down Expand Up @@ -141,3 +88,4 @@ program
});

program.parse(process.argv);

40 changes: 40 additions & 0 deletions src/filterFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Options} from "./options";
import {simpleGit} from "simple-git";
import {glob} from "glob";
import path from "path";

export async function filterFiles(options: Options, directory: string, globalExclude?: string) {

const userExclude = options.exclude || '';
const combinedExclude = [globalExclude ?? '', userExclude].filter(Boolean).join(',');
const excludePatterns = combinedExclude.split(',');

let files: string[];

try {
const git = simpleGit(directory);
const isGitRepo = await git.checkIsRepo();

if (isGitRepo) {
const gitFiles = await git.raw(['ls-files', directory]);
files = gitFiles.split('\n').filter(Boolean);

if (combinedExclude.length > 0) {
files = files.filter(file =>
!excludePatterns.some(pattern =>
file.endsWith(pattern) ||
(glob.hasMagic(pattern)
? glob.sync(pattern, {cwd: directory}).includes(file)
: false)));
}
} else {
const globPattern = path.join(directory, '**/*');
files = await glob(globPattern, {nodir: true, ignore: excludePatterns});
}
} catch (error: any) {
console.error('Error listing files:', error);
throw new Error('Error listing files:', error);
}

return files;
}
5 changes: 5 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Options {
exclude?: string;
verbose?: boolean;
file?: string;
}
20 changes: 20 additions & 0 deletions src/readGlobalConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import path from "path";
import os from "os";
import fs from "fs/promises";

export async function readGlobalConfig(): Promise<string> {
const configPath = path.join(os.homedir(), '.copa');
try {
const configContent = await fs.readFile(configPath, 'utf-8');
const ignoreLine = configContent.split('\n').find(line => line.startsWith('ignore:'));
if (ignoreLine) {
return ignoreLine.split(':')[1].trim();
}
} catch (error) {
// If the file doesn't exist or can't be read, return an empty string
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
console.warn('Warning: Unable to read global config file:', error);
}
}
return '';
}
57 changes: 57 additions & 0 deletions tests/sanity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { filterFiles } from '../src/filterFiles';
import * as path from 'path';

const testDir = path.join(__dirname, 'test_files');

describe('CoPa Functionality', () => {
describe('filterFiles', () => {
test('includes all files when no exclusions', async () => {
const files = await filterFiles({}, testDir);
expect(files.length).toBe(9); // Assuming there are 9 files in the test_files directory
expect(files).toEqual(expect.arrayContaining([
expect.stringMatching(/file_1\.js$/),
expect.stringMatching(/file_1\.md$/),
expect.stringMatching(/file_1\.yml$/),
expect.stringMatching(/file_2\.js$/),
expect.stringMatching(/file_2\.md$/),
expect.stringMatching(/file_2\.yml$/),
expect.stringMatching(/file_3\.js$/),
expect.stringMatching(/file_3\.md$/),
expect.stringMatching(/file_3\.yml$/),
]));
});

test('excludes files based on command line options', async () => {
const files = await filterFiles({ exclude: 'js,md' }, testDir);
expect(files.length).toBe(3);
expect(files).toEqual(expect.arrayContaining([
expect.stringMatching(/\.yml$/),
]));
expect(files).not.toEqual(expect.arrayContaining([
expect.stringMatching(/\.js$/),
expect.stringMatching(/\.md$/),
]));
});

test('excludes files based on command line options', async () => {
const files = await filterFiles({ exclude: 'yml' }, testDir);
expect(files.length).toBe(6);
expect(files).toEqual(expect.arrayContaining([
expect.stringMatching(/\.js$/),
expect.stringMatching(/\.md$/),
]));
expect(files).not.toEqual(expect.arrayContaining([
expect.stringMatching(/\.yml$/),
]));
});

test('handles wildcard patterns', async () => {
const files = await filterFiles({ exclude: 'file_*.yml' }, testDir);
console.log(files);
const yamlFiles = files.filter(file => file.endsWith('.yml'));
expect(yamlFiles.length).toBe(0);
expect(files.length).toBe(6); // Should include all js and md files
});
});

});
Empty file added tests/test_files/file_1.js
Empty file.
Empty file added tests/test_files/file_1.md
Empty file.
Empty file added tests/test_files/file_1.yml
Empty file.
Empty file added tests/test_files/file_2.js
Empty file.
Empty file added tests/test_files/file_2.md
Empty file.
Empty file added tests/test_files/file_2.yml
Empty file.
Empty file added tests/test_files/file_3.js
Empty file.
Empty file added tests/test_files/file_3.md
Empty file.
Empty file added tests/test_files/file_3.yml
Empty file.
Loading

0 comments on commit 32764a4

Please sign in to comment.