create-nx-archweave 0.1.1 → 0.2.1

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`
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
@@ -218,7 +220,8 @@ function parseCliArgs(argv) {
218
220
  plugins: { type: "string" },
219
221
  "review-provider": { type: "string" },
220
222
  "plugin-source": { type: "string" },
221
- "package-manager": { type: "string" }
223
+ "package-manager": { type: "string" },
224
+ "allow-nested": { type: "boolean" }
222
225
  };
223
226
  let parsed;
224
227
  try {
@@ -309,11 +312,31 @@ function parseCliArgs(argv) {
309
312
  reviewProvider,
310
313
  review,
311
314
  pluginSource: parsed.values["plugin-source"],
312
- packageManager
315
+ packageManager,
316
+ allowNested: parsed.values["allow-nested"] === true
313
317
  }
314
318
  };
315
319
  }
316
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
+
317
340
  // src/cli/prompts.ts
318
341
  async function askChoice(ask, question, choices) {
319
342
  for (; ; ) {
@@ -469,6 +492,7 @@ function renderUsage() {
469
492
  " --review, --no-review Force AI PR review on/off (quick-start asks y/N; solo/team: on)",
470
493
  " --review-provider <name> claude, gemini, gpt or cursor (asked when review is on; default: the first AI tool's)",
471
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",
472
496
  " --package-manager <name> npm, pnpm, yarn or bun (asked if omitted; without a terminal: the one that ran this command, else npm)",
473
497
  " -h, --help Show this help",
474
498
  " -v, --version Show the CLI version",
@@ -1043,6 +1067,13 @@ async function main(argv) {
1043
1067
  return 1;
1044
1068
  }
1045
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
+ }
1046
1077
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1047
1078
  const ask = (question) => {
1048
1079
  if (!process.stdin.isTTY) {
@@ -1188,3 +1219,7 @@ main(process.argv.slice(2)).then((code) => {
1188
1219
  console.error(error instanceof Error ? error.stack ?? error.message : String(error));
1189
1220
  process.exitCode = 1;
1190
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';
package/dist/index.js CHANGED
@@ -115,7 +115,8 @@ function parseCliArgs(argv) {
115
115
  plugins: { type: "string" },
116
116
  "review-provider": { type: "string" },
117
117
  "plugin-source": { type: "string" },
118
- "package-manager": { type: "string" }
118
+ "package-manager": { type: "string" },
119
+ "allow-nested": { type: "boolean" }
119
120
  };
120
121
  let parsed;
121
122
  try {
@@ -206,7 +207,8 @@ function parseCliArgs(argv) {
206
207
  reviewProvider,
207
208
  review,
208
209
  pluginSource: parsed.values["plugin-source"],
209
- packageManager
210
+ packageManager,
211
+ allowNested: parsed.values["allow-nested"] === true
210
212
  }
211
213
  };
212
214
  }
@@ -366,6 +368,7 @@ function renderUsage() {
366
368
  " --review, --no-review Force AI PR review on/off (quick-start asks y/N; solo/team: on)",
367
369
  " --review-provider <name> claude, gemini, gpt or cursor (asked when review is on; default: the first AI tool's)",
368
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",
369
372
  " --package-manager <name> npm, pnpm, yarn or bun (asked if omitted; without a terminal: the one that ran this command, else npm)",
370
373
  " -h, --help Show this help",
371
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.1",
3
+ "version": "0.2.1",
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)",
@@ -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
- }
@@ -1,171 +0,0 @@
1
- import { parseArgs as parseNodeArgs } from 'node:util';
2
- import { AI_HOST_VALUES, BACKEND_VALUES, FRONTEND_VALUES, LOCALE_VALUES, PLUGIN_VALUES, PRESET_VALUES, REVIEW_PROVIDER_VALUES, } from "./types.js";
3
- import { isCliPackageManager } from "./package-manager.js";
4
- import { invalidWorkspaceNameMessage, workspaceNameProblem } from "./workspace-name.js";
5
- /** The string-valued flags declared below, in their separated two-token form
6
- * (`--preset solo`). extractReviewFlag must never treat the token AFTER one of
7
- * these as a review flag — it is that flag's value, so both tokens pass through
8
- * to parseArgs untouched. parseArgs then consumes it as the value, or, for a
9
- * dash-leading value like `--plugin-source --review`, rejects it with its own
10
- * clear "argument is ambiguous / use --plugin-source=-XYZ" error — either way
11
- * it is never silently reinterpreted as the review flag. */
12
- const STRING_VALUE_FLAGS = new Set(['--preset', '--frontend', '--backend', '--locale', '--ai-hosts', '--plugins', '--review-provider', '--plugin-source', '--package-manager']);
13
- /** node:util's parseArgs has no built-in `--no-foo` negation (verified locally: it
14
- * throws "Unknown option '--no-review'" when `review` is declared as a plain boolean
15
- * option), so --review/--no-review/--review=true|false are hand-rolled here and
16
- * stripped out before the rest of argv reaches parseArgs. */
17
- function extractReviewFlag(argv) {
18
- const rest = [];
19
- let review;
20
- let error;
21
- for (let i = 0; i < argv.length; i++) {
22
- const token = argv[i];
23
- if (STRING_VALUE_FLAGS.has(token)) {
24
- rest.push(token);
25
- if (i + 1 < argv.length) {
26
- rest.push(argv[i + 1]);
27
- i++;
28
- }
29
- }
30
- else if (token === '--review') {
31
- review = true;
32
- }
33
- else if (token === '--no-review') {
34
- review = false;
35
- }
36
- else if (token.startsWith('--review=')) {
37
- const value = token.slice('--review='.length);
38
- if (value === 'true')
39
- review = true;
40
- else if (value === 'false')
41
- review = false;
42
- else
43
- error ??= `Invalid --review value "${value}". Expected "true" or "false".`;
44
- }
45
- else {
46
- rest.push(token);
47
- }
48
- }
49
- return { review, error, rest };
50
- }
51
- function invalidChoiceMessage(flag, value, choices) {
52
- return `Invalid ${flag} value "${value}". Expected one of: ${choices.join(', ')}`;
53
- }
54
- /** Pure flag parsing: no I/O, no prompting — bin.ts decides what to do with each
55
- * outcome (print help/version, print an error and exit non-zero, or hand `args` to
56
- * prompts.ts for anything still missing). */
57
- export function parseCliArgs(argv) {
58
- const { review, error: reviewError, rest } = extractReviewFlag(argv);
59
- if (reviewError)
60
- return { kind: 'error', message: reviewError };
61
- const options = {
62
- help: { type: 'boolean', short: 'h' },
63
- version: { type: 'boolean', short: 'v' },
64
- preset: { type: 'string' },
65
- frontend: { type: 'string' },
66
- backend: { type: 'string' },
67
- locale: { type: 'string' },
68
- 'ai-hosts': { type: 'string' },
69
- plugins: { type: 'string' },
70
- 'review-provider': { type: 'string' },
71
- 'plugin-source': { type: 'string' },
72
- 'package-manager': { type: 'string' },
73
- };
74
- let parsed;
75
- try {
76
- parsed = parseNodeArgs({ args: rest, allowPositionals: true, options });
77
- }
78
- catch (error) {
79
- return { kind: 'error', message: error instanceof Error ? error.message : String(error) };
80
- }
81
- if (parsed.values.help)
82
- return { kind: 'help' };
83
- if (parsed.values.version)
84
- return { kind: 'version' };
85
- if (parsed.positionals.length > 1) {
86
- return {
87
- kind: 'error',
88
- message: `Unexpected extra argument(s): ${parsed.positionals.slice(1).join(', ')}`,
89
- };
90
- }
91
- const name = parsed.positionals[0];
92
- const nameProblem = name === undefined ? undefined : workspaceNameProblem(name);
93
- if (name !== undefined && nameProblem !== undefined) {
94
- return { kind: 'error', message: invalidWorkspaceNameMessage(name, nameProblem) };
95
- }
96
- const preset = parsed.values.preset;
97
- if (preset !== undefined && !PRESET_VALUES.includes(preset)) {
98
- return { kind: 'error', message: invalidChoiceMessage('--preset', preset, PRESET_VALUES) };
99
- }
100
- const frontend = parsed.values.frontend;
101
- if (frontend !== undefined && !FRONTEND_VALUES.includes(frontend)) {
102
- return { kind: 'error', message: invalidChoiceMessage('--frontend', frontend, FRONTEND_VALUES) };
103
- }
104
- const backend = parsed.values.backend;
105
- if (backend !== undefined && !BACKEND_VALUES.includes(backend)) {
106
- return { kind: 'error', message: invalidChoiceMessage('--backend', backend, BACKEND_VALUES) };
107
- }
108
- // A workspace needs something to build: no frontend means a backend-only workspace.
109
- if (frontend === 'none' && backend === 'none') {
110
- return { kind: 'error', message: '--frontend=none creates a backend-only workspace: pass --backend=express or --backend=nest.' };
111
- }
112
- const locale = parsed.values.locale;
113
- if (locale !== undefined && !LOCALE_VALUES.includes(locale)) {
114
- return { kind: 'error', message: invalidChoiceMessage('--locale', locale, LOCALE_VALUES) };
115
- }
116
- const aiHostsRaw = parsed.values['ai-hosts'];
117
- let aiHosts;
118
- if (aiHostsRaw !== undefined) {
119
- const tokens = aiHostsRaw.split(',').map((token) => token.trim().toLowerCase()).filter((token) => token.length > 0);
120
- const unknown = tokens.find((token) => !AI_HOST_VALUES.includes(token));
121
- if (tokens.length === 0 || unknown !== undefined) {
122
- return { kind: 'error', message: invalidChoiceMessage('--ai-hosts', unknown ?? aiHostsRaw, AI_HOST_VALUES) + ' (comma-separated)' };
123
- }
124
- aiHosts = [...new Set(tokens)];
125
- }
126
- const pluginsRaw = parsed.values.plugins;
127
- let plugins;
128
- if (pluginsRaw !== undefined) {
129
- const tokens = pluginsRaw.split(',').map((token) => token.trim().toLowerCase()).filter((token) => token.length > 0);
130
- if (tokens.length === 1 && tokens[0] === 'none') {
131
- plugins = [];
132
- }
133
- else {
134
- const unknown = tokens.find((token) => !PLUGIN_VALUES.includes(token));
135
- if (tokens.length === 0 || unknown !== undefined) {
136
- return {
137
- kind: 'error',
138
- message: invalidChoiceMessage('--plugins', unknown ?? pluginsRaw, [...PLUGIN_VALUES, 'none']) + ' (comma-separated)',
139
- };
140
- }
141
- plugins = [...new Set(tokens)];
142
- }
143
- }
144
- const reviewProvider = parsed.values['review-provider'];
145
- if (reviewProvider !== undefined && !REVIEW_PROVIDER_VALUES.includes(reviewProvider)) {
146
- return { kind: 'error', message: invalidChoiceMessage('--review-provider', reviewProvider, REVIEW_PROVIDER_VALUES) };
147
- }
148
- const packageManager = parsed.values['package-manager'];
149
- if (packageManager !== undefined && !isCliPackageManager(packageManager)) {
150
- return {
151
- kind: 'error',
152
- message: `--package-manager "${packageManager}" is not supported — create-nx-archweave supports npm, pnpm, yarn and bun.`,
153
- };
154
- }
155
- return {
156
- kind: 'ok',
157
- args: {
158
- name,
159
- preset: preset,
160
- frontend: frontend,
161
- backend: backend,
162
- locale: locale,
163
- aiHosts,
164
- plugins,
165
- reviewProvider: reviewProvider,
166
- review,
167
- pluginSource: parsed.values['plugin-source'],
168
- packageManager,
169
- },
170
- };
171
- }