@clearlydev/nx-archweave 0.0.0-stage → 0.1.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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +265 -2
  3. package/dist/bin/review-verdict.js +330 -0
  4. package/dist/eslint/fsd-boundaries.d.ts +119 -0
  5. package/dist/eslint/fsd-boundaries.js +144 -0
  6. package/dist/eslint/index.d.ts +78 -0
  7. package/dist/eslint/index.js +256 -0
  8. package/dist/eslint/tsconfig-aliases.d.ts +18 -0
  9. package/dist/eslint/tsconfig-aliases.js +114 -0
  10. package/dist/generators/add-agent-rules/generator.js +550 -0
  11. package/dist/generators/add-agent-rules/render.js +46 -0
  12. package/dist/generators/add-backend/generator.js +784 -0
  13. package/dist/generators/add-backend/render.js +102 -0
  14. package/dist/generators/add-fsd-lite/generator.js +553 -0
  15. package/dist/generators/add-fsd-lite/render.js +72 -0
  16. package/dist/generators/add-github-templates/files/en/codeowners/.github/CODEOWNERS.template +4 -0
  17. package/dist/generators/add-github-templates/files/en/issue/.github/ISSUE_TEMPLATE/bug-report.yml.template +28 -0
  18. package/dist/generators/add-github-templates/files/en/issue/.github/ISSUE_TEMPLATE/feature-request.yml.template +17 -0
  19. package/dist/generators/add-github-templates/files/en/pr/.github/PULL_REQUEST_TEMPLATE.md.template +12 -0
  20. package/dist/generators/add-github-templates/files/ko/codeowners/.github/CODEOWNERS.template +4 -0
  21. package/dist/generators/add-github-templates/files/ko/issue/.github/ISSUE_TEMPLATE/bug-report.yml.template +28 -0
  22. package/dist/generators/add-github-templates/files/ko/issue/.github/ISSUE_TEMPLATE/feature-request.yml.template +17 -0
  23. package/dist/generators/add-github-templates/files/ko/pr/.github/PULL_REQUEST_TEMPLATE.md.template +12 -0
  24. package/dist/generators/add-github-templates/generator.js +125 -0
  25. package/dist/generators/add-github-templates/render.js +26 -0
  26. package/dist/generators/add-integration/generator.js +818 -0
  27. package/dist/generators/add-integration/render.js +34 -0
  28. package/dist/generators/add-review/generator.js +921 -0
  29. package/dist/generators/add-review/render.js +96 -0
  30. package/dist/generators/allow-builds/generator.js +436 -0
  31. package/dist/generators/allow-builds/render.js +82 -0
  32. package/dist/generators/doctor/generator.js +2431 -0
  33. package/dist/generators/doctor/integration-report.js +70 -0
  34. package/dist/generators/doctor/render.js +10 -0
  35. package/dist/generators/entity/generator.js +395 -0
  36. package/dist/generators/feature/generator.js +395 -0
  37. package/dist/generators/init/generator.js +3211 -0
  38. package/dist/generators/init/render.js +125 -0
  39. package/dist/generators/init/validation.js +74 -0
  40. package/dist/generators/install-integration/generator.js +1233 -0
  41. package/dist/generators/install-integration/render.js +128 -0
  42. package/dist/generators/page/generator.js +395 -0
  43. package/dist/generators/record-integration/generator.js +852 -0
  44. package/dist/generators/record-integration/render.js +31 -0
  45. package/dist/generators/shared/dry-run.js +12 -0
  46. package/dist/generators/shared/frontend-app.js +57 -0
  47. package/dist/generators/shared/generator.js +395 -0
  48. package/dist/generators/shared/project-type.js +39 -0
  49. package/dist/generators/shared/record-pipeline.js +34 -0
  50. package/dist/generators/shared/run-nx.js +24 -0
  51. package/dist/generators/shared/template-files.js +38 -0
  52. package/dist/generators/shared/toolkit-config-file.js +110 -0
  53. package/dist/generators/slice/core.js +185 -0
  54. package/dist/generators/slice/files/index/index.ts.template +14 -0
  55. package/dist/generators/slice/files/module/__name__.spec.ts.template +11 -0
  56. package/dist/generators/slice/files/module/__name__.ts.template +11 -0
  57. package/dist/generators/slice/files/segment/index.ts.template +20 -0
  58. package/dist/generators/slice/render.js +70 -0
  59. package/dist/generators/widget/generator.js +395 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +680 -0
  62. package/dist/lib/nx-plugin.d.ts +2 -0
  63. package/dist/lib/nx-plugin.js +7 -0
  64. package/dist/runtime/integrations-composition-root.js +20 -0
  65. package/dist/runtime/node-command-runner.js +81 -0
  66. package/generators.json +85 -0
  67. package/package.json +95 -4
  68. package/src/generators/add-agent-rules/schema.json +43 -0
  69. package/src/generators/add-backend/schema.json +39 -0
  70. package/src/generators/add-fsd-lite/schema.json +43 -0
  71. package/src/generators/add-github-templates/schema.json +35 -0
  72. package/src/generators/add-integration/schema.json +35 -0
  73. package/src/generators/add-review/schema.json +72 -0
  74. package/src/generators/allow-builds/schema.json +20 -0
  75. package/src/generators/doctor/schema.json +20 -0
  76. package/src/generators/entity/schema.json +37 -0
  77. package/src/generators/feature/schema.json +37 -0
  78. package/src/generators/init/schema.json +98 -0
  79. package/src/generators/install-integration/schema.json +35 -0
  80. package/src/generators/page/schema.json +53 -0
  81. package/src/generators/record-integration/schema.json +26 -0
  82. package/src/generators/shared/schema.json +32 -0
  83. package/src/generators/widget/schema.json +45 -0
