@mnci/cli 4.51.0 → 4.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -507,6 +507,28 @@ from File`), and the curated root scripts.
507
507
  4. Installs the chosen **stack** (see below), `husky` + `@commitlint/*` for
508
508
  real, so versions resolve at generation time.
509
509
 
510
+ ## A whole shape in one step (`mnci new --preset`)
511
+
512
+ Everything else in mnci is per project. `mnci new my-shop --preset web-api` also scaffolds a wired-together shape, so the
513
+ result is a starting point that already runs rather than three projects to connect:
514
+
515
+ | Preset | Adds | Wiring |
516
+ |---|---|---|
517
+ | `web-api` | `libs/shared` (an internal library), `apps/api` (Express) and `apps/web` (React) | the API answers `GET /api/greeting` with the shared library's `greet`; the frontend asks that route and types the answer with the shared `Greeting`; both declare `@<scope>/shared`; Vite forwards `/api` to the API in development |
518
+
519
+ Each project is added through `mnci add`, so it is exactly what `add` makes, and the preset then edits them into one shape:
520
+ the API and the frontend drop their own copy of the sample use case and contract (the library owns them now), each gets a
521
+ spec that matches (written without a mock library, so it runs under Jest or Vitest), and one install, one `nx sync` and one
522
+ format finish it. `mnci dev web api` starts both servers, and asking the frontend's origin for `/api/greeting` returns the
523
+ API's answer through the proxy.
524
+
525
+ A misspelt preset is refused before the workspace is generated, and the wiring edits fail with the file named, not
526
+ silently, if a generator's output ever stops having the text they change. A preset is a starting point to edit: delete what
527
+ you do not need. New presets are an entry in `workspace-presets/preset-catalog.config.ts` plus their wiring.
528
+
529
+ The `web-api preset` e2e section generates one and checks lint, typecheck, test and build for all three projects, the
530
+ specs under Vitest, and the proxied request while both servers run.
531
+
510
532
  ## Starting projects together (`mnci dev`)
511
533
 
512
534
  `<name>:start` starts one project. `mnci dev web api` starts several at once, the usual case being a frontend and the
package/dist/cli.js CHANGED
@@ -10317,9 +10317,281 @@ async function runInteractive(program, prompter = createInquirerPrompter(), work
10317
10317
  }
10318
10318
  }
10319
10319
 
10320
- // src/workspace-creation/adopt-directory.use-case.ts
10320
+ // src/workspace-presets/preset-catalog.config.ts
10321
+ var PRESETS = [
10322
+ {
10323
+ id: "web-api",
10324
+ summary: "a React frontend and an Express API that share one internal library: the web asks the API, the API answers with the shared greeting, and `mnci dev web api` starts both",
10325
+ projects: [
10326
+ { kind: "internal-lib", name: "shared", options: {} },
10327
+ { kind: "node-app", name: "api", options: { framework: "express" } },
10328
+ { kind: "react-app", name: "web", options: {} }
10329
+ ]
10330
+ }
10331
+ ];
10332
+ var PRESET_IDS = PRESETS.map((preset) => preset.id);
10333
+
10334
+ // src/workspace-presets/wire-web-api.use-case.ts
10321
10335
  var import_node_fs40 = require("fs");
10322
10336
  var import_node_path50 = require("path");
10337
+
10338
+ // src/workspace-presets/web-api-wiring.algorithm.ts
10339
+ function apiHandler(shared) {
10340
+ return `import { greet } from '${shared}'
10341
+ import type { Request, Response } from 'express'
10342
+
10343
+ export function helloHandler (request: Request, response: Response): void {
10344
+ const name = typeof request.query.name === 'string' ? request.query.name : 'world'
10345
+ response.send(greet(name))
10346
+ }
10347
+ `;
10348
+ }
10349
+ function apiHandlerSpec(shared) {
10350
+ return String.raw`import { greet } from '${shared}'
10351
+ import type { Request, Response } from 'express'
10352
+ import { helloHandler } from './hello.handler'
10353
+
10354
+ /** A response that keeps what it was sent, so the spec needs no mock library and runs under Jest or Vitest. */
10355
+ function recordingResponse (): { response: Response, sent: unknown[] } {
10356
+ const sent: unknown[] = []
10357
+
10358
+ return { sent, response: { send: (value: unknown) => { sent.push(value) } } as unknown as Response }
10359
+ }
10360
+
10361
+ describe('helloHandler', () => {
10362
+ it('answers with the shared library\'s greeting for the name asked', () => {
10363
+ const { response, sent } = recordingResponse()
10364
+
10365
+ helloHandler({ query: { name: 'web' } } as unknown as Request, response)
10366
+
10367
+ expect(sent).toEqual([greet('web')])
10368
+ })
10369
+
10370
+ it('greets the world when no name is asked', () => {
10371
+ const { response, sent } = recordingResponse()
10372
+
10373
+ helloHandler({ query: {} } as unknown as Request, response)
10374
+
10375
+ expect(sent).toEqual([greet('world')])
10376
+ })
10377
+ })
10378
+ `;
10379
+ }
10380
+ function apiBarrel() {
10381
+ return "export * from './hello.handler'\n";
10382
+ }
10383
+ function webGreetingClient(shared) {
10384
+ return `import type { Greeting } from '${shared}'
10385
+
10386
+ export async function fetchGreeting (name: string, request: typeof fetch = fetch): Promise<Greeting> {
10387
+ const response = await request('/api/greeting?name=' + encodeURIComponent(name))
10388
+
10389
+ return await response.json() as Greeting
10390
+ }
10391
+ `;
10392
+ }
10393
+ function webGreetingClientSpec() {
10394
+ return String.raw`import { fetchGreeting } from './greeting.client'
10395
+
10396
+ describe('fetchGreeting', () => {
10397
+ it('asks the API\'s greeting route for the name and returns the answer', async () => {
10398
+ const requested: string[] = []
10399
+ const answering = (async (input: string) => {
10400
+ requested.push(input)
10401
+
10402
+ return { json: async () => ({ message: 'Hello, web!' }) } as Response
10403
+ }) as unknown as typeof fetch
10404
+
10405
+ expect(await fetchGreeting('web', answering)).toEqual({ message: 'Hello, web!' })
10406
+ expect(requested).toEqual(['/api/greeting?name=web'])
10407
+ })
10408
+ })
10409
+ `;
10410
+ }
10411
+ function webGreetingComponent(shared) {
10412
+ return `import { useEffect, useState } from 'react'
10413
+ import type { Greeting } from '${shared}'
10414
+ import { fetchGreeting } from './greeting.client'
10415
+
10416
+ export interface GreetingViewProps {
10417
+ readonly name: string
10418
+ readonly load?: (name: string) => Promise<Greeting>
10419
+ }
10420
+
10421
+ export function GreetingView ({ name, load = fetchGreeting }: GreetingViewProps) {
10422
+ const [greeting, setGreeting] = useState<Greeting | undefined>()
10423
+ const [failed, setFailed] = useState(false)
10424
+
10425
+ useEffect(() => {
10426
+ void (async () => {
10427
+ try {
10428
+ setGreeting(await load(name))
10429
+ } catch {
10430
+ setFailed(true)
10431
+ }
10432
+ })()
10433
+ }, [name, load])
10434
+
10435
+ if (failed) {
10436
+ return <p>Could not reach the API.</p>
10437
+ }
10438
+
10439
+ return <p>{greeting?.message ?? 'Loading...'}</p>
10440
+ }
10441
+ `;
10442
+ }
10443
+ function webGreetingSpec() {
10444
+ return `import { render } from '@testing-library/react'
10445
+
10446
+ import { GreetingView } from './greeting.component'
10447
+
10448
+ async function answering (name: string): Promise<{ message: string }> {
10449
+ return { message: 'Hello, ' + name + '!' }
10450
+ }
10451
+
10452
+ async function offline (): Promise<never> {
10453
+ throw new Error('offline')
10454
+ }
10455
+
10456
+ describe('GreetingView', () => {
10457
+ it('shows the greeting its loader returns for its name', async () => {
10458
+ const { findByText } = render(<GreetingView name='world' load={answering} />)
10459
+
10460
+ expect(await findByText('Hello, world!')).toBeTruthy()
10461
+ })
10462
+
10463
+ it('says so when the API cannot be reached', async () => {
10464
+ const { findByText } = render(<GreetingView name='world' load={offline} />)
10465
+
10466
+ expect(await findByText('Could not reach the API.')).toBeTruthy()
10467
+ })
10468
+ })
10469
+ `;
10470
+ }
10471
+ function webGreetingBarrel() {
10472
+ return "export * from './greeting.client'\nexport * from './greeting.component'\n";
10473
+ }
10474
+ function webAppSpec() {
10475
+ return `import { render } from '@testing-library/react'
10476
+
10477
+ import App from './app.component'
10478
+
10479
+ describe('App', () => {
10480
+ it('renders, and shows that it is loading the greeting', () => {
10481
+ const { getByText } = render(<App />)
10482
+
10483
+ expect(getByText('Loading...')).toBeTruthy()
10484
+ })
10485
+ })
10486
+ `;
10487
+ }
10488
+ function withApiProxy(config, target) {
10489
+ const start = config.indexOf("server:");
10490
+ const end = start === -1 ? -1 : config.indexOf("},", start);
10491
+ if (end === -1 || config.includes("proxy:")) {
10492
+ return config;
10493
+ }
10494
+ const block = config.slice(start, end).trimEnd();
10495
+ return `${config.slice(0, start)}${block}
10496
+ proxy: { '/api': '${target}' },
10497
+ ${config.slice(end)}`;
10498
+ }
10499
+
10500
+ // src/workspace-presets/wire-web-api.use-case.ts
10501
+ var API_ORIGIN = "http://localhost:3000";
10502
+ function declareDependency(manifestPath, name) {
10503
+ const manifest = readJson(manifestPath);
10504
+ writeFileEnsured(manifestPath, toJson({ ...manifest, dependencies: { ...manifest.dependencies, [name]: "*" } }));
10505
+ }
10506
+ function removeFiles(slice, files) {
10507
+ for (const file of files) {
10508
+ (0, import_node_fs40.rmSync)((0, import_node_path50.join)(slice, file), { force: true });
10509
+ }
10510
+ }
10511
+ function replaceOnce(path, anchor, replacement) {
10512
+ const text = (0, import_node_fs40.readFileSync)(path, "utf8");
10513
+ if (!text.includes(anchor)) {
10514
+ throw new Error(`${path} has no ${anchor}; the generator's output changed, so the preset cannot wire it.`);
10515
+ }
10516
+ writeFileEnsured(path, text.replace(anchor, () => replacement));
10517
+ }
10518
+ function proxyApi(viteConfigPath) {
10519
+ const text = (0, import_node_fs40.readFileSync)(viteConfigPath, "utf8");
10520
+ const proxied = withApiProxy(text, API_ORIGIN);
10521
+ if (proxied === text && !text.includes("proxy:")) {
10522
+ throw new Error(`${viteConfigPath} has no server block to add the /api proxy to; the generator's output changed, so the preset cannot wire it.`);
10523
+ }
10524
+ writeFileEnsured(viteConfigPath, proxied);
10525
+ }
10526
+ function wireWebApi(workspaceRoot, projects) {
10527
+ const sharedManifest = (0, import_node_path50.join)(workspaceRoot, "libs", projects.shared, "package.json");
10528
+ if (!fileExists(sharedManifest)) {
10529
+ throw new Error(`The shared library was not added at libs/${projects.shared}, so there is nothing to wire the API and the frontend to.`);
10530
+ }
10531
+ const shared = readJson(sharedManifest).name;
10532
+ const api = (0, import_node_path50.join)(workspaceRoot, "apps", projects.api);
10533
+ declareDependency((0, import_node_path50.join)(api, "package.json"), shared);
10534
+ const hello = (0, import_node_path50.join)(api, "src", "hello");
10535
+ writeFileEnsured((0, import_node_path50.join)(hello, "hello.handler.ts"), apiHandler(shared));
10536
+ writeFileEnsured((0, import_node_path50.join)(hello, "hello.handler.spec.ts"), apiHandlerSpec(shared));
10537
+ writeFileEnsured((0, import_node_path50.join)(hello, "index.ts"), apiBarrel());
10538
+ removeFiles(hello, ["greet.use-case.ts", "greet.use-case.spec.ts", "greeting.contract.ts"]);
10539
+ replaceOnce((0, import_node_path50.join)(api, "src", "main.ts"), "app.get('/', helloHandler)", "app.get('/api/greeting', helloHandler)");
10540
+ const web = (0, import_node_path50.join)(workspaceRoot, "apps", projects.web);
10541
+ declareDependency((0, import_node_path50.join)(web, "package.json"), shared);
10542
+ const greeting = (0, import_node_path50.join)(web, "src", "greeting");
10543
+ writeFileEnsured((0, import_node_path50.join)(greeting, "greeting.client.ts"), webGreetingClient(shared));
10544
+ writeFileEnsured((0, import_node_path50.join)(greeting, "greeting.client.spec.ts"), webGreetingClientSpec());
10545
+ writeFileEnsured((0, import_node_path50.join)(greeting, "greeting.component.tsx"), webGreetingComponent(shared));
10546
+ writeFileEnsured((0, import_node_path50.join)(greeting, "greeting.component.spec.tsx"), webGreetingSpec());
10547
+ writeFileEnsured((0, import_node_path50.join)(greeting, "index.ts"), webGreetingBarrel());
10548
+ removeFiles(greeting, ["greet.use-case.ts", "greet.use-case.spec.ts", "greeting.contract.ts"]);
10549
+ writeFileEnsured((0, import_node_path50.join)(web, "src", "app", "app.component.spec.tsx"), webAppSpec());
10550
+ proxyApi((0, import_node_path50.join)(web, "vite.config.mts"));
10551
+ }
10552
+
10553
+ // src/workspace-presets/apply-preset.use-case.ts
10554
+ function findPreset(id) {
10555
+ const preset = PRESETS.find((candidate) => candidate.id === id);
10556
+ if (preset === void 0) {
10557
+ throw new Error(`Unknown preset '${id}'. Presets: ${PRESET_IDS.join(", ")}.`);
10558
+ }
10559
+ return preset;
10560
+ }
10561
+ function nameOf(preset, kind) {
10562
+ const project = preset.projects.find((candidate) => candidate.kind === kind);
10563
+ if (project === void 0) {
10564
+ throw new Error(`The ${preset.id} preset has no ${kind}.`);
10565
+ }
10566
+ return project.name;
10567
+ }
10568
+ async function applyPreset(workspaceRoot, presetId, dependencies = {}) {
10569
+ const preset = findPreset(presetId);
10570
+ const add = dependencies.add ?? runAdd;
10571
+ const install = dependencies.install ?? ((root) => runShell("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund"], root));
10572
+ const sync = dependencies.sync ?? syncProjectReferences;
10573
+ const format = dependencies.format ?? runFormatter;
10574
+ logger.step(`Adding the ${preset.id} preset: ${preset.projects.map((project) => project.name).join(", ")}`);
10575
+ const previous = process.cwd();
10576
+ process.chdir(workspaceRoot);
10577
+ try {
10578
+ for (const project of preset.projects) {
10579
+ await add(project.kind, project.name, project.options);
10580
+ }
10581
+ } finally {
10582
+ process.chdir(previous);
10583
+ }
10584
+ wireWebApi(workspaceRoot, { shared: nameOf(preset, "internal-lib"), api: nameOf(preset, "node-app"), web: nameOf(preset, "react-app") });
10585
+ if (install(workspaceRoot) !== 0) {
10586
+ throw new Error("npm install failed after wiring the preset. Run `npm install` and read its error; the projects themselves were added.");
10587
+ }
10588
+ sync(workspaceRoot);
10589
+ format(workspaceRoot);
10590
+ }
10591
+
10592
+ // src/workspace-creation/adopt-directory.use-case.ts
10593
+ var import_node_fs41 = require("fs");
10594
+ var import_node_path51 = require("path");
10323
10595
  var NEVER_COPIED = /* @__PURE__ */ new Set([".git", "node_modules"]);
