amca 3.0.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.
- amca/__init__.py +17 -0
- amca/api.py +120 -0
- amca/cli/__init__.py +1 -0
- amca/cli/amca_cli.py +190 -0
- amca/cli/amcapl_cli.py +388 -0
- amca/cli/commands/__init__.py +1 -0
- amca/cli/commands/args_edit.py +67 -0
- amca/cli/commands/completions_cmd.py +136 -0
- amca/cli/commands/config_cmd.py +270 -0
- amca/cli/commands/doctor.py +161 -0
- amca/cli/commands/root_cmd.py +102 -0
- amca/cli/commands/run_cmd.py +36 -0
- amca/cli/common.py +142 -0
- amca/cli/completions.py +588 -0
- amca/cli/prompt.py +138 -0
- amca/config/__init__.py +1 -0
- amca/config/migrate.py +152 -0
- amca/config/schema.py +206 -0
- amca/config/store.py +256 -0
- amca/core/__init__.py +1 -0
- amca/core/context.py +162 -0
- amca/core/dirparse.py +76 -0
- amca/core/logger.py +233 -0
- amca/core/paths.py +103 -0
- amca/core/proc.py +114 -0
- amca/plugins/__init__.py +1 -0
- amca/plugins/argfiles.py +46 -0
- amca/plugins/loader.py +199 -0
- amca/plugins/markers.py +170 -0
- amca/plugins/registry.py +201 -0
- amca/plugins/sources.py +191 -0
- amca-3.0.0.dist-info/METADATA +548 -0
- amca-3.0.0.dist-info/RECORD +52 -0
- amca-3.0.0.dist-info/WHEEL +5 -0
- amca-3.0.0.dist-info/entry_points.txt +3 -0
- amca-3.0.0.dist-info/licenses/LICENSE +674 -0
- amca-3.0.0.dist-info/top_level.txt +2 -0
- amca_presets/__init__.py +14 -0
- amca_presets/autoscript/__init__.py +1 -0
- amca_presets/autoscript/_impl/__init__.py +1 -0
- amca_presets/autoscript/_impl/scripts.py +61 -0
- amca_presets/autoscript/plugin.py +152 -0
- amca_presets/example/__init__.py +1 -0
- amca_presets/example/plugin.py +106 -0
- amca_presets/meson/__init__.py +1 -0
- amca_presets/meson/_impl/__init__.py +1 -0
- amca_presets/meson/_impl/args.py +145 -0
- amca_presets/meson/_impl/project.py +93 -0
- amca_presets/meson/_impl/source_cache.py +93 -0
- amca_presets/meson/_impl/steps.py +248 -0
- amca_presets/meson/meson.build.template +55 -0
- amca_presets/meson/plugin.py +97 -0
amca/cli/amcapl_cli.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""``amcapl`` — install, enable, and inspect plugins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from ..config.store import ConfigError
|
|
10
|
+
from ..core.context import AmcaContext
|
|
11
|
+
from ..core.paths import remove_tree
|
|
12
|
+
from ..plugins.registry import Registry
|
|
13
|
+
from ..plugins.sources import Source, parse_source
|
|
14
|
+
from . import common
|
|
15
|
+
from .prompt import confirm, interactive, multiselect, select
|
|
16
|
+
|
|
17
|
+
__all__ = ["build_parser", "main"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="amcapl",
|
|
23
|
+
description="Manage amca plugins.",
|
|
24
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
25
|
+
epilog=(
|
|
26
|
+
"sources:\n"
|
|
27
|
+
" builtin presets shipped with amca (offline)\n"
|
|
28
|
+
" /path/to/dir a local directory of plugins\n"
|
|
29
|
+
" github:owner/repo@ref:subpath a GitHub tree (needs `requests`)\n"
|
|
30
|
+
"\n"
|
|
31
|
+
" configure with: amca config set plugins.sources builtin,/my/plugins\n"
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
common.add_global_flags(parser)
|
|
35
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
36
|
+
|
|
37
|
+
listing = sub.add_parser("list", aliases=["l"], help="show installed plugins")
|
|
38
|
+
listing.add_argument("--available", action="store_true",
|
|
39
|
+
help="also list what the configured sources offer")
|
|
40
|
+
listing.add_argument("--names", action="store_true",
|
|
41
|
+
help="print names only, one per line (for shell completion)")
|
|
42
|
+
listing.set_defaults(handler=_list)
|
|
43
|
+
|
|
44
|
+
for name, alias, help_text, handler in (
|
|
45
|
+
("enable", "e", "allow plugin(s) to run", _enable),
|
|
46
|
+
("disable", "d", "stop plugin(s) from running", _disable),
|
|
47
|
+
("toggle", "t", "flip plugin(s) between enabled and disabled", _toggle),
|
|
48
|
+
):
|
|
49
|
+
parser_ = sub.add_parser(name, aliases=[alias], help=help_text)
|
|
50
|
+
parser_.add_argument("plugins", nargs="*", metavar="PLUGIN")
|
|
51
|
+
parser_.set_defaults(handler=handler)
|
|
52
|
+
|
|
53
|
+
install = sub.add_parser("install", aliases=["i"], help="fetch plugin(s) from a source")
|
|
54
|
+
install.add_argument("plugins", nargs="*", metavar="PLUGIN")
|
|
55
|
+
install.add_argument("--source", default=None,
|
|
56
|
+
help="use only this source instead of plugins.sources")
|
|
57
|
+
install.add_argument("--no-enable", action="store_true",
|
|
58
|
+
help="install without enabling")
|
|
59
|
+
install.add_argument("--force", action="store_true",
|
|
60
|
+
help="overwrite an already-installed plugin")
|
|
61
|
+
install.set_defaults(handler=_install)
|
|
62
|
+
|
|
63
|
+
uninstall = sub.add_parser("uninstall", aliases=["u"], help="delete plugin(s) from disk")
|
|
64
|
+
uninstall.add_argument("plugins", nargs="*", metavar="PLUGIN")
|
|
65
|
+
uninstall.add_argument("-y", "--yes", action="store_true")
|
|
66
|
+
uninstall.set_defaults(handler=_uninstall)
|
|
67
|
+
|
|
68
|
+
update = sub.add_parser("update", aliases=["up"], help="re-fetch installed plugin(s)")
|
|
69
|
+
update.add_argument("plugins", nargs="*", metavar="PLUGIN",
|
|
70
|
+
help="omit or pass '*' for everything installed")
|
|
71
|
+
update.set_defaults(handler=_update)
|
|
72
|
+
|
|
73
|
+
call = sub.add_parser("call", aliases=["c"],
|
|
74
|
+
help="run one plugin directly, bypassing should_load")
|
|
75
|
+
call.add_argument("plugin", nargs="?")
|
|
76
|
+
call.add_argument("args", nargs=argparse.REMAINDER, metavar="ARG")
|
|
77
|
+
call.set_defaults(handler=_call)
|
|
78
|
+
|
|
79
|
+
return parser
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
def _sources(ctx: AmcaContext, override: str | None) -> list[Source]:
|
|
85
|
+
specs = [override] if override else [str(s) for s in ctx.config.get_list("plugins.sources")]
|
|
86
|
+
resolved: list[Source] = []
|
|
87
|
+
for spec in specs:
|
|
88
|
+
try:
|
|
89
|
+
resolved.append(parse_source(spec))
|
|
90
|
+
except ValueError as exc:
|
|
91
|
+
ctx.log.warning(f"skipping plugin source: {exc}")
|
|
92
|
+
return resolved
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _pick(ctx: AmcaContext, requested: list[str], options: list[str], message: str) -> list[str] | None:
|
|
96
|
+
"""Resolve an explicit list, or fall back to an interactive picker."""
|
|
97
|
+
if requested:
|
|
98
|
+
unknown = [name for name in requested if name not in options]
|
|
99
|
+
if unknown:
|
|
100
|
+
print(f"amcapl: not available: {', '.join(unknown)}")
|
|
101
|
+
print(f" choices: {', '.join(options) or '(none)'}")
|
|
102
|
+
return None
|
|
103
|
+
return requested
|
|
104
|
+
if not options:
|
|
105
|
+
print("nothing to choose from")
|
|
106
|
+
return []
|
|
107
|
+
chosen = multiselect(message, options)
|
|
108
|
+
if chosen is None:
|
|
109
|
+
# Genuine cancellation, or no TTY to ask on.
|
|
110
|
+
if not interactive():
|
|
111
|
+
print(f"amcapl: no plugin named — pass one or more names: {', '.join(options)}")
|
|
112
|
+
return None
|
|
113
|
+
return chosen
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _set_enabled(ctx: AmcaContext, names: list[str]) -> None:
|
|
117
|
+
ctx.config.set_persistent("plugins.enabled", sorted(set(names)))
|
|
118
|
+
ctx.config.save()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ── Commands ─────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
def _list(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
124
|
+
registry = Registry(ctx)
|
|
125
|
+
entries = registry.entries
|
|
126
|
+
prefix = ctx.config.get_str("plugins.marker_prefix")
|
|
127
|
+
|
|
128
|
+
if getattr(args, "names", False):
|
|
129
|
+
names = set(registry.names())
|
|
130
|
+
if getattr(args, "available", False):
|
|
131
|
+
for source in _sources(ctx, getattr(args, "source", None)):
|
|
132
|
+
try:
|
|
133
|
+
names.update(source.list_plugins())
|
|
134
|
+
except Exception: # noqa: S112 - a completion helper must never
|
|
135
|
+
continue # fail or print noise; an unreachable source
|
|
136
|
+
# simply contributes no candidates.
|
|
137
|
+
for name in sorted(names):
|
|
138
|
+
print(name)
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
print(f"installed in {ctx.plugins_dir}")
|
|
142
|
+
if not entries:
|
|
143
|
+
print(" (none)")
|
|
144
|
+
else:
|
|
145
|
+
width = max(len(entry.name) for entry in entries)
|
|
146
|
+
for entry in entries:
|
|
147
|
+
instance = registry.instantiate(entry)
|
|
148
|
+
state = "enabled " if entry.enabled else "disabled"
|
|
149
|
+
note = entry.error if instance is None else (instance.description or "")
|
|
150
|
+
print(f" {state} {entry.name:<{width}} {prefix}{entry.name.replace('_', '-')}"
|
|
151
|
+
+ (f" {note}" if note else ""))
|
|
152
|
+
|
|
153
|
+
missing = registry.missing_enabled()
|
|
154
|
+
if missing:
|
|
155
|
+
print(f"\nenabled but not installed: {', '.join(missing)}")
|
|
156
|
+
|
|
157
|
+
if getattr(args, "available", False):
|
|
158
|
+
print("\navailable from sources")
|
|
159
|
+
installed = set(registry.names())
|
|
160
|
+
for source in _sources(ctx, getattr(args, "source", None)):
|
|
161
|
+
try:
|
|
162
|
+
offered = source.list_plugins()
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
print(f" {source.label}: unavailable ({exc})")
|
|
165
|
+
continue
|
|
166
|
+
for name in offered:
|
|
167
|
+
mark = "installed" if name in installed else " "
|
|
168
|
+
print(f" {mark} {name} ({source.label})")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _enable(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
173
|
+
registry = Registry(ctx)
|
|
174
|
+
candidates = [name for name in registry.names() if name not in registry.enabled_names()]
|
|
175
|
+
if not args.plugins and not candidates:
|
|
176
|
+
if registry.names():
|
|
177
|
+
print("every installed plugin is already enabled")
|
|
178
|
+
else:
|
|
179
|
+
print(f"no plugins installed in {ctx.plugins_dir}")
|
|
180
|
+
print(" install one with: amcapl install")
|
|
181
|
+
return 0
|
|
182
|
+
chosen = _pick(ctx, args.plugins, candidates, "Enable which plugins?")
|
|
183
|
+
if chosen is None:
|
|
184
|
+
return 1
|
|
185
|
+
if not chosen:
|
|
186
|
+
print("nothing selected, nothing enabled")
|
|
187
|
+
return 0
|
|
188
|
+
_set_enabled(ctx, registry.enabled_names() + chosen)
|
|
189
|
+
print(f"enabled: {', '.join(chosen)}")
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _disable(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
194
|
+
registry = Registry(ctx)
|
|
195
|
+
enabled = registry.enabled_names()
|
|
196
|
+
if not args.plugins and not enabled:
|
|
197
|
+
print("no plugins are enabled")
|
|
198
|
+
return 0
|
|
199
|
+
chosen = _pick(ctx, args.plugins, enabled, "Disable which plugins?")
|
|
200
|
+
if chosen is None:
|
|
201
|
+
return 1
|
|
202
|
+
if not chosen:
|
|
203
|
+
print("nothing selected, nothing disabled")
|
|
204
|
+
return 0
|
|
205
|
+
_set_enabled(ctx, [name for name in enabled if name not in chosen])
|
|
206
|
+
print(f"disabled: {', '.join(chosen)}")
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _toggle(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
211
|
+
registry = Registry(ctx)
|
|
212
|
+
if not args.plugins and not registry.names():
|
|
213
|
+
print(f"no plugins installed in {ctx.plugins_dir}")
|
|
214
|
+
print(" install one with: amcapl install")
|
|
215
|
+
return 0
|
|
216
|
+
chosen = _pick(ctx, args.plugins, registry.names(), "Toggle which plugins?")
|
|
217
|
+
if chosen is None:
|
|
218
|
+
return 1
|
|
219
|
+
if not chosen:
|
|
220
|
+
print("nothing selected, nothing changed")
|
|
221
|
+
return 0
|
|
222
|
+
enabled = set(registry.enabled_names())
|
|
223
|
+
for name in chosen:
|
|
224
|
+
enabled.symmetric_difference_update({name})
|
|
225
|
+
_set_enabled(ctx, sorted(enabled))
|
|
226
|
+
print(f"enabled: {', '.join(sorted(enabled)) or '(none)'}")
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _install(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
231
|
+
sources = _sources(ctx, args.source)
|
|
232
|
+
if not sources:
|
|
233
|
+
print("amcapl: no usable plugin sources configured")
|
|
234
|
+
return 2
|
|
235
|
+
|
|
236
|
+
offered: dict[str, Source] = {}
|
|
237
|
+
for source in sources:
|
|
238
|
+
try:
|
|
239
|
+
for name in source.list_plugins():
|
|
240
|
+
offered.setdefault(name, source)
|
|
241
|
+
except Exception as exc:
|
|
242
|
+
ctx.log.warning(f"{source.label}: {exc}")
|
|
243
|
+
|
|
244
|
+
if not offered:
|
|
245
|
+
print("amcapl: no plugins offered by any configured source")
|
|
246
|
+
return 1
|
|
247
|
+
|
|
248
|
+
registry = Registry(ctx)
|
|
249
|
+
installed = set(registry.names())
|
|
250
|
+
# Explicit names are validated against everything on offer; the
|
|
251
|
+
# already-installed filter only narrows the *interactive* picker. Checking
|
|
252
|
+
# explicit names against the narrowed list reported an installed plugin as
|
|
253
|
+
# "not available", which is both wrong and unactionable.
|
|
254
|
+
candidates = sorted(offered) if args.force else sorted(set(offered) - installed)
|
|
255
|
+
chosen = _pick(
|
|
256
|
+
ctx, args.plugins,
|
|
257
|
+
sorted(offered) if args.plugins else (candidates or sorted(offered)),
|
|
258
|
+
"Install which plugins?",
|
|
259
|
+
)
|
|
260
|
+
if chosen is None:
|
|
261
|
+
return 1
|
|
262
|
+
if not chosen:
|
|
263
|
+
print("nothing selected, nothing installed")
|
|
264
|
+
print(f" available: {', '.join(sorted(offered))}")
|
|
265
|
+
print(f" or name them directly: amcapl install {' '.join(sorted(offered))}")
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
ctx.plugins_dir.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
newly: list[str] = []
|
|
270
|
+
for name in chosen:
|
|
271
|
+
target = ctx.plugins_dir / name
|
|
272
|
+
if target.exists() and not args.force:
|
|
273
|
+
print(f" {name}: already installed (use --force to overwrite)")
|
|
274
|
+
continue
|
|
275
|
+
if target.exists():
|
|
276
|
+
remove_tree(target)
|
|
277
|
+
try:
|
|
278
|
+
offered[name].fetch(name, target)
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
print(f" {name}: install failed — {exc}")
|
|
281
|
+
remove_tree(target)
|
|
282
|
+
continue
|
|
283
|
+
print(f" {name}: installed from {offered[name].label}")
|
|
284
|
+
newly.append(name)
|
|
285
|
+
|
|
286
|
+
if not newly and chosen:
|
|
287
|
+
# Everything requested was already present; that is not a failure the
|
|
288
|
+
# caller needs to branch on.
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
if newly and not args.no_enable:
|
|
292
|
+
_set_enabled(ctx, registry.enabled_names() + newly)
|
|
293
|
+
print(f"enabled: {', '.join(newly)}")
|
|
294
|
+
return 0 if newly else 1
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _uninstall(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
298
|
+
registry = Registry(ctx)
|
|
299
|
+
if not args.plugins and not registry.names():
|
|
300
|
+
print(f"no plugins installed in {ctx.plugins_dir}")
|
|
301
|
+
return 0
|
|
302
|
+
chosen = _pick(ctx, args.plugins, registry.names(), "Uninstall which plugins?")
|
|
303
|
+
if chosen is None:
|
|
304
|
+
return 1
|
|
305
|
+
if not chosen:
|
|
306
|
+
print("nothing selected, nothing removed")
|
|
307
|
+
return 0
|
|
308
|
+
if not args.yes and not confirm(f"Delete {', '.join(chosen)} from disk?", default=False):
|
|
309
|
+
print("cancelled")
|
|
310
|
+
return 1
|
|
311
|
+
for name in chosen:
|
|
312
|
+
remove_tree(ctx.plugins_dir / name)
|
|
313
|
+
print(f" {name}: removed")
|
|
314
|
+
_set_enabled(ctx, [n for n in registry.enabled_names() if n not in chosen])
|
|
315
|
+
return 0
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _update(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
319
|
+
registry = Registry(ctx)
|
|
320
|
+
installed = registry.names()
|
|
321
|
+
requested = [] if args.plugins in ([], ["*"]) else args.plugins
|
|
322
|
+
targets = requested or installed
|
|
323
|
+
if not targets:
|
|
324
|
+
print("no plugins installed")
|
|
325
|
+
return 0
|
|
326
|
+
namespace = argparse.Namespace(
|
|
327
|
+
plugins=targets, source=getattr(args, "source", None),
|
|
328
|
+
no_enable=True, force=True,
|
|
329
|
+
)
|
|
330
|
+
return _install(ctx, namespace, {})
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _call(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
334
|
+
registry = Registry(ctx)
|
|
335
|
+
name = args.plugin or select("Call which plugin?", registry.names())
|
|
336
|
+
if name is None:
|
|
337
|
+
return 1
|
|
338
|
+
entry = registry.get(name)
|
|
339
|
+
if entry is None:
|
|
340
|
+
print(f"amcapl: no plugin named {name!r} (installed: {', '.join(registry.names())})")
|
|
341
|
+
return 2
|
|
342
|
+
instance = registry.instantiate(entry)
|
|
343
|
+
if instance is None:
|
|
344
|
+
return 1
|
|
345
|
+
|
|
346
|
+
forwarded = list(args.args or [])
|
|
347
|
+
if forwarded and forwarded[0] == "--":
|
|
348
|
+
forwarded = forwarded[1:]
|
|
349
|
+
context = registry.make_context(name, forwarded, dry_run=False)
|
|
350
|
+
try:
|
|
351
|
+
return int(instance.load(context) or 0)
|
|
352
|
+
except KeyboardInterrupt:
|
|
353
|
+
return 130
|
|
354
|
+
except Exception as exc:
|
|
355
|
+
ctx.log.error(f"{name}: {type(exc).__name__}: {exc}")
|
|
356
|
+
if ctx.config.get_bool("core.debug"):
|
|
357
|
+
import traceback
|
|
358
|
+
|
|
359
|
+
traceback.print_exc()
|
|
360
|
+
return 1
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _main(argv: Sequence[str] | None = None) -> int:
|
|
364
|
+
parser = build_parser()
|
|
365
|
+
args = parser.parse_args(list(sys.argv[1:] if argv is None else argv))
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
ctx = AmcaContext(config_dir_override=args.config_dir)
|
|
369
|
+
common.apply_global_flags(ctx, args)
|
|
370
|
+
except ConfigError as exc:
|
|
371
|
+
print(f"amcapl: {exc}", file=sys.stderr)
|
|
372
|
+
return 2
|
|
373
|
+
|
|
374
|
+
common.first_run_completion_notice(ctx)
|
|
375
|
+
|
|
376
|
+
handler = getattr(args, "handler", None)
|
|
377
|
+
if handler is None:
|
|
378
|
+
parser.print_help()
|
|
379
|
+
return 0
|
|
380
|
+
return int(handler(ctx, args, {}) or 0)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
384
|
+
return common.run_entry(_main, argv)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if __name__ == "__main__": # pragma: no cover
|
|
388
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Individual amca subcommands, one module each."""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""``amca args`` — edit the per-project default arguments for a plugin.
|
|
2
|
+
|
|
3
|
+
Each enabled plugin can have ``<root>/.Amca/args/<plugin>.args``: one argument
|
|
4
|
+
per line, ``#`` comments ignored. Those lines are prepended to whatever the
|
|
5
|
+
user types after the plugin's marker, so a project can pin ``--buildtype=debug``
|
|
6
|
+
without anyone having to remember it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
|
|
13
|
+
from ...core import proc
|
|
14
|
+
from ...core.context import AmcaContext
|
|
15
|
+
from ...plugins.argfiles import write_template
|
|
16
|
+
from ...plugins.registry import Registry
|
|
17
|
+
from ..prompt import select
|
|
18
|
+
|
|
19
|
+
__all__ = ["register"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register(sub: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
24
|
+
parser = sub.add_parser("args", aliases=["a"],
|
|
25
|
+
help="edit per-project default arguments for a plugin")
|
|
26
|
+
parser.add_argument("plugin", nargs="?", help="plugin name (omit to pick from a list)")
|
|
27
|
+
parser.add_argument("--show", action="store_true", help="print instead of opening an editor")
|
|
28
|
+
parser.set_defaults(handler=handle)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def handle(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
34
|
+
root = ctx.find_root()
|
|
35
|
+
if root is None:
|
|
36
|
+
print("no amca root here — create one with `amca new`")
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
registry = Registry(ctx)
|
|
40
|
+
available = registry.names()
|
|
41
|
+
if not available:
|
|
42
|
+
print(f"no plugins installed in {ctx.plugins_dir}")
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
name = args.plugin
|
|
46
|
+
if name is None:
|
|
47
|
+
name = select("Which plugin's arguments?", available)
|
|
48
|
+
if name is None:
|
|
49
|
+
return 1
|
|
50
|
+
if name not in available:
|
|
51
|
+
print(f"amca: no plugin named {name!r} (installed: {', '.join(available)})")
|
|
52
|
+
return 2
|
|
53
|
+
|
|
54
|
+
root.args_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
target = root.args_dir / f"{name}.args"
|
|
56
|
+
|
|
57
|
+
if args.show:
|
|
58
|
+
if not target.exists():
|
|
59
|
+
print(f"(no argument file at {target})")
|
|
60
|
+
return 0
|
|
61
|
+
print(target.read_text(encoding="utf-8"), end="")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
if not target.exists():
|
|
65
|
+
write_template(target, name, ctx.config.get_str("plugins.marker_prefix"))
|
|
66
|
+
|
|
67
|
+
return proc.call([ctx.editor, str(target)])
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""``amca completions`` — print, install or remove shell completion scripts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from ...core.context import AmcaContext
|
|
9
|
+
from .. import completions as comp
|
|
10
|
+
|
|
11
|
+
__all__ = ["handle", "register"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register(sub: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
15
|
+
parser = sub.add_parser(
|
|
16
|
+
"completions",
|
|
17
|
+
help="print, install or remove shell tab-completion",
|
|
18
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
19
|
+
epilog=(
|
|
20
|
+
"A wheel cannot register completions at install time, so amca does it\n"
|
|
21
|
+
"on first run instead: the script is written to your shell's per-user\n"
|
|
22
|
+
"completion directory. Nothing is added to a shell rc file unless you\n"
|
|
23
|
+
"pass --rc.\n\n"
|
|
24
|
+
f"Set {comp.ENV_DISABLE}=1 to opt out entirely.\n"
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument("shell", nargs="?", choices=list(comp.SHELLS), default=None,
|
|
28
|
+
help="defaults to the shell named in $SHELL")
|
|
29
|
+
action = parser.add_mutually_exclusive_group()
|
|
30
|
+
action.add_argument("--install", action="store_true",
|
|
31
|
+
help="write the script to the standard per-user location")
|
|
32
|
+
action.add_argument("--uninstall", action="store_true",
|
|
33
|
+
help="remove the script and any rc block amca added")
|
|
34
|
+
action.add_argument("--status", action="store_true",
|
|
35
|
+
help="report where completions are, or would be, installed")
|
|
36
|
+
action.add_argument("--where", action="store_true",
|
|
37
|
+
help="print manual installation instructions")
|
|
38
|
+
parser.add_argument("--rc", action="store_true",
|
|
39
|
+
help="with --install, also add the fpath line to ~/.zshrc (zsh only)")
|
|
40
|
+
parser.add_argument("--command", action="append", default=[], metavar="NAME",
|
|
41
|
+
help="also complete this name as `amca` — for an alias or "
|
|
42
|
+
"wrapper (repeatable)")
|
|
43
|
+
parser.add_argument("--pl-command", dest="pl_command", action="append", default=[],
|
|
44
|
+
metavar="NAME", help="also complete this name as `amcapl`")
|
|
45
|
+
parser.set_defaults(handler=handle)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve(shell: str | None) -> str | None:
|
|
49
|
+
return shell or comp.detect_shell()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def handle(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
53
|
+
if args.status:
|
|
54
|
+
return _status()
|
|
55
|
+
|
|
56
|
+
shell = _resolve(args.shell)
|
|
57
|
+
if shell is None:
|
|
58
|
+
print(
|
|
59
|
+
f"amca: name a shell ({', '.join(comp.SHELLS)}); "
|
|
60
|
+
f"$SHELL={_shell_env()!r} was not recognised",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
return 2
|
|
64
|
+
|
|
65
|
+
if args.uninstall:
|
|
66
|
+
removed, edited = comp.uninstall(shell)
|
|
67
|
+
if not removed and not edited:
|
|
68
|
+
print(f"nothing installed for {shell}")
|
|
69
|
+
return 0
|
|
70
|
+
for path in removed:
|
|
71
|
+
print(f"deleted {path}")
|
|
72
|
+
for path in edited:
|
|
73
|
+
print(f"removed the amca block from {path}")
|
|
74
|
+
print(" restart your shell for it to take effect")
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
if args.install:
|
|
78
|
+
result = comp.install(shell, patch_rc=args.rc, names=args.command,
|
|
79
|
+
pl_names=args.pl_command)
|
|
80
|
+
if result.note and not result.written and "could not write" in result.note:
|
|
81
|
+
print(f"amca: {result.note}", file=sys.stderr)
|
|
82
|
+
return 1
|
|
83
|
+
print(f"{'installed' if result.written else 'already up to date'}: {result.path}")
|
|
84
|
+
if result.rc_patched and result.rc_file:
|
|
85
|
+
print(f"added an fpath block to {result.rc_file}")
|
|
86
|
+
elif result.note:
|
|
87
|
+
print(f"\n {result.note}")
|
|
88
|
+
if shell == "zsh":
|
|
89
|
+
for name in args.command:
|
|
90
|
+
print(f" for the alias {name!r}, also add to your rc: compdef _amca {name}")
|
|
91
|
+
for name in args.pl_command:
|
|
92
|
+
print(f" for the alias {name!r}, also add to your rc: compdef _amcapl {name}")
|
|
93
|
+
print("\n restart your shell (or `exec $SHELL`) to pick it up")
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
if args.where:
|
|
97
|
+
print(f"# {shell}\n {comp.install_hint(shell)}")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
if sys.stdout.isatty():
|
|
101
|
+
# Nobody wants 200 lines of shell in their scrollback; they meant to
|
|
102
|
+
# redirect it, or they wanted --install.
|
|
103
|
+
print(f"# amca {shell} completion. To install it for real:", file=sys.stderr)
|
|
104
|
+
print("# amca completions --install\n", file=sys.stderr)
|
|
105
|
+
|
|
106
|
+
print(comp.script_for(shell, names=args.command, pl_names=args.pl_command), end="")
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _shell_env() -> str:
|
|
111
|
+
import os
|
|
112
|
+
|
|
113
|
+
return os.environ.get("SHELL", "")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _status() -> int:
|
|
117
|
+
current = comp.detect_shell()
|
|
118
|
+
incomplete = False
|
|
119
|
+
for shell in comp.SHELLS:
|
|
120
|
+
path = comp.target_path(shell)
|
|
121
|
+
state = "installed" if comp.is_installed(shell) else "not installed"
|
|
122
|
+
mark = " <- your shell" if shell == current else ""
|
|
123
|
+
print(f" {shell:<5} {state:<14} {path}{mark}")
|
|
124
|
+
if shell == "zsh" and comp.is_installed("zsh") and comp.needs_fpath_line("zsh"):
|
|
125
|
+
incomplete = True
|
|
126
|
+
# "may not be on $fpath" understated it: without the fpath entry
|
|
127
|
+
# zsh never reads the file, so completion does not work at all.
|
|
128
|
+
print(f" NOT ACTIVE: zsh does not read {path.parent}.")
|
|
129
|
+
print(" Fix with: amca completions --install --rc")
|
|
130
|
+
|
|
131
|
+
print(f"\n helper used by the scripts: {comp._executable('amca')}")
|
|
132
|
+
if incomplete:
|
|
133
|
+
print(" (a file being 'installed' is not enough for zsh — see above)")
|
|
134
|
+
print(" aliases need their own registration: "
|
|
135
|
+
"amca completions --install --command <alias>")
|
|
136
|
+
return 0
|