@clearlydev/nx-archweave 0.1.1 → 0.2.1

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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/dist/eslint/fsd-boundaries.d.ts +25 -8
  3. package/dist/eslint/fsd-policy.d.ts +38 -0
  4. package/dist/eslint/index.d.ts +10 -9
  5. package/dist/eslint/index.js +227 -34
  6. package/dist/eslint/slice-groups.d.ts +12 -0
  7. package/dist/generators/add-agent-rules/generator.js +1175 -23
  8. package/dist/generators/add-backend/generator.js +1127 -75
  9. package/dist/generators/add-fsd-lite/generator.js +1274 -96
  10. package/dist/generators/add-review/files/dependabot.yml +7 -0
  11. package/dist/generators/add-review/generator.js +1100 -46
  12. package/dist/generators/doctor/generator.js +780 -96
  13. package/dist/generators/entity/generator.js +251 -32
  14. package/dist/generators/feature/generator.js +251 -32
  15. package/dist/generators/init/generator.js +1955 -549
  16. package/dist/generators/page/generator.js +251 -32
  17. package/dist/generators/shared/generator.js +251 -32
  18. package/dist/generators/widget/generator.js +251 -32
  19. package/package.json +4 -1
  20. package/src/generators/add-agent-rules/schema.json +5 -0
  21. package/src/generators/add-fsd-lite/schema.json +2 -2
  22. package/src/generators/entity/schema.json +11 -1
  23. package/src/generators/feature/schema.json +11 -1
  24. package/src/generators/init/schema.json +13 -2
  25. package/src/generators/page/schema.json +15 -3
  26. package/src/generators/shared/schema.json +7 -1
  27. package/src/generators/widget/schema.json +11 -1
  28. package/dist/eslint/fsd-boundaries.js +0 -144
  29. package/dist/eslint/tsconfig-aliases.js +0 -114
  30. package/dist/generators/add-agent-rules/render.js +0 -46
  31. package/dist/generators/add-backend/render.js +0 -102
  32. package/dist/generators/add-fsd-lite/render.js +0 -72
  33. package/dist/generators/add-github-templates/render.js +0 -26
  34. package/dist/generators/add-integration/render.js +0 -34
  35. package/dist/generators/add-review/render.js +0 -96
  36. package/dist/generators/allow-builds/render.js +0 -82
  37. package/dist/generators/doctor/integration-report.js +0 -70
  38. package/dist/generators/doctor/render.js +0 -10
  39. package/dist/generators/init/render.js +0 -125
  40. package/dist/generators/init/validation.js +0 -74
  41. package/dist/generators/install-integration/render.js +0 -128
  42. package/dist/generators/record-integration/render.js +0 -31
  43. package/dist/generators/shared/dry-run.js +0 -12
  44. package/dist/generators/shared/frontend-app.js +0 -57
  45. package/dist/generators/shared/project-type.js +0 -39
  46. package/dist/generators/shared/record-pipeline.js +0 -34
  47. package/dist/generators/shared/run-nx.js +0 -24
  48. package/dist/generators/shared/template-files.js +0 -38
  49. package/dist/generators/shared/toolkit-config-file.js +0 -110
  50. package/dist/generators/slice/core.js +0 -185
  51. package/dist/generators/slice/render.js +0 -70
  52. package/dist/lib/nx-plugin.js +0 -7
  53. package/dist/runtime/integrations-composition-root.js +0 -20
  54. package/dist/runtime/node-command-runner.js +0 -81
@@ -243,6 +243,20 @@ function buildReviewConfigSnippet(input) {
243
243
  };
244
244
  }
245
245
 
246
+ // ../template-engine/dist/review/action-pins.js
247
+ var ACTION_PINS = {
248
+ "actions/checkout": "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1",
249
+ "actions/setup-node": "actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0",
250
+ "pnpm/action-setup": "pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0",
251
+ "oven-sh/setup-bun": "oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0",
252
+ "anthropics/claude-code-action": "anthropics/claude-code-action@6b6870e649b5ff408753befeaee5fc7989d4e6a9 # v1.0.258",
253
+ "google-github-actions/run-gemini-cli": "google-github-actions/run-gemini-cli@f77273f4c914e4bf38440cf36a0369cb64a37489 # v0.1.22",
254
+ "openai/codex-action": "openai/codex-action@bdf19a4a223ec2549a3e2274a0cf61556bc07675 # v1.13"
255
+ };
256
+ function pinnedUses(action) {
257
+ return ACTION_PINS[action];
258
+ }
259
+
246
260
  // ../template-engine/dist/review/workflow-skeleton.js
