agent-lock-cli 0.1.1__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.
- agent_lock/__init__.py +3 -0
- agent_lock/__main__.py +5 -0
- agent_lock/allow.py +40 -0
- agent_lock/cli.py +181 -0
- agent_lock/completion.py +414 -0
- agent_lock/darwin.py +276 -0
- agent_lock/discover.py +542 -0
- agent_lock/doctor.py +1347 -0
- agent_lock/errors.py +22 -0
- agent_lock/install.py +1024 -0
- agent_lock/linux.py +336 -0
- agent_lock/state.py +275 -0
- agent_lock/windows.py +706 -0
- agent_lock/wrapper.py +259 -0
- agent_lock_cli-0.1.1.dist-info/METADATA +139 -0
- agent_lock_cli-0.1.1.dist-info/RECORD +20 -0
- agent_lock_cli-0.1.1.dist-info/WHEEL +5 -0
- agent_lock_cli-0.1.1.dist-info/entry_points.txt +2 -0
- agent_lock_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
- agent_lock_cli-0.1.1.dist-info/top_level.txt +1 -0
agent_lock/__init__.py
ADDED
agent_lock/__main__.py
ADDED
agent_lock/allow.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""--allow-tcp values. Only 127.0.0.1:PORT and ::1:PORT."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
_VALUE = re.compile(r"^(127\.0\.0\.1|::1):([0-9]+)$")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_allow_tcp_value(value):
|
|
10
|
+
"""Return (host, port) or raise ValueError. Used by firewall renderers."""
|
|
11
|
+
match = _VALUE.match(value or "")
|
|
12
|
+
if not match:
|
|
13
|
+
raise ValueError(value)
|
|
14
|
+
port = int(match.group(2))
|
|
15
|
+
if match.group(2) != str(port):
|
|
16
|
+
# Reject leading zeros so the stored form is canonical.
|
|
17
|
+
raise ValueError(value)
|
|
18
|
+
if port < 1 or port > 65535:
|
|
19
|
+
raise ValueError(value)
|
|
20
|
+
return match.group(1), port
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def allow_tcp_arg(value):
|
|
24
|
+
"""argparse type. Illegal values are exit 2."""
|
|
25
|
+
try:
|
|
26
|
+
host, port = parse_allow_tcp_value(value)
|
|
27
|
+
except ValueError:
|
|
28
|
+
raise argparse.ArgumentTypeError(
|
|
29
|
+
"must be 127.0.0.1:PORT or ::1:PORT (port 1-65535), not {!r}".format(value))
|
|
30
|
+
return "{}:{}".format(host, port)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def port_arg(value):
|
|
34
|
+
try:
|
|
35
|
+
port = int(value)
|
|
36
|
+
except ValueError:
|
|
37
|
+
raise argparse.ArgumentTypeError("port must be an integer")
|
|
38
|
+
if str(port) != str(value).lstrip("+") or port < 1 or port > 65535:
|
|
39
|
+
raise argparse.ArgumentTypeError("port must be 1-65535")
|
|
40
|
+
return port
|
agent_lock/cli.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""agent-lock command line. Help text and examples come from this module."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from . import __version__
|
|
7
|
+
from .allow import allow_tcp_arg, port_arg
|
|
8
|
+
from .completion import SHELLS, script
|
|
9
|
+
from .errors import AgentLockError
|
|
10
|
+
from .install import cmd_bind_bin, cmd_doctor, cmd_exec, cmd_install, cmd_uninstall
|
|
11
|
+
|
|
12
|
+
COMMAND_GROUPS = (
|
|
13
|
+
("Setup", (
|
|
14
|
+
("install", "install the account, firewall, and wrappers"),
|
|
15
|
+
("uninstall", "remove the lock and keep linked directories"),
|
|
16
|
+
("bind-bin", "mark a directory setgid for new launchers"),
|
|
17
|
+
("completion", "print a shell completion script"),
|
|
18
|
+
)),
|
|
19
|
+
("Everyday", (
|
|
20
|
+
("doctor", "check the lock and repair cleared ACLs"),
|
|
21
|
+
("exec", "run one command as agentnet"),
|
|
22
|
+
)),
|
|
23
|
+
)
|
|
24
|
+
COMMAND_SUMMARY = {name: summary for _title, commands in COMMAND_GROUPS for name, summary in commands}
|
|
25
|
+
|
|
26
|
+
TOP_EXAMPLES = (
|
|
27
|
+
("agent-lock install", "install the lock"),
|
|
28
|
+
("agent-lock doctor", "check the lock"),
|
|
29
|
+
("agent-lock exec -- true", "run one command as agentnet"),
|
|
30
|
+
("agent-lock completion bash", "print a bash script"),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
COMMAND_EXAMPLES = {
|
|
34
|
+
"install": (
|
|
35
|
+
("agent-lock install", "install lock"),
|
|
36
|
+
("agent-lock install --port 7897 --allow-tcp 127.0.0.1:11434", "pin the port"),
|
|
37
|
+
("agent-lock install --controller 127.0.0.1:9097 --secret-stdin", "read secret"),
|
|
38
|
+
("agent-lock install --share-dir ~/.cx --trust-exec ~/bin", "share paths"),
|
|
39
|
+
),
|
|
40
|
+
"uninstall": (
|
|
41
|
+
("agent-lock uninstall", "remove the lock"),
|
|
42
|
+
("agent-lock uninstall", "safe to repeat"),
|
|
43
|
+
),
|
|
44
|
+
"doctor": (
|
|
45
|
+
("agent-lock doctor", "run every check"),
|
|
46
|
+
("agent-lock doctor --json", "print version 1 JSON"),
|
|
47
|
+
("agent-lock doctor --repair", "restore cleared ACLs"),
|
|
48
|
+
),
|
|
49
|
+
"exec": (
|
|
50
|
+
("agent-lock exec -- true", "run true as agentnet"),
|
|
51
|
+
("agent-lock exec -- curl -I https://example.com", "only via the proxy"),
|
|
52
|
+
("agent-lock exec -- git status", "run git as agentnet"),
|
|
53
|
+
),
|
|
54
|
+
"bind-bin": (
|
|
55
|
+
("sudo agent-lock bind-bin ~/.local/bin", "setgid this directory"),
|
|
56
|
+
("sudo agent-lock bind-bin /tmp/bin", "clear group write again"),
|
|
57
|
+
),
|
|
58
|
+
"completion": (
|
|
59
|
+
("eval \"$(agent-lock completion bash)\"", "add this to ~/.bashrc"),
|
|
60
|
+
("eval \"$(agent-lock completion zsh)\"", "run this after compinit"),
|
|
61
|
+
("agent-lock completion fish | source", "load the fish script"),
|
|
62
|
+
("agent-lock completion powershell", "print the PowerShell script"),
|
|
63
|
+
),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ALLOW_TCP_HELP = (
|
|
67
|
+
"loopback TCP port to allow (127.0.0.1:PORT or ::1:PORT). "
|
|
68
|
+
"A loopback service that then dials the network is a second exit beside the proxy."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _examples_text(examples):
|
|
73
|
+
"""examples: epilog. Command lines are padded to the longest plus two."""
|
|
74
|
+
width = max(len(command) for command, _summary in examples) + 2
|
|
75
|
+
lines = ["examples:"]
|
|
76
|
+
lines.extend(" {}{}".format(command.ljust(width), summary) for command, summary in examples)
|
|
77
|
+
return "\n".join(lines) + "\n"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _grouped_commands_text():
|
|
81
|
+
width = max(len(name) for name in COMMAND_SUMMARY) + 2
|
|
82
|
+
lines = []
|
|
83
|
+
for title, commands in COMMAND_GROUPS:
|
|
84
|
+
lines.append("{}:".format(title))
|
|
85
|
+
lines.extend(" {}{}".format(name.ljust(width), summary) for name, summary in commands)
|
|
86
|
+
lines.append("")
|
|
87
|
+
return "\n".join(lines).rstrip("\n")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
HELP_EXAMPLES = _examples_text(TOP_EXAMPLES) + "\nRun agent-lock COMMAND -h for more examples.\n"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_parser():
|
|
94
|
+
parser = argparse.ArgumentParser(
|
|
95
|
+
prog="agent-lock",
|
|
96
|
+
allow_abbrev=False,
|
|
97
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
98
|
+
description="Lock agent commands to the local HTTP proxy by running them as the user agentnet.\n\n"
|
|
99
|
+
+ _grouped_commands_text(),
|
|
100
|
+
epilog=HELP_EXAMPLES,
|
|
101
|
+
)
|
|
102
|
+
parser.add_argument("--version", action="version", version="%(prog)s " + __version__)
|
|
103
|
+
sub = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
104
|
+
sub.required = True
|
|
105
|
+
|
|
106
|
+
install = sub.add_parser("install", description=COMMAND_SUMMARY["install"], allow_abbrev=False)
|
|
107
|
+
install.add_argument("--port", type=port_arg, help="mixed port; required when discovery fails")
|
|
108
|
+
install.add_argument("--controller", help="loopback controller (host:port or unix:PATH)")
|
|
109
|
+
install.add_argument("--secret-stdin", action="store_true", help="read one secret line from stdin")
|
|
110
|
+
install.add_argument("--claude-dir", action="append", default=None, help="extra Claude settings directory")
|
|
111
|
+
install.add_argument("--share-dir", action="append", default=None, help="extra directory to share with agentnet")
|
|
112
|
+
install.add_argument("--trust-exec", action="append", default=None, help="program or directory agentnet may execute")
|
|
113
|
+
install.add_argument("--allow-tcp", action="append", default=None, type=allow_tcp_arg, help=ALLOW_TCP_HELP)
|
|
114
|
+
|
|
115
|
+
sub.add_parser("uninstall", description=COMMAND_SUMMARY["uninstall"], allow_abbrev=False)
|
|
116
|
+
|
|
117
|
+
doctor = sub.add_parser("doctor", description=COMMAND_SUMMARY["doctor"], allow_abbrev=False)
|
|
118
|
+
doctor.add_argument("--json", action="store_true", help="print version 1 JSON")
|
|
119
|
+
doctor.add_argument("--repair", action="store_true", help="reapply credential ACLs; does not change the port")
|
|
120
|
+
|
|
121
|
+
sub.add_parser("exec", description=COMMAND_SUMMARY["exec"], allow_abbrev=False)
|
|
122
|
+
|
|
123
|
+
bind = sub.add_parser("bind-bin", description=COMMAND_SUMMARY["bind-bin"], allow_abbrev=False)
|
|
124
|
+
bind.add_argument("path", metavar="PATH", help="directory to mark setgid for the agentnet group")
|
|
125
|
+
|
|
126
|
+
completion = sub.add_parser("completion", description=COMMAND_SUMMARY["completion"], allow_abbrev=False)
|
|
127
|
+
completion.add_argument("shell", choices=SHELLS, help="bash, zsh, fish, or powershell")
|
|
128
|
+
|
|
129
|
+
for name, subparser in sub.choices.items():
|
|
130
|
+
subparser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
131
|
+
subparser.epilog = _examples_text(COMMAND_EXAMPLES[name])
|
|
132
|
+
subparser.allow_abbrev = False
|
|
133
|
+
return parser
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _split_exec(argv):
|
|
137
|
+
"""Drop the command tail at the first --. Flags such as -h stay on the parser side."""
|
|
138
|
+
if not argv or argv[0] != "exec":
|
|
139
|
+
return argv, None
|
|
140
|
+
if "--" not in argv:
|
|
141
|
+
return argv, None
|
|
142
|
+
index = argv.index("--")
|
|
143
|
+
return argv[:index] or ["exec"], argv[index + 1:]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main(argv=None):
|
|
147
|
+
if argv is None:
|
|
148
|
+
argv = sys.argv[1:]
|
|
149
|
+
argv = list(argv)
|
|
150
|
+
argv, tail = _split_exec(argv)
|
|
151
|
+
try:
|
|
152
|
+
parser = build_parser()
|
|
153
|
+
if argv and argv[0] == "completion":
|
|
154
|
+
# Print the script even when agent-lock is not installed.
|
|
155
|
+
args = parser.parse_args(argv)
|
|
156
|
+
sys.stdout.write(script(args.shell, parser))
|
|
157
|
+
return 0
|
|
158
|
+
args = parser.parse_args(argv)
|
|
159
|
+
except SystemExit as exc:
|
|
160
|
+
code = exc.code
|
|
161
|
+
if code is None:
|
|
162
|
+
return 0
|
|
163
|
+
return int(code)
|
|
164
|
+
try:
|
|
165
|
+
if args.command == "install":
|
|
166
|
+
return cmd_install(args)
|
|
167
|
+
if args.command == "uninstall":
|
|
168
|
+
return cmd_uninstall(args)
|
|
169
|
+
if args.command == "doctor":
|
|
170
|
+
return cmd_doctor(args)
|
|
171
|
+
if args.command == "exec":
|
|
172
|
+
return cmd_exec(tail)
|
|
173
|
+
if args.command == "bind-bin":
|
|
174
|
+
return cmd_bind_bin(args)
|
|
175
|
+
if args.command == "completion":
|
|
176
|
+
sys.stdout.write(script(args.shell, parser))
|
|
177
|
+
return 0
|
|
178
|
+
except AgentLockError as exc:
|
|
179
|
+
sys.stderr.write("agent-lock: {}\n".format(exc))
|
|
180
|
+
return int(exc.code)
|
|
181
|
+
return 2
|
agent_lock/completion.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Generate bash, zsh, fish, and PowerShell completion from the argparse parser.
|
|
2
|
+
|
|
3
|
+
Subcommand names, options, and choices are read when the script is generated.
|
|
4
|
+
There is no second word list and no account lookup. Filename completion is only
|
|
5
|
+
for --claude-dir, --share-dir, --trust-exec, and the bind-bin positional.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
|
|
10
|
+
SHELLS = ("bash", "zsh", "fish", "powershell")
|
|
11
|
+
|
|
12
|
+
# These options take a path. Every other valued option completes nothing.
|
|
13
|
+
PATH_OPTIONS = ("--claude-dir", "--share-dir", "--trust-exec")
|
|
14
|
+
# Printed as separate `return $null` statements, in this order, then exec --.
|
|
15
|
+
NO_FILE_OPTIONS = ("--port", "--allow-tcp", "--controller")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def command_spec(parser):
|
|
19
|
+
"""Return {subcommand: (options, valued options)}. Help actions are skipped."""
|
|
20
|
+
spec = {}
|
|
21
|
+
for action in parser._actions: # noqa: SLF001 — argparse has no public walker
|
|
22
|
+
if not isinstance(action, argparse._SubParsersAction): # noqa: SLF001
|
|
23
|
+
continue
|
|
24
|
+
for name, sub in action.choices.items():
|
|
25
|
+
options = []
|
|
26
|
+
valued = []
|
|
27
|
+
for sub_action in sub._actions: # noqa: SLF001
|
|
28
|
+
if not sub_action.option_strings or sub_action.help == argparse.SUPPRESS:
|
|
29
|
+
continue
|
|
30
|
+
if isinstance(sub_action, argparse._HelpAction): # noqa: SLF001
|
|
31
|
+
continue
|
|
32
|
+
options.extend(sub_action.option_strings)
|
|
33
|
+
if sub_action.nargs != 0:
|
|
34
|
+
valued.extend(sub_action.option_strings)
|
|
35
|
+
spec[name] = (options, valued)
|
|
36
|
+
return spec
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _positional_choices(parser):
|
|
40
|
+
"""Choice words, and bind-bin's path positional, from one subparser walk."""
|
|
41
|
+
found = {}
|
|
42
|
+
files = []
|
|
43
|
+
for action in parser._actions: # noqa: SLF001
|
|
44
|
+
if not isinstance(action, argparse._SubParsersAction): # noqa: SLF001
|
|
45
|
+
continue
|
|
46
|
+
for name, sub in action.choices.items():
|
|
47
|
+
index = 0
|
|
48
|
+
for sub_action in sub._actions: # noqa: SLF001
|
|
49
|
+
if sub_action.option_strings or sub_action.help == argparse.SUPPRESS:
|
|
50
|
+
continue
|
|
51
|
+
if isinstance(sub_action, argparse._HelpAction): # noqa: SLF001
|
|
52
|
+
continue
|
|
53
|
+
if sub_action.choices and sub_action.nargs in (None, "?"):
|
|
54
|
+
found[(name, index)] = [str(choice) for choice in sub_action.choices]
|
|
55
|
+
elif not sub_action.choices and name == "bind-bin" and index == 0:
|
|
56
|
+
files.append((name, index))
|
|
57
|
+
if sub_action.nargs != "*":
|
|
58
|
+
index += 1
|
|
59
|
+
return found, files
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _path_options(spec):
|
|
63
|
+
present = []
|
|
64
|
+
valued = set()
|
|
65
|
+
for _name, pair in spec.items():
|
|
66
|
+
valued.update(pair[1])
|
|
67
|
+
for option in PATH_OPTIONS:
|
|
68
|
+
if option in valued:
|
|
69
|
+
present.append(option)
|
|
70
|
+
return present
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _no_file_options(spec):
|
|
74
|
+
valued = set()
|
|
75
|
+
for _name, pair in spec.items():
|
|
76
|
+
valued.update(pair[1])
|
|
77
|
+
return [option for option in NO_FILE_OPTIONS if option in valued]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def script(shell, parser):
|
|
81
|
+
spec = command_spec(parser)
|
|
82
|
+
choices, files = _positional_choices(parser)
|
|
83
|
+
if shell == "bash":
|
|
84
|
+
return _bash(spec, choices, files)
|
|
85
|
+
if shell == "zsh":
|
|
86
|
+
return _zsh(spec, choices, files)
|
|
87
|
+
if shell == "fish":
|
|
88
|
+
return _fish(spec, choices, files)
|
|
89
|
+
if shell == "powershell":
|
|
90
|
+
return _powershell(spec, choices, files)
|
|
91
|
+
raise ValueError(shell)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _words(items):
|
|
95
|
+
return " ".join(items)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _option_cases_bash(spec):
|
|
99
|
+
lines = []
|
|
100
|
+
for name, pair in sorted(spec.items()):
|
|
101
|
+
lines.append(" {name}) opts={opts}; vopts={vopts} ;;".format(
|
|
102
|
+
name=name,
|
|
103
|
+
opts=_sq(_words(pair[0])),
|
|
104
|
+
vopts=_sq(_words(pair[1])),
|
|
105
|
+
))
|
|
106
|
+
return "\n".join(lines)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _sq(text):
|
|
110
|
+
return "'" + text.replace("'", "'\\''") + "'"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _bash(spec, choices, files):
|
|
114
|
+
commands = sorted(spec)
|
|
115
|
+
path_pattern = "|".join(_path_options(spec)) or "__no_path_option__"
|
|
116
|
+
no_file_pattern = "|".join(_no_file_options(spec)) or "__no_value_option__"
|
|
117
|
+
positional = []
|
|
118
|
+
for key, words in sorted(choices.items()):
|
|
119
|
+
positional.append(" {cmd}:{index}) candidates={words} ;;".format(
|
|
120
|
+
cmd=key[0], index=key[1], words=_sq(_words(words))))
|
|
121
|
+
for command, index in files:
|
|
122
|
+
positional.append(
|
|
123
|
+
" {cmd}:{index}) COMPREPLY=($(compgen -f -- \"$cur\")); return 0 ;;".format(
|
|
124
|
+
cmd=command, index=index))
|
|
125
|
+
return """# bash completion for agent-lock. Generated from the parser. No bash-completion package.
|
|
126
|
+
_agent_lock() {{
|
|
127
|
+
# Split the raw line on whitespace. The default word breaks include a colon,
|
|
128
|
+
# which would cut 127.0.0.1:PORT and ::1:PORT into extra words.
|
|
129
|
+
local line="${{COMP_LINE:0:COMP_POINT}}"
|
|
130
|
+
local cur="${{line##* }}"
|
|
131
|
+
local before="${{line%%"$cur"}}"
|
|
132
|
+
local -a words
|
|
133
|
+
read -r -a words <<< "$before"
|
|
134
|
+
local cmd="" i w
|
|
135
|
+
for ((i = 1; i < ${{#words[@]}}; i++)); do
|
|
136
|
+
if [[ "${{words[i]}}" != -* ]]; then cmd="${{words[i]}}"; break; fi
|
|
137
|
+
done
|
|
138
|
+
local opts="" vopts=""
|
|
139
|
+
case "$cmd" in
|
|
140
|
+
{option_cases}
|
|
141
|
+
esac
|
|
142
|
+
local npos=0 skip=0 prev=""
|
|
143
|
+
for ((i = i + 1; i < ${{#words[@]}}; i++)); do
|
|
144
|
+
w="${{words[i]}}"
|
|
145
|
+
if ((skip)); then skip=0; prev=""; continue; fi
|
|
146
|
+
if [[ "$w" == "--" ]]; then npos=-1; break; fi
|
|
147
|
+
if [[ "$w" == -* ]]; then
|
|
148
|
+
if [[ " $vopts " == *" $w "* ]]; then skip=1; prev="$w"; fi
|
|
149
|
+
continue
|
|
150
|
+
fi
|
|
151
|
+
((npos++))
|
|
152
|
+
done
|
|
153
|
+
local candidates=""
|
|
154
|
+
if ((skip)); then
|
|
155
|
+
case "$prev" in
|
|
156
|
+
{path_pattern}) COMPREPLY=($(compgen -f -- "$cur")); return 0 ;;
|
|
157
|
+
{no_file_pattern}) COMPREPLY=(); return 0 ;;
|
|
158
|
+
*) COMPREPLY=(); return 0 ;;
|
|
159
|
+
esac
|
|
160
|
+
elif [[ -z "$cmd" ]]; then
|
|
161
|
+
candidates="{commands} --help --version"
|
|
162
|
+
elif ((npos < 0)); then
|
|
163
|
+
COMPREPLY=(); return 0
|
|
164
|
+
elif [[ "$cur" == -* ]]; then
|
|
165
|
+
candidates="$opts"
|
|
166
|
+
else
|
|
167
|
+
case "$cmd:$npos" in
|
|
168
|
+
{positional}
|
|
169
|
+
esac
|
|
170
|
+
fi
|
|
171
|
+
COMPREPLY=($(compgen -W "$candidates" -- "$cur"))
|
|
172
|
+
local j prefix=""
|
|
173
|
+
for ((j = ${{#cur}} - 1; j >= 0; j--)); do
|
|
174
|
+
case "$COMP_WORDBREAKS" in
|
|
175
|
+
*"${{cur:j:1}}"*) prefix="${{cur:0:j+1}}"; break ;;
|
|
176
|
+
esac
|
|
177
|
+
done
|
|
178
|
+
if [[ -n "$prefix" ]]; then
|
|
179
|
+
for i in "${{!COMPREPLY[@]}}"; do COMPREPLY[i]="${{COMPREPLY[i]#"$prefix"}}"; done
|
|
180
|
+
fi
|
|
181
|
+
}}
|
|
182
|
+
complete -F _agent_lock agent-lock
|
|
183
|
+
""".format(
|
|
184
|
+
option_cases=_option_cases_bash(spec),
|
|
185
|
+
commands=_words(commands),
|
|
186
|
+
path_pattern=path_pattern,
|
|
187
|
+
no_file_pattern=no_file_pattern,
|
|
188
|
+
positional="\n".join(positional),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _zsh(spec, choices, files):
|
|
193
|
+
commands = sorted(spec)
|
|
194
|
+
option_cases = []
|
|
195
|
+
for name, pair in sorted(spec.items()):
|
|
196
|
+
option_cases.append(" {name}) opts=({opts}); vopts=({vopts}) ;;".format(
|
|
197
|
+
name=name,
|
|
198
|
+
opts=_words([_sq(item) for item in pair[0]]),
|
|
199
|
+
vopts=_words([_sq(item) for item in pair[1]]),
|
|
200
|
+
))
|
|
201
|
+
path_pattern = "|".join(_path_options(spec)) or "__no_path_option__"
|
|
202
|
+
positional = []
|
|
203
|
+
for key, words in sorted(choices.items()):
|
|
204
|
+
positional.append(" {cmd}:{index}) candidates=({words}) ;;".format(
|
|
205
|
+
cmd=key[0], index=key[1], words=_words([_sq(word) for word in words])))
|
|
206
|
+
for command, index in files:
|
|
207
|
+
positional.append(" {cmd}:{index}) _files; return ;;".format(cmd=command, index=index))
|
|
208
|
+
return """#compdef agent-lock
|
|
209
|
+
# zsh completion for agent-lock. Generated from the parser.
|
|
210
|
+
_agent_lock() {{
|
|
211
|
+
local cmd="" i w
|
|
212
|
+
local -a opts vopts candidates
|
|
213
|
+
for ((i = 2; i < CURRENT; i++)); do
|
|
214
|
+
if [[ "${{words[i]}}" != -* ]]; then cmd="${{words[i]}}"; break; fi
|
|
215
|
+
done
|
|
216
|
+
case "$cmd" in
|
|
217
|
+
{option_cases}
|
|
218
|
+
esac
|
|
219
|
+
local npos=0 skip=0 prev=""
|
|
220
|
+
for ((i = i + 1; i < CURRENT; i++)); do
|
|
221
|
+
w="${{words[i]}}"
|
|
222
|
+
if ((skip)); then skip=0; prev=""; continue; fi
|
|
223
|
+
if [[ "$w" == "--" ]]; then npos=-1; break; fi
|
|
224
|
+
if [[ "$w" == -* ]]; then
|
|
225
|
+
if (( ${{vopts[(Ie)$w]}} )); then skip=1; prev="$w"; fi
|
|
226
|
+
continue
|
|
227
|
+
fi
|
|
228
|
+
((npos++))
|
|
229
|
+
done
|
|
230
|
+
local cur="${{words[CURRENT]}}"
|
|
231
|
+
if ((skip)); then
|
|
232
|
+
case "$prev" in
|
|
233
|
+
{path_pattern}) _files; return ;;
|
|
234
|
+
*) return ;;
|
|
235
|
+
esac
|
|
236
|
+
elif [[ -z "$cmd" ]]; then
|
|
237
|
+
candidates=({commands} --help --version)
|
|
238
|
+
elif ((npos < 0)); then
|
|
239
|
+
return
|
|
240
|
+
elif [[ "$cur" == -* ]]; then
|
|
241
|
+
candidates=($opts)
|
|
242
|
+
else
|
|
243
|
+
case "$cmd:$npos" in
|
|
244
|
+
{positional}
|
|
245
|
+
esac
|
|
246
|
+
fi
|
|
247
|
+
compadd -- $candidates
|
|
248
|
+
}}
|
|
249
|
+
compdef _agent_lock agent-lock
|
|
250
|
+
""".format(
|
|
251
|
+
option_cases="\n".join(option_cases),
|
|
252
|
+
commands=_words(commands),
|
|
253
|
+
path_pattern=path_pattern,
|
|
254
|
+
positional="\n".join(positional),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _fish(spec, choices, files):
|
|
259
|
+
commands = sorted(spec)
|
|
260
|
+
lines = [
|
|
261
|
+
"# fish completion for agent-lock. Generated from the parser.",
|
|
262
|
+
"complete -c agent-lock -f",
|
|
263
|
+
"function __agent_lock_positionals",
|
|
264
|
+
" set -l tokens (commandline -opc)",
|
|
265
|
+
" set -l value_options $argv",
|
|
266
|
+
" set -l seen 0",
|
|
267
|
+
" set -l skip 0",
|
|
268
|
+
" set -l count 0",
|
|
269
|
+
" for token in $tokens[2..-1]",
|
|
270
|
+
" if test $seen -eq 0",
|
|
271
|
+
" if not string match -q -- '-*' $token",
|
|
272
|
+
" set seen 1",
|
|
273
|
+
" end",
|
|
274
|
+
" continue",
|
|
275
|
+
" end",
|
|
276
|
+
" if test $skip -eq 1",
|
|
277
|
+
" set skip 0",
|
|
278
|
+
" continue",
|
|
279
|
+
" end",
|
|
280
|
+
" if test \"$token\" = '--'",
|
|
281
|
+
" echo -1",
|
|
282
|
+
" return",
|
|
283
|
+
" end",
|
|
284
|
+
" if string match -q -- '-*' $token",
|
|
285
|
+
" if contains -- $token $value_options",
|
|
286
|
+
" set skip 1",
|
|
287
|
+
" end",
|
|
288
|
+
" continue",
|
|
289
|
+
" end",
|
|
290
|
+
" set count (math $count + 1)",
|
|
291
|
+
" end",
|
|
292
|
+
" echo $count",
|
|
293
|
+
"end",
|
|
294
|
+
"complete -c agent-lock -n __fish_use_subcommand -a {}".format(_sq(_words(commands))),
|
|
295
|
+
"complete -c agent-lock -n __fish_use_subcommand -l help -l version",
|
|
296
|
+
]
|
|
297
|
+
paths = set(_path_options(spec))
|
|
298
|
+
for name, pair in sorted(spec.items()):
|
|
299
|
+
condition = "__fish_seen_subcommand_from {}".format(name)
|
|
300
|
+
for option in pair[0]:
|
|
301
|
+
flag = "-l {}".format(option[2:]) if option.startswith("--") else "-s {}".format(option[1:])
|
|
302
|
+
requires = " -r" if option in pair[1] else ""
|
|
303
|
+
files_flag = " -F" if option in paths else ""
|
|
304
|
+
lines.append("complete -c agent-lock -n {} {}{}{}".format(
|
|
305
|
+
_sq(condition), flag, requires, files_flag))
|
|
306
|
+
for key, words in sorted(choices.items()):
|
|
307
|
+
if key[0] != name:
|
|
308
|
+
continue
|
|
309
|
+
guard = "{}; and test (__agent_lock_positionals {}) -eq {}".format(
|
|
310
|
+
condition, _words(pair[1]), key[1])
|
|
311
|
+
lines.append("complete -c agent-lock -n {} -a {}".format(
|
|
312
|
+
_sq(guard), _sq(_words(words))))
|
|
313
|
+
for command, index in files:
|
|
314
|
+
pair = spec.get(command, ([], []))
|
|
315
|
+
guard = "__fish_seen_subcommand_from {}; and test (__agent_lock_positionals {}) -eq {}".format(
|
|
316
|
+
command, _words(pair[1]), index)
|
|
317
|
+
lines.append("complete -c agent-lock -n {} -a '(__fish_complete_path)'".format(_sq(guard)))
|
|
318
|
+
return "\n".join(lines) + "\n"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _ps_quote(text):
|
|
322
|
+
return "'" + text.replace("'", "''") + "'"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _ps_array(items):
|
|
326
|
+
return "@(" + ", ".join(_ps_quote(item) for item in items) + ")"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _powershell(spec, choices, files):
|
|
330
|
+
"""ASCII only. No-file positions return $null. Path positions bare-return."""
|
|
331
|
+
commands = sorted(spec)
|
|
332
|
+
option_lines = []
|
|
333
|
+
value_lines = []
|
|
334
|
+
for name in commands:
|
|
335
|
+
if spec[name][0]:
|
|
336
|
+
option_lines.append(" {} = {}".format(_ps_quote(name), _ps_array(spec[name][0])))
|
|
337
|
+
if spec[name][1]:
|
|
338
|
+
value_lines.append(" {} = {}".format(_ps_quote(name), _ps_array(spec[name][1])))
|
|
339
|
+
choice_lines = []
|
|
340
|
+
for key, words in sorted(choices.items()):
|
|
341
|
+
choice_lines.append(" {} = {}".format(
|
|
342
|
+
_ps_quote("{}:{}".format(key[0], key[1])), _ps_array(words)))
|
|
343
|
+
path_checks = []
|
|
344
|
+
for option in _path_options(spec):
|
|
345
|
+
path_checks.append(" if ($skip -and $lastOpt -eq {}) {{ return }}".format(_ps_quote(option)))
|
|
346
|
+
for command, index in files:
|
|
347
|
+
path_checks.append(
|
|
348
|
+
" if ($cmd -eq {cmd} -and -not $skip -and -not $afterDd -and"
|
|
349
|
+
" -not $wordToComplete.StartsWith('-') -and $npos -eq {index}) {{ return }}".format(
|
|
350
|
+
cmd=_ps_quote(command), index=index))
|
|
351
|
+
null_checks = []
|
|
352
|
+
for option in _no_file_options(spec):
|
|
353
|
+
null_checks.append(
|
|
354
|
+
" if ($skip -and $lastOpt -eq {}) {{ return $null }}".format(_ps_quote(option)))
|
|
355
|
+
null_checks.append(" if ($cmd -eq 'exec' -and $afterDd) { return $null }")
|
|
356
|
+
lines = [
|
|
357
|
+
"# agent-lock PowerShell completion. ASCII only.",
|
|
358
|
+
"Register-ArgumentCompleter -Native -CommandName agent-lock, agent-lock.exe -ScriptBlock {",
|
|
359
|
+
" param($wordToComplete, $commandAst, $cursorPosition)",
|
|
360
|
+
" $commands = {}".format(_ps_array(commands)),
|
|
361
|
+
" $options = @{",
|
|
362
|
+
]
|
|
363
|
+
lines.extend(option_lines)
|
|
364
|
+
lines.extend([
|
|
365
|
+
" }",
|
|
366
|
+
" $valueOptions = @{",
|
|
367
|
+
])
|
|
368
|
+
lines.extend(value_lines)
|
|
369
|
+
lines.extend([
|
|
370
|
+
" }",
|
|
371
|
+
" $positional = @{",
|
|
372
|
+
])
|
|
373
|
+
lines.extend(choice_lines)
|
|
374
|
+
lines.extend([
|
|
375
|
+
" }",
|
|
376
|
+
" $before = @($commandAst.CommandElements | Where-Object { $_.Extent.EndOffset -lt $cursorPosition } |",
|
|
377
|
+
" ForEach-Object { $_.Extent.Text })",
|
|
378
|
+
" $cmd = ''",
|
|
379
|
+
" $npos = 0",
|
|
380
|
+
" $skip = $false",
|
|
381
|
+
" $afterDd = $false",
|
|
382
|
+
" $lastOpt = ''",
|
|
383
|
+
" foreach ($w in ($before | Select-Object -Skip 1)) {",
|
|
384
|
+
" if ($cmd -eq '') {",
|
|
385
|
+
" if (-not $w.StartsWith('-')) { $cmd = $w }",
|
|
386
|
+
" continue",
|
|
387
|
+
" }",
|
|
388
|
+
" if ($w -eq '--') { $afterDd = $true; break }",
|
|
389
|
+
" if ($skip) { $skip = $false; continue }",
|
|
390
|
+
" if ($w.StartsWith('-')) {",
|
|
391
|
+
" if ($valueOptions[$cmd] -contains $w) { $skip = $true; $lastOpt = $w }",
|
|
392
|
+
" } else {",
|
|
393
|
+
" $npos++",
|
|
394
|
+
" }",
|
|
395
|
+
" }",
|
|
396
|
+
])
|
|
397
|
+
lines.extend(path_checks)
|
|
398
|
+
lines.extend(null_checks)
|
|
399
|
+
lines.extend([
|
|
400
|
+
" $candidates = @()",
|
|
401
|
+
" if ($cmd -eq '') {",
|
|
402
|
+
" $candidates = $commands",
|
|
403
|
+
" } elseif ($wordToComplete.StartsWith('-')) {",
|
|
404
|
+
" $candidates = $options[$cmd]",
|
|
405
|
+
" } else {",
|
|
406
|
+
" $candidates = $positional[($cmd + ':' + $npos)]",
|
|
407
|
+
" }",
|
|
408
|
+
" $candidates | Where-Object { $_ -and $_.StartsWith($wordToComplete, "
|
|
409
|
+
"[System.StringComparison]::OrdinalIgnoreCase) } |",
|
|
410
|
+
" ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }",
|
|
411
|
+
"}",
|
|
412
|
+
"",
|
|
413
|
+
])
|
|
414
|
+
return "\n".join(lines)
|