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 +2 -1
- package/dist/bin.js +60 -2
- package/dist/cli/location-check.d.ts +23 -0
- package/dist/cli/parse-args.d.ts +3 -0
- package/dist/cli/workspace-name.d.ts +9 -0
- package/dist/index.js +28 -2
- package/package.json +1 -4
- 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 -166
- 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/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`
|
|
@@ -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
|
|
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
|
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';
|
|
@@ -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.
|
|
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
|
}
|
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
|
-
}
|