@mnci/cli 1.1.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/dist/cli.js ADDED
@@ -0,0 +1,1173 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/cli.ts
32
+ var cli_exports = {};
33
+ __export(cli_exports, {
34
+ buildProgram: () => buildProgram,
35
+ main: () => main
36
+ });
37
+ module.exports = __toCommonJS(cli_exports);
38
+ var import_node_fs3 = require("fs");
39
+ var import_node_path11 = require("path");
40
+ var import_commander = require("commander");
41
+
42
+ // src/commands/add.ts
43
+ var import_node_path7 = require("path");
44
+ var import_prompts3 = require("@inquirer/prompts");
45
+
46
+ // src/nx.ts
47
+ var import_cross_spawn = __toESM(require("cross-spawn"));
48
+ function runShell(command, arguments_, cwd) {
49
+ const result = import_cross_spawn.default.sync(command, arguments_, { stdio: "inherit", cwd });
50
+ return result.status ?? 1;
51
+ }
52
+ function runNx(arguments_, cwd) {
53
+ const status = runShell("npx", ["nx", ...arguments_], cwd);
54
+ if (status !== 0) {
55
+ throw new Error(`nx ${arguments_.join(" ")} failed with exit code ${status}`);
56
+ }
57
+ }
58
+ function runNpx(arguments_, cwd) {
59
+ const status = runShell("npx", arguments_, cwd);
60
+ if (status !== 0) {
61
+ throw new Error(`npx ${arguments_.join(" ")} failed with exit code ${status}`);
62
+ }
63
+ }
64
+
65
+ // src/prompts.ts
66
+ var import_prompts = require("@inquirer/prompts");
67
+ async function promptText(message, fallback) {
68
+ const value = await (0, import_prompts.input)({
69
+ message,
70
+ default: fallback,
71
+ validate: (value2) => value2.trim().length > 0 || "A value is required"
72
+ });
73
+ return value.trim();
74
+ }
75
+ async function promptRegistry(fallbackOrganization) {
76
+ const kind = await (0, import_prompts.select)({
77
+ message: "Package registry for publishable libraries",
78
+ choices: [
79
+ { name: "Azure Artifacts", value: "azure-artifacts" },
80
+ { name: "Public npm", value: "npm" }
81
+ ]
82
+ });
83
+ if (kind === "npm") {
84
+ return { kind };
85
+ }
86
+ return {
87
+ kind,
88
+ organization: await promptText("Azure DevOps organization", fallbackOrganization),
89
+ project: await promptText("Azure DevOps project"),
90
+ artifactsFeed: await promptText("Artifacts feed name")
91
+ };
92
+ }
93
+ async function promptCi() {
94
+ return await (0, import_prompts.select)({
95
+ message: "CI provider",
96
+ choices: [
97
+ { name: "Azure Pipelines", value: "azure" },
98
+ { name: "GitHub Actions", value: "github" },
99
+ { name: "Both", value: "both" }
100
+ ]
101
+ });
102
+ }
103
+ async function promptStack() {
104
+ const linter = await (0, import_prompts.select)({
105
+ message: "Linter",
106
+ choices: [
107
+ { name: "ESLint", value: "eslint" },
108
+ { name: "Oxlint", value: "oxlint" }
109
+ ]
110
+ });
111
+ const testRunner = await (0, import_prompts.select)({
112
+ message: "Unit-test runner",
113
+ choices: [
114
+ { name: "Jest", value: "jest" },
115
+ { name: "Vitest", value: "vitest" }
116
+ ]
117
+ });
118
+ return { linter, testRunner };
119
+ }
120
+
121
+ // src/util/fsx.ts
122
+ var import_node_fs = require("fs");
123
+ var import_node_path = require("path");
124
+ function readJson(path) {
125
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
126
+ }
127
+ function toJson(value) {
128
+ return `${JSON.stringify(value, void 0, 2)}
129
+ `;
130
+ }
131
+ function writeFileEnsured(path, content) {
132
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
133
+ (0, import_node_fs.writeFileSync)(path, content);
134
+ }
135
+ function markExecutable(path) {
136
+ (0, import_node_fs.chmodSync)(path, 493);
137
+ }
138
+ function fileExists(path) {
139
+ return (0, import_node_fs.existsSync)(path);
140
+ }
141
+
142
+ // src/util/logger.ts
143
+ var logger = {
144
+ info(message) {
145
+ console.log(message);
146
+ },
147
+ step(message) {
148
+ console.log(`\u2192 ${message}`);
149
+ },
150
+ success(message) {
151
+ console.log(`\u2713 ${message}`);
152
+ },
153
+ warn(message) {
154
+ console.warn(`! ${message}`);
155
+ },
156
+ error(message) {
157
+ console.error(`\u2717 ${message}`);
158
+ }
159
+ };
160
+
161
+ // src/util/names.ts
162
+ function assertValidProjectName(name, label) {
163
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
164
+ throw new Error(`${label} '${name}' is invalid \u2014 use lowercase letters, digits and hyphens, starting with a letter (e.g. 'my-project').`);
165
+ }
166
+ }
167
+
168
+ // src/commands/add/node.ts
169
+ var import_node_path3 = require("path");
170
+
171
+ // src/commands/add/shared.ts
172
+ var import_node_path2 = require("path");
173
+ function hasPlugin(workspaceRoot, packageName) {
174
+ const manifest = readJson((0, import_node_path2.join)(workspaceRoot, "package.json"));
175
+ const installed = { ...manifest.dependencies, ...manifest.devDependencies };
176
+ return Object.hasOwn(installed, packageName);
177
+ }
178
+ function ensureAdmZip(workspaceRoot) {
179
+ if (hasPlugin(workspaceRoot, "adm-zip")) {
180
+ return;
181
+ }
182
+ logger.step("Installing the app packager (adm-zip)");
183
+ if (runShell("npm", ["install", "--save-dev", "adm-zip", "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
184
+ throw new Error("npm install of adm-zip failed");
185
+ }
186
+ }
187
+ function defaultScope(workspaceRoot) {
188
+ const { name } = readJson((0, import_node_path2.join)(workspaceRoot, "package.json"));
189
+ const base = (name.startsWith("@") ? name.slice(1) : name).split("/", 1)[0];
190
+ return `@${base}`;
191
+ }
192
+ function addNxTargets(manifestPath, newTargets) {
193
+ const manifest = fileExists(manifestPath) ? readJson(manifestPath) : {};
194
+ const nx = manifest.nx ?? {};
195
+ const targets = nx.targets ?? {};
196
+ writeFileEnsured(manifestPath, toJson({ ...manifest, nx: { ...nx, targets: { ...targets, ...newTargets } } }));
197
+ }
198
+
199
+ // src/commands/add/node.ts
200
+ function ensurePlugin(workspaceRoot, packageName) {
201
+ if (hasPlugin(workspaceRoot, packageName)) {
202
+ return;
203
+ }
204
+ logger.step(`Installing Nx plugin ${packageName}`);
205
+ runNx(["add", packageName], workspaceRoot);
206
+ }
207
+ function runNodeApp(workspaceRoot, name, stack) {
208
+ ensurePlugin(workspaceRoot, "@nx/node");
209
+ runNx([
210
+ "g",
211
+ "@nx/node:application",
212
+ `apps/${name}`,
213
+ "--bundler=esbuild",
214
+ `--unitTestRunner=${stack.testRunner}`,
215
+ `--linter=${stack.linter}`,
216
+ "--e2eTestRunner=none",
217
+ "--framework=none",
218
+ "--no-interactive"
219
+ ], workspaceRoot);
220
+ }
221
+ function nodeAppPackageTarget(name) {
222
+ const zip = `dist/drop/node-app-${name}.zip`;
223
+ const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('apps/${name}/dist');z.writeZip('${zip}')"`;
224
+ return {
225
+ executor: "nx:run-commands",
226
+ dependsOn: ["build"],
227
+ // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
228
+ outputs: [`{workspaceRoot}/${zip}`],
229
+ options: { command }
230
+ };
231
+ }
232
+ function addNodeApp(workspaceRoot, name, stack) {
233
+ runNodeApp(workspaceRoot, name, stack);
234
+ ensureAdmZip(workspaceRoot);
235
+ addNxTargets((0, import_node_path3.join)(workspaceRoot, "apps", name, "package.json"), { package: nodeAppPackageTarget(name) });
236
+ }
237
+ function ensureAzureFunctionsPackage(workspaceRoot) {
238
+ if (hasPlugin(workspaceRoot, "@azure/functions")) {
239
+ return;
240
+ }
241
+ logger.step("Installing @azure/functions");
242
+ if (runShell("npm", ["install", "@azure/functions", "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
243
+ throw new Error("npm install of @azure/functions failed");
244
+ }
245
+ }
246
+ var NODE_FUNCTION_APP_MAIN = `// esbuild only includes what is reachable from here, so add one import per
247
+ // function file you create under src/functions/.
248
+ import './functions/hello'
249
+ `;
250
+ var NODE_FUNCTION_APP_HOST_JSON = `{
251
+ "version": "2.0",
252
+ "extensionBundle": {
253
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
254
+ "version": "[4.*, 5.0.0)"
255
+ }
256
+ }
257
+ `;
258
+ var NODE_FUNCTION_APP_HELLO = `import { app, HttpRequest, HttpResponseInit } from '@azure/functions'
259
+ import { buildGreeting } from './greeting'
260
+
261
+ async function hello (request: HttpRequest): Promise<HttpResponseInit> {
262
+ const name = request.query.get('name') ?? 'world'
263
+ return { body: buildGreeting(name) }
264
+ }
265
+
266
+ app.http('hello', {
267
+ methods: ['GET'],
268
+ authLevel: 'anonymous',
269
+ route: 'hello',
270
+ handler: hello,
271
+ })
272
+ `;
273
+ var NODE_FUNCTION_APP_GREETING = `export function buildGreeting (name: string): string {
274
+ return 'Hello, ' + name + '!'
275
+ }
276
+ `;
277
+ var NODE_FUNCTION_APP_GREETING_SPEC = `import { buildGreeting } from './greeting'
278
+
279
+ describe('buildGreeting', () => {
280
+ it('greets a name', () => {
281
+ expect(buildGreeting('world')).toBe('Hello, world!')
282
+ })
283
+ })
284
+ `;
285
+ function repairNodeFunctionAppManifest(nodeFunctionAppRoot, workspaceRoot) {
286
+ const manifestPath = (0, import_node_path3.join)(nodeFunctionAppRoot, "package.json");
287
+ const manifest = readJson(manifestPath);
288
+ const azureFunctionsVersion = readJson((0, import_node_path3.join)(workspaceRoot, "node_modules/@azure/functions/package.json")).version;
289
+ const dependencies = manifest.dependencies ?? {};
290
+ writeFileEnsured(manifestPath, toJson({
291
+ ...manifest,
292
+ main: "main.js",
293
+ dependencies: { ...dependencies, "@azure/functions": `^${azureFunctionsVersion}` }
294
+ }));
295
+ }
296
+ function nodeFunctionAppPackageTarget(name) {
297
+ const zip = `dist/drop/node-function-app-${name}.zip`;
298
+ const root = `apps/${name}`;
299
+ const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('${root}/dist');z.addLocalFile('${root}/host.json');z.addLocalFile('${root}/package.json');z.writeZip('${zip}')"`;
300
+ return {
301
+ executor: "nx:run-commands",
302
+ dependsOn: ["build"],
303
+ // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
304
+ outputs: [`{workspaceRoot}/${zip}`],
305
+ options: { command }
306
+ };
307
+ }
308
+ function addNodeFunctionApp(workspaceRoot, name, stack) {
309
+ runNodeApp(workspaceRoot, name, stack);
310
+ ensureAzureFunctionsPackage(workspaceRoot);
311
+ const nodeFunctionAppRoot = (0, import_node_path3.join)(workspaceRoot, "apps", name);
312
+ writeFileEnsured((0, import_node_path3.join)(nodeFunctionAppRoot, "src/main.ts"), NODE_FUNCTION_APP_MAIN);
313
+ writeFileEnsured((0, import_node_path3.join)(nodeFunctionAppRoot, "src/functions/hello.ts"), NODE_FUNCTION_APP_HELLO);
314
+ writeFileEnsured((0, import_node_path3.join)(nodeFunctionAppRoot, "src/functions/greeting.ts"), NODE_FUNCTION_APP_GREETING);
315
+ writeFileEnsured((0, import_node_path3.join)(nodeFunctionAppRoot, "src/functions/greeting.spec.ts"), NODE_FUNCTION_APP_GREETING_SPEC);
316
+ writeFileEnsured((0, import_node_path3.join)(nodeFunctionAppRoot, "host.json"), NODE_FUNCTION_APP_HOST_JSON);
317
+ repairNodeFunctionAppManifest(nodeFunctionAppRoot, workspaceRoot);
318
+ ensureAdmZip(workspaceRoot);
319
+ addNxTargets((0, import_node_path3.join)(nodeFunctionAppRoot, "package.json"), { package: nodeFunctionAppPackageTarget(name) });
320
+ }
321
+
322
+ // src/commands/add/npmLib.ts
323
+ var import_node_path4 = require("path");
324
+ var NPM_LIB_ESLINT_CONFIG = `import { globSync, readFileSync } from 'node:fs';
325
+ import { dirname, join } from 'node:path';
326
+ import { fileURLToPath } from 'node:url';
327
+ import baseConfig from '../../eslint.config.mjs';
328
+
329
+ // Private workspace libs are compiled INTO this package's rollup bundle and
330
+ // never declared in the manifest (a consumer could not install them), so the
331
+ // dependency check must ignore them. Computed at lint time: adding a new
332
+ // internal lib never requires touching this file.
333
+ const workspaceRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
334
+ const privateWorkspacePackages = globSync(['libs/*/package.json', 'packages/*/package.json'], { cwd: workspaceRoot })
335
+ .map((manifestPath) => JSON.parse(readFileSync(join(workspaceRoot, manifestPath), 'utf8')))
336
+ .filter((manifest) => manifest.private === true)
337
+ .map((manifest) => manifest.name);
338
+
339
+ export default [
340
+ ...baseConfig,
341
+ {
342
+ files: ['**/*.json'],
343
+ rules: {
344
+ '@nx/dependency-checks': [
345
+ 'error',
346
+ {
347
+ ignoredDependencies: privateWorkspacePackages,
348
+ ignoredFiles: [
349
+ '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
350
+ '{projectRoot}/rollup.config.{js,ts,mjs,mts,cjs,cts}',
351
+ ],
352
+ },
353
+ ],
354
+ },
355
+ languageOptions: {
356
+ parser: await import('jsonc-eslint-parser'),
357
+ },
358
+ },
359
+ {
360
+ ignores: ['**/out-tsc'],
361
+ },
362
+ ];
363
+ `;
364
+ async function addNpmLib(workspaceRoot, name, options, kindProvided, stack) {
365
+ const scope = options.scope ?? (kindProvided ? defaultScope(workspaceRoot) : await promptText("npm scope for the published package", defaultScope(workspaceRoot)));
366
+ runNx([
367
+ "g",
368
+ "@nx/js:lib",
369
+ `packages/${name}`,
370
+ "--publishable",
371
+ `--importPath=${scope}/${name}`,
372
+ "--bundler=rollup",
373
+ `--unitTestRunner=${stack.testRunner}`,
374
+ `--linter=${stack.linter}`,
375
+ "--no-interactive"
376
+ ], workspaceRoot);
377
+ markPublic((0, import_node_path4.join)(workspaceRoot, "packages", name, "package.json"));
378
+ if (stack.linter === "eslint") {
379
+ writeFileEnsured((0, import_node_path4.join)(workspaceRoot, "packages", name, "eslint.config.mjs"), NPM_LIB_ESLINT_CONFIG);
380
+ }
381
+ }
382
+ function markPublic(manifestPath) {
383
+ const manifest = readJson(manifestPath);
384
+ writeFileEnsured(manifestPath, toJson({ ...manifest, publishConfig: { access: "public" } }));
385
+ }
386
+
387
+ // src/commands/add/python.ts
388
+ var import_node_path5 = require("path");
389
+ function ensurePython(workspaceRoot) {
390
+ const hasPython = runShell("python3", ["--version"], workspaceRoot) === 0 || runShell("python", ["--version"], workspaceRoot) === 0;
391
+ if (!hasPython) {
392
+ throw new Error("Python not found. Install Python 3.9+ first: https://www.python.org/downloads/");
393
+ }
394
+ }
395
+ function pythonPipPluginSpec() {
396
+ return process.env.MNCI2_PYTHON_PIP_SPEC ?? "@mnci/nx-python-pip";
397
+ }
398
+ function ensurePythonPipPlugin(workspaceRoot) {
399
+ if (hasPlugin(workspaceRoot, "@mnci/nx-python-pip")) {
400
+ return;
401
+ }
402
+ const spec = pythonPipPluginSpec();
403
+ logger.step(`Installing the Python toolchain plugin (${spec})`);
404
+ if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
405
+ throw new Error("npm install of @mnci/nx-python-pip failed");
406
+ }
407
+ }
408
+ var PYTHON_REQUIREMENTS_DEV = `build
409
+ twine
410
+ ruff
411
+ pytest
412
+ `;
413
+ function ensureRequirementsDev(workspaceRoot) {
414
+ const requirementsDevPath = (0, import_node_path5.join)(workspaceRoot, "requirements-dev.txt");
415
+ if (!fileExists(requirementsDevPath)) {
416
+ writeFileEnsured(requirementsDevPath, PYTHON_REQUIREMENTS_DEV);
417
+ }
418
+ }
419
+ function pythonModuleDirectory(name) {
420
+ return name.replaceAll("-", "_");
421
+ }
422
+ function addProjectJsonTargets(projectJsonPath, newTargets) {
423
+ const project = readJson(projectJsonPath);
424
+ const targets = project.targets ?? {};
425
+ writeFileEnsured(projectJsonPath, toJson({ ...project, targets: { ...targets, ...newTargets } }));
426
+ }
427
+ function pythonAppPackageTarget(name) {
428
+ const zip = `dist/drop/python-app-${name}.zip`;
429
+ const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('apps/${name}/dist');z.writeZip('${zip}')"`;
430
+ return {
431
+ executor: "nx:run-commands",
432
+ dependsOn: ["build"],
433
+ // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
434
+ outputs: [`{workspaceRoot}/${zip}`],
435
+ options: { command }
436
+ };
437
+ }
438
+ function pythonFunctionAppPackageTarget(name, moduleDirectory) {
439
+ const zip = `dist/drop/python-function-app-${name}.zip`;
440
+ const root = `apps/${name}`;
441
+ const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFile('${root}/function_app.py');z.addLocalFile('${root}/host.json');z.addLocalFile('${root}/requirements.txt');z.addLocalFolder('${root}/${moduleDirectory}','${moduleDirectory}');z.writeZip('${zip}')"`;
442
+ return {
443
+ executor: "nx:run-commands",
444
+ // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
445
+ outputs: [`{workspaceRoot}/${zip}`],
446
+ options: { command }
447
+ };
448
+ }
449
+ function addPythonApp(workspaceRoot, name) {
450
+ ensurePython(workspaceRoot);
451
+ ensurePythonPipPlugin(workspaceRoot);
452
+ ensureRequirementsDev(workspaceRoot);
453
+ ensureAdmZip(workspaceRoot);
454
+ runNx(["g", "@mnci/nx-python-pip:application", name, `--directory=apps/${name}`, "--no-interactive"], workspaceRoot);
455
+ addProjectJsonTargets((0, import_node_path5.join)(workspaceRoot, "apps", name, "project.json"), { package: pythonAppPackageTarget(name) });
456
+ }
457
+ function addPythonFunctionApp(workspaceRoot, name) {
458
+ ensurePython(workspaceRoot);
459
+ ensurePythonPipPlugin(workspaceRoot);
460
+ ensureRequirementsDev(workspaceRoot);
461
+ ensureAdmZip(workspaceRoot);
462
+ runNx(["g", "@mnci/nx-python-pip:function-application", name, `--directory=apps/${name}`, "--no-interactive"], workspaceRoot);
463
+ const moduleDirectory = pythonModuleDirectory(name);
464
+ addProjectJsonTargets((0, import_node_path5.join)(workspaceRoot, "apps", name, "project.json"), { package: pythonFunctionAppPackageTarget(name, moduleDirectory) });
465
+ }
466
+ function addPythonLib(workspaceRoot, name) {
467
+ ensurePython(workspaceRoot);
468
+ ensurePythonPipPlugin(workspaceRoot);
469
+ ensureRequirementsDev(workspaceRoot);
470
+ runNx(["g", "@mnci/nx-python-pip:library", name, `--directory=python-packages/${name}`, "--no-interactive"], workspaceRoot);
471
+ }
472
+ function addPythonInternalLib(workspaceRoot, name) {
473
+ ensurePython(workspaceRoot);
474
+ ensurePythonPipPlugin(workspaceRoot);
475
+ ensureRequirementsDev(workspaceRoot);
476
+ runNx(["g", "@mnci/nx-python-pip:internal-library", name, `--directory=libs/${name}`, "--no-interactive"], workspaceRoot);
477
+ }
478
+
479
+ // src/commands/add/reactApp.ts
480
+ var import_node_fs2 = require("fs");
481
+ var import_node_path6 = require("path");
482
+ function ensurePlugin2(workspaceRoot, packageName) {
483
+ if (hasPlugin(workspaceRoot, packageName)) {
484
+ return;
485
+ }
486
+ logger.step(`Installing Nx plugin ${packageName}`);
487
+ runNx(["add", packageName], workspaceRoot);
488
+ }
489
+ var REACT_ENVIRONMENTS = ["dev", "uat", "prod"];
490
+ function reactEnvironmentFile(environment) {
491
+ return `# Vite compiles VITE_-prefixed vars into the '${environment}' bundle at build time.
492
+ # They ship to the browser, so keep only PUBLIC config here \u2014 never secrets.
493
+ VITE_ENVIRONMENT=${environment}
494
+ VITE_API_URL=https://api.${environment}.example.com
495
+ `;
496
+ }
497
+ function reactAppTargets(name) {
498
+ const targets = {};
499
+ for (const environment of REACT_ENVIRONMENTS) {
500
+ targets[`build-${environment}`] = {
501
+ executor: "nx:run-commands",
502
+ // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
503
+ outputs: [`{workspaceRoot}/apps/${name}/dist-${environment}`],
504
+ options: { command: `vite build --mode ${environment} --outDir dist-${environment}`, cwd: `apps/${name}` }
505
+ };
506
+ }
507
+ const zipStatements = REACT_ENVIRONMENTS.map((environment) => `z=new A();z.addLocalFolder('apps/${name}/dist-${environment}');z.writeZip('dist/drop/react-app-${name}-${environment}.zip')`).join(";");
508
+ targets.package = {
509
+ executor: "nx:run-commands",
510
+ dependsOn: REACT_ENVIRONMENTS.map((environment) => `build-${environment}`),
511
+ // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} tokens, not JS interpolation
512
+ outputs: REACT_ENVIRONMENTS.map((environment) => `{workspaceRoot}/dist/drop/react-app-${name}-${environment}.zip`),
513
+ options: { command: `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');let z;${zipStatements}"` }
514
+ };
515
+ return targets;
516
+ }
517
+ function allowEnvFiles(workspaceRoot) {
518
+ const gitignorePath = (0, import_node_path6.join)(workspaceRoot, ".gitignore");
519
+ if (!fileExists(gitignorePath)) {
520
+ return;
521
+ }
522
+ const marker = "# MoNecromanCI: keep the committed per-environment Vite config (public VITE_ values)";
523
+ const current = (0, import_node_fs2.readFileSync)(gitignorePath, "utf8");
524
+ if (current.includes(marker)) {
525
+ return;
526
+ }
527
+ const allow = REACT_ENVIRONMENTS.map((environment) => `!apps/*/.env.${environment}`).join("\n");
528
+ writeFileEnsured(gitignorePath, `${current.replace(/\n*$/, "\n")}
529
+ ${marker}
530
+ ${allow}
531
+ `);
532
+ }
533
+ function addReactApp(workspaceRoot, name, stack) {
534
+ ensurePlugin2(workspaceRoot, "@nx/react");
535
+ runNx([
536
+ "g",
537
+ "@nx/react:app",
538
+ `apps/${name}`,
539
+ "--bundler=vite",
540
+ `--unitTestRunner=${stack.testRunner}`,
541
+ `--linter=${stack.linter}`,
542
+ "--style=css",
543
+ "--e2eTestRunner=none",
544
+ "--no-interactive"
545
+ ], workspaceRoot);
546
+ ensureAdmZip(workspaceRoot);
547
+ const reactAppRoot = (0, import_node_path6.join)(workspaceRoot, "apps", name);
548
+ for (const environment of REACT_ENVIRONMENTS) {
549
+ writeFileEnsured((0, import_node_path6.join)(reactAppRoot, `.env.${environment}`), reactEnvironmentFile(environment));
550
+ }
551
+ allowEnvFiles(workspaceRoot);
552
+ addNxTargets((0, import_node_path6.join)(reactAppRoot, "package.json"), reactAppTargets(name));
553
+ }
554
+
555
+ // src/commands/add.ts
556
+ var PROJECT_KINDS = [
557
+ "react-app",
558
+ "node-app",
559
+ "node-function-app",
560
+ "npm-lib",
561
+ "internal-lib",
562
+ "python-app",
563
+ "python-function-app",
564
+ "python-lib",
565
+ "python-internal-lib"
566
+ ];
567
+ async function runAdd(kind, name, options) {
568
+ const workspaceRoot = process.cwd();
569
+ if (!fileExists((0, import_node_path7.join)(workspaceRoot, "nx.json"))) {
570
+ throw new Error("No nx.json found here. Run `add` from the workspace root.");
571
+ }
572
+ const stack = readWorkspaceStack(workspaceRoot);
573
+ const kindProvided = kind !== void 0;
574
+ const resolvedKind = kind ?? await (0, import_prompts3.select)({
575
+ message: "What kind of project?",
576
+ choices: PROJECT_KINDS.map((value) => ({ name: value, value }))
577
+ });
578
+ const resolvedName = name ?? await promptText("Project name");
579
+ assertValidProjectName(resolvedName, "Project name");
580
+ switch (resolvedKind) {
581
+ case "react-app": {
582
+ addReactApp(workspaceRoot, resolvedName, stack);
583
+ break;
584
+ }
585
+ case "node-app": {
586
+ addNodeApp(workspaceRoot, resolvedName, stack);
587
+ break;
588
+ }
589
+ case "node-function-app": {
590
+ addNodeFunctionApp(workspaceRoot, resolvedName, stack);
591
+ break;
592
+ }
593
+ case "npm-lib": {
594
+ await addNpmLib(workspaceRoot, resolvedName, options, kindProvided, stack);
595
+ break;
596
+ }
597
+ case "internal-lib": {
598
+ runNx([
599
+ "g",
600
+ "@nx/js:lib",
601
+ `libs/${resolvedName}`,
602
+ "--bundler=tsc",
603
+ `--unitTestRunner=${stack.testRunner}`,
604
+ `--linter=${stack.linter}`,
605
+ "--no-interactive"
606
+ ], workspaceRoot);
607
+ markPrivate((0, import_node_path7.join)(workspaceRoot, "libs", resolvedName, "package.json"));
608
+ break;
609
+ }
610
+ case "python-app": {
611
+ addPythonApp(workspaceRoot, resolvedName);
612
+ break;
613
+ }
614
+ case "python-function-app": {
615
+ addPythonFunctionApp(workspaceRoot, resolvedName);
616
+ break;
617
+ }
618
+ case "python-lib": {
619
+ addPythonLib(workspaceRoot, resolvedName);
620
+ break;
621
+ }
622
+ case "python-internal-lib": {
623
+ addPythonInternalLib(workspaceRoot, resolvedName);
624
+ break;
625
+ }
626
+ default: {
627
+ const exhaustive = resolvedKind;
628
+ throw new Error(`Unknown project kind '${exhaustive}'. Expected one of: ${PROJECT_KINDS.join(", ")}.`);
629
+ }
630
+ }
631
+ syncProjectReferences(workspaceRoot);
632
+ logger.success(`Added ${resolvedKind} '${resolvedName}'.`);
633
+ }
634
+ function syncProjectReferences(workspaceRoot) {
635
+ logger.step("Syncing TypeScript project references (nx sync)");
636
+ if (runShell("npx", ["nx", "sync"], workspaceRoot) !== 0) {
637
+ logger.warn("nx sync did not complete \u2014 run `npx nx sync` yourself so cross-project imports resolve in your editor.");
638
+ }
639
+ }
640
+ function readWorkspaceStack(workspaceRoot) {
641
+ const nxJson = readJson((0, import_node_path7.join)(workspaceRoot, "nx.json"));
642
+ const stack = nxJson.mnci2?.stack;
643
+ return {
644
+ linter: stack?.linter === "oxlint" ? "none" : "eslint",
645
+ testRunner: stack?.testRunner === "vitest" ? "vitest" : "jest"
646
+ };
647
+ }
648
+ function markPrivate(manifestPath) {
649
+ const manifest = readJson(manifestPath);
650
+ writeFileEnsured(manifestPath, toJson({ ...manifest, private: true }));
651
+ }
652
+
653
+ // src/commands/interactive.ts
654
+ var import_node_path10 = require("path");
655
+ var import_prompts6 = require("@inquirer/prompts");
656
+
657
+ // src/commands/new.ts
658
+ var import_node_path9 = require("path");
659
+
660
+ // src/overlay.ts
661
+ var import_node_path8 = require("path");
662
+ var DEFAULT_STACK = { linter: "eslint", testRunner: "jest" };
663
+ var TS_COMPILER_DEPENDENCIES = {
664
+ "@typescript/native": "npm:typescript@^7.0.2",
665
+ typescript: "npm:@typescript/typescript6@^6.0.2"
666
+ };
667
+ function npmrcContent(registry, scope) {
668
+ const scopeName = scope.replace(/^@/, "");
669
+ const lines = [
670
+ "registry=https://registry.npmjs.org/",
671
+ "; Community Nx plugins (e.g. the Azure Functions one) peer on the previous",
672
+ "; Nx major for a while after each release; accept the resolved tree.",
673
+ "legacy-peer-deps=true"
674
+ ];
675
+ if (registry.kind === "azure-artifacts") {
676
+ const base = `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}`;
677
+ const registryHost = `${base}/npm/registry/`.replace(/^https:\/\//, "");
678
+ const feedHost = `${base}/`.replace(/^https:\/\//, "");
679
+ lines.push(
680
+ `@${scopeName}:registry=${base}/npm/registry/`,
681
+ "; Azure Artifacts auth \u2014 the base64 PAT is expanded at runtime from the",
682
+ "; PAT env var (CI: your PAT variable group; locally: export PAT=<base64 token>).",
683
+ `//${registryHost}:username=${registry.organization}`,
684
+ `//${registryHost}:_password=\${PAT}`,
685
+ `//${registryHost}:email=npm@example.com`,
686
+ `//${feedHost}:username=${registry.organization}`,
687
+ `//${feedHost}:_password=\${PAT}`,
688
+ `//${feedHost}:email=npm@example.com`
689
+ );
690
+ } else {
691
+ lines.push(`//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}`);
692
+ }
693
+ lines.push("");
694
+ return lines.join("\n");
695
+ }
696
+ var RELEASE_CONFIG = {
697
+ projectsRelationship: "independent",
698
+ projects: ["packages/*", "python-packages/*"],
699
+ releaseTag: { pattern: "{projectName}@{version}" },
700
+ git: { commit: false, tag: true, push: true },
701
+ version: {
702
+ conventionalCommits: true,
703
+ fallbackCurrentVersionResolver: "disk",
704
+ // Build only what is being released. Without this, @nx/js:lib's generator
705
+ // defaults the pre-version command to building EVERY project, so a broken
706
+ // (or merely slow) app build would block releasing unrelated packages.
707
+ // Set here at `new` time it wins: the generator only fills this in when
708
+ // absent (it spreads the existing release.version over its default). Both
709
+ // globs are listed; `nx run-many` no-ops cleanly when one matches nothing.
710
+ preVersionCommand: "npx nx run-many -t build --projects=packages/*,python-packages/*"
711
+ },
712
+ changelog: { workspaceChangelog: false }
713
+ };
714
+ var SYNC_CONFIG = { applyChanges: true };
715
+ function withReleaseConfig(nxJson) {
716
+ return { ...nxJson, defaultBase: "main", release: RELEASE_CONFIG };
717
+ }
718
+ var COMMITLINT_CONFIG = `export default { extends: ['@commitlint/config-conventional'] }
719
+ `;
720
+ var COMMIT_MSG_HOOK = `npx --no -- commitlint --edit "$1"
721
+ `;
722
+ var ROOT_SCRIPTS = {
723
+ build: "nx run-many -t build",
724
+ lint: "nx run-many -t lint",
725
+ test: "nx run-many -t test",
726
+ affected: "nx affected -t lint,test,build",
727
+ graph: "nx graph",
728
+ "release:preview": "nx release --dry-run",
729
+ prepare: "husky"
730
+ };
731
+ function rootScripts(stack) {
732
+ if (stack.linter === "oxlint") {
733
+ return {
734
+ ...ROOT_SCRIPTS,
735
+ lint: "oxlint",
736
+ format: "oxfmt -c oxfmt.config.mts .",
737
+ "format:check": "oxfmt -c oxfmt.config.mts --check ."
738
+ };
739
+ }
740
+ return { ...ROOT_SCRIPTS };
741
+ }
742
+ function generatorDefaults(stack) {
743
+ const shared = { linter: stack.linter === "oxlint" ? "none" : "eslint", unitTestRunner: stack.testRunner };
744
+ return {
745
+ "@nx/react:application": shared,
746
+ "@nx/react:library": shared,
747
+ "@nx/js:library": shared
748
+ };
749
+ }
750
+ function mnci2Config(stack) {
751
+ return { stack: { linter: stack.linter, testRunner: stack.testRunner } };
752
+ }
753
+ var OXLINT_CONFIG = `import { defineConfig } from 'oxlint'
754
+ import standard from 'oxc-standard/.oxlintrc.json' with { type: 'json' }
755
+
756
+ // Generated by MoNecromanCI. JavaScript Standard Style linting via the
757
+ // oxc-standard preset (unicorn + React + react-perf + TypeScript + oxc).
758
+ // Add project-specific rule overrides in the \`rules\` block below.
759
+ export default defineConfig({
760
+ extends: [standard],
761
+ rules: {
762
+ // The modern JSX transform (Nx's default) needs no React import in scope.
763
+ 'react/react-in-jsx-scope': 'off',
764
+ },
765
+ })
766
+ `;
767
+ var OXFMT_CONFIG = `import { defineConfig } from 'oxfmt'
768
+
769
+ // Generated by MoNecromanCI. JavaScript Standard Style formatting, matching the
770
+ // oxc-standard lint preset: no semicolons, single quotes, 2-space indent.
771
+ export default defineConfig({
772
+ singleQuote: true,
773
+ semi: false,
774
+ printWidth: 100,
775
+ tabWidth: 2,
776
+ trailingComma: 'es5',
777
+ arrowParens: 'avoid',
778
+ })
779
+ `;
780
+ function pythonPublishUrl(registry) {
781
+ if (registry.kind === "azure-artifacts") {
782
+ return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/pypi/upload/`;
783
+ }
784
+ return void 0;
785
+ }
786
+ var PYTHON_INSTALL_GUARD = `node -e "if(!require('node:fs').existsSync('requirements-dev.txt')){console.log('No Python projects - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('python3 -m pip install -r requirements-dev.txt',{stdio:'inherit',shell:true}).status ?? 1)"`;
787
+ var PACK_APPS_GUARD = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});if(fs.globSync('apps/*/project.json').length===0){console.log('No apps to pack - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('npx nx run-many -t package',{stdio:'inherit',shell:true}).status ?? 1)"`;
788
+ function releaseGuard(pythonPublishEnv) {
789
+ return `node -e "const fs=require('node:fs'),cp=require('node:child_process');const hasNpm=fs.globSync('packages/*/package.json').length>0;const hasPython=fs.globSync('python-packages/*/pyproject.toml').length>0;if(!hasNpm&&!hasPython){console.log('Nothing to release - skipping.');process.exit(0)}const env={...process.env};${pythonPublishEnv}process.exit(cp.spawnSync('npx nx release --yes',{stdio:'inherit',shell:true,env}).status ?? 1)"`;
790
+ }
791
+ function pythonPublishEnvFragment(pythonPublishUrl2) {
792
+ return pythonPublishUrl2 ? `if(hasPython){env.TWINE_REPOSITORY_URL='${pythonPublishUrl2}';env.TWINE_USERNAME='AzureArtifacts';env.TWINE_PASSWORD=Buffer.from(process.env.PAT,'base64').toString()}` : "";
793
+ }
794
+ function npmAuthEnvVariable(registryKind, variableReference) {
795
+ return registryKind === "npm" ? ["NODE_AUTH_TOKEN", variableReference("NPM_TOKEN")] : ["PAT", variableReference("PAT")];
796
+ }
797
+ function poolBlock(agent) {
798
+ return /^(ubuntu|windows|macos)-/i.test(agent) ? ` vmImage: ${agent}` : ` name: ${agent}`;
799
+ }
800
+ function azurePipelinesYaml(agent, variableGroup, pythonPublishUrl2, registryKind = "azure-artifacts") {
801
+ const onMain = `and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranchName'], 'main'))`;
802
+ const [npmAuthName, npmAuthValue] = npmAuthEnvVariable(registryKind, (name) => `$(${name})`);
803
+ return `name: monorepo-ci-$(Date:yyyyMMdd)$(Rev:.r)
804
+
805
+ # Generated by MoNecromanCI v2. Deliberately thin: Nx builds, 'nx release'
806
+ # versions from conventional commits and pushes ONLY a tag to main, and each
807
+ # app is packed into dist/drop/<type>-<name>.zip by its own 'package' target.
808
+ #
809
+ # Cross-platform by construction: no bash, no PowerShell. Every step is a
810
+ # built-in task or a single-line git/npm/npx/node command, so the pipeline
811
+ # runs unchanged on Linux, macOS and Windows agents.
812
+
813
+ trigger:
814
+ branches:
815
+ include: [main]
816
+
817
+ pr:
818
+ branches:
819
+ include: [main]
820
+
821
+ pool:
822
+ ${poolBlock(agent)}
823
+
824
+ variables:
825
+ # Holds the npm auth secret the root .npmrc reads: the base64-encoded \`PAT\`
826
+ # for an Azure Artifacts feed, or a raw npm automation token as \`NPM_TOKEN\`
827
+ # for public npm. Mark it secret in Library. Add app build vars here too if needed.
828
+ - group: ${variableGroup}
829
+
830
+ steps:
831
+ - checkout: self
832
+ # Lets later steps push release tags back to the repo. The Project
833
+ # Collection Build Service account needs Contribute permission on this
834
+ # repo (Project Settings -> Repositories -> Security) \u2014 one-time grant.
835
+ persistCredentials: true
836
+ fetchDepth: 0
837
+
838
+ # checkout leaves a detached HEAD; nx release needs a real branch.
839
+ - script: git checkout -B $(Build.SourceBranchName)
840
+ displayName: Attach HEAD to the source branch
841
+
842
+ # Version resolution needs the release tags.
843
+ - script: git fetch --all --prune --tags
844
+ displayName: Fetch branches and release tags
845
+
846
+ - script: git config user.name "Azure Pipelines" && git config user.email "pipeline@dev.azure.com"
847
+ displayName: Set the git identity used for release tags
848
+
849
+ - task: UseNode@1
850
+ inputs:
851
+ version: 24.x
852
+
853
+ - script: npm ci
854
+ displayName: Install dependencies
855
+ env:
856
+ ${npmAuthName}: ${npmAuthValue}
857
+
858
+ # Installs the fixed Python toolchain (ruff, pytest, build, twine) \u2014 written
859
+ # by 'mnci2 add' to requirements-dev.txt on the first Python project. Plain
860
+ # pip, no uv/Poetry: portable guard skips cleanly on a workspace with none.
861
+ - script: ${PYTHON_INSTALL_GUARD}
862
+ displayName: Install Python dependencies (ruff, pytest, build, twine)
863
+
864
+ # Fails fast, with an unambiguous message, when a stale TypeScript project
865
+ # reference (or another sync generator's drift) was never synced+committed
866
+ # locally \u2014 sync.applyChanges (nx.json) only auto-applies interactively, so
867
+ # CI still needs its own explicit, early check rather than surfacing this as
868
+ # a confusing failure buried inside the build step below.
869
+ - script: npx nx sync:check
870
+ displayName: Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)
871
+
872
+ # One verify for every run (PR and main). \`npm run lint\` abstracts the chosen
873
+ # linter (eslint via nx, or oxlint), so the pipeline needs no linter branch;
874
+ # Nx cache makes unchanged test/build projects instant.
875
+ - script: npm run lint
876
+ displayName: Lint
877
+ # \`lint\` here also runs any Nx lint targets \`npm run lint\` does not cover \u2014
878
+ # notably Python's ruff (every Python project owns a hand-authored \`lint\`
879
+ # target). For the eslint stack the JS lint runs twice, but Nx caches the
880
+ # repeat instantly.
881
+ - script: npx nx run-many -t lint,test,build
882
+ displayName: Lint, test and build everything
883
+
884
+ # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
885
+ # target. Portable guard: skip cleanly when the workspace has no apps yet.
886
+ - script: ${PACK_APPS_GUARD}
887
+ displayName: Pack all apps (one zip per app -> dist/drop)
888
+ condition: ${onMain}
889
+
890
+ - task: PublishBuildArtifacts@1
891
+ displayName: Publish the drop (one zip per app)
892
+ condition: ${onMain}
893
+ inputs:
894
+ PathtoPublish: $(Build.SourcesDirectory)/dist/drop
895
+ ArtifactName: drop
896
+
897
+ # One build tag per packed app, EXACTLY the zip name (type-name), so the
898
+ # classic release pipeline knows which app to run for. Derived from the zip
899
+ # filenames so the tag can never drift from the artifact.
900
+ - script: node -e "const fs=require('node:fs');const path=require('node:path');for(const f of fs.globSync('dist/drop/*.zip')){console.log('##vso[build.addbuildtag]'+path.basename(f,'.zip'))}"
901
+ displayName: Tag the run per app (type-name)
902
+ condition: ${onMain}
903
+
904
+ # Version + tag + publish, in one release, for npm (packages/*) AND Python
905
+ # (python-packages/*) \u2014 conventional commits, tag-only push. Portable guard:
906
+ # nx release errors on an empty scope, so skip cleanly when there is nothing
907
+ # to release. When there are Python packages and an Azure feed, twine
908
+ # publish credentials are exported (raw PAT, decoded from the base64 variable).
909
+ - script: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2))}
910
+ displayName: Release \u2014 version, tag and publish (npm + Python)
911
+ condition: ${onMain}
912
+ env:
913
+ ${npmAuthName}: ${npmAuthValue}
914
+ `;
915
+ }
916
+ function githubActionsYaml(agent, pythonPublishUrl2, registryKind = "azure-artifacts") {
917
+ const onMain = `github.event_name != 'pull_request' && github.ref_name == 'main'`;
918
+ const [npmAuthName, npmAuthValue] = npmAuthEnvVariable(registryKind, (name) => `\${{ secrets.${name} }}`);
919
+ return `name: CI
920
+
921
+ # Generated by MoNecromanCI v2. Deliberately thin: Nx builds, 'nx release'
922
+ # versions from conventional commits and pushes ONLY a tag to main, and each
923
+ # app is packed into dist/drop/<type>-<name>.zip by its own 'package' target.
924
+ # The GitHub Actions equivalent of azure-pipelines.yml \u2014 see there for the
925
+ # fuller rationale; both stay hand-kept in lockstep, there is no shared template.
926
+
927
+ on:
928
+ push:
929
+ branches: [main]
930
+ pull_request:
931
+ branches: [main]
932
+
933
+ permissions:
934
+ # Lets the release step push the version tag nx release creates back to main.
935
+ contents: write
936
+
937
+ jobs:
938
+ ci:
939
+ runs-on: ${agent}
940
+ steps:
941
+ - uses: actions/checkout@v4
942
+ with:
943
+ fetch-depth: 0
944
+
945
+ # Version resolution needs the release tags.
946
+ - run: git fetch --all --prune --tags
947
+ name: Fetch branches and release tags
948
+
949
+ - run: git config user.name "github-actions[bot]" && git config user.email "github-actions[bot]@users.noreply.github.com"
950
+ name: Set the git identity used for release tags
951
+
952
+ - uses: actions/setup-node@v4
953
+ with:
954
+ node-version: 24
955
+
956
+ - run: npm ci
957
+ name: Install dependencies
958
+ env:
959
+ ${npmAuthName}: ${npmAuthValue}
960
+
961
+ # Installs the fixed Python toolchain (ruff, pytest, build, twine) \u2014 written
962
+ # by 'mnci2 add' to requirements-dev.txt on the first Python project. Plain
963
+ # pip, no uv/Poetry: portable guard skips cleanly on a workspace with none.
964
+ - run: ${PYTHON_INSTALL_GUARD}
965
+ name: Install Python dependencies (ruff, pytest, build, twine)
966
+
967
+ # Fails fast, with an unambiguous message, when a stale TypeScript project
968
+ # reference (or another sync generator's drift) was never synced+committed
969
+ # locally \u2014 sync.applyChanges (nx.json) only auto-applies interactively, so
970
+ # CI still needs its own explicit, early check rather than surfacing this as
971
+ # a confusing failure buried inside the build step below.
972
+ - run: npx nx sync:check
973
+ name: Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)
974
+
975
+ # One verify for every run (PR and main). \`npm run lint\` abstracts the chosen
976
+ # linter (eslint via nx, or oxlint), so the workflow needs no linter branch;
977
+ # Nx cache makes unchanged test/build projects instant.
978
+ - run: npm run lint
979
+ name: Lint
980
+ # \`lint\` here also runs any Nx lint targets \`npm run lint\` does not cover \u2014
981
+ # notably Python's ruff (every Python project owns a hand-authored \`lint\`
982
+ # target). For the eslint stack the JS lint runs twice, but Nx caches the
983
+ # repeat instantly.
984
+ - run: npx nx run-many -t lint,test,build
985
+ name: Lint, test and build everything
986
+
987
+ # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
988
+ # target. Portable guard: skip cleanly when the workspace has no apps yet.
989
+ - run: ${PACK_APPS_GUARD}
990
+ name: Pack all apps (one zip per app -> dist/drop)
991
+ if: \${{ ${onMain} }}
992
+
993
+ - uses: actions/upload-artifact@v4
994
+ if: \${{ ${onMain} }}
995
+ with:
996
+ name: drop
997
+ path: dist/drop
998
+ if-no-files-found: ignore
999
+
1000
+ # Version + tag + publish, in one release, for npm (packages/*) AND Python
1001
+ # (python-packages/*) \u2014 conventional commits, tag-only push. Portable guard:
1002
+ # nx release errors on an empty scope, so skip cleanly when there is nothing
1003
+ # to release. When there are Python packages and an Azure feed, twine
1004
+ # publish credentials are exported (raw PAT, decoded from the base64 secret).
1005
+ - run: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2))}
1006
+ name: Release \u2014 version, tag and publish (npm + Python)
1007
+ if: \${{ ${onMain} }}
1008
+ env:
1009
+ ${npmAuthName}: ${npmAuthValue}
1010
+ `;
1011
+ }
1012
+ function applyOverlay(workspaceRoot, options) {
1013
+ const nxJsonPath = (0, import_node_path8.join)(workspaceRoot, "nx.json");
1014
+ const nxJson = readJson(nxJsonPath);
1015
+ const generators = { ...nxJson.generators, ...generatorDefaults(options.stack) };
1016
+ const sync = { ...nxJson.sync, ...SYNC_CONFIG };
1017
+ const mnci2 = { ...nxJson.mnci2, ...mnci2Config(options.stack) };
1018
+ writeFileEnsured(nxJsonPath, toJson({ ...withReleaseConfig(nxJson), generators, sync, mnci2 }));
1019
+ const manifestPath = (0, import_node_path8.join)(workspaceRoot, "package.json");
1020
+ const manifest = readJson(manifestPath);
1021
+ const scripts = { ...manifest.scripts, ...rootScripts(options.stack) };
1022
+ const devDependencies = { ...manifest.devDependencies, ...TS_COMPILER_DEPENDENCIES };
1023
+ writeFileEnsured(manifestPath, toJson({ ...manifest, name: `${options.scope}/source`, scripts, devDependencies }));
1024
+ writeFileEnsured((0, import_node_path8.join)(workspaceRoot, ".npmrc"), npmrcContent(options.registry, options.scope));
1025
+ writeFileEnsured((0, import_node_path8.join)(workspaceRoot, "commitlint.config.mjs"), COMMITLINT_CONFIG);
1026
+ const hookPath = (0, import_node_path8.join)(workspaceRoot, ".husky/commit-msg");
1027
+ writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
1028
+ markExecutable(hookPath);
1029
+ if (options.stack.linter === "oxlint") {
1030
+ writeFileEnsured((0, import_node_path8.join)(workspaceRoot, "oxlint.config.mts"), OXLINT_CONFIG);
1031
+ writeFileEnsured((0, import_node_path8.join)(workspaceRoot, "oxfmt.config.mts"), OXFMT_CONFIG);
1032
+ }
1033
+ const publishUrl = pythonPublishUrl(options.registry);
1034
+ if (options.ci === "azure" || options.ci === "both") {
1035
+ writeFileEnsured((0, import_node_path8.join)(workspaceRoot, "azure-pipelines.yml"), azurePipelinesYaml(options.agent, options.variableGroup, publishUrl, options.registry.kind));
1036
+ }
1037
+ if (options.ci === "github" || options.ci === "both") {
1038
+ writeFileEnsured((0, import_node_path8.join)(workspaceRoot, ".github/workflows/ci.yml"), githubActionsYaml(options.agent, publishUrl, options.registry.kind));
1039
+ }
1040
+ }
1041
+
1042
+ // src/commands/new.ts
1043
+ async function resolveStack(options) {
1044
+ const fromFlags = { linter: options.linter, testRunner: options.testRunner };
1045
+ const complete = Boolean(fromFlags.linter && fromFlags.testRunner);
1046
+ if (complete || options.yes) {
1047
+ return {
1048
+ linter: fromFlags.linter ?? DEFAULT_STACK.linter,
1049
+ testRunner: fromFlags.testRunner ?? DEFAULT_STACK.testRunner
1050
+ };
1051
+ }
1052
+ const prompted = await promptStack();
1053
+ return {
1054
+ linter: fromFlags.linter ?? prompted.linter,
1055
+ testRunner: fromFlags.testRunner ?? prompted.testRunner
1056
+ };
1057
+ }
1058
+ var CI_PROVIDERS = /* @__PURE__ */ new Set(["azure", "github", "both"]);
1059
+ async function resolveCi(options) {
1060
+ if (options.ci && CI_PROVIDERS.has(options.ci)) {
1061
+ return options.ci;
1062
+ }
1063
+ if (options.yes) {
1064
+ return "azure";
1065
+ }
1066
+ return await promptCi();
1067
+ }
1068
+ async function resolveRegistry(options) {
1069
+ if (options.registry === "azure-artifacts" || options.organization && options.artifactsFeed) {
1070
+ return {
1071
+ kind: "azure-artifacts",
1072
+ organization: options.organization ?? await promptText("Azure DevOps organization"),
1073
+ project: options.project ?? await promptText("Azure DevOps project"),
1074
+ artifactsFeed: options.artifactsFeed ?? await promptText("Artifacts feed name")
1075
+ };
1076
+ }
1077
+ if (options.registry === "npm" || options.yes) {
1078
+ return { kind: "npm" };
1079
+ }
1080
+ return await promptRegistry();
1081
+ }
1082
+ async function runNew(name, options) {
1083
+ const workspaceName = name ?? await promptText("Workspace name");
1084
+ assertValidProjectName(workspaceName, "Workspace name");
1085
+ const scope = options.scope ?? (options.yes ? `@${workspaceName}` : await promptText("npm scope for publishable packages", `@${workspaceName}`));
1086
+ const registry = await resolveRegistry(options);
1087
+ const ci = await resolveCi(options);
1088
+ const agent = options.agent ?? (options.yes ? "ubuntu-latest" : await promptText("CI build agent/runner (vmImage, GitHub Actions runner label, or self-hosted pool name)", "ubuntu-latest"));
1089
+ const variableGroup = ci === "github" ? options.variableGroup ?? "Build" : options.variableGroup ?? (options.yes ? "Build" : await promptText("Azure DevOps variable group holding the npm PAT", "Build"));
1090
+ const stack = await resolveStack(options);
1091
+ logger.step(`Creating Nx workspace '${workspaceName}' (preset: ts)`);
1092
+ runNpx([
1093
+ "--yes",
1094
+ "create-nx-workspace@latest",
1095
+ workspaceName,
1096
+ "--preset=ts",
1097
+ "--pm=npm",
1098
+ "--nxCloud=skip",
1099
+ "--no-interactive"
1100
+ ], process.cwd());
1101
+ const workspaceRoot = (0, import_node_path9.join)(process.cwd(), workspaceName);
1102
+ logger.step("Applying MoNecromanCI overlay (release config, .npmrc, commitlint, pipeline, stack)");
1103
+ applyOverlay(workspaceRoot, { scope, registry, agent, variableGroup, ci, stack });
1104
+ const stackDependencies = stack.linter === "oxlint" ? ["oxc-standard"] : [];
1105
+ logger.step(`Installing the toolchain (${stack.linter === "oxlint" ? "oxc-standard" : "eslint"}, commit toolchain)`);
1106
+ const installStatus = runShell("npm", ["install", "--save-dev", ...stackDependencies, "husky", "@commitlint/cli", "@commitlint/config-conventional"], workspaceRoot);
1107
+ if (installStatus !== 0) {
1108
+ throw new Error(`npm install of the toolchain failed with exit code ${installStatus}`);
1109
+ }
1110
+ runShell("npx", ["husky"], workspaceRoot);
1111
+ logger.success("Done. Next steps:");
1112
+ logger.info(` cd ${workspaceName}`);
1113
+ logger.info(" mnci2 add react-app web # or: node-app, node-function-app, npm-lib, internal-lib,");
1114
+ logger.info(" # python-app, python-function-app, python-lib, python-internal-lib");
1115
+ logger.info(' git add -A && git commit -m "feat: initial workspace"');
1116
+ }
1117
+
1118
+ // src/commands/interactive.ts
1119
+ async function runInteractive() {
1120
+ const inWorkspace = fileExists((0, import_node_path10.join)(process.cwd(), "nx.json"));
1121
+ const newChoice = { name: "Create a new monorepo", value: "new" };
1122
+ const addChoice = { name: "Add a project to this workspace", value: "add" };
1123
+ const action = await (0, import_prompts6.select)({
1124
+ message: "What would you like to do?",
1125
+ choices: inWorkspace ? [addChoice, newChoice] : [newChoice, addChoice]
1126
+ });
1127
+ if (action === "new") {
1128
+ await runNew(void 0, {});
1129
+ } else {
1130
+ await runAdd(void 0, void 0, {});
1131
+ }
1132
+ }
1133
+
1134
+ // src/cli.ts
1135
+ function readVersion() {
1136
+ try {
1137
+ const package_ = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path11.join)(__dirname, "..", "package.json"), "utf8"));
1138
+ return package_.version ?? "0.0.0";
1139
+ } catch {
1140
+ return "0.0.0";
1141
+ }
1142
+ }
1143
+ function buildProgram() {
1144
+ const program = new import_commander.Command();
1145
+ program.name("mnci2").description("MoNecromanCI v2 \u2014 a thin CLI over official Nx plugins").version(readVersion(), "-v, --version", "display the version");
1146
+ program.command("new").argument("[name]", "workspace name").description("Create a new monorepo (Nx TS preset + release/pipeline overlay)").option("-y, --yes", "accept defaults for anything not passed as a flag").option("--scope <scope>", "npm scope for publishable packages (e.g. @demo)").option("--registry <kind>", "azure-artifacts | npm").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option("--agent <pool>", "CI build agent: a vmImage (e.g. ubuntu-latest) or self-hosted pool name").option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option("--ci <provider>", "CI provider: azure | github | both").option("--linter <linter>", "linter: eslint | oxlint").option("--test-runner <runner>", "unit-test runner: jest | vitest").action(async (name, options) => {
1147
+ await runNew(name, options);
1148
+ });
1149
+ program.command("add").addArgument(new import_commander.Argument("[kind]", "project kind").choices(PROJECT_KINDS)).argument("[name]", "project name").description("Add a project by delegating to the matching Nx plugin generator").option("--scope <scope>", "npm scope for a publishable lib (defaults to @<workspace name>)").action(async (kind, name, options) => {
1150
+ await runAdd(kind, name, options);
1151
+ });
1152
+ program.action(async () => {
1153
+ await runInteractive();
1154
+ });
1155
+ return program;
1156
+ }
1157
+ async function main() {
1158
+ try {
1159
+ await buildProgram().parseAsync(process.argv);
1160
+ } catch (error) {
1161
+ logger.error(error instanceof Error ? error.message : String(error));
1162
+ process.exitCode = 1;
1163
+ }
1164
+ }
1165
+ if (require.main === module) {
1166
+ main();
1167
+ }
1168
+ // Annotate the CommonJS export names for ESM import in node:
1169
+ 0 && (module.exports = {
1170
+ buildProgram,
1171
+ main
1172
+ });
1173
+ //# sourceMappingURL=cli.js.map