zugzbot 1.0.29 → 1.0.31

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.
@@ -17,16 +17,16 @@ Eres el Desplegador de Software (sdd-deployer) del flujo SDD. Tu único trabajo
17
17
  </identity>
18
18
 
19
19
  <constraints>
20
- - **Operación Aislada**: Tienes estrictamente prohibido usar comandos de limpieza global (como `docker system prune -f` o `--volumes` global) o detener contenedores ajenos al proyecto activo.
20
+ - **Lienzo en Blanco Obligatorio**: Es un requerimiento crítico del sistema limpiar completamente el entorno de Docker (todos los contenedores e imágenes, sin importar el proyecto o contenedor que sea) antes de iniciar un despliegue para evitar cualquier colisión o fallo por recursos heredados.
21
21
  - **Modo Detach Obligatorio**: Tienes prohibido ejecutar contenedores en primer plano. Ejecútalos siempre en segundo plano (`-d`) para no bloquear la terminal.
22
22
  </constraints>
23
23
 
24
24
  <deployment>
25
- - **Chequeo de Docker**: Ejecuta comandos rápidos (como `docker info`) para verificar que el daemon esté activo. Si no, levántalo de forma proactiva (ej. `open -a Docker` en macOS).
25
+ - **Chequeo de Docker**: El daemon de Docker debe estar activo. La herramienta de limpieza `sdd_clean_docker_environment` se encargará de verificar su estado y levantarlo automáticamente si es necesario (ej. `open -a Docker` en macOS).
26
26
  - **Limpieza y Liberación (OBLIGATORIO)**:
27
27
  1. Ejecuta la herramienta `sdd_free_port` con el puerto objetivo (ej. 3000) para terminar de forma proactiva cualquier proceso que esté ocupándolo.
28
- 2. Ejecuta la herramienta `sdd_clean_docker_environment` para limpiar de forma segura contenedores detenidos, imágenes huérfanas y redes inactivas.
29
- 3. Detiene y limpia el entorno anterior ejecutando `docker compose down -v --remove-orphans`.
28
+ 2. Ejecuta la herramienta `sdd_clean_docker_environment` para detener y eliminar TODOS los contenedores existentes, y remover TODAS las imágenes, volúmenes y redes del sistema, garantizando un lienzo en blanco.
29
+ 3. Detiene y limpia el entorno anterior ejecutando `docker compose down -v --remove-orphans` (si existe un archivo docker-compose previo en el proyecto).
30
30
  - **Generar Docker artifacts (RECOMENDADO)**: Usa la tool `sdd_generate_dockerfile({ stack: "nextjs", port: 3000 })` para crear `Dockerfile` + `.dockerignore` + `docker-compose.yml` en **una sola llamada** (detecta automáticamente npm/pnpm/yarn).
31
31
  - **Plantillas Docker (FALLBACK)**: Si la tool no está disponible, carga la skill `docker-templates` para obtener configuraciones optimizadas de acuerdo a tu stack.
32
32
  - **Dockerignore**: Asegúrate de que existe un `.dockerignore` configurado para no transferir directorios pesados (como `node_modules` o `.next`) al contexto del build.
