neo-cmd 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- neo/__init__.py +2 -0
- neo/cli.py +76 -0
- neo/commands/__init__.py +1 -0
- neo/commands/_credentials.py +105 -0
- neo/commands/commits.py +95 -0
- neo/commands/config.py +58 -0
- neo/commands/git.py +134 -0
- neo/commands/repo.py +214 -0
- neo/commands/tasks.py +722 -0
- neo/commands/workspace.py +204 -0
- neo/config.py +380 -0
- neo/integrations/__init__.py +1 -0
- neo/integrations/bitbucket.py +217 -0
- neo/integrations/bitbucket_models.py +154 -0
- neo/integrations/github.py +232 -0
- neo/integrations/github_models.py +68 -0
- neo/integrations/resolver.py +150 -0
- neo/integrations/shortcut.py +210 -0
- neo/integrations/shortcut_models.py +210 -0
- neo/output.py +58 -0
- neo/py.typed +0 -0
- neo_cmd-0.1.0.dist-info/METADATA +119 -0
- neo_cmd-0.1.0.dist-info/RECORD +26 -0
- neo_cmd-0.1.0.dist-info/WHEEL +4 -0
- neo_cmd-0.1.0.dist-info/entry_points.txt +3 -0
- neo_cmd-0.1.0.dist-info/licenses/LICENSE +21 -0
neo/__init__.py
ADDED
neo/cli.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Entrypoint principal da CLI Neo."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich import print as rprint
|
|
9
|
+
|
|
10
|
+
from neo.commands.config import config_app
|
|
11
|
+
from neo.commands.git import git_app
|
|
12
|
+
from neo.commands.tasks import tasks_app
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AppContext:
|
|
19
|
+
json_mode: bool = False
|
|
20
|
+
verbose: bool = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
name="neo",
|
|
25
|
+
help="""[bold cyan]Neo — Personal Command Assistant[/bold cyan]
|
|
26
|
+
|
|
27
|
+
Compreenda seus sistemas. Seja dono das suas escolhas.
|
|
28
|
+
|
|
29
|
+
Centraliza operações de trabalho (Bitbucket, Shortcut) e vida pessoal,
|
|
30
|
+
investigando dados com autonomia e sem atrito.
|
|
31
|
+
""",
|
|
32
|
+
no_args_is_help=True,
|
|
33
|
+
rich_markup_mode="rich",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Registra subcomandos
|
|
37
|
+
app.add_typer(config_app, name="config")
|
|
38
|
+
app.add_typer(git_app, name="git")
|
|
39
|
+
app.add_typer(tasks_app, name="tasks")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def version_callback(value: bool) -> None:
|
|
43
|
+
if value:
|
|
44
|
+
rprint(f"[bold cyan]Neo[/bold cyan] v{__version__}")
|
|
45
|
+
raise typer.Exit()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.callback()
|
|
49
|
+
def main(
|
|
50
|
+
ctx: typer.Context,
|
|
51
|
+
version: bool | None = typer.Option(
|
|
52
|
+
None,
|
|
53
|
+
"--version",
|
|
54
|
+
"-V",
|
|
55
|
+
help="Exibe a versão do Neo e encerra.",
|
|
56
|
+
callback=version_callback,
|
|
57
|
+
is_eager=True,
|
|
58
|
+
),
|
|
59
|
+
json_mode: bool = typer.Option(
|
|
60
|
+
False,
|
|
61
|
+
"--json",
|
|
62
|
+
help="Emite a saída em formato JSON estruturado no stdout.",
|
|
63
|
+
),
|
|
64
|
+
verbose: bool = typer.Option(
|
|
65
|
+
False,
|
|
66
|
+
"--verbose",
|
|
67
|
+
"-v",
|
|
68
|
+
help="Habilita mensagens de debug e detalhes de rede.",
|
|
69
|
+
),
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Configura o contexto global da aplicação."""
|
|
72
|
+
ctx.obj = AppContext(json_mode=json_mode, verbose=verbose)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
app()
|
neo/commands/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Comandos da CLI do Neo."""
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Operações de credenciais compartilhadas por `neo git config auth` e `neo tasks config`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from keyring.errors import KeyringError
|
|
7
|
+
|
|
8
|
+
from neo.config import (
|
|
9
|
+
delete_credential,
|
|
10
|
+
get_credential,
|
|
11
|
+
get_credential_source,
|
|
12
|
+
mask_token,
|
|
13
|
+
set_credential,
|
|
14
|
+
)
|
|
15
|
+
from neo.output import print_error, print_json, print_panel, print_success
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_json_mode(ctx: typer.Context) -> bool:
|
|
19
|
+
return getattr(ctx.obj, "json_mode", False) if ctx.obj else False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def save_token(ctx: typer.Context, svc: str, token: str | None) -> None:
|
|
23
|
+
"""Grava o token de `svc` (prompt oculto se `token` for omitido)."""
|
|
24
|
+
as_json = is_json_mode(ctx)
|
|
25
|
+
|
|
26
|
+
if not token:
|
|
27
|
+
if as_json:
|
|
28
|
+
print_error(
|
|
29
|
+
"A flag --token é obrigatória quando o modo --json está ativo.",
|
|
30
|
+
as_json=True,
|
|
31
|
+
code=1,
|
|
32
|
+
)
|
|
33
|
+
raise typer.Exit(code=1)
|
|
34
|
+
token = typer.prompt(f"Digite o token para '{svc}'", hide_input=True)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
set_credential(svc, token)
|
|
38
|
+
source = get_credential_source(svc)
|
|
39
|
+
print_success(
|
|
40
|
+
f"Token para o serviço '{svc}' configurado com sucesso (armazenado via {source})!",
|
|
41
|
+
as_json=as_json,
|
|
42
|
+
json_payload={"status": "success", "service": svc, "source": source},
|
|
43
|
+
)
|
|
44
|
+
except (KeyringError, OSError, RuntimeError, ValueError) as e:
|
|
45
|
+
print_error(f"Falha ao salvar credencial: {e}", as_json=as_json, code=1)
|
|
46
|
+
raise typer.Exit(code=1) from e
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def show_token(ctx: typer.Context, svc: str) -> None:
|
|
50
|
+
"""Exibe o status e o token mascarado de `svc`."""
|
|
51
|
+
as_json = is_json_mode(ctx)
|
|
52
|
+
token = get_credential(svc)
|
|
53
|
+
source = get_credential_source(svc)
|
|
54
|
+
|
|
55
|
+
if not token:
|
|
56
|
+
print_error(
|
|
57
|
+
f"Serviço '{svc}' não possui credenciais configuradas.",
|
|
58
|
+
as_json=as_json,
|
|
59
|
+
json_payload={
|
|
60
|
+
"service": svc,
|
|
61
|
+
"configured": False,
|
|
62
|
+
"source": None,
|
|
63
|
+
"masked_token": None,
|
|
64
|
+
},
|
|
65
|
+
code=1,
|
|
66
|
+
)
|
|
67
|
+
raise typer.Exit(code=1)
|
|
68
|
+
|
|
69
|
+
masked = mask_token(token)
|
|
70
|
+
if as_json:
|
|
71
|
+
print_json(
|
|
72
|
+
{
|
|
73
|
+
"service": svc,
|
|
74
|
+
"configured": True,
|
|
75
|
+
"source": source,
|
|
76
|
+
"masked_token": masked,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
print_panel(
|
|
81
|
+
f"Autenticação: {svc}",
|
|
82
|
+
f"[bold]Status:[/bold] [green]Configurado[/green]\n"
|
|
83
|
+
f"[bold]Origem:[/bold] {source}\n"
|
|
84
|
+
f"[bold]Token:[/bold] {masked}",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def remove_token(ctx: typer.Context, svc: str) -> None:
|
|
89
|
+
"""Remove a credencial armazenada de `svc`."""
|
|
90
|
+
as_json = is_json_mode(ctx)
|
|
91
|
+
|
|
92
|
+
if delete_credential(svc):
|
|
93
|
+
print_success(
|
|
94
|
+
f"Credencial para o serviço '{svc}' removida com sucesso.",
|
|
95
|
+
as_json=as_json,
|
|
96
|
+
json_payload={"status": "success", "service": svc, "deleted": True},
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
print_error(
|
|
100
|
+
f"Nenhuma credencial encontrada para remover no serviço '{svc}'.",
|
|
101
|
+
as_json=as_json,
|
|
102
|
+
json_payload={"status": "error", "service": svc, "deleted": False},
|
|
103
|
+
code=1,
|
|
104
|
+
)
|
|
105
|
+
raise typer.Exit(code=1)
|
neo/commands/commits.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Comandos de consulta e histórico de commits Git (Bitbucket e GitHub) (`neo git commits`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typer import Option
|
|
7
|
+
|
|
8
|
+
from neo.integrations.bitbucket import BitbucketError
|
|
9
|
+
from neo.integrations.resolver import (
|
|
10
|
+
detect_git_remote_info,
|
|
11
|
+
resolve_git_provider,
|
|
12
|
+
)
|
|
13
|
+
from neo.output import print_error, print_json, print_panel, print_table
|
|
14
|
+
|
|
15
|
+
commits_app = typer.Typer(
|
|
16
|
+
name="commits",
|
|
17
|
+
help="Histórico e pesquisa de commits no Bitbucket e GitHub.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _infer_repo_slug_from_git() -> str | None:
|
|
23
|
+
"""Tenta inferir o slug do repositório a partir do remote origin do diretório local."""
|
|
24
|
+
return detect_git_remote_info()[1]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@commits_app.command("search")
|
|
28
|
+
def commits_search(
|
|
29
|
+
ctx: typer.Context,
|
|
30
|
+
repo: str | None = Option(
|
|
31
|
+
None, "--repo", "-r", help="Slug do repositório (inferido do git local se omitido)."
|
|
32
|
+
),
|
|
33
|
+
workspace: str | None = Option(
|
|
34
|
+
None, "--workspace", "-w", help="Workspace ou organização/usuário de destino."
|
|
35
|
+
),
|
|
36
|
+
provider: str | None = Option(
|
|
37
|
+
None, "--provider", "-p", help="Provedor Git: 'bitbucket' ou 'github'."
|
|
38
|
+
),
|
|
39
|
+
author: str | None = Option(None, "--author", "-a", help="Filtra por nome ou e-mail do autor."),
|
|
40
|
+
query: str | None = Option(
|
|
41
|
+
None, "--query", "-q", help="Filtra por termo na mensagem do commit."
|
|
42
|
+
),
|
|
43
|
+
limit: int = Option(20, "--limit", "-l", help="Quantidade máxima de commits a retornar."),
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Pesquisa commits em um repositório no Bitbucket ou GitHub."""
|
|
46
|
+
as_json = getattr(ctx.obj, "json_mode", False) if ctx.obj else False
|
|
47
|
+
|
|
48
|
+
target_repo = repo or _infer_repo_slug_from_git()
|
|
49
|
+
if not target_repo:
|
|
50
|
+
msg = (
|
|
51
|
+
"Repositório não informado e não foi possível inferir do git local. "
|
|
52
|
+
"Use a flag `--repo <slug>` para especificar o repositório."
|
|
53
|
+
)
|
|
54
|
+
print_error(msg, as_json=as_json, code=1)
|
|
55
|
+
raise typer.Exit(code=1)
|
|
56
|
+
|
|
57
|
+
prov_name, client = resolve_git_provider(provider, as_json=as_json)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
commits = client.list_commits(
|
|
61
|
+
repo_slug=target_repo,
|
|
62
|
+
workspace=workspace,
|
|
63
|
+
author=author,
|
|
64
|
+
query=query,
|
|
65
|
+
limit=limit,
|
|
66
|
+
)
|
|
67
|
+
except BitbucketError as e:
|
|
68
|
+
print_error(str(e), as_json=as_json, code=1)
|
|
69
|
+
raise typer.Exit(code=1) from e
|
|
70
|
+
|
|
71
|
+
if as_json:
|
|
72
|
+
print_json([c.model_dump() for c in commits])
|
|
73
|
+
else:
|
|
74
|
+
prov_title = "GitHub" if prov_name == "github" else "Bitbucket"
|
|
75
|
+
if not commits:
|
|
76
|
+
print_panel(
|
|
77
|
+
"Commits",
|
|
78
|
+
f"Nenhum commit encontrado para os filtros informados em '{target_repo}' ({prov_title}).",
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
columns = ["Hash", "Autor", "Data", "Mensagem"]
|
|
83
|
+
rows: list[list[str]] = []
|
|
84
|
+
for c in commits:
|
|
85
|
+
date_str = c.date.split("T")[0] if "T" in c.date else c.date
|
|
86
|
+
rows.append(
|
|
87
|
+
[
|
|
88
|
+
f"[bold yellow]{c.short_hash}[/bold yellow]",
|
|
89
|
+
c.author.author_name,
|
|
90
|
+
date_str,
|
|
91
|
+
c.summary,
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
print_table(f"Histórico de Commits ({target_repo} via {prov_title})", columns, rows)
|
neo/commands/config.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Comandos de visão geral das configurações do Neo (`neo config`).
|
|
2
|
+
|
|
3
|
+
A configuração de credenciais fica em `neo git config` (Bitbucket/GitHub) e
|
|
4
|
+
`neo tasks config` (Shortcut).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from neo.commands._credentials import is_json_mode
|
|
12
|
+
from neo.config import (
|
|
13
|
+
DEFAULT_CONFIG_FILE,
|
|
14
|
+
get_all_services_status,
|
|
15
|
+
get_default_provider_config,
|
|
16
|
+
)
|
|
17
|
+
from neo.output import print_json, print_panel, print_table
|
|
18
|
+
|
|
19
|
+
config_app = typer.Typer(
|
|
20
|
+
name="config",
|
|
21
|
+
help="Visão geral das configurações do Neo (credenciais: `neo git config` e `neo tasks config`).",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@config_app.command("show")
|
|
27
|
+
def config_show(ctx: typer.Context) -> None:
|
|
28
|
+
"""Exibe o status geral das configurações e credenciais do Neo."""
|
|
29
|
+
as_json = is_json_mode(ctx)
|
|
30
|
+
statuses = get_all_services_status()
|
|
31
|
+
default_provider = get_default_provider_config() or "bitbucket"
|
|
32
|
+
|
|
33
|
+
if as_json:
|
|
34
|
+
print_json(
|
|
35
|
+
{
|
|
36
|
+
"config_file": str(DEFAULT_CONFIG_FILE),
|
|
37
|
+
"default_provider": default_provider,
|
|
38
|
+
"services": statuses,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
columns = ["Serviço", "Status", "Origem", "Token Mascarado"]
|
|
43
|
+
rows: list[list[str]] = []
|
|
44
|
+
for svc, info in statuses.items():
|
|
45
|
+
status_str = (
|
|
46
|
+
"[bold green]Configurado[/bold green]"
|
|
47
|
+
if info["configured"]
|
|
48
|
+
else "[dim]Não configurado[/dim]"
|
|
49
|
+
)
|
|
50
|
+
source_str = str(info["source"]) if info["source"] else "-"
|
|
51
|
+
token_str = str(info["masked_token"]) if info["masked_token"] else "-"
|
|
52
|
+
rows.append([svc, status_str, source_str, token_str])
|
|
53
|
+
|
|
54
|
+
print_table("Status de Autenticação do Neo", columns, rows)
|
|
55
|
+
print_panel(
|
|
56
|
+
"Provedor Git Ativo",
|
|
57
|
+
f"[bold]Provedor padrão:[/bold] [cyan]{default_provider}[/cyan]",
|
|
58
|
+
)
|
neo/commands/git.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Guarda-chuva dos comandos Git (Bitbucket e GitHub) (`neo git`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typer import Argument, Option
|
|
7
|
+
|
|
8
|
+
from neo.commands._credentials import is_json_mode, remove_token, save_token, show_token
|
|
9
|
+
from neo.commands.commits import commits_app
|
|
10
|
+
from neo.commands.repo import repo_app
|
|
11
|
+
from neo.commands.workspace import workspace_app
|
|
12
|
+
from neo.config import (
|
|
13
|
+
SUPPORTED_GIT_PROVIDERS,
|
|
14
|
+
SUPPORTED_TASK_SERVICES,
|
|
15
|
+
GitProviderType,
|
|
16
|
+
get_default_provider_config,
|
|
17
|
+
set_default_provider_config,
|
|
18
|
+
validate_git_provider,
|
|
19
|
+
)
|
|
20
|
+
from neo.output import print_error, print_json, print_panel, print_success
|
|
21
|
+
|
|
22
|
+
git_app = typer.Typer(
|
|
23
|
+
name="git",
|
|
24
|
+
help="Comandos Git unificados para Bitbucket e GitHub (config, workspace, repo, commits).",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
git_config_app = typer.Typer(
|
|
29
|
+
name="config",
|
|
30
|
+
help="Credenciais e provedor Git padrão (Bitbucket, GitHub).",
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
auth_app = typer.Typer(
|
|
35
|
+
name="auth",
|
|
36
|
+
help="Gerencia tokens de autenticação dos provedores Git (Bitbucket, GitHub).",
|
|
37
|
+
no_args_is_help=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
git_config_app.add_typer(auth_app, name="auth")
|
|
41
|
+
git_app.add_typer(git_config_app, name="config")
|
|
42
|
+
git_app.add_typer(workspace_app, name="workspace")
|
|
43
|
+
git_app.add_typer(repo_app, name="repo")
|
|
44
|
+
git_app.add_typer(commits_app, name="commits")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _validate_git_service(ctx: typer.Context, service: str) -> GitProviderType:
|
|
48
|
+
"""Valida o serviço Git; `shortcut` é rejeitado com dica para `neo tasks config`."""
|
|
49
|
+
as_json = is_json_mode(ctx)
|
|
50
|
+
if service.strip().lower() in SUPPORTED_TASK_SERVICES:
|
|
51
|
+
print_error(
|
|
52
|
+
f"'{service}' é um serviço de tasks, não um provedor Git. "
|
|
53
|
+
f"Use `neo tasks config` para configurá-lo.",
|
|
54
|
+
as_json=as_json,
|
|
55
|
+
code=1,
|
|
56
|
+
)
|
|
57
|
+
raise typer.Exit(code=1)
|
|
58
|
+
try:
|
|
59
|
+
return validate_git_provider(service)
|
|
60
|
+
except ValueError as e:
|
|
61
|
+
print_error(str(e), as_json=as_json, code=1)
|
|
62
|
+
raise typer.Exit(code=1) from e
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@auth_app.command("set")
|
|
66
|
+
def auth_set(
|
|
67
|
+
ctx: typer.Context,
|
|
68
|
+
service: str = Argument(
|
|
69
|
+
..., help=f"Provedor para autenticar: {', '.join(SUPPORTED_GIT_PROVIDERS)}"
|
|
70
|
+
),
|
|
71
|
+
token: str = Option(
|
|
72
|
+
None,
|
|
73
|
+
"--token",
|
|
74
|
+
"-t",
|
|
75
|
+
help="Token de acesso da API. Se omitido, solicita via prompt seguro.",
|
|
76
|
+
),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Configura o token de autenticação de um provedor Git de forma segura."""
|
|
79
|
+
save_token(ctx, _validate_git_service(ctx, service), token)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@auth_app.command("get")
|
|
83
|
+
def auth_get(
|
|
84
|
+
ctx: typer.Context,
|
|
85
|
+
service: str = Argument(..., help=f"Provedor: {', '.join(SUPPORTED_GIT_PROVIDERS)}"),
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Exibe o status e o token mascarado de um provedor Git."""
|
|
88
|
+
show_token(ctx, _validate_git_service(ctx, service))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@auth_app.command("delete")
|
|
92
|
+
def auth_delete(
|
|
93
|
+
ctx: typer.Context,
|
|
94
|
+
service: str = Argument(..., help=f"Provedor: {', '.join(SUPPORTED_GIT_PROVIDERS)}"),
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Remove a credencial armazenada de um provedor Git."""
|
|
97
|
+
remove_token(ctx, _validate_git_service(ctx, service))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@git_config_app.command("set-provider")
|
|
101
|
+
def config_set_provider(
|
|
102
|
+
ctx: typer.Context,
|
|
103
|
+
provider: str = Argument(
|
|
104
|
+
...,
|
|
105
|
+
help=f"Provedor Git padrão: {', '.join(SUPPORTED_GIT_PROVIDERS)}",
|
|
106
|
+
),
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Define o provedor Git padrão ('bitbucket' ou 'github') em config.toml."""
|
|
109
|
+
as_json = is_json_mode(ctx)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
prov = validate_git_provider(provider)
|
|
113
|
+
except ValueError as e:
|
|
114
|
+
print_error(str(e), as_json=as_json, code=1)
|
|
115
|
+
raise typer.Exit(code=1) from e
|
|
116
|
+
|
|
117
|
+
set_default_provider_config(prov)
|
|
118
|
+
print_success(
|
|
119
|
+
f"Provedor Git padrão definido com sucesso: [cyan]{prov}[/cyan]",
|
|
120
|
+
as_json=as_json,
|
|
121
|
+
json_payload={"status": "success", "default_provider": prov},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@git_config_app.command("get-provider")
|
|
126
|
+
def config_get_provider(ctx: typer.Context) -> None:
|
|
127
|
+
"""Exibe o provedor Git padrão atualmente configurado."""
|
|
128
|
+
as_json = is_json_mode(ctx)
|
|
129
|
+
prov = get_default_provider_config() or "bitbucket (padrão implícito)"
|
|
130
|
+
|
|
131
|
+
if as_json:
|
|
132
|
+
print_json({"default_provider": get_default_provider_config()})
|
|
133
|
+
else:
|
|
134
|
+
print_panel("Provedor Git Padrão", f"[bold]Provedor:[/bold] [cyan]{prov}[/cyan]")
|
neo/commands/repo.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Comandos de gerenciamento de repositórios Git (Bitbucket e GitHub) (`neo git repo`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from typer import Argument, Option
|
|
9
|
+
|
|
10
|
+
from neo.integrations.bitbucket import (
|
|
11
|
+
BitbucketError,
|
|
12
|
+
BitbucketNotFoundError,
|
|
13
|
+
)
|
|
14
|
+
from neo.integrations.resolver import resolve_git_provider
|
|
15
|
+
from neo.output import print_error, print_json, print_panel, print_success, print_table
|
|
16
|
+
|
|
17
|
+
repo_app = typer.Typer(
|
|
18
|
+
name="repo",
|
|
19
|
+
help="Gerenciamento e busca de repositórios no Bitbucket e GitHub.",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@repo_app.command("list")
|
|
25
|
+
def repo_list(
|
|
26
|
+
ctx: typer.Context,
|
|
27
|
+
workspace: str | None = Option(
|
|
28
|
+
None, "--workspace", "-w", help="Workspace ou organização/usuário de destino."
|
|
29
|
+
),
|
|
30
|
+
provider: str | None = Option(
|
|
31
|
+
None, "--provider", "-p", help="Provedor Git: 'bitbucket' ou 'github'."
|
|
32
|
+
),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Lista todos os repositórios disponíveis no workspace ou organização."""
|
|
35
|
+
as_json = getattr(ctx.obj, "json_mode", False) if ctx.obj else False
|
|
36
|
+
prov_name, client = resolve_git_provider(provider, as_json=as_json)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
repos = client.list_repositories(workspace=workspace)
|
|
40
|
+
except BitbucketError as e:
|
|
41
|
+
print_error(str(e), as_json=as_json, code=1)
|
|
42
|
+
raise typer.Exit(code=1) from e
|
|
43
|
+
|
|
44
|
+
if as_json:
|
|
45
|
+
print_json([r.model_dump() for r in repos])
|
|
46
|
+
else:
|
|
47
|
+
if not repos:
|
|
48
|
+
print_panel("Repositórios", "Nenhum repositório encontrado no workspace informado.")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
columns = ["Nome", "Slug", "Visibilidade", "Branch Padrão", "Atualizado em"]
|
|
52
|
+
rows: list[list[str]] = []
|
|
53
|
+
for r in repos:
|
|
54
|
+
vis = "[red]Privado[/red]" if r.is_private else "[green]Público[/green]"
|
|
55
|
+
updated = r.updated_on.split("T")[0] if "T" in r.updated_on else r.updated_on
|
|
56
|
+
rows.append([r.name, r.slug, vis, r.main_branch, updated])
|
|
57
|
+
|
|
58
|
+
prov_title = "GitHub" if prov_name == "github" else "Bitbucket"
|
|
59
|
+
print_table(f"Repositórios no {prov_title}", columns, rows)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@repo_app.command("search")
|
|
63
|
+
def repo_search(
|
|
64
|
+
ctx: typer.Context,
|
|
65
|
+
query: str = Argument(..., help="Termo de pesquisa (nome, slug ou descrição)."),
|
|
66
|
+
workspace: str | None = Option(
|
|
67
|
+
None, "--workspace", "-w", help="Workspace ou organização/usuário de destino."
|
|
68
|
+
),
|
|
69
|
+
provider: str | None = Option(
|
|
70
|
+
None, "--provider", "-p", help="Provedor Git: 'bitbucket' ou 'github'."
|
|
71
|
+
),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Pesquisa repositórios por nome, slug ou descrição."""
|
|
74
|
+
as_json = getattr(ctx.obj, "json_mode", False) if ctx.obj else False
|
|
75
|
+
prov_name, client = resolve_git_provider(provider, as_json=as_json)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
repos = client.search_repositories(query=query, workspace=workspace)
|
|
79
|
+
except BitbucketError as e:
|
|
80
|
+
print_error(str(e), as_json=as_json, code=1)
|
|
81
|
+
raise typer.Exit(code=1) from e
|
|
82
|
+
|
|
83
|
+
if as_json:
|
|
84
|
+
print_json([r.model_dump() for r in repos])
|
|
85
|
+
else:
|
|
86
|
+
if not repos:
|
|
87
|
+
print_panel(
|
|
88
|
+
f"Busca: '{query}'", "Nenhum repositório encontrado para o termo pesquisado."
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
columns = ["Nome", "Slug", "Visibilidade", "Descrição"]
|
|
93
|
+
rows: list[list[str]] = []
|
|
94
|
+
for r in repos:
|
|
95
|
+
vis = "[red]Privado[/red]" if r.is_private else "[green]Público[/green]"
|
|
96
|
+
desc = r.description or "-"
|
|
97
|
+
rows.append([r.name, r.slug, vis, desc])
|
|
98
|
+
|
|
99
|
+
prov_title = "GitHub" if prov_name == "github" else "Bitbucket"
|
|
100
|
+
print_table(f"Repositórios no {prov_title} encontrados para '{query}'", columns, rows)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@repo_app.command("clone")
|
|
104
|
+
def repo_clone(
|
|
105
|
+
ctx: typer.Context,
|
|
106
|
+
slug: str = Argument(..., help="Slug ou nome do repositório a clonar."),
|
|
107
|
+
workspace: str | None = Option(
|
|
108
|
+
None, "--workspace", "-w", help="Workspace ou organização/usuário de destino."
|
|
109
|
+
),
|
|
110
|
+
provider: str | None = Option(
|
|
111
|
+
None, "--provider", "-p", help="Provedor Git: 'bitbucket' ou 'github'."
|
|
112
|
+
),
|
|
113
|
+
use_ssh: bool = Option(False, "--ssh", help="Força clonagem via protocolo SSH."),
|
|
114
|
+
use_https: bool = Option(False, "--https", help="Força clonagem via protocolo HTTPS."),
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Clona um repositório para o diretório atual."""
|
|
117
|
+
as_json = getattr(ctx.obj, "json_mode", False) if ctx.obj else False
|
|
118
|
+
prov_name, client = resolve_git_provider(provider, as_json=as_json)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
repo = client.get_repository(repo_slug=slug, workspace=workspace)
|
|
122
|
+
except BitbucketNotFoundError as e:
|
|
123
|
+
print_error(f"Repositório '{slug}' não encontrado: {e}", as_json=as_json, code=1)
|
|
124
|
+
raise typer.Exit(code=1) from e
|
|
125
|
+
except BitbucketError as e:
|
|
126
|
+
print_error(str(e), as_json=as_json, code=1)
|
|
127
|
+
raise typer.Exit(code=1) from e
|
|
128
|
+
|
|
129
|
+
# Resolução de URL
|
|
130
|
+
clone_url: str | None = None
|
|
131
|
+
if use_ssh:
|
|
132
|
+
clone_url = repo.ssh_clone_url
|
|
133
|
+
elif use_https:
|
|
134
|
+
clone_url = repo.https_clone_url
|
|
135
|
+
else:
|
|
136
|
+
clone_url = repo.ssh_clone_url or repo.https_clone_url
|
|
137
|
+
|
|
138
|
+
if not clone_url:
|
|
139
|
+
print_error(
|
|
140
|
+
f"Não foi possível obter uma URL de clone válida para o repositório '{slug}'.",
|
|
141
|
+
as_json=as_json,
|
|
142
|
+
code=1,
|
|
143
|
+
)
|
|
144
|
+
raise typer.Exit(code=1)
|
|
145
|
+
|
|
146
|
+
prov_title = "GitHub" if prov_name == "github" else "Bitbucket"
|
|
147
|
+
if as_json:
|
|
148
|
+
print_json(
|
|
149
|
+
{
|
|
150
|
+
"status": "cloning",
|
|
151
|
+
"provider": prov_name,
|
|
152
|
+
"repo": repo.slug,
|
|
153
|
+
"clone_url": clone_url,
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
print_success(
|
|
158
|
+
f"Iniciando clone de [bold]{repo.name}[/bold] ({prov_title}) via [cyan]{clone_url}[/cyan]..."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
proc = subprocess.run(["git", "clone", clone_url], check=False)
|
|
162
|
+
if proc.returncode != 0:
|
|
163
|
+
print_error(
|
|
164
|
+
f"Falha ao executar git clone (código {proc.returncode}).",
|
|
165
|
+
as_json=as_json,
|
|
166
|
+
code=proc.returncode,
|
|
167
|
+
)
|
|
168
|
+
raise typer.Exit(code=proc.returncode)
|
|
169
|
+
|
|
170
|
+
print_success(f"Repositório '{repo.slug}' clonado com sucesso!", as_json=as_json)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@repo_app.command("create")
|
|
174
|
+
def repo_create(
|
|
175
|
+
ctx: typer.Context,
|
|
176
|
+
name: str = Argument(..., help="Nome do repositório a ser criado."),
|
|
177
|
+
workspace: str | None = Option(
|
|
178
|
+
None, "--workspace", "-w", help="Workspace ou organização/usuário de destino."
|
|
179
|
+
),
|
|
180
|
+
provider: str | None = Option(
|
|
181
|
+
None, "--provider", "-p", help="Provedor Git: 'bitbucket' ou 'github'."
|
|
182
|
+
),
|
|
183
|
+
is_private: bool = Option(
|
|
184
|
+
True, "--private/--public", help="Define se o repositório é privado ou público."
|
|
185
|
+
),
|
|
186
|
+
description: str | None = Option(None, "--description", "-d", help="Descrição do repositório."),
|
|
187
|
+
yes: bool = Option(False, "--yes", "-y", help="Pula confirmação interativa."),
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Cria um novo repositório no provedor Git (Bitbucket ou GitHub)."""
|
|
190
|
+
as_json = getattr(ctx.obj, "json_mode", False) if ctx.obj else False
|
|
191
|
+
prov_name, client = resolve_git_provider(provider, as_json=as_json)
|
|
192
|
+
|
|
193
|
+
prov_title = "GitHub" if prov_name == "github" else "Bitbucket"
|
|
194
|
+
if not yes and not as_json:
|
|
195
|
+
confirmed = typer.confirm(f"Deseja criar o repositório '{name}' no {prov_title}?")
|
|
196
|
+
if not confirmed:
|
|
197
|
+
print_error("Operação cancelada pelo usuário.", as_json=as_json, code=1)
|
|
198
|
+
raise typer.Exit(code=1)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
repo = client.create_repository(
|
|
202
|
+
name=name,
|
|
203
|
+
workspace=workspace,
|
|
204
|
+
is_private=is_private,
|
|
205
|
+
description=description,
|
|
206
|
+
)
|
|
207
|
+
print_success(
|
|
208
|
+
f"Repositório '{repo.name}' criado com sucesso no {prov_title} ({repo.workspace})!",
|
|
209
|
+
as_json=as_json,
|
|
210
|
+
json_payload=repo.model_dump(),
|
|
211
|
+
)
|
|
212
|
+
except BitbucketError as e:
|
|
213
|
+
print_error(f"Falha ao criar repositório: {e}", as_json=as_json, code=1)
|
|
214
|
+
raise typer.Exit(code=1) from e
|