paris-immersion 0.1.17 → 0.1.19
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/dist/commands/doctor.js +54 -4
- package/dist/commands/serve.js +43 -3
- package/dist/commands/tostudy-login.d.ts +19 -0
- package/dist/commands/tostudy-login.js +101 -0
- package/dist/index.js +7 -1
- package/dist/lib/relay-agent.js +14 -0
- package/dist/templates/tostudy-command.js +7 -1
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -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',
|
package/dist/commands/serve.js
CHANGED
|
@@ -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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type TostudyLoginOptions = {
|
|
2
|
+
/** Re-loga mesmo se já houver sessão (default: respeita a sessão existente). */
|
|
3
|
+
force?: boolean;
|
|
4
|
+
/** Silencia o passo-a-passo (usado quando chamado de dentro do curso). */
|
|
5
|
+
quiet?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Conecta o ToStudy CLI à conta da imersão — a rede de segurança quando o
|
|
9
|
+
* auto-login do install falhou (tipicamente a provisão ainda estava pendente:
|
|
10
|
+
* ela é async, então no install o code volta 409 e o login não acontece; aqui,
|
|
11
|
+
* no momento de abrir o curso, ela já costuma ter terminado).
|
|
12
|
+
*
|
|
13
|
+
* REGRA do produto: se o aluno JÁ tem sessão ToStudy (a conta dele), NÃO
|
|
14
|
+
* mexemos — o curso da imersão entra nela via claim-by-email. Só logamos via
|
|
15
|
+
* code quando NÃO há sessão (ou com --force).
|
|
16
|
+
*
|
|
17
|
+
* NUNCA loga o `code` (é single-use, efêmero) — só o resultado.
|
|
18
|
+
*/
|
|
19
|
+
export declare function tostudyLoginCommand(opts?: TostudyLoginOptions): Promise<void>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { readCredentials } from '../lib/credentials.js';
|
|
4
|
+
import { apiFetch, defaultApiUrl, ApiError } from '../lib/api.js';
|
|
5
|
+
import { c } from '../lib/colors.js';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8
|
+
/** `tostudy courses` exit 0 = sessão ToStudy válida nesta máquina. */
|
|
9
|
+
async function tostudyAuthed() {
|
|
10
|
+
try {
|
|
11
|
+
await execFileAsync('tostudy', ['courses'], { timeout: 12000 });
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Conecta o ToStudy CLI à conta da imersão — a rede de segurança quando o
|
|
20
|
+
* auto-login do install falhou (tipicamente a provisão ainda estava pendente:
|
|
21
|
+
* ela é async, então no install o code volta 409 e o login não acontece; aqui,
|
|
22
|
+
* no momento de abrir o curso, ela já costuma ter terminado).
|
|
23
|
+
*
|
|
24
|
+
* REGRA do produto: se o aluno JÁ tem sessão ToStudy (a conta dele), NÃO
|
|
25
|
+
* mexemos — o curso da imersão entra nela via claim-by-email. Só logamos via
|
|
26
|
+
* code quando NÃO há sessão (ou com --force).
|
|
27
|
+
*
|
|
28
|
+
* NUNCA loga o `code` (é single-use, efêmero) — só o resultado.
|
|
29
|
+
*/
|
|
30
|
+
export async function tostudyLoginCommand(opts = {}) {
|
|
31
|
+
const log = (s) => { if (!opts.quiet)
|
|
32
|
+
console.log(s); };
|
|
33
|
+
const creds = await readCredentials().catch(() => null);
|
|
34
|
+
if (!creds) {
|
|
35
|
+
console.error(c.red('Não logado no paris. Rode `paris login` primeiro.'));
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!opts.force && (await tostudyAuthed())) {
|
|
40
|
+
log(c.green('ToStudy já está conectado — o curso da imersão usa essa conta.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Mintar o code single-use. A provisão é async: enquanto pendente, a rota
|
|
44
|
+
// responde 409 (tostudy_provision_pending) e dispara a provisão em background
|
|
45
|
+
// — re-tentamos até o code sair (ou esgotar). 404 = sem participante (bail).
|
|
46
|
+
let code = '';
|
|
47
|
+
let apiUrl = '';
|
|
48
|
+
for (let i = 0; i < 8; i++) {
|
|
49
|
+
try {
|
|
50
|
+
const r = await apiFetch('/api/onboarding/v2/tostudy-cli-code', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body: {},
|
|
53
|
+
token: creds.token,
|
|
54
|
+
apiUrl: creds.api_url ?? defaultApiUrl(),
|
|
55
|
+
});
|
|
56
|
+
code = r.code;
|
|
57
|
+
apiUrl = r.apiUrl;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
62
|
+
console.error(c.yellow('Sem participante ativo nesta conta — nada pra conectar.'));
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const transient = e instanceof ApiError && (e.status === 409 || e.status >= 500);
|
|
67
|
+
if (!transient && i === 0) {
|
|
68
|
+
// Erro inesperado logo de cara — não fica martelando.
|
|
69
|
+
console.error(c.yellow('Não consegui preparar o acesso ao ToStudy agora. Rode `tostudy login` ou avise a equipe.'));
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (i < 7) {
|
|
74
|
+
log(c.dim(`preparando seu acesso ao ToStudy… (${i + 1}/8)`));
|
|
75
|
+
await sleep(4000);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!code || !apiUrl) {
|
|
80
|
+
console.error(c.yellow('Seu curso ainda está sendo preparado. Tente de novo em ~1 min — se persistir, avise a equipe da imersão.'));
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Logout primeiro garante que a sessão da imersão entre limpa (a sessão é
|
|
85
|
+
// global, uma conta por vez). Best-effort.
|
|
86
|
+
try {
|
|
87
|
+
await execFileAsync('tostudy', ['logout'], { timeout: 12000 });
|
|
88
|
+
}
|
|
89
|
+
catch { /* sem sessão prévia: ok */ }
|
|
90
|
+
try {
|
|
91
|
+
await execFileAsync('tostudy', ['login', '--code', code, '--api-url', apiUrl], { timeout: 20000 });
|
|
92
|
+
}
|
|
93
|
+
catch { /* o verify abaixo decide */ }
|
|
94
|
+
if (await tostudyAuthed()) {
|
|
95
|
+
log(c.green('✓ ToStudy conectado à conta da imersão. Reabra o curso.'));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.error(c.yellow('Não consegui conectar o ToStudy automaticamente. Rode `tostudy login` manualmente e reabra o curso.'));
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,10 +10,11 @@ import { graduateCommand } from './commands/graduate.js';
|
|
|
10
10
|
import { logoutCommand } from './commands/logout.js';
|
|
11
11
|
import { serveCommand } from './commands/serve.js';
|
|
12
12
|
import { daemonCommand, restartCommand } from './commands/daemon.js';
|
|
13
|
+
import { tostudyLoginCommand } from './commands/tostudy-login.js';
|
|
13
14
|
const program = new Command()
|
|
14
15
|
.name('paris')
|
|
15
16
|
.description('Paris Immersion — CLI do aluno (login, workspace, tarefas)')
|
|
16
|
-
.version('0.1.
|
|
17
|
+
.version('0.1.19');
|
|
17
18
|
program
|
|
18
19
|
.command('login')
|
|
19
20
|
.description('Autenticar com sua conta Paris (device-code OR --token direto)')
|
|
@@ -65,6 +66,11 @@ program
|
|
|
65
66
|
.command('restart')
|
|
66
67
|
.description('Religa o agente do terminal embutido (libera a porta + reinicia o serviço). Use se o terminal não conectar.')
|
|
67
68
|
.action(restartCommand);
|
|
69
|
+
program
|
|
70
|
+
.command('tostudy-login')
|
|
71
|
+
.description('Conecta o ToStudy CLI à conta da imersão (rede de segurança quando o curso não abre). Respeita sessão existente.')
|
|
72
|
+
.option('--force', 'Re-loga mesmo se já houver sessão ToStudy')
|
|
73
|
+
.action(tostudyLoginCommand);
|
|
68
74
|
program.parseAsync().catch((err) => {
|
|
69
75
|
console.error('\n[paris] erro:', err?.message ?? err);
|
|
70
76
|
process.exit(1);
|
package/dist/lib/relay-agent.js
CHANGED
|
@@ -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) => {
|
|
@@ -12,7 +12,13 @@ export function renderTostudyCommand() {
|
|
|
12
12
|
description: Inicia ou retoma o curso de onboarding da imersão (tutor ToStudy no terminal)
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
Você é o **tutor do curso de onboarding** da imersão Paris Group, rodando no terminal do aluno. Conduza o curso da ToStudy em **português**, no ritmo do aluno.
|
|
15
|
+
Você é o **tutor do curso de onboarding** da imersão Paris Group, rodando no terminal do aluno. Conduza o curso da ToStudy em **português**, no ritmo do aluno.
|
|
16
|
+
|
|
17
|
+
## 0. Garanta o acesso ao ToStudy (em silêncio, ANTES de tudo)
|
|
18
|
+
|
|
19
|
+
1. Rode \`tostudy courses --json\`. Se listar o curso, siga para o passo 1.
|
|
20
|
+
2. Se der **erro de autenticação / "não autenticado"**, rode \`paris tostudy-login\` (conecta a conta da imersão; é normal a primeira vez, porque a matrícula é preparada em segundo plano). Depois rode \`tostudy courses --json\` de novo.
|
|
21
|
+
3. Se mesmo assim continuar sem curso, avise o aluno **uma vez**, gentil: "Seu curso ainda está sendo preparado — tente reabrir em 1 minuto, ou avise a equipe da imersão." e use o fallback do passo 3 (MCP). Nunca trave nem fique em loop.
|
|
16
22
|
|
|
17
23
|
## 1. Prepare o curso (em silêncio, antes de falar)
|
|
18
24
|
|
package/package.json
CHANGED