snapcontext 3.1.0__tar.gz → 3.2.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.
Files changed (43) hide show
  1. {snapcontext-3.1.0/snapcontext.egg-info → snapcontext-3.2.0}/PKG-INFO +16 -2
  2. {snapcontext-3.1.0 → snapcontext-3.2.0}/README.md +15 -1
  3. {snapcontext-3.1.0 → snapcontext-3.2.0}/pyproject.toml +1 -1
  4. {snapcontext-3.1.0 → snapcontext-3.2.0/snapcontext.egg-info}/PKG-INFO +16 -2
  5. {snapcontext-3.1.0 → snapcontext-3.2.0}/snapcontext.egg-info/SOURCES.txt +2 -0
  6. {snapcontext-3.1.0 → snapcontext-3.2.0}/snapcontext.py +106 -3
  7. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_auto_017.py +1 -1
  8. snapcontext-3.2.0/tests/test_bienvenida_311.py +133 -0
  9. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_diagnostico_310.py +1 -1
  10. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_diff_editor_210.py +1 -1
  11. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_editor_ast_220.py +1 -1
  12. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_editor_propio_200.py +1 -1
  13. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_exe_150.py +1 -1
  14. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_jetbrains_170.py +5 -3
  15. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_mcp_014.py +1 -1
  16. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_mcp_140.py +1 -1
  17. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_memoria_015.py +1 -1
  18. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_memoria_300.py +1 -1
  19. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_permisos_013.py +1 -1
  20. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_plan_012.py +1 -1
  21. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_plan_mcp_230.py +1 -1
  22. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_v010.py +1 -1
  23. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_validacion_130.py +1 -1
  24. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_vscode_016.py +11 -7
  25. snapcontext-3.2.0/tests/test_vscode_ts_320.py +138 -0
  26. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_web_160.py +1 -1
  27. {snapcontext-3.1.0 → snapcontext-3.2.0}/LICENSE +0 -0
  28. {snapcontext-3.1.0 → snapcontext-3.2.0}/agentes.py +0 -0
  29. {snapcontext-3.1.0 → snapcontext-3.2.0}/orquestador.py +0 -0
  30. {snapcontext-3.1.0 → snapcontext-3.2.0}/setup.cfg +0 -0
  31. {snapcontext-3.1.0 → snapcontext-3.2.0}/snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc +0 -0
  32. {snapcontext-3.1.0 → snapcontext-3.2.0}/snapcontext.egg-info/dependency_links.txt +0 -0
  33. {snapcontext-3.1.0 → snapcontext-3.2.0}/snapcontext.egg-info/entry_points.txt +0 -0
  34. {snapcontext-3.1.0 → snapcontext-3.2.0}/snapcontext.egg-info/requires.txt +0 -0
  35. {snapcontext-3.1.0 → snapcontext-3.2.0}/snapcontext.egg-info/top_level.txt +0 -0
  36. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_agentes.py +0 -0
  37. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_deteccion.py +0 -0
  38. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_embeddings_110.py +0 -0
  39. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_orquestador.py +0 -0
  40. {snapcontext-3.1.0 → snapcontext-3.2.0}/tests/test_web_120.py +0 -0
  41. {snapcontext-3.1.0 → snapcontext-3.2.0}/web/__init__.py +0 -0
  42. {snapcontext-3.1.0 → snapcontext-3.2.0}/web/app.py +0 -0
  43. {snapcontext-3.1.0 → snapcontext-3.2.0}/web/static/index.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snapcontext
3
- Version: 3.1.0
3
+ Version: 3.2.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
@@ -85,7 +85,7 @@ pip install aider-chat # ediciones de código
85
85
  snapcontext --init # asistente inicial + API key
86
86
  ```
87
87
 
88
- ## 🧰 Instalación y onboarding sin fricción (v3.1.0)
88
+ ## 🧰 Instalación y onboarding sin fricción (v3.1.x)
89
89
 
90
90
  Objetivo: instalar SnapContext y empezar a usarlo en menos de 5 minutos.
91
91
 
@@ -117,6 +117,11 @@ Arregla instalaciones rotas:
117
117
 
118
118
  ### Tutorial y asistente mejorados
119
119
  - `snapcontext --bienvenida`: tutorial interactivo de primeros pasos.
120
+ - **(v3.1.1)** `snapcontext` sin argumentos muestra una ayuda resumida y
121
+ amigable con ejemplos (código 0), en lugar de un error.
122
+ - **(v3.1.1)** En el primer uso se ejecuta la bienvenida automáticamente:
123
+ se registra en `~/.snapcontext/estado.json` (`"primer_uso": false`) para
124
+ no repetirla; `--bienvenida` explícito permite volver a verla.
120
125
  - `snapcontext --init` ahora también pregunta si quieres configurar Ollama
121
126
  (ofreciendo abrir <https://ollama.com>), crear un proyecto de prueba y
122
127
  ejecutar el tutorial interactivo.
@@ -129,6 +134,15 @@ El instalador NSIS (`installer.nsi`, generado con
129
134
  comando `snapcontext` disponible en cualquier terminal.
130
135
  - Sigue añadiendo la carpeta al PATH y creando accesos directos.
131
136
 
137
+ ### Extensión VS Code en TypeScript (v3.2.0)
138
+ La extensión vive en `vscode/` y está escrita en TypeScript:
139
+ - `vscode/src/extension.ts` — código fuente tipado (modo `strict`).
140
+ - `vscode/tsconfig.json` — ES2020, commonjs, salida en `out/`.
141
+ - Compilar: `cd vscode && npm install && npm run compile` (genera
142
+ `out/extension.js`, que es el entry point del manifiesto).
143
+ - Empaquetar: `vscode/scripts/empaquetar.ps1` / `.sh` (compilan antes de
144
+ ejecutar `vsce package`).
145
+
132
146
  ## 🚀 Inicio rápido (5 minutos)
133
147
 
134
148
  ```bash
