Some checks failed
tamq-ci / test (push) Failing after 7s
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
524 lines
24 KiB
Python
524 lines
24 KiB
Python
"""Initial tamq command-line surface."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import signal
|
|
import sqlite3
|
|
import fcntl
|
|
from uuid import uuid4
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
from . import __version__
|
|
from .config import db_path, pid_path, lock_path, config_path, socket_path, state_dir, setting
|
|
from .service import Service, ping, request
|
|
from .store import Store
|
|
from .tmux import TmuxError, TmuxManager
|
|
from .broker import BrokerIdentity, InputBroker
|
|
from .ptytap import PtyTap
|
|
from .control import ControlModeClient
|
|
from .diagnostics import configure
|
|
from .policy import load_profile
|
|
from .registry import RegistryError, validate_targets
|
|
from .terminal import format_comment
|
|
|
|
|
|
SUBCOMMANDS = frozenset(
|
|
"start attach serve stop status ping inbox history inspect ack send export replay purge completion db-version config tap".split()
|
|
)
|
|
START_OPTIONS = frozenset({"--command", "--cmd", "--tap", "--detach", "--no-service", "--no-display"})
|
|
GLOBAL_FLAGS = frozenset({"--orwell", "--verbose"})
|
|
|
|
|
|
def parse_size(value: str) -> int:
|
|
units = {"B": 1, "KB": 1000, "MB": 1000**2, "GB": 1000**3}
|
|
text = value.upper()
|
|
for unit, multiplier in sorted(units.items(), key=lambda item: -len(item[0])):
|
|
if text.endswith(unit):
|
|
return int(float(text[:-len(unit)]) * multiplier)
|
|
return int(text)
|
|
|
|
|
|
def history_advisory(store: Store) -> str | None:
|
|
size, oldest = store.history_stats()
|
|
limit_text = setting("history_max_size", "100MB")
|
|
limit = parse_size(limit_text)
|
|
if size <= limit:
|
|
return None
|
|
since = time.strftime("%y%m%d", time.localtime(oldest)) if oldest else "unknown"
|
|
return f"Size of history is {size / 1000**2:.0f} MB since {since}, exceeding {limit_text}. Consider using 'tamq purge'."
|
|
|
|
|
|
def ensure_service() -> bool:
|
|
if asyncio.run(ping()):
|
|
return True
|
|
path = lock_path()
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with path.open("a+") as lock:
|
|
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
|
if asyncio.run(ping()):
|
|
return True
|
|
subprocess.Popen(
|
|
[sys.executable, "-m", "tamq.cli", "serve"],
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
start_new_session=True,
|
|
env=os.environ.copy(),
|
|
)
|
|
for _ in range(20):
|
|
if asyncio.run(ping()):
|
|
return True
|
|
time.sleep(0.05)
|
|
return False
|
|
|
|
|
|
def stop_service_process() -> int | None:
|
|
try:
|
|
pid = int(pid_path().read_text(encoding="utf-8"))
|
|
except (FileNotFoundError, ValueError):
|
|
return None
|
|
try:
|
|
os.kill(pid, signal.SIGTERM)
|
|
except ProcessLookupError:
|
|
pid_path().unlink(missing_ok=True)
|
|
return None
|
|
deadline = time.monotonic() + 2.0
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
os.kill(pid, 0)
|
|
except ProcessLookupError:
|
|
break
|
|
time.sleep(0.05)
|
|
return pid
|
|
|
|
|
|
def ensure_service_capabilities(required: set[str]) -> bool:
|
|
"""Restart an older broker once before registering a newer endpoint mode."""
|
|
if not ensure_service():
|
|
return False
|
|
try:
|
|
capabilities = asyncio.run(request({"op": "ping"})).get("capabilities", [])
|
|
except (OSError, json.JSONDecodeError):
|
|
return False
|
|
if required.issubset(capabilities):
|
|
return True
|
|
stop_service_process()
|
|
if not ensure_service():
|
|
return False
|
|
try:
|
|
capabilities = asyncio.run(request({"op": "ping"})).get("capabilities", [])
|
|
except (OSError, json.JSONDecodeError):
|
|
return False
|
|
return required.issubset(capabilities)
|
|
|
|
|
|
def ensure_manual_service() -> bool:
|
|
return ensure_service_capabilities({"manual_delivery"})
|
|
|
|
|
|
def ensure_output_service() -> bool:
|
|
return ensure_service_capabilities({"manual_delivery", "terminal_output"})
|
|
|
|
|
|
def preflight_runtime_paths() -> None:
|
|
"""Ensure local state/runtime parents are available before tmux mutation."""
|
|
parents = {db_path().parent, socket_path().parent, pid_path().parent, lock_path().parent}
|
|
for parent in parents:
|
|
parent.mkdir(parents=True, exist_ok=True)
|
|
if not os.access(parent, os.W_OK | os.X_OK):
|
|
raise OSError(f"runtime directory is not writable: {parent}")
|
|
|
|
|
|
def attach_session(session: str) -> int:
|
|
command = ["tmux", "switch-client" if os.environ.get("TMUX") else "attach-session", "-t", session]
|
|
return subprocess.run(command, check=False).returncode
|
|
|
|
|
|
def format_comment_message(row: sqlite3.Row) -> str:
|
|
"""Render a durable message so every displayed line remains a shell comment."""
|
|
return format_comment(row["sender_repo"], row["body"], row["message_id"])
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
prog="tamq",
|
|
description="Repository-aware tmux sessions with durable local messaging.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""session shorthand:
|
|
tamq [--detach] [--command COMMAND] [--no-display] REPO [REPO ...]
|
|
|
|
With no --command, tamq opens ordinary repository shells. --command is an
|
|
explicit initial command for newly created windows. Messages appear as
|
|
sanitized terminal output by default; --no-display keeps them inbox-only.
|
|
Neither mode injects message bytes into pane input.
|
|
|
|
manual messaging from a managed shell:
|
|
@TARGET: MESSAGE...
|
|
tamq inbox [--filter COMMAND]
|
|
""",
|
|
)
|
|
parser.add_argument("--version", "-V", action="version", version=__version__)
|
|
parser.add_argument("--orwell", action="store_true", help="enable unsafe local diagnostics")
|
|
parser.add_argument("--verbose", action="store_true", help="enable debug diagnostics")
|
|
parser.add_argument("--policy-profile", default=None, help="select configured policy profile")
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
start = subparsers.add_parser("start", help="start or reuse a tamq endpoint")
|
|
start.add_argument("repos", nargs="*", help="gita-registered repository slugs")
|
|
start.add_argument("--command", "--cmd", dest="initial_command", default=None, help="explicit initial command for newly created windows (default: ordinary shell)")
|
|
start.add_argument("--tap", action="store_true", help="explicitly opt in to PTY input observation and pane message injection")
|
|
start.add_argument("--no-display", action="store_true", help="keep messages in the durable inbox without writing target terminal output")
|
|
start.add_argument("--detach", action="store_true", help="detach after startup")
|
|
start.add_argument("--no-service", action="store_true", help="open tmux windows without service registration or messaging")
|
|
attach = subparsers.add_parser("attach", help="attach to the managed tmux session")
|
|
attach.add_argument("--session", default="tamq", help="tmux session name")
|
|
|
|
subparsers.add_parser("serve", help="run the local service in the foreground")
|
|
subparsers.add_parser("stop", help="stop the local service")
|
|
subparsers.add_parser("status", help="show endpoint health")
|
|
subparsers.add_parser("ping", help="check local service liveness")
|
|
history = subparsers.add_parser("history", help="inspect local message history")
|
|
history.add_argument("--repo", dest="target_repo")
|
|
history.add_argument("--state")
|
|
inbox = subparsers.add_parser("inbox", help="show durable messages for a repository without injecting terminal input")
|
|
inbox.add_argument("--repo", dest="target_repo", help="target repository (default: TAMQ_REPO in a managed window)")
|
|
inbox.add_argument("--all", action="store_true", help="include non-pending messages")
|
|
inbox.add_argument("--json", action="store_true", help="emit one JSON object per message")
|
|
inbox.add_argument(
|
|
"--filter",
|
|
dest="filter_command",
|
|
help="consume each pending comment through COMMAND and acknowledge it after a zero exit",
|
|
)
|
|
inspect = subparsers.add_parser("inspect", help="inspect one message")
|
|
inspect.add_argument("message_id")
|
|
ack = subparsers.add_parser("ack", help="acknowledge one durable message")
|
|
ack.add_argument("message_id")
|
|
send = subparsers.add_parser("send", help="queue a direct message")
|
|
send.add_argument("address", help="@repo: message")
|
|
send.add_argument("body", nargs="*", help="message body when address is a repo slug")
|
|
send.add_argument("--endpoint-id")
|
|
send.add_argument("--from", dest="sender_repo", help="sender repository (default: TAMQ_REPO or local)")
|
|
export = subparsers.add_parser("export", help="export history as JSONL")
|
|
export.add_argument("--output", required=True)
|
|
export.add_argument("--repo", dest="target_repo")
|
|
export.add_argument("--state")
|
|
replay = subparsers.add_parser("replay", help="replay a JSONL message file")
|
|
replay.add_argument("file")
|
|
replay.add_argument("--endpoint-id", help="target a registered tmux-amq endpoint")
|
|
purge = subparsers.add_parser("purge", help="dry-run or remove history")
|
|
purge.add_argument("--before", default=None)
|
|
purge.add_argument("--max-size", default=None)
|
|
purge.add_argument("--yes", action="store_true")
|
|
purge.add_argument("--orwell", action="store_true")
|
|
completion = subparsers.add_parser("completion", help="emit shell completion script")
|
|
completion.add_argument("shell", choices=("bash", "zsh", "fish"))
|
|
subparsers.add_parser("db-version", help="show SQLite schema version")
|
|
subparsers.add_parser("config", help="show effective configuration paths and policy")
|
|
tap = subparsers.add_parser("tap", help="explicitly run a command behind the full-duplex PTY tap")
|
|
tap.add_argument("--repo", required=True, help="source gita repository slug")
|
|
tap.add_argument("--endpoint", required=True, help="tmux-amq endpoint identity")
|
|
tap.add_argument("wrapped_command", nargs=argparse.REMAINDER, help="command after --")
|
|
return parser
|
|
|
|
|
|
def normalize_argv(argv: list[str]) -> list[str]:
|
|
"""Allow repository-first startup while retaining explicit subcommands."""
|
|
if not argv:
|
|
return argv
|
|
index = 0
|
|
while index < len(argv):
|
|
token = argv[index]
|
|
if token in GLOBAL_FLAGS or token.startswith("--policy-profile="):
|
|
index += 1
|
|
continue
|
|
if token == "--policy-profile" and index + 1 < len(argv):
|
|
index += 2
|
|
continue
|
|
break
|
|
if index == len(argv):
|
|
return argv
|
|
candidate = argv[index]
|
|
if candidate in START_OPTIONS or (
|
|
not candidate.startswith("-") and candidate not in SUBCOMMANDS
|
|
):
|
|
return [*argv[:index], "start", *argv[index:]]
|
|
return argv
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = build_parser()
|
|
raw_argv = list(sys.argv[1:] if argv is None else argv)
|
|
args = parser.parse_args(normalize_argv(raw_argv))
|
|
configure(args.orwell, args.verbose)
|
|
try:
|
|
profile = load_profile(selected=args.policy_profile or os.environ.get("TAMQ_POLICY_PROFILE", "default"))
|
|
except ValueError as exc:
|
|
print(f"tamq: {exc}", file=sys.stderr)
|
|
return 2
|
|
if args.command is None:
|
|
parser.print_help()
|
|
return 0
|
|
if args.command == "db-version":
|
|
store = Store(db_path()); print(store.db.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()[0]); store.close(); return 0
|
|
if args.command == "ping":
|
|
return 0 if asyncio.run(ping()) else 1
|
|
if args.command == "stop":
|
|
pid = stop_service_process()
|
|
if pid is not None:
|
|
print(f"stopped tamq service {pid}")
|
|
return 0
|
|
print("tamq service is not running", file=sys.stderr)
|
|
return 1
|
|
if args.command == "serve":
|
|
try: asyncio.run(Service().run())
|
|
except KeyboardInterrupt: return 0
|
|
return 0
|
|
if args.command == "attach":
|
|
return attach_session(args.session)
|
|
if args.command == "completion":
|
|
print(completion_script(args.shell), end="")
|
|
return 0
|
|
if args.command == "config":
|
|
print(json.dumps({"config": str(config_path()), "state_dir": str(state_dir()), "database": str(db_path()), "socket": str(socket_path()), "pidfile": str(pid_path()), "lockfile": str(lock_path()), "policy_profile": profile.name, "delivery_ack_mode": profile.delivery_ack_mode}, sort_keys=True))
|
|
return 0
|
|
if args.command == "tap":
|
|
command = list(args.wrapped_command)
|
|
if command and command[0] == "--":
|
|
command = command[1:]
|
|
if not command:
|
|
print("tamq tap requires a command after --", file=sys.stderr)
|
|
return 2
|
|
store = Store(db_path())
|
|
try:
|
|
return PtyTap(command, InputBroker(store, BrokerIdentity(args.endpoint, args.repo))).run()
|
|
finally:
|
|
store.close()
|
|
if args.command == "start":
|
|
if args.tap and args.initial_command is None:
|
|
print("tamq: --tap requires an explicit --command", file=sys.stderr)
|
|
return 2
|
|
if args.tap and args.no_service:
|
|
print("tamq: --tap cannot be combined with --no-service", file=sys.stderr)
|
|
return 2
|
|
if args.tap and args.no_display:
|
|
print("tamq: --tap cannot be combined with --no-display", file=sys.stderr)
|
|
return 2
|
|
manager = TmuxManager()
|
|
try:
|
|
if not args.no_service:
|
|
preflight_runtime_paths()
|
|
launch_plan = manager.preflight(args.repos, args.initial_command)
|
|
except (TmuxError, OSError) as exc:
|
|
print(f"tamq: {exc}", file=sys.stderr)
|
|
return 2
|
|
service_ready = (
|
|
ensure_manual_service() if args.no_display else ensure_output_service()
|
|
) if not args.no_service else True
|
|
if not service_ready:
|
|
print("tamq service failed to start with the required delivery capability; use --no-service to open repos without messaging", file=sys.stderr)
|
|
return 1
|
|
try:
|
|
endpoint = manager.ensure_plan(launch_plan, tap=args.tap)
|
|
except (TmuxError, OSError) as exc:
|
|
print(f"tamq: {exc}", file=sys.stderr)
|
|
return 2
|
|
delivered = 0
|
|
registered = False
|
|
if not args.no_service:
|
|
try:
|
|
delivery_mode = "pane" if args.tap else ("manual" if args.no_display else "output")
|
|
registration = asyncio.run(request({"op": "register", "endpoint_id": endpoint.endpoint_id, "instance_id": endpoint.instance_key, "pid": endpoint.pid, "session": endpoint.session, "repos": endpoint.repos, "delivery_mode": delivery_mode}))
|
|
if not registration.get("ok"):
|
|
raise RuntimeError(f"endpoint registration failed: {registration.get('error', 'unknown error')}")
|
|
registered = True
|
|
if args.tap:
|
|
control = ControlModeClient(endpoint.session)
|
|
queue_store = Store(db_path())
|
|
try:
|
|
control.start()
|
|
delivered = InputBroker(queue_store, BrokerIdentity(endpoint.instance_key, endpoint.repos[0])).deliver_pending(
|
|
control, window_for_repo=lambda repo: f"{endpoint.session}:{repo}"
|
|
)
|
|
finally:
|
|
control.close()
|
|
queue_store.close()
|
|
except (OSError, RuntimeError, ValueError, json.JSONDecodeError) as exc:
|
|
manager.rollback(endpoint)
|
|
print(f"tamq: {exc}", file=sys.stderr)
|
|
return 1
|
|
summary = {
|
|
"endpoint_id": endpoint.endpoint_id,
|
|
"instance_id": endpoint.instance_key,
|
|
"session": endpoint.session,
|
|
"repos": endpoint.repos,
|
|
"delivered": delivered,
|
|
"service": not args.no_service,
|
|
"messaging": registered,
|
|
"delivery_mode": "none" if args.no_service else ("pane" if args.tap else ("manual" if args.no_display else "output")),
|
|
}
|
|
print(json.dumps(summary), flush=True)
|
|
if args.detach:
|
|
return 0
|
|
return attach_session(endpoint.session)
|
|
store = Store(db_path())
|
|
try:
|
|
advisory = history_advisory(store)
|
|
if advisory and args.command in {"start", "serve", "status", "history", "inbox", "send"}:
|
|
print(advisory, file=sys.stderr)
|
|
if args.command == "send":
|
|
text = " ".join([args.address, *args.body]).strip()
|
|
if not text.startswith("@") or ":" not in text:
|
|
print("tamq send expects @repo: message", file=sys.stderr); return 2
|
|
target, body = text[1:].split(":", 1); body = body.strip()
|
|
if not target or not body: print("tamq send requires a target and body", file=sys.stderr); return 2
|
|
sender = args.sender_repo or os.environ.get("TAMQ_REPO") or "local"
|
|
try:
|
|
validate_targets([target])
|
|
if sender != "local":
|
|
validate_targets([sender])
|
|
except RegistryError as exc:
|
|
print(f"tamq: {exc}", file=sys.stderr); return 2
|
|
if asyncio.run(ping()):
|
|
payload = {"op": "send", "sender_repo": sender, "target_repo": target, "body": body}
|
|
if args.endpoint_id:
|
|
payload["endpoint_id"] = args.endpoint_id
|
|
response = asyncio.run(request(payload))
|
|
if not response.get("ok"):
|
|
print(f"tamq: {response.get('error', 'send failed')}", file=sys.stderr); return 1
|
|
print(response["message_id"]); return 0
|
|
if args.endpoint_id:
|
|
print("tamq: service is not running", file=sys.stderr); return 1
|
|
try:
|
|
print(store.add(sender, target, body))
|
|
except ValueError as exc:
|
|
print(f"tamq: {exc}", file=sys.stderr); return 2
|
|
return 0
|
|
if args.command == "inbox":
|
|
target = args.target_repo or os.environ.get("TAMQ_REPO")
|
|
if not target:
|
|
print("tamq inbox requires --repo outside a managed tamq window", file=sys.stderr)
|
|
return 2
|
|
try:
|
|
validate_targets([target])
|
|
except RegistryError as exc:
|
|
print(f"tamq: {exc}", file=sys.stderr); return 2
|
|
if args.filter_command is not None and not args.filter_command.strip():
|
|
print("tamq: --filter command must not be empty", file=sys.stderr)
|
|
return 2
|
|
if args.filter_command and (args.all or args.json):
|
|
print("tamq: --filter cannot be combined with --all or --json", file=sys.stderr)
|
|
return 2
|
|
rows = store.list(target, None if args.all else "pending")
|
|
for row in rows:
|
|
if args.json:
|
|
print(json.dumps(dict(row), sort_keys=True))
|
|
elif args.filter_command:
|
|
environment = os.environ.copy()
|
|
environment.update(
|
|
{
|
|
"TAMQ_MESSAGE_ID": row["message_id"],
|
|
"TAMQ_SENDER_REPO": row["sender_repo"],
|
|
"TAMQ_TARGET_REPO": row["target_repo"],
|
|
}
|
|
)
|
|
result = subprocess.run(
|
|
args.filter_command,
|
|
shell=True,
|
|
input=format_comment_message(row) + "\n",
|
|
text=True,
|
|
env=environment,
|
|
check=False,
|
|
)
|
|
if result.returncode:
|
|
print(
|
|
f"tamq: filter failed for {row['message_id']} with exit status {result.returncode}; message remains pending",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
store.acknowledge(row["message_id"])
|
|
else:
|
|
print(format_comment_message(row))
|
|
return 0
|
|
if args.command == "history":
|
|
for row in store.list(args.target_repo, args.state): print(json.dumps(dict(row), sort_keys=True))
|
|
return 0
|
|
if args.command == "inspect":
|
|
rows = [row for row in store.list() if row["message_id"] == args.message_id]
|
|
if not rows:
|
|
print("tamq: message not found", file=sys.stderr)
|
|
return 1
|
|
print(json.dumps(dict(rows[0]), sort_keys=True)); return 0
|
|
if args.command == "ack":
|
|
if not store.acknowledge(args.message_id):
|
|
print("tamq: message not found", file=sys.stderr); return 1
|
|
print(args.message_id); return 0
|
|
if args.command == "export":
|
|
store.export(store.list(args.target_repo, args.state), Path(args.output)); return 0
|
|
if args.command == "replay":
|
|
batch_id = f"replay-{uuid4()}"
|
|
count = 0
|
|
if args.endpoint_id and store.endpoint(args.endpoint_id) is None:
|
|
print(f"tamq: endpoint is not registered: {args.endpoint_id}", file=sys.stderr)
|
|
return 1
|
|
try:
|
|
lines = Path(args.file).read_text(encoding="utf-8").splitlines()
|
|
for line in lines:
|
|
if not line.strip():
|
|
continue
|
|
item = json.loads(line)
|
|
provenance = json.dumps({"batch_id": batch_id, "original_message_id": item.get("message_id")}, sort_keys=True)
|
|
endpoint = args.endpoint_id or item.get("endpoint_id")
|
|
store.add(item.get("sender_repo", "replay"), item["target_repo"], item["body"], endpoint=endpoint, provenance=provenance)
|
|
count += 1
|
|
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
print(f"tamq: cannot replay {args.file}: {exc}", file=sys.stderr)
|
|
return 2
|
|
print(json.dumps({"batch_id": batch_id, "count": count})); return 0
|
|
if args.command == "purge":
|
|
before_text = args.before or setting("purge_before", "365d")
|
|
max_size_text = args.max_size or setting("purge_max_size", "100MB")
|
|
before = time.time() - int(before_text[:-1]) * 86400 if before_text.endswith("d") else None
|
|
if not args.yes:
|
|
matching = [row for row in store.list() if before is None or row["created_at"] < before]
|
|
print(f"Dry run: {len(matching)} message(s) match; re-run with --yes to purge.")
|
|
return 0
|
|
count = store.purge(before=before, max_bytes=parse_size(max_size_text))
|
|
print(f"Purged {count} message(s)."); return 0
|
|
if args.command == "status":
|
|
live = asyncio.run(ping())
|
|
endpoints = asyncio.run(request({"op": "endpoints"})) if live else {"endpoints": []}
|
|
print(json.dumps({"version": __version__, "db": str(db_path()), "messages": len(store.list()), "pending": len(store.list(state="pending")), "leases": store.db.execute("SELECT COUNT(*) FROM leases").fetchone()[0], "service": live, "policy_profile": profile.name, "delivery_ack_mode": profile.delivery_ack_mode, "endpoints": endpoints.get("endpoints", [])}, sort_keys=True)); return 0
|
|
print(f"tamq {__version__}: command '{args.command}' is not implemented yet", file=sys.stderr)
|
|
return 2
|
|
finally:
|
|
store.close()
|
|
|
|
|
|
def completion_script(shell: str) -> str:
|
|
commands = " ".join(sorted(SUBCOMMANDS))
|
|
if shell == "bash":
|
|
return f"""_tamq_complete() {{
|
|
local commands=\"{commands}\"
|
|
if [[ ${{COMP_WORDS[1]}} == start || ${{COMP_WORDS[1]}} == send ]]; then
|
|
COMPREPLY=( $(compgen -W \"$(gita ls 2>/dev/null)\" -- \"${{COMP_WORDS[COMP_CWORD]}}\") )
|
|
else
|
|
COMPREPLY=( $(compgen -W \"$commands\" -- \"${{COMP_WORDS[COMP_CWORD]}}\") )
|
|
fi
|
|
}}
|
|
complete -F _tamq_complete tamq
|
|
"""
|
|
if shell == "zsh":
|
|
return f"#compdef tamq\n_arguments '1:command:(({commands}))' '*:repo:->repos'\nif [[ $state == repos ]]; then _values 'gita repo' $(gita ls 2>/dev/null); fi\n"
|
|
return f"complete -c tamq -f -n '__fish_seen_subcommand_from start send' -a '(gita ls 2>/dev/null)'\ncomplete -c tamq -f -a '{commands}'\n"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|