@fefsbenson/jarvis 1.1.0 → 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 +362 -1
- package/package.json +1 -1
- package/bin/cli.js.bak-antes-windows +0 -297
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';
|
|
@@ -323,11 +325,368 @@ async function instalar(args) {
|
|
|
323
325
|
return r.status ?? 0;
|
|
324
326
|
}
|
|
325
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
|
+
|
|
326
681
|
function ajuda() {
|
|
327
682
|
abertura();
|
|
328
683
|
console.log(` ${branco('npx @fefsbenson/jarvis install')} ${fraco('baixa e instala em ./jarvis')}`);
|
|
329
684
|
console.log(` ${branco('npx @fefsbenson/jarvis install PASTA')} ${fraco('instala na pasta que você escolher')}`);
|
|
330
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();
|
|
331
690
|
console.log(` ${fraco('Precisa de: Node 18+, Python 3.10+ e o token de acesso.')}`);
|
|
332
691
|
console.log(` ${fraco('O token também pode vir da variável JARVIS_TOKEN.')}`);
|
|
333
692
|
console.log();
|
|
@@ -336,6 +695,8 @@ function ajuda() {
|
|
|
336
695
|
const [, , comando, ...resto] = process.argv;
|
|
337
696
|
if (comando === 'install') {
|
|
338
697
|
instalar(resto).then(cod => process.exit(cod));
|
|
698
|
+
} else if (comando === 'update' || comando === 'atualizar') {
|
|
699
|
+
atualizar(resto).then(cod => process.exit(cod));
|
|
339
700
|
} else {
|
|
340
701
|
ajuda();
|
|
341
702
|
process.exit(comando ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* JARVIS — baixador.
|
|
4
|
-
*
|
|
5
|
-
* Este pacote npm é SÓ o transporte (~20 KB). O sistema em si (108 MB, com
|
|
6
|
-
* conhecimento destilado de material de terceiros) vive num Release PRIVADO
|
|
7
|
-
* do GitHub e exige token de acesso — a licença proíbe distribuição pública.
|
|
8
|
-
*
|
|
9
|
-
* npx @fefsbenson/jarvis install
|
|
10
|
-
*
|
|
11
|
-
* Só Node built-in: precisa rodar antes de qualquer dependência existir.
|
|
12
|
-
*
|
|
13
|
-
* JARVIS · PRAGMA · by Felipe Brandão
|
|
14
|
-
*/
|
|
15
|
-
import { createWriteStream, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
16
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
17
|
-
import { createInterface } from 'node:readline';
|
|
18
|
-
import { tmpdir } from 'node:os';
|
|
19
|
-
import { join, resolve } from 'node:path';
|
|
20
|
-
import { pipeline } from 'node:stream/promises';
|
|
21
|
-
import { Readable } from 'node:stream';
|
|
22
|
-
|
|
23
|
-
const REPO = 'fefsbenson/jarvis-entrega';
|
|
24
|
-
const PASTA_PADRAO = 'jarvis';
|
|
25
|
-
|
|
26
|
-
// ── cores (somem se o terminal não suportar) ─────────────────────────
|
|
27
|
-
const tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
28
|
-
const c = (t, k) => (tty ? `\x1b[${k}m${t}\x1b[0m` : t);
|
|
29
|
-
const laranja = t => c(t, '38;5;208');
|
|
30
|
-
const branco = t => c(t, '38;5;255');
|
|
31
|
-
const cinza = t => c(t, '38;5;250');
|
|
32
|
-
const fraco = t => c(t, '38;5;240');
|
|
33
|
-
const verde = t => c(t, '38;5;71');
|
|
34
|
-
const vermelho = t => c(t, '38;5;167');
|
|
35
|
-
const neg = t => c(t, '1');
|
|
36
|
-
|
|
37
|
-
const LOGO = [
|
|
38
|
-
' ██ █████ ██████ ██ ██ ██ ███████',
|
|
39
|
-
' ██ ██ ██ ██ ██ ██ ██ ██ ██',
|
|
40
|
-
' ██ ███████ ██████ ██ ██ ██ ███████',
|
|
41
|
-
'██ ██ ██ ██ ██ ██ ██ ██ ██ ██',
|
|
42
|
-
' █████ ██ ██ ██ ██ ████ ██ ███████',
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
function abertura() {
|
|
46
|
-
console.log();
|
|
47
|
-
for (const l of LOGO) console.log(' ' + laranja(neg(l)));
|
|
48
|
-
console.log();
|
|
49
|
-
console.log(` ${cinza('Sistema operacional de conhecimento e agentes')}`);
|
|
50
|
-
console.log(` ${fraco('PRAGMA · by Felipe Brandão')}`);
|
|
51
|
-
console.log();
|
|
52
|
-
console.log(' ' + fraco('─'.repeat(66)));
|
|
53
|
-
console.log();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function perguntar(texto, oculto = false) {
|
|
57
|
-
return new Promise(res => {
|
|
58
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
59
|
-
if (oculto && process.stdin.isTTY) {
|
|
60
|
-
// não ecoa o token na tela nem deixa no scrollback
|
|
61
|
-
const escrever = rl._writeToOutput.bind(rl);
|
|
62
|
-
rl._writeToOutput = function (s) {
|
|
63
|
-
if (s.includes(texto)) escrever(s);
|
|
64
|
-
else escrever('*');
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
rl.question(` ${laranja('▸')} ${texto} `, r => { rl.close(); if (oculto) console.log(); res(r.trim()); });
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Precisa de Python — o instalador de verdade é ele.
|
|
73
|
-
*
|
|
74
|
-
* ARMADILHA DO WINDOWS: `python3.exe` em WindowsApps é um STUB da Microsoft
|
|
75
|
-
* Store. Ele imprime "Python não foi encontrado" e sai com código 0 — passa
|
|
76
|
-
* por qualquer teste ingênuo de status. O jeito confiável é validar a SAÍDA,
|
|
77
|
-
* e tentar o `py` launcher, que é o padrão no Windows.
|
|
78
|
-
* (o pyrun.sh do harness já sabia disso; eu não tinha reusado a lição)
|
|
79
|
-
*/
|
|
80
|
-
function acharPython() {
|
|
81
|
-
const tentativas = [
|
|
82
|
-
['py', ['-3']], // launcher oficial do Windows — o mais confiável lá
|
|
83
|
-
['python3', []],
|
|
84
|
-
['python', []],
|
|
85
|
-
];
|
|
86
|
-
for (const [cmd, pre] of tentativas) {
|
|
87
|
-
const r = spawnSync(cmd, [...pre, '-c', 'import sys;print(sys.version_info[:2])'],
|
|
88
|
-
{ encoding: 'utf8' });
|
|
89
|
-
const saida = (r.stdout || '') + (r.stderr || '');
|
|
90
|
-
// o stub da Store não imprime tupla nenhuma — só o texto da propaganda
|
|
91
|
-
if (/n.o foi encontrado|not found|Microsoft Store/i.test(saida)) continue;
|
|
92
|
-
const m = saida.match(/\((\d+),\s*(\d+)\)/);
|
|
93
|
-
if (r.status === 0 && m && (+m[1] > 3 || (+m[1] === 3 && +m[2] >= 10))) {
|
|
94
|
-
return { cmd, args: pre, versao: `${m[1]}.${m[2]}` };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function api(caminho, token, aceitar = 'application/vnd.github+json') {
|
|
101
|
-
const r = await fetch(`https://api.github.com${caminho}`, {
|
|
102
|
-
headers: {
|
|
103
|
-
Authorization: `Bearer ${token}`,
|
|
104
|
-
Accept: aceitar,
|
|
105
|
-
'User-Agent': 'jarvis-installer',
|
|
106
|
-
'X-GitHub-Api-Version': '2022-11-28',
|
|
107
|
-
},
|
|
108
|
-
redirect: 'follow',
|
|
109
|
-
});
|
|
110
|
-
return r;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function barra(feito, total) {
|
|
114
|
-
const largura = 28;
|
|
115
|
-
const pct = total ? feito / total : 0;
|
|
116
|
-
const cheio = Math.round(pct * largura);
|
|
117
|
-
return ` ${laranja('█'.repeat(cheio))}${fraco('░'.repeat(largura - cheio))} ${cinza(`${Math.round(pct * 100)}%`)}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function baixar(token, destino) {
|
|
121
|
-
// 1. qual é a versão mais recente
|
|
122
|
-
const rel = await api(`/repos/${REPO}/releases/latest`, token);
|
|
123
|
-
if (rel.status === 401) throw new Error('TOKEN_INVALIDO');
|
|
124
|
-
if (rel.status === 404) throw new Error('SEM_ACESSO');
|
|
125
|
-
if (!rel.ok) throw new Error(`GitHub respondeu ${rel.status}`);
|
|
126
|
-
const dados = await rel.json();
|
|
127
|
-
|
|
128
|
-
const asset = (dados.assets || []).find(a => a.name.endsWith('.tar.gz'));
|
|
129
|
-
if (!asset) throw new Error('Release sem pacote .tar.gz');
|
|
130
|
-
|
|
131
|
-
console.log(` ${verde('✓')} ${branco('Versão')} ${cinza(dados.tag_name)} ${fraco((asset.size / 1048576).toFixed(1) + ' MB')}`);
|
|
132
|
-
|
|
133
|
-
// 2. baixa o tarball (asset privado exige Accept: octet-stream)
|
|
134
|
-
const bin = await api(`/repos/${REPO}/releases/assets/${asset.id}`, token,
|
|
135
|
-
'application/octet-stream');
|
|
136
|
-
if (!bin.ok) throw new Error(`download falhou (${bin.status})`);
|
|
137
|
-
|
|
138
|
-
const tmp = join(tmpdir(), `jarvis-${Date.now()}.tar.gz`);
|
|
139
|
-
const total = Number(bin.headers.get('content-length')) || asset.size;
|
|
140
|
-
let feito = 0;
|
|
141
|
-
let ultimo = 0;
|
|
142
|
-
|
|
143
|
-
const origem = Readable.fromWeb(bin.body);
|
|
144
|
-
origem.on('data', pedaco => {
|
|
145
|
-
feito += pedaco.length;
|
|
146
|
-
const agora = Date.now();
|
|
147
|
-
// A barra so redesenha em TTY: com o output capturado (pipe, log, CI) o
|
|
148
|
-
// \r nao apaga a linha e o progresso vira uma parede de barras.
|
|
149
|
-
if (tty && agora - ultimo > 200) {
|
|
150
|
-
ultimo = agora;
|
|
151
|
-
process.stdout.write('\r' + barra(feito, total));
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
await pipeline(origem, createWriteStream(tmp));
|
|
155
|
-
if (tty) process.stdout.write('\r' + barra(total, total) + '\n');
|
|
156
|
-
else console.log(` ${verde('✓')} ${branco('Baixado')} ${fraco((total / 1048576).toFixed(1) + ' MB')}`);
|
|
157
|
-
|
|
158
|
-
// 3. extrai
|
|
159
|
-
mkdirSync(destino, { recursive: true });
|
|
160
|
-
const tar = spawnSync('tar', ['xzf', tmp, '-C', destino, '--strip-components=1'],
|
|
161
|
-
{ encoding: 'utf8' });
|
|
162
|
-
if (tar.status !== 0) throw new Error(`tar falhou: ${(tar.stderr || '').slice(0, 120)}`);
|
|
163
|
-
|
|
164
|
-
const n = contar(destino);
|
|
165
|
-
console.log(` ${verde('✓')} ${branco('Extraído')} ${fraco(n.toLocaleString('pt-BR') + ' arquivos')}`);
|
|
166
|
-
return destino;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function contar(dir) {
|
|
170
|
-
let n = 0;
|
|
171
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
172
|
-
if (e.name === 'node_modules' || e.name === '.git') continue;
|
|
173
|
-
n += e.isDirectory() ? contar(join(dir, e.name)) : 1;
|
|
174
|
-
}
|
|
175
|
-
return n;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function erroDeAcesso(tipo) {
|
|
179
|
-
console.log();
|
|
180
|
-
if (tipo === 'TOKEN_INVALIDO') {
|
|
181
|
-
console.log(` ${vermelho(neg('Token inválido.'))}`);
|
|
182
|
-
console.log(` ${cinza('O token expirou ou foi digitado errado.')}`);
|
|
183
|
-
} else {
|
|
184
|
-
console.log(` ${vermelho(neg('Este token não tem acesso ao JARVIS.'))}`);
|
|
185
|
-
console.log(` ${cinza('A distribuição é privada — o acesso é liberado por quem entregou o sistema.')}`);
|
|
186
|
-
}
|
|
187
|
-
console.log();
|
|
188
|
-
console.log(` ${fraco('Fale com quem forneceu o JARVIS para receber um token válido.')}`);
|
|
189
|
-
console.log();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* O harness roda em POSIX, não em Windows nativo: os 104 hooks do
|
|
194
|
-
* settings.json são invocados com `bash .../pyrun.sh` e 153 arquivos usam
|
|
195
|
-
* caminho POSIX. Instalar no PowerShell criaria um sistema que copia mas
|
|
196
|
-
* não funciona — pior que não instalar.
|
|
197
|
-
*/
|
|
198
|
-
function avisarWindows() {
|
|
199
|
-
if (process.platform !== 'win32') return true;
|
|
200
|
-
console.log(` ${vermelho(neg('O JARVIS precisa do WSL — não roda no Windows direto.'))}`);
|
|
201
|
-
console.log();
|
|
202
|
-
console.log(` ${cinza('O sistema usa bash para executar seus 104 hooks. No PowerShell')}`);
|
|
203
|
-
console.log(` ${cinza('ele até instalaria, mas não funcionaria.')}`);
|
|
204
|
-
console.log();
|
|
205
|
-
console.log(` ${branco('Abra o Ubuntu (WSL) e rode lá o mesmo comando:')}`);
|
|
206
|
-
console.log();
|
|
207
|
-
console.log(` ${fraco('wsl')}`);
|
|
208
|
-
console.log(` ${fraco('npx @fefsbenson/jarvis install')}`);
|
|
209
|
-
console.log();
|
|
210
|
-
console.log(` ${fraco('Não tem WSL? No PowerShell como administrador:')} ${cinza('wsl --install')}`);
|
|
211
|
-
console.log();
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async function instalar(args) {
|
|
216
|
-
abertura();
|
|
217
|
-
|
|
218
|
-
if (!avisarWindows()) return 1;
|
|
219
|
-
|
|
220
|
-
const py = acharPython();
|
|
221
|
-
if (!py) {
|
|
222
|
-
console.log(` ${vermelho(neg('Falta Python 3.10 ou superior.'))}`);
|
|
223
|
-
console.log();
|
|
224
|
-
console.log(` ${cinza('O JARVIS roda em Python. O Node serve só para este baixador.')}`);
|
|
225
|
-
console.log();
|
|
226
|
-
console.log(` ${branco('No Ubuntu/WSL:')} ${fraco('sudo apt install python3 python3-venv')}`);
|
|
227
|
-
console.log(` ${branco('No macOS:')} ${fraco('brew install python@3.12')}`);
|
|
228
|
-
console.log();
|
|
229
|
-
return 1;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const destino = resolve(process.cwd(), args[0] || PASTA_PADRAO);
|
|
233
|
-
if (existsSync(destino) && readdirSync(destino).length) {
|
|
234
|
-
console.log(` ${vermelho(neg('A pasta ' + destino + ' já existe e não está vazia.'))}`);
|
|
235
|
-
console.log(` ${cinza('Escolha outro nome: ')}${fraco('npx @fefsbenson/jarvis install minha-pasta')}`);
|
|
236
|
-
console.log();
|
|
237
|
-
return 1;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
console.log(` ${branco('Vou baixar o JARVIS e instalar aqui.')}`);
|
|
241
|
-
console.log(` ${fraco('Destino:')} ${branco(destino)}`);
|
|
242
|
-
console.log(` ${fraco('Python:')} ${branco(py.cmd + ' ' + py.versao)}`);
|
|
243
|
-
console.log();
|
|
244
|
-
console.log(` ${cinza('O JARVIS não é público: ele carrega conhecimento licenciado, então')}`);
|
|
245
|
-
console.log(` ${cinza('mora num repositório privado. Preciso de um token para baixá-lo.')}`);
|
|
246
|
-
console.log();
|
|
247
|
-
console.log(` ${fraco('Quem te entregou o sistema forneceu esse token junto.')}`);
|
|
248
|
-
console.log(` ${fraco('Se você mesmo publicou e tem o GitHub CLI aqui, use:')}`);
|
|
249
|
-
console.log(` ${cinza('JARVIS_TOKEN=$(gh auth token) npx @fefsbenson/jarvis install')}`);
|
|
250
|
-
console.log();
|
|
251
|
-
console.log(` ${fraco('(o token não aparece na tela enquanto você digita)')}`);
|
|
252
|
-
console.log();
|
|
253
|
-
|
|
254
|
-
const token = process.env.JARVIS_TOKEN || await perguntar('Token:', true);
|
|
255
|
-
if (!token) {
|
|
256
|
-
console.log(`\n ${fraco('Sem token, não há o que baixar. Cancelado.')}\n`);
|
|
257
|
-
return 130;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
console.log();
|
|
261
|
-
try {
|
|
262
|
-
await baixar(token, destino);
|
|
263
|
-
} catch (e) {
|
|
264
|
-
if (e.message === 'TOKEN_INVALIDO' || e.message === 'SEM_ACESSO') {
|
|
265
|
-
erroDeAcesso(e.message);
|
|
266
|
-
} else {
|
|
267
|
-
console.log(`\n ${vermelho(neg('Não consegui baixar.'))}`);
|
|
268
|
-
console.log(` ${cinza(e.message)}`);
|
|
269
|
-
console.log(`\n ${fraco('Verifique sua conexão e tente de novo.')}\n`);
|
|
270
|
-
}
|
|
271
|
-
return 1;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// entrega o comando ao instalador de verdade, que é Python
|
|
275
|
-
console.log();
|
|
276
|
-
const r = spawnSync(py.cmd, [...(py.args || []), join(destino, 'install.py'), ...args.slice(1)],
|
|
277
|
-
{ stdio: 'inherit', cwd: destino });
|
|
278
|
-
return r.status ?? 0;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function ajuda() {
|
|
282
|
-
abertura();
|
|
283
|
-
console.log(` ${branco('npx @fefsbenson/jarvis install')} ${fraco('baixa e instala em ./jarvis')}`);
|
|
284
|
-
console.log(` ${branco('npx @fefsbenson/jarvis install PASTA')} ${fraco('instala na pasta que você escolher')}`);
|
|
285
|
-
console.log();
|
|
286
|
-
console.log(` ${fraco('Precisa de: Node 18+, Python 3.10+ e o token de acesso.')}`);
|
|
287
|
-
console.log(` ${fraco('O token também pode vir da variável JARVIS_TOKEN.')}`);
|
|
288
|
-
console.log();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const [, , comando, ...resto] = process.argv;
|
|
292
|
-
if (comando === 'install') {
|
|
293
|
-
instalar(resto).then(cod => process.exit(cod));
|
|
294
|
-
} else {
|
|
295
|
-
ajuda();
|
|
296
|
-
process.exit(comando ? 1 : 0);
|
|
297
|
-
}
|