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.
- package/.auroq-core/como-funciona.md +5 -5
- package/.auroq-core/core-config.yaml +1 -1
- package/.claude/CLAUDE.md +2 -3
- package/.claude/rules/agent-authority.md +8 -5
- package/.claude/rules/mcp-usage.md +2 -2
- package/.claude/rules/memoria-inteligente.md +3 -2
- package/.claude/rules/operacao-autonoma.md +11 -2
- package/.claude/rules/puxar-e-entregar.md +7 -7
- package/.claude/settings.json +7 -0
- package/.synapse/agent-ops +1 -1
- package/AGENTS.md +49 -14
- package/CHANGELOG.md +92 -0
- package/README.md +48 -13
- package/agents/organizer/agents/organizer.md +4 -4
- package/bin/auroq-os.js +33 -5
- package/installer/install.ps1 +2 -2
- package/installer/install.sh +2 -2
- package/lib/cofre.js +1453 -0
- package/lib/infisical-api.js +670 -0
- package/lib/infisical-cli.js +869 -0
- package/lib/infisical.js +1271 -0
- package/lib/legacy-shims.json +2 -1
- package/lib/onepassword.js +1315 -22
- package/lib/runtime-integrity.json +23 -7
- package/package.json +3 -3
- package/seeds/docs/knowledge/expert-business/canais-e-numeros.md +21 -0
- package/seeds/docs/knowledge/expert-business/equipe-e-ferramentas.md +11 -0
- package/seeds/docs/knowledge/expert-business/metas.md +20 -0
- package/seeds/docs/knowledge/expert-business/provas.md +15 -0
- package/seeds/docs/knowledge/expert-business/referencias.md +11 -0
- package/seeds/docs/knowledge/expert-mind/vida-pessoal.md +20 -0
- package/seeds/docs/knowledge/mapa-do-negocio.md +40 -0
package/lib/onepassword.js
CHANGED
|
@@ -3,18 +3,25 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Auroq OS — Conexao 1Password CLI
|
|
5
5
|
*
|
|
6
|
-
* Fluxo
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Fluxo padrao (automatico): o aluno cria a conta do 1Password, instala o app, liga
|
|
7
|
+
* "Integrate with 1Password CLI" nas configuracoes do app e aprova UMA vez com a digital
|
|
8
|
+
* (Touch ID / Windows Hello). O resto e daqui: cria o cofre "Claude", cria a conta de
|
|
9
|
+
* servico "Auroq" com acesso so a esse cofre, grava o token no computador e confere.
|
|
10
|
+
* O token nasce e e gravado dentro deste processo: este comando nunca o imprime nem o passa por
|
|
11
|
+
* argumento de linha de comando. (Depois de gravado ele e uma variavel de ambiente do aluno: quem
|
|
12
|
+
* rodar `env` ou abrir o arquivo de ambiente o ve, por isso o Ops nunca faz isso.)
|
|
13
|
+
*
|
|
14
|
+
* Modo manual (escondido, `--manual`): o aluno copia o token de uma conta de servico que ele
|
|
15
|
+
* mesmo criou (ops_...) e o comando le da area de transferencia, valida e grava.
|
|
10
16
|
*
|
|
11
17
|
* @module auroq-os/onepassword
|
|
12
18
|
*/
|
|
13
19
|
|
|
14
20
|
const os = require('os');
|
|
21
|
+
const crypto = require('crypto');
|
|
15
22
|
const path = require('path');
|
|
16
23
|
const fs = require('fs-extra');
|
|
17
|
-
const { execSync, execFileSync } = require('child_process');
|
|
24
|
+
const { execSync, execFileSync, spawn, spawnSync } = require('child_process');
|
|
18
25
|
|
|
19
26
|
const PURPLE = '\x1b[38;2;120;80;200m';
|
|
20
27
|
const GOLD = '\x1b[38;2;245;158;11m';
|
|
@@ -80,18 +87,19 @@ function opVersion() {
|
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
function installOp() {
|
|
90
|
+
function installOp({ upgrade = false } = {}) {
|
|
84
91
|
if (process.platform === 'darwin') {
|
|
85
92
|
if (!hasCommand('brew')) {
|
|
86
93
|
err('Homebrew nao encontrado. Instale em https://brew.sh e rode este comando de novo.');
|
|
87
94
|
return false;
|
|
88
95
|
}
|
|
89
|
-
log(`${DIM}Instalando via Homebrew (pode levar um minuto)...${RESET}`);
|
|
96
|
+
log(`${DIM}${upgrade ? 'Atualizando' : 'Instalando'} via Homebrew (pode levar um minuto)...${RESET}`);
|
|
97
|
+
const brewCmd = upgrade ? 'brew upgrade --cask 1password-cli' : 'brew install --cask 1password-cli';
|
|
90
98
|
try {
|
|
91
|
-
execSync(
|
|
99
|
+
execSync(brewCmd, { stdio: 'pipe', maxBuffer: 64 * 1024 * 1024, env: opEnv() });
|
|
92
100
|
return true;
|
|
93
101
|
} catch {
|
|
94
|
-
err('Instalacao via Homebrew falhou. Tente manualmente:
|
|
102
|
+
err(`${upgrade ? 'Atualizacao' : 'Instalacao'} via Homebrew falhou. Tente manualmente: ${brewCmd}`);
|
|
95
103
|
return false;
|
|
96
104
|
}
|
|
97
105
|
}
|
|
@@ -102,12 +110,13 @@ function installOp() {
|
|
|
102
110
|
err('https://developer.1password.com/docs/cli/get-started/');
|
|
103
111
|
return false;
|
|
104
112
|
}
|
|
105
|
-
log(`${DIM}Instalando via winget (pode levar um minuto)...${RESET}`);
|
|
113
|
+
log(`${DIM}${upgrade ? 'Atualizando' : 'Instalando'} via winget (pode levar um minuto)...${RESET}`);
|
|
114
|
+
const verb = upgrade ? 'upgrade' : 'install';
|
|
106
115
|
try {
|
|
107
|
-
execSync(
|
|
116
|
+
execSync(`winget ${verb} --id AgileBits.1Password.CLI --accept-source-agreements --accept-package-agreements`, { stdio: 'pipe', maxBuffer: 64 * 1024 * 1024, env: opEnv() });
|
|
108
117
|
return true;
|
|
109
118
|
} catch {
|
|
110
|
-
err('Instalacao via winget falhou. Tente manualmente: winget
|
|
119
|
+
err(`${upgrade ? 'Atualizacao' : 'Instalacao'} via winget falhou. Tente manualmente: winget ${verb} AgileBits.1Password.CLI`);
|
|
111
120
|
return false;
|
|
112
121
|
}
|
|
113
122
|
}
|
|
@@ -141,6 +150,27 @@ function readClipboard() {
|
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
/** Apaga a area de transferencia (melhor esforco): o token copiado nao deve ficar la nem no historico de copias. */
|
|
154
|
+
function clearClipboard() {
|
|
155
|
+
const opts = { stdio: ['pipe', 'ignore', 'ignore'], input: '' };
|
|
156
|
+
try {
|
|
157
|
+
if (process.platform === 'darwin') {
|
|
158
|
+
execFileSync('pbcopy', [], opts);
|
|
159
|
+
} else if (process.platform === 'win32') {
|
|
160
|
+
execFileSync('powershell', ['-NoProfile', '-Command', 'Set-Clipboard -Value $null'], { stdio: 'ignore' });
|
|
161
|
+
} else if (hasCommand('xclip')) {
|
|
162
|
+
execFileSync('xclip', ['-selection', 'clipboard', '-i'], opts);
|
|
163
|
+
} else if (hasCommand('xsel')) {
|
|
164
|
+
execFileSync('xsel', ['--clipboard', '--clear'], { stdio: 'ignore' });
|
|
165
|
+
} else {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
144
174
|
// ─── Validacao online e persistencia ─────────────────────
|
|
145
175
|
|
|
146
176
|
function testToken(token) {
|
|
@@ -177,18 +207,174 @@ function persistToken(token) {
|
|
|
177
207
|
return 'variavel de ambiente do usuario (Windows)';
|
|
178
208
|
}
|
|
179
209
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const envFile = (process.platform !== 'darwin' && shell.includes('bash'))
|
|
184
|
-
? path.join(os.homedir(), '.bashrc')
|
|
185
|
-
: path.join(os.homedir(), '.zshenv');
|
|
186
|
-
|
|
210
|
+
const envFile = envFilePath();
|
|
211
|
+
const problem = envFileProblem();
|
|
212
|
+
if (problem) throw new Error(problem);
|
|
187
213
|
const existing = fs.existsSync(envFile) ? fs.readFileSync(envFile, 'utf8') : '';
|
|
188
|
-
|
|
214
|
+
writeFileAtomic(envFile, upsertEnvFileContent(existing, token));
|
|
189
215
|
return envFile.replace(os.homedir(), '~');
|
|
190
216
|
}
|
|
191
217
|
|
|
218
|
+
/** O arquivo esta no git? Alem do repositorio normal, olha os de dotfiles "bare" mais comuns (~/.cfg, yadm...). */
|
|
219
|
+
function isTrackedByGit(file) {
|
|
220
|
+
const name = path.basename(file);
|
|
221
|
+
const home = os.homedir();
|
|
222
|
+
const attempts = [['-C', path.dirname(file), 'ls-files', '--error-unmatch', '--', name]];
|
|
223
|
+
if (path.dirname(file) === home) {
|
|
224
|
+
const bare = [process.env.GIT_DIR, ...['.cfg', '.dotfiles', '.dotfiles.git', '.local/share/yadm/repo.git', '.config/yadm/repo.git'].map((p) => path.join(home, p))];
|
|
225
|
+
for (const dir of bare.filter(Boolean)) {
|
|
226
|
+
try {
|
|
227
|
+
if (fs.statSync(dir).isDirectory()) attempts.push([`--git-dir=${dir}`, `--work-tree=${home}`, 'ls-files', '--error-unmatch', '--', name]);
|
|
228
|
+
} catch { /* nao existe */ }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return attempts.some((args) => {
|
|
232
|
+
try {
|
|
233
|
+
execFileSync('git', args, { stdio: 'ignore' });
|
|
234
|
+
return true;
|
|
235
|
+
} catch {
|
|
236
|
+
return false; // git ausente, fora de repositorio ou arquivo nao rastreado
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Linha do aluno que define a variavel de outro jeito (ex.: busca no Keychain): o valor nao e um token. */
|
|
242
|
+
function hasDynamicTokenDefinition(content) {
|
|
243
|
+
return String(content || '').split('\n').some((line) => {
|
|
244
|
+
const m = line.match(/^\s*(?:export\s+)?OP_SERVICE_ACCOUNT_TOKEN=(.*)$/);
|
|
245
|
+
if (!m) return false;
|
|
246
|
+
const value = m[1].trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
247
|
+
return value !== '' && !isValidToken(value);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Por que NAO da para gravar a chave no arquivo de ambiente (ou null se da). Roda ANTES de criar o cofre
|
|
253
|
+
* e a conta de servico, porque depois que a chave nasce ela so aparece uma vez:
|
|
254
|
+
* - atalho (link) para outro lugar ou arquivo no git (dotfiles): um `git push` mandaria a chave, que
|
|
255
|
+
* escreve no cofre Claude, para o GitHub;
|
|
256
|
+
* - linha do aluno que ja define a variavel de outro jeito: sobrescrever apagaria o mecanismo dele;
|
|
257
|
+
* - sem permissao de escrita.
|
|
258
|
+
* No Windows a chave vai para a variavel do usuario: nao se aplica.
|
|
259
|
+
*/
|
|
260
|
+
function envFileProblem() {
|
|
261
|
+
if (process.platform === 'win32') return null;
|
|
262
|
+
const file = envFilePath();
|
|
263
|
+
const name = path.basename(file);
|
|
264
|
+
let st = null;
|
|
265
|
+
try { st = fs.lstatSync(file); } catch { /* ainda nao existe: tudo bem */ }
|
|
266
|
+
if (st && st.isSymbolicLink()) {
|
|
267
|
+
return `${name} e um atalho (link) para outro lugar; nao gravo a chave nele para ela nao ir parar num repositorio`;
|
|
268
|
+
}
|
|
269
|
+
if (st && isTrackedByGit(file)) {
|
|
270
|
+
return `${name} esta versionado no git; nao gravo a chave nele para ela nao ir parar no GitHub`;
|
|
271
|
+
}
|
|
272
|
+
if (st) {
|
|
273
|
+
let content = '';
|
|
274
|
+
try { content = fs.readFileSync(file, 'utf8'); } catch { return `${name} nao pode ser lido`; }
|
|
275
|
+
if (hasDynamicTokenDefinition(content)) {
|
|
276
|
+
return `${name} ja define OP_SERVICE_ACCOUNT_TOKEN de outro jeito (uma linha sua que busca o valor em outro lugar); nao vou apagar a sua linha`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
fs.accessSync(st ? file : path.dirname(file), fs.constants.W_OK);
|
|
281
|
+
} catch {
|
|
282
|
+
return `${name} (ou a pasta dele) nao aceita escrita`;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Escreve por arquivo temporario criado ja com permissao 0600 na mesma pasta + rename: o token nunca
|
|
289
|
+
* fica legivel por outros, e uma falha no meio (disco cheio) nao trunca o arquivo que o aluno ja tinha.
|
|
290
|
+
*/
|
|
291
|
+
function writeFileAtomic(file, content) {
|
|
292
|
+
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.auroq-${process.pid}.tmp`);
|
|
293
|
+
let fd = null;
|
|
294
|
+
try {
|
|
295
|
+
fd = fs.openSync(tmp, 'wx', 0o600);
|
|
296
|
+
fs.writeSync(fd, content);
|
|
297
|
+
fs.fsyncSync(fd);
|
|
298
|
+
fs.closeSync(fd);
|
|
299
|
+
fd = null;
|
|
300
|
+
fs.renameSync(tmp, file);
|
|
301
|
+
} catch (e) {
|
|
302
|
+
if (fd !== null) { try { fs.closeSync(fd); } catch { /* ja fechado */ } }
|
|
303
|
+
try { fs.unlinkSync(tmp); } catch { /* nao chegou a existir */ }
|
|
304
|
+
throw e;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Qual shell o aluno usa (SHELL; reserva: o shell de login do usuario): 'zsh' | 'bash' | 'outro'.
|
|
309
|
+
function shellKind() {
|
|
310
|
+
let shell = process.env.SHELL || '';
|
|
311
|
+
if (!shell) {
|
|
312
|
+
try { shell = os.userInfo().shell || ''; } catch { shell = ''; }
|
|
313
|
+
}
|
|
314
|
+
const base = path.basename(shell).toLowerCase();
|
|
315
|
+
if (base.includes('zsh')) return 'zsh';
|
|
316
|
+
if (base.includes('bash')) return 'bash';
|
|
317
|
+
return 'outro';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Arquivo que o terminal NOVO do aluno le sozinho:
|
|
321
|
+
// macOS: ~/.zshenv (zsh e o padrao do sistema); com bash, ~/.bash_profile.
|
|
322
|
+
// Linux: ~/.bashrc (bash), ~/.zshenv (zsh) ou ~/.profile (sh/dash/ksh).
|
|
323
|
+
function envFilePath() {
|
|
324
|
+
const kind = shellKind();
|
|
325
|
+
const home = os.homedir();
|
|
326
|
+
if (process.platform === 'darwin') return path.join(home, kind === 'bash' ? '.bash_profile' : '.zshenv');
|
|
327
|
+
if (kind === 'bash') return path.join(home, '.bashrc');
|
|
328
|
+
if (kind === 'zsh') return path.join(home, '.zshenv');
|
|
329
|
+
return path.join(home, '.profile');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Como abrir um shell NOVO que leia exatamente o arquivo acima.
|
|
333
|
+
function freshShellFor(file) {
|
|
334
|
+
const name = path.basename(file);
|
|
335
|
+
if (name === '.zshenv') return { bin: 'zsh', flag: '-c' };
|
|
336
|
+
if (name === '.bashrc') return { bin: 'bash', flag: '-ic' };
|
|
337
|
+
if (name === '.bash_profile') return { bin: 'bash', flag: '-lc' };
|
|
338
|
+
return { bin: 'sh', flag: '-lc' };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Le o token da LINHA que o Auroq grava no arquivo de ambiente (ultima definicao vence).
|
|
343
|
+
* Devolve null se nao ha linha. Aceita aspas simples, duplas ou nenhuma.
|
|
344
|
+
*/
|
|
345
|
+
function parseEnvFileToken(content) {
|
|
346
|
+
let found = null;
|
|
347
|
+
for (const line of String(content || '').split('\n')) {
|
|
348
|
+
const m = line.match(/^\s*(?:export\s+)?OP_SERVICE_ACCOUNT_TOKEN=(.*)$/);
|
|
349
|
+
if (!m) continue;
|
|
350
|
+
found = m[1].trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
351
|
+
}
|
|
352
|
+
return found || null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** O que um terminal NOVO vai enxergar: arquivo de ambiente (Mac/Linux) ou variavel do usuario (Windows). */
|
|
356
|
+
function readPersistedToken() {
|
|
357
|
+
if (process.platform === 'win32') {
|
|
358
|
+
const run = (script) => {
|
|
359
|
+
try {
|
|
360
|
+
return execFileSync('powershell', ['-NoProfile', '-Command', script], { stdio: 'pipe', encoding: 'utf8' }).trim();
|
|
361
|
+
} catch {
|
|
362
|
+
return '';
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const viaDotNet = run(`[Environment]::GetEnvironmentVariable('${TOKEN_VAR}', 'User')`);
|
|
366
|
+
if (viaDotNet) return viaDotNet;
|
|
367
|
+
// Modo restrito (Constrained Language): le do registro, par do fallback do persistToken.
|
|
368
|
+
return run(`(Get-ItemProperty -Path 'HKCU:\\Environment' -Name '${TOKEN_VAR}' -ErrorAction SilentlyContinue).${TOKEN_VAR}`) || null;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const file = envFilePath();
|
|
372
|
+
return fs.existsSync(file) ? parseEnvFileToken(fs.readFileSync(file, 'utf8')) : null;
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
192
378
|
// ─── Comando principal ───────────────────────────────────
|
|
193
379
|
|
|
194
380
|
// Orientacao de escopo: o token fica salvo no computador e qualquer programa do
|
|
@@ -204,8 +390,8 @@ function accessTips() {
|
|
|
204
390
|
log(` ${DIM} com o cofre dedicado, um vazamento so alcanca o que esta nele.${RESET}`);
|
|
205
391
|
}
|
|
206
392
|
|
|
207
|
-
async function
|
|
208
|
-
console.log(`\n${PURPLE}${BOLD} Conectar 1Password — Auroq OS${RESET}`);
|
|
393
|
+
async function connectManual() {
|
|
394
|
+
console.log(`\n${PURPLE}${BOLD} Conectar 1Password — Auroq OS (modo manual)${RESET}`);
|
|
209
395
|
|
|
210
396
|
// 1. Garantir que o op existe
|
|
211
397
|
step('Verificando 1Password CLI...');
|
|
@@ -258,6 +444,8 @@ async function connect() {
|
|
|
258
444
|
step('Salvando conexao...');
|
|
259
445
|
const where = persistToken(token);
|
|
260
446
|
ok(`Token salvo em ${where}`);
|
|
447
|
+
if (clearClipboard()) ok('Area de transferencia limpa (o token nao fica copiado)');
|
|
448
|
+
else warn('Nao consegui limpar a area de transferencia: copie outra coisa por cima (no Windows, apague tambem o historico com Win + V).');
|
|
261
449
|
|
|
262
450
|
// 5. Resultado
|
|
263
451
|
const restartHint = process.platform === 'win32'
|
|
@@ -277,9 +465,1114 @@ ${restartHint}
|
|
|
277
465
|
log('');
|
|
278
466
|
}
|
|
279
467
|
|
|
468
|
+
// ─── Fluxo automatico (conta de servico criada pela sessao do aluno) ──────
|
|
469
|
+
|
|
470
|
+
const VAULT_NAME = 'Claude';
|
|
471
|
+
const SERVICE_ACCOUNT_NAME = 'Auroq';
|
|
472
|
+
// `op service-account create` so existe desde a 2.26.0 e `op whoami` so diz QUEM esta falando (conta de
|
|
473
|
+
// servico ou pessoa) desde a 2.30.0; o teste final precisa dessa resposta, entao o minimo e a 2.30.0.
|
|
474
|
+
const MIN_OP_VERSION = '2.30.0';
|
|
475
|
+
const INTEGRATION_URL = 'onepassword://settings/developers'; // abre o app direto em Configuracoes > Desenvolvedor
|
|
476
|
+
const BACKUP_TITLE = 'Auroq OS - token da conta de servico';
|
|
477
|
+
const BACKUP_TAG = 'auroq-os';
|
|
478
|
+
// Contas pessoais (Individual e Families) entram em my.1password.<regiao>; Teams e Business em <time>.1password.*.
|
|
479
|
+
const PERSONAL_ACCOUNT_URL = /^my\.1password\.(com|eu|ca)$/;
|
|
480
|
+
const LOCK_STALE_MS = 2 * 60 * 60 * 1000;
|
|
481
|
+
const MAX_WAIT_SECONDS = 30 * 60; // espera da chave do app nunca passa disso (a trava de instancia vale 2 h)
|
|
482
|
+
// "Aguardando" (falta uma acao do aluno) e uma ETAPA do fluxo, nao falha: sai com 0 e a linha
|
|
483
|
+
// AUROQ_RESULTADO diz o que falta. Sair com erro faria a telemetria do instalador registrar
|
|
484
|
+
// INSTALL_FAILED para todo aluno que ainda nao ligou a chave no app.
|
|
485
|
+
const EXIT_CODE = { ok: 0, aguardando: 0, erro: 1 };
|
|
486
|
+
|
|
487
|
+
// ─── Helpers puros (testaveis) ───────────────────────────
|
|
488
|
+
|
|
489
|
+
function parseVersion(text) {
|
|
490
|
+
const m = String(text || '').match(/(\d+)\.(\d+)\.(\d+)/);
|
|
491
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function isVersionAtLeast(version, min = MIN_OP_VERSION) {
|
|
495
|
+
const have = parseVersion(version);
|
|
496
|
+
const need = parseVersion(min);
|
|
497
|
+
if (!have || !need) return false;
|
|
498
|
+
for (let i = 0; i < 3; i++) {
|
|
499
|
+
if (have[i] !== need[i]) return have[i] > need[i];
|
|
500
|
+
}
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Tira qualquer coisa com cara de token de um texto antes de mostrar. */
|
|
505
|
+
function redact(text) {
|
|
506
|
+
return String(text || '').replace(/ops_[A-Za-z0-9_=+/.-]{10,}/g, 'ops_[oculto]');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Uma linha so, sem token e sem o prefixo "[ERROR] data hora" do op. Normaliza ANTES de redigir:
|
|
511
|
+
* um token cortado por codigo de cor ou quebra de linha precisa voltar a ser uma peca so para ser pego.
|
|
512
|
+
*/
|
|
513
|
+
function oneLine(text, max = 300) {
|
|
514
|
+
const flat = String(text || '')
|
|
515
|
+
.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, '') // OSC (titulo de janela, area de transferencia...)
|
|
516
|
+
.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '') // cores e movimento de cursor
|
|
517
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '') // controle sem efeito visivel
|
|
518
|
+
.replace(/[\n\r\u0085\u2028\u2029\t]+/g, ' ');
|
|
519
|
+
return redact(flat)
|
|
520
|
+
.replace(/\[(ERROR|WARNING)\]\s+\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s*/g, '')
|
|
521
|
+
.replace(/\s+/g, ' ')
|
|
522
|
+
.trim()
|
|
523
|
+
.slice(0, max);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Pega o token (ops_...) de dentro da saida de `--raw`, ignorando qualquer outra linha. */
|
|
527
|
+
function extractToken(stdout) {
|
|
528
|
+
for (const piece of String(stdout || '').split(/\s+/)) {
|
|
529
|
+
if (isValidToken(piece)) return piece;
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function parseJson(text) {
|
|
535
|
+
try {
|
|
536
|
+
return JSON.parse(text);
|
|
537
|
+
} catch {
|
|
538
|
+
// Shell interativo (.bashrc que escreve na tela): fica so com o trecho entre colchetes/chaves.
|
|
539
|
+
const t = String(text || '');
|
|
540
|
+
for (const [open, close] of [['[', ']'], ['{', '}']]) {
|
|
541
|
+
const a = t.indexOf(open);
|
|
542
|
+
const b = t.lastIndexOf(close);
|
|
543
|
+
if (a !== -1 && b > a) {
|
|
544
|
+
try {
|
|
545
|
+
return JSON.parse(t.slice(a, b + 1));
|
|
546
|
+
} catch {
|
|
547
|
+
/* tenta o proximo formato */
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Ambiente do `op` filho. Sem token: sessao do aluno (app). Com token: so a conta de servico.
|
|
557
|
+
* O token do ambiente atual, a conexao do Connect e o modo debug SEMPRE saem: o primeiro faria o
|
|
558
|
+
* `op` ignorar o app, o segundo tem prioridade sobre tudo e o terceiro escreve detalhes na tela.
|
|
559
|
+
* `isolate` (so com token) desliga tambem a integracao com o app, para o teste valer so pelo token.
|
|
560
|
+
*/
|
|
561
|
+
function opEnv(token, { isolate = false } = {}) {
|
|
562
|
+
const env = { ...process.env };
|
|
563
|
+
const drop = new Set([TOKEN_VAR, 'OP_CONNECT_HOST', 'OP_CONNECT_TOKEN', 'OP_BIOMETRIC_UNLOCK_ENABLED', 'OP_DEBUG', 'OP_FORMAT']);
|
|
564
|
+
for (const k of Object.keys(env)) {
|
|
565
|
+
if (drop.has(k.toUpperCase())) delete env[k];
|
|
566
|
+
}
|
|
567
|
+
if (token) {
|
|
568
|
+
env[TOKEN_VAR] = token;
|
|
569
|
+
if (isolate) env.OP_BIOMETRIC_UNLOCK_ENABLED = 'false';
|
|
570
|
+
}
|
|
571
|
+
return env;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Classifica a falha de um comando do `op` pelo texto de erro (o `op` fala ingles). */
|
|
575
|
+
function classifyFailure(r) {
|
|
576
|
+
if (r && r.timedOut) return 'tempo';
|
|
577
|
+
const text = String((r && r.stderr) || '').toLowerCase();
|
|
578
|
+
if (/(429|too many requests|rate.?limit)/.test(text)) return 'limite';
|
|
579
|
+
if (/(dial tcp|no such host|network|connection refused|connection reset|timed out|timeout|offline|tls handshake|certificate)/.test(text)) return 'rede';
|
|
580
|
+
if (/(no active session|not signed in|no accounts? configured|no account found)/.test(text)) return 'sem-sessao';
|
|
581
|
+
if (/(forbidden|not authorized|unauthorized|permission|not allowed|insufficient|denied|only .*owner|administrator)/.test(text)) return 'permissao';
|
|
582
|
+
if (/(authoriz|locked|biometric|integration|sign in|signin)/.test(text)) return 'sessao';
|
|
583
|
+
return 'falha';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Falha de um comando feito COM o token da conta de servico. So "token-invalido" (recusa de
|
|
588
|
+
* autenticacao clara do 1Password) prova que o token morreu; limite de uso, rede, tempo, resposta de
|
|
589
|
+
* proxy, erro de disco ou qualquer texto desconhecido sao inconclusivos e NUNCA podem levar a criar
|
|
590
|
+
* outra conta de servico nem a arquivar a copia que ainda pode estar boa.
|
|
591
|
+
*/
|
|
592
|
+
const TOKEN_DEAD_RE = /(not authorized|\(401\)|\bunauthori[sz]ed\b|\brevoked\b|token\b[^.\n]{0,40}\b(?:invalid|expired|deleted|disabled)\b|\binvalid\b[^.\n]{0,30}\btoken\b|service account\b[^.\n]{0,40}\b(?:deleted|disabled)\b)/i;
|
|
593
|
+
|
|
594
|
+
function classifyTokenFailure(r) {
|
|
595
|
+
const c = classifyFailure(r);
|
|
596
|
+
if (c === 'tempo' || c === 'rede' || c === 'limite') return c;
|
|
597
|
+
return TOKEN_DEAD_RE.test(String((r && r.stderr) || '')) ? 'token-invalido' : 'falha';
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const WHY = {
|
|
601
|
+
limite: 'o 1Password limitou os acessos por enquanto (limite de uso)',
|
|
602
|
+
rede: 'sem conexao com o 1Password',
|
|
603
|
+
tempo: 'o 1Password demorou demais para responder',
|
|
604
|
+
falha: 'resposta inesperada do 1Password',
|
|
605
|
+
'resposta-estranha': 'resposta inesperada do 1Password',
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
function normalizeAddress(value) {
|
|
609
|
+
return String(value || '').trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function accountLabel(a) {
|
|
613
|
+
return `${a.email || 'conta sem e-mail'} (${a.url || 'endereco desconhecido'})`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function accountKey(a) {
|
|
617
|
+
return a.account_uuid || a.user_uuid || a.shorthand || a.url;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function isPersonalAccount(a) {
|
|
621
|
+
return PERSONAL_ACCOUNT_URL.test(normalizeAddress(a && a.url));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Escolhe a conta do app. Com varias contas NAO adivinha: criar a chave na conta errada
|
|
626
|
+
* (a da empresa, por exemplo) seria pior que perguntar.
|
|
627
|
+
* Devolve { conta } ou { erro: 'nenhuma' | 'varias' | 'nao-achou' | 'ambigua' }.
|
|
628
|
+
*/
|
|
629
|
+
function pickAccount(accounts, wanted) {
|
|
630
|
+
const list = Array.isArray(accounts) ? accounts : [];
|
|
631
|
+
if (!list.length) return { erro: 'nenhuma' };
|
|
632
|
+
if (wanted) {
|
|
633
|
+
const w = normalizeAddress(wanted);
|
|
634
|
+
const hit = list.filter((a) => [a.email, a.url, a.account_uuid, a.user_uuid, a.shorthand]
|
|
635
|
+
.some((v) => v && normalizeAddress(v) === w));
|
|
636
|
+
if (hit.length === 1) return { conta: hit[0] };
|
|
637
|
+
return { erro: hit.length ? 'ambigua' : 'nao-achou' };
|
|
638
|
+
}
|
|
639
|
+
return list.length === 1 ? { conta: list[0] } : { erro: 'varias' };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Argumentos de `auroq-os conectar-1password [...]`. */
|
|
643
|
+
function parseArgs(argv) {
|
|
644
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
645
|
+
const opts = { manual: false, rotacionar: false, agente: false, estado: false, conta: null, esperar: null };
|
|
646
|
+
for (let i = 0; i < args.length; i++) {
|
|
647
|
+
const a = args[i];
|
|
648
|
+
if (a === '--manual') opts.manual = true;
|
|
649
|
+
else if (a === '--rotacionar') opts.rotacionar = true;
|
|
650
|
+
else if (a === '--agente') opts.agente = true;
|
|
651
|
+
else if (a === '--estado') opts.estado = true;
|
|
652
|
+
else if (a === '--conta' && args[i + 1] && !args[i + 1].startsWith('--')) opts.conta = args[++i];
|
|
653
|
+
else if (a.startsWith('--conta=')) opts.conta = a.slice('--conta='.length) || null;
|
|
654
|
+
else if (a === '--esperar' && args[i + 1] && !args[i + 1].startsWith('--')) opts.esperar = Number(args[++i]);
|
|
655
|
+
else if (a.startsWith('--esperar=')) opts.esperar = Number(a.slice('--esperar='.length));
|
|
656
|
+
}
|
|
657
|
+
if (opts.esperar !== null && (!Number.isFinite(opts.esperar) || opts.esperar < 0)) opts.esperar = null;
|
|
658
|
+
return opts;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** Receita de bolo para ligar a chave no app (o que o aluno faz uma vez so). */
|
|
662
|
+
function enableIntegrationSteps(platform) {
|
|
663
|
+
const abrir = platform === 'darwin'
|
|
664
|
+
? 'Abra as Configuracoes: no menu de cima do Mac, 1Password > Configuracoes (ou Cmd + virgula). Se eu ja abri a tela de Desenvolvedor pra voce, pule pro passo 3.'
|
|
665
|
+
: 'Abra as Configuracoes: clique no nome da sua conta (canto de cima, a esquerda) > Configuracoes. Se eu ja abri a tela de Desenvolvedor pra voce, pule pro passo 3.';
|
|
666
|
+
const digital = platform === 'darwin'
|
|
667
|
+
? 'Para aprovar com a digital: Configuracoes > Seguranca > "Desbloquear com Touch ID". Sem isso o app pede a senha do Mac.'
|
|
668
|
+
: 'Para aprovar com o Windows Hello: Configuracoes > Seguranca > "Desbloquear com Windows Hello". Sem isso o app pede a senha.';
|
|
669
|
+
return [
|
|
670
|
+
'Abra o app do 1Password, entre na sua conta e deixe o app aberto.',
|
|
671
|
+
abrir,
|
|
672
|
+
'Clique em "Desenvolvedor" (Developer) e marque "Integrar com a CLI do 1Password" (Integrate with 1Password CLI).',
|
|
673
|
+
digital,
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ─── Dependencias reais (trocaveis nos testes) ───────────
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Roda o `op`. Nunca lanca. A saida pode ter segredo (token): quem chama decide o que mostrar.
|
|
681
|
+
* O token NUNCA vai em `args`: so por `env` ou por `input` (stdin).
|
|
682
|
+
*/
|
|
683
|
+
function runOp(args, { env, input, timeoutMs = 120000 } = {}) {
|
|
684
|
+
// Sem texto para mandar e com terminal de verdade: o `op` herda o terminal, como se o aluno
|
|
685
|
+
// tivesse digitado (a aprovacao do app e amarrada ao terminal; com pipe nao ha terminal).
|
|
686
|
+
const useTerminal = input === undefined && Boolean(process.stdin.isTTY);
|
|
687
|
+
const r = spawnSync('op', args, {
|
|
688
|
+
encoding: 'utf8',
|
|
689
|
+
env: env || opEnv(),
|
|
690
|
+
...(useTerminal ? { stdio: ['inherit', 'pipe', 'pipe'] } : { input: input === undefined ? '' : input }),
|
|
691
|
+
timeout: timeoutMs,
|
|
692
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
693
|
+
windowsHide: true,
|
|
694
|
+
});
|
|
695
|
+
return {
|
|
696
|
+
ok: !r.error && r.status === 0,
|
|
697
|
+
status: r.status,
|
|
698
|
+
stdout: r.stdout || '',
|
|
699
|
+
stderr: r.stderr || '',
|
|
700
|
+
timedOut: !!(r.error && r.error.code === 'ETIMEDOUT'),
|
|
701
|
+
missing: !!(r.error && r.error.code === 'ENOENT'),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* "Terminal novo": no Mac/Linux abre um shell de verdade, que le sozinho o arquivo de ambiente
|
|
707
|
+
* (o token do processo atual sai do ambiente, entao so o arquivo conta). No Windows um processo
|
|
708
|
+
* filho herda o ambiente deste, entao usa o valor que um terminal novo leria (variavel do usuario).
|
|
709
|
+
*/
|
|
710
|
+
function runOpInFreshShell(args) {
|
|
711
|
+
if (process.platform === 'win32') {
|
|
712
|
+
const persisted = readPersistedToken();
|
|
713
|
+
return runOp(args, { env: opEnv(persisted || undefined) });
|
|
714
|
+
}
|
|
715
|
+
const { bin, flag } = freshShellFor(envFilePath());
|
|
716
|
+
const quoted = ['op', ...args].map((a) => `'${String(a).replace(/'/g, "'\\''")}'`).join(' ');
|
|
717
|
+
const r = spawnSync(bin, [flag, quoted], {
|
|
718
|
+
encoding: 'utf8',
|
|
719
|
+
env: opEnv(),
|
|
720
|
+
input: '',
|
|
721
|
+
timeout: 60000,
|
|
722
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
723
|
+
});
|
|
724
|
+
return {
|
|
725
|
+
ok: !r.error && r.status === 0,
|
|
726
|
+
status: r.status,
|
|
727
|
+
stdout: r.stdout || '',
|
|
728
|
+
stderr: r.stderr || '',
|
|
729
|
+
timedOut: !!(r.error && r.error.code === 'ETIMEDOUT'),
|
|
730
|
+
missing: !!(r.error && r.error.code === 'ENOENT'),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Abre um endereco no programa certo do sistema (aqui, a tela do app). Melhor esforco: nunca lanca.
|
|
736
|
+
* `AUROQ_NAO_ABRIR_APP=1` (usado nos testes) desliga. O filho nao herda o token do ambiente.
|
|
737
|
+
*/
|
|
738
|
+
function openUrl(url) {
|
|
739
|
+
if (process.env.AUROQ_NAO_ABRIR_APP) return;
|
|
740
|
+
try {
|
|
741
|
+
const [cmd, args] = process.platform === 'darwin'
|
|
742
|
+
? ['open', [url]]
|
|
743
|
+
: (process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : ['xdg-open', [url]]);
|
|
744
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true, env: opEnv() });
|
|
745
|
+
child.on('error', () => {});
|
|
746
|
+
child.unref();
|
|
747
|
+
} catch {
|
|
748
|
+
/* sem problema: a receita escrita continua valendo */
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Windows: o winget so muda o PATH no registro, nao no processo que ja esta rodando. Recompoe o
|
|
754
|
+
* PATH deste processo (maquina + usuario + pasta de links do winget) para achar o `op` recem-instalado.
|
|
755
|
+
*/
|
|
756
|
+
function refreshWindowsPath() {
|
|
757
|
+
if (process.platform !== 'win32') return;
|
|
758
|
+
const parts = [];
|
|
759
|
+
for (const scope of ['Machine', 'User']) {
|
|
760
|
+
try {
|
|
761
|
+
const v = execFileSync('powershell', ['-NoProfile', '-Command', `[Environment]::GetEnvironmentVariable('Path','${scope}')`], { stdio: 'pipe', encoding: 'utf8' }).trim();
|
|
762
|
+
if (v) parts.push(v);
|
|
763
|
+
} catch { /* modo restrito: segue com o que tem */ }
|
|
764
|
+
}
|
|
765
|
+
const links = process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Microsoft', 'WinGet', 'Links') : null;
|
|
766
|
+
if (links && fs.existsSync(links)) parts.push(links);
|
|
767
|
+
if (parts.length) process.env.PATH = [...parts, process.env.PATH || ''].join(';');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Pasta do aluno fora do git, a mesma onde ficam as credenciais do Auroq (~/.arcane). Aqui vivem a trava de
|
|
771
|
+
// instancia unica e o estado das etapas (nenhum segredo).
|
|
772
|
+
function auroqDir() {
|
|
773
|
+
const dir = path.join(os.homedir(), '.arcane');
|
|
774
|
+
try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch { /* sem pasta: quem usa trata */ }
|
|
775
|
+
return dir;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Trava de instancia unica: duas execucoes juntas criariam dois cofres e duas contas de servico.
|
|
780
|
+
* O arquivo guarda pid, hora e um codigo aleatorio; so quem tem o codigo apaga. A trava e velha se o dono
|
|
781
|
+
* morreu (Ctrl+C, terminal fechado), se o conteudo esta estragado ou se passou de 2 h (pid reaproveitado).
|
|
782
|
+
*/
|
|
783
|
+
function makeLock(name = '1password.lock') {
|
|
784
|
+
const lockPath = () => path.join(auroqDir(), name); // calculado so quando usa: criar o objeto nao toca no disco
|
|
785
|
+
const nonce = crypto.randomBytes(8).toString('hex');
|
|
786
|
+
let held = false;
|
|
787
|
+
const readInfo = () => {
|
|
788
|
+
try { return JSON.parse(fs.readFileSync(lockPath(), 'utf8')); } catch { return null; }
|
|
789
|
+
};
|
|
790
|
+
const stale = () => {
|
|
791
|
+
const info = readInfo();
|
|
792
|
+
if (!info || !Number.isInteger(info.pid) || info.pid <= 0 || !Number.isFinite(info.at)) return true;
|
|
793
|
+
const age = Date.now() - info.at;
|
|
794
|
+
if (age < -60000 || age > LOCK_STALE_MS) return true;
|
|
795
|
+
try {
|
|
796
|
+
process.kill(info.pid, 0);
|
|
797
|
+
return false;
|
|
798
|
+
} catch (e) {
|
|
799
|
+
return e.code === 'ESRCH'; // dono morreu
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
return {
|
|
803
|
+
acquire() {
|
|
804
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
805
|
+
const file = lockPath();
|
|
806
|
+
try {
|
|
807
|
+
const fd = fs.openSync(file, 'wx', 0o600);
|
|
808
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now(), nonce }));
|
|
809
|
+
fs.closeSync(fd);
|
|
810
|
+
held = true;
|
|
811
|
+
return true;
|
|
812
|
+
} catch (e) {
|
|
813
|
+
if (e.code !== 'EEXIST') return true; // sem como travar (permissao, disco): segue sem trava
|
|
814
|
+
if (!stale()) return false;
|
|
815
|
+
// toma a trava velha por rename atomico: dois processos vendo a mesma trava velha nao apagam um ao outro
|
|
816
|
+
try { fs.renameSync(file, `${file}.velha-${process.pid}-${attempt}`); fs.unlinkSync(`${file}.velha-${process.pid}-${attempt}`); } catch { /* outro pegou antes */ }
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
},
|
|
821
|
+
release() {
|
|
822
|
+
if (!held) return;
|
|
823
|
+
held = false;
|
|
824
|
+
const info = readInfo();
|
|
825
|
+
if (info && info.nonce === nonce) {
|
|
826
|
+
try { fs.unlinkSync(lockPath()); } catch { /* ja foi */ }
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ─── Estado das etapas (retomada) ────────────────────────
|
|
833
|
+
//
|
|
834
|
+
// O pipeline tem 8 etapas. O arquivo guarda so o que ja foi conferido e onde parou: nenhum segredo, nada
|
|
835
|
+
// de e-mail. A fonte da verdade e sempre a conferencia ao vivo; o arquivo so deixa o Ops/Companion saber
|
|
836
|
+
// em que etapa o aluno esta quando ele volta dias depois (ou em outro dia, em outra conversa).
|
|
837
|
+
const ETAPAS = ['conta', 'app', 'op', 'aprovacao', 'cofre', 'chave', 'teste', 'pronto'];
|
|
838
|
+
const ETAPA_NOME = {
|
|
839
|
+
conta: 'conta e plano do 1Password',
|
|
840
|
+
app: 'app instalado e chave "Integrar com a CLI" ligada',
|
|
841
|
+
op: 'ferramenta op instalada',
|
|
842
|
+
aprovacao: 'aprovacao com a digital',
|
|
843
|
+
cofre: 'cofre Claude',
|
|
844
|
+
chave: 'chave do agente (conta de servico) criada e guardada',
|
|
845
|
+
teste: 'teste em terminal novo',
|
|
846
|
+
pronto: 'pronto',
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
function stateFile() {
|
|
850
|
+
return path.join(auroqDir(), '1password-etapas.json');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function emptyEtapas() {
|
|
854
|
+
return Object.fromEntries(ETAPAS.map((e) => [e, 'pendente']));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** Estado salvo (ou vazio). Nunca lanca: arquivo ausente ou estragado = comeca do zero. */
|
|
858
|
+
function readState() {
|
|
859
|
+
try {
|
|
860
|
+
const data = JSON.parse(fs.readFileSync(stateFile(), 'utf8'));
|
|
861
|
+
if (!data || typeof data !== 'object' || data.versao !== 1 || !data.etapas) return null;
|
|
862
|
+
const etapas = emptyEtapas();
|
|
863
|
+
for (const e of ETAPAS) if (data.etapas[e] === 'feito') etapas[e] = 'feito';
|
|
864
|
+
return { versao: 1, etapas, ultimo: data.ultimo || null, atualizado_em: data.atualizado_em || null };
|
|
865
|
+
} catch {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function writeState(state) {
|
|
871
|
+
try {
|
|
872
|
+
writeFileAtomic(stateFile(), `${JSON.stringify(state, null, 2)}\n`);
|
|
873
|
+
} catch {
|
|
874
|
+
/* o estado e conforto, nunca motivo para falhar a conexao */
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function nextStage(etapas) {
|
|
879
|
+
return ETAPAS.find((e) => etapas[e] !== 'feito') || null;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Acompanha as etapas durante uma execucao e grava o estado no fim. So avanca; recua so no que foi provado quebrado. */
|
|
883
|
+
function makeProgress(d) {
|
|
884
|
+
const prev = d.state.read();
|
|
885
|
+
const etapas = prev ? { ...prev.etapas } : emptyEtapas();
|
|
886
|
+
return {
|
|
887
|
+
etapas,
|
|
888
|
+
mark(...stages) { for (const e of stages) etapas[e] = 'feito'; },
|
|
889
|
+
reset(...stages) { for (const e of stages) etapas[e] = 'pendente'; },
|
|
890
|
+
all() { this.mark(...ETAPAS); },
|
|
891
|
+
save(result) {
|
|
892
|
+
d.state.write({
|
|
893
|
+
versao: 1,
|
|
894
|
+
atualizado_em: new Date(d.now()).toISOString(),
|
|
895
|
+
etapas: { ...etapas },
|
|
896
|
+
proxima: nextStage(etapas),
|
|
897
|
+
ultimo: result ? { status: result.status, codigo: result.codigo } : null,
|
|
898
|
+
});
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function makeDefaultDeps() {
|
|
904
|
+
return {
|
|
905
|
+
platform: process.platform,
|
|
906
|
+
openUrl,
|
|
907
|
+
refreshPath: refreshWindowsPath,
|
|
908
|
+
lock: makeLock(),
|
|
909
|
+
state: { read: readState, write: writeState },
|
|
910
|
+
envFileProblem,
|
|
911
|
+
op: runOp,
|
|
912
|
+
opInFreshShell: runOpInFreshShell,
|
|
913
|
+
opVersion,
|
|
914
|
+
installOp,
|
|
915
|
+
persistToken,
|
|
916
|
+
readPersistedToken,
|
|
917
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
918
|
+
now: () => Date.now(),
|
|
919
|
+
isTTY: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
920
|
+
env: process.env,
|
|
921
|
+
out: { log, step, ok, warn, err, tick: () => process.stdout.write('.') },
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ─── Etapas ──────────────────────────────────────────────
|
|
926
|
+
|
|
927
|
+
function listAccounts(d) {
|
|
928
|
+
const r = d.op(['account', 'list', '--format', 'json'], { env: opEnv() });
|
|
929
|
+
const data = r.ok ? parseJson(r.stdout) : null;
|
|
930
|
+
return Array.isArray(data) ? data : [];
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* O que o token enxerga: precisa listar o cofre Claude e quem fala tem de ser a conta de servico
|
|
935
|
+
* (um token morto com o app aberto poderia "passar" pela sessao do aluno). Nunca mostra o token.
|
|
936
|
+
* `isolate`: desliga a integracao com o app no teste, para valer so pelo token.
|
|
937
|
+
* `expectAccount`: o token tem de ser da conta escolhida (nao adota token de outra conta).
|
|
938
|
+
* Falha: `motivo` 'token-invalido' = morreu; 'limite' | 'rede' | 'tempo' | 'falha' = inconclusivo.
|
|
939
|
+
*/
|
|
940
|
+
function checkToken(d, token, { runner, isolate = false, expectAccount = null } = {}) {
|
|
941
|
+
const run = runner || ((args) => d.op(args, { env: opEnv(token, { isolate }) }));
|
|
942
|
+
const r = run(['vault', 'list', '--format', 'json']);
|
|
943
|
+
if (!r.ok) return { ok: false, motivo: classifyTokenFailure(r), detalhe: oneLine(r.stderr) };
|
|
944
|
+
const data = parseJson(r.stdout);
|
|
945
|
+
if (!Array.isArray(data)) return { ok: false, motivo: 'resposta-estranha', detalhe: '' };
|
|
946
|
+
const who = run(['whoami', '--format', 'json']);
|
|
947
|
+
if (!who.ok) return { ok: false, motivo: classifyTokenFailure(who), detalhe: oneLine(who.stderr) };
|
|
948
|
+
const me = parseJson(who.stdout);
|
|
949
|
+
if (!me || me.user_type !== 'SERVICE_ACCOUNT') return { ok: false, motivo: 'app-tem-prioridade', detalhe: '' };
|
|
950
|
+
if (expectAccount) {
|
|
951
|
+
// O token tem de ser da conta escolhida. Sem identificador comparavel NAO adota (falha fechada).
|
|
952
|
+
if (expectAccount.account_uuid && me.account_uuid) {
|
|
953
|
+
if (String(me.account_uuid).toLowerCase() !== String(expectAccount.account_uuid).toLowerCase()) return { ok: false, motivo: 'conta-diferente', detalhe: '' };
|
|
954
|
+
} else if (expectAccount.url && me.url) {
|
|
955
|
+
if (normalizeAddress(me.url) !== normalizeAddress(expectAccount.url)) return { ok: false, motivo: 'conta-diferente', detalhe: '' };
|
|
956
|
+
} else {
|
|
957
|
+
return { ok: false, motivo: 'conta-nao-confirmada', detalhe: '' };
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const nomes = data.map((v) => String((v && v.name) || '').trim());
|
|
961
|
+
const same = (n) => n.toLowerCase() === VAULT_NAME.toLowerCase();
|
|
962
|
+
return { ok: true, temClaude: nomes.some(same), nomes, extras: nomes.filter((n) => !same(n)) };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Resultados em que repetir o teste nao muda nada.
|
|
966
|
+
const NO_RETRY = new Set(['app-tem-prioridade', 'conta-diferente', 'conta-nao-confirmada']);
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Repete o teste so quando repetir pode mudar o resultado. `retryInvalid`: logo depois de criar a conta
|
|
970
|
+
* de servico, "token invalido" e "ainda sem o cofre" podem ser so atraso do 1Password.
|
|
971
|
+
*/
|
|
972
|
+
async function checkTokenWithRetry(d, token, options = {}, tries = 8) {
|
|
973
|
+
let last = null;
|
|
974
|
+
for (let i = 0; i < tries; i++) {
|
|
975
|
+
last = checkToken(d, token, options);
|
|
976
|
+
if (last.ok && last.temClaude) return last;
|
|
977
|
+
if (NO_RETRY.has(last.motivo)) return last; // repetir nao muda
|
|
978
|
+
if (!options.retryInvalid && (last.ok || last.motivo === 'token-invalido')) return last;
|
|
979
|
+
if (i < tries - 1) await d.sleep(Math.min(1500 * (i + 1), 6000));
|
|
980
|
+
}
|
|
981
|
+
return last;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function checkItems(d, runner) {
|
|
985
|
+
const run = runner || ((args) => d.op(args, { env: opEnv() }));
|
|
986
|
+
const r = run(['item', 'list', '--vault', VAULT_NAME, '--format', 'json']);
|
|
987
|
+
return r.ok && Array.isArray(parseJson(r.stdout));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Molde "API Credential" (o que `op item template get "API Credential"` devolve na 2.38). O 1Password
|
|
991
|
+
// recomenda partir do molde completo; so title, notas e credential recebem valor.
|
|
992
|
+
function backupItemTemplate(token) {
|
|
993
|
+
const empty = (id, type, label, extra = {}) => ({ id, type, label, value: '', ...extra });
|
|
994
|
+
return {
|
|
995
|
+
title: BACKUP_TITLE,
|
|
996
|
+
category: 'API_CREDENTIAL',
|
|
997
|
+
fields: [
|
|
998
|
+
{
|
|
999
|
+
id: 'notesPlain',
|
|
1000
|
+
type: 'STRING',
|
|
1001
|
+
purpose: 'NOTES',
|
|
1002
|
+
label: 'notesPlain',
|
|
1003
|
+
value: `Token da conta de servico "${SERVICE_ACCOUNT_NAME}" (enxerga so o cofre ${VAULT_NAME}). Criado pelo Auroq OS. Nao apague: os outros computadores seus reaproveitam este token em vez de criar outro.`,
|
|
1004
|
+
},
|
|
1005
|
+
empty('username', 'STRING', 'username'),
|
|
1006
|
+
{ id: 'credential', type: 'CONCEALED', label: 'credential', value: token },
|
|
1007
|
+
empty('type', 'MENU', 'type'),
|
|
1008
|
+
empty('filename', 'STRING', 'filename'),
|
|
1009
|
+
empty('validFrom', 'DATE', 'valid from'),
|
|
1010
|
+
empty('expires', 'DATE', 'expires'),
|
|
1011
|
+
empty('hostname', 'STRING', 'hostname'),
|
|
1012
|
+
],
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** Guarda uma copia do token no cofre pessoal do aluno (a conta de servico nao enxerga esse cofre). */
|
|
1017
|
+
function saveBackup(d, account, token) {
|
|
1018
|
+
// Sem --vault: o `op` guarda no cofre pessoal da conta, que se chama Personal, Private ou Employee
|
|
1019
|
+
// conforme o plano (e pode vir traduzido), por isso nunca se usa o nome.
|
|
1020
|
+
const r = d.op(
|
|
1021
|
+
['item', 'create', '-', '--tags', BACKUP_TAG, '--account', accountKey(account), '--format', 'json'],
|
|
1022
|
+
{ env: opEnv(), input: JSON.stringify(backupItemTemplate(token)) },
|
|
1023
|
+
);
|
|
1024
|
+
return r.ok ? { ok: true } : { ok: false, detalhe: oneLine(r.stderr) };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Copias do token ja guardadas (mais nova primeiro). `ok:false` = nao deu para ler a lista.
|
|
1029
|
+
* A busca pega todos os cofres a que a conta tem acesso; quem adota uma copia confere que o token e
|
|
1030
|
+
* da MESMA conta (checkToken com `expectAccount`), entao item plantado em cofre compartilhado nao serve.
|
|
1031
|
+
*/
|
|
1032
|
+
function findBackups(d, account) {
|
|
1033
|
+
const r = d.op(['item', 'list', '--tags', BACKUP_TAG, '--account', accountKey(account), '--format', 'json'], { env: opEnv() });
|
|
1034
|
+
const list = r.ok ? parseJson(r.stdout) : null;
|
|
1035
|
+
if (!Array.isArray(list)) return { ok: false, itens: [] };
|
|
1036
|
+
return {
|
|
1037
|
+
ok: true,
|
|
1038
|
+
itens: list
|
|
1039
|
+
.filter((i) => i && i.id && i.title === BACKUP_TITLE)
|
|
1040
|
+
.sort((a, b) => String(b.updated_at || '').localeCompare(String(a.updated_at || '')))
|
|
1041
|
+
.map((i) => ({ id: i.id, vaultId: i.vault && i.vault.id ? i.vault.id : null })),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const FIELD_MISSING_RE = /(isn't a field|is not a field|no field|field[^.\n]{0,30}not found|could not be found|isn't an item|not found)/i;
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Le o token da copia: `op read` (a via documentada para um valor so), com `item get` de reserva.
|
|
1049
|
+
* Devolve { token } | {} (o item nao tem token valido: nao e nosso, ignora) | { erro: true } (nao deu para
|
|
1050
|
+
* ler agora; `incerto` = motivo desconhecido, que pode ser permanente).
|
|
1051
|
+
*/
|
|
1052
|
+
function readBackupToken(d, account, backup) {
|
|
1053
|
+
const acct = accountKey(account);
|
|
1054
|
+
if (backup.vaultId) {
|
|
1055
|
+
const r = d.op(['read', `op://${backup.vaultId}/${backup.id}/credential`, '--account', acct], { env: opEnv() });
|
|
1056
|
+
const value = r.ok ? r.stdout.trim() : '';
|
|
1057
|
+
if (isValidToken(value)) return { token: value };
|
|
1058
|
+
}
|
|
1059
|
+
const g = d.op(['item', 'get', backup.id, '--fields', 'label=credential', '--reveal', '--account', acct, '--format', 'json'], { env: opEnv() });
|
|
1060
|
+
if (!g.ok) {
|
|
1061
|
+
if (FIELD_MISSING_RE.test(String(g.stderr || ''))) return {};
|
|
1062
|
+
return { erro: true, incerto: classifyFailure(g) === 'falha' };
|
|
1063
|
+
}
|
|
1064
|
+
const data = parseJson(g.stdout);
|
|
1065
|
+
const field = Array.isArray(data) ? data[0] : data;
|
|
1066
|
+
const value = String(typeof field === 'string' ? field : (field && typeof field.value === 'string' ? field.value : g.stdout)).trim();
|
|
1067
|
+
return isValidToken(value) ? { token: value } : {};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function archiveBackups(d, account, ids) {
|
|
1071
|
+
for (const id of ids) {
|
|
1072
|
+
d.op(['item', 'delete', id, '--archive', '--account', accountKey(account)], { env: opEnv() });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function fim(status, codigo, mensagem, extra = {}) {
|
|
1077
|
+
return { status, codigo, mensagem, ...extra };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function showIntegrationHelp(d, out) {
|
|
1081
|
+
out.warn('O app do 1Password ainda nao esta ligado ao terminal. Falta uma chave, uma vez so:');
|
|
1082
|
+
d.openUrl(INTEGRATION_URL); // leva o app direto para Configuracoes > Desenvolvedor
|
|
1083
|
+
enableIntegrationSteps(d.platform).forEach((line, i) => out.log(` ${i + 1}. ${line}`));
|
|
1084
|
+
out.log('');
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const APP_PRIORITY_MSG = 'O 1Password continua respondendo pelo app (pedindo digital) em vez de usar a chave. Sem isso o agente nao fica sem pedir digital. Nao criei nada novo; peca ajuda ao Arcano com este codigo: app-tem-prioridade.';
|
|
1088
|
+
const NET_STRONG_RE = /(dial tcp|no such host|tls handshake|network is unreachable|no route to host|i\/o timeout)/i;
|
|
1089
|
+
|
|
1090
|
+
// Erros que o aluno (ou o plano dele) resolve: a linha AUROQ_RESULTADO ja diz o que fazer. Saem com 1 mas a
|
|
1091
|
+
// telemetria do instalador nao os conta como instalacao quebrada.
|
|
1092
|
+
const STUDENT_ACTIONABLE = new Set(['sem-op', 'op-antigo', 'rede', 'conta-nao-encontrada', 'conta-ambigua', 'cofre-duplicado', 'sem-permissao', 'token-indisponivel', 'arquivo-ambiente']);
|
|
1093
|
+
|
|
1094
|
+
function isStudentActionable(codigo) {
|
|
1095
|
+
return STUDENT_ACTIONABLE.has(codigo);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Conecta o 1Password ao Auroq sem o aluno tocar em token.
|
|
1100
|
+
*
|
|
1101
|
+
* @param {object} opts { rotacionar, conta, esperar (segundos), agente }
|
|
1102
|
+
* @param {object} deps so para teste: troca o `op`, o disco, o relogio e a tela
|
|
1103
|
+
* @returns {Promise<{status:'ok'|'aguardando'|'erro', codigo:string, mensagem:string, contas?:string[]}>}
|
|
1104
|
+
*/
|
|
1105
|
+
async function connectAutomatic(opts = {}, deps = {}) {
|
|
1106
|
+
const d = { ...makeDefaultDeps(), ...deps };
|
|
1107
|
+
if (!d.lock.acquire()) {
|
|
1108
|
+
return fim('aguardando', 'outra-execucao', 'Ja tem outra execucao do conectar-1password em andamento neste computador. Espere ela terminar (ate uns 15 minutos) e rode de novo; nao abri nada em duplicidade.');
|
|
1109
|
+
}
|
|
1110
|
+
const prog = makeProgress(d);
|
|
1111
|
+
let result = null;
|
|
1112
|
+
try {
|
|
1113
|
+
result = await runConnect(opts, d, prog);
|
|
1114
|
+
return result;
|
|
1115
|
+
} catch (e) {
|
|
1116
|
+
result = fim('erro', 'falha', `Falha inesperada: ${oneLine(e && e.message)}`);
|
|
1117
|
+
throw e;
|
|
1118
|
+
} finally {
|
|
1119
|
+
prog.save(result);
|
|
1120
|
+
d.lock.release();
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
async function runConnect(opts, d, prog) {
|
|
1125
|
+
const out = d.out;
|
|
1126
|
+
const waitSeconds = Math.min(MAX_WAIT_SECONDS, opts.esperar !== null && opts.esperar !== undefined
|
|
1127
|
+
? opts.esperar
|
|
1128
|
+
: (opts.agente || !d.isTTY() ? 0 : 300));
|
|
1129
|
+
|
|
1130
|
+
out.step('Verificando 1Password CLI...');
|
|
1131
|
+
d.refreshPath(); // Windows: o winget (do Ops ou do aluno) so muda o PATH no registro, nao neste processo
|
|
1132
|
+
let version = d.opVersion();
|
|
1133
|
+
if (!version) {
|
|
1134
|
+
out.warn('1Password CLI (op) nao encontrado — instalando...');
|
|
1135
|
+
if (!d.installOp()) return fim('erro', 'sem-op', 'Nao consegui instalar o 1Password CLI (op).');
|
|
1136
|
+
d.refreshPath();
|
|
1137
|
+
version = d.opVersion();
|
|
1138
|
+
if (!version) {
|
|
1139
|
+
return fim('erro', 'sem-op', 'O op foi instalado mas nao aparece no PATH. Feche e abra o terminal (ou o programa inteiro) e rode de novo.');
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (!isVersionAtLeast(version)) {
|
|
1143
|
+
out.warn(`1Password CLI v${version} e antigo (precisa da ${MIN_OP_VERSION} ou mais) — atualizando...`);
|
|
1144
|
+
if (d.installOp({ upgrade: true })) {
|
|
1145
|
+
d.refreshPath();
|
|
1146
|
+
version = d.opVersion() || version;
|
|
1147
|
+
}
|
|
1148
|
+
if (!isVersionAtLeast(version)) {
|
|
1149
|
+
return fim('erro', 'op-antigo', `O 1Password CLI v${version} e antigo (precisa da ${MIN_OP_VERSION} ou mais) e nao consegui atualizar sozinho.`);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
prog.mark('op');
|
|
1153
|
+
out.ok(`1Password CLI v${version}`);
|
|
1154
|
+
|
|
1155
|
+
// 1. Ja esta conectado? (token deste processo ou o gravado no computador)
|
|
1156
|
+
if (opts.rotacionar) {
|
|
1157
|
+
prog.reset('chave', 'teste', 'pronto');
|
|
1158
|
+
} else {
|
|
1159
|
+
const persisted = d.readPersistedToken();
|
|
1160
|
+
const candidates = [d.env[TOKEN_VAR], persisted].filter((t, i, all) => t && isValidToken(t) && all.indexOf(t) === i);
|
|
1161
|
+
let broken = false;
|
|
1162
|
+
let foreign = false;
|
|
1163
|
+
for (const token of candidates) {
|
|
1164
|
+
const check = await checkTokenWithRetry(d, token, {}, 3);
|
|
1165
|
+
if (check.ok && check.temClaude) {
|
|
1166
|
+
if (token === persisted) {
|
|
1167
|
+
// "Conectado" so vale se um terminal NOVO tambem enxerga a chave (outro shell pode ler outro arquivo)
|
|
1168
|
+
const fresh = checkToken(d, null, { runner: (args) => d.opInFreshShell(args) });
|
|
1169
|
+
if ((!fresh.ok && fresh.motivo === 'app-tem-prioridade') || (fresh.ok && !fresh.temClaude)) {
|
|
1170
|
+
return fim('erro', 'nao-gravou', 'A chave esta gravada neste computador, mas um terminal novo nao a enxerga (o seu terminal parece ler outro arquivo de configuracao). Peca ajuda ao Arcano com o codigo nao-gravou; nao criei nada novo.');
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
out.ok(`Ja conectado: o token deste computador enxerga o cofre ${VAULT_NAME}.`);
|
|
1174
|
+
if (check.extras.length) {
|
|
1175
|
+
out.warn(`Esse token tambem enxerga ${check.extras.length} outro(s) cofre(s). Para deixar so o ${VAULT_NAME}, rode de novo com --rotacionar.`);
|
|
1176
|
+
}
|
|
1177
|
+
prog.all();
|
|
1178
|
+
return fim('ok', 'ja-conectado', `Ja conectado: o token enxerga o cofre ${VAULT_NAME}.`, check.extras.length ? { aviso: 'cofres-extras' } : {});
|
|
1179
|
+
}
|
|
1180
|
+
if (!check.ok && check.motivo === 'app-tem-prioridade') return fim('erro', 'app-tem-prioridade', APP_PRIORITY_MSG);
|
|
1181
|
+
if (!check.ok && check.motivo !== 'token-invalido') {
|
|
1182
|
+
// Nao deu para conferir agora (limite de uso, rede, resposta estranha...): NAO e prova de token morto.
|
|
1183
|
+
// Criar outra conta de servico aqui duplicaria a chave a cada tentativa.
|
|
1184
|
+
return fim('erro', 'token-indisponivel', `Nao consegui conferir a chave deste computador agora (${WHY[check.motivo] || WHY.falha}). Nao criei nada novo, para nao duplicar. Tente de novo em alguns minutos; se continuar assim por mais de uma hora, a chave pode ter sido revogada: rode com --rotacionar.`);
|
|
1185
|
+
}
|
|
1186
|
+
if (check.ok) foreign = true;
|
|
1187
|
+
else broken = true;
|
|
1188
|
+
}
|
|
1189
|
+
if (broken || foreign) prog.reset('chave', 'teste', 'pronto');
|
|
1190
|
+
if (broken) {
|
|
1191
|
+
out.warn('O token que estava salvo neste computador nao funciona mais (revogado, vencido ou o endereco de login da conta mudou). Vou gerar um novo.');
|
|
1192
|
+
} else if (foreign) {
|
|
1193
|
+
out.warn(`O token salvo neste computador e de outra conta de servico (nao enxerga o cofre ${VAULT_NAME}). O Auroq vai passar a usar o proprio.`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Antes de criar QUALQUER coisa: a chave so aparece uma vez, entao tem de haver onde guarda-la.
|
|
1198
|
+
const envProblem = d.envFileProblem();
|
|
1199
|
+
if (envProblem) {
|
|
1200
|
+
return fim('erro', 'arquivo-ambiente', `Nao criei nada. ${envProblem}. Peca ajuda ao Arcano com o codigo arquivo-ambiente.`);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// 2. Sessao do aluno: o app do 1Password com a chave "Integrate with 1Password CLI" ligada
|
|
1204
|
+
out.step('Procurando a sua conta no app do 1Password...');
|
|
1205
|
+
let accounts = listAccounts(d);
|
|
1206
|
+
if (!accounts.length) {
|
|
1207
|
+
showIntegrationHelp(d, out);
|
|
1208
|
+
if (waitSeconds > 0) {
|
|
1209
|
+
out.log(`${DIM}Fico esperando ate ${Math.round(waitSeconds / 60) || 1} min. Pode ligar agora.${RESET}`);
|
|
1210
|
+
const start = d.now();
|
|
1211
|
+
while (!accounts.length && d.now() - start < waitSeconds * 1000) {
|
|
1212
|
+
await d.sleep(3000);
|
|
1213
|
+
out.tick();
|
|
1214
|
+
accounts = listAccounts(d);
|
|
1215
|
+
}
|
|
1216
|
+
out.log('');
|
|
1217
|
+
}
|
|
1218
|
+
if (!accounts.length) {
|
|
1219
|
+
prog.reset('app');
|
|
1220
|
+
return fim('aguardando', 'ligar-integracao', 'Falta ligar "Integrate with 1Password CLI" nas configuracoes do app do 1Password (Configuracoes > Desenvolvedor). Depois e so rodar de novo.');
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
prog.mark('conta', 'app');
|
|
1224
|
+
|
|
1225
|
+
const labels = accounts.map(accountLabel);
|
|
1226
|
+
const picked = pickAccount(accounts, opts.conta);
|
|
1227
|
+
if (picked.erro === 'varias') {
|
|
1228
|
+
out.warn('Tem mais de uma conta no app. Escolha em qual vai ficar o cofre do Auroq:');
|
|
1229
|
+
labels.forEach((l) => out.log(` - ${l}`));
|
|
1230
|
+
return fim('aguardando', 'escolher-conta', 'Tem mais de uma conta no app do 1Password. Diga qual usar e rode de novo com --conta "<e-mail>" (se dois e-mails forem iguais, use o endereco de login, ex.: --conta "my.1password.com").', { contas: labels });
|
|
1231
|
+
}
|
|
1232
|
+
if (picked.erro === 'ambigua') {
|
|
1233
|
+
return fim('erro', 'conta-ambigua', `Mais de uma conta combina com "${opts.conta}". Use o endereco de login da conta, ex.: --conta "my.1password.com".`, { contas: labels });
|
|
1234
|
+
}
|
|
1235
|
+
if (picked.erro === 'nao-achou') {
|
|
1236
|
+
return fim('erro', 'conta-nao-encontrada', `Nao achei a conta "${opts.conta}" no app do 1Password.`, { contas: labels });
|
|
1237
|
+
}
|
|
1238
|
+
const account = picked.conta;
|
|
1239
|
+
if (!opts.conta && !isPersonalAccount(account)) {
|
|
1240
|
+
return fim('aguardando', 'confirmar-conta', `A conta ${accountLabel(account)} parece ser de empresa (Teams/Business). O cofre do Auroq deve ficar na SUA conta pessoal, nao na da empresa. Se for esta mesmo, confirme rodando de novo com --conta "${account.email || account.url}".`, { contas: labels });
|
|
1241
|
+
}
|
|
1242
|
+
out.ok(`Conta: ${accountLabel(account)}`);
|
|
1243
|
+
|
|
1244
|
+
out.step('Pedindo sua aprovacao ao app do 1Password...');
|
|
1245
|
+
out.log(`${BOLD}Vai abrir uma janela do 1Password pedindo a sua digital (Touch ID / Windows Hello) ou a senha. Aprove. Se ela aparecer mais de uma vez, e so aprovar de novo.${RESET}`);
|
|
1246
|
+
const auth = d.op(['whoami', '--account', accountKey(account), '--format', 'json'], { env: opEnv(), timeoutMs: 180000 });
|
|
1247
|
+
if (!auth.ok) {
|
|
1248
|
+
const text = String(auth.stderr || '');
|
|
1249
|
+
if (NET_STRONG_RE.test(text)) return fim('erro', 'rede', `Sem conexao com o 1Password. ${oneLine(text)}`.trim());
|
|
1250
|
+
if (!auth.timedOut && classifyFailure(auth) === 'sem-sessao') {
|
|
1251
|
+
// Conta so adicionada na mao (`op account add`): aparece na lista mas sem sessao; falta a chave do app.
|
|
1252
|
+
showIntegrationHelp(d, out);
|
|
1253
|
+
return fim('aguardando', 'ligar-integracao', 'Falta ligar "Integrate with 1Password CLI" nas configuracoes do app do 1Password (Configuracoes > Desenvolvedor). Depois e so rodar de novo.');
|
|
1254
|
+
}
|
|
1255
|
+
const detail = oneLine(text, 160);
|
|
1256
|
+
return fim('aguardando', 'aprovar-no-app', `Nao consegui entrar na conta${auth.timedOut ? ': a janela da digital nao foi aprovada a tempo' : ''}. Confira se o app do 1Password esta aberto e desbloqueado, aprove a janela da digital e rode de novo.${detail ? ` (O 1Password disse: ${detail})` : ''}`);
|
|
1257
|
+
}
|
|
1258
|
+
prog.mark('aprovacao');
|
|
1259
|
+
out.ok('Aprovado');
|
|
1260
|
+
|
|
1261
|
+
// 3. Copias do token no cofre pessoal: segunda maquina reaproveita; rotacao e chave morta arquivam
|
|
1262
|
+
const found = findBackups(d, account);
|
|
1263
|
+
if (!found.ok) {
|
|
1264
|
+
return fim('erro', 'token-indisponivel', 'Nao consegui ler a lista de copias da chave no seu cofre pessoal agora. Nao criei nada novo, para nao duplicar. Tente de novo em alguns minutos.');
|
|
1265
|
+
}
|
|
1266
|
+
const stale = []; // copias velhas (chave morta, ou trocada na rotacao) que vao pro arquivo depois que a nova estiver salva
|
|
1267
|
+
for (const b of found.itens) {
|
|
1268
|
+
const read = readBackupToken(d, account, b);
|
|
1269
|
+
if (read.erro) {
|
|
1270
|
+
if (opts.rotacionar) continue; // pediu chave nova de proposito: copia ilegivel nao bloqueia
|
|
1271
|
+
return fim('erro', 'token-indisponivel', `Achei a copia da chave no seu cofre pessoal mas nao consegui le-la agora. Nao criei nada novo, para nao duplicar. Tente de novo em alguns minutos${read.incerto ? '; se continuar assim por mais de uma hora, a copia pode estar estragada: rode com --rotacionar' : ''}.`);
|
|
1272
|
+
}
|
|
1273
|
+
if (!read.token) continue; // item com esse nome mas sem chave valida: nao e nosso, nao mexe
|
|
1274
|
+
const check = await checkTokenWithRetry(d, read.token, { isolate: true, expectAccount: account }, 2);
|
|
1275
|
+
if (check.ok && check.temClaude) {
|
|
1276
|
+
if (opts.rotacionar) {
|
|
1277
|
+
stale.push(b);
|
|
1278
|
+
} else if (check.extras.length) {
|
|
1279
|
+
out.warn(`A copia guardada no seu cofre pessoal enxerga mais do que o cofre ${VAULT_NAME}; nao vou usar essa. Vou criar uma chave so do ${VAULT_NAME}.`);
|
|
1280
|
+
} else {
|
|
1281
|
+
out.step('Achei a chave do Auroq guardada no seu cofre pessoal — vou reaproveitar (nao crio outra).');
|
|
1282
|
+
prog.mark('cofre');
|
|
1283
|
+
return finishWithToken(d, out, account, read.token, prog, { reaproveitada: true, oldBackups: [] });
|
|
1284
|
+
}
|
|
1285
|
+
} else if (!check.ok && check.motivo === 'token-invalido') {
|
|
1286
|
+
stale.push(b);
|
|
1287
|
+
} else if (!check.ok && !NO_RETRY.has(check.motivo)) {
|
|
1288
|
+
return fim('erro', 'token-indisponivel', `Nao consegui conferir a copia da chave agora (${WHY[check.motivo] || WHY.falha}). Nao criei nada novo, para nao duplicar. Tente de novo em alguns minutos.`);
|
|
1289
|
+
}
|
|
1290
|
+
// conta-diferente / conta-nao-confirmada (token de outra conta) ou valido sem o cofre Claude: nao e nosso, fica como esta
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// 4. Cofre Claude
|
|
1294
|
+
out.step(`Preparando o cofre ${VAULT_NAME}...`);
|
|
1295
|
+
const vaultList = d.op(['vault', 'list', '--account', accountKey(account), '--format', 'json'], { env: opEnv() });
|
|
1296
|
+
const vaults = vaultList.ok ? parseJson(vaultList.stdout) : null;
|
|
1297
|
+
if (!Array.isArray(vaults)) {
|
|
1298
|
+
return fim('erro', 'sem-cofre', `Nao consegui ler os cofres da conta. ${oneLine(vaultList.stderr)}`.trim());
|
|
1299
|
+
}
|
|
1300
|
+
const sameName = vaults.filter((v) => String((v && v.name) || '').trim().toLowerCase() === VAULT_NAME.toLowerCase());
|
|
1301
|
+
if (sameName.length > 1) {
|
|
1302
|
+
return fim('erro', 'cofre-duplicado', `Tem ${sameName.length} cofres chamados ${VAULT_NAME} na sua conta. Apague ou renomeie os que sobram no app do 1Password e rode de novo (nao criei nada).`);
|
|
1303
|
+
}
|
|
1304
|
+
let createdVault = false;
|
|
1305
|
+
if (sameName.length === 1) {
|
|
1306
|
+
out.ok(`Cofre ${VAULT_NAME} ja existe — vou usar ele (nada do que esta nele muda).`);
|
|
1307
|
+
} else {
|
|
1308
|
+
const created = d.op(
|
|
1309
|
+
['vault', 'create', VAULT_NAME, '--description', 'Cofre do Auroq OS: chaves e tokens dos servicos conectados', '--account', accountKey(account)],
|
|
1310
|
+
{ env: opEnv() },
|
|
1311
|
+
);
|
|
1312
|
+
if (!created.ok) {
|
|
1313
|
+
const motivo = classifyFailure(created);
|
|
1314
|
+
return fim('erro', motivo === 'permissao' ? 'sem-permissao' : 'sem-cofre', `Nao consegui criar o cofre ${VAULT_NAME}. ${oneLine(created.stderr)}`.trim());
|
|
1315
|
+
}
|
|
1316
|
+
createdVault = true;
|
|
1317
|
+
out.ok(`Cofre ${VAULT_NAME} criado`);
|
|
1318
|
+
}
|
|
1319
|
+
prog.mark('cofre');
|
|
1320
|
+
|
|
1321
|
+
// 5. Conta de servico: o token nasce aqui dentro e nunca e impresso
|
|
1322
|
+
out.step('Criando a chave do agente (conta de servico, sem validade, so no cofre Claude)...');
|
|
1323
|
+
const sa = d.op(
|
|
1324
|
+
['service-account', 'create', SERVICE_ACCOUNT_NAME, '--vault', `${VAULT_NAME}:read_items,write_items`, '--raw', '--account', accountKey(account)],
|
|
1325
|
+
{ env: opEnv(), timeoutMs: 120000 },
|
|
1326
|
+
);
|
|
1327
|
+
// Mesmo que o `op` tenha saido com erro, se o token veio na saida ele e a unica copia: segue com ele.
|
|
1328
|
+
const token = extractToken(sa.stdout);
|
|
1329
|
+
if (!token) {
|
|
1330
|
+
const motivo = classifyFailure(sa);
|
|
1331
|
+
const orphan = ' O 1Password so mostra a chave uma vez: se a conta de servico "Auroq" foi criada agora ha pouco (veja a data em 1password.com > Developer > Service accounts), revogue essa e rode de novo.';
|
|
1332
|
+
if (sa.ok) {
|
|
1333
|
+
return fim('erro', 'falha-criar', `A conta de servico foi criada mas o 1Password nao devolveu um token valido.${orphan}`);
|
|
1334
|
+
}
|
|
1335
|
+
if (motivo === 'permissao') {
|
|
1336
|
+
const vazio = createdVault ? ` O cofre ${VAULT_NAME} que acabei de criar ficou vazio nesta conta; pode apagar no app.` : '';
|
|
1337
|
+
return fim('erro', 'sem-permissao', `Nao consegui criar a conta de servico. ${oneLine(sa.stderr)} (Em conta de empresa so o dono ou administrador cria; no plano individual, quem criou a conta consegue.)${vazio}`.trim());
|
|
1338
|
+
}
|
|
1339
|
+
if (motivo === 'rede') return fim('erro', 'rede', `Sem conexao com o 1Password ao criar a chave. ${oneLine(sa.stderr)}${orphan}`.trim());
|
|
1340
|
+
const incerto = sa.timedOut || sa.status === null || sa.status === undefined ? orphan : '';
|
|
1341
|
+
return fim('erro', 'falha-criar', `Nao consegui criar a conta de servico. ${oneLine(sa.stderr)}${incerto}`.trim());
|
|
1342
|
+
}
|
|
1343
|
+
if (!sa.ok) out.warn('O 1Password avisou de um problema ao criar a chave, mas ela veio na resposta; sigo com ela.');
|
|
1344
|
+
out.ok('Chave criada (nao aparece na tela)');
|
|
1345
|
+
|
|
1346
|
+
return finishWithToken(d, out, account, token, prog, { reaproveitada: false, oldBackups: stale });
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Daqui em diante o token ja existe e so aparece UMA vez: grava no computador e na copia pessoal
|
|
1351
|
+
* ANTES de testar (se o teste falhar, o token nao se perde), depois confere como um terminal novo.
|
|
1352
|
+
*/
|
|
1353
|
+
async function finishWithToken(d, out, account, token, prog, { reaproveitada, oldBackups }) {
|
|
1354
|
+
out.step('Salvando a conexao neste computador...');
|
|
1355
|
+
let where = null;
|
|
1356
|
+
let persistError = null;
|
|
1357
|
+
try {
|
|
1358
|
+
where = d.persistToken(token);
|
|
1359
|
+
out.ok(`Chave salva em ${where}`);
|
|
1360
|
+
} catch (e) {
|
|
1361
|
+
persistError = oneLine(e && e.message);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
let backupOk = reaproveitada; // reaproveitar = a copia ja existe
|
|
1365
|
+
if (!reaproveitada) {
|
|
1366
|
+
const backup = saveBackup(d, account, token);
|
|
1367
|
+
backupOk = backup.ok;
|
|
1368
|
+
if (backup.ok) {
|
|
1369
|
+
out.ok(`Copia guardada no seu cofre pessoal (item "${BACKUP_TITLE}"). Outro computador seu reaproveita essa chave.`);
|
|
1370
|
+
if (oldBackups && oldBackups.length) archiveBackups(d, account, oldBackups.map((b) => b.id));
|
|
1371
|
+
} else {
|
|
1372
|
+
out.warn(`Nao consegui guardar a copia no cofre pessoal. ${backup.detalhe}`.trim());
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (persistError) {
|
|
1377
|
+
if (backupOk) {
|
|
1378
|
+
prog.mark('chave');
|
|
1379
|
+
return fim('erro', 'nao-gravou', `Nao consegui salvar a chave neste computador (${persistError}), mas deixei uma copia no seu cofre pessoal. Resolva isso e rode de novo (sem --rotacionar): ele reaproveita a copia.`);
|
|
1380
|
+
}
|
|
1381
|
+
return fim('erro', 'nao-gravou', `A conta de servico "Auroq" foi criada mas a chave nao foi salva em lugar nenhum (${persistError}). Revogue a conta de servico criada agora (veja a data) em 1password.com > Developer > Service accounts e rode de novo.`);
|
|
1382
|
+
}
|
|
1383
|
+
prog.mark('chave');
|
|
1384
|
+
|
|
1385
|
+
out.step('Testando como um terminal novo...');
|
|
1386
|
+
const persisted = d.readPersistedToken();
|
|
1387
|
+
if (persisted !== token) {
|
|
1388
|
+
return fim('erro', 'nao-gravou', 'A chave foi gravada mas um terminal novo nao a enxerga. Feche e abra o terminal (no Windows, o programa inteiro) e rode de novo (sem --rotacionar) para conferir.');
|
|
1389
|
+
}
|
|
1390
|
+
const fresh = (args) => d.opInFreshShell(args);
|
|
1391
|
+
const check = await checkTokenWithRetry(d, null, { runner: fresh, retryInvalid: true });
|
|
1392
|
+
if (check && !check.ok && check.motivo === 'app-tem-prioridade') {
|
|
1393
|
+
return fim('erro', 'app-tem-prioridade', APP_PRIORITY_MSG.replace('Nao criei nada novo;', 'A chave ja esta salva;'));
|
|
1394
|
+
}
|
|
1395
|
+
if (!check || !check.ok) {
|
|
1396
|
+
return fim('erro', 'token-recusado', `A chave foi salva mas o teste falhou (${check ? (WHY[check.motivo] || check.motivo) : 'sem resposta'}). ${check && check.detalhe ? check.detalhe : ''} Rode de novo (sem --rotacionar) para conferir.`.trim());
|
|
1397
|
+
}
|
|
1398
|
+
if (!check.temClaude) {
|
|
1399
|
+
return fim('erro', 'token-recusado', `A chave foi salva mas nao enxerga o cofre ${VAULT_NAME}. Espere um minuto e rode de novo (sem --rotacionar).`);
|
|
1400
|
+
}
|
|
1401
|
+
if (check.extras.length) {
|
|
1402
|
+
return fim('erro', 'cofres-extras', `A chave foi salva mas enxerga mais do que o cofre ${VAULT_NAME} (${check.extras.length} outro(s)). O combinado e so o ${VAULT_NAME}: rode de novo com --rotacionar.`);
|
|
1403
|
+
}
|
|
1404
|
+
out.ok(`Cofres que a chave enxerga: ${check.nomes.join(', ')}`);
|
|
1405
|
+
if (!checkItems(d, (args) => d.opInFreshShell(args))) {
|
|
1406
|
+
return fim('erro', 'token-recusado', `A chave enxerga o cofre ${VAULT_NAME} mas nao conseguiu listar os itens dele. Rode de novo (sem --rotacionar) para conferir.`);
|
|
1407
|
+
}
|
|
1408
|
+
out.ok(`Itens do cofre ${VAULT_NAME}: leitura funcionando`);
|
|
1409
|
+
prog.mark('teste', 'pronto');
|
|
1410
|
+
|
|
1411
|
+
const restart = d.platform === 'win32'
|
|
1412
|
+
? 'Feche TODAS as janelas do terminal (se usa VS Code ou Windows Terminal, feche o programa inteiro) e abra de novo, para eles enxergarem a conexao nova.'
|
|
1413
|
+
: 'Feche e abra de novo o terminal (e o Claude/Codex), para eles enxergarem a conexao nova.';
|
|
1414
|
+
out.log('');
|
|
1415
|
+
out.ok(`${BOLD}1Password conectado.${RESET} O agente usa so a chave: nao pede mais digital.`);
|
|
1416
|
+
out.log(`${DIM}${restart}${RESET}`);
|
|
1417
|
+
out.log(`${DIM}Limite do plano individual/Familia: 1.000 leituras e 100 escritas por hora, 1.000 chamadas por dia. O Auroq guarda o que le, para nao gastar a cota.${RESET}`);
|
|
1418
|
+
if (!backupOk) out.log(`${DIM}Sem copia no cofre pessoal: se perder este computador, e so rodar de novo com --rotacionar.${RESET}`);
|
|
1419
|
+
if (oldBackups && oldBackups.length && !reaproveitada) {
|
|
1420
|
+
out.log(`${DIM}A chave antiga continua valendo ate voce revogar a conta de servico antiga (a mais velha chamada "Auroq") em 1password.com > Developer > Service accounts.${RESET}`);
|
|
1421
|
+
}
|
|
1422
|
+
return fim('ok', reaproveitada ? 'reaproveitada' : 'criada', `1Password conectado. O token enxerga so o cofre ${VAULT_NAME}.`);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Onde o aluno esta no pipeline, SEM falar com o 1Password (nao gasta a cota): estado salvo + o que da para
|
|
1427
|
+
* ver no proprio computador. `conferido=nao`: so rodar o comando principal prova que a chave funciona.
|
|
1428
|
+
*/
|
|
1429
|
+
function describeState(d) {
|
|
1430
|
+
const saved = d.state.read();
|
|
1431
|
+
const etapas = saved ? { ...saved.etapas } : emptyEtapas();
|
|
1432
|
+
const version = d.opVersion();
|
|
1433
|
+
const opOk = Boolean(version && isVersionAtLeast(version));
|
|
1434
|
+
etapas.op = opOk ? 'feito' : 'pendente';
|
|
1435
|
+
const accounts = opOk ? listAccounts(d) : [];
|
|
1436
|
+
if (accounts.length) {
|
|
1437
|
+
etapas.conta = 'feito';
|
|
1438
|
+
etapas.app = 'feito';
|
|
1439
|
+
}
|
|
1440
|
+
const hasToken = [d.env[TOKEN_VAR], d.readPersistedToken()].some((t) => t && isValidToken(t));
|
|
1441
|
+
if (!hasToken) {
|
|
1442
|
+
// chave apagada, computador novo ou arquivo de ambiente trocado: o que dependia dela volta a ficar pendente
|
|
1443
|
+
for (const e of ['chave', 'teste', 'pronto']) etapas[e] = 'pendente';
|
|
1444
|
+
} else if (!saved) {
|
|
1445
|
+
etapas.chave = 'feito'; // ha uma chave no computador (modo antigo ou outra execucao); ainda nao provada
|
|
1446
|
+
}
|
|
1447
|
+
if (etapas.chave !== 'feito' || etapas.teste !== 'feito') etapas.pronto = 'pendente';
|
|
1448
|
+
const feitas = ETAPAS.filter((e) => etapas[e] === 'feito');
|
|
1449
|
+
const proxima = nextStage(etapas);
|
|
1450
|
+
const resumo = !proxima ? 'pronto' : (feitas.length || saved ? 'em-andamento' : 'nao-iniciado');
|
|
1451
|
+
return {
|
|
1452
|
+
resumo,
|
|
1453
|
+
proxima,
|
|
1454
|
+
feitas,
|
|
1455
|
+
etapas,
|
|
1456
|
+
token: hasToken ? 'configurado' : 'ausente',
|
|
1457
|
+
ultimo: saved && saved.ultimo ? saved.ultimo.codigo : null,
|
|
1458
|
+
atualizado_em: saved ? saved.atualizado_em : null,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function buildStateLine(st) {
|
|
1463
|
+
return `AUROQ_ESTADO 1password=${st.resumo} proxima=${st.proxima || 'nenhuma'} feitas=${st.feitas.join(',') || 'nenhuma'} token=${st.token} ultimo=${st.ultimo || 'nenhum'} conferido=nao`;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/** Linha que o Ops le (mesmo padrao da manutencao). */
|
|
1467
|
+
function buildResultLine(result) {
|
|
1468
|
+
const msg = oneLine(result.mensagem, 400).replace(/"/g, "'");
|
|
1469
|
+
const contas = result.contas && result.contas.length ? ` contas="${result.contas.map((c) => oneLine(c, 120).replace(/"/g, "'")).join('|')}"` : '';
|
|
1470
|
+
const aviso = result.aviso ? ` aviso=${result.aviso}` : '';
|
|
1471
|
+
return `AUROQ_RESULTADO 1password=${result.status} codigo=${result.codigo}${aviso}${contas} mensagem="${msg}"`;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function showState(st) {
|
|
1475
|
+
console.log(`\n${PURPLE}${BOLD} 1Password — onde voce esta${RESET}`);
|
|
1476
|
+
for (const e of ETAPAS.filter((x) => x !== 'pronto')) {
|
|
1477
|
+
const feito = st.etapas[e] === 'feito';
|
|
1478
|
+
console.log(` ${feito ? `${GREEN}✓${RESET}` : `${GOLD}·${RESET}`} ${ETAPA_NOME[e]}`);
|
|
1479
|
+
}
|
|
1480
|
+
log('');
|
|
1481
|
+
if (st.resumo === 'pronto') log(`${GREEN}${BOLD}Tudo pronto.${RESET} ${DIM}(Nao conferi com o 1Password agora: rode sem --estado para provar.)${RESET}`);
|
|
1482
|
+
else log(`Proxima etapa: ${BOLD}${ETAPA_NOME[st.proxima]}${RESET}. Rode ${CYAN}npx auroq-os conectar-1password${RESET} para continuar de onde parou.`);
|
|
1483
|
+
log('');
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// ─── Entrada do comando ──────────────────────────────────
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* `auroq-os conectar-1password [--agente] [--conta X] [--rotacionar] [--esperar N] [--manual]`
|
|
1490
|
+
* Codigo de saida: 0 conectado OU falta uma acao do aluno (ligar a chave no app, escolher conta: a
|
|
1491
|
+
* linha AUROQ_RESULTADO diz qual), 1 erro.
|
|
1492
|
+
*/
|
|
1493
|
+
async function connect(options = {}) {
|
|
1494
|
+
const opts = options.argv ? parseArgs(options.argv) : { ...parseArgs([]), ...options };
|
|
1495
|
+
if (opts.manual) return connectManual();
|
|
1496
|
+
if (opts.estado) {
|
|
1497
|
+
const st = describeState({ ...makeDefaultDeps(), ...(options.deps || {}) });
|
|
1498
|
+
if (opts.agente) console.log(buildStateLine(st));
|
|
1499
|
+
else showState(st);
|
|
1500
|
+
return st;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (!opts.agente) console.log(`\n${PURPLE}${BOLD} Conectar 1Password — Auroq OS${RESET}`);
|
|
1504
|
+
let result;
|
|
1505
|
+
try {
|
|
1506
|
+
result = await connectAutomatic(opts);
|
|
1507
|
+
} catch (e) {
|
|
1508
|
+
// Excecao inesperada: o Ops sempre recebe a linha de resultado (e a mensagem nunca leva token).
|
|
1509
|
+
result = fim('erro', 'falha', `Falha inesperada: ${oneLine(e && e.message)}`);
|
|
1510
|
+
}
|
|
1511
|
+
if (result.status === 'aguardando') {
|
|
1512
|
+
warn(result.mensagem);
|
|
1513
|
+
} else if (result.status === 'erro') {
|
|
1514
|
+
err(result.mensagem);
|
|
1515
|
+
if (['sem-permissao', 'falha-criar', 'sem-cofre'].includes(result.codigo)) {
|
|
1516
|
+
log(`${DIM}Plano B (so se o automatico nao for possivel nesta conta): npx auroq-os conectar-1password --manual${RESET}`);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (opts.agente) console.log(buildResultLine(result));
|
|
1520
|
+
process.exitCode = EXIT_CODE[result.status] ?? 1;
|
|
1521
|
+
return result;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
280
1524
|
module.exports = {
|
|
281
1525
|
connect,
|
|
1526
|
+
connectAutomatic,
|
|
1527
|
+
connectManual,
|
|
282
1528
|
// exportados para testes
|
|
283
1529
|
isValidToken,
|
|
284
1530
|
upsertEnvFileContent,
|
|
1531
|
+
parseEnvFileToken,
|
|
1532
|
+
envFilePath,
|
|
1533
|
+
persistToken,
|
|
1534
|
+
readPersistedToken,
|
|
1535
|
+
parseVersion,
|
|
1536
|
+
isVersionAtLeast,
|
|
1537
|
+
redact,
|
|
1538
|
+
oneLine,
|
|
1539
|
+
extractToken,
|
|
1540
|
+
pickAccount,
|
|
1541
|
+
parseArgs,
|
|
1542
|
+
enableIntegrationSteps,
|
|
1543
|
+
buildResultLine,
|
|
1544
|
+
classifyFailure,
|
|
1545
|
+
classifyTokenFailure,
|
|
1546
|
+
isStudentActionable,
|
|
1547
|
+
describeState,
|
|
1548
|
+
buildStateLine,
|
|
1549
|
+
readState,
|
|
1550
|
+
writeState,
|
|
1551
|
+
stateFile,
|
|
1552
|
+
nextStage,
|
|
1553
|
+
envFileProblem,
|
|
1554
|
+
isTrackedByGit,
|
|
1555
|
+
hasDynamicTokenDefinition,
|
|
1556
|
+
shellKind,
|
|
1557
|
+
freshShellFor,
|
|
1558
|
+
clearClipboard,
|
|
1559
|
+
ETAPAS,
|
|
1560
|
+
makeLock,
|
|
1561
|
+
isPersonalAccount,
|
|
1562
|
+
normalizeAddress,
|
|
1563
|
+
opEnv,
|
|
1564
|
+
// reaproveitados pela rota do Infisical (lib/infisical.js) e pela camada comum (lib/cofre.js)
|
|
1565
|
+
hasCommand,
|
|
1566
|
+
parseJson,
|
|
1567
|
+
writeFileAtomic,
|
|
1568
|
+
auroqDir,
|
|
1569
|
+
openUrl,
|
|
1570
|
+
refreshWindowsPath,
|
|
1571
|
+
runOp,
|
|
1572
|
+
TOKEN_VAR,
|
|
1573
|
+
MIN_OP_VERSION,
|
|
1574
|
+
VAULT_NAME,
|
|
1575
|
+
SERVICE_ACCOUNT_NAME,
|
|
1576
|
+
BACKUP_TITLE,
|
|
1577
|
+
BACKUP_TAG,
|
|
285
1578
|
};
|