@handykit/cli
Version:
A collection of handy CLI tools
215 lines β’ 7.86 kB
JavaScript
import fs from "fs/promises";
import path from "path";
import { exec } from "child_process";
import util from "util";
const execPromise = util.promisify(exec);
/**
* Display outdated dependencies in a clean, readable format,
* including their location and who depends on them.
*/
export async function showOutdatedPackages() {
try {
const { stdout } = await execPromise("npm outdated --long --json", {
cwd: process.cwd(),
});
const data = stdout.trim();
if (!data) {
console.log("β
All dependencies are up to date.\n");
return;
}
const outdated = JSON.parse(data);
const entries = Object.entries(outdated);
if (entries.length === 0) {
console.log("β
All dependencies are up to date.\n");
return;
}
console.log("π¦ Outdated dependencies:\n");
for (const [pkg, info] of entries) {
console.log(`β’ ${pkg}`);
console.log(` β’ Current: ${info.current}`);
console.log(` β’ Wanted: ${info.wanted}`);
console.log(` β’ Latest: ${info.latest}`);
if (info.location)
console.log(` β’ Location: ${info.location}`);
if (info["depended by"])
console.log(` β’ Depended by: ${info["depended by"]}`);
if (info.type)
console.log(` β’ Type: ${info.type}`);
console.log(""); // blank line for spacing
}
}
catch (err) {
// Handle non-zero exit code but valid stdout
if (err.stdout) {
try {
const outdated = JSON.parse(err.stdout);
const entries = Object.entries(outdated);
if (entries.length === 0) {
console.log("β
All dependencies are up to date.\n");
return;
}
console.log("π¦ Outdated dependencies:\n");
for (const [pkg, info] of entries) {
console.log(`β’ ${pkg}`);
console.log(` β’ Current: ${info.current}`);
console.log(` β’ Wanted: ${info.wanted}`);
console.log(` β’ Latest: ${info.latest}`);
if (info.location)
console.log(` β’ Location: ${info.location}`);
if (info["depended by"])
console.log(` β’ Depended by: ${info["depended by"]}`);
if (info.type)
console.log(` β’ Type: ${info.type}`);
console.log("");
}
return;
}
catch {
// Parsing failedβfall through to error
}
}
console.error("β Failed to get outdated packages:", err.stderr || err.message);
}
}
/**
* Upgrade dependencies by type: standard (npm update), minor, or major.
* Supports dry-run mode to preview changes.
*/
export async function manageDependencies(upgradeType, dryRun = false) {
const pkgPath = path.resolve(process.cwd(), "package.json");
let pkg;
try {
const pkgRaw = await fs.readFile(pkgPath, "utf8");
pkg = JSON.parse(pkgRaw);
}
catch (err) {
console.error("β Failed to read package.json at", pkgPath);
return;
}
// Fetch outdated packages upfront
let outdated = {};
try {
const { stdout } = await execPromise("npm outdated --json", {
cwd: process.cwd(),
});
if (stdout.trim()) {
outdated = JSON.parse(stdout);
}
}
catch (err) {
if (err.stdout) {
try {
const parsed = JSON.parse(err.stdout);
outdated = Object.keys(parsed).length === 0 ? {} : parsed;
}
catch {
console.error("β Failed to parse npm outdated output:", err.stdout);
return;
}
}
else {
console.error("β Failed to get outdated packages:", err.stderr || err.message);
return;
}
}
const allDeps = {
...(pkg.dependencies || {}),
...(pkg.devDependencies || {}),
};
if (upgradeType === "standard") {
// npm update respects semver ranges and upgrades to 'wanted'
const updatable = Object.entries(outdated).filter(([_, info]) => info.wanted !== info.current);
if (dryRun) {
if (updatable.length === 0) {
console.log("β
All dependencies are up to date. Nothing to update.");
return;
}
console.log("π§ͺ Dry-run: The following packages would be updated with `npm update`:\n");
for (const [pkg, info] of updatable) {
console.log(`β’ ${pkg}: ${info.current} β ${info.wanted}`);
}
console.log("");
}
else {
console.log("π Running npm update...");
await runCommand("npm update --save");
}
return;
}
// For minor and major upgrades: filter packages by semver difference
const upgradesToApply = []; // [pkg, current, latest]
for (const [dep, currentVersion] of Object.entries(allDeps)) {
const outdatedInfo = outdated[dep];
if (!outdatedInfo)
continue; // up to date or not listed
const currentMajor = getMajor(currentVersion);
const currentMinor = getMinor(currentVersion);
const latestVersion = outdatedInfo.latest;
const latestMajor = getMajor(latestVersion);
const latestMinor = getMinor(latestVersion);
const isMinor = latestMajor === currentMajor && latestMinor > currentMinor;
const isMajor = latestMajor > currentMajor;
if ((upgradeType === "minor" && isMinor) ||
(upgradeType === "major" && isMajor)) {
upgradesToApply.push([dep, currentVersion, latestVersion]);
}
}
if (dryRun) {
if (upgradesToApply.length === 0) {
console.log(`β
All dependencies are up to date for ${upgradeType} upgrades.`);
return;
}
console.log(`π§ͺ Dry-run: The following packages would be upgraded (${upgradeType}):\n`);
for (const [dep, current, latest] of upgradesToApply) {
console.log(`β’ ${dep}: ${current} β ${latest}`);
}
console.log("");
}
else {
for (const [dep, , latest] of upgradesToApply) {
console.log(`β¬οΈ Upgrading ${dep} to version ${latest}...`);
await runCommand(`npm install ${dep}@${latest} --save-exact`);
}
console.log("\nβ
Dependency upgrades complete.\n");
}
}
/**
* Parse major version number from semver string.
*/
function getMajor(version) {
return parseInt(version.replace(/^[^\d]*/, "").split(".")[0] || "0", 10);
}
/**
* Parse minor version number from semver string.
*/
function getMinor(version) {
return parseInt(version.replace(/^[^\d]*/, "").split(".")[1] || "0", 10);
}
/**
* Get the latest published version of a package.
*/
async function getLatestVersion(pkgName) {
try {
const { stdout } = await execPromise(`npm view ${pkgName} version`, {
cwd: process.cwd(),
});
return stdout.trim();
}
catch {
return null;
}
}
/**
* Run a command in the CWD and stream output to console.
*/
async function runCommand(cmd) {
console.log(`> ${cmd}`);
const { stdout, stderr } = await execPromise(cmd, {
cwd: process.cwd(),
});
if (stdout)
process.stdout.write(stdout);
if (stderr)
process.stderr.write(stderr);
}
//# sourceMappingURL=deps-manage.js.map