create-nx-archweave 0.2.1 → 0.3.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
@@ -2,10 +2,11 @@
2
2
 
3
3
  One command takes you from an empty directory to a configured Nx workspace with:
4
4
 
5
- - FSD Lite structure
5
+ - an FSD Lite lint rule for the frontend app's layers (`warn` or `error` by preset), plus slice generators
6
+ - frontend/backend boundaries as lint errors between the projects it tags, when you pick a backend
6
7
  - AI agent rules
7
8
  - GitHub templates
8
- - optional AI PR review (Claude, Gemini, GPT, Cursor)
9
+ - optional AI PR review with the provider you choose (Claude supported; Gemini, GPT and Cursor in beta)
9
10
 
10
11
  ```bash
11
12
  # npm
@@ -27,15 +28,12 @@ Anything you don't pass as a flag, the CLI asks you interactively. The `quick-st
27
28
 
28
29
  - Node >= 22.18
29
30
  - npm, pnpm >= 10, yarn (classic or berry) or bun
30
- - network access to github.com (Nx's react template is downloaded from there)
31
-
32
- **react-vite and older package managers.** You can't create react-vite with npm 10 or yarn 1. Both of those fail inside `create-nx-workspace`'s own install of Nx's react template. So the CLI never offers npm 10 or yarn 1 for react-vite. If a flag names one of them, the CLI stops before creating anything.
33
31
 
34
32
  ## Choosing a package manager
35
33
 
36
34
  The CLI asks for the package manager too:
37
35
 
38
- - It only offers package managers that are installed and can create the workspace.
36
+ - It only offers package managers that are installed.
39
37
  - It preselects the one that launched the CLI: `npx` → npm, `pnpm dlx` → pnpm, `yarn dlx` → yarn, `bunx` → bun.
40
38
  - Without a terminal, it uses the launching package manager, else npm.
41
39
 
@@ -47,29 +45,35 @@ A few things to know for specific package managers:
47
45
 
48
46
  ## What it runs
49
47
 
50
- The CLI prints its plan up front. Then it runs three steps with the package manager you chose.
48
+ The CLI prints its plan up front. Then it runs the steps below with the package manager you chose.
51
49
 
52
- - It never re-implements `create-nx-workspace`.
50
+ - It never re-implements `create-nx-workspace` or Nx's generators.
53
51
  - It never commits for you.
54
- - `--preset=next` replaces the template for `--frontend=next`.
55
- - pnpm adds a fourth step that records the build scripts pnpm skipped. bun adds one that lists the install scripts bun blocked (`bun pm untrusted`).
52
+ - `--frontend=next` creates the workspace with `--preset=next --appName=web --e2eTestRunner=none` instead, which already holds the app.
53
+ - No path creates an e2e project. See the [getting-started guide](https://github.com/clearlydev/nx-archweave/blob/main/docs/getting-started.md#adding-e2e-tests) to add one with Nx's own generator.
54
+ - pnpm adds a last step that records the build scripts pnpm skipped. bun adds one that lists the install scripts bun blocked (`bun pm untrusted`).
56
55
 
57
- Here are the three steps for npm and pnpm.
56
+ Here are the steps for npm and pnpm.
58
57
 
59
- **Step 1: create the workspace**
58
+ **Step 1: create the workspace, then the React app**
60
59
 
61
60
  ```bash
62
61
  # npm
63
- npx -y create-nx-workspace@23.3.0 <name> \
64
- --template=nrwl/react-template \
65
- --pm=npm --nxCloud=skip --interactive=false
62
+ npx -y create-nx-workspace@24.0.1 <name> \
63
+ --preset=empty --pm=npm --nxCloud=skip --interactive=false
64
+ npm exec nx -- add @nx/react
65
+ npm exec nx -- g @nx/react:app apps/web --bundler=vite --linter=eslint \
66
+ --style=css --unitTestRunner=vitest --e2eTestRunner=none --routing=false
66
67
 
67
68
  # pnpm
68
- pnpm dlx create-nx-workspace@23.3.0 <name> \
69
- --template=nrwl/react-template \
70
- --pm=pnpm --nxCloud=skip --interactive=false
69
+ pnpm dlx create-nx-workspace@24.0.1 <name> \
70
+ --preset=empty --pm=pnpm --nxCloud=skip --interactive=false
71
+ pnpm nx add @nx/react
72
+ pnpm nx g @nx/react:app apps/web … (same flags)
71
73
  ```
72
74
 
75
+ `--preset=empty` downloads nothing and keeps the workspace on that create-nx-workspace's Nx version. `nx add` installs `@nx/react` at the same version.
76
+
73
77
  **Step 2: install the plugin**
74
78
 
75
79
  ```bash
@@ -94,13 +98,13 @@ pnpm nx g @clearlydev/nx-archweave:init …
94
98
  With `--backend=express|nest`, the CLI inserts two official steps before init:
95
99
 
96
100
  1. `@nx/<backend>`, pinned to the workspace's exact nx version
97
- 2. `nx g @nx/<backend>:application apps/api --frontendProject=<detected app>`
101
+ 2. `nx g @nx/<backend>:application apps/api --frontendProject=<detected app> --unitTestRunner=vitest --e2eTestRunner=none` (vitest like the frontend; the generator would otherwise ask)
98
102
 
99
103
  init then wires `shared-types` and `scope:` tags, and writes the boundary constraints (`--applyEslint=true` on this path).
100
104
 
101
105
  When something goes wrong:
102
106
 
103
- - If a step fails, the CLI stops right away. It leaves the created directory in place (it never auto-deletes) and tells you how to recover.
107
+ - If a step fails, the CLI stops right away. It leaves the created directory in place (it never auto-deletes), tells you how to recover, and prints the failed step and every step after it as commands you can run yourself. It never re-runs them for you.
104
108
  - When stdin isn't a TTY and a required input is missing, the CLI fails fast instead of hanging.
105
109
 
106
110
  ## Options
package/dist/bin.js CHANGED
@@ -22,9 +22,9 @@ var AI_HOST_CHOICES = [
22
22
  ];
23
23
  var REVIEW_PROVIDER_CHOICES = [
24
24
  { id: "claude", label: "Claude" },
25
- { id: "gemini", label: "Gemini" },
26
- { id: "gpt", label: "GPT" },
27
- { id: "cursor", label: "Cursor" }
25
+ { id: "gemini", label: "Gemini (beta)" },
26
+ { id: "gpt", label: "GPT (beta)" },
27
+ { id: "cursor", label: "Cursor (beta)" }
28
28
  ];
29
29
  var REVIEW_PROVIDER_VALUES = REVIEW_PROVIDER_CHOICES.map((provider) => provider.id);
30
30
  var AI_HOST_VALUES = AI_HOST_CHOICES.map((host) => host.id);
@@ -46,12 +46,6 @@ var PLUGIN_CHOICES = [
46
46
  ];
47
47
  var PLUGIN_VALUES = PLUGIN_CHOICES.map((plugin) => plugin.id);
48
48
 
49
- // src/cli/preflight.ts
50
- function parseMajor(version) {
51
- const match = /^\s*v?(\d+)\./.exec(version ?? "");
52
- return match ? Number(match[1]) : void 0;
53
- }
54
-
55
49
  // src/cli/package-manager.ts
56
50
  var SUPPORTED = ["npm", "pnpm", "yarn", "bun"];
57
51
  function isCliPackageManager(name) {
@@ -71,24 +65,11 @@ function listInstalledManagers(launcher, probe) {
71
65
  }
72
66
  return installed;
73
67
  }
74
- function managerIncompatibility(manager, frontend) {
75
- const major = parseMajor(manager.version);
76
- if (frontend !== "react-vite" || major === void 0) return void 0;
77
- if (manager.name === "npm" && major < 11) return "react-vite needs npm 11 \u2014 npm 10 crashes in Nx's react template";
78
- if (manager.name === "yarn" && major < 2) return "react-vite needs yarn 2+ \u2014 yarn 1 cannot resolve the template's workspace:* packages";
79
- return void 0;
80
- }
81
68
  var NOTHING_CREATED = "Nothing was created.";
82
69
  async function choosePackageManager(input, ask, tell) {
83
70
  const launcher = parseUserAgent(input.userAgent);
84
71
  const resolveNamed = (name, source) => {
85
72
  const version = launcher?.name === name && launcher.version ? launcher.version : input.probe(name)?.trim() || void 0;
86
- const reason = managerIncompatibility({ name, version }, input.frontend);
87
- if (reason) {
88
- return {
89
- error: `create-nx-archweave stopped before creating anything: ${name} ${version} cannot create this workspace \u2014 ${reason}. ${INSTALL_ADVICE} ${NOTHING_CREATED}`
90
- };
91
- }
92
73
  return { packageManager: name, source, version };
93
74
  };
94
75
  if (input.flag) return resolveNamed(input.flag, "flag");
@@ -105,50 +86,30 @@ async function choosePackageManager(input, ask, tell) {
105
86
  tell(`${launcher.name} is not supported (${SUPPORTED_TEXT} are) \u2014 choosing from the supported ones instead.`);
106
87
  }
107
88
  const installed = listInstalledManagers(launcher, input.probe);
108
- const reasons = installed.map((manager) => managerIncompatibility(manager, input.frontend));
109
- const selectable = installed.filter((_, index) => reasons[index] === void 0);
110
- const unavailable = installed.filter((_, index) => reasons[index] !== void 0);
111
- const unavailableLines = unavailable.map(
112
- (manager) => ` ${manager.name.padEnd(6)}${manager.version ?? ""} \u2014 not available: ${reasons[installed.indexOf(manager)]}`
113
- );
114
- if (selectable.length === 0) {
115
- return {
116
- error: ["create-nx-archweave stopped before creating anything: no installed package manager can create this workspace.", ...unavailableLines, `${INSTALL_ADVICE} ${NOTHING_CREATED}`].join("\n")
117
- };
118
- }
119
- if (selectable.length === 1) {
120
- if (unavailable.length > 0) tell(["Package manager:", ...unavailableLines].join("\n"));
121
- return { packageManager: selectable[0].name, source: unavailable.length > 0 ? "only-compatible" : "only-installed", version: selectable[0].version };
122
- }
123
- const defaultIndex = Math.max(0, selectable.findIndex((manager) => manager.name === launcher?.name));
89
+ if (installed.length === 1) return { packageManager: installed[0].name, source: "only-installed", version: installed[0].version };
90
+ const defaultIndex = Math.max(0, installed.findIndex((manager) => manager.name === launcher?.name));
124
91
  tell("? Package manager");
125
- selectable.forEach((manager, index) => {
92
+ installed.forEach((manager, index) => {
126
93
  const head = ` ${index + 1}) ${manager.name.padEnd(6)}${manager.version ?? ""}`.trimEnd();
127
94
  tell(index === defaultIndex ? `${head} (default)` : head);
128
95
  });
129
- unavailableLines.forEach((line) => tell(line));
130
96
  for (; ; ) {
131
- const answer = (await ask(`Choose 1-${selectable.length} [${defaultIndex + 1}]: `)).trim().toLowerCase();
97
+ const answer = (await ask(`Choose 1-${installed.length} [${defaultIndex + 1}]: `)).trim().toLowerCase();
132
98
  const byNumber = /^\d+$/.test(answer) ? Number(answer) - 1 : -1;
133
- const index = answer === "" ? defaultIndex : byNumber >= 0 ? byNumber : selectable.findIndex((manager) => manager.name === answer);
134
- if (index >= 0 && index < selectable.length) {
135
- return { packageManager: selectable[index].name, source: "asked", version: selectable[index].version };
99
+ const index = answer === "" ? defaultIndex : byNumber >= 0 ? byNumber : installed.findIndex((manager) => manager.name === answer);
100
+ if (index >= 0 && index < installed.length) {
101
+ return { packageManager: installed[index].name, source: "asked", version: installed[index].version };
136
102
  }
137
- const blocked = unavailable.find((manager) => manager.name === answer);
138
- tell(
139
- blocked ? `${blocked.name} is not available here: ${reasons[installed.indexOf(blocked)]}.` : `Choose a number from 1 to ${selectable.length} (or a name: ${selectable.map((manager) => manager.name).join(", ")}).`
140
- );
103
+ tell(`Choose a number from 1 to ${installed.length} (or a name: ${installed.map((manager) => manager.name).join(", ")}).`);
141
104
  }
142
105
  }
143
106
  var SUPPORTED_TEXT = "npm, pnpm, yarn and bun";
144
- var INSTALL_ADVICE = "Use npm 11+, pnpm, yarn 2+ (Corepack: corepack enable) or bun, or pass --package-manager= with one of them.";
145
107
  var SOURCE_TEXT = {
146
108
  flag: "--package-manager",
147
109
  launcher: "the package manager that ran this command",
148
110
  default: "default \u2014 no launching package manager detected",
149
111
  asked: "chosen",
150
- "only-installed": "the only one installed",
151
- "only-compatible": "the only installed one that can create this workspace"
112
+ "only-installed": "the only one installed"
152
113
  };
153
114
  function describeResolution(resolution) {
154
115
  const name = resolution.version ? `${resolution.packageManager} ${resolution.version}` : resolution.packageManager;
@@ -509,9 +470,7 @@ function renderVersion() {
509
470
 
510
471
  // src/cli/plan.ts
511
472
  var DEFAULT_PLUGIN_SOURCE = `@clearlydev/nx-archweave@^${CLI_VERSION}`;
512
- var VALIDATED_CREATE_NX_WORKSPACE_VERSION = "23.3.0";
513
- var NPM_10_CREATE_WORKSPACE_HINT = `If the npm error above is "Cannot read properties of null (reading 'edgesOut')", that is a known npm 10 bug with the current Nx workspace dependencies \u2014 upgrade npm (npm install -g npm@11), or, if pnpm is installed, re-run with --package-manager=pnpm.`;
514
- var YARN_1_CREATE_WORKSPACE_HINT = `If the yarn error above is "Couldn't find package \u2026@workspace:*", that is yarn 1 (classic), which cannot resolve the workspace:* packages in Nx's react template \u2014 use yarn 2+ (corepack enable, then yarn set version stable) or re-run with another package manager.`;
473
+ var VALIDATED_CREATE_NX_WORKSPACE_VERSION = "24.0.1";
515
474
  function nxArgs(pm) {
516
475
  return pm === "npm" ? ["exec", "nx", "--"] : ["nx"];
517
476
  }
@@ -528,10 +487,11 @@ function isYarnBerry(ctx) {
528
487
  return ctx.readFile(".yarnrc.yml") !== void 0 || /^__metadata:/m.test(ctx.readFile("yarn.lock") ?? "");
529
488
  }
530
489
  var YARN_PLAN_ENV = { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" };
490
+ var NO_E2E = ["--e2eTestRunner=none"];
531
491
  function createWorkspaceSourceArgs(frontend, backend) {
532
- if (frontend === "react-vite") return ["--template=nrwl/react-template"];
533
- if (frontend === "next") return ["--preset=next"];
534
- return [`--preset=${backend}`, "--appName=api"];
492
+ if (frontend === "react-vite") return ["--preset=empty"];
493
+ if (frontend === "next") return ["--preset=next", "--appName=web", ...NO_E2E];
494
+ return [`--preset=${backend}`, "--appName=api", ...NO_E2E, "--unitTestRunner=vitest"];
535
495
  }
536
496
  function buildNextSteps(input) {
537
497
  const lines = [
@@ -556,7 +516,7 @@ function buildNextSteps(input) {
556
516
  function buildPlan(input) {
557
517
  const pluginSource = input.pluginSource ?? DEFAULT_PLUGIN_SOURCE;
558
518
  const workspaceSource = createWorkspaceSourceArgs(input.frontend, input.backend);
559
- const aiAgentArgs = input.aiHosts.map((host) => `--aiAgents=${AI_HOST_CHOICES.find((choice) => choice.id === host)?.nxAgent ?? host}`);
519
+ const aiAgentArgs = input.aiHosts.length === 0 ? ["--aiAgents=none"] : input.aiHosts.map((host) => `--aiAgents=${AI_HOST_CHOICES.find((choice) => choice.id === host)?.nxAgent ?? host}`);
560
520
  const pm = input.packageManager;
561
521
  const nxPrefix = nxArgs(pm);
562
522
  const initArgs = [
@@ -572,8 +532,10 @@ function buildPlan(input) {
572
532
  // Step 2 (the plugin install) always dirties the fresh workspace's tree
573
533
  // (package.json + lockfile), so init's clean-tree refusal would otherwise
574
534
  // always fire here. This is orchestration knowledge, not policy: the CLI
575
- // knows what its own previous step did.
576
- "--allowDirty=true"
535
+ // knows what its own previous step did. For the same reason the report's note about
536
+ // reverting earlier user changes doesn't apply: the only changes are that install's.
537
+ "--allowDirty=true",
538
+ "--dirtyFromCaller=true"
577
539
  ];
578
540
  if (input.review !== void 0) {
579
541
  initArgs.push(`--review=${input.review}`);
@@ -608,10 +570,9 @@ function buildPlan(input) {
608
570
  "--nxCloud=skip",
609
571
  "--interactive=false"
610
572
  ],
611
- cwd: null,
612
- ...input.frontend === "react-vite" && pm === "npm" ? { failureHint: NPM_10_CREATE_WORKSPACE_HINT } : {},
613
- ...input.frontend === "react-vite" && pm === "yarn" ? { failureHint: YARN_1_CREATE_WORKSPACE_HINT } : {}
573
+ cwd: null
614
574
  },
575
+ ...input.frontend === "react-vite" ? reactAppSteps(input.name, pm) : [],
615
576
  {
616
577
  id: "install-plugin",
617
578
  description: `Install ${pluginSource}`,
@@ -728,11 +689,31 @@ function listBlockedScriptsStep(workspaceName) {
728
689
  failureHint: `The workspace itself is complete \u2014 every earlier step finished; only listing the blocked install scripts failed. Inside "${workspaceName}", run bun pm untrusted to see them.`
729
690
  };
730
691
  }
692
+ function reactAppSteps(workspaceName, pm) {
693
+ return [
694
+ {
695
+ id: "add-react-plugin",
696
+ description: "Add @nx/react with nx add (at the workspace nx version)",
697
+ command: pm,
698
+ args: [...nxArgs(pm), "add", "@nx/react"],
699
+ cwd: workspaceName
700
+ },
701
+ {
702
+ id: "create-web-app",
703
+ description: "Create the React + Vite app in apps/web with the official @nx/react generator",
704
+ command: pm,
705
+ args: [...nxArgs(pm), "g", "@nx/react:app", "apps/web", ...REACT_APP_OPTIONS],
706
+ cwd: workspaceName
707
+ }
708
+ ];
709
+ }
710
+ var REACT_APP_OPTIONS = ["--bundler=vite", "--linter=eslint", "--style=css", "--unitTestRunner=vitest", ...NO_E2E, "--routing=false"];
711
+ var API_APP_OPTIONS = ["--unitTestRunner=vitest", ...NO_E2E];
731
712
  function backendSteps(backend, workspaceName, pm, initCommand) {
732
713
  const nxG = nxGenerateCommand(pm);
733
- const templateApi = (ctx) => ctx.listDir("apps").includes("api");
714
+ const apiExists = (ctx) => ctx.listDir("apps").includes("api");
734
715
  const standaloneError = `this workspace has no apps/ directory \u2014 CLI backend wiring needs the frontend under apps/. Create the api app with the official @nx generator, then run \`${nxG} @clearlydev/nx-archweave:add-backend\` yourself. Finish setup inside "${workspaceName}" with: ${initCommand}`;
735
- const nestConflictError = `the workspace template already ships an Express apps/api \u2014 re-run with --backend=express to wire it, or remove apps/api and scaffold Nest yourself, then run add-backend. Either way, finish setup inside "${workspaceName}" with: ${initCommand}`;
716
+ const apiExistsError = `the workspace already has an apps/api \u2014 wire it with \`${nxG} @clearlydev/nx-archweave:add-backend --backend=${backend}\`, or remove it and run the official generator yourself. Either way, finish setup inside "${workspaceName}" with: ${initCommand}`;
736
717
  return [
737
718
  {
738
719
  id: "install-backend-plugin",
@@ -742,9 +723,7 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
742
723
  cwd: workspaceName,
743
724
  finalizeArgs: (ctx) => {
744
725
  if (ctx.listDir("apps").length === 0) return { error: standaloneError };
745
- if (templateApi(ctx)) {
746
- return backend === "express" ? { skip: "the workspace template already ships the Express api" } : { error: nestConflictError };
747
- }
726
+ if (apiExists(ctx)) return { error: apiExistsError };
748
727
  const raw = ctx.readFile("package.json");
749
728
  if (raw === void 0) return { error: "could not read the created workspace's package.json to pin the @nx version" };
750
729
  try {
@@ -763,14 +742,12 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
763
742
  command: pm,
764
743
  // Since Nx 20 the positional is a directory relative to the workspace root —
765
744
  // a bare `api` would land at the root instead of beside the frontend.
766
- args: [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api"],
745
+ args: [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", ...API_APP_OPTIONS],
767
746
  cwd: workspaceName,
768
747
  finalizeArgs: (ctx) => {
769
748
  const all = ctx.listDir("apps");
770
749
  if (all.length === 0) return { error: standaloneError };
771
- if (all.includes("api")) {
772
- return backend === "express" ? { skip: "the workspace template already ships the Express api" } : { error: nestConflictError };
773
- }
750
+ if (all.includes("api")) return { error: apiExistsError };
774
751
  const apps = all.filter((name) => name !== "api" && name !== "e2e" && !name.endsWith("-e2e"));
775
752
  if (apps.length === 0) return { error: "no frontend app directory found under apps/ in the created workspace" };
776
753
  if (apps.length > 1) return { error: `multiple app directories found under apps/ (${apps.join(", ")}) \u2014 cannot pick --frontendProject` };
@@ -786,7 +763,7 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
786
763
  }
787
764
  return apps[0];
788
765
  })();
789
- return [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", `--frontendProject=${frontendProject}`];
766
+ return [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", `--frontendProject=${frontendProject}`, ...API_APP_OPTIONS];
790
767
  }
791
768
  }
792
769
  ];
@@ -998,7 +975,7 @@ function executePlan(plan, exec, invokedFromDir, ctxFactory = createStepContext)
998
975
  completed.push({ step, result, args, optionalFailure });
999
976
  continue;
1000
977
  }
1001
- return { ok: false, completed, failedStep: step, failedResult: result };
978
+ return { ok: false, completed, failedStep: step, failedResult: result, failedArgs: args };
1002
979
  }
1003
980
  completed.push({ step, result, args });
1004
981
  }
@@ -1036,9 +1013,32 @@ function renderExecuteReport(report, plan, workspaceName) {
1036
1013
  ...!report.finalizeError && failedStep.failureHint ? [` ${failedStep.failureHint}`] : [],
1037
1014
  "",
1038
1015
  `The "${workspaceName}" directory (if it was created) was left in place \u2014 create-nx-archweave never deletes it automatically.`,
1039
- `Inspect the error above, fix the issue, then either re-run the failed command yourself inside "${workspaceName}" or remove the directory and start over.`
1016
+ `Inspect the error above and fix the issue. Then either run the remaining steps below yourself, or remove the directory and start over.`,
1017
+ "",
1018
+ ...renderRemainingSteps(report, plan, workspaceName)
1040
1019
  ].join("\n");
1041
1020
  }
1021
+ function renderRemainingSteps(report, plan, workspaceName) {
1022
+ const start = plan.steps.indexOf(report.failedStep);
1023
+ const remaining = start === -1 ? [report.failedStep] : plan.steps.slice(start);
1024
+ const lines = [`Remaining steps, in order (printed only \u2014 create-nx-archweave does not run them again):`];
1025
+ const env = Object.entries(plan.env ?? {});
1026
+ if (env.length > 0) lines.push(` Run them with ${env.map(([key, value]) => `${key}=${value}`).join(" ")} set, as the CLI does.`);
1027
+ remaining.forEach((step, index) => {
1028
+ const ranArgs = step === report.failedStep ? report.failedArgs : void 0;
1029
+ const args = ranArgs ?? step.args;
1030
+ const where = step.cwd === null ? "in the directory you ran create-nx-archweave from" : `inside "${workspaceName}"`;
1031
+ lines.push(` ${index + 1}. ${step.description}${step.optional ? " (optional)" : ""} \u2014 ${where}:`);
1032
+ lines.push(` ${[step.command, ...args].map(shellQuote).join(" ")}`);
1033
+ if (step.finalizeArgs && ranArgs === void 0) {
1034
+ lines.push(" (the CLI works out this step's exact arguments when it runs \u2014 check them against the workspace first)");
1035
+ }
1036
+ });
1037
+ return lines;
1038
+ }
1039
+ function shellQuote(word) {
1040
+ return /^[\w@%+=:,./^~-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
1041
+ }
1042
1042
 
1043
1043
  // src/bin.ts
1044
1044
  var NonInteractiveInputError = class extends Error {
@@ -1126,7 +1126,6 @@ async function main(argv) {
1126
1126
  {
1127
1127
  flag: args.packageManager,
1128
1128
  userAgent: process.env["npm_config_user_agent"],
1129
- frontend: inputs.frontend,
1130
1129
  interactive,
1131
1130
  probe: readVersion
1132
1131
  },
@@ -35,6 +35,8 @@ export type ExecuteReport = {
35
35
  completed: CompletedStep[];
36
36
  failedStep: PlanStep;
37
37
  failedResult: ExecResult;
38
+ /** The args the failed step ran with (after finalizeArgs); absent when it never executed. */
39
+ failedArgs?: string[];
38
40
  /** Set when finalizeArgs refused — the step never executed. */
39
41
  finalizeError?: string;
40
42
  };
@@ -1,5 +1,4 @@
1
1
  import type { Ask } from './prompts.ts';
2
- import type { CliFrontend } from './types.ts';
3
2
  /** The managers this CLI builds workspaces with. */
4
3
  export type CliPackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
5
4
  export declare function isCliPackageManager(name: string): name is CliPackageManager;
@@ -24,14 +23,7 @@ export type Probe = (name: CliPackageManager) => string | undefined;
24
23
  * launched this command. The launcher's own version is used instead of probing it again.
25
24
  */
26
25
  export declare function listInstalledManagers(launcher: Launcher | undefined, probe: Probe): InstalledManager[];
27
- /**
28
- * Why this manager cannot create the chosen workspace, or undefined when it can.
29
- * Both known cases fail inside create-nx-workspace's own install of
30
- * Nx's react template, so a pick is never allowed — an unknown version is not held
31
- * against the manager.
32
- */
33
- export declare function managerIncompatibility(manager: InstalledManager, frontend: CliFrontend): string | undefined;
34
- export type PackageManagerSource = 'flag' | 'launcher' | 'default' | 'asked' | 'only-installed' | 'only-compatible';
26
+ export type PackageManagerSource = 'flag' | 'launcher' | 'default' | 'asked' | 'only-installed';
35
27
  export interface PackageManagerResolution {
36
28
  packageManager: CliPackageManager;
37
29
  source: PackageManagerSource;
@@ -42,17 +34,15 @@ export interface ChooseInput {
42
34
  /** `--package-manager`, already validated by parse-args. */
43
35
  flag: CliPackageManager | undefined;
44
36
  userAgent: string | undefined;
45
- frontend: CliFrontend;
46
37
  /** stdin is a TTY — only then is a question asked. */
47
38
  interactive: boolean;
48
39
  probe: Probe;
49
40
  }
50
41
  /**
51
- * Flag > (interactive) the question > launcher > npm. One rule everywhere: a manager
52
- * that cannot create the chosen workspace is never used — the question shows it with its
53
- * reason but does not offer it, and a flag or launcher naming it stops the run before
54
- * anything is created. Non-interactive runs launched by an unsupported manager are
55
- * refused the same way.
42
+ * Flag > (interactive) the question > launcher > npm. Every supported manager can create
43
+ * every workspace (npm 10 and yarn 1 included, measured 2026-10-25 on the empty-preset
44
+ * react-vite path and on next). Non-interactive runs launched by an unsupported manager are
45
+ * refused before anything is created.
56
46
  */
57
47
  export declare function choosePackageManager(input: ChooseInput, ask: Ask, tell: (line: string) => void): Promise<PackageManagerResolution | {
58
48
  error: string;
@@ -35,7 +35,7 @@ export interface StepContext {
35
35
  listDir(relativePath: string): string[];
36
36
  }
37
37
  export interface PlanStep {
38
- id: 'create-workspace' | 'install-plugin' | 'install-backend-plugin' | 'create-api-app' | 'run-init' | 'settle-build-approvals' | 'list-blocked-scripts' | 'install-ai-plugin';
38
+ id: 'create-workspace' | 'add-react-plugin' | 'create-web-app' | 'install-plugin' | 'install-backend-plugin' | 'create-api-app' | 'run-init' | 'settle-build-approvals' | 'list-blocked-scripts' | 'install-ai-plugin';
39
39
  description: string;
40
40
  command: string;
41
41
  args: string[];
@@ -47,7 +47,7 @@ export interface PlanStep {
47
47
  * @nx/<backend> version pin and --frontendProject are only knowable after
48
48
  * step 1 ran). Returning { error } stops the plan before executing the
49
49
  * step; { skip } records the step as satisfied without executing it
50
- * (measured reality: the react template already ships apps/api). */
50
+ * (express already declared, so there is nothing to align). */
51
51
  finalizeArgs?: (ctx: StepContext) => string[] | {
52
52
  error: string;
53
53
  } | {
@@ -72,17 +72,6 @@ export interface CreatePlan {
72
72
  * automatic commit — that stays outside what this CLI does). */
73
73
  nextSteps: string[];
74
74
  }
75
- /** npm 10 (bundled with Node 22) crashes inside create-nx-workspace's own install
76
- * of the react template's dependency tree — an arborist bug in its peer-set
77
- * resolution, reproduced on every npm 10 release up to 10.9.9 and absent in npm 11
78
- * (E2E CI finding, 2026-10-13). The next preset installs fine on npm 10
79
- * (2026-10-14), so only react-vite + npm is affected: package-manager.ts never lets a
80
- * known npm 10 pick it, and the create step keeps this hint for the case the version
81
- * could not be read. */
82
- export declare const NPM_10_CREATE_WORKSPACE_HINT: string;
83
- /** yarn 1 cannot resolve the `workspace:*` packages Nx's react template declares
84
- * (measured 2026-10-15); the same guard and fallback as npm 10 above. */
85
- export declare const YARN_1_CREATE_WORKSPACE_HINT: string;
86
75
  /** `pnpm nx g` / `npm exec nx -- g` — how the user runs a generator in the new workspace. */
87
76
  export declare function nxGenerateCommand(pm: CliPackageManager): string;
88
77
  export declare function isYarnBerry(ctx: Pick<StepContext, 'readFile'>): boolean;
package/dist/index.js CHANGED
@@ -11,9 +11,9 @@ var AI_HOST_CHOICES = [
11
11
  ];
12
12
  var REVIEW_PROVIDER_CHOICES = [
13
13
  { id: "claude", label: "Claude" },
14
- { id: "gemini", label: "Gemini" },
15
- { id: "gpt", label: "GPT" },
16
- { id: "cursor", label: "Cursor" }
14
+ { id: "gemini", label: "Gemini (beta)" },
15
+ { id: "gpt", label: "GPT (beta)" },
16
+ { id: "cursor", label: "Cursor (beta)" }
17
17
  ];
18
18
  var REVIEW_PROVIDER_VALUES = REVIEW_PROVIDER_CHOICES.map((provider) => provider.id);
19
19
  var AI_HOST_VALUES = AI_HOST_CHOICES.map((host) => host.id);
@@ -38,12 +38,6 @@ var PLUGIN_VALUES = PLUGIN_CHOICES.map((plugin) => plugin.id);
38
38
  // src/cli/parse-args.ts
39
39
  import { parseArgs as parseNodeArgs } from "node:util";
40
40
 
41
- // src/cli/preflight.ts
42
- function parseMajor(version) {
43
- const match = /^\s*v?(\d+)\./.exec(version ?? "");
44
- return match ? Number(match[1]) : void 0;
45
- }
46
-
47
41
  // src/cli/package-manager.ts
48
42
  var SUPPORTED = ["npm", "pnpm", "yarn", "bun"];
49
43
  function isCliPackageManager(name) {
@@ -385,9 +379,7 @@ function renderVersion() {
385
379
 
386
380
  // src/cli/plan.ts
387
381
  var DEFAULT_PLUGIN_SOURCE = `@clearlydev/nx-archweave@^${CLI_VERSION}`;
388
- var VALIDATED_CREATE_NX_WORKSPACE_VERSION = "23.3.0";
389
- var NPM_10_CREATE_WORKSPACE_HINT = `If the npm error above is "Cannot read properties of null (reading 'edgesOut')", that is a known npm 10 bug with the current Nx workspace dependencies \u2014 upgrade npm (npm install -g npm@11), or, if pnpm is installed, re-run with --package-manager=pnpm.`;
390
- var YARN_1_CREATE_WORKSPACE_HINT = `If the yarn error above is "Couldn't find package \u2026@workspace:*", that is yarn 1 (classic), which cannot resolve the workspace:* packages in Nx's react template \u2014 use yarn 2+ (corepack enable, then yarn set version stable) or re-run with another package manager.`;
382
+ var VALIDATED_CREATE_NX_WORKSPACE_VERSION = "24.0.1";
391
383
  function nxArgs(pm) {
392
384
  return pm === "npm" ? ["exec", "nx", "--"] : ["nx"];
393
385
  }
@@ -404,10 +396,11 @@ function isYarnBerry(ctx) {
404
396
  return ctx.readFile(".yarnrc.yml") !== void 0 || /^__metadata:/m.test(ctx.readFile("yarn.lock") ?? "");
405
397
  }
406
398
  var YARN_PLAN_ENV = { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" };
399
+ var NO_E2E = ["--e2eTestRunner=none"];
407
400
  function createWorkspaceSourceArgs(frontend, backend) {
408
- if (frontend === "react-vite") return ["--template=nrwl/react-template"];
409
- if (frontend === "next") return ["--preset=next"];
410
- return [`--preset=${backend}`, "--appName=api"];
401
+ if (frontend === "react-vite") return ["--preset=empty"];
402
+ if (frontend === "next") return ["--preset=next", "--appName=web", ...NO_E2E];
403
+ return [`--preset=${backend}`, "--appName=api", ...NO_E2E, "--unitTestRunner=vitest"];
411
404
  }
412
405
  function buildNextSteps(input) {
413
406
  const lines = [
@@ -432,7 +425,7 @@ function buildNextSteps(input) {
432
425
  function buildPlan(input) {
433
426
  const pluginSource = input.pluginSource ?? DEFAULT_PLUGIN_SOURCE;
434
427
  const workspaceSource = createWorkspaceSourceArgs(input.frontend, input.backend);
435
- const aiAgentArgs = input.aiHosts.map((host) => `--aiAgents=${AI_HOST_CHOICES.find((choice) => choice.id === host)?.nxAgent ?? host}`);
428
+ const aiAgentArgs = input.aiHosts.length === 0 ? ["--aiAgents=none"] : input.aiHosts.map((host) => `--aiAgents=${AI_HOST_CHOICES.find((choice) => choice.id === host)?.nxAgent ?? host}`);
436
429
  const pm = input.packageManager;
437
430
  const nxPrefix = nxArgs(pm);
438
431
  const initArgs = [
@@ -448,8 +441,10 @@ function buildPlan(input) {
448
441
  // Step 2 (the plugin install) always dirties the fresh workspace's tree
449
442
  // (package.json + lockfile), so init's clean-tree refusal would otherwise
450
443
  // always fire here. This is orchestration knowledge, not policy: the CLI
451
- // knows what its own previous step did.
452
- "--allowDirty=true"
444
+ // knows what its own previous step did. For the same reason the report's note about
445
+ // reverting earlier user changes doesn't apply: the only changes are that install's.
446
+ "--allowDirty=true",
447
+ "--dirtyFromCaller=true"
453
448
  ];
454
449
  if (input.review !== void 0) {
455
450
  initArgs.push(`--review=${input.review}`);
@@ -484,10 +479,9 @@ function buildPlan(input) {
484
479
  "--nxCloud=skip",
485
480
  "--interactive=false"
486
481
  ],
487
- cwd: null,
488
- ...input.frontend === "react-vite" && pm === "npm" ? { failureHint: NPM_10_CREATE_WORKSPACE_HINT } : {},
489
- ...input.frontend === "react-vite" && pm === "yarn" ? { failureHint: YARN_1_CREATE_WORKSPACE_HINT } : {}
482
+ cwd: null
490
483
  },
484
+ ...input.frontend === "react-vite" ? reactAppSteps(input.name, pm) : [],
491
485
  {
492
486
  id: "install-plugin",
493
487
  description: `Install ${pluginSource}`,
@@ -604,11 +598,31 @@ function listBlockedScriptsStep(workspaceName) {
604
598
  failureHint: `The workspace itself is complete \u2014 every earlier step finished; only listing the blocked install scripts failed. Inside "${workspaceName}", run bun pm untrusted to see them.`
605
599
  };
606
600
  }
601
+ function reactAppSteps(workspaceName, pm) {
602
+ return [
603
+ {
604
+ id: "add-react-plugin",
605
+ description: "Add @nx/react with nx add (at the workspace nx version)",
606
+ command: pm,
607
+ args: [...nxArgs(pm), "add", "@nx/react"],
608
+ cwd: workspaceName
609
+ },
610
+ {
611
+ id: "create-web-app",
612
+ description: "Create the React + Vite app in apps/web with the official @nx/react generator",
613
+ command: pm,
614
+ args: [...nxArgs(pm), "g", "@nx/react:app", "apps/web", ...REACT_APP_OPTIONS],
615
+ cwd: workspaceName
616
+ }
617
+ ];
618
+ }
619
+ var REACT_APP_OPTIONS = ["--bundler=vite", "--linter=eslint", "--style=css", "--unitTestRunner=vitest", ...NO_E2E, "--routing=false"];
620
+ var API_APP_OPTIONS = ["--unitTestRunner=vitest", ...NO_E2E];
607
621
  function backendSteps(backend, workspaceName, pm, initCommand) {
608
622
  const nxG = nxGenerateCommand(pm);
609
- const templateApi = (ctx) => ctx.listDir("apps").includes("api");
623
+ const apiExists = (ctx) => ctx.listDir("apps").includes("api");
610
624
  const standaloneError = `this workspace has no apps/ directory \u2014 CLI backend wiring needs the frontend under apps/. Create the api app with the official @nx generator, then run \`${nxG} @clearlydev/nx-archweave:add-backend\` yourself. Finish setup inside "${workspaceName}" with: ${initCommand}`;
611
- const nestConflictError = `the workspace template already ships an Express apps/api \u2014 re-run with --backend=express to wire it, or remove apps/api and scaffold Nest yourself, then run add-backend. Either way, finish setup inside "${workspaceName}" with: ${initCommand}`;
625
+ const apiExistsError = `the workspace already has an apps/api \u2014 wire it with \`${nxG} @clearlydev/nx-archweave:add-backend --backend=${backend}\`, or remove it and run the official generator yourself. Either way, finish setup inside "${workspaceName}" with: ${initCommand}`;
612
626
  return [
613
627
  {
614
628
  id: "install-backend-plugin",
@@ -618,9 +632,7 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
618
632
  cwd: workspaceName,
619
633
  finalizeArgs: (ctx) => {
620
634
  if (ctx.listDir("apps").length === 0) return { error: standaloneError };
621
- if (templateApi(ctx)) {
622
- return backend === "express" ? { skip: "the workspace template already ships the Express api" } : { error: nestConflictError };
623
- }
635
+ if (apiExists(ctx)) return { error: apiExistsError };
624
636
  const raw = ctx.readFile("package.json");
625
637
  if (raw === void 0) return { error: "could not read the created workspace's package.json to pin the @nx version" };
626
638
  try {
@@ -639,14 +651,12 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
639
651
  command: pm,
640
652
  // Since Nx 20 the positional is a directory relative to the workspace root —
641
653
  // a bare `api` would land at the root instead of beside the frontend.
642
- args: [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api"],
654
+ args: [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", ...API_APP_OPTIONS],
643
655
  cwd: workspaceName,
644
656
  finalizeArgs: (ctx) => {
645
657
  const all = ctx.listDir("apps");
646
658
  if (all.length === 0) return { error: standaloneError };
647
- if (all.includes("api")) {
648
- return backend === "express" ? { skip: "the workspace template already ships the Express api" } : { error: nestConflictError };
649
- }
659
+ if (all.includes("api")) return { error: apiExistsError };
650
660
  const apps = all.filter((name) => name !== "api" && name !== "e2e" && !name.endsWith("-e2e"));
651
661
  if (apps.length === 0) return { error: "no frontend app directory found under apps/ in the created workspace" };
652
662
  if (apps.length > 1) return { error: `multiple app directories found under apps/ (${apps.join(", ")}) \u2014 cannot pick --frontendProject` };
@@ -662,7 +672,7 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
662
672
  }
663
673
  return apps[0];
664
674
  })();
665
- return [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", `--frontendProject=${frontendProject}`];
675
+ return [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", `--frontendProject=${frontendProject}`, ...API_APP_OPTIONS];
666
676
  }
667
677
  }
668
678
  ];
@@ -795,7 +805,7 @@ function executePlan(plan, exec, invokedFromDir, ctxFactory = createStepContext)
795
805
  completed.push({ step, result, args, optionalFailure });
796
806
  continue;
797
807
  }
798
- return { ok: false, completed, failedStep: step, failedResult: result };
808
+ return { ok: false, completed, failedStep: step, failedResult: result, failedArgs: args };
799
809
  }
800
810
  completed.push({ step, result, args });
801
811
  }
@@ -833,9 +843,38 @@ function renderExecuteReport(report, plan, workspaceName) {
833
843
  ...!report.finalizeError && failedStep.failureHint ? [` ${failedStep.failureHint}`] : [],
834
844
  "",
835
845
  `The "${workspaceName}" directory (if it was created) was left in place \u2014 create-nx-archweave never deletes it automatically.`,
836
- `Inspect the error above, fix the issue, then either re-run the failed command yourself inside "${workspaceName}" or remove the directory and start over.`
846
+ `Inspect the error above and fix the issue. Then either run the remaining steps below yourself, or remove the directory and start over.`,
847
+ "",
848
+ ...renderRemainingSteps(report, plan, workspaceName)
837
849
  ].join("\n");
838
850
  }
851
+ function renderRemainingSteps(report, plan, workspaceName) {
852
+ const start = plan.steps.indexOf(report.failedStep);
853
+ const remaining = start === -1 ? [report.failedStep] : plan.steps.slice(start);
854
+ const lines = [`Remaining steps, in order (printed only \u2014 create-nx-archweave does not run them again):`];
855
+ const env = Object.entries(plan.env ?? {});
856
+ if (env.length > 0) lines.push(` Run them with ${env.map(([key, value]) => `${key}=${value}`).join(" ")} set, as the CLI does.`);
857
+ remaining.forEach((step, index) => {
858
+ const ranArgs = step === report.failedStep ? report.failedArgs : void 0;
859
+ const args = ranArgs ?? step.args;
860
+ const where = step.cwd === null ? "in the directory you ran create-nx-archweave from" : `inside "${workspaceName}"`;
861
+ lines.push(` ${index + 1}. ${step.description}${step.optional ? " (optional)" : ""} \u2014 ${where}:`);
862
+ lines.push(` ${[step.command, ...args].map(shellQuote).join(" ")}`);
863
+ if (step.finalizeArgs && ranArgs === void 0) {
864
+ lines.push(" (the CLI works out this step's exact arguments when it runs \u2014 check them against the workspace first)");
865
+ }
866
+ });
867
+ return lines;
868
+ }
869
+ function shellQuote(word) {
870
+ return /^[\w@%+=:,./^~-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
871
+ }
872
+
873
+ // src/cli/preflight.ts
874
+ function parseMajor(version) {
875
+ const match = /^\s*v?(\d+)\./.exec(version ?? "");
876
+ return match ? Number(match[1]) : void 0;
877
+ }
839
878
  export {
840
879
  AI_HOST_CHOICES,
841
880
  AI_HOST_PROBES,
@@ -845,14 +884,12 @@ export {
845
884
  DEFAULT_AI_HOSTS,
846
885
  FRONTEND_VALUES,
847
886
  LOCALE_VALUES,
848
- NPM_10_CREATE_WORKSPACE_HINT,
849
887
  PLUGIN_CHOICES,
850
888
  PLUGIN_VALUES,
851
889
  PNPM_PENDING_BUILD_PLACEHOLDER,
852
890
  PRESET_VALUES,
853
891
  REVIEW_PROVIDER_CHOICES,
854
892
  REVIEW_PROVIDER_VALUES,
855
- YARN_1_CREATE_WORKSPACE_HINT,
856
893
  aiHostsQuestion,
857
894
  buildPlan,
858
895
  childEnv,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-nx-archweave",
3
- "version": "0.2.1",
4
- "description": "One command from an empty directory to a configured Nx workspace: FSD Lite structure, AI agent rules, GitHub templates, and optional AI PR review (Claude, Gemini, GPT, Cursor)",
3
+ "version": "0.3.0",
4
+ "description": "One command from an empty directory to a configured Nx workspace: an FSD Lite lint rule, frontend/backend boundaries, AI agent rules, GitHub templates, and optional AI PR review with the provider you choose (Claude supported; Gemini, GPT and Cursor in beta)",
5
5
  "license": "MIT",
6
6
  "author": "clearlydev (https://github.com/clearlydev)",
7
7
  "engines": {