@nectarcrm/ccx 0.2.0 → 0.3.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.
- package/README.md +1 -0
- package/bin/ccx.js +134 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ ccx login # autoriza este computador (abre o navegador
|
|
|
17
17
|
ccx # mostra suas contas e troca a conta deste computador
|
|
18
18
|
ccx claude-infra # troca direto
|
|
19
19
|
ccx status # conta atual e uso de cada conta
|
|
20
|
+
ccx doctor # confere se o claude está mesmo usando a conta escolhida (e o que pode atrapalhar)
|
|
20
21
|
ccx sync # manda pro app o resumo das sessões (roda sozinho após o login e a cada 5 min)
|
|
21
22
|
ccx sair # o claude volta ao seu login normal
|
|
22
23
|
ccx logout # sai do app neste computador
|
package/bin/ccx.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// ccx mostra suas contas e troca a conta deste computador
|
|
7
7
|
// ccx <conta> troca direto (nome curto, ex.: claude-infra)
|
|
8
8
|
// ccx status conta atual e uso das contas liberadas pra você
|
|
9
|
+
// ccx doctor confere se o claude está mesmo usando a conta escolhida
|
|
9
10
|
// ccx sync manda pro app o resumo das sessões do Claude (totais por projeto e dia; nunca o texto)
|
|
10
11
|
// ccx sair tira a conta deste computador (o claude volta ao login normal)
|
|
11
12
|
// ccx logout sai do app: tira a conta, a autorização e o vigia
|
|
@@ -402,6 +403,136 @@ async function sync(app, key, quiet = false) {
|
|
|
402
403
|
finally { rmSync(SYNC_LOCK, { force: true }) }
|
|
403
404
|
}
|
|
404
405
|
|
|
406
|
+
// ---------- doctor: confere se o claude está mesmo usando a conta escolhida ----------
|
|
407
|
+
// Nunca mostra token: compara só a impressão digital (fingerprint).
|
|
408
|
+
const AUTH_VARS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX']
|
|
409
|
+
const MANAGED = WIN ? ['C:\\Program Files\\ClaudeCode\\managed-settings.json', 'C:\\ProgramData\\ClaudeCode\\managed-settings.json']
|
|
410
|
+
: MAC ? ['/Library/Application Support/ClaudeCode/managed-settings.json'] : ['/etc/claude-code/managed-settings.json']
|
|
411
|
+
|
|
412
|
+
function winUserEnv(name) {
|
|
413
|
+
try { return ps(`[Environment]::GetEnvironmentVariable('${name}','User'); [Environment]::GetEnvironmentVariable('${name}','Machine')`).split(/\r?\n/).find(Boolean) || null }
|
|
414
|
+
catch { return null }
|
|
415
|
+
}
|
|
416
|
+
function agentInstalled() {
|
|
417
|
+
try {
|
|
418
|
+
if (MAC) return existsSync(PLIST)
|
|
419
|
+
if (WIN) { run('schtasks', ['/Query', '/TN', AGENT_ID]); return true }
|
|
420
|
+
if (existsSync(join(UNIT_DIR, 'ccx-vigia.timer'))) return true
|
|
421
|
+
return run('crontab', ['-l']).includes(CRON_TAG)
|
|
422
|
+
}
|
|
423
|
+
catch { return false }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function doctor() {
|
|
427
|
+
let bad = 0, warn = 0
|
|
428
|
+
const ok = msg => console.log(`${c(32, '✓')} ${msg}`)
|
|
429
|
+
const aviso = (msg, fix) => { warn++; console.log(`${c(33, '!')} ${msg}${fix ? `\n ${c(90, '→ ' + fix)}` : ''}`) }
|
|
430
|
+
const erro = (msg, fix) => { bad++; console.log(`${c(31, '✗')} ${msg}${fix ? `\n ${c(90, '→ ' + fix)}` : ''}`) }
|
|
431
|
+
const mine = readJson(join(dirname(SELF), '..', 'package.json'), {}).version
|
|
432
|
+
|
|
433
|
+
console.log(c(1, 'ccx doctor\n'))
|
|
434
|
+
// Versões
|
|
435
|
+
const [major, minor] = process.versions.node.split('.').map(Number)
|
|
436
|
+
if (major > 18 || (major === 18 && minor >= 17)) ok(`Node ${process.versions.node}`)
|
|
437
|
+
else erro(`Node ${process.versions.node} é velho demais`, 'instale o Node 18.17 ou mais novo')
|
|
438
|
+
try {
|
|
439
|
+
const r = await fetch('https://registry.npmjs.org/@nectarcrm%2fccx/latest', { signal: AbortSignal.timeout(8000) })
|
|
440
|
+
const latest = (await r.json()).version
|
|
441
|
+
if (latest && latest !== mine) aviso(`ccx ${mine} (a mais nova é ${latest})`, 'npm install -g @nectarcrm/ccx@latest')
|
|
442
|
+
else ok(`ccx ${mine} (a mais nova)`)
|
|
443
|
+
}
|
|
444
|
+
catch { ok(`ccx ${mine} (não deu pra ver se há versão nova)`) }
|
|
445
|
+
try { ok(`Claude Code ${execFileSync('claude', ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], shell: WIN }).toString().trim()}`) }
|
|
446
|
+
catch { aviso('Não achei o comando claude neste terminal', 'instale o Claude Code ou abra um terminal novo') }
|
|
447
|
+
|
|
448
|
+
// Login no app
|
|
449
|
+
const app = readJson(APP, null)
|
|
450
|
+
const key = app && secretGet()
|
|
451
|
+
if (!app || !key) {
|
|
452
|
+
erro('Este computador não está conectado ao app', 'ccx login')
|
|
453
|
+
return finish()
|
|
454
|
+
}
|
|
455
|
+
if (new Date(app.expires_at) < new Date()) erro(`O login venceu em ${new Date(app.expires_at).toLocaleDateString('pt-BR')}`, 'ccx login')
|
|
456
|
+
else ok(`Conectado como ${app.email} em ${app.url} (vale até ${new Date(app.expires_at).toLocaleDateString('pt-BR')})`)
|
|
457
|
+
|
|
458
|
+
const st = await api(app, key, 'GET', '/api/cli/status', null, 20_000)
|
|
459
|
+
if (st.status !== 200) {
|
|
460
|
+
erro(`O app não respondeu: ${st.data.message ?? `HTTP ${st.status}`}`, st.status === 401 ? 'ccx login' : 'confira a internet ou fale com um admin')
|
|
461
|
+
return finish()
|
|
462
|
+
}
|
|
463
|
+
ok('O app respondeu e a chave deste computador vale')
|
|
464
|
+
|
|
465
|
+
// Conta escolhida
|
|
466
|
+
if (!app.account) {
|
|
467
|
+
erro('Nenhuma conta escolhida neste computador', 'ccx')
|
|
468
|
+
return finish()
|
|
469
|
+
}
|
|
470
|
+
const hb = await api(app, key, 'POST', '/api/cli/heartbeat', { account_id: app.account.id }, 20_000)
|
|
471
|
+
if (hb.status !== 200 || !hb.data.allowed) erro(`A conta ${app.account.label} não está mais liberada pra você`, 'ccx (e escolha outra) ou peça a um admin')
|
|
472
|
+
else ok(`Conta escolhida: ${app.account.label}, liberada`)
|
|
473
|
+
|
|
474
|
+
// Token no settings.json do Claude Code
|
|
475
|
+
let settings = null
|
|
476
|
+
try { settings = readJson(SETTINGS, {}) }
|
|
477
|
+
catch { erro(`${SETTINGS} não é um JSON válido`, 'corrija o arquivo e rode ccx de novo') }
|
|
478
|
+
const token = settings?.env?.CLAUDE_CODE_OAUTH_TOKEN
|
|
479
|
+
if (process.env.CLAUDE_CONFIG_DIR) aviso(`CLAUDE_CONFIG_DIR=${process.env.CLAUDE_CONFIG_DIR}: o claude só usa a conta se também tiver essa variável`)
|
|
480
|
+
if (settings && !token) erro(`O token da conta não está em ${SETTINGS}`, 'ccx (escolha a conta de novo)')
|
|
481
|
+
else if (token && fingerprint(token) !== app.token_fp) erro('O token no settings.json não é o que o ccx gravou (alguém mexeu no arquivo)', 'ccx (escolha a conta de novo)')
|
|
482
|
+
else if (token && hb.data.token_fp && fingerprint(token) !== hb.data.token_fp) aviso('O admin trocou o token desta conta; o vigia atualiza em até 5 min', 'ou rode: ccx (escolha a conta de novo)')
|
|
483
|
+
else if (token) ok(`Token da conta ${app.account.label} gravado em ${SETTINGS}`)
|
|
484
|
+
|
|
485
|
+
// O que passa por cima do token
|
|
486
|
+
const envToken = process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
487
|
+
if (envToken && envToken !== token) erro('Este terminal tem outro CLAUDE_CODE_OAUTH_TOKEN, que pode passar por cima da conta do ccx', WIN ? 'apague a variável (comando abaixo) e abra um terminal novo' : 'tire o export do seu ~/.zshrc ou ~/.bashrc e abra um terminal novo')
|
|
488
|
+
if (WIN) {
|
|
489
|
+
const winToken = winUserEnv('CLAUDE_CODE_OAUTH_TOKEN')
|
|
490
|
+
if (winToken && winToken !== token) erro('O Windows tem um CLAUDE_CODE_OAUTH_TOKEN antigo guardado', `PowerShell: [Environment]::SetEnvironmentVariable('CLAUDE_CODE_OAUTH_TOKEN', $null, 'User') e abra um terminal novo`)
|
|
491
|
+
}
|
|
492
|
+
for (const v of AUTH_VARS) {
|
|
493
|
+
const val = process.env[v] || (WIN ? winUserEnv(v) : null) || settings?.env?.[v]
|
|
494
|
+
if (val) erro(`${v} está definida: ela passa por cima da conta do ccx`, `tire ${v} do terminal${WIN ? ', das variáveis do Windows' : ''} e do settings.json`)
|
|
495
|
+
}
|
|
496
|
+
if (settings?.apiKeyHelper) erro('O settings.json tem apiKeyHelper, que passa por cima da conta do ccx', `tire "apiKeyHelper" de ${SETTINGS}`)
|
|
497
|
+
|
|
498
|
+
// Settings de projeto (nesta pasta) e da empresa também podem trocar a conta
|
|
499
|
+
const overrides = [join(process.cwd(), '.claude', 'settings.json'), join(process.cwd(), '.claude', 'settings.local.json'), ...MANAGED]
|
|
500
|
+
for (const f of overrides) {
|
|
501
|
+
if (!existsSync(f) || f === SETTINGS) continue
|
|
502
|
+
let s
|
|
503
|
+
try { s = JSON.parse(readFileSync(f, 'utf8')) }
|
|
504
|
+
catch { continue }
|
|
505
|
+
const keys = ['CLAUDE_CODE_OAUTH_TOKEN', ...AUTH_VARS].filter(k => s?.env?.[k])
|
|
506
|
+
if (s?.apiKeyHelper) keys.push('apiKeyHelper')
|
|
507
|
+
if (keys.length) erro(`${f} define ${keys.join(', ')}: passa por cima da conta do ccx nesta pasta`, 'tire essas chaves do arquivo')
|
|
508
|
+
}
|
|
509
|
+
if (!bad) ok('Nada passando por cima da conta do ccx')
|
|
510
|
+
|
|
511
|
+
// Telemetria e vigia
|
|
512
|
+
const env = settings?.env ?? {}
|
|
513
|
+
if (env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT !== `${app.url}/api/cli/otel/v1/logs` || env.OTEL_EXPORTER_OTLP_LOGS_HEADERS !== `Authorization=Bearer ${key}`) {
|
|
514
|
+
aviso('A telemetria não está apontando pro app com a chave atual (os tokens não aparecem no painel)', 'ccx (escolha a conta de novo)')
|
|
515
|
+
}
|
|
516
|
+
else ok('Telemetria ligada (tokens vão pro painel)')
|
|
517
|
+
if (agentInstalled()) ok(`Vigia instalado (roda a cada ${AGENT_MINUTES} min)`)
|
|
518
|
+
else aviso('O vigia não está instalado: token novo e sync não chegam sozinhos', 'ccx (escolha a conta de novo)')
|
|
519
|
+
// Só o que deu errado nas últimas 24 h (o log tem a data no começo de cada linha)
|
|
520
|
+
const dayAgo = new Date(Date.now() - 86_400_000).toISOString()
|
|
521
|
+
const lastErr = existsSync(LOG) && readFileSync(LOG, 'utf8').trim().split('\n').filter(l => l >= dayAgo && /falhou|removid|vencid/.test(l)).pop()
|
|
522
|
+
if (lastErr) aviso(`Problema recente do vigia: ${lastErr}`)
|
|
523
|
+
const sync = existsSync(SYNC_STATE) && statSync(SYNC_STATE).mtime
|
|
524
|
+
if (sync) ok(`Último sync: ${sync.toLocaleString('pt-BR')}`)
|
|
525
|
+
else aviso('O sync nunca rodou neste computador', 'ccx sync')
|
|
526
|
+
|
|
527
|
+
return finish()
|
|
528
|
+
|
|
529
|
+
function finish() {
|
|
530
|
+
console.log(`\n${bad ? c(31, `${bad} problema(s)`) : c(32, 'Tudo certo')}${warn ? c(33, ` · ${warn} aviso(s)`) : ''}.`)
|
|
531
|
+
if (!bad && app?.account) console.log(c(90, `Todo claude aberto a partir de agora usa a conta ${app.account.label}. Os que já estavam abertos continuam na anterior até serem fechados.`))
|
|
532
|
+
process.exit(bad ? 1 : 0)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
405
536
|
// ---------- saída ----------
|
|
406
537
|
const isTTY = process.stderr.isTTY
|
|
407
538
|
const c = (code, s) => (isTTY ? `\x1b[${code}m${s}\x1b[0m` : s)
|
|
@@ -506,7 +637,7 @@ async function useAccount(app, key, wanted) {
|
|
|
506
637
|
const [cmd, ...rest] = process.argv.slice(2)
|
|
507
638
|
|
|
508
639
|
if (cmd === 'help' || cmd === '-h' || cmd === '--help') {
|
|
509
|
-
console.log(readFileSync(SELF, 'utf8').split('\n').filter(l => l.startsWith('//')).slice(0,
|
|
640
|
+
console.log(readFileSync(SELF, 'utf8').split('\n').filter(l => l.startsWith('//')).slice(0, 18).map(l => l.slice(3)).join('\n'))
|
|
510
641
|
process.exit(0)
|
|
511
642
|
}
|
|
512
643
|
|
|
@@ -520,6 +651,8 @@ if (cmd === 'vigia') {
|
|
|
520
651
|
process.exit(0)
|
|
521
652
|
}
|
|
522
653
|
|
|
654
|
+
if (cmd === 'doctor') await doctor()
|
|
655
|
+
|
|
523
656
|
if (cmd === 'login') await login(rest[0] ?? readJson(APP, null)?.url ?? DEFAULT_URL)
|
|
524
657
|
|
|
525
658
|
const app = readJson(APP, null)
|