@mnci/cli 1.6.0 → 1.7.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/README.md CHANGED
@@ -29,6 +29,7 @@ first-party (or established community) Nx equivalent:
29
29
  ```sh
30
30
  mnci new my-repo # create a monorepo (prompts scope + registry)
31
31
  mnci new my-repo --yes --registry npm --scope @my
32
+ mnci new my-repo --yes --registry npm --scope @my --nx-cloud # opt in to Nx Cloud
32
33
 
33
34
  cd my-repo
34
35
  mnci add react-app web # @nx/react (Vite + Jest)
@@ -43,6 +44,7 @@ mnci add python-app svc # app -> apps/ (wheel, zipped into the drop)
43
44
  mnci add python-function-app fn # Azure Functions (Python v2) -> apps/
44
45
  mnci add python-lib shared # publishable -> python-packages/ (twine upload)
45
46
  mnci add python-internal-lib core # private shared lib -> libs/
47
+ mnci add python-vendor shared --lib core # wire core's module into shared's built wheel
46
48
 
47
49
  mnci upgrade # re-apply the latest overlay (see below)
48
50
  mnci upgrade --agent windows-latest # ...with an explicit override
@@ -59,6 +61,7 @@ each a single cross-platform command:
59
61
  | `npm run affected` | `nx affected -t lint,test,build` (vs `main`) |
60
62
  | `npm run graph` | `nx graph` |
61
63
  | `npm run release:preview` | `nx release --dry-run` |
64
+ | `npm run python:install` | fixed Python toolchain (ruff/pytest/build/twine) + editable-install every Python project — the same two guards CI runs, for local dev |
62
65
  | `prepare` | `husky` (commit-msg lint hook) |
63
66
 
64
67
  ## What `new` actually does
@@ -340,6 +343,30 @@ only for `github`/`both` — an `azure`-only workspace gets no
340
343
  `.github/dependabot.yml`, matching every other GitHub-only file this CLI
341
344
  writes.
342
345
 
346
+ ### Nx Cloud (`--nx-cloud`, opt-in)
347
+
348
+ `mnci new` never connects to Nx Cloud unless asked — `--nx-cloud` (or
349
+ answering "yes" to the interactive prompt) opts in; the flagless/`--yes`
350
+ default stays fully disconnected, exactly as before this existed. When
351
+ opted in, `mnci` passes `create-nx-workspace` a **named** `--nxCloud`
352
+ provider value derived from the chosen `--ci` (`azure`→`azure`,
353
+ `github`/`both`→`github`) — never the bare `--nxCloud=yes`. Verified
354
+ empirically: bare `yes` prompts "Will you be using GitHub as your git
355
+ hosting provider?" even with `--no-interactive` set, and exits without
356
+ creating the workspace at all when stdin isn't a TTY — a real
357
+ `create-nx-workspace` inconsistency, not something `mnci` can configure
358
+ around. The named-provider value sidesteps it and completes non-interactively
359
+ every time. The only visible effect of *which* named value is chosen is a
360
+ throwaway CI workflow file `create-nx-workspace` writes as a side effect of
361
+ Cloud setup — this CLI's own overlay unconditionally overwrites whatever
362
+ lands at that path immediately after, so the pipeline you actually get is
363
+ always the same one described above, Nx Cloud or not.
364
+
365
+ Connecting still requires finishing setup in a browser: `create-nx-workspace`
366
+ prints a `https://cloud.nx.app/connect/…` URL to complete linking the
367
+ workspace to an Nx Cloud account (remote caching, CI insights, `nx
368
+ fix-ci`) — `mnci` does not automate that step.
369
+
343
370
  ## Dependency & risk notes
344
371
 
345
372
  Being upfront about what mnci leans on, so it's a conscious trade-off rather
@@ -395,9 +422,10 @@ than a surprise:
395
422
  Python project workspace-wide (see "Workspace-wide install" above),
396
423
  straight into whatever `<python>` resolves to on the agent (`python3` on
397
424
  POSIX, `python` on Windows — see above); locally, create your own venv
398
- (`python3 -m venv` / `python -m venv` on Windows) and reproduce the same
399
- two installs by hand (`pip install -r requirements-dev.txt`, then
400
- `pip install -e <dir>` for each Python project).
425
+ (`python3 -m venv` / `python -m venv` on Windows) and run
426
+ `npm run python:install` to reproduce the same two installs — the root
427
+ script chains the identical two guards CI runs (see the scripts table
428
+ above).
401
429
 
402
430
  ## How Node apps work (plain `@nx/node:application`, no Azure Functions plugin)
