stackforge-cli 0.1.1__tar.gz → 0.1.3__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.
Files changed (29) hide show
  1. stackforge_cli-0.1.3/PKG-INFO +107 -0
  2. stackforge_cli-0.1.3/README.md +93 -0
  3. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/pyproject.toml +1 -1
  4. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge/engines/dotnet/__init__.py +1 -1
  5. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge/engines/dotnet/api_generator.py +2 -1
  6. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge/engines/dotnet/config_generator.py +4 -3
  7. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge/engines/dotnet/csproj_generator.py +2 -1
  8. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge/engines/dotnet/docker_generator.py +3 -2
  9. stackforge_cli-0.1.3/src/stackforge/engines/dotnet/git_generator.py +111 -0
  10. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge/engines/dotnet/layers_generator.py +5 -4
  11. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge/engines/dotnet/program_generator.py +3 -2
  12. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge/engines/dotnet/scaffolder.py +19 -16
  13. stackforge_cli-0.1.3/src/stackforge/project_name.py +34 -0
  14. stackforge_cli-0.1.3/src/stackforge/scaffold.py +90 -0
  15. stackforge_cli-0.1.3/src/stackforge/ui.py +72 -0
  16. stackforge_cli-0.1.3/src/stackforge/version.py +48 -0
  17. stackforge_cli-0.1.3/src/stackforge_cli.egg-info/PKG-INFO +107 -0
  18. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge_cli.egg-info/SOURCES.txt +3 -0
  19. stackforge_cli-0.1.1/PKG-INFO +0 -47
  20. stackforge_cli-0.1.1/README.md +0 -33
  21. stackforge_cli-0.1.1/src/stackforge/engines/dotnet/git_generator.py +0 -67
  22. stackforge_cli-0.1.1/src/stackforge/scaffold.py +0 -83
  23. stackforge_cli-0.1.1/src/stackforge_cli.egg-info/PKG-INFO +0 -47
  24. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/setup.cfg +0 -0
  25. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge/__init__.py +0 -0
  26. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge_cli.egg-info/dependency_links.txt +0 -0
  27. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge_cli.egg-info/entry_points.txt +0 -0
  28. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge_cli.egg-info/requires.txt +0 -0
  29. {stackforge_cli-0.1.1 → stackforge_cli-0.1.3}/src/stackforge_cli.egg-info/top_level.txt +0 -0
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: stackforge-cli
3
+ Version: 0.1.3
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
+ Requires-Dist: colorama>=0.4.6
14
+
15
+ # StackForge
16
+
17
+ A CLI scaffolder for .NET Clean Architecture (CQRS) projects.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install stackforge-cli
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ stackforge
29
+ ```
30
+
31
+ Then follow the prompts to scaffold a new project.
32
+
33
+ ## Version and updates
34
+
35
+ Check the version you have installed:
36
+
37
+ ```bash
38
+ stackforge --version
39
+ ```
40
+
41
+ Update to the latest release:
42
+
43
+ ```bash
44
+ pip install --upgrade stackforge-cli
45
+ ```
46
+
47
+ When you run `stackforge`, it checks PyPI for a newer version and prints an upgrade notice if one is available (network required; fails silently if offline).
48
+
49
+ ## What it generates
50
+
51
+ - .NET 10 Clean Architecture structure (API, Application, Domain, Infrastructure, Persistence)
52
+ - JWT authentication with ASP.NET Identity
53
+ - Swagger UI with Bearer token support
54
+ - PostgreSQL with EF Core
55
+ - Docker + docker-compose with healthchecks
56
+ - Role and user seeders (SuperAdmin out of the box)
57
+ - Git initialization on `main` with `staging` and `development` branches
58
+ - GitHub Actions workflow to create `staging` and `development` on first push to `main`
59
+
60
+ ## Quickstart after scaffolding
61
+
62
+ 1. Open the project in VS Code or Visual Studio
63
+ 2. Run: `docker-compose up --build`
64
+ 3. Visit: `http://localhost:5000/swagger`
65
+
66
+ ## Git workflow
67
+
68
+ Each scaffolded project is initialized with three local branches:
69
+
70
+ | Branch | Purpose |
71
+ |---------------|----------------------------------|
72
+ | `main` | Production / default branch |
73
+ | `staging` | Pre-production environment |
74
+ | `development` | Active development environment |
75
+
76
+ A GitHub Actions workflow (`.github/workflows/sync-branches.yml`) is also included as a fallback: if you only push `main` first, it creates remote `staging` and `development` on GitHub automatically.
77
+
78
+ ### Example: push to GitHub
79
+
80
+ From your scaffolded project directory (e.g. `~/stackforge/MyApi`):
81
+
82
+ ```bash
83
+ # 1. Create a new repo on GitHub (empty, no README), then add the remote
84
+ git remote add origin https://github.com/<your-user>/MyApi.git
85
+
86
+ # 2. Push all branches in one go
87
+ git push -u origin --all
88
+ ```
89
+
90
+ That publishes `main`, `staging`, and `development` in a single command and sets upstream tracking for each.
91
+
92
+ ### Day-to-day branching
93
+
94
+ ```bash
95
+ # Work on a feature
96
+ git checkout development
97
+ git checkout -b feature/my-feature
98
+ # ... commit changes ...
99
+ git push -u origin feature/my-feature
100
+
101
+ # Merge into development when ready (via PR or locally)
102
+ git checkout development
103
+ git merge feature/my-feature
104
+ git push origin development
105
+ ```
106
+
107
+ Promote changes up the chain (`development` → `staging` → `main`) using pull requests or merges as your team prefers.
@@ -0,0 +1,93 @@
1
+ # StackForge
2
+
3
+ A CLI scaffolder for .NET Clean Architecture (CQRS) projects.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install stackforge-cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ stackforge
15
+ ```
16
+
17
+ Then follow the prompts to scaffold a new project.
18
+
19
+ ## Version and updates
20
+
21
+ Check the version you have installed:
22
+
23
+ ```bash
24
+ stackforge --version
25
+ ```
26
+
27
+ Update to the latest release:
28
+
29
+ ```bash
30
+ pip install --upgrade stackforge-cli
31
+ ```
32
+
33
+ When you run `stackforge`, it checks PyPI for a newer version and prints an upgrade notice if one is available (network required; fails silently if offline).
34
+
35
+ ## What it generates
36
+
37
+ - .NET 10 Clean Architecture structure (API, Application, Domain, Infrastructure, Persistence)
38
+ - JWT authentication with ASP.NET Identity
39
+ - Swagger UI with Bearer token support
40
+ - PostgreSQL with EF Core
41
+ - Docker + docker-compose with healthchecks
42
+ - Role and user seeders (SuperAdmin out of the box)
43
+ - Git initialization on `main` with `staging` and `development` branches
44
+ - GitHub Actions workflow to create `staging` and `development` on first push to `main`
45
+
46
+ ## Quickstart after scaffolding
47
+
48
+ 1. Open the project in VS Code or Visual Studio
49
+ 2. Run: `docker-compose up --build`
50
+ 3. Visit: `http://localhost:5000/swagger`
51
+
52
+ ## Git workflow
53
+
54
+ Each scaffolded project is initialized with three local branches:
55
+
56
+ | Branch | Purpose |
57
+ |---------------|----------------------------------|
58
+ | `main` | Production / default branch |
59
+ | `staging` | Pre-production environment |
60
+ | `development` | Active development environment |
61
+
62
+ A GitHub Actions workflow (`.github/workflows/sync-branches.yml`) is also included as a fallback: if you only push `main` first, it creates remote `staging` and `development` on GitHub automatically.
63
+
64
+ ### Example: push to GitHub
65
+
66
+ From your scaffolded project directory (e.g. `~/stackforge/MyApi`):
67
+
68
+ ```bash
69
+ # 1. Create a new repo on GitHub (empty, no README), then add the remote
70
+ git remote add origin https://github.com/<your-user>/MyApi.git
71
+
72
+ # 2. Push all branches in one go
73
+ git push -u origin --all
74
+ ```
75
+
76
+ That publishes `main`, `staging`, and `development` in a single command and sets upstream tracking for each.
77
+
78
+ ### Day-to-day branching
79
+
80
+ ```bash
81
+ # Work on a feature
82
+ git checkout development
83
+ git checkout -b feature/my-feature
84
+ # ... commit changes ...
85
+ git push -u origin feature/my-feature
86
+
87
+ # Merge into development when ready (via PR or locally)
88
+ git checkout development
89
+ git merge feature/my-feature
90
+ git push origin development
91
+ ```
92
+
93
+ Promote changes up the chain (`development` → `staging` → `main`) using pull requests or merges as your team prefers.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stackforge-cli"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "A CLI scaffolder for .NET Clean Architecture projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -3,6 +3,6 @@ from .csproj_generator import generate_csproj
3
3
  from .program_generator import generate_configure_services, generate_program_cs
