paris-immersion 0.1.15 → 0.1.16

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.
@@ -7,6 +7,7 @@ import { readCredentials } from '../lib/credentials.js';
7
7
  import { ensureWorkspace } from './install.js';
8
8
  import { startRelayAgent } from '../lib/relay-agent.js';
9
9
  import { renderTostudyCommand } from '../templates/tostudy-command.js';
10
+ import { maybeSelfUpdate } from '../lib/self-update.js';
10
11
  import { c } from '../lib/colors.js';
11
12
  /**
12
13
  * Restore the executable bit on node-pty's `spawn-helper`. Some install paths
@@ -340,6 +341,12 @@ export async function serveCommand(opts) {
340
341
  console.error(c.red('Não logado.') + ' Rode: ' + c.bold('paris login'));
341
342
  process.exit(1);
342
343
  }
344
+ // Auto-update no startup (throttle 3h): o aluno NUNCA roda comando de update à
345
+ // mão. Se há versão nova no npm, atualiza ANTES de servir e sai — o daemon
346
+ // (launchd/systemd KeepAlive) respawna na versão nova. Best-effort.
347
+ if (await maybeSelfUpdate((m) => console.log(` ${c.dim(m)}`))) {
348
+ process.exit(0);
349
+ }
343
350
  const portNum = opts.port == null ? 31000 : Number(opts.port);
344
351
  if (!Number.isFinite(portNum) || portNum < 0 || portNum > 65535) {
345
352
  console.error(c.red(`Porta inválida: ${opts.port}`));
@@ -422,8 +429,11 @@ export async function serveCommand(opts) {
422
429
  .catch(() => { });
423
430
  console.log(` ${c.dim('Ctrl+C pra encerrar.')}`);
424
431
  console.log();
432
+ let updateTimer;
425
433
  const shutdown = () => {
426
434
  try {
435
+ if (updateTimer)
436
+ clearInterval(updateTimer);
427
437
  relay?.close();
428
438
  }
429
439
  catch {
@@ -432,6 +442,18 @@ export async function serveCommand(opts) {
432
442
  handle.close();
433
443
  process.exit(0);
434
444
  };
445
+ // Auto-update periódico: a cada 3h, SE não houver sessão do curso em uso, se
446
+ // atualiza e reinicia (o daemon respawna na versão nova) — sem ninguém rodar
447
+ // nada. Gate por isBusy() pra nunca interromper o aluno no meio do curso.
448
+ updateTimer = setInterval(() => {
449
+ if (relay?.isBusy())
450
+ return;
451
+ void maybeSelfUpdate((m) => console.log(` ${c.dim(m)}`)).then((updated) => {
452
+ if (updated)
453
+ shutdown();
454
+ });
455
+ }, 3 * 3600_000);
456
+ updateTimer.unref?.();
435
457
  process.on('SIGINT', shutdown);
436
458
  process.on('SIGTERM', shutdown);
437
459
  }
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { daemonCommand, restartCommand } from './commands/daemon.js';
13
13
  const program = new Command()
14
14
  .name('paris')
15
15
  .description('Paris Immersion — CLI do aluno (login, workspace, tarefas)')
16
- .version('0.1.15');
16
+ .version('0.1.16');
17
17
  program
18
18
  .command('login')
19
19
  .description('Autenticar com sua conta Paris (device-code OR --token direto)')
@@ -1,5 +1,7 @@
1
1
  export interface RelayAgentHandle {
2
2
  close: () => void;
3
+ /** true se há uma sessão do Claude viva (pty) — usado pra não auto-atualizar/reiniciar no meio do curso. */
4
+ isBusy: () => boolean;
3
5
  }
