snapcontext 2.0.0__tar.gz → 2.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.
- {snapcontext-2.0.0/snapcontext.egg-info → snapcontext-2.1.0}/PKG-INFO +12 -4
- {snapcontext-2.0.0 → snapcontext-2.1.0}/README.md +11 -3
- {snapcontext-2.0.0 → snapcontext-2.1.0}/agentes.py +114 -2
- {snapcontext-2.0.0 → snapcontext-2.1.0}/orquestador.py +8 -32
- {snapcontext-2.0.0 → snapcontext-2.1.0}/pyproject.toml +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0/snapcontext.egg-info}/PKG-INFO +12 -4
- {snapcontext-2.0.0 → snapcontext-2.1.0}/snapcontext.egg-info/SOURCES.txt +1 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/snapcontext.py +90 -37
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_auto_017.py +1 -1
- snapcontext-2.1.0/tests/test_diff_editor_210.py +103 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_editor_propio_200.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_exe_150.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_jetbrains_170.py +2 -2
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_mcp_014.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_mcp_140.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_memoria_015.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_permisos_013.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_plan_012.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_v010.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_validacion_130.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_vscode_016.py +2 -2
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_web_160.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/web/app.py +1 -1
- {snapcontext-2.0.0 → snapcontext-2.1.0}/LICENSE +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/setup.cfg +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/snapcontext.egg-info/dependency_links.txt +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/snapcontext.egg-info/entry_points.txt +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/snapcontext.egg-info/requires.txt +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/snapcontext.egg-info/top_level.txt +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_agentes.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_deteccion.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_embeddings_110.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_orquestador.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/tests/test_web_120.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/web/__init__.py +0 -0
- {snapcontext-2.0.0 → snapcontext-2.1.0}/web/static/index.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snapcontext
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: SnapContext: Asistente de IA con contexto automático para desarrollo
|
|
5
5
|
Author-email: SnapContext Contributors <tu@email.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
# SnapContext
|
|
47
47
|
|
|
48
|
-

