shilo-cli 0.6.0 → 0.8.0

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.
package/README.md CHANGED
@@ -1,243 +1,319 @@
1
- # shilo-cli
2
-
3
- CLI para subir carpetas locales a repositorios de [Shilo](https://www.shilo.city),
4
- con una experiencia tipo git: `shilo login` una vez y después `shilo push`.
5
-
6
- > **Estado:** Fase 5 (colaboración asíncrona). Disponibles `login`, `whoami`, `logout`,
7
- > `init`, `push`, `pull`, `status`, `log`, `diff`.
8
- > Ciclo completo: `shilo status` → `shilo pull` → trabajar local → `shilo push`.
9
-
10
- ## Requisitos
11
-
12
- - Node.js **>= 18** (usa `fetch` y `crypto` nativos; sin dependencias de runtime).
13
-
14
- ## Instalación (local, durante desarrollo)
15
-
16
- ```bash
17
- cd cli
18
- npm link # expone el comando `shilo` global
19
- # o, sin link:
20
- node bin/shilo.js <comando>
21
- ```
22
-
23
- ## Uso
24
-
25
- ```bash
26
- shilo login # pregunta email y contraseña (oculta)
27
- shilo login --email you@example.com
28
- shilo whoami # muestra la sesión activa
29
- shilo logout # borra las credenciales locales
30
- ```
31
-
32
- ### Subir una carpeta (flujo tipo git)
33
-
34
- ```bash
35
- cd mi-carpeta
36
- shilo init --project <uuid> # ata la carpeta al repo (escribe .shilo.json)
37
- shilo push # sube: crea/actualiza + commit
38
- shilo push -m "mi mensaje" # con mensaje de commit
39
- shilo push --dry-run # muestra qué haría sin subir
40
- shilo push --card ABC-123 # agrega "[card: ABC-123]" al mensaje del commit
41
- shilo push --yes # sin pedir confirmación
42
- shilo push --verbose # lista archivo por archivo en vez de resumir
43
- shilo push --concurrency 4 # requests en paralelo (1-32, default 8)
44
- shilo push ./otra-carpeta --project <uuid> # sin init, proyecto explícito
45
- ```
46
-
47
- Sólo se suben archivos de **texto**; binarios, oversize (>25 MB) y nombres con
48
- caracteres no permitidos se saltan con aviso. Los archivos sin cambios no se
49
- re-suben.
50
-
51
- Los nombres aceptan **espacios, acentos y Unicode** (`Notas Metrónomo.txt`, `日本語.md`):
52
- sólo se rechaza lo que rompe una ruta o no es portable — `/ \ : * ? " < > |`, bytes de
53
- control, `.`/`..`, nombres de dispositivo de Windows (`CON`, `NUL`, `COM1`…) y el punto
54
- final. Los nombres se normalizan a NFC, igual que el backend, para que el mismo archivo
55
- subido desde macOS y desde Windows sea uno solo.
56
-
57
- La subida va **por lotes** (hasta 100 archivos o 4 MB por request) y **cada lote se
58
- commitea apenas termina**: si el push se corta a la mitad, lo ya escrito queda en el
59
- historial en vez de quedar suelto en storage. Contra un servidor viejo que no conoce el
60
- endpoint de lotes, el CLI avisa y sigue archivo por archivo.
61
-
62
- #### Qué se ignora
63
-
64
- `push` respeta **`.gitignore` y `.shiloignore`**, incluidos los de subcarpetas (una
65
- regla de `src/.gitignore` sólo alcanza a `src/`). `.shiloignore` se lee último, así que
66
- podés re-incluir con `!` algo que git ignora. Por defecto ya se saltean `.git/`,
67
- `node_modules/`, `.shilo/`, `.vercel/`, `.DS_Store` y `Thumbs.db`.
68
-
69
- También se poda cualquier carpeta que **tenga forma de repo git** (`HEAD` + `objects/`
70
- + `refs/`), aunque no se llame `.git`: un `.git-ORIGINAL-BACKUP/` trae todo el
71
- historial, incluidos secretos de commits ya borrados del árbol de trabajo.
72
-
73
- > **Limitación conocida:** la negación `!` se evalúa por regla, no por directorio. Como
74
- > en git, `!` no puede re-incluir un archivo si su carpeta padre ya está excluida
75
- > (`dist/` + `!dist/keep.txt` no funciona); excluí el contenido en vez de la carpeta
76
- > (`dist/*` + `!dist/keep.txt`).
77
-
78
- Lo salteado se reporta **agrupado por motivo** (binario, ilegible, symlink, nombre
79
- inválido, profundidad) con unos pocos ejemplos; si una carpeta concentra muchos
80
- descartes, el CLI sugiere la línea de `.shiloignore` que la cubre. Con `--verbose` ves
81
- la lista completa.
82
-
83
- #### Escaneo de secretos
84
-
85
- Antes de tocar la red, `push` revisa nombres sospechosos (`.env`, `id_rsa`,
86
- `*.pem`, credenciales de service account…) y los primeros 8 KB de cada archivo
87
- buscando patrones de claves (AWS, JWT de Supabase, tokens de GitHub, claves privadas
88
- PEM…). Si encuentra algo, **no sube nada** y sale con código 1, indicando archivo y
89
- línea — nunca el valor detectado. Salidas: agregarlo a `.shiloignore`, sacar el
90
- secreto, o `--allow-secrets` si es un falso positivo.
91
-
92
- #### Índice local
93
-
94
- `push` guarda un índice en `.shilo/index.json` (tamaño + mtime + hash de lo último
95
- subido). Los archivos que no cambiaron se resuelven sin bajar el contenido remoto,
96
- que es lo que hace rápido el segundo push de una carpeta grande. Se puede ignorar con
97
- `--no-index` para recomparar todo contra el servidor. Agregá `.shilo/` a tu
98
- `.gitignore` (el CLI ya no lo sube).
99
-
100
- En `--dry-run` (y antes de la confirmación interactiva) cada archivo modificado
101
- muestra **quién lo tocó por última vez y cuándo**, y se avisa explícitamente si vas
102
- a pisar el trabajo de otra persona. Con token de acceso personal el autor se muestra
103
- igual, pero no se puede marcar cuál sos vos (el PAT no expone tu id de usuario).
104
-
105
- ### ¿Estoy desactualizado? (status)
106
-
107
- ```bash
108
- shilo status # compara local vs remoto SIN bajar contenido
109
- shilo status --short # sólo rutas
110
- shilo status --json # para scripts
111
- ```
112
-
113
- Es el comando barato: usa sólo listados de carpeta (una request por carpeta, cero
114
- descargas de contenido) y compara por **ruta + tamaño**. Informa qué tiene el remoto
115
- que vos no (con autor y fecha del último commit de cada archivo), qué tenés local sin
116
- subir, y marca aparte los archivos que cambiaron **de los dos lados** — el caso
117
- peligroso. **Exit code 2** = estás atrasado respecto del remoto, útil en scripts.
118
-
119
- Compensación consciente: dos archivos del mismo tamaño se reportan como iguales
120
- aunque difieran byte a byte. `shilo diff` y `shilo push` sí comparan contenido.
121
-
122
- La comparación con "desde la última vez que sincronicé" se apoya en `lastSync`, que
123
- `push` y los `pull` completos escriben en `.shilo.json`. Si nunca sincronizaste desde
124
- esta carpeta, no hay línea de base y los cambios remotos se reportan igual.
125
-
126
- ### Historial (log)
127
-
128
- ```bash
129
- shilo log # últimos commits del proyecto
130
- shilo log src/app.js # sólo los commits que tocaron ese archivo
131
- shilo log -n 50 --files # más commits, listando archivos de cada uno
132
- shilo log --json
133
- ```
134
-
135
- La paginación es **por commit**, no por archivo: si un archivo no aparece, subí `-n`.
136
-
137
- ### Diferencias (diff)
138
-
139
- ```bash
140
- shilo diff # resumen de todo lo que difiere (sólo metadata)
141
- shilo diff src/app.js # diff de contenido línea a línea
142
- shilo diff src/app.js --context 6
143
- shilo diff --stat
144
- ```
145
-
146
- El diff usa el remoto como base (`---`) y tu copia local como destino (`+++`), o sea
147
- que las líneas `+` son lo que un `push` agregaría. **Exit code 1** = hay diferencias.
148
-
149
- ### Bajar un proyecto (pull)
150
-
151
- ```bash
152
- shilo pull ./mi-carpeta --project <uuid> # baja el repo a una carpeta local
153
- shilo pull --dry-run # muestra qué cambiaría localmente, sin bajar nada
154
- shilo pull --yes # sin pedir confirmación
155
- shilo pull --force # sobrescribe también los archivos que difieren
156
- ```
157
-
158
- `pull` recorre el árbol remoto (carpeta por carpeta), baja los archivos de texto
159
- y deja `.shilo.json` listo para un `push` posterior sin `--project`.
160
-
161
- Protección del trabajo local sin guardar: **no pisa** un archivo local que difiera del
162
- remoto salvo con `--force`. Sin `--force` no aborta el pull entero — baja lo que es
163
- seguro, deja afuera los conflictivos y sale con código 1 para que decidas (mirá
164
- `shilo diff <archivo>`). Los archivos que sólo existen localmente nunca se tocan: el
165
- pull no borra. `--dry-run` muestra el impacto sobre tu disco (nuevo / sobrescribe /
166
- se conserva), no el árbol remoto crudo.
167
-
168
- También bloquea nombres que intenten escribir fuera de la carpeta destino (path
169
- traversal) y saltea binarios con aviso (volverían corruptos). Espejo de `push`:
170
- `--project` pisa a `.shilo.json`. Con `shilo init --pull` se ata y baja en un solo paso.
171
-
172
- ### Servidor alternativo
173
-
174
- ```bash
175
- shilo login --base-url https://mi-shilo.vercel.app
176
- ```
177
-
178
- ## Credenciales
179
-
180
- Se guardan en `~/.shilo/config.json` (override con `SHILO_CONFIG_DIR`), **cifradas**
181
- con AES-256-CBC y con permisos `0600`.
182
-
183
- - Por defecto la clave de cifrado se deriva de la máquina (hostname + usuario).
184
- Esto es **ofuscación en reposo**, no secreto fuerte.
185
- - Para cifrado fuerte, exportá `SHILO_ENCRYPTION_KEY` (una passphrase propia) antes
186
- de `shilo login`; necesitarás la misma variable para futuras ejecuciones.
187
-
188
- Se almacena el JWT, el refresh token, la expiración y el email; **no** se guarda la
189
- contraseña.
190
-
191
- ### Tokens de acceso personal (PAT)
192
-
193
- Un token pasado como `--token abc123` queda en el historial del shell y en la lista de
194
- procesos (`ps` lo muestra a cualquier usuario de la máquina). El CLI lo acepta —es la
195
- forma que se copia desde la web— pero avisa. Alternativas:
196
-
197
- ```bash
198
- cat pat.txt | shilo login --token - # lee el token de stdin
199
- SHILO_TOKEN=<pat> shilo init --project <uuid> # variable de entorno (lo natural en CI)
200
- ```
201
-
202
- ## Variables de entorno
203
-
204
- | Variable | Efecto |
205
- | ----------------------- | ------------------------------------------------------------- |
206
- | `SHILO_CONFIG_DIR` | Directorio de config (default `~/.shilo`). |
207
- | `SHILO_ENCRYPTION_KEY` | Passphrase para cifrar credenciales (recomendado). |
208
- | `SHILO_TOKEN` | Token de acceso personal (evita pasarlo por `--token`). |
209
- | `SHILO_CONCURRENCY` | Requests en paralelo del push (1-32, default 8). |
210
- | `NO_COLOR` | Desactiva colores ANSI. |
211
-
212
- ## Roadmap
213
-
214
- - **Fase 1 — auth** *(hecho)*: `login`, `whoami`, `logout` + config cifrada.
215
- - **Fase 2 — motor** *(hecho)*: cliente de archivos (`list`/`getMeta`/`create`/`write`/
216
- `ensurePath`/`commit`) + recorrido de árbol (`.shiloignore`) + detección de binarios.
217
- - **Fase 3 — push** *(hecho)*: `shilo init` + `shilo push` (crea/actualiza + commit,
218
- detección de cambios, `.shiloignore`, `--dry-run`).
219
- - **Fase 4 — pull** *(hecho)*: `shilo pull` (recorrido del árbol remoto con pool,
220
- guarda anti path-traversal, salto de binarios, colisiones, binding `.shilo.json`,
221
- `--dry-run`). Cierra el ciclo `pull → trabajar local → push`.
222
- - **Fase 5 — colaboración asíncrona** *(hecho)*: `status` (comparación barata local vs
223
- remoto + exit code 2), `log` (historial por proyecto o archivo), `diff` (comparación
224
- de contenido + exit code 1), atribución de autor en `push --dry-run`, `pull` que
225
- resuelve conflictos por archivo en vez de abortar, `lastSync` en `.shilo.json` y
226
- `push --card <id>` para cruzar commits con un tablero externo (Metrónomo).
227
- - **Fase 6 — robustez del push** *(hecho)*: `.gitignore` (incluidos los anidados),
228
- poda de repos git por forma, escaneo de secretos con corte del push, preflight
229
- 100 % offline antes de tocar la red, planificación con un recorrido del árbol
230
- remoto (una request por carpeta en vez de una por archivo), índice local
231
- `.shilo/index.json`, lectura perezosa del contenido, reintentos con backoff ante
232
- 429/5xx, concurrencia adaptativa, barra de progreso y confirmación real en
233
- `init --push`.
234
-
235
- ## Exit codes
236
-
237
- | Comando | Código | Significado |
238
- | -------- | ------ | -------------------------------------------- |
239
- | `status` | 2 | El remoto tiene cambios que no tenés. |
240
- | `diff` | 1 | Hay diferencias entre local y remoto. |
241
- | `pull` | 1 | Quedaron conflictos sin bajar (o fallas). |
242
- | `push` | 1 | Se detectaron posibles secretos (nada subido). |
243
- | cualquiera | 1 | Error. |
1
+ # shilo-cli
2
+
3
+ CLI para subir carpetas locales a repositorios de [Shilo](https://www.shilo.city),
4
+ con una experiencia tipo git: `shilo login` una vez y después `shilo push`.
5
+
6
+ > **Estado:** Fase 5 (colaboración asíncrona). Disponibles `login`, `whoami`, `logout`,
7
+ > `init`, `push`, `pull`, `status`, `log`, `diff`.
8
+ > Ciclo completo: `shilo status` → `shilo pull` → trabajar local → `shilo push`.
9
+
10
+ ## Requisitos
11
+
12
+ - Node.js **>= 18** (usa `fetch` y `crypto` nativos; sin dependencias de runtime).
13
+
14
+ ## Instalación (local, durante desarrollo)
15
+
16
+ ```bash
17
+ cd cli
18
+ npm link # expone el comando `shilo` global
19
+ # o, sin link:
20
+ node bin/shilo.js <comando>
21
+ ```
22
+
23
+ ## Uso
24
+
25
+ ```bash
26
+ shilo login # pregunta email y contraseña (oculta)
27
+ shilo login --email you@example.com
28
+ shilo whoami # muestra la sesión activa
29
+ shilo logout # borra las credenciales locales
30
+ ```
31
+
32
+ ### Subir una carpeta (flujo tipo git)
33
+
34
+ ```bash
35
+ cd mi-carpeta
36
+ shilo init --project <uuid> # ata la carpeta al repo (escribe .shilo.json)
37
+ shilo push # sube: crea/actualiza + commit
38
+ shilo push -m "mi mensaje" # con mensaje de commit
39
+ shilo push --dry-run # muestra qué haría sin subir
40
+ shilo push --card ABC-123 # agrega "[card: ABC-123]" al mensaje del commit
41
+ shilo push --agent claude-code --model claude-opus-5 # lo escribió un agente, y queda anotado
42
+ shilo push --agent claude-code --prompt-file pedido.md # guarda además el pedido que lo originó
43
+ shilo push --yes # sin pedir confirmación
44
+ shilo push --verbose # lista archivo por archivo en vez de resumir
45
+ shilo push --concurrency 4 # requests en paralelo (1-32, default 8)
46
+ shilo push ./otra-carpeta --project <uuid> # sin init, proyecto explícito
47
+ ```
48
+
49
+ Sólo se suben archivos de **texto**; binarios, oversize (>25 MB) y nombres con
50
+ caracteres no permitidos se saltan con aviso. Los archivos sin cambios no se
51
+ re-suben.
52
+
53
+ Los nombres aceptan **espacios, acentos y Unicode** (`Notas Metrónomo.txt`, `日本語.md`):
54
+ sólo se rechaza lo que rompe una ruta o no es portable — `/ \ : * ? " < > |`, bytes de
55
+ control, `.`/`..`, nombres de dispositivo de Windows (`CON`, `NUL`, `COM1`…) y el punto
56
+ final. Los nombres se normalizan a NFC, igual que el backend, para que el mismo archivo
57
+ subido desde macOS y desde Windows sea uno solo.
58
+
59
+ Guardar es **proponer**: el push manda todo lo que cambió como una propuesta que parte
60
+ de la versión que esta carpeta vio por última vez (`base` en `.shilo.json`, que
61
+ `pull` y `push` van anotando). El servidor la aplica solo si combina limpio con la
62
+ versión actual. Si otra persona cambió alguno de los **mismos archivos** desde esa
63
+ base, la propuesta **queda abierta y no se pisa nada**: el CLI dice qué archivos son,
64
+ tus cambios quedan guardados en esa propuesta para que el dueño decida, y vos podés
65
+ traer la versión actual con `shilo pull`, ajustar y volver a `push`. Un push nuevo
66
+ reemplaza tus propuestas abiertas anteriores.
67
+
68
+ El texto viaja inline (hasta 500 archivos o 4 MB por propuesta; más que eso son varias
69
+ propuestas de la misma base); los binarios y los textos de más de 1 MB se suben por URL
70
+ firmada y entran por su identificador de contenido. Contra un servidor anterior a las
71
+ propuestas, el CLI avisa y cae al camino viejo (escribir + commit), que sí puede pisar.
72
+
73
+ #### Subir sólo una parte (`--only`)
74
+
75
+ ```bash
76
+ shilo push --only Documentacion/decisiones.md --only LEEME.md -m "decisiones de la semana"
77
+ shilo push --only Documentacion/ # un prefijo de carpeta también vale
78
+ ```
79
+
80
+ `--only` es repetible y recorta **qué se sube**, no qué se revisa: los ignores y el
81
+ escaneo de secretos corren igual sobre toda la carpeta. Como el push nunca borra
82
+ archivos remotos, subir un subconjunto es seguro por construcción.
83
+
84
+ Un selector que **no matchea nada es un error** (código 1) y no sube nada: nunca vas a
85
+ pedir dos archivos y terminar subiendo el repo entero. Al final, el resumen dice
86
+ cuántos archivos con cambios locales quedaron fuera, para que "una pieza = una tarjeta"
87
+ no se convierta en trabajo olvidado. El índice local no se poda con la selección: el
88
+ push completo siguiente sigue siendo rápido.
89
+
90
+ #### Qué se ignora
91
+
92
+ `push` respeta **`.gitignore` y `.shiloignore`**, incluidos los de subcarpetas (una
93
+ regla de `src/.gitignore` sólo alcanza a `src/`). `.shiloignore` se lee último, así que
94
+ podés re-incluir con `!` algo que git ignora. Por defecto ya se saltean `.git/`,
95
+ `node_modules/`, `.shilo/`, `.vercel/`, `.DS_Store` y `Thumbs.db`.
96
+
97
+ También se poda cualquier carpeta que **tenga forma de repo git** (`HEAD` + `objects/`
98
+ + `refs/`), aunque no se llame `.git`: un `.git-ORIGINAL-BACKUP/` trae todo el
99
+ historial, incluidos secretos de commits ya borrados del árbol de trabajo.
100
+
101
+ > **Limitación conocida:** la negación `!` se evalúa por regla, no por directorio. Como
102
+ > en git, `!` no puede re-incluir un archivo si su carpeta padre ya está excluida
103
+ > (`dist/` + `!dist/keep.txt` no funciona); excluí el contenido en vez de la carpeta
104
+ > (`dist/*` + `!dist/keep.txt`).
105
+
106
+ Lo salteado se reporta **agrupado por motivo** (binario, ilegible, symlink, nombre
107
+ inválido, profundidad) con unos pocos ejemplos; si una carpeta concentra muchos
108
+ descartes, el CLI sugiere la línea de `.shiloignore` que la cubre. Con `--verbose` ves
109
+ la lista completa.
110
+
111
+ #### Escaneo de secretos
112
+
113
+ Antes de tocar la red, `push` revisa nombres sospechosos (`.env`, `id_rsa`,
114
+ `*.pem`, credenciales de service account…) y los primeros 8 KB de cada archivo
115
+ buscando patrones de claves (AWS, JWT de Supabase, tokens de GitHub, claves privadas
116
+ PEM…). Si encuentra algo, **no sube nada** y sale con código 1, indicando archivo y
117
+ línea — nunca el valor detectado. Salidas: agregarlo a `.shiloignore`, sacar el
118
+ secreto, o `--allow-secrets` si es un falso positivo.
119
+
120
+ #### Índice local
121
+
122
+ `push` guarda un índice en `.shilo/index.json` (tamaño + mtime + hash de lo último
123
+ subido). Los archivos que no cambiaron se resuelven sin bajar el contenido remoto,
124
+ que es lo que hace rápido el segundo push de una carpeta grande. Se puede ignorar con
125
+ `--no-index` para recomparar todo contra el servidor. Agregá `.shilo/` a tu
126
+ `.gitignore` (el CLI ya no lo sube).
127
+
128
+ `--dry-run` lista qué se crearía y qué se actualizaría, sin tocar la red más que para
129
+ leer el árbol. Ya no hace falta adivinar si vas a pisar a alguien: eso lo decide el
130
+ servidor con la base exacta, y si pasa, no pisa.
131
+
132
+ ### ¿Estoy desactualizado? (status)
133
+
134
+ ```bash
135
+ shilo status # compara local vs remoto SIN bajar contenido
136
+ shilo status --short # sólo rutas
137
+ shilo status --show-ignored # lista los archivos del remoto ignorados localmente
138
+ shilo status --json # para scripts
139
+ ```
140
+
141
+ Es el comando barato: usa sólo listados de carpeta (una request por carpeta, cero
142
+ descargas de contenido) y compara por **ruta + tamaño**. Informa qué tiene el remoto
143
+ que vos no (con autor y fecha del último commit de cada archivo), qué tenés local sin
144
+ subir, y marca aparte los archivos que cambiaron **de los dos lados** — el caso
145
+ peligroso. **Exit code 2** = estás atrasado respecto del remoto, útil en scripts.
146
+
147
+ **Los ignores valen en los dos sentidos.** Un archivo que existe en el remoto pero
148
+ está ignorado localmente (un `.env.ejemplo`, un `settings.local.json` que alguien subió
149
+ desde otra máquina) **no cuenta como "atrás"**: se informa en una línea aparte —con
150
+ `--show-ignored` para verlos, y en el campo `ignoredRemote` del `--json`— pero no
151
+ enciende el exit code 2. Antes quedaba en "faltan localmente" para siempre y la señal
152
+ que debería frenarte era ruido permanente.
153
+
154
+ Compensación consciente: dos archivos del mismo tamaño se reportan como iguales
155
+ aunque difieran byte a byte. `shilo diff` y `shilo push` sí comparan contenido.
156
+
157
+ La comparación con "desde la última vez que sincronicé" se apoya en `lastSync`, que
158
+ `push` y los `pull` completos escriben en `.shilo.json`. Si nunca sincronizaste desde
159
+ esta carpeta, no hay línea de base y los cambios remotos se reportan igual.
160
+
161
+ ### Historial (log)
162
+
163
+ ```bash
164
+ shilo log # últimos commits del proyecto
165
+ shilo log src/app.js # sólo los commits que tocaron ese archivo
166
+ shilo log -n 50 --files # más commits, listando archivos de cada uno
167
+ shilo log --json
168
+ ```
169
+
170
+ La paginación es **por commit**, no por archivo: si un archivo no aparece, subí `-n`.
171
+
172
+ ### Diferencias (diff)
173
+
174
+ ```bash
175
+ shilo diff # resumen de todo lo que difiere (sólo metadata)
176
+ shilo diff src/app.js # diff de contenido línea a línea
177
+ shilo diff src/app.js --context 6
178
+ shilo diff --stat
179
+ ```
180
+
181
+ El diff usa el remoto como base (`---`) y tu copia local como destino (`+++`), o sea
182
+ que las líneas `+` son lo que un `push` agregaría. **Exit code 1** = hay diferencias.
183
+
184
+ ### Bajar un proyecto (pull)
185
+
186
+ ```bash
187
+ shilo pull ./mi-carpeta --project <uuid> # baja el repo a una carpeta local
188
+ shilo pull --dry-run # muestra qué cambiaría localmente, sin bajar nada
189
+ shilo pull --yes # sin pedir confirmación
190
+ shilo pull --force # sobrescribe también los archivos que difieren
191
+ shilo pull --include-ignored # baja también lo ignorado localmente
192
+ ```
193
+
194
+ `pull` recorre el árbol remoto (carpeta por carpeta), baja los archivos de texto
195
+ y deja `.shilo.json` listo para un `push` posterior sin `--project`.
196
+
197
+ Protección del trabajo local sin guardar: **no pisa** un archivo local que difiera del
198
+ remoto salvo con `--force`. Sin `--force` no aborta el pull entero — baja lo que es
199
+ seguro, deja afuera los conflictivos y sale con código 1 para que decidas (mirá
200
+ `shilo diff <archivo>`). Los archivos que sólo existen localmente nunca se tocan: el
201
+ pull no borra. `--dry-run` muestra el impacto sobre tu disco (nuevo / sobrescribe /
202
+ se conserva), no el árbol remoto crudo.
203
+
204
+ **Respaldo automático con `--force`.** Antes de pisar un archivo local que difería, el
205
+ pull guarda tu versión en `.shilo/respaldo/<fecha-ISO>/<ruta>` y te dice la carpeta al
206
+ terminar. Va dentro de `.shilo/` —ignorado por defecto— y no como un `<archivo>.local`
207
+ al lado, que el push siguiente subiría: cambiaríamos "perder trabajo" por "publicar tu
208
+ versión en conflicto". Al ir por fecha, dos pulls seguidos no se pisan entre sí.
209
+
210
+ **Lo ignorado localmente no se baja.** Si `.env.*` está en tu `.shiloignore`, que
211
+ alguien haya subido un `.env.ejemplo` desde otra máquina no lo hace aparecer acá: se
212
+ cuenta aparte como "ignorados (no se bajan)", y con `--include-ignored` se bajan igual.
213
+ Esto también arregla el conteo, que antes prometía más archivos de los que escribía.
214
+
215
+ También bloquea nombres que intenten escribir fuera de la carpeta destino (path
216
+ traversal) y saltea binarios con aviso (volverían corruptos). Espejo de `push`:
217
+ `--project` pisa a `.shilo.json`. Con `shilo init --pull` se ata y baja en un solo paso.
218
+
219
+ ### Exportar a Git (export)
220
+
221
+ ```bash
222
+ shilo export ../mi-repo --project <uuid> # la historia entera, como repositorio Git
223
+ shilo export ../mi-repo # otra vez: trae sólo lo nuevo
224
+ git -C ../mi-repo push <remoto> main
225
+ git -C ../mi-repo push <remoto> +refs/notes/shilo
226
+ ```
227
+
228
+ Escribe todos los puntos de guardado de `main` como commits de Git, con su autor y
229
+ su fecha, y la procedencia (agente, tarjeta, propuesta) al pie de cada mensaje y
230
+ completa en `refs/notes/shilo`. Es de sólo lectura para el proyecto. Necesita `git`
231
+ en el PATH; un proyecto público se exporta sin sesión.
232
+
233
+ - **La primera vez**, la carpeta tiene que estar vacía o no existir. Después, repetir
234
+ el comando sobre la misma carpeta baja sólo los archivos nuevos
235
+ (`.git/shilo/mapa` recuerda lo exportado) y exige que lo viejo dé los mismos SHA-1.
236
+ - **Determinístico:** el mismo proyecto da los mismos SHA-1 en cualquier máquina. Los
237
+ commits firman con el nombre congelado de cada persona y
238
+ `<usuario>@users.noreply.shilo.city`, nunca con su correo real.
239
+ - **No pisa trabajo ajeno:** si `main` tiene commits que no salieron de Shilo, o hay
240
+ cambios sin guardar, se niega y no toca nada.
241
+ - **Git no guarda carpetas vacías:** quedan afuera y el informe las lista.
242
+ - La ref de notas se rehace entera en cada exportación: por eso el `+` en su push.
243
+
244
+ ### Servidor alternativo
245
+
246
+ ```bash
247
+ shilo login --base-url https://mi-shilo.vercel.app
248
+ ```
249
+
250
+ ## Credenciales
251
+
252
+ Se guardan en `~/.shilo/config.json` (override con `SHILO_CONFIG_DIR`), **cifradas**
253
+ con AES-256-CBC y con permisos `0600`.
254
+
255
+ - Por defecto la clave de cifrado se deriva de la máquina (hostname + usuario).
256
+ Esto es **ofuscación en reposo**, no secreto fuerte.
257
+ - Para cifrado fuerte, exportá `SHILO_ENCRYPTION_KEY` (una passphrase propia) antes
258
+ de `shilo login`; necesitarás la misma variable para futuras ejecuciones.
259
+
260
+ Se almacena el JWT, el refresh token, la expiración y el email; **no** se guarda la
261
+ contraseña.
262
+
263
+ ### Tokens de acceso personal (PAT)
264
+
265
+ Un token pasado como `--token abc123` queda en el historial del shell y en la lista de
266
+ procesos (`ps` lo muestra a cualquier usuario de la máquina). El CLI lo acepta —es la
267
+ forma que se copia desde la web— pero avisa. Alternativas:
268
+
269
+ ```bash
270
+ cat pat.txt | shilo login --token - # lee el token de stdin
271
+ SHILO_TOKEN=<pat> shilo init --project <uuid> # variable de entorno (lo natural en CI)
272
+ ```
273
+
274
+ ## Variables de entorno
275
+
276
+ | Variable | Efecto |
277
+ | ----------------------- | ------------------------------------------------------------- |
278
+ | `SHILO_CONFIG_DIR` | Directorio de config (default `~/.shilo`). |
279
+ | `SHILO_ENCRYPTION_KEY` | Passphrase para cifrar credenciales (recomendado). |
280
+ | `SHILO_TOKEN` | Token de acceso personal (evita pasarlo por `--token`). |
281
+ | `SHILO_CONCURRENCY` | Requests en paralelo del push (1-32, default 8). |
282
+ | `NO_COLOR` | Desactiva colores ANSI. |
283
+
284
+ ## Roadmap
285
+
286
+ - **Fase 1 — auth** *(hecho)*: `login`, `whoami`, `logout` + config cifrada.
287
+ - **Fase 2 — motor** *(hecho)*: cliente de archivos (`list`/`getMeta`/`create`/`write`/
288
+ `ensurePath`/`commit`) + recorrido de árbol (`.shiloignore`) + detección de binarios.
289
+ - **Fase 3 — push** *(hecho)*: `shilo init` + `shilo push` (crea/actualiza + commit,
290
+ detección de cambios, `.shiloignore`, `--dry-run`).
291
+ - **Fase 4 — pull** *(hecho)*: `shilo pull` (recorrido del árbol remoto con pool,
292
+ guarda anti path-traversal, salto de binarios, colisiones, binding `.shilo.json`,
293
+ `--dry-run`). Cierra el ciclo `pull → trabajar local → push`.
294
+ - **Fase 5 — colaboración asíncrona** *(hecho)*: `status` (comparación barata local vs
295
+ remoto + exit code 2), `log` (historial por proyecto o archivo), `diff` (comparación
296
+ de contenido + exit code 1), atribución de autor en `push --dry-run`, `pull` que
297
+ resuelve conflictos por archivo en vez de abortar, `lastSync` en `.shilo.json` y
298
+ `push --card <id>` para cruzar commits con un tablero externo (Metrónomo).
299
+ - **Fase 6 — robustez del push** *(hecho)*: `.gitignore` (incluidos los anidados),
300
+ poda de repos git por forma, escaneo de secretos con corte del push, preflight
301
+ 100 % offline antes de tocar la red, planificación con un recorrido del árbol
302
+ remoto (una request por carpeta en vez de una por archivo), índice local
303
+ `.shilo/index.json`, lectura perezosa del contenido, reintentos con backoff ante
304
+ 429/5xx, concurrencia adaptativa, barra de progreso y confirmación real en
305
+ `init --push`.
306
+ - **Fase 7 — propuestas** *(hecho)*: `push` deja de escribir archivo por archivo y
307
+ manda una propuesta con la base exacta; el servidor la combina con la versión actual
308
+ y nunca pisa un archivo que otra persona cambió. `base` en `.shilo.json`,
309
+ `registerBlob` para binarios, camino viejo sólo como reserva.
310
+
311
+ ## Exit codes
312
+
313
+ | Comando | Código | Significado |
314
+ | -------- | ------ | -------------------------------------------- |
315
+ | `status` | 2 | El remoto tiene cambios que no tenés. |
316
+ | `diff` | 1 | Hay diferencias entre local y remoto. |
317
+ | `pull` | 1 | Quedaron conflictos sin bajar (o fallas). |
318
+ | `push` | 1 | Se detectaron posibles secretos (nada subido). |
319
+ | cualquiera | 1 | Error. |