shilo-cli 0.9.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 +38 -0
- package/bin/shilo.js +16 -2
- package/package.json +4 -3
- package/src/commands/sync.js +240 -0
- package/src/folders.js +141 -0
- package/src/remote.js +3 -0
package/README.md
CHANGED
|
@@ -186,6 +186,41 @@ shilo deploys -n 20 --json
|
|
|
186
186
|
`--deploy` sólo agrega el trailer `[deploy: …]` al mensaje; escribirlo a mano en
|
|
187
187
|
`-m` da lo mismo.
|
|
188
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
|
+
|
|
189
224
|
### Diferencias (diff)
|
|
190
225
|
|
|
191
226
|
```bash
|
|
@@ -326,6 +361,8 @@ SHILO_TOKEN=<pat> shilo init --project <uuid> # variable de entorno (lo natura
|
|
|
326
361
|
`registerBlob` para binarios, camino viejo sólo como reserva.
|
|
327
362
|
- **Fase 8 — deploys** *(hecho)*: `push --deploy <prod|preview|off>` y
|
|
328
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`.
|
|
329
366
|
|
|
330
367
|
## Exit codes
|
|
331
368
|
|
|
@@ -335,4 +372,5 @@ SHILO_TOKEN=<pat> shilo init --project <uuid> # variable de entorno (lo natura
|
|
|
335
372
|
| `diff` | 1 | Hay diferencias entre local y remoto. |
|
|
336
373
|
| `pull` | 1 | Quedaron conflictos sin bajar (o fallas). |
|
|
337
374
|
| `push` | 1 | Se detectaron posibles secretos (nada subido). |
|
|
375
|
+
| `sync` | 1 | Alguna carpeta no se respaldó (falló o se salteó). |
|
|
338
376
|
| cualquiera | 1 | Error. |
|
package/bin/shilo.js
CHANGED
|
@@ -25,7 +25,8 @@ ${ui.c.bold('Comandos:')}
|
|
|
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
27
|
deploys Destino e historial de deploys a Vercel del proyecto.
|
|
28
|
-
|
|
28
|
+
sync [carpeta|uuid] Respalda las carpetas conectadas (--all: todas).
|
|
29
|
+
help Muestra esta ayuda.
|
|
29
30
|
|
|
30
31
|
${ui.c.bold('Opciones de login:')}
|
|
31
32
|
--email <e> Email (si se omite, se pregunta).
|
|
@@ -117,6 +118,18 @@ ${ui.c.bold('Opciones de deploys:')}
|
|
|
117
118
|
--json Salida JSON.
|
|
118
119
|
--project <uuid> Proyecto (pisa el .shilo.json de la carpeta).
|
|
119
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
|
+
|
|
120
133
|
${ui.c.bold('Globales:')}
|
|
121
134
|
-v, --version Versión del CLI.
|
|
122
135
|
-h, --help Ayuda.
|
|
@@ -135,6 +148,7 @@ const COMMANDS = {
|
|
|
135
148
|
review: () => require('../src/commands/review'),
|
|
136
149
|
export: () => require('../src/commands/export'),
|
|
137
150
|
deploys: () => require('../src/commands/deploys'),
|
|
151
|
+
sync: () => require('../src/commands/sync'),
|
|
138
152
|
};
|
|
139
153
|
|
|
140
154
|
async function main() {
|
|
@@ -146,7 +160,7 @@ async function main() {
|
|
|
146
160
|
booleans: [
|
|
147
161
|
'help', 'version', 'h', 'v', 'dry-run', 'yes', 'force', 'push', 'pull',
|
|
148
162
|
'json', 'short', 'stat', 'files', 'allow-secrets', 'no-index', 'verbose',
|
|
149
|
-
'allow-failing-checks',
|
|
163
|
+
'allow-failing-checks', 'all',
|
|
150
164
|
'show-ignored', 'include-ignored', 'aprobado', 'reparos', 'rechazado',
|
|
151
165
|
],
|
|
152
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.
|
|
4
|
-
"description": "CLI para sincronizar carpetas locales con repositorios de Shilo: push, pull, status, log y
|
|
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"
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `shilo sync` — respaldo de las carpetas conectadas (PLAN_SINCRONIZAR, Etapa 1).
|
|
5
|
+
*
|
|
6
|
+
* shilo sync lista las carpetas registradas y su último sync
|
|
7
|
+
* shilo sync --all sube todas, una tras otra
|
|
8
|
+
* shilo sync <carpeta|uuid> sube sólo esa (por ruta o por id de proyecto)
|
|
9
|
+
* shilo sync --scan [raíz] registra las carpetas atadas que haya bajo raíz
|
|
10
|
+
* shilo sync --forget <dir> saca una carpeta del registro (no toca el disco)
|
|
11
|
+
*
|
|
12
|
+
* Sincronizar es SUBIR: cada carpeta pasa por `shilo push --yes`, con todas sus
|
|
13
|
+
* garantías — un conflicto queda como propuesta abierta sin pisar a nadie, y una
|
|
14
|
+
* carpeta con posibles secretos no sube nada. Cada push corre en su propio
|
|
15
|
+
* proceso: lo que falle en una carpeta no arrastra a las demás.
|
|
16
|
+
*
|
|
17
|
+
* Una carpeta sin `base` en su `.shilo.json` se saltea (ver `checkFolder`).
|
|
18
|
+
*
|
|
19
|
+
* Un respaldo no despliega: se manda `--deploy off` salvo que se pida otra cosa,
|
|
20
|
+
* porque con `auto_target = production` un respaldo nocturno iría a producción.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { spawn } = require('child_process');
|
|
26
|
+
const folders = require('../folders');
|
|
27
|
+
const remote = require('../remote');
|
|
28
|
+
const ui = require('../ui');
|
|
29
|
+
|
|
30
|
+
const BIN = path.join(__dirname, '..', '..', 'bin', 'shilo.js');
|
|
31
|
+
// Un prefijo del id alcanza (como un hash corto de git), pero no menos de 4.
|
|
32
|
+
const UUID_RE = /^[0-9a-f-]{4,36}$/i;
|
|
33
|
+
|
|
34
|
+
async function sync(args) {
|
|
35
|
+
if (args.forget !== undefined) return forget(args.forget);
|
|
36
|
+
if (args.scan !== undefined) return scan(args.scan === true ? '.' : args.scan);
|
|
37
|
+
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = folders.load();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
ui.error(e.message);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const target = args._[0];
|
|
47
|
+
if (!args.all && !target) return list(entries);
|
|
48
|
+
|
|
49
|
+
let selected;
|
|
50
|
+
if (args.all) {
|
|
51
|
+
selected = entries;
|
|
52
|
+
} else {
|
|
53
|
+
selected = resolveTarget(target, entries);
|
|
54
|
+
if (!selected) return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (selected.length === 0) {
|
|
58
|
+
ui.warn('No hay carpetas registradas.');
|
|
59
|
+
ui.info('Corré "shilo sync --scan <carpeta>" para encontrar las que ya están conectadas.');
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const deploy = typeof args.deploy === 'string' ? args.deploy : 'off';
|
|
64
|
+
const message =
|
|
65
|
+
typeof args.message === 'string' && args.message.trim()
|
|
66
|
+
? args.message.trim()
|
|
67
|
+
: `Respaldo (shilo sync) ${new Date().toLocaleString('es-AR')}`;
|
|
68
|
+
|
|
69
|
+
const results = [];
|
|
70
|
+
for (const [i, entry] of selected.entries()) {
|
|
71
|
+
ui.info('');
|
|
72
|
+
ui.info(ui.c.bold(`[${i + 1}/${selected.length}] ${entry.dir}`));
|
|
73
|
+
|
|
74
|
+
const check = checkFolder(entry);
|
|
75
|
+
if (check.problem) {
|
|
76
|
+
ui.warn(check.problem);
|
|
77
|
+
results.push({ dir: entry.dir, status: 'salteada', detail: check.problem });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const pushArgs = ['push', entry.dir, '--yes', '-m', message, '--deploy', deploy];
|
|
82
|
+
if (args['dry-run']) pushArgs.push('--dry-run');
|
|
83
|
+
const code = await runChild(pushArgs);
|
|
84
|
+
results.push(resultFor(entry.dir, code));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return summarize(results, !!args['dry-run']);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Código de salida de `push` → fila del resumen. */
|
|
91
|
+
function resultFor(dir, code) {
|
|
92
|
+
if (code === 0) return { dir, status: 'ok', detail: '' };
|
|
93
|
+
// 3 = todo guardado, pero el proyecto exige aprobación: el respaldo está hecho.
|
|
94
|
+
if (code === 3) return { dir, status: 'ok', detail: 'guardado; la propuesta espera aprobación' };
|
|
95
|
+
return { dir, status: 'falló', detail: `push salió con código ${code} (ver arriba)` };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Lista el registro con el estado que dice cada `.shilo.json`. */
|
|
99
|
+
function list(entries) {
|
|
100
|
+
if (entries.length === 0) {
|
|
101
|
+
ui.info('No hay carpetas registradas todavía.');
|
|
102
|
+
ui.info(
|
|
103
|
+
ui.c.dim(
|
|
104
|
+
'Se registran solas con init, push o pull; las ya conectadas se suman con "shilo sync --scan <carpeta>".'
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
ui.info(ui.c.bold(`Carpetas conectadas (${entries.length}):`));
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
const check = checkFolder(entry);
|
|
112
|
+
const when = check.remote && check.remote.lastSync
|
|
113
|
+
? new Date(check.remote.lastSync).toLocaleString('es-AR')
|
|
114
|
+
: 'nunca';
|
|
115
|
+
const state = check.problem ? ui.c.yellow(check.problem) : ui.c.dim(`último sync: ${when}`);
|
|
116
|
+
ui.info(` ${entry.dir}`);
|
|
117
|
+
ui.info(` ${ui.c.dim(entry.projectId || '?')} ${state}`);
|
|
118
|
+
}
|
|
119
|
+
ui.info('');
|
|
120
|
+
ui.info(ui.c.dim('"shilo sync --all" las sube todas; "shilo sync <carpeta|uuid>" sólo una.'));
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* `<carpeta|uuid>` → lista de una entrada. Una carpeta existente que no estaba
|
|
126
|
+
* en el registro se registra (si está atada).
|
|
127
|
+
*/
|
|
128
|
+
function resolveTarget(target, entries) {
|
|
129
|
+
const abs = path.resolve(target);
|
|
130
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
|
|
131
|
+
const r = safeRead(abs);
|
|
132
|
+
if (!r || !r.projectId) {
|
|
133
|
+
ui.error(`La carpeta no está conectada a Shilo (no tiene .shilo.json): ${abs}`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
folders.add(abs, r.projectId);
|
|
137
|
+
return [{ dir: abs, projectId: r.projectId }];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (UUID_RE.test(target)) {
|
|
141
|
+
const t = target.toLowerCase();
|
|
142
|
+
const matches = entries.filter((e) => e.projectId && e.projectId.toLowerCase().startsWith(t));
|
|
143
|
+
if (matches.length > 0) return matches; // un proyecto atado en dos carpetas: las dos
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ui.error(`No encontré "${target}" ni como carpeta ni como proyecto registrado.`);
|
|
147
|
+
ui.info('Corré "shilo sync" para ver la lista.');
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** ¿Se puede sincronizar? Devuelve `{ remote, problem }`. */
|
|
152
|
+
function checkFolder(entry) {
|
|
153
|
+
if (!fs.existsSync(entry.dir)) {
|
|
154
|
+
return { remote: null, problem: 'la carpeta ya no existe ("shilo sync --forget" para sacarla)' };
|
|
155
|
+
}
|
|
156
|
+
const r = safeRead(entry.dir);
|
|
157
|
+
if (!r || !r.projectId) {
|
|
158
|
+
return { remote: null, problem: 'ya no tiene .shilo.json (¿se desconectó?)' };
|
|
159
|
+
}
|
|
160
|
+
if (r.projectId !== entry.projectId) folders.addQuietly(entry.dir, r.projectId);
|
|
161
|
+
// Sin `base`, el push toma el main actual como base y trata como cambios
|
|
162
|
+
// todos los archivos locales que difieren: una copia vieja pisaría trabajo
|
|
163
|
+
// más nuevo (pasó el 2026-10-12). Eso no puede pasar sin que alguien mire.
|
|
164
|
+
if (!r.base) return { remote: r, problem: NO_BASE };
|
|
165
|
+
return { remote: r, problem: null };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const NO_BASE =
|
|
169
|
+
'requiere atención: sin "base" en .shilo.json no sé si está al día. Revisala con ' +
|
|
170
|
+
'"shilo status" y "shilo diff", y subila a mano una vez con "shilo push" (o "shilo pull" si está atrasada)';
|
|
171
|
+
|
|
172
|
+
function safeRead(dir) {
|
|
173
|
+
try {
|
|
174
|
+
return remote.read(dir);
|
|
175
|
+
} catch (_) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function scan(root) {
|
|
181
|
+
const abs = path.resolve(root);
|
|
182
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
183
|
+
ui.error(`No es una carpeta válida: ${abs}`);
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
ui.info(ui.c.dim(`Buscando carpetas conectadas bajo ${abs}…`));
|
|
187
|
+
const found = folders.scan(abs);
|
|
188
|
+
let added = 0;
|
|
189
|
+
for (const f of found) {
|
|
190
|
+
const isNew = folders.add(f.dir, f.projectId);
|
|
191
|
+
if (isNew) added++;
|
|
192
|
+
ui.info(` ${isNew ? ui.c.green('+') : ui.c.dim('=')} ${f.dir}`);
|
|
193
|
+
}
|
|
194
|
+
if (found.length === 0) ui.info('No encontré ninguna carpeta con .shilo.json.');
|
|
195
|
+
else ui.success(`${found.length} encontrada(s), ${added} nueva(s) en el registro.`);
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function forget(dir) {
|
|
200
|
+
if (dir === true) {
|
|
201
|
+
ui.error('Falta la carpeta: shilo sync --forget <carpeta>');
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
if (folders.remove(dir)) {
|
|
205
|
+
ui.success(`Sacada del registro: ${path.resolve(dir)}`);
|
|
206
|
+
ui.info(ui.c.dim('No se tocó nada en disco; un push o pull futuro la vuelve a registrar.'));
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
ui.warn(`No estaba en el registro: ${path.resolve(dir)}`);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function runChild(argv) {
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
const child = spawn(process.execPath, [BIN, ...argv], { stdio: 'inherit' });
|
|
216
|
+
child.on('error', (e) => {
|
|
217
|
+
ui.error(`No se pudo lanzar el push: ${e.message}`);
|
|
218
|
+
resolve(1);
|
|
219
|
+
});
|
|
220
|
+
child.on('close', (code) => resolve(typeof code === 'number' ? code : 1));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function summarize(results, dryRun) {
|
|
225
|
+
ui.info('');
|
|
226
|
+
ui.info(ui.c.bold('Resumen del respaldo:'));
|
|
227
|
+
for (const r of results) {
|
|
228
|
+
const mark =
|
|
229
|
+
r.status === 'ok' ? ui.c.green('✓') : r.status === 'salteada' ? ui.c.yellow('!') : ui.c.red('✗');
|
|
230
|
+
ui.info(` ${mark} ${r.dir}${r.detail ? ui.c.dim(` — ${r.detail}`) : ''}`);
|
|
231
|
+
}
|
|
232
|
+
const ok = results.filter((r) => r.status === 'ok').length;
|
|
233
|
+
const bad = results.length - ok;
|
|
234
|
+
const verb = dryRun ? 'revisada(s) (--dry-run: no se subió nada)' : 'respaldada(s)';
|
|
235
|
+
if (bad === 0) ui.success(`${ok} carpeta(s) ${verb}.`);
|
|
236
|
+
else ui.warn(`${ok} ${verb}, ${bad} con problemas.`);
|
|
237
|
+
return bad === 0 ? 0 : 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = sync;
|
package/src/folders.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Registro local de carpetas conectadas: `~/.shilo/carpetas.json`.
|
|
5
|
+
*
|
|
6
|
+
* El servidor conoce los proyectos, pero no en qué carpetas de qué máquina
|
|
7
|
+
* está atado cada uno: eso sólo lo sabe el disco (cada `.shilo.json`). Este
|
|
8
|
+
* registro es la lista que `shilo sync --all` recorre (PLAN_SINCRONIZAR, Etapa 1).
|
|
9
|
+
*
|
|
10
|
+
* Se llena solo: `remote.write` —el único lugar donde se escribe un
|
|
11
|
+
* `.shilo.json`, lo usen `init`, `pull` o `push`— registra la carpeta. Las que
|
|
12
|
+
* se ataron antes de que existiera el registro se suman con `shilo sync --scan`.
|
|
13
|
+
*
|
|
14
|
+
* Estructura en disco:
|
|
15
|
+
* {
|
|
16
|
+
* "version": 1,
|
|
17
|
+
* "carpetas": [
|
|
18
|
+
* { "dir": "C:\\...\\boda", "projectId": "<uuid>", "addedAt": "<ISO>" }
|
|
19
|
+
* ]
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* No guarda credenciales ni el estado de sincronización: el estado vive en el
|
|
23
|
+
* `.shilo.json` de cada carpeta (`lastSync`), que es la fuente de verdad.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const config = require('./config');
|
|
29
|
+
const remote = require('./remote');
|
|
30
|
+
|
|
31
|
+
const FILENAME = 'carpetas.json';
|
|
32
|
+
|
|
33
|
+
// Lo que `--scan` no recorre: pesado y nunca contiene una carpeta atada propia.
|
|
34
|
+
const SCAN_SKIP = new Set(['node_modules', '.git', '.shilo', '.vercel', 'dist', 'build']);
|
|
35
|
+
|
|
36
|
+
function registryPath() {
|
|
37
|
+
return path.join(config.configDir(), FILENAME);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** En Windows y macOS las rutas no distinguen mayúsculas: una carpeta, una entrada. */
|
|
41
|
+
function keyOf(dir) {
|
|
42
|
+
const abs = path.resolve(dir);
|
|
43
|
+
return process.platform === 'linux' ? abs : abs.toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function load() {
|
|
47
|
+
const p = registryPath();
|
|
48
|
+
if (!fs.existsSync(p)) return [];
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
raw = JSON.parse(fs.readFileSync(p, 'utf8').replace(/^/, ''));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
throw new Error(`Registro de carpetas corrupto en ${p}: ${e.message}`);
|
|
54
|
+
}
|
|
55
|
+
return Array.isArray(raw.carpetas) ? raw.carpetas.filter((c) => c && typeof c.dir === 'string') : [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function save(list) {
|
|
59
|
+
fs.mkdirSync(config.configDir(), { recursive: true });
|
|
60
|
+
const p = registryPath();
|
|
61
|
+
fs.writeFileSync(p, JSON.stringify({ version: 1, carpetas: list }, null, 2) + '\n', 'utf8');
|
|
62
|
+
return p;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Registra (o actualiza el proyecto de) una carpeta. Devuelve `true` si era nueva.
|
|
67
|
+
*/
|
|
68
|
+
function add(dir, projectId) {
|
|
69
|
+
const abs = path.resolve(dir);
|
|
70
|
+
const list = load();
|
|
71
|
+
const k = keyOf(abs);
|
|
72
|
+
const found = list.find((c) => keyOf(c.dir) === k);
|
|
73
|
+
if (found) {
|
|
74
|
+
if (projectId && found.projectId !== projectId) {
|
|
75
|
+
found.projectId = projectId;
|
|
76
|
+
save(list);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
list.push({ dir: abs, projectId: projectId || null, addedAt: new Date().toISOString() });
|
|
81
|
+
save(list);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Versión para `remote.write`: registrar nunca puede romper un push o un pull
|
|
87
|
+
* (config de sólo lectura, registro corrupto, etc.).
|
|
88
|
+
*/
|
|
89
|
+
function addQuietly(dir, projectId) {
|
|
90
|
+
try {
|
|
91
|
+
add(dir, projectId);
|
|
92
|
+
} catch (_) {
|
|
93
|
+
/* el registro es una comodidad, no un requisito */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Saca una carpeta del registro. Devuelve `true` si estaba. */
|
|
98
|
+
function remove(dir) {
|
|
99
|
+
const k = keyOf(dir);
|
|
100
|
+
const list = load();
|
|
101
|
+
const rest = list.filter((c) => keyOf(c.dir) !== k);
|
|
102
|
+
if (rest.length === list.length) return false;
|
|
103
|
+
save(rest);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Busca `.shilo.json` bajo `root` (incluida) hasta `maxDepth` niveles.
|
|
109
|
+
* No entra en una carpeta atada: un repo de Shilo no contiene otro.
|
|
110
|
+
* Devuelve `[{ dir, projectId }]`.
|
|
111
|
+
*/
|
|
112
|
+
function scan(root, { maxDepth = 6 } = {}) {
|
|
113
|
+
const found = [];
|
|
114
|
+
const walk = (dir, depth) => {
|
|
115
|
+
let info = null;
|
|
116
|
+
try {
|
|
117
|
+
info = remote.read(dir);
|
|
118
|
+
} catch (_) {
|
|
119
|
+
/* `.shilo.json` corrupto: no es una carpeta sincronizable */
|
|
120
|
+
}
|
|
121
|
+
if (info && info.projectId) {
|
|
122
|
+
found.push({ dir, projectId: info.projectId });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (depth >= maxDepth) return;
|
|
126
|
+
let entries;
|
|
127
|
+
try {
|
|
128
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
129
|
+
} catch (_) {
|
|
130
|
+
return; // sin permiso de lectura
|
|
131
|
+
}
|
|
132
|
+
for (const e of entries) {
|
|
133
|
+
if (!e.isDirectory() || SCAN_SKIP.has(e.name)) continue;
|
|
134
|
+
walk(path.join(dir, e.name), depth + 1);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
walk(path.resolve(root), 0);
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { registryPath, load, add, addQuietly, remove, scan, keyOf };
|
package/src/remote.js
CHANGED
|
@@ -72,6 +72,9 @@ function write(dir, { projectId, baseURL, lastSync, base }) {
|
|
|
72
72
|
if (lastSync) onDisk.lastSync = lastSync;
|
|
73
73
|
if (base) onDisk.base = base;
|
|
74
74
|
fs.writeFileSync(p, JSON.stringify(onDisk, null, 2) + '\n', 'utf8');
|
|
75
|
+
// Toda carpeta atada queda en el registro de `shilo sync --all`. Require
|
|
76
|
+
// perezoso: `folders` ya depende de este módulo.
|
|
77
|
+
require('./folders').addQuietly(dir, projectId);
|
|
75
78
|
return p;
|
|
76
79
|
}
|
|
77
80
|
|