4
6
  /**
5
7
  * Inicia o agente relay. Busca o serve-token + info do relay na API (Bearer
@@ -305,6 +305,7 @@ export async function startRelayAgent(opts) {
305
305
  };
306
306
  connect();
307
307
  return {
308
+ isBusy: () => !!pty,
308
309
  close: () => {
309
310
  closed = true;
310
311
  if (reconnect)
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Se houver uma versão MAIS NOVA no npm, atualiza global e retorna true (o
3
+ * chamador deve `process.exit(0)` pra o daemon respawnar na nova). Throttle de 3h.
4
+ * `force` ignora o throttle (usado no startup; o periódico respeita o throttle).
5
+ */
6
+ export declare function maybeSelfUpdate(log?: (m: string) => void, force?: boolean): Promise<boolean>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Self-update do `paris serve`. O aluno NUNCA deve rodar um comando de update à
3
+ * mão — o serve (rodando como daemon) checa o npm, se atualiza sozinho e o
4
+ * KeepAlive do launchd/systemd respawna no binário novo. Best-effort: qualquer
5
+ * falha (offline, sem permissão) é engolida e o serve segue na versão atual.
6
+ */
7
+ import { execFile } from 'node:child_process';
8
+ import { promisify } from 'node:util';
9
+ import { promises as fs } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+ import path from 'node:path';
12
+ import { createRequire } from 'node:module';
13
+ const execFileAsync = promisify(execFile);
14
+ const PKG = 'paris-immersion';
15
+ const THROTTLE_MS = 3 * 3600_000; // checa no máximo a cada 3h
16
+ function currentVersion() {
17
+ try {
18
+ const req = createRequire(import.meta.url);
19
+ return req('../../package.json').version;
20
+ }
21
+ catch {
22
+ return '0.0.0';
23
+ }
24
+ }
25
+ /** retorna 1 se a > b, -1 se a < b, 0 se igual (semver x.y.z, ignora pré-release). */
26
+ function cmpSemver(a, b) {
27
+ const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
28
+ const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
29
+ for (let i = 0; i < 3; i++) {
30
+ if ((pa[i] ?? 0) > (pb[i] ?? 0))
31
+ return 1;
32
+ if ((pa[i] ?? 0) < (pb[i] ?? 0))
33
+ return -1;
34
+ }
35
+ return 0;
36
+ }
37
+ async function stampPath() {
38
+ const dir = path.join(homedir(), '.paris');
39
+ await fs.mkdir(dir, { recursive: true }).catch(() => { });
40
+ return path.join(dir, '.update-check');
41
+ }
42
+ /**
43
+ * Se houver uma versão MAIS NOVA no npm, atualiza global e retorna true (o
44
+ * chamador deve `process.exit(0)` pra o daemon respawnar na nova). Throttle de 3h.
45
+ * `force` ignora o throttle (usado no startup; o periódico respeita o throttle).
46
+ */
47
+ export async function maybeSelfUpdate(log = () => { }, force = false) {
48
+ if (process.env.PARIS_NO_SELF_UPDATE)
49
+ return false; // escape-hatch (dev/CI/offline forçado)
50
+ try {
51
+ const stamp = await stampPath();
52
+ if (!force) {
53
+ try {
54
+ const last = Number(await fs.readFile(stamp, 'utf8'));
55
+ if (Number.isFinite(last) && Date.now() - last < THROTTLE_MS)
56
+ return false;
57
+ }
58
+ catch {
59
+ /* sem stamp — checa */
60
+ }
61
+ }
62
+ await fs.writeFile(stamp, String(Date.now())).catch(() => { });
63
+ const { stdout } = await execFileAsync('npm', ['view', PKG, 'version'], { timeout: 15000 });
64
+ const latest = stdout.trim();
65
+ const current = currentVersion();
66
+ if (!/^\d+\.\d+\.\d+/.test(latest) || cmpSemver(latest, current) <= 0)
67
+ return false;
68
+ log(`nova versão da CLI: ${current} → ${latest}. Atualizando…`);
69
+ await execFileAsync('npm', ['install', '-g', `${PKG}@${latest}`], { timeout: 120_000 });
70
+ log(`atualizado para ${latest}. Reiniciando o agente…`);
71
+ return true;
72
+ }
73
+ catch {
74
+ return false; // best-effort — nunca quebra o serve
75
+ }
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paris-immersion",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Paris Immersion CLI — login, install workspace, status, task control. Para alunos das imersões da Paris Group.",
5
5
  "homepage": "https://parisgroup.ai",
6
6
  "repository": {