@nectarcrm/ccx 0.4.0 → 0.4.2

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.
Files changed (2) hide show
  1. package/bin/ccx.js +21 -18
  2. package/package.json +1 -1
package/bin/ccx.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // (terminal, VS Code, JetBrains) usa essa conta. Claudes já abertos continuam na anterior.
17
17
  // O vigia roda sozinho a cada 5 min: avisa o app, atualiza o token se o admin trocou
18
18
  // e tira a conta se o acesso for removido. Os tokens gastos vão pro app pela telemetria.
19
- // O vigia também roda o sync, então o resumo dos projetos chega sozinho.
19
+ // O resumo dos projetos só vai pro app quando você roda ccx sync.
20
20
 
21
21
  import { execFileSync } from 'node:child_process'
22
22
  import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync, writeFileSync } from 'node:fs'
@@ -45,13 +45,24 @@ const fingerprint = t => createHash('sha256').update(t).digest('hex').slice(0, 1
45
45
  // macOS: Keychain. Windows: DPAPI (só o usuário do Windows abre). Linux: cofre do sistema
46
46
  // (secret-tool/libsecret) ou, sem ele (servidor, WSL), arquivo que só o usuário lê.
47
47
  // O segredo sempre passa por stdin, nunca pela linha de comando.
48
- const ps = (script, input) => execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], { input, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim()
48
+ // PSModulePath herdado de um PowerShell 7 impede o 5.1 de carregar módulos (ConvertTo-SecureString some),
49
+ // então o DPAPI é chamado direto pelo .NET e o PSModulePath é descartado.
50
+ const ps = (script, input) => {
51
+ const { PSModulePath, ...env } = process.env
52
+ return execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], { input, env, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim()
53
+ }
54
+ const DPAPI = "Add-Type -AssemblyName System.Security; $v = [Console]::In.ReadToEnd().Trim(); "
55
+ const DPAPI_PREFIX = 'dpapi:'
49
56
  const KEY_FILE = join(DIR, 'chave') // Windows (DPAPI) e Linux sem cofre
50
57
  const SECRET_TOOL = ['service', SERVICE, 'account', 'chave']
51
58
 
52
59
  function secretGet() {
53
60
  try {
54
- if (WIN) return ps('$s = [Console]::In.ReadToEnd().Trim() | ConvertTo-SecureString; [Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($s))', readFileSync(KEY_FILE, 'utf8'))
61
+ if (WIN) {
62
+ const stored = readFileSync(KEY_FILE, 'utf8').trim()
63
+ if (!stored.startsWith(DPAPI_PREFIX)) return ps('$s = [Console]::In.ReadToEnd().Trim() | ConvertTo-SecureString; [Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($s))', stored) // formato antigo
64
+ return ps(DPAPI + '[Text.Encoding]::UTF8.GetString([Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($v), $null, "CurrentUser"))', stored.slice(DPAPI_PREFIX.length))
65
+ }
55
66
  if (MAC) return execFileSync('security', ['find-generic-password', '-s', SERVICE, '-a', 'chave', '-w'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
56
67
  if (existsSync(KEY_FILE)) return readFileSync(KEY_FILE, 'utf8').trim()
57
68
  return execFileSync('secret-tool', ['lookup', ...SECRET_TOOL], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim() || null
@@ -61,7 +72,7 @@ function secretGet() {
61
72
  function secretSet(value) {
62
73
  if (WIN) {
63
74
  mkdirSync(DIR, { recursive: true })
64
- writeFileSync(KEY_FILE, ps('[Console]::In.ReadToEnd().Trim() | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString', value))
75
+ writeFileSync(KEY_FILE, DPAPI_PREFIX + ps(DPAPI + '[Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect([Text.Encoding]::UTF8.GetBytes($v), $null, "CurrentUser"))', value))
65
76
  return
66
77
  }
67
78
  if (MAC) {
@@ -200,8 +211,9 @@ function installAgent() {
200
211
  return true
201
212
  }
202
213
  if (WIN) {
214
+ // node.exe é app de console: direto pelo Agendador abre uma janela a cada rodada. O conhost --headless roda sem janela.
203
215
  execFileSync('schtasks', ['/Create', '/F', '/SC', 'MINUTE', '/MO', String(AGENT_MINUTES), '/TN', AGENT_ID,
204
- '/TR', `"${process.execPath}" "${SELF}" vigia`], { stdio: 'ignore' })
216
+ '/TR', `conhost.exe --headless "${process.execPath}" "${SELF}" vigia`], { stdio: 'ignore' })
205
217
  return true
206
218
  }
207
219
  return installLinuxAgent()
@@ -232,8 +244,6 @@ async function agentOnce() {
232
244
  writeJson(APP, removeAccount(app))
233
245
  return log('login vencido: conta tirada deste computador')
234
246
  }
235
- try { await sync(app, key, true) }
236
- catch (e) { log(`sync falhou: ${e.message}`) }
237
247
  const r = await api(app, key, 'POST', '/api/cli/heartbeat', { account_id: app.account?.id ?? null }, 20_000)
238
248
  if (r.status === 401) {
239
249
  writeJson(APP, removeAccount(app))
@@ -367,7 +377,7 @@ function addToDay(days, e) {
367
377
  return key
368
378
  }
369
379
 
370
- // Só um sync por vez (o vigia pode rodar junto com o login). Trava velha (sync que caiu) é ignorada.
380
+ // Só um sync por vez (dois ccx sync ao mesmo tempo). Trava velha (sync que caiu) é ignorada.
371
381
  function takeLock() {
372
382
  mkdirSync(DIR, { recursive: true })
373
383
  try { closeSync(openSync(SYNC_LOCK, 'wx')); return true }
@@ -378,8 +388,8 @@ function takeLock() {
378
388
  }
379
389
  }
380
390
 
381
- async function sync(app, key, quiet = false) {
382
- const say = msg => { if (!quiet) process.stderr.write(msg) }
391
+ async function sync(app, key) {
392
+ const say = msg => process.stderr.write(msg)
383
393
  if (!takeLock()) return say('Outro sync já está rodando. Tente de novo daqui a pouco.\n')
384
394
  try {
385
395
  let state = readJson(SYNC_STATE, {})
@@ -552,7 +562,7 @@ async function doctor() {
552
562
  }
553
563
  else ok('Telemetria ligada (tokens vão pro painel)')
554
564
  if (agentInstalled()) ok(`Vigia instalado (roda a cada ${AGENT_MINUTES} min)`)
555
- else aviso('O vigia não está instalado: token novo e sync não chegam sozinhos', 'ccx (escolha a conta de novo)')
565
+ else aviso('O vigia não está instalado: token novo e acesso removido não chegam sozinhos', 'ccx (escolha a conta de novo)')
556
566
  // Só o que deu errado nas últimas 24 h (o log tem a data no começo de cada linha)
557
567
  const dayAgo = new Date(Date.now() - 86_400_000).toISOString()
558
568
  const lastErr = existsSync(LOG) && readFileSync(LOG, 'utf8').trim().split('\n').filter(l => l >= dayAgo && /falhou|removid|vencid/.test(l)).pop()
@@ -729,10 +739,3 @@ if (cmd === 'status') {
729
739
 
730
740
  // `ccx`, `ccx login ...` (depois de autorizar) ou `ccx <conta>`
731
741
  await useAccount(app, key, cmd && cmd !== 'login' ? cmd : await chooseAccount(app, key))
732
-
733
- // Logo depois do login, manda o resumo das sessões (as próximas vão pelo vigia)
734
- if (cmd === 'login') {
735
- console.error('')
736
- try { await sync(app, key) }
737
- catch (e) { console.error(c(33, `\nO sync falhou (${e.message}). Tente: ccx sync`)) }
738
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectarcrm/ccx",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Escolhe qual conta Claude este computador usa, conectado ao app Contas Claude da Nectar.",
5
5
  "type": "module",
6
6
  "bin": { "ccx": "bin/ccx.js" },