shilo-cli 0.2.0 → 0.3.1
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 +93 -2
- package/bin/shilo.js +41 -2
- package/package.json +6 -2
- package/src/ShiloClient.js +22 -0
- package/src/commands/diff.js +158 -0
- package/src/commands/init.js +6 -1
- package/src/commands/log.js +155 -0
- package/src/commands/pull.js +239 -0
- package/src/commands/push.js +190 -75
- package/src/commands/status.js +187 -0
- package/src/context.js +82 -0
- package/src/remote.js +29 -3
- package/src/sync.js +344 -0
- package/src/textdiff.js +196 -0
package/README.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
CLI para subir carpetas locales a repositorios de [Shilo](https://shilo-six.vercel.app),
|
|
4
4
|
con una experiencia tipo git: `shilo login` una vez y después `shilo push`.
|
|
5
5
|
|
|
6
|
-
> **Estado:** Fase
|
|
7
|
-
>
|
|
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`.
|
|
8
9
|
|
|
9
10
|
## Requisitos
|
|
10
11
|
|
|
@@ -36,6 +37,7 @@ shilo init --project <uuid> # ata la carpeta al repo (escribe .shilo.json)
|
|
|
36
37
|
shilo push # sube: crea/actualiza + commit
|
|
37
38
|
shilo push -m "mi mensaje" # con mensaje de commit
|
|
38
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
|
|
39
41
|
shilo push --yes # sin pedir confirmación
|
|
40
42
|
shilo push ./otra-carpeta --project <uuid> # sin init, proyecto explícito
|
|
41
43
|
```
|
|
@@ -44,6 +46,78 @@ Sólo se suben archivos de **texto**; binarios, oversize (>25 MB) y nombres con
|
|
|
44
46
|
caracteres no permitidos se saltan con aviso. Los archivos sin cambios no se
|
|
45
47
|
re-suben. Ignorá rutas con un `.shiloignore` (sintaxis estilo `.gitignore`).
|
|
46
48
|
|
|
49
|
+
En `--dry-run` (y antes de la confirmación interactiva) cada archivo modificado
|
|
50
|
+
muestra **quién lo tocó por última vez y cuándo**, y se avisa explícitamente si vas
|
|
51
|
+
a pisar el trabajo de otra persona. Con token de acceso personal el autor se muestra
|
|
52
|
+
igual, pero no se puede marcar cuál sos vos (el PAT no expone tu id de usuario).
|
|
53
|
+
|
|
54
|
+
### ¿Estoy desactualizado? (status)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
shilo status # compara local vs remoto SIN bajar contenido
|
|
58
|
+
shilo status --short # sólo rutas
|
|
59
|
+
shilo status --json # para scripts
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Es el comando barato: usa sólo listados de carpeta (una request por carpeta, cero
|
|
63
|
+
descargas de contenido) y compara por **ruta + tamaño**. Informa qué tiene el remoto
|
|
64
|
+
que vos no (con autor y fecha del último commit de cada archivo), qué tenés local sin
|
|
65
|
+
subir, y marca aparte los archivos que cambiaron **de los dos lados** — el caso
|
|
66
|
+
peligroso. **Exit code 2** = estás atrasado respecto del remoto, útil en scripts.
|
|
67
|
+
|
|
68
|
+
Compensación consciente: dos archivos del mismo tamaño se reportan como iguales
|
|
69
|
+
aunque difieran byte a byte. `shilo diff` y `shilo push` sí comparan contenido.
|
|
70
|
+
|
|
71
|
+
La comparación con "desde la última vez que sincronicé" se apoya en `lastSync`, que
|
|
72
|
+
`push` y los `pull` completos escriben en `.shilo.json`. Si nunca sincronizaste desde
|
|
73
|
+
esta carpeta, no hay línea de base y los cambios remotos se reportan igual.
|
|
74
|
+
|
|
75
|
+
### Historial (log)
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
shilo log # últimos commits del proyecto
|
|
79
|
+
shilo log src/app.js # sólo los commits que tocaron ese archivo
|
|
80
|
+
shilo log -n 50 --files # más commits, listando archivos de cada uno
|
|
81
|
+
shilo log --json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
La paginación es **por commit**, no por archivo: si un archivo no aparece, subí `-n`.
|
|
85
|
+
|
|
86
|
+
### Diferencias (diff)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
shilo diff # resumen de todo lo que difiere (sólo metadata)
|
|
90
|
+
shilo diff src/app.js # diff de contenido línea a línea
|
|
91
|
+
shilo diff src/app.js --context 6
|
|
92
|
+
shilo diff --stat
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
El diff usa el remoto como base (`---`) y tu copia local como destino (`+++`), o sea
|
|
96
|
+
que las líneas `+` son lo que un `push` agregaría. **Exit code 1** = hay diferencias.
|
|
97
|
+
|
|
98
|
+
### Bajar un proyecto (pull)
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
shilo pull ./mi-carpeta --project <uuid> # baja el repo a una carpeta local
|
|
102
|
+
shilo pull --dry-run # muestra qué cambiaría localmente, sin bajar nada
|
|
103
|
+
shilo pull --yes # sin pedir confirmación
|
|
104
|
+
shilo pull --force # sobrescribe también los archivos que difieren
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`pull` recorre el árbol remoto (carpeta por carpeta), baja los archivos de texto
|
|
108
|
+
y deja `.shilo.json` listo para un `push` posterior sin `--project`.
|
|
109
|
+
|
|
110
|
+
Protección del trabajo local sin guardar: **no pisa** un archivo local que difiera del
|
|
111
|
+
remoto salvo con `--force`. Sin `--force` no aborta el pull entero — baja lo que es
|
|
112
|
+
seguro, deja afuera los conflictivos y sale con código 1 para que decidas (mirá
|
|
113
|
+
`shilo diff <archivo>`). Los archivos que sólo existen localmente nunca se tocan: el
|
|
114
|
+
pull no borra. `--dry-run` muestra el impacto sobre tu disco (nuevo / sobrescribe /
|
|
115
|
+
se conserva), no el árbol remoto crudo.
|
|
116
|
+
|
|
117
|
+
También bloquea nombres que intenten escribir fuera de la carpeta destino (path
|
|
118
|
+
traversal) y saltea binarios con aviso (volverían corruptos). Espejo de `push`:
|
|
119
|
+
`--project` pisa a `.shilo.json`. Con `shilo init --pull` se ata y baja en un solo paso.
|
|
120
|
+
|
|
47
121
|
### Servidor alternativo
|
|
48
122
|
|
|
49
123
|
```bash
|
|
@@ -78,3 +152,20 @@ contraseña.
|
|
|
78
152
|
`ensurePath`/`commit`) + recorrido de árbol (`.shiloignore`) + detección de binarios.
|
|
79
153
|
- **Fase 3 — push** *(hecho)*: `shilo init` + `shilo push` (crea/actualiza + commit,
|
|
80
154
|
detección de cambios, `.shiloignore`, `--dry-run`).
|
|
155
|
+
- **Fase 4 — pull** *(hecho)*: `shilo pull` (recorrido del árbol remoto con pool,
|
|
156
|
+
guarda anti path-traversal, salto de binarios, colisiones, binding `.shilo.json`,
|
|
157
|
+
`--dry-run`). Cierra el ciclo `pull → trabajar local → push`.
|
|
158
|
+
- **Fase 5 — colaboración asíncrona** *(hecho)*: `status` (comparación barata local vs
|
|
159
|
+
remoto + exit code 2), `log` (historial por proyecto o archivo), `diff` (comparación
|
|
160
|
+
de contenido + exit code 1), atribución de autor en `push --dry-run`, `pull` que
|
|
161
|
+
resuelve conflictos por archivo en vez de abortar, `lastSync` en `.shilo.json` y
|
|
162
|
+
`push --card <id>` para cruzar commits con un tablero externo (Metrónomo).
|
|
163
|
+
|
|
164
|
+
## Exit codes
|
|
165
|
+
|
|
166
|
+
| Comando | Código | Significado |
|
|
167
|
+
| -------- | ------ | -------------------------------------------- |
|
|
168
|
+
| `status` | 2 | El remoto tiene cambios que no tenés. |
|
|
169
|
+
| `diff` | 1 | Hay diferencias entre local y remoto. |
|
|
170
|
+
| `pull` | 1 | Quedaron conflictos sin bajar (o fallas). |
|
|
171
|
+
| cualquiera | 1 | Error. |
|
package/bin/shilo.js
CHANGED
|
@@ -17,7 +17,11 @@ ${ui.c.bold('Comandos:')}
|
|
|
17
17
|
whoami Muestra la sesión activa.
|
|
18
18
|
logout Cierra sesión (borra credenciales locales).
|
|
19
19
|
init [carpeta] Ata una carpeta a un repo de Shilo (--project <uuid>).
|
|
20
|
+
status [carpeta] Compara local vs remoto sin bajar nada (¿estoy desactualizado?).
|
|
20
21
|
push [carpeta] Sube una carpeta al repo de Shilo (crea/actualiza + commit).
|
|
22
|
+
pull [carpeta] Baja un repo de Shilo a una carpeta local (deja .shilo.json).
|
|
23
|
+
log [archivo] Historial de commits del proyecto o de un archivo.
|
|
24
|
+
diff [archivo] Diferencias entre tu copia local y la remota.
|
|
21
25
|
help Muestra esta ayuda.
|
|
22
26
|
|
|
23
27
|
${ui.c.bold('Opciones de login:')}
|
|
@@ -30,16 +34,44 @@ ${ui.c.bold('Opciones de init:')}
|
|
|
30
34
|
--project <uuid> Id del proyecto de Shilo (requerido).
|
|
31
35
|
--token <pat> Token de acceso personal (evita el "login" previo).
|
|
32
36
|
--push Sube la carpeta apenas queda atada.
|
|
37
|
+
--pull Baja la carpeta apenas queda atada.
|
|
33
38
|
--base-url <url> Servidor Shilo.
|
|
34
39
|
--force No verifica acceso ni pregunta antes de sobrescribir.
|
|
35
40
|
|
|
41
|
+
${ui.c.bold('Opciones de status:')}
|
|
42
|
+
--project <uuid> Proyecto a comparar (pisa el .shilo.json de la carpeta).
|
|
43
|
+
--short Sólo las rutas, sin autor ni fecha.
|
|
44
|
+
--json Salida JSON (para scripts).
|
|
45
|
+
--base-url <url> Servidor Shilo.
|
|
46
|
+
${ui.c.dim('Exit code 2 = el remoto tiene cambios que no tenés.')}
|
|
47
|
+
|
|
36
48
|
${ui.c.bold('Opciones de push:')}
|
|
37
49
|
--project <uuid> Proyecto destino (pisa el .shilo.json de la carpeta).
|
|
38
50
|
-m, --message <m> Mensaje del commit.
|
|
39
|
-
--
|
|
51
|
+
--card <id> Id de tarjeta/tablero; se agrega al mensaje como [card: id].
|
|
52
|
+
--dry-run Muestra qué haría (y quién tocó cada archivo), sin subir nada.
|
|
40
53
|
--yes No pide confirmación.
|
|
41
54
|
--base-url <url> Servidor Shilo.
|
|
42
55
|
|
|
56
|
+
${ui.c.bold('Opciones de pull:')}
|
|
57
|
+
--project <uuid> Proyecto origen (pisa el .shilo.json de la carpeta).
|
|
58
|
+
--dry-run Muestra qué cambiaría localmente, sin bajar nada.
|
|
59
|
+
--yes No pide confirmación.
|
|
60
|
+
--force Sobrescribe también los archivos locales que difieren.
|
|
61
|
+
--base-url <url> Servidor Shilo.
|
|
62
|
+
|
|
63
|
+
${ui.c.bold('Opciones de log:')}
|
|
64
|
+
-n <N> Cuántos commits mirar (1-100, default 20).
|
|
65
|
+
--files Lista los archivos de cada commit.
|
|
66
|
+
--json Salida JSON.
|
|
67
|
+
--project <uuid> Proyecto (pisa el .shilo.json de la carpeta).
|
|
68
|
+
|
|
69
|
+
${ui.c.bold('Opciones de diff:')}
|
|
70
|
+
--context <n> Líneas de contexto (0-20, default 3).
|
|
71
|
+
--stat Sólo el resumen de líneas agregadas/quitadas.
|
|
72
|
+
--project <uuid> Proyecto (pisa el .shilo.json de la carpeta).
|
|
73
|
+
${ui.c.dim('Sin archivo, resume todo lo que difiere. Exit code 1 = hay diferencias.')}
|
|
74
|
+
|
|
43
75
|
${ui.c.bold('Globales:')}
|
|
44
76
|
-v, --version Versión del CLI.
|
|
45
77
|
-h, --help Ayuda.
|
|
@@ -51,6 +83,10 @@ const COMMANDS = {
|
|
|
51
83
|
logout: () => require('../src/commands/logout'),
|
|
52
84
|
init: () => require('../src/commands/init'),
|
|
53
85
|
push: () => require('../src/commands/push'),
|
|
86
|
+
pull: () => require('../src/commands/pull'),
|
|
87
|
+
status: () => require('../src/commands/status'),
|
|
88
|
+
log: () => require('../src/commands/log'),
|
|
89
|
+
diff: () => require('../src/commands/diff'),
|
|
54
90
|
};
|
|
55
91
|
|
|
56
92
|
async function main() {
|
|
@@ -59,7 +95,10 @@ async function main() {
|
|
|
59
95
|
// 'push' tiene que estar acá: la lista de booleanos es global, y sin
|
|
60
96
|
// registrarlo `shilo init --project X --push .` se comería el `.` como
|
|
61
97
|
// valor de --push, perdiendo el positional de la carpeta.
|
|
62
|
-
booleans: [
|
|
98
|
+
booleans: [
|
|
99
|
+
'help', 'version', 'h', 'v', 'dry-run', 'yes', 'force', 'push', 'pull',
|
|
100
|
+
'json', 'short', 'stat', 'files',
|
|
101
|
+
],
|
|
63
102
|
aliases: { h: 'help', v: 'version', m: 'message' },
|
|
64
103
|
});
|
|
65
104
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shilo-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI para
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "CLI para sincronizar carpetas locales con repositorios de Shilo: push, pull, status, log y diff.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shilo",
|
|
7
7
|
"cli",
|
|
8
8
|
"push",
|
|
9
|
+
"pull",
|
|
10
|
+
"status",
|
|
11
|
+
"diff",
|
|
12
|
+
"sync",
|
|
9
13
|
"upload",
|
|
10
14
|
"repositorios"
|
|
11
15
|
],
|
package/src/ShiloClient.js
CHANGED
|
@@ -402,6 +402,28 @@ class ShiloClient {
|
|
|
402
402
|
return currentCreds;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Lista el historial de commits del proyecto (más nuevo primero).
|
|
407
|
+
* `includeFiles` pide además los `commit_files` de cada commit, que es lo que
|
|
408
|
+
* permite saber quién tocó cada archivo por última vez.
|
|
409
|
+
* Devuelve `{ commits, hasMore, credentials }`.
|
|
410
|
+
*/
|
|
411
|
+
async listCommits(projectId, credentials, { limit = 20, page = 1, includeFiles = false } = {}) {
|
|
412
|
+
const params = new URLSearchParams({ limit: String(limit), page: String(page) });
|
|
413
|
+
if (includeFiles) params.set('include', 'files');
|
|
414
|
+
const url = `${this.baseURL}/api/commits/${projectId}?${params.toString()}`;
|
|
415
|
+
const { res, data, credentials: creds } = await this._authFetch(url, credentials);
|
|
416
|
+
if (!res.ok) {
|
|
417
|
+
throw new ShiloError(
|
|
418
|
+
`No se pudo listar el historial: ${this._errMessage(data, `HTTP ${res.status}`)}`,
|
|
419
|
+
{ status: res.status, body: data }
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const commits = data && Array.isArray(data.commits) ? data.commits : [];
|
|
423
|
+
const hasMore = !!(data && data.pagination && data.pagination.hasMore);
|
|
424
|
+
return { commits, hasMore, credentials: creds };
|
|
425
|
+
}
|
|
426
|
+
|
|
405
427
|
/**
|
|
406
428
|
* Crea un commit con la lista de cambios de archivos (máx 100 por lote).
|
|
407
429
|
* `changes`: [{ file_id, action, previous_storage_path?, new_storage_path? }].
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const config = require('../config');
|
|
4
|
+
const ui = require('../ui');
|
|
5
|
+
const sync = require('../sync');
|
|
6
|
+
const textdiff = require('../textdiff');
|
|
7
|
+
const { resolveContext } = require('../context');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* shilo diff [archivo] [--context <n>] [--stat] [--project <uuid>]
|
|
11
|
+
*
|
|
12
|
+
* Compara la versión local con la remota. Sin argumento hace un resumen de todo
|
|
13
|
+
* lo que difiere (barato: sólo listados). Con un archivo baja SOLO ese archivo y
|
|
14
|
+
* muestra el diff línea a línea.
|
|
15
|
+
*
|
|
16
|
+
* Convención de signos: el remoto es la base, lo local es el cambio. O sea
|
|
17
|
+
* `+` es lo que un push agregaría al remoto, `-` lo que borraría. Así el diff se
|
|
18
|
+
* lee igual que el push que vendría después.
|
|
19
|
+
*/
|
|
20
|
+
async function diff(args) {
|
|
21
|
+
const filePath = typeof args._[0] === 'string' ? args._[0] : null;
|
|
22
|
+
const ctx = resolveContext({ ...args, _: [] });
|
|
23
|
+
if (!ctx.ok) return ctx.code;
|
|
24
|
+
|
|
25
|
+
const { dir, projectId, baseURL, cfg, client, mgr } = ctx;
|
|
26
|
+
|
|
27
|
+
const context = parseContext(args.context);
|
|
28
|
+
if (context === null) {
|
|
29
|
+
ui.error('El valor de --context debe ser un entero entre 0 y 20.');
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return filePath
|
|
34
|
+
? diffOne({ dir, projectId, cfg, client, mgr, filePath, context, stat: !!args.stat })
|
|
35
|
+
: diffAll({ dir, projectId, baseURL, cfg, client, mgr });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Diff de un archivo puntual: una sola descarga.
|
|
40
|
+
*/
|
|
41
|
+
async function diffOne({ dir, projectId, cfg, client, mgr, filePath, context, stat }) {
|
|
42
|
+
const repoPath = sync.toRepoPath(dir, filePath);
|
|
43
|
+
if (!repoPath) {
|
|
44
|
+
ui.error(`La ruta queda fuera de la carpeta: ${filePath}`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const absPath = require('path').resolve(dir, repoPath);
|
|
49
|
+
const localText = sync.readLocalText(absPath);
|
|
50
|
+
|
|
51
|
+
let remoteText = null;
|
|
52
|
+
let exists = false;
|
|
53
|
+
try {
|
|
54
|
+
const creds = await mgr.valid();
|
|
55
|
+
const meta = await client.getFileMeta(projectId, repoPath, creds);
|
|
56
|
+
exists = meta.exists && meta.file && meta.file.type !== 'folder';
|
|
57
|
+
if (exists) {
|
|
58
|
+
if (!sync.isTextMime(meta.file.mime_type)) {
|
|
59
|
+
ui.error(`"${repoPath}" no es texto en el remoto (${meta.file.mime_type}): no se puede diffear.`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const read = await client.readFile(projectId, repoPath, await mgr.valid());
|
|
63
|
+
remoteText = read.file && read.file.content != null ? read.file.content : '';
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
ui.error(`No se pudo leer "${repoPath}" del remoto: ${e.message}`);
|
|
67
|
+
return 1;
|
|
68
|
+
} finally {
|
|
69
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!exists && localText === null) {
|
|
73
|
+
ui.error(`"${repoPath}" no existe ni local ni remotamente.`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (!exists) {
|
|
77
|
+
ui.info(`${ui.c.green('+')} ${repoPath} ${ui.c.dim('(sólo local: un push lo crearía)')}`);
|
|
78
|
+
remoteText = '';
|
|
79
|
+
}
|
|
80
|
+
if (localText === null) {
|
|
81
|
+
ui.info(`${ui.c.red('-')} ${repoPath} ${ui.c.dim('(sólo remoto: un pull lo bajaría)')}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = textdiff.diffText(remoteText || '', localText === null ? '' : localText, { context });
|
|
85
|
+
|
|
86
|
+
if (result.identical) {
|
|
87
|
+
ui.success(`Sin diferencias: ${repoPath}`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ui.info('');
|
|
92
|
+
ui.info(`${ui.c.dim('--- remoto')} ${ui.c.dim(repoPath)}`);
|
|
93
|
+
ui.info(`${ui.c.dim('+++ local')} ${ui.c.dim(absPath)}`);
|
|
94
|
+
|
|
95
|
+
if (stat || result.tooBig) {
|
|
96
|
+
for (const line of textdiff.renderDiff({ ...result, hunks: [] }, { paint: ui.c })) {
|
|
97
|
+
ui.info(line);
|
|
98
|
+
}
|
|
99
|
+
if (!result.tooBig) {
|
|
100
|
+
ui.info(` ${ui.c.green(`+${result.added}`)} / ${ui.c.red(`-${result.removed}`)} líneas`);
|
|
101
|
+
}
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const line of textdiff.renderDiff(result, { paint: ui.c })) ui.info(line);
|
|
106
|
+
ui.info('');
|
|
107
|
+
ui.info(` ${ui.c.green(`+${result.added}`)} / ${ui.c.red(`-${result.removed}`)} líneas`);
|
|
108
|
+
// Exit 1 = "hay diferencias", como `git diff --exit-code`.
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resumen de diferencias de toda la carpeta, sin bajar contenido.
|
|
114
|
+
*/
|
|
115
|
+
async function diffAll({ dir, projectId, baseURL, cfg, client, mgr }) {
|
|
116
|
+
ui.info(`Proyecto ${ui.c.bold(projectId)} en ${ui.c.cyan(baseURL)} — comparando ...`);
|
|
117
|
+
|
|
118
|
+
const local = sync.walkLocal(dir);
|
|
119
|
+
let tree;
|
|
120
|
+
try {
|
|
121
|
+
tree = await sync.walkRemote(client, projectId, mgr);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
ui.error(`No se pudo consultar el proyecto: ${e.message}`);
|
|
124
|
+
return 1;
|
|
125
|
+
} finally {
|
|
126
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const cmp = sync.compareTrees(local.files, tree.files);
|
|
130
|
+
const total = cmp.onlyLocal.length + cmp.onlyRemote.length + cmp.differing.length;
|
|
131
|
+
|
|
132
|
+
ui.info('');
|
|
133
|
+
if (total === 0) {
|
|
134
|
+
ui.success('Sin diferencias de tamaño ni de archivos entre local y remoto.');
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const item of cmp.onlyLocal) ui.info(` ${ui.c.green('+')} ${item.repoPath} ${ui.c.dim('(sólo local)')}`);
|
|
139
|
+
for (const item of cmp.onlyRemote) ui.info(` ${ui.c.red('-')} ${item.repoPath} ${ui.c.dim('(sólo remoto)')}`);
|
|
140
|
+
for (const item of cmp.differing) {
|
|
141
|
+
const delta = item.local.size - item.remote.size;
|
|
142
|
+
const sign = delta > 0 ? `+${delta}` : String(delta);
|
|
143
|
+
ui.info(` ${ui.c.yellow('~')} ${item.repoPath} ${ui.c.dim(`(${sign} bytes)`)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ui.info('');
|
|
147
|
+
ui.info(ui.c.dim(`${total} archivo(s) con diferencias. Usá "shilo diff <archivo>" para ver el detalle.`));
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseContext(raw) {
|
|
152
|
+
if (raw === undefined || raw === null || raw === '') return 3;
|
|
153
|
+
const n = Number(raw);
|
|
154
|
+
if (!Number.isInteger(n) || n < 0 || n > 20) return null;
|
|
155
|
+
return n;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = diff;
|
package/src/commands/init.js
CHANGED
|
@@ -23,7 +23,7 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
|
23
23
|
*
|
|
24
24
|
* Con --token guarda las credenciales antes de verificar, de modo que el
|
|
25
25
|
* comando de una línea que se copia desde la web funcione sin "login" previo.
|
|
26
|
-
* Con --push, sube la carpeta al terminar.
|
|
26
|
+
* Con --push, sube la carpeta al terminar; con --pull, la baja.
|
|
27
27
|
*/
|
|
28
28
|
async function init(args) {
|
|
29
29
|
const dir = path.resolve(args._[0] || '.');
|
|
@@ -103,6 +103,11 @@ async function init(args) {
|
|
|
103
103
|
return require('./push')({ ...args, _: [dir], yes: true });
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
if (args.pull) {
|
|
107
|
+
// Espejo de --push: el usuario ya confirmó al pasar --pull.
|
|
108
|
+
return require('./pull')({ ...args, _: [dir], yes: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
106
111
|
ui.info(`Ahora podés subir con: ${ui.c.bold('shilo push')}`);
|
|
107
112
|
return 0;
|
|
108
113
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
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 = 20;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* shilo log [archivo] [-n <N>] [--project <uuid>] [--files] [--json]
|
|
12
|
+
*
|
|
13
|
+
* Historial de commits del proyecto, o sólo los que tocaron `archivo`.
|
|
14
|
+
* Sólo lectura: no toca ni el disco ni el remoto.
|
|
15
|
+
*
|
|
16
|
+
* Ojo con el filtro por archivo: el backend pagina commits, no cambios, así que
|
|
17
|
+
* filtramos del lado del cliente sobre las últimas páginas. Si un archivo no se
|
|
18
|
+
* toca hace mucho, puede necesitar un `-n` más grande para aparecer; el comando
|
|
19
|
+
* avisa cuando se queda sin historial explorado.
|
|
20
|
+
*/
|
|
21
|
+
async function log(args) {
|
|
22
|
+
// El positional de `log` es un archivo, no una carpeta: el contexto se
|
|
23
|
+
// resuelve siempre sobre el directorio actual.
|
|
24
|
+
const filePath = typeof args._[0] === 'string' ? args._[0] : null;
|
|
25
|
+
const ctx = resolveContext({ ...args, _: [] });
|
|
26
|
+
if (!ctx.ok) return ctx.code;
|
|
27
|
+
|
|
28
|
+
const { dir, projectId, baseURL, cfg, client, mgr } = ctx;
|
|
29
|
+
const asJson = !!args.json;
|
|
30
|
+
const showFiles = !!args.files || !!filePath;
|
|
31
|
+
|
|
32
|
+
const limit = parseLimit(args.n || args.number, DEFAULT_LIMIT);
|
|
33
|
+
if (limit === null) {
|
|
34
|
+
ui.error('El valor de -n debe ser un entero entre 1 y 100.');
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Si se pidió un archivo puntual, primero resolvemos su id en el remoto.
|
|
39
|
+
let targetId = null;
|
|
40
|
+
let repoPath = null;
|
|
41
|
+
if (filePath) {
|
|
42
|
+
repoPath = sync.toRepoPath(dir, filePath);
|
|
43
|
+
if (!repoPath) {
|
|
44
|
+
ui.error(`La ruta queda fuera de la carpeta: ${filePath}`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const creds = await mgr.valid();
|
|
49
|
+
const meta = await client.getFileMeta(projectId, repoPath, creds);
|
|
50
|
+
if (!meta.exists || !meta.file) {
|
|
51
|
+
ui.error(`No existe en el remoto: ${repoPath}`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
targetId = meta.file.id;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
ui.error(`No se pudo resolver "${repoPath}": ${e.message}`);
|
|
57
|
+
return 1;
|
|
58
|
+
} finally {
|
|
59
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let result;
|
|
64
|
+
try {
|
|
65
|
+
const creds = await mgr.valid();
|
|
66
|
+
result = await client.listCommits(projectId, creds, {
|
|
67
|
+
limit,
|
|
68
|
+
includeFiles: showFiles,
|
|
69
|
+
});
|
|
70
|
+
} catch (e) {
|
|
71
|
+
ui.error(e.message);
|
|
72
|
+
return 1;
|
|
73
|
+
} finally {
|
|
74
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let commits = result.commits;
|
|
78
|
+
if (targetId) {
|
|
79
|
+
commits = commits.filter((c) => (c.files || []).some((cf) => cf.files && cf.files.id === targetId));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (asJson) {
|
|
83
|
+
console.log(
|
|
84
|
+
JSON.stringify(
|
|
85
|
+
{
|
|
86
|
+
projectId,
|
|
87
|
+
path: repoPath,
|
|
88
|
+
commits: commits.map((c) => ({
|
|
89
|
+
hash: c.hash,
|
|
90
|
+
message: c.message,
|
|
91
|
+
author: sync.describeAuthor(c.users),
|
|
92
|
+
date: c.created_at,
|
|
93
|
+
files: (c.files || []).map((cf) => ({
|
|
94
|
+
name: cf.files ? cf.files.name : null,
|
|
95
|
+
action: cf.action,
|
|
96
|
+
})),
|
|
97
|
+
})),
|
|
98
|
+
hasMore: result.hasMore,
|
|
99
|
+
},
|
|
100
|
+
null,
|
|
101
|
+
2
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const scope = repoPath ? `de ${ui.c.bold(repoPath)}` : 'del proyecto';
|
|
108
|
+
ui.info(`Historial ${scope} — ${ui.c.cyan(baseURL)}`);
|
|
109
|
+
ui.info('');
|
|
110
|
+
|
|
111
|
+
if (commits.length === 0) {
|
|
112
|
+
ui.info(
|
|
113
|
+
repoPath
|
|
114
|
+
? `Sin commits para "${repoPath}" en los últimos ${limit} del proyecto.`
|
|
115
|
+
: 'El proyecto todavía no tiene commits.'
|
|
116
|
+
);
|
|
117
|
+
if (result.hasMore) ui.info(ui.c.dim(`Probá con -n mayor (hay más historial).`));
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const commit of commits) {
|
|
122
|
+
const hash = commit.hash ? String(commit.hash).slice(0, 8) : '(sin hash)';
|
|
123
|
+
ui.info(`${ui.c.yellow(hash)} ${ui.c.bold(sync.describeAuthor(commit.users))} ${ui.c.dim(sync.formatDate(commit.created_at))}`);
|
|
124
|
+
ui.info(` ${commit.message}`);
|
|
125
|
+
if (showFiles && Array.isArray(commit.files) && commit.files.length > 0) {
|
|
126
|
+
const shown = repoPath
|
|
127
|
+
? commit.files.filter((cf) => cf.files && cf.files.id === targetId)
|
|
128
|
+
: commit.files;
|
|
129
|
+
for (const cf of shown) {
|
|
130
|
+
ui.info(` ${ui.c.dim(`${actionMark(cf.action)} ${cf.files ? cf.files.name : '?'}`)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
ui.info('');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (result.hasMore) {
|
|
137
|
+
ui.info(ui.c.dim(`(mostrando los últimos ${limit} commits del proyecto; hay más)`));
|
|
138
|
+
}
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function actionMark(action) {
|
|
143
|
+
if (action === 'added') return '+';
|
|
144
|
+
if (action === 'deleted') return '-';
|
|
145
|
+
return '~';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseLimit(raw, fallback) {
|
|
149
|
+
if (raw === undefined || raw === null || raw === '') return fallback;
|
|
150
|
+
const n = Number(raw);
|
|
151
|
+
if (!Number.isInteger(n) || n < 1 || n > 100) return null;
|
|
152
|
+
return n;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = log;
|