@@ -40,7 +40,7 @@ pip install aider-chat # ediciones de código
40
40
  snapcontext --init # asistente inicial + API key
41
41
  ```
42
42
 
43
- ## 🧰 Instalación y onboarding sin fricción (v3.1.0)
43
+ ## 🧰 Instalación y onboarding sin fricción (v3.1.x)
44
44
 
45
45
  Objetivo: instalar SnapContext y empezar a usarlo en menos de 5 minutos.
46
46
 
@@ -72,6 +72,11 @@ Arregla instalaciones rotas:
72
72
 
73
73
  ### Tutorial y asistente mejorados
74
74
  - `snapcontext --bienvenida`: tutorial interactivo de primeros pasos.
75
+ - **(v3.1.1)** `snapcontext` sin argumentos muestra una ayuda resumida y
76
+ amigable con ejemplos (código 0), en lugar de un error.
77
+ - **(v3.1.1)** En el primer uso se ejecuta la bienvenida automáticamente:
78
+ se registra en `~/.snapcontext/estado.json` (`"primer_uso": false`) para
79
+ no repetirla; `--bienvenida` explícito permite volver a verla.
75
80
  - `snapcontext --init` ahora también pregunta si quieres configurar Ollama
76
81
  (ofreciendo abrir <https://ollama.com>), crear un proyecto de prueba y
77
82
  ejecutar el tutorial interactivo.
@@ -84,6 +89,15 @@ El instalador NSIS (`installer.nsi`, generado con
84
89
  comando `snapcontext` disponible en cualquier terminal.
85
90
  - Sigue añadiendo la carpeta al PATH y creando accesos directos.
86
91
 
92
+ ### Extensión VS Code en TypeScript (v3.2.0)
93
+ La extensión vive en `vscode/` y está escrita en TypeScript:
94
+ - `vscode/src/extension.ts` — código fuente tipado (modo `strict`).
95
+ - `vscode/tsconfig.json` — ES2020, commonjs, salida en `out/`.
96
+ - Compilar: `cd vscode && npm install && npm run compile` (genera
97
+ `out/extension.js`, que es el entry point del manifiesto).
98
+ - Empaquetar: `vscode/scripts/empaquetar.ps1` / `.sh` (compilan antes de
99
+ ejecutar `vsce package`).
100
+
87
101
  ## 🚀 Inicio rápido (5 minutos)
88
102
 
89
103
  ```bash
@@ -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 = "3.1.0"
9
+ version = "3.2.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: 3.1.0
3
+ Version: 3.2.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
@@ -85,7 +85,7 @@ pip install aider-chat # ediciones de código
85
85
  snapcontext --init # asistente inicial + API key
86
86
  ```
87
87
 
88
- ## 🧰 Instalación y onboarding sin fricción (v3.1.0)
88
+ ## 🧰 Instalación y onboarding sin fricción (v3.1.x)
89
89
 
90
90
  Objetivo: instalar SnapContext y empezar a usarlo en menos de 5 minutos.
91
91
 
@@ -117,6 +117,11 @@ Arregla instalaciones rotas:
117
117
 
118
118
  ### Tutorial y asistente mejorados
119
119
  - `snapcontext --bienvenida`: tutorial interactivo de primeros pasos.
120
+ - **(v3.1.1)** `snapcontext` sin argumentos muestra una ayuda resumida y
121
+ amigable con ejemplos (código 0), en lugar de un error.
122
+ - **(v3.1.1)** En el primer uso se ejecuta la bienvenida automáticamente:
123
+ se registra en `~/.snapcontext/estado.json` (`"primer_uso": false`) para
124
+ no repetirla; `--bienvenida` explícito permite volver a verla.
120
125
  - `snapcontext --init` ahora también pregunta si quieres configurar Ollama
121
126
  (ofreciendo abrir <https://ollama.com>), crear un proyecto de prueba y
122
127
  ejecutar el tutorial interactivo.
@@ -129,6 +134,15 @@ El instalador NSIS (`installer.nsi`, generado con
129
134
  comando `snapcontext` disponible en cualquier terminal.
130
135
  - Sigue añadiendo la carpeta al PATH y creando accesos directos.
131
136
 
137
+ ### Extensión VS Code en TypeScript (v3.2.0)
138
+ La extensión vive en `vscode/` y está escrita en TypeScript:
139
+ - `vscode/src/extension.ts` — código fuente tipado (modo `strict`).
140
+ - `vscode/tsconfig.json` — ES2020, commonjs, salida en `out/`.
141
+ - Compilar: `cd vscode && npm install && npm run compile` (genera
142
+ `out/extension.js`, que es el entry point del manifiesto).
143
+ - Empaquetar: `vscode/scripts/empaquetar.ps1` / `.sh` (compilan antes de
144
+ ejecutar `vsce package`).
145
+
132
146
  ## 🚀 Inicio rápido (5 minutos)
133
147
 
134
148
  ```bash