247
261
  function indentBlock(text, spaces) {
248
262
  const pad = " ".repeat(spaces);
@@ -261,6 +275,17 @@ function renderConcurrencyBlock() {
261
275
  function renderPermissionsBlock() {
262
276
  return ["permissions:", " contents: read", " pull-requests: write", " id-token: write"].join("\n");
263
277
  }
278
+ function renderCheckoutStep(gateCondition, extraWith = []) {
279
+ return [
280
+ "- name: Checkout repository",
281
+ ` if: ${gateCondition}`,
282
+ ` uses: ${pinnedUses("actions/checkout")}`,
283
+ " with:",
284
+ ...extraWith.map((line) => ` ${line}`),
285
+ " fetch-depth: 1",
286
+ " persist-credentials: false"
287
+ ].join("\n");
288
+ }
264
289
  function renderSecretGateStep(secretName) {
265
290
  return [
266
291
  "- name: Check review secret",
@@ -284,7 +309,7 @@ function renderRoundGateStep(maxRounds) {
284
309
  " env:",
285
310
  " GH_TOKEN: ${{ github.token }}",
286
311
  " run: |",
287
- ` counts=$(gh api "repos/\${{ github.repository }}/issues/\${{ github.event.pull_request.number }}/comments" --paginate --jq '[([.[] | select(.body | contains("${ROUND_MARKER}"))] | length), ([.[].id] | max // 0)]' | jq -s -r '[(map(.[0]) | add // 0), (map(.[1]) | max // 0)] | @tsv')`,
312
+ ` counts=$(gh api "repos/\${{ github.repository }}/issues/\${{ github.event.pull_request.number }}/comments" --paginate --jq '[([.[] | select(.user.type == "Bot") | select(.body | contains("${ROUND_MARKER}"))] | length), ([.[].id] | max // 0)]' | jq -s -r '[(map(.[0]) | add // 0), (map(.[1]) | max // 0)] | @tsv')`,
288
313
  ` rounds=$(printf '%s' "$counts" | cut -f1)`,
289
314
  ` echo "last-comment-id=$(printf '%s' "$counts" | cut -f2)" >> "$GITHUB_OUTPUT"`,
290
315
  ` if [ "$rounds" -ge ${maxRounds} ]; then`,
@@ -316,8 +341,7 @@ function renderVerdictGateStep(maxRounds, locale) {
316
341
  ].join("\n");
317
342
  }
318
343
  var SETUP_NODE_VERSION = "22";
319
- var SETUP_PNPM_VERSION = "10";
320
- var SETUP_BUN_ACTION = "oven-sh/setup-bun@v2";
344
+ var FALLBACK_PNPM_VERSION = "10";
321
345
  var INSTALL_LINES = {
322
346
  pnpm: [" pnpm install --frozen-lockfile"],
323
347
  npm: [" npm ci"],
@@ -327,19 +351,39 @@ var INSTALL_LINES = {
327
351
  ],
328
352
  bun: [" bun install --frozen-lockfile"]
329
353
  };
354
+ function renderPnpmVersionStep(gate) {
355
+ return [
356
+ "- name: Detect pnpm version",
357
+ " id: pnpm-version",
358
+ gate,
359
+ " run: |",
360
+ ` if [ -n "$(jq -r '.packageManager // empty' package.json)" ]; then`,
361
+ ' echo "version=" >> "$GITHUB_OUTPUT"',
362
+ " else",
363
+ ` echo "version=${FALLBACK_PNPM_VERSION}" >> "$GITHUB_OUTPUT"`,
364
+ " fi"
365
+ ].join("\n");
366
+ }
330
367
  function renderWorkspaceSetupSteps(packageManager) {
331
368
  const gate = " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
332
369
  const steps = [];
333
370
  if (packageManager === "pnpm") {
334
- steps.push(["- name: Set up pnpm", gate, " uses: pnpm/action-setup@v4", " with:", ` version: ${SETUP_PNPM_VERSION}`].join("\n"));
371
+ steps.push(renderPnpmVersionStep(gate));
372
+ steps.push([
373
+ "- name: Set up pnpm",
374
+ gate,
375
+ ` uses: ${pinnedUses("pnpm/action-setup")}`,
376
+ " with:",
377
+ " version: ${{ steps.pnpm-version.outputs.version }}"
378
+ ].join("\n"));
335
379
  }
336
380
  if (packageManager === "bun") {
337
- steps.push(["- name: Set up bun", gate, ` uses: ${SETUP_BUN_ACTION}`].join("\n"));
381
+ steps.push(["- name: Set up bun", gate, ` uses: ${pinnedUses("oven-sh/setup-bun")}`].join("\n"));
338
382
  }
339
383
  steps.push([
340
384
  "- name: Set up Node",
341
385
  gate,
342
- " uses: actions/setup-node@v4",
386
+ ` uses: ${pinnedUses("actions/setup-node")}`,
343
387
  " with:",
344
388
  ` node-version: ${SETUP_NODE_VERSION}`,
345
389
  // yarn: no setup-node cache — it would query the runner's global yarn 1 even in a
@@ -391,17 +435,11 @@ function buildWorkflowYaml(input) {
391
435
  const steps = [
392
436
  renderSecretGateStep(SECRET_NAME),
393
437
  renderRoundGateStep(input.maxRounds),
394
- [
395
- "- name: Checkout repository",
396
- " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
397
- " uses: actions/checkout@v4",
398
- " with:",
399
- " fetch-depth: 1"
400
- ].join("\n"),
438
+ renderCheckoutStep("steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'"),
401
439
  [
402
440
  "- name: Run Claude review",
403
441
  " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
404
- " uses: anthropics/claude-code-action@v1",
442
+ ` uses: ${pinnedUses("anthropics/claude-code-action")}`,
405
443
  " with:",
406
444
  ` claude_code_oauth_token: \${{ secrets.${SECRET_NAME} }}`,
407
445
  " prompt: |",
@@ -471,13 +509,7 @@ function buildWorkflowYaml2(input) {
471
509
  const steps = [
472
510
  renderSecretGateStep(SECRET_NAME2),
473
511
  renderRoundGateStep(input.maxRounds),
474
- [
475
- "- name: Checkout repository",
476
- ` if: ${gateCondition}`,
477
- " uses: actions/checkout@v4",
478
- " with:",
479
- " fetch-depth: 1"
480
- ].join("\n"),
512
+ renderCheckoutStep(gateCondition),
481
513
  [
482
514
  "- name: Install cursor-agent CLI",
483
515
  ` if: ${gateCondition}`,
@@ -557,17 +589,11 @@ function buildWorkflowYaml3(input) {
557
589
  const steps = [
558
590
  renderSecretGateStep(SECRET_NAME3),
559
591
  renderRoundGateStep(input.maxRounds),
560
- [
561
- "- name: Checkout repository",
562
- " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
563
- " uses: actions/checkout@v4",
564
- " with:",
565
- " fetch-depth: 1"
566
- ].join("\n"),
592
+ renderCheckoutStep("steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'"),
567
593
  [
568
594
  "- name: Run Gemini review",
569
595
  " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
570
- " uses: google-github-actions/run-gemini-cli@v0",
596
+ ` uses: ${pinnedUses("google-github-actions/run-gemini-cli")}`,
571
597
  " env:",
572
598
  " GITHUB_TOKEN: ${{ github.token }}",
573
599
  " with:",
@@ -636,15 +662,7 @@ function buildWorkflowYaml4(input) {
636
662
  const steps = [
637
663
  renderSecretGateStep(SECRET_NAME4),
638
664
  renderRoundGateStep(input.maxRounds),
639
- [
640
- "- name: Checkout repository",
641
- ` if: ${gateCondition}`,
642
- " uses: actions/checkout@v4",
643
- " with:",
644
- " ref: refs/pull/${{ github.event.pull_request.number }}/merge",
645
- " fetch-depth: 1",
646
- " persist-credentials: false"
647
- ].join("\n"),
665
+ renderCheckoutStep(gateCondition, ["ref: refs/pull/${{ github.event.pull_request.number }}/merge"]),
648
666
  [
649
667
  "- name: Fetch base and head refs for the PR",
650
668
  ` if: ${gateCondition}`,
@@ -662,7 +680,7 @@ function buildWorkflowYaml4(input) {
662
680
  "- name: Run Codex review",
663
681
  " id: codex-review",
664
682
  ` if: ${gateCondition}`,
665
- " uses: openai/codex-action@v1",
683
+ ` uses: ${pinnedUses("openai/codex-action")}`,
666
684
  " with:",
667
685
  ` openai-api-key: \${{ secrets.${SECRET_NAME4} }}`,
668
686
  ' permission-profile: ":read-only"',
@@ -737,13 +755,26 @@ function judgeReviewFileConflict(existing, generated) {
737
755
  }
738
756
 
739
757
  // ../template-engine/dist/backend/eslint-boundaries.js
740
- var SCOPE_CONSTRAINTS = [
741
- { sourceTag: "scope:frontend", notDependOnLibsWithTags: ["scope:backend"] },
742
- { sourceTag: "scope:backend", notDependOnLibsWithTags: ["scope:frontend"] },
743
- { sourceTag: "scope:shared", notDependOnLibsWithTags: ["scope:frontend", "scope:backend"] }
744
- ];
758
+ var DEFAULT_BOUNDARY_TAGS = { frontend: "scope:frontend", backend: "scope:backend", shared: "scope:shared" };
759
+ function readBoundaryTags(config) {
760
+ const boundaries = config?.["boundaries"];
761
+ const tags = typeof boundaries === "object" && boundaries !== null ? boundaries["tags"] : void 0;
762
+ const pick = (role) => {
763
+ const value = typeof tags === "object" && tags !== null ? tags[role] : void 0;
764
+ return typeof value === "string" && value.trim() !== "" ? value : DEFAULT_BOUNDARY_TAGS[role];
765
+ };
766
+ return { frontend: pick("frontend"), backend: pick("backend"), shared: pick("shared") };
767
+ }
768
+ function scopeConstraints(tags = DEFAULT_BOUNDARY_TAGS) {
769
+ return [
770
+ { sourceTag: tags.frontend, notDependOnLibsWithTags: [tags.backend] },
771
+ { sourceTag: tags.backend, notDependOnLibsWithTags: [tags.frontend] },
772
+ { sourceTag: tags.shared, notDependOnLibsWithTags: [tags.frontend, tags.backend] }
773
+ ];
774
+ }
775
+ var SCOPE_CONSTRAINTS = scopeConstraints();
745
776
  var ESLINT_CONFIG_CANDIDATES = ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"];
746
- var APPLIED_MARKER = "sourceTag: 'scope:frontend'";
777
+ var appliedMarker = (tags) => `sourceTag: '${tags.frontend}'`;
747
778
  var ALLOW_ALL_BLOCK = /depConstraints:\s*\[\s*\{\s*sourceTag:\s*'\*',\s*onlyDependOnLibsWithTags:\s*\[\s*'\*'\s*\],?\s*\},?\s*\]/g;
748
779
  function maskComments(text) {
749
780
  const out = text.split("");
@@ -796,17 +827,17 @@ function findAllowAllMatches(masked) {
796
827
  }
797
828
  return matches;
798
829
  }
799
- function classifyBoundaryConstraints(configText) {
830
+ function classifyBoundaryConstraints(configText, tags = DEFAULT_BOUNDARY_TAGS) {
800
831
  const masked = maskComments(configText);
801
- if (masked.includes(APPLIED_MARKER))
832
+ if (masked.includes(appliedMarker(tags)))
802
833
  return "enforced";
803
834
  if (findAllowAllMatches(masked).length > 0)
804
835
  return "allow-all";
805
836
  return "unknown";
806
837
  }
807
- function renderConstraintsBlock(indent, eol) {
808
- const renderList = (tags) => tags.map((t) => `'${t}'`).join(", ");
809
- const inner = SCOPE_CONSTRAINTS.map((c) => {
838
+ function renderConstraintsBlock(indent, eol, tags) {
839
+ const renderList = (list) => list.map((t) => `'${t}'`).join(", ");
840
+ const inner = scopeConstraints(tags).map((c) => {
810
841
  const field = c.notDependOnLibsWithTags ? `notDependOnLibsWithTags: [${renderList(c.notDependOnLibsWithTags)}]` : `onlyDependOnLibsWithTags: [${renderList(c.onlyDependOnLibsWithTags ?? [])}]`;
811
842
  return [`${indent} {`, `${indent} sourceTag: '${c.sourceTag}',`, `${indent} ${field},`, `${indent} },`].join(eol);
812
843
  }).join(eol);
@@ -824,8 +855,8 @@ function renderConstraintsBlock(indent, eol) {
824
855
  `${indent}]`
825
856
  ].join(eol);
826
857
  }
827
- function applyBoundaryConstraints(configText) {
828
- const classification = classifyBoundaryConstraints(configText);
858
+ function applyBoundaryConstraints(configText, tags = DEFAULT_BOUNDARY_TAGS) {
859
+ const classification = classifyBoundaryConstraints(configText, tags);
829
860
  if (classification === "enforced")
830
861
  return { status: "already-applied" };
831
862
  if (classification === "unknown")
@@ -839,17 +870,17 @@ function applyBoundaryConstraints(configText) {
839
870
  const eol = configText.includes("\r\n") ? "\r\n" : "\n";
840
871
  const lineStart = configText.lastIndexOf("\n", match.index) + 1;
841
872
  const indent = /^[ \t]*/.exec(configText.slice(lineStart, match.index))?.[0] ?? "";
842
- const replacement = renderConstraintsBlock(indent, eol);
873
+ const replacement = renderConstraintsBlock(indent, eol, tags);
843
874
  const text = configText.slice(0, match.index) + replacement + configText.slice(match.index + match.length);
844
875
  return { status: "applied", text };
845
876
  }
846
- function buildBoundarySnippet() {
877
+ function buildBoundarySnippet(tags = DEFAULT_BOUNDARY_TAGS) {
847
878
  return [
848
879
  "// nx-archweave project boundaries \u2014 merge these entries into the depConstraints",
849
880
  "// array of the '@nx/enforce-module-boundaries' rule in your root ESLint config.",
850
881
  "// They ride on top of your existing entries: constraints apply conjunctively,",
851
882
  "// so the allow-all baseline can stay.",
852
- renderConstraintsBlock("", "\n")
883
+ renderConstraintsBlock("", "\n", tags)
853
884
  ].join("\n");
854
885
  }
855
886
 
@@ -1140,25 +1171,53 @@ var AGENT_RULE_SECTIONS = {
1140
1171
  ko: KO_SECTIONS
1141
1172
  };
1142
1173
 
1174
+ // ../templates/dist/agents/fsd-policy-notes.js
1175
+ var NOTES = {
1176
+ en: {
1177
+ heading: "This workspace also configures:",
1178
+ groups: "- A folder that only groups slices (`features/mypage/{order,claims}`) holds no code and no `index.ts`; each slice in it is separate, and slices in one group never import each other either. Create one with `--group=<name>`.",
1179
+ component: "- `shared` exposes each component through its own `index.ts` (`shared/ui/button`); import that, not the files inside it.",
1180
+ file: "- Inside `shared`, files may be imported directly (`shared/lib/format/currency`); `shared` still never imports a layer above it.",
1181
+ tests: "- Test files (`*.spec.ts`, `*.test.ts`, `__tests__/`) may import another slice's internals; upward imports stay forbidden there too."
1182
+ },
1183
+ ko: {
1184
+ heading: "\uC774 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB294 \uB2E4\uC74C\uB3C4 \uC124\uC815\uD588\uB2E4:",
1185
+ groups: "- \uC2AC\uB77C\uC774\uC2A4\uB97C \uBB36\uAE30\uB9CC \uD558\uB294 \uD3F4\uB354(`features/mypage/{order,claims}`)\uC5D0\uB294 \uCF54\uB4DC\uB3C4 `index.ts`\uB3C4 \uB450\uC9C0 \uC54A\uB294\uB2E4. \uADF8 \uC548\uC758 \uC2AC\uB77C\uC774\uC2A4\uB294 \uAC01\uAC01 \uB3C5\uB9BD\uC774\uB77C, \uAC19\uC740 \uADF8\uB8F9 \uC548\uC5D0\uC11C\uB3C4 \uC11C\uB85C import\uD558\uC9C0 \uC54A\uB294\uB2E4. \uADF8\uB8F9 \uC548\uC5D0 \uB9CC\uB4E4 \uB54C\uB294 `--group=<\uC774\uB984>`\uC744 \uC4F4\uB2E4.",
1186
+ component: "- `shared`\uB294 \uCEF4\uD3EC\uB10C\uD2B8\uB9C8\uB2E4 \uC790\uAE30 `index.ts`\uB85C \uACF5\uAC1C\uD55C\uB2E4(`shared/ui/button`). \uADF8 index\uB97C import\uD558\uACE0, \uC548\uCABD \uD30C\uC77C\uC5D0\uB294 \uB2FF\uC9C0 \uC54A\uB294\uB2E4.",
1187
+ file: "- `shared` \uC548\uC758 \uD30C\uC77C\uC740 \uC9C1\uC811 import\uD574\uB3C4 \uB41C\uB2E4(`shared/lib/format/currency`). \uB2E4\uB9CC `shared`\uB294 \uC5EC\uC804\uD788 \uC704 \uB808\uC774\uC5B4\uB97C import\uD558\uC9C0 \uC54A\uB294\uB2E4.",
1188
+ tests: "- \uD14C\uC2A4\uD2B8 \uD30C\uC77C(`*.spec.ts`, `*.test.ts`, `__tests__/`)\uC740 \uB2E4\uB978 \uC2AC\uB77C\uC774\uC2A4\uC758 \uB0B4\uBD80 \uD30C\uC77C\uC744 import\uD574\uB3C4 \uB41C\uB2E4. \uC704 \uB808\uC774\uC5B4 import\uB294 \uD14C\uC2A4\uD2B8\uC5D0\uC11C\uB3C4 \uAE08\uC9C0\uB2E4."
1189
+ }
1190
+ };
1191
+ function buildFsdPolicyNotes(locale, notes) {
1192
+ const text = NOTES[locale];
1193
+ const lines = [
1194
+ ...notes.sliceGroups ? [text.groups] : [],
1195
+ ...notes.sharedPublicApi === "component" ? [text.component] : notes.sharedPublicApi === "file" ? [text.file] : [],
1196
+ ...notes.testFiles ? [text.tests] : []
1197
+ ];
1198
+ return lines.length === 0 ? "" : [text.heading, ...lines].join("\n");
1199
+ }
1200
+
1143
1201
  // ../templates/dist/fsd/fsd-eslint.js
1144
1202
  var FSD_ESLINT_MODULE = "@clearlydev/nx-archweave/eslint";
1145
1203
  function buildFsdEslintImportLine() {
1146
1204
  return `import { fsdBoundaries } from '${FSD_ESLINT_MODULE}';`;
1147
1205
  }
1148
- function buildFsdEslintEntry(severity) {
1149
- return `...fsdBoundaries({ severity: '${severity}' }),`;
1206
+ function buildFsdEslintEntry(severity, root = "src", framework = {}) {
1207
+ const list = (key, values) => values === void 0 || values.length === 0 ? "" : `, ${key}: [${values.map((value) => `'${value}'`).join(", ")}]`;
1208
+ return `...fsdBoundaries({ severity: '${severity}', cwd: import.meta.dirname, root: '${root}'${list("platformSuffixes", framework.platformSuffixes)}${list("routerDirs", framework.routerDirs)} }),`;
1150
1209
  }
1151
- function buildFsdEslintSnippet({ severity, locale }) {
1210
+ function buildFsdEslintSnippet({ severity, locale, root, framework }) {
1152
1211
  const lines = locale === "ko" ? [
1153
1212
  "// nx-archweave FSD \uB808\uC774\uC5B4 \uADDC\uCE59 \u2014 \uD504\uB860\uD2B8\uC5D4\uB4DC \uC571\uC758 eslint.config.mjs\uC5D0 \uC9C1\uC811 \uB123\uC73C\uC138\uC694:",
1154
1213
  "// 1) \uB9C8\uC9C0\uB9C9 import \uB4A4\uC5D0 \uC544\uB798 import\uB97C,",
1155
- "// 2) export default [...] \uBC30\uC5F4\uC758 \uB9C8\uC9C0\uB9C9 \uD56D\uBAA9\uC73C\uB85C \uC544\uB798 spread\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4."
1214
+ "// 2) \uC124\uC815 \uBC30\uC5F4(export default [...], \uB610\uB294 export default \uD558\uB294 \uBCC0\uC218\uC758 \uBC30\uC5F4)\uC758 \uB9C8\uC9C0\uB9C9 \uD56D\uBAA9\uC73C\uB85C \uC544\uB798 spread\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4."
1156
1215
  ] : [
1157
1216
  "// nx-archweave FSD layer rules \u2014 add these to the frontend app's eslint.config.mjs yourself:",
1158
1217
  "// 1) the import after the last import,",
1159
- "// 2) the spread as the last element of the export default [...] array."
1218
+ "// 2) the spread as the last element of the config array (export default [...], or the array your config exports by name)."
1160
1219
  ];
1161
- return [...lines, buildFsdEslintImportLine(), buildFsdEslintEntry(severity)].join("\n");
1220
+ return [...lines, buildFsdEslintImportLine(), buildFsdEslintEntry(severity, root, framework)].join("\n");
1162
1221
  }
1163
1222
  var LAYER_GUIDE_EN = {
1164
1223
  app: "composition root: providers, routing, global styles (the framework entry point lives here)",
@@ -1204,7 +1263,7 @@ var FSD_CHECK_TARGET = "fsd-check";
1204
1263
  function buildFsdCheckTarget(sourceRootName = "src") {
1205
1264
  return { command: `steiger ${sourceRootName}`, options: { cwd: "{projectRoot}" } };
1206
1265
  }
1207
- function buildSteigerConfig({ frontend, locale }) {
1266
+ function buildSteigerConfig({ renamedLayerDirs, locale }) {
1208
1267
  const ko = locale === "ko";
1209
1268
  const lines = [
1210
1269
  "import { defineConfig } from 'steiger';",
@@ -1217,8 +1276,8 @@ function buildSteigerConfig({ frontend, locale }) {
1217
1276
  " rules: {",
1218
1277
  ko ? " // \uC544\uBB34\uB3C4 \uCC38\uC870\uD558\uC9C0 \uC54A\uB294 \uC2AC\uB77C\uC774\uC2A4 = \uBC29\uAE08 \uC0DD\uC131\uD55C \uC2AC\uB77C\uC774\uC2A4. \uACBD\uACE0\uB85C \uBCF4\uC774\uB418 \uAC80\uC0AC\uB97C \uB9C9\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." : " // a slice nothing references yet is every freshly generated slice: visible, not blocking",
1219
1278
  " 'fsd/insignificant-slice': 'warn',",
1220
- ...frontend === "next" ? [
1221
- ko ? " // Next.js: pages \uB808\uC774\uC5B4\uB294 _pages/ (FSD \uACF5\uC2DD Next.js \uAC00\uC774\uB4DC) \u2014 steiger 0.7\uC740 \uC544\uC9C1 \uC624\uD0C0\uB85C \uBD05\uB2C8\uB2E4." : " // Next.js: the pages layer is _pages/ (FSD's own Next.js guide) \u2014 steiger 0.7 still reads it as a typo",
1279
+ ...renamedLayerDirs.length > 0 ? [
1280
+ ko ? ` // ${renamedLayerDirs.join(", ")}: \uD504\uB808\uC784\uC6CC\uD06C \uB77C\uC6B0\uD130\uC640 \uACB9\uCE58\uC9C0 \uC54A\uAC8C \uC774\uB984\uC744 \uBC14\uAFBC \uB808\uC774\uC5B4 (FSD \uACF5\uC2DD \uBA85\uBA85) \u2014 steiger 0.7\uC740 \uC544\uC9C1 \uC624\uD0C0\uB85C \uBD05\uB2C8\uB2E4.` : ` // ${renamedLayerDirs.join(", ")}: layers renamed out of the framework router's way (FSD's own naming) \u2014 steiger 0.7 still reads them as typos`,
1222
1281
  " 'fsd/typo-in-layer-name': 'off',"
1223
1282
  ] : [],
1224
1283
  " },",
@@ -1245,7 +1304,7 @@ function sharedTypesApiTypesContent() {
1245
1304
  ""
1246
1305
  ].join("\n");
1247
1306
  }
1248
- function sharedTypesPackageJsonContent(packageName) {
1307
+ function sharedTypesPackageJsonContent(packageName, sharedTag = "scope:shared") {
1249
1308
  return `${JSON.stringify({
1250
1309
  name: packageName,
1251
1310
  version: "0.0.1",
@@ -1262,13 +1321,296 @@ function sharedTypesPackageJsonContent(packageName) {
1262
1321
  },
1263
1322
  nx: {
1264
1323
  projectType: "library",
1265
- tags: ["scope:shared"],
1324
+ tags: [sharedTag],
1266
1325
  targets: {}
1267
1326
  }
1268
1327
  }, null, 2)}
1269
1328
  `;
1270
1329
  }
1271
1330
 
1331
+ // ../template-engine/dist/backend/compose-backend-wiring.js
1332
+ var DEFAULT_SHARED_TYPES_ROOT = "libs/shared-types";
1333
+ function readSharedTypesConfig(config) {
1334
+ const value = config?.["sharedTypes"];
1335
+ if (value === false)
1336
+ return false;
1337
+ if (typeof value !== "object" || value === null)
1338
+ return { path: DEFAULT_SHARED_TYPES_ROOT };
1339
+ const record = value;
1340
+ const path = typeof record["path"] === "string" && record["path"].trim() !== "" ? record["path"].replace(/\/+$/, "") : DEFAULT_SHARED_TYPES_ROOT;
1341
+ const importPath = typeof record["importPath"] === "string" && record["importPath"].trim() !== "" ? record["importPath"] : void 0;
1342
+ return importPath === void 0 ? { path } : { path, importPath };
1343
+ }
1344
+ function composeBackendWiring(input) {
1345
+ const scoped = /^@([^/]+)\//.exec(input.workspaceName);
1346
+ const scope = scoped ? `@${scoped[1]}` : `@${input.workspaceName.split("/").pop() || "workspace"}`;
1347
+ const sharedTypesRoot = input.sharedTypes?.path ?? DEFAULT_SHARED_TYPES_ROOT;
1348
+ const sharedTypesPackageName = input.sharedTypes?.importPath ?? `${scope}/shared-types`;
1349
+ const tagByRole = input.tags ?? DEFAULT_BOUNDARY_TAGS;
1350
+ return {
1351
+ sharedTypesFiles: [
1352
+ { filePath: `${sharedTypesRoot}/package.json`, content: sharedTypesPackageJsonContent(sharedTypesPackageName, tagByRole.shared) },
1353
+ { filePath: `${sharedTypesRoot}/src/index.ts`, content: sharedTypesIndexContent() },
1354
+ { filePath: `${sharedTypesRoot}/src/lib/api-types.ts`, content: sharedTypesApiTypesContent() }
1355
+ ],
1356
+ sharedTypesPackageName,
1357
+ sharedTypesRoot,
1358
+ tagByRole
1359
+ };
1360
+ }
1361
+ function addSharedTypesNpmWorkspace(packageJsonText, entry = DEFAULT_SHARED_TYPES_ROOT) {
1362
+ let parsed;
1363
+ try {
1364
+ parsed = JSON.parse(packageJsonText);
1365
+ } catch {
1366
+ return { status: "unrecognized" };
1367
+ }
1368
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
1369
+ return { status: "unrecognized" };
1370
+ const pkg = parsed;
1371
+ const field = pkg["workspaces"];
1372
+ let target;
1373
+ if (field === void 0) {
1374
+ target = [];
1375
+ } else if (Array.isArray(field)) {
1376
+ target = field;
1377
+ } else if (field !== null && typeof field === "object" && Array.isArray(field.packages)) {
1378
+ target = field.packages;
1379
+ } else {
1380
+ return { status: "unrecognized" };
1381
+ }
1382
+ const parentGlob = `${entry.split("/").slice(0, -1).join("/")}/*`;
1383
+ if (target.some((existing) => existing === entry || existing === parentGlob))
1384
+ return { status: "present" };
1385
+ const surgical = insertSharedTypesWorkspace(packageJsonText, field === void 0 ? "absent" : Array.isArray(field) ? "array" : "object", JSON.stringify(entry));
1386
+ if (surgical !== null)
1387
+ return { status: "added", text: surgical };
1388
+ if (field === void 0)
1389
+ pkg["workspaces"] = target;
1390
+ target.push(entry);
1391
+ const indent = /\n([ \t]+)"/.exec(packageJsonText)?.[1] ?? " ";
1392
+ const trailing = packageJsonText.endsWith("\n") ? "\n" : "";
1393
+ return { status: "added", text: `${JSON.stringify(pkg, null, indent)}${trailing}` };
1394
+ }
1395
+ function skipJsonValue(text, start) {
1396
+ if (text[start] === '"') {
1397
+ let i = start + 1;
1398
+ while (i < text.length && text[i] !== '"')
1399
+ i += text[i] === "\\" ? 2 : 1;
1400
+ return i + 1;
1401
+ }
1402
+ if (text[start] === "[" || text[start] === "{") {
1403
+ let depth = 0;
1404
+ let i = start;
1405
+ while (i < text.length) {
1406
+ const c = text[i];
1407
+ if (c === '"') {
1408
+ i = skipJsonValue(text, i);
1409
+ continue;
1410
+ }
1411
+ if (c === "[" || c === "{")
1412
+ depth++;
1413
+ if (c === "]" || c === "}") {
1414
+ depth--;
1415
+ if (depth === 0)
1416
+ return i + 1;
1417
+ }
1418
+ i++;
1419
+ }
1420
+ return -1;
1421
+ }
1422
+ const literal = /^[^,}\]\s]+/.exec(text.slice(start));
1423
+ return literal ? start + literal[0].length : -1;
1424
+ }
1425
+ function objectMembers(text, open) {
1426
+ const members = [];
1427
+ let i = open + 1;
1428
+ for (; ; ) {
1429
+ while (/[\s,]/.test(text[i] ?? ""))
1430
+ i++;
1431
+ if (text[i] === "}")
1432
+ return members;
1433
+ if (text[i] !== '"')
1434
+ return null;
1435
+ const keyEnd = skipJsonValue(text, i);
1436
+ const key = JSON.parse(text.slice(i, keyEnd));
1437
+ i = keyEnd;
1438
+ while (/\s/.test(text[i] ?? ""))
1439
+ i++;
1440
+ if (text[i] !== ":")
1441
+ return null;
1442
+ i++;
1443
+ while (/\s/.test(text[i] ?? ""))
1444
+ i++;
1445
+ const valueEnd = skipJsonValue(text, i);
1446
+ if (valueEnd === -1)
1447
+ return null;
1448
+ members.push({ key, valueStart: i, valueEnd });
1449
+ i = valueEnd;
1450
+ }
1451
+ }
1452
+ function appendToArray(text, open, close, eol, quoted) {
1453
+ const inner = text.slice(open + 1, close - 1);
1454
+ if (inner.trim() === "")
1455
+ return `${text.slice(0, open + 1)}${quoted}${text.slice(close - 1)}`;
1456
+ if (!inner.includes("\n")) {
1457
+ const body = inner.replace(/\s+$/, "");
1458
+ return `${text.slice(0, open + 1)}${body}, ${quoted}${inner.slice(body.length)}${text.slice(close - 1)}`;
1459
+ }
1460
+ const lastEntryLineStart = text.lastIndexOf("\n", open + 1 + inner.trimEnd().length - 1) + 1;
1461
+ const itemIndent = /^[ \t]*/.exec(text.slice(lastEntryLineStart))?.[0] ?? " ";
1462
+ const bodyEnd = open + 1 + inner.trimEnd().length;
1463
+ return `${text.slice(0, bodyEnd)},${eol}${itemIndent}${quoted}${text.slice(bodyEnd)}`;
1464
+ }
1465
+ function insertSharedTypesWorkspace(text, form, quoted) {
1466
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
1467
+ const rootOpen = text.indexOf("{");
1468
+ if (rootOpen === -1)
1469
+ return null;
1470
+ const members = objectMembers(text, rootOpen);
1471
+ if (members === null)
1472
+ return null;
1473
+ const workspaces = members.find((member) => member.key === "workspaces");
1474
+ if (form === "absent") {
1475
+ const last = members[members.length - 1];
1476
+ const indent = /\n([ \t]+)"/.exec(text)?.[1] ?? " ";
1477
+ if (last === void 0) {
1478
+ const close = skipJsonValue(text, rootOpen) - 1;
1479
+ return `${text.slice(0, rootOpen + 1)}${eol}${indent}"workspaces": [${quoted}]${eol}${text.slice(close)}`;
1480
+ }
1481
+ return `${text.slice(0, last.valueEnd)},${eol}${indent}"workspaces": [${quoted}]${text.slice(last.valueEnd)}`;
1482
+ }
1483
+ if (workspaces === void 0)
1484
+ return null;
1485
+ if (form === "array") {
1486
+ if (text[workspaces.valueStart] !== "[")
1487
+ return null;
1488
+ return appendToArray(text, workspaces.valueStart, workspaces.valueEnd, eol, quoted);
1489
+ }
1490
+ if (text[workspaces.valueStart] !== "{")
1491
+ return null;
1492
+ const packages = objectMembers(text, workspaces.valueStart)?.find((member) => member.key === "packages");
1493
+ if (packages === void 0 || text[packages.valueStart] !== "[")
1494
+ return null;
1495
+ return appendToArray(text, packages.valueStart, packages.valueEnd, eol, quoted);
1496
+ }
1497
+ function addSharedTypesWorkspaceGlob(yamlText, entry = DEFAULT_SHARED_TYPES_ROOT) {
1498
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
1499
+ const parentGlob = `${entry.split("/").slice(0, -1).join("/")}/*`;
1500
+ if (new RegExp(`^\\s*-\\s*['"]?(${escape(entry)}|${escape(parentGlob)})['"]?\\s*$`, "m").test(yamlText)) {
1501
+ return { status: "present" };
1502
+ }
1503
+ const lines = yamlText.split("\n");
1504
+ const packagesIndex = lines.findIndex((line) => /^packages:\s*$/.test(line));
1505
+ if (packagesIndex === -1)
1506
+ return { status: "unrecognized" };
1507
+ let lastEntry = packagesIndex;
1508
+ for (let i = packagesIndex + 1; i < lines.length; i++) {
1509
+ if (/^\s+-\s+\S/.test(lines[i])) {
1510
+ lastEntry = i;
1511
+ } else if (lines[i].trim() === "") {
1512
+ continue;
1513
+ } else {
1514
+ break;
1515
+ }
1516
+ }
1517
+ if (lastEntry === packagesIndex)
1518
+ return { status: "unrecognized" };
1519
+ const indent = /^(\s+)-/.exec(lines[lastEntry])?.[1] ?? " ";
1520
+ lines.splice(lastEntry + 1, 0, `${indent}- '${entry}'`);
1521
+ return { status: "added", text: lines.join("\n") };
1522
+ }
1523
+
1524
+ // ../template-engine/dist/frameworks/profiles.js
1525
+ var ALL_LAYERS = ["app", "pages", "features", "entities", "shared"];
1526
+ var NEXT = {
1527
+ id: "next",
1528
+ recognised: true,
1529
+ // Next.js's own app/ (App Router) directory is the composition root; FSD's Next.js guide
1530
+ // names the FSD app and pages layers _app and _pages, since Next.js reads src/app and
1531
+ // src/pages as its routers.
1532
+ layers: ["pages", "features", "entities", "shared"],
1533
+ layerDirs: { app: "_app", pages: "_pages" },
1534
+ buildOutputDirs: [".next", "out"],
1535
+ platformSuffixes: [],
1536
+ routerDirs: ["pages"]
1537
+ };
1538
+ var REACT_VITE = {
1539
+ id: "react-vite",
1540
+ recognised: true,
1541
+ layers: ALL_LAYERS,
1542
+ layerDirs: {},
1543
+ buildOutputDirs: ["dist"],
1544
+ platformSuffixes: [],
1545
+ routerDirs: []
1546
+ };
1547
+ var EXPO_ROUTER = {
1548
+ id: "expo",
1549
+ recognised: true,
1550
+ layers: ALL_LAYERS,
1551
+ layerDirs: { app: "_app" },
1552
+ buildOutputDirs: [".expo", "dist"],
1553
+ platformSuffixes: ["ios", "android", "native", "web"],
1554
+ routerDirs: []
1555
+ };
1556
+ var EXPO = { ...EXPO_ROUTER, layerDirs: {} };
1557
+ var GENERIC = {
1558
+ id: "generic",
1559
+ recognised: false,
1560
+ layers: ALL_LAYERS,
1561
+ layerDirs: {},
1562
+ buildOutputDirs: ["dist", "build"],
1563
+ platformSuffixes: [],
1564
+ routerDirs: []
1565
+ };
1566
+ var PROFILES = { next: NEXT, "react-vite": REACT_VITE, expo: EXPO_ROUTER, generic: GENERIC };
1567
+ function profileById(id) {
1568
+ return PROFILES[id];
1569
+ }
1570
+ function allFrameworkProfiles() {
1571
+ return Object.values(PROFILES);
1572
+ }
1573
+ var NEXT_CONFIGS = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"];
1574
+ var VITE_CONFIGS = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
1575
+ var EXPO_CONFIGS = ["app.config.ts", "app.config.js", "app.config.mjs"];
1576
+ function hasExpoKey(input, file) {
1577
+ const text = input.read(file);
1578
+ if (typeof text !== "string")
1579
+ return false;
1580
+ try {
1581
+ return typeof JSON.parse(text)["expo"] === "object";
1582
+ } catch {
1583
+ return false;
1584
+ }
1585
+ }
1586
+ function dependsOn(input, manifest, name) {
1587
+ const text = input.read(manifest);
1588
+ if (typeof text !== "string")
1589
+ return false;
1590
+ try {
1591
+ const parsed = JSON.parse(text);
1592
+ return ["dependencies", "devDependencies", "peerDependencies"].some((field) => parsed[field]?.[name] !== void 0);
1593
+ } catch {
1594
+ return false;
1595
+ }
1596
+ }
1597
+ function detectFramework(input) {
1598
+ const prefix = input.projectRoot === "" || input.projectRoot === "." ? "" : `${input.projectRoot}/`;
1599
+ const executors = input.executors ?? [];
1600
+ if (NEXT_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/next:"))) {
1601
+ return NEXT;
1602
+ }
1603
+ const dependsAnywhere = (name) => dependsOn(input, `${prefix}package.json`, name) || dependsOn(input, "package.json", name);
1604
+ const usesExpo = executors.some((executor) => executor.startsWith("@nx/expo:")) || hasExpoKey(input, `${prefix}app.json`) || EXPO_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || dependsOn(input, `${prefix}package.json`, "expo");
1605
+ if (usesExpo)
1606
+ return dependsAnywhere("expo-router") ? EXPO_ROUTER : EXPO;
1607
+ const usesReact = dependsAnywhere("react");
1608
+ const usesVite = VITE_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/vite:"));
1609
+ if (usesVite && usesReact)
1610
+ return REACT_VITE;
1611
+ return GENERIC;
1612
+ }
1613
+
1272
1614
  // ../template-engine/dist/fsd/fsd-eslint-config.js
1273
1615
  function classifyFsdEslintConfig(configText) {
1274
1616
  return maskComments(configText).includes(FSD_ESLINT_MODULE) ? "applied" : "absent";
@@ -1276,21 +1618,47 @@ function classifyFsdEslintConfig(configText) {
1276
1618
  var EXPORT_DEFAULT_ARRAY = /export\s+default\s+\[/g;
1277
1619
  var IMPORT_STATEMENT = /^import\b[^;]*?\bfrom\s+['"][^'"]+['"]\s*;?[ \t]*$/gm;
1278
1620
  var CLOSING_BRACKET = /\]\s*;?\s*$/;
1279
- function applyFsdEslintConfig(configText, severity) {
1621
+ function closingBracketOf(masked, open) {
1622
+ let depth = 0;
1623
+ for (let i = open; i < masked.length; i++) {
1624
+ const ch = masked[i];
1625
+ if (ch === '"' || ch === "'" || ch === "`") {
1626
+ for (i++; i < masked.length && masked[i] !== ch; i++)
1627
+ if (masked[i] === "\\")
1628
+ i++;
1629
+ continue;
1630
+ }
1631
+ if (ch === "[")
1632
+ depth++;
1633
+ else if (ch === "]" && --depth === 0)
1634
+ return i;
1635
+ }
1636
+ return -1;
1637
+ }
1638
+ function namedConfigArrayEnd(masked) {
1639
+ const exported = [...masked.matchAll(/export\s+default\s+([A-Za-z_$][\w$]*)\s*;?/g)];
1640
+ if (exported.length !== 1)
1641
+ return -1;
1642
+ const declaration = new RegExp(`(?:const|let|var)\\s+${exported[0][1].replace(/\$/g, "\\$")}\\s*=\\s*(?:defineConfig\\s*\\(\\s*)?\\[`).exec(masked);
1643
+ if (declaration === null)
1644
+ return -1;
1645
+ return closingBracketOf(masked, declaration.index + declaration[0].length - 1);
1646
+ }
1647
+ function applyFsdEslintConfig(configText, severity, root = "src", framework = {}) {
1280
1648
  if (classifyFsdEslintConfig(configText) === "applied")
1281
1649
  return { status: "already-applied" };
1282
1650
  const masked = maskComments(configText);
1283
1651
  EXPORT_DEFAULT_ARRAY.lastIndex = 0;
1284
1652
  const exportMatches = masked.match(EXPORT_DEFAULT_ARRAY) ?? [];
1285
1653
  const closing = CLOSING_BRACKET.exec(masked);
1286
- if (exportMatches.length !== 1 || closing === null)
1654
+ const closingIndex = exportMatches.length === 1 && closing !== null ? closing.index : namedConfigArrayEnd(masked);
1655
+ if (closingIndex === -1)
1287
1656
  return { status: "unrecognized" };
1288
1657
  const eol = configText.includes("\r\n") ? "\r\n" : "\n";
1289
- const closingIndex = closing.index;
1290
1658
  const before = configText.slice(0, closingIndex);
1291
1659
  const lastNonSpace = before.replace(/\s+$/, "");
1292
1660
  const needsComma = !lastNonSpace.endsWith(",") && !lastNonSpace.endsWith("[");
1293
- const entryText = `${needsComma ? "," : ""}${eol} ${buildFsdEslintEntry(severity)}${eol}`;
1661
+ const entryText = `${needsComma ? "," : ""}${eol} ${buildFsdEslintEntry(severity, root, framework)}${eol}`;
1294
1662
  const withEntry = `${lastNonSpace}${entryText}${configText.slice(closingIndex)}`;
1295
1663
  IMPORT_STATEMENT.lastIndex = 0;
1296
1664
  let lastImportEnd = -1;
@@ -1302,20 +1670,12 @@ function applyFsdEslintConfig(configText, severity) {
1302
1670
  const text = lastImportEnd === -1 ? `${importLine}${eol}${eol}${withEntry}` : `${withEntry.slice(0, lastImportEnd)}${eol}${importLine}${withEntry.slice(lastImportEnd)}`;
1303
1671
  return { status: "applied", text };
1304
1672
  }
1305
- function buildFsdEslintPasteSnippet(severity, locale) {
1306
- return buildFsdEslintSnippet({ severity, locale });
1307
- }
1308
- var REACT_VITE_LAYERS = ["app", "pages", "features", "entities", "shared"];
1309
- var NEXT_LAYERS = ["pages", "features", "entities", "shared"];
1310
- function fsdLayerDirName(frontend, layer) {
1311
- return frontend === "next" && (layer === "pages" || layer === "app") ? `_${layer}` : layer;
1312
- }
1313
- function fsdLayersFor(frontend) {
1314
- return frontend === "next" ? [...NEXT_LAYERS] : [...REACT_VITE_LAYERS];
1673
+ function buildFsdEslintPasteSnippet(severity, locale, root = "src", framework = {}) {
1674
+ return buildFsdEslintSnippet({ severity, locale, root, framework });
1315
1675
  }
1316
- function composeFsdLayerGuide({ frontend, sourceRoot, locale }) {
1317
- const layers = fsdLayersFor(frontend);
1318
- return buildFsdLayerGuide({ layers, directories: layers.map((layer) => fsdLayerDirName(frontend, layer)), sourceRoot, locale });
1676
+ function composeFsdLayerGuide({ profile, sourceRoot, locale }) {
1677
+ const layers = [...profile.layers];
1678
+ return buildFsdLayerGuide({ layers, directories: layers.map((layer) => profile.layerDirs[layer] ?? layer), sourceRoot, locale });
1319
1679
  }
1320
1680
 
1321
1681
  // ../template-engine/dist/ai-hosts/ai-hosts.js
@@ -1346,18 +1706,13 @@ function reviewProviderFor(hosts) {
1346
1706
  }
1347
1707
 
1348
1708
  // ../template-engine/dist/presets/preset-config.js
1349
- var REACT_VITE_LAYERS2 = ["app", "pages", "features", "entities", "shared"];
1350
- var NEXT_LAYERS2 = ["pages", "features", "entities", "shared"];
1351
1709
  function buildFsdConfig(enforcement, frontend) {
1352
1710
  return {
1353
1711
  // All three presets enable FSD — it is on whenever there is a
1354
1712
  // frontend; a backend-only workspace has none to structure.
1355
1713
  enabled: frontend !== "none",
1356
1714
  enforcement,
1357
- layerMapping: {
1358
- "react-vite": REACT_VITE_LAYERS2,
1359
- next: NEXT_LAYERS2
1360
- }
1715
+ layerMapping: Object.fromEntries(allFrameworkProfiles().map((profile) => [profile.id, [...profile.layers]]))
1361
1716
  };
1362
1717
  }
1363
1718
  var PRESET_DEFAULTS = {
@@ -1459,7 +1814,13 @@ ${section.body}`).join("\n\n")}
1459
1814
  }
1460
1815
  function composeAgentRules(input) {
1461
1816
  const sectionIds = input.sections ?? resolveDefaultAgentSectionIds(input.teamRules ?? false, input.fsdStructure ?? false);
1462
- const sections = sectionIds.map((id) => AGENT_RULE_SECTIONS[input.locale][id]);
1817
+ const notes = input.fsdPolicy === void 0 ? "" : buildFsdPolicyNotes(input.locale, input.fsdPolicy);
1818
+ const sections = sectionIds.map((id) => {
1819
+ const section = AGENT_RULE_SECTIONS[input.locale][id];
1820
+ return id === FSD_AGENT_SECTION_ID && notes !== "" ? { ...section, body: `${section.body}
1821
+
1822
+ ${notes}` } : section;
1823
+ });
1463
1824
  const content = renderSections(sections);
1464
1825
  return input.files.map((filePath) => ({ filePath, content }));
1465
1826
  }
@@ -1475,297 +1836,1102 @@ function validateAgentRules(content, extraRequired = []) {
1475
1836
  }
1476
1837
 
1477
1838
  // ../template-engine/dist/agent-rules/marker-block.js
1839
+ import { createHash } from "node:crypto";
1478
1840
  var AGENT_RULES_BLOCK_START = "<!-- nx-archweave:agent-rules start -->";
1479
1841
  var AGENT_RULES_BLOCK_END = "<!-- nx-archweave:agent-rules end -->";
1842
+ var START_MARKER = /<!-- nx-archweave:agent-rules start(?: hash=([0-9a-f]{12}))? -->/g;
1843
+ function normalizeForHash(text) {
1844
+ return text.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "");
1845
+ }
1846
+ function hashOf(body) {
1847
+ return createHash("sha256").update(normalizeForHash(body)).digest("hex").slice(0, 12);
1848
+ }
1849
+ function withFinalNewline(body) {
1850
+ return body.endsWith("\n") ? body : `${body}
1851
+ `;
1852
+ }
1480
1853
  function renderAgentRulesBlock(body, eol = "\n") {
1481
- const lines = `${AGENT_RULES_BLOCK_START}
1482
- ${body.endsWith("\n") ? body : `${body}
1483
- `}${AGENT_RULES_BLOCK_END}`;
1854
+ const content = withFinalNewline(normalizeForHash(body));
1855
+ const lines = `<!-- nx-archweave:agent-rules start hash=${hashOf(content)} -->
1856
+ ${content}${AGENT_RULES_BLOCK_END}`;
1484
1857
  return eol === "\n" ? lines : lines.replace(/\r?\n/g, eol);
1485
1858
  }
1486
1859
  function countOf(text, needle) {
1487
1860
  return text.split(needle).length - 1;
1488
1861
  }
1489
- function applyAgentRulesBlock(existing, body) {
1862
+ function applyAgentRulesBlock(existing, body, options = {}) {
1490
1863
  if (existing === null || existing.length === 0)
1491
1864
  return { kind: "create", text: `${renderAgentRulesBlock(body)}
1492
1865
  ` };
1493
1866
  const eol = existing.includes("\r\n") ? "\r\n" : "\n";
1494
1867
  const block = renderAgentRulesBlock(body, eol);
1495
- const starts = countOf(existing, AGENT_RULES_BLOCK_START);
1868
+ const starts = [...existing.matchAll(START_MARKER)];
1496
1869
  const ends = countOf(existing, AGENT_RULES_BLOCK_END);
1497
- if (starts === 0 && ends === 0) {
1870
+ if (starts.length === 0 && ends === 0) {
1498
1871
  const head = existing.endsWith("\n") ? existing : `${existing}${eol}`;
1499
1872
  return { kind: "append", text: `${head}${eol}${block}${eol}` };
1500
1873
  }
1501
- if (starts !== 1 || ends !== 1) {
1874
+ if (starts.length !== 1 || ends !== 1) {
1502
1875
  return {
1503
1876
  kind: "unrecognized",
1504
- reason: `expected one ${AGENT_RULES_BLOCK_START} \u2026 ${AGENT_RULES_BLOCK_END} pair, found ${starts} start and ${ends} end marker(s)`
1877
+ reason: `expected one ${AGENT_RULES_BLOCK_START} \u2026 ${AGENT_RULES_BLOCK_END} pair, found ${starts.length} start and ${ends} end marker(s)`
1505
1878
  };
1506
1879
  }
1507
- const start = existing.indexOf(AGENT_RULES_BLOCK_START);
1880
+ const startMatch = starts[0];
1881
+ const start = startMatch.index ?? 0;
1508
1882
  const end = existing.indexOf(AGENT_RULES_BLOCK_END);
1509
1883
  if (end < start)
1510
1884
  return { kind: "unrecognized", reason: "the end marker comes before the start marker" };
1511
1885
  const text = existing.slice(0, start) + block + existing.slice(end + AGENT_RULES_BLOCK_END.length);
1512
- return text === existing ? { kind: "unchanged" } : { kind: "replace", text };
1886
+ if (text === existing)
1887
+ return { kind: "unchanged" };
1888
+ const bodyStart = start + startMatch[0].length + (existing.startsWith("\r\n", start + startMatch[0].length) ? 2 : 1);
1889
+ const existingBody = normalizeForHash(existing.slice(bodyStart, end));
1890
+ const generatedBody = withFinalNewline(normalizeForHash(body));
1891
+ const recordedHash = startMatch[1];
1892
+ const untouched = recordedHash !== void 0 ? hashOf(existingBody) === recordedHash : existingBody === generatedBody;
1893
+ if (!untouched && options.overwrite !== true)
1894
+ return { kind: "user-modified", existingBody, generatedBody };
1895
+ return { kind: "replace", text };
1513
1896
  }
1514
1897
 
1515
- // ../template-engine/dist/backend/compose-backend-wiring.js
1516
- function composeBackendWiring(input) {
1517
- const scoped = /^@([^/]+)\//.exec(input.workspaceName);
1518
- const scope = scoped ? `@${scoped[1]}` : `@${input.workspaceName.split("/").pop() || "workspace"}`;
1519
- const sharedTypesPackageName = `${scope}/shared-types`;
1520
- return {
1521
- sharedTypesFiles: [
1522
- { filePath: "libs/shared-types/package.json", content: sharedTypesPackageJsonContent(sharedTypesPackageName) },
1523
- { filePath: "libs/shared-types/src/index.ts", content: sharedTypesIndexContent() },
1524
- { filePath: "libs/shared-types/src/lib/api-types.ts", content: sharedTypesApiTypesContent() }
1525
- ],
1526
- sharedTypesPackageName,
1527
- tagByRole: { frontend: "scope:frontend", backend: "scope:backend", shared: "scope:shared" }
1528
- };
1898
+ // src/generators/add-fsd-lite/generator.ts
1899
+ import { posix } from "node:path";
1900
+
1901
+ // src/generators/shared/dry-run.ts
1902
+ function resolveDryRun(options) {
1903
+ return process.env["NX_DRY_RUN"] === "true" || (options.dryRun ?? false);
1529
1904
  }
1530
- function addSharedTypesNpmWorkspace(packageJsonText) {
1531
- let parsed;
1532
- try {
1533
- parsed = JSON.parse(packageJsonText);
1534
- } catch {
1535
- return { status: "unrecognized" };
1536
- }
1537
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
1538
- return { status: "unrecognized" };
1539
- const pkg = parsed;
1540
- const field = pkg["workspaces"];
1541
- let target;
1542
- if (field === void 0) {
1543
- target = [];
1544
- } else if (Array.isArray(field)) {
1545
- target = field;
1546
- } else if (field !== null && typeof field === "object" && Array.isArray(field.packages)) {
1547
- target = field.packages;
1548
- } else {
1549
- return { status: "unrecognized" };
1905
+
1906
+ // src/generators/shared/frontend-app.ts
1907
+ import { getProjects } from "@nx/devkit";
1908
+
1909
+ // ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
1910
+ function createScanner(text, ignoreTrivia = false) {
1911
+ const len = text.length;
1912
+ let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
1913
+ function scanHexDigits(count, exact) {
1914
+ let digits = 0;
1915
+ let value2 = 0;
1916
+ while (digits < count || !exact) {
1917
+ let ch = text.charCodeAt(pos);
1918
+ if (ch >= 48 && ch <= 57) {
1919
+ value2 = value2 * 16 + ch - 48;
1920
+ } else if (ch >= 65 && ch <= 70) {
1921
+ value2 = value2 * 16 + ch - 65 + 10;
1922
+ } else if (ch >= 97 && ch <= 102) {
1923
+ value2 = value2 * 16 + ch - 97 + 10;
1924
+ } else {
1925
+ break;
1926
+ }
1927
+ pos++;
1928
+ digits++;
1929
+ }
1930
+ if (digits < count) {
1931
+ value2 = -1;
1932
+ }
1933
+ return value2;
1550
1934
  }
1551
- if (target.some((entry) => entry === "libs/shared-types" || entry === "libs/*"))
1552
- return { status: "present" };
1553
- const surgical = insertSharedTypesWorkspace(packageJsonText, field === void 0 ? "absent" : Array.isArray(field) ? "array" : "object");
1554
- if (surgical !== null)
1555
- return { status: "added", text: surgical };
1556
- if (field === void 0)
1557
- pkg["workspaces"] = target;
1558
- target.push("libs/shared-types");
1559
- const indent = /\n([ \t]+)"/.exec(packageJsonText)?.[1] ?? " ";
1560
- const trailing = packageJsonText.endsWith("\n") ? "\n" : "";
1561
- return { status: "added", text: `${JSON.stringify(pkg, null, indent)}${trailing}` };
1562
- }
1563
- var SHARED_TYPES_ENTRY = '"libs/shared-types"';
1564
- function skipJsonValue(text, start) {
1565
- if (text[start] === '"') {
1566
- let i = start + 1;
1567
- while (i < text.length && text[i] !== '"')
1568
- i += text[i] === "\\" ? 2 : 1;
1569
- return i + 1;
1935
+ function setPosition(newPosition) {
1936
+ pos = newPosition;
1937
+ value = "";
1938
+ tokenOffset = 0;
1939
+ token = 16;
1940
+ scanError = 0;
1570
1941
  }
1571
- if (text[start] === "[" || text[start] === "{") {
1572
- let depth = 0;
1573
- let i = start;
1574
- while (i < text.length) {
1575
- const c = text[i];
1576
- if (c === '"') {
1577
- i = skipJsonValue(text, i);
1942
+ function scanNumber() {
1943
+ let start = pos;
1944
+ if (text.charCodeAt(pos) === 48) {
1945
+ pos++;
1946
+ } else {
1947
+ pos++;
1948
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
1949
+ pos++;
1950
+ }
1951
+ }
1952
+ if (pos < text.length && text.charCodeAt(pos) === 46) {
1953
+ pos++;
1954
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
1955
+ pos++;
1956
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
1957
+ pos++;
1958
+ }
1959
+ } else {
1960
+ scanError = 3;
1961
+ return text.substring(start, pos);
1962
+ }
1963
+ }
1964
+ let end = pos;
1965
+ if (pos < text.length && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) {
1966
+ pos++;
1967
+ if (pos < text.length && text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45) {
1968
+ pos++;
1969
+ }
1970
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
1971
+ pos++;
1972
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
1973
+ pos++;
1974
+ }
1975
+ end = pos;
1976
+ } else {
1977
+ scanError = 3;
1978
+ }
1979
+ }
1980
+ return text.substring(start, end);
1981
+ }
1982
+ function scanString() {
1983
+ let result = "", start = pos;
1984
+ while (true) {
1985
+ if (pos >= len) {
1986
+ result += text.substring(start, pos);
1987
+ scanError = 2;
1988
+ break;
1989
+ }
1990
+ const ch = text.charCodeAt(pos);
1991
+ if (ch === 34) {
1992
+ result += text.substring(start, pos);
1993
+ pos++;
1994
+ break;
1995
+ }
1996
+ if (ch === 92) {
1997
+ result += text.substring(start, pos);
1998
+ pos++;
1999
+ if (pos >= len) {
2000
+ scanError = 2;
2001
+ break;
2002
+ }
2003
+ const ch2 = text.charCodeAt(pos++);
2004
+ switch (ch2) {
2005
+ case 34:
2006
+ result += '"';
2007
+ break;
2008
+ case 92:
2009
+ result += "\\";
2010
+ break;
2011
+ case 47:
2012
+ result += "/";
2013
+ break;
2014
+ case 98:
2015
+ result += "\b";
2016
+ break;
2017
+ case 102:
2018
+ result += "\f";
2019
+ break;
2020
+ case 110:
2021
+ result += "\n";
2022
+ break;
2023
+ case 114:
2024
+ result += "\r";
2025
+ break;
2026
+ case 116:
2027
+ result += " ";
2028
+ break;
2029
+ case 117:
2030
+ const ch3 = scanHexDigits(4, true);
2031
+ if (ch3 >= 0) {
2032
+ result += String.fromCharCode(ch3);
2033
+ } else {
2034
+ scanError = 4;
2035
+ }
2036
+ break;
2037
+ default:
2038
+ scanError = 5;
2039
+ }
2040
+ start = pos;
1578
2041
  continue;
1579
2042
  }
1580
- if (c === "[" || c === "{")
1581
- depth++;
1582
- if (c === "]" || c === "}") {
1583
- depth--;
1584
- if (depth === 0)
1585
- return i + 1;
2043
+ if (ch >= 0 && ch <= 31) {
2044
+ if (isLineBreak(ch)) {
2045
+ result += text.substring(start, pos);
2046
+ scanError = 2;
2047
+ break;
2048
+ } else {
2049
+ scanError = 6;
2050
+ }
1586
2051
  }
1587
- i++;
2052
+ pos++;
1588
2053
  }
1589
- return -1;
2054
+ return result;
1590
2055
  }
1591
- const literal = /^[^,}\]\s]+/.exec(text.slice(start));
1592
- return literal ? start + literal[0].length : -1;
1593
- }
1594
- function objectMembers(text, open) {
1595
- const members = [];
1596
- let i = open + 1;
1597
- for (; ; ) {
1598
- while (/[\s,]/.test(text[i] ?? ""))
1599
- i++;
1600
- if (text[i] === "}")
1601
- return members;
1602
- if (text[i] !== '"')
1603
- return null;
1604
- const keyEnd = skipJsonValue(text, i);
1605
- const key = JSON.parse(text.slice(i, keyEnd));
1606
- i = keyEnd;
1607
- while (/\s/.test(text[i] ?? ""))
1608
- i++;
1609
- if (text[i] !== ":")
1610
- return null;
1611
- i++;
1612
- while (/\s/.test(text[i] ?? ""))
1613
- i++;
1614
- const valueEnd = skipJsonValue(text, i);
1615
- if (valueEnd === -1)
1616
- return null;
1617
- members.push({ key, valueStart: i, valueEnd });
1618
- i = valueEnd;
2056
+ function scanNext() {
2057
+ value = "";
2058
+ scanError = 0;
2059
+ tokenOffset = pos;
2060
+ lineStartOffset = lineNumber;
2061
+ prevTokenLineStartOffset = tokenLineStartOffset;
2062
+ if (pos >= len) {
2063
+ tokenOffset = len;
2064
+ return token = 17;
2065
+ }
2066
+ let code = text.charCodeAt(pos);
2067
+ if (isWhiteSpace(code)) {
2068
+ do {
2069
+ pos++;
2070
+ value += String.fromCharCode(code);
2071
+ code = text.charCodeAt(pos);
2072
+ } while (isWhiteSpace(code));
2073
+ return token = 15;
2074
+ }
2075
+ if (isLineBreak(code)) {
2076
+ pos++;
2077
+ value += String.fromCharCode(code);
2078
+ if (code === 13 && text.charCodeAt(pos) === 10) {
2079
+ pos++;
2080
+ value += "\n";
2081
+ }
2082
+ lineNumber++;
2083
+ tokenLineStartOffset = pos;
2084
+ return token = 14;
2085
+ }
2086
+ switch (code) {
2087
+ // tokens: []{}:,
2088
+ case 123:
2089
+ pos++;
2090
+ return token = 1;
2091
+ case 125:
2092
+ pos++;
2093
+ return token = 2;
2094
+ case 91:
2095
+ pos++;
2096
+ return token = 3;
2097
+ case 93:
2098
+ pos++;
2099
+ return token = 4;
2100
+ case 58:
2101
+ pos++;
2102
+ return token = 6;
2103
+ case 44:
2104
+ pos++;
2105
+ return token = 5;
2106
+ // strings
2107
+ case 34:
2108
+ pos++;
2109
+ value = scanString();
2110
+ return token = 10;
2111
+ // comments
2112
+ case 47:
2113
+ const start = pos - 1;
2114
+ if (text.charCodeAt(pos + 1) === 47) {
2115
+ pos += 2;
2116
+ while (pos < len) {
2117
+ if (isLineBreak(text.charCodeAt(pos))) {
2118
+ break;
2119
+ }
2120
+ pos++;
2121
+ }
2122
+ value = text.substring(start, pos);
2123
+ return token = 12;
2124
+ }
2125
+ if (text.charCodeAt(pos + 1) === 42) {
2126
+ pos += 2;
2127
+ const safeLength = len - 1;
2128
+ let commentClosed = false;
2129
+ while (pos < safeLength) {
2130
+ const ch = text.charCodeAt(pos);
2131
+ if (ch === 42 && text.charCodeAt(pos + 1) === 47) {
2132
+ pos += 2;
2133
+ commentClosed = true;
2134
+ break;
2135
+ }
2136
+ pos++;
2137
+ if (isLineBreak(ch)) {
2138
+ if (ch === 13 && text.charCodeAt(pos) === 10) {
2139
+ pos++;
2140
+ }
2141
+ lineNumber++;
2142
+ tokenLineStartOffset = pos;
2143
+ }
2144
+ }
2145
+ if (!commentClosed) {
2146
+ pos++;
2147
+ scanError = 1;
2148
+ }
2149
+ value = text.substring(start, pos);
2150
+ return token = 13;
2151
+ }
2152
+ value += String.fromCharCode(code);
2153
+ pos++;
2154
+ return token = 16;
2155
+ // numbers
2156
+ case 45:
2157
+ value += String.fromCharCode(code);
2158
+ pos++;
2159
+ if (pos === len || !isDigit(text.charCodeAt(pos))) {
2160
+ return token = 16;
2161
+ }
2162
+ // found a minus, followed by a number so
2163
+ // we fall through to proceed with scanning
2164
+ // numbers
2165
+ case 48:
2166
+ case 49:
2167
+ case 50:
2168
+ case 51:
2169
+ case 52:
2170
+ case 53:
2171
+ case 54:
2172
+ case 55:
2173
+ case 56:
2174
+ case 57:
2175
+ value += scanNumber();
2176
+ return token = 11;
2177
+ // literals and unknown symbols
2178
+ default:
2179
+ while (pos < len && isUnknownContentCharacter(code)) {
2180
+ pos++;
2181
+ code = text.charCodeAt(pos);
2182
+ }
2183
+ if (tokenOffset !== pos) {
2184
+ value = text.substring(tokenOffset, pos);
2185
+ switch (value) {
2186
+ case "true":
2187
+ return token = 8;
2188
+ case "false":
2189
+ return token = 9;
2190
+ case "null":
2191
+ return token = 7;
2192
+ }
2193
+ return token = 16;
2194
+ }
2195
+ value += String.fromCharCode(code);
2196
+ pos++;
2197
+ return token = 16;
2198
+ }
1619
2199
  }
1620
- }
1621
- function appendToArray(text, open, close, eol) {
1622
- const inner = text.slice(open + 1, close - 1);
1623
- if (inner.trim() === "")
1624
- return `${text.slice(0, open + 1)}${SHARED_TYPES_ENTRY}${text.slice(close - 1)}`;
1625
- if (!inner.includes("\n")) {
1626
- const body = inner.replace(/\s+$/, "");
1627
- return `${text.slice(0, open + 1)}${body}, ${SHARED_TYPES_ENTRY}${inner.slice(body.length)}${text.slice(close - 1)}`;
2200
+ function isUnknownContentCharacter(code) {
2201
+ if (isWhiteSpace(code) || isLineBreak(code)) {
2202
+ return false;
2203
+ }
2204
+ switch (code) {
2205
+ case 125:
2206
+ case 93:
2207
+ case 123:
2208
+ case 91:
2209
+ case 34:
2210
+ case 58:
2211
+ case 44:
2212
+ case 47:
2213
+ return false;
2214
+ }
2215
+ return true;
1628
2216
  }
1629
- const lastEntryLineStart = text.lastIndexOf("\n", open + 1 + inner.trimEnd().length - 1) + 1;
1630
- const itemIndent = /^[ \t]*/.exec(text.slice(lastEntryLineStart))?.[0] ?? " ";
1631
- const bodyEnd = open + 1 + inner.trimEnd().length;
1632
- return `${text.slice(0, bodyEnd)},${eol}${itemIndent}${SHARED_TYPES_ENTRY}${text.slice(bodyEnd)}`;
2217
+ function scanNextNonTrivia() {
2218
+ let result;
2219
+ do {
2220
+ result = scanNext();
2221
+ } while (result >= 12 && result <= 15);
2222
+ return result;
2223
+ }
2224
+ return {
2225
+ setPosition,
2226
+ getPosition: () => pos,
2227
+ scan: ignoreTrivia ? scanNextNonTrivia : scanNext,
2228
+ getToken: () => token,
2229
+ getTokenValue: () => value,
2230
+ getTokenOffset: () => tokenOffset,
2231
+ getTokenLength: () => pos - tokenOffset,
2232
+ getTokenStartLine: () => lineStartOffset,
2233
+ getTokenStartCharacter: () => tokenOffset - prevTokenLineStartOffset,
2234
+ getTokenError: () => scanError
2235
+ };
1633
2236
  }
1634
- function insertSharedTypesWorkspace(text, form) {
1635
- const eol = text.includes("\r\n") ? "\r\n" : "\n";
1636
- const rootOpen = text.indexOf("{");
1637
- if (rootOpen === -1)
1638
- return null;
1639
- const members = objectMembers(text, rootOpen);
1640
- if (members === null)
1641
- return null;
1642
- const workspaces = members.find((member) => member.key === "workspaces");
1643
- if (form === "absent") {
1644
- const last = members[members.length - 1];
1645
- const indent = /\n([ \t]+)"/.exec(text)?.[1] ?? " ";
1646
- if (last === void 0) {
1647
- const close = skipJsonValue(text, rootOpen) - 1;
1648
- return `${text.slice(0, rootOpen + 1)}${eol}${indent}"workspaces": [${SHARED_TYPES_ENTRY}]${eol}${text.slice(close)}`;
2237
+ function isWhiteSpace(ch) {
2238
+ return ch === 32 || ch === 9;
2239
+ }
2240
+ function isLineBreak(ch) {
2241
+ return ch === 10 || ch === 13;
2242
+ }
2243
+ function isDigit(ch) {
2244
+ return ch >= 48 && ch <= 57;
2245
+ }
2246
+ var CharacterCodes;
2247
+ (function(CharacterCodes2) {
2248
+ CharacterCodes2[CharacterCodes2["lineFeed"] = 10] = "lineFeed";
2249
+ CharacterCodes2[CharacterCodes2["carriageReturn"] = 13] = "carriageReturn";
2250
+ CharacterCodes2[CharacterCodes2["space"] = 32] = "space";
2251
+ CharacterCodes2[CharacterCodes2["_0"] = 48] = "_0";
2252
+ CharacterCodes2[CharacterCodes2["_1"] = 49] = "_1";
2253
+ CharacterCodes2[CharacterCodes2["_2"] = 50] = "_2";
2254
+ CharacterCodes2[CharacterCodes2["_3"] = 51] = "_3";
2255
+ CharacterCodes2[CharacterCodes2["_4"] = 52] = "_4";
2256
+ CharacterCodes2[CharacterCodes2["_5"] = 53] = "_5";
2257
+ CharacterCodes2[CharacterCodes2["_6"] = 54] = "_6";
2258
+ CharacterCodes2[CharacterCodes2["_7"] = 55] = "_7";
2259
+ CharacterCodes2[CharacterCodes2["_8"] = 56] = "_8";
2260
+ CharacterCodes2[CharacterCodes2["_9"] = 57] = "_9";
2261
+ CharacterCodes2[CharacterCodes2["a"] = 97] = "a";
2262
+ CharacterCodes2[CharacterCodes2["b"] = 98] = "b";
2263
+ CharacterCodes2[CharacterCodes2["c"] = 99] = "c";
2264
+ CharacterCodes2[CharacterCodes2["d"] = 100] = "d";
2265
+ CharacterCodes2[CharacterCodes2["e"] = 101] = "e";
2266
+ CharacterCodes2[CharacterCodes2["f"] = 102] = "f";
2267
+ CharacterCodes2[CharacterCodes2["g"] = 103] = "g";
2268
+ CharacterCodes2[CharacterCodes2["h"] = 104] = "h";
2269
+ CharacterCodes2[CharacterCodes2["i"] = 105] = "i";
2270
+ CharacterCodes2[CharacterCodes2["j"] = 106] = "j";
2271
+ CharacterCodes2[CharacterCodes2["k"] = 107] = "k";
2272
+ CharacterCodes2[CharacterCodes2["l"] = 108] = "l";
2273
+ CharacterCodes2[CharacterCodes2["m"] = 109] = "m";
2274
+ CharacterCodes2[CharacterCodes2["n"] = 110] = "n";
2275
+ CharacterCodes2[CharacterCodes2["o"] = 111] = "o";
2276
+ CharacterCodes2[CharacterCodes2["p"] = 112] = "p";
2277
+ CharacterCodes2[CharacterCodes2["q"] = 113] = "q";
2278
+ CharacterCodes2[CharacterCodes2["r"] = 114] = "r";
2279
+ CharacterCodes2[CharacterCodes2["s"] = 115] = "s";
2280
+ CharacterCodes2[CharacterCodes2["t"] = 116] = "t";
2281
+ CharacterCodes2[CharacterCodes2["u"] = 117] = "u";
2282
+ CharacterCodes2[CharacterCodes2["v"] = 118] = "v";
2283
+ CharacterCodes2[CharacterCodes2["w"] = 119] = "w";
2284
+ CharacterCodes2[CharacterCodes2["x"] = 120] = "x";
2285
+ CharacterCodes2[CharacterCodes2["y"] = 121] = "y";
2286
+ CharacterCodes2[CharacterCodes2["z"] = 122] = "z";
2287
+ CharacterCodes2[CharacterCodes2["A"] = 65] = "A";
2288
+ CharacterCodes2[CharacterCodes2["B"] = 66] = "B";
2289
+ CharacterCodes2[CharacterCodes2["C"] = 67] = "C";
2290
+ CharacterCodes2[CharacterCodes2["D"] = 68] = "D";
2291
+ CharacterCodes2[CharacterCodes2["E"] = 69] = "E";
2292
+ CharacterCodes2[CharacterCodes2["F"] = 70] = "F";
2293
+ CharacterCodes2[CharacterCodes2["G"] = 71] = "G";
2294
+ CharacterCodes2[CharacterCodes2["H"] = 72] = "H";
2295
+ CharacterCodes2[CharacterCodes2["I"] = 73] = "I";
2296
+ CharacterCodes2[CharacterCodes2["J"] = 74] = "J";
2297
+ CharacterCodes2[CharacterCodes2["K"] = 75] = "K";
2298
+ CharacterCodes2[CharacterCodes2["L"] = 76] = "L";
2299
+ CharacterCodes2[CharacterCodes2["M"] = 77] = "M";
2300
+ CharacterCodes2[CharacterCodes2["N"] = 78] = "N";
2301
+ CharacterCodes2[CharacterCodes2["O"] = 79] = "O";
2302
+ CharacterCodes2[CharacterCodes2["P"] = 80] = "P";
2303
+ CharacterCodes2[CharacterCodes2["Q"] = 81] = "Q";
2304
+ CharacterCodes2[CharacterCodes2["R"] = 82] = "R";
2305
+ CharacterCodes2[CharacterCodes2["S"] = 83] = "S";
2306
+ CharacterCodes2[CharacterCodes2["T"] = 84] = "T";
2307
+ CharacterCodes2[CharacterCodes2["U"] = 85] = "U";
2308
+ CharacterCodes2[CharacterCodes2["V"] = 86] = "V";
2309
+ CharacterCodes2[CharacterCodes2["W"] = 87] = "W";
2310
+ CharacterCodes2[CharacterCodes2["X"] = 88] = "X";
2311
+ CharacterCodes2[CharacterCodes2["Y"] = 89] = "Y";
2312
+ CharacterCodes2[CharacterCodes2["Z"] = 90] = "Z";
2313
+ CharacterCodes2[CharacterCodes2["asterisk"] = 42] = "asterisk";
2314
+ CharacterCodes2[CharacterCodes2["backslash"] = 92] = "backslash";
2315
+ CharacterCodes2[CharacterCodes2["closeBrace"] = 125] = "closeBrace";
2316
+ CharacterCodes2[CharacterCodes2["closeBracket"] = 93] = "closeBracket";
2317
+ CharacterCodes2[CharacterCodes2["colon"] = 58] = "colon";
2318
+ CharacterCodes2[CharacterCodes2["comma"] = 44] = "comma";
2319
+ CharacterCodes2[CharacterCodes2["dot"] = 46] = "dot";
2320
+ CharacterCodes2[CharacterCodes2["doubleQuote"] = 34] = "doubleQuote";
2321
+ CharacterCodes2[CharacterCodes2["minus"] = 45] = "minus";
2322
+ CharacterCodes2[CharacterCodes2["openBrace"] = 123] = "openBrace";
2323
+ CharacterCodes2[CharacterCodes2["openBracket"] = 91] = "openBracket";
2324
+ CharacterCodes2[CharacterCodes2["plus"] = 43] = "plus";
2325
+ CharacterCodes2[CharacterCodes2["slash"] = 47] = "slash";
2326
+ CharacterCodes2[CharacterCodes2["formFeed"] = 12] = "formFeed";
2327
+ CharacterCodes2[CharacterCodes2["tab"] = 9] = "tab";
2328
+ })(CharacterCodes || (CharacterCodes = {}));
2329
+
2330
+ // ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/string-intern.js
2331
+ var cachedSpaces = new Array(20).fill(0).map((_, index) => {
2332
+ return " ".repeat(index);
2333
+ });
2334
+ var maxCachedValues = 200;
2335
+ var cachedBreakLinesWithSpaces = {
2336
+ " ": {
2337
+ "\n": new Array(maxCachedValues).fill(0).map((_, index) => {
2338
+ return "\n" + " ".repeat(index);
2339
+ }),
2340
+ "\r": new Array(maxCachedValues).fill(0).map((_, index) => {
2341
+ return "\r" + " ".repeat(index);
2342
+ }),
2343
+ "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
2344
+ return "\r\n" + " ".repeat(index);
2345
+ })
2346
+ },
2347
+ " ": {
2348
+ "\n": new Array(maxCachedValues).fill(0).map((_, index) => {
2349
+ return "\n" + " ".repeat(index);
2350
+ }),
2351
+ "\r": new Array(maxCachedValues).fill(0).map((_, index) => {
2352
+ return "\r" + " ".repeat(index);
2353
+ }),
2354
+ "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
2355
+ return "\r\n" + " ".repeat(index);
2356
+ })
2357
+ }
2358
+ };
2359
+
2360
+ // ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/parser.js
2361
+ var ParseOptions;
2362
+ (function(ParseOptions2) {
2363
+ ParseOptions2.DEFAULT = {
2364
+ allowTrailingComma: false
2365
+ };
2366
+ })(ParseOptions || (ParseOptions = {}));
2367
+ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
2368
+ let currentParent = { type: "array", offset: -1, length: -1, children: [], parent: void 0 };
2369
+ function ensurePropertyComplete(endOffset) {
2370
+ if (currentParent.type === "property") {
2371
+ currentParent.length = endOffset - currentParent.offset;
2372
+ currentParent = currentParent.parent;
1649
2373
  }
1650
- return `${text.slice(0, last.valueEnd)},${eol}${indent}"workspaces": [${SHARED_TYPES_ENTRY}]${text.slice(last.valueEnd)}`;
1651
2374
  }
1652
- if (workspaces === void 0)
1653
- return null;
1654
- if (form === "array") {
1655
- if (text[workspaces.valueStart] !== "[")
1656
- return null;
1657
- return appendToArray(text, workspaces.valueStart, workspaces.valueEnd, eol);
2375
+ function onValue(valueNode) {
2376
+ currentParent.children.push(valueNode);
2377
+ return valueNode;
1658
2378
  }
1659
- if (text[workspaces.valueStart] !== "{")
1660
- return null;
1661
- const packages = objectMembers(text, workspaces.valueStart)?.find((member) => member.key === "packages");
1662
- if (packages === void 0 || text[packages.valueStart] !== "[")
1663
- return null;
1664
- return appendToArray(text, packages.valueStart, packages.valueEnd, eol);
2379
+ const visitor = {
2380
+ onObjectBegin: (offset) => {
2381
+ currentParent = onValue({ type: "object", offset, length: -1, parent: currentParent, children: [] });
2382
+ },
2383
+ onObjectProperty: (name, offset, length) => {
2384
+ currentParent = onValue({ type: "property", offset, length: -1, parent: currentParent, children: [] });
2385
+ currentParent.children.push({ type: "string", value: name, offset, length, parent: currentParent });
2386
+ },
2387
+ onObjectEnd: (offset, length) => {
2388
+ ensurePropertyComplete(offset + length);
2389
+ currentParent.length = offset + length - currentParent.offset;
2390
+ currentParent = currentParent.parent;
2391
+ ensurePropertyComplete(offset + length);
2392
+ },
2393
+ onArrayBegin: (offset, length) => {
2394
+ currentParent = onValue({ type: "array", offset, length: -1, parent: currentParent, children: [] });
2395
+ },
2396
+ onArrayEnd: (offset, length) => {
2397
+ currentParent.length = offset + length - currentParent.offset;
2398
+ currentParent = currentParent.parent;
2399
+ ensurePropertyComplete(offset + length);
2400
+ },
2401
+ onLiteralValue: (value, offset, length) => {
2402
+ onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
2403
+ ensurePropertyComplete(offset + length);
2404
+ },
2405
+ onSeparator: (sep, offset, length) => {
2406
+ if (currentParent.type === "property") {
2407
+ if (sep === ":") {
2408
+ currentParent.colonOffset = offset;
2409
+ } else if (sep === ",") {
2410
+ ensurePropertyComplete(offset);
2411
+ }
2412
+ }
2413
+ },
2414
+ onError: (error, offset, length) => {
2415
+ errors.push({ error, offset, length });
2416
+ }
2417
+ };
2418
+ visit(text, visitor, options);
2419
+ const result = currentParent.children[0];
2420
+ if (result) {
2421
+ delete result.parent;
2422
+ }
2423
+ return result;
1665
2424
  }
1666
- function addSharedTypesWorkspaceGlob(yamlText) {
1667
- if (/^\s*-\s*['"]?(libs\/shared-types|libs\/\*)['"]?\s*$/m.test(yamlText)) {
1668
- return { status: "present" };
2425
+ function findNodeAtLocation(root, path) {
2426
+ if (!root) {
2427
+ return void 0;
1669
2428
  }
1670
- const lines = yamlText.split("\n");
1671
- const packagesIndex = lines.findIndex((line) => /^packages:\s*$/.test(line));
1672
- if (packagesIndex === -1)
1673
- return { status: "unrecognized" };
1674
- let lastEntry = packagesIndex;
1675
- for (let i = packagesIndex + 1; i < lines.length; i++) {
1676
- if (/^\s+-\s+\S/.test(lines[i])) {
1677
- lastEntry = i;
1678
- } else if (lines[i].trim() === "") {
1679
- continue;
2429
+ let node = root;
2430
+ for (let segment of path) {
2431
+ if (typeof segment === "string") {
2432
+ if (node.type !== "object" || !Array.isArray(node.children)) {
2433
+ return void 0;
2434
+ }
2435
+ let found = false;
2436
+ for (const propertyNode of node.children) {
2437
+ if (Array.isArray(propertyNode.children) && propertyNode.children[0].value === segment && propertyNode.children.length === 2) {
2438
+ node = propertyNode.children[1];
2439
+ found = true;
2440
+ break;
2441
+ }
2442
+ }
2443
+ if (!found) {
2444
+ return void 0;
2445
+ }
1680
2446
  } else {
1681
- break;
2447
+ const index = segment;
2448
+ if (node.type !== "array" || index < 0 || !Array.isArray(node.children) || index >= node.children.length) {
2449
+ return void 0;
2450
+ }
2451
+ node = node.children[index];
1682
2452
  }
1683
2453
  }
1684
- if (lastEntry === packagesIndex)
1685
- return { status: "unrecognized" };
1686
- const indent = /^(\s+)-/.exec(lines[lastEntry])?.[1] ?? " ";
1687
- lines.splice(lastEntry + 1, 0, `${indent}- 'libs/shared-types'`);
1688
- return { status: "added", text: lines.join("\n") };
1689
- }
1690
-
1691
- // src/generators/shared/dry-run.ts
1692
- function resolveDryRun(options) {
1693
- return process.env["NX_DRY_RUN"] === "true" || (options.dryRun ?? false);
2454
+ return node;
2455
+ }
2456
+ function visit(text, visitor, options = ParseOptions.DEFAULT) {
2457
+ const _scanner = createScanner(text, false);
2458
+ const _jsonPath = [];
2459
+ let suppressedCallbacks = 0;
2460
+ function toNoArgVisit(visitFunction) {
2461
+ return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
2462
+ }
2463
+ function toOneArgVisit(visitFunction) {
2464
+ return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
2465
+ }
2466
+ function toOneArgVisitWithPath(visitFunction) {
2467
+ return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
2468
+ }
2469
+ function toBeginVisit(visitFunction) {
2470
+ return visitFunction ? () => {
2471
+ if (suppressedCallbacks > 0) {
2472
+ suppressedCallbacks++;
2473
+ } else {
2474
+ let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice());
2475
+ if (cbReturn === false) {
2476
+ suppressedCallbacks = 1;
2477
+ }
2478
+ }
2479
+ } : () => true;
2480
+ }
2481
+ function toEndVisit(visitFunction) {
2482
+ return visitFunction ? () => {
2483
+ if (suppressedCallbacks > 0) {
2484
+ suppressedCallbacks--;
2485
+ }
2486
+ if (suppressedCallbacks === 0) {
2487
+ visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter());
2488
+ }
2489
+ } : () => true;
2490
+ }
2491
+ const onObjectBegin = toBeginVisit(visitor.onObjectBegin), onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty), onObjectEnd = toEndVisit(visitor.onObjectEnd), onArrayBegin = toBeginVisit(visitor.onArrayBegin), onArrayEnd = toEndVisit(visitor.onArrayEnd), onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue), onSeparator = toOneArgVisit(visitor.onSeparator), onComment = toNoArgVisit(visitor.onComment), onError = toOneArgVisit(visitor.onError);
2492
+ const disallowComments = options && options.disallowComments;
2493
+ const allowTrailingComma = options && options.allowTrailingComma;
2494
+ function scanNext() {
2495
+ while (true) {
2496
+ const token = _scanner.scan();
2497
+ switch (_scanner.getTokenError()) {
2498
+ case 4:
2499
+ handleError(
2500
+ 14
2501
+ /* ParseErrorCode.InvalidUnicode */
2502
+ );
2503
+ break;
2504
+ case 5:
2505
+ handleError(
2506
+ 15
2507
+ /* ParseErrorCode.InvalidEscapeCharacter */
2508
+ );
2509
+ break;
2510
+ case 3:
2511
+ handleError(
2512
+ 13
2513
+ /* ParseErrorCode.UnexpectedEndOfNumber */
2514
+ );
2515
+ break;
2516
+ case 1:
2517
+ if (!disallowComments) {
2518
+ handleError(
2519
+ 11
2520
+ /* ParseErrorCode.UnexpectedEndOfComment */
2521
+ );
2522
+ }
2523
+ break;
2524
+ case 2:
2525
+ handleError(
2526
+ 12
2527
+ /* ParseErrorCode.UnexpectedEndOfString */
2528
+ );
2529
+ break;
2530
+ case 6:
2531
+ handleError(
2532
+ 16
2533
+ /* ParseErrorCode.InvalidCharacter */
2534
+ );
2535
+ break;
2536
+ }
2537
+ switch (token) {
2538
+ case 12:
2539
+ case 13:
2540
+ if (disallowComments) {
2541
+ handleError(
2542
+ 10
2543
+ /* ParseErrorCode.InvalidCommentToken */
2544
+ );
2545
+ } else {
2546
+ onComment();
2547
+ }
2548
+ break;
2549
+ case 16:
2550
+ handleError(
2551
+ 1
2552
+ /* ParseErrorCode.InvalidSymbol */
2553
+ );
2554
+ break;
2555
+ case 15:
2556
+ case 14:
2557
+ break;
2558
+ default:
2559
+ return token;
2560
+ }
2561
+ }
2562
+ }
2563
+ function handleError(error, skipUntilAfter = [], skipUntil = []) {
2564
+ onError(error);
2565
+ if (skipUntilAfter.length + skipUntil.length > 0) {
2566
+ let token = _scanner.getToken();
2567
+ while (token !== 17) {
2568
+ if (skipUntilAfter.indexOf(token) !== -1) {
2569
+ scanNext();
2570
+ break;
2571
+ } else if (skipUntil.indexOf(token) !== -1) {
2572
+ break;
2573
+ }
2574
+ token = scanNext();
2575
+ }
2576
+ }
2577
+ }
2578
+ function parseString(isValue) {
2579
+ const value = _scanner.getTokenValue();
2580
+ if (isValue) {
2581
+ onLiteralValue(value);
2582
+ } else {
2583
+ onObjectProperty(value);
2584
+ _jsonPath.push(value);
2585
+ }
2586
+ scanNext();
2587
+ return true;
2588
+ }
2589
+ function parseLiteral() {
2590
+ switch (_scanner.getToken()) {
2591
+ case 11:
2592
+ const tokenValue = _scanner.getTokenValue();
2593
+ let value = Number(tokenValue);
2594
+ if (isNaN(value)) {
2595
+ handleError(
2596
+ 2
2597
+ /* ParseErrorCode.InvalidNumberFormat */
2598
+ );
2599
+ value = 0;
2600
+ }
2601
+ onLiteralValue(value);
2602
+ break;
2603
+ case 7:
2604
+ onLiteralValue(null);
2605
+ break;
2606
+ case 8:
2607
+ onLiteralValue(true);
2608
+ break;
2609
+ case 9:
2610
+ onLiteralValue(false);
2611
+ break;
2612
+ default:
2613
+ return false;
2614
+ }
2615
+ scanNext();
2616
+ return true;
2617
+ }
2618
+ function parseProperty() {
2619
+ if (_scanner.getToken() !== 10) {
2620
+ handleError(3, [], [
2621
+ 2,
2622
+ 5
2623
+ /* SyntaxKind.CommaToken */
2624
+ ]);
2625
+ return false;
2626
+ }
2627
+ parseString(false);
2628
+ if (_scanner.getToken() === 6) {
2629
+ onSeparator(":");
2630
+ scanNext();
2631
+ if (!parseValue()) {
2632
+ handleError(4, [], [
2633
+ 2,
2634
+ 5
2635
+ /* SyntaxKind.CommaToken */
2636
+ ]);
2637
+ }
2638
+ } else {
2639
+ handleError(5, [], [
2640
+ 2,
2641
+ 5
2642
+ /* SyntaxKind.CommaToken */
2643
+ ]);
2644
+ }
2645
+ _jsonPath.pop();
2646
+ return true;
2647
+ }
2648
+ function parseObject() {
2649
+ onObjectBegin();
2650
+ scanNext();
2651
+ let needsComma = false;
2652
+ while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
2653
+ if (_scanner.getToken() === 5) {
2654
+ if (!needsComma) {
2655
+ handleError(4, [], []);
2656
+ }
2657
+ onSeparator(",");
2658
+ scanNext();
2659
+ if (_scanner.getToken() === 2 && allowTrailingComma) {
2660
+ break;
2661
+ }
2662
+ } else if (needsComma) {
2663
+ handleError(6, [], []);
2664
+ }
2665
+ if (!parseProperty()) {
2666
+ handleError(4, [], [
2667
+ 2,
2668
+ 5
2669
+ /* SyntaxKind.CommaToken */
2670
+ ]);
2671
+ }
2672
+ needsComma = true;
2673
+ }
2674
+ onObjectEnd();
2675
+ if (_scanner.getToken() !== 2) {
2676
+ handleError(7, [
2677
+ 2
2678
+ /* SyntaxKind.CloseBraceToken */
2679
+ ], []);
2680
+ } else {
2681
+ scanNext();
2682
+ }
2683
+ return true;
2684
+ }
2685
+ function parseArray() {
2686
+ onArrayBegin();
2687
+ scanNext();
2688
+ let isFirstElement = true;
2689
+ let needsComma = false;
2690
+ while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
2691
+ if (_scanner.getToken() === 5) {
2692
+ if (!needsComma) {
2693
+ handleError(4, [], []);
2694
+ }
2695
+ onSeparator(",");
2696
+ scanNext();
2697
+ if (_scanner.getToken() === 4 && allowTrailingComma) {
2698
+ break;
2699
+ }
2700
+ } else if (needsComma) {
2701
+ handleError(6, [], []);
2702
+ }
2703
+ if (isFirstElement) {
2704
+ _jsonPath.push(0);
2705
+ isFirstElement = false;
2706
+ } else {
2707
+ _jsonPath[_jsonPath.length - 1]++;
2708
+ }
2709
+ if (!parseValue()) {
2710
+ handleError(4, [], [
2711
+ 4,
2712
+ 5
2713
+ /* SyntaxKind.CommaToken */
2714
+ ]);
2715
+ }
2716
+ needsComma = true;
2717
+ }
2718
+ onArrayEnd();
2719
+ if (!isFirstElement) {
2720
+ _jsonPath.pop();
2721
+ }
2722
+ if (_scanner.getToken() !== 4) {
2723
+ handleError(8, [
2724
+ 4
2725
+ /* SyntaxKind.CloseBracketToken */
2726
+ ], []);
2727
+ } else {
2728
+ scanNext();
2729
+ }
2730
+ return true;
2731
+ }
2732
+ function parseValue() {
2733
+ switch (_scanner.getToken()) {
2734
+ case 3:
2735
+ return parseArray();
2736
+ case 1:
2737
+ return parseObject();
2738
+ case 10:
2739
+ return parseString(true);
2740
+ default:
2741
+ return parseLiteral();
2742
+ }
2743
+ }
2744
+ scanNext();
2745
+ if (_scanner.getToken() === 17) {
2746
+ if (options.allowEmptyContent) {
2747
+ return true;
2748
+ }
2749
+ handleError(4, [], []);
2750
+ return false;
2751
+ }
2752
+ if (!parseValue()) {
2753
+ handleError(4, [], []);
2754
+ return false;
2755
+ }
2756
+ if (_scanner.getToken() !== 17) {
2757
+ handleError(9, [], []);
2758
+ }
2759
+ return true;
2760
+ }
2761
+ function getNodeType(value) {
2762
+ switch (typeof value) {
2763
+ case "boolean":
2764
+ return "boolean";
2765
+ case "number":
2766
+ return "number";
2767
+ case "string":
2768
+ return "string";
2769
+ case "object": {
2770
+ if (!value) {
2771
+ return "null";
2772
+ } else if (Array.isArray(value)) {
2773
+ return "array";
2774
+ }
2775
+ return "object";
2776
+ }
2777
+ default:
2778
+ return "null";
2779
+ }
1694
2780
  }
1695
2781
 
1696
- // src/generators/shared/frontend-app.ts
1697
- import { getProjects } from "@nx/devkit";
2782
+ // ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/main.js
2783
+ var ScanError;
2784
+ (function(ScanError2) {
2785
+ ScanError2[ScanError2["None"] = 0] = "None";
2786
+ ScanError2[ScanError2["UnexpectedEndOfComment"] = 1] = "UnexpectedEndOfComment";
2787
+ ScanError2[ScanError2["UnexpectedEndOfString"] = 2] = "UnexpectedEndOfString";
2788
+ ScanError2[ScanError2["UnexpectedEndOfNumber"] = 3] = "UnexpectedEndOfNumber";
2789
+ ScanError2[ScanError2["InvalidUnicode"] = 4] = "InvalidUnicode";
2790
+ ScanError2[ScanError2["InvalidEscapeCharacter"] = 5] = "InvalidEscapeCharacter";
2791
+ ScanError2[ScanError2["InvalidCharacter"] = 6] = "InvalidCharacter";
2792
+ })(ScanError || (ScanError = {}));
2793
+ var SyntaxKind;
2794
+ (function(SyntaxKind2) {
2795
+ SyntaxKind2[SyntaxKind2["OpenBraceToken"] = 1] = "OpenBraceToken";
2796
+ SyntaxKind2[SyntaxKind2["CloseBraceToken"] = 2] = "CloseBraceToken";
2797
+ SyntaxKind2[SyntaxKind2["OpenBracketToken"] = 3] = "OpenBracketToken";
2798
+ SyntaxKind2[SyntaxKind2["CloseBracketToken"] = 4] = "CloseBracketToken";
2799
+ SyntaxKind2[SyntaxKind2["CommaToken"] = 5] = "CommaToken";
2800
+ SyntaxKind2[SyntaxKind2["ColonToken"] = 6] = "ColonToken";
2801
+ SyntaxKind2[SyntaxKind2["NullKeyword"] = 7] = "NullKeyword";
2802
+ SyntaxKind2[SyntaxKind2["TrueKeyword"] = 8] = "TrueKeyword";
2803
+ SyntaxKind2[SyntaxKind2["FalseKeyword"] = 9] = "FalseKeyword";
2804
+ SyntaxKind2[SyntaxKind2["StringLiteral"] = 10] = "StringLiteral";
2805
+ SyntaxKind2[SyntaxKind2["NumericLiteral"] = 11] = "NumericLiteral";
2806
+ SyntaxKind2[SyntaxKind2["LineCommentTrivia"] = 12] = "LineCommentTrivia";
2807
+ SyntaxKind2[SyntaxKind2["BlockCommentTrivia"] = 13] = "BlockCommentTrivia";
2808
+ SyntaxKind2[SyntaxKind2["LineBreakTrivia"] = 14] = "LineBreakTrivia";
2809
+ SyntaxKind2[SyntaxKind2["Trivia"] = 15] = "Trivia";
2810
+ SyntaxKind2[SyntaxKind2["Unknown"] = 16] = "Unknown";
2811
+ SyntaxKind2[SyntaxKind2["EOF"] = 17] = "EOF";
2812
+ })(SyntaxKind || (SyntaxKind = {}));
2813
+ var parseTree2 = parseTree;
2814
+ var findNodeAtLocation2 = findNodeAtLocation;
2815
+ var ParseErrorCode;
2816
+ (function(ParseErrorCode2) {
2817
+ ParseErrorCode2[ParseErrorCode2["InvalidSymbol"] = 1] = "InvalidSymbol";
2818
+ ParseErrorCode2[ParseErrorCode2["InvalidNumberFormat"] = 2] = "InvalidNumberFormat";
2819
+ ParseErrorCode2[ParseErrorCode2["PropertyNameExpected"] = 3] = "PropertyNameExpected";
2820
+ ParseErrorCode2[ParseErrorCode2["ValueExpected"] = 4] = "ValueExpected";
2821
+ ParseErrorCode2[ParseErrorCode2["ColonExpected"] = 5] = "ColonExpected";
2822
+ ParseErrorCode2[ParseErrorCode2["CommaExpected"] = 6] = "CommaExpected";
2823
+ ParseErrorCode2[ParseErrorCode2["CloseBraceExpected"] = 7] = "CloseBraceExpected";
2824
+ ParseErrorCode2[ParseErrorCode2["CloseBracketExpected"] = 8] = "CloseBracketExpected";
2825
+ ParseErrorCode2[ParseErrorCode2["EndOfFileExpected"] = 9] = "EndOfFileExpected";
2826
+ ParseErrorCode2[ParseErrorCode2["InvalidCommentToken"] = 10] = "InvalidCommentToken";
2827
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfComment"] = 11] = "UnexpectedEndOfComment";
2828
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfString"] = 12] = "UnexpectedEndOfString";
2829
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfNumber"] = 13] = "UnexpectedEndOfNumber";
2830
+ ParseErrorCode2[ParseErrorCode2["InvalidUnicode"] = 14] = "InvalidUnicode";
2831
+ ParseErrorCode2[ParseErrorCode2["InvalidEscapeCharacter"] = 15] = "InvalidEscapeCharacter";
2832
+ ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
2833
+ })(ParseErrorCode || (ParseErrorCode = {}));
1698
2834
 
1699
- // src/generators/shared/project-type.ts
1700
- import { parseJson } from "@nx/devkit";
1701
- function resolveProjectType(tree, root, declared) {
1702
- if (declared) return declared === "library" ? "library" : "application";
1703
- const prefix = root === "" || root === "." ? "" : `${root}/`;
1704
- if (tree.exists(`${prefix}tsconfig.lib.json`)) return "library";
1705
- if (tree.exists(`${prefix}tsconfig.app.json`)) return "application";
1706
- const raw = tree.read(`${prefix}package.json`, "utf-8");
1707
- if (raw === null) return "library";
1708
- try {
1709
- const manifest = parseJson(raw);
1710
- if (manifest && !["exports", "main", "module", "bin"].some((key) => Boolean(manifest[key]))) return "application";
1711
- } catch {
2835
+ // src/generators/shared/json-edit.ts
2836
+ function detectIndent(raw) {
2837
+ for (const line of raw.split("\n")) {
2838
+ const match = /^(\t+| +)/.exec(line);
2839
+ if (match) return match[1];
1712
2840
  }
1713
- return "library";
2841
+ return " ";
1714
2842
  }
1715
-
1716
- // src/generators/shared/frontend-app.ts
1717
- var baseNameOf = (root) => root.split("/").pop() ?? root;
1718
- function isEndToEnd(root) {
1719
- const dir = baseNameOf(root);
1720
- return dir === "e2e" || dir.endsWith("-e2e");
2843
+ function styleOf(raw) {
2844
+ return { eol: raw.includes("\r\n") ? "\r\n" : "\n", unit: detectIndent(raw) };
1721
2845
  }
1722
- function isBackend(root, config) {
1723
- return baseNameOf(root) === "api" || (config.tags ?? []).includes("scope:backend");
2846
+ function lineIndentAt(raw, offset) {
2847
+ const lineStart = raw.lastIndexOf("\n", offset - 1) + 1;
2848
+ return /^[ \t]*/.exec(raw.slice(lineStart))?.[0] ?? "";
1724
2849
  }
1725
- function sourceRootOf(config) {
1726
- if (config.sourceRoot) return config.sourceRoot;
1727
- return config.root === "" || config.root === "." ? "src" : `${config.root}/src`;
2850
+ function isMultiline(raw, node) {
2851
+ return raw.slice(node.offset, node.offset + node.length).includes("\n");
1728
2852
  }
1729
- function resolveFrontendApp(tree, explicitProject) {
1730
- let projects;
1731
- try {
1732
- projects = getProjects(tree);
1733
- } catch (error) {
1734
- const message = error instanceof Error ? error.message : String(error);
1735
- const pathMatch = /Cannot parse ([^:]+):/.exec(message);
1736
- return { kind: "invalid-project-json", detail: pathMatch ? pathMatch[1] : message };
2853
+ function isPrimitive(value) {
2854
+ return value === null || ["string", "number", "boolean"].includes(typeof value);
2855
+ }
2856
+ function renderValue(value, indent, style) {
2857
+ if (isPrimitive(value)) return JSON.stringify(value);
2858
+ if (Array.isArray(value)) {
2859
+ if (value.every(isPrimitive)) return `[${value.map((item) => JSON.stringify(item)).join(", ")}]`;
2860
+ const inner2 = `${indent}${style.unit}`;
2861
+ return `[${style.eol}${value.map((item) => `${inner2}${renderValue(item, inner2, style)}`).join(`,${style.eol}`)}${style.eol}${indent}]`;
1737
2862
  }
1738
- const found = (project2, config2) => ({
1739
- kind: "found",
1740
- project: project2,
1741
- root: config2.root,
1742
- sourceRoot: sourceRootOf(config2)
1743
- });
1744
- if (explicitProject !== void 0) {
1745
- const config2 = projects.get(explicitProject);
1746
- return config2 ? found(explicitProject, config2) : { kind: "not-found", project: explicitProject };
2863
+ const entries = Object.entries(value);
2864
+ if (entries.length === 0) return "{}";
2865
+ const inner = `${indent}${style.unit}`;
2866
+ const body = entries.map(([key, item]) => `${inner}${JSON.stringify(key)}: ${renderValue(item, inner, style)}`).join(`,${style.eol}`);
2867
+ return `{${style.eol}${body}${style.eol}${indent}}`;
2868
+ }
2869
+ function splice(raw, start, end, text) {
2870
+ return raw.slice(0, start) + text + raw.slice(end);
2871
+ }
2872
+ function nest(keys, value) {
2873
+ return keys.reduceRight((inner, key) => ({ [key]: inner }), value);
2874
+ }
2875
+ function insertProperty(raw, object, key, value, style) {
2876
+ const properties = object.children ?? [];
2877
+ const objectIndent = lineIndentAt(raw, object.offset);
2878
+ if (properties.length === 0) {
2879
+ const inner = `${objectIndent}${style.unit}`;
2880
+ const text = `{${style.eol}${inner}${JSON.stringify(String(key))}: ${renderValue(value, inner, style)}${style.eol}${objectIndent}}`;
2881
+ return splice(raw, object.offset, object.offset + object.length, text);
1747
2882
  }
1748
- const eligible = [...projects.entries()].filter(([, config2]) => !isEndToEnd(config2.root) && !isBackend(config2.root, config2));
1749
- const declared = eligible.filter(([, config2]) => config2.projectType === "application");
1750
- const inferred = eligible.filter(
1751
- ([, config2]) => config2.projectType === void 0 && config2.root !== "" && config2.root !== "." && resolveProjectType(tree, config2.root, void 0) === "application"
1752
- );
1753
- const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
1754
- if (candidates.length === 0) return { kind: "none" };
1755
- if (candidates.length > 1) return { kind: "ambiguous", candidates: candidates.map(([name]) => name) };
1756
- const [project, config] = candidates[0];
1757
- return found(project, config);
2883
+ const last = properties[properties.length - 1];
2884
+ const end = last.offset + last.length;
2885
+ if (!isMultiline(raw, object)) {
2886
+ return splice(raw, end, end, `, ${JSON.stringify(String(key))}: ${renderValue(value, "", style)}`);
2887
+ }
2888
+ const indent = lineIndentAt(raw, last.offset);
2889
+ return splice(raw, end, end, `,${style.eol}${indent}${JSON.stringify(String(key))}: ${renderValue(value, indent, style)}`);
2890
+ }
2891
+ function setJsonValue(raw, path, value) {
2892
+ const style = styleOf(raw);
2893
+ const root = parseTree2(raw);
2894
+ if (root === void 0) throw new Error("setJsonValue: not a JSON document");
2895
+ const existing = findNodeAtLocation2(root, path);
2896
+ if (existing !== void 0) {
2897
+ return splice(raw, existing.offset, existing.offset + existing.length, renderValue(value, lineIndentAt(raw, existing.offset), style));
2898
+ }
2899
+ for (let depth = path.length - 1; depth >= 0; depth--) {
2900
+ const parent = depth === 0 ? root : findNodeAtLocation2(root, path.slice(0, depth));
2901
+ if (parent === void 0) continue;
2902
+ if (parent.type !== "object") throw new Error(`setJsonValue: ${path.slice(0, depth).join(".") || "the root"} is not an object`);
2903
+ return insertProperty(raw, parent, path[depth], nest(path.slice(depth + 1), value), style);
2904
+ }
2905
+ throw new Error("setJsonValue: unreachable");
2906
+ }
2907
+ function appendJsonArrayItem(raw, path, item) {
2908
+ const style = styleOf(raw);
2909
+ const root = parseTree2(raw);
2910
+ const array = root === void 0 ? void 0 : findNodeAtLocation2(root, path);
2911
+ if (array === void 0) return setJsonValue(raw, path, [item]);
2912
+ if (array.type !== "array") throw new Error(`appendJsonArrayItem: ${path.join(".")} is not an array`);
2913
+ const items = array.children ?? [];
2914
+ if (items.length === 0) return splice(raw, array.offset, array.offset + array.length, renderValue([item], "", style));
2915
+ const last = items[items.length - 1];
2916
+ const end = last.offset + last.length;
2917
+ if (!isMultiline(raw, array)) {
2918
+ const separator = items.length >= 2 ? raw.slice(items[0].offset + items[0].length, items[1].offset) : ", ";
2919
+ return splice(raw, end, end, `${separator}${renderValue(item, "", style)}`);
2920
+ }
2921
+ const indent = lineIndentAt(raw, last.offset);
2922
+ return splice(raw, end, end, `,${style.eol}${indent}${renderValue(item, indent, style)}`);
2923
+ }
2924
+ function applyJsonEdits(raw, edits) {
2925
+ if (raw === null) {
2926
+ let text = "{}\n";
2927
+ for (const [path, value] of edits) text = setJsonValue(text, path, value);
2928
+ return text;
2929
+ }
2930
+ return edits.reduce((text, [path, value]) => setJsonValue(text, path, value), raw);
1758
2931
  }
1759
2932
 
1760
2933
  // src/generators/shared/toolkit-config-file.ts
1761
2934
  var TOOLKIT_CONFIG_PATH = "toolkit.config.json";
1762
- function detectIndent(raw) {
1763
- for (const line of raw.split("\n")) {
1764
- const match = /^(\t+| +)/.exec(line);
1765
- if (match) return match[1];
1766
- }
1767
- return " ";
1768
- }
1769
2935
  function readToolkitConfigFile(tree) {
1770
2936
  const raw = tree.read(TOOLKIT_CONFIG_PATH, "utf-8");
1771
2937
  if (raw === null) return { raw: null, parsed: null };
@@ -1791,18 +2957,18 @@ function recordedChoice(config, key, allowed) {
1791
2957
  const value = config?.[key];
1792
2958
  return typeof value === "string" && allowed.includes(value) ? value : void 0;
1793
2959
  }
1794
- var RECORDED_FRONTENDS = ["react-vite", "next", "none"];
2960
+ var RECORDED_FRONTENDS = ["react-vite", "next", "expo", "generic", "none"];
1795
2961
  var RECORDED_BACKENDS = ["express", "nest", "none"];
1796
2962
  var RECORDED_PRESETS = ["quick-start", "solo", "team"];
1797
2963
  var RECORDED_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
1798
2964
  function resolveFrontendOption(option, config, generator) {
1799
2965
  const frontend = option ?? recordedChoice(config, "frontend", RECORDED_FRONTENDS);
1800
2966
  if (frontend === void 0) {
1801
- throw new Error(`${generator}: pass --frontend=react-vite|next (toolkit.config.json records none \u2014 init writes it).`);
2967
+ throw new Error(`${generator}: pass --frontend=react-vite|next|expo|generic (toolkit.config.json records none \u2014 init writes it).`);
1802
2968
  }
1803
2969
  if (frontend === "none") {
1804
2970
  throw new Error(
1805
- `${generator}: this is a backend-only workspace (toolkit.config.json frontend: none) \u2014 FSD structures a frontend app. Pass --frontend=react-vite|next if you added one.`
2971
+ `${generator}: this is a backend-only workspace (toolkit.config.json frontend: none) \u2014 FSD structures a frontend app. Pass --frontend=react-vite|next|expo|generic if you added one.`
1806
2972
  );
1807
2973
  }
1808
2974
  return frontend;
@@ -1810,7 +2976,7 @@ function resolveFrontendOption(option, config, generator) {
1810
2976
  function resolveInitFrontend(option, config) {
1811
2977
  const frontend = option ?? recordedChoice(config, "frontend", RECORDED_FRONTENDS);
1812
2978
  if (frontend === void 0) {
1813
- throw new Error("init: pass --frontend=react-vite|next|none (toolkit.config.json records none \u2014 init writes it).");
2979
+ throw new Error("init: pass --frontend=react-vite|next|expo|generic|none (toolkit.config.json records none \u2014 init writes it).");
1814
2980
  }
1815
2981
  return frontend;
1816
2982
  }
@@ -1826,21 +2992,123 @@ function resolveAiHostsOption(option, config) {
1826
2992
  const fromConfig = Array.isArray(recorded) ? [...new Set(recorded.filter(isAiHost))] : [];
1827
2993
  return fromConfig.length > 0 ? fromConfig : [...DEFAULT_AI_HOSTS];
1828
2994
  }
1829
- function recordAiHosts(tree, hosts, write) {
1830
- const { raw, parsed } = readToolkitConfigFile(tree);
1831
- if (raw !== null && parsed === null) return "unrecognized";
1832
- const existing = parsed ?? {};
1833
- const recorded = existing["aiHosts"];
1834
- if (Array.isArray(recorded) && recorded.length === hosts.length && recorded.every((id, index) => id === hosts[index])) {
1835
- return "unchanged";
2995
+ function recordAiHosts(tree, hosts, write) {
2996
+ const { raw, parsed } = readToolkitConfigFile(tree);
2997
+ if (raw !== null && parsed === null) return "unrecognized";
2998
+ const existing = parsed ?? {};
2999
+ const recorded = existing["aiHosts"];
3000
+ if (Array.isArray(recorded) && recorded.length === hosts.length && recorded.every((id, index) => id === hosts[index])) {
3001
+ return "unchanged";
3002
+ }
3003
+ if (write) tree.write(TOOLKIT_CONFIG_PATH, applyJsonEdits(raw, [[["aiHosts"], [...hosts]]]));
3004
+ return raw === null ? "created" : "updated";
3005
+ }
3006
+ function recordedFrontends(config) {
3007
+ const frontends = config?.["frontends"];
3008
+ if (!Array.isArray(frontends)) return [];
3009
+ return frontends.flatMap(
3010
+ (entry) => typeof entry === "object" && entry !== null && typeof entry["project"] === "string" ? [entry.project] : []
3011
+ );
3012
+ }
3013
+
3014
+ // src/generators/shared/project-type.ts
3015
+ import { parseJson } from "@nx/devkit";
3016
+ function resolveProjectType(tree, root, declared) {
3017
+ if (declared) return declared === "library" ? "library" : "application";
3018
+ const prefix = root === "" || root === "." ? "" : `${root}/`;
3019
+ if (tree.exists(`${prefix}tsconfig.lib.json`)) return "library";
3020
+ if (tree.exists(`${prefix}tsconfig.app.json`)) return "application";
3021
+ const raw = tree.read(`${prefix}package.json`, "utf-8");
3022
+ if (raw === null) return "library";
3023
+ try {
3024
+ const manifest = parseJson(raw);
3025
+ if (manifest && !["exports", "main", "module", "bin"].some((key) => Boolean(manifest[key]))) return "application";
3026
+ } catch {
3027
+ }
3028
+ return "library";
3029
+ }
3030
+
3031
+ // src/generators/shared/frontend-app.ts
3032
+ var baseNameOf = (root) => root.split("/").pop() ?? root;
3033
+ function isEndToEnd(root) {
3034
+ const dir = baseNameOf(root);
3035
+ return dir === "e2e" || dir.endsWith("-e2e");
3036
+ }
3037
+ function isBackend(root, config, backendTag) {
3038
+ return baseNameOf(root) === "api" || (config.tags ?? []).includes(backendTag);
3039
+ }
3040
+ function prefixOf(root) {
3041
+ return root === "" || root === "." ? "" : `${root}/`;
3042
+ }
3043
+ function sourceRootOf(tree, config) {
3044
+ const prefix = prefixOf(config.root);
3045
+ const declared = config.sourceRoot?.replace(/\/$/, "");
3046
+ const atRoot = declared !== void 0 && (declared === config.root || prefix === "" && (declared === "." || declared === ""));
3047
+ if (declared && !atRoot) return declared;
3048
+ if (atRoot && !tree.exists(`${prefix}src`)) return prefix === "" ? "." : config.root;
3049
+ return `${prefix}src`;
3050
+ }
3051
+ function frameworkOf(tree, config) {
3052
+ return detectFramework({
3053
+ exists: (path) => tree.exists(path),
3054
+ read: (path) => tree.read(path, "utf-8"),
3055
+ projectRoot: config.root,
3056
+ executors: Object.values(config.targets ?? {}).flatMap((target) => target.executor ? [target.executor] : [])
3057
+ });
3058
+ }
3059
+ function resolveFrontendApp(tree, explicitProject) {
3060
+ let projects;
3061
+ try {
3062
+ projects = getProjects(tree);
3063
+ } catch (error) {
3064
+ const message = error instanceof Error ? error.message : String(error);
3065
+ const pathMatch = /Cannot parse ([^:]+):/.exec(message);
3066
+ return { kind: "invalid-project-json", detail: pathMatch ? pathMatch[1] : message };
1836
3067
  }
1837
- if (write) {
1838
- const indent = raw === null ? " " : detectIndent(raw);
1839
- const body = JSON.stringify({ ...existing, aiHosts: [...hosts] }, null, indent);
1840
- tree.write(TOOLKIT_CONFIG_PATH, raw === null || raw.endsWith("\n") ? `${body}
1841
- ` : body);
3068
+ const found = (project2, config2) => {
3069
+ const profile = frameworkOf(tree, config2);
3070
+ return { kind: "found", project: project2, root: config2.root, sourceRoot: sourceRootOf(tree, config2), framework: profile.id, profile };
3071
+ };
3072
+ if (explicitProject !== void 0) {
3073
+ const config2 = projects.get(explicitProject);
3074
+ if (config2) return found(explicitProject, config2);
3075
+ const rootApp = inferredRootApp(tree, projects);
3076
+ return rootApp?.kind === "found" && rootApp.project === explicitProject ? rootApp : { kind: "not-found", project: explicitProject };
1842
3077
  }
1843
- return raw === null ? "created" : "updated";
3078
+ const backendTag = readBoundaryTags(readToolkitConfig(tree)).backend;
3079
+ const eligible = [...projects.entries()].filter(([, config2]) => !isEndToEnd(config2.root) && !isBackend(config2.root, config2, backendTag));
3080
+ const declared = eligible.filter(([, config2]) => config2.projectType === "application");
3081
+ const inferred = eligible.filter(([, config2]) => {
3082
+ if (config2.projectType !== void 0) return false;
3083
+ if (config2.root === "" || config2.root === ".") return frameworkOf(tree, config2).recognised;
3084
+ return resolveProjectType(tree, config2.root, void 0) === "application";
3085
+ });
3086
+ const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
3087
+ if (candidates.length === 0) {
3088
+ const rootApp = inferredRootApp(tree, projects);
3089
+ if (rootApp !== null) return rootApp;
3090
+ const otherProjects = [...projects.keys()].filter((name) => !isEndToEnd(projects.get(name)?.root ?? "")).sort();
3091
+ return { kind: "none", otherProjects };
3092
+ }
3093
+ if (candidates.length > 1) return { kind: "ambiguous", candidates: candidates.map(([name]) => name) };
3094
+ const [project, config] = candidates[0];
3095
+ return found(project, config);
3096
+ }
3097
+ function inferredRootApp(tree, projects) {
3098
+ if ([...projects.values()].some((config2) => config2.root === "" || config2.root === ".")) return null;
3099
+ const raw = tree.read("package.json", "utf-8");
3100
+ if (raw === null) return null;
3101
+ let name;
3102
+ try {
3103
+ name = JSON.parse(raw).name;
3104
+ } catch {
3105
+ return null;
3106
+ }
3107
+ if (typeof name !== "string" || name === "") return null;
3108
+ const config = { name, root: "." };
3109
+ const framework = frameworkOf(tree, config);
3110
+ if (!framework.recognised) return null;
3111
+ return { kind: "found", project: name, root: ".", sourceRoot: sourceRootOf(tree, config), framework: framework.id, profile: framework, inferredRoot: true };
1844
3112
  }
1845
3113
 
1846
3114
  // src/generators/add-fsd-lite/render.ts
@@ -1859,8 +3127,11 @@ function renderAddFsdLiteReport(report) {
1859
3127
  for (const name of refusal.candidates) lines.push(` - ${name}`);
1860
3128
  } else if (refusal.reason === "invalid-project-json") {
1861
3129
  lines.push(` a project.json could not be parsed (${refusal.detail}) \u2014 fix it, then rerun.`);
3130
+ } else if (refusal.otherProjects.length > 0) {
3131
+ lines.push(" no project was recognised as a frontend app. If one of these is it, rerun with --project=<name>:");
3132
+ for (const name of refusal.otherProjects) lines.push(` - ${name}`);
1862
3133
  } else {
1863
- lines.push(" the workspace has no frontend application \u2014 create one (e.g. nx g @nx/react:app) or pass --project.");
3134
+ lines.push(" the workspace has no projects \u2014 create one (e.g. nx g @nx/react:app or nx g @nx/next:app), then rerun.");
1864
3135
  }
1865
3136
  }
1866
3137
  return lines.join("\n");
@@ -1870,6 +3141,9 @@ function renderAddFsdLiteReport(report) {
1870
3141
  lines.push("");
1871
3142
  }
1872
3143
  lines.push(`FSD layers (in ${report.project}, ${report.sourceRoot}):`);
3144
+ if (report.layerGuide !== null && report.frontend === "generic") {
3145
+ lines.push("Frontend: framework not recognised \u2014 the common FSD rules apply, with no framework-specific layer names.");
3146
+ }
1873
3147
  if (report.layerGuide !== null) lines.push(report.layerGuide);
1874
3148
  lines.push("");
1875
3149
  const where = report.eslintPath ?? `${report.project}'s eslint.config.mjs`;
@@ -1921,26 +3195,31 @@ function runAddFsdLite(tree, options) {
1921
3195
  const frontend = resolveFrontendOption(options.frontend, config, "add-fsd-lite");
1922
3196
  const dryRun = options.dryRun ?? false;
1923
3197
  const applyEslint = options.applyEslint ?? false;
1924
- const snippet = buildFsdEslintPasteSnippet(enforcement, locale);
1925
3198
  const app = resolveFrontendApp(tree, options.project);
1926
3199
  if (app.kind !== "found") {
1927
- const appRefusal = app.kind === "not-found" ? { reason: "project-not-found", project: app.project } : app.kind === "ambiguous" ? { reason: "ambiguous-frontend", candidates: app.candidates } : app.kind === "invalid-project-json" ? { reason: "invalid-project-json", detail: app.detail } : { reason: "no-frontend-app" };
1928
- return { dryRun, aborted: true, appRefusal, project: null, sourceRoot: null, enforcement, eslint: "not-applied", eslintPath: null, snippet, layerGuide: null, steiger: null };
3200
+ const snippet2 = buildFsdEslintPasteSnippet(enforcement, locale);
3201
+ const appRefusal = app.kind === "not-found" ? { reason: "project-not-found", project: app.project } : app.kind === "ambiguous" ? { reason: "ambiguous-frontend", candidates: app.candidates } : app.kind === "invalid-project-json" ? { reason: "invalid-project-json", detail: app.detail } : { reason: "no-frontend-app", otherProjects: app.otherProjects };
3202
+ return { dryRun, aborted: true, appRefusal, project: null, sourceRoot: null, enforcement, frontend, eslint: "not-applied", eslintPath: null, snippet: snippet2, layerGuide: null, steiger: null };
1929
3203
  }
1930
3204
  const appDir = app.root === "" || app.root === "." ? "" : `${app.root}/`;
3205
+ const eslintRoot = posix.relative(app.root === "" ? "." : app.root, app.sourceRoot) || ".";
3206
+ const profile = app.framework === frontend ? app.profile : profileById(frontend);
3207
+ const { platformSuffixes, routerDirs } = profile;
3208
+ const framework = { platformSuffixes, routerDirs };
3209
+ const snippet = buildFsdEslintPasteSnippet(enforcement, locale, eslintRoot, framework);
1931
3210
  const eslintPath = ESLINT_CONFIG_CANDIDATES.map((name) => `${appDir}${name}`).find((path) => tree.exists(path)) ?? null;
1932
3211
  const configText = eslintPath === null ? null : tree.read(eslintPath, "utf-8");
1933
3212
  let eslint;
1934
3213
  if (!applyEslint) {
1935
- eslint = configText !== null && applyFsdEslintConfig(configText, enforcement).status === "already-applied" ? "already-applied" : "snippet-only";
3214
+ eslint = configText !== null && applyFsdEslintConfig(configText, enforcement, eslintRoot, framework).status === "already-applied" ? "already-applied" : "snippet-only";
1936
3215
  } else if (configText === null) {
1937
3216
  eslint = "degraded-unrecognized";
1938
3217
  } else {
1939
- const result = applyFsdEslintConfig(configText, enforcement);
3218
+ const result = applyFsdEslintConfig(configText, enforcement, eslintRoot, framework);
1940
3219
  eslint = result.status === "applied" ? "applied" : result.status === "already-applied" ? "already-applied" : "degraded-unrecognized";
1941
3220
  if (result.status === "applied" && !dryRun && eslintPath !== null) tree.write(eslintPath, result.text);
1942
3221
  }
1943
- const steiger = options.steiger ? addSteiger(tree, { appDir, appRoot: app.root, sourceRoot: app.sourceRoot, frontend, locale, write: !dryRun, config }) : null;
3222
+ const steiger = options.steiger ? addSteiger(tree, { appDir, appRoot: app.root, sourceRoot: app.sourceRoot, profile, locale, write: !dryRun, config }) : null;
1944
3223
  return {
1945
3224
  dryRun,
1946
3225
  aborted: false,
@@ -1949,10 +3228,11 @@ function runAddFsdLite(tree, options) {
1949
3228
  sourceRoot: app.sourceRoot,
1950
3229
  steiger,
1951
3230
  enforcement,
3231
+ frontend,
1952
3232
  eslint,
1953
3233
  eslintPath,
1954
3234
  snippet: eslint === "applied" || eslint === "already-applied" ? null : snippet,
1955
- layerGuide: composeFsdLayerGuide({ frontend, sourceRoot: app.sourceRoot, locale })
3235
+ layerGuide: composeFsdLayerGuide({ profile, sourceRoot: app.sourceRoot, locale })
1956
3236
  };
1957
3237
  }
1958
3238
  function readJsonObject(tree, path) {
@@ -1967,7 +3247,7 @@ function readJsonObject(tree, path) {
1967
3247
  }
1968
3248
  function addSteiger(tree, input) {
1969
3249
  const configPath = `${input.appDir}${STEIGER_CONFIG_FILE}`;
1970
- const configText = buildSteigerConfig({ frontend: input.frontend, locale: input.locale });
3250
+ const configText = buildSteigerConfig({ renamedLayerDirs: Object.values(input.profile.layerDirs), locale: input.locale });
1971
3251
  const existing = tree.read(configPath, "utf-8");
1972
3252
  let configAction;
1973
3253
  if (existing === null) {
@@ -1994,8 +3274,8 @@ function addSteiger(tree, input) {
1994
3274
  } else {
1995
3275
  targets[FSD_CHECK_TARGET] = target;
1996
3276
  targetAction = "added";
1997
- if (input.write) tree.write(candidate, `${JSON.stringify(file.parsed, null, detectIndent(file.raw))}
1998
- `);
3277
+ const path = candidate.endsWith("project.json") ? ["targets", FSD_CHECK_TARGET] : ["nx", "targets", FSD_CHECK_TARGET];
3278
+ if (input.write) tree.write(candidate, setJsonValue(file.raw, path, target));
1999
3279
  }
2000
3280
  break;
2001
3281
  }
@@ -2010,8 +3290,8 @@ function addSteiger(tree, input) {
2010
3290
  } else {
2011
3291
  for (const [name, range] of missing) devDependencies[name] = range;
2012
3292
  dependenciesAction = "added";
2013
- if (input.write) tree.write("package.json", `${JSON.stringify(rootPackage.parsed, null, detectIndent(rootPackage.raw))}
2014
- `);
3293
+ const edited = applyJsonEdits(rootPackage.raw, missing.map(([name, range]) => [["devDependencies", name], range]));
3294
+ if (input.write) tree.write("package.json", edited);
2015
3295
  }
2016
3296
  }
2017
3297
  const pm = treePackageManager(tree, input.config) ?? "pnpm";
@@ -2033,7 +3313,8 @@ var ACTION_LINE = {
2033
3313
  appended: (result) => `${result.filePath}: nx-archweave block added after the existing content (nothing else changed)`,
2034
3314
  updated: (result) => `${result.filePath}: nx-archweave block updated (nothing outside it changed)`,
2035
3315
  unchanged: (result) => `${result.filePath}: unchanged`,
2036
- unrecognized: (result) => `${result.filePath}: not written \u2014 ${result.reason ?? "the nx-archweave markers could not be placed"}`
3316
+ unrecognized: (result) => `${result.filePath}: not written \u2014 ${result.reason ?? "the nx-archweave markers could not be placed"}`,
3317
+ "user-modified": (result) => `${result.filePath}: not written \u2014 the nx-archweave block was edited by hand, so it is kept`
2037
3318
  };
2038
3319
  function renderAddAgentRulesReport(report) {
2039
3320
  const lines = [];
@@ -2044,6 +3325,11 @@ function renderAddAgentRulesReport(report) {
2044
3325
  lines.push("");
2045
3326
  for (const file of report.files) {
2046
3327
  lines.push(` ${ACTION_LINE[file.action](file)}`);
3328
+ if (file.action === "user-modified") {
3329
+ lines.push(" What this run would write instead (your block \u2192 generated):");
3330
+ for (const line of (file.diff ?? "").split("\n")) lines.push(` ${line}`);
3331
+ lines.push(" Rerun with --overwriteBlock to replace the block with the generated one.");
3332
+ }
2047
3333
  }
2048
3334
  const RECORD_LINE = {
2049
3335
  created: "toolkit.config.json: aiHosts recorded (file created)",
@@ -2057,7 +3343,7 @@ function renderAddAgentRulesReport(report) {
2057
3343
  if (recordLine !== null) lines.push(` ${recordLine}`);
2058
3344
  if (report.files.some((file) => file.action === "unrecognized")) {
2059
3345
  lines.push("");
2060
- lines.push('Manual fix needed: keep exactly one "<!-- nx-archweave:agent-rules start -->" \u2026 "<!-- nx-archweave:agent-rules end -->"');
3346
+ lines.push('Manual fix needed: keep exactly one "<!-- nx-archweave:agent-rules start \u2026 -->" \u2026 "<!-- nx-archweave:agent-rules end -->"');
2061
3347
  lines.push("pair in the file (or remove both markers), then rerun add-agent-rules.");
2062
3348
  }
2063
3349
  if (report.dryRun) {
@@ -2073,6 +3359,158 @@ function renderAddAgentRulesReport(report) {
2073
3359
  return lines.join("\n");
2074
3360
  }
2075
3361
 
3362
+ // src/generators/add-review/render.ts
3363
+ var DEPENDABOT_PATH = ".github/dependabot.yml";
3364
+ function renderLineDiff(existing, generated) {
3365
+ const a = existing.split("\n");
3366
+ const b = generated.split("\n");
3367
+ const m = a.length;
3368
+ const n = b.length;
3369
+ const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
3370
+ for (let i2 = m - 1; i2 >= 0; i2--) {
3371
+ for (let j2 = n - 1; j2 >= 0; j2--) {
3372
+ lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
3373
+ }
3374
+ }
3375
+ const out = ["--- existing", "+++ generated"];
3376
+ let i = 0;
3377
+ let j = 0;
3378
+ while (i < m && j < n) {
3379
+ if (a[i] === b[j]) {
3380
+ out.push(` ${a[i]}`);
3381
+ i++;
3382
+ j++;
3383
+ } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
3384
+ out.push(`- ${a[i]}`);
3385
+ i++;
3386
+ } else {
3387
+ out.push(`+ ${b[j]}`);
3388
+ j++;
3389
+ }
3390
+ }
3391
+ while (i < m) out.push(`- ${a[i++]}`);
3392
+ while (j < n) out.push(`+ ${b[j++]}`);
3393
+ return out.join("\n");
3394
+ }
3395
+ var WORKFLOW_ACTION_LINES = {
3396
+ created: "created",
3397
+ overwritten: "overwritten with the generated version (per --onConflict=overwrite)",
3398
+ unchanged: "unchanged (already matches the generated version)",
3399
+ proposed: "NOT written \u2014 differs from the generated version (merge proposal below)",
3400
+ skipped: "left as-is (per --onConflict=skip)",
3401
+ alternate: "generated alongside the existing file",
3402
+ aborted: "NOT written"
3403
+ };
3404
+ var CONFIG_ACTION_LINES = {
3405
+ created: "created with the review field",
3406
+ updated: "review field updated",
3407
+ unchanged: "review field unchanged",
3408
+ "not-updated": "NOT updated \u2014 kept consistent with the workflow file that is actually in effect",
3409
+ aborted: "NOT updated"
3410
+ };
3411
+ function renderAddReviewReport(report) {
3412
+ const lines = [];
3413
+ if (report.packageManagerNote !== void 0) {
3414
+ lines.push(`Note: ${report.packageManagerNote}`);
3415
+ }
3416
+ if (report.workflowAction === "aborted") {
3417
+ lines.push("add-review aborted \u2014 nothing was written.");
3418
+ lines.push(` toolkit.config.json: ${report.configError ?? "unusable"}`);
3419
+ lines.push(" Fix the config file manually, then rerun the generator.");
3420
+ return lines.join("\n");
3421
+ }
3422
+ if (report.dryRun) {
3423
+ lines.push("add-review dry-run \u2014 no files were written.");
3424
+ lines.push("");
3425
+ lines.push("Generated workflow YAML:");
3426
+ lines.push(report.workflowYaml ?? "");
3427
+ lines.push("");
3428
+ lines.push("toolkit.config.json review field:");
3429
+ lines.push(report.configSnippetJson ?? "");
3430
+ lines.push("");
3431
+ lines.push("Effective decision:");
3432
+ }
3433
+ lines.push(` ${report.workflowPath}: ${WORKFLOW_ACTION_LINES[report.workflowAction]}`);
3434
+ lines.push(` toolkit.config.json: ${CONFIG_ACTION_LINES[report.configAction]}`);
3435
+ if (report.workflowAction === "proposed" && report.diff) {
3436
+ lines.push("");
3437
+ lines.push(report.diff);
3438
+ lines.push("");
3439
+ lines.push("To resolve, rerun with one of:");
3440
+ lines.push(" --onConflict=overwrite replace the existing file with the generated version");
3441
+ lines.push(" --onConflict=skip keep the existing file");
3442
+ lines.push(" --onConflict=alternate-filename write the generated version alongside it");
3443
+ }
3444
+ if (report.dependabot === "created") {
3445
+ lines.push(` ${DEPENDABOT_PATH}: created \u2014 Dependabot proposes newer pins for the workflow's actions`);
3446
+ } else if (report.dependabot === "present") {
3447
+ lines.push(` ${DEPENDABOT_PATH}: already updates github-actions`);
3448
+ } else if (report.dependabot === "proposed") {
3449
+ lines.push(` ${DEPENDABOT_PATH}: NOT changed \u2014 it has no github-actions entry. The workflow pins each action`);
3450
+ lines.push(" to a commit SHA; add this entry under `updates:` so those pins get update pull requests:");
3451
+ lines.push("");
3452
+ lines.push(report.dependabotProposal ?? "");
3453
+ }
3454
+ if (report.workflowAction === "alternate") {
3455
+ lines.push("");
3456
+ lines.push(" Review the generated file, then replace the main workflow file with it yourself;");
3457
+ lines.push(" the config stays pointing at the workflow that is actually in effect until you do.");
3458
+ }
3459
+ lines.push("");
3460
+ lines.push(`Note: the review workflow stays green-skipped until you register the ${report.requiredSecretName} secret.`);
3461
+ lines.push("Note: this workflow runs with elevated permissions only after it is merged to the default branch \u2014");
3462
+ lines.push("merging is the first real validation.");
3463
+ return lines.join("\n");
3464
+ }
3465
+
3466
+ // src/eslint/fsd-policy.ts
3467
+ var PRESETS = {
3468
+ basic: {
3469
+ sliceGroups: { auto: true, include: [], exclude: [] },
3470
+ sharedPublicApi: "segment",
3471
+ testFiles: false,
3472
+ segments: [],
3473
+ platformSuffixes: [],
3474
+ routerDirs: []
3475
+ },
3476
+ practical: {
3477
+ sliceGroups: { auto: true, include: [], exclude: [] },
3478
+ sharedPublicApi: "file",
3479
+ testFiles: ["**/*.{spec,test}.*", "**/__tests__/**"],
3480
+ segments: [],
3481
+ platformSuffixes: [],
3482
+ routerDirs: []
3483
+ }
3484
+ };
3485
+ var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
3486
+ function resolveFsdPolicy(recorded, overrides = {}) {
3487
+ const fsd = typeof recorded === "object" && recorded !== null ? recorded : {};
3488
+ const preset = fsd["preset"] === "practical" ? "practical" : "basic";
3489
+ const policy = { ...PRESETS[preset] };
3490
+ const groups = fsd["sliceGroups"];
3491
+ if (groups === false) policy.sliceGroups = false;
3492
+ else if (groups === "auto") policy.sliceGroups = { auto: true, include: [], exclude: [] };
3493
+ else if (typeof groups === "object" && groups !== null) {
3494
+ const value = groups;
3495
+ policy.sliceGroups = {
3496
+ auto: value["auto"] !== false,
3497
+ include: isStringArray(value["include"]) ? value["include"] : [],
3498
+ exclude: isStringArray(value["exclude"]) ? value["exclude"] : []
3499
+ };
3500
+ }
3501
+ const shared = fsd["sharedPublicApi"];
3502
+ if (shared === "segment" || shared === "component" || shared === "file") policy.sharedPublicApi = shared;
3503
+ const tests = fsd["testFiles"];
3504
+ if (tests === false || isStringArray(tests)) policy.testFiles = tests;
3505
+ if (isStringArray(fsd["segments"])) policy.segments = fsd["segments"];
3506
+ if (isStringArray(fsd["platformSuffixes"])) policy.platformSuffixes = fsd["platformSuffixes"];
3507
+ if (isStringArray(fsd["routerDirs"])) policy.routerDirs = fsd["routerDirs"];
3508
+ for (const [key, value] of Object.entries(overrides)) {
3509
+ if (value !== void 0) policy[key] = value;
3510
+ }
3511
+ return policy;
3512
+ }
3513
+
2076
3514
  // src/generators/add-agent-rules/generator.ts
2077
3515
  function resolveTeamRules(option, config) {
2078
3516
  if (option !== void 0) return option;
@@ -2087,6 +3525,13 @@ function resolveFsdStructure(config) {
2087
3525
  const frontend = config?.["frontend"];
2088
3526
  return typeof frontend === "string" && frontend !== "none";
2089
3527
  }
3528
+ var POLICY_KEYS = ["preset", "sliceGroups", "sharedPublicApi", "testFiles"];
3529
+ function resolveFsdPolicyNotes(config) {
3530
+ const fsd = config?.["fsd"];
3531
+ if (typeof fsd !== "object" || fsd === null || !POLICY_KEYS.some((key) => key in fsd)) return void 0;
3532
+ const policy = resolveFsdPolicy(fsd);
3533
+ return { sliceGroups: policy.sliceGroups !== false, sharedPublicApi: policy.sharedPublicApi, testFiles: policy.testFiles !== false };
3534
+ }
2090
3535
  function runAddAgentRules(tree, options, { recordHosts = true, fsdStructure } = {}) {
2091
3536
  const config = readToolkitConfig(tree);
2092
3537
  const hosts = resolveAiHostsOption(options.hosts, config);
@@ -2094,7 +3539,7 @@ function runAddAgentRules(tree, options, { recordHosts = true, fsdStructure } =
2094
3539
  const locale = resolveLocaleOption(options.locale, config);
2095
3540
  const teamRules = resolveTeamRules(options.teamRules, config);
2096
3541
  const dryRun = options.dryRun ?? false;
2097
- const composed = composeAgentRules({ files, locale, teamRules, fsdStructure: fsdStructure ?? resolveFsdStructure(config) });
3542
+ const composed = composeAgentRules({ files, locale, teamRules, fsdStructure: fsdStructure ?? resolveFsdStructure(config), fsdPolicy: resolveFsdPolicyNotes(config) });
2098
3543
  const results = composed.map(({ filePath, content }) => {
2099
3544
  if (!validateAgentRules(content, teamRules ? ["team-rules"] : [])) {
2100
3545
  throw new Error(
@@ -2102,12 +3547,20 @@ function runAddAgentRules(tree, options, { recordHosts = true, fsdStructure } =
2102
3547
  );
2103
3548
  }
2104
3549
  const existing = tree.read(filePath, "utf-8");
2105
- const outcome = applyAgentRulesBlock(existing, content);
3550
+ const outcome = applyAgentRulesBlock(existing, content, { overwrite: options.overwriteBlock === true });
2106
3551
  switch (outcome.kind) {
2107
3552
  case "unchanged":
2108
3553
  return { filePath, action: "unchanged", reason: null, content: null };
2109
3554
  case "unrecognized":
2110
3555
  return { filePath, action: "unrecognized", reason: outcome.reason, content: null };
3556
+ case "user-modified":
3557
+ return {
3558
+ filePath,
3559
+ action: "user-modified",
3560
+ reason: null,
3561
+ content: null,
3562
+ diff: renderLineDiff(outcome.existingBody, outcome.generatedBody)
3563
+ };
2111
3564
  case "create":
2112
3565
  case "append":
2113
3566
  case "replace": {
@@ -2203,99 +3656,6 @@ function runAddGithubTemplates(tree, options) {
2203
3656
  return { dryRun, files };
2204
3657
  }
2205
3658
 
2206
- // src/generators/add-review/render.ts
2207
- function renderLineDiff(existing, generated) {
2208
- const a = existing.split("\n");
2209
- const b = generated.split("\n");
2210
- const m = a.length;
2211
- const n = b.length;
2212
- const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
2213
- for (let i2 = m - 1; i2 >= 0; i2--) {
2214
- for (let j2 = n - 1; j2 >= 0; j2--) {
2215
- lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
2216
- }
2217
- }
2218
- const out = ["--- existing", "+++ generated"];
2219
- let i = 0;
2220
- let j = 0;
2221
- while (i < m && j < n) {
2222
- if (a[i] === b[j]) {
2223
- out.push(` ${a[i]}`);
2224
- i++;
2225
- j++;
2226
- } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
2227
- out.push(`- ${a[i]}`);
2228
- i++;
2229
- } else {
2230
- out.push(`+ ${b[j]}`);
2231
- j++;
2232
- }
2233
- }
2234
- while (i < m) out.push(`- ${a[i++]}`);
2235
- while (j < n) out.push(`+ ${b[j++]}`);
2236
- return out.join("\n");
2237
- }
2238
- var WORKFLOW_ACTION_LINES = {
2239
- created: "created",
2240
- overwritten: "overwritten with the generated version (per --onConflict=overwrite)",
2241
- unchanged: "unchanged (already matches the generated version)",
2242
- proposed: "NOT written \u2014 differs from the generated version (merge proposal below)",
2243
- skipped: "left as-is (per --onConflict=skip)",
2244
- alternate: "generated alongside the existing file",
2245
- aborted: "NOT written"
2246
- };
2247
- var CONFIG_ACTION_LINES = {
2248
- created: "created with the review field",
2249
- updated: "review field updated",
2250
- unchanged: "review field unchanged",
2251
- "not-updated": "NOT updated \u2014 kept consistent with the workflow file that is actually in effect",
2252
- aborted: "NOT updated"
2253
- };
2254
- function renderAddReviewReport(report) {
2255
- const lines = [];
2256
- if (report.packageManagerNote !== void 0) {
2257
- lines.push(`Note: ${report.packageManagerNote}`);
2258
- }
2259
- if (report.workflowAction === "aborted") {
2260
- lines.push("add-review aborted \u2014 nothing was written.");
2261
- lines.push(` toolkit.config.json: ${report.configError ?? "unusable"}`);
2262
- lines.push(" Fix the config file manually, then rerun the generator.");
2263
- return lines.join("\n");
2264
- }
2265
- if (report.dryRun) {
2266
- lines.push("add-review dry-run \u2014 no files were written.");
2267
- lines.push("");
2268
- lines.push("Generated workflow YAML:");
2269
- lines.push(report.workflowYaml ?? "");
2270
- lines.push("");
2271
- lines.push("toolkit.config.json review field:");
2272
- lines.push(report.configSnippetJson ?? "");
2273
- lines.push("");
2274
- lines.push("Effective decision:");
2275
- }
2276
- lines.push(` ${report.workflowPath}: ${WORKFLOW_ACTION_LINES[report.workflowAction]}`);
2277
- lines.push(` toolkit.config.json: ${CONFIG_ACTION_LINES[report.configAction]}`);
2278
- if (report.workflowAction === "proposed" && report.diff) {
2279
- lines.push("");
2280
- lines.push(report.diff);
2281
- lines.push("");
2282
- lines.push("To resolve, rerun with one of:");
2283
- lines.push(" --onConflict=overwrite replace the existing file with the generated version");
2284
- lines.push(" --onConflict=skip keep the existing file");
2285
- lines.push(" --onConflict=alternate-filename write the generated version alongside it");
2286
- }
2287
- if (report.workflowAction === "alternate") {
2288
- lines.push("");
2289
- lines.push(" Review the generated file, then replace the main workflow file with it yourself;");
2290
- lines.push(" the config stays pointing at the workflow that is actually in effect until you do.");
2291
- }
2292
- lines.push("");
2293
- lines.push(`Note: the review workflow stays green-skipped until you register the ${report.requiredSecretName} secret.`);
2294
- lines.push("Note: this workflow runs with elevated permissions only after it is merged to the default branch \u2014");
2295
- lines.push("merging is the first real validation.");
2296
- return lines.join("\n");
2297
- }
2298
-
2299
3659
  // src/generators/add-review/generator.ts
2300
3660
  var CONFIG_PATH = "toolkit.config.json";
2301
3661
  function deriveAltWorkflowPath(mainWorkflowPath) {
@@ -2369,7 +3729,9 @@ function runAddReview(tree, options) {
2369
3729
  diff: null,
2370
3730
  workflowYaml: null,
2371
3731
  configSnippetJson: null,
2372
- configError: config.error
3732
+ configError: config.error,
3733
+ dependabot: "not-applied",
3734
+ dependabotProposal: null
2373
3735
  };
2374
3736
  }
2375
3737
  const existing = tree.read(plan.workflowFilePath, "utf-8");
@@ -2421,17 +3783,17 @@ function runAddReview(tree, options) {
2421
3783
  configAction = "unchanged";
2422
3784
  } else {
2423
3785
  configAction = config.raw === null ? "created" : "updated";
2424
- const indent = config.raw === null ? " " : detectIndent(config.raw);
2425
- const trailingNewline = config.raw === null || config.raw.endsWith("\n");
2426
- const body = JSON.stringify({ ...config.parsed ?? {}, review: plan.configSnippet }, null, indent);
2427
- newConfigContent = trailingNewline ? `${body}
2428
- ` : body;
3786
+ newConfigContent = applyJsonEdits(config.raw, [[["review"], plan.configSnippet]]);
2429
3787
  }
2430
3788
  }
3789
+ const [dependabotFile] = renderTemplateFiles(generatorFilesDir("add-review", import.meta.url), ".github", {});
3790
+ const existingDependabot = tree.read(DEPENDABOT_PATH, "utf-8");
3791
+ const dependabot = existingDependabot === null ? "created" : /github-actions/.test(existingDependabot) ? "present" : "proposed";
2431
3792
  const dryRun = options.dryRun ?? false;
2432
3793
  if (!dryRun) {
2433
3794
  if (writeWorkflowTo !== null) tree.write(writeWorkflowTo, plan.workflowYaml);
2434
3795
  if (newConfigContent !== null) tree.write(CONFIG_PATH, newConfigContent);
3796
+ if (dependabot === "created") tree.write(DEPENDABOT_PATH, dependabotFile.content);
2435
3797
  }
2436
3798
  return {
2437
3799
  dryRun,
@@ -2443,6 +3805,8 @@ function runAddReview(tree, options) {
2443
3805
  workflowYaml: dryRun ? plan.workflowYaml : null,
2444
3806
  configSnippetJson: dryRun ? JSON.stringify(plan.configSnippet, null, 2) : null,
2445
3807
  configError: null,
3808
+ dependabot,
3809
+ dependabotProposal: dependabot === "proposed" ? dependabotFile.content : null,
2446
3810
  ...packageManagerNote !== void 0 ? { packageManagerNote } : {}
2447
3811
  };
2448
3812
  }
@@ -2492,8 +3856,9 @@ function renderAddBackendBody(report) {
2492
3856
  }
2493
3857
  if (report.packageManagerNote !== void 0) lines.push(` note: ${report.packageManagerNote}`);
2494
3858
  lines.push(` frontend project: ${report.frontendProject ?? "none (backend-only workspace)"}`);
3859
+ const libPath = report.sharedTypesRoot ?? "libs/shared-types";
2495
3860
  lines.push(
2496
- report.sharedTypes === "created" ? ` libs/shared-types: created (import it as ${report.sharedTypesPackage})` : " libs/shared-types: already exists \u2014 skipped"
3861
+ report.sharedTypes === "created" ? ` ${libPath}: created (import it as ${report.sharedTypesPackage})` : report.sharedTypes === "disabled" ? " shared-types lib: none (sharedTypes: false in toolkit.config.json)" : ` ${libPath}: already exists \u2014 skipped`
2497
3862
  );
2498
3863
  if (report.config !== void 0 && report.config !== "unchanged") {
2499
3864
  lines.push(` toolkit.config.json: backend "${report.backend}" recorded (${report.config}) \u2014 doctor's backend checks now run`);
@@ -2503,11 +3868,11 @@ function renderAddBackendBody(report) {
2503
3868
  const registry = packageManagerCommands(pmName).workspaceRegistration;
2504
3869
  lines.push(
2505
3870
  {
2506
- added: ` ${registry}: 'libs/shared-types' registered \u2014 run \`${pmName} install\` once to link it`,
2507
- present: ` ${registry}: already covers libs/shared-types`,
3871
+ added: ` ${registry}: '${libPath}' registered \u2014 run \`${pmName} install\` once to link it`,
3872
+ present: ` ${registry}: already covers ${libPath}`,
2508
3873
  unrecognized: (
2509
3874
  // an integrated layout lists no workspace packages: the paths alias below links it
2510
- report.tsconfigPaths === "added" || report.tsconfigPaths === "present" ? ` ${registry}: no workspace package list (integrated layout) \u2014 the tsconfig paths alias links libs/shared-types` : ` ${registry}: shape not recognized \u2014 add 'libs/shared-types' to its workspace list yourself`
3875
+ report.tsconfigPaths === "added" || report.tsconfigPaths === "present" ? ` ${registry}: no workspace package list (integrated layout) \u2014 the tsconfig paths alias links ${libPath}` : ` ${registry}: shape not recognized \u2014 add '${libPath}' to its workspace list yourself`
2511
3876
  )
2512
3877
  }[report.workspaceGlob]
2513
3878
  );
@@ -2520,7 +3885,8 @@ function renderAddBackendBody(report) {
2520
3885
  added: ` tsconfig.base.json paths: ${report.sharedTypesPackage} added (a classic or integrated setup \u2014 libs resolve through paths)`,
2521
3886
  present: " tsconfig.base.json paths: already wired",
2522
3887
  "workspace-link": " tsconfig paths: not needed \u2014 the workspace package link is the wiring",
2523
- unrecognized: " tsconfig.base.json: could not be parsed \u2014 left untouched"
3888
+ unrecognized: " tsconfig.base.json: could not be parsed \u2014 left untouched",
3889
+ none: " tsconfig paths: nothing to wire (no shared-types lib)"
2524
3890
  }[report.tsconfigPaths]
2525
3891
  );
2526
3892
  switch (report.eslint) {
@@ -2547,7 +3913,6 @@ function renderAddBackendBody(report) {
2547
3913
  }
2548
3914
 
2549
3915
  // src/generators/add-backend/generator.ts
2550
- var SHARED_TYPES_ROOT = "libs/shared-types";
2551
3916
  function runAddBackend(tree, options) {
2552
3917
  const backend = options.backend;
2553
3918
  const apiName = options.apiName ?? "api";
@@ -2622,8 +3987,9 @@ function runAddBackend(tree, options) {
2622
3987
  } catch {
2623
3988
  }
2624
3989
  }
2625
- const frontendParent = (projects.get(frontendProject)?.root ?? "").split("/").slice(0, -1).join("/");
2626
- const apiDirectory = `${frontendParent === "" || frontendParent === "." ? "apps" : frontendParent}/${apiName}`;
3990
+ const frontendRoot = projects.get(frontendProject)?.root ?? "";
3991
+ const frontendParent = frontendRoot.split("/").slice(0, -1).join("/");
3992
+ const apiDirectory = frontendRoot === "" || frontendRoot === "." ? `apps/${apiName}` : `${frontendParent === "" ? "" : `${frontendParent}/`}${apiName}`;
2627
3993
  const expressAlignment = backend === "express" && !expressDeclared ? readExpressPeerRange(tree) : void 0;
2628
3994
  const quotedExpress = expressAlignment ? `"express@${expressAlignment}"` : void 0;
2629
3995
  return {
@@ -2639,16 +4005,21 @@ function runAddBackend(tree, options) {
2639
4005
  ...refusalShape
2640
4006
  };
2641
4007
  }
2642
- const sharedTypesExists = tree.exists(`${SHARED_TYPES_ROOT}/package.json`) || tree.exists(`${SHARED_TYPES_ROOT}/project.json`);
4008
+ const recordedConfig = readToolkitConfigFile(tree).parsed;
4009
+ const boundaryTags = readBoundaryTags(recordedConfig);
4010
+ const sharedTypesConfig = readSharedTypesConfig(recordedConfig);
4011
+ const withSharedTypes = sharedTypesConfig !== false;
4012
+ const sharedTypesRoot = sharedTypesConfig === false ? "" : sharedTypesConfig.path;
4013
+ const sharedTypesExists = withSharedTypes && (tree.exists(`${sharedTypesRoot}/package.json`) || tree.exists(`${sharedTypesRoot}/project.json`));
2643
4014
  const rootPackageJson = tree.read("package.json", "utf-8");
2644
4015
  const workspaceName = rootPackageJson ? JSON.parse(rootPackageJson).name ?? "workspace" : "workspace";
2645
- const wiring = composeBackendWiring({ workspaceName });
4016
+ const wiring = composeBackendWiring({ workspaceName, tags: boundaryTags, ...sharedTypesConfig === false ? {} : { sharedTypes: sharedTypesConfig } });
2646
4017
  const [apiProjectName, apiConfig] = apiEntry;
2647
4018
  const tagTargets = [
2648
4019
  ...frontendProject === void 0 ? [] : [{ project: frontendProject, root: projects.get(frontendProject)?.root ?? `apps/${frontendProject}` }],
2649
4020
  { project: apiProjectName, root: apiConfig.root }
2650
4021
  ];
2651
- if (sharedTypesExists) tagTargets.push({ project: wiring.sharedTypesPackageName, root: SHARED_TYPES_ROOT });
4022
+ if (sharedTypesExists) tagTargets.push({ project: wiring.sharedTypesPackageName, root: sharedTypesRoot });
2652
4023
  const parsedTargets = /* @__PURE__ */ new Map();
2653
4024
  const invalidProjects = [];
2654
4025
  for (const target of tagTargets) {
@@ -2670,7 +4041,7 @@ function runAddBackend(tree, options) {
2670
4041
  if (invalidProjects.length > 0) {
2671
4042
  return { ...base, refused: "invalid-project-json", frontendProject, invalidProjects, ...refusalShape };
2672
4043
  }
2673
- const sharedTypes = sharedTypesExists ? "skipped" : "created";
4044
+ const sharedTypes = !withSharedTypes ? "disabled" : sharedTypesExists ? "skipped" : "created";
2674
4045
  if (sharedTypes === "created" && write) {
2675
4046
  for (const file of wiring.sharedTypesFiles) tree.write(file.filePath, file.content);
2676
4047
  }
@@ -2681,20 +4052,19 @@ function runAddBackend(tree, options) {
2681
4052
  const existing = configParsed ?? {};
2682
4053
  if (parseOk && existing["backend"] !== backend) {
2683
4054
  configOutcome = configRaw === null ? "created" : "updated";
2684
- const indent = configRaw === null ? " " : detectIndent(configRaw);
2685
- if (write) tree.write(TOOLKIT_CONFIG_PATH, `${JSON.stringify({ ...existing, backend }, null, indent)}
2686
- `);
4055
+ if (write) tree.write(TOOLKIT_CONFIG_PATH, applyJsonEdits(configRaw, [[["backend"], backend]]));
2687
4056
  }
2688
4057
  }
2689
- let workspaceGlob = "unrecognized";
2690
- const workspaceYaml = tree.read("pnpm-workspace.yaml", "utf-8");
2691
- if (workspaceYaml !== null) {
2692
- const globResult = addSharedTypesWorkspaceGlob(workspaceYaml);
4058
+ let workspaceGlob = withSharedTypes ? "unrecognized" : void 0;
4059
+ const workspaceYaml = withSharedTypes ? tree.read("pnpm-workspace.yaml", "utf-8") : null;
4060
+ if (!withSharedTypes) {
4061
+ } else if (workspaceYaml !== null) {
4062
+ const globResult = addSharedTypesWorkspaceGlob(workspaceYaml, sharedTypesRoot);
2693
4063
  workspaceGlob = globResult.status;
2694
4064
  if (globResult.status === "added" && write) tree.write("pnpm-workspace.yaml", globResult.text);
2695
4065
  } else if ((detectedPm === "npm" || detectedPm === "yarn" || detectedPm === "bun") && rootPackageJson !== null) {
2696
4066
  const yarnNeedsPrivate = detectedPm === "yarn" && JSON.parse(rootPackageJson).private !== true;
2697
- const npmResult = yarnNeedsPrivate ? { status: "unrecognized" } : addSharedTypesNpmWorkspace(rootPackageJson);
4067
+ const npmResult = yarnNeedsPrivate ? { status: "unrecognized" } : addSharedTypesNpmWorkspace(rootPackageJson, sharedTypesRoot);
2698
4068
  workspaceGlob = npmResult.status;
2699
4069
  if (npmResult.status === "added" && write) tree.write("package.json", npmResult.text);
2700
4070
  }
@@ -2705,17 +4075,17 @@ function runAddBackend(tree, options) {
2705
4075
  const tags2 = holder.tags ?? [];
2706
4076
  if (tags2.includes(tag)) return { project, action: "present" };
2707
4077
  holder.tags = [...tags2, tag];
2708
- if (write) tree.write(target.path, `${JSON.stringify(target.parsed, null, detectIndent(target.raw))}
2709
- `);
4078
+ target.raw = appendJsonArrayItem(target.raw, target.kind === "project" ? ["tags"] : ["nx", "tags"], tag);
4079
+ if (write) tree.write(target.path, target.raw);
2710
4080
  return { project, action: "added" };
2711
4081
  };
2712
4082
  const tags = [
2713
4083
  ...frontendProject === void 0 ? [] : [mergeTag(frontendProject, wiring.tagByRole.frontend)],
2714
4084
  mergeTag(apiProjectName, wiring.tagByRole.backend),
2715
- mergeTag(wiring.sharedTypesPackageName, wiring.tagByRole.shared)
4085
+ ...withSharedTypes ? [mergeTag(wiring.sharedTypesPackageName, wiring.tagByRole.shared)] : []
2716
4086
  ];
2717
- let tsconfigPaths = "workspace-link";
2718
- const tsconfigRaw = tree.read("tsconfig.base.json", "utf-8");
4087
+ let tsconfigPaths = withSharedTypes ? "workspace-link" : "none";
4088
+ const tsconfigRaw = withSharedTypes ? tree.read("tsconfig.base.json", "utf-8") : null;
2719
4089
  if (tsconfigRaw !== null) {
2720
4090
  try {
2721
4091
  const tsconfig = JSON.parse(tsconfigRaw);
@@ -2726,10 +4096,8 @@ function runAddBackend(tree, options) {
2726
4096
  tsconfigPaths = "present";
2727
4097
  } else {
2728
4098
  tsconfigPaths = "added";
2729
- paths[wiring.sharedTypesPackageName] = [`${baseUrl ? "" : "./"}${SHARED_TYPES_ROOT}/src/index.ts`];
2730
- tsconfig.compilerOptions.paths = paths;
2731
- if (write) tree.write("tsconfig.base.json", `${JSON.stringify(tsconfig, null, detectIndent(tsconfigRaw))}
2732
- `);
4099
+ const entry = [`${baseUrl ? "" : "./"}${sharedTypesRoot}/src/index.ts`];
4100
+ if (write) tree.write("tsconfig.base.json", setJsonValue(tsconfigRaw, ["compilerOptions", "paths", wiring.sharedTypesPackageName], entry));
2733
4101
  }
2734
4102
  }
2735
4103
  } catch {
@@ -2740,11 +4108,11 @@ function runAddBackend(tree, options) {
2740
4108
  let snippet;
2741
4109
  if (!applyEslint) {
2742
4110
  eslint = "snippet-only";
2743
- snippet = buildBoundarySnippet();
4111
+ snippet = buildBoundarySnippet(boundaryTags);
2744
4112
  } else {
2745
4113
  const configPath = ESLINT_CONFIG_CANDIDATES.find((candidate) => tree.exists(candidate));
2746
4114
  const configText = configPath ? tree.read(configPath, "utf-8") : null;
2747
- const result = configText !== null ? applyBoundaryConstraints(configText) : { status: "unrecognized" };
4115
+ const result = configText !== null ? applyBoundaryConstraints(configText, boundaryTags) : { status: "unrecognized" };
2748
4116
  if (result.status === "applied") {
2749
4117
  eslint = "applied";
2750
4118
  if (write && configPath) tree.write(configPath, result.text);
@@ -2752,7 +4120,7 @@ function runAddBackend(tree, options) {
2752
4120
  eslint = "already-applied";
2753
4121
  } else {
2754
4122
  eslint = "degraded-unrecognized";
2755
- snippet = buildBoundarySnippet();
4123
+ snippet = buildBoundarySnippet(boundaryTags);
2756
4124
  }
2757
4125
  }
2758
4126
  return {
@@ -2764,7 +4132,8 @@ function runAddBackend(tree, options) {
2764
4132
  tsconfigPaths,
2765
4133
  eslint,
2766
4134
  snippet,
2767
- sharedTypesPackage: wiring.sharedTypesPackageName,
4135
+ sharedTypesPackage: withSharedTypes ? wiring.sharedTypesPackageName : void 0,
4136
+ sharedTypesRoot: withSharedTypes ? sharedTypesRoot : void 0,
2768
4137
  config: configOutcome,
2769
4138
  packageManager,
2770
4139
  ...packageManagerNote !== void 0 ? { packageManagerNote } : {}
@@ -2808,8 +4177,12 @@ function sectionAborted(section) {
2808
4177
  if (section.report === null) return false;
2809
4178
  if (section.report.kind === "fsd") return section.report.report.aborted;
2810
4179
  if (section.report.kind === "review") return section.report.report.workflowAction === "aborted";
4180
+ if (section.report.kind === "backend") return section.report.report.refused !== void 0;
2811
4181
  return false;
2812
4182
  }
4183
+ function failedSections(report) {
4184
+ return report.sections.filter(sectionAborted).map((section) => SECTION_TITLES[section.id]);
4185
+ }
2813
4186
  function renderInitReport(report) {
2814
4187
  const lines = [];
2815
4188
  const backendSuffix = report.backend === "none" ? "" : `, backend: ${report.backend}`;
@@ -2818,8 +4191,8 @@ function renderInitReport(report) {
2818
4191
  if (report.refused) {
2819
4192
  lines.push("");
2820
4193
  lines.push("Refused: the working tree has uncommitted changes.");
2821
- lines.push(" init only runs against a clean working tree, so a partial run can always be undone");
2822
- lines.push(" with a single `git restore .` \u2014 commit or stash your changes, then rerun.");
4194
+ lines.push(" init only runs against a clean working tree, so a partial run can be rolled back exactly");
4195
+ lines.push(" to what was committed \u2014 commit or stash your changes, then rerun.");
2823
4196
  return lines.join("\n");
2824
4197
  }
2825
4198
  for (const section of report.sections) {
@@ -2871,21 +4244,20 @@ function renderInitReport(report) {
2871
4244
  if (ranDirty) {
2872
4245
  lines.push("");
2873
4246
  lines.push("Note: --allowDirty was set and the working tree already had uncommitted changes \u2014");
2874
- lines.push("`git restore .` also reverts those pre-existing changes, not only what this run wrote.");
4247
+ lines.push("restoring the files this run modified also reverts your earlier changes to them.");
2875
4248
  }
2876
4249
  const hadAbort = report.sections.some(sectionAborted);
2877
4250
  if (!report.dryRun && hadAbort) {
2878
4251
  lines.push("");
2879
- if (ranDirty) {
2880
- lines.push("One or more sections aborted. Everything this run wrote can be undone with:");
2881
- lines.push(" git restore .");
2882
- lines.push(" (--allowDirty: this also reverts the pre-existing uncommitted changes noted above.)");
2883
- } else {
2884
- lines.push(
2885
- "One or more sections aborted. init never runs against a dirty tree (see above), so everything"
2886
- );
2887
- lines.push("this run wrote can be undone in one step:");
2888
- lines.push(" git restore .");
4252
+ lines.push("One or more sections aborted. To undo what this run wrote:");
4253
+ const written = report.written ?? { created: [], modified: [] };
4254
+ if (written.modified.length > 0) {
4255
+ lines.push(` git restore -- ${written.modified.join(" ")}`);
4256
+ if (ranDirty) lines.push(" (--allowDirty: this also reverts the uncommitted changes those files already had.)");
4257
+ }
4258
+ if (written.created.length > 0) {
4259
+ lines.push(" Created by this run (untracked \u2014 delete them yourself; `git clean -n` lists them):");
4260
+ for (const path of written.created) lines.push(` ${path}`);
2889
4261
  }
2890
4262
  }
2891
4263
  return lines.join("\n");
@@ -2958,48 +4330,34 @@ function recordPresetChoices(tree, choices, notRecorded, write) {
2958
4330
  if (choices.enforcement !== void 0) fields.push("fsd.enforcement");
2959
4331
  if (choices.teamRules !== void 0) fields.push("agentRules.teamRules");
2960
4332
  if (choices.aiHosts !== void 0) fields.push("aiHosts");
4333
+ if (choices.frontends !== void 0) fields.push("frontends");
2961
4334
  if (fields.length === 0) return { action: "skipped", fields, notRecorded };
2962
4335
  const { raw, parsed } = readToolkitConfigFile(tree);
2963
4336
  if (raw !== null && parsed === null) return { action: "unrecognized", fields, notRecorded };
2964
4337
  const existing = parsed ?? {};
2965
- const asObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
2966
- const next = { ...existing };
2967
- let changed = false;
4338
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4339
+ const edits = [];
2968
4340
  for (const key of topLevel) {
2969
- if (existing[key] !== choices[key]) {
2970
- next[key] = choices[key];
2971
- changed = true;
2972
- }
2973
- }
2974
- if (choices.enforcement !== void 0) {
2975
- const fsd = asObject(existing["fsd"]);
2976
- if (fsd["enforcement"] !== choices.enforcement) {
2977
- next["fsd"] = { ...fsd, enforcement: choices.enforcement };
2978
- changed = true;
2979
- }
2980
- }
2981
- if (choices.teamRules !== void 0) {
2982
- const agentRules = asObject(existing["agentRules"]);
2983
- if (agentRules["teamRules"] !== choices.teamRules) {
2984
- next["agentRules"] = { ...agentRules, teamRules: choices.teamRules };
2985
- changed = true;
2986
- }
4341
+ if (existing[key] !== choices[key]) edits.push([[key], choices[key]]);
2987
4342
  }
4343
+ const nested = (parent, key, value) => {
4344
+ const holder = existing[parent];
4345
+ if (isObject(holder) && holder[key] === value) return;
4346
+ edits.push(isObject(holder) ? [[parent, key], value] : [[parent], { [key]: value }]);
4347
+ };
4348
+ if (choices.enforcement !== void 0) nested("fsd", "enforcement", choices.enforcement);
4349
+ if (choices.teamRules !== void 0) nested("agentRules", "teamRules", choices.teamRules);
2988
4350
  if (choices.aiHosts !== void 0) {
2989
4351
  const recorded = existing["aiHosts"];
2990
4352
  if (!Array.isArray(recorded) || recorded.join("\0") !== choices.aiHosts.join("\0")) {
2991
- next["aiHosts"] = [...choices.aiHosts];
2992
- changed = true;
4353
+ edits.push([["aiHosts"], [...choices.aiHosts]]);
2993
4354
  }
2994
4355
  }
2995
- if (!changed) return { action: "unchanged", fields, notRecorded };
2996
- if (write) {
2997
- const indent = raw === null ? " " : detectIndent(raw);
2998
- const trailingNewline = raw === null || raw.endsWith("\n");
2999
- const body = JSON.stringify(next, null, indent);
3000
- tree.write(TOOLKIT_CONFIG_PATH, trailingNewline ? `${body}
3001
- ` : body);
4356
+ if (choices.frontends !== void 0 && JSON.stringify(existing["frontends"]) !== JSON.stringify(choices.frontends)) {
4357
+ edits.push([["frontends"], choices.frontends]);
3002
4358
  }
4359
+ if (edits.length === 0) return { action: "unchanged", fields, notRecorded };
4360
+ if (write) tree.write(TOOLKIT_CONFIG_PATH, applyJsonEdits(raw, edits));
3003
4361
  return { action: raw === null ? "created" : "updated", fields, notRecorded };
3004
4362
  }
3005
4363
  function keptReviewProvider(option, config) {
@@ -3007,13 +4365,32 @@ function keptReviewProvider(option, config) {
3007
4365
  const hosts = resolveAiHostsOption(option, config);
3008
4366
  return hosts[0] === recordedHosts[0] ? recordedReview(config).provider : void 0;
3009
4367
  }
4368
+ function pendingContents(tree) {
4369
+ return new Map(tree.listChanges().map((change) => [change.path, change.content?.toString("utf-8") ?? null]));
4370
+ }
4371
+ function writtenSince(before, tree) {
4372
+ const created = [];
4373
+ const modified = [];
4374
+ for (const change of tree.listChanges()) {
4375
+ if (change.type === "DELETE") continue;
4376
+ if (!before.has(change.path)) {
4377
+ (change.type === "CREATE" ? created : modified).push(change.path);
4378
+ } else if (before.get(change.path) !== (change.content?.toString("utf-8") ?? null)) {
4379
+ modified.push(change.path);
4380
+ }
4381
+ }
4382
+ return { created: created.sort(), modified: modified.sort() };
4383
+ }
3010
4384
  function runInit(tree, options, deps) {
4385
+ const before = pendingContents(tree);
3011
4386
  const recorded = readToolkitConfigFile(tree).parsed;
3012
4387
  const preset = options.preset ?? recordedChoice(recorded, "preset", RECORDED_PRESETS);
3013
4388
  if (preset === void 0) {
3014
4389
  throw new Error("init: pass --preset=quick-start|solo|team (toolkit.config.json records none).");
3015
4390
  }
3016
- const frontend = resolveInitFrontend(options.frontend, recorded);
4391
+ const app = options.frontend === void 0 && recordedChoice(recorded, "frontend", RECORDED_FRONTENDS) === void 0 ? resolveFrontendApp(tree, void 0) : null;
4392
+ const detected = app?.kind === "found" && app.framework !== "generic" ? app.framework : void 0;
4393
+ const frontend = resolveInitFrontend(options.frontend ?? detected, recorded);
3017
4394
  const backend = options.backend ?? recordedChoice(recorded, "backend", RECORDED_BACKENDS) ?? "none";
3018
4395
  if (frontend === "none" && backend === "none") {
3019
4396
  throw new Error("init: --frontend=none is a backend-only workspace \u2014 pass --backend=express|nest.");
@@ -3043,17 +4420,30 @@ function runInit(tree, options, deps) {
3043
4420
  });
3044
4421
  const sections = [];
3045
4422
  const fsdEnabled = frontend !== "none" && (options.fsd ?? config.fsd.enabled);
4423
+ const passedProjects = (Array.isArray(options.project) ? options.project : (options.project ?? "").split(",")).map((name) => name.trim()).filter((name) => name !== "");
4424
+ const projects = passedProjects.length > 0 ? passedProjects : recordedFrontends(recorded);
3046
4425
  if (fsdEnabled) {
3047
- try {
3048
- const report = runAddFsdLite(tree, { frontend, locale, dryRun, enforcement: config.fsd.enforcement, applyEslint: options.applyEslint ?? false });
3049
- sections.push({ id: "fsd", enabled: true, report: { kind: "fsd", report } });
3050
- } catch (error) {
3051
- sections.push({
3052
- id: "fsd",
3053
- enabled: true,
3054
- report: null,
3055
- thrownError: error instanceof Error ? error.message : String(error)
3056
- });
4426
+ for (const project of projects.length === 0 ? [void 0] : projects) {
4427
+ const found = project === void 0 ? null : resolveFrontendApp(tree, project);
4428
+ const appFrontend = found?.kind === "found" && found.framework !== "generic" ? found.framework : frontend;
4429
+ try {
4430
+ const report = runAddFsdLite(tree, {
4431
+ frontend: appFrontend,
4432
+ locale,
4433
+ dryRun,
4434
+ enforcement: config.fsd.enforcement,
4435
+ applyEslint: options.applyEslint ?? false,
4436
+ ...project === void 0 ? {} : { project }
4437
+ });
4438
+ sections.push({ id: "fsd", enabled: true, report: { kind: "fsd", report } });
4439
+ } catch (error) {
4440
+ sections.push({
4441
+ id: "fsd",
4442
+ enabled: true,
4443
+ report: null,
4444
+ thrownError: error instanceof Error ? error.message : String(error)
4445
+ });
4446
+ }
3057
4447
  }
3058
4448
  } else {
3059
4449
  sections.push({ id: "fsd", enabled: false, report: null });
@@ -3093,7 +4483,9 @@ function runInit(tree, options, deps) {
3093
4483
  const report = runAddBackend(tree, {
3094
4484
  backend: config.backend,
3095
4485
  applyEslint: options.applyEslint ?? false,
3096
- dryRun
4486
+ dryRun,
4487
+ // add-backend wires one frontend: the one --project names, when it names one
4488
+ ...projects.length === 1 ? { frontendProject: projects[0] } : {}
3097
4489
  });
3098
4490
  sections.push({ id: "backend", enabled: true, report: { kind: "backend", report } });
3099
4491
  } else {
@@ -3115,12 +4507,13 @@ function runInit(tree, options, deps) {
3115
4507
  } else {
3116
4508
  sections.push({ id: "review", enabled: false, report: null });
3117
4509
  }
3118
- const fsdSection = sections.find((s) => s.id === "fsd");
3119
- const fsdRan = fsdSection?.enabled === true;
3120
- const fsdApplied = fsdSection?.report?.kind === "fsd" && !fsdSection.report.report.aborted;
4510
+ const fsdSections = sections.filter((s) => s.id === "fsd");
4511
+ const fsdRan = fsdSections.some((s) => s.enabled);
4512
+ const fsdApplied = fsdRan && fsdSections.every((s) => s.report?.kind === "fsd" && !s.report.report.aborted);
3121
4513
  const agentRulesSection = sections.find((s) => s.id === "agentRules");
3122
4514
  const agentRulesRan = agentRulesSection?.enabled === true;
3123
- const agentRulesApplied = agentRulesSection?.report?.kind === "agentRules" && agentRulesSection.report.report.files.every((f) => f.action !== "unrecognized");
4515
+ const agentRulesApplied = agentRulesSection?.report?.kind === "agentRules" && // a block kept because the user edited it does not carry this run's choices either
4516
+ agentRulesSection.report.report.files.every((f) => f.action !== "unrecognized" && f.action !== "user-modified");
3124
4517
  const reviewSection = sections.find((s) => s.id === "review");
3125
4518
  const reviewApplied = reviewSection?.report?.kind === "review" && ["created", "updated", "unchanged"].includes(reviewSection.report.report.configAction);
3126
4519
  const anyChoiceApplied = fsdApplied || agentRulesApplied || reviewApplied;
@@ -3149,7 +4542,11 @@ function runInit(tree, options, deps) {
3149
4542
  packageManager: anyChoiceApplied && knownPackageManager !== null ? knownPackageManager : void 0,
3150
4543
  enforcement: fsdApplied ? config.fsd.enforcement : void 0,
3151
4544
  teamRules: agentRulesApplied ? config.agentRules.teamRules : void 0,
3152
- aiHosts: agentRulesApplied ? config.aiHosts : void 0
4545
+ aiHosts: agentRulesApplied ? config.aiHosts : void 0,
4546
+ // with --project, the apps each FSD section set up
4547
+ frontends: fsdApplied && projects.length > 0 ? fsdSections.flatMap(
4548
+ (s) => s.report?.kind === "fsd" && s.report.report.project !== null && s.report.report.sourceRoot !== null ? [{ project: s.report.report.project, framework: s.report.report.frontend, sourceRoot: s.report.report.sourceRoot }] : []
4549
+ ) : void 0
3153
4550
  },
3154
4551
  notRecorded,
3155
4552
  !dryRun
@@ -3164,7 +4561,8 @@ function runInit(tree, options, deps) {
3164
4561
  allowDirty,
3165
4562
  refused: false,
3166
4563
  sections,
3167
- config: configOutcome
4564
+ config: configOutcome,
4565
+ written: dryRun ? void 0 : writtenSince(before, tree)
3168
4566
  };
3169
4567
  }
3170
4568
  function gitIsWorkingTreeClean(root) {
@@ -3194,13 +4592,21 @@ async function initGenerator(tree, options, deps = { isWorkingTreeClean: gitIsWo
3194
4592
  const changesBefore = tree.listChanges().length;
3195
4593
  const report = runInit(tree, { ...options, dryRun: resolveDryRun(options) }, deps);
3196
4594
  console.log(renderInitReport(report));
4595
+ const failed = options.strict === true ? report.refused ? ["the dirty-tree check"] : failedSections(report) : [];
4596
+ const strictFailure = failed.length === 0 ? null : new Error(`init --strict: a section aborted (${failed.join(", ")}) \u2014 see the report above.`);
4597
+ if (strictFailure !== null && (report.dryRun || report.refused)) throw strictFailure;
3197
4598
  if (report.dryRun || report.refused) return;
3198
- if (tree.listChanges().length === changesBefore) return;
4599
+ const wrote = tree.listChanges().length !== changesBefore;
4600
+ if (!wrote) {
4601
+ if (strictFailure !== null) throw strictFailure;
4602
+ return;
4603
+ }
3199
4604
  const workflowPaths = collectWrittenWorkflowPaths(report);
3200
4605
  const root = tree.root;
3201
4606
  const graphOutputFile = join2(tmpdir(), `nx-archweave-init-graph-${Date.now()}.json`);
3202
4607
  return () => {
3203
4608
  runPostInitValidation({ root, workflowPaths, graphOutputFile }, createProcessExec());
4609
+ if (strictFailure !== null) throw strictFailure;
3204
4610
  };
3205
4611
  }
3206
4612
  var generator_default = initGenerator;