tscribe
Version:
CLI to dump TypeScript sources with headings for LLM ingestion
137 lines (136 loc) • 5.01 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildGlobPattern = buildGlobPattern;
exports.tscribe = tscribe;
exports.applySort = applySort;
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const glob_1 = require("glob");
const micromatch_1 = __importDefault(require("micromatch"));
const natural_orderby_1 = require("natural-orderby");
function log(message, opts) {
if (!opts.quiet)
console.log(message);
}
function debug(message, opts) {
if (opts.verbose && !opts.quiet)
console.log("[debug]", message);
}
function filterIgnoredPaths(files, ignore, basePath) {
if (!ignore?.trim())
return files;
const patterns = ignore
.split(",")
.map((p) => p.trim().replace(/\\/g, "/"))
.filter(Boolean);
return files.filter((file) => {
const relativePath = path_1.default.relative(basePath, file).replace(/\\/g, "/");
return !micromatch_1.default.isMatch(relativePath, patterns);
});
}
function buildGlobPattern(srcPath, ext) {
const resolved = srcPath.replace(/\\/g, "/");
if (ext === "" || ext === "*") {
return `${resolved}/**/*`;
}
const exts = ext
.split(",")
.map((e) => e.trim())
.filter(Boolean);
if (exts.length > 1) {
return `${resolved}/**/*.{${exts.join(",")}}`;
}
return `${resolved}/**/*.${exts[0]}`;
}
async function tscribe(opts) {
const headingTpl = opts.heading ||
(opts.format === "plain" ? "// --- {file} ---" : "### {file}");
debug(`src = ${opts.src}`, opts);
debug(`ext = ${opts.ext}`, opts);
debug(`ignore = ${opts.ignore}`, opts);
debug(`sort = ${opts.sort}`, opts);
debug(`verbose = ${opts.verbose}`, opts);
let files = [];
const srcPath = path_1.default.resolve(opts.src);
// Check if directory exists
if (!(0, fs_1.existsSync)(srcPath)) {
debug(`Source directory ${srcPath} does not exist`, opts);
log(`✅ Processed 0 files.`, opts);
return;
}
const pattern = buildGlobPattern(srcPath, opts.ext);
debug(`glob pattern: ${pattern}`, opts);
// handle ignore patterns properly
const ignoreList = (opts.ignore ?? "node_modules,dist,.git")
.split(",")
.map((p) => p.trim())
.filter(Boolean);
try {
debug(`srcPath exists: ${(0, fs_1.existsSync)(srcPath)}`, opts);
debug(`ignoreList: ${ignoreList.join(", ")}`, opts);
debug(`directory contents: ${(0, fs_1.readdirSync)(srcPath).join(", ")}`, opts);
files = await (0, glob_1.glob)(pattern, {
ignore: ignoreList,
absolute: true,
nodir: true,
});
debug(`found ${files.length} files via glob`, opts);
}
catch (err) {
log(`error in glob: ${err}`, opts);
}
// apply manual filtering
files = filterIgnoredPaths(files, opts.ignore || "", srcPath);
debug(`final filtered files: ${files.length}`, opts);
// show matched files only in verbose mode
debug(`files matched before output: ${files.join(", ")}`, opts);
if (files.length === 0) {
debug("No files found", opts);
log(`✅ Processed 0 files.`, opts);
const empty = "";
if (opts.out) {
await fs_1.promises.writeFile(path_1.default.resolve(opts.out), empty, "utf8"); // write empty file
}
else {
process.stdout.write(empty); // default fallback
}
return;
}
const sorted = await applySort(files, opts.sort);
if (opts.list) {
log(sorted.join("\n"), opts);
return;
}
const sections = await Promise.all(sorted.map(async (f) => {
const body = await fs_1.promises.readFile(f, "utf8");
const title = headingTpl.replace("{file}", path_1.default.relative(process.cwd(), f).replace(/\\/g, "/"));
return `${title}\n\n${body}`;
}));
const fullOutput = sections.join("\n\n");
if (opts.out) {
// Fix: Make sure output file is written properly
const outPath = path_1.default.resolve(opts.out);
await fs_1.promises.writeFile(outPath, fullOutput, "utf8");
debug(`Output written to ${outPath}`, opts);
}
else {
process.stdout.write(fullOutput);
}
log(`✅ Processed ${sections.length} files.`, opts);
}
async function applySort(files, mode) {
switch (mode) {
case "alpha":
return (0, natural_orderby_1.orderBy)([...files], [(f) => path_1.default.basename(f)]);
case "mtime": {
const stats = await Promise.all(files.map(async (f) => ({ file: f, mtime: (await fs_1.promises.stat(f)).mtimeMs })));
return stats.sort((a, b) => a.mtime - b.mtime).map((s) => s.file);
}
case "path":
default:
return files;
}
}