shilo-cli 0.8.0 → 0.9.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,22 @@ 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
+
172
189
  ### Diferencias (diff)
173
190
 
174
191
  ```bash
@@ -307,6 +324,8 @@ SHILO_TOKEN=<pat> shilo init --project <uuid> # variable de entorno (lo natura
307
324
  manda una propuesta con la base exacta; el servidor la combina con la versión actual
308
325
  y nunca pisa un archivo que otra persona cambió. `base` en `.shilo.json`,
309
326
  `registerBlob` para binarios, camino viejo sólo como reserva.
327
+ - **Fase 8 — deploys** *(hecho)*: `push --deploy <prod|preview|off>` y
328
+ `shilo deploys` para el deploy automático a Vercel (PLAN_DEPLOY_VERCEL).
310
329
 
311
330
  ## Exit codes
312
331
 
package/bin/shilo.js CHANGED
@@ -24,6 +24,7 @@ ${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
+ deploys Destino e historial de deploys a Vercel del proyecto.
27
28
  help Muestra esta ayuda.
28
29
 
29
30
  ${ui.c.bold('Opciones de login:')}
@@ -55,6 +56,7 @@ ${ui.c.bold('Opciones de push:')}
55
56
  -m, --message <m> Mensaje del commit.
56
57
  --only <ruta> Sube sólo esa ruta (archivo o carpeta). Repetible.
57
58
  --card <id> Id de tarjeta/tablero; se agrega al mensaje como [card: id].
59
+ --deploy <d> prod | preview | off: pisa el deploy automático de este push.
58
60
  --agent <nombre> Declara que lo escribió un agente (ej: claude-code).
59
61
  --model <modelo> Qué modelo, junto con --agent.
60
62
  --session <id> Id de la conversación, para agrupar guardados.
@@ -62,12 +64,17 @@ ${ui.c.bold('Opciones de push:')}
62
64
  --prompt-file <f> Igual, leyendo el pedido de un archivo (máx. 64 KB).
63
65
  --dry-run Muestra qué haría (y quién tocó cada archivo), sin subir nada.
64
66
  --yes No pide confirmación.
65
- --allow-secrets Sube igual los archivos donde se detectaron posibles secretos.
67
+ --allow-secrets Sube igual TODO lo que parezca un secreto (avanzado). Mejor:
68
+ responder en la terminal; la decisión queda en .shilo/.
66
69
  --concurrency <n> Requests en paralelo (1-32, default 8; o SHILO_CONCURRENCY).
67
70
  --no-index Ignora el índice local (.shilo/index.json) y recompara todo.
71
+ --allow-failing-checks
72
+ Si falla un chequeo de shilo.config.json, propone igual
73
+ (queda marcada como fallida y aplicarla exige forzar).
68
74
  --verbose Lista archivo por archivo en vez de resumir.
69
75
  --base-url <url> Servidor Shilo.
70
76
  ${ui.c.dim('Respeta .gitignore y .shiloignore, incluidos los de subcarpetas.')}
77
+ ${ui.c.dim('Exit code 3 = todo guardado, pero espera aprobación (no es un error).')}
71
78
 
72
79
  ${ui.c.bold('Opciones de pull:')}
73
80
  --project <uuid> Proyecto origen (pisa el .shilo.json de la carpeta).
@@ -105,6 +112,11 @@ ${ui.c.bold('Opciones de export:')}
105
112
  --base-url <url> Servidor Shilo.
106
113
  ${ui.c.dim('La primera vez, a una carpeta vacía; después sólo trae lo nuevo. Público sin sesión.')}
107
114
 
115
+ ${ui.c.bold('Opciones de deploys:')}
116
+ -n <N> Cuántos deploys mostrar (1-20, default 10).
117
+ --json Salida JSON.
118
+ --project <uuid> Proyecto (pisa el .shilo.json de la carpeta).
119
+
108
120
  ${ui.c.bold('Globales:')}
109
121
  -v, --version Versión del CLI.
110
122
  -h, --help Ayuda.
@@ -122,6 +134,7 @@ const COMMANDS = {
122
134
  diff: () => require('../src/commands/diff'),
123
135
  review: () => require('../src/commands/review'),
124
136
  export: () => require('../src/commands/export'),
137
+ deploys: () => require('../src/commands/deploys'),
125
138
  };
126
139
 
127
140
  async function main() {
@@ -133,6 +146,7 @@ async function main() {
133
146
  booleans: [
134
147
  'help', 'version', 'h', 'v', 'dry-run', 'yes', 'force', 'push', 'pull',
135
148
  'json', 'short', 'stat', 'files', 'allow-secrets', 'no-index', 'verbose',
149
+ 'allow-failing-checks',
136
150
  'show-ignored', 'include-ignored', 'aprobado', 'reparos', 'rechazado',
137
151
  ],
138
152
  aliases: { h: 'help', v: 'version', m: 'message' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shilo-cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "CLI para sincronizar carpetas locales con repositorios de Shilo: push, pull, status, log y diff.",
5
5
  "keywords": [
6
6
  "shilo",
@@ -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