shilo-cli 0.8.0 → 0.10.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
@@ -36,6 +36,7 @@ cd mi-carpeta
36
36
  shilo init --project <uuid> # ata la carpeta al repo (escribe .shilo.json)
37
37
  shilo push # sube: crea/actualiza + commit
38
38
  shilo push -m "mi mensaje" # con mensaje de commit
39
+ shilo push --deploy prod # este push despliega a producción ("[deploy: prod]")
39
40
  shilo push --dry-run # muestra qué haría sin subir
40
41
  shilo push --card ABC-123 # agrega "[card: ABC-123]" al mensaje del commit
41
42
  shilo push --agent claude-code --model claude-opus-5 # lo escribió un agente, y queda anotado
@@ -169,6 +170,57 @@ shilo log --json
169
170
 
170
171
  La paginación es **por commit**, no por archivo: si un archivo no aparece, subí `-n`.
171
172
 
173
+ ### Deploys a Vercel (deploys)
174
+
175
+ Si el owner conectó el proyecto con Vercel (pestaña **Deploys** en la web), cada
176
+ push que cambia algo dispara un deploy: por defecto a **preview**. Producción sólo
177
+ si se pide explícitamente.
178
+
179
+ ```bash
180
+ shilo push --deploy prod # este push va a producción
181
+ shilo push --deploy off # este push no despliega
182
+ shilo deploys # destino y últimos deploys, con estado y URL
183
+ shilo deploys -n 20 --json
184
+ ```
185
+
186
+ `--deploy` sólo agrega el trailer `[deploy: …]` al mensaje; escribirlo a mano en
187
+ `-m` da lo mismo.
188
+
189
+ ### Respaldar todas las carpetas (sync)
190
+
191
+ ```bash
192
+ shilo sync # lista las carpetas conectadas y su último sync
193
+ shilo sync --all # sube todas, una tras otra
194
+ shilo sync ./boda # sólo esa carpeta
195
+ shilo sync a4c2 # o por id de proyecto (alcanza un prefijo)
196
+ shilo sync --scan ~/proyectos # registra las carpetas ya conectadas que haya ahí
197
+ shilo sync --forget ./vieja # la saca del registro (no toca el disco)
198
+ shilo sync --all --dry-run # qué subiría cada una, sin subir nada
199
+ ```
200
+
201
+ El CLI lleva un registro de carpetas conectadas en `~/.shilo/carpetas.json`. Se llena
202
+ solo: cada `init`, `push` o `pull` registra su carpeta. Las que se conectaron antes
203
+ de esta versión se suman una vez con `--scan`, que busca `.shilo.json` hasta 6 niveles
204
+ abajo (no entra en `node_modules`, `.git` ni `dist`).
205
+
206
+ Sincronizar es **subir**: cada carpeta pasa por `shilo push --yes`, con sus mismas
207
+ garantías (un conflicto queda como propuesta abierta, una carpeta con posibles
208
+ secretos no sube nada). Cada push corre en su propio proceso, así que si una carpeta
209
+ falla las demás siguen, y al final hay un resumen. **Un respaldo no despliega:**
210
+ manda `--deploy off` salvo que pases otro `--deploy`.
211
+
212
+ **Una carpeta sin `base` se saltea.** `base` es la versión del proyecto sobre la que
213
+ está tu copia; la escriben `push` y `pull`. Sin ella, un push no puede saber si tu
214
+ copia está vieja y subiría tus archivos como cambios, pisando lo más nuevo. Esas
215
+ carpetas aparecen como "requiere atención": revisalas con `shilo status` y
216
+ `shilo diff`, subilas a mano una vez con `shilo push` (o bajá lo nuevo con
217
+ `shilo pull`) y desde ahí entran solas al respaldo.
218
+
219
+ Exit code 1 = alguna carpeta falló o se salteó. Una propuesta que queda esperando
220
+ aprobación (exit 3 de `push`) cuenta como respaldada. El plan de las etapas
221
+ siguientes (respaldo programado, vista en la web) está en `PLAN_SINCRONIZAR.md` en la
222
+ raíz del repo.
223
+
172
224
  ### Diferencias (diff)
