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.
- git_repokit-0.9.10.dist-info/METADATA +422 -0
- git_repokit-0.9.10.dist-info/RECORD +174 -0
- git_repokit-0.9.10.dist-info/WHEEL +5 -0
- git_repokit-0.9.10.dist-info/entry_points.txt +2 -0
- git_repokit-0.9.10.dist-info/licenses/LICENSE +9 -0
- git_repokit-0.9.10.dist-info/top_level.txt +1 -0
- repokit/__init__.py +8 -0
- repokit/__main__.py +12 -0
- repokit/__version__.py +4 -0
- repokit/_vendor/__init__.py +0 -0
- repokit/_vendor/ailib/__init__.py +1 -0
- repokit/_vendor/ailib/analyzer.py +177 -0
- repokit/_vendor/ailib/backend.py +95 -0
- repokit/_vendor/ailib/cache.py +97 -0
- repokit/_vendor/ailib/parsers.py +52 -0
- repokit/_vendor/ailib/transports/__init__.py +16 -0
- repokit/_vendor/ailib/transports/cli.py +312 -0
- repokit/_vendor/ailib/transports/openai_compat.py +383 -0
- repokit/_vendor/ailib/transports/prompt_file.py +41 -0
- repokit/_vendor/ailib/types.py +224 -0
- repokit/auth_integration.py +244 -0
- repokit/badges.py +183 -0
- repokit/branch_utils.py +364 -0
- repokit/cli.py +2997 -0
- repokit/common_kit.py +202 -0
- repokit/config.py +686 -0
- repokit/defaults.py +220 -0
- repokit/directory_analyzer.py +1244 -0
- repokit/directory_profiles.py +423 -0
- repokit/git_config.py +370 -0
- repokit/github_create.py +142 -0
- repokit/github_setup.py +248 -0
- repokit/guardrails.py +707 -0
- repokit/history_cleaner.py +449 -0
- repokit/history_protection.py +434 -0
- repokit/hooks/pre-commit-guardrails +122 -0
- repokit/hooks/pre-commit-template +28 -0
- repokit/layers.py +510 -0
- repokit/licenses/0bsd.txt +14 -0
- repokit/licenses/LICENSES.md +27 -0
- repokit/licenses/__init__.py +142 -0
- repokit/licenses/agpl-3.0.txt +661 -0
- repokit/licenses/all-rights-reserved.txt +10 -0
- repokit/licenses/apache-2.0.txt +201 -0
- repokit/licenses/bsd-2-clause.txt +24 -0
- repokit/licenses/bsd-3-clause.txt +28 -0
- repokit/licenses/bsl-1.0.txt +23 -0
- repokit/licenses/cc-by-4.0.txt +395 -0
- repokit/licenses/cc-by-sa-4.0.txt +427 -0
- repokit/licenses/cc0-1.0.txt +121 -0
- repokit/licenses/epl-2.0.txt +277 -0
- repokit/licenses/gpl-2.0.txt +339 -0
- repokit/licenses/gpl-3.0.txt +674 -0
- repokit/licenses/isc.txt +15 -0
- repokit/licenses/lgpl-2.1.txt +504 -0
- repokit/licenses/lgpl-3.0.txt +165 -0
- repokit/licenses/mit.txt +21 -0
- repokit/licenses/mpl-2.0.txt +373 -0
- repokit/licenses/unlicense.txt +24 -0
- repokit/readme_blocks.py +112 -0
- repokit/remote_integration.py +627 -0
- repokit/repo_manager.py +2796 -0
- repokit/template_engine.py +391 -0
- repokit/templates/ai/claude/CLAUDE-nested.md.template +25 -0
- repokit/templates/ai/claude/CLAUDE.md.template +98 -0
- repokit/templates/ai/claude/instructions/step1_context_rebuilder.md +33 -0
- repokit/templates/ai/claude/instructions/step2_dev_workflow_process.md +67 -0
- repokit/templates/ai/claude/instructions/step3_context_bridge.md +52 -0
- repokit/templates/common/CHANGELOG.md.template +15 -0
- repokit/templates/common/CONTRIBUTING.md.template +31 -0
- repokit/templates/common/README.md.template +30 -0
- repokit/templates/common/gitattributes.template +17 -0
- repokit/templates/common/gitignore.template +141 -0
- repokit/templates/extras/FUNDING.yml.template +13 -0
- repokit/templates/extras/ROADMAP.md.template +17 -0
- repokit/templates/extras/dependabot.yml.template +22 -0
- repokit/templates/extras/one-offs-README.md.template +28 -0
- repokit/templates/extras/platform-support.md.template +11 -0
- repokit/templates/extras/pull_request_template.md.template +25 -0
- repokit/templates/extras/release-template.md.template +34 -0
- repokit/templates/extras/settings.json.template +11 -0
- repokit/templates/extras/stale.yml.template +14 -0
- repokit/templates/github/CODEOWNERS.template +11 -0
- repokit/templates/github/ISSUE_TEMPLATE/bug-report.md.template +29 -0
- repokit/templates/github/ISSUE_TEMPLATE/feature-request.md.template +19 -0
- repokit/templates/github/issues/notes.md.template +18 -0
- repokit/templates/github/issues/roadmap.md.template +15 -0
- repokit/templates/hooks/pre-commit.template +64 -0
- repokit/templates/layers/archetypes/any/claude-plugin/.claude-plugin/marketplace.json +20 -0
- repokit/templates/layers/archetypes/any/claude-plugin/plugin/.claude-plugin/plugin.json +15 -0
- repokit/templates/layers/archetypes/any/claude-plugin/plugin/README.md +28 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.github/workflows/ci-c-cpp.yml +24 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.repokit-common.toml.rkappend +4 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/launch.json +29 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/tasks.json +26 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakeLists.txt +47 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakePresets.json +23 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/cmake/GenerateStamp.cmake +45 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/src/main.cpp +27 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/tests/test_version.cpp +20 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/vcpkg.json +5 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/.vscode/launch.json +27 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/Makefile +10 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/build.bat +7 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/src/__RK_SNAKE__.c +26 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/launch.json +17 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/tasks.json +20 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.sln +19 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.vcxproj +41 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/src/main.cpp +26 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/.repokit-common.toml +28 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/.vscode/launch.json +34 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/hooks.json +15 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/scripts/session-start.mjs +23 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/tests/manifest.test.js +30 -0
- repokit/templates/layers/archetypes/js-ts/node/.repokit-common.toml.rkappend +6 -0
- repokit/templates/layers/archetypes/js-ts/node/package.json +13 -0
- repokit/templates/layers/archetypes/js-ts/node-package/.github/workflows/release.yml +39 -0
- repokit/templates/layers/archetypes/js-ts/node-package/package.json +32 -0
- repokit/templates/layers/archetypes/js-ts/node-package/src/index.ts +13 -0
- repokit/templates/layers/archetypes/js-ts/node-package/tests/index.test.ts +11 -0
- repokit/templates/layers/archetypes/js-ts/script/.vscode/launch.json +34 -0
- repokit/templates/layers/archetypes/js-ts/script/__RK_PROJECT__.js +37 -0
- repokit/templates/layers/archetypes/js-ts/script/tests/version.test.js +26 -0
- repokit/templates/layers/archetypes/js-ts/ts/package.json +18 -0
- repokit/templates/layers/archetypes/js-ts/ts/tsconfig.json +15 -0
- repokit/templates/layers/archetypes/python/claude-plugin/.github/workflows/ci-python.yml +44 -0
- repokit/templates/layers/archetypes/python/claude-plugin/.repokit-common.toml +21 -0
- repokit/templates/layers/archetypes/python/claude-plugin/.vscode/launch.json +33 -0
- repokit/templates/layers/archetypes/python/claude-plugin/VERSION +1 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/hooks.json +15 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/run-hook.mjs +42 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/session_start.py +31 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/tests/test_manifest.py +42 -0
- repokit/templates/layers/archetypes/python/pypi/.github/workflows/release.yml +63 -0
- repokit/templates/layers/archetypes/python/pypi/.vscode/launch.json +43 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__init__.py +5 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__main__.py +6 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/_version.py +96 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/cli.py +18 -0
- repokit/templates/layers/archetypes/python/pypi/pyproject.toml +71 -0
- repokit/templates/layers/archetypes/python/pypi/tests/conftest.py +11 -0
- repokit/templates/layers/archetypes/python/pypi/tests/test_version.py +50 -0
- repokit/templates/layers/archetypes/python/script/.github/workflows/ci-python.yml +43 -0
- repokit/templates/layers/archetypes/python/script/.repokit-common.toml +14 -0
- repokit/templates/layers/archetypes/python/script/.vscode/launch.json +33 -0
- repokit/templates/layers/archetypes/python/script/VERSION +1 -0
- repokit/templates/layers/archetypes/python/script/__RK_PROJECT__.py +40 -0
- repokit/templates/layers/archetypes/python/script/tests/test_version.py +25 -0
- repokit/templates/layers/archetypes/python/setuppy/requirements.txt +8 -0
- repokit/templates/layers/archetypes/python/setuppy/setup.py +23 -0
- repokit/templates/layers/languages/c-cpp/.gitignore.rkappend +20 -0
- repokit/templates/layers/languages/c-cpp/.repokit-common.toml +7 -0
- repokit/templates/layers/languages/c-cpp/include/__RK_SNAKE__/version.h +20 -0
- repokit/templates/layers/languages/c-cpp/src/app.rc +25 -0
- repokit/templates/layers/languages/js-ts/.editorconfig +15 -0
- repokit/templates/layers/languages/js-ts/.github/workflows/ci-js-ts.yml +41 -0
- repokit/templates/layers/languages/js-ts/.gitignore.rkappend +13 -0
- repokit/templates/layers/languages/js-ts/.repokit-common.toml +19 -0
- repokit/templates/layers/languages/js-ts/.vscode/launch.json +24 -0
- repokit/templates/layers/languages/js-ts/VERSION +1 -0
- repokit/templates/layers/languages/python/.github/workflows/ci-python.yml +76 -0
- repokit/templates/layers/languages/python/.gitignore.rkappend +15 -0
- repokit/templates/layers/languages/python/.vscode/launch.json +33 -0
- repokit/templates/layers/manifest.toml +302 -0
- repokit/templates/vault/CLAUDE.md +29 -0
- repokit/templates/vault/claude/_maps/MOC-project.md +15 -0
- repokit/templates/vault/claude/_oracle/backlinks.md +3 -0
- repokit/templates/vault/claude/_oracle/concepts.md +12 -0
- repokit/templates/vault/claude/_oracle/manifest.md +10 -0
- repokit/templates/vault/claude/_todo/ACTIVE-WORK.md +18 -0
- repokit/templates/vault/claude/_todo/OPEN-LOOPS.md +14 -0
- repokit/utils.py +316 -0
- 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
|