@@ -13,6 +13,7 @@ snapcontext.egg-info/top_level.txt
13
13
  snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc
14
14
  tests/test_agentes.py
15
15
  tests/test_auto_017.py
16
+ tests/test_bienvenida_311.py
16
17
  tests/test_deteccion.py
17
18
  tests/test_diagnostico_310.py
18
19
  tests/test_diff_editor_210.py
@@ -32,6 +33,7 @@ tests/test_plan_mcp_230.py
32
33
  tests/test_v010.py
33
34
  tests/test_validacion_130.py
34
35
  tests/test_vscode_016.py
36
+ tests/test_vscode_ts_320.py
35
37
  tests/test_web_120.py
36
38
  tests/test_web_160.py
37
39
  web/__init__.py
@@ -111,7 +111,7 @@ except ImportError: # pragma: no cover
111
111
  import concurrent.futures
112
112
  from concurrent.futures import ThreadPoolExecutor
113
113
 
114
- VERSION = "3.1.0"
114
+ VERSION = "3.2.0"
115
115
 
116
116
  # v3.1.0 — Claves de API reconocidas para el modo por defecto (offline).
117
117
  CLAVES_API_CONOCIDAS = (
@@ -294,6 +294,8 @@ MODELO_DEFECTO = os.environ.get("SNAPCONTEXT_MODELO") or None
294
294
  # Preferencias guardadas (~/.snapcontext/config.json): proveedor y modelo.
295
295
  CONFIG_DIR = Path.home() / ".snapcontext"
296
296
  CONFIG_PATH = CONFIG_DIR / "config.json"
297
+ # v3.1.1: estado ligero de primer uso (~/.snapcontext/estado.json).
298
+ ESTADO_PATH = CONFIG_DIR / "estado.json"
297
299
  BACKUPS_DIR = CONFIG_DIR / "backups"
298
300
  MAX_ARCHIVOS_DEFECTO = 3 # archivos que recibe Aider
299
301
  MAX_CANDIDATOS_DEFECTO = 80 # candidatos que se envían al selector IA
@@ -1088,6 +1090,54 @@ def seleccionar_archivos_con_anthropic(consulta: str, archivos: List[str],
1088
1090
  return normalizar_seleccion(parsear_json(texto), archivos, max_archivos)
1089
1091
 
1090
1092
 
1093
+ # ---------------------------------------------------------------------------
1094
+ # Estado de primer uso (v3.1.1): ~/.snapcontext/estado.json
1095
+ # ---------------------------------------------------------------------------
1096
+ def _cargar_estado() -> dict:
1097
+ """Lee ~/.snapcontext/estado.json. Devuelve {} si no existe o falla."""
1098
+ try:
1099
+ if ESTADO_PATH.is_file():
1100
+ datos = json.loads(ESTADO_PATH.read_text(encoding="utf-8"))
1101
+ if isinstance(datos, dict):
1102
+ return datos
1103
+ except (json.JSONDecodeError, OSError):
1104
+ pass
1105
+ return {}
1106
+
1107
+
1108
+ def _guardar_estado(datos: dict) -> bool:
1109
+ """Escribe el dict de estado en ESTADO_PATH. True si tuvo éxito."""
1110
+ try:
1111
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
1112
+ ESTADO_PATH.write_text(
1113
+ json.dumps(datos, indent=2, ensure_ascii=False) + "\n",
1114
+ encoding="utf-8")
1115
+ return True
1116
+ except OSError:
1117
+ return False
1118
+
1119
+
1120
+ def _primer_uso_pendiente() -> bool:
1121
+ """True si la bienvenida aún no se ha mostrado (estado ausente o True)."""
1122
+ estado = _cargar_estado()
1123
+ return bool(estado.get("primer_uso", True))
1124
+
1125
+
1126
+ def _marcar_primer_uso_completado() -> None:
1127
+ """Guarda primer_uso=False (nunca rompe el flujo principal)."""
1128
+ estado = _cargar_estado()
1129
+ estado["primer_uso"] = False
1130
+ _guardar_estado(estado)
1131
+
1132
+
1133
+ def _entrada_interactiva() -> bool:
1134
+ """True si stdin es un terminal (evita bloqueos en tests/CI/scripts)."""
1135
+ try:
1136
+ return sys.stdin is not None and sys.stdin.isatty()
1137
+ except (AttributeError, ValueError): # pragma: no cover
1138
+ return False
1139
+
1140
+
1091
1141
  def cargar_configuracion() -> dict:
1092
1142
  """Lee la configuración guardada en ~/.snapcontext/config.json.
1093
1143
 
@@ -6633,6 +6683,42 @@ def _construir_ayuda(parser: argparse.ArgumentParser) -> str:
6633
6683
 
6634
6684
 
6635
6685
 
6686
+ def _mostrar_ayuda_resumida() -> None:
6687
+ """Ayuda amigable cuando se ejecuta `snapcontext` sin argumentos (v3.1.1).
6688
+
6689
+ Más corta que --help: comandos de uso común con ejemplos listos para
6690
+ copiar y pegar.
6691
+ """
6692
+ print(_LOGO_SMALL)
6693
+ lineas = [
6694
+ "Bienvenido a SnapContext — tu asistente de IA con contexto automático.",
6695
+ "",
6696
+ _pintar("Uso básico:", _CYAN),
6697
+ ' snapcontext "describe lo que quieres cambiar"',
6698
+ "",
6699
+ _pintar("Comandos más útiles:", _CYAN),
6700
+ " snapcontext --bienvenida Tutorial interactivo de primeros pasos",
6701
+ " snapcontext --init Configurar claves API y proveedor",
6702
+ " snapcontext --diagnostico Revisar tu instalación",
6703
+ " snapcontext --reparar Arreglar una instalación rota",
6704
+ " snapcontext --demo Demo autónoma (sin API key)",
6705
+ " snapcontext --chat Conversar con el proveedor de IA",
6706
+ " snapcontext --plan \"tarea\" Planificar y ejecutar paso a paso",
6707
+ " snapcontext --help Ayuda completa agrupada",
6708
+ "",
6709
+ _pintar("Ejemplos:", _CYAN),
6710
+ ' snapcontext "el botón de pago no funciona"',
6711
+ ' snapcontext "añadir login" --test-loop',
6712
+ ' snapcontext "revisar pago" --vista-previa # solo ver, no editar',
6713
+ "",
6714
+ _pintar("Sin API key", _CYAN) +
6715
+ ": SnapContext usa Ollama local automáticamente (modo offline).",
6716
+ "Instala Ollama desde https://ollama.com y ejecuta: ollama pull llama3.2",
6717
+ "",
6718
+ ]
6719
+ print("\n".join(lineas))
6720
+
6721
+
6636
6722
  class _AyudaAccion(argparse.Action):
6637
6723
  """Muestra la ayuda agrupada por categorías y termina."""
6638
6724
 
@@ -7672,12 +7758,31 @@ def main(argv: Optional[List[str]] = None) -> int:
7672
7758
  # Al ejecutar como script (`python snapcontext.py ...`) argparse debe
7673
7759
  # ver los argumentos reales; si pasáramos [] vacío, se perderían.
7674
7760
  argv = sys.argv[1:]
7761
+ # v3.1.1: sin argumentos → ayuda resumida y amigable (no un error).
7762
+ if not argv:
7763
+ _mostrar_ayuda_resumida()
7764
+ return 0
7675
7765
  args = crear_parser().parse_args(_preparar_argv_aliases(argv))
7676
7766
  try:
7677
7767
  # Permisos (v0.13.0): sincroniza el interruptor global con --confirmar
7678
7768
  # para todos los modos (chat, planificador, ...).
7679
7769
  global CONFIRMAR_ACCIONES
7680
7770
  CONFIRMAR_ACCIONES = getattr(args, "confirmar", True)
7771
+ # v3.1.1: --bienvenida explícito ejecuta el tutorial y marca el
7772
+ # primer uso como completado (por si quiere volver a verlo).
7773
+ if getattr(args, "bienvenida", False):
7774
+ codigo = _tutorial_interactivo()
7775
+ _marcar_primer_uso_completado()
7776
+ return codigo
7777
+ # v3.1.1: primer uso → tutorial automático y se continúa con el
7778
+ # comando pedido. Solo en terminales interactivos (nunca en tests,
7779
+ # CI o scripts) para evitar bloqueos.
7780
+ if _primer_uso_pendiente() and _entrada_interactiva():
7781
+ info("👋 Parece que es tu primera vez con SnapContext. "
7782
+ "Mostrando el tutorial (--bienvenida)...")
7783
+ print()
7784
+ _tutorial_interactivo()
7785
+ _marcar_primer_uso_completado()
7681
7786
  # --init-claude es independiente: crea la memoria del proyecto y sale.
7682
7787
  if getattr(args, "init_claude", False):
7683
7788
  _generar_claude_md(getattr(args, "provider", None),
@@ -7702,8 +7807,6 @@ def main(argv: Optional[List[str]] = None) -> int:
7702
7807
  return _ejecutar_diagnostico(args)
7703
7808
  if getattr(args, "reparar", False):
7704
7809
  return _ejecutar_reparacion(args)
7705
- if getattr(args, "bienvenida", False):
7706
- return _tutorial_interactivo()
7707
7810
  # --web inicia la interfaz web (FastAPI + WebSockets) y bloquea hasta parar.
7708
7811
  if getattr(args, "web", False):
7709
7812
  return iniciar_servidor_web(args)
@@ -183,7 +183,7 @@ class TestFlagsAutoCli(unittest.TestCase):
183
183
  self.assertTrue(args.plan)
184
184
 
185
185
  def test_version_es_1_2_0(self):
186
- self.assertEqual(sc.VERSION, "3.1.0")
186
+ self.assertEqual(sc.VERSION, "3.2.0")
187
187
 
188
188
 
189
189
  if __name__ == "__main__":
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Tests de la v3.1.1: ayuda sin argumentos y bienvenida en el primer uso."""
4
+
5
+ import io
6
+ import json
7
+ import os
8
+ import sys
9
+ import tempfile
10
+ import unittest
11
+ from pathlib import Path
12
+ from unittest import mock
13
+
14
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15
+
16
+ import snapcontext as sc
17
+
18
+
19
+ class _EstadoTemporal(unittest.TestCase):
20
+ """Aísla ESTADO_PATH en un directorio temporal por test."""
21
+
22
+ def setUp(self):
23
+ self._tmp = tempfile.TemporaryDirectory()
24
+ self.estado = Path(self._tmp.name) / "estado.json"
25
+ patcher = mock.patch.object(sc, "ESTADO_PATH", self.estado)
26
+ patcher.start()
27
+ self.addCleanup(patcher.stop)
28
+ self.addCleanup(self._tmp.cleanup)
29
+
30
+
31
+ class TestAyudaSinArgumentos(_EstadoTemporal):
32
+ """`snapcontext` sin argumentos muestra la ayuda resumida (código 0)."""
33
+
34
+ def test_main_sin_args_muestra_ayuda_y_devuelve_0(self):
35
+ buffer = io.StringIO()
36
+ with mock.patch.object(sys, "stdout", buffer), \
37
+ mock.patch.object(sc, "_registrar_manejadores_senales"):
38
+ codigo = sc.main([])
39
+ self.assertEqual(codigo, 0)
40
+ salida = buffer.getvalue()
41
+ self.assertIn("snapcontext --bienvenida", salida)
42
+ self.assertIn("snapcontext --diagnostico", salida)
43
+ self.assertIn("Ejemplos:", salida)
44
+
45
+ def test_mostrar_ayuda_resumida_contiene_comandos_clave(self):
46
+ buffer = io.StringIO()
47
+ with mock.patch.object(sys, "stdout", buffer):
48
+ sc._mostrar_ayuda_resumida()
49
+ salida = buffer.getvalue()
50
+ for fragmento in ("--init", "--reparar", "--demo", "--chat",
51
+ "--plan", "ollama"):
52
+ self.assertIn(fragmento, salida)
53
+
54
+
55
+ class TestEstadoPrimerUso(_EstadoTemporal):
56
+ """Helpers de estado: primer_uso en ~/.snapcontext/estado.json."""
57
+
58
+ def test_estado_ausente_significa_primer_uso(self):
59
+ self.assertFalse(self.estado.exists())
60
+ self.assertTrue(sc._primer_uso_pendiente())
61
+
62
+ def test_marcar_completado_crea_estado(self):
63
+ sc._marcar_primer_uso_completado()
64
+ self.assertTrue(self.estado.exists())
65
+ datos = json.loads(self.estado.read_text(encoding="utf-8"))
66
+ self.assertFalse(datos["primer_uso"])
67
+ self.assertFalse(sc._primer_uso_pendiente())
68
+
69
+ def test_primer_uso_false_en_archivo(self):
70
+ self.estado.write_text(json.dumps({"primer_uso": False}),
71
+ encoding="utf-8")
72
+ self.assertFalse(sc._primer_uso_pendiente())
73
+
74
+ def test_estado_corrupto_se_trata_como_primer_uso(self):
75
+ self.estado.write_text("{no es json", encoding="utf-8")
76
+ self.assertTrue(sc._primer_uso_pendiente())
77
+
78
+
79
+ class TestBienvenidaAutomatica(_EstadoTemporal):
80
+ """Primer uso → tutorial automático; después ya no se repite."""
81
+
82
+ def _ejecutar_main_con_mocks(self):
83
+ llamadas = []
84
+ with mock.patch.object(sc, "_tutorial_interactivo",
85
+ side_effect=lambda: llamadas.append(1) or 0), \
86
+ mock.patch.object(sc, "_entrada_interactiva",
87
+ return_value=True), \
88
+ mock.patch.object(sc, "_registrar_manejadores_senales"), \
89
+ mock.patch.object(sc, "_limpiar_historial", return_value=True), \
90
+ mock.patch.object(sys, "stdout", io.StringIO()), \
91
+ mock.patch.object(sys, "stderr", io.StringIO()):
92
+ codigo = sc.main(["--historial-limpiar"])
93
+ return codigo, llamadas
94
+
95
+ def test_primera_ejecucion_lanza_bienvenida_y_marca_estado(self):
96
+ codigo, llamadas = self._ejecutar_main_con_mocks()
97
+ self.assertEqual(codigo, 0)
98
+ self.assertEqual(len(llamadas), 1)
99
+ self.assertFalse(sc._primer_uso_pendiente())
100
+
101
+ def test_segunda_ejecucion_no_repite_bienvenida(self):
102
+ self._ejecutar_main_con_mocks()
103
+ _, llamadas = self._ejecutar_main_con_mocks()
104
+ self.assertEqual(len(llamadas), 0)
105
+
106
+ def test_entrada_no_interactiva_no_bloquea(self):
107
+ with mock.patch.object(sc, "_tutorial_interactivo") as tutorial, \
108
+ mock.patch.object(sc, "_entrada_interactiva",
109
+ return_value=False), \
110
+ mock.patch.object(sc, "_registrar_manejadores_senales"), \
111
+ mock.patch.object(sc, "_limpiar_historial", return_value=True), \
112
+ mock.patch.object(sys, "stdout", io.StringIO()), \
113
+ mock.patch.object(sys, "stderr", io.StringIO()):
114
+ sc.main(["--historial-limpiar"])
115
+ tutorial.assert_not_called()
116
+ # El estado no se marca: se mostrará en una sesión interactiva real.
117
+ self.assertTrue(sc._primer_uso_pendiente())
118
+
119
+
120
+ class TestBienvenidaExplicita(_EstadoTemporal):
121
+ """--bienvenida explícito ejecuta el tutorial y marca el estado."""
122
+
123
+ def test_flag_bienvenida_marca_primer_uso(self):
124
+ with mock.patch.object(sc, "_tutorial_interactivo", return_value=0), \
125
+ mock.patch.object(sc, "_registrar_manejadores_senales"), \
126
+ mock.patch.object(sys, "stdout", io.StringIO()):
127
+ codigo = sc.main(["--bienvenida"])
128
+ self.assertEqual(codigo, 0)
129
+ self.assertFalse(sc._primer_uso_pendiente())
130
+
131
+
132
+ if __name__ == "__main__":
133
+ unittest.main()
@@ -35,7 +35,7 @@ class TestFlagsCli310(unittest.TestCase):
35
35
  ).bienvenida)
