@mnci/cli 4.0.7 → 4.1.0

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/cli.js CHANGED
@@ -38,7 +38,7 @@ module.exports = __toCommonJS(cli_exports);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/add.ts
41
- var import_node_path11 = require("path");
41
+ var import_node_path13 = require("path");
42
42
  var import_prompts5 = require("@inquirer/prompts");
43
43
 
44
44
  // src/nx.ts
@@ -78,6 +78,52 @@ function runShell(command, arguments_, cwd) {
78
78
  const result = import_cross_spawn.default.sync(command, arguments_, { stdio: "inherit", cwd });
79
79
  return result.status ?? 1;
80
80
  }
81
+ var CAPTURE_TIMEOUT_MS = 6e4;
82
+ function runCapture(command, arguments_, cwd) {
83
+ const result = import_cross_spawn.default.sync(command, arguments_, {
84
+ cwd,
85
+ encoding: "utf8",
86
+ timeout: CAPTURE_TIMEOUT_MS,
87
+ stdio: ["ignore", "pipe", "ignore"]
88
+ });
89
+ if (result.error) {
90
+ return { status: 1, stdout: "" };
91
+ }
92
+ return { status: result.status ?? 1, stdout: result.stdout ?? "" };
93
+ }
94
+ async function runCaptureAsync(command, arguments_, cwd) {
95
+ return await new Promise((resolve) => {
96
+ const child = (0, import_cross_spawn.default)(command, arguments_, {
97
+ cwd,
98
+ timeout: CAPTURE_TIMEOUT_MS,
99
+ stdio: ["ignore", "pipe", "ignore"]
100
+ });
101
+ let stdout = "";
102
+ child.stdout?.setEncoding("utf8");
103
+ child.stdout?.on("data", (chunk) => {
104
+ stdout += chunk;
105
+ });
106
+ child.on("error", () => {
107
+ resolve({ status: 1, stdout: "" });
108
+ });
109
+ child.on("close", (code) => {
110
+ resolve({ status: code ?? 1, stdout });
111
+ });
112
+ });
113
+ }
114
+ async function pool(items, limit, job) {
115
+ const results = Array.from({ length: items.length });
116
+ let next = 0;
117
+ const worker = async () => {
118
+ while (next < items.length) {
119
+ const index = next;
120
+ next += 1;
121
+ results[index] = await job(items[index]);
122
+ }
123
+ };
124
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => await worker()));
125
+ return results;
126
+ }
81
127
  function runNx(arguments_, cwd) {
82
128
  const status = runShell("npx", ["nx", ...arguments_], cwd);
83
129
  if (status !== 0) {
@@ -1343,6 +1389,161 @@ function markPublic(manifestPath) {
1343
1389
  var WRONG_TYPES_PATH = "./dist/index.esm.d.ts";
1344
1390
  var ACTUAL_TYPES_PATH = "./dist/src/index.d.ts";
1345
1391
  var DECLARATION_MAP_EXCLUSION = "!**/*.d.ts.map";
1392
+ var SOURCE_MAP_EXCLUSION = "!**/*.js.map";
1393
+ var ROLLUP_CONFIG_PLACEHOLDER = [
1394
+ " {",
1395
+ " // Provide additional rollup configuration here. See: https://rollupjs.org/configuration-options",
1396
+ " // e.g.",
1397
+ " // output: { sourcemap: true },",
1398
+ " }"
1399
+ ].join("\n");
1400
+ var GENERATED_COMPILER = " compiler: 'swc',";
1401
+ var SOURCE_MAP_CAPABLE_COMPILER = [
1402
+ " // Swapped from swc by MoNecromanCI. @nx/rollup runs swc without",
1403
+ " // sourceMaps, so it returns no map and the bundle's map comes out empty -",
1404
+ " // valid-looking, and useless for debugging. See ROADMAP.",
1405
+ " compiler: 'babel',"
1406
+ ].join("\n");
1407
+ var ROLLUP_ARG_ONE_BOUNDARY = [" },", " {", ""].join("\n");
1408
+ var ROLLUP_ARG_ONE_WITH_SOURCE_MAPS = [
1409
+ " // Added by MoNecromanCI: without this rollup emits no .js.map at all, so",
1410
+ " // a breakpoint in a .ts file can never bind. Not published - see `files`.",
1411
+ " sourceMap: true",
1412
+ " },",
1413
+ " {",
1414
+ ""
1415
+ ].join("\n");
1416
+ function hasRollupSourceMaps(config) {
1417
+ return config.includes("sourceMap: true");
1418
+ }
1419
+ function withRollupSourceMaps(config) {
1420
+ if (hasRollupSourceMaps(config) || !config.includes(ROLLUP_ARG_ONE_BOUNDARY)) {
1421
+ return config;
1422
+ }
1423
+ const withCompiler = config.replace(GENERATED_COMPILER, () => SOURCE_MAP_CAPABLE_COMPILER);
1424
+ const withFlag = withCompiler.replace(
1425
+ ROLLUP_ARG_ONE_BOUNDARY,
1426
+ () => ROLLUP_ARG_ONE_WITH_SOURCE_MAPS
1427
+ );
1428
+ return withFlag.includes("sourcemapPathTransform") ? withFlag : withFlag.replace(
1429
+ ROLLUP_ARG_ONE_WITH_SOURCE_MAPS,
1430
+ () => `${ROLLUP_ARG_ONE_WITH_SOURCE_MAPS}${SOURCEMAP_PATH_TRANSFORM}`
1431
+ );
1432
+ }
1433
+ function repairRollupSourceMaps(workspaceRoot) {
1434
+ const changed = [];
1435
+ const configs = (0, import_node_fs3.globSync)(["packages/*/rollup.config.cjs", "libs/*/rollup.config.cjs"], {
1436
+ cwd: workspaceRoot
1437
+ });
1438
+ for (const relativePath of configs) {
1439
+ const configPath = (0, import_node_path3.join)(workspaceRoot, relativePath);
1440
+ const before = (0, import_node_fs3.readFileSync)(configPath, "utf8");
1441
+ const after = withRollupSourceMaps(before);
1442
+ if (after !== before) {
1443
+ writeFileEnsured(configPath, after);
1444
+ changed.push(relativePath.replaceAll("\\", "/"));
1445
+ }
1446
+ }
1447
+ return changed;
1448
+ }
1449
+ var SOURCEMAP_PATH_TRANSFORM = [
1450
+ " // Added by MoNecromanCI. rollup hands sourcemapPathTransform an OS-NATIVE",
1451
+ " // path with one parent segment too many, so `sources` resolve to nothing",
1452
+ " // and no breakpoint can bind. Separators are normalised too: a sources",
1453
+ " // entry is URL-style, so a backslash is wrong on every platform.",
1454
+ " output: {",
1455
+ " sourcemapPathTransform: relativeSourcePath =>",
1456
+ " relativeSourcePath",
1457
+ " .replaceAll(String.fromCodePoint(92), '/')",
1458
+ " .replace(/^([.][.][/])+/, '../')",
1459
+ " },",
1460
+ ""
1461
+ ].join("\n");
1462
+ var ROLLUP_CONFIG_WITH_DTS_FIX = [
1463
+ " {",
1464
+ " // Added by MoNecromanCI. @nx/rollup's dts-bundle plugin writes dist/index.d.ts",
1465
+ " // as a stub re-exporting the real declarations, and builds that specifier with",
1466
+ " // path.relative() - an OS-NATIVE path. On Windows it emits",
1467
+ String.raw` // export * from "./src\\index";`,
1468
+ " // which is not a valid module specifier on ANY platform: a specifier is",
1469
+ " // URL-style, so / is correct everywhere and a backslash nowhere. It resolves on",
1470
+ " // Windows only because the resolver normalises separators there, leaving the",
1471
+ " // package untyped on Linux and macOS.",
1472
+ " //",
1473
+ " // mnci also points `types` past this stub, so nothing depends on it being",
1474
+ " // correct; this makes the emitted file correct too. Remove once Nx fixes the",
1475
+ " // plugin - its own devkit already exports normalizePath for exactly this.",
1476
+ " plugins: [",
1477
+ " {",
1478
+ " name: 'mnci-normalise-declaration-specifiers',",
1479
+ " writeBundle (outputOptions) {",
1480
+ " const { readFileSync, writeFileSync } = require('node:fs')",
1481
+ " const { join } = require('node:path')",
1482
+ " const stub = join(outputOptions.dir ?? './dist', 'index.d.ts')",
1483
+ " let source",
1484
+ " try {",
1485
+ " source = readFileSync(stub, 'utf8')",
1486
+ " } catch {",
1487
+ " return",
1488
+ " }",
1489
+ " // The stub carries a TWO-character escape (JSON.stringify escaped one",
1490
+ " // backslash), so this must not match a single one - that would turn",
1491
+ String.raw` // "./src\index" into "./src//index". Built from char codes so there is`,
1492
+ " // no escaping in this file to get wrong.",
1493
+ " const separator = String.fromCodePoint(92, 92)",
1494
+ " const normalised = source.replaceAll(separator, '/')",
1495
+ " if (normalised !== source) writeFileSync(stub, normalised)",
1496
+ " }",
1497
+ " }",
1498
+ " ]",
1499
+ " }"
1500
+ ].join("\n");
1501
+ function repairDeclarationSpecifiers(projectRoot) {
1502
+ const configPath = (0, import_node_path3.join)(projectRoot, "rollup.config.cjs");
1503
+ if (!fileExists(configPath)) {
1504
+ return;
1505
+ }
1506
+ const config = (0, import_node_fs3.readFileSync)(configPath, "utf8");
1507
+ if (!config.includes(ROLLUP_CONFIG_PLACEHOLDER)) {
1508
+ return;
1509
+ }
1510
+ const withDtsFix = config.split(ROLLUP_CONFIG_PLACEHOLDER).join(ROLLUP_CONFIG_WITH_DTS_FIX);
1511
+ writeFileEnsured(configPath, withRollupSourceMaps(withDtsFix));
1512
+ }
1513
+ function writeProjectReadme(projectRoot, projectName, testRunner) {
1514
+ const runner = testRunner === "vitest" ? "[Vitest](https://vitest.dev)" : "[Jest](https://jestjs.io)";
1515
+ writeFileEnsured(
1516
+ (0, import_node_path3.join)(projectRoot, "README.md"),
1517
+ [
1518
+ `# ${projectName}`,
1519
+ "",
1520
+ "Generated by [MoNecromanCI](https://github.com/russoedu/MoNecromanCi).",
1521
+ "",
1522
+ "## Building",
1523
+ "",
1524
+ `Run \`nx build ${projectName}\` to build this project.`,
1525
+ "",
1526
+ "## Running unit tests",
1527
+ "",
1528
+ `Run \`nx test ${projectName}\` to execute the unit tests via ${runner}.`,
1529
+ ""
1530
+ ].join("\n")
1531
+ );
1532
+ }
1533
+ function removeStaleGitkeeps(workspaceRoot) {
1534
+ for (const scaffold of ["apps", "libs", "packages"]) {
1535
+ const directory = (0, import_node_path3.join)(workspaceRoot, scaffold);
1536
+ try {
1537
+ const holdsAProject = (0, import_node_fs3.readdirSync)(directory, { withFileTypes: true }).some(
1538
+ (entry) => entry.isDirectory()
1539
+ );
1540
+ if (holdsAProject) {
1541
+ (0, import_node_fs3.rmSync)((0, import_node_path3.join)(directory, ".gitkeep"), { force: true });
1542
+ }
1543
+ } catch {
1544
+ }
1545
+ }
1546
+ }
1346
1547
  function repairPublishableManifest(manifestPath) {
1347
1548
  const manifest = readJson(manifestPath);
1348
1549
  if (manifest.types === WRONG_TYPES_PATH) {
@@ -1352,8 +1553,10 @@ function repairPublishableManifest(manifestPath) {
1352
1553
  if (typeof dot === "object" && dot.types === WRONG_TYPES_PATH) {
1353
1554
  dot.types = ACTUAL_TYPES_PATH;
1354
1555
  }
1355
- if (manifest.files && !manifest.files.includes(DECLARATION_MAP_EXCLUSION)) {
1356
- manifest.files.push(DECLARATION_MAP_EXCLUSION);
1556
+ for (const exclusion of [DECLARATION_MAP_EXCLUSION, SOURCE_MAP_EXCLUSION]) {
1557
+ if (manifest.files && !manifest.files.includes(exclusion)) {
1558
+ manifest.files.push(exclusion);
1559
+ }
1357
1560
  }
1358
1561
  writeFileEnsured(manifestPath, toJson(manifest));
1359
1562
  }
@@ -1412,6 +1615,7 @@ function projectTask(name, kind) {
1412
1615
  return kind === "start" ? { ...base, isBackground: true } : { ...base, group: kind };
1413
1616
  }
1414
1617
  function registerProjectCommands(workspaceRoot, name, commands) {
1618
+ removeStaleGitkeeps(workspaceRoot);
1415
1619
  const scripts = {
1416
1620
  [`${name}:qa`]: `nx run ${name}:lint && nx run ${name}:test`
1417
1621
  };
@@ -1879,6 +2083,8 @@ async function addNpmLib(workspaceRoot, name, options, kindProvided, stack) {
1879
2083
  const manifestPath = (0, import_node_path7.join)(workspaceRoot, "packages", name, "package.json");
1880
2084
  markPublic(manifestPath);
1881
2085
  repairPublishableManifest(manifestPath);
2086
+ repairDeclarationSpecifiers((0, import_node_path7.join)(workspaceRoot, `packages/${name}`));
2087
+ writeProjectReadme((0, import_node_path7.join)(workspaceRoot, `packages/${name}`), `${scope}/${name}`, stack.testRunner);
1882
2088
  removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
1883
2089
  registerProjectCommands(workspaceRoot, name, { build: true });
1884
2090
  }
@@ -2207,6 +2413,8 @@ async function addReactLib(workspaceRoot, name, options, kindProvided, stack) {
2207
2413
  const publishableManifest = (0, import_node_path10.join)(workspaceRoot, "packages", name, "package.json");
2208
2414
  markPublic(publishableManifest);
2209
2415
  repairPublishableManifest(publishableManifest);
2416
+ repairDeclarationSpecifiers((0, import_node_path10.join)(workspaceRoot, "packages", name));
2417
+ writeProjectReadme((0, import_node_path10.join)(workspaceRoot, "packages", name), `${scope}/${name}`, stack.testRunner);
2210
2418
  removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
2211
2419
  registerProjectCommands(workspaceRoot, name, { build: true });
2212
2420
  }
@@ -2228,10 +2436,611 @@ function addReactInternalLib(workspaceRoot, name, stack) {
2228
2436
  const privateManifest = (0, import_node_path10.join)(workspaceRoot, "libs", name, "package.json");
2229
2437
  markPrivate(privateManifest);
2230
2438
  repairPublishableManifest(privateManifest);
2439
+ repairDeclarationSpecifiers((0, import_node_path10.join)(workspaceRoot, "libs", name));
2440
+ writeProjectReadme((0, import_node_path10.join)(workspaceRoot, "libs", name), name, stack.testRunner);
2231
2441
  removeGeneratedEslintConfig(workspaceRoot, `libs/${name}`);
2232
2442
  registerProjectCommands(workspaceRoot, name, { build: true });
2233
2443
  }
2234
2444
 
2445
+ // src/commands/sync.ts
2446
+ var import_node_path12 = require("path");
2447
+
2448
+ // src/deps/inventory.ts
2449
+ var import_node_fs7 = require("fs");
2450
+ var import_node_path11 = require("path");
2451
+ var ECOSYSTEMS = ["npm", "pip", "pub", "go"];
2452
+ var ROOT_LABEL = "(root)";
2453
+ var MANIFEST_GLOBS = {
2454
+ // `libs/*` carries private internal libs; `packages/*` the publishable ones.
2455
+ npm: ["apps/*/package.json", "libs/*/package.json", "packages/*/package.json"],
2456
+ // Matches PYTHON_PROJECT_DIRS in commands/add/python.ts.
2457
+ pip: ["apps/*/pyproject.toml", "python-packages/*/pyproject.toml", "libs/*/pyproject.toml"],
2458
+ pub: ["apps/*/pubspec.yaml", "packages/*/pubspec.yaml", "libs/*/pubspec.yaml"],
2459
+ // Single root module by design — there are no per-project Go manifests.
2460
+ go: []
2461
+ };
2462
+ var PEER_SECTION = "peerDep";
2463
+ var NPM_SECTIONS = {
2464
+ dependencies: "dep",
2465
+ devDependencies: "devDep",
2466
+ peerDependencies: "peerDep",
2467
+ optionalDependencies: "optionalDep"
2468
+ };
2469
+ function toPosix(path) {
2470
+ return path.replaceAll("\\", "/");
2471
+ }
2472
+ function projectOf(manifestRelativePath) {
2473
+ const posix = toPosix(manifestRelativePath);
2474
+ const lastSlash = posix.lastIndexOf("/");
2475
+ return lastSlash === -1 ? ROOT_LABEL : posix.slice(0, lastSlash);
2476
+ }
2477
+ function hasEcosystem(workspaceRoot, ecosystem) {
2478
+ if (ecosystem === "go") {
2479
+ return fileExists((0, import_node_path11.join)(workspaceRoot, "go.mod"));
2480
+ }
2481
+ const rootMarker = {
2482
+ npm: "package.json",
2483
+ pip: "requirements-dev.txt",
2484
+ pub: "pubspec.yaml"
2485
+ };
2486
+ return fileExists((0, import_node_path11.join)(workspaceRoot, rootMarker[ecosystem])) || (0, import_node_fs7.globSync)(MANIFEST_GLOBS[ecosystem], { cwd: workspaceRoot }).length > 0;
2487
+ }
2488
+ function collectNpm(workspaceRoot) {
2489
+ const sites = [];
2490
+ const relativePaths = ["package.json", ...(0, import_node_fs7.globSync)(MANIFEST_GLOBS.npm, { cwd: workspaceRoot })];
2491
+ for (const relativePath of relativePaths) {
2492
+ const manifestPath = (0, import_node_path11.join)(workspaceRoot, relativePath);
2493
+ try {
2494
+ const manifest = readJson(manifestPath);
2495
+ sites.push(...npmManifestSites(manifest, relativePath, manifestPath));
2496
+ } catch {
2497
+ }
2498
+ }
2499
+ return sites;
2500
+ }
2501
+ function npmManifestSites(manifest, relativePath, manifestPath) {
2502
+ const sites = [];
2503
+ const project = projectOf(relativePath);
2504
+ for (const [block, section] of Object.entries(NPM_SECTIONS)) {
2505
+ const declared = manifest[block] ?? {};
2506
+ for (const [name, spec] of Object.entries(declared)) {
2507
+ sites.push({
2508
+ name,
2509
+ ecosystem: "npm",
2510
+ project,
2511
+ manifestPath,
2512
+ section,
2513
+ spec,
2514
+ // Two independent reasons to refuse a rewrite. A `file:`/`git+`/
2515
+ // `https:` target, a `workspace:` protocol or npm's `npm:pkg@range`
2516
+ // alias is a shape this cannot safely edit; a peer range is a shape it
2517
+ // could edit and must not (see PEER_SECTION).
2518
+ rewritable: section !== PEER_SECTION && /^[\^~>=<]*\d/.test(spec.trim())
2519
+ });
2520
+ }
2521
+ }
2522
+ return sites;
2523
+ }
2524
+ function parseRequirement(requirement) {
2525
+ const comment = requirement.indexOf("#");
2526
+ const text = (comment === -1 ? requirement : requirement.slice(0, comment)).trim();
2527
+ if (!text || text.startsWith("-")) {
2528
+ return void 0;
2529
+ }
2530
+ const name = /^[a-z0-9][\w.-]*/i.exec(text)?.[0];
2531
+ if (!name) {
2532
+ return void 0;
2533
+ }
2534
+ const spec = text.slice(name.length).trim();
2535
+ return {
2536
+ name,
2537
+ spec,
2538
+ // Extras, environment markers and compound ranges are reportable only.
2539
+ rewritable: !spec.startsWith("[") && !spec.includes(";") && !spec.includes(",")
2540
+ };
2541
+ }
2542
+ function collectPip(workspaceRoot) {
2543
+ const sites = [];
2544
+ const push = (relativePath, section, requirement) => {
2545
+ const parsed = parseRequirement(requirement);
2546
+ if (!parsed) {
2547
+ return;
2548
+ }
2549
+ sites.push({
2550
+ name: parsed.name,
2551
+ ecosystem: "pip",
2552
+ project: projectOf(relativePath),
2553
+ manifestPath: (0, import_node_path11.join)(workspaceRoot, relativePath),
2554
+ section,
2555
+ spec: parsed.spec,
2556
+ rewritable: parsed.rewritable && parsed.spec !== ""
2557
+ });
2558
+ };
2559
+ const pyprojectPaths = (0, import_node_fs7.globSync)(MANIFEST_GLOBS.pip, { cwd: workspaceRoot });
2560
+ for (const relativePath of pyprojectPaths) {
2561
+ let content;
2562
+ try {
2563
+ content = (0, import_node_fs7.readFileSync)((0, import_node_path11.join)(workspaceRoot, relativePath), "utf8");
2564
+ } catch {
2565
+ continue;
2566
+ }
2567
+ const declared = pyprojectDependencies(content);
2568
+ for (const entry of declared) {
2569
+ push(relativePath, "dep", entry);
2570
+ }
2571
+ }
2572
+ const requirementsPaths = [
2573
+ "requirements-dev.txt",
2574
+ ...(0, import_node_fs7.globSync)("apps/*/requirements.txt", { cwd: workspaceRoot })
2575
+ ];
2576
+ for (const relativePath of requirementsPaths) {
2577
+ if (!fileExists((0, import_node_path11.join)(workspaceRoot, relativePath))) {
2578
+ continue;
2579
+ }
2580
+ const section = relativePath === "requirements-dev.txt" ? "devDep" : "dep";
2581
+ const lines = (0, import_node_fs7.readFileSync)((0, import_node_path11.join)(workspaceRoot, relativePath), "utf8").split(/\r?\n/);
2582
+ for (const line of lines) {
2583
+ push(relativePath, section, line);
2584
+ }
2585
+ }
2586
+ return sites;
2587
+ }
2588
+ function pyprojectDependencies(content) {
2589
+ const entries = [];
2590
+ let inProjectTable = false;
2591
+ let inArray = false;
2592
+ for (const line of content.split(/\r?\n/)) {
2593
+ const text = line.trim();
2594
+ if (text.startsWith("[")) {
2595
+ inProjectTable = text === "[project]";
2596
+ inArray = false;
2597
+ continue;
2598
+ }
2599
+ if (!inProjectTable) {
2600
+ continue;
2601
+ }
2602
+ if (!inArray && /^dependencies\s*=\s*\[/.test(text)) {
2603
+ inArray = true;
2604
+ }
2605
+ if (!inArray) {
2606
+ continue;
2607
+ }
2608
+ for (const quoted of text.matchAll(/"([^"]*)"|'([^']*)'/g)) {
2609
+ entries.push(quoted[1] ?? quoted[2]);
2610
+ }
2611
+ if (text.includes("]")) {
2612
+ inArray = false;
2613
+ }
2614
+ }
2615
+ return entries;
2616
+ }
2617
+ function collectPub(workspaceRoot) {
2618
+ const sites = [];
2619
+ const pubspecPaths = (0, import_node_fs7.globSync)(MANIFEST_GLOBS.pub, { cwd: workspaceRoot });
2620
+ for (const relativePath of pubspecPaths) {
2621
+ let content;
2622
+ try {
2623
+ content = (0, import_node_fs7.readFileSync)((0, import_node_path11.join)(workspaceRoot, relativePath), "utf8");
2624
+ } catch {
2625
+ continue;
2626
+ }
2627
+ for (const block of ["dependencies", "dev_dependencies"]) {
2628
+ for (const entry of pubspecBlockEntries(content, block)) {
2629
+ sites.push({
2630
+ name: entry.name,
2631
+ ecosystem: "pub",
2632
+ project: projectOf(relativePath),
2633
+ manifestPath: (0, import_node_path11.join)(workspaceRoot, relativePath),
2634
+ section: block === "dependencies" ? "dep" : "devDep",
2635
+ spec: entry.spec,
2636
+ rewritable: entry.spec !== ""
2637
+ });
2638
+ }
2639
+ }
2640
+ }
2641
+ return sites;
2642
+ }
2643
+ function pubspecBlockEntries(content, block) {
2644
+ const lines = content.split(/\r?\n/);
2645
+ const start = lines.findIndex((line) => line.trimEnd() === `${block}:`);
2646
+ if (start === -1) {
2647
+ return [];
2648
+ }
2649
+ const entries = [];
2650
+ let indent;
2651
+ const body = lines.slice(start + 1);
2652
+ for (const line of body) {
2653
+ if (line.trim() === "" || line.trimStart().startsWith("#")) {
2654
+ continue;
2655
+ }
2656
+ const lineIndent = line.length - line.trimStart().length;
2657
+ if (lineIndent === 0) {
2658
+ break;
2659
+ }
2660
+ indent ??= lineIndent;
2661
+ if (lineIndent > indent) {
2662
+ continue;
2663
+ }
2664
+ if (lineIndent < indent) {
2665
+ break;
2666
+ }
2667
+ const colon = line.indexOf(":");
2668
+ if (colon === -1) {
2669
+ continue;
2670
+ }
2671
+ const key = line.slice(0, colon).trim();
2672
+ if (/^[a-z_][\w-]*$/i.test(key)) {
2673
+ entries.push({ name: key, spec: stripQuotes(line.slice(colon + 1).trim()) });
2674
+ }
2675
+ }
2676
+ return entries;
2677
+ }
2678
+ function stripQuotes(value) {
2679
+ return /^(['"]).*\1$/.test(value) ? value.slice(1, -1) : value;
2680
+ }
2681
+ function collectGo(workspaceRoot) {
2682
+ const manifestPath = (0, import_node_path11.join)(workspaceRoot, "go.mod");
2683
+ if (!fileExists(manifestPath)) {
2684
+ return [];
2685
+ }
2686
+ let content;
2687
+ try {
2688
+ content = (0, import_node_fs7.readFileSync)(manifestPath, "utf8");
2689
+ } catch {
2690
+ return [];
2691
+ }
2692
+ const sites = Array.from(content.matchAll(
2693
+ /^\s*(?:require\s+)?([\w.~-]+(?:\/[\w.~-]+)+)\s+(v\S+)(\s*\/\/\s*indirect)?/gm
2694
+ ), (match) => ({
2695
+ name: match[1],
2696
+ ecosystem: "go",
2697
+ project: ROOT_LABEL,
2698
+ manifestPath,
2699
+ section: match[3] ? "indirect" : "module",
2700
+ spec: match[2],
2701
+ rewritable: false
2702
+ }));
2703
+ return sites;
2704
+ }
2705
+ function collectInventory(workspaceRoot, ecosystems = ECOSYSTEMS) {
2706
+ const readers = {
2707
+ npm: collectNpm,
2708
+ pip: collectPip,
2709
+ pub: collectPub,
2710
+ go: collectGo
2711
+ };
2712
+ const inventory = /* @__PURE__ */ new Map();
2713
+ for (const ecosystem of ecosystems) {
2714
+ if (!hasEcosystem(workspaceRoot, ecosystem)) {
2715
+ continue;
2716
+ }
2717
+ const sites = readers[ecosystem](workspaceRoot);
2718
+ for (const site of sites) {
2719
+ const existing = inventory.get(site.name);
2720
+ if (existing) {
2721
+ existing.push(site);
2722
+ } else {
2723
+ inventory.set(site.name, [site]);
2724
+ }
2725
+ }
2726
+ }
2727
+ return inventory;
2728
+ }
2729
+ function rewriteSpec(site, spec) {
2730
+ if (!site.rewritable || spec === site.spec) {
2731
+ return false;
2732
+ }
2733
+ const { name } = site;
2734
+ if (site.ecosystem === "npm") {
2735
+ const manifest = readJson(site.manifestPath);
2736
+ const block = Object.entries(NPM_SECTIONS).find(([, label]) => label === site.section)?.[0];
2737
+ if (!block || manifest[block]?.[name] === void 0) {
2738
+ return false;
2739
+ }
2740
+ manifest[block][name] = spec;
2741
+ writeFileEnsured(site.manifestPath, toJson(manifest));
2742
+ return true;
2743
+ }
2744
+ const before = (0, import_node_fs7.readFileSync)(site.manifestPath, "utf8");
2745
+ const after = site.ecosystem === "pip" ? replacePipSpec(before, name, spec) : replacePubSpec(before, name, spec);
2746
+ if (after === before) {
2747
+ return false;
2748
+ }
2749
+ writeFileEnsured(site.manifestPath, after);
2750
+ return true;
2751
+ }
2752
+ function replacePipSpec(content, name, spec) {
2753
+ const escaped = escapeForRegex(name);
2754
+ return content.replaceAll(
2755
+ new RegExp(String.raw`(^|["'\s])(${escaped})\s*(?:[<>=!~^][^"'\n,]*)?(?=["'\n]|$)`, "gm"),
2756
+ (match, lead, matched) => `${lead}${matched}${spec}`
2757
+ );
2758
+ }
2759
+ function replacePubSpec(content, name, spec) {
2760
+ return content.replaceAll(
2761
+ new RegExp(String.raw`^(\s+${escapeForRegex(name)}:[^\S\n]*)\S[^\n]*$`, "gm"),
2762
+ (match, lead) => `${lead}${spec}`
2763
+ );
2764
+ }
2765
+ function escapeForRegex(value) {
2766
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
2767
+ }
2768
+ function isWorkspaceProject(workspaceRoot, name) {
2769
+ const short = name.includes("/") ? name.split("/").at(-1) : name;
2770
+ return ["apps", "libs", "packages"].some(
2771
+ (directory) => fileExists((0, import_node_path11.join)(workspaceRoot, directory, short ?? name, "package.json"))
2772
+ );
2773
+ }
2774
+ function isAliasedInstall(workspaceRoot, name) {
2775
+ try {
2776
+ const installed = readJson(
2777
+ (0, import_node_path11.join)(workspaceRoot, "node_modules", name, "package.json")
2778
+ );
2779
+ return installed.name !== void 0 && installed.name !== name;
2780
+ } catch {
2781
+ return false;
2782
+ }
2783
+ }
2784
+ function resolvedVersion(workspaceRoot, ecosystem, name) {
2785
+ if (ecosystem === "npm") {
2786
+ try {
2787
+ return readJson(
2788
+ (0, import_node_path11.join)(workspaceRoot, "node_modules", name, "package.json")
2789
+ ).version;
2790
+ } catch {
2791
+ return void 0;
2792
+ }
2793
+ }
2794
+ if (ecosystem === "pub") {
2795
+ const lockPath = (0, import_node_path11.join)(workspaceRoot, "pubspec.lock");
2796
+ if (!fileExists(lockPath)) {
2797
+ return void 0;
2798
+ }
2799
+ try {
2800
+ const match = new RegExp(
2801
+ String.raw`^\s{2}${escapeForRegex(name)}:[^]*?^\s+version:\s*["']?([^"'\s]+)`,
2802
+ "m"
2803
+ ).exec((0, import_node_fs7.readFileSync)(lockPath, "utf8"));
2804
+ return match?.[1];
2805
+ } catch {
2806
+ return void 0;
2807
+ }
2808
+ }
2809
+ if (ecosystem === "pip") {
2810
+ const python = process.platform === "win32" ? "python" : "python3";
2811
+ const result = runCapture(python, ["-m", "pip", "show", name], workspaceRoot);
2812
+ if (result.status !== 0) {
2813
+ return void 0;
2814
+ }
2815
+ return /^Version:\s*(\S+)/m.exec(result.stdout)?.[1];
2816
+ }
2817
+ return void 0;
2818
+ }
2819
+
2820
+ // src/deps/semver.ts
2821
+ var UPDATE_KINDS = ["patch", "minor", "major", "non-semver"];
2822
+ var UPDATE_KIND_LABELS = {
2823
+ patch: { title: "Patch Update", blurb: "Backwards-compatible bug fixes." },
2824
+ minor: { title: "Minor Update", blurb: "New backwards-compatible features." },
2825
+ major: { title: "Major Update", blurb: "Potentially breaking API changes. Use caution." },
2826
+ "non-semver": { title: "Non-Semver", blurb: "Versions less than 1.0.0, caution." }
2827
+ };
2828
+ var RANGE_OPERATORS = [">=", "<=", "^", "~", ">", "<", "="];
2829
+ function parseVersion(raw) {
2830
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?/.exec(raw.trim());
2831
+ if (!match) {
2832
+ return void 0;
2833
+ }
2834
+ return {
2835
+ major: Number(match[1]),
2836
+ minor: Number(match[2]),
2837
+ patch: Number(match[3]),
2838
+ prerelease: match[4]
2839
+ };
2840
+ }
2841
+ function rangeOperator(spec) {
2842
+ const trimmed = spec.trim();
2843
+ return RANGE_OPERATORS.find((operator) => trimmed.startsWith(operator)) ?? "";
2844
+ }
2845
+ function specVersion(spec) {
2846
+ const bare2 = spec.trim().slice(rangeOperator(spec).length).trim();
2847
+ return /^\d+\.\d+(?:\.\d+)?(?:-[\w.-]+)?$/.test(bare2) ? bare2 : void 0;
2848
+ }
2849
+ function compareVersions(left, right) {
2850
+ const a = parseVersion(left);
2851
+ const b = parseVersion(right);
2852
+ if (!a || !b) {
2853
+ return 0;
2854
+ }
2855
+ if (a.major !== b.major) {
2856
+ return a.major - b.major;
2857
+ }
2858
+ if (a.minor !== b.minor) {
2859
+ return a.minor - b.minor;
2860
+ }
2861
+ if (a.patch !== b.patch) {
2862
+ return a.patch - b.patch;
2863
+ }
2864
+ if (a.prerelease === b.prerelease) {
2865
+ return 0;
2866
+ }
2867
+ if (!a.prerelease) {
2868
+ return 1;
2869
+ }
2870
+ if (!b.prerelease) {
2871
+ return -1;
2872
+ }
2873
+ return a.prerelease < b.prerelease ? -1 : 1;
2874
+ }
2875
+ function isNewer(candidate, current) {
2876
+ return compareVersions(candidate, current) > 0;
2877
+ }
2878
+ function classify(current, latest) {
2879
+ const from = parseVersion(current);
2880
+ const to = parseVersion(latest);
2881
+ if (!from || !to || from.major === 0) {
2882
+ return "non-semver";
2883
+ }
2884
+ if (from.major !== to.major) {
2885
+ return "major";
2886
+ }
2887
+ return from.minor === to.minor ? "patch" : "minor";
2888
+ }
2889
+ function highestSpec(specs) {
2890
+ let best;
2891
+ let bestVersion;
2892
+ for (const spec of specs) {
2893
+ const version = specVersion(spec);
2894
+ if (!version) {
2895
+ continue;
2896
+ }
2897
+ if (!bestVersion || isNewer(version, bestVersion)) {
2898
+ best = spec;
2899
+ bestVersion = version;
2900
+ }
2901
+ }
2902
+ return best;
2903
+ }
2904
+
2905
+ // src/commands/sync.ts
2906
+ function resolveEcosystems(requested) {
2907
+ if (!requested) {
2908
+ return ECOSYSTEMS;
2909
+ }
2910
+ const match = ECOSYSTEMS.find((ecosystem) => ecosystem === requested);
2911
+ if (!match) {
2912
+ throw new Error(
2913
+ `Unknown ecosystem '${requested}'. Expected one of: ${ECOSYSTEMS.join(", ")}.`
2914
+ );
2915
+ }
2916
+ return [match];
2917
+ }
2918
+ function collectDrift(workspaceRoot, ecosystems = ECOSYSTEMS) {
2919
+ const reconcilable = ecosystems.filter((ecosystem) => ecosystem !== "go");
2920
+ const inventory = collectInventory(workspaceRoot, reconcilable);
2921
+ const drift = [];
2922
+ for (const [name, allSites] of inventory) {
2923
+ const sites = allSites.filter((site) => site.section !== PEER_SECTION);
2924
+ if (sites.length < 2 || isWorkspaceLocal(workspaceRoot, sites, name)) {
2925
+ continue;
2926
+ }
2927
+ const distinct = new Set(sites.map((site) => site.spec));
2928
+ if (distinct.size < 2) {
2929
+ continue;
2930
+ }
2931
+ const installed = resolvedVersion(workspaceRoot, sites[0].ecosystem, name);
2932
+ const target = chooseTarget(sites, installed);
2933
+ if (!target) {
2934
+ continue;
2935
+ }
2936
+ const disagreeing = sites.filter((site) => site.spec !== target);
2937
+ drift.push({
2938
+ name,
2939
+ ecosystem: sites[0].ecosystem,
2940
+ target,
2941
+ reason: installed ? "resolved" : "highest",
2942
+ fixable: disagreeing.filter((site) => site.rewritable),
2943
+ blocked: disagreeing.filter((site) => !site.rewritable)
2944
+ });
2945
+ }
2946
+ return drift.toSorted((left, right) => left.name.localeCompare(right.name));
2947
+ }
2948
+ function isWorkspaceLocal(workspaceRoot, sites, name) {
2949
+ return sites[0].ecosystem === "npm" && isWorkspaceProject(workspaceRoot, name);
2950
+ }
2951
+ function chooseTarget(sites, installed) {
2952
+ if (!installed) {
2953
+ return highestSpec(sites.map((site) => site.spec));
2954
+ }
2955
+ const counts = /* @__PURE__ */ new Map();
2956
+ for (const site of sites) {
2957
+ if (!specVersion(site.spec)) {
2958
+ continue;
2959
+ }
2960
+ const operator2 = rangeOperator(site.spec);
2961
+ counts.set(operator2, (counts.get(operator2) ?? 0) + 1);
2962
+ }
2963
+ if (counts.size === 0) {
2964
+ return void 0;
2965
+ }
2966
+ const [operator] = [...counts].toSorted(
2967
+ (left, right) => right[1] - left[1] || (left[0] === "^" ? -1 : 1)
2968
+ )[0];
2969
+ return `${operator}${installed}`;
2970
+ }
2971
+ function applyDrift(drift) {
2972
+ const changed = /* @__PURE__ */ new Set();
2973
+ for (const finding of drift) {
2974
+ for (const site of finding.fixable) {
2975
+ if (rewriteSpec(site, finding.target)) {
2976
+ changed.add(site.manifestPath);
2977
+ }
2978
+ }
2979
+ }
2980
+ return [...changed];
2981
+ }
2982
+ function syncProjectReferences(workspaceRoot) {
2983
+ logger.step("Syncing TypeScript project references (nx sync)");
2984
+ if (runShell("npx", ["nx", "sync"], workspaceRoot) !== 0) {
2985
+ logger.warn(
2986
+ "nx sync did not complete \u2014 run `npx nx sync` yourself so cross-project imports resolve in your editor."
2987
+ );
2988
+ }
2989
+ }
2990
+ function describe(finding) {
2991
+ const sites = [...finding.fixable, ...finding.blocked].map((site) => `${site.project === ROOT_LABEL ? ROOT_LABEL : site.project} ${site.spec}`).join(", ");
2992
+ return `${finding.name} \u2192 ${finding.target} (${finding.reason}); disagreeing: ${sites}`;
2993
+ }
2994
+ function runSync(workspaceRoot, options) {
2995
+ if (!fileExists((0, import_node_path12.join)(workspaceRoot, "nx.json"))) {
2996
+ throw new Error(
2997
+ `No nx.json found in ${workspaceRoot} \u2014 run 'mnci sync' from the workspace root.`
2998
+ );
2999
+ }
3000
+ const ecosystems = resolveEcosystems(options.ecosystem);
3001
+ for (const ecosystem of ecosystems) {
3002
+ if (!hasEcosystem(workspaceRoot, ecosystem)) {
3003
+ logger.info(`\u2298 SKIPPED ${ecosystem} \u2014 no ${ecosystem} project in this workspace.`);
3004
+ }
3005
+ }
3006
+ if (ecosystems.includes("go") && hasEcosystem(workspaceRoot, "go")) {
3007
+ logger.success("go \u2014 nothing to sync; one root go.mod means one version of every module.");
3008
+ }
3009
+ const drift = collectDrift(workspaceRoot, ecosystems);
3010
+ if (drift.length === 0) {
3011
+ logger.success("Every project agrees on every shared dependency version.");
3012
+ } else {
3013
+ for (const finding of drift) {
3014
+ logger.info(` ${describe(finding)}`);
3015
+ }
3016
+ }
3017
+ if (options.check) {
3018
+ if (drift.length > 0) {
3019
+ logger.error(
3020
+ `${drift.length} package(s) declared at more than one version. Run 'mnci sync' to converge them.`
3021
+ );
3022
+ process.exitCode = 1;
3023
+ }
3024
+ return;
3025
+ }
3026
+ const changed = applyDrift(drift);
3027
+ for (const path of changed) {
3028
+ logger.detail(`updated ${path}`);
3029
+ }
3030
+ const blocked = drift.flatMap((finding) => finding.blocked);
3031
+ for (const site of blocked) {
3032
+ logger.warn(
3033
+ `${site.name} in ${site.project} is declared as '${site.spec}', which is not a plain version range \u2014 left unchanged.`
3034
+ );
3035
+ }
3036
+ syncProjectReferences(workspaceRoot);
3037
+ if (changed.length > 0) {
3038
+ logger.step("Formatting the changed manifests (eslint --fix)");
3039
+ runFormatter(workspaceRoot);
3040
+ }
3041
+ logger.success("Done. Review the changes with `git diff` before committing.");
3042
+ }
3043
+
2235
3044
  // src/commands/add.ts
2236
3045
  var PROJECT_KINDS = [
2237
3046
  "react-app",
@@ -2256,7 +3065,7 @@ var PROJECT_KINDS = [
2256
3065
  ];
2257
3066
  async function runAdd(kind, name, options) {
2258
3067
  const workspaceRoot = process.cwd();
2259
- if (!fileExists((0, import_node_path11.join)(workspaceRoot, "nx.json"))) {
3068
+ if (!fileExists((0, import_node_path13.join)(workspaceRoot, "nx.json"))) {
2260
3069
  throw new Error("No nx.json found here. Run `add` from the workspace root.");
2261
3070
  }
2262
3071
  const stack = readWorkspaceStack(workspaceRoot);
@@ -2305,7 +3114,7 @@ async function runAdd(kind, name, options) {
2305
3114
  ],
2306
3115
  workspaceRoot
2307
3116
  );
2308
- markPrivate((0, import_node_path11.join)(workspaceRoot, "libs", resolvedName, "package.json"));
3117
+ markPrivate((0, import_node_path13.join)(workspaceRoot, "libs", resolvedName, "package.json"));
2309
3118
  removeGeneratedEslintConfig(workspaceRoot, `libs/${resolvedName}`);
2310
3119
  registerProjectCommands(workspaceRoot, resolvedName, { build: true });
2311
3120
  break;
@@ -2371,17 +3180,9 @@ async function runAdd(kind, name, options) {
2371
3180
  runFormatter(workspaceRoot);
2372
3181
  logger.success(`Added ${resolvedKind} '${resolvedName}'.`);
2373
3182
  }
2374
- function syncProjectReferences(workspaceRoot) {
2375
- logger.step("Syncing TypeScript project references (nx sync)");
2376
- if (runShell("npx", ["nx", "sync"], workspaceRoot) !== 0) {
2377
- logger.warn(
2378
- "nx sync did not complete \u2014 run `npx nx sync` yourself so cross-project imports resolve in your editor."
2379
- );
2380
- }
2381
- }
2382
3183
  function readWorkspaceStack(workspaceRoot) {
2383
3184
  const nxJson = readJson(
2384
- (0, import_node_path11.join)(workspaceRoot, "nx.json")
3185
+ (0, import_node_path13.join)(workspaceRoot, "nx.json")
2385
3186
  );
2386
3187
  const stack = nxJson.mnci?.stack;
2387
3188
  return {
@@ -2390,17 +3191,17 @@ function readWorkspaceStack(workspaceRoot) {
2390
3191
  }
2391
3192
 
2392
3193
  // src/commands/doctor.ts
2393
- var import_node_fs7 = require("fs");
2394
- var import_node_path12 = require("path");
3194
+ var import_node_fs8 = require("fs");
3195
+ var import_node_path14 = require("path");
2395
3196
  var SUPPORTED_ESLINT_MAJOR = ESLINT_VERSION.replace(/^\D*/, "").split(".", 1)[0];
2396
- function toPosix(path) {
3197
+ function toPosix2(path) {
2397
3198
  return path.replaceAll("\\", "/");
2398
3199
  }
2399
3200
  function checkEslintConfigs(workspaceRoot) {
2400
- const rootConfigs = (0, import_node_fs7.globSync)("eslint.config.{js,mjs,cjs,ts,mts,cts}", { cwd: workspaceRoot });
2401
- const projectConfigs = (0, import_node_fs7.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
3201
+ const rootConfigs = (0, import_node_fs8.globSync)("eslint.config.{js,mjs,cjs,ts,mts,cts}", { cwd: workspaceRoot });
3202
+ const projectConfigs = (0, import_node_fs8.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
2402
3203
  cwd: workspaceRoot
2403
- }).map((config) => toPosix(config));
3204
+ }).map((config) => toPosix2(config));
2404
3205
  return [
2405
3206
  {
2406
3207
  check: "root ESLint config",
@@ -2419,7 +3220,7 @@ function checkEslintConfigs(workspaceRoot) {
2419
3220
  function declaredDevDependencies(workspaceRoot) {
2420
3221
  try {
2421
3222
  const manifest = readJson(
2422
- (0, import_node_path12.join)(workspaceRoot, "package.json")
3223
+ (0, import_node_path14.join)(workspaceRoot, "package.json")
2423
3224
  );
2424
3225
  return manifest.devDependencies ?? {};
2425
3226
  } catch {
@@ -2427,7 +3228,7 @@ function declaredDevDependencies(workspaceRoot) {
2427
3228
  }
2428
3229
  }
2429
3230
  function checkNoRetiredFormatter(workspaceRoot) {
2430
- const files = RETIRED_FORMATTER_FILES.filter((file) => fileExists((0, import_node_path12.join)(workspaceRoot, file)));
3231
+ const files = RETIRED_FORMATTER_FILES.filter((file) => fileExists((0, import_node_path14.join)(workspaceRoot, file)));
2431
3232
  const declared = ["prettier", "eslint-config-prettier", "oxlint", "oxfmt", "@mnci/oxlint-config"].filter((name) => declaredDevDependencies(workspaceRoot)[name] !== void 0);
2432
3233
  const stale = [...files, ...declared];
2433
3234
  if (stale.length === 0) {
@@ -2453,7 +3254,7 @@ function checkEslintPlugin(nxJson) {
2453
3254
  };
2454
3255
  }
2455
3256
  function checkResolvedEslint(workspaceRoot) {
2456
- const manifestPath = (0, import_node_path12.join)(workspaceRoot, "node_modules/eslint/package.json");
3257
+ const manifestPath = (0, import_node_path14.join)(workspaceRoot, "node_modules/eslint/package.json");
2457
3258
  if (!fileExists(manifestPath)) {
2458
3259
  return void 0;
2459
3260
  }
@@ -2471,11 +3272,11 @@ function checkResolvedEslint(workspaceRoot) {
2471
3272
  }
2472
3273
  }
2473
3274
  function checkNpmrc(workspaceRoot, registry, scope) {
2474
- const npmrcPath = (0, import_node_path12.join)(workspaceRoot, ".npmrc");
3275
+ const npmrcPath = (0, import_node_path14.join)(workspaceRoot, ".npmrc");
2475
3276
  if (!registry || !fileExists(npmrcPath)) {
2476
3277
  return void 0;
2477
3278
  }
2478
- const npmrc = (0, import_node_fs7.readFileSync)(npmrcPath, "utf8");
3279
+ const npmrc = (0, import_node_fs8.readFileSync)(npmrcPath, "utf8");
2479
3280
  if (registry.kind === "npm") {
2480
3281
  return {
2481
3282
  check: ".npmrc authenticates the public registry",
@@ -2493,12 +3294,12 @@ function checkNpmrc(workspaceRoot, registry, scope) {
2493
3294
  }
2494
3295
  function checkVersionActions(workspaceRoot) {
2495
3296
  const candidates = [
2496
- ...(0, import_node_fs7.globSync)("packages/*/pubspec.yaml", { cwd: workspaceRoot }),
2497
- ...(0, import_node_fs7.globSync)("python-packages/*/pyproject.toml", { cwd: workspaceRoot })
2498
- ].map((manifest) => toPosix(manifest));
3297
+ ...(0, import_node_fs8.globSync)("packages/*/pubspec.yaml", { cwd: workspaceRoot }),
3298
+ ...(0, import_node_fs8.globSync)("python-packages/*/pyproject.toml", { cwd: workspaceRoot })
3299
+ ].map((manifest) => toPosix2(manifest));
2499
3300
  return candidates.flatMap((manifest) => {
2500
3301
  const projectRoot = manifest.slice(0, manifest.lastIndexOf("/"));
2501
- const projectJsonPath = (0, import_node_path12.join)(workspaceRoot, projectRoot, "project.json");
3302
+ const projectJsonPath = (0, import_node_path14.join)(workspaceRoot, projectRoot, "project.json");
2502
3303
  let hasOverride = false;
2503
3304
  try {
2504
3305
  const projectJson = readJson(projectJsonPath);
@@ -2527,7 +3328,7 @@ function declaredTargets(workspaceRoot, projectRoot) {
2527
3328
  ]) {
2528
3329
  try {
2529
3330
  const targets = extract(
2530
- readJson((0, import_node_path12.join)(workspaceRoot, projectRoot, file))
3331
+ readJson((0, import_node_path14.join)(workspaceRoot, projectRoot, file))
2531
3332
  );
2532
3333
  if (targets && typeof targets === "object") {
2533
3334
  sources.push({
@@ -2543,9 +3344,9 @@ function declaredTargets(workspaceRoot, projectRoot) {
2543
3344
  function checkTargetFilesExist(workspaceRoot) {
2544
3345
  const projectRoots = new Set(
2545
3346
  [
2546
- ...(0, import_node_fs7.globSync)("{apps,libs,packages}/*/project.json", { cwd: workspaceRoot }),
2547
- ...(0, import_node_fs7.globSync)("{apps,libs,packages}/*/package.json", { cwd: workspaceRoot })
2548
- ].map((manifest) => toPosix(manifest).split("/").slice(0, 2).join("/"))
3347
+ ...(0, import_node_fs8.globSync)("{apps,libs,packages}/*/project.json", { cwd: workspaceRoot }),
3348
+ ...(0, import_node_fs8.globSync)("{apps,libs,packages}/*/package.json", { cwd: workspaceRoot })
3349
+ ].map((manifest) => toPosix2(manifest).split("/").slice(0, 2).join("/"))
2549
3350
  );
2550
3351
  return [...projectRoots].flatMap(
2551
3352
  (projectRoot) => declaredTargets(workspaceRoot, projectRoot).flatMap(
@@ -2559,7 +3360,7 @@ function checkOneTargetsFiles(workspaceRoot, source, targetName, target) {
2559
3360
  return TARGET_FILE_OPTIONS.flatMap((option) => {
2560
3361
  const value = target?.options?.[option];
2561
3362
  const checkable = typeof value === "string" && !value.includes("{");
2562
- if (!checkable || fileExists((0, import_node_path12.join)(workspaceRoot, value))) {
3363
+ if (!checkable || fileExists((0, import_node_path14.join)(workspaceRoot, value))) {
2563
3364
  return [];
2564
3365
  }
2565
3366
  return [
@@ -2580,8 +3381,49 @@ function checkSync(workspaceRoot) {
2580
3381
  remedy: "run `npx nx sync` and commit the result"
2581
3382
  };
2582
3383
  }
3384
+ function checkNoRootRuntimeDependencies(workspaceRoot) {
3385
+ const manifestPath = (0, import_node_path14.join)(workspaceRoot, "package.json");
3386
+ if (!fileExists(manifestPath)) {
3387
+ return void 0;
3388
+ }
3389
+ let declared;
3390
+ try {
3391
+ declared = Object.keys(
3392
+ readJson(manifestPath).dependencies ?? {}
3393
+ );
3394
+ } catch {
3395
+ return void 0;
3396
+ }
3397
+ return {
3398
+ check: "no runtime dependencies in the root manifest",
3399
+ ok: declared.length === 0,
3400
+ detail: `the root package.json declares ${declared.join(", ")} \u2014 the root is private and never published, and @nx/rollup externalises only what a project's own manifest declares, so a package importing one of these ships an inlined private copy instead`,
3401
+ remedy: `move ${declared.join(", ")} into the dependencies of each package that imports it (devDependencies at the root are fine \u2014 that is what the root is for)`
3402
+ };
3403
+ }
3404
+ function checkRollupSourceMaps(workspaceRoot) {
3405
+ const configs = (0, import_node_fs8.globSync)(["packages/*/rollup.config.cjs", "libs/*/rollup.config.cjs"], {
3406
+ cwd: workspaceRoot
3407
+ }).map((config) => toPosix2(config));
3408
+ return configs.flatMap((relativePath) => {
3409
+ let config;
3410
+ try {
3411
+ config = (0, import_node_fs8.readFileSync)((0, import_node_path14.join)(workspaceRoot, relativePath), "utf8");
3412
+ } catch {
3413
+ return [];
3414
+ }
3415
+ return [
3416
+ {
3417
+ check: `source maps enabled in ${relativePath}`,
3418
+ ok: hasRollupSourceMaps(config),
3419
+ detail: "rollup emits no .js.map without it, so a breakpoint in a .ts file can never bind",
3420
+ remedy: "run `mnci upgrade`, which adds it to every rollup config"
3421
+ }
3422
+ ];
3423
+ });
3424
+ }
2583
3425
  function collectFindings(workspaceRoot) {
2584
- const nxJsonPath = (0, import_node_path12.join)(workspaceRoot, "nx.json");
3426
+ const nxJsonPath = (0, import_node_path14.join)(workspaceRoot, "nx.json");
2585
3427
  if (!fileExists(nxJsonPath)) {
2586
3428
  throw new Error(
2587
3429
  `No nx.json found in ${workspaceRoot} \u2014 run 'mnci doctor' from the workspace root.`
@@ -2594,6 +3436,8 @@ function collectFindings(workspaceRoot) {
2594
3436
  checkEslintPlugin(nxJson),
2595
3437
  checkResolvedEslint(workspaceRoot),
2596
3438
  checkNpmrc(workspaceRoot, nxJson.mnci?.registry, nxJson.mnci?.scope),
3439
+ checkNoRootRuntimeDependencies(workspaceRoot),
3440
+ ...checkRollupSourceMaps(workspaceRoot),
2597
3441
  ...checkVersionActions(workspaceRoot),
2598
3442
  ...checkTargetFilesExist(workspaceRoot),
2599
3443
  checkSync(workspaceRoot)
@@ -2621,12 +3465,12 @@ function runDoctor(workspaceRoot) {
2621
3465
  }
2622
3466
 
2623
3467
  // src/commands/interactive.ts
2624
- var import_node_path15 = require("path");
3468
+ var import_node_path17 = require("path");
2625
3469
  var import_prompts8 = require("@inquirer/prompts");
2626
3470
 
2627
3471
  // src/commands/new.ts
2628
- var import_node_fs8 = require("fs");
2629
- var import_node_path13 = require("path");
3472
+ var import_node_fs9 = require("fs");
3473
+ var import_node_path15 = require("path");
2630
3474
  function nxCloudProviderValue(ci) {
2631
3475
  return ci === "azure" ? "azure" : "github";
2632
3476
  }
@@ -2687,7 +3531,7 @@ async function runNew(name, options) {
2687
3531
  ],
2688
3532
  process.cwd()
2689
3533
  );
2690
- const workspaceRoot = (0, import_node_path13.join)(process.cwd(), workspaceName);
3534
+ const workspaceRoot = (0, import_node_path15.join)(process.cwd(), workspaceName);
2691
3535
  logger.step(
2692
3536
  "Applying MoNecromanCI overlay (VS Code workspace, release config, .npmrc, commitlint, pipeline, stack)"
2693
3537
  );
@@ -2696,8 +3540,8 @@ async function runNew(name, options) {
2696
3540
  { workspaceName, scope, registry, agent, variableGroup, ci, stack },
2697
3541
  logger.detail
2698
3542
  );
2699
- (0, import_node_fs8.rmSync)((0, import_node_path13.join)(workspaceRoot, "node_modules"), { recursive: true, force: true });
2700
- (0, import_node_fs8.rmSync)((0, import_node_path13.join)(workspaceRoot, "package-lock.json"), { force: true });
3543
+ (0, import_node_fs9.rmSync)((0, import_node_path15.join)(workspaceRoot, "node_modules"), { recursive: true, force: true });
3544
+ (0, import_node_fs9.rmSync)((0, import_node_path15.join)(workspaceRoot, "package-lock.json"), { force: true });
2701
3545
  logger.step("Installing the toolchain (commitlint + husky + linting/formatting)");
2702
3546
  const installStatus = runShell(
2703
3547
  "npm",
@@ -2727,8 +3571,8 @@ async function runNew(name, options) {
2727
3571
  }
2728
3572
 
2729
3573
  // src/commands/upgrade.ts
2730
- var import_node_fs9 = require("fs");
2731
- var import_node_path14 = require("path");
3574
+ var import_node_fs10 = require("fs");
3575
+ var import_node_path16 = require("path");
2732
3576
  function resolveRegistry2(options, persisted) {
2733
3577
  if (options.registry === "azure-artifacts" || options.organization && options.artifactsFeed) {
2734
3578
  const persistedAzure = persisted?.kind === "azure-artifacts" ? persisted : void 0;
@@ -2757,7 +3601,7 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
2757
3601
  return persisted.workspaceName;
2758
3602
  }
2759
3603
  try {
2760
- const existing = (0, import_node_fs9.readdirSync)(workspaceRoot).find(
3604
+ const existing = (0, import_node_fs10.readdirSync)(workspaceRoot).find(
2761
3605
  (file) => file.endsWith(".code-workspace") && file !== "undefined.code-workspace"
2762
3606
  );
2763
3607
  if (existing) {
@@ -2765,7 +3609,7 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
2765
3609
  }
2766
3610
  } catch {
2767
3611
  }
2768
- return (0, import_node_path14.basename)(workspaceRoot);
3612
+ return (0, import_node_path16.basename)(workspaceRoot);
2769
3613
  }
2770
3614
  function resolveOverlayOptions(workspaceRoot, options, persisted) {
2771
3615
  const scope = options.scope ?? persisted.scope;
@@ -2803,7 +3647,7 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
2803
3647
  };
2804
3648
  }
2805
3649
  function runUpgrade(workspaceRoot, options) {
2806
- if (!fileExists((0, import_node_path14.join)(workspaceRoot, "nx.json"))) {
3650
+ if (!fileExists((0, import_node_path16.join)(workspaceRoot, "nx.json"))) {
2807
3651
  throw new Error(
2808
3652
  `No nx.json found in ${workspaceRoot} \u2014 this does not look like an Nx workspace. Run 'mnci upgrade' from the workspace root.`
2809
3653
  );
@@ -2812,6 +3656,13 @@ function runUpgrade(workspaceRoot, options) {
2812
3656
  const resolved = resolveOverlayOptions(workspaceRoot, options, persisted);
2813
3657
  logger.step("Re-applying the MoNecromanCI overlay");
2814
3658
  applyOverlay(workspaceRoot, resolved, logger.detail);
3659
+ const repaired = repairRollupSourceMaps(workspaceRoot);
3660
+ if (repaired.length > 0) {
3661
+ logger.step("Enabling source maps in rollup configs (breakpoints in .ts files)");
3662
+ for (const path of repaired) {
3663
+ logger.detail(`updated ${path}`);
3664
+ }
3665
+ }
2815
3666
  logger.step(
2816
3667
  "Formatting the workspace (eslint --fix) \u2014 the slowest step, minutes on a large workspace"
2817
3668
  );
@@ -2821,7 +3672,7 @@ function runUpgrade(workspaceRoot, options) {
2821
3672
 
2822
3673
  // src/commands/interactive.ts
2823
3674
  async function runInteractive() {
2824
- const inWorkspace = fileExists((0, import_node_path15.join)(process.cwd(), "nx.json"));
3675
+ const inWorkspace = fileExists((0, import_node_path17.join)(process.cwd(), "nx.json"));
2825
3676
  const choiceNew = { name: "Create a new monorepo", value: "new" };
2826
3677
  const choiceAdd = { name: "Add a project to this workspace", value: "add" };
2827
3678
  const choiceUpgrade = {
@@ -2841,27 +3692,345 @@ async function runInteractive() {
2841
3692
  }
2842
3693
  }
2843
3694
 
3695
+ // src/commands/up.ts
3696
+ var import_node_path18 = require("path");
3697
+ var import_prompts9 = require("@inquirer/prompts");
3698
+
3699
+ // src/deps/registry.ts
3700
+ var LOOKUP_CONCURRENCY = 8;
3701
+ async function latestNpmVersions(names, cwd) {
3702
+ const results = await pool(names, LOOKUP_CONCURRENCY, async (name) => {
3703
+ const result = await runCaptureAsync("npm", ["view", name, "version", "--json"], cwd);
3704
+ if (result.status !== 0) {
3705
+ return;
3706
+ }
3707
+ try {
3708
+ const parsed = JSON.parse(result.stdout);
3709
+ const version = Array.isArray(parsed) ? parsed.at(-1) : parsed;
3710
+ return typeof version === "string" ? [name, version] : void 0;
3711
+ } catch {
3712
+ return;
3713
+ }
3714
+ });
3715
+ return new Map(results.filter((entry) => entry !== void 0));
3716
+ }
3717
+ async function latestPipVersions(names, cwd) {
3718
+ const python = process.platform === "win32" ? "python" : "python3";
3719
+ const results = await pool(names, LOOKUP_CONCURRENCY, async (name) => {
3720
+ const result = await runCaptureAsync(python, ["-m", "pip", "index", "versions", name], cwd);
3721
+ if (result.status !== 0) {
3722
+ return;
3723
+ }
3724
+ const match = /\(([^)]+)\)/.exec(result.stdout);
3725
+ return match ? [name, match[1]] : void 0;
3726
+ });
3727
+ return new Map(results.filter((entry) => entry !== void 0));
3728
+ }
3729
+ async function latestGoVersions(cwd) {
3730
+ const result = await runCaptureAsync("go", ["list", "-m", "-u", "-json", "all"], cwd);
3731
+ if (result.status !== 0) {
3732
+ return /* @__PURE__ */ new Map();
3733
+ }
3734
+ const latest = /* @__PURE__ */ new Map();
3735
+ for (const chunk of result.stdout.split(/^\}\s*$/m)) {
3736
+ const text = `${chunk.trim()}${chunk.trim() ? "}" : ""}`;
3737
+ if (!text.startsWith("{")) {
3738
+ continue;
3739
+ }
3740
+ try {
3741
+ const module_ = JSON.parse(text);
3742
+ if (module_.Path && module_.Update?.Version) {
3743
+ latest.set(module_.Path, module_.Update.Version);
3744
+ }
3745
+ } catch {
3746
+ }
3747
+ }
3748
+ return latest;
3749
+ }
3750
+ async function latestPubVersions(cwd) {
3751
+ const result = await runCaptureAsync("flutter", ["pub", "outdated", "--json"], cwd);
3752
+ if (result.status !== 0) {
3753
+ return /* @__PURE__ */ new Map();
3754
+ }
3755
+ try {
3756
+ const report = JSON.parse(result.stdout);
3757
+ const latest = /* @__PURE__ */ new Map();
3758
+ const packages = report.packages ?? [];
3759
+ for (const entry of packages) {
3760
+ if (entry.package && entry.latest?.version) {
3761
+ latest.set(entry.package, entry.latest.version);
3762
+ }
3763
+ }
3764
+ return latest;
3765
+ } catch {
3766
+ return /* @__PURE__ */ new Map();
3767
+ }
3768
+ }
3769
+ async function latestVersions(ecosystem, names, cwd) {
3770
+ switch (ecosystem) {
3771
+ case "npm": {
3772
+ return await latestNpmVersions(names, cwd);
3773
+ }
3774
+ case "pip": {
3775
+ return await latestPipVersions(names, cwd);
3776
+ }
3777
+ case "go": {
3778
+ return await latestGoVersions(cwd);
3779
+ }
3780
+ default: {
3781
+ return await latestPubVersions(cwd);
3782
+ }
3783
+ }
3784
+ }
3785
+
3786
+ // src/commands/up.ts
3787
+ function currentVersion(workspaceRoot, sites) {
3788
+ const installed = resolvedVersion(workspaceRoot, sites[0].ecosystem, sites[0].name);
3789
+ if (installed) {
3790
+ return installed;
3791
+ }
3792
+ if (sites[0].ecosystem === "go") {
3793
+ return bare(sites[0].spec);
3794
+ }
3795
+ const highest = highestSpec(sites.map((site) => site.spec));
3796
+ return highest ? specVersion(highest) : void 0;
3797
+ }
3798
+ async function collectOutdated(workspaceRoot, ecosystems) {
3799
+ const present = ecosystems.filter((ecosystem) => hasEcosystem(workspaceRoot, ecosystem));
3800
+ const inventory = collectInventory(workspaceRoot, present);
3801
+ const outdated = [];
3802
+ for (const ecosystem of present) {
3803
+ const forEcosystem = [...inventory].filter(
3804
+ ([name, sites]) => offerable(workspaceRoot, ecosystem, name, sites)
3805
+ );
3806
+ const latest = await latestVersions(
3807
+ ecosystem,
3808
+ forEcosystem.map(([name]) => name),
3809
+ workspaceRoot
3810
+ );
3811
+ outdated.push(...upgradable(workspaceRoot, ecosystem, forEcosystem, latest));
3812
+ }
3813
+ return outdated.toSorted(
3814
+ (left, right) => UPDATE_KINDS.indexOf(left.kind) - UPDATE_KINDS.indexOf(right.kind) || left.name.localeCompare(right.name)
3815
+ );
3816
+ }
3817
+ function offerable(workspaceRoot, ecosystem, name, sites) {
3818
+ if (sites[0].ecosystem !== ecosystem || sites[0].section === "indirect") {
3819
+ return false;
3820
+ }
3821
+ if (sites.every((site) => site.section === PEER_SECTION)) {
3822
+ return false;
3823
+ }
3824
+ if (ecosystem !== "npm") {
3825
+ return true;
3826
+ }
3827
+ return !isWorkspaceProject(workspaceRoot, name) && !isAliasedInstall(workspaceRoot, name);
3828
+ }
3829
+ function bare(version) {
3830
+ return version.replace(/^v/, "");
3831
+ }
3832
+ function upgradable(workspaceRoot, ecosystem, declared, latest) {
3833
+ const outdated = [];
3834
+ for (const [name, sites] of declared) {
3835
+ const newest = latest.get(name);
3836
+ const current = currentVersion(workspaceRoot, sites);
3837
+ if (!newest || !current || !isNewer(bare(newest), bare(current))) {
3838
+ continue;
3839
+ }
3840
+ outdated.push({
3841
+ name,
3842
+ ecosystem,
3843
+ current,
3844
+ latest: newest,
3845
+ kind: classify(bare(current), bare(newest)),
3846
+ sites
3847
+ });
3848
+ }
3849
+ return outdated;
3850
+ }
3851
+ function row(entry, widths) {
3852
+ const sections = [...new Set(entry.sites.map((site) => site.section))].join("/");
3853
+ const projects = [...new Set(entry.sites.map((site) => site.project))].join(", ");
3854
+ return [
3855
+ entry.name.padEnd(widths.name),
3856
+ sections.padEnd(widths.section),
3857
+ `${entry.current.padStart(widths.current)} \u203A ${entry.latest}`,
3858
+ projects
3859
+ ].join(" ");
3860
+ }
3861
+ function columnWidths(entries) {
3862
+ return {
3863
+ name: widthOf(entries.map((entry) => entry.name)),
3864
+ section: widthOf(
3865
+ entries.map((entry) => [...new Set(entry.sites.map((site) => site.section))].join("/"))
3866
+ ),
3867
+ current: widthOf(entries.map((entry) => entry.current))
3868
+ };
3869
+ }
3870
+ function widthOf(values) {
3871
+ return Math.max(0, ...values.map((value) => value.length));
3872
+ }
3873
+ function reportOutdated(outdated) {
3874
+ const widths = columnWidths(outdated);
3875
+ for (const kind of UPDATE_KINDS) {
3876
+ const section = outdated.filter((entry) => entry.kind === kind);
3877
+ if (section.length === 0) {
3878
+ continue;
3879
+ }
3880
+ const { title, blurb } = UPDATE_KIND_LABELS[kind];
3881
+ logger.info("");
3882
+ logger.info(`${title} ${blurb}`);
3883
+ for (const entry of section) {
3884
+ logger.info(` ${row(entry, widths)}`);
3885
+ }
3886
+ }
3887
+ }
3888
+ async function promptForUpdates(outdated) {
3889
+ const widths = columnWidths(outdated);
3890
+ const choices = [];
3891
+ for (const kind of UPDATE_KINDS) {
3892
+ const section = outdated.filter((entry) => entry.kind === kind);
3893
+ if (section.length === 0) {
3894
+ continue;
3895
+ }
3896
+ const { title, blurb } = UPDATE_KIND_LABELS[kind];
3897
+ choices.push(new import_prompts9.Separator(`
3898
+ ${title} ${blurb}`));
3899
+ for (const entry of section) {
3900
+ choices.push({ name: row(entry, widths), value: entry });
3901
+ }
3902
+ }
3903
+ return await (0, import_prompts9.checkbox)({
3904
+ message: "Choose which packages to update.",
3905
+ choices,
3906
+ pageSize: 20
3907
+ });
3908
+ }
3909
+ function applyUpdate(entry) {
3910
+ const changed = [];
3911
+ for (const site of entry.sites) {
3912
+ if (!site.rewritable) {
3913
+ continue;
3914
+ }
3915
+ const spec = `${rangeOperator(site.spec)}${entry.latest}`;
3916
+ if (rewriteSpec(site, spec)) {
3917
+ changed.push(site.manifestPath);
3918
+ }
3919
+ }
3920
+ return changed;
3921
+ }
3922
+ function reinstall(workspaceRoot, ecosystems) {
3923
+ const commands = [
3924
+ ["npm", "npm", ["install"]],
3925
+ ["pip", "npm", ["run", "python:install"]],
3926
+ ["pub", "flutter", ["pub", "get"]],
3927
+ ["go", "go", ["mod", "tidy"]]
3928
+ ];
3929
+ for (const [ecosystem, command, arguments_] of commands) {
3930
+ if (!ecosystems.has(ecosystem)) {
3931
+ continue;
3932
+ }
3933
+ if (ecosystem === "pip" && !hasPythonInstallScript(workspaceRoot)) {
3934
+ logger.warn(
3935
+ "This workspace has no 'python:install' script \u2014 run 'mnci upgrade' to add it, then reinstall by hand."
3936
+ );
3937
+ continue;
3938
+ }
3939
+ logger.step(`Reinstalling ${ecosystem} dependencies (${command} ${arguments_.join(" ")})`);
3940
+ if (runShell(command, arguments_, workspaceRoot) !== 0) {
3941
+ logger.warn(
3942
+ `${command} ${arguments_.join(" ")} failed \u2014 the manifests were updated, so re-run it once the cause is fixed.`
3943
+ );
3944
+ }
3945
+ }
3946
+ }
3947
+ function hasPythonInstallScript(workspaceRoot) {
3948
+ try {
3949
+ const manifest = readJson(
3950
+ (0, import_node_path18.join)(workspaceRoot, "package.json")
3951
+ );
3952
+ return manifest.scripts?.["python:install"] !== void 0;
3953
+ } catch {
3954
+ return false;
3955
+ }
3956
+ }
3957
+ function applyGoUpdates(workspaceRoot, entries) {
3958
+ for (const entry of entries) {
3959
+ logger.step(`go get ${entry.name}@${entry.latest}`);
3960
+ if (runShell("go", ["get", `${entry.name}@${entry.latest}`], workspaceRoot) !== 0) {
3961
+ logger.warn(`go get ${entry.name}@${entry.latest} failed \u2014 left at ${entry.current}.`);
3962
+ }
3963
+ }
3964
+ }
3965
+ async function runUp(workspaceRoot, options) {
3966
+ if (!fileExists((0, import_node_path18.join)(workspaceRoot, "nx.json"))) {
3967
+ throw new Error(`No nx.json found in ${workspaceRoot} \u2014 run 'mnci up' from the workspace root.`);
3968
+ }
3969
+ const ecosystems = resolveEcosystems(options.ecosystem);
3970
+ for (const ecosystem of ecosystems) {
3971
+ if (!hasEcosystem(workspaceRoot, ecosystem)) {
3972
+ logger.info(`\u2298 SKIPPED ${ecosystem} \u2014 no ${ecosystem} project in this workspace.`);
3973
+ }
3974
+ }
3975
+ logger.step("Querying registries for the latest published versions");
3976
+ const outdated = await collectOutdated(workspaceRoot, ecosystems);
3977
+ if (outdated.length === 0) {
3978
+ logger.success("Every dependency is on its latest published version.");
3979
+ return;
3980
+ }
3981
+ const readOnly = options.check ?? !process.stdout.isTTY;
3982
+ if (readOnly) {
3983
+ reportOutdated(outdated);
3984
+ logger.info("");
3985
+ logger.info(`${outdated.length} package(s) have a newer release. Run 'mnci up' to update them.`);
3986
+ return;
3987
+ }
3988
+ const selected = options.yes ? [...outdated] : await promptForUpdates(outdated);
3989
+ if (selected.length === 0) {
3990
+ logger.info("Nothing selected; no manifest was changed.");
3991
+ return;
3992
+ }
3993
+ const goSelections = selected.filter((entry) => entry.ecosystem === "go");
3994
+ const changed = selected.filter((entry) => entry.ecosystem !== "go").flatMap((entry) => applyUpdate(entry));
3995
+ const changedManifests = new Set(changed);
3996
+ for (const path of changedManifests) {
3997
+ logger.detail(`updated ${path}`);
3998
+ }
3999
+ applyGoUpdates(workspaceRoot, goSelections);
4000
+ if (options.install !== false) {
4001
+ reinstall(workspaceRoot, new Set(selected.map((entry) => entry.ecosystem)));
4002
+ }
4003
+ if (changed.length > 0) {
4004
+ syncProjectReferences(workspaceRoot);
4005
+ logger.step("Formatting the changed manifests (eslint --fix)");
4006
+ runFormatter(workspaceRoot);
4007
+ }
4008
+ logger.success(
4009
+ `Updated ${selected.length} package(s). Review the changes with \`git diff\` before committing.`
4010
+ );
4011
+ }
4012
+
2844
4013
  // src/util/versionChecker.ts
2845
- var import_node_fs10 = require("fs");
2846
- var import_node_path16 = require("path");
4014
+ var import_node_fs11 = require("fs");
4015
+ var import_node_path19 = require("path");
2847
4016
  var import_cross_spawn2 = __toESM(require("cross-spawn"));
2848
4017
  var PACKAGE_NAME = "@mnci/cli";
2849
4018
  var REGISTRY_TIMEOUT_MS = 2e3;
2850
- function checkForUpdate(currentVersion) {
4019
+ function checkForUpdate(currentVersion2) {
2851
4020
  try {
2852
4021
  if (!process.stdout.isTTY) {
2853
4022
  return;
2854
4023
  }
2855
4024
  setImmediate(() => {
2856
4025
  try {
2857
- reportIfOutdated(currentVersion);
4026
+ reportIfOutdated(currentVersion2);
2858
4027
  } catch {
2859
4028
  }
2860
4029
  });
2861
4030
  } catch {
2862
4031
  }
2863
4032
  }
2864
- function reportIfOutdated(currentVersion) {
4033
+ function reportIfOutdated(currentVersion2) {
2865
4034
  const result = import_cross_spawn2.default.sync("npm", ["view", PACKAGE_NAME, "version"], {
2866
4035
  encoding: "utf8",
2867
4036
  timeout: REGISTRY_TIMEOUT_MS,
@@ -2871,12 +4040,12 @@ function reportIfOutdated(currentVersion) {
2871
4040
  return;
2872
4041
  }
2873
4042
  const latestVersion = (result.stdout ?? "").trim();
2874
- if (!latestVersion || !isNewerVersion(latestVersion, currentVersion)) {
4043
+ if (!latestVersion || !isNewerVersion(latestVersion, currentVersion2)) {
2875
4044
  return;
2876
4045
  }
2877
4046
  logger.info("");
2878
4047
  logger.info(`A new version of ${PACKAGE_NAME} is available: ${latestVersion}`);
2879
- logger.info(` Current version: ${currentVersion}`);
4048
+ logger.info(` Current version: ${currentVersion2}`);
2880
4049
  logger.info(` Update with: npm install -g ${PACKAGE_NAME}@latest`);
2881
4050
  logger.info("");
2882
4051
  }
@@ -2899,8 +4068,8 @@ function releaseTriple(version) {
2899
4068
  }
2900
4069
  function readCliVersion(packageDirectory) {
2901
4070
  try {
2902
- const manifestPath = (0, import_node_path16.join)(packageDirectory, "..", "package.json");
2903
- const manifest = JSON.parse((0, import_node_fs10.readFileSync)(manifestPath, "utf8"));
4071
+ const manifestPath = (0, import_node_path19.join)(packageDirectory, "..", "package.json");
4072
+ const manifest = JSON.parse((0, import_node_fs11.readFileSync)(manifestPath, "utf8"));
2904
4073
  return manifest.version ?? "0.0.0";
2905
4074
  } catch {
2906
4075
  return "0.0.0";
@@ -2930,6 +4099,16 @@ function buildProgram(cliVersion) {
2930
4099
  ).action(() => {
2931
4100
  runDoctor(process.cwd());
2932
4101
  });
4102
+ program.command("sync").description(
4103
+ "Make every project agree: converge external dependency ranges declared at more than one version, then run 'nx sync' for TypeScript project references. Go is a no-op \u2014 one root go.mod means one version of every module"
4104
+ ).option("--check", "report drift and exit non-zero without writing anything").option("--ecosystem <name>", "restrict to one ecosystem: npm | pip | pub | go").action((options) => {
4105
+ runSync(process.cwd(), options);
4106
+ });
4107
+ program.command("up").description(
4108
+ "Show every dependency with a newer published release \u2014 grouped patch/minor/major/non-semver, with the projects declaring each one \u2014 and interactively update the ones you pick"
4109
+ ).option("--check", "report only; never prompt and never write (the default when piped)").option("-y, --yes", "select every available update without prompting").option("--ecosystem <name>", "restrict to one ecosystem: npm | pip | pub | go").option("--no-install", "update the manifests but skip the reinstall step").action(async (options) => {
4110
+ await runUp(process.cwd(), options);
4111
+ });
2933
4112
  program.command("add").addArgument(new import_commander.Argument("[kind]", "project kind").choices(PROJECT_KINDS)).argument("[name]", "project name").description("Add a project by delegating to the matching Nx plugin generator").option("--scope <scope>", "npm scope for a publishable lib (defaults to @<workspace name>)").option(
2934
4113
  "--framework <framework>",
2935
4114
  "node-app only: express | fastify | koa | nest | none (default: none)"