|
|
49
49
|
[](https://pypi.org/project/snapcontext/)
|
|
50
50
|
[](https://github.com/NicolasBruña24/snapcontext/actions)
|
|
51
51
|

|
|
@@ -104,14 +104,22 @@ snapcontext interactive # o snapcontext --web
|
|
|
104
104
|
Auto-detección de proyecto (Flutter, Node/React, Python, Go, Rust…) que ajusta
|
|
105
105
|
carpetas, extensiones y comandos de test por defecto.
|
|
106
106
|
|
|
107
|
-
## 🛠️ Editor Integrado Propio (v2.
|
|
107
|
+
## 🛠️ Editor Integrado Propio (v2.1.0)
|
|
108
108
|
|
|
109
|
-
SnapContext
|
|
109
|
+
SnapContext incluye su propio editor integrado con soporte de parches unificados:
|
|
110
110
|
- **`--editor propio`**: Aplica cambios directamente y crea copias de seguridad automáticas en `~/.snapcontext/backups/`.
|
|
111
|
+
- **`--modo-edicion {auto,parche,sobrescribir}`**:
|
|
112
|
+
- `auto` (por defecto): Genera un parche unificado (`git apply` / `patch`) y hace fallback a sobrescritura si no es aplicable.
|
|
113
|
+
- `parche`: Fuerza la aplicación de parches unificados para ediciones precisas.
|
|
114
|
+
- `sobrescribir`: Sobrescribe el archivo completo.
|
|
111
115
|
- **`--editor aider`** (por defecto): Mantiene el flujo de edición con Aider para máxima compatibilidad.
|
|
112
116
|
|
|
113
117
|
```bash
|
|
118
|
+
# Edición precisa mediante diffs unificados
|
|
114
119
|
snapcontext "añadir endpoint de métricas" --editor propio
|
|
120
|
+
|
|
121
|
+
# Forzar modo parche
|
|
122
|
+
snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
|
|
115
123
|
```
|
|
116
124
|
|
|
117
125
|
## 🧭 Modos y alias
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SnapContext
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
[](https://pypi.org/project/snapcontext/)
|
|
5
5
|
[](https://github.com/NicolasBruña24/snapcontext/actions)
|
|
6
6
|

|
|
@@ -59,14 +59,22 @@ snapcontext interactive # o snapcontext --web
|
|
|
59
59
|
Auto-detección de proyecto (Flutter, Node/React, Python, Go, Rust…) que ajusta
|
|
60
60
|
carpetas, extensiones y comandos de test por defecto.
|
|
61
61
|
|
|
62
|
-
## 🛠️ Editor Integrado Propio (v2.
|
|
62
|
+
## 🛠️ Editor Integrado Propio (v2.1.0)
|
|
63
63
|
|
|
64
|
-
SnapContext
|
|
64
|
+
SnapContext incluye su propio editor integrado con soporte de parches unificados:
|
|
65
65
|
- **`--editor propio`**: Aplica cambios directamente y crea copias de seguridad automáticas en `~/.snapcontext/backups/`.
|
|
66
|
+
- **`--modo-edicion {auto,parche,sobrescribir}`**:
|
|
67
|
+
- `auto` (por defecto): Genera un parche unificado (`git apply` / `patch`) y hace fallback a sobrescritura si no es aplicable.
|
|
68
|
+
- `parche`: Fuerza la aplicación de parches unificados para ediciones precisas.
|
|
69
|
+
- `sobrescribir`: Sobrescribe el archivo completo.
|
|
66
70
|
- **`--editor aider`** (por defecto): Mantiene el flujo de edición con Aider para máxima compatibilidad.
|
|
67
71
|
|
|
68
72
|
```bash
|
|
73
|
+
# Edición precisa mediante diffs unificados
|
|
69
74
|
snapcontext "añadir endpoint de métricas" --editor propio
|
|
75
|
+
|
|
76
|
+
# Forzar modo parche
|
|
77
|
+
snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
|
|
70
78
|
```
|
|
71
79
|
|
|
72
80
|
## 🧭 Modos y alias
|
|
@@ -138,8 +138,10 @@ class AgenteEditorPropio:
|
|
|
138
138
|
"""Agente Editor Propio: editor integrado de SnapContext.
|
|
139
139
|
|
|
140
140
|
Aplica cambios directamente sobre el sistema de archivos sin depender de
|
|
141
|
-
herramientas externas.
|
|
142
|
-
seguridad previa
|
|
141
|
+
herramientas externas obligatorias.
|
|
142
|
+
- Fase 1: Sobrescritura segura con copia de seguridad previa en ``~/.snapcontext/backups/``.
|
|
143
|
+
- Fase 2: Generación y aplicación de parches unificados (unified diffs) con ``git apply`` / ``patch``
|
|
144
|
+
y fallback automático a sobrescritura.
|
|
143
145
|
"""
|
|
144
146
|
|
|
145
147
|
def sobrescribir(
|
|
@@ -159,6 +161,116 @@ class AgenteEditorPropio:
|
|
|
159
161
|
)
|
|
160
162
|
return sc._editor_sobrescribir(archivo, contenido, directorio=directorio)
|
|
161
163
|
|
|
164
|
+
def aplicar_parche(
|
|
165
|
+
self,
|
|
166
|
+
parche: str,
|
|
167
|
+
directorio: str = ".",
|
|
168
|
+
) -> bool:
|
|
169
|
+
"""Aplica un parche unificado en ``directorio``.
|
|
170
|
+
|
|
171
|
+
Devuelve ``True`` si se aplicó con éxito, ``False`` si falló.
|
|
172
|
+
"""
|
|
173
|
+
import snapcontext as sc
|
|
174
|
+
|
|
175
|
+
sc.depurar(f"[AgenteEditorPropio] Aplicando parche en '{directorio}'")
|
|
176
|
+
return sc._aplicar_parche(parche, directorio=directorio)
|
|
177
|
+
|
|
178
|
+
def ejecutar(
|
|
179
|
+
self,
|
|
180
|
+
archivos: List[str],
|
|
181
|
+
mensaje: str,
|
|
182
|
+
directorio: str = ".",
|
|
183
|
+
modo_edicion: str = "auto",
|
|
184
|
+
modelo: Optional[str] = None,
|
|
185
|
+
) -> bool:
|
|
186
|
+
"""Aplica las modificaciones para `archivos` con el `mensaje` especificado.
|
|
187
|
+
|
|
188
|
+
- modo_edicion == 'parche': intenta aplicar parche unificado (falla si no puede).
|
|
189
|
+
- modo_edicion == 'sobrescribir': aplica directamente sobrescritura de archivo.
|
|
190
|
+
- modo_edicion == 'auto': intenta primero parche unificado; si falla o no es diff,
|
|
191
|
+
hace fallback a sobrescritura.
|
|
192
|
+
"""
|
|
193
|
+
import snapcontext as sc
|
|
194
|
+
from pathlib import Path
|
|
195
|
+
|
|
196
|
+
pref = sc.cargar_configuracion()
|
|
197
|
+
proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
|
|
198
|
+
raiz = Path(directorio).resolve()
|
|
199
|
+
todo_ok = True
|
|
200
|
+
|
|
201
|
+
for arch in archivos:
|
|
202
|
+
camino = (raiz / arch).resolve()
|
|
203
|
+
contenido_actual = ""
|
|
204
|
+
if camino.is_file():
|
|
205
|
+
try:
|
|
206
|
+
contenido_actual = camino.read_text(encoding="utf-8", errors="replace")
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
if modo_edicion in ("parche", "auto"):
|
|
211
|
+
prompt = (
|
|
212
|
+
f"Genera un parche unificado (unified diff) que modifique el archivo para cumplir con la tarea. "
|
|
213
|
+
f"El parche debe ser aplicable con 'patch -p1' o 'git apply' (encabezados --- a/{arch} y +++ b/{arch}).\n\n"
|
|
214
|
+
f"Tarea: {mensaje}\n"
|
|
215
|
+
f"Archivo: {arch}\n\n"
|
|
216
|
+
f"Contenido actual:\n```\n{contenido_actual}\n```\n\n"
|
|
217
|
+
f"Devuelve ÚNICAMENTE el diff unificado, sin explicaciones ni rodeos."
|
|
218
|
+
)
|
|
219
|
+
try:
|
|
220
|
+
respuesta = sc._enviar_al_proveedor(
|
|
221
|
+
proveedor, modelo,
|
|
222
|
+
[{"role": "user", "content": prompt}]
|
|
223
|
+
)
|
|
224
|
+
# Limpiar delimitadores markdown si vinieron
|
|
225
|
+
diff_limpio = respuesta
|
|
226
|
+
if "--- " in diff_limpio and "+++ " in diff_limpio:
|
|
227
|
+
# Extraer solo desde ---
|
|
228
|
+
idx_inicio = diff_limpio.find("--- ")
|
|
229
|
+
diff_limpio = diff_limpio[idx_inicio:]
|
|
230
|
+
if "```" in diff_limpio:
|
|
231
|
+
diff_limpio = diff_limpio[:diff_limpio.find("```")]
|
|
232
|
+
|
|
233
|
+
# Si parece un diff unificado válido, intentar aplicarlo
|
|
234
|
+
if ("--- " in diff_limpio and "+++ " in diff_limpio and "@@" in diff_limpio):
|
|
235
|
+
if self.aplicar_parche(diff_limpio, str(raiz)):
|
|
236
|
+
continue
|
|
237
|
+
sc.aviso(f"[AgenteEditorPropio] Parche para '{arch}' falló al aplicarse.")
|
|
238
|
+
if modo_edicion == "parche":
|
|
239
|
+
todo_ok = False
|
|
240
|
+
continue
|
|
241
|
+
except Exception as exc:
|
|
242
|
+
sc.depurar(f"[AgenteEditorPropio] Error pidiendo parche para '{arch}': {exc}")
|
|
243
|
+
if modo_edicion == "parche":
|
|
244
|
+
sc.error(f"Error generando parche para {arch}: {exc}")
|
|
245
|
+
todo_ok = False
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
# Fallback o modo sobrescribir
|
|
249
|
+
sc.depurar(f"[AgenteEditorPropio] Aplicando edición por sobrescritura para '{arch}'...")
|
|
250
|
+
prompt_sobrescribir = (
|
|
251
|
+
f"Modifica el siguiente archivo para cumplir con la tarea.\n\n"
|
|
252
|
+
f"Tarea: {mensaje}\n"
|
|
253
|
+
f"Archivo: {arch}\n\n"
|
|
254
|
+
f"Contenido actual:\n```\n{contenido_actual}\n```\n\n"
|
|
255
|
+
f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
|
|
256
|
+
)
|
|
257
|
+
try:
|
|
258
|
+
nuevo_contenido = sc._enviar_al_proveedor(
|
|
259
|
+
proveedor, modelo,
|
|
260
|
+
[{"role": "user", "content": prompt_sobrescribir}]
|
|
261
|
+
)
|
|
262
|
+
if nuevo_contenido.startswith("```"):
|
|
263
|
+
lineas = nuevo_contenido.splitlines()
|
|
264
|
+
if len(lineas) >= 2 and lineas[-1].startswith("```"):
|
|
265
|
+
nuevo_contenido = "\n".join(lineas[1:-1])
|
|
266
|
+
if not self.sobrescribir(arch, nuevo_contenido, str(raiz)):
|
|
267
|
+
todo_ok = False
|
|
268
|
+
except Exception as exc:
|
|
269
|
+
sc.error(f"Error generando cambios por sobrescritura para {arch}: {exc}")
|
|
270
|
+
todo_ok = False
|
|
271
|
+
|
|
272
|
+
return todo_ok
|
|
273
|
+
|
|
162
274
|
|
|
163
275
|
|
|
164
276
|
|
|
@@ -180,38 +180,14 @@ class Orquestador:
|
|
|
180
180
|
if editor_tipo == "propio":
|
|
181
181
|
sc.depurar("[Orquestador] Modo edición directa (AgenteEditorPropio).")
|
|
182
182
|
self._emitir_tipo("editor", accion="iniciar", archivos=seleccion)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
contenido_actual = camino.read_text(encoding="utf-8", errors="replace")
|
|
192
|
-
except Exception:
|
|
193
|
-
pass
|
|
194
|
-
prompt_editor = (
|
|
195
|
-
f"Modifica el siguiente archivo para cumplir con la tarea.\n\n"
|
|
196
|
-
f"Tarea: {consulta}\n"
|
|
197
|
-
f"Archivo: {arch}\n\n"
|
|
198
|
-
f"Contenido actual:\n```\n{contenido_actual}\n```\n\n"
|
|
199
|
-
f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
|
|
200
|
-
)
|
|
201
|
-
try:
|
|
202
|
-
nuevo_contenido = sc._enviar_al_proveedor(
|
|
203
|
-
proveedor, getattr(args, "modelo", None),
|
|
204
|
-
[{"role": "user", "content": prompt_editor}]
|
|
205
|
-
)
|
|
206
|
-
if nuevo_contenido.startswith("```"):
|
|
207
|
-
lineas = nuevo_contenido.splitlines()
|
|
208
|
-
if len(lineas) >= 2 and lineas[-1].startswith("```"):
|
|
209
|
-
nuevo_contenido = "\n".join(lineas[1:-1])
|
|
210
|
-
if not self.agente_editor_propio.sobrescribir(arch, nuevo_contenido, str(raiz)):
|
|
211
|
-
ok = False
|
|
212
|
-
except Exception as exc:
|
|
213
|
-
sc.error(f"Error generando cambios para {arch}: {exc}")
|
|
214
|
-
ok = False
|
|
183
|
+
modo_ed = getattr(args, "modo_edicion", "auto") or "auto"
|
|
184
|
+
ok = self.agente_editor_propio.ejecutar(
|
|
185
|
+
seleccion,
|
|
186
|
+
consulta,
|
|
187
|
+
directorio=str(raiz),
|
|
188
|
+
modo_edicion=modo_ed,
|
|
189
|
+
modelo=getattr(args, "modelo", None),
|
|
190
|
+
)
|
|
215
191
|
self._emitir_tipo("editor", accion="fin", ok=ok)
|
|
216
192
|
else:
|
|
217
193
|
sc.depurar("[Orquestador] Modo edición directa (AgenteEditor).")
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
# Nombre verificado disponible en PyPI (18/08/2026): pypi.org/pypi/snapcontext → 404.
|
|
7
7
|
# Alternativas si algún día estuviera ocupado: "snapcontext-cli" o "snapcontext-tool".
|
|
8
8
|
name = "snapcontext"
|
|
9
|
-
version = "2.
|
|
9
|
+
version = "2.1.0"
|
|
10
10
|
description = "SnapContext: Asistente de IA con contexto automático para desarrollo"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.9"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snapcontext
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: SnapContext: Asistente de IA con contexto automático para desarrollo
|
|
5
5
|
Author-email: SnapContext Contributors <tu@email.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
# SnapContext
|
|
47
47
|
|
|
48
|
-

|
|
49
49
|
[](https://pypi.org/project/snapcontext/)
|
|
50
50
|
[](https://github.com/NicolasBruña24/snapcontext/actions)
|
|
51
51
|

|
|
@@ -104,14 +104,22 @@ snapcontext interactive # o snapcontext --web
|
|
|
104
104
|
Auto-detección de proyecto (Flutter, Node/React, Python, Go, Rust…) que ajusta
|
|
105
105
|
carpetas, extensiones y comandos de test por defecto.
|
|
106
106
|
|
|
107
|
-
## 🛠️ Editor Integrado Propio (v2.
|
|
107
|
+
## 🛠️ Editor Integrado Propio (v2.1.0)
|
|
108
108
|
|
|
109
|
-
SnapContext
|
|
109
|
+
SnapContext incluye su propio editor integrado con soporte de parches unificados:
|
|
110
110
|
- **`--editor propio`**: Aplica cambios directamente y crea copias de seguridad automáticas en `~/.snapcontext/backups/`.
|
|
111
|
+
- **`--modo-edicion {auto,parche,sobrescribir}`**:
|
|
112
|
+
- `auto` (por defecto): Genera un parche unificado (`git apply` / `patch`) y hace fallback a sobrescritura si no es aplicable.
|
|
113
|
+
- `parche`: Fuerza la aplicación de parches unificados para ediciones precisas.
|
|
114
|
+
- `sobrescribir`: Sobrescribe el archivo completo.
|
|
111
115
|
- **`--editor aider`** (por defecto): Mantiene el flujo de edición con Aider para máxima compatibilidad.
|
|
112
116
|
|
|
113
117
|
```bash
|
|
118
|
+
# Edición precisa mediante diffs unificados
|
|
114
119
|
snapcontext "añadir endpoint de métricas" --editor propio
|
|
120
|
+
|
|
121
|
+
# Forzar modo parche
|
|
122
|
+
snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
|
|
115
123
|
```
|
|
116
124
|
|
|
117
125
|
## 🧭 Modos y alias
|
|
@@ -39,6 +39,7 @@ Open-source y pensado para ser fácil de extender (ver ejecutar_bucle_test).
|
|
|
39
39
|
|
|
40
40
|
import argparse
|
|
41
41
|
import ast
|
|
42
|
+
import difflib
|
|
42
43
|
import fnmatch
|
|
43
44
|
import json
|
|
44
45
|
import os
|
|
@@ -110,7 +111,7 @@ except ImportError: # pragma: no cover
|
|
|
110
111
|
import concurrent.futures
|
|
111
112
|
from concurrent.futures import ThreadPoolExecutor
|
|
112
113
|
|
|
113
|
-
VERSION = "2.
|
|
114
|
+
VERSION = "2.1.0"
|
|
114
115
|
|
|
115
116
|
|
|
116
117
|
# ─── Configuración por tipo de proyecto ────────────────────────────────────
|
|
@@ -1532,6 +1533,78 @@ def _editor_sobrescribir(archivo: str, contenido: str,
|
|
|
1532
1533
|
return False
|
|
1533
1534
|
|
|
1534
1535
|
|
|
1536
|
+
def _generar_parche(original: str, nuevo: str, ruta_archivo: str) -> str:
|
|
1537
|
+
"""Genera un parche unificado (unified diff) entre `original` y `nuevo`.
|
|
1538
|
+
|
|
1539
|
+
El encabezado cumple con el estándar de `patch` y `git apply` (a/ruta b/ruta).
|
|
1540
|
+
"""
|
|
1541
|
+
ruta_posix = str(ruta_archivo).replace("\\", "/").strip()
|
|
1542
|
+
if ruta_posix.startswith("./"):
|
|
1543
|
+
ruta_posix = ruta_posix[2:]
|
|
1544
|
+
|
|
1545
|
+
lineas_orig = original.splitlines(keepends=True)
|
|
1546
|
+
lineas_nuevo = nuevo.splitlines(keepends=True)
|
|
1547
|
+
|
|
1548
|
+
diff = difflib.unified_diff(
|
|
1549
|
+
lineas_orig,
|
|
1550
|
+
lineas_nuevo,
|
|
1551
|
+
fromfile=f"a/{ruta_posix}",
|
|
1552
|
+
tofile=f"b/{ruta_posix}",
|
|
1553
|
+
)
|
|
1554
|
+
return "".join(diff)
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
def _aplicar_parche(parche: str, directorio: str = ".") -> bool:
|
|
1558
|
+
"""Aplica un parche unificado en `directorio` usando `git apply` o `patch`.
|
|
1559
|
+
|
|
1560
|
+
1. Escribe el parche en un archivo temporal.
|
|
1561
|
+
2. Intenta aplicar con `git apply --whitespace=nowarn <temp_file>`.
|
|
1562
|
+
3. Si `git` falla o no está disponible, intenta con `patch -p1 -i <temp_file>`.
|
|
1563
|
+
4. Devuelve True si se aplicó limpiamente, False si hubo error o conflicto.
|
|
1564
|
+
"""
|
|
1565
|
+
if not parche or not parche.strip():
|
|
1566
|
+
aviso("[EditorPropio] Parche vacío; no se aplicaron cambios.")
|
|
1567
|
+
return False
|
|
1568
|
+
|
|
1569
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".diff", encoding="utf-8", delete=False) as f:
|
|
1570
|
+
f.write(parche)
|
|
1571
|
+
temp_path = f.name
|
|
1572
|
+
|
|
1573
|
+
try:
|
|
1574
|
+
flags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
|
1575
|
+
raiz_res = str(Path(directorio).resolve())
|
|
1576
|
+
|
|
1577
|
+
# Intentar primero con git apply (muy estándar en repos de desarrollo)
|
|
1578
|
+
if shutil.which("git"):
|
|
1579
|
+
cmd_git = ["git", "apply", "--whitespace=nowarn", temp_path]
|
|
1580
|
+
res_git = subprocess.run(
|
|
1581
|
+
cmd_git, cwd=raiz_res, capture_output=True, text=True, creationflags=flags
|
|
1582
|
+
)
|
|
1583
|
+
if res_git.returncode == 0:
|
|
1584
|
+
exito("[EditorPropio] Parche unificado aplicado correctamente con git apply.")
|
|
1585
|
+
return True
|
|
1586
|
+
depurar(f"[EditorPropio] git apply falló (código {res_git.returncode}): {res_git.stderr}")
|
|
1587
|
+
|
|
1588
|
+
# Fallback a patch
|
|
1589
|
+
if shutil.which("patch"):
|
|
1590
|
+
cmd_patch = ["patch", "-p1", "-i", temp_path]
|
|
1591
|
+
res_patch = subprocess.run(
|
|
1592
|
+
cmd_patch, cwd=raiz_res, capture_output=True, text=True, creationflags=flags
|
|
1593
|
+
)
|
|
1594
|
+
if res_patch.returncode == 0:
|
|
1595
|
+
exito("[EditorPropio] Parche unificado aplicado correctamente con patch.")
|
|
1596
|
+
return True
|
|
1597
|
+
depurar(f"[EditorPropio] patch falló (código {res_patch.returncode}): {res_patch.stderr}")
|
|
1598
|
+
|
|
1599
|
+
aviso("[EditorPropio] No se pudo aplicar el parche automáticamente (ni git apply ni patch tuvieron éxito).")
|
|
1600
|
+
return False
|
|
1601
|
+
finally:
|
|
1602
|
+
try:
|
|
1603
|
+
os.remove(temp_path)
|
|
1604
|
+
except OSError:
|
|
1605
|
+
pass
|
|
1606
|
+
|
|
1607
|
+
|
|
1535
1608
|
def _extraer_error(resultado: "subprocess.CompletedProcess") -> str:
|
|
1536
1609
|
"""Une stdout+stderr, limpia códigos ANSI y limita el tamaño del error
|
|
1537
1610
|
que se mostrará a Aider (evita llenar el contexto)."""
|
|
@@ -3139,40 +3212,14 @@ def _ejecutar_paso_plan(paso: dict, args: argparse.Namespace,
|
|
|
3139
3212
|
_, ruta_raiz, _, seleccion = plan
|
|
3140
3213
|
|
|
3141
3214
|
if editor_elegido == "propio":
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
try:
|
|
3151
|
-
contenido_actual = camino.read_text(encoding="utf-8", errors="replace")
|
|
3152
|
-
except Exception:
|
|
3153
|
-
pass
|
|
3154
|
-
prompt_editor = (
|
|
3155
|
-
f"Modifica el siguiente archivo para cumplir con la tarea.\n\n"
|
|
3156
|
-
f"Tarea: {descripcion}\n"
|
|
3157
|
-
f"Archivo: {arch}\n\n"
|
|
3158
|
-
f"Contenido actual:\n```\n{contenido_actual}\n```\n\n"
|
|
3159
|
-
f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
|
|
3160
|
-
)
|
|
3161
|
-
try:
|
|
3162
|
-
nuevo_contenido = _enviar_al_proveedor(
|
|
3163
|
-
proveedor, getattr(args, "modelo", None),
|
|
3164
|
-
[{"role": "user", "content": prompt_editor}]
|
|
3165
|
-
)
|
|
3166
|
-
# Limpiar bloques de código si vinieron envueltos en ```
|
|
3167
|
-
if nuevo_contenido.startswith("```"):
|
|
3168
|
-
lineas = nuevo_contenido.splitlines()
|
|
3169
|
-
if len(lineas) >= 2 and lineas[-1].startswith("```"):
|
|
3170
|
-
nuevo_contenido = "\n".join(lineas[1:-1])
|
|
3171
|
-
if not _editor_sobrescribir(arch, nuevo_contenido, str(ruta_raiz)):
|
|
3172
|
-
todo_ok = False
|
|
3173
|
-
except Exception as exc:
|
|
3174
|
-
error(f"Error generando cambios para {arch}: {exc}")
|
|
3175
|
-
todo_ok = False
|
|
3215
|
+
modo_ed = getattr(args, "modo_edicion", "auto") or "auto"
|
|
3216
|
+
todo_ok = orch.agente_editor_propio.ejecutar(
|
|
3217
|
+
seleccion,
|
|
3218
|
+
descripcion,
|
|
3219
|
+
directorio=str(ruta_raiz),
|
|
3220
|
+
modo_edicion=modo_ed,
|
|
3221
|
+
modelo=getattr(args, "modelo", None),
|
|
3222
|
+
)
|
|
3176
3223
|
return (todo_ok, f"EditorPropio sobre {len(seleccion)} archivo(s)")
|
|
3177
3224
|
|
|
3178
3225
|
if getattr(args, "test_loop", False):
|
|
@@ -4988,7 +5035,7 @@ def _pintar(texto: str, clave: str) -> str:
|
|
|
4988
5035
|
# Categorías en orden de aparición; cada opción se muestra una sola vez.
|
|
4989
5036
|
CATEGORIAS_AYUDA = (
|
|
4990
5037
|
("Modos de ejecución",
|
|
4991
|
-
("--plan", "--auto", "--editor", "--chat", "--web", "--web-puerto", "--demo",
|
|
5038
|
+
("--plan", "--auto", "--editor", "--modo-edicion", "--chat", "--web", "--web-puerto", "--demo",
|
|
4992
5039
|
"--init", "--init-claude", "--historial", "--historial-limpiar")),
|
|
4993
5040
|
("Selección de archivos",
|
|
4994
5041
|
("consulta", "--local", "--iniciar-proyecto", "--experto",
|
|
@@ -5427,7 +5474,13 @@ def crear_parser() -> argparse.ArgumentParser:
|
|
|
5427
5474
|
"--editor", choices=["aider", "propio"], default="aider",
|
|
5428
5475
|
help="Editor a usar para aplicar cambios: 'aider' (por defecto, "
|
|
5429
5476
|
"requiere Aider instalado) o 'propio' (editor integrado de "
|
|
5430
|
-
"SnapContext
|
|
5477
|
+
"SnapContext con soporte de parches unificados y backups automáticos).",
|
|
5478
|
+
)
|
|
5479
|
+
parser.add_argument(
|
|
5480
|
+
"--modo-edicion", choices=["sobrescribir", "parche", "auto"], default="auto",
|
|
5481
|
+
help="Estrategia del editor propio: 'auto' (intenta aplicar parche unificado, "
|
|
5482
|
+
"fallback a sobrescritura), 'parche' (solo parches unificados) o "
|
|
5483
|
+
"'sobrescribir' (sobrescritura completa del archivo).",
|
|
5431
5484
|
)
|
|
5432
5485
|
# Ayuda agrupada por categorías (-h/--help) — v1.7.
|
|
5433
5486
|
parser.add_argument(
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Tests de la Fase 2 del Editor Propio (Diffs y Parches Unificados) — v2.1.0."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from unittest import mock
|
|
11
|
+
|
|
12
|
+
import snapcontext as sc
|
|
13
|
+
from agentes import AgenteEditorPropio
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestDiffYParches(unittest.TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
self.tmp_dir = tempfile.mkdtemp()
|
|
19
|
+
self.raiz = Path(self.tmp_dir).resolve()
|
|
20
|
+
self.backups_dir = self.raiz / ".snapcontext_backups"
|
|
21
|
+
self.patch_backup = mock.patch.object(sc, "BACKUPS_DIR", self.backups_dir)
|
|
22
|
+
self.patch_backup.start()
|
|
23
|
+
|
|
24
|
+
def tearDown(self):
|
|
25
|
+
self.patch_backup.stop()
|
|
26
|
+
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
|
27
|
+
|
|
28
|
+
def test_generar_parche_unificado(self):
|
|
29
|
+
orig = "linea 1\nlinea 2\nlinea 3\n"
|
|
30
|
+
nuevo = "linea 1\nlinea 2 modificada\nlinea 3\n"
|
|
31
|
+
parche = sc._generar_parche(orig, nuevo, "src/archivo.py")
|
|
32
|
+
self.assertIn("--- a/src/archivo.py", parche)
|
|
33
|
+
self.assertIn("+++ b/src/archivo.py", parche)
|
|
34
|
+
self.assertIn("-linea 2", parche)
|
|
35
|
+
self.assertIn("+linea 2 modificada", parche)
|
|
36
|
+
|
|
37
|
+
def test_aplicar_parche_vacio_devuelve_false(self):
|
|
38
|
+
self.assertFalse(sc._aplicar_parche("", str(self.raiz)))
|
|
39
|
+
self.assertFalse(sc._aplicar_parche(" ", str(self.raiz)))
|
|
40
|
+
|
|
41
|
+
def test_aplicar_parche_con_git_mock(self):
|
|
42
|
+
parche = "--- a/test.py\n+++ b/test.py\n@@ -1 +1 @@\n-hola\n+mundo\n"
|
|
43
|
+
proc_mock = mock.MagicMock(returncode=0, stderr="")
|
|
44
|
+
with mock.patch("shutil.which", return_value="git"), \
|
|
45
|
+
mock.patch("subprocess.run", return_value=proc_mock) as sub_mock:
|
|
46
|
+
ok = sc._aplicar_parche(parche, str(self.raiz))
|
|
47
|
+
self.assertTrue(ok)
|
|
48
|
+
sub_mock.assert_called_once()
|
|
49
|
+
cmd_args = sub_mock.call_args[0][0]
|
|
50
|
+
self.assertEqual(cmd_args[:2], ["git", "apply"])
|
|
51
|
+
|
|
52
|
+
def test_aplicar_parche_fallback_patch_mock(self):
|
|
53
|
+
parche = "--- a/test.py\n+++ b/test.py\n@@ -1 +1 @@\n-hola\n+mundo\n"
|
|
54
|
+
proc_fail = mock.MagicMock(returncode=1, stderr="error git")
|
|
55
|
+
proc_ok = mock.MagicMock(returncode=0, stderr="")
|
|
56
|
+
with mock.patch("shutil.which", side_effect=lambda x: x if x in ("git", "patch") else None), \
|
|
57
|
+
mock.patch("subprocess.run", side_effect=[proc_fail, proc_ok]) as sub_mock:
|
|
58
|
+
ok = sc._aplicar_parche(parche, str(self.raiz))
|
|
59
|
+
self.assertTrue(ok)
|
|
60
|
+
self.assertEqual(sub_mock.call_count, 2)
|
|
61
|
+
|
|
62
|
+
def test_ejecutar_modo_parche_exitoso(self):
|
|
63
|
+
agente = AgenteEditorPropio()
|
|
64
|
+
archivo = "modulo.py"
|
|
65
|
+
(self.raiz / archivo).write_text("def fn(): return 1\n", encoding="utf-8")
|
|
66
|
+
diff_simulado = "--- a/modulo.py\n+++ b/modulo.py\n@@ -1 +1 @@\n-def fn(): return 1\n+def fn(): return 2\n"
|
|
67
|
+
|
|
68
|
+
with mock.patch.object(sc, "_enviar_al_proveedor", return_value=diff_simulado), \
|
|
69
|
+
mock.patch.object(agente, "aplicar_parche", return_value=True) as ap_mock:
|
|
70
|
+
ok = agente.ejecutar([archivo], "cambiar retorno a 2", directorio=str(self.raiz), modo_edicion="parche")
|
|
71
|
+
self.assertTrue(ok)
|
|
72
|
+
ap_mock.assert_called_once()
|
|
73
|
+
|
|
74
|
+
def test_ejecutar_fallback_a_sobrescritura(self):
|
|
75
|
+
agente = AgenteEditorPropio()
|
|
76
|
+
archivo = "modulo.py"
|
|
77
|
+
(self.raiz / archivo).write_text("def fn(): return 1\n", encoding="utf-8")
|
|
78
|
+
# Proveedor devuelve código completo en lugar de diff
|
|
79
|
+
codigo_completo = "def fn(): return 2\n"
|
|
80
|
+
|
|
81
|
+
with mock.patch.object(sc, "_enviar_al_proveedor", side_effect=["no es un diff", codigo_completo]), \
|
|
82
|
+
mock.patch.object(agente, "sobrescribir", return_value=True) as sob_mock:
|
|
83
|
+
ok = agente.ejecutar([archivo], "cambiar retorno a 2", directorio=str(self.raiz), modo_edicion="auto")
|
|
84
|
+
self.assertTrue(ok)
|
|
85
|
+
sob_mock.assert_called_once_with(archivo, codigo_completo, str(self.raiz))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestFlagsEdicion(unittest.TestCase):
|
|
89
|
+
def test_flag_modo_edicion_por_defecto_auto(self):
|
|
90
|
+
args = sc.crear_parser().parse_args(["consulta"])
|
|
91
|
+
self.assertEqual(args.modo_edicion, "auto")
|
|
92
|
+
|
|
93
|
+
def test_flag_modo_edicion_parche_y_sobrescribir(self):
|
|
94
|
+
parser = sc.crear_parser()
|
|
95
|
+
self.assertEqual(parser.parse_args(["--modo-edicion", "parche", "c"]).modo_edicion, "parche")
|
|
96
|
+
self.assertEqual(parser.parse_args(["--modo-edicion", "sobrescribir", "c"]).modo_edicion, "sobrescribir")
|
|
97
|
+
|
|
98
|
+
def test_version_es_2_1_0(self):
|
|
99
|
+
self.assertEqual(sc.VERSION, "2.1.0")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
unittest.main()
|
|
@@ -84,7 +84,7 @@ class TestFlagsEditor(unittest.TestCase):
|
|
|
84
84
|
self.assertIsInstance(orch.agente_editor_propio, AgenteEditorPropio)
|
|
85
85
|
|
|
86
86
|
def test_version_es_2_0_0(self):
|
|
87
|
-
self.assertEqual(sc.VERSION, "2.
|
|
87
|
+
self.assertEqual(sc.VERSION, "2.1.0")
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
if __name__ == "__main__":
|
|
@@ -17,7 +17,7 @@ import snapcontext as sc # noqa: E402
|
|
|
17
17
|
|
|
18
18
|
class TestVersion(unittest.TestCase):
|
|
19
19
|
def test_version_170_coherente(self):
|
|
20
|
-
self.assertEqual(sc.VERSION, "2.
|
|
20
|
+
self.assertEqual(sc.VERSION, "2.1.0")
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class TestEstructuraJetBrains(unittest.TestCase):
|
|
@@ -90,7 +90,7 @@ class TestGradleYKotlin(unittest.TestCase):
|
|
|
90
90
|
def test_gradle_configura_intellij(self):
|
|
91
91
|
gradle = (JB / "build.gradle.kts").read_text(encoding="utf-8")
|
|
92
92
|
self.assertIn("org.jetbrains.intellij", gradle)
|
|
93
|
-
self.assertIn('version = "2.
|
|
93
|
+
self.assertIn('version = "2.1.0"', gradle)
|
|
94
94
|
self.assertIn("patchPluginXml", gradle)
|
|
95
95
|
|
|
96
96
|
def test_kotlin_usa_processbuilder(self):
|
|
@@ -248,7 +248,7 @@ class TestChatComandosGrafo(unittest.TestCase):
|
|
|
248
248
|
|
|
249
249
|
class TestVersion(unittest.TestCase):
|
|
250
250
|
def test_version_140_coherente(self):
|
|
251
|
-
self.assertEqual(sc.VERSION, "2.
|
|
251
|
+
self.assertEqual(sc.VERSION, "2.1.0")
|
|
252
252
|
|
|
253
253
|
|
|
254
254
|
if __name__ == "__main__":
|
|
@@ -182,7 +182,7 @@ class TestFlagsConfirmarCLI(unittest.TestCase):
|
|
|
182
182
|
self.assertFalse(self._parse(["--chat", "--no-confirmar"]).confirmar)
|
|
183
183
|
|
|
184
184
|
def test_version_es_1_2_0(self):
|
|
185
|
-
self.assertEqual(sc.VERSION, "2.
|
|
185
|
+
self.assertEqual(sc.VERSION, "2.1.0")
|
|
186
186
|
|
|
187
187
|
|
|
188
188
|
if __name__ == "__main__":
|
|
@@ -269,7 +269,7 @@ class TestFlagsPlanCLI(unittest.TestCase):
|
|
|
269
269
|
self.assertIsNone(self._parse(["--plan", "x"]).branch)
|
|
270
270
|
|
|
271
271
|
def test_version_es_1_2_0(self):
|
|
272
|
-
self.assertEqual(sc.VERSION, "2.
|
|
272
|
+
self.assertEqual(sc.VERSION, "2.1.0")
|
|
273
273
|
|
|
274
274
|
|
|
275
275
|
if __name__ == "__main__":
|
|
@@ -168,7 +168,7 @@ class TestFlagsCLI(unittest.TestCase):
|
|
|
168
168
|
self.assertTrue(args.vista_previa)
|
|
169
169
|
|
|
170
170
|
def test_version_es_1_2_0(self):
|
|
171
|
-
self.assertEqual(sc.VERSION, "2.
|
|
171
|
+
self.assertEqual(sc.VERSION, "2.1.0")
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
class TestComandosAgenteChat(unittest.TestCase):
|
|
@@ -126,7 +126,7 @@ class TestIniciarProyecto(unittest.TestCase):
|
|
|
126
126
|
class TestVersion(unittest.TestCase):
|
|
127
127
|
def test_version_130_coherente(self):
|
|
128
128
|
import snapcontext as sc
|
|
129
|
-
self.assertEqual(sc.VERSION, "2.
|
|
129
|
+
self.assertEqual(sc.VERSION, "2.1.0")
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
if __name__ == "__main__":
|
|
@@ -30,7 +30,7 @@ class TestEstructuraVsCode(unittest.TestCase):
|
|
|
30
30
|
def test_package_json_valido(self):
|
|
31
31
|
manifiesto = json.loads((VSCODE / "package.json").read_text("utf-8"))
|
|
32
32
|
self.assertEqual(manifiesto["name"], "snapcontext-vscode")
|
|
33
|
-
self.assertEqual(manifiesto["version"], "2.
|
|
33
|
+
self.assertEqual(manifiesto["version"], "2.1.0")
|
|
34
34
|
self.assertEqual(manifiesto["main"], "./extension.js")
|
|
35
35
|
|
|
36
36
|
def test_comandos_contribuidos(self):
|
|
@@ -87,7 +87,7 @@ class TestEstructuraVsCode(unittest.TestCase):
|
|
|
87
87
|
|
|
88
88
|
class TestVersionCli(unittest.TestCase):
|
|
89
89
|
def test_version_es_1_2_0(self):
|
|
90
|
-
self.assertEqual(sc.VERSION, "2.
|
|
90
|
+
self.assertEqual(sc.VERSION, "2.1.0")
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
if __name__ == "__main__":
|
|
@@ -17,7 +17,7 @@ INDEX = RAIZ / "web" / "static" / "index.html"
|
|
|
17
17
|
|
|
18
18
|
class TestVersion(unittest.TestCase):
|
|
19
19
|
def test_version_160_coherente(self):
|
|
20
|
-
self.assertEqual(sc.VERSION, "2.
|
|
20
|
+
self.assertEqual(sc.VERSION, "2.1.0")
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class TestFrontendV160(unittest.TestCase):
|
|
@@ -44,7 +44,7 @@ _DIRECTORIO_DEFECTO = "."
|
|
|
44
44
|
|
|
45
45
|
def crear_app() -> FastAPI:
|
|
46
46
|
"""Construye y devuelve la app FastAPI (rutas + WebSocket)."""
|
|
47
|
-
app = FastAPI(title="SnapContext Web", version="2.
|
|
47
|
+
app = FastAPI(title="SnapContext Web", version="2.1.0")
|
|
48
48
|
|
|
49
49
|
@app.get("/")
|
|
50
50
|
async def raiz():
|
|
File without changes
|
|
File without changes
|
{snapcontext-2.0.0 → snapcontext-2.1.0}/snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|