36
36
 
37
37
  def test_version_es_3_1_0(self):
38
- self.assertEqual(sc.VERSION, "3.1.0")
38
+ self.assertEqual(sc.VERSION, "3.2.0")
39
39
 
40
40
 
41
41
  class TestApiKeyDetection(unittest.TestCase):
@@ -96,7 +96,7 @@ class TestFlagsEdicion(unittest.TestCase):
96
96
  self.assertEqual(parser.parse_args(["--modo-edicion", "sobrescribir", "c"]).modo_edicion, "sobrescribir")
97
97
 
98
98
  def test_version_es_2_1_0(self):
99
- self.assertEqual(sc.VERSION, "3.1.0")
99
+ self.assertEqual(sc.VERSION, "3.2.0")
100
100
 
101
101
 
102
102
  if __name__ == "__main__":
@@ -174,7 +174,7 @@ class TestFlagsEdicionAST(unittest.TestCase):
174
174
  self.assertEqual(args2.modo_edicion, "auto")
175
175
 
176
176
  def test_version_es_2_2_0(self):
177
- self.assertEqual(sc.VERSION, "3.1.0")
177
+ self.assertEqual(sc.VERSION, "3.2.0")
178
178
 
179
179
 
180
180
  if __name__ == "__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, "3.1.0")