@@ -0,0 +1,31 @@
1
+ // Never renders installPath or any absolute/home-directory path — the
2
+ // manifest entry types used here already exclude installPath by
3
+ // construction, and this function only ever reads from
4
+ // those already-narrowed values.
5
+ export function renderRecordIntegrationView(view) {
6
+ const lines = [
7
+ `Integration: ${view.tool}`,
8
+ `Target: ${view.target}`,
9
+ `Source repository: ${view.sourceRepository}`,
10
+ `License: ${view.license}`,
11
+ `Verification source: ${view.verificationSource}`,
12
+ `Current verification: ${view.verification.verified ? 'passed' : 'not verified'}`,
13
+ ...view.verification.problems.map((problem) => ` ! ${problem}`),
14
+ '',
15
+ ];
16
+ if (view.hasConflict && view.existingEntry) {
17
+ lines.push('Conflict: an existing manifest entry differs from what would be recorded.', ` Existing: ${JSON.stringify(view.existingEntry)}`, ` Proposed: ${JSON.stringify(view.proposedEntry)}`, '');
18
+ }
19
+ if (view.proposedEntry) {
20
+ lines.push(view.wasWritten
21
+ ? 'Recorded — the following manifest entry was written:'
22
+ : 'The following manifest entry would be recorded:', ` ${JSON.stringify(view.proposedEntry)}`);
23
+ }
24
+ else {
25
+ lines.push('Nothing would be recorded — verification did not succeed via the authoritative claude-cli signal.');
26
+ }
27
+ lines.push('', view.wasWritten
28
+ ? '(recorded — the manifest was written)'
29
+ : '(dry-run — no files were changed, no installer was executed, manifest was not written)');
30
+ return lines.join('\n');
31
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `nx g` owns the dry-run flag: Nx 23.1's generate command deletes `dryRun` /
3
+ * `dry-run` from the options it hands a generator and then refills the schema
4
+ * default, and signals its own `--dry-run` only through `NX_DRY_RUN=true`
5
+ * (`nx/src/command-line/generate/generate.js`, `command-object.js`). So an Nx
6
+ * entry point that read only `options.dryRun` would always see `false` and print
7
+ * "created" for a run Nx never flushes. Programmatic callers (init → the `run*`
8
+ * cores, tests) keep passing `dryRun` explicitly; Nx's own flag always wins.
9
+ */
10
+ export function resolveDryRun(options) {
11
+ return process.env['NX_DRY_RUN'] === 'true' || (options.dryRun ?? false);
12
+ }
@@ -0,0 +1,57 @@
1
+ import { getProjects } from '@nx/devkit';
2
+ import { resolveProjectType } from "./project-type.js";
3
+ const baseNameOf = (root) => root.split('/').pop() ?? root;
4
+ function isEndToEnd(root) {
5
+ const dir = baseNameOf(root);
6
+ return dir === 'e2e' || dir.endsWith('-e2e');
7
+ }
8
+ /** add-backend's api app (default name `api`) and anything tagged as the backend scope are
9
+ * applications, but not frontends. */
10
+ function isBackend(root, config) {
11
+ return baseNameOf(root) === 'api' || (config.tags ?? []).includes('scope:backend');
12
+ }
13
+ function sourceRootOf(config) {
14
+ if (config.sourceRoot)
15
+ return config.sourceRoot;
16
+ return config.root === '' || config.root === '.' ? 'src' : `${config.root}/src`;
17
+ }
18
+ /**
19
+ * Same two tiers as add-backend's frontend lookup: declared applications first; only when
20
+ * there are none do undeclared projects count, typed the way Nx infers them (Nx's Next.js
21
+ * and classic-preset apps are bare package.json projects). A declared root application
22
+ * (a standalone workspace, root `.`) counts; an undeclared root package.json never does.
23
+ */
24
+ export function resolveFrontendApp(tree, explicitProject) {
25
+ let projects;
26
+ try {
27
+ projects = getProjects(tree);
28
+ }
29
+ catch (error) {
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ const pathMatch = /Cannot parse ([^:]+):/.exec(message);
32
+ return { kind: 'invalid-project-json', detail: pathMatch ? pathMatch[1] : message };
33
+ }
34
+ const found = (project, config) => ({
35
+ kind: 'found',
36
+ project,
37
+ root: config.root,
38
+ sourceRoot: sourceRootOf(config),
39
+ });
40
+ if (explicitProject !== undefined) {
41
+ const config = projects.get(explicitProject);
42
+ return config ? found(explicitProject, config) : { kind: 'not-found', project: explicitProject };
43
+ }
44
+ const eligible = [...projects.entries()].filter(([, config]) => !isEndToEnd(config.root) && !isBackend(config.root, config));
45
+ const declared = eligible.filter(([, config]) => config.projectType === 'application');
46
+ const inferred = eligible.filter(([, config]) => config.projectType === undefined &&
47
+ config.root !== '' &&
48
+ config.root !== '.' &&
49
+ resolveProjectType(tree, config.root, undefined) === 'application');
50
+ const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
51
+ if (candidates.length === 0)
52
+ return { kind: 'none' };
53
+ if (candidates.length > 1)
54
+ return { kind: 'ambiguous', candidates: candidates.map(([name]) => name) };
55
+ const [project, config] = candidates[0];
56
+ return found(project, config);
57
+ }
@@ -0,0 +1,395 @@
1
+ // src/generators/slice/core.ts
2
+ import { join } from "node:path";
3
+
4
+ // ../template-engine/dist/fsd/slice-rules.js
5
+ function toCamelCase(kebabName) {
6
+ return kebabName.split("-").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
7
+ }
8
+ function resolveSlicePublicApiName(name, publicApiName) {
9
+ return publicApiName ?? toCamelCase(name);
10
+ }
11
+ var IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
12
+ var RESERVED_WORDS = /* @__PURE__ */ new Set([
13
+ "await",
14
+ "break",
15
+ "case",
16
+ "catch",
17
+ "class",
18
+ "const",
19
+ "continue",
20
+ "debugger",
21
+ "default",
22
+ "delete",
23
+ "do",
24
+ "else",
25
+ "enum",
26
+ "export",
27
+ "extends",
28
+ "false",
29
+ "finally",
30
+ "for",
31
+ "function",
32
+ "if",
33
+ "implements",
34
+ "import",
35
+ "in",
36
+ "instanceof",
37
+ "interface",
38
+ "let",
39
+ "new",
40
+ "null",
41
+ "package",
42
+ "private",
43
+ "protected",
44
+ "public",
45
+ "return",
46
+ "static",
47
+ "super",
48
+ "switch",
49
+ "this",
50
+ "throw",
51
+ "true",
52
+ "try",
53
+ "typeof",
54
+ "var",
55
+ "void",
56
+ "while",
57
+ "with",
58
+ "yield"
59
+ ]);
60
+ function isUsableSliceApiName(candidate) {
61
+ return IDENTIFIER_PATTERN.test(candidate) && !RESERVED_WORDS.has(candidate);
62
+ }
63
+ var ESSENCE_SEGMENT_NAMES = [
64
+ "components",
65
+ "hooks",
66
+ "helpers",
67
+ "utils",
68
+ "types",
69
+ "constants",
70
+ "consts",
71
+ "interfaces",
72
+ "enums",
73
+ "modals",
74
+ "stores",
75
+ "services"
76
+ ];
77
+ function isEssenceSegmentName(name) {
78
+ return ESSENCE_SEGMENT_NAMES.includes(name);
79
+ }
80
+ var SEGMENT_NAME_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
81
+
82
+ // src/generators/shared/toolkit-config-file.ts
83
+ var TOOLKIT_CONFIG_PATH = "toolkit.config.json";
84
+ function readToolkitConfigFile(tree) {
85
+ const raw = tree.read(TOOLKIT_CONFIG_PATH, "utf-8");
86
+ if (raw === null) return { raw: null, parsed: null };
87
+ try {
88
+ const parsed = JSON.parse(raw);
89
+ return {
90
+ raw,
91
+ parsed: typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null
92
+ };
93
+ } catch {
94
+ return { raw, parsed: null };
95
+ }
96
+ }
97
+ function readToolkitConfig(tree) {
98
+ return readToolkitConfigFile(tree).parsed;
99
+ }
100
+ function resolveLocaleOption(option, config) {
101
+ if (option) return option;
102
+ const locale = config?.["locale"];
103
+ return locale === "ko" || locale === "en" ? locale : "en";
104
+ }
105
+ var RECORDED_FRONTENDS = ["react-vite", "next", "none"];
106
+
107
+ // src/generators/shared/frontend-app.ts
108
+ import { getProjects } from "@nx/devkit";
109
+
110
+ // src/generators/shared/project-type.ts
111
+ import { parseJson } from "@nx/devkit";
112
+ function resolveProjectType(tree, root, declared) {
113
+ if (declared) return declared === "library" ? "library" : "application";
114
+ const prefix = root === "" || root === "." ? "" : `${root}/`;
115
+ if (tree.exists(`${prefix}tsconfig.lib.json`)) return "library";
116
+ if (tree.exists(`${prefix}tsconfig.app.json`)) return "application";
117
+ const raw = tree.read(`${prefix}package.json`, "utf-8");
118
+ if (raw === null) return "library";
119
+ try {
120
+ const manifest = parseJson(raw);
121
+ if (manifest && !["exports", "main", "module", "bin"].some((key) => Boolean(manifest[key]))) return "application";
122
+ } catch {
123
+ }
124
+ return "library";
125
+ }
126
+
127
+ // src/generators/shared/frontend-app.ts
128
+ var baseNameOf = (root) => root.split("/").pop() ?? root;
129
+ function isEndToEnd(root) {
130
+ const dir = baseNameOf(root);
131
+ return dir === "e2e" || dir.endsWith("-e2e");
132
+ }
133
+ function isBackend(root, config) {
134
+ return baseNameOf(root) === "api" || (config.tags ?? []).includes("scope:backend");
135
+ }
136
+ function sourceRootOf(config) {
137
+ if (config.sourceRoot) return config.sourceRoot;
138
+ return config.root === "" || config.root === "." ? "src" : `${config.root}/src`;
139
+ }
140
+ function resolveFrontendApp(tree, explicitProject) {
141
+ let projects;
142
+ try {
143
+ projects = getProjects(tree);
144
+ } catch (error) {
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ const pathMatch = /Cannot parse ([^:]+):/.exec(message);
147
+ return { kind: "invalid-project-json", detail: pathMatch ? pathMatch[1] : message };
148
+ }
149
+ const found = (project2, config2) => ({
150
+ kind: "found",
151
+ project: project2,
152
+ root: config2.root,
153
+ sourceRoot: sourceRootOf(config2)
154
+ });
155
+ if (explicitProject !== void 0) {
156
+ const config2 = projects.get(explicitProject);
157
+ return config2 ? found(explicitProject, config2) : { kind: "not-found", project: explicitProject };
158
+ }
159
+ const eligible = [...projects.entries()].filter(([, config2]) => !isEndToEnd(config2.root) && !isBackend(config2.root, config2));
160
+ const declared = eligible.filter(([, config2]) => config2.projectType === "application");
161
+ const inferred = eligible.filter(
162
+ ([, config2]) => config2.projectType === void 0 && config2.root !== "" && config2.root !== "." && resolveProjectType(tree, config2.root, void 0) === "application"
163
+ );
164
+ const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
165
+ if (candidates.length === 0) return { kind: "none" };
166
+ if (candidates.length > 1) return { kind: "ambiguous", candidates: candidates.map(([name]) => name) };
167
+ const [project, config] = candidates[0];
168
+ return found(project, config);
169
+ }
170
+
171
+ // src/generators/shared/template-files.ts
172
+ import { existsSync } from "node:fs";
173
+ import { fileURLToPath } from "node:url";
174
+ import { generateFiles } from "@nx/devkit";
175
+ import { createTree } from "@nx/devkit/testing";
176
+ function generatorFilesDir(generator, fromUrl) {
177
+ for (const relative of [`../${generator}/files`, `./generators/${generator}/files`]) {
178
+ const dir = fileURLToPath(new URL(relative, fromUrl));
179
+ if (existsSync(dir)) return dir;
180
+ }
181
+ throw new Error(`The "${generator}" generator's template files are missing next to ${fileURLToPath(fromUrl)}`);
182
+ }
183
+ function renderTemplateFiles(srcFolder, target, substitutions) {
184
+ const scratch = createTree();
185
+ const seeded = new Set(scratch.listChanges().map((change) => change.path));
186
+ generateFiles(scratch, srcFolder, target, substitutions);
187
+ return scratch.listChanges().flatMap(
188
+ (change) => change.type === "CREATE" && change.content !== null && !seeded.has(change.path) ? [{ filePath: change.path, content: change.content.toString("utf-8") }] : []
189
+ ).sort((a, b) => a.filePath.localeCompare(b.filePath));
190
+ }
191
+
192
+ // src/generators/slice/core.ts
193
+ function composeSlice(input) {
194
+ const files = generatorFilesDir("slice", import.meta.url);
195
+ const directory = `${input.sourceRoot}/${input.layerDirName}/${input.name}`;
196
+ const shared = input.layer === "shared";
197
+ const values = {
198
+ name: input.name,
199
+ publicApiName: input.publicApiName,
200
+ layer: input.layer,
201
+ locale: input.locale,
202
+ modulePath: shared ? `./${input.name}` : `./model/${input.name}`
203
+ };
204
+ const module = renderTemplateFiles(join(files, "module"), shared ? directory : `${directory}/model`, values).sort((a, b) => Number(a.filePath.endsWith(".spec.ts")) - Number(b.filePath.endsWith(".spec.ts")));
205
+ const segments = shared ? [] : Array.from(new Set(input.segments)).filter((segment) => segment !== "model").flatMap((segment) => renderTemplateFiles(join(files, "segment"), `${directory}/${segment}`, { ...values, segment }));
206
+ return {
207
+ directory,
208
+ publicApiName: input.publicApiName,
209
+ files: [...renderTemplateFiles(join(files, "index"), directory, values), ...module, ...segments]
210
+ };
211
+ }
212
+ var KEBAB_CASE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
213
+ function layerDirNameFor(layer, frontend) {
214
+ return layer === "pages" && frontend === "next" ? "_pages" : layer;
215
+ }
216
+ function layerDirFor(layer, sourceRoot, frontend = "react-vite") {
217
+ return `${sourceRoot}/${layerDirNameFor(layer, frontend)}`;
218
+ }
219
+ var NEXT_CONFIG_FILES = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"];
220
+ function resolveFrontendForApp(option, config, appRoot, tree) {
221
+ if (option === "react-vite" || option === "next") return option;
222
+ const recorded = config?.["frontend"];
223
+ if (typeof recorded === "string" && RECORDED_FRONTENDS.includes(recorded) && recorded !== "none") {
224
+ return recorded;
225
+ }
226
+ const prefix = appRoot === "" || appRoot === "." ? "" : `${appRoot}/`;
227
+ return NEXT_CONFIG_FILES.some((file) => tree.exists(`${prefix}${file}`)) ? "next" : "react-vite";
228
+ }
229
+ function listFilesUnder(tree, dir, prefix = "") {
230
+ return tree.children(dir).flatMap((child) => {
231
+ const path = `${dir}/${child}`;
232
+ return tree.isFile(path) ? [`${prefix}${child}`] : listFilesUnder(tree, path, `${prefix}${child}/`);
233
+ });
234
+ }
235
+ function computeSliceAction(tree, plan) {
236
+ if (!tree.exists(plan.directory)) return "created";
237
+ const expectedNames = plan.files.map((f) => f.filePath.slice(plan.directory.length + 1)).sort();
238
+ const actualNames = listFilesUnder(tree, plan.directory).sort();
239
+ if (actualNames.length !== expectedNames.length || !actualNames.every((n, i) => n === expectedNames[i])) {
240
+ return "skipped";
241
+ }
242
+ const allMatch = plan.files.every((f) => tree.read(f.filePath, "utf-8") === f.content);
243
+ return allMatch ? "unchanged" : "skipped";
244
+ }
245
+ function refusal(reason, dryRun, layer, name, app, extra = {}) {
246
+ const layerDir = layerDirFor(layer, app.sourceRoot);
247
+ return {
248
+ dryRun,
249
+ layer,
250
+ name,
251
+ project: app.project,
252
+ layerDir,
253
+ directory: `${layerDir}/${name}`,
254
+ publicApiName: null,
255
+ refused: true,
256
+ refusalReason: reason,
257
+ action: null,
258
+ files: [],
259
+ ...extra
260
+ };
261
+ }
262
+ function runFsdSlice(tree, layer, options) {
263
+ const dryRun = options.dryRun ?? false;
264
+ const unresolved = { project: null, sourceRoot: "<app>/src" };
265
+ if (!KEBAB_CASE.test(options.name)) {
266
+ return refusal("invalid-name", dryRun, layer, options.name, unresolved);
267
+ }
268
+ if (!isUsableSliceApiName(resolveSlicePublicApiName(options.name, options.publicApiName))) {
269
+ return refusal("invalid-public-api-name", dryRun, layer, options.name, unresolved);
270
+ }
271
+ if (layer === "shared" && isEssenceSegmentName(options.name)) {
272
+ return refusal("segment-by-essence", dryRun, layer, options.name, unresolved);
273
+ }
274
+ const segments = Array.from(new Set(options.segments ?? []));
275
+ if (segments.length > 0 && layer === "shared") {
276
+ return refusal("segments-on-shared", dryRun, layer, options.name, unresolved);
277
+ }
278
+ const badSegment = segments.find((segment) => !SEGMENT_NAME_PATTERN.test(segment));
279
+ if (badSegment !== void 0) return refusal("invalid-segment", dryRun, layer, options.name, unresolved, { detail: badSegment });
280
+ const essenceSegment = segments.find((segment) => isEssenceSegmentName(segment));
281
+ if (essenceSegment !== void 0) return refusal("segment-by-essence", dryRun, layer, options.name, unresolved, { detail: essenceSegment });
282
+ const app = resolveFrontendApp(tree, options.project);
283
+ if (app.kind === "not-found") return refusal("project-not-found", dryRun, layer, options.name, { project: app.project, sourceRoot: unresolved.sourceRoot });
284
+ if (app.kind === "ambiguous") return refusal("ambiguous-frontend", dryRun, layer, options.name, unresolved, { candidates: app.candidates });
285
+ if (app.kind === "invalid-project-json") return refusal("invalid-project-json", dryRun, layer, options.name, unresolved, { detail: app.detail });
286
+ if (app.kind === "none") return refusal("no-frontend-app", dryRun, layer, options.name, unresolved);
287
+ const config = readToolkitConfig(tree);
288
+ const frontend = resolveFrontendForApp(options.frontend, config, app.root, tree);
289
+ const layerDir = layerDirFor(layer, app.sourceRoot, frontend);
290
+ const locale = resolveLocaleOption(options.locale, config);
291
+ const plan = composeSlice({
292
+ layer,
293
+ name: options.name,
294
+ publicApiName: resolveSlicePublicApiName(options.name, options.publicApiName),
295
+ locale,
296
+ sourceRoot: app.sourceRoot,
297
+ layerDirName: layerDirNameFor(layer, frontend),
298
+ segments
299
+ });
300
+ const action = computeSliceAction(tree, plan);
301
+ if (action === "created" && !dryRun) {
302
+ for (const file of plan.files) tree.write(file.filePath, file.content);
303
+ }
304
+ return {
305
+ dryRun,
306
+ layer,
307
+ name: options.name,
308
+ project: app.project,
309
+ layerDir,
310
+ directory: plan.directory,
311
+ publicApiName: plan.publicApiName,
312
+ refused: false,
313
+ refusalReason: null,
314
+ action,
315
+ files: plan.files
316
+ };
317
+ }
318
+
319
+ // src/generators/slice/render.ts
320
+ var ACTION_LINES = {
321
+ created: "created",
322
+ unchanged: "unchanged (already matches the generated content)",
323
+ skipped: "skipped \u2014 a slice with this name already exists with different content"
324
+ };
325
+ function renderSliceReport(report) {
326
+ const lines = [];
327
+ const label = `${report.layer} "${report.name}"`;
328
+ if (report.refused) {
329
+ lines.push(`${label}: refused \u2014 nothing was written.`);
330
+ lines.push("");
331
+ if (report.refusalReason === "invalid-name") {
332
+ lines.push(` "${report.name}" is not a valid kebab-case slice name.`);
333
+ lines.push(' Use lowercase letters, digits, and single hyphens, starting with a letter (e.g. "add-to-cart").');
334
+ } else if (report.refusalReason === "invalid-public-api-name") {
335
+ lines.push(" The public API name is not a usable JavaScript identifier (or is a reserved word).");
336
+ lines.push(' Pass --publicApiName with a valid identifier, e.g. "addToCart" (this also happens when');
337
+ lines.push(' the slice name camelCases into a reserved word, like "class" or "delete").');
338
+ } else if (report.refusalReason === "project-not-found") {
339
+ lines.push(` --project=${report.project} names no project in this workspace.`);
340
+ } else if (report.refusalReason === "ambiguous-frontend") {
341
+ lines.push(" More than one frontend app was found \u2014 rerun with --project=<one of them>:");
342
+ for (const name of report.candidates ?? []) lines.push(` - ${name}`);
343
+ } else if (report.refusalReason === "invalid-project-json") {
344
+ lines.push(` A project.json could not be parsed (${report.detail ?? "unknown"}) \u2014 fix it, then rerun.`);
345
+ } else if (report.refusalReason === "no-frontend-app") {
346
+ lines.push(" The workspace has no frontend application to hold FSD layers \u2014 create one, or pass --project.");
347
+ } else if (report.refusalReason === "invalid-segment") {
348
+ lines.push(` --segments: "${report.detail ?? ""}" is not a valid segment name.`);
349
+ lines.push(" Use lowercase letters, digits, and single hyphens, starting with a letter (e.g. ui, api, lib).");
350
+ } else if (report.refusalReason === "segments-on-shared") {
351
+ lines.push(" --segments does not apply to shared: a shared segment holds no segments of its own.");
352
+ lines.push(" Run the generator once per segment instead (e.g. --name=ui, then --name=api).");
353
+ } else {
354
+ const offender = report.detail ?? report.name;
355
+ lines.push(` "${offender}" names what a segment contains, not what it is for.`);
356
+ lines.push(" Canonical FSD names segments by purpose \u2014 ui, api, model, lib, config \u2014 so the code stays");
357
+ lines.push(" findable by what it does (e.g. a date helper lives in shared/lib, a Button in shared/ui).");
358
+ }
359
+ return lines.join("\n");
360
+ }
361
+ if (report.dryRun) {
362
+ lines.push("dry-run \u2014 no files were written.");
363
+ lines.push("");
364
+ }
365
+ lines.push(`${label}: ${ACTION_LINES[report.action]}`);
366
+ if (report.action === "skipped") {
367
+ lines.push(` ${report.directory} already exists and does not exactly match the generated content.`);
368
+ lines.push(" Pick a new slice name (a name collision is a signal to rename, not to merge)");
369
+ lines.push(" \u2014 or remove the existing directory manually first if you intended to replace it.");
370
+ return lines.join("\n");
371
+ }
372
+ if (report.dryRun) {
373
+ for (const file of report.files) {
374
+ lines.push("");
375
+ lines.push(`${file.filePath}:`);
376
+ lines.push(file.content);
377
+ }
378
+ }
379
+ return lines.join("\n");
380
+ }
381
+
382
+ // src/generators/shared/dry-run.ts
383
+ function resolveDryRun(options) {
384
+ return process.env["NX_DRY_RUN"] === "true" || (options.dryRun ?? false);
385
+ }
386
+
387
+ // src/generators/shared/generator.ts
388
+ async function sharedGenerator(tree, options) {
389
+ console.log(renderSliceReport(runFsdSlice(tree, "shared", { ...options, dryRun: resolveDryRun(options) })));
390
+ }
391
+ var generator_default = sharedGenerator;
392
+ export {
393
+ generator_default as default,
394
+ sharedGenerator
395
+ };
@@ -0,0 +1,39 @@
1
+ import { parseJson } from '@nx/devkit';
2
+ /**
3
+ * The project type Nx itself would use when a project declares none — the
4
+ * classic-preset Nx 23 layout configures apps through a bare package.json, so a
5
+ * filter on the declared `projectType` alone misses every one of them.
6
+ *
7
+ * Mirrors the project graph's rule (nx 23 core `normalize-project-nodes`
8
+ * `getProjectType`): a declared type wins (anything but `library` counts as an
9
+ * application, as in Nx); then `tsconfig.lib.json` → library; `tsconfig.app.json`
10
+ * → application; a readable package.json with none of exports/main/module/bin
11
+ * set → application; anything else — including a missing or unparseable
12
+ * package.json — → library. Mirrored, not imported: the plugin's only runtime
13
+ * dependency stays @nx/devkit. Not mirrored: the package-json plugin's
14
+ * `workspaceLayout.appsDir`/`libsDir` override, which devkit's tree-based
15
+ * `getProjects` never applies either.
16
+ */
17
+ export function resolveProjectType(tree, root, declared) {
18
+ if (declared)
19
+ return declared === 'library' ? 'library' : 'application';
20
+ const prefix = root === '' || root === '.' ? '' : `${root}/`;
21
+ if (tree.exists(`${prefix}tsconfig.lib.json`))
22
+ return 'library';
23
+ if (tree.exists(`${prefix}tsconfig.app.json`))
24
+ return 'application';
25
+ const raw = tree.read(`${prefix}package.json`, 'utf-8');
26
+ if (raw === null)
27
+ return 'library';
28
+ try {
29
+ // parseJson accepts comments and trailing commas, like Nx's readJsonFile
30
+ const manifest = parseJson(raw);
31
+ // truthiness, like Nx: an empty "main": "" is no entry point
32
+ if (manifest && !['exports', 'main', 'module', 'bin'].some((key) => Boolean(manifest[key])))
33
+ return 'application';
34
+ }
35
+ catch {
36
+ // unparseable — Nx's graph falls through to library as well
37
+ }
38
+ return 'library';
39
+ }
@@ -0,0 +1,34 @@
1
+ import { buildRecordPlan, getCatalogEntry, loadManifest, recordMetadataFor, verifyClaudeMarketplacePlugin, writeManifestAtomic, } from '@nx-archweave/integrations';
2
+ /**
3
+ * Live verify → manifest record, shared by record-integration and install-integration.
4
+ * Always a fresh call — never a cached result. Only a
5
+ * claude-cli-sourced verified:true is recordable (buildRecordPlan enforces it); the write
6
+ * is the atomic manifest write and happens only when `write` is set.
7
+ */
8
+ export async function runRecordPipeline(input) {
9
+ const entry = getCatalogEntry(input.id);
10
+ // Where it came from comes from the catalog entry.
11
+ const metadata = entry === undefined ? undefined : recordMetadataFor(entry);
12
+ if (!entry || !metadata) {
13
+ throw new Error(`No manifest metadata is defined for "${input.id}".`);
14
+ }
15
+ const { result: verification, source: verificationSource } = await verifyClaudeMarketplacePlugin(entry, { runner: input.runner }, { workspaceRoot: input.workspaceRoot, targetEnvironment: input.target });
16
+ const manifest = loadManifest(input.workspaceRoot);
17
+ const existingEntry = manifest.integrations[input.id];
18
+ const plan = buildRecordPlan({ id: input.id, target: input.target, verificationSource, verification, existingEntry, metadata });
19
+ let wasWritten = false;
20
+ if (input.write && plan.proposedEntry) {
21
+ writeManifestAtomic(input.workspaceRoot, {
22
+ integrations: { ...manifest.integrations, [input.id]: plan.proposedEntry },
23
+ });
24
+ wasWritten = true;
25
+ }
26
+ return {
27
+ verificationSource,
28
+ verification,
29
+ existingEntry,
30
+ proposedEntry: plan.proposedEntry,
31
+ hasConflict: plan.hasConflict,
32
+ wasWritten,
33
+ };
34
+ }
@@ -0,0 +1,24 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { packageManagerCommands } from '@nx-archweave/template-engine';
4
+ import { TOOLKIT_CONFIG_PATH, workspacePackageManager } from "./toolkit-config-file.js";
5
+ /** How a person runs the workspace's own nx — `pnpm nx`, `yarn nx`, `bun nx` or
6
+ * `npm exec nx --` — for the commands generators print. The detected package manager, then
7
+ * the one init recorded, else pnpm (the same order init, add-backend and add-review use). */
8
+ export function runNxFor(exists, read) {
9
+ let config = null;
10
+ try {
11
+ const parsed = JSON.parse(read(TOOLKIT_CONFIG_PATH) ?? 'null');
12
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed))
13
+ config = parsed;
14
+ }
15
+ catch {
16
+ // an unreadable config only means nothing is recorded
17
+ }
18
+ return packageManagerCommands(workspacePackageManager(exists, read, config) ?? 'pnpm').runNx;
19
+ }
20
+ /** From the workspace on disk — doctor and the integration generators, which act on the
21
+ * real workspace (tree.root) rather than the Nx Tree. */
22
+ export function diskRunNx(root) {
23
+ return runNxFor((path) => existsSync(join(root, path)), (path) => (existsSync(join(root, path)) ? readFileSync(join(root, path), 'utf-8') : undefined));
24
+ }
@@ -0,0 +1,38 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { generateFiles } from '@nx/devkit';
4
+ // createTree is the devkit's in-memory Tree; it holds the rendered files until the caller has
5
+ // compared each one with the workspace.
6
+ import { createTree } from '@nx/devkit/testing';
7
+ /**
8
+ * The `files/` directory of a generator. `fromUrl` is the calling module's `import.meta.url`:
9
+ * from source and from a generator's own bundle that is `../<generator>/files`; from a bundle
10
+ * that inlines another generator (init runs add-github-templates) the sibling directory is
11
+ * the same, and from the package's root `index.js` it is `./generators/<generator>/files`.
12
+ */
13
+ export function generatorFilesDir(generator, fromUrl) {
14
+ for (const relative of [`../${generator}/files`, `./generators/${generator}/files`]) {
15
+ const dir = fileURLToPath(new URL(relative, fromUrl));
16
+ if (existsSync(dir))
17
+ return dir;
18
+ }
19
+ throw new Error(`The "${generator}" generator's template files are missing next to ${fileURLToPath(fromUrl)}`);
20
+ }
21
+ /**
22
+ * Renders a template folder the way every Nx generator does — `generateFiles`: `__key__` in
23
+ * paths and EJS in contents take the substitutions, a `.template` suffix is dropped — into a
24
+ * scratch tree, and returns the files sorted by path. The caller decides what reaches the
25
+ * workspace (create, leave an existing file alone, or only report it in a dry run).
26
+ */
27
+ export function renderTemplateFiles(srcFolder, target, substitutions) {
28
+ const scratch = createTree();
29
+ // createTree seeds a .prettierrc; only what generateFiles adds belongs to the result.
30
+ const seeded = new Set(scratch.listChanges().map((change) => change.path));
31
+ generateFiles(scratch, srcFolder, target, substitutions);
32
+ return scratch
33
+ .listChanges()
34
+ .flatMap((change) => change.type === 'CREATE' && change.content !== null && !seeded.has(change.path)
35
+ ? [{ filePath: change.path, content: change.content.toString('utf-8') }]
36
+ : [])
37
+ .sort((a, b) => a.filePath.localeCompare(b.filePath));
38
+ }