paris-immersion 0.1.11 → 0.1.12

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.
@@ -4,3 +4,10 @@
4
4
  * instalador chama `install`; o `paris logout` chama `uninstall`.
5
5
  */
6
6
  export declare function daemonCommand(action: string): Promise<void>;
7
+ /**
8
+ * `paris restart` — religa o agente do terminal embutido. Sempre dá certo:
9
+ * libera a porta (mata serve preso) e reinstala/reinicia o serviço persistente,
10
+ * que sobe um serve NOVO com serve-token fresco e reconecta ao relay. É o comando
11
+ * que a UI do curso oferece quando o terminal não conecta sozinho.
12
+ */
13
+ export declare function restartCommand(): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { installDaemon, uninstallDaemon, daemonStatus } from '../lib/daemon.js';
1
+ import { installDaemon, uninstallDaemon, daemonStatus, restartDaemon } from '../lib/daemon.js';
2
2
  import { c } from '../lib/colors.js';
3
3
  /**
4
4
  * `paris daemon <install|uninstall|status>` — gerencia o serviço persistente
@@ -33,3 +33,22 @@ export async function daemonCommand(action) {
33
33
  console.log(c.yellow("Uso: paris daemon <install|uninstall|status>"));
34
34
  process.exitCode = 1;
35
35
  }
36
+ /**
37
+ * `paris restart` — religa o agente do terminal embutido. Sempre dá certo:
38
+ * libera a porta (mata serve preso) e reinstala/reinicia o serviço persistente,
39
+ * que sobe um serve NOVO com serve-token fresco e reconecta ao relay. É o comando
40
+ * que a UI do curso oferece quando o terminal não conecta sozinho.
41
+ */
42
+ export async function restartCommand() {
43
+ try {
44
+ process.stdout.write(c.dim('Religando o agente do terminal embutido…\n'));
45
+ const { mechanism } = await restartDaemon();
46
+ console.log(c.green('✓ Agente reiniciado.') + c.dim(` (${mechanism})`));
47
+ console.log(c.dim(' Volte para a aba do curso — o terminal reconecta sozinho em alguns segundos.'));
48
+ }
49
+ catch (err) {
50
+ console.log(c.yellow(`! Não consegui reiniciar o serviço: ${err instanceof Error ? err.message : String(err)}`));
51
+ console.log(c.dim(' Tente: paris daemon install'));
52
+ process.exitCode = 1;
53
+ }
54
+ }
@@ -337,6 +337,16 @@ export async function serveCommand(opts) {
337
337
  }
338
338
  catch (err) {
339
339
  const msg = err instanceof Error ? err.message : String(err);
340
+ // EADDRINUSE não é erro: o serviço em background (daemon) já tem um `paris
341
+ // serve` segurando a porta. Não precisa rodar de novo — o terminal embutido
342
+ // conecta no que já está rodando. Se ele NÃO conectar, é melhor reiniciar
343
+ // (religa com serve-token fresco) do que falhar com vermelho assustador.
344
+ if (/EADDRINUSE/i.test(msg)) {
345
+ console.log(c.green('✓ paris já está rodando') + c.dim(` (porta ${portNum} em uso pelo serviço em background).`));
346
+ console.log(c.dim(' O terminal do curso conecta no agente que já está ativo.'));
347
+ console.log(c.dim(' Se o terminal não conectar, rode: ') + c.bold('paris restart'));
348
+ process.exit(0);
349
+ }
340
350
  console.error(c.red(`Falha ao iniciar paris serve: ${msg}`));
341
351
  process.exit(1);
342
352
  }
package/dist/index.js CHANGED
@@ -9,11 +9,11 @@ import { reopenCommand } from './commands/reopen.js';
9
9
  import { graduateCommand } from './commands/graduate.js';
10
10
  import { logoutCommand } from './commands/logout.js';
11
11
  import { serveCommand } from './commands/serve.js';
12
- import { daemonCommand } from './commands/daemon.js';
12
+ 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.11');
16
+ .version('0.1.12');
17
17
  program