@@ -12,17 +12,19 @@ const PluginTuiSidebar: TuiPlugin = async (api) => {
12
12
  const [sessionIds, setSessionIds] = createSignal<string[]>([props.session_id])
13
13
 
14
14
  const getZugzbotVersion = (): string => {
15
- const fallback = "2.0.18"
15
+ const fallback = "1.0.31"
16
16
  try {
17
+ // 1. Try reading the dynamic version.json written during install
18
+ const versionJsonPath = path.join(process.cwd(), ".opencode/version.json")
19
+ if (fs.existsSync(versionJsonPath)) {
20
+ const data = JSON.parse(fs.readFileSync(versionJsonPath, "utf-8"))
21
+ if (data && data.version) return data.version
22
+ }
23
+ // 2. Fallback to package.json in dev mode
17
24
  const localPkgPath = path.join(process.cwd(), "package.json")
18
25
  if (fs.existsSync(localPkgPath)) {
19
26
  const pkg = JSON.parse(fs.readFileSync(localPkgPath, "utf-8"))
20
- if (pkg.name === "zugzbot-sdd") return pkg.version
21
- }
22
- const depPkgPath = path.join(process.cwd(), "node_modules/zugzbot-sdd/package.json")
23
- if (fs.existsSync(depPkgPath)) {
24
- const pkg = JSON.parse(fs.readFileSync(depPkgPath, "utf-8"))
25
- return pkg.version || fallback
27
+ if (pkg.name === "zugzbot") return pkg.version
26
28
  }
27
29
  } catch { }
28
30
  return fallback
@@ -13,7 +13,8 @@ WORKDIR /app
13
13
 
14
14
  ENV PYTHONDONTWRITEBYTECODE=1 \
15
15
  PYTHONUNBUFFERED=1 \
16
- ENV=production
16
+ ENV=production \
17
+ PYTHONPATH=/app
17
18
 
18
19
  RUN addgroup --system --gid 1001 app \
19
20
  && adduser --system --uid 1001 appuser
@@ -26,6 +27,6 @@ USER appuser
26
27
  EXPOSE 8000
27
28
 
28
29
  HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
29
- CMD python -c "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()" || exit 1
30
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=5).read()" || exit 1
30
31
 
31
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
32
+ CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -5,19 +5,19 @@ Backend FastAPI bootstrapped por el arnés SDD.
5
5
  ## Quickstart
6
6
 
7
7
  ```bash
8
- # Con uv (recomendado)
9
- uv sync
10
- uv run uvicorn app.main:app --reload --port 8000
11
-
12
- # O con pip
8
+ # Con pip (con o sin venv activo)
13
9
  pip install -e ".[dev]"
14
- uvicorn app.main:app --reload --port 8000
10
+ uvicorn src.main:app --reload --port 8000
11
+
12
+ # Con uv (si está disponible)
13
+ uv sync
14
+ uv run uvicorn src.main:app --reload --port 8000
15
15
  ```
16
16
 
17
17
  ## Tests
18
18
 
19
19
  ```bash
20
- uv run pytest
20
+ python -m pytest
21
21
  # o
22
22
  pytest
23
23
  ```
@@ -25,8 +25,8 @@ pytest
25
25
  ## Lint + format
26
26
 
27
27
  ```bash
28
- uv run ruff check .
29
- uv run ruff format .
28
+ ruff check .
29
+ ruff format .
30
30
  ```
31
31
 
32
32
  ## Docker
@@ -40,12 +40,11 @@ curl http://localhost:8000/health
40
40
 
41
41
  ```
42
42
  src/
43
- ├── app/
44
- ├── main.py # FastAPI app
45
- ├── core/config.py # Settings (pydantic-settings)
46
- ├── routers/ # Endpoints (vacío por defecto)
47
- └── schemas/ # Pydantic models (vacío por defecto)
48
- └── tests/ # Pytest suite
43
+ ├── main.py # FastAPI app
44
+ ├── core/config.py # Settings (pydantic-settings)
45
+ ├── routers/ # Endpoints (vacío por defecto)
46
+ ├── schemas/ # Pydantic models (vacío por defecto)
47
+ └── tests/ # Pytest suite
49
48
  ```
50
49
 
51
50
  ## Stack
@@ -57,4 +56,4 @@ src/
57
56
  - Pytest 8 + pytest-asyncio
58
57
  - Ruff 0.8+ (lint + format)
59
58
 
60
- Ver `.opencode/templates/fastapi-sdd/pyproject.toml` para versiones pinneadas.
59
+ Ver `pyproject.toml` para versiones pinneadas.
@@ -9,7 +9,7 @@ services:
9
9
  - ENV=production
10
10
  restart: unless-stopped
11
11
  healthcheck:
12
- test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"]
12
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=5).read()"]
13
13
  interval: 30s
14
14
  timeout: 5s
15
15
  retries: 3
@@ -23,7 +23,7 @@ requires = ["hatchling"]
23
23
  build-backend = "hatchling.build"
24
24
 
25
25
  [tool.hatch.build.targets.wheel]
26
- packages = ["src/app"]
26
+ packages = ["src"]
27
27
 
28
28
  [tool.ruff]
29
29
  line-length = 100
@@ -36,4 +36,4 @@ ignore = ["E501"]
36
36
  [tool.pytest.ini_options]
37
37
  testpaths = ["src/tests", "tests"]
38
38
  asyncio_mode = "auto"
39
- pythonpath = ["src"]
39
+ pythonpath = [".", "src"]
@@ -1,7 +1,7 @@
1
1
  """FastAPI application entry point."""
2
2
  from fastapi import FastAPI
3
3
 
4
- from app.core.config import settings
4
+ from src.core.config import settings
5
5
 
6
6
 
7
7
  def create_app() -> FastAPI:
@@ -2,7 +2,7 @@
2
2
  import pytest
3
3
  from fastapi.testclient import TestClient
4
4
 
5
- from app.main import app
5
+ from src.main import app
6
6
 
7
7
 
8
8
  @pytest.fixture
@@ -11,7 +11,7 @@ def test_health_endpoint_returns_ok(client: TestClient) -> None:
11
11
 
12
12
 
13
13
  def test_app_metadata() -> None:
14
- from app.main import app
14
+ from src.main import app
15
15
 
16
16
  assert app.title
17
17
  assert app.version
