UNPKG

pncat

Version:

Enhanced pnpm catalogs management with advanced workspace dependency control.

990 lines (974 loc) 34 kB
import process from 'node:process'; import * as p from '@clack/prompts'; import c from 'ansis'; import { cac } from 'cac'; import { writeFile, readFile } from 'node:fs/promises'; import { findUp } from 'find-up'; import { dirname, join } from 'pathe'; import { parsePnpmWorkspaceYaml } from 'pnpm-workspace-yaml'; import { a as DEPS_FIELDS, p as parseSpec, s as sortSpecs, P as PnpmCatalogManager, b as DEPENDENCIES_TYPE_SHORT_MAP, c as DEFAULT_CATALOG_OPTIONS, D as DEFAULT_CATALOG_RULES, M as MODE_CHOICES } from './shared/pncat.DZVqskSj.mjs'; import { execa } from 'execa'; import { existsSync } from 'node:fs'; import { writePackageJSON, readPackageJSON as readPackageJSON$1 } from 'pkg-types'; import { diffLines } from 'diff'; import deepmerge from 'deepmerge'; import { createConfigLoader } from 'unconfig'; import 'tinyglobby'; import 'semver'; const name = "pncat"; const version = "0.5.4"; async function findWorkspaceRoot() { const workspaceYamlPath = await findWorkspaceYAML(); if (workspaceYamlPath) return dirname(workspaceYamlPath); return process.cwd(); } async function findWorkspaceYAML() { return await findUp("pnpm-workspace.yaml", { cwd: process.cwd() }); } async function ensureWorkspaceYAML() { let workspaceYamlPath = await findWorkspaceYAML(); if (!workspaceYamlPath) { const root = await findUp([".git", "pnpm-lock.yaml"], { cwd: process.cwd() }).then((r) => r ? dirname(r) : process.cwd()); p.log.warn(c.yellow("no pnpm-workspace.yaml found")); const result = await p.confirm({ message: `do you want to create it under project root ${c.dim(root)} ?` }); if (!result) { p.outro(c.red("aborting")); process.exit(1); } workspaceYamlPath = join(root, "pnpm-workspace.yaml"); await writeFile(workspaceYamlPath, "packages: []"); } const workspaceYaml = parsePnpmWorkspaceYaml(await readFile(workspaceYamlPath, "utf-8")); return { workspaceYaml, workspaceYamlPath }; } function parseArgs(args) { const options = {}; const deps = []; let i = 0; while (i < args.length) { const arg = args[i]; if (arg === "--") { deps.push(...args.slice(i + 1)); break; } if (arg.startsWith("--")) { const key = arg.slice(2); if (key.startsWith("no-")) { options[key.slice(3)] = false; i++; } else if (i + 1 < args.length && !args[i + 1].startsWith("-")) { options[key] = args[i + 1]; i += 2; } else { options[key] = true; i++; } } else if (arg.startsWith("-") && arg.length === 2) { const key = arg.slice(1); if (i + 1 < args.length && !args[i + 1].startsWith("-")) { options[key] = args[i + 1]; i += 2; } else { options[key] = true; i++; } } else { deps.push(arg); i++; } } return { options, deps }; } function parsePnpmOptions(args) { const { deps } = parseArgs(args); const isRecursive = ["--recursive", "-r"].some((i) => args.includes(i)); const isDev = ["--save-dev", "-D"].some((i) => args.includes(i)); const isOptional = ["--save-optional", "-O"].some((i) => args.includes(i)); const isProd = ["--save-prod", "-P"].some((i) => args.includes(i)); return { deps, isRecursive, isDev, isOptional, isProd }; } async function runPnpmInstall(options = {}) { const { cwd = process.cwd(), stdio = "inherit", silent = false } = options; if (!silent) p.outro("running pnpm install"); await execa("pnpm", ["install"], { stdio, cwd }); } async function runPnpmRemove(dependencies, options = {}) { const { cwd = process.cwd(), recursive = false, stdio = "inherit" } = options; if (dependencies.length === 0) return; const args = ["remove", ...dependencies]; if (recursive) args.push("--recursive"); await execa("pnpm", args, { stdio, cwd }); } function highlightYAMLContent(content, indentSize = 2, highlight = false) { if (content.trim() === "") { return content; } const currentIndent = content.search(/\S/); const indentLevel = Math.floor(currentIndent / indentSize); const colonIndex = content.indexOf(":"); if (colonIndex === -1) { return content; } const beforeColon = content.substring(0, colonIndex); const afterColon = content.substring(colonIndex); const ansi = highlight ? c.cyan : c.reset; if (indentLevel === 0 || indentLevel === 1) { const propertyName = beforeColon.trim(); const leadingSpaces = content.substring(0, content.indexOf(propertyName)); const versionMatch = afterColon.match(/:\s*(.+)/); if (versionMatch && versionMatch[1].trim()) return leadingSpaces + ansi(propertyName) + c.dim(afterColon); else return leadingSpaces + ansi(propertyName) + c.dim(":"); } else { const versionMatch = afterColon.match(/:\s*(.+)/); if (versionMatch && versionMatch[1].trim()) return beforeColon + c.dim(afterColon); else return beforeColon + c.dim(":"); } } function diffYAML(original, updated, options = {}) { const { indentSize = 2, verbose = false } = options; const changed = diffLines(original, updated, { ignoreNewlineAtEof: true }); const diffs = []; let lineNumber = 0; changed.forEach((part) => { const lines = part.value.split("\n"); if (lines[lines.length - 1] === "") { lines.pop(); } lines.forEach((line) => { diffs.push({ content: line, type: part.added ? "added" : part.removed ? "removed" : "unchanged", lineNumber: lineNumber++ }); }); }); const changedLines = /* @__PURE__ */ new Set(); diffs.forEach((line, index) => { if (line.type === "added" || line.type === "removed") changedLines.add(index); }); const lineHierarchy = []; diffs.forEach((line, index) => { const content = line.content; if (content.trim() === "") { lineHierarchy.push({ indentLevel: -1, parentIndices: [] }); return; } const currentIndent = content.search(/\S/); const indentLevel = Math.floor(currentIndent / indentSize); const parentIndices = []; for (let i = index - 1; i >= 0; i--) { const prevLine = diffs[i]; const prevHierarchy = lineHierarchy[i]; if (prevLine.content.trim() === "") continue; const prevIndent = prevLine.content.search(/\S/); const prevIndentLevel = Math.floor(prevIndent / indentSize); if (prevIndentLevel < indentLevel) { parentIndices.unshift(i); if (prevIndentLevel === indentLevel - 1) { parentIndices.unshift(...prevHierarchy.parentIndices); break; } } } lineHierarchy.push({ indentLevel, parentIndices }); }); const linesToKeep = /* @__PURE__ */ new Set(); if (verbose) { diffs.forEach((_, index) => { linesToKeep.add(index); }); } else { changedLines.forEach((lineIndex) => { linesToKeep.add(lineIndex); lineHierarchy[lineIndex].parentIndices.forEach((parentIndex) => { linesToKeep.add(parentIndex); }); }); } let addedCount = 0; let removedCount = 0; diffs.forEach((line) => { if (line.type === "added" || line.type === "removed") { const content = line.content; if (content.trim() === "") return; const currentIndent = content.search(/\S/); const indentLevel = Math.floor(currentIndent / indentSize); if (indentLevel >= 2 && content.includes(":")) { const colonIndex = content.indexOf(":"); const afterColon = content.substring(colonIndex); const versionMatch = afterColon.match(/:\s*(.+)/); if (versionMatch && versionMatch[1].trim()) { if (line.type === "added") addedCount++; else if (line.type === "removed") removedCount++; } } } }); const summaryParts = []; if (addedCount > 0) { summaryParts.push(`${c.yellow(addedCount)} added`); } if (removedCount > 0) { summaryParts.push(`${c.yellow(removedCount)} removed`); } const result = []; let lastKeptIndex = -1; diffs.forEach((line, index) => { if (linesToKeep.has(index)) { if (!verbose && lastKeptIndex !== -1 && index > lastKeptIndex + 1) { const skippedCount = index - lastKeptIndex - 1; result.push(c.dim`${c.yellow(skippedCount)} unchanged line${skippedCount > 1 ? "s" : ""}`); } let coloredLine = line.content; if (line.type === "added") { const highlightedContent = highlightYAMLContent(line.content, indentSize, verbose); coloredLine = c.green(`+ ${highlightedContent}`); } else if (line.type === "removed") { const highlightedContent = highlightYAMLContent(line.content, indentSize, verbose); coloredLine = c.red(`- ${highlightedContent}`); } else { const highlightedContent = highlightYAMLContent(line.content, indentSize, verbose); coloredLine = ` ${highlightedContent}`; } result.push(coloredLine); lastKeptIndex = index; } }); if (summaryParts.length > 0) { result.push(""); result.push(summaryParts.join(" ")); } return result.join("\n"); } async function confirmWorkspaceChanges(modifier, options) { const { pnpmCatalogManager, workspaceYaml, workspaceYamlPath, updatedPackages, yes = false, verbose = false, bailout = true, confirmMessage = "continue?", completeMessage } = options ?? {}; const commandOptions = pnpmCatalogManager.getOptions(); const rawContent = workspaceYaml.toString(); await modifier(); const content = workspaceYaml.toString(); if (rawContent === content) { if (bailout) { p.outro(c.yellow("no changes to pnpm-workspace.yaml")); process.exit(0); } else { p.log.info(c.green("no changes to pnpm-workspace.yaml")); } } const diff = diffYAML(rawContent, content, { verbose }); if (diff) { p.note(c.reset(diff), c.dim(workspaceYamlPath)); if (!yes) { const result = await p.confirm({ message: confirmMessage }); if (!result || p.isCancel(result)) { p.outro(c.red("aborting")); process.exit(1); } } await writePnpmWorkspace(workspaceYamlPath, content); } if (updatedPackages) await writePackageJSONs(updatedPackages); if (completeMessage) { if (commandOptions.install) { p.log.info(c.green(completeMessage)); await runPnpmInstall({ cwd: pnpmCatalogManager.getCwd() }); } else { p.outro(c.green(completeMessage)); } } } async function writePnpmWorkspace(filePath, content) { p.log.info("writing pnpm-workspace.yaml"); await writeFile(filePath, content, "utf-8"); } async function readPackageJSON() { const pkgPath = join(process.cwd(), "package.json"); if (!existsSync(pkgPath)) { p.outro(c.red("no package.json found, aborting")); process.exit(1); } const pkgJson = await readPackageJSON$1(pkgPath); if (typeof pkgJson.name !== "string") { p.outro(c.red("package.json is missing name, aborting")); process.exit(1); } return { pkgPath, pkgJson }; } async function writePackageJSONs(updatedPackages) { if (Object.keys(updatedPackages).length === 0) return; p.log.info("writing package.json"); await Promise.all( Object.values(updatedPackages).map((pkg) => writePackageJSON(pkg.filepath, cleanupPackageJSON(pkg.raw))) ); } function cleanupPackageJSON(pkgJson) { for (const field of DEPS_FIELDS) { const deps = pkgJson[field]; if (!deps) continue; if (Object.keys(deps).length === 0) delete pkgJson[field]; } return pkgJson; } function generateWorkspaceYAML(dependencies, workspaceYaml) { const document = workspaceYaml.getDocument(); document.deleteIn(["catalog"]); document.deleteIn(["catalogs"]); const catalogs = {}; for (const dep of dependencies) { if (!catalogs[dep.catalogName]) catalogs[dep.catalogName] = {}; catalogs[dep.catalogName][dep.name] = dep.specifier; } Object.entries(catalogs).sort((a, b) => a[0].localeCompare(b[0])).forEach(([catalogName, deps]) => { Object.entries(deps).forEach(([name, specifier]) => { if (catalogName === "default") workspaceYaml.setPath(["catalog", name], specifier); else workspaceYaml.setPath(["catalogs", catalogName, name], specifier); }); }); } function cleanupWorkspaceYAML(workspaceYaml) { const document = workspaceYaml.getDocument(); const workspaceJson = workspaceYaml.toJSON(); if (workspaceJson.catalog && !Object.keys(workspaceJson.catalog).length) document.deleteIn(["catalog"]); if (workspaceJson.catalogs) { const emptyCatalogs = []; for (const [catalogKey, catalogValue] of Object.entries(workspaceJson.catalogs)) { if (!catalogValue || Object.keys(catalogValue).length === 0) emptyCatalogs.push(catalogKey); } emptyCatalogs.forEach((key) => { document.deleteIn(["catalogs", key]); }); } const updatedWorkspaceJson = workspaceYaml.toJSON(); if (!updatedWorkspaceJson.catalogs || Object.keys(updatedWorkspaceJson.catalogs).length === 0) { document.deleteIn(["catalogs"]); } } function removeWorkspaceYAMLDeps(dependencies, workspaceYaml) { const document = workspaceYaml.getDocument(); dependencies.forEach((dep) => { if (dep.catalogName === "default") document.deleteIn(["catalog", dep.name]); else document.deleteIn(["catalogs", dep.catalogName, dep.name]); }); cleanupWorkspaceYAML(workspaceYaml); } async function resolveAdd(args, context) { const { options, workspaceYaml, pnpmCatalogManager } = context; await pnpmCatalogManager.loadPackages(); const { deps, isDev } = parsePnpmOptions(args); if (!deps.length) { p.outro(c.red("no dependencies provided, aborting")); process.exit(1); } const parsed = deps.map((x) => x.trim()).filter(Boolean).map(parseSpec); const workspaceJson = workspaceYaml.toJSON(); const workspacePackages = pnpmCatalogManager.getWorkspacePackages(); const createDep = (dep) => { return { name: dep.name, specifier: dep.specifier, source: isDev ? "devDependencies" : "dependencies", catalog: false, catalogable: true, catalogName: dep.catalogName }; }; for (const dep of parsed) { if (!dep.specifier && workspacePackages.includes(dep.name)) { dep.specifier = "workspace:*"; dep.specifierSource ||= "workspace"; continue; } if (options.catalog) dep.catalogName ||= options.catalog; if (dep.specifier) dep.specifierSource ||= "user"; if (!dep.specifier) { const catalogs = workspaceYaml.getPackageCatalogs(dep.name); if (catalogs[0]) { dep.catalogName = catalogs[0]; dep.specifierSource ||= "catalog"; } } if (dep.catalogName && !dep.specifier) { const spec = dep.catalogName === "default" ? workspaceJson?.catalog?.[dep.name] : workspaceJson?.catalogs?.[dep.catalogName]?.[dep.name]; if (spec) { dep.specifier = spec; } } if (!dep.specifier) { const spinner = p.spinner({ indicator: "dots" }); spinner.start(`resolving ${c.cyan(dep.name)} from npm...`); const { getLatestVersion } = await import('fast-npm-meta'); const { version } = await getLatestVersion(dep.name); if (version) { dep.specifier = `^${version}`; dep.specifierSource ||= "npm"; spinner.stop(`${c.dim("resolved")} ${c.cyan(dep.name)}${c.dim(`@${c.green(dep.specifier)}`)}`); } else { spinner.stop(`failed to resolve ${c.cyan(dep.name)} from npm`); p.outro(c.red("aborting")); process.exit(1); } } if (!dep.catalogName) { dep.catalogName = options.catalog || pnpmCatalogManager.inferCatalogName(createDep(dep)); } } return { isDev, dependencies: parsed.map((dep) => createDep(dep)) }; } async function resolveClean(context) { const { pnpmCatalogManager } = context; const packages = await pnpmCatalogManager.loadPackages(); const dependencies = []; for (const pkg of packages) { if (pkg.type === "package.json") continue; for (const dep of pkg.deps) { const resolvedDep = pnpmCatalogManager.resolveDep(dep, false); if (!pnpmCatalogManager.isDepInPackage(resolvedDep)) { dependencies.push(resolvedDep); } } } return { dependencies }; } async function resolveMigrate(context) { const { options, pnpmCatalogManager } = context; const packages = await pnpmCatalogManager.loadPackages(); const dependencies = /* @__PURE__ */ new Map(); const updatedPackages = /* @__PURE__ */ new Map(); const setDep = (dep) => { if (!dependencies.has(dep.name)) dependencies.set(dep.name, /* @__PURE__ */ new Map()); const catalogDeps = dependencies.get(dep.name); if (!catalogDeps.has(dep.catalogName)) catalogDeps.set(dep.catalogName, []); catalogDeps.get(dep.catalogName).push(dep); }; const setPackage = (dep, pkg) => { if (!updatedPackages.has(pkg.name)) updatedPackages.set(pkg.name, structuredClone(pkg)); const pkgJson = updatedPackages.get(pkg.name); pkgJson.raw[dep.source][dep.name] = dep.catalogName === "default" ? "catalog:" : `catalog:${dep.catalogName}`; }; for (const pkg of packages) { if (pkg.type === "pnpm-workspace.yaml") continue; for (const dep of pkg.deps) { if (!dep.catalogable) continue; const resolvedDep = pnpmCatalogManager.resolveDep(dep); setDep(resolvedDep); if (resolvedDep.update) setPackage(resolvedDep, pkg); } } await resolveConflict(dependencies, options); return { dependencies: Array.from(dependencies.values()).flatMap((i) => Array.from(i.values()).flat()), updatedPackages: Object.fromEntries(updatedPackages.entries()) }; } async function resolveConflict(dependencies, options) { const conflicts = []; for (const [depName, catalogDeps] of dependencies) { for (const [catalogName, deps] of catalogDeps) { const specs = [...new Set(deps.map((i) => i.specifier))]; if (specs.length > 1) { const specifiers = sortSpecs(specs); conflicts.push({ depName, catalogName, specifiers, resolvedSpecifier: specifiers[0] }); } else { const dep = deps[0]; dependencies.get(dep.name).set(dep.catalogName, [dep]); } } } if (conflicts.length === 0) return; p.log.warn(`\u{1F4E6} Found ${c.yellow(conflicts.length)} dependencies that need manual version selection`); for (const item of conflicts) { if (options.yes) continue; const result = await p.select({ message: c.yellow(`${item.depName} (${item.catalogName}):`), options: item.specifiers.map((i) => ({ label: i, value: i })), initialValue: item.resolvedSpecifier }); if (!result || p.isCancel(result)) { p.outro(c.red("aborting")); process.exit(1); } item.resolvedSpecifier = result; } for (const item of conflicts) { const deps = dependencies.get(item.depName).get(item.catalogName); const dep = deps.find((dep2) => dep2.specifier === item.resolvedSpecifier); dependencies.get(item.depName).set(item.catalogName, [dep]); } } async function resolveRemove(args, context) { const { pnpmCatalogManager } = context; await pnpmCatalogManager.loadPackages(); const { deps, isRecursive } = parsePnpmOptions(args); if (!deps.length) { p.outro(c.red("no dependencies provided, aborting")); process.exit(1); } const nonCatalogDeps = []; const dependencies = []; const updatedPackages = /* @__PURE__ */ new Map(); for (const dep of deps) { const packages = pnpmCatalogManager.getDepPackages(dep); if (!packages.length) { p.outro(c.red(`${dep} is not used in any package, aborting`)); process.exit(1); } let catalogPkgs = packages.filter((i) => pnpmCatalogManager.isCatalogPackageName(i)); if (catalogPkgs.length === 0) { nonCatalogDeps.push(dep); } if (catalogPkgs.length > 1) { const result = await p.multiselect({ message: `${c.cyan(dep)} found in multiple catalogs, please select the catalog to remove it from`, options: catalogPkgs.map((i) => ({ label: i, value: i })), initialValues: catalogPkgs }); if (!result || p.isCancel(result)) { p.outro(c.red("no catalog selected, aborting")); process.exit(1); } catalogPkgs = catalogPkgs.filter((i) => result.includes(i)); } await Promise.all(catalogPkgs.map(async (catalog) => { const rawDep = pnpmCatalogManager.getCatalogDep(dep, catalog); if (rawDep) { const { catalogDeletable } = await pnpmCatalogManager.removePackageDep(dep, rawDep.catalogName, isRecursive, updatedPackages); if (catalogDeletable) dependencies.push(rawDep); } })); } if (nonCatalogDeps.length) { p.outro(c.yellow(`${nonCatalogDeps.join(", ")} is not used in any catalog`)); await runPnpmRemove(nonCatalogDeps, { cwd: process.cwd(), recursive: isRecursive }); } return { dependencies, updatedPackages: Object.fromEntries(updatedPackages.entries()) }; } async function resolveRevert(args, context) { const { pnpmCatalogManager } = context; const { deps } = parsePnpmOptions(args); let pkgName; if (deps.length) { const { pkgJson } = await readPackageJSON(); pkgName = pkgJson.name; } const depFilter = (depName) => { if (!deps.length) return true; return deps.includes(depName); }; const pkgFilter = (name) => { if (!pkgName) return true; return pkgName === name; }; const packages = await pnpmCatalogManager.loadPackages(); const dependencies = []; const updatedPackages = /* @__PURE__ */ new Map(); const setPackage = (dep, pkg) => { if (!pkgFilter(pkg.name)) return; if (!updatedPackages.has(pkg.name)) updatedPackages.set(pkg.name, structuredClone(pkg)); const pkgJson = updatedPackages.get(pkg.name); pkgJson.raw[dep.source][dep.name] = dep.specifier; }; for (const pkg of packages) { if (pkg.type === "pnpm-workspace.yaml") continue; for (const dep of pkg.deps) { if (!depFilter(dep.name)) continue; const resolvedDep = pnpmCatalogManager.resolveDep(dep); dependencies.push(resolvedDep); setPackage(resolvedDep, pkg); } } return { isRevertAll: !deps.length, dependencies, updatedPackages: Object.fromEntries(updatedPackages.entries()) }; } async function addCommand(options) { const args = process.argv.slice(3); if (args.length === 0) { p.outro(c.red("no dependencies provided, aborting")); process.exit(1); } const { pkgJson, pkgPath } = await readPackageJSON(); const { workspaceYaml, workspaceYamlPath } = await ensureWorkspaceYAML(); const pnpmCatalogManager = new PnpmCatalogManager(options); const { isDev = false, dependencies = [] } = await resolveAdd(args, { options, pnpmCatalogManager, workspaceYaml }); const depsName = isDev ? "devDependencies" : "dependencies"; const depNameOppsite = isDev ? "dependencies" : "devDependencies"; const deps = pkgJson[depsName] ||= {}; for (const dep of dependencies) { deps[dep.name] = dep.catalogName ? dep.catalogName === "default" ? "catalog:" : `catalog:${dep.catalogName}` : dep.specifier || "^0.0.0"; if (pkgJson[depNameOppsite]?.[dep.name]) delete pkgJson[depNameOppsite][dep.name]; } const updatedPackages = { [pkgJson.name]: { filepath: pkgPath, raw: pkgJson } }; await confirmWorkspaceChanges( async () => { for (const dep of dependencies) { if (dep.catalogName) workspaceYaml.setPackage(dep.catalogName, dep.name, dep.specifier || "^0.0.0"); } }, { pnpmCatalogManager, workspaceYaml, workspaceYamlPath, updatedPackages, yes: options.yes, verbose: options.verbose, bailout: false, completeMessage: "add complete" } ); } async function cleanCommand(options) { const workspaceYamlPath = await findWorkspaceYAML(); if (!workspaceYamlPath) { p.outro(c.red("no pnpm-workspace.yaml found, aborting")); process.exit(1); } const pnpmCatalogManager = new PnpmCatalogManager(options); const { dependencies = [] } = await resolveClean({ pnpmCatalogManager }); if (!dependencies.length) { p.outro(c.yellow("no dependencies to clean, aborting")); process.exit(0); } const { workspaceYaml } = await ensureWorkspaceYAML(); p.log.info(`\u{1F4E6} Found ${c.yellow(dependencies.length)} dependencies not in package.json`); await confirmWorkspaceChanges( async () => { removeWorkspaceYAMLDeps(dependencies, workspaceYaml); }, { pnpmCatalogManager, workspaceYaml, workspaceYamlPath, yes: options.yes, verbose: options.verbose, bailout: true, completeMessage: "clean complete" } ); } const MIN_DEP_NAME_WIDTH = 12; const MIN_DEP_TYPE_WIDTH = 6; const MIN_SPECIFIER_WIDTH = 10; const MIN_CATALOG_WIDTH = 10; function renderChanges(deps, updatedPackages) { if (!deps.length) { return ""; } let maxDepNameWidth = MIN_DEP_NAME_WIDTH; let maxDepTypeWidth = MIN_DEP_TYPE_WIDTH; let maxSpecifierWidth = MIN_SPECIFIER_WIDTH; let maxCatalogWidth = MIN_CATALOG_WIDTH; for (const dep of deps) { maxDepNameWidth = Math.max(maxDepNameWidth, dep.name.length); maxDepTypeWidth = Math.max(maxDepTypeWidth, DEPENDENCIES_TYPE_SHORT_MAP[dep.source].length); maxSpecifierWidth = Math.max(maxSpecifierWidth, (dep.specifier || "").length); maxCatalogWidth = Math.max(maxCatalogWidth, dep.catalogName.length); } const depsByPackage = /* @__PURE__ */ new Map(); for (const dep of deps) { for (const [pkgName, pkgMeta] of Object.entries(updatedPackages)) { if (pkgMeta.deps.some((d) => d.name === dep.name && d.source === dep.source)) { if (!depsByPackage.has(pkgName)) { depsByPackage.set(pkgName, []); } depsByPackage.get(pkgName).push(dep); break; } } } const lines = []; for (const [pkgName, pkgMeta] of Object.entries(updatedPackages)) { const pkgDeps = depsByPackage.get(pkgName) || []; if (pkgDeps.length === 0) continue; lines.push(`${c.cyan(pkgName)} ${c.dim(pkgMeta.relative)}`); lines.push(""); for (const dep of pkgDeps) { const depName = dep.name.padEnd(maxDepNameWidth); const depType = DEPENDENCIES_TYPE_SHORT_MAP[dep.source].padEnd(maxDepTypeWidth); const depSpecifier = (dep.specifier || "").padStart(maxSpecifierWidth); const catalogRef = (dep.catalogName === "default" ? "" : dep.catalogName).padEnd(maxCatalogWidth); lines.push(` ${depName} ${c.dim(depType)} ${c.red(depSpecifier)} ${c.dim("\u2192")} catalog:${c.reset(c.green(catalogRef))}`); } lines.push(""); } const pkgCount = Object.keys(updatedPackages).length; lines.push(`${c.yellow(pkgCount)} package${pkgCount > 1 ? "s" : ""} ${c.yellow(deps.length)} dependenc${deps.length > 1 ? "ies" : "y"}`); return lines.join("\n"); } async function detectCommand(options) { const pnpmCatalogManager = new PnpmCatalogManager(options); const { dependencies = [], updatedPackages = {} } = await resolveMigrate({ options, pnpmCatalogManager }); const deps = dependencies.filter((i) => i.update); if (!deps.length) { p.outro(c.yellow("no dependencies to migrate, aborting")); process.exit(0); } p.log.info(`\u{1F4E6} Found ${c.yellow(deps.length)} dependencies to migrate`); let result = renderChanges(deps, updatedPackages); if (result) { result += ` run ${c.green("pncat migrate")}${options.force ? c.green(" -f") : ""} to apply changes`; p.note(c.reset(result)); } p.outro(c.green("detect complete")); } async function migrateCommand(options) { const pnpmCatalogManager = new PnpmCatalogManager(options); const { dependencies = [], updatedPackages = {} } = await resolveMigrate({ options, pnpmCatalogManager }); const { workspaceYaml, workspaceYamlPath } = await ensureWorkspaceYAML(); await confirmWorkspaceChanges( async () => { generateWorkspaceYAML(dependencies, workspaceYaml); }, { pnpmCatalogManager, workspaceYaml, workspaceYamlPath, updatedPackages, yes: options.yes, verbose: options.verbose, bailout: true, completeMessage: "migrate complete" } ); } async function removeCommand(options) { const args = process.argv.slice(3); if (args.length === 0) { p.outro(c.red("no dependencies provided, aborting")); process.exit(1); } const workspaceYamlPath = await findWorkspaceYAML(); if (!workspaceYamlPath) { p.outro(c.red("no pnpm-workspace.yaml found, aborting")); process.exit(1); } const { workspaceYaml } = await ensureWorkspaceYAML(); const pnpmCatalogManager = new PnpmCatalogManager(options); const { dependencies = [], updatedPackages = {} } = await resolveRemove(args, { pnpmCatalogManager}); await confirmWorkspaceChanges( async () => { removeWorkspaceYAMLDeps(dependencies, workspaceYaml); }, { pnpmCatalogManager, workspaceYaml, workspaceYamlPath, updatedPackages, yes: options.yes, verbose: options.verbose, bailout: false, completeMessage: "remove complete" } ); } async function revertCommand(options) { const args = process.argv.slice(3); const workspaceYamlPath = await findWorkspaceYAML(); if (!workspaceYamlPath) { p.outro(c.red("no pnpm-workspace.yaml found, aborting")); process.exit(1); } const { workspaceYaml } = await ensureWorkspaceYAML(); const pnpmCatalogManager = new PnpmCatalogManager(options); const { isRevertAll, dependencies = [], updatedPackages = {} } = await resolveRevert(args, { pnpmCatalogManager}); if (isRevertAll) { if (!options.yes) { const result = await p.confirm({ message: c.green("all catalog dependencies will be reverted, are you sure?") }); if (!result || p.isCancel(result)) { p.outro(c.red("aborting")); process.exit(1); } } const document = workspaceYaml.getDocument(); document.deleteIn(["catalog"]); document.deleteIn(["catalogs"]); await writePnpmWorkspace(workspaceYamlPath, workspaceYaml.toString()); await writePackageJSONs(updatedPackages); } else { await confirmWorkspaceChanges( async () => { removeWorkspaceYAMLDeps(dependencies, workspaceYaml); }, { pnpmCatalogManager, workspaceYaml, workspaceYamlPath, updatedPackages, yes: options.yes, verbose: options.verbose, bailout: true, completeMessage: "revert complete" } ); } } function normalizeConfig(options) { if ("default" in options) options = options.default; return options; } async function resolveConfig(options) { const defaults = structuredClone(DEFAULT_CATALOG_OPTIONS); options = normalizeConfig(options); const loader = createConfigLoader({ sources: [ { files: ["pncat.config"] }, { files: [".pncatrc"], extensions: ["json", ""] } ], cwd: options.cwd || process.cwd(), merge: false }); const config = await loader.load(); const configOptions = config.sources.length ? normalizeConfig(config.config) : {}; const catalogRules = configOptions.catalogRules || structuredClone(DEFAULT_CATALOG_RULES) || []; delete configOptions.catalogRules; const merged = deepmerge(deepmerge(defaults, configOptions), options); merged.cwd = merged.cwd || await findWorkspaceRoot(); if (typeof merged.catalog === "boolean") delete merged.catalog; merged.catalogRules = catalogRules; return merged; } try { const cli = cac(name); cli.command("[mode]", "Enhanced pnpm catalogs management with advanced workspace dependency control").option("--catalog [name]", "Install from a specific catalog, auto detect if not provided").option("--recursive, -r", "Recursively search for package.json in subdirectories").option("--force, -f", "Force cataloging according to rules, ignoring original configurations").option("--include, -n <deps>", "Only included dependencies will be checked for catalog").option("--exclude, -x <deps>", "Exclude dependencies to be checked, will override --include options").option("--ignore-paths <paths>", "Ignore paths for search package.json").option("--ignore-other-workspaces", "Ignore package.json that in other workspaces (with their own .git,pnpm-workspace.yaml,etc.)").option("--yes", "Skip prompt confirmation").option("--install", "Run pnpm install after command").option("--verbose", "Show complete pnpm-workspace.yaml instead of only the diff").allowUnknownOptions().action(async (mode, options) => { if (mode) { if (!MODE_CHOICES.includes(mode)) { console.error(`Invalid mode: ${mode}. Please use one of the following: ${MODE_CHOICES.join("|")}`); process.exit(1); } options.mode = mode; } p.intro(`${c.yellow`${name} `}${c.dim`v${version}`}`); const config = await resolveConfig(options); switch (config.mode) { case "detect": await detectCommand(config); break; case "migrate": await migrateCommand(config); break; case "add": await addCommand(config); break; case "remove": await removeCommand(config); break; case "clean": await cleanCommand(config); break; case "revert": await revertCommand(config); break; } }); cli.help(); cli.version(version); cli.parse(); } catch (error) { console.error(error); process.exit(1); }