18
18
  .command('login')
19
19
  .description('Autenticar com sua conta Paris (device-code OR --token direto)')
@@ -61,6 +61,10 @@ program
61
61
  .command('daemon <action>')
62
62
  .description('Serviço persistente do paris serve (install | uninstall | status)')
63
63
  .action(daemonCommand);
64
+ program
65
+ .command('restart')
66
+ .description('Religa o agente do terminal embutido (libera a porta + reinicia o serviço). Use se o terminal não conectar.')
67
+ .action(restartCommand);
64
68
  program.parseAsync().catch((err) => {
65
69
  console.error('\n[paris] erro:', err?.message ?? err);
66
70
  process.exit(1);
@@ -4,3 +4,18 @@ export declare function installDaemon(): Promise<{
4
4
  /** Best-effort — nunca lança (usado no logout, não pode travar). */
5
5
  export declare function uninstallDaemon(): Promise<void>;
6
6
  export declare function daemonStatus(): Promise<boolean>;
7
+ /**
8
+ * Mata, best-effort, qualquer processo escutando na porta do serve (default
9
+ * 31000). Cobre o caso de um `paris serve` "preso" (manual ou daemon zumbi)
10
+ * segurando a porta — origem do `EADDRINUSE` e do terminal que não conecta.
11
+ */
12
+ export declare function killServePort(port?: number): Promise<void>;
13
+ /**
14
+ * Reinicia o agente do terminal embutido de forma que SEMPRE dá certo: libera a
15
+ * porta (mata serve preso) e re-registra o serviço persistente (que sobe um serve
16
+ * NOVO — com serve-token fresco, reconectando ao relay). É o que o comando
17
+ * copiável da UI roda quando o terminal não conecta sozinho.
18
+ */
19
+ export declare function restartDaemon(port?: number): Promise<{
20
+ mechanism: string;
21
+ }>;
@@ -244,3 +244,46 @@ export async function daemonStatus() {
244
244
  return false;
245
245
  }
246
246
  }
247
+ /**
248
+ * Mata, best-effort, qualquer processo escutando na porta do serve (default
249
+ * 31000). Cobre o caso de um `paris serve` "preso" (manual ou daemon zumbi)
250
+ * segurando a porta — origem do `EADDRINUSE` e do terminal que não conecta.
251
+ */
252
+ export async function killServePort(port = 31000) {
253
+ try {
254
+ if (platform() === 'win32') {
255
+ await execFileAsync('powershell', [
256
+ '-NoProfile',
257
+ '-Command',
258
+ `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | ` +
259
+ `Select-Object -ExpandProperty OwningProcess -Unique | ` +
260
+ `ForEach-Object { Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue }`,
261
+ ]).catch(() => { });
262
+ return;
263
+ }
264
+ // macOS / Linux: lsof → PIDs escutando na porta → kill.
265
+ const { stdout } = await execFileAsync('lsof', ['-ti', `tcp:${port}`, '-sTCP:LISTEN']).catch(() => ({ stdout: '' }));
266
+ const pids = stdout
267
+ .split('\n')
268
+ .map((s) => s.trim())
269
+ .filter(Boolean);
270
+ for (const pid of pids) {
271
+ await execFileAsync('kill', ['-TERM', pid]).catch(() => { });
272
+ }
273
+ }
274
+ catch {
275
+ /* best-effort */
276
+ }
277
+ }
278
+ /**
279
+ * Reinicia o agente do terminal embutido de forma que SEMPRE dá certo: libera a
280
+ * porta (mata serve preso) e re-registra o serviço persistente (que sobe um serve
281
+ * NOVO — com serve-token fresco, reconectando ao relay). É o que o comando
282
+ * copiável da UI roda quando o terminal não conecta sozinho.
283
+ */
284
+ export async function restartDaemon(port = 31000) {
285
+ await killServePort(port);
286
+ // pequena folga pro SO liberar o socket antes do novo bind
287
+ await new Promise((r) => setTimeout(r, 600));
288
+ return installDaemon();
289
+ }
@@ -45,23 +45,40 @@ export async function startRelayAgent(opts) {
45
45
  if (!opts.apiUrl || !opts.loginToken)
46
46
  return null;
47
47
  const log = opts.log ?? (() => { });
48
- let info;
49
- try {
50
- const res = await fetch(`${opts.apiUrl.replace(/\/$/, '')}/api/onboarding/v2/serve-token`, {
51
- headers: { authorization: `Bearer ${opts.loginToken}` },
52
- signal: AbortSignal.timeout(8000),
53
- });
54
- if (!res.ok)
48
+ const fetchInfo = async () => {
49
+ try {
50
+ const res = await fetch(`${opts.apiUrl.replace(/\/$/, '')}/api/onboarding/v2/serve-token`, {
51
+ headers: { authorization: `Bearer ${opts.loginToken}` },
52
+ signal: AbortSignal.timeout(8000),
53
+ });
54
+ if (!res.ok)
55
+ return null;
56
+ return (await res.json());
57
+ }
58
+ catch {
55
59
  return null;
56
- info = (await res.json());
60
+ }
61
+ };
62
+ // Primeira busca decide se o relay está configurado (senão → modo local-only).
63
+ // Retry só quando a API está inalcançável (fetchInfo → null): cobre o blip de
64
+ // rede no boot (o daemon sobe antes da rede) sem penalizar o dev local (a API
65
+ // responde 200 na 1ª tentativa). KeepAlive mantém o serve, mas sem este retry o
66
+ // agente relay nunca subia se a 1ª busca caísse no boot.
67
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
68
+ let initial = await fetchInfo();
69
+ for (let i = 0; i < 6 && !initial; i++) {
70
+ await sleep(5000);
71
+ initial = await fetchInfo();
57
72
  }
58
- catch {
73
+ if (!initial || !initial.relayUrl || !initial.token || !initial.e2eKeyB64)
59
74
  return null;
60
- }
61
- const { token, relayUrl, e2eKeyB64 } = info;
62
- if (!relayUrl || !token || !e2eKeyB64)
63
- return null; // relay não configurado → local-only
64
- const key = Buffer.from(e2eKeyB64, 'base64');
75
+ // Mutáveis: o serve-token é de vida curta e é RENOVADO a cada (re)conexão (o
76
+ // loginToken/JWT do aluno é estável). Sem isso, o daemon que sobe no boot fica
77
+ // preso com um token expirado, o relay rejeita o agente, e o terminal embutido
78
+ // fica "Esperando o paris CLI subir" pra sempre mesmo com o serve rodando.
79
+ let relayUrl = initial.relayUrl;
80
+ let token = initial.token;
81
+ let key = Buffer.from(initial.e2eKeyB64, 'base64'); // chave E2E é estável por participante
65
82
  if (key.length !== 32)
66
83
  return null;
67
84
  let ws = null;
@@ -167,7 +184,19 @@ export async function startRelayAgent(opts) {
167
184
  }
168
185
  }, 80);
169
186
  };
170
- const connect = () => {
187
+ const connect = async () => {
188
+ if (closed)
189
+ return;
190
+ // Renova token + relayUrl + chave a cada tentativa (o serve-token expira;
191
+ // o loginToken é estável). É isto que conserta o "agente preso no boot".
192
+ const fresh = await fetchInfo();
193
+ if (fresh?.relayUrl && fresh.token && fresh.e2eKeyB64) {
194
+ relayUrl = fresh.relayUrl;
195
+ token = fresh.token;
196
+ const k = Buffer.from(fresh.e2eKeyB64, 'base64');
197
+ if (k.length === 32)
198
+ key = k;
199
+ }
171
200
  if (closed)
172
201
  return;
173
202
  const url = `${relayUrl.replace(/\/$/, '')}/agent?token=${encodeURIComponent(token)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paris-immersion",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
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": {