create-nx-archweave 0.1.1 → 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 +1 -0
- package/dist/bin.js +37 -2
- package/dist/cli/location-check.d.ts +23 -0
- package/dist/cli/parse-args.d.ts +3 -0
- package/dist/index.js +5 -2
- package/package.json +1 -1
- package/dist/cli/execute.js +0 -153
- package/dist/cli/host-probe.js +0 -27
- package/dist/cli/package-manager.js +0 -139
- package/dist/cli/parse-args.js +0 -171
- package/dist/cli/plan.js +0 -434
- package/dist/cli/plugins.js +0 -74
- package/dist/cli/preflight.js +0 -7
- package/dist/cli/prompts.js +0 -150
- package/dist/cli/spawnable-command.js +0 -71
- package/dist/cli/types.js +0 -53
- package/dist/cli/workspace-name.js +0 -29
- package/dist/render-usage.js +0 -71
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
|
package/dist/cli/parse-args.d.ts
CHANGED
|
@@ -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.
|
|
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)",
|
package/dist/cli/execute.js
DELETED
|
@@ -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
|
-
}
|
package/dist/cli/host-probe.js
DELETED
|
@@ -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
|
-
}
|
package/dist/cli/parse-args.js
DELETED
|
@@ -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
|
-
}
|