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
repokit/cli.py ADDED
@@ -0,0 +1,2997 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ RepoKit: Repository Template Generator
4
+
5
+ Command-line interface for creating standardized Git repositories.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import json
11
+ import argparse
12
+ import logging
13
+ import subprocess
14
+ from typing import Dict, Any, Optional
15
+
16
+ from .config import ConfigManager
17
+ from .defaults import BRANCH_STRATEGIES, DEFAULT_ADOPT_STRATEGY, PRIVATE_BRANCH_STRATEGIES
18
+ from .repo_manager import RepoManager
19
+ from .template_engine import TemplateEngine
20
+ from .directory_analyzer import (
21
+ analyze_directory,
22
+ plan_migration,
23
+ migrate_directory,
24
+ analyze_project,
25
+ adopt_project,
26
+ )
27
+
28
+ # Set UTF-8 encoding for subprocess on Windows to prevent Unicode errors
29
+ if sys.platform == 'win32':
30
+ import locale
31
+ # Force UTF-8 for subprocess communication
32
+ os.environ['PYTHONIOENCODING'] = 'utf-8'
33
+ os.environ['PYTHONUTF8'] = '1'
34
+
35
+
36
+ def parse_arguments() -> argparse.Namespace:
37
+ """
38
+ Parse command-line arguments.
39
+
40
+ Returns:
41
+ Parsed arguments
42
+ """
43
+ parser = argparse.ArgumentParser(
44
+ prog="repokit",
45
+ description="RepoKit - A Git repository template generator with standardized structures",
46
+ epilog="""
47
+ Examples:
48
+ # Create a new Python project with standard structure
49
+ repokit create myproject --language python --dir-profile standard
50
+
51
+ # Adopt an existing project with RepoKit structure
52
+ repokit adopt ./existing-project --strategy safe
53
+
54
+ # Create with custom branch strategy
55
+ repokit create webapp --branch-strategy gitflow --publish-to github
56
+
57
+ For detailed help on any command, use: repokit <command> --help
58
+ """,
59
+ formatter_class=argparse.RawDescriptionHelpFormatter
60
+ )
61
+
62
+ # Version argument
63
+ from . import __version__
64
+ parser.add_argument("--version", action="version", version=f"RepoKit {__version__}")
65
+
66
+ # Global options that apply to all commands
67
+ parser.add_argument(
68
+ "--config", "-c",
69
+ help="Path to configuration file (JSON)",
70
+ metavar="FILE"
71
+ )
72
+
73
+ parser.add_argument(
74
+ "--save-config",
75
+ help="Save the final configuration to the specified file",
76
+ metavar="FILE"
77
+ )
78
+
79
+ # Verbosity control
80
+ verbosity_group = parser.add_mutually_exclusive_group()
81
+ verbosity_group.add_argument(
82
+ "--verbose",
83
+ "-v",
84
+ action="count",
85
+ default=0,
86
+ help="Increase verbosity (-v for INFO, -vv for DEBUG, -vvv for detailed DEBUG)"
87
+ )
88
+ verbosity_group.add_argument(
89
+ "--quiet", "-q", action="store_true", help="Suppress all output except errors"
90
+ )
91
+
92
+ # Create subparsers for commands
93
+ subparsers = parser.add_subparsers(
94
+ title="Commands",
95
+ description="Available commands",
96
+ dest="command",
97
+ help="Use 'repokit <command> --help' for command-specific help",
98
+ required=True
99
+ )
100
+
101
+ # CREATE command
102
+ create_parser = subparsers.add_parser(
103
+ "create",
104
+ help="Create a new repository with RepoKit structure",
105
+ description="""
106
+ Create a new Git repository with standardized structure, branches, and templates.
107
+
108
+ This command initializes a complete project structure including:
109
+ - Directory hierarchy based on profiles or custom specification
110
+ - Git setup for the chosen branch strategy
111
+ - Template files for documentation, CI/CD, and language-specific needs
112
+ - Pre-commit hooks for private content protection
113
+ - Optional remote repository creation and pushing
114
+
115
+ Directory Profiles:
116
+ minimal - Basic structure: src, tests, docs
117
+ standard - Common structure with config, logs, private directories
118
+ complete - Full structure including examples, assets, resources
119
+
120
+ Branch Strategies:
121
+ main-only - one branch, main; the repository is <name>/ itself and
122
+ private/ stays out of git
123
+
124
+ Private-branch model (work on a local-only 'private' branch, move finished
125
+ work to the public branches; repository in <name>/local/, main checked out
126
+ in <name>/github/):
127
+ standard - private, main, dev, staging, test, live
128
+ simple - private, main, dev
129
+ gitflow - private, main, develop, release, hotfix
130
+ github-flow - private, main
131
+ minimal - private, main (no worktrees)
132
+ """,
133
+ epilog="""
134
+ Examples:
135
+ # Basic Python project
136
+ repokit create myapp --language python
137
+
138
+ # Web project with standard profile
139
+ repokit create webapp --dir-profile standard --language javascript
140
+
141
+ # Enterprise project with custom directories
142
+ repokit create enterprise-app --dir-profile complete --directories "analytics,ml-models"
143
+
144
+ # Create and publish to GitHub
145
+ repokit create myproject --publish-to github --private-repo
146
+
147
+ Recipe: Data Science Project
148
+ repokit create ml-project \\
149
+ --language python \\
150
+ --dir-profile standard \\
151
+ --directories "data,models,notebooks" \\
152
+ --branch-strategy simple
153
+
154
+ Recipe: Microservice
155
+ repokit create user-service \\
156
+ --dir-profile minimal \\
157
+ --branch-strategy github-flow \\
158
+ --publish-to github \\
159
+ --organization mycompany
160
+ """,
161
+ formatter_class=argparse.RawDescriptionHelpFormatter
162
+ )
163
+
164
+ create_parser.add_argument(
165
+ "name",
166
+ help="Name of the repository to create"
167
+ )
168
+
169
+ # Language and description. The names come from the code-type layers'
170
+ # manifest (repokit/templates/layers/manifest.toml) and are validated
171
+ # against it before anything is created, so the list here is the help only.
172
+ create_parser.add_argument(
173
+ "--language", "-l",
174
+ action="append",
175
+ metavar="LANG[,LANG...]",
176
+ help=f"The project's language(s), in the order their layers apply; a list or repeated. "
177
+ f"Known: {', '.join(_known_language_names())} (default: generic, no code-type layers)"
178
+ )
179
+
180
+ create_parser.add_argument(
181
+ "--with",
182
+ dest="with_layers",
183
+ action="append",
184
+ metavar="NAME[,NAME...]",
185
+ help="What the project is, below its language: a preset such as python/pypi or "
186
+ "js-ts/claude-plugin, a layer such as c-cpp/qt, or bare names when unique "
187
+ "(claude-plugin; cmake-lib,qt). Comma-separated or repeated; each implies its "
188
+ "language; a list that equals a preset is that preset. See `repokit list`."
189
+ )
190
+
191
+ create_parser.add_argument(
192
+ "--license",
193
+ metavar="ID|file=PATH|none",
194
+ help="The LICENSE to write: a catalogue id (mit, gpl-3.0, gpl-3.0-or-later, apache-2.0, "
195
+ "all-rights-reserved, ...; `repokit list` shows them), file=PATH to copy a text as is, or "
196
+ "none. Required unless the global config names `license` (or `license_private` for "
197
+ "--private-repo); holder and year come from the git identity and the clock."
198
+ )
199
+
200
+ create_parser.add_argument(
201
+ "--extras",
202
+ dest="extras",
203
+ action="append",
204
+ metavar="NAME[,NAME...]",
205
+ help="The structure's optional files, by name: funding, dependabot, stale, pr-template, "
206
+ "release-template, roadmap, platform-support, vscode-settings, placeholders, or all. "
207
+ "Off unless named here or in the global config's `extras` list."
208
+ )
209
+ create_parser.add_argument(
210
+ "--no-extras",
211
+ dest="no_extras",
212
+ action="store_true",
213
+ help="Write none of the extras, whatever the global config says"
214
+ )
215
+
216
+ create_parser.add_argument(
217
+ "--github-setup",
218
+ dest="github_setup",
219
+ action="append",
220
+ metavar="STEP[,STEP...]",
221
+ help="With --github, what to set up on the new repository: labels, issues (the Roadmap and "
222
+ "Quick Notes as #1 and #2, before the first push), pin, discussions, topics, "
223
+ "pypi-environment, traffic (prints ghtraf's commands), or all. Off unless named here "
224
+ "or in the global config's `github_setup` list."
225
+ )
226
+ create_parser.add_argument(
227
+ "--topics",
228
+ metavar="TOPIC[,TOPIC...]",
229
+ help="Repository topics for --github-setup topics"
230
+ )
231
+ create_parser.add_argument(
232
+ "--badges",
233
+ metavar="TEMPLATE|BADGE[,BADGE...]",
234
+ help="README badges: a template (pypi, github, comfyui, minimal, or one saved as "
235
+ "badge_templates in the config) or a list of badges (pypi-version, release-date, "
236
+ "downloads, release-version, ci, python, node, comfyui-registry, license, discussions, "
237
+ "installs, views, platform). Default: the preset's template"
238
+ )
239
+
240
+ create_parser.add_argument(
241
+ "--description", "-d",
242
+ help="Short description of the repository"
243
+ )
244
+
245
+ # Directory configuration - NEW PROFILE SUPPORT
246
+ dir_group = create_parser.add_argument_group("Directory Configuration")
247
+ dir_group.add_argument(
248
+ "--dir-profile",
249
+ choices=["minimal", "standard", "complete"],
250
+ help="Use a predefined directory profile"
251
+ )
252
+
253
+ dir_group.add_argument(
254
+ "--dir-groups",
255
+ help="Comma-separated list of directory groups (development,documentation,operations,privacy)",
256
+ metavar="GROUPS"
257
+ )
258
+
259
+ dir_group.add_argument(
260
+ "--directories", "-dir",
261
+ help="Comma-separated list of additional directories to create",
262
+ metavar="DIRS"
263
+ )
264
+
265
+ dir_group.add_argument(
266
+ "--private-dirs", "-pd",
267
+ help="Comma-separated list of directories to mark as private",
268
+ metavar="DIRS"
269
+ )
270
+
271
+ dir_group.add_argument(
272
+ "--private-set",
273
+ choices=["standard", "enhanced"],
274
+ default="standard",
275
+ help="Private directory set to use (default: standard)"
276
+ )
277
+
278
+ dir_group.add_argument(
279
+ "--sensitive-files", "-sf",
280
+ help="Comma-separated list of sensitive files to exclude from public branches",
281
+ metavar="FILES"
282
+ )
283
+
284
+ dir_group.add_argument(
285
+ "--sensitive-patterns", "-sp",
286
+ help="Comma-separated list of file patterns to exclude from public branches (supports glob)",
287
+ metavar="PATTERNS"
288
+ )
289
+
290
+ # Branch configuration
291
+ branch_group = create_parser.add_argument_group("Branch Configuration")
292
+ branch_group.add_argument(
293
+ "--branches", "-b",
294
+ help="Comma-separated list of branches to create",
295
+ metavar="BRANCHES"
296
+ )
297
+
298
+ branch_group.add_argument(
299
+ "--worktrees", "-w",
300
+ help="Comma-separated list of branches to create worktrees for",
301
+ metavar="BRANCHES"
302
+ )
303
+
304
+ branch_group.add_argument(
305
+ "--private-branch",
306
+ default=None,
307
+ help="Name of the private branch (default: the strategy's; 'private' in the private-branch strategies)"
308
+ )
309
+
310
+ branch_group.add_argument(
311
+ "--default-branch",
312
+ help="Name of the default branch (default: main)"
313
+ )
314
+
315
+ branch_group.add_argument(
316
+ "--branch-strategy",
317
+ choices=list(BRANCH_STRATEGIES),
318
+ help="Predefined branch strategy to use (default: main-only; or a private-branch strategy)"
319
+ )
320
+
321
+ branch_group.add_argument(
322
+ "--private-mode",
323
+ choices=["nested", "branch"],
324
+ help="How private work is kept: 'nested' makes private/ its own git repository "
325
+ "(linked into every worktree); 'branch' keeps it on the private branch only. "
326
+ "Default: nested for main-only, branch for the private-branch strategies"
327
+ )
328
+
329
+ common_group = branch_group.add_mutually_exclusive_group()
330
+ common_group.add_argument(
331
+ "--with-common", dest="with_common", action="store_true", default=None,
332
+ help="Vendor git-repokit-common (hooks, version sync, tools) at scripts/repokit-common/ "
333
+ "and install its hooks (default for main-only)"
334
+ )
335
+ common_group.add_argument(
336
+ "--no-common", dest="with_common", action="store_false",
337
+ help="Do not vendor git-repokit-common; git-repokit's own pre-commit guard is installed instead "
338
+ "(default for the private-branch strategies)"
339
+ )
340
+
341
+ branch_group.add_argument(
342
+ "--branch-config",
343
+ help="Path to branch configuration file (JSON)",
344
+ metavar="FILE"
345
+ )
346
+
347
+ branch_group.add_argument(
348
+ "--branch-dir-main",
349
+ help="Directory name for main branch worktree (default: github)"
350
+ )
351
+
352
+ branch_group.add_argument(
353
+ "--branch-dir-dev",
354
+ help="Directory name for dev branch worktree (default: dev)"
355
+ )
356
+
357
+ # Git configuration
358
+ git_group = create_parser.add_argument_group("Git Configuration")
359
+ git_group.add_argument(
360
+ "--user-name",
361
+ help="Git user name for commits"
362
+ )
363
+
364
+ git_group.add_argument(
365
+ "--user-email",
366
+ help="Git user email for commits"
367
+ )
368
+
369
+ # Remote/Publishing options
370
+ remote_group = create_parser.add_argument_group("Remote Repository")
371
+ remote_group.add_argument(
372
+ "--github",
373
+ action="store_true",
374
+ help="Create the repository on GitHub with the gh CLI (its login is used; private unless "
375
+ "--public) and push the public branches. With --no-push nothing is pushed"
376
+ )
377
+ remote_group.add_argument(
378
+ "--public",
379
+ action="store_true",
380
+ help="With --github: make the GitHub repository public"
381
+ )
382
+ remote_group.add_argument(
383
+ "--publish-to",
384
+ choices=["github", "gitlab"],
385
+ help="Publish repository to a remote service through its REST API and a token"
386
+ )
387
+
388
+ remote_group.add_argument(
389
+ "--remote-name",
390
+ default="origin",
391
+ help="Name for the remote (default: origin)"
392
+ )
393
+
394
+ remote_group.add_argument(
395
+ "--private-repo",
396
+ action="store_true",
397
+ help="Create a private repository on remote service"
398
+ )
399
+
400
+ remote_group.add_argument(
401
+ "--organization", "--org",
402
+ help="GitHub organization or GitLab group name"
403
+ )
404
+
405
+ remote_group.add_argument(
406
+ "--no-push",
407
+ action="store_true",
408
+ help="Don't push branches after creating remote repository"
409
+ )
410
+
411
+ # Authentication
412
+ auth_group = create_parser.add_argument_group("Authentication")
413
+ auth_group.add_argument(
414
+ "--token",
415
+ help="Authentication token for remote service"
416
+ )
417
+
418
+ auth_group.add_argument(
419
+ "--token-command",
420
+ help="Command to retrieve token (e.g., 'pass show github/token')"
421
+ )
422
+
423
+ auth_group.add_argument(
424
+ "--credentials-file",
425
+ help="Path to credentials file",
426
+ metavar="FILE"
427
+ )
428
+
429
+ # Template options
430
+ template_group = create_parser.add_argument_group("Template Configuration")
431
+ template_group.add_argument(
432
+ "--templates-dir",
433
+ help="Path to custom templates directory",
434
+ metavar="DIR"
435
+ )
436
+
437
+ template_group.add_argument(
438
+ "--ai",
439
+ choices=["none", "claude"],
440
+ default="claude",
441
+ help="AI tool integration to include (default: claude)"
442
+ )
443
+
444
+ # ANALYZE command
445
+ analyze_parser = subparsers.add_parser(
446
+ "analyze",
447
+ help="Analyze an existing directory for RepoKit compatibility",
448
+ description="""
449
+ Analyze an existing directory to understand its structure and provide recommendations
450
+ for RepoKit adoption or migration.
451
+
452
+ This command examines:
453
+ - Current directory structure and how it maps to RepoKit profiles
454
+ - Git repository state and branch configuration
455
+ - Existing configuration files and potential conflicts
456
+ - Project type detection (language, framework)
457
+ - Migration complexity assessment
458
+
459
+ The analysis provides:
460
+ - Recommended migration strategy (safe, replace, merge)
461
+ - Suggested branch strategy based on current setup
462
+ - List of actions that would be taken during migration
463
+ - Potential issues or conflicts to resolve
464
+ """,
465
+ epilog="""
466
+ Examples:
467
+ # Analyze current directory
468
+ repokit analyze .
469
+
470
+ # Analyze a specific project
471
+ repokit analyze /path/to/project
472
+
473
+ # Verbose analysis with detailed information
474
+ repokit analyze ./myproject -vv
475
+ """,
476
+ formatter_class=argparse.RawDescriptionHelpFormatter
477
+ )
478
+
479
+ analyze_parser.add_argument(
480
+ "name",
481
+ help="Path to directory to analyze"
482
+ )
483
+
484
+ # MIGRATE command
485
+ migrate_parser = subparsers.add_parser(
486
+ "migrate",
487
+ help="Migrate an existing directory to RepoKit structure",
488
+ description="""
489
+ Migrate an existing directory to use RepoKit's standardized structure.
490
+
491
+ Migration Strategies:
492
+ safe - Preserve all existing files, only add missing RepoKit elements
493
+ replace - Replace conflicting files with RepoKit templates
494
+ merge - Attempt to merge existing content with RepoKit templates
495
+
496
+ This command will:
497
+ - Create missing standard directories
498
+ - Add RepoKit configuration files
499
+ - Set up Git hooks for private content protection
500
+ - Generate appropriate .gitignore entries
501
+ - Create template files where needed
502
+ """,
503
+ epilog="""
504
+ Examples:
505
+ # Safe migration (preserves all existing files)
506
+ repokit migrate ./myproject
507
+
508
+ # Replace strategy for clean template files
509
+ repokit migrate ./myproject --migration-strategy replace
510
+
511
+ # Dry run to see what would happen
512
+ repokit migrate ./myproject --dry-run
513
+
514
+ # Migrate and publish to GitHub
515
+ repokit migrate ./myproject --publish-to github
516
+ """,
517
+ formatter_class=argparse.RawDescriptionHelpFormatter
518
+ )
519
+
520
+ migrate_parser.add_argument(
521
+ "name",
522
+ help="Path to directory to migrate"
523
+ )
524
+
525
+ migrate_parser.add_argument(
526
+ "--migration-strategy",
527
+ choices=["safe", "replace", "merge"],
528
+ default="safe",
529
+ help="Migration strategy (default: safe)"
530
+ )
531
+
532
+ migrate_parser.add_argument(
533
+ "--dry-run",
534
+ action="store_true",
535
+ help="Show what would be done without making changes"
536
+ )
537
+
538
+ migrate_parser.add_argument(
539
+ "--backup",
540
+ action="store_true",
541
+ help="Create a backup of the directory before making changes"
542
+ )
543
+
544
+ migrate_parser.add_argument(
545
+ "--backup-location",
546
+ help="Custom backup location (default: <dir>_backup_<timestamp>)",
547
+ metavar="PATH"
548
+ )
549
+
550
+ migrate_parser.add_argument(
551
+ "--publish-to",
552
+ choices=["github", "gitlab"],
553
+ help="Publish to remote after migration"
554
+ )
555
+
556
+ # ADOPT command
557
+ adopt_parser = subparsers.add_parser(
558
+ "adopt",
559
+ help="Adopt RepoKit in an existing Git repository",
560
+ description="""
561
+ Adopt RepoKit structure in an existing Git repository without disrupting
562
+ current development workflow.
563
+
564
+ This is the recommended approach for active projects. It:
565
+ - Preserves all existing Git history
566
+ - Adds RepoKit structure incrementally
567
+ - Sets up branches according to detected or specified strategy
568
+ - Maintains existing remote connections
569
+ - Can optionally publish to a new remote
570
+
571
+ The adopt command is safer than migrate for repositories with:
572
+ - Active development
573
+ - Existing CI/CD pipelines
574
+ - Multiple contributors
575
+ - Production deployments
576
+ """,
577
+ epilog="""
578
+ Examples:
579
+
580
+ BASIC ADOPTION:
581
+ # Adopt current directory with minimal setup
582
+ repokit adopt
583
+
584
+ # Adopt specific directory with standard profile
585
+ repokit adopt ./my-project --dir-profile standard
586
+
587
+ PYTHON PROJECT ADOPTION (Real Example):
588
+ # Adopt a Python project with comprehensive setup and GitHub publishing
589
+ # This is the command used for the UNCtools project deployment
590
+ repokit adopt ./my-python-project \\
591
+ --branch-strategy simple \\
592
+ --migration-strategy safe \\
593
+ --dir-profile standard \\
594
+ --language python \\
595
+ --description "Universal Naming Convention (UNC) path tools for Python" \\
596
+ --publish-to github \\
597
+ --repo-name MyPythonTools \\
598
+ --private-repo \\
599
+ --ai claude \\
600
+ --backup \\
601
+ --backup-location ../backup-before-adoption
602
+
603
+ GITHUB DEPLOYMENT RECIPES:
604
+ # Public GitHub repository
605
+ repokit adopt ./open-source-project \\
606
+ --publish-to github \\
607
+ --repo-name awesome-open-source \\
608
+ --language javascript \\
609
+ --dir-profile complete
610
+
611
+ # Private GitHub repository with organization
612
+ repokit adopt ./company-project \\
613
+ --publish-to github \\
614
+ --repo-name internal-tools \\
615
+ --organization mycompany \\
616
+ --private-repo \\
617
+ --language python
618
+
619
+ # Personal project with full configuration
620
+ repokit adopt ./personal-project \\
621
+ --publish-to github \\
622
+ --repo-name my-awesome-project \\
623
+ --private-repo \\
624
+ --description "My personal coding project" \\
625
+ --user-name "John Doe" \\
626
+ --user-email "john@example.com"
627
+
628
+ BRANCH STRATEGY EXAMPLES:
629
+ # Simple strategy (main, dev, private)
630
+ repokit adopt --branch-strategy simple --language python
631
+
632
+ # GitFlow strategy (main, dev, feature, release, hotfix branches)
633
+ repokit adopt --branch-strategy gitflow --dir-profile complete
634
+
635
+ # GitHub Flow (main, feature branches)
636
+ repokit adopt --branch-strategy github-flow
637
+
638
+ # Custom branches
639
+ repokit adopt --branches "main,staging,production" --default-branch main
640
+
641
+ DIRECTORY PROFILE EXAMPLES:
642
+ # Minimal profile (docs, tests, scripts)
643
+ repokit adopt --dir-profile minimal
644
+
645
+ # Standard profile (docs, tests, scripts, examples, config)
646
+ repokit adopt --dir-profile standard
647
+
648
+ # Complete profile (all directories including logs, private, revisions)
649
+ repokit adopt --dir-profile complete
650
+
651
+ # Custom directories
652
+ repokit adopt --directories "data,models,notebooks,experiments"
653
+
654
+ SENSITIVE FILE PROTECTION:
655
+ # Specify sensitive files to exclude from public branches
656
+ repokit adopt --sensitive-files ".env,secrets.json,api_keys.txt"
657
+
658
+ # Specify sensitive patterns (vim backups, logs, private dirs)
659
+ repokit adopt --sensitive-patterns "*.log,*~,private/*,secrets/*"
660
+
661
+ # Use enhanced privacy protection
662
+ repokit adopt --private-set enhanced --private-dirs "experiments,drafts"
663
+
664
+ BACKUP AND SAFETY:
665
+ # Create backup before adoption
666
+ repokit adopt --backup --backup-location ../project-backup-$(date +%Y%m%d)
667
+
668
+ # Dry run to preview all changes
669
+ repokit adopt --dry-run -vv
670
+
671
+ # Clean git history during adoption
672
+ repokit adopt --clean-history --cleaning-recipe pre-open-source
673
+
674
+ AI INTEGRATION:
675
+ # Add Claude AI integration files
676
+ repokit adopt --ai claude
677
+
678
+ # Skip AI integration
679
+ repokit adopt --ai none
680
+
681
+ COMMON WORKFLOWS:
682
+ 1. Test adoption (recommended first step):
683
+ repokit adopt ./my-project --dry-run -vv
684
+
685
+ 2. Safe adoption with backup:
686
+ repokit adopt ./my-project --backup --backup-location ../backup
687
+
688
+ 3. GitHub deployment:
689
+ repokit adopt ./my-project --publish-to github --repo-name my-project --private-repo
690
+
691
+ 4. Complete adoption (full features):
692
+ repokit adopt ./my-project \\
693
+ --dir-profile complete \\
694
+ --branch-strategy simple \\
695
+ --language python \\
696
+ --ai claude \\
697
+ --publish-to github \\
698
+ --private-repo
699
+
700
+ For more detailed documentation, see: docs/Adoption-Guide.md
701
+ """,
702
+ formatter_class=argparse.RawDescriptionHelpFormatter
703
+ )
704
+
705
+ adopt_parser.add_argument(
706
+ "name",
707
+ nargs="?",
708
+ help="Path to repository (default: current directory)"
709
+ )
710
+
711
+ # Language and description (the same names as create; the layers are laid
712
+ # down without overwriting the adopted project's own files)
713
+ adopt_parser.add_argument(
714
+ "--language", "-l",
715
+ action="append",
716
+ metavar="LANG[,LANG...]",
717
+ help=f"The project's language(s). Known: {', '.join(_known_language_names())} (default: generic)"
718
+ )
719
+
720
+ adopt_parser.add_argument(
721
+ "--with",
722
+ dest="with_layers",
723
+ action="append",
724
+ metavar="NAME[,NAME...]",
725
+ help="Presets and layers to lay down without overwriting the project's own files, "
726
+ "as for create (see `repokit list`)"
727
+ )
728
+
729
+ adopt_parser.add_argument(
730
+ "--license",
731
+ metavar="ID|file=PATH|none",
732
+ help="The LICENSE to write when the project has none, as for create; an existing LICENSE is kept"
733
+ )
734
+
735
+ adopt_parser.add_argument(
736
+ "--description", "-d",
737
+ help="Short description of the repository"
738
+ )
739
+
740
+ # Directory configuration - NEW PROFILE SUPPORT
741
+ adopt_dir_group = adopt_parser.add_argument_group("Directory Configuration")
742
+ adopt_dir_group.add_argument(
743
+ "--dir-profile",
744
+ choices=["minimal", "standard", "complete"],
745
+ help="Use a predefined directory profile"
746
+ )
747
+
748
+ adopt_dir_group.add_argument(
749
+ "--dir-groups",
750
+ help="Comma-separated list of directory groups (development,documentation,operations,privacy)",
751
+ metavar="GROUPS"
752
+ )
753
+
754
+ adopt_dir_group.add_argument(
755
+ "--directories", "-dir",
756
+ help="Comma-separated list of additional directories to create",
757
+ metavar="DIRS"
758
+ )
759
+
760
+ adopt_dir_group.add_argument(
761
+ "--private-dirs", "-pd",
762
+ help="Comma-separated list of directories to mark as private",
763
+ metavar="DIRS"
764
+ )
765
+
766
+ adopt_dir_group.add_argument(
767
+ "--private-set",
768
+ choices=["standard", "enhanced"],
769
+ default="standard",
770
+ help="Private directory set to use (default: standard)"
771
+ )
772
+
773
+ adopt_dir_group.add_argument(
774
+ "--sensitive-files", "-sf",
775
+ help="Comma-separated list of sensitive files to exclude from public branches",
776
+ metavar="FILES"
777
+ )
778
+
779
+ adopt_dir_group.add_argument(
780
+ "--sensitive-patterns", "-sp",
781
+ help="Comma-separated list of file patterns to exclude from public branches (supports glob)",
782
+ metavar="PATTERNS"
783
+ )
784
+
785
+ # Branch configuration
786
+ adopt_branch_group = adopt_parser.add_argument_group("Branch Configuration")
787
+ adopt_branch_group.add_argument(
788
+ "--branches", "-b",
789
+ help="Comma-separated list of branches to create",
790
+ metavar="BRANCHES"
791
+ )
792
+
793
+ adopt_branch_group.add_argument(
794
+ "--worktrees", "-w",
795
+ help="Comma-separated list of branches to create worktrees for",
796
+ metavar="BRANCHES"
797
+ )
798
+
799
+ adopt_branch_group.add_argument(
800
+ "--private-branch",
801
+ default=None,
802
+ help="Name of the private branch (default: the strategy's; 'private' in the private-branch strategies)"
803
+ )
804
+
805
+ adopt_branch_group.add_argument(
806
+ "--default-branch",
807
+ help="Name of the default branch (default: main)"
808
+ )
809
+
810
+ # Adoption builds the private-branch model only; main-only adoption is not
811
+ # implemented yet.
812
+ adopt_branch_group.add_argument(
813
+ "--branch-strategy",
814
+ choices=PRIVATE_BRANCH_STRATEGIES,
815
+ help="Branch strategy to implement"
816
+ )
817
+
818
+ adopt_branch_group.add_argument(
819
+ "--branch-config",
820
+ help="Path to branch configuration file (JSON)",
821
+ metavar="FILE"
822
+ )
823
+
824
+ adopt_branch_group.add_argument(
825
+ "--branch-dir-main",
826
+ help="Directory name for main branch worktree (default: github)"
827
+ )
828
+
829
+ adopt_branch_group.add_argument(
830
+ "--branch-dir-dev",
831
+ help="Directory name for dev branch worktree (default: dev)"
832
+ )
833
+
834
+ # Git configuration
835
+ adopt_git_group = adopt_parser.add_argument_group("Git Configuration")
836
+ adopt_git_group.add_argument(
837
+ "--user-name",
838
+ help="Git user name for commits"
839
+ )
840
+
841
+ adopt_git_group.add_argument(
842
+ "--user-email",
843
+ help="Git user email for commits"
844
+ )
845
+
846
+ # Migration strategy
847
+ adopt_parser.add_argument(
848
+ "--migration-strategy",
849
+ choices=["safe", "replace", "merge"],
850
+ default="safe",
851
+ help="File handling strategy (default: safe)"
852
+ )
853
+
854
+ adopt_parser.add_argument(
855
+ "--dry-run",
856
+ action="store_true",
857
+ help="Preview changes without modifying repository"
858
+ )
859
+
860
+ adopt_parser.add_argument(
861
+ "--backup",
862
+ action="store_true",
863
+ help="Create a backup of the repository before making changes"
864
+ )
865
+
866
+ adopt_parser.add_argument(
867
+ "--backup-location",
868
+ help="Custom backup location (default: <repo>_backup_<timestamp>)",
869
+ metavar="PATH"
870
+ )
871
+
872
+ adopt_parser.add_argument(
873
+ "--clean-history",
874
+ action="store_true",
875
+ help="Clean private content from git history before creating public branches"
876
+ )
877
+
878
+ adopt_parser.add_argument(
879
+ "--cleaning-recipe",
880
+ choices=["pre-open-source", "windows-safe", "remove-secrets"],
881
+ default="pre-open-source",
882
+ help="Recipe for cleaning history (default: pre-open-source)"
883
+ )
884
+
885
+ # Remote/Publishing options
886
+ adopt_remote_group = adopt_parser.add_argument_group("Remote Repository")
887
+ adopt_remote_group.add_argument(
888
+ "--publish-to",
889
+ choices=["github", "gitlab"],
890
+ help="Publish repository to a remote service"
891
+ )
892
+
893
+ adopt_remote_group.add_argument(
894
+ "--repo-name",
895
+ help="Name for the remote repository (default: directory name)"
896
+ )
897
+
898
+ adopt_remote_group.add_argument(
899
+ "--remote-name",
900
+ default="origin",
901
+ help="Name for the remote (default: origin)"
902
+ )
903
+
904
+ adopt_remote_group.add_argument(
905
+ "--private-repo",
906
+ action="store_true",
907
+ help="Create a private repository on remote service"
908
+ )
909
+
910
+ adopt_remote_group.add_argument(
911
+ "--organization",
912
+ help="GitHub organization or GitLab group name"
913
+ )
914
+
915
+ adopt_remote_group.add_argument(
916
+ "--no-push",
917
+ action="store_true",
918
+ help="Don't push branches after creating remote repository"
919
+ )
920
+
921
+ # Authentication
922
+ adopt_auth_group = adopt_parser.add_argument_group("Authentication")
923
+ adopt_auth_group.add_argument(
924
+ "--token",
925
+ help="Authentication token for remote service"
926
+ )
927
+
928
+ adopt_auth_group.add_argument(
929
+ "--token-command",
930
+ help="Command to retrieve token (e.g., 'pass show github/token')"
931
+ )
932
+
933
+ adopt_auth_group.add_argument(
934
+ "--credentials-file",
935
+ help="Path to credentials file",
936
+ metavar="FILE"
937
+ )
938
+
939
+ # Template options
940
+ adopt_template_group = adopt_parser.add_argument_group("Template Configuration")
941
+ adopt_template_group.add_argument(
942
+ "--templates-dir",
943
+ help="Path to custom templates directory",
944
+ metavar="DIR"
945
+ )
946
+
947
+ adopt_template_group.add_argument(
948
+ "--ai",
949
+ choices=["none", "claude"],
950
+ default="claude",
951
+ help="AI tool integration to include (default: claude)"
952
+ )
953
+
954
+ # PUBLISH command
955
+ publish_parser = subparsers.add_parser(
956
+ "publish",
957
+ help="Publish an existing repository to GitHub/GitLab",
958
+ description="""
959
+ Publish an existing RepoKit repository to a remote service.
960
+
961
+ This command:
962
+ - Creates a remote repository on GitHub or GitLab
963
+ - Sets up authentication using tokens or stored credentials
964
+ - Configures remote tracking for all branches
965
+ - Pushes all branches and tags
966
+ - Can work with organization/group repositories
967
+ """,
968
+ epilog="""
969
+ Examples:
970
+ # Publish current repository to GitHub
971
+ repokit publish . --publish-to github
972
+
973
+ # Publish to organization with private visibility
974
+ repokit publish ./myproject --publish-to github --organization myorg --private-repo
975
+
976
+ # Publish to GitLab group
977
+ repokit publish . --publish-to gitlab --organization mygroup
978
+
979
+ # Publish without pushing branches
980
+ repokit publish . --publish-to github --no-push
981
+ """,
982
+ formatter_class=argparse.RawDescriptionHelpFormatter
983
+ )
984
+
985
+ publish_parser.add_argument(
986
+ "name",
987
+ help="Path to repository to publish"
988
+ )
989
+
990
+ publish_parser.add_argument(
991
+ "--repo-name",
992
+ help="Name for the remote repository (default: directory name)"
993
+ )
994
+
995
+ publish_parser.add_argument(
996
+ "--publish-to",
997
+ choices=["github", "gitlab"],
998
+ required=True,
999
+ help="Remote service to publish to"
1000
+ )
1001
+
1002
+ publish_parser.add_argument(
1003
+ "--remote-name",
1004
+ default="origin",
1005
+ help="Name for the remote (default: origin)"
1006
+ )
1007
+
1008
+ publish_parser.add_argument(
1009
+ "--private-repo",
1010
+ action="store_true",
1011
+ help="Create as private repository"
1012
+ )
1013
+
1014
+ publish_parser.add_argument(
1015
+ "--organization",
1016
+ help="Organization/group name"
1017
+ )
1018
+
1019
+ publish_parser.add_argument(
1020
+ "--no-push",
1021
+ action="store_true",
1022
+ help="Create remote but don't push"
1023
+ )
1024
+
1025
+ publish_parser.add_argument(
1026
+ "--token",
1027
+ help="Authentication token"
1028
+ )
1029
+
1030
+ publish_parser.add_argument(
1031
+ "--token-command",
1032
+ help="Command to retrieve token"
1033
+ )
1034
+
1035
+ publish_parser.add_argument(
1036
+ "--credentials-file",
1037
+ help="Path to credentials file"
1038
+ )
1039
+
1040
+ publish_parser.add_argument(
1041
+ "--templates-dir",
1042
+ help="Path to custom templates directory",
1043
+ metavar="DIR"
1044
+ )
1045
+
1046
+ publish_parser.add_argument(
1047
+ "--include-branches",
1048
+ nargs="+",
1049
+ help="Only push these specific branches (overrides strategy defaults)"
1050
+ )
1051
+
1052
+ publish_parser.add_argument(
1053
+ "--exclude-branches",
1054
+ nargs="+",
1055
+ help="Exclude these branches from publishing (in addition to private)"
1056
+ )
1057
+
1058
+ # BOOTSTRAP command
1059
+ bootstrap_parser = subparsers.add_parser(
1060
+ "bootstrap",
1061
+ help="Generate a bootstrap script for RepoKit setup",
1062
+ description="""
1063
+ Generate a self-contained Python script that can bootstrap a complete
1064
+ RepoKit project with a single command.
1065
+
1066
+ The bootstrap script:
1067
+ - Checks for Git installation
1068
+ - Configures Git user if needed
1069
+ - Creates the repository structure
1070
+ - Sets up all branches and worktrees
1071
+ - Can optionally publish to remote
1072
+ - Includes all RepoKit functionality in one file
1073
+
1074
+ This is useful for:
1075
+ - Quick project initialization
1076
+ - CI/CD pipelines
1077
+ - Containerized environments
1078
+ - Sharing project templates
1079
+ """,
1080
+ epilog="""
1081
+ Examples:
1082
+ # Generate basic bootstrap script
1083
+ repokit bootstrap --name myproject
1084
+
1085
+ # Bootstrap with GitHub publishing
1086
+ repokit bootstrap --name webapp --service github --private
1087
+
1088
+ # Custom output location
1089
+ repokit bootstrap --name api --output setup_api.py
1090
+
1091
+ # For organization project
1092
+ repokit bootstrap --name service --service github --organization mycompany
1093
+ """,
1094
+ formatter_class=argparse.RawDescriptionHelpFormatter
1095
+ )
1096
+
1097
+ bootstrap_parser.add_argument(
1098
+ "--name",
1099
+ help="Project name for the bootstrap script"
1100
+ )
1101
+
1102
+ bootstrap_parser.add_argument(
1103
+ "--output", "-o",
1104
+ default="bootstrap_repokit.py",
1105
+ help="Output file path (default: bootstrap_repokit.py)"
1106
+ )
1107
+
1108
+ bootstrap_parser.add_argument(
1109
+ "--service",
1110
+ choices=["github", "gitlab"],
1111
+ help="Include remote publishing in bootstrap"
1112
+ )
1113
+
1114
+ bootstrap_parser.add_argument(
1115
+ "--private",
1116
+ action="store_true",
1117
+ help="Create private repository"
1118
+ )
1119
+
1120
+ bootstrap_parser.add_argument(
1121
+ "--organization",
1122
+ help="Organization/group name"
1123
+ )
1124
+
1125
+ bootstrap_parser.add_argument(
1126
+ "--no-publish",
1127
+ action="store_true",
1128
+ help="Skip publishing step in bootstrap"
1129
+ )
1130
+
1131
+ bootstrap_parser.add_argument(
1132
+ "--git-user-name",
1133
+ help="Git user name for bootstrap"
1134
+ )
1135
+
1136
+ bootstrap_parser.add_argument(
1137
+ "--git-user-email",
1138
+ help="Git user email for bootstrap"
1139
+ )
1140
+
1141
+ # Other simple commands
1142
+ list_parser = subparsers.add_parser(
1143
+ "list",
1144
+ help="List the languages, presets and layers that --language and --with accept",
1145
+ description="Per language: its presets (name = layers; planned ones marked), then each "
1146
+ "facet with its choices and whether one or any may be picked."
1147
+ )
1148
+ list_parser.add_argument("language", nargs="?", help="One language only")
1149
+
1150
+ subparsers.add_parser(
1151
+ "list-templates",
1152
+ help="List all available templates",
1153
+ description="Display all available template files that RepoKit can use."
1154
+ )
1155
+
1156
+ init_config_parser = subparsers.add_parser(
1157
+ "init-config",
1158
+ help="Initialize a .repokit.json configuration file",
1159
+ description="""
1160
+ Create a default .repokit.json configuration file in the current directory.
1161
+
1162
+ This file can be customized to set project-specific defaults for:
1163
+ - Directory structure and profiles
1164
+ - Branch strategies and names
1165
+ - Git user configuration
1166
+ - Remote service preferences
1167
+ - Template directories
1168
+ """
1169
+ )
1170
+
1171
+ store_creds_parser = subparsers.add_parser(
1172
+ "store-credentials",
1173
+ help="Store credentials for GitHub/GitLab",
1174
+ description="""
1175
+ Securely store authentication credentials for GitHub or GitLab.
1176
+
1177
+ Credentials are stored in:
1178
+ - ~/.repokit/credentials.json (default)
1179
+ - Or a custom location specified by --credentials-file
1180
+
1181
+ The stored credentials are used automatically when publishing repositories.
1182
+ """
1183
+ )
1184
+
1185
+ store_creds_parser.add_argument(
1186
+ "--publish-to",
1187
+ choices=["github", "gitlab"],
1188
+ required=True,
1189
+ help="Service to store credentials for"
1190
+ )
1191
+
1192
+ store_creds_parser.add_argument(
1193
+ "--token",
1194
+ help="Authentication token to store"
1195
+ )
1196
+
1197
+ store_creds_parser.add_argument(
1198
+ "--token-command",
1199
+ help="Command to retrieve token"
1200
+ )
1201
+
1202
+ store_creds_parser.add_argument(
1203
+ "--organization",
1204
+ help="Default organization/group"
1205
+ )
1206
+
1207
+ store_creds_parser.add_argument(
1208
+ "--credentials-file",
1209
+ help="Custom credentials file path"
1210
+ )
1211
+
1212
+ # Add guardrails commands
1213
+ guardrails_parser = subparsers.add_parser(
1214
+ "setup-guardrails",
1215
+ help="Set up private content protection guardrails",
1216
+ description="""
1217
+ Set up guardrails that keep private content off public branches.
1218
+
1219
+ The hooks are git-repokit-common's: this installs them from the project's
1220
+ vendored copy (scripts/repokit-common/), writes the settings the private-branch
1221
+ model needs, and configures branch-specific exclusions.
1222
+ """,
1223
+ epilog="""
1224
+ Examples:
1225
+ # Install git-repokit-common's hooks from the vendored copy
1226
+ repokit setup-guardrails
1227
+
1228
+ # No copy yet: vendor git-repokit-common first (makes two commits)
1229
+ repokit setup-guardrails --vendor
1230
+
1231
+ # The previous git-repokit hooks (pre-commit + post-checkout restore)
1232
+ repokit setup-guardrails --legacy --force
1233
+ """
1234
+ )
1235
+
1236
+ guardrails_parser.add_argument(
1237
+ "--force",
1238
+ action="store_true",
1239
+ help="Overwrite existing hooks (--legacy only; install-hooks.sh always rewrites its stubs)"
1240
+ )
1241
+ guardrails_parser.add_argument(
1242
+ "--vendor",
1243
+ action="store_true",
1244
+ help="Add git-repokit-common at scripts/repokit-common/ first if the project has no copy "
1245
+ "(git subtree; needs a clean working tree)"
1246
+ )
1247
+ guardrails_parser.add_argument(
1248
+ "--legacy",
1249
+ action="store_true",
1250
+ help="Install git-repokit's previous hooks instead (its own pre-commit and the post-checkout "
1251
+ "that restores private files from the private branch)"
1252
+ )
1253
+
1254
+ check_guardrails_parser = subparsers.add_parser(
1255
+ "check-guardrails",
1256
+ help="Check guardrails status and validate current state",
1257
+ description="Check if guardrails are properly installed and validate staged changes."
1258
+ )
1259
+
1260
+ safe_merge_parser = subparsers.add_parser(
1261
+ "safe-merge",
1262
+ help="Merge branches with private content protection",
1263
+ description="""
1264
+ Safely merge branches while automatically excluding private content when
1265
+ merging from private to public branches.
1266
+ """,
1267
+ epilog="""
1268
+ Examples:
1269
+ # Preview merge from private to current branch
1270
+ repokit safe-merge private --preview
1271
+
1272
+ # Perform safe merge
1273
+ repokit safe-merge private --no-ff --no-commit
1274
+ """
1275
+ )
1276
+
1277
+ safe_merge_parser.add_argument(
1278
+ "source_branch",
1279
+ help="Branch to merge from"
1280
+ )
1281
+
1282
+ safe_merge_parser.add_argument(
1283
+ "--no-commit",
1284
+ action="store_true",
1285
+ default=True,
1286
+ help="Perform merge without committing (default)"
1287
+ )
1288
+
1289
+ safe_merge_parser.add_argument(
1290
+ "--no-ff",
1291
+ action="store_true",
1292
+ default=True,
1293
+ help="Create merge commit even if fast-forward (default)"
1294
+ )
1295
+
1296
+ safe_merge_parser.add_argument(
1297
+ "--preview",
1298
+ action="store_true",
1299
+ help="Preview what would be merged without doing it"
1300
+ )
1301
+
1302
+ # Add safe-merge-dev command
1303
+ safe_merge_dev_parser = subparsers.add_parser(
1304
+ "safe-merge-dev",
1305
+ help="Merge development branches with history protection",
1306
+ description="""
1307
+ Merge development branches while protecting sensitive commit history.
1308
+
1309
+ This command helps prevent detailed implementation history from leaking
1310
+ to public branches by intelligently squashing or preserving commits based
1311
+ on branch patterns and configurable rules.
1312
+
1313
+ Branch categories and default behaviors:
1314
+ - prototype/*, experiment/*, spike/*: Always squash (development/exploration)
1315
+ - feature/*: Interactive mode (ask whether to squash)
1316
+ - bugfix/*, hotfix/*: Preserve history (important for tracking)
1317
+
1318
+ The command will:
1319
+ - Analyze commits in the branch
1320
+ - Remove sensitive patterns from messages
1321
+ - Generate meaningful squash commit messages
1322
+ - Provide interactive options for feature branches
1323
+ """,
1324
+ epilog="""
1325
+ Examples:
1326
+ # Merge a prototype branch (auto-squashes)
1327
+ repokit safe-merge-dev prototype/new-feature
1328
+
1329
+ # Merge a feature branch (interactive)
1330
+ repokit safe-merge-dev feature/oauth-integration
1331
+
1332
+ # Force squash with custom message
1333
+ repokit safe-merge-dev feature/api --squash --message "Add REST API"
1334
+
1335
+ # Preserve full history
1336
+ repokit safe-merge-dev feature/refactor --no-squash
1337
+
1338
+ # Preview without merging
1339
+ repokit safe-merge-dev experiment/ml-model --preview
1340
+ """,
1341
+ formatter_class=argparse.RawDescriptionHelpFormatter
1342
+ )
1343
+
1344
+ safe_merge_dev_parser.add_argument(
1345
+ "branch",
1346
+ help="Development branch to merge"
1347
+ )
1348
+
1349
+ safe_merge_dev_parser.add_argument(
1350
+ "--target",
1351
+ help="Target branch (default: current branch)"
1352
+ )
1353
+
1354
+ safe_merge_dev_parser.add_argument(
1355
+ "--squash",
1356
+ action="store_true",
1357
+ help="Force squash merge regardless of branch rules"
1358
+ )
1359
+
1360
+ safe_merge_dev_parser.add_argument(
1361
+ "--no-squash",
1362
+ action="store_true",
1363
+ help="Preserve all commits regardless of branch rules"
1364
+ )
1365
+
1366
+ safe_merge_dev_parser.add_argument(
1367
+ "--message", "-m",
1368
+ help="Custom commit message for squashed merge"
1369
+ )
1370
+
1371
+ safe_merge_dev_parser.add_argument(
1372
+ "--preserve-last",
1373
+ type=int,
1374
+ help="Preserve the last N commits (not yet implemented)"
1375
+ )
1376
+
1377
+ safe_merge_dev_parser.add_argument(
1378
+ "--preview",
1379
+ action="store_true",
1380
+ help="Preview merge without executing"
1381
+ )
1382
+
1383
+ # Add clean-history command
1384
+ clean_history_parser = subparsers.add_parser(
1385
+ "clean-history",
1386
+ help="Clean sensitive data from repository history",
1387
+ description="""
1388
+ Clean sensitive data from repository history using git filter-repo.
1389
+
1390
+ This command provides safe, user-friendly tools to remove private files,
1391
+ secrets, and other sensitive data from git history. It includes pre-built
1392
+ recipes for common scenarios and comprehensive safety features.
1393
+
1394
+ WARNING: This rewrites repository history and requires force-push!
1395
+
1396
+ Pre-built recipes:
1397
+ - pre-open-source: Remove common private files (private/, CLAUDE.md, etc.)
1398
+ - windows-safe: Fix Windows reserved names (nul, con, aux, etc.)
1399
+ - remove-secrets: Remove API keys and passwords (coming soon)
1400
+ - cutoff-date: Remove old private data before a date (coming soon)
1401
+
1402
+ Safety features:
1403
+ - Automatic backup before cleaning
1404
+ - Dry-run preview mode
1405
+ - Confirmation prompts
1406
+ - Clear warnings and instructions
1407
+ """,
1408
+ epilog="""
1409
+ Examples:
1410
+ # Interactive mode - analyze and suggest cleaning
1411
+ repokit clean-history
1412
+
1413
+ # Remove common private files before open-sourcing
1414
+ repokit clean-history --recipe pre-open-source
1415
+
1416
+ # Preview what would be removed
1417
+ repokit clean-history --recipe pre-open-source --dry-run
1418
+
1419
+ # Fix Windows compatibility issues
1420
+ repokit clean-history --recipe windows-safe
1421
+
1422
+ # Remove specific paths
1423
+ repokit clean-history --remove-paths private/ logs/ secrets/
1424
+
1425
+ # Skip backup (not recommended)
1426
+ repokit clean-history --recipe pre-open-source --no-backup
1427
+ """,
1428
+ formatter_class=argparse.RawDescriptionHelpFormatter
1429
+ )
1430
+
1431
+ clean_history_parser.add_argument(
1432
+ "--recipe",
1433
+ choices=["pre-open-source", "windows-safe", "remove-secrets", "cutoff-date"],
1434
+ help="Use a pre-built cleaning recipe"
1435
+ )
1436
+
1437
+ clean_history_parser.add_argument(
1438
+ "--remove-paths",
1439
+ nargs="+",
1440
+ help="Specific paths to remove from history"
1441
+ )
1442
+
1443
+ clean_history_parser.add_argument(
1444
+ "--cutoff-sha",
1445
+ help="Remove private data only before this commit"
1446
+ )
1447
+
1448
+ clean_history_parser.add_argument(
1449
+ "--cutoff-date",
1450
+ help="Remove private data only before this date (YYYY-MM-DD)"
1451
+ )
1452
+
1453
+ clean_history_parser.add_argument(
1454
+ "--dry-run",
1455
+ action="store_true",
1456
+ help="Preview changes without modifying repository"
1457
+ )
1458
+
1459
+ clean_history_parser.add_argument(
1460
+ "--no-backup",
1461
+ action="store_true",
1462
+ help="Skip creating backup (not recommended)"
1463
+ )
1464
+
1465
+ clean_history_parser.add_argument(
1466
+ "--backup-location",
1467
+ help="Custom backup location (default: <repo>_backup_<timestamp>)"
1468
+ )
1469
+
1470
+ clean_history_parser.add_argument(
1471
+ "--force",
1472
+ action="store_true",
1473
+ help="Skip confirmation prompts"
1474
+ )
1475
+
1476
+ # Add manage-gitkeep command
1477
+ gitkeep_parser = subparsers.add_parser(
1478
+ "manage-gitkeep",
1479
+ help="Manage .gitkeep files in directories",
1480
+ description="""
1481
+ Manage .gitkeep files based on directory contents.
1482
+
1483
+ This command automatically:
1484
+ - Adds .gitkeep files to empty directories to ensure they are tracked by Git
1485
+ - Removes .gitkeep files from directories that contain other files
1486
+
1487
+ This is useful for maintaining clean Git history while ensuring empty directories
1488
+ are properly tracked.
1489
+ """,
1490
+ epilog="""
1491
+ Examples:
1492
+ # Manage .gitkeep files in current directory
1493
+ repokit manage-gitkeep
1494
+
1495
+ # Manage .gitkeep files in specific directory
1496
+ repokit manage-gitkeep --directory ./src
1497
+
1498
+ # Process only top-level directories (no recursion)
1499
+ repokit manage-gitkeep --no-recursive
1500
+
1501
+ # Preview changes without applying them
1502
+ repokit manage-gitkeep --dry-run
1503
+ """,
1504
+ formatter_class=argparse.RawDescriptionHelpFormatter
1505
+ )
1506
+
1507
+ gitkeep_parser.add_argument(
1508
+ "--directory",
1509
+ "-d",
1510
+ default=".",
1511
+ help="Directory to process (default: current directory)"
1512
+ )
1513
+
1514
+ gitkeep_parser.add_argument(
1515
+ "--recursive",
1516
+ action="store_true",
1517
+ default=True,
1518
+ help="Process subdirectories recursively (default: True)"
1519
+ )
1520
+
1521
+ gitkeep_parser.add_argument(
1522
+ "--no-recursive",
1523
+ action="store_true",
1524
+ help="Do not process subdirectories recursively"
1525
+ )
1526
+
1527
+ gitkeep_parser.add_argument(
1528
+ "--dry-run",
1529
+ action="store_true",
1530
+ help="Show what would be changed without actually making changes"
1531
+ )
1532
+
1533
+ # Add analyze-history command
1534
+ analyze_history_parser = subparsers.add_parser(
1535
+ "analyze-history",
1536
+ help="Analyze repository history for sensitive data",
1537
+ description="""
1538
+ Analyze repository history to identify sensitive data that should be cleaned.
1539
+
1540
+ This command scans your repository history and reports:
1541
+ - Private directories (private/, logs/, etc.)
1542
+ - Windows compatibility issues (nul, con, aux files)
1543
+ - Large files that might not belong
1544
+ - Potential secrets (coming soon)
1545
+ - Branch structure and commit count
1546
+
1547
+ Use this before clean-history to understand what needs cleaning.
1548
+ """,
1549
+ epilog="""
1550
+ Examples:
1551
+ # Analyze current repository
1552
+ repokit analyze-history
1553
+
1554
+ # Analyze with detailed output
1555
+ repokit analyze-history -v
1556
+ """,
1557
+ formatter_class=argparse.RawDescriptionHelpFormatter
1558
+ )
1559
+
1560
+ return parser.parse_args()
1561
+
1562
+
1563
+ def setup_logging(verbosity: int, quiet: bool = False) -> None:
1564
+ """
1565
+ Set up logging based on verbosity level.
1566
+
1567
+ Args:
1568
+ verbosity: Verbosity level (0-3+)
1569
+ quiet: Whether to suppress all output except errors
1570
+ """
1571
+ if quiet:
1572
+ log_level = logging.ERROR
1573
+ else:
1574
+ # Map verbosity levels to logging levels
1575
+ log_levels = {
1576
+ 0: logging.WARNING, # Default - only warnings and errors
1577
+ 1: logging.INFO, # -v - basic operations
1578
+ 2: logging.DEBUG, # -vv - file operations and detailed flow
1579
+ 3: logging.DEBUG, # -vvv - pattern matching and cleanup details
1580
+ }
1581
+ # Cap at maximum level
1582
+ capped_verbosity = min(verbosity, 3)
1583
+ log_level = log_levels[capped_verbosity]
1584
+
1585
+ # Configure basic logging
1586
+ logging.basicConfig(
1587
+ level=log_level,
1588
+ format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
1589
+ datefmt="%Y-%m-%d %H:%M:%S",
1590
+ )
1591
+
1592
+ # For highest verbosity, adjust formatter to include more details
1593
+ if verbosity >= 3:
1594
+ for handler in logging.root.handlers:
1595
+ handler.setFormatter(
1596
+ logging.Formatter(
1597
+ "%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s",
1598
+ datefmt="%Y-%m-%d %H:%M:%S",
1599
+ )
1600
+ )
1601
+
1602
+ # Get logger for repokit
1603
+ logger = logging.getLogger("repokit")
1604
+
1605
+ # Set up verbosity-specific loggers for detailed debugging
1606
+ if verbosity >= 2:
1607
+ # Enable detailed file operation logging
1608
+ logging.getLogger("repokit.utils").setLevel(logging.DEBUG)
1609
+ logging.getLogger("repokit.repo_manager").setLevel(logging.DEBUG)
1610
+
1611
+ if verbosity >= 3:
1612
+ # Enable pattern matching and cleanup debugging
1613
+ logging.getLogger("repokit.cleanup").setLevel(logging.DEBUG)
1614
+ logging.getLogger("repokit.patterns").setLevel(logging.DEBUG)
1615
+
1616
+ # Log verbosity level for debugging
1617
+ if verbosity >= 1:
1618
+ logger.info(f"Verbosity level: {verbosity} (-{'v' * verbosity})")
1619
+ if verbosity >= 2:
1620
+ logger.info("File operations debugging enabled")
1621
+ if verbosity >= 3:
1622
+ logger.info("Pattern matching and cleanup debugging enabled")
1623
+
1624
+
1625
+ def _known_language_names():
1626
+ """The languages the layers' manifest names (for help text); generic alone
1627
+ when the manifest cannot be read."""
1628
+ try:
1629
+ from . import layers
1630
+ root = layers.layers_root()
1631
+ return layers.known_languages(layers.load_manifest(root)) if root else [layers.GENERIC]
1632
+ except Exception:
1633
+ return ["generic"]
1634
+
1635
+
1636
+ def args_to_config(args: argparse.Namespace) -> Dict[str, Any]:
1637
+ """
1638
+ Convert command-line arguments to configuration dictionary.
1639
+
1640
+ Args:
1641
+ args: Parsed command-line arguments
1642
+
1643
+ Returns:
1644
+ Configuration dictionary
1645
+ """
1646
+ # --language is a list (repeated, or comma-separated); `language` keeps the
1647
+ # first for everything that reads one value, `languages` carries them all.
1648
+ languages = []
1649
+ if getattr(args, "language", None):
1650
+ from .layers import parse_languages
1651
+ languages = parse_languages(args.language)
1652
+
1653
+ # Base configuration from CLI arguments
1654
+ cli_config = {
1655
+ "name": getattr(args, "name", None),
1656
+ "language": languages[0] if languages else None,
1657
+ "description": getattr(args, "description", None),
1658
+ "private_branch": getattr(args, "private_branch", None),
1659
+ "user": (
1660
+ {"name": args.user_name, "email": args.user_email}
1661
+ if getattr(args, "user_name", None) or getattr(args, "user_email", None)
1662
+ else None
1663
+ ),
1664
+ }
1665
+
1666
+ if languages:
1667
+ cli_config["languages"] = languages
1668
+ if getattr(args, "with_layers", None):
1669
+ from .layers import parse_languages
1670
+ cli_config["with"] = parse_languages(args.with_layers)
1671
+ if getattr(args, "license", None):
1672
+ cli_config["license"] = args.license
1673
+ cli_config["license_cli"] = True # the flag beats license_private; a config value does not
1674
+ if getattr(args, "organization", None):
1675
+ cli_config["organization"] = args.organization # the owner in the extras' URLs
1676
+ if getattr(args, "github_setup", None):
1677
+ from .layers import parse_languages as _split_steps
1678
+ cli_config["github_setup"] = _split_steps(args.github_setup)
1679
+ if getattr(args, "topics", None):
1680
+ cli_config["topics"] = [t.strip() for t in args.topics.split(",") if t.strip()]
1681
+ if getattr(args, "badges", None):
1682
+ cli_config["badges"] = args.badges.strip()
1683
+ if getattr(args, "no_extras", False):
1684
+ cli_config["extras"] = []
1685
+ elif getattr(args, "extras", None):
1686
+ from .layers import parse_languages as _split
1687
+ cli_config["extras"] = _split(args.extras)
1688
+ if getattr(args, "private_repo", False):
1689
+ cli_config["private_repo"] = True
1690
+
1691
+ # Handle directory profile configuration
1692
+ if hasattr(args, "dir_profile") and args.dir_profile:
1693
+ cli_config["directory_profile"] = args.dir_profile
1694
+
1695
+ if hasattr(args, "dir_groups") and args.dir_groups:
1696
+ cli_config["directory_groups"] = [
1697
+ group.strip() for group in args.dir_groups.split(",")
1698
+ ]
1699
+
1700
+ if hasattr(args, "private_set") and args.private_set:
1701
+ cli_config["private_set"] = args.private_set
1702
+
1703
+ # Handle list-type arguments
1704
+ if hasattr(args, "branches") and args.branches:
1705
+ cli_config["branches"] = [branch.strip() for branch in args.branches.split(",")]
1706
+
1707
+ if hasattr(args, "worktrees") and args.worktrees:
1708
+ cli_config["worktrees"] = [
1709
+ worktree.strip() for worktree in args.worktrees.split(",")
1710
+ ]
1711
+
1712
+ if hasattr(args, "directories") and args.directories:
1713
+ cli_config["directories"] = [
1714
+ directory.strip() for directory in args.directories.split(",")
1715
+ ]
1716
+
1717
+ if hasattr(args, "private_dirs") and args.private_dirs:
1718
+ cli_config["private_dirs"] = [
1719
+ dir.strip() for dir in args.private_dirs.split(",")
1720
+ ]
1721
+
1722
+ if hasattr(args, "sensitive_files") and args.sensitive_files:
1723
+ cli_config["sensitive_files"] = [
1724
+ file.strip() for file in args.sensitive_files.split(",")
1725
+ ]
1726
+
1727
+ if hasattr(args, "sensitive_patterns") and args.sensitive_patterns:
1728
+ cli_config["sensitive_patterns"] = [
1729
+ pattern.strip() for pattern in args.sensitive_patterns.split(",")
1730
+ ]
1731
+
1732
+ # Handle branch strategy
1733
+ if hasattr(args, "branch_strategy") and args.branch_strategy:
1734
+ cli_config["branch_strategy"] = args.branch_strategy
1735
+ elif getattr(args, "command", None) in ("adopt", "migrate"):
1736
+ # Adoption builds the private-branch model only (see defaults)
1737
+ cli_config["branch_strategy"] = DEFAULT_ADOPT_STRATEGY
1738
+
1739
+ if getattr(args, "private_mode", None):
1740
+ cli_config["private_mode"] = args.private_mode
1741
+
1742
+ if getattr(args, "with_common", None) is not None:
1743
+ cli_config["with_common"] = args.with_common
1744
+
1745
+ # Handle AI integration
1746
+ if hasattr(args, "ai") and args.ai and args.ai != "none":
1747
+ cli_config["ai_integration"] = args.ai
1748
+
1749
+ # Handle backup configuration
1750
+ if hasattr(args, "backup") and args.backup:
1751
+ cli_config["backup"] = True
1752
+
1753
+ if hasattr(args, "backup_location") and args.backup_location:
1754
+ cli_config["backup_location"] = args.backup_location
1755
+
1756
+ # Handle migration strategy
1757
+ if hasattr(args, "migration_strategy") and args.migration_strategy:
1758
+ cli_config["migration_strategy"] = args.migration_strategy
1759
+
1760
+ # Handle remote publishing configuration
1761
+ if hasattr(args, "publish_to") and args.publish_to:
1762
+ cli_config["publish_to"] = args.publish_to
1763
+
1764
+ if hasattr(args, "repo_name") and args.repo_name:
1765
+ cli_config["repo_name"] = args.repo_name
1766
+
1767
+ if hasattr(args, "private_repo") and args.private_repo:
1768
+ cli_config["private_repo"] = True
1769
+
1770
+ # Handle branch directory mappings
1771
+ branch_directories = {}
1772
+
1773
+ if hasattr(args, "branch_dir_main") and args.branch_dir_main:
1774
+ branch_directories["main"] = args.branch_dir_main
1775
+
1776
+ if hasattr(args, "branch_dir_dev") and args.branch_dir_dev:
1777
+ branch_directories["dev"] = args.branch_dir_dev
1778
+
1779
+ if branch_directories:
1780
+ cli_config["branch_config"] = {"branch_directories": branch_directories}
1781
+
1782
+ # default branch from CLI
1783
+ if hasattr(args, "default_branch") and args.default_branch:
1784
+ cli_config["default_branch"] = args.default_branch
1785
+
1786
+ # Remove None values from config
1787
+ return {k: v for k, v in cli_config.items() if v is not None}
1788
+
1789
+
1790
+ def main() -> int:
1791
+ """
1792
+ Main entry point.
1793
+
1794
+ Returns:
1795
+ Exit code (0 for success, non-zero for failure)
1796
+ """
1797
+ # Status output uses symbols a non-UTF-8 console (Windows' default code
1798
+ # page, output piped to a tool) cannot encode; replace them rather than
1799
+ # crash after the work is done.
1800
+ for stream in (sys.stdout, sys.stderr):
1801
+ try:
1802
+ stream.reconfigure(errors="replace")
1803
+ except (AttributeError, ValueError):
1804
+ pass
1805
+
1806
+ args = parse_arguments()
1807
+
1808
+ # Set up logging based on verbosity
1809
+ setup_logging(args.verbose, args.quiet)
1810
+ logger = logging.getLogger("repokit")
1811
+
1812
+ # Initialize configuration manager
1813
+ config_manager = ConfigManager(verbose=args.verbose)
1814
+
1815
+ # Load config file if provided
1816
+ if args.config and os.path.exists(args.config):
1817
+ config_manager.load_config_file(args.config)
1818
+
1819
+ # Load branch configuration file if provided
1820
+ if hasattr(args, 'branch_config') and args.branch_config and os.path.exists(args.branch_config):
1821
+ config_manager.load_branch_config_file(args.branch_config)
1822
+
1823
+ # Add CLI arguments to configuration
1824
+ cli_config = args_to_config(args)
1825
+ config_manager.set_cli_config(cli_config)
1826
+
1827
+ # Get the final, merged configuration
1828
+ config = config_manager.get_config()
1829
+
1830
+ # Add remote integration configuration
1831
+ if hasattr(args, 'publish_to') and args.publish_to:
1832
+ service_config = config.get(args.publish_to, {})
1833
+ # Ensure service_config is a dict, not a boolean
1834
+ if not isinstance(service_config, dict):
1835
+ service_config = {}
1836
+ if hasattr(args, 'organization') and args.organization:
1837
+ if args.publish_to == "github":
1838
+ service_config["organization"] = args.organization
1839
+ else:
1840
+ service_config["group"] = args.organization
1841
+ config[args.publish_to] = service_config
1842
+
1843
+ # Validate configuration
1844
+ errors = config_manager.validate_config()
1845
+ if errors:
1846
+ for error in errors:
1847
+ logger.error(f"Configuration error: {error}")
1848
+ return 1
1849
+
1850
+ # Save configuration if requested
1851
+ if hasattr(args, 'save_config') and args.save_config:
1852
+ config_manager.save_config(args.save_config)
1853
+
1854
+ # Handle commands
1855
+ if args.command == "list":
1856
+ from . import layers as _layers
1857
+ from .template_engine import TemplateEngine as _TemplateEngine
1858
+ _root = _layers.layers_root(_TemplateEngine(templates_dir=getattr(args, "templates_dir", None)))
1859
+ if _root is None:
1860
+ logger.error("no code-type layers found with the templates")
1861
+ return 1
1862
+ try:
1863
+ for line in _layers.catalogue_lines(_layers.load_manifest(_root), args.language):
1864
+ print(line)
1865
+ except _layers.LayerError as e:
1866
+ logger.error(str(e))
1867
+ return 1
1868
+ return 0
1869
+
1870
+ elif args.command == "list-templates":
1871
+ template_engine = TemplateEngine(
1872
+ templates_dir=args.templates_dir, verbose=args.verbose
1873
+ )
1874
+ templates = template_engine.list_templates()
1875
+ print("Available templates:")
1876
+ for template in sorted(templates):
1877
+ print(f" {template}")
1878
+ return 0
1879
+
1880
+ elif args.command == "init-config":
1881
+ # Create a default configuration file in the current directory
1882
+ config_path = os.path.join(os.getcwd(), ".repokit.json")
1883
+ if os.path.exists(config_path):
1884
+ logger.error(f"Configuration file already exists at {config_path}")
1885
+ return 1
1886
+
1887
+ config_manager.save_config(config_path)
1888
+ logger.info(f"Created default configuration at {config_path}")
1889
+ print(f"Created default configuration at {config_path}")
1890
+ print("Edit this file to customize your repository settings.")
1891
+ return 0
1892
+
1893
+ elif args.command == "store-credentials":
1894
+ from .remote_integration import RemoteIntegration
1895
+ from .auth_integration import AuthenticationHandler
1896
+
1897
+ if not args.publish_to:
1898
+ logger.error(
1899
+ "Please specify a service with --publish-to (github or gitlab)"
1900
+ )
1901
+ return 1
1902
+
1903
+ if not args.token and not args.token_command:
1904
+ logger.error("Please provide either --token or --token-command")
1905
+ return 1
1906
+
1907
+ auth_handler = AuthenticationHandler(verbose=args.verbose)
1908
+
1909
+ # If token_command is provided, use it to get the token
1910
+ if args.token_command:
1911
+ token = auth_handler.get_token(args.publish_to, args.token_command)
1912
+ if not token:
1913
+ logger.error(
1914
+ f"Failed to retrieve token using command: {args.token_command}"
1915
+ )
1916
+ return 1
1917
+ else:
1918
+ token = args.token
1919
+
1920
+ # Store the credentials
1921
+ success = auth_handler.store_credentials(
1922
+ args.publish_to, token, args.organization, args.credentials_file
1923
+ )
1924
+
1925
+ if success:
1926
+ logger.info(f"Successfully stored credentials for {args.publish_to}")
1927
+ print(f"Credentials stored successfully for {args.publish_to}")
1928
+ return 0
1929
+ else:
1930
+ logger.error(f"Failed to store credentials for {args.publish_to}")
1931
+ return 1
1932
+
1933
+ elif args.command == "setup-guardrails":
1934
+ from .guardrails import setup_guardrails_command
1935
+ return setup_guardrails_command(args)
1936
+
1937
+ elif args.command == "check-guardrails":
1938
+ from .guardrails import check_guardrails_command
1939
+ return check_guardrails_command(args)
1940
+
1941
+ elif args.command == "safe-merge":
1942
+ from .guardrails import GuardrailsManager
1943
+
1944
+ manager = GuardrailsManager()
1945
+
1946
+ if args.preview:
1947
+ success = manager.safe_merge(args.source_branch, preview=True)
1948
+ else:
1949
+ success = manager.safe_merge(
1950
+ args.source_branch,
1951
+ no_commit=args.no_commit,
1952
+ no_ff=args.no_ff
1953
+ )
1954
+
1955
+ return 0 if success else 1
1956
+
1957
+ elif args.command == "safe-merge-dev":
1958
+ from .history_protection import HistoryProtectionManager
1959
+
1960
+ # Get configuration for history protection
1961
+ history_config = config.get('history_protection', {})
1962
+
1963
+ # Initialize manager
1964
+ manager = HistoryProtectionManager(
1965
+ repo_path=os.getcwd(),
1966
+ config=config,
1967
+ verbose=args.verbose
1968
+ )
1969
+
1970
+ # Determine squash behavior
1971
+ squash = None
1972
+ if args.squash:
1973
+ squash = True
1974
+ elif args.no_squash:
1975
+ squash = False
1976
+
1977
+ # Perform the merge
1978
+ success = manager.safe_merge_dev(
1979
+ branch=args.branch,
1980
+ target_branch=args.target,
1981
+ squash=squash,
1982
+ message=args.message,
1983
+ preserve_last=args.preserve_last,
1984
+ dry_run=args.preview
1985
+ )
1986
+
1987
+ return 0 if success else 1
1988
+
1989
+ elif args.command == "clean-history":
1990
+ from .history_cleaner import HistoryCleaner, CleaningConfig, CleaningRecipe, GitFilterRepoNotFound
1991
+
1992
+ try:
1993
+ # Initialize cleaner
1994
+ cleaner = HistoryCleaner(repo_path=os.getcwd(), verbose=args.verbose)
1995
+
1996
+ # Interactive mode if no recipe specified
1997
+ if not args.recipe and not args.remove_paths:
1998
+ # Analyze repository
1999
+ analysis = cleaner.analyze_repository()
2000
+
2001
+ print("\n=== Repository Analysis ===")
2002
+ print(f"Total commits: {analysis['total_commits']}")
2003
+ print(f"Branches: {len(analysis['branches'])}")
2004
+
2005
+ if analysis['private_paths']:
2006
+ print(f"\nFound {len(analysis['private_paths'])} private files:")
2007
+ for path in analysis['private_paths'][:10]:
2008
+ print(f" - {path}")
2009
+ if len(analysis['private_paths']) > 10:
2010
+ print(f" ... and {len(analysis['private_paths']) - 10} more")
2011
+
2012
+ if analysis['windows_issues']:
2013
+ print(f"\nFound {len(analysis['windows_issues'])} Windows compatibility issues:")
2014
+ for path in analysis['windows_issues'][:5]:
2015
+ print(f" - {path}")
2016
+ if len(analysis['windows_issues']) > 5:
2017
+ print(f" ... and {len(analysis['windows_issues']) - 5} more")
2018
+
2019
+ # Suggest recipe
2020
+ if analysis['private_paths']:
2021
+ print("\n💡 Suggestion: Use --recipe pre-open-source to remove private files")
2022
+ if analysis['windows_issues']:
2023
+ print("💡 Suggestion: Use --recipe windows-safe to fix Windows issues")
2024
+
2025
+ return 0
2026
+
2027
+ # Build configuration
2028
+ if args.recipe:
2029
+ recipe = CleaningRecipe(args.recipe)
2030
+ config = cleaner.get_recipe_config(recipe)
2031
+ else:
2032
+ config = CleaningConfig(recipe=CleaningRecipe.CUSTOM)
2033
+
2034
+ # Override with command-line options
2035
+ if args.remove_paths:
2036
+ config.paths_to_remove = args.remove_paths
2037
+ if args.cutoff_sha:
2038
+ config.cutoff_sha = args.cutoff_sha
2039
+ if args.cutoff_date:
2040
+ config.cutoff_date = args.cutoff_date
2041
+
2042
+ config.dry_run = args.dry_run
2043
+ config.force = args.force
2044
+ if args.no_backup:
2045
+ config.backup_location = 'skip'
2046
+ elif args.backup_location:
2047
+ config.backup_location = args.backup_location
2048
+
2049
+ # Preview if requested
2050
+ if args.dry_run:
2051
+ preview = cleaner.preview_cleaning(config)
2052
+ print("\n=== Cleaning Preview ===")
2053
+ print(f"Recipe: {preview['recipe']}")
2054
+ if preview['paths_to_remove']:
2055
+ print(f"Paths to remove: {', '.join(preview['paths_to_remove'])}")
2056
+ if preview['estimated_impact']:
2057
+ print(f"Estimated impact: {preview['estimated_impact']}")
2058
+ if preview['warnings']:
2059
+ print("\nWarnings:")
2060
+ for warning in preview['warnings']:
2061
+ print(f" ⚠️ {warning}")
2062
+
2063
+ # Execute cleaning
2064
+ success = cleaner.clean_history(config)
2065
+ return 0 if success else 1
2066
+
2067
+ except GitFilterRepoNotFound as e:
2068
+ logger.error(str(e))
2069
+ print("\nTo install git filter-repo:")
2070
+ print(" pip install git-filter-repo")
2071
+ print("\nOr see: https://github.com/newren/git-filter-repo")
2072
+ return 1
2073
+ except Exception as e:
2074
+ logger.error(f"Error cleaning history: {str(e)}")
2075
+ if args.verbose >= 2:
2076
+ import traceback
2077
+ traceback.print_exc()
2078
+ return 1
2079
+
2080
+ elif args.command == "manage-gitkeep":
2081
+ from .directory_profiles import DirectoryProfileManager
2082
+
2083
+ try:
2084
+ # Resolve directory path
2085
+ directory = os.path.abspath(args.directory)
2086
+ if not os.path.exists(directory):
2087
+ logger.error(f"Directory does not exist: {directory}")
2088
+ return 1
2089
+
2090
+ if not os.path.isdir(directory):
2091
+ logger.error(f"Path is not a directory: {directory}")
2092
+ return 1
2093
+
2094
+ # Determine recursiveness (--no-recursive overrides default)
2095
+ recursive = args.recursive and not args.no_recursive
2096
+
2097
+ # Initialize directory profile manager
2098
+ profile_manager = DirectoryProfileManager(verbose=args.verbose)
2099
+
2100
+ if args.dry_run:
2101
+ logger.info(f"DRY RUN: Analyzing .gitkeep files in {directory} (recursive: {recursive})")
2102
+
2103
+ # Create a temporary function to simulate the changes
2104
+ def simulate_manage_gitkeep(base_path: str, recursive: bool = True):
2105
+ result = {"added": [], "removed": []}
2106
+
2107
+ def process_directory(dir_path: str, relative_path: str = ""):
2108
+ if not os.path.isdir(dir_path):
2109
+ return
2110
+
2111
+ gitkeep_path = os.path.join(dir_path, ".gitkeep")
2112
+ gitkeep_exists = os.path.exists(gitkeep_path)
2113
+
2114
+ try:
2115
+ items = [item for item in os.listdir(dir_path) if item != ".gitkeep"]
2116
+ except PermissionError:
2117
+ return
2118
+
2119
+ non_hidden_items = [item for item in items if not item.startswith(".")]
2120
+ is_empty = len(non_hidden_items) == 0
2121
+
2122
+ if is_empty and not gitkeep_exists:
2123
+ result["added"].append(os.path.join(relative_path, ".gitkeep") if relative_path else ".gitkeep")
2124
+ elif not is_empty and gitkeep_exists:
2125
+ result["removed"].append(os.path.join(relative_path, ".gitkeep") if relative_path else ".gitkeep")
2126
+
2127
+ if recursive:
2128
+ for item in items:
2129
+ item_path = os.path.join(dir_path, item)
2130
+ if os.path.isdir(item_path) and not item.startswith("."):
2131
+ item_relative = os.path.join(relative_path, item) if relative_path else item
2132
+ process_directory(item_path, item_relative)
2133
+
2134
+ process_directory(base_path)
2135
+ return result
2136
+
2137
+ result = simulate_manage_gitkeep(directory, recursive)
2138
+ else:
2139
+ # Actually manage the .gitkeep files
2140
+ logger.info(f"Managing .gitkeep files in {directory} (recursive: {recursive})")
2141
+ result = profile_manager.manage_gitkeep_files(directory, recursive)
2142
+
2143
+ # Report results
2144
+ if result["added"]:
2145
+ if args.dry_run:
2146
+ print(f"\nWould ADD .gitkeep files to {len(result['added'])} empty directories:")
2147
+ else:
2148
+ print(f"\nADDED .gitkeep files to {len(result['added'])} empty directories:")
2149
+ for file_path in result["added"]:
2150
+ print(f" + {file_path}")
2151
+
2152
+ if result["removed"]:
2153
+ if args.dry_run:
2154
+ print(f"\nWould REMOVE .gitkeep files from {len(result['removed'])} non-empty directories:")
2155
+ else:
2156
+ print(f"\nREMOVED .gitkeep files from {len(result['removed'])} non-empty directories:")
2157
+ for file_path in result["removed"]:
2158
+ print(f" - {file_path}")
2159
+
2160
+ if not result["added"] and not result["removed"]:
2161
+ print("\nNo .gitkeep file changes needed.")
2162
+ else:
2163
+ total_changes = len(result["added"]) + len(result["removed"])
2164
+ if args.dry_run:
2165
+ print(f"\nDry run complete. {total_changes} changes would be made.")
2166
+ else:
2167
+ print(f"\nManagement complete. {total_changes} changes made.")
2168
+
2169
+ return 0
2170
+
2171
+ except Exception as e:
2172
+ logger.error(f"Error managing .gitkeep files: {str(e)}")
2173
+ if args.verbose >= 2:
2174
+ import traceback
2175
+ traceback.print_exc()
2176
+ return 1
2177
+
2178
+ elif args.command == "analyze-history":
2179
+ from .history_cleaner import HistoryCleaner, GitFilterRepoNotFound
2180
+
2181
+ try:
2182
+ # Initialize cleaner
2183
+ cleaner = HistoryCleaner(repo_path=os.getcwd(), verbose=args.verbose)
2184
+
2185
+ # Analyze repository
2186
+ analysis = cleaner.analyze_repository()
2187
+
2188
+ print("\n=== Repository History Analysis ===")
2189
+ print(f"Repository: {os.getcwd()}")
2190
+ print(f"Total commits: {analysis['total_commits']}")
2191
+ print(f"Branches: {', '.join(analysis['branches'][:5])}")
2192
+ if len(analysis['branches']) > 5:
2193
+ print(f" ... and {len(analysis['branches']) - 5} more")
2194
+
2195
+ # Private paths
2196
+ if analysis['private_paths']:
2197
+ print(f"\n🔒 Private/Sensitive Paths ({len(analysis['private_paths'])} found):")
2198
+ by_type = {}
2199
+ for path in analysis['private_paths']:
2200
+ if path.startswith('private/'):
2201
+ by_type.setdefault('private/', []).append(path)
2202
+ elif path.startswith('logs/'):
2203
+ by_type.setdefault('logs/', []).append(path)
2204
+ elif path == 'CLAUDE.md':
2205
+ by_type.setdefault('AI files', []).append(path)
2206
+ else:
2207
+ by_type.setdefault('other', []).append(path)
2208
+
2209
+ for type_name, paths in by_type.items():
2210
+ print(f"\n {type_name}: {len(paths)} files")
2211
+ for path in paths[:3]:
2212
+ print(f" - {path}")
2213
+ if len(paths) > 3:
2214
+ print(f" ... and {len(paths) - 3} more")
2215
+
2216
+ # Windows issues
2217
+ if analysis['windows_issues']:
2218
+ print(f"\n⚠️ Windows Compatibility Issues ({len(analysis['windows_issues'])} found):")
2219
+ for path in analysis['windows_issues'][:5]:
2220
+ print(f" - {path}")
2221
+ if len(analysis['windows_issues']) > 5:
2222
+ print(f" ... and {len(analysis['windows_issues']) - 5} more")
2223
+
2224
+ # Recommendations
2225
+ print("\n📋 Recommendations:")
2226
+ if analysis['private_paths']:
2227
+ print(" 1. Remove private files with: repokit clean-history --recipe pre-open-source")
2228
+ if analysis['windows_issues']:
2229
+ print(" 2. Fix Windows issues with: repokit clean-history --recipe windows-safe")
2230
+ if not analysis['private_paths'] and not analysis['windows_issues']:
2231
+ print(" ✅ No sensitive data detected!")
2232
+
2233
+ print("\n💡 Tip: Use --dry-run to preview changes before cleaning")
2234
+
2235
+ return 0
2236
+
2237
+ except GitFilterRepoNotFound as e:
2238
+ logger.error(str(e))
2239
+ print("\nTo install git filter-repo:")
2240
+ print(" pip install git-filter-repo")
2241
+ return 1
2242
+ except Exception as e:
2243
+ logger.error(f"Error analyzing history: {str(e)}")
2244
+ if args.verbose >= 2:
2245
+ import traceback
2246
+ traceback.print_exc()
2247
+ return 1
2248
+
2249
+ elif args.command == "bootstrap":
2250
+ bootstrap_script = os.path.join(
2251
+ os.path.dirname(os.path.dirname(__file__)), "scripts", "bootstrap.py"
2252
+ )
2253
+
2254
+ if not os.path.exists(bootstrap_script):
2255
+ logger.error(f"Bootstrap script not found at {bootstrap_script}")
2256
+ return 1
2257
+
2258
+ # Build arguments for bootstrap script
2259
+ bootstrap_args = [sys.executable, bootstrap_script]
2260
+
2261
+ # Pass through relevant arguments
2262
+ if args.name:
2263
+ bootstrap_args.extend(["--name", args.name])
2264
+
2265
+ if args.publish_to:
2266
+ bootstrap_args.extend(["--service", args.publish_to])
2267
+
2268
+ if args.private_repo:
2269
+ bootstrap_args.append("--private")
2270
+
2271
+ if args.organization:
2272
+ bootstrap_args.extend(["--organization", args.organization])
2273
+
2274
+ if args.output:
2275
+ bootstrap_args.extend(["--config-path", args.output])
2276
+
2277
+ if args.no_push:
2278
+ bootstrap_args.append("--no-publish")
2279
+
2280
+ if args.user_name:
2281
+ bootstrap_args.extend(["--git-user-name", args.user_name])
2282
+
2283
+ if args.user_email:
2284
+ bootstrap_args.extend(["--git-user-email", args.user_email])
2285
+
2286
+ # Add verbosity flags
2287
+ for _ in range(args.verbose):
2288
+ bootstrap_args.append("-v")
2289
+
2290
+ # Execute the bootstrap script
2291
+ logger.info(f"Running bootstrap script: {' '.join(bootstrap_args)}")
2292
+
2293
+ try:
2294
+ # Use subprocess.run for better control over encoding
2295
+ result = subprocess.run(bootstrap_args, encoding='utf-8', errors='replace')
2296
+ exit_code = result.returncode
2297
+ return exit_code
2298
+ except Exception as e:
2299
+ logger.error(f"Error running bootstrap script: {str(e)}")
2300
+ return 1
2301
+
2302
+ elif args.command == "analyze":
2303
+ if not args.name:
2304
+ logger.error("Directory path is required for 'analyze' command")
2305
+ return 1
2306
+
2307
+ try:
2308
+ # Analyze the directory
2309
+ dir_path = os.path.abspath(args.name)
2310
+ if not os.path.exists(dir_path):
2311
+ logger.error(f"Directory not found: {dir_path}")
2312
+ return 1
2313
+
2314
+ # Use comprehensive project analysis
2315
+ summary = analyze_project(dir_path, verbose=args.verbose)
2316
+
2317
+ # Display the enhanced analysis results
2318
+ print(f"\n=== PROJECT ANALYSIS: {dir_path} ===")
2319
+ print(f" Project type: {summary['project_type']}")
2320
+ print(f" Detected language: {summary['detected_language']}")
2321
+ print(f" Migration complexity: {summary['migration_complexity']}")
2322
+ print(f" Recommended strategy: {summary['recommended_strategy']}")
2323
+ print(
2324
+ f" Recommended branch strategy: {summary['recommended_branch_strategy']}"
2325
+ )
2326
+
2327
+ # Git state information
2328
+ git_state = summary.get("git_state", {})
2329
+ if git_state.get("is_repo"):
2330
+ print(f"\n Git repository:")
2331
+ print(
2332
+ f" Current branch: {git_state.get('current_branch', 'Unknown')}"
2333
+ )
2334
+ print(f" Branches: {', '.join(git_state.get('branches', []))}")
2335
+ print(
2336
+ f" Has uncommitted changes: {'Yes' if git_state.get('has_uncommitted_changes') else 'No'}"
2337
+ )
2338
+ print(
2339
+ f" Remotes: {', '.join(git_state.get('remotes', {}).keys()) or 'None'}"
2340
+ )
2341
+ print(f" Branch strategy: {summary['branch_strategy']}")
2342
+ else:
2343
+ print(f" Git repository: No")
2344
+
2345
+ # Directory structure
2346
+ print(f"\n Directory structure:")
2347
+ print(
2348
+ f" Existing standard directories: {', '.join(summary['existing_dirs']) or 'None'}"
2349
+ )
2350
+ print(
2351
+ f" Missing standard directories: {', '.join(summary['missing_dirs']) or 'None'}"
2352
+ )
2353
+ print(
2354
+ f" Special directories: {', '.join(summary['special_dirs']) or 'None'}"
2355
+ )
2356
+ print(
2357
+ f" Has template conflicts: {'Yes' if summary['has_template_conflicts'] else 'No'}"
2358
+ )
2359
+
2360
+ # File information
2361
+ print(f"\n File counts:")
2362
+ for category, count in summary["file_counts"].items():
2363
+ if count > 0:
2364
+ print(f" {category}: {count}")
2365
+ print(f" Total: {summary['total_files']}")
2366
+
2367
+ # Migration recommendations
2368
+ print(f"\n Migration steps:")
2369
+ for i, step in enumerate(summary["migration_steps"], 1):
2370
+ print(f" {i}. {step}")
2371
+
2372
+ # Command suggestions
2373
+ print(f"\n=== NEXT STEPS ===")
2374
+ if summary["project_type"] == "repokit":
2375
+ print(" Project is already using RepoKit structure.")
2376
+ elif summary["project_type"] == "empty":
2377
+ print(" Create new RepoKit project:")
2378
+ print(
2379
+ f" repokit create {os.path.basename(dir_path)} --language {summary['detected_language']}"
2380
+ )
2381
+ else:
2382
+ print(" Migrate to RepoKit structure:")
2383
+ print(
2384
+ f" repokit migrate {args.name} --migration-strategy {summary['recommended_strategy']}"
2385
+ )
2386
+ print(" Or adopt in-place:")
2387
+ print(
2388
+ f" repokit adopt {args.name} --strategy {summary['recommended_strategy']}"
2389
+ )
2390
+ if getattr(args, "publish_to", None):
2391
+ print(f" With publishing:")
2392
+ print(
2393
+ f" repokit adopt {args.name} --publish-to {args.publish_to}"
2394
+ )
2395
+
2396
+ return 0
2397
+ except Exception as e:
2398
+ logger.error(f"Error analyzing directory: {str(e)}")
2399
+ if args.verbose >= 2:
2400
+ import traceback
2401
+
2402
+ traceback.print_exc()
2403
+ return 1
2404
+
2405
+ elif args.command == "migrate":
2406
+ if not args.name:
2407
+ logger.error("Directory path is required for 'migrate' command")
2408
+ return 1
2409
+
2410
+ try:
2411
+ # Analyze and migrate the directory
2412
+ dir_path = os.path.abspath(args.name)
2413
+ if not os.path.exists(dir_path):
2414
+ logger.error(f"Directory not found: {dir_path}")
2415
+ return 1
2416
+
2417
+ # First show the analysis
2418
+ summary = analyze_directory(dir_path, verbose=args.verbose)
2419
+
2420
+ print(f"\nAnalysis of {dir_path}:")
2421
+ print(
2422
+ f" Existing standard directories: {', '.join(summary['existing_dirs']) or 'None'}"
2423
+ )
2424
+ print(
2425
+ f" Missing standard directories: {', '.join(summary['missing_dirs']) or 'None'}"
2426
+ )
2427
+ print(
2428
+ f" Special directories: {', '.join(summary['special_dirs']) or 'None'}"
2429
+ )
2430
+ print(
2431
+ f" Has Git repository: {'Yes' if summary['has_git_repository'] else 'No'}"
2432
+ )
2433
+ print(
2434
+ f" Has template conflicts: {'Yes' if summary['has_template_conflicts'] else 'No'}"
2435
+ )
2436
+
2437
+ # Get migration plan
2438
+ plan = plan_migration(
2439
+ dir_path, strategy=args.migration_strategy, verbose=args.verbose
2440
+ )
2441
+
2442
+ # Show migration plan
2443
+ print("\nMigration plan:")
2444
+ print(f" Strategy: {plan['strategy']}")
2445
+ print(
2446
+ f" Directories to create: {', '.join(plan['create_dirs']) or 'None'}"
2447
+ )
2448
+
2449
+ for dirname, info in plan["special_dirs"].items():
2450
+ print(f" Special directory '{dirname}': {info['action']}")
2451
+
2452
+ for template_dir, info in plan["template_conflicts"].items():
2453
+ print(
2454
+ f" Template conflicts in '{template_dir}' ({info['conflicts']} files): {info['action']}"
2455
+ )
2456
+
2457
+ # Create backup if requested
2458
+ if hasattr(args, 'backup') and args.backup and not args.dry_run:
2459
+ import shutil
2460
+ import datetime
2461
+
2462
+ # Determine backup location
2463
+ if hasattr(args, 'backup_location') and args.backup_location:
2464
+ backup_path = os.path.abspath(args.backup_location)
2465
+ else:
2466
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
2467
+ backup_path = os.path.abspath(f"{os.path.basename(dir_path)}_backup_{timestamp}")
2468
+
2469
+ try:
2470
+ print(f"\nCreating backup at: {backup_path}")
2471
+ shutil.copytree(dir_path, backup_path, symlinks=True)
2472
+ print(f"✓ Backup created successfully")
2473
+ except Exception as e:
2474
+ logger.error(f"Failed to create backup: {str(e)}")
2475
+ return 1
2476
+
2477
+ # Confirm migration
2478
+ if not args.dry_run and not args.quiet:
2479
+ confirm = input("\nProceed with migration? [y/N]: ")
2480
+ if confirm.lower() != "y":
2481
+ print("Migration cancelled.")
2482
+ return 0
2483
+
2484
+ # Execute migration
2485
+ success = migrate_directory(
2486
+ dir_path,
2487
+ strategy=args.migration_strategy,
2488
+ dry_run=args.dry_run,
2489
+ verbose=args.verbose,
2490
+ )
2491
+
2492
+ if success:
2493
+ msg = (
2494
+ "Migration analysis completed (dry run)"
2495
+ if args.dry_run
2496
+ else "Migration completed successfully"
2497
+ )
2498
+ print(f"\n{msg}")
2499
+ print("\nNext steps:")
2500
+
2501
+ if summary["has_git_repository"]:
2502
+ print(" - Your directory already has a Git repository.")
2503
+ print(" - You may want to update templates and configuration.")
2504
+ else:
2505
+ print(" - Initialize a Git repository (if not already done):")
2506
+ print(" git init")
2507
+ print(" - Set up your initial branches:")
2508
+ print(" git branch dev")
2509
+ print(" git branch staging")
2510
+ print(" git branch test")
2511
+ print(" git branch live")
2512
+
2513
+ if args.publish_to:
2514
+ print(f" - Publish to {args.publish_to}:")
2515
+ print(
2516
+ f" repokit publish {dir_path} --publish-to {args.publish_to}"
2517
+ )
2518
+
2519
+ return 0
2520
+ else:
2521
+ logger.error("Migration failed.")
2522
+ return 1
2523
+ except Exception as e:
2524
+ logger.error(f"Error migrating directory: {str(e)}")
2525
+ if args.verbose >= 2:
2526
+ import traceback
2527
+
2528
+ traceback.print_exc()
2529
+ return 1
2530
+
2531
+ elif args.command == "adopt":
2532
+ # Use current directory if no name provided
2533
+ dir_path = os.path.abspath(args.name) if args.name else os.getcwd()
2534
+
2535
+ if not os.path.exists(dir_path):
2536
+ logger.error(f"Directory not found: {dir_path}")
2537
+ return 1
2538
+
2539
+ try:
2540
+ # Update configuration with adopt-specific options
2541
+ adopt_config = config.copy()
2542
+
2543
+ # Set repo name based on --repo-name or directory name
2544
+ repo_name = getattr(args, 'repo_name', None) or os.path.basename(dir_path)
2545
+ adopt_config["name"] = repo_name
2546
+
2547
+ # Override language if specified
2548
+ if hasattr(args, 'language') and args.language:
2549
+ adopt_config["language"] = args.language
2550
+ if getattr(args, "with_layers", None):
2551
+ from .layers import parse_languages as _parse
2552
+ adopt_config["with"] = _parse(args.with_layers)
2553
+ if getattr(args, "license", None):
2554
+ adopt_config["license"] = args.license
2555
+
2556
+ # Override description if specified
2557
+ if hasattr(args, 'description') and args.description:
2558
+ adopt_config["description"] = args.description
2559
+
2560
+ # Merge adoption-specific sensitive patterns (CLI args + config file)
2561
+ if hasattr(args, 'sensitive_patterns') and args.sensitive_patterns:
2562
+ # Get existing patterns from config file
2563
+ existing_patterns = adopt_config.get("sensitive_patterns", [])
2564
+ # Add CLI patterns
2565
+ cli_patterns = [pattern.strip() for pattern in args.sensitive_patterns.split(",")]
2566
+ # Merge and deduplicate
2567
+ adopt_config["sensitive_patterns"] = list(set(existing_patterns + cli_patterns))
2568
+
2569
+ # Merge adoption-specific private directories (CLI args + config file)
2570
+ if hasattr(args, 'private_dirs') and args.private_dirs:
2571
+ # Get existing private dirs from config file
2572
+ existing_dirs = adopt_config.get("private_dirs", [])
2573
+ # Add CLI dirs
2574
+ cli_dirs = [dir_name.strip() for dir_name in args.private_dirs.split(",")]
2575
+ # Merge and deduplicate
2576
+ adopt_config["private_dirs"] = list(set(existing_dirs + cli_dirs))
2577
+
2578
+ # First analyze the project
2579
+ summary = analyze_project(dir_path, verbose=args.verbose)
2580
+
2581
+ print(f"\n=== ADOPTING PROJECT: {dir_path} ===")
2582
+ print(f" Project type: {summary['project_type']}")
2583
+ print(f" Detected language: {summary['detected_language']}")
2584
+ print(f" Migration complexity: {summary['migration_complexity']}")
2585
+
2586
+ # Check if already RepoKit
2587
+ if summary["project_type"] == "repokit":
2588
+ print(" Project is already using RepoKit structure.")
2589
+ if not args.publish_to:
2590
+ return 0
2591
+
2592
+ # Warn about uncommitted changes
2593
+ git_state = summary.get("git_state", {})
2594
+ if git_state.get("has_uncommitted_changes"):
2595
+ print(" WARNING: Uncommitted changes detected!")
2596
+ if not args.dry_run and not args.quiet:
2597
+ confirm = input(
2598
+ " Continue with adoption? Changes should be committed first. [y/N]: "
2599
+ )
2600
+ if confirm.lower() != "y":
2601
+ print(" Adoption cancelled.")
2602
+ return 0
2603
+
2604
+ # Determine strategies
2605
+ strategy = (
2606
+ args.migration_strategy
2607
+ if hasattr(args, "migration_strategy")
2608
+ else summary["recommended_strategy"]
2609
+ )
2610
+ branch_strategy = getattr(args, "branch_strategy", None)
2611
+ if not branch_strategy:
2612
+ branch_strategy = summary["recommended_branch_strategy"]
2613
+
2614
+ print(f" Using migration strategy: {strategy}")
2615
+ print(f" Using branch strategy: {branch_strategy}")
2616
+
2617
+ if args.dry_run:
2618
+ print(f" DRY RUN - No changes will be made")
2619
+
2620
+ # Create backup if requested
2621
+ if hasattr(args, 'backup') and args.backup and not args.dry_run:
2622
+ import shutil
2623
+ import datetime
2624
+
2625
+ # Determine backup location
2626
+ if hasattr(args, 'backup_location') and args.backup_location:
2627
+ backup_path = os.path.abspath(args.backup_location)
2628
+ else:
2629
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
2630
+ backup_path = os.path.abspath(f"{os.path.basename(dir_path)}_backup_{timestamp}")
2631
+
2632
+ try:
2633
+ print(f" Creating backup at: {backup_path}")
2634
+ shutil.copytree(dir_path, backup_path, symlinks=True)
2635
+ print(f" ✓ Backup created successfully")
2636
+ except Exception as e:
2637
+ logger.error(f"Failed to create backup: {str(e)}")
2638
+ return 1
2639
+
2640
+ # Clean history if requested
2641
+ if hasattr(args, 'clean_history') and args.clean_history and not args.dry_run:
2642
+ try:
2643
+ from .history_cleaner import HistoryCleaner, CleaningConfig, CleaningRecipe, GitFilterRepoNotFound
2644
+
2645
+ print(f" Cleaning repository history using '{args.cleaning_recipe}' recipe...")
2646
+
2647
+ # Initialize cleaner
2648
+ cleaner = HistoryCleaner(repo_path=dir_path, verbose=args.verbose)
2649
+
2650
+ # Build cleaning config using default settings
2651
+ recipe_map = {
2652
+ "pre-open-source": CleaningRecipe.PRE_OPEN_SOURCE,
2653
+ "windows-safe": CleaningRecipe.WINDOWS_SAFE,
2654
+ "remove-secrets": CleaningRecipe.REMOVE_SECRETS
2655
+ }
2656
+
2657
+ # Get default config for the recipe
2658
+ recipe_enum = recipe_map[args.cleaning_recipe]
2659
+ config = cleaner.get_recipe_config(recipe_enum)
2660
+
2661
+ # Override with adopt-specific settings
2662
+ config.backup_location = f"{backup_path}_pre_clean" if 'backup_path' in locals() else None
2663
+ config.dry_run = False
2664
+ config.force = True # Skip confirmation prompts during adopt
2665
+
2666
+ # Execute history cleaning
2667
+ success = cleaner.clean_history(config)
2668
+ if not success:
2669
+ logger.error("History cleaning failed - aborting adoption")
2670
+ return 1
2671
+
2672
+ print(f" ✓ Repository history cleaned successfully")
2673
+
2674
+ except GitFilterRepoNotFound as e:
2675
+ logger.error(str(e))
2676
+ print("\nTo install git filter-repo:")
2677
+ print(" pip install git-filter-repo")
2678
+ print("\nOr see: https://github.com/newren/git-filter-repo")
2679
+ return 1
2680
+ except Exception as e:
2681
+ logger.error(f"Failed to clean history: {str(e)}")
2682
+ return 1
2683
+
2684
+ # Execute adoption using enhanced RepoManager approach
2685
+ # Use RepoManager for non-repokit projects OR when publishing/templates are needed
2686
+ if (summary["project_type"] != "repokit" or args.publish_to):
2687
+ if args.dry_run:
2688
+ print(f"\n=== DRY RUN MODE - No changes will be made ===")
2689
+ print(f"Would execute RepoManager setup_repository() for:")
2690
+ print(f" Project: {repo_name}")
2691
+ print(f" Path: {dir_path}")
2692
+ print(f" Language: {adopt_config.get('language', 'generic')}")
2693
+ print(f" Branch strategy: {adopt_config.get('branch_strategy', 'standard')}")
2694
+ publish_to = getattr(args, 'publish_to', None) or adopt_config.get('publish_to')
2695
+ print(f" Would publish to: {publish_to if publish_to else 'none'}")
2696
+ print(f" Would create backup: {hasattr(args, 'backup') and args.backup}")
2697
+ print(f" Would clean history: {hasattr(args, 'clean_history') and args.clean_history}")
2698
+
2699
+ # Show what sensitive file cleanup would do
2700
+ from .defaults import DEFAULT_SENSITIVE_PATTERNS
2701
+ print(f"\nWould clean files matching these patterns:")
2702
+ for pattern in DEFAULT_SENSITIVE_PATTERNS[:5]:
2703
+ print(f" - {pattern}")
2704
+ if len(DEFAULT_SENSITIVE_PATTERNS) > 5:
2705
+ print(f" ... and {len(DEFAULT_SENSITIVE_PATTERNS) - 5} more patterns")
2706
+
2707
+ print(f"\n=== DRY RUN COMPLETE - No actual changes made ===")
2708
+ return 0
2709
+ # Initialize RepoManager with the merged configuration for in-place adoption
2710
+ adopt_config["name"] = repo_name
2711
+ # Set preserve_existing flag for adoption to avoid overwriting project files
2712
+ adopt_config["preserve_existing"] = True
2713
+ adopt_config["is_adoption"] = True # Flag to indicate this is an adoption
2714
+
2715
+ # Debug: Log final adopt_config
2716
+ logger.debug(f"Final adopt_config before RepoManager: {adopt_config}")
2717
+ logger.debug(f"Sensitive patterns in adopt_config: {adopt_config.get('sensitive_patterns', 'NOT FOUND')}")
2718
+
2719
+ repo_manager = RepoManager(
2720
+ adopt_config, templates_dir=getattr(args, 'templates_dir', None), verbose=args.verbose
2721
+ )
2722
+
2723
+ # Override the project_root and repo_root to adopt in-place
2724
+ repo_manager.project_root = os.path.dirname(dir_path)
2725
+ repo_manager.repo_root = dir_path
2726
+ # Fix github_root path after override to ensure proper template deployment
2727
+ repo_manager.github_root = os.path.join(repo_manager.project_root, "github")
2728
+
2729
+ # Set up repository structure (this will add missing RepoKit structure)
2730
+ success = repo_manager.setup_repository()
2731
+ if not success:
2732
+ logger.error("Failed to set up RepoKit structure")
2733
+ return 1
2734
+ else:
2735
+ # Use the simple adopt_project for dry runs or already RepoKit projects
2736
+ success = adopt_project(
2737
+ directory=dir_path,
2738
+ strategy=strategy,
2739
+ branch_strategy=branch_strategy,
2740
+ publish_to=args.publish_to,
2741
+ dry_run=args.dry_run,
2742
+ verbose=args.verbose,
2743
+ )
2744
+ if not success:
2745
+ logger.error("Project adoption failed.")
2746
+ return 1
2747
+
2748
+ # Handle remote publishing if requested (from CLI args or config file)
2749
+ publish_to = getattr(args, 'publish_to', None) or adopt_config.get('publish_to')
2750
+ if publish_to and not args.dry_run:
2751
+ from .remote_integration import RemoteIntegration
2752
+
2753
+ # Update configuration for remote publishing
2754
+ organization = getattr(args, 'organization', None) or adopt_config.get('organization')
2755
+ if organization:
2756
+ if publish_to == "github":
2757
+ adopt_config.setdefault("github", {})["organization"] = organization
2758
+ else:
2759
+ adopt_config.setdefault("gitlab", {})["group"] = organization
2760
+
2761
+ # Initialize RepoManager for publishing (if not already done)
2762
+ if summary["project_type"] == "repokit":
2763
+ repo_manager = RepoManager(
2764
+ adopt_config, templates_dir=getattr(args, 'templates_dir', None), verbose=args.verbose
2765
+ )
2766
+ repo_manager.project_root = os.path.dirname(dir_path)
2767
+ repo_manager.repo_root = dir_path
2768
+
2769
+ remote_integration = RemoteIntegration(
2770
+ repo_manager,
2771
+ credentials_file=getattr(args, 'credentials_file', None),
2772
+ verbose=args.verbose,
2773
+ )
2774
+
2775
+ # Handle token - if direct token provided, create a command that echoes it
2776
+ token_cmd = getattr(args, 'token_command', None)
2777
+ if hasattr(args, 'token') and args.token and not token_cmd:
2778
+ token_cmd = f"echo {args.token}"
2779
+
2780
+ publish_success = remote_integration.setup_remote_repository(
2781
+ service=publish_to,
2782
+ remote_name=getattr(args, 'remote_name', None) or adopt_config.get('remote_name', 'origin'),
2783
+ private=getattr(args, 'private_repo', None) or adopt_config.get('private_repo', False),
2784
+ push_branches=not getattr(args, 'no_push', False),
2785
+ organization=organization,
2786
+ token_command=token_cmd,
2787
+ )
2788
+
2789
+ if not publish_success:
2790
+ logger.error(f"Failed to publish repository to {publish_to}")
2791
+ return 1
2792
+
2793
+ logger.info(f"Successfully published repository to {publish_to}")
2794
+
2795
+ if args.dry_run:
2796
+ print(f"\nAdoption analysis completed (dry run)")
2797
+ else:
2798
+ print(f"\nProject adopted successfully!")
2799
+
2800
+ print("\nNext steps:")
2801
+ print(f" - Your project now uses RepoKit structure")
2802
+ print(f" - Check the generated templates and configuration")
2803
+ print(f" - Commit any new files to your repository")
2804
+
2805
+ if publish_to:
2806
+ print(f" - Repository has been published to {publish_to}")
2807
+ elif not git_state.get("remotes"):
2808
+ print(f" - Consider publishing to a remote service:")
2809
+ print(f" repokit publish {dir_path} --publish-to github")
2810
+
2811
+ return 0
2812
+
2813
+ except Exception as e:
2814
+ logger.error(f"Error adopting project: {str(e)}")
2815
+ if args.verbose >= 2:
2816
+ import traceback
2817
+
2818
+ traceback.print_exc()
2819
+ return 1
2820
+
2821
+ elif args.command == "create":
2822
+ if not args.name:
2823
+ logger.error("Repository name is required for 'create' command")
2824
+ return 1
2825
+
2826
+ # The code-type names are checked here, before a folder or a repository
2827
+ # exists: an unknown language or archetype, or a stub that is not built
2828
+ # yet, stops with the known names.
2829
+ from . import layers as _layers
2830
+ from .template_engine import TemplateEngine as _TemplateEngine
2831
+ try:
2832
+ _root = _layers.layers_root(_TemplateEngine(templates_dir=args.templates_dir))
2833
+ if _root is not None:
2834
+ _layers.resolve(_layers.load_manifest(_root),
2835
+ config.get("languages") or [config.get("language") or _layers.GENERIC],
2836
+ config.get("with"))
2837
+ elif config.get("with"):
2838
+ raise _layers.LayerError("no code-type layers found with the templates, so --with cannot be used")
2839
+ except _layers.LayerError as e:
2840
+ logger.error(str(e))
2841
+ return 1
2842
+
2843
+ # The GitHub login, before any file is written: the files' URLs,
2844
+ # CODEOWNERS and FUNDING name it (0.9.6; before, they carried the
2845
+ # YOUR_GITHUB_USERNAME placeholder even with --github)
2846
+ if getattr(args, "github", False) and not args.no_push:
2847
+ from .github_create import gh_login
2848
+ config["github_create"] = True
2849
+ login = gh_login(os.getcwd())
2850
+ if login:
2851
+ config["github_login"] = login
2852
+
2853
+ try:
2854
+ # Initialize RepoManager with the merged configuration
2855
+ repo_manager = RepoManager(
2856
+ config, templates_dir=args.templates_dir, verbose=args.verbose
2857
+ )
2858
+
2859
+ # Set up repository
2860
+ success = repo_manager.setup_repository()
2861
+ if not success:
2862
+ return 1
2863
+
2864
+ # GitHub through gh (its login, no token here)
2865
+ if getattr(args, "github", False):
2866
+ if args.no_push:
2867
+ logger.info("--github with --no-push: nothing created on GitHub")
2868
+ else:
2869
+ from .github_create import create_on_github
2870
+ setup_report: list = []
2871
+ # repo_manager.config carries what the create settled
2872
+ # (the archetype, the extras) beside the person's settings
2873
+ problem = create_on_github(
2874
+ repo_manager.repo_root, repo_manager.project_name, repo_manager.config,
2875
+ repo_manager.private_branch, organization=args.organization,
2876
+ public=getattr(args, "public", False), logger=logger,
2877
+ report=setup_report,
2878
+ )
2879
+ for line in setup_report:
2880
+ print(f"GitHub: {line}")
2881
+ if problem:
2882
+ logger.error(f"GitHub: {problem}")
2883
+ logger.error("The local repository is complete; retry with gh once fixed.")
2884
+ return 1
2885
+
2886
+ # Publish to remote if requested
2887
+ if args.publish_to:
2888
+ # --token reaches the token lookup through its environment variable
2889
+ if getattr(args, "token", None):
2890
+ os.environ[f"{args.publish_to.upper()}_TOKEN"] = args.token
2891
+ from .remote_integration import RemoteIntegration
2892
+
2893
+ remote_integration = RemoteIntegration(
2894
+ repo_manager,
2895
+ credentials_file=args.credentials_file,
2896
+ verbose=args.verbose,
2897
+ )
2898
+
2899
+ publish_success = remote_integration.setup_remote_repository(
2900
+ service=args.publish_to,
2901
+ remote_name=args.remote_name,
2902
+ private=args.private_repo,
2903
+ push_branches=not args.no_push,
2904
+ organization=args.organization,
2905
+ token_command=args.token_command,
2906
+ )
2907
+
2908
+ if not publish_success:
2909
+ logger.error(f"Failed to publish repository to {args.publish_to}")
2910
+ return 1
2911
+
2912
+ logger.info(f"Successfully published repository to {args.publish_to}")
2913
+
2914
+ return 0
2915
+ except Exception as e:
2916
+ logger.error(f"Error: {str(e)}")
2917
+ if args.verbose >= 2:
2918
+ import traceback
2919
+
2920
+ traceback.print_exc()
2921
+ return 1
2922
+
2923
+ elif args.command == "publish":
2924
+ if not args.name:
2925
+ logger.error("Repository path is required for 'publish' command")
2926
+ return 1
2927
+
2928
+ if not args.publish_to:
2929
+ logger.error("--publish-to is required for 'publish' command")
2930
+ return 1
2931
+
2932
+ try:
2933
+ # Use the provided path as the repository root
2934
+ repo_path = os.path.abspath(args.name)
2935
+ if not os.path.exists(repo_path):
2936
+ logger.error(f"Repository not found at {repo_path}")
2937
+ return 1
2938
+
2939
+ # Update configuration with the correct path and name
2940
+ repo_name = args.repo_name if args.repo_name else os.path.basename(repo_path)
2941
+ config["name"] = repo_name
2942
+
2943
+ # Initialize RepoManager with the repository path
2944
+ repo_manager = RepoManager(
2945
+ config, templates_dir=args.templates_dir, verbose=args.verbose
2946
+ )
2947
+
2948
+ # Override the project_root and repo_root
2949
+ repo_manager.project_root = os.path.dirname(repo_path)
2950
+ repo_manager.repo_root = repo_path
2951
+
2952
+ # Publish to remote
2953
+ from .remote_integration import RemoteIntegration
2954
+
2955
+ remote_integration = RemoteIntegration(
2956
+ repo_manager,
2957
+ credentials_file=args.credentials_file,
2958
+ verbose=args.verbose,
2959
+ )
2960
+
2961
+ # Handle token - if direct token provided, create a command that echoes it
2962
+ token_cmd = args.token_command
2963
+ if args.token and not token_cmd:
2964
+ token_cmd = f"echo {args.token}"
2965
+
2966
+ publish_success = remote_integration.setup_remote_repository(
2967
+ service=args.publish_to,
2968
+ remote_name=args.remote_name,
2969
+ private=args.private_repo,
2970
+ push_branches=not args.no_push,
2971
+ organization=args.organization,
2972
+ token_command=token_cmd,
2973
+ include_branches=getattr(args, 'include_branches', None),
2974
+ exclude_branches=getattr(args, 'exclude_branches', None),
2975
+ )
2976
+
2977
+ if not publish_success:
2978
+ logger.error(f"Failed to publish repository to {args.publish_to}")
2979
+ return 1
2980
+
2981
+ logger.info(f"Successfully published repository to {args.publish_to}")
2982
+ return 0
2983
+ except Exception as e:
2984
+ logger.error(f"Error: {str(e)}")
2985
+ if args.verbose >= 2:
2986
+ import traceback
2987
+
2988
+ traceback.print_exc()
2989
+ return 1
2990
+
2991
+ # Unknown command (should not happen due to choices in argparse)
2992
+ logger.error(f"Unknown command: {args.command}")
2993
+ return 1
2994
+
2995
+
2996
+ if __name__ == "__main__":
2997
+ sys.exit(main())