gitwise-cli 0.29.0__py3-none-any.whl → 0.30.0__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 +4 -1
- gitwise/__main__.py +4 -0
- gitwise/_cli_completions.py +4 -0
- gitwise/_cli_dispatch.py +89 -0
- gitwise/_cli_introspection.py +14 -0
- gitwise/_cli_parser.py +19 -0
- gitwise/_cli_setup_agents.py +5 -0
- gitwise/_i18n_data.json +12 -0
- gitwise/_runtime_config.py +17 -0
- gitwise/audit.py +11 -0
- gitwise/branches.py +10 -0
- gitwise/clean.py +6 -0
- gitwise/commit.py +15 -0
- gitwise/conflicts.py +8 -0
- gitwise/context.py +9 -0
- gitwise/design.py +26 -0
- gitwise/diff.py +196 -23
- gitwise/doctor.py +5 -0
- gitwise/git.py +40 -0
- gitwise/health.py +25 -0
- gitwise/i18n.py +13 -0
- gitwise/log.py +20 -0
- gitwise/merge.py +10 -0
- gitwise/optimize.py +12 -0
- gitwise/output.py +45 -0
- gitwise/pick.py +6 -0
- gitwise/pr.py +31 -0
- gitwise/schema.py +5 -0
- gitwise/setup.py +28 -0
- gitwise/setup_agents/exec.py +31 -3
- gitwise/setup_agents/format.py +8 -0
- gitwise/setup_agents/plan.py +17 -0
- gitwise/setup_agents/plan_gitfiles.py +11 -0
- gitwise/setup_agents/plan_skills.py +10 -0
- gitwise/setup_agents/providers/__init__.py +9 -0
- gitwise/setup_agents/providers/base.py +9 -0
- gitwise/setup_agents/providers/claude.py +18 -0
- gitwise/setup_agents/state.py +7 -0
- gitwise/setup_agents/types.py +5 -0
- gitwise/share/schemas/v1/input/diff.json +16 -0
- gitwise/show.py +9 -0
- gitwise/snapshot.py +6 -0
- gitwise/stash.py +8 -0
- gitwise/status.py +1 -0
- gitwise/suggest.py +10 -0
- gitwise/summarize.py +5 -0
- gitwise/sync.py +13 -0
- gitwise/tag.py +11 -0
- gitwise/undo.py +9 -0
- gitwise/update.py +1 -0
- gitwise/worktree.py +5 -0
- {gitwise_cli-0.29.0.dist-info → gitwise_cli-0.30.0.dist-info}/METADATA +1 -1
- {gitwise_cli-0.29.0.dist-info → gitwise_cli-0.30.0.dist-info}/RECORD +56 -56
- {gitwise_cli-0.29.0.dist-info → gitwise_cli-0.30.0.dist-info}/WHEEL +0 -0
- {gitwise_cli-0.29.0.dist-info → gitwise_cli-0.30.0.dist-info}/entry_points.txt +0 -0
- {gitwise_cli-0.29.0.dist-info → gitwise_cli-0.30.0.dist-info}/licenses/LICENSE +0 -0
gitwise/__init__.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
"""gitwise -- CLI for optimizing git workflows and coding-agent integration."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.30.0"
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
def get_version() -> str:
|
|
7
|
+
"""Return the installed package version, falling back to the source version."""
|
|
5
8
|
from importlib.metadata import PackageNotFoundError
|
|
6
9
|
from importlib.metadata import version as _version
|
|
7
10
|
|
gitwise/__main__.py
CHANGED
|
@@ -11,16 +11,19 @@ from .output import print_dim, print_json, set_json_mode, set_json_pretty
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _is_log_json_enabled() -> bool:
|
|
14
|
+
"""Return True when GITWISE_LOG_JSON is set to a truthy value."""
|
|
14
15
|
import os
|
|
15
16
|
|
|
16
17
|
return os.environ.get("GITWISE_LOG_JSON", "").lower() in ("1", "true")
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _should_show_rich_traceback() -> bool:
|
|
21
|
+
"""Return True when rich tracebacks should be used (tty and not in JSON log mode)."""
|
|
20
22
|
return (not _is_log_json_enabled()) and sys.stderr.isatty()
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
def _install_rich_traceback() -> None:
|
|
26
|
+
"""Install the rich traceback handler if conditions allow; no-op otherwise."""
|
|
24
27
|
if not _should_show_rich_traceback():
|
|
25
28
|
return
|
|
26
29
|
try:
|
|
@@ -56,6 +59,7 @@ def _ensure_utf8_stdio() -> None:
|
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
def main() -> int:
|
|
62
|
+
"""Parse args, dispatch to the matching subcommand handler, and return an exit code."""
|
|
59
63
|
import os
|
|
60
64
|
|
|
61
65
|
from ._runtime_config import reset_runtime_config
|
gitwise/_cli_completions.py
CHANGED
|
@@ -7,6 +7,7 @@ from ._cli_parser import build_parser
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def build_completions_script(*, shell: str, prog: str) -> str:
|
|
10
|
+
"""Return a shell completion script for the given shell and program name."""
|
|
10
11
|
parser = build_parser()
|
|
11
12
|
parser.prog = prog
|
|
12
13
|
|
|
@@ -23,10 +24,12 @@ def build_completions_script(*, shell: str, prog: str) -> str:
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _fish_escape(text: str) -> str:
|
|
27
|
+
"""Escape single quotes for use inside a fish completion string literal."""
|
|
26
28
|
return text.replace("'", "\\'")
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
def _build_fish_option_line(*, prog: str, condition: str, flag: str, help_text: str) -> str:
|
|
32
|
+
"""Return a single fish ``complete`` line for an option, or an empty string for unrecognized flags."""
|
|
30
33
|
escaped_help = _fish_escape(help_text)
|
|
31
34
|
escaped_prog = _fish_escape(prog)
|
|
32
35
|
escaped_condition = _fish_escape(condition)
|
|
@@ -44,6 +47,7 @@ def _build_fish_option_line(*, prog: str, condition: str, flag: str, help_text:
|
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
def _build_fish_completions_script(*, parser: argparse.ArgumentParser, prog: str) -> str:
|
|
50
|
+
"""Generate a fish completion script from the full argparse parser tree."""
|
|
47
51
|
escaped_prog = _fish_escape(prog)
|
|
48
52
|
lines: list[str] = [f"# fish completion for {escaped_prog}"]
|
|
49
53
|
|
gitwise/_cli_dispatch.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Command dispatchers: thin wrappers that delegate to subcommand modules."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import sys
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
|
|
6
7
|
from . import __version__
|
|
@@ -17,18 +18,21 @@ from .utils.json_envelope import error_envelope
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _run_update(args: argparse.Namespace) -> int:
|
|
21
|
+
"""Dispatch to ``update`` subcommand."""
|
|
20
22
|
from .update import run_update
|
|
21
23
|
|
|
22
24
|
return run_update(dry_run=args.dry_run, as_json=args.json)
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
def _run_doctor(args: argparse.Namespace) -> int:
|
|
28
|
+
"""Dispatch to ``doctor`` subcommand."""
|
|
26
29
|
from .doctor import run_doctor
|
|
27
30
|
|
|
28
31
|
return run_doctor(as_json=args.json)
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
def _run_setup_agents(args: argparse.Namespace) -> int:
|
|
35
|
+
"""Dispatch to ``setup-agents`` subcommand, handling provider listing."""
|
|
32
36
|
if getattr(args, "list_providers", False) or getattr(args, "list_adapters", False):
|
|
33
37
|
from .i18n import t as _t
|
|
34
38
|
from .setup_agents.providers import list_providers
|
|
@@ -68,6 +72,7 @@ def _run_setup_agents(args: argparse.Namespace) -> int:
|
|
|
68
72
|
|
|
69
73
|
|
|
70
74
|
def _run_setup(args: argparse.Namespace) -> int:
|
|
75
|
+
"""Dispatch to ``setup`` subcommand."""
|
|
71
76
|
from .setup import run_setup
|
|
72
77
|
|
|
73
78
|
return run_setup(
|
|
@@ -79,24 +84,28 @@ def _run_setup(args: argparse.Namespace) -> int:
|
|
|
79
84
|
|
|
80
85
|
|
|
81
86
|
def _run_audit(args: argparse.Namespace) -> int:
|
|
87
|
+
"""Dispatch to ``audit`` subcommand."""
|
|
82
88
|
from .audit import run_audit
|
|
83
89
|
|
|
84
90
|
return run_audit(quick=args.quick, as_json=args.json)
|
|
85
91
|
|
|
86
92
|
|
|
87
93
|
def _run_summarize(args: argparse.Namespace) -> int:
|
|
94
|
+
"""Dispatch to ``summarize`` subcommand."""
|
|
88
95
|
from .summarize import run_summarize
|
|
89
96
|
|
|
90
97
|
return run_summarize(as_json=args.json, diff=args.diff, max_commits=args.max_commits)
|
|
91
98
|
|
|
92
99
|
|
|
93
100
|
def _run_snapshot(args: argparse.Namespace) -> int:
|
|
101
|
+
"""Dispatch to ``snapshot`` subcommand."""
|
|
94
102
|
from .snapshot import run_snapshot
|
|
95
103
|
|
|
96
104
|
return run_snapshot(as_json=args.json)
|
|
97
105
|
|
|
98
106
|
|
|
99
107
|
def _run_clean(args: argparse.Namespace) -> int:
|
|
108
|
+
"""Dispatch to ``clean`` subcommand."""
|
|
100
109
|
from .clean import run_clean
|
|
101
110
|
|
|
102
111
|
return run_clean(
|
|
@@ -109,12 +118,14 @@ def _run_clean(args: argparse.Namespace) -> int:
|
|
|
109
118
|
|
|
110
119
|
|
|
111
120
|
def _run_optimize(args: argparse.Namespace) -> int:
|
|
121
|
+
"""Dispatch to ``optimize`` subcommand."""
|
|
112
122
|
from .optimize import run_optimize
|
|
113
123
|
|
|
114
124
|
return run_optimize(dry_run=args.dry_run, yes=args.yes, as_json=args.json)
|
|
115
125
|
|
|
116
126
|
|
|
117
127
|
def _run_worktree(args: argparse.Namespace) -> int:
|
|
128
|
+
"""Dispatch to ``worktree`` subcommand."""
|
|
118
129
|
from .worktree import run_worktree
|
|
119
130
|
|
|
120
131
|
return run_worktree(
|
|
@@ -122,19 +133,72 @@ def _run_worktree(args: argparse.Namespace) -> int:
|
|
|
122
133
|
)
|
|
123
134
|
|
|
124
135
|
|
|
136
|
+
def _recover_diff_pathspec_boundary() -> tuple[str | None, list[str] | None]:
|
|
137
|
+
"""Recover the explicit ``--`` pathspec boundary argparse loses.
|
|
138
|
+
|
|
139
|
+
With ``refspec nargs="?"`` + ``paths nargs="*"``, a path-only invocation
|
|
140
|
+
like ``gitwise diff -- src/`` mis-assigns ``src/`` to ``refspec``. git
|
|
141
|
+
treats ``--`` as a hard pathspec separator, so honor it here: anything
|
|
142
|
+
positional before ``--`` (excluding flags) feeds ``refspec``; anything
|
|
143
|
+
after ``--`` is literal paths, even values starting with ``-``.
|
|
144
|
+
|
|
145
|
+
Raises ``ValueError`` when more than one positional precedes ``--``, since
|
|
146
|
+
``gitwise diff`` accepts a single refspec (two-commit diffs use ``a..b``);
|
|
147
|
+
failing fast beats silently dropping the extras.
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
sub_idx = sys.argv.index("diff")
|
|
151
|
+
except ValueError:
|
|
152
|
+
return None, None
|
|
153
|
+
cmd_argv = sys.argv[sub_idx + 1 :]
|
|
154
|
+
if "--" not in cmd_argv:
|
|
155
|
+
return None, None
|
|
156
|
+
sep = cmd_argv.index("--")
|
|
157
|
+
before = [a for a in cmd_argv[:sep] if not a.startswith("-")]
|
|
158
|
+
after = list(cmd_argv[sep + 1 :])
|
|
159
|
+
if len(before) > 1:
|
|
160
|
+
raise ValueError(t("diff_too_many_refs", count=str(len(before))))
|
|
161
|
+
refspec = before[0] if before else None
|
|
162
|
+
paths = after if after else None
|
|
163
|
+
return refspec, paths
|
|
164
|
+
|
|
165
|
+
|
|
125
166
|
def _run_diff(args: argparse.Namespace) -> int:
|
|
167
|
+
"""Dispatch to ``diff`` subcommand."""
|
|
126
168
|
from .diff import run_diff
|
|
169
|
+
from .output import error as error_out
|
|
170
|
+
|
|
171
|
+
refspec = args.refspec
|
|
172
|
+
paths = args.paths
|
|
173
|
+
# Honor `--` as a pathspec separator (git semantics). argparse's nargs="?"
|
|
174
|
+
# + nargs="*" otherwise mis-assigns path-only invocations to refspec.
|
|
175
|
+
if "--" in sys.argv:
|
|
176
|
+
try:
|
|
177
|
+
recovered_refspec, recovered_paths = _recover_diff_pathspec_boundary()
|
|
178
|
+
except ValueError as exc:
|
|
179
|
+
if args.json:
|
|
180
|
+
print_json(error_envelope(error=str(exc), code="too_many_refs"))
|
|
181
|
+
else:
|
|
182
|
+
error_out(str(exc))
|
|
183
|
+
return 1
|
|
184
|
+
if recovered_refspec is not None or recovered_paths is not None:
|
|
185
|
+
refspec = recovered_refspec
|
|
186
|
+
paths = recovered_paths
|
|
127
187
|
|
|
128
188
|
return run_diff(
|
|
189
|
+
refspec=refspec,
|
|
190
|
+
paths=paths,
|
|
129
191
|
staged=args.staged,
|
|
130
192
|
stat=args.stat,
|
|
131
193
|
name_only=args.name_only,
|
|
132
194
|
full=args.full,
|
|
195
|
+
summary=args.summary,
|
|
133
196
|
as_json=args.json,
|
|
134
197
|
)
|
|
135
198
|
|
|
136
199
|
|
|
137
200
|
def _run_log(args: argparse.Namespace) -> int:
|
|
201
|
+
"""Dispatch to ``log`` subcommand."""
|
|
138
202
|
from .log import run_log
|
|
139
203
|
|
|
140
204
|
return run_log(
|
|
@@ -151,12 +215,14 @@ def _run_log(args: argparse.Namespace) -> int:
|
|
|
151
215
|
|
|
152
216
|
|
|
153
217
|
def _run_show(args: argparse.Namespace) -> int:
|
|
218
|
+
"""Dispatch to ``show`` subcommand."""
|
|
154
219
|
from .show import run_show
|
|
155
220
|
|
|
156
221
|
return run_show(ref=args.ref, stat=args.stat, as_json=args.json)
|
|
157
222
|
|
|
158
223
|
|
|
159
224
|
def _run_commit(args: argparse.Namespace) -> int:
|
|
225
|
+
"""Dispatch to ``commit`` subcommand."""
|
|
160
226
|
from .commit import run_commit
|
|
161
227
|
|
|
162
228
|
return run_commit(
|
|
@@ -171,12 +237,14 @@ def _run_commit(args: argparse.Namespace) -> int:
|
|
|
171
237
|
|
|
172
238
|
|
|
173
239
|
def _run_branches(args: argparse.Namespace) -> int:
|
|
240
|
+
"""Dispatch to ``branches`` subcommand."""
|
|
174
241
|
from .branches import run_branches
|
|
175
242
|
|
|
176
243
|
return run_branches(stale=args.stale, remote=args.remote, sort=args.sort, as_json=args.json)
|
|
177
244
|
|
|
178
245
|
|
|
179
246
|
def _run_sync(args: argparse.Namespace) -> int:
|
|
247
|
+
"""Dispatch to ``sync`` subcommand."""
|
|
180
248
|
from .sync import run_sync
|
|
181
249
|
|
|
182
250
|
return run_sync(
|
|
@@ -189,12 +257,14 @@ def _run_sync(args: argparse.Namespace) -> int:
|
|
|
189
257
|
|
|
190
258
|
|
|
191
259
|
def _run_pr(args: argparse.Namespace) -> int:
|
|
260
|
+
"""Dispatch to ``pr`` subcommand."""
|
|
192
261
|
from .pr import run_pr
|
|
193
262
|
|
|
194
263
|
return run_pr(action=args.action, selector=args.selector, as_json=args.json)
|
|
195
264
|
|
|
196
265
|
|
|
197
266
|
def _run_undo(args: argparse.Namespace) -> int:
|
|
267
|
+
"""Dispatch to ``undo`` subcommand."""
|
|
198
268
|
from .undo import run_undo
|
|
199
269
|
|
|
200
270
|
return run_undo(
|
|
@@ -208,18 +278,21 @@ def _run_undo(args: argparse.Namespace) -> int:
|
|
|
208
278
|
|
|
209
279
|
|
|
210
280
|
def _run_context(args: argparse.Namespace) -> int:
|
|
281
|
+
"""Dispatch to ``context`` subcommand."""
|
|
211
282
|
from .context import run_context
|
|
212
283
|
|
|
213
284
|
return run_context(as_json=args.json)
|
|
214
285
|
|
|
215
286
|
|
|
216
287
|
def _run_health(args: argparse.Namespace) -> int:
|
|
288
|
+
"""Dispatch to ``health`` subcommand."""
|
|
217
289
|
from .health import run_health
|
|
218
290
|
|
|
219
291
|
return run_health(as_json=args.json)
|
|
220
292
|
|
|
221
293
|
|
|
222
294
|
def _run_stash(args: argparse.Namespace) -> int:
|
|
295
|
+
"""Dispatch to ``stash`` subcommand."""
|
|
223
296
|
from .stash import run_stash
|
|
224
297
|
|
|
225
298
|
return run_stash(
|
|
@@ -233,6 +306,7 @@ def _run_stash(args: argparse.Namespace) -> int:
|
|
|
233
306
|
|
|
234
307
|
|
|
235
308
|
def _run_tag(args: argparse.Namespace) -> int:
|
|
309
|
+
"""Dispatch to ``tag`` subcommand."""
|
|
236
310
|
from .tag import run_tag
|
|
237
311
|
|
|
238
312
|
return run_tag(
|
|
@@ -247,6 +321,7 @@ def _run_tag(args: argparse.Namespace) -> int:
|
|
|
247
321
|
|
|
248
322
|
|
|
249
323
|
def _run_merge(args: argparse.Namespace) -> int:
|
|
324
|
+
"""Dispatch to ``merge`` subcommand."""
|
|
250
325
|
from .merge import run_merge
|
|
251
326
|
|
|
252
327
|
return run_merge(
|
|
@@ -262,18 +337,21 @@ def _run_merge(args: argparse.Namespace) -> int:
|
|
|
262
337
|
|
|
263
338
|
|
|
264
339
|
def _run_conflicts(args: argparse.Namespace) -> int:
|
|
340
|
+
"""Dispatch to ``conflicts`` subcommand."""
|
|
265
341
|
from .conflicts import run_conflicts
|
|
266
342
|
|
|
267
343
|
return run_conflicts(ours=args.ours, theirs=args.theirs, as_json=args.json)
|
|
268
344
|
|
|
269
345
|
|
|
270
346
|
def _run_suggest(args: argparse.Namespace) -> int:
|
|
347
|
+
"""Dispatch to ``suggest`` subcommand."""
|
|
271
348
|
from .suggest import run_suggest
|
|
272
349
|
|
|
273
350
|
return run_suggest(as_json=args.json)
|
|
274
351
|
|
|
275
352
|
|
|
276
353
|
def _run_pick(args: argparse.Namespace) -> int:
|
|
354
|
+
"""Dispatch to ``pick`` / ``cherry-pick`` subcommand."""
|
|
277
355
|
from .pick import run_pick
|
|
278
356
|
|
|
279
357
|
return run_pick(
|
|
@@ -287,12 +365,18 @@ def _run_pick(args: argparse.Namespace) -> int:
|
|
|
287
365
|
|
|
288
366
|
|
|
289
367
|
def _run_status(args: argparse.Namespace) -> int:
|
|
368
|
+
"""Dispatch to ``status`` subcommand."""
|
|
290
369
|
from .status import run_status
|
|
291
370
|
|
|
292
371
|
return run_status(as_json=args.json)
|
|
293
372
|
|
|
294
373
|
|
|
295
374
|
def _run_completions(args: argparse.Namespace) -> int:
|
|
375
|
+
"""Generate and output a shell completions script.
|
|
376
|
+
|
|
377
|
+
Returns 1 on missing dependency (shtab), unsupported shell, or
|
|
378
|
+
runtime error.
|
|
379
|
+
"""
|
|
296
380
|
shell = args.shell
|
|
297
381
|
prog = args.prog
|
|
298
382
|
try:
|
|
@@ -346,6 +430,7 @@ def _run_completions(args: argparse.Namespace) -> int:
|
|
|
346
430
|
|
|
347
431
|
|
|
348
432
|
def _run_commands(args: argparse.Namespace) -> int:
|
|
433
|
+
"""List all registered subcommands with aliases."""
|
|
349
434
|
parser = build_parser()
|
|
350
435
|
commands = commands_metadata(parser)
|
|
351
436
|
payload = {
|
|
@@ -374,6 +459,10 @@ def _run_commands(args: argparse.Namespace) -> int:
|
|
|
374
459
|
|
|
375
460
|
|
|
376
461
|
def _run_schema(args: argparse.Namespace) -> int:
|
|
462
|
+
"""Print the JSON Schema for a subcommand's CLI input.
|
|
463
|
+
|
|
464
|
+
Returns 1 when the command is unknown or its schema file is missing.
|
|
465
|
+
"""
|
|
377
466
|
from .schema import load_command_input_schema
|
|
378
467
|
|
|
379
468
|
parser = build_parser()
|
gitwise/_cli_introspection.py
CHANGED
|
@@ -7,6 +7,7 @@ from . import __version__
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _json_safe(value: object) -> object:
|
|
10
|
+
"""Recursively convert an argparse value to a JSON-serializable form."""
|
|
10
11
|
if value is argparse.SUPPRESS:
|
|
11
12
|
return None
|
|
12
13
|
if value is None or isinstance(value, str | int | float | bool):
|
|
@@ -19,6 +20,7 @@ def _json_safe(value: object) -> object:
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def _subparsers_action(parser: argparse.ArgumentParser) -> argparse._SubParsersAction | None:
|
|
23
|
+
"""Return the first _SubParsersAction from a parser, or None."""
|
|
22
24
|
for action in parser._actions:
|
|
23
25
|
if isinstance(action, argparse._SubParsersAction):
|
|
24
26
|
return action
|
|
@@ -26,6 +28,7 @@ def _subparsers_action(parser: argparse.ArgumentParser) -> argparse._SubParsersA
|
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def _serialize_actions(parser: argparse.ArgumentParser) -> list[dict[str, object]]:
|
|
31
|
+
"""Serialize a parser's non-help, non-subparser actions into JSON-friendly dicts."""
|
|
29
32
|
items: list[dict[str, object]] = []
|
|
30
33
|
for action in parser._actions:
|
|
31
34
|
if action.dest in {"help"}:
|
|
@@ -49,6 +52,7 @@ def _serialize_actions(parser: argparse.ArgumentParser) -> list[dict[str, object
|
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
def extract_command_token(argv: list[str]) -> str | None:
|
|
55
|
+
"""Extract the subcommand name from a raw argv, skipping global flags and their values."""
|
|
52
56
|
skip_next = False
|
|
53
57
|
for token in argv:
|
|
54
58
|
if skip_next:
|
|
@@ -64,6 +68,7 @@ def extract_command_token(argv: list[str]) -> str | None:
|
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
def help_payload(parser: argparse.ArgumentParser, command: str | None = None) -> dict[str, object]:
|
|
71
|
+
"""Build a structured help payload for the root parser or a specific subcommand."""
|
|
67
72
|
payload: dict[str, object] = {
|
|
68
73
|
"v": 2,
|
|
69
74
|
"ok": True,
|
|
@@ -130,6 +135,8 @@ def help_payload(parser: argparse.ArgumentParser, command: str | None = None) ->
|
|
|
130
135
|
|
|
131
136
|
|
|
132
137
|
class CommandMetadata(TypedDict):
|
|
138
|
+
"""Minimal metadata for a single subcommand."""
|
|
139
|
+
|
|
133
140
|
name: str
|
|
134
141
|
help: str
|
|
135
142
|
aliases: list[str]
|
|
@@ -137,6 +144,7 @@ class CommandMetadata(TypedDict):
|
|
|
137
144
|
|
|
138
145
|
|
|
139
146
|
def canonical_command_name(command_parser: argparse.ArgumentParser) -> str:
|
|
147
|
+
"""Return the canonical name (last prog token) of a subcommand parser."""
|
|
140
148
|
prog = command_parser.prog.strip()
|
|
141
149
|
if not prog:
|
|
142
150
|
return ""
|
|
@@ -145,6 +153,7 @@ def canonical_command_name(command_parser: argparse.ArgumentParser) -> str:
|
|
|
145
153
|
|
|
146
154
|
|
|
147
155
|
def commands_metadata(parser: argparse.ArgumentParser) -> list[CommandMetadata]:
|
|
156
|
+
"""Return metadata for every unique subcommand, deduplicating by parser identity."""
|
|
148
157
|
sub_action = _subparsers_action(parser)
|
|
149
158
|
if sub_action is None:
|
|
150
159
|
return []
|
|
@@ -189,6 +198,7 @@ def commands_metadata(parser: argparse.ArgumentParser) -> list[CommandMetadata]:
|
|
|
189
198
|
|
|
190
199
|
|
|
191
200
|
def _json_type_for_action(action: argparse.Action) -> str:
|
|
201
|
+
"""Map an argparse action to a JSON Schema type string."""
|
|
192
202
|
if isinstance(action, argparse._StoreTrueAction | argparse._StoreFalseAction):
|
|
193
203
|
return "boolean"
|
|
194
204
|
|
|
@@ -201,6 +211,7 @@ def _json_type_for_action(action: argparse.Action) -> str:
|
|
|
201
211
|
|
|
202
212
|
|
|
203
213
|
def _action_property_schema(action: argparse.Action) -> dict[str, object]:
|
|
214
|
+
"""Build a JSON Schema property dict for a single argparse action."""
|
|
204
215
|
value_schema: dict[str, object] = {"type": _json_type_for_action(action)}
|
|
205
216
|
|
|
206
217
|
if action.choices:
|
|
@@ -230,6 +241,7 @@ def _action_property_schema(action: argparse.Action) -> dict[str, object]:
|
|
|
230
241
|
|
|
231
242
|
|
|
232
243
|
def _action_required(action: argparse.Action) -> bool:
|
|
244
|
+
"""Determine whether an argparse action produces a required JSON field."""
|
|
233
245
|
if action.option_strings:
|
|
234
246
|
return bool(getattr(action, "required", False))
|
|
235
247
|
|
|
@@ -242,6 +254,7 @@ def _action_required(action: argparse.Action) -> bool:
|
|
|
242
254
|
|
|
243
255
|
|
|
244
256
|
def command_input_schema(command_parser: argparse.ArgumentParser) -> dict[str, object]:
|
|
257
|
+
"""Generate a JSON Schema (draft 2020-12) describing the CLI input of a subcommand."""
|
|
245
258
|
properties: dict[str, object] = {}
|
|
246
259
|
required: list[str] = []
|
|
247
260
|
|
|
@@ -269,6 +282,7 @@ def command_input_schema(command_parser: argparse.ArgumentParser) -> dict[str, o
|
|
|
269
282
|
def resolve_command_parser(
|
|
270
283
|
*, parser: argparse.ArgumentParser, name: str
|
|
271
284
|
) -> argparse.ArgumentParser | None:
|
|
285
|
+
"""Look up the sub-parser registered under *name*, or return None."""
|
|
272
286
|
sub_action = _subparsers_action(parser)
|
|
273
287
|
if sub_action is None:
|
|
274
288
|
return None
|
gitwise/_cli_parser.py
CHANGED
|
@@ -8,10 +8,12 @@ from .i18n import t
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def _root_help_epilog() -> str:
|
|
11
|
+
"""Return the localized environment-variable epilog for root help."""
|
|
11
12
|
return t("help_root_environment_epilog")
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def build_parser() -> argparse.ArgumentParser:
|
|
16
|
+
"""Build and return the top-level argparse parser with all subcommands registered."""
|
|
15
17
|
parent = argparse.ArgumentParser(add_help=False)
|
|
16
18
|
parent.add_argument(
|
|
17
19
|
"--lang",
|
|
@@ -176,7 +178,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
176
178
|
diff_group.add_argument(
|
|
177
179
|
"--full", "--patch", action="store_true", help="show full patch with delta integration"
|
|
178
180
|
)
|
|
181
|
+
diff_group.add_argument(
|
|
182
|
+
"--summary",
|
|
183
|
+
action="store_true",
|
|
184
|
+
help="compact summary: additions/deletions per file, no patch",
|
|
185
|
+
)
|
|
179
186
|
p.add_argument("--stat", action="store_true", help="show diffstat (default behavior)")
|
|
187
|
+
p.add_argument(
|
|
188
|
+
"refspec",
|
|
189
|
+
nargs="?",
|
|
190
|
+
default=None,
|
|
191
|
+
help="commit, branch, or range to diff (e.g. HEAD~3, main..feat, main...feat)",
|
|
192
|
+
)
|
|
193
|
+
p.add_argument(
|
|
194
|
+
"paths",
|
|
195
|
+
nargs="*",
|
|
196
|
+
metavar="PATH",
|
|
197
|
+
help="limit to paths (use -- to separate from refspec)",
|
|
198
|
+
)
|
|
180
199
|
|
|
181
200
|
p = sub.add_parser("log", help="pretty git log with filters", parents=[parent])
|
|
182
201
|
p.add_argument("--oneline", action="store_true", help="one line per commit")
|
gitwise/_cli_setup_agents.py
CHANGED
|
@@ -194,6 +194,11 @@ def _run_setup_local(
|
|
|
194
194
|
providers: list[str] | None = None,
|
|
195
195
|
adapters_legacy_used: bool = False,
|
|
196
196
|
) -> int:
|
|
197
|
+
"""Install per-repo setup-agents artifacts (CLAUDE.md, settings, skills).
|
|
198
|
+
|
|
199
|
+
Returns 0 on success, 1 on plan/execution errors, 2 when ``--strict``
|
|
200
|
+
mode encounters warnings.
|
|
201
|
+
"""
|
|
197
202
|
cwd = target or Path.cwd()
|
|
198
203
|
|
|
199
204
|
if not is_repo(cwd):
|
gitwise/_i18n_data.json
CHANGED
|
@@ -447,10 +447,22 @@
|
|
|
447
447
|
"es": "Diagnóstico{suffix} — {count} observación(es):",
|
|
448
448
|
"en": "Diagnostic{suffix} — {count} finding(s):"
|
|
449
449
|
},
|
|
450
|
+
"diff_binary_lfs_hint": {
|
|
451
|
+
"es": "archivo binario {path} ({mib} MiB) -- considera Git LFS",
|
|
452
|
+
"en": "binary file {path} ({mib} MiB) -- consider Git LFS"
|
|
453
|
+
},
|
|
450
454
|
"diff_prefix": {
|
|
451
455
|
"es": "diff: {stat}",
|
|
452
456
|
"en": "diff: {stat}"
|
|
453
457
|
},
|
|
458
|
+
"diff_summary_header": {
|
|
459
|
+
"es": "Resumen -- {count} archivo(s):",
|
|
460
|
+
"en": "Summary -- {count} file(s):"
|
|
461
|
+
},
|
|
462
|
+
"diff_too_many_refs": {
|
|
463
|
+
"es": "demasiados argumentos posicionales antes de '--' ({count}); gitwise diff acepta un unico refspec (usa a..b para un rango)",
|
|
464
|
+
"en": "too many positional arguments before '--' ({count}); gitwise diff accepts a single refspec (use a..b for a range)"
|
|
465
|
+
},
|
|
454
466
|
"directory_exists": {
|
|
455
467
|
"es": "el directorio ya existe: {path}",
|
|
456
468
|
"en": "directory already exists: {path}"
|
gitwise/_runtime_config.py
CHANGED
|
@@ -16,6 +16,7 @@ _OSC_TIMEOUT = 0.5
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def _drain_fd(fd: int, timeout: float = 0.1) -> None:
|
|
19
|
+
"""Read and discard pending data from a file descriptor."""
|
|
19
20
|
while True:
|
|
20
21
|
ready, _, _ = select.select([fd], [], [], timeout)
|
|
21
22
|
if not ready:
|
|
@@ -27,6 +28,7 @@ def _drain_fd(fd: int, timeout: float = 0.1) -> None:
|
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def _query_bg_color() -> str | None:
|
|
31
|
+
"""Query the terminal background color via OSC 11; returns a hex string or None."""
|
|
30
32
|
if os.environ.get("NO_COLOR", "") != "":
|
|
31
33
|
return None
|
|
32
34
|
if os.environ.get("GITWISE_NO_COLOR", "").lower() in ("1", "true"):
|
|
@@ -67,6 +69,7 @@ def _query_bg_color() -> str | None:
|
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
def _read_osc_response(fd: int) -> str | None:
|
|
72
|
+
"""Read an OSC response from a tty fd, with a timeout."""
|
|
70
73
|
buf = bytearray()
|
|
71
74
|
osc_received = False
|
|
72
75
|
while len(buf) < 50:
|
|
@@ -92,6 +95,7 @@ def _read_osc_response(fd: int) -> str | None:
|
|
|
92
95
|
|
|
93
96
|
|
|
94
97
|
def _parse_osc_color(resp: str) -> str | None:
|
|
98
|
+
"""Extract a hex color from an OSC 11 response string."""
|
|
95
99
|
idx = resp.find("\x1b]")
|
|
96
100
|
if idx == -1:
|
|
97
101
|
return None
|
|
@@ -125,6 +129,7 @@ def _parse_osc_color(resp: str) -> str | None:
|
|
|
125
129
|
|
|
126
130
|
|
|
127
131
|
def _parse_rgb_component(s: str) -> int:
|
|
132
|
+
"""Parse a single hex RGB component, normalizing values wider than 8 bits."""
|
|
128
133
|
val = int(s, 16)
|
|
129
134
|
if len(s) > 2:
|
|
130
135
|
val = val >> (4 * (len(s) - 2))
|
|
@@ -132,20 +137,25 @@ def _parse_rgb_component(s: str) -> int:
|
|
|
132
137
|
|
|
133
138
|
|
|
134
139
|
def _relative_luminance(hex_color: str) -> float:
|
|
140
|
+
"""Compute the sRGB relative luminance of a hex color (WCG 2.4)."""
|
|
135
141
|
h = hex_color.lstrip("#")
|
|
136
142
|
r, g, b = int(h[0:2], 16) / 255, int(h[2:4], 16) / 255, int(h[4:6], 16) / 255
|
|
137
143
|
|
|
138
144
|
def linearize(c: float) -> float:
|
|
145
|
+
"""Apply the sRGB gamma curve."""
|
|
139
146
|
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
|
140
147
|
|
|
141
148
|
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)
|
|
142
149
|
|
|
143
150
|
|
|
144
151
|
def _is_dark_background(bg_hex: str) -> bool:
|
|
152
|
+
"""Return True if the background luminance is below the dark threshold."""
|
|
145
153
|
return _relative_luminance(bg_hex) < _BRIGHTNESS_THRESHOLD
|
|
146
154
|
|
|
147
155
|
|
|
148
156
|
class RuntimeConfig:
|
|
157
|
+
"""Immutable runtime settings detected from the environment."""
|
|
158
|
+
|
|
149
159
|
__slots__ = (
|
|
150
160
|
"_theme_tokens",
|
|
151
161
|
"color_depth",
|
|
@@ -167,6 +177,7 @@ class RuntimeConfig:
|
|
|
167
177
|
theme: str
|
|
168
178
|
|
|
169
179
|
def __init__(self) -> None:
|
|
180
|
+
"""Detect theme, TTY, tools, and color settings from the current environment."""
|
|
170
181
|
self.has_bat = bool(shutil.which("bat"))
|
|
171
182
|
self.has_delta = bool(shutil.which("delta"))
|
|
172
183
|
self.theme = self._detect_theme()
|
|
@@ -178,10 +189,12 @@ class RuntimeConfig:
|
|
|
178
189
|
|
|
179
190
|
@property
|
|
180
191
|
def theme_tokens(self) -> ThemeTokens:
|
|
192
|
+
"""Return the resolved ThemeTokens for the current theme."""
|
|
181
193
|
return self._theme_tokens
|
|
182
194
|
|
|
183
195
|
@staticmethod
|
|
184
196
|
def _detect_theme() -> str:
|
|
197
|
+
"""Detect theme preference from env vars, OSC 11 query, or COLORFGBG."""
|
|
185
198
|
explicit = os.environ.get("GITWISE_THEME", "").lower()
|
|
186
199
|
if explicit in ("dark", "light"):
|
|
187
200
|
return explicit
|
|
@@ -216,6 +229,7 @@ class RuntimeConfig:
|
|
|
216
229
|
|
|
217
230
|
@staticmethod
|
|
218
231
|
def _detect_terminal_width() -> int:
|
|
232
|
+
"""Return terminal width from GITWISE_WIDTH or shutil detection."""
|
|
219
233
|
from .design import MAX_WIDTH, MIN_WIDTH, detect_terminal_width
|
|
220
234
|
|
|
221
235
|
env_width = os.environ.get("GITWISE_WIDTH", "")
|
|
@@ -226,6 +240,7 @@ class RuntimeConfig:
|
|
|
226
240
|
|
|
227
241
|
@staticmethod
|
|
228
242
|
def _detect_color_depth() -> ColorDepth:
|
|
243
|
+
"""Delegate color-depth detection to design.detect_color_depth."""
|
|
229
244
|
from .design import detect_color_depth
|
|
230
245
|
|
|
231
246
|
return detect_color_depth()
|
|
@@ -235,6 +250,7 @@ _runtime_config: RuntimeConfig | None = None
|
|
|
235
250
|
|
|
236
251
|
|
|
237
252
|
def get_runtime_config() -> RuntimeConfig:
|
|
253
|
+
"""Return the module-level RuntimeConfig singleton, creating it on first call."""
|
|
238
254
|
global _runtime_config
|
|
239
255
|
if _runtime_config is None:
|
|
240
256
|
_runtime_config = RuntimeConfig()
|
|
@@ -242,5 +258,6 @@ def get_runtime_config() -> RuntimeConfig:
|
|
|
242
258
|
|
|
243
259
|
|
|
244
260
|
def reset_runtime_config() -> None:
|
|
261
|
+
"""Clear the cached RuntimeConfig so it is re-detected on next access."""
|
|
245
262
|
global _runtime_config
|
|
246
263
|
_runtime_config = None
|