@fefsbenson/jarvis 1.1.0 → 1.3.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 +446 -1
- package/bin/teste-preservacao.mjs +65 -0
- package/bin/teste-update-backup.py +89 -0
- package/bin/teste-update-cliente.py +133 -0
- package/bin/teste-update-entrega.py +88 -0
- package/bin/teste-update-hostil.py +147 -0
- package/bin/teste-update-preserva.py +112 -0
- package/bin/testes.sh +29 -0
- package/package.json +5 -2
- 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,452 @@ 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
|
+
'.claude/mission-control/', // estado de missão e índices
|
|
368
|
+
'.claude/trash/', // o que o dono mandou para a lixeira
|
|
369
|
+
'workspace/', // o negócio do dono
|
|
370
|
+
'knowledge/business/', 'knowledge/personal/',
|
|
371
|
+
'knowledge/external/inbox/', // o material que o DONO ingere
|
|
372
|
+
'logs/', '.data/', 'artifacts/', 'processing/',
|
|
373
|
+
'outputs/', 'research/', // L3 no directory-contract: produção do dono
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Sufixos protegidos DENTRO de área do sistema.
|
|
378
|
+
*
|
|
379
|
+
* `agents/` é do pacote — AGENT.md, DNA-CONFIG.yaml e ACTIVATION.yaml vêm daqui
|
|
380
|
+
* e devem receber correção. Mas MEMORY.md e SOUL.md são a memória que o agente
|
|
381
|
+
* ACUMULA conversando com o dono: sobrescrevê-los apaga meses de uso.
|
|
382
|
+
*
|
|
383
|
+
* Medido no pacote 1.1.24: 91 arquivos nessa condição. Sem esta lista, um
|
|
384
|
+
* `npx jarvis update` os zeraria em silêncio — o dono só descobriria quando o
|
|
385
|
+
* agente esquecesse tudo o que sabia sobre ele.
|
|
386
|
+
*/
|
|
387
|
+
const SUFIXOS_DO_DONO = ['/MEMORY.md', '/SOUL.md'];
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Exceções DENTRO de pasta protegida: arquivos que o sistema mantém e o dono
|
|
391
|
+
* não escreve. Sem esta lista, `.claude/jarvis/` inteiro fica congelado — e o
|
|
392
|
+
* manifesto nunca se renova, então uma instalação antiga seguiria para sempre
|
|
393
|
+
* sem saber o que o dono alterou.
|
|
394
|
+
*/
|
|
395
|
+
const SEMPRE_ATUALIZA = ['VERSION', 'MANIFESTO-PACOTE.json', 'LICENSE'];
|
|
396
|
+
|
|
397
|
+
const EXCECOES = [
|
|
398
|
+
'.claude/jarvis/instalacao-original.json',
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
function ehIntocavel(rel) {
|
|
402
|
+
const n = rel.split('\\').join('/');
|
|
403
|
+
if (EXCECOES.includes(n)) return false;
|
|
404
|
+
if (SUFIXOS_DO_DONO.some(s => n.endsWith(s))) return true;
|
|
405
|
+
return INTOCAVEIS.some(i => i.endsWith('/') ? n.startsWith(i) : n === i);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function listar(dir, base = dir, saida = []) {
|
|
409
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
410
|
+
if (e.name === 'node_modules' || e.name === '.git' ||
|
|
411
|
+
e.name === '__pycache__' || e.name === '.venv') continue;
|
|
412
|
+
const cheio = join(dir, e.name);
|
|
413
|
+
const rel = cheio.slice(base.length + 1).split('\\').join('/');
|
|
414
|
+
if (e.isDirectory()) listar(cheio, base, saida);
|
|
415
|
+
else saida.push(rel);
|
|
416
|
+
}
|
|
417
|
+
return saida;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Compara a instalação atual com o pacote novo e classifica cada arquivo.
|
|
422
|
+
*
|
|
423
|
+
* Os quatro casos, e por que cada um se resolve assim:
|
|
424
|
+
*
|
|
425
|
+
* novo — não existe aqui: entra.
|
|
426
|
+
* igual — o dono não mexeu E o pacote não mudou: nada a fazer.
|
|
427
|
+
* atualiza — o dono não mexeu e o pacote mudou: entra (é a correção).
|
|
428
|
+
* preserva — o dono MEXEU: fica o dele. É o ponto todo do manifesto;
|
|
429
|
+
* sem ele, "atualizar" seria apagar o trabalho de alguém.
|
|
430
|
+
*/
|
|
431
|
+
/**
|
|
432
|
+
* O "motor": o que VEM DO PACOTE, por oposição ao que o dono produz.
|
|
433
|
+
*
|
|
434
|
+
* Numa instalação sem manifesto é a única forma de distinguir a correção
|
|
435
|
+
* chegando de uma edição do dono — e a chance de alguém ter editado um
|
|
436
|
+
* arquivo do sistema à mão é muito menor que a de ficar sem o conserto.
|
|
437
|
+
*
|
|
438
|
+
* A fronteira já foi estreita demais: cobria só hooks, scripts e core, e
|
|
439
|
+
* medido no pacote 1.1.24 isso deixava **4.642 arquivos sem correção** —
|
|
440
|
+
* 2.118 skills, 1.897 de conhecimento curado, 266 agentes, 73 rules. Uma
|
|
441
|
+
* instalação antiga atualizava e recebia quase nada: um update que parece
|
|
442
|
+
* seguro por não fazer nada.
|
|
443
|
+
*
|
|
444
|
+
* O que torna a fronteira larga SEGURA é o backup: tudo que entra por aqui
|
|
445
|
+
* e difere ganha `.antigo` ao lado (ver `aplicar`). Nada se perde sem volta.
|
|
446
|
+
*
|
|
447
|
+
* O que o dono produz nunca chega aqui — `ehIntocavel` roda antes e já tirou
|
|
448
|
+
* workspace, buckets business/personal, o inbox dele, outputs, research,
|
|
449
|
+
* estado, sessões, credenciais, e MEMORY.md/SOUL.md dos agentes.
|
|
450
|
+
*/
|
|
451
|
+
function ehDoMotor(rel) {
|
|
452
|
+
const n = rel.split('\\').join('/');
|
|
453
|
+
return n.startsWith('.claude/hooks/') ||
|
|
454
|
+
n.startsWith('.claude/scripts/') ||
|
|
455
|
+
n.startsWith('.claude/skills/') || // 2.118 arquivos
|
|
456
|
+
n.startsWith('.claude/rules/') || // 73
|
|
457
|
+
n.startsWith('.claude/commands/') ||
|
|
458
|
+
n.startsWith('.claude/get-shit-done/') ||
|
|
459
|
+
n.startsWith('core/') ||
|
|
460
|
+
n.startsWith('bin/') ||
|
|
461
|
+
n.startsWith('reference/') ||
|
|
462
|
+
n.startsWith('system/') ||
|
|
463
|
+
// conhecimento CURADO (L2): dna, dossiers, playbooks, sources, mmos.
|
|
464
|
+
// O inbox do dono já saiu antes, por ehIntocavel.
|
|
465
|
+
n.startsWith('knowledge/external/') ||
|
|
466
|
+
// definição do agente vem do pacote; MEMORY.md e SOUL.md dele não
|
|
467
|
+
// chegam aqui — ehIntocavel os barra antes.
|
|
468
|
+
n.startsWith('agents/') ||
|
|
469
|
+
n === 'install.py' ||
|
|
470
|
+
n === 'requirements.txt' ||
|
|
471
|
+
n === 'requirements-opcional.txt';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function planejar(raizAtual, raizNova, manifesto) {
|
|
475
|
+
const orig = manifesto ? manifesto.arquivos : null;
|
|
476
|
+
const plano = { novo: [], atualiza: [], preserva: [], igual: [], protegido: [] };
|
|
477
|
+
|
|
478
|
+
for (const rel of listar(raizNova)) {
|
|
479
|
+
if (ehIntocavel(rel)) { plano.protegido.push(rel); continue; }
|
|
480
|
+
|
|
481
|
+
const aqui = join(raizAtual, rel);
|
|
482
|
+
if (!existsSync(aqui)) { plano.novo.push(rel); continue; }
|
|
483
|
+
|
|
484
|
+
const hAqui = hashDe(aqui);
|
|
485
|
+
const hNovo = hashDe(join(raizNova, rel));
|
|
486
|
+
if (hAqui === hNovo) { plano.igual.push(rel); continue; }
|
|
487
|
+
|
|
488
|
+
// difere do pacote novo. Foi o dono que mexeu, ou é a correção chegando?
|
|
489
|
+
// Metadados do pacote são do sistema por definição — o dono nunca os
|
|
490
|
+
// edita. Preservar o VERSION deixaria o update dizendo "já está na versão
|
|
491
|
+
// mais recente" para sempre numa instalação sem manifesto.
|
|
492
|
+
if (SEMPRE_ATUALIZA.includes(rel)) { plano.atualiza.push(rel); continue; }
|
|
493
|
+
|
|
494
|
+
if (orig) {
|
|
495
|
+
const hOrig = orig[rel];
|
|
496
|
+
// sem registro de origem = arquivo que o dono criou depois: preserva
|
|
497
|
+
if (!hOrig) { plano.preserva.push(rel); continue; }
|
|
498
|
+
// igual ao que foi instalado = intocado pelo dono: pode atualizar
|
|
499
|
+
if (hAqui === hOrig) { plano.atualiza.push(rel); continue; }
|
|
500
|
+
plano.preserva.push(rel);
|
|
501
|
+
} else {
|
|
502
|
+
// Sem manifesto não dá para saber quem alterou o quê. Preservar TUDO
|
|
503
|
+
// parece seguro, mas trava o update onde ele mais importa: numa
|
|
504
|
+
// instalação antiga, os arquivos do MOTOR (scripts, hooks, instalador)
|
|
505
|
+
// são exatamente os que carregam a correção — e quase nunca são
|
|
506
|
+
// editados pelo dono. Preservá-los entregaria um update que não
|
|
507
|
+
// atualiza nada, com um .novo inerte ao lado.
|
|
508
|
+
//
|
|
509
|
+
// Então: motor atualiza, conteúdo preserva. E o que for sobrescrito
|
|
510
|
+
// ganha backup .antigo, para nada ser perdido sem volta.
|
|
511
|
+
plano[ehDoMotor(rel) ? 'atualiza' : 'preserva'].push(rel);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return plano;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function mostrarPlano(plano, semManifesto) {
|
|
518
|
+
const l = (rot, arr, cor) => {
|
|
519
|
+
if (!arr.length) return;
|
|
520
|
+
console.log(` ${cor(String(arr.length).padStart(5))} ${branco(rot)}`);
|
|
521
|
+
};
|
|
522
|
+
console.log();
|
|
523
|
+
console.log(` ${branco(neg('O que vai acontecer'))}`);
|
|
524
|
+
console.log();
|
|
525
|
+
l('arquivos novos', plano.novo, verde);
|
|
526
|
+
l('atualizados', plano.atualiza, verde);
|
|
527
|
+
l('preservados (você alterou)', plano.preserva, laranja);
|
|
528
|
+
l('já estavam iguais', plano.igual, fraco);
|
|
529
|
+
l('protegidos (memória, workspace, .env)', plano.protegido, cinza);
|
|
530
|
+
console.log();
|
|
531
|
+
|
|
532
|
+
if (plano.preserva.length) {
|
|
533
|
+
const amostra = plano.preserva.slice(0, 6);
|
|
534
|
+
console.log(` ${laranja('Preservados')} ${fraco('— fica a SUA versão, e guardo a nova ao lado como .novo')}`);
|
|
535
|
+
for (const a of amostra) console.log(` ${fraco('·')} ${cinza(a)}`);
|
|
536
|
+
if (plano.preserva.length > amostra.length) {
|
|
537
|
+
console.log(` ${fraco(`· e mais ${plano.preserva.length - amostra.length}`)}`);
|
|
538
|
+
}
|
|
539
|
+
console.log();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (semManifesto) {
|
|
543
|
+
console.log(` ${laranja('!')} ${cinza('Sua instalação é anterior ao registro de arquivos.')}`);
|
|
544
|
+
console.log(` ${fraco('Sem ele não sei o que você alterou. Então atualizo o motor do sistema')}`);
|
|
545
|
+
console.log(` ${fraco('(hooks, scripts, instalador) — é onde a correção vem — e preservo o')}`);
|
|
546
|
+
console.log(` ${fraco('resto. O que for sobrescrito fica salvo como .antigo, e dá para voltar.')}`);
|
|
547
|
+
console.log(` ${fraco('A partir desta atualização, o registro passa a existir.')}`);
|
|
548
|
+
console.log();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function aplicar(raizAtual, raizNova, plano) {
|
|
553
|
+
let n = 0;
|
|
554
|
+
for (const rel of [...plano.novo, ...plano.atualiza]) {
|
|
555
|
+
const destino = join(raizAtual, rel);
|
|
556
|
+
mkdirSync(join(destino, '..'), { recursive: true });
|
|
557
|
+
// guarda o anterior quando o conteúdo difere: sem manifesto a decisão de
|
|
558
|
+
// atualizar é uma aposta informada, e aposta precisa ter volta.
|
|
559
|
+
if (existsSync(destino) && hashDe(destino) !== hashDe(join(raizNova, rel))) {
|
|
560
|
+
try { copyFileSync(destino, destino + '.antigo'); } catch {}
|
|
561
|
+
}
|
|
562
|
+
copyFileSync(join(raizNova, rel), destino);
|
|
563
|
+
n++;
|
|
564
|
+
}
|
|
565
|
+
// o que o dono alterou fica intacto; a versão nova vai ao lado, para ele ver
|
|
566
|
+
for (const rel of plano.preserva) {
|
|
567
|
+
copyFileSync(join(raizNova, rel), join(raizAtual, rel + '.novo'));
|
|
568
|
+
}
|
|
569
|
+
return n;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function atualizar(args) {
|
|
573
|
+
abertura();
|
|
574
|
+
|
|
575
|
+
const raiz = resolve(process.cwd(), args[0] || '.');
|
|
576
|
+
const marca = join(raiz, '.claude', 'hooks');
|
|
577
|
+
if (!existsSync(marca)) {
|
|
578
|
+
console.log(` ${vermelho(neg('Não encontrei um JARVIS aqui.'))}`);
|
|
579
|
+
console.log(` ${cinza('Rode este comando DENTRO da pasta do JARVIS, ou passe o caminho:')}`);
|
|
580
|
+
console.log(` ${fraco('npx @fefsbenson/jarvis update C:\\Users\\voce\\jarvis')}`);
|
|
581
|
+
console.log();
|
|
582
|
+
return 1;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const versaoAtual = existsSync(join(raiz, 'VERSION'))
|
|
586
|
+
? readFileSync(join(raiz, 'VERSION'), 'utf8').trim() : '?';
|
|
587
|
+
|
|
588
|
+
const py = acharPython();
|
|
589
|
+
const plataforma = existsSync(join(raiz, '.claude', 'hooks', 'pyrun.py'))
|
|
590
|
+
? 'windows' : 'linux';
|
|
591
|
+
|
|
592
|
+
console.log(` ${verde('✓')} ${branco('JARVIS encontrado')} ${fraco(raiz)}`);
|
|
593
|
+
console.log(` ${verde('✓')} ${branco('Versão atual')} ${cinza('v' + versaoAtual)}` +
|
|
594
|
+
` ${fraco('edição ' + PLATAFORMAS[plataforma].rotulo)}`);
|
|
595
|
+
console.log();
|
|
596
|
+
console.log(` ${cinza('Vou baixar a versão mais recente e trazer só o que mudou.')}`);
|
|
597
|
+
console.log(` ${cinza('O que você alterou fica como está — nada seu é sobrescrito.')}`);
|
|
598
|
+
console.log();
|
|
599
|
+
|
|
600
|
+
// Pacote já em disco: pula o download. Serve a dois casos reais — o dono que
|
|
601
|
+
// recebeu o pacote por outro meio (rede local, pen drive), e o TESTE, que
|
|
602
|
+
// precisa exercitar este caminho sem rede nem token. Sem esta porta, a única
|
|
603
|
+
// forma de provar o update é baixando de verdade, e o que não se testa não
|
|
604
|
+
// se garante.
|
|
605
|
+
const localIdx = args.indexOf('--de');
|
|
606
|
+
const pacoteLocal = localIdx >= 0 ? resolve(args[localIdx + 1] || '') : null;
|
|
607
|
+
|
|
608
|
+
let tmp;
|
|
609
|
+
if (pacoteLocal) {
|
|
610
|
+
if (!existsSync(join(pacoteLocal, 'VERSION'))) {
|
|
611
|
+
console.log(`\n ${vermelho(neg('Isso não parece um pacote do JARVIS.'))}`);
|
|
612
|
+
console.log(` ${cinza(pacoteLocal + ' — não tem VERSION')}\n`);
|
|
613
|
+
return 1;
|
|
614
|
+
}
|
|
615
|
+
tmp = pacoteLocal;
|
|
616
|
+
console.log(` ${verde('✓')} ${branco('Usando o pacote local')} ${fraco(tmp)}`);
|
|
617
|
+
} else {
|
|
618
|
+
const token = process.env.JARVIS_TOKEN || await perguntar('Token:', true);
|
|
619
|
+
if (!token) {
|
|
620
|
+
console.log(`\n ${fraco('Sem token, não há o que baixar. Cancelado.')}\n`);
|
|
621
|
+
return 130;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
tmp = join(tmpdir(), `jarvis-update-${Date.now()}`);
|
|
625
|
+
console.log();
|
|
626
|
+
try {
|
|
627
|
+
await baixar(token, tmp, plataforma);
|
|
628
|
+
} catch (e) {
|
|
629
|
+
if (e.message === 'TOKEN_INVALIDO' || e.message === 'SEM_ACESSO') erroDeAcesso(e.message);
|
|
630
|
+
else {
|
|
631
|
+
console.log(`\n ${vermelho(neg('Não consegui baixar.'))}`);
|
|
632
|
+
console.log(` ${cinza(e.message)}\n`);
|
|
633
|
+
}
|
|
634
|
+
return 1;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// NUNCA apagar um diretório que o dono nos deu. Com `--de`, `tmp` É o pacote
|
|
639
|
+
// dele — removê-lo seria destruir a origem no meio de uma operação cujo
|
|
640
|
+
// propósito inteiro é não destruir nada.
|
|
641
|
+
const limparTmp = () => {
|
|
642
|
+
if (!pacoteLocal) rmSync(tmp, { recursive: true, force: true });
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const versaoNova = existsSync(join(tmp, 'VERSION'))
|
|
646
|
+
? readFileSync(join(tmp, 'VERSION'), 'utf8').trim() : '?';
|
|
647
|
+
|
|
648
|
+
if (versaoNova === versaoAtual) {
|
|
649
|
+
console.log();
|
|
650
|
+
console.log(` ${verde('✓')} ${branco('Você já está na versão mais recente')} ${cinza('(v' + versaoNova + ')')}`);
|
|
651
|
+
console.log();
|
|
652
|
+
limparTmp();
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Sanidade do que foi baixado, ANTES de encostar na pasta do dono.
|
|
657
|
+
// Um pacote que extrai pela metade (rede caindo, disco cheio) não faz o tar
|
|
658
|
+
// falhar necessariamente — e aplicar um pacote incompleto por cima de uma
|
|
659
|
+
// instalação boa é exatamente o estrago que este comando não pode causar.
|
|
660
|
+
const marcasEsperadas = ['.claude/hooks', '.claude/scripts', 'VERSION', 'install.py'];
|
|
661
|
+
const faltando = marcasEsperadas.filter(m => !existsSync(join(tmp, m)));
|
|
662
|
+
const totalBaixado = listar(tmp).length;
|
|
663
|
+
if (faltando.length || totalBaixado < 500) {
|
|
664
|
+
console.log();
|
|
665
|
+
console.log(` ${vermelho(neg('O pacote baixado veio incompleto.'))}`);
|
|
666
|
+
console.log(` ${cinza(faltando.length ? 'falta: ' + faltando.join(', ')
|
|
667
|
+
: `só ${totalBaixado} arquivos`)}`);
|
|
668
|
+
console.log();
|
|
669
|
+
console.log(` ${branco('Sua instalação NÃO foi tocada.')} ${fraco('Tente de novo.')}`);
|
|
670
|
+
console.log();
|
|
671
|
+
limparTmp();
|
|
672
|
+
return 1;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const manifesto = lerManifesto(raiz);
|
|
676
|
+
const plano = planejar(raiz, tmp, manifesto);
|
|
677
|
+
|
|
678
|
+
console.log();
|
|
679
|
+
console.log(` ${branco(neg(`v${versaoAtual}`))} ${fraco('→')} ${verde(neg(`v${versaoNova}`))}`);
|
|
680
|
+
mostrarPlano(plano, !manifesto);
|
|
681
|
+
|
|
682
|
+
if (!plano.novo.length && !plano.atualiza.length) {
|
|
683
|
+
console.log(` ${verde('✓')} ${branco('Nada a mudar nos arquivos do sistema.')}`);
|
|
684
|
+
console.log();
|
|
685
|
+
limparTmp();
|
|
686
|
+
return 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// `--seco`: mostra o plano e para. É o "ver antes de aplicar" que o dono
|
|
690
|
+
// deve usar sempre na primeira vez — e o único modo em que este comando
|
|
691
|
+
// não escreve nada, por construção.
|
|
692
|
+
if (args.includes('--seco')) {
|
|
693
|
+
console.log(` ${fraco('(--seco: nada foi escrito)')}\n`);
|
|
694
|
+
limparTmp();
|
|
695
|
+
return 0;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// `--sim` pula a confirmação. Existe para automação e teste; o caminho
|
|
699
|
+
// normal do dono continua perguntando, porque a decisão é dele.
|
|
700
|
+
if (!args.includes('--sim')) {
|
|
701
|
+
const ok = (await perguntar(`Aplicar? ${fraco('(enter = sim, n = cancelar)')}`)).trim().toLowerCase();
|
|
702
|
+
if (ok === 'n' || ok === 'nao' || ok === 'não') {
|
|
703
|
+
console.log(`\n ${fraco('Cancelado. Nada foi alterado.')}\n`);
|
|
704
|
+
limparTmp();
|
|
705
|
+
return 130;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
console.log();
|
|
710
|
+
const n = aplicar(raiz, tmp, plano);
|
|
711
|
+
console.log(` ${verde('✓')} ${branco('Aplicado')} ${fraco(n.toLocaleString('pt-BR') + ' arquivo(s)')}`);
|
|
712
|
+
limparTmp();
|
|
713
|
+
|
|
714
|
+
// O settings.json que veio no pacote traz o caminho como `$CLAUDE_PROJECT_DIR`
|
|
715
|
+
// — sintaxe POSIX. Precisa virar o caminho ABSOLUTO desta máquina, senão no
|
|
716
|
+
// Windows os 105 hooks morrem (foi o defeito que quebrou a instalação de um
|
|
717
|
+
// aluno em 24/08).
|
|
718
|
+
//
|
|
719
|
+
// Chamar o médico com --fix NÃO basta: em Linux o exame passa (lá a variável
|
|
720
|
+
// expande), então o conserto nem é acionado e o marcador fica. Por isso a
|
|
721
|
+
// regravação é INCONDICIONAL — o install.py sabe fazê-la, e é a mesma função
|
|
722
|
+
// que roda em toda instalação.
|
|
723
|
+
if (py) {
|
|
724
|
+
console.log();
|
|
725
|
+
console.log(` ${cinza('Ajustando os caminhos para esta máquina...')}`);
|
|
726
|
+
const fix = spawnSync(py.cmd, [...(py.args || []), '-c',
|
|
727
|
+
'import sys;sys.path.insert(0,sys.argv[1]);import install;' +
|
|
728
|
+
'r=install.fixar_launcher_dos_hooks();print(r[0],r[1])', raiz],
|
|
729
|
+
{ encoding: 'utf8', cwd: raiz });
|
|
730
|
+
if (fix.status === 0) {
|
|
731
|
+
console.log(` ${verde('✓')} ${branco('Caminhos e launcher gravados')} ${fraco((fix.stdout || '').trim())}`);
|
|
732
|
+
} else {
|
|
733
|
+
console.log(` ${laranja('!')} ${cinza('Não consegui regravar os caminhos automaticamente.')}`);
|
|
734
|
+
console.log(` ${fraco('Rode: ' + py.cmd + ' .claude/scripts/jarvis_doctor.py --fix')}`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// e o registro de arquivos precisa refletir a versão que acabou de entrar,
|
|
738
|
+
// senão a PRÓXIMA atualização compara contra a versão errada
|
|
739
|
+
spawnSync(py.cmd, [...(py.args || []), '-c',
|
|
740
|
+
'import sys;sys.path.insert(0,sys.argv[1]);import install;install.gravar_impressao_digital()', raiz],
|
|
741
|
+
{ encoding: 'utf8', cwd: raiz });
|
|
742
|
+
|
|
743
|
+
console.log();
|
|
744
|
+
console.log(` ${cinza('Conferindo a instalação...')}`);
|
|
745
|
+
console.log();
|
|
746
|
+
const r = spawnSync(py.cmd, [...(py.args || []),
|
|
747
|
+
join(raiz, '.claude', 'scripts', 'jarvis_doctor.py')],
|
|
748
|
+
{ stdio: 'inherit', cwd: raiz });
|
|
749
|
+
if (r.status !== 0) {
|
|
750
|
+
console.log();
|
|
751
|
+
console.log(` ${laranja('!')} ${cinza('O médico apontou algo. Veja acima o que ficou pendente.')}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
console.log();
|
|
756
|
+
console.log(` ${verde(neg('Atualizado para v' + versaoNova + '.'))}`);
|
|
757
|
+
if (plano.preserva.length) {
|
|
758
|
+
console.log(` ${fraco('Seus arquivos alterados continuam lá; a versão nova de cada um está como *.novo')}`);
|
|
759
|
+
}
|
|
760
|
+
console.log();
|
|
761
|
+
return 0;
|
|
762
|
+
}
|
|
763
|
+
|
|
326
764
|
function ajuda() {
|
|
327
765
|
abertura();
|
|
328
766
|
console.log(` ${branco('npx @fefsbenson/jarvis install')} ${fraco('baixa e instala em ./jarvis')}`);
|
|
329
767
|
console.log(` ${branco('npx @fefsbenson/jarvis install PASTA')} ${fraco('instala na pasta que você escolher')}`);
|
|
330
768
|
console.log();
|
|
769
|
+
console.log(` ${branco('npx @fefsbenson/jarvis update')} ${fraco('atualiza o JARVIS desta pasta')}`);
|
|
770
|
+
console.log(` ${branco('npx @fefsbenson/jarvis update PASTA')} ${fraco('atualiza o de outra pasta')}`);
|
|
771
|
+
console.log(` ${fraco(' o update preserva o que você alterou — nada seu é sobrescrito')}`);
|
|
772
|
+
console.log(` ${branco('npx @fefsbenson/jarvis update --seco')} ${fraco('mostra o que mudaria, sem escrever nada')}`);
|
|
773
|
+
console.log();
|
|
331
774
|
console.log(` ${fraco('Precisa de: Node 18+, Python 3.10+ e o token de acesso.')}`);
|
|
332
775
|
console.log(` ${fraco('O token também pode vir da variável JARVIS_TOKEN.')}`);
|
|
333
776
|
console.log();
|
|
@@ -336,6 +779,8 @@ function ajuda() {
|
|
|
336
779
|
const [, , comando, ...resto] = process.argv;
|
|
337
780
|
if (comando === 'install') {
|
|
338
781
|
instalar(resto).then(cod => process.exit(cod));
|
|
782
|
+
} else if (comando === 'update' || comando === 'atualizar') {
|
|
783
|
+
atualizar(resto).then(cod => process.exit(cod));
|
|
339
784
|
} else {
|
|
340
785
|
ajuda();
|
|
341
786
|
process.exit(comando ? 1 : 0);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* O update do cli.js preserva o que é do dono?
|
|
4
|
+
*
|
|
5
|
+
* Existe porque a política de preservação vive em DOIS lugares — aqui e no
|
|
6
|
+
* `atualizar.py` da instalação. Divergir significa que o mesmo arquivo é
|
|
7
|
+
* sagrado num caminho e apagado no outro, e ninguém percebe até um cliente
|
|
8
|
+
* perder trabalho.
|
|
9
|
+
*
|
|
10
|
+
* Rodar: node bin/teste-preservacao.mjs
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { join, dirname } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const cli = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'cli.js'), 'utf8');
|
|
17
|
+
const trecho = cli.slice(cli.indexOf('const INTOCAVEIS'), cli.indexOf('function listar('));
|
|
18
|
+
const { ehIntocavel } = await import(
|
|
19
|
+
'data:text/javascript,' + encodeURIComponent(trecho + '\nexport { ehIntocavel };'));
|
|
20
|
+
|
|
21
|
+
// [caminho, protegido?, por quê]
|
|
22
|
+
const CASOS = [
|
|
23
|
+
['.env', true, 'credencial'],
|
|
24
|
+
['jarvis.config.yaml', true, 'config do dono'],
|
|
25
|
+
['.claude/settings.local.json', true, 'overrides da máquina dele'],
|
|
26
|
+
['.claude/jarvis/STATE.json', true, 'estado'],
|
|
27
|
+
['.claude/sessions/s.md', true, 'histórico'],
|
|
28
|
+
['.claude/mission-control/S.json', true, 'estado de missão'],
|
|
29
|
+
['.claude/trash/x.md', true, 'lixeira dele'],
|
|
30
|
+
['workspace/businesses/x/brand.md', true, 'o negócio dele'],
|
|
31
|
+
['knowledge/business/insights/r.md', true, 'bucket dele'],
|
|
32
|
+
['knowledge/personal/cognitive/d.md', true, 'bucket dele'],
|
|
33
|
+
['knowledge/external/inbox/CURSO/a.txt', true, 'material que ELE ingeriu'],
|
|
34
|
+
['outputs/relatorio.md', true, 'produção dele (L3)'],
|
|
35
|
+
['research/analise.md', true, 'produção dele (L3)'],
|
|
36
|
+
['logs/x.jsonl', true, 'registro'],
|
|
37
|
+
['.data/rag/x.db', true, 'índice'],
|
|
38
|
+
['artifacts/x.json', true, 'saída de pipeline'],
|
|
39
|
+
['processing/x.json', true, 'artefato de pipeline'],
|
|
40
|
+
['agents/external/ah/MEMORY.md', true, 'memória acumulada do agente'],
|
|
41
|
+
['agents/external/ah/SOUL.md', true, 'voz calibrada do agente'],
|
|
42
|
+
// do sistema — DEVEM receber a correção
|
|
43
|
+
['agents/external/ah/AGENT.md', false, 'definição vem do pacote'],
|
|
44
|
+
['agents/external/ah/DNA-CONFIG.yaml', false, 'ponteiro vem do pacote'],
|
|
45
|
+
['knowledge/external/dna/persons/ah/H.yaml',false, 'DNA curado — L2'],
|
|
46
|
+
['knowledge/external/dossiers/d.md', false, 'dossiê curado — L2'],
|
|
47
|
+
['knowledge/external/playbooks/p.md', false, 'playbook curado — L2'],
|
|
48
|
+
['.claude/rules/goal-mindset.md', false, 'rule do sistema'],
|
|
49
|
+
['.claude/hooks/x.py', false, 'hook do sistema'],
|
|
50
|
+
['.claude/skills/jarvis/SKILL.md', false, 'skill do sistema'],
|
|
51
|
+
['core/paths.py', false, 'motor'],
|
|
52
|
+
['install.py', false, 'instalador'],
|
|
53
|
+
['VERSION', false, 'metadado — atualiza'],
|
|
54
|
+
['.claude/jarvis/instalacao-original.json', false, 'baseline — precisa renovar'],
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
let falhas = 0;
|
|
58
|
+
for (const [rel, esperado, porque] of CASOS) {
|
|
59
|
+
const got = ehIntocavel(rel);
|
|
60
|
+
const ok = got === esperado;
|
|
61
|
+
if (!ok) falhas++;
|
|
62
|
+
console.log(` ${ok ? 'PASS' : 'FALHA'} protegido=${String(got).padEnd(5)} ${rel.padEnd(44)} ${porque}`);
|
|
63
|
+
}
|
|
64
|
+
console.log(`\n ${falhas ? `${falhas} FALHA(S)` : `${CASOS.length}/${CASOS.length} PASSARAM`}`);
|
|
65
|
+
process.exit(falhas ? 1 : 0);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""BATERIA 6 — o risco que a fronteira larga criou.
|
|
3
|
+
|
|
4
|
+
Ampliar `ehDoMotor` para skills/rules/agents significa que, SEM manifesto, um
|
|
5
|
+
arquivo desses que o dono editou é sobrescrito. Isso só é aceitável porque o
|
|
6
|
+
`.antigo` existe. Esta bateria prova que ele existe SEMPRE, e que o conteúdo
|
|
7
|
+
do dono está lá dentro — recuperável com um `mv`.
|
|
8
|
+
"""
|
|
9
|
+
import hashlib, json, shutil, subprocess, sys, tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
PAC = Path.home()/"jarvis-dist"
|
|
13
|
+
CLI = Path.home()/"jarvis-npx/bin/cli.js"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def h(p): return hashlib.sha256(p.read_bytes()).hexdigest()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# O dono editou arquivos do SISTEMA — cada família que a fronteira agora cobre
|
|
20
|
+
EDITADOS = {
|
|
21
|
+
".claude/rules/goal-mindset.md": "# regra do sistema COM MINHA ANOTACAO\n",
|
|
22
|
+
".claude/skills/jarvis/SKILL.md": "# skill do sistema QUE EU AJUSTEI\n",
|
|
23
|
+
".claude/hooks/hook_output.py": "# hook QUE EU MEXI\n",
|
|
24
|
+
"agents/system/decisor/AGENT.md": "# agente QUE EU CUSTOMIZEI\n",
|
|
25
|
+
"core/intelligence/agents/activation_generator.py": "# core QUE EU MUDEI\n",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def montar(com_manifesto):
|
|
30
|
+
B = Path(tempfile.gettempdir())/f"bkp-{'m' if com_manifesto else 's'}"
|
|
31
|
+
if B.exists(): shutil.rmtree(B, ignore_errors=True)
|
|
32
|
+
inst = B/"inst"
|
|
33
|
+
for a in (".claude/hooks", ".claude/rules", ".claude/skills/jarvis",
|
|
34
|
+
"agents/system", "core/intelligence/agents"):
|
|
35
|
+
shutil.copytree(PAC/a, inst/a, dirs_exist_ok=True,
|
|
36
|
+
ignore=shutil.ignore_patterns("__pycache__"))
|
|
37
|
+
for f in ("VERSION", ".claude/settings.json", "install.py"):
|
|
38
|
+
(inst/f).parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
shutil.copy2(PAC/f, inst/f)
|
|
40
|
+
(inst/"VERSION").write_text("1.1.23\n")
|
|
41
|
+
if com_manifesto:
|
|
42
|
+
# manifesto do estado LIMPO (antes das edições do dono)
|
|
43
|
+
arq = {str(f.relative_to(inst)).replace("\\","/"): h(f)
|
|
44
|
+
for f in inst.rglob("*") if f.is_file() and "__pycache__" not in str(f)}
|
|
45
|
+
mp = inst/".claude/jarvis/instalacao-original.json"
|
|
46
|
+
mp.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
mp.write_text(json.dumps({"versao":"1.1.23","arquivos":arq}, indent=2))
|
|
48
|
+
for rel, txt in EDITADOS.items():
|
|
49
|
+
(inst/rel).write_text(txt, encoding="utf-8")
|
|
50
|
+
return inst
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
falhas = 0
|
|
54
|
+
for com_man in (True, False):
|
|
55
|
+
rot = "COM manifesto (deve PRESERVAR)" if com_man else "SEM manifesto (sobrescreve COM backup)"
|
|
56
|
+
print(f"\n ── {rot} ──")
|
|
57
|
+
inst = montar(com_man)
|
|
58
|
+
subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
|
|
59
|
+
capture_output=True, text=True, timeout=600)
|
|
60
|
+
for rel, meu in EDITADOS.items():
|
|
61
|
+
p = inst/rel
|
|
62
|
+
atual = p.read_text(encoding="utf-8", errors="replace")
|
|
63
|
+
marca = meu.strip()
|
|
64
|
+
no_lugar = marca in atual
|
|
65
|
+
bkp = inst/(rel + ".antigo")
|
|
66
|
+
no_backup = bkp.is_file() and marca in bkp.read_text(encoding="utf-8", errors="replace")
|
|
67
|
+
novo = inst/(rel + ".novo")
|
|
68
|
+
|
|
69
|
+
if com_man:
|
|
70
|
+
# com manifesto: o arquivo do dono FICA, e a versão nova vai .novo
|
|
71
|
+
ok = no_lugar and novo.is_file()
|
|
72
|
+
detalhe = f"no_lugar={no_lugar} tem_.novo={novo.is_file()}"
|
|
73
|
+
else:
|
|
74
|
+
# sem manifesto: é sobrescrito, MAS o dele tem que estar no .antigo
|
|
75
|
+
ok = no_backup
|
|
76
|
+
detalhe = f"no_lugar={no_lugar} no_backup={no_backup}"
|
|
77
|
+
falhas += not ok
|
|
78
|
+
print(f" {'PASS' if ok else 'FALHA'} {rel}" + ("" if ok else f" -> {detalhe}"))
|
|
79
|
+
|
|
80
|
+
# recuperável com um comando? (o teste que prova que o backup SERVE)
|
|
81
|
+
if not com_man:
|
|
82
|
+
rel = ".claude/rules/goal-mindset.md"
|
|
83
|
+
shutil.move(inst/(rel + ".antigo"), inst/rel)
|
|
84
|
+
ok = EDITADOS[rel].strip() in (inst/rel).read_text(encoding="utf-8")
|
|
85
|
+
falhas += not ok
|
|
86
|
+
print(f" {'PASS' if ok else 'FALHA'} recuperável com um `mv arquivo.antigo arquivo`")
|
|
87
|
+
|
|
88
|
+
print(f"\n BATERIA 6: {'TODOS PASSARAM' if not falhas else f'{falhas} FALHA(S)'}")
|
|
89
|
+
sys.exit(1 if falhas else 0)
|