codeloop-mcp-server 0.1.15 → 0.1.17
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 +75 -0
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/runners/base.d.ts +1 -1
- package/dist/runners/base.d.ts.map +1 -1
- package/dist/runners/base.js +28 -3
- package/dist/runners/base.js.map +1 -1
- package/dist/runners/generic.js +23 -20
- package/dist/runners/generic.js.map +1 -1
- package/dist/runners/window_manager.d.ts.map +1 -1
- package/dist/runners/window_manager.js +12 -5
- package/dist/runners/window_manager.js.map +1 -1
- package/dist/tools/gate_check.js +5 -2
- package/dist/tools/gate_check.js.map +1 -1
- package/dist/tools/init-project.d.ts.map +1 -1
- package/dist/tools/init-project.js +40 -262
- package/dist/tools/init-project.js.map +1 -1
- package/dist/tools/verify.d.ts +21 -0
- package/dist/tools/verify.d.ts.map +1 -1
- package/dist/tools/verify.js +154 -6
- package/dist/tools/verify.js.map +1 -1
- package/package.json +28 -4
|
@@ -1,273 +1,51 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import { join
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { setupProject, detectIDE, } from "@codelooptech/shared/init/setup-project";
|
|
4
|
+
import { detectProject } from "@codelooptech/shared/init/detect-project";
|
|
5
|
+
const VALID_PROJECT_TYPES = [
|
|
6
|
+
"flutter",
|
|
7
|
+
"web",
|
|
8
|
+
"node",
|
|
9
|
+
"mobile",
|
|
10
|
+
"xcode",
|
|
11
|
+
"android",
|
|
12
|
+
"dotnet",
|
|
13
|
+
"python",
|
|
14
|
+
"ruby",
|
|
15
|
+
"rails",
|
|
16
|
+
"rust",
|
|
17
|
+
"unknown",
|
|
18
|
+
];
|
|
3
19
|
export async function runInitProject(input) {
|
|
4
20
|
const cwd = input.project_dir;
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
// 2. Create artifacts directory
|
|
20
|
-
const artifactsDir = join(cwd, "artifacts");
|
|
21
|
-
if (!existsSync(artifactsDir)) {
|
|
22
|
-
mkdirSync(artifactsDir, { recursive: true });
|
|
23
|
-
filesCreated.push("artifacts/");
|
|
24
|
-
}
|
|
25
|
-
// 3. IDE-specific setup
|
|
26
|
-
if (ide === "cursor" || ide === "both") {
|
|
27
|
-
setupCursorProject(cwd, projectType, filesCreated, filesMerged);
|
|
28
|
-
}
|
|
29
|
-
if (ide === "claude" || ide === "both") {
|
|
30
|
-
setupClaudeProject(cwd, projectType, filesCreated, filesMerged);
|
|
31
|
-
}
|
|
32
|
-
if (ide === "unknown") {
|
|
33
|
-
setupCursorProject(cwd, projectType, filesCreated, filesMerged);
|
|
34
|
-
setupClaudeProject(cwd, projectType, filesCreated, filesMerged);
|
|
35
|
-
}
|
|
36
|
-
// 4. Update .gitignore
|
|
37
|
-
updateGitignore(cwd, filesCreated);
|
|
21
|
+
const requested = input.project_type;
|
|
22
|
+
const projectType = !requested || requested === "auto"
|
|
23
|
+
? detectProject(cwd).type
|
|
24
|
+
: VALID_PROJECT_TYPES.includes(requested)
|
|
25
|
+
? requested
|
|
26
|
+
: "unknown";
|
|
27
|
+
// Track whether this run touched anything so we can emit a friendlier message.
|
|
28
|
+
const ideBefore = detectIdeOrUnknown(cwd);
|
|
29
|
+
const result = setupProject({
|
|
30
|
+
cwd,
|
|
31
|
+
apiKey: process.env.CODELOOP_API_KEY ?? null,
|
|
32
|
+
projectType,
|
|
33
|
+
});
|
|
38
34
|
return {
|
|
39
35
|
initialized: true,
|
|
40
36
|
project_dir: cwd,
|
|
41
|
-
project_type: projectType,
|
|
42
|
-
ide_detected: ide,
|
|
43
|
-
files_created: filesCreated,
|
|
44
|
-
files_merged: filesMerged,
|
|
45
|
-
message: `CodeLoop initialized for ${projectType} project. Created ${filesCreated.length} files, merged ${filesMerged.length} configs.`,
|
|
37
|
+
project_type: result.projectType,
|
|
38
|
+
ide_detected: ideBefore === "unknown" ? result.ide : ideBefore,
|
|
39
|
+
files_created: result.filesCreated,
|
|
40
|
+
files_merged: result.filesMerged,
|
|
41
|
+
message: `CodeLoop initialized for ${result.projectType} project. Created ${result.filesCreated.length} files, merged ${result.filesMerged.length} configs.`,
|
|
46
42
|
};
|
|
47
43
|
}
|
|
48
|
-
function
|
|
49
|
-
if (existsSync(join(cwd, "pubspec.yaml")))
|
|
50
|
-
return "flutter";
|
|
51
|
-
if (existsSync(join(cwd, "playwright.config.ts")) ||
|
|
52
|
-
existsSync(join(cwd, "playwright.config.js")))
|
|
53
|
-
return "web";
|
|
54
|
-
if (existsSync(join(cwd, "package.json"))) {
|
|
55
|
-
try {
|
|
56
|
-
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
|
|
57
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
58
|
-
const webFrameworks = [
|
|
59
|
-
"react", "next", "vue", "nuxt", "svelte", "@sveltejs/kit",
|
|
60
|
-
"angular", "@angular/core", "gatsby", "remix", "astro", "vite",
|
|
61
|
-
"@playwright/test",
|
|
62
|
-
];
|
|
63
|
-
if (webFrameworks.some(fw => fw in deps))
|
|
64
|
-
return "web";
|
|
65
|
-
}
|
|
66
|
-
catch { /* fall through */ }
|
|
67
|
-
return "node";
|
|
68
|
-
}
|
|
69
|
-
if (hasFileWith(cwd, ".xcodeproj") || hasFileWith(cwd, ".xcworkspace"))
|
|
70
|
-
return "xcode";
|
|
71
|
-
if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts")))
|
|
72
|
-
return "android";
|
|
73
|
-
if (hasFileWith(cwd, ".sln") || hasFileWith(cwd, ".csproj"))
|
|
74
|
-
return "dotnet";
|
|
75
|
-
return "unknown";
|
|
76
|
-
}
|
|
77
|
-
function hasFileWith(dir, ext) {
|
|
78
|
-
try {
|
|
79
|
-
return readdirSync(dir).some(f => f.endsWith(ext));
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function detectIDE(cwd) {
|
|
44
|
+
function detectIdeOrUnknown(cwd) {
|
|
86
45
|
const hasCursor = existsSync(join(cwd, ".cursor"));
|
|
87
46
|
const hasClaude = existsSync(join(cwd, ".claude")) || existsSync(join(cwd, "CLAUDE.md"));
|
|
88
|
-
if (hasCursor && hasClaude)
|
|
89
|
-
return "
|
|
90
|
-
|
|
91
|
-
return "cursor";
|
|
92
|
-
if (hasClaude)
|
|
93
|
-
return "claude";
|
|
94
|
-
return "unknown";
|
|
95
|
-
}
|
|
96
|
-
function createConfig(apiKey, projectType) {
|
|
97
|
-
return {
|
|
98
|
-
api_key: apiKey,
|
|
99
|
-
vision_model: "auto",
|
|
100
|
-
platforms: projectType !== "unknown" ? [projectType] : ["auto"],
|
|
101
|
-
screenshot_viewports: ["375x812", "390x844", "1440x900"],
|
|
102
|
-
design_match_threshold: 0.85,
|
|
103
|
-
max_loop_iterations: 10,
|
|
104
|
-
auto_verify_on_complete: true,
|
|
105
|
-
auto_gate_check: true,
|
|
106
|
-
recommendations: {
|
|
107
|
-
enabled: true,
|
|
108
|
-
external_tools: true,
|
|
109
|
-
sponsored: false,
|
|
110
|
-
preferences: {
|
|
111
|
-
budget: "low",
|
|
112
|
-
self_hosted: false,
|
|
113
|
-
open_source_only: false,
|
|
114
|
-
gdpr_sensitive: false,
|
|
115
|
-
preferred_cloud: "",
|
|
116
|
-
region: "",
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
evidence: {
|
|
120
|
-
capture_screenshots: true,
|
|
121
|
-
capture_traces: true,
|
|
122
|
-
capture_videos: true,
|
|
123
|
-
baseline_auto_update: false,
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function setupCursorProject(cwd, projectType, filesCreated, filesMerged) {
|
|
128
|
-
// Merge MCP config
|
|
129
|
-
const mcpPath = join(cwd, ".cursor", "mcp.json");
|
|
130
|
-
mergeMcpEntry(mcpPath);
|
|
131
|
-
filesMerged.push(".cursor/mcp.json");
|
|
132
|
-
// Create core rules
|
|
133
|
-
const rulesDir = join(cwd, ".cursor", "rules");
|
|
134
|
-
if (!existsSync(rulesDir))
|
|
135
|
-
mkdirSync(rulesDir, { recursive: true });
|
|
136
|
-
writeIfMissing(join(rulesDir, "core.mdc"), CORE_RULE, filesCreated, ".cursor/rules/core.mdc");
|
|
137
|
-
writeIfMissing(join(rulesDir, "loop.mdc"), LOOP_RULE, filesCreated, ".cursor/rules/loop.mdc");
|
|
138
|
-
if (projectType === "web") {
|
|
139
|
-
writeIfMissing(join(rulesDir, "web.mdc"), WEB_RULE, filesCreated, ".cursor/rules/web.mdc");
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
function setupClaudeProject(cwd, projectType, filesCreated, filesMerged) {
|
|
143
|
-
// Merge MCP config
|
|
144
|
-
const settingsPath = join(cwd, ".claude", "settings.local.json");
|
|
145
|
-
mergeMcpEntry(settingsPath);
|
|
146
|
-
filesMerged.push(".claude/settings.local.json");
|
|
147
|
-
// Create CLAUDE.md
|
|
148
|
-
const claudeMdPath = join(cwd, "CLAUDE.md");
|
|
149
|
-
const platformNote = projectType === "web"
|
|
150
|
-
? "\n- For web projects: run `codeloop_verify` with platform 'web' after UI changes"
|
|
151
|
-
: "";
|
|
152
|
-
writeIfMissing(claudeMdPath, CLAUDE_RULE(platformNote), filesCreated, "CLAUDE.md");
|
|
153
|
-
}
|
|
154
|
-
function mergeMcpEntry(filePath) {
|
|
155
|
-
const dir = dirname(filePath);
|
|
156
|
-
if (!existsSync(dir))
|
|
157
|
-
mkdirSync(dir, { recursive: true });
|
|
158
|
-
let existing = {};
|
|
159
|
-
if (existsSync(filePath)) {
|
|
160
|
-
try {
|
|
161
|
-
existing = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
162
|
-
}
|
|
163
|
-
catch { /* use empty */ }
|
|
164
|
-
}
|
|
165
|
-
const servers = (existing.mcpServers ?? {});
|
|
166
|
-
if (!servers.codeloop) {
|
|
167
|
-
servers.codeloop = {
|
|
168
|
-
command: "npx",
|
|
169
|
-
args: ["-y", "codeloop-mcp-server"],
|
|
170
|
-
env: { CODELOOP_API_KEY: "${CODELOOP_API_KEY}" },
|
|
171
|
-
};
|
|
172
|
-
existing.mcpServers = servers;
|
|
173
|
-
writeFileSync(filePath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
function updateGitignore(cwd, filesCreated) {
|
|
177
|
-
const gitignorePath = join(cwd, ".gitignore");
|
|
178
|
-
const entries = [".codeloop/config.json", "artifacts/", ".env", ".env.*", "!.env.example"];
|
|
179
|
-
let content = "";
|
|
180
|
-
if (existsSync(gitignorePath)) {
|
|
181
|
-
content = readFileSync(gitignorePath, "utf-8");
|
|
182
|
-
}
|
|
183
|
-
const newEntries = entries.filter(e => !content.includes(e));
|
|
184
|
-
if (newEntries.length > 0) {
|
|
185
|
-
const addition = (content.endsWith("\n") ? "" : "\n") +
|
|
186
|
-
"\n# CodeLoop\n" + newEntries.join("\n") + "\n";
|
|
187
|
-
writeFileSync(gitignorePath, content + addition, "utf-8");
|
|
188
|
-
filesCreated.push(".gitignore");
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
function writeFileEnsureDir(filePath, content) {
|
|
192
|
-
const dir = dirname(filePath);
|
|
193
|
-
if (!existsSync(dir))
|
|
194
|
-
mkdirSync(dir, { recursive: true });
|
|
195
|
-
writeFileSync(filePath, content, "utf-8");
|
|
196
|
-
}
|
|
197
|
-
function writeIfMissing(filePath, content, filesCreated, displayPath) {
|
|
198
|
-
if (!existsSync(filePath)) {
|
|
199
|
-
writeFileEnsureDir(filePath, content);
|
|
200
|
-
filesCreated.push(displayPath);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// Minimal rule templates — enough to activate CodeLoop's verification loop.
|
|
204
|
-
// Full templates are available via `npx codeloop init` (CLI package).
|
|
205
|
-
const CORE_RULE = `---
|
|
206
|
-
description: CodeLoop automated verification — core workflow
|
|
207
|
-
globs: "**/*"
|
|
208
|
-
alwaysApply: true
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
# CodeLoop Core Rules
|
|
212
|
-
|
|
213
|
-
## After every code change
|
|
214
|
-
|
|
215
|
-
1. Call \`codeloop_verify\` to run tests and static analysis
|
|
216
|
-
2. Call \`codeloop_capture_screenshot\` to capture visual evidence
|
|
217
|
-
3. If tests fail, call \`codeloop_diagnose\` and fix issues
|
|
218
|
-
4. Call \`codeloop_gate_check\` before marking complete
|
|
219
|
-
|
|
220
|
-
## Never
|
|
221
|
-
|
|
222
|
-
- Never skip verification after code changes
|
|
223
|
-
- Never mark a task complete without evidence
|
|
224
|
-
- Never ignore test failures
|
|
225
|
-
`;
|
|
226
|
-
const LOOP_RULE = `---
|
|
227
|
-
description: CodeLoop verify-diagnose-fix cycle
|
|
228
|
-
globs: "**/*"
|
|
229
|
-
alwaysApply: false
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
# CodeLoop Loop
|
|
233
|
-
|
|
234
|
-
When implementing features:
|
|
235
|
-
|
|
236
|
-
1. Make the code change
|
|
237
|
-
2. \`codeloop_verify\` — run tests
|
|
238
|
-
3. If failures → \`codeloop_diagnose\` → fix → goto 2
|
|
239
|
-
4. \`codeloop_capture_screenshot\` — visual evidence
|
|
240
|
-
5. \`codeloop_gate_check\` — final quality gate
|
|
241
|
-
`;
|
|
242
|
-
const WEB_RULE = `---
|
|
243
|
-
description: CodeLoop web project verification
|
|
244
|
-
globs: "**/*.{ts,tsx,js,jsx,html,css}"
|
|
245
|
-
alwaysApply: false
|
|
246
|
-
---
|
|
247
|
-
|
|
248
|
-
# CodeLoop Web Rules
|
|
249
|
-
|
|
250
|
-
## Verification for web projects
|
|
251
|
-
|
|
252
|
-
- Use platform "web" with \`codeloop_verify\`
|
|
253
|
-
- Playwright tests are the primary verification method
|
|
254
|
-
- Capture screenshots at key viewports (375, 390, 1440)
|
|
255
|
-
- Use \`codeloop_start_recording\` for interaction testing
|
|
256
|
-
- Interact with ALL elements during video capture
|
|
257
|
-
`;
|
|
258
|
-
function CLAUDE_RULE(platformNote) {
|
|
259
|
-
return `# CodeLoop Integration
|
|
260
|
-
|
|
261
|
-
This project uses CodeLoop for automated verification.
|
|
262
|
-
|
|
263
|
-
## After every code change
|
|
264
|
-
|
|
265
|
-
1. Call \`codeloop_verify\` to run tests and static analysis
|
|
266
|
-
2. Call \`codeloop_capture_screenshot\` for visual evidence
|
|
267
|
-
3. If tests fail, call \`codeloop_diagnose\` and fix issues
|
|
268
|
-
4. Call \`codeloop_gate_check\` before marking complete${platformNote}
|
|
269
|
-
|
|
270
|
-
## Never skip verification after code changes.
|
|
271
|
-
`;
|
|
47
|
+
if (!hasCursor && !hasClaude)
|
|
48
|
+
return "unknown";
|
|
49
|
+
return detectIDE(cwd);
|
|
272
50
|
}
|
|
273
51
|
//# sourceMappingURL=init-project.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init-project.js","sourceRoot":"","sources":["../../src/tools/init-project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"init-project.js","sourceRoot":"","sources":["../../src/tools/init-project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EACL,YAAY,EACZ,SAAS,GAEV,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,aAAa,EAAoB,MAAM,0CAA0C,CAAC;AAiB3F,MAAM,mBAAmB,GAAkB;IACzC,SAAS;IACT,KAAK;IACL,MAAM;IACN,QAAQ;IACR,OAAO;IACP,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,OAAO;IACP,MAAM;IACN,SAAS;CACV,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,KAAuB;IAEvB,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC;IAC9B,MAAM,SAAS,GAAG,KAAK,CAAC,YAAY,CAAC;IACrC,MAAM,WAAW,GACf,CAAC,SAAS,IAAI,SAAS,KAAK,MAAM;QAChC,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI;QACzB,CAAC,CAAC,mBAAmB,CAAC,QAAQ,CAAC,SAAwB,CAAC;YACtD,CAAC,CAAE,SAAyB;YAC5B,CAAC,CAAC,SAAS,CAAC;IAElB,+EAA+E;IAC/E,MAAM,SAAS,GAAoB,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAE3D,MAAM,MAAM,GAAG,YAAY,CAAC;QAC1B,GAAG;QACH,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,IAAI;QAC5C,WAAW;KACZ,CAAC,CAAC;IAEH,OAAO;QACL,WAAW,EAAE,IAAI;QACjB,WAAW,EAAE,GAAG;QAChB,YAAY,EAAE,MAAM,CAAC,WAAW;QAChC,YAAY,EAAE,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;QAC9D,aAAa,EAAE,MAAM,CAAC,YAAY;QAClC,YAAY,EAAE,MAAM,CAAC,WAAW;QAChC,OAAO,EAAE,4BAA4B,MAAM,CAAC,WAAW,qBAAqB,MAAM,CAAC,YAAY,CAAC,MAAM,kBAAkB,MAAM,CAAC,WAAW,CAAC,MAAM,WAAW;KAC7J,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAW;IACrC,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC;IACnD,MAAM,SAAS,GACb,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC;IACzE,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IAC/C,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC;AACxB,CAAC"}
|
package/dist/tools/verify.d.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import type { VerifyInput, VerifyOutput, CodeLoopConfig } from "@codelooptech/shared";
|
|
2
2
|
export declare function detectPlatform(cwd: string): string;
|
|
3
3
|
export declare function runVerify(input: VerifyInput, config: CodeLoopConfig, cwd?: string): Promise<VerifyOutput>;
|
|
4
|
+
/**
|
|
5
|
+
* Pick a single .sln (preferred) or .csproj target for `dotnet build` so we
|
|
6
|
+
* don't trigger MSB1011 ("Specify which project or solution file to use…")
|
|
7
|
+
* on workspaces that contain multiple solution files.
|
|
8
|
+
*
|
|
9
|
+
* Precedence:
|
|
10
|
+
* 1. `config.dotnet.solution` — explicit user override.
|
|
11
|
+
* 2. Single .sln at cwd → use it.
|
|
12
|
+
* 3. Multiple .sln → prefer the one whose basename matches the directory
|
|
13
|
+
* name; otherwise prefer non "-Dev" / "-Debug" / "-Test" variants;
|
|
14
|
+
* otherwise pick the alphabetically-first remaining candidate.
|
|
15
|
+
* 4. Same heuristic on .csproj at cwd.
|
|
16
|
+
* 5. Recursive scan under cwd looking for a single .csproj (skipping
|
|
17
|
+
* bin/, obj/, node_modules/).
|
|
18
|
+
* 6. Returns null only if we cannot tell — caller marks the run as
|
|
19
|
+
* skipped with a clear remediation hint.
|
|
20
|
+
*/
|
|
21
|
+
export declare function pickDotnetTarget(cwd: string, config?: CodeLoopConfig): {
|
|
22
|
+
file: string;
|
|
23
|
+
reason: string;
|
|
24
|
+
} | null;
|
|
4
25
|
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/tools/verify.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAYtF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA0BlD;AAQD,wBAAsB,SAAS,CAC7B,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,cAAc,EACtB,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA+PvB"}
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/tools/verify.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAYtF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA0BlD;AAQD,wBAAsB,SAAS,CAC7B,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,cAAc,EACtB,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA+PvB;AA6LD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,cAAc,GACtB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAoCzC"}
|
package/dist/tools/verify.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, copyFileSync, mkdirSync, readFileSync } from "fs";
|
|
1
|
+
import { existsSync, readdirSync, copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
|
|
2
2
|
import { join, basename, dirname } from "path";
|
|
3
3
|
import { createRunDir, saveRunMeta, getArtifactsBaseDir } from "../evidence/artifacts.js";
|
|
4
4
|
import { buildLineage, appendToIndex } from "../evidence/run_lineage.js";
|
|
@@ -95,7 +95,7 @@ export async function runVerify(input, config, cwd = process.cwd()) {
|
|
|
95
95
|
results.push(await runNativeBuild("android", cwd, join(logDir, "gradle_build.log")));
|
|
96
96
|
}
|
|
97
97
|
if (platform === "dotnet" || input.scope === "full") {
|
|
98
|
-
results.push(await runNativeBuild("dotnet", cwd, join(logDir, "dotnet_build.log")));
|
|
98
|
+
results.push(await runNativeBuild("dotnet", cwd, join(logDir, "dotnet_build.log"), config));
|
|
99
99
|
}
|
|
100
100
|
// Custom plugin runners (Phase 6 §39) — registered via .codeloop/plugins.json.
|
|
101
101
|
// Each plugin's `detect()` decides whether it applies to the current cwd.
|
|
@@ -294,7 +294,7 @@ function determineBuildStatus(results) {
|
|
|
294
294
|
const anyFailed = buildResults.some(r => r.exit_code !== 0);
|
|
295
295
|
return anyFailed ? "failed" : "passed";
|
|
296
296
|
}
|
|
297
|
-
async function runNativeBuild(nativePlatform, cwd, logPath) {
|
|
297
|
+
async function runNativeBuild(nativePlatform, cwd, logPath, config) {
|
|
298
298
|
const { runCommand, checkToolAvailable, makeSkippedResult } = await import("../runners/base.js");
|
|
299
299
|
if (nativePlatform === "xcode") {
|
|
300
300
|
if (!hasFileWith(cwd, ".xcodeproj") && !hasFileWith(cwd, ".xcworkspace")) {
|
|
@@ -363,13 +363,31 @@ async function runNativeBuild(nativePlatform, cwd, logPath) {
|
|
|
363
363
|
if (!(await checkToolAvailable("dotnet"))) {
|
|
364
364
|
return makeSkippedResult("dotnet_build", "dotnet CLI not found — install .NET SDK: https://dotnet.microsoft.com/download");
|
|
365
365
|
}
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
// Pick a single .sln / .csproj target so `dotnet build` doesn't fail
|
|
367
|
+
// with MSB1011 ("Specify which project or solution file to use…")
|
|
368
|
+
// when the workspace contains multiple solutions (e.g. App.sln +
|
|
369
|
+
// App-Dev.sln). Order of precedence:
|
|
370
|
+
// 1. config.dotnet.solution (explicit override from .codeloop/config.json)
|
|
371
|
+
// 2. exactly one .sln in cwd
|
|
372
|
+
// 3. heuristic pick from the .sln set (prefers non-Dev / non-Test /
|
|
373
|
+
// shortest name)
|
|
374
|
+
// 4. exactly one .csproj at root (for project-only repos)
|
|
375
|
+
// 5. heuristic pick from the .csproj set
|
|
376
|
+
const target = pickDotnetTarget(cwd, config);
|
|
377
|
+
if (!target) {
|
|
378
|
+
return makeSkippedResult("dotnet_build", "Multiple .sln/.csproj files found and none could be picked automatically. " +
|
|
379
|
+
"Set 'dotnet.solution' in .codeloop/config.json to the file you want CodeLoop to build.");
|
|
380
|
+
}
|
|
381
|
+
const buildArgs = ["build", target.file, "--nologo"];
|
|
382
|
+
const testArgs = ["test", target.file, "--nologo", "--no-build"];
|
|
383
|
+
const buildResult = await runCommand("dotnet", buildArgs, cwd, logPath);
|
|
384
|
+
const testResult = await runCommand("dotnet", testArgs, cwd);
|
|
368
385
|
const testOutput = testResult.stdout + testResult.stderr;
|
|
369
386
|
const passedMatch = testOutput.match(/Passed:\s+(\d+)/);
|
|
370
387
|
const failedMatch = testOutput.match(/Failed:\s+(\d+)/);
|
|
371
388
|
const passed = (buildResult.exit_code === 0 ? 1 : 0) + (passedMatch ? parseInt(passedMatch[1], 10) : 0);
|
|
372
389
|
const failed = (buildResult.exit_code !== 0 ? 1 : 0) + (failedMatch ? parseInt(failedMatch[1], 10) : 0);
|
|
390
|
+
const targetNote = `Built ${target.file} (${target.reason}).`;
|
|
373
391
|
return {
|
|
374
392
|
runner_name: "dotnet_build",
|
|
375
393
|
available: true,
|
|
@@ -379,12 +397,142 @@ async function runNativeBuild(nativePlatform, cwd, logPath) {
|
|
|
379
397
|
skipped: 0,
|
|
380
398
|
log_path: logPath,
|
|
381
399
|
duration_ms: buildResult.duration_ms,
|
|
382
|
-
stdout: buildResult.stdout + "\n" + testResult.stdout,
|
|
400
|
+
stdout: targetNote + "\n" + buildResult.stdout + "\n" + testResult.stdout,
|
|
383
401
|
stderr: buildResult.stderr + "\n" + testResult.stderr,
|
|
384
402
|
};
|
|
385
403
|
}
|
|
386
404
|
return makeSkippedResult("native_build", `Unknown native platform: ${nativePlatform}`);
|
|
387
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Pick a single .sln (preferred) or .csproj target for `dotnet build` so we
|
|
408
|
+
* don't trigger MSB1011 ("Specify which project or solution file to use…")
|
|
409
|
+
* on workspaces that contain multiple solution files.
|
|
410
|
+
*
|
|
411
|
+
* Precedence:
|
|
412
|
+
* 1. `config.dotnet.solution` — explicit user override.
|
|
413
|
+
* 2. Single .sln at cwd → use it.
|
|
414
|
+
* 3. Multiple .sln → prefer the one whose basename matches the directory
|
|
415
|
+
* name; otherwise prefer non "-Dev" / "-Debug" / "-Test" variants;
|
|
416
|
+
* otherwise pick the alphabetically-first remaining candidate.
|
|
417
|
+
* 4. Same heuristic on .csproj at cwd.
|
|
418
|
+
* 5. Recursive scan under cwd looking for a single .csproj (skipping
|
|
419
|
+
* bin/, obj/, node_modules/).
|
|
420
|
+
* 6. Returns null only if we cannot tell — caller marks the run as
|
|
421
|
+
* skipped with a clear remediation hint.
|
|
422
|
+
*/
|
|
423
|
+
export function pickDotnetTarget(cwd, config) {
|
|
424
|
+
// 1. Explicit override.
|
|
425
|
+
const override = config
|
|
426
|
+
?.dotnet?.solution;
|
|
427
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
428
|
+
const abs = join(cwd, override);
|
|
429
|
+
if (existsSync(abs)) {
|
|
430
|
+
return { file: override, reason: `pinned by config.dotnet.solution` };
|
|
431
|
+
}
|
|
432
|
+
// Fall through if config points at a missing file — better to pick
|
|
433
|
+
// something than to fail with a confusing "file does not exist".
|
|
434
|
+
}
|
|
435
|
+
let entries = [];
|
|
436
|
+
try {
|
|
437
|
+
entries = readdirSync(cwd);
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const slns = entries.filter((f) => f.toLowerCase().endsWith(".sln"));
|
|
443
|
+
const csprojs = entries.filter((f) => f.toLowerCase().endsWith(".csproj"));
|
|
444
|
+
const picked = pickPreferred(slns, cwd) ?? pickPreferred(csprojs, cwd);
|
|
445
|
+
if (picked)
|
|
446
|
+
return picked;
|
|
447
|
+
// 5. Recursive single-csproj fallback (one project nested inside cwd).
|
|
448
|
+
const nested = findSingleCsprojRecursive(cwd, 3);
|
|
449
|
+
if (nested) {
|
|
450
|
+
return {
|
|
451
|
+
file: nested,
|
|
452
|
+
reason: "single nested .csproj found",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
function pickPreferred(candidates, cwd) {
|
|
458
|
+
if (candidates.length === 0)
|
|
459
|
+
return null;
|
|
460
|
+
if (candidates.length === 1) {
|
|
461
|
+
return { file: candidates[0], reason: "only candidate" };
|
|
462
|
+
}
|
|
463
|
+
// 3a. Prefer the one whose basename (without extension) matches the
|
|
464
|
+
// workspace directory name — that's almost always the canonical solution.
|
|
465
|
+
const dirName = basename(cwd);
|
|
466
|
+
const dirMatch = candidates.find((f) => baseNoExt(f).toLowerCase() === dirName.toLowerCase());
|
|
467
|
+
if (dirMatch) {
|
|
468
|
+
return { file: dirMatch, reason: `matches directory name "${dirName}"` };
|
|
469
|
+
}
|
|
470
|
+
// 3b. Filter out obvious dev/test/debug variants.
|
|
471
|
+
const SUFFIXES = ["-dev", ".dev", "-debug", ".debug", "-test", ".test", "-tests", ".tests"];
|
|
472
|
+
const nonDev = candidates.filter((f) => {
|
|
473
|
+
const stem = baseNoExt(f).toLowerCase();
|
|
474
|
+
return !SUFFIXES.some((sfx) => stem.endsWith(sfx));
|
|
475
|
+
});
|
|
476
|
+
if (nonDev.length === 1) {
|
|
477
|
+
return {
|
|
478
|
+
file: nonDev[0],
|
|
479
|
+
reason: `picked non-dev variant from ${candidates.length} candidates`,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
// 3c. Last resort — alphabetical first of whatever's left.
|
|
483
|
+
const pool = nonDev.length > 0 ? nonDev : candidates;
|
|
484
|
+
pool.sort((a, b) => a.localeCompare(b));
|
|
485
|
+
return {
|
|
486
|
+
file: pool[0],
|
|
487
|
+
reason: `auto-picked first of ${candidates.length} candidates (set config.dotnet.solution to override)`,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function baseNoExt(file) {
|
|
491
|
+
const ix = file.lastIndexOf(".");
|
|
492
|
+
return ix === -1 ? file : file.slice(0, ix);
|
|
493
|
+
}
|
|
494
|
+
function findSingleCsprojRecursive(root, depth) {
|
|
495
|
+
const found = [];
|
|
496
|
+
const skip = new Set(["node_modules", "bin", "obj", ".git", "artifacts", ".codeloop"]);
|
|
497
|
+
const walk = (dir, remaining) => {
|
|
498
|
+
if (remaining < 0)
|
|
499
|
+
return;
|
|
500
|
+
let entries = [];
|
|
501
|
+
try {
|
|
502
|
+
entries = readdirSync(dir);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
for (const entry of entries) {
|
|
508
|
+
if (skip.has(entry))
|
|
509
|
+
continue;
|
|
510
|
+
const full = join(dir, entry);
|
|
511
|
+
let stat;
|
|
512
|
+
try {
|
|
513
|
+
stat = statSync(full);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (stat.isDirectory()) {
|
|
519
|
+
walk(full, remaining - 1);
|
|
520
|
+
if (found.length > 1)
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
else if (entry.toLowerCase().endsWith(".csproj")) {
|
|
524
|
+
found.push(full);
|
|
525
|
+
if (found.length > 1)
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
walk(root, depth);
|
|
531
|
+
if (found.length !== 1)
|
|
532
|
+
return null;
|
|
533
|
+
// Return relative-ish path so logs are readable.
|
|
534
|
+
return found[0].startsWith(root) ? found[0].slice(root.length + 1) : found[0];
|
|
535
|
+
}
|
|
388
536
|
function findPngFiles(dir) {
|
|
389
537
|
const results = [];
|
|
390
538
|
if (!existsSync(dir))
|