@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.
- package/LICENSE +21 -0
- package/README.md +265 -2
- package/dist/bin/review-verdict.js +330 -0
- package/dist/eslint/fsd-boundaries.d.ts +119 -0
- package/dist/eslint/fsd-boundaries.js +144 -0
- package/dist/eslint/index.d.ts +78 -0
- package/dist/eslint/index.js +256 -0
- package/dist/eslint/tsconfig-aliases.d.ts +18 -0
- package/dist/eslint/tsconfig-aliases.js +114 -0
- package/dist/generators/add-agent-rules/generator.js +550 -0
- package/dist/generators/add-agent-rules/render.js +46 -0
- package/dist/generators/add-backend/generator.js +784 -0
- package/dist/generators/add-backend/render.js +102 -0
- package/dist/generators/add-fsd-lite/generator.js +553 -0
- package/dist/generators/add-fsd-lite/render.js +72 -0
- package/dist/generators/add-github-templates/files/en/codeowners/.github/CODEOWNERS.template +4 -0
- package/dist/generators/add-github-templates/files/en/issue/.github/ISSUE_TEMPLATE/bug-report.yml.template +28 -0
- package/dist/generators/add-github-templates/files/en/issue/.github/ISSUE_TEMPLATE/feature-request.yml.template +17 -0
- package/dist/generators/add-github-templates/files/en/pr/.github/PULL_REQUEST_TEMPLATE.md.template +12 -0
- package/dist/generators/add-github-templates/files/ko/codeowners/.github/CODEOWNERS.template +4 -0
- package/dist/generators/add-github-templates/files/ko/issue/.github/ISSUE_TEMPLATE/bug-report.yml.template +28 -0
- package/dist/generators/add-github-templates/files/ko/issue/.github/ISSUE_TEMPLATE/feature-request.yml.template +17 -0
- package/dist/generators/add-github-templates/files/ko/pr/.github/PULL_REQUEST_TEMPLATE.md.template +12 -0
- package/dist/generators/add-github-templates/generator.js +125 -0
- package/dist/generators/add-github-templates/render.js +26 -0
- package/dist/generators/add-integration/generator.js +818 -0
- package/dist/generators/add-integration/render.js +34 -0
- package/dist/generators/add-review/generator.js +921 -0
- package/dist/generators/add-review/render.js +96 -0
- package/dist/generators/allow-builds/generator.js +436 -0
- package/dist/generators/allow-builds/render.js +82 -0
- package/dist/generators/doctor/generator.js +2431 -0
- package/dist/generators/doctor/integration-report.js +70 -0
- package/dist/generators/doctor/render.js +10 -0
- package/dist/generators/entity/generator.js +395 -0
- package/dist/generators/feature/generator.js +395 -0
- package/dist/generators/init/generator.js +3211 -0
- package/dist/generators/init/render.js +125 -0
- package/dist/generators/init/validation.js +74 -0
- package/dist/generators/install-integration/generator.js +1233 -0
- package/dist/generators/install-integration/render.js +128 -0
- package/dist/generators/page/generator.js +395 -0
- package/dist/generators/record-integration/generator.js +852 -0
- package/dist/generators/record-integration/render.js +31 -0
- package/dist/generators/shared/dry-run.js +12 -0
- package/dist/generators/shared/frontend-app.js +57 -0
- package/dist/generators/shared/generator.js +395 -0
- package/dist/generators/shared/project-type.js +39 -0
- package/dist/generators/shared/record-pipeline.js +34 -0
- package/dist/generators/shared/run-nx.js +24 -0
- package/dist/generators/shared/template-files.js +38 -0
- package/dist/generators/shared/toolkit-config-file.js +110 -0
- package/dist/generators/slice/core.js +185 -0
- package/dist/generators/slice/files/index/index.ts.template +14 -0
- package/dist/generators/slice/files/module/__name__.spec.ts.template +11 -0
- package/dist/generators/slice/files/module/__name__.ts.template +11 -0
- package/dist/generators/slice/files/segment/index.ts.template +20 -0
- package/dist/generators/slice/render.js +70 -0
- package/dist/generators/widget/generator.js +395 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +680 -0
- package/dist/lib/nx-plugin.d.ts +2 -0
- package/dist/lib/nx-plugin.js +7 -0
- package/dist/runtime/integrations-composition-root.js +20 -0
- package/dist/runtime/node-command-runner.js +81 -0
- package/generators.json +85 -0
- package/package.json +95 -4
- package/src/generators/add-agent-rules/schema.json +43 -0
- package/src/generators/add-backend/schema.json +39 -0
- package/src/generators/add-fsd-lite/schema.json +43 -0
- package/src/generators/add-github-templates/schema.json +35 -0
- package/src/generators/add-integration/schema.json +35 -0
- package/src/generators/add-review/schema.json +72 -0
- package/src/generators/allow-builds/schema.json +20 -0
- package/src/generators/doctor/schema.json +20 -0
- package/src/generators/entity/schema.json +37 -0
- package/src/generators/feature/schema.json +37 -0
- package/src/generators/init/schema.json +98 -0
- package/src/generators/install-integration/schema.json +35 -0
- package/src/generators/page/schema.json +53 -0
- package/src/generators/record-integration/schema.json +26 -0
- package/src/generators/shared/schema.json +32 -0
- package/src/generators/widget/schema.json +45 -0
|
@@ -0,0 +1,3211 @@
|
|
|
1
|
+
// src/generators/init/generator.ts
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join as join2 } from "node:path";
|
|
5
|
+
|
|
6
|
+
// ../shared/dist/lib/toolkit-config.js
|
|
7
|
+
function readFsdEnforcement(fsd) {
|
|
8
|
+
if (typeof fsd !== "object" || fsd === null)
|
|
9
|
+
return void 0;
|
|
10
|
+
const enforcement = fsd["enforcement"];
|
|
11
|
+
return enforcement === "error" || enforcement === "warn" ? enforcement : void 0;
|
|
12
|
+
}
|
|
13
|
+
var DEFAULT_CONFLICT_POLICY = "merge-proposal";
|
|
14
|
+
|
|
15
|
+
// ../shared/dist/lib/spawnable-command.js
|
|
16
|
+
import { statSync } from "node:fs";
|
|
17
|
+
import { win32 } from "node:path";
|
|
18
|
+
function isFile(path) {
|
|
19
|
+
try {
|
|
20
|
+
return statSync(path).isFile();
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
var DEFAULT_ENV = { platform: process.platform, env: process.env, isFile };
|
|
26
|
+
function envValue(env, name) {
|
|
27
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
28
|
+
return key === void 0 ? void 0 : env[key];
|
|
29
|
+
}
|
|
30
|
+
function resolveWindowsCommand(command, environment) {
|
|
31
|
+
const exts = (envValue(environment.env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
32
|
+
const candidates = (base) => win32.extname(base) !== "" ? [base] : exts.map((ext) => base + ext);
|
|
33
|
+
if (win32.isAbsolute(command) || /[\\/]/.test(command)) {
|
|
34
|
+
return candidates(command).find(environment.isFile);
|
|
35
|
+
}
|
|
36
|
+
const dirs = (envValue(environment.env, "PATH") ?? "").split(";").filter(Boolean);
|
|
37
|
+
for (const dir of dirs) {
|
|
38
|
+
const found = candidates(win32.join(dir, command)).find(environment.isFile);
|
|
39
|
+
if (found !== void 0)
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
var META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
45
|
+
var BIN_SHIM = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i;
|
|
46
|
+
function escapeCmdArgument(arg, doubleEscape = false) {
|
|
47
|
+
let quoted = arg.replace(/(\\*)"/g, '$1$1\\"');
|
|
48
|
+
quoted = quoted.replace(/(\\*)$/, "$1$1");
|
|
49
|
+
const escaped = `"${quoted}"`.replace(META_CHARS, "^$1");
|
|
50
|
+
return doubleEscape ? escaped.replace(META_CHARS, "^$1") : escaped;
|
|
51
|
+
}
|
|
52
|
+
function spawnableCommand(command, args, environment = DEFAULT_ENV) {
|
|
53
|
+
if (environment.platform !== "win32")
|
|
54
|
+
return { command, args: [...args] };
|
|
55
|
+
const resolved = resolveWindowsCommand(command, environment);
|
|
56
|
+
if (resolved === void 0 || !/\.(cmd|bat)$/i.test(resolved))
|
|
57
|
+
return { command, args: [...args] };
|
|
58
|
+
const doubleEscape = BIN_SHIM.test(resolved);
|
|
59
|
+
const line = [resolved.replace(META_CHARS, "^$1"), ...args.map((arg) => escapeCmdArgument(arg, doubleEscape))].join(" ");
|
|
60
|
+
return {
|
|
61
|
+
command: envValue(environment.env, "COMSPEC") ?? "cmd.exe",
|
|
62
|
+
args: ["/d", "/s", "/c", `"${line}"`],
|
|
63
|
+
windowsVerbatimArguments: true
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ../template-engine/dist/environment/package-manager.js
|
|
68
|
+
var LOCKFILES = [
|
|
69
|
+
["yarn", ["yarn.lock"]],
|
|
70
|
+
["bun", ["bun.lock", "bun.lockb"]],
|
|
71
|
+
["npm", ["package-lock.json"]]
|
|
72
|
+
];
|
|
73
|
+
function detectWorkspacePackageManager(exists, read = () => void 0) {
|
|
74
|
+
if (exists("pnpm-lock.yaml") || exists("pnpm-workspace.yaml"))
|
|
75
|
+
return "pnpm";
|
|
76
|
+
const found = LOCKFILES.filter(([, files]) => files.some((file) => exists(file))).map(([pm]) => pm);
|
|
77
|
+
if (found.length <= 1)
|
|
78
|
+
return found[0] ?? null;
|
|
79
|
+
const declared = /"packageManager"\s*:\s*"(npm|yarn|bun)@/.exec(read("package.json") ?? "")?.[1];
|
|
80
|
+
const named = found.find((pm) => pm === declared);
|
|
81
|
+
if (named)
|
|
82
|
+
return named;
|
|
83
|
+
return found.includes("npm") ? "npm" : found[0];
|
|
84
|
+
}
|
|
85
|
+
function detectYarnFlavor(exists, read) {
|
|
86
|
+
if (exists(".yarnrc.yml"))
|
|
87
|
+
return "berry";
|
|
88
|
+
if (/^__metadata:/m.test(read("yarn.lock") ?? ""))
|
|
89
|
+
return "berry";
|
|
90
|
+
const major = /"packageManager"\s*:\s*"yarn@(\d+)\./.exec(read("package.json") ?? "")?.[1];
|
|
91
|
+
return major !== void 0 && Number(major) >= 2 ? "berry" : "classic";
|
|
92
|
+
}
|
|
93
|
+
function packageManagerCommands(pm, flavor = "classic") {
|
|
94
|
+
switch (pm) {
|
|
95
|
+
case "pnpm":
|
|
96
|
+
return { addDevAtRoot: (spec) => `pnpm add -D -w ${spec}`, addAtRoot: (spec) => `pnpm add -w ${spec}`, runNx: "pnpm nx", install: "pnpm install", workspaceRegistration: "pnpm-workspace.yaml" };
|
|
97
|
+
case "yarn":
|
|
98
|
+
return {
|
|
99
|
+
addDevAtRoot: (spec) => flavor === "berry" ? `yarn add -D ${spec}` : `yarn add -D -W ${spec}`,
|
|
100
|
+
addAtRoot: (spec) => flavor === "berry" ? `yarn add ${spec}` : `yarn add -W ${spec}`,
|
|
101
|
+
runNx: "yarn nx",
|
|
102
|
+
install: "yarn install",
|
|
103
|
+
workspaceRegistration: "package.json workspaces"
|
|
104
|
+
};
|
|
105
|
+
case "bun":
|
|
106
|
+
return { addDevAtRoot: (spec) => `bun add -d ${spec}`, addAtRoot: (spec) => `bun add ${spec}`, runNx: "bun nx", install: "bun install", workspaceRegistration: "package.json workspaces" };
|
|
107
|
+
case "npm":
|
|
108
|
+
return { addDevAtRoot: (spec) => `npm install -D ${spec}`, addAtRoot: (spec) => `npm install ${spec}`, runNx: "npm exec nx --", install: "npm install", workspaceRegistration: "package.json workspaces" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ../review-policy/dist/schema/review-policy-config.js
|
|
113
|
+
var DEFAULT_MAX_ROUNDS = 3;
|
|
114
|
+
|
|
115
|
+
// ../review-policy/dist/modes/review-modes.js
|
|
116
|
+
var REVIEW_MODES = {
|
|
117
|
+
fast: {
|
|
118
|
+
mode: "fast",
|
|
119
|
+
promptStyle: "fast-condensed",
|
|
120
|
+
includesCorrectnessSecurity: true,
|
|
121
|
+
includesOverEngineering: false
|
|
122
|
+
},
|
|
123
|
+
standard: {
|
|
124
|
+
mode: "standard",
|
|
125
|
+
promptStyle: "sectioned",
|
|
126
|
+
includesCorrectnessSecurity: true,
|
|
127
|
+
includesOverEngineering: false
|
|
128
|
+
},
|
|
129
|
+
thorough: {
|
|
130
|
+
mode: "thorough",
|
|
131
|
+
promptStyle: "sectioned",
|
|
132
|
+
includesCorrectnessSecurity: true,
|
|
133
|
+
includesOverEngineering: true
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ../review-policy/dist/verdict-gate/findings-block.js
|
|
138
|
+
var FINDINGS_MARKER = "<!-- archweave-findings-v1 -->";
|
|
139
|
+
var FINDINGS_SCHEMA_ID = "archweave/findings@1";
|
|
140
|
+
|
|
141
|
+
// ../review-policy/dist/prompts/prompt-shared.js
|
|
142
|
+
function renderFindingSchemaInstruction(locale, priorities) {
|
|
143
|
+
const intro = locale === "en" ? "For every finding, fill in every field of this schema:" : "\uBAA8\uB4E0 finding\uC5D0 \uB300\uD574 \uC544\uB798 \uC2A4\uD0A4\uB9C8\uC758 \uBAA8\uB4E0 \uD544\uB4DC\uB97C \uCC44\uC6B0\uC138\uC694:";
|
|
144
|
+
return [
|
|
145
|
+
intro,
|
|
146
|
+
`- priority: ${priorities.join(" | ")}`,
|
|
147
|
+
"- category: kebab-case category name",
|
|
148
|
+
"- title: one-line summary",
|
|
149
|
+
"- file: path of the affected file",
|
|
150
|
+
"- lineRange: { start, end? }",
|
|
151
|
+
"- evidence: what in the code proves the problem",
|
|
152
|
+
"- impact: what goes wrong and for whom",
|
|
153
|
+
"- reproductionCondition: the exact input/state that triggers it",
|
|
154
|
+
"- recommendation: the concrete fix",
|
|
155
|
+
"- confidence: confirmed | plausible"
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
158
|
+
function renderFindingsBlockInstruction(locale) {
|
|
159
|
+
const lines = locale === "en" ? [
|
|
160
|
+
"At the END of the top-level summary comment, append the machine-readable findings block:",
|
|
161
|
+
`the literal marker line ${FINDINGS_MARKER}, then a fenced \`\`\`json block containing`,
|
|
162
|
+
`{ "schema": "${FINDINGS_SCHEMA_ID}", "findings": [ ... ] } with one entry per finding,`,
|
|
163
|
+
"using exactly the finding schema above. If there are no findings, still append the block with an empty findings array."
|
|
164
|
+
] : [
|
|
165
|
+
"top-level \uC694\uC57D \uCF54\uBA58\uD2B8\uC758 \uB9E8 \uB05D\uC5D0 \uAE30\uACC4 \uD310\uB3C5\uC6A9 findings \uBE14\uB85D\uC744 \uB367\uBD99\uC774\uC138\uC694:",
|
|
166
|
+
`\uB9AC\uD130\uB7F4 \uB9C8\uCEE4 \uC904 ${FINDINGS_MARKER} \uB2E4\uC74C\uC5D0 fenced \`\`\`json \uBE14\uB85D\uC73C\uB85C`,
|
|
167
|
+
`{ "schema": "${FINDINGS_SCHEMA_ID}", "findings": [ ... ] }\uB97C, finding\uB9C8\uB2E4 \uD55C \uC6D0\uC18C\uC529, \uC704 finding \uC2A4\uD0A4\uB9C8 \uADF8\uB300\uB85C \uC791\uC131\uD558\uC138\uC694.`,
|
|
168
|
+
"finding\uC774 \uC5C6\uC5B4\uB3C4 \uBE48 \uBC30\uC5F4\uB85C \uBE14\uB85D\uC744 \uBC18\uB4DC\uC2DC \uD3EC\uD568\uD558\uC138\uC694."
|
|
169
|
+
];
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
function renderDedupInstruction(locale) {
|
|
173
|
+
return locale === "en" ? "If multiple findings share the same root cause, merge them into a single finding instead of reporting duplicates." : "\uAC19\uC740 \uC6D0\uC778\uC5D0\uC11C \uB098\uC628 finding\uC774 \uC5EC\uB7EC \uAC1C\uB77C\uBA74 \uC911\uBCF5 \uBCF4\uACE0\uD558\uC9C0 \uB9D0\uACE0 \uD558\uB098\uC758 finding\uC73C\uB85C \uD569\uCCD0\uC11C \uBCF4\uACE0\uD558\uC138\uC694.";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ../review-policy/dist/prompts/correctness-security-prompt.js
|
|
177
|
+
var CORRECTNESS_SECURITY_PRIORITIES = ["P0", "P1", "P2", "Nit"];
|
|
178
|
+
var FAST_PRIORITIES = ["P0", "P1", "P2"];
|
|
179
|
+
function buildCorrectnessSecurityPrompt(locale, style = "sectioned") {
|
|
180
|
+
if (style === "fast-condensed") {
|
|
181
|
+
const fastBody = locale === "en" ? [
|
|
182
|
+
"You are a code reviewer focused on correctness and security.",
|
|
183
|
+
"",
|
|
184
|
+
"Review the provided diff and report only the most critical problems. Skip minor style and taste remarks entirely \u2014 report nothing below P2.",
|
|
185
|
+
"",
|
|
186
|
+
`Only report findings with priority ${FAST_PRIORITIES.join(", ")}. Do NOT report P3 (over-engineering) findings \u2014 they are out of scope here.`
|
|
187
|
+
] : [
|
|
188
|
+
"\uB2F9\uC2E0\uC740 \uC815\uD655\uC131(correctness)\uACFC \uBCF4\uC548(security)\uC5D0 \uC9D1\uC911\uD558\uB294 \uCF54\uB4DC \uB9AC\uBDF0\uC5B4\uC785\uB2C8\uB2E4.",
|
|
189
|
+
"",
|
|
190
|
+
"\uC8FC\uC5B4\uC9C4 diff\uC5D0\uC11C \uAC00\uC7A5 \uCE58\uBA85\uC801\uC778 \uBB38\uC81C\uB9CC \uBCF4\uACE0\uD558\uC138\uC694. \uC0AC\uC18C\uD55C \uC2A4\uD0C0\uC77C\xB7\uCDE8\uD5A5 \uC9C0\uC801\uC740 \uC644\uC804\uD788 \uC0DD\uB7B5\uD558\uACE0, P2 \uBBF8\uB9CC\uC740 \uBCF4\uACE0\uD558\uC9C0 \uB9C8\uC138\uC694.",
|
|
191
|
+
"",
|
|
192
|
+
`\uC6B0\uC120\uC21C\uC704 ${FAST_PRIORITIES.join(", ")}\uC5D0 \uD574\uB2F9\uD558\uB294 finding\uB9CC \uBCF4\uACE0\uD558\uC138\uC694. P3(\uACFC\uC789 \uC124\uACC4)\uB294 \uBCF4\uACE0\uD558\uC9C0 \uB9C8\uC138\uC694 \u2014 \uC774 \uB9AC\uBDF0\uC758 \uBC94\uC704 \uBC16\uC785\uB2C8\uB2E4.`
|
|
193
|
+
];
|
|
194
|
+
return [...fastBody, "", renderFindingSchemaInstruction(locale, FAST_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
195
|
+
}
|
|
196
|
+
const body = locale === "en" ? [
|
|
197
|
+
"You are a code reviewer focused on correctness and security.",
|
|
198
|
+
"",
|
|
199
|
+
'Review the provided diff and report findings in two explicitly separated top-level sections: "Correctness" and "Security". Never mix the two.',
|
|
200
|
+
"",
|
|
201
|
+
`Only report findings with priority ${CORRECTNESS_SECURITY_PRIORITIES.join(", ")}. Do NOT report P3 (over-engineering) findings \u2014 they are covered by a separate reviewer and are out of scope here.`
|
|
202
|
+
] : [
|
|
203
|
+
"\uB2F9\uC2E0\uC740 \uC815\uD655\uC131(correctness)\uACFC \uBCF4\uC548(security)\uC5D0 \uC9D1\uC911\uD558\uB294 \uCF54\uB4DC \uB9AC\uBDF0\uC5B4\uC785\uB2C8\uB2E4.",
|
|
204
|
+
"",
|
|
205
|
+
'\uC8FC\uC5B4\uC9C4 diff\uB97C \uB9AC\uBDF0\uD558\uACE0, finding\uC744 "Correctness"\uC640 "Security" \uB450 \uAC1C\uC758 \uCD5C\uC0C1\uC704 \uC139\uC158\uC73C\uB85C \uBD84\uB9AC\uD574 \uBCF4\uACE0\uD558\uC138\uC694. \uB450 \uC139\uC158\uC744 \uC808\uB300 \uC11E\uC9C0 \uB9C8\uC138\uC694.',
|
|
206
|
+
"",
|
|
207
|
+
`\uC6B0\uC120\uC21C\uC704 ${CORRECTNESS_SECURITY_PRIORITIES.join(", ")}\uC5D0 \uD574\uB2F9\uD558\uB294 finding\uB9CC \uBCF4\uACE0\uD558\uC138\uC694. P3\uB294 \uBCF4\uACE0\uD558\uC9C0 \uB9C8\uC138\uC694 \u2014 P3(\uACFC\uC789 \uC124\uACC4)\uB294 \uBCC4\uB3C4\uC758 \uB9AC\uBDF0\uC5B4\uAC00 \uB2F4\uB2F9\uD558\uBBC0\uB85C \uC774 \uB9AC\uBDF0\uC758 \uBC94\uC704 \uBC16\uC785\uB2C8\uB2E4.`
|
|
208
|
+
];
|
|
209
|
+
return [...body, "", renderFindingSchemaInstruction(locale, CORRECTNESS_SECURITY_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ../review-policy/dist/prompts/over-engineering-prompt.js
|
|
213
|
+
var OVER_ENGINEERING_PRIORITIES = ["P3"];
|
|
214
|
+
function buildOverEngineeringPrompt(locale) {
|
|
215
|
+
const body = locale === "en" ? [
|
|
216
|
+
"You are a code reviewer focused exclusively on over-engineering.",
|
|
217
|
+
"",
|
|
218
|
+
"Review the provided diff and report findings with priority P3 only. Do NOT report P0, P1, P2, or Nit findings \u2014 correctness and security are covered by a separate reviewer.",
|
|
219
|
+
"",
|
|
220
|
+
'Report your findings in their own top-level section titled "Over-engineering"; never mix them into correctness or security sections.'
|
|
221
|
+
] : [
|
|
222
|
+
"\uB2F9\uC2E0\uC740 \uACFC\uC789 \uC124\uACC4(over-engineering)\uB9CC\uC744 \uC804\uB2F4\uD558\uB294 \uCF54\uB4DC \uB9AC\uBDF0\uC5B4\uC785\uB2C8\uB2E4.",
|
|
223
|
+
"",
|
|
224
|
+
"\uC8FC\uC5B4\uC9C4 diff\uB97C \uB9AC\uBDF0\uD558\uACE0 \uC6B0\uC120\uC21C\uC704 P3\uB9CC \uBCF4\uACE0\uD558\uC138\uC694. P0, P1, P2, Nit\uC740 \uBCF4\uACE0\uD558\uC9C0 \uB9C8\uC138\uC694 \u2014 \uC815\uD655\uC131\uACFC \uBCF4\uC548\uC740 \uBCC4\uB3C4\uC758 \uB9AC\uBDF0\uC5B4\uAC00 \uB2F4\uB2F9\uD569\uB2C8\uB2E4.",
|
|
225
|
+
"",
|
|
226
|
+
'finding\uC740 "Over-engineering"\uC774\uB77C\uB294 \uBCC4\uB3C4\uC758 \uCD5C\uC0C1\uC704 \uC139\uC158\uC73C\uB85C \uBCF4\uACE0\uD558\uACE0, correctness/security \uC139\uC158\uACFC \uC808\uB300 \uC11E\uC9C0 \uB9C8\uC138\uC694.'
|
|
227
|
+
];
|
|
228
|
+
return [...body, "", renderFindingSchemaInstruction(locale, OVER_ENGINEERING_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ../review-policy/dist/verdict-gate/verdict-gate.js
|
|
232
|
+
var ROUND_MARKER = "<!-- archweave-review-round -->";
|
|
233
|
+
|
|
234
|
+
// ../template-engine/dist/review/config-snippet.js
|
|
235
|
+
function buildReviewConfigSnippet(input) {
|
|
236
|
+
return {
|
|
237
|
+
enabled: true,
|
|
238
|
+
provider: input.provider,
|
|
239
|
+
mode: input.mode,
|
|
240
|
+
locale: input.locale,
|
|
241
|
+
maxRounds: input.maxRounds,
|
|
242
|
+
blockingPriorities: ["P0", "P1"]
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ../template-engine/dist/review/workflow-skeleton.js
|
|
247
|
+
function indentBlock(text, spaces) {
|
|
248
|
+
const pad = " ".repeat(spaces);
|
|
249
|
+
return text.split("\n").map((line) => line.length === 0 ? line : pad + line).join("\n");
|
|
250
|
+
}
|
|
251
|
+
function renderTriggerBlock() {
|
|
252
|
+
return ["on:", " pull_request:", " types: [opened, synchronize, ready_for_review, reopened]"].join("\n");
|
|
253
|
+
}
|
|
254
|
+
function renderConcurrencyBlock() {
|
|
255
|
+
return [
|
|
256
|
+
"concurrency:",
|
|
257
|
+
" group: archweave-review-${{ github.event.pull_request.number }}",
|
|
258
|
+
" cancel-in-progress: false"
|
|
259
|
+
].join("\n");
|
|
260
|
+
}
|
|
261
|
+
function renderPermissionsBlock() {
|
|
262
|
+
return ["permissions:", " contents: read", " pull-requests: write", " id-token: write"].join("\n");
|
|
263
|
+
}
|
|
264
|
+
function renderSecretGateStep(secretName) {
|
|
265
|
+
return [
|
|
266
|
+
"- name: Check review secret",
|
|
267
|
+
" id: secret-gate",
|
|
268
|
+
" env:",
|
|
269
|
+
` REVIEW_SECRET: \${{ secrets.${secretName} }}`,
|
|
270
|
+
" run: |",
|
|
271
|
+
' if [ -z "$REVIEW_SECRET" ]; then',
|
|
272
|
+
` echo "AI review is not active: register the ${secretName} secret to enable it."`,
|
|
273
|
+
' echo "enabled=false" >> "$GITHUB_OUTPUT"',
|
|
274
|
+
" else",
|
|
275
|
+
' echo "enabled=true" >> "$GITHUB_OUTPUT"',
|
|
276
|
+
" fi"
|
|
277
|
+
].join("\n");
|
|
278
|
+
}
|
|
279
|
+
function renderRoundGateStep(maxRounds) {
|
|
280
|
+
return [
|
|
281
|
+
"- name: Check review round limit",
|
|
282
|
+
" id: round-gate",
|
|
283
|
+
" if: steps.secret-gate.outputs.enabled == 'true'",
|
|
284
|
+
" env:",
|
|
285
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
286
|
+
" 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')`,
|
|
288
|
+
` rounds=$(printf '%s' "$counts" | cut -f1)`,
|
|
289
|
+
` echo "last-comment-id=$(printf '%s' "$counts" | cut -f2)" >> "$GITHUB_OUTPUT"`,
|
|
290
|
+
` if [ "$rounds" -ge ${maxRounds} ]; then`,
|
|
291
|
+
` echo "Automated review round limit (${maxRounds}) reached for this PR; skipping."`,
|
|
292
|
+
' echo "allowed=false" >> "$GITHUB_OUTPUT"',
|
|
293
|
+
" else",
|
|
294
|
+
' echo "allowed=true" >> "$GITHUB_OUTPUT"',
|
|
295
|
+
" fi"
|
|
296
|
+
].join("\n");
|
|
297
|
+
}
|
|
298
|
+
function renderVerdictGateStep(maxRounds, locale) {
|
|
299
|
+
return [
|
|
300
|
+
"- name: Compute review verdict",
|
|
301
|
+
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
302
|
+
" env:",
|
|
303
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
304
|
+
" GITHUB_REPOSITORY: ${{ github.repository }}",
|
|
305
|
+
" ARCHWEAVE_PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
306
|
+
` ARCHWEAVE_MAX_ROUNDS: '${maxRounds}'`,
|
|
307
|
+
` ARCHWEAVE_LOCALE: '${locale}'`,
|
|
308
|
+
" ARCHWEAVE_HEAD_SHA: ${{ github.event.pull_request.head.sha }}",
|
|
309
|
+
" ARCHWEAVE_ROUND_AFTER_ID: ${{ steps.round-gate.outputs.last-comment-id }}",
|
|
310
|
+
" run: |",
|
|
311
|
+
" if [ -x node_modules/.bin/archweave-review-verdict ]; then",
|
|
312
|
+
" node_modules/.bin/archweave-review-verdict",
|
|
313
|
+
" else",
|
|
314
|
+
' echo "nx-archweave is not installed in this workspace - skipping the verdict gate."',
|
|
315
|
+
" fi"
|
|
316
|
+
].join("\n");
|
|
317
|
+
}
|
|
318
|
+
var SETUP_NODE_VERSION = "22";
|
|
319
|
+
var SETUP_PNPM_VERSION = "10";
|
|
320
|
+
var SETUP_BUN_ACTION = "oven-sh/setup-bun@v2";
|
|
321
|
+
var INSTALL_LINES = {
|
|
322
|
+
pnpm: [" pnpm install --frozen-lockfile"],
|
|
323
|
+
npm: [" npm ci"],
|
|
324
|
+
yarn: [
|
|
325
|
+
" corepack enable yarn",
|
|
326
|
+
' if [ "$(yarn --version | cut -d. -f1)" -ge 2 ]; then yarn install --immutable; else yarn install --frozen-lockfile; fi'
|
|
327
|
+
],
|
|
328
|
+
bun: [" bun install --frozen-lockfile"]
|
|
329
|
+
};
|
|
330
|
+
function renderWorkspaceSetupSteps(packageManager) {
|
|
331
|
+
const gate = " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
|
|
332
|
+
const steps = [];
|
|
333
|
+
if (packageManager === "pnpm") {
|
|
334
|
+
steps.push(["- name: Set up pnpm", gate, " uses: pnpm/action-setup@v4", " with:", ` version: ${SETUP_PNPM_VERSION}`].join("\n"));
|
|
335
|
+
}
|
|
336
|
+
if (packageManager === "bun") {
|
|
337
|
+
steps.push(["- name: Set up bun", gate, ` uses: ${SETUP_BUN_ACTION}`].join("\n"));
|
|
338
|
+
}
|
|
339
|
+
steps.push([
|
|
340
|
+
"- name: Set up Node",
|
|
341
|
+
gate,
|
|
342
|
+
" uses: actions/setup-node@v4",
|
|
343
|
+
" with:",
|
|
344
|
+
` node-version: ${SETUP_NODE_VERSION}`,
|
|
345
|
+
// yarn: no setup-node cache — it would query the runner's global yarn 1 even in a
|
|
346
|
+
// berry project. Corepack (shipped with Node) provides the project's own yarn below.
|
|
347
|
+
// bun: setup-node has no bun cache. Node is still set up — nx and the verdict bin
|
|
348
|
+
// run on it.
|
|
349
|
+
...packageManager === "yarn" || packageManager === "bun" ? [] : [` cache: ${packageManager}`]
|
|
350
|
+
].join("\n"));
|
|
351
|
+
steps.push([
|
|
352
|
+
"- name: Install workspace dependencies",
|
|
353
|
+
gate,
|
|
354
|
+
" run: |",
|
|
355
|
+
...INSTALL_LINES[packageManager]
|
|
356
|
+
].join("\n"));
|
|
357
|
+
return steps.join("\n\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ../template-engine/dist/review/claude/claude-provider.js
|
|
361
|
+
var SECRET_NAME = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
362
|
+
var ALLOWED_TOOLS = "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)";
|
|
363
|
+
function renderWorkflowInstructions(locale) {
|
|
364
|
+
const lines = locale === "en" ? [
|
|
365
|
+
"Review the diff of this pull request using `gh pr diff` and `gh pr view`.",
|
|
366
|
+
"Post specific findings as inline comments and exactly one top-level summary comment.",
|
|
367
|
+
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
368
|
+
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
369
|
+
"Do not modify any code, and never approve or merge the pull request.",
|
|
370
|
+
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
371
|
+
] : [
|
|
372
|
+
"\uC774 pull request\uC758 diff\uB97C `gh pr diff`\uC640 `gh pr view`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
|
|
373
|
+
"\uAD6C\uCCB4\uC801 \uC9C0\uC801\uC740 inline comment\uB85C, \uC804\uCCB4 \uC694\uC57D\uC740 \uC815\uD655\uD788 \uD55C \uAC1C\uC758 top-level comment\uB85C \uAC8C\uC2DC\uD558\uC138\uC694.",
|
|
374
|
+
`top-level \uC694\uC57D \uCF54\uBA58\uD2B8\uC5D0\uB294 \uBC18\uB4DC\uC2DC \uB9AC\uD130\uB7F4 \uB9C8\uCEE4 ${ROUND_MARKER}\uB97C \uD3EC\uD568\uD558\uC138\uC694.`,
|
|
375
|
+
'\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uAC00 \uC5C6\uC73C\uBA74 "\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."\uB77C\uB294 top-level comment\uB97C \uC815\uD655\uD788 \uD55C \uAC1C \uAC8C\uC2DC\uD558\uC138\uC694(\uC774\uB54C\uB3C4 \uB9C8\uCEE4\uB294 \uD3EC\uD568).',
|
|
376
|
+
"\uCF54\uB4DC\uB97C \uC218\uC815\uD558\uC9C0 \uB9C8\uC138\uC694(Do not modify any code). PR\uC744 \uC2B9\uC778\uD558\uAC70\uB098 \uBCD1\uD569\uD558\uC9C0 \uB9C8\uC138\uC694(never approve or merge).",
|
|
377
|
+
"\uC694\uC57D\uACFC \uC124\uBA85\uC740 \uD55C\uAD6D\uC5B4\uB85C \uC4F0\uB418, \uD30C\uC77C\uBA85\xB7\uD568\uC218\uBA85\xB7\uC2DD\uBCC4\uC790\uB294 \uC6D0\uBB38 \uADF8\uB300\uB85C \uC720\uC9C0\uD558\uC138\uC694."
|
|
378
|
+
];
|
|
379
|
+
return lines.join("\n");
|
|
380
|
+
}
|
|
381
|
+
function composePrompt(input) {
|
|
382
|
+
const mode = REVIEW_MODES[input.mode];
|
|
383
|
+
const sections = [renderWorkflowInstructions(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
384
|
+
if (mode.includesOverEngineering) {
|
|
385
|
+
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
386
|
+
}
|
|
387
|
+
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
388
|
+
return sections.join("\n\n---\n\n");
|
|
389
|
+
}
|
|
390
|
+
function buildWorkflowYaml(input) {
|
|
391
|
+
const steps = [
|
|
392
|
+
renderSecretGateStep(SECRET_NAME),
|
|
393
|
+
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"),
|
|
401
|
+
[
|
|
402
|
+
"- name: Run Claude review",
|
|
403
|
+
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
404
|
+
" uses: anthropics/claude-code-action@v1",
|
|
405
|
+
" with:",
|
|
406
|
+
` claude_code_oauth_token: \${{ secrets.${SECRET_NAME} }}`,
|
|
407
|
+
" prompt: |",
|
|
408
|
+
indentBlock(composePrompt(input), 6),
|
|
409
|
+
" claude_args: |",
|
|
410
|
+
` --allowedTools "${ALLOWED_TOOLS}"`
|
|
411
|
+
].join("\n"),
|
|
412
|
+
renderWorkspaceSetupSteps(input.packageManager),
|
|
413
|
+
renderVerdictGateStep(input.maxRounds, input.locale)
|
|
414
|
+
];
|
|
415
|
+
return [
|
|
416
|
+
"name: Claude Code Review",
|
|
417
|
+
"",
|
|
418
|
+
renderTriggerBlock(),
|
|
419
|
+
"",
|
|
420
|
+
renderConcurrencyBlock(),
|
|
421
|
+
"",
|
|
422
|
+
renderPermissionsBlock(),
|
|
423
|
+
"",
|
|
424
|
+
"jobs:",
|
|
425
|
+
" review:",
|
|
426
|
+
" runs-on: ubuntu-latest",
|
|
427
|
+
" steps:",
|
|
428
|
+
indentBlock(steps.join("\n\n"), 6),
|
|
429
|
+
""
|
|
430
|
+
].join("\n");
|
|
431
|
+
}
|
|
432
|
+
var claudeProvider = {
|
|
433
|
+
id: "claude",
|
|
434
|
+
workflowFileName: ".github/workflows/claude-code-review.yml",
|
|
435
|
+
requiredSecretName: SECRET_NAME,
|
|
436
|
+
buildWorkflowYaml
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// ../template-engine/dist/review/cursor/cursor-provider.js
|
|
440
|
+
var SECRET_NAME2 = "CURSOR_API_KEY";
|
|
441
|
+
var PROMPT_HEREDOC_DELIMITER = "ARCHWEAVE_CURSOR_REVIEW_PROMPT";
|
|
442
|
+
function renderWorkflowInstructions2(locale) {
|
|
443
|
+
const lines = locale === "en" ? [
|
|
444
|
+
"Review the diff of this pull request using `gh pr diff` and `gh pr view`.",
|
|
445
|
+
"Post specific findings as inline comments and exactly one top-level summary comment.",
|
|
446
|
+
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
447
|
+
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
448
|
+
"Do not modify any code, and never approve or merge the pull request.",
|
|
449
|
+
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
450
|
+
] : [
|
|
451
|
+
"\uC774 pull request\uC758 diff\uB97C `gh pr diff`\uC640 `gh pr view`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
|
|
452
|
+
"\uAD6C\uCCB4\uC801 \uC9C0\uC801\uC740 inline comment\uB85C, \uC804\uCCB4 \uC694\uC57D\uC740 \uC815\uD655\uD788 \uD55C \uAC1C\uC758 top-level comment\uB85C \uAC8C\uC2DC\uD558\uC138\uC694.",
|
|
453
|
+
`top-level \uC694\uC57D \uCF54\uBA58\uD2B8\uC5D0\uB294 \uBC18\uB4DC\uC2DC \uB9AC\uD130\uB7F4 \uB9C8\uCEE4 ${ROUND_MARKER}\uB97C \uD3EC\uD568\uD558\uC138\uC694.`,
|
|
454
|
+
'\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uAC00 \uC5C6\uC73C\uBA74 "\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."\uB77C\uB294 top-level comment\uB97C \uC815\uD655\uD788 \uD55C \uAC1C \uAC8C\uC2DC\uD558\uC138\uC694(\uC774\uB54C\uB3C4 \uB9C8\uCEE4\uB294 \uD3EC\uD568).',
|
|
455
|
+
"\uCF54\uB4DC\uB97C \uC218\uC815\uD558\uC9C0 \uB9C8\uC138\uC694(Do not modify any code). PR\uC744 \uC2B9\uC778\uD558\uAC70\uB098 \uBCD1\uD569\uD558\uC9C0 \uB9C8\uC138\uC694(never approve or merge).",
|
|
456
|
+
"\uC694\uC57D\uACFC \uC124\uBA85\uC740 \uD55C\uAD6D\uC5B4\uB85C \uC4F0\uB418, \uD30C\uC77C\uBA85\xB7\uD568\uC218\uBA85\xB7\uC2DD\uBCC4\uC790\uB294 \uC6D0\uBB38 \uADF8\uB300\uB85C \uC720\uC9C0\uD558\uC138\uC694."
|
|
457
|
+
];
|
|
458
|
+
return lines.join("\n");
|
|
459
|
+
}
|
|
460
|
+
function composePrompt2(input) {
|
|
461
|
+
const mode = REVIEW_MODES[input.mode];
|
|
462
|
+
const sections = [renderWorkflowInstructions2(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
463
|
+
if (mode.includesOverEngineering) {
|
|
464
|
+
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
465
|
+
}
|
|
466
|
+
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
467
|
+
return sections.join("\n\n---\n\n");
|
|
468
|
+
}
|
|
469
|
+
function buildWorkflowYaml2(input) {
|
|
470
|
+
const gateCondition = "steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
|
|
471
|
+
const steps = [
|
|
472
|
+
renderSecretGateStep(SECRET_NAME2),
|
|
473
|
+
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"),
|
|
481
|
+
[
|
|
482
|
+
"- name: Install cursor-agent CLI",
|
|
483
|
+
` if: ${gateCondition}`,
|
|
484
|
+
" run: |",
|
|
485
|
+
" curl https://cursor.com/install -fsS | bash",
|
|
486
|
+
' echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"'
|
|
487
|
+
].join("\n"),
|
|
488
|
+
[
|
|
489
|
+
"- name: Run Cursor review",
|
|
490
|
+
` if: ${gateCondition}`,
|
|
491
|
+
" env:",
|
|
492
|
+
` CURSOR_API_KEY: \${{ secrets.${SECRET_NAME2} }}`,
|
|
493
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
494
|
+
" run: |",
|
|
495
|
+
` agent -p "$(cat <<'${PROMPT_HEREDOC_DELIMITER}'`,
|
|
496
|
+
indentBlock(composePrompt2(input), 4),
|
|
497
|
+
` ${PROMPT_HEREDOC_DELIMITER}`,
|
|
498
|
+
' )"'
|
|
499
|
+
].join("\n"),
|
|
500
|
+
renderWorkspaceSetupSteps(input.packageManager),
|
|
501
|
+
renderVerdictGateStep(input.maxRounds, input.locale)
|
|
502
|
+
];
|
|
503
|
+
return [
|
|
504
|
+
"name: Cursor Code Review",
|
|
505
|
+
"",
|
|
506
|
+
renderTriggerBlock(),
|
|
507
|
+
"",
|
|
508
|
+
renderConcurrencyBlock(),
|
|
509
|
+
"",
|
|
510
|
+
renderPermissionsBlock(),
|
|
511
|
+
"",
|
|
512
|
+
"jobs:",
|
|
513
|
+
" review:",
|
|
514
|
+
" runs-on: ubuntu-latest",
|
|
515
|
+
" steps:",
|
|
516
|
+
indentBlock(steps.join("\n\n"), 6),
|
|
517
|
+
""
|
|
518
|
+
].join("\n");
|
|
519
|
+
}
|
|
520
|
+
var cursorProvider = {
|
|
521
|
+
id: "cursor",
|
|
522
|
+
workflowFileName: ".github/workflows/cursor-code-review.yml",
|
|
523
|
+
requiredSecretName: SECRET_NAME2,
|
|
524
|
+
buildWorkflowYaml: buildWorkflowYaml2
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// ../template-engine/dist/review/gemini/gemini-provider.js
|
|
528
|
+
var SECRET_NAME3 = "GEMINI_API_KEY";
|
|
529
|
+
function renderWorkflowInstructions3(locale) {
|
|
530
|
+
const lines = locale === "en" ? [
|
|
531
|
+
"Review the diff of this pull request using `gh pr diff` and `gh pr view`.",
|
|
532
|
+
"Post specific findings as inline comments and exactly one top-level summary comment.",
|
|
533
|
+
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
534
|
+
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
535
|
+
"Do not modify any code, and never approve or merge the pull request.",
|
|
536
|
+
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
537
|
+
] : [
|
|
538
|
+
"\uC774 pull request\uC758 diff\uB97C `gh pr diff`\uC640 `gh pr view`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
|
|
539
|
+
"\uAD6C\uCCB4\uC801 \uC9C0\uC801\uC740 inline comment\uB85C, \uC804\uCCB4 \uC694\uC57D\uC740 \uC815\uD655\uD788 \uD55C \uAC1C\uC758 top-level comment\uB85C \uAC8C\uC2DC\uD558\uC138\uC694.",
|
|
540
|
+
`top-level \uC694\uC57D \uCF54\uBA58\uD2B8\uC5D0\uB294 \uBC18\uB4DC\uC2DC \uB9AC\uD130\uB7F4 \uB9C8\uCEE4 ${ROUND_MARKER}\uB97C \uD3EC\uD568\uD558\uC138\uC694.`,
|
|
541
|
+
'\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uAC00 \uC5C6\uC73C\uBA74 "\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."\uB77C\uB294 top-level comment\uB97C \uC815\uD655\uD788 \uD55C \uAC1C \uAC8C\uC2DC\uD558\uC138\uC694(\uC774\uB54C\uB3C4 \uB9C8\uCEE4\uB294 \uD3EC\uD568).',
|
|
542
|
+
"\uCF54\uB4DC\uB97C \uC218\uC815\uD558\uC9C0 \uB9C8\uC138\uC694(Do not modify any code). PR\uC744 \uC2B9\uC778\uD558\uAC70\uB098 \uBCD1\uD569\uD558\uC9C0 \uB9C8\uC138\uC694(never approve or merge).",
|
|
543
|
+
"\uC694\uC57D\uACFC \uC124\uBA85\uC740 \uD55C\uAD6D\uC5B4\uB85C \uC4F0\uB418, \uD30C\uC77C\uBA85\xB7\uD568\uC218\uBA85\xB7\uC2DD\uBCC4\uC790\uB294 \uC6D0\uBB38 \uADF8\uB300\uB85C \uC720\uC9C0\uD558\uC138\uC694."
|
|
544
|
+
];
|
|
545
|
+
return lines.join("\n");
|
|
546
|
+
}
|
|
547
|
+
function composePrompt3(input) {
|
|
548
|
+
const mode = REVIEW_MODES[input.mode];
|
|
549
|
+
const sections = [renderWorkflowInstructions3(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
550
|
+
if (mode.includesOverEngineering) {
|
|
551
|
+
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
552
|
+
}
|
|
553
|
+
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
554
|
+
return sections.join("\n\n---\n\n");
|
|
555
|
+
}
|
|
556
|
+
function buildWorkflowYaml3(input) {
|
|
557
|
+
const steps = [
|
|
558
|
+
renderSecretGateStep(SECRET_NAME3),
|
|
559
|
+
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"),
|
|
567
|
+
[
|
|
568
|
+
"- name: Run Gemini review",
|
|
569
|
+
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
570
|
+
" uses: google-github-actions/run-gemini-cli@v0",
|
|
571
|
+
" env:",
|
|
572
|
+
" GITHUB_TOKEN: ${{ github.token }}",
|
|
573
|
+
" with:",
|
|
574
|
+
` gemini_api_key: \${{ secrets.${SECRET_NAME3} }}`,
|
|
575
|
+
" prompt: |",
|
|
576
|
+
indentBlock(composePrompt3(input), 6)
|
|
577
|
+
].join("\n"),
|
|
578
|
+
renderWorkspaceSetupSteps(input.packageManager),
|
|
579
|
+
renderVerdictGateStep(input.maxRounds, input.locale)
|
|
580
|
+
];
|
|
581
|
+
return [
|
|
582
|
+
"name: Gemini Code Review",
|
|
583
|
+
"",
|
|
584
|
+
renderTriggerBlock(),
|
|
585
|
+
"",
|
|
586
|
+
renderConcurrencyBlock(),
|
|
587
|
+
"",
|
|
588
|
+
renderPermissionsBlock(),
|
|
589
|
+
"",
|
|
590
|
+
"jobs:",
|
|
591
|
+
" review:",
|
|
592
|
+
" runs-on: ubuntu-latest",
|
|
593
|
+
" steps:",
|
|
594
|
+
indentBlock(steps.join("\n\n"), 6),
|
|
595
|
+
""
|
|
596
|
+
].join("\n");
|
|
597
|
+
}
|
|
598
|
+
var geminiProvider = {
|
|
599
|
+
id: "gemini",
|
|
600
|
+
workflowFileName: ".github/workflows/gemini-code-review.yml",
|
|
601
|
+
requiredSecretName: SECRET_NAME3,
|
|
602
|
+
buildWorkflowYaml: buildWorkflowYaml3
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// ../template-engine/dist/review/gpt/gpt-provider.js
|
|
606
|
+
var SECRET_NAME4 = "OPENAI_API_KEY";
|
|
607
|
+
function renderWorkflowInstructions4(locale) {
|
|
608
|
+
const lines = locale === "en" ? [
|
|
609
|
+
"Review ONLY the changes introduced by this pull request. The base and head commits are already fetched under fixed ref names: run `git diff origin/pr-base...origin/pr-head` (and `git log origin/pr-base..origin/pr-head` for commit context) \u2014 this sandboxed step has no network access, so do not attempt to run `gh` or reach the GitHub API.",
|
|
610
|
+
"Write your entire response as a single message: report specific findings, then close with one overall summary.",
|
|
611
|
+
`Always include the literal marker ${ROUND_MARKER} in your message.`,
|
|
612
|
+
"If there are no findings that require action, write exactly one message saying so (and still include the marker).",
|
|
613
|
+
"Do not modify any code, and never approve or merge the pull request.",
|
|
614
|
+
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
615
|
+
] : [
|
|
616
|
+
"\uC774 pull request\uAC00 \uB3C4\uC785\uD55C \uBCC0\uACBD \uC0AC\uD56D\uB9CC \uB9AC\uBDF0\uD558\uC138\uC694. base/head \uCEE4\uBC0B\uC740 \uACE0\uC815 ref \uC774\uB984\uC73C\uB85C \uC774\uBBF8 fetch\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4: `git diff origin/pr-base...origin/pr-head`\uB85C \uBE44\uAD50\uD558\uACE0, \uCEE4\uBC0B \uB9E5\uB77D\uC740 `git log origin/pr-base..origin/pr-head`\uB85C \uD655\uC778\uD558\uC138\uC694(\uC774 \uC0CC\uB4DC\uBC15\uC2A4 \uC2A4\uD15D\uC740 \uB124\uD2B8\uC6CC\uD06C\uAC00 \uC5C6\uC73C\uBBC0\uB85C `gh`\uB098 GitHub API\uB97C \uD638\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694).",
|
|
617
|
+
"\uC804\uCCB4 \uC751\uB2F5\uC744 \uD558\uB098\uC758 \uBA54\uC2DC\uC9C0\uB85C \uC791\uC131\uD558\uC138\uC694: \uAD6C\uCCB4\uC801 \uC9C0\uC801\uC744 \uC801\uC740 \uB4A4 \uB9C8\uC9C0\uB9C9\uC5D0 \uC804\uCCB4 \uC694\uC57D\uC744 \uD55C \uBC88 \uB354 \uB0A8\uAE30\uC138\uC694.",
|
|
618
|
+
`\uBA54\uC2DC\uC9C0\uC5D0\uB294 \uBC18\uB4DC\uC2DC \uB9AC\uD130\uB7F4 \uB9C8\uCEE4 ${ROUND_MARKER}\uB97C \uD3EC\uD568\uD558\uC138\uC694.`,
|
|
619
|
+
'\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uAC00 \uC5C6\uC73C\uBA74 "\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."\uB77C\uB294 \uBA54\uC2DC\uC9C0\uB97C \uC815\uD655\uD788 \uD55C \uAC1C \uC791\uC131\uD558\uC138\uC694(\uC774\uB54C\uB3C4 \uB9C8\uCEE4\uB294 \uD3EC\uD568).',
|
|
620
|
+
"\uCF54\uB4DC\uB97C \uC218\uC815\uD558\uC9C0 \uB9C8\uC138\uC694(Do not modify any code). PR\uC744 \uC2B9\uC778\uD558\uAC70\uB098 \uBCD1\uD569\uD558\uC9C0 \uB9C8\uC138\uC694(never approve or merge).",
|
|
621
|
+
"\uC694\uC57D\uACFC \uC124\uBA85\uC740 \uD55C\uAD6D\uC5B4\uB85C \uC4F0\uB418, \uD30C\uC77C\uBA85\xB7\uD568\uC218\uBA85\xB7\uC2DD\uBCC4\uC790\uB294 \uC6D0\uBB38 \uADF8\uB300\uB85C \uC720\uC9C0\uD558\uC138\uC694."
|
|
622
|
+
];
|
|
623
|
+
return lines.join("\n");
|
|
624
|
+
}
|
|
625
|
+
function composePrompt4(input) {
|
|
626
|
+
const mode = REVIEW_MODES[input.mode];
|
|
627
|
+
const sections = [renderWorkflowInstructions4(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
628
|
+
if (mode.includesOverEngineering) {
|
|
629
|
+
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
630
|
+
}
|
|
631
|
+
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
632
|
+
return sections.join("\n\n---\n\n");
|
|
633
|
+
}
|
|
634
|
+
function buildWorkflowYaml4(input) {
|
|
635
|
+
const gateCondition = "steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
|
|
636
|
+
const steps = [
|
|
637
|
+
renderSecretGateStep(SECRET_NAME4),
|
|
638
|
+
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"),
|
|
648
|
+
[
|
|
649
|
+
"- name: Fetch base and head refs for the PR",
|
|
650
|
+
` if: ${gateCondition}`,
|
|
651
|
+
" env:",
|
|
652
|
+
" PR_BASE_REF: ${{ github.event.pull_request.base.ref }}",
|
|
653
|
+
" PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
654
|
+
" run: |",
|
|
655
|
+
" # Explicit destinations: the sandboxed review step has no network, so both sides",
|
|
656
|
+
" # of the diff must survive as stable local refs (FETCH_HEAD alone is ambiguous).",
|
|
657
|
+
" git fetch --no-tags origin \\",
|
|
658
|
+
' "+refs/heads/$PR_BASE_REF:refs/remotes/origin/pr-base" \\',
|
|
659
|
+
' "+refs/pull/$PR_NUMBER/head:refs/remotes/origin/pr-head"'
|
|
660
|
+
].join("\n"),
|
|
661
|
+
[
|
|
662
|
+
"- name: Run Codex review",
|
|
663
|
+
" id: codex-review",
|
|
664
|
+
` if: ${gateCondition}`,
|
|
665
|
+
" uses: openai/codex-action@v1",
|
|
666
|
+
" with:",
|
|
667
|
+
` openai-api-key: \${{ secrets.${SECRET_NAME4} }}`,
|
|
668
|
+
' permission-profile: ":read-only"',
|
|
669
|
+
" prompt: |",
|
|
670
|
+
indentBlock(composePrompt4(input), 6)
|
|
671
|
+
].join("\n"),
|
|
672
|
+
[
|
|
673
|
+
"- name: Post review comment",
|
|
674
|
+
` if: ${gateCondition}`,
|
|
675
|
+
" env:",
|
|
676
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
677
|
+
" PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
678
|
+
" REVIEW_BODY: ${{ steps.codex-review.outputs.final-message }}",
|
|
679
|
+
" run: |",
|
|
680
|
+
' printf %s "$REVIEW_BODY" | gh pr comment "$PR_NUMBER" --body-file -'
|
|
681
|
+
].join("\n"),
|
|
682
|
+
renderWorkspaceSetupSteps(input.packageManager),
|
|
683
|
+
renderVerdictGateStep(input.maxRounds, input.locale)
|
|
684
|
+
];
|
|
685
|
+
return [
|
|
686
|
+
"name: GPT Code Review",
|
|
687
|
+
"",
|
|
688
|
+
renderTriggerBlock(),
|
|
689
|
+
"",
|
|
690
|
+
renderConcurrencyBlock(),
|
|
691
|
+
"",
|
|
692
|
+
renderPermissionsBlock(),
|
|
693
|
+
"",
|
|
694
|
+
"jobs:",
|
|
695
|
+
" review:",
|
|
696
|
+
" runs-on: ubuntu-latest",
|
|
697
|
+
" steps:",
|
|
698
|
+
indentBlock(steps.join("\n\n"), 6),
|
|
699
|
+
""
|
|
700
|
+
].join("\n");
|
|
701
|
+
}
|
|
702
|
+
var gptProvider = {
|
|
703
|
+
id: "gpt",
|
|
704
|
+
workflowFileName: ".github/workflows/gpt-code-review.yml",
|
|
705
|
+
requiredSecretName: SECRET_NAME4,
|
|
706
|
+
buildWorkflowYaml: buildWorkflowYaml4
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// ../template-engine/dist/review/provider.js
|
|
710
|
+
var REVIEW_PROVIDERS = {
|
|
711
|
+
claude: claudeProvider,
|
|
712
|
+
gemini: geminiProvider,
|
|
713
|
+
gpt: gptProvider,
|
|
714
|
+
cursor: cursorProvider
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
// ../template-engine/dist/review/compose-review-setup.js
|
|
718
|
+
function composeReviewSetup(input) {
|
|
719
|
+
if (!input.enabled)
|
|
720
|
+
return null;
|
|
721
|
+
const provider = REVIEW_PROVIDERS[input.provider];
|
|
722
|
+
return {
|
|
723
|
+
workflowFilePath: provider.workflowFileName,
|
|
724
|
+
workflowYaml: provider.buildWorkflowYaml(input),
|
|
725
|
+
requiredSecretName: provider.requiredSecretName,
|
|
726
|
+
configSnippet: buildReviewConfigSnippet(input)
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ../template-engine/dist/review/conflict.js
|
|
731
|
+
function judgeReviewFileConflict(existing, generated) {
|
|
732
|
+
if (existing === null)
|
|
733
|
+
return { kind: "create" };
|
|
734
|
+
if (existing === generated)
|
|
735
|
+
return { kind: "unchanged" };
|
|
736
|
+
return { kind: "merge-proposal", existing, generated };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ../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
|
+
];
|
|
745
|
+
var ESLINT_CONFIG_CANDIDATES = ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"];
|
|
746
|
+
var APPLIED_MARKER = "sourceTag: 'scope:frontend'";
|
|
747
|
+
var ALLOW_ALL_BLOCK = /depConstraints:\s*\[\s*\{\s*sourceTag:\s*'\*',\s*onlyDependOnLibsWithTags:\s*\[\s*'\*'\s*\],?\s*\},?\s*\]/g;
|
|
748
|
+
function maskComments(text) {
|
|
749
|
+
const out = text.split("");
|
|
750
|
+
let mode = "code";
|
|
751
|
+
let quote = "";
|
|
752
|
+
for (let i = 0; i < text.length; i++) {
|
|
753
|
+
const c = text[i];
|
|
754
|
+
const d = text[i + 1];
|
|
755
|
+
if (mode === "code") {
|
|
756
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
757
|
+
mode = "string";
|
|
758
|
+
quote = c;
|
|
759
|
+
} else if (c === "/" && d === "/") {
|
|
760
|
+
mode = "line";
|
|
761
|
+
out[i] = " ";
|
|
762
|
+
} else if (c === "/" && d === "*") {
|
|
763
|
+
mode = "block";
|
|
764
|
+
out[i] = " ";
|
|
765
|
+
}
|
|
766
|
+
} else if (mode === "string") {
|
|
767
|
+
if (c === "\\") {
|
|
768
|
+
i++;
|
|
769
|
+
} else if (c === quote) {
|
|
770
|
+
mode = "code";
|
|
771
|
+
}
|
|
772
|
+
} else if (mode === "line") {
|
|
773
|
+
if (c === "\n") {
|
|
774
|
+
mode = "code";
|
|
775
|
+
} else {
|
|
776
|
+
out[i] = " ";
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
if (c === "*" && d === "/") {
|
|
780
|
+
out[i] = " ";
|
|
781
|
+
out[i + 1] = " ";
|
|
782
|
+
i++;
|
|
783
|
+
mode = "code";
|
|
784
|
+
} else if (c !== "\n") {
|
|
785
|
+
out[i] = " ";
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return out.join("");
|
|
790
|
+
}
|
|
791
|
+
function findAllowAllMatches(masked) {
|
|
792
|
+
const matches = [];
|
|
793
|
+
ALLOW_ALL_BLOCK.lastIndex = 0;
|
|
794
|
+
for (let m = ALLOW_ALL_BLOCK.exec(masked); m !== null; m = ALLOW_ALL_BLOCK.exec(masked)) {
|
|
795
|
+
matches.push({ index: m.index, length: m[0].length });
|
|
796
|
+
}
|
|
797
|
+
return matches;
|
|
798
|
+
}
|
|
799
|
+
function classifyBoundaryConstraints(configText) {
|
|
800
|
+
const masked = maskComments(configText);
|
|
801
|
+
if (masked.includes(APPLIED_MARKER))
|
|
802
|
+
return "enforced";
|
|
803
|
+
if (findAllowAllMatches(masked).length > 0)
|
|
804
|
+
return "allow-all";
|
|
805
|
+
return "unknown";
|
|
806
|
+
}
|
|
807
|
+
function renderConstraintsBlock(indent, eol) {
|
|
808
|
+
const renderList = (tags) => tags.map((t) => `'${t}'`).join(", ");
|
|
809
|
+
const inner = SCOPE_CONSTRAINTS.map((c) => {
|
|
810
|
+
const field = c.notDependOnLibsWithTags ? `notDependOnLibsWithTags: [${renderList(c.notDependOnLibsWithTags)}]` : `onlyDependOnLibsWithTags: [${renderList(c.onlyDependOnLibsWithTags ?? [])}]`;
|
|
811
|
+
return [`${indent} {`, `${indent} sourceTag: '${c.sourceTag}',`, `${indent} ${field},`, `${indent} },`].join(eol);
|
|
812
|
+
}).join(eol);
|
|
813
|
+
return [
|
|
814
|
+
`// nx-archweave boundaries \u2014 the frontend and the backend may never import each other`,
|
|
815
|
+
`${indent}depConstraints: [`,
|
|
816
|
+
inner,
|
|
817
|
+
[
|
|
818
|
+
`${indent} // baseline: everything else stays allowed (constraints apply conjunctively)`,
|
|
819
|
+
`${indent} {`,
|
|
820
|
+
`${indent} sourceTag: '*',`,
|
|
821
|
+
`${indent} onlyDependOnLibsWithTags: ['*'],`,
|
|
822
|
+
`${indent} },`
|
|
823
|
+
].join(eol),
|
|
824
|
+
`${indent}]`
|
|
825
|
+
].join(eol);
|
|
826
|
+
}
|
|
827
|
+
function applyBoundaryConstraints(configText) {
|
|
828
|
+
const classification = classifyBoundaryConstraints(configText);
|
|
829
|
+
if (classification === "enforced")
|
|
830
|
+
return { status: "already-applied" };
|
|
831
|
+
if (classification === "unknown")
|
|
832
|
+
return { status: "unrecognized" };
|
|
833
|
+
const masked = maskComments(configText);
|
|
834
|
+
const matches = findAllowAllMatches(masked);
|
|
835
|
+
if (matches.length !== 1) {
|
|
836
|
+
return { status: "unrecognized" };
|
|
837
|
+
}
|
|
838
|
+
const [match] = matches;
|
|
839
|
+
const eol = configText.includes("\r\n") ? "\r\n" : "\n";
|
|
840
|
+
const lineStart = configText.lastIndexOf("\n", match.index) + 1;
|
|
841
|
+
const indent = /^[ \t]*/.exec(configText.slice(lineStart, match.index))?.[0] ?? "";
|
|
842
|
+
const replacement = renderConstraintsBlock(indent, eol);
|
|
843
|
+
const text = configText.slice(0, match.index) + replacement + configText.slice(match.index + match.length);
|
|
844
|
+
return { status: "applied", text };
|
|
845
|
+
}
|
|
846
|
+
function buildBoundarySnippet() {
|
|
847
|
+
return [
|
|
848
|
+
"// nx-archweave project boundaries \u2014 merge these entries into the depConstraints",
|
|
849
|
+
"// array of the '@nx/enforce-module-boundaries' rule in your root ESLint config.",
|
|
850
|
+
"// They ride on top of your existing entries: constraints apply conjunctively,",
|
|
851
|
+
"// so the allow-all baseline can stay.",
|
|
852
|
+
renderConstraintsBlock("", "\n")
|
|
853
|
+
].join("\n");
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ../templates/dist/agents/agent-sections.js
|
|
857
|
+
var UNIVERSAL_AGENT_SECTION_IDS = [
|
|
858
|
+
"think-before-coding",
|
|
859
|
+
"simplicity-first",
|
|
860
|
+
"surgical-changes",
|
|
861
|
+
"goal-driven-execution",
|
|
862
|
+
"code-structure"
|
|
863
|
+
];
|
|
864
|
+
var PLACEHOLDER_AGENT_SECTION_IDS = ["project-goal", "architecture-boundary"];
|
|
865
|
+
var TEAM_AGENT_SECTION_ID = "team-rules";
|
|
866
|
+
var FSD_AGENT_SECTION_ID = "fsd-structure";
|
|
867
|
+
var DEFAULT_AGENT_SECTION_IDS = [
|
|
868
|
+
...UNIVERSAL_AGENT_SECTION_IDS,
|
|
869
|
+
"safety-rules",
|
|
870
|
+
"language-policy",
|
|
871
|
+
...PLACEHOLDER_AGENT_SECTION_IDS
|
|
872
|
+
];
|
|
873
|
+
var EN_SECTIONS = {
|
|
874
|
+
"think-before-coding": {
|
|
875
|
+
id: "think-before-coding",
|
|
876
|
+
heading: "Think Before Coding",
|
|
877
|
+
body: [
|
|
878
|
+
"Before writing any code, restate the problem in your own words and identify what could go wrong.",
|
|
879
|
+
"Read the surrounding code and existing patterns before introducing new ones \u2014 consistency beats",
|
|
880
|
+
"personal preference. When a request is ambiguous, make the most reasonable interpretation and say",
|
|
881
|
+
"what you assumed, rather than guessing silently or blocking on a question you could answer yourself.",
|
|
882
|
+
"Prefer understanding the root cause of a problem over patching its symptoms."
|
|
883
|
+
].join("\n")
|
|
884
|
+
},
|
|
885
|
+
"simplicity-first": {
|
|
886
|
+
id: "simplicity-first",
|
|
887
|
+
heading: "Simplicity First",
|
|
888
|
+
body: [
|
|
889
|
+
"Choose the simplest design that solves the actual problem in front of you, not the one you might",
|
|
890
|
+
"have tomorrow. Avoid speculative abstraction, unnecessary configuration, and generic frameworks",
|
|
891
|
+
"built for requirements nobody has asked for yet. A straightforward, slightly duplicated solution",
|
|
892
|
+
"that is easy to read beats a clever, general one that is hard to follow. Add complexity only when",
|
|
893
|
+
"a concrete, current need demands it."
|
|
894
|
+
].join("\n")
|
|
895
|
+
},
|
|
896
|
+
"surgical-changes": {
|
|
897
|
+
id: "surgical-changes",
|
|
898
|
+
heading: "Surgical Changes",
|
|
899
|
+
body: [
|
|
900
|
+
"Change only what the task requires. Do not reformat, rename, or refactor unrelated code while",
|
|
901
|
+
"fixing something else \u2014 unrelated changes make review harder and hide the actual fix in noise.",
|
|
902
|
+
"Keep diffs small and focused so a reviewer can see exactly what changed and why. If you notice an",
|
|
903
|
+
"unrelated problem while working, note it separately instead of folding it into the current change."
|
|
904
|
+
].join("\n")
|
|
905
|
+
},
|
|
906
|
+
"goal-driven-execution": {
|
|
907
|
+
id: "goal-driven-execution",
|
|
908
|
+
heading: "Goal-Driven Execution",
|
|
909
|
+
body: [
|
|
910
|
+
"Keep the actual goal in view, not just the literal instruction. If a requested step turns out to",
|
|
911
|
+
"be unnecessary, harmful, or based on a wrong assumption once you're in the code, say so and propose",
|
|
912
|
+
"the better path instead of executing it mechanically. Finish what you start: a task is done when it",
|
|
913
|
+
"actually works, not when the obvious-looking code has been written. Verify before declaring success."
|
|
914
|
+
].join("\n")
|
|
915
|
+
},
|
|
916
|
+
"code-structure": {
|
|
917
|
+
id: "code-structure",
|
|
918
|
+
heading: "Code Structure and Abstraction",
|
|
919
|
+
body: [
|
|
920
|
+
"Keep functions and modules focused on one responsibility; split a file or function when it starts",
|
|
921
|
+
"accumulating unrelated concerns rather than letting it grow into a god object. Name things after",
|
|
922
|
+
"what they do or represent, not after their implementation detail. Extract an abstraction only after",
|
|
923
|
+
"a pattern has actually repeated \u2014 not in anticipation of a repetition that may never come."
|
|
924
|
+
].join("\n")
|
|
925
|
+
},
|
|
926
|
+
"safety-rules": {
|
|
927
|
+
id: "safety-rules",
|
|
928
|
+
heading: "Safety Rules",
|
|
929
|
+
body: [
|
|
930
|
+
"Never force-push to shared branches, never bypass required status checks, and never disable or",
|
|
931
|
+
"delete CI/CD configuration just to make a failing check pass.",
|
|
932
|
+
"Do not read, print, or commit the contents of secret files (`.env`, credential files, private",
|
|
933
|
+
"keys) \u2014 treat anything matching a common secret pattern as off-limits unless the user has already",
|
|
934
|
+
"shared its contents with you directly.",
|
|
935
|
+
"Do not run destructive commands (`rm -rf`, `git reset --hard`, `git clean -f`, force pushes)",
|
|
936
|
+
"without explicit confirmation, and check for uncommitted work first.",
|
|
937
|
+
"Do not send repository contents, credentials, or user data to any external service.",
|
|
938
|
+
"When you are unsure whether an action is safe, stop and ask instead of proceeding.",
|
|
939
|
+
"",
|
|
940
|
+
"<!-- TODO: list project-specific protected resources (e.g. production config files, protected",
|
|
941
|
+
"branches, restricted directories, external services this project must never call in tests). -->"
|
|
942
|
+
].join("\n")
|
|
943
|
+
},
|
|
944
|
+
"language-policy": {
|
|
945
|
+
id: "language-policy",
|
|
946
|
+
heading: "Language Policy",
|
|
947
|
+
body: [
|
|
948
|
+
"Communicate with the user in English. Write code, identifiers, comments, commit messages, and",
|
|
949
|
+
"documentation in English as well, unless the user explicitly asks for another language for a",
|
|
950
|
+
"specific piece of content."
|
|
951
|
+
].join("\n")
|
|
952
|
+
},
|
|
953
|
+
"team-rules": {
|
|
954
|
+
id: "team-rules",
|
|
955
|
+
heading: "Team Rules",
|
|
956
|
+
body: [
|
|
957
|
+
"This repository is shared by a team: optimize for the reviewer, not for yourself.",
|
|
958
|
+
"Keep each pull request small and about one thing, fill in the pull request template, and link",
|
|
959
|
+
"the issue it closes.",
|
|
960
|
+
"Treat review findings as work, not opinions: address every P0/P1 finding before asking for",
|
|
961
|
+
"another look, answer each comment, and never merge your own pull request or dismiss a review",
|
|
962
|
+
"to get it through.",
|
|
963
|
+
"Respect module ownership: a change that crosses a module or layer boundary needs that",
|
|
964
|
+
"module's owner in the review. Never weaken a module-boundary lint rule, add an eslint-disable",
|
|
965
|
+
"for one, or retag a project to get an import past the boundary \u2014 propose the boundary change",
|
|
966
|
+
"in the pull request description instead.",
|
|
967
|
+
"Changes to shared code (the `shared` layer, `shared-types`, root configuration) affect every",
|
|
968
|
+
"team member: keep them backward compatible, call them out in the pull request, and keep them",
|
|
969
|
+
"separate from feature work.",
|
|
970
|
+
"Never commit directly to a protected branch; work on a branch and bring it up to date with",
|
|
971
|
+
"its base before requesting review.",
|
|
972
|
+
"",
|
|
973
|
+
"<!-- TODO: list team-specific conventions (module owners / CODEOWNERS, required reviewers,",
|
|
974
|
+
"branch naming, release process). -->"
|
|
975
|
+
].join("\n")
|
|
976
|
+
},
|
|
977
|
+
"fsd-structure": {
|
|
978
|
+
id: "fsd-structure",
|
|
979
|
+
heading: "Frontend Structure (FSD Lite)",
|
|
980
|
+
body: [
|
|
981
|
+
'The frontend app follows canonical Feature-Sliced Design. "Lite" means only that the default scope',
|
|
982
|
+
"is `app`, `pages`, `features`, `entities` and `shared` (`widgets` only on request) and that Solo",
|
|
983
|
+
"workspaces get warnings instead of errors \u2014 the rules themselves are the canonical ones:",
|
|
984
|
+
"- A layer imports only layers below it: app > pages > widgets > features > entities > shared.",
|
|
985
|
+
"- Slices on one layer (pages, widgets, features, entities) never import each other; move the",
|
|
986
|
+
" shared part one layer down, or expose it through the other slice's `@x/<consumer>` public API.",
|
|
987
|
+
"- Import a slice only through its `index.ts`; never reach into its `ui/`, `model/` or `api/` files.",
|
|
988
|
+
"- `app` and `shared` hold segments, not slices; shared segments are named by purpose (`ui`, `api`,",
|
|
989
|
+
" `model`, `lib`, `config`), never by essence (`components`, `hooks`, `utils`, `types`).",
|
|
990
|
+
"Create slices with the generators (`nx g @clearlydev/nx-archweave:page|widget|feature|entity|shared --name=\u2026`,",
|
|
991
|
+
"`--segments=ui,api` for extra segments).",
|
|
992
|
+
"The `archweave/fsd-boundaries` ESLint rule reports violations: fix the import, never disable the",
|
|
993
|
+
"rule or add an eslint-disable for it. On Next.js the pages layer is the `_pages/` directory."
|
|
994
|
+
].join("\n")
|
|
995
|
+
},
|
|
996
|
+
"project-goal": {
|
|
997
|
+
id: "project-goal",
|
|
998
|
+
heading: "Project Goal",
|
|
999
|
+
body: "<!-- TODO: describe this project's goal, target users, and what success looks like. -->"
|
|
1000
|
+
},
|
|
1001
|
+
"architecture-boundary": {
|
|
1002
|
+
id: "architecture-boundary",
|
|
1003
|
+
heading: "Architecture Boundary",
|
|
1004
|
+
body: "<!-- TODO: describe this project's module boundaries, ownership rules, and which modules may or may not depend on which. -->"
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
var KO_SECTIONS = {
|
|
1008
|
+
"think-before-coding": {
|
|
1009
|
+
id: "think-before-coding",
|
|
1010
|
+
heading: "\uCF54\uB529 \uC804\uC5D0 \uBA3C\uC800 \uC0DD\uAC01\uD558\uAE30",
|
|
1011
|
+
body: [
|
|
1012
|
+
"\uCF54\uB4DC\uB97C \uC791\uC131\uD558\uAE30 \uC804\uC5D0 \uBB38\uC81C\uB97C \uC2A4\uC2A4\uB85C\uC758 \uB9D0\uB85C \uB2E4\uC2DC \uC815\uB9AC\uD558\uACE0 \uBB34\uC5C7\uC774 \uC798\uBABB\uB420 \uC218 \uC788\uB294\uC9C0 \uD30C\uC545\uD55C\uB2E4.",
|
|
1013
|
+
"\uC0C8 \uD328\uD134\uC744 \uB3C4\uC785\uD558\uAE30 \uC804\uC5D0 \uC8FC\uBCC0 \uCF54\uB4DC\uC640 \uAE30\uC874 \uAD00\uB840\uB97C \uBA3C\uC800 \uC77D\uB294\uB2E4 \u2014 \uAC1C\uC778 \uCDE8\uD5A5\uBCF4\uB2E4 \uC77C\uAD00\uC131\uC774 \uC6B0\uC120\uC774\uB2E4.",
|
|
1014
|
+
"\uC694\uCCAD\uC774 \uBAA8\uD638\uD560 \uB54C\uB294 \uAC00\uC7A5 \uD569\uB9AC\uC801\uC778 \uD574\uC11D\uC744 \uD0DD\uD558\uACE0 \uBB34\uC5C7\uC744 \uAC00\uC815\uD588\uB294\uC9C0 \uBC1D\uD78C\uB2E4. \uC2A4\uC2A4\uB85C \uB2F5\uD560 \uC218 \uC788\uB294",
|
|
1015
|
+
"\uC9C8\uBB38\uC73C\uB85C \uC791\uC5C5\uC744 \uB9C9\uC9C0 \uB9D0\uACE0, \uC870\uC6A9\uD788 \uCD94\uCE21\uB9CC \uD558\uC9C0\uB3C4 \uC54A\uB294\uB2E4. \uC99D\uC0C1\uC744 \uB55C\uC9C8\uD558\uAE30\uBCF4\uB2E4 \uADFC\uBCF8 \uC6D0\uC778\uC744 \uC774\uD574\uD558\uB294",
|
|
1016
|
+
"\uCABD\uC744 \uC6B0\uC120\uD55C\uB2E4."
|
|
1017
|
+
].join("\n")
|
|
1018
|
+
},
|
|
1019
|
+
"simplicity-first": {
|
|
1020
|
+
id: "simplicity-first",
|
|
1021
|
+
heading: "\uB2E8\uC21C\uD568 \uC6B0\uC120",
|
|
1022
|
+
body: [
|
|
1023
|
+
"\uC9C0\uAE08 \uB208\uC55E\uC758 \uBB38\uC81C\uB97C \uD574\uACB0\uD558\uB294 \uAC00\uC7A5 \uB2E8\uC21C\uD55C \uC124\uACC4\uB97C \uC120\uD0DD\uD55C\uB2E4 \u2014 \uB0B4\uC77C \uD544\uC694\uD560\uC9C0\uB3C4 \uBAA8\uB974\uB294 \uAC83\uAE4C\uC9C0 \uB300\uBE44\uD558\uC9C0",
|
|
1024
|
+
"\uC54A\uB294\uB2E4. \uC544\uBB34\uB3C4 \uC694\uCCAD\uD558\uC9C0 \uC54A\uC740 \uC694\uAD6C\uC0AC\uD56D\uC744 \uC704\uD55C \uCD94\uCE21\uC131 \uCD94\uC0C1\uD654, \uBD88\uD544\uC694\uD55C \uC124\uC815, \uBC94\uC6A9 \uD504\uB808\uC784\uC6CC\uD06C\uB97C",
|
|
1025
|
+
"\uD53C\uD55C\uB2E4. \uC77D\uAE30 \uC26C\uC6B4 \uC57D\uAC04\uC758 \uC911\uBCF5\uC774 \uC788\uB294 \uD574\uBC95\uC774, \uC774\uD574\uD558\uAE30 \uC5B4\uB824\uC6B4 \uC601\uB9AC\uD558\uACE0 \uBC94\uC6A9\uC801\uC778 \uD574\uBC95\uBCF4\uB2E4 \uB0AB\uB2E4.",
|
|
1026
|
+
"\uAD6C\uCCB4\uC801\uC774\uACE0 \uD604\uC7AC \uC874\uC7AC\uD558\uB294 \uD544\uC694\uAC00 \uC788\uC744 \uB54C\uB9CC \uBCF5\uC7A1\uB3C4\uB97C \uCD94\uAC00\uD55C\uB2E4."
|
|
1027
|
+
].join("\n")
|
|
1028
|
+
},
|
|
1029
|
+
"surgical-changes": {
|
|
1030
|
+
id: "surgical-changes",
|
|
1031
|
+
heading: "\uC678\uACFC\uC801 \uBCC0\uACBD",
|
|
1032
|
+
body: [
|
|
1033
|
+
"\uC791\uC5C5\uC5D0 \uD544\uC694\uD55C \uBD80\uBD84\uB9CC \uBCC0\uACBD\uD55C\uB2E4. \uB2E4\uB978 \uBB38\uC81C\uB97C \uACE0\uCE58\uB294 \uAE40\uC5D0 \uAD00\uB828 \uC5C6\uB294 \uCF54\uB4DC\uB97C \uC7AC\uD3EC\uB9F7\uD558\uAC70\uB098, \uC774\uB984\uC744",
|
|
1034
|
+
"\uBC14\uAFB8\uAC70\uB098, \uB9AC\uD329\uD130\uB9C1\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uAD00\uB828 \uC5C6\uB294 \uBCC0\uACBD\uC740 \uB9AC\uBDF0\uB97C \uC5B4\uB835\uAC8C \uB9CC\uB4E4\uACE0 \uC2E4\uC81C \uC218\uC815 \uB0B4\uC6A9\uC744 \uB178\uC774\uC988",
|
|
1035
|
+
"\uC18D\uC5D0 \uC228\uAE34\uB2E4. diff\uB97C \uC791\uACE0 \uC9D1\uC911\uB418\uAC8C \uC720\uC9C0\uD574 \uB9AC\uBDF0\uC5B4\uAC00 \uBB34\uC5C7\uC774, \uC65C \uBC14\uB00C\uC5C8\uB294\uC9C0 \uC815\uD655\uD788 \uBCFC \uC218 \uC788\uAC8C \uD55C\uB2E4.",
|
|
1036
|
+
"\uC791\uC5C5 \uC911 \uAD00\uB828 \uC5C6\uB294 \uBB38\uC81C\uB97C \uBC1C\uACAC\uD558\uBA74 \uD604\uC7AC \uBCC0\uACBD\uC5D0 \uC11E\uC9C0 \uB9D0\uACE0 \uBCC4\uB3C4\uB85C \uC54C\uB9B0\uB2E4."
|
|
1037
|
+
].join("\n")
|
|
1038
|
+
},
|
|
1039
|
+
"goal-driven-execution": {
|
|
1040
|
+
id: "goal-driven-execution",
|
|
1041
|
+
heading: "\uBAA9\uD45C \uC9C0\uD5A5 \uC2E4\uD589",
|
|
1042
|
+
body: [
|
|
1043
|
+
"\uBB38\uC790 \uADF8\uB300\uB85C\uC758 \uC9C0\uC2DC\uAC00 \uC544\uB2C8\uB77C \uC2E4\uC81C \uBAA9\uD45C\uB97C \uACC4\uC18D \uC5FC\uB450\uC5D0 \uB454\uB2E4. \uCF54\uB4DC\uB97C \uC0B4\uD3B4\uBCF4\uB2C8 \uC694\uCCAD\uBC1B\uC740 \uB2E8\uACC4\uAC00",
|
|
1044
|
+
"\uBD88\uD544\uC694\uD558\uAC70\uB098, \uD574\uB86D\uAC70\uB098, \uC798\uBABB\uB41C \uAC00\uC815\uC5D0 \uAE30\uBC18\uD55C \uAC83\uC73C\uB85C \uB4DC\uB7EC\uB098\uBA74 \uAE30\uACC4\uC801\uC73C\uB85C \uC2E4\uD589\uD558\uC9C0 \uB9D0\uACE0 \uADF8\uB807\uB2E4\uACE0",
|
|
1045
|
+
"\uB9D0\uD55C \uB4A4 \uB354 \uB098\uC740 \uACBD\uB85C\uB97C \uC81C\uC548\uD55C\uB2E4. \uC2DC\uC791\uD55C \uC77C\uC740 \uB05D\uB0B8\uB2E4 \u2014 \uADF8\uB7F4\uB4EF\uD574 \uBCF4\uC774\uB294 \uCF54\uB4DC\uB97C \uC791\uC131\uD588\uC744 \uB54C\uAC00 \uC544\uB2C8\uB77C",
|
|
1046
|
+
"\uC2E4\uC81C\uB85C \uB3D9\uC791\uD560 \uB54C\uAC00 \uC644\uB8CC\uB2E4. \uC131\uACF5\uC744 \uC120\uC5B8\uD558\uAE30 \uC804\uC5D0 \uBC18\uB4DC\uC2DC \uAC80\uC99D\uD55C\uB2E4."
|
|
1047
|
+
].join("\n")
|
|
1048
|
+
},
|
|
1049
|
+
"code-structure": {
|
|
1050
|
+
id: "code-structure",
|
|
1051
|
+
heading: "\uCF54\uB4DC \uAD6C\uC870\uC640 \uCD94\uC0C1\uD654",
|
|
1052
|
+
body: [
|
|
1053
|
+
"\uD568\uC218\uC640 \uBAA8\uB4C8\uC744 \uD558\uB098\uC758 \uCC45\uC784\uC5D0 \uC9D1\uC911\uC2DC\uD0A8\uB2E4. God object\uB85C \uCEE4\uC9C0\uB3C4\uB85D \uB450\uC9C0 \uB9D0\uACE0, \uAD00\uB828 \uC5C6\uB294 \uAD00\uC2EC\uC0AC\uAC00",
|
|
1054
|
+
"\uC313\uC774\uAE30 \uC2DC\uC791\uD558\uBA74 \uD30C\uC77C\uC774\uB098 \uD568\uC218\uB97C \uBD84\uB9AC\uD55C\uB2E4. \uAD6C\uD604 \uC138\uBD80\uC0AC\uD56D\uC774 \uC544\uB2C8\uB77C \uD558\uB294 \uC77C\uC774\uB098 \uB098\uD0C0\uB0B4\uB294 \uB300\uC0C1\uC744",
|
|
1055
|
+
"\uAE30\uC900\uC73C\uB85C \uC774\uB984\uC744 \uC9D3\uB294\uB2E4. \uD328\uD134\uC774 \uC2E4\uC81C\uB85C \uBC18\uBCF5\uB41C \uB4A4\uC5D0\uB9CC \uCD94\uC0C1\uD654\uB97C \uCD94\uCD9C\uD55C\uB2E4 \u2014 \uC5B8\uC820\uAC00 \uBC18\uBCF5\uB420\uC9C0\uB3C4 \uBAA8\uB978\uB2E4\uB294",
|
|
1056
|
+
"\uC608\uCE21\uB9CC\uC73C\uB85C \uBBF8\uB9AC \uCD94\uCD9C\uD558\uC9C0 \uC54A\uB294\uB2E4."
|
|
1057
|
+
].join("\n")
|
|
1058
|
+
},
|
|
1059
|
+
"safety-rules": {
|
|
1060
|
+
id: "safety-rules",
|
|
1061
|
+
heading: "\uC548\uC804 \uADDC\uCE59",
|
|
1062
|
+
body: [
|
|
1063
|
+
"\uACF5\uC720 \uBE0C\uB79C\uCE58\uC5D0 force-push\uD558\uC9C0 \uC54A\uACE0, \uD544\uC218 \uC0C1\uD0DC \uAC80\uC0AC\uB97C \uC6B0\uD68C\uD558\uC9C0 \uC54A\uC73C\uBA70, \uC2E4\uD328\uD558\uB294 \uAC80\uC0AC\uB97C \uD1B5\uACFC\uC2DC\uD0A4\uAE30",
|
|
1064
|
+
"\uC704\uD574 CI/CD \uC124\uC815\uC744 \uBE44\uD65C\uC131\uD654\uD558\uAC70\uB098 \uC0AD\uC81C\uD558\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1065
|
+
"\uBE44\uBC00 \uD30C\uC77C(`.env`, \uC790\uACA9 \uC99D\uBA85 \uD30C\uC77C, \uAC1C\uC778 \uD0A4)\uC758 \uB0B4\uC6A9\uC744 \uC77D\uAC70\uB098 \uCD9C\uB825\uD558\uAC70\uB098 \uCEE4\uBC0B\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uC0AC\uC6A9\uC790\uAC00",
|
|
1066
|
+
"\uBA3C\uC800 \uC9C1\uC811 \uB0B4\uC6A9\uC744 \uACF5\uC720\uD558\uC9C0 \uC54A\uB294 \uD55C, \uC77C\uBC18\uC801\uC778 \uBE44\uBC00 \uD30C\uC77C \uD328\uD134\uC5D0 \uD574\uB2F9\uD558\uB294 \uAC83\uC740 \uC811\uADFC \uAE08\uC9C0 \uB300\uC0C1\uC73C\uB85C",
|
|
1067
|
+
"\uCDE8\uAE09\uD55C\uB2E4.",
|
|
1068
|
+
"\uBA85\uC2DC\uC801 \uD655\uC778 \uC5C6\uC774 \uD30C\uAD34\uC801 \uBA85\uB839(`rm -rf`, `git reset --hard`, `git clean -f`, force-push)\uC744 \uC2E4\uD589\uD558\uC9C0",
|
|
1069
|
+
"\uC54A\uC73C\uBA70, \uC2E4\uD589 \uC804 \uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC740 \uC791\uC5C5\uC774 \uC788\uB294\uC9C0 \uBA3C\uC800 \uD655\uC778\uD55C\uB2E4.",
|
|
1070
|
+
"\uC800\uC7A5\uC18C \uB0B4\uC6A9, \uC790\uACA9 \uC99D\uBA85, \uC0AC\uC6A9\uC790 \uB370\uC774\uD130\uB97C \uC678\uBD80 \uC11C\uBE44\uC2A4\uB85C \uC804\uC1A1\uD558\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1071
|
+
"\uC5B4\uB5A4 \uB3D9\uC791\uC774 \uC548\uC804\uD55C\uC9C0 \uD655\uC2E0\uC774 \uC11C\uC9C0 \uC54A\uC73C\uBA74 \uC9C4\uD589\uD558\uC9C0 \uB9D0\uACE0 \uBA3C\uC800 \uC9C8\uBB38\uD55C\uB2E4.",
|
|
1072
|
+
"",
|
|
1073
|
+
// The marker itself stays in English, matching the Language Policy's "code/comments in
|
|
1074
|
+
// English" rule and giving tooling a single locale-invariant string to check for.
|
|
1075
|
+
"<!-- TODO: list project-specific protected resources (e.g. production config files, protected",
|
|
1076
|
+
"branches, restricted directories, external services this project must never call in tests). -->"
|
|
1077
|
+
].join("\n")
|
|
1078
|
+
},
|
|
1079
|
+
"language-policy": {
|
|
1080
|
+
id: "language-policy",
|
|
1081
|
+
heading: "\uC5B8\uC5B4 \uC815\uCC45",
|
|
1082
|
+
body: [
|
|
1083
|
+
"\uC0AC\uC6A9\uC790\uC640\uC758 \uB300\uD654\uB294 \uD55C\uAD6D\uC5B4\uB85C \uC9C4\uD589\uD55C\uB2E4. \uCF54\uB4DC, \uC2DD\uBCC4\uC790, \uC8FC\uC11D, \uCEE4\uBC0B \uBA54\uC2DC\uC9C0, \uBB38\uC11C\uB294 \uC0AC\uC6A9\uC790\uAC00 \uD2B9\uC815",
|
|
1084
|
+
"\uD56D\uBAA9\uC5D0 \uB300\uD574 \uB2E4\uB978 \uC5B8\uC5B4\uB97C \uBA85\uC2DC\uC801\uC73C\uB85C \uC694\uCCAD\uD558\uC9C0 \uC54A\uB294 \uD55C \uC601\uC5B4\uB85C \uC791\uC131\uD55C\uB2E4."
|
|
1085
|
+
].join("\n")
|
|
1086
|
+
},
|
|
1087
|
+
"team-rules": {
|
|
1088
|
+
id: "team-rules",
|
|
1089
|
+
heading: "\uD300 \uADDC\uCE59",
|
|
1090
|
+
body: [
|
|
1091
|
+
"\uC774 \uC800\uC7A5\uC18C\uB294 \uD300\uC774 \uD568\uAED8 \uC4F4\uB2E4 \u2014 \uC790\uC2E0\uC774 \uC544\uB2C8\uB77C \uB9AC\uBDF0\uC5B4\uB97C \uC704\uD574 \uCD5C\uC801\uD654\uD55C\uB2E4.",
|
|
1092
|
+
"PR\uC740 \uC791\uAC8C, \uD55C \uAC00\uC9C0 \uC8FC\uC81C\uB9CC \uB2E4\uB8E8\uBA70, PR \uD15C\uD50C\uB9BF\uC744 \uCC44\uC6B0\uACE0 \uB2EB\uB294 \uC774\uC288\uB97C \uB9C1\uD06C\uD55C\uB2E4.",
|
|
1093
|
+
"\uB9AC\uBDF0 finding\uC740 \uC758\uACAC\uC774 \uC544\uB2C8\uB77C \uC791\uC5C5\uC774\uB2E4 \u2014 \uC7AC\uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uAE30 \uC804\uC5D0 P0/P1 finding\uC744 \uBAA8\uB450 \uD574\uACB0\uD558\uACE0,",
|
|
1094
|
+
"\uCF54\uBA58\uD2B8\uB9C8\uB2E4 \uB2F5\uD558\uBA70, \uC790\uAE30 PR\uC744 \uC2A4\uC2A4\uB85C \uBA38\uC9C0\uD558\uAC70\uB098 \uB9AC\uBDF0\uB97C dismiss\uD574\uC11C \uD1B5\uACFC\uC2DC\uD0A4\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1095
|
+
"\uBAA8\uB4C8 \uC18C\uC720\uAD8C\uC744 \uC874\uC911\uD55C\uB2E4 \u2014 \uBAA8\uB4C8\uC774\uB098 \uB808\uC774\uC5B4 \uACBD\uACC4\uB97C \uB118\uB294 \uBCC0\uACBD\uC740 \uD574\uB2F9 \uBAA8\uB4C8 \uC624\uB108\uC758 \uB9AC\uBDF0\uAC00 \uD544\uC694\uD558\uB2E4.",
|
|
1096
|
+
"import\uB97C \uACBD\uACC4 \uB108\uBA38\uB85C \uD1B5\uACFC\uC2DC\uD0A4\uAE30 \uC704\uD574 module-boundary \uB9B0\uD2B8 \uADDC\uCE59\uC744 \uC57D\uD654\uD558\uAC70\uB098, eslint-disable\uC744",
|
|
1097
|
+
"\uCD94\uAC00\uD558\uAC70\uB098, \uD504\uB85C\uC81D\uD2B8 \uD0DC\uADF8\uB97C \uBC14\uAFB8\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uACBD\uACC4 \uBCC0\uACBD\uC740 PR \uBCF8\uBB38\uC5D0\uC11C \uC81C\uC548\uD55C\uB2E4.",
|
|
1098
|
+
"\uACF5\uC720 \uCF54\uB4DC(`shared` \uB808\uC774\uC5B4, `shared-types`, \uB8E8\uD2B8 \uC124\uC815) \uBCC0\uACBD\uC740 \uD300 \uC804\uC6D0\uC5D0\uAC8C \uC601\uD5A5\uC744 \uC900\uB2E4 \u2014 \uD558\uC704",
|
|
1099
|
+
"\uD638\uD658\uC744 \uC720\uC9C0\uD558\uACE0, PR\uC5D0 \uBA85\uC2DC\uD558\uBA70, \uAE30\uB2A5 \uC791\uC5C5\uACFC \uBD84\uB9AC\uD55C\uB2E4.",
|
|
1100
|
+
"\uBCF4\uD638 \uBE0C\uB79C\uCE58\uC5D0 \uC9C1\uC811 \uCEE4\uBC0B\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uBE0C\uB79C\uCE58\uC5D0\uC11C \uC791\uC5C5\uD558\uACE0 \uB9AC\uBDF0 \uC694\uCCAD \uC804\uC5D0 \uBCA0\uC774\uC2A4\uC640 \uB3D9\uAE30\uD654\uD55C\uB2E4.",
|
|
1101
|
+
"",
|
|
1102
|
+
// The marker stays in English, matching the Safety Rules precedent (one locale-invariant
|
|
1103
|
+
// string for tooling to check).
|
|
1104
|
+
"<!-- TODO: list team-specific conventions (module owners / CODEOWNERS, required reviewers,",
|
|
1105
|
+
"branch naming, release process). -->"
|
|
1106
|
+
].join("\n")
|
|
1107
|
+
},
|
|
1108
|
+
"fsd-structure": {
|
|
1109
|
+
id: "fsd-structure",
|
|
1110
|
+
heading: "\uD504\uB860\uD2B8\uC5D4\uB4DC \uAD6C\uC870 (FSD Lite)",
|
|
1111
|
+
body: [
|
|
1112
|
+
'\uD504\uB860\uD2B8\uC5D4\uB4DC \uC571\uC740 \uC815\uC2DD Feature-Sliced Design\uC744 \uB530\uB978\uB2E4. "Lite"\uB294 \uAE30\uBCF8 \uBC94\uC704\uAC00 `app`, `pages`, `features`,',
|
|
1113
|
+
"`entities`, `shared`\uC774\uACE0(`widgets`\uB294 \uC694\uCCAD\uD560 \uB54C\uB9CC), Solo \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC5D0\uC11C\uB294 \uC704\uBC18\uC744 \uC5D0\uB7EC\uAC00 \uC544\uB2CC",
|
|
1114
|
+
"\uACBD\uACE0\uB85C \uC54C\uB9B0\uB2E4\uB294 \uB73B\uC77C \uBFD0\uC774\uB2E4 \u2014 \uADDC\uCE59 \uC790\uCCB4\uB294 \uC815\uC2DD FSD \uADF8\uB300\uB85C\uB2E4:",
|
|
1115
|
+
"- \uB808\uC774\uC5B4\uB294 \uC790\uAE30\uBCF4\uB2E4 \uC544\uB798 \uB808\uC774\uC5B4\uB9CC import\uD55C\uB2E4: app > pages > widgets > features > entities > shared.",
|
|
1116
|
+
"- \uAC19\uC740 \uB808\uC774\uC5B4\uC758 \uC2AC\uB77C\uC774\uC2A4\uB07C\uB9AC(pages, widgets, features, entities)\uB294 import\uD558\uC9C0 \uC54A\uB294\uB2E4. \uACF5\uC720\uD560 \uCF54\uB4DC\uB294",
|
|
1117
|
+
" \uD55C \uB808\uC774\uC5B4 \uC544\uB798\uB85C \uB0B4\uB9AC\uAC70\uB098, \uC0C1\uB300 \uC2AC\uB77C\uC774\uC2A4\uC758 `@x/<consumer>` \uACF5\uAC1C API\uB85C\uB9CC \uB178\uCD9C\uD55C\uB2E4.",
|
|
1118
|
+
"- \uC2AC\uB77C\uC774\uC2A4\uB294 `index.ts`\uB97C \uD1B5\uD574\uC11C\uB9CC import\uD55C\uB2E4. `ui/`, `model/`, `api/` \uB0B4\uBD80 \uD30C\uC77C\uC5D0 \uC9C1\uC811 \uB2FF\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1119
|
+
"- `app`\uACFC `shared`\uB294 \uC2AC\uB77C\uC774\uC2A4\uAC00 \uC544\uB2C8\uB77C \uC138\uADF8\uBA3C\uD2B8\uB97C \uAC00\uC9C4\uB2E4. shared \uC138\uADF8\uBA3C\uD2B8 \uC774\uB984\uC740 \uBAA9\uC801\uD615(`ui`, `api`,",
|
|
1120
|
+
" `model`, `lib`, `config`)\uC73C\uB85C \uC9D3\uACE0, \uBCF8\uC9C8\uD615(`components`, `hooks`, `utils`, `types`)\uC740 \uC4F0\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1121
|
+
"\uC2AC\uB77C\uC774\uC2A4\uB294 generator\uB85C \uB9CC\uB4E0\uB2E4(`nx g @clearlydev/nx-archweave:page|widget|feature|entity|shared --name=\u2026`,",
|
|
1122
|
+
"\uCD94\uAC00 \uC138\uADF8\uBA3C\uD2B8\uB294 `--segments=ui,api`).",
|
|
1123
|
+
"`archweave/fsd-boundaries` ESLint \uADDC\uCE59\uC774 \uC704\uBC18\uC744 \uC54C\uB824 \uC900\uB2E4: import\uB97C \uACE0\uCE58\uB418, \uADDC\uCE59\uC744 \uB044\uAC70\uB098 eslint-disable\uB85C",
|
|
1124
|
+
"\uB118\uAE30\uC9C0 \uC54A\uB294\uB2E4. Next.js\uC5D0\uC11C\uB294 pages \uB808\uC774\uC5B4\uAC00 `_pages/` \uD3F4\uB354\uB2E4."
|
|
1125
|
+
].join("\n")
|
|
1126
|
+
},
|
|
1127
|
+
"project-goal": {
|
|
1128
|
+
id: "project-goal",
|
|
1129
|
+
heading: "\uD504\uB85C\uC81D\uD2B8 \uBAA9\uD45C",
|
|
1130
|
+
body: "<!-- TODO: \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uBAA9\uD45C, \uB300\uC0C1 \uC0AC\uC6A9\uC790, \uC131\uACF5 \uAE30\uC900\uC744 \uC791\uC131\uD558\uC138\uC694. -->"
|
|
1131
|
+
},
|
|
1132
|
+
"architecture-boundary": {
|
|
1133
|
+
id: "architecture-boundary",
|
|
1134
|
+
heading: "\uC544\uD0A4\uD14D\uCC98 \uACBD\uACC4",
|
|
1135
|
+
body: "<!-- TODO: \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uBAA8\uB4C8 \uACBD\uACC4, \uC18C\uC720 \uADDC\uCE59, \uC5B4\uB5A4 \uBAA8\uB4C8\uC774 \uC5B4\uB5A4 \uBAA8\uB4C8\uC5D0 \uC758\uC874\uD560 \uC218 \uC788\uB294\uC9C0\uB97C \uC791\uC131\uD558\uC138\uC694. -->"
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
var AGENT_RULE_SECTIONS = {
|
|
1139
|
+
en: EN_SECTIONS,
|
|
1140
|
+
ko: KO_SECTIONS
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
// ../templates/dist/fsd/fsd-eslint.js
|
|
1144
|
+
var FSD_ESLINT_MODULE = "@clearlydev/nx-archweave/eslint";
|
|
1145
|
+
function buildFsdEslintImportLine() {
|
|
1146
|
+
return `import { fsdBoundaries } from '${FSD_ESLINT_MODULE}';`;
|
|
1147
|
+
}
|
|
1148
|
+
function buildFsdEslintEntry(severity) {
|
|
1149
|
+
return `...fsdBoundaries({ severity: '${severity}' }),`;
|
|
1150
|
+
}
|
|
1151
|
+
function buildFsdEslintSnippet({ severity, locale }) {
|
|
1152
|
+
const lines = locale === "ko" ? [
|
|
1153
|
+
"// 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
|
+
"// 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."
|
|
1156
|
+
] : [
|
|
1157
|
+
"// nx-archweave FSD layer rules \u2014 add these to the frontend app's eslint.config.mjs yourself:",
|
|
1158
|
+
"// 1) the import after the last import,",
|
|
1159
|
+
"// 2) the spread as the last element of the export default [...] array."
|
|
1160
|
+
];
|
|
1161
|
+
return [...lines, buildFsdEslintImportLine(), buildFsdEslintEntry(severity)].join("\n");
|
|
1162
|
+
}
|
|
1163
|
+
var LAYER_GUIDE_EN = {
|
|
1164
|
+
app: "composition root: providers, routing, global styles (the framework entry point lives here)",
|
|
1165
|
+
pages: "one slice per screen, composed from the layers below",
|
|
1166
|
+
features: "one slice per user-facing capability (add-to-cart, sign-in)",
|
|
1167
|
+
entities: "one slice per business object (user, product)",
|
|
1168
|
+
shared: "segments, not slices: ui, api, lib, model, config \u2014 named by purpose"
|
|
1169
|
+
};
|
|
1170
|
+
var LAYER_GUIDE_KO = {
|
|
1171
|
+
app: "\uC870\uD569 \uB8E8\uD2B8: providers, \uB77C\uC6B0\uD305, \uC804\uC5ED \uC2A4\uD0C0\uC77C (\uD504\uB808\uC784\uC6CC\uD06C \uC9C4\uC785\uC810\uC774 \uC5EC\uAE30 \uC788\uC74C)",
|
|
1172
|
+
pages: "\uD654\uBA74 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 \u2014 \uC544\uB798 \uB808\uC774\uC5B4\uB4E4\uC744 \uC870\uD569",
|
|
1173
|
+
features: "\uC0AC\uC6A9\uC790 \uAE30\uB2A5 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 (add-to-cart, sign-in)",
|
|
1174
|
+
entities: "\uBE44\uC988\uB2C8\uC2A4 \uAC1D\uCCB4 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 (user, product)",
|
|
1175
|
+
shared: "\uC2AC\uB77C\uC774\uC2A4\uAC00 \uC544\uB2C8\uB77C \uC138\uADF8\uBA3C\uD2B8: ui, api, lib, model, config \u2014 \uBAA9\uC801\uC73C\uB85C \uC774\uB984 \uC9D3\uAE30"
|
|
1176
|
+
};
|
|
1177
|
+
function buildFsdLayerGuide({ layers, directories, sourceRoot, locale }) {
|
|
1178
|
+
const guide = locale === "ko" ? LAYER_GUIDE_KO : LAYER_GUIDE_EN;
|
|
1179
|
+
const width = Math.max(...directories.map((d) => d.length));
|
|
1180
|
+
const rows = layers.map((layer, index) => ` ${sourceRoot}/${directories[index].padEnd(width)} ${guide[layer] ?? ""}`);
|
|
1181
|
+
const footer = locale === "ko" ? [
|
|
1182
|
+
"\uB808\uC774\uC5B4 \uD3F4\uB354\uB294 \uCCAB \uC2AC\uB77C\uC774\uC2A4\uAC00 \uC0DD\uAE38 \uB54C \uB9CC\uB4E4\uC5B4\uC9D1\uB2C8\uB2E4 (\uC815\uC2DD FSD):",
|
|
1183
|
+
" nx g @clearlydev/nx-archweave:feature --name=<name> (page, entity, shared\uB3C4 \uAC19\uC740 \uBC29\uC2DD; --segments=ui,api)",
|
|
1184
|
+
" widgets/ \uB808\uC774\uC5B4\uB294 \uAE30\uBCF8 \uBC94\uC704 \uBC16\uC774\uC9C0\uB9CC nx g @clearlydev/nx-archweave:widget \uC73C\uB85C \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
1185
|
+
"\uC704\uC5D0\uC11C \uC544\uB798\uB85C\uB9CC import\uD560 \uC218 \uC788\uACE0, \uAC19\uC740 \uB808\uC774\uC5B4\uC758 \uC2AC\uB77C\uC774\uC2A4\uB07C\uB9AC\uB294 import\uD558\uC9C0 \uC54A\uC73C\uBA70, \uC2AC\uB77C\uC774\uC2A4\uB294",
|
|
1186
|
+
"index.ts\uB97C \uD1B5\uD574\uC11C\uB9CC import\uD569\uB2C8\uB2E4 \u2014 archweave/fsd-boundaries ESLint \uADDC\uCE59\uC774 \uC704\uBC18\uC744 \uC54C\uB824 \uC90D\uB2C8\uB2E4."
|
|
1187
|
+
] : [
|
|
1188
|
+
"Layer directories appear with their first slice (canonical FSD):",
|
|
1189
|
+
" nx g @clearlydev/nx-archweave:feature --name=<name> (page, entity and shared work the same way; --segments=ui,api)",
|
|
1190
|
+
" widgets/ is outside the default scope but one command away: nx g @clearlydev/nx-archweave:widget",
|
|
1191
|
+
"Imports go downward only, slices on one layer never import each other, and a slice is",
|
|
1192
|
+
"imported through its index.ts \u2014 the archweave/fsd-boundaries ESLint rule reports violations."
|
|
1193
|
+
];
|
|
1194
|
+
return [...rows, ...footer].join("\n");
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// ../templates/dist/fsd/fsd-steiger.js
|
|
1198
|
+
var STEIGER_DEPENDENCIES = {
|
|
1199
|
+
steiger: "^0.7.0",
|
|
1200
|
+
"@feature-sliced/steiger-plugin": "^0.8.0"
|
|
1201
|
+
};
|
|
1202
|
+
var STEIGER_CONFIG_FILE = "steiger.config.mjs";
|
|
1203
|
+
var FSD_CHECK_TARGET = "fsd-check";
|
|
1204
|
+
function buildFsdCheckTarget(sourceRootName = "src") {
|
|
1205
|
+
return { command: `steiger ${sourceRootName}`, options: { cwd: "{projectRoot}" } };
|
|
1206
|
+
}
|
|
1207
|
+
function buildSteigerConfig({ frontend, locale }) {
|
|
1208
|
+
const ko = locale === "ko";
|
|
1209
|
+
const lines = [
|
|
1210
|
+
"import { defineConfig } from 'steiger';",
|
|
1211
|
+
"import fsd from '@feature-sliced/steiger-plugin';",
|
|
1212
|
+
"",
|
|
1213
|
+
ko ? "// nx-archweave: \uC815\uC2DD FSD \uADDC\uCE59 \uC804\uCCB4(recommended). \uAD6C\uC870\uB294 generator\uAC00 \uC815\uC2DD\uB300\uB85C \uB9CC\uB4E4\uBBC0\uB85C \uADF8\uB300\uB85C \uB9DE\uBB3C\uB9BD\uB2C8\uB2E4." : "// nx-archweave: the canonical rule set (recommended). The generators scaffold canonical FSD, so it fits as is.",
|
|
1214
|
+
"export default defineConfig([",
|
|
1215
|
+
" ...fsd.configs.recommended,",
|
|
1216
|
+
" {",
|
|
1217
|
+
" rules: {",
|
|
1218
|
+
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
|
+
" '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",
|
|
1222
|
+
" 'fsd/typo-in-layer-name': 'off',"
|
|
1223
|
+
] : [],
|
|
1224
|
+
" },",
|
|
1225
|
+
" },",
|
|
1226
|
+
"]);",
|
|
1227
|
+
""
|
|
1228
|
+
];
|
|
1229
|
+
return lines.join("\n");
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// ../templates/dist/backend/shared-types.js
|
|
1233
|
+
function sharedTypesIndexContent() {
|
|
1234
|
+
return `export * from './lib/api-types.js';
|
|
1235
|
+
`;
|
|
1236
|
+
}
|
|
1237
|
+
function sharedTypesApiTypesContent() {
|
|
1238
|
+
return [
|
|
1239
|
+
"// Types shared between the frontend and the backend live here.",
|
|
1240
|
+
"// Example: the api app returns this from GET /api/health, and the",
|
|
1241
|
+
"// frontend consumes the same shape \u2014 one definition, no drift.",
|
|
1242
|
+
"export interface ApiHealthResponse {",
|
|
1243
|
+
" status: 'ok';",
|
|
1244
|
+
"}",
|
|
1245
|
+
""
|
|
1246
|
+
].join("\n");
|
|
1247
|
+
}
|
|
1248
|
+
function sharedTypesPackageJsonContent(packageName) {
|
|
1249
|
+
return `${JSON.stringify({
|
|
1250
|
+
name: packageName,
|
|
1251
|
+
version: "0.0.1",
|
|
1252
|
+
private: true,
|
|
1253
|
+
type: "module",
|
|
1254
|
+
main: "./src/index.ts",
|
|
1255
|
+
types: "./src/index.ts",
|
|
1256
|
+
exports: {
|
|
1257
|
+
".": {
|
|
1258
|
+
types: "./src/index.ts",
|
|
1259
|
+
import: "./src/index.ts",
|
|
1260
|
+
default: "./src/index.ts"
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
nx: {
|
|
1264
|
+
projectType: "library",
|
|
1265
|
+
tags: ["scope:shared"],
|
|
1266
|
+
targets: {}
|
|
1267
|
+
}
|
|
1268
|
+
}, null, 2)}
|
|
1269
|
+
`;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// ../template-engine/dist/fsd/fsd-eslint-config.js
|
|
1273
|
+
function classifyFsdEslintConfig(configText) {
|
|
1274
|
+
return maskComments(configText).includes(FSD_ESLINT_MODULE) ? "applied" : "absent";
|
|
1275
|
+
}
|
|
1276
|
+
var EXPORT_DEFAULT_ARRAY = /export\s+default\s+\[/g;
|
|
1277
|
+
var IMPORT_STATEMENT = /^import\b[^;]*?\bfrom\s+['"][^'"]+['"]\s*;?[ \t]*$/gm;
|
|
1278
|
+
var CLOSING_BRACKET = /\]\s*;?\s*$/;
|
|
1279
|
+
function applyFsdEslintConfig(configText, severity) {
|
|
1280
|
+
if (classifyFsdEslintConfig(configText) === "applied")
|
|
1281
|
+
return { status: "already-applied" };
|
|
1282
|
+
const masked = maskComments(configText);
|
|
1283
|
+
EXPORT_DEFAULT_ARRAY.lastIndex = 0;
|
|
1284
|
+
const exportMatches = masked.match(EXPORT_DEFAULT_ARRAY) ?? [];
|
|
1285
|
+
const closing = CLOSING_BRACKET.exec(masked);
|
|
1286
|
+
if (exportMatches.length !== 1 || closing === null)
|
|
1287
|
+
return { status: "unrecognized" };
|
|
1288
|
+
const eol = configText.includes("\r\n") ? "\r\n" : "\n";
|
|
1289
|
+
const closingIndex = closing.index;
|
|
1290
|
+
const before = configText.slice(0, closingIndex);
|
|
1291
|
+
const lastNonSpace = before.replace(/\s+$/, "");
|
|
1292
|
+
const needsComma = !lastNonSpace.endsWith(",") && !lastNonSpace.endsWith("[");
|
|
1293
|
+
const entryText = `${needsComma ? "," : ""}${eol} ${buildFsdEslintEntry(severity)}${eol}`;
|
|
1294
|
+
const withEntry = `${lastNonSpace}${entryText}${configText.slice(closingIndex)}`;
|
|
1295
|
+
IMPORT_STATEMENT.lastIndex = 0;
|
|
1296
|
+
let lastImportEnd = -1;
|
|
1297
|
+
const maskedWithEntry = maskComments(withEntry);
|
|
1298
|
+
for (let m = IMPORT_STATEMENT.exec(maskedWithEntry); m !== null; m = IMPORT_STATEMENT.exec(maskedWithEntry)) {
|
|
1299
|
+
lastImportEnd = m.index + m[0].length;
|
|
1300
|
+
}
|
|
1301
|
+
const importLine = buildFsdEslintImportLine();
|
|
1302
|
+
const text = lastImportEnd === -1 ? `${importLine}${eol}${eol}${withEntry}` : `${withEntry.slice(0, lastImportEnd)}${eol}${importLine}${withEntry.slice(lastImportEnd)}`;
|
|
1303
|
+
return { status: "applied", text };
|
|
1304
|
+
}
|
|
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];
|
|
1315
|
+
}
|
|
1316
|
+
function composeFsdLayerGuide({ frontend, sourceRoot, locale }) {
|
|
1317
|
+
const layers = fsdLayersFor(frontend);
|
|
1318
|
+
return buildFsdLayerGuide({ layers, directories: layers.map((layer) => fsdLayerDirName(frontend, layer)), sourceRoot, locale });
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// ../template-engine/dist/ai-hosts/ai-hosts.js
|
|
1322
|
+
var AI_HOSTS = [
|
|
1323
|
+
{ id: "claude-code", label: "Claude Code", nxAgent: "claude", rulesFile: "CLAUDE.md", reviewProvider: "claude" },
|
|
1324
|
+
{ id: "codex", label: "Codex", nxAgent: "codex", rulesFile: "AGENTS.md", reviewProvider: "gpt" },
|
|
1325
|
+
{ id: "cursor", label: "Cursor", nxAgent: "cursor", rulesFile: "AGENTS.md", reviewProvider: "cursor" },
|
|
1326
|
+
{ id: "gemini-cli", label: "Gemini CLI", nxAgent: "gemini", rulesFile: "AGENTS.md", reviewProvider: "gemini" }
|
|
1327
|
+
];
|
|
1328
|
+
var AI_HOST_IDS = AI_HOSTS.map((host) => host.id);
|
|
1329
|
+
var DEFAULT_AI_HOSTS = ["claude-code"];
|
|
1330
|
+
function isAiHost(value) {
|
|
1331
|
+
return typeof value === "string" && AI_HOST_IDS.includes(value);
|
|
1332
|
+
}
|
|
1333
|
+
function definitionOf(id) {
|
|
1334
|
+
const host = AI_HOSTS.find((candidate) => candidate.id === id);
|
|
1335
|
+
if (!host)
|
|
1336
|
+
throw new Error(`Unknown AI host "${id}"`);
|
|
1337
|
+
return host;
|
|
1338
|
+
}
|
|
1339
|
+
function rulesFilesFor(hosts) {
|
|
1340
|
+
const picked = hosts.length > 0 ? hosts : DEFAULT_AI_HOSTS;
|
|
1341
|
+
const files = new Set(picked.map((id) => definitionOf(id).rulesFile));
|
|
1342
|
+
return ["CLAUDE.md", "AGENTS.md"].filter((file) => files.has(file));
|
|
1343
|
+
}
|
|
1344
|
+
function reviewProviderFor(hosts) {
|
|
1345
|
+
return definitionOf(hosts[0] ?? DEFAULT_AI_HOSTS[0]).reviewProvider;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// ../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
|
+
function buildFsdConfig(enforcement, frontend) {
|
|
1352
|
+
return {
|
|
1353
|
+
// All three presets enable FSD — it is on whenever there is a
|
|
1354
|
+
// frontend; a backend-only workspace has none to structure.
|
|
1355
|
+
enabled: frontend !== "none",
|
|
1356
|
+
enforcement,
|
|
1357
|
+
layerMapping: {
|
|
1358
|
+
"react-vite": REACT_VITE_LAYERS2,
|
|
1359
|
+
next: NEXT_LAYERS2
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
var PRESET_DEFAULTS = {
|
|
1364
|
+
"quick-start": {
|
|
1365
|
+
// add-review: opt-in (a single install question), `fast` mode by default.
|
|
1366
|
+
reviewEnabledDefault: false,
|
|
1367
|
+
reviewMode: "fast",
|
|
1368
|
+
// add-fsd-lite: enabled with every default layer, no questions — violations warn.
|
|
1369
|
+
fsdEnforcement: "warn",
|
|
1370
|
+
// add-agent-rules: enabled (simple version). Which files follow from the AI hosts
|
|
1371
|
+
// picked, no longer from the preset.
|
|
1372
|
+
agentRulesTeamRules: false,
|
|
1373
|
+
// add-github-templates: minimal (PR template only).
|
|
1374
|
+
githubTemplates: { issue: false, pr: true, codeowners: false }
|
|
1375
|
+
},
|
|
1376
|
+
solo: {
|
|
1377
|
+
// add-review: `fast` mode by default — no opt-in question, so enabled by default.
|
|
1378
|
+
reviewEnabledDefault: true,
|
|
1379
|
+
reviewMode: "fast",
|
|
1380
|
+
// add-fsd-lite: enabled — violations warn (no over-enforcement).
|
|
1381
|
+
fsdEnforcement: "warn",
|
|
1382
|
+
// add-agent-rules: enabled — files follow the AI hosts picked.
|
|
1383
|
+
agentRulesTeamRules: false,
|
|
1384
|
+
// add-github-templates: optional — see note #2 above.
|
|
1385
|
+
githubTemplates: { issue: false, pr: true, codeowners: false }
|
|
1386
|
+
},
|
|
1387
|
+
team: {
|
|
1388
|
+
// Requiring review only for P2 findings when CODEOWNERS is wired up is out of v0.1
|
|
1389
|
+
// scope and has no field here. The other two Team-only behaviors are the two
|
|
1390
|
+
// preset-specific fields below.
|
|
1391
|
+
// add-review: `standard` mode by default, `thorough` available — standard is the
|
|
1392
|
+
// default mode; `thorough` stays a user override this function does not apply automatically.
|
|
1393
|
+
reviewEnabledDefault: true,
|
|
1394
|
+
reviewMode: "standard",
|
|
1395
|
+
// add-fsd-lite: enabled + stricter module boundaries — the canonical FSD rule
|
|
1396
|
+
// reports as an error here, as a warning in the other presets.
|
|
1397
|
+
fsdEnforcement: "error",
|
|
1398
|
+
// add-agent-rules: enabled + an extra team-rules section — the extra section is
|
|
1399
|
+
// content `template-engine/agent-rules` composes, not a different `files` list.
|
|
1400
|
+
agentRulesTeamRules: true,
|
|
1401
|
+
// add-github-templates: full (Issue+PR, CODEOWNERS optional) — see note #2 above.
|
|
1402
|
+
githubTemplates: { issue: true, pr: true, codeowners: false }
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
function buildPresetConfig(input) {
|
|
1406
|
+
const defaults = PRESET_DEFAULTS[input.preset];
|
|
1407
|
+
const reviewEnabled = input.reviewEnabled ?? defaults.reviewEnabledDefault;
|
|
1408
|
+
const aiHosts = input.aiHosts !== void 0 && input.aiHosts.length > 0 ? [...input.aiHosts] : [...DEFAULT_AI_HOSTS];
|
|
1409
|
+
return {
|
|
1410
|
+
preset: input.preset,
|
|
1411
|
+
packageManager: input.packageManager,
|
|
1412
|
+
frontend: input.frontend,
|
|
1413
|
+
backend: input.backend,
|
|
1414
|
+
locale: input.locale,
|
|
1415
|
+
aiHosts,
|
|
1416
|
+
fsd: buildFsdConfig(defaults.fsdEnforcement, input.frontend),
|
|
1417
|
+
review: reviewEnabled ? buildReviewConfigSnippet({
|
|
1418
|
+
enabled: true,
|
|
1419
|
+
provider: input.reviewProvider ?? reviewProviderFor(aiHosts),
|
|
1420
|
+
mode: defaults.reviewMode,
|
|
1421
|
+
locale: input.locale,
|
|
1422
|
+
maxRounds: DEFAULT_MAX_ROUNDS,
|
|
1423
|
+
// preset mapping never overrides maxRounds.
|
|
1424
|
+
packageManager: input.packageManager
|
|
1425
|
+
// snippet ignores it; the type rides ReviewWorkflowInput
|
|
1426
|
+
}) : { enabled: false },
|
|
1427
|
+
agentRules: { teamRules: defaults.agentRulesTeamRules },
|
|
1428
|
+
githubTemplates: defaults.githubTemplates,
|
|
1429
|
+
// conflictPolicy.default is 'merge-proposal' — presets have no conflictPolicy
|
|
1430
|
+
// setting, so every preset keeps this one default.
|
|
1431
|
+
conflictPolicy: { default: DEFAULT_CONFLICT_POLICY },
|
|
1432
|
+
// No preset ever auto-fills integrations — always user-driven via
|
|
1433
|
+
// `add-integration`.
|
|
1434
|
+
integrations: { enabled: [] }
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// ../template-engine/dist/agent-rules/compose-agent-rules.js
|
|
1439
|
+
function resolveDefaultAgentSectionIds(teamRules, fsdStructure = false) {
|
|
1440
|
+
let ids = [...DEFAULT_AGENT_SECTION_IDS];
|
|
1441
|
+
const insertAfter = (anchorId, id) => {
|
|
1442
|
+
const anchor = ids.indexOf(anchorId);
|
|
1443
|
+
if (anchor === -1) {
|
|
1444
|
+
throw new Error(`composeAgentRules: the default composition no longer contains ${anchorId}`);
|
|
1445
|
+
}
|
|
1446
|
+
ids = [...ids.slice(0, anchor + 1), id, ...ids.slice(anchor + 1)];
|
|
1447
|
+
};
|
|
1448
|
+
if (fsdStructure)
|
|
1449
|
+
insertAfter("code-structure", FSD_AGENT_SECTION_ID);
|
|
1450
|
+
if (teamRules)
|
|
1451
|
+
insertAfter("language-policy", TEAM_AGENT_SECTION_ID);
|
|
1452
|
+
return ids;
|
|
1453
|
+
}
|
|
1454
|
+
function renderSections(sections) {
|
|
1455
|
+
return `${sections.map((section) => `## ${section.heading}
|
|
1456
|
+
|
|
1457
|
+
${section.body}`).join("\n\n")}
|
|
1458
|
+
`;
|
|
1459
|
+
}
|
|
1460
|
+
function composeAgentRules(input) {
|
|
1461
|
+
const sectionIds = input.sections ?? resolveDefaultAgentSectionIds(input.teamRules ?? false, input.fsdStructure ?? false);
|
|
1462
|
+
const sections = sectionIds.map((id) => AGENT_RULE_SECTIONS[input.locale][id]);
|
|
1463
|
+
const content = renderSections(sections);
|
|
1464
|
+
return input.files.map((filePath) => ({ filePath, content }));
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// ../template-engine/dist/agent-rules/validate-agent-rules.js
|
|
1468
|
+
var LOCALES = ["en", "ko"];
|
|
1469
|
+
function headingsFor(id) {
|
|
1470
|
+
return LOCALES.map((locale) => `## ${AGENT_RULE_SECTIONS[locale][id].heading}`);
|
|
1471
|
+
}
|
|
1472
|
+
var REQUIRED_SECTION_IDS = ["safety-rules", "language-policy"];
|
|
1473
|
+
function validateAgentRules(content, extraRequired = []) {
|
|
1474
|
+
return [...REQUIRED_SECTION_IDS, ...extraRequired].every((id) => headingsFor(id).some((heading) => content.includes(heading)));
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// ../template-engine/dist/agent-rules/marker-block.js
|
|
1478
|
+
var AGENT_RULES_BLOCK_START = "<!-- nx-archweave:agent-rules start -->";
|
|
1479
|
+
var AGENT_RULES_BLOCK_END = "<!-- nx-archweave:agent-rules end -->";
|
|
1480
|
+
function renderAgentRulesBlock(body, eol = "\n") {
|
|
1481
|
+
const lines = `${AGENT_RULES_BLOCK_START}
|
|
1482
|
+
${body.endsWith("\n") ? body : `${body}
|
|
1483
|
+
`}${AGENT_RULES_BLOCK_END}`;
|
|
1484
|
+
return eol === "\n" ? lines : lines.replace(/\r?\n/g, eol);
|
|
1485
|
+
}
|
|
1486
|
+
function countOf(text, needle) {
|
|
1487
|
+
return text.split(needle).length - 1;
|
|
1488
|
+
}
|
|
1489
|
+
function applyAgentRulesBlock(existing, body) {
|
|
1490
|
+
if (existing === null || existing.length === 0)
|
|
1491
|
+
return { kind: "create", text: `${renderAgentRulesBlock(body)}
|
|
1492
|
+
` };
|
|
1493
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
1494
|
+
const block = renderAgentRulesBlock(body, eol);
|
|
1495
|
+
const starts = countOf(existing, AGENT_RULES_BLOCK_START);
|
|
1496
|
+
const ends = countOf(existing, AGENT_RULES_BLOCK_END);
|
|
1497
|
+
if (starts === 0 && ends === 0) {
|
|
1498
|
+
const head = existing.endsWith("\n") ? existing : `${existing}${eol}`;
|
|
1499
|
+
return { kind: "append", text: `${head}${eol}${block}${eol}` };
|
|
1500
|
+
}
|
|
1501
|
+
if (starts !== 1 || ends !== 1) {
|
|
1502
|
+
return {
|
|
1503
|
+
kind: "unrecognized",
|
|
1504
|
+
reason: `expected one ${AGENT_RULES_BLOCK_START} \u2026 ${AGENT_RULES_BLOCK_END} pair, found ${starts} start and ${ends} end marker(s)`
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
const start = existing.indexOf(AGENT_RULES_BLOCK_START);
|
|
1508
|
+
const end = existing.indexOf(AGENT_RULES_BLOCK_END);
|
|
1509
|
+
if (end < start)
|
|
1510
|
+
return { kind: "unrecognized", reason: "the end marker comes before the start marker" };
|
|
1511
|
+
const text = existing.slice(0, start) + block + existing.slice(end + AGENT_RULES_BLOCK_END.length);
|
|
1512
|
+
return text === existing ? { kind: "unchanged" } : { kind: "replace", text };
|
|
1513
|
+
}
|
|
1514
|
+
|
|
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
|
+
};
|
|
1529
|
+
}
|
|
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" };
|
|
1550
|
+
}
|
|
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;
|
|
1570
|
+
}
|
|
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);
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
if (c === "[" || c === "{")
|
|
1581
|
+
depth++;
|
|
1582
|
+
if (c === "]" || c === "}") {
|
|
1583
|
+
depth--;
|
|
1584
|
+
if (depth === 0)
|
|
1585
|
+
return i + 1;
|
|
1586
|
+
}
|
|
1587
|
+
i++;
|
|
1588
|
+
}
|
|
1589
|
+
return -1;
|
|
1590
|
+
}
|
|
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;
|
|
1619
|
+
}
|
|
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)}`;
|
|
1628
|
+
}
|
|
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)}`;
|
|
1633
|
+
}
|
|
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)}`;
|
|
1649
|
+
}
|
|
1650
|
+
return `${text.slice(0, last.valueEnd)},${eol}${indent}"workspaces": [${SHARED_TYPES_ENTRY}]${text.slice(last.valueEnd)}`;
|
|
1651
|
+
}
|
|
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);
|
|
1658
|
+
}
|
|
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);
|
|
1665
|
+
}
|
|
1666
|
+
function addSharedTypesWorkspaceGlob(yamlText) {
|
|
1667
|
+
if (/^\s*-\s*['"]?(libs\/shared-types|libs\/\*)['"]?\s*$/m.test(yamlText)) {
|
|
1668
|
+
return { status: "present" };
|
|
1669
|
+
}
|
|
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;
|
|
1680
|
+
} else {
|
|
1681
|
+
break;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
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);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/generators/shared/frontend-app.ts
|
|
1697
|
+
import { getProjects } from "@nx/devkit";
|
|
1698
|
+
|
|
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 {
|
|
1712
|
+
}
|
|
1713
|
+
return "library";
|
|
1714
|
+
}
|
|
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");
|
|
1721
|
+
}
|
|
1722
|
+
function isBackend(root, config) {
|
|
1723
|
+
return baseNameOf(root) === "api" || (config.tags ?? []).includes("scope:backend");
|
|
1724
|
+
}
|
|
1725
|
+
function sourceRootOf(config) {
|
|
1726
|
+
if (config.sourceRoot) return config.sourceRoot;
|
|
1727
|
+
return config.root === "" || config.root === "." ? "src" : `${config.root}/src`;
|
|
1728
|
+
}
|
|
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 };
|
|
1737
|
+
}
|
|
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 };
|
|
1747
|
+
}
|
|
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);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/generators/shared/toolkit-config-file.ts
|
|
1761
|
+
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
|
+
function readToolkitConfigFile(tree) {
|
|
1770
|
+
const raw = tree.read(TOOLKIT_CONFIG_PATH, "utf-8");
|
|
1771
|
+
if (raw === null) return { raw: null, parsed: null };
|
|
1772
|
+
try {
|
|
1773
|
+
const parsed = JSON.parse(raw);
|
|
1774
|
+
return {
|
|
1775
|
+
raw,
|
|
1776
|
+
parsed: typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null
|
|
1777
|
+
};
|
|
1778
|
+
} catch {
|
|
1779
|
+
return { raw, parsed: null };
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function readToolkitConfig(tree) {
|
|
1783
|
+
return readToolkitConfigFile(tree).parsed;
|
|
1784
|
+
}
|
|
1785
|
+
function resolveLocaleOption(option, config) {
|
|
1786
|
+
if (option) return option;
|
|
1787
|
+
const locale = config?.["locale"];
|
|
1788
|
+
return locale === "ko" || locale === "en" ? locale : "en";
|
|
1789
|
+
}
|
|
1790
|
+
function recordedChoice(config, key, allowed) {
|
|
1791
|
+
const value = config?.[key];
|
|
1792
|
+
return typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
1793
|
+
}
|
|
1794
|
+
var RECORDED_FRONTENDS = ["react-vite", "next", "none"];
|
|
1795
|
+
var RECORDED_BACKENDS = ["express", "nest", "none"];
|
|
1796
|
+
var RECORDED_PRESETS = ["quick-start", "solo", "team"];
|
|
1797
|
+
var RECORDED_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
1798
|
+
function resolveFrontendOption(option, config, generator) {
|
|
1799
|
+
const frontend = option ?? recordedChoice(config, "frontend", RECORDED_FRONTENDS);
|
|
1800
|
+
if (frontend === void 0) {
|
|
1801
|
+
throw new Error(`${generator}: pass --frontend=react-vite|next (toolkit.config.json records none \u2014 init writes it).`);
|
|
1802
|
+
}
|
|
1803
|
+
if (frontend === "none") {
|
|
1804
|
+
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.`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
return frontend;
|
|
1809
|
+
}
|
|
1810
|
+
function resolveInitFrontend(option, config) {
|
|
1811
|
+
const frontend = option ?? recordedChoice(config, "frontend", RECORDED_FRONTENDS);
|
|
1812
|
+
if (frontend === void 0) {
|
|
1813
|
+
throw new Error("init: pass --frontend=react-vite|next|none (toolkit.config.json records none \u2014 init writes it).");
|
|
1814
|
+
}
|
|
1815
|
+
return frontend;
|
|
1816
|
+
}
|
|
1817
|
+
function workspacePackageManager(exists, read, config) {
|
|
1818
|
+
return detectWorkspacePackageManager(exists, read) ?? recordedChoice(config, "packageManager", RECORDED_PACKAGE_MANAGERS) ?? null;
|
|
1819
|
+
}
|
|
1820
|
+
function treePackageManager(tree, config) {
|
|
1821
|
+
return workspacePackageManager((p) => tree.exists(p), (p) => tree.read(p, "utf-8"), config);
|
|
1822
|
+
}
|
|
1823
|
+
function resolveAiHostsOption(option, config) {
|
|
1824
|
+
if (option !== void 0 && option.length > 0) return [...new Set(option)];
|
|
1825
|
+
const recorded = config?.["aiHosts"];
|
|
1826
|
+
const fromConfig = Array.isArray(recorded) ? [...new Set(recorded.filter(isAiHost))] : [];
|
|
1827
|
+
return fromConfig.length > 0 ? fromConfig : [...DEFAULT_AI_HOSTS];
|
|
1828
|
+
}
|
|
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";
|
|
1836
|
+
}
|
|
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);
|
|
1842
|
+
}
|
|
1843
|
+
return raw === null ? "created" : "updated";
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// src/generators/add-fsd-lite/render.ts
|
|
1847
|
+
function renderAddFsdLiteReport(report) {
|
|
1848
|
+
const lines = [];
|
|
1849
|
+
if (report.aborted) {
|
|
1850
|
+
lines.push("add-fsd-lite aborted \u2014 nothing was written.");
|
|
1851
|
+
const refusal = report.appRefusal;
|
|
1852
|
+
if (refusal !== null) {
|
|
1853
|
+
lines.push("");
|
|
1854
|
+
lines.push(" FSD layers live inside the frontend app's source root (e.g. apps/web/src), and no app could be picked:");
|
|
1855
|
+
if (refusal.reason === "project-not-found") {
|
|
1856
|
+
lines.push(` --project=${refusal.project} names no project in this workspace.`);
|
|
1857
|
+
} else if (refusal.reason === "ambiguous-frontend") {
|
|
1858
|
+
lines.push(" more than one frontend app was found \u2014 rerun with --project=<one of them>:");
|
|
1859
|
+
for (const name of refusal.candidates) lines.push(` - ${name}`);
|
|
1860
|
+
} else if (refusal.reason === "invalid-project-json") {
|
|
1861
|
+
lines.push(` a project.json could not be parsed (${refusal.detail}) \u2014 fix it, then rerun.`);
|
|
1862
|
+
} else {
|
|
1863
|
+
lines.push(" the workspace has no frontend application \u2014 create one (e.g. nx g @nx/react:app) or pass --project.");
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return lines.join("\n");
|
|
1867
|
+
}
|
|
1868
|
+
if (report.dryRun) {
|
|
1869
|
+
lines.push("add-fsd-lite dry-run \u2014 no files were written.");
|
|
1870
|
+
lines.push("");
|
|
1871
|
+
}
|
|
1872
|
+
lines.push(`FSD layers (in ${report.project}, ${report.sourceRoot}):`);
|
|
1873
|
+
if (report.layerGuide !== null) lines.push(report.layerGuide);
|
|
1874
|
+
lines.push("");
|
|
1875
|
+
const where = report.eslintPath ?? `${report.project}'s eslint.config.mjs`;
|
|
1876
|
+
switch (report.eslint) {
|
|
1877
|
+
case "applied":
|
|
1878
|
+
lines.push(`ESLint: archweave/fsd-boundaries (${report.enforcement}) written to ${where}`);
|
|
1879
|
+
break;
|
|
1880
|
+
case "already-applied":
|
|
1881
|
+
lines.push(`ESLint: archweave/fsd-boundaries already wired in ${where} \u2014 no change`);
|
|
1882
|
+
break;
|
|
1883
|
+
case "degraded-unrecognized":
|
|
1884
|
+
lines.push(`ESLint: ${report.eslintPath === null ? `no ESLint config found for ${report.project}` : `${where} was NOT recognized \u2014 left untouched`}.`);
|
|
1885
|
+
lines.push(" Add the rule yourself (then the layer rules are actually enforced):");
|
|
1886
|
+
break;
|
|
1887
|
+
case "snippet-only":
|
|
1888
|
+
lines.push(`ESLint: report-only (pass --applyEslint to write the rule into ${where}).`);
|
|
1889
|
+
break;
|
|
1890
|
+
case "not-applied":
|
|
1891
|
+
break;
|
|
1892
|
+
}
|
|
1893
|
+
if (report.snippet !== null) lines.push(report.snippet);
|
|
1894
|
+
if (report.steiger !== null) {
|
|
1895
|
+
const st = report.steiger;
|
|
1896
|
+
lines.push("");
|
|
1897
|
+
lines.push("steiger (official FSD linter, opt-in):");
|
|
1898
|
+
lines.push(
|
|
1899
|
+
` ${st.configPath}: ${st.config === "created" ? "created" : st.config === "unchanged" ? "unchanged" : "kept \u2014 an existing config with other content is never replaced"}`
|
|
1900
|
+
);
|
|
1901
|
+
lines.push(
|
|
1902
|
+
st.target === "added" ? ` target ${"fsd-check"}: added to ${st.targetPath}` : st.target === "present" ? ` target ${"fsd-check"}: already present in ${st.targetPath}` : " target fsd-check: NOT added \u2014 no parseable project.json / package.json for the app"
|
|
1903
|
+
);
|
|
1904
|
+
lines.push(
|
|
1905
|
+
st.dependencies === "added" ? ` devDependencies: steiger + @feature-sliced/steiger-plugin added to package.json \u2014 run \`${st.installCommand}\`` : st.dependencies === "present" ? " devDependencies: already present" : " devDependencies: NOT added \u2014 the root package.json could not be parsed"
|
|
1906
|
+
);
|
|
1907
|
+
lines.push(` run it with: ${st.runCommand} (steiger needs Node >= 22.18)`);
|
|
1908
|
+
}
|
|
1909
|
+
return lines.join("\n");
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/generators/add-fsd-lite/generator.ts
|
|
1913
|
+
function resolveEnforcement(option, config) {
|
|
1914
|
+
if (option === "error" || option === "warn") return option;
|
|
1915
|
+
return readFsdEnforcement(config?.["fsd"]) ?? "warn";
|
|
1916
|
+
}
|
|
1917
|
+
function runAddFsdLite(tree, options) {
|
|
1918
|
+
const config = readToolkitConfig(tree);
|
|
1919
|
+
const locale = resolveLocaleOption(options.locale, config);
|
|
1920
|
+
const enforcement = resolveEnforcement(options.enforcement, config);
|
|
1921
|
+
const frontend = resolveFrontendOption(options.frontend, config, "add-fsd-lite");
|
|
1922
|
+
const dryRun = options.dryRun ?? false;
|
|
1923
|
+
const applyEslint = options.applyEslint ?? false;
|
|
1924
|
+
const snippet = buildFsdEslintPasteSnippet(enforcement, locale);
|
|
1925
|
+
const app = resolveFrontendApp(tree, options.project);
|
|
1926
|
+
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 };
|
|
1929
|
+
}
|
|
1930
|
+
const appDir = app.root === "" || app.root === "." ? "" : `${app.root}/`;
|
|
1931
|
+
const eslintPath = ESLINT_CONFIG_CANDIDATES.map((name) => `${appDir}${name}`).find((path) => tree.exists(path)) ?? null;
|
|
1932
|
+
const configText = eslintPath === null ? null : tree.read(eslintPath, "utf-8");
|
|
1933
|
+
let eslint;
|
|
1934
|
+
if (!applyEslint) {
|
|
1935
|
+
eslint = configText !== null && applyFsdEslintConfig(configText, enforcement).status === "already-applied" ? "already-applied" : "snippet-only";
|
|
1936
|
+
} else if (configText === null) {
|
|
1937
|
+
eslint = "degraded-unrecognized";
|
|
1938
|
+
} else {
|
|
1939
|
+
const result = applyFsdEslintConfig(configText, enforcement);
|
|
1940
|
+
eslint = result.status === "applied" ? "applied" : result.status === "already-applied" ? "already-applied" : "degraded-unrecognized";
|
|
1941
|
+
if (result.status === "applied" && !dryRun && eslintPath !== null) tree.write(eslintPath, result.text);
|
|
1942
|
+
}
|
|
1943
|
+
const steiger = options.steiger ? addSteiger(tree, { appDir, appRoot: app.root, sourceRoot: app.sourceRoot, frontend, locale, write: !dryRun, config }) : null;
|
|
1944
|
+
return {
|
|
1945
|
+
dryRun,
|
|
1946
|
+
aborted: false,
|
|
1947
|
+
appRefusal: null,
|
|
1948
|
+
project: app.project,
|
|
1949
|
+
sourceRoot: app.sourceRoot,
|
|
1950
|
+
steiger,
|
|
1951
|
+
enforcement,
|
|
1952
|
+
eslint,
|
|
1953
|
+
eslintPath,
|
|
1954
|
+
snippet: eslint === "applied" || eslint === "already-applied" ? null : snippet,
|
|
1955
|
+
layerGuide: composeFsdLayerGuide({ frontend, sourceRoot: app.sourceRoot, locale })
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
function readJsonObject(tree, path) {
|
|
1959
|
+
const raw = tree.read(path, "utf-8");
|
|
1960
|
+
if (raw === null) return null;
|
|
1961
|
+
try {
|
|
1962
|
+
const parsed = JSON.parse(raw);
|
|
1963
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { raw, parsed } : null;
|
|
1964
|
+
} catch {
|
|
1965
|
+
return null;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
function addSteiger(tree, input) {
|
|
1969
|
+
const configPath = `${input.appDir}${STEIGER_CONFIG_FILE}`;
|
|
1970
|
+
const configText = buildSteigerConfig({ frontend: input.frontend, locale: input.locale });
|
|
1971
|
+
const existing = tree.read(configPath, "utf-8");
|
|
1972
|
+
let configAction;
|
|
1973
|
+
if (existing === null) {
|
|
1974
|
+
configAction = "created";
|
|
1975
|
+
if (input.write) tree.write(configPath, configText);
|
|
1976
|
+
} else {
|
|
1977
|
+
configAction = existing === configText ? "unchanged" : "kept";
|
|
1978
|
+
}
|
|
1979
|
+
const rootPrefix = input.appRoot === "" || input.appRoot === "." ? "" : `${input.appRoot}/`;
|
|
1980
|
+
const sourceRootName = input.sourceRoot.startsWith(rootPrefix) ? input.sourceRoot.slice(rootPrefix.length) : "src";
|
|
1981
|
+
const target = buildFsdCheckTarget(sourceRootName);
|
|
1982
|
+
let targetAction = "unrecognized";
|
|
1983
|
+
let targetPath = null;
|
|
1984
|
+
for (const candidate of [`${input.appDir}project.json`, `${input.appDir}package.json`]) {
|
|
1985
|
+
const file = readJsonObject(tree, candidate);
|
|
1986
|
+
if (file === null) continue;
|
|
1987
|
+
targetPath = candidate;
|
|
1988
|
+
const holder = candidate.endsWith("project.json") ? file.parsed : file.parsed["nx"] ??= {};
|
|
1989
|
+
if (typeof holder !== "object" || holder === null) break;
|
|
1990
|
+
const targets = holder["targets"] ??= {};
|
|
1991
|
+
if (typeof targets !== "object" || targets === null) break;
|
|
1992
|
+
if (targets[FSD_CHECK_TARGET] !== void 0) {
|
|
1993
|
+
targetAction = "present";
|
|
1994
|
+
} else {
|
|
1995
|
+
targets[FSD_CHECK_TARGET] = target;
|
|
1996
|
+
targetAction = "added";
|
|
1997
|
+
if (input.write) tree.write(candidate, `${JSON.stringify(file.parsed, null, detectIndent(file.raw))}
|
|
1998
|
+
`);
|
|
1999
|
+
}
|
|
2000
|
+
break;
|
|
2001
|
+
}
|
|
2002
|
+
let dependenciesAction = "unrecognized";
|
|
2003
|
+
const rootPackage = readJsonObject(tree, "package.json");
|
|
2004
|
+
if (rootPackage !== null) {
|
|
2005
|
+
const devDependencies = rootPackage.parsed["devDependencies"] ??= {};
|
|
2006
|
+
const dependencies = rootPackage.parsed["dependencies"] ?? {};
|
|
2007
|
+
const missing = Object.entries(STEIGER_DEPENDENCIES).filter(([name]) => devDependencies[name] === void 0 && dependencies[name] === void 0);
|
|
2008
|
+
if (missing.length === 0) {
|
|
2009
|
+
dependenciesAction = "present";
|
|
2010
|
+
} else {
|
|
2011
|
+
for (const [name, range] of missing) devDependencies[name] = range;
|
|
2012
|
+
dependenciesAction = "added";
|
|
2013
|
+
if (input.write) tree.write("package.json", `${JSON.stringify(rootPackage.parsed, null, detectIndent(rootPackage.raw))}
|
|
2014
|
+
`);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
const pm = treePackageManager(tree, input.config) ?? "pnpm";
|
|
2018
|
+
const commands = packageManagerCommands(pm, pm === "yarn" ? detectYarnFlavor((p) => tree.exists(p), (p) => tree.read(p, "utf-8")) : void 0);
|
|
2019
|
+
return {
|
|
2020
|
+
configPath,
|
|
2021
|
+
config: configAction,
|
|
2022
|
+
targetPath,
|
|
2023
|
+
target: targetAction,
|
|
2024
|
+
dependencies: dependenciesAction,
|
|
2025
|
+
installCommand: commands.install,
|
|
2026
|
+
runCommand: `${commands.runNx} run <project>:${FSD_CHECK_TARGET}`
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// src/generators/add-agent-rules/render.ts
|
|
2031
|
+
var ACTION_LINE = {
|
|
2032
|
+
created: (result) => `${result.filePath}: created`,
|
|
2033
|
+
appended: (result) => `${result.filePath}: nx-archweave block added after the existing content (nothing else changed)`,
|
|
2034
|
+
updated: (result) => `${result.filePath}: nx-archweave block updated (nothing outside it changed)`,
|
|
2035
|
+
unchanged: (result) => `${result.filePath}: unchanged`,
|
|
2036
|
+
unrecognized: (result) => `${result.filePath}: not written \u2014 ${result.reason ?? "the nx-archweave markers could not be placed"}`
|
|
2037
|
+
};
|
|
2038
|
+
function renderAddAgentRulesReport(report) {
|
|
2039
|
+
const lines = [];
|
|
2040
|
+
lines.push(report.dryRun ? "add-agent-rules dry-run \u2014 no files were written." : "add-agent-rules:");
|
|
2041
|
+
lines.push(` hosts: ${report.hosts.join(", ")}`);
|
|
2042
|
+
lines.push(` locale: ${report.locale}`);
|
|
2043
|
+
lines.push(` teamRules: ${report.teamRules}`);
|
|
2044
|
+
lines.push("");
|
|
2045
|
+
for (const file of report.files) {
|
|
2046
|
+
lines.push(` ${ACTION_LINE[file.action](file)}`);
|
|
2047
|
+
}
|
|
2048
|
+
const RECORD_LINE = {
|
|
2049
|
+
created: "toolkit.config.json: aiHosts recorded (file created)",
|
|
2050
|
+
updated: "toolkit.config.json: aiHosts recorded",
|
|
2051
|
+
unchanged: "toolkit.config.json: aiHosts already recorded",
|
|
2052
|
+
unrecognized: "toolkit.config.json: not a JSON object \u2014 aiHosts not recorded (fix the file, then rerun)",
|
|
2053
|
+
"not-applied": "toolkit.config.json: aiHosts not recorded \u2014 a file above was not written",
|
|
2054
|
+
"not-requested": null
|
|
2055
|
+
};
|
|
2056
|
+
const recordLine = RECORD_LINE[report.hostsRecord];
|
|
2057
|
+
if (recordLine !== null) lines.push(` ${recordLine}`);
|
|
2058
|
+
if (report.files.some((file) => file.action === "unrecognized")) {
|
|
2059
|
+
lines.push("");
|
|
2060
|
+
lines.push('Manual fix needed: keep exactly one "<!-- nx-archweave:agent-rules start -->" \u2026 "<!-- nx-archweave:agent-rules end -->"');
|
|
2061
|
+
lines.push("pair in the file (or remove both markers), then rerun add-agent-rules.");
|
|
2062
|
+
}
|
|
2063
|
+
if (report.dryRun) {
|
|
2064
|
+
lines.push("");
|
|
2065
|
+
lines.push("Generated content preview:");
|
|
2066
|
+
for (const file of report.files) {
|
|
2067
|
+
if (file.content === null) continue;
|
|
2068
|
+
lines.push("");
|
|
2069
|
+
lines.push(`--- ${file.filePath} ---`);
|
|
2070
|
+
lines.push(file.content);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return lines.join("\n");
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// src/generators/add-agent-rules/generator.ts
|
|
2077
|
+
function resolveTeamRules(option, config) {
|
|
2078
|
+
if (option !== void 0) return option;
|
|
2079
|
+
const agentRules = config?.["agentRules"];
|
|
2080
|
+
const teamRules = typeof agentRules === "object" && agentRules !== null ? agentRules["teamRules"] : void 0;
|
|
2081
|
+
return typeof teamRules === "boolean" ? teamRules : false;
|
|
2082
|
+
}
|
|
2083
|
+
function resolveFsdStructure(config) {
|
|
2084
|
+
const fsd = config?.["fsd"];
|
|
2085
|
+
const enabled = typeof fsd === "object" && fsd !== null ? fsd["enabled"] : void 0;
|
|
2086
|
+
if (typeof enabled === "boolean") return enabled;
|
|
2087
|
+
const frontend = config?.["frontend"];
|
|
2088
|
+
return typeof frontend === "string" && frontend !== "none";
|
|
2089
|
+
}
|
|
2090
|
+
function runAddAgentRules(tree, options, { recordHosts = true, fsdStructure } = {}) {
|
|
2091
|
+
const config = readToolkitConfig(tree);
|
|
2092
|
+
const hosts = resolveAiHostsOption(options.hosts, config);
|
|
2093
|
+
const files = rulesFilesFor(hosts);
|
|
2094
|
+
const locale = resolveLocaleOption(options.locale, config);
|
|
2095
|
+
const teamRules = resolveTeamRules(options.teamRules, config);
|
|
2096
|
+
const dryRun = options.dryRun ?? false;
|
|
2097
|
+
const composed = composeAgentRules({ files, locale, teamRules, fsdStructure: fsdStructure ?? resolveFsdStructure(config) });
|
|
2098
|
+
const results = composed.map(({ filePath, content }) => {
|
|
2099
|
+
if (!validateAgentRules(content, teamRules ? ["team-rules"] : [])) {
|
|
2100
|
+
throw new Error(
|
|
2101
|
+
`Generated ${filePath} is missing a required section (Safety Rules, Language Policy${teamRules ? ", Team Rules" : ""})`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
const existing = tree.read(filePath, "utf-8");
|
|
2105
|
+
const outcome = applyAgentRulesBlock(existing, content);
|
|
2106
|
+
switch (outcome.kind) {
|
|
2107
|
+
case "unchanged":
|
|
2108
|
+
return { filePath, action: "unchanged", reason: null, content: null };
|
|
2109
|
+
case "unrecognized":
|
|
2110
|
+
return { filePath, action: "unrecognized", reason: outcome.reason, content: null };
|
|
2111
|
+
case "create":
|
|
2112
|
+
case "append":
|
|
2113
|
+
case "replace": {
|
|
2114
|
+
if (!dryRun) tree.write(filePath, outcome.text);
|
|
2115
|
+
const action = outcome.kind === "create" ? "created" : outcome.kind === "append" ? "appended" : "updated";
|
|
2116
|
+
return { filePath, action, reason: null, content: dryRun ? outcome.text : null };
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
const explicit = options.hosts !== void 0 && options.hosts.length > 0;
|
|
2121
|
+
const allApplied = results.every((file) => file.action !== "unrecognized");
|
|
2122
|
+
const hostsRecord = explicit && recordHosts ? allApplied ? recordAiHosts(tree, hosts, !dryRun) : "not-applied" : "not-requested";
|
|
2123
|
+
return { dryRun, locale, hosts, teamRules, files: results, hostsRecord };
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// src/generators/add-github-templates/generator.ts
|
|
2127
|
+
import { join } from "node:path";
|
|
2128
|
+
|
|
2129
|
+
// src/generators/shared/template-files.ts
|
|
2130
|
+
import { existsSync } from "node:fs";
|
|
2131
|
+
import { fileURLToPath } from "node:url";
|
|
2132
|
+
import { generateFiles } from "@nx/devkit";
|
|
2133
|
+
import { createTree } from "@nx/devkit/testing";
|
|
2134
|
+
function generatorFilesDir(generator, fromUrl) {
|
|
2135
|
+
for (const relative of [`../${generator}/files`, `./generators/${generator}/files`]) {
|
|
2136
|
+
const dir = fileURLToPath(new URL(relative, fromUrl));
|
|
2137
|
+
if (existsSync(dir)) return dir;
|
|
2138
|
+
}
|
|
2139
|
+
throw new Error(`The "${generator}" generator's template files are missing next to ${fileURLToPath(fromUrl)}`);
|
|
2140
|
+
}
|
|
2141
|
+
function renderTemplateFiles(srcFolder, target, substitutions) {
|
|
2142
|
+
const scratch = createTree();
|
|
2143
|
+
const seeded = new Set(scratch.listChanges().map((change) => change.path));
|
|
2144
|
+
generateFiles(scratch, srcFolder, target, substitutions);
|
|
2145
|
+
return scratch.listChanges().flatMap(
|
|
2146
|
+
(change) => change.type === "CREATE" && change.content !== null && !seeded.has(change.path) ? [{ filePath: change.path, content: change.content.toString("utf-8") }] : []
|
|
2147
|
+
).sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// src/generators/add-github-templates/render.ts
|
|
2151
|
+
var ACTION_LINES = {
|
|
2152
|
+
created: "created",
|
|
2153
|
+
unchanged: "unchanged (already matches the generated version)",
|
|
2154
|
+
skipped: "left as-is (a different file already exists there \u2014 never overwritten)"
|
|
2155
|
+
};
|
|
2156
|
+
function renderAddGithubTemplatesReport(report) {
|
|
2157
|
+
const lines = [];
|
|
2158
|
+
if (report.files.length === 0) {
|
|
2159
|
+
lines.push("add-github-templates: nothing selected (issue/pr/codeowners all false) \u2014 no files to install.");
|
|
2160
|
+
return lines.join("\n");
|
|
2161
|
+
}
|
|
2162
|
+
if (report.dryRun) {
|
|
2163
|
+
lines.push("add-github-templates dry-run \u2014 no files were written.");
|
|
2164
|
+
lines.push("");
|
|
2165
|
+
for (const file of report.files) {
|
|
2166
|
+
lines.push(`${file.filePath}:`);
|
|
2167
|
+
lines.push(file.content);
|
|
2168
|
+
lines.push("");
|
|
2169
|
+
}
|
|
2170
|
+
lines.push("Effective decision:");
|
|
2171
|
+
}
|
|
2172
|
+
for (const file of report.files) {
|
|
2173
|
+
lines.push(` ${file.filePath}: ${ACTION_LINES[file.action]}`);
|
|
2174
|
+
}
|
|
2175
|
+
return lines.join("\n");
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/generators/add-github-templates/generator.ts
|
|
2179
|
+
function composeGithubTemplates(groups, locale) {
|
|
2180
|
+
const files = generatorFilesDir("add-github-templates", import.meta.url);
|
|
2181
|
+
return ["issue", "pr", "codeowners"].filter((group) => groups[group]).flatMap((group) => renderTemplateFiles(join(files, locale, group), ".", {}));
|
|
2182
|
+
}
|
|
2183
|
+
function runAddGithubTemplates(tree, options) {
|
|
2184
|
+
const locale = resolveLocaleOption(options.locale, readToolkitConfig(tree));
|
|
2185
|
+
const plan = composeGithubTemplates(
|
|
2186
|
+
{ issue: options.issue ?? false, pr: options.pr ?? true, codeowners: options.codeowners ?? false },
|
|
2187
|
+
locale
|
|
2188
|
+
);
|
|
2189
|
+
const dryRun = options.dryRun ?? false;
|
|
2190
|
+
const files = plan.map(({ filePath, content }) => {
|
|
2191
|
+
const existing = tree.read(filePath, "utf-8");
|
|
2192
|
+
let action;
|
|
2193
|
+
if (existing === null) {
|
|
2194
|
+
action = "created";
|
|
2195
|
+
if (!dryRun) tree.write(filePath, content);
|
|
2196
|
+
} else if (existing === content) {
|
|
2197
|
+
action = "unchanged";
|
|
2198
|
+
} else {
|
|
2199
|
+
action = "skipped";
|
|
2200
|
+
}
|
|
2201
|
+
return { filePath, action, content };
|
|
2202
|
+
});
|
|
2203
|
+
return { dryRun, files };
|
|
2204
|
+
}
|
|
2205
|
+
|
|
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
|
+
// src/generators/add-review/generator.ts
|
|
2300
|
+
var CONFIG_PATH = "toolkit.config.json";
|
|
2301
|
+
function deriveAltWorkflowPath(mainWorkflowPath) {
|
|
2302
|
+
return mainWorkflowPath.replace(/\.yml$/, ".archweave.yml");
|
|
2303
|
+
}
|
|
2304
|
+
function readConfig(tree) {
|
|
2305
|
+
const raw = tree.read(CONFIG_PATH, "utf-8");
|
|
2306
|
+
if (raw === null) return { raw: null, parsed: null, error: null };
|
|
2307
|
+
try {
|
|
2308
|
+
const parsed = JSON.parse(raw);
|
|
2309
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2310
|
+
return { raw, parsed: null, error: `${CONFIG_PATH} must contain a top-level JSON object` };
|
|
2311
|
+
}
|
|
2312
|
+
return { raw, parsed, error: null };
|
|
2313
|
+
} catch {
|
|
2314
|
+
return { raw, parsed: null, error: `${CONFIG_PATH} is not valid JSON` };
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
function resolveLocale(option, config) {
|
|
2318
|
+
if (option) return option;
|
|
2319
|
+
const fromConfig = config.parsed?.["locale"];
|
|
2320
|
+
return fromConfig === "ko" || fromConfig === "en" ? fromConfig : "en";
|
|
2321
|
+
}
|
|
2322
|
+
function deepEqual(a, b) {
|
|
2323
|
+
if (a === b) return true;
|
|
2324
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
2325
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
2326
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, index) => deepEqual(value, b[index]));
|
|
2327
|
+
}
|
|
2328
|
+
const aKeys = Object.keys(a).sort();
|
|
2329
|
+
const bKeys = Object.keys(b).sort();
|
|
2330
|
+
if (aKeys.length !== bKeys.length || aKeys.some((key, index) => key !== bKeys[index])) return false;
|
|
2331
|
+
return aKeys.every((key) => deepEqual(a[key], b[key]));
|
|
2332
|
+
}
|
|
2333
|
+
var PROVIDERS = ["claude", "gemini", "gpt", "cursor"];
|
|
2334
|
+
var MODES = ["fast", "standard", "thorough"];
|
|
2335
|
+
function recordedReview(config) {
|
|
2336
|
+
const review = config?.["review"];
|
|
2337
|
+
if (typeof review !== "object" || review === null || Array.isArray(review)) return {};
|
|
2338
|
+
const record = review;
|
|
2339
|
+
const pick = (value, allowed) => typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
2340
|
+
const rounds = record["maxRounds"];
|
|
2341
|
+
return {
|
|
2342
|
+
provider: pick(record["provider"], PROVIDERS),
|
|
2343
|
+
mode: pick(record["mode"], MODES),
|
|
2344
|
+
maxRounds: rounds === 1 || rounds === 2 || rounds === 3 || rounds === 4 || rounds === 5 ? rounds : void 0
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
function runAddReview(tree, options) {
|
|
2348
|
+
const config = readConfig(tree);
|
|
2349
|
+
const detectedPm = treePackageManager(tree, config.parsed);
|
|
2350
|
+
const packageManager = options.packageManager ?? detectedPm ?? "pnpm";
|
|
2351
|
+
const packageManagerNote = options.packageManager === void 0 && detectedPm === null ? "the package manager could not be detected (no pnpm, yarn, bun or npm lockfile) \u2014 the workflow was rendered for pnpm; re-run with --packageManager=npm|yarn|bun if that is wrong." : void 0;
|
|
2352
|
+
const recorded = recordedReview(config.parsed);
|
|
2353
|
+
const plan = composeReviewSetup({
|
|
2354
|
+
enabled: true,
|
|
2355
|
+
provider: options.provider ?? recorded.provider ?? "claude",
|
|
2356
|
+
mode: options.mode ?? recorded.mode ?? "standard",
|
|
2357
|
+
locale: resolveLocale(options.locale, config),
|
|
2358
|
+
maxRounds: options.maxRounds ?? recorded.maxRounds ?? 3,
|
|
2359
|
+
packageManager
|
|
2360
|
+
});
|
|
2361
|
+
if (plan === null) throw new Error("unreachable: add-review always composes with enabled: true");
|
|
2362
|
+
if (config.error !== null) {
|
|
2363
|
+
return {
|
|
2364
|
+
dryRun: options.dryRun ?? false,
|
|
2365
|
+
workflowPath: plan.workflowFilePath,
|
|
2366
|
+
workflowAction: "aborted",
|
|
2367
|
+
configAction: "aborted",
|
|
2368
|
+
requiredSecretName: plan.requiredSecretName,
|
|
2369
|
+
diff: null,
|
|
2370
|
+
workflowYaml: null,
|
|
2371
|
+
configSnippetJson: null,
|
|
2372
|
+
configError: config.error
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
const existing = tree.read(plan.workflowFilePath, "utf-8");
|
|
2376
|
+
const verdict = judgeReviewFileConflict(existing, plan.workflowYaml);
|
|
2377
|
+
let workflowAction;
|
|
2378
|
+
let workflowPath = plan.workflowFilePath;
|
|
2379
|
+
let writeWorkflowTo = null;
|
|
2380
|
+
let diff = null;
|
|
2381
|
+
let mainFileMatchesOptions;
|
|
2382
|
+
if (verdict.kind === "create") {
|
|
2383
|
+
workflowAction = "created";
|
|
2384
|
+
writeWorkflowTo = plan.workflowFilePath;
|
|
2385
|
+
mainFileMatchesOptions = true;
|
|
2386
|
+
} else if (verdict.kind === "unchanged") {
|
|
2387
|
+
workflowAction = "unchanged";
|
|
2388
|
+
mainFileMatchesOptions = true;
|
|
2389
|
+
} else {
|
|
2390
|
+
switch (options.onConflict) {
|
|
2391
|
+
case "overwrite":
|
|
2392
|
+
workflowAction = "overwritten";
|
|
2393
|
+
writeWorkflowTo = plan.workflowFilePath;
|
|
2394
|
+
mainFileMatchesOptions = true;
|
|
2395
|
+
break;
|
|
2396
|
+
case "skip":
|
|
2397
|
+
workflowAction = "skipped";
|
|
2398
|
+
mainFileMatchesOptions = false;
|
|
2399
|
+
break;
|
|
2400
|
+
case "alternate-filename": {
|
|
2401
|
+
const altWorkflowPath = deriveAltWorkflowPath(plan.workflowFilePath);
|
|
2402
|
+
workflowAction = "alternate";
|
|
2403
|
+
workflowPath = altWorkflowPath;
|
|
2404
|
+
writeWorkflowTo = altWorkflowPath;
|
|
2405
|
+
mainFileMatchesOptions = false;
|
|
2406
|
+
break;
|
|
2407
|
+
}
|
|
2408
|
+
default:
|
|
2409
|
+
workflowAction = "proposed";
|
|
2410
|
+
diff = renderLineDiff(verdict.existing, verdict.generated);
|
|
2411
|
+
mainFileMatchesOptions = false;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
let configAction;
|
|
2415
|
+
let newConfigContent = null;
|
|
2416
|
+
if (!mainFileMatchesOptions) {
|
|
2417
|
+
configAction = "not-updated";
|
|
2418
|
+
} else {
|
|
2419
|
+
const existingReview = config.parsed?.["review"];
|
|
2420
|
+
if (deepEqual(existingReview, plan.configSnippet)) {
|
|
2421
|
+
configAction = "unchanged";
|
|
2422
|
+
} else {
|
|
2423
|
+
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;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
const dryRun = options.dryRun ?? false;
|
|
2432
|
+
if (!dryRun) {
|
|
2433
|
+
if (writeWorkflowTo !== null) tree.write(writeWorkflowTo, plan.workflowYaml);
|
|
2434
|
+
if (newConfigContent !== null) tree.write(CONFIG_PATH, newConfigContent);
|
|
2435
|
+
}
|
|
2436
|
+
return {
|
|
2437
|
+
dryRun,
|
|
2438
|
+
workflowPath,
|
|
2439
|
+
workflowAction,
|
|
2440
|
+
configAction,
|
|
2441
|
+
requiredSecretName: plan.requiredSecretName,
|
|
2442
|
+
diff,
|
|
2443
|
+
workflowYaml: dryRun ? plan.workflowYaml : null,
|
|
2444
|
+
configSnippetJson: dryRun ? JSON.stringify(plan.configSnippet, null, 2) : null,
|
|
2445
|
+
configError: null,
|
|
2446
|
+
...packageManagerNote !== void 0 ? { packageManagerNote } : {}
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/generators/add-backend/generator.ts
|
|
2451
|
+
import { getProjects as getProjects2 } from "@nx/devkit";
|
|
2452
|
+
|
|
2453
|
+
// src/generators/add-backend/render.ts
|
|
2454
|
+
function renderAddBackendBody(report) {
|
|
2455
|
+
const lines = [];
|
|
2456
|
+
if (report.refused === "no-frontend") {
|
|
2457
|
+
lines.push(`Refused: no frontend application and no "${report.apiName}" app found in this workspace.`);
|
|
2458
|
+
lines.push(" add-backend wires a backend app \u2014 beside a frontend, or on its own in a backend-only");
|
|
2459
|
+
lines.push(` workspace (create one with: npx create-nx-workspace --preset=${report.backend} --appName=${report.apiName}).`);
|
|
2460
|
+
lines.push(" If the backend app has another name, pass --apiName=<its directory name>.");
|
|
2461
|
+
return lines.join("\n");
|
|
2462
|
+
}
|
|
2463
|
+
if (report.refused === "frontend-not-found") {
|
|
2464
|
+
lines.push(`Refused: --frontendProject "${report.frontendProject}" does not match any project.`);
|
|
2465
|
+
lines.push(" Nothing was written. Check the name with `nx show projects`.");
|
|
2466
|
+
return lines.join("\n");
|
|
2467
|
+
}
|
|
2468
|
+
if (report.refused === "ambiguous-frontend") {
|
|
2469
|
+
lines.push("Refused: multiple frontend applications found \u2014 add-backend will not guess:");
|
|
2470
|
+
for (const name of report.frontendCandidates ?? []) lines.push(` - ${name}`);
|
|
2471
|
+
lines.push(" Nothing was written. Re-run with --frontendProject=<name> to pick one.");
|
|
2472
|
+
return lines.join("\n");
|
|
2473
|
+
}
|
|
2474
|
+
if (report.refused === "invalid-project-json") {
|
|
2475
|
+
lines.push("Refused: a project.json this run must update could not be parsed:");
|
|
2476
|
+
for (const path of report.invalidProjects ?? []) lines.push(` - ${path}`);
|
|
2477
|
+
lines.push(" Nothing was written \u2014 fix the JSON, then re-run add-backend.");
|
|
2478
|
+
return lines.join("\n");
|
|
2479
|
+
}
|
|
2480
|
+
if (report.refused === "missing-api-project") {
|
|
2481
|
+
lines.push(`Refused: no "${report.apiName}" application project found.`);
|
|
2482
|
+
lines.push(" Create it with the official commands first (copy-paste, names already filled in),");
|
|
2483
|
+
lines.push(" then re-run add-backend to wire it up:");
|
|
2484
|
+
lines.push("");
|
|
2485
|
+
for (const command of report.commands ?? []) lines.push(` ${command}`);
|
|
2486
|
+
if (report.expressRange !== void 0) {
|
|
2487
|
+
lines.push("");
|
|
2488
|
+
lines.push(` The express line declares express@${report.expressRange}, the range this workspace's @nx/react supports \u2014`);
|
|
2489
|
+
lines.push(" the official generator then keeps it instead of adding express 5, which npm rejects as a peer conflict.");
|
|
2490
|
+
}
|
|
2491
|
+
return lines.join("\n");
|
|
2492
|
+
}
|
|
2493
|
+
if (report.packageManagerNote !== void 0) lines.push(` note: ${report.packageManagerNote}`);
|
|
2494
|
+
lines.push(` frontend project: ${report.frontendProject ?? "none (backend-only workspace)"}`);
|
|
2495
|
+
lines.push(
|
|
2496
|
+
report.sharedTypes === "created" ? ` libs/shared-types: created (import it as ${report.sharedTypesPackage})` : " libs/shared-types: already exists \u2014 skipped"
|
|
2497
|
+
);
|
|
2498
|
+
if (report.config !== void 0 && report.config !== "unchanged") {
|
|
2499
|
+
lines.push(` toolkit.config.json: backend "${report.backend}" recorded (${report.config}) \u2014 doctor's backend checks now run`);
|
|
2500
|
+
}
|
|
2501
|
+
if (report.workspaceGlob !== void 0) {
|
|
2502
|
+
const pmName = report.packageManager ?? "pnpm";
|
|
2503
|
+
const registry = packageManagerCommands(pmName).workspaceRegistration;
|
|
2504
|
+
lines.push(
|
|
2505
|
+
{
|
|
2506
|
+
added: ` ${registry}: 'libs/shared-types' registered \u2014 run \`${pmName} install\` once to link it`,
|
|
2507
|
+
present: ` ${registry}: already covers libs/shared-types`,
|
|
2508
|
+
unrecognized: (
|
|
2509
|
+
// 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`
|
|
2511
|
+
)
|
|
2512
|
+
}[report.workspaceGlob]
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
for (const tag of report.tags) {
|
|
2516
|
+
lines.push(` tag ${tag.project}: ${tag.action === "added" ? "added" : "already present"}`);
|
|
2517
|
+
}
|
|
2518
|
+
lines.push(
|
|
2519
|
+
{
|
|
2520
|
+
added: ` tsconfig.base.json paths: ${report.sharedTypesPackage} added (a classic or integrated setup \u2014 libs resolve through paths)`,
|
|
2521
|
+
present: " tsconfig.base.json paths: already wired",
|
|
2522
|
+
"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"
|
|
2524
|
+
}[report.tsconfigPaths]
|
|
2525
|
+
);
|
|
2526
|
+
switch (report.eslint) {
|
|
2527
|
+
case "applied":
|
|
2528
|
+
lines.push(" ESLint boundaries: cross-side deny rules written (existing imports stay allowed)");
|
|
2529
|
+
break;
|
|
2530
|
+
case "already-applied":
|
|
2531
|
+
lines.push(" ESLint boundaries: already enforced \u2014 no change");
|
|
2532
|
+
break;
|
|
2533
|
+
case "degraded-unrecognized":
|
|
2534
|
+
lines.push(" ESLint boundaries: your config was NOT recognized \u2014 left untouched.");
|
|
2535
|
+
lines.push(" Merge this yourself (then boundaries are actually enforced):");
|
|
2536
|
+
break;
|
|
2537
|
+
case "snippet-only":
|
|
2538
|
+
lines.push(" ESLint boundaries: report-only (pass --applyEslint to write them).");
|
|
2539
|
+
lines.push(" Paste-ready snippet:");
|
|
2540
|
+
break;
|
|
2541
|
+
}
|
|
2542
|
+
if (report.snippet) {
|
|
2543
|
+
lines.push("");
|
|
2544
|
+
lines.push(report.snippet);
|
|
2545
|
+
}
|
|
2546
|
+
return lines.join("\n");
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// src/generators/add-backend/generator.ts
|
|
2550
|
+
var SHARED_TYPES_ROOT = "libs/shared-types";
|
|
2551
|
+
function runAddBackend(tree, options) {
|
|
2552
|
+
const backend = options.backend;
|
|
2553
|
+
const apiName = options.apiName ?? "api";
|
|
2554
|
+
const applyEslint = options.applyEslint ?? false;
|
|
2555
|
+
const dryRun = options.dryRun ?? false;
|
|
2556
|
+
const write = !dryRun;
|
|
2557
|
+
const base = { backend, apiName };
|
|
2558
|
+
const refusalShape = {
|
|
2559
|
+
sharedTypes: "skipped",
|
|
2560
|
+
tags: [],
|
|
2561
|
+
tsconfigPaths: "unrecognized",
|
|
2562
|
+
eslint: "snippet-only"
|
|
2563
|
+
};
|
|
2564
|
+
let projects;
|
|
2565
|
+
try {
|
|
2566
|
+
projects = getProjects2(tree);
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2569
|
+
const pathMatch = /Cannot parse ([^:]+):/.exec(message);
|
|
2570
|
+
return {
|
|
2571
|
+
...base,
|
|
2572
|
+
refused: "invalid-project-json",
|
|
2573
|
+
invalidProjects: [pathMatch ? pathMatch[1] : message],
|
|
2574
|
+
...refusalShape
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
const detectedPm = treePackageManager(tree, readToolkitConfig(tree));
|
|
2578
|
+
const packageManager = detectedPm ?? "pnpm";
|
|
2579
|
+
const pmCommands = packageManagerCommands(
|
|
2580
|
+
packageManager,
|
|
2581
|
+
packageManager === "yarn" ? detectYarnFlavor((p) => tree.exists(p), (p) => tree.read(p, "utf-8")) : void 0
|
|
2582
|
+
);
|
|
2583
|
+
const packageManagerNote = detectedPm === null ? "the package manager could not be detected (no pnpm, yarn, bun or npm markers) \u2014 commands below assume pnpm." : void 0;
|
|
2584
|
+
const baseNameOf2 = (root) => root.split("/").pop() ?? root;
|
|
2585
|
+
const entries = [...projects.entries()];
|
|
2586
|
+
const declaredApp = ([, config]) => config.projectType === "application";
|
|
2587
|
+
const inferredApp = ([, config]) => config.projectType === void 0 && config.root !== "" && config.root !== "." && resolveProjectType(tree, config.root, void 0) === "application";
|
|
2588
|
+
const apiNamed = entries.filter(([, config]) => baseNameOf2(config.root) === apiName);
|
|
2589
|
+
const apiEntry = apiNamed.find(declaredApp) ?? apiNamed.find((entry) => inferredApp(entry) && tree.exists(`${entry[1].root}/tsconfig.app.json`));
|
|
2590
|
+
let frontendProject;
|
|
2591
|
+
if (options.frontendProject !== void 0) {
|
|
2592
|
+
if (!projects.has(options.frontendProject)) {
|
|
2593
|
+
return { ...base, refused: "frontend-not-found", frontendProject: options.frontendProject, ...refusalShape };
|
|
2594
|
+
}
|
|
2595
|
+
frontendProject = options.frontendProject;
|
|
2596
|
+
} else {
|
|
2597
|
+
const eligible = entries.filter(([, config]) => {
|
|
2598
|
+
const dir = baseNameOf2(config.root);
|
|
2599
|
+
return dir !== apiName && dir !== "e2e" && !dir.endsWith("-e2e");
|
|
2600
|
+
});
|
|
2601
|
+
const declared = eligible.filter(declaredApp);
|
|
2602
|
+
const candidates = (declared.length > 0 ? declared : eligible.filter(inferredApp)).map(([name]) => name).sort();
|
|
2603
|
+
if (candidates.length === 0 && !apiEntry) {
|
|
2604
|
+
return { ...base, refused: "no-frontend", ...refusalShape };
|
|
2605
|
+
}
|
|
2606
|
+
if (candidates.length > 1) {
|
|
2607
|
+
return { ...base, refused: "ambiguous-frontend", frontendCandidates: candidates, ...refusalShape };
|
|
2608
|
+
}
|
|
2609
|
+
frontendProject = candidates[0];
|
|
2610
|
+
}
|
|
2611
|
+
if (!apiEntry) {
|
|
2612
|
+
if (frontendProject === void 0) return { ...base, refused: "no-frontend", ...refusalShape };
|
|
2613
|
+
let nxPin = "";
|
|
2614
|
+
let expressDeclared = false;
|
|
2615
|
+
const rootPkgRaw = tree.read("package.json", "utf-8");
|
|
2616
|
+
if (rootPkgRaw !== null) {
|
|
2617
|
+
try {
|
|
2618
|
+
const pkg = JSON.parse(rootPkgRaw);
|
|
2619
|
+
const nxVersion = pkg.devDependencies?.["nx"] ?? pkg.dependencies?.["nx"];
|
|
2620
|
+
if (nxVersion && /^[~^]?\d/.test(nxVersion)) nxPin = `@${nxVersion}`;
|
|
2621
|
+
expressDeclared = pkg.dependencies?.["express"] !== void 0 || pkg.devDependencies?.["express"] !== void 0;
|
|
2622
|
+
} catch {
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
const frontendParent = (projects.get(frontendProject)?.root ?? "").split("/").slice(0, -1).join("/");
|
|
2626
|
+
const apiDirectory = `${frontendParent === "" || frontendParent === "." ? "apps" : frontendParent}/${apiName}`;
|
|
2627
|
+
const expressAlignment = backend === "express" && !expressDeclared ? readExpressPeerRange(tree) : void 0;
|
|
2628
|
+
const quotedExpress = expressAlignment ? `"express@${expressAlignment}"` : void 0;
|
|
2629
|
+
return {
|
|
2630
|
+
...base,
|
|
2631
|
+
refused: "missing-api-project",
|
|
2632
|
+
frontendProject,
|
|
2633
|
+
...expressAlignment !== void 0 ? { expressRange: expressAlignment } : {},
|
|
2634
|
+
commands: [
|
|
2635
|
+
pmCommands.addDevAtRoot(`@nx/${backend}${nxPin}`),
|
|
2636
|
+
...quotedExpress ? [pmCommands.addAtRoot(quotedExpress)] : [],
|
|
2637
|
+
`${pmCommands.runNx} g @nx/${backend}:application ${apiDirectory} --frontendProject=${frontendProject}`
|
|
2638
|
+
],
|
|
2639
|
+
...refusalShape
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
const sharedTypesExists = tree.exists(`${SHARED_TYPES_ROOT}/package.json`) || tree.exists(`${SHARED_TYPES_ROOT}/project.json`);
|
|
2643
|
+
const rootPackageJson = tree.read("package.json", "utf-8");
|
|
2644
|
+
const workspaceName = rootPackageJson ? JSON.parse(rootPackageJson).name ?? "workspace" : "workspace";
|
|
2645
|
+
const wiring = composeBackendWiring({ workspaceName });
|
|
2646
|
+
const [apiProjectName, apiConfig] = apiEntry;
|
|
2647
|
+
const tagTargets = [
|
|
2648
|
+
...frontendProject === void 0 ? [] : [{ project: frontendProject, root: projects.get(frontendProject)?.root ?? `apps/${frontendProject}` }],
|
|
2649
|
+
{ project: apiProjectName, root: apiConfig.root }
|
|
2650
|
+
];
|
|
2651
|
+
if (sharedTypesExists) tagTargets.push({ project: wiring.sharedTypesPackageName, root: SHARED_TYPES_ROOT });
|
|
2652
|
+
const parsedTargets = /* @__PURE__ */ new Map();
|
|
2653
|
+
const invalidProjects = [];
|
|
2654
|
+
for (const target of tagTargets) {
|
|
2655
|
+
const projectJsonPath = `${target.root}/project.json`;
|
|
2656
|
+
const packageJsonPath = `${target.root}/package.json`;
|
|
2657
|
+
const kind = tree.exists(projectJsonPath) ? "project" : "package";
|
|
2658
|
+
const path = kind === "project" ? projectJsonPath : packageJsonPath;
|
|
2659
|
+
const raw = tree.read(path, "utf-8");
|
|
2660
|
+
if (raw === null) {
|
|
2661
|
+
invalidProjects.push(path);
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
try {
|
|
2665
|
+
parsedTargets.set(target.project, { path, raw, kind, parsed: JSON.parse(raw) });
|
|
2666
|
+
} catch {
|
|
2667
|
+
invalidProjects.push(path);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
if (invalidProjects.length > 0) {
|
|
2671
|
+
return { ...base, refused: "invalid-project-json", frontendProject, invalidProjects, ...refusalShape };
|
|
2672
|
+
}
|
|
2673
|
+
const sharedTypes = sharedTypesExists ? "skipped" : "created";
|
|
2674
|
+
if (sharedTypes === "created" && write) {
|
|
2675
|
+
for (const file of wiring.sharedTypesFiles) tree.write(file.filePath, file.content);
|
|
2676
|
+
}
|
|
2677
|
+
let configOutcome = "unchanged";
|
|
2678
|
+
{
|
|
2679
|
+
const { raw: configRaw, parsed: configParsed } = readToolkitConfigFile(tree);
|
|
2680
|
+
const parseOk = configRaw === null || configParsed !== null;
|
|
2681
|
+
const existing = configParsed ?? {};
|
|
2682
|
+
if (parseOk && existing["backend"] !== backend) {
|
|
2683
|
+
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
|
+
`);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
let workspaceGlob = "unrecognized";
|
|
2690
|
+
const workspaceYaml = tree.read("pnpm-workspace.yaml", "utf-8");
|
|
2691
|
+
if (workspaceYaml !== null) {
|
|
2692
|
+
const globResult = addSharedTypesWorkspaceGlob(workspaceYaml);
|
|
2693
|
+
workspaceGlob = globResult.status;
|
|
2694
|
+
if (globResult.status === "added" && write) tree.write("pnpm-workspace.yaml", globResult.text);
|
|
2695
|
+
} else if ((detectedPm === "npm" || detectedPm === "yarn" || detectedPm === "bun") && rootPackageJson !== null) {
|
|
2696
|
+
const yarnNeedsPrivate = detectedPm === "yarn" && JSON.parse(rootPackageJson).private !== true;
|
|
2697
|
+
const npmResult = yarnNeedsPrivate ? { status: "unrecognized" } : addSharedTypesNpmWorkspace(rootPackageJson);
|
|
2698
|
+
workspaceGlob = npmResult.status;
|
|
2699
|
+
if (npmResult.status === "added" && write) tree.write("package.json", npmResult.text);
|
|
2700
|
+
}
|
|
2701
|
+
const mergeTag = (project, tag) => {
|
|
2702
|
+
const target = parsedTargets.get(project);
|
|
2703
|
+
if (!target) return { project, action: "added" };
|
|
2704
|
+
const holder = target.kind === "project" ? target.parsed : target.parsed.nx ??= {};
|
|
2705
|
+
const tags2 = holder.tags ?? [];
|
|
2706
|
+
if (tags2.includes(tag)) return { project, action: "present" };
|
|
2707
|
+
holder.tags = [...tags2, tag];
|
|
2708
|
+
if (write) tree.write(target.path, `${JSON.stringify(target.parsed, null, detectIndent(target.raw))}
|
|
2709
|
+
`);
|
|
2710
|
+
return { project, action: "added" };
|
|
2711
|
+
};
|
|
2712
|
+
const tags = [
|
|
2713
|
+
...frontendProject === void 0 ? [] : [mergeTag(frontendProject, wiring.tagByRole.frontend)],
|
|
2714
|
+
mergeTag(apiProjectName, wiring.tagByRole.backend),
|
|
2715
|
+
mergeTag(wiring.sharedTypesPackageName, wiring.tagByRole.shared)
|
|
2716
|
+
];
|
|
2717
|
+
let tsconfigPaths = "workspace-link";
|
|
2718
|
+
const tsconfigRaw = tree.read("tsconfig.base.json", "utf-8");
|
|
2719
|
+
if (tsconfigRaw !== null) {
|
|
2720
|
+
try {
|
|
2721
|
+
const tsconfig = JSON.parse(tsconfigRaw);
|
|
2722
|
+
const baseUrl = tsconfig.compilerOptions?.baseUrl;
|
|
2723
|
+
if (tsconfig.compilerOptions && (baseUrl || tsconfig.compilerOptions.paths !== void 0)) {
|
|
2724
|
+
const paths = tsconfig.compilerOptions.paths ?? {};
|
|
2725
|
+
if (paths[wiring.sharedTypesPackageName]) {
|
|
2726
|
+
tsconfigPaths = "present";
|
|
2727
|
+
} else {
|
|
2728
|
+
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
|
+
`);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
} catch {
|
|
2736
|
+
tsconfigPaths = "unrecognized";
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
let eslint;
|
|
2740
|
+
let snippet;
|
|
2741
|
+
if (!applyEslint) {
|
|
2742
|
+
eslint = "snippet-only";
|
|
2743
|
+
snippet = buildBoundarySnippet();
|
|
2744
|
+
} else {
|
|
2745
|
+
const configPath = ESLINT_CONFIG_CANDIDATES.find((candidate) => tree.exists(candidate));
|
|
2746
|
+
const configText = configPath ? tree.read(configPath, "utf-8") : null;
|
|
2747
|
+
const result = configText !== null ? applyBoundaryConstraints(configText) : { status: "unrecognized" };
|
|
2748
|
+
if (result.status === "applied") {
|
|
2749
|
+
eslint = "applied";
|
|
2750
|
+
if (write && configPath) tree.write(configPath, result.text);
|
|
2751
|
+
} else if (result.status === "already-applied") {
|
|
2752
|
+
eslint = "already-applied";
|
|
2753
|
+
} else {
|
|
2754
|
+
eslint = "degraded-unrecognized";
|
|
2755
|
+
snippet = buildBoundarySnippet();
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
return {
|
|
2759
|
+
...base,
|
|
2760
|
+
frontendProject,
|
|
2761
|
+
sharedTypes,
|
|
2762
|
+
tags,
|
|
2763
|
+
workspaceGlob,
|
|
2764
|
+
tsconfigPaths,
|
|
2765
|
+
eslint,
|
|
2766
|
+
snippet,
|
|
2767
|
+
sharedTypesPackage: wiring.sharedTypesPackageName,
|
|
2768
|
+
config: configOutcome,
|
|
2769
|
+
packageManager,
|
|
2770
|
+
...packageManagerNote !== void 0 ? { packageManagerNote } : {}
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
function readExpressPeerRange(tree) {
|
|
2774
|
+
const raw = tree.read("node_modules/@nx/react/package.json", "utf-8");
|
|
2775
|
+
if (raw === null) return void 0;
|
|
2776
|
+
try {
|
|
2777
|
+
const range = JSON.parse(raw).peerDependencies?.["express"];
|
|
2778
|
+
return typeof range === "string" && range.trim().length > 0 ? range.trim() : void 0;
|
|
2779
|
+
} catch {
|
|
2780
|
+
return void 0;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// src/generators/init/render.ts
|
|
2785
|
+
var SECTION_TITLES = {
|
|
2786
|
+
fsd: "add-fsd-lite",
|
|
2787
|
+
agentRules: "add-agent-rules",
|
|
2788
|
+
githubTemplates: "add-github-templates",
|
|
2789
|
+
review: "add-review",
|
|
2790
|
+
backend: "add-backend"
|
|
2791
|
+
};
|
|
2792
|
+
function renderSectionReport(report) {
|
|
2793
|
+
switch (report.kind) {
|
|
2794
|
+
case "fsd":
|
|
2795
|
+
return renderAddFsdLiteReport(report.report);
|
|
2796
|
+
case "agentRules":
|
|
2797
|
+
return renderAddAgentRulesReport(report.report);
|
|
2798
|
+
case "githubTemplates":
|
|
2799
|
+
return renderAddGithubTemplatesReport(report.report);
|
|
2800
|
+
case "review":
|
|
2801
|
+
return renderAddReviewReport(report.report);
|
|
2802
|
+
case "backend":
|
|
2803
|
+
return renderAddBackendBody(report.report);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
function sectionAborted(section) {
|
|
2807
|
+
if (section.thrownError !== void 0) return true;
|
|
2808
|
+
if (section.report === null) return false;
|
|
2809
|
+
if (section.report.kind === "fsd") return section.report.report.aborted;
|
|
2810
|
+
if (section.report.kind === "review") return section.report.report.workflowAction === "aborted";
|
|
2811
|
+
return false;
|
|
2812
|
+
}
|
|
2813
|
+
function renderInitReport(report) {
|
|
2814
|
+
const lines = [];
|
|
2815
|
+
const backendSuffix = report.backend === "none" ? "" : `, backend: ${report.backend}`;
|
|
2816
|
+
const header = `init (preset: ${report.preset}, frontend: ${report.frontend}${backendSuffix})`;
|
|
2817
|
+
lines.push(report.dryRun ? `${header} \u2014 dry run` : header);
|
|
2818
|
+
if (report.refused) {
|
|
2819
|
+
lines.push("");
|
|
2820
|
+
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.");
|
|
2823
|
+
return lines.join("\n");
|
|
2824
|
+
}
|
|
2825
|
+
for (const section of report.sections) {
|
|
2826
|
+
if (section.id === "backend" && !section.enabled) continue;
|
|
2827
|
+
lines.push("");
|
|
2828
|
+
lines.push(`## ${SECTION_TITLES[section.id]}`);
|
|
2829
|
+
if (!section.enabled) {
|
|
2830
|
+
lines.push(
|
|
2831
|
+
section.id === "fsd" && report.frontend === "none" ? " skipped (backend-only workspace \u2014 no frontend to structure)" : " skipped (disabled for this run)"
|
|
2832
|
+
);
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
if (section.thrownError !== void 0) {
|
|
2836
|
+
lines.push(` aborted \u2014 unexpected error: ${section.thrownError}`);
|
|
2837
|
+
lines.push(" Nothing was written for this section. Rerun add-fsd-lite directly once resolved.");
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
if (section.report !== null) {
|
|
2841
|
+
lines.push(renderSectionReport(section.report));
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
if (report.config !== void 0 && (report.config.action !== "skipped" || report.config.notRecorded.length > 0)) {
|
|
2845
|
+
lines.push("");
|
|
2846
|
+
lines.push("## toolkit.config.json");
|
|
2847
|
+
const fieldList = report.config.fields.join(", ");
|
|
2848
|
+
switch (report.config.action) {
|
|
2849
|
+
case "created":
|
|
2850
|
+
case "updated":
|
|
2851
|
+
lines.push(` ${report.dryRun ? "would record" : "recorded"} ${fieldList} (${report.config.action})`);
|
|
2852
|
+
break;
|
|
2853
|
+
case "unchanged":
|
|
2854
|
+
lines.push(` already records ${fieldList} (unchanged)`);
|
|
2855
|
+
break;
|
|
2856
|
+
case "unrecognized":
|
|
2857
|
+
lines.push(` not updated \u2014 the file is not a JSON object, so init left it alone (it never rewrites a shape it`);
|
|
2858
|
+
lines.push(` does not recognize). Add ${fieldList} by hand so standalone reruns resolve the same values.`);
|
|
2859
|
+
break;
|
|
2860
|
+
case "skipped":
|
|
2861
|
+
break;
|
|
2862
|
+
}
|
|
2863
|
+
if (report.config.notRecorded.length > 0) {
|
|
2864
|
+
lines.push(` not recorded: ${report.config.notRecorded.map((n) => n.field).join(", ")} \u2014 that section did not apply cleanly`);
|
|
2865
|
+
lines.push(" this run (aborted, or written as an alternate file). Once resolved, record it with:");
|
|
2866
|
+
const runNx = packageManagerCommands(report.packageManager ?? "pnpm").runNx;
|
|
2867
|
+
for (const entry of report.config.notRecorded) lines.push(` ${runNx} g @clearlydev/nx-archweave:${entry.rerun}`);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
const ranDirty = report.allowDirty === true && !report.cleanTree && !report.dryRun;
|
|
2871
|
+
if (ranDirty) {
|
|
2872
|
+
lines.push("");
|
|
2873
|
+
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.");
|
|
2875
|
+
}
|
|
2876
|
+
const hadAbort = report.sections.some(sectionAborted);
|
|
2877
|
+
if (!report.dryRun && hadAbort) {
|
|
2878
|
+
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 .");
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
return lines.join("\n");
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// src/generators/init/validation.ts
|
|
2895
|
+
import { spawnSync } from "node:child_process";
|
|
2896
|
+
function createProcessExec() {
|
|
2897
|
+
return (command, args, options) => {
|
|
2898
|
+
const spawnable = spawnableCommand(command, args);
|
|
2899
|
+
const result = spawnSync(spawnable.command, spawnable.args, {
|
|
2900
|
+
cwd: options?.cwd,
|
|
2901
|
+
encoding: "utf-8",
|
|
2902
|
+
windowsVerbatimArguments: spawnable.windowsVerbatimArguments
|
|
2903
|
+
});
|
|
2904
|
+
return { status: result.status ?? null, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
function safeExec(exec, command, args, options) {
|
|
2908
|
+
try {
|
|
2909
|
+
return exec(command, args, options);
|
|
2910
|
+
} catch (error) {
|
|
2911
|
+
return { status: null, stdout: "", stderr: error instanceof Error ? error.message : String(error) };
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
function runPostInitValidation(input, exec) {
|
|
2915
|
+
console.log("");
|
|
2916
|
+
console.log("Post-run validation (best-effort):");
|
|
2917
|
+
if (input.workflowPaths.length === 0) {
|
|
2918
|
+
console.log(" actionlint: skipped (no GitHub Actions workflow files were written this run)");
|
|
2919
|
+
} else {
|
|
2920
|
+
const versionCheck = safeExec(exec, "actionlint", ["--version"], { cwd: input.root });
|
|
2921
|
+
if (versionCheck.status !== 0) {
|
|
2922
|
+
console.log(" actionlint: not found on PATH \u2014 skipped.");
|
|
2923
|
+
console.log(" Install it from https://github.com/rhysd/actionlint to validate workflow files locally.");
|
|
2924
|
+
} else {
|
|
2925
|
+
const lintResult = safeExec(exec, "actionlint", input.workflowPaths, { cwd: input.root });
|
|
2926
|
+
if (lintResult.status === 0) {
|
|
2927
|
+
console.log(` actionlint: passed (${input.workflowPaths.join(", ")})`);
|
|
2928
|
+
} else {
|
|
2929
|
+
console.log(" actionlint: reported issues \u2014");
|
|
2930
|
+
for (const line of (lintResult.stdout || lintResult.stderr).split("\n")) {
|
|
2931
|
+
if (line.length > 0) console.log(` ${line}`);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
const nxVersionCheck = safeExec(exec, "nx", ["--version"], { cwd: input.root });
|
|
2937
|
+
if (nxVersionCheck.status !== 0) {
|
|
2938
|
+
console.log(" nx graph: nx not found on PATH \u2014 skipped.");
|
|
2939
|
+
console.log(" Run it via your package manager instead, e.g. `pnpm nx graph`, `yarn nx graph`, `bun nx graph` or `npm exec nx graph`.");
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
const graphResult = safeExec(exec, "nx", ["graph", `--file=${input.graphOutputFile}`], { cwd: input.root });
|
|
2943
|
+
if (graphResult.status === 0) {
|
|
2944
|
+
console.log(` nx graph: valid (written to ${input.graphOutputFile})`);
|
|
2945
|
+
} else {
|
|
2946
|
+
console.log(" nx graph: failed \u2014");
|
|
2947
|
+
for (const line of (graphResult.stdout || graphResult.stderr).split("\n")) {
|
|
2948
|
+
if (line.length > 0) console.log(` ${line}`);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
// src/generators/init/generator.ts
|
|
2954
|
+
function recordPresetChoices(tree, choices, notRecorded, write) {
|
|
2955
|
+
const fields = [];
|
|
2956
|
+
const topLevel = ["locale", "preset", "frontend", "packageManager"].filter((key) => choices[key] !== void 0);
|
|
2957
|
+
fields.push(...topLevel);
|
|
2958
|
+
if (choices.enforcement !== void 0) fields.push("fsd.enforcement");
|
|
2959
|
+
if (choices.teamRules !== void 0) fields.push("agentRules.teamRules");
|
|
2960
|
+
if (choices.aiHosts !== void 0) fields.push("aiHosts");
|
|
2961
|
+
if (fields.length === 0) return { action: "skipped", fields, notRecorded };
|
|
2962
|
+
const { raw, parsed } = readToolkitConfigFile(tree);
|
|
2963
|
+
if (raw !== null && parsed === null) return { action: "unrecognized", fields, notRecorded };
|
|
2964
|
+
const existing = parsed ?? {};
|
|
2965
|
+
const asObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
2966
|
+
const next = { ...existing };
|
|
2967
|
+
let changed = false;
|
|
2968
|
+
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
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
if (choices.aiHosts !== void 0) {
|
|
2989
|
+
const recorded = existing["aiHosts"];
|
|
2990
|
+
if (!Array.isArray(recorded) || recorded.join("\0") !== choices.aiHosts.join("\0")) {
|
|
2991
|
+
next["aiHosts"] = [...choices.aiHosts];
|
|
2992
|
+
changed = true;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
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);
|
|
3002
|
+
}
|
|
3003
|
+
return { action: raw === null ? "created" : "updated", fields, notRecorded };
|
|
3004
|
+
}
|
|
3005
|
+
function keptReviewProvider(option, config) {
|
|
3006
|
+
const recordedHosts = resolveAiHostsOption(void 0, config);
|
|
3007
|
+
const hosts = resolveAiHostsOption(option, config);
|
|
3008
|
+
return hosts[0] === recordedHosts[0] ? recordedReview(config).provider : void 0;
|
|
3009
|
+
}
|
|
3010
|
+
function runInit(tree, options, deps) {
|
|
3011
|
+
const recorded = readToolkitConfigFile(tree).parsed;
|
|
3012
|
+
const preset = options.preset ?? recordedChoice(recorded, "preset", RECORDED_PRESETS);
|
|
3013
|
+
if (preset === void 0) {
|
|
3014
|
+
throw new Error("init: pass --preset=quick-start|solo|team (toolkit.config.json records none).");
|
|
3015
|
+
}
|
|
3016
|
+
const frontend = resolveInitFrontend(options.frontend, recorded);
|
|
3017
|
+
const backend = options.backend ?? recordedChoice(recorded, "backend", RECORDED_BACKENDS) ?? "none";
|
|
3018
|
+
if (frontend === "none" && backend === "none") {
|
|
3019
|
+
throw new Error("init: --frontend=none is a backend-only workspace \u2014 pass --backend=express|nest.");
|
|
3020
|
+
}
|
|
3021
|
+
const dryRun = options.dryRun ?? false;
|
|
3022
|
+
const locale = resolveLocaleOption(options.locale, recorded);
|
|
3023
|
+
const allowDirty = options.allowDirty ?? false;
|
|
3024
|
+
const cleanTree = dryRun ? true : deps.isWorkingTreeClean(tree.root);
|
|
3025
|
+
if (!cleanTree && !allowDirty) {
|
|
3026
|
+
return { preset, frontend, backend, dryRun, cleanTree: false, allowDirty, refused: true, sections: [] };
|
|
3027
|
+
}
|
|
3028
|
+
const knownPackageManager = options.packageManager ?? treePackageManager(tree, recorded);
|
|
3029
|
+
const packageManager = knownPackageManager ?? "pnpm";
|
|
3030
|
+
const config = buildPresetConfig({
|
|
3031
|
+
preset,
|
|
3032
|
+
frontend,
|
|
3033
|
+
backend,
|
|
3034
|
+
locale,
|
|
3035
|
+
packageManager,
|
|
3036
|
+
reviewEnabled: options.review,
|
|
3037
|
+
// option > toolkit.config.json's aiHosts > Claude Code: a standalone `nx g init` on a
|
|
3038
|
+
// workspace set up for other hosts keeps them (and their review provider).
|
|
3039
|
+
aiHosts: resolveAiHostsOption(options.aiHosts, recorded),
|
|
3040
|
+
// option > the recorded review.provider (unless --aiHosts changes the first host) > the
|
|
3041
|
+
// first AI host's: a bare rerun keeps it.
|
|
3042
|
+
reviewProvider: options.reviewProvider ?? keptReviewProvider(options.aiHosts, recorded)
|
|
3043
|
+
});
|
|
3044
|
+
const sections = [];
|
|
3045
|
+
const fsdEnabled = frontend !== "none" && (options.fsd ?? config.fsd.enabled);
|
|
3046
|
+
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
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
} else {
|
|
3059
|
+
sections.push({ id: "fsd", enabled: false, report: null });
|
|
3060
|
+
}
|
|
3061
|
+
const agentRulesEnabled = options.agentRules ?? true;
|
|
3062
|
+
if (agentRulesEnabled) {
|
|
3063
|
+
const report = runAddAgentRules(
|
|
3064
|
+
tree,
|
|
3065
|
+
{
|
|
3066
|
+
hosts: config.aiHosts,
|
|
3067
|
+
teamRules: config.agentRules.teamRules,
|
|
3068
|
+
locale,
|
|
3069
|
+
dryRun
|
|
3070
|
+
},
|
|
3071
|
+
// init records aiHosts below, with its other choices; the FSD section follows this run's
|
|
3072
|
+
// frontend axis
|
|
3073
|
+
{ recordHosts: false, fsdStructure: fsdEnabled }
|
|
3074
|
+
);
|
|
3075
|
+
sections.push({ id: "agentRules", enabled: true, report: { kind: "agentRules", report } });
|
|
3076
|
+
} else {
|
|
3077
|
+
sections.push({ id: "agentRules", enabled: false, report: null });
|
|
3078
|
+
}
|
|
3079
|
+
const githubTemplatesEnabled = options.githubTemplates ?? true;
|
|
3080
|
+
if (githubTemplatesEnabled) {
|
|
3081
|
+
const report = runAddGithubTemplates(tree, {
|
|
3082
|
+
issue: config.githubTemplates.issue,
|
|
3083
|
+
pr: config.githubTemplates.pr,
|
|
3084
|
+
codeowners: config.githubTemplates.codeowners,
|
|
3085
|
+
locale,
|
|
3086
|
+
dryRun
|
|
3087
|
+
});
|
|
3088
|
+
sections.push({ id: "githubTemplates", enabled: true, report: { kind: "githubTemplates", report } });
|
|
3089
|
+
} else {
|
|
3090
|
+
sections.push({ id: "githubTemplates", enabled: false, report: null });
|
|
3091
|
+
}
|
|
3092
|
+
if (config.backend !== "none") {
|
|
3093
|
+
const report = runAddBackend(tree, {
|
|
3094
|
+
backend: config.backend,
|
|
3095
|
+
applyEslint: options.applyEslint ?? false,
|
|
3096
|
+
dryRun
|
|
3097
|
+
});
|
|
3098
|
+
sections.push({ id: "backend", enabled: true, report: { kind: "backend", report } });
|
|
3099
|
+
} else {
|
|
3100
|
+
sections.push({ id: "backend", enabled: false, report: null });
|
|
3101
|
+
}
|
|
3102
|
+
if (config.review.enabled) {
|
|
3103
|
+
const report = runAddReview(tree, {
|
|
3104
|
+
mode: config.review.mode,
|
|
3105
|
+
locale,
|
|
3106
|
+
maxRounds: config.review.maxRounds,
|
|
3107
|
+
packageManager,
|
|
3108
|
+
// the resolved value reaches the workflow
|
|
3109
|
+
dryRun,
|
|
3110
|
+
// The first AI host's provider — buildPresetConfig
|
|
3111
|
+
// resolved it; claude when no host was picked.
|
|
3112
|
+
provider: config.review.provider
|
|
3113
|
+
});
|
|
3114
|
+
sections.push({ id: "review", enabled: true, report: { kind: "review", report } });
|
|
3115
|
+
} else {
|
|
3116
|
+
sections.push({ id: "review", enabled: false, report: null });
|
|
3117
|
+
}
|
|
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;
|
|
3121
|
+
const agentRulesSection = sections.find((s) => s.id === "agentRules");
|
|
3122
|
+
const agentRulesRan = agentRulesSection?.enabled === true;
|
|
3123
|
+
const agentRulesApplied = agentRulesSection?.report?.kind === "agentRules" && agentRulesSection.report.report.files.every((f) => f.action !== "unrecognized");
|
|
3124
|
+
const reviewSection = sections.find((s) => s.id === "review");
|
|
3125
|
+
const reviewApplied = reviewSection?.report?.kind === "review" && ["created", "updated", "unchanged"].includes(reviewSection.report.report.configAction);
|
|
3126
|
+
const anyChoiceApplied = fsdApplied || agentRulesApplied || reviewApplied;
|
|
3127
|
+
const notRecorded = [];
|
|
3128
|
+
if (fsdRan && !fsdApplied) {
|
|
3129
|
+
notRecorded.push({ field: "fsd.enforcement", rerun: `add-fsd-lite --enforcement=${config.fsd.enforcement}` });
|
|
3130
|
+
}
|
|
3131
|
+
if (agentRulesRan && !agentRulesApplied) {
|
|
3132
|
+
notRecorded.push({
|
|
3133
|
+
field: "agentRules.teamRules",
|
|
3134
|
+
rerun: config.agentRules.teamRules ? "add-agent-rules --teamRules" : "add-agent-rules --teamRules=false"
|
|
3135
|
+
});
|
|
3136
|
+
notRecorded.push({ field: "aiHosts", rerun: `add-agent-rules --hosts=${config.aiHosts.join(",")}` });
|
|
3137
|
+
}
|
|
3138
|
+
const configOutcome = recordPresetChoices(
|
|
3139
|
+
tree,
|
|
3140
|
+
{
|
|
3141
|
+
// Every later generator (slices, templates, agent rules, review) reads locale; a bare
|
|
3142
|
+
// rerun of init or add-fsd-lite reads preset / frontend; packageManager backs up
|
|
3143
|
+
// lockfile detection.
|
|
3144
|
+
locale: anyChoiceApplied ? locale : void 0,
|
|
3145
|
+
preset: anyChoiceApplied ? preset : void 0,
|
|
3146
|
+
frontend: anyChoiceApplied ? frontend : void 0,
|
|
3147
|
+
// Never the blind pnpm guess: recording it would silence add-review's "could not be
|
|
3148
|
+
// detected" note later. Only a flag, a lockfile or an earlier record is recorded.
|
|
3149
|
+
packageManager: anyChoiceApplied && knownPackageManager !== null ? knownPackageManager : void 0,
|
|
3150
|
+
enforcement: fsdApplied ? config.fsd.enforcement : void 0,
|
|
3151
|
+
teamRules: agentRulesApplied ? config.agentRules.teamRules : void 0,
|
|
3152
|
+
aiHosts: agentRulesApplied ? config.aiHosts : void 0
|
|
3153
|
+
},
|
|
3154
|
+
notRecorded,
|
|
3155
|
+
!dryRun
|
|
3156
|
+
);
|
|
3157
|
+
return {
|
|
3158
|
+
preset,
|
|
3159
|
+
frontend,
|
|
3160
|
+
backend,
|
|
3161
|
+
packageManager,
|
|
3162
|
+
dryRun,
|
|
3163
|
+
cleanTree,
|
|
3164
|
+
allowDirty,
|
|
3165
|
+
refused: false,
|
|
3166
|
+
sections,
|
|
3167
|
+
config: configOutcome
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
function gitIsWorkingTreeClean(root) {
|
|
3171
|
+
try {
|
|
3172
|
+
const output = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf-8" });
|
|
3173
|
+
return output.trim().length === 0;
|
|
3174
|
+
} catch (error) {
|
|
3175
|
+
console.warn(
|
|
3176
|
+
"init: could not determine the working tree status (the git command failed) \u2014 treating it as dirty for safety."
|
|
3177
|
+
);
|
|
3178
|
+
if (error instanceof Error) console.warn(` ${error.message}`);
|
|
3179
|
+
return false;
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
function collectWrittenWorkflowPaths(report) {
|
|
3183
|
+
const paths = [];
|
|
3184
|
+
for (const section of report.sections) {
|
|
3185
|
+
if (section.report?.kind !== "review") continue;
|
|
3186
|
+
const { workflowAction, workflowPath } = section.report.report;
|
|
3187
|
+
if (workflowAction === "created" || workflowAction === "overwritten" || workflowAction === "alternate") {
|
|
3188
|
+
paths.push(workflowPath);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
return paths;
|
|
3192
|
+
}
|
|
3193
|
+
async function initGenerator(tree, options, deps = { isWorkingTreeClean: gitIsWorkingTreeClean }) {
|
|
3194
|
+
const changesBefore = tree.listChanges().length;
|
|
3195
|
+
const report = runInit(tree, { ...options, dryRun: resolveDryRun(options) }, deps);
|
|
3196
|
+
console.log(renderInitReport(report));
|
|
3197
|
+
if (report.dryRun || report.refused) return;
|
|
3198
|
+
if (tree.listChanges().length === changesBefore) return;
|
|
3199
|
+
const workflowPaths = collectWrittenWorkflowPaths(report);
|
|
3200
|
+
const root = tree.root;
|
|
3201
|
+
const graphOutputFile = join2(tmpdir(), `nx-archweave-init-graph-${Date.now()}.json`);
|
|
3202
|
+
return () => {
|
|
3203
|
+
runPostInitValidation({ root, workflowPaths, graphOutputFile }, createProcessExec());
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
var generator_default = initGenerator;
|
|
3207
|
+
export {
|
|
3208
|
+
generator_default as default,
|
|
3209
|
+
initGenerator,
|
|
3210
|
+
runInit
|
|
3211
|
+
};
|