@fefsbenson/jarvis 1.0.2 → 1.2.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/bin/cli.js +430 -24
- package/package.json +3 -3
package/bin/cli.js
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* JARVIS · PRAGMA · by Felipe Brandão
|
|
14
14
|
*/
|
|
15
|
-
import { createWriteStream, existsSync, mkdirSync, readdirSync, statSync
|
|
15
|
+
import { createWriteStream, existsSync, mkdirSync, readdirSync, statSync,
|
|
16
|
+
readFileSync, writeFileSync, copyFileSync, rmSync } from 'node:fs';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
16
18
|
import { spawn, spawnSync } from 'node:child_process';
|
|
17
19
|
import { createInterface } from 'node:readline';
|
|
18
20
|
import { tmpdir } from 'node:os';
|
|
@@ -117,7 +119,7 @@ function barra(feito, total) {
|
|
|
117
119
|
return ` ${laranja('█'.repeat(cheio))}${fraco('░'.repeat(largura - cheio))} ${cinza(`${Math.round(pct * 100)}%`)}`;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
async function baixar(token, destino) {
|
|
122
|
+
async function baixar(token, destino, plataforma = 'linux') {
|
|
121
123
|
// 1. qual é a versão mais recente
|
|
122
124
|
const rel = await api(`/repos/${REPO}/releases/latest`, token);
|
|
123
125
|
if (rel.status === 401) throw new Error('TOKEN_INVALIDO');
|
|
@@ -125,8 +127,20 @@ async function baixar(token, destino) {
|
|
|
125
127
|
if (!rel.ok) throw new Error(`GitHub respondeu ${rel.status}`);
|
|
126
128
|
const dados = await rel.json();
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
// 2. o pacote da plataforma escolhida.
|
|
131
|
+
// Um release traz os dois assets (jarvis-X.Y.Z-windows.tar.gz e -linux).
|
|
132
|
+
// Só cai no primeiro .tar.gz disponível quando o release é antigo e tem um
|
|
133
|
+
// pacote único — assim uma versão nova do baixador continua instalando
|
|
134
|
+
// releases publicados antes desta separação.
|
|
135
|
+
const sufixo = PLATAFORMAS[plataforma]?.sufixo || 'linux';
|
|
136
|
+
const tarballs = (dados.assets || []).filter(a => a.name.endsWith('.tar.gz'));
|
|
137
|
+
const asset = tarballs.find(a => a.name.includes(`-${sufixo}`)) ||
|
|
138
|
+
(tarballs.length === 1 ? tarballs[0] : null);
|
|
139
|
+
if (!asset) {
|
|
140
|
+
const nomes = tarballs.map(a => a.name).join(', ') || 'nenhum';
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Release ${dados.tag_name} não tem pacote para ${sufixo}. Assets: ${nomes}`);
|
|
143
|
+
}
|
|
130
144
|
|
|
131
145
|
console.log(` ${verde('✓')} ${branco('Versão')} ${cinza(dados.tag_name)} ${fraco((asset.size / 1048576).toFixed(1) + ' MB')}`);
|
|
132
146
|
|
|
@@ -190,32 +204,59 @@ function erroDeAcesso(tipo) {
|
|
|
190
204
|
}
|
|
191
205
|
|
|
192
206
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
207
|
+
* Existem dois pacotes, porque são dois sistemas de verdade.
|
|
208
|
+
*
|
|
209
|
+
* O de Linux/WSL invoca os hooks com `bash pyrun.sh`; o de Windows usa
|
|
210
|
+
* `python pyrun.py` e resolve o ambiente virtual em `Scripts\` em vez de
|
|
211
|
+
* `bin/`. Instalar o pacote errado produz um sistema que copia mas não
|
|
212
|
+
* funciona — que é pior que não instalar, porque o dono só descobre depois.
|
|
213
|
+
*
|
|
214
|
+
* O padrão vem do sistema detectado, mas quem decide é o dono: no Windows
|
|
215
|
+
* ele pode legitimamente querer o pacote POSIX, se for rodar dentro do WSL.
|
|
197
216
|
*/
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
217
|
+
const PLATAFORMAS = {
|
|
218
|
+
windows: { rotulo: 'Windows (PowerShell)', sufixo: 'windows' },
|
|
219
|
+
linux: { rotulo: 'Linux, WSL ou macOS', sufixo: 'linux' },
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
async function escolherPlataforma() {
|
|
223
|
+
const provavel = process.platform === 'win32' ? 'windows' : 'linux';
|
|
224
|
+
|
|
225
|
+
console.log(` ${branco(neg('Onde o JARVIS vai rodar?'))}`);
|
|
206
226
|
console.log();
|
|
207
|
-
console.log(`
|
|
208
|
-
|
|
227
|
+
console.log(` ${laranja('[1]')} ${branco(PLATAFORMAS.windows.rotulo)}` +
|
|
228
|
+
`${provavel === 'windows' ? fraco(' ← detectei este') : ''}`);
|
|
229
|
+
console.log(` ${laranja('[2]')} ${branco(PLATAFORMAS.linux.rotulo)}` +
|
|
230
|
+
`${provavel === 'linux' ? fraco(' ← detectei este') : ''}`);
|
|
209
231
|
console.log();
|
|
210
|
-
console.log(` ${fraco('
|
|
232
|
+
console.log(` ${fraco('São pacotes diferentes: o de Windows não usa bash.')}`);
|
|
211
233
|
console.log();
|
|
212
|
-
|
|
234
|
+
|
|
235
|
+
const mapa = { '1': 'windows', '2': 'linux' };
|
|
236
|
+
for (;;) {
|
|
237
|
+
// `perguntar` já desenha o marcador ▸ — repetir aqui produzia "▸ ▸ Escolha"
|
|
238
|
+
const r = (await perguntar(`Escolha ${fraco('(1-2, enter = detectado)')}`))
|
|
239
|
+
.trim().toLowerCase();
|
|
240
|
+
if (!r) return provavel;
|
|
241
|
+
if (mapa[r]) return mapa[r];
|
|
242
|
+
// Ordem importa: "wsl" começa com 'w' e cairia em Windows se a regra
|
|
243
|
+
// genérica viesse antes — mas WSL é justamente o pacote POSIX. As
|
|
244
|
+
// palavras inteiras são testadas primeiro; os prefixos, depois.
|
|
245
|
+
if (/\b(wsl|ubuntu|linux|debian|mac|macos|posix)\b/.test(r)) return 'linux';
|
|
246
|
+
if (/\b(windows|powershell|win|ps)\b/.test(r)) return 'windows';
|
|
247
|
+
if (r.startsWith('l') || r.startsWith('m')) return 'linux';
|
|
248
|
+
if (r.startsWith('w') || r.startsWith('p')) return 'windows';
|
|
249
|
+
console.log(` ${fraco('não entendi — digite 1 ou 2')}`);
|
|
250
|
+
}
|
|
213
251
|
}
|
|
214
252
|
|
|
215
253
|
async function instalar(args) {
|
|
216
254
|
abertura();
|
|
217
255
|
|
|
218
|
-
|
|
256
|
+
const plataforma = await escolherPlataforma();
|
|
257
|
+
console.log();
|
|
258
|
+
console.log(` ${verde('✓')} ${branco('Pacote')} ${cinza(PLATAFORMAS[plataforma].rotulo)}`);
|
|
259
|
+
console.log();
|
|
219
260
|
|
|
220
261
|
const py = acharPython();
|
|
221
262
|
if (!py) {
|
|
@@ -223,8 +264,14 @@ async function instalar(args) {
|
|
|
223
264
|
console.log();
|
|
224
265
|
console.log(` ${cinza('O JARVIS roda em Python. O Node serve só para este baixador.')}`);
|
|
225
266
|
console.log();
|
|
226
|
-
|
|
227
|
-
|
|
267
|
+
if (plataforma === 'windows') {
|
|
268
|
+
console.log(` ${branco('Baixe em:')} ${fraco('https://python.org/downloads')}`);
|
|
269
|
+
console.log(` ${fraco('Marque "Add python.exe to PATH" durante a instalação.')}`);
|
|
270
|
+
console.log(` ${fraco('O Python da Microsoft Store não serve — ele é um atalho para a loja.')}`);
|
|
271
|
+
} else {
|
|
272
|
+
console.log(` ${branco('No Ubuntu/WSL:')} ${fraco('sudo apt install python3 python3-venv')}`);
|
|
273
|
+
console.log(` ${branco('No macOS:')} ${fraco('brew install python@3.12')}`);
|
|
274
|
+
}
|
|
228
275
|
console.log();
|
|
229
276
|
return 1;
|
|
230
277
|
}
|
|
@@ -259,7 +306,7 @@ async function instalar(args) {
|
|
|
259
306
|
|
|
260
307
|
console.log();
|
|
261
308
|
try {
|
|
262
|
-
await baixar(token, destino);
|
|
309
|
+
await baixar(token, destino, plataforma);
|
|
263
310
|
} catch (e) {
|
|
264
311
|
if (e.message === 'TOKEN_INVALIDO' || e.message === 'SEM_ACESSO') {
|
|
265
312
|
erroDeAcesso(e.message);
|
|
@@ -278,11 +325,368 @@ async function instalar(args) {
|
|
|
278
325
|
return r.status ?? 0;
|
|
279
326
|
}
|
|
280
327
|
|
|
328
|
+
// ══════════════════════════════════════════════════════════════════
|
|
329
|
+
// UPDATE — traz a versão nova sem apagar o que o dono fez
|
|
330
|
+
// ══════════════════════════════════════════════════════════════════
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* O manifesto que a instalação grava: hash de cada arquivo COMO SAIU do
|
|
334
|
+
* pacote. É ele que torna a atualização possível sem escolher entre duas
|
|
335
|
+
* coisas ruins — apagar o trabalho do dono, ou nunca entregar correção.
|
|
336
|
+
*
|
|
337
|
+
* Sem manifesto (instalação anterior à v1.1.7) o update ainda funciona,
|
|
338
|
+
* mas não sabe o que foi alterado: aí ele preserva por precaução tudo que
|
|
339
|
+
* difere do pacote novo, e diz isso na tela.
|
|
340
|
+
*/
|
|
341
|
+
function lerManifesto(raiz) {
|
|
342
|
+
const f = join(raiz, '.claude', 'jarvis', 'instalacao-original.json');
|
|
343
|
+
if (!existsSync(f)) return null;
|
|
344
|
+
try {
|
|
345
|
+
const d = JSON.parse(readFileSync(f, 'utf8'));
|
|
346
|
+
return (d && d.arquivos) ? d : null;
|
|
347
|
+
} catch { return null; }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function hashDe(caminho) {
|
|
351
|
+
try {
|
|
352
|
+
return createHash('sha256').update(readFileSync(caminho)).digest('hex');
|
|
353
|
+
} catch { return null; }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Pastas que NUNCA se sobrescreve, porque são do dono por natureza:
|
|
358
|
+
* credenciais, memória do sistema, o trabalho dele, estado e registros.
|
|
359
|
+
* Isto não depende do manifesto — é contrato, e vale mesmo sem ele.
|
|
360
|
+
*/
|
|
361
|
+
const INTOCAVEIS = [
|
|
362
|
+
'.env', '.venv', 'node_modules', '.git',
|
|
363
|
+
'jarvis.config.yaml',
|
|
364
|
+
'.claude/settings.local.json',
|
|
365
|
+
'.claude/jarvis/', // memória, estado, ponteiros
|
|
366
|
+
'.claude/sessions/', // histórico de sessões
|
|
367
|
+
'workspace/', // o negócio do dono
|
|
368
|
+
'knowledge/business/', 'knowledge/personal/',
|
|
369
|
+
'logs/', '.data/', 'artifacts/', 'processing/',
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Exceções DENTRO de pasta protegida: arquivos que o sistema mantém e o dono
|
|
374
|
+
* não escreve. Sem esta lista, `.claude/jarvis/` inteiro fica congelado — e o
|
|
375
|
+
* manifesto nunca se renova, então uma instalação antiga seguiria para sempre
|
|
376
|
+
* sem saber o que o dono alterou.
|
|
377
|
+
*/
|
|
378
|
+
const SEMPRE_ATUALIZA = ['VERSION', 'MANIFESTO-PACOTE.json', 'LICENSE'];
|
|
379
|
+
|
|
380
|
+
const EXCECOES = [
|
|
381
|
+
'.claude/jarvis/instalacao-original.json',
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
function ehIntocavel(rel) {
|
|
385
|
+
const n = rel.split('\\').join('/');
|
|
386
|
+
if (EXCECOES.includes(n)) return false;
|
|
387
|
+
return INTOCAVEIS.some(i => i.endsWith('/') ? n.startsWith(i) : n === i);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function listar(dir, base = dir, saida = []) {
|
|
391
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
392
|
+
if (e.name === 'node_modules' || e.name === '.git' ||
|
|
393
|
+
e.name === '__pycache__' || e.name === '.venv') continue;
|
|
394
|
+
const cheio = join(dir, e.name);
|
|
395
|
+
const rel = cheio.slice(base.length + 1).split('\\').join('/');
|
|
396
|
+
if (e.isDirectory()) listar(cheio, base, saida);
|
|
397
|
+
else saida.push(rel);
|
|
398
|
+
}
|
|
399
|
+
return saida;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Compara a instalação atual com o pacote novo e classifica cada arquivo.
|
|
404
|
+
*
|
|
405
|
+
* Os quatro casos, e por que cada um se resolve assim:
|
|
406
|
+
*
|
|
407
|
+
* novo — não existe aqui: entra.
|
|
408
|
+
* igual — o dono não mexeu E o pacote não mudou: nada a fazer.
|
|
409
|
+
* atualiza — o dono não mexeu e o pacote mudou: entra (é a correção).
|
|
410
|
+
* preserva — o dono MEXEU: fica o dele. É o ponto todo do manifesto;
|
|
411
|
+
* sem ele, "atualizar" seria apagar o trabalho de alguém.
|
|
412
|
+
*/
|
|
413
|
+
/**
|
|
414
|
+
* O "motor": o que faz o sistema funcionar, por oposição ao que ele guarda.
|
|
415
|
+
* Numa instalação sem manifesto é a única forma de distinguir a correção
|
|
416
|
+
* chegando de uma edição do dono — e a chance de alguém ter editado um hook
|
|
417
|
+
* à mão é muito menor que a de ficar sem o conserto.
|
|
418
|
+
*/
|
|
419
|
+
function ehDoMotor(rel) {
|
|
420
|
+
const n = rel.split('\\').join('/');
|
|
421
|
+
return n.startsWith('.claude/hooks/') ||
|
|
422
|
+
n.startsWith('.claude/scripts/') ||
|
|
423
|
+
n.startsWith('core/') ||
|
|
424
|
+
n.startsWith('bin/') ||
|
|
425
|
+
n === 'install.py' ||
|
|
426
|
+
n === 'requirements.txt' ||
|
|
427
|
+
n === 'requirements-opcional.txt';
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function planejar(raizAtual, raizNova, manifesto) {
|
|
431
|
+
const orig = manifesto ? manifesto.arquivos : null;
|
|
432
|
+
const plano = { novo: [], atualiza: [], preserva: [], igual: [], protegido: [] };
|
|
433
|
+
|
|
434
|
+
for (const rel of listar(raizNova)) {
|
|
435
|
+
if (ehIntocavel(rel)) { plano.protegido.push(rel); continue; }
|
|
436
|
+
|
|
437
|
+
const aqui = join(raizAtual, rel);
|
|
438
|
+
if (!existsSync(aqui)) { plano.novo.push(rel); continue; }
|
|
439
|
+
|
|
440
|
+
const hAqui = hashDe(aqui);
|
|
441
|
+
const hNovo = hashDe(join(raizNova, rel));
|
|
442
|
+
if (hAqui === hNovo) { plano.igual.push(rel); continue; }
|
|
443
|
+
|
|
444
|
+
// difere do pacote novo. Foi o dono que mexeu, ou é a correção chegando?
|
|
445
|
+
// Metadados do pacote são do sistema por definição — o dono nunca os
|
|
446
|
+
// edita. Preservar o VERSION deixaria o update dizendo "já está na versão
|
|
447
|
+
// mais recente" para sempre numa instalação sem manifesto.
|
|
448
|
+
if (SEMPRE_ATUALIZA.includes(rel)) { plano.atualiza.push(rel); continue; }
|
|
449
|
+
|
|
450
|
+
if (orig) {
|
|
451
|
+
const hOrig = orig[rel];
|
|
452
|
+
// sem registro de origem = arquivo que o dono criou depois: preserva
|
|
453
|
+
if (!hOrig) { plano.preserva.push(rel); continue; }
|
|
454
|
+
// igual ao que foi instalado = intocado pelo dono: pode atualizar
|
|
455
|
+
if (hAqui === hOrig) { plano.atualiza.push(rel); continue; }
|
|
456
|
+
plano.preserva.push(rel);
|
|
457
|
+
} else {
|
|
458
|
+
// Sem manifesto não dá para saber quem alterou o quê. Preservar TUDO
|
|
459
|
+
// parece seguro, mas trava o update onde ele mais importa: numa
|
|
460
|
+
// instalação antiga, os arquivos do MOTOR (scripts, hooks, instalador)
|
|
461
|
+
// são exatamente os que carregam a correção — e quase nunca são
|
|
462
|
+
// editados pelo dono. Preservá-los entregaria um update que não
|
|
463
|
+
// atualiza nada, com um .novo inerte ao lado.
|
|
464
|
+
//
|
|
465
|
+
// Então: motor atualiza, conteúdo preserva. E o que for sobrescrito
|
|
466
|
+
// ganha backup .antigo, para nada ser perdido sem volta.
|
|
467
|
+
plano[ehDoMotor(rel) ? 'atualiza' : 'preserva'].push(rel);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return plano;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function mostrarPlano(plano, semManifesto) {
|
|
474
|
+
const l = (rot, arr, cor) => {
|
|
475
|
+
if (!arr.length) return;
|
|
476
|
+
console.log(` ${cor(String(arr.length).padStart(5))} ${branco(rot)}`);
|
|
477
|
+
};
|
|
478
|
+
console.log();
|
|
479
|
+
console.log(` ${branco(neg('O que vai acontecer'))}`);
|
|
480
|
+
console.log();
|
|
481
|
+
l('arquivos novos', plano.novo, verde);
|
|
482
|
+
l('atualizados', plano.atualiza, verde);
|
|
483
|
+
l('preservados (você alterou)', plano.preserva, laranja);
|
|
484
|
+
l('já estavam iguais', plano.igual, fraco);
|
|
485
|
+
l('protegidos (memória, workspace, .env)', plano.protegido, cinza);
|
|
486
|
+
console.log();
|
|
487
|
+
|
|
488
|
+
if (plano.preserva.length) {
|
|
489
|
+
const amostra = plano.preserva.slice(0, 6);
|
|
490
|
+
console.log(` ${laranja('Preservados')} ${fraco('— fica a SUA versão, e guardo a nova ao lado como .novo')}`);
|
|
491
|
+
for (const a of amostra) console.log(` ${fraco('·')} ${cinza(a)}`);
|
|
492
|
+
if (plano.preserva.length > amostra.length) {
|
|
493
|
+
console.log(` ${fraco(`· e mais ${plano.preserva.length - amostra.length}`)}`);
|
|
494
|
+
}
|
|
495
|
+
console.log();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (semManifesto) {
|
|
499
|
+
console.log(` ${laranja('!')} ${cinza('Sua instalação é anterior ao registro de arquivos.')}`);
|
|
500
|
+
console.log(` ${fraco('Sem ele não sei o que você alterou. Então atualizo o motor do sistema')}`);
|
|
501
|
+
console.log(` ${fraco('(hooks, scripts, instalador) — é onde a correção vem — e preservo o')}`);
|
|
502
|
+
console.log(` ${fraco('resto. O que for sobrescrito fica salvo como .antigo, e dá para voltar.')}`);
|
|
503
|
+
console.log(` ${fraco('A partir desta atualização, o registro passa a existir.')}`);
|
|
504
|
+
console.log();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function aplicar(raizAtual, raizNova, plano) {
|
|
509
|
+
let n = 0;
|
|
510
|
+
for (const rel of [...plano.novo, ...plano.atualiza]) {
|
|
511
|
+
const destino = join(raizAtual, rel);
|
|
512
|
+
mkdirSync(join(destino, '..'), { recursive: true });
|
|
513
|
+
// guarda o anterior quando o conteúdo difere: sem manifesto a decisão de
|
|
514
|
+
// atualizar é uma aposta informada, e aposta precisa ter volta.
|
|
515
|
+
if (existsSync(destino) && hashDe(destino) !== hashDe(join(raizNova, rel))) {
|
|
516
|
+
try { copyFileSync(destino, destino + '.antigo'); } catch {}
|
|
517
|
+
}
|
|
518
|
+
copyFileSync(join(raizNova, rel), destino);
|
|
519
|
+
n++;
|
|
520
|
+
}
|
|
521
|
+
// o que o dono alterou fica intacto; a versão nova vai ao lado, para ele ver
|
|
522
|
+
for (const rel of plano.preserva) {
|
|
523
|
+
copyFileSync(join(raizNova, rel), join(raizAtual, rel + '.novo'));
|
|
524
|
+
}
|
|
525
|
+
return n;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function atualizar(args) {
|
|
529
|
+
abertura();
|
|
530
|
+
|
|
531
|
+
const raiz = resolve(process.cwd(), args[0] || '.');
|
|
532
|
+
const marca = join(raiz, '.claude', 'hooks');
|
|
533
|
+
if (!existsSync(marca)) {
|
|
534
|
+
console.log(` ${vermelho(neg('Não encontrei um JARVIS aqui.'))}`);
|
|
535
|
+
console.log(` ${cinza('Rode este comando DENTRO da pasta do JARVIS, ou passe o caminho:')}`);
|
|
536
|
+
console.log(` ${fraco('npx @fefsbenson/jarvis update C:\\Users\\voce\\jarvis')}`);
|
|
537
|
+
console.log();
|
|
538
|
+
return 1;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const versaoAtual = existsSync(join(raiz, 'VERSION'))
|
|
542
|
+
? readFileSync(join(raiz, 'VERSION'), 'utf8').trim() : '?';
|
|
543
|
+
|
|
544
|
+
const py = acharPython();
|
|
545
|
+
const plataforma = existsSync(join(raiz, '.claude', 'hooks', 'pyrun.py'))
|
|
546
|
+
? 'windows' : 'linux';
|
|
547
|
+
|
|
548
|
+
console.log(` ${verde('✓')} ${branco('JARVIS encontrado')} ${fraco(raiz)}`);
|
|
549
|
+
console.log(` ${verde('✓')} ${branco('Versão atual')} ${cinza('v' + versaoAtual)}` +
|
|
550
|
+
` ${fraco('edição ' + PLATAFORMAS[plataforma].rotulo)}`);
|
|
551
|
+
console.log();
|
|
552
|
+
console.log(` ${cinza('Vou baixar a versão mais recente e trazer só o que mudou.')}`);
|
|
553
|
+
console.log(` ${cinza('O que você alterou fica como está — nada seu é sobrescrito.')}`);
|
|
554
|
+
console.log();
|
|
555
|
+
|
|
556
|
+
const token = process.env.JARVIS_TOKEN || await perguntar('Token:', true);
|
|
557
|
+
if (!token) {
|
|
558
|
+
console.log(`\n ${fraco('Sem token, não há o que baixar. Cancelado.')}\n`);
|
|
559
|
+
return 130;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const tmp = join(tmpdir(), `jarvis-update-${Date.now()}`);
|
|
563
|
+
console.log();
|
|
564
|
+
try {
|
|
565
|
+
await baixar(token, tmp, plataforma);
|
|
566
|
+
} catch (e) {
|
|
567
|
+
if (e.message === 'TOKEN_INVALIDO' || e.message === 'SEM_ACESSO') erroDeAcesso(e.message);
|
|
568
|
+
else {
|
|
569
|
+
console.log(`\n ${vermelho(neg('Não consegui baixar.'))}`);
|
|
570
|
+
console.log(` ${cinza(e.message)}\n`);
|
|
571
|
+
}
|
|
572
|
+
return 1;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const versaoNova = existsSync(join(tmp, 'VERSION'))
|
|
576
|
+
? readFileSync(join(tmp, 'VERSION'), 'utf8').trim() : '?';
|
|
577
|
+
|
|
578
|
+
if (versaoNova === versaoAtual) {
|
|
579
|
+
console.log();
|
|
580
|
+
console.log(` ${verde('✓')} ${branco('Você já está na versão mais recente')} ${cinza('(v' + versaoNova + ')')}`);
|
|
581
|
+
console.log();
|
|
582
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
583
|
+
return 0;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Sanidade do que foi baixado, ANTES de encostar na pasta do dono.
|
|
587
|
+
// Um pacote que extrai pela metade (rede caindo, disco cheio) não faz o tar
|
|
588
|
+
// falhar necessariamente — e aplicar um pacote incompleto por cima de uma
|
|
589
|
+
// instalação boa é exatamente o estrago que este comando não pode causar.
|
|
590
|
+
const marcasEsperadas = ['.claude/hooks', '.claude/scripts', 'VERSION', 'install.py'];
|
|
591
|
+
const faltando = marcasEsperadas.filter(m => !existsSync(join(tmp, m)));
|
|
592
|
+
const totalBaixado = listar(tmp).length;
|
|
593
|
+
if (faltando.length || totalBaixado < 500) {
|
|
594
|
+
console.log();
|
|
595
|
+
console.log(` ${vermelho(neg('O pacote baixado veio incompleto.'))}`);
|
|
596
|
+
console.log(` ${cinza(faltando.length ? 'falta: ' + faltando.join(', ')
|
|
597
|
+
: `só ${totalBaixado} arquivos`)}`);
|
|
598
|
+
console.log();
|
|
599
|
+
console.log(` ${branco('Sua instalação NÃO foi tocada.')} ${fraco('Tente de novo.')}`);
|
|
600
|
+
console.log();
|
|
601
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
602
|
+
return 1;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const manifesto = lerManifesto(raiz);
|
|
606
|
+
const plano = planejar(raiz, tmp, manifesto);
|
|
607
|
+
|
|
608
|
+
console.log();
|
|
609
|
+
console.log(` ${branco(neg(`v${versaoAtual}`))} ${fraco('→')} ${verde(neg(`v${versaoNova}`))}`);
|
|
610
|
+
mostrarPlano(plano, !manifesto);
|
|
611
|
+
|
|
612
|
+
if (!plano.novo.length && !plano.atualiza.length) {
|
|
613
|
+
console.log(` ${verde('✓')} ${branco('Nada a mudar nos arquivos do sistema.')}`);
|
|
614
|
+
console.log();
|
|
615
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
616
|
+
return 0;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const ok = (await perguntar(`Aplicar? ${fraco('(enter = sim, n = cancelar)')}`)).trim().toLowerCase();
|
|
620
|
+
if (ok === 'n' || ok === 'nao' || ok === 'não') {
|
|
621
|
+
console.log(`\n ${fraco('Cancelado. Nada foi alterado.')}\n`);
|
|
622
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
623
|
+
return 130;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
console.log();
|
|
627
|
+
const n = aplicar(raiz, tmp, plano);
|
|
628
|
+
console.log(` ${verde('✓')} ${branco('Aplicado')} ${fraco(n.toLocaleString('pt-BR') + ' arquivo(s)')}`);
|
|
629
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
630
|
+
|
|
631
|
+
// O settings.json que veio no pacote traz o caminho como `$CLAUDE_PROJECT_DIR`
|
|
632
|
+
// — sintaxe POSIX. Precisa virar o caminho ABSOLUTO desta máquina, senão no
|
|
633
|
+
// Windows os 105 hooks morrem (foi o defeito que quebrou a instalação de um
|
|
634
|
+
// aluno em 24/08).
|
|
635
|
+
//
|
|
636
|
+
// Chamar o médico com --fix NÃO basta: em Linux o exame passa (lá a variável
|
|
637
|
+
// expande), então o conserto nem é acionado e o marcador fica. Por isso a
|
|
638
|
+
// regravação é INCONDICIONAL — o install.py sabe fazê-la, e é a mesma função
|
|
639
|
+
// que roda em toda instalação.
|
|
640
|
+
if (py) {
|
|
641
|
+
console.log();
|
|
642
|
+
console.log(` ${cinza('Ajustando os caminhos para esta máquina...')}`);
|
|
643
|
+
const fix = spawnSync(py.cmd, [...(py.args || []), '-c',
|
|
644
|
+
'import sys;sys.path.insert(0,sys.argv[1]);import install;' +
|
|
645
|
+
'r=install.fixar_launcher_dos_hooks();print(r[0],r[1])', raiz],
|
|
646
|
+
{ encoding: 'utf8', cwd: raiz });
|
|
647
|
+
if (fix.status === 0) {
|
|
648
|
+
console.log(` ${verde('✓')} ${branco('Caminhos e launcher gravados')} ${fraco((fix.stdout || '').trim())}`);
|
|
649
|
+
} else {
|
|
650
|
+
console.log(` ${laranja('!')} ${cinza('Não consegui regravar os caminhos automaticamente.')}`);
|
|
651
|
+
console.log(` ${fraco('Rode: ' + py.cmd + ' .claude/scripts/jarvis_doctor.py --fix')}`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// e o registro de arquivos precisa refletir a versão que acabou de entrar,
|
|
655
|
+
// senão a PRÓXIMA atualização compara contra a versão errada
|
|
656
|
+
spawnSync(py.cmd, [...(py.args || []), '-c',
|
|
657
|
+
'import sys;sys.path.insert(0,sys.argv[1]);import install;install.gravar_impressao_digital()', raiz],
|
|
658
|
+
{ encoding: 'utf8', cwd: raiz });
|
|
659
|
+
|
|
660
|
+
console.log();
|
|
661
|
+
console.log(` ${cinza('Conferindo a instalação...')}`);
|
|
662
|
+
console.log();
|
|
663
|
+
const r = spawnSync(py.cmd, [...(py.args || []),
|
|
664
|
+
join(raiz, '.claude', 'scripts', 'jarvis_doctor.py')],
|
|
665
|
+
{ stdio: 'inherit', cwd: raiz });
|
|
666
|
+
if (r.status !== 0) {
|
|
667
|
+
console.log();
|
|
668
|
+
console.log(` ${laranja('!')} ${cinza('O médico apontou algo. Veja acima o que ficou pendente.')}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
console.log();
|
|
673
|
+
console.log(` ${verde(neg('Atualizado para v' + versaoNova + '.'))}`);
|
|
674
|
+
if (plano.preserva.length) {
|
|
675
|
+
console.log(` ${fraco('Seus arquivos alterados continuam lá; a versão nova de cada um está como *.novo')}`);
|
|
676
|
+
}
|
|
677
|
+
console.log();
|
|
678
|
+
return 0;
|
|
679
|
+
}
|
|
680
|
+
|
|
281
681
|
function ajuda() {
|
|
282
682
|
abertura();
|
|
283
683
|
console.log(` ${branco('npx @fefsbenson/jarvis install')} ${fraco('baixa e instala em ./jarvis')}`);
|
|
284
684
|
console.log(` ${branco('npx @fefsbenson/jarvis install PASTA')} ${fraco('instala na pasta que você escolher')}`);
|
|
285
685
|
console.log();
|
|
686
|
+
console.log(` ${branco('npx @fefsbenson/jarvis update')} ${fraco('atualiza o JARVIS desta pasta')}`);
|
|
687
|
+
console.log(` ${branco('npx @fefsbenson/jarvis update PASTA')} ${fraco('atualiza o de outra pasta')}`);
|
|
688
|
+
console.log(` ${fraco(' o update preserva o que você alterou — nada seu é sobrescrito')}`);
|
|
689
|
+
console.log();
|
|
286
690
|
console.log(` ${fraco('Precisa de: Node 18+, Python 3.10+ e o token de acesso.')}`);
|
|
287
691
|
console.log(` ${fraco('O token também pode vir da variável JARVIS_TOKEN.')}`);
|
|
288
692
|
console.log();
|
|
@@ -291,6 +695,8 @@ function ajuda() {
|
|
|
291
695
|
const [, , comando, ...resto] = process.argv;
|
|
292
696
|
if (comando === 'install') {
|
|
293
697
|
instalar(resto).then(cod => process.exit(cod));
|
|
698
|
+
} else if (comando === 'update' || comando === 'atualizar') {
|
|
699
|
+
atualizar(resto).then(cod => process.exit(cod));
|
|
294
700
|
} else {
|
|
295
701
|
ajuda();
|
|
296
702
|
process.exit(comando ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fefsbenson/jarvis",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "JARVIS — sistema operacional de conhecimento e agentes para Claude Code",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "JARVIS — sistema operacional de conhecimento e agentes para Claude Code (Windows e Linux)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"jarvis": "bin/cli.js"
|
|
7
7
|
},
|
|
@@ -27,4 +27,4 @@
|
|
|
27
27
|
"knowledge-management",
|
|
28
28
|
"pragma"
|
|
29
29
|
]
|
|
30
|
-
}
|
|
30
|
+
}
|