@mnci/cli 4.47.0 → 4.48.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
@@ -36,7 +36,9 @@ plays (`.use-case`, `.client`, `.repository`, `.algorithm`, `.validator`,
36
36
  src/
37
37
  main.ts the CLI transport — decodes argv, calls one use case
38
38
  workspace-overlay/ the config files mnci owns and rewrites
39
- workspace-creation/ mnci new, and the interactive wizard
39
+ workspace-creation/ mnci new
40
+ interactive-wizard/ bare mnci: every command and option, asked from the command's own description
41
+ repository-adoption/ mnci adopt — bringing an existing repository under mnci, step by step
40
42
  workspace-upgrade/ mnci upgrade
41
43
  workspace-diagnostics/ mnci doctor
42
44
  project-scaffolding/ mnci add — one use case per kind, plus post-generation repairs
@@ -505,6 +507,18 @@ from File`), and the curated root scripts.
505
507
  4. Installs the chosen **stack** (see below), `husky` + `@commitlint/*` for
506
508
  real, so versions resolve at generation time.
507
509
 
510
+ ## The interactive wizard (`mnci` with no arguments)
511
+
512
+ Bare `mnci` lists **every command** (inside a workspace the Projects and Dependencies sections come first, outside
513
+ one the Workspace section does), asks the command's arguments and then the options that apply, prints the command
514
+ line it is about to run, and runs it through the same program the flags go through. It reads the commands from the
515
+ program itself, so a command or option added to `main.ts` is offered without a wizard change. Two groupings are
516
+ kept by hand and checked by a spec: `add` offers the flags of the kind just chosen, and `adopt` asks which step
517
+ first (report, baseline tags, toolchain, project kinds, dependencies, overlay) and offers that step's own flags.
518
+ `wizard-coverage.integration.spec.ts` fails when a command is missing from the menu, an `adopt` flag belongs to no
519
+ step, an `add` flag is reachable for no kind, or the line the wizard builds for an option is not one the program
520
+ accepts.
521
+
508
522
  ## `mnci adopt`: reading an existing repository before changing it
509
523
 
510
524
  `mnci adopt` is the start of bringing a repository that already has history, tooling and published
package/dist/cli.js CHANGED
@@ -9657,9 +9657,203 @@ function runInfo(workspaceRoot, cliVersion, options) {
9657
9657
  logger.info(info.workspace ? `workspace: ${info.workspace.root}` : "not inside a workspace");
9658
9658
  }
9659
9659
 
9660
+ // src/interactive-wizard/interactive-wizard.use-case.ts
9661
+ var import_node_path46 = require("path");
9662
+
9663
+ // src/interactive-wizard/adopt-steps.config.ts
9664
+ var ADOPT_GLOBAL_OPTIONS = ["json"];
9665
+ var ADOPT_STEPS = [
9666
+ { id: "report", label: "Report", summary: "read the repository and say what adopting it involves (changes nothing)", options: [] },
9667
+ { id: "tags", label: "Baseline release tags", summary: "create the tag each renamed project needs so a release continues from its last version", switch: "tags", options: [] },
9668
+ { id: "toolchain", label: "Toolchain", summary: "retire old tooling, align Nx to one version, make the audit pass", switch: "toolchain", options: ["nx"] },
9669
+ { id: "kinds", label: "Project kinds", summary: "record each project's kind as a type tag", switch: "kinds", options: ["kind"] },
9670
+ { id: "dependencies", label: "Dependencies", summary: "move root runtime dependencies into the projects that import them", switch: "dependencies", options: [] },
9671
+ {
9672
+ id: "overlay",
9673
+ label: "Overlay and pipeline",
9674
+ summary: "apply the release config, .npmrc, commitlint and the CI pipeline",
9675
+ switch: "overlay",
9676
+ options: ["scope", "registry", "organization", "project", "artifacts-feed", "agent", "variable-group", "npm-auth", "ci", "test-runner"]
9677
+ }
9678
+ ];
9679
+
9680
+ // src/interactive-wizard/applicable-options.policy.ts
9681
+ function optionsOfAdd(command, kind) {
9682
+ const claimed = new Set(PROJECT_KIND_CATALOG.flatMap((entry) => entry.flags));
9683
+ const own = new Set(PROJECT_KIND_CATALOG.find((entry) => entry.kind === kind)?.flags);
9684
+ return command.options.filter((option) => !claimed.has(option.name) || own.has(option.name));
9685
+ }
9686
+ function applicableOptions(command, context = {}) {
9687
+ if (command.name === "add") {
9688
+ return optionsOfAdd(command, context.kind);
9689
+ }
9690
+ if (command.name === "adopt") {
9691
+ const step = context.adoptStep;
9692
+ const wanted = /* @__PURE__ */ new Set([...step?.options ?? [], ...ADOPT_GLOBAL_OPTIONS]);
9693
+ return command.options.filter((option) => wanted.has(option.name));
9694
+ }
9695
+ return [...command.options];
9696
+ }
9697
+
9698
+ // src/interactive-wizard/ask-command.use-case.ts
9699
+ var NOT_ASKED = /* @__PURE__ */ new Set(["help", "version"]);
9700
+ var SKIP = "";
9701
+ async function askArgument(argument, prompter) {
9702
+ const message = `${argument.name}${argument.required ? "" : " (optional)"}: ${argument.description}`;
9703
+ if (argument.choices !== void 0) {
9704
+ const choices = argument.choices.map((choice) => ({ name: choice, value: choice }));
9705
+ const value = await prompter.select(message, argument.required ? choices : [{ name: "(skip)", value: SKIP }, ...choices]);
9706
+ return value === SKIP ? [] : [value];
9707
+ }
9708
+ const typed = await prompter.input(message, argument.required);
9709
+ const answer = typed.trim();
9710
+ if (answer === "") {
9711
+ return [];
9712
+ }
9713
+ return argument.variadic ? answer.split(/\s+/) : [answer];
9714
+ }
9715
+ async function askOptionValue(option, prompter) {
9716
+ if (option.takesValue === "none") {
9717
+ return true;
9718
+ }
9719
+ const message = `${option.flags}: ${option.description}`;
9720
+ if (option.choices !== void 0) {
9721
+ return await prompter.select(message, option.choices.map((choice) => ({ name: choice, value: choice })));
9722
+ }
9723
+ if (option.repeatable) {
9724
+ const values = [];
9725
+ let typed2 = await prompter.input(`${message} (an empty answer ends the list)`, false);
9726
+ while (typed2.trim() !== "") {
9727
+ values.push(typed2.trim());
9728
+ typed2 = await prompter.input(`${option.flags} (another, or empty to finish)`, false);
9729
+ }
9730
+ return values;
9731
+ }
9732
+ const typed = await prompter.input(message, option.takesValue === "required");
9733
+ const answer = typed.trim();
9734
+ return answer === "" ? true : answer;
9735
+ }
9736
+ async function askAdoptStep(prompter) {
9737
+ const id = await prompter.select("Which adoption step?", ADOPT_STEPS.map((step) => ({ name: step.label, value: step.id, description: step.summary })));
9738
+ return ADOPT_STEPS.find((step) => step.id === id);
9739
+ }
9740
+ async function askCommand(command, prompter) {
9741
+ const answers = { arguments: [], options: {} };
9742
+ for (const argument of command.arguments) {
9743
+ answers.arguments.push(...await askArgument(argument, prompter));
9744
+ }
9745
+ let adoptStep;
9746
+ if (command.name === "adopt") {
9747
+ adoptStep = await askAdoptStep(prompter);
9748
+ if (adoptStep.switch !== void 0) {
9749
+ answers.options[adoptStep.switch] = true;
9750
+ }
9751
+ }
9752
+ const offered = applicableOptions(command, { kind: command.name === "add" ? answers.arguments[0] : void 0, adoptStep }).filter((option) => !NOT_ASKED.has(option.name) && answers.options[option.name] === void 0);
9753
+ if (offered.length === 0) {
9754
+ return answers;
9755
+ }
9756
+ const chosen = await prompter.checkbox("Options (space to select, enter to continue)", offered.map((option) => ({ name: option.flags, value: option.name, description: option.description })));
9757
+ for (const name of chosen) {
9758
+ const option = offered.find((candidate) => candidate.name === name);
9759
+ const value = await askOptionValue(option, prompter);
9760
+ if (!(Array.isArray(value) && value.length === 0)) {
9761
+ answers.options[name] = value;
9762
+ }
9763
+ }
9764
+ return answers;
9765
+ }
9766
+
9767
+ // src/interactive-wizard/build-argv.algorithm.ts
9768
+ function longFlag(option) {
9769
+ return `--${option.name}`;
9770
+ }
9771
+ function buildArgv(command, answers) {
9772
+ const argv = [command.name, ...answers.arguments];
9773
+ for (const [name, answer] of Object.entries(answers.options)) {
9774
+ const option = command.options.find((candidate) => candidate.name === name);
9775
+ if (option === void 0) {
9776
+ throw new Error(`mnci ${command.name} has no option --${name}`);
9777
+ }
9778
+ if (answer === false) {
9779
+ continue;
9780
+ }
9781
+ if (typeof answer === "boolean") {
9782
+ argv.push(longFlag(option));
9783
+ } else {
9784
+ const values = Array.isArray(answer) ? answer : [answer];
9785
+ for (const value of values) {
9786
+ argv.push(longFlag(option), value);
9787
+ }
9788
+ }
9789
+ }
9790
+ return argv;
9791
+ }
9792
+ function formatCommandLine(argv) {
9793
+ return ["mnci", ...argv.map((argument) => /\s/.test(argument) ? JSON.stringify(argument) : argument)].join(" ");
9794
+ }
9795
+
9796
+ // src/interactive-wizard/inquirer-prompter.client.ts
9797
+ var import_prompts4 = require("@inquirer/prompts");
9798
+ function toRows(choices) {
9799
+ const rows = [];
9800
+ let current;
9801
+ for (const choice of choices) {
9802
+ if (choice.group !== void 0 && choice.group !== current) {
9803
+ current = choice.group;
9804
+ rows.push(new import_prompts4.Separator(`-- ${choice.group} --`));
9805
+ }
9806
+ rows.push({ name: choice.name, value: choice.value, description: choice.description });
9807
+ }
9808
+ return rows;
9809
+ }
9810
+ function createInquirerPrompter() {
9811
+ return {
9812
+ select: async (message, choices) => await (0, import_prompts4.select)({ message, choices: toRows(choices), pageSize: 18 }),
9813
+ checkbox: async (message, choices) => await (0, import_prompts4.checkbox)({ message, choices: toRows(choices), pageSize: 18 }),
9814
+ input: async (message, required) => await (0, import_prompts4.input)({ message, validate: (value) => !required || value.trim().length > 0 || "A value is required" }),
9815
+ confirm: async (message, fallback) => await (0, import_prompts4.confirm)({ message, default: fallback })
9816
+ };
9817
+ }
9818
+
9819
+ // src/interactive-wizard/interactive-wizard.use-case.ts
9820
+ var GROUP_LABELS = {
9821
+ workspace: "Workspace",
9822
+ projects: "Projects",
9823
+ dependencies: "Dependencies",
9824
+ pipeline: "Pipeline",
9825
+ inspect: "Inspect"
9826
+ };
9827
+ var WIZARD_GROUP_OVERRIDES = { adopt: "workspace" };
9828
+ var ORDER_IN_WORKSPACE = ["projects", "dependencies", "workspace", "pipeline", "inspect"];
9829
+ var ORDER_OUTSIDE = ["workspace", "projects", "dependencies", "pipeline", "inspect"];
9830
+ var SUMMARY_LENGTH = 110;
9831
+ function summarise(description) {
9832
+ const first = description.split(/\.\s| — /, 1)[0];
9833
+ return first.length > SUMMARY_LENGTH ? `${first.slice(0, SUMMARY_LENGTH - 1)}\u2026` : first;
9834
+ }
9835
+ function wizardMenu(commands, inWorkspace) {
9836
+ const groupOf = (command) => WIZARD_GROUP_OVERRIDES[command.name] ?? command.group;
9837
+ const order = inWorkspace ? ORDER_IN_WORKSPACE : ORDER_OUTSIDE;
9838
+ return order.flatMap((group) => commands.filter((command) => groupOf(command) === group).map((command) => ({ name: `${command.name.padEnd(9)} ${summarise(command.description)}`, value: command.name, group: GROUP_LABELS[group] })));
9839
+ }
9840
+ async function runInteractive(program, prompter = createInquirerPrompter(), workingDirectory = process.cwd()) {
9841
+ const commands = describeCommands(program);
9842
+ const inWorkspace = fileExists((0, import_node_path46.join)(workingDirectory, "nx.json"));
9843
+ const name = await prompter.select("What would you like to do?", wizardMenu(commands, inWorkspace));
9844
+ const command = commands.find((candidate) => candidate.name === name);
9845
+ const argv = buildArgv(command, await askCommand(command, prompter));
9846
+ logger.info(`Running: ${formatCommandLine(argv)}`);
9847
+ if (await prompter.confirm("Run it?", true)) {
9848
+ await program.parseAsync(argv, { from: "user" });
9849
+ } else {
9850
+ logger.info("Nothing was run.");
9851
+ }
9852
+ }
9853
+
9660
9854
  // src/workspace-creation/adopt-directory.use-case.ts