403
431
 
@@ -534,18 +562,21 @@ identically to a Linux/macOS one.
534
562
  <name>` just like the TS apps.
535
563
  - **Internal-lib vendoring** replaces `@nxlv/python`'s `bundleLocalDependencies`:
536
564
  plain pip has no bundled-local-dependency feature, so a project that imports
537
- a workspace-internal Python library needs a hand-added `vendor` entry (under
565
+ a workspace-internal Python library needs a `vendor` entry (under
538
566
  `[tool.mnci-python-pip]`) in its own `pyproject.toml` (the pip-world
539
- counterpart of hand-wiring a `dependencies = [...]` entry — neither mnci
540
- nor the plugin wires cross-project Python dependencies automatically). The
541
- plugin's `build` executor reads that entry, resolves the named project's
542
- root via the **Nx project graph**, copies its module into a staged copy of
543
- the consuming project, and builds from there — so the resulting wheel
544
- contains the vendored module as a real top-level package. Verified
545
- empirically that this does **not** reproduce the old `@nxlv/python` bug
546
- where combining a vendored internal lib and a real external dependency on
547
- the same project silently dropped the external one from the wheel's
548
- metadata — both survive correctly.
567
+ counterpart of a `dependencies = [...]` entry — neither mnci nor the plugin
568
+ wires cross-project Python dependencies automatically). `mnci add
569
+ python-vendor <consumer> --lib <name>` automates writing that entry —
570
+ idempotent (safe to run twice), and works on any consumer with a
571
+ `pyproject.toml` (app, publishable lib, or another internal lib), not just
572
+ apps. The plugin's `build` executor reads the entry, resolves the named
573
+ project's root via the **Nx project graph**, copies its module into a
574
+ staged copy of the consuming project, and builds from there — so the
575
+ resulting wheel contains the vendored module as a real top-level package.
576
+ Verified empirically that this does **not** reproduce the old
577
+ `@nxlv/python` bug where combining a vendored internal lib and a real
578
+ external dependency on the same project silently dropped the external one
579
+ from the wheel's metadata — both survive correctly.
549
580
  - **Workspace-wide install** (mnci's own CI step, not the plugin's) — pip has
550
581
  no npm-workspaces-style hoisting, so mnci writes one: a guarded step
551
582
  editable-installs every Python project (`apps/*`, `python-packages/*`,
package/dist/cli.js CHANGED
@@ -35,13 +35,13 @@ __export(cli_exports, {
35
35
  main: () => main
36
36
  });
37
37
  module.exports = __toCommonJS(cli_exports);
38
- var import_node_fs3 = require("fs");
38
+ var import_node_fs4 = require("fs");
39
39
  var import_node_path12 = require("path");
40
40
  var import_commander = require("commander");
41
41
 
42
42
  // src/commands/add.ts
43
43
  var import_node_path7 = require("path");
44
- var import_prompts3 = require("@inquirer/prompts");
44
+ var import_prompts4 = require("@inquirer/prompts");
45
45
 
46
46
  // src/nx.ts
47
47
  var import_cross_spawn = __toESM(require("cross-spawn"));
@@ -100,6 +100,12 @@ async function promptCi() {
100
100
  ]
101
101
  });
102
102
  }
103
+ async function promptNxCloud() {
104
+ return await (0, import_prompts.confirm)({
105
+ message: "Connect this workspace to Nx Cloud (remote caching + CI insights)?",
106
+ default: false
107
+ });
108
+ }
103
109
  async function promptStack() {
104
110
  const linter = await (0, import_prompts.select)({
105
111
  message: "Linter",
@@ -385,6 +391,7 @@ function markPublic(manifestPath) {
385
391
  }
386
392
 
387
393
  // src/commands/add/python.ts
394
+ var import_node_fs2 = require("fs");
388
395
  var import_node_path5 = require("path");
389
396
  function ensurePython(workspaceRoot) {
390
397
  const hasPython = runShell("python3", ["--version"], workspaceRoot) === 0 || runShell("python", ["--version"], workspaceRoot) === 0;
@@ -476,9 +483,57 @@ function addPythonInternalLib(workspaceRoot, name) {
476
483
  ensureRequirementsDev(workspaceRoot);
477
484
  runNx(["g", "@mnci/nx-python-pip:internal-library", name, `--directory=libs/${name}`, "--no-interactive"], workspaceRoot);
478
485
  }
486
+ var PYTHON_PROJECT_DIRS = ["apps", "python-packages", "libs"];
487
+ function findPythonProjectDirectory(workspaceRoot, name) {
488
+ return PYTHON_PROJECT_DIRS.find((directory) => fileExists((0, import_node_path5.join)(workspaceRoot, directory, name, "pyproject.toml")));
489
+ }
490
+ function parseVendorEntries(pyprojectToml) {
491
+ const match = /^\s*vendor\s*=\s*\[([^\]]*)\]/m.exec(pyprojectToml);
492
+ if (!match) {
493
+ return [];
494
+ }
495
+ return match[1].split(",").map((entry) => entry.trim().replaceAll(/^["']|["']$/g, "")).filter(Boolean);
496
+ }
497
+ function addVendorEntry(pyprojectToml, lib) {
498
+ const existing = parseVendorEntries(pyprojectToml);
499
+ if (existing.length > 0) {
500
+ const merged = [...existing, lib].map((name) => `"${name}"`).join(", ");
501
+ return pyprojectToml.replace(/^(\s*vendor\s*=\s*)\[([^\]]*)\]/m, (_match, prefix) => `${prefix}[${merged}]`);
502
+ }
503
+ const table = `[tool.mnci-python-pip]
504
+ vendor = ["${lib}"]
505
+
506
+ `;
507
+ return pyprojectToml.includes("[tool.pytest.ini_options]") ? pyprojectToml.replace("[tool.pytest.ini_options]", () => `${table}[tool.pytest.ini_options]`) : `${pyprojectToml}
508
+ ${table}`;
509
+ }
510
+ async function addPythonVendor(workspaceRoot, consumer, options, kindProvided) {
511
+ const lib = options.lib ?? (kindProvided ? void 0 : await promptText("Internal Python library to vendor (an existing libs/<name>)"));
512
+ if (!lib) {
513
+ throw new Error("`add python-vendor` needs the library to vendor: pass --lib <name> (an existing libs/<name> internal library).");
514
+ }
515
+ const consumerDirectory = findPythonProjectDirectory(workspaceRoot, consumer);
516
+ if (!consumerDirectory) {
517
+ throw new Error(`No Python project named '${consumer}' with a pyproject.toml found (checked apps/, python-packages/, libs/). A Python function app has no pyproject.toml \u2014 there is nothing to vendor into.`);
518
+ }
519
+ if (!fileExists((0, import_node_path5.join)(workspaceRoot, "libs", lib, "pyproject.toml"))) {
520
+ throw new Error(`No internal Python library named '${lib}' found at libs/${lib}/pyproject.toml. Only an internal library (added via 'mnci add python-internal-lib') can be vendored.`);
521
+ }
522
+ if (consumerDirectory === "libs" && consumer === lib) {
523
+ throw new Error(`'${consumer}' cannot vendor itself.`);
524
+ }
525
+ const pyprojectPath = (0, import_node_path5.join)(workspaceRoot, consumerDirectory, consumer, "pyproject.toml");
526
+ const pyprojectToml = (0, import_node_fs2.readFileSync)(pyprojectPath, "utf8");
527
+ if (parseVendorEntries(pyprojectToml).includes(lib)) {
528
+ logger.info(`'${consumer}' already vendors '${lib}' \u2014 nothing to do.`);
529
+ return;
530
+ }
531
+ writeFileEnsured(pyprojectPath, addVendorEntry(pyprojectToml, lib));
532
+ logger.success(`'${consumer}' now vendors '${lib}' \u2014 its wheel will include '${lib}''s module on the next build.`);
533
+ }
479
534
 
480
535
  // src/commands/add/reactApp.ts
481
- var import_node_fs2 = require("fs");
536
+ var import_node_fs3 = require("fs");
482
537
  var import_node_path6 = require("path");
483
538
  function ensurePlugin2(workspaceRoot, packageName) {
484
539
  if (hasPlugin(workspaceRoot, packageName)) {
@@ -521,7 +576,7 @@ function allowEnvFiles(workspaceRoot) {
521
576
  return;
522
577
  }
523
578
  const marker = "# MoNecromanCI: keep the committed per-environment Vite config (public VITE_ values)";
524
- const current = (0, import_node_fs2.readFileSync)(gitignorePath, "utf8");
579
+ const current = (0, import_node_fs3.readFileSync)(gitignorePath, "utf8");
525
580
  if (current.includes(marker)) {
526
581
  return;
527
582
  }
@@ -563,7 +618,8 @@ var PROJECT_KINDS = [
563
618
  "python-app",
564
619
  "python-function-app",
565
620
  "python-lib",
566
- "python-internal-lib"
621
+ "python-internal-lib",
622
+ "python-vendor"
567
623
  ];
568
624
  async function runAdd(kind, name, options) {
569
625
  const workspaceRoot = process.cwd();
@@ -572,7 +628,7 @@ async function runAdd(kind, name, options) {
572
628
  }
573
629
  const stack = readWorkspaceStack(workspaceRoot);
574
630
  const kindProvided = kind !== void 0;
575
- const resolvedKind = kind ?? await (0, import_prompts3.select)({
631
+ const resolvedKind = kind ?? await (0, import_prompts4.select)({
576
632
  message: "What kind of project?",
577
633
  choices: PROJECT_KINDS.map((value) => ({ name: value, value }))
578
634
  });
@@ -624,6 +680,11 @@ async function runAdd(kind, name, options) {
624
680
  addPythonInternalLib(workspaceRoot, resolvedName);
625
681
  break;
626
682
  }
683
+ case "python-vendor": {
684
+ await addPythonVendor(workspaceRoot, resolvedName, options, kindProvided);
685
+ syncProjectReferences(workspaceRoot);
686
+ return;
687
+ }
627
688
  default: {
628
689
  const exhaustive = resolvedKind;
629
690
  throw new Error(`Unknown project kind '${exhaustive}'. Expected one of: ${PROJECT_KINDS.join(", ")}.`);
@@ -653,7 +714,7 @@ function markPrivate(manifestPath) {
653
714
 
654
715
  // src/commands/interactive.ts
655
716
  var import_node_path10 = require("path");
656
- var import_prompts6 = require("@inquirer/prompts");
717
+ var import_prompts7 = require("@inquirer/prompts");
657
718
 
658
719
  // src/commands/new.ts
659
720
  var import_node_path9 = require("path");
@@ -730,15 +791,13 @@ var ROOT_SCRIPTS = {
730
791
  prepare: "husky"
731
792
  };
732
793
  function rootScripts(stack) {
733
- if (stack.linter === "oxlint") {
734
- return {
735
- ...ROOT_SCRIPTS,
736
- lint: "oxlint",
737
- format: "oxfmt -c oxfmt.config.mts .",
738
- "format:check": "oxfmt -c oxfmt.config.mts --check ."
739
- };
740
- }
741
- return { ...ROOT_SCRIPTS };
794
+ const scripts = stack.linter === "oxlint" ? {
795
+ ...ROOT_SCRIPTS,
796
+ lint: "oxlint",
797
+ format: "oxfmt -c oxfmt.config.mts .",
798
+ "format:check": "oxfmt -c oxfmt.config.mts --check ."
799
+ } : { ...ROOT_SCRIPTS };
800
+ return { ...scripts, "python:install": `${PYTHON_INSTALL_GUARD} && ${PYTHON_WORKSPACE_INSTALL_GUARD}` };
742
801
  }
743
802
  function generatorDefaults(stack) {
744
803
  const shared = { linter: stack.linter === "oxlint" ? "none" : "eslint", unitTestRunner: stack.testRunner };
@@ -1142,6 +1201,9 @@ function applyOverlay(workspaceRoot, options) {
1142
1201
  }
1143
1202
 
1144
1203
  // src/commands/new.ts
1204
+ function nxCloudProviderValue(ci) {
1205
+ return ci === "azure" ? "azure" : "github";
1206
+ }
1145
1207
  async function resolveStack(options) {
1146
1208
  const fromFlags = { linter: options.linter, testRunner: options.testRunner };
1147
1209
  const complete = Boolean(fromFlags.linter && fromFlags.testRunner);
@@ -1190,6 +1252,7 @@ async function runNew(name, options) {
1190
1252
  const agent = options.agent ?? (options.yes ? "ubuntu-latest" : await promptText("CI build agent/runner (vmImage, GitHub Actions runner label, or self-hosted pool name)", "ubuntu-latest"));
1191
1253
  const variableGroup = ci === "github" ? options.variableGroup ?? "Build" : options.variableGroup ?? (options.yes ? "Build" : await promptText("Azure DevOps variable group holding the npm PAT", "Build"));
1192
1254
  const stack = await resolveStack(options);
1255
+ const nxCloud = options.nxCloud ?? (options.yes ? false : await promptNxCloud());
1193
1256
  logger.step(`Creating Nx workspace '${workspaceName}' (preset: ts)`);
1194
1257
  runNpx([
1195
1258
  "--yes",
@@ -1197,7 +1260,7 @@ async function runNew(name, options) {
1197
1260
  workspaceName,
1198
1261
  "--preset=ts",
1199
1262
  "--pm=npm",
1200
- "--nxCloud=skip",
1263
+ nxCloud ? `--nxCloud=${nxCloudProviderValue(ci)}` : "--nxCloud=skip",
1201
1264
  "--no-interactive"
1202
1265
  ], process.cwd());
1203
1266
  const workspaceRoot = (0, import_node_path9.join)(process.cwd(), workspaceName);
@@ -1222,7 +1285,7 @@ async function runInteractive() {
1222
1285
  const inWorkspace = fileExists((0, import_node_path10.join)(process.cwd(), "nx.json"));
1223
1286
  const newChoice = { name: "Create a new monorepo", value: "new" };
1224
1287
  const addChoice = { name: "Add a project to this workspace", value: "add" };
1225
- const action = await (0, import_prompts6.select)({
1288
+ const action = await (0, import_prompts7.select)({
1226
1289
  message: "What would you like to do?",
1227
1290
  choices: inWorkspace ? [addChoice, newChoice] : [newChoice, addChoice]
1228
1291
  });
@@ -1290,7 +1353,7 @@ function runUpgrade(workspaceRoot, options) {
1290
1353
  // src/cli.ts
1291
1354
  function readVersion() {
1292
1355
  try {
1293
- const package_ = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path12.join)(__dirname, "..", "package.json"), "utf8"));
1356
+ const package_ = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path12.join)(__dirname, "..", "package.json"), "utf8"));
1294
1357
  return package_.version ?? "0.0.0";
1295
1358
  } catch {
1296
1359
  return "0.0.0";
@@ -1299,13 +1362,13 @@ function readVersion() {
1299
1362
  function buildProgram() {
1300
1363
  const program = new import_commander.Command();
1301
1364
  program.name("mnci").description("MoNecromanCI \u2014 a thin CLI over official Nx plugins").version(readVersion(), "-v, --version", "display the version");
1302
- program.command("new").argument("[name]", "workspace name").description("Create a new monorepo (Nx TS preset + release/pipeline overlay)").option("-y, --yes", "accept defaults for anything not passed as a flag").option("--scope <scope>", "npm scope for publishable packages (e.g. @demo)").option("--registry <kind>", "azure-artifacts | npm").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option("--agent <pool>", "CI build agent: a vmImage (e.g. ubuntu-latest) or self-hosted pool name").option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option("--ci <provider>", "CI provider: azure | github | both").option("--linter <linter>", "linter: eslint | oxlint").option("--test-runner <runner>", "unit-test runner: jest | vitest").action(async (name, options) => {
1365
+ program.command("new").argument("[name]", "workspace name").description("Create a new monorepo (Nx TS preset + release/pipeline overlay)").option("-y, --yes", "accept defaults for anything not passed as a flag").option("--scope <scope>", "npm scope for publishable packages (e.g. @demo)").option("--registry <kind>", "azure-artifacts | npm").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option("--agent <pool>", "CI build agent: a vmImage (e.g. ubuntu-latest) or self-hosted pool name").option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option("--ci <provider>", "CI provider: azure | github | both").option("--linter <linter>", "linter: eslint | oxlint").option("--test-runner <runner>", "unit-test runner: jest | vitest").option("--nx-cloud", "connect the workspace to Nx Cloud (remote caching + CI insights)").action(async (name, options) => {
1303
1366
  await runNew(name, options);
1304
1367
  });
1305
1368
  program.command("upgrade").description("Re-apply the latest MoNecromanCI overlay to this workspace (release config, pipeline, npmrc, commitlint, husky hook, curated scripts)").option("--scope <scope>", "npm scope for publishable packages (overrides the persisted value)").option("--registry <kind>", "azure-artifacts | npm (overrides the persisted value)").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option("--agent <pool>", "CI build agent (overrides the persisted value)").option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option("--ci <provider>", "CI provider: azure | github | both (overrides the persisted value)").option("--linter <linter>", "linter: eslint | oxlint (overrides the persisted value)").option("--test-runner <runner>", "unit-test runner: jest | vitest (overrides the persisted value)").action((options) => {
1306
1369
  runUpgrade(process.cwd(), options);
1307
1370
  });
1308
- 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("--framework <framework>", "node-app only: express | fastify | koa | nest | none (default: none)").action(async (kind, name, options) => {
1371
+ 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("--framework <framework>", "node-app only: express | fastify | koa | nest | none (default: none)").option("--lib <name>", "python-vendor only: the internal Python library (libs/<name>) to vendor into <name>").action(async (kind, name, options) => {
1309
1372
  await runAdd(kind, name, options);
1310
1373
  });
1311
1374
  program.action(async () => {