4
4
  from .docker_generator import generate_dockerfile, generate_docker_compose
5
5
  from .config_generator import generate_env, generate_appsettings, generate_appsettings_development
6
- from .git_generator import generate_gitignore, git_init
6
+ from .git_generator import generate_gitignore, generate_github_workflow, git_init
7
7
  from .api_generator import generate_api_structure
8
8
  from .layers_generator import generate_domain_layer, generate_application_layer, generate_infrastructure_layer, generate_persistence_layer
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ from stackforge import ui
2
3
 
3
4
 
4
5
  def generate_api_structure(base: Path, project: str):
@@ -28,7 +29,7 @@ def generate_api_structure(base: Path, project: str):
28
29
  _generate_login_dto(base, project)
29
30
  _generate_account_controller(base, project)
30
31
 
31
- print(" [+] API/ structure and files")
32
+ ui.file_created("API/ structure and files")
32
33
 
33
34
 
34
35
  def _generate_base_controller(base: Path, project: str):
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ from stackforge import ui
2
3
 
3
4
 
4
5
  def generate_env(base: Path, project: str):
@@ -18,7 +19,7 @@ JWT_EXPIRY_MINUTES=60
18
19
 
19
20
  file_path = base / ".env"
20
21
  file_path.write_text(content)
21
- print(" [+] .env")
22
+ ui.file_created(".env")
22
23
 
23
24
 
24
25
  def generate_appsettings(base: Path, project: str):
@@ -46,7 +47,7 @@ def generate_appsettings(base: Path, project: str):
46
47
 
47
48
  file_path = base / "API" / "appsettings.json"
48
49
  file_path.write_text(content)
49
- print(" [+] API/appsettings.json")
50
+ ui.file_created("API/appsettings.json")
50
51
 
51
52
 
52
53
  def generate_appsettings_development(base: Path, project: str):
@@ -73,4 +74,4 @@ def generate_appsettings_development(base: Path, project: str):
73
74
 
74
75
  file_path = base / "API" / "appsettings.Development.json"
75
76
  file_path.write_text(content)
76
- print(" [+] API/appsettings.Development.json")
77
+ ui.file_created("API/appsettings.Development.json")
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ from stackforge import ui
2
3
 
