Add endpoint-aware token management commands
This commit is contained in:
@@ -1,10 +1,19 @@
|
|||||||
# RV Launcher
|
# RV Launcher
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
|
Mateusz Pabiszczak, 2026
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Draft
|
||||||
|
|
||||||
Launcher do workspace `rv`.
|
Launcher do workspace `rv`.
|
||||||
|
|
||||||
- `workspace.json` trzyma sciezki workspace i tools
|
- `workspace.json` trzyma sciezki workspace i tools
|
||||||
- `workspace.py` listuje serie i karty
|
- `workspace.py` listuje serie i karty
|
||||||
- szczegolowy opis przelacznikow skryptu jest w `doc/usage.md`
|
- szczegolowy opis przelacznikow skryptu jest w `doc/usage.md`
|
||||||
|
- model tokenow i synchronizacji repo <-> store jest w `doc/tokens.md`
|
||||||
|
|
||||||
## Przygotowanie katalogu
|
## Przygotowanie katalogu
|
||||||
|
|
||||||
@@ -57,6 +66,13 @@ Minimalny format:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"version": 2,
|
||||||
|
"servers": {
|
||||||
|
"http://77.90.8.171:3001": {
|
||||||
|
"type": "gitea",
|
||||||
|
"scheme": "http",
|
||||||
|
"host": "77.90.8.171",
|
||||||
|
"port": 3001,
|
||||||
"users": {
|
"users": {
|
||||||
"u1": {
|
"u1": {
|
||||||
"tokens": {
|
"tokens": {
|
||||||
@@ -65,6 +81,8 @@ Minimalny format:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Plik powinien byc lokalny, niewersjonowany i miec prawa `600`.
|
Plik powinien byc lokalny, niewersjonowany i miec prawa `600`.
|
||||||
@@ -87,6 +105,14 @@ git remote add r1 http://77.90.8.171:3001/edu-tools/rv-launcher.git
|
|||||||
Launcher umie tez przemigrowac stary plik `tokens/gitea_tokens.json` do nowego
|
Launcher umie tez przemigrowac stary plik `tokens/gitea_tokens.json` do nowego
|
||||||
`tokens/tokens.json`.
|
`tokens/tokens.json`.
|
||||||
|
|
||||||
|
Podstawowe komendy tokenow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens scan
|
||||||
|
python3 workspace.py tokens read
|
||||||
|
python3 workspace.py tokens stats
|
||||||
|
```
|
||||||
|
|
||||||
## Fetch i switch
|
## Fetch i switch
|
||||||
|
|
||||||
Domyslna galaz launchera to `main`.
|
Domyslna galaz launchera to `main`.
|
||||||
|
|||||||
+231
@@ -0,0 +1,231 @@
|
|||||||
|
# Tokens
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
|
Mateusz Pabiszczak, 2026
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Draft
|
||||||
|
|
||||||
|
Plik opisuje aktualny model pracy z tokenami w launcherze.
|
||||||
|
|
||||||
|
## Zrodlo prawdy
|
||||||
|
|
||||||
|
Sa dwa miejsca, w ktorych moga byc zapisane tokeny:
|
||||||
|
|
||||||
|
- remote URL-e w repo, na przyklad `http://LOGIN:TOKEN@host/org/repo.git`
|
||||||
|
- lokalny plik `~/dev/workspace/rv/tokens/tokens.json`
|
||||||
|
|
||||||
|
Jesli remote URL zawiera `LOGIN:TOKEN@...`, to remote jest zrodlem prawdy.
|
||||||
|
Launcher moze wtedy zeskanowac repo i zapisac ten token do `tokens.json`.
|
||||||
|
|
||||||
|
Jesli remote nie ma tokena, `tokens.json` moze byc uzyty jako lokalny store
|
||||||
|
przy operacjach `tokens write` i `tokens update --from store`.
|
||||||
|
|
||||||
|
## Format `tokens.json`
|
||||||
|
|
||||||
|
Tokeny sa trzymane per endpoint serwera, a dopiero pod nim per user i token:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"servers": {
|
||||||
|
"http://77.90.8.171:3001": {
|
||||||
|
"type": "gitea",
|
||||||
|
"scheme": "http",
|
||||||
|
"host": "77.90.8.171",
|
||||||
|
"port": 3001,
|
||||||
|
"users": {
|
||||||
|
"u1": {
|
||||||
|
"tokens": {
|
||||||
|
"t1": "SECRET"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To pozwala odroznic:
|
||||||
|
|
||||||
|
- typ serwera, na przyklad `gitea`, `github`, `gitlab`, `unknown`
|
||||||
|
- endpoint, czyli `scheme + host + port`
|
||||||
|
- uzytkownikow na danym serwerze
|
||||||
|
- wiele tokenow dla jednego usera
|
||||||
|
|
||||||
|
## Skanowanie remota
|
||||||
|
|
||||||
|
Przy `tokens scan` launcher:
|
||||||
|
|
||||||
|
- czyta wszystkie remote URL-e w repo
|
||||||
|
- wybiera tylko `http` i `https`
|
||||||
|
- jesli URL ma `LOGIN:TOKEN@...`, wyciaga login i token
|
||||||
|
- zapisuje je pod odpowiednim endpointem w `tokens.json`
|
||||||
|
|
||||||
|
Endpoint jest liczony z:
|
||||||
|
|
||||||
|
- scheme
|
||||||
|
- host
|
||||||
|
- port
|
||||||
|
|
||||||
|
Przy skanowaniu launcher zapisuje tez metadane serwera:
|
||||||
|
|
||||||
|
- `type`
|
||||||
|
- `scheme`
|
||||||
|
- `host`
|
||||||
|
- `port`
|
||||||
|
|
||||||
|
## Migracja starego pliku
|
||||||
|
|
||||||
|
Jesli istnieje stary plik:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/dev/workspace/rv/tokens/gitea_tokens.json
|
||||||
|
```
|
||||||
|
|
||||||
|
launcher przemigruje go do:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/dev/workspace/rv/tokens/tokens.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Jesli plik ma jeszcze stary format `users -> tokens`, launcher zamieni go na
|
||||||
|
nowy format `servers -> users -> tokens`.
|
||||||
|
|
||||||
|
## Komendy
|
||||||
|
|
||||||
|
### `tokens scan`
|
||||||
|
|
||||||
|
Kierunek:
|
||||||
|
|
||||||
|
```text
|
||||||
|
repo -> tokens.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Dzialanie:
|
||||||
|
|
||||||
|
- skanuje remote URL-e w repo
|
||||||
|
- zapisuje znalezione tokeny do `tokens.json`
|
||||||
|
- zapisuje je per endpoint serwera
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens scan
|
||||||
|
python3 workspace.py tokens scan --repo ~/dev/workspace/rv/series/inf/03
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tokens read`
|
||||||
|
|
||||||
|
Pokazuje szczegolowa zawartosc `tokens.json`.
|
||||||
|
|
||||||
|
Wynik zawiera:
|
||||||
|
|
||||||
|
- endpoint
|
||||||
|
- type
|
||||||
|
- scheme
|
||||||
|
- host
|
||||||
|
- port
|
||||||
|
- users
|
||||||
|
- tokens
|
||||||
|
|
||||||
|
Oraz liste userow i nazw tokenow dla kazdego endpointu.
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens read
|
||||||
|
python3 workspace.py tokens read --server http://77.90.8.171:3001
|
||||||
|
python3 workspace.py tokens read --show-secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tokens stats`
|
||||||
|
|
||||||
|
Pokazuje statystyki per endpoint serwera.
|
||||||
|
|
||||||
|
Wynik ma ten sam model co `tokens read`, ale w formie zbiorczej:
|
||||||
|
|
||||||
|
- endpoint
|
||||||
|
- type
|
||||||
|
- scheme
|
||||||
|
- host
|
||||||
|
- port
|
||||||
|
- users
|
||||||
|
- tokens
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tokens write`
|
||||||
|
|
||||||
|
Kierunek:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tokens.json -> repo
|
||||||
|
```
|
||||||
|
|
||||||
|
Dzialanie:
|
||||||
|
|
||||||
|
- bierze token z `tokens.json`
|
||||||
|
- wybiera endpoint, usera i token
|
||||||
|
- wpisuje dane auth do wybranego remota repo
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens write \
|
||||||
|
--repo ~/dev/workspace/rv/series/inf/03 \
|
||||||
|
--remote r1 \
|
||||||
|
--server http://77.90.8.171:3001 \
|
||||||
|
--user u1 \
|
||||||
|
--token-name t1
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tokens update`
|
||||||
|
|
||||||
|
Uruchamia synchronizacje w zadanym kierunku.
|
||||||
|
|
||||||
|
Dozwolone kierunki:
|
||||||
|
|
||||||
|
- `tokens update --from remotes`
|
||||||
|
- `tokens update --from store`
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens update --from remotes --repo ~/dev/workspace/rv/series/inf/03
|
||||||
|
python3 workspace.py tokens update --from store --repo ~/dev/workspace/rv/series/inf/03 --remote r1 --server http://77.90.8.171:3001 --user u1 --token-name t1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konflikty
|
||||||
|
|
||||||
|
Domyslnie launcher nie zgaduje przy konflikcie.
|
||||||
|
|
||||||
|
Jesli:
|
||||||
|
|
||||||
|
- token jest w repo, ale nie ma go w `tokens.json`
|
||||||
|
uzyj `tokens scan`
|
||||||
|
- token jest w `tokens.json`, ale nie ma go w repo
|
||||||
|
uzyj `tokens write`
|
||||||
|
- token jest i tu, i tu, ale wartosci sa rozne
|
||||||
|
wybierz kierunek jawnie przez `tokens update --from ...`
|
||||||
|
|
||||||
|
Przy `tokens write` i `tokens update --from store` mozna uzyc:
|
||||||
|
|
||||||
|
- `--replace`
|
||||||
|
- `--dry-run`
|
||||||
|
|
||||||
|
## Rekomendacja
|
||||||
|
|
||||||
|
Najbezpieczniejszy model pracy:
|
||||||
|
|
||||||
|
- `tokens scan` do zczytywania danych z remote'ow
|
||||||
|
- `tokens read` do podgladu store
|
||||||
|
- `tokens stats` do zbiorczego przegladu per endpoint
|
||||||
|
- `tokens write` do jawnego wpisania auth do remota
|
||||||
|
- `tokens update --from ...` tylko z jawnym kierunkiem
|
||||||
+166
@@ -1,5 +1,13 @@
|
|||||||
# RV Launcher CLI
|
# RV Launcher CLI
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
|
Mateusz Pabiszczak, 2026
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Draft
|
||||||
|
|
||||||
Plik opisuje przelaczniki i liste komend skryptu `workspace.py`.
|
Plik opisuje przelaczniki i liste komend skryptu `workspace.py`.
|
||||||
|
|
||||||
## Pobranie repo i przelaczenie galezi
|
## Pobranie repo i przelaczenie galezi
|
||||||
@@ -55,6 +63,13 @@ Minimalny format pliku:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"version": 2,
|
||||||
|
"servers": {
|
||||||
|
"http://77.90.8.171:3001": {
|
||||||
|
"type": "gitea",
|
||||||
|
"scheme": "http",
|
||||||
|
"host": "77.90.8.171",
|
||||||
|
"port": 3001,
|
||||||
"users": {
|
"users": {
|
||||||
"u1": {
|
"u1": {
|
||||||
"tokens": {
|
"tokens": {
|
||||||
@@ -63,6 +78,8 @@ Minimalny format pliku:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Ten plik powinien byc lokalny, niewersjonowany i miec prawa `600`.
|
Ten plik powinien byc lokalny, niewersjonowany i miec prawa `600`.
|
||||||
@@ -133,6 +150,11 @@ Komendy:
|
|||||||
- `show-config`
|
- `show-config`
|
||||||
- `list-series`
|
- `list-series`
|
||||||
- `list-cards [series]`
|
- `list-cards [series]`
|
||||||
|
- `tokens scan`
|
||||||
|
- `tokens read`
|
||||||
|
- `tokens stats`
|
||||||
|
- `tokens write`
|
||||||
|
- `tokens update`
|
||||||
- `submission [series] [card]`
|
- `submission [series] [card]`
|
||||||
- `tmux-container [series] [card]`
|
- `tmux-container [series] [card]`
|
||||||
|
|
||||||
@@ -211,6 +233,150 @@ python3 workspace.py list-cards
|
|||||||
python3 workspace.py list-cards inf
|
python3 workspace.py list-cards inf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `tokens scan`
|
||||||
|
|
||||||
|
Skanuje remote URL-e w repo i zapisuje znalezione dane do `tokens/tokens.json`.
|
||||||
|
|
||||||
|
Przelaczniki:
|
||||||
|
|
||||||
|
- `--repo PATH`
|
||||||
|
Sciezka wewnatrz docelowego repo. Domyslnie biezacy katalog.
|
||||||
|
|
||||||
|
Typowy wynik:
|
||||||
|
|
||||||
|
```text
|
||||||
|
repo_root<TAB>...
|
||||||
|
scanned<TAB>1
|
||||||
|
added<TAB>1
|
||||||
|
servers<TAB>1
|
||||||
|
token_path<TAB>...
|
||||||
|
```
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens scan
|
||||||
|
python3 workspace.py tokens scan --repo ~/dev/workspace/rv/series/inf/03
|
||||||
|
```
|
||||||
|
|
||||||
|
## `tokens read`
|
||||||
|
|
||||||
|
Pokazuje zawartosc `tokens/tokens.json` w podziale na endpointy serwerow.
|
||||||
|
|
||||||
|
Przelaczniki:
|
||||||
|
|
||||||
|
- `--server ENDPOINT`
|
||||||
|
Ogranicza wynik do jednego endpointu.
|
||||||
|
- `--show-secrets`
|
||||||
|
Pokazuje pelne wartosci tokenow zamiast maskowania.
|
||||||
|
|
||||||
|
Typowy wynik:
|
||||||
|
|
||||||
|
```text
|
||||||
|
token_path<TAB>...
|
||||||
|
endpoint<TAB>http://77.90.8.171:3001
|
||||||
|
type<TAB>gitea
|
||||||
|
scheme<TAB>http
|
||||||
|
host<TAB>77.90.8.171
|
||||||
|
port<TAB>3001
|
||||||
|
users<TAB>1
|
||||||
|
tokens<TAB>1
|
||||||
|
user<TAB>u1
|
||||||
|
token<TAB>t1<TAB>SE****23
|
||||||
|
```
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens read
|
||||||
|
python3 workspace.py tokens read --server http://77.90.8.171:3001
|
||||||
|
```
|
||||||
|
|
||||||
|
## `tokens stats`
|
||||||
|
|
||||||
|
Pokazuje statystyki tokenow per endpoint serwera.
|
||||||
|
|
||||||
|
Przelaczniki:
|
||||||
|
|
||||||
|
- `--server ENDPOINT`
|
||||||
|
Ogranicza wynik do jednego endpointu.
|
||||||
|
|
||||||
|
Typowy wynik:
|
||||||
|
|
||||||
|
```text
|
||||||
|
endpoint<TAB>type<TAB>scheme<TAB>host<TAB>port<TAB>users<TAB>tokens
|
||||||
|
http://77.90.8.171:3001<TAB>gitea<TAB>http<TAB>77.90.8.171<TAB>3001<TAB>1<TAB>1
|
||||||
|
```
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## `tokens write`
|
||||||
|
|
||||||
|
Wpisuje dane z `tokens/tokens.json` do wybranego remota repo.
|
||||||
|
|
||||||
|
Przelaczniki:
|
||||||
|
|
||||||
|
- `--repo PATH`
|
||||||
|
Sciezka wewnatrz docelowego repo. Domyslnie biezacy katalog.
|
||||||
|
- `--remote NAME`
|
||||||
|
Nazwa remota do aktualizacji lub utworzenia.
|
||||||
|
- `--url URL`
|
||||||
|
URL remota, jesli remote jeszcze nie istnieje.
|
||||||
|
- `--server ENDPOINT`
|
||||||
|
Endpoint serwera z `tokens.json`.
|
||||||
|
- `--user NAME`
|
||||||
|
Uzytkownik z wybranego endpointu.
|
||||||
|
- `--token-name NAME`
|
||||||
|
Nazwa tokena, na przyklad `t1`.
|
||||||
|
- `--replace`
|
||||||
|
Nadpisuje inne dane auth juz wpisane w remote URL.
|
||||||
|
- `--dry-run`
|
||||||
|
Pokazuje plan bez zapisu.
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens write --repo ~/dev/workspace/rv/series/inf/03 --remote r1 --server http://77.90.8.171:3001 --user u1 --token-name t1
|
||||||
|
```
|
||||||
|
|
||||||
|
## `tokens update`
|
||||||
|
|
||||||
|
Uruchamia synchronizacje w zadanym kierunku.
|
||||||
|
|
||||||
|
Przelaczniki:
|
||||||
|
|
||||||
|
- `--from remotes`
|
||||||
|
Skanuje remote URL-e i zapisuje wynik do `tokens.json`.
|
||||||
|
- `--from store`
|
||||||
|
Bierze dane z `tokens.json` i wpisuje je do remota repo.
|
||||||
|
- `--repo PATH`
|
||||||
|
Sciezka wewnatrz repo.
|
||||||
|
- `--remote NAME`
|
||||||
|
Wymagane dla `--from store`.
|
||||||
|
- `--url URL`
|
||||||
|
Opcjonalny URL dla `--from store`.
|
||||||
|
- `--server ENDPOINT`
|
||||||
|
Opcjonalny wybor endpointu dla `--from store`.
|
||||||
|
- `--user NAME`
|
||||||
|
Opcjonalny wybor usera dla `--from store`.
|
||||||
|
- `--token-name NAME`
|
||||||
|
Opcjonalny wybor tokena dla `--from store`.
|
||||||
|
- `--replace`
|
||||||
|
Nadpisuje inne auth przy `--from store`.
|
||||||
|
- `--dry-run`
|
||||||
|
Pokazuje plan bez zapisu przy `--from store`.
|
||||||
|
|
||||||
|
Przyklady:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 workspace.py tokens update --from remotes --repo ~/dev/workspace/rv/series/inf/03
|
||||||
|
python3 workspace.py tokens update --from store --repo ~/dev/workspace/rv/series/inf/03 --remote r1 --server http://77.90.8.171:3001 --user u1 --token-name t1
|
||||||
|
```
|
||||||
|
|
||||||
## `submission [series] [card]`
|
## `submission [series] [card]`
|
||||||
|
|
||||||
Wylicza flow oddawania rozwiazan:
|
Wylicza flow oddawania rozwiazan:
|
||||||
|
|||||||
+499
-26
@@ -11,7 +11,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import unquote, urlsplit
|
from urllib.parse import quote, unquote, urlsplit, urlunsplit
|
||||||
|
|
||||||
|
|
||||||
def expand_path(raw_path: str, base_dir: Path) -> Path:
|
def expand_path(raw_path: str, base_dir: Path) -> Path:
|
||||||
@@ -200,6 +200,62 @@ def git_repo_root(repo_path: Path) -> Path | None:
|
|||||||
return Path(repo_root)
|
return Path(repo_root)
|
||||||
|
|
||||||
|
|
||||||
|
def default_port_for_scheme(scheme: str) -> int | None:
|
||||||
|
if scheme == "http":
|
||||||
|
return 80
|
||||||
|
if scheme == "https":
|
||||||
|
return 443
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def endpoint_from_split(split_url) -> tuple[str, int | None]:
|
||||||
|
host_name = split_url.hostname
|
||||||
|
if not host_name:
|
||||||
|
raise SystemExit(f"Missing hostname in URL: {split_url.geturl()}")
|
||||||
|
|
||||||
|
port_value = split_url.port or default_port_for_scheme(split_url.scheme)
|
||||||
|
default_port = default_port_for_scheme(split_url.scheme)
|
||||||
|
if port_value is not None and port_value != default_port:
|
||||||
|
endpoint = f"{split_url.scheme}://{host_name}:{port_value}"
|
||||||
|
else:
|
||||||
|
endpoint = f"{split_url.scheme}://{host_name}"
|
||||||
|
return endpoint, port_value
|
||||||
|
|
||||||
|
|
||||||
|
def server_type_from_split(config: WorkspaceConfig, split_url) -> str:
|
||||||
|
endpoint, _ = endpoint_from_split(split_url)
|
||||||
|
base_split = urlsplit(config.git.base_url)
|
||||||
|
base_endpoint, _ = endpoint_from_split(base_split)
|
||||||
|
if endpoint == base_endpoint:
|
||||||
|
return "gitea"
|
||||||
|
|
||||||
|
host_name = (split_url.hostname or "").lower()
|
||||||
|
if host_name == "github.com" or host_name.endswith(".github.com"):
|
||||||
|
return "github"
|
||||||
|
if "gitlab" in host_name:
|
||||||
|
return "gitlab"
|
||||||
|
if "gitea" in host_name:
|
||||||
|
return "gitea"
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def server_info_from_url(config: WorkspaceConfig, remote_url: str) -> dict | None:
|
||||||
|
split_url = urlsplit(remote_url)
|
||||||
|
if split_url.scheme not in {"http", "https"}:
|
||||||
|
return None
|
||||||
|
if split_url.hostname is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
endpoint, port_value = endpoint_from_split(split_url)
|
||||||
|
return {
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"type": server_type_from_split(config, split_url),
|
||||||
|
"scheme": split_url.scheme,
|
||||||
|
"host": split_url.hostname,
|
||||||
|
"port": port_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def migrate_legacy_token_store(config: WorkspaceConfig) -> None:
|
def migrate_legacy_token_store(config: WorkspaceConfig) -> None:
|
||||||
if config.token_path.exists() or not config.legacy_token_path.exists():
|
if config.token_path.exists() or not config.legacy_token_path.exists():
|
||||||
return
|
return
|
||||||
@@ -210,17 +266,106 @@ def migrate_legacy_token_store(config: WorkspaceConfig) -> None:
|
|||||||
os.chmod(config.token_path, 0o600)
|
os.chmod(config.token_path, 0o600)
|
||||||
|
|
||||||
|
|
||||||
|
def empty_token_store() -> dict:
|
||||||
|
return {"version": 2, "servers": {}}
|
||||||
|
|
||||||
|
|
||||||
|
def next_token_name(tokens: dict[str, str]) -> str:
|
||||||
|
index = 1
|
||||||
|
while f"t{index}" in tokens:
|
||||||
|
index += 1
|
||||||
|
return f"t{index}"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_server_entry(token_data: dict, server_info: dict) -> dict:
|
||||||
|
servers = token_data.setdefault("servers", {})
|
||||||
|
endpoint = server_info["endpoint"]
|
||||||
|
server_entry = servers.setdefault(
|
||||||
|
endpoint,
|
||||||
|
{
|
||||||
|
"type": server_info["type"],
|
||||||
|
"scheme": server_info["scheme"],
|
||||||
|
"host": server_info["host"],
|
||||||
|
"port": server_info["port"],
|
||||||
|
"users": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
server_entry.setdefault("users", {})
|
||||||
|
for field_name in ["type", "scheme", "host", "port"]:
|
||||||
|
if field_name not in server_entry or server_entry[field_name] in {"", None, "unknown"}:
|
||||||
|
server_entry[field_name] = server_info[field_name]
|
||||||
|
return server_entry
|
||||||
|
|
||||||
|
|
||||||
|
def copy_legacy_users(token_data: dict, server_info: dict, raw_users: dict) -> dict:
|
||||||
|
server_entry = ensure_server_entry(token_data, server_info)
|
||||||
|
server_users = server_entry.setdefault("users", {})
|
||||||
|
|
||||||
|
for user_name, user_entry in raw_users.items():
|
||||||
|
if not isinstance(user_entry, dict):
|
||||||
|
continue
|
||||||
|
raw_tokens = user_entry.get("tokens", {})
|
||||||
|
if not isinstance(raw_tokens, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
target_user = server_users.setdefault(str(user_name), {"tokens": {}})
|
||||||
|
target_tokens = target_user.setdefault("tokens", {})
|
||||||
|
for token_name, token_value in raw_tokens.items():
|
||||||
|
if not isinstance(token_value, str) or not token_value:
|
||||||
|
continue
|
||||||
|
if token_value in target_tokens.values():
|
||||||
|
continue
|
||||||
|
|
||||||
|
normalized_name = str(token_name)
|
||||||
|
if normalized_name in target_tokens and target_tokens[normalized_name] != token_value:
|
||||||
|
normalized_name = next_token_name(target_tokens)
|
||||||
|
target_tokens[normalized_name] = token_value
|
||||||
|
|
||||||
|
return token_data
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_token_store(config: WorkspaceConfig, raw_data: dict) -> dict:
|
||||||
|
if "servers" in raw_data and isinstance(raw_data["servers"], dict):
|
||||||
|
token_data = empty_token_store()
|
||||||
|
token_data["version"] = raw_data.get("version", 2)
|
||||||
|
for endpoint, server_entry in raw_data["servers"].items():
|
||||||
|
if not isinstance(server_entry, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
base_server_info = {
|
||||||
|
"endpoint": str(endpoint),
|
||||||
|
"type": server_entry.get("type", "unknown"),
|
||||||
|
"scheme": server_entry.get("scheme", ""),
|
||||||
|
"host": server_entry.get("host", ""),
|
||||||
|
"port": server_entry.get("port"),
|
||||||
|
}
|
||||||
|
target_server = ensure_server_entry(token_data, base_server_info)
|
||||||
|
copy_legacy_users(token_data, base_server_info, server_entry.get("users", {}))
|
||||||
|
target_server["users"] = token_data["servers"][str(endpoint)]["users"]
|
||||||
|
return token_data
|
||||||
|
|
||||||
|
token_data = empty_token_store()
|
||||||
|
if "users" in raw_data and isinstance(raw_data["users"], dict):
|
||||||
|
base_server_info = server_info_from_url(config, config.git.base_url)
|
||||||
|
if base_server_info is None:
|
||||||
|
raise SystemExit(f"Invalid git base URL: {config.git.base_url}")
|
||||||
|
copy_legacy_users(token_data, base_server_info, raw_data["users"])
|
||||||
|
return token_data
|
||||||
|
|
||||||
|
|
||||||
def load_token_store(config: WorkspaceConfig) -> dict:
|
def load_token_store(config: WorkspaceConfig) -> dict:
|
||||||
migrate_legacy_token_store(config)
|
migrate_legacy_token_store(config)
|
||||||
|
|
||||||
if not config.token_path.exists():
|
if not config.token_path.exists():
|
||||||
return {"users": {}}
|
return empty_token_store()
|
||||||
|
|
||||||
raw_data = json.loads(config.token_path.read_text(encoding="utf-8"))
|
raw_data = json.loads(config.token_path.read_text(encoding="utf-8"))
|
||||||
if not isinstance(raw_data, dict):
|
if not isinstance(raw_data, dict):
|
||||||
raise SystemExit(f"Invalid token store format: {config.token_path}")
|
raise SystemExit(f"Invalid token store format: {config.token_path}")
|
||||||
raw_data.setdefault("users", {})
|
normalized_data = normalize_token_store(config, raw_data)
|
||||||
return raw_data
|
if raw_data != normalized_data:
|
||||||
|
write_token_store(config, normalized_data)
|
||||||
|
return normalized_data
|
||||||
|
|
||||||
|
|
||||||
def write_token_store(config: WorkspaceConfig, token_data: dict) -> None:
|
def write_token_store(config: WorkspaceConfig, token_data: dict) -> None:
|
||||||
@@ -234,16 +379,10 @@ def write_token_store(config: WorkspaceConfig, token_data: dict) -> None:
|
|||||||
os.chmod(config.token_path, 0o600)
|
os.chmod(config.token_path, 0o600)
|
||||||
|
|
||||||
|
|
||||||
def next_token_name(tokens: dict[str, str]) -> str:
|
def register_token(token_data: dict, server_info: dict, user_name: str, token_value: str) -> bool:
|
||||||
index = 1
|
server_entry = ensure_server_entry(token_data, server_info)
|
||||||
while f"t{index}" in tokens:
|
users = server_entry.setdefault("users", {})
|
||||||
index += 1
|
user_entry = users.setdefault(user_name, {"tokens": {}})
|
||||||
return f"t{index}"
|
|
||||||
|
|
||||||
|
|
||||||
def register_token(token_data: dict, user_name: str, token_value: str) -> bool:
|
|
||||||
users = token_data.setdefault("users", {})
|
|
||||||
user_entry = users.setdefault(user_name, {})
|
|
||||||
tokens = user_entry.setdefault("tokens", {})
|
tokens = user_entry.setdefault("tokens", {})
|
||||||
|
|
||||||
for existing_token in tokens.values():
|
for existing_token in tokens.values():
|
||||||
@@ -283,32 +422,284 @@ def remote_credentials(remote_url: str) -> tuple[str, str] | None:
|
|||||||
return unquote(split_url.username), unquote(split_url.password)
|
return unquote(split_url.username), unquote(split_url.password)
|
||||||
|
|
||||||
|
|
||||||
def sync_tokens_from_repo(config: WorkspaceConfig, repo_path: Path) -> bool:
|
def sync_tokens_from_repo(config: WorkspaceConfig, repo_path: Path) -> dict:
|
||||||
repo_root = git_repo_root(repo_path)
|
repo_root = git_repo_root(repo_path)
|
||||||
if repo_root is None:
|
if repo_root is None:
|
||||||
migrate_legacy_token_store(config)
|
migrate_legacy_token_store(config)
|
||||||
return False
|
return {
|
||||||
|
"repo_root": None,
|
||||||
|
"scanned": 0,
|
||||||
|
"added": 0,
|
||||||
|
"servers": 0,
|
||||||
|
"changed": False,
|
||||||
|
}
|
||||||
|
|
||||||
found_credentials: list[tuple[str, str]] = []
|
found_credentials: list[tuple[dict, str, str]] = []
|
||||||
for remote_url in remote_urls(repo_root):
|
for remote_url in remote_urls(repo_root):
|
||||||
|
server_info = server_info_from_url(config, remote_url)
|
||||||
credentials = remote_credentials(remote_url)
|
credentials = remote_credentials(remote_url)
|
||||||
if credentials is not None:
|
if server_info is not None and credentials is not None:
|
||||||
found_credentials.append(credentials)
|
user_name, token_value = credentials
|
||||||
|
found_credentials.append((server_info, user_name, token_value))
|
||||||
|
|
||||||
if not found_credentials:
|
if not found_credentials:
|
||||||
migrate_legacy_token_store(config)
|
migrate_legacy_token_store(config)
|
||||||
return False
|
return {
|
||||||
|
"repo_root": repo_root,
|
||||||
|
"scanned": 0,
|
||||||
|
"added": 0,
|
||||||
|
"servers": 0,
|
||||||
|
"changed": False,
|
||||||
|
}
|
||||||
|
|
||||||
token_data = load_token_store(config)
|
token_data = load_token_store(config)
|
||||||
changed = False
|
added = 0
|
||||||
for user_name, token_value in found_credentials:
|
touched_servers: set[str] = set()
|
||||||
if register_token(token_data, user_name, token_value):
|
for server_info, user_name, token_value in found_credentials:
|
||||||
changed = True
|
touched_servers.add(server_info["endpoint"])
|
||||||
|
if register_token(token_data, server_info, user_name, token_value):
|
||||||
|
added += 1
|
||||||
|
|
||||||
if changed or not config.token_path.exists():
|
if added or not config.token_path.exists():
|
||||||
write_token_store(config, token_data)
|
write_token_store(config, token_data)
|
||||||
|
|
||||||
return changed
|
return {
|
||||||
|
"repo_root": repo_root,
|
||||||
|
"scanned": len(found_credentials),
|
||||||
|
"added": added,
|
||||||
|
"servers": len(touched_servers),
|
||||||
|
"changed": added > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def count_server_tokens(server_entry: dict) -> int:
|
||||||
|
return sum(len(user_entry.get("tokens", {})) for user_entry in server_entry.get("users", {}).values())
|
||||||
|
|
||||||
|
|
||||||
|
def mask_secret(secret_value: str) -> str:
|
||||||
|
if len(secret_value) <= 4:
|
||||||
|
return "*" * len(secret_value)
|
||||||
|
return f"{secret_value[:2]}{'*' * (len(secret_value) - 4)}{secret_value[-2:]}"
|
||||||
|
|
||||||
|
|
||||||
|
def token_store_servers(token_data: dict) -> list[tuple[str, dict]]:
|
||||||
|
servers = token_data.get("servers", {})
|
||||||
|
return sorted(servers.items())
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_server_from_store(token_data: dict, endpoint: str | None) -> tuple[str, dict]:
|
||||||
|
servers = token_data.get("servers", {})
|
||||||
|
if endpoint:
|
||||||
|
server_entry = servers.get(endpoint)
|
||||||
|
if server_entry is None:
|
||||||
|
raise SystemExit(f"Missing server endpoint in token store: {endpoint}")
|
||||||
|
return endpoint, server_entry
|
||||||
|
|
||||||
|
if not servers:
|
||||||
|
raise SystemExit("Token store is empty.")
|
||||||
|
if len(servers) > 1:
|
||||||
|
available = ", ".join(sorted(servers))
|
||||||
|
raise SystemExit(f"Multiple servers in token store. Pass --server. Available: {available}")
|
||||||
|
|
||||||
|
endpoint_value = next(iter(sorted(servers)))
|
||||||
|
return endpoint_value, servers[endpoint_value]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_user_from_server(server_entry: dict, user_name: str | None) -> tuple[str, dict]:
|
||||||
|
users = server_entry.get("users", {})
|
||||||
|
if user_name:
|
||||||
|
user_entry = users.get(user_name)
|
||||||
|
if user_entry is None:
|
||||||
|
raise SystemExit(f"Missing user in token store: {user_name}")
|
||||||
|
return user_name, user_entry
|
||||||
|
|
||||||
|
if not users:
|
||||||
|
raise SystemExit("No users found in the selected server entry.")
|
||||||
|
if len(users) > 1:
|
||||||
|
available = ", ".join(sorted(users))
|
||||||
|
raise SystemExit(f"Multiple users in selected server entry. Pass --user. Available: {available}")
|
||||||
|
|
||||||
|
user_value = next(iter(sorted(users)))
|
||||||
|
return user_value, users[user_value]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_token_from_user(user_entry: dict, token_name: str | None) -> tuple[str, str]:
|
||||||
|
tokens = user_entry.get("tokens", {})
|
||||||
|
if token_name:
|
||||||
|
token_value = tokens.get(token_name)
|
||||||
|
if token_value is None:
|
||||||
|
raise SystemExit(f"Missing token name in token store: {token_name}")
|
||||||
|
return token_name, token_value
|
||||||
|
|
||||||
|
if not tokens:
|
||||||
|
raise SystemExit("No tokens found for the selected user.")
|
||||||
|
if len(tokens) > 1:
|
||||||
|
available = ", ".join(sorted(tokens))
|
||||||
|
raise SystemExit(f"Multiple tokens for the selected user. Pass --token-name. Available: {available}")
|
||||||
|
|
||||||
|
token_key = next(iter(sorted(tokens)))
|
||||||
|
return token_key, tokens[token_key]
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_remote_url(remote_url: str) -> tuple[str, bool]:
|
||||||
|
split_url = urlsplit(remote_url)
|
||||||
|
if split_url.scheme not in {"http", "https"} or split_url.hostname is None:
|
||||||
|
raise SystemExit(f"Only http/https remote URLs are supported: {remote_url}")
|
||||||
|
|
||||||
|
_, port_value = endpoint_from_split(split_url)
|
||||||
|
default_port = default_port_for_scheme(split_url.scheme)
|
||||||
|
netloc = split_url.hostname
|
||||||
|
if port_value is not None and port_value != default_port:
|
||||||
|
netloc = f"{netloc}:{port_value}"
|
||||||
|
|
||||||
|
had_credentials = split_url.username is not None or split_url.password is not None
|
||||||
|
sanitized_url = urlunsplit((split_url.scheme, netloc, split_url.path, split_url.query, split_url.fragment))
|
||||||
|
return sanitized_url, had_credentials
|
||||||
|
|
||||||
|
|
||||||
|
def credentialed_remote_url(remote_url: str, user_name: str, token_value: str) -> str:
|
||||||
|
sanitized_url, _ = sanitize_remote_url(remote_url)
|
||||||
|
split_url = urlsplit(sanitized_url)
|
||||||
|
_, port_value = endpoint_from_split(split_url)
|
||||||
|
default_port = default_port_for_scheme(split_url.scheme)
|
||||||
|
host_part = split_url.hostname or ""
|
||||||
|
if port_value is not None and port_value != default_port:
|
||||||
|
host_part = f"{host_part}:{port_value}"
|
||||||
|
|
||||||
|
netloc = f"{quote(user_name, safe='')}:{quote(token_value, safe='')}@{host_part}"
|
||||||
|
return urlunsplit((split_url.scheme, netloc, split_url.path, split_url.query, split_url.fragment))
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_repo_argument(repo_arg: str | None) -> Path:
|
||||||
|
return Path(repo_arg).expanduser().resolve() if repo_arg else Path.cwd().resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def run_tokens_scan(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
|
report = sync_tokens_from_repo(config, resolve_repo_argument(args.repo))
|
||||||
|
repo_root = report["repo_root"]
|
||||||
|
print(f"repo_root\t{repo_root or ''}")
|
||||||
|
print(f"scanned\t{report['scanned']}")
|
||||||
|
print(f"added\t{report['added']}")
|
||||||
|
print(f"servers\t{report['servers']}")
|
||||||
|
print(f"token_path\t{config.token_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_tokens_read(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
|
token_data = load_token_store(config)
|
||||||
|
servers = token_store_servers(token_data)
|
||||||
|
if args.server:
|
||||||
|
server_entry = token_data.get("servers", {}).get(args.server)
|
||||||
|
if server_entry is None:
|
||||||
|
raise SystemExit(f"Missing server endpoint in token store: {args.server}")
|
||||||
|
servers = [(args.server, server_entry)]
|
||||||
|
|
||||||
|
if not servers:
|
||||||
|
print(f"token_path\t{config.token_path}")
|
||||||
|
print("servers\t0")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"token_path\t{config.token_path}")
|
||||||
|
for index, (endpoint, server_entry) in enumerate(servers):
|
||||||
|
if index:
|
||||||
|
print()
|
||||||
|
print(f"endpoint\t{endpoint}")
|
||||||
|
print(f"type\t{server_entry.get('type', 'unknown')}")
|
||||||
|
print(f"scheme\t{server_entry.get('scheme', '')}")
|
||||||
|
print(f"host\t{server_entry.get('host', '')}")
|
||||||
|
print(f"port\t{server_entry.get('port', '')}")
|
||||||
|
print(f"users\t{len(server_entry.get('users', {}))}")
|
||||||
|
print(f"tokens\t{count_server_tokens(server_entry)}")
|
||||||
|
for user_name in sorted(server_entry.get("users", {})):
|
||||||
|
user_entry = server_entry["users"][user_name]
|
||||||
|
print(f"user\t{user_name}")
|
||||||
|
for token_name in sorted(user_entry.get("tokens", {})):
|
||||||
|
token_value = user_entry["tokens"][token_name]
|
||||||
|
secret_value = token_value if args.show_secrets else mask_secret(token_value)
|
||||||
|
print(f"token\t{token_name}\t{secret_value}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_tokens_stats(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
|
token_data = load_token_store(config)
|
||||||
|
servers = token_store_servers(token_data)
|
||||||
|
if args.server:
|
||||||
|
server_entry = token_data.get("servers", {}).get(args.server)
|
||||||
|
if server_entry is None:
|
||||||
|
raise SystemExit(f"Missing server endpoint in token store: {args.server}")
|
||||||
|
servers = [(args.server, server_entry)]
|
||||||
|
|
||||||
|
print("endpoint\ttype\tscheme\thost\tport\tusers\ttokens")
|
||||||
|
for endpoint, server_entry in servers:
|
||||||
|
print(
|
||||||
|
"\t".join(
|
||||||
|
[
|
||||||
|
endpoint,
|
||||||
|
str(server_entry.get("type", "unknown")),
|
||||||
|
str(server_entry.get("scheme", "")),
|
||||||
|
str(server_entry.get("host", "")),
|
||||||
|
str(server_entry.get("port", "")),
|
||||||
|
str(len(server_entry.get("users", {}))),
|
||||||
|
str(count_server_tokens(server_entry)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_tokens_write(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
|
repo_path = resolve_repo_argument(args.repo)
|
||||||
|
repo_root = git_repo_root(repo_path)
|
||||||
|
if repo_root is None:
|
||||||
|
raise SystemExit(f"Missing git repo at path: {repo_path}")
|
||||||
|
|
||||||
|
if not args.remote:
|
||||||
|
raise SystemExit("Pass --remote for tokens write.")
|
||||||
|
|
||||||
|
token_data = load_token_store(config)
|
||||||
|
existing_remote_url = git_capture(repo_root, ["remote", "get-url", args.remote])
|
||||||
|
target_url = args.url or existing_remote_url
|
||||||
|
if not target_url:
|
||||||
|
raise SystemExit("Missing target remote URL. Provide --url or create the remote first.")
|
||||||
|
|
||||||
|
target_server_info = server_info_from_url(config, target_url)
|
||||||
|
if target_server_info is None:
|
||||||
|
raise SystemExit(f"Only http/https remote URLs are supported: {target_url}")
|
||||||
|
|
||||||
|
server_endpoint, server_entry = resolve_server_from_store(token_data, args.server or target_server_info["endpoint"])
|
||||||
|
user_name, user_entry = resolve_user_from_server(server_entry, args.user)
|
||||||
|
token_name, token_value = resolve_token_from_user(user_entry, args.token_name)
|
||||||
|
|
||||||
|
final_url = credentialed_remote_url(target_url, user_name, token_value)
|
||||||
|
status = "added" if existing_remote_url is None else "updated"
|
||||||
|
existing_credentials = remote_credentials(existing_remote_url) if existing_remote_url else None
|
||||||
|
if existing_credentials == (user_name, token_value):
|
||||||
|
status = "unchanged"
|
||||||
|
elif existing_credentials is not None and not args.replace:
|
||||||
|
raise SystemExit(
|
||||||
|
f"Remote '{args.remote}' already has different credentials. "
|
||||||
|
"Pass --replace to overwrite them."
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"repo_root\t{repo_root}")
|
||||||
|
print(f"remote\t{args.remote}")
|
||||||
|
print(f"server\t{server_endpoint}")
|
||||||
|
print(f"user\t{user_name}")
|
||||||
|
print(f"token_name\t{token_name}")
|
||||||
|
print(f"status\t{status}")
|
||||||
|
print(f"url\t{sanitize_remote_url(final_url)[0]}")
|
||||||
|
|
||||||
|
if args.dry_run or status == "unchanged":
|
||||||
|
return
|
||||||
|
|
||||||
|
set_git_remote(repo_root, args.remote, final_url)
|
||||||
|
|
||||||
|
|
||||||
|
def run_tokens_update(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
|
if args.from_source == "remotes":
|
||||||
|
run_tokens_scan(config, args)
|
||||||
|
return
|
||||||
|
if args.from_source == "store":
|
||||||
|
run_tokens_write(config, args)
|
||||||
|
return
|
||||||
|
raise SystemExit(f"Unsupported tokens update source: {args.from_source}")
|
||||||
|
|
||||||
|
|
||||||
def git_current_branch(config: WorkspaceConfig, repo_path: Path) -> str:
|
def git_current_branch(config: WorkspaceConfig, repo_path: Path) -> str:
|
||||||
@@ -519,6 +910,19 @@ def print_config(config: WorkspaceConfig) -> None:
|
|||||||
print(f" {path}")
|
print(f" {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def add_repo_option(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.add_argument(
|
||||||
|
"--repo",
|
||||||
|
help="Path inside a target git repo. Default: current working directory.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_store_selection_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.add_argument("--server", help="Server endpoint from tokens.json.")
|
||||||
|
parser.add_argument("--user", help="User name inside the selected server entry.")
|
||||||
|
parser.add_argument("--token-name", help="Token name inside the selected user entry, for example 't1'.")
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(description="Workspace launcher for rv cards and tools.")
|
parser = argparse.ArgumentParser(description="Workspace launcher for rv cards and tools.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -575,6 +979,57 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
help="Write remotes into the selected card repo instead of only printing the plan.",
|
help="Write remotes into the selected card repo instead of only printing the plan.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tokens_parser = subparsers.add_parser("tokens", help="Manage local token store and repo remotes.")
|
||||||
|
tokens_subparsers = tokens_parser.add_subparsers(dest="tokens_command", required=True)
|
||||||
|
|
||||||
|
tokens_scan_parser = tokens_subparsers.add_parser(
|
||||||
|
"scan",
|
||||||
|
help="Scan remote URLs in a git repo and save discovered credentials into tokens.json.",
|
||||||
|
)
|
||||||
|
add_repo_option(tokens_scan_parser)
|
||||||
|
|
||||||
|
tokens_read_parser = tokens_subparsers.add_parser(
|
||||||
|
"read",
|
||||||
|
help="Read tokens.json and print stored servers, users and token names.",
|
||||||
|
)
|
||||||
|
tokens_read_parser.add_argument("--server", help="Filter output to one server endpoint.")
|
||||||
|
tokens_read_parser.add_argument("--show-secrets", action="store_true", help="Print full token values.")
|
||||||
|
|
||||||
|
tokens_stats_parser = tokens_subparsers.add_parser(
|
||||||
|
"stats",
|
||||||
|
help="Print per-server token statistics from tokens.json.",
|
||||||
|
)
|
||||||
|
tokens_stats_parser.add_argument("--server", help="Filter output to one server endpoint.")
|
||||||
|
|
||||||
|
tokens_write_parser = tokens_subparsers.add_parser(
|
||||||
|
"write",
|
||||||
|
help="Write credentials from tokens.json into a repo remote URL.",
|
||||||
|
)
|
||||||
|
add_repo_option(tokens_write_parser)
|
||||||
|
tokens_write_parser.add_argument("--remote", help="Remote name to update or create.")
|
||||||
|
tokens_write_parser.add_argument("--url", help="Target remote URL if the remote does not exist yet.")
|
||||||
|
add_store_selection_options(tokens_write_parser)
|
||||||
|
tokens_write_parser.add_argument("--replace", action="store_true", help="Replace different credentials in the remote URL.")
|
||||||
|
tokens_write_parser.add_argument("--dry-run", action="store_true", help="Print the planned update without writing it.")
|
||||||
|
|
||||||
|
tokens_update_parser = tokens_subparsers.add_parser(
|
||||||
|
"update",
|
||||||
|
help="Synchronize token data in a selected direction.",
|
||||||
|
)
|
||||||
|
tokens_update_parser.add_argument(
|
||||||
|
"--from",
|
||||||
|
dest="from_source",
|
||||||
|
required=True,
|
||||||
|
choices=["remotes", "store"],
|
||||||
|
help="Synchronization direction source: 'remotes' or 'store'.",
|
||||||
|
)
|
||||||
|
add_repo_option(tokens_update_parser)
|
||||||
|
tokens_update_parser.add_argument("--remote", help="Remote name used when --from store.")
|
||||||
|
tokens_update_parser.add_argument("--url", help="Target remote URL used when --from store.")
|
||||||
|
add_store_selection_options(tokens_update_parser)
|
||||||
|
tokens_update_parser.add_argument("--replace", action="store_true", help="Replace different credentials when writing to remotes.")
|
||||||
|
tokens_update_parser.add_argument("--dry-run", action="store_true", help="Print the planned update without writing it.")
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -599,6 +1054,24 @@ def main() -> int:
|
|||||||
if args.command == "submission":
|
if args.command == "submission":
|
||||||
print_submission_plan(config, args)
|
print_submission_plan(config, args)
|
||||||
return 0
|
return 0
|
||||||
|
if args.command == "tokens":
|
||||||
|
if args.tokens_command == "scan":
|
||||||
|
run_tokens_scan(config, args)
|
||||||
|
return 0
|
||||||
|
if args.tokens_command == "read":
|
||||||
|
run_tokens_read(config, args)
|
||||||
|
return 0
|
||||||
|
if args.tokens_command == "stats":
|
||||||
|
run_tokens_stats(config, args)
|
||||||
|
return 0
|
||||||
|
if args.tokens_command == "write":
|
||||||
|
run_tokens_write(config, args)
|
||||||
|
return 0
|
||||||
|
if args.tokens_command == "update":
|
||||||
|
run_tokens_update(config, args)
|
||||||
|
return 0
|
||||||
|
parser.error(f"Unsupported tokens command: {args.tokens_command}")
|
||||||
|
return 2
|
||||||
|
|
||||||
parser.error(f"Unsupported command: {args.command}")
|
parser.error(f"Unsupported command: {args.command}")
|
||||||
return 2
|
return 2
|
||||||
|
|||||||
Reference in New Issue
Block a user