Skip to content

Instantly share code, notes, and snippets.

@hargup
Created February 13, 2025 09:20
Show Gist options
  • Save hargup/1287ac4c4e31642e25eb357eb1909015 to your computer and use it in GitHub Desktop.
Save hargup/1287ac4c4e31642e25eb357eb1909015 to your computer and use it in GitHub Desktop.
#! /usr/bin/env bun
import { Command } from 'commander';
import * as ts from 'typescript';
import * as path from 'path';
import * as fs from 'fs';
import fg from 'fast-glob';
import clipboardy from 'clipboardy';
const program = new Command();
program
.description('Convert selected TypeScript files (plus their dependencies) into a text format, with optional clipboard copy.')
.option('-s, --select <patterns...>', 'Glob patterns to include in code block format')
.option('-i, --ignore <patterns...>', 'Glob patterns to ignore', [])
.option('--list', 'List matched files with approximate token counts, sorted descending')
.option('--out <file>', 'Output file (defaults to clipboard). Use "stdout" for standard output.', 'clipboard')
.parse(process.argv);
interface Options {
select?: string[];
ignore?: string[];
list?: boolean;
out: string;
}
const options = program.opts<Options>();
async function main() {
// 1. Check for required --select patterns
const patterns: string[] | undefined = options.select;
if (!patterns || patterns.length === 0) {
console.error('Error: No glob patterns provided. Use --select to specify which files to include.');
process.exit(1);
}
// 2. Gather the matched files (absolute paths) as our "root" files, respecting any ignored patterns
const rootFilePaths = await fg(patterns, {
absolute: true,
cwd: process.cwd(),
ignore: options.ignore,
});
if (rootFilePaths.length === 0) {
console.log('No files matched the provided patterns (after ignore).');
return;
}
// 3. Create a TypeScript Program from these root files
const compilerOptions: ts.CompilerOptions = {
allowJs: true,
skipLibCheck: true,
esModuleInterop: true,
resolveJsonModule: true,
};
const tsProgram = ts.createProgram(rootFilePaths, compilerOptions);
// 4. Retrieve all source files in the program
const allSourceFiles = tsProgram.getSourceFiles();
// Helper to approximate token count
// (You could choose a more sophisticated approach if needed)
function approximateTokenCount(text: string) {
return text.split(/\s+/).filter(Boolean).length;
}
// 5. Build output in-memory & keep track of file info for --list
let output = '';
const fileInfo: Array<{ path: string; tokens: number }> = [];
for (const sourceFile of allSourceFiles) {
const fileName = sourceFile.fileName;
// Skip node_modules and .d.ts
if (fileName.includes('node_modules')) {
continue;
}
if (fileName.endsWith('.d.ts')) {
continue;
}
// Read file contents
const fileContents = fs.readFileSync(fileName, 'utf8');
// Make path relative to the current working directory
const relPath = path.relative(process.cwd(), fileName);
// Track approximate token count
const tokens = approximateTokenCount(fileContents);
fileInfo.push({ path: relPath, tokens });
// Write block
output += `<code file:${relPath}>\n`;
output += fileContents;
output += `\n</code>\n\n`;
}
// If --list is specified, output a table sorted by descending token count
if (options.list) {
// Sort file info by descending token count
fileInfo.sort((a, b) => b.tokens - a.tokens);
console.log('Matched files (approx. token count, descending):');
console.log('--------------------------------------------------');
console.log(`| Tokens | File Path`);
console.log('--------------------------------------------------');
for (const info of fileInfo) {
// Pad the token count to align columns nicely
const tokenStr = info.tokens.toString().padEnd(10);
console.log(`| ${tokenStr} | ${info.path}`);
}
console.log('--------------------------------------------------');
console.log('');
}
// 6. Output handling
// Default: Copy to clipboard. If --out is specified:
// - 'stdout': print to stdout
// - otherwise: write to the file path given
if (options.out === 'clipboard') {
await clipboardy.write(output);
console.log('Output has been copied to the clipboard.');
} else if (options.out === 'stdout') {
process.stdout.write(output);
} else {
fs.writeFileSync(options.out, output, 'utf8');
console.log(`Output written to ${options.out}`);
}
}
main().catch(err => {
console.error(err);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment