preamble
Version:
Automated License & Metadata applicators for Codebases.
225 lines (188 loc) • 6.42 kB
JavaScript
/* *******************************************************
* preamble
*
* @license
*
* Apache-2.0
*
* Copyright 2024 Alex Stevovich
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @meta
*
* package_name: preamble
* file_name: src/cli.js
* purpose: {{PURPOSE}}
*
* @system
*
* generated_on: 2025-03-09T02:11:28.478Z
* certified_version: 1.0.0
* file_uuid: cdd4332d-5f04-4a4e-a453-7854ac4ce38c
* file_size: 6423 bytes
* file_hash: 0d34fd40b0c619ded8b081a4bfe2104bc833aa6add67f4437e0f6a062e4cef66
* mast_hash: 9065cac05eb806118170eae0c5af6908d4a54a1cd7b6eae679a0981bab28c8c8
* generated_by: preamble on npm!
*
* [Preamble Metadata]
********************************************************/
import fs from 'fs/promises';
import path from 'path';
import { inscribeJs } from './index.js';
// Utility Functions
async function getPackageJson() {
try {
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
const packageData = await fs.readFile(packageJsonPath, 'utf-8');
return JSON.parse(packageData);
} catch (_error) {
console.warn(
'⚠️ Warning: Could not read package.json. Defaulting to empty object.',
);
return {};
}
}
async function findFilesByExtension(dir, extensions, ignoredPaths = []) {
const results = [];
async function searchDirectory(directory) {
const entries = await fs.readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (!ignoredPaths.includes(entry.name)) {
await searchDirectory(fullPath);
}
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
results.push(fullPath);
}
}
}
await searchDirectory(dir);
return results;
}
async function runSequentially(tasks, fn) {
for (const task of tasks) {
await fn(task);
}
}
// Parse CLI Arguments
const args = new Set(process.argv.slice(2));
if (args.has('--help')) {
console.log(`
Usage:
npx preamble --apply --dir <directory> [--template <preamble>]
Options:
--apply Required. Enables update mode.
--dir Required. Directory containing files to process.
--template Optional. Path to the preamble template (defaults to './.preamble').
--help Show this help message.
Examples:
npx preamble --apply --dir ./src/
npx preamble --apply --dir ./src/ --template ./internal/.preamble
Errors:
- Fails if --apply is missing.
- Fails if --dir is missing.
- Defaults to './.preamble' if --template is not provided.
`);
process.exit(0);
}
// Ensure Required Flags
if (!args.has('--apply')) {
console.error('❌ Error: --apply is required.');
process.exit(1);
}
if (!args.has('--dir')) {
console.error('❌ Error: --dir is required.');
process.exit(1);
}
// Extract `--dir` and `--template`
const dirIndex = process.argv.indexOf('--dir');
const templateIndex = process.argv.indexOf('--template');
if (dirIndex === -1 || !process.argv[dirIndex + 1]) {
console.error('❌ Error: No valid directory provided for --dir.');
process.exit(1);
}
const configPath = process.argv[dirIndex + 1];
const configMastPath =
templateIndex !== -1 && process.argv[templateIndex + 1]
? process.argv[templateIndex + 1]
: './.preamble';
runUpdateMode(configPath, configMastPath);
async function runUpdateMode(configPath, configMastPath) {
const projectRoot = process.cwd();
const resolvedPath = path.resolve(configPath);
configMastPath = path.resolve(configMastPath);
if (!resolvedPath.startsWith(projectRoot)) {
console.error(
`❌ Refusing to run outside project directory.\nProject root: ${projectRoot}\nAttempted root: ${resolvedPath}`,
);
process.exit(1);
}
const forbiddenRoots = ['/', 'C:\\', 'C:/'];
if (forbiddenRoots.includes(resolvedPath)) {
console.error(
`❌ Refusing to run on entire filesystem root (${resolvedPath}).`,
);
process.exit(1);
}
try {
await fs.access(configMastPath);
} catch {
console.error(`❌ Template not found at "${configMastPath}".`);
process.exit(1);
}
const extensions = ['.mjs', '.js', '.cjs', '.ts', '.tsx', '.jsx'];
const ignoredPaths = ['node_modules', '.git', 'dist', 'build'];
const template = await fs.readFile(configMastPath, 'utf-8');
const files = await findFilesByExtension(
resolvedPath,
extensions,
ignoredPaths,
);
const visibleFiles = files.filter(
(file) =>
!path
.relative(projectRoot, file)
.split(path.sep)
.some((part) => part.startsWith('.')),
);
const MAX_FILES = 1000;
if (visibleFiles.length > MAX_FILES) {
console.warn(
`⚠️ Warning: You're about to process ${visibleFiles.length} files.`,
);
}
const MAX_FILE_SIZE = 1024 * 100; // 100 KB
const pkg = await getPackageJson();
await runSequentially(files, async (file) => {
const stats = await fs.stat(file);
if (stats.size > MAX_FILE_SIZE) {
console.warn(
`⚠️ Skipping large file (${stats.size} bytes): ${file}`,
);
return;
}
const content = await fs.readFile(file, 'utf-8');
const data = {
file: { path: file, size: stats.size },
pkg,
};
const { result, updated } = await inscribeJs(content, template, data);
if (updated) {
await fs.writeFile(file, result, 'utf-8');
console.log('✅ Preamble applied to', file);
}
});
}