lazyplan 0.1.0__tar.gz
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.
- lazyplan-0.1.0/PKG-INFO +8 -0
- lazyplan-0.1.0/README.md +197 -0
- lazyplan-0.1.0/lazyplan/__init__.py +0 -0
- lazyplan-0.1.0/lazyplan/app.py +102 -0
- lazyplan-0.1.0/lazyplan/cli.py +53 -0
- lazyplan-0.1.0/lazyplan/github.py +126 -0
- lazyplan-0.1.0/lazyplan/lazyplan.tcss +332 -0
- lazyplan-0.1.0/lazyplan/models.py +44 -0
- lazyplan-0.1.0/lazyplan/screens/__init__.py +0 -0
- lazyplan-0.1.0/lazyplan/screens/base.py +20 -0
- lazyplan-0.1.0/lazyplan/screens/confirm_delete.py +53 -0
- lazyplan-0.1.0/lazyplan/screens/detail.py +123 -0
- lazyplan-0.1.0/lazyplan/screens/editor.py +160 -0
- lazyplan-0.1.0/lazyplan/screens/github_screen.py +132 -0
- lazyplan-0.1.0/lazyplan/screens/main.py +203 -0
- lazyplan-0.1.0/lazyplan/storage.py +59 -0
- lazyplan-0.1.0/pyproject.toml +20 -0
lazyplan-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lazyplan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TUI para capturar y gestionar ideas de proyectos
|
|
5
|
+
Author-email: Marcelo Mastroiani <mastroianimarcelo04@gmail.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: textual>=0.47.0
|
|
8
|
+
Requires-Dist: typer>=0.9.0
|
lazyplan-0.1.0/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# 💤 LazyPlan
|
|
2
|
+
|
|
3
|
+
TUI (interfaz de terminal) para capturar y gestionar ideas de proyectos, sin salir de la consola. Anotá una idea en 5 segundos, hacé seguimiento de su estado, y creá el repo en GitHub cuando estés listo para arrancar — todo desde el teclado.
|
|
4
|
+
|
|
5
|
+
<!--
|
|
6
|
+

