create-nx-archweave 0.1.0 → 0.2.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
@@ -118,6 +118,7 @@ When something goes wrong:
118
118
  | `--ai-hosts` | `claude-code`, `codex`, `cursor`, `gemini-cli` |
119
119
  | `--plugins` | catalog ids (`ponytail`), or `none` |
120
120
  | `--package-manager` | `npm` \| `pnpm` \| `yarn` \| `bun` |
121
+ | `--allow-nested` | create the workspace even inside a git repository or a folder with a `package.json` (refused otherwise) |
121
122
  | `-h`, `--help` / `-v`, `--version` | — |
122
123
 
123
124
  ### `--preset`
@@ -185,7 +186,7 @@ The printed next steps cover the first commit. If review is enabled, they also c
185
186
 
186
187
  - `nx build create-nx-archweave` runs tsc, for local dev and typecheck.
187
188
  - `nx test` runs Jest.
188
- - `nx bundle` (depends on `build`) uses esbuild to bundle `src/index.ts` and `src/bin.ts` into `dist/`. The `bin.js` shebang is preserved, and only `tslib` stays external.
189
+ - `nx bundle` (depends on `build`) uses esbuild to bundle `src/index.ts` and `src/bin.ts` into `dist/`. The `bin.js` shebang is preserved, and the CLI has no runtime dependencies.
189
190
  - The CLI has no runtime import of the plugin. It only names the plugin as an install spec.
190
191
 
191
192
  ## License
package/dist/bin.js CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  // src/bin.ts
4
4
  import { execFile, spawnSync as spawnSync2 } from "node:child_process";
5
+ import { existsSync as existsSync2 } from "node:fs";
6
+ import { join as join3 } from "node:path";
5
7
  import { createInterface } from "node:readline/promises";
6
8
 
7
9
  // src/cli/parse-args.ts
@@ -153,6 +155,25 @@ function describeResolution(resolution) {
153
155
  return `${name} (${SOURCE_TEXT[resolution.source]})`;
154
156
  }
155
157
 
158
+ // src/cli/workspace-name.ts
159
+ var MAX_LENGTH = 214;
160
+ var EXAMPLE = "my-app";
161
+ function workspaceNameProblem(name) {
162
+ if (name.length === 0) return "it is empty";
163
+ if (/\s/.test(name)) return "it contains whitespace";
164
+ if (name.length > MAX_LENGTH) return `it is ${name.length} characters long, and npm allows at most ${MAX_LENGTH}`;
165
+ if (name.startsWith(".") || name.startsWith("_")) return `it starts with "${name[0]}"`;
166
+ if (/[A-Z]/.test(name)) return "it contains uppercase letters, and npm package names are lowercase";
167
+ const others = [...new Set(name.replace(/[a-z0-9._-]/g, ""))];
168
+ if (others.length > 0) {
169
+ return `it contains ${others.map((char) => `"${char}"`).join(", ")} \u2014 only lowercase letters, digits, "-", "_" and "." are allowed`;
170
+ }
171
+ return void 0;
172
+ }
173
+ function invalidWorkspaceNameMessage(name, problem) {
174
+ return `Invalid workspace name "${name}": ${problem}. For example: ${EXAMPLE}`;
175
+ }
176
+
156
177
  // src/cli/parse-args.ts
157
178
  var STRING_VALUE_FLAGS = /* @__PURE__ */ new Set(["--preset", "--frontend", "--backend", "--locale", "--ai-hosts", "--plugins", "--review-provider", "--plugin-source", "--package-manager"]);
