shilo-cli 0.5.0 → 0.7.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 +20 -8
- package/bin/shilo.js +21 -2
- package/package.json +1 -1
- package/src/ShiloClient.js +265 -4
- package/src/commands/pull.js +56 -12
- package/src/commands/push.js +695 -338
- package/src/pushindex.js +6 -1
- package/src/remote.js +15 -3
- package/src/textguard.js +70 -19
package/src/pushindex.js
CHANGED
|
@@ -40,7 +40,12 @@ function indexPath(dir) {
|
|
|
40
40
|
|
|
41
41
|
/** SHA-256 hex de un contenido utf8. */
|
|
42
42
|
function hashContent(content) {
|
|
43
|
-
|
|
43
|
+
// Un binario llega como Buffer y un archivo de texto como string. Se hashean
|
|
44
|
+
// los BYTES en los dos casos: el hash tiene que ser el mismo que calcula el
|
|
45
|
+
// backend sobre el contenido crudo (`files.content_hash`, migración 010), o
|
|
46
|
+
// el push reportaría "modificado" en cada corrida.
|
|
47
|
+
const buf = Buffer.isBuffer(content) ? content : Buffer.from(String(content), 'utf8');
|
|
48
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
/**
|
package/src/remote.js
CHANGED
|
@@ -11,13 +11,22 @@
|
|
|
11
11
|
* {
|
|
12
12
|
* "projectId": "<uuid>",
|
|
13
13
|
* "baseURL": "https://www.shilo.city",
|
|
14
|
-
* "lastSync": "2026-07-27T12:00:00.000Z"
|
|
14
|
+
* "lastSync": "2026-07-27T12:00:00.000Z",
|
|
15
|
+
* "base": "<oid de la versión actual que esta carpeta vio por última vez>"
|
|
15
16
|
* }
|
|
16
17
|
*
|
|
17
18
|
* `lastSync` es la marca de la última vez que esta carpeta quedó alineada con
|
|
18
19
|
* el remoto (fin de un push o pull exitoso). `status` la usa para distinguir
|
|
19
20
|
* "el remoto cambió desde que sincronicé" de "esto lo subí yo".
|
|
20
21
|
*
|
|
22
|
+
* `base` es lo mismo pero exacto: el punto de guardado del que parte esta
|
|
23
|
+
* carpeta. `push` lo manda con cada propuesta para que el servidor sepa contra
|
|
24
|
+
* qué comparar — un archivo que otra persona cambió después de esa base y que
|
|
25
|
+
* acá también se cambió es un conflicto, no una sobreescritura. Va acá y no en
|
|
26
|
+
* `.shilo/index.json` porque el índice es una optimización descartable
|
|
27
|
+
* (`--no-index` lo ignora) y la base es un dato: perderla no rompe nada, pero
|
|
28
|
+
* vuelve el push siguiente ciego a lo que cambiaron los demás.
|
|
29
|
+
*
|
|
21
30
|
* NO contiene credenciales (esas están en ~/.shilo/config.json, cifradas), así
|
|
22
31
|
* que es seguro commitearlo al repo si el usuario quiere.
|
|
23
32
|
*/
|
|
@@ -49,17 +58,19 @@ function read(dir) {
|
|
|
49
58
|
projectId: raw.projectId || null,
|
|
50
59
|
baseURL: raw.baseURL || null,
|
|
51
60
|
lastSync: raw.lastSync || null,
|
|
61
|
+
base: typeof raw.base === 'string' && /^[0-9a-f]{64}$/.test(raw.base) ? raw.base : null,
|
|
52
62
|
};
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
/**
|
|
56
66
|
* Escribe `.shilo.json` en `dir`. Devuelve la ruta escrita.
|
|
57
67
|
*/
|
|
58
|
-
function write(dir, { projectId, baseURL, lastSync }) {
|
|
68
|
+
function write(dir, { projectId, baseURL, lastSync, base }) {
|
|
59
69
|
const p = remotePath(dir);
|
|
60
70
|
const onDisk = { projectId };
|
|
61
71
|
if (baseURL) onDisk.baseURL = baseURL;
|
|
62
72
|
if (lastSync) onDisk.lastSync = lastSync;
|
|
73
|
+
if (base) onDisk.base = base;
|
|
63
74
|
fs.writeFileSync(p, JSON.stringify(onDisk, null, 2) + '\n', 'utf8');
|
|
64
75
|
return p;
|
|
65
76
|
}
|
|
@@ -69,13 +80,14 @@ function write(dir, { projectId, baseURL, lastSync }) {
|
|
|
69
80
|
* remote. No es fatal si falla (la carpeta puede ser de sólo lectura): devuelve
|
|
70
81
|
* `false` y el caller decide si avisar.
|
|
71
82
|
*/
|
|
72
|
-
function touchSync(dir, { projectId, baseURL } = {}) {
|
|
83
|
+
function touchSync(dir, { projectId, baseURL, base } = {}) {
|
|
73
84
|
try {
|
|
74
85
|
const current = read(dir) || {};
|
|
75
86
|
write(dir, {
|
|
76
87
|
projectId: projectId || current.projectId,
|
|
77
88
|
baseURL: baseURL || current.baseURL,
|
|
78
89
|
lastSync: new Date().toISOString(),
|
|
90
|
+
base: base || current.base || null,
|
|
79
91
|
});
|
|
80
92
|
return true;
|
|
81
93
|
} catch (_) {
|
package/src/textguard.js
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Validación de nombres y
|
|
4
|
+
* Validación de nombres y clasificación de contenido para el push.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* como utf8, así que un binario se
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Hasta la Etapa 2 del PLAN_MOTOR_SHILO, Shilo sólo aceptaba texto: el endpoint
|
|
7
|
+
* por ruta sube el body como utf8, así que un binario se corrompía. Ahora los
|
|
8
|
+
* binarios SÍ se suben, pero por otro camino —`POST /api/files/:id/direct`, con
|
|
9
|
+
* URL firmada, sin pasar por JSON— y por eso este módulo ya no los rechaza:
|
|
10
|
+
* los CLASIFICA, para que el push sepa por dónde mandar cada archivo.
|
|
11
|
+
*
|
|
12
|
+
* Lo que sigue rechazando, y no cambió: los nombres que rompen una ruta o no son
|
|
13
|
+
* portables, y los archivos que superan el techo de tamaño. El escaneo de
|
|
14
|
+
* secretos también se queda; vive en `secrets.js` y sólo corre sobre texto,
|
|
15
|
+
* porque buscar patrones de credenciales en un JPEG es ruido.
|
|
16
|
+
*
|
|
17
|
+
* Espejo de `backend/utils/binary.js`: mismas constantes, misma heurística. Son
|
|
18
|
+
* dos paquetes sin dependencia entre sí (el CLI se publica solo y no tiene un
|
|
19
|
+
* solo `dependencies`), así que la duplicación es deliberada — pero si cambia
|
|
20
|
+
* una tiene que cambiar la otra, o un archivo se clasifica de un modo en el
|
|
21
|
+
* cliente y de otro en el servidor.
|
|
10
22
|
*/
|
|
11
23
|
|
|
12
24
|
// Espejo de `backend/utils/filenames.js`: lista negra, no lista blanca. El
|
|
@@ -19,7 +31,9 @@ const ILLEGAL_CHARS = /[<>:"/\\|?*]/;
|
|
|
19
31
|
const WIN_RESERVED = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)/i;
|
|
20
32
|
|
|
21
33
|
const MAX_NAME_LEN = 255;
|
|
22
|
-
|
|
34
|
+
// Espejo de MAX_FILE_BYTES en backend/utils/binary.js y del CHECK
|
|
35
|
+
// `file_size_valid` de la migración 014. Los tres son el mismo número.
|
|
36
|
+
const MAX_FILE_BYTES = 52428800; // 50 MB, igual que el backend
|
|
23
37
|
const MAX_DEPTH = 10; // profundidad máxima de carpetas
|
|
24
38
|
|
|
25
39
|
// Extensiones típicamente binarias: pre-filtro rápido antes de leer bytes.
|
|
@@ -126,23 +140,58 @@ function looksBinary(buffer, sample = 8000) {
|
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
|
|
143
|
+
* Mapa mínimo extensión -> mime. El backend vuelve a adivinar por su cuenta con
|
|
144
|
+
* `mime-types`, pero mandar el tipo desde acá es mejor: el cliente vio los bytes
|
|
145
|
+
* y el servidor, en una subida directa, sólo ve el nombre.
|
|
146
|
+
*/
|
|
147
|
+
const MIME_BY_EXT = {
|
|
148
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
|
149
|
+
webp: 'image/webp', avif: 'image/avif', bmp: 'image/bmp', ico: 'image/x-icon',
|
|
150
|
+
tiff: 'image/tiff', tif: 'image/tiff', heic: 'image/heic',
|
|
151
|
+
pdf: 'application/pdf', zip: 'application/zip', gz: 'application/gzip',
|
|
152
|
+
tar: 'application/x-tar', mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg',
|
|
153
|
+
mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm',
|
|
154
|
+
ttf: 'font/ttf', otf: 'font/otf', woff: 'font/woff', woff2: 'font/woff2',
|
|
155
|
+
doc: 'application/msword',
|
|
156
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
157
|
+
xls: 'application/vnd.ms-excel',
|
|
158
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
159
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
160
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** Tipo de contenido de un binario. `octet-stream` cuando no se sabe. */
|
|
164
|
+
function binaryMimeType(name) {
|
|
165
|
+
return MIME_BY_EXT[extensionOf(name)] || 'application/octet-stream';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Clasifica el contenido de un archivo. Recibe el nombre (para la extensión) y
|
|
170
|
+
* el Buffer con su contenido. Devuelve:
|
|
171
|
+
* { isText: true, content } -> sube por JSON (batch)
|
|
172
|
+
* { isText: false, isBinary: true, buffer, mimeType }
|
|
173
|
+
* -> sube por URL firmada (direct)
|
|
174
|
+
* { isText: false, isBinary: false, reason } -> saltar con aviso
|
|
175
|
+
*
|
|
176
|
+
* El techo de tamaño se comprueba PRIMERO, antes de decidir texto o binario: un
|
|
177
|
+
* archivo demasiado grande se saltea sea lo que sea, y ponerlo después haría que
|
|
178
|
+
* el mensaje dijera "binario" cuando el problema es el tamaño.
|
|
133
179
|
*/
|
|
134
180
|
function classifyContent(name, buffer) {
|
|
135
|
-
const ext = extensionOf(name);
|
|
136
|
-
if (ext && BINARY_EXTENSIONS.has(ext)) {
|
|
137
|
-
return { isText: false, reason: `extensión binaria .${ext}` };
|
|
138
|
-
}
|
|
139
|
-
if (looksBinary(buffer)) {
|
|
140
|
-
return { isText: false, reason: 'contenido binario (bytes no imprimibles)' };
|
|
141
|
-
}
|
|
142
181
|
if (buffer.length > MAX_FILE_BYTES) {
|
|
143
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
isText: false,
|
|
184
|
+
isBinary: false,
|
|
185
|
+
reason: `supera ${MAX_FILE_BYTES / (1024 * 1024)} MB (${buffer.length} bytes)`,
|
|
186
|
+
};
|
|
144
187
|
}
|
|
145
|
-
|
|
188
|
+
|
|
189
|
+
const ext = extensionOf(name);
|
|
190
|
+
if ((ext && BINARY_EXTENSIONS.has(ext)) || looksBinary(buffer)) {
|
|
191
|
+
return { isText: false, isBinary: true, buffer, mimeType: binaryMimeType(name) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { isText: true, isBinary: false, content: buffer.toString('utf8') };
|
|
146
195
|
}
|
|
147
196
|
|
|
148
197
|
module.exports = {
|
|
@@ -157,5 +206,7 @@ module.exports = {
|
|
|
157
206
|
validatePath,
|
|
158
207
|
looksBinary,
|
|
159
208
|
classifyContent,
|
|
209
|
+
binaryMimeType,
|
|
210
|
+
MIME_BY_EXT,
|
|
160
211
|
extensionOf,
|
|
161
212
|
};
|