|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ✨ Características
|
|
12
|
+
|
|
13
|
+
- **Captura rápida de ideas** desde la TUI o directo desde la terminal (`lazyplan new`).
|
|
14
|
+
- **Estados de proyecto**: 🟡 Cruda · 🟢 Activa · ⚪ Pausada · 🔴 Descartada.
|
|
15
|
+
- **Búsqueda y filtros** en tiempo real por título, descripción o stack.
|
|
16
|
+
- **Vista de detalle** con toda la info del proyecto.
|
|
17
|
+
- **Editor integrado** para crear y modificar proyectos sin salir de la app.
|
|
18
|
+
- **Integración con GitHub CLI (`gh`)**: creá un repositorio (público o privado) directamente desde la TUI, sin abrir el navegador.
|
|
19
|
+
- **Persistencia local** en JSON, sin bases de datos ni servicios externos.
|
|
20
|
+
- Tema oscuro propio (`onyx-violet`) 🎨
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 📦 Requisitos
|
|
25
|
+
|
|
26
|
+
- Python **3.11** o superior
|
|
27
|
+
- [GitHub CLI (`gh`)](https://cli.github.com/) instalado y autenticado — **solo si** querés usar la creación de repos desde la app (`ctrl+g`). El resto de LazyPlan funciona sin `gh`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🚀 Instalación
|
|
32
|
+
|
|
33
|
+
### Con pipx (recomendado)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pipx install lazyplan
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Con pip
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install lazyplan
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Desde el código fuente
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/tu-usuario/lazyplan.git
|
|
49
|
+
cd lazyplan
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🕹️ Uso
|
|
56
|
+
|
|
57
|
+
### Abrir la TUI
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
lazyplan
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Crear un proyecto sin abrir la TUI
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
lazyplan new -t "Mi idea genial" -s "python,textual" -d "Una app de terminal" --status cruda
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| Opción | Descripción |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `-t`, `--title` | Título del proyecto **(obligatorio)** |
|
|
72
|
+
| `-s`, `--stack` | Tecnologías, separadas por coma |
|
|
73
|
+
| `-d`, `--desc` | Descripción breve |
|
|
74
|
+
| `--status` | Estado inicial: `cruda`, `activa`, `pausada`, `descartada` (default: `cruda`) |
|
|
75
|
+
|
|
76
|
+
### Listar proyectos desde la terminal
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
lazyplan ls
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## ⌨️ Atajos de teclado (dentro de la TUI)
|
|
85
|
+
|
|
86
|
+
**Pantalla principal**
|
|
87
|
+
|
|
88
|
+
| Tecla | Acción |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `n` | Nuevo proyecto |
|
|
91
|
+
| `enter` | Ver detalle |
|
|
92
|
+
| `e` | Editar proyecto |
|
|
93
|
+
| `d` | Eliminar proyecto |
|
|
94
|
+
| `j` / `k` | Mover cursor abajo / arriba |
|
|
95
|
+
| `ctrl+f` | Buscar |
|
|
96
|
+
| `esc` | Limpiar búsqueda |
|
|
97
|
+
| `q` | Salir |
|
|
98
|
+
|
|
99
|
+
**Detalle del proyecto**
|
|
100
|
+
|
|
101
|
+
| Tecla | Acción |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `e` | Editar |
|
|
104
|
+
| `d` | Eliminar |
|
|
105
|
+
| `ctrl+g` | Crear repositorio en GitHub |
|
|
106
|
+
| `esc` | Volver |
|
|
107
|
+
|
|
108
|
+
**Editor**
|
|
109
|
+
|
|
110
|
+
| Tecla | Acción |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `ctrl+s` | Guardar |
|
|
113
|
+
| `esc` | Cancelar |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 💾 Dónde se guardan los datos
|
|
118
|
+
|
|
119
|
+
LazyPlan guarda todo en un archivo JSON local, sin necesidad de conexión a internet ni servicios externos:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
~/.local/share/lazyplan/projects.json
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 🏗️ Estructura del proyecto
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
lazyplan/
|
|
131
|
+
├── lazyplan/
|
|
132
|
+
│ ├── app.py # App principal de Textual
|
|
133
|
+
│ ├── cli.py # Entry point y comandos (typer)
|
|
134
|
+
│ ├── models.py # Modelo Project / Status
|
|
135
|
+
│ ├── storage.py # Persistencia en JSON
|
|
136
|
+
│ ├── github.py # Integración con GitHub CLI
|
|
137
|
+
│ ├── lazyplan.tcss # Estilos (Textual CSS)
|
|
138
|
+
│ └── screens/
|
|
139
|
+
│ ├── main.py # Listado principal
|
|
140
|
+
│ ├── detail.py # Vista de detalle
|
|
141
|
+
│ ├── editor.py # Crear/editar proyecto
|
|
142
|
+
│ ├── confirm_delete.py # Confirmación de borrado
|
|
143
|
+
│ └── github_screen.py # Modal de creación de repo
|
|
144
|
+
└── pyproject.toml
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 🛠️ Desarrollo
|
|
150
|
+
|
|
151
|
+
Cloná el repo e instalá las dependencias de desarrollo (incluye herramientas de build y empaquetado):
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
git clone https://github.com/tu-usuario/lazyplan.git
|
|
155
|
+
cd lazyplan
|
|
156
|
+
pip install -e ".[dev]"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Correr la app en modo desarrollo:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
python -m lazyplan.cli
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Generar el paquete
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
python -m build
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Publicar en PyPI
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
twine upload dist/*
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 🤝 Contribuir
|
|
180
|
+
|
|
181
|
+
Las contribuciones son bienvenidas. Si encontrás un bug o tenés una idea:
|
|
182
|
+
|
|
183
|
+
1. Abrí un issue describiendo el problema o la propuesta.
|
|
184
|
+
2. Forkeá el repo y creá una rama (`git checkout -b feature/mi-mejora`).
|
|
185
|
+
3. Hacé tus cambios y abrí un Pull Request.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 📄 Licencia
|
|
190
|
+
|
|
191
|
+
Este proyecto no tiene licencia definida todavía. Si pensás distribuirlo o aceptar contribuciones externas, considerá agregar una (por ejemplo, [MIT](https://choosealicense.com/licenses/mit/)).
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## ✍️ Autor
|
|
196
|
+
|
|
197
|
+
**Marcelo Mastroiani** — mastroianimarcelo04@gmail.com
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
|
|
2
|
+
from textual.theme import Theme
|
|
3
|
+
from textual.app import App
|
|
4
|
+
|
|
5
|
+
from lazyplan.models import Project
|
|
6
|
+
from lazyplan.storage import load_projects, save_projects, upsert_project, delete_project
|
|
7
|
+
from lazyplan.screens.main import MainScreen
|
|
8
|
+
from lazyplan.screens.detail import DetailScreen
|
|
9
|
+
from lazyplan.screens.editor import EditorScreen
|
|
10
|
+
from lazyplan.screens.confirm_delete import ConfirmDeleteScreen
|
|
11
|
+
from lazyplan.screens.github_screen import GithubScreen
|
|
12
|
+
|
|
13
|
+
arctic_theme = Theme(
|
|
14
|
+
name="onyx-violet",
|
|
15
|
+
primary="#A78BFA",
|
|
16
|
+
secondary="#7C3AED",
|
|
17
|
+
accent="#C4B5FD",
|
|
18
|
+
background="#0C0C10",
|
|
19
|
+
surface="#16151D",
|
|
20
|
+
panel="#100F17",
|
|
21
|
+
foreground="#EDEDF0",
|
|
22
|
+
boost="#0A0912",
|
|
23
|
+
success="#4ADE80",
|
|
24
|
+
warning="#FCD34D",
|
|
25
|
+
error="#F87171",
|
|
26
|
+
dark=True,
|
|
27
|
+
variables={
|
|
28
|
+
"block-cursor-text-style": "none",
|
|
29
|
+
"footer-key-foreground": "#A78BFA",
|
|
30
|
+
"input-selection-background": "#A78BFA 35%",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LazyPlanApp(App):
|
|
36
|
+
"""TUI para capturar y gestionar ideas de proyectos."""
|
|
37
|
+
|
|
38
|
+
CSS_PATH = "lazyplan.tcss"
|
|
39
|
+
TITLE = "LazyPlan"
|
|
40
|
+
DARK = True
|
|
41
|
+
|
|
42
|
+
# SCREENS = {
|
|
43
|
+
# "main": MainScreen,
|
|
44
|
+
# "detail": DetailScreen,
|
|
45
|
+
# "editor": EditorScreen,
|
|
46
|
+
# "confirm_delete": ConfirmDeleteScreen,
|
|
47
|
+
# "github": GithubScreen,
|
|
48
|
+
# }
|
|
49
|
+
|
|
50
|
+
def __init__(self):
|
|
51
|
+
super().__init__()
|
|
52
|
+
self._projects = load_projects()
|
|
53
|
+
|
|
54
|
+
def on_mount(self) -> None:
|
|
55
|
+
self.register_theme(arctic_theme)
|
|
56
|
+
self.theme = "onyx-violet"
|
|
57
|
+
self.push_screen(MainScreen(self._projects))
|
|
58
|
+
|
|
59
|
+
# ── Screen helpers ────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def push_screen(self, screen, *args, **kwargs):
|
|
62
|
+
"""Override para pasar argumentos a las screens por nombre."""
|
|
63
|
+
if isinstance(screen, str):
|
|
64
|
+
name = screen
|
|
65
|
+
if name == "detail" and args:
|
|
66
|
+
screen = DetailScreen(args[0])
|
|
67
|
+
elif name == "editor":
|
|
68
|
+
screen = EditorScreen(args[0] if args else None)
|
|
69
|
+
elif name == "confirm_delete" and args:
|
|
70
|
+
screen = ConfirmDeleteScreen(args[0])
|
|
71
|
+
elif name == "github" and args:
|
|
72
|
+
screen = GithubScreen(args[0])
|
|
73
|
+
else:
|
|
74
|
+
screen = self.SCREENS[name]()
|
|
75
|
+
return super().push_screen(screen, **kwargs)
|
|
76
|
+
|
|
77
|
+
# ── Data operations (llamadas desde cualquier screen) ────────────────────
|
|
78
|
+
|
|
79
|
+
def save_project(self, project: Project) -> None:
|
|
80
|
+
self._projects = upsert_project(self._projects, project)
|
|
81
|
+
save_projects(self._projects)
|
|
82
|
+
self._refresh_main()
|
|
83
|
+
|
|
84
|
+
def delete_project(self, project_id: str) -> None:
|
|
85
|
+
self._projects = delete_project(self._projects, project_id)
|
|
86
|
+
save_projects(self._projects)
|
|
87
|
+
self._refresh_main()
|
|
88
|
+
|
|
89
|
+
def _refresh_main(self) -> None:
|
|
90
|
+
"""Actualiza la lista en MainScreen si está en el stack."""
|
|
91
|
+
for screen in self.screen_stack:
|
|
92
|
+
if isinstance(screen, MainScreen):
|
|
93
|
+
screen.refresh_projects(self._projects)
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run():
|
|
98
|
+
LazyPlanApp().run()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
run()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from lazyplan.storage import load_projects, save_projects, upsert_project
|
|
3
|
+
from lazyplan.models import Project, Status
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(
|
|
6
|
+
name="lazyplan",
|
|
7
|
+
help="TUI para capturar y gestionar ideas de proyectos.",
|
|
8
|
+
no_args_is_help=False,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.callback(invoke_without_command=True)
|
|
13
|
+
def default(ctx: typer.Context):
|
|
14
|
+
"""Abre la TUI si no se pasa ningún subcomando."""
|
|
15
|
+
if ctx.invoked_subcommand is None:
|
|
16
|
+
from lazyplan.app import LazyPlanApp
|
|
17
|
+
LazyPlanApp().run()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def new(
|
|
22
|
+
title: str = typer.Option(..., "-t", "--title", help="Título del proyecto"),
|
|
23
|
+
stack: str = typer.Option("", "-s", "--stack", help="Stack separado por comas"),
|
|
24
|
+
desc: str = typer.Option("", "-d", "--desc", help="Descripción"),
|
|
25
|
+
status: str = typer.Option("cruda", "--status", help="Estado inicial"),
|
|
26
|
+
):
|
|
27
|
+
"""Guarda un proyecto rápido sin abrir la TUI."""
|
|
28
|
+
stack_list = [s.strip() for s in stack.split(",") if s.strip()]
|
|
29
|
+
try:
|
|
30
|
+
st = Status(status)
|
|
31
|
+
except ValueError:
|
|
32
|
+
st = Status.CRUDA
|
|
33
|
+
|
|
34
|
+
project = Project(title=title, description=desc, stack=stack_list, status=st)
|
|
35
|
+
projects = load_projects()
|
|
36
|
+
projects = upsert_project(projects, project)
|
|
37
|
+
save_projects(projects)
|
|
38
|
+
typer.echo(f"✅ Proyecto '{title}' guardado [ID: {project.id}]")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def ls():
|
|
43
|
+
"""Lista los proyectos en la terminal (sin TUI)."""
|
|
44
|
+
projects = load_projects()
|
|
45
|
+
if not projects:
|
|
46
|
+
typer.echo("Sin proyectos todavía. Usá: lazyplan new -t 'Mi idea'")
|
|
47
|
+
return
|
|
48
|
+
for p in projects:
|
|
49
|
+
typer.echo(f" {p.id} {p.status_label} {p.title} [{p.stack_str}]")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
app()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integración con GitHub CLI (gh) para LazyPlan.
|
|
3
|
+
Permite crear repos y hacer push inicial desde la TUI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class GitHubResult:
|
|
13
|
+
success: bool
|
|
14
|
+
url: str = ""
|
|
15
|
+
error: str = ""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def gh_available() -> bool:
|
|
19
|
+
"""Devuelve True si gh está instalado y autenticado."""
|
|
20
|
+
try:
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
["gh", "auth", "status"],
|
|
23
|
+
capture_output=True, text=True
|
|
24
|
+
)
|
|
25
|
+
return result.returncode == 0
|
|
26
|
+
except FileNotFoundError:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def gh_username() -> str:
|
|
31
|
+
"""Devuelve el username de GitHub autenticado."""
|
|
32
|
+
try:
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
["gh", "api", "user", "--jq", ".login"],
|
|
35
|
+
capture_output=True, text=True
|
|
36
|
+
)
|
|
37
|
+
return result.stdout.strip()
|
|
38
|
+
except Exception:
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create_github_repo(
|
|
43
|
+
title: str,
|
|
44
|
+
description: str = "",
|
|
45
|
+
private: bool = True,
|
|
46
|
+
folder_path: str = "",
|
|
47
|
+
) -> GitHubResult:
|
|
48
|
+
"""
|
|
49
|
+
Crea un repo en GitHub y hace push inicial si hay carpeta.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
title: Nombre del proyecto (se convierte a slug)
|
|
53
|
+
description: Descripción del proyecto
|
|
54
|
+
private: True = privado, False = público
|
|
55
|
+
folder_path: Ruta local del proyecto para git init + push
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
GitHubResult con success, url y error
|
|
59
|
+
"""
|
|
60
|
+
slug = _to_slug(title)
|
|
61
|
+
visibility = "--private" if private else "--public"
|
|
62
|
+
|
|
63
|
+
# 1. Crear el repo en GitHub
|
|
64
|
+
cmd = ["gh", "repo", "create", slug, visibility, "--confirm"]
|
|
65
|
+
if description:
|
|
66
|
+
cmd += ["--description", description[:255]]
|
|
67
|
+
|
|
68
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
69
|
+
if result.returncode != 0:
|
|
70
|
+
return GitHubResult(
|
|
71
|
+
success=False,
|
|
72
|
+
error=result.stderr.strip() or "Error al crear el repositorio"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# URL del repo
|
|
76
|
+
username = gh_username()
|
|
77
|
+
repo_url = f"https://github.com/{username}/{slug}"
|
|
78
|
+
|
|
79
|
+
# 2. Si hay carpeta local, inicializar git y hacer push
|
|
80
|
+
if folder_path:
|
|
81
|
+
folder = Path(folder_path)
|
|
82
|
+
if folder.exists():
|
|
83
|
+
push_result = _git_init_and_push(folder, repo_url, slug)
|
|
84
|
+
if not push_result.success:
|
|
85
|
+
# El repo se creó pero el push falló: no es fatal
|
|
86
|
+
return GitHubResult(
|
|
87
|
+
success=True,
|
|
88
|
+
url=repo_url,
|
|
89
|
+
error=f"Repo creado pero push falló: {push_result.error}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return GitHubResult(success=True, url=repo_url)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _git_init_and_push(folder: Path, repo_url: str, slug: str) -> GitHubResult:
|
|
96
|
+
"""git init + add + commit + push en la carpeta dada."""
|
|
97
|
+
commands = [
|
|
98
|
+
["git", "init"],
|
|
99
|
+
["git", "add", "."],
|
|
100
|
+
["git", "commit", "-m", "Initial commit – LazyPlan 💤"],
|
|
101
|
+
["git", "branch", "-M", "main"],
|
|
102
|
+
["git", "remote", "add", "origin", f"{repo_url}.git"],
|
|
103
|
+
["git", "push", "-u", "origin", "main"],
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for cmd in commands:
|
|
107
|
+
result = subprocess.run(
|
|
108
|
+
cmd, cwd=str(folder), capture_output=True, text=True
|
|
109
|
+
)
|
|
110
|
+
if result.returncode != 0:
|
|
111
|
+
return GitHubResult(
|
|
112
|
+
success=False,
|
|
113
|
+
error=f"Falló '{' '.join(cmd)}': {result.stderr.strip()}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return GitHubResult(success=True, url=repo_url)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _to_slug(title: str) -> str:
|
|
120
|
+
"""Convierte un título a slug válido para GitHub."""
|
|
121
|
+
import re
|
|
122
|
+
slug = title.lower().strip()
|
|
123
|
+
slug = re.sub(r"[^\w\s-]", "", slug)
|
|
124
|
+
slug = re.sub(r"[\s_]+", "-", slug)
|
|
125
|
+
slug = re.sub(r"-+", "-", slug).strip("-")
|
|
126
|
+
return slug or "mi-proyecto"
|