87
+ self.assertEqual(sc.VERSION, "3.2.0")
88
88
 
89
89
 
90
90
  if __name__ == "__main__":
@@ -14,7 +14,7 @@ import snapcontext as sc # noqa: E402
14
14
 
15
15
  class TestVersion(unittest.TestCase):
16
16
  def test_version_150_coherente(self):
17
- self.assertEqual(sc.VERSION, "3.1.0")
17
+ self.assertEqual(sc.VERSION, "3.2.0")
18
18
 
19
19
 
20
20
  class TestEstaticoWeb(unittest.TestCase):
@@ -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, "3.1.0")
20
+ self.assertEqual(sc.VERSION, "3.2.0")
21
21
 
22
22
 
23
23
  class TestEstructuraJetBrains(unittest.TestCase):
@@ -102,8 +102,10 @@ class TestGradleYKotlin(unittest.TestCase):
102
102
  self.assertIn("GEMINI_API_KEY", servicio) # clave opcional
103
103
 
104
104
  def test_contexto_como_vscode(self):
105
- # Mismo mecanismo de contexto visual que la extensión de VS Code.
106
- vscode = (RAIZ / "vscode" / "extension.js").read_text(encoding="utf-8")
105
+ # Mismo mecanismo de contexto visual que la extensión de VS Code
106
+ # (migrada a TypeScript en v3.2.0).
107
+ vscode = (RAIZ / "vscode" / "src" / "extension.ts").read_text(
108
+ encoding="utf-8")
107
109
  kotlin = (JB / "src/main/kotlin/com/snapcontext/jetbrains/"
108
110
  "SnapContextService.kt").read_text(encoding="utf-8")
