@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.
Files changed (2) hide show
  1. package/bin/cli.js +430 -24
  2. 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 } from 'node:fs';
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
- const asset = (dados.assets || []).find(a => a.name.endsWith('.tar.gz'));
129
- if (!asset) throw new Error('Release sem pacote .tar.gz');
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
- * 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.
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
- 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:')}`);
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(` ${fraco('wsl')}`);
208
- console.log(` ${fraco('npx @fefsbenson/jarvis install')}`);
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('Não tem WSL? No PowerShell como administrador:')} ${cinza('wsl --install')}`);
232
+ console.log(` ${fraco('São pacotes diferentes: o de Windows não usa bash.')}`);
211
233
  console.log();
212
- return false;
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
- if (!avisarWindows()) return 1;
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
- 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')}`);
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.2",
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
+ }