paris-immersion 0.1.17 → 0.1.18

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.
@@ -1,9 +1,14 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { createRequire } from 'node:module';
3
7
  import { apiAuthFetch, apiFetch, defaultApiUrl, ApiError } from '../lib/api.js';
4
8
  import { readCredentials, credentialsPath } from '../lib/credentials.js';
5
9
  import { findMarker } from '../lib/marker.js';
6
10
  import { c } from '../lib/colors.js';
11
+ const execFileAsync = promisify(execFile);
7
12
  export async function doctorCommand(opts = {}) {
8
13
  const checks = [];
9
14
  // 1. Credentials file
@@ -44,6 +49,51 @@ export async function doctorCommand(opts = {}) {
44
49
  const settingsExists = await fs.access(settingsPath).then(() => true).catch(() => false);
45
50
  checks.push({ name: '.claude/settings.json', ok: settingsExists, detail: settingsExists ? settingsPath : 'missing — re-run `paris install`' });
46
51
  }
52
+ // 6. Claude Code instalado (o terminal embutido roda `claude`) — bloqueia o install.
53
+ let claudeInstalled = false;
54
+ try {
55
+ await execFileAsync('claude', ['--version'], { timeout: 8000 });
56
+ claudeInstalled = true;
57
+ }
58
+ catch {
59
+ claudeInstalled = false;
60
+ }
61
+ checks.push({
62
+ name: 'claude code',
63
+ ok: claudeInstalled,
64
+ detail: claudeInstalled ? 'instalado' : 'não instalado / fora do PATH — rode: npm i -g @anthropic-ai/claude-code',
65
+ });
66
+ // 7. Login do Claude (INFORMATIVO — o aluno loga na etapa do curso, não bloqueia install).
67
+ if (claudeInstalled) {
68
+ let loggedIn = false;
69
+ try {
70
+ const j = JSON.parse(await fs.readFile(path.join(homedir(), '.claude.json'), 'utf8'));
71
+ loggedIn = !!j.oauthAccount && j.hasCompletedOnboarding === true;
72
+ }
73
+ catch (e) {
74
+ loggedIn = e.code !== 'ENOENT';
75
+ }
76
+ checks.push({
77
+ name: 'claude login',
78
+ ok: loggedIn,
79
+ info: true,
80
+ detail: loggedIn ? 'logado' : 'ainda não logado — você loga uma vez no terminal do curso (rápido)',
81
+ });
82
+ }
83
+ // 8. node-pty (motor do terminal embutido) — bloqueia o install se não carrega.
84
+ let ptyOk = false;
85
+ try {
86
+ createRequire(import.meta.url)('node-pty');
87
+ ptyOk = true;
88
+ }
89
+ catch {
90
+ ptyOk = false;
91
+ }
92
+ checks.push({
93
+ name: 'node-pty',
94
+ ok: ptyOk,
95
+ detail: ptyOk ? 'ok' : 'falhou ao carregar — o terminal embutido não abre. Reinstale: npm i -g paris-immersion@latest',
96
+ });
47
97
  // Report
48
98
  console.log();
49
99
  console.log(c.bold('paris doctor'));
@@ -51,9 +101,9 @@ export async function doctorCommand(opts = {}) {
51
101
  console.log();
52
102
  let failures = 0;
53
103
  for (const ch of checks) {
54
- const icon = ch.ok ? c.green('✓') : c.red('✗');
104
+ const icon = ch.ok ? c.green('✓') : ch.info ? c.yellow('○') : c.red('✗');
55
105
  console.log(` ${icon} ${ch.name}${ch.detail ? c.dim(' — ' + ch.detail) : ''}`);
56
- if (!ch.ok)
106
+ if (!ch.ok && !ch.info)
57
107
  failures++;
58
108
  }
59
109
  console.log();
@@ -79,8 +129,8 @@ async function reportInstallStatus(checks, token, apiUrl) {
79
129
  console.error(c.yellow(' ! --report ignorado: sem credenciais (rode `paris login`).'));
80
130
  return;
81
131
  }
82
- const allOk = checks.every((c) => c.ok);
83
- const failedNames = checks.filter((c) => !c.ok).map((c) => c.name);
132
+ const allOk = checks.filter((c) => !c.info).every((c) => c.ok);
133
+ const failedNames = checks.filter((c) => !c.ok && !c.info).map((c) => c.name);
84
134
  const body = {
85
135
  step: 'all',
86
136
  status: allOk ? 'ok' : 'failed',
@@ -205,6 +205,28 @@ function ensureWorkspaceVendored(cwd) {
205
205
  /* best-effort — sem o bundle, o /tostudy ainda roda; só o fallback MCP fica velho */
206
206
  }
207
207
  }
208
+ /**
209
+ * O aluno está logado no Claude Code? Checagem FILE-BASED (rápida, zero-custo) —
210
+ * NÃO usar `claude -p` como probe (ele trava esperando input/streama). No mac o
211
+ * token OAuth fica no Keychain, mas `~/.claude.json` guarda `oauthAccount` +
212
+ * `hasCompletedOnboarding` quando logado. Sem arquivo (ENOENT) = nunca rodou =
213
+ * não-logado. Arquivo ilegível por OUTRO motivo = fail-open (não bloqueia o curso).
214
+ */
215
+ function isClaudeLoggedIn() {
216
+ try {
217
+ const raw = readFileSync(path.join(homedir(), '.claude.json'), 'utf8');
218
+ try {
219
+ const j = JSON.parse(raw);
220
+ return !!j.oauthAccount && j.hasCompletedOnboarding === true;
221
+ }
222
+ catch {
223
+ return true; // arquivo corrompido → não trava o curso
224
+ }
225
+ }
226
+ catch (e) {
227
+ return e.code !== 'ENOENT';
228
+ }
229
+ }
208
230
  export function spawnCoachPty(cwd) {
209
231
  const req = createRequire(import.meta.url);
210
232
  ensureSpawnHelperExecutable(req);
@@ -214,9 +236,27 @@ export function spawnCoachPty(cwd) {
214
236
  ensureWorkspaceVendored(cwd);
215
237
  const kickoff = resolveKickoff(cwd);
216
238
  const shell = process.env.SHELL || (process.platform === 'win32' ? 'powershell.exe' : 'bash');
217
- const shellArgs = process.platform === 'win32'
218
- ? ['-NoLogo', '-Command', `claude "${kickoff.replace(/"/g, '`"')}"`]
219
- : ['-l', '-c', `claude '${kickoff.replace(/'/g, `'\\''`)}'`];
239
+ // --dangerously-skip-permissions: terminal de curso CONTROLADO — sem isso o Claude
240
+ // pede aprovação a CADA comando (`tostudy next` etc.) e o curso trava esperando o
241
+ // aluno escolher "Yes/No" sem entender. Aqui o tutor (/tostudy) precisa rodar a
242
+ // tostudy CLI livremente; é o mesmo modo que o terminal nativo do dev usa.
243
+ let shellArgs;
244
+ if (process.platform === 'win32') {
245
+ shellArgs = ['-NoLogo', '-Command', `claude --dangerously-skip-permissions "${kickoff.replace(/"/g, '`"')}"`];
246
+ }
247
+ else {
248
+ const q = kickoff.replace(/'/g, `'\\''`);
249
+ // (1) Claude não instalado → mensagem clara + cai num shell (sem tela branca/erro cru).
250
+ const notInstalled = `if ! command -v claude >/dev/null 2>&1; then ` +
251
+ `printf '\\n \\033[33m! O Claude Code nao esta instalado nesta maquina.\\033[0m\\n Rode: \\033[1mnpm i -g @anthropic-ai/claude-code\\033[0m e reabra a aba do curso.\\n\\n'; ` +
252
+ `exec bash -l; fi`;
253
+ // (2) Não logado → banner explicando o login (o kickoff /tostudy roda DEPOIS do
254
+ // login automaticamente — o Claude difere o prompt inicial até autenticar).
255
+ const loginBanner = isClaudeLoggedIn()
256
+ ? ''
257
+ : `printf '\\n \\033[36mAntes do curso voce vai logar no Claude Code uma vez (e rapido):\\033[0m\\n escolha um tema com as setas, depois Sign in with Claude, abra a URL e cole o codigo.\\n Assim que logar, o curso comeca sozinho.\\n\\n'; `;
258
+ shellArgs = ['-l', '-c', `${notInstalled}\n${loginBanner}exec claude --dangerously-skip-permissions '${q}'`];
259
+ }
220
260
  return ptyMod.spawn(shell, shellArgs, {
221
261
  name: 'xterm-color',
222
262
  cols: 80,
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.17');
16
+ .version('0.1.18');
17
17
  program
18
18
  .command('login')
19
19
  .description('Autenticar com sua conta Paris (device-code OR --token direto)')
@@ -168,6 +168,20 @@ export async function startRelayAgent(opts) {
168
168
  }
169
169
  catch (err) {
170
170
  log(`relay: pty spawn falhou (${err instanceof Error ? err.message : String(err)})`);
171
+ // Sem isto o viewer já recebeu 'agent-online' (verde "conectado") e ficaria
172
+ // com a TELA EM BRANCO pra sempre, sem pista. Manda um frame de texto E2E
173
+ // explicando + o conserto — renderiza direto no xterm do aluno.
174
+ if (ws && ws.readyState === WebSocket.OPEN) {
175
+ const msg = '\r\n \x1b[33m! Nao consegui abrir o terminal nesta maquina.\x1b[0m\r\n' +
176
+ ' Rode no Terminal: \x1b[1mnpm i -g paris-immersion@latest && paris restart\x1b[0m\r\n' +
177
+ ' Se persistir, rode o instalador de novo (a CLI pode nao ter compilado aqui).\r\n\r\n';
178
+ try {
179
+ ws.send(seal(key, TYPE_DATA, Buffer.from(msg, 'utf8')), { binary: true });
180
+ }
181
+ catch {
182
+ /* ws caiu */
183
+ }
184
+ }
171
185
  return;
172
186
  }
173
187
  pty.onData((d) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paris-immersion",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
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": {