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.
@@ -1,273 +1,51 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from "fs";
2
- import { join, dirname } from "path";
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 projectType = input.project_type === "auto" || !input.project_type
6
- ? detectProjectType(cwd)
7
- : input.project_type;
8
- const ide = detectIDE(cwd);
9
- const filesCreated = [];
10
- const filesMerged = [];
11
- // 1. Create .codeloop/config.json
12
- const configPath = join(cwd, ".codeloop", "config.json");
13
- if (!existsSync(configPath)) {
14
- const apiKey = process.env.CODELOOP_API_KEY || "";
15
- const config = createConfig(apiKey, projectType);
16
- writeFileEnsureDir(configPath, JSON.stringify(config, null, 2) + "\n");
17
- filesCreated.push(".codeloop/config.json");
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 detectProjectType(cwd) {
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 "both";
90
- if (hasCursor)
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":"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"}
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"}
@@ -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"}
@@ -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
- const buildResult = await runCommand("dotnet", ["build", "--nologo"], cwd, logPath);
367
- const testResult = await runCommand("dotnet", ["test", "--nologo", "--no-build"], cwd);
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))