173
225
 
174
226
  ```bash
@@ -307,6 +359,10 @@ SHILO_TOKEN=<pat> shilo init --project <uuid> # variable de entorno (lo natura
307
359
  manda una propuesta con la base exacta; el servidor la combina con la versión actual
308
360
  y nunca pisa un archivo que otra persona cambió. `base` en `.shilo.json`,
309
361
  `registerBlob` para binarios, camino viejo sólo como reserva.
362
+ - **Fase 8 — deploys** *(hecho)*: `push --deploy <prod|preview|off>` y
363
+ `shilo deploys` para el deploy automático a Vercel (PLAN_DEPLOY_VERCEL).
364
+ - **Fase 9 — respaldo** *(Etapa 1 hecha)*: registro de carpetas conectadas y
365
+ `shilo sync [--all]`. Lo que sigue, en `PLAN_SINCRONIZAR.md`.
310
366
 
311
367
  ## Exit codes
312
368
 
@@ -316,4 +372,5 @@ SHILO_TOKEN=<pat> shilo init --project <uuid> # variable de entorno (lo natura
316
372
  | `diff` | 1 | Hay diferencias entre local y remoto. |
317
373
  | `pull` | 1 | Quedaron conflictos sin bajar (o fallas). |
318
374
  | `push` | 1 | Se detectaron posibles secretos (nada subido). |
375
+ | `sync` | 1 | Alguna carpeta no se respaldó (falló o se salteó). |
319
376
  | cualquiera | 1 | Error. |
package/bin/shilo.js CHANGED
@@ -24,7 +24,9 @@ ${ui.c.bold('Comandos:')}
24
24
  diff [archivo] Diferencias entre tu copia local y la remota.
25
25
  review [versión] Quién revisó una versión, o dejar tu revisión.
26
26
  export <carpeta> Escribe la historia del proyecto como un repositorio Git.
27
- help Muestra esta ayuda.
27
+ deploys Destino e historial de deploys a Vercel del proyecto.
28
+ sync [carpeta|uuid] Respalda las carpetas conectadas (--all: todas).
29
+ help Muestra esta ayuda.
28
30
 
29
31
  ${ui.c.bold('Opciones de login:')}
30
32
  --email <e> Email (si se omite, se pregunta).
@@ -55,6 +57,7 @@ ${ui.c.bold('Opciones de push:')}
55
57
  -m, --message <m> Mensaje del commit.
56
58
  --only <ruta> Sube sólo esa ruta (archivo o carpeta). Repetible.
57
59
  --card <id> Id de tarjeta/tablero; se agrega al mensaje como [card: id].
60
+ --deploy <d> prod | preview | off: pisa el deploy automático de este push.
58
61
  --agent <nombre> Declara que lo escribió un agente (ej: claude-code).
59
62
  --model <modelo> Qué modelo, junto con --agent.
60
63
  --session <id> Id de la conversación, para agrupar guardados.
@@ -62,12 +65,17 @@ ${ui.c.bold('Opciones de push:')}
62
65
  --prompt-file <f> Igual, leyendo el pedido de un archivo (máx. 64 KB).
63
66
  --dry-run Muestra qué haría (y quién tocó cada archivo), sin subir nada.
64
67
  --yes No pide confirmación.
65
- --allow-secrets Sube igual los archivos donde se detectaron posibles secretos.
68
+ --allow-secrets Sube igual TODO lo que parezca un secreto (avanzado). Mejor:
69
+ responder en la terminal; la decisión queda en .shilo/.
66
70
  --concurrency <n> Requests en paralelo (1-32, default 8; o SHILO_CONCURRENCY).
67
71
  --no-index Ignora el índice local (.shilo/index.json) y recompara todo.
72
+ --allow-failing-checks
73
+ Si falla un chequeo de shilo.config.json, propone igual
74
+ (queda marcada como fallida y aplicarla exige forzar).
68
75
  --verbose Lista archivo por archivo en vez de resumir.
69
76
  --base-url <url> Servidor Shilo.
70
77
  ${ui.c.dim('Respeta .gitignore y .shiloignore, incluidos los de subcarpetas.')}
78
+ ${ui.c.dim('Exit code 3 = todo guardado, pero espera aprobación (no es un error).')}
71
79
 
72
80
  ${ui.c.bold('Opciones de pull:')}
73
81
  --project <uuid> Proyecto origen (pisa el .shilo.json de la carpeta).
@@ -105,6 +113,23 @@ ${ui.c.bold('Opciones de export:')}
105
113
  --base-url <url> Servidor Shilo.
106
114
  ${ui.c.dim('La primera vez, a una carpeta vacía; después sólo trae lo nuevo. Público sin sesión.')}
107
115
 
116
+ ${ui.c.bold('Opciones de deploys:')}
117
+ -n <N> Cuántos deploys mostrar (1-20, default 10).
118
+ --json Salida JSON.
119
+ --project <uuid> Proyecto (pisa el .shilo.json de la carpeta).
120
+
121
+ ${ui.c.bold('Opciones de sync:')}
122
+ (sin argumentos) Lista las carpetas conectadas y su último sync.
123
+ --all Sube todas las carpetas registradas, una tras otra.
124
+ --scan [raíz] Registra las carpetas con .shilo.json bajo raíz (default: la actual).
125
+ --forget <carpeta> La saca del registro (no toca nada en disco).
126
+ -m, --message <m> Mensaje de los commits (default: "Respaldo (shilo sync) <fecha>").
127
+ --deploy <d> prod | preview | off (default off: un respaldo no despliega).
128
+ --dry-run Muestra qué subiría cada carpeta, sin subir nada.
129
+ ${ui.c.dim('Se registran solas con init, push o pull. Una carpeta sin "base" en su')}
130
+ ${ui.c.dim('.shilo.json se saltea: subila a mano una vez con "shilo push".')}
131
+ ${ui.c.dim('Exit code 1 = alguna carpeta falló o se salteó.')}
132
+
108
133
  ${ui.c.bold('Globales:')}
109
134
  -v, --version Versión del CLI.
110
135
  -h, --help Ayuda.
@@ -122,6 +147,8 @@ const COMMANDS = {
122
147
  diff: () => require('../src/commands/diff'),
123
148
  review: () => require('../src/commands/review'),
124
149
  export: () => require('../src/commands/export'),
150
+ deploys: () => require('../src/commands/deploys'),
151
+ sync: () => require('../src/commands/sync'),
125
152
  };
126
153
 
127
154
  async function main() {
@@ -133,6 +160,7 @@ async function main() {
133
160
  booleans: [
134
161
  'help', 'version', 'h', 'v', 'dry-run', 'yes', 'force', 'push', 'pull',
135
162
  'json', 'short', 'stat', 'files', 'allow-secrets', 'no-index', 'verbose',
163
+ 'allow-failing-checks', 'all',
136
164
  'show-ignored', 'include-ignored', 'aprobado', 'reparos', 'rechazado',
137
165
  ],
138
166
  aliases: { h: 'help', v: 'version', m: 'message' },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shilo-cli",
3
- "version": "0.8.0",
4
- "description": "CLI para sincronizar carpetas locales con repositorios de Shilo: push, pull, status, log y diff.",
3
+ "version": "0.10.0",
4
+ "description": "CLI para sincronizar carpetas locales con repositorios de Shilo: push, pull, status, log, diff y sync.",
5
5
  "keywords": [
6
6
  "shilo",
7
7
  "cli",
@@ -25,7 +25,8 @@
25
25
  "README.md"
26
26
  ],
27
27
  "scripts": {
28
- "start": "node bin/shilo.js"
28
+ "start": "node bin/shilo.js",
29
+ "test": "node --test"
29
30
  },
30
31
  "engines": {
31
32
  "node": ">=18"
@@ -791,6 +791,29 @@ class ShiloClient {
791
791
  };
792
792
  }
793
793
 
794
+ /**
795
+ * Destino e historial de deploys a Vercel (PLAN_DEPLOY_VERCEL). Devuelve
796
+ * `{ connected, can_manage, target, deployments, credentials }`. Un servidor
797
+ * sin deploys responde 404: se marca `notSupported`.
798
+ */
799
+ async getDeploys(projectId, credentials) {
800
+ const url = `${this.baseURL}/api/deploys/${encodeURIComponent(projectId)}`;
801
+ const { res, data, credentials: creds } = await this._authFetch(url, credentials);
802
+ if (!res.ok) {
803
+ throw new ShiloError(
804
+ `No se pudieron leer los deploys: ${this._errMessage(data, `HTTP ${res.status}`)}`,
805
+ { status: res.status, body: data, notSupported: res.status === 404 || res.status === 405 }
806
+ );
807
+ }
808
+ return {
809
+ connected: !!(data && data.connected),
810
+ can_manage: !!(data && data.can_manage),
811
+ target: (data && data.target) || null,
812
+ deployments: data && Array.isArray(data.deployments) ? data.deployments : [],
813
+ credentials: creds,
814
+ };
815
+ }
816
+
794
817
  /**
795
818
  * Propuestas (Etapa 3). Guardar es proponer: el cliente dice de qué versión
796
819
  * partió (`base`, el oid de la versión actual que vio) y qué cambió, y el
@@ -818,6 +841,9 @@ class ShiloClient {
818
841
  return {
819
842
  main: (data && data.main) || null,
820
843
  proposals: data && Array.isArray(data.proposals) ? data.proposals : [],
844
+ // Servidores anteriores no los mandan: quien llama tiene que tolerar null.
845
+ viewer: (data && data.viewer) || null,
846
+ project: (data && data.project) || null,
821
847
  credentials: creds,
822
848
  };
823
849
  }
@@ -836,11 +862,16 @@ class ShiloClient {
836
862
  * `[card: …]` del mensaje, que se queda porque es el formato de todo lo ya
837
863
  * guardado; la clave es la que se puede consultar.
838
864
  *
839
- * Devuelve `{ applied, empty, proposal, conflicts, blockedBy, main, message,
840
- * warnings, credentials }`. Los 200 y 202 son respuestas normales (aplicada /
865
+ * `chequeos` es el resultado de los chequeos del proyecto (MU-93,
866
+ * `checks.js`): la propuesta lo lleva y una fallida no se aplica sola.
867
+ *
868
+ * Devuelve `{ applied, empty, proposal, conflicts, blockedBy, lineas,
869
+ * chequeos, piezas, main, message, warnings, credentials }`; `lineas` son las rutas
870
+ * que el servidor combinó por líneas (MU-92) y `piezas`, las piezas USEE
871
+ * que dejarían de pasar el Nivel 1 (MU-94). Los 200 y 202 son respuestas normales (aplicada /
841
872
  * quedó abierta); cualquier otro estado es error.
842
873
  */
843
- async propose(projectId, { base, title, summary, changes, agent, prompt, card }, credentials) {
874
+ async propose(projectId, { base, title, summary, changes, agent, prompt, card, chequeos }, credentials) {
844
875
  const url = `${this.baseURL}/api/projects/proposals`;
845
876
  const { res, data, credentials: creds } = await this._authFetch(url, credentials, {
846
877
  method: 'POST',
@@ -853,6 +884,7 @@ class ShiloClient {
853
884
  ...(agent ? { agent } : {}),
854
885
  ...(prompt ? { prompt } : {}),
855
886
  ...(card ? { card } : {}),
887
+ ...(chequeos ? { chequeos } : {}),
856
888
  },
857
889
  });
858
890
  if (!res.ok && res.status !== 202) {
@@ -867,6 +899,9 @@ class ShiloClient {
867
899
  proposal: (data && data.proposal) || null,
868
900
  conflicts: data && Array.isArray(data.conflicts) ? data.conflicts : [],
869
901
  blockedBy: data && Array.isArray(data.blocked_by) ? data.blocked_by : [],
902
+ lineas: data && Array.isArray(data.lineas) ? data.lineas : [],
903
+ chequeos: (data && data.chequeos) || null,
904
+ piezas: data && Array.isArray(data.piezas) ? data.piezas : [],
870
905
  main: (data && data.main) || null,
871
906
  message: (data && data.message) || '',
872
907
  warnings: data && Array.isArray(data.warnings) ? data.warnings : [],
@@ -876,14 +911,16 @@ class ShiloClient {
876
911
 
877
912
  /**
878
913
  * Resuelve una propuesta abierta: `apply` (dueño) o `discard` (dueño o
879
- * autor). Devuelve `{ applied, proposal, conflicts, main, credentials }`; con
914
+ * autor). `resolutions` ({ ruta: 'actual' | 'propuesta' | 'ambas' }) decide
915
+ * los archivos en conflicto al aplicar; `force` aplica aunque sus chequeos
916
+ * o sus piezas USEE hayan fallado (MU-93, MU-94). Devuelve `{ applied, proposal, conflicts, main, credentials }`; con
880
917
  * `apply` un 409 significa que sigue en conflicto y viene con los datos.
881
918
  */
882
- async resolveProposal(projectId, id, action, credentials) {
919
+ async resolveProposal(projectId, id, action, credentials, { resolutions = null, force = false } = {}) {
883
920
  const url = `${this.baseURL}/api/projects/proposals`;
884
921
  const { res, data, credentials: creds } = await this._authFetch(url, credentials, {
885
922
  method: 'POST',
886
- body: { projectId, id, action },
923
+ body: { projectId, id, action, ...(resolutions ? { resolutions } : {}), ...(force ? { force: true } : {}) },
887
924
  });
888
925
  const stillOpen = action === 'apply' && res.status === 409 && data && data.proposal;
889
926
  if (!res.ok && !stillOpen) {
@@ -896,6 +933,8 @@ class ShiloClient {
896
933
  applied: !!(data && data.applied),
897
934
  proposal: (data && data.proposal) || null,
898
935
  conflicts: data && Array.isArray(data.conflicts) ? data.conflicts : [],
936
+ lineas: data && Array.isArray(data.lineas) ? data.lineas : [],
937
+ piezas: data && Array.isArray(data.piezas) ? data.piezas : [],
899
938
  main: (data && data.main) || null,
900
939
  message: (data && data.message) || '',
901
940
  credentials: creds,
package/src/checks.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Chequeos de propuesta (EP-S2, MU-93).
3
+ *
4
+ * Un proyecto declara en `shilo.config.json` los comandos que tienen que pasar
5
+ * antes de proponer (`"chequeos": ["npm test"]`). `shilo push` los corre en la
6
+ * carpeta, uno por uno y en orden, con la salida a la vista, y el resultado
7
+ * viaja con la propuesta: la web la muestra verificada o fallida, y aplicar una
8
+ * fallida exige forzar.
9
+ *
10
+ * Se corta en el primero que falla, como un hook de git: lo que viene después
11
+ * suele depender de lo anterior (instalar, compilar, probar) y correrlo sólo
12
+ * hace más largo el error. El resultado registra lo que se corrió.
13
+ *
14
+ * `run` se inyecta para probarlo sin lanzar procesos.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const childProcess = require('child_process');
20
+
21
+ // Un chequeo colgado no puede dejar colgado el push: a los diez minutos se corta
22
+ // y cuenta como fallido.
23
+ const CHECK_TIMEOUT_MS = 10 * 60 * 1000;
24
+
25
+ function spawnCheck(comando, dir) {
26
+ const r = childProcess.spawnSync(comando, {
27
+ cwd: dir,
28
+ shell: true,
29
+ stdio: 'inherit',
30
+ timeout: CHECK_TIMEOUT_MS,
31
+ windowsHide: true,
32
+ });
33
+ // Sin `status` = no llegó a terminar (no se pudo lanzar, o se cortó por tiempo).
34
+ return { codigo: typeof r.status === 'number' ? r.status : null };
35
+ }
36
+
37
+ /**
38
+ * @param {string} dir - la carpeta donde se corren
39
+ * @param {string[]} comandos
40
+ * @param {{ run?: Function, now?: Function, onStart?: Function }} [opts]
41
+ * @returns {{ resultado: 'ok'|'fallido', comandos: Array<{ comando, ok, codigo?, ms }> }}
42
+ */
43
+ function runChecks(dir, comandos, { run = spawnCheck, now = Date.now, onStart = () => {} } = {}) {
44
+ const out = [];
45
+ for (const comando of comandos) {
46
+ onStart(comando, out.length + 1, comandos.length);
47
+ const t0 = now();
48
+ const { codigo } = run(comando, dir);
49
+ const entry = { comando, ok: codigo === 0, ms: Math.max(0, Math.round(now() - t0)) };
50
+ if (codigo !== null) entry.codigo = codigo;
51
+ out.push(entry);
52
+ if (!entry.ok) break;
53
+ }
54
+ return { resultado: out.every((c) => c.ok) ? 'ok' : 'fallido', comandos: out };
55
+ }
56
+
57
+ module.exports = { runChecks, CHECK_TIMEOUT_MS };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const config = require('../config');
4
+ const ui = require('../ui');
5
+ const sync = require('../sync');
6
+ const { resolveContext } = require('../context');
7
+
8
+ const DEFAULT_LIMIT = 10;
9
+
10
+ const STATUS = {
11
+ queued: (s) => ui.c.dim(s),
12
+ running: (s) => ui.c.cyan(s),
13
+ uploading: (s) => ui.c.cyan(s),
14
+ building: (s) => ui.c.cyan(s),
15
+ ready: (s) => ui.c.green(s),
16
+ error: (s) => ui.c.red(s),
17
+ canceled: (s) => ui.c.dim(s),
18
+ superseded: (s) => ui.c.dim(s),
19
+ };
20
+
21
+ const AUTO = {
22
+ off: 'sólo manual',
23
+ preview: 'preview en cada guardado',
24
+ production: 'producción en cada guardado',
25
+ };
26
+
27
+ /**
28
+ * shilo deploys [-n <N>] [--project <uuid>] [--json]
29
+ *
30
+ * Destino e historial de deploys a Vercel del proyecto (PLAN_DEPLOY_VERCEL,
31
+ * Etapa 4.2). Sólo lectura. El servidor devuelve como mucho los últimos 20.
32
+ */
33
+ async function deploys(args) {
34
+ const ctx = resolveContext({ ...args, _: [] });
35
+ if (!ctx.ok) return ctx.code;
36
+
37
+ const { projectId, baseURL, cfg, client, mgr } = ctx;
38
+
39
+ const limit = parseLimit(args.n || args.number);
40
+ if (limit === null) {
41
+ ui.error('El valor de -n debe ser un entero entre 1 y 20.');
42
+ return 1;
43
+ }
44
+
45
+ let info;
46
+ try {
47
+ info = await client.getDeploys(projectId, await mgr.valid());
48
+ } catch (e) {
49
+ ui.error(e.notSupported ? 'Este servidor de Shilo todavía no tiene deploys.' : e.message);
50
+ return 1;
51
+ } finally {
52
+ config.persistCredsIfChanged(cfg, mgr.current);
53
+ }
54
+
55
+ const list = info.deployments.slice(0, limit);
56
+
57
+ if (args.json) {
58
+ console.log(JSON.stringify({ projectId, connected: info.connected, target: info.target, deployments: list }, null, 2));
59
+ return 0;
60
+ }
61
+
62
+ ui.info(`Deploys del proyecto — ${ui.c.cyan(baseURL)}`);
63
+ if (!info.target) {
64
+ ui.info('');
65
+ ui.info('Este proyecto no despliega a ningún lado. El owner lo configura en la');
66
+ ui.info('pestaña "Deploys" del proyecto en la web.');
67
+ return 0;
68
+ }
69
+
70
+ const t = info.target;
71
+ ui.info(`Vercel: ${ui.c.bold(t.vercel_project_name || t.vercel_project)} · ${AUTO[t.auto_target] || t.auto_target}`);
72
+ if (!info.connected) {
73
+ ui.warn('La conexión con Vercel no está activa: el owner tiene que volver a conectar.');
74
+ }
75
+ ui.info('');
76
+
77
+ if (list.length === 0) {
78
+ ui.info('Todavía no hubo deploys.');
79
+ return 0;
80
+ }
81
+
82
+ for (const d of list) {
83
+ const paint = STATUS[d.status] || ((s) => s);
84
+ const commit = d.commit_oid ? String(d.commit_oid).slice(0, 8) : '--------';
85
+ const target = d.target === 'production' ? ui.c.bold('prod ') : 'preview';
86
+ const card = d.card ? ui.c.dim(` [card: ${d.card}]`) : '';
87
+ ui.info(`${paint(pad(d.status, 10))} ${target} ${ui.c.yellow(commit)} ${ui.c.dim(sync.formatDate(d.created_at))}${card}`);
88
+ if (d.url) ui.info(` ${d.url}`);
89
+ if (d.error) ui.info(` ${ui.c.red(d.error)}`);
90
+ }
91
+ return 0;
92
+ }
93
+
94
+ function pad(s, n) {
95
+ return String(s).padEnd(n);
96
+ }
97
+
98
+ function parseLimit(raw) {
99
+ if (raw === undefined || raw === null || raw === '') return DEFAULT_LIMIT;
100
+ const n = Number(raw);
101
+ if (!Number.isInteger(n) || n < 1 || n > 20) return null;
102
+ return n;
103
+ }
104
+
105
+ module.exports = deploys;
@@ -126,7 +126,7 @@ async function diffAll({ dir, projectId, baseURL, cfg, client, mgr }) {
126
126
  config.persistCredsIfChanged(cfg, mgr.current);
127
127
  }
128
128
 
129
- const cmp = sync.compareTrees(local.files, tree.files);
129
+ const cmp = sync.refineByHash(sync.compareTrees(local.files, tree.files), { dir, projectId, baseURL });
130
130
 
131
131
  // Un archivo ignorado localmente no es una diferencia que haya que resolver:
132
132
  // el push no lo sube y el pull no lo baja. Se nombra aparte, no como "sólo
@@ -106,6 +106,14 @@ async function init(args) {
106
106
  const written = remote.write(dir, { projectId, baseURL });
107
107
  ui.success(`Carpeta atada al proyecto ${ui.c.bold(projectId)}.`);
108
108
  ui.info(ui.c.dim(`Remote guardado en ${written}`));
109
+ // Se lee siempre (ver walk.js), pero no decirlo hacía que la gente copiara
110
+ // a mano cada línea del .gitignore al .shiloignore.
111
+ if (fs.existsSync(path.join(dir, '.gitignore'))) {
112
+ ui.info(
113
+ ui.c.dim('Se respeta tu .gitignore: lo que git ignora tampoco se sube. ') +
114
+ ui.c.dim('.shiloignore es sólo para exclusiones extra.')
115
+ );
116
+ }
109
117
 
110
118
  // --push/--pull no implican confirmación: el usuario pidió *encadenar* el
111
119
  // comando, no saltearse su preflight. Sin --yes, push muestra el plan (y el