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/utils.py ADDED
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Utility functions for RepoKit.
4
+
5
+ Provides general-purpose utility functions for file operations,
6
+ pattern matching, and other common tasks.
7
+ """
8
+
9
+ import os
10
+ import fnmatch
11
+ import shutil
12
+ import logging
13
+ from pathlib import Path
14
+ from typing import List, Set, Dict, Any, Optional, Union
15
+
16
+ # Set up logger
17
+ logger = logging.getLogger("repokit.utils")
18
+
19
+
20
+ def match_pattern(path: str, pattern: str) -> bool:
21
+ """
22
+ Check if a path matches a pattern.
23
+
24
+ Args:
25
+ path: Path to check
26
+ pattern: Pattern to match against (supports * wildcard)
27
+
28
+ Returns:
29
+ True if path matches pattern, False otherwise
30
+ """
31
+ # Handle directory patterns (ending with /)
32
+ if pattern.endswith("/"):
33
+ dir_pattern = pattern[:-1]
34
+ path_parts = path.split(os.sep)
35
+ return any(fnmatch.fnmatch(part, dir_pattern) for part in path_parts)
36
+
37
+ # Check if the filename matches the pattern
38
+ return fnmatch.fnmatch(os.path.basename(path), pattern)
39
+
40
+
41
+ def glob_to_ere(pattern: str) -> str:
42
+ """A private-content pattern as a POSIX extended regex for `grep -E`.
43
+
44
+ Anchored at the start of the path. Regex metacharacters are escaped, so
45
+ `*.log` no longer matches `catalog.py`; `*` matches any run of characters
46
+ (across `/`, as the hooks always read it), `**/` any directory prefix or
47
+ none, a trailing `/` everything inside that directory, and a pattern with
48
+ no `*` the whole path.
49
+ """
50
+ is_dir = pattern.endswith("/")
51
+ body = pattern.rstrip("/") if is_dir else pattern
52
+ out: List[str] = []
53
+ i = 0
54
+ while i < len(body):
55
+ if body.startswith("**/", i):
56
+ out.append("(.*/)?")
57
+ i += 3
58
+ continue
59
+ c = body[i]
60
+ if c == "*":
61
+ out.append(".*")
62
+ elif c in ".+?()[]{}^$|\\":
63
+ out.append("\\" + c)
64
+ else:
65
+ out.append(c)
66
+ i += 1
67
+ core = "".join(out)
68
+ if is_dir:
69
+ return f"^{core}/"
70
+ if "*" in body:
71
+ return f"^{core}"
72
+ return f"^{core}$"
73
+
74
+
75
+ def should_include_file(
76
+ path: str,
77
+ rel_path: str,
78
+ include_patterns: Optional[List[str]] = None,
79
+ exclude_patterns: Optional[List[str]] = None,
80
+ ) -> bool:
81
+ """
82
+ Check if a file should be included based on patterns.
83
+
84
+ Args:
85
+ path: Full path to the file
86
+ rel_path: Relative path from source directory
87
+ include_patterns: List of patterns to include (if None, include all)
88
+ exclude_patterns: List of patterns to exclude
89
+
90
+ Returns:
91
+ True if the file should be included, False otherwise
92
+ """
93
+ # Always exclude .git directory
94
+ if ".git" in rel_path.split(os.sep):
95
+ return False
96
+
97
+ # Check exclude patterns first
98
+ if exclude_patterns:
99
+ for pattern in exclude_patterns:
100
+ if match_pattern(rel_path, pattern):
101
+ return False
102
+
103
+ # If no include patterns, include everything not excluded
104
+ if not include_patterns:
105
+ return True
106
+
107
+ # Check if path matches any include pattern
108
+ for pattern in include_patterns:
109
+ if match_pattern(rel_path, pattern):
110
+ return True
111
+
112
+ # Check if any parent directory matches an include pattern
113
+ path_parts = rel_path.split(os.sep)
114
+ for i in range(len(path_parts)):
115
+ parent_path = os.sep.join(path_parts[: i + 1])
116
+ for pattern in include_patterns:
117
+ if pattern.endswith("/") and match_pattern(parent_path, pattern[:-1]):
118
+ return True
119
+
120
+ return False
121
+
122
+
123
+ def copy_files(
124
+ source_dir: str,
125
+ target_dir: str,
126
+ include_patterns: Optional[List[str]] = None,
127
+ exclude_patterns: Optional[List[str]] = None,
128
+ preserve_directory_structure: bool = True,
129
+ ) -> int:
130
+ """
131
+ Copy files from source to target directory based on patterns.
132
+
133
+ Args:
134
+ source_dir: Source directory
135
+ target_dir: Target directory
136
+ include_patterns: List of patterns to include (if None, include all)
137
+ exclude_patterns: List of patterns to exclude
138
+ preserve_directory_structure: Whether to preserve directory structure
139
+
140
+ Returns:
141
+ Number of files copied
142
+ """
143
+ copied_files = 0
144
+ source_dir = os.path.abspath(source_dir)
145
+ target_dir = os.path.abspath(target_dir)
146
+
147
+ logger.info(f"Copying files from {source_dir} to {target_dir}")
148
+
149
+ # Create standard exclude patterns if not provided
150
+ if exclude_patterns is None:
151
+ exclude_patterns = [
152
+ "__pycache__/",
153
+ "*.py[cod]",
154
+ "*$py.class",
155
+ "*.so",
156
+ ".git/",
157
+ "*.egg-info/",
158
+ "*.egg",
159
+ "dist/",
160
+ "build/",
161
+ "credentials.json",
162
+ "*.token",
163
+ ".env",
164
+ "*.bak",
165
+ "*.tmp",
166
+ "*.swp",
167
+ "*~",
168
+ "*.*~",
169
+ ]
170
+
171
+ # Walk through the source directory
172
+ for dirpath, dirnames, filenames in os.walk(source_dir):
173
+ # Get relative path
174
+ rel_path = os.path.relpath(dirpath, source_dir)
175
+
176
+ # Skip entire directories if they match exclude patterns
177
+ skip_dir = False
178
+ for pattern in exclude_patterns:
179
+ if pattern.endswith("/") and match_pattern(rel_path, pattern[:-1]):
180
+ logger.debug(f"Skipping directory: {rel_path}")
181
+ dirnames[:] = [] # Clear dirnames to skip recursion
182
+ skip_dir = True
183
+ break
184
+
185
+ if skip_dir:
186
+ continue
187
+
188
+ # Process files in this directory
189
+ for filename in filenames:
190
+ src_file = os.path.join(dirpath, filename)
191
+ rel_file_path = (
192
+ os.path.join(rel_path, filename) if rel_path != "." else filename
193
+ )
194
+
195
+ # Check if this file should be copied
196
+ if not should_include_file(
197
+ src_file, rel_file_path, include_patterns, exclude_patterns
198
+ ):
199
+ logger.debug(f"Skipping file: {rel_file_path}")
200
+ continue
201
+
202
+ # Determine target path
203
+ if preserve_directory_structure:
204
+ target_file = os.path.join(target_dir, rel_file_path)
205
+ # Create target directory if needed
206
+ os.makedirs(os.path.dirname(target_file), exist_ok=True)
207
+ else:
208
+ target_file = os.path.join(target_dir, filename)
209
+
210
+ # Copy the file
211
+ try:
212
+ shutil.copy2(src_file, target_file)
213
+ copied_files += 1
214
+ logger.debug(f"Copied: {rel_file_path}")
215
+ except Exception as e:
216
+ logger.error(f"Error copying {src_file}: {str(e)}")
217
+
218
+ logger.info(f"Copied {copied_files} files to {target_dir}")
219
+ return copied_files
220
+
221
+
222
+ def copy_file_with_directory(source_file: str, target_dir: str) -> bool:
223
+ """
224
+ Copy a single file to a target directory, creating directories as needed.
225
+
226
+ Args:
227
+ source_file: Path to source file
228
+ target_dir: Target directory
229
+
230
+ Returns:
231
+ True if successful, False otherwise
232
+ """
233
+ try:
234
+ source_file = os.path.abspath(source_file)
235
+ target_dir = os.path.abspath(target_dir)
236
+
237
+ if not os.path.exists(source_file):
238
+ logger.error(f"Source file does not exist: {source_file}")
239
+ return False
240
+
241
+ # Create target directory if it doesn't exist
242
+ os.makedirs(target_dir, exist_ok=True)
243
+
244
+ # Copy the file
245
+ target_file = os.path.join(target_dir, os.path.basename(source_file))
246
+ shutil.copy2(source_file, target_file)
247
+ logger.debug(f"Copied: {source_file} to {target_file}")
248
+ return True
249
+ except Exception as e:
250
+ logger.error(f"Error copying {source_file}: {str(e)}")
251
+ return False
252
+
253
+
254
+ def get_essential_file_patterns() -> Dict[str, List[str]]:
255
+ """
256
+ Get patterns for essential files and directories in a project.
257
+
258
+ Returns:
259
+ Dictionary with include and exclude patterns for essential files
260
+ """
261
+ return {
262
+ "include": [
263
+ # Standard Python project files
264
+ "setup.py",
265
+ "pyproject.toml",
266
+ "README.md",
267
+ "LICENSE",
268
+ "MANIFEST.in",
269
+ "requirements.txt",
270
+ # Main package and critical directories
271
+ "repokit/",
272
+ "docs/",
273
+ "scripts/",
274
+ "templates/",
275
+ ],
276
+ "exclude": [
277
+ # Python bytecode and cache
278
+ "__pycache__/",
279
+ "*.py[cod]",
280
+ "*$py.class",
281
+ "*.so",
282
+ # Build artifacts
283
+ "dist/",
284
+ "build/",
285
+ "*.egg-info/",
286
+ "*.egg",
287
+ # Version control
288
+ ".git/",
289
+ ".gitignore",
290
+ ".gitattributes",
291
+ # Credentials and secrets
292
+ "credentials.json",
293
+ "*.token",
294
+ ".env",
295
+ # Virtual environments
296
+ "venv/",
297
+ ".venv/",
298
+ "env/",
299
+ ".env/",
300
+ # Temporary files
301
+ "*.bak",
302
+ "*.tmp",
303
+ "*.swp",
304
+ "*~",
305
+ # IDE files
306
+ ".vscode/",
307
+ ".idea/",
308
+ "*.sublime-*",
309
+ # Test directories
310
+ "test*/",
311
+ # Private directories
312
+ "private/",
313
+ "convos/",
314
+ "logs/",
315
+ ],
316
+ }
repokit/vault.py ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ The nested private repository: private/ as its own git repository.
4
+
5
+ A project's private notes live in private/, which the project's .gitignore
6
+ excludes, and private/ is itself a git repository with its own history. Nothing
7
+ private can reach the project's branches, so no hook has to guard them.
8
+
9
+ Ported from dazzlecmd's `dz private-init` (dazzletools/private-init/
10
+ private_init.py, cmd_init and its constants) so that git-repokit creates the
11
+ vault itself; dazzlecmd may later call this module instead of keeping its own.
12
+
13
+ (Named vault.py, not private_repo.py: the private-branch model's guards treat
14
+ any path containing "private" as private content and would refuse or cut it.)
15
+ """
16
+
17
+ import os
18
+ import subprocess
19
+ import sys
20
+ from typing import List, Optional, Tuple
21
+
22
+ # The vault's standard folders (private_init.PRIVATE_STRUCTURE)
23
+ PRIVATE_STRUCTURE = [
24
+ "claude",
25
+ "claude/commits",
26
+ "claude/issues",
27
+ "docs",
28
+ "notes",
29
+ ]
30
+
31
+ # What the vault ignores (private_init.PRIVATE_GITIGNORE_SECTIONS)
32
+ PRIVATE_GITIGNORE_SECTIONS = [
33
+ ("Python", ["__pycache__/", "*.py[cod]", "*.so"]),
34
+ ("Editors", ["*.swp", "*~", "*.*~", ".*.swp", ".vscode/"]),
35
+ ("OS", [".DS_Store", "Thumbs.db", "desktop.ini"]),
36
+ ("Temp files", ["*.tmp", "*.bak"]),
37
+ ("Agent working state -- written by tools, never versioned", [".gauntlet/"]),
38
+ ]
39
+
40
+ # What the parent project ignores besides private/ itself
41
+ # (private_init.PARENT_GITIGNORE_SECTIONS)
42
+ PARENT_GITIGNORE_SECTIONS = [
43
+ ("Private repository -- its own git repository, never part of this one", ["/private/"]),
44
+ ("Agent working state -- written by tools, never versioned", ["CLAUDE.local.md"]),
45
+ ]
46
+
47
+
48
+ def render_gitignore(sections) -> str:
49
+ """Render (title, patterns) pairs as .gitignore text."""
50
+ return "\n".join("# " + title + "\n" + "\n".join(patterns) + "\n" for title, patterns in sections)
51
+
52
+
53
+ def _git(args: List[str], cwd: str) -> Tuple[int, str, str]:
54
+ result = subprocess.run(
55
+ ["git"] + args, cwd=cwd, capture_output=True, text=True,
56
+ encoding="utf-8", errors="replace",
57
+ )
58
+ return result.returncode, result.stdout.strip(), result.stderr.strip()
59
+
60
+
61
+ def ensure_parent_ignores(repo_root: str) -> List[str]:
62
+ """Append the parent .gitignore lines that are missing; return what was added."""
63
+ path = os.path.join(repo_root, ".gitignore")
64
+ existing = ""
65
+ if os.path.exists(path):
66
+ with open(path, encoding="utf-8") as f:
67
+ existing = f.read()
68
+ present = {line.strip() for line in existing.splitlines()}
69
+
70
+ missing = [(title, [p for p in patterns if p not in present])
71
+ for title, patterns in PARENT_GITIGNORE_SECTIONS]
72
+ missing = [(title, patterns) for title, patterns in missing if patterns]
73
+ if not missing:
74
+ return []
75
+
76
+ with open(path, "a", encoding="utf-8") as f:
77
+ if existing and not existing.endswith("\n"):
78
+ f.write("\n")
79
+ f.write("\n" + render_gitignore(missing))
80
+ return [p for _title, patterns in missing for p in patterns]
81
+
82
+
83
+ SKELETON = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates", "vault")
84
+ VAULT_INIT_MODES = ("prompt-file", "none")
85
+
86
+
87
+ def write_skeleton(private_path: str, project_name: str) -> List[str]:
88
+ """Lay the vault skeleton (0.9.4): the vault's CLAUDE.md with its
89
+ conventions and the three-session note, the map stub, the oracle's index
90
+ stubs, the two boards. Files that exist are kept. Returns what was written."""
91
+ import datetime
92
+ import string
93
+ values = {"project_name": project_name, "date": datetime.date.today().isoformat()}
94
+ written = []
95
+ for root, _dirs, files in os.walk(SKELETON):
96
+ for name in files:
97
+ src = os.path.join(root, name)
98
+ rel = os.path.relpath(src, SKELETON).replace("\\", "/")
99
+ if rel == "claude/_maps/MOC-project.md":
100
+ rel = f"claude/_maps/MOC-{project_name}.md"
101
+ dest = os.path.join(private_path, rel)
102
+ if os.path.exists(dest):
103
+ continue
104
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
105
+ with open(src, encoding="utf-8") as f:
106
+ text = string.Template(f.read()).safe_substitute(values)
107
+ with open(dest, "w", encoding="utf-8", newline="\n") as f:
108
+ f.write(text)
109
+ written.append(rel)
110
+ return sorted(written)
111
+
112
+
113
+ def bootstrap_prompt_text(repo_root: str, project_name: str) -> str:
114
+ """What the oracle session is asked to do first: build the map and the
115
+ index from the project and the vault (the half of /obsidian-init a model
116
+ has to do, because it reads the project)."""
117
+ top = sorted(n for n in os.listdir(repo_root) if not n.startswith(".git") and n != "private")
118
+ docs = sorted(os.path.relpath(os.path.join(r, n), os.path.join(repo_root, "private")).replace("\\", "/")
119
+ for r, _d, fs in os.walk(os.path.join(repo_root, "private", "claude")) for n in fs
120
+ if n.endswith(".md") and ".git" not in r)
121
+ return "\n".join([
122
+ f"# Bootstrap the vault of {project_name}",
123
+ "",
124
+ f"You are the oracle session for `{project_name}`. git-repokit laid the vault's skeleton; "
125
+ "fill the parts that need someone who has read the project:",
126
+ "",
127
+ f"1. Read the project's README, CHANGELOG and top-level layout ({', '.join(top)}).",
128
+ f"2. Fill `claude/_maps/MOC-{project_name}.md` with the project's arcs (one so far: its creation) "
129
+ "and link every document under `claude/`.",
130
+ "3. Fill `claude/_oracle/manifest.md` with one row per document, and `concepts.md` with any term "
131
+ "the project uses in its own sense.",
132
+ "4. Run `python scripts/repokit-common/generate-backlinks.py private/claude --validate` from the project root.",
133
+ "5. Commit the vault by named path; never push it.",
134
+ "",
135
+ "Then run `/obsidian-update` and `/obsidian-loop` as usual.",
136
+ "",
137
+ "## The vault today",
138
+ "",
139
+ *[f"- `{d}`" for d in docs],
140
+ "",
141
+ ])
142
+
143
+
144
+ def bootstrap(repo_root: str, project_name: str, mode: str = "prompt-file") -> Optional[str]:
145
+ """The vault's AI door (0.9.4). `prompt-file` (the default) writes the
146
+ oracle's first prompt through ailib's prompt-file transport to
147
+ private/claude/_oracle/ and returns its path; `none` writes nothing. The
148
+ tool never commits what it writes here: the oracle session reviews it."""
149
+ if mode not in VAULT_INIT_MODES:
150
+ raise ValueError(f"unknown vault_init {mode!r}; known: {', '.join(VAULT_INIT_MODES)}")
151
+ if mode == "none":
152
+ return None
153
+ from ._vendor.ailib.types import Request, Spec
154
+ from ._vendor.ailib.backend import build
155
+ oracle = os.path.join(repo_root, "private", "claude", "_oracle")
156
+ backend = build(Spec(transport="prompt-file", name="vault-bootstrap", endpoint=oracle))
157
+ response = backend.invoke(Request(prompt=bootstrap_prompt_text(repo_root, project_name)))
158
+ if response.status != "deferred" or not response.artifact:
159
+ return None
160
+ final = os.path.join(oracle, "bootstrap-prompt.md")
161
+ os.replace(response.artifact, final)
162
+ return final
163
+
164
+
165
+ def init_private_repo(repo_root: str, project_name: str, logger=None) -> bool:
166
+ """Create private/ as its own git repository with a first commit.
167
+
168
+ Refuses an existing private repository (never re-initialises). Signing is
169
+ turned off inside the vault, as dz private-init does: it is local-only.
170
+ Returns True when the vault exists afterwards.
171
+ """
172
+ log = logger.info if logger else (lambda msg: None)
173
+ private_path = os.path.join(repo_root, "private")
174
+
175
+ added = ensure_parent_ignores(repo_root)
176
+ if added:
177
+ log(f"Parent .gitignore now ignores: {', '.join(added)}")
178
+
179
+ if os.path.exists(os.path.join(private_path, ".git")):
180
+ log("private/ is already a git repository; left as it is")
181
+ return True
182
+
183
+ existing_content = os.path.isdir(private_path) and os.listdir(private_path)
184
+ os.makedirs(private_path, exist_ok=True)
185
+
186
+ rc, _, err = _git(["init", "-q"], private_path)
187
+ if rc != 0:
188
+ if logger:
189
+ logger.error(f"git init failed in private/: {err}")
190
+ return False
191
+ _git(["config", "commit.gpgsign", "false"], private_path)
192
+ _git(["config", "tag.gpgsign", "false"], private_path)
193
+
194
+ gitignore = os.path.join(private_path, ".gitignore")
195
+ if not os.path.exists(gitignore):
196
+ with open(gitignore, "w", encoding="utf-8") as f:
197
+ f.write(render_gitignore(PRIVATE_GITIGNORE_SECTIONS))
198
+
199
+ if not existing_content:
200
+ # The skeleton /obsidian-init laid by hand (0.9.4), before the
201
+ # folders' gitkeeps so a folder with a file needs none.
202
+ write_skeleton(private_path, project_name)
203
+ for sub in PRIVATE_STRUCTURE:
204
+ folder = os.path.join(private_path, sub)
205
+ os.makedirs(folder, exist_ok=True)
206
+ if not os.listdir(folder):
207
+ open(os.path.join(folder, ".gitkeep"), "w").close()
208
+
209
+ _git(["add", "-A"], private_path)
210
+ message = (f"init: adopt existing private content for {project_name}" if existing_content
211
+ else f"init: private workspace for {project_name}")
212
+ rc, _, err = _git(["commit", "-q", "-m", message], private_path)
213
+ if rc != 0 and logger:
214
+ logger.warning(f"private/: first commit failed: {err}")
215
+ log("private/ is its own git repository")
216
+ return True
217
+
218
+
219
+ def link_private(worktree_path: str, private_path: str) -> Optional[str]:
220
+ """Give a worktree a link to the real vault: <worktree>/private -> private_path.
221
+
222
+ A directory junction on Windows (no administrator rights needed, no shell),
223
+ a symlink elsewhere. Returns None on success, or the reason it failed.
224
+ Never replaces something that is already there.
225
+ """
226
+ link = os.path.join(worktree_path, "private")
227
+ if os.path.lexists(link):
228
+ return f"{link} already exists"
229
+ target = os.path.abspath(private_path)
230
+ try:
231
+ if sys.platform == "win32":
232
+ try:
233
+ import _winapi
234
+ _winapi.CreateJunction(target, link)
235
+ except (ImportError, AttributeError, OSError):
236
+ # Fallback: PowerShell, never `cmd /c mklink` (it fails silently from bash)
237
+ result = subprocess.run(
238
+ ["powershell", "-NoProfile", "-Command",
239
+ f"New-Item -ItemType Junction -Path '{link}' -Target '{target}' | Out-Null"],
240
+ capture_output=True, text=True,
241
+ )
242
+ if result.returncode != 0:
243
+ return result.stderr.strip() or "PowerShell could not create the junction"
244
+ else:
245
+ os.symlink(target, link, target_is_directory=True)
246
+ except OSError as e:
247
+ return str(e)
248
+ return None