shilo-cli
v0.7.0
Published
CLI para sincronizar carpetas locales con repositorios de Shilo: push, pull, status, log y diff.
Maintainers
Readme
shilo-cli
CLI para subir carpetas locales a repositorios de Shilo,
con una experiencia tipo git: shilo login una vez y después shilo push.
Estado: Fase 5 (colaboración asíncrona). Disponibles
login,whoami,logout,init,push,pull,status,log,diff. Ciclo completo:shilo status→shilo pull→ trabajar local →shilo push.
Requisitos
- Node.js >= 18 (usa
fetchycryptonativos; sin dependencias de runtime).
Instalación (local, durante desarrollo)
cd cli
npm link # expone el comando `shilo` global
# o, sin link:
node bin/shilo.js <comando>Uso
shilo login # pregunta email y contraseña (oculta)
shilo login --email [email protected]
shilo whoami # muestra la sesión activa
shilo logout # borra las credenciales localesSubir una carpeta (flujo tipo git)
cd mi-carpeta
shilo init --project <uuid> # ata la carpeta al repo (escribe .shilo.json)
shilo push # sube: crea/actualiza + commit
shilo push -m "mi mensaje" # con mensaje de commit
shilo push --dry-run # muestra qué haría sin subir
shilo push --card ABC-123 # agrega "[card: ABC-123]" al mensaje del commit
shilo push --yes # sin pedir confirmación
shilo push --verbose # lista archivo por archivo en vez de resumir
shilo push --concurrency 4 # requests en paralelo (1-32, default 8)
shilo push ./otra-carpeta --project <uuid> # sin init, proyecto explícitoSólo se suben archivos de texto; binarios, oversize (>25 MB) y nombres con caracteres no permitidos se saltan con aviso. Los archivos sin cambios no se re-suben.
Los nombres aceptan espacios, acentos y Unicode (Notas Metrónomo.txt, 日本語.md):
sólo se rechaza lo que rompe una ruta o no es portable — / \ : * ? " < > |, bytes de
control, ./.., nombres de dispositivo de Windows (CON, NUL, COM1…) y el punto
final. Los nombres se normalizan a NFC, igual que el backend, para que el mismo archivo
subido desde macOS y desde Windows sea uno solo.
Guardar es proponer: el push manda todo lo que cambió como una propuesta que parte
de la versión que esta carpeta vio por última vez (base en .shilo.json, que
pull y push van anotando). El servidor la aplica solo si combina limpio con la
versión actual. Si otra persona cambió alguno de los mismos archivos desde esa
base, la propuesta queda abierta y no se pisa nada: el CLI dice qué archivos son,
tus cambios quedan guardados en esa propuesta para que el dueño decida, y vos podés
traer la versión actual con shilo pull, ajustar y volver a push. Un push nuevo
reemplaza tus propuestas abiertas anteriores.
El texto viaja inline (hasta 500 archivos o 4 MB por propuesta; más que eso son varias propuestas de la misma base); los binarios y los textos de más de 1 MB se suben por URL firmada y entran por su identificador de contenido. Contra un servidor anterior a las propuestas, el CLI avisa y cae al camino viejo (escribir + commit), que sí puede pisar.
Qué se ignora
push respeta .gitignore y .shiloignore, incluidos los de subcarpetas (una
regla de src/.gitignore sólo alcanza a src/). .shiloignore se lee último, así que
podés re-incluir con ! algo que git ignora. Por defecto ya se saltean .git/,
node_modules/, .shilo/, .vercel/, .DS_Store y Thumbs.db.
También se poda cualquier carpeta que tenga forma de repo git (HEAD + objects/
refs/), aunque no se llame.git: un.git-ORIGINAL-BACKUP/trae todo el historial, incluidos secretos de commits ya borrados del árbol de trabajo.
Limitación conocida: la negación
!se evalúa por regla, no por directorio. Como en git,!no puede re-incluir un archivo si su carpeta padre ya está excluida (dist/+!dist/keep.txtno funciona); excluí el contenido en vez de la carpeta (dist/*+!dist/keep.txt).
Lo salteado se reporta agrupado por motivo (binario, ilegible, symlink, nombre
inválido, profundidad) con unos pocos ejemplos; si una carpeta concentra muchos
descartes, el CLI sugiere la línea de .shiloignore que la cubre. Con --verbose ves
la lista completa.
Escaneo de secretos
Antes de tocar la red, push revisa nombres sospechosos (.env, id_rsa,
*.pem, credenciales de service account…) y los primeros 8 KB de cada archivo
buscando patrones de claves (AWS, JWT de Supabase, tokens de GitHub, claves privadas
PEM…). Si encuentra algo, no sube nada y sale con código 1, indicando archivo y
línea — nunca el valor detectado. Salidas: agregarlo a .shiloignore, sacar el
secreto, o --allow-secrets si es un falso positivo.
Índice local
push guarda un índice en .shilo/index.json (tamaño + mtime + hash de lo último
subido). Los archivos que no cambiaron se resuelven sin bajar el contenido remoto,
que es lo que hace rápido el segundo push de una carpeta grande. Se puede ignorar con
--no-index para recomparar todo contra el servidor. Agregá .shilo/ a tu
.gitignore (el CLI ya no lo sube).
--dry-run lista qué se crearía y qué se actualizaría, sin tocar la red más que para
leer el árbol. Ya no hace falta adivinar si vas a pisar a alguien: eso lo decide el
servidor con la base exacta, y si pasa, no pisa.
¿Estoy desactualizado? (status)
shilo status # compara local vs remoto SIN bajar contenido
shilo status --short # sólo rutas
shilo status --json # para scriptsEs el comando barato: usa sólo listados de carpeta (una request por carpeta, cero descargas de contenido) y compara por ruta + tamaño. Informa qué tiene el remoto que vos no (con autor y fecha del último commit de cada archivo), qué tenés local sin subir, y marca aparte los archivos que cambiaron de los dos lados — el caso peligroso. Exit code 2 = estás atrasado respecto del remoto, útil en scripts.
Compensación consciente: dos archivos del mismo tamaño se reportan como iguales
aunque difieran byte a byte. shilo diff y shilo push sí comparan contenido.
La comparación con "desde la última vez que sincronicé" se apoya en lastSync, que
push y los pull completos escriben en .shilo.json. Si nunca sincronizaste desde
esta carpeta, no hay línea de base y los cambios remotos se reportan igual.
Historial (log)
shilo log # últimos commits del proyecto
shilo log src/app.js # sólo los commits que tocaron ese archivo
shilo log -n 50 --files # más commits, listando archivos de cada uno
shilo log --jsonLa paginación es por commit, no por archivo: si un archivo no aparece, subí -n.
Diferencias (diff)
shilo diff # resumen de todo lo que difiere (sólo metadata)
shilo diff src/app.js # diff de contenido línea a línea
shilo diff src/app.js --context 6
shilo diff --statEl diff usa el remoto como base (---) y tu copia local como destino (+++), o sea
que las líneas + son lo que un push agregaría. Exit code 1 = hay diferencias.
Bajar un proyecto (pull)
shilo pull ./mi-carpeta --project <uuid> # baja el repo a una carpeta local
shilo pull --dry-run # muestra qué cambiaría localmente, sin bajar nada
shilo pull --yes # sin pedir confirmación
shilo pull --force # sobrescribe también los archivos que difierenpull recorre el árbol remoto (carpeta por carpeta), baja los archivos de texto
y deja .shilo.json listo para un push posterior sin --project.
Protección del trabajo local sin guardar: no pisa un archivo local que difiera del
remoto salvo con --force. Sin --force no aborta el pull entero — baja lo que es
seguro, deja afuera los conflictivos y sale con código 1 para que decidas (mirá
shilo diff <archivo>). Los archivos que sólo existen localmente nunca se tocan: el
pull no borra. --dry-run muestra el impacto sobre tu disco (nuevo / sobrescribe /
se conserva), no el árbol remoto crudo.
También bloquea nombres que intenten escribir fuera de la carpeta destino (path
traversal) y saltea binarios con aviso (volverían corruptos). Espejo de push:
--project pisa a .shilo.json. Con shilo init --pull se ata y baja en un solo paso.
Servidor alternativo
shilo login --base-url https://mi-shilo.vercel.appCredenciales
Se guardan en ~/.shilo/config.json (override con SHILO_CONFIG_DIR), cifradas
con AES-256-CBC y con permisos 0600.
- Por defecto la clave de cifrado se deriva de la máquina (hostname + usuario). Esto es ofuscación en reposo, no secreto fuerte.
- Para cifrado fuerte, exportá
SHILO_ENCRYPTION_KEY(una passphrase propia) antes deshilo login; necesitarás la misma variable para futuras ejecuciones.
Se almacena el JWT, el refresh token, la expiración y el email; no se guarda la contraseña.
Tokens de acceso personal (PAT)
Un token pasado como --token abc123 queda en el historial del shell y en la lista de
procesos (ps lo muestra a cualquier usuario de la máquina). El CLI lo acepta —es la
forma que se copia desde la web— pero avisa. Alternativas:
cat pat.txt | shilo login --token - # lee el token de stdin
SHILO_TOKEN=<pat> shilo init --project <uuid> # variable de entorno (lo natural en CI)Variables de entorno
| Variable | Efecto |
| ----------------------- | ------------------------------------------------------------- |
| SHILO_CONFIG_DIR | Directorio de config (default ~/.shilo). |
| SHILO_ENCRYPTION_KEY | Passphrase para cifrar credenciales (recomendado). |
| SHILO_TOKEN | Token de acceso personal (evita pasarlo por --token). |
| SHILO_CONCURRENCY | Requests en paralelo del push (1-32, default 8). |
| NO_COLOR | Desactiva colores ANSI. |
Roadmap
- Fase 1 — auth (hecho):
login,whoami,logout+ config cifrada. - Fase 2 — motor (hecho): cliente de archivos (
list/getMeta/create/write/ensurePath/commit) + recorrido de árbol (.shiloignore) + detección de binarios. - Fase 3 — push (hecho):
shilo init+shilo push(crea/actualiza + commit, detección de cambios,.shiloignore,--dry-run). - Fase 4 — pull (hecho):
shilo pull(recorrido del árbol remoto con pool, guarda anti path-traversal, salto de binarios, colisiones, binding.shilo.json,--dry-run). Cierra el ciclopull → trabajar local → push. - Fase 5 — colaboración asíncrona (hecho):
status(comparación barata local vs remoto + exit code 2),log(historial por proyecto o archivo),diff(comparación de contenido + exit code 1), atribución de autor enpush --dry-run,pullque resuelve conflictos por archivo en vez de abortar,lastSyncen.shilo.jsonypush --card <id>para cruzar commits con un tablero externo (Metrónomo). - Fase 6 — robustez del push (hecho):
.gitignore(incluidos los anidados), poda de repos git por forma, escaneo de secretos con corte del push, preflight 100 % offline antes de tocar la red, planificación con un recorrido del árbol remoto (una request por carpeta en vez de una por archivo), índice local.shilo/index.json, lectura perezosa del contenido, reintentos con backoff ante 429/5xx, concurrencia adaptativa, barra de progreso y confirmación real eninit --push. - Fase 7 — propuestas (hecho):
pushdeja de escribir archivo por archivo y manda una propuesta con la base exacta; el servidor la combina con la versión actual y nunca pisa un archivo que otra persona cambió.baseen.shilo.json,registerBlobpara binarios, camino viejo sólo como reserva.
Exit codes
| Comando | Código | Significado |
| -------- | ------ | -------------------------------------------- |
| status | 2 | El remoto tiene cambios que no tenés. |
| diff | 1 | Hay diferencias entre local y remoto. |
| pull | 1 | Quedaron conflictos sin bajar (o fallas). |
| push | 1 | Se detectaron posibles secretos (nada subido). |
| cualquiera | 1 | Error. |
