create-nx-archweave 0.3.2 → 0.3.3
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/dist/_chunks/chunk-WIJBUA2L.js +981 -0
- package/dist/bin.js +26 -947
- package/dist/index.js +37 -878
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,284 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AI_HOST_CHOICES,
|
|
4
|
+
AI_HOST_PROBES,
|
|
5
|
+
AI_HOST_VALUES,
|
|
6
|
+
PLUGIN_CHOICES,
|
|
7
|
+
buildPlan,
|
|
8
|
+
choosePackageManager,
|
|
9
|
+
collectMissingInputs,
|
|
10
|
+
createProcessExec,
|
|
11
|
+
describeResolution,
|
|
12
|
+
executePlan,
|
|
13
|
+
nxGenerateCommand,
|
|
14
|
+
parseCliArgs,
|
|
15
|
+
renderExecuteReport,
|
|
16
|
+
renderStepTimings,
|
|
17
|
+
renderUsage,
|
|
18
|
+
renderVersion,
|
|
19
|
+
reviewProviderConflict,
|
|
20
|
+
spawnableCommand
|
|
21
|
+
} from "./_chunks/chunk-WIJBUA2L.js";
|
|
2
22
|
|
|
3
23
|
// src/bin.ts
|
|
4
|
-
import { execFile, spawnSync
|
|
5
|
-
import { existsSync
|
|
6
|
-
import { join
|
|
24
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
25
|
+
import { existsSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
7
27
|
import { createInterface } from "node:readline/promises";
|
|
8
28
|
|
|
9
|
-
// src/cli/parse-args.ts
|
|
10
|
-
import { parseArgs as parseNodeArgs } from "node:util";
|
|
11
|
-
|
|
12
|
-
// src/cli/types.ts
|
|
13
|
-
var PRESET_VALUES = ["quick-start", "solo", "team"];
|
|
14
|
-
var FRONTEND_VALUES = ["react-vite", "next", "none"];
|
|
15
|
-
var BACKEND_VALUES = ["express", "nest", "none"];
|
|
16
|
-
var LOCALE_VALUES = ["ko", "en"];
|
|
17
|
-
var AI_HOST_CHOICES = [
|
|
18
|
-
{ id: "claude-code", label: "Claude Code", nxAgent: "claude", reviewProvider: "claude" },
|
|
19
|
-
{ id: "codex", label: "Codex", nxAgent: "codex", reviewProvider: "gpt" },
|
|
20
|
-
{ id: "cursor", label: "Cursor", nxAgent: "cursor", reviewProvider: "cursor" },
|
|
21
|
-
{ id: "gemini-cli", label: "Gemini CLI", nxAgent: "gemini", reviewProvider: "gemini" }
|
|
22
|
-
];
|
|
23
|
-
var REVIEW_PROVIDER_CHOICES = [
|
|
24
|
-
{ id: "claude", label: "Claude" },
|
|
25
|
-
{ id: "gemini", label: "Gemini (beta)" },
|
|
26
|
-
{ id: "gpt", label: "GPT (beta)" },
|
|
27
|
-
{ id: "cursor", label: "Cursor (beta)" }
|
|
28
|
-
];
|
|
29
|
-
var REVIEW_PROVIDER_VALUES = REVIEW_PROVIDER_CHOICES.map((provider) => provider.id);
|
|
30
|
-
var AI_HOST_VALUES = AI_HOST_CHOICES.map((host) => host.id);
|
|
31
|
-
var DEFAULT_AI_HOSTS = ["claude-code"];
|
|
32
|
-
var AI_HOST_PROBES = {
|
|
33
|
-
"claude-code": "claude",
|
|
34
|
-
codex: "codex",
|
|
35
|
-
cursor: "cursor-agent",
|
|
36
|
-
"gemini-cli": "gemini"
|
|
37
|
-
};
|
|
38
|
-
var PLUGIN_CHOICES = [
|
|
39
|
-
{
|
|
40
|
-
id: "ponytail",
|
|
41
|
-
label: "ponytail",
|
|
42
|
-
description: 'over-engineering review rules and skills \u2014 "makes your AI agent think like the laziest senior dev in the room"',
|
|
43
|
-
license: "MIT",
|
|
44
|
-
host: "claude-code"
|
|
45
|
-
}
|
|
46
|
-
];
|
|
47
|
-
var PLUGIN_VALUES = PLUGIN_CHOICES.map((plugin) => plugin.id);
|
|
48
|
-
|
|
49
|
-
// src/cli/package-manager.ts
|
|
50
|
-
var SUPPORTED = ["npm", "pnpm", "yarn", "bun"];
|
|
51
|
-
function isCliPackageManager(name) {
|
|
52
|
-
return SUPPORTED.includes(name);
|
|
53
|
-
}
|
|
54
|
-
function parseUserAgent(userAgent) {
|
|
55
|
-
const match = /^([a-z][a-z0-9-]*)\/(\S+)/.exec(userAgent?.trim() ?? "");
|
|
56
|
-
if (!match) return void 0;
|
|
57
|
-
return { name: match[1], version: match[2] === "?" ? void 0 : match[2] };
|
|
58
|
-
}
|
|
59
|
-
function listInstalledManagers(launcher, probe) {
|
|
60
|
-
const installed = [];
|
|
61
|
-
for (const name of SUPPORTED) {
|
|
62
|
-
const version = launcher?.name === name ? launcher.version ?? "" : probe(name)?.trim();
|
|
63
|
-
if (version !== void 0) installed.push({ name, version: version === "" ? void 0 : version });
|
|
64
|
-
else if (name === "npm") installed.push({ name, version: void 0 });
|
|
65
|
-
}
|
|
66
|
-
return installed;
|
|
67
|
-
}
|
|
68
|
-
var NOTHING_CREATED = "Nothing was created.";
|
|
69
|
-
async function choosePackageManager(input, ask, tell) {
|
|
70
|
-
const launcher = parseUserAgent(input.userAgent);
|
|
71
|
-
const resolveNamed = (name, source) => {
|
|
72
|
-
const version = launcher?.name === name && launcher.version ? launcher.version : input.probe(name)?.trim() || void 0;
|
|
73
|
-
return { packageManager: name, source, version };
|
|
74
|
-
};
|
|
75
|
-
if (input.flag) return resolveNamed(input.flag, "flag");
|
|
76
|
-
if (!input.interactive) {
|
|
77
|
-
if (!launcher) 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). Re-run with npx, pnpm dlx, yarn dlx or bunx, or pass --package-manager=${SUPPORTED.join("|")}. ${NOTHING_CREATED}`
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
return resolveNamed(launcher.name, "launcher");
|
|
84
|
-
}
|
|
85
|
-
if (launcher && !isCliPackageManager(launcher.name)) {
|
|
86
|
-
tell(`${launcher.name} is not supported (${SUPPORTED_TEXT} are) \u2014 choosing from the supported ones instead.`);
|
|
87
|
-
}
|
|
88
|
-
const installed = listInstalledManagers(launcher, input.probe);
|
|
89
|
-
if (installed.length === 1) return { packageManager: installed[0].name, source: "only-installed", version: installed[0].version };
|
|
90
|
-
const defaultIndex = Math.max(0, installed.findIndex((manager) => manager.name === launcher?.name));
|
|
91
|
-
tell("? Package manager");
|
|
92
|
-
installed.forEach((manager, index) => {
|
|
93
|
-
const head = ` ${index + 1}) ${manager.name.padEnd(6)}${manager.version ?? ""}`.trimEnd();
|
|
94
|
-
tell(index === defaultIndex ? `${head} (default)` : head);
|
|
95
|
-
});
|
|
96
|
-
for (; ; ) {
|
|
97
|
-
const answer = (await ask(`Choose 1-${installed.length} [${defaultIndex + 1}]: `)).trim().toLowerCase();
|
|
98
|
-
const byNumber = /^\d+$/.test(answer) ? Number(answer) - 1 : -1;
|
|
99
|
-
const index = answer === "" ? defaultIndex : byNumber >= 0 ? byNumber : installed.findIndex((manager) => manager.name === answer);
|
|
100
|
-
if (index >= 0 && index < installed.length) {
|
|
101
|
-
return { packageManager: installed[index].name, source: "asked", version: installed[index].version };
|
|
102
|
-
}
|
|
103
|
-
tell(`Choose a number from 1 to ${installed.length} (or a name: ${installed.map((manager) => manager.name).join(", ")}).`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
var SUPPORTED_TEXT = "npm, pnpm, yarn and bun";
|
|
107
|
-
var SOURCE_TEXT = {
|
|
108
|
-
flag: "--package-manager",
|
|
109
|
-
launcher: "the package manager that ran this command",
|
|
110
|
-
default: "default \u2014 no launching package manager detected",
|
|
111
|
-
asked: "chosen",
|
|
112
|
-
"only-installed": "the only one installed"
|
|
113
|
-
};
|
|
114
|
-
function describeResolution(resolution) {
|
|
115
|
-
const name = resolution.version ? `${resolution.packageManager} ${resolution.version}` : resolution.packageManager;
|
|
116
|
-
return `${name} (${SOURCE_TEXT[resolution.source]})`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// src/cli/workspace-name.ts
|
|
120
|
-
var MAX_LENGTH = 214;
|
|
121
|
-
var EXAMPLE = "my-app";
|
|
122
|
-
function workspaceNameProblem(name) {
|
|
123
|
-
if (name.length === 0) return "it is empty";
|
|
124
|
-
if (/\s/.test(name)) return "it contains whitespace";
|
|
125
|
-
if (name.length > MAX_LENGTH) return `it is ${name.length} characters long, and npm allows at most ${MAX_LENGTH}`;
|
|
126
|
-
if (name.startsWith(".") || name.startsWith("_")) return `it starts with "${name[0]}"`;
|
|
127
|
-
if (/[A-Z]/.test(name)) return "it contains uppercase letters, and npm package names are lowercase";
|
|
128
|
-
const others = [...new Set(name.replace(/[a-z0-9._-]/g, ""))];
|
|
129
|
-
if (others.length > 0) {
|
|
130
|
-
return `it contains ${others.map((char) => `"${char}"`).join(", ")} \u2014 only lowercase letters, digits, "-", "_" and "." are allowed`;
|
|
131
|
-
}
|
|
132
|
-
return void 0;
|
|
133
|
-
}
|
|
134
|
-
function invalidWorkspaceNameMessage(name, problem) {
|
|
135
|
-
return `Invalid workspace name "${name}": ${problem}. For example: ${EXAMPLE}`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// src/cli/parse-args.ts
|
|
139
|
-
var STRING_VALUE_FLAGS = /* @__PURE__ */ new Set(["--preset", "--frontend", "--backend", "--locale", "--ai-hosts", "--plugins", "--review-provider", "--plugin-source", "--package-manager"]);
|
|
140
|
-
function extractReviewFlag(argv) {
|
|
141
|
-
const rest = [];
|
|
142
|
-
let review;
|
|
143
|
-
let error;
|
|
144
|
-
for (let i = 0; i < argv.length; i++) {
|
|
145
|
-
const token = argv[i];
|
|
146
|
-
if (STRING_VALUE_FLAGS.has(token)) {
|
|
147
|
-
rest.push(token);
|
|
148
|
-
if (i + 1 < argv.length) {
|
|
149
|
-
rest.push(argv[i + 1]);
|
|
150
|
-
i++;
|
|
151
|
-
}
|
|
152
|
-
} else if (token === "--review") {
|
|
153
|
-
review = true;
|
|
154
|
-
} else if (token === "--no-review") {
|
|
155
|
-
review = false;
|
|
156
|
-
} else if (token.startsWith("--review=")) {
|
|
157
|
-
const value = token.slice("--review=".length);
|
|
158
|
-
if (value === "true") review = true;
|
|
159
|
-
else if (value === "false") review = false;
|
|
160
|
-
else error ??= `Invalid --review value "${value}". Expected "true" or "false".`;
|
|
161
|
-
} else {
|
|
162
|
-
rest.push(token);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return { review, error, rest };
|
|
166
|
-
}
|
|
167
|
-
function invalidChoiceMessage(flag, value, choices) {
|
|
168
|
-
return `Invalid ${flag} value "${value}". Expected one of: ${choices.join(", ")}`;
|
|
169
|
-
}
|
|
170
|
-
function parseCliArgs(argv) {
|
|
171
|
-
const { review, error: reviewError, rest } = extractReviewFlag(argv);
|
|
172
|
-
if (reviewError) return { kind: "error", message: reviewError };
|
|
173
|
-
const options = {
|
|
174
|
-
help: { type: "boolean", short: "h" },
|
|
175
|
-
version: { type: "boolean", short: "v" },
|
|
176
|
-
preset: { type: "string" },
|
|
177
|
-
frontend: { type: "string" },
|
|
178
|
-
backend: { type: "string" },
|
|
179
|
-
locale: { type: "string" },
|
|
180
|
-
"ai-hosts": { type: "string" },
|
|
181
|
-
plugins: { type: "string" },
|
|
182
|
-
"review-provider": { type: "string" },
|
|
183
|
-
"plugin-source": { type: "string" },
|
|
184
|
-
"package-manager": { type: "string" },
|
|
185
|
-
"allow-nested": { type: "boolean" }
|
|
186
|
-
};
|
|
187
|
-
let parsed;
|
|
188
|
-
try {
|
|
189
|
-
parsed = parseNodeArgs({ args: rest, allowPositionals: true, options });
|
|
190
|
-
} catch (error) {
|
|
191
|
-
return { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
|
192
|
-
}
|
|
193
|
-
if (parsed.values.help) return { kind: "help" };
|
|
194
|
-
if (parsed.values.version) return { kind: "version" };
|
|
195
|
-
if (parsed.positionals.length > 1) {
|
|
196
|
-
return {
|
|
197
|
-
kind: "error",
|
|
198
|
-
message: `Unexpected extra argument(s): ${parsed.positionals.slice(1).join(", ")}`
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
const name = parsed.positionals[0];
|
|
202
|
-
const nameProblem = name === void 0 ? void 0 : workspaceNameProblem(name);
|
|
203
|
-
if (name !== void 0 && nameProblem !== void 0) {
|
|
204
|
-
return { kind: "error", message: invalidWorkspaceNameMessage(name, nameProblem) };
|
|
205
|
-
}
|
|
206
|
-
const preset = parsed.values.preset;
|
|
207
|
-
if (preset !== void 0 && !PRESET_VALUES.includes(preset)) {
|
|
208
|
-
return { kind: "error", message: invalidChoiceMessage("--preset", preset, PRESET_VALUES) };
|
|
209
|
-
}
|
|
210
|
-
const frontend = parsed.values.frontend;
|
|
211
|
-
if (frontend !== void 0 && !FRONTEND_VALUES.includes(frontend)) {
|
|
212
|
-
return { kind: "error", message: invalidChoiceMessage("--frontend", frontend, FRONTEND_VALUES) };
|
|
213
|
-
}
|
|
214
|
-
const backend = parsed.values.backend;
|
|
215
|
-
if (backend !== void 0 && !BACKEND_VALUES.includes(backend)) {
|
|
216
|
-
return { kind: "error", message: invalidChoiceMessage("--backend", backend, BACKEND_VALUES) };
|
|
217
|
-
}
|
|
218
|
-
if (frontend === "none" && backend === "none") {
|
|
219
|
-
return { kind: "error", message: "--frontend=none creates a backend-only workspace: pass --backend=express or --backend=nest." };
|
|
220
|
-
}
|
|
221
|
-
const locale = parsed.values.locale;
|
|
222
|
-
if (locale !== void 0 && !LOCALE_VALUES.includes(locale)) {
|
|
223
|
-
return { kind: "error", message: invalidChoiceMessage("--locale", locale, LOCALE_VALUES) };
|
|
224
|
-
}
|
|
225
|
-
const aiHostsRaw = parsed.values["ai-hosts"];
|
|
226
|
-
let aiHosts;
|
|
227
|
-
if (aiHostsRaw !== void 0) {
|
|
228
|
-
const tokens = aiHostsRaw.split(",").map((token) => token.trim().toLowerCase()).filter((token) => token.length > 0);
|
|
229
|
-
const unknown = tokens.find((token) => !AI_HOST_VALUES.includes(token));
|
|
230
|
-
if (tokens.length === 0 || unknown !== void 0) {
|
|
231
|
-
return { kind: "error", message: invalidChoiceMessage("--ai-hosts", unknown ?? aiHostsRaw, AI_HOST_VALUES) + " (comma-separated)" };
|
|
232
|
-
}
|
|
233
|
-
aiHosts = [...new Set(tokens)];
|
|
234
|
-
}
|
|
235
|
-
const pluginsRaw = parsed.values.plugins;
|
|
236
|
-
let plugins;
|
|
237
|
-
if (pluginsRaw !== void 0) {
|
|
238
|
-
const tokens = pluginsRaw.split(",").map((token) => token.trim().toLowerCase()).filter((token) => token.length > 0);
|
|
239
|
-
if (tokens.length === 1 && tokens[0] === "none") {
|
|
240
|
-
plugins = [];
|
|
241
|
-
} else {
|
|
242
|
-
const unknown = tokens.find((token) => !PLUGIN_VALUES.includes(token));
|
|
243
|
-
if (tokens.length === 0 || unknown !== void 0) {
|
|
244
|
-
return {
|
|
245
|
-
kind: "error",
|
|
246
|
-
message: invalidChoiceMessage("--plugins", unknown ?? pluginsRaw, [...PLUGIN_VALUES, "none"]) + " (comma-separated)"
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
plugins = [...new Set(tokens)];
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
const reviewProvider = parsed.values["review-provider"];
|
|
253
|
-
if (reviewProvider !== void 0 && !REVIEW_PROVIDER_VALUES.includes(reviewProvider)) {
|
|
254
|
-
return { kind: "error", message: invalidChoiceMessage("--review-provider", reviewProvider, REVIEW_PROVIDER_VALUES) };
|
|
255
|
-
}
|
|
256
|
-
const packageManager = parsed.values["package-manager"];
|
|
257
|
-
if (packageManager !== void 0 && !isCliPackageManager(packageManager)) {
|
|
258
|
-
return {
|
|
259
|
-
kind: "error",
|
|
260
|
-
message: `--package-manager "${packageManager}" is not supported \u2014 create-nx-archweave supports npm, pnpm, yarn and bun.`
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
return {
|
|
264
|
-
kind: "ok",
|
|
265
|
-
args: {
|
|
266
|
-
name,
|
|
267
|
-
preset,
|
|
268
|
-
frontend,
|
|
269
|
-
backend,
|
|
270
|
-
locale,
|
|
271
|
-
aiHosts,
|
|
272
|
-
plugins,
|
|
273
|
-
reviewProvider,
|
|
274
|
-
review,
|
|
275
|
-
pluginSource: parsed.values["plugin-source"],
|
|
276
|
-
packageManager,
|
|
277
|
-
allowNested: parsed.values["allow-nested"] === true
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
29
|
// src/cli/location-check.ts
|
|
283
30
|
function checkInvocationDirectory(dir, probe) {
|
|
284
31
|
const gitRoot2 = probe.gitRoot(dir);
|
|
@@ -298,477 +45,6 @@ function renderLocationRefusal(check) {
|
|
|
298
45
|
].join("\n");
|
|
299
46
|
}
|
|
300
47
|
|
|
301
|
-
// src/cli/prompts.ts
|
|
302
|
-
async function askChoice(ask, question, choices) {
|
|
303
|
-
for (; ; ) {
|
|
304
|
-
const answer = (await ask(question)).trim();
|
|
305
|
-
if (choices.includes(answer)) return answer;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
function parseYesNo(answer) {
|
|
309
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
310
|
-
}
|
|
311
|
-
function parseAiHostAnswer(answer) {
|
|
312
|
-
const tokens = answer.split(/[\s,]+/).filter((token) => token.length > 0);
|
|
313
|
-
if (tokens.length === 0) return [...DEFAULT_AI_HOSTS];
|
|
314
|
-
const picked = [];
|
|
315
|
-
for (const token of tokens) {
|
|
316
|
-
const byNumber = /^\d+$/.test(token) ? AI_HOST_CHOICES[Number(token) - 1] : void 0;
|
|
317
|
-
const host = byNumber ?? AI_HOST_CHOICES.find((choice) => choice.id === token.toLowerCase());
|
|
318
|
-
if (!host) return void 0;
|
|
319
|
-
if (!picked.includes(host.id)) picked.push(host.id);
|
|
320
|
-
}
|
|
321
|
-
return picked;
|
|
322
|
-
}
|
|
323
|
-
function hostStatus(host, installed) {
|
|
324
|
-
if (installed === void 0) return "";
|
|
325
|
-
const version = installed[host];
|
|
326
|
-
if (version === void 0) return "not found";
|
|
327
|
-
return version.length > 0 ? `installed ${version}` : "installed";
|
|
328
|
-
}
|
|
329
|
-
function aiHostsQuestion(installed) {
|
|
330
|
-
const width = Math.max(...AI_HOST_CHOICES.map((host) => host.label.length)) + 2;
|
|
331
|
-
return [
|
|
332
|
-
"? AI coding tools you use (choose one or more)",
|
|
333
|
-
...AI_HOST_CHOICES.map((host, index) => {
|
|
334
|
-
const status = hostStatus(host.id, installed);
|
|
335
|
-
return ` ${index + 1}) ${status ? host.label.padEnd(width) + status : host.label}`;
|
|
336
|
-
}),
|
|
337
|
-
`Choose, e.g. 1,3 [1]: `
|
|
338
|
-
].join("\n");
|
|
339
|
-
}
|
|
340
|
-
async function askAiHosts(ask, installed) {
|
|
341
|
-
const question = aiHostsQuestion(installed);
|
|
342
|
-
for (; ; ) {
|
|
343
|
-
const picked = parseAiHostAnswer(await ask(question));
|
|
344
|
-
if (picked) return picked;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
async function collectMissingInputs(input, ask) {
|
|
348
|
-
const preset = input.preset ?? await askChoice(ask, `Preset (${PRESET_VALUES.join("/")}): `, PRESET_VALUES);
|
|
349
|
-
const frontendChoices = input.backend === "none" ? FRONTEND_VALUES.filter((value) => value !== "none") : FRONTEND_VALUES;
|
|
350
|
-
const frontend = input.frontend ?? await askChoice(ask, `Frontend (${frontendChoices.join("/")}): `, frontendChoices);
|
|
351
|
-
const backendChoices = frontend === "none" ? BACKEND_VALUES.filter((value) => value !== "none") : BACKEND_VALUES;
|
|
352
|
-
const backend = input.backend ?? await askChoice(ask, `Backend (${backendChoices.join("/")}): `, backendChoices);
|
|
353
|
-
const locale = preset === "quick-start" ? input.locale ?? "en" : input.locale ?? await askChoice(ask, `Locale (${LOCALE_VALUES.join("/")}): `, LOCALE_VALUES);
|
|
354
|
-
const aiHosts = input.aiHosts ?? (input.interactive ? await askAiHosts(ask, input.installedHosts) : [...DEFAULT_AI_HOSTS]);
|
|
355
|
-
let review = input.review;
|
|
356
|
-
if (review === void 0 && preset === "quick-start") {
|
|
357
|
-
const answer = await ask("Enable AI PR review? (y/N): ");
|
|
358
|
-
review = parseYesNo(answer);
|
|
359
|
-
}
|
|
360
|
-
let reviewProvider = input.reviewProvider;
|
|
361
|
-
if (reviewProvider === void 0 && reviewWillBeOn(preset, review) && input.interactive) {
|
|
362
|
-
reviewProvider = await askReviewProvider(ask, aiHosts[0]);
|
|
363
|
-
}
|
|
364
|
-
return { preset, frontend, backend, locale, aiHosts, review, reviewProvider };
|
|
365
|
-
}
|
|
366
|
-
function reviewWillBeOn(preset, review) {
|
|
367
|
-
return review ?? preset !== "quick-start";
|
|
368
|
-
}
|
|
369
|
-
function reviewProviderConflict(preset, review, reviewProvider) {
|
|
370
|
-
if (reviewProvider === void 0) return void 0;
|
|
371
|
-
if (review === false) return "--review-provider needs AI PR review on, but --no-review was passed (or review was declined): drop one of them.";
|
|
372
|
-
if (preset === void 0 || reviewWillBeOn(preset, review)) return void 0;
|
|
373
|
-
return "--review-provider needs AI PR review on, and the quick-start preset leaves it off: add --review, or drop --review-provider.";
|
|
374
|
-
}
|
|
375
|
-
function reviewProviderQuestion(firstHost) {
|
|
376
|
-
const host = AI_HOST_CHOICES.find((choice) => choice.id === firstHost);
|
|
377
|
-
const defaultIndex = REVIEW_PROVIDER_CHOICES.findIndex((provider) => provider.id === host?.reviewProvider);
|
|
378
|
-
return [
|
|
379
|
-
"? AI PR review provider",
|
|
380
|
-
...REVIEW_PROVIDER_CHOICES.map(
|
|
381
|
-
(provider, index) => ` ${index + 1}) ${provider.label}${index === defaultIndex ? ` (default \u2014 ${host?.label})` : ""}`
|
|
382
|
-
),
|
|
383
|
-
`Choose 1-${REVIEW_PROVIDER_CHOICES.length} [${defaultIndex + 1}]: `
|
|
384
|
-
].join("\n");
|
|
385
|
-
}
|
|
386
|
-
function parseReviewProviderAnswer(answer) {
|
|
387
|
-
const token = answer.trim().toLowerCase();
|
|
388
|
-
if (token === "") return void 0;
|
|
389
|
-
const byNumber = /^\d+$/.test(token) ? REVIEW_PROVIDER_CHOICES[Number(token) - 1] : void 0;
|
|
390
|
-
const provider = byNumber ?? REVIEW_PROVIDER_CHOICES.find((choice) => choice.id === token);
|
|
391
|
-
return provider ? provider.id : null;
|
|
392
|
-
}
|
|
393
|
-
async function askReviewProvider(ask, firstHost) {
|
|
394
|
-
const question = reviewProviderQuestion(firstHost);
|
|
395
|
-
for (; ; ) {
|
|
396
|
-
const picked = parseReviewProviderAnswer(await ask(question));
|
|
397
|
-
if (picked !== null) return picked;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// src/render-usage.ts
|
|
402
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
403
|
-
import { dirname, join } from "node:path";
|
|
404
|
-
import { fileURLToPath } from "node:url";
|
|
405
|
-
var AVAILABLE_GENERATORS = [
|
|
406
|
-
"doctor",
|
|
407
|
-
"init",
|
|
408
|
-
"add-integration",
|
|
409
|
-
"add-review",
|
|
410
|
-
"add-backend",
|
|
411
|
-
"add-agent-rules",
|
|
412
|
-
"add-github-templates",
|
|
413
|
-
"add-fsd-lite",
|
|
414
|
-
"page",
|
|
415
|
-
"widget",
|
|
416
|
-
"feature",
|
|
417
|
-
"entity",
|
|
418
|
-
"shared",
|
|
419
|
-
"install-integration",
|
|
420
|
-
"record-integration",
|
|
421
|
-
"allow-builds"
|
|
422
|
-
];
|
|
423
|
-
function ownVersion() {
|
|
424
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
425
|
-
for (; ; ) {
|
|
426
|
-
const manifest = join(dir, "package.json");
|
|
427
|
-
if (existsSync(manifest)) {
|
|
428
|
-
const pkg = JSON.parse(readFileSync(manifest, "utf-8"));
|
|
429
|
-
if (pkg.name === "create-nx-archweave" && pkg.version) return pkg.version;
|
|
430
|
-
}
|
|
431
|
-
const parent = dirname(dir);
|
|
432
|
-
if (parent === dir) throw new Error("create-nx-archweave: its own package.json was not found");
|
|
433
|
-
dir = parent;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
var CLI_VERSION = ownVersion();
|
|
437
|
-
var PLUGIN_LIST = PLUGIN_CHOICES.map(
|
|
438
|
-
(plugin) => `${plugin.id} (${AI_HOST_CHOICES.find((host) => host.id === plugin.host)?.label ?? plugin.host})`
|
|
439
|
-
).join(", ");
|
|
440
|
-
function renderUsage() {
|
|
441
|
-
return [
|
|
442
|
-
"Usage: create-nx-archweave <name> [options]",
|
|
443
|
-
"",
|
|
444
|
-
"Options:",
|
|
445
|
-
" --preset <quick-start|solo|team> Preset that configures the generated workspace",
|
|
446
|
-
" --frontend <react-vite|next|none> Target frontend (none: a backend-only workspace \u2014 needs --backend)",
|
|
447
|
-
" --backend <express|nest|none> Backend app, wired with shared-types and enforced boundaries (asked if omitted)",
|
|
448
|
-
" --locale <ko|en> Generated content language (asked if omitted; quick-start: en)",
|
|
449
|
-
" --ai-hosts <list> AI coding tools, comma-separated: claude-code, codex, cursor, gemini-cli",
|
|
450
|
-
" (asked if omitted, showing which are installed; without a terminal: claude-code)",
|
|
451
|
-
" --plugins <list|none> Optional plugins for the picked AI hosts, comma-separated: " + PLUGIN_LIST,
|
|
452
|
-
" (asked if omitted, for hosts installed here; without a terminal: none)",
|
|
453
|
-
" --review, --no-review Force AI PR review on/off (quick-start asks y/N; solo/team: on)",
|
|
454
|
-
" --review-provider <name> claude, gemini, gpt or cursor (asked when review is on; default: the first AI tool's)",
|
|
455
|
-
" --plugin-source <spec> Override the nx-archweave plugin package/version to install",
|
|
456
|
-
" --allow-nested Create the workspace even inside a git repository or package.json project",
|
|
457
|
-
" --package-manager <name> npm, pnpm, yarn or bun (asked if omitted; without a terminal: the one that ran this command, else npm)",
|
|
458
|
-
" -h, --help Show this help",
|
|
459
|
-
" -v, --version Show the CLI version",
|
|
460
|
-
"",
|
|
461
|
-
"Any required option left unset above is asked interactively.",
|
|
462
|
-
"",
|
|
463
|
-
"Available generators (once installed):",
|
|
464
|
-
` ${AVAILABLE_GENERATORS.join(", ")}`
|
|
465
|
-
].join("\n");
|
|
466
|
-
}
|
|
467
|
-
function renderVersion() {
|
|
468
|
-
return CLI_VERSION;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// src/cli/plan.ts
|
|
472
|
-
var DEFAULT_PLUGIN_SOURCE = `@clearlydev/nx-archweave@^${CLI_VERSION}`;
|
|
473
|
-
var VALIDATED_CREATE_NX_WORKSPACE_VERSION = "24.0.1";
|
|
474
|
-
function nxArgs(pm) {
|
|
475
|
-
return pm === "npm" ? ["exec", "nx", "--"] : ["nx"];
|
|
476
|
-
}
|
|
477
|
-
function nxGenerateCommand(pm) {
|
|
478
|
-
return `${pm} ${nxArgs(pm).join(" ")} g`;
|
|
479
|
-
}
|
|
480
|
-
function addDevArgs(pm, spec, ctx) {
|
|
481
|
-
if (pm === "pnpm") return ["add", "-D", "-w", spec];
|
|
482
|
-
if (pm === "npm") return ["install", "-D", spec];
|
|
483
|
-
if (pm === "bun") return ["add", "-d", spec];
|
|
484
|
-
return ctx && isYarnBerry(ctx) ? ["add", "-D", spec] : ["add", "-D", "-W", spec];
|
|
485
|
-
}
|
|
486
|
-
function isYarnBerry(ctx) {
|
|
487
|
-
return ctx.readFile(".yarnrc.yml") !== void 0 || /^__metadata:/m.test(ctx.readFile("yarn.lock") ?? "");
|
|
488
|
-
}
|
|
489
|
-
var YARN_PLAN_ENV = { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" };
|
|
490
|
-
var NO_E2E = ["--e2eTestRunner=none"];
|
|
491
|
-
function createWorkspaceSourceArgs(frontend, backend) {
|
|
492
|
-
if (frontend === "react-vite") return ["--preset=empty"];
|
|
493
|
-
if (frontend === "next") return ["--preset=next", "--appName=web", ...NO_E2E];
|
|
494
|
-
return [`--preset=${backend}`, "--appName=api", ...NO_E2E, "--unitTestRunner=vitest"];
|
|
495
|
-
}
|
|
496
|
-
function buildNextSteps(input) {
|
|
497
|
-
const lines = [
|
|
498
|
-
`cd ${input.name}`,
|
|
499
|
-
...input.backend !== "none" ? [`${input.packageManager} install (links the new shared-types workspace package \u2014 do this before the first commit)`] : [],
|
|
500
|
-
'git add -A && git commit -m "chore: initial commit"',
|
|
501
|
-
"(create-nx-archweave never commits for you \u2014 review what the init report above generated first.)"
|
|
502
|
-
];
|
|
503
|
-
if (input.review === false) return lines;
|
|
504
|
-
if (input.review === true) {
|
|
505
|
-
lines.push(
|
|
506
|
-
"AI PR review is enabled: add the review secret named in the init report above to the repository before opening a pull request.",
|
|
507
|
-
"Verify the default branch matches what the init report above expects before relying on branch protection."
|
|
508
|
-
);
|
|
509
|
-
} else {
|
|
510
|
-
lines.push(
|
|
511
|
-
"If the init report above shows AI PR review enabled, add the review secret it names to the repository before opening a pull request, and verify the default branch first."
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
return lines;
|
|
515
|
-
}
|
|
516
|
-
function buildPlan(input) {
|
|
517
|
-
const pluginSource = input.pluginSource ?? DEFAULT_PLUGIN_SOURCE;
|
|
518
|
-
const workspaceSource = createWorkspaceSourceArgs(input.frontend, input.backend);
|
|
519
|
-
const aiAgentArgs = input.aiHosts.length === 0 ? ["--aiAgents=none"] : input.aiHosts.map((host) => `--aiAgents=${AI_HOST_CHOICES.find((choice) => choice.id === host)?.nxAgent ?? host}`);
|
|
520
|
-
const pm = input.packageManager;
|
|
521
|
-
const nxPrefix = nxArgs(pm);
|
|
522
|
-
const initArgs = [
|
|
523
|
-
...nxPrefix,
|
|
524
|
-
"g",
|
|
525
|
-
"@clearlydev/nx-archweave:init",
|
|
526
|
-
`--preset=${input.preset}`,
|
|
527
|
-
`--frontend=${input.frontend}`,
|
|
528
|
-
`--backend=${input.backend}`,
|
|
529
|
-
`--locale=${input.locale}`,
|
|
530
|
-
`--aiHosts=${input.aiHosts.join(",")}`,
|
|
531
|
-
`--packageManager=${pm}`,
|
|
532
|
-
// Step 2 (the plugin install) always dirties the fresh workspace's tree
|
|
533
|
-
// (package.json + lockfile), so init's clean-tree refusal would otherwise
|
|
534
|
-
// always fire here. This is orchestration knowledge, not policy: the CLI
|
|
535
|
-
// knows what its own previous step did. For the same reason the report's note about
|
|
536
|
-
// reverting earlier user changes doesn't apply: the only changes are that install's.
|
|
537
|
-
"--allowDirty=true",
|
|
538
|
-
"--dirtyFromCaller=true"
|
|
539
|
-
];
|
|
540
|
-
if (input.review !== void 0) {
|
|
541
|
-
initArgs.push(`--review=${input.review}`);
|
|
542
|
-
}
|
|
543
|
-
if (input.reviewProvider !== void 0 && input.review !== false) {
|
|
544
|
-
initArgs.push(`--reviewProvider=${input.reviewProvider}`);
|
|
545
|
-
}
|
|
546
|
-
initArgs.push("--applyEslint=true");
|
|
547
|
-
const steps = [
|
|
548
|
-
{
|
|
549
|
-
id: "create-workspace",
|
|
550
|
-
description: `Create the Nx workspace "${input.name}"`,
|
|
551
|
-
// pnpm launches with its own dlx; npm, yarn and bun through npx (always present
|
|
552
|
-
// with Node, and how every yarn and bun row of the measurements was produced).
|
|
553
|
-
command: pm === "pnpm" ? "pnpm" : "npx",
|
|
554
|
-
args: pm === "pnpm" ? [
|
|
555
|
-
"dlx",
|
|
556
|
-
`create-nx-workspace@${VALIDATED_CREATE_NX_WORKSPACE_VERSION}`,
|
|
557
|
-
input.name,
|
|
558
|
-
...workspaceSource,
|
|
559
|
-
...aiAgentArgs,
|
|
560
|
-
"--pm=pnpm",
|
|
561
|
-
"--nxCloud=skip",
|
|
562
|
-
"--interactive=false"
|
|
563
|
-
] : [
|
|
564
|
-
"-y",
|
|
565
|
-
`create-nx-workspace@${VALIDATED_CREATE_NX_WORKSPACE_VERSION}`,
|
|
566
|
-
input.name,
|
|
567
|
-
...workspaceSource,
|
|
568
|
-
...aiAgentArgs,
|
|
569
|
-
`--pm=${pm}`,
|
|
570
|
-
"--nxCloud=skip",
|
|
571
|
-
"--interactive=false"
|
|
572
|
-
],
|
|
573
|
-
cwd: null
|
|
574
|
-
},
|
|
575
|
-
...input.frontend === "react-vite" ? reactAppSteps(input.name, pm) : [],
|
|
576
|
-
{
|
|
577
|
-
id: "install-plugin",
|
|
578
|
-
description: `Install ${pluginSource}`,
|
|
579
|
-
command: pm,
|
|
580
|
-
args: addDevArgs(pm, pluginSource),
|
|
581
|
-
cwd: input.name,
|
|
582
|
-
// yarn's flags depend on its flavor, which only the created workspace tells.
|
|
583
|
-
...pm === "yarn" ? { finalizeArgs: (ctx) => addDevArgs(pm, pluginSource, ctx) } : {}
|
|
584
|
-
},
|
|
585
|
-
// Backend-only: the preset already built apps/api, so neither official step runs.
|
|
586
|
-
...input.backend === "none" || input.frontend === "none" ? [] : backendSteps(input.backend, input.name, pm, `${pm} ${initArgs.join(" ")}`),
|
|
587
|
-
{
|
|
588
|
-
id: "run-init",
|
|
589
|
-
description: "Run the nx-archweave init generator",
|
|
590
|
-
command: pm,
|
|
591
|
-
args: initArgs,
|
|
592
|
-
cwd: input.name
|
|
593
|
-
},
|
|
594
|
-
...pm === "pnpm" ? [settleBuildApprovalsStep(input.name)] : [],
|
|
595
|
-
...pm === "bun" ? [listBlockedScriptsStep(input.name)] : [],
|
|
596
|
-
...(input.plugins ?? []).map((plugin) => installPluginStep(plugin, input.name, pm))
|
|
597
|
-
];
|
|
598
|
-
return {
|
|
599
|
-
steps,
|
|
600
|
-
nextSteps: buildNextSteps(input),
|
|
601
|
-
...pm === "pnpm" ? { env: PNPM_PLAN_ENV } : {},
|
|
602
|
-
...pm === "yarn" ? { env: YARN_PLAN_ENV } : {}
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
function installPluginStep(plugin, workspaceName, pm) {
|
|
606
|
-
const hostLabel2 = AI_HOST_CHOICES.find((choice) => choice.id === plugin.host)?.label ?? plugin.host;
|
|
607
|
-
const args = [
|
|
608
|
-
...nxArgs(pm),
|
|
609
|
-
"g",
|
|
610
|
-
"@clearlydev/nx-archweave:install-integration",
|
|
611
|
-
`--tool=${plugin.id}`,
|
|
612
|
-
`--target=${plugin.host}`,
|
|
613
|
-
"--scope=project",
|
|
614
|
-
"--apply"
|
|
615
|
-
];
|
|
616
|
-
return {
|
|
617
|
-
id: "install-ai-plugin",
|
|
618
|
-
description: `Install the ${plugin.label} plugin for ${hostLabel2} (project scope)`,
|
|
619
|
-
command: pm,
|
|
620
|
-
args,
|
|
621
|
-
cwd: workspaceName,
|
|
622
|
-
optional: true,
|
|
623
|
-
successNote: () => `the ${plugin.label} report above says whether it was installed and recorded; if not, it names what to do next.`,
|
|
624
|
-
failureHint: `The workspace itself is complete. To retry inside "${workspaceName}": ${pm} ${args.join(" ")}`
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
var PNPM_PLAN_ENV = { pnpm_config_strict_dep_builds: "false" };
|
|
628
|
-
var PNPM_PENDING_BUILD_PLACEHOLDER = "set this to true or false";
|
|
629
|
-
function pendingBuildDecisions(workspaceYaml) {
|
|
630
|
-
const placeholder = PNPM_PENDING_BUILD_PLACEHOLDER;
|
|
631
|
-
const entry = new RegExp(`^(['"]?)(.+)\\1:\\s+(['"]?)${placeholder}\\3$`);
|
|
632
|
-
const pendingKey = (text) => {
|
|
633
|
-
const trimmed = text.trim();
|
|
634
|
-
if (trimmed.startsWith("#")) return void 0;
|
|
635
|
-
return entry.exec(trimmed)?.[2];
|
|
636
|
-
};
|
|
637
|
-
const names = [];
|
|
638
|
-
let inAllowBuilds = false;
|
|
639
|
-
for (const line of workspaceYaml.split(/\r?\n/)) {
|
|
640
|
-
if (/^\s*#/.test(line)) continue;
|
|
641
|
-
if (/^\S/.test(line)) {
|
|
642
|
-
inAllowBuilds = /^allowBuilds:\s*$/.test(line);
|
|
643
|
-
const flow = /^allowBuilds:\s*\{(.*)\}\s*$/.exec(line);
|
|
644
|
-
if (flow) {
|
|
645
|
-
for (const item of flow[1].split(",")) {
|
|
646
|
-
const name2 = pendingKey(item);
|
|
647
|
-
if (name2 !== void 0) names.push(name2);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
if (!inAllowBuilds) continue;
|
|
653
|
-
const name = pendingKey(line);
|
|
654
|
-
if (name !== void 0) names.push(name);
|
|
655
|
-
}
|
|
656
|
-
return names;
|
|
657
|
-
}
|
|
658
|
-
function settleBuildApprovalsStep(workspaceName) {
|
|
659
|
-
return {
|
|
660
|
-
id: "settle-build-approvals",
|
|
661
|
-
description: "Record the dependency build scripts pnpm skipped as not allowed (pnpm-workspace.yaml allowBuilds)",
|
|
662
|
-
command: "pnpm",
|
|
663
|
-
args: ["approve-builds"],
|
|
664
|
-
cwd: workspaceName,
|
|
665
|
-
finalizeArgs: (ctx) => {
|
|
666
|
-
const yaml = ctx.readFile("pnpm-workspace.yaml");
|
|
667
|
-
const pending = yaml === void 0 ? [] : pendingBuildDecisions(yaml);
|
|
668
|
-
if (pending.length === 0) return { skip: "no dependency build script is awaiting a decision" };
|
|
669
|
-
return ["approve-builds", ...pending.map((name) => `!${name}`)];
|
|
670
|
-
},
|
|
671
|
-
// Once denied, pnpm 11.9's `approve-builds` no longer lists a package (it only
|
|
672
|
-
// offers what is still pending), so the way back is the file plus a rebuild —
|
|
673
|
-
// which the plugin's allow-builds generator (installed by an earlier step) does.
|
|
674
|
-
successNote: (args) => {
|
|
675
|
-
const names = args.filter((arg) => arg.startsWith("!")).map((arg) => arg.slice(1));
|
|
676
|
-
return `pnpm skipped the build scripts of ${names.join(", ")}; they stay disabled (allowBuilds: false in pnpm-workspace.yaml). To allow one later, run inside "${workspaceName}": pnpm nx g @clearlydev/nx-archweave:allow-builds --packages=<package> --apply`;
|
|
677
|
-
},
|
|
678
|
-
failureHint: 'The workspace itself is complete \u2014 every earlier step finished; only recording the build decisions failed. Inside it, set each allowBuilds entry in pnpm-workspace.yaml that still reads "set this to true or false" to true or false yourself, then run pnpm install.'
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
function listBlockedScriptsStep(workspaceName) {
|
|
682
|
-
return {
|
|
683
|
-
id: "list-blocked-scripts",
|
|
684
|
-
description: "List the dependency install scripts bun blocked (bun pm untrusted)",
|
|
685
|
-
command: "bun",
|
|
686
|
-
args: ["pm", "untrusted"],
|
|
687
|
-
cwd: workspaceName,
|
|
688
|
-
successNote: () => `bun blocks the install scripts of dependencies it does not trust; the list above (bun pm untrusted) shows the ones it blocked here \u2014 they stay blocked. To allow one later, run inside "${workspaceName}": bun pm trust <package>`,
|
|
689
|
-
failureHint: `The workspace itself is complete \u2014 every earlier step finished; only listing the blocked install scripts failed. Inside "${workspaceName}", run bun pm untrusted to see them.`
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
function reactAppSteps(workspaceName, pm) {
|
|
693
|
-
return [
|
|
694
|
-
{
|
|
695
|
-
id: "add-react-plugin",
|
|
696
|
-
description: "Add @nx/react with nx add (at the workspace nx version)",
|
|
697
|
-
command: pm,
|
|
698
|
-
args: [...nxArgs(pm), "add", "@nx/react"],
|
|
699
|
-
cwd: workspaceName
|
|
700
|
-
},
|
|
701
|
-
{
|
|
702
|
-
id: "create-web-app",
|
|
703
|
-
description: "Create the React + Vite app in apps/web with the official @nx/react generator",
|
|
704
|
-
command: pm,
|
|
705
|
-
args: [...nxArgs(pm), "g", "@nx/react:app", "apps/web", ...REACT_APP_OPTIONS],
|
|
706
|
-
cwd: workspaceName
|
|
707
|
-
}
|
|
708
|
-
];
|
|
709
|
-
}
|
|
710
|
-
var REACT_APP_OPTIONS = ["--bundler=vite", "--linter=eslint", "--style=css", "--unitTestRunner=vitest", ...NO_E2E, "--routing=false"];
|
|
711
|
-
var API_APP_OPTIONS = ["--unitTestRunner=vitest", ...NO_E2E];
|
|
712
|
-
function backendSteps(backend, workspaceName, pm, initCommand) {
|
|
713
|
-
const nxG = nxGenerateCommand(pm);
|
|
714
|
-
const apiExists = (ctx) => ctx.listDir("apps").includes("api");
|
|
715
|
-
const standaloneError = `this workspace has no apps/ directory \u2014 CLI backend wiring needs the frontend under apps/. Create the api app with the official @nx generator, then run \`${nxG} @clearlydev/nx-archweave:add-backend\` yourself. Finish setup inside "${workspaceName}" with: ${initCommand}`;
|
|
716
|
-
const apiExistsError = `the workspace already has an apps/api \u2014 wire it with \`${nxG} @clearlydev/nx-archweave:add-backend --backend=${backend}\`, or remove it and run the official generator yourself. Either way, finish setup inside "${workspaceName}" with: ${initCommand}`;
|
|
717
|
-
return [
|
|
718
|
-
{
|
|
719
|
-
id: "install-backend-plugin",
|
|
720
|
-
description: `Install @nx/${backend} (pinned to the workspace's nx version)`,
|
|
721
|
-
command: pm,
|
|
722
|
-
args: addDevArgs(pm, `@nx/${backend}`),
|
|
723
|
-
cwd: workspaceName,
|
|
724
|
-
finalizeArgs: (ctx) => {
|
|
725
|
-
if (ctx.listDir("apps").length === 0) return { error: standaloneError };
|
|
726
|
-
if (apiExists(ctx)) return { error: apiExistsError };
|
|
727
|
-
const raw = ctx.readFile("package.json");
|
|
728
|
-
if (raw === void 0) return { error: "could not read the created workspace's package.json to pin the @nx version" };
|
|
729
|
-
try {
|
|
730
|
-
const parsed = JSON.parse(raw);
|
|
731
|
-
const nxVersion = parsed.devDependencies?.["nx"] ?? parsed.dependencies?.["nx"];
|
|
732
|
-
if (!nxVersion) return { error: "nx version not found in the created workspace's package.json" };
|
|
733
|
-
return addDevArgs(pm, `@nx/${backend}@${nxVersion}`, ctx);
|
|
734
|
-
} catch {
|
|
735
|
-
return { error: "could not parse the created workspace's package.json" };
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
},
|
|
739
|
-
{
|
|
740
|
-
id: "create-api-app",
|
|
741
|
-
description: `Create the api app with the official @nx/${backend} generator`,
|
|
742
|
-
command: pm,
|
|
743
|
-
// Since Nx 20 the positional is a directory relative to the workspace root —
|
|
744
|
-
// a bare `api` would land at the root instead of beside the frontend.
|
|
745
|
-
args: [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", ...API_APP_OPTIONS],
|
|
746
|
-
cwd: workspaceName,
|
|
747
|
-
finalizeArgs: (ctx) => {
|
|
748
|
-
const all = ctx.listDir("apps");
|
|
749
|
-
if (all.length === 0) return { error: standaloneError };
|
|
750
|
-
if (all.includes("api")) return { error: apiExistsError };
|
|
751
|
-
const apps = all.filter((name) => name !== "api" && name !== "e2e" && !name.endsWith("-e2e"));
|
|
752
|
-
if (apps.length === 0) return { error: "no frontend app directory found under apps/ in the created workspace" };
|
|
753
|
-
if (apps.length > 1) return { error: `multiple app directories found under apps/ (${apps.join(", ")}) \u2014 cannot pick --frontendProject` };
|
|
754
|
-
const frontendProject = (() => {
|
|
755
|
-
for (const file of [`apps/${apps[0]}/project.json`, `apps/${apps[0]}/package.json`]) {
|
|
756
|
-
const raw = ctx.readFile(file);
|
|
757
|
-
if (raw === void 0) continue;
|
|
758
|
-
try {
|
|
759
|
-
const name = JSON.parse(raw).name;
|
|
760
|
-
if (typeof name === "string" && name.length > 0) return name;
|
|
761
|
-
} catch {
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return apps[0];
|
|
765
|
-
})();
|
|
766
|
-
return [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", `--frontendProject=${frontendProject}`, ...API_APP_OPTIONS];
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
];
|
|
770
|
-
}
|
|
771
|
-
|
|
772
48
|
// src/cli/host-probe.ts
|
|
773
49
|
function parseProbeVersion(output) {
|
|
774
50
|
const firstLine = output.trim().split(/\r?\n/)[0]?.trim() ?? "";
|
|
@@ -850,203 +126,6 @@ async function choosePlugins(input, ask, log) {
|
|
|
850
126
|
return { plugins };
|
|
851
127
|
}
|
|
852
128
|
|
|
853
|
-
// src/cli/spawnable-command.ts
|
|
854
|
-
import { statSync } from "node:fs";
|
|
855
|
-
import { win32 } from "node:path";
|
|
856
|
-
function isFile(path) {
|
|
857
|
-
try {
|
|
858
|
-
return statSync(path).isFile();
|
|
859
|
-
} catch {
|
|
860
|
-
return false;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
var DEFAULT_ENV = { platform: process.platform, env: process.env, isFile };
|
|
864
|
-
function envValue(env, name) {
|
|
865
|
-
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
866
|
-
return key === void 0 ? void 0 : env[key];
|
|
867
|
-
}
|
|
868
|
-
function resolveWindowsCommand(command, environment) {
|
|
869
|
-
const exts = (envValue(environment.env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
870
|
-
const candidates = (base) => win32.extname(base) !== "" ? [base] : exts.map((ext) => base + ext);
|
|
871
|
-
if (win32.isAbsolute(command) || /[\\/]/.test(command)) {
|
|
872
|
-
return candidates(command).find(environment.isFile);
|
|
873
|
-
}
|
|
874
|
-
const dirs = (envValue(environment.env, "PATH") ?? "").split(";").filter(Boolean);
|
|
875
|
-
for (const dir of dirs) {
|
|
876
|
-
const found = candidates(win32.join(dir, command)).find(environment.isFile);
|
|
877
|
-
if (found !== void 0) return found;
|
|
878
|
-
}
|
|
879
|
-
return void 0;
|
|
880
|
-
}
|
|
881
|
-
var META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
882
|
-
var BIN_SHIM = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i;
|
|
883
|
-
function escapeCmdArgument(arg, doubleEscape = false) {
|
|
884
|
-
let quoted = arg.replace(/(\\*)"/g, '$1$1\\"');
|
|
885
|
-
quoted = quoted.replace(/(\\*)$/, "$1$1");
|
|
886
|
-
const escaped = `"${quoted}"`.replace(META_CHARS, "^$1");
|
|
887
|
-
return doubleEscape ? escaped.replace(META_CHARS, "^$1") : escaped;
|
|
888
|
-
}
|
|
889
|
-
function spawnableCommand(command, args, environment = DEFAULT_ENV) {
|
|
890
|
-
if (environment.platform !== "win32") return { command, args: [...args] };
|
|
891
|
-
const resolved = resolveWindowsCommand(command, environment);
|
|
892
|
-
if (resolved === void 0 || !/\.(cmd|bat)$/i.test(resolved)) return { command, args: [...args] };
|
|
893
|
-
const doubleEscape = BIN_SHIM.test(resolved);
|
|
894
|
-
const line = [resolved.replace(META_CHARS, "^$1"), ...args.map((arg) => escapeCmdArgument(arg, doubleEscape))].join(" ");
|
|
895
|
-
return {
|
|
896
|
-
command: envValue(environment.env, "COMSPEC") ?? "cmd.exe",
|
|
897
|
-
args: ["/d", "/s", "/c", `"${line}"`],
|
|
898
|
-
windowsVerbatimArguments: true
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// src/cli/execute.ts
|
|
903
|
-
import { spawnSync } from "node:child_process";
|
|
904
|
-
import { readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
905
|
-
import { join as join2 } from "node:path";
|
|
906
|
-
var LAUNCHER_ONLY = /^npm_config_package$/i;
|
|
907
|
-
function childEnv(parent, overrides = {}) {
|
|
908
|
-
const env = {};
|
|
909
|
-
for (const [key, value] of Object.entries(parent)) if (!LAUNCHER_ONLY.test(key)) env[key] = value;
|
|
910
|
-
return { ...env, ...overrides };
|
|
911
|
-
}
|
|
912
|
-
function isFileOnDisk(path) {
|
|
913
|
-
try {
|
|
914
|
-
return statSync2(path).isFile();
|
|
915
|
-
} catch {
|
|
916
|
-
return false;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
function createProcessExec() {
|
|
920
|
-
return (command, args, options) => {
|
|
921
|
-
const env = childEnv(process.env, options.env);
|
|
922
|
-
const spawnable = spawnableCommand(command, args, { platform: process.platform, env, isFile: isFileOnDisk });
|
|
923
|
-
const result = spawnSync(spawnable.command, spawnable.args, {
|
|
924
|
-
cwd: options.cwd,
|
|
925
|
-
stdio: "inherit",
|
|
926
|
-
env,
|
|
927
|
-
windowsVerbatimArguments: spawnable.windowsVerbatimArguments
|
|
928
|
-
});
|
|
929
|
-
return { status: result.status ?? null };
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
function createStepContext(workspacePath) {
|
|
933
|
-
return {
|
|
934
|
-
workspacePath,
|
|
935
|
-
readFile: (relativePath) => {
|
|
936
|
-
try {
|
|
937
|
-
return readFileSync2(join2(workspacePath, relativePath), "utf-8");
|
|
938
|
-
} catch {
|
|
939
|
-
return void 0;
|
|
940
|
-
}
|
|
941
|
-
},
|
|
942
|
-
listDir: (relativePath) => {
|
|
943
|
-
try {
|
|
944
|
-
return readdirSync(join2(workspacePath, relativePath), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
945
|
-
} catch {
|
|
946
|
-
return [];
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
function executePlan(plan, exec, invokedFromDir, ctxFactory = createStepContext, now = Date.now) {
|
|
952
|
-
const completed = [];
|
|
953
|
-
for (const step of plan.steps) {
|
|
954
|
-
const cwd = step.cwd === null ? invokedFromDir : join2(invokedFromDir, step.cwd);
|
|
955
|
-
let args = step.args;
|
|
956
|
-
if (step.finalizeArgs) {
|
|
957
|
-
const finalized = step.finalizeArgs(ctxFactory(cwd));
|
|
958
|
-
if ("error" in finalized) {
|
|
959
|
-
if (step.optional) {
|
|
960
|
-
completed.push({ step, result: { status: null }, optionalFailure: `stopped before execution: ${finalized.error}` });
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
return { ok: false, completed, failedStep: step, failedResult: { status: null }, finalizeError: finalized.error };
|
|
964
|
-
}
|
|
965
|
-
if ("skip" in finalized) {
|
|
966
|
-
completed.push({ step, result: { status: 0 }, skipped: finalized.skip });
|
|
967
|
-
continue;
|
|
968
|
-
}
|
|
969
|
-
args = finalized;
|
|
970
|
-
}
|
|
971
|
-
const startedAt = now();
|
|
972
|
-
const result = exec(step.command, args, plan.env ? { cwd, env: plan.env } : { cwd });
|
|
973
|
-
const durationMs = now() - startedAt;
|
|
974
|
-
if (result.status !== 0) {
|
|
975
|
-
if (step.optional) {
|
|
976
|
-
const optionalFailure = result.status === null ? "it did not start or was stopped (no exit code)" : `exit code: ${result.status} (its own error output is printed above)`;
|
|
977
|
-
completed.push({ step, result, args, optionalFailure, durationMs });
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
return { ok: false, completed, failedStep: step, failedResult: result, failedArgs: args, failedDurationMs: durationMs };
|
|
981
|
-
}
|
|
982
|
-
completed.push({ step, result, args, durationMs });
|
|
983
|
-
}
|
|
984
|
-
return { ok: true, completed };
|
|
985
|
-
}
|
|
986
|
-
function renderExecuteReport(report, plan, workspaceName) {
|
|
987
|
-
if (report.ok) {
|
|
988
|
-
const skippedLines = report.completed.filter((c) => c.skipped !== void 0).map((c) => ` skipped "${c.step.id}": ${c.skipped}`);
|
|
989
|
-
const noteLines = report.completed.flatMap(
|
|
990
|
-
(c) => c.skipped === void 0 && c.optionalFailure === void 0 && c.args !== void 0 && c.step.successNote ? [` note: ${c.step.successNote(c.args)}`] : []
|
|
991
|
-
);
|
|
992
|
-
const failedOptional = report.completed.filter((c) => c.optionalFailure !== void 0);
|
|
993
|
-
const optionalLines = failedOptional.flatMap((c) => [
|
|
994
|
-
` optional step "${c.step.id}" failed \u2014 ${c.step.description}.`,
|
|
995
|
-
` ${c.optionalFailure}`,
|
|
996
|
-
...c.step.failureHint ? [` ${c.step.failureHint}`] : []
|
|
997
|
-
]);
|
|
998
|
-
return [
|
|
999
|
-
"",
|
|
1000
|
-
failedOptional.length === 0 ? "create-nx-archweave finished successfully." : `create-nx-archweave finished \u2014 the workspace is complete, but ${failedOptional.length} optional step(s) failed:`,
|
|
1001
|
-
...optionalLines,
|
|
1002
|
-
...skippedLines,
|
|
1003
|
-
...noteLines,
|
|
1004
|
-
"",
|
|
1005
|
-
"Next steps:",
|
|
1006
|
-
...plan.nextSteps.map((line) => ` ${line}`)
|
|
1007
|
-
].join("\n");
|
|
1008
|
-
}
|
|
1009
|
-
const { failedStep, failedResult } = report;
|
|
1010
|
-
const detail = report.finalizeError ? `stopped before execution: ${report.finalizeError}` : `exit code: ${failedResult.status ?? "unknown"} (the command's own error output is printed above)`;
|
|
1011
|
-
return [
|
|
1012
|
-
"",
|
|
1013
|
-
`create-nx-archweave stopped: step "${failedStep.id}" failed \u2014 ${failedStep.description}.`,
|
|
1014
|
-
` ${detail}`,
|
|
1015
|
-
...!report.finalizeError && failedStep.failureHint ? [` ${failedStep.failureHint}`] : [],
|
|
1016
|
-
"",
|
|
1017
|
-
`The "${workspaceName}" directory (if it was created) was left in place \u2014 create-nx-archweave never deletes it automatically.`,
|
|
1018
|
-
`Inspect the error above and fix the issue. Then either run the remaining steps below yourself, or remove the directory and start over.`,
|
|
1019
|
-
"",
|
|
1020
|
-
...renderRemainingSteps(report, plan, workspaceName)
|
|
1021
|
-
].join("\n");
|
|
1022
|
-
}
|
|
1023
|
-
function renderStepTimings(report) {
|
|
1024
|
-
const rows = report.completed.filter((c) => c.durationMs !== void 0).map((c) => [c.step.id, c.durationMs]);
|
|
1025
|
-
if (!report.ok && report.failedDurationMs !== void 0) rows.push([`${report.failedStep.id} (failed)`, report.failedDurationMs]);
|
|
1026
|
-
return ["Step timings:", ...rows.map(([id, ms]) => ` ${(ms / 1e3).toFixed(1).padStart(7)}s ${id}`)].join("\n");
|
|
1027
|
-
}
|
|
1028
|
-
function renderRemainingSteps(report, plan, workspaceName) {
|
|
1029
|
-
const start = plan.steps.indexOf(report.failedStep);
|
|
1030
|
-
const remaining = start === -1 ? [report.failedStep] : plan.steps.slice(start);
|
|
1031
|
-
const lines = [`Remaining steps, in order (printed only \u2014 create-nx-archweave does not run them again):`];
|
|
1032
|
-
const env = Object.entries(plan.env ?? {});
|
|
1033
|
-
if (env.length > 0) lines.push(` Run them with ${env.map(([key, value]) => `${key}=${value}`).join(" ")} set, as the CLI does.`);
|
|
1034
|
-
remaining.forEach((step, index) => {
|
|
1035
|
-
const ranArgs = step === report.failedStep ? report.failedArgs : void 0;
|
|
1036
|
-
const args = ranArgs ?? step.args;
|
|
1037
|
-
const where = step.cwd === null ? "in the directory you ran create-nx-archweave from" : `inside "${workspaceName}"`;
|
|
1038
|
-
lines.push(` ${index + 1}. ${step.description}${step.optional ? " (optional)" : ""} \u2014 ${where}:`);
|
|
1039
|
-
lines.push(` ${[step.command, ...args].map(shellQuote).join(" ")}`);
|
|
1040
|
-
if (step.finalizeArgs && ranArgs === void 0) {
|
|
1041
|
-
lines.push(" (the CLI works out this step's exact arguments when it runs \u2014 check them against the workspace first)");
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
1044
|
-
return lines;
|
|
1045
|
-
}
|
|
1046
|
-
function shellQuote(word) {
|
|
1047
|
-
return /^[\w@%+=:,./^~-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
129
|
// src/bin.ts
|
|
1051
130
|
var NonInteractiveInputError = class extends Error {
|
|
1052
131
|
};
|
|
@@ -1075,7 +154,7 @@ async function main(argv) {
|
|
|
1075
154
|
}
|
|
1076
155
|
const name = args.name;
|
|
1077
156
|
if (!args.allowNested) {
|
|
1078
|
-
const location = checkInvocationDirectory(process.cwd(), { gitRoot, hasPackageJson: (dir) =>
|
|
157
|
+
const location = checkInvocationDirectory(process.cwd(), { gitRoot, hasPackageJson: (dir) => existsSync(join(dir, "package.json")) });
|
|
1079
158
|
if (!location.ok) {
|
|
1080
159
|
console.error(renderLocationRefusal(location));
|
|
1081
160
|
return 1;
|
|
@@ -1199,7 +278,7 @@ ${renderStepTimings(report)}`);
|
|
|
1199
278
|
function readVersion(manager) {
|
|
1200
279
|
const env = { ...process.env, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", COREPACK_ENABLE_STRICT: "0" };
|
|
1201
280
|
const spawnable = spawnableCommand(manager, ["--version"]);
|
|
1202
|
-
const result =
|
|
281
|
+
const result = spawnSync(spawnable.command, spawnable.args, {
|
|
1203
282
|
encoding: "utf-8",
|
|
1204
283
|
timeout: 5e3,
|
|
1205
284
|
env,
|
|
@@ -1228,6 +307,6 @@ main(process.argv.slice(2)).then((code) => {
|
|
|
1228
307
|
process.exitCode = 1;
|
|
1229
308
|
});
|
|
1230
309
|
function gitRoot(dir) {
|
|
1231
|
-
const result =
|
|
310
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: dir, encoding: "utf-8" });
|
|
1232
311
|
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
1233
312
|
}
|