blaze-install
Version:
A new package manager
1,181 lines ⢠61.1 kB
JavaScript
"use strict";
const { readPackageJson } = require("./readPackageJson");
const { readLockfile } = require("./readLockfile");
const { resolveDependencies } = require("./resolveDependencies");
const { installTree, runLifecycleScript } = require("./installTree");
const { writeLockfile } = require("./writeLockfile");
const fs = require("fs/promises");
const path = require("path");
const axios = require("axios");
const semver = require("semver");
const glob = require("glob");
const { spawn } = require("child_process");
const os = require("os");
const { version } = require('../package.json');
const chalk = require('chalk');
let boxen = require('boxen');
if (boxen && boxen.default)
boxen = boxen.default;
const Spinner = require('./spinner');
let plugins = [];
function parsePackageArg(arg) {
// e.g. lodash@4.17.21 or lodash@^4.17.0 or lodash@next
const match = arg.match(/^(@?[^@]+)(?:@(.+))?$/);
if (!match)
return { name: arg, range: undefined };
return { name: match[1], range: match[2] };
}
function isGithubOrTarballSpec(pkgName) {
// github:user/repo, user/repo, user/repo#branch, tarball URLs
return (/^github:[^/]+\/[^#]+(#.+)?$/.test(pkgName) ||
/^[^/]+\/[^#]+(#.+)?$/.test(pkgName) ||
/^https?:\/\/.+\.(tgz|tar\.gz)$/.test(pkgName));
}
async function resolveVersionOrRange(pkgName, rangeOrTag, { offline = false } = {}) {
if (isGithubOrTarballSpec(pkgName)) {
// Return as-is for special handling in installTree
return pkgName;
}
if (offline) {
// Try to resolve from local package.json or lockfile
let pkg, lock;
try {
pkg = JSON.parse(await fs.readFile("package.json", "utf-8"));
}
catch { }
try {
lock = JSON.parse(await fs.readFile("blaze-lock.json", "utf-8"));
}
catch { }
// Prefer lockfile
if (lock && lock[pkgName]) {
return `^${lock[pkgName].version || lock[pkgName]}`;
}
if (pkg && pkg.dependencies && pkg.dependencies[pkgName]) {
return pkg.dependencies[pkgName];
}
if (pkg && pkg.devDependencies && pkg.devDependencies[pkgName]) {
return pkg.devDependencies[pkgName];
}
throw new Error(`Offline mode: Cannot resolve version for ${pkgName}. Not found in local package.json or lockfile.`);
}
const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}`;
const { data } = await axios.get(url);
if (!rangeOrTag || rangeOrTag === "latest") {
return `^${data["dist-tags"].latest}`;
}
// If it's a known tag
if (data["dist-tags"][rangeOrTag]) {
return `^${data["dist-tags"][rangeOrTag]}`;
}
// If it's a semver range, resolve to the max satisfying version
const versions = Object.keys(data.versions);
const max = semver.maxSatisfying(versions, rangeOrTag);
if (max) {
return rangeOrTag; // keep the range in package.json
}
// If it's not a valid semver or tag, show error
console.error(`Unknown tag or invalid version/range: '${rangeOrTag}' for package '${pkgName}'.`);
process.exit(1);
}
async function addDependencyToPackageJson(pkgName, versionOrRange, dev) {
const pkgPath = path.resolve(process.cwd(), "package.json");
const pkg = await readPackageJson();
if (dev) {
pkg.devDependencies = pkg.devDependencies || {};
pkg.devDependencies[pkgName] = versionOrRange;
console.log(`Added ${pkgName}@${versionOrRange} to devDependencies in package.json.`);
}
else {
pkg.dependencies = pkg.dependencies || {};
pkg.dependencies[pkgName] = versionOrRange;
console.log(`Added ${pkgName}@${versionOrRange} to dependencies in package.json.`);
}
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), "utf-8");
}
async function readWorkspacePackageJsons(workspaces) {
const allDeps = {};
const allDevDeps = {};
const workspacePaths = [];
for (const pattern of workspaces) {
const matches = glob.sync(pattern, { cwd: process.cwd(), absolute: true });
for (const wsPath of matches) {
const pkgPath = path.join(wsPath, "package.json");
try {
const data = await fs.readFile(pkgPath, "utf-8");
const pkg = JSON.parse(data);
Object.assign(allDeps, pkg.dependencies || {});
Object.assign(allDevDeps, pkg.devDependencies || {});
workspacePaths.push(wsPath);
}
catch { }
}
}
return { allDeps, allDevDeps, workspacePaths };
}
async function fetchLatestVersion(pkgName) {
const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}`;
const { data } = await axios.get(url);
return data["dist-tags"].latest;
}
async function uninstallPackage(pkgName) {
const pkgPath = path.resolve(process.cwd(), "package.json");
const pkg = await readPackageJson();
let removed = false;
if (pkg.dependencies && pkg.dependencies[pkgName]) {
delete pkg.dependencies[pkgName];
removed = true;
}
if (pkg.devDependencies && pkg.devDependencies[pkgName]) {
delete pkg.devDependencies[pkgName];
removed = true;
}
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), "utf-8");
// Remove from node_modules
const modPath = path.join(process.cwd(), "node_modules", pkgName);
try {
await fs.rm(modPath, { recursive: true, force: true });
}
catch { }
if (removed) {
console.log(`Uninstalled ${pkgName}.`);
}
else {
console.log(`${pkgName} was not found in dependencies.`);
}
}
async function updatePackage(pkgName, dev) {
const versionOrRange = `^${await fetchLatestVersion(pkgName)}`;
await addDependencyToPackageJson(pkgName, versionOrRange, dev);
console.log(`Updated ${pkgName} to ${versionOrRange}.`);
}
async function auditPackages() {
// Plugin hook
for (const plugin of plugins) {
if (typeof plugin.beforeAudit === "function") {
await plugin.beforeAudit({ context: { cwd: process.cwd() } });
}
}
const lock = await readLockfile();
if (!lock) {
console.log(boxen(chalk.bold.cyan('Usage: blaze <command> [options]'), { padding: 1, borderColor: 'cyan', borderStyle: 'round' }));
console.log(chalk.cyan('ā¹ļø'), chalk.gray('No blaze-lock.json found. Run blaze install first.'));
return;
}
// Build dependencies object in npm audit format
const dependencies = {};
for (const [name, info] of Object.entries(lock)) {
dependencies[name] = { version: info.version };
}
const payload = {
name: "blaze-install",
version: "0.0.0",
dependencies,
};
const spinner = new Spinner("Running security audit");
spinner.start();
try {
const { data } = await axios.post("https://registry.npmjs.org/-/npm/v1/security/audits", payload, {
headers: { "Content-Type": "application/json" },
});
if (data.metadata &&
data.metadata.vulnerabilities &&
data.metadata.vulnerabilities.total === 0) {
spinner.stop(chalk.green('No known vulnerabilities found!'));
await new Promise((r) => setTimeout(r, 10));
return;
}
if (data.advisories) {
let found = 0;
for (const key in data.advisories) {
const advisory = data.advisories[key];
found++;
console.log(chalk.red.bold(`\nVULNERABILITY: ${advisory.module_name}@${advisory.findings[0].version}`));
console.log(chalk.red(` Severity: ${advisory.severity}`));
console.log(chalk.yellow(` Title: ${advisory.title}`));
console.log(chalk.gray(` URL: ${advisory.url}`));
console.log(chalk.gray(` Vulnerable: ${advisory.vulnerable_versions}`));
console.log();
}
spinner.stop(chalk.red.bold(`Found ${found} vulnerable packages!`), false);
await new Promise((r) => setTimeout(r, 10));
}
else {
spinner.stop(chalk.green('Found 0 vulnerable packages!'));
await new Promise((r) => setTimeout(r, 10));
}
}
catch (err) {
spinner.stop(`Could not audit: ${err.message}`, false);
}
// Plugin hook
}
async function pruneLockfile() {
const { readLockfile } = require("./readLockfile");
const { writeLockfile } = require("./writeLockfile");
const { resolveDependencies } = require("./resolveDependencies");
const pkg = await readPackageJson();
const deps = pkg.dependencies || {};
const devDeps = pkg.devDependencies || {};
const allDeps = { ...deps, ...devDeps };
if (Object.keys(allDeps).length === 0) {
await writeLockfile({});
return;
}
const prunedTree = await resolveDependencies(allDeps);
await writeLockfile(prunedTree);
}
function printHelp() {
console.log(chalk.bold.cyan("\nš blaze-install: A fast, modern alternative to npm install\n"));
console.log(chalk.bold.white("Usage:"));
console.log(chalk.gray(" blaze <command> [options]\n"));
console.log(chalk.bold.white("Commands:"));
console.log(chalk.green(" init") + " Create a new package.json file");
console.log(chalk.green(" install [package]") + " Install all or a specific package");
console.log(chalk.green(" uninstall <package>") + " Remove a package and prune lockfile");
console.log(chalk.green(" update <package>") + " Update a package to the latest version");
console.log(chalk.green(" run <script>") + " Run a script defined in package.json");
console.log(chalk.green(" audit") + " Run a security audit");
console.log(chalk.green(" list") + " List installed packages");
console.log(chalk.green(" clean") + " Remove node_modules and cache");
console.log(chalk.green(" outdated") + " Show outdated dependencies");
console.log(chalk.green(" info <package>") + " Show info about a package");
console.log(chalk.green(" graph") + " Generate a dependency graph");
console.log(chalk.green(" help, --help") + " Show this help message");
console.log(chalk.green(" prefetch") + " Prefetch/cache all dependencies for offline use");
console.log(chalk.green(" fix") + " Run all available auto-fixers (lint, deps, doctor, audit)\n");
console.log(chalk.bold.white("Options:"));
console.log(chalk.yellow(" --save-dev") + " Add to devDependencies");
console.log(chalk.yellow(" --production") + " Only install production dependencies");
console.log(chalk.yellow(" --symlink") + " Use symlinks instead of copying");
console.log(chalk.yellow(" --no-symlink") + " Disable symlinks (use copy instead). Takes precedence over --symlink if both are provided.");
console.log(chalk.yellow(" --audit-fix") + " Run a security audit and fix after install");
console.log(chalk.yellow(" --no-lockfile") + " Do not use or write blaze-lock.json (lockfile-less mode)");
console.log(chalk.yellow(" --ci") + " Remove node_modules before install (like npm ci)");
console.log(chalk.yellow(" --offline") + " Use only local cache for installs");
console.log(chalk.yellow(" --doctor") + " Diagnose and fix common project issues\n");
console.log(chalk.bold.white("Examples:"));
console.log(chalk.gray(" blaze install"));
console.log(chalk.gray(" blaze install lodash"));
console.log(chalk.gray(" blaze install --audit-fix"));
console.log(chalk.gray(" blaze install --no-lockfile"));
console.log(chalk.gray(" blaze install --ci"));
console.log(chalk.gray(" blaze uninstall lodash"));
console.log(chalk.gray(" blaze update lodash"));
console.log(chalk.gray(" blaze run build"));
console.log(chalk.gray(" blaze run test"));
console.log(chalk.gray(" blaze audit"));
console.log(chalk.gray(" blaze list"));
console.log(chalk.gray(" blaze clean"));
console.log(chalk.gray(" blaze outdated"));
console.log(chalk.gray(" blaze info lodash"));
console.log(chalk.gray(" blaze graph"));
console.log(chalk.gray(" blaze install [pkg] [--offline]"));
console.log(chalk.gray(" blaze doctor"));
console.log(chalk.gray(" blaze prefetch\n"));
}
async function loadBlazerc() {
try {
const configPath = path.join(process.cwd(), ".blazerc");
const data = await fs.readFile(configPath, "utf-8");
return JSON.parse(data);
}
catch {
return {};
}
}
// Plugin loader
async function loadPlugins() {
const path = require('path');
const glob = require('glob');
const fs = require('fs');
// Look for plugins in both the CLI root and the current project
const roots = [
path.join(__dirname, '../plugins'), // CLI root
path.join(process.cwd(), 'plugins'), // Project root
];
let plugins = [];
for (const pluginsDir of roots) {
try {
if (fs.existsSync(pluginsDir)) {
const files = glob.sync('*.js', { cwd: pluginsDir, absolute: true });
for (const file of files) {
try {
const plugin = require(file);
plugins.push(plugin);
}
catch (err) {
console.warn(`Failed to load plugin ${file}: ${err.message}`);
}
}
}
}
catch { }
}
return plugins;
}
function readNpmrc() {
const paths = [
path.join(os.homedir(), ".npmrc"),
path.join(process.cwd(), ".npmrc"),
];
let config = {};
for (const p of paths) {
try {
const data = require("fs").readFileSync(p, "utf-8");
// Object.assign(config, ini.parse(data)); // ini is not defined, comment out
}
catch { }
}
return config;
}
function getRegistryForPackage(pkgName, npmrc) {
// Per-scope registry: @scope:registry=https://...
const match = pkgName.match(/^@([^/]+)\//);
if (match) {
const scope = match[1];
const scoped = npmrc[`@${scope}:registry`];
if (scoped)
return scoped;
}
return npmrc.registry || "https://registry.npmjs.org/";
}
function getAuthForRegistry(registry, npmrc) {
// Normalize registry URL for token lookup
let reg = registry.replace(/^https?:/, "").replace(/\/$/, "");
let token = npmrc[`//${reg}/:_authToken`] || npmrc[`//${reg}:_authToken`];
if (!token && process.env.NPM_TOKEN)
token = process.env.NPM_TOKEN;
return token;
}
function getAxiosOptions(npmrc, registry) {
const opts = {};
if (npmrc.proxy)
opts.proxy = npmrc.proxy;
if (npmrc["strict-ssl"] === false || npmrc["strict-ssl"] === "false")
opts.httpsAgent = new (require("https").Agent)({
rejectUnauthorized: false,
});
if (npmrc.ca)
opts.ca = npmrc.ca;
return opts;
}
async function publishPackage() {
const config = readNpmrc();
const pkg = await readPackageJson();
const tar = require("tar");
const axios = require("axios");
const registry = getRegistryForPackage(pkg.name, config);
const token = getAuthForRegistry(registry, config);
const tarball = `${pkg.name}-${pkg.version}.tgz`;
// Pack the package
await tar.c({ gzip: true, file: tarball, cwd: process.cwd() }, ["."]);
// Read tarball
const data = require("fs").readFileSync(tarball);
// Publish
const url = `${registry.replace(/\/$/, "")}/${encodeURIComponent(pkg.name)}`;
try {
await axios.put(url, data, {
headers: {
"Content-Type": "application/octet-stream",
Authorization: token ? `Bearer ${token}` : undefined,
},
});
console.log("Published to", url);
}
catch (err) {
console.error("Publish failed:", err.response ? err.response.data : err.message);
}
require("fs").unlinkSync(tarball);
}
async function bumpVersion(newVersion) {
const pkgPath = path.join(process.cwd(), "package.json");
const pkg = JSON.parse(require("fs").readFileSync(pkgPath, "utf-8"));
pkg.version = newVersion;
require("fs").writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), "utf-8");
const { execSync } = require("child_process");
execSync(`git add package.json`);
execSync(`git commit -m "chore: bump version to ${newVersion}"`);
execSync(`git tag v${newVersion}`);
console.log(`Version bumped to ${newVersion} and git tag created.`);
}
async function auditAndFix() {
const lock = await readLockfile();
if (!lock) {
console.log(boxen(chalk.bold.cyan('Usage: blaze <command> [options]'), { padding: 1, borderColor: 'cyan', borderStyle: 'round' }));
console.log(chalk.cyan('ā¹ļø'), chalk.gray('No blaze-lock.json found. Run blaze install first.'));
return;
}
const dependencies = {};
for (const [name, info] of Object.entries(lock)) {
dependencies[name] = { version: info.version };
}
const payload = {
name: "blaze-install",
version: "0.0.0",
dependencies,
};
try {
const { data } = await require("axios").post("https://registry.npmjs.org/-/npm/v1/security/audits", payload, {
headers: { "Content-Type": "application/json" },
});
if (data.advisories && Object.keys(data.advisories).length > 0) {
let updated = false;
const pkg = await readPackageJson();
for (const key in data.advisories) {
const advisory = data.advisories[key];
const dep = advisory.module_name;
const latest = advisory.patched_versions
.replace(/[<>=^~| ]/g, "")
.split(",")
.pop();
if (pkg.dependencies && pkg.dependencies[dep]) {
pkg.dependencies[dep] = `^${latest}`;
updated = true;
console.log(`Updated ${dep} to ^${latest}`);
}
if (pkg.devDependencies && pkg.devDependencies[dep]) {
pkg.devDependencies[dep] = `^${latest}`;
updated = true;
console.log(`Updated ${dep} to ^${latest}`);
}
}
if (updated) {
require("fs").writeFileSync("package.json", JSON.stringify(pkg, null, 2), "utf-8");
console.log("Reinstalling dependencies...");
require("child_process").execSync("npm install", { stdio: "inherit" });
console.log("Dependencies updated and reinstalled.");
}
else {
console.log("No updatable vulnerable dependencies found.");
}
}
else {
console.log("No vulnerable dependencies found.");
}
}
catch (err) {
console.warn(`Could not audit: ${err.message}`);
}
}
async function runScript(scriptName) {
const pkgDir = process.cwd();
const pkg = await readPackageJson();
if (pkg.scripts && pkg.scripts[scriptName]) {
await runLifecycleScript(pkgDir, scriptName, pkg.name);
}
else {
console.log(`No script named '${scriptName}' in package.json.`);
}
}
const GLOBAL_LINKS_DIR = path.join(os.homedir(), ".blaze-links");
async function blazeLink() {
const pkg = await readPackageJson();
await fs.mkdir(GLOBAL_LINKS_DIR, { recursive: true });
const linkPath = path.join(GLOBAL_LINKS_DIR, pkg.name);
try {
await fs.rm(linkPath, { recursive: true, force: true });
}
catch { }
try {
await fs.symlink(process.cwd(), linkPath, "dir");
console.log(`Linked ${pkg.name} globally at ${linkPath}`);
}
catch (err) {
if (err.code === "EPERM" || err.code === "EEXIST") {
await fs.cp(process.cwd(), linkPath, { recursive: true });
console.log(`Copied ${pkg.name} globally at ${linkPath} (symlink not permitted)`);
}
else {
throw err;
}
}
}
async function blazeUnlink() {
const pkg = await readPackageJson();
const linkPath = path.join(GLOBAL_LINKS_DIR, pkg.name);
try {
await fs.rm(linkPath, { recursive: true, force: true });
console.log(`Unlinked ${pkg.name} from global links.`);
}
catch {
console.log(`No global link found for ${pkg.name}.`);
}
}
async function blazeLinkInstall(pkgName) {
// Link a globally linked package into node_modules
await fs.mkdir("node_modules", { recursive: true });
const linkPath = path.join(GLOBAL_LINKS_DIR, pkgName);
const dest = path.join(process.cwd(), "node_modules", pkgName);
try {
await fs.rm(dest, { recursive: true, force: true });
}
catch { }
try {
await fs.symlink(linkPath, dest, "dir");
console.log(`Linked ${pkgName} into node_modules.`);
}
catch (err) {
if (err.code === "EPERM" || err.code === "EEXIST") {
await fs.cp(linkPath, dest, { recursive: true });
console.log(`Copied ${pkgName} into node_modules (symlink not permitted)`);
}
else {
throw err;
}
}
}
async function generateDependencyGraph() {
const { resolveDependencies } = require("./resolveDependencies");
const pkg = await readPackageJson();
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
if (Object.keys(deps).length === 0) {
console.log("No dependencies to graph.");
return;
}
const tree = await resolveDependencies(deps);
let graph = "graph TD;\n";
const nodes = new Set();
function addNode(name, version) {
const safeVersion = String(version || "local");
const id = `${name.replace(/[@/]/g, "_")}_${safeVersion.replace(/[\^~.]/g, "_")}`;
if (!nodes.has(id)) {
graph += ` ${id}["${name}@${safeVersion}"];\n`;
nodes.add(id);
}
return id;
}
for (const [name, info] of Object.entries(tree)) {
const parentId = addNode(name, info.version);
if (info.dependencies) {
for (const [depName, depInfo] of Object.entries(info.dependencies)) {
const childId = addNode(depName, depInfo.version);
graph += ` ${parentId} --> ${childId};\n`;
}
}
}
console.log(graph);
}
async function cleanGithubSpecs() {
const glob = require('glob');
const fs = require('fs/promises');
const path = require('path');
const pkgs = glob.sync('**/package.json', { ignore: 'node_modules/**' });
let cleaned = 0;
for (const pkgPath of pkgs) {
const absPath = path.resolve(pkgPath);
let changed = false;
let pkg;
try {
pkg = JSON.parse(await fs.readFile(absPath, 'utf-8'));
}
catch {
continue;
}
for (const depType of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
if (!pkg[depType])
continue;
for (const dep of Object.keys(pkg[depType])) {
if (/^(github:|[\w-]+\/[\w-]+(#.+)?$|https?:\/\/)/.test(dep)) {
delete pkg[depType][dep];
changed = true;
}
}
if (Object.keys(pkg[depType]).length === 0)
delete pkg[depType];
}
if (changed) {
await fs.writeFile(absPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
console.log(chalk.yellow(`Cleaned non-npm specs from ${pkgPath}`));
cleaned++;
}
}
if (cleaned === 0) {
console.log(chalk.green('No non-npm specs found in any package.json.'));
}
else {
console.log(chalk.green(`Cleaned ${cleaned} package.json file(s).`));
}
}
async function main(args) {
const startTime = process.hrtime.bigint();
// Welcome banner
console.log(boxen(`${chalk.bold.cyan('š blaze-install')} ${chalk.gray(`v${version}`)}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan', align: 'center' }));
// Arguments (only if debug mode is on)
if (args.includes('--debug')) {
console.log(chalk.gray('Arguments:'), chalk.magenta(JSON.stringify(args)));
}
// Lockfile status (more subtle)
const lockfileExists = await fs.access('blaze-lock.json').then(() => true, () => false);
if (lockfileExists) {
console.log(chalk.green('š Using blaze-lock.json for installation.'));
}
else {
console.log(chalk.yellow('ā ļø No blaze-lock.json found. A new one will be generated after install.'));
}
console.log(); // Add a newline for spacing
try {
const blazerc = await loadBlazerc();
plugins = await loadPlugins();
const config = { ...blazerc, ...readNpmrc() };
const saveDev = args.includes("--save-dev");
const production = args.includes("--production");
const useSymlinks = (args.includes("--symlink") || config.symlink) && !args.includes("--no-symlink");
const jsonOutput = args.includes("--json");
// Handle --interactive flag
if (args.includes("--interactive") && process.stdout.isTTY) {
const inquirer = (await import("inquirer")).default;
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: "What would you like to do?",
choices: [
{ name: "Install a package", value: "install" },
{ name: "Uninstall a package", value: "uninstall" },
{ name: "Update a package", value: "update" },
{ name: "Audit dependencies", value: "audit" },
{ name: "List installed packages", value: "list" },
{ name: "Clean node_modules and cache", value: "clean" },
{ name: "Show outdated dependencies", value: "outdated" },
{ name: "Show info about a package", value: "info" },
{ name: "Generate dependency graph", value: "graph" },
{ name: "Exit", value: "exit" },
],
},
]);
if (action === "exit")
return;
let pkgName = "";
if (["install", "uninstall", "update", "info", "graph"].includes(action)) {
const answer = await inquirer.prompt([
{
type: "input",
name: "pkg",
message: `Package name:`,
when: () => action !== "install" || true,
},
]);
pkgName = answer.pkg;
}
// Re-run main with the selected action and package name
if (pkgName) {
await main([action, pkgName]);
}
else {
await main([action]);
}
return;
}
// Add this after argument parsing, before command dispatch
if (args.includes('--version') || args.includes('-v')) {
console.log(`blaze-install version ${version}`);
process.exit(0);
}
const [command, ...rest] = args;
// Help command
if (command === "help" || command === "--help") {
printHelp();
return;
}
// Prefetch command (must be before install handler)
if (command === "prefetch") {
// Prefetch/cache all dependencies and tarballs for offline use
const rootPkg = await readPackageJson();
let depsToPrefetch = {};
if (rootPkg.workspaces && Array.isArray(rootPkg.workspaces)) {
const { allDeps, allDevDeps } = await readWorkspacePackageJsons(rootPkg.workspaces);
depsToPrefetch = {
...(rootPkg.dependencies || {}),
...(rootPkg.devDependencies || {}),
...allDeps,
...allDevDeps,
};
}
else {
depsToPrefetch = {
...(rootPkg.dependencies || {}),
...(rootPkg.devDependencies || {}),
};
}
if (Object.keys(depsToPrefetch).length === 0) {
console.log("No dependencies to prefetch.");
return;
}
const { prefetchAll } = require("./prefetch");
await prefetchAll(depsToPrefetch);
return;
}
// Call onCommand plugin hook
for (const plugin of plugins) {
if (typeof plugin.onCommand === "function") {
await plugin.onCommand({
command,
args,
context: { cwd: process.cwd() },
});
}
}
// --- blaze fix command ---
if (command === "fix") {
console.log("Running all available auto-fixers (lint, deps, doctor, audit)...");
// Lint/Prettier fix
try {
require("./plugins/eslintPrettierRunner.js").runLintersFix();
}
catch (e) {
console.warn("[fix] Lint/Prettier auto-fix failed:", e.message);
}
// Remove unused dependencies
try {
require("./plugins/unusedDependencyLinter.js").removeUnusedDeps();
}
catch (e) {
console.warn("[fix] Unused dependency removal failed:", e.message);
}
// Update outdated dependencies
try {
await require("./plugins/outdatedDependencyNotifier.js").updateOutdatedDeps();
}
catch (e) {
console.warn("[fix] Outdated dependency update failed:", e.message);
}
// Run doctor with fix
try {
await require("./diagnostics").runDoctor(true);
}
catch (e) {
console.warn("[fix] Doctor auto-fix failed:", e.message);
}
// Run auditAndFix if available
try {
if (typeof auditAndFix === "function") {
await auditAndFix();
}
}
catch (e) {
console.warn("[fix] Audit auto-fix failed:", e.message);
}
console.log("All auto-fixers complete. Please review the output above for any manual steps.");
return;
}
if (command === "uninstall") {
if (!rest[0]) {
console.log("Usage: blaze uninstall <package>");
return;
}
// Plugin hook
for (const plugin of plugins) {
if (typeof plugin.beforeUninstall === "function") {
await plugin.beforeUninstall({
args,
context: { cwd: process.cwd() },
});
}
}
// Run preuninstall/uninstall/postuninstall scripts
const pkgDir = path.join(process.cwd(), "node_modules", rest[0]);
await runLifecycleScript(pkgDir, "preuninstall", rest[0]);
await runLifecycleScript(pkgDir, "uninstall", rest[0]);
await uninstallPackage(rest[0]);
await runLifecycleScript(pkgDir, "postuninstall", rest[0]);
await pruneLockfile();
// Add a short delay to avoid race conditions on Windows
await new Promise((res) => setTimeout(res, 100));
// Plugin hook
for (const plugin of plugins) {
if (typeof plugin.afterUninstall === "function") {
await plugin.afterUninstall({
args,
context: { cwd: process.cwd() },
});
}
}
console.log("Running install to update dependencies...");
await main(["install"]);
return;
}
if (command === "update") {
// Plugin hook
for (const plugin of plugins) {
if (typeof plugin.beforeUpdate === "function") {
await plugin.beforeUpdate({ args, context: { cwd: process.cwd() } });
}
}
if (!rest[0]) {
console.log("Usage: blaze update <package>");
return;
}
const pkg = await readPackageJson();
const isDev = pkg.devDependencies && pkg.devDependencies[rest[0]];
await updatePackage(rest[0], isDev);
// Plugin hook
for (const plugin of plugins) {
if (typeof plugin.afterUpdate === "function") {
await plugin.afterUpdate({ args, context: { cwd: process.cwd() } });
}
}
}
if (command === "list") {
const pkg = await readPackageJson();
const deps = pkg.dependencies || {};
const devDeps = pkg.devDependencies || {};
const fsSync = require("fs");
const path = require("path");
function isInstalled(name) {
try {
return fsSync.existsSync(path.join(process.cwd(), "node_modules", name));
}
catch {
return false;
}
}
console.log(chalk.yellow("(Note: This list is based on package.json and checks node_modules for actual installs.)"));
console.log(chalk.bold("\nInstalled dependencies:"));
if (Object.keys(deps).length === 0) {
console.log(chalk.gray(" (none)"));
}
else {
for (const [name, version] of Object.entries(deps)) {
if (isInstalled(name)) {
console.log(chalk.green(` ${name}@${version} (installed)`));
}
else {
console.log(chalk.red(` ${name}@${version} (missing in node_modules)`));
}
}
}
console.log(chalk.bold("\nInstalled devDependencies:"));
if (Object.keys(devDeps).length === 0) {
console.log(chalk.gray(" (none)"));
}
else {
for (const [name, version] of Object.entries(devDeps)) {
if (isInstalled(name)) {
console.log(chalk.cyan(` ${name}@${version} (installed)`));
}
else {
console.log(chalk.red(` ${name}@${version} (missing in node_modules)`));
}
}
}
// Check for orphaned node_modules
const nodeModulesPath = path.join(process.cwd(), "node_modules");
if (fsSync.existsSync(nodeModulesPath)) {
const installed = fsSync
.readdirSync(nodeModulesPath)
.filter((f) => !f.startsWith("."));
const allDeclared = [...Object.keys(deps), ...Object.keys(devDeps)];
const orphaned = installed.filter((name) => !allDeclared.includes(name));
if (orphaned.length > 0) {
console.log(chalk.yellow("\nOrphaned packages in node_modules (not listed in package.json):"));
for (const name of orphaned) {
console.log(chalk.yellow(` ${name}`));
}
}
}
console.log();
return;
}
if (command === "clean") {
// Plugin hook
for (const plugin of plugins) {
if (typeof plugin.beforeClean === "function") {
await plugin.beforeClean({ context: { cwd: process.cwd() } });
}
}
let removed = false;
async function tryRemove(target) {
try {
await fs.rm(target, { recursive: true, force: true });
console.log(chalk.green(`Removed ${target}`));
removed = true;
}
catch (err) {
// Only print if not ENOENT
if (err.code !== "ENOENT") {
console.log(chalk.red(`Failed to remove ${target}: ${err.message}`));
}
}
}
await tryRemove(path.join(process.cwd(), "node_modules"));
await tryRemove(path.join(process.cwd(), ".cache"));
await tryRemove(path.join(process.cwd(), "node_modules", ".cache"));
if (!removed) {
console.log(chalk.yellow("Nothing to clean."));
}
// Plugin hook
for (const plugin of plugins) {
if (typeof plugin.afterClean === "function") {
await plugin.afterClean({ context: { cwd: process.cwd() } });
}
}
return;
}
if (command === "outdated") {
const pkg = await readPackageJson();
const deps = pkg.dependencies || {};
const devDeps = pkg.devDependencies || {};
const all = { ...deps, ...devDeps };
const axios = require("axios");
const semver = require("semver");
const pad = (str, len) => str + " ".repeat(Math.max(0, len - str.length));
const { readLockfile } = require("./readLockfile");
const lock = await readLockfile();
if (Object.keys(all).length === 0) {
console.log(chalk.yellow("No dependencies found."));
return;
}
console.log(chalk.bold("\nOutdated dependencies:"));
console.log(pad("Package", 25) + pad("Current", 15) + pad("Latest", 15) + "Status");
for (const [name, current] of Object.entries(all)) {
try {
const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
const { data } = await axios.get(url);
const latest = data["dist-tags"].latest;
let status = "";
let installed = null;
if (lock && lock[name] && lock[name].version) {
installed = lock[name].version;
}
if (current === "latest" && installed) {
if (semver.eq(installed, latest)) {
status = chalk.green("Up to date");
}
else if (semver.lt(installed, latest)) {
status = chalk.red("Outdated");
}
else {
status = chalk.yellow("Unknown");
}
console.log(pad(name, 25) + pad(installed, 15) + pad(latest, 15) + status);
continue;
}
if (semver.validRange(current) &&
semver.lt(semver.minVersion(current), latest)) {
status = chalk.red("Outdated");
}
else {
status = chalk.green("Up to date");
}
console.log(pad(name, 25) + pad(current, 15) + pad(latest, 15) + status);
}
catch (err) {
console.log(pad(name, 25) +
pad(current, 15) +
pad("-", 15) +
chalk.yellow("Error fetching latest"));
}
}
console.log();
return;
}
if (command === "info") {
const axios = require("axios");
if (!rest[0]) {
console.log("Usage: blaze info <package>");
return;
}
const pkgName = rest[0];
try {
const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}`;
const { data } = await axios.get(url);
const latest = data["dist-tags"].latest;
const info = data.versions[latest];
console.log(chalk.bold(`\n${pkgName}`));
console.log(chalk.green("Latest version:"), latest);
if (info.description)
console.log(chalk.green("Description:"), info.description);
if (info.homepage)
console.log(chalk.green("Homepage:"), info.homepage);
if (info.repository && info.repository.url)
console.log(chalk.green("Repository:"), info.repository.url.replace(/^git\+/, ""));
if (info.license)
console.log(chalk.green("License:"), info.license);
if (info.maintainers) {
const maintainers = Array.isArray(info.maintainers)
? info.maintainers.map((m) => m.name).join(", ")
: info.maintainers;
console.log(chalk.green("Maintainers:"), maintainers);
}
console.log();
}
catch (err) {
console.log(chalk.red("Error fetching info for"), pkgName, "-", err.message);
}
return;
}
if (command === "publish") {
// Run prepublish, prepare, prepack, postpack, postpublish scripts
await runLifecycleScript(process.cwd(), "prepublish", "");
await runLifecycleScript(process.cwd(), "prepare", "");
await runLifecycleScript(process.cwd(), "prepack", "");
await publishPackage();
await runLifecycleScript(process.cwd(), "postpack", "");
await runLifecycleScript(process.cwd(), "postpublish", "");
return;
}
if (command === "version") {
if (!rest[0]) {
console.log("Usage: blaze version <newversion>");
return;
}
// Run preversion, version, postversion scripts
await runLifecycleScript(process.cwd(), "preversion", "");
await bumpVersion(rest[0]);
await runLifecycleScript(process.cwd(), "version", "");
await runLifecycleScript(process.cwd(), "postversion", "");
return;
}
if (command === "run") {
if (!rest[0]) {
console.log("Usage: blaze run <script>");
return;
}
await runScript(rest[0]);
return;
}
if (command === "graph") {
await generateDependencyGraph();
return;
}
if (command === "link") {
if (rest[0]) {
await blazeLinkInstall(rest[0]);
}
else {
await blazeLink();
}
return;
}
if (command === "unlink") {
await blazeUnlink();
return;
}
if (command === "audit") {
// Plugin hook
for (const plugin of plugins) {
if (typeof plugin.beforeAudit === "function") {
await plugin.beforeAudit({ context: { cwd: process.cwd() } });
}
}
await auditPackages();
// Plugin hook
for (const plugin of plugins) {
if (typeof plugin.afterAudit === "function") {
await plugin.afterAudit({ context: { cwd: process.cwd() } });
}
}
return;
}
if (command === "init") {
const fsSync = require("fs");
const pkgPath = path.join(process.cwd(), "package.json");
if (fsSync.existsSync(pkgPath)) {
console.log(chalk.yellow("ā ļø package.json already exists in this directory."));
return;
}
const defaultPkg = {
name: path.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, '-'),
version: "1.0.0",
description: "",
main: "index.js",
scripts: {
test: "echo \"Error: no test specified\" && exit 1"
},
keywords: [],
author: "",
license: "ISC"
};
await fs.writeFile(pkgPath, JSON.stringify(defaultPkg, null, 2) + "\n", "utf-8");
console.log(chalk.green("ā
Created package.json"));
console.log(chalk.cyan("š Edit package.json to add your project details"));
return;
}
if (command === "doctor") {
const fix = args.includes("--fix");
const diagnostics = require("./diagnostics");
await diagnostics.runDoctor(fix);
return;
}
if (command === "clean-github-specs") {
await cleanGithubSpecs();
process.exit(0);
}
console.log("Welcome to blaze-install!");
console.log("Arguments:", args);
if (command === "install" || command === undefined) {
const offline = args.includes("--offline");
const auditFix = args.includes("--audit-fix");
const noLockfile = args.includes("--no-lockfile");
const workspaceFlagIndex = args.findIndex((arg) => arg === "--workspace");
let workspaceTarget = null;
if (workspaceFlagIndex !== -1 && args[workspaceFlagIndex + 1]) {
workspaceTarget = args[workspaceFlagIndex + 1];
}
// Filter out CLI flags from rest (package names)
const rest = args.filter((arg) => !arg.startsWith("--") && arg !== command);
if (auditFix && noLockfile) {
console.error("Error: --audit-fix cannot be used with --no-lockfile. The audit requires a lockfile.");
process.exit(1);
}
for (const plugin of plugins) {
if (typeof plugin.beforeInstall === "function") {
await plugin.beforeInstall({ args, context: { cwd: process.cwd() } });
}
}
let addingPackage = rest.length >= 1 && rest[0];
let pkgName, range, versionOrRange;
let targetPkgPath = path.resolve(process.cwd(), "package.json");
let rootPkg = await readPackageJson();
let isMonorepo = Array.isArray(rootPkg.workspaces) && rootPkg.workspaces.length > 0;
let workspaceChoices = [];
if (isMonorepo) {
// Find all workspace package.json files
for (const wsGlob of rootPkg.workspaces) {
const wsAbsPath = path.resolve(process.cwd(), wsGlob);
const wsPkgPath = path.join(wsAbsPath, "package.json");
try {
await fs.access(wsPkgPath);
workspaceChoices.push({ name: wsGlob, value: wsPkgPath });
}
catch { }
}
// Add root as a choice
workspaceChoices.unshift({
name: "[root]",
value: path.resolve(process.cwd(), "package.json"),
});
}
// If adding a package, determine the target package.json
if (addingPackage && rest[0]) {
({ name: pkgName, range } = parsePackageArg(rest[0]));
versionOrRange = await resolveVersionOrRange(pkgName, range, {
offline,
});
// If --workspace flag is used
if (workspaceTarget) {
const found = workspaceChoices.find((w) => w.name === workspaceTarget || w.value.includes(workspaceTarget));
if (found) {
targetPkgPath = found.value;
}
else {
console.error(`Workspace '${workspaceTarget}' not found.`);
process.exit(1);
}
}
else if (isMonorepo) {
// If inside a workspace, use that workspace
const cwd = process.cwd();
const found = workspaceChoices.find((w) => cwd.startsWith(path.dirname(w.value)) &&
w.value !== path.resolve(process.cwd(), "package.json"));
if (found) {
targetPkgPath = found.value;
}
else {
// Prompt user to select target
const inquirer = (await import("inquirer")).default;
const { chosen } = await inquirer.prompt([
{
type: "list",
name