gitwise-cli 0.24.2__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.
- gitwise/__init__.py +11 -0
- gitwise/__main__.py +113 -0
- gitwise/_cli_completions.py +88 -0
- gitwise/_cli_dispatch.py +469 -0
- gitwise/_cli_introspection.py +275 -0
- gitwise/_cli_parser.py +345 -0
- gitwise/_cli_setup_agents.py +439 -0
- gitwise/_i18n_data.json +1934 -0
- gitwise/_paths.py +22 -0
- gitwise/_runtime_config.py +246 -0
- gitwise/audit.py +338 -0
- gitwise/branches.py +183 -0
- gitwise/clean.py +197 -0
- gitwise/commit.py +142 -0
- gitwise/conflicts.py +112 -0
- gitwise/context.py +163 -0
- gitwise/design.py +383 -0
- gitwise/diff.py +309 -0
- gitwise/doctor.py +116 -0
- gitwise/git.py +254 -0
- gitwise/health.py +345 -0
- gitwise/i18n.py +99 -0
- gitwise/log.py +329 -0
- gitwise/merge.py +193 -0
- gitwise/optimize.py +212 -0
- gitwise/output.py +652 -0
- gitwise/pick.py +102 -0
- gitwise/pr.py +543 -0
- gitwise/py.typed +0 -0
- gitwise/schema.py +49 -0
- gitwise/setup.py +551 -0
- gitwise/setup_agents/__init__.py +36 -0
- gitwise/setup_agents/adapters/__init__.py +17 -0
- gitwise/setup_agents/adapters/aider.py +5 -0
- gitwise/setup_agents/adapters/base.py +5 -0
- gitwise/setup_agents/adapters/codex.py +5 -0
- gitwise/setup_agents/adapters/continue_adapter.py +5 -0
- gitwise/setup_agents/adapters/cursor.py +5 -0
- gitwise/setup_agents/adapters/opencode.py +5 -0
- gitwise/setup_agents/adapters/pi.py +5 -0
- gitwise/setup_agents/exec.py +449 -0
- gitwise/setup_agents/format.py +164 -0
- gitwise/setup_agents/plan.py +254 -0
- gitwise/setup_agents/plan_gitfiles.py +167 -0
- gitwise/setup_agents/plan_skills.py +256 -0
- gitwise/setup_agents/providers/__init__.py +96 -0
- gitwise/setup_agents/providers/aider.py +11 -0
- gitwise/setup_agents/providers/base.py +79 -0
- gitwise/setup_agents/providers/claude.py +408 -0
- gitwise/setup_agents/providers/codex.py +11 -0
- gitwise/setup_agents/providers/continue_adapter.py +11 -0
- gitwise/setup_agents/providers/cursor.py +11 -0
- gitwise/setup_agents/providers/opencode.py +11 -0
- gitwise/setup_agents/providers/pi.py +11 -0
- gitwise/setup_agents/state.py +141 -0
- gitwise/setup_agents/types.py +48 -0
- gitwise/share/agents/skills/git-audit/SKILL.md +25 -0
- gitwise/share/agents/skills/git-clean/SKILL.md +22 -0
- gitwise/share/agents/skills/git-optimize/SKILL.md +21 -0
- gitwise/share/aider/CONVENTIONS.md.template +8 -0
- gitwise/share/aider/aider.conf.yml.template +4 -0
- gitwise/share/claude/CLAUDE.md.template +9 -0
- gitwise/share/claude/rules/gitwise.md +16 -0
- gitwise/share/claude/settings.json.template +47 -0
- gitwise/share/claude/skills/git-audit/SKILL.md +25 -0
- gitwise/share/claude/skills/git-clean/SKILL.md +22 -0
- gitwise/share/claude/skills/git-optimize/SKILL.md +21 -0
- gitwise/share/codex/agents/gitwise.toml.template +18 -0
- gitwise/share/continue/rules/gitwise.md.template +14 -0
- gitwise/share/cursor/rules/gitwise.mdc.template +16 -0
- gitwise/share/git-config-modern.txt +48 -0
- gitwise/share/hooks/commit-msg +22 -0
- gitwise/share/hooks/pre-commit +19 -0
- gitwise/share/opencode/agents/gitwise.md.template +14 -0
- gitwise/share/pi/skills/gitwise.md.template +14 -0
- gitwise/share/schemas/v1/input/audit.json +40 -0
- gitwise/share/schemas/v1/input/branches.json +51 -0
- gitwise/share/schemas/v1/input/clean.json +52 -0
- gitwise/share/schemas/v1/input/commands.json +36 -0
- gitwise/share/schemas/v1/input/commit.json +63 -0
- gitwise/share/schemas/v1/input/completions.json +51 -0
- gitwise/share/schemas/v1/input/conflicts.json +46 -0
- gitwise/share/schemas/v1/input/context.json +36 -0
- gitwise/share/schemas/v1/input/diff.json +56 -0
- gitwise/share/schemas/v1/input/doctor.json +36 -0
- gitwise/share/schemas/v1/input/health.json +36 -0
- gitwise/share/schemas/v1/input/log.json +71 -0
- gitwise/share/schemas/v1/input/merge.json +63 -0
- gitwise/share/schemas/v1/input/optimize.json +44 -0
- gitwise/share/schemas/v1/input/pick.json +63 -0
- gitwise/share/schemas/v1/input/pr.json +51 -0
- gitwise/share/schemas/v1/input/schema.json +48 -0
- gitwise/share/schemas/v1/input/setup-agents.json +108 -0
- gitwise/share/schemas/v1/input/setup.json +55 -0
- gitwise/share/schemas/v1/input/show.json +46 -0
- gitwise/share/schemas/v1/input/snapshot.json +36 -0
- gitwise/share/schemas/v1/input/stash.json +68 -0
- gitwise/share/schemas/v1/input/status.json +36 -0
- gitwise/share/schemas/v1/input/suggest.json +36 -0
- gitwise/share/schemas/v1/input/summarize.json +44 -0
- gitwise/share/schemas/v1/input/sync.json +55 -0
- gitwise/share/schemas/v1/input/tag.json +73 -0
- gitwise/share/schemas/v1/input/undo.json +60 -0
- gitwise/share/schemas/v1/input/update.json +40 -0
- gitwise/share/schemas/v1/input/worktree.json +50 -0
- gitwise/show.py +118 -0
- gitwise/snapshot.py +110 -0
- gitwise/stash.py +188 -0
- gitwise/status.py +93 -0
- gitwise/suggest.py +148 -0
- gitwise/summarize.py +202 -0
- gitwise/sync.py +257 -0
- gitwise/tag.py +252 -0
- gitwise/undo.py +145 -0
- gitwise/update.py +42 -0
- gitwise/utils/__init__.py +1 -0
- gitwise/utils/git_output.py +51 -0
- gitwise/utils/json_envelope.py +58 -0
- gitwise/utils/parsing.py +34 -0
- gitwise/worktree.py +182 -0
- gitwise_cli-0.24.2.dist-info/METADATA +151 -0
- gitwise_cli-0.24.2.dist-info/RECORD +125 -0
- gitwise_cli-0.24.2.dist-info/WHEEL +4 -0
- gitwise_cli-0.24.2.dist-info/entry_points.txt +2 -0
- gitwise_cli-0.24.2.dist-info/licenses/LICENSE +21 -0
gitwise/health.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""gitwise health — repo health score (0-100) with grade and breakdown."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
from .git import (
|
|
7
|
+
gpg_status,
|
|
8
|
+
has_commit_graph,
|
|
9
|
+
has_remote,
|
|
10
|
+
has_upstream,
|
|
11
|
+
require_root,
|
|
12
|
+
)
|
|
13
|
+
from .git import run as git_run
|
|
14
|
+
from .i18n import t
|
|
15
|
+
from .output import print_header, print_json, print_status_line
|
|
16
|
+
from .utils.json_envelope import ok_envelope
|
|
17
|
+
from .utils.parsing import non_empty_lines, to_int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HealthDetails(TypedDict):
|
|
21
|
+
has_remote: bool
|
|
22
|
+
has_upstream: bool
|
|
23
|
+
gpg_ready: bool
|
|
24
|
+
stale_branches: int
|
|
25
|
+
stashes: int
|
|
26
|
+
untracked: int
|
|
27
|
+
commits: int
|
|
28
|
+
branches: int
|
|
29
|
+
commit_graph: bool
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HealthResult(TypedDict):
|
|
33
|
+
score: int
|
|
34
|
+
grade: str
|
|
35
|
+
breakdown: dict[str, int]
|
|
36
|
+
details: HealthDetails
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _untracked_count(cwd: Path) -> int:
|
|
40
|
+
r = git_run(["ls-files", "--others", "--exclude-standard"], cwd=cwd, check=False)
|
|
41
|
+
return len(non_empty_lines(r.stdout)) if r.returncode == 0 else 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _stash_count(cwd: Path) -> int:
|
|
45
|
+
r = git_run(["stash", "list"], cwd=cwd, check=False)
|
|
46
|
+
return len(non_empty_lines(r.stdout)) if r.returncode == 0 else 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _commit_count(cwd: Path) -> int:
|
|
50
|
+
r = git_run(["rev-list", "--count", "HEAD"], cwd=cwd, check=False)
|
|
51
|
+
return to_int(r.stdout, default=0) if r.returncode == 0 else 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _branch_info(cwd: Path) -> tuple[int, list[str]]:
|
|
55
|
+
r = git_run(
|
|
56
|
+
["for-each-ref", "--format=%(refname:short) %(upstream:track)", "refs/heads/"],
|
|
57
|
+
cwd=cwd,
|
|
58
|
+
check=False,
|
|
59
|
+
)
|
|
60
|
+
if r.returncode != 0:
|
|
61
|
+
return 0, []
|
|
62
|
+
branches: list[str] = []
|
|
63
|
+
stale: list[str] = []
|
|
64
|
+
for line in r.stdout.splitlines():
|
|
65
|
+
parts = line.strip().split(" ", 1)
|
|
66
|
+
name = parts[0] if parts else ""
|
|
67
|
+
if name:
|
|
68
|
+
branches.append(name)
|
|
69
|
+
if len(parts) >= 2 and "[gone]" in parts[1]:
|
|
70
|
+
stale.append(name)
|
|
71
|
+
return len(branches), stale
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _breakdown_labels() -> dict[str, str]:
|
|
75
|
+
return {
|
|
76
|
+
"remote": t("health_remote"),
|
|
77
|
+
"upstream": t("health_upstream"),
|
|
78
|
+
"gpg_signing": t("health_gpg_signing"),
|
|
79
|
+
"stale_branches": t("health_stale_branches"),
|
|
80
|
+
"commit_graph": t("health_commit_graph"),
|
|
81
|
+
"old_stashes": t("health_old_stashes"),
|
|
82
|
+
"untracked_clutter": t("health_untracked_clutter"),
|
|
83
|
+
"no_commits": t("health_no_commits"),
|
|
84
|
+
"too_many_branches": t("health_too_many_branches"),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_GRADE_MAP = [(90, "A"), (75, "B"), (60, "C"), (40, "D"), (0, "F")]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _grade(score: int) -> str:
|
|
92
|
+
for threshold, letter in _GRADE_MAP:
|
|
93
|
+
if score >= threshold:
|
|
94
|
+
return letter
|
|
95
|
+
return "F"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _apply_remote_penalties(
|
|
99
|
+
*, score: int, breakdown: dict[str, int], root: Path
|
|
100
|
+
) -> tuple[int, bool, bool]:
|
|
101
|
+
has_rem = has_remote(root)
|
|
102
|
+
has_up = has_upstream(root) if has_rem else False
|
|
103
|
+
if not has_rem:
|
|
104
|
+
score -= 20
|
|
105
|
+
breakdown["remote"] = -20
|
|
106
|
+
if has_rem and not has_up:
|
|
107
|
+
score -= 10
|
|
108
|
+
breakdown["upstream"] = -10
|
|
109
|
+
return score, has_rem, has_up
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _apply_gpg_penalty(*, score: int, breakdown: dict[str, int], root: Path) -> tuple[int, dict]:
|
|
113
|
+
gpg = gpg_status(root)
|
|
114
|
+
if not gpg["gpgsign_enabled"]:
|
|
115
|
+
score -= 10
|
|
116
|
+
breakdown["gpg_signing"] = -10
|
|
117
|
+
return score, gpg
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _apply_branch_penalties(
|
|
121
|
+
*,
|
|
122
|
+
score: int,
|
|
123
|
+
breakdown: dict[str, int],
|
|
124
|
+
root: Path,
|
|
125
|
+
stale_override: list[str] | None,
|
|
126
|
+
) -> tuple[int, int, list[str]]:
|
|
127
|
+
branch_count, stale = _branch_info(root) if stale_override is None else (0, stale_override)
|
|
128
|
+
if stale:
|
|
129
|
+
penalty = min(len(stale) * 5, 15)
|
|
130
|
+
score -= penalty
|
|
131
|
+
breakdown["stale_branches"] = -penalty
|
|
132
|
+
if branch_count > 15:
|
|
133
|
+
penalty = min((branch_count - 15) * 2, 10)
|
|
134
|
+
score -= penalty
|
|
135
|
+
breakdown["too_many_branches"] = -penalty
|
|
136
|
+
return score, branch_count, stale
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _apply_commit_graph_penalty(
|
|
140
|
+
*,
|
|
141
|
+
score: int,
|
|
142
|
+
breakdown: dict[str, int],
|
|
143
|
+
root: Path,
|
|
144
|
+
commit_graph_override: bool | None,
|
|
145
|
+
) -> tuple[int, bool]:
|
|
146
|
+
has_cg = commit_graph_override if commit_graph_override is not None else has_commit_graph(root)
|
|
147
|
+
if not has_cg:
|
|
148
|
+
score -= 5
|
|
149
|
+
breakdown["commit_graph"] = -5
|
|
150
|
+
return score, has_cg
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _apply_repo_size_penalties(
|
|
154
|
+
*, score: int, breakdown: dict[str, int], root: Path
|
|
155
|
+
) -> tuple[int, int, int, int]:
|
|
156
|
+
stashes = _stash_count(root)
|
|
157
|
+
if stashes > 3:
|
|
158
|
+
penalty = min((stashes - 3) * 2, 10)
|
|
159
|
+
score -= penalty
|
|
160
|
+
breakdown["old_stashes"] = -penalty
|
|
161
|
+
|
|
162
|
+
untracked = _untracked_count(root)
|
|
163
|
+
if untracked > 20:
|
|
164
|
+
penalty = min((untracked - 20), 10)
|
|
165
|
+
score -= penalty
|
|
166
|
+
breakdown["untracked_clutter"] = -penalty
|
|
167
|
+
|
|
168
|
+
commits = _commit_count(root)
|
|
169
|
+
if commits == 0:
|
|
170
|
+
score -= 20
|
|
171
|
+
breakdown["no_commits"] = -20
|
|
172
|
+
|
|
173
|
+
return score, stashes, untracked, commits
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _remote_state(
|
|
177
|
+
*,
|
|
178
|
+
root: Path,
|
|
179
|
+
score: int,
|
|
180
|
+
breakdown: dict[str, int],
|
|
181
|
+
has_remote_override: bool | None,
|
|
182
|
+
has_upstream_override: bool | None,
|
|
183
|
+
) -> tuple[int, bool, bool]:
|
|
184
|
+
if has_remote_override is not None or has_upstream_override is not None:
|
|
185
|
+
has_rem = has_remote_override if has_remote_override is not None else has_remote(root)
|
|
186
|
+
has_up = (
|
|
187
|
+
has_upstream_override
|
|
188
|
+
if has_upstream_override is not None
|
|
189
|
+
else (has_upstream(root) if has_rem else False)
|
|
190
|
+
)
|
|
191
|
+
if not has_rem:
|
|
192
|
+
score -= 20
|
|
193
|
+
breakdown["remote"] = -20
|
|
194
|
+
if has_rem and not has_up:
|
|
195
|
+
score -= 10
|
|
196
|
+
breakdown["upstream"] = -10
|
|
197
|
+
return score, has_rem, has_up
|
|
198
|
+
return _apply_remote_penalties(score=score, breakdown=breakdown, root=root)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _gpg_state(
|
|
202
|
+
*,
|
|
203
|
+
root: Path,
|
|
204
|
+
score: int,
|
|
205
|
+
breakdown: dict[str, int],
|
|
206
|
+
gpg_override: dict | None,
|
|
207
|
+
) -> tuple[int, dict]:
|
|
208
|
+
if gpg_override is not None:
|
|
209
|
+
gpg = gpg_override
|
|
210
|
+
if not gpg["gpgsign_enabled"]:
|
|
211
|
+
score -= 10
|
|
212
|
+
breakdown["gpg_signing"] = -10
|
|
213
|
+
return score, gpg
|
|
214
|
+
return _apply_gpg_penalty(score=score, breakdown=breakdown, root=root)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _build_health_result(
|
|
218
|
+
*,
|
|
219
|
+
score: int,
|
|
220
|
+
breakdown: dict[str, int],
|
|
221
|
+
has_rem: bool,
|
|
222
|
+
has_up: bool,
|
|
223
|
+
gpg: dict,
|
|
224
|
+
stale: list[str],
|
|
225
|
+
stashes: int,
|
|
226
|
+
untracked: int,
|
|
227
|
+
commits: int,
|
|
228
|
+
branches: int,
|
|
229
|
+
has_cg: bool,
|
|
230
|
+
) -> HealthResult:
|
|
231
|
+
bounded_score = max(0, min(100, score))
|
|
232
|
+
return {
|
|
233
|
+
"score": bounded_score,
|
|
234
|
+
"grade": _grade(bounded_score),
|
|
235
|
+
"breakdown": breakdown,
|
|
236
|
+
"details": {
|
|
237
|
+
"has_remote": has_rem,
|
|
238
|
+
"has_upstream": has_up,
|
|
239
|
+
"gpg_ready": gpg["ready"],
|
|
240
|
+
"stale_branches": len(stale),
|
|
241
|
+
"stashes": stashes,
|
|
242
|
+
"untracked": untracked,
|
|
243
|
+
"commits": commits,
|
|
244
|
+
"branches": branches,
|
|
245
|
+
"commit_graph": has_cg,
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _remaining_health_state(
|
|
251
|
+
*,
|
|
252
|
+
root: Path,
|
|
253
|
+
score: int,
|
|
254
|
+
breakdown: dict[str, int],
|
|
255
|
+
stale_override: list[str] | None,
|
|
256
|
+
commit_graph_override: bool | None,
|
|
257
|
+
) -> tuple[int, int, list[str], bool, int, int, int]:
|
|
258
|
+
score, branches, stale = _apply_branch_penalties(
|
|
259
|
+
score=score,
|
|
260
|
+
breakdown=breakdown,
|
|
261
|
+
root=root,
|
|
262
|
+
stale_override=stale_override,
|
|
263
|
+
)
|
|
264
|
+
score, has_cg = _apply_commit_graph_penalty(
|
|
265
|
+
score=score,
|
|
266
|
+
breakdown=breakdown,
|
|
267
|
+
root=root,
|
|
268
|
+
commit_graph_override=commit_graph_override,
|
|
269
|
+
)
|
|
270
|
+
score, stashes, untracked, commits = _apply_repo_size_penalties(
|
|
271
|
+
score=score,
|
|
272
|
+
breakdown=breakdown,
|
|
273
|
+
root=root,
|
|
274
|
+
)
|
|
275
|
+
return score, branches, stale, has_cg, stashes, untracked, commits
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def compute_health(
|
|
279
|
+
root: Path,
|
|
280
|
+
*,
|
|
281
|
+
_gpg_override: dict | None = None,
|
|
282
|
+
_has_commit_graph: bool | None = None,
|
|
283
|
+
_has_remote: bool | None = None,
|
|
284
|
+
_has_upstream: bool | None = None,
|
|
285
|
+
_stale_branches: list[str] | None = None,
|
|
286
|
+
) -> HealthResult:
|
|
287
|
+
score = 100
|
|
288
|
+
breakdown: dict[str, int] = {}
|
|
289
|
+
score, has_rem, has_up = _remote_state(
|
|
290
|
+
root=root,
|
|
291
|
+
score=score,
|
|
292
|
+
breakdown=breakdown,
|
|
293
|
+
has_remote_override=_has_remote,
|
|
294
|
+
has_upstream_override=_has_upstream,
|
|
295
|
+
)
|
|
296
|
+
score, gpg = _gpg_state(
|
|
297
|
+
root=root,
|
|
298
|
+
score=score,
|
|
299
|
+
breakdown=breakdown,
|
|
300
|
+
gpg_override=_gpg_override,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
score, branches, stale, has_cg, stashes, untracked, commits = _remaining_health_state(
|
|
304
|
+
root=root,
|
|
305
|
+
score=score,
|
|
306
|
+
breakdown=breakdown,
|
|
307
|
+
stale_override=_stale_branches,
|
|
308
|
+
commit_graph_override=_has_commit_graph,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
return _build_health_result(
|
|
312
|
+
score=score,
|
|
313
|
+
breakdown=breakdown,
|
|
314
|
+
has_rem=has_rem,
|
|
315
|
+
has_up=has_up,
|
|
316
|
+
gpg=gpg,
|
|
317
|
+
stale=stale,
|
|
318
|
+
stashes=stashes,
|
|
319
|
+
untracked=untracked,
|
|
320
|
+
commits=commits,
|
|
321
|
+
branches=branches,
|
|
322
|
+
has_cg=has_cg,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def run_health(*, as_json: bool = False) -> int:
|
|
327
|
+
root, err = require_root()
|
|
328
|
+
if err:
|
|
329
|
+
return err
|
|
330
|
+
if root is None:
|
|
331
|
+
return 1
|
|
332
|
+
|
|
333
|
+
h = compute_health(root)
|
|
334
|
+
|
|
335
|
+
if as_json:
|
|
336
|
+
print_json(ok_envelope(payload=h))
|
|
337
|
+
else:
|
|
338
|
+
print_header(t("health_label", score=str(h["score"]), grade=h["grade"]))
|
|
339
|
+
if h["breakdown"]:
|
|
340
|
+
for key, delta in h["breakdown"].items():
|
|
341
|
+
label = _breakdown_labels().get(key) or key
|
|
342
|
+
is_ok = delta == 0
|
|
343
|
+
print_status_line("✓" if is_ok else "✗", label, str(delta), ok_flag=is_ok)
|
|
344
|
+
|
|
345
|
+
return 0
|
gitwise/i18n.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Zero-dependency i18n: es/en string catalog with auto locale detection."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
Locale = Literal["es", "en"]
|
|
9
|
+
OutputMode = Literal["human", "agent"]
|
|
10
|
+
|
|
11
|
+
_CACHE: dict[str, str] = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_strings() -> dict[str, dict[str, str]]:
|
|
15
|
+
data_path = Path(__file__).with_name("_i18n_data.json")
|
|
16
|
+
with open(data_path, encoding="utf-8") as f:
|
|
17
|
+
return json.load(f)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_STRINGS: dict[str, dict[str, str]] = _load_strings()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _detect_locale() -> Locale:
|
|
24
|
+
lang = os.environ.get("GITWISE_LANG", "").lower()[:2]
|
|
25
|
+
if lang in ("es", "en"):
|
|
26
|
+
return lang
|
|
27
|
+
for var in ("LC_MESSAGES", "LC_ALL", "LANG"):
|
|
28
|
+
val = os.environ.get(var, "").lower()
|
|
29
|
+
if val.startswith("es"):
|
|
30
|
+
return "es"
|
|
31
|
+
if val.startswith("en"):
|
|
32
|
+
return "en"
|
|
33
|
+
return "en"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _detect_output_mode() -> OutputMode:
|
|
37
|
+
mode = os.environ.get("GITWISE_OUTPUT", "").lower()
|
|
38
|
+
if mode in ("human", "agent"):
|
|
39
|
+
return mode
|
|
40
|
+
if os.environ.get("GITWISE_AGENT", "").lower() in ("1", "true"):
|
|
41
|
+
return "agent"
|
|
42
|
+
if not os.environ.get("TERM", ""):
|
|
43
|
+
return "agent"
|
|
44
|
+
return "human"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _I18nState:
|
|
48
|
+
__slots__ = ("locale", "mode")
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self.locale: Locale = _detect_locale()
|
|
52
|
+
self.mode: OutputMode = _detect_output_mode()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_state = _I18nState()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_locale() -> Locale:
|
|
59
|
+
return _state.locale
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_mode() -> OutputMode:
|
|
63
|
+
return _state.mode
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def set_locale(locale: Locale) -> None:
|
|
67
|
+
_state.locale = locale
|
|
68
|
+
_CACHE.clear()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def set_mode(mode: OutputMode) -> None:
|
|
72
|
+
_state.mode = mode
|
|
73
|
+
_CACHE.clear()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def t(key: str, **kwargs: str) -> str:
|
|
77
|
+
cached_key = f"{_state.locale}:{key}:{json.dumps(kwargs, sort_keys=True, default=str)}"
|
|
78
|
+
if cached_key in _CACHE and "GITWISE_DEBUG" not in os.environ:
|
|
79
|
+
return _CACHE[cached_key]
|
|
80
|
+
entry = _STRINGS.get(key)
|
|
81
|
+
if entry is None:
|
|
82
|
+
return key
|
|
83
|
+
template = entry.get(_state.locale, entry.get("en", key))
|
|
84
|
+
try:
|
|
85
|
+
result = template.format(**kwargs) if kwargs else template
|
|
86
|
+
except (KeyError, IndexError, ValueError):
|
|
87
|
+
result = template
|
|
88
|
+
_CACHE[cached_key] = result
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def confirm_responses() -> set[str]:
|
|
93
|
+
if _state.locale == "es":
|
|
94
|
+
return {"s", "si", "sí", "y", "yes"}
|
|
95
|
+
return {"y", "yes", "s", "si"}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def reset_cache() -> None:
|
|
99
|
+
_CACHE.clear()
|