mcp-souschef 2.8.0__py3-none-any.whl → 3.2.0__py3-none-any.whl

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 (36) hide show
  1. {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/METADATA +159 -384
  2. mcp_souschef-3.2.0.dist-info/RECORD +47 -0
  3. {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/WHEEL +1 -1
  4. souschef/__init__.py +31 -7
  5. souschef/assessment.py +1451 -105
  6. souschef/ci/common.py +126 -0
  7. souschef/ci/github_actions.py +3 -92
  8. souschef/ci/gitlab_ci.py +2 -52
  9. souschef/ci/jenkins_pipeline.py +2 -59
  10. souschef/cli.py +149 -16
  11. souschef/converters/playbook.py +378 -138
  12. souschef/converters/resource.py +12 -11
  13. souschef/converters/template.py +177 -0
  14. souschef/core/__init__.py +6 -1
  15. souschef/core/metrics.py +313 -0
  16. souschef/core/path_utils.py +233 -19
  17. souschef/core/validation.py +53 -0
  18. souschef/deployment.py +71 -12
  19. souschef/generators/__init__.py +13 -0
  20. souschef/generators/repo.py +695 -0
  21. souschef/parsers/attributes.py +1 -1
  22. souschef/parsers/habitat.py +1 -1
  23. souschef/parsers/inspec.py +25 -2
  24. souschef/parsers/metadata.py +5 -3
  25. souschef/parsers/recipe.py +1 -1
  26. souschef/parsers/resource.py +1 -1
  27. souschef/parsers/template.py +1 -1
  28. souschef/server.py +1039 -121
  29. souschef/ui/app.py +486 -374
  30. souschef/ui/pages/ai_settings.py +74 -8
  31. souschef/ui/pages/cookbook_analysis.py +3216 -373
  32. souschef/ui/pages/validation_reports.py +274 -0
  33. mcp_souschef-2.8.0.dist-info/RECORD +0 -42
  34. souschef/converters/cookbook_specific.py.backup +0 -109
  35. {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/entry_points.txt +0 -0
  36. {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,695 @@
1
+ """
2
+ Ansible repository structure generation.
3
+
4
+ This module analyses converted Chef cookbooks and generates appropriate
5
+ Ansible repository structures with proper organisation, configuration files,
6
+ and git initialisation.
7
+ """
8
+
9
+ import subprocess
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ # Constants
15
+ HOSTS_FILE = "hosts.yml"
16
+
17
+
18
+ class RepoType(Enum):
19
+ """Types of Ansible repository structures."""
20
+
21
+ INVENTORY_FIRST = "inventory_first" # Classic inventory-first (infra management)
22
+ PLAYBOOKS_ROLES = "playbooks_roles" # Simpler playbooks + roles
23
+ COLLECTION = "collection" # Ansible Collection (reusable automation)
24
+ MONO_REPO = "mono_repo" # Multi-project mono-repo
25
+
26
+
27
+ def analyse_conversion_output(
28
+ cookbook_path: str,
29
+ num_recipes: int = 0,
30
+ num_roles: int = 0,
31
+ has_multiple_apps: bool = False,
32
+ needs_multi_env: bool = True,
33
+ ai_provider: str = "",
34
+ api_key: str = "",
35
+ model: str = "",
36
+ ) -> RepoType:
37
+ """
38
+ Analyse conversion output and determine the best repo type.
39
+
40
+ Uses AI assessment if credentials are provided to make smarter decisions
41
+ based on cookbook complexity, otherwise falls back to heuristic rules.
42
+
43
+ Args:
44
+ cookbook_path: Path to the Chef cookbook
45
+ num_recipes: Number of recipes converted
46
+ num_roles: Number of roles that would be created
47
+ has_multiple_apps: Whether multiple applications are being managed
48
+ needs_multi_env: Whether multiple environments are needed
49
+ ai_provider: AI provider name (anthropic, openai, watson) optional
50
+ api_key: API key for AI provider optional
51
+ model: AI model to use optional
52
+
53
+ Returns:
54
+ The recommended RepoType
55
+
56
+ """
57
+ # Try AI-enhanced analysis if credentials provided
58
+ if ai_provider and api_key:
59
+ repo_type_ai = _analyse_with_ai(
60
+ cookbook_path, num_recipes, num_roles, ai_provider, api_key, model
61
+ )
62
+ if repo_type_ai is not None:
63
+ return repo_type_ai
64
+
65
+ # Fall back to heuristic-based analysis
66
+ return _analyse_with_heuristics(
67
+ num_recipes, num_roles, has_multiple_apps, needs_multi_env
68
+ )
69
+
70
+
71
+ def _analyse_with_heuristics(
72
+ num_recipes: int,
73
+ num_roles: int,
74
+ has_multiple_apps: bool,
75
+ needs_multi_env: bool,
76
+ ) -> RepoType:
77
+ """Apply heuristic rules for repository type selection."""
78
+ # Collection layout for reusable automation (3+ roles)
79
+ if num_roles >= 3:
80
+ return RepoType.COLLECTION
81
+
82
+ # Mono-repo for multiple applications
83
+ if has_multiple_apps:
84
+ return RepoType.MONO_REPO
85
+
86
+ # Simple playbooks + roles for small projects
87
+ if not needs_multi_env and num_recipes <= 3:
88
+ return RepoType.PLAYBOOKS_ROLES
89
+
90
+ # Default: inventory-first for infrastructure management
91
+ return RepoType.INVENTORY_FIRST
92
+
93
+
94
+ def _analyse_with_ai(
95
+ cookbook_path: str,
96
+ num_recipes: int,
97
+ num_roles: int,
98
+ ai_provider: str,
99
+ api_key: str,
100
+ model: str,
101
+ ) -> RepoType | None:
102
+ """
103
+ Analyse cookbook using AI to determine optimal repository type.
104
+
105
+ Args:
106
+ cookbook_path: Path to the Chef cookbook
107
+ num_recipes: Number of recipes converted
108
+ num_roles: Number of roles estimate
109
+ ai_provider: AI provider name
110
+ api_key: API key for AI provider
111
+ model: AI model to use
112
+
113
+ Returns:
114
+ Recommended RepoType if AI assessment succeeds, None otherwise
115
+
116
+ """
117
+ try:
118
+ # Import here to avoid circular dependencies
119
+ from souschef.assessment import assess_single_cookbook_with_ai # noqa: F401
120
+
121
+ # Perform AI assessment
122
+ assessment = assess_single_cookbook_with_ai(
123
+ cookbook_path=cookbook_path,
124
+ ai_provider=ai_provider,
125
+ api_key=api_key,
126
+ model=model or "claude-3-5-sonnet-20241022",
127
+ temperature=0.3,
128
+ max_tokens=2000,
129
+ )
130
+
131
+ # Check for errors in assessment
132
+ if "error" in assessment:
133
+ return None
134
+
135
+ # Extract complexity score (0-100)
136
+ complexity_score = assessment.get("complexity_score", 0)
137
+
138
+ # AI-informed decision making
139
+ # High complexity + multiple roles should be a collection
140
+ if complexity_score > 70 and num_roles >= 2:
141
+ return RepoType.COLLECTION
142
+
143
+ # Medium-high complexity with multiple roles
144
+ if complexity_score > 50 and num_roles >= 2:
145
+ return RepoType.COLLECTION
146
+
147
+ # High complexity generally benefits from inventory-first
148
+ if complexity_score > 70:
149
+ return RepoType.INVENTORY_FIRST
150
+
151
+ # Low complexity recipes with minimal roles
152
+ if complexity_score < 30 and num_roles <= 1 and num_recipes <= 3:
153
+ return RepoType.PLAYBOOKS_ROLES
154
+
155
+ # Medium complexity with simple structure
156
+ if complexity_score < 50 and num_roles <= 1:
157
+ return RepoType.PLAYBOOKS_ROLES
158
+
159
+ # Default to inventory-first for AI-assessed cookbooks
160
+ return RepoType.INVENTORY_FIRST
161
+
162
+ except Exception:
163
+ # If AI fails, return None to fall back to heuristics
164
+ return None
165
+
166
+
167
+ def _create_ansible_cfg(repo_path: Path, repo_type: RepoType) -> None:
168
+ """Create ansible.cfg with appropriate settings."""
169
+ inventory_path = (
170
+ "./inventory" if repo_type == RepoType.PLAYBOOKS_ROLES else "./inventories/prod"
171
+ )
172
+ roles_path = (
173
+ "./roles"
174
+ if repo_type != RepoType.COLLECTION
175
+ else "./ansible_collections/*/*/roles"
176
+ )
177
+
178
+ cfg_content = f"""[defaults]
179
+ inventory = {inventory_path}
180
+ roles_path = {roles_path}
181
+ host_key_checking = False
182
+ retry_files_enabled = False
183
+ gathering = smart
184
+ fact_caching = jsonfile
185
+ fact_caching_connection = /tmp/ansible_facts
186
+ fact_caching_timeout = 3600
187
+ callbacks_enabled = profile_tasks, timer
188
+
189
+ [privilege_escalation]
190
+ become = True
191
+ become_method = sudo
192
+ become_user = root
193
+ become_ask_pass = False
194
+
195
+ [ssh_connection]
196
+ pipelining = True
197
+ ssh_args = -o ControlMaster=auto -o ControlPersist=60s
198
+
199
+ [diff]
200
+ always = False
201
+ context = 3
202
+ """
203
+ (repo_path / "ansible.cfg").write_text(cfg_content)
204
+
205
+
206
+ def _create_requirements_yml(repo_path: Path) -> None:
207
+ """Create requirements.yml for dependencies."""
208
+ requirements_content = """---
209
+ # Ansible Collections
210
+ collections:
211
+ - name: ansible.posix
212
+ version: ">=1.5.0"
213
+ - name: community.general
214
+ version: ">=8.0.0"
215
+
216
+ # Ansible Roles (if using Galaxy roles)
217
+ roles: []
218
+ """
219
+ (repo_path / "requirements.yml").write_text(requirements_content)
220
+
221
+
222
+ def _create_gitignore(repo_path: Path) -> None:
223
+ """Create .gitignore for Ansible projects."""
224
+ gitignore_content = """# Ansible
225
+ *.retry
226
+ .ansible/
227
+ /tmp/
228
+ /temp/
229
+
230
+ # Vault
231
+ vault-password.txt
232
+ .vault_pass*
233
+
234
+ # Python
235
+ __pycache__/
236
+ *.py[cod]
237
+ *$py.class
238
+ *.so
239
+ .Python
240
+ venv/
241
+ ENV/
242
+
243
+ # IDE
244
+ .vscode/
245
+ .idea/
246
+ *.swp
247
+ *.swo
248
+ *~
249
+
250
+ # OS
251
+ .DS_Store
252
+ Thumbs.db
253
+
254
+ # Logs
255
+ *.log
256
+ """
257
+ (repo_path / ".gitignore").write_text(gitignore_content)
258
+
259
+
260
+ def _create_gitattributes(repo_path: Path) -> None:
261
+ """Create .gitattributes for consistent line endings and file handling."""
262
+ gitattributes_content = """# Auto detect text files and perform LF normalisation
263
+ * text=auto eol=lf
264
+
265
+ # Explicitly declare text files
266
+ *.py text
267
+ *.yml text
268
+ *.yaml text
269
+ *.ini text
270
+ *.cfg text
271
+ *.conf text
272
+ *.txt text
273
+ *.md text
274
+ *.rst text
275
+ *.j2 text
276
+
277
+ # Declare files that will always have LF line endings on checkout
278
+ *.sh text eol=lf
279
+
280
+ # Denote binary files
281
+ *.png binary
282
+ *.jpg binary
283
+ *.jpeg binary
284
+ *.gif binary
285
+ *.ico binary
286
+ *.zip binary
287
+ *.tar binary
288
+ *.gz binary
289
+ """
290
+ (repo_path / ".gitattributes").write_text(gitattributes_content)
291
+
292
+
293
+ def _create_editorconfig(repo_path: Path) -> None:
294
+ """Create .editorconfig for consistent coding styles."""
295
+ editorconfig_content = """# EditorConfig for Ansible projects
296
+ # https://editorconfig.org
297
+
298
+ root = true
299
+
300
+ # All files
301
+ [*]
302
+ charset = utf-8
303
+ end_of_line = lf
304
+ insert_final_newline = true
305
+ trim_trailing_whitespace = true
306
+
307
+ # YAML files (Ansible playbooks, vars, etc.)
308
+ [*.{yml,yaml}]
309
+ indent_style = space
310
+ indent_size = 2
311
+
312
+ # Python files
313
+ [*.py]
314
+ indent_style = space
315
+ indent_size = 4
316
+ max_line_length = 88
317
+
318
+ # Jinja2 templates
319
+ [*.j2]
320
+ indent_style = space
321
+ indent_size = 2
322
+
323
+ # Shell scripts
324
+ [*.sh]
325
+ indent_style = space
326
+ indent_size = 2
327
+
328
+ # Markdown
329
+ [*.md]
330
+ trim_trailing_whitespace = false
331
+ max_line_length = off
332
+
333
+ # Makefile
334
+ [Makefile]
335
+ indent_style = tab
336
+ """
337
+ (repo_path / ".editorconfig").write_text(editorconfig_content)
338
+
339
+
340
+ def _create_readme(repo_path: Path, repo_type: RepoType, org_name: str) -> None:
341
+ """Create README.md with usage instructions."""
342
+ type_desc = {
343
+ RepoType.INVENTORY_FIRST: (
344
+ "inventory-first structure for infrastructure management"
345
+ ),
346
+ RepoType.PLAYBOOKS_ROLES: "simple playbooks and roles structure",
347
+ RepoType.COLLECTION: "Ansible Collection for reusable automation",
348
+ RepoType.MONO_REPO: "mono-repository for multiple projects",
349
+ }
350
+
351
+ readme_content = f"""# {org_name} Ansible Automation
352
+
353
+ This repository uses a {type_desc.get(repo_type, "standard")} approach.
354
+
355
+ ## Structure
356
+
357
+ Generated from Chef cookbook conversion using SousChef.
358
+
359
+ ## Quick Start
360
+
361
+ 1. Install dependencies:
362
+ ```bash
363
+ ansible-galaxy install -r requirements.yml
364
+ ```
365
+
366
+ 2. Configure inventory:
367
+ - Update inventory files with your hosts
368
+ - Set environment-specific variables in group_vars/host_vars
369
+
370
+ 3. Run playbooks:
371
+ ```bash
372
+ ansible-playbook playbooks/site.yml
373
+ ```
374
+
375
+ ## Requirements
376
+
377
+ - Ansible >= 2.15
378
+ - Python >= 3.9
379
+
380
+ ## Security
381
+
382
+ - Never commit secrets to git
383
+ - Use Ansible Vault for sensitive data: `ansible-vault encrypt_string`
384
+ - Store vault password securely (not in this repo)
385
+
386
+ ## Documentation
387
+
388
+ - [Ansible Best Practices](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html)
389
+ - [Ansible Vault](https://docs.ansible.com/ansible/latest/user_guide/vault.html)
390
+
391
+ ---
392
+ Generated by [SousChef](https://github.com/kpeacocke/souschef)
393
+ """
394
+ (repo_path / "README.md").write_text(readme_content)
395
+
396
+
397
+ def _create_inventory_first_structure(repo_path: Path) -> None:
398
+ """Create classic inventory-first repo structure."""
399
+ # Top-level directories
400
+ (repo_path / "inventories" / "prod" / "group_vars").mkdir(parents=True)
401
+ (repo_path / "inventories" / "prod" / "host_vars").mkdir(parents=True)
402
+ (repo_path / "inventories" / "nonprod" / "group_vars").mkdir(parents=True)
403
+ (repo_path / "inventories" / "nonprod" / "host_vars").mkdir(parents=True)
404
+ (repo_path / "playbooks").mkdir()
405
+ (repo_path / "roles").mkdir()
406
+ (repo_path / "filter_plugins").mkdir()
407
+ (repo_path / "library").mkdir()
408
+
409
+ # Create sample inventory files
410
+ prod_hosts = """---
411
+ all:
412
+ children:
413
+ webservers:
414
+ hosts:
415
+ web1.example.com:
416
+ web2.example.com:
417
+ databases:
418
+ hosts:
419
+ db1.example.com:
420
+ """
421
+ (repo_path / "inventories" / "prod" / HOSTS_FILE).write_text(prod_hosts)
422
+ (repo_path / "inventories" / "nonprod" / HOSTS_FILE).write_text(prod_hosts)
423
+
424
+ # Create sample group_vars
425
+ all_vars = """---
426
+ # Variables for all hosts
427
+ ansible_user: ansible
428
+ ansible_python_interpreter: /usr/bin/python3
429
+ """
430
+ (repo_path / "inventories" / "prod" / "group_vars" / "all.yml").write_text(all_vars)
431
+
432
+ # Create sample playbook
433
+ site_playbook = """---
434
+ - name: Site-wide configuration
435
+ hosts: all
436
+ gather_facts: true
437
+ tasks:
438
+ - name: Ensure system is up to date
439
+ ansible.builtin.package:
440
+ name: "*"
441
+ state: latest
442
+ when: ansible_os_family == "RedHat"
443
+ """
444
+ (repo_path / "playbooks" / "site.yml").write_text(site_playbook)
445
+
446
+
447
+ def _create_playbooks_roles_structure(repo_path: Path) -> None:
448
+ """Create simple playbooks + roles structure."""
449
+ (repo_path / "inventory" / "group_vars").mkdir(parents=True)
450
+ (repo_path / "inventory" / "host_vars").mkdir(parents=True)
451
+ (repo_path / "playbooks").mkdir()
452
+ (repo_path / "roles").mkdir()
453
+
454
+ # Simple inventory
455
+ hosts = """---
456
+ all:
457
+ hosts:
458
+ localhost:
459
+ ansible_connection: local
460
+ """
461
+ (repo_path / "inventory" / "hosts.yml").write_text(hosts)
462
+
463
+ # Simple playbook
464
+ deploy_playbook = """---
465
+ - name: Deploy application
466
+ hosts: all
467
+ gather_facts: true
468
+ roles:
469
+ - common
470
+ """
471
+ (repo_path / "playbooks" / "deploy.yml").write_text(deploy_playbook)
472
+
473
+
474
+ def _create_collection_structure(repo_path: Path, org_name: str) -> None:
475
+ """Create Ansible Collection layout."""
476
+ collection_name = org_name.lower().replace("-", "_")
477
+ base_path = repo_path / "ansible_collections" / collection_name / "platform"
478
+
479
+ (base_path / "plugins" / "modules").mkdir(parents=True)
480
+ (base_path / "plugins" / "filter").mkdir(parents=True)
481
+ (base_path / "roles").mkdir(parents=True)
482
+ (base_path / "playbooks").mkdir(parents=True)
483
+ (base_path / "docs").mkdir(parents=True)
484
+ (base_path / "tests").mkdir(parents=True)
485
+
486
+ # Galaxy metadata
487
+ galaxy_yml = f"""---
488
+ namespace: {collection_name}
489
+ name: platform
490
+ version: 1.0.0
491
+ readme: README.md
492
+ authors:
493
+ - Your Name <you@example.com>
494
+ description: Platform automation collection converted from Chef
495
+ license:
496
+ - MIT
497
+ tags:
498
+ - infrastructure
499
+ - automation
500
+ dependencies: {{}}
501
+ repository: https://github.com/{org_name}/ansible-platform
502
+ """
503
+ (base_path / "galaxy.yml").write_text(galaxy_yml)
504
+
505
+ # Collection README
506
+ collection_readme = f"""# {collection_name}.platform Collection
507
+
508
+ Ansible Collection for platform automation.
509
+
510
+ ## Installation
511
+
512
+ ```bash
513
+ ansible-galaxy collection install {collection_name}.platform
514
+ ```
515
+
516
+ ## Usage
517
+
518
+ ```yaml
519
+ - hosts: all
520
+ collections:
521
+ - {collection_name}.platform
522
+ tasks:
523
+ - import_role:
524
+ name: common
525
+ ```
526
+ """
527
+ (base_path / "README.md").write_text(collection_readme)
528
+
529
+
530
+ def _create_mono_repo_structure(repo_path: Path) -> None:
531
+ """Create multi-project mono-repo structure."""
532
+ (repo_path / "inventories" / "prod").mkdir(parents=True)
533
+ (repo_path / "inventories" / "nonprod").mkdir(parents=True)
534
+ (repo_path / "projects" / "app1" / "playbooks").mkdir(parents=True)
535
+ (repo_path / "projects" / "app1" / "roles").mkdir(parents=True)
536
+ (repo_path / "shared_roles").mkdir()
537
+ (repo_path / "collections").mkdir()
538
+
539
+ # Sample project structure
540
+ app1_playbook = """---
541
+ - name: Deploy App1
542
+ hosts: app1_servers
543
+ gather_facts: true
544
+ roles:
545
+ - role: shared_roles/common
546
+ - role: app1_config
547
+ """
548
+ (repo_path / "projects" / "app1" / "playbooks" / "deploy.yml").write_text(
549
+ app1_playbook
550
+ )
551
+
552
+
553
+ def _init_git_repo(repo_path: Path) -> str:
554
+ """Initialise git repository with souschef user configuration."""
555
+ try:
556
+ # Initialize git
557
+ subprocess.run(
558
+ ["git", "init"],
559
+ cwd=repo_path,
560
+ check=True,
561
+ capture_output=True,
562
+ text=True,
563
+ )
564
+
565
+ # Configure git user for this repository
566
+ subprocess.run(
567
+ ["git", "config", "user.name", "souschef"],
568
+ cwd=repo_path,
569
+ check=True,
570
+ capture_output=True,
571
+ text=True,
572
+ )
573
+
574
+ subprocess.run(
575
+ ["git", "config", "user.email", "souschef@ansible.local"],
576
+ cwd=repo_path,
577
+ check=True,
578
+ capture_output=True,
579
+ text=True,
580
+ )
581
+
582
+ # Create initial commit
583
+ subprocess.run(
584
+ ["git", "add", "."],
585
+ cwd=repo_path,
586
+ check=True,
587
+ capture_output=True,
588
+ text=True,
589
+ )
590
+
591
+ subprocess.run(
592
+ ["git", "commit", "-m", "Initial commit: Ansible repo structure"],
593
+ cwd=repo_path,
594
+ check=True,
595
+ capture_output=True,
596
+ text=True,
597
+ )
598
+
599
+ return "Git repository initialised with initial commit"
600
+ except subprocess.CalledProcessError as e:
601
+ return f"Git initialisation failed: {e.stderr}"
602
+ except FileNotFoundError:
603
+ return "Git not found - skipped repository initialisation"
604
+
605
+
606
+ def generate_ansible_repository(
607
+ output_path: str,
608
+ repo_type: RepoType | str,
609
+ org_name: str = "myorg",
610
+ init_git: bool = True,
611
+ ) -> dict[str, Any]:
612
+ """
613
+ Generate a complete Ansible repository structure.
614
+
615
+ Args:
616
+ output_path: Path where the repository should be created
617
+ repo_type: Type of repository structure to generate
618
+ org_name: Organisation name for the repository
619
+ init_git: Whether to initialise a git repository
620
+
621
+ Returns:
622
+ Dictionary with generation results including:
623
+ - success: Whether generation succeeded
624
+ - repo_path: Path to the created repository
625
+ - repo_type: Type of repository created
626
+ - files_created: List of files created
627
+ - git_status: Git initialisation status
628
+
629
+ """
630
+ # Convert string to enum if needed
631
+ if isinstance(repo_type, str):
632
+ try:
633
+ repo_type = RepoType(repo_type)
634
+ except ValueError:
635
+ return {
636
+ "success": False,
637
+ "error": f"Invalid repo_type: {repo_type}. "
638
+ f"Valid types: {[t.value for t in RepoType]}",
639
+ }
640
+
641
+ repo_path = Path(output_path)
642
+
643
+ # Check if path already exists
644
+ if repo_path.exists():
645
+ return {
646
+ "success": False,
647
+ "error": f"Path already exists: {output_path}",
648
+ }
649
+
650
+ try:
651
+ # Create base directory
652
+ repo_path.mkdir(parents=True)
653
+
654
+ # Create common files
655
+ _create_ansible_cfg(repo_path, repo_type)
656
+ _create_requirements_yml(repo_path)
657
+ _create_gitignore(repo_path)
658
+ _create_gitattributes(repo_path)
659
+ _create_editorconfig(repo_path)
660
+ _create_readme(repo_path, repo_type, org_name)
661
+
662
+ # Create structure based on repo type
663
+ if repo_type == RepoType.INVENTORY_FIRST:
664
+ _create_inventory_first_structure(repo_path)
665
+ elif repo_type == RepoType.PLAYBOOKS_ROLES:
666
+ _create_playbooks_roles_structure(repo_path)
667
+ elif repo_type == RepoType.COLLECTION:
668
+ _create_collection_structure(repo_path, org_name)
669
+ elif repo_type == RepoType.MONO_REPO:
670
+ _create_mono_repo_structure(repo_path)
671
+
672
+ # Collect created files
673
+ files_created = [
674
+ str(p.relative_to(repo_path)) for p in repo_path.rglob("*") if p.is_file()
675
+ ]
676
+
677
+ # Initialize git if requested
678
+ git_status = ""
679
+ if init_git:
680
+ git_status = _init_git_repo(repo_path)
681
+
682
+ return {
683
+ "success": True,
684
+ "repo_path": str(repo_path),
685
+ "repo_type": repo_type.value,
686
+ "files_created": sorted(files_created),
687
+ "git_status": git_status,
688
+ "message": f"Successfully created {repo_type.value} repository structure",
689
+ }
690
+
691
+ except Exception as e:
692
+ return {
693
+ "success": False,
694
+ "error": f"Failed to generate repository: {e}",
695
+ }
@@ -41,7 +41,7 @@ def parse_attributes(path: str, resolve_precedence: bool = True) -> str:
41
41
  """
42
42
  try:
43
43
  file_path = _normalize_path(path)
44
- content = file_path.read_text(encoding="utf-8")
44
+ content = file_path.read_text(encoding="utf-8") # nosonar
45
45
 
46
46
  attributes = _extract_attributes(content)
47
47
 
@@ -37,7 +37,7 @@ def parse_habitat_plan(plan_path: str) -> str:
37
37
  if normalized_path.is_dir():
38
38
  return ERROR_IS_DIRECTORY.format(path=normalized_path)
39
39
 
40
- content = normalized_path.read_text(encoding="utf-8")
40
+ content = normalized_path.read_text(encoding="utf-8") # nosonar
41
41
  metadata: dict[str, Any] = {
42
42
  "package": {},
43
43
  "dependencies": {"build": [], "runtime": []},