9661
9855
  var import_node_fs38 = require("fs");
9662
- var import_node_path46 = require("path");
9856
+ var import_node_path47 = require("path");
9663
9857
  var NEVER_COPIED = /* @__PURE__ */ new Set([".git", "node_modules"]);
9664
9858
  var AGENT_AND_CI_ENTRIES = [
9665
9859
  "AGENTS.md",
@@ -9696,7 +9890,7 @@ function assertAdoptableDirectory(target) {
9696
9890
  if (!(0, import_node_fs38.statSync)(target).isDirectory()) {
9697
9891
  throw new Error(`--into '${target}' is not a directory.`);
9698
9892
  }
9699
- const blockers = PREFLIGHT_BLOCKERS.filter((entry) => (0, import_node_fs38.existsSync)((0, import_node_path46.join)(target, entry)));
9893
+ const blockers = PREFLIGHT_BLOCKERS.filter((entry) => (0, import_node_fs38.existsSync)((0, import_node_path47.join)(target, entry)));
9700
9894
  if (blockers.length > 0) {
9701
9895
  throw new Error(
9702
9896
  `--into '${target}' already holds ${blockers.join(", ")}, so it is a project already. Use \`mnci upgrade\` to re-apply the overlay to an existing workspace, or point --into at an empty checkout.`
@@ -9724,7 +9918,7 @@ ${additions.join("\n")}
9724
9918
  function adoptGeneratedWorkspace(generatedRoot, target) {
9725
9919
  const entries = (0, import_node_fs38.readdirSync)(generatedRoot).filter((entry) => !NEVER_COPIED.has(entry));
9726
9920
  const collisions = entries.filter(
9727
- (entry) => (0, import_node_fs38.existsSync)((0, import_node_path46.join)(target, entry)) && !EXISTING_WINS.has(entry) && entry !== ".gitignore"
9921
+ (entry) => (0, import_node_fs38.existsSync)((0, import_node_path47.join)(target, entry)) && !EXISTING_WINS.has(entry) && entry !== ".gitignore"
9728
9922
  );
9729
9923
  if (collisions.length > 0) {
9730
9924
  throw new Error(
@@ -9733,8 +9927,8 @@ function adoptGeneratedWorkspace(generatedRoot, target) {
9733
9927
  }
9734
9928
  const result = { copied: [], kept: [], mergedGitignore: false };
9735
9929
  for (const entry of entries) {
9736
- const from = (0, import_node_path46.join)(generatedRoot, entry);
9737
- const to = (0, import_node_path46.join)(target, entry);
9930
+ const from = (0, import_node_path47.join)(generatedRoot, entry);
9931
+ const to = (0, import_node_path47.join)(target, entry);
9738
9932
  if (entry === ".gitignore" && (0, import_node_fs38.existsSync)(to)) {
9739
9933
  result.mergedGitignore = mergeGitignore(from, to);
9740
9934
  result.kept.push(entry);
@@ -9754,7 +9948,7 @@ function adoptGeneratedWorkspace(generatedRoot, target) {
9754
9948
  // src/workspace-creation/create-workspace.use-case.ts
9755
9949
  var import_node_fs39 = require("fs");
9756
9950
  var import_node_os2 = require("os");
9757
- var import_node_path47 = require("path");
9951
+ var import_node_path48 = require("path");
9758
9952
  function nxCloudProviderValue(ci) {
9759
9953
  return ci === "azure" ? "azure" : "github";
9760
9954
  }
@@ -9797,9 +9991,9 @@ async function resolveRegistry2(options) {
9797
9991
  return await promptRegistry();
9798
9992
  }
9799
9993
  async function runNew(name, options) {
9800
- const adoptTarget = options.into === void 0 ? void 0 : (0, import_node_path47.resolve)(process.cwd(), options.into);
9994
+ const adoptTarget = options.into === void 0 ? void 0 : (0, import_node_path48.resolve)(process.cwd(), options.into);
9801
9995
  if (adoptTarget !== void 0) assertAdoptableDirectory(adoptTarget);
9802
- const workspaceName = name ?? (adoptTarget === void 0 ? await promptText("Workspace name") : (0, import_node_path47.basename)(adoptTarget));
9996
+ const workspaceName = name ?? (adoptTarget === void 0 ? await promptText("Workspace name") : (0, import_node_path48.basename)(adoptTarget));
9803
9997
  assertValidProjectName(workspaceName, "Workspace name");
9804
9998
  const scope = options.scope ?? (options.yes ? `@${workspaceName}` : await promptText("npm scope for publishable packages", `@${workspaceName}`));
9805
9999
  const registry = await resolveRegistry2(options);
@@ -9812,8 +10006,8 @@ async function runNew(name, options) {
9812
10006
  const npmAuth = resolveNpmAuth(options.npmAuth, registry, ci);
9813
10007
  const stack = await resolveStack(options);
9814
10008
  const nxCloud = options.nxCloud ?? (options.yes ? false : await promptNxCloud());
9815
- const stagingParent = adoptTarget === void 0 ? process.cwd() : (0, import_node_fs39.mkdtempSync)((0, import_node_path47.join)((0, import_node_os2.tmpdir)(), "mnci-new-"));
9816
- let workspaceRoot = (0, import_node_path47.join)(stagingParent, workspaceName);
10009
+ const stagingParent = adoptTarget === void 0 ? process.cwd() : (0, import_node_fs39.mkdtempSync)((0, import_node_path48.join)((0, import_node_os2.tmpdir)(), "mnci-new-"));
10010
+ let workspaceRoot = (0, import_node_path48.join)(stagingParent, workspaceName);
9817
10011
  try {
9818
10012
  logger.step(`Creating Nx workspace '${workspaceName}' (preset: ts)`);
9819
10013
  runNpx(
@@ -9846,8 +10040,8 @@ async function runNew(name, options) {
9846
10040
  { workspaceName, scope, registry, agent, variableGroup, ci, stack, ...npmAuth && { npmAuth } },
9847
10041
  logger.detail
9848
10042
  );
9849
- (0, import_node_fs39.rmSync)((0, import_node_path47.join)(workspaceRoot, "node_modules"), { recursive: true, force: true });
9850
- (0, import_node_fs39.rmSync)((0, import_node_path47.join)(workspaceRoot, "package-lock.json"), { force: true });
10043
+ (0, import_node_fs39.rmSync)((0, import_node_path48.join)(workspaceRoot, "node_modules"), { recursive: true, force: true });
10044
+ (0, import_node_fs39.rmSync)((0, import_node_path48.join)(workspaceRoot, "package-lock.json"), { force: true });
9851
10045
  logger.step("Installing the toolchain (commitlint + husky + linting/formatting)");
9852
10046
  const installStatus = runShell(
9853
10047
  "npm",
@@ -9884,30 +10078,6 @@ function projectKindHintLines(kinds = PROJECT_KINDS) {
9884
10078
  return lines;
9885
10079
  }
9886
10080
 
9887
- // src/workspace-creation/interactive-wizard.use-case.ts
9888
- var import_node_path48 = require("path");
9889
- var import_prompts4 = require("@inquirer/prompts");
9890
- async function runInteractive() {
9891
- const inWorkspace = fileExists((0, import_node_path48.join)(process.cwd(), "nx.json"));
9892
- const choiceNew = { name: "Create a new monorepo", value: "new" };
9893
- const choiceAdd = { name: "Add a project to this workspace", value: "add" };
9894
- const choiceUpgrade = {
9895
- name: "Upgrade this workspace (re-apply the latest overlay)",
9896
- value: "upgrade"
9897
- };
9898
- const action = await (0, import_prompts4.select)({
9899
- message: "What would you like to do?",
9900
- choices: inWorkspace ? [choiceAdd, choiceUpgrade, choiceNew] : [choiceNew, choiceAdd, choiceUpgrade]
9901
- });
9902
- if (action === "new") {
9903
- await runNew(void 0, {});
9904
- } else if (action === "upgrade") {
9905
- runUpgrade(process.cwd(), {});
9906
- } else {
9907
- await runAdd(void 0, void 0, {});
9908
- }
9909
- }
9910
-
9911
10081
  // src/main.ts
9912
10082
  function buildProgram(cliVersion) {
9913
10083
  const program = new import_commander.Command();
@@ -10009,7 +10179,7 @@ function buildProgram(cliVersion) {
10009
10179
  runInfo(process.cwd(), cliVersion, options);
10010
10180
  });
10011
10181
  program.action(async () => {
10012
- await runInteractive();
10182
+ await runInteractive(program);
10013
10183
  });
10014
10184
  return program;
10015
10185
  }