UNPKG

@json-express/context-builder

Version:

A cli tool to build your project context that can be uploaded to ai when chatting

543 lines (540 loc) 18.4 kB
#!/usr/bin/env node "use strict"; const fs = require("fs"); const path = require("path"); function getDefaultExportFromCjs(x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x; } var ignore$1 = { exports: {} }; var hasRequiredIgnore; function requireIgnore() { if (hasRequiredIgnore) return ignore$1.exports; hasRequiredIgnore = 1; (function(module2) { function makeArray(subject) { return Array.isArray(subject) ? subject : [subject]; } const UNDEFINED = void 0; const EMPTY = ""; const SPACE = " "; const ESCAPE = "\\"; const REGEX_TEST_BLANK_LINE = /^\s+$/; const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/; const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/; const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/; const REGEX_SPLITALL_CRLF = /\r?\n/g; const REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/; const REGEX_TEST_TRAILING_SLASH = /\/$/; const SLASH = "/"; let TMP_KEY_IGNORE = "node-ignore"; if (typeof Symbol !== "undefined") { TMP_KEY_IGNORE = Symbol.for("node-ignore"); } const KEY_IGNORE = TMP_KEY_IGNORE; const define = (object, key, value) => { Object.defineProperty(object, key, { value }); return value; }; const REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g; const RETURN_FALSE = () => false; const sanitizeRange = (range) => range.replace( REGEX_REGEXP_RANGE, (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) ? match : EMPTY ); const cleanRangeBackSlash = (slashes) => { const { length } = slashes; return slashes.slice(0, length - length % 2); }; const REPLACERS = [ [ // Remove BOM // TODO: // Other similar zero-width characters? /^\uFEFF/, () => EMPTY ], // > Trailing spaces are ignored unless they are quoted with backslash ("\") [ // (a\ ) -> (a ) // (a ) -> (a) // (a ) -> (a) // (a \ ) -> (a ) /((?:\\\\)*?)(\\?\s+)$/, (_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY) ], // Replace (\ ) with ' ' // (\ ) -> ' ' // (\\ ) -> '\\ ' // (\\\ ) -> '\\ ' [ /(\\+?)\s/g, (_, m1) => { const { length } = m1; return m1.slice(0, length - length % 2) + SPACE; } ], // Escape metacharacters // which is written down by users but means special for regular expressions. // > There are 12 characters with special meanings: // > - the backslash \, // > - the caret ^, // > - the dollar sign $, // > - the period or dot ., // > - the vertical bar or pipe symbol |, // > - the question mark ?, // > - the asterisk or star *, // > - the plus sign +, // > - the opening parenthesis (, // > - the closing parenthesis ), // > - and the opening square bracket [, // > - the opening curly brace {, // > These special characters are often called "metacharacters". [ /[\\$.|*+(){^]/g, (match) => `\\${match}` ], [ // > a question mark (?) matches a single character /(?!\\)\?/g, () => "[^/]" ], // leading slash [ // > A leading slash matches the beginning of the pathname. // > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c". // A leading slash matches the beginning of the pathname /^\//, () => "^" ], // replace special metacharacter slash after the leading slash [ /\//g, () => "\\/" ], [ // > A leading "**" followed by a slash means match in all directories. // > For example, "**/foo" matches file or directory "foo" anywhere, // > the same as pattern "foo". // > "**/foo/bar" matches file or directory "bar" anywhere that is directly // > under directory "foo". // Notice that the '*'s have been replaced as '\\*' /^\^*\\\*\\\*\\\//, // '**/foo' <-> 'foo' () => "^(?:.*\\/)?" ], // starting [ // there will be no leading '/' // (which has been replaced by section "leading slash") // If starts with '**', adding a '^' to the regular expression also works /^(?=[^^])/, function startingReplacer() { return !/\/(?!$)/.test(this) ? "(?:^|\\/)" : "^"; } ], // two globstars [ // Use lookahead assertions so that we could match more than one `'/**'` /\\\/\\\*\\\*(?=\\\/|$)/g, // Zero, one or several directories // should not use '*', or it will be replaced by the next replacer // Check if it is not the last `'/**'` (_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+" ], // normal intermediate wildcards [ // Never replace escaped '*' // ignore rule '\*' will match the path '*' // 'abc.*/' -> go // 'abc.*' -> skip this rule, // coz trailing single wildcard will be handed by [trailing wildcard] /(^|[^\\]+)(\\\*)+(?=.+)/g, // '*.js' matches '.js' // '*.js' doesn't match 'abc' (_, p1, p2) => { const unescaped = p2.replace(/\\\*/g, "[^\\/]*"); return p1 + unescaped; } ], [ // unescape, revert step 3 except for back slash // For example, if a user escape a '\\*', // after step 3, the result will be '\\\\\\*' /\\\\\\(?=[$.|*+(){^])/g, () => ESCAPE ], [ // '\\\\' -> '\\' /\\\\/g, () => ESCAPE ], [ // > The range notation, e.g. [a-zA-Z], // > can be used to match one of the characters in a range. // `\` is escaped by step 3 /(\\)?\[([^\]/]*?)(\\*)($|\])/g, (match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` : close === "]" ? endEscape.length % 2 === 0 ? `[${sanitizeRange(range)}${endEscape}]` : "[]" : "[]" ], // ending [ // 'js' will not match 'js.' // 'ab' will not match 'abc' /(?:[^*])$/, // WTF! // https://git-scm.com/docs/gitignore // changes in [2.22.1](https://git-scm.com/docs/gitignore/2.22.1) // which re-fixes #24, #38 // > If there is a separator at the end of the pattern then the pattern // > will only match directories, otherwise the pattern can match both // > files and directories. // 'js*' will not match 'a.js' // 'js/' will not match 'a.js' // 'js' will match 'a.js' and 'a.js/' (match) => /\/$/.test(match) ? `${match}$` : `${match}(?=$|\\/$)` ] ]; const REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/; const MODE_IGNORE = "regex"; const MODE_CHECK_IGNORE = "checkRegex"; const UNDERSCORE = "_"; const TRAILING_WILD_CARD_REPLACERS = { [MODE_IGNORE](_, p1) { const prefix = p1 ? `${p1}[^/]+` : "[^/]*"; return `${prefix}(?=$|\\/$)`; }, [MODE_CHECK_IGNORE](_, p1) { const prefix = p1 ? `${p1}[^/]*` : "[^/]*"; return `${prefix}(?=$|\\/$)`; } }; const makeRegexPrefix = (pattern) => REPLACERS.reduce( (prev, [matcher, replacer]) => prev.replace(matcher, replacer.bind(pattern)), pattern ); const isString = (subject) => typeof subject === "string"; const checkPattern = (pattern) => pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) && pattern.indexOf("#") !== 0; const splitPattern = (pattern) => pattern.split(REGEX_SPLITALL_CRLF).filter(Boolean); class IgnoreRule { constructor(pattern, mark, body, ignoreCase, negative, prefix) { this.pattern = pattern; this.mark = mark; this.negative = negative; define(this, "body", body); define(this, "ignoreCase", ignoreCase); define(this, "regexPrefix", prefix); } get regex() { const key = UNDERSCORE + MODE_IGNORE; if (this[key]) { return this[key]; } return this._make(MODE_IGNORE, key); } get checkRegex() { const key = UNDERSCORE + MODE_CHECK_IGNORE; if (this[key]) { return this[key]; } return this._make(MODE_CHECK_IGNORE, key); } _make(mode, key) { const str = this.regexPrefix.replace( REGEX_REPLACE_TRAILING_WILDCARD, // It does not need to bind pattern TRAILING_WILD_CARD_REPLACERS[mode] ); const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str); return define(this, key, regex); } } const createRule = ({ pattern, mark }, ignoreCase) => { let negative = false; let body = pattern; if (body.indexOf("!") === 0) { negative = true; body = body.substr(1); } body = body.replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!").replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#"); const regexPrefix = makeRegexPrefix(body); return new IgnoreRule( pattern, mark, body, ignoreCase, negative, regexPrefix ); }; class RuleManager { constructor(ignoreCase) { this._ignoreCase = ignoreCase; this._rules = []; } _add(pattern) { if (pattern && pattern[KEY_IGNORE]) { this._rules = this._rules.concat(pattern._rules._rules); this._added = true; return; } if (isString(pattern)) { pattern = { pattern }; } if (checkPattern(pattern.pattern)) { const rule = createRule(pattern, this._ignoreCase); this._added = true; this._rules.push(rule); } } // @param {Array<string> | string | Ignore} pattern add(pattern) { this._added = false; makeArray( isString(pattern) ? splitPattern(pattern) : pattern ).forEach(this._add, this); return this._added; } // Test one single path without recursively checking parent directories // // - checkUnignored `boolean` whether should check if the path is unignored, // setting `checkUnignored` to `false` could reduce additional // path matching. // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE` // @returns {TestResult} true if a file is ignored test(path2, checkUnignored, mode) { let ignored = false; let unignored = false; let matchedRule; this._rules.forEach((rule) => { const { negative } = rule; if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) { return; } const matched = rule[mode].test(path2); if (!matched) { return; } ignored = !negative; unignored = negative; matchedRule = negative ? UNDEFINED : rule; }); const ret = { ignored, unignored }; if (matchedRule) { ret.rule = matchedRule; } return ret; } } const throwError = (message, Ctor) => { throw new Ctor(message); }; const checkPath = (path2, originalPath, doThrow) => { if (!isString(path2)) { return doThrow( `path must be a string, but got \`${originalPath}\``, TypeError ); } if (!path2) { return doThrow(`path must not be empty`, TypeError); } if (checkPath.isNotRelative(path2)) { const r = "`path.relative()`d"; return doThrow( `path should be a ${r} string, but got "${originalPath}"`, RangeError ); } return true; }; const isNotRelative = (path2) => REGEX_TEST_INVALID_PATH.test(path2); checkPath.isNotRelative = isNotRelative; checkPath.convert = (p) => p; class Ignore { constructor({ ignorecase = true, ignoreCase = ignorecase, allowRelativePaths = false } = {}) { define(this, KEY_IGNORE, true); this._rules = new RuleManager(ignoreCase); this._strictPathCheck = !allowRelativePaths; this._initCache(); } _initCache() { this._ignoreCache = /* @__PURE__ */ Object.create(null); this._testCache = /* @__PURE__ */ Object.create(null); } add(pattern) { if (this._rules.add(pattern)) { this._initCache(); } return this; } // legacy addPattern(pattern) { return this.add(pattern); } // @returns {TestResult} _test(originalPath, cache, checkUnignored, slices) { const path2 = originalPath && checkPath.convert(originalPath); checkPath( path2, originalPath, this._strictPathCheck ? throwError : RETURN_FALSE ); return this._t(path2, cache, checkUnignored, slices); } checkIgnore(path2) { if (!REGEX_TEST_TRAILING_SLASH.test(path2)) { return this.test(path2); } const slices = path2.split(SLASH).filter(Boolean); slices.pop(); if (slices.length) { const parent = this._t( slices.join(SLASH) + SLASH, this._testCache, true, slices ); if (parent.ignored) { return parent; } } return this._rules.test(path2, false, MODE_CHECK_IGNORE); } _t(path2, cache, checkUnignored, slices) { if (path2 in cache) { return cache[path2]; } if (!slices) { slices = path2.split(SLASH).filter(Boolean); } slices.pop(); if (!slices.length) { return cache[path2] = this._rules.test(path2, checkUnignored, MODE_IGNORE); } const parent = this._t( slices.join(SLASH) + SLASH, cache, checkUnignored, slices ); return cache[path2] = parent.ignored ? parent : this._rules.test(path2, checkUnignored, MODE_IGNORE); } ignores(path2) { return this._test(path2, this._ignoreCache, false).ignored; } createFilter() { return (path2) => !this.ignores(path2); } filter(paths) { return makeArray(paths).filter(this.createFilter()); } // @returns {TestResult} test(path2) { return this._test(path2, this._testCache, true); } } const factory = (options) => new Ignore(options); const isPathValid = (path2) => checkPath(path2 && checkPath.convert(path2), path2, RETURN_FALSE); const setupWindows = () => { const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/"); checkPath.convert = makePosix; const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i; checkPath.isNotRelative = (path2) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path2) || isNotRelative(path2); }; if ( // Detect `process` so that it can run in browsers. typeof process !== "undefined" && process.platform === "win32" ) { setupWindows(); } module2.exports = factory; factory.default = factory; module2.exports.isPathValid = isPathValid; define(module2.exports, Symbol.for("setupWindows"), setupWindows); })(ignore$1); return ignore$1.exports; } var ignoreExports = requireIgnore(); const ignore = /* @__PURE__ */ getDefaultExportFromCjs(ignoreExports); const startingDirectory = "."; const outputFilePath = "./project-context.txt"; const gitIg = ignore(); const gitignorePath = ".gitignore"; if (fs.existsSync(gitignorePath)) { const gitignoreContent = fs.readFileSync(gitignorePath, "utf8"); gitIg.add(gitignoreContent); } gitIg.add("node_modules"); gitIg.add(path.basename(outputFilePath)); gitIg.add(".git"); const contextIg = ignore(); const contextIgnorePath = ".contextignore"; if (fs.existsSync(contextIgnorePath)) { const contextIgnoreContent = fs.readFileSync(contextIgnorePath, "utf8"); contextIg.add(contextIgnoreContent); } try { fs.writeFileSync(outputFilePath, ""); } catch (err) { console.error(`Error clearing output file: ${err.message}`); } const generateTree = (dir, prefix = "") => { const allFiles = fs.readdirSync(dir); const allowedFiles = allFiles.filter((file) => !gitIg.ignores(path.join(dir, file))); let tree = ""; allowedFiles.forEach((file, index) => { const filePath = path.join(dir, file); const isLast = index === allowedFiles.length - 1; const connector = isLast ? "└── " : "├── "; tree += `${prefix}${connector}${file} `; if (fs.statSync(filePath).isDirectory()) { const newPrefix = prefix + (isLast ? " " : "│ "); tree += generateTree(filePath, newPrefix); } }); return tree; }; const readDirectoryAndWriteToFile = (directoryPath) => { const files = fs.readdirSync(directoryPath); files.forEach((file) => { const filePath = path.join(directoryPath, file); if (gitIg.ignores(filePath)) { return; } const stat = fs.statSync(filePath); if (stat.isDirectory()) { readDirectoryAndWriteToFile(filePath); } else if (stat.isFile()) { let outputContent; if (contextIg.ignores(filePath)) { outputContent = `${filePath} `; } else { const content = fs.readFileSync(filePath, "utf8"); outputContent = `${filePath} \`\`\` ${content} \`\`\` `; } fs.appendFileSync(outputFilePath, outputContent); } }); }; console.log("Generating file tree and content..."); const treeStructure = generateTree(startingDirectory); fs.appendFileSync(outputFilePath, path.basename(startingDirectory) + "\n" + treeStructure + "\n\n"); readDirectoryAndWriteToFile(startingDirectory); console.log(`Processing complete. The output has been written to ${outputFilePath}`);