Files
2025-11-30 08:48:32 +08:00

986 lines
30 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Code Extractor for Expert Consultations
*
* Extracts file contents, line ranges, or git diffs with automatic size tracking
* to stay within the 125 KB limit for expert consultation documents.
*
* @author Propstreet
* @license MIT
* @requires Node.js 18+
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { parseArgs } from "util";
import { execSync } from "child_process";
// ============================================================================
// Constants
// ============================================================================
/** Maximum size for expert consultation documents (125 KB) */
const MAX_SIZE_BYTES = 125 * 1024;
/** Warning threshold at 100 KB */
const WARNING_THRESHOLD_1 = 100 * 1024;
/** Warning threshold at 115 KB (very close to limit) */
const WARNING_THRESHOLD_2 = 115 * 1024;
/** Regex pattern for parsing file arguments with ranges/diffs */
const FILE_ARG_PATTERN = /^(.+?):([\d,:-]+|diff(?:=.+)?)$/;
// ============================================================================
// Utility Functions
// ============================================================================
/**
* Format bytes as human-readable size in KB
* @param {number} bytes - Size in bytes
* @returns {string} Formatted size (e.g., "25.5 KB")
*/
function formatSize(bytes) {
return (bytes / 1024).toFixed(1) + " KB";
}
/**
* Detect programming language from file extension
* @param {string} filePath - Path to file
* @returns {string} Language identifier for syntax highlighting
*/
function detectLanguage(filePath) {
const ext = path.extname(filePath).toLowerCase();
const langMap = {
".cs": "csharp",
".js": "javascript",
".ts": "typescript",
".vue": "vue",
".json": "json",
".md": "markdown",
".sql": "sql",
".html": "html",
".css": "css",
".scss": "scss",
".xml": "xml",
".yaml": "yaml",
".yml": "yaml",
".sh": "bash",
".py": "python",
".jsx": "jsx",
".tsx": "tsx",
};
return langMap[ext] || "text";
}
// ============================================================================
// File Argument Parsing
// ============================================================================
/**
* Parse file argument into path and range/diff specification
* Supports formats:
* - "path/to/file.cs" (full file)
* - "path/to/file.cs:100-200" (line range)
* - "path/to/file.cs:1-30,100-150" (multiple ranges)
* - "path/to/file.cs:diff" (git diff vs master)
* - "path/to/file.cs:diff=master..HEAD" (git diff with range)
*
* @param {string} fileArg - File argument from command line
* @returns {{filePath: string, rangeStr: string|null}} Parsed components
*/
function parseFileArgument(fileArg) {
const rangeMatch = fileArg.match(FILE_ARG_PATTERN);
if (rangeMatch) {
return {
filePath: rangeMatch[1],
rangeStr: rangeMatch[2],
};
}
return {
filePath: fileArg,
rangeStr: null,
};
}
// ============================================================================
// Git Operations
// ============================================================================
/**
* Check if current directory is in a git repository
* @throws {Error} If not in a git repository
*/
function validateGitRepository() {
try {
execSync("git rev-parse --git-dir", { stdio: "pipe" });
} catch {
throw new Error("Not in a git repository");
}
}
/**
* Split git diff range into individual refs for validation
* @param {string} diffRange - Git range (e.g., "master", "master..HEAD", "HEAD~3")
* @returns {string[]} Array of git refs to validate
*/
function splitGitRefs(diffRange) {
return diffRange.includes("..")
? diffRange.split("..").filter((r) => r)
: [diffRange];
}
/**
* Validate that git references exist
* @param {string[]} refs - Array of git refs to validate
* @throws {Error} If any ref is invalid
*/
function validateGitRefs(refs) {
for (const ref of refs) {
try {
execSync(`git rev-parse --verify ${ref}`, { stdio: "pipe" });
} catch {
throw new Error(`Invalid git reference: ${ref}`);
}
}
}
/**
* Parse diff specification from range string
* @param {string|null} specStr - Diff specification (e.g., "diff", "diff=master..HEAD")
* @returns {{type: string, range: string}|null} Parsed diff spec or null
*/
function parseDiffSpec(specStr) {
if (!specStr) {
return null;
}
if (specStr === "diff") {
return { type: "diff", range: "master" };
}
const match = specStr.match(/^diff=(.+)$/);
if (match) {
return { type: "diff", range: match[1] };
}
return null;
}
/**
* Read git diff content for a file
* @param {string} filePath - Absolute path to the file
* @param {string} diffRange - Git range (e.g., "master", "master..HEAD", "HEAD~3")
* @returns {string} Unified diff output
* @throws {Error} If git operations fail
*/
function readDiffContent(filePath, diffRange) {
try {
validateGitRepository();
// Validate all refs in the range
const refs = splitGitRefs(diffRange);
validateGitRefs(refs);
// Get relative path from git root for git diff
const gitRoot = execSync("git rev-parse --show-toplevel", {
encoding: "utf8",
}).trim();
const relativePath = path.relative(gitRoot, filePath);
// Execute git diff
const diffCommand = diffRange.includes("..")
? `git diff ${diffRange} -- "${relativePath}"`
: `git diff ${diffRange} -- "${relativePath}"`;
return execSync(diffCommand, {
encoding: "utf8",
cwd: gitRoot,
});
} catch (error) {
// Re-throw with context
if (
error.message.includes("Not in a git repository") ||
error.message.includes("Invalid git reference")
) {
throw error;
}
throw new Error(`Git diff failed: ${error.message}`);
}
}
// ============================================================================
// Line Range Operations
// ============================================================================
/**
* Parse line range string into structured format
* Supports: "10-20", "10:20", "10-20,50-60,100-150"
*
* @param {string|null} rangeStr - Line range string
* @returns {{from: number, to: number}[]|null} Array of range objects or null
* @throws {Error} If range format is invalid
*/
function parseLineRanges(rangeStr) {
if (!rangeStr) {
return null;
}
const ranges = rangeStr.split(",").map((r) => r.trim());
const parsed = [];
for (const range of ranges) {
const match = range.match(/^(\d+)[-:](\d+)$/);
if (!match) {
throw new Error(
`Invalid line range format: "${range}". Use format "10-20" or "10:20"`
);
}
const from = parseInt(match[1], 10);
const to = parseInt(match[2], 10);
if (from < 1) {
throw new Error(`Line numbers must be >= 1, got ${from}`);
}
if (to < from) {
throw new Error(`End line (${to}) must be >= start line (${from})`);
}
parsed.push({ from, to });
}
return parsed;
}
/**
* Read file content and optionally extract line ranges
* @param {string} filePath - Absolute path to file
* @param {{from: number, to: number}[]|null} lineRanges - Line ranges to extract
* @returns {string} File content (full or extracted ranges)
* @throws {Error} If line ranges exceed file length
*/
function readFileContent(filePath, lineRanges) {
const content = fs.readFileSync(filePath, "utf8");
if (!lineRanges || lineRanges.length === 0) {
return content;
}
const lines = content.split("\n");
const totalLines = lines.length;
const extractedSegments = [];
for (const range of lineRanges) {
if (range.from > totalLines) {
throw new Error(
`Start line ${range.from} exceeds file length (${totalLines} lines)`
);
}
const endLine = Math.min(range.to, totalLines);
const segment = lines.slice(range.from - 1, endLine);
extractedSegments.push(segment.join("\n"));
}
return extractedSegments.join("\n\n");
}
// ============================================================================
// Output Formatting
// ============================================================================
/**
* Format file content as markdown code block
* @param {string} filePath - Path to file
* @param {string} language - Language for syntax highlighting
* @param {string} content - File content
* @param {{from: number, to: number}[]|null} lineRanges - Line ranges (for display)
* @returns {string} Formatted markdown code block
*/
function formatCodeBlock(filePath, language, content, lineRanges) {
let lineRangeStr = "";
if (lineRanges && lineRanges.length > 0) {
const rangeStrings = lineRanges.map((r) => `${r.from}-${r.to}`);
lineRangeStr = ` (lines ${rangeStrings.join(", ")})`;
}
return `# File: ${filePath}${lineRangeStr}
\`\`\`${language}
${content}
\`\`\``;
}
/**
* Format git diff output as markdown code block
* @param {string} filePath - Path to file
* @param {string} diffContent - Git diff output
* @param {string} diffRange - Git range for display
* @returns {string} Formatted markdown diff block
*/
function formatDiffBlock(filePath, diffContent, diffRange) {
return `# File: ${filePath} (diff=${diffRange})
\`\`\`diff
${diffContent}
\`\`\``;
}
// ============================================================================
// File Validation
// ============================================================================
/**
* Validate file argument without processing it
* Checks file existence, git refs, and line ranges
*
* @param {string} fileArg - File argument from command line
* @returns {{valid: boolean, fileArg?: string, error?: string}} Validation result
*/
function validateFile(fileArg) {
const { filePath: parsedPath, rangeStr } = parseFileArgument(fileArg);
// Resolve to absolute path
const filePath = path.isAbsolute(parsedPath)
? parsedPath
: path.resolve(process.cwd(), parsedPath);
// Check if file exists
if (!fs.existsSync(filePath)) {
const cwd = process.cwd();
const suggestion = getSuggestion(filePath);
return {
valid: false,
fileArg,
error: `File not found: ${filePath}\n Current directory: ${cwd}${suggestion}`,
};
}
// Validate range specification if present
if (rangeStr) {
const diffSpec = parseDiffSpec(rangeStr);
if (diffSpec) {
// Validate git diff specification
try {
validateGitRepository();
const refs = splitGitRefs(diffSpec.range);
validateGitRefs(refs);
} catch (error) {
return {
valid: false,
fileArg,
error: error.message,
};
}
} else {
// Validate line ranges
try {
const lineRanges = parseLineRanges(rangeStr);
const content = fs.readFileSync(filePath, "utf8");
const totalLines = content.split("\n").length;
for (const range of lineRanges) {
if (range.from > totalLines || range.to > totalLines) {
return {
valid: false,
fileArg,
error: `Line range ${range.from}-${range.to} exceeds file length (${totalLines} lines) in ${filePath}`,
};
}
}
} catch (error) {
return {
valid: false,
fileArg,
error: `Invalid line range format in "${fileArg}": ${error.message}`,
};
}
}
}
return { valid: true };
}
/**
* Process a single file argument and return formatted content
* @param {string} fileArg - File argument (path with optional range/diff spec)
* @returns {string} Formatted markdown output
* @throws {Error} If file processing fails
*/
function processFile(fileArg) {
const { filePath: parsedPath, rangeStr } = parseFileArgument(fileArg);
// Resolve to absolute path
const filePath = path.isAbsolute(parsedPath)
? parsedPath
: path.resolve(process.cwd(), parsedPath);
// Check if file exists
if (!fs.existsSync(filePath)) {
const cwd = process.cwd();
const suggestion = getSuggestion(filePath);
throw new Error(
`File not found: ${filePath}\n Current directory: ${cwd}${suggestion}`
);
}
// Check for diff specification
const diffSpec = rangeStr ? parseDiffSpec(rangeStr) : null;
if (diffSpec) {
// Handle git diff mode
const diffContent = readDiffContent(filePath, diffSpec.range);
// Handle empty diff
if (!diffContent || diffContent.trim() === "") {
return formatDiffBlock(
filePath,
`(No changes between ${diffSpec.range})`,
diffSpec.range
);
}
return formatDiffBlock(filePath, diffContent, diffSpec.range);
}
// Handle line range or full file mode
const lineRanges = rangeStr ? parseLineRanges(rangeStr) : null;
const language = detectLanguage(filePath);
const content = readFileContent(filePath, lineRanges);
return formatCodeBlock(filePath, language, content, lineRanges);
}
// ============================================================================
// Config File Operations
// ============================================================================
/**
* Read and validate JSON config file
* @param {string} configPath - Path to config file
* @returns {object} Validated config object
* @throws {Error} If config is invalid
*/
function readConfigFile(configPath) {
if (!fs.existsSync(configPath)) {
throw new Error(`Config file not found: ${configPath}`);
}
let config;
try {
const content = fs.readFileSync(configPath, "utf8");
config = JSON.parse(content);
} catch (error) {
throw new Error(`Failed to parse config file: ${error.message}`);
}
// Validate schema
if (!config.sections || !Array.isArray(config.sections)) {
throw new Error(
'Config must have "sections" array. See example config for format.'
);
}
if (config.sections.length === 0) {
throw new Error("Config must have at least one section");
}
for (const [index, section] of config.sections.entries()) {
if (!section.files || !Array.isArray(section.files)) {
throw new Error(
`Section ${index + 1} must have "files" array. Header: ${section.header || "(no header)"}`
);
}
if (section.files.length === 0) {
throw new Error(
`Section ${index + 1} must have at least one file. Header: ${section.header || "(no header)"}`
);
}
}
return config;
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Provide helpful error suggestions when file is not found
* @param {string} filePath - Path that was not found
* @returns {string} Suggestion text (empty if none applicable)
*/
function getSuggestion(filePath) {
// Provide helpful suggestion for relative path issues
if (!path.isAbsolute(filePath) && filePath.includes(path.sep)) {
return `\n 💡 Tip: Verify you're in the correct directory:\n pwd # Check current directory\n ls ${path.dirname(filePath)} # Check parent directory exists`;
}
// Check if file exists with different casing
const dir = path.dirname(filePath);
const filename = path.basename(filePath);
if (fs.existsSync(dir)) {
try {
const files = fs.readdirSync(dir);
const match = files.find(
(f) => f.toLowerCase() === filename.toLowerCase()
);
if (match && match !== filename) {
return `\n 💡 Tip: File exists with different casing: ${match}`;
}
} catch {
// Ignore directory read errors
}
}
return "";
}
// ============================================================================
// CLI Interface
// ============================================================================
/**
* Display help message
*/
function showHelp() {
console.log(`
📄 Code Extractor for Expert Consultations
Usage:
extract-code [options] <file1> [file2] [file3] ...
extract-code --help
Arguments:
file File path with optional line range(s) or diff spec
Formats:
/path/to/file.cs (extract full file)
/path/to/file.cs:10-50 (extract lines 10-50)
/path/to/file.cs:10:50 (extract lines 10-50, alternative)
/path/to/file.cs:10-50,100-150 (multiple ranges, comma-separated)
/path/to/file.cs:1-30,86-213,500-600 (multiple ranges)
/path/to/file.cs:diff (git diff vs master)
/path/to/file.cs:diff=master..HEAD (git diff with explicit range)
/path/to/file.cs:diff=HEAD~3 (git diff vs 3 commits ago)
relative/path/file.cs (resolved to absolute path)
relative/path/file.cs:5-15 (with line range)
Options:
--help, -h Show this help message
--output, -o <file> Write output to file (appends to existing file)
--track-size Show size tracking and progress (requires --output)
--section <header> Add markdown section header before next file
Can be used multiple times for different files
--config <file> Use JSON config file for batch extraction
See example-config.json for format
Output:
Prints markdown-formatted code blocks with file paths and line ranges.
Output can be redirected to a file or piped to other commands.
Examples:
# Extract full files
extract-code src/Service.cs tests/ServiceTests.cs
# Extract specific line ranges
extract-code src/Service.cs:100-200 tests/ServiceTests.cs:50-75
# Extract multiple ranges from a single file
extract-code src/Service.cs:1-30,86-213
# Mix full files and ranges
extract-code src/Models/User.cs src/Service.cs:100-150
# Show git diff vs master (default)
extract-code src/Service.cs:diff
# Show git diff with explicit range
extract-code src/Service.cs:diff=master..feature-branch
extract-code src/Service.cs:diff=HEAD~3..HEAD
# Show what changed in recent commits
extract-code src/Service.cs:diff=HEAD~5
# Combine diffs with regular files
extract-code src/Service.cs:diff src/Tests.cs:100-200
# Save to file with size tracking (appends to existing file)
extract-code --track-size --output=consultation.md src/Service.cs
# Add section headers
extract-code --section="Core Interfaces" Interface.cs \\
--section="Domain Models" Contact.cs Company.cs \\
--output=doc.md
# Combined: size tracking + sections
extract-code --track-size --output=doc.md \\
--section="Core" Interface.cs \\
--section="Tests" Tests.cs:100-200
# Include diffs in consultation documents
extract-code --track-size --output=consultation.md \\
--section="What Changed" \\
src/NetworkService.cs:diff \\
src/PineconeHelper.cs:diff=master..feature-branch
# Build document incrementally (appends each time)
extract-code --track-size -o doc.md File1.cs
extract-code --track-size -o doc.md File2.cs # Appends to existing
extract-code --track-size -o doc.md File3.cs # Appends again
# Traditional output redirection still works
extract-code src/Service.cs > expert-consultation.md
extract-code src/Tests.cs >> expert-consultation.md # Append
# Use config file for complex extractions
extract-code --config=extraction-plan.json
extract-code --config=extraction-plan.json --track-size # Override trackSize
Notes:
• Automatically detects language from file extension
• Line numbers are 1-indexed (first line is line 1)
• Line ranges are inclusive (10-20 includes both lines 10 and 20)
• Multiple ranges are separated by blank lines in output
• Supports 20+ file types (cs, js, ts, vue, py, etc.)
• Error messages go to stderr, formatted output to stdout
• --output mode always appends (matches >> behavior)
• Size tracking shows warnings at 100KB, 115KB, errors at 125KB
• Section headers apply to the immediately following file only
• Diff mode requires git repository and valid refs
• Diff output uses unified diff format (standard git diff)
`);
}
/**
* Main entry point
*/
function main() {
const options = {
help: {
type: "boolean",
short: "h",
},
"track-size": {
type: "boolean",
},
output: {
type: "string",
short: "o",
},
section: {
type: "string",
multiple: true,
},
config: {
type: "string",
},
};
let args;
try {
const parsed = parseArgs({ options, allowPositionals: true });
args = parsed.values;
args.positionals = parsed.positionals;
} catch (error) {
console.error(`${error.message}`);
showHelp();
process.exit(1);
}
if (args.help) {
showHelp();
process.exit(0);
}
// Handle config file mode
if (args.config) {
try {
const config = readConfigFile(args.config);
processConfigFile(config, args);
return;
} catch (error) {
console.error(`❌ Error processing config file: ${error.message}`);
process.exit(1);
}
}
if (!args.positionals || args.positionals.length === 0) {
console.error("❌ No files specified");
showHelp();
process.exit(1);
}
// Filter out empty arguments
const fileArgs = args.positionals.filter((arg) => arg && arg.trim() !== "");
// VALIDATE ALL FILES FIRST - before writing anything
const validationErrors = [];
for (const fileArg of fileArgs) {
const validation = validateFile(fileArg);
if (!validation.valid) {
validationErrors.push({
fileArg: validation.fileArg,
error: validation.error,
});
}
}
// If any validation errors, report them all and exit without modifying output
if (validationErrors.length > 0) {
console.error(
`❌ Validation failed for ${validationErrors.length} file(s):\n`
);
for (const { fileArg, error } of validationErrors) {
console.error(` • "${fileArg}":`);
console.error(` ${error.replace(/\n/g, "\n ")}`);
console.error("");
}
console.error("⚠️ No files were written to avoid partial output.");
process.exit(1);
}
const results = [];
let hasErrors = false;
let totalBytes = 0;
let sectionIndex = 0;
// Read existing file size if output file specified
if (args.output && fs.existsSync(args.output)) {
const stats = fs.statSync(args.output);
totalBytes = stats.size;
if (args["track-size"]) {
console.error(`📄 ${args.output}: ${formatSize(totalBytes)} (existing)`);
}
}
// Process each file
for (const [index, fileArg] of fileArgs.entries()) {
try {
// Add section header if specified
let output = "";
if (args.section && sectionIndex < args.section.length) {
const sectionHeader = args.section[sectionIndex];
if (sectionHeader) {
output = `### ${sectionHeader}\n\n`;
}
sectionIndex++;
}
const result = processFile(fileArg);
output += result;
results.push(output);
// Calculate size
const contentSize = Buffer.byteLength(output + "\n\n", "utf8");
totalBytes += contentSize;
// Write to file or collect for stdout
if (args.output) {
fs.appendFileSync(args.output, output + "\n\n", "utf8");
}
// Show progress if tracking size
if (args["track-size"]) {
const percent = ((totalBytes / MAX_SIZE_BYTES) * 100).toFixed(1);
const filename = path.basename(fileArg.split(":")[0]);
console.error(
`[${index + 1}/${fileArgs.length}] ${filename} → +${formatSize(contentSize)} (${formatSize(totalBytes)} / 125 KB, ${percent}%)`
);
// Check thresholds
if (totalBytes >= MAX_SIZE_BYTES) {
console.error(
`❌ Error: Exceeded 125 KB limit (${formatSize(totalBytes)})`
);
console.error(
` Stop processing to stay within expert consultation limits`
);
process.exit(1);
} else if (totalBytes >= WARNING_THRESHOLD_2) {
console.error(`⚠️ Very close to 125 KB limit!`);
} else if (totalBytes >= WARNING_THRESHOLD_1) {
console.error(`⚠️ Approaching 100 KB`);
}
}
} catch (error) {
console.error(`❌ Error processing "${fileArg}": ${error.message}`);
hasErrors = true;
}
}
if (results.length === 0) {
console.error("\n❌ No files were successfully processed");
process.exit(1);
}
// Output results to stdout if no output file specified
if (!args.output) {
console.log(results.join("\n\n"));
} else if (args["track-size"]) {
const status = hasErrors ? "⚠️ Completed with errors" : "✅ Saved";
const fileCount = `${results.length} ${results.length === 1 ? "file" : "files"}`;
console.error(
`${status}: ${fileCount} to ${args.output} (${formatSize(totalBytes)} / 125 KB)`
);
}
process.exit(hasErrors ? 1 : 0);
}
/**
* Process files from config file
* @param {object} config - Validated config object
* @param {object} args - Parsed command-line arguments
*/
function processConfigFile(config, args) {
let totalBytes = 0;
let totalFilesProcessed = 0;
let hasErrors = false;
// Use output from config or args
const outputFile = args.output || config.output;
const trackSize = args["track-size"] || config.trackSize || false;
if (!outputFile) {
console.error(
"❌ Config mode requires output file. Specify in config file or use --output flag"
);
process.exit(1);
}
// Read existing file size if output file exists
if (fs.existsSync(outputFile)) {
const stats = fs.statSync(outputFile);
totalBytes = stats.size;
if (trackSize) {
console.error(`📄 ${outputFile}: ${formatSize(totalBytes)} (existing)`);
}
}
// VALIDATE ALL FILES FIRST - before writing anything
const validationErrors = [];
for (const section of config.sections) {
for (const fileArg of section.files) {
const validation = validateFile(fileArg);
if (!validation.valid) {
validationErrors.push({
fileArg: validation.fileArg,
error: validation.error,
section: section.header || "(no header)",
});
}
}
}
// If any validation errors, report them all and exit without modifying output
if (validationErrors.length > 0) {
console.error(
`❌ Validation failed for ${validationErrors.length} file(s):\n`
);
for (const { fileArg, error, section } of validationErrors) {
console.error(` • "${fileArg}" in section "${section}":`);
console.error(` ${error.replace(/\n/g, "\n ")}`);
console.error("");
}
console.error("⚠️ No files were written to avoid partial output.");
process.exit(1);
}
// Process each section
for (const [sectionIndex, section] of config.sections.entries()) {
if (trackSize) {
console.error(
`[Section ${sectionIndex + 1}/${config.sections.length}] ${section.header || "(no header)"}`
);
}
// Add section header if specified
if (section.header) {
const headerContent = `### ${section.header}\n\n`;
fs.appendFileSync(outputFile, headerContent, "utf8");
totalBytes += Buffer.byteLength(headerContent, "utf8");
}
// Process each file in section
for (const [fileIndex, fileArg] of section.files.entries()) {
try {
const result = processFile(fileArg);
const content = result + "\n\n";
const contentSize = Buffer.byteLength(content, "utf8");
totalBytes += contentSize;
// Write to file
fs.appendFileSync(outputFile, content, "utf8");
totalFilesProcessed++;
// Show progress if tracking size
if (trackSize) {
const percent = ((totalBytes / MAX_SIZE_BYTES) * 100).toFixed(1);
const filename = path.basename(fileArg.split(":")[0]);
console.error(
` [${fileIndex + 1}/${section.files.length}] ${filename} → +${formatSize(contentSize)} (${formatSize(totalBytes)} / 125 KB, ${percent}%)`
);
// Check thresholds
if (totalBytes >= MAX_SIZE_BYTES) {
console.error(
`❌ Error: Exceeded 125 KB limit (${formatSize(totalBytes)})`
);
console.error(
` Stop processing to stay within expert consultation limits`
);
process.exit(1);
} else if (totalBytes >= WARNING_THRESHOLD_2) {
console.error(`⚠️ Very close to 125 KB limit!`);
} else if (totalBytes >= WARNING_THRESHOLD_1) {
console.error(`⚠️ Approaching 100 KB`);
}
}
} catch (error) {
console.error(
`❌ Error processing "${fileArg}" in section "${section.header || "(no header)"}": ${error.message}`
);
hasErrors = true;
}
}
}
if (trackSize) {
const status = hasErrors ? "⚠️ Completed with errors" : "✅ Saved";
const fileCount = `${totalFilesProcessed} ${totalFilesProcessed === 1 ? "file" : "files"}`;
const sectionCount = `${config.sections.length} ${config.sections.length === 1 ? "section" : "sections"}`;
console.error(
`${status}: ${fileCount}, ${sectionCount} to ${outputFile} (${formatSize(totalBytes)} / 125 KB)`
);
}
process.exit(hasErrors ? 1 : 0);
}
// ============================================================================
// Entry Point
// ============================================================================
// Handle unhandled errors
process.on("unhandledRejection", (err) => {
console.error("❌ Error:", err.message);
process.exit(1);
});
// Run the script only if executed directly (not imported)
const scriptPath = path.normalize(process.argv[1]);
const modulePath = path.normalize(fileURLToPath(import.meta.url));
if (modulePath === scriptPath) {
try {
main();
} catch (err) {
console.error("❌ Error:", err.message);
process.exit(1);
}
}