git-repokit 0.9.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. git_repokit-0.9.10.dist-info/METADATA +422 -0
  2. git_repokit-0.9.10.dist-info/RECORD +174 -0
  3. git_repokit-0.9.10.dist-info/WHEEL +5 -0
  4. git_repokit-0.9.10.dist-info/entry_points.txt +2 -0
  5. git_repokit-0.9.10.dist-info/licenses/LICENSE +9 -0
  6. git_repokit-0.9.10.dist-info/top_level.txt +1 -0
  7. repokit/__init__.py +8 -0
  8. repokit/__main__.py +12 -0
  9. repokit/__version__.py +4 -0
  10. repokit/_vendor/__init__.py +0 -0
  11. repokit/_vendor/ailib/__init__.py +1 -0
  12. repokit/_vendor/ailib/analyzer.py +177 -0
  13. repokit/_vendor/ailib/backend.py +95 -0
  14. repokit/_vendor/ailib/cache.py +97 -0
  15. repokit/_vendor/ailib/parsers.py +52 -0
  16. repokit/_vendor/ailib/transports/__init__.py +16 -0
  17. repokit/_vendor/ailib/transports/cli.py +312 -0
  18. repokit/_vendor/ailib/transports/openai_compat.py +383 -0
  19. repokit/_vendor/ailib/transports/prompt_file.py +41 -0
  20. repokit/_vendor/ailib/types.py +224 -0
  21. repokit/auth_integration.py +244 -0
  22. repokit/badges.py +183 -0
  23. repokit/branch_utils.py +364 -0
  24. repokit/cli.py +2997 -0
  25. repokit/common_kit.py +202 -0
  26. repokit/config.py +686 -0
  27. repokit/defaults.py +220 -0
  28. repokit/directory_analyzer.py +1244 -0
  29. repokit/directory_profiles.py +423 -0
  30. repokit/git_config.py +370 -0
  31. repokit/github_create.py +142 -0
  32. repokit/github_setup.py +248 -0
  33. repokit/guardrails.py +707 -0
  34. repokit/history_cleaner.py +449 -0
  35. repokit/history_protection.py +434 -0
  36. repokit/hooks/pre-commit-guardrails +122 -0
  37. repokit/hooks/pre-commit-template +28 -0
  38. repokit/layers.py +510 -0
  39. repokit/licenses/0bsd.txt +14 -0
  40. repokit/licenses/LICENSES.md +27 -0
  41. repokit/licenses/__init__.py +142 -0
  42. repokit/licenses/agpl-3.0.txt +661 -0
  43. repokit/licenses/all-rights-reserved.txt +10 -0
  44. repokit/licenses/apache-2.0.txt +201 -0
  45. repokit/licenses/bsd-2-clause.txt +24 -0
  46. repokit/licenses/bsd-3-clause.txt +28 -0
  47. repokit/licenses/bsl-1.0.txt +23 -0
  48. repokit/licenses/cc-by-4.0.txt +395 -0
  49. repokit/licenses/cc-by-sa-4.0.txt +427 -0
  50. repokit/licenses/cc0-1.0.txt +121 -0
  51. repokit/licenses/epl-2.0.txt +277 -0
  52. repokit/licenses/gpl-2.0.txt +339 -0
  53. repokit/licenses/gpl-3.0.txt +674 -0
  54. repokit/licenses/isc.txt +15 -0
  55. repokit/licenses/lgpl-2.1.txt +504 -0
  56. repokit/licenses/lgpl-3.0.txt +165 -0
  57. repokit/licenses/mit.txt +21 -0
  58. repokit/licenses/mpl-2.0.txt +373 -0
  59. repokit/licenses/unlicense.txt +24 -0
  60. repokit/readme_blocks.py +112 -0
  61. repokit/remote_integration.py +627 -0
  62. repokit/repo_manager.py +2796 -0
  63. repokit/template_engine.py +391 -0
  64. repokit/templates/ai/claude/CLAUDE-nested.md.template +25 -0
  65. repokit/templates/ai/claude/CLAUDE.md.template +98 -0
  66. repokit/templates/ai/claude/instructions/step1_context_rebuilder.md +33 -0
  67. repokit/templates/ai/claude/instructions/step2_dev_workflow_process.md +67 -0
  68. repokit/templates/ai/claude/instructions/step3_context_bridge.md +52 -0
  69. repokit/templates/common/CHANGELOG.md.template +15 -0
  70. repokit/templates/common/CONTRIBUTING.md.template +31 -0
  71. repokit/templates/common/README.md.template +30 -0
  72. repokit/templates/common/gitattributes.template +17 -0
  73. repokit/templates/common/gitignore.template +141 -0
  74. repokit/templates/extras/FUNDING.yml.template +13 -0
  75. repokit/templates/extras/ROADMAP.md.template +17 -0
  76. repokit/templates/extras/dependabot.yml.template +22 -0
  77. repokit/templates/extras/one-offs-README.md.template +28 -0
  78. repokit/templates/extras/platform-support.md.template +11 -0
  79. repokit/templates/extras/pull_request_template.md.template +25 -0
  80. repokit/templates/extras/release-template.md.template +34 -0
  81. repokit/templates/extras/settings.json.template +11 -0
  82. repokit/templates/extras/stale.yml.template +14 -0
  83. repokit/templates/github/CODEOWNERS.template +11 -0
  84. repokit/templates/github/ISSUE_TEMPLATE/bug-report.md.template +29 -0
  85. repokit/templates/github/ISSUE_TEMPLATE/feature-request.md.template +19 -0
  86. repokit/templates/github/issues/notes.md.template +18 -0
  87. repokit/templates/github/issues/roadmap.md.template +15 -0
  88. repokit/templates/hooks/pre-commit.template +64 -0
  89. repokit/templates/layers/archetypes/any/claude-plugin/.claude-plugin/marketplace.json +20 -0
  90. repokit/templates/layers/archetypes/any/claude-plugin/plugin/.claude-plugin/plugin.json +15 -0
  91. repokit/templates/layers/archetypes/any/claude-plugin/plugin/README.md +28 -0
  92. repokit/templates/layers/archetypes/c-cpp/cmake-app/.github/workflows/ci-c-cpp.yml +24 -0
  93. repokit/templates/layers/archetypes/c-cpp/cmake-app/.repokit-common.toml.rkappend +4 -0
  94. repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/launch.json +29 -0
  95. repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/tasks.json +26 -0
  96. repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakeLists.txt +47 -0
  97. repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakePresets.json +23 -0
  98. repokit/templates/layers/archetypes/c-cpp/cmake-app/cmake/GenerateStamp.cmake +45 -0
  99. repokit/templates/layers/archetypes/c-cpp/cmake-app/src/main.cpp +27 -0
  100. repokit/templates/layers/archetypes/c-cpp/cmake-app/tests/test_version.cpp +20 -0
  101. repokit/templates/layers/archetypes/c-cpp/cmake-app/vcpkg.json +5 -0
  102. repokit/templates/layers/archetypes/c-cpp/minimal/.vscode/launch.json +27 -0
  103. repokit/templates/layers/archetypes/c-cpp/minimal/Makefile +10 -0
  104. repokit/templates/layers/archetypes/c-cpp/minimal/build.bat +7 -0
  105. repokit/templates/layers/archetypes/c-cpp/minimal/src/__RK_SNAKE__.c +26 -0
  106. repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/launch.json +17 -0
  107. repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/tasks.json +20 -0
  108. repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.sln +19 -0
  109. repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.vcxproj +41 -0
  110. repokit/templates/layers/archetypes/c-cpp/msbuild/src/main.cpp +26 -0
  111. repokit/templates/layers/archetypes/js-ts/claude-plugin/.repokit-common.toml +28 -0
  112. repokit/templates/layers/archetypes/js-ts/claude-plugin/.vscode/launch.json +34 -0
  113. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/hooks.json +15 -0
  114. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/scripts/session-start.mjs +23 -0
  115. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/tests/manifest.test.js +30 -0
  116. repokit/templates/layers/archetypes/js-ts/node/.repokit-common.toml.rkappend +6 -0
  117. repokit/templates/layers/archetypes/js-ts/node/package.json +13 -0
  118. repokit/templates/layers/archetypes/js-ts/node-package/.github/workflows/release.yml +39 -0
  119. repokit/templates/layers/archetypes/js-ts/node-package/package.json +32 -0
  120. repokit/templates/layers/archetypes/js-ts/node-package/src/index.ts +13 -0
  121. repokit/templates/layers/archetypes/js-ts/node-package/tests/index.test.ts +11 -0
  122. repokit/templates/layers/archetypes/js-ts/script/.vscode/launch.json +34 -0
  123. repokit/templates/layers/archetypes/js-ts/script/__RK_PROJECT__.js +37 -0
  124. repokit/templates/layers/archetypes/js-ts/script/tests/version.test.js +26 -0
  125. repokit/templates/layers/archetypes/js-ts/ts/package.json +18 -0
  126. repokit/templates/layers/archetypes/js-ts/ts/tsconfig.json +15 -0
  127. repokit/templates/layers/archetypes/python/claude-plugin/.github/workflows/ci-python.yml +44 -0
  128. repokit/templates/layers/archetypes/python/claude-plugin/.repokit-common.toml +21 -0
  129. repokit/templates/layers/archetypes/python/claude-plugin/.vscode/launch.json +33 -0
  130. repokit/templates/layers/archetypes/python/claude-plugin/VERSION +1 -0
  131. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/hooks.json +15 -0
  132. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/run-hook.mjs +42 -0
  133. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/session_start.py +31 -0
  134. repokit/templates/layers/archetypes/python/claude-plugin/plugin/tests/test_manifest.py +42 -0
  135. repokit/templates/layers/archetypes/python/pypi/.github/workflows/release.yml +63 -0
  136. repokit/templates/layers/archetypes/python/pypi/.vscode/launch.json +43 -0
  137. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__init__.py +5 -0
  138. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__main__.py +6 -0
  139. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/_version.py +96 -0
  140. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/cli.py +18 -0
  141. repokit/templates/layers/archetypes/python/pypi/pyproject.toml +71 -0
  142. repokit/templates/layers/archetypes/python/pypi/tests/conftest.py +11 -0
  143. repokit/templates/layers/archetypes/python/pypi/tests/test_version.py +50 -0
  144. repokit/templates/layers/archetypes/python/script/.github/workflows/ci-python.yml +43 -0
  145. repokit/templates/layers/archetypes/python/script/.repokit-common.toml +14 -0
  146. repokit/templates/layers/archetypes/python/script/.vscode/launch.json +33 -0
  147. repokit/templates/layers/archetypes/python/script/VERSION +1 -0
  148. repokit/templates/layers/archetypes/python/script/__RK_PROJECT__.py +40 -0
  149. repokit/templates/layers/archetypes/python/script/tests/test_version.py +25 -0
  150. repokit/templates/layers/archetypes/python/setuppy/requirements.txt +8 -0
  151. repokit/templates/layers/archetypes/python/setuppy/setup.py +23 -0
  152. repokit/templates/layers/languages/c-cpp/.gitignore.rkappend +20 -0
  153. repokit/templates/layers/languages/c-cpp/.repokit-common.toml +7 -0
  154. repokit/templates/layers/languages/c-cpp/include/__RK_SNAKE__/version.h +20 -0
  155. repokit/templates/layers/languages/c-cpp/src/app.rc +25 -0
  156. repokit/templates/layers/languages/js-ts/.editorconfig +15 -0
  157. repokit/templates/layers/languages/js-ts/.github/workflows/ci-js-ts.yml +41 -0
  158. repokit/templates/layers/languages/js-ts/.gitignore.rkappend +13 -0
  159. repokit/templates/layers/languages/js-ts/.repokit-common.toml +19 -0
  160. repokit/templates/layers/languages/js-ts/.vscode/launch.json +24 -0
  161. repokit/templates/layers/languages/js-ts/VERSION +1 -0
  162. repokit/templates/layers/languages/python/.github/workflows/ci-python.yml +76 -0
  163. repokit/templates/layers/languages/python/.gitignore.rkappend +15 -0
  164. repokit/templates/layers/languages/python/.vscode/launch.json +33 -0
  165. repokit/templates/layers/manifest.toml +302 -0
  166. repokit/templates/vault/CLAUDE.md +29 -0
  167. repokit/templates/vault/claude/_maps/MOC-project.md +15 -0
  168. repokit/templates/vault/claude/_oracle/backlinks.md +3 -0
  169. repokit/templates/vault/claude/_oracle/concepts.md +12 -0
  170. repokit/templates/vault/claude/_oracle/manifest.md +10 -0
  171. repokit/templates/vault/claude/_todo/ACTIVE-WORK.md +18 -0
  172. repokit/templates/vault/claude/_todo/OPEN-LOOPS.md +14 -0
  173. repokit/utils.py +316 -0
  174. repokit/vault.py +248 -0
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Authentication handler for RepoKit.
4
+
5
+ Manages authentication with remote services like GitHub and GitLab.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import logging
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import Dict, Any, Optional, Tuple
14
+
15
+
16
+ class AuthenticationHandler:
17
+ """
18
+ Handles authentication with remote services.
19
+ """
20
+
21
+ def __init__(self, verbose: int = 0):
22
+ """
23
+ Initialize the authentication handler.
24
+
25
+ Args:
26
+ verbose: Verbosity level (0=normal, 1=info, 2=debug)
27
+ """
28
+ self.verbose = verbose
29
+ self.logger = logging.getLogger("repokit.auth")
30
+
31
+ # Default locations for credentials
32
+ self.user_home = Path.home()
33
+ self.default_creds_path = self.user_home / ".repokit" / "credentials.json"
34
+
35
+ # Initialize with empty credentials
36
+ self.credentials = {}
37
+
38
+ def load_credentials(self, credentials_file: Optional[str] = None) -> bool:
39
+ """
40
+ Load credentials from a file.
41
+
42
+ Args:
43
+ credentials_file: Path to credentials file (or None to use default)
44
+
45
+ Returns:
46
+ True if loaded successfully, False otherwise
47
+ """
48
+ if credentials_file:
49
+ creds_path = Path(credentials_file)
50
+ elif self.default_creds_path.exists():
51
+ creds_path = self.default_creds_path
52
+ else:
53
+ if self.verbose >= 1:
54
+ self.logger.info("No credentials file found")
55
+ return False
56
+
57
+ try:
58
+ with open(creds_path, "r") as f:
59
+ self.credentials = json.load(f)
60
+
61
+ if self.verbose >= 1:
62
+ self.logger.info(f"Loaded credentials from {creds_path}")
63
+ return True
64
+ except Exception as e:
65
+ self.logger.error(f"Error loading credentials: {str(e)}")
66
+ return False
67
+
68
+ def get_token(
69
+ self, service: str, token_command: Optional[str] = None
70
+ ) -> Optional[str]:
71
+ """
72
+ Get authentication token for a service.
73
+
74
+ Tries multiple sources in this order:
75
+ 1. Command provided token command
76
+ 2. Environment variable
77
+ 3. Credentials file
78
+
79
+ Args:
80
+ service: Service name ('github' or 'gitlab')
81
+ token_command: Optional command to retrieve token from password manager
82
+
83
+ Returns:
84
+ Authentication token or None if not found
85
+ """
86
+ # Standardize service name
87
+ service = service.lower()
88
+
89
+ # 1. Try token command if provided
90
+ if token_command:
91
+ try:
92
+ if self.verbose >= 2:
93
+ self.logger.debug(f"Running token command: {token_command}")
94
+ result = subprocess.run(
95
+ token_command,
96
+ shell=True,
97
+ check=True,
98
+ capture_output=True,
99
+ text=True,
100
+ )
101
+ token = result.stdout.strip()
102
+ if token:
103
+ if self.verbose >= 1:
104
+ self.logger.info(f"Retrieved {service} token using command")
105
+ return token
106
+ except Exception as e:
107
+ self.logger.error(f"Failed to get token from command: {str(e)}")
108
+
109
+ # 2. Try environment variable
110
+ env_var = f"{service.upper()}_TOKEN"
111
+ token = os.environ.get(env_var)
112
+ if token:
113
+ if self.verbose >= 1:
114
+ self.logger.info(f"Using {service} token from environment variable")
115
+ return token
116
+
117
+ # 3. Try credentials file
118
+ if service in self.credentials and "token" in self.credentials[service]:
119
+ if self.verbose >= 1:
120
+ self.logger.info(f"Using {service} token from credentials file")
121
+ return self.credentials[service]["token"]
122
+
123
+ # Token not found
124
+ self.logger.warning(f"No {service} token found")
125
+ return None
126
+
127
+ def get_organization(self, service: str) -> Optional[str]:
128
+ """
129
+ Get organization or group for a service.
130
+
131
+ Args:
132
+ service: Service name ('github' or 'gitlab')
133
+
134
+ Returns:
135
+ Organization/group name or None if not specified
136
+ """
137
+ # Standardize service name
138
+ service = service.lower()
139
+
140
+ # Try environment variable first
141
+ env_var = f"{service.upper()}_ORGANIZATION"
142
+ if service == "gitlab":
143
+ env_var = "GITLAB_GROUP" # GitLab uses "group" terminology
144
+
145
+ org = os.environ.get(env_var)
146
+ if org:
147
+ return org
148
+
149
+ # Try credentials file
150
+ if service in self.credentials:
151
+ if service == "github" and "organization" in self.credentials[service]:
152
+ return self.credentials[service]["organization"]
153
+ elif service == "gitlab" and "group" in self.credentials[service]:
154
+ return self.credentials[service]["group"]
155
+
156
+ return None
157
+
158
+ def get_api_url(self, service: str) -> str:
159
+ """
160
+ Get API URL for a service.
161
+
162
+ Args:
163
+ service: Service name ('github' or 'gitlab')
164
+
165
+ Returns:
166
+ API URL for the service
167
+ """
168
+ if service.lower() == "github":
169
+ return os.environ.get("GITHUB_API_URL", "https://api.github.com")
170
+ elif service.lower() == "gitlab":
171
+ return os.environ.get("GITLAB_API_URL", "https://gitlab.com/api/v4")
172
+ else:
173
+ return ""
174
+
175
+ def store_credentials(
176
+ self,
177
+ service: str,
178
+ token: str,
179
+ organization: Optional[str] = None,
180
+ file_path: Optional[str] = None,
181
+ ) -> bool:
182
+ """
183
+ Store credentials in the credentials file.
184
+
185
+ Args:
186
+ service: Service name ('github' or 'gitlab')
187
+ token: Authentication token
188
+ organization: Organization or group name (optional)
189
+ file_path: Path to credentials file (or None to use default)
190
+
191
+ Returns:
192
+ True if stored successfully, False otherwise
193
+ """
194
+ # Standardize service name
195
+ service = service.lower()
196
+
197
+ # Load existing credentials if file exists
198
+ if file_path:
199
+ creds_path = Path(file_path)
200
+ else:
201
+ creds_path = self.default_creds_path
202
+ # Ensure directory exists
203
+ creds_path.parent.mkdir(parents=True, exist_ok=True)
204
+
205
+ # Load existing credentials if file exists
206
+ existing_creds = {}
207
+ if creds_path.exists():
208
+ try:
209
+ with open(creds_path, "r") as f:
210
+ existing_creds = json.load(f)
211
+ except Exception as e:
212
+ self.logger.error(f"Error loading existing credentials: {str(e)}")
213
+ # Continue with empty credentials
214
+
215
+ # Update credentials
216
+ service_creds = existing_creds.get(service, {})
217
+ service_creds["token"] = token
218
+
219
+ if organization:
220
+ if service == "github":
221
+ service_creds["organization"] = organization
222
+ elif service == "gitlab":
223
+ service_creds["group"] = organization
224
+
225
+ existing_creds[service] = service_creds
226
+
227
+ # Save credentials
228
+ try:
229
+ with open(creds_path, "w") as f:
230
+ json.dump(existing_creds, f, indent=2)
231
+
232
+ # Set restrictive permissions
233
+ os.chmod(creds_path, 0o600)
234
+
235
+ if self.verbose >= 1:
236
+ self.logger.info(f"Stored {service} credentials in {creds_path}")
237
+
238
+ # Update in-memory credentials
239
+ self.credentials = existing_creds
240
+
241
+ return True
242
+ except Exception as e:
243
+ self.logger.error(f"Error storing credentials: {str(e)}")
244
+ return False
repokit/badges.py ADDED
@@ -0,0 +1,183 @@
1
+ """README badges in the house's shape (0.9.7).
2
+
3
+ Measured on 2026-10-25 across the 107 house projects under C:\\code
4
+ (tests/one-offs/thinking/p4-measure-badges/) and settled with djdarcy; the
5
+ design is notes/2026-10-25__00-02-16__both_readme-badge-sets__the-house-badges-and-how-repokit-picks-them.md
6
+ in the vault.
7
+
8
+ - TYPES is the catalogue: each badge's commonest house form, filled per
9
+ project, inline (`[![alt](image)](link)`) as the house writes them.
10
+ - TEMPLATES are ordered lists. The general ones follow djdarcy's slots:
11
+ where to get it (PyPI, else the release), when it was released, package
12
+ downloads, runtime, other package locations, licence, discussions,
13
+ ghstats, platform last (the commitment that the tools work everywhere).
14
+ ComfyUI keeps its own order: the Registry is where a node comes from.
15
+ - CI is a filler: in when it lands on a row that already exists, out when
16
+ it would start a row by itself (ROW badges per row).
17
+ - The licence's colour is its house colour (GPL/AGPL green, MIT yellow,
18
+ others orange), switched to the next of green, yellow, orange when it would
19
+ equal a neighbour's colour.
20
+
21
+ A person's own templates (`badge_templates`) and badge types
22
+ (`badge_types`, each naming `after` to take its place in the order) come
23
+ from the global config; `--badges NAME|a,b,c` or the config's `badges`
24
+ picks one.
25
+ """
26
+ from typing import Dict, List, Optional, Sequence
27
+
28
+ ROW = 7 # shields badges per row of a GitHub README (to be checked on a render)
29
+
30
+ # id -> (alt, image, link, colour). {owner} {repo} {dist} {python} {node}
31
+ # {license_label} {license_url} {gist_user} {gist} {pages} are filled per
32
+ # project; {colour} is the badge's resolved colour.
33
+ TYPES: Dict[str, tuple] = {
34
+ "pypi-version": ("PyPI", "https://img.shields.io/pypi/v/{dist}?color={colour}",
35
+ "https://pypi.org/project/{dist}/", "green"),
36
+ "release-date": ("Release Date", "https://img.shields.io/github/release-date/{owner}/{repo}?color={colour}",
37
+ "https://github.com/{owner}/{repo}/releases", "green"),
38
+ "downloads": ("PyPI Downloads",
39
+ "https://static.pepy.tech/personalized-badge/{dist}?period=total&units=international_system"
40
+ "&left_color=black&right_color={colour}&left_text=downloads",
41
+ "https://pypistats.org/packages/{dist}", "green"),
42
+ "release-version": ("GitHub release",
43
+ "https://img.shields.io/github/v/release/{owner}/{repo}?include_prereleases&label=version",
44
+ "https://github.com/{owner}/{repo}/releases", "blue"),
45
+ "ci": ("CI", "https://github.com/{owner}/{repo}/actions/workflows/{ci}/badge.svg",
46
+ "https://github.com/{owner}/{repo}/actions/workflows/{ci}", "default"),
47
+ "python": ("Python {python}+", "https://img.shields.io/badge/python-{python}+-{colour}.svg",
48
+ "https://www.python.org/downloads/", "blue"),
49
+ "node": ("Node {node}+", "https://img.shields.io/badge/node-{node}+-{colour}.svg",
50
+ "https://nodejs.org/en/download", "blue"),
51
+ "comfyui-registry": ("ComfyUI Registry", "https://img.shields.io/badge/ComfyUI-Registry-{colour}.svg",
52
+ "https://registry.comfy.org/publishers/{gist_user}/nodes/{repo_lower}", "green"),
53
+ "license": ("License: {license_label}", "https://img.shields.io/badge/license-{license_badge}-{colour}.svg",
54
+ "{license_url}", "contrast"),
55
+ "discussions": ("GitHub Discussions", "https://img.shields.io/github/discussions/{owner}/{repo}",
56
+ "https://github.com/{owner}/{repo}/discussions", "blue"),
57
+ "installs": ("Installs",
58
+ "https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/{gist_user}/{gist}/raw/installs.json",
59
+ "https://{pages}.github.io/{repo}/stats/#installs", "blue"),
60
+ "views": ("Views",
61
+ "https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/{gist_user}/{gist}/raw/views.json",
62
+ "https://{pages}.github.io/{repo}/stats/#views", "blue"),
63
+ "platform": ("Platform", "https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-{colour}.svg",
64
+ "{platform_link}", "lightgrey"),
65
+ }
66
+
67
+ # The default order: djdarcy's slots, CI after the release slots. A badge type
68
+ # a person adds names its place with `after`.
69
+ ORDER = ["pypi-version", "release-date", "downloads", "release-version", "ci", "python", "node",
70
+ "comfyui-registry", "license", "discussions", "installs", "views", "platform"]
71
+
72
+ TEMPLATES: Dict[str, List[str]] = {
73
+ "pypi": ["pypi-version", "release-date", "downloads", "ci", "runtime", "license", "discussions",
74
+ "installs", "platform"],
75
+ "github": ["release-version", "ci", "runtime", "license", "discussions", "installs", "platform"],
76
+ # its own order: the Registry is where a node comes from (djdarcy, 2026-10-25)
77
+ "comfyui": ["runtime", "comfyui-registry", "release-version", "license", "installs", "views"],
78
+ "minimal": ["runtime", "license"],
79
+ }
80
+
81
+ # The default template for a preset
82
+ PRESET_TEMPLATE = {"python/pypi": "pypi", "python/comfyui": "comfyui"}
83
+
84
+ # licence id -> (badge text, the licence's page, house colour)
85
+ LICENSES = {
86
+ "gpl-3.0": ("GPL v3", "https://www.gnu.org/licenses/gpl-3.0.html", "green"),
87
+ "gpl-3.0-or-later": ("GPL v3", "https://www.gnu.org/licenses/gpl-3.0.html", "green"),
88
+ "agpl-3.0": ("AGPL v3", "https://www.gnu.org/licenses/agpl-3.0.html", "green"),
89
+ "agpl-3.0-or-later": ("AGPL v3", "https://www.gnu.org/licenses/agpl-3.0.html", "green"),
90
+ "lgpl-3.0": ("LGPL v3", "https://www.gnu.org/licenses/lgpl-3.0.html", "green"),
91
+ "lgpl-3.0-or-later": ("LGPL v3", "https://www.gnu.org/licenses/lgpl-3.0.html", "green"),
92
+ "gpl-2.0": ("GPL v2", "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html", "green"),
93
+ "mit": ("MIT", "https://opensource.org/licenses/MIT", "yellow"),
94
+ "apache-2.0": ("Apache 2.0", "https://opensource.org/licenses/Apache-2.0", "orange"),
95
+ }
96
+ CONTRAST = ["green", "yellow", "orange"]
97
+
98
+
99
+ class BadgeError(ValueError):
100
+ """A template or badge name the catalogue does not know."""
101
+
102
+
103
+ def _shields_text(text: str) -> str:
104
+ """A static shields badge's text: '-' doubled, '_' doubled, spaces escaped."""
105
+ return text.replace("-", "--").replace("_", "__").replace(" ", "%20")
106
+
107
+
108
+ def resolve(request, archetype: Optional[str], templates: Optional[dict] = None,
109
+ types: Optional[dict] = None) -> List[str]:
110
+ """The badge ids for a request: a template name, a list of ids, or None
111
+ (the preset's default template)."""
112
+ templates = {**TEMPLATES, **(templates or {})}
113
+ known = set(TYPES) | set(types or {}) | {"runtime"}
114
+ if request is None or request == "":
115
+ request = PRESET_TEMPLATE.get(archetype or "", "github")
116
+ if isinstance(request, str) and "," not in request:
117
+ if request not in templates:
118
+ if request in known:
119
+ return [request]
120
+ raise BadgeError(f"unknown badge template {request!r}; known: {', '.join(sorted(templates))}")
121
+ names = list(templates[request])
122
+ else:
123
+ names = [n.strip() for n in (request.split(",") if isinstance(request, str) else request) if n.strip()]
124
+ order = ORDER + [t for t in (types or {})]
125
+ names = sorted(dict.fromkeys(names), key=lambda n: order.index(n) if n in order else len(order))
126
+ unknown = [n for n in names if n not in known]
127
+ if unknown:
128
+ raise BadgeError(f"unknown badges {', '.join(unknown)}; known: {', '.join(sorted(known))}")
129
+ return names
130
+
131
+
132
+ def render(names: Sequence[str], values: Dict[str, str], ci: str = "auto",
133
+ types: Optional[dict] = None) -> str:
134
+ """The badge lines, one per line, inline. `values` fills the tokens; a
135
+ badge whose required value is missing (no CI file, no runtime floor) is
136
+ left out. `ci` is auto, on or off."""
137
+ catalogue = dict(TYPES)
138
+ for tid, spec in (types or {}).items(): # a person's own badge lines
139
+ catalogue[tid] = (spec.get("alt", tid), spec["image"], spec.get("link", ""), spec.get("colour", "default"))
140
+ picked = []
141
+ for n in names:
142
+ if n == "runtime":
143
+ n = "python" if values.get("python") else "node" if values.get("node") else None
144
+ if not n:
145
+ continue
146
+ if n == "ci" and not values.get("ci"):
147
+ continue
148
+ if n == "license" and not values.get("license_badge"):
149
+ continue
150
+ if n in ("python",) and not values.get("python"):
151
+ continue
152
+ if n in ("node",) and not values.get("node"):
153
+ continue
154
+ picked.append(n)
155
+ if "ci" in picked and ci != "on":
156
+ others = len(picked) - 1
157
+ if ci == "off" or (others and others % ROW == 0):
158
+ picked.remove("ci")
159
+ colours = [catalogue[n][3] for n in picked]
160
+ for i, n in enumerate(picked):
161
+ if colours[i] != "contrast":
162
+ continue
163
+ neighbours = {colours[j] for j in (i - 1, i + 1) if 0 <= j < len(picked)}
164
+ neighbours |= {"blue"} if "default" in neighbours else set() # shields' default reads blue
165
+ house = values.get("license_colour") or "orange"
166
+ colours[i] = house if house not in neighbours else next(
167
+ (c for c in CONTRAST if c not in neighbours), house)
168
+ lines = []
169
+ for n, colour in zip(picked, colours):
170
+ alt, image, link, _ = catalogue[n]
171
+ fill = {**values, "colour": colour}
172
+ lines.append(f"[![{alt.format(**fill)}]({image.format(**fill)})]({link.format(**fill)})")
173
+ return "\n".join(lines)
174
+
175
+
176
+ def license_values(lid: Optional[str], spdx: Optional[str]) -> Dict[str, str]:
177
+ """The licence badge's text, link and house colour; empty when the project
178
+ has no catalogue licence (a file, none, or all rights reserved)."""
179
+ if not lid or not spdx or spdx.startswith("LicenseRef"):
180
+ return {}
181
+ label, url, colour = LICENSES.get(lid, (spdx, "LICENSE", "orange"))
182
+ return {"license_label": label, "license_badge": _shields_text(label), "license_url": url,
183
+ "license_colour": colour}