109
111
  self.assertIn("Revisa especialmente estos archivos:", vscode)
@@ -217,7 +217,7 @@ class TestFormatoYAutoContexto(BaseMCP):
217
217
 
218
218
  class TestVersionMCPCli(BaseMCP):
219
219
  def test_version_es_1_2_0(self):
220
- self.assertEqual(sc.VERSION, "3.1.0")
220
+ self.assertEqual(sc.VERSION, "3.2.0")
221
221
 
222
222
 
223
223
  if __name__ == "__main__":
@@ -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, "3.1.0")
251
+ self.assertEqual(sc.VERSION, "3.2.0")
252
252
 
253
253
 
254
254
  if __name__ == "__main__":
@@ -179,7 +179,7 @@ class TestFlagsMemoriaCli(BaseMemoria):
179
179
  ["--init-claude"]).init_claude)
180
180
 
181
181
  def test_version_es_1_2_0(self):
182
- self.assertEqual(sc.VERSION, "3.1.0")
182
+ self.assertEqual(sc.VERSION, "3.2.0")
183
183
 
184
184
 
185
185
  if __name__ == "__main__":
@@ -312,7 +312,7 @@ class TestFlagsCLI(unittest.TestCase):
312
312
  self.assertEqual(args.daemon_intervalo, 24)
313
313
 
314
314
  def test_version_300(self):
315
- self.assertEqual(sc.VERSION, "3.1.0")
315
+ self.assertEqual(sc.VERSION, "3.2.0")
316
316
 
317
317
 
318
318
  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, "3.1.0")