158
179
  function extractReviewFlag(argv) {
@@ -199,7 +220,8 @@ function parseCliArgs(argv) {
199
220
  plugins: { type: "string" },
200
221
  "review-provider": { type: "string" },
201
222
  "plugin-source": { type: "string" },
202
- "package-manager": { type: "string" }
223
+ "package-manager": { type: "string" },
224
+ "allow-nested": { type: "boolean" }
203
225
  };
204
226
  let parsed;
205
227
  try {
@@ -216,6 +238,10 @@ function parseCliArgs(argv) {
216
238
  };
217
239
  }
218
240
  const name = parsed.positionals[0];
241
+ const nameProblem = name === void 0 ? void 0 : workspaceNameProblem(name);
242
+ if (name !== void 0 && nameProblem !== void 0) {
243
+ return { kind: "error", message: invalidWorkspaceNameMessage(name, nameProblem) };
244
+ }
219
245
  const preset = parsed.values.preset;
220
246
  if (preset !== void 0 && !PRESET_VALUES.includes(preset)) {
221
247
  return { kind: "error", message: invalidChoiceMessage("--preset", preset, PRESET_VALUES) };
@@ -286,11 +312,31 @@ function parseCliArgs(argv) {
286
312
  reviewProvider,
287
313
  review,
288
314
  pluginSource: parsed.values["plugin-source"],
289
- packageManager
315
+ packageManager,
316
+ allowNested: parsed.values["allow-nested"] === true
290
317
  }
291
318
  };
292
319
  }
293
320
 
321
+ // src/cli/location-check.ts
322
+ function checkInvocationDirectory(dir, probe) {
323
+ const gitRoot2 = probe.gitRoot(dir);
324
+ if (gitRoot2 !== null) return { ok: false, reason: "git", at: gitRoot2 };
325
+ if (probe.hasPackageJson(dir)) return { ok: false, reason: "package", at: dir };
326
+ return { ok: true };
327
+ }
328
+ function renderLocationRefusal(check) {
329
+ const where = check.reason === "git" ? `${check.at} is a git repository` : `${check.at} has a package.json`;
330
+ return [
331
+ `create-nx-archweave makes a new workspace in a new folder, but ${where}.`,
332
+ "To set up an existing repository instead, run these in it:",
333
+ " npx nx@latest init",
334
+ " nx g @clearlydev/nx-archweave:init",
335
+ "To create a separate workspace, run create-nx-archweave from a folder outside it.",
336
+ "To nest a workspace here anyway, pass --allow-nested."
337
+ ].join("\n");
338
+ }
339
+
294
340
  // src/cli/prompts.ts