3
4
 
4
5
  LAYER_CONFIGS = {
@@ -79,4 +80,4 @@ def generate_csproj(layer: str, base: Path):
79
80
 
80
81
  csproj_path = base / layer / f"{layer}.csproj"
81
82
  csproj_path.write_text(content.strip())
82
- print(f" [+] {layer}/{layer}.csproj")
83
+ ui.file_created(f"{layer}/{layer}.csproj")
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ from stackforge import ui
2
3
 
3
4
 
4
5
  def generate_dockerfile(base: Path):
@@ -14,7 +15,7 @@ ENTRYPOINT ["dotnet", "out/API.dll"]
14
15
 
15
16
  file_path = base / "Dockerfile"
16
17
  file_path.write_text(content)
17
- print(" [+] Dockerfile")
18
+ ui.file_created("Dockerfile")
18
19
 
19
20
 
20
21
  def generate_docker_compose(base: Path, project: str):
@@ -60,4 +61,4 @@ volumes:
60
61
 
61
62
  file_path = base / "docker-compose.yml"
62
63
  file_path.write_text(content)
63
- print(" [+] docker-compose.yml")
64
+ ui.file_created("docker-compose.yml")
@@ -0,0 +1,111 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from stackforge import ui
4
+
5
+ def generate_sln(base: Path, project: str):
6
+ layers = ["API", "Application", "Domain", "Infrastructure", "Persistence"]
7
+
8
+ project_entries = ""
9
+ for layer in layers:
10
+ guid = str(__import__('uuid').uuid4()).upper()
11
+ project_entries += f"""Project("{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}") = "{layer}", "{layer}\\{layer}.csproj", "{{{guid}}}"
12
+ EndProject
13
+ """
14
+
15
+ content = f"""
16
+ Microsoft Visual Studio Solution File, Format Version 12.00
17
+ # Visual Studio Version 17
18
+ {project_entries}"""
19
+
20
+ file_path = base / f"{project}.sln"
21
+ file_path.write_text(content)
22
+ ui.file_created(f"{project}.sln")
23
+
24
+
25
+ def generate_gitignore(base: Path):
26
+ content = """# Build results
27
+ bin/
28
+ obj/
29
+ out/
30
+
31
+ # Environment
32
+ .env
33
+
34
+ # VS / Rider
35
+ .vs/
36
+ .idea/
37
+ *.user
38
+
39
+ # Logs
40
+ *.log
41
+ """
42
+
43
+ file_path = base / ".gitignore"
44
+ file_path.write_text(content)
45
+ ui.file_created(".gitignore")
46
+
47
+
48
+ def generate_github_workflow(base: Path):
49
+ workflow_dir = base / ".github" / "workflows"
50
+ workflow_dir.mkdir(parents=True, exist_ok=True)
51
+
52
+ content = """name: Sync Environment Branches
53
+
54
+ on:
55
+ push:
56
+ branches:
57
+ - main
58
+
59
+ permissions:
60
+ contents: write
61
+
62
+ jobs:
63
+ sync-branches:
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - uses: actions/checkout@v4
67
+ with:
68
+ fetch-depth: 0
69
+
70
+ - name: Create staging branch if missing
71
+ run: |
72
+ if ! git ls-remote --exit-code origin refs/heads/staging; then
73
+ git push origin HEAD:refs/heads/staging
74
+ fi
75
+
76
+ - name: Create development branch if missing
77
+ run: |
78
+ if ! git ls-remote --exit-code origin refs/heads/development; then
79
+ git push origin HEAD:refs/heads/development
80
+ fi
81
+ """
82
+ file_path = workflow_dir / "sync-branches.yml"
83
+ file_path.write_text(content.lstrip())
84
+ ui.file_created(".github/workflows/sync-branches.yml")
85
+
86
+
87
+ def git_init(base: Path, project: str):
88
+ try:
89
+ subprocess.run(["git", "init", "-b", "main"], cwd=base, check=True, capture_output=True)
90
+ ui.file_created("git init (main)")
91
+
92
+ subprocess.run(["git", "add", "."], cwd=base, check=True, capture_output=True)
93
+ ui.file_created("git add .")
94
+
95
+ subprocess.run(
96
+ ["git", "commit", "-m", f"initial: scaffold {project} with stackforge"],
97
+ cwd=base,
98
+ check=True,
99
+ capture_output=True
100
+ )
101
+ ui.file_created("initial commit")
102
+
103
+ subprocess.run(["git", "branch", "staging"], cwd=base, check=True, capture_output=True)
104
+ subprocess.run(["git", "branch", "development"], cwd=base, check=True, capture_output=True)
105
+ ui.file_created("branches: main, staging, development")
106
+
107
+ except subprocess.CalledProcessError as e:
108
+ ui.error(f"Git error: {e}")
109
+ except FileNotFoundError:
110
+ ui.error("Git not found. Make sure Git is installed and in your PATH.")
111
+
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ from stackforge import ui
2
3
 
3
4
 
4
5
  def generate_application_layer(base: Path, project: str):
@@ -27,7 +28,7 @@ def generate_application_layer(base: Path, project: str):
27
28
  _gen_paged_response(base, project)
28
29
  _gen_iso_datetime(base, project)
29
30
 
30
- print(" [+] Application/ structure and files")
31
+ ui.file_created("Application/ structure and files")
31
32
 
32
33
 
33
34
  def generate_domain_layer(base: Path, project: str):
@@ -47,7 +48,7 @@ def generate_domain_layer(base: Path, project: str):
47
48
  _gen_user_role(base, project)
48
49
  _gen_global_usings(base, project)
49
50
 
50
- print(" [+] Domain/ structure and files")
51
+ ui.file_created("Domain/ structure and files")
51
52
 
52
53
 
53
54
  def generate_infrastructure_layer(base: Path, project: str):
@@ -60,7 +61,7 @@ def generate_infrastructure_layer(base: Path, project: str):
60
61
  for folder in folders:
61
62
  (base / folder).mkdir(parents=True, exist_ok=True)
62
63
 
63
- print(" [+] Infrastructure/ structure and files")
64
+ ui.file_created("Infrastructure/ structure and files")
64
65
 
65
66
 
66
67
  def generate_persistence_layer(base: Path, project: str):
@@ -78,7 +79,7 @@ def generate_persistence_layer(base: Path, project: str):
78
79
  _gen_user_seeder(base, project)
79
80
  _gen_user_role_configuration(base, project)
80
81
 
81
- print(" [+] Persistence/ structure and files")
82
+ ui.file_created("Persistence/ structure and files")
82
83
 
83
84
 
84
85
  # ── Application files ──────────────────────────────────────────────────────────
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ from stackforge import ui
2
3
 
3
4
 
4
5
  def generate_configure_services(layer: str, base: Path, project: str):
@@ -67,7 +68,7 @@ namespace {project}.Persistence
67
68
 
68
69
  file_path = base / layer / "ConfigureServices.cs"
69
70
  file_path.write_text(templates[layer])
70
- print(f" [+] {layer}/ConfigureServices.cs")
71
+ ui.file_created(f"{layer}/ConfigureServices.cs")
71
72
 
72
73
 
73
74
  def generate_program_cs(base: Path, project: str):
@@ -191,4 +192,4 @@ app.Run();
191
192
 
192
193
  file_path = base / "API" / "Program.cs"
193
194
  file_path.write_text(content)
194
- print(" [+] API/Program.cs")
195
+ ui.file_created("API/Program.cs")
@@ -1,9 +1,10 @@
1
1
  from pathlib import Path
2
+ from stackforge import ui
2
3
  from .csproj_generator import generate_csproj
3
4
  from .program_generator import generate_configure_services, generate_program_cs
4
5
  from .docker_generator import generate_dockerfile, generate_docker_compose
5
6
  from .config_generator import generate_env, generate_appsettings, generate_appsettings_development
6
- from .git_generator import generate_gitignore, generate_sln, git_init
7
+ from .git_generator import generate_gitignore, generate_github_workflow, generate_sln, git_init
7
8
  from .api_generator import generate_api_structure
8
9
  from .layers_generator import (
9
10
  generate_application_layer,
@@ -21,51 +22,53 @@ def scaffold_folders(base: Path, project: str):
21
22
  "Persistence"
22
23
  ]
23
24
 
24
- print(f"\nScaffolding folders for project '{project}'...")
25
+ ui.section(f"Scaffolding '{project}'")
25
26
 
27
+ ui.step("Creating folders")
26
28
  for layer in layers:
27
29
  folder = base / layer
28
30
  folder.mkdir(parents=True, exist_ok=True)
29
- print(f" [+] {layer}")
31
+ ui.file_created(layer)
30
32
 
31
- print("\nFolder structure created.")
32
-
33
- print("\nGenerating .csproj files...")
33
+ ui.section("Generating .csproj files")
34
34
  for layer in layers:
35
35
  generate_csproj(layer, base)
36
36
 
37
- print("\nGenerating service registrations...")
37
+ ui.section("Generating service registrations")
38
38
  for layer in ["Application", "Infrastructure", "Persistence"]:
39
39
  generate_configure_services(layer, base, project)
40
40
 
41
- print("\nGenerating Program.cs...")
41
+ ui.section("Generating Program.cs")
42
42
  generate_program_cs(base, project)
43
43
 
44
- print("\nGenerating Docker files...")
44
+ ui.section("Generating Docker files")
45
45
  generate_dockerfile(base)
46
46
  generate_docker_compose(base, project)
47
47
 
48
- print("\nGenerating config files...")
48
+ ui.section("Generating config files")
49
49
  generate_env(base, project)
50
50
  generate_appsettings(base, project)
51
51
  generate_appsettings_development(base, project)
52
52
 
53
- print("\nGenerating API layer...")
53
+ ui.section("Generating API layer")
54
54
  generate_api_structure(base, project)
55
55
 
56
- print("\nGenerating layer files...")
56
+ ui.section("Generating layer files")
57
57
  generate_application_layer(base, project)
58
58
  generate_domain_layer(base, project)
59
59
  generate_infrastructure_layer(base, project)
60
60
  generate_persistence_layer(base, project)
61
61
 
62
- print("\nGenerating solution file...")
62
+ ui.section("Generating solution file")
63
63
  generate_sln(base, project)
64
64
 
65
- print("\nGenerating .gitignore...")
65
+ ui.section("Generating .gitignore")
66
66
  generate_gitignore(base)
67
67
 
68
- print("\nInitializing Git repository...")
68
+ ui.section("Generating GitHub workflow")
69
+ generate_github_workflow(base)
70
+
71
+ ui.section("Initializing Git repository")
69
72
  git_init(base, project)
70
73
 
71
- print("\nDone.")
74
+ ui.done()
@@ -0,0 +1,34 @@
1
+ import re
2
+
3
+ _CAMEL_PARTS = re.compile(r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\b)|\d+")
4
+
5
+
6
+ def is_valid_project_name(name: str) -> bool:
7
+ if not name or not name[0].isalpha():
8
+ return False
9
+ if not re.fullmatch(r"[A-Za-z0-9_-]+", name):
10
+ return False
11
+ if name.startswith(("-", "_")) or name.endswith(("-", "_")):
12
+ return False
13
+ if "__" in name or "--" in name:
14
+ return False
15
+ return True
16
+
17
+
18
+ def _title_part(part: str) -> str:
19
+ if not part:
20
+ return ""
21
+ if part.isdigit():
22
+ return part
23
+ return part[0].upper() + part[1:]
24
+
25
+
26
+ def to_pascal_case(name: str) -> str:
27
+ if "_" in name:
28
+ parts = name.split("_")
29
+ elif "-" in name:
30
+ parts = name.split("-")
31
+ else:
32
+ parts = _CAMEL_PARTS.findall(name) or [name]
33
+
34
+ return "".join(_title_part(part) for part in parts if part)
@@ -0,0 +1,90 @@
1
+ from pathlib import Path
2
+ from stackforge.engines.dotnet.scaffolder import scaffold_folders
3
+ from stackforge.version import get_installed_version, notify_if_update_available
4
+ from stackforge.project_name import is_valid_project_name, to_pascal_case
5
+ from stackforge import ui
6
+ import sys
7
+
8
+
9
+ FRAMEWORK_CHOICES = frozenset({"dotnet", "django", "fastapi", "flask", "exit"})
10
+
11
+
12
+ def select_framework() -> str:
13
+ ui.info("\n Select a framework:\n")
14
+ ui.menu_item("dotnet", ".NET Clean Architecture (CQRS)")
15
+ ui.menu_item("django", "Django [coming soon]", style=ui.Fore.YELLOW)
16
+ ui.menu_item("fastapi", "FastAPI [coming soon]", style=ui.Fore.YELLOW)
17
+ ui.menu_item("flask", "Flask [coming soon]", style=ui.Fore.YELLOW)
18
+ ui.menu_item("exit", "Exit", style=ui.Fore.RED)
19
+
20
+ choice = ui.prompt("\n Framework [dotnet]:").lower() or "dotnet"
21
+ if choice not in FRAMEWORK_CHOICES:
22
+ ui.error(f"Unknown framework '{choice}'.")
23
+ ui.step("Options: dotnet, django, fastapi, flask, exit\n")
24
+ return ""
25
+ return choice
26
+
27
+
28
+ def main():
29
+ if "--version" in sys.argv or "-V" in sys.argv:
30
+ print(get_installed_version())
31
+ return
32
+
33
+ notify_if_update_available()
34
+ ui.banner(".NET Clean Architecture Scaffolder", get_installed_version())
35
+ print()
36
+
37
+ while True:
38
+ framework = select_framework()
39
+
40
+ if not framework:
41
+ continue
42
+
43
+ if framework == "exit":
44
+ ui.warn("\n Exiting...")
45
+ break
46
+
47
+ if framework != "dotnet":
48
+ ui.warn("\n This framework is under development. Stay tuned!\n")
49
+ continue
50
+
51
+ raw_name = ui.prompt(
52
+ "\n Enter project name (PascalCase, camelCase, snake_case, or kebab-case):"
53
+ )
54
+
55
+ if not raw_name:
56
+ ui.error("Project name cannot be empty.\n")
57
+ continue
58
+
59
+ if not is_valid_project_name(raw_name):
60
+ ui.error("Invalid project name. Use letters, numbers, hyphens, or underscores.")
61
+ ui.step("Examples: MyApi, myApi, my_api, my-api\n")
62
+ continue
63
+
64
+ project_name = to_pascal_case(raw_name)
65
+ if project_name != raw_name:
66
+ ui.info(f" Normalized to: {project_name}")
67
+
68
+ base_path = Path.home() / "stackforge" / project_name
69
+
70
+ if base_path.exists():
71
+ ui.error(f"\n Project '{project_name}' already exists in the stackforge workspace.")
72
+ ui.step(f"Path: {base_path}")
73
+ ui.step("Choose a different project name or delete the existing one.\n")
74
+ continue
75
+
76
+ base_path.mkdir(parents=True, exist_ok=True)
77
+ scaffold_folders(base_path, project_name)
78
+
79
+ ui.section("Project ready")
80
+ ui.success(f"Created '{project_name}'")
81
+ ui.success(f"Location: {base_path}")
82
+ ui.info("\n Next steps:")
83
+ ui.step("Open the project in Visual Studio or VS Code")
84
+ ui.step("Run: docker-compose up --build")
85
+ ui.step("Visit: http://localhost:5000/swagger")
86
+ ui.muted("\n Press Ctrl+C to exit or continue to scaffold another project.\n")
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()
@@ -0,0 +1,72 @@
1
+ from colorama import Fore, Style, init
2
+
3
+ init(autoreset=True)
4
+
5
+ __all__ = [
6
+ "BANNER", "banner", "section", "step", "file_created", "success",
7
+ "warn", "error", "info", "prompt", "menu_item", "done", "muted",
8
+ "Fore", "Style",
9
+ ]
10
+
11
+ BANNER = r"""
12
+ _____ _ _ ______
13
+ / ____| | | | | ____|
14
+ | (___ | |_ __ _ ___| | _| |__ ___ _ __ __ _ ___
15
+ \___ \| __/ _` |/ __| |/ / __/ _ \| '__/ _` |/ _ \
16
+ ____) | || (_| | (__| <| | | (_) | | | (_| | __/
17
+ |_____/ \__\__,_|\___|_|\_\_| \___/|_| \__, |\___|
18
+ __/ |
19
+ |___/"""
20
+
21
+
22
+ def banner(subtitle: str, version: str = "") -> None:
23
+ print(f"{Style.BRIGHT}{Fore.CYAN}{BANNER}{Style.RESET_ALL}")
24
+ print(f" {Style.BRIGHT}{Fore.WHITE}{subtitle}{Style.RESET_ALL}", end="")
25
+ if version:
26
+ print(f" {Fore.LIGHTBLACK_EX}v{version}{Style.RESET_ALL}")
27
+ else:
28
+ print()
29
+
30
+
31
+ def section(title: str) -> None:
32
+ print(f"\n {Style.BRIGHT}{Fore.CYAN}> {title}{Style.RESET_ALL}")
33
+
34
+
35
+ def step(label: str) -> None:
36
+ print(f" {Fore.BLUE}|{Style.RESET_ALL} {label}")
37
+
38
+
39
+ def file_created(path: str) -> None:
40
+ print(f" {Fore.GREEN}+{Style.RESET_ALL} {Fore.LIGHTWHITE_EX}{path}{Style.RESET_ALL}")
41
+
42
+
43
+ def success(message: str) -> None:
44
+ print(f" {Fore.GREEN}+{Style.RESET_ALL} {message}")
45
+
46
+
47
+ def warn(message: str) -> None:
48
+ print(f" {Fore.YELLOW}!{Style.RESET_ALL} {message}")
49
+
50
+
51
+ def error(message: str) -> None:
52
+ print(f" {Fore.RED}x{Style.RESET_ALL} {message}")
53
+
54
+
55
+ def info(message: str) -> None:
56
+ print(f" {Fore.CYAN}{message}{Style.RESET_ALL}")
57
+
58
+
59
+ def prompt(label: str) -> str:
60
+ return input(f" {Style.BRIGHT}{Fore.CYAN}{label}{Style.RESET_ALL} ").strip()
61
+
62
+
63
+ def menu_item(key: str, label: str, *, style: str = Fore.GREEN) -> None:
64
+ print(f" {style}{key:<10}{Style.RESET_ALL} {label}")
65
+
66
+
67
+ def done() -> None:
68
+ print(f"\n {Style.BRIGHT}{Fore.GREEN}+ Scaffold complete.{Style.RESET_ALL}")
69
+
70
+
71
+ def muted(message: str) -> None:
72
+ print(f" {Fore.LIGHTBLACK_EX}{message}{Style.RESET_ALL}")
@@ -0,0 +1,48 @@
1
+ import json
2
+ import urllib.error
3
+ import urllib.request
4
+ from importlib.metadata import PackageNotFoundError, version
5
+
6
+ PACKAGE_NAME = "stackforge-cli"
7
+ PYPI_JSON_URL = f"https://pypi.org/pypi/{PACKAGE_NAME}/json"
8
+
9
+
10
+ def get_installed_version() -> str:
11
+ try:
12
+ return version(PACKAGE_NAME)
13
+ except PackageNotFoundError:
14
+ return "0.0.0-dev"
15
+
16
+
17
+ def _version_tuple(value: str) -> tuple[int, ...]:
18
+ parts: list[int] = []
19
+ for segment in value.split("."):
20
+ digits = "".join(c for c in segment if c.isdigit())
21
+ if digits:
22
+ parts.append(int(digits))
23
+ return tuple(parts)
24
+
25
+
26
+ def fetch_latest_version(timeout: float = 2.0) -> str | None:
27
+ request = urllib.request.Request(
28
+ PYPI_JSON_URL,
29
+ headers={"Accept": "application/json"},
30
+ )
31
+ try:
32
+ with urllib.request.urlopen(request, timeout=timeout) as response:
33
+ payload = json.load(response)
34
+ return payload["info"]["version"]
35
+ except (urllib.error.URLError, TimeoutError, KeyError, json.JSONDecodeError):
36
+ return None
37
+
38
+
39
+ def notify_if_update_available() -> None:
40
+ from stackforge import ui
41
+
42
+ installed = get_installed_version()
43
+ latest = fetch_latest_version()
44
+ if not latest or _version_tuple(latest) <= _version_tuple(installed):
45
+ return
46
+
47
+ ui.warn(f"StackForge {latest} is available (you have {installed}).")
48
+ ui.step(f"Update: pip install --upgrade {PACKAGE_NAME}\n")
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: stackforge-cli
3
+ Version: 0.1.3
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
+ Requires-Dist: colorama>=0.4.6
14
+
15
+ # StackForge
16
+
17
+ A CLI scaffolder for .NET Clean Architecture (CQRS) projects.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install stackforge-cli
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ stackforge
29
+ ```
30
+
31
+ Then follow the prompts to scaffold a new project.
32
+
33
+ ## Version and updates
34
+
35
+ Check the version you have installed:
36
+
37
+ ```bash
38
+ stackforge --version
39
+ ```
40
+
41
+ Update to the latest release:
42
+
43
+ ```bash
44
+ pip install --upgrade stackforge-cli
45
+ ```
46
+
47
+ When you run `stackforge`, it checks PyPI for a newer version and prints an upgrade notice if one is available (network required; fails silently if offline).
48
+
49
+ ## What it generates
50
+
51
+ - .NET 10 Clean Architecture structure (API, Application, Domain, Infrastructure, Persistence)
52
+ - JWT authentication with ASP.NET Identity
53
+ - Swagger UI with Bearer token support
54
+ - PostgreSQL with EF Core
55
+ - Docker + docker-compose with healthchecks
56
+ - Role and user seeders (SuperAdmin out of the box)
57
+ - Git initialization on `main` with `staging` and `development` branches
58
+ - GitHub Actions workflow to create `staging` and `development` on first push to `main`
59
+
60
+ ## Quickstart after scaffolding
61
+
62
+ 1. Open the project in VS Code or Visual Studio
63
+ 2. Run: `docker-compose up --build`
64
+ 3. Visit: `http://localhost:5000/swagger`
65
+
66
+ ## Git workflow
67
+
68
+ Each scaffolded project is initialized with three local branches:
69
+
70
+ | Branch | Purpose |
71
+ |---------------|----------------------------------|
72
+ | `main` | Production / default branch |
73
+ | `staging` | Pre-production environment |
74
+ | `development` | Active development environment |
75
+
76
+ A GitHub Actions workflow (`.github/workflows/sync-branches.yml`) is also included as a fallback: if you only push `main` first, it creates remote `staging` and `development` on GitHub automatically.
77
+
78
+ ### Example: push to GitHub
79
+
80
+ From your scaffolded project directory (e.g. `~/stackforge/MyApi`):
81
+
82
+ ```bash
83
+ # 1. Create a new repo on GitHub (empty, no README), then add the remote
84
+ git remote add origin https://github.com/<your-user>/MyApi.git
85
+
86
+ # 2. Push all branches in one go
87
+ git push -u origin --all
88
+ ```
89
+
90
+ That publishes `main`, `staging`, and `development` in a single command and sets upstream tracking for each.
91
+
92
+ ### Day-to-day branching
93
+
94
+ ```bash
95
+ # Work on a feature
96
+ git checkout development
97
+ git checkout -b feature/my-feature
98
+ # ... commit changes ...
99
+ git push -u origin feature/my-feature
100
+
101
+ # Merge into development when ready (via PR or locally)
102
+ git checkout development
103
+ git merge feature/my-feature
104
+ git push origin development
105
+ ```
106
+
107
+ Promote changes up the chain (`development` → `staging` → `main`) using pull requests or merges as your team prefers.
@@ -1,7 +1,10 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  src/stackforge/__init__.py
4
+ src/stackforge/project_name.py
4
5
  src/stackforge/scaffold.py
6
+ src/stackforge/ui.py
7
+ src/stackforge/version.py
5
8
  src/stackforge/engines/dotnet/__init__.py
6
9
  src/stackforge/engines/dotnet/api_generator.py
7
10
  src/stackforge/engines/dotnet/config_generator.py
@@ -1,47 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: stackforge-cli
3
- Version: 0.1.1
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
- Requires-Dist: colorama>=0.4.6
14
-
15
- # StackForge
16
-
17
- A CLI scaffolder for .NET Clean Architecture (CQRS) projects.
18
-
19
- ## Installation
20
-
21
- ```bash
22
- pip install stackforge
23
- ```
24
-
25
- ## Usage
26
-
27
- ```bash
28
- stackforge
29
- ```
30
-
31
- Then follow the prompts to scaffold a new project.
32
-
33
- ## What it generates
34
-
35
- - .NET 10 Clean Architecture structure (API, Application, Domain, Infrastructure, Persistence)
36
- - JWT authentication with ASP.NET Identity
37
- - Swagger UI with Bearer token support
38
- - PostgreSQL with EF Core
39
- - Docker + docker-compose with healthchecks
40
- - Role and user seeders (SuperAdmin out of the box)
41
- - Git initialization with initial commit
42
-
43
- ## Quickstart after scaffolding
44
-
45
- 1. Open the project in VS Code or Visual Studio
46
- 2. Run: `docker-compose up --build`
47
- 3. Visit: `http://localhost:5000/swagger`
@@ -1,33 +0,0 @@
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`
@@ -1,67 +0,0 @@
1
- import subprocess
2
- from pathlib import Path
3
-
4
- def generate_sln(base: Path, project: str):
5
- layers = ["API", "Application", "Domain", "Infrastructure", "Persistence"]
6
-
7
- project_entries = ""
8
- for layer in layers:
9
- guid = str(__import__('uuid').uuid4()).upper()
10
- project_entries += f"""Project("{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}") = "{layer}", "{layer}\\{layer}.csproj", "{{{guid}}}"
11
- EndProject
12
- """
13
-
14
- content = f"""
15
- Microsoft Visual Studio Solution File, Format Version 12.00
16
- # Visual Studio Version 17
17
- {project_entries}"""
18
-
19
- file_path = base / f"{project}.sln"
20
- file_path.write_text(content)
21
- print(f" [+] {project}.sln")
22
-
23
-
24
- def generate_gitignore(base: Path):
25
- content = """# Build results
26
- bin/
27
- obj/
28
- out/
29
-
30
- # Environment
31
- .env
32
-
33
- # VS / Rider
34
- .vs/
35
- .idea/
36
- *.user
37
-
38
- # Logs
39
- *.log
40
- """
41
-
42
- file_path = base / ".gitignore"
43
- file_path.write_text(content)
44
- print(" [+] .gitignore")
45
-
46
-
47
- def git_init(base: Path, project: str):
48
- try:
49
- subprocess.run(["git", "init"], cwd=base, check=True, capture_output=True)
50
- print(" [+] git init")
51
-
52
- subprocess.run(["git", "add", "."], cwd=base, check=True, capture_output=True)
53
- print(" [+] git add .")
54
-
55
- subprocess.run(
56
- ["git", "commit", "-m", f"initial: scaffold {project} with stackforge"],
57
- cwd=base,
58
- check=True,
59
- capture_output=True
60
- )
61
- print(" [+] initial commit")
62
-
63
- except subprocess.CalledProcessError as e:
64
- print(f" [!] Git error: {e}")
65
- except FileNotFoundError:
66
- print(" [!] Git not found. Make sure Git is installed and in your PATH.")
67
-
@@ -1,83 +0,0 @@
1
- from pathlib import Path
2
- from stackforge.engines.dotnet.scaffolder import scaffold_folders
3
- from colorama import init, Fore, Style
4
- import re
5
-
6
- init(autoreset=True)
7
-
8
- def is_valid_project_name(name: str) -> bool:
9
- return bool(re.match(r'^[A-Za-z][A-Za-z0-9]*$', name))
10
-
11
-
12
- def select_framework() -> str:
13
- print(f" {Fore.CYAN}Select a framework:{Style.RESET_ALL}\n")
14
- print(f" {Fore.GREEN}1.{Style.RESET_ALL} .NET Clean Architecture (CQRS)")
15
- print(f" {Fore.YELLOW}2.{Style.RESET_ALL} Django [coming soon]")
16
- print(f" {Fore.YELLOW}3.{Style.RESET_ALL} FastAPI [coming soon]")
17
- print(f" {Fore.YELLOW}4.{Style.RESET_ALL} Flask [coming soon]")
18
- print(f" {Fore.RED}5.{Style.RESET_ALL} exit")
19
-
20
- choice = input(f"\n {Fore.CYAN}Choice [n]:{Style.RESET_ALL} ").strip() or "1"
21
- return choice
22
-
23
-
24
- def main():
25
- print(Fore.CYAN + r"""
26
- _____ _ _ ______
27
- / ____| | | | | ____|
28
- | (___ | |_ __ _ ___| | _| |__ ___ _ __ __ _ ___
29
- \___ \| __/ _` |/ __| |/ / __/ _ \| '__/ _` |/ _ \
30
- ____) | || (_| | (__| <| | | (_) | | | (_| | __/
31
- |_____/ \__\__,_|\___|_|\_\_| \___/|_| \__, |\___|
32
- __/ |
33
- |___/
34
- """ + Style.RESET_ALL + f" {Fore.WHITE}.NET Clean Architecture Scaffolder{Style.RESET_ALL}\n")
35
-
36
- while True:
37
- framework = select_framework()
38
-
39
- if framework == "5":
40
- print(f"\n {Fore.YELLOW}Exiting...{Style.RESET_ALL}")
41
- break
42
-
43
- if framework != "1":
44
- print(f"\n {Fore.YELLOW}[!] This framework is under development. Stay tuned!{Style.RESET_ALL}\n")
45
- continue
46
-
47
- project_name = input(f"\n {Fore.CYAN}Enter project name (PascalCase, no spaces):{Style.RESET_ALL} ").strip()
48
-
49
- if not project_name:
50
- print(f" {Fore.RED}[!] Project name cannot be empty.{Style.RESET_ALL}\n")
51
- continue
52
-
53
- if not is_valid_project_name(project_name):
54
- print(f" {Fore.RED}[!] Project name must start with a letter and contain only letters and numbers.")
55
- print(f" Example: RaiderXpress, MyApi, StackForge{Style.RESET_ALL}\n")
56
- continue
57
-
58
- base_path = Path.home() / "stackforge" / project_name
59
-
60
- if base_path.exists():
61
- print(f"\n {Fore.RED}[!] Project '{project_name}' already exists in the stackforge workspace.")
62
- print(f" Path: {base_path}{Style.RESET_ALL}")
63
- print(" Choose a different project name or delete the existing one.\n")
64
- continue
65
-
66
- base_path.mkdir(parents=True, exist_ok=True)
67
- scaffold_folders(base_path, project_name)
68
-
69
- print(f"""
70
- {Fore.GREEN}✔ Project '{project_name}' created successfully!
71
- ✔ Location: {base_path}{Style.RESET_ALL}
72
-
73
- {Fore.CYAN}Next steps:{Style.RESET_ALL}
74
- 1. Open the project in Visual Studio or VS Code
75
- 2. Run: docker-compose up --build
76
- 3. Visit: http://localhost:5000/swagger
77
-
78
- Press Ctrl+C to exit or continue to scaffold another project.
79
- """)
80
-
81
-
82
- if __name__ == "__main__":
83
- main()
@@ -1,47 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: stackforge-cli
3
- Version: 0.1.1
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
- Requires-Dist: colorama>=0.4.6
14
-
15
- # StackForge
16
-
17
- A CLI scaffolder for .NET Clean Architecture (CQRS) projects.
18
-
19
- ## Installation
20
-
21
- ```bash
22
- pip install stackforge
23
- ```
24
-
25
- ## Usage
26
-
27
- ```bash
28
- stackforge
29
- ```
30
-
31
- Then follow the prompts to scaffold a new project.
32
-
33
- ## What it generates
34
-
35
- - .NET 10 Clean Architecture structure (API, Application, Domain, Infrastructure, Persistence)
36
- - JWT authentication with ASP.NET Identity
37
- - Swagger UI with Bearer token support
38
- - PostgreSQL with EF Core
39
- - Docker + docker-compose with healthchecks
40
- - Role and user seeders (SuperAdmin out of the box)
41
- - Git initialization with initial commit
42
-
43
- ## Quickstart after scaffolding
44
-
45
- 1. Open the project in VS Code or Visual Studio
46
- 2. Run: `docker-compose up --build`
47
- 3. Visit: `http://localhost:5000/swagger`
File without changes