auroq-os 2.8.0 → 2.8.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.
@@ -0,0 +1,869 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Auroq OS — Rota B (Infisical gratis): tudo que toca o binario `infisical`.
5
+ *
6
+ * Achar o programa e a versao, instalar sem admin, ver se ha sessao, entrar pelo navegador, ler o token
7
+ * da sessao do aluno e sair. Nenhuma funcao daqui cria conta, projeto ou identidade (isso e da API, em
8
+ * lib/infisical-api.js).
9
+ *
10
+ * Regras que valem para o arquivo inteiro:
11
+ * - Nenhum segredo (JWT da sessao, client secret) vai para tela, log, mensagem de erro ou argumento de
12
+ * comando. O JWT so volta no retorno de `getUserToken`; qualquer texto do CLI que va para `detalhe`
13
+ * passa por `scrub` (apaga JWT, 64 hex, st.- e ops_-tokens).
14
+ * - Nenhum passo usa sudo/admin, e nenhum edita o PATH do aluno (nem o do processo): o chamador guarda o
15
+ * CAMINHO ABSOLUTO do binario.
16
+ * - O filho nunca herda INFISICAL_* / TOKEN do aluno (viram credencial do CLI e mudam a sessao) nem as
17
+ * variaveis AUROQ_INFISICAL_* (a chave do agente: o CLI nao precisa dela).
18
+ * - Tudo que fala com o mundo entra por `d` (deps): `run`, `download`, `fs`, `exists`, `platform`, `arch`, `env`, `tmpdir`.
19
+ * Os testes so usam falsos; `makeDefaultCliDeps()` traz os reais. Forma de cada um:
20
+ * run(file, args, { env, input, timeoutMs, stdinIgnore, onStderr }) -> Promise<{ ok, status, stdout, stderr, timedOut, missing }>
21
+ * download(url) -> Promise<Buffer> (lanca se falhar; `error.status` quando for resposta HTTP)
22
+ * fs: mkdtempSync, mkdirSync, writeFileSync, copyFileSync, chmodSync, renameSync, rmSync, readdirSync, existsSync
23
+ * - Funcoes com rede/processo sao `async` (o `run` padrao e assincrono). Nenhuma lanca por falha do CLI.
24
+ *
25
+ * Fatos de comportamento do binario: pesquisa/resultado-cli.md e resultado-instalacao.md.
26
+ *
27
+ * @module auroq-os/infisical-cli
28
+ */
29
+
30
+ const os = require('os');
31
+ const path = require('path');
32
+ const crypto = require('crypto');
33
+ const fs = require('fs');
34
+ const { spawn, spawnSync } = require('child_process');
35
+
36
+ // Versao minima aceita: login por navegador SEM terminal + perfis (--profile/--save-as) so existem a partir daqui
37
+ // (resultado-cli.md, secao 7: "Login por navegador seguro sem TTY: v0.43.139"). O projeto e criado pela API,
38
+ // entao o `sm projects create` (0.44.3) nao e necessario.
39
+ const MIN_CLI_VERSION = '0.43.139';
40
+ // Versao que o instalador direto baixa. Fixa de proposito ("latest" muda todo dia e a API do GitHub tem limite baixo).
41
+ const PINNED_CLI_VERSION = '0.44.8';
42
+ // Perfil proprio do Auroq no CLI: nao mexe no login que o aluno ja tenha para o trabalho dele.
43
+ const PROFILE = 'auroq';
44
+ const DEFAULT_DOMAIN = 'https://app.infisical.com';
45
+
46
+ const RELEASE_BASE = 'https://github.com/Infisical/cli/releases/download';
47
+ const VERSION_TIMEOUT_MS = 30000; // primeira execucao de um binario de 160 MB pode demorar
48
+ const STATUS_TIMEOUT_MS = 30000;
49
+ const TOKEN_TIMEOUT_MS = 30000;
50
+ const LOGOUT_TIMEOUT_MS = 60000;
51
+ const EXTRACT_TIMEOUT_MS = 300000;
52
+ const INSTALL_TIMEOUT_MS = 15 * 60 * 1000;
53
+ const DEFAULT_LOGIN_TIMEOUT_MS = 540000;
54
+ const DOWNLOAD_TIMEOUT_MS = 15 * 60 * 1000;
55
+ const DOWNLOAD_MAX_BYTES = 400 * 1024 * 1024;
56
+ const OUTPUT_CAP = 16 * 1024 * 1024;
57
+ const SCRUB_MAX_INPUT = 2000;
58
+
59
+ // ─── Helpers puros ───────────────────────────────────────
60
+
61
+ const pathFor = (platform) => (platform === 'win32' ? path.win32 : path.posix);
62
+
63
+ /** Le variavel de ambiente sem ligar para maiusculas (no Windows `Path` e `PATH` sao a mesma). */
64
+ function envGet(env, name) {
65
+ if (!env) return undefined;
66
+ if (env[name] !== undefined) return env[name];
67
+ const low = name.toLowerCase();
68
+ for (const k of Object.keys(env)) {
69
+ if (k.toLowerCase() === low) return env[k];
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
75
+ const stripAnsi = (text) => String(text == null ? '' : text).replace(ANSI_RE, '');
76
+
77
+ /**
78
+ * Apaga de um texto o que parece segredo (JWT, client secret de 64 hex, token st. e ops_), junta em uma linha
79
+ * e limita o tamanho. Tudo o que o CLI disser e vai para `detalhe` passa por aqui.
80
+ */
81
+ function scrub(text, max = 300) {
82
+ // Corta ANTES das regex: as de 'token' custam N^2 em texto longo e o stderr do CLI pode ter megabytes.
83
+ // O resultado final tem `max` caracteres, entao o que passa de SCRUB_MAX_INPUT nunca apareceria mesmo.
84
+ let t = stripAnsi(String(text == null ? '' : text).slice(0, SCRUB_MAX_INPUT * 4)).replace(/\s+/g, ' ').trim().slice(0, SCRUB_MAX_INPUT);
85
+ t = t.replace(/\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]*/g, '[oculto]');
86
+ t = t.replace(/\b[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,}\b/g, '[oculto]');
87
+ t = t.replace(/\b[0-9a-fA-F]{64}\b/g, '[oculto]');
88
+ t = t.replace(/\bst\.[A-Za-z0-9._-]{10,}/g, '[oculto]');
89
+ t = t.replace(/\bops_[A-Za-z0-9_=+/.-]{10,}/g, '[oculto]');
90
+ t = t.replace(/\s+/g, ' ').trim();
91
+ return t.length > max ? `${t.slice(0, max - 3)}...` : t;
92
+ }
93
+
94
+ /** Ultima linha util do stderr (preferindo a que fala de erro), ja sem segredo. */
95
+ function lastErrorLine(stderr, max = 200) {
96
+ const lines = stripAnsi(stderr).split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
97
+ if (!lines.length) return '';
98
+ const hit = [...lines].reverse().find((l) => /(error|failed|unable|denied|not found|does not exist|timeout|refused|no such host)/i.test(l));
99
+ return scrub(hit || lines[lines.length - 1], max);
100
+ }
101
+
102
+ const NET_RE = /(no such host|dial tcp|connection refused|connection reset|network is unreachable|i\/o timeout|tls handshake|enotfound|econnrefused|econnreset|eai_again|unable to complete api request|temporary failure in name resolution)/i;
103
+
104
+ function validProfile(profile) {
105
+ return typeof profile === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$/.test(profile);
106
+ }
107
+
108
+ function validDomain(domain) {
109
+ return typeof domain === 'string' && /^https?:\/\/[^\s"'<>]+$/i.test(domain);
110
+ }
111
+
112
+ /**
113
+ * Ambiente do `infisical` filho. Tira INFISICAL_* e TOKEN (o CLI le INFISICAL_TOKEN/TOKEN como credencial
114
+ * e eles passam por cima da sessao do perfil `auroq`) e as variaveis da chave do agente; liga a
115
+ * desativacao do aviso de atualizacao (senao cada comando espera ate ~16 s pelo GitHub). Tira tambem as
116
+ * variaveis OP_* (token do 1Password): cada rota so enxerga a sua credencial.
117
+ */
118
+ function cliEnv(base) {
119
+ const src = base || process.env;
120
+ const env = {};
121
+ for (const [k, v] of Object.entries(src)) {
122
+ if (v === undefined || v === null) continue;
123
+ // OP_*: a credencial da outra rota (1Password) nao tem por que chegar ao infisical, curl, tar, brew ou winget
124
+ if (/^(AUROQ_)?INFISICAL_/i.test(k) || /^TOKEN$/i.test(k) || /^OP_/i.test(k)) continue;
125
+ env[k] = v;
126
+ }
127
+ env.INFISICAL_DISABLE_UPDATE_CHECK = '1';
128
+ return env;
129
+ }
130
+
131
+ /** `infisical --version` imprime `infisical version 0.44.8`. Build local imprime `devel` e lixo vira null. */
132
+ function parseCliVersion(stdout) {
133
+ const m = /^infisical version (\d+\.\d+\.\d+)/m.exec(stripAnsi(stdout));
134
+ return m ? m[1] : null;
135
+ }
136
+
137
+ function splitVersion(v) {
138
+ if (typeof v !== 'string') return null;
139
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
140
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
141
+ }
142
+
143
+ /** Comparacao numerica (0.43.1000 > 0.43.139). Versao ilegivel nunca passa. */
144
+ function isCliVersionAtLeast(version, min = MIN_CLI_VERSION) {
145
+ const a = splitVersion(version);
146
+ const b = splitVersion(min);
147
+ if (!a || !b) return false;
148
+ for (let i = 0; i < 3; i++) {
149
+ if (a[i] > b[i]) return true;
150
+ if (a[i] < b[i]) return false;
151
+ }
152
+ return true;
153
+ }
154
+
155
+ /** Pastas onde o `infisical` costuma ficar quando o PATH do terminal nao o enxerga. */
156
+ function knownCliDirs(platform, env) {
157
+ const P = pathFor(platform);
158
+ const e = env || process.env;
159
+ const dirs = [];
160
+ const add = (...parts) => {
161
+ if (parts.every(Boolean)) dirs.push(P.join(...parts));
162
+ };
163
+ if (platform === 'win32') {
164
+ const local = envGet(e, 'LOCALAPPDATA');
165
+ const profile = envGet(e, 'USERPROFILE');
166
+ const appData = envGet(e, 'APPDATA');
167
+ add(local, 'Programs', 'infisical');
168
+ add(local, 'Microsoft', 'WinGet', 'Links');
169
+ add(profile, 'scoop', 'shims');
170
+ add(profile, '.local', 'bin');
171
+ add(appData, 'npm', 'node_modules', '@infisical', 'cli', 'bin');
172
+ return dirs;
173
+ }
174
+ const home = envGet(e, 'HOME') || os.homedir();
175
+ add(home, '.local', 'bin');
176
+ if (platform === 'darwin') {
177
+ dirs.push('/opt/homebrew/bin', '/usr/local/bin');
178
+ } else {
179
+ dirs.push('/usr/local/bin', '/usr/bin');
180
+ }
181
+ return dirs;
182
+ }
183
+
184
+ const binName = (platform) => (platform === 'win32' ? 'infisical.exe' : 'infisical');
185
+
186
+ /** Pastas do PATH (so as absolutas: pasta vazia/relativa significaria "a pasta atual"). */
187
+ function pathDirs(platform, env) {
188
+ const P = pathFor(platform);
189
+ const raw = envGet(env, 'PATH') || '';
190
+ return String(raw)
191
+ .split(platform === 'win32' ? ';' : ':')
192
+ .map((s) => s.trim().replace(/^"(.*)"$/, '$1'))
193
+ .filter((s) => s && P.isAbsolute(s));
194
+ }
195
+
196
+ // ─── Dependencias reais (trocaveis nos testes) ───────────
197
+
198
+ /**
199
+ * Roda um programa sem shell. Nunca lanca. A saida pode ter segredo: quem chama decide o que mostrar.
200
+ * Opcoes: env, input (stdin), stdinIgnore (stdin = /dev/null: sem terminal, o que faz o CLI esperar o
201
+ * navegador), timeoutMs (mata o processo), onStdout/onStderr (pedacos em tempo real), cwd.
202
+ * Devolve `{ ok, status, stdout, stderr, timedOut, missing }`.
203
+ */
204
+ function runProcess(file, args, opts = {}) {
205
+ const { env, input, timeoutMs = 120000, stdinIgnore = false, cwd, onStdout, onStderr } = opts;
206
+ return new Promise((resolve) => {
207
+ let child;
208
+ try {
209
+ child = spawn(file, args || [], {
210
+ env: env || process.env,
211
+ cwd,
212
+ stdio: [stdinIgnore ? 'ignore' : 'pipe', 'pipe', 'pipe'],
213
+ windowsHide: true,
214
+ });
215
+ } catch (e) {
216
+ resolve({ ok: false, status: null, stdout: '', stderr: String((e && e.message) || e), timedOut: false, missing: Boolean(e && e.code === 'ENOENT') });
217
+ return;
218
+ }
219
+ let stdout = '';
220
+ let stderr = '';
221
+ let timedOut = false;
222
+ let error = null;
223
+ let status = null;
224
+ let done = false;
225
+ let killTimer = null;
226
+ let graceTimer = null;
227
+ let timer = null;
228
+
229
+ const finish = () => {
230
+ if (done) return;
231
+ done = true;
232
+ clearTimeout(timer);
233
+ clearTimeout(killTimer);
234
+ clearTimeout(graceTimer);
235
+ if (error && !stderr) stderr = String(error.message || error); // ex.: EACCES (arquivo sem permissao de execucao)
236
+ try { child.stdout.destroy(); } catch { /* ja fechado */ }
237
+ try { child.stderr.destroy(); } catch { /* ja fechado */ }
238
+ resolve({
239
+ ok: !error && !timedOut && status === 0,
240
+ status,
241
+ stdout,
242
+ stderr,
243
+ timedOut,
244
+ missing: Boolean(error && error.code === 'ENOENT'),
245
+ });
246
+ };
247
+
248
+ const feed = (which, cb) => (chunk) => {
249
+ if (which === 'out') { if (stdout.length < OUTPUT_CAP) stdout += chunk; } else if (stderr.length < OUTPUT_CAP) stderr += chunk;
250
+ if (typeof cb === 'function') {
251
+ try { cb(chunk); } catch { /* callback do chamador nunca derruba o processo */ }
252
+ }
253
+ };
254
+ child.stdout.setEncoding('utf8');
255
+ child.stderr.setEncoding('utf8');
256
+ child.stdout.on('data', feed('out', onStdout));
257
+ child.stderr.on('data', feed('err', onStderr));
258
+
259
+ if (!stdinIgnore && child.stdin) {
260
+ child.stdin.on('error', () => {}); // o filho pode fechar antes de ler tudo (EPIPE)
261
+ child.stdin.end(input === undefined || input === null ? '' : String(input));
262
+ }
263
+
264
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
265
+ timer = setTimeout(() => {
266
+ timedOut = true;
267
+ try { child.kill('SIGTERM'); } catch { /* ja morreu */ }
268
+ killTimer = setTimeout(() => {
269
+ try { child.kill('SIGKILL'); } catch { /* ja morreu */ }
270
+ }, 2000);
271
+ }, timeoutMs);
272
+ }
273
+
274
+ child.on('error', (e) => { error = e; finish(); });
275
+ child.on('exit', (code) => {
276
+ status = code;
277
+ // Um neto (ex.: o login que o CLI dispara sozinho) pode segurar os canais abertos: nao espera por ele.
278
+ graceTimer = setTimeout(finish, 1500);
279
+ });
280
+ child.on('close', (code) => {
281
+ if (status === null) status = code;
282
+ finish();
283
+ });
284
+ });
285
+ }
286
+
287
+ /** Baixa por `curl` (respeita o proxy do sistema, que o fetch do Node ignora). Lanca se falhar. */
288
+ function curlDownload(url, platform, spawn = spawnSync) {
289
+ const curl = platform === 'darwin' ? '/usr/bin/curl' : (platform === 'win32' ? 'curl.exe' : 'curl');
290
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'auroq-dl-'));
291
+ const file = path.join(dir, 'arquivo');
292
+ try {
293
+ const r = spawn(curl, ['-fL', '--silent', '--show-error', '--connect-timeout', '30', '--max-time', '900', '-o', file, url], {
294
+ env: cliEnv(process.env), // o curl nao precisa da chave do agente nem da credencial do 1Password; o proxy do sistema continua valendo
295
+ stdio: ['ignore', 'ignore', 'pipe'],
296
+ encoding: 'utf8',
297
+ windowsHide: true,
298
+ timeout: 16 * 60 * 1000,
299
+ });
300
+ if (r.error || r.status !== 0) {
301
+ const e = new Error(`curl falhou (${r.error ? r.error.code || r.error.message : `saida ${r.status}`})`);
302
+ e.curl = true;
303
+ throw e;
304
+ }
305
+ return fs.readFileSync(file);
306
+ } finally {
307
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* fica no temp do sistema */ }
308
+ }
309
+ }
310
+
311
+ /** Baixa um arquivo inteiro para a memoria. `fetch` primeiro; reserva: `curl -fL`. 404 nao tenta de novo. Lanca se falhar. */
312
+ async function defaultDownload(url, platform = process.platform) {
313
+ let first = null;
314
+ if (typeof fetch === 'function') {
315
+ const ctrl = new AbortController();
316
+ const timer = setTimeout(() => ctrl.abort(), DOWNLOAD_TIMEOUT_MS);
317
+ try {
318
+ const res = await fetch(url, { redirect: 'follow', signal: ctrl.signal, headers: { 'User-Agent': 'auroq-os' } });
319
+ if (!res.ok) {
320
+ const e = new Error(`HTTP ${res.status}`);
321
+ e.status = res.status;
322
+ throw e;
323
+ }
324
+ const len = Number(res.headers.get('content-length'));
325
+ if (Number.isFinite(len) && len > DOWNLOAD_MAX_BYTES) throw new Error('arquivo grande demais');
326
+ const buf = Buffer.from(await res.arrayBuffer());
327
+ if (buf.length > DOWNLOAD_MAX_BYTES) throw new Error('arquivo grande demais');
328
+ return buf;
329
+ } catch (e) {
330
+ if (e && e.status === 404) throw e; // a release ainda nao tem o arquivo (acontece nos primeiros ~35 min): curl nao muda isso
331
+ first = e;
332
+ } finally {
333
+ clearTimeout(timer);
334
+ }
335
+ }
336
+ try {
337
+ return curlDownload(url, platform);
338
+ } catch (e) {
339
+ if (first && first.status) { e.status = first.status; }
340
+ throw e;
341
+ }
342
+ }
343
+
344
+ function makeDefaultCliDeps(overrides = {}) {
345
+ return {
346
+ platform: process.platform,
347
+ arch: process.arch,
348
+ env: process.env,
349
+ fs,
350
+ exists: (p) => fs.existsSync(p),
351
+ tmpdir: os.tmpdir(),
352
+ run: runProcess,
353
+ download: (url) => defaultDownload(url, process.platform),
354
+ ...overrides,
355
+ };
356
+ }
357
+
358
+ /** Completa o `d` recebido com os padroes reais (os testes passam so o que querem trocar). */
359
+ function withDefaults(d) {
360
+ const base = makeDefaultCliDeps();
361
+ const out = { ...base };
362
+ for (const [k, v] of Object.entries(d || {})) {
363
+ if (v !== undefined) out[k] = v;
364
+ }
365
+ if (!(d && d.exists) && d && d.fs) out.exists = (p) => d.fs.existsSync(p);
366
+ return out;
367
+ }
368
+
369
+ // ─── Achar o CLI e a versao ──────────────────────────────
370
+
371
+ /** Roda `<arquivo> --version`. Devolve a versao ou null (nao rodou, saiu com erro, saida vazia = binario falso, `devel`). */
372
+ async function probeVersion(d, file) {
373
+ let r;
374
+ try {
375
+ r = await d.run(file, ['--version'], { env: cliEnv(d.env), timeoutMs: VERSION_TIMEOUT_MS, stdinIgnore: true });
376
+ } catch {
377
+ return null;
378
+ }
379
+ if (!r || !r.ok || r.timedOut) return null;
380
+ return parseCliVersion(r.stdout);
381
+ }
382
+
383
+ /**
384
+ * Acha o `infisical`: primeiro o caminho gravado (`preferred`), depois o PATH, depois as pastas conhecidas.
385
+ * Cada candidato roda `--version` pelo caminho ABSOLUTO. Devolve `{ path, version, antigo, via }` do primeiro
386
+ * com versao valida e >= MIN; se so houver velhos, devolve o primeiro deles com `antigo: true`; senao null.
387
+ * Saida vazia com exit 0 (atalho falso do npm no Git Bash) e `devel` sao ignorados.
388
+ */
389
+ async function findCli(dIn, { preferred } = {}) {
390
+ const d = withDefaults(dIn);
391
+ const P = pathFor(d.platform);
392
+ const seen = new Set();
393
+ const candidates = [];
394
+ const add = (file, via) => {
395
+ if (!file || typeof file !== 'string') return;
396
+ const key = d.platform === 'win32' ? file.toLowerCase() : file;
397
+ if (seen.has(key)) return;
398
+ seen.add(key);
399
+ candidates.push({ file, via });
400
+ };
401
+ add(preferred, 'gravado');
402
+ for (const dir of pathDirs(d.platform, d.env)) add(P.join(dir, binName(d.platform)), 'path');
403
+ for (const dir of knownCliDirs(d.platform, d.env)) add(P.join(dir, binName(d.platform)), 'conhecido');
404
+
405
+ let antigo = null;
406
+ for (const c of candidates) {
407
+ let exists = false;
408
+ try { exists = Boolean(d.exists(c.file)); } catch { exists = false; }
409
+ if (!exists) continue;
410
+ const version = await probeVersion(d, c.file);
411
+ if (!version) continue;
412
+ if (isCliVersionAtLeast(version, MIN_CLI_VERSION)) return { path: c.file, version, antigo: false, via: c.via };
413
+ if (!antigo) antigo = { path: c.file, version, antigo: true, via: c.via };
414
+ }
415
+ return antigo;
416
+ }
417
+
418
+ // ─── Instalar o CLI (sem admin, sem mexer no PATH) ───────
419
+
420
+ /** Nome do arquivo, URLs e binario da release pinada para este sistema, ou null se nao houver. */
421
+ function releaseTarget(platform, arch, version = PINNED_CLI_VERSION) {
422
+ const osName = platform === 'darwin' ? 'darwin' : (platform === 'win32' ? 'windows' : null);
423
+ const archName = arch === 'arm64' ? 'arm64' : (arch === 'x64' ? 'amd64' : null);
424
+ if (!osName || !archName) return null;
425
+ const ext = platform === 'darwin' ? 'tar.gz' : 'zip';
426
+ const file = `cli_${version}_${osName}_${archName}.${ext}`;
427
+ const base = `${RELEASE_BASE}/v${version}`;
428
+ return {
429
+ file,
430
+ url: `${base}/${file}`,
431
+ checksumsFile: platform === 'darwin' ? 'checksums-darwin.txt' : 'checksums.txt',
432
+ checksumsUrl: `${base}/${platform === 'darwin' ? 'checksums-darwin.txt' : 'checksums.txt'}`,
433
+ binary: binName(platform),
434
+ };
435
+ }
436
+
437
+ /** `checksums*.txt` do goreleaser: uma linha `<sha256> <arquivo>` por arquivo. */
438
+ function parseChecksums(text) {
439
+ const map = new Map();
440
+ for (const line of String(text == null ? '' : text).split(/\r?\n/)) {
441
+ const m = /^([0-9a-fA-F]{64})\s+\*?(\S+)\s*$/.exec(line.trim());
442
+ if (m) map.set(m[2].replace(/^\.\//, ''), m[1].toLowerCase());
443
+ }
444
+ return map;
445
+ }
446
+
447
+ function toBuffer(x) {
448
+ return Buffer.isBuffer(x) ? x : Buffer.from(x == null ? '' : x);
449
+ }
450
+
451
+ function downloadFailure(what, e) {
452
+ const status = e && Number.isFinite(e.status) ? e.status : null;
453
+ if (status) return { ok: false, motivo: 'falhou', detalhe: `${what}: o servidor respondeu ${status}` };
454
+ return { ok: false, motivo: 'sem-rede', detalhe: `${what}: nao consegui baixar (${scrub((e && e.message) || 'sem resposta', 120)})` };
455
+ }
456
+
457
+ function homeFor(d) {
458
+ const P = pathFor(d.platform);
459
+ const env = d.env || {};
460
+ const h = d.platform === 'win32' ? (envGet(env, 'USERPROFILE') || envGet(env, 'HOME')) : envGet(env, 'HOME');
461
+ return h && P.isAbsolute(h) ? h : os.homedir();
462
+ }
463
+
464
+ function localAppDataFor(d) {
465
+ const P = pathFor(d.platform);
466
+ const env = d.env || {};
467
+ const l = envGet(env, 'LOCALAPPDATA');
468
+ if (l) return l;
469
+ const u = envGet(env, 'USERPROFILE');
470
+ return u ? P.join(u, 'AppData', 'Local') : null;
471
+ }
472
+
473
+ /** Passo 1: binario direto da release pinada, com SHA-256 conferido, em pasta do usuario. */
474
+ async function installDirect(d) {
475
+ const P = pathFor(d.platform);
476
+ const t = releaseTarget(d.platform, d.arch);
477
+ if (!t) return { ok: false, motivo: 'plataforma', detalhe: `nao ha binario direto para ${d.platform}/${d.arch}` };
478
+
479
+ let destDir;
480
+ if (d.platform === 'darwin') {
481
+ destDir = P.join(homeFor(d), '.local', 'bin');
482
+ } else {
483
+ const local = localAppDataFor(d);
484
+ if (!local) return { ok: false, motivo: 'falhou', detalhe: 'nao achei a pasta LOCALAPPDATA' };
485
+ destDir = P.join(local, 'Programs', 'infisical');
486
+ }
487
+ const dest = P.join(destDir, t.binary);
488
+
489
+ // 1) checksums da MESMA release (pequeno): se faltar, nem baixa o arquivo grande
490
+ let sums;
491
+ try {
492
+ sums = toBuffer(await d.download(t.checksumsUrl)).toString('utf8');
493
+ } catch (e) {
494
+ return downloadFailure('checksums', e);
495
+ }
496
+ const expected = parseChecksums(sums).get(t.file);
497
+ if (!expected) return { ok: false, motivo: 'sha256', detalhe: `o arquivo de checksums nao traz ${t.file}` };
498
+
499
+ // 2) arquivo: confere ANTES de gravar qualquer coisa
500
+ let archive;
501
+ try {
502
+ archive = toBuffer(await d.download(t.url));
503
+ } catch (e) {
504
+ return downloadFailure(t.file, e);
505
+ }
506
+ const actual = crypto.createHash('sha256').update(archive).digest('hex');
507
+ if (actual !== expected) {
508
+ return { ok: false, motivo: 'sha256', detalhe: `SHA-256 de ${t.file} diferente do publicado (${actual.slice(0, 12)} em vez de ${expected.slice(0, 12)}); nada foi instalado` };
509
+ }
510
+
511
+ // 3) extrai so o binario para uma pasta temporaria propria, copia para o destino por temporario + rename
512
+ const f = d.fs;
513
+ let tmp = null;
514
+ let placed = null;
515
+ const existedBefore = (() => { try { return Boolean(d.exists(dest)); } catch { return false; } })();
516
+ try {
517
+ tmp = f.mkdtempSync(P.join(d.tmpdir || os.tmpdir(), 'auroq-infisical-'));
518
+ const archivePath = P.join(tmp, t.file);
519
+ const outDir = P.join(tmp, 'out');
520
+ f.mkdirSync(outDir, { recursive: true });
521
+ f.writeFileSync(archivePath, archive, { mode: 0o600 });
522
+
523
+ const env = cliEnv(d.env);
524
+ const extracted = P.join(outDir, t.binary);
525
+ if (d.platform === 'darwin') {
526
+ const r = await d.run('/usr/bin/tar', ['-xzf', archivePath, '-C', outDir, t.binary], { env, timeoutMs: EXTRACT_TIMEOUT_MS, stdinIgnore: true });
527
+ if (!r || !r.ok) return { ok: false, motivo: 'falhou', detalhe: `tar nao conseguiu abrir o arquivo (${lastErrorLine(r && r.stderr) || 'sem detalhe'})` };
528
+ } else {
529
+ const sysRoot = envGet(d.env, 'SystemRoot') || envGet(d.env, 'windir');
530
+ const tarExe = sysRoot ? P.join(sysRoot, 'System32', 'tar.exe') : 'tar.exe'; // o do Windows (bsdtar); o do Git nao le zip
531
+ let r = await d.run(tarExe, ['-xf', archivePath, '-C', outDir, t.binary], { env, timeoutMs: EXTRACT_TIMEOUT_MS, stdinIgnore: true });
532
+ let ok = Boolean(r && r.ok) && Boolean(d.exists(extracted));
533
+ if (!ok) {
534
+ const ps = sysRoot ? P.join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe') : 'powershell.exe';
535
+ // caminhos por variavel de ambiente: nada de aspas nem de nome de usuario com apostrofo dentro do comando
536
+ r = await d.run(ps, [
537
+ '-NoProfile', '-NonInteractive', '-Command',
538
+ "$ProgressPreference='SilentlyContinue'; Expand-Archive -LiteralPath $env:AUROQ_ZIP -DestinationPath $env:AUROQ_OUT -Force",
539
+ ], { env: { ...env, AUROQ_ZIP: archivePath, AUROQ_OUT: outDir }, timeoutMs: EXTRACT_TIMEOUT_MS, stdinIgnore: true });
540
+ ok = Boolean(r && r.ok);
541
+ }
542
+ if (!ok) return { ok: false, motivo: 'falhou', detalhe: `nao consegui abrir o zip (${lastErrorLine(r && r.stderr) || 'sem detalhe'})` };
543
+ }
544
+ if (!d.exists(extracted)) return { ok: false, motivo: 'falhou', detalhe: `${t.binary} nao estava dentro de ${t.file}` };
545
+
546
+ f.mkdirSync(destDir, { recursive: true });
547
+ placed = P.join(destDir, `.${t.binary}.auroq-${process.pid}-${crypto.randomBytes(3).toString('hex')}.tmp`);
548
+ f.copyFileSync(extracted, placed);
549
+ f.chmodSync(placed, 0o755);
550
+ f.renameSync(placed, dest);
551
+ placed = null;
552
+ if (d.platform === 'darwin') {
553
+ // melhor esforco: arquivo baixado por curl/Node nao costuma ter quarentena, mas o Gatekeeper nao deve barrar
554
+ await d.run('/usr/bin/xattr', ['-d', 'com.apple.quarantine', dest], { env, timeoutMs: 30000, stdinIgnore: true });
555
+ }
556
+ } catch (e) {
557
+ return { ok: false, motivo: 'falhou', detalhe: `nao consegui gravar em ${destDir} (${scrub((e && e.message) || 'erro', 120)})` };
558
+ } finally {
559
+ if (placed) { try { f.rmSync(placed, { force: true }); } catch { /* sobra so um arquivo oculto */ } }
560
+ if (tmp) { try { f.rmSync(tmp, { recursive: true, force: true }); } catch { /* fica no temp do sistema */ } }
561
+ }
562
+
563
+ const version = await probeVersion(d, dest);
564
+ if (!version || !isCliVersionAtLeast(version, MIN_CLI_VERSION)) {
565
+ if (!existedBefore) { try { f.rmSync(dest, { force: true }); } catch { /* sem problema */ } }
566
+ return { ok: false, motivo: 'falhou', detalhe: version ? `o infisical instalado diz ser a versao ${version} (minima ${MIN_CLI_VERSION})` : 'o infisical instalado nao respondeu a --version (o sistema pode ter bloqueado o programa)' };
567
+ }
568
+ return { ok: true, path: dest, version };
569
+ }
570
+
571
+ function findBrew(d) {
572
+ const P = pathFor(d.platform);
573
+ const list = ['/opt/homebrew/bin/brew', '/usr/local/bin/brew'];
574
+ for (const dir of pathDirs(d.platform, d.env)) list.push(P.join(dir, 'brew'));
575
+ for (const file of list) {
576
+ try { if (d.exists(file)) return file; } catch { /* proximo */ }
577
+ }
578
+ return null;
579
+ }
580
+
581
+ /** Passo 2 (Mac): Homebrew, so se ele ja existir (nunca instala o brew: isso pede senha de admin). Nome qualificado SEMPRE. */
582
+ async function installBrew(d) {
583
+ const P = pathFor(d.platform);
584
+ const brew = findBrew(d);
585
+ if (!brew) return { ok: false, motivo: 'falhou', detalhe: 'brew nao encontrado (nao instalo o Homebrew)' };
586
+ const env = { ...cliEnv(d.env), HOMEBREW_NO_AUTO_UPDATE: '1', HOMEBREW_NO_ENV_HINTS: '1' };
587
+ const r = await d.run(brew, ['install', 'infisical/get-cli/infisical'], { env, timeoutMs: INSTALL_TIMEOUT_MS, stdinIgnore: true });
588
+ const check = async (file) => {
589
+ let exists = false;
590
+ try { exists = Boolean(d.exists(file)); } catch { exists = false; }
591
+ if (!exists) return null;
592
+ const version = await probeVersion(d, file);
593
+ return version && isCliVersionAtLeast(version, MIN_CLI_VERSION) ? { ok: true, path: file, version } : null;
594
+ };
595
+ // o brew liga o programa na mesma pasta do proprio `brew` (/opt/homebrew/bin ou /usr/local/bin)
596
+ let found = await check(P.join(P.dirname(brew), 'infisical'));
597
+ if (!found && r && r.ok) {
598
+ const q = await d.run(brew, ['--prefix'], { env, timeoutMs: 30000, stdinIgnore: true });
599
+ const prefix = q && q.ok ? String(q.stdout).trim().split(/\r?\n/)[0] : '';
600
+ if (prefix && P.isAbsolute(prefix)) found = await check(P.join(prefix, 'bin', 'infisical'));
601
+ }
602
+ if (found) return found;
603
+ if (!r || !r.ok) return { ok: false, motivo: 'falhou', detalhe: `brew install falhou (${(r && r.timedOut) ? 'demorou demais' : (lastErrorLine(r && r.stderr) || 'sem detalhe')})` };
604
+ return { ok: false, motivo: 'falhou', detalhe: 'o brew terminou mas o infisical nao respondeu a --version' };
605
+ }
606
+
607
+ /** Passo 2 (Windows): winget, escopo do usuario, sem perguntas. Pode entregar versao um pouco antiga: conferida contra o minimo. */
608
+ async function installWinget(d) {
609
+ const P = pathFor(d.platform);
610
+ const env = cliEnv(d.env);
611
+ const probe = await d.run('winget', ['--version'], { env, timeoutMs: 30000, stdinIgnore: true });
612
+ if (!probe || !probe.ok) return { ok: false, motivo: 'falhou', detalhe: 'winget nao encontrado' };
613
+ const r = await d.run('winget', [
614
+ 'install', '--id', 'infisical.infisical', '-e', '--source', 'winget', '--scope', 'user',
615
+ '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity',
616
+ ], { env, timeoutMs: INSTALL_TIMEOUT_MS, stdinIgnore: true });
617
+
618
+ // Sem Modo Desenvolvedor o winget nao cria o atalho em Links: o programa fica na pasta do pacote.
619
+ const candidates = [];
620
+ const local = localAppDataFor(d);
621
+ if (local) {
622
+ candidates.push(P.join(local, 'Microsoft', 'WinGet', 'Links', 'infisical.exe'));
623
+ const pkgs = P.join(local, 'Microsoft', 'WinGet', 'Packages');
624
+ try {
625
+ for (const name of d.fs.readdirSync(pkgs)) {
626
+ if (/^infisical\.infisical/i.test(String(name))) candidates.push(P.join(pkgs, String(name), 'infisical.exe'));
627
+ }
628
+ } catch { /* pasta ainda nao existe */ }
629
+ }
630
+ for (const file of candidates) {
631
+ let exists = false;
632
+ try { exists = Boolean(d.exists(file)); } catch { exists = false; }
633
+ if (!exists) continue;
634
+ const version = await probeVersion(d, file);
635
+ if (version && isCliVersionAtLeast(version, MIN_CLI_VERSION)) return { ok: true, path: file, version };
636
+ }
637
+ if (!r || !r.ok) return { ok: false, motivo: 'falhou', detalhe: `winget install falhou (${(r && r.timedOut) ? 'demorou demais' : (lastErrorLine(r && r.stderr) || `saida ${r && r.status}`)})` };
638
+ return { ok: false, motivo: 'falhou', detalhe: 'o winget terminou mas nao achei um infisical que responda a --version' };
639
+ }
640
+
641
+ /**
642
+ * Instala o `infisical` sem admin. Ordem: (1) binario direto pinado, com SHA-256; (2) Mac: `brew install
643
+ * infisical/get-cli/infisical` (so se o brew ja existe); Windows: winget. Cada passo confere `--version` pelo
644
+ * caminho absoluto. Nenhum passo usa sudo nem mexe no PATH. Devolve `{ ok:true, path, version, via }` ou
645
+ * `{ ok:false, motivo:'sem-rede'|'sha256'|'plataforma'|'falhou', detalhe }` (o motivo e o do primeiro passo que falhou).
646
+ */
647
+ async function installCli(dIn) {
648
+ const d = withDefaults(dIn);
649
+ if (d.platform !== 'darwin' && d.platform !== 'win32') {
650
+ return { ok: false, motivo: 'plataforma', detalhe: `instalacao automatica so no macOS e no Windows (aqui: ${d.platform})` };
651
+ }
652
+ const falhas = [];
653
+ const steps = [['direto', installDirect], [d.platform === 'darwin' ? 'brew' : 'winget', d.platform === 'darwin' ? installBrew : installWinget]];
654
+ for (const [via, fn] of steps) {
655
+ let r;
656
+ try {
657
+ r = await fn(d);
658
+ } catch (e) {
659
+ r = { ok: false, motivo: 'falhou', detalhe: scrub((e && e.message) || 'erro inesperado', 120) };
660
+ }
661
+ if (r.ok) return { ok: true, path: r.path, version: r.version, via };
662
+ falhas.push({ via, motivo: r.motivo, detalhe: r.detalhe });
663
+ }
664
+ return {
665
+ ok: false,
666
+ motivo: falhas[0].motivo,
667
+ detalhe: falhas.map((f) => `${f.via}: ${f.detalhe}`).join(' | '),
668
+ };
669
+ }
670
+
671
+ // ─── Sessao do aluno ─────────────────────────────────────
672
+
673
+ function parseJsonObject(text) {
674
+ const t = String(text == null ? '' : text).trim();
675
+ try {
676
+ const v = JSON.parse(t);
677
+ return v && typeof v === 'object' ? v : null;
678
+ } catch {
679
+ const a = t.indexOf('{');
680
+ const b = t.lastIndexOf('}');
681
+ if (a !== -1 && b > a) {
682
+ try { return JSON.parse(t.slice(a, b + 1)); } catch { /* ilegivel */ }
683
+ }
684
+ return null;
685
+ }
686
+ }
687
+
688
+ const emptyStatus = (state, extra = {}) => ({ state, email: null, organizationName: null, domain: null, exp: null, ...extra });
689
+
690
+ /**
691
+ * Ha sessao no perfil? `<cli> login status --json --profile auroq` (nao dispara login, nao grava nada).
692
+ * - authenticated: exit 0 e status authenticated (offline = verification "unknown" tambem entra, com `offline:true`)
693
+ * - expired: venceu (exit 1) ou o servidor recusou ("rejected")
694
+ * - none: sem sessao (`{"sessions":[]}`) ou perfil inexistente
695
+ * - unknown: saida ilegivel, tempo esgotado, keyring ilegivel
696
+ * Devolve `{ state, email, organizationName, domain, exp }` (exp em segundos unix, ou null).
697
+ */
698
+ async function loginStatus(dIn, cli, { profile = PROFILE } = {}) {
699
+ const d = withDefaults(dIn);
700
+ if (!cli || typeof cli !== 'string' || !validProfile(profile)) return emptyStatus('unknown', { detalhe: 'caminho do CLI ou perfil invalido' });
701
+ let r;
702
+ try {
703
+ r = await d.run(cli, ['login', 'status', '--json', '--profile', profile], { env: cliEnv(d.env), timeoutMs: STATUS_TIMEOUT_MS, stdinIgnore: true });
704
+ } catch {
705
+ return emptyStatus('unknown', { detalhe: 'nao consegui rodar o CLI' });
706
+ }
707
+ if (!r) return emptyStatus('unknown', { detalhe: 'sem resposta do CLI' });
708
+ if (r.timedOut) return emptyStatus('unknown', { detalhe: 'o CLI demorou demais' });
709
+ if (r.missing) return emptyStatus('unknown', { detalhe: 'CLI nao encontrado' });
710
+
711
+ const doc = parseJsonObject(r.stdout);
712
+ if (doc && Array.isArray(doc.sessions)) {
713
+ if (!doc.sessions.length) return emptyStatus('none');
714
+ const s = doc.sessions.find((x) => x && x.principalType === 'user') || doc.sessions[0];
715
+ if (!s || typeof s !== 'object') return emptyStatus('unknown', { detalhe: 'sessao ilegivel' });
716
+ const info = {
717
+ email: typeof s.email === 'string' ? s.email : null,
718
+ organizationName: typeof s.organizationName === 'string' ? s.organizationName : null,
719
+ domain: typeof s.domain === 'string' ? s.domain : null,
720
+ exp: s.token && Number.isFinite(Number(s.token.exp)) && Number(s.token.exp) > 0 ? Number(s.token.exp) : null,
721
+ };
722
+ if (s.status === 'authenticated') {
723
+ if (!r.ok) return { state: 'unknown', ...info, detalhe: 'o CLI disse autenticado mas saiu com erro' };
724
+ const offline = Boolean(s.verification && s.verification.state === 'unknown');
725
+ return { state: 'authenticated', ...info, offline };
726
+ }
727
+ if (s.status === 'expired') return { state: 'expired', ...info };
728
+ if (s.status === 'rejected') return { state: 'expired', ...info, rejeitada: true };
729
+ return { state: 'unknown', ...info, detalhe: `situacao desconhecida (${scrub(s.status, 40)})` };
730
+ }
731
+
732
+ // Sem JSON. Perfil que nao existe na configuracao: o CLI avisa no stderr e sai com 1 (sem sessao, na pratica).
733
+ const err = stripAnsi(r.stderr);
734
+ if (!r.ok && /(does not exist|ErrProfileNotFound|no login profiles|not authenticated)/i.test(`${err}\n${stripAnsi(r.stdout)}`)) return emptyStatus('none');
735
+ return emptyStatus('unknown', { detalhe: lastErrorLine(r.stderr) || 'saida ilegivel do CLI' });
736
+ }
737
+
738
+ /**
739
+ * Acha, num texto do stderr, o endereco do login (`.../login?callback_port=NNNN`). Com `complete` falso so
740
+ * vale URL seguida de espaco ou quebra de linha (o texto pode ainda estar chegando).
741
+ */
742
+ function extractLoginUrl(text, { complete = false } = {}) {
743
+ const clean = stripAnsi(text);
744
+ const re = /https?:\/\/[^\s"'<>]+/g;
745
+ let m;
746
+ while ((m = re.exec(clean)) !== null) {
747
+ if (!complete && m.index + m[0].length >= clean.length) break;
748
+ const url = m[0].replace(/[.,;:!?)\]]+$/, '');
749
+ const before = clean.slice(clean.lastIndexOf('\n', m.index) + 1, m.index);
750
+ if (/callback_port=/.test(url) || /\/login(\?|\/|$)/.test(url) || /(browser|address|endereco)/i.test(before)) return url;
751
+ }
752
+ return null;
753
+ }
754
+
755
+ /**
756
+ * Login do aluno no navegador: `<cli> login --domain <dominio> --save-as auroq --silent`, com stdin IGNORADO
757
+ * (sem terminal o CLI so espera o navegador, sem perguntar nada). Le o stderr em tempo real, chama
758
+ * `onUrl(url)` UMA vez com o endereco do login e mata o processo ao estourar `timeoutMs`.
759
+ * NUNCA usa `--plain` (imprimiria o JWT da sessao). Devolve `{ ok:true, url }` (exit 0) ou
760
+ * `{ ok:false, motivo:'tempo'|'rede'|'falhou', url, detalhe }`.
761
+ */
762
+ async function browserLogin(dIn, cli, { domain, profile = PROFILE, timeoutMs = DEFAULT_LOGIN_TIMEOUT_MS, onUrl } = {}) {
763
+ const d = withDefaults(dIn);
764
+ if (!cli || typeof cli !== 'string') return { ok: false, motivo: 'falhou', url: null, detalhe: 'caminho do CLI ausente' };
765
+ if (!validProfile(profile)) return { ok: false, motivo: 'falhou', url: null, detalhe: 'nome de perfil invalido' };
766
+ // Sem --domain o CLI pergunta a regiao (menu) e sem terminal isso trava ou falha: nunca roda sem.
767
+ if (!validDomain(domain)) return { ok: false, motivo: 'falhou', url: null, detalhe: 'dominio do Infisical ausente ou invalido' };
768
+
769
+ let url = null;
770
+ let buffer = '';
771
+ const found = (candidate) => {
772
+ if (url || !candidate) return;
773
+ url = candidate;
774
+ if (typeof onUrl === 'function') {
775
+ try {
776
+ const r = onUrl(candidate);
777
+ if (r && typeof r.then === 'function') r.then(null, () => {});
778
+ } catch { /* quem mostra a URL nao derruba o login */ }
779
+ }
780
+ };
781
+ const onStderr = (chunk) => {
782
+ if (url) return;
783
+ buffer = (buffer + chunk).slice(-65536);
784
+ found(extractLoginUrl(buffer));
785
+ };
786
+
787
+ let r;
788
+ try {
789
+ r = await d.run(cli, ['login', '--domain', domain, '--save-as', profile, '--silent'], {
790
+ env: cliEnv(d.env), timeoutMs, stdinIgnore: true, onStderr,
791
+ });
792
+ } catch {
793
+ return { ok: false, motivo: 'falhou', url, detalhe: 'nao consegui rodar o CLI' };
794
+ }
795
+ r = r || {};
796
+ // `run` sem tempo real (ou saida que chegou toda no fim): olha o texto final
797
+ if (!url) found(extractLoginUrl(`${buffer}\n${r.stderr || ''}\n`, { complete: true }));
798
+
799
+ if (r.timedOut) return { ok: false, motivo: 'tempo', url, detalhe: 'o login no navegador nao terminou a tempo' };
800
+ if (r.missing) return { ok: false, motivo: 'falhou', url, detalhe: 'CLI nao encontrado' };
801
+ if (r.ok) return { ok: true, url };
802
+ const err = lastErrorLine(r.stderr) || `o CLI saiu com codigo ${r.status}`;
803
+ return { ok: false, motivo: NET_RE.test(stripAnsi(r.stderr)) ? 'rede' : 'falhou', url, detalhe: err };
804
+ }
805
+
806
+ // JWT: tres partes base64url; o cabecalho de um JWT sempre comeca com eyJ (base64 de `{"`). Evita confundir com "0.44.8".
807
+ const JWT_RE = /^eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
808
+
809
+ /**
810
+ * JWT da sessao do aluno (`<cli> user get token --plain --profile auroq`). So chamar DEPOIS de `loginStatus` =
811
+ * authenticated: com a sessao vencida o CLI dispararia um login por conta propria. O token so existe no retorno:
812
+ * nunca vai para tela, log nem mensagem de erro. Devolve `{ ok:true, token }` ou `{ ok:false, motivo, detalhe }`.
813
+ */
814
+ async function getUserToken(dIn, cli, { profile = PROFILE } = {}) {
815
+ const d = withDefaults(dIn);
816
+ if (!cli || typeof cli !== 'string' || !validProfile(profile)) return { ok: false, motivo: 'falhou', detalhe: 'caminho do CLI ou perfil invalido' };
817
+ let r;
818
+ try {
819
+ r = await d.run(cli, ['user', 'get', 'token', '--plain', '--profile', profile], { env: cliEnv(d.env), timeoutMs: TOKEN_TIMEOUT_MS, stdinIgnore: true });
820
+ } catch {
821
+ return { ok: false, motivo: 'falhou', detalhe: 'nao consegui rodar o CLI' };
822
+ }
823
+ if (!r) return { ok: false, motivo: 'falhou', detalhe: 'sem resposta do CLI' };
824
+ if (r.timedOut) return { ok: false, motivo: 'tempo', detalhe: 'o CLI demorou demais' };
825
+ if (r.missing) return { ok: false, motivo: 'falhou', detalhe: 'CLI nao encontrado' };
826
+ // Em caso de erro so o stderr entra na mensagem (o stdout e onde o token apareceria).
827
+ if (!r.ok) return { ok: false, motivo: 'falhou', detalhe: lastErrorLine(r.stderr) || `o CLI saiu com codigo ${r.status}` };
828
+ const token = stripAnsi(r.stdout).split(/\r?\n/).map((l) => l.trim()).find((l) => JWT_RE.test(l));
829
+ if (!token) return { ok: false, motivo: 'falhou', detalhe: 'a saida do CLI nao tem a forma de um token de sessao' };
830
+ return { ok: true, token };
831
+ }
832
+
833
+ /** Sai do perfil (`<cli> logout --profile auroq`). Melhor esforco: nunca lanca. Devolve `{ ok, detalhe? }`. */
834
+ async function logoutProfile(dIn, cli, { profile = PROFILE } = {}) {
835
+ try {
836
+ const d = withDefaults(dIn);
837
+ if (!cli || typeof cli !== 'string' || !validProfile(profile)) return { ok: false, detalhe: 'caminho do CLI ou perfil invalido' };
838
+ const r = await d.run(cli, ['logout', '--profile', profile], { env: cliEnv(d.env), timeoutMs: LOGOUT_TIMEOUT_MS, stdinIgnore: true });
839
+ if (r && r.ok) return { ok: true };
840
+ return { ok: false, detalhe: (r && r.timedOut) ? 'o CLI demorou demais' : (lastErrorLine(r && r.stderr) || 'o CLI nao conseguiu sair do perfil') };
841
+ } catch {
842
+ return { ok: false, detalhe: 'erro ao rodar o CLI' };
843
+ }
844
+ }
845
+
846
+ module.exports = {
847
+ MIN_CLI_VERSION,
848
+ PINNED_CLI_VERSION,
849
+ PROFILE,
850
+ DEFAULT_DOMAIN,
851
+ parseCliVersion,
852
+ isCliVersionAtLeast,
853
+ knownCliDirs,
854
+ findCli,
855
+ installCli,
856
+ loginStatus,
857
+ browserLogin,
858
+ getUserToken,
859
+ logoutProfile,
860
+ cliEnv,
861
+ // alem do contrato: dependencias reais e pecas puras que os testes e o orquestrador usam
862
+ makeDefaultCliDeps,
863
+ runProcess,
864
+ curlDownload,
865
+ extractLoginUrl,
866
+ releaseTarget,
867
+ parseChecksums,
868
+ scrub,
869
+ };