@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
@@ -1,144 +0,0 @@
1
- import { posix, relative, resolve, sep } from 'node:path';
2
- const LAYER_ORDER = ['app', 'pages', 'widgets', 'features', 'entities', 'shared'];
3
- /** Directory name → layer. `_app`/`_pages` are FSD's own Next.js naming (its routers own
4
- * `app/` and `pages/`). */
5
- const LAYER_BY_DIR = {
6
- app: 'app',
7
- _app: 'app',
8
- pages: 'pages',
9
- _pages: 'pages',
10
- widgets: 'widgets',
11
- features: 'features',
12
- entities: 'entities',
13
- shared: 'shared',
14
- };
15
- const SLICED_LAYERS = new Set(['pages', 'widgets', 'features', 'entities']);
16
- const INDEX_FILE = /^index(\.[cm]?[jt]sx?)?$/;
17
- export function locate(rootRelativePath) {
18
- const segments = rootRelativePath.split('/').filter((s) => s.length > 0 && s !== '.');
19
- if (segments.length === 0)
20
- return null;
21
- const layer = LAYER_BY_DIR[segments[0]];
22
- if (layer === undefined)
23
- return null;
24
- const hasUnit = layer !== 'app';
25
- const unit = hasUnit ? (segments[1] ?? null) : null;
26
- const rest = segments.slice(hasUnit ? 2 : 1);
27
- if (rest.length > 0 && INDEX_FILE.test(rest[rest.length - 1]))
28
- rest.pop();
29
- return { layer, unit, rest };
30
- }
31
- /** Pure core: the violation (if any) of `from` importing `to`. */
32
- export function judge(from, to) {
33
- if (LAYER_ORDER.indexOf(to.layer) < LAYER_ORDER.indexOf(from.layer))
34
- return { kind: 'upward', from, to };
35
- if (to.unit === null)
36
- return null; // the app layer, or a bare layer directory — nothing finer to guard
37
- const sameUnit = from.layer === to.layer && from.unit === to.unit;
38
- if (sameUnit)
39
- return null; // inside one slice/segment anything goes
40
- const viaCrossImportApi = to.rest[0] === '@x' && to.rest[1] === from.unit && from.layer === to.layer;
41
- if (viaCrossImportApi)
42
- return null;
43
- if (from.layer === to.layer && SLICED_LAYERS.has(to.layer))
44
- return { kind: 'cross-slice', from, to };
45
- if (to.rest.length > 0)
46
- return { kind: 'sidestep', from, to };
47
- return null;
48
- }
49
- export function describeLocation(location) {
50
- return location.unit === null ? location.layer : `${location.layer}/${location.unit}`;
51
- }
52
- /** Resolves an import specifier to a path relative to the source root, or null when it is
53
- * a package specifier or lands outside the root. */
54
- export function resolveSpecifier(specifier, importerRootRelative, options) {
55
- let rootRelative = null;
56
- if (specifier.startsWith('./') || specifier.startsWith('../') || specifier === '.' || specifier === '..') {
57
- rootRelative = posix.normalize(posix.join(posix.dirname(importerRootRelative), specifier));
58
- }
59
- else {
60
- for (const [prefix, target] of Object.entries(options.aliases)) {
61
- if (!specifier.startsWith(prefix))
62
- continue;
63
- const targetAbsolute = resolve(options.cwd, target);
64
- const targetRootRelative = toPosix(relative(options.rootAbsolute, targetAbsolute));
65
- rootRelative = posix.normalize(posix.join(targetRootRelative === '' ? '.' : targetRootRelative, specifier.slice(prefix.length)));
66
- break;
67
- }
68
- }
69
- if (rootRelative === null)
70
- return null;
71
- if (rootRelative === '..' || rootRelative.startsWith('../'))
72
- return null;
73
- return rootRelative;
74
- }
75
- function toPosix(path) {
76
- return sep === '/' ? path : path.split(sep).join('/');
77
- }
78
- export const fsdBoundariesRule = {
79
- meta: {
80
- type: 'problem',
81
- docs: {
82
- description: 'Enforce canonical Feature-Sliced Design import rules inside a frontend app',
83
- url: 'https://github.com/clearlydev/nx-archweave#fsd-lite',
84
- },
85
- schema: [
86
- {
87
- type: 'object',
88
- properties: {
89
- root: { type: 'string' },
90
- aliases: { type: 'object', additionalProperties: { type: 'string' } },
91
- },
92
- additionalProperties: false,
93
- },
94
- ],
95
- messages: {
96
- upward: 'FSD: "{{from}}" may not import from "{{to}}" (a higher layer).',
97
- crossSlice: 'FSD: "{{from}}" may not import a sibling slice "{{to}}" — move the shared part down a layer, or expose it through "{{to}}/@x/{{fromUnit}}".',
98
- sidestep: 'FSD: import "{{to}}" through its public API (index.ts), not its internal file "{{path}}".',
99
- },
100
- },
101
- create(context) {
102
- const options = (context.options[0] ?? {});
103
- const root = options.root ?? 'src';
104
- const aliases = options.aliases ?? { '@/': root };
105
- const cwd = context.cwd;
106
- const rootAbsolute = resolve(cwd, root);
107
- const importerRootRelative = toPosix(relative(rootAbsolute, context.filename));
108
- if (importerRootRelative === '' || importerRootRelative.startsWith('..'))
109
- return {};
110
- const from = locate(importerRootRelative);
111
- if (from === null)
112
- return {};
113
- const check = (node, specifier) => {
114
- if (typeof specifier !== 'string')
115
- return;
116
- const target = resolveSpecifier(specifier, importerRootRelative, { rootAbsolute, cwd, aliases });
117
- if (target === null)
118
- return;
119
- const to = locate(target);
120
- if (to === null)
121
- return;
122
- const violation = judge(from, to);
123
- if (violation === null)
124
- return;
125
- const data = { from: describeLocation(from), to: describeLocation(to), fromUnit: from.unit ?? '', path: target };
126
- context.report({
127
- node,
128
- messageId: violation.kind === 'upward' ? 'upward' : violation.kind === 'cross-slice' ? 'crossSlice' : 'sidestep',
129
- data,
130
- });
131
- };
132
- const fromSource = (node) => check(node, node.source?.value);
133
- return {
134
- ImportDeclaration: fromSource,
135
- ExportNamedDeclaration: fromSource,
136
- ExportAllDeclaration: fromSource,
137
- ImportExpression: (node) => {
138
- const argument = node.source;
139
- if (argument && argument.type === 'Literal')
140
- check(node, argument.value);
141
- },
142
- };
143
- },
144
- };
@@ -1,114 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
3
- /**
4
- * Import aliases from a tsconfig's `compilerOptions.paths`: every
5
- * `"<prefix>*": ["<dir>/*"]` entry becomes `{ "<prefix>": "<dir>" }`, the directory relative
6
- * to `cwd` (what the rule's `aliases` option takes). The `extends` chain is followed
7
- * (relative paths only — a package preset is skipped), the nearest config winning per key;
8
- * `baseUrl` of the config that declares the paths resolves the targets. Comments and
9
- * trailing commas are tolerated, like tsc. Anything unreadable yields nothing: the rule
10
- * then falls back to its default alias — a wrong alias map costs a missed report, never a
11
- * false one, so this stays best-effort.
12
- */
13
- export function aliasesFromTsconfig({ cwd, tsconfig = 'tsconfig.json', maxDepth = 8 }) {
14
- const aliases = {};
15
- let current = resolve(cwd, tsconfig);
16
- const seen = new Set();
17
- for (let depth = 0; current !== null && depth < maxDepth && !seen.has(current); depth++) {
18
- seen.add(current);
19
- const config = readJsonc(current);
20
- if (config === null)
21
- break;
22
- const compilerOptions = asRecord(config['compilerOptions']);
23
- const paths = asRecord(compilerOptions?.['paths']);
24
- if (paths) {
25
- const baseUrl = typeof compilerOptions?.['baseUrl'] === 'string' ? resolve(dirname(current), compilerOptions['baseUrl']) : dirname(current);
26
- for (const [pattern, targets] of Object.entries(paths)) {
27
- if (!pattern.endsWith('*'))
28
- continue;
29
- const prefix = pattern.slice(0, -1);
30
- if (prefix in aliases)
31
- continue; // a nearer config already decided this prefix
32
- const target = Array.isArray(targets) ? targets.find((t) => typeof t === 'string') : undefined;
33
- if (target === undefined || !target.endsWith('/*'))
34
- continue;
35
- const dir = toPosix(relative(cwd, resolve(baseUrl, target.slice(0, -'/*'.length))));
36
- aliases[prefix] = dir === '' ? '.' : dir;
37
- }
38
- }
39
- const parent = config['extends'];
40
- const parentPath = typeof parent === 'string' ? parent : Array.isArray(parent) ? parent.find((p) => typeof p === 'string') : undefined;
41
- current = parentPath !== undefined && (parentPath.startsWith('.') || isAbsolute(parentPath)) ? resolveExtends(dirname(current), parentPath) : null;
42
- }
43
- return aliases;
44
- }
45
- function resolveExtends(fromDir, target) {
46
- const candidates = [resolve(fromDir, target), resolve(fromDir, `${target}.json`)];
47
- return candidates.find((candidate) => existsSync(candidate)) ?? null;
48
- }
49
- function asRecord(value) {
50
- return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null;
51
- }
52
- function toPosix(path) {
53
- return sep === '/' ? path : path.split(sep).join('/');
54
- }
55
- /** tsconfig is JSON with comments and trailing commas; strip both outside strings. */
56
- export function readJsonc(path) {
57
- let text;
58
- try {
59
- text = readFileSync(path, 'utf-8');
60
- }
61
- catch {
62
- return null;
63
- }
64
- let out = '';
65
- let mode = 'code';
66
- for (let i = 0; i < text.length; i++) {
67
- const c = text[i];
68
- const d = text[i + 1];
69
- if (mode === 'code') {
70
- if (c === '"') {
71
- mode = 'string';
72
- out += c;
73
- }
74
- else if (c === '/' && d === '/') {
75
- mode = 'line';
76
- i++;
77
- }
78
- else if (c === '/' && d === '*') {
79
- mode = 'block';
80
- i++;
81
- }
82
- else {
83
- out += c;
84
- }
85
- }
86
- else if (mode === 'string') {
87
- out += c;
88
- if (c === '\\') {
89
- out += d ?? '';
90
- i++;
91
- }
92
- else if (c === '"') {
93
- mode = 'code';
94
- }
95
- }
96
- else if (mode === 'line') {
97
- if (c === '\n') {
98
- mode = 'code';
99
- out += c;
100
- }
101
- }
102
- else if (c === '*' && d === '/') {
103
- mode = 'code';
104
- i++;
105
- }
106
- }
107
- try {
108
- const parsed = JSON.parse(out.replace(/,(\s*[}\]])/g, '$1'));
109
- return asRecord(parsed);
110
- }
111
- catch {
112
- return null;
113
- }
114
- }
@@ -1,46 +0,0 @@
1
- const ACTION_LINE = {
2
- created: (result) => `${result.filePath}: created`,
3
- appended: (result) => `${result.filePath}: nx-archweave block added after the existing content (nothing else changed)`,
4
- updated: (result) => `${result.filePath}: nx-archweave block updated (nothing outside it changed)`,
5
- unchanged: (result) => `${result.filePath}: unchanged`,
6
- unrecognized: (result) => `${result.filePath}: not written — ${result.reason ?? 'the nx-archweave markers could not be placed'}`,
7
- };
8
- export function renderAddAgentRulesReport(report) {
9
- const lines = [];
10
- lines.push(report.dryRun ? 'add-agent-rules dry-run — no files were written.' : 'add-agent-rules:');
11
- lines.push(` hosts: ${report.hosts.join(', ')}`);
12
- lines.push(` locale: ${report.locale}`);
13
- lines.push(` teamRules: ${report.teamRules}`);
14
- lines.push('');
15
- for (const file of report.files) {
16
- lines.push(` ${ACTION_LINE[file.action](file)}`);
17
- }
18
- const RECORD_LINE = {
19
- created: 'toolkit.config.json: aiHosts recorded (file created)',
20
- updated: 'toolkit.config.json: aiHosts recorded',
21
- unchanged: 'toolkit.config.json: aiHosts already recorded',
22
- unrecognized: 'toolkit.config.json: not a JSON object — aiHosts not recorded (fix the file, then rerun)',
23
- 'not-applied': 'toolkit.config.json: aiHosts not recorded — a file above was not written',
24
- 'not-requested': null,
25
- };
26
- const recordLine = RECORD_LINE[report.hostsRecord];
27
- if (recordLine !== null)
28
- lines.push(` ${recordLine}`);
29
- if (report.files.some((file) => file.action === 'unrecognized')) {
30
- lines.push('');
31
- lines.push('Manual fix needed: keep exactly one "<!-- nx-archweave:agent-rules start -->" … "<!-- nx-archweave:agent-rules end -->"');
32
- lines.push('pair in the file (or remove both markers), then rerun add-agent-rules.');
33
- }
34
- if (report.dryRun) {
35
- lines.push('');
36
- lines.push('Generated content preview:');
37
- for (const file of report.files) {
38
- if (file.content === null)
39
- continue;
40
- lines.push('');
41
- lines.push(`--- ${file.filePath} ---`);
42
- lines.push(file.content);
43
- }
44
- }
45
- return lines.join('\n');
46
- }
@@ -1,102 +0,0 @@
1
- import { packageManagerCommands } from '@nx-archweave/template-engine';
2
- /** Section body, no header — init prints its own `## add-backend` title and
3
- * dry-run marker, so the body must not repeat them. */
4
- export function renderAddBackendBody(report) {
5
- const lines = [];
6
- if (report.refused === 'no-frontend') {
7
- lines.push(`Refused: no frontend application and no "${report.apiName}" app found in this workspace.`);
8
- lines.push(' add-backend wires a backend app — beside a frontend, or on its own in a backend-only');
9
- lines.push(` workspace (create one with: npx create-nx-workspace --preset=${report.backend} --appName=${report.apiName}).`);
10
- lines.push(' If the backend app has another name, pass --apiName=<its directory name>.');
11
- return lines.join('\n');
12
- }
13
- if (report.refused === 'frontend-not-found') {
14
- lines.push(`Refused: --frontendProject "${report.frontendProject}" does not match any project.`);
15
- lines.push(' Nothing was written. Check the name with `nx show projects`.');
16
- return lines.join('\n');
17
- }
18
- if (report.refused === 'ambiguous-frontend') {
19
- lines.push('Refused: multiple frontend applications found — add-backend will not guess:');
20
- for (const name of report.frontendCandidates ?? [])
21
- lines.push(` - ${name}`);
22
- lines.push(' Nothing was written. Re-run with --frontendProject=<name> to pick one.');
23
- return lines.join('\n');
24
- }
25
- if (report.refused === 'invalid-project-json') {
26
- lines.push('Refused: a project.json this run must update could not be parsed:');
27
- for (const path of report.invalidProjects ?? [])
28
- lines.push(` - ${path}`);
29
- lines.push(' Nothing was written — fix the JSON, then re-run add-backend.');
30
- return lines.join('\n');
31
- }
32
- if (report.refused === 'missing-api-project') {
33
- lines.push(`Refused: no "${report.apiName}" application project found.`);
34
- lines.push(' Create it with the official commands first (copy-paste, names already filled in),');
35
- lines.push(' then re-run add-backend to wire it up:');
36
- lines.push('');
37
- for (const command of report.commands ?? [])
38
- lines.push(` ${command}`);
39
- if (report.expressRange !== undefined) {
40
- lines.push('');
41
- lines.push(` The express line declares express@${report.expressRange}, the range this workspace's @nx/react supports —`);
42
- lines.push(' the official generator then keeps it instead of adding express 5, which npm rejects as a peer conflict.');
43
- }
44
- return lines.join('\n');
45
- }
46
- if (report.packageManagerNote !== undefined)
47
- lines.push(` note: ${report.packageManagerNote}`);
48
- lines.push(` frontend project: ${report.frontendProject ?? 'none (backend-only workspace)'}`);
49
- lines.push(report.sharedTypes === 'created'
50
- ? ` libs/shared-types: created (import it as ${report.sharedTypesPackage})`
51
- : ' libs/shared-types: already exists — skipped');
52
- if (report.config !== undefined && report.config !== 'unchanged') {
53
- lines.push(` toolkit.config.json: backend "${report.backend}" recorded (${report.config}) — doctor's backend checks now run`);
54
- }
55
- if (report.workspaceGlob !== undefined) {
56
- const pmName = report.packageManager ?? 'pnpm';
57
- const registry = packageManagerCommands(pmName).workspaceRegistration;
58
- lines.push({
59
- added: ` ${registry}: 'libs/shared-types' registered — run \`${pmName} install\` once to link it`,
60
- present: ` ${registry}: already covers libs/shared-types`,
61
- unrecognized:
62
- // an integrated layout lists no workspace packages: the paths alias below links it
63
- report.tsconfigPaths === 'added' || report.tsconfigPaths === 'present'
64
- ? ` ${registry}: no workspace package list (integrated layout) — the tsconfig paths alias links libs/shared-types`
65
- : ` ${registry}: shape not recognized — add 'libs/shared-types' to its workspace list yourself`,
66
- }[report.workspaceGlob]);
67
- }
68
- for (const tag of report.tags) {
69
- lines.push(` tag ${tag.project}: ${tag.action === 'added' ? 'added' : 'already present'}`);
70
- }
71
- lines.push({
72
- added: ` tsconfig.base.json paths: ${report.sharedTypesPackage} added (a classic or integrated setup — libs resolve through paths)`,
73
- present: ' tsconfig.base.json paths: already wired',
74
- 'workspace-link': ' tsconfig paths: not needed — the workspace package link is the wiring',
75
- unrecognized: ' tsconfig.base.json: could not be parsed — left untouched',
76
- }[report.tsconfigPaths]);
77
- switch (report.eslint) {
78
- case 'applied':
79
- lines.push(' ESLint boundaries: cross-side deny rules written (existing imports stay allowed)');
80
- break;
81
- case 'already-applied':
82
- lines.push(' ESLint boundaries: already enforced — no change');
83
- break;
84
- case 'degraded-unrecognized':
85
- lines.push(' ESLint boundaries: your config was NOT recognized — left untouched.');
86
- lines.push(' Merge this yourself (then boundaries are actually enforced):');
87
- break;
88
- case 'snippet-only':
89
- lines.push(' ESLint boundaries: report-only (pass --applyEslint to write them).');
90
- lines.push(' Paste-ready snippet:');
91
- break;
92
- }
93
- if (report.snippet) {
94
- lines.push('');
95
- lines.push(report.snippet);
96
- }
97
- return lines.join('\n');
98
- }
99
- /** Standalone rendering (the add-backend generator's own console output). */
100
- export function renderAddBackendReport(report, dryRun) {
101
- return [`add-backend (${report.backend})${dryRun ? ' — dry run' : ''}`, '', renderAddBackendBody(report)].join('\n');
102
- }
@@ -1,72 +0,0 @@
1
- export function renderAddFsdLiteReport(report) {
2
- const lines = [];
3
- if (report.aborted) {
4
- lines.push('add-fsd-lite aborted — nothing was written.');
5
- const refusal = report.appRefusal;
6
- if (refusal !== null) {
7
- lines.push('');
8
- lines.push(" FSD layers live inside the frontend app's source root (e.g. apps/web/src), and no app could be picked:");
9
- if (refusal.reason === 'project-not-found') {
10
- lines.push(` --project=${refusal.project} names no project in this workspace.`);
11
- }
12
- else if (refusal.reason === 'ambiguous-frontend') {
13
- lines.push(' more than one frontend app was found — rerun with --project=<one of them>:');
14
- for (const name of refusal.candidates)
15
- lines.push(` - ${name}`);
16
- }
17
- else if (refusal.reason === 'invalid-project-json') {
18
- lines.push(` a project.json could not be parsed (${refusal.detail}) — fix it, then rerun.`);
19
- }
20
- else {
21
- lines.push(' the workspace has no frontend application — create one (e.g. nx g @nx/react:app) or pass --project.');
22
- }
23
- }
24
- return lines.join('\n');
25
- }
26
- if (report.dryRun) {
27
- lines.push('add-fsd-lite dry-run — no files were written.');
28
- lines.push('');
29
- }
30
- lines.push(`FSD layers (in ${report.project}, ${report.sourceRoot}):`);
31
- if (report.layerGuide !== null)
32
- lines.push(report.layerGuide);
33
- lines.push('');
34
- const where = report.eslintPath ?? `${report.project}'s eslint.config.mjs`;
35
- switch (report.eslint) {
36
- case 'applied':
37
- lines.push(`ESLint: archweave/fsd-boundaries (${report.enforcement}) written to ${where}`);
38
- break;
39
- case 'already-applied':
40
- lines.push(`ESLint: archweave/fsd-boundaries already wired in ${where} — no change`);
41
- break;
42
- case 'degraded-unrecognized':
43
- lines.push(`ESLint: ${report.eslintPath === null ? `no ESLint config found for ${report.project}` : `${where} was NOT recognized — left untouched`}.`);
44
- lines.push(' Add the rule yourself (then the layer rules are actually enforced):');
45
- break;
46
- case 'snippet-only':
47
- lines.push(`ESLint: report-only (pass --applyEslint to write the rule into ${where}).`);
48
- break;
49
- case 'not-applied':
50
- break;
51
- }
52
- if (report.snippet !== null)
53
- lines.push(report.snippet);
54
- if (report.steiger !== null) {
55
- const st = report.steiger;
56
- lines.push('');
57
- lines.push('steiger (official FSD linter, opt-in):');
58
- lines.push(` ${st.configPath}: ${st.config === 'created' ? 'created' : st.config === 'unchanged' ? 'unchanged' : 'kept — an existing config with other content is never replaced'}`);
59
- lines.push(st.target === 'added'
60
- ? ` target ${'fsd-check'}: added to ${st.targetPath}`
61
- : st.target === 'present'
62
- ? ` target ${'fsd-check'}: already present in ${st.targetPath}`
63
- : ' target fsd-check: NOT added — no parseable project.json / package.json for the app');
64
- lines.push(st.dependencies === 'added'
65
- ? ` devDependencies: steiger + @feature-sliced/steiger-plugin added to package.json — run \`${st.installCommand}\``
66
- : st.dependencies === 'present'
67
- ? ' devDependencies: already present'
68
- : ' devDependencies: NOT added — the root package.json could not be parsed');
69
- lines.push(` run it with: ${st.runCommand} (steiger needs Node >= 22.18)`);
70
- }
71
- return lines.join('\n');
72
- }
@@ -1,26 +0,0 @@
1
- const ACTION_LINES = {
2
- created: 'created',
3
- unchanged: 'unchanged (already matches the generated version)',
4
- skipped: 'left as-is (a different file already exists there — never overwritten)',
5
- };
6
- export function renderAddGithubTemplatesReport(report) {
7
- const lines = [];
8
- if (report.files.length === 0) {
9
- lines.push('add-github-templates: nothing selected (issue/pr/codeowners all false) — no files to install.');
10
- return lines.join('\n');
11
- }
12
- if (report.dryRun) {
13
- lines.push('add-github-templates dry-run — no files were written.');
14
- lines.push('');
15
- for (const file of report.files) {
16
- lines.push(`${file.filePath}:`);
17
- lines.push(file.content);
18
- lines.push('');
19
- }
20
- lines.push('Effective decision:');
21
- }
22
- for (const file of report.files) {
23
- lines.push(` ${file.filePath}: ${ACTION_LINES[file.action]}`);
24
- }
25
- return lines.join('\n');
26
- }
@@ -1,34 +0,0 @@
1
- import { renderUserCommand } from '@nx-archweave/integrations';
2
- export function renderIntegrationPlan(view) {
3
- const command = view.plan.officialCommand;
4
- const scopeFlag = view.scopeExplicit ? ` --scope=${view.scope}` : '';
5
- const lines = [
6
- `Integration: ${view.tool}`,
7
- `Source repository: ${view.plan.sourceRepository}`,
8
- `Official homepage: ${view.plan.officialHomepage}`,
9
- `License: ${view.plan.license}`,
10
- `Resolved version: ${view.plan.resolvedVersion}`,
11
- `Official install method: ${command === null ? 'manual (no scriptable command for this target)' : renderUserCommand(command)}`,
12
- ...(command !== null && view.teamMarketplaceCommand !== null
13
- ? [`For teammates (project scope), install-integration first declares the marketplace in .claude/settings.json: ${view.teamMarketplaceCommand}`]
14
- : []),
15
- ...(command === null
16
- ? []
17
- : [
18
- `Run it for you (verifies and records afterwards): ${view.runNx} g @clearlydev/nx-archweave:install-integration --tool=${view.tool} --apply${scopeFlag}`,
19
- ]),
20
- '',
21
- command === null ? 'Manual steps:' : 'Manual alternative (inside a Claude Code session):',
22
- ...view.manualSteps.map((step) => ` - ${step}`),
23
- '',
24
- 'Uninstall instructions:',
25
- ...view.uninstallSteps.map((step) => ` - ${step}`),
26
- '',
27
- `Current detection: ${view.detection.installed ? 'installed' : 'not installed'}`,
28
- `Current verification: ${view.verification.verified ? 'passed' : 'not verified'}`,
29
- ...view.verification.problems.map((problem) => ` ! ${problem}`),
30
- '',
31
- 'Manifest write: none here — install-integration records it after installing; record-integration records an install done elsewhere.',
32
- ];
33
- return lines.join('\n');
34
- }
@@ -1,96 +0,0 @@
1
- /** Line-level LCS diff — small generated files, no diff library. */
2
- export function renderLineDiff(existing, generated) {
3
- const a = existing.split('\n');
4
- const b = generated.split('\n');
5
- const m = a.length;
6
- const n = b.length;
7
- const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
8
- for (let i = m - 1; i >= 0; i--) {
9
- for (let j = n - 1; j >= 0; j--) {
10
- lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
11
- }
12
- }
13
- const out = ['--- existing', '+++ generated'];
14
- let i = 0;
15
- let j = 0;
16
- while (i < m && j < n) {
17
- if (a[i] === b[j]) {
18
- out.push(` ${a[i]}`);
19
- i++;
20
- j++;
21
- }
22
- else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
23
- out.push(`- ${a[i]}`);
24
- i++;
25
- }
26
- else {
27
- out.push(`+ ${b[j]}`);
28
- j++;
29
- }
30
- }
31
- while (i < m)
32
- out.push(`- ${a[i++]}`);
33
- while (j < n)
34
- out.push(`+ ${b[j++]}`);
35
- return out.join('\n');
36
- }
37
- const WORKFLOW_ACTION_LINES = {
38
- created: 'created',
39
- overwritten: 'overwritten with the generated version (per --onConflict=overwrite)',
40
- unchanged: 'unchanged (already matches the generated version)',
41
- proposed: 'NOT written — differs from the generated version (merge proposal below)',
42
- skipped: 'left as-is (per --onConflict=skip)',
43
- alternate: 'generated alongside the existing file',
44
- aborted: 'NOT written',
45
- };
46
- const CONFIG_ACTION_LINES = {
47
- created: 'created with the review field',
48
- updated: 'review field updated',
49
- unchanged: 'review field unchanged',
50
- 'not-updated': 'NOT updated — kept consistent with the workflow file that is actually in effect',
51
- aborted: 'NOT updated',
52
- };
53
- export function renderAddReviewReport(report) {
54
- const lines = [];
55
- if (report.packageManagerNote !== undefined) {
56
- lines.push(`Note: ${report.packageManagerNote}`);
57
- }
58
- if (report.workflowAction === 'aborted') {
59
- lines.push('add-review aborted — nothing was written.');
60
- lines.push(` toolkit.config.json: ${report.configError ?? 'unusable'}`);
61
- lines.push(' Fix the config file manually, then rerun the generator.');
62
- return lines.join('\n');
63
- }
64
- if (report.dryRun) {
65
- lines.push('add-review dry-run — no files were written.');
66
- lines.push('');
67
- lines.push('Generated workflow YAML:');
68
- lines.push(report.workflowYaml ?? '');
69
- lines.push('');
70
- lines.push('toolkit.config.json review field:');
71
- lines.push(report.configSnippetJson ?? '');
72
- lines.push('');
73
- lines.push('Effective decision:');
74
- }
75
- lines.push(` ${report.workflowPath}: ${WORKFLOW_ACTION_LINES[report.workflowAction]}`);
76
- lines.push(` toolkit.config.json: ${CONFIG_ACTION_LINES[report.configAction]}`);
77
- if (report.workflowAction === 'proposed' && report.diff) {
78
- lines.push('');
79
- lines.push(report.diff);
80
- lines.push('');
81
- lines.push('To resolve, rerun with one of:');
82
- lines.push(' --onConflict=overwrite replace the existing file with the generated version');
83
- lines.push(' --onConflict=skip keep the existing file');
84
- lines.push(' --onConflict=alternate-filename write the generated version alongside it');
85
- }
86
- if (report.workflowAction === 'alternate') {
87
- lines.push('');
88
- lines.push(' Review the generated file, then replace the main workflow file with it yourself;');
89
- lines.push(' the config stays pointing at the workflow that is actually in effect until you do.');
90
- }
91
- lines.push('');
92
- lines.push(`Note: the review workflow stays green-skipped until you register the ${report.requiredSecretName} secret.`);
93
- lines.push('Note: this workflow runs with elevated permissions only after it is merged to the default branch —');
94
- lines.push('merging is the first real validation.');
95
- return lines.join('\n');
96
- }