@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.
- package/bin/ccx.js +21 -18
- 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
|
|
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
|
-
|
|
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)
|
|
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('[
|
|
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 (
|
|
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
|
|
382
|
-
const say = 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
|
|
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
|
-
}
|