pep-agentes 1.2.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.
- pep/__init__.py +5 -0
- pep/cli.py +69 -0
- pep/core/__init__.py +1 -0
- pep/core/detection.py +47 -0
- pep/core/markers.py +66 -0
- pep/core/models.py +41 -0
- pep/core/paths.py +27 -0
- pep/core/version.py +5 -0
- pep/providers/__init__.py +24 -0
- pep/providers/base.py +23 -0
- pep/providers/claude.py +101 -0
- pep/providers/codex.py +131 -0
- pep/services/__init__.py +1 -0
- pep/services/manager.py +93 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/claude/CLAUDE.md +45 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/claude/commands/pep.md +29 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/codex/AGENTS.md +59 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/codex/prompts/pepcodex.md +15 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/codex/skills/pepcodex/SKILL.md +139 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/codex/skills/pepcodex/agents/openai.yaml +6 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/codex/skills/pepcodex/references/PROTOCOLO.md +141 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/codex/skills/pepcodex/tools/context_eco.py +340 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/docs/COMO_FOI_GERADO.md +34 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/docs/PEP-CODEX.md +67 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/docs/documentacao.pdf +74 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/prompts/pep-agentes-claude.txt +132 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/prompts/pep-agentes-completo.txt +207 -0
- pep_agentes-1.2.0.data/data/share/pep-agentes/prompts/pep-agentes-curto.txt +7 -0
- pep_agentes-1.2.0.dist-info/METADATA +192 -0
- pep_agentes-1.2.0.dist-info/RECORD +34 -0
- pep_agentes-1.2.0.dist-info/WHEEL +5 -0
- pep_agentes-1.2.0.dist-info/entry_points.txt +2 -0
- pep_agentes-1.2.0.dist-info/licenses/LICENSE +201 -0
- pep_agentes-1.2.0.dist-info/top_level.txt +1 -0
pep/__init__.py
ADDED
pep/cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Console entrypoint for the PEP-Agentes Manager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from pep.core.version import __version__
|
|
8
|
+
from pep.services.manager import (
|
|
9
|
+
collect_doctor,
|
|
10
|
+
collect_status,
|
|
11
|
+
format_results,
|
|
12
|
+
format_status,
|
|
13
|
+
resolve_scopes,
|
|
14
|
+
run_operation,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
PROVIDERS = ("claude", "codex", "all")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def add_scope_args(parser: argparse.ArgumentParser) -> None:
|
|
21
|
+
parser.add_argument("--here", action="store_true", help="usar o diretorio atual")
|
|
22
|
+
parser.add_argument("--path", action="append", metavar="CAMINHO", help="usar um projeto especifico")
|
|
23
|
+
parser.add_argument("--global", dest="global_scope", action="store_true", help="usar instalacao global")
|
|
24
|
+
parser.add_argument("--legacy-prompt", action="store_true", help="incluir /prompts:pepcodex para Codex")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
28
|
+
parser = argparse.ArgumentParser(description="PEP-Agentes Manager")
|
|
29
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
30
|
+
for command in ("install", "update", "uninstall", "repair", "status"):
|
|
31
|
+
p = sub.add_parser(command)
|
|
32
|
+
p.add_argument("provider", nargs="?", default="all", choices=PROVIDERS)
|
|
33
|
+
add_scope_args(p)
|
|
34
|
+
if command in {"install", "update"}:
|
|
35
|
+
p.add_argument("--force", action="store_true", help="sobrescrever arquivos gerenciados")
|
|
36
|
+
p = sub.add_parser("doctor")
|
|
37
|
+
p.add_argument("provider", nargs="?", default="all", choices=PROVIDERS)
|
|
38
|
+
add_scope_args(p)
|
|
39
|
+
sub.add_parser("version")
|
|
40
|
+
return parser
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: list[str] | None = None) -> int:
|
|
44
|
+
args = build_parser().parse_args(argv)
|
|
45
|
+
if args.command == "version":
|
|
46
|
+
print(__version__)
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
scopes = resolve_scopes(args.here, args.path, args.global_scope)
|
|
50
|
+
if not scopes:
|
|
51
|
+
print("Nenhum alvo selecionado. Use --here, --path CAMINHO ou --global.")
|
|
52
|
+
return 2
|
|
53
|
+
|
|
54
|
+
if args.command == "status":
|
|
55
|
+
print(format_status(collect_status(args.provider, scopes, args.legacy_prompt)))
|
|
56
|
+
return 0
|
|
57
|
+
if args.command == "doctor":
|
|
58
|
+
print(collect_doctor(args.provider, scopes, args.legacy_prompt))
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
results = run_operation(
|
|
62
|
+
args.command,
|
|
63
|
+
args.provider,
|
|
64
|
+
scopes,
|
|
65
|
+
force=getattr(args, "force", False),
|
|
66
|
+
legacy_prompt=args.legacy_prompt,
|
|
67
|
+
)
|
|
68
|
+
print(format_results(results))
|
|
69
|
+
return 0
|
pep/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core utilities for PEP-Agentes Manager."""
|
pep/core/detection.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Local-only tool detection used by doctor/status."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ToolStatus:
|
|
14
|
+
name: str
|
|
15
|
+
found: bool
|
|
16
|
+
detail: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def detect_command(name: str, command: str) -> ToolStatus:
|
|
20
|
+
path = shutil.which(command)
|
|
21
|
+
return ToolStatus(name, bool(path), path or "nao encontrado no PATH")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def detect_python() -> ToolStatus:
|
|
25
|
+
return ToolStatus("Python", True, sys.version.split()[0])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def detect_system() -> ToolStatus:
|
|
29
|
+
return ToolStatus("Sistema", True, f"{platform.system()} {platform.release()}".strip())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def detect_genpyexe() -> ToolStatus:
|
|
33
|
+
if importlib.util.find_spec("genpyexeks"):
|
|
34
|
+
return ToolStatus("GenPyEXE", True, "modulo genpyexeks disponivel")
|
|
35
|
+
path = shutil.which("genpyexe")
|
|
36
|
+
return ToolStatus("GenPyEXE", bool(path), path or "instale com: pip install genpyexe")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def doctor_tools() -> list[ToolStatus]:
|
|
40
|
+
return [
|
|
41
|
+
detect_system(),
|
|
42
|
+
detect_python(),
|
|
43
|
+
detect_command("Git", "git"),
|
|
44
|
+
detect_command("Claude Code", "claude"),
|
|
45
|
+
detect_command("Codex", "codex"),
|
|
46
|
+
detect_genpyexe(),
|
|
47
|
+
]
|
pep/core/markers.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Marker-managed Markdown block operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MarkerError(RuntimeError):
|
|
10
|
+
"""Raised when a managed marker block is malformed."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _has_broken_markers(text: str, start: str, end: str, pattern: re.Pattern[str]) -> bool:
|
|
14
|
+
starts = text.count(start)
|
|
15
|
+
ends = text.count(end)
|
|
16
|
+
if starts != ends:
|
|
17
|
+
return True
|
|
18
|
+
return starts > 0 and not pattern.search(text)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def read_managed_block(source: Path, pattern: re.Pattern[str]) -> str:
|
|
22
|
+
text = source.read_text(encoding="utf-8")
|
|
23
|
+
match = pattern.search(text)
|
|
24
|
+
if not match:
|
|
25
|
+
raise MarkerError(f"Marcadores PEP nao encontrados em {source}")
|
|
26
|
+
return match.group(0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def upsert_block(path: Path, block: str, pattern: re.Pattern[str], start: str, end: str, header: str) -> str:
|
|
30
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
if path.exists():
|
|
32
|
+
current = path.read_text(encoding="utf-8")
|
|
33
|
+
if _has_broken_markers(current, start, end, pattern):
|
|
34
|
+
raise MarkerError(f"Markers corrompidos em {path}")
|
|
35
|
+
if pattern.search(current):
|
|
36
|
+
path.write_text(pattern.sub(lambda _: block, current), encoding="utf-8")
|
|
37
|
+
return "atualizado"
|
|
38
|
+
sep = "" if current.endswith("\n") else "\n"
|
|
39
|
+
path.write_text(f"{current}{sep}\n{block}\n", encoding="utf-8")
|
|
40
|
+
return "bloco adicionado"
|
|
41
|
+
path.write_text(f"{header}{block}\n", encoding="utf-8")
|
|
42
|
+
return "criado"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def remove_block(path: Path, pattern: re.Pattern[str], start: str, end: str) -> str:
|
|
46
|
+
if not path.exists():
|
|
47
|
+
return "inexistente"
|
|
48
|
+
current = path.read_text(encoding="utf-8")
|
|
49
|
+
if _has_broken_markers(current, start, end, pattern):
|
|
50
|
+
raise MarkerError(f"Markers corrompidos em {path}")
|
|
51
|
+
if not pattern.search(current):
|
|
52
|
+
return "sem bloco"
|
|
53
|
+
path.write_text(pattern.sub("", current).rstrip() + "\n", encoding="utf-8")
|
|
54
|
+
return "bloco removido"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def block_state(path: Path, expected: str, pattern: re.Pattern[str], start: str, end: str) -> str:
|
|
58
|
+
if not path.exists():
|
|
59
|
+
return "missing"
|
|
60
|
+
current = path.read_text(encoding="utf-8")
|
|
61
|
+
if _has_broken_markers(current, start, end, pattern):
|
|
62
|
+
return "corrupt"
|
|
63
|
+
match = pattern.search(current)
|
|
64
|
+
if not match:
|
|
65
|
+
return "missing"
|
|
66
|
+
return "current" if match.group(0) == expected else "outdated"
|
pep/core/models.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Small data models used by providers and services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InstallState(str, Enum):
|
|
11
|
+
NOT_INSTALLED = "not_installed"
|
|
12
|
+
INSTALLED = "installed"
|
|
13
|
+
CURRENT = "current"
|
|
14
|
+
OUTDATED = "outdated"
|
|
15
|
+
PARTIAL = "partial"
|
|
16
|
+
CORRUPT = "corrupt"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Scope:
|
|
21
|
+
path: Path
|
|
22
|
+
is_global: bool = False
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def label(self) -> str:
|
|
26
|
+
return "GLOBAL" if self.is_global else str(self.path)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Status:
|
|
31
|
+
provider: str
|
|
32
|
+
scope: Scope
|
|
33
|
+
state: InstallState
|
|
34
|
+
detail: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class OperationResult:
|
|
39
|
+
provider: str
|
|
40
|
+
scope: Scope
|
|
41
|
+
messages: tuple[str, ...]
|
pep/core/paths.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Path helpers shared by CLI, GUI and providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def package_root() -> Path:
|
|
10
|
+
"""Return the project/resource root, supporting PyInstaller onefile."""
|
|
11
|
+
if getattr(sys, "frozen", False):
|
|
12
|
+
return Path(getattr(sys, "_MEIPASS", Path(sys.executable).resolve().parent))
|
|
13
|
+
source_root = Path(__file__).resolve().parents[2]
|
|
14
|
+
if (source_root / "claude").exists() and (source_root / "codex").exists():
|
|
15
|
+
return source_root
|
|
16
|
+
installed_root = Path(sys.prefix) / "share" / "pep-agentes"
|
|
17
|
+
if installed_root.exists():
|
|
18
|
+
return installed_root
|
|
19
|
+
return source_root
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def user_home() -> Path:
|
|
23
|
+
return Path.home()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def prompt_path(relative: str) -> Path:
|
|
27
|
+
return package_root() / relative
|
pep/core/version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Provider registry for PEP-Agentes Manager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pep.providers.claude import ClaudeProvider
|
|
6
|
+
from pep.providers.codex import CodexProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PROVIDERS = {
|
|
10
|
+
"claude": ClaudeProvider(),
|
|
11
|
+
"codex": CodexProvider(),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_provider(name: str):
|
|
16
|
+
if name not in PROVIDERS:
|
|
17
|
+
raise ValueError(f"Provider desconhecido: {name}")
|
|
18
|
+
return PROVIDERS[name]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def selected_providers(name: str):
|
|
22
|
+
if name == "all":
|
|
23
|
+
return list(PROVIDERS.values())
|
|
24
|
+
return [get_provider(name)]
|
pep/providers/base.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Provider protocol shared by Claude and Codex adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from pep.core.models import OperationResult, Scope, Status
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Provider(Protocol):
|
|
12
|
+
name: str
|
|
13
|
+
display_name: str
|
|
14
|
+
|
|
15
|
+
def prompt_path(self) -> Path | None: ...
|
|
16
|
+
|
|
17
|
+
def install(self, scope: Scope, force: bool = False, legacy_prompt: bool = False) -> OperationResult: ...
|
|
18
|
+
|
|
19
|
+
def uninstall(self, scope: Scope, legacy_prompt: bool = False) -> OperationResult: ...
|
|
20
|
+
|
|
21
|
+
def status(self, scope: Scope, legacy_prompt: bool = False) -> Status: ...
|
|
22
|
+
|
|
23
|
+
def repair(self, scope: Scope, legacy_prompt: bool = False) -> OperationResult: ...
|
pep/providers/claude.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Claude Code provider adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pep.core import paths
|
|
10
|
+
from pep.core.markers import block_state, read_managed_block, remove_block, upsert_block
|
|
11
|
+
from pep.core.models import InstallState, OperationResult, Scope, Status
|
|
12
|
+
|
|
13
|
+
START = "<!-- PEP-AGENTES:START"
|
|
14
|
+
END = "<!-- PEP-AGENTES:END -->"
|
|
15
|
+
BLOCK_RE = re.compile(r"<!-- PEP-AGENTES:START.*?<!-- PEP-AGENTES:END -->", re.DOTALL)
|
|
16
|
+
HEADER = (
|
|
17
|
+
"# Instrucoes do projeto (Claude Code)\n\n"
|
|
18
|
+
"Bloco PEP-Agentes gerenciado por scripts/install_claude.py (nao edite entre os marcadores).\n\n"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClaudeProvider:
|
|
23
|
+
name = "claude"
|
|
24
|
+
display_name = "Claude Code"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def source_claude_md(self) -> Path:
|
|
28
|
+
return paths.package_root() / "claude" / "CLAUDE.md"
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def source_command(self) -> Path:
|
|
32
|
+
return paths.package_root() / "claude" / "commands" / "pep.md"
|
|
33
|
+
|
|
34
|
+
def prompt_path(self) -> Path:
|
|
35
|
+
return paths.package_root() / "prompts" / "pep-agentes-claude.txt"
|
|
36
|
+
|
|
37
|
+
def block(self) -> str:
|
|
38
|
+
return read_managed_block(self.source_claude_md, BLOCK_RE)
|
|
39
|
+
|
|
40
|
+
def command_text(self) -> str:
|
|
41
|
+
return self.source_command.read_text(encoding="utf-8")
|
|
42
|
+
|
|
43
|
+
def targets_for(self, scope: Scope) -> tuple[Path, Path]:
|
|
44
|
+
if scope.is_global:
|
|
45
|
+
base = scope.path
|
|
46
|
+
return base / "CLAUDE.md", base / "commands" / "pep.md"
|
|
47
|
+
return scope.path / "CLAUDE.md", scope.path / ".claude" / "commands" / "pep.md"
|
|
48
|
+
|
|
49
|
+
def install(self, scope: Scope, force: bool = False, legacy_prompt: bool = False) -> OperationResult:
|
|
50
|
+
del legacy_prompt
|
|
51
|
+
claude_md, command_path = self.targets_for(scope)
|
|
52
|
+
block = self.block()
|
|
53
|
+
command_text = self.command_text()
|
|
54
|
+
messages = [f"CLAUDE.md: {upsert_block(claude_md, block, BLOCK_RE, START, END, HEADER)} -> {claude_md}"]
|
|
55
|
+
command_path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
if command_path.exists() and not force:
|
|
57
|
+
if command_path.read_text(encoding="utf-8") == command_text:
|
|
58
|
+
messages.append(f"/pep: ja atualizado -> {command_path}")
|
|
59
|
+
else:
|
|
60
|
+
messages.append(f"/pep: JA EXISTE (use --force para sobrescrever) -> {command_path}")
|
|
61
|
+
else:
|
|
62
|
+
command_path.write_text(command_text, encoding="utf-8")
|
|
63
|
+
messages.append(f"/pep: instalado -> {command_path}")
|
|
64
|
+
return OperationResult(self.name, scope, tuple(messages))
|
|
65
|
+
|
|
66
|
+
def uninstall(self, scope: Scope, legacy_prompt: bool = False) -> OperationResult:
|
|
67
|
+
del legacy_prompt
|
|
68
|
+
claude_md, command_path = self.targets_for(scope)
|
|
69
|
+
messages = [f"CLAUDE.md: {remove_block(claude_md, BLOCK_RE, START, END)} -> {claude_md}"]
|
|
70
|
+
if command_path.exists():
|
|
71
|
+
command_path.unlink()
|
|
72
|
+
messages.append(f"/pep: removido -> {command_path}")
|
|
73
|
+
else:
|
|
74
|
+
messages.append(f"/pep: inexistente -> {command_path}")
|
|
75
|
+
return OperationResult(self.name, scope, tuple(messages))
|
|
76
|
+
|
|
77
|
+
def status(self, scope: Scope, legacy_prompt: bool = False) -> Status:
|
|
78
|
+
del legacy_prompt
|
|
79
|
+
claude_md, command_path = self.targets_for(scope)
|
|
80
|
+
block = self.block()
|
|
81
|
+
md_state = block_state(claude_md, block, BLOCK_RE, START, END)
|
|
82
|
+
command_ok = command_path.exists() and command_path.read_text(encoding="utf-8") == self.command_text()
|
|
83
|
+
if md_state == "corrupt":
|
|
84
|
+
state = InstallState.CORRUPT
|
|
85
|
+
elif md_state == "missing" and not command_path.exists():
|
|
86
|
+
state = InstallState.NOT_INSTALLED
|
|
87
|
+
elif md_state == "current" and command_ok:
|
|
88
|
+
state = InstallState.CURRENT
|
|
89
|
+
elif md_state == "outdated" or (command_path.exists() and not command_ok):
|
|
90
|
+
state = InstallState.OUTDATED
|
|
91
|
+
else:
|
|
92
|
+
state = InstallState.PARTIAL
|
|
93
|
+
return Status(self.name, scope, state, f"CLAUDE.md={md_state}; command={'ok' if command_ok else 'missing/outdated'}")
|
|
94
|
+
|
|
95
|
+
def repair(self, scope: Scope, legacy_prompt: bool = False) -> OperationResult:
|
|
96
|
+
status = self.status(scope, legacy_prompt)
|
|
97
|
+
if status.state == InstallState.CURRENT:
|
|
98
|
+
return OperationResult(self.name, scope, ("Claude Code ja esta atualizado.",))
|
|
99
|
+
if status.state == InstallState.CORRUPT:
|
|
100
|
+
return OperationResult(self.name, scope, ("Markers corrompidos; reparo automatico cancelado para preservar conteudo manual.",))
|
|
101
|
+
return self.install(scope, force=True)
|
pep/providers/codex.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Codex provider adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import filecmp
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pep.core import paths
|
|
11
|
+
from pep.core.markers import block_state, read_managed_block, remove_block, upsert_block
|
|
12
|
+
from pep.core.models import InstallState, OperationResult, Scope, Status
|
|
13
|
+
|
|
14
|
+
START = "<!-- PEP-CODEX:START"
|
|
15
|
+
END = "<!-- PEP-CODEX:END -->"
|
|
16
|
+
BLOCK_RE = re.compile(r"<!-- PEP-CODEX:START.*?<!-- PEP-CODEX:END -->", re.DOTALL)
|
|
17
|
+
HEADER = (
|
|
18
|
+
"# Instrucoes do projeto (Codex)\n\n"
|
|
19
|
+
"Bloco PEP-Codex gerenciado por scripts/install_codex.py.\n\n"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CodexProvider:
|
|
24
|
+
name = "codex"
|
|
25
|
+
display_name = "Codex"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def source_agents(self) -> Path:
|
|
29
|
+
return paths.package_root() / "codex" / "AGENTS.md"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def source_skill(self) -> Path:
|
|
33
|
+
return paths.package_root() / "codex" / "skills" / "pepcodex"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def source_prompt(self) -> Path:
|
|
37
|
+
return paths.package_root() / "codex" / "prompts" / "pepcodex.md"
|
|
38
|
+
|
|
39
|
+
def prompt_path(self) -> Path:
|
|
40
|
+
return self.source_prompt
|
|
41
|
+
|
|
42
|
+
def block(self) -> str:
|
|
43
|
+
return read_managed_block(self.source_agents, BLOCK_RE)
|
|
44
|
+
|
|
45
|
+
def targets_for(self, scope: Scope) -> tuple[Path, Path]:
|
|
46
|
+
if scope.is_global:
|
|
47
|
+
return scope.path / "AGENTS.md", paths.user_home() / ".agents" / "skills" / "pepcodex"
|
|
48
|
+
return scope.path / "AGENTS.md", scope.path / ".agents" / "skills" / "pepcodex"
|
|
49
|
+
|
|
50
|
+
def legacy_prompt_target(self) -> Path:
|
|
51
|
+
return paths.user_home() / ".codex" / "prompts" / "pepcodex.md"
|
|
52
|
+
|
|
53
|
+
def _copy_skill(self, dest: Path, force: bool) -> str:
|
|
54
|
+
if dest.exists():
|
|
55
|
+
if not force:
|
|
56
|
+
return "ja existe (use --force para atualizar)"
|
|
57
|
+
shutil.rmtree(dest)
|
|
58
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
shutil.copytree(self.source_skill, dest)
|
|
60
|
+
return "instalada"
|
|
61
|
+
|
|
62
|
+
def _skill_matches(self, dest: Path) -> bool:
|
|
63
|
+
if not dest.exists():
|
|
64
|
+
return False
|
|
65
|
+
src_files = sorted(p.relative_to(self.source_skill) for p in self.source_skill.rglob("*") if p.is_file())
|
|
66
|
+
dst_files = sorted(p.relative_to(dest) for p in dest.rglob("*") if p.is_file())
|
|
67
|
+
if src_files != dst_files:
|
|
68
|
+
return False
|
|
69
|
+
return all(filecmp.cmp(self.source_skill / rel, dest / rel, shallow=False) for rel in src_files)
|
|
70
|
+
|
|
71
|
+
def install(self, scope: Scope, force: bool = False, legacy_prompt: bool = False) -> OperationResult:
|
|
72
|
+
agents, skill = self.targets_for(scope)
|
|
73
|
+
messages = [f"AGENTS.md: {upsert_block(agents, self.block(), BLOCK_RE, START, END, HEADER)} -> {agents}"]
|
|
74
|
+
messages.append(f"$pepcodex: {self._copy_skill(skill, force)} -> {skill}")
|
|
75
|
+
if legacy_prompt:
|
|
76
|
+
dest = self.legacy_prompt_target()
|
|
77
|
+
if dest.exists() and not force:
|
|
78
|
+
messages.append(f"/prompts:pepcodex: ja existe (use --force para atualizar) -> {dest}")
|
|
79
|
+
else:
|
|
80
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
shutil.copy2(self.source_prompt, dest)
|
|
82
|
+
messages.append(f"/prompts:pepcodex: instalado -> {dest}")
|
|
83
|
+
return OperationResult(self.name, scope, tuple(messages))
|
|
84
|
+
|
|
85
|
+
def uninstall(self, scope: Scope, legacy_prompt: bool = False) -> OperationResult:
|
|
86
|
+
agents, skill = self.targets_for(scope)
|
|
87
|
+
messages = [f"AGENTS.md: {remove_block(agents, BLOCK_RE, START, END)} -> {agents}"]
|
|
88
|
+
if skill.exists():
|
|
89
|
+
shutil.rmtree(skill)
|
|
90
|
+
messages.append(f"$pepcodex: removida -> {skill}")
|
|
91
|
+
else:
|
|
92
|
+
messages.append(f"$pepcodex: inexistente -> {skill}")
|
|
93
|
+
if legacy_prompt:
|
|
94
|
+
dest = self.legacy_prompt_target()
|
|
95
|
+
if dest.exists():
|
|
96
|
+
dest.unlink()
|
|
97
|
+
messages.append(f"/prompts:pepcodex: removido -> {dest}")
|
|
98
|
+
else:
|
|
99
|
+
messages.append(f"/prompts:pepcodex: inexistente -> {dest}")
|
|
100
|
+
return OperationResult(self.name, scope, tuple(messages))
|
|
101
|
+
|
|
102
|
+
def status(self, scope: Scope, legacy_prompt: bool = False) -> Status:
|
|
103
|
+
agents, skill = self.targets_for(scope)
|
|
104
|
+
md_state = block_state(agents, self.block(), BLOCK_RE, START, END)
|
|
105
|
+
skill_ok = self._skill_matches(skill)
|
|
106
|
+
legacy_ok = True
|
|
107
|
+
if legacy_prompt:
|
|
108
|
+
dest = self.legacy_prompt_target()
|
|
109
|
+
legacy_ok = dest.exists() and filecmp.cmp(self.source_prompt, dest, shallow=False)
|
|
110
|
+
if md_state == "corrupt":
|
|
111
|
+
state = InstallState.CORRUPT
|
|
112
|
+
elif md_state == "missing" and not skill.exists():
|
|
113
|
+
state = InstallState.NOT_INSTALLED
|
|
114
|
+
elif md_state == "current" and skill_ok and legacy_ok:
|
|
115
|
+
state = InstallState.CURRENT
|
|
116
|
+
elif md_state == "outdated" or (skill.exists() and not skill_ok) or not legacy_ok:
|
|
117
|
+
state = InstallState.OUTDATED
|
|
118
|
+
else:
|
|
119
|
+
state = InstallState.PARTIAL
|
|
120
|
+
detail = f"AGENTS.md={md_state}; skill={'ok' if skill_ok else 'missing/outdated'}"
|
|
121
|
+
if legacy_prompt:
|
|
122
|
+
detail += f"; legacy={'ok' if legacy_ok else 'missing/outdated'}"
|
|
123
|
+
return Status(self.name, scope, state, detail)
|
|
124
|
+
|
|
125
|
+
def repair(self, scope: Scope, legacy_prompt: bool = False) -> OperationResult:
|
|
126
|
+
status = self.status(scope, legacy_prompt)
|
|
127
|
+
if status.state == InstallState.CURRENT:
|
|
128
|
+
return OperationResult(self.name, scope, ("Codex ja esta atualizado.",))
|
|
129
|
+
if status.state == InstallState.CORRUPT:
|
|
130
|
+
return OperationResult(self.name, scope, ("Markers corrompidos; reparo automatico cancelado para preservar conteudo manual.",))
|
|
131
|
+
return self.install(scope, force=True, legacy_prompt=legacy_prompt)
|
pep/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Service layer for PEP-Agentes Manager."""
|
pep/services/manager.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""High-level operations shared by CLI and GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pep.core.detection import doctor_tools
|
|
8
|
+
from pep.core.models import OperationResult, Scope, Status
|
|
9
|
+
from pep.providers import selected_providers
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_scopes(here: bool = False, paths: list[str] | None = None, global_scope: bool = False) -> list[Scope]:
|
|
13
|
+
scopes: list[Scope] = []
|
|
14
|
+
if here:
|
|
15
|
+
scopes.append(Scope(Path.cwd(), False))
|
|
16
|
+
for raw in paths or []:
|
|
17
|
+
scopes.append(Scope(Path(raw).expanduser().resolve(), False))
|
|
18
|
+
if global_scope:
|
|
19
|
+
from pep.core.paths import user_home
|
|
20
|
+
|
|
21
|
+
scopes.append(Scope(user_home() / ".codex", True))
|
|
22
|
+
return scopes
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_provider_scope(provider_name: str, scope: Scope) -> Scope:
|
|
26
|
+
if not scope.is_global:
|
|
27
|
+
return scope
|
|
28
|
+
from pep.core.paths import user_home
|
|
29
|
+
|
|
30
|
+
if provider_name == "claude":
|
|
31
|
+
return Scope(user_home() / ".claude", True)
|
|
32
|
+
if provider_name == "codex":
|
|
33
|
+
return Scope(user_home() / ".codex", True)
|
|
34
|
+
return scope
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_operation(
|
|
38
|
+
action: str,
|
|
39
|
+
provider_name: str,
|
|
40
|
+
scopes: list[Scope],
|
|
41
|
+
force: bool = False,
|
|
42
|
+
legacy_prompt: bool = False,
|
|
43
|
+
) -> list[OperationResult]:
|
|
44
|
+
results: list[OperationResult] = []
|
|
45
|
+
for provider in selected_providers(provider_name):
|
|
46
|
+
for scope in scopes:
|
|
47
|
+
provider_scope = resolve_provider_scope(provider.name, scope)
|
|
48
|
+
if action in {"install", "update"}:
|
|
49
|
+
results.append(provider.install(provider_scope, force=force or action == "update", legacy_prompt=legacy_prompt))
|
|
50
|
+
elif action == "uninstall":
|
|
51
|
+
results.append(provider.uninstall(provider_scope, legacy_prompt=legacy_prompt))
|
|
52
|
+
elif action == "repair":
|
|
53
|
+
results.append(provider.repair(provider_scope, legacy_prompt=legacy_prompt))
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError(f"Acao desconhecida: {action}")
|
|
56
|
+
return results
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def collect_status(provider_name: str, scopes: list[Scope], legacy_prompt: bool = False) -> list[Status]:
|
|
60
|
+
rows: list[Status] = []
|
|
61
|
+
for provider in selected_providers(provider_name):
|
|
62
|
+
for scope in scopes:
|
|
63
|
+
rows.append(provider.status(resolve_provider_scope(provider.name, scope), legacy_prompt=legacy_prompt))
|
|
64
|
+
return rows
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def collect_doctor(provider_name: str = "all", scopes: list[Scope] | None = None, legacy_prompt: bool = False) -> str:
|
|
68
|
+
lines = ["PEP Doctor", ""]
|
|
69
|
+
for tool in doctor_tools():
|
|
70
|
+
status = "OK" if tool.found else "ERRO"
|
|
71
|
+
lines.append(f"{tool.name:.<22} {status} - {tool.detail}")
|
|
72
|
+
if scopes:
|
|
73
|
+
lines.append("")
|
|
74
|
+
for row in collect_status(provider_name, scopes, legacy_prompt=legacy_prompt):
|
|
75
|
+
lines.append(f"{row.provider} {row.scope.label}: {row.state.value} - {row.detail}")
|
|
76
|
+
return "\n".join(lines)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def format_results(results: list[OperationResult]) -> str:
|
|
80
|
+
chunks: list[str] = []
|
|
81
|
+
for result in results:
|
|
82
|
+
chunks.append(f"== {result.provider} | {result.scope.label} ==")
|
|
83
|
+
chunks.extend(result.messages)
|
|
84
|
+
chunks.append("")
|
|
85
|
+
chunks.append("Concluido.")
|
|
86
|
+
return "\n".join(chunks)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def format_status(rows: list[Status]) -> str:
|
|
90
|
+
lines = []
|
|
91
|
+
for row in rows:
|
|
92
|
+
lines.append(f"{row.provider} | {row.scope.label}: {row.state.value} - {row.detail}")
|
|
93
|
+
return "\n".join(lines) if lines else "Nenhum alvo selecionado."
|