Add token list command

This commit is contained in:
mpabi
2026-04-26 22:28:31 +02:00
parent bd2d530852
commit 8c6f713de5
4 changed files with 300 additions and 53 deletions
+153 -6
View File
@@ -1277,6 +1277,24 @@ TOKEN_COLUMNS = [
("repo_mask", "repo", 6),
]
TOKEN_LIST_COLUMNS = [
("item", "item", 4),
("source", "source", 6),
("kind", "kind", 5),
("server", "server", 6),
("proto", "proto", 5),
("host", "host", 18),
("owner", "org", 9),
("repo_name", "repo", 11),
("user", "user", 4),
("remote", "remote", 6),
("token", "token", 12),
("valid", "valid", 19),
("scope_mask", "scope", 9),
("org_mask", "org", 6),
("repo_mask", "repo", 6),
]
TOKEN_SEPARATOR_OVERRIDES = {
"scope_mask": "aAimnopru",
"org_mask": "oawrc-",
@@ -1409,6 +1427,94 @@ def repo_token_rows(
return rows
def list_store_rows(store_servers: dict[str, dict], server_filter: str | None = None) -> list[dict[str, str]]:
rows: list[dict[str, str]] = []
endpoints = sorted(store_servers)
for endpoint in endpoints:
if server_filter and endpoint != server_filter:
continue
server_entry = store_servers[endpoint]
endpoint_values = endpoint_column_values(endpoint, None, server_entry)
for row in sorted(server_entry.get("tokens", []), key=lambda item: item.get("token_id", "")):
token_value = str(row.get("token_value", ""))
rows.append(
{
"item": str(len(rows) + 1),
"source": "store",
"kind": "auth" if token_value else "empty",
"server": endpoint_values["server"],
"proto": endpoint_values["scheme"],
"host": endpoint_values["host"],
"owner": str(row.get("owner", "")),
"repo_name": str(row.get("repo_name", "")),
"user": str(row.get("user", "")),
"remote": str(row.get("remote", "")),
"token": short_secret(token_value) if token_value else "",
"valid": str(row.get("valid", "")),
"scope_mask": str(row.get("scope_mask", "")),
"org_mask": str(row.get("org_mask", "")),
"repo_mask": str(row.get("repo_mask", "")),
}
)
return rows
def list_remote_rows(repo_servers: dict[str, dict], server_filter: str | None = None) -> list[dict[str, str]]:
rows: list[dict[str, str]] = []
endpoints = sorted(repo_servers)
for endpoint in endpoints:
if server_filter and endpoint != server_filter:
continue
server_entry = repo_servers[endpoint]
endpoint_values = endpoint_column_values(endpoint, server_entry, None)
remote_rows = []
for row in server_entry.get("tokens", []):
remote_rows.append(
{
"kind": "auth",
"remote": row.get("remote", ""),
"owner": row.get("org", ""),
"repo_name": row.get("repo", ""),
"user": row.get("user", ""),
"token_value": row.get("token_value", ""),
}
)
for row in server_entry.get("plain_remotes", []):
remote_rows.append(
{
"kind": "plain",
"remote": row.get("remote", ""),
"owner": row.get("org", ""),
"repo_name": row.get("repo", ""),
"user": "",
"token_value": "",
}
)
for row in sorted(remote_rows, key=lambda item: (item.get("remote", ""), item.get("kind", ""))):
token_value = str(row.get("token_value", ""))
rows.append(
{
"item": str(len(rows) + 1),
"source": "remote",
"kind": str(row.get("kind", "")),
"server": endpoint_values["server"],
"proto": endpoint_values["scheme"],
"host": endpoint_values["host"],
"owner": str(row.get("owner", "")),
"repo_name": str(row.get("repo_name", "")),
"user": str(row.get("user", "")),
"remote": str(row.get("remote", "")),
"token": short_secret(token_value) if token_value else "",
"valid": "",
"scope_mask": "",
"org_mask": "",
"repo_mask": "",
}
)
return rows
def fallback_project_by_endpoint(
endpoint_names: list[str],
repo_servers: dict[str, dict],
@@ -1750,7 +1856,7 @@ def safe_remote_url_label(remote_url: str | None) -> str:
def run_tokens_scan(config: WorkspaceConfig, args: argparse.Namespace) -> None:
repo_path = resolve_repo_argument(args.repo)
repo_path = resolve_launcher_repo_argument(args.repo)
token_data = load_token_store(config, write_normalized=False)
store_servers = store_servers_from_tokens(token_data)
report = scan_repo_remotes(config, repo_path)
@@ -1759,6 +1865,24 @@ def run_tokens_scan(config: WorkspaceConfig, args: argparse.Namespace) -> None:
print_token_item_rows(endpoint_names, repo_servers, store_servers, report["remote_rows"])
def run_tokens_list(config: WorkspaceConfig, args: argparse.Namespace) -> None:
rows: list[dict[str, str]] = []
if args.target in {"store", "both"}:
token_data = load_token_store(config, write_normalized=False)
store_servers = store_servers_from_tokens(token_data)
rows.extend(list_store_rows(store_servers, args.server))
if args.target in {"remote", "both"}:
repo_root, repo_servers = collect_repo_server_entries(config, resolve_launcher_repo_argument(args.repo))
if repo_root is None:
raise SystemExit(f"Missing git repo at path: {resolve_launcher_repo_argument(args.repo)}")
rows.extend(list_remote_rows(repo_servers, args.server))
for index, row in enumerate(rows, start=1):
row["item"] = str(index)
print_fixed_table("tokens", TOKEN_LIST_COLUMNS, rows)
def run_tokens_read(config: WorkspaceConfig, args: argparse.Namespace) -> None:
token_data = load_token_store(config, write_normalized=False)
servers = token_store_servers(token_data)
@@ -1804,7 +1928,7 @@ def run_tokens_read(config: WorkspaceConfig, args: argparse.Namespace) -> None:
def run_tokens_stats(config: WorkspaceConfig, args: argparse.Namespace) -> None:
repo_root, repo_servers = collect_repo_server_entries(config, resolve_repo_argument(args.repo))
repo_root, repo_servers = collect_repo_server_entries(config, resolve_launcher_repo_argument(args.repo))
token_data = load_token_store(config, write_normalized=False)
store_servers = store_servers_from_tokens(token_data)
endpoint_names = sorted(set(repo_servers) | set(store_servers))
@@ -2345,7 +2469,7 @@ def print_main_overview(config_path: Path) -> None:
["list-cards", "[series]", "list cards in a selected series"],
["tmux-container", "[series] [card]", "start tmux with container in pane 0"],
["submission", "[series] [card] --class K --nick N", "prepare answer repo and student branch"],
["tokens", "scan|sync|update|remove|add|read|stats|write", "manage tokens.json and Git remote credentials"],
["tokens", "scan|list|sync|update|remove|add|read|stats|write", "manage tokens.json and Git remote credentials"],
],
)
print()
@@ -2382,6 +2506,7 @@ def print_tokens_overview() -> None:
["command", "common options", "direction", "purpose"],
[
["scan", "[--repo PATH]", "read-only", "print token table with remote/store marker and auth masks"],
["list", "store|remote|both", "read-only", "list one source without comparing it"],
["add", "REMOTE_ID", "tokens.json", "add an empty token skeleton for manual editing"],
["read", "[--server ENDPOINT]", "tokens.json", "show servers, remote ids and tokens"],
["stats", "[--repo PATH]", "repo + tokens.json", "compare remote URLs with local token store"],
@@ -2397,7 +2522,9 @@ def print_tokens_overview() -> None:
print_table(
["case", "command"],
[
["scan current repo", "./rvctl tokens scan"],
["scan launcher repo", "./rvctl tokens scan"],
["list token store", "./rvctl tokens list store"],
["list launcher remotes", "./rvctl tokens list remote"],
["read remote r1 into store", "./rvctl tokens sync remote r1"],
["refresh r1 metadata", "./rvctl tokens update r1"],
["write store r1 to remote", "./rvctl tokens sync store r1 --repo PATH"],
@@ -2500,7 +2627,21 @@ def build_parser() -> argparse.ArgumentParser:
"scan",
help="Read repo remotes and tokens.json, then print paired token rows.",
)
add_repo_option(tokens_scan_parser)
tokens_scan_parser.add_argument(
"--repo",
help="Path inside a target git repo. Default: repo containing rvctl.",
)
tokens_list_parser = tokens_subparsers.add_parser(
"list",
help="List token store records, Git remotes, or both without comparing them.",
)
tokens_list_parser.add_argument("target", choices=["store", "remote", "both"], help="Source to list.")
tokens_list_parser.add_argument(
"--repo",
help="Path inside a target git repo for target remote/both. Default: repo containing rvctl.",
)
tokens_list_parser.add_argument("--server", help="Filter output to one server endpoint.")
tokens_read_parser = tokens_subparsers.add_parser(
"read",
@@ -2526,7 +2667,10 @@ def build_parser() -> argparse.ArgumentParser:
"stats",
help="Print per-server token statistics from tokens.json.",
)
add_repo_option(tokens_stats_parser)
tokens_stats_parser.add_argument(
"--repo",
help="Path inside a target git repo. Default: repo containing rvctl.",
)
tokens_stats_parser.add_argument("--server", help="Filter output to one server endpoint.")
tokens_sync_parser = tokens_subparsers.add_parser(
@@ -2621,6 +2765,9 @@ def main() -> int:
if args.tokens_command == "scan":
run_tokens_scan(config, args)
return 0
if args.tokens_command == "list":
run_tokens_list(config, args)
return 0
if args.tokens_command == "add":
run_tokens_add(config, args)
return 0