Después de que sh0 push aterrizó, el CLI podía desplegar. Pero desplegar es una acción. El día de un desarrollador involucra docenas de pequeñas interacciones con sus herramientas: inicializar un proyecto, vincular un directorio a una app existente, abrir una URL, verificar la configuración. Estas no son funcionalidades. Son ergonomía. Y la ergonomía es la diferencia entre una herramienta que los desarrolladores toleran y una que buscan instintivamente.
La Fase 2 agregó cuatro comandos en una sola sesión. Ninguno es técnicamente impresionante. Todos hacen que el CLI se sienta completo.
sh0 init -- Detectar y preparar
Toda herramienta de despliegue tiene un comando init. Vercel tiene vercel init. Fly tiene fly launch. El propósito siempre es el mismo: mirar el proyecto actual, detectar qué es, y prepararlo para el despliegue.
sh0 init hace dos cosas:
- Detecta el stack e imprime lo que encontró
- Genera un archivo
.sh0ignorecon patrones conscientes del stack
$ sh0 init
Detected stack: nodejs
Framework: Next.js
Package manager: npm
Default port: 3000
Created .sh0ignore (12 patterns)La detección de stack reutiliza la misma función detect_stack() que sh0 push llama. No hay lógica de detección separada. Una función, una fuente de verdad.
Patrones de ignorado conscientes del stack
La parte interesante es la generación de .sh0ignore. Un proyecto Node.js debería excluir node_modules/, .next/, .turbo/. Un proyecto Rust debería excluir target/. Un proyecto Python debería excluir __pycache__/, .venv/, *.pyc. Un proyecto Go debería excluir la salida binaria.
El generador comienza con los patrones siempre excluidos (compartidos con sh0 push) y luego agrega patrones específicos del stack:
rustfn stack_specific_patterns(stack_type: &str) -> Vec<&'static str> {
match stack_type {
"nodejs" => vec![".next", ".nuxt", ".output", ".turbo", ".cache"],
"python" => vec!["*.egg-info", ".mypy_cache", ".pytest_cache", "htmlcov"],
"rust" => vec!["target"],
"go" => vec!["vendor"],
"java" => vec![".gradle", ".mvn", "*.class"],
"php" => vec!["vendor"],
"ruby" => vec![".bundle", "vendor/bundle"],
"dotnet" => vec!["bin", "obj", "*.user"],
_ => vec![],
}
}La auditoría detectó una sutileza: algunos patrones específicos del stack ya estaban en la lista de siempre excluidos. El patrón .next, por ejemplo, aparecía tanto en la lista de siempre excluidos como en la lista específica de Node.js. La corrección fue deduplicar: el generador solo agrega patrones que no están ya en la lista base.
sh0 link -- Conectar un directorio a una app existente
sh0 push crea apps nuevas. Pero ¿qué pasa con una app existente que fue desplegada a través del dashboard o Git? El desarrollador quiere enviar actualizaciones desde su terminal sin crear un duplicado.
sh0 link resuelve esto:
$ sh0 link my-existing-app
Linked to my-existing-app
-> https://my-existing-app.sh0.app
Next push will update this appInternamente, llama client.resolve_app("my-existing-app"), que busca en la lista de apps del servidor por nombre o UUID. Si lo encuentra, escribe el mismo .sh0/link.json que sh0 push crea en un despliegue exitoso.
La decisión clave de diseño fue reutilizar save_link() de push.rs en lugar de escribir una implementación separada. Esto garantiza que el formato del archivo de enlace sea idéntico ya sea creado por push o link.
sh0 open -- Abrir la URL en el navegador
Este es el comando más simple de todo el CLI. Lee el archivo de enlace o resuelve un argumento de app, obtiene el dominio primario, y lo abre en el navegador predeterminado.
$ sh0 open
Opening https://my-app.sh0.appLa lógica de apertura del navegador es consciente de la plataforma:
rustfn open_url(url: &str) -> Result<()> {
#[cfg(target_os = "macos")]
{
std::process::Command::new("open").arg(url).spawn()?;
}
#[cfg(target_os = "linux")]
{
std::process::Command::new("xdg-open").arg(url).spawn()?;
}
Ok(())
}Dos plataformas, dos comandos. sh0 apunta a servidores Linux y máquinas de desarrollo macOS.
Son seis líneas de código interesante y sesenta líneas de manejo de errores. Esa proporción es típica para herramientas CLI.
sh0 config -- Gestionar el archivo de configuración
El CLI de sh0 almacena su configuración en ~/.sh0/config.toml. El comando config proporciona tres subcomandos para gestionarlo:
$ sh0 config show
Server: https://sh0.example.com
Token: sh0_a1b2c3d4****
Config: /Users/dev/.sh0/config.toml
$ sh0 config get api_url
https://sh0.example.com
$ sh0 config set api_url https://new-server.example.com
Set api_urlEnmascaramiento de token
El subcomando show enmascara el token, mostrando solo los primeros 12 caracteres seguidos de <em>*</em>*. El subcomando get también enmascara -- no emite valores crudos para prevenir exposición accidental de credenciales.
Escrituras atómicas
El subcomando set escribe la configuración actualizada atómicamente: escribe a un archivo temporal, luego renombra. En Unix, también establece permisos 0600 en el archivo de configuración, asegurando que solo el usuario actual pueda leer el token.
El patrón de código compartido
La Fase 2 creó un patrón que las Fases 3 y 4 seguirían: los nuevos comandos reutilizan infraestructura existente de push.rs y client.rs en lugar de reimplementar funcionalidad.
| Función compartida | Usada por |
|---|---|
save_link() | push, link |
read_link() | push, open, watch |
ALWAYS_EXCLUDE | push, init, watch |
resolve_app() | link, open, restart, stop, start, delete, domains |
create_spinner() | push, watch |
Esto no es una capa de abstracción. No hay trait CliCommand ni struct CommandContext. Cada comando es un módulo independiente con una función run(). Comparten código importando funciones específicas, no heredando de una clase base.
Resultados de auditoría
La Fase 2 pasó por una sola ronda de auditoría:
- 0 Críticos
- 1 Importante: patrones duplicados en
.sh0ignorecuando patrones específicos del stack se solapaban conALWAYS_EXCLUDE - 2 Menores (1 corregido)
La tesis de ergonomía
Ninguno de estos cuatro comandos es técnicamente interesante. Pero juntos, transforman la experiencia del desarrollador. Cuatro comandos que cada uno ahorra 30 segundos. A lo largo de un día de desarrollo, eso son minutos. A lo largo de un mes, horas. A lo largo de la vida de un proyecto, la herramienta desaparece en la memoria muscular. Eso es lo que significa ergonomía.
Siguiente en la serie: Ciclo de vida de aplicaciones desde la terminal -- Cinco comandos para gestionar aplicaciones en ejecución: restart, stop, start, delete y gestión de dominios.