devops-project-generator 1.2.0__tar.gz → 1.3.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.
- {devops_project_generator-1.2.0/devops_project_generator.egg-info → devops_project_generator-1.3.0}/PKG-INFO +52 -3
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/README.md +51 -2
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/cli/__init__.py +1 -1
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/cli/cli.py +1040 -1
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0/devops_project_generator.egg-info}/PKG-INFO +52 -3
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/pyproject.toml +1 -1
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/LICENSE +0 -0
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/devops_project_generator.egg-info/SOURCES.txt +0 -0
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/devops_project_generator.egg-info/dependency_links.txt +0 -0
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/devops_project_generator.egg-info/entry_points.txt +0 -0
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/devops_project_generator.egg-info/requires.txt +0 -0
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/devops_project_generator.egg-info/top_level.txt +0 -0
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/generator/__init__.py +0 -0
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/generator/config.py +0 -0
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/generator/generator.py +0 -0
- {devops_project_generator-1.2.0 → devops_project_generator-1.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devops-project-generator
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A CLI tool that scaffolds production-ready DevOps repositories
|
|
5
5
|
Author-email: NotHarshhaa <devops-project-generator@notHarshhaa.com>
|
|
6
6
|
License: MIT
|
|
@@ -167,6 +167,48 @@ devops-project-generator cleanup my-project --force
|
|
|
167
167
|
devops-project-generator cleanup my-project --keep-config
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
### Template Management (NEW v1.3.0)
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# List available templates
|
|
174
|
+
devops-project-generator template list
|
|
175
|
+
|
|
176
|
+
# Create custom template
|
|
177
|
+
devops-project-generator template create --name my-template
|
|
178
|
+
|
|
179
|
+
# Customize existing template
|
|
180
|
+
devops-project-generator template customize --name my-template
|
|
181
|
+
|
|
182
|
+
# Export templates
|
|
183
|
+
devops-project-generator template export --output ./templates
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Project Backup (NEW v1.3.0)
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Create backup
|
|
190
|
+
devops-project-generator backup create my-project
|
|
191
|
+
|
|
192
|
+
# List available backups
|
|
193
|
+
devops-project-generator backup list
|
|
194
|
+
|
|
195
|
+
# Restore from backup
|
|
196
|
+
devops-project-generator backup restore my-project --file backup.tar.gz
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Health Check (NEW v1.3.0)
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Basic health check
|
|
203
|
+
devops-project-generator health my-project
|
|
204
|
+
|
|
205
|
+
# Detailed health analysis
|
|
206
|
+
devops-project-generator health my-project --detailed
|
|
207
|
+
|
|
208
|
+
# Auto-fix health issues
|
|
209
|
+
devops-project-generator health my-project --fix
|
|
210
|
+
```
|
|
211
|
+
|
|
170
212
|
## 🏗️ Generated Project Structure (Example)
|
|
171
213
|
|
|
172
214
|
```
|
|
@@ -410,7 +452,7 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f
|
|
|
410
452
|
- [x] Template caching and pre-loading
|
|
411
453
|
- [x] Better user experience with improved messages
|
|
412
454
|
|
|
413
|
-
### v1.2 ✅
|
|
455
|
+
### v1.2 ✅
|
|
414
456
|
- [x] Project validation and structure checking
|
|
415
457
|
- [x] Configuration file management system
|
|
416
458
|
- [x] Project cleanup and teardown utilities
|
|
@@ -418,7 +460,14 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f
|
|
|
418
460
|
- [x] DevOps maturity scoring
|
|
419
461
|
- [x] Intelligent recommendations system
|
|
420
462
|
|
|
421
|
-
### v1.3
|
|
463
|
+
### v1.3 ✅ (Current)
|
|
464
|
+
- [x] Template management and customization system
|
|
465
|
+
- [x] Project backup and restore functionality
|
|
466
|
+
- [x] Comprehensive health monitoring and scoring
|
|
467
|
+
- [x] Auto-fix capabilities for common issues
|
|
468
|
+
- [x] Advanced project analysis and recommendations
|
|
469
|
+
|
|
470
|
+
### v1.4
|
|
422
471
|
- [ ] Support for Azure DevOps
|
|
423
472
|
- [ ] Additional cloud providers (GCP, Azure)
|
|
424
473
|
- [ ] More deployment targets (AWS ECS, Fargate)
|
|
@@ -136,6 +136,48 @@ devops-project-generator cleanup my-project --force
|
|
|
136
136
|
devops-project-generator cleanup my-project --keep-config
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
### Template Management (NEW v1.3.0)
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# List available templates
|
|
143
|
+
devops-project-generator template list
|
|
144
|
+
|
|
145
|
+
# Create custom template
|
|
146
|
+
devops-project-generator template create --name my-template
|
|
147
|
+
|
|
148
|
+
# Customize existing template
|
|
149
|
+
devops-project-generator template customize --name my-template
|
|
150
|
+
|
|
151
|
+
# Export templates
|
|
152
|
+
devops-project-generator template export --output ./templates
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Project Backup (NEW v1.3.0)
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Create backup
|
|
159
|
+
devops-project-generator backup create my-project
|
|
160
|
+
|
|
161
|
+
# List available backups
|
|
162
|
+
devops-project-generator backup list
|
|
163
|
+
|
|
164
|
+
# Restore from backup
|
|
165
|
+
devops-project-generator backup restore my-project --file backup.tar.gz
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Health Check (NEW v1.3.0)
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Basic health check
|
|
172
|
+
devops-project-generator health my-project
|
|
173
|
+
|
|
174
|
+
# Detailed health analysis
|
|
175
|
+
devops-project-generator health my-project --detailed
|
|
176
|
+
|
|
177
|
+
# Auto-fix health issues
|
|
178
|
+
devops-project-generator health my-project --fix
|
|
179
|
+
```
|
|
180
|
+
|
|
139
181
|
## 🏗️ Generated Project Structure (Example)
|
|
140
182
|
|
|
141
183
|
```
|
|
@@ -379,7 +421,7 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f
|
|
|
379
421
|
- [x] Template caching and pre-loading
|
|
380
422
|
- [x] Better user experience with improved messages
|
|
381
423
|
|
|
382
|
-
### v1.2 ✅
|
|
424
|
+
### v1.2 ✅
|
|
383
425
|
- [x] Project validation and structure checking
|
|
384
426
|
- [x] Configuration file management system
|
|
385
427
|
- [x] Project cleanup and teardown utilities
|
|
@@ -387,7 +429,14 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f
|
|
|
387
429
|
- [x] DevOps maturity scoring
|
|
388
430
|
- [x] Intelligent recommendations system
|
|
389
431
|
|
|
390
|
-
### v1.3
|
|
432
|
+
### v1.3 ✅ (Current)
|
|
433
|
+
- [x] Template management and customization system
|
|
434
|
+
- [x] Project backup and restore functionality
|
|
435
|
+
- [x] Comprehensive health monitoring and scoring
|
|
436
|
+
- [x] Auto-fix capabilities for common issues
|
|
437
|
+
- [x] Advanced project analysis and recommendations
|
|
438
|
+
|
|
439
|
+
### v1.4
|
|
391
440
|
- [ ] Support for Azure DevOps
|
|
392
441
|
- [ ] Additional cloud providers (GCP, Azure)
|
|
393
442
|
- [ ] More deployment targets (AWS ECS, Fargate)
|
|
@@ -6,6 +6,9 @@ CLI interface for DevOps Project Generator
|
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
8
|
import shutil
|
|
9
|
+
import json
|
|
10
|
+
import yaml
|
|
11
|
+
import datetime
|
|
9
12
|
from pathlib import Path
|
|
10
13
|
from typing import Optional, List
|
|
11
14
|
import typer
|
|
@@ -1093,13 +1096,1049 @@ def _display_project_info(stats: dict, detailed: bool) -> None:
|
|
|
1093
1096
|
console.print(f" {file_path} ({mod_time.strftime('%Y-%m-%d %H:%M')})")
|
|
1094
1097
|
|
|
1095
1098
|
|
|
1099
|
+
@app.command()
|
|
1100
|
+
def template(
|
|
1101
|
+
action: str = typer.Argument(
|
|
1102
|
+
"list",
|
|
1103
|
+
help="Action: list, create, customize, or export"
|
|
1104
|
+
),
|
|
1105
|
+
template_name: Optional[str] = typer.Option(
|
|
1106
|
+
None,
|
|
1107
|
+
"--name",
|
|
1108
|
+
help="Template name for create/customize actions"
|
|
1109
|
+
),
|
|
1110
|
+
output_dir: Optional[str] = typer.Option(
|
|
1111
|
+
None,
|
|
1112
|
+
"--output",
|
|
1113
|
+
help="Output directory for exported templates"
|
|
1114
|
+
),
|
|
1115
|
+
) -> None:
|
|
1116
|
+
"""Manage and customize project templates"""
|
|
1117
|
+
if action == "list":
|
|
1118
|
+
_list_available_templates()
|
|
1119
|
+
elif action == "create":
|
|
1120
|
+
if not template_name:
|
|
1121
|
+
console.print("[red]❌ Template name required for create action[/red]")
|
|
1122
|
+
console.print("[yellow]Usage: devops-project-generator template create --name <template-name>[/yellow]")
|
|
1123
|
+
raise typer.Exit(1)
|
|
1124
|
+
_create_custom_template(template_name)
|
|
1125
|
+
elif action == "customize":
|
|
1126
|
+
if not template_name:
|
|
1127
|
+
console.print("[red]❌ Template name required for customize action[/red]")
|
|
1128
|
+
console.print("[yellow]Usage: devops-project-generator template customize --name <template-name>[/yellow]")
|
|
1129
|
+
raise typer.Exit(1)
|
|
1130
|
+
_customize_template(template_name)
|
|
1131
|
+
elif action == "export":
|
|
1132
|
+
if not output_dir:
|
|
1133
|
+
console.print("[red]❌ Output directory required for export action[/red]")
|
|
1134
|
+
console.print("[yellow]Usage: devops-project-generator template export --output <directory>[/yellow]")
|
|
1135
|
+
raise typer.Exit(1)
|
|
1136
|
+
_export_templates(output_dir)
|
|
1137
|
+
else:
|
|
1138
|
+
console.print(f"[red]❌ Unknown action: {action}[/red]")
|
|
1139
|
+
console.print("[yellow]Available actions: list, create, customize, export[/yellow]")
|
|
1140
|
+
raise typer.Exit(1)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _list_available_templates() -> None:
|
|
1144
|
+
"""List all available templates"""
|
|
1145
|
+
console.print(Panel.fit(
|
|
1146
|
+
"[bold blue]📋 Available Templates[/bold blue]\n"
|
|
1147
|
+
"[dim]Built-in templates for different DevOps configurations[/dim]",
|
|
1148
|
+
border_style="blue"
|
|
1149
|
+
))
|
|
1150
|
+
|
|
1151
|
+
template_categories = {
|
|
1152
|
+
"CI/CD": {
|
|
1153
|
+
"github-actions": "GitHub Actions workflows with multi-stage pipelines",
|
|
1154
|
+
"gitlab-ci": "GitLab CI/CD with auto-deployment",
|
|
1155
|
+
"jenkins": "Jenkins pipelines with Docker integration",
|
|
1156
|
+
"azure-pipelines": "Azure DevOps pipelines with YAML"
|
|
1157
|
+
},
|
|
1158
|
+
"Infrastructure": {
|
|
1159
|
+
"terraform": "Terraform modules for multi-cloud deployment",
|
|
1160
|
+
"cloudformation": "AWS CloudFormation templates",
|
|
1161
|
+
" pulumi": "Pulumi infrastructure as code",
|
|
1162
|
+
"ansible": "Ansible playbooks for configuration management"
|
|
1163
|
+
},
|
|
1164
|
+
"Deployment": {
|
|
1165
|
+
"kubernetes": "K8s manifests with Helm charts",
|
|
1166
|
+
"docker": "Docker containers with compose files",
|
|
1167
|
+
"serverless": "AWS Lambda and serverless functions",
|
|
1168
|
+
"static": "Static site deployment with CDN"
|
|
1169
|
+
},
|
|
1170
|
+
"Monitoring": {
|
|
1171
|
+
"prometheus": "Prometheus + Grafana monitoring stack",
|
|
1172
|
+
"datadog": "Datadog APM and monitoring",
|
|
1173
|
+
"elasticsearch": "ELK stack for logging and analytics",
|
|
1174
|
+
"cloudwatch": "AWS CloudWatch monitoring"
|
|
1175
|
+
},
|
|
1176
|
+
"Security": {
|
|
1177
|
+
"owasp": "OWASP security scanning and policies",
|
|
1178
|
+
"vault": "HashiCorp Vault secrets management",
|
|
1179
|
+
"cert-manager": "SSL certificate automation",
|
|
1180
|
+
"istio": "Service mesh security policies"
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
for category, templates in template_categories.items():
|
|
1185
|
+
console.print(f"\n[bold]{category}:[/bold]")
|
|
1186
|
+
table = Table()
|
|
1187
|
+
table.add_column("Template", style="cyan")
|
|
1188
|
+
table.add_column("Description")
|
|
1189
|
+
|
|
1190
|
+
for name, description in templates.items():
|
|
1191
|
+
table.add_row(name, description)
|
|
1192
|
+
|
|
1193
|
+
console.print(table)
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def _create_custom_template(template_name: str) -> None:
|
|
1197
|
+
"""Create a new custom template"""
|
|
1198
|
+
console.print(f"[blue]📝 Creating custom template: {template_name}[/blue]")
|
|
1199
|
+
|
|
1200
|
+
template_dir = Path.home() / ".devops-generator" / "templates" / template_name
|
|
1201
|
+
template_dir.mkdir(parents=True, exist_ok=True)
|
|
1202
|
+
|
|
1203
|
+
# Create template structure
|
|
1204
|
+
subdirs = ["ci", "infra", "deploy", "monitoring", "security", "app"]
|
|
1205
|
+
for subdir in subdirs:
|
|
1206
|
+
(template_dir / subdir).mkdir(exist_ok=True)
|
|
1207
|
+
|
|
1208
|
+
# Create template metadata
|
|
1209
|
+
metadata = {
|
|
1210
|
+
"name": template_name,
|
|
1211
|
+
"version": "1.0.0",
|
|
1212
|
+
"description": f"Custom template: {template_name}",
|
|
1213
|
+
"author": "Custom User",
|
|
1214
|
+
"created": datetime.datetime.now().isoformat(),
|
|
1215
|
+
"components": {
|
|
1216
|
+
"ci": ["github-actions"],
|
|
1217
|
+
"infra": ["terraform"],
|
|
1218
|
+
"deploy": ["kubernetes"],
|
|
1219
|
+
"monitoring": ["prometheus"],
|
|
1220
|
+
"security": ["owasp"]
|
|
1221
|
+
},
|
|
1222
|
+
"variables": {
|
|
1223
|
+
"project_name": "string",
|
|
1224
|
+
"environment": "string",
|
|
1225
|
+
"cloud_provider": "string",
|
|
1226
|
+
"docker_registry": "string"
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
metadata_file = template_dir / "template.yaml"
|
|
1231
|
+
with open(metadata_file, "w", encoding="utf-8") as f:
|
|
1232
|
+
yaml.dump(metadata, f, default_flow_style=False)
|
|
1233
|
+
|
|
1234
|
+
# Create sample files
|
|
1235
|
+
sample_files = {
|
|
1236
|
+
"ci/github-actions.yml.j2": """# GitHub Actions Workflow for {{ project_name }}
|
|
1237
|
+
name: {{ environment }}-pipeline
|
|
1238
|
+
|
|
1239
|
+
on:
|
|
1240
|
+
push:
|
|
1241
|
+
branches: [ main, develop ]
|
|
1242
|
+
pull_request:
|
|
1243
|
+
branches: [ main ]
|
|
1244
|
+
|
|
1245
|
+
jobs:
|
|
1246
|
+
test:
|
|
1247
|
+
runs-on: ubuntu-latest
|
|
1248
|
+
steps:
|
|
1249
|
+
- uses: actions/checkout@v3
|
|
1250
|
+
- name: Run tests
|
|
1251
|
+
run: echo "Running tests for {{ project_name }}"
|
|
1252
|
+
|
|
1253
|
+
deploy:
|
|
1254
|
+
needs: test
|
|
1255
|
+
runs-on: ubuntu-latest
|
|
1256
|
+
if: github.ref == 'refs/heads/main'
|
|
1257
|
+
steps:
|
|
1258
|
+
- name: Deploy to {{ environment }}
|
|
1259
|
+
run: echo "Deploying to {{ environment }}"
|
|
1260
|
+
|
|
1261
|
+
""",
|
|
1262
|
+
"infra/main.tf.j2": """# Terraform configuration for {{ project_name }}
|
|
1263
|
+
provider "{{ cloud_provider }}" {
|
|
1264
|
+
region = var.region
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
variable "project_name" {
|
|
1268
|
+
description = "Project name"
|
|
1269
|
+
default = "{{ project_name }}"
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
variable "environment" {
|
|
1273
|
+
description = "Environment"
|
|
1274
|
+
default = "{{ environment }}"
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
# Add your infrastructure resources here
|
|
1278
|
+
""",
|
|
1279
|
+
"deploy/k8s-deployment.yaml.j2": """# Kubernetes deployment for {{ project_name }}
|
|
1280
|
+
apiVersion: apps/v1
|
|
1281
|
+
kind: Deployment
|
|
1282
|
+
metadata:
|
|
1283
|
+
name: {{ project_name }}
|
|
1284
|
+
namespace: {{ environment }}
|
|
1285
|
+
spec:
|
|
1286
|
+
replicas: 3
|
|
1287
|
+
selector:
|
|
1288
|
+
matchLabels:
|
|
1289
|
+
app: {{ project_name }}
|
|
1290
|
+
template:
|
|
1291
|
+
metadata:
|
|
1292
|
+
labels:
|
|
1293
|
+
app: {{ project_name }}
|
|
1294
|
+
spec:
|
|
1295
|
+
containers:
|
|
1296
|
+
- name: {{ project_name }}
|
|
1297
|
+
image: {{ docker_registry }}/{{ project_name }}:latest
|
|
1298
|
+
ports:
|
|
1299
|
+
- containerPort: 8080
|
|
1300
|
+
"""
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
for file_path, content in sample_files.items():
|
|
1304
|
+
full_path = template_dir / file_path
|
|
1305
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
1306
|
+
f.write(content)
|
|
1307
|
+
|
|
1308
|
+
console.print(f"[green]✅ Custom template created: {template_dir}[/green]")
|
|
1309
|
+
console.print("[yellow]💡 Edit the template files to customize your project structure[/yellow]")
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def _customize_template(template_name: str) -> None:
|
|
1313
|
+
"""Customize an existing template"""
|
|
1314
|
+
template_dir = Path.home() / ".devops-generator" / "templates" / template_name
|
|
1315
|
+
|
|
1316
|
+
if not template_dir.exists():
|
|
1317
|
+
console.print(f"[red]❌ Template '{template_name}' not found[/red]")
|
|
1318
|
+
console.print(f"[yellow]Available templates in: {template_dir.parent}[/yellow]")
|
|
1319
|
+
raise typer.Exit(1)
|
|
1320
|
+
|
|
1321
|
+
console.print(f"[blue]🔧 Customizing template: {template_name}[/blue]")
|
|
1322
|
+
|
|
1323
|
+
# Show template structure
|
|
1324
|
+
console.print(f"\n[dim]Template structure:[/dim]")
|
|
1325
|
+
for item in template_dir.rglob("*"):
|
|
1326
|
+
if item.is_file():
|
|
1327
|
+
relative_path = item.relative_to(template_dir)
|
|
1328
|
+
console.print(f" 📄 {relative_path}")
|
|
1329
|
+
|
|
1330
|
+
console.print(f"\n[yellow]💡 Edit files in: {template_dir}[/yellow]")
|
|
1331
|
+
console.print("[dim]Use .j2 extension for Jinja2 templates[/dim]")
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def _export_templates(output_dir: str) -> None:
|
|
1335
|
+
"""Export built-in templates to a directory"""
|
|
1336
|
+
output_path = Path(output_dir)
|
|
1337
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
1338
|
+
|
|
1339
|
+
console.print(f"[blue]📤 Exporting templates to: {output_path}[/blue]")
|
|
1340
|
+
|
|
1341
|
+
# Get the built-in templates directory
|
|
1342
|
+
builtin_templates = Path(__file__).parent.parent / "templates"
|
|
1343
|
+
|
|
1344
|
+
if builtin_templates.exists():
|
|
1345
|
+
import shutil
|
|
1346
|
+
export_dir = output_path / "builtin-templates"
|
|
1347
|
+
shutil.copytree(builtin_templates, export_dir, dirs_exist_ok=True)
|
|
1348
|
+
console.print(f"[green]✅ Built-in templates exported to: {export_dir}[/green]")
|
|
1349
|
+
else:
|
|
1350
|
+
console.print("[yellow]⚠️ Built-in templates directory not found[/yellow]")
|
|
1351
|
+
|
|
1352
|
+
# Export custom templates if they exist
|
|
1353
|
+
custom_templates_dir = Path.home() / ".devops-generator" / "templates"
|
|
1354
|
+
if custom_templates_dir.exists():
|
|
1355
|
+
custom_export_dir = output_path / "custom-templates"
|
|
1356
|
+
shutil.copytree(custom_templates_dir, custom_export_dir, dirs_exist_ok=True)
|
|
1357
|
+
console.print(f"[green]✅ Custom templates exported to: {custom_export_dir}[/green]")
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
@app.command()
|
|
1361
|
+
def backup(
|
|
1362
|
+
action: str = typer.Argument(
|
|
1363
|
+
"create",
|
|
1364
|
+
help="Action: create, restore, or list"
|
|
1365
|
+
),
|
|
1366
|
+
project_path: str = typer.Argument(
|
|
1367
|
+
".",
|
|
1368
|
+
help="Path to the DevOps project"
|
|
1369
|
+
),
|
|
1370
|
+
backup_file: Optional[str] = typer.Option(
|
|
1371
|
+
None,
|
|
1372
|
+
"--file",
|
|
1373
|
+
help="Backup file path for restore action"
|
|
1374
|
+
),
|
|
1375
|
+
include_config: bool = typer.Option(
|
|
1376
|
+
True,
|
|
1377
|
+
"--include-config/--no-config",
|
|
1378
|
+
help="Include configuration files in backup"
|
|
1379
|
+
),
|
|
1380
|
+
compress: bool = typer.Option(
|
|
1381
|
+
True,
|
|
1382
|
+
"--compress/--no-compress",
|
|
1383
|
+
help="Compress backup file"
|
|
1384
|
+
),
|
|
1385
|
+
) -> None:
|
|
1386
|
+
"""Create and restore project backups"""
|
|
1387
|
+
project_path = Path(project_path).resolve()
|
|
1388
|
+
|
|
1389
|
+
if action == "create":
|
|
1390
|
+
_create_backup(project_path, include_config, compress)
|
|
1391
|
+
elif action == "restore":
|
|
1392
|
+
if not backup_file:
|
|
1393
|
+
console.print("[red]❌ Backup file required for restore action[/red]")
|
|
1394
|
+
console.print("[yellow]Usage: devops-project-generator backup restore --file <backup-file>[/yellow]")
|
|
1395
|
+
raise typer.Exit(1)
|
|
1396
|
+
_restore_backup(project_path, backup_file)
|
|
1397
|
+
elif action == "list":
|
|
1398
|
+
_list_backups()
|
|
1399
|
+
else:
|
|
1400
|
+
console.print(f"[red]❌ Unknown action: {action}[/red]")
|
|
1401
|
+
console.print("[yellow]Available actions: create, restore, list[/yellow]")
|
|
1402
|
+
raise typer.Exit(1)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
def _create_backup(project_path: Path, include_config: bool, compress: bool) -> None:
|
|
1406
|
+
"""Create a backup of the project"""
|
|
1407
|
+
if not project_path.exists():
|
|
1408
|
+
console.print(f"[red]❌ Project path '{project_path}' does not exist[/red]")
|
|
1409
|
+
raise typer.Exit(1)
|
|
1410
|
+
|
|
1411
|
+
console.print(Panel.fit(
|
|
1412
|
+
"[bold blue]💾 Creating Project Backup[/bold blue]\n"
|
|
1413
|
+
"[dim]Archive project files and configuration[/dim]",
|
|
1414
|
+
border_style="blue"
|
|
1415
|
+
))
|
|
1416
|
+
|
|
1417
|
+
# Generate backup filename
|
|
1418
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1419
|
+
backup_name = f"{project_path.name}_backup_{timestamp}"
|
|
1420
|
+
|
|
1421
|
+
if compress:
|
|
1422
|
+
backup_file = project_path.parent / f"{backup_name}.tar.gz"
|
|
1423
|
+
else:
|
|
1424
|
+
backup_file = project_path.parent / f"{backup_name}.tar"
|
|
1425
|
+
|
|
1426
|
+
console.print(f"[blue]📦 Creating backup: {backup_file.name}[/blue]")
|
|
1427
|
+
|
|
1428
|
+
try:
|
|
1429
|
+
import tarfile
|
|
1430
|
+
|
|
1431
|
+
with tarfile.open(backup_file, "w:gz" if compress else "w") as tar:
|
|
1432
|
+
# Add project files
|
|
1433
|
+
for item in project_path.rglob("*"):
|
|
1434
|
+
if item.is_file():
|
|
1435
|
+
# Skip certain files if not including config
|
|
1436
|
+
if not include_config and any(pattern in item.name for pattern in [".env", "secret", "key"]):
|
|
1437
|
+
continue
|
|
1438
|
+
|
|
1439
|
+
arcname = str(item.relative_to(project_path.parent))
|
|
1440
|
+
tar.add(item, arcname=arcname)
|
|
1441
|
+
|
|
1442
|
+
# Create backup metadata
|
|
1443
|
+
backup_info = {
|
|
1444
|
+
"project_name": project_path.name,
|
|
1445
|
+
"created": datetime.datetime.now().isoformat(),
|
|
1446
|
+
"size_bytes": backup_file.stat().st_size,
|
|
1447
|
+
"include_config": include_config,
|
|
1448
|
+
"compressed": compress,
|
|
1449
|
+
"file_count": len(list(project_path.rglob("*"))),
|
|
1450
|
+
"version": "1.2.0"
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
metadata_file = backup_file.with_suffix(".json")
|
|
1454
|
+
with open(metadata_file, "w", encoding="utf-8") as f:
|
|
1455
|
+
json.dump(backup_info, f, indent=2)
|
|
1456
|
+
|
|
1457
|
+
size_mb = backup_file.stat().st_size / (1024 * 1024)
|
|
1458
|
+
console.print(f"[green]✅ Backup created successfully![/green]")
|
|
1459
|
+
console.print(f" File: {backup_file.name}")
|
|
1460
|
+
console.print(f" Size: {size_mb:.2f} MB")
|
|
1461
|
+
console.print(f" Files: {backup_info['file_count']}")
|
|
1462
|
+
|
|
1463
|
+
except Exception as e:
|
|
1464
|
+
console.print(f"[red]❌ Backup failed: {str(e)}[/red]")
|
|
1465
|
+
raise typer.Exit(1)
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def _restore_backup(project_path: Path, backup_file: str) -> None:
|
|
1469
|
+
"""Restore a project from backup"""
|
|
1470
|
+
backup_path = Path(backup_file)
|
|
1471
|
+
|
|
1472
|
+
if not backup_path.exists():
|
|
1473
|
+
console.print(f"[red]❌ Backup file '{backup_file}' does not exist[/red]")
|
|
1474
|
+
raise typer.Exit(1)
|
|
1475
|
+
|
|
1476
|
+
# Check for metadata file
|
|
1477
|
+
metadata_file = backup_path.with_suffix(".json")
|
|
1478
|
+
if metadata_file.exists():
|
|
1479
|
+
with open(metadata_file, "r", encoding="utf-8") as f:
|
|
1480
|
+
backup_info = json.load(f)
|
|
1481
|
+
|
|
1482
|
+
console.print(Panel.fit(
|
|
1483
|
+
f"[bold blue]🔄 Restoring Project Backup[/bold blue]\n"
|
|
1484
|
+
f"[dim]Project: {backup_info.get('project_name', 'Unknown')}[/dim]\n"
|
|
1485
|
+
f"[dim]Created: {backup_info.get('created', 'Unknown')}[/dim]",
|
|
1486
|
+
border_style="blue"
|
|
1487
|
+
))
|
|
1488
|
+
else:
|
|
1489
|
+
console.print(Panel.fit(
|
|
1490
|
+
"[bold blue]🔄 Restoring Project Backup[/bold blue]\n"
|
|
1491
|
+
"[dim]Backup information not available[/dim]",
|
|
1492
|
+
border_style="blue"
|
|
1493
|
+
))
|
|
1494
|
+
|
|
1495
|
+
# Check if project directory already exists
|
|
1496
|
+
if project_path.exists():
|
|
1497
|
+
console.print(f"[yellow]⚠️ Project directory '{project_path.name}' already exists[/yellow]")
|
|
1498
|
+
if not typer.confirm("Continue and overwrite?"):
|
|
1499
|
+
console.print("[dim]Operation cancelled.[/dim]")
|
|
1500
|
+
raise typer.Exit(0)
|
|
1501
|
+
|
|
1502
|
+
# Remove existing directory
|
|
1503
|
+
shutil.rmtree(project_path)
|
|
1504
|
+
|
|
1505
|
+
console.print(f"[blue]📦 Restoring from: {backup_path.name}[/blue]")
|
|
1506
|
+
|
|
1507
|
+
try:
|
|
1508
|
+
import tarfile
|
|
1509
|
+
|
|
1510
|
+
with tarfile.open(backup_path, "r:*") as tar:
|
|
1511
|
+
tar.extractall(project_path.parent)
|
|
1512
|
+
|
|
1513
|
+
console.print(f"[green]✅ Project restored successfully![/green]")
|
|
1514
|
+
console.print(f" Location: {project_path}")
|
|
1515
|
+
console.print(f"[yellow]💡 Run 'devops-project-generator validate {project_path.name}' to check the project[/yellow]")
|
|
1516
|
+
|
|
1517
|
+
except Exception as e:
|
|
1518
|
+
console.print(f"[red]❌ Restore failed: {str(e)}[/red]")
|
|
1519
|
+
raise typer.Exit(1)
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
def _list_backups() -> None:
|
|
1523
|
+
"""List all available backups"""
|
|
1524
|
+
console.print(Panel.fit(
|
|
1525
|
+
"[bold blue]📋 Available Backups[/bold blue]\n"
|
|
1526
|
+
"[dim]Project backups in current directory[/dim]",
|
|
1527
|
+
border_style="blue"
|
|
1528
|
+
))
|
|
1529
|
+
|
|
1530
|
+
current_dir = Path.cwd()
|
|
1531
|
+
backup_files = []
|
|
1532
|
+
|
|
1533
|
+
# Find backup files
|
|
1534
|
+
for item in current_dir.glob("*backup*.tar*"):
|
|
1535
|
+
if item.is_file():
|
|
1536
|
+
backup_files.append(item)
|
|
1537
|
+
|
|
1538
|
+
if not backup_files:
|
|
1539
|
+
console.print("[yellow]No backup files found in current directory[/yellow]")
|
|
1540
|
+
return
|
|
1541
|
+
|
|
1542
|
+
console.print()
|
|
1543
|
+
table = Table()
|
|
1544
|
+
table.add_column("Backup File", style="cyan")
|
|
1545
|
+
table.add_column("Size", style="green")
|
|
1546
|
+
table.add_column("Created", style="blue")
|
|
1547
|
+
table.add_column("Project", style="yellow")
|
|
1548
|
+
|
|
1549
|
+
for backup_file in sorted(backup_files, key=lambda x: x.stat().st_mtime, reverse=True):
|
|
1550
|
+
size_mb = backup_file.stat().st_size / (1024 * 1024)
|
|
1551
|
+
mtime = datetime.datetime.fromtimestamp(backup_file.stat().st_mtime)
|
|
1552
|
+
|
|
1553
|
+
# Try to get project name from filename
|
|
1554
|
+
project_name = backup_file.name.split("_backup_")[0] if "_backup_" in backup_file.name else "Unknown"
|
|
1555
|
+
|
|
1556
|
+
# Check for metadata
|
|
1557
|
+
metadata_file = backup_file.with_suffix(".json")
|
|
1558
|
+
created = mtime.strftime("%Y-%m-%d %H:%M")
|
|
1559
|
+
|
|
1560
|
+
if metadata_file.exists():
|
|
1561
|
+
try:
|
|
1562
|
+
with open(metadata_file, "r", encoding="utf-8") as f:
|
|
1563
|
+
metadata = json.load(f)
|
|
1564
|
+
project_name = metadata.get("project_name", project_name)
|
|
1565
|
+
created = metadata.get("created", created)
|
|
1566
|
+
if isinstance(created, str):
|
|
1567
|
+
created = created[:16] # Just date and time
|
|
1568
|
+
except:
|
|
1569
|
+
pass
|
|
1570
|
+
|
|
1571
|
+
table.add_row(
|
|
1572
|
+
backup_file.name,
|
|
1573
|
+
f"{size_mb:.1f} MB",
|
|
1574
|
+
created,
|
|
1575
|
+
project_name
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1578
|
+
console.print(table)
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
@app.command()
|
|
1582
|
+
def health(
|
|
1583
|
+
project_path: str = typer.Argument(
|
|
1584
|
+
".",
|
|
1585
|
+
help="Path to the DevOps project to check"
|
|
1586
|
+
),
|
|
1587
|
+
detailed: bool = typer.Option(
|
|
1588
|
+
False,
|
|
1589
|
+
"--detailed",
|
|
1590
|
+
help="Show detailed health analysis"
|
|
1591
|
+
),
|
|
1592
|
+
fix: bool = typer.Option(
|
|
1593
|
+
False,
|
|
1594
|
+
"--fix",
|
|
1595
|
+
help="Attempt to fix health issues automatically"
|
|
1596
|
+
),
|
|
1597
|
+
) -> None:
|
|
1598
|
+
"""Perform comprehensive health check on DevOps project"""
|
|
1599
|
+
project_path = Path(project_path).resolve()
|
|
1600
|
+
|
|
1601
|
+
if not project_path.exists():
|
|
1602
|
+
console.print(f"[red]❌ Project path '{project_path}' does not exist[/red]")
|
|
1603
|
+
raise typer.Exit(1)
|
|
1604
|
+
|
|
1605
|
+
console.print(Panel.fit(
|
|
1606
|
+
"[bold blue]🏥 Project Health Check[/bold blue]\n"
|
|
1607
|
+
"[dim]Comprehensive analysis of project health and best practices[/dim]",
|
|
1608
|
+
border_style="blue"
|
|
1609
|
+
))
|
|
1610
|
+
|
|
1611
|
+
# Perform health check
|
|
1612
|
+
health_report = _perform_health_check(project_path, detailed)
|
|
1613
|
+
|
|
1614
|
+
# Display results
|
|
1615
|
+
_display_health_report(health_report, detailed)
|
|
1616
|
+
|
|
1617
|
+
# Auto-fix if requested
|
|
1618
|
+
if fix and health_report["fixable_issues"]:
|
|
1619
|
+
console.print("\n[yellow]🔧 Attempting to fix health issues...[/yellow]")
|
|
1620
|
+
_fix_health_issues(project_path, health_report["fixable_issues"])
|
|
1621
|
+
|
|
1622
|
+
# Overall health score
|
|
1623
|
+
_display_health_score(health_report)
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
def _perform_health_check(project_path: Path, detailed: bool) -> dict:
|
|
1627
|
+
"""Perform comprehensive health check"""
|
|
1628
|
+
report = {
|
|
1629
|
+
"overall_score": 0,
|
|
1630
|
+
"categories": {
|
|
1631
|
+
"structure": {"score": 0, "issues": [], "fixable_issues": [], "checks_passed": []},
|
|
1632
|
+
"security": {"score": 0, "issues": [], "fixable_issues": [], "checks_passed": []},
|
|
1633
|
+
"performance": {"score": 0, "issues": [], "fixable_issues": [], "checks_passed": []},
|
|
1634
|
+
"maintenance": {"score": 0, "issues": [], "fixable_issues": [], "checks_passed": []},
|
|
1635
|
+
"documentation": {"score": 0, "issues": [], "fixable_issues": [], "checks_passed": []}
|
|
1636
|
+
},
|
|
1637
|
+
"recommendations": [],
|
|
1638
|
+
"critical_issues": [],
|
|
1639
|
+
"fixable_issues": []
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
# Structure health
|
|
1643
|
+
_check_structure_health(project_path, report["categories"]["structure"])
|
|
1644
|
+
|
|
1645
|
+
# Security health
|
|
1646
|
+
_check_security_health(project_path, report["categories"]["security"])
|
|
1647
|
+
|
|
1648
|
+
# Performance health
|
|
1649
|
+
_check_performance_health(project_path, report["categories"]["performance"])
|
|
1650
|
+
|
|
1651
|
+
# Maintenance health
|
|
1652
|
+
_check_maintenance_health(project_path, report["categories"]["maintenance"])
|
|
1653
|
+
|
|
1654
|
+
# Documentation health
|
|
1655
|
+
_check_documentation_health(project_path, report["categories"]["documentation"])
|
|
1656
|
+
|
|
1657
|
+
# Calculate overall score
|
|
1658
|
+
total_score = sum(cat["score"] for cat in report["categories"].values())
|
|
1659
|
+
report["overall_score"] = total_score // len(report["categories"])
|
|
1660
|
+
|
|
1661
|
+
# Collect all issues
|
|
1662
|
+
for category in report["categories"].values():
|
|
1663
|
+
report["critical_issues"].extend([issue for issue in category["issues"] if "critical" in issue.lower()])
|
|
1664
|
+
report["fixable_issues"].extend(category["fixable_issues"])
|
|
1665
|
+
|
|
1666
|
+
# Generate recommendations
|
|
1667
|
+
report["recommendations"] = _generate_health_recommendations(report)
|
|
1668
|
+
|
|
1669
|
+
return report
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
def _check_structure_health(project_path: Path, structure: dict) -> None:
|
|
1673
|
+
"""Check project structure health"""
|
|
1674
|
+
checks = {
|
|
1675
|
+
"required_dirs": ["app", "ci", "infra", "deploy", "monitoring", "security"],
|
|
1676
|
+
"required_files": ["README.md", "Makefile", ".gitignore"],
|
|
1677
|
+
"recommended_dirs": ["scripts", "docs", "tests"],
|
|
1678
|
+
"recommended_files": ["Dockerfile", "docker-compose.yml"]
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
score = 100
|
|
1682
|
+
|
|
1683
|
+
# Check required directories
|
|
1684
|
+
for dir_name in checks["required_dirs"]:
|
|
1685
|
+
dir_path = project_path / dir_name
|
|
1686
|
+
if dir_path.exists():
|
|
1687
|
+
structure["checks_passed"].append(f"✅ {dir_name}/ directory exists")
|
|
1688
|
+
else:
|
|
1689
|
+
structure["issues"].append(f"❌ Missing required {dir_name}/ directory")
|
|
1690
|
+
structure["fixable_issues"].append(("create_dir", dir_name))
|
|
1691
|
+
score -= 15
|
|
1692
|
+
|
|
1693
|
+
# Check required files
|
|
1694
|
+
for file_name in checks["required_files"]:
|
|
1695
|
+
file_path = project_path / file_name
|
|
1696
|
+
if file_path.exists():
|
|
1697
|
+
structure["checks_passed"].append(f"✅ {file_name} exists")
|
|
1698
|
+
else:
|
|
1699
|
+
structure["issues"].append(f"❌ Missing required {file_name}")
|
|
1700
|
+
if file_name == "README.md":
|
|
1701
|
+
structure["fixable_issues"].append(("create_readme", None))
|
|
1702
|
+
elif file_name == "Makefile":
|
|
1703
|
+
structure["fixable_issues"].append(("create_makefile", None))
|
|
1704
|
+
elif file_name == ".gitignore":
|
|
1705
|
+
structure["fixable_issues"].append(("create_gitignore", None))
|
|
1706
|
+
score -= 10
|
|
1707
|
+
|
|
1708
|
+
# Check recommended items
|
|
1709
|
+
for dir_name in checks["recommended_dirs"]:
|
|
1710
|
+
dir_path = project_path / dir_name
|
|
1711
|
+
if dir_path.exists():
|
|
1712
|
+
structure["checks_passed"].append(f"✅ {dir_name}/ directory exists")
|
|
1713
|
+
else:
|
|
1714
|
+
structure["issues"].append(f"⚠️ Consider adding {dir_name}/ directory")
|
|
1715
|
+
score -= 5
|
|
1716
|
+
|
|
1717
|
+
structure["score"] = max(0, score)
|
|
1718
|
+
|
|
1719
|
+
|
|
1720
|
+
def _check_security_health(project_path: Path, security: dict) -> None:
|
|
1721
|
+
"""Check security health"""
|
|
1722
|
+
score = 100
|
|
1723
|
+
|
|
1724
|
+
# Check for secrets
|
|
1725
|
+
secret_patterns = [".env", "secret", "key", "password", "token"]
|
|
1726
|
+
for item in project_path.rglob("*"):
|
|
1727
|
+
if item.is_file() and any(pattern in item.name.lower() for pattern in secret_patterns):
|
|
1728
|
+
if item.suffix in [".txt", ".yml", ".yaml", ".json", ".env"]:
|
|
1729
|
+
security["issues"].append(f"🔒 Potential secret file: {item.relative_to(project_path)}")
|
|
1730
|
+
score -= 20
|
|
1731
|
+
|
|
1732
|
+
# Check for security directories
|
|
1733
|
+
security_dir = project_path / "security"
|
|
1734
|
+
if security_dir.exists():
|
|
1735
|
+
security["checks_passed"].append("✅ Security directory exists")
|
|
1736
|
+
security_files = list(security_dir.rglob("*.yml")) + list(security_dir.rglob("*.yaml"))
|
|
1737
|
+
if security_files:
|
|
1738
|
+
security["checks_passed"].append(f"✅ Found {len(security_files)} security configuration files")
|
|
1739
|
+
else:
|
|
1740
|
+
security["issues"].append("⚠️ Security directory exists but no configuration files")
|
|
1741
|
+
score -= 10
|
|
1742
|
+
else:
|
|
1743
|
+
security["issues"].append("❌ No security configuration found")
|
|
1744
|
+
score -= 25
|
|
1745
|
+
|
|
1746
|
+
# Check .gitignore for sensitive files
|
|
1747
|
+
gitignore_path = project_path / ".gitignore"
|
|
1748
|
+
if gitignore_path.exists():
|
|
1749
|
+
with open(gitignore_path, "r", encoding="utf-8") as f:
|
|
1750
|
+
gitignore_content = f.read().lower()
|
|
1751
|
+
|
|
1752
|
+
ignored_patterns = [".env", "secret", "key", "*.pem", "*.p12"]
|
|
1753
|
+
ignored_count = sum(1 for pattern in ignored_patterns if pattern in gitignore_content)
|
|
1754
|
+
|
|
1755
|
+
if ignored_count >= 3:
|
|
1756
|
+
security["checks_passed"].append("✅ .gitignore properly excludes sensitive files")
|
|
1757
|
+
else:
|
|
1758
|
+
security["issues"].append("⚠️ .gitignore may not exclude all sensitive files")
|
|
1759
|
+
security["fixable_issues"].append(("update_gitignore", None))
|
|
1760
|
+
score -= 15
|
|
1761
|
+
else:
|
|
1762
|
+
security["issues"].append("❌ No .gitignore file found")
|
|
1763
|
+
score -= 20
|
|
1764
|
+
|
|
1765
|
+
security["score"] = max(0, score)
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
def _check_performance_health(project_path: Path, performance: dict) -> None:
|
|
1769
|
+
"""Check performance-related health"""
|
|
1770
|
+
score = 100
|
|
1771
|
+
|
|
1772
|
+
# Check for Docker files
|
|
1773
|
+
docker_files = ["Dockerfile", "docker-compose.yml", "docker-compose.yaml"]
|
|
1774
|
+
docker_found = any((project_path / f).exists() for f in docker_files)
|
|
1775
|
+
|
|
1776
|
+
if docker_found:
|
|
1777
|
+
performance["checks_passed"].append("✅ Containerization files found")
|
|
1778
|
+
else:
|
|
1779
|
+
performance["issues"].append("⚠️ No containerization files found")
|
|
1780
|
+
performance["fixable_issues"].append(("create_dockerfile", None))
|
|
1781
|
+
score -= 15
|
|
1782
|
+
|
|
1783
|
+
# Check for CI/CD optimization
|
|
1784
|
+
ci_dir = project_path / "ci"
|
|
1785
|
+
if ci_dir.exists():
|
|
1786
|
+
ci_files = list(ci_dir.rglob("*.yml")) + list(ci_dir.rglob("*.yaml"))
|
|
1787
|
+
if ci_files:
|
|
1788
|
+
performance["checks_passed"].append("✅ CI/CD configuration found")
|
|
1789
|
+
|
|
1790
|
+
# Check for caching in CI files
|
|
1791
|
+
cache_found = False
|
|
1792
|
+
for ci_file in ci_files:
|
|
1793
|
+
try:
|
|
1794
|
+
with open(ci_file, "r", encoding="utf-8") as f:
|
|
1795
|
+
content = f.read().lower()
|
|
1796
|
+
if "cache" in content:
|
|
1797
|
+
cache_found = True
|
|
1798
|
+
break
|
|
1799
|
+
except:
|
|
1800
|
+
pass
|
|
1801
|
+
|
|
1802
|
+
if cache_found:
|
|
1803
|
+
performance["checks_passed"].append("✅ CI/CD caching configured")
|
|
1804
|
+
else:
|
|
1805
|
+
performance["issues"].append("⚠️ Consider adding CI/CD caching for better performance")
|
|
1806
|
+
score -= 10
|
|
1807
|
+
else:
|
|
1808
|
+
performance["issues"].append("❌ CI/CD directory exists but no configuration files")
|
|
1809
|
+
score -= 20
|
|
1810
|
+
|
|
1811
|
+
# Check project size
|
|
1812
|
+
total_size = sum(f.stat().st_size for f in project_path.rglob("*") if f.is_file())
|
|
1813
|
+
size_mb = total_size / (1024 * 1024)
|
|
1814
|
+
|
|
1815
|
+
if size_mb > 100:
|
|
1816
|
+
performance["issues"].append(f"⚠️ Large project size: {size_mb:.1f} MB")
|
|
1817
|
+
performance["fixable_issues"].append(("optimize_size", None))
|
|
1818
|
+
score -= 10
|
|
1819
|
+
else:
|
|
1820
|
+
performance["checks_passed"].append(f"✅ Reasonable project size: {size_mb:.1f} MB")
|
|
1821
|
+
|
|
1822
|
+
performance["score"] = max(0, score)
|
|
1823
|
+
|
|
1824
|
+
|
|
1825
|
+
def _check_maintenance_health(project_path: Path, maintenance: dict) -> None:
|
|
1826
|
+
"""Check maintenance-related health"""
|
|
1827
|
+
score = 100
|
|
1828
|
+
|
|
1829
|
+
# Check for automation scripts
|
|
1830
|
+
scripts_dir = project_path / "scripts"
|
|
1831
|
+
if scripts_dir.exists():
|
|
1832
|
+
script_files = list(scripts_dir.rglob("*.sh")) + list(scripts_dir.rglob("*.py"))
|
|
1833
|
+
if script_files:
|
|
1834
|
+
maintenance["checks_passed"].append(f"✅ Found {len(script_files)} automation scripts")
|
|
1835
|
+
else:
|
|
1836
|
+
maintenance["issues"].append("⚠️ Scripts directory exists but no scripts found")
|
|
1837
|
+
score -= 10
|
|
1838
|
+
else:
|
|
1839
|
+
maintenance["issues"].append("⚠️ No automation scripts directory")
|
|
1840
|
+
maintenance["fixable_issues"].append(("create_scripts_dir", None))
|
|
1841
|
+
score -= 15
|
|
1842
|
+
|
|
1843
|
+
# Check for Makefile targets
|
|
1844
|
+
makefile_path = project_path / "Makefile"
|
|
1845
|
+
if makefile_path.exists():
|
|
1846
|
+
try:
|
|
1847
|
+
with open(makefile_path, "r", encoding="utf-8") as f:
|
|
1848
|
+
makefile_content = f.read()
|
|
1849
|
+
|
|
1850
|
+
common_targets = ["build", "deploy", "test", "clean"]
|
|
1851
|
+
found_targets = [target for target in common_targets if f"{target}:" in makefile_content]
|
|
1852
|
+
|
|
1853
|
+
if len(found_targets) >= 3:
|
|
1854
|
+
maintenance["checks_passed"].append(f"✅ Makefile has {len(found_targets)} common targets")
|
|
1855
|
+
else:
|
|
1856
|
+
maintenance["issues"].append(f"⚠️ Makefile has only {len(found_targets)} common targets")
|
|
1857
|
+
score -= 10
|
|
1858
|
+
except:
|
|
1859
|
+
maintenance["issues"].append("❌ Error reading Makefile")
|
|
1860
|
+
score -= 5
|
|
1861
|
+
|
|
1862
|
+
# Check for recent activity
|
|
1863
|
+
import time
|
|
1864
|
+
current_time = time.time()
|
|
1865
|
+
recent_files = []
|
|
1866
|
+
|
|
1867
|
+
for item in project_path.rglob("*"):
|
|
1868
|
+
if item.is_file():
|
|
1869
|
+
file_age = current_time - item.stat().st_mtime
|
|
1870
|
+
if file_age < 7 * 24 * 60 * 60: # Less than 7 days
|
|
1871
|
+
recent_files.append(item)
|
|
1872
|
+
|
|
1873
|
+
if len(recent_files) >= 3:
|
|
1874
|
+
maintenance["checks_passed"].append(f"✅ Recent activity: {len(recent_files)} files modified in last 7 days")
|
|
1875
|
+
else:
|
|
1876
|
+
maintenance["issues"].append("⚠️ Low recent activity - project may need maintenance")
|
|
1877
|
+
score -= 10
|
|
1878
|
+
|
|
1879
|
+
maintenance["score"] = max(0, score)
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
def _check_documentation_health(project_path: Path, documentation: dict) -> None:
|
|
1883
|
+
"""Check documentation health"""
|
|
1884
|
+
score = 100
|
|
1885
|
+
|
|
1886
|
+
# Check README quality
|
|
1887
|
+
readme_path = project_path / "README.md"
|
|
1888
|
+
if readme_path.exists():
|
|
1889
|
+
try:
|
|
1890
|
+
with open(readme_path, "r", encoding="utf-8") as f:
|
|
1891
|
+
readme_content = f.read()
|
|
1892
|
+
|
|
1893
|
+
readme_size = len(readme_content)
|
|
1894
|
+
sections = ["#", "##", "###"]
|
|
1895
|
+
section_count = sum(readme_content.count(section) for section in sections)
|
|
1896
|
+
|
|
1897
|
+
if readme_size > 1000 and section_count >= 5:
|
|
1898
|
+
documentation["checks_passed"].append("✅ Comprehensive README documentation")
|
|
1899
|
+
elif readme_size > 500:
|
|
1900
|
+
documentation["issues"].append("⚠️ README could be more detailed")
|
|
1901
|
+
score -= 10
|
|
1902
|
+
else:
|
|
1903
|
+
documentation["issues"].append("❌ README is too short")
|
|
1904
|
+
documentation["fixable_issues"].append(("enhance_readme", None))
|
|
1905
|
+
score -= 20
|
|
1906
|
+
except:
|
|
1907
|
+
documentation["issues"].append("❌ Error reading README")
|
|
1908
|
+
score -= 15
|
|
1909
|
+
else:
|
|
1910
|
+
documentation["issues"].append("❌ No README.md file found")
|
|
1911
|
+
score -= 30
|
|
1912
|
+
|
|
1913
|
+
# Check for additional documentation
|
|
1914
|
+
docs_dir = project_path / "docs"
|
|
1915
|
+
if docs_dir.exists():
|
|
1916
|
+
doc_files = list(docs_dir.rglob("*.md")) + list(docs_dir.rglob("*.txt"))
|
|
1917
|
+
if doc_files:
|
|
1918
|
+
documentation["checks_passed"].append(f"✅ Found {len(doc_files)} additional documentation files")
|
|
1919
|
+
else:
|
|
1920
|
+
documentation["issues"].append("⚠️ docs directory exists but no documentation files")
|
|
1921
|
+
score -= 5
|
|
1922
|
+
else:
|
|
1923
|
+
documentation["issues"].append("⚠️ No docs directory found")
|
|
1924
|
+
score -= 10
|
|
1925
|
+
|
|
1926
|
+
# Check for inline documentation
|
|
1927
|
+
code_files = []
|
|
1928
|
+
for ext in [".py", ".js", ".ts", ".go", ".java"]:
|
|
1929
|
+
code_files.extend(project_path.rglob(f"*{ext}"))
|
|
1930
|
+
|
|
1931
|
+
documented_files = 0
|
|
1932
|
+
for code_file in code_files[:20]: # Check first 20 files
|
|
1933
|
+
try:
|
|
1934
|
+
with open(code_file, "r", encoding="utf-8") as f:
|
|
1935
|
+
content = f.read()
|
|
1936
|
+
if any(marker in content for marker in ["#", "//", "/*", "*"]):
|
|
1937
|
+
documented_files += 1
|
|
1938
|
+
except:
|
|
1939
|
+
pass
|
|
1940
|
+
|
|
1941
|
+
if code_files and documented_files / len(code_files) > 0.7:
|
|
1942
|
+
documentation["checks_passed"].append("✅ Good inline documentation coverage")
|
|
1943
|
+
elif code_files:
|
|
1944
|
+
documentation["issues"].append("⚠️ Consider adding more inline documentation")
|
|
1945
|
+
score -= 10
|
|
1946
|
+
|
|
1947
|
+
documentation["score"] = max(0, score)
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
def _generate_health_recommendations(report: dict) -> list:
|
|
1951
|
+
"""Generate health improvement recommendations"""
|
|
1952
|
+
recommendations = []
|
|
1953
|
+
|
|
1954
|
+
if report["overall_score"] < 60:
|
|
1955
|
+
recommendations.append("🚨 Project needs significant improvement - focus on critical issues first")
|
|
1956
|
+
elif report["overall_score"] < 80:
|
|
1957
|
+
recommendations.append("⚡ Good foundation - address the identified issues to reach excellence")
|
|
1958
|
+
else:
|
|
1959
|
+
recommendations.append("🎉 Excellent project health! Consider sharing your practices")
|
|
1960
|
+
|
|
1961
|
+
# Category-specific recommendations
|
|
1962
|
+
for category_name, category_data in report["categories"].items():
|
|
1963
|
+
if category_data["score"] < 70:
|
|
1964
|
+
if category_name == "structure":
|
|
1965
|
+
recommendations.append("🏗️ Improve project structure with missing directories and files")
|
|
1966
|
+
elif category_name == "security":
|
|
1967
|
+
recommendations.append("🔒 Enhance security with proper secrets management and policies")
|
|
1968
|
+
elif category_name == "performance":
|
|
1969
|
+
recommendations.append("⚡ Optimize performance with caching and containerization")
|
|
1970
|
+
elif category_name == "maintenance":
|
|
1971
|
+
recommendations.append("🔧 Add automation scripts and improve maintainability")
|
|
1972
|
+
elif category_name == "documentation":
|
|
1973
|
+
recommendations.append("📚 Enhance documentation for better project understanding")
|
|
1974
|
+
|
|
1975
|
+
return recommendations
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
def _display_health_report(report: dict, detailed: bool) -> None:
|
|
1979
|
+
"""Display comprehensive health report"""
|
|
1980
|
+
console.print(f"\n[bold]🏥 Overall Health Score: {report['overall_score']}/100[/bold]")
|
|
1981
|
+
|
|
1982
|
+
# Health score color coding
|
|
1983
|
+
if report["overall_score"] >= 80:
|
|
1984
|
+
console.print("[green]✅ Excellent project health[/green]")
|
|
1985
|
+
elif report["overall_score"] >= 60:
|
|
1986
|
+
console.print("[yellow]⚠️ Good project health with room for improvement[/yellow]")
|
|
1987
|
+
else:
|
|
1988
|
+
console.print("[red]❌ Project needs attention[/red]")
|
|
1989
|
+
|
|
1990
|
+
# Category breakdown
|
|
1991
|
+
console.print(f"\n[bold]📊 Category Breakdown:[/bold]")
|
|
1992
|
+
for category_name, category_data in report["categories"].items():
|
|
1993
|
+
score = category_data["score"]
|
|
1994
|
+
icon = "🟢" if score >= 80 else "🟡" if score >= 60 else "🔴"
|
|
1995
|
+
console.print(f" {icon} {category_name.title()}: {score}/100")
|
|
1996
|
+
|
|
1997
|
+
# Critical issues
|
|
1998
|
+
if report["critical_issues"]:
|
|
1999
|
+
console.print(f"\n[bold red]🚨 Critical Issues:[/bold red]")
|
|
2000
|
+
for issue in report["critical_issues"]:
|
|
2001
|
+
console.print(f" {issue}")
|
|
2002
|
+
|
|
2003
|
+
# Category details if detailed
|
|
2004
|
+
if detailed:
|
|
2005
|
+
for category_name, category_data in report["categories"].items():
|
|
2006
|
+
console.print(f"\n[bold]{category_name.title()} Details:[/bold]")
|
|
2007
|
+
|
|
2008
|
+
if category_data["checks_passed"]:
|
|
2009
|
+
console.print("[green]✅ Passed Checks:[/green]")
|
|
2010
|
+
for check in category_data["checks_passed"][:5]:
|
|
2011
|
+
console.print(f" {check}")
|
|
2012
|
+
if len(category_data["checks_passed"]) > 5:
|
|
2013
|
+
console.print(f" ... and {len(category_data['checks_passed']) - 5} more")
|
|
2014
|
+
|
|
2015
|
+
if category_data["issues"]:
|
|
2016
|
+
console.print("[red]❌ Issues:[/red]")
|
|
2017
|
+
for issue in category_data["issues"]:
|
|
2018
|
+
console.print(f" {issue}")
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
def _fix_health_issues(project_path: Path, fixable_issues: list) -> None:
|
|
2022
|
+
"""Attempt to fix health issues automatically"""
|
|
2023
|
+
for issue_type, issue_data in fixable_issues:
|
|
2024
|
+
try:
|
|
2025
|
+
if issue_type == "create_dir":
|
|
2026
|
+
(project_path / issue_data).mkdir(parents=True, exist_ok=True)
|
|
2027
|
+
console.print(f"[green]✅ Created directory: {issue_data}/[/green]")
|
|
2028
|
+
|
|
2029
|
+
elif issue_type == "create_readme":
|
|
2030
|
+
readme_content = """# Project Name
|
|
2031
|
+
|
|
2032
|
+
## Description
|
|
2033
|
+
Add your project description here.
|
|
2034
|
+
|
|
2035
|
+
## Installation
|
|
2036
|
+
```bash
|
|
2037
|
+
# Add installation instructions
|
|
2038
|
+
```
|
|
2039
|
+
|
|
2040
|
+
## Usage
|
|
2041
|
+
```bash
|
|
2042
|
+
# Add usage instructions
|
|
2043
|
+
```
|
|
2044
|
+
|
|
2045
|
+
## Contributing
|
|
2046
|
+
Add contributing guidelines here.
|
|
2047
|
+
|
|
2048
|
+
## License
|
|
2049
|
+
Add license information here.
|
|
2050
|
+
"""
|
|
2051
|
+
with open(project_path / "README.md", "w", encoding="utf-8") as f:
|
|
2052
|
+
f.write(readme_content)
|
|
2053
|
+
console.print("[green]✅ Created README.md[/green]")
|
|
2054
|
+
|
|
2055
|
+
elif issue_type == "create_gitignore":
|
|
2056
|
+
gitignore_content = """# Python
|
|
2057
|
+
__pycache__/
|
|
2058
|
+
*.py[cod]
|
|
2059
|
+
*$py.class
|
|
2060
|
+
*.so
|
|
2061
|
+
.Python
|
|
2062
|
+
build/
|
|
2063
|
+
develop-eggs/
|
|
2064
|
+
dist/
|
|
2065
|
+
downloads/
|
|
2066
|
+
eggs/
|
|
2067
|
+
.eggs/
|
|
2068
|
+
lib/
|
|
2069
|
+
lib64/
|
|
2070
|
+
parts/
|
|
2071
|
+
sdist/
|
|
2072
|
+
var/
|
|
2073
|
+
wheels/
|
|
2074
|
+
*.egg-info/
|
|
2075
|
+
.installed.cfg
|
|
2076
|
+
*.egg
|
|
2077
|
+
|
|
2078
|
+
# Environment variables
|
|
2079
|
+
.env
|
|
2080
|
+
.env.local
|
|
2081
|
+
.env.*.local
|
|
2082
|
+
|
|
2083
|
+
# Secrets
|
|
2084
|
+
*.key
|
|
2085
|
+
*.pem
|
|
2086
|
+
*.p12
|
|
2087
|
+
secrets/
|
|
2088
|
+
*.secret
|
|
2089
|
+
|
|
2090
|
+
# IDE
|
|
2091
|
+
.vscode/
|
|
2092
|
+
.idea/
|
|
2093
|
+
*.swp
|
|
2094
|
+
*.swo
|
|
2095
|
+
|
|
2096
|
+
# OS
|
|
2097
|
+
.DS_Store
|
|
2098
|
+
Thumbs.db
|
|
2099
|
+
"""
|
|
2100
|
+
with open(project_path / ".gitignore", "w", encoding="utf-8") as f:
|
|
2101
|
+
f.write(gitignore_content)
|
|
2102
|
+
console.print("[green]✅ Created .gitignore[/green]")
|
|
2103
|
+
|
|
2104
|
+
elif issue_type == "create_scripts_dir":
|
|
2105
|
+
scripts_dir = project_path / "scripts"
|
|
2106
|
+
scripts_dir.mkdir(exist_ok=True)
|
|
2107
|
+
|
|
2108
|
+
# Create sample script
|
|
2109
|
+
sample_script = """#!/bin/bash
|
|
2110
|
+
# Sample automation script
|
|
2111
|
+
|
|
2112
|
+
echo "Running automation..."
|
|
2113
|
+
|
|
2114
|
+
# Add your automation commands here
|
|
2115
|
+
"""
|
|
2116
|
+
with open(scripts_dir / "setup.sh", "w", encoding="utf-8") as f:
|
|
2117
|
+
f.write(sample_script)
|
|
2118
|
+
os.chmod(scripts_dir / "setup.sh", 0o755)
|
|
2119
|
+
console.print("[green]✅ Created scripts directory with sample script[/green]")
|
|
2120
|
+
|
|
2121
|
+
except Exception as e:
|
|
2122
|
+
console.print(f"[red]❌ Could not fix {issue_type}: {str(e)}[/red]")
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
def _display_health_score(report: dict) -> None:
|
|
2126
|
+
"""Display final health score and recommendations"""
|
|
2127
|
+
console.print(f"\n[bold]🎯 Final Health Score: {report['overall_score']}/100[/bold]")
|
|
2128
|
+
|
|
2129
|
+
if report["recommendations"]:
|
|
2130
|
+
console.print(f"\n[bold]💡 Recommendations:[/bold]")
|
|
2131
|
+
for rec in report["recommendations"]:
|
|
2132
|
+
console.print(f" {rec}")
|
|
2133
|
+
|
|
2134
|
+
|
|
1096
2135
|
@app.command()
|
|
1097
2136
|
def version() -> None:
|
|
1098
2137
|
"""Show version information"""
|
|
1099
2138
|
try:
|
|
1100
2139
|
from . import __version__
|
|
1101
2140
|
except ImportError:
|
|
1102
|
-
__version__ = "1.
|
|
2141
|
+
__version__ = "1.3.0"
|
|
1103
2142
|
console.print(f"[bold blue]DevOps Project Generator[/bold blue] v{__version__}")
|
|
1104
2143
|
|
|
1105
2144
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devops-project-generator
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A CLI tool that scaffolds production-ready DevOps repositories
|
|
5
5
|
Author-email: NotHarshhaa <devops-project-generator@notHarshhaa.com>
|
|
6
6
|
License: MIT
|
|
@@ -167,6 +167,48 @@ devops-project-generator cleanup my-project --force
|
|
|
167
167
|
devops-project-generator cleanup my-project --keep-config
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
### Template Management (NEW v1.3.0)
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# List available templates
|
|
174
|
+
devops-project-generator template list
|
|
175
|
+
|
|
176
|
+
# Create custom template
|
|
177
|
+
devops-project-generator template create --name my-template
|
|
178
|
+
|
|
179
|
+
# Customize existing template
|
|
180
|
+
devops-project-generator template customize --name my-template
|
|
181
|
+
|
|
182
|
+
# Export templates
|
|
183
|
+
devops-project-generator template export --output ./templates
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Project Backup (NEW v1.3.0)
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Create backup
|
|
190
|
+
devops-project-generator backup create my-project
|
|
191
|
+
|
|
192
|
+
# List available backups
|
|
193
|
+
devops-project-generator backup list
|
|
194
|
+
|
|
195
|
+
# Restore from backup
|
|
196
|
+
devops-project-generator backup restore my-project --file backup.tar.gz
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Health Check (NEW v1.3.0)
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Basic health check
|
|
203
|
+
devops-project-generator health my-project
|
|
204
|
+
|
|
205
|
+
# Detailed health analysis
|
|
206
|
+
devops-project-generator health my-project --detailed
|
|
207
|
+
|
|
208
|
+
# Auto-fix health issues
|
|
209
|
+
devops-project-generator health my-project --fix
|
|
210
|
+
```
|
|
211
|
+
|
|
170
212
|
## 🏗️ Generated Project Structure (Example)
|
|
171
213
|
|
|
172
214
|
```
|
|
@@ -410,7 +452,7 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f
|
|
|
410
452
|
- [x] Template caching and pre-loading
|
|
411
453
|
- [x] Better user experience with improved messages
|
|
412
454
|
|
|
413
|
-
### v1.2 ✅
|
|
455
|
+
### v1.2 ✅
|
|
414
456
|
- [x] Project validation and structure checking
|
|
415
457
|
- [x] Configuration file management system
|
|
416
458
|
- [x] Project cleanup and teardown utilities
|
|
@@ -418,7 +460,14 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f
|
|
|
418
460
|
- [x] DevOps maturity scoring
|
|
419
461
|
- [x] Intelligent recommendations system
|
|
420
462
|
|
|
421
|
-
### v1.3
|
|
463
|
+
### v1.3 ✅ (Current)
|
|
464
|
+
- [x] Template management and customization system
|
|
465
|
+
- [x] Project backup and restore functionality
|
|
466
|
+
- [x] Comprehensive health monitoring and scoring
|
|
467
|
+
- [x] Auto-fix capabilities for common issues
|
|
468
|
+
- [x] Advanced project analysis and recommendations
|
|
469
|
+
|
|
470
|
+
### v1.4
|
|
422
471
|
- [ ] Support for Azure DevOps
|
|
423
472
|
- [ ] Additional cloud providers (GCP, Azure)
|
|
424
473
|
- [ ] More deployment targets (AWS ECS, Fargate)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devops-project-generator"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
description = "A CLI tool that scaffolds production-ready DevOps repositories"
|
|
9
9
|
authors = [{name = "NotHarshhaa", email = "devops-project-generator@notHarshhaa.com"}]
|
|
10
10
|
license = {text = "MIT"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|