@cellajs/cli 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env tsx
2
2
 
3
3
  // src/cella-cli.ts
4
- import { existsSync as existsSync12, statSync as statSync3 } from "fs";
5
- import { join as join18, resolve as resolve7 } from "path";
4
+ import { existsSync as existsSync13, statSync as statSync3 } from "fs";
5
+ import { join as join19, resolve as resolve7 } from "path";
6
6
  import process9 from "process";
7
7
 
8
8
  // src/cli.ts
@@ -219,7 +219,7 @@ import process4 from "process";
219
219
  // package.json
220
220
  var package_default = {
221
221
  name: "@cellajs/cli",
222
- version: "0.4.1",
222
+ version: "0.4.2",
223
223
  private: false,
224
224
  license: "MIT",
225
225
  description: "CLI for managing your monorepo and syncing with upstream Cella.",
@@ -2872,13 +2872,18 @@ async function runAnalyze(config) {
2872
2872
 
2873
2873
  // src/services/audit.ts
2874
2874
  import { readFileSync as readFileSync4 } from "fs";
2875
- import { basename as basename4, join as join12, relative as relative3 } from "path";
2875
+ import { basename as basename4, join as join13, relative as relative3 } from "path";
2876
2876
  import { checkbox, Separator } from "@inquirer/prompts";
2877
2877
 
2878
2878
  // src/utils/audit-utils.ts
2879
2879
  import { execFile as execFile2 } from "child_process";
2880
2880
  import { promisify as promisify2 } from "util";
2881
2881
  var execFileAsync2 = promisify2(execFile2);
2882
+ function outdatedPackageName(key) {
2883
+ const name = key.split(" ")[0];
2884
+ const at = name.indexOf("@", 1);
2885
+ return at === -1 ? name : name.slice(0, at);
2886
+ }
2882
2887
  function getRepoUrl(repositoryUrl) {
2883
2888
  if (!repositoryUrl) return null;
2884
2889
  let url = repositoryUrl;
@@ -2973,6 +2978,13 @@ function formatDependents(dependents) {
2973
2978
  return `${dependents[0]} +${dependents.length - 1}`;
2974
2979
  }
2975
2980
 
2981
+ // src/utils/local-tools.ts
2982
+ import { execFile as execFile3 } from "child_process";
2983
+ import { existsSync as existsSync7, realpathSync as realpathSync2 } from "fs";
2984
+ import { tmpdir } from "os";
2985
+ import { delimiter, dirname as dirname6, join as join11, sep as sep2 } from "path";
2986
+ import { promisify as promisify3 } from "util";
2987
+
2976
2988
  // src/utils/runtimes.ts
2977
2989
  import { readFileSync as readFileSync2 } from "fs";
2978
2990
  import { basename as basename3, dirname as dirname5, join as join10 } from "path";
@@ -3094,24 +3106,23 @@ async function fetchImageTags(pin) {
3094
3106
  if (!Array.isArray(results)) return null;
3095
3107
  return { pinned: hubTag(pinned), tags: results.flatMap((entry) => hubTag(entry) ?? []) };
3096
3108
  }
3109
+ async function fetchPnpmAdvisories(version) {
3110
+ const answer = await fetchJson("https://registry.npmjs.org/-/npm/v1/security/advisories/bulk", {
3111
+ method: "POST",
3112
+ headers: { "content-type": "application/json" },
3113
+ body: JSON.stringify({ pnpm: [version] })
3114
+ });
3115
+ return answer ? answer.pnpm ?? [] : null;
3116
+ }
3097
3117
  async function fetchRuntimeFeeds(pins, cwd, now) {
3098
3118
  const nodeMajor = pins.node ? parseComparableVersion(pins.node)?.major : void 0;
3099
3119
  const [node, published, advisories, images] = await Promise.all([
3100
3120
  nodeMajor === void 0 ? null : fetchNodeReleases(nodeMajor, now),
3101
3121
  pins.pnpm ? fetchPublishTimes("pnpm", cwd) : null,
3102
- pins.pnpm ? fetchJson("https://registry.npmjs.org/-/npm/v1/security/advisories/bulk", {
3103
- method: "POST",
3104
- headers: { "content-type": "application/json" },
3105
- body: JSON.stringify({ pnpm: [pins.pnpm] })
3106
- }) : null,
3122
+ pins.pnpm ? fetchPnpmAdvisories(pins.pnpm) : null,
3107
3123
  Promise.all(pins.images.map(fetchImageTags))
3108
3124
  ]);
3109
- const pnpmAdvisories = advisories?.pnpm;
3110
- return {
3111
- node,
3112
- pnpm: published && advisories ? { published, advisories: pnpmAdvisories ?? [] } : null,
3113
- images
3114
- };
3125
+ return { node, pnpm: published && advisories ? { published, advisories } : null, images };
3115
3126
  }
3116
3127
  var count = (n, noun, plural2 = `${noun}s`) => `${n} ${n === 1 ? noun : plural2}`;
3117
3128
  var monthYear = (time) => new Date(time).toLocaleString("en", { month: "short", year: "numeric", timeZone: "UTC" }).toLowerCase();
@@ -3341,6 +3352,130 @@ function liveExcludes(input) {
3341
3352
  });
3342
3353
  }
3343
3354
 
3355
+ // src/utils/local-tools.ts
3356
+ var execFileAsync3 = promisify3(execFile3);
3357
+ function findOnPath(name, skip = []) {
3358
+ for (const dir of (getEnv("PATH") ?? "").split(delimiter)) {
3359
+ if (!dir || skip.includes(dir) || dir.endsWith(`node_modules${sep2}.bin`)) continue;
3360
+ if (existsSync7(join11(dir, name))) return join11(dir, name);
3361
+ }
3362
+ return null;
3363
+ }
3364
+ function installerOf(file) {
3365
+ let real = file;
3366
+ try {
3367
+ real = realpathSync2(file);
3368
+ } catch {
3369
+ }
3370
+ if (real.includes(`${sep2}.volta${sep2}`)) return "volta";
3371
+ return /[\\/](Cellar|homebrew|linuxbrew)[\\/]/.test(real) ? "brew" : null;
3372
+ }
3373
+ async function versionOf(file, cwd) {
3374
+ try {
3375
+ const { stdout } = await execFileAsync3(file, ["--version"], { encoding: "utf8", timeout: 1e4, cwd });
3376
+ return stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
3377
+ } catch {
3378
+ return null;
3379
+ }
3380
+ }
3381
+ async function readNode(now) {
3382
+ const version = process.version.slice(1);
3383
+ const major = parseComparableVersion(version)?.major ?? 0;
3384
+ return { version, installer: installerOf(process.execPath), releases: await fetchNodeReleases(major, now) };
3385
+ }
3386
+ async function readPnpm(cwd) {
3387
+ const running = getEnv("npm_execpath");
3388
+ const file = findOnPath("pnpm", running ? [dirname6(running)] : []);
3389
+ const version = file && await versionOf(file, tmpdir());
3390
+ if (!file || !version) return null;
3391
+ const [published, advisories] = await Promise.all([fetchPublishTimes("pnpm", cwd), fetchPnpmAdvisories(version)]);
3392
+ return { version, installer: installerOf(file), published, advisories };
3393
+ }
3394
+ async function fetchOsvSeverities(module, version) {
3395
+ const answer = await fetchJson("https://api.osv.dev/v1/query", {
3396
+ method: "POST",
3397
+ body: JSON.stringify({ package: { name: module, ecosystem: "Go" }, version })
3398
+ });
3399
+ if (!answer) return null;
3400
+ const byGhsa = /* @__PURE__ */ new Map();
3401
+ for (const vuln of answer.vulns ?? []) {
3402
+ const id = [vuln.id, ...vuln.aliases ?? []].find((alias) => alias.startsWith("GHSA-")) ?? vuln.id;
3403
+ const severity = SEVERITIES.find((level) => level === vuln.database_specific?.severity?.toLowerCase());
3404
+ if (severity || !byGhsa.has(id)) byGhsa.set(id, severity ?? "moderate");
3405
+ }
3406
+ return [...byGhsa.values()];
3407
+ }
3408
+ async function readGh() {
3409
+ const file = findOnPath("gh");
3410
+ const version = file && await versionOf(file);
3411
+ if (!file || !version) return null;
3412
+ const [list, advisories] = await Promise.all([
3413
+ fetchJson("https://api.github.com/repos/cli/cli/releases?per_page=30"),
3414
+ fetchOsvSeverities("github.com/cli/cli/v2", version)
3415
+ ]);
3416
+ const releases = Array.isArray(list) ? list.flatMap((entry) => {
3417
+ const { tag_name, published_at } = entry;
3418
+ const publishedAt = Date.parse(published_at ?? "");
3419
+ return tag_name && !Number.isNaN(publishedAt) ? [{ version: tag_name.replace(/^v/, ""), publishedAt }] : [];
3420
+ }) : null;
3421
+ return { version, installer: installerOf(file), releases, advisories };
3422
+ }
3423
+ async function readLocalTools(cwd, now) {
3424
+ const [node, pnpm, gh] = await Promise.all([readNode(now), readPnpm(cwd), readGh()]);
3425
+ return { node, pnpm, gh };
3426
+ }
3427
+ function upgradeCommand(name, installer, target) {
3428
+ if (installer === "brew") return `brew upgrade ${name}`;
3429
+ if (!target) return "no fixed release older than the release age yet";
3430
+ if (installer === "volta") return `volta install ${name}@${target}`;
3431
+ return name === "pnpm" ? `pnpm self-update ${target}` : `update to ${target}`;
3432
+ }
3433
+ function toCheck(name, tool, missed, target, noun) {
3434
+ if (!missed)
3435
+ return { name, version: tool.version, severity: null, note: "not checked, no answer from the feed", fix: "" };
3436
+ if (missed.length === 0) return { name, version: tool.version, severity: null, note: "", fix: "" };
3437
+ const high = missed.filter((severity) => severity === "critical" || severity === "high").length;
3438
+ return {
3439
+ name,
3440
+ version: tool.version,
3441
+ severity: SEVERITIES.find((severity) => missed.includes(severity)) ?? null,
3442
+ note: `${plural(missed.length, ...noun)}${high > 0 && high < missed.length ? `, ${high} high` : ""}`,
3443
+ fix: upgradeCommand(name, tool.installer, target)
3444
+ };
3445
+ }
3446
+ function planLocalTools(tools, options) {
3447
+ const cutoff = options.now - options.releaseAgeMinutes * 60 * 1e3;
3448
+ const { node, pnpm, gh } = tools;
3449
+ const nodeTarget = node.releases ? newestAged(node.version, node.releases, cutoff) : void 0;
3450
+ const securityReleases = (node.releases ?? []).filter(
3451
+ (release) => release.security && nodeTarget && compareVersionStrings(release.version, node.version) > 0 && compareVersionStrings(release.version, nodeTarget.version) <= 0
3452
+ );
3453
+ const checks = [
3454
+ toCheck(
3455
+ "node",
3456
+ node,
3457
+ node.releases && securityReleases.map(() => "high"),
3458
+ nodeTarget?.version,
3459
+ ["security release", "security releases"]
3460
+ )
3461
+ ];
3462
+ if (pnpm) {
3463
+ const affecting = (version) => (pnpm.advisories ?? []).filter((advisory) => satisfiesComparators(version, advisory.vulnerable_versions));
3464
+ const fixed = Object.entries(pnpm.published ?? {}).map(([version, time]) => ({ version, publishedAt: Date.parse(time) })).filter(
3465
+ (release) => parseComparableVersion(release.version) && !isPrerelease(release.version) && release.publishedAt <= cutoff && compareVersionStrings(release.version, pnpm.version) > 0 && affecting(release.version).length === 0
3466
+ ).sort((a, b) => compareVersionStrings(b.version, a.version));
3467
+ const major = parseComparableVersion(pnpm.version)?.major;
3468
+ const target = fixed.find((release) => parseComparableVersion(release.version)?.major === major) ?? fixed[0];
3469
+ const missed = pnpm.advisories && pnpm.published && affecting(pnpm.version).map((advisory) => advisory.severity);
3470
+ checks.push(toCheck("pnpm", pnpm, missed, target?.version, ["advisory", "advisories"]));
3471
+ }
3472
+ if (gh) {
3473
+ const target = gh.releases ? newestAged(gh.version, gh.releases, cutoff) : void 0;
3474
+ checks.push(toCheck("gh", gh, gh.advisories, target?.version, ["advisory", "advisories"]));
3475
+ }
3476
+ return checks;
3477
+ }
3478
+
3344
3479
  // src/utils/workspace-yaml.ts
3345
3480
  import { isMap, isScalar, isSeq, parseDocument } from "yaml";
3346
3481
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -3421,37 +3556,37 @@ function writeAuditSettings(content, next) {
3421
3556
 
3422
3557
  // src/services/audit-security.ts
3423
3558
  import { spawnSync as spawnSync2 } from "child_process";
3424
- import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
3425
- import { tmpdir } from "os";
3426
- import { dirname as dirname6, join as join11, relative as relative2 } from "path";
3559
+ import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
3560
+ import { tmpdir as tmpdir2 } from "os";
3561
+ import { dirname as dirname7, join as join12, relative as relative2 } from "path";
3427
3562
  import { confirm, select as select2 } from "@inquirer/prompts";
3428
3563
  var WORKSPACE_FILE = "pnpm-workspace.yaml";
3429
3564
  var LOCK_FILE = "pnpm-lock.yaml";
3430
3565
  var RESOLUTION_FILES = ["package.json", LOCK_FILE, ".npmrc", ".pnpmfile.cjs"];
3431
- var readOr = (path, fallback = "") => existsSync7(path) ? readFileSync3(path, "utf8") : fallback;
3566
+ var readOr = (path, fallback = "") => existsSync8(path) ? readFileSync3(path, "utf8") : fallback;
3432
3567
  var count2 = (n, noun, plural2 = `${noun}s`) => `${n} ${n === 1 ? noun : plural2}`;
3433
3568
  var isBreaking = (item) => item.remedy.kind !== "none" && item.remedy.breaking;
3434
3569
  var needsDecision = (item) => item.standing === "new" && (item.remedy.kind !== "override" || item.remedy.breaking || item.direct);
3435
3570
  var sameEntries = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([key, value]) => b[key] === value);
3436
3571
  async function createStaging(forkPath, workspaceYaml) {
3437
- const staging = mkdtempSync(join11(tmpdir(), "cella-audit-"));
3572
+ const staging = mkdtempSync(join12(tmpdir2(), "cella-audit-"));
3438
3573
  const projects = await runPnpmJson(["ls", "-r", "--depth", "-1", "--json"], forkPath) ?? [];
3439
3574
  const files = [
3440
3575
  ...RESOLUTION_FILES,
3441
3576
  ...readPatchFiles(workspaceYaml),
3442
- ...projects.map((project) => join11(relative2(forkPath, project.path), "package.json"))
3577
+ ...projects.map((project) => join12(relative2(forkPath, project.path), "package.json"))
3443
3578
  ];
3444
3579
  for (const file of new Set(files)) {
3445
- if (file.startsWith("..") || !existsSync7(join11(forkPath, file))) continue;
3446
- mkdirSync2(dirname6(join11(staging, file)), { recursive: true });
3447
- cpSync(join11(forkPath, file), join11(staging, file));
3580
+ if (file.startsWith("..") || !existsSync8(join12(forkPath, file))) continue;
3581
+ mkdirSync2(dirname7(join12(staging, file)), { recursive: true });
3582
+ cpSync(join12(forkPath, file), join12(staging, file));
3448
3583
  }
3449
3584
  return staging;
3450
3585
  }
3451
3586
  async function resolveStaging(staging, workspaceYaml, settings) {
3452
- writeFileSync2(join11(staging, WORKSPACE_FILE), writeAuditSettings(workspaceYaml, settings));
3587
+ writeFileSync2(join12(staging, WORKSPACE_FILE), writeAuditSettings(workspaceYaml, settings));
3453
3588
  const result = await runPnpm(["install", "--lockfile-only", "--no-frozen-lockfile", "--ignore-scripts"], staging);
3454
- return result.ok ? { lock: readOr(join11(staging, LOCK_FILE)) } : { error: `${result.stdout}${result.stderr}`.trim() };
3589
+ return result.ok ? { lock: readOr(join12(staging, LOCK_FILE)) } : { error: `${result.stdout}${result.stderr}`.trim() };
3455
3590
  }
3456
3591
  function lockedVersions(lock, name) {
3457
3592
  const escaped = name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
@@ -3555,8 +3690,8 @@ function changeLine(label, before, after) {
3555
3690
  var NO_EDITS = { manifests: {}, files: {} };
3556
3691
  async function runSecurityPass(forkPath, options) {
3557
3692
  const { releaseAgeMinutes, edits = NO_EDITS } = options;
3558
- const workspaceYaml = readOr(join11(forkPath, WORKSPACE_FILE));
3559
- const workspaceLock = readOr(join11(forkPath, LOCK_FILE));
3693
+ const workspaceYaml = readOr(join12(forkPath, WORKSPACE_FILE));
3694
+ const workspaceLock = readOr(join12(forkPath, LOCK_FILE));
3560
3695
  let previous;
3561
3696
  try {
3562
3697
  previous = readAuditSettings(workspaceYaml);
@@ -3582,7 +3717,7 @@ async function runSecurityPass(forkPath, options) {
3582
3717
  };
3583
3718
  const staging = await createStaging(forkPath, workspaceYaml);
3584
3719
  try {
3585
- for (const [file, content] of Object.entries(edits.manifests)) writeFileSync2(join11(staging, file), content);
3720
+ for (const [file, content] of Object.entries(edits.manifests)) writeFileSync2(join12(staging, file), content);
3586
3721
  const bare = await resolveStaging(staging, workspaceYaml, { ...previous, overrides: lasting, ignoredGhsas: [] });
3587
3722
  if ("error" in bare) {
3588
3723
  const without = updatesManifests ? "with the selected updates" : "without its audit overrides";
@@ -3634,7 +3769,7 @@ async function runSecurityPass(forkPath, options) {
3634
3769
  const settingsChanged = nextYaml !== workspaceYaml;
3635
3770
  const otherFiles = Object.keys(edits.files);
3636
3771
  const writeFiles = () => {
3637
- for (const [file, content] of Object.entries(edits.files)) writeFileSync2(join11(forkPath, file), content);
3772
+ for (const [file, content] of Object.entries(edits.files)) writeFileSync2(join12(forkPath, file), content);
3638
3773
  if (otherFiles.length > 0) console.info(`${checkMark} ${otherFiles.join(", ")} updated`);
3639
3774
  };
3640
3775
  if (!settingsChanged && !updatesManifests) {
@@ -3654,7 +3789,7 @@ async function runSecurityPass(forkPath, options) {
3654
3789
  };
3655
3790
  if (!updatesManifests && sameEntries(next.overrides, previous.overrides)) {
3656
3791
  writeFiles();
3657
- writeFileSync2(join11(forkPath, WORKSPACE_FILE), nextYaml);
3792
+ writeFileSync2(join12(forkPath, WORKSPACE_FILE), nextYaml);
3658
3793
  console.info(`${checkMark} ${WORKSPACE_FILE} updated ${colors_default.dim("\xB7 nothing to install")}`);
3659
3794
  printOpen(open);
3660
3795
  summarize();
@@ -3665,14 +3800,14 @@ async function runSecurityPass(forkPath, options) {
3665
3800
  stagingSpinner.stop();
3666
3801
  if ("error" in staged) return fail("could not resolve the workspace with the new settings", staged.error);
3667
3802
  const written = { ...edits.files, ...edits.manifests, [WORKSPACE_FILE]: nextYaml, [LOCK_FILE]: staged.lock };
3668
- const before = Object.fromEntries(Object.keys(written).map((file) => [file, readOr(join11(forkPath, file))]));
3803
+ const before = Object.fromEntries(Object.keys(written).map((file) => [file, readOr(join12(forkPath, file))]));
3669
3804
  before[WORKSPACE_FILE] = workspaceYaml;
3670
3805
  before[LOCK_FILE] = workspaceLock;
3671
- for (const [file, content] of Object.entries(written)) writeFileSync2(join11(forkPath, file), content);
3806
+ for (const [file, content] of Object.entries(written)) writeFileSync2(join12(forkPath, file), content);
3672
3807
  const install = spawnSync2("pnpm", ["install"], { cwd: forkPath, stdio: "inherit" });
3673
3808
  console.info();
3674
3809
  if (install.status !== 0) {
3675
- for (const [file, content] of Object.entries(before)) writeFileSync2(join11(forkPath, file), content);
3810
+ for (const [file, content] of Object.entries(before)) writeFileSync2(join12(forkPath, file), content);
3676
3811
  console.error(`${colors_default.red("\u2717")} pnpm install failed (exit code ${install.status})`);
3677
3812
  console.info(colors_default.dim(` ${Object.keys(before).join(", ")} are back as they were; run pnpm install to match`));
3678
3813
  console.info();
@@ -3741,7 +3876,7 @@ function createDependentPkgReader() {
3741
3876
  let cached = cache.get(location);
3742
3877
  if (cached === void 0) {
3743
3878
  try {
3744
- cached = JSON.parse(readFileSync4(join12(location, "package.json"), "utf8"));
3879
+ cached = JSON.parse(readFileSync4(join13(location, "package.json"), "utf8"));
3745
3880
  } catch {
3746
3881
  cached = null;
3747
3882
  }
@@ -3756,11 +3891,12 @@ async function runAudit(config) {
3756
3891
  try {
3757
3892
  const pins = await readRuntimePins(forkPath);
3758
3893
  const now = Date.now();
3759
- const [outdated, auditResult, releaseAgeMinutes, feeds] = await Promise.all([
3894
+ const [outdated, auditResult, releaseAgeMinutes, feeds, machine] = await Promise.all([
3760
3895
  runPnpmJson(["-r", "outdated", "--json", "--long"], forkPath),
3761
3896
  runPnpmAudit(forkPath),
3762
3897
  readReleaseAgeMinutes(forkPath),
3763
- fetchRuntimeFeeds(pins, forkPath, now)
3898
+ fetchRuntimeFeeds(pins, forkPath, now),
3899
+ readLocalTools(forkPath, now)
3764
3900
  ]);
3765
3901
  if (!outdated) {
3766
3902
  spinnerFail("pnpm outdated gave no answer: run pnpm -r outdated to see why");
@@ -3768,6 +3904,7 @@ async function runAudit(config) {
3768
3904
  return;
3769
3905
  }
3770
3906
  const runtimes = planRuntimeUpdates(pins, feeds, { releaseAgeMinutes, now });
3907
+ const localTools = planLocalTools(machine, { releaseAgeMinutes, now });
3771
3908
  const rows = [
3772
3909
  ...packageRows(outdated, auditResult ? advisoriesFromAudit(auditResult) : []),
3773
3910
  ...runtimes.updates.map(runtimeRow)
@@ -3776,6 +3913,7 @@ async function runAudit(config) {
3776
3913
  spinnerSuccess("all packages and runtimes are up to date");
3777
3914
  printNotes(forkPath, runtimes.unchecked);
3778
3915
  console.info();
3916
+ printLocalTools(localTools);
3779
3917
  await runSecurityPass(forkPath, { apply: !config.list, releaseAgeMinutes });
3780
3918
  return;
3781
3919
  }
@@ -3783,6 +3921,7 @@ async function runAudit(config) {
3783
3921
  printOutdatedResults(rows, forkPath);
3784
3922
  printNotes(forkPath, runtimes.unchecked);
3785
3923
  console.info();
3924
+ printLocalTools(localTools);
3786
3925
  if (config.list) {
3787
3926
  await runSecurityPass(forkPath, { apply: false, releaseAgeMinutes });
3788
3927
  return;
@@ -3798,7 +3937,8 @@ async function runAudit(config) {
3798
3937
  function packageRows(outdated, advisories) {
3799
3938
  const severityOf = (name) => SEVERITIES.find((severity) => advisories.some((a) => a.module === name && a.severity === severity)) ?? null;
3800
3939
  const readDependentPkg = createDependentPkgReader();
3801
- const rows = Object.entries(outdated).map(([name, pkg]) => {
3940
+ const rows = Object.entries(outdated).map(([key, pkg]) => {
3941
+ const name = outdatedPackageName(key);
3802
3942
  const repoUrl = getRepoUrl(pkg.latestManifest?.repository?.url);
3803
3943
  const current = pkg.current ?? "missing";
3804
3944
  const pinnedIn = [];
@@ -3809,6 +3949,7 @@ function packageRows(outdated, advisories) {
3809
3949
  }
3810
3950
  }
3811
3951
  return {
3952
+ key,
3812
3953
  name,
3813
3954
  current,
3814
3955
  latest: pkg.latest,
@@ -3825,6 +3966,7 @@ function packageRows(outdated, advisories) {
3825
3966
  }
3826
3967
  function runtimeRow(update) {
3827
3968
  return {
3969
+ key: `runtime:${update.file}:${update.name}`,
3828
3970
  name: update.name,
3829
3971
  current: update.current,
3830
3972
  latest: update.target,
@@ -3838,7 +3980,6 @@ function runtimeRow(update) {
3838
3980
  runtime: update
3839
3981
  };
3840
3982
  }
3841
- var rowKey = (row) => row.runtime ? `runtime:${row.runtime.file}:${row.name}` : row.name;
3842
3983
  var isRed = (row) => row.severity === "critical" || row.severity === "high";
3843
3984
  var inText = (row) => row.runtime ? pinLocation(row.runtime.file) : formatDependents(row.dependentPackages.map((d) => d.name));
3844
3985
  function printOutdatedResults(rows, forkPath) {
@@ -3894,7 +4035,7 @@ function printOutdatedResults(rows, forkPath) {
3894
4035
  const sortedDependents = [...dependentMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
3895
4036
  const maxDependentNameLen = Math.max(0, ...sortedDependents.map(([name]) => name.length));
3896
4037
  for (const [dependentName, { location, count: count3 }] of sortedDependents) {
3897
- const packageJsonPath2 = join12(location, "package.json");
4038
+ const packageJsonPath2 = join13(location, "package.json");
3898
4039
  const relativePath = relative3(forkPath, packageJsonPath2);
3899
4040
  const paddedName = dependentName.padEnd(maxDependentNameLen);
3900
4041
  console.info(
@@ -3905,7 +4046,7 @@ function printOutdatedResults(rows, forkPath) {
3905
4046
  function printNotes(forkPath, unchecked) {
3906
4047
  let held = [];
3907
4048
  try {
3908
- const updateConfig = readTopLevel(readFileSync4(join12(forkPath, "pnpm-workspace.yaml"), "utf8"), "updateConfig");
4049
+ const updateConfig = readTopLevel(readFileSync4(join13(forkPath, "pnpm-workspace.yaml"), "utf8"), "updateConfig");
3909
4050
  const ignored = updateConfig?.ignoreDependencies;
3910
4051
  held = Array.isArray(ignored) ? ignored.map(String) : [];
3911
4052
  } catch {
@@ -3914,6 +4055,16 @@ function printNotes(forkPath, unchecked) {
3914
4055
  if (unchecked.length > 0)
3915
4056
  console.info(` ${colors_default.dim(`not checked, no answer from the feed: ${unchecked.join(", ")}`)}`);
3916
4057
  }
4058
+ function printLocalTools(checks) {
4059
+ const width = Math.max(...checks.map((tool) => `${tool.name} ${tool.version}`.length));
4060
+ console.info(colors_default.bold("this machine"));
4061
+ for (const tool of checks) {
4062
+ const label = `${tool.name} ${tool.version}`.padEnd(width);
4063
+ const detail = tool.severity ? `${tool.note} ${colors_default.dim("\xB7")} ${tool.fix}` : colors_default.dim(tool.note || "no missing security fixes");
4064
+ console.info(`${tool.severity ? getVulnIcon(tool.severity) : " "} ${label} ${detail}`);
4065
+ }
4066
+ console.info();
4067
+ }
3917
4068
  function buildUpdateChoices(rows) {
3918
4069
  const makeChoice = (row) => {
3919
4070
  const devTag = row.isDev ? colors_default.dim(" (dev)") : "";
@@ -3925,7 +4076,7 @@ function buildUpdateChoices(rows) {
3925
4076
  ].filter(Boolean);
3926
4077
  return {
3927
4078
  name: `${row.name}${devTag} ${version}${notes.length > 0 ? ` ${notes.join(" ")}` : ""}`,
3928
- value: rowKey(row),
4079
+ value: row.key,
3929
4080
  checked: isRed(row)
3930
4081
  };
3931
4082
  };
@@ -3985,17 +4136,17 @@ async function planEdits(forkPath, rows, selected, nodeImageSuffixes) {
3985
4136
  const summary = /* @__PURE__ */ new Map();
3986
4137
  const bumped = [];
3987
4138
  const edit = (file, label, item, resolves, change) => {
3988
- const entry = contents.get(file) ?? { content: readFileSync4(join12(forkPath, file), "utf8"), resolves: false };
4139
+ const entry = contents.get(file) ?? { content: readFileSync4(join13(forkPath, file), "utf8"), resolves: false };
3989
4140
  const next = change(entry.content);
3990
4141
  if (next === entry.content) return;
3991
4142
  contents.set(file, { content: next, resolves: entry.resolves || resolves });
3992
4143
  summary.set(label, [...summary.get(label) ?? [], item]);
3993
4144
  };
3994
- const chosen = rows.filter((row) => selected.includes(rowKey(row)));
4145
+ const chosen = rows.filter((row) => selected.includes(row.key));
3995
4146
  for (const row of chosen) {
3996
4147
  if (row.runtime) continue;
3997
4148
  for (const { name: dependent, location } of row.dependentPackages) {
3998
- const file = relative3(forkPath, join12(location, "package.json"));
4149
+ const file = relative3(forkPath, join13(location, "package.json"));
3999
4150
  edit(
4000
4151
  file,
4001
4152
  dependent || file,
@@ -4069,7 +4220,7 @@ import { select as select3 } from "@inquirer/prompts";
4069
4220
 
4070
4221
  // src/services/contrib-core.ts
4071
4222
  import { rmSync as rmSync2 } from "fs";
4072
- import { dirname as dirname7, join as join13, resolve as resolve4 } from "path";
4223
+ import { dirname as dirname8, join as join14, resolve as resolve4 } from "path";
4073
4224
  async function listFilesAtRef(repoPath, ref) {
4074
4225
  const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
4075
4226
  return new Set(raw.split("\n").filter(Boolean));
@@ -4085,10 +4236,10 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
4085
4236
  const baseFiles = await listFilesAtRef(repoPath, baseRef);
4086
4237
  const baseDirs = /* @__PURE__ */ new Set(["."]);
4087
4238
  for (const f of baseFiles) {
4088
- let dir = dirname7(f);
4239
+ let dir = dirname8(f);
4089
4240
  while (dir !== ".") {
4090
4241
  baseDirs.add(dir);
4091
- dir = dirname7(dir);
4242
+ dir = dirname8(dir);
4092
4243
  }
4093
4244
  }
4094
4245
  const files = [];
@@ -4101,7 +4252,7 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
4101
4252
  if (inCella && !inFork) {
4102
4253
  kind = "deleted";
4103
4254
  } else if (!inCella && inFork) {
4104
- if (!baseDirs.has(dirname7(file.path))) continue;
4255
+ if (!baseDirs.has(dirname8(file.path))) continue;
4105
4256
  kind = "created";
4106
4257
  } else {
4107
4258
  kind = "modified";
@@ -4125,7 +4276,7 @@ async function readBlobInfo(repoPath, ref, filePath) {
4125
4276
  async function buildContribBranch(repoPath, baseRef, forkRef, files, forkName) {
4126
4277
  const branch = `contrib/${forkName}`;
4127
4278
  const gitDir = resolve4(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
4128
- const tmpIndex = join13(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
4279
+ const tmpIndex = join14(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
4129
4280
  const indexEnv = { GIT_INDEX_FILE: tmpIndex };
4130
4281
  try {
4131
4282
  await git(["read-tree", baseRef], repoPath, { env: indexEnv });
@@ -4160,20 +4311,20 @@ ${commitBody}`;
4160
4311
  }
4161
4312
 
4162
4313
  // src/services/fork-utils.ts
4163
- import { existsSync as existsSync8 } from "fs";
4164
- import { dirname as dirname8, resolve as resolve5 } from "path";
4314
+ import { existsSync as existsSync9 } from "fs";
4315
+ import { dirname as dirname9, resolve as resolve5 } from "path";
4165
4316
  async function resolveForkBasePath(repoPath) {
4166
4317
  const commonDir = await git(["rev-parse", "--git-common-dir"], repoPath, { ignoreErrors: true });
4167
- return commonDir ? dirname8(resolve5(repoPath, commonDir)) : repoPath;
4318
+ return commonDir ? dirname9(resolve5(repoPath, commonDir)) : repoPath;
4168
4319
  }
4169
4320
  function validateForkPath(fork, basePath, requireConfig = false) {
4170
4321
  const resolvedPath = resolve5(basePath, fork.localPath);
4171
4322
  if (!resolvedPath.startsWith(resolve5(basePath, ".."))) {
4172
4323
  return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
4173
4324
  }
4174
- if (!existsSync8(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
4175
- if (!existsSync8(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
4176
- if (requireConfig && !existsSync8(`${resolvedPath}/${CONFIG_FILE}`)) {
4325
+ if (!existsSync9(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
4326
+ if (!existsSync9(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
4327
+ if (requireConfig && !existsSync9(`${resolvedPath}/${CONFIG_FILE}`)) {
4177
4328
  return { fork, resolvedPath, valid: false, error: `missing ${CONFIG_FILE}` };
4178
4329
  }
4179
4330
  return { fork, resolvedPath, valid: true };
@@ -4524,12 +4675,12 @@ function closePr(cwd, ref) {
4524
4675
  }
4525
4676
 
4526
4677
  // src/services/packages.ts
4527
- import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
4528
- import { join as join15 } from "path";
4678
+ import { existsSync as existsSync11, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
4679
+ import { join as join16 } from "path";
4529
4680
 
4530
4681
  // src/services/package-usage.ts
4531
- import { existsSync as existsSync9, readFileSync as readFileSync5, statSync } from "fs";
4532
- import { join as join14 } from "path";
4682
+ import { existsSync as existsSync10, readFileSync as readFileSync5, statSync } from "fs";
4683
+ import { join as join15 } from "path";
4533
4684
  var MAX_FILE_BYTES = 1024 * 1024;
4534
4685
  function escapeRegExp(value) {
4535
4686
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -4545,7 +4696,7 @@ function readSearchable(filePath) {
4545
4696
  }
4546
4697
  function fileSource(forkPath, path) {
4547
4698
  let text;
4548
- return { path, label: path, text: () => text ??= readSearchable(join14(forkPath, path)) };
4699
+ return { path, label: path, text: () => text ??= readSearchable(join15(forkPath, path)) };
4549
4700
  }
4550
4701
  async function readScriptsAtRef(forkPath, ref, path) {
4551
4702
  try {
@@ -4557,7 +4708,7 @@ async function readScriptsAtRef(forkPath, ref, path) {
4557
4708
  async function scriptSources(forkPath, upstreamRef, baseRef, path) {
4558
4709
  let forkScripts;
4559
4710
  try {
4560
- forkScripts = JSON.parse(readFileSync5(join14(forkPath, path), "utf8")).scripts ?? {};
4711
+ forkScripts = JSON.parse(readFileSync5(join15(forkPath, path), "utf8")).scripts ?? {};
4561
4712
  } catch {
4562
4713
  return [];
4563
4714
  }
@@ -4567,9 +4718,9 @@ async function scriptSources(forkPath, upstreamRef, baseRef, path) {
4567
4718
  }
4568
4719
  function binNames(forkPath, location, name) {
4569
4720
  const bareName = name.split("/").pop();
4570
- for (const dir of [join14(forkPath, location), forkPath]) {
4571
- const manifest = join14(dir, "node_modules", name, "package.json");
4572
- if (!existsSync9(manifest)) continue;
4721
+ for (const dir of [join15(forkPath, location), forkPath]) {
4722
+ const manifest = join15(dir, "node_modules", name, "package.json");
4723
+ if (!existsSync10(manifest)) continue;
4573
4724
  try {
4574
4725
  const { bin } = JSON.parse(readFileSync5(manifest, "utf8"));
4575
4726
  if (typeof bin === "string") return [bareName];
@@ -4750,7 +4901,7 @@ function setTopLevelKey(pkg, key, value) {
4750
4901
  Object.assign(pkg, Object.fromEntries(entries));
4751
4902
  }
4752
4903
  function readPackageJson(filePath) {
4753
- if (!existsSync10(filePath)) return null;
4904
+ if (!existsSync11(filePath)) return null;
4754
4905
  try {
4755
4906
  return JSON.parse(readFileSync6(filePath, "utf8"));
4756
4907
  } catch (error) {
@@ -4784,13 +4935,13 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
4784
4935
  const changes = [];
4785
4936
  const kept = [];
4786
4937
  const pkgRelPath = packageJsonPath(relativePath);
4787
- const pkgPath = join15(forkPath, pkgRelPath);
4938
+ const pkgPath = join16(forkPath, pkgRelPath);
4788
4939
  const forkPkg = readPackageJson(pkgPath);
4789
4940
  const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
4790
4941
  const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
4791
4942
  if (!upstream) return { updated: false, changes, kept };
4792
4943
  if (!forkPkg) {
4793
- const workspaceArrived = relativePath !== "" && existsSync10(join15(forkPath, relativePath));
4944
+ const workspaceArrived = relativePath !== "" && existsSync11(join16(forkPath, relativePath));
4794
4945
  if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
4795
4946
  writeFileSync3(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
4796
4947
  `, "utf8");
@@ -5417,11 +5568,11 @@ async function runForks(config) {
5417
5568
 
5418
5569
  // src/services/stats.ts
5419
5570
  import { readFile as readFile3 } from "fs/promises";
5420
- import { join as join17 } from "path";
5571
+ import { join as join18 } from "path";
5421
5572
 
5422
5573
  // src/utils/coverage-utils.ts
5423
- import { existsSync as existsSync11, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
5424
- import { join as join16, relative as relative4, sep as sep2 } from "path";
5574
+ import { existsSync as existsSync12, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
5575
+ import { join as join17, relative as relative4, sep as sep3 } from "path";
5425
5576
  var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
5426
5577
  function colorPct(pct) {
5427
5578
  if (typeof pct !== "number" || !Number.isFinite(pct)) return colors_default.dim("n/a");
@@ -5439,11 +5590,11 @@ function formatAge(ms) {
5439
5590
  return `${mins} minute${mins === 1 ? "" : "s"} ago`;
5440
5591
  }
5441
5592
  function printCoverageSummary(forkPath) {
5442
- const file = join16(forkPath, ".coverage", "coverage-summary.json");
5593
+ const file = join17(forkPath, ".coverage", "coverage-summary.json");
5443
5594
  let summary;
5444
5595
  let mtimeMs = 0;
5445
5596
  let problem = "no coverage data found";
5446
- if (existsSync11(file)) {
5597
+ if (existsSync12(file)) {
5447
5598
  try {
5448
5599
  mtimeMs = statSync2(file).mtimeMs;
5449
5600
  summary = JSON.parse(readFileSync7(file, "utf8"));
@@ -5464,7 +5615,7 @@ function printCoverageSummary(forkPath) {
5464
5615
  if (key === "total") continue;
5465
5616
  const rel = relative4(forkPath, key);
5466
5617
  if (rel.startsWith("..")) continue;
5467
- const pkg = rel.split(sep2)[0];
5618
+ const pkg = rel.split(sep3)[0];
5468
5619
  const acc = perPackage.get(pkg) ?? { covered: 0, total: 0 };
5469
5620
  acc.covered += entry.lines.covered;
5470
5621
  acc.total += entry.lines.total;
@@ -5490,7 +5641,7 @@ function printCoverageSummary(forkPath) {
5490
5641
  // src/services/stats.ts
5491
5642
  var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
5492
5643
  async function getWorkspacePackages(forkPath) {
5493
- const content = await readFile3(join17(forkPath, "pnpm-workspace.yaml"), "utf8");
5644
+ const content = await readFile3(join18(forkPath, "pnpm-workspace.yaml"), "utf8");
5494
5645
  const patterns = readTopLevel(content, "packages");
5495
5646
  return (Array.isArray(patterns) ? patterns.map(String) : []).map((pattern) => {
5496
5647
  if (pattern.endsWith("/*")) {
@@ -5538,9 +5689,9 @@ async function collectStats(forkPath) {
5538
5689
  const entries = [];
5539
5690
  for (const line of output.split("\n")) {
5540
5691
  if (!line) continue;
5541
- const sep3 = line.lastIndexOf(":");
5542
- if (sep3 === -1) continue;
5543
- entries.push({ path: line.slice(0, sep3), loc: Number.parseInt(line.slice(sep3 + 1), 10) });
5692
+ const sep4 = line.lastIndexOf(":");
5693
+ if (sep4 === -1) continue;
5694
+ entries.push({ path: line.slice(0, sep4), loc: Number.parseInt(line.slice(sep4 + 1), 10) });
5544
5695
  }
5545
5696
  const emptyCat = () => ({
5546
5697
  test: { files: 0, loc: 0 },
@@ -5843,7 +5994,7 @@ function getForkPath() {
5843
5994
  const envPath = getEnv("CELLA_FORK_PATH");
5844
5995
  if (envPath) {
5845
5996
  const resolved = resolve7(envPath);
5846
- if (!existsSync12(resolved) || !statSync3(resolved).isDirectory()) {
5997
+ if (!existsSync13(resolved) || !statSync3(resolved).isDirectory()) {
5847
5998
  throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
5848
5999
  }
5849
6000
  return resolved;
@@ -5855,7 +6006,7 @@ function getForkPath() {
5855
6006
  return cwd;
5856
6007
  }
5857
6008
  function preflight(forkPath) {
5858
- if (!existsSync12(join18(forkPath, ".git"))) {
6009
+ if (!existsSync13(join19(forkPath, ".git"))) {
5859
6010
  throw new Error(`not a git repository: ${forkPath}`);
5860
6011
  }
5861
6012
  }