Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Contracts;
3
4namespace CascadeIDE.Features.Workspace.Application;
5
6/// <summary>
7/// <c>dotnet new</c> + <c>dotnet sln add</c> в открытое решение (ADR 0107 §6, ADR 0125 W2).
8/// </summary>
9public static class ProjectInSolutionCreator
10{
11 public static readonly IReadOnlySet<string> WhitelistedTemplates =
12 new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "console", "classlib", "webapi" };
13
14 public static async Task<ProjectInSolutionCreateResult> TryCreateAsync(
15 string? solutionFilePath,
16 string template,
17 string projectName,
18 IDotnetCommandRunner dotnetRunner,
19 CancellationToken cancellationToken = default)
20 {
21 ArgumentNullException.ThrowIfNull(dotnetRunner);
22
23 if (string.IsNullOrWhiteSpace(solutionFilePath))
24 return ProjectInSolutionCreateResult.Failure("Сначала открой решение (.sln).");
25
26 if (!WhitelistedTemplates.Contains(template.Trim()))
27 {
28 return ProjectInSolutionCreateResult.Failure(
29 "Шаблон не поддерживается. Доступны: console, classlib, webapi.");
30 }
31
32 var name = projectName.Trim();
33 if (string.IsNullOrWhiteSpace(name))
34 return ProjectInSolutionCreateResult.Failure("Укажи имя проекта в хвосте: /solution new console MyApp");
35
36 string slnPath;
37 try
38 {
39 slnPath = CanonicalFilePath.Normalize(solutionFilePath.Trim());
40 }
41 catch (Exception ex)
42 {
43 return ProjectInSolutionCreateResult.Failure("Некорректный путь к решению: " + ex.Message);
44 }
45
46 if (!slnPath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
47 return ProjectInSolutionCreateResult.Failure("Нужен открытый файл .sln.");
48
49 if (!File.Exists(slnPath))
50 return ProjectInSolutionCreateResult.Failure("Файл решения не найден: " + slnPath);
51
52 var solutionDir = Path.GetDirectoryName(slnPath);
53 if (string.IsNullOrWhiteSpace(solutionDir))
54 return ProjectInSolutionCreateResult.Failure("Не удалось определить каталог решения.");
55
56 var outputDir = Path.Combine(solutionDir, name);
57 if (Directory.Exists(outputDir) || File.Exists(outputDir))
58 return ProjectInSolutionCreateResult.Failure("Каталог или файл уже существует: " + outputDir);
59
60 var templateId = template.Trim().ToLowerInvariant();
61 IReadOnlyList<string> newArgs = ["new", templateId, "-n", name, "-o", outputDir];
62 var (newOk, newExit, newOutput) = await dotnetRunner
63 .RunAsync(newArgs, workingDirectory: solutionDir, cancellationToken)
64 .ConfigureAwait(false);
65
66 if (!newOk)
67 {
68 var tail = string.IsNullOrWhiteSpace(newOutput) ? "" : "\r\n" + newOutput.Trim();
69 return ProjectInSolutionCreateResult.Failure($"dotnet new {templateId} завершился с кодом {newExit}.{tail}");
70 }
71
72 var csproj = Directory.EnumerateFiles(outputDir, "*.csproj", SearchOption.TopDirectoryOnly)
73 .FirstOrDefault();
74 if (string.IsNullOrWhiteSpace(csproj))
75 {
76 return ProjectInSolutionCreateResult.Failure(
77 "Проект создан, но .csproj не найден в " + outputDir);
78 }
79
80 IReadOnlyList<string> slnAddArgs = ["sln", "add", csproj];
81 var (addOk, addExit, addOutput) = await dotnetRunner
82 .RunAsync(slnAddArgs, workingDirectory: solutionDir, cancellationToken)
83 .ConfigureAwait(false);
84
85 if (!addOk)
86 {
87 var tail = string.IsNullOrWhiteSpace(addOutput) ? "" : "\r\n" + addOutput.Trim();
88 return ProjectInSolutionCreateResult.Failure($"dotnet sln add завершился с кодом {addExit}.{tail}");
89 }
90
91 return ProjectInSolutionCreateResult.Success(csproj);
92 }
93}
94
95public readonly record struct ProjectInSolutionCreateResult(bool Ok, string? ProjectPath, string? ErrorMessage)
96{
97 public static ProjectInSolutionCreateResult Success(string projectPath) => new(true, projectPath, null);
98
99 public static ProjectInSolutionCreateResult Failure(string message) => new(false, null, message);
100}
101
View only · write via MCP/CIDE