shilo-cli 0.3.0 → 0.3.2
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 +1 -1
- package/bin/shilo.js +1 -1
- package/package.json +2 -2
- package/src/ShiloClient.js +13 -2
- package/src/commands/init.js +1 -1
- package/src/commands/login.js +1 -1
- package/src/commands/push.js +110 -69
- package/src/config.js +1 -1
- package/src/context.js +1 -1
- package/src/remote.js +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# shilo-cli
|
|
2
2
|
|
|
3
|
-
CLI para subir carpetas locales a repositorios de [Shilo](https://shilo
|
|
3
|
+
CLI para subir carpetas locales a repositorios de [Shilo](https://www.shilo.city),
|
|
4
4
|
con una experiencia tipo git: `shilo login` una vez y después `shilo push`.
|
|
5
5
|
|
|
6
6
|
> **Estado:** Fase 5 (colaboración asíncrona). Disponibles `login`, `whoami`, `logout`,
|
package/bin/shilo.js
CHANGED
|
@@ -28,7 +28,7 @@ ${ui.c.bold('Opciones de login:')}
|
|
|
28
28
|
--email <e> Email (si se omite, se pregunta).
|
|
29
29
|
--password <p> Contraseña (si se omite, se pregunta oculta).
|
|
30
30
|
--token <pat> Token de acceso personal (en vez de email + contraseña).
|
|
31
|
-
--base-url <url> Servidor Shilo (default: https://shilo
|
|
31
|
+
--base-url <url> Servidor Shilo (default: https://www.shilo.city).
|
|
32
32
|
|
|
33
33
|
${ui.c.bold('Opciones de init:')}
|
|
34
34
|
--project <uuid> Id del proyecto de Shilo (requerido).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shilo-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "CLI para sincronizar carpetas locales con repositorios de Shilo: push, pull, status, log y diff.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shilo",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"upload",
|
|
14
14
|
"repositorios"
|
|
15
15
|
],
|
|
16
|
-
"homepage": "https://shilo
|
|
16
|
+
"homepage": "https://www.shilo.city",
|
|
17
17
|
"bin": {
|
|
18
18
|
"shilo": "bin/shilo.js"
|
|
19
19
|
},
|
package/src/ShiloClient.js
CHANGED
|
@@ -18,9 +18,20 @@
|
|
|
18
18
|
* con las credenciales posiblemente refrescadas, para que el caller las persista.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
const DEFAULT_BASE_URL = 'https://shilo
|
|
21
|
+
const DEFAULT_BASE_URL = 'https://www.shilo.city';
|
|
22
22
|
const DEFAULT_TIMEOUT_MS = 45000;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* El ápex https://shilo.city responde 307 hacia https://www.shilo.city. Como
|
|
26
|
+
* `fetch` sigue redirects pero descarta el header Authorization al cruzar de
|
|
27
|
+
* origen, apuntar al ápex haría fallar con 401 todo lo autenticado (push, pull,
|
|
28
|
+
* projects). Normalizamos a www para que nunca ocurra ese salto.
|
|
29
|
+
*/
|
|
30
|
+
function normalizeBaseURL(url) {
|
|
31
|
+
const clean = String(url).replace(/\/+$/, '');
|
|
32
|
+
return clean.replace(/^(https?:\/\/)shilo\.city(?=$|\/)/i, '$1www.shilo.city');
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
class ShiloError extends Error {
|
|
25
36
|
constructor(message, { status = null, body = null } = {}) {
|
|
26
37
|
super(message);
|
|
@@ -32,7 +43,7 @@ class ShiloError extends Error {
|
|
|
32
43
|
|
|
33
44
|
class ShiloClient {
|
|
34
45
|
constructor(options = {}) {
|
|
35
|
-
this.baseURL = (options.baseURL || DEFAULT_BASE_URL)
|
|
46
|
+
this.baseURL = normalizeBaseURL(options.baseURL || DEFAULT_BASE_URL);
|
|
36
47
|
this.timeoutMs = Number(options.timeoutMs) || DEFAULT_TIMEOUT_MS;
|
|
37
48
|
// Opcionales: habilitan refresh de token sin re-login.
|
|
38
49
|
this.supabaseUrl = options.supabaseUrl || null;
|
package/src/commands/init.js
CHANGED
|
@@ -8,7 +8,7 @@ const prompt = require('../prompt');
|
|
|
8
8
|
const ui = require('../ui');
|
|
9
9
|
const { ShiloClient } = require('../ShiloClient');
|
|
10
10
|
|
|
11
|
-
const DEFAULT_BASE_URL = 'https://shilo
|
|
11
|
+
const DEFAULT_BASE_URL = 'https://www.shilo.city';
|
|
12
12
|
|
|
13
13
|
// UUID v4 laxo (acepta cualquier versión). El backend valida de verdad; esto es
|
|
14
14
|
// sólo un aviso temprano ante un typo evidente.
|
package/src/commands/login.js
CHANGED
|
@@ -5,7 +5,7 @@ const prompt = require('../prompt');
|
|
|
5
5
|
const ui = require('../ui');
|
|
6
6
|
const { ShiloClient } = require('../ShiloClient');
|
|
7
7
|
|
|
8
|
-
const DEFAULT_BASE_URL = 'https://shilo
|
|
8
|
+
const DEFAULT_BASE_URL = 'https://www.shilo.city';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* shilo login [--email <e>] [--password <p>] [--token <pat>] [--base-url <url>]
|
package/src/commands/push.js
CHANGED
|
@@ -11,7 +11,7 @@ const { walk } = require('../walk');
|
|
|
11
11
|
const { classifyContent } = require('../textguard');
|
|
12
12
|
const { ShiloClient } = require('../ShiloClient');
|
|
13
13
|
|
|
14
|
-
const DEFAULT_BASE_URL = 'https://shilo
|
|
14
|
+
const DEFAULT_BASE_URL = 'https://www.shilo.city';
|
|
15
15
|
const COMMIT_BATCH = 100; // máximo de archivos por commit (límite del backend)
|
|
16
16
|
|
|
17
17
|
// Mapa mínimo extensión -> mime para texto. El backend default es text/plain.
|
|
@@ -138,40 +138,45 @@ async function push(args) {
|
|
|
138
138
|
|
|
139
139
|
// 5) Clasificar contra el remote (added / modified / unchanged) para planear.
|
|
140
140
|
// Esto ya toca la red (getFileMeta / readFile) pero no escribe nada.
|
|
141
|
-
|
|
141
|
+
// Se hace con un pool de concurrencia (mismo patrón que walkRemote en
|
|
142
|
+
// pull/status) en vez de un archivo a la vez: es lo que hacía que un push
|
|
143
|
+
// de muchos archivos tardara minutos por puro round-trip secuencial.
|
|
144
|
+
// `mgr` serializa el refresh del JWT para que llamadas paralelas no
|
|
145
|
+
// quemen el mismo refresh_token.
|
|
146
|
+
const mgr = sync.makeCredsManager(client, cfg);
|
|
142
147
|
const plan = { added: [], modified: [], unchanged: [], failed: [] };
|
|
143
148
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
creds = meta.credentials;
|
|
149
|
+
const planResults = await sync.runPool(uploads, sync.CONCURRENCY, async (u) => {
|
|
150
|
+
const creds = await mgr.valid();
|
|
151
|
+
const meta = await client.getFileMeta(projectId, u.relPath, creds);
|
|
148
152
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (meta.file && meta.file.type === 'folder') {
|
|
154
|
-
plan.failed.push({ ...u, reason: 'ya existe una carpeta con ese nombre' });
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
153
|
+
if (!meta.exists) return { kind: 'added', u };
|
|
154
|
+
if (meta.file && meta.file.type === 'folder') {
|
|
155
|
+
return { kind: 'failed', u, reason: 'ya existe una carpeta con ese nombre' };
|
|
156
|
+
}
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
158
|
+
// Existe como archivo: ver si cambió comparando tamaño y luego contenido.
|
|
159
|
+
const localSize = Buffer.byteLength(u.content, 'utf8');
|
|
160
|
+
if (meta.file && typeof meta.file.size === 'number' && meta.file.size === localSize) {
|
|
161
|
+
const readCreds = await mgr.valid();
|
|
162
|
+
const remoteRead = await client.readFile(projectId, u.relPath, readCreds);
|
|
163
|
+
if (remoteRead.file && remoteRead.file.content === u.content) {
|
|
164
|
+
return { kind: 'unchanged', u };
|
|
167
165
|
}
|
|
168
|
-
plan.modified.push({ ...u, remote: meta.file });
|
|
169
|
-
} catch (e) {
|
|
170
|
-
plan.failed.push({ ...u, reason: e.message });
|
|
171
166
|
}
|
|
167
|
+
return { kind: 'modified', u: { ...u, remote: meta.file } };
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
for (const r of planResults) {
|
|
171
|
+
if (r && r.__error) {
|
|
172
|
+
plan.failed.push({ ...r.item, reason: r.__error });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (r.kind === 'failed') plan.failed.push({ ...r.u, reason: r.reason });
|
|
176
|
+
else plan[r.kind].push(r.u);
|
|
172
177
|
}
|
|
173
178
|
|
|
174
|
-
config.persistCredsIfChanged(cfg,
|
|
179
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
175
180
|
|
|
176
181
|
// 5b) Atribución: para los archivos que vamos a pisar, quién los tocó último.
|
|
177
182
|
// Es lo que distingue "esto lo subí yo antes" de "esto lo va a perder un
|
|
@@ -179,7 +184,6 @@ async function push(args) {
|
|
|
179
184
|
let attribution = { index: new Map() };
|
|
180
185
|
const myId = cfg.credentials && cfg.credentials.user ? cfg.credentials.user.id : null;
|
|
181
186
|
if (plan.modified.length > 0) {
|
|
182
|
-
const mgr = sync.makeCredsManager(client, cfg);
|
|
183
187
|
attribution = await sync.buildAuthorIndex(client, projectId, mgr, { commits: 50 });
|
|
184
188
|
config.persistCredsIfChanged(cfg, mgr.current);
|
|
185
189
|
}
|
|
@@ -266,57 +270,94 @@ async function push(args) {
|
|
|
266
270
|
const ensuredCache = new Set();
|
|
267
271
|
const failedWrites = [];
|
|
268
272
|
|
|
273
|
+
// 8a) Asegurar carpetas primero, EN SERIE. `ensuredCache` evita re-listar una
|
|
274
|
+
// carpeta ya confirmada, pero además evita la carrera real: el backend no
|
|
275
|
+
// siempre responde 409 de forma confiable si dos requests intentan crear
|
|
276
|
+
// la misma carpeta al mismo tiempo, así que esta fase no se paraleliza.
|
|
277
|
+
// Como la mayoría de los archivos comparten carpetas, el cache hace que
|
|
278
|
+
// esto sea barato (proporcional a carpetas únicas, no a archivos).
|
|
279
|
+
const ensureFailed = new Set();
|
|
269
280
|
for (const u of plan.added) {
|
|
270
281
|
try {
|
|
271
|
-
creds = await
|
|
272
|
-
|
|
273
|
-
projectId, u.relPath, u.content, u.mimeType, creds
|
|
274
|
-
);
|
|
275
|
-
creds = created.credentials;
|
|
276
|
-
|
|
277
|
-
if (created.created && created.file) {
|
|
278
|
-
changes.push({
|
|
279
|
-
file_id: created.file.id,
|
|
280
|
-
action: 'added',
|
|
281
|
-
new_storage_path: created.file.storage_path,
|
|
282
|
-
});
|
|
283
|
-
ui.success(`${ui.c.green('+')} ${u.relPath}`);
|
|
284
|
-
} else {
|
|
285
|
-
// 409: apareció entremedio; caemos a update.
|
|
286
|
-
const upd = await client.writeFile(projectId, u.relPath, u.content, u.mimeType, creds);
|
|
287
|
-
creds = upd.credentials;
|
|
288
|
-
changes.push({
|
|
289
|
-
file_id: upd.file.id,
|
|
290
|
-
action: 'modified',
|
|
291
|
-
previous_storage_path: (created.file && created.file.storage_path) || upd.file.storage_path,
|
|
292
|
-
new_storage_path: upd.file.storage_path,
|
|
293
|
-
});
|
|
294
|
-
ui.success(`${ui.c.yellow('~')} ${u.relPath}`);
|
|
295
|
-
}
|
|
296
|
-
config.persistCredsIfChanged(cfg, creds);
|
|
282
|
+
const creds = await mgr.valid();
|
|
283
|
+
await client.ensurePath(projectId, u.relPath, creds, ensuredCache);
|
|
297
284
|
} catch (e) {
|
|
285
|
+
ensureFailed.add(u.relPath);
|
|
298
286
|
failedWrites.push({ relPath: u.relPath, reason: e.message });
|
|
299
287
|
ui.error(`${u.relPath} — ${e.message}`);
|
|
300
288
|
}
|
|
301
289
|
}
|
|
290
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
291
|
+
|
|
292
|
+
// 8b) Subidas propiamente dichas, en paralelo: acá es donde vive el grueso
|
|
293
|
+
// del tiempo (un request por archivo), y a diferencia de ensurePath no
|
|
294
|
+
// hay riesgo de carrera porque cada archivo tiene una ruta propia.
|
|
295
|
+
const addedReady = plan.added.filter((u) => !ensureFailed.has(u.relPath));
|
|
296
|
+
const addedResults = await sync.runPool(addedReady, sync.CONCURRENCY, async (u) => {
|
|
297
|
+
const creds = await mgr.valid();
|
|
298
|
+
const created = await client.createFile(projectId, u.relPath, u.content, u.mimeType, creds);
|
|
299
|
+
|
|
300
|
+
if (created.created && created.file) {
|
|
301
|
+
return {
|
|
302
|
+
u,
|
|
303
|
+
symbol: '+',
|
|
304
|
+
change: {
|
|
305
|
+
file_id: created.file.id,
|
|
306
|
+
action: 'added',
|
|
307
|
+
new_storage_path: created.file.storage_path,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// 409: apareció entremedio; caemos a update.
|
|
312
|
+
const writeCreds = await mgr.valid();
|
|
313
|
+
const upd = await client.writeFile(projectId, u.relPath, u.content, u.mimeType, writeCreds);
|
|
314
|
+
return {
|
|
315
|
+
u,
|
|
316
|
+
symbol: '~',
|
|
317
|
+
change: {
|
|
318
|
+
file_id: upd.file.id,
|
|
319
|
+
action: 'modified',
|
|
320
|
+
previous_storage_path: (created.file && created.file.storage_path) || upd.file.storage_path,
|
|
321
|
+
new_storage_path: upd.file.storage_path,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
});
|
|
302
325
|
|
|
303
|
-
for (const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
326
|
+
for (const r of addedResults) {
|
|
327
|
+
if (r && r.__error) {
|
|
328
|
+
failedWrites.push({ relPath: r.item.relPath, reason: r.__error });
|
|
329
|
+
ui.error(`${r.item.relPath} — ${r.__error}`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
changes.push(r.change);
|
|
333
|
+
ui.success(`${ui.c[r.symbol === '+' ? 'green' : 'yellow'](r.symbol)} ${r.u.relPath}`);
|
|
334
|
+
}
|
|
335
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
336
|
+
|
|
337
|
+
const modifiedResults = await sync.runPool(plan.modified, sync.CONCURRENCY, async (u) => {
|
|
338
|
+
const creds = await mgr.valid();
|
|
339
|
+
const upd = await client.writeFile(projectId, u.relPath, u.content, u.mimeType, creds);
|
|
340
|
+
return {
|
|
341
|
+
u,
|
|
342
|
+
change: {
|
|
308
343
|
file_id: upd.file.id,
|
|
309
344
|
action: 'modified',
|
|
310
345
|
previous_storage_path: u.remote ? u.remote.storage_path : undefined,
|
|
311
346
|
new_storage_path: upd.file.storage_path,
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
for (const r of modifiedResults) {
|
|
352
|
+
if (r && r.__error) {
|
|
353
|
+
failedWrites.push({ relPath: r.item.relPath, reason: r.__error });
|
|
354
|
+
ui.error(`${r.item.relPath} — ${r.__error}`);
|
|
355
|
+
continue;
|
|
318
356
|
}
|
|
357
|
+
changes.push(r.change);
|
|
358
|
+
ui.success(`${ui.c.yellow('~')} ${r.u.relPath}`);
|
|
319
359
|
}
|
|
360
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
320
361
|
|
|
321
362
|
// 9) Commit(s) en lotes de <=100.
|
|
322
363
|
let committed = 0;
|
|
@@ -328,10 +369,10 @@ async function push(args) {
|
|
|
328
369
|
const partNo = Math.floor(i / COMMIT_BATCH) + 1;
|
|
329
370
|
const msg = buildMessage(message, batch.length, total, partNo, nBatches, card);
|
|
330
371
|
try {
|
|
331
|
-
const
|
|
332
|
-
|
|
372
|
+
const creds = await mgr.valid();
|
|
373
|
+
await client.createCommit(projectId, msg, batch, creds);
|
|
333
374
|
committed += batch.length;
|
|
334
|
-
config.persistCredsIfChanged(cfg,
|
|
375
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
335
376
|
} catch (e) {
|
|
336
377
|
ui.error(`Fallo al crear el commit (lote ${partNo}/${nBatches}): ${e.message}`);
|
|
337
378
|
failedWrites.push({ relPath: `<commit lote ${partNo}>`, reason: e.message });
|
|
@@ -339,7 +380,7 @@ async function push(args) {
|
|
|
339
380
|
}
|
|
340
381
|
}
|
|
341
382
|
|
|
342
|
-
config.persistCredsIfChanged(cfg,
|
|
383
|
+
config.persistCredsIfChanged(cfg, mgr.current);
|
|
343
384
|
|
|
344
385
|
// 10) Anotar la sincronización: `status` la usa después para saber qué del
|
|
345
386
|
// remoto es posterior a lo último que subimos.
|
package/src/config.js
CHANGED
|
@@ -58,7 +58,7 @@ function decrypt(payload) {
|
|
|
58
58
|
* Estructura en disco:
|
|
59
59
|
* {
|
|
60
60
|
* version: 1,
|
|
61
|
-
* baseURL: "https://shilo
|
|
61
|
+
* baseURL: "https://www.shilo.city",
|
|
62
62
|
* supabaseUrl?: "...", // opcional, habilita refresh de token
|
|
63
63
|
* supabaseAnonKey?: "...", // opcional (valor público)
|
|
64
64
|
* credentials: { iv, data } // cifrado -> { jwt, refreshToken, expiresAt, user }
|
package/src/context.js
CHANGED
|
@@ -17,7 +17,7 @@ const ui = require('./ui');
|
|
|
17
17
|
const sync = require('./sync');
|
|
18
18
|
const { ShiloClient } = require('./ShiloClient');
|
|
19
19
|
|
|
20
|
-
const DEFAULT_BASE_URL = 'https://shilo
|
|
20
|
+
const DEFAULT_BASE_URL = 'https://www.shilo.city';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Devuelve `{ ok: true, dir, projectId, baseURL, cfg, client, mgr, savedRemote }`
|