@clearlydev/nx-archweave 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +849 -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 +2462 -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 +1265 -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 +883 -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 +711 -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 +96 -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,2462 @@
|
|
|
1
|
+
// ../shared/dist/lib/doctor-report.js
|
|
2
|
+
function createDoctorCheck(input) {
|
|
3
|
+
return { suggestedAction: void 0, ...input };
|
|
4
|
+
}
|
|
5
|
+
function computeOverallStatus(checks) {
|
|
6
|
+
if (checks.some((c) => c.status === "error"))
|
|
7
|
+
return "conflict";
|
|
8
|
+
if (checks.some((c) => c.status === "warning"))
|
|
9
|
+
return "manual-review-required";
|
|
10
|
+
return "safe";
|
|
11
|
+
}
|
|
12
|
+
var DOCTOR_REPORT_SCHEMA_ID = "archweave/doctor-report@1";
|
|
13
|
+
function serializeDoctorReport(report) {
|
|
14
|
+
const count = (status) => report.checks.filter((c) => c.status === status).length;
|
|
15
|
+
const body = {
|
|
16
|
+
schema: DOCTOR_REPORT_SCHEMA_ID,
|
|
17
|
+
overallStatus: report.overallStatus,
|
|
18
|
+
summary: {
|
|
19
|
+
pass: count("pass"),
|
|
20
|
+
warning: count("warning"),
|
|
21
|
+
error: count("error"),
|
|
22
|
+
notApplicable: count("not-applicable")
|
|
23
|
+
},
|
|
24
|
+
checks: report.checks.map((c) => ({
|
|
25
|
+
id: c.id,
|
|
26
|
+
label: c.label,
|
|
27
|
+
status: c.status,
|
|
28
|
+
message: c.message,
|
|
29
|
+
evidence: c.evidence,
|
|
30
|
+
...c.suggestedAction !== void 0 ? { suggestedAction: c.suggestedAction } : {}
|
|
31
|
+
}))
|
|
32
|
+
};
|
|
33
|
+
return `${JSON.stringify(body, null, 2)}
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ../shared/dist/lib/spawnable-command.js
|
|
38
|
+
import { statSync } from "node:fs";
|
|
39
|
+
import { win32 } from "node:path";
|
|
40
|
+
function isFile(path) {
|
|
41
|
+
try {
|
|
42
|
+
return statSync(path).isFile();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
var DEFAULT_ENV = { platform: process.platform, env: process.env, isFile };
|
|
48
|
+
function envValue(env, name) {
|
|
49
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
50
|
+
return key === void 0 ? void 0 : env[key];
|
|
51
|
+
}
|
|
52
|
+
function resolveWindowsCommand(command, environment) {
|
|
53
|
+
const exts = (envValue(environment.env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
54
|
+
const candidates = (base) => win32.extname(base) !== "" ? [base] : exts.map((ext) => base + ext);
|
|
55
|
+
if (win32.isAbsolute(command) || /[\\/]/.test(command)) {
|
|
56
|
+
return candidates(command).find(environment.isFile);
|
|
57
|
+
}
|
|
58
|
+
const dirs = (envValue(environment.env, "PATH") ?? "").split(";").filter(Boolean);
|
|
59
|
+
for (const dir of dirs) {
|
|
60
|
+
const found = candidates(win32.join(dir, command)).find(environment.isFile);
|
|
61
|
+
if (found !== void 0)
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
var META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
67
|
+
var BIN_SHIM = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i;
|
|
68
|
+
function escapeCmdArgument(arg, doubleEscape = false) {
|
|
69
|
+
let quoted = arg.replace(/(\\*)"/g, '$1$1\\"');
|
|
70
|
+
quoted = quoted.replace(/(\\*)$/, "$1$1");
|
|
71
|
+
const escaped = `"${quoted}"`.replace(META_CHARS, "^$1");
|
|
72
|
+
return doubleEscape ? escaped.replace(META_CHARS, "^$1") : escaped;
|
|
73
|
+
}
|
|
74
|
+
function spawnableCommand(command, args, environment = DEFAULT_ENV) {
|
|
75
|
+
if (environment.platform !== "win32")
|
|
76
|
+
return { command, args: [...args] };
|
|
77
|
+
const resolved = resolveWindowsCommand(command, environment);
|
|
78
|
+
if (resolved === void 0 || !/\.(cmd|bat)$/i.test(resolved))
|
|
79
|
+
return { command, args: [...args] };
|
|
80
|
+
const doubleEscape = BIN_SHIM.test(resolved);
|
|
81
|
+
const line = [resolved.replace(META_CHARS, "^$1"), ...args.map((arg) => escapeCmdArgument(arg, doubleEscape))].join(" ");
|
|
82
|
+
return {
|
|
83
|
+
command: envValue(environment.env, "COMSPEC") ?? "cmd.exe",
|
|
84
|
+
args: ["/d", "/s", "/c", `"${line}"`],
|
|
85
|
+
windowsVerbatimArguments: true
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/generators/doctor/render.ts
|
|
90
|
+
function renderDoctorReport(report) {
|
|
91
|
+
const lines = [`Overall status: ${report.overallStatus}`, ""];
|
|
92
|
+
for (const check of report.checks) {
|
|
93
|
+
lines.push(`[${check.status.toUpperCase()}] ${check.label}: ${check.message}`);
|
|
94
|
+
if (check.suggestedAction) {
|
|
95
|
+
lines.push(` \u2192 ${check.suggestedAction}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ../template-engine/dist/doctor/detect-environment.js
|
|
102
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
103
|
+
import { join } from "node:path";
|
|
104
|
+
|
|
105
|
+
// ../template-engine/dist/environment/package-manager.js
|
|
106
|
+
var LOCKFILES = [
|
|
107
|
+
["yarn", ["yarn.lock"]],
|
|
108
|
+
["bun", ["bun.lock", "bun.lockb"]],
|
|
109
|
+
["npm", ["package-lock.json"]]
|
|
110
|
+
];
|
|
111
|
+
function detectWorkspacePackageManager(exists, read = () => void 0) {
|
|
112
|
+
if (exists("pnpm-lock.yaml") || exists("pnpm-workspace.yaml"))
|
|
113
|
+
return "pnpm";
|
|
114
|
+
const found = LOCKFILES.filter(([, files]) => files.some((file) => exists(file))).map(([pm]) => pm);
|
|
115
|
+
if (found.length <= 1)
|
|
116
|
+
return found[0] ?? null;
|
|
117
|
+
const declared = /"packageManager"\s*:\s*"(npm|yarn|bun)@/.exec(read("package.json") ?? "")?.[1];
|
|
118
|
+
const named = found.find((pm) => pm === declared);
|
|
119
|
+
if (named)
|
|
120
|
+
return named;
|
|
121
|
+
return found.includes("npm") ? "npm" : found[0];
|
|
122
|
+
}
|
|
123
|
+
function packageManagerCommands(pm, flavor = "classic") {
|
|
124
|
+
switch (pm) {
|
|
125
|
+
case "pnpm":
|
|
126
|
+
return { addDevAtRoot: (spec) => `pnpm add -D -w ${spec}`, addAtRoot: (spec) => `pnpm add -w ${spec}`, runNx: "pnpm nx", install: "pnpm install", workspaceRegistration: "pnpm-workspace.yaml" };
|
|
127
|
+
case "yarn":
|
|
128
|
+
return {
|
|
129
|
+
addDevAtRoot: (spec) => flavor === "berry" ? `yarn add -D ${spec}` : `yarn add -D -W ${spec}`,
|
|
130
|
+
addAtRoot: (spec) => flavor === "berry" ? `yarn add ${spec}` : `yarn add -W ${spec}`,
|
|
131
|
+
runNx: "yarn nx",
|
|
132
|
+
install: "yarn install",
|
|
133
|
+
workspaceRegistration: "package.json workspaces"
|
|
134
|
+
};
|
|
135
|
+
case "bun":
|
|
136
|
+
return { addDevAtRoot: (spec) => `bun add -d ${spec}`, addAtRoot: (spec) => `bun add ${spec}`, runNx: "bun nx", install: "bun install", workspaceRegistration: "package.json workspaces" };
|
|
137
|
+
case "npm":
|
|
138
|
+
return { addDevAtRoot: (spec) => `npm install -D ${spec}`, addAtRoot: (spec) => `npm install ${spec}`, runNx: "npm exec nx --", install: "npm install", workspaceRegistration: "package.json workspaces" };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ../template-engine/dist/doctor/detect-environment.js
|
|
143
|
+
function detectEnvironment(root) {
|
|
144
|
+
const hasNxWorkspace = existsSync(join(root, "nx.json"));
|
|
145
|
+
if (!hasNxWorkspace) {
|
|
146
|
+
return { hasNxWorkspace: false };
|
|
147
|
+
}
|
|
148
|
+
let nxVersion;
|
|
149
|
+
let rootPackageJson;
|
|
150
|
+
let rootPackageJsonError;
|
|
151
|
+
const packageJsonPath = join(root, "package.json");
|
|
152
|
+
if (existsSync(packageJsonPath)) {
|
|
153
|
+
try {
|
|
154
|
+
rootPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
155
|
+
const deps = rootPackageJson?.["devDependencies"];
|
|
156
|
+
const deps2 = rootPackageJson?.["dependencies"];
|
|
157
|
+
nxVersion = deps?.["nx"] ?? deps2?.["nx"];
|
|
158
|
+
} catch (error) {
|
|
159
|
+
rootPackageJsonError = error instanceof Error ? error.message : String(error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const exists = (path) => existsSync(join(root, path));
|
|
163
|
+
const packageManager = detectWorkspacePackageManager(exists, (path) => exists(path) ? readFileSync(join(root, path), "utf-8") : void 0) ?? void 0;
|
|
164
|
+
return { hasNxWorkspace, nxVersion, packageManager, rootPackageJson, rootPackageJsonError };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ../template-engine/dist/doctor/run-doctor-checks.js
|
|
168
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
169
|
+
import { join as join6 } from "node:path";
|
|
170
|
+
|
|
171
|
+
// ../template-engine/dist/doctor/review-checks.js
|
|
172
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
173
|
+
import { join as join2 } from "node:path";
|
|
174
|
+
|
|
175
|
+
// ../review-policy/dist/modes/review-modes.js
|
|
176
|
+
var REVIEW_MODES = {
|
|
177
|
+
fast: {
|
|
178
|
+
mode: "fast",
|
|
179
|
+
promptStyle: "fast-condensed",
|
|
180
|
+
includesCorrectnessSecurity: true,
|
|
181
|
+
includesOverEngineering: false
|
|
182
|
+
},
|
|
183
|
+
standard: {
|
|
184
|
+
mode: "standard",
|
|
185
|
+
promptStyle: "sectioned",
|
|
186
|
+
includesCorrectnessSecurity: true,
|
|
187
|
+
includesOverEngineering: false
|
|
188
|
+
},
|
|
189
|
+
thorough: {
|
|
190
|
+
mode: "thorough",
|
|
191
|
+
promptStyle: "sectioned",
|
|
192
|
+
includesCorrectnessSecurity: true,
|
|
193
|
+
includesOverEngineering: true
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// ../review-policy/dist/verdict-gate/findings-block.js
|
|
198
|
+
var FINDINGS_MARKER = "<!-- archweave-findings-v1 -->";
|
|
199
|
+
var FINDINGS_SCHEMA_ID = "archweave/findings@1";
|
|
200
|
+
|
|
201
|
+
// ../review-policy/dist/prompts/prompt-shared.js
|
|
202
|
+
function renderFindingSchemaInstruction(locale, priorities) {
|
|
203
|
+
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:";
|
|
204
|
+
return [
|
|
205
|
+
intro,
|
|
206
|
+
`- priority: ${priorities.join(" | ")}`,
|
|
207
|
+
"- category: kebab-case category name",
|
|
208
|
+
"- title: one-line summary",
|
|
209
|
+
"- file: path of the affected file",
|
|
210
|
+
"- lineRange: { start, end? }",
|
|
211
|
+
"- evidence: what in the code proves the problem",
|
|
212
|
+
"- impact: what goes wrong and for whom",
|
|
213
|
+
"- reproductionCondition: the exact input/state that triggers it",
|
|
214
|
+
"- recommendation: the concrete fix",
|
|
215
|
+
"- confidence: confirmed | plausible"
|
|
216
|
+
].join("\n");
|
|
217
|
+
}
|
|
218
|
+
function renderFindingsBlockInstruction(locale) {
|
|
219
|
+
const lines = locale === "en" ? [
|
|
220
|
+
"At the END of the top-level summary comment, append the machine-readable findings block:",
|
|
221
|
+
`the literal marker line ${FINDINGS_MARKER}, then a fenced \`\`\`json block containing`,
|
|
222
|
+
`{ "schema": "${FINDINGS_SCHEMA_ID}", "findings": [ ... ] } with one entry per finding,`,
|
|
223
|
+
"using exactly the finding schema above. If there are no findings, still append the block with an empty findings array."
|
|
224
|
+
] : [
|
|
225
|
+
"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:",
|
|
226
|
+
`\uB9AC\uD130\uB7F4 \uB9C8\uCEE4 \uC904 ${FINDINGS_MARKER} \uB2E4\uC74C\uC5D0 fenced \`\`\`json \uBE14\uB85D\uC73C\uB85C`,
|
|
227
|
+
`{ "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.`,
|
|
228
|
+
"finding\uC774 \uC5C6\uC5B4\uB3C4 \uBE48 \uBC30\uC5F4\uB85C \uBE14\uB85D\uC744 \uBC18\uB4DC\uC2DC \uD3EC\uD568\uD558\uC138\uC694."
|
|
229
|
+
];
|
|
230
|
+
return lines.join("\n");
|
|
231
|
+
}
|
|
232
|
+
function renderDedupInstruction(locale) {
|
|
233
|
+
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.";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ../review-policy/dist/prompts/correctness-security-prompt.js
|
|
237
|
+
var CORRECTNESS_SECURITY_PRIORITIES = ["P0", "P1", "P2", "Nit"];
|
|
238
|
+
var FAST_PRIORITIES = ["P0", "P1", "P2"];
|
|
239
|
+
function buildCorrectnessSecurityPrompt(locale, style = "sectioned") {
|
|
240
|
+
if (style === "fast-condensed") {
|
|
241
|
+
const fastBody = locale === "en" ? [
|
|
242
|
+
"You are a code reviewer focused on correctness and security.",
|
|
243
|
+
"",
|
|
244
|
+
"Review the provided diff and report only the most critical problems. Skip minor style and taste remarks entirely \u2014 report nothing below P2.",
|
|
245
|
+
"",
|
|
246
|
+
`Only report findings with priority ${FAST_PRIORITIES.join(", ")}. Do NOT report P3 (over-engineering) findings \u2014 they are out of scope here.`
|
|
247
|
+
] : [
|
|
248
|
+
"\uB2F9\uC2E0\uC740 \uC815\uD655\uC131(correctness)\uACFC \uBCF4\uC548(security)\uC5D0 \uC9D1\uC911\uD558\uB294 \uCF54\uB4DC \uB9AC\uBDF0\uC5B4\uC785\uB2C8\uB2E4.",
|
|
249
|
+
"",
|
|
250
|
+
"\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.",
|
|
251
|
+
"",
|
|
252
|
+
`\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.`
|
|
253
|
+
];
|
|
254
|
+
return [...fastBody, "", renderFindingSchemaInstruction(locale, FAST_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
255
|
+
}
|
|
256
|
+
const body = locale === "en" ? [
|
|
257
|
+
"You are a code reviewer focused on correctness and security.",
|
|
258
|
+
"",
|
|
259
|
+
'Review the provided diff and report findings in two explicitly separated top-level sections: "Correctness" and "Security". Never mix the two.',
|
|
260
|
+
"",
|
|
261
|
+
`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.`
|
|
262
|
+
] : [
|
|
263
|
+
"\uB2F9\uC2E0\uC740 \uC815\uD655\uC131(correctness)\uACFC \uBCF4\uC548(security)\uC5D0 \uC9D1\uC911\uD558\uB294 \uCF54\uB4DC \uB9AC\uBDF0\uC5B4\uC785\uB2C8\uB2E4.",
|
|
264
|
+
"",
|
|
265
|
+
'\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.',
|
|
266
|
+
"",
|
|
267
|
+
`\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.`
|
|
268
|
+
];
|
|
269
|
+
return [...body, "", renderFindingSchemaInstruction(locale, CORRECTNESS_SECURITY_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ../review-policy/dist/prompts/over-engineering-prompt.js
|
|
273
|
+
var OVER_ENGINEERING_PRIORITIES = ["P3"];
|
|
274
|
+
function buildOverEngineeringPrompt(locale) {
|
|
275
|
+
const body = locale === "en" ? [
|
|
276
|
+
"You are a code reviewer focused exclusively on over-engineering.",
|
|
277
|
+
"",
|
|
278
|
+
"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.",
|
|
279
|
+
"",
|
|
280
|
+
'Report your findings in their own top-level section titled "Over-engineering"; never mix them into correctness or security sections.'
|
|
281
|
+
] : [
|
|
282
|
+
"\uB2F9\uC2E0\uC740 \uACFC\uC789 \uC124\uACC4(over-engineering)\uB9CC\uC744 \uC804\uB2F4\uD558\uB294 \uCF54\uB4DC \uB9AC\uBDF0\uC5B4\uC785\uB2C8\uB2E4.",
|
|
283
|
+
"",
|
|
284
|
+
"\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.",
|
|
285
|
+
"",
|
|
286
|
+
'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.'
|
|
287
|
+
];
|
|
288
|
+
return [...body, "", renderFindingSchemaInstruction(locale, OVER_ENGINEERING_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ../review-policy/dist/verdict-gate/verdict-gate.js
|
|
292
|
+
var ROUND_MARKER = "<!-- archweave-review-round -->";
|
|
293
|
+
|
|
294
|
+
// ../template-engine/dist/review/workflow-skeleton.js
|
|
295
|
+
function indentBlock(text, spaces) {
|
|
296
|
+
const pad = " ".repeat(spaces);
|
|
297
|
+
return text.split("\n").map((line) => line.length === 0 ? line : pad + line).join("\n");
|
|
298
|
+
}
|
|
299
|
+
function renderTriggerBlock() {
|
|
300
|
+
return ["on:", " pull_request:", " types: [opened, synchronize, ready_for_review, reopened]"].join("\n");
|
|
301
|
+
}
|
|
302
|
+
function renderConcurrencyBlock() {
|
|
303
|
+
return [
|
|
304
|
+
"concurrency:",
|
|
305
|
+
" group: archweave-review-${{ github.event.pull_request.number }}",
|
|
306
|
+
" cancel-in-progress: false"
|
|
307
|
+
].join("\n");
|
|
308
|
+
}
|
|
309
|
+
function renderPermissionsBlock() {
|
|
310
|
+
return ["permissions:", " contents: read", " pull-requests: write", " id-token: write"].join("\n");
|
|
311
|
+
}
|
|
312
|
+
function renderSecretGateStep(secretName) {
|
|
313
|
+
return [
|
|
314
|
+
"- name: Check review secret",
|
|
315
|
+
" id: secret-gate",
|
|
316
|
+
" env:",
|
|
317
|
+
` REVIEW_SECRET: \${{ secrets.${secretName} }}`,
|
|
318
|
+
" run: |",
|
|
319
|
+
' if [ -z "$REVIEW_SECRET" ]; then',
|
|
320
|
+
` echo "AI review is not active: register the ${secretName} secret to enable it."`,
|
|
321
|
+
' echo "enabled=false" >> "$GITHUB_OUTPUT"',
|
|
322
|
+
" else",
|
|
323
|
+
' echo "enabled=true" >> "$GITHUB_OUTPUT"',
|
|
324
|
+
" fi"
|
|
325
|
+
].join("\n");
|
|
326
|
+
}
|
|
327
|
+
function renderRoundGateStep(maxRounds) {
|
|
328
|
+
return [
|
|
329
|
+
"- name: Check review round limit",
|
|
330
|
+
" id: round-gate",
|
|
331
|
+
" if: steps.secret-gate.outputs.enabled == 'true'",
|
|
332
|
+
" env:",
|
|
333
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
334
|
+
" run: |",
|
|
335
|
+
` 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')`,
|
|
336
|
+
` rounds=$(printf '%s' "$counts" | cut -f1)`,
|
|
337
|
+
` echo "last-comment-id=$(printf '%s' "$counts" | cut -f2)" >> "$GITHUB_OUTPUT"`,
|
|
338
|
+
` if [ "$rounds" -ge ${maxRounds} ]; then`,
|
|
339
|
+
` echo "Automated review round limit (${maxRounds}) reached for this PR; skipping."`,
|
|
340
|
+
' echo "allowed=false" >> "$GITHUB_OUTPUT"',
|
|
341
|
+
" else",
|
|
342
|
+
' echo "allowed=true" >> "$GITHUB_OUTPUT"',
|
|
343
|
+
" fi"
|
|
344
|
+
].join("\n");
|
|
345
|
+
}
|
|
346
|
+
function renderVerdictGateStep(maxRounds, locale) {
|
|
347
|
+
return [
|
|
348
|
+
"- name: Compute review verdict",
|
|
349
|
+
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
350
|
+
" env:",
|
|
351
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
352
|
+
" GITHUB_REPOSITORY: ${{ github.repository }}",
|
|
353
|
+
" ARCHWEAVE_PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
354
|
+
` ARCHWEAVE_MAX_ROUNDS: '${maxRounds}'`,
|
|
355
|
+
` ARCHWEAVE_LOCALE: '${locale}'`,
|
|
356
|
+
" ARCHWEAVE_HEAD_SHA: ${{ github.event.pull_request.head.sha }}",
|
|
357
|
+
" ARCHWEAVE_ROUND_AFTER_ID: ${{ steps.round-gate.outputs.last-comment-id }}",
|
|
358
|
+
" run: |",
|
|
359
|
+
" if [ -x node_modules/.bin/archweave-review-verdict ]; then",
|
|
360
|
+
" node_modules/.bin/archweave-review-verdict",
|
|
361
|
+
" else",
|
|
362
|
+
' echo "nx-archweave is not installed in this workspace - skipping the verdict gate."',
|
|
363
|
+
" fi"
|
|
364
|
+
].join("\n");
|
|
365
|
+
}
|
|
366
|
+
var SETUP_NODE_VERSION = "22";
|
|
367
|
+
var SETUP_PNPM_VERSION = "10";
|
|
368
|
+
var SETUP_BUN_ACTION = "oven-sh/setup-bun@v2";
|
|
369
|
+
var INSTALL_LINES = {
|
|
370
|
+
pnpm: [" pnpm install --frozen-lockfile"],
|
|
371
|
+
npm: [" npm ci"],
|
|
372
|
+
yarn: [
|
|
373
|
+
" corepack enable yarn",
|
|
374
|
+
' if [ "$(yarn --version | cut -d. -f1)" -ge 2 ]; then yarn install --immutable; else yarn install --frozen-lockfile; fi'
|
|
375
|
+
],
|
|
376
|
+
bun: [" bun install --frozen-lockfile"]
|
|
377
|
+
};
|
|
378
|
+
function renderWorkspaceSetupSteps(packageManager) {
|
|
379
|
+
const gate = " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
|
|
380
|
+
const steps = [];
|
|
381
|
+
if (packageManager === "pnpm") {
|
|
382
|
+
steps.push(["- name: Set up pnpm", gate, " uses: pnpm/action-setup@v4", " with:", ` version: ${SETUP_PNPM_VERSION}`].join("\n"));
|
|
383
|
+
}
|
|
384
|
+
if (packageManager === "bun") {
|
|
385
|
+
steps.push(["- name: Set up bun", gate, ` uses: ${SETUP_BUN_ACTION}`].join("\n"));
|
|
386
|
+
}
|
|
387
|
+
steps.push([
|
|
388
|
+
"- name: Set up Node",
|
|
389
|
+
gate,
|
|
390
|
+
" uses: actions/setup-node@v4",
|
|
391
|
+
" with:",
|
|
392
|
+
` node-version: ${SETUP_NODE_VERSION}`,
|
|
393
|
+
// yarn: no setup-node cache — it would query the runner's global yarn 1 even in a
|
|
394
|
+
// berry project. Corepack (shipped with Node) provides the project's own yarn below.
|
|
395
|
+
// bun: setup-node has no bun cache. Node is still set up — nx and the verdict bin
|
|
396
|
+
// run on it.
|
|
397
|
+
...packageManager === "yarn" || packageManager === "bun" ? [] : [` cache: ${packageManager}`]
|
|
398
|
+
].join("\n"));
|
|
399
|
+
steps.push([
|
|
400
|
+
"- name: Install workspace dependencies",
|
|
401
|
+
gate,
|
|
402
|
+
" run: |",
|
|
403
|
+
...INSTALL_LINES[packageManager]
|
|
404
|
+
].join("\n"));
|
|
405
|
+
return steps.join("\n\n");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ../template-engine/dist/review/claude/claude-provider.js
|
|
409
|
+
var SECRET_NAME = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
410
|
+
var ALLOWED_TOOLS = "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)";
|
|
411
|
+
function renderWorkflowInstructions(locale) {
|
|
412
|
+
const lines = locale === "en" ? [
|
|
413
|
+
"Review the diff of this pull request using `gh pr diff` and `gh pr view`.",
|
|
414
|
+
"Post specific findings as inline comments and exactly one top-level summary comment.",
|
|
415
|
+
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
416
|
+
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
417
|
+
"Do not modify any code, and never approve or merge the pull request.",
|
|
418
|
+
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
419
|
+
] : [
|
|
420
|
+
"\uC774 pull request\uC758 diff\uB97C `gh pr diff`\uC640 `gh pr view`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
|
|
421
|
+
"\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.",
|
|
422
|
+
`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.`,
|
|
423
|
+
'\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).',
|
|
424
|
+
"\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).",
|
|
425
|
+
"\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."
|
|
426
|
+
];
|
|
427
|
+
return lines.join("\n");
|
|
428
|
+
}
|
|
429
|
+
function composePrompt(input) {
|
|
430
|
+
const mode = REVIEW_MODES[input.mode];
|
|
431
|
+
const sections = [renderWorkflowInstructions(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
432
|
+
if (mode.includesOverEngineering) {
|
|
433
|
+
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
434
|
+
}
|
|
435
|
+
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
436
|
+
return sections.join("\n\n---\n\n");
|
|
437
|
+
}
|
|
438
|
+
function buildWorkflowYaml(input) {
|
|
439
|
+
const steps = [
|
|
440
|
+
renderSecretGateStep(SECRET_NAME),
|
|
441
|
+
renderRoundGateStep(input.maxRounds),
|
|
442
|
+
[
|
|
443
|
+
"- name: Checkout repository",
|
|
444
|
+
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
445
|
+
" uses: actions/checkout@v4",
|
|
446
|
+
" with:",
|
|
447
|
+
" fetch-depth: 1"
|
|
448
|
+
].join("\n"),
|
|
449
|
+
[
|
|
450
|
+
"- name: Run Claude review",
|
|
451
|
+
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
452
|
+
" uses: anthropics/claude-code-action@v1",
|
|
453
|
+
" with:",
|
|
454
|
+
` claude_code_oauth_token: \${{ secrets.${SECRET_NAME} }}`,
|
|
455
|
+
" prompt: |",
|
|
456
|
+
indentBlock(composePrompt(input), 6),
|
|
457
|
+
" claude_args: |",
|
|
458
|
+
` --allowedTools "${ALLOWED_TOOLS}"`
|
|
459
|
+
].join("\n"),
|
|
460
|
+
renderWorkspaceSetupSteps(input.packageManager),
|
|
461
|
+
renderVerdictGateStep(input.maxRounds, input.locale)
|
|
462
|
+
];
|
|
463
|
+
return [
|
|
464
|
+
"name: Claude Code Review",
|
|
465
|
+
"",
|
|
466
|
+
renderTriggerBlock(),
|
|
467
|
+
"",
|
|
468
|
+
renderConcurrencyBlock(),
|
|
469
|
+
"",
|
|
470
|
+
renderPermissionsBlock(),
|
|
471
|
+
"",
|
|
472
|
+
"jobs:",
|
|
473
|
+
" review:",
|
|
474
|
+
" runs-on: ubuntu-latest",
|
|
475
|
+
" steps:",
|
|
476
|
+
indentBlock(steps.join("\n\n"), 6),
|
|
477
|
+
""
|
|
478
|
+
].join("\n");
|
|
479
|
+
}
|
|
480
|
+
var claudeProvider = {
|
|
481
|
+
id: "claude",
|
|
482
|
+
workflowFileName: ".github/workflows/claude-code-review.yml",
|
|
483
|
+
requiredSecretName: SECRET_NAME,
|
|
484
|
+
buildWorkflowYaml
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// ../template-engine/dist/review/cursor/cursor-provider.js
|
|
488
|
+
var SECRET_NAME2 = "CURSOR_API_KEY";
|
|
489
|
+
var PROMPT_HEREDOC_DELIMITER = "ARCHWEAVE_CURSOR_REVIEW_PROMPT";
|
|
490
|
+
function renderWorkflowInstructions2(locale) {
|
|
491
|
+
const lines = locale === "en" ? [
|
|
492
|
+
"Review the diff of this pull request using `gh pr diff` and `gh pr view`.",
|
|
493
|
+
"Post specific findings as inline comments and exactly one top-level summary comment.",
|
|
494
|
+
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
495
|
+
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
496
|
+
"Do not modify any code, and never approve or merge the pull request.",
|
|
497
|
+
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
498
|
+
] : [
|
|
499
|
+
"\uC774 pull request\uC758 diff\uB97C `gh pr diff`\uC640 `gh pr view`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
|
|
500
|
+
"\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.",
|
|
501
|
+
`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.`,
|
|
502
|
+
'\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).',
|
|
503
|
+
"\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).",
|
|
504
|
+
"\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."
|
|
505
|
+
];
|
|
506
|
+
return lines.join("\n");
|
|
507
|
+
}
|
|
508
|
+
function composePrompt2(input) {
|
|
509
|
+
const mode = REVIEW_MODES[input.mode];
|
|
510
|
+
const sections = [renderWorkflowInstructions2(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
511
|
+
if (mode.includesOverEngineering) {
|
|
512
|
+
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
513
|
+
}
|
|
514
|
+
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
515
|
+
return sections.join("\n\n---\n\n");
|
|
516
|
+
}
|
|
517
|
+
function buildWorkflowYaml2(input) {
|
|
518
|
+
const gateCondition = "steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
|
|
519
|
+
const steps = [
|
|
520
|
+
renderSecretGateStep(SECRET_NAME2),
|
|
521
|
+
renderRoundGateStep(input.maxRounds),
|
|
522
|
+
[
|
|
523
|
+
"- name: Checkout repository",
|
|
524
|
+
` if: ${gateCondition}`,
|
|
525
|
+
" uses: actions/checkout@v4",
|
|
526
|
+
" with:",
|
|
527
|
+
" fetch-depth: 1"
|
|
528
|
+
].join("\n"),
|
|
529
|
+
[
|
|
530
|
+
"- name: Install cursor-agent CLI",
|
|
531
|
+
` if: ${gateCondition}`,
|
|
532
|
+
" run: |",
|
|
533
|
+
" curl https://cursor.com/install -fsS | bash",
|
|
534
|
+
' echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"'
|
|
535
|
+
].join("\n"),
|
|
536
|
+
[
|
|
537
|
+
"- name: Run Cursor review",
|
|
538
|
+
` if: ${gateCondition}`,
|
|
539
|
+
" env:",
|
|
540
|
+
` CURSOR_API_KEY: \${{ secrets.${SECRET_NAME2} }}`,
|
|
541
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
542
|
+
" run: |",
|
|
543
|
+
` agent -p "$(cat <<'${PROMPT_HEREDOC_DELIMITER}'`,
|
|
544
|
+
indentBlock(composePrompt2(input), 4),
|
|
545
|
+
` ${PROMPT_HEREDOC_DELIMITER}`,
|
|
546
|
+
' )"'
|
|
547
|
+
].join("\n"),
|
|
548
|
+
renderWorkspaceSetupSteps(input.packageManager),
|
|
549
|
+
renderVerdictGateStep(input.maxRounds, input.locale)
|
|
550
|
+
];
|
|
551
|
+
return [
|
|
552
|
+
"name: Cursor Code Review",
|
|
553
|
+
"",
|
|
554
|
+
renderTriggerBlock(),
|
|
555
|
+
"",
|
|
556
|
+
renderConcurrencyBlock(),
|
|
557
|
+
"",
|
|
558
|
+
renderPermissionsBlock(),
|
|
559
|
+
"",
|
|
560
|
+
"jobs:",
|
|
561
|
+
" review:",
|
|
562
|
+
" runs-on: ubuntu-latest",
|
|
563
|
+
" steps:",
|
|
564
|
+
indentBlock(steps.join("\n\n"), 6),
|
|
565
|
+
""
|
|
566
|
+
].join("\n");
|
|
567
|
+
}
|
|
568
|
+
var cursorProvider = {
|
|
569
|
+
id: "cursor",
|
|
570
|
+
workflowFileName: ".github/workflows/cursor-code-review.yml",
|
|
571
|
+
requiredSecretName: SECRET_NAME2,
|
|
572
|
+
buildWorkflowYaml: buildWorkflowYaml2
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// ../template-engine/dist/review/gemini/gemini-provider.js
|
|
576
|
+
var SECRET_NAME3 = "GEMINI_API_KEY";
|
|
577
|
+
function renderWorkflowInstructions3(locale) {
|
|
578
|
+
const lines = locale === "en" ? [
|
|
579
|
+
"Review the diff of this pull request using `gh pr diff` and `gh pr view`.",
|
|
580
|
+
"Post specific findings as inline comments and exactly one top-level summary comment.",
|
|
581
|
+
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
582
|
+
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
583
|
+
"Do not modify any code, and never approve or merge the pull request.",
|
|
584
|
+
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
585
|
+
] : [
|
|
586
|
+
"\uC774 pull request\uC758 diff\uB97C `gh pr diff`\uC640 `gh pr view`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
|
|
587
|
+
"\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.",
|
|
588
|
+
`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.`,
|
|
589
|
+
'\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).',
|
|
590
|
+
"\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).",
|
|
591
|
+
"\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."
|
|
592
|
+
];
|
|
593
|
+
return lines.join("\n");
|
|
594
|
+
}
|
|
595
|
+
function composePrompt3(input) {
|
|
596
|
+
const mode = REVIEW_MODES[input.mode];
|
|
597
|
+
const sections = [renderWorkflowInstructions3(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
598
|
+
if (mode.includesOverEngineering) {
|
|
599
|
+
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
600
|
+
}
|
|
601
|
+
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
602
|
+
return sections.join("\n\n---\n\n");
|
|
603
|
+
}
|
|
604
|
+
function buildWorkflowYaml3(input) {
|
|
605
|
+
const steps = [
|
|
606
|
+
renderSecretGateStep(SECRET_NAME3),
|
|
607
|
+
renderRoundGateStep(input.maxRounds),
|
|
608
|
+
[
|
|
609
|
+
"- name: Checkout repository",
|
|
610
|
+
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
611
|
+
" uses: actions/checkout@v4",
|
|
612
|
+
" with:",
|
|
613
|
+
" fetch-depth: 1"
|
|
614
|
+
].join("\n"),
|
|
615
|
+
[
|
|
616
|
+
"- name: Run Gemini review",
|
|
617
|
+
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
618
|
+
" uses: google-github-actions/run-gemini-cli@v0",
|
|
619
|
+
" env:",
|
|
620
|
+
" GITHUB_TOKEN: ${{ github.token }}",
|
|
621
|
+
" with:",
|
|
622
|
+
` gemini_api_key: \${{ secrets.${SECRET_NAME3} }}`,
|
|
623
|
+
" prompt: |",
|
|
624
|
+
indentBlock(composePrompt3(input), 6)
|
|
625
|
+
].join("\n"),
|
|
626
|
+
renderWorkspaceSetupSteps(input.packageManager),
|
|
627
|
+
renderVerdictGateStep(input.maxRounds, input.locale)
|
|
628
|
+
];
|
|
629
|
+
return [
|
|
630
|
+
"name: Gemini Code Review",
|
|
631
|
+
"",
|
|
632
|
+
renderTriggerBlock(),
|
|
633
|
+
"",
|
|
634
|
+
renderConcurrencyBlock(),
|
|
635
|
+
"",
|
|
636
|
+
renderPermissionsBlock(),
|
|
637
|
+
"",
|
|
638
|
+
"jobs:",
|
|
639
|
+
" review:",
|
|
640
|
+
" runs-on: ubuntu-latest",
|
|
641
|
+
" steps:",
|
|
642
|
+
indentBlock(steps.join("\n\n"), 6),
|
|
643
|
+
""
|
|
644
|
+
].join("\n");
|
|
645
|
+
}
|
|
646
|
+
var geminiProvider = {
|
|
647
|
+
id: "gemini",
|
|
648
|
+
workflowFileName: ".github/workflows/gemini-code-review.yml",
|
|
649
|
+
requiredSecretName: SECRET_NAME3,
|
|
650
|
+
buildWorkflowYaml: buildWorkflowYaml3
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// ../template-engine/dist/review/gpt/gpt-provider.js
|
|
654
|
+
var SECRET_NAME4 = "OPENAI_API_KEY";
|
|
655
|
+
function renderWorkflowInstructions4(locale) {
|
|
656
|
+
const lines = locale === "en" ? [
|
|
657
|
+
"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.",
|
|
658
|
+
"Write your entire response as a single message: report specific findings, then close with one overall summary.",
|
|
659
|
+
`Always include the literal marker ${ROUND_MARKER} in your message.`,
|
|
660
|
+
"If there are no findings that require action, write exactly one message saying so (and still include the marker).",
|
|
661
|
+
"Do not modify any code, and never approve or merge the pull request.",
|
|
662
|
+
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
663
|
+
] : [
|
|
664
|
+
"\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).",
|
|
665
|
+
"\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.",
|
|
666
|
+
`\uBA54\uC2DC\uC9C0\uC5D0\uB294 \uBC18\uB4DC\uC2DC \uB9AC\uD130\uB7F4 \uB9C8\uCEE4 ${ROUND_MARKER}\uB97C \uD3EC\uD568\uD558\uC138\uC694.`,
|
|
667
|
+
'\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).',
|
|
668
|
+
"\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).",
|
|
669
|
+
"\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."
|
|
670
|
+
];
|
|
671
|
+
return lines.join("\n");
|
|
672
|
+
}
|
|
673
|
+
function composePrompt4(input) {
|
|
674
|
+
const mode = REVIEW_MODES[input.mode];
|
|
675
|
+
const sections = [renderWorkflowInstructions4(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
676
|
+
if (mode.includesOverEngineering) {
|
|
677
|
+
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
678
|
+
}
|
|
679
|
+
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
680
|
+
return sections.join("\n\n---\n\n");
|
|
681
|
+
}
|
|
682
|
+
function buildWorkflowYaml4(input) {
|
|
683
|
+
const gateCondition = "steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
|
|
684
|
+
const steps = [
|
|
685
|
+
renderSecretGateStep(SECRET_NAME4),
|
|
686
|
+
renderRoundGateStep(input.maxRounds),
|
|
687
|
+
[
|
|
688
|
+
"- name: Checkout repository",
|
|
689
|
+
` if: ${gateCondition}`,
|
|
690
|
+
" uses: actions/checkout@v4",
|
|
691
|
+
" with:",
|
|
692
|
+
" ref: refs/pull/${{ github.event.pull_request.number }}/merge",
|
|
693
|
+
" fetch-depth: 1",
|
|
694
|
+
" persist-credentials: false"
|
|
695
|
+
].join("\n"),
|
|
696
|
+
[
|
|
697
|
+
"- name: Fetch base and head refs for the PR",
|
|
698
|
+
` if: ${gateCondition}`,
|
|
699
|
+
" env:",
|
|
700
|
+
" PR_BASE_REF: ${{ github.event.pull_request.base.ref }}",
|
|
701
|
+
" PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
702
|
+
" run: |",
|
|
703
|
+
" # Explicit destinations: the sandboxed review step has no network, so both sides",
|
|
704
|
+
" # of the diff must survive as stable local refs (FETCH_HEAD alone is ambiguous).",
|
|
705
|
+
" git fetch --no-tags origin \\",
|
|
706
|
+
' "+refs/heads/$PR_BASE_REF:refs/remotes/origin/pr-base" \\',
|
|
707
|
+
' "+refs/pull/$PR_NUMBER/head:refs/remotes/origin/pr-head"'
|
|
708
|
+
].join("\n"),
|
|
709
|
+
[
|
|
710
|
+
"- name: Run Codex review",
|
|
711
|
+
" id: codex-review",
|
|
712
|
+
` if: ${gateCondition}`,
|
|
713
|
+
" uses: openai/codex-action@v1",
|
|
714
|
+
" with:",
|
|
715
|
+
` openai-api-key: \${{ secrets.${SECRET_NAME4} }}`,
|
|
716
|
+
' permission-profile: ":read-only"',
|
|
717
|
+
" prompt: |",
|
|
718
|
+
indentBlock(composePrompt4(input), 6)
|
|
719
|
+
].join("\n"),
|
|
720
|
+
[
|
|
721
|
+
"- name: Post review comment",
|
|
722
|
+
` if: ${gateCondition}`,
|
|
723
|
+
" env:",
|
|
724
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
725
|
+
" PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
726
|
+
" REVIEW_BODY: ${{ steps.codex-review.outputs.final-message }}",
|
|
727
|
+
" run: |",
|
|
728
|
+
' printf %s "$REVIEW_BODY" | gh pr comment "$PR_NUMBER" --body-file -'
|
|
729
|
+
].join("\n"),
|
|
730
|
+
renderWorkspaceSetupSteps(input.packageManager),
|
|
731
|
+
renderVerdictGateStep(input.maxRounds, input.locale)
|
|
732
|
+
];
|
|
733
|
+
return [
|
|
734
|
+
"name: GPT Code Review",
|
|
735
|
+
"",
|
|
736
|
+
renderTriggerBlock(),
|
|
737
|
+
"",
|
|
738
|
+
renderConcurrencyBlock(),
|
|
739
|
+
"",
|
|
740
|
+
renderPermissionsBlock(),
|
|
741
|
+
"",
|
|
742
|
+
"jobs:",
|
|
743
|
+
" review:",
|
|
744
|
+
" runs-on: ubuntu-latest",
|
|
745
|
+
" steps:",
|
|
746
|
+
indentBlock(steps.join("\n\n"), 6),
|
|
747
|
+
""
|
|
748
|
+
].join("\n");
|
|
749
|
+
}
|
|
750
|
+
var gptProvider = {
|
|
751
|
+
id: "gpt",
|
|
752
|
+
workflowFileName: ".github/workflows/gpt-code-review.yml",
|
|
753
|
+
requiredSecretName: SECRET_NAME4,
|
|
754
|
+
buildWorkflowYaml: buildWorkflowYaml4
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// ../template-engine/dist/review/provider.js
|
|
758
|
+
var REVIEW_PROVIDERS = {
|
|
759
|
+
claude: claudeProvider,
|
|
760
|
+
gemini: geminiProvider,
|
|
761
|
+
gpt: gptProvider,
|
|
762
|
+
cursor: cursorProvider
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
// ../template-engine/dist/review/conflict.js
|
|
766
|
+
function judgeReviewFileConflict(existing, generated) {
|
|
767
|
+
if (existing === null)
|
|
768
|
+
return { kind: "create" };
|
|
769
|
+
if (existing === generated)
|
|
770
|
+
return { kind: "unchanged" };
|
|
771
|
+
return { kind: "merge-proposal", existing, generated };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ../template-engine/dist/doctor/review-checks.js
|
|
775
|
+
var CONFIG_PATH = "toolkit.config.json";
|
|
776
|
+
var DEFAULT_PROVIDER_ID = "claude";
|
|
777
|
+
function deriveAltWorkflowPath(mainWorkflowPath) {
|
|
778
|
+
return mainWorkflowPath.replace(/\.yml$/, ".archweave.yml");
|
|
779
|
+
}
|
|
780
|
+
function resolveProviderId(config) {
|
|
781
|
+
if (config.parseError || typeof config.review !== "object" || config.review === null) {
|
|
782
|
+
return DEFAULT_PROVIDER_ID;
|
|
783
|
+
}
|
|
784
|
+
const provider = config.review["provider"];
|
|
785
|
+
if (typeof provider === "string" && provider in REVIEW_PROVIDERS) {
|
|
786
|
+
return provider;
|
|
787
|
+
}
|
|
788
|
+
return DEFAULT_PROVIDER_ID;
|
|
789
|
+
}
|
|
790
|
+
var REVIEW_MODE_IDS = Object.keys(REVIEW_MODES);
|
|
791
|
+
var REVIEW_LOCALES = ["ko", "en"];
|
|
792
|
+
var REVIEW_MAX_ROUNDS = [1, 2, 3, 4, 5];
|
|
793
|
+
function readToolkitConfigState(root) {
|
|
794
|
+
const path = join2(root, CONFIG_PATH);
|
|
795
|
+
if (!existsSync2(path))
|
|
796
|
+
return { parseError: false, review: void 0 };
|
|
797
|
+
try {
|
|
798
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
799
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
800
|
+
return { parseError: true, review: void 0 };
|
|
801
|
+
}
|
|
802
|
+
return { parseError: false, review: parsed["review"] };
|
|
803
|
+
} catch {
|
|
804
|
+
return { parseError: true, review: void 0 };
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function isReviewEnabledTrue(review) {
|
|
808
|
+
return typeof review === "object" && review !== null && review["enabled"] === true;
|
|
809
|
+
}
|
|
810
|
+
function validateReviewConfig(review) {
|
|
811
|
+
if (typeof review !== "object" || review === null)
|
|
812
|
+
return null;
|
|
813
|
+
const r = review;
|
|
814
|
+
const { provider, mode, locale, maxRounds } = r;
|
|
815
|
+
if (typeof provider !== "string" || !(provider in REVIEW_PROVIDERS))
|
|
816
|
+
return null;
|
|
817
|
+
if (typeof mode !== "string" || !REVIEW_MODE_IDS.includes(mode))
|
|
818
|
+
return null;
|
|
819
|
+
if (typeof locale !== "string" || !REVIEW_LOCALES.includes(locale))
|
|
820
|
+
return null;
|
|
821
|
+
if (typeof maxRounds !== "number" || !REVIEW_MAX_ROUNDS.includes(maxRounds))
|
|
822
|
+
return null;
|
|
823
|
+
return {
|
|
824
|
+
provider,
|
|
825
|
+
mode,
|
|
826
|
+
locale,
|
|
827
|
+
maxRounds
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
var REVIEW_CHECK_IDS = [
|
|
831
|
+
"review-workflow-present",
|
|
832
|
+
"review-alt-file-coexistence",
|
|
833
|
+
"review-config-consistency"
|
|
834
|
+
];
|
|
835
|
+
function buildReviewChecks(root) {
|
|
836
|
+
const config = readToolkitConfigState(root);
|
|
837
|
+
const mainWorkflowPath = REVIEW_PROVIDERS[resolveProviderId(config)].workflowFileName;
|
|
838
|
+
const altWorkflowPath = deriveAltWorkflowPath(mainWorkflowPath);
|
|
839
|
+
const mainExists = existsSync2(join2(root, mainWorkflowPath));
|
|
840
|
+
const altExists = existsSync2(join2(root, altWorkflowPath));
|
|
841
|
+
return [
|
|
842
|
+
buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, config),
|
|
843
|
+
buildReviewAltFileCoexistenceCheck(mainWorkflowPath, altWorkflowPath, mainExists, altExists),
|
|
844
|
+
buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, config)
|
|
845
|
+
];
|
|
846
|
+
}
|
|
847
|
+
function buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, config) {
|
|
848
|
+
if (mainExists) {
|
|
849
|
+
return createDoctorCheck({
|
|
850
|
+
id: "review-workflow-present",
|
|
851
|
+
label: "Review workflow installed",
|
|
852
|
+
status: "pass",
|
|
853
|
+
message: `${mainWorkflowPath} found`,
|
|
854
|
+
evidence: [mainWorkflowPath]
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
const claimsEnabled = !config.parseError && isReviewEnabledTrue(config.review);
|
|
858
|
+
if (claimsEnabled) {
|
|
859
|
+
return createDoctorCheck({
|
|
860
|
+
id: "review-workflow-present",
|
|
861
|
+
label: "Review workflow installed",
|
|
862
|
+
status: "warning",
|
|
863
|
+
message: `${CONFIG_PATH} declares review.enabled: true, but ${mainWorkflowPath} was not found`,
|
|
864
|
+
evidence: [CONFIG_PATH],
|
|
865
|
+
suggestedAction: `Run the add-review generator to install ${mainWorkflowPath}, or remove the review.enabled claim from ${CONFIG_PATH} if review is no longer intended.`
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
return createDoctorCheck({
|
|
869
|
+
id: "review-workflow-present",
|
|
870
|
+
label: "Review workflow installed",
|
|
871
|
+
status: "not-applicable",
|
|
872
|
+
message: `${mainWorkflowPath} not found \u2014 review is not installed`,
|
|
873
|
+
evidence: []
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
function buildReviewAltFileCoexistenceCheck(mainWorkflowPath, altWorkflowPath, mainExists, altExists) {
|
|
877
|
+
if (!altExists) {
|
|
878
|
+
return createDoctorCheck({
|
|
879
|
+
id: "review-alt-file-coexistence",
|
|
880
|
+
label: "Review alternate workflow file",
|
|
881
|
+
status: "not-applicable",
|
|
882
|
+
message: `${altWorkflowPath} not found`,
|
|
883
|
+
evidence: []
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
if (mainExists) {
|
|
887
|
+
return createDoctorCheck({
|
|
888
|
+
id: "review-alt-file-coexistence",
|
|
889
|
+
label: "Review alternate workflow file",
|
|
890
|
+
status: "warning",
|
|
891
|
+
message: `Both ${mainWorkflowPath} and ${altWorkflowPath} exist`,
|
|
892
|
+
evidence: [mainWorkflowPath, altWorkflowPath],
|
|
893
|
+
suggestedAction: `Review ${altWorkflowPath} and, if it reflects the options you want, manually swap it in for ${mainWorkflowPath} \u2014 then delete the alternate file. Do not let both run side by side.`
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
return createDoctorCheck({
|
|
897
|
+
id: "review-alt-file-coexistence",
|
|
898
|
+
label: "Review alternate workflow file",
|
|
899
|
+
status: "warning",
|
|
900
|
+
message: `${altWorkflowPath} exists without ${mainWorkflowPath} \u2014 orphaned alternate file`,
|
|
901
|
+
evidence: [altWorkflowPath],
|
|
902
|
+
suggestedAction: `Review ${altWorkflowPath} and either rename it to ${mainWorkflowPath} to use it, or delete it.`
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
function buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, config) {
|
|
906
|
+
if (!mainExists) {
|
|
907
|
+
return createDoctorCheck({
|
|
908
|
+
id: "review-config-consistency",
|
|
909
|
+
label: "Review config/workflow consistency",
|
|
910
|
+
status: "not-applicable",
|
|
911
|
+
message: `${mainWorkflowPath} not found \u2014 nothing to compare against ${CONFIG_PATH}`,
|
|
912
|
+
evidence: []
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
if (config.parseError) {
|
|
916
|
+
return createDoctorCheck({
|
|
917
|
+
id: "review-config-consistency",
|
|
918
|
+
label: "Review config/workflow consistency",
|
|
919
|
+
status: "error",
|
|
920
|
+
message: `${CONFIG_PATH} could not be parsed \u2014 cannot verify it matches ${mainWorkflowPath}`,
|
|
921
|
+
evidence: [CONFIG_PATH],
|
|
922
|
+
suggestedAction: `Fix the JSON syntax in ${CONFIG_PATH} before running add-review, then re-run doctor.`
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
if (config.review === void 0) {
|
|
926
|
+
return createDoctorCheck({
|
|
927
|
+
id: "review-config-consistency",
|
|
928
|
+
label: "Review config/workflow consistency",
|
|
929
|
+
status: "not-applicable",
|
|
930
|
+
message: `${CONFIG_PATH} has no review field \u2014 nothing to compare against ${mainWorkflowPath}`,
|
|
931
|
+
evidence: []
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
const validated = validateReviewConfig(config.review);
|
|
935
|
+
if (validated === null) {
|
|
936
|
+
return createDoctorCheck({
|
|
937
|
+
id: "review-config-consistency",
|
|
938
|
+
label: "Review config/workflow consistency",
|
|
939
|
+
status: "warning",
|
|
940
|
+
message: `${CONFIG_PATH}'s review field has an unrecognized shape \u2014 cannot verify it matches ${mainWorkflowPath}`,
|
|
941
|
+
evidence: [CONFIG_PATH],
|
|
942
|
+
suggestedAction: `Fix the review field (provider/mode/locale/maxRounds) in ${CONFIG_PATH} and re-run doctor.`
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
const workflowContent = readFileSync2(join2(root, mainWorkflowPath), "utf8");
|
|
946
|
+
const generated = REVIEW_PROVIDERS[validated.provider].buildWorkflowYaml({
|
|
947
|
+
...validated,
|
|
948
|
+
// The expected workflow is rendered for the DETECTED
|
|
949
|
+
// package manager — detection is the truth, config drift is its own check.
|
|
950
|
+
packageManager: detectWorkspacePackageManager((p) => existsSync2(join2(root, p)), (p) => existsSync2(join2(root, p)) ? readFileSync2(join2(root, p), "utf-8") : void 0) ?? "pnpm"
|
|
951
|
+
});
|
|
952
|
+
const verdict = judgeReviewFileConflict(workflowContent, generated);
|
|
953
|
+
if (verdict.kind === "unchanged") {
|
|
954
|
+
return createDoctorCheck({
|
|
955
|
+
id: "review-config-consistency",
|
|
956
|
+
label: "Review config/workflow consistency",
|
|
957
|
+
status: "pass",
|
|
958
|
+
message: `${mainWorkflowPath} matches ${CONFIG_PATH}'s review options`,
|
|
959
|
+
evidence: [mainWorkflowPath, CONFIG_PATH]
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
return createDoctorCheck({
|
|
963
|
+
id: "review-config-consistency",
|
|
964
|
+
label: "Review config/workflow consistency",
|
|
965
|
+
status: "warning",
|
|
966
|
+
message: `${mainWorkflowPath} differs from what ${CONFIG_PATH}'s review options would generate`,
|
|
967
|
+
evidence: [mainWorkflowPath, CONFIG_PATH],
|
|
968
|
+
suggestedAction: `Re-run the add-review generator with --onConflict=overwrite to re-sync ${mainWorkflowPath} with ${CONFIG_PATH}, or update ${CONFIG_PATH} to match the installed file.`
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// ../template-engine/dist/doctor/backend-checks.js
|
|
973
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
974
|
+
import { join as join3 } from "node:path";
|
|
975
|
+
|
|
976
|
+
// ../template-engine/dist/backend/eslint-boundaries.js
|
|
977
|
+
var ESLINT_CONFIG_CANDIDATES = ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"];
|
|
978
|
+
var APPLIED_MARKER = "sourceTag: 'scope:frontend'";
|
|
979
|
+
var ALLOW_ALL_BLOCK = /depConstraints:\s*\[\s*\{\s*sourceTag:\s*'\*',\s*onlyDependOnLibsWithTags:\s*\[\s*'\*'\s*\],?\s*\},?\s*\]/g;
|
|
980
|
+
function maskComments(text) {
|
|
981
|
+
const out = text.split("");
|
|
982
|
+
let mode = "code";
|
|
983
|
+
let quote = "";
|
|
984
|
+
for (let i = 0; i < text.length; i++) {
|
|
985
|
+
const c = text[i];
|
|
986
|
+
const d = text[i + 1];
|
|
987
|
+
if (mode === "code") {
|
|
988
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
989
|
+
mode = "string";
|
|
990
|
+
quote = c;
|
|
991
|
+
} else if (c === "/" && d === "/") {
|
|
992
|
+
mode = "line";
|
|
993
|
+
out[i] = " ";
|
|
994
|
+
} else if (c === "/" && d === "*") {
|
|
995
|
+
mode = "block";
|
|
996
|
+
out[i] = " ";
|
|
997
|
+
}
|
|
998
|
+
} else if (mode === "string") {
|
|
999
|
+
if (c === "\\") {
|
|
1000
|
+
i++;
|
|
1001
|
+
} else if (c === quote) {
|
|
1002
|
+
mode = "code";
|
|
1003
|
+
}
|
|
1004
|
+
} else if (mode === "line") {
|
|
1005
|
+
if (c === "\n") {
|
|
1006
|
+
mode = "code";
|
|
1007
|
+
} else {
|
|
1008
|
+
out[i] = " ";
|
|
1009
|
+
}
|
|
1010
|
+
} else {
|
|
1011
|
+
if (c === "*" && d === "/") {
|
|
1012
|
+
out[i] = " ";
|
|
1013
|
+
out[i + 1] = " ";
|
|
1014
|
+
i++;
|
|
1015
|
+
mode = "code";
|
|
1016
|
+
} else if (c !== "\n") {
|
|
1017
|
+
out[i] = " ";
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return out.join("");
|
|
1022
|
+
}
|
|
1023
|
+
function findAllowAllMatches(masked) {
|
|
1024
|
+
const matches = [];
|
|
1025
|
+
ALLOW_ALL_BLOCK.lastIndex = 0;
|
|
1026
|
+
for (let m = ALLOW_ALL_BLOCK.exec(masked); m !== null; m = ALLOW_ALL_BLOCK.exec(masked)) {
|
|
1027
|
+
matches.push({ index: m.index, length: m[0].length });
|
|
1028
|
+
}
|
|
1029
|
+
return matches;
|
|
1030
|
+
}
|
|
1031
|
+
function classifyBoundaryConstraints(configText) {
|
|
1032
|
+
const masked = maskComments(configText);
|
|
1033
|
+
if (masked.includes(APPLIED_MARKER))
|
|
1034
|
+
return "enforced";
|
|
1035
|
+
if (findAllowAllMatches(masked).length > 0)
|
|
1036
|
+
return "allow-all";
|
|
1037
|
+
return "unknown";
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// ../template-engine/dist/doctor/backend-checks.js
|
|
1041
|
+
var CONFIG_PATH2 = "toolkit.config.json";
|
|
1042
|
+
var SHARED_TYPES_PACKAGE = "libs/shared-types/package.json";
|
|
1043
|
+
var SHARED_TYPES_LEGACY = "libs/shared-types/project.json";
|
|
1044
|
+
var BACKEND_CHECK_IDS = [
|
|
1045
|
+
"backend-framework",
|
|
1046
|
+
"backend-config-consistency",
|
|
1047
|
+
"shared-types-lib",
|
|
1048
|
+
"boundary-constraints-applied",
|
|
1049
|
+
"boundary-tags-exist"
|
|
1050
|
+
];
|
|
1051
|
+
var CHECK_LABELS = {
|
|
1052
|
+
"backend-framework": "Backend framework (root package.json only)",
|
|
1053
|
+
"backend-config-consistency": "Backend config consistency",
|
|
1054
|
+
"shared-types-lib": "shared-types library",
|
|
1055
|
+
"boundary-constraints-applied": "Module-boundary constraints",
|
|
1056
|
+
"boundary-tags-exist": "Boundary tags exist"
|
|
1057
|
+
};
|
|
1058
|
+
function collectScopeTags(root) {
|
|
1059
|
+
const tags = /* @__PURE__ */ new Set();
|
|
1060
|
+
const dirsIn = (rel) => {
|
|
1061
|
+
try {
|
|
1062
|
+
return readdirSync(join3(root, rel), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => `${rel}/${entry.name}`);
|
|
1063
|
+
} catch {
|
|
1064
|
+
return [];
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
const apps = dirsIn("apps");
|
|
1068
|
+
const libs = dirsIn("libs");
|
|
1069
|
+
const packages = dirsIn("packages");
|
|
1070
|
+
const roots = [
|
|
1071
|
+
...apps,
|
|
1072
|
+
...apps.flatMap((p) => dirsIn(p)),
|
|
1073
|
+
...libs,
|
|
1074
|
+
...libs.flatMap((p) => dirsIn(p)),
|
|
1075
|
+
...packages,
|
|
1076
|
+
...packages.flatMap((p) => dirsIn(p))
|
|
1077
|
+
];
|
|
1078
|
+
for (const projectRoot of roots) {
|
|
1079
|
+
for (const [file, path] of [
|
|
1080
|
+
["project.json", ["tags"]],
|
|
1081
|
+
["package.json", ["nx", "tags"]]
|
|
1082
|
+
]) {
|
|
1083
|
+
try {
|
|
1084
|
+
let node = JSON.parse(readFileSync3(join3(root, projectRoot, file), "utf-8"));
|
|
1085
|
+
for (const key of path)
|
|
1086
|
+
node = node?.[key];
|
|
1087
|
+
if (Array.isArray(node)) {
|
|
1088
|
+
for (const tag of node)
|
|
1089
|
+
if (typeof tag === "string")
|
|
1090
|
+
tags.add(tag);
|
|
1091
|
+
}
|
|
1092
|
+
} catch {
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return tags;
|
|
1097
|
+
}
|
|
1098
|
+
function notApplicable(message) {
|
|
1099
|
+
return BACKEND_CHECK_IDS.map((id) => createDoctorCheck({ id, label: CHECK_LABELS[id], status: "not-applicable", message, evidence: [] }));
|
|
1100
|
+
}
|
|
1101
|
+
function readsBackendOnly(root) {
|
|
1102
|
+
try {
|
|
1103
|
+
return JSON.parse(readFileSync3(join3(root, CONFIG_PATH2), "utf-8")).frontend === "none";
|
|
1104
|
+
} catch {
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function readConfiguredBackend(root) {
|
|
1109
|
+
const configPath = join3(root, CONFIG_PATH2);
|
|
1110
|
+
if (!existsSync3(configPath))
|
|
1111
|
+
return null;
|
|
1112
|
+
try {
|
|
1113
|
+
const parsed = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1114
|
+
const backend = parsed.backend;
|
|
1115
|
+
if (backend === void 0 || backend === "none")
|
|
1116
|
+
return null;
|
|
1117
|
+
if (backend === "express" || backend === "nest")
|
|
1118
|
+
return backend;
|
|
1119
|
+
return "invalid";
|
|
1120
|
+
} catch {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
function buildBackendChecks(root) {
|
|
1125
|
+
const backend = readConfiguredBackend(root);
|
|
1126
|
+
if (backend === null) {
|
|
1127
|
+
return notApplicable("Skipped \u2014 no backend configured in toolkit.config.json");
|
|
1128
|
+
}
|
|
1129
|
+
const checks = [];
|
|
1130
|
+
if (backend === "invalid") {
|
|
1131
|
+
checks.push(createDoctorCheck({
|
|
1132
|
+
id: "backend-framework",
|
|
1133
|
+
label: CHECK_LABELS["backend-framework"],
|
|
1134
|
+
status: "not-applicable",
|
|
1135
|
+
message: "Skipped \u2014 backend value in toolkit.config.json is not recognized",
|
|
1136
|
+
evidence: [CONFIG_PATH2]
|
|
1137
|
+
}), createDoctorCheck({
|
|
1138
|
+
id: "backend-config-consistency",
|
|
1139
|
+
label: CHECK_LABELS["backend-config-consistency"],
|
|
1140
|
+
status: "error",
|
|
1141
|
+
message: "toolkit.config.json declares an unknown backend (expected 'express', 'nest', or 'none')",
|
|
1142
|
+
evidence: [CONFIG_PATH2],
|
|
1143
|
+
suggestedAction: "Fix the backend field, then re-run doctor."
|
|
1144
|
+
}));
|
|
1145
|
+
} else {
|
|
1146
|
+
let hasDependency = false;
|
|
1147
|
+
try {
|
|
1148
|
+
const pkg = JSON.parse(readFileSync3(join3(root, "package.json"), "utf-8"));
|
|
1149
|
+
const packageName = backend === "nest" ? "@nestjs/core" : "express";
|
|
1150
|
+
hasDependency = Boolean(pkg.dependencies?.[packageName] ?? pkg.devDependencies?.[packageName]);
|
|
1151
|
+
} catch {
|
|
1152
|
+
hasDependency = false;
|
|
1153
|
+
}
|
|
1154
|
+
checks.push(createDoctorCheck({
|
|
1155
|
+
id: "backend-framework",
|
|
1156
|
+
label: CHECK_LABELS["backend-framework"],
|
|
1157
|
+
status: hasDependency ? "pass" : "warning",
|
|
1158
|
+
message: hasDependency ? `${backend} dependency detected in the root package.json` : `backend is configured as ${backend} but its dependency was not found in the root package.json`,
|
|
1159
|
+
evidence: hasDependency ? ["package.json"] : [],
|
|
1160
|
+
suggestedAction: hasDependency ? void 0 : "Run the official generator commands shown by add-backend."
|
|
1161
|
+
}), createDoctorCheck({
|
|
1162
|
+
id: "backend-config-consistency",
|
|
1163
|
+
label: CHECK_LABELS["backend-config-consistency"],
|
|
1164
|
+
status: "pass",
|
|
1165
|
+
message: `backend '${backend}' is a recognized value`,
|
|
1166
|
+
evidence: [CONFIG_PATH2]
|
|
1167
|
+
}));
|
|
1168
|
+
}
|
|
1169
|
+
const sharedTypesCurrent = existsSync3(join3(root, SHARED_TYPES_PACKAGE));
|
|
1170
|
+
const sharedTypesLegacy = !sharedTypesCurrent && existsSync3(join3(root, SHARED_TYPES_LEGACY));
|
|
1171
|
+
const sharedTypesExists = sharedTypesCurrent || sharedTypesLegacy;
|
|
1172
|
+
checks.push(createDoctorCheck({
|
|
1173
|
+
id: "shared-types-lib",
|
|
1174
|
+
label: CHECK_LABELS["shared-types-lib"],
|
|
1175
|
+
status: sharedTypesExists ? "pass" : "warning",
|
|
1176
|
+
message: sharedTypesCurrent ? "libs/shared-types present" : sharedTypesLegacy ? "libs/shared-types present (legacy project.json shape)" : "libs/shared-types not found \u2014 run add-backend to wire it",
|
|
1177
|
+
evidence: sharedTypesCurrent ? [SHARED_TYPES_PACKAGE] : sharedTypesLegacy ? [SHARED_TYPES_LEGACY] : []
|
|
1178
|
+
}));
|
|
1179
|
+
const eslintPath = ESLINT_CONFIG_CANDIDATES.find((candidate) => existsSync3(join3(root, candidate)));
|
|
1180
|
+
const classification = eslintPath ? classifyBoundaryConstraints(readFileSync3(join3(root, eslintPath), "utf-8")) : "unknown";
|
|
1181
|
+
checks.push(createDoctorCheck({
|
|
1182
|
+
id: "boundary-constraints-applied",
|
|
1183
|
+
label: CHECK_LABELS["boundary-constraints-applied"],
|
|
1184
|
+
status: classification === "enforced" ? "pass" : "warning",
|
|
1185
|
+
message: classification === "enforced" ? "scope depConstraints are enforced" : classification === "allow-all" ? "ESLint still has the allow-all default \u2014 boundaries are NOT enforced" : "could not verify the ESLint config (missing or unrecognized shape)",
|
|
1186
|
+
evidence: eslintPath ? [eslintPath] : [],
|
|
1187
|
+
suggestedAction: classification === "enforced" ? void 0 : "Run add-backend with --applyEslint, or merge its snippet yourself."
|
|
1188
|
+
}));
|
|
1189
|
+
if (classification !== "enforced") {
|
|
1190
|
+
checks.push(createDoctorCheck({
|
|
1191
|
+
id: "boundary-tags-exist",
|
|
1192
|
+
label: CHECK_LABELS["boundary-tags-exist"],
|
|
1193
|
+
status: "not-applicable",
|
|
1194
|
+
message: "Skipped \u2014 boundary constraints are not enforced (see boundary-constraints-applied)",
|
|
1195
|
+
evidence: []
|
|
1196
|
+
}));
|
|
1197
|
+
} else {
|
|
1198
|
+
const tags = collectScopeTags(root);
|
|
1199
|
+
const required = readsBackendOnly(root) ? ["scope:backend"] : ["scope:frontend", "scope:backend"];
|
|
1200
|
+
const missing = required.filter((tag) => !tags.has(tag));
|
|
1201
|
+
checks.push(createDoctorCheck({
|
|
1202
|
+
id: "boundary-tags-exist",
|
|
1203
|
+
label: CHECK_LABELS["boundary-tags-exist"],
|
|
1204
|
+
status: missing.length === 0 ? "pass" : "warning",
|
|
1205
|
+
message: missing.length === 0 ? required.length === 1 ? "scope:backend is carried by at least one project (backend-only workspace \u2014 no frontend to tag)" : "both scope:frontend and scope:backend are carried by at least one project" : `the toolkit's enforced deny rules reference ${missing.join(" and ")}, but no project in the scanned directories (apps/, libs/, packages/) carries ${missing.length === 1 ? "it" : "them"} \u2014 the boundary is a dead letter`,
|
|
1206
|
+
evidence: missing.length === 0 ? [] : missing,
|
|
1207
|
+
suggestedAction: missing.length === 0 ? void 0 : "Add the missing scope tag(s) to the app project.json tags or package.json nx.tags (add-backend does this for the two apps)."
|
|
1208
|
+
}));
|
|
1209
|
+
}
|
|
1210
|
+
return checks;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// ../template-engine/dist/doctor/fsd-like-check.js
|
|
1214
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
|
|
1215
|
+
import { join as join4 } from "node:path";
|
|
1216
|
+
var FSD_LAYERS = ["app", "_app", "pages", "_pages", "widgets", "features", "entities", "shared"];
|
|
1217
|
+
function buildFsdLikeCheck(root) {
|
|
1218
|
+
const scanRoots = ["src"];
|
|
1219
|
+
try {
|
|
1220
|
+
for (const entry of readdirSync2(join4(root, "apps"), { withFileTypes: true })) {
|
|
1221
|
+
if (entry.isDirectory())
|
|
1222
|
+
scanRoots.push(`apps/${entry.name}/src`);
|
|
1223
|
+
}
|
|
1224
|
+
} catch {
|
|
1225
|
+
}
|
|
1226
|
+
const found = [];
|
|
1227
|
+
for (const scanRoot of scanRoots) {
|
|
1228
|
+
for (const layer of FSD_LAYERS) {
|
|
1229
|
+
if (existsSync4(join4(root, scanRoot, layer)))
|
|
1230
|
+
found.push(`${scanRoot}/${layer}`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (existsSync4(join4(root, "toolkit.config.json"))) {
|
|
1234
|
+
return createDoctorCheck({
|
|
1235
|
+
id: "fsd-like-structure",
|
|
1236
|
+
label: "FSD-like structure",
|
|
1237
|
+
status: "pass",
|
|
1238
|
+
message: found.length > 0 ? "FSD-like layers found, but this workspace is already managed by the toolkit" : "no FSD-like layers found; workspace is toolkit-managed",
|
|
1239
|
+
evidence: found
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
if (found.length >= 2) {
|
|
1243
|
+
return createDoctorCheck({
|
|
1244
|
+
id: "fsd-like-structure",
|
|
1245
|
+
label: "FSD-like structure",
|
|
1246
|
+
status: "warning",
|
|
1247
|
+
message: `an existing FSD-like layout was found (${found.length} layer directories) \u2014 init's FSD section may overlap it`,
|
|
1248
|
+
evidence: found,
|
|
1249
|
+
suggestedAction: "Review the existing structure before running init, or run init with --fsd=false."
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
return createDoctorCheck({
|
|
1253
|
+
id: "fsd-like-structure",
|
|
1254
|
+
label: "FSD-like structure",
|
|
1255
|
+
status: "pass",
|
|
1256
|
+
message: "no collision signal \u2014 fewer than two FSD layer directories found",
|
|
1257
|
+
evidence: found
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// ../template-engine/dist/doctor/fsd-enforcement-check.js
|
|
1262
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
1263
|
+
import { join as join5 } from "node:path";
|
|
1264
|
+
|
|
1265
|
+
// ../templates/dist/fsd/fsd-eslint.js
|
|
1266
|
+
var FSD_ESLINT_MODULE = "@clearlydev/nx-archweave/eslint";
|
|
1267
|
+
|
|
1268
|
+
// ../template-engine/dist/fsd/fsd-eslint-config.js
|
|
1269
|
+
function classifyFsdEslintConfig(configText) {
|
|
1270
|
+
return maskComments(configText).includes(FSD_ESLINT_MODULE) ? "applied" : "absent";
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// ../template-engine/dist/doctor/fsd-enforcement-check.js
|
|
1274
|
+
function fsdEnabledPerConfig(root) {
|
|
1275
|
+
const path = join5(root, "toolkit.config.json");
|
|
1276
|
+
if (!existsSync5(path))
|
|
1277
|
+
return null;
|
|
1278
|
+
try {
|
|
1279
|
+
const config = JSON.parse(readFileSync4(path, "utf-8"));
|
|
1280
|
+
const fsd = config["fsd"];
|
|
1281
|
+
if (typeof fsd === "object" && fsd !== null && typeof fsd["enabled"] === "boolean") {
|
|
1282
|
+
return fsd["enabled"];
|
|
1283
|
+
}
|
|
1284
|
+
return typeof config["frontend"] === "string" && config["frontend"] !== "none";
|
|
1285
|
+
} catch {
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
function eslintConfigPaths(root) {
|
|
1290
|
+
const dirsIn = (rel) => {
|
|
1291
|
+
try {
|
|
1292
|
+
return readdirSync3(join5(root, rel), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => rel === "" ? entry.name : `${rel}/${entry.name}`);
|
|
1293
|
+
} catch {
|
|
1294
|
+
return [];
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
const apps = dirsIn("apps");
|
|
1298
|
+
const dirs = ["", ...apps, ...apps.flatMap((app) => dirsIn(app))];
|
|
1299
|
+
return dirs.flatMap((dir) => ESLINT_CONFIG_CANDIDATES.map((name) => dir === "" ? name : `${dir}/${name}`)).filter((rel) => existsSync5(join5(root, rel)));
|
|
1300
|
+
}
|
|
1301
|
+
function buildFsdEnforcementCheck(root) {
|
|
1302
|
+
const id = "fsd-enforcement-applied";
|
|
1303
|
+
const label = "FSD layer rule applied";
|
|
1304
|
+
const enabled = fsdEnabledPerConfig(root);
|
|
1305
|
+
if (enabled !== true) {
|
|
1306
|
+
return createDoctorCheck({
|
|
1307
|
+
id,
|
|
1308
|
+
label,
|
|
1309
|
+
status: "pass",
|
|
1310
|
+
message: enabled === null ? "not a toolkit-managed workspace \u2014 nothing to verify" : "FSD is off for this workspace",
|
|
1311
|
+
evidence: []
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
const applied = eslintConfigPaths(root).filter((rel) => classifyFsdEslintConfig(readFileSync4(join5(root, rel), "utf-8")) === "applied");
|
|
1315
|
+
if (applied.length > 0) {
|
|
1316
|
+
return createDoctorCheck({ id, label, status: "pass", message: "the archweave/fsd-boundaries rule is wired into an ESLint config", evidence: applied });
|
|
1317
|
+
}
|
|
1318
|
+
return createDoctorCheck({
|
|
1319
|
+
id,
|
|
1320
|
+
label,
|
|
1321
|
+
status: "warning",
|
|
1322
|
+
message: "no ESLint config imports @clearlydev/nx-archweave/eslint \u2014 FSD layer rules are NOT enforced",
|
|
1323
|
+
evidence: [],
|
|
1324
|
+
suggestedAction: "Run add-fsd-lite --applyEslint (or paste the snippet it prints into the frontend app's eslint.config.mjs)."
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// ../template-engine/dist/doctor/run-doctor-checks.js
|
|
1329
|
+
var NOT_APPLICABLE_IDS = [
|
|
1330
|
+
"fsd-like-structure",
|
|
1331
|
+
"fsd-enforcement-applied",
|
|
1332
|
+
"nx-version",
|
|
1333
|
+
"package-manager",
|
|
1334
|
+
"workspace-root",
|
|
1335
|
+
"framework-react-vite",
|
|
1336
|
+
"framework-next",
|
|
1337
|
+
"existing-config",
|
|
1338
|
+
"agent-rules-files",
|
|
1339
|
+
"github-templates",
|
|
1340
|
+
"integration-manifest",
|
|
1341
|
+
...REVIEW_CHECK_IDS,
|
|
1342
|
+
...BACKEND_CHECK_IDS
|
|
1343
|
+
];
|
|
1344
|
+
function runDoctorChecks(root) {
|
|
1345
|
+
const env = detectEnvironment(root);
|
|
1346
|
+
if (!env.hasNxWorkspace) {
|
|
1347
|
+
const checks2 = [
|
|
1348
|
+
createDoctorCheck({
|
|
1349
|
+
id: "nx-workspace-detected",
|
|
1350
|
+
label: "Nx workspace detected",
|
|
1351
|
+
status: "error",
|
|
1352
|
+
message: "nx.json not found \u2014 this is not an Nx workspace.",
|
|
1353
|
+
evidence: [],
|
|
1354
|
+
suggestedAction: "Run this command from the root of an Nx workspace."
|
|
1355
|
+
}),
|
|
1356
|
+
...NOT_APPLICABLE_IDS.map((id) => createDoctorCheck({
|
|
1357
|
+
id,
|
|
1358
|
+
label: id,
|
|
1359
|
+
status: "not-applicable",
|
|
1360
|
+
message: "Skipped \u2014 not an Nx workspace",
|
|
1361
|
+
evidence: []
|
|
1362
|
+
}))
|
|
1363
|
+
];
|
|
1364
|
+
return { checks: checks2, overallStatus: "unsupported" };
|
|
1365
|
+
}
|
|
1366
|
+
const checks = [
|
|
1367
|
+
createDoctorCheck({
|
|
1368
|
+
id: "nx-workspace-detected",
|
|
1369
|
+
label: "Nx workspace detected",
|
|
1370
|
+
status: "pass",
|
|
1371
|
+
message: "nx.json found",
|
|
1372
|
+
evidence: ["nx.json"]
|
|
1373
|
+
}),
|
|
1374
|
+
buildNxVersionCheck(env),
|
|
1375
|
+
buildFsdLikeCheck(root),
|
|
1376
|
+
buildFsdEnforcementCheck(root),
|
|
1377
|
+
createDoctorCheck({
|
|
1378
|
+
id: "package-manager",
|
|
1379
|
+
label: "Package manager",
|
|
1380
|
+
status: env.packageManager ? "pass" : "warning",
|
|
1381
|
+
message: env.packageManager ? `${env.packageManager} detected` : "Unknown package manager",
|
|
1382
|
+
evidence: env.packageManager ? [env.packageManager] : []
|
|
1383
|
+
}),
|
|
1384
|
+
createDoctorCheck({
|
|
1385
|
+
id: "workspace-root",
|
|
1386
|
+
label: "Workspace root",
|
|
1387
|
+
status: "pass",
|
|
1388
|
+
message: root,
|
|
1389
|
+
evidence: [root]
|
|
1390
|
+
}),
|
|
1391
|
+
...buildFrameworkChecks(env),
|
|
1392
|
+
buildExistingConfigCheck(root),
|
|
1393
|
+
buildAgentRulesCheck(root),
|
|
1394
|
+
buildGithubTemplatesCheck(root),
|
|
1395
|
+
buildIntegrationManifestCheck(root),
|
|
1396
|
+
...buildReviewChecks(root),
|
|
1397
|
+
...buildBackendChecks(root)
|
|
1398
|
+
];
|
|
1399
|
+
const overallStatus = computeOverallStatus(checks);
|
|
1400
|
+
return { checks, overallStatus };
|
|
1401
|
+
}
|
|
1402
|
+
var VALIDATED_NX_MAJOR = 23;
|
|
1403
|
+
function extractMajor(version) {
|
|
1404
|
+
const match = /(\d+)/.exec(version);
|
|
1405
|
+
return match ? Number(match[1]) : null;
|
|
1406
|
+
}
|
|
1407
|
+
function buildNxVersionCheck(env) {
|
|
1408
|
+
if (env.rootPackageJsonError) {
|
|
1409
|
+
return createDoctorCheck({
|
|
1410
|
+
id: "nx-version",
|
|
1411
|
+
label: "Nx version",
|
|
1412
|
+
status: "error",
|
|
1413
|
+
message: `Could not read root package.json: ${env.rootPackageJsonError}`,
|
|
1414
|
+
evidence: ["package.json"],
|
|
1415
|
+
suggestedAction: "Fix the JSON syntax in the root package.json and re-run doctor."
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
if (!env.nxVersion) {
|
|
1419
|
+
return createDoctorCheck({
|
|
1420
|
+
id: "nx-version",
|
|
1421
|
+
label: "Nx version",
|
|
1422
|
+
status: "warning",
|
|
1423
|
+
message: "Could not determine Nx version",
|
|
1424
|
+
evidence: []
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
const major = extractMajor(env.nxVersion);
|
|
1428
|
+
if (major === null) {
|
|
1429
|
+
return createDoctorCheck({
|
|
1430
|
+
id: "nx-version",
|
|
1431
|
+
label: "Nx version",
|
|
1432
|
+
status: "warning",
|
|
1433
|
+
message: `Nx version "${env.nxVersion}" could not be parsed \u2014 range judgment skipped`,
|
|
1434
|
+
evidence: [`nx@${env.nxVersion}`]
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
if (major === VALIDATED_NX_MAJOR) {
|
|
1438
|
+
return createDoctorCheck({
|
|
1439
|
+
id: "nx-version",
|
|
1440
|
+
label: "Nx version",
|
|
1441
|
+
status: "pass",
|
|
1442
|
+
message: `Nx ${env.nxVersion} \u2014 within the toolkit-validated range (${VALIDATED_NX_MAJOR}.x)`,
|
|
1443
|
+
evidence: [`nx@${env.nxVersion}`]
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
if (major < VALIDATED_NX_MAJOR) {
|
|
1447
|
+
return createDoctorCheck({
|
|
1448
|
+
id: "nx-version",
|
|
1449
|
+
label: "Nx version",
|
|
1450
|
+
status: "warning",
|
|
1451
|
+
message: `Nx ${env.nxVersion} is below the toolkit-validated range (${VALIDATED_NX_MAJOR}.x) \u2014 generator behavior is not guaranteed`,
|
|
1452
|
+
evidence: [`nx@${env.nxVersion}`],
|
|
1453
|
+
suggestedAction: `Upgrade the workspace to Nx ${VALIDATED_NX_MAJOR}.x before running the generators.`
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
return createDoctorCheck({
|
|
1457
|
+
id: "nx-version",
|
|
1458
|
+
label: "Nx version",
|
|
1459
|
+
status: "warning",
|
|
1460
|
+
message: `Nx ${env.nxVersion} is above the toolkit-validated range (${VALIDATED_NX_MAJOR}.x) \u2014 may work, unvalidated`,
|
|
1461
|
+
evidence: [`nx@${env.nxVersion}`],
|
|
1462
|
+
suggestedAction: "Proceed with care and report issues; validation for newer majors lands with toolkit updates."
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
function buildFrameworkChecks(env) {
|
|
1466
|
+
if (env.rootPackageJsonError) {
|
|
1467
|
+
const message = `Could not read root package.json: ${env.rootPackageJsonError}`;
|
|
1468
|
+
return [
|
|
1469
|
+
createDoctorCheck({
|
|
1470
|
+
id: "framework-react-vite",
|
|
1471
|
+
label: "React + Vite (root package.json only)",
|
|
1472
|
+
status: "error",
|
|
1473
|
+
message,
|
|
1474
|
+
evidence: ["package.json"]
|
|
1475
|
+
}),
|
|
1476
|
+
createDoctorCheck({
|
|
1477
|
+
id: "framework-next",
|
|
1478
|
+
label: "Next.js (root package.json only)",
|
|
1479
|
+
status: "error",
|
|
1480
|
+
message,
|
|
1481
|
+
evidence: ["package.json"]
|
|
1482
|
+
})
|
|
1483
|
+
];
|
|
1484
|
+
}
|
|
1485
|
+
const dependencies = env.rootPackageJson?.["dependencies"] ?? {};
|
|
1486
|
+
const devDependencies = env.rootPackageJson?.["devDependencies"] ?? {};
|
|
1487
|
+
const allDeps = { ...dependencies, ...devDependencies };
|
|
1488
|
+
const hasReactVite = Boolean(allDeps.vite && allDeps.react);
|
|
1489
|
+
const hasNext = Boolean(allDeps.next);
|
|
1490
|
+
return [
|
|
1491
|
+
createDoctorCheck({
|
|
1492
|
+
id: "framework-react-vite",
|
|
1493
|
+
label: "React + Vite (root package.json only)",
|
|
1494
|
+
status: hasReactVite ? "pass" : "not-applicable",
|
|
1495
|
+
message: hasReactVite ? "react + vite dependencies detected in the root package.json" : "Not detected in the root package.json (sub-packages are not scanned)",
|
|
1496
|
+
evidence: hasReactVite ? ["package.json"] : []
|
|
1497
|
+
}),
|
|
1498
|
+
createDoctorCheck({
|
|
1499
|
+
id: "framework-next",
|
|
1500
|
+
label: "Next.js (root package.json only)",
|
|
1501
|
+
status: hasNext ? "pass" : "not-applicable",
|
|
1502
|
+
message: hasNext ? "next dependency detected in the root package.json" : "Not detected in the root package.json (sub-packages are not scanned)",
|
|
1503
|
+
evidence: hasNext ? ["package.json"] : []
|
|
1504
|
+
})
|
|
1505
|
+
];
|
|
1506
|
+
}
|
|
1507
|
+
function isToolkitManaged(root) {
|
|
1508
|
+
return existsSync6(join6(root, "toolkit.config.json"));
|
|
1509
|
+
}
|
|
1510
|
+
function buildExistingConfigCheck(root) {
|
|
1511
|
+
const exists = isToolkitManaged(root);
|
|
1512
|
+
return createDoctorCheck({
|
|
1513
|
+
id: "existing-config",
|
|
1514
|
+
label: "toolkit.config.json",
|
|
1515
|
+
status: "pass",
|
|
1516
|
+
message: exists ? "Found \u2014 the workspace is managed by nx-archweave; init and the generators merge into it, changing only their own fields" : "Not found \u2014 init creates it",
|
|
1517
|
+
evidence: exists ? ["toolkit.config.json"] : []
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
function buildIntegrationManifestCheck(root) {
|
|
1521
|
+
const exists = existsSync6(join6(root, "archweave-integrations.json"));
|
|
1522
|
+
return createDoctorCheck({
|
|
1523
|
+
id: "integration-manifest",
|
|
1524
|
+
label: "Integration manifest",
|
|
1525
|
+
status: "pass",
|
|
1526
|
+
message: exists ? "Found \u2014 nx-archweave's record of installed integrations (checked against what is installed below)" : "Not found \u2014 written when an integration is installed",
|
|
1527
|
+
evidence: exists ? ["archweave-integrations.json"] : []
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
function buildAgentRulesCheck(root) {
|
|
1531
|
+
const claudeExists = existsSync6(join6(root, "CLAUDE.md"));
|
|
1532
|
+
const agentsExists = existsSync6(join6(root, "AGENTS.md"));
|
|
1533
|
+
const exists = claudeExists || agentsExists;
|
|
1534
|
+
const evidence = [claudeExists && "CLAUDE.md", agentsExists && "AGENTS.md"].filter(Boolean);
|
|
1535
|
+
return createDoctorCheck({
|
|
1536
|
+
id: "agent-rules-files",
|
|
1537
|
+
label: "CLAUDE.md / AGENTS.md",
|
|
1538
|
+
status: "pass",
|
|
1539
|
+
message: exists ? "Existing agent rules file(s) found \u2014 nx-archweave adds or updates only its own marked block" : "Not found \u2014 safe to create",
|
|
1540
|
+
evidence
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
function buildGithubTemplatesCheck(root) {
|
|
1544
|
+
const issueTemplateExists = existsSync6(join6(root, ".github", "ISSUE_TEMPLATE"));
|
|
1545
|
+
const prTemplateExists = existsSync6(join6(root, ".github", "PULL_REQUEST_TEMPLATE.md"));
|
|
1546
|
+
const exists = issueTemplateExists || prTemplateExists;
|
|
1547
|
+
const managed = isToolkitManaged(root);
|
|
1548
|
+
return createDoctorCheck({
|
|
1549
|
+
id: "github-templates",
|
|
1550
|
+
label: "GitHub Issue/PR templates",
|
|
1551
|
+
status: exists && !managed ? "warning" : "pass",
|
|
1552
|
+
message: !exists ? "Not found \u2014 safe to create" : managed ? "Found \u2014 kept as they are (add-github-templates never overwrites a template); the workspace is managed by nx-archweave" : "Existing template(s) found \u2014 add-github-templates keeps them as they are (it never overwrites a template)",
|
|
1553
|
+
evidence: [issueTemplateExists && ".github/ISSUE_TEMPLATE", prTemplateExists && ".github/PULL_REQUEST_TEMPLATE.md"].filter(Boolean)
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// ../integrations/dist/registry/integration-registry.js
|
|
1558
|
+
function createIntegrationRegistry(integrations) {
|
|
1559
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1560
|
+
for (const integration of integrations) {
|
|
1561
|
+
if (seenIds.has(integration.id)) {
|
|
1562
|
+
throw new Error(`Duplicate integration id: "${integration.id}"`);
|
|
1563
|
+
}
|
|
1564
|
+
seenIds.add(integration.id);
|
|
1565
|
+
}
|
|
1566
|
+
return integrations;
|
|
1567
|
+
}
|
|
1568
|
+
function listIntegrations(registry) {
|
|
1569
|
+
return registry;
|
|
1570
|
+
}
|
|
1571
|
+
function getIntegration(registry, id) {
|
|
1572
|
+
return registry.find((integration) => integration.id === id);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// ../integrations/dist/manifest/manifest.js
|
|
1576
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1577
|
+
import { join as join7 } from "node:path";
|
|
1578
|
+
var MANIFEST_FILENAME = "archweave-integrations.json";
|
|
1579
|
+
function createEmptyManifest() {
|
|
1580
|
+
return { integrations: {} };
|
|
1581
|
+
}
|
|
1582
|
+
function loadManifest(root) {
|
|
1583
|
+
const manifestPath = join7(root, MANIFEST_FILENAME);
|
|
1584
|
+
if (!existsSync7(manifestPath)) {
|
|
1585
|
+
return createEmptyManifest();
|
|
1586
|
+
}
|
|
1587
|
+
try {
|
|
1588
|
+
const parsed = JSON.parse(readFileSync5(manifestPath, "utf8"));
|
|
1589
|
+
if (parsed && typeof parsed === "object" && typeof parsed.integrations === "object") {
|
|
1590
|
+
return { integrations: parsed.integrations ?? {} };
|
|
1591
|
+
}
|
|
1592
|
+
return createEmptyManifest();
|
|
1593
|
+
} catch {
|
|
1594
|
+
return createEmptyManifest();
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// ../integrations/dist/doctor/detect-drift.js
|
|
1599
|
+
function classifyIntegrationDrift(input) {
|
|
1600
|
+
const { id, declared, manifestEntry, detected } = input;
|
|
1601
|
+
const nxG = `${input.runNx ?? "pnpm nx"} g`;
|
|
1602
|
+
if (!declared && !manifestEntry && !detected.installed) {
|
|
1603
|
+
return {
|
|
1604
|
+
id,
|
|
1605
|
+
status: "not-installed",
|
|
1606
|
+
reason: "Not declared in ToolkitConfig.integrations.enabled, no manifest entry, and not detected in this workspace."
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
if (!declared) {
|
|
1610
|
+
if (manifestEntry) {
|
|
1611
|
+
return {
|
|
1612
|
+
id,
|
|
1613
|
+
status: "undeclared-leftover",
|
|
1614
|
+
reason: "The configuration no longer declares this integration, but its install record remains.",
|
|
1615
|
+
suggestedAction: "Either re-declare it in ToolkitConfig.integrations.enabled or clean up the leftover installation and manifest entry.",
|
|
1616
|
+
source: manifestEntry.source,
|
|
1617
|
+
verification: manifestEntry.verification
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
return {
|
|
1621
|
+
id,
|
|
1622
|
+
status: "externally-installed",
|
|
1623
|
+
reason: "Detected live, but neither declared in ToolkitConfig.integrations.enabled nor recorded in the manifest \u2014 installed outside nx-archweave.",
|
|
1624
|
+
suggestedAction: "Adopt it by declaring it in ToolkitConfig.integrations.enabled, or leave it unmanaged (doctor will keep noting it)."
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
if (manifestEntry && manifestEntry.verification === "passed") {
|
|
1628
|
+
return {
|
|
1629
|
+
id,
|
|
1630
|
+
status: "configured",
|
|
1631
|
+
reason: "Declared and installed; the last verification passed.",
|
|
1632
|
+
source: manifestEntry.source,
|
|
1633
|
+
verification: "passed"
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
if (manifestEntry) {
|
|
1637
|
+
return {
|
|
1638
|
+
id,
|
|
1639
|
+
status: "verification-failed",
|
|
1640
|
+
reason: `Declared and an install record exists, but the last verification did not pass (status: ${manifestEntry.verification}).`,
|
|
1641
|
+
suggestedAction: "Inspect the tool installation and re-run its verification (or reinstall the integration).",
|
|
1642
|
+
source: manifestEntry.source,
|
|
1643
|
+
verification: manifestEntry.verification
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
if (detected.installed) {
|
|
1647
|
+
return {
|
|
1648
|
+
id,
|
|
1649
|
+
status: "declared-missing",
|
|
1650
|
+
reason: "Declared and detected as installed, but no install record exists \u2014 the installation happened outside nx-archweave bookkeeping.",
|
|
1651
|
+
suggestedAction: `Adopt the existing installation into the manifest: ${nxG} @clearlydev/nx-archweave:record-integration --tool=${id} --apply`
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
id,
|
|
1656
|
+
status: "declared-missing",
|
|
1657
|
+
reason: "Declared in ToolkitConfig.integrations.enabled, but no install record exists \u2014 the installation never happened or was removed.",
|
|
1658
|
+
suggestedAction: input.installable ? `Install it: ${nxG} @clearlydev/nx-archweave:install-integration --tool=${id} --apply \u2014 or remove the declaration if it is no longer wanted.` : "Re-install the integration, or remove the declaration if it is no longer wanted (nx-archweave has no installer for this id \u2014 check it for a typo)."
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
async function detectIntegrationsDrift(input) {
|
|
1662
|
+
const manifest = loadManifest(input.workspaceRoot);
|
|
1663
|
+
const ids = /* @__PURE__ */ new Set([
|
|
1664
|
+
...input.declaredIds,
|
|
1665
|
+
...Object.keys(manifest.integrations),
|
|
1666
|
+
...input.integrations.map((integration) => integration.id)
|
|
1667
|
+
]);
|
|
1668
|
+
const context = {
|
|
1669
|
+
workspaceRoot: input.workspaceRoot,
|
|
1670
|
+
targetEnvironment: input.targetEnvironment
|
|
1671
|
+
};
|
|
1672
|
+
const results = [];
|
|
1673
|
+
for (const id of ids) {
|
|
1674
|
+
const integration = input.integrations.find((candidate) => candidate.id === id);
|
|
1675
|
+
const detected = integration ? await integration.detect(context) : { installed: false, managedBy: "unknown", evidence: [] };
|
|
1676
|
+
results.push(classifyIntegrationDrift({
|
|
1677
|
+
id,
|
|
1678
|
+
installable: integration !== void 0,
|
|
1679
|
+
declared: input.declaredIds.includes(id),
|
|
1680
|
+
manifestEntry: manifest.integrations[id],
|
|
1681
|
+
detected,
|
|
1682
|
+
...input.runNx === void 0 ? {} : { runNx: input.runNx }
|
|
1683
|
+
}));
|
|
1684
|
+
}
|
|
1685
|
+
return results;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// ../integrations/dist/doctor/integration-doctor-checks.js
|
|
1689
|
+
function buildIntegrationDoctorChecks(results) {
|
|
1690
|
+
return results.map((result) => {
|
|
1691
|
+
const evidence = [
|
|
1692
|
+
result.source && `manifest source: ${result.source}`,
|
|
1693
|
+
result.verification && `verification: ${result.verification}`
|
|
1694
|
+
].filter(Boolean);
|
|
1695
|
+
const status = (() => {
|
|
1696
|
+
switch (result.status) {
|
|
1697
|
+
case "not-installed":
|
|
1698
|
+
return "not-applicable";
|
|
1699
|
+
case "declared-missing":
|
|
1700
|
+
case "undeclared-leftover":
|
|
1701
|
+
case "externally-installed":
|
|
1702
|
+
return "warning";
|
|
1703
|
+
case "verification-failed":
|
|
1704
|
+
return "error";
|
|
1705
|
+
case "configured":
|
|
1706
|
+
return "pass";
|
|
1707
|
+
default:
|
|
1708
|
+
return result.status;
|
|
1709
|
+
}
|
|
1710
|
+
})();
|
|
1711
|
+
return createDoctorCheck({
|
|
1712
|
+
id: `integration-${result.id}`,
|
|
1713
|
+
label: `Integration: ${result.id}`,
|
|
1714
|
+
status,
|
|
1715
|
+
message: result.reason,
|
|
1716
|
+
evidence,
|
|
1717
|
+
suggestedAction: result.suggestedAction
|
|
1718
|
+
});
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// ../integrations/dist/config/read-enabled-integrations.js
|
|
1723
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
1724
|
+
import { join as join8 } from "node:path";
|
|
1725
|
+
var CONFIG_FILENAME = "toolkit.config.json";
|
|
1726
|
+
function readEnabledIntegrationIds(root) {
|
|
1727
|
+
const configPath = join8(root, CONFIG_FILENAME);
|
|
1728
|
+
if (!existsSync8(configPath)) {
|
|
1729
|
+
return [];
|
|
1730
|
+
}
|
|
1731
|
+
try {
|
|
1732
|
+
const parsed = JSON.parse(readFileSync6(configPath, "utf8"));
|
|
1733
|
+
const enabled = parsed?.integrations?.enabled;
|
|
1734
|
+
return Array.isArray(enabled) && enabled.every((id) => typeof id === "string") ? enabled : [];
|
|
1735
|
+
} catch {
|
|
1736
|
+
return [];
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// ../integrations/dist/process/read-only-command-runner.js
|
|
1741
|
+
var CLAUDE_CLI_TIMEOUT_MS = 5e3;
|
|
1742
|
+
var CLAUDE_CLI_STDOUT_LIMIT_BYTES = 1048576;
|
|
1743
|
+
var CLAUDE_CLI_STDERR_LIMIT_BYTES = 65536;
|
|
1744
|
+
|
|
1745
|
+
// ../integrations/dist/catalog/plugin-catalog.js
|
|
1746
|
+
var PLUGIN_HOSTS = ["claude-code"];
|
|
1747
|
+
var PLUGIN_CATALOG = [
|
|
1748
|
+
{
|
|
1749
|
+
// Researched 2026-07-15. The claude-code values are the ones the manifest and verifier
|
|
1750
|
+
// were measured with. ponytail documents official installs for ~20 hosts; only claude-code
|
|
1751
|
+
// has an adapter.
|
|
1752
|
+
id: "ponytail",
|
|
1753
|
+
displayName: "ponytail",
|
|
1754
|
+
// The project's own tagline.
|
|
1755
|
+
description: 'over-engineering review rules and skills \u2014 "makes your AI agent think like the laziest senior dev in the room"',
|
|
1756
|
+
homepage: "https://ponytail.dev",
|
|
1757
|
+
repository: "https://github.com/DietrichGebert/ponytail",
|
|
1758
|
+
license: "MIT",
|
|
1759
|
+
npmPackage: "@dietrichgebert/ponytail",
|
|
1760
|
+
installDocs: "https://github.com/DietrichGebert/ponytail#install",
|
|
1761
|
+
uninstallSteps: [
|
|
1762
|
+
"Run this from a clone of https://github.com/DietrichGebert/ponytail (or before removing the plugin below \u2014 the script is itself a plugin file, so removing the plugin first deletes it): node scripts/uninstall.js",
|
|
1763
|
+
"Then remove the plugin via your host's own command, e.g. in Claude Code: /plugin uninstall ponytail@ponytail (or use the Claude Code Desktop app: Customize > manage marketplaces > remove plugin).",
|
|
1764
|
+
"See https://github.com/DietrichGebert/ponytail#uninstall for the full, current official instructions."
|
|
1765
|
+
],
|
|
1766
|
+
hosts: {
|
|
1767
|
+
"claude-code": { name: "ponytail", marketplace: "DietrichGebert/ponytail", pluginId: "ponytail@ponytail" }
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
];
|
|
1771
|
+
var PLUGIN_IDS = PLUGIN_CATALOG.map((entry) => entry.id);
|
|
1772
|
+
|
|
1773
|
+
// ../integrations/dist/catalog/resolve-version.js
|
|
1774
|
+
import { get } from "node:https";
|
|
1775
|
+
function resolveVersionFromNpmRegistry(npmPackage) {
|
|
1776
|
+
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(npmPackage)}`;
|
|
1777
|
+
return new Promise((resolve2, reject) => {
|
|
1778
|
+
get(registryUrl, (response) => {
|
|
1779
|
+
let body = "";
|
|
1780
|
+
response.on("data", (chunk) => {
|
|
1781
|
+
body += chunk;
|
|
1782
|
+
});
|
|
1783
|
+
response.on("end", () => {
|
|
1784
|
+
try {
|
|
1785
|
+
const parsed = JSON.parse(body);
|
|
1786
|
+
const latest = parsed?.["dist-tags"]?.["latest"];
|
|
1787
|
+
if (typeof latest !== "string") {
|
|
1788
|
+
reject(new Error(`Could not resolve the latest version of ${npmPackage} from the npm registry.`));
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
resolve2(latest);
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
}).on("error", reject);
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// ../integrations/dist/hosts/claude-code/detect.js
|
|
1801
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
1802
|
+
import { join as join9 } from "node:path";
|
|
1803
|
+
async function detectWorkspacePackage(entry, context) {
|
|
1804
|
+
const npmPackage = entry.npmPackage;
|
|
1805
|
+
if (npmPackage === void 0) {
|
|
1806
|
+
return { installed: false, managedBy: "unknown", evidence: [] };
|
|
1807
|
+
}
|
|
1808
|
+
const packageJsonPath = join9(context.workspaceRoot, "package.json");
|
|
1809
|
+
if (!existsSync9(packageJsonPath)) {
|
|
1810
|
+
return { installed: false, managedBy: "unknown", evidence: [] };
|
|
1811
|
+
}
|
|
1812
|
+
let pkg;
|
|
1813
|
+
try {
|
|
1814
|
+
pkg = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
|
|
1815
|
+
} catch {
|
|
1816
|
+
return { installed: false, managedBy: "unknown", evidence: [] };
|
|
1817
|
+
}
|
|
1818
|
+
const deps = pkg["dependencies"] ?? {};
|
|
1819
|
+
const devDeps = pkg["devDependencies"] ?? {};
|
|
1820
|
+
const version = deps[npmPackage] ?? devDeps[npmPackage];
|
|
1821
|
+
if (!version) {
|
|
1822
|
+
return { installed: false, managedBy: "unknown", evidence: [] };
|
|
1823
|
+
}
|
|
1824
|
+
return {
|
|
1825
|
+
installed: true,
|
|
1826
|
+
installedVersion: version,
|
|
1827
|
+
managedBy: "external-tool",
|
|
1828
|
+
evidence: [`package.json: ${npmPackage}@${version}`]
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// ../integrations/dist/hosts/claude-code/plan.js
|
|
1833
|
+
var VERSION_DECIDED_BY_MARKETPLACE = "latest (decided by the marketplace)";
|
|
1834
|
+
var WORKSPACE_FILES_BY_SCOPE = {
|
|
1835
|
+
user: [],
|
|
1836
|
+
project: [".claude/settings.json"],
|
|
1837
|
+
local: [".claude/settings.local.json"]
|
|
1838
|
+
};
|
|
1839
|
+
var DEFAULT_PLUGIN_SCOPE = "project";
|
|
1840
|
+
function createClaudeMarketplacePlan(entry, deps) {
|
|
1841
|
+
return async (options) => {
|
|
1842
|
+
const distribution = entry.hosts["claude-code"];
|
|
1843
|
+
if (options.targetEnvironment !== "claude-code" || distribution === void 0) {
|
|
1844
|
+
const supported = Object.entries(entry.hosts).filter(([, value]) => value !== void 0).map(([host]) => host).join(", ");
|
|
1845
|
+
throw new Error(`Target environment "${options.targetEnvironment}" is not supported by this integration. Supported: ${supported}.`);
|
|
1846
|
+
}
|
|
1847
|
+
const resolvedVersion = options.versionRange ?? (options.resolveLatestVersion === false ? VERSION_DECIDED_BY_MARKETPLACE : await deps.resolveVersion(options.versionRange));
|
|
1848
|
+
const scope = options.scope ?? DEFAULT_PLUGIN_SCOPE;
|
|
1849
|
+
return {
|
|
1850
|
+
officialCommand: {
|
|
1851
|
+
executable: "claude",
|
|
1852
|
+
args: [
|
|
1853
|
+
"plugin",
|
|
1854
|
+
"install",
|
|
1855
|
+
distribution.name,
|
|
1856
|
+
"--marketplace",
|
|
1857
|
+
distribution.marketplace,
|
|
1858
|
+
"--scope",
|
|
1859
|
+
scope,
|
|
1860
|
+
"--json"
|
|
1861
|
+
]
|
|
1862
|
+
},
|
|
1863
|
+
resolvedVersion,
|
|
1864
|
+
filesCreated: [],
|
|
1865
|
+
// project/local scope: Claude Code records the plugin in the workspace's own
|
|
1866
|
+
// .claude settings — shown before approval as the files that will change.
|
|
1867
|
+
filesModified: [...WORKSPACE_FILES_BY_SCOPE[scope]],
|
|
1868
|
+
conflicts: [],
|
|
1869
|
+
sourceRepository: entry.repository,
|
|
1870
|
+
officialHomepage: entry.homepage,
|
|
1871
|
+
license: entry.license,
|
|
1872
|
+
requiresManualExecution: false
|
|
1873
|
+
};
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// ../integrations/dist/hosts/claude-code/install.js
|
|
1878
|
+
import { homedir } from "node:os";
|
|
1879
|
+
function manualInstallSteps(entry) {
|
|
1880
|
+
const distribution = entry.hosts["claude-code"];
|
|
1881
|
+
if (distribution === void 0)
|
|
1882
|
+
return [`See ${entry.installDocs} for the official instructions.`];
|
|
1883
|
+
return [
|
|
1884
|
+
`In Claude Code, send this as its own prompt: /plugin marketplace add ${distribution.marketplace}`,
|
|
1885
|
+
`Then, as a second separate prompt: /plugin install ${distribution.pluginId}`,
|
|
1886
|
+
`See ${entry.installDocs} for the official instructions.`
|
|
1887
|
+
];
|
|
1888
|
+
}
|
|
1889
|
+
var CLAUDE_CLI_INSTALL_TIMEOUT_MS = 12e4;
|
|
1890
|
+
var CLAUDE_CLI_INSTALL_STDOUT_LIMIT_BYTES = 1048576;
|
|
1891
|
+
var CLAUDE_CLI_INSTALL_STDERR_LIMIT_BYTES = 262144;
|
|
1892
|
+
function defaultRerunCommand(entry, runNx = "pnpm nx") {
|
|
1893
|
+
return `${runNx} g @clearlydev/nx-archweave:install-integration --tool=${entry.id} --apply`;
|
|
1894
|
+
}
|
|
1895
|
+
function chatAlternative(entry) {
|
|
1896
|
+
return ["Alternatively, inside a Claude Code session (the chat path):", ...manualInstallSteps(entry)];
|
|
1897
|
+
}
|
|
1898
|
+
var MARKETPLACE_NOTE = "If the marketplace was registered before the failure, it stays in your user settings \u2014 see `claude plugin marketplace list`.";
|
|
1899
|
+
function claudeCliNotFoundSteps(entry, rerunCommand) {
|
|
1900
|
+
return [
|
|
1901
|
+
"The claude CLI was not found on PATH. Install the Claude Code CLI from your terminal: npm install -g @anthropic-ai/claude-code (setup guide: https://code.claude.com/docs/en/setup)",
|
|
1902
|
+
`Then rerun: ${rerunCommand}`,
|
|
1903
|
+
...chatAlternative(entry)
|
|
1904
|
+
];
|
|
1905
|
+
}
|
|
1906
|
+
function claudeCliUnsupportedSteps(entry, rerunCommand, cliSaid) {
|
|
1907
|
+
return [
|
|
1908
|
+
`Your claude CLI does not know this command (it said: ${cliSaid}). Scriptable plugin install needs Claude Code 2.1.285 or newer: run \`claude update\`, or reinstall it with npm install -g @anthropic-ai/claude-code@latest`,
|
|
1909
|
+
`Then rerun: ${rerunCommand}`,
|
|
1910
|
+
...chatAlternative(entry)
|
|
1911
|
+
];
|
|
1912
|
+
}
|
|
1913
|
+
var TAIL_CHARS = 500;
|
|
1914
|
+
var UNSUPPORTED_CLI_PATTERN = /error: unknown (command|option) '(plugin|install|marketplace|--json|--marketplace|--scope)'/;
|
|
1915
|
+
function tail(text) {
|
|
1916
|
+
const trimmed = text.trim();
|
|
1917
|
+
return trimmed.length > TAIL_CHARS ? trimmed.slice(-TAIL_CHARS) : trimmed;
|
|
1918
|
+
}
|
|
1919
|
+
var ESCAPED_HOME = homedir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1920
|
+
var HOME_PATTERN = new RegExp(`${ESCAPED_HOME}(?=[/\\\\\\s'"\`]|$)`, "g");
|
|
1921
|
+
function redactHomePaths(text) {
|
|
1922
|
+
let out = ESCAPED_HOME.length > 1 ? text.replace(HOME_PATTERN, "~") : text;
|
|
1923
|
+
out = out.replace(/\/(?:Users|home)\/[^/\s'"`]+/g, "~");
|
|
1924
|
+
out = out.replace(/[A-Za-z]:\\Users\\[^\\\s'"`]+/g, "~");
|
|
1925
|
+
out = out.replace(/\/root(?=[/\s'"`]|$)/g, "~");
|
|
1926
|
+
return out;
|
|
1927
|
+
}
|
|
1928
|
+
function parseLastJsonObject(stdout) {
|
|
1929
|
+
const lines = stdout.split("\n");
|
|
1930
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1931
|
+
const line = lines[i].trim();
|
|
1932
|
+
if (!line.startsWith("{"))
|
|
1933
|
+
continue;
|
|
1934
|
+
try {
|
|
1935
|
+
const parsed = JSON.parse(line);
|
|
1936
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
|
|
1937
|
+
return parsed;
|
|
1938
|
+
} catch {
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
function renderUserCommand(command) {
|
|
1944
|
+
return [command.executable, ...command.args.filter((arg) => arg !== "--json")].join(" ");
|
|
1945
|
+
}
|
|
1946
|
+
async function classifyRun(runner, command, cwd) {
|
|
1947
|
+
let raw;
|
|
1948
|
+
try {
|
|
1949
|
+
raw = await runner.run({
|
|
1950
|
+
executable: command.executable,
|
|
1951
|
+
argv: command.args,
|
|
1952
|
+
...cwd === void 0 ? {} : { cwd },
|
|
1953
|
+
timeoutMs: CLAUDE_CLI_INSTALL_TIMEOUT_MS,
|
|
1954
|
+
stdoutLimitBytes: CLAUDE_CLI_INSTALL_STDOUT_LIMIT_BYTES,
|
|
1955
|
+
stderrLimitBytes: CLAUDE_CLI_INSTALL_STDERR_LIMIT_BYTES
|
|
1956
|
+
});
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
const code = typeof error?.code === "string" ? error.code : "unknown";
|
|
1959
|
+
return code === "ENOENT" ? { kind: "cli-not-found" } : { kind: "failed-to-start", code };
|
|
1960
|
+
}
|
|
1961
|
+
if (raw.timedOut)
|
|
1962
|
+
return { kind: "timeout" };
|
|
1963
|
+
if (raw.outputExceeded)
|
|
1964
|
+
return { kind: "output-too-large" };
|
|
1965
|
+
const json = parseLastJsonObject(raw.stdout);
|
|
1966
|
+
const jsonSaysFailed = json !== null && json["outcome"] === "failed";
|
|
1967
|
+
if (raw.exitCode === 0 && !jsonSaysFailed)
|
|
1968
|
+
return { kind: "executed" };
|
|
1969
|
+
if (json !== null) {
|
|
1970
|
+
const message = redactHomePaths(typeof json["message"] === "string" ? json["message"] : `the claude CLI exited with code ${raw.exitCode}`);
|
|
1971
|
+
const failureCode = typeof json["failureCode"] === "string" ? json["failureCode"] : void 0;
|
|
1972
|
+
const shown = json["shownCommand"];
|
|
1973
|
+
if (typeof shown === "object" && shown !== null)
|
|
1974
|
+
return { kind: "confirmation-required", message };
|
|
1975
|
+
return failureCode === void 0 ? { kind: "failed", message } : { kind: "failed", failureCode, message };
|
|
1976
|
+
}
|
|
1977
|
+
if (UNSUPPORTED_CLI_PATTERN.test(raw.stderr)) {
|
|
1978
|
+
const line = raw.stderr.split("\n").find((l) => UNSUPPORTED_CLI_PATTERN.test(l)) ?? "";
|
|
1979
|
+
return { kind: "cli-unsupported", stderrTail: redactHomePaths(line.trim()) };
|
|
1980
|
+
}
|
|
1981
|
+
const said = redactHomePaths(tail(raw.stderr.length > 0 ? raw.stderr : raw.stdout));
|
|
1982
|
+
return { kind: "failed", message: said.length > 0 ? said : `the claude CLI exited with code ${raw.exitCode}` };
|
|
1983
|
+
}
|
|
1984
|
+
function manualStepsFor(entry, detail, command, rerunCommand) {
|
|
1985
|
+
const chat = chatAlternative(entry);
|
|
1986
|
+
const userCommand = command === null ? "" : renderUserCommand(command);
|
|
1987
|
+
const rerun = `Once resolved, rerun to install, verify and record it: ${rerunCommand}`;
|
|
1988
|
+
switch (detail.kind) {
|
|
1989
|
+
case "executed":
|
|
1990
|
+
return [];
|
|
1991
|
+
case "manual-only":
|
|
1992
|
+
return manualInstallSteps(entry);
|
|
1993
|
+
case "cli-not-found":
|
|
1994
|
+
return claudeCliNotFoundSteps(entry, rerunCommand);
|
|
1995
|
+
case "failed-to-start":
|
|
1996
|
+
return [
|
|
1997
|
+
`The claude CLI was found but could not be started (${detail.code}) \u2014 check its permissions and your system limits.`,
|
|
1998
|
+
rerun,
|
|
1999
|
+
...chat
|
|
2000
|
+
];
|
|
2001
|
+
case "cli-unsupported":
|
|
2002
|
+
return claudeCliUnsupportedSteps(entry, rerunCommand, detail.stderrTail);
|
|
2003
|
+
case "confirmation-required":
|
|
2004
|
+
return [
|
|
2005
|
+
`The marketplace declares a command that must be confirmed by a person; nx-archweave never accepts it for you (${detail.message})`,
|
|
2006
|
+
`Review it and run the install yourself: ${userCommand}`,
|
|
2007
|
+
rerun,
|
|
2008
|
+
...chat
|
|
2009
|
+
];
|
|
2010
|
+
case "failed":
|
|
2011
|
+
return [`The install command did not succeed: ${detail.message}`, MARKETPLACE_NOTE, rerun, ...chat];
|
|
2012
|
+
case "timeout":
|
|
2013
|
+
return [
|
|
2014
|
+
`The install command did not finish within ${CLAUDE_CLI_INSTALL_TIMEOUT_MS / 1e3} s and was stopped.`,
|
|
2015
|
+
MARKETPLACE_NOTE,
|
|
2016
|
+
rerun,
|
|
2017
|
+
...chat
|
|
2018
|
+
];
|
|
2019
|
+
case "output-too-large":
|
|
2020
|
+
return [
|
|
2021
|
+
"The install command produced more output than nx-archweave is willing to buffer and was stopped.",
|
|
2022
|
+
`Run it yourself to see the full output: ${userCommand}`,
|
|
2023
|
+
rerun,
|
|
2024
|
+
...chat
|
|
2025
|
+
];
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
async function installClaudeMarketplacePlugin(entry, deps, plan, options = {}) {
|
|
2029
|
+
const rerunCommand = options.rerunCommand ?? defaultRerunCommand(entry);
|
|
2030
|
+
const detail = plan.officialCommand === null ? { kind: "manual-only" } : await classifyRun(deps.runner, plan.officialCommand, options.cwd);
|
|
2031
|
+
const executed = detail.kind === "executed";
|
|
2032
|
+
const result = {
|
|
2033
|
+
executed,
|
|
2034
|
+
// One command: it either reached exit 0 or it did not. What a failed run may
|
|
2035
|
+
// have left behind is re-checked live by install-integration, not guessed here.
|
|
2036
|
+
partialFailure: false,
|
|
2037
|
+
filesCreated: [],
|
|
2038
|
+
// The workspace files the plan said the host would change (project/local scope);
|
|
2039
|
+
// Claude's own plugin store is outside the workspace and never listed.
|
|
2040
|
+
filesModified: executed ? [...plan.filesModified] : [],
|
|
2041
|
+
manualStepsRemaining: manualStepsFor(entry, detail, plan.officialCommand, rerunCommand)
|
|
2042
|
+
};
|
|
2043
|
+
return { result, detail };
|
|
2044
|
+
}
|
|
2045
|
+
function createClaudeMarketplaceInstall(entry, deps) {
|
|
2046
|
+
return async (plan) => (await installClaudeMarketplacePlugin(entry, deps, plan)).result;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// ../integrations/dist/hosts/claude-code/verify-via-claude-cli.js
|
|
2050
|
+
import { realpathSync } from "node:fs";
|
|
2051
|
+
import { resolve } from "node:path";
|
|
2052
|
+
|
|
2053
|
+
// ../integrations/dist/hosts/claude-code/parse-claude-plugin-list.js
|
|
2054
|
+
var KNOWN_SCOPES = /* @__PURE__ */ new Set(["user", "project", "local", "managed"]);
|
|
2055
|
+
function isKnownScope(scope) {
|
|
2056
|
+
return KNOWN_SCOPES.has(scope);
|
|
2057
|
+
}
|
|
2058
|
+
function isValidEntry(value) {
|
|
2059
|
+
if (typeof value !== "object" || value === null) {
|
|
2060
|
+
return false;
|
|
2061
|
+
}
|
|
2062
|
+
const record = value;
|
|
2063
|
+
if (typeof record["id"] !== "string") {
|
|
2064
|
+
return false;
|
|
2065
|
+
}
|
|
2066
|
+
if (typeof record["enabled"] !== "boolean") {
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
if (typeof record["scope"] !== "string" || record["scope"].length === 0) {
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
if (record["version"] !== void 0 && typeof record["version"] !== "string") {
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
return true;
|
|
2076
|
+
}
|
|
2077
|
+
function parseClaudePluginList(rawStdout, options = {}) {
|
|
2078
|
+
let parsed;
|
|
2079
|
+
try {
|
|
2080
|
+
parsed = JSON.parse(rawStdout);
|
|
2081
|
+
} catch {
|
|
2082
|
+
return { ok: false, reason: "invalid-json" };
|
|
2083
|
+
}
|
|
2084
|
+
if (!Array.isArray(parsed)) {
|
|
2085
|
+
return { ok: false, reason: "non-array" };
|
|
2086
|
+
}
|
|
2087
|
+
const entries = [];
|
|
2088
|
+
const otherScopeIds = [];
|
|
2089
|
+
for (const item of parsed) {
|
|
2090
|
+
if (!isValidEntry(item)) {
|
|
2091
|
+
return { ok: false, reason: "missing-fields" };
|
|
2092
|
+
}
|
|
2093
|
+
if (typeof item.projectPath === "string" && options.isThisWorkspace && !options.isThisWorkspace(item.projectPath)) {
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
const scope = item.scope;
|
|
2097
|
+
if (!isKnownScope(scope)) {
|
|
2098
|
+
otherScopeIds.push(item.id);
|
|
2099
|
+
continue;
|
|
2100
|
+
}
|
|
2101
|
+
entries.push({
|
|
2102
|
+
id: item.id,
|
|
2103
|
+
version: item.version,
|
|
2104
|
+
scope,
|
|
2105
|
+
enabled: item.enabled
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
return { ok: true, entries, otherScopeIds };
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// ../integrations/dist/hosts/claude-code/match-claude-plugin.js
|
|
2112
|
+
function matchClaudePlugin(entries, pluginId) {
|
|
2113
|
+
const matches = entries.filter((candidate) => candidate.id === pluginId);
|
|
2114
|
+
if (matches.length === 0) {
|
|
2115
|
+
return { kind: "absent" };
|
|
2116
|
+
}
|
|
2117
|
+
if (matches.length > 1) {
|
|
2118
|
+
return { kind: "ambiguous", entries: matches };
|
|
2119
|
+
}
|
|
2120
|
+
return { kind: "matched", entry: matches[0] };
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// ../integrations/dist/hosts/claude-code/claude-cli-verification-result.js
|
|
2124
|
+
function evaluateClaudeCliMatch(match) {
|
|
2125
|
+
switch (match.kind) {
|
|
2126
|
+
case "matched":
|
|
2127
|
+
return match.entry.enabled ? { status: "verified", version: match.entry.version, scope: match.entry.scope } : { status: "installed-but-disabled", scope: match.entry.scope };
|
|
2128
|
+
case "absent":
|
|
2129
|
+
return { status: "absent" };
|
|
2130
|
+
case "ambiguous":
|
|
2131
|
+
return { status: "ambiguous" };
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// ../integrations/dist/hosts/claude-code/verify-via-claude-cli.js
|
|
2136
|
+
async function verifyViaClaudeCli(runner, pluginId, cwd) {
|
|
2137
|
+
let raw;
|
|
2138
|
+
try {
|
|
2139
|
+
raw = await runner.run({
|
|
2140
|
+
executable: "claude",
|
|
2141
|
+
argv: ["plugin", "list", "--json"],
|
|
2142
|
+
...cwd === void 0 ? {} : { cwd },
|
|
2143
|
+
timeoutMs: CLAUDE_CLI_TIMEOUT_MS,
|
|
2144
|
+
stdoutLimitBytes: CLAUDE_CLI_STDOUT_LIMIT_BYTES,
|
|
2145
|
+
stderrLimitBytes: CLAUDE_CLI_STDERR_LIMIT_BYTES
|
|
2146
|
+
});
|
|
2147
|
+
} catch {
|
|
2148
|
+
return { source: "claude-cli-unavailable", reason: "not-found" };
|
|
2149
|
+
}
|
|
2150
|
+
if (raw.timedOut) {
|
|
2151
|
+
return { source: "claude-cli-unavailable", reason: "timeout" };
|
|
2152
|
+
}
|
|
2153
|
+
if (raw.outputExceeded) {
|
|
2154
|
+
return { source: "claude-cli-unavailable", reason: "output-too-large" };
|
|
2155
|
+
}
|
|
2156
|
+
if (raw.exitCode !== 0) {
|
|
2157
|
+
return { source: "claude-cli-unavailable", reason: "non-zero-exit" };
|
|
2158
|
+
}
|
|
2159
|
+
const parsed = parseClaudePluginList(raw.stdout, cwd === void 0 ? {} : { isThisWorkspace: samePathAs(cwd) });
|
|
2160
|
+
if (!parsed.ok) {
|
|
2161
|
+
return {
|
|
2162
|
+
source: "claude-cli-unavailable",
|
|
2163
|
+
reason: parsed.reason === "invalid-json" ? "invalid-json" : "unexpected-schema"
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
const match = matchClaudePlugin(parsed.entries, pluginId);
|
|
2167
|
+
if (parsed.otherScopeIds.includes(pluginId)) {
|
|
2168
|
+
return match.kind === "absent" ? { source: "claude-cli-unavailable", reason: "unexpected-schema" } : { source: "claude-cli", outcome: { status: "ambiguous" } };
|
|
2169
|
+
}
|
|
2170
|
+
return { source: "claude-cli", outcome: evaluateClaudeCliMatch(match) };
|
|
2171
|
+
}
|
|
2172
|
+
function canonical(path) {
|
|
2173
|
+
try {
|
|
2174
|
+
return realpathSync(path);
|
|
2175
|
+
} catch {
|
|
2176
|
+
return resolve(path);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
function samePathAs(workspaceRoot) {
|
|
2180
|
+
const root = canonical(workspaceRoot);
|
|
2181
|
+
return (projectPath) => canonical(projectPath) === root;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// ../integrations/dist/hosts/claude-code/verify.js
|
|
2185
|
+
async function verifyClaudeMarketplacePlugin(entry, deps, context) {
|
|
2186
|
+
const detect = deps.detect ?? detectWorkspacePackage;
|
|
2187
|
+
const pluginId = entry.hosts["claude-code"]?.pluginId;
|
|
2188
|
+
if (pluginId === void 0) {
|
|
2189
|
+
throw new Error(`${entry.displayName} has no Claude Code distribution \u2014 it cannot be verified through the claude CLI.`);
|
|
2190
|
+
}
|
|
2191
|
+
const cliResult = await verifyViaClaudeCli(deps.runner, pluginId, context.workspaceRoot);
|
|
2192
|
+
if (cliResult.source === "claude-cli") {
|
|
2193
|
+
switch (cliResult.outcome.status) {
|
|
2194
|
+
case "verified":
|
|
2195
|
+
return {
|
|
2196
|
+
source: "claude-cli",
|
|
2197
|
+
result: { verified: true, resolvedVersion: cliResult.outcome.version, problems: [] }
|
|
2198
|
+
};
|
|
2199
|
+
case "installed-but-disabled":
|
|
2200
|
+
return {
|
|
2201
|
+
source: "claude-cli",
|
|
2202
|
+
result: {
|
|
2203
|
+
verified: false,
|
|
2204
|
+
problems: [
|
|
2205
|
+
`${entry.displayName} is installed (scope: ${cliResult.outcome.scope}) but currently disabled in Claude Code.`,
|
|
2206
|
+
"Enable it before recording this integration \u2014 a disabled plugin is never recorded as verified."
|
|
2207
|
+
]
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
case "absent":
|
|
2211
|
+
return {
|
|
2212
|
+
source: "claude-cli",
|
|
2213
|
+
result: {
|
|
2214
|
+
verified: false,
|
|
2215
|
+
problems: [
|
|
2216
|
+
`${pluginId} was not found in the installed Claude Code plugin list.`,
|
|
2217
|
+
`Install it (${defaultRerunCommand(entry)}) or complete the manual Claude Code steps, then try again.`
|
|
2218
|
+
]
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
case "ambiguous":
|
|
2222
|
+
return {
|
|
2223
|
+
source: "claude-cli",
|
|
2224
|
+
result: {
|
|
2225
|
+
verified: false,
|
|
2226
|
+
problems: [
|
|
2227
|
+
`Verification error: more than one installed plugin matches ${pluginId} exactly. This is ambiguous and cannot be safely resolved automatically.`
|
|
2228
|
+
]
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (cliResult.reason === "not-found") {
|
|
2234
|
+
const detected = await detect(entry, context);
|
|
2235
|
+
if (detected.installed) {
|
|
2236
|
+
return {
|
|
2237
|
+
source: "package-json",
|
|
2238
|
+
result: { verified: true, resolvedVersion: detected.installedVersion, problems: [] }
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
return {
|
|
2242
|
+
source: "package-json",
|
|
2243
|
+
result: {
|
|
2244
|
+
verified: false,
|
|
2245
|
+
problems: [
|
|
2246
|
+
`The claude CLI was not found, and ${entry.displayName} was not found as a workspace dependency either.`,
|
|
2247
|
+
`Install the Claude Code CLI to use the authoritative verification signal, or add ${entry.displayName} as a workspace dependency for the weaker fallback signal.`
|
|
2248
|
+
]
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
return {
|
|
2253
|
+
source: "claude-cli",
|
|
2254
|
+
result: {
|
|
2255
|
+
verified: false,
|
|
2256
|
+
problems: [`Verification error: the claude CLI returned an unexpected result (${cliResult.reason}).`]
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
function createClaudeMarketplaceVerifier(entry, deps) {
|
|
2261
|
+
return async (context) => (await verifyClaudeMarketplacePlugin(entry, deps, context)).result;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// ../integrations/dist/catalog/create-catalog-integration.js
|
|
2265
|
+
function defaultResolveVersion(entry) {
|
|
2266
|
+
const npmPackage = entry.npmPackage;
|
|
2267
|
+
return npmPackage === void 0 ? async () => VERSION_DECIDED_BY_MARKETPLACE : () => resolveVersionFromNpmRegistry(npmPackage);
|
|
2268
|
+
}
|
|
2269
|
+
function createCatalogIntegration(entry, deps) {
|
|
2270
|
+
return {
|
|
2271
|
+
id: entry.id,
|
|
2272
|
+
displayName: entry.displayName,
|
|
2273
|
+
homepage: entry.homepage,
|
|
2274
|
+
repository: entry.repository,
|
|
2275
|
+
license: entry.license,
|
|
2276
|
+
supportedEnvironments: PLUGIN_HOSTS.filter((host) => entry.hosts[host] !== void 0),
|
|
2277
|
+
detect: (context) => detectWorkspacePackage(entry, context),
|
|
2278
|
+
plan: createClaudeMarketplacePlan(entry, { resolveVersion: deps.resolveVersion ?? defaultResolveVersion(entry) }),
|
|
2279
|
+
install: createClaudeMarketplaceInstall(entry, { runner: deps.runner }),
|
|
2280
|
+
verify: createClaudeMarketplaceVerifier(entry, { runner: deps.runner }),
|
|
2281
|
+
uninstallInstructions: async () => [...entry.uninstallSteps]
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
function createCatalogIntegrations(deps) {
|
|
2285
|
+
return PLUGIN_CATALOG.map((entry) => createCatalogIntegration(entry, deps));
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// src/runtime/node-command-runner.ts
|
|
2289
|
+
import { spawn } from "node:child_process";
|
|
2290
|
+
var KILL_GRACE_MS = 500;
|
|
2291
|
+
var NodeCommandRunner = class {
|
|
2292
|
+
run(input) {
|
|
2293
|
+
return new Promise((resolve2, reject) => {
|
|
2294
|
+
let stdout = "";
|
|
2295
|
+
let stderr = "";
|
|
2296
|
+
let timedOut = false;
|
|
2297
|
+
let outputExceeded = false;
|
|
2298
|
+
let settled = false;
|
|
2299
|
+
let killTimer;
|
|
2300
|
+
const spawnable = spawnableCommand(input.executable, input.argv);
|
|
2301
|
+
const child = spawn(spawnable.command, spawnable.args, {
|
|
2302
|
+
cwd: input.cwd,
|
|
2303
|
+
shell: false,
|
|
2304
|
+
windowsVerbatimArguments: spawnable.windowsVerbatimArguments,
|
|
2305
|
+
windowsHide: true,
|
|
2306
|
+
// No stdin: nothing here ever answers a prompt (a command that wants
|
|
2307
|
+
// confirmation must fail fast instead of waiting on an open pipe).
|
|
2308
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2309
|
+
});
|
|
2310
|
+
const terminate = () => {
|
|
2311
|
+
child.kill("SIGTERM");
|
|
2312
|
+
killTimer = setTimeout(() => {
|
|
2313
|
+
child.kill("SIGKILL");
|
|
2314
|
+
}, KILL_GRACE_MS);
|
|
2315
|
+
};
|
|
2316
|
+
const timer = setTimeout(() => {
|
|
2317
|
+
timedOut = true;
|
|
2318
|
+
terminate();
|
|
2319
|
+
}, input.timeoutMs);
|
|
2320
|
+
child.stdout.on("data", (chunk) => {
|
|
2321
|
+
stdout += chunk.toString("utf8");
|
|
2322
|
+
if (Buffer.byteLength(stdout, "utf8") > input.stdoutLimitBytes) {
|
|
2323
|
+
outputExceeded = true;
|
|
2324
|
+
terminate();
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
child.stderr.on("data", (chunk) => {
|
|
2328
|
+
stderr += chunk.toString("utf8");
|
|
2329
|
+
if (Buffer.byteLength(stderr, "utf8") > input.stderrLimitBytes) {
|
|
2330
|
+
outputExceeded = true;
|
|
2331
|
+
terminate();
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2334
|
+
child.on("error", (error) => {
|
|
2335
|
+
if (settled) return;
|
|
2336
|
+
settled = true;
|
|
2337
|
+
clearTimeout(timer);
|
|
2338
|
+
clearTimeout(killTimer);
|
|
2339
|
+
reject(error);
|
|
2340
|
+
});
|
|
2341
|
+
child.on("close", (code) => {
|
|
2342
|
+
if (settled) return;
|
|
2343
|
+
settled = true;
|
|
2344
|
+
clearTimeout(timer);
|
|
2345
|
+
clearTimeout(killTimer);
|
|
2346
|
+
resolve2({ stdout, stderr, exitCode: code, timedOut, outputExceeded });
|
|
2347
|
+
});
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
};
|
|
2351
|
+
|
|
2352
|
+
// src/runtime/integrations-composition-root.ts
|
|
2353
|
+
function createIntegrationsCompositionRoot(deps = {}) {
|
|
2354
|
+
const runner = deps.runner ?? new NodeCommandRunner();
|
|
2355
|
+
const registry = createIntegrationRegistry(createCatalogIntegrations({ runner, resolveVersion: deps.resolveVersion }));
|
|
2356
|
+
return {
|
|
2357
|
+
registry,
|
|
2358
|
+
runner,
|
|
2359
|
+
getIntegration: (id) => getIntegration(registry, id),
|
|
2360
|
+
listIntegrations: () => listIntegrations(registry)
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
// src/generators/shared/run-nx.ts
|
|
2365
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
|
|
2366
|
+
import { join as join10 } from "node:path";
|
|
2367
|
+
|
|
2368
|
+
// src/generators/shared/toolkit-config-file.ts
|
|
2369
|
+
var TOOLKIT_CONFIG_PATH = "toolkit.config.json";
|
|
2370
|
+
function recordedChoice(config, key, allowed) {
|
|
2371
|
+
const value = config?.[key];
|
|
2372
|
+
return typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
2373
|
+
}
|
|
2374
|
+
var RECORDED_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
2375
|
+
function workspacePackageManager(exists, read, config) {
|
|
2376
|
+
return detectWorkspacePackageManager(exists, read) ?? recordedChoice(config, "packageManager", RECORDED_PACKAGE_MANAGERS) ?? null;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// src/generators/shared/run-nx.ts
|
|
2380
|
+
function runNxFor(exists, read) {
|
|
2381
|
+
let config = null;
|
|
2382
|
+
try {
|
|
2383
|
+
const parsed = JSON.parse(read(TOOLKIT_CONFIG_PATH) ?? "null");
|
|
2384
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) config = parsed;
|
|
2385
|
+
} catch {
|
|
2386
|
+
}
|
|
2387
|
+
return packageManagerCommands(workspacePackageManager(exists, read, config) ?? "pnpm").runNx;
|
|
2388
|
+
}
|
|
2389
|
+
function diskRunNx(root) {
|
|
2390
|
+
return runNxFor(
|
|
2391
|
+
(path) => existsSync10(join10(root, path)),
|
|
2392
|
+
(path) => existsSync10(join10(root, path)) ? readFileSync8(join10(root, path), "utf-8") : void 0
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// src/generators/doctor/integration-report.ts
|
|
2397
|
+
var DOCTOR_TARGET_ENVIRONMENT = "claude-code";
|
|
2398
|
+
async function buildDoctorReportWithIntegrations(root, options = {}, deps = {}) {
|
|
2399
|
+
const integrationsRoot = deps.integrationsRoot ?? createIntegrationsCompositionRoot();
|
|
2400
|
+
const baseReport = runDoctorChecks(root);
|
|
2401
|
+
const declaredIds = readEnabledIntegrationIds(root);
|
|
2402
|
+
const runNx = diskRunNx(root);
|
|
2403
|
+
const driftResults = await detectIntegrationsDrift({
|
|
2404
|
+
workspaceRoot: root,
|
|
2405
|
+
targetEnvironment: DOCTOR_TARGET_ENVIRONMENT,
|
|
2406
|
+
declaredIds,
|
|
2407
|
+
integrations: integrationsRoot.listIntegrations(),
|
|
2408
|
+
runNx
|
|
2409
|
+
});
|
|
2410
|
+
const finalDriftResults = options.liveIntegrations ? await applyLiveVerification(driftResults, {
|
|
2411
|
+
root,
|
|
2412
|
+
declaredIds,
|
|
2413
|
+
runner: integrationsRoot.runner,
|
|
2414
|
+
runNx
|
|
2415
|
+
}) : driftResults;
|
|
2416
|
+
const integrationChecks = buildIntegrationDoctorChecks(finalDriftResults);
|
|
2417
|
+
const checks = [...baseReport.checks, ...integrationChecks];
|
|
2418
|
+
return { checks, overallStatus: computeOverallStatus(checks) };
|
|
2419
|
+
}
|
|
2420
|
+
async function applyLiveVerification(driftResults, ctx) {
|
|
2421
|
+
const context = { workspaceRoot: ctx.root, targetEnvironment: DOCTOR_TARGET_ENVIRONMENT };
|
|
2422
|
+
const manifest = loadManifest(ctx.root);
|
|
2423
|
+
const live = /* @__PURE__ */ new Map();
|
|
2424
|
+
const reported = new Set(driftResults.map((result) => result.id));
|
|
2425
|
+
for (const entry of PLUGIN_CATALOG) {
|
|
2426
|
+
if (!reported.has(entry.id) || entry.hosts[DOCTOR_TARGET_ENVIRONMENT] === void 0) continue;
|
|
2427
|
+
const existingEntry = manifest.integrations[entry.id];
|
|
2428
|
+
const detected = await detectWorkspacePackage(entry, context);
|
|
2429
|
+
const { result: liveVerification } = await verifyClaudeMarketplacePlugin(entry, { runner: ctx.runner }, context);
|
|
2430
|
+
const liveEntry = existingEntry ? { ...existingEntry, verification: liveVerification.verified ? "passed" : "failed" } : existingEntry;
|
|
2431
|
+
live.set(
|
|
2432
|
+
entry.id,
|
|
2433
|
+
classifyIntegrationDrift({
|
|
2434
|
+
id: entry.id,
|
|
2435
|
+
installable: true,
|
|
2436
|
+
declared: ctx.declaredIds.includes(entry.id),
|
|
2437
|
+
manifestEntry: liveEntry,
|
|
2438
|
+
detected,
|
|
2439
|
+
runNx: ctx.runNx
|
|
2440
|
+
})
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
return driftResults.map((result) => live.get(result.id) ?? result);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// src/generators/doctor/generator.ts
|
|
2447
|
+
async function doctorGenerator(tree, options) {
|
|
2448
|
+
const report = await buildDoctorReportWithIntegrations(tree.root, {
|
|
2449
|
+
liveIntegrations: options.liveIntegrations ?? false
|
|
2450
|
+
});
|
|
2451
|
+
if (options.json ?? false) {
|
|
2452
|
+
console.log(serializeDoctorReport(report).trimEnd());
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
console.log(renderDoctorReport(report));
|
|
2456
|
+
console.log("\n(read-only \u2014 no files were changed)");
|
|
2457
|
+
}
|
|
2458
|
+
var generator_default = doctorGenerator;
|
|
2459
|
+
export {
|
|
2460
|
+
generator_default as default,
|
|
2461
|
+
doctorGenerator
|
|
2462
|
+
};
|