295
341
  async function askChoice(ask, question, choices) {
296
342
  for (; ; ) {
@@ -446,6 +492,7 @@ function renderUsage() {
446
492
  " --review, --no-review Force AI PR review on/off (quick-start asks y/N; solo/team: on)",
447
493
  " --review-provider <name> claude, gemini, gpt or cursor (asked when review is on; default: the first AI tool's)",
448
494
  " --plugin-source <spec> Override the nx-archweave plugin package/version to install",
495
+ " --allow-nested Create the workspace even inside a git repository or package.json project",
449
496
  " --package-manager <name> npm, pnpm, yarn or bun (asked if omitted; without a terminal: the one that ran this command, else npm)",
450
497
  " -h, --help Show this help",
451
498
  " -v, --version Show the CLI version",
@@ -1020,6 +1067,13 @@ async function main(argv) {
1020
1067
  return 1;
1021
1068
  }
1022
1069
  const name = args.name;
1070
+ if (!args.allowNested) {
1071
+ const location = checkInvocationDirectory(process.cwd(), { gitRoot, hasPackageJson: (dir) => existsSync2(join3(dir, "package.json")) });
1072
+ if (!location.ok) {
1073
+ console.error(renderLocationRefusal(location));
1074
+ return 1;
1075
+ }
1076
+ }
1023
1077
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1024
1078
  const ask = (question) => {
1025
1079
  if (!process.stdin.isTTY) {
@@ -1165,3 +1219,7 @@ main(process.argv.slice(2)).then((code) => {
1165
1219
  console.error(error instanceof Error ? error.stack ?? error.message : String(error));
1166
1220
  process.exitCode = 1;
1167
1221
  });
1222
+ function gitRoot(dir) {
1223
+ const result = spawnSync2("git", ["rev-parse", "--show-toplevel"], { cwd: dir, encoding: "utf-8" });
1224
+ return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
1225
+ }
@@ -0,0 +1,23 @@
1
+ /** Where create-nx-archweave was started: a new workspace goes into a new folder, so a
2
+ * directory that already belongs to a project is almost always a mistake — the workspace would
3
+ * land inside that repository as untracked files, its `.github/` would have no effect, and the
4
+ * suggested `git add -A` would stage the outer repository's changes too. */
5
+ export type InvocationCheck = {
6
+ ok: true;
7
+ } | {
8
+ ok: false;
9
+ reason: 'git' | 'package';
10
+ at: string;
11
+ };
12
+ export interface LocationProbe {
13
+ /** The top of the git repository containing `dir`, or null outside one. */
14
+ gitRoot(dir: string): string | null;
15
+ hasPackageJson(dir: string): boolean;
16
+ }
17
+ /** A package.json counts only in `dir` itself: one further up (a home directory's) says
18
+ * nothing about the folder the workspace goes into. */
19
+ export declare function checkInvocationDirectory(dir: string, probe: LocationProbe): InvocationCheck;
20
+ export declare function renderLocationRefusal(check: Extract<InvocationCheck, {
21
+ ok: false;
22
+ }>): string;
23
+ //# sourceMappingURL=location-check.d.ts.map
@@ -22,6 +22,9 @@ export interface ParsedArgs {
22
22
  * passed" — package-manager.ts then asks or reads the launching manager. Any other
23
23
  * value is refused before this field is ever set. */
24
24
  packageManager: CliPackageManager | undefined;
25
+ /** `--allow-nested`: create the workspace even inside a git repository or a package.json
26
+ * project (location-check.ts refuses that otherwise). */
27
+ allowNested: boolean;
25
28
  }
26
29
  export type ParseArgsOutcome = {
27
30
  kind: 'help';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Why `name` can't be a workspace name, or undefined when it can. The name becomes the
3
+ * directory and the root package.json name, and it reaches create-nx-workspace as an
4
+ * argument — on Windows through cmd.exe — so only npm's package-name characters pass:
5
+ * lowercase letters, digits, "-", "_" and ".", not starting with "." or "_".
6
+ */
7
+ export declare function workspaceNameProblem(name: string): string | undefined;
8
+ export declare function invalidWorkspaceNameMessage(name: string, problem: string): string;
9
+ //# sourceMappingURL=workspace-name.d.ts.map
package/dist/index.js CHANGED
@@ -50,6 +50,25 @@ function isCliPackageManager(name) {
50
50
  return SUPPORTED.includes(name);
51
51
  }
52
52
 
53
+ // src/cli/workspace-name.ts
54
+ var MAX_LENGTH = 214;
55
+ var EXAMPLE = "my-app";
56
+ function workspaceNameProblem(name) {
57
+ if (name.length === 0) return "it is empty";
58
+ if (/\s/.test(name)) return "it contains whitespace";
59
+ if (name.length > MAX_LENGTH) return `it is ${name.length} characters long, and npm allows at most ${MAX_LENGTH}`;
60
+ if (name.startsWith(".") || name.startsWith("_")) return `it starts with "${name[0]}"`;
61
+ if (/[A-Z]/.test(name)) return "it contains uppercase letters, and npm package names are lowercase";
62
+ const others = [...new Set(name.replace(/[a-z0-9._-]/g, ""))];
63
+ if (others.length > 0) {
64
+ return `it contains ${others.map((char) => `"${char}"`).join(", ")} \u2014 only lowercase letters, digits, "-", "_" and "." are allowed`;
65
+ }
66
+ return void 0;
67
+ }
68
+ function invalidWorkspaceNameMessage(name, problem) {
69
+ return `Invalid workspace name "${name}": ${problem}. For example: ${EXAMPLE}`;
70
+ }
71
+
53
72
  // src/cli/parse-args.ts
54
73
  var STRING_VALUE_FLAGS = /* @__PURE__ */ new Set(["--preset", "--frontend", "--backend", "--locale", "--ai-hosts", "--plugins", "--review-provider", "--plugin-source", "--package-manager"]);
55
74
  function extractReviewFlag(argv) {
@@ -96,7 +115,8 @@ function parseCliArgs(argv) {
96
115
  plugins: { type: "string" },
97
116
  "review-provider": { type: "string" },
98
117
  "plugin-source": { type: "string" },
99
- "package-manager": { type: "string" }
118
+ "package-manager": { type: "string" },
119
+ "allow-nested": { type: "boolean" }
100
120
  };
101
121
  let parsed;
102
122
  try {
@@ -113,6 +133,10 @@ function parseCliArgs(argv) {
113
133
  };
114
134
  }
115
135
  const name = parsed.positionals[0];
136
+ const nameProblem = name === void 0 ? void 0 : workspaceNameProblem(name);
137
+ if (name !== void 0 && nameProblem !== void 0) {
138
+ return { kind: "error", message: invalidWorkspaceNameMessage(name, nameProblem) };
139
+ }
116
140
  const preset = parsed.values.preset;
117
141
  if (preset !== void 0 && !PRESET_VALUES.includes(preset)) {
118
142
  return { kind: "error", message: invalidChoiceMessage("--preset", preset, PRESET_VALUES) };
@@ -183,7 +207,8 @@ function parseCliArgs(argv) {
183
207
  reviewProvider,
184
208
  review,
185
209
  pluginSource: parsed.values["plugin-source"],
186
- packageManager
210
+ packageManager,
211
+ allowNested: parsed.values["allow-nested"] === true
187
212
  }
188
213
  };
189
214
  }
@@ -343,6 +368,7 @@ function renderUsage() {
343
368
  " --review, --no-review Force AI PR review on/off (quick-start asks y/N; solo/team: on)",
344
369
  " --review-provider <name> claude, gemini, gpt or cursor (asked when review is on; default: the first AI tool's)",
345
370
  " --plugin-source <spec> Override the nx-archweave plugin package/version to install",
371
+ " --allow-nested Create the workspace even inside a git repository or package.json project",
346
372
  " --package-manager <name> npm, pnpm, yarn or bun (asked if omitted; without a terminal: the one that ran this command, else npm)",
347
373
  " -h, --help Show this help",
348
374
  " -v, --version Show the CLI version",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nx-archweave",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
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)",
5
5
  "license": "MIT",
6
6
  "author": "clearlydev (https://github.com/clearlydev)",
@@ -74,8 +74,5 @@
74
74
  ]
75
75
  }
76
76
  }
77
- },
78
- "dependencies": {
79
- "tslib": "^2.3.0"
80
77
  }
81
78
  }
@@ -1,153 +0,0 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { readdirSync, readFileSync, statSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { spawnableCommand } from "./spawnable-command.js";
5
- /** Variables a launcher sets only for the command it started. `npm exec --package=<x>`
6
- * exports `npm_config_package=<x>`, and a child `npx -y create-nx-workspace@…` would read
7
- * it and treat create-nx-workspace as a command inside <x> ("not found"). The positional
8
- * `npx create-nx-archweave` form sets nothing here (measured 2026-10-15). */
9
- const LAUNCHER_ONLY = /^npm_config_package$/i;
10
- /** The environment every step runs with: the inherited one minus launcher-only
11
- * variables, plus the plan's overrides. PATH, HOME, proxies and registry settings stay. */
12
- export function childEnv(parent, overrides = {}) {
13
- const env = {};
14
- for (const [key, value] of Object.entries(parent))
15
- if (!LAUNCHER_ONLY.test(key))
16
- env[key] = value;
17
- return { ...env, ...overrides };
18
- }
19
- function isFileOnDisk(path) {
20
- try {
21
- return statSync(path).isFile();
22
- }
23
- catch {
24
- return false;
25
- }
26
- }
27
- export function createProcessExec() {
28
- return (command, args, options) => {
29
- // npx, npm, pnpm, yarn and bun are .cmd shims on Windows, which Node cannot start
30
- // without cmd.exe; there they run through it with every argument escaped.
31
- const env = childEnv(process.env, options.env);
32
- const spawnable = spawnableCommand(command, args, { platform: process.platform, env, isFile: isFileOnDisk });
33
- const result = spawnSync(spawnable.command, spawnable.args, {
34
- cwd: options.cwd,
35
- stdio: 'inherit',
36
- env,
37
- windowsVerbatimArguments: spawnable.windowsVerbatimArguments,
38
- });
39
- return { status: result.status ?? null };
40
- };
41
- }
42
- /** Real StepContext over the created workspace (tests inject their own). */
43
- export function createStepContext(workspacePath) {
44
- return {
45
- workspacePath,
46
- readFile: (relativePath) => {
47
- try {
48
- return readFileSync(join(workspacePath, relativePath), 'utf-8');
49
- }
50
- catch {
51
- return undefined;
52
- }
53
- },
54
- listDir: (relativePath) => {
55
- try {
56
- // directories only — a stray apps/.DS_Store must never count as an app
57
- return readdirSync(join(workspacePath, relativePath), { withFileTypes: true })
58
- .filter((entry) => entry.isDirectory())
59
- .map((entry) => entry.name);
60
- }
61
- catch {
62
- return [];
63
- }
64
- },
65
- };
66
- }
67
- /**
68
- * Runs the plan's steps in order. On the first non-zero exit, stops immediately —
69
- * later steps never run — and reports which step failed clearly, leaving the generated
70
- * directory in place with cleanup guidance instead of deleting it automatically. An
71
- * `optional` step is the exception: its failure is recorded and the run goes on. This function itself never deletes anything;
72
- * it only reports.
73
- */
74
- export function executePlan(plan, exec, invokedFromDir, ctxFactory = createStepContext) {
75
- const completed = [];
76
- for (const step of plan.steps) {
77
- const cwd = step.cwd === null ? invokedFromDir : join(invokedFromDir, step.cwd);
78
- let args = step.args;
79
- if (step.finalizeArgs) {
80
- const finalized = step.finalizeArgs(ctxFactory(cwd));
81
- if ('error' in finalized) {
82
- if (step.optional) {
83
- completed.push({ step, result: { status: null }, optionalFailure: `stopped before execution: ${finalized.error}` });
84
- continue;
85
- }
86
- return { ok: false, completed, failedStep: step, failedResult: { status: null }, finalizeError: finalized.error };
87
- }
88
- if ('skip' in finalized) {
89
- // satisfied without running (e.g. the template already provides it)
90
- completed.push({ step, result: { status: 0 }, skipped: finalized.skip });
91
- continue;
92
- }
93
- args = finalized;
94
- }
95
- const result = exec(step.command, args, plan.env ? { cwd, env: plan.env } : { cwd });
96
- if (result.status !== 0) {
97
- if (step.optional) {
98
- const optionalFailure = result.status === null
99
- ? 'it did not start or was stopped (no exit code)'
100
- : `exit code: ${result.status} (its own error output is printed above)`;
101
- completed.push({ step, result, args, optionalFailure });
102
- continue;
103
- }
104
- return { ok: false, completed, failedStep: step, failedResult: result };
105
- }
106
- completed.push({ step, result, args });
107
- }
108
- return { ok: true, completed };
109
- }
110
- /** Renders the final message printed after `executePlan` returns — success prints the
111
- * plan's next-steps guidance; failure explains which step failed and how to recover,
112
- * explicitly noting the created directory is left in place (never auto-deleted). */
113
- export function renderExecuteReport(report, plan, workspaceName) {
114
- if (report.ok) {
115
- const skippedLines = report.completed
116
- .filter((c) => c.skipped !== undefined)
117
- .map((c) => ` skipped "${c.step.id}": ${c.skipped}`);
118
- const noteLines = report.completed.flatMap((c) => c.skipped === undefined && c.optionalFailure === undefined && c.args !== undefined && c.step.successNote
119
- ? [` note: ${c.step.successNote(c.args)}`]
120
- : []);
121
- const failedOptional = report.completed.filter((c) => c.optionalFailure !== undefined);
122
- const optionalLines = failedOptional.flatMap((c) => [
123
- ` optional step "${c.step.id}" failed — ${c.step.description}.`,
124
- ` ${c.optionalFailure}`,
125
- ...(c.step.failureHint ? [` ${c.step.failureHint}`] : []),
126
- ]);
127
- return [
128
- '',
129
- failedOptional.length === 0
130
- ? 'create-nx-archweave finished successfully.'
131
- : `create-nx-archweave finished — the workspace is complete, but ${failedOptional.length} optional step(s) failed:`,
132
- ...optionalLines,
133
- ...skippedLines,
134
- ...noteLines,
135
- '',
136
- 'Next steps:',
137
- ...plan.nextSteps.map((line) => ` ${line}`),
138
- ].join('\n');
139
- }
140
- const { failedStep, failedResult } = report;
141
- const detail = report.finalizeError
142
- ? `stopped before execution: ${report.finalizeError}`
143
- : `exit code: ${failedResult.status ?? 'unknown'} (the command's own error output is printed above)`;
144
- return [
145
- '',
146
- `create-nx-archweave stopped: step "${failedStep.id}" failed — ${failedStep.description}.`,
147
- ` ${detail}`,
148
- ...(!report.finalizeError && failedStep.failureHint ? [` ${failedStep.failureHint}`] : []),
149
- '',
150
- `The "${workspaceName}" directory (if it was created) was left in place — create-nx-archweave never deletes it automatically.`,
151
- `Inspect the error above, fix the issue, then either re-run the failed command yourself inside "${workspaceName}" or remove the directory and start over.`,
152
- ].join('\n');
153
- }
@@ -1,27 +0,0 @@
1
- import { AI_HOST_PROBES, AI_HOST_VALUES } from "./types.js";
2
- /** The version in a probe's output — `2.1.300 (Claude Code)` → `2.1.300`, `codex-cli
3
- * 0.162.1` → `0.162.1` (measured) — else its first line, shortened. */
4
- export function parseProbeVersion(output) {
5
- const firstLine = output.trim().split(/\r?\n/)[0]?.trim() ?? '';
6
- const version = /\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.]+)?/.exec(firstLine)?.[0];
7
- if (version)
8
- return version;
9
- return firstLine.length > 40 ? `${firstLine.slice(0, 39)}…` : firstLine;
10
- }
11
- /** Runs every host's probe at once (gemini alone takes ~2.4 s). A probe that cannot run
12
- * means "not found", never an error. */
13
- export async function detectInstalledHosts(probe) {
14
- const results = await Promise.all(AI_HOST_VALUES.map(async (host) => {
15
- try {
16
- return [host, await probe(AI_HOST_PROBES[host])];
17
- }
18
- catch {
19
- return [host, undefined];
20
- }
21
- }));
22
- const installed = {};
23
- for (const [host, output] of results)
24
- if (output !== undefined)
25
- installed[host] = parseProbeVersion(output);
26
- return installed;
27
- }
@@ -1,139 +0,0 @@
1
- import { parseMajor } from "./preflight.js";
2
- /** Question order: npm first (always installed), then the rest. */
3
- const SUPPORTED = ['npm', 'pnpm', 'yarn', 'bun'];
4
- export function isCliPackageManager(name) {
5
- return SUPPORTED.includes(name);
6
- }
7
- /** The package manager that started this process, from `npm_config_user_agent`
8
- * (`npm/10.9.4 node/v22 …`, `pnpm/11.9.0 npm/? node/v22 …`, `yarn/4.18.1 npm/? …`,
9
- * `bun/1.4.2 npm/? node/v26 …`) — set
10
- * by npm, pnpm, yarn and bun for everything they run, unset for a direct `node` call. */
11
- export function parseUserAgent(userAgent) {
12
- const match = /^([a-z][a-z0-9-]*)\/(\S+)/.exec(userAgent?.trim() ?? '');
13
- if (!match)
14
- return undefined;
15
- return { name: match[1], version: match[2] === '?' ? undefined : match[2] };
16
- }
17
- /**
18
- * The supported managers that are installed. npm always is (Node ships it); pnpm, yarn
19
- * and bun are when they run (a Windows `*.cmd` shim runs through cmd.exe), or when they
20
- * launched this command. The launcher's own version is used instead of probing it again.
21
- */
22
- export function listInstalledManagers(launcher, probe) {
23
- const installed = [];
24
- for (const name of SUPPORTED) {
25
- const version = launcher?.name === name ? launcher.version ?? '' : probe(name)?.trim();
26
- if (version !== undefined)
27
- installed.push({ name, version: version === '' ? undefined : version });
28
- // npm ships with Node, so it is listed even when its version cannot be read
29
- else if (name === 'npm')
30
- installed.push({ name, version: undefined });
31
- }
32
- return installed;
33
- }
34
- /**
35
- * Why this manager cannot create the chosen workspace, or undefined when it can.
36
- * Both known cases fail inside create-nx-workspace's own install of
37
- * Nx's react template, so a pick is never allowed — an unknown version is not held
38
- * against the manager.
39
- */
40
- export function managerIncompatibility(manager, frontend) {
41
- const major = parseMajor(manager.version);
42
- if (frontend !== 'react-vite' || major === undefined)
43
- return undefined;
44
- if (manager.name === 'npm' && major < 11)
45
- return "react-vite needs npm 11 — npm 10 crashes in Nx's react template";
46
- if (manager.name === 'yarn' && major < 2)
47
- return "react-vite needs yarn 2+ — yarn 1 cannot resolve the template's workspace:* packages";
48
- return undefined;
49
- }
50
- const NOTHING_CREATED = 'Nothing was created.';
51
- /**
52
- * Flag > (interactive) the question > launcher > npm. One rule everywhere: a manager
53
- * that cannot create the chosen workspace is never used — the question shows it with its
54
- * reason but does not offer it, and a flag or launcher naming it stops the run before
55
- * anything is created. Non-interactive runs launched by an unsupported manager are
56
- * refused the same way.
57
- */
58
- export async function choosePackageManager(input, ask, tell) {
59
- const launcher = parseUserAgent(input.userAgent);
60
- // flag / launcher / default: the version comes from the launcher when it is the same
61
- // manager, otherwise from a probe — so the compatibility rule can apply.
62
- const resolveNamed = (name, source) => {
63
- const version = launcher?.name === name && launcher.version ? launcher.version : input.probe(name)?.trim() || undefined;
64
- const reason = managerIncompatibility({ name, version }, input.frontend);
65
- if (reason) {
66
- return {
67
- error: `create-nx-archweave stopped before creating anything: ${name} ${version} cannot create this workspace — ${reason}. ` +
68
- `${INSTALL_ADVICE} ${NOTHING_CREATED}`,
69
- };
70
- }
71
- return { packageManager: name, source, version };
72
- };
73
- if (input.flag)
74
- return resolveNamed(input.flag, 'flag');
75
- if (!input.interactive) {
76
- if (!launcher)
77
- return resolveNamed('npm', 'default');
78
- if (!isCliPackageManager(launcher.name)) {
79
- return {
80
- error: `create-nx-archweave was started with ${launcher.name}, which it does not support (${SUPPORTED_TEXT} are). ` +
81
- `Re-run with npx, pnpm dlx, yarn dlx or bunx, or pass --package-manager=${SUPPORTED.join('|')}. ${NOTHING_CREATED}`,
82
- };
83
- }
84
- return resolveNamed(launcher.name, 'launcher');
85
- }
86
- if (launcher && !isCliPackageManager(launcher.name)) {
87
- tell(`${launcher.name} is not supported (${SUPPORTED_TEXT} are) — choosing from the supported ones instead.`);
88
- }
89
- const installed = listInstalledManagers(launcher, input.probe);
90
- const reasons = installed.map((manager) => managerIncompatibility(manager, input.frontend));
91
- const selectable = installed.filter((_, index) => reasons[index] === undefined);
92
- const unavailable = installed.filter((_, index) => reasons[index] !== undefined);
93
- const unavailableLines = unavailable.map((manager) => ` ${manager.name.padEnd(6)}${manager.version ?? ''} — not available: ${reasons[installed.indexOf(manager)]}`);
94
- if (selectable.length === 0) {
95
- return {
96
- error: ['create-nx-archweave stopped before creating anything: no installed package manager can create this workspace.', ...unavailableLines, `${INSTALL_ADVICE} ${NOTHING_CREATED}`].join('\n'),
97
- };
98
- }
99
- // Nothing to choose: the plan print names the manager (and why) next.
100
- if (selectable.length === 1) {
101
- if (unavailable.length > 0)
102
- tell(['Package manager:', ...unavailableLines].join('\n'));
103
- return { packageManager: selectable[0].name, source: unavailable.length > 0 ? 'only-compatible' : 'only-installed', version: selectable[0].version };
104
- }
105
- const defaultIndex = Math.max(0, selectable.findIndex((manager) => manager.name === launcher?.name));
106
- tell('? Package manager');
107
- selectable.forEach((manager, index) => {
108
- const head = ` ${index + 1}) ${manager.name.padEnd(6)}${manager.version ?? ''}`.trimEnd();
109
- tell(index === defaultIndex ? `${head} (default)` : head);
110
- });
111
- unavailableLines.forEach((line) => tell(line));
112
- for (;;) {
113
- const answer = (await ask(`Choose 1-${selectable.length} [${defaultIndex + 1}]: `)).trim().toLowerCase();
114
- const byNumber = /^\d+$/.test(answer) ? Number(answer) - 1 : -1;
115
- const index = answer === '' ? defaultIndex : byNumber >= 0 ? byNumber : selectable.findIndex((manager) => manager.name === answer);
116
- if (index >= 0 && index < selectable.length) {
117
- return { packageManager: selectable[index].name, source: 'asked', version: selectable[index].version };
118
- }
119
- const blocked = unavailable.find((manager) => manager.name === answer);
120
- tell(blocked
121
- ? `${blocked.name} is not available here: ${reasons[installed.indexOf(blocked)]}.`
122
- : `Choose a number from 1 to ${selectable.length} (or a name: ${selectable.map((manager) => manager.name).join(', ')}).`);
123
- }
124
- }
125
- const SUPPORTED_TEXT = 'npm, pnpm, yarn and bun';
126
- const INSTALL_ADVICE = 'Use npm 11+, pnpm, yarn 2+ (Corepack: corepack enable) or bun, or pass --package-manager= with one of them.';
127
- const SOURCE_TEXT = {
128
- flag: '--package-manager',
129
- launcher: 'the package manager that ran this command',
130
- default: 'default — no launching package manager detected',
131
- asked: 'chosen',
132
- 'only-installed': 'the only one installed',
133
- 'only-compatible': 'the only installed one that can create this workspace',
134
- };
135
- /** `npm 11.6.2 (the package manager that ran this command)` — for the plan print. */
136
- export function describeResolution(resolution) {
137
- const name = resolution.version ? `${resolution.packageManager} ${resolution.version}` : resolution.packageManager;
138
- return `${name} (${SOURCE_TEXT[resolution.source]})`;
139
- }