10324
10596
  var AGENT_AND_CI_ENTRIES = [
10325
10597
  "AGENTS.md",
@@ -10350,13 +10622,13 @@ var PREFLIGHT_BLOCKERS = [
10350
10622
  "packages"
10351
10623
  ];
10352
10624
  function assertAdoptableDirectory(target) {
10353
- if (!(0, import_node_fs40.existsSync)(target)) {
10625
+ if (!(0, import_node_fs41.existsSync)(target)) {
10354
10626
  throw new Error(`--into '${target}' does not exist. Create or clone it first.`);
10355
10627
  }
10356
- if (!(0, import_node_fs40.statSync)(target).isDirectory()) {
10628
+ if (!(0, import_node_fs41.statSync)(target).isDirectory()) {
10357
10629
  throw new Error(`--into '${target}' is not a directory.`);
10358
10630
  }
10359
- const blockers = PREFLIGHT_BLOCKERS.filter((entry) => (0, import_node_fs40.existsSync)((0, import_node_path50.join)(target, entry)));
10631
+ const blockers = PREFLIGHT_BLOCKERS.filter((entry) => (0, import_node_fs41.existsSync)((0, import_node_path51.join)(target, entry)));
10360
10632
  if (blockers.length > 0) {
10361
10633
  throw new Error(
10362
10634
  `--into '${target}' already holds ${blockers.join(", ")}, so it is a project already. Use \`mnci upgrade\` to re-apply the overlay to an existing workspace, or point --into at an empty checkout.`
@@ -10364,15 +10636,15 @@ function assertAdoptableDirectory(target) {
10364
10636
  }
10365
10637
  }
10366
10638
  function mergeGitignore(generatedFile, targetFile) {
10367
- const existing = (0, import_node_fs40.readFileSync)(targetFile, "utf8");
10639
+ const existing = (0, import_node_fs41.readFileSync)(targetFile, "utf8");
10368
10640
  const present = new Set(existing.split("\n").map((line) => line.trim()));
10369
- const additions = (0, import_node_fs40.readFileSync)(generatedFile, "utf8").split("\n").map((line) => line.trimEnd()).filter((line) => {
10641
+ const additions = (0, import_node_fs41.readFileSync)(generatedFile, "utf8").split("\n").map((line) => line.trimEnd()).filter((line) => {
10370
10642
  const trimmed = line.trim();
10371
10643
  return trimmed !== "" && !trimmed.startsWith("#") && !present.has(trimmed);
10372
10644
  });
10373
10645
  if (additions.length === 0) return false;
10374
10646
  const separator = existing.endsWith("\n") ? "" : "\n";
10375
- (0, import_node_fs40.writeFileSync)(
10647
+ (0, import_node_fs41.writeFileSync)(
10376
10648
  targetFile,
10377
10649
  `${existing}${separator}
10378
10650
  # Added by mnci (Nx workspace)
@@ -10382,9 +10654,9 @@ ${additions.join("\n")}
10382
10654
  return true;
10383
10655
  }
10384
10656
  function adoptGeneratedWorkspace(generatedRoot, target) {
10385
- const entries = (0, import_node_fs40.readdirSync)(generatedRoot).filter((entry) => !NEVER_COPIED.has(entry));
10657
+ const entries = (0, import_node_fs41.readdirSync)(generatedRoot).filter((entry) => !NEVER_COPIED.has(entry));
10386
10658
  const collisions = entries.filter(
10387
- (entry) => (0, import_node_fs40.existsSync)((0, import_node_path50.join)(target, entry)) && !EXISTING_WINS.has(entry) && entry !== ".gitignore"
10659
+ (entry) => (0, import_node_fs41.existsSync)((0, import_node_path51.join)(target, entry)) && !EXISTING_WINS.has(entry) && entry !== ".gitignore"
10388
10660
  );
10389
10661
  if (collisions.length > 0) {
10390
10662
  throw new Error(
@@ -10393,28 +10665,28 @@ function adoptGeneratedWorkspace(generatedRoot, target) {
10393
10665
  }
10394
10666
  const result = { copied: [], kept: [], mergedGitignore: false };
10395
10667
  for (const entry of entries) {
10396
- const from = (0, import_node_path50.join)(generatedRoot, entry);
10397
- const to = (0, import_node_path50.join)(target, entry);
10398
- if (entry === ".gitignore" && (0, import_node_fs40.existsSync)(to)) {
10668
+ const from = (0, import_node_path51.join)(generatedRoot, entry);
10669
+ const to = (0, import_node_path51.join)(target, entry);
10670
+ if (entry === ".gitignore" && (0, import_node_fs41.existsSync)(to)) {
10399
10671
  result.mergedGitignore = mergeGitignore(from, to);
10400
10672
  result.kept.push(entry);
10401
10673
  continue;
10402
10674
  }
10403
- if ((0, import_node_fs40.existsSync)(to)) {
10675
+ if ((0, import_node_fs41.existsSync)(to)) {
10404
10676
  result.kept.push(entry);
10405
10677
  continue;
10406
10678
  }
10407
- (0, import_node_fs40.cpSync)(from, to, { recursive: true });
10679
+ (0, import_node_fs41.cpSync)(from, to, { recursive: true });
10408
10680
  result.copied.push(entry);
10409
10681
  }
10410
- (0, import_node_fs40.rmSync)(generatedRoot, { recursive: true, force: true });
10682
+ (0, import_node_fs41.rmSync)(generatedRoot, { recursive: true, force: true });
10411
10683
  return result;
10412
10684
  }
10413
10685
 
10414
10686
  // src/workspace-creation/create-workspace.use-case.ts
10415
- var import_node_fs41 = require("fs");
10687
+ var import_node_fs42 = require("fs");
10416
10688
  var import_node_os2 = require("os");
10417
- var import_node_path51 = require("path");
10689
+ var import_node_path52 = require("path");
10418
10690
  function nxCloudProviderValue(ci) {
10419
10691
  return ci === "azure" ? "azure" : "github";
10420
10692
  }
@@ -10457,9 +10729,10 @@ async function resolveRegistry2(options) {
10457
10729
  return await promptRegistry();
10458
10730
  }
10459
10731
  async function runNew(name, options) {
10460
- const adoptTarget = options.into === void 0 ? void 0 : (0, import_node_path51.resolve)(process.cwd(), options.into);
10732
+ const adoptTarget = options.into === void 0 ? void 0 : (0, import_node_path52.resolve)(process.cwd(), options.into);
10461
10733
  if (adoptTarget !== void 0) assertAdoptableDirectory(adoptTarget);
10462
- const workspaceName = name ?? (adoptTarget === void 0 ? await promptText("Workspace name") : (0, import_node_path51.basename)(adoptTarget));
10734
+ if (options.preset !== void 0) findPreset(options.preset);
10735
+ const workspaceName = name ?? (adoptTarget === void 0 ? await promptText("Workspace name") : (0, import_node_path52.basename)(adoptTarget));
10463
10736
  assertValidProjectName(workspaceName, "Workspace name");
10464
10737
  const scope = options.scope ?? (options.yes ? `@${workspaceName}` : await promptText("npm scope for publishable packages", `@${workspaceName}`));
10465
10738
  const registry = await resolveRegistry2(options);
@@ -10472,8 +10745,8 @@ async function runNew(name, options) {
10472
10745
  const npmAuth = resolveNpmAuth(options.npmAuth, registry, ci);
10473
10746
  const stack = await resolveStack(options);
10474
10747
  const nxCloud = options.nxCloud ?? (options.yes ? false : await promptNxCloud());
10475
- const stagingParent = adoptTarget === void 0 ? process.cwd() : (0, import_node_fs41.mkdtempSync)((0, import_node_path51.join)((0, import_node_os2.tmpdir)(), "mnci-new-"));
10476
- let workspaceRoot = (0, import_node_path51.join)(stagingParent, workspaceName);
10748
+ const stagingParent = adoptTarget === void 0 ? process.cwd() : (0, import_node_fs42.mkdtempSync)((0, import_node_path52.join)((0, import_node_os2.tmpdir)(), "mnci-new-"));
10749
+ let workspaceRoot = (0, import_node_path52.join)(stagingParent, workspaceName);
10477
10750
  try {
10478
10751
  logger.step(`Creating Nx workspace '${workspaceName}' (preset: ts)`);
10479
10752
  runNpx(
@@ -10496,7 +10769,7 @@ async function runNew(name, options) {
10496
10769
  workspaceRoot = adoptTarget;
10497
10770
  }
10498
10771
  } finally {
10499
- if (adoptTarget !== void 0) (0, import_node_fs41.rmSync)(stagingParent, { recursive: true, force: true });
10772
+ if (adoptTarget !== void 0) (0, import_node_fs42.rmSync)(stagingParent, { recursive: true, force: true });
10500
10773
  }
10501
10774
  logger.step(
10502
10775
  "Applying MoNecromanCI overlay (VS Code workspace, release config, .npmrc, commitlint, pipeline, stack)"
@@ -10506,8 +10779,8 @@ async function runNew(name, options) {
10506
10779
  { workspaceName, scope, registry, agent, variableGroup, ci, stack, ...npmAuth && { npmAuth } },
10507
10780
  logger.detail
10508
10781
  );
10509
- (0, import_node_fs41.rmSync)((0, import_node_path51.join)(workspaceRoot, "node_modules"), { recursive: true, force: true });
10510
- (0, import_node_fs41.rmSync)((0, import_node_path51.join)(workspaceRoot, "package-lock.json"), { force: true });
10782
+ (0, import_node_fs42.rmSync)((0, import_node_path52.join)(workspaceRoot, "node_modules"), { recursive: true, force: true });
10783
+ (0, import_node_fs42.rmSync)((0, import_node_path52.join)(workspaceRoot, "package-lock.json"), { force: true });
10511
10784
  logger.step("Installing the toolchain (commitlint + husky + linting/formatting)");
10512
10785
  const installStatus = runShell(
10513
10786
  "npm",
@@ -10520,6 +10793,9 @@ async function runNew(name, options) {
10520
10793
  runShell("npx", ["husky"], workspaceRoot);
10521
10794
  logger.step("Formatting the workspace (eslint --fix, JavaScript Standard Style)");
10522
10795
  runFormatter(workspaceRoot);
10796
+ if (options.preset !== void 0) {
10797
+ await applyPreset(workspaceRoot, options.preset);
10798
+ }
10523
10799
  logger.success("Done. Next steps:");
10524
10800
  if (adoptTarget === void 0) logger.info(` cd ${workspaceName}`);
10525
10801
  for (const line of projectKindHintLines()) {
@@ -10557,6 +10833,8 @@ function buildProgram(cliVersion) {
10557
10833
  ).addOption(new import_commander.Option("--ci <provider>", "CI provider").choices(["azure", "github", "both"])).option("--test-runner <runner>", "unit-test runner: jest | vitest").option("--nx-cloud", "connect the workspace to Nx Cloud (remote caching + CI insights)").option(
10558
10834
  "--into <dir>",
10559
10835
  "bootstrap into an existing directory (a fresh clone holding only .git); the directory name is the workspace name unless one is given"
10836
+ ).addOption(
10837
+ new import_commander.Option("--preset <preset>", "also scaffold a whole shape of workspace, its projects wired together (web-api: a React frontend and an Express API sharing one library)").choices(PRESET_IDS)
10560
10838
  ).action(async (name, options) => {
10561
10839
  await runNew(name, options);
10562
10840
  });