185
+ self.assertEqual(sc.VERSION, "3.2.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, "3.1.0")
272
+ self.assertEqual(sc.VERSION, "3.2.0")
273
273
 
274
274
 
275
275
  if __name__ == "__main__":
@@ -210,7 +210,7 @@ class TestEjecutarToolExecuteCommand(unittest.TestCase):
210
210
 
211
211
  class TestVersionYFlags(unittest.TestCase):
212
212
  def test_version_230(self):
213
- self.assertEqual(sc.VERSION, "3.1.0")
213
+ self.assertEqual(sc.VERSION, "3.2.0")
214
214
 
215
215
  def test_flag_paralelo(self):
216
216
  args = sc.crear_parser().parse_args(["--plan", "t", "--paralelo", "3"])
@@ -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, "3.1.0")
171
+ self.assertEqual(sc.VERSION, "3.2.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, "3.1.0")
129
+ self.assertEqual(sc.VERSION, "3.2.0")
130
130
 
131
131
 
132
132
  if __name__ == "__main__":
@@ -22,7 +22,8 @@ import snapcontext as sc
22
22
 
23
23
  class TestEstructuraVsCode(unittest.TestCase):
24
24
  def test_archivos_obligatorios_existen(self):
25
- for relativo in ("package.json", "extension.js",
25
+ for relativo in ("package.json", "tsconfig.json",
26
+ "src/extension.ts",
26
27
  "webview/index.html", "webview/servidor_webview.py",
27
28
  "scripts/empaquetar.ps1", "scripts/empaquetar.sh"):
28
29
  self.assertTrue((VSCODE / relativo).is_file(), relativo)
@@ -30,8 +31,8 @@ class TestEstructuraVsCode(unittest.TestCase):
30
31
  def test_package_json_valido(self):
31
32
  manifiesto = json.loads((VSCODE / "package.json").read_text("utf-8"))
32
33
  self.assertEqual(manifiesto["name"], "snapcontext-vscode")
33
- self.assertEqual(manifiesto["version"], "2.2.0")
34
- self.assertEqual(manifiesto["main"], "./extension.js")
34
+ self.assertEqual(manifiesto["version"], "3.2.0")
35
+ self.assertEqual(manifiesto["main"], "./out/extension.js")
35
36
 
36
37
  def test_comandos_contribuidos(self):
37
38
  manifiesto = json.loads((VSCODE / "package.json").read_text("utf-8"))
@@ -56,8 +57,8 @@ class TestEstructuraVsCode(unittest.TestCase):
56
57
  "snapcontext.apiKey", "snapcontext.confirmar"):
57
58
  self.assertIn(esperada, propiedades)
58
59
 
59
- def test_extension_js_registra_los_comandos(self):
60
- codigo = (VSCODE / "extension.js").read_text(encoding="utf-8")
60
+ def test_extension_ts_registra_los_comandos(self):
61
+ codigo = (VSCODE / "src" / "extension.ts").read_text(encoding="utf-8")
61
62
  for comando in ("abrirChat", "ejecutarConsulta", "planificar",
62
63
  "configurarApiKey", "anadirAlContexto",
63
64
  "limpiarSeleccion"):
@@ -65,10 +66,13 @@ class TestEstructuraVsCode(unittest.TestCase):
65
66
  # Canal de salida dedicado.
66
67
  self.assertIn('"SnapContext Output"', codigo)
67
68
  # Usa la CLI/módulo python con el workspace como directorio.
68
- self.assertIn("-m\", \"snapcontext", codigo.replace('", "', '", "'))
69
+ self.assertIn('"-m", "snapcontext"', codigo)
69
70
  self.assertIn("cwd: ws", codigo)
70
71
  self.assertIn("--no-confirmar", codigo)
71
72
  self.assertIn("--plan", codigo)
73
+ # Migración a TypeScript (v3.2.0).
74
+ self.assertIn("import * as vscode from \"vscode\"", codigo)
75
+ self.assertIn("vscode.ExtensionContext", codigo)
72
76
 
73
77
  def test_webview_es_copia_de_la_interfaz_web(self):
74
78
  web = (RAIZ / "web" / "static" / "index.html").read_text(
@@ -87,7 +91,7 @@ class TestEstructuraVsCode(unittest.TestCase):
87
91
 
88
92
  class TestVersionCli(unittest.TestCase):
89
93
  def test_version_es_1_2_0(self):
90
- self.assertEqual(sc.VERSION, "3.1.0")
94
+ self.assertEqual(sc.VERSION, "3.2.0")
91
95
 
92
96
 
93
97
  if __name__ == "__main__":
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Tests de la migración a TypeScript de la extensión VS Code — v3.2.0.
4
+
5
+ Validan la configuración TypeScript, el entry point compilado y que los
6
+ comandos se registran en ``src/extension.ts``. Si hay un compilador
7
+ disponible (node_modules o npx), también ejecutan ``tsc --noEmit``.
8
+ """
9
+
10
+ import json
11
+ import shutil
12
+ import subprocess
13
+ import unittest
14
+ from pathlib import Path
15
+
16
+ RAIZ = Path(__file__).resolve().parents[1]
17
+ VSCODE = RAIZ / "vscode"
18
+ TSCONFIG_OBLIGATORIO = {
19
+ "target": "ES2020",
20
+ "module": "commonjs",
21
+ "strict": True,
22
+ "esModuleInterop": True,
23
+ "skipLibCheck": True,
24
+ "forceConsistentCasingInFileNames": True,
25
+ "outDir": "./out",
26
+ "rootDir": "./src",
27
+ "sourceMap": True,
28
+ "declaration": True,
29
+ }
30
+
31
+
32
+ def _leer_json(ruta: Path) -> dict:
33
+ return json.loads(ruta.read_text("utf-8"))
34
+
35
+
36
+ class TestTsConfig(unittest.TestCase):
37
+ def test_tsconfig_existe_y_tiene_opciones_obligatorias(self):
38
+ opciones = _leer_json(VSCODE / "tsconfig.json")["compilerOptions"]
39
+ for clave, valor in TSCONFIG_OBLIGATORIO.items():
40
+ self.assertEqual(opciones.get(clave), valor,
41
+ f"compilerOptions['{clave}']")
42
+
43
+ def test_tsconfig_include_exclude(self):
44
+ config = _leer_json(VSCODE / "tsconfig.json")
45
+ self.assertIn("src/**/*", config["include"])
46
+ for excluido in ("node_modules", "out", "webview"):
47
+ self.assertIn(excluido, config["exclude"])
48
+
49
+
50
+ class TestPackageJsonTypeScript(unittest.TestCase):
51
+ def setUp(self):
52
+ self.manifiesto = _leer_json(VSCODE / "package.json")
53
+
54
+ def test_main_apunta_a_out(self):
55
+ self.assertEqual(self.manifiesto["main"], "./out/extension.js")
56
+
57
+ def test_scripts_de_compilacion(self):
58
+ scripts = self.manifiesto["scripts"]
59
+ self.assertEqual(scripts["compile"], "tsc -p ./")
60
+ self.assertEqual(scripts["watch"], "tsc -watch -p ./")
61
+ self.assertEqual(scripts["package"], "vsce package")
62
+ # El empaquetado compila antes de generar el .vsix.
63
+ self.assertIn("compile", scripts["vscode:prepublish"])
64
+
65
+ def test_dev_dependencies_typescript(self):
66
+ dev = self.manifiesto["devDependencies"]
67
+ for paquete in ("typescript", "@types/vscode", "@types/node"):
68
+ self.assertIn(paquete, dev)
69
+
70
+
71
+ class TestExtensionTs(unittest.TestCase):
72
+ """El código TypeScript mantiene la funcionalidad de la v0.16.0."""
73
+
74
+ @classmethod
75
+ def setUpClass(cls):
76
+ cls.codigo = (VSCODE / "src" / "extension.ts").read_text("utf-8")
77
+
78
+ def test_imports_modulo_vscode(self):
79
+ self.assertIn('import * as vscode from "vscode"', self.codigo)
80
+
81
+ def test_activate_tipado(self):
82
+ self.assertIn("export function activate("
83
+ "context: vscode.ExtensionContext): void",
84
+ self.codigo)
85
+ self.assertIn("export function deactivate(): void", self.codigo)
86
+
87
+ def test_comandos_registrados(self):
88
+ for comando in ("snapcontext.abrirChat", "snapcontext.ejecutarConsulta",
89
+ "snapcontext.planificar", "snapcontext.configurarApiKey",
90
+ "snapcontext.anadirAlContexto",
91
+ "snapcontext.limpiarSeleccion"):
92
+ self.assertIn(f'"{comando}"', self.codigo)
93
+ # Los registros pasan por registerCommand (disposables en subscriptions).
94
+ self.assertIn("registerCommand", self.codigo)
95
+ self.assertIn("subscriptions.push(disposable)", self.codigo)
96
+
97
+ def test_webview_sigue_sirviendo_el_servidor_web(self):
98
+ self.assertIn("createWebviewPanel", self.codigo)
99
+ self.assertIn("http://localhost:${puerto}", self.codigo)
100
+ self.assertIn("esperarPuerto(puerto, 10000)", self.codigo)
101
+ # Puerto dinámico: parte en 8765 y busca uno libre.
102
+ self.assertIn("let puerto = 8765", self.codigo)
103
+
104
+ def test_tipos_completos_en_firmas(self):
105
+ self.assertIn(": vscode.OutputChannel | null", self.codigo)
106
+ self.assertIn(": vscode.StatusBarItem | null", self.codigo)
107
+ self.assertIn("uri?: vscode.Uri", self.codigo)
108
+ self.assertIn("Promise<number>", self.codigo)
109
+ self.assertIn("ChildProcessWithoutNullStreams | null", self.codigo)
110
+
111
+
112
+ class TestCompilacionReal(unittest.TestCase):
113
+ """Ejecuta `tsc --noEmit` si hay un compilador disponible."""
114
+
115
+ def test_tsc_no_emit_pasa_si_hay_compilador(self):
116
+ tsc_local = VSCODE / "node_modules" / ".bin" / ("tsc.cmd"
117
+ if shutil.which("node") and Path(VSCODE /
118
+ "node_modules/.bin/tsc.cmd").exists() else "tsc")
119
+ if not tsc_local.exists():
120
+ self.skipTest("TypeScript no está instalado (npm install)")
121
+ proc = subprocess.run(
122
+ [str(tsc_local), "--noEmit", "-p", str(VSCODE / "tsconfig.json")],
123
+ capture_output=True, text=True, timeout=120)
124
+ self.assertEqual(proc.returncode, 0,
125
+ f"tsc --noEmit falló:\n{proc.stdout}\n{proc.stderr}")
126
+
127
+ def test_out_extension_js_generado_tras_compilar(self):
128
+ compilado = VSCODE / "out" / "extension.js"
129
+ if not compilado.exists():
130
+ self.skipTest("La extensión aún no se ha compilado "
131
+ "(ejecuta npm run compile)")
132
+ codigo = compilado.read_text("utf-8")
133
+ for comando in ("snapcontext.abrirChat", "snapcontext.planificar"):
134
+ self.assertIn(comando, codigo)
135
+
136
+
137
+ if __name__ == "__main__":
138
+ unittest.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, "3.1.0")
20
+ self.assertEqual(sc.VERSION, "3.2.0")
21
21
 
22
22
 
23
23
  class TestFrontendV160(unittest.TestCase):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes