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/layers.py ADDED
@@ -0,0 +1,510 @@
1
+ """Code-type template layers: the second of git-repokit's two template systems.
2
+
3
+ The structure system (repo_manager._generate_template_files) writes the files
4
+ every project gets, from *.template files with $var placeholders. This module
5
+ lays the code type on top: a manifest-driven walk over layer folders under
6
+ repokit/templates/layers/ -- a language's layer, then an archetype's -- where
7
+ a later file replaces an earlier one, a file named <name>.rkappend is appended
8
+ to <name> (including a file the structure system already wrote), __RK_NAME__
9
+ tokens are replaced in file names and text contents, and workflow files are
10
+ copied byte for byte (a token inside one is an error, because GitHub Actions'
11
+ own ${{ }} lives there). Both systems render through one TemplateEngine and
12
+ one context (template_engine.token_context), so a project has one set of names.
13
+
14
+ Ported from the proof in git-repokit-common,
15
+ tests/one-offs/thinking/poc-templates-c-cpp/assemble.py (8/8 arms on
16
+ 2026-10-07); design: the Phase 2 v2 analysis of 2026-10-09 in the vault.
17
+
18
+ The manifest (repokit/templates/layers/manifest.toml):
19
+
20
+ [languages.python]
21
+ layers = ["languages/python"]
22
+ default = "python/setuppy" # the archetype a bare --language gets
23
+
24
+ [languages.javascript]
25
+ stub = true # named, announced, not built yet
26
+ description = "planned as the js-ts layer"
27
+
28
+ [archetypes."python/setuppy"]
29
+ languages = ["python"] # implied when --archetype names it
30
+ layers = ["archetypes/python/setuppy"]
31
+ description = "setup.py and requirements.txt (git-repokit's output before 0.7)"
32
+ """
33
+
34
+ import difflib
35
+ import os
36
+ import re
37
+ from dataclasses import dataclass, field
38
+ from pathlib import Path
39
+ from typing import Any, Dict, List, Optional, Tuple
40
+
41
+ from .template_engine import TemplateEngine, RK_TOKEN
42
+
43
+ MANIFEST = "manifest.toml"
44
+ APPEND = ".rkappend"
45
+ WORKFLOWS = ".github/workflows/"
46
+ GENERIC = "generic" # no code-type layers at all
47
+ LAYERS_DIR = "layers" # under the templates directory
48
+ ARCHETYPES_DIR = "archetypes"
49
+ NOT_A_PRESET = "assembled from layers; not a tested preset"
50
+ UNRANKED = 99 # a layer folder with no [layers] entry sorts last, in the order given
51
+ # Files only the primary language writes in a multi-language project (0.9.2):
52
+ # VS Code reads one of each, and merging two languages' JSON is out of scope.
53
+ PRIMARY_ONLY = (".vscode/launch.json", ".vscode/tasks.json")
54
+
55
+
56
+ class LayerError(Exception):
57
+ """A language or archetype that cannot be assembled: unknown, a stub, or a
58
+ broken layer tree. The message is for the person; create stops before
59
+ `git init` when it is raised."""
60
+
61
+
62
+ @dataclass
63
+ class Resolution:
64
+ layers: List[str] = field(default_factory=list) # layer folders in apply order
65
+ languages: List[str] = field(default_factory=list)
66
+ archetype: Optional[str] = None # the preset the request equals, or None
67
+ tokens: List[str] = field(default_factory=list) # what --with asked for, as given
68
+ names: List[str] = field(default_factory=list) # qualified layer names in apply order
69
+ notes: List[str] = field(default_factory=list) # e.g. NOT_A_PRESET
70
+
71
+
72
+ def _load_toml(path: Path) -> Dict[str, Any]:
73
+ try:
74
+ import tomllib
75
+ except ImportError: # Python < 3.11
76
+ try:
77
+ import tomli as tomllib # type: ignore
78
+ except ImportError:
79
+ raise LayerError(f"{path} needs a TOML parser: Python 3.11+ or the tomli package") from None
80
+ try:
81
+ with open(path, "rb") as f:
82
+ return tomllib.load(f)
83
+ except (tomllib.TOMLDecodeError, OSError) as e:
84
+ raise LayerError(f"cannot read {path}: {e}") from None
85
+
86
+
87
+ def layers_root(engine: Optional[TemplateEngine] = None) -> Optional[Path]:
88
+ """The layers folder of the templates directory the engine found, or None."""
89
+ engine = engine or TemplateEngine()
90
+ if not engine.templates_dir:
91
+ return None
92
+ root = Path(engine.templates_dir) / LAYERS_DIR
93
+ return root if (root / MANIFEST).is_file() else None
94
+
95
+
96
+ def load_manifest(root: Path) -> Dict[str, Any]:
97
+ data = _load_toml(root / MANIFEST)
98
+ data.setdefault("languages", {})
99
+ data.setdefault("archetypes", {})
100
+ data.setdefault("layers", {})
101
+ data.setdefault("facets", {})
102
+ return data
103
+
104
+
105
+ def known_languages(manifest: Dict[str, Any]) -> List[str]:
106
+ """Every language the manifest names (stubs included), plus generic. A
107
+ pseudo-language (`pseudo = true`, such as `any`, which holds layer files
108
+ shared by several languages) is not a language a project can have."""
109
+ return [GENERIC] + sorted(k for k, v in manifest["languages"].items() if not v.get("pseudo"))
110
+
111
+
112
+ def is_pseudo(manifest: Dict[str, Any], name: str) -> bool:
113
+ return bool(manifest["languages"].get(name, {}).get("pseudo"))
114
+
115
+
116
+ def known_archetypes(manifest: Dict[str, Any]) -> List[str]:
117
+ """The presets: what --with <lang>/<name> expands to."""
118
+ return sorted(manifest["archetypes"])
119
+
120
+
121
+ def known_layers(manifest: Dict[str, Any]) -> List[str]:
122
+ """Every [layers] entry, qualified (stubs included)."""
123
+ return sorted(manifest["layers"])
124
+
125
+
126
+ def layer_folder(manifest: Dict[str, Any], key: str) -> str:
127
+ """The folder a [layers] entry stands for: archetypes/<lang>/<name>, by convention."""
128
+ return f"{ARCHETYPES_DIR}/{key}"
129
+
130
+
131
+ def facet_rank(manifest: Dict[str, Any], facet: Optional[str]) -> int:
132
+ f = manifest["facets"].get(facet or "", {})
133
+ return f.get("rank", UNRANKED)
134
+
135
+
136
+ def facet_exclusive(manifest: Dict[str, Any], facet: Optional[str]) -> bool:
137
+ return bool(manifest["facets"].get(facet or "", {}).get("exclusive", False))
138
+
139
+
140
+ def parse_languages(value) -> List[str]:
141
+ """A list, or a comma-separated string, or a list of either -> names in
142
+ the order given, without repeats or blanks."""
143
+ items: List[str] = []
144
+ for part in (value if isinstance(value, (list, tuple)) else [value or ""]):
145
+ for name in str(part).split(","):
146
+ name = name.strip()
147
+ if name and name not in items:
148
+ items.append(name)
149
+ return items
150
+
151
+
152
+ def _suggest(token: str, choices: List[str]) -> str:
153
+ close = difflib.get_close_matches(token, choices, n=3, cutoff=0.6)
154
+ return f"; did you mean {', '.join(close)}?" if close else ""
155
+
156
+
157
+ def _check_language(manifest: Dict[str, Any], name: str) -> None:
158
+ entry = manifest["languages"].get(name)
159
+ if entry is None:
160
+ raise LayerError(f"unknown language {name!r}; known: {', '.join(known_languages(manifest))}")
161
+ if entry.get("stub"):
162
+ raise LayerError(f"language {name} is not built yet: {entry.get('description', '')}".rstrip(": "))
163
+
164
+
165
+ def _preset_layers(manifest: Dict[str, Any], key: str) -> List[Tuple[str, Optional[str]]]:
166
+ """A preset's (folder, qualified layer name or None) pairs, after the stub check."""
167
+ preset = manifest["archetypes"][key]
168
+ if preset.get("stub"):
169
+ raise LayerError(f"archetype {key} is not built yet: {preset.get('description', '')}".rstrip(": "))
170
+ out = []
171
+ for folder in preset.get("layers", []):
172
+ name = folder[len(ARCHETYPES_DIR) + 1:] if folder.startswith(ARCHETYPES_DIR + "/") else None
173
+ out.append((folder, name if name in manifest["layers"] else None))
174
+ return out
175
+
176
+
177
+ def _layer(manifest: Dict[str, Any], key: str) -> Tuple[str, str]:
178
+ entry = manifest["layers"][key]
179
+ if entry.get("stub"):
180
+ raise LayerError(f"layer {key} is not built yet: {entry.get('description', '')}".rstrip(": "))
181
+ return layer_folder(manifest, key), key
182
+
183
+
184
+ def resolve(manifest: Dict[str, Any], languages, with_tokens=None, _recognise: bool = True) -> Resolution:
185
+ """The ordered layers for these languages and these --with tokens.
186
+
187
+ The eight rules (Phase 3 design, D2): tokens are split on commas and
188
+ lower-cased; a preset (an [archetypes] key) expands to its layers; a bare
189
+ name is looked up across layers and presets, narrowed by the languages
190
+ listed or already implied, and refused with every qualified form when it
191
+ stays ambiguous; every layer implies its language and whatever its entry
192
+ `implies`, transitively; two layers in one exclusive facet of one language
193
+ are refused naming the pair and the facet; layers are ordered by facet
194
+ rank, then by the order given, after the languages' own layers; a list
195
+ that equals a preset's expansion is reported as that preset, any other
196
+ list carries the NOT_A_PRESET note; with no tokens the FIRST language's
197
+ default preset applies (the first language is the project's primary one);
198
+ stubs refuse with their description; an unknown name suggests close ones.
199
+ generic means no layers.
200
+ """
201
+ table = manifest["languages"]
202
+ presets = manifest["archetypes"]
203
+ layer_table = manifest["layers"]
204
+ langs = [name for name in parse_languages(languages) if name != GENERIC]
205
+ tokens: List[str] = []
206
+ for t in parse_languages(with_tokens):
207
+ if t.lower() not in tokens:
208
+ tokens.append(t.lower())
209
+ for name in langs:
210
+ _check_language(manifest, name)
211
+ if is_pseudo(manifest, name):
212
+ raise LayerError(f"{name!r} holds layers shared by several languages; it is not a project language")
213
+ res = Resolution(languages=langs, tokens=list(tokens))
214
+
215
+ chosen: List[Tuple[str, Optional[str]]] = [] # (folder, qualified name or None), in the order given
216
+ presets_used: List[str] = []
217
+
218
+ def add_language(name: str) -> None:
219
+ if name not in langs:
220
+ _check_language(manifest, name)
221
+ if is_pseudo(manifest, name):
222
+ return # a shared layer's folder prefix, never a project language
223
+ langs.append(name)
224
+
225
+ def add_preset(key: str) -> None:
226
+ presets_used.append(key)
227
+ for lang_name in presets[key].get("languages", []) or [key.split("/")[0]]:
228
+ add_language(lang_name)
229
+ for pair in _preset_layers(manifest, key):
230
+ if pair not in chosen:
231
+ chosen.append(pair)
232
+
233
+ def add_layer(key: str) -> None:
234
+ pair = _layer(manifest, key)
235
+ add_language(key.split("/")[0])
236
+ if pair not in chosen:
237
+ chosen.append(pair)
238
+
239
+ # Pass 1: presets and qualified names; bare names that are unique anywhere.
240
+ # Pass 2: the bare names left, narrowed by the languages now known.
241
+ deferred: List[str] = []
242
+ for tok in tokens:
243
+ if tok in presets:
244
+ add_preset(tok)
245
+ elif "/" in tok:
246
+ if tok in layer_table:
247
+ add_layer(tok)
248
+ else:
249
+ known = sorted(set(presets) | set(layer_table))
250
+ raise LayerError(f"unknown archetype {tok!r}; known: {', '.join(known) or 'none'}{_suggest(tok, known)}")
251
+ else:
252
+ deferred.append(tok)
253
+ for tok in deferred:
254
+ candidates = _bare_candidates(manifest, tok)
255
+ if not candidates:
256
+ bare = sorted({k.split('/', 1)[1] for k in list(presets) + list(layer_table) if '/' in k})
257
+ raise LayerError(f"unknown name {tok!r}; known: {', '.join(bare) or 'none'}{_suggest(tok, bare + sorted(set(presets) | set(layer_table)))}")
258
+ if len(candidates) > 1:
259
+ narrowed = [c for c in candidates if c.split("/")[0] in langs]
260
+ # With no language named at all, a candidate marked `primary` in the
261
+ # manifest settles a bare name (js-ts is the primary shape of a Claude
262
+ # plugin; the Python one is supported, not the default -- djdarcy,
263
+ # 2026-10-22). A named language that does not narrow stays ambiguous.
264
+ primary = [c for c in candidates if (presets.get(c) or layer_table.get(c) or {}).get("primary")]
265
+ if len(narrowed) == 1:
266
+ candidates = narrowed
267
+ elif not langs and len(primary) == 1:
268
+ candidates = primary
269
+ else:
270
+ raise LayerError(f"{tok!r} is ambiguous: {' or '.join(candidates)}")
271
+ key = candidates[0]
272
+ if key in presets:
273
+ add_preset(key)
274
+ else:
275
+ add_layer(key)
276
+
277
+ # Rule 4: imply, transitively (a layer implies its language; its entry may imply more).
278
+ i = 0
279
+ while i < len(chosen):
280
+ folder, name = chosen[i]
281
+ if name:
282
+ lang_name = name.split("/")[0]
283
+ for imp in layer_table[name].get("implies", []):
284
+ target = imp if "/" in imp else f"{lang_name}/{imp}"
285
+ if target in table: # a language
286
+ add_language(target)
287
+ elif target in layer_table:
288
+ pair = _layer(manifest, target)
289
+ if pair not in chosen:
290
+ chosen.append(pair)
291
+ else:
292
+ raise LayerError(f"layer {name} implies {imp!r}, which the manifest does not define")
293
+ i += 1
294
+
295
+ # Rule 5: exclusivity, per language and facet.
296
+ seen: Dict[Tuple[str, str], str] = {}
297
+ for folder, name in chosen:
298
+ if not name:
299
+ continue
300
+ facet = layer_table[name].get("facet")
301
+ if facet and facet_exclusive(manifest, facet):
302
+ slot = (name.split("/")[0], facet)
303
+ other = seen.get(slot)
304
+ if other and other != name:
305
+ a, b = other.split("/", 1)[1], name.split("/", 1)[1]
306
+ raise LayerError(f"'{a}' and '{b}' are both a {facet} for {slot[0]}; pick one")
307
+ seen[slot] = name
308
+
309
+ # Rule 7 (default) when nothing was asked.
310
+ if not tokens:
311
+ for name in langs[:1]: # the primary language's default only
312
+ default = table[name].get("default")
313
+ if default:
314
+ entry = presets.get(default)
315
+ if entry is None or entry.get("stub"):
316
+ raise LayerError(f"language {name} names default archetype {default!r}, which is {'unknown' if entry is None else 'a stub'}")
317
+ add_preset(default)
318
+ res.archetype = default
319
+
320
+ # Rule 6: order -- the languages' own layers first (in the order given), then by facet rank, then as given.
321
+ order: List[Tuple[str, Optional[str]]] = []
322
+ for name in langs:
323
+ for folder in table[name].get("layers", []):
324
+ order.append((folder, name))
325
+ ranked = sorted(enumerate(chosen), key=lambda ic: (facet_rank(manifest, layer_table.get(ic[1][1] or "", {}).get("facet")), ic[0]))
326
+ order += [pair for _, pair in ranked]
327
+ for folder, name in order:
328
+ if folder not in res.layers:
329
+ res.layers.append(folder)
330
+ res.names.append(name or folder)
331
+ res.languages = langs
332
+
333
+ # Rule 8: recognise a preset -- the WHOLE request must equal the preset's
334
+ # expansion, languages included (`--language python --with minimal` is a
335
+ # two-language composition no preset vouches for, even though `minimal`
336
+ # alone is one).
337
+ if tokens and _recognise:
338
+ for key in presets_used + [k for k in presets if k not in presets_used]:
339
+ if presets[key].get("stub"):
340
+ continue
341
+ try:
342
+ expansion = resolve(manifest, [], [key], _recognise=False).layers
343
+ except LayerError:
344
+ continue
345
+ if expansion == res.layers:
346
+ res.archetype = key
347
+ break
348
+ if res.archetype is None:
349
+ res.notes.append(NOT_A_PRESET)
350
+
351
+ # A language with a default preset that ended up with no layer of its own
352
+ # (the second of two languages and no --with, or a --with naming only the
353
+ # other language's layers) is said out loud rather than left bare
354
+ # (Q261022-02, answered 2026-10-22: the person refines; no second default).
355
+ for name in langs:
356
+ default = table[name].get("default")
357
+ if default and not any(f.startswith(f"{ARCHETYPES_DIR}/{name}/") for f in res.layers):
358
+ res.notes.append(f"{name} got no preset of its own; add --with {name}/<name> (see `repokit list {name}`)")
359
+ return res
360
+
361
+
362
+ def catalogue_lines(manifest: Dict[str, Any], language: Optional[str] = None) -> List[str]:
363
+ """`repokit list [LANG]`: per language, the presets (name = layers; planned
364
+ ones marked), then each facet with its choices and one-of / any-of."""
365
+ table = manifest["languages"]
366
+ if language and language not in table:
367
+ raise LayerError(f"unknown language {language!r}; known: {', '.join(known_languages(manifest))}")
368
+ facets = sorted(manifest["facets"].items(), key=lambda kv: kv[1].get("rank", UNRANKED))
369
+ lines: List[str] = []
370
+ for lang in sorted(table):
371
+ if language and lang != language:
372
+ continue
373
+ entry = table[lang]
374
+ if entry.get("pseudo"):
375
+ continue
376
+ head = f"{lang}:" + (f" (planned: {entry.get('description', '')})" if entry.get("stub") else "")
377
+ lines.append(head)
378
+ default = entry.get("default")
379
+ for key in sorted(k for k in manifest["archetypes"] if k.startswith(lang + "/")):
380
+ preset = manifest["archetypes"][key]
381
+ if preset.get("stub"):
382
+ lines.append(f" {key:28} (planned: {preset.get('description', '')})")
383
+ else:
384
+ parts = [f.split("/", 2)[-1] for f in preset.get("layers", [])]
385
+ mark = " <- default" if key == default else ""
386
+ lines.append(f" {key:28} = {', '.join(parts)}{mark}")
387
+ for facet, spec in facets:
388
+ if facet == "language":
389
+ continue
390
+ names = []
391
+ for key, l_entry in manifest["layers"].items():
392
+ if key.startswith(lang + "/") and l_entry.get("facet") == facet:
393
+ names.append(key.split("/", 1)[1] + (" (planned)" if l_entry.get("stub") else ""))
394
+ if names:
395
+ pick = "one of" if spec.get("exclusive") else "any of"
396
+ lines.append(f" {facet:10} {pick}: {', '.join(sorted(names))}")
397
+ return lines
398
+
399
+
400
+ def _bare_candidates(manifest: Dict[str, Any], tok: str) -> List[str]:
401
+ """Qualified keys whose bare name is tok: one per language, a preset
402
+ preferred over a layer of the same name (the preset is the tested one)."""
403
+ by_lang: Dict[str, str] = {}
404
+ for key in manifest["layers"]:
405
+ lang, name = key.split("/", 1)
406
+ if name == tok and not is_pseudo(manifest, lang):
407
+ by_lang.setdefault(lang, key)
408
+ for key in manifest["archetypes"]:
409
+ lang, name = key.split("/", 1)
410
+ if name == tok:
411
+ by_lang[lang] = key
412
+ return [by_lang[l] for l in sorted(by_lang)]
413
+
414
+
415
+ def _is_text(data: bytes) -> bool:
416
+ return b"\0" not in data[:8192]
417
+
418
+
419
+ def layer_language(folder: str) -> Optional[str]:
420
+ """The language a layer folder belongs to: languages/<lang> or archetypes/<lang>/<name>."""
421
+ parts = folder.split("/")
422
+ return parts[1] if len(parts) >= 2 and parts[0] in ("languages", ARCHETYPES_DIR) else None
423
+
424
+
425
+ def assemble(root: Path, layers: List[str], dest: str, context: Dict[str, Any],
426
+ engine: Optional[TemplateEngine] = None, preserve_existing: bool = False,
427
+ logger=None, primary_language: Optional[str] = None,
428
+ skipped: Optional[List[str]] = None) -> List[str]:
429
+ """Lay the layers down under ``dest``. Returns the files written or appended,
430
+ as dest-relative POSIX paths. ``context`` is token_context(...).
431
+
432
+ With ``primary_language`` set (a project of two or more languages), the
433
+ files in PRIMARY_ONLY come from that language's layers and the shared
434
+ `any` ones only: VS Code reads one launch.json, and merging two languages'
435
+ debug configurations is out of scope (0.9.2). Each file left out is
436
+ reported in ``skipped`` as "<language>: <path>"."""
437
+ engine = engine or TemplateEngine()
438
+ files: Dict[str, bytes] = {} # template-relative path (tokens unresolved) -> content
439
+ appends: Dict[str, bytes] = {} # appends whose target no layer provides: a file already on disk
440
+
441
+ for layer in layers:
442
+ base = root / layer
443
+ if not base.is_dir():
444
+ raise LayerError(f"layer folder missing: {layer} (under {root})")
445
+ lang = layer_language(layer)
446
+ for src in sorted(p for p in base.rglob("*") if p.is_file()):
447
+ # Bytecode beside the template sources (a syntax check compiled
448
+ # them) is not part of any layer, and carries the tokens inside.
449
+ if "__pycache__" in src.parts or src.suffix in (".pyc", ".pyo"):
450
+ continue
451
+ rel = src.relative_to(base).as_posix()
452
+ if primary_language and rel in PRIMARY_ONLY and lang not in (primary_language, "any", None):
453
+ if skipped is not None and f"{lang}: {rel}" not in skipped:
454
+ skipped.append(f"{lang}: {rel}")
455
+ continue
456
+ data = src.read_bytes()
457
+ if rel.endswith(APPEND):
458
+ target = rel[: -len(APPEND)]
459
+ if target in files:
460
+ files[target] = _joined(files[target], data)
461
+ else:
462
+ appends[target] = _joined(appends.get(target, b""), data) if target in appends else data
463
+ else:
464
+ files[rel] = data # a later layer's file replaces an earlier one
465
+
466
+ def out_path(rel: str) -> str:
467
+ resolved = engine.render_bytes(rel.encode("utf-8"), context, dollar=False).decode("utf-8")
468
+ if ".." in Path(resolved).parts:
469
+ raise LayerError(f"a layer path escapes the project: {rel}")
470
+ return resolved
471
+
472
+ written: List[str] = []
473
+ for rel, data in files.items():
474
+ is_workflow = rel.startswith(WORKFLOWS)
475
+ if is_workflow and RK_TOKEN.search(data.decode("latin-1")):
476
+ raise LayerError(f"token in workflow file {rel}: workflow files must stay placeholder-free")
477
+ target = out_path(rel)
478
+ full = os.path.join(dest, target)
479
+ if preserve_existing and os.path.exists(full):
480
+ if logger:
481
+ logger.info(f"Preserving existing file: {target}")
482
+ continue
483
+ if not is_workflow and _is_text(data):
484
+ data = engine.render_bytes(data, context, dollar=False)
485
+ os.makedirs(os.path.dirname(full) or dest, exist_ok=True)
486
+ with open(full, "wb") as f:
487
+ f.write(data)
488
+ written.append(target)
489
+
490
+ for rel, data in appends.items():
491
+ target = out_path(rel)
492
+ full = os.path.join(dest, target)
493
+ if preserve_existing and os.path.exists(full):
494
+ if logger:
495
+ logger.info(f"Preserving existing file, not appending: {target}")
496
+ continue
497
+ data = engine.render_bytes(data, context, dollar=False) if _is_text(data) else data
498
+ os.makedirs(os.path.dirname(full) or dest, exist_ok=True)
499
+ existing = open(full, "rb").read() if os.path.exists(full) else b""
500
+ with open(full, "wb") as f:
501
+ f.write(_joined(existing, data))
502
+ written.append(target)
503
+ return written
504
+
505
+
506
+ def _joined(head: bytes, tail: bytes) -> bytes:
507
+ """head + tail, with a newline between when head has none at its end."""
508
+ if head and not head.endswith((b"\n", b"\r\n")):
509
+ head += b"\n"
510
+ return head + tail
@@ -0,0 +1,14 @@
1
+ BSD Zero Clause License
2
+
3
+ Copyright (c) __RK_YEAR__ __RK_HOLDER__
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
10
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
12
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
13
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
14
+ PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,27 @@
1
+ # The licence catalogue: where each text came from, and its fingerprint
2
+
3
+ Source: `github/choosealicense.com`, `_licenses/<id>.txt`, commit `f66c5dd01866041688edd87f658b6d5ac3304f1b` (branch gh-pages, fetched 2026-10-23 by `tests/one-offs/thinking/p4-fetch-licenses/fetch.py`). Each file's YAML front matter was stripped and the placeholders `[year]`, `[yyyy]`, `[fullname]`, `[name of copyright owner]` became `__RK_YEAR__` and `__RK_HOLDER__`; nothing else changed. `all-rights-reserved.txt` is authored here, not fetched. The "or-later" variants (`gpl-3.0-or-later`, `lgpl-3.0-or-later`, `agpl-3.0-or-later`) are not files: `repokit.licenses` renders them from the base text with a notice line, because the licence text is identical and only the project's notice says "or later".
4
+
5
+ `tests/test_licenses.py` checks every shipped file against its shipped sha256; a changed file without a new row here fails.
6
+
7
+ | id | SPDX | title | sha256 as received | sha256 as shipped | tokens |
8
+ |---|---|---|---|---|---|
9
+ | 0bsd | 0BSD | BSD Zero Clause License | 749c160d5a8290da254f5fd02a510c6a234df5aea2f4f0711f309f2d9a821899 | e41dbf5f8aa40ed502ea4ab4023de21795d556d9a341734f2d24872dc4973d0c | holder, year |
10
+ | agpl-3.0 | AGPL-3.0 | GNU Affero General Public License v3.0 | 2a1fda78234b71502e2cf8035021b5592f5eba7e8107228aca4d95bba645e397 | 8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef | none |
11
+ | apache-2.0 | Apache-2.0 | Apache License 2.0 | 609e2cb599f84aaa41d8ef29d8fdb04d164fab22e8d9292ca34a599d0f56a338 | 7ab3d1eca77d69cf60cb1e56379fa9a619b6fc300155904e1f8539ee6155e9c4 | holder, year |
12
+ | bsd-2-clause | BSD-2-Clause | BSD 2-Clause "Simplified" License | d8237d165c176416a8ab938bf7e120d8f345098163fcec9017c5652f467fecaf | fc29f42f5b8468afa3811a721ec1dd5c629f725e04785597ff4b7219887fe98e | holder, year |
13
+ | bsd-3-clause | BSD-3-Clause | BSD 3-Clause "New" or "Revised" License | 732e653d6b57d83ee9bd21ff53c6dd1eb332628cefd05df4b99d523727dff664 | f5d1fc68fbc06c286a11125154784b0b21fab066a1574a29f928f7c564ec8461 | holder, year |
14
+ | bsl-1.0 | BSL-1.0 | Boost Software License 1.0 | f8f6e066934a2365c12debd413b413caa0152791c87c0b16a12c8a65c3b92f9a | c9bff75738922193e67fa726fa225535870d2aa1059f91452c411736284ad566 | none |
15
+ | cc-by-4.0 | CC-BY-4.0 | Creative Commons Attribution 4.0 International | 736763bf08829e36266b8a3bebcc041ac68575424e976490671a7e6b43a525cf | f5b745ef98087f531e719ee8ca6a96809444573ecc7173c6fa68eaad39b3cc3f | none |
16
+ | cc-by-sa-4.0 | CC-BY-SA-4.0 | Creative Commons Attribution Share Alike 4.0 International | 8f85091fc554652d297119db8a092d8fc3d942ef3d768bd674decf48067a151f | 2003829cf643d61b00a76088dd31bb11b8029546f24c56b39fd492adb9281483 | none |
17
+ | cc0-1.0 | CC0-1.0 | Creative Commons Zero v1.0 Universal | db8278dfdfc1f73224f9f3762922c378344ca0443d36800baa61b3aaf31ef0e2 | a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499 | none |
18
+ | epl-2.0 | EPL-2.0 | Eclipse Public License 2.0 | 93c73b1678467af856e43dc4e7cc346991ab187b8da09b0e1a0cb010f6401ccd | 8c349f80764d0648e645f41ef23772a70c995a0924b5235f735f4a3d09df127c | none |
19
+ | gpl-2.0 | GPL-2.0 | GNU General Public License v2.0 | b7c21f2a2d0a1558458adbd90b9380d2514cebf970d12f26f84f862571c30ee2 | 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 | none |
20
+ | gpl-3.0 | GPL-3.0 | GNU General Public License v3.0 | e9c13804a52b8aea2e30033764d1b19f98369bae58028d9d1eb6b84e42b888bc | 3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986 | none |
21
+ | isc | ISC | ISC License | 8ea92c9e801b11e1303173c3ce3fc566bbe1ede96190aaffbc5a0c97712146c8 | 491a83f2c57434db8e97565ec680a129960783ef2c5c10d3c1b9734bf0b4ee79 | holder, year |
22
+ | lgpl-2.1 | LGPL-2.1 | GNU Lesser General Public License v2.1 | 0094ff7d2b4451d2231cd0c20b2798574c29bc81ef740d719a9909aabd86d633 | 20c17d8b8c48a600800dfd14f95d5cb9ff47066a9641ddeab48dc54aec96e331 | none |
23
+ | lgpl-3.0 | LGPL-3.0 | GNU Lesser General Public License v3.0 | 0b2e8a7c7a977908fa0bfaa2e2a7a54e3fd73676662e9fedae264db2916e0135 | e3a994d82e644b03a792a930f574002658412f62407f5fee083f2555c5f23118 | none |
24
+ | mit | MIT | MIT License | e6919db13cacb3300f918d1048d996d0f4506e1f110b7caa21f31ea7ca3ef5b8 | e3a0a9c39448343918a8bcbbf2dc5a0bce37cd0a937045ef6eeb869653fc0ec7 | holder, year |
25
+ | mpl-2.0 | MPL-2.0 | Mozilla Public License 2.0 | 0e03b8171b7b830a6a416b403eced17d39baed5b3a60563971f8b27aaa4f29e6 | 3f3d9e0024b1921b067d6f7f88deb4a60cbe7a78e76c64e3f1d7fc3b779b9d04 | none |
26
+ | unlicense | Unlicense | The Unlicense | ddc2e070df0bb25ce951b0954d9c1b5d22b17f150d58eac66bda653d044bb5ed | 6b0382b16279f26ff69014300541967a356a666eb0b91b422f6862f6b7dad17e | none |
27
+ | all-rights-reserved | LicenseRef-AllRightsReserved | All rights reserved (authored here) | - | 1025ba5f1f4d5a5583b50ea511aa986aa557b1a417ef92f52fe0696b4e5565c9 | holder, year |