stackforge-cli 0.1.0__tar.gz

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.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: stackforge-cli
3
+ Version: 0.1.0
4
+ Summary: A CLI scaffolder for .NET Clean Architecture projects
5
+ License: MIT
6
+ Keywords: scaffolder,dotnet,clean-architecture,cli
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Environment :: Console
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+
14
+ # StackForge
15
+
16
+ A CLI scaffolder for .NET Clean Architecture (CQRS) projects.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install stackforge
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ stackforge
28
+ ```
29
+
30
+ Then follow the prompts to scaffold a new project.
31
+
32
+ ## What it generates
33
+
34
+ - .NET 10 Clean Architecture structure (API, Application, Domain, Infrastructure, Persistence)
35
+ - JWT authentication with ASP.NET Identity
36
+ - Swagger UI with Bearer token support
37
+ - PostgreSQL with EF Core
38
+ - Docker + docker-compose with healthchecks
39
+ - Role and user seeders (SuperAdmin out of the box)
40
+ - Git initialization with initial commit
41
+
42
+ ## Quickstart after scaffolding
43
+
44
+ 1. Open the project in VS Code or Visual Studio
45
+ 2. Run: `docker-compose up --build`
46
+ 3. Visit: `http://localhost:5000/swagger`
@@ -0,0 +1,33 @@
1
+ # StackForge
2
+
3
+ A CLI scaffolder for .NET Clean Architecture (CQRS) projects.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install stackforge
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ stackforge
15
+ ```
16
+
17
+ Then follow the prompts to scaffold a new project.
18
+
19
+ ## What it generates
20
+
21
+ - .NET 10 Clean Architecture structure (API, Application, Domain, Infrastructure, Persistence)
22
+ - JWT authentication with ASP.NET Identity
23
+ - Swagger UI with Bearer token support
24
+ - PostgreSQL with EF Core
25
+ - Docker + docker-compose with healthchecks
26
+ - Role and user seeders (SuperAdmin out of the box)
27
+ - Git initialization with initial commit
28
+
29
+ ## Quickstart after scaffolding
30
+
31
+ 1. Open the project in VS Code or Visual Studio
32
+ 2. Run: `docker-compose up --build`
33
+ 3. Visit: `http://localhost:5000/swagger`
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "stackforge-cli"
7
+ version = "0.1.0"
8
+ description = "A CLI scaffolder for .NET Clean Architecture projects"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ keywords = ["scaffolder", "dotnet", "clean-architecture", "cli"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Environment :: Console",
18
+ ]
19
+
20
+ [project.scripts]
21
+ stackforge = "stackforge.scaffold:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,8 @@
1
+ from .scaffolder import scaffold_folders
2
+ from .csproj_generator import generate_csproj
3
+ from .program_generator import generate_configure_services, generate_program_cs
4
+ from .docker_generator import generate_dockerfile, generate_docker_compose
5
+ from .config_generator import generate_env, generate_appsettings, generate_appsettings_development
6
+ from .git_generator import generate_gitignore, git_init
7
+ from .api_generator import generate_api_structure
8
+ from .layers_generator import generate_domain_layer, generate_application_layer, generate_infrastructure_layer, generate_persistence_layer
@@ -0,0 +1,312 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def generate_api_structure(base: Path, project: str):
5
+ # Folders
6
+ folders = [
7
+ "API/Configurations/Swagger",
8
+ "API/Controllers/Main",
9
+ "API/Helpers",
10
+ "API/Interface",
11
+ "API/Options",
12
+ "API/Properties",
13
+ "API/Services",
14
+ "API/Swagger",
15
+ "API/DTOs",
16
+ ]
17
+
18
+ for folder in folders:
19
+ (base / folder).mkdir(parents=True, exist_ok=True)
20
+
21
+ _generate_base_controller(base, project)
22
+ _generate_jwt_options(base, project)
23
+ _generate_ijwt_service(base, project)
24
+ _generate_jwt_service(base, project)
25
+ _generate_enum_schema_filter(base, project)
26
+ _generate_api_roles(base, project)
27
+ _generate_launch_settings(base)
28
+ _generate_login_dto(base, project)
29
+ _generate_account_controller(base, project)
30
+
31
+ print(" [+] API/ structure and files")
32
+
33
+
34
+ def _generate_base_controller(base: Path, project: str):
35
+ content = f"""using Microsoft.AspNetCore.Mvc;
36
+ using System.Security.Claims;
37
+
38
+ namespace {project}.API.Controllers
39
+ {{
40
+ [Route("api/[controller]")]
41
+ [ApiController]
42
+ public class BaseApiController : ControllerBase
43
+ {{
44
+ protected Guid GetUserId() => Guid.Parse(User?.FindFirst(ClaimTypes.NameIdentifier)?.Value!);
45
+
46
+ protected string GetCurrentEmail() => User?.FindFirstValue(ClaimTypes.Email)!;
47
+
48
+ protected Guid? GetOrganizationId()
49
+ {{
50
+ var claim = User?.FindFirst("OrganizationId");
51
+
52
+ if (claim == null || !Guid.TryParse(claim.Value, out var orgId))
53
+ return null;
54
+
55
+ return orgId;
56
+ }}
57
+ }}
58
+ }}
59
+ """
60
+ (base / "API/Controllers/BaseApiController.cs").write_text(content)
61
+
62
+
63
+ def _generate_jwt_options(base: Path, project: str):
64
+ content = f"""namespace {project}.API.Options
65
+ {{
66
+ public class JwtOptions
67
+ {{
68
+ public string Issuer {{ get; set; }} = string.Empty;
69
+ public string Audience {{ get; set; }} = string.Empty;
70
+ public string SecretKey {{ get; set; }} = string.Empty;
71
+ public int ExpiryMinutes {{ get; set; }}
72
+ }}
73
+ }}
74
+ """
75
+ (base / "API/Options/JwtOptions.cs").write_text(content)
76
+
77
+
78
+ def _generate_ijwt_service(base: Path, project: str):
79
+ content = f"""namespace {project}.API.Interface
80
+ {{
81
+ public interface IJwtService
82
+ {{
83
+ string GenerateToken(Guid userId, string email);
84
+ }}
85
+ }}
86
+ """
87
+ (base / "API/Interface/IJwtService.cs").write_text(content)
88
+
89
+
90
+ def _generate_jwt_service(base: Path, project: str):
91
+ content = f"""using {project}.API.Interface;
92
+ using {project}.API.Options;
93
+ using Microsoft.Extensions.Options;
94
+ using Microsoft.IdentityModel.Tokens;
95
+ using System.IdentityModel.Tokens.Jwt;
96
+ using System.Security.Claims;
97
+ using System.Text;
98
+
99
+ namespace {project}.API.Services
100
+ {{
101
+ public class JwtService : IJwtService
102
+ {{
103
+ private readonly JwtOptions _options;
104
+
105
+ public JwtService(IOptions<JwtOptions> options)
106
+ {{
107
+ _options = options.Value;
108
+ }}
109
+
110
+ public string GenerateToken(Guid userId, string email)
111
+ {{
112
+ var claims = new[]
113
+ {{
114
+ new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
115
+ new Claim(ClaimTypes.Email, email),
116
+ }};
117
+
118
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SecretKey));
119
+ var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
120
+
121
+ var token = new JwtSecurityToken(
122
+ issuer: _options.Issuer,
123
+ audience: _options.Audience,
124
+ claims: claims,
125
+ expires: DateTime.UtcNow.AddMinutes(_options.ExpiryMinutes),
126
+ signingCredentials: creds);
127
+
128
+ return new JwtSecurityTokenHandler().WriteToken(token);
129
+ }}
130
+ }}
131
+ }}
132
+ """
133
+ (base / "API/Services/JwtService.cs").write_text(content)
134
+
135
+
136
+ def _generate_enum_schema_filter(base: Path, project: str):
137
+ content = f"""using Microsoft.OpenApi.Any;
138
+ using Microsoft.OpenApi.Models;
139
+ using Swashbuckle.AspNetCore.SwaggerGen;
140
+ using System.Runtime.Serialization;
141
+
142
+ namespace {project}.API.Swagger
143
+ {{
144
+ public class EnumMemberSchemaFilter : ISchemaFilter
145
+ {{
146
+ public void Apply(OpenApiSchema schema, SchemaFilterContext context)
147
+ {{
148
+ if (!context.Type.IsEnum) return;
149
+
150
+ schema.Enum.Clear();
151
+ foreach (var member in Enum.GetNames(context.Type))
152
+ {{
153
+ var field = context.Type.GetField(member);
154
+ var attr = field?.GetCustomAttributes(typeof(EnumMemberAttribute), false)
155
+ .OfType<EnumMemberAttribute>()
156
+ .FirstOrDefault();
157
+ schema.Enum.Add(new OpenApiString(attr?.Value ?? member));
158
+ }}
159
+ }}
160
+ }}
161
+ }}
162
+ """
163
+ (base / "API/Swagger/EnumMemberSchemaFilter.cs").write_text(content)
164
+
165
+
166
+ def _generate_api_roles(base: Path, project: str):
167
+ content = f"""namespace {project}.API.Configurations.Swagger
168
+ {{
169
+ public static class ApiRoles
170
+ {{
171
+ public const string SuperAdmin = "SuperAdmin";
172
+ public const string Admin = "Admin";
173
+ public const string User = "User";
174
+ }}
175
+ }}
176
+ """
177
+ (base / "API/Configurations/Swagger/ApiRoles.cs").write_text(content)
178
+
179
+
180
+ def _generate_launch_settings(base: Path):
181
+ content = """{
182
+ "$schema": "https://json.schemastore.org/launchsettings.json",
183
+ "profiles": {
184
+ "http": {
185
+ "commandName": "Project",
186
+ "environmentVariables": {
187
+ "ASPNETCORE_ENVIRONMENT": "Development"
188
+ },
189
+ "dotnetRunMessages": true,
190
+ "applicationUrl": "http://localhost:5000"
191
+ },
192
+ "https": {
193
+ "commandName": "Project",
194
+ "environmentVariables": {
195
+ "ASPNETCORE_ENVIRONMENT": "Development"
196
+ },
197
+ "dotnetRunMessages": true,
198
+ "applicationUrl": "https://localhost:7013;http://localhost:5282"
199
+ },
200
+ "Container (Dockerfile)": {
201
+ "commandName": "Docker",
202
+ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
203
+ "environmentVariables": {
204
+ "ASPNETCORE_HTTPS_PORTS": "8081",
205
+ "ASPNETCORE_HTTP_PORTS": "8080"
206
+ },
207
+ "publishAllPorts": true,
208
+ "useSSL": true
209
+ }
210
+ }
211
+ }
212
+ """
213
+ (base / "API/Properties/launchSettings.json").write_text(content)
214
+
215
+ def _generate_login_dto(base: Path, project: str):
216
+ content = f"""namespace {project}.API.DTOs
217
+ {{
218
+ public class LoginRequest
219
+ {{
220
+ public string Email {{ get; set; }} = string.Empty;
221
+ public string Password {{ get; set; }} = string.Empty;
222
+ }}
223
+
224
+ public class LoginResponse
225
+ {{
226
+ public string Token {{ get; set; }} = string.Empty;
227
+ public string Email {{ get; set; }} = string.Empty;
228
+ public string FullName {{ get; set; }} = string.Empty;
229
+ }}
230
+
231
+ public class CurrentUserResponse
232
+ {{
233
+ public Guid Id {{ get; set; }}
234
+ public string Email {{ get; set; }} = string.Empty;
235
+ public string FullName {{ get; set; }} = string.Empty;
236
+ public IList<string> Roles {{ get; set; }} = new List<string>();
237
+ }}
238
+ }}
239
+ """
240
+ (base / "API/DTOs/AccountDTOs.cs").write_text(content)
241
+
242
+
243
+ def _generate_account_controller(base: Path, project: str):
244
+ content = f"""using {project}.API.Controllers;
245
+ using {project}.API.DTOs;
246
+ using {project}.API.Interface;
247
+ using {project}.Domain.Entities.Identity;
248
+ using Microsoft.AspNetCore.Authorization;
249
+ using Microsoft.AspNetCore.Identity;
250
+ using Microsoft.AspNetCore.Mvc;
251
+
252
+ namespace {project}.API.Controllers.Main
253
+ {{
254
+ public class AccountController : BaseApiController
255
+ {{
256
+ private readonly UserManager<User> _userManager;
257
+ private readonly SignInManager<User> _signInManager;
258
+ private readonly IJwtService _jwtService;
259
+
260
+ public AccountController(
261
+ UserManager<User> userManager,
262
+ SignInManager<User> signInManager,
263
+ IJwtService jwtService)
264
+ {{
265
+ _userManager = userManager;
266
+ _signInManager = signInManager;
267
+ _jwtService = jwtService;
268
+ }}
269
+
270
+ [HttpPost("login")]
271
+ public async Task<IActionResult> Login([FromBody] LoginRequest request)
272
+ {{
273
+ var user = await _userManager.FindByEmailAsync(request.Email);
274
+ if (user == null)
275
+ return Unauthorized("Invalid credentials.");
276
+
277
+ var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
278
+ if (!result.Succeeded)
279
+ return Unauthorized("Invalid credentials.");
280
+
281
+ var token = _jwtService.GenerateToken(user.Id, user.Email!);
282
+
283
+ return Ok(new LoginResponse
284
+ {{
285
+ Token = token,
286
+ Email = user.Email!,
287
+ FullName = $"{{user.FirstName}} {{user.LastName}}"
288
+ }});
289
+ }}
290
+
291
+ [Authorize]
292
+ [HttpGet("me")]
293
+ public async Task<IActionResult> Me()
294
+ {{
295
+ var user = await _userManager.FindByIdAsync(GetUserId().ToString());
296
+ if (user == null)
297
+ return NotFound("User not found.");
298
+
299
+ var roles = await _userManager.GetRolesAsync(user);
300
+
301
+ return Ok(new CurrentUserResponse
302
+ {{
303
+ Id = user.Id,
304
+ Email = user.Email!,
305
+ FullName = $"{{user.FirstName}} {{user.LastName}}",
306
+ Roles = roles
307
+ }});
308
+ }}
309
+ }}
310
+ }}
311
+ """
312
+ (base / "API/Controllers/Main/AccountController.cs").write_text(content)
@@ -0,0 +1,76 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def generate_env(base: Path, project: str):
5
+ project_lower = project.lower()
6
+
7
+ content = f"""ASPNETCORE_ENVIRONMENT=Development
8
+ DB_HOST=db
9
+ DB_PORT=5432
10
+ DB_NAME={project_lower}_db
11
+ DB_USER=postgres
12
+ DB_PASSWORD=postgres
13
+ JWT_ISSUER=https://localhost:5000
14
+ JWT_AUDIENCE=https://localhost:5000
15
+ JWT_SECRET_KEY=your-super-secret-key-change-this-in-production
16
+ JWT_EXPIRY_MINUTES=60
17
+ """
18
+
19
+ file_path = base / ".env"
20
+ file_path.write_text(content)
21
+ print(" [+] .env")
22
+
23
+
24
+ def generate_appsettings(base: Path, project: str):
25
+ project_lower = project.lower()
26
+
27
+ content = f"""{{
28
+ "ConnectionStrings": {{
29
+ "DefaultConnection": "Host=db;Port=5432;Database={project_lower}_db;Username=postgres;Password=postgres"
30
+ }},
31
+ "Jwt": {{
32
+ "Issuer": "https://localhost:5000",
33
+ "Audience": "https://localhost:5000",
34
+ "SecretKey": "your-super-secret-key-change-this-in-production",
35
+ "ExpiryMinutes": 60
36
+ }},
37
+ "Logging": {{
38
+ "LogLevel": {{
39
+ "Default": "Information",
40
+ "Microsoft.AspNetCore": "Warning"
41
+ }}
42
+ }},
43
+ "AllowedHosts": "*"
44
+ }}
45
+ """
46
+
47
+ file_path = base / "API" / "appsettings.json"
48
+ file_path.write_text(content)
49
+ print(" [+] API/appsettings.json")
50
+
51
+
52
+ def generate_appsettings_development(base: Path, project: str):
53
+ project_lower = project.lower()
54
+
55
+ content = f"""{{
56
+ "ConnectionStrings": {{
57
+ "DefaultConnection": "Host=localhost;Port=5432;Database={project_lower}_db;Username=postgres;Password=postgres"
58
+ }},
59
+ "Jwt": {{
60
+ "Issuer": "https://localhost:5000",
61
+ "Audience": "https://localhost:5000",
62
+ "SecretKey": "your-super-secret-key-change-this-in-production",
63
+ "ExpiryMinutes": 60
64
+ }},
65
+ "Logging": {{
66
+ "LogLevel": {{
67
+ "Default": "Information",
68
+ "Microsoft.AspNetCore": "Warning"
69
+ }}
70
+ }}
71
+ }}
72
+ """
73
+
74
+ file_path = base / "API" / "appsettings.Development.json"
75
+ file_path.write_text(content)
76
+ print(" [+] API/appsettings.Development.json")
@@ -0,0 +1,82 @@
1
+ from pathlib import Path
2
+
3
+
4
+ LAYER_CONFIGS = {
5
+ "Domain": {
6
+ "sdk": "Microsoft.NET.Sdk",
7
+ "references": [],
8
+ "packages": [
9
+ ("Microsoft.AspNetCore.Identity.EntityFrameworkCore", "10.0.4"),
10
+ ],
11
+ },
12
+ "Application": {
13
+ "sdk": "Microsoft.NET.Sdk",
14
+ "references": ["Domain"],
15
+ "packages": [
16
+ ("MediatR", "14.1.0"),
17
+ ("FluentValidation", "11.9.0"),
18
+ ("FluentValidation.DependencyInjectionExtensions", "11.9.0"),
19
+ ("AutoMapper", "16.1.1"),
20
+ ("Microsoft.Extensions.DependencyInjection.Abstractions", "10.0.4"),
21
+ ]
22
+ },
23
+ "Infrastructure": {
24
+ "sdk": "Microsoft.NET.Sdk",
25
+ "references": ["Application"],
26
+ "packages": [],
27
+ },
28
+ "Persistence": {
29
+ "sdk": "Microsoft.NET.Sdk",
30
+ "references": ["Application"],
31
+ "packages": [
32
+ ("Microsoft.EntityFrameworkCore", "10.0.4"),
33
+ ("Npgsql.EntityFrameworkCore.PostgreSQL", "10.0.2"),
34
+ ("Microsoft.EntityFrameworkCore.Tools", "10.0.4"),
35
+ ],
36
+ },
37
+ "API": {
38
+ "sdk": "Microsoft.NET.Sdk.Web",
39
+ "references": ["Application", "Infrastructure", "Persistence"],
40
+ "packages": [
41
+ ("Swashbuckle.AspNetCore", "6.5.0"),
42
+ ("Microsoft.AspNetCore.Authentication.JwtBearer", "10.0.4"),
43
+ ],
44
+ },
45
+ }
46
+
47
+
48
+ def generate_csproj(layer: str, base: Path):
49
+ config = LAYER_CONFIGS[layer]
50
+ sdk = config["sdk"]
51
+
52
+ project_refs = ""
53
+ for ref in config["references"]:
54
+ project_refs += f'\n <ProjectReference Include="..\\{ref}\\{ref}.csproj" />'
55
+
56
+ package_refs = ""
57
+ for name, version in config["packages"]:
58
+ package_refs += f'\n <PackageReference Include="{name}" Version="{version}" />'
59
+
60
+ item_group = ""
61
+ if project_refs:
62
+ item_group += f"""
63
+ <ItemGroup>{project_refs}
64
+ </ItemGroup>"""
65
+ if package_refs:
66
+ item_group += f"""
67
+ <ItemGroup>{package_refs}
68
+ </ItemGroup>"""
69
+
70
+ content = f"""<Project Sdk="{sdk}">
71
+
72
+ <PropertyGroup>
73
+ <TargetFramework>net10.0</TargetFramework>
74
+ <Nullable>enable</Nullable>
75
+ <ImplicitUsings>enable</ImplicitUsings>
76
+ </PropertyGroup>
77
+ {item_group}
78
+ </Project>"""
79
+
80
+ csproj_path = base / layer / f"{layer}.csproj"
81
+ csproj_path.write_text(content.strip())
82
+ print(f" [+] {layer}/{layer}.csproj")
@@ -0,0 +1,63 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def generate_dockerfile(base: Path):
5
+ content = """FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
6
+ WORKDIR /app
7
+
8
+ COPY . .
9
+
10
+ RUN dotnet publish API/API.csproj -c Release -o out
11
+
12
+ ENTRYPOINT ["dotnet", "out/API.dll"]
13
+ """
14
+
15
+ file_path = base / "Dockerfile"
16
+ file_path.write_text(content)
17
+ print(" [+] Dockerfile")
18
+
19
+
20
+ def generate_docker_compose(base: Path, project: str):
21
+ project_lower = project.lower()
22
+
23
+ content = f"""services:
24
+ api:
25
+ build: .
26
+ container_name: {project_lower}_api
27
+ ports:
28
+ - "5000:8080"
29
+ environment:
30
+ - ASPNETCORE_ENVIRONMENT=Development
31
+ - ConnectionStrings__DefaultConnection=Host=db;Port=5432;Database={project_lower}_db;Username=postgres;Password=postgres
32
+ - Jwt__Issuer=http://localhost:5000
33
+ - Jwt__Audience=http://localhost:5000
34
+ - Jwt__SecretKey=your-super-secret-key-change-this-in-production
35
+ - Jwt__ExpiryMinutes=60
36
+ depends_on:
37
+ db:
38
+ condition: service_healthy
39
+
40
+ db:
41
+ image: postgres:latest
42
+ container_name: {project_lower}_db
43
+ environment:
44
+ POSTGRES_DB: {project_lower}_db
45
+ POSTGRES_USER: postgres
46
+ POSTGRES_PASSWORD: postgres
47
+ ports:
48
+ - "5432:5432"
49
+ volumes:
50
+ - {project_lower}_pgdata:/var/lib/postgresql
51
+ healthcheck:
52
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
53
+ interval: 5s
54
+ timeout: 5s
55
+ retries: 5
56
+
57
+ volumes:
58
+ {project_lower}_pgdata:
59
+ """
60
+
61
+ file_path = base / "docker-compose.yml"
62
+ file_path.write_text(content)
63
+ print(" [+] docker-compose.yml")