git-repokit 0.9.10__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 (174) hide show
  1. git_repokit-0.9.10.dist-info/METADATA +422 -0
  2. git_repokit-0.9.10.dist-info/RECORD +174 -0
  3. git_repokit-0.9.10.dist-info/WHEEL +5 -0
  4. git_repokit-0.9.10.dist-info/entry_points.txt +2 -0
  5. git_repokit-0.9.10.dist-info/licenses/LICENSE +9 -0
  6. git_repokit-0.9.10.dist-info/top_level.txt +1 -0
  7. repokit/__init__.py +8 -0
  8. repokit/__main__.py +12 -0
  9. repokit/__version__.py +4 -0
  10. repokit/_vendor/__init__.py +0 -0
  11. repokit/_vendor/ailib/__init__.py +1 -0
  12. repokit/_vendor/ailib/analyzer.py +177 -0
  13. repokit/_vendor/ailib/backend.py +95 -0
  14. repokit/_vendor/ailib/cache.py +97 -0
  15. repokit/_vendor/ailib/parsers.py +52 -0
  16. repokit/_vendor/ailib/transports/__init__.py +16 -0
  17. repokit/_vendor/ailib/transports/cli.py +312 -0
  18. repokit/_vendor/ailib/transports/openai_compat.py +383 -0
  19. repokit/_vendor/ailib/transports/prompt_file.py +41 -0
  20. repokit/_vendor/ailib/types.py +224 -0
  21. repokit/auth_integration.py +244 -0
  22. repokit/badges.py +183 -0
  23. repokit/branch_utils.py +364 -0
  24. repokit/cli.py +2997 -0
  25. repokit/common_kit.py +202 -0
  26. repokit/config.py +686 -0
  27. repokit/defaults.py +220 -0
  28. repokit/directory_analyzer.py +1244 -0
  29. repokit/directory_profiles.py +423 -0
  30. repokit/git_config.py +370 -0
  31. repokit/github_create.py +142 -0
  32. repokit/github_setup.py +248 -0
  33. repokit/guardrails.py +707 -0
  34. repokit/history_cleaner.py +449 -0
  35. repokit/history_protection.py +434 -0
  36. repokit/hooks/pre-commit-guardrails +122 -0
  37. repokit/hooks/pre-commit-template +28 -0
  38. repokit/layers.py +510 -0
  39. repokit/licenses/0bsd.txt +14 -0
  40. repokit/licenses/LICENSES.md +27 -0
  41. repokit/licenses/__init__.py +142 -0
  42. repokit/licenses/agpl-3.0.txt +661 -0
  43. repokit/licenses/all-rights-reserved.txt +10 -0
  44. repokit/licenses/apache-2.0.txt +201 -0
  45. repokit/licenses/bsd-2-clause.txt +24 -0
  46. repokit/licenses/bsd-3-clause.txt +28 -0
  47. repokit/licenses/bsl-1.0.txt +23 -0
  48. repokit/licenses/cc-by-4.0.txt +395 -0
  49. repokit/licenses/cc-by-sa-4.0.txt +427 -0
  50. repokit/licenses/cc0-1.0.txt +121 -0
  51. repokit/licenses/epl-2.0.txt +277 -0
  52. repokit/licenses/gpl-2.0.txt +339 -0
  53. repokit/licenses/gpl-3.0.txt +674 -0
  54. repokit/licenses/isc.txt +15 -0
  55. repokit/licenses/lgpl-2.1.txt +504 -0
  56. repokit/licenses/lgpl-3.0.txt +165 -0
  57. repokit/licenses/mit.txt +21 -0
  58. repokit/licenses/mpl-2.0.txt +373 -0
  59. repokit/licenses/unlicense.txt +24 -0
  60. repokit/readme_blocks.py +112 -0
  61. repokit/remote_integration.py +627 -0
  62. repokit/repo_manager.py +2796 -0
  63. repokit/template_engine.py +391 -0
  64. repokit/templates/ai/claude/CLAUDE-nested.md.template +25 -0
  65. repokit/templates/ai/claude/CLAUDE.md.template +98 -0
  66. repokit/templates/ai/claude/instructions/step1_context_rebuilder.md +33 -0
  67. repokit/templates/ai/claude/instructions/step2_dev_workflow_process.md +67 -0
  68. repokit/templates/ai/claude/instructions/step3_context_bridge.md +52 -0
  69. repokit/templates/common/CHANGELOG.md.template +15 -0
  70. repokit/templates/common/CONTRIBUTING.md.template +31 -0
  71. repokit/templates/common/README.md.template +30 -0
  72. repokit/templates/common/gitattributes.template +17 -0
  73. repokit/templates/common/gitignore.template +141 -0
  74. repokit/templates/extras/FUNDING.yml.template +13 -0
  75. repokit/templates/extras/ROADMAP.md.template +17 -0
  76. repokit/templates/extras/dependabot.yml.template +22 -0
  77. repokit/templates/extras/one-offs-README.md.template +28 -0
  78. repokit/templates/extras/platform-support.md.template +11 -0
  79. repokit/templates/extras/pull_request_template.md.template +25 -0
  80. repokit/templates/extras/release-template.md.template +34 -0
  81. repokit/templates/extras/settings.json.template +11 -0
  82. repokit/templates/extras/stale.yml.template +14 -0
  83. repokit/templates/github/CODEOWNERS.template +11 -0
  84. repokit/templates/github/ISSUE_TEMPLATE/bug-report.md.template +29 -0
  85. repokit/templates/github/ISSUE_TEMPLATE/feature-request.md.template +19 -0
  86. repokit/templates/github/issues/notes.md.template +18 -0
  87. repokit/templates/github/issues/roadmap.md.template +15 -0
  88. repokit/templates/hooks/pre-commit.template +64 -0
  89. repokit/templates/layers/archetypes/any/claude-plugin/.claude-plugin/marketplace.json +20 -0
  90. repokit/templates/layers/archetypes/any/claude-plugin/plugin/.claude-plugin/plugin.json +15 -0
  91. repokit/templates/layers/archetypes/any/claude-plugin/plugin/README.md +28 -0
  92. repokit/templates/layers/archetypes/c-cpp/cmake-app/.github/workflows/ci-c-cpp.yml +24 -0
  93. repokit/templates/layers/archetypes/c-cpp/cmake-app/.repokit-common.toml.rkappend +4 -0
  94. repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/launch.json +29 -0
  95. repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/tasks.json +26 -0
  96. repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakeLists.txt +47 -0
  97. repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakePresets.json +23 -0
  98. repokit/templates/layers/archetypes/c-cpp/cmake-app/cmake/GenerateStamp.cmake +45 -0
  99. repokit/templates/layers/archetypes/c-cpp/cmake-app/src/main.cpp +27 -0
  100. repokit/templates/layers/archetypes/c-cpp/cmake-app/tests/test_version.cpp +20 -0
  101. repokit/templates/layers/archetypes/c-cpp/cmake-app/vcpkg.json +5 -0
  102. repokit/templates/layers/archetypes/c-cpp/minimal/.vscode/launch.json +27 -0
  103. repokit/templates/layers/archetypes/c-cpp/minimal/Makefile +10 -0
  104. repokit/templates/layers/archetypes/c-cpp/minimal/build.bat +7 -0
  105. repokit/templates/layers/archetypes/c-cpp/minimal/src/__RK_SNAKE__.c +26 -0
  106. repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/launch.json +17 -0
  107. repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/tasks.json +20 -0
  108. repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.sln +19 -0
  109. repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.vcxproj +41 -0
  110. repokit/templates/layers/archetypes/c-cpp/msbuild/src/main.cpp +26 -0
  111. repokit/templates/layers/archetypes/js-ts/claude-plugin/.repokit-common.toml +28 -0
  112. repokit/templates/layers/archetypes/js-ts/claude-plugin/.vscode/launch.json +34 -0
  113. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/hooks.json +15 -0
  114. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/scripts/session-start.mjs +23 -0
  115. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/tests/manifest.test.js +30 -0
  116. repokit/templates/layers/archetypes/js-ts/node/.repokit-common.toml.rkappend +6 -0
  117. repokit/templates/layers/archetypes/js-ts/node/package.json +13 -0
  118. repokit/templates/layers/archetypes/js-ts/node-package/.github/workflows/release.yml +39 -0
  119. repokit/templates/layers/archetypes/js-ts/node-package/package.json +32 -0
  120. repokit/templates/layers/archetypes/js-ts/node-package/src/index.ts +13 -0
  121. repokit/templates/layers/archetypes/js-ts/node-package/tests/index.test.ts +11 -0
  122. repokit/templates/layers/archetypes/js-ts/script/.vscode/launch.json +34 -0
  123. repokit/templates/layers/archetypes/js-ts/script/__RK_PROJECT__.js +37 -0
  124. repokit/templates/layers/archetypes/js-ts/script/tests/version.test.js +26 -0
  125. repokit/templates/layers/archetypes/js-ts/ts/package.json +18 -0
  126. repokit/templates/layers/archetypes/js-ts/ts/tsconfig.json +15 -0
  127. repokit/templates/layers/archetypes/python/claude-plugin/.github/workflows/ci-python.yml +44 -0
  128. repokit/templates/layers/archetypes/python/claude-plugin/.repokit-common.toml +21 -0
  129. repokit/templates/layers/archetypes/python/claude-plugin/.vscode/launch.json +33 -0
  130. repokit/templates/layers/archetypes/python/claude-plugin/VERSION +1 -0
  131. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/hooks.json +15 -0
  132. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/run-hook.mjs +42 -0
  133. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/session_start.py +31 -0
  134. repokit/templates/layers/archetypes/python/claude-plugin/plugin/tests/test_manifest.py +42 -0
  135. repokit/templates/layers/archetypes/python/pypi/.github/workflows/release.yml +63 -0
  136. repokit/templates/layers/archetypes/python/pypi/.vscode/launch.json +43 -0
  137. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__init__.py +5 -0
  138. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__main__.py +6 -0
  139. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/_version.py +96 -0
  140. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/cli.py +18 -0
  141. repokit/templates/layers/archetypes/python/pypi/pyproject.toml +71 -0
  142. repokit/templates/layers/archetypes/python/pypi/tests/conftest.py +11 -0
  143. repokit/templates/layers/archetypes/python/pypi/tests/test_version.py +50 -0
  144. repokit/templates/layers/archetypes/python/script/.github/workflows/ci-python.yml +43 -0
  145. repokit/templates/layers/archetypes/python/script/.repokit-common.toml +14 -0
  146. repokit/templates/layers/archetypes/python/script/.vscode/launch.json +33 -0
  147. repokit/templates/layers/archetypes/python/script/VERSION +1 -0
  148. repokit/templates/layers/archetypes/python/script/__RK_PROJECT__.py +40 -0
  149. repokit/templates/layers/archetypes/python/script/tests/test_version.py +25 -0
  150. repokit/templates/layers/archetypes/python/setuppy/requirements.txt +8 -0
  151. repokit/templates/layers/archetypes/python/setuppy/setup.py +23 -0
  152. repokit/templates/layers/languages/c-cpp/.gitignore.rkappend +20 -0
  153. repokit/templates/layers/languages/c-cpp/.repokit-common.toml +7 -0
  154. repokit/templates/layers/languages/c-cpp/include/__RK_SNAKE__/version.h +20 -0
  155. repokit/templates/layers/languages/c-cpp/src/app.rc +25 -0
  156. repokit/templates/layers/languages/js-ts/.editorconfig +15 -0
  157. repokit/templates/layers/languages/js-ts/.github/workflows/ci-js-ts.yml +41 -0
  158. repokit/templates/layers/languages/js-ts/.gitignore.rkappend +13 -0
  159. repokit/templates/layers/languages/js-ts/.repokit-common.toml +19 -0
  160. repokit/templates/layers/languages/js-ts/.vscode/launch.json +24 -0
  161. repokit/templates/layers/languages/js-ts/VERSION +1 -0
  162. repokit/templates/layers/languages/python/.github/workflows/ci-python.yml +76 -0
  163. repokit/templates/layers/languages/python/.gitignore.rkappend +15 -0
  164. repokit/templates/layers/languages/python/.vscode/launch.json +33 -0
  165. repokit/templates/layers/manifest.toml +302 -0
  166. repokit/templates/vault/CLAUDE.md +29 -0
  167. repokit/templates/vault/claude/_maps/MOC-project.md +15 -0
  168. repokit/templates/vault/claude/_oracle/backlinks.md +3 -0
  169. repokit/templates/vault/claude/_oracle/concepts.md +12 -0
  170. repokit/templates/vault/claude/_oracle/manifest.md +10 -0
  171. repokit/templates/vault/claude/_todo/ACTIVE-WORK.md +18 -0
  172. repokit/templates/vault/claude/_todo/OPEN-LOOPS.md +14 -0
  173. repokit/utils.py +316 -0
  174. repokit/vault.py +248 -0
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Directory Profiles for RepoKit.
4
+
5
+ Implements a flexible directory structure system with profiles, groups, and types.
6
+ This allows for customized directory setups based on project needs.
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Dict, List, Set, Any, Optional
14
+
15
+ from .defaults import (
16
+ DEFAULT_PRIVATE_DIRS as CENTRALIZED_PRIVATE_DIRS,
17
+ DEFAULT_DIRECTORY_PROFILES,
18
+ DEFAULT_DIRECTORY_GROUPS,
19
+ DEFAULT_DIRECTORY_TYPE_MAPPING,
20
+ DEFAULT_PRIVATE_DIR_SETS
21
+ )
22
+
23
+ # Set up logging
24
+ logger = logging.getLogger("repokit.directories")
25
+
26
+ # Use centralized configurations from defaults.py
27
+ # These are now imported directly, eliminating duplication
28
+
29
+
30
+ class DirectoryProfileManager:
31
+ """Manages directory profiles, groups, and types for project setup."""
32
+
33
+ def __init__(self, config_file: Optional[str] = None, verbose: int = 0):
34
+ """
35
+ Initialize the directory profile manager.
36
+
37
+ Args:
38
+ config_file: Optional path to configuration file
39
+ verbose: Verbosity level
40
+ """
41
+ self.verbose = verbose
42
+
43
+ # Initialize defaults using centralized configurations
44
+ self.profiles = DEFAULT_DIRECTORY_PROFILES.copy()
45
+ self.groups = DEFAULT_DIRECTORY_GROUPS.copy()
46
+ self.private_dirs = DEFAULT_PRIVATE_DIR_SETS.copy()
47
+ self.type_mapping = DEFAULT_DIRECTORY_TYPE_MAPPING.copy()
48
+
49
+ # Load custom configuration if provided
50
+ if config_file:
51
+ self.load_config(config_file)
52
+
53
+ def load_config(self, config_file: str) -> bool:
54
+ """
55
+ Load directory configuration from a file.
56
+
57
+ Args:
58
+ config_file: Path to configuration file
59
+
60
+ Returns:
61
+ True if loaded successfully, False otherwise
62
+ """
63
+ try:
64
+ with open(config_file, "r") as f:
65
+ config = json.load(f)
66
+
67
+ # Update with custom configuration
68
+ if "directory_profiles" in config:
69
+ self.profiles.update(config["directory_profiles"])
70
+
71
+ if "directory_groups" in config:
72
+ self.groups.update(config["directory_groups"])
73
+
74
+ if "private_dirs" in config:
75
+ self.private_dirs.update(config["private_dirs"])
76
+
77
+ if "directory_types" in config:
78
+ self.type_mapping.update(config["directory_types"])
79
+
80
+ logger.info(f"Loaded directory configuration from {config_file}")
81
+ return True
82
+ except Exception as e:
83
+ logger.error(f"Failed to load directory configuration: {str(e)}")
84
+ return False
85
+
86
+ def save_config(self, config_file: str) -> bool:
87
+ """
88
+ Save directory configuration to a file.
89
+
90
+ Args:
91
+ config_file: Path to configuration file
92
+
93
+ Returns:
94
+ True if saved successfully, False otherwise
95
+ """
96
+ try:
97
+ config = {
98
+ "directory_profiles": self.profiles,
99
+ "directory_groups": self.groups,
100
+ "private_dirs": self.private_dirs,
101
+ "directory_types": self.type_mapping,
102
+ }
103
+
104
+ # Create parent directory if it doesn't exist
105
+ os.makedirs(os.path.dirname(os.path.abspath(config_file)), exist_ok=True)
106
+
107
+ with open(config_file, "w") as f:
108
+ json.dump(config, f, indent=4)
109
+
110
+ logger.info(f"Saved directory configuration to {config_file}")
111
+ return True
112
+ except Exception as e:
113
+ logger.error(f"Failed to save directory configuration: {str(e)}")
114
+ return False
115
+
116
+ def get_directories_for_profile(self, profile: str) -> List[str]:
117
+ """
118
+ Get directory types for a specific profile.
119
+
120
+ Args:
121
+ profile: Profile name (minimal, standard, complete, or custom)
122
+
123
+ Returns:
124
+ List of directory types in the profile
125
+ """
126
+ return self.profiles.get(profile, self.profiles["standard"]).copy()
127
+
128
+ def get_directories_for_groups(self, groups: List[str]) -> List[str]:
129
+ """
130
+ Get directory types for specific groups.
131
+
132
+ Args:
133
+ groups: List of group names
134
+
135
+ Returns:
136
+ List of directory types in the groups
137
+ """
138
+ result = set()
139
+ for group in groups:
140
+ if group in self.groups:
141
+ result.update(self.groups[group])
142
+ return sorted(list(result))
143
+
144
+ def get_private_directories(self, private_set: str = "standard") -> List[str]:
145
+ """
146
+ Get list of private directories.
147
+
148
+ Args:
149
+ private_set: Private directory set name
150
+
151
+ Returns:
152
+ List of private directory types
153
+ """
154
+ return self.private_dirs.get(private_set, self.private_dirs["standard"]).copy()
155
+
156
+ def get_actual_directory_name(
157
+ self, dir_type: str, package_name: Optional[str] = None
158
+ ) -> str:
159
+ """
160
+ Get actual directory name for a directory type.
161
+
162
+ Args:
163
+ dir_type: Directory type (src, tests, etc.)
164
+ package_name: Optional package name to use for src directory
165
+
166
+ Returns:
167
+ Actual directory name to create
168
+ """
169
+ # Special handling for source directory
170
+ if dir_type == "src" and package_name:
171
+ return package_name
172
+
173
+ return self.type_mapping.get(dir_type, dir_type)
174
+
175
+ def get_all_directories(
176
+ self,
177
+ profile: str,
178
+ groups: Optional[List[str]] = None,
179
+ private_set: str = "standard",
180
+ package_name: Optional[str] = None,
181
+ ) -> Dict[str, List[str]]:
182
+ """
183
+ Get all directories to create, mapped to actual names.
184
+
185
+ Args:
186
+ profile: Profile name
187
+ groups: Optional additional groups
188
+ private_set: Private directory set name
189
+ package_name: Optional package name for src directory
190
+
191
+ Returns:
192
+ Dictionary with categories and actual directory names
193
+ """
194
+ # Start with profile directories
195
+ dir_types = set(self.get_directories_for_profile(profile))
196
+
197
+ # Add group directories if specified
198
+ if groups:
199
+ dir_types.update(self.get_directories_for_groups(groups))
200
+
201
+ # Get private directories
202
+ private_types = set(self.get_private_directories(private_set))
203
+
204
+ # Convert types to actual directory names
205
+ standard_dirs = []
206
+ private_dirs = []
207
+
208
+ for dir_type in dir_types:
209
+ actual_name = self.get_actual_directory_name(dir_type, package_name)
210
+ if dir_type in private_types:
211
+ private_dirs.append(actual_name)
212
+ else:
213
+ standard_dirs.append(actual_name)
214
+
215
+ # Add all private directories from the private set (even if not in profile)
216
+ for private_type in private_types:
217
+ actual_name = self.get_actual_directory_name(private_type, package_name)
218
+ if actual_name not in private_dirs:
219
+ private_dirs.append(actual_name)
220
+
221
+ return {"standard": sorted(standard_dirs), "private": sorted(private_dirs)}
222
+
223
+ def create_directories(
224
+ self,
225
+ base_path: str,
226
+ profile: str,
227
+ groups: Optional[List[str]] = None,
228
+ private_set: str = "standard",
229
+ package_name: Optional[str] = None,
230
+ ) -> Dict[str, List[str]]:
231
+ """
232
+ Create all directories for a project.
233
+
234
+ Args:
235
+ base_path: Base path to create directories in
236
+ profile: Profile name
237
+ groups: Optional additional groups
238
+ private_set: Private directory set name
239
+ package_name: Optional package name for src directory
240
+
241
+ Returns:
242
+ Dictionary with created directories by category
243
+ """
244
+ directories = self.get_all_directories(
245
+ profile, groups, private_set, package_name
246
+ )
247
+
248
+ created_dirs = {"standard": [], "private": []}
249
+
250
+ # Create standard directories
251
+ for dirname in directories["standard"]:
252
+ dir_path = os.path.join(base_path, dirname)
253
+ os.makedirs(dir_path, exist_ok=True)
254
+ created_dirs["standard"].append(dirname)
255
+
256
+ # Create .gitkeep file
257
+ gitkeep_path = os.path.join(dir_path, ".gitkeep")
258
+ if not os.path.exists(gitkeep_path):
259
+ with open(gitkeep_path, "w") as f:
260
+ f.write("# This file ensures the directory is tracked by Git\n")
261
+
262
+ # Create private directories
263
+ for dirname in directories["private"]:
264
+ dir_path = os.path.join(base_path, dirname)
265
+ os.makedirs(dir_path, exist_ok=True)
266
+ created_dirs["private"].append(dirname)
267
+
268
+ # Create .gitkeep file (will be ignored in .gitignore)
269
+ gitkeep_path = os.path.join(dir_path, ".gitkeep")
270
+ if not os.path.exists(gitkeep_path):
271
+ with open(gitkeep_path, "w") as f:
272
+ f.write("# This file ensures the directory exists\n")
273
+
274
+ logger.info(f"Created {len(created_dirs['standard'])} standard directories")
275
+ logger.info(f"Created {len(created_dirs['private'])} private directories")
276
+
277
+ return created_dirs
278
+
279
+ def validate_directories(self, base_path: str) -> Dict[str, List[str]]:
280
+ """
281
+ Validate existing directories against all known directory types.
282
+
283
+ Args:
284
+ base_path: Base path to check
285
+
286
+ Returns:
287
+ Dictionary with existing, missing, and unknown directories
288
+ """
289
+ # Get all known directory types
290
+ all_known_types = set()
291
+ for profile in self.profiles.values():
292
+ all_known_types.update(profile)
293
+
294
+ # Get all directory names that could exist
295
+ all_possible_names = set()
296
+ for dir_type in all_known_types:
297
+ # Add with default name
298
+ all_possible_names.add(self.get_actual_directory_name(dir_type))
299
+ # Also add common package names for src
300
+ if dir_type == "src":
301
+ all_possible_names.update(["src", "app", "lib", "pkg"])
302
+
303
+ # Check existing directories
304
+ existing_dirs = []
305
+ for item in os.listdir(base_path):
306
+ item_path = os.path.join(base_path, item)
307
+ if os.path.isdir(item_path) and not item.startswith("."):
308
+ existing_dirs.append(item)
309
+
310
+ # Categorize directories
311
+ known_dirs = [d for d in existing_dirs if d in all_possible_names]
312
+ unknown_dirs = [d for d in existing_dirs if d not in all_possible_names]
313
+ missing_dirs = [d for d in all_possible_names if d not in existing_dirs]
314
+
315
+ return {
316
+ "existing": sorted(known_dirs),
317
+ "unknown": sorted(unknown_dirs),
318
+ "missing": sorted(missing_dirs),
319
+ }
320
+
321
+ def suggest_profile(self, base_path: str) -> str:
322
+ """
323
+ Suggest a profile based on existing directories.
324
+
325
+ Args:
326
+ base_path: Base path to check
327
+
328
+ Returns:
329
+ Suggested profile name
330
+ """
331
+ validation = self.validate_directories(base_path)
332
+ existing_dirs = set(validation["existing"])
333
+
334
+ # Check each profile to see how many directories match
335
+ profile_matches = {}
336
+ for profile_name, profile_dirs in self.profiles.items():
337
+ # Convert profile types to actual directory names
338
+ profile_names = set(self.get_actual_directory_name(d) for d in profile_dirs)
339
+ # Count matches
340
+ matches = len(existing_dirs.intersection(profile_names))
341
+ profile_matches[profile_name] = matches
342
+
343
+ # Find the profile with the most matches
344
+ best_profile = max(profile_matches.items(), key=lambda x: x[1])
345
+
346
+ # If minimal matches, default to standard
347
+ if best_profile[1] <= 1:
348
+ return "standard"
349
+
350
+ return best_profile[0]
351
+
352
+ def manage_gitkeep_files(self, base_path: str, recursive: bool = True) -> Dict[str, List[str]]:
353
+ """
354
+ Manage .gitkeep files based on directory contents.
355
+
356
+ Adds .gitkeep to empty directories and removes .gitkeep from non-empty directories.
357
+
358
+ Args:
359
+ base_path: Base path to manage
360
+ recursive: Whether to process subdirectories recursively
361
+
362
+ Returns:
363
+ Dictionary with added and removed .gitkeep files
364
+ """
365
+ result = {"added": [], "removed": []}
366
+
367
+ def process_directory(dir_path: str, relative_path: str = ""):
368
+ """Process a single directory for .gitkeep management."""
369
+ if not os.path.isdir(dir_path):
370
+ return
371
+
372
+ gitkeep_path = os.path.join(dir_path, ".gitkeep")
373
+ gitkeep_exists = os.path.exists(gitkeep_path)
374
+
375
+ # Get all items in directory (excluding .gitkeep itself)
376
+ try:
377
+ items = [item for item in os.listdir(dir_path) if item != ".gitkeep"]
378
+ except PermissionError:
379
+ logger.warning(f"Permission denied accessing directory: {dir_path}")
380
+ return
381
+
382
+ # Filter out hidden files and directories that should be ignored
383
+ non_hidden_items = [item for item in items if not item.startswith(".")]
384
+
385
+ # Check if directory is effectively empty (only has hidden files or .gitkeep)
386
+ is_empty = len(non_hidden_items) == 0
387
+
388
+ # Manage .gitkeep based on directory state
389
+ if is_empty and not gitkeep_exists:
390
+ # Add .gitkeep to empty directory
391
+ try:
392
+ with open(gitkeep_path, "w") as f:
393
+ f.write("# This file ensures the directory is tracked by Git\n")
394
+ result["added"].append(os.path.join(relative_path, ".gitkeep") if relative_path else ".gitkeep")
395
+ logger.debug(f"Added .gitkeep to empty directory: {dir_path}")
396
+ except PermissionError:
397
+ logger.warning(f"Permission denied creating .gitkeep in: {dir_path}")
398
+ elif not is_empty and gitkeep_exists:
399
+ # Remove .gitkeep from non-empty directory
400
+ try:
401
+ os.remove(gitkeep_path)
402
+ result["removed"].append(os.path.join(relative_path, ".gitkeep") if relative_path else ".gitkeep")
403
+ logger.debug(f"Removed .gitkeep from non-empty directory: {dir_path}")
404
+ except PermissionError:
405
+ logger.warning(f"Permission denied removing .gitkeep from: {dir_path}")
406
+
407
+ # Process subdirectories recursively if requested
408
+ if recursive:
409
+ for item in items:
410
+ item_path = os.path.join(dir_path, item)
411
+ if os.path.isdir(item_path) and not item.startswith("."):
412
+ item_relative = os.path.join(relative_path, item) if relative_path else item
413
+ process_directory(item_path, item_relative)
414
+
415
+ # Start processing from base path
416
+ process_directory(base_path)
417
+
418
+ if result["added"] or result["removed"]:
419
+ logger.info(f"Managed .gitkeep files: {len(result['added'])} added, {len(result['removed'])} removed")
420
+ else:
421
+ logger.debug("No .gitkeep file changes needed")
422
+
423
+ return result