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,1244 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Directory Analyzer for RepoKit.
4
+
5
+ Analyzes directories for migration to the RepoKit structure.
6
+ Supports universal project migration for any project type.
7
+ """
8
+
9
+ import os
10
+ import fnmatch
11
+ import shutil
12
+ import logging
13
+ import subprocess
14
+ import json
15
+ from pathlib import Path
16
+ from typing import Dict, List, Set, Tuple, Optional, Any
17
+
18
+ from .defaults import DEFAULT_PRIVATE_DIRS, DEFAULT_BRANCH_STRATEGIES
19
+
20
+
21
+ class GitManager:
22
+ """
23
+ Manages Git repository state detection and operations.
24
+ """
25
+
26
+ def __init__(self, target_dir: str, verbose: int = 0):
27
+ """Initialize GitManager."""
28
+ self.target_dir = os.path.abspath(target_dir)
29
+ self.verbose = verbose
30
+ self.logger = logging.getLogger("repokit.git")
31
+
32
+ def run_git(self, args: List[str], check: bool = True) -> Optional[str]:
33
+ """Run a git command and return output."""
34
+ cmd = ["git"] + args
35
+
36
+ if self.verbose >= 2:
37
+ self.logger.debug(f"Running: {' '.join(cmd)} in {self.target_dir}")
38
+
39
+ try:
40
+ result = subprocess.run(
41
+ cmd, cwd=self.target_dir, check=check, capture_output=True, text=True
42
+ )
43
+ return result.stdout.strip()
44
+ except subprocess.CalledProcessError as e:
45
+ if self.verbose >= 1:
46
+ self.logger.warning(f"Git command failed: {e.stderr}")
47
+ if check:
48
+ raise
49
+ return None
50
+
51
+ def is_git_repo(self) -> bool:
52
+ """Check if directory is a Git repository."""
53
+ return os.path.exists(os.path.join(self.target_dir, ".git"))
54
+
55
+ def get_repo_state(self) -> Dict[str, Any]:
56
+ """Get comprehensive Git repository state."""
57
+ if not self.is_git_repo():
58
+ return {"is_repo": False}
59
+
60
+ state = {"is_repo": True}
61
+
62
+ try:
63
+ # Get current branch
64
+ state["current_branch"] = self.run_git(
65
+ ["branch", "--show-current"], check=False
66
+ )
67
+
68
+ # Get all branches
69
+ branches_output = self.run_git(["branch", "-a"], check=False)
70
+ if branches_output:
71
+ branches = []
72
+ for line in branches_output.split("\n"):
73
+ line = line.strip()
74
+ if line and not line.startswith("remotes/"):
75
+ branch = line.lstrip("* ").strip()
76
+ if branch:
77
+ branches.append(branch)
78
+ state["branches"] = branches
79
+
80
+ # Check for uncommitted changes
81
+ status_output = self.run_git(["status", "--porcelain"], check=False)
82
+ state["has_uncommitted_changes"] = bool(
83
+ status_output and status_output.strip()
84
+ )
85
+
86
+ # Get remotes
87
+ remotes_output = self.run_git(["remote", "-v"], check=False)
88
+ remotes = {}
89
+ if remotes_output:
90
+ for line in remotes_output.split("\n"):
91
+ if line.strip():
92
+ parts = line.split()
93
+ if len(parts) >= 2:
94
+ remote_name = parts[0]
95
+ remote_url = parts[1]
96
+ if remote_name not in remotes:
97
+ remotes[remote_name] = remote_url
98
+ state["remotes"] = remotes
99
+
100
+ # Check if repo has commits
101
+ try:
102
+ self.run_git(["rev-parse", "HEAD"], check=True)
103
+ state["has_commits"] = True
104
+ except:
105
+ state["has_commits"] = False
106
+
107
+ except Exception as e:
108
+ self.logger.warning(f"Error getting Git state: {str(e)}")
109
+
110
+ return state
111
+
112
+ def get_branch_strategy(self) -> str:
113
+ """Detect existing branch strategy."""
114
+ state = self.get_repo_state()
115
+ if not state["is_repo"]:
116
+ return "none"
117
+
118
+ branches = set(state.get("branches", []))
119
+
120
+ # Check for GitFlow pattern
121
+ if "develop" in branches and any(b.startswith("feature/") for b in branches):
122
+ return "gitflow"
123
+
124
+ # Check for standard RepoKit pattern
125
+ repokit_branches = {"main", "dev", "staging", "test", "live"}
126
+ if len(repokit_branches & branches) >= 3:
127
+ return "standard"
128
+
129
+ # Check for simple pattern
130
+ if "main" in branches and "dev" in branches:
131
+ return "simple"
132
+
133
+ # Single branch or unknown
134
+ return "minimal"
135
+
136
+
137
+ class ProjectAnalyzer:
138
+ """
139
+ Universal project analyzer for any type of project.
140
+
141
+ Analyzes project structure, language, Git state, and provides
142
+ comprehensive migration recommendations for RepoKit adoption.
143
+ """
144
+
145
+ # Language detection patterns
146
+ LANGUAGE_PATTERNS = {
147
+ "python": {
148
+ "files": [
149
+ "setup.py",
150
+ "pyproject.toml",
151
+ "requirements.txt",
152
+ "Pipfile",
153
+ "poetry.lock",
154
+ ],
155
+ "extensions": [".py", ".pyw"],
156
+ "directories": ["src", "lib", "tests"],
157
+ },
158
+ "javascript": {
159
+ "files": [
160
+ "package.json",
161
+ "package-lock.json",
162
+ "yarn.lock",
163
+ "webpack.config.js",
164
+ ],
165
+ "extensions": [".js", ".jsx", ".ts", ".tsx"],
166
+ "directories": ["src", "lib", "node_modules", "dist"],
167
+ },
168
+ "java": {
169
+ "files": ["pom.xml", "build.gradle", "build.xml"],
170
+ "extensions": [".java", ".class"],
171
+ "directories": ["src", "target", "build"],
172
+ },
173
+ "csharp": {
174
+ "files": ["*.csproj", "*.sln", "packages.config"],
175
+ "extensions": [".cs", ".vb"],
176
+ "directories": ["bin", "obj"],
177
+ },
178
+ "go": {
179
+ "files": ["go.mod", "go.sum"],
180
+ "extensions": [".go"],
181
+ "directories": ["cmd", "pkg", "internal"],
182
+ },
183
+ "rust": {
184
+ "files": ["Cargo.toml", "Cargo.lock"],
185
+ "extensions": [".rs"],
186
+ "directories": ["src", "target"],
187
+ },
188
+ }
189
+
190
+ def __init__(self, target_dir: str, verbose: int = 0):
191
+ """Initialize ProjectAnalyzer."""
192
+ self.target_dir = os.path.abspath(target_dir)
193
+ self.verbose = verbose
194
+ self.logger = logging.getLogger("repokit.project_analyzer")
195
+
196
+ # Initialize Git manager
197
+ self.git_manager = GitManager(target_dir, verbose)
198
+
199
+ # Initialize directory analyzer for RepoKit-specific analysis
200
+ self.directory_analyzer = DirectoryAnalyzer(target_dir, verbose)
201
+
202
+ # Project analysis results
203
+ self.project_type = None
204
+ self.language = None
205
+ self.git_state = None
206
+ self.migration_complexity = None
207
+
208
+ # Perform analysis
209
+ self._analyze_project()
210
+
211
+ def _analyze_project(self) -> None:
212
+ """Perform comprehensive project analysis."""
213
+ self.logger.info(f"Analyzing project: {self.target_dir}")
214
+
215
+ # Detect project type and language
216
+ self.language = self._detect_language()
217
+ self.project_type = self._determine_project_type()
218
+
219
+ # Analyze Git state
220
+ self.git_state = self.git_manager.get_repo_state()
221
+
222
+ # Assess migration complexity
223
+ self.migration_complexity = self._assess_migration_complexity()
224
+
225
+ def _detect_language(self) -> str:
226
+ """Detect primary programming language."""
227
+ scores = {lang: 0 for lang in self.LANGUAGE_PATTERNS}
228
+
229
+ # Check for characteristic files
230
+ for lang, patterns in self.LANGUAGE_PATTERNS.items():
231
+ for file_pattern in patterns["files"]:
232
+ if "*" in file_pattern:
233
+ # Handle glob patterns
234
+ import glob
235
+
236
+ matches = glob.glob(os.path.join(self.target_dir, file_pattern))
237
+ if matches:
238
+ scores[lang] += 10
239
+ else:
240
+ # Direct file check
241
+ if os.path.exists(os.path.join(self.target_dir, file_pattern)):
242
+ scores[lang] += 10
243
+
244
+ # Count files by extension
245
+ ext_count = 0
246
+ for root, dirs, files in os.walk(self.target_dir):
247
+ if ".git" in root:
248
+ continue
249
+ for file in files:
250
+ if any(file.endswith(ext) for ext in patterns["extensions"]):
251
+ ext_count += 1
252
+ scores[lang] += ext_count
253
+
254
+ # Check for characteristic directories
255
+ for dir_name in patterns["directories"]:
256
+ if os.path.exists(os.path.join(self.target_dir, dir_name)):
257
+ scores[lang] += 5
258
+
259
+ # Return language with highest score
260
+ if max(scores.values()) > 0:
261
+ return max(scores, key=scores.get)
262
+ return "generic"
263
+
264
+ def _determine_project_type(self) -> str:
265
+ """Determine project type based on structure and content."""
266
+ # Check if it's definitively a RepoKit-managed project
267
+ repokit_config_path = os.path.join(self.target_dir, ".repokit.json")
268
+ if os.path.exists(repokit_config_path):
269
+ try:
270
+ with open(repokit_config_path, 'r') as f:
271
+ config = json.load(f)
272
+
273
+ # Check for RepoKit signature fields
274
+ if config.get("repokit_managed") or config.get("generated_by") == "repokit":
275
+ return "repokit"
276
+
277
+ # Even non-empty .repokit.json suggests RepoKit intent
278
+ if config: # Non-empty config file
279
+ return "repokit"
280
+
281
+ except (json.JSONDecodeError, IOError):
282
+ # If .repokit.json exists but is invalid, treat as RepoKit attempt
283
+ return "repokit"
284
+
285
+ # Check for RepoKit-like structure (but not definitively RepoKit-managed)
286
+ if self._has_repokit_structure():
287
+ return "repokit_like"
288
+
289
+ # Check for empty directory
290
+ try:
291
+ contents = os.listdir(self.target_dir)
292
+ # Filter out hidden files for empty check
293
+ visible_contents = [item for item in contents if not item.startswith(".")]
294
+ if not visible_contents:
295
+ return "empty"
296
+ except OSError:
297
+ return "inaccessible"
298
+
299
+ # Check if it's a Git repository
300
+ if self.git_state and self.git_state.get("is_repo"):
301
+ if self.git_state.get("has_commits"):
302
+ return "git_with_history"
303
+ else:
304
+ return "git_new"
305
+
306
+ # Check if it has source files
307
+ has_source = False
308
+ for root, dirs, files in os.walk(self.target_dir):
309
+ for file in files:
310
+ if any(
311
+ file.endswith(ext)
312
+ for lang_patterns in self.LANGUAGE_PATTERNS.values()
313
+ for ext in lang_patterns["extensions"]
314
+ ):
315
+ has_source = True
316
+ break
317
+ if has_source:
318
+ break
319
+
320
+ if has_source:
321
+ return "source_no_git"
322
+ else:
323
+ return "files_no_git"
324
+
325
+ def _assess_migration_complexity(self) -> str:
326
+ """Assess migration complexity level."""
327
+ complexity_score = 0
328
+
329
+ # Git repository complexity
330
+ if self.git_state and self.git_state.get("is_repo"):
331
+ complexity_score += 2
332
+
333
+ # Multiple branches add complexity
334
+ branches = self.git_state.get("branches", [])
335
+ if len(branches) > 2:
336
+ complexity_score += 2
337
+
338
+ # Uncommitted changes add complexity
339
+ if self.git_state.get("has_uncommitted_changes"):
340
+ complexity_score += 1
341
+
342
+ # Existing remotes add complexity
343
+ if self.git_state.get("remotes"):
344
+ complexity_score += 1
345
+
346
+ # File structure complexity
347
+ dir_summary = self.directory_analyzer.get_summary()
348
+ if dir_summary.get("has_template_conflicts"):
349
+ complexity_score += 2
350
+
351
+ # Large number of files
352
+ if dir_summary.get("total_files", 0) > 100:
353
+ complexity_score += 1
354
+
355
+ if complexity_score <= 2:
356
+ return "low"
357
+ elif complexity_score <= 5:
358
+ return "medium"
359
+ else:
360
+ return "high"
361
+
362
+ def get_comprehensive_summary(self) -> Dict[str, Any]:
363
+ """Get comprehensive project analysis summary."""
364
+ # Get base directory analysis
365
+ dir_summary = self.directory_analyzer.get_summary()
366
+
367
+ # Enhance with project analysis
368
+ summary = {
369
+ "target_dir": self.target_dir,
370
+ "project_type": self.project_type,
371
+ "detected_language": self.language,
372
+ "migration_complexity": self.migration_complexity,
373
+ "git_state": self.git_state,
374
+ "branch_strategy": self.git_manager.get_branch_strategy(),
375
+ # Directory analysis results
376
+ "existing_dirs": dir_summary["existing_dirs"],
377
+ "missing_dirs": dir_summary["missing_dirs"],
378
+ "special_dirs": dir_summary["special_dirs"],
379
+ "has_git_repository": dir_summary["has_git_repository"],
380
+ "has_template_conflicts": dir_summary["has_template_conflicts"],
381
+ "file_counts": dir_summary["file_counts"],
382
+ "total_files": dir_summary["total_files"],
383
+ # Recommendations
384
+ "recommended_strategy": self._recommend_migration_strategy(),
385
+ "recommended_branch_strategy": self._recommend_branch_strategy(),
386
+ "migration_steps": self._generate_migration_steps(),
387
+ }
388
+
389
+ return summary
390
+
391
+ def _recommend_migration_strategy(self) -> str:
392
+ """Recommend migration strategy based on analysis."""
393
+ if self.project_type == "empty":
394
+ return "create_new"
395
+ elif self.project_type == "repokit":
396
+ return "already_repokit"
397
+ elif self.migration_complexity == "low":
398
+ return "safe"
399
+ elif self.git_state and self.git_state.get("has_uncommitted_changes"):
400
+ return "safe" # Always safe when there are uncommitted changes
401
+ else:
402
+ return "merge"
403
+
404
+ def _recommend_branch_strategy(self) -> str:
405
+ """Recommend branch strategy based on project characteristics."""
406
+ if self.project_type == "empty":
407
+ return "standard" # Default for new projects
408
+
409
+ current_strategy = self.git_manager.get_branch_strategy()
410
+
411
+ if current_strategy in ["gitflow", "standard", "simple"]:
412
+ return current_strategy # Keep existing if recognized
413
+
414
+ # For small projects, recommend simple
415
+ dir_summary = self.directory_analyzer.get_summary()
416
+ if dir_summary.get("total_files", 0) < 50:
417
+ return "simple"
418
+
419
+ return "standard" # Default recommendation
420
+
421
+ def _generate_migration_steps(self) -> List[str]:
422
+ """Generate step-by-step migration recommendations."""
423
+ steps = []
424
+
425
+ if self.project_type == "empty":
426
+ steps = [
427
+ "Initialize new RepoKit project structure",
428
+ "Set up chosen branch strategy",
429
+ "Generate language-specific templates",
430
+ "Create remote repository if requested",
431
+ ]
432
+ elif self.project_type == "repokit":
433
+ steps = ["Project is already using RepoKit structure"]
434
+ elif self.git_state and self.git_state.get("has_uncommitted_changes"):
435
+ steps = [
436
+ "WARNING: Uncommitted changes detected",
437
+ "Commit or stash changes before migration",
438
+ "Run migration with 'safe' strategy",
439
+ ]
440
+ else:
441
+ steps = [
442
+ "Backup existing project (recommended)",
443
+ "Analyze and resolve any template conflicts",
444
+ "Apply RepoKit directory structure",
445
+ "Initialize or update Git repository",
446
+ "Set up branch strategy and worktrees",
447
+ "Generate appropriate templates",
448
+ "Create remote repository if requested",
449
+ ]
450
+
451
+ return steps
452
+
453
+ def _has_repokit_structure(self) -> bool:
454
+ """
455
+ Check for RepoKit-specific directory structure patterns.
456
+
457
+ Returns:
458
+ True if directory structure suggests RepoKit-like organization
459
+ """
460
+ # Check for RepoKit standard directories
461
+ repokit_dirs = ["private", "logs", "scripts", "tests", "docs"]
462
+ found_dirs = 0
463
+
464
+ for directory in repokit_dirs:
465
+ if os.path.exists(os.path.join(self.target_dir, directory)):
466
+ found_dirs += 1
467
+
468
+ # Require majority of RepoKit directories to suggest RepoKit structure
469
+ return found_dirs >= len(repokit_dirs) * 0.6
470
+
471
+
472
+ class DirectoryAnalyzer:
473
+ """
474
+ Analyzes directories for migration to RepoKit structure.
475
+
476
+ This class helps identify existing files and directories that match
477
+ RepoKit's expected structure, detects conflicts, and provides migration
478
+ strategies.
479
+ """
480
+
481
+ # Standard RepoKit directories
482
+ STANDARD_DIRS = [
483
+ "convos",
484
+ "docs",
485
+ "logs",
486
+ "private",
487
+ "revisions",
488
+ "scripts",
489
+ "tests",
490
+ ]
491
+
492
+ # Private directories that shouldn't be checked in
493
+ PRIVATE_DIRS = DEFAULT_PRIVATE_DIRS
494
+
495
+ # Special directories that need special handling
496
+ SPECIAL_DIRS = [".git", ".github", ".vscode"]
497
+
498
+ # Template directories to look for in the target
499
+ TEMPLATE_DIRS = [".github"]
500
+
501
+ def __init__(self, target_dir: str, verbose: int = 0):
502
+ """
503
+ Initialize the directory analyzer.
504
+
505
+ Args:
506
+ target_dir: Directory to analyze
507
+ verbose: Verbosity level (0=normal, 1=info, 2=debug)
508
+ """
509
+ self.target_dir = os.path.abspath(target_dir)
510
+ self.verbose = verbose
511
+ self.logger = logging.getLogger("repokit.analyzer")
512
+
513
+ # Analysis results
514
+ self.existing_dirs = set()
515
+ self.missing_dirs = set()
516
+ self.special_dirs = {}
517
+ self.template_conflicts = {}
518
+
519
+ # Scan target directory
520
+ self._scan_directory()
521
+
522
+ def _scan_directory(self) -> None:
523
+ """Scan the target directory and categorize content."""
524
+ if not os.path.exists(self.target_dir):
525
+ self.logger.warning(f"Target directory does not exist: {self.target_dir}")
526
+ return
527
+
528
+ self.logger.info(f"Scanning directory: {self.target_dir}")
529
+
530
+ # Check for standard directories
531
+ for dirname in self.STANDARD_DIRS:
532
+ dirpath = os.path.join(self.target_dir, dirname)
533
+ if os.path.exists(dirpath) and os.path.isdir(dirpath):
534
+ self.existing_dirs.add(dirname)
535
+ else:
536
+ self.missing_dirs.add(dirname)
537
+
538
+ # Check for special directories
539
+ for dirname in self.SPECIAL_DIRS:
540
+ dirpath = os.path.join(self.target_dir, dirname)
541
+ if os.path.exists(dirpath) and os.path.isdir(dirpath):
542
+ self.special_dirs[dirname] = dirpath
543
+
544
+ # Check for template conflicts
545
+ for template_dir in self.TEMPLATE_DIRS:
546
+ dirpath = os.path.join(self.target_dir, template_dir)
547
+ if os.path.exists(dirpath) and os.path.isdir(dirpath):
548
+ self.template_conflicts[template_dir] = self._scan_template_conflicts(
549
+ template_dir
550
+ )
551
+
552
+ if self.verbose >= 1:
553
+ self._log_scan_results()
554
+
555
+ def _scan_template_conflicts(self, template_dir: str) -> Dict[str, str]:
556
+ """
557
+ Scan a template directory for potential conflicts.
558
+
559
+ Args:
560
+ template_dir: Template directory name (e.g., ".github")
561
+
562
+ Returns:
563
+ Dictionary of {filename: path} for conflicting files
564
+ """
565
+ conflicts = {}
566
+ dirpath = os.path.join(self.target_dir, template_dir)
567
+
568
+ if not os.path.exists(dirpath):
569
+ return conflicts
570
+
571
+ for root, dirs, files in os.walk(dirpath):
572
+ for file in files:
573
+ filepath = os.path.join(root, file)
574
+ rel_path = os.path.relpath(filepath, dirpath)
575
+ conflicts[rel_path] = filepath
576
+
577
+ return conflicts
578
+
579
+ def _log_scan_results(self) -> None:
580
+ """Log the results of the directory scan."""
581
+ self.logger.info(
582
+ f"Existing standard directories: {', '.join(self.existing_dirs) or 'None'}"
583
+ )
584
+ self.logger.info(
585
+ f"Missing standard directories: {', '.join(self.missing_dirs) or 'None'}"
586
+ )
587
+ self.logger.info(
588
+ f"Special directories found: {', '.join(self.special_dirs.keys()) or 'None'}"
589
+ )
590
+
591
+ if self.template_conflicts:
592
+ for template_dir, conflicts in self.template_conflicts.items():
593
+ self.logger.info(
594
+ f"Potential conflicts in {template_dir}: {len(conflicts)} files"
595
+ )
596
+
597
+ if self.verbose >= 2:
598
+ for rel_path in conflicts:
599
+ self.logger.debug(f" - {rel_path}")
600
+
601
+ def has_git_repository(self) -> bool:
602
+ """Check if the target directory is a Git repository."""
603
+ return ".git" in self.special_dirs
604
+
605
+ def has_template_conflicts(self) -> bool:
606
+ """Check if there are any template conflicts."""
607
+ return any(len(conflicts) > 0 for conflicts in self.template_conflicts.values())
608
+
609
+ def get_migration_plan(self, strategy: str = "safe") -> Dict[str, Any]:
610
+ """
611
+ Generate a migration plan based on the analysis.
612
+
613
+ Args:
614
+ strategy: Migration strategy ("safe", "replace", "merge")
615
+
616
+ Returns:
617
+ Dictionary with migration plan details
618
+ """
619
+ plan = {
620
+ "target_dir": self.target_dir,
621
+ "create_dirs": list(self.missing_dirs),
622
+ "move_dirs": {},
623
+ "special_dirs": {
624
+ k: {"path": v, "action": self._get_special_dir_action(k, strategy)}
625
+ for k, v in self.special_dirs.items()
626
+ },
627
+ "template_conflicts": {
628
+ k: {
629
+ "conflicts": len(v),
630
+ "action": self._get_template_conflict_action(k, strategy),
631
+ }
632
+ for k, v in self.template_conflicts.items()
633
+ },
634
+ "strategy": strategy,
635
+ }
636
+
637
+ return plan
638
+
639
+ def _get_special_dir_action(self, dirname: str, strategy: str) -> str:
640
+ """
641
+ Determine action for a special directory based on strategy.
642
+
643
+ Args:
644
+ dirname: Directory name
645
+ strategy: Migration strategy
646
+
647
+ Returns:
648
+ Action to take ("keep", "backup", "replace", "merge")
649
+ """
650
+ if strategy == "safe":
651
+ if dirname == ".git":
652
+ return "keep"
653
+ else:
654
+ return "backup"
655
+ elif strategy == "replace":
656
+ return "replace"
657
+ elif strategy == "merge":
658
+ return "merge"
659
+ else:
660
+ return "keep"
661
+
662
+ def _get_template_conflict_action(self, template_dir: str, strategy: str) -> str:
663
+ """
664
+ Determine action for template conflicts based on strategy.
665
+
666
+ Args:
667
+ template_dir: Template directory name
668
+ strategy: Migration strategy
669
+
670
+ Returns:
671
+ Action to take ("keep", "backup", "replace", "merge")
672
+ """
673
+ if strategy == "safe":
674
+ return "backup"
675
+ elif strategy == "replace":
676
+ return "replace"
677
+ elif strategy == "merge":
678
+ return "merge"
679
+ else:
680
+ return "keep"
681
+
682
+ def execute_migration_plan(
683
+ self, plan: Dict[str, Any], dry_run: bool = False
684
+ ) -> bool:
685
+ """
686
+ Execute a migration plan.
687
+
688
+ Args:
689
+ plan: Migration plan from get_migration_plan()
690
+ dry_run: If True, only log actions without executing
691
+
692
+ Returns:
693
+ True if successful, False otherwise
694
+ """
695
+ self.logger.info(f"Executing migration plan (dry_run={dry_run})")
696
+
697
+ # Create missing directories
698
+ for dirname in plan["create_dirs"]:
699
+ dirpath = os.path.join(self.target_dir, dirname)
700
+ self.logger.info(f"Creating directory: {dirpath}")
701
+ if not dry_run:
702
+ os.makedirs(dirpath, exist_ok=True)
703
+
704
+ # Handle special directories
705
+ for dirname, info in plan["special_dirs"].items():
706
+ action = info["action"]
707
+ path = info["path"]
708
+
709
+ if action == "keep":
710
+ self.logger.info(f"Keeping {dirname} as is")
711
+ elif action == "backup":
712
+ backup_path = f"{path}_backup"
713
+ self.logger.info(f"Backing up {dirname} to {backup_path}")
714
+ if not dry_run:
715
+ if os.path.exists(backup_path):
716
+ shutil.rmtree(backup_path)
717
+ shutil.copytree(path, backup_path)
718
+ elif action == "replace":
719
+ self.logger.info(f"Replacing {dirname}")
720
+ if not dry_run:
721
+ shutil.rmtree(path)
722
+ elif action == "merge":
723
+ self.logger.info(
724
+ f"Marking {dirname} for merge (requires template content)"
725
+ )
726
+
727
+ # Handle template conflicts
728
+ for template_dir, info in plan["template_conflicts"].items():
729
+ action = info["action"]
730
+ conflicts = self.template_conflicts[template_dir]
731
+
732
+ if action == "keep":
733
+ self.logger.info(f"Keeping existing {template_dir} files")
734
+ elif action == "backup":
735
+ backup_dir = os.path.join(self.target_dir, f"{template_dir}_backup")
736
+ self.logger.info(
737
+ f"Backing up {len(conflicts)} files from {template_dir} to {backup_dir}"
738
+ )
739
+ if not dry_run:
740
+ if not os.path.exists(backup_dir):
741
+ os.makedirs(backup_dir, exist_ok=True)
742
+ for rel_path, filepath in conflicts.items():
743
+ backup_path = os.path.join(backup_dir, rel_path)
744
+ os.makedirs(os.path.dirname(backup_path), exist_ok=True)
745
+ shutil.copy2(filepath, backup_path)
746
+ elif action == "replace":
747
+ self.logger.info(f"Replacing {len(conflicts)} files in {template_dir}")
748
+ dirpath = os.path.join(self.target_dir, template_dir)
749
+ if not dry_run:
750
+ shutil.rmtree(dirpath)
751
+ elif action == "merge":
752
+ self.logger.info(
753
+ f"Marking {template_dir} for merge (requires template content)"
754
+ )
755
+
756
+ return True
757
+
758
+ def categorize_files(self, path: str = None) -> Dict[str, List[str]]:
759
+ """
760
+ Categorize files in a directory by type.
761
+
762
+ Args:
763
+ path: Directory path to categorize (default: target_dir)
764
+
765
+ Returns:
766
+ Dictionary with categorized files
767
+ """
768
+ path = path or self.target_dir
769
+
770
+ categories = {
771
+ "python": [], # Python source files
772
+ "javascript": [], # JavaScript source files
773
+ "html": [], # HTML files
774
+ "css": [], # CSS files
775
+ "markdown": [], # Markdown files
776
+ "images": [], # Image files
777
+ "documents": [], # Document files
778
+ "data": [], # Data files
779
+ "config": [], # Configuration files
780
+ "other": [], # Other files
781
+ }
782
+
783
+ for root, dirs, files in os.walk(path):
784
+ # Skip .git directory
785
+ if ".git" in root.split(os.sep):
786
+ continue
787
+
788
+ for file in files:
789
+ filepath = os.path.join(root, file)
790
+
791
+ # Categorize by extension
792
+ if file.endswith((".py", ".pyw")):
793
+ categories["python"].append(filepath)
794
+ elif file.endswith((".js", ".jsx", ".ts", ".tsx")):
795
+ categories["javascript"].append(filepath)
796
+ elif file.endswith((".html", ".htm")):
797
+ categories["html"].append(filepath)
798
+ elif file.endswith((".css", ".scss", ".sass")):
799
+ categories["css"].append(filepath)
800
+ elif file.endswith((".md", ".markdown")):
801
+ categories["markdown"].append(filepath)
802
+ elif file.endswith((".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico")):
803
+ categories["images"].append(filepath)
804
+ elif file.endswith((".doc", ".docx", ".pdf", ".txt", ".rtf")):
805
+ categories["documents"].append(filepath)
806
+ elif file.endswith((".csv", ".json", ".xml", ".yaml", ".yml")):
807
+ categories["data"].append(filepath)
808
+ elif file.endswith((".ini", ".cfg", ".conf", ".config", ".toml")):
809
+ categories["config"].append(filepath)
810
+ else:
811
+ categories["other"].append(filepath)
812
+
813
+ return categories
814
+
815
+ def suggest_language(self) -> str:
816
+ """
817
+ Suggest a programming language based on the file categories.
818
+
819
+ Returns:
820
+ Suggested language ("python", "javascript", or "generic")
821
+ """
822
+ categories = self.categorize_files()
823
+
824
+ # Count files by language
825
+ python_count = len(categories["python"])
826
+ javascript_count = len(categories["javascript"])
827
+
828
+ if python_count > javascript_count:
829
+ return "python"
830
+ elif javascript_count > python_count:
831
+ return "javascript"
832
+ else:
833
+ return "generic"
834
+
835
+ def get_summary(self) -> Dict[str, Any]:
836
+ """
837
+ Get a summary of the directory analysis.
838
+
839
+ Returns:
840
+ Summary dictionary
841
+ """
842
+ categories = self.categorize_files()
843
+
844
+ summary = {
845
+ "target_dir": self.target_dir,
846
+ "existing_dirs": sorted(list(self.existing_dirs)),
847
+ "missing_dirs": sorted(list(self.missing_dirs)),
848
+ "special_dirs": list(self.special_dirs.keys()),
849
+ "has_git_repository": self.has_git_repository(),
850
+ "has_template_conflicts": self.has_template_conflicts(),
851
+ "suggested_language": self.suggest_language(),
852
+ "file_counts": {
853
+ category: len(files) for category, files in categories.items()
854
+ },
855
+ "total_files": sum(len(files) for files in categories.values()),
856
+ }
857
+
858
+ return summary
859
+
860
+
861
+ class MigrationManager:
862
+ """
863
+ Manages migration of directories to RepoKit structure.
864
+ """
865
+
866
+ def __init__(self, source_dir: str, target_dir: str = None, verbose: int = 0):
867
+ """
868
+ Initialize the migration manager.
869
+
870
+ Args:
871
+ source_dir: Source directory
872
+ target_dir: Target directory (default: source_dir)
873
+ verbose: Verbosity level (0=normal, 1=info, 2=debug)
874
+ """
875
+ self.source_dir = os.path.abspath(source_dir)
876
+ self.target_dir = os.path.abspath(target_dir) if target_dir else self.source_dir
877
+ self.verbose = verbose
878
+ self.logger = logging.getLogger("repokit.migration")
879
+
880
+ # Initialize analyzer
881
+ self.analyzer = DirectoryAnalyzer(self.source_dir, verbose)
882
+
883
+ def analyze(self) -> Dict[str, Any]:
884
+ """
885
+ Analyze the source directory.
886
+
887
+ Returns:
888
+ Analysis summary
889
+ """
890
+ return self.analyzer.get_summary()
891
+
892
+ def plan_migration(self, strategy: str = "safe") -> Dict[str, Any]:
893
+ """
894
+ Plan migration from source to target.
895
+
896
+ Args:
897
+ strategy: Migration strategy ("safe", "replace", "merge")
898
+
899
+ Returns:
900
+ Migration plan
901
+ """
902
+ return self.analyzer.get_migration_plan(strategy)
903
+
904
+ def execute_migration(self, plan: Dict[str, Any], dry_run: bool = False) -> bool:
905
+ """
906
+ Execute a migration plan.
907
+
908
+ Args:
909
+ plan: Migration plan from plan_migration()
910
+ dry_run: If True, only log actions without executing
911
+
912
+ Returns:
913
+ True if successful, False otherwise
914
+ """
915
+ return self.analyzer.execute_migration_plan(plan, dry_run)
916
+
917
+ def migrate(self, strategy: str = "safe", dry_run: bool = False) -> bool:
918
+ """
919
+ Analyze, plan, and execute migration in one step.
920
+
921
+ Args:
922
+ strategy: Migration strategy ("safe", "replace", "merge")
923
+ dry_run: If True, only log actions without executing
924
+
925
+ Returns:
926
+ True if successful, False otherwise
927
+ """
928
+ plan = self.plan_migration(strategy)
929
+ return self.execute_migration(plan, dry_run)
930
+
931
+
932
+ def analyze_directory(directory: str, verbose: int = 0) -> Dict[str, Any]:
933
+ """
934
+ Analyze a directory for RepoKit compatibility.
935
+
936
+ Args:
937
+ directory: Directory to analyze
938
+ verbose: Verbosity level (0=normal, 1=info, 2=debug)
939
+
940
+ Returns:
941
+ Analysis summary
942
+ """
943
+ analyzer = DirectoryAnalyzer(directory, verbose)
944
+ return analyzer.get_summary()
945
+
946
+
947
+ def plan_migration(
948
+ directory: str, strategy: str = "safe", verbose: int = 0
949
+ ) -> Dict[str, Any]:
950
+ """
951
+ Plan migration for a directory.
952
+
953
+ Args:
954
+ directory: Directory to analyze
955
+ strategy: Migration strategy ("safe", "replace", "merge")
956
+ verbose: Verbosity level (0=normal, 1=info, 2=debug)
957
+
958
+ Returns:
959
+ Migration plan
960
+ """
961
+ analyzer = DirectoryAnalyzer(directory, verbose)
962
+ return analyzer.get_migration_plan(strategy)
963
+
964
+
965
+ def migrate_directory(
966
+ directory: str, strategy: str = "safe", dry_run: bool = False, verbose: int = 0
967
+ ) -> bool:
968
+ """
969
+ Migrate a directory to RepoKit structure.
970
+
971
+ Args:
972
+ directory: Directory to migrate
973
+ strategy: Migration strategy ("safe", "replace", "merge")
974
+ dry_run: If True, only log actions without executing
975
+ verbose: Verbosity level (0=normal, 1=info, 2=debug)
976
+
977
+ Returns:
978
+ True if successful, False otherwise
979
+ """
980
+ migration = MigrationManager(directory, verbose=verbose)
981
+ return migration.migrate(strategy, dry_run)
982
+
983
+
984
+ def analyze_project(directory: str, verbose: int = 0) -> Dict[str, Any]:
985
+ """
986
+ Comprehensive project analysis for universal migration.
987
+
988
+ Args:
989
+ directory: Directory to analyze
990
+ verbose: Verbosity level (0=normal, 1=info, 2=debug)
991
+
992
+ Returns:
993
+ Comprehensive analysis summary
994
+ """
995
+ analyzer = ProjectAnalyzer(directory, verbose)
996
+ return analyzer.get_comprehensive_summary()
997
+
998
+
999
+ def universal_migrate_project(
1000
+ directory: str,
1001
+ target_name: Optional[str] = None,
1002
+ strategy: str = "safe",
1003
+ branch_strategy: str = "auto",
1004
+ publish_to: Optional[str] = None,
1005
+ dry_run: bool = False,
1006
+ verbose: int = 0,
1007
+ ) -> bool:
1008
+ """
1009
+ Universal project migration to RepoKit structure.
1010
+
1011
+ This function handles any type of project and migrates it to RepoKit structure
1012
+ with appropriate branch strategies and remote publishing.
1013
+
1014
+ Args:
1015
+ directory: Source directory to migrate
1016
+ target_name: Target repository name (default: use directory name)
1017
+ strategy: Migration strategy ("safe", "replace", "merge", "auto")
1018
+ branch_strategy: Branch strategy ("standard", "simple", "gitflow", "auto")
1019
+ publish_to: Remote service to publish to ("github", "gitlab")
1020
+ dry_run: If True, only log actions without executing
1021
+ verbose: Verbosity level (0=normal, 1=info, 2=debug)
1022
+
1023
+ Returns:
1024
+ True if successful, False otherwise
1025
+ """
1026
+ # Use ProjectAnalyzer for comprehensive analysis
1027
+ analyzer = ProjectAnalyzer(directory, verbose)
1028
+ summary = analyzer.get_comprehensive_summary()
1029
+
1030
+ # Determine target name
1031
+ if not target_name:
1032
+ target_name = os.path.basename(os.path.abspath(directory))
1033
+
1034
+ # Auto-select strategies if requested
1035
+ if strategy == "auto":
1036
+ strategy = summary["recommended_strategy"]
1037
+
1038
+ if branch_strategy == "auto":
1039
+ branch_strategy = summary["recommended_branch_strategy"]
1040
+
1041
+ # Use UniversalMigrationExecutor for the actual migration
1042
+ executor = UniversalMigrationExecutor(directory, target_name, verbose)
1043
+ return executor.execute_migration(strategy, branch_strategy, publish_to, dry_run)
1044
+
1045
+
1046
+ def adopt_project(
1047
+ directory: str = ".",
1048
+ strategy: str = "auto",
1049
+ branch_strategy: str = "auto",
1050
+ publish_to: Optional[str] = None,
1051
+ dry_run: bool = False,
1052
+ verbose: int = 0,
1053
+ ) -> bool:
1054
+ """
1055
+ Adopt an existing project in-place to use RepoKit structure.
1056
+
1057
+ This function converts the current directory to use RepoKit structure
1058
+ without creating a new directory.
1059
+
1060
+ Args:
1061
+ directory: Directory to adopt (default: current directory)
1062
+ strategy: Migration strategy ("safe", "replace", "merge", "auto")
1063
+ branch_strategy: Branch strategy ("standard", "simple", "gitflow", "auto")
1064
+ publish_to: Remote service to publish to ("github", "gitlab")
1065
+ dry_run: If True, only log actions without executing
1066
+ verbose: Verbosity level (0=normal, 1=info, 2=debug)
1067
+
1068
+ Returns:
1069
+ True if successful, False otherwise
1070
+ """
1071
+ # Use the directory name as the project name
1072
+ target_name = os.path.basename(os.path.abspath(directory))
1073
+
1074
+ # For adoption, we modify in-place rather than creating a new structure
1075
+ return universal_migrate_project(
1076
+ directory=directory,
1077
+ target_name=target_name,
1078
+ strategy=strategy,
1079
+ branch_strategy=branch_strategy,
1080
+ publish_to=publish_to,
1081
+ dry_run=dry_run,
1082
+ verbose=verbose,
1083
+ )
1084
+
1085
+
1086
+ class UniversalMigrationExecutor:
1087
+ """
1088
+ Executes universal project migrations to RepoKit structure.
1089
+
1090
+ This class handles the actual migration process for any type of project,
1091
+ including Git repository setup, file migration, and remote publishing.
1092
+ """
1093
+
1094
+ def __init__(self, source_dir: str, target_name: str, verbose: int = 0):
1095
+ """Initialize UniversalMigrationExecutor."""
1096
+ self.source_dir = os.path.abspath(source_dir)
1097
+ self.target_name = target_name
1098
+ self.verbose = verbose
1099
+ self.logger = logging.getLogger("repokit.universal_executor")
1100
+
1101
+ # Initialize project analyzer
1102
+ self.analyzer = ProjectAnalyzer(source_dir, verbose)
1103
+
1104
+ def execute_migration(
1105
+ self,
1106
+ strategy: str = "safe",
1107
+ branch_strategy: str = "standard",
1108
+ publish_to: Optional[str] = None,
1109
+ dry_run: bool = False,
1110
+ ) -> bool:
1111
+ """
1112
+ Execute the universal migration.
1113
+
1114
+ Args:
1115
+ strategy: Migration strategy
1116
+ branch_strategy: Branch strategy to implement
1117
+ publish_to: Remote service to publish to ("github", "gitlab")
1118
+ dry_run: If True, only log actions without executing
1119
+
1120
+ Returns:
1121
+ True if successful, False otherwise
1122
+ """
1123
+ if dry_run:
1124
+ return self._simulate_migration(strategy, branch_strategy, publish_to)
1125
+
1126
+ try:
1127
+ # Step 1: Prepare target structure
1128
+ if not self._prepare_target_structure():
1129
+ return False
1130
+
1131
+ # Step 2: Migrate files based on strategy
1132
+ if not self._migrate_files(strategy):
1133
+ return False
1134
+
1135
+ # Step 3: Set up Git repository
1136
+ if not self._setup_git_repository(branch_strategy):
1137
+ return False
1138
+
1139
+ # Step 4: Generate templates
1140
+ if not self._generate_templates():
1141
+ return False
1142
+
1143
+ # Step 5: Publish to remote if requested
1144
+ if publish_to and not self._publish_to_remote(publish_to):
1145
+ return False
1146
+
1147
+ self.logger.info("Universal migration completed successfully")
1148
+ return True
1149
+
1150
+ except Exception as e:
1151
+ self.logger.error(f"Migration failed: {str(e)}")
1152
+ return False
1153
+
1154
+ def _simulate_migration(
1155
+ self, strategy: str, branch_strategy: str, publish_to: Optional[str]
1156
+ ) -> bool:
1157
+ """Simulate migration for dry run."""
1158
+ summary = self.analyzer.get_comprehensive_summary()
1159
+
1160
+ self.logger.info("=== MIGRATION SIMULATION ===")
1161
+ self.logger.info(f"Source: {self.source_dir}")
1162
+ self.logger.info(f"Target: {self.target_name}")
1163
+ self.logger.info(f"Project type: {summary['project_type']}")
1164
+ self.logger.info(f"Language: {summary['detected_language']}")
1165
+ self.logger.info(f"Migration strategy: {strategy}")
1166
+ self.logger.info(f"Branch strategy: {branch_strategy}")
1167
+
1168
+ if publish_to:
1169
+ self.logger.info(f"Would publish to: {publish_to}")
1170
+
1171
+ self.logger.info("Migration steps:")
1172
+ for i, step in enumerate(summary["migration_steps"], 1):
1173
+ self.logger.info(f" {i}. {step}")
1174
+
1175
+ return True
1176
+
1177
+ def _prepare_target_structure(self) -> bool:
1178
+ """Prepare the target RepoKit structure."""
1179
+ self.logger.info("Preparing target structure...")
1180
+ # Implementation would go here
1181
+ return True
1182
+
1183
+ def _migrate_files(self, strategy: str) -> bool:
1184
+ """Migrate files based on strategy."""
1185
+ self.logger.info(f"Migrating files using strategy: {strategy}")
1186
+ # Implementation would go here
1187
+ return True
1188
+
1189
+ def _setup_git_repository(self, branch_strategy: str) -> bool:
1190
+ """Set up Git repository with chosen branch strategy."""
1191
+ self.logger.info(f"Setting up Git repository with strategy: {branch_strategy}")
1192
+
1193
+ try:
1194
+ git_manager = GitManager(self.source_dir, self.verbose)
1195
+
1196
+ # Define branch strategies
1197
+ # Use centralized branch configurations from defaults.py
1198
+ branches = DEFAULT_BRANCH_STRATEGIES.get(branch_strategy, ["private", "dev", "main"])
1199
+
1200
+ # Get current repo state
1201
+ repo_state = git_manager.get_repo_state()
1202
+ current_branch = repo_state.get("current_branch")
1203
+ existing_branches = set(repo_state.get("branches", []))
1204
+
1205
+ self.logger.info(f"Current branch: {current_branch}")
1206
+ self.logger.info(f"Existing branches: {existing_branches}")
1207
+ self.logger.info(f"Target branches for {branch_strategy}: {branches}")
1208
+
1209
+ # Create missing branches
1210
+ for branch in branches:
1211
+ if branch not in existing_branches:
1212
+ try:
1213
+ git_manager.run_git(["checkout", "-b", branch])
1214
+ self.logger.info(f"Created branch: {branch}")
1215
+ existing_branches.add(branch)
1216
+ except Exception as e:
1217
+ self.logger.warning(f"Failed to create branch {branch}: {e}")
1218
+
1219
+ # Switch to main/default branch
1220
+ default_branch = "main" if "main" in branches else branches[-1]
1221
+ if default_branch in existing_branches:
1222
+ try:
1223
+ git_manager.run_git(["checkout", default_branch])
1224
+ self.logger.info(f"Switched to default branch: {default_branch}")
1225
+ except Exception as e:
1226
+ self.logger.warning(f"Failed to switch to {default_branch}: {e}")
1227
+
1228
+ return True
1229
+
1230
+ except Exception as e:
1231
+ self.logger.error(f"Failed to setup Git repository: {e}")
1232
+ return False
1233
+
1234
+ def _generate_templates(self) -> bool:
1235
+ """Generate appropriate templates."""
1236
+ self.logger.info("Generating templates...")
1237
+ # Implementation would go here
1238
+ return True
1239
+
1240
+ def _publish_to_remote(self, service: str) -> bool:
1241
+ """Publish to remote service."""
1242
+ self.logger.info(f"Publishing to {service}...")
1243
+ # Implementation would go here
1244
+ return True