@@ -375,7 +375,7 @@ export const bootstrap_fastapi = tool({
375
375
  }
376
376
 
377
377
  const isInitialized =
378
- fs.existsSync(path.resolve(root, "src/app/main.py")) &&
378
+ fs.existsSync(path.resolve(root, "src/main.py")) &&
379
379
  fs.existsSync(path.resolve(root, "pyproject.toml"))
380
380
 
381
381
  if (isInitialized && !args.force) {
@@ -430,12 +430,12 @@ export const bootstrap_fastapi = tool({
430
430
  ".dockerignore",
431
431
  "docker-compose.yml",
432
432
  "README.md",
433
- "src/app/__init__.py",
434
- "src/app/main.py",
435
- "src/app/core/__init__.py",
436
- "src/app/core/config.py",
437
- "src/app/routers/__init__.py",
438
- "src/app/schemas/__init__.py",
433
+ "src/__init__.py",
434
+ "src/main.py",
435
+ "src/core/__init__.py",
436
+ "src/core/config.py",
437
+ "src/routers/__init__.py",
438
+ "src/schemas/__init__.py",
439
439
  "src/tests/__init__.py",
440
440
  "src/tests/conftest.py",
441
441
  "src/tests/test_main.py",
@@ -522,7 +522,7 @@ export const bootstrap_fastapi = tool({
522
522
  installDuration,
523
523
  extrasInstalled,
524
524
  finalPyprojectToml,
525
- nextSteps: "Run `uv run uvicorn app.main:app --reload --port 8000` (o `uvicorn ...` con venv activo) para arrancar el dev server.",
525
+ nextSteps: "Run `uvicorn src.main:app --reload --port 8000` (o `python -m uvicorn src.main:app --reload --port 8000`) para arrancar el dev server.",
526
526
  _bootstrapRecord: bootstrapRecord,
527
527
  }, null, 2)
528
528
  }
@@ -13,31 +13,79 @@ const getRoot = (context: any) => {
13
13
 
14
14
  // Tool: sdd_clean_docker_environment
15
15
  export const clean_docker_environment = tool({
16
- description: "Limpia de forma agresiva y segura el entorno de Docker: remueve contenedores detenidos, imágenes huérfanas/dangling creadas por compilaciones fallidas, y redes no utilizadas. Se debe ejecutar antes de desplegar un nuevo contenedor para liberar recursos y evitar colisiones.",
16
+ description: "Asegura que Docker esté abierto y realiza una limpieza total y agresiva: detiene y elimina TODOS los contenedores (activos o inactivos), remueve TODAS las imágenes, volúmenes y redes para garantizar un lienzo en blanco absoluto antes de desplegar.",
17
17
  args: {},
18
18
  async execute(args, context) {
19
19
  const results: Record<string, string> = {}
20
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
21
+
22
+ // 1. Asegurar que Docker daemon esté listo
23
+ let dockerReady = false
24
+ for (let i = 0; i < 12; i++) {
25
+ try {
26
+ execSync("docker info", { stdio: "ignore", timeout: 3000 })
27
+ dockerReady = true
28
+ break
29
+ } catch (e) {
30
+ if (i === 0) {
31
+ try { execSync("open -a Docker", { stdio: "ignore" }) } catch (err) {}
32
+ }
33
+ await sleep(5000)
34
+ }
35
+ }
36
+
37
+ if (!dockerReady) {
38
+ return JSON.stringify({
39
+ status: "ERROR",
40
+ message: "No se pudo iniciar o conectar al demonio de Docker. Por favor, asegúrese de que Docker Desktop esté corriendo."
41
+ }, null, 2)
42
+ }
43
+
44
+ // 2. Detener y eliminar todos los contenedores de forma agresiva
20
45
  try {
21
- results.containers = execSync("docker container prune -f", { encoding: "utf8" }).toString().trim()
46
+ const containerIds = execSync("docker ps -aq", { encoding: "utf8" }).toString().trim()
47
+ if (containerIds) {
48
+ const ids = containerIds.split(/\s+/).join(" ")
49
+ execSync(`docker rm -f ${ids}`, { encoding: "utf8" })
50
+ results.containers = `Todos los contenedores detenidos y eliminados con éxito: ${ids}`
51
+ } else {
52
+ results.containers = "No se encontraron contenedores para eliminar."
53
+ }
54
+ } catch (e: any) {
55
+ results.containers = `Error eliminando contenedores: ${e.message}`
56
+ }
57
+
58
+ // 3. Eliminar todas las imágenes de forma agresiva
59
+ try {
60
+ const imageIds = execSync("docker images -aq", { encoding: "utf8" }).toString().trim()
61
+ if (imageIds) {
62
+ const ids = imageIds.split(/\s+/).join(" ")
63
+ execSync(`docker rmi -f ${ids}`, { encoding: "utf8" })
64
+ results.images = `Todas las imágenes eliminadas con éxito: ${ids}`
65
+ } else {
66
+ results.images = "No se encontraron imágenes para eliminar."
67
+ }
22
68
  } catch (e: any) {
23
- results.containers = `Error: ${e.message}`
69
+ results.images = `Error eliminando imágenes: ${e.message}`
24
70
  }
25
71
 
72
+ // 4. Eliminar volúmenes huérfanos
26
73
  try {
27
- results.images = execSync("docker image prune -f", { encoding: "utf8" }).toString().trim()
74
+ results.volumes = execSync("docker volume prune -af", { encoding: "utf8" }).toString().trim()
28
75
  } catch (e: any) {
29
- results.images = `Error: ${e.message}`
76
+ results.volumes = `Error podando volúmenes: ${e.message}`
30
77
  }
31
78
 
79
+ // 5. Eliminar redes inactivas
32
80
  try {
33
81
  results.networks = execSync("docker network prune -f", { encoding: "utf8" }).toString().trim()
34
82
  } catch (e: any) {
35
- results.networks = `Error: ${e.message}`
83
+ results.networks = `Error podando redes: ${e.message}`
36
84
  }
37
85
 
38
86
  return JSON.stringify({
39
87
  status: "SUCCESS",
40
- message: "Entorno de Docker limpiado de forma segura y exitosa.",
88
+ message: "Lienzo en blanco garantizado: Docker limpio al 100%.",
41
89
  details: results
42
90
  }, null, 2)
43
91
  }
@@ -9,7 +9,7 @@ DB_PATH = "/Users/wavesbyte/.local/share/opencode/opencode.db"
9
9
 
10
10
  # Escribe aquí el ID de la sesión que deseas exportar (ejemplo: "ses_1234...")
11
11
  # Si se deja vacío, el script requerirá el ID como argumento al ejecutarlo
12
- TARGET_SESSION_ID = "ses_12f8"
12
+ TARGET_SESSION_ID = "ses_11e1"
13
13
 
14
14
 
15
15
  def format_timestamp(ts):
package/bin/init.js CHANGED
@@ -19,6 +19,18 @@ const bold = '\x1b[1m';
19
19
  const pkgRoot = join(__dirname, '..');
20
20
  const targetDir = process.cwd();
21
21
 
22
+ // Load dynamic version from package.json
23
+ let pkgVersion = '1.0.31';
24
+ try {
25
+ const pkgJsonPath = join(pkgRoot, 'package.json');
26
+ if (fs.existsSync(pkgJsonPath)) {
27
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
28
+ pkgVersion = pkg.version || pkgVersion;
29
+ }
30
+ } catch (e) {
31
+ // ignore
32
+ }
33
+
22
34
  const banner = `
23
35
  ${bold}${orange}███████╗██╗ ██╗ ██████╗ ███████╗
24
36
  ╚══███╔╝██║ ██║██╔════╝ ╚══███╔╝
@@ -26,7 +38,7 @@ ${bold}${orange}███████╗██╗ ██╗ ██████
26
38
  ███╔╝ ██║ ██║██║ ██║ ███╔╝
27
39
  ███████╗╚██████╔╝╚██████╔╝███████╗
28
40
  ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝${reset}
29
- ${bold}${yellow} Harness Installer v1.0.7${reset}\n`;
41
+ ${bold}${yellow} Harness Installer v${pkgVersion}${reset}\n`;
30
42
 
31
43
  console.log(banner);
32
44
  console.log(`${bold}${cyan}🔍 Detectando entorno de trabajo...${reset}`);
@@ -88,6 +100,19 @@ for (const item of itemsToCopy) {
88
100
  console.error(`${red}❌ Error copying ${item.name}: ${error.message}${reset}`);
89
101
  }
90
102
  }
103
+
104
+ // Write .opencode/version.json dynamically with the installed package version
105
+ try {
106
+ const versionJsonPath = join(targetDir, '.opencode/version.json');
107
+ const targetOpencodeDir = join(targetDir, '.opencode');
108
+ if (!fs.existsSync(targetOpencodeDir)) {
109
+ fs.mkdirSync(targetOpencodeDir, { recursive: true });
110
+ }
111
+ fs.writeFileSync(versionJsonPath, JSON.stringify({ version: pkgVersion }, null, 2), 'utf8');
112
+ } catch (error) {
113
+ console.error(`${red}❌ Error writing version metadata: ${error.message}${reset}`);
114
+ }
115
+
91
116
  // Ensure .openspec/ is ignored in the target's .gitignore to avoid propagating local state
92
117
  try {
93
118
  const gitignorePath = join(targetDir, '.gitignore');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zugzbot",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "description": "Fácil instalador del arnés SDD de Zugzbot para proyectos OpenCode",
5
5
  "